├── .github
└── workflows
│ ├── deploy-to-server.yml
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── Tools
├── HeroMaker
│ ├── HeroMaker.csproj
│ └── MonkeyDude.cs
├── config.txt
├── connect.url
├── server-example.cfg
├── start-server.bat
└── update.bat
├── WarcraftPlugin
├── Adverts
│ └── AdvertManager.cs
├── Classes
│ ├── Barbarian.cs
│ ├── DwarfEngineer.cs
│ ├── Mage.cs
│ ├── Necromancer.cs
│ ├── Paladin.cs
│ ├── Ranger.cs
│ ├── Rogue.cs
│ ├── Shadowblade.cs
│ ├── Shapeshifter.cs
│ └── Tinker.cs
├── Compiler
│ └── CustomHero.cs
├── Core
│ ├── ClassManager.cs
│ ├── CooldownManager.cs
│ ├── Database.cs
│ ├── Effects
│ │ ├── EffectManager.cs
│ │ └── WarcraftEffect.cs
│ ├── Preload
│ │ └── Particles.cs
│ └── XpSystem.cs
├── Events
│ ├── EventSystem.cs
│ └── ExtendedEvents
│ │ ├── EventExtensions.cs
│ │ ├── EventPlayerHurtOther.cs
│ │ ├── EventPlayerKilledOther.cs
│ │ ├── EventSpottedByEnemy.cs
│ │ ├── EventSpottedEnemy.cs
│ │ └── ICustomGameEvent.cs
├── Helpers
│ ├── Geometry.cs
│ ├── Memory.cs
│ ├── RayTracer.cs
│ ├── VolumeFix.cs
│ ├── Warcraft.cs
│ └── WeaponTypes.cs
├── Menu
│ ├── FontSizes.cs
│ ├── Menu.cs
│ ├── MenuAPI.cs
│ ├── MenuManager.cs
│ ├── MenuOption.cs
│ ├── MenuPlayer.cs
│ └── WarcraftMenu
│ │ ├── ClassMenu.cs
│ │ └── SkillsMenu.cs
├── Models
│ ├── DefaultClassModel.cs
│ ├── GameAction.cs
│ ├── KillFeedIcon.cs
│ ├── WarcraftClass.cs
│ └── WarcraftPlayer.cs
├── Properties
│ ├── AssemblyInfo.cs
│ └── launchSettings.json
├── Resources
│ ├── arrow-down.gif
│ └── nuget
│ │ ├── Readme.md
│ │ └── wc-icon.png
├── Summons
│ ├── Drone.cs
│ └── Zombie.cs
├── WarcraftPlugin.cs
├── WarcraftPlugin.csproj
├── WarcraftPlugin.sln
├── lang
│ ├── LocalizerMiddleware.cs
│ ├── da.json
│ ├── en.json
│ ├── ru.json
│ └── tr.json
└── packages.config
└── version.txt
/.github/workflows/deploy-to-server.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Warcraft to server
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | schedule:
8 | - cron: '0 0 * * *' # Runs daily at midnight UTC
9 |
10 | jobs:
11 |
12 | build:
13 | name: Deploy
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Setup .NET Core SDK
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: '8.x.x'
23 |
24 | - name: Publish build artifacts
25 | run: dotnet publish WarcraftPlugin/WarcraftPlugin.csproj --configuration Release --output ./publish
26 |
27 | - name: executing remote ssh commands
28 | uses: easingthemes/ssh-deploy@main
29 | with:
30 | REMOTE_HOST: ${{ secrets.HOST }}
31 | REMOTE_USER: ${{ secrets.USERNAME }}
32 | SSH_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
33 | SOURCE: "./publish/"
34 | TARGET: "/home/steam/steamcmd/cs2-ds/game/csgo/addons/counterstrikesharp/plugins/WarcraftPlugin"
35 | ARGS: "-rlgoDzvc -i"
36 | SCRIPT_AFTER: |
37 | sudo chown -R steam:steam /home/steam/steamcmd/
38 | bash -ic 'cs2 update'
39 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Release Warcraft plugin
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | release_type:
6 | description: "Type of release"
7 | required: true
8 | default: patch
9 | type: choice
10 | options:
11 | - patch
12 | - minor
13 | - major
14 | # body:
15 | # description: 'Release notes'
16 | # required: false
17 | # default: ''
18 |
19 | jobs:
20 | build:
21 | name: Build and Release
22 | runs-on: ubuntu-latest
23 | steps:
24 |
25 | - name: Checkout repository
26 | uses: actions/checkout@v4
27 |
28 | - name: Read Current Version
29 | id: read_version
30 | run: |
31 | current_version=$(cat version.txt)
32 | echo "current_version=$current_version" >> $GITHUB_ENV
33 |
34 | - name: Calculate New Version
35 | id: calculate_version
36 | run: |
37 | current_version=${{ env.current_version }}
38 | release_type=${{ github.event.inputs.release_type }}
39 |
40 | # Parse current version into major, minor, and patch
41 | major=$(echo $current_version | cut -d. -f1)
42 | minor=$(echo $current_version | cut -d. -f2)
43 | patch=$(echo $current_version | cut -d. -f3)
44 |
45 | # Increment based on release type
46 | if [ "$release_type" = "major" ]; then
47 | major=$((major + 1))
48 | minor=0
49 | patch=0
50 | elif [ "$release_type" = "minor" ]; then
51 | minor=$((minor + 1))
52 | patch=0
53 | else
54 | patch=$((patch + 1))
55 | fi
56 |
57 | # Create new version string
58 | new_version="$major.$minor.$patch"
59 | echo "new_version=$new_version" >> $GITHUB_ENV
60 |
61 | - name: Update Version File
62 | run: echo "${{ env.new_version }}" > version.txt
63 |
64 | - name: Commit Updated Version
65 | run: |
66 | git config user.name "GitHub Actions"
67 | git config user.email "actions@github.com"
68 | git add version.txt
69 | git commit -m "Release version ${{ env.new_version }}"
70 | git push
71 |
72 | - name: Setup .NET Core SDK
73 | uses: actions/setup-dotnet@v4
74 | with:
75 | dotnet-version: '8.x.x' # Use the version of .NET you need
76 |
77 | - name: Update C# Version
78 | run: |
79 | sed -i "s/public override string ModuleVersion => \".*\";/public override string ModuleVersion => \"${{ env.new_version }}\";/g" WarcraftPlugin/WarcraftPlugin.cs
80 |
81 | - name: Publish build artifacts
82 | run: dotnet publish WarcraftPlugin/WarcraftPlugin.csproj --configuration Release --output ./publish/WarcraftPlugin
83 |
84 | - name: Create CustomHeroes folder
85 | run: mkdir -p ./publish/WarcraftPlugin/CustomHeroes
86 |
87 | - name: Create zip of publish directory contents
88 | run: |
89 | cd publish
90 | zip -r ../warcraft-plugin.zip * # Zip the contents of the publish folder
91 |
92 | - name: Create Release
93 | id: create_release
94 | uses: actions/create-release@v1
95 | with:
96 | tag_name: ${{ env.new_version }}
97 | release_name: Release ${{ env.new_version }}
98 | # body: ${{ github.event.inputs.body }}
99 | draft: false
100 | prerelease: false
101 | env:
102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103 |
104 | - name: Upload Release Asset
105 | uses: actions/upload-release-asset@v1
106 | with:
107 | upload_url: ${{ steps.create_release.outputs.upload_url }}
108 | asset_path: ./warcraft-plugin.zip
109 | asset_name: warcraft-plugin-${{ env.new_version }}.zip
110 | asset_content_type: application/zip
111 | env:
112 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113 |
114 | - name: Pack NuGet package
115 | run: dotnet pack WarcraftPlugin/WarcraftPlugin.csproj --configuration Release --no-build --output nupkgs /p:Version=${{ env.new_version }}
116 |
117 | - name: Publish to NuGet
118 | run: dotnet nuget push nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
119 |
120 | # - name: Github Releases To Discord
121 | # uses: SethCohen/github-releases-to-discord@v1
122 | # with:
123 | # webhook_url: ${{ secrets.WEBHOOK_URL }}
124 | # color: "2105893"
125 | # username: "Release Changelog"
126 | # avatar_url: "https://github.com/Wngui/CS2WarcraftMod/blob/master/WarcraftPlugin/Resources/nuget/wc-icon.png?raw=true"
127 | # content: ""
128 | # footer_title: "Changelog"
129 | # footer_icon_url: "https://github.com/Wngui/CS2WarcraftMod/blob/master/WarcraftPlugin/Resources/nuget/wc-icon.png?raw=true"
130 | # footer_timestamp: true
131 | # max_description: '4096'
132 | # reduce_headings: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 | WarcraftPlugin/PostBuild.targets
400 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | # Warcraft Mod for CS2
12 |
13 | An open-source Warcraft mod for CS2 featuring a fully-fledged RPG system.
14 | Try the plugin here: [Connect](https://cs2browser.com/connect/136.244.80.208:27015)
15 |
16 | ## Features
17 |
18 | Many unique classes:
19 |
20 | - **Barbarian**
21 | - **Mage**
22 | - **Necromancer**
23 | - **Paladin**
24 | - **Ranger**
25 | - **Rogue**
26 | - **Shapeshifter**
27 | - **Tinker**
28 | - **ShadowBlade**
29 | - **More in the [Discord](https://discord.gg/VvD8aUHCNW)**!
30 |
31 | Each class has 3 passive abilities and an ultimate which is unlocked at max level 16.
32 |
33 | 
34 |
35 | Ultimate can be activated by binding it in the console, example
36 | ```
37 | bind x ultimate
38 | ```
39 |
40 | ## Commands
41 | ```!class``` - Change current class
42 |
43 | ```!skills``` - Opens skill selection menu
44 |
45 | ```!reset``` - Unassign skill points for current class
46 |
47 | ```!factoryreset``` - Completely resets progress for current class
48 |
49 | ```!addxp [player]``` - Admin only, adds x amount of xp to current class. Player name/#steamid is optional
50 |
51 | ```!commands``` - Lists all commands
52 |
53 | ## Class Abilities
54 |
55 | ### Barbarian
56 |
57 | - **Carnage**: Increase damage dealt with shotguns.
58 | - **Battle-Hardened**: Increase your health by 20/40/60/80/100.
59 | - **Exploding Barrel**: Chance to hurl an exploding barrel when firing.
60 | - **Bloodlust**: Grants infinite ammo, movement speed, and health regeneration.
61 |
62 | ### Mage
63 |
64 | - **Fireball**: Infuses molotovs with fire magic, causing a huge explosion on impact.
65 | - **Ice Beam**: Chance to freeze enemies in place.
66 | - **Mana Shield**: Passive magical shield, which regenerates armor over time.
67 | - **Teleport**: When you press your ultimate key, you will teleport to the spot you're aiming.
68 |
69 | ### Necromancer
70 |
71 | - **Life Drain**: Harness dark magic to siphon health from foes and restore your own vitality.
72 | - **Poison Cloud**: Infuses smoke grenades with potent toxins, damaging enemies over time.
73 | - **Splintered Soul**: Chance to cheat death with a fraction of vitality.
74 | - **Raise Dead**: Resurrect powerful undead minions to fight alongside you.
75 |
76 | ### Paladin
77 |
78 | - **Healing Aura**: Emit an aura that gradually heals nearby allies over time.
79 | - **Holy Shield**: Surround yourself with a protective barrier that absorbs incoming damage.
80 | - **Smite**: Infuse your attacks with divine energy, potentially stripping enemy armor.
81 | - **Divine Resurrection**: Instantly revive a random fallen ally.
82 |
83 | ### Ranger
84 |
85 | - **Light Footed**: Nimbly perform a dash in midair, by pressing jump.
86 | - **Ensnare Trap**: Place a trap by throwing a decoy.
87 | - **Marksman**: Additional damage with scoped weapons.
88 | - **Arrowstorm**: Call down a deadly volley of arrows using the ultimate key.
89 |
90 | ### Rogue
91 |
92 | - **Stealth**: Become partially invisible for 1/2/3/4/5 seconds, when killing someone.
93 | - **Sneak Attack**: When you hit an enemy in the back, you do an additional 5/10/15/20/25 damage.
94 | - **Blade Dance**: Increases movement speed and damage with knives.
95 | - **Smokebomb**: When nearing death, you will automatically drop a smokebomb, letting you cheat death.
96 |
97 | ### Shapeshifter
98 |
99 | - **Adaptive Disguise**: Chance to spawn with an enemy disguise, revealed upon attacking.
100 | - **Doppelganger**: Create a temporary inanimate clone of yourself, using a decoy grenade.
101 | - **Imposter Syndrome**: Chance to be notified when revealed by enemies on radar.
102 | - **Morphling**: Transform into an unassuming object.
103 |
104 | ### Tinker
105 | - **Attack Drone**: Deploy a drone that attacks nearby enemies.
106 | - **Spare Parts**: Chance to not lose ammo when firing
107 | - **Spring Trap**: Deploy a trap which launches players into the air.
108 | - **Drone Swarm**: Summon a swarm of attack drones that damage all nearby enemies.
109 |
110 | ## Setup
111 |
112 | **Install the Plugin**
113 | - Install [CounterStrikeSharp](https://docs.cssharp.dev/docs/guides/getting-started.html)
114 | - Download the latest [Warcraft release](https://github.com/Wngui/CS2WarcraftMod/releases/latest)
115 | - Copy the `WarcraftPlugin` folder to `counterstrikesharp -> plugins`
116 |
117 | ## Configuration example
118 | Config path: *counterstrikesharp\configs\plugins\WarcraftPlugin\WarcraftPlugin.json*
119 | ```jsonc
120 | {
121 | "ConfigVersion": 3,
122 | "DeactivatedClasses": ["Shapeshifter", "Rogue"], //Disables Shapeshifter & Rogue from the plugin
123 | "ShowCommandAdverts": true, //Enables adverts teaching new players about available commands
124 | "DefaultClass": "ranger", //Sets the default class for new players
125 | "DisableNamePrefix": true, //Removes level and class info from player names
126 | "XpPerKill": 40, // Experience per kill
127 | "XpHeadshotModifier": 0.15, // Experience Modifier for headshots
128 | "XpKnifeModifier": 0.25, // Experience Modifier for knife kills
129 | "MatchReset": true, // Reset all character progress at map start/end
130 | "TotalLevelRequired": { // Total level required to unlock class
131 | "Shadowblade": 48, // Unlocks when you have 48 levels in total
132 | "Tinker": 60 // Unlocks when you have 60 levels in total
133 | }
134 | }
135 | ```
136 |
137 | ## Credits
138 |
139 | **Roflmuffin** - CounterStrikeSharp & Base plugin
140 | **csportalsk** - Testing and bug reporting
141 | **pZyk** - Development
142 | **Poisoned** - Development
143 |
--------------------------------------------------------------------------------
/Tools/HeroMaker/HeroMaker.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | disable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Tools/HeroMaker/MonkeyDude.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Drawing;
5 | using WarcraftPlugin.Models;
6 |
7 | namespace WarcraftPlugin.Classes
8 | {
9 | public class MonkeyDude : WarcraftClass
10 | {
11 | public override string DisplayName => "MonkeyDude";
12 |
13 | public override Color DefaultColor => Color.GreenYellow;
14 |
15 | public override List Abilities =>
16 | [
17 | new WarcraftAbility("Banana Gun", "Shoots a barrage of bananas that explode on impact."),
18 | new WarcraftAbility("Monkey Agility", "Increases movement speed and evasion."),
19 | new WarcraftAbility("Primal Roar", "Emits a roar when killing an enemy that stuns nearby enemies."),
20 | new WarcraftCooldownAbility("Jungle Fury", "Temporarily increases attack speed and damage.", 60f)
21 | ];
22 |
23 | public override void Register()
24 | {
25 | HookEvent(PlayerSpawn);
26 |
27 | HookAbility(3, Ultimate);
28 | }
29 |
30 | private void PlayerSpawn(EventPlayerSpawn spawn)
31 | {
32 | Console.WriteLine("MonkeyDude has spawned!");
33 | }
34 |
35 | private void Ultimate()
36 | {
37 | Console.WriteLine("MonkeyDude used ultimate!");
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tools/config.txt:
--------------------------------------------------------------------------------
1 | STEAMCMD_PATH=C:\cs2-server\steamcmd.exe
2 | BASE_PATH=C:\cs2-server\cs2-ds-test
--------------------------------------------------------------------------------
/Tools/connect.url:
--------------------------------------------------------------------------------
1 | [{000214A0-0000-0000-C000-000000000046}]
2 | Prop3=19,0
3 | [InternetShortcut]
4 | IDList=
5 | URL=steam://connect/127.0.0.1:27015/
6 | IconIndex=170
7 | HotKey=0
8 | IconFile=C:\WINDOWS\System32\SHELL32.dll
9 |
--------------------------------------------------------------------------------
/Tools/server-example.cfg:
--------------------------------------------------------------------------------
1 | hostname "WarcraftPlugin Dev"
2 | sv_tags rpg,warcraft,modded // Server tags. Used to provide extra information to clients when theyre browsing for servers. Separate tags with a comma.
3 | rcon_password "" //Disable
4 | mp_teamname_1 "Alliance" // A non-empty string overrides the second teams name.
5 | mp_teamname_2 "Horde" // A non-empty string overrides the first teams name.
6 | bot_stop 1
7 | mp_free_armor 0
8 |
9 | //Can be removed if testing death related triggers
10 | mp_respawn_on_death_ct 1
11 | mp_respawn_on_death_t 1
12 |
13 | mp_disconnect_kills_players 1
14 | mp_autoteambalance 1
15 | mp_limitteams 0
16 | sv_visiblemaxplayers 30
17 | sv_holiday_mode 1 // 0 = OFF 1 = Halloween 2 = Winter
18 | bot_controllable 1 //Might be buggy but keeps player retention
19 | sv_lan 0 // Server is a lan server ( no heartbeat// no authentication// no non-class C addresses )
20 | mp_force_assign_teams 1
21 | mp_force_pick_time 0
22 | sv_hibernate_when_empty 0
23 | bot_join_after_player 0
24 | mp_warmuptime 0
25 | mp_warmup_end
26 | sv_warmup_to_freezetime_delay 0
27 | mp_warmup_online_enabled 0
28 | mp_autokick 0 //Auto kick off
29 | bot_autodifficulty_threshold_high 0.0 // Value between -20.0 and 20.0 (Amount above avg human contribution score, above which a bot should lower its difficulty)
30 | bot_autodifficulty_threshold_low -2.0 // Value between -20.0 and 20.0 (Amount below avg human contribution score, below which a bot should raise its difficulty)
31 | bot_chatter off
32 | bot_defer_to_human_goals 0
33 | bot_defer_to_human_items 1
34 | bot_difficulty 2
35 | bot_quota 10
36 | bot_quota_mode fill
37 | cash_player_bomb_defused 200
38 | cash_player_bomb_planted 200
39 | cash_player_damage_hostage -30
40 | cash_player_interact_with_hostage 300
41 | cash_player_killed_enemy_default 200
42 | cash_player_killed_enemy_factor 0.5
43 | cash_player_killed_hostage -1000
44 | cash_player_killed_teammate -300
45 | cash_player_rescued_hostage 1000
46 | cash_team_elimination_bomb_map 2700
47 | cash_team_elimination_hostage_map_t 2000
48 | cash_team_elimination_hostage_map_ct 2300
49 | cash_team_hostage_alive 0
50 | cash_team_hostage_interaction 500
51 | cash_team_loser_bonus 2400
52 | cash_team_bonus_shorthanded 0
53 | cash_team_loser_bonus_consecutive_rounds 0
54 | cash_team_planted_bomb_but_defused 200
55 | cash_team_rescued_hostage 0
56 | cash_team_terrorist_win_bomb 2700
57 | cash_team_win_by_defusing_bomb 2700
58 | cash_team_win_by_hostage_rescue 3000
59 | cash_team_win_by_time_running_out_hostage 2000
60 | cash_team_win_by_time_running_out_bomb 2700
61 | ff_damage_reduction_bullets 0.5
62 | ff_damage_reduction_grenade 0.5
63 | ff_damage_reduction_grenade_self 0.5
64 | ff_damage_reduction_other 0.5
65 | mp_afterroundmoney 1000
66 | mp_buytime 100
67 | mp_buy_anywhere 0
68 | mp_buy_during_immunity 0
69 | mp_death_drop_defuser 1
70 | mp_death_drop_grenade 2 // 0=none, 1=best, 2=current or best
71 | mp_death_drop_gun 1 // 0=none, 1=best, 2=current or best
72 | mp_fists_replace_melee 1
73 | mp_defuser_allocation 2 // 0=none, 1=random, 2=everyone
74 | mp_force_pick_time 10
75 | mp_forcecamera 0 // Set to 1 for team only spectating.
76 | mp_free_armor 0
77 | mp_freezetime 0
78 | mp_friendlyfire 1
79 | mp_win_panel_display_time 1
80 | mp_respawn_immunitytime 0
81 | mp_halftime 0
82 | mp_match_can_clinch 0 // 0=No mercy rule, 1=team can clinch match win early if they win > 1/2 total rounds
83 | mp_maxmoney 11337
84 | mp_maxrounds 50
85 | mp_match_end_restart_delay 0
86 | //mp_molotovusedelay 0
87 | mp_playercashawards 1
88 | mp_round_restart_delay 5
89 | mp_roundtime 1.60
90 | mp_roundtime_hostage 1.60
91 | mp_roundtime_defuse 1.60 // Typical ValveOfficial Casual defuse rounds are 90-100 seconds.
92 | mp_solid_teammates 0
93 | mp_startmoney 999999
94 | mp_teamcashawards 1
95 | mp_timelimit 0
96 | mp_weapons_allow_zeus 2
97 | mp_weapons_allow_typecount 2
98 | //spec_freeze_panel_extended_time 0
99 | spec_freeze_time 3.0
100 | sv_allow_votes 1 // Voting allowed in this mode
101 | sv_talk_enemy_living 0
102 | sv_talk_enemy_dead 1
103 | sv_vote_to_changelevel_before_match_point 1
104 | sv_deadtalk 0
105 | sv_ignoregrenaderadio 0
106 | tv_delay 30
107 | mp_warmup_pausetimer 0
108 | mp_halftime_pausetimer 0
109 | mp_randomspawn 0
110 | mp_randomspawn_los 0
111 | sv_infinite_ammo 0
112 | ammo_grenade_limit_flashbang 1
113 | ammo_grenade_limit_total 3
114 | //mp_match_end_changelevel 1 // At the end of the match perform a changelevel even if next map is the same
115 | mp_weapons_allow_map_placed 1
116 | mp_weapons_glow_on_ground 0
117 | mp_display_kill_assists 1
118 | mp_ct_default_melee weapon_knife
119 | mp_ct_default_secondary weapon_hkp2000
120 | mp_ct_default_primary ""
121 | mp_t_default_melee weapon_knife
122 | mp_t_default_secondary weapon_glock
123 | mp_t_default_primary ""
124 | mp_default_team_winner_no_objective -1 // 2 == CTs, 3 == Ts
125 |
126 | spec_replay_enable 1
127 | mp_round_restart_delay 4 // need more time for replay
128 |
129 | sv_gameinstructor_enable 0
130 | mp_promoted_item_enabled 0
131 | sv_minimum_desired_chicken_count 0
132 |
--------------------------------------------------------------------------------
/Tools/start-server.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | :: Read paths from config.txt
3 | for /f "tokens=1,* delims==" %%A in (config.txt) do set "%%A=%%B"
4 |
5 | :: Construct the full path to cs2.exe
6 | set "CS2_EXE=%BASE_PATH%\game\bin\win64\cs2.exe"
7 |
8 | :: Run the command
9 | start "" "%CS2_EXE%" -dedicated -autoupdate -maxplayers 64 -tickrate 128 +game_mode 0 +game_type 3 +map de_dust2
10 |
--------------------------------------------------------------------------------
/Tools/update.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | :: Read paths from config.txt
3 | for /f "tokens=1,* delims==" %%A in (config.txt) do set "%%A=%%B"
4 |
5 | :: Display variables (for testing)
6 | echo STEAMCMD_PATH=%STEAMCMD_PATH%
7 | echo BASE_PATH=%BASE_PATH%
8 |
9 | set "CSGO_PATH=%BASE_PATH%\game\csgo"
10 | set "CFG_PATH=%CSGO_PATH%\cfg"
11 | set "DOWNLOAD_DIR=%BASE_PATH%\downloads"
12 |
13 | :: Specific file paths
14 | set "SERVER_CFG_PATH=%CFG_PATH%\server.cfg"
15 | set "SERVER_BACKUP_PATH=%CFG_PATH%\server_backup.cfg"
16 | set "GAMEINFO_FILE=%CSGO_PATH%\gameinfo.gi"
17 |
18 | :: Backup server.cfg
19 | echo Backing up server.cfg...
20 | copy /Y "%SERVER_CFG_PATH%" "%SERVER_BACKUP_PATH%"
21 |
22 | :: Update CS2
23 | echo Updating CS2...
24 | "%STEAMCMD_PATH%" +login anonymous +force_install_dir "%BASE_PATH%" +app_update 730 validate +quit
25 |
26 | :: Add metamod line to gameinfo.gi
27 | powershell -Command "(Get-Content $env:GAMEINFO_FILE) | Where-Object { $_ -notmatch \".*csgo/addons/metamod.*\" } | Set-Content $env:GAMEINFO_FILE"
28 | powershell -Command "(Get-Content $env:GAMEINFO_FILE) -replace \"(Game_LowViolence.*)\", \"`$1`r`n`t`t`tGame`tcsgo/addons/metamod\" | Set-Content $env:GAMEINFO_FILE"
29 |
30 | :: Create download folder if does not exist
31 | if not exist "%DOWNLOAD_DIR%" mkdir "%DOWNLOAD_DIR%"
32 |
33 | :: Update Metamod
34 | echo Updating metamod...
35 | for /f "delims=" %%A in ('curl -s https://mms.alliedmods.net/mmsdrop/2.0/mmsource-latest-windows') do set "latestDownload=%%A"
36 | curl -o "%DOWNLOAD_DIR%\%latestDownload%" "https://mms.alliedmods.net/mmsdrop/2.0/%latestDownload%"
37 | echo Installing metamod version %latestDownload%...
38 | tar -xf "%DOWNLOAD_DIR%\%latestDownload%" -C "%CSGO_PATH%"
39 | del "%DOWNLOAD_DIR%\%latestDownload%"
40 |
41 | :: Update CounterStrikeSharp
42 | echo Updating CounterStrikeSharp...
43 | set "latestDownload="
44 | for /f "delims=" %%i in ('curl -s https://api.github.com/repos/roflmuffin/CounterStrikeSharp/releases/latest ^| findstr "browser_download_url" ^| findstr "with-runtime-windows"') do set "latestDownload=%%i"
45 | set "latestDownload=%latestDownload:*: =%"
46 | echo Downloading CounterStrikeSharp: %latestDownload%
47 | set "zipFile=counterstrikesharp"
48 | curl -L -o "%DOWNLOAD_DIR%\%zipFile%" "%latestDownload%"
49 | echo Installing CounterStrikeSharp version %latestDownload%...
50 | tar -xf "%DOWNLOAD_DIR%\%zipFile%" -C "%CSGO_PATH%"
51 | del "%DOWNLOAD_DIR%\%zipFile%"
52 |
53 | :: Restore server.cfg
54 | echo Restoring server.cfg...
55 | copy /Y "%SERVER_BACKUP_PATH%" "%SERVER_CFG_PATH%"
56 |
57 | :: Updating Warcraft
58 | echo Updating Warcraft...
59 | set "latestDownload="
60 | for /f "delims=" %%i in ('curl -s https://api.github.com/repos/Wngui/CS2WarcraftMod/releases/latest ^| findstr "browser_download_url" ^| findstr "warcraft-plugin-.* "') do set "latestDownload=%%i"
61 | set "latestDownload=%latestDownload:*: =%"
62 | echo Downloading Warcraft: %latestDownload%
63 | set "zipFile=warcraft"
64 | curl -L -o "%DOWNLOAD_DIR%\%zipFile%" "%latestDownload%"
65 | echo Installing Warcraft version %latestDownload%...
66 | tar -xf "%DOWNLOAD_DIR%\%zipFile%" -C "%CSGO_PATH%\addons\counterstrikesharp\plugins"
67 | del "%DOWNLOAD_DIR%\%zipFile%"
68 |
69 | echo Done!
70 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Adverts/AdvertManager.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API;
2 | using CounterStrikeSharp.API.Modules.Timers;
3 | using Microsoft.Extensions.Localization;
4 | using System.Linq;
5 |
6 | namespace WarcraftPlugin.Adverts
7 | {
8 | internal class AdvertManager
9 | {
10 | private readonly float _interval = 180f;
11 | private readonly int _advertCount = WarcraftPlugin.Instance.Localizer.GetAllStrings().Count(x => x.Name.Contains("advert."));
12 | private int _advertIndex = 0;
13 |
14 | internal void Initialize()
15 | {
16 | WarcraftPlugin.Instance.AddTimer(_interval, AdvertTick, TimerFlags.REPEAT);
17 | }
18 |
19 | private void AdvertTick()
20 | {
21 | foreach (var player in Utilities.GetPlayers())
22 | {
23 | if (!player.IsValid || player.IsBot) continue;
24 | player.PrintToChat($" {WarcraftPlugin.Instance.Localizer[$"advert.{_advertIndex}"]}");
25 | }
26 |
27 | _advertIndex++;
28 |
29 | if(_advertIndex >= _advertCount) _advertIndex = 0;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Classes/Barbarian.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Utils;
3 | using CounterStrikeSharp.API;
4 | using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
5 | using CounterStrikeSharp.API.Modules.Memory;
6 | using WarcraftPlugin.Helpers;
7 | using WarcraftPlugin.Models;
8 | using System.Drawing;
9 | using WarcraftPlugin.Core.Effects;
10 | using System.Collections.Generic;
11 | using WarcraftPlugin.Events.ExtendedEvents;
12 |
13 | namespace WarcraftPlugin.Classes
14 | {
15 | internal class Barbarian : WarcraftClass
16 | {
17 | public override string DisplayName => "Barbarian";
18 | public override DefaultClassModel DefaultModel => new()
19 | {
20 | TModel = "characters/models/tm_phoenix_heavy/tm_phoenix_heavy.vmdl",
21 | CTModel = "characters/models/ctm_heavy/ctm_heavy.vmdl"
22 | };
23 |
24 | public override List PreloadResources =>
25 | [
26 | "models/cs_italy/props/barrel/italy_barrel_wood_1.vmdl"
27 | ];
28 |
29 | public override Color DefaultColor => Color.Brown;
30 |
31 | public override List Abilities =>
32 | [
33 | new WarcraftAbility("Carnage", "Increase damage dealt with shotguns."),
34 | new WarcraftAbility("Battle-Hardened", "Increase your health by 20/40/60/80/100."),
35 | new WarcraftAbility("Throwing Axe", "Chance to throw an exploding barrel when firing."),
36 | new WarcraftCooldownAbility("Bloodlust", "Grants infinite ammo, movement speed & health regeneration.", 50f)
37 | ];
38 |
39 | private readonly int _battleHardenedHealthMultiplier = 20;
40 | private readonly float _bloodlustLength = 10;
41 |
42 | public override void Register()
43 | {
44 | HookEvent(PlayerHurtOther);
45 | HookEvent(PlayerSpawn);
46 | HookEvent(PlayerShoot);
47 |
48 | HookAbility(3, Ultimate);
49 | }
50 |
51 | private void PlayerShoot(EventWeaponFire @event)
52 | {
53 | var activeWeapon = Player.PlayerPawn.Value.WeaponServices?.ActiveWeapon.Value;
54 | if (activeWeapon != null && activeWeapon.IsValid)
55 | {
56 | var maxClip = activeWeapon.VData.MaxClip1;
57 | if (maxClip == 0) return;
58 |
59 | var maxChance = 400 / maxClip; // The bigger the mag, the lower the chance, to avoid negev spam
60 |
61 | if (Warcraft.RollDice(WarcraftPlayer.GetAbilityLevel(2), maxChance))
62 | {
63 | new ThrowingAxeEffect(Player, 2).Start();
64 | }
65 | }
66 | }
67 |
68 | private void PlayerSpawn(EventPlayerSpawn @event)
69 | {
70 | if (WarcraftPlayer.GetAbilityLevel(1) > 0)
71 | {
72 | Server.NextFrame(() =>
73 | {
74 | Player.SetHp(100 + WarcraftPlayer.GetAbilityLevel(1) * _battleHardenedHealthMultiplier);
75 | Player.PlayerPawn.Value.MaxHealth = Player.PlayerPawn.Value.Health;
76 | });
77 | }
78 | }
79 |
80 | private void Ultimate()
81 | {
82 | if (WarcraftPlayer.GetAbilityLevel(3) < 1 || !IsAbilityReady(3)) return;
83 |
84 | new BloodlustEffect(Player, _bloodlustLength).Start();
85 | StartCooldown(3);
86 | }
87 |
88 | private void PlayerHurtOther(EventPlayerHurtOther @event)
89 | {
90 | if (!@event.Userid.IsAlive() || @event.Userid.UserId == Player.UserId) return;
91 |
92 | var carnageLevel = WarcraftPlayer.GetAbilityLevel(0);
93 |
94 | if (carnageLevel > 0 && WeaponTypes.Shotguns.Contains(@event.Weapon))
95 | {
96 | var victim = @event.Userid;
97 | @event.AddBonusDamage(carnageLevel * 5);
98 | Warcraft.SpawnParticle(victim.PlayerPawn.Value.AbsOrigin.With(z: victim.PlayerPawn.Value.AbsOrigin.Z + 60), "particles/blood_impact/blood_impact_basic.vpcf");
99 | victim.EmitSound("Flesh.ImpactHard", volume:0.5f);
100 | }
101 | }
102 | }
103 |
104 | internal class ThrowingAxeEffect(CCSPlayerController owner, float duration) : WarcraftEffect(owner, duration)
105 | {
106 | private CHEGrenadeProjectile _throwingAxe;
107 |
108 | public override void OnStart()
109 | {
110 | _throwingAxe = Utilities.CreateEntityByName("hegrenade_projectile");
111 |
112 | Vector velocity = Owner.CalculateVelocityAwayFromPlayer(1800);
113 |
114 | var rotation = new QAngle(0, Owner.PlayerPawn.Value.EyeAngles.Y + 90, 0);
115 |
116 | _throwingAxe.Teleport(Owner.CalculatePositionInFront(10, 60), rotation, velocity);
117 | _throwingAxe.DispatchSpawn();
118 | _throwingAxe.SetModel("models/cs_italy/props/barrel/italy_barrel_wood_1.vmdl");
119 | Schema.SetSchemaValue(_throwingAxe.Handle, "CBaseGrenade", "m_hThrower", Owner.PlayerPawn.Raw); //Fixes killfeed
120 |
121 | _throwingAxe.AcceptInput("InitializeSpawnFromWorld");
122 | _throwingAxe.Damage = 40;
123 | _throwingAxe.DmgRadius = 180;
124 | _throwingAxe.DetonateTime = float.MaxValue;
125 |
126 | Owner.EmitSound("Door.wood_full_open", volume: 0.5f);
127 | }
128 |
129 | public override void OnTick()
130 | {
131 | if (!_throwingAxe.IsValid) return;
132 | var hasHitPlayer = _throwingAxe?.HasEverHitEnemy ?? false;
133 | if (hasHitPlayer)
134 | {
135 | try
136 | {
137 | _throwingAxe.DetonateTime = 0;
138 | }
139 | catch { }
140 | }
141 | }
142 |
143 | public override void OnFinish()
144 | {
145 | if (_throwingAxe.IsValid)
146 | {
147 | _throwingAxe.DetonateTime = 0;
148 | WarcraftPlugin.Instance.AddTimer(1, () => _throwingAxe?.RemoveIfValid());
149 | }
150 | }
151 | }
152 |
153 | internal class BloodlustEffect(CCSPlayerController owner, float duration) : WarcraftEffect(owner, duration)
154 | {
155 | private const float _maxSize = 1.1f;
156 |
157 | public override void OnStart()
158 | {
159 | Owner.AdrenalineSurgeEffect(Duration);
160 | Owner.PlayerPawn.Value.VelocityModifier = 1.3f;
161 | Owner.PlayerPawn.Value.SetColor(Color.IndianRed);
162 | Owner.EmitSound("BaseGrenade.JumpThrowM", volume: 0.5f);
163 | }
164 |
165 | public override void OnTick()
166 | {
167 | if (!Owner.IsAlive()) return;
168 |
169 | //Refill ammo
170 | Owner.PlayerPawn.Value.WeaponServices.ActiveWeapon.Value.Clip1 = Owner.PlayerPawn.Value.WeaponServices.ActiveWeapon.Value.GetVData().MaxClip1;
171 |
172 | //Regenerate health
173 | if (Owner.PlayerPawn.Value.Health < Owner.PlayerPawn.Value.MaxHealth)
174 | {
175 | Owner.SetHp(Owner.PlayerPawn.Value.Health + 1);
176 | }
177 |
178 | //Rage growth spurt
179 | var scale = Owner.PlayerPawn.Value.CBodyComponent.SceneNode.GetSkeletonInstance().Scale;
180 | if (scale < _maxSize)
181 | {
182 | Owner.PlayerPawn.Value.SetScale(scale + 0.01f);
183 | }
184 | }
185 |
186 | public override void OnFinish()
187 | {
188 | if (!Owner.IsAlive()) return;
189 |
190 | var pawn = Owner.PlayerPawn.Value;
191 | pawn.SetColor(Color.White);
192 | pawn.VelocityModifier = 1f;
193 | pawn.SetScale(1);
194 | }
195 | }
196 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Classes/Mage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 | using CounterStrikeSharp.API.Core;
4 | using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
5 | using CounterStrikeSharp.API;
6 | using WarcraftPlugin.Helpers;
7 | using WarcraftPlugin.Models;
8 | using System.Linq;
9 | using WarcraftPlugin.Core.Effects;
10 | using System.Collections.Generic;
11 | using WarcraftPlugin.Events.ExtendedEvents;
12 |
13 | namespace WarcraftPlugin.Classes
14 | {
15 | internal class Mage : WarcraftClass
16 | {
17 | public override string DisplayName => "Mage";
18 | public override Color DefaultColor => Color.Blue;
19 |
20 | public override List PreloadResources =>
21 | [
22 | "models/weapons/w_muzzlefireshape.vmdl",
23 | "models/anubis/structures/pillar02_base01.vmdl"
24 | ];
25 |
26 | public override List Abilities =>
27 | [
28 | new WarcraftAbility("Fireball", "Infuses molotovs with fire magic, causing a huge explosion on impact."),
29 | new WarcraftAbility("Ice Beam", "Chance to freeze enemies in place."),
30 | new WarcraftAbility("Mana Shield", "Passive magical shield, which regenerates armor over time."),
31 | new WarcraftCooldownAbility("Teleport", "When you press your ultimate key, you will teleport to the spot you're aiming.", 20f)
32 | ];
33 |
34 | public override void Register()
35 | {
36 | HookEvent(PlayerHurtOther);
37 | HookEvent(MolotovDetonate);
38 | HookEvent(PlayerSpawn);
39 | HookEvent(PlayerPing);
40 | HookEvent(GrenadeThrown);
41 |
42 | HookAbility(3, Ultimate);
43 | }
44 |
45 | private void PlayerPing(EventPlayerPing ping)
46 | {
47 | //Teleport ultimate
48 | if (WarcraftPlayer.GetAbilityLevel(3) > 0 && IsAbilityReady(3))
49 | {
50 | StartCooldown(3);
51 | Player.DropWeaponByDesignerName("weapon_c4");
52 | //To avoid getting stuck we offset towards the players original pos
53 | var offset = 40;
54 | var playerOrigin = Player.PlayerPawn.Value.AbsOrigin;
55 | float deltaX = playerOrigin.X - ping.X;
56 | float deltaY = playerOrigin.Y - ping.Y;
57 | float deltaZ = playerOrigin.Z - ping.Z;
58 | float distance = (float)Math.Sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ);
59 | float newX = ping.X + deltaX / distance * offset;
60 | float newY = ping.Y + deltaY / distance * offset;
61 | float newZ = ping.Z + deltaZ / distance * offset;
62 |
63 | Player.EmitSound("UIPanorama.equip_musicKit", volume: 0.5f);
64 | Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 20), "particles/ui/ui_electric_exp_glow.vpcf", 3);
65 | Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin, "particles/explosions_fx/explosion_smokegrenade_distort.vpcf", 2);
66 | Player.PlayerPawn.Value.Teleport(new Vector(newX, newY, newZ), Player.PlayerPawn.Value.AbsRotation, new Vector());
67 | Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 20), "particles/ui/ui_electric_exp_glow.vpcf", 3);
68 | Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin, "particles/explosions_fx/explosion_smokegrenade_distort.vpcf", 2);
69 | }
70 | }
71 |
72 | private void PlayerSpawn(EventPlayerSpawn @event)
73 | {
74 | //Mana shield
75 | if (WarcraftPlayer.GetAbilityLevel(2) > 0)
76 | {
77 | var regenArmorRate = 5 / WarcraftPlayer.GetAbilityLevel(2);
78 | new ManaShieldEffect(Player, regenArmorRate).Start();
79 | }
80 |
81 | //Fireball
82 | if (WarcraftPlayer.GetAbilityLevel(0) > 0)
83 | {
84 | var decoy = new CDecoyGrenade(Player.GiveNamedItem("weapon_molotov"));
85 | decoy.AttributeManager.Item.CustomName = "Fireball";
86 | }
87 | }
88 |
89 | private void Ultimate()
90 | {
91 | if (WarcraftPlayer.GetAbilityLevel(3) < 1 || !IsAbilityReady(3)) return;
92 |
93 | // Hack to get players aim point in the world, see player ping event
94 | // TODO replace with ray-tracing
95 | Player.ExecuteClientCommandFromServer("player_ping");
96 | }
97 |
98 | private void PlayerHurtOther(EventPlayerHurtOther @event)
99 | {
100 | if (!@event.Userid.IsAlive() || @event.Userid.UserId == Player.UserId) return;
101 |
102 | if (Warcraft.RollDice(WarcraftPlayer.GetAbilityLevel(1), 25))
103 | {
104 | var victim = @event.Userid;
105 | new FreezeEffect(Player, 1.0f, victim).Start();
106 | }
107 | }
108 |
109 | private void GrenadeThrown(EventGrenadeThrown thrown)
110 | {
111 | if (WarcraftPlayer.GetAbilityLevel(0) > 0 && thrown.Weapon == "molotov")
112 | {
113 | var molotov = Utilities.FindAllEntitiesByDesignerName("molotov_projectile")
114 | .Where(x => x.Thrower.Index == Player.PlayerPawn.Index)
115 | .OrderByDescending(x => x.CreateTime).FirstOrDefault();
116 |
117 | if (molotov == null) return;
118 |
119 | molotov.SetModel("models/weapons/w_muzzlefireshape.vmdl");
120 | molotov.SetColor(Color.OrangeRed);
121 |
122 | var particle = Warcraft.SpawnParticle(molotov.AbsOrigin, "particles/inferno_fx/molotov_fire01.vpcf");
123 | particle.SetParent(molotov);
124 |
125 | Vector velocity = Player.CalculateVelocityAwayFromPlayer(1800);
126 | molotov.Teleport(Player.CalculatePositionInFront(10, 60), molotov.AbsRotation, velocity);
127 |
128 | }
129 | }
130 |
131 | private void MolotovDetonate(EventMolotovDetonate @event)
132 | {
133 | var damage = WarcraftPlayer.GetAbilityLevel(0) * 40f;
134 | var radius = WarcraftPlayer.GetAbilityLevel(0) * 100f;
135 | Warcraft.SpawnExplosion(new Vector(@event.X, @event.Y, @event.Z), damage, radius, Player);
136 | Warcraft.SpawnParticle(new Vector(@event.X, @event.Y, @event.Z), "particles/survival_fx/gas_cannister_impact.vpcf");
137 | }
138 | }
139 |
140 | internal class ManaShieldEffect(CCSPlayerController owner, float onTickInterval) : WarcraftEffect(owner, onTickInterval: onTickInterval)
141 | {
142 | public override void OnStart()
143 | {
144 | if (Owner.PlayerPawn.Value.ArmorValue == 0)
145 | {
146 | Owner.GiveNamedItem("item_assaultsuit");
147 | Owner.SetArmor(1);
148 | }
149 | }
150 | public override void OnTick()
151 | {
152 | if (Owner.PlayerPawn.Value.ArmorValue < 100)
153 | {
154 | Owner.SetArmor(Owner.PlayerPawn.Value.ArmorValue + 1);
155 | }
156 | }
157 | public override void OnFinish() { }
158 | }
159 |
160 | internal class FreezeEffect(CCSPlayerController owner, float duration, CCSPlayerController target) : WarcraftEffect(owner, duration)
161 | {
162 | public override void OnStart()
163 | {
164 | target.PrintToChat(" " + Localizer["mage.frozen"]);
165 | var targetPlayerModel = target.PlayerPawn.Value;
166 |
167 | targetPlayerModel.VelocityModifier = targetPlayerModel.VelocityModifier / 2;
168 |
169 | Warcraft.DrawLaserBetween(Owner.EyePosition(-10), target.EyePosition(-10), Color.Cyan);
170 | targetPlayerModel.SetColor(Color.Cyan);
171 | }
172 | public override void OnTick() { }
173 | public override void OnFinish()
174 | {
175 | target.PlayerPawn.Value.SetColor(Color.White);
176 | }
177 | }
178 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Classes/Necromancer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 | using CounterStrikeSharp.API.Core;
4 | using CounterStrikeSharp.API.Modules.Utils;
5 | using WarcraftPlugin.Helpers;
6 | using CounterStrikeSharp.API;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using g3;
10 | using WarcraftPlugin.Models;
11 | using WarcraftPlugin.Core.Effects;
12 | using CounterStrikeSharp.API.Modules.Timers;
13 | using WarcraftPlugin.Summons;
14 | using WarcraftPlugin.Events.ExtendedEvents;
15 |
16 | namespace WarcraftPlugin.Classes
17 | {
18 | internal class Necromancer : WarcraftClass
19 | {
20 | public override string DisplayName => "Necromancer";
21 | public override Color DefaultColor => Color.Black;
22 |
23 | public override List Abilities =>
24 | [
25 | new WarcraftAbility("Life Drain", "Harness dark magic to siphon health from foes and restore your own vitality."),
26 | new WarcraftAbility("Poison Cloud", "Infuses smoke grenades with potent toxins, damaging enemies over time."),
27 | new WarcraftAbility("Splintered Soul", "Chance to cheat death with a fraction of vitality."),
28 | new WarcraftCooldownAbility("Raise Dead", "Summon a horde of undead chicken to fight for you.", 50f)
29 | ];
30 |
31 | private readonly List _zombies = new();
32 | private const int _maxZombies = 10;
33 | private bool _hasCheatedDeath = true;
34 | private Timer _zombieUpdateTimer;
35 |
36 | public override void Register()
37 | {
38 | HookEvent(PlayerSpawn);
39 | HookEvent(RoundEnd);
40 | HookEvent(RoundStart);
41 | HookEvent(PlayerDeath);
42 | HookEvent(PlayerHurtOther);
43 | HookEvent(GrenadeThrown);
44 | HookEvent(SmokegrenadeDetonate);
45 | HookEvent(SpottedPlayer);
46 | HookAbility(3, Ultimate);
47 | }
48 |
49 | private void PlayerSpawn(EventPlayerSpawn spawn)
50 | {
51 | if (WarcraftPlayer.GetAbilityLevel(1) > 0)
52 | {
53 | var decoy = new CDecoyGrenade(Player.GiveNamedItem("weapon_smokegrenade"));
54 | decoy.AttributeManager.Item.CustomName = "Posion cloud";
55 | }
56 | }
57 |
58 | private void PlayerDeath(EventPlayerDeath death)
59 | {
60 | KillZombies();
61 | if (WarcraftPlayer.GetAbilityLevel(2) == 0) return;
62 |
63 | var pawn = Player.PlayerPawn.Value;
64 | if (!_hasCheatedDeath && pawn.Health <= 0)
65 | {
66 | double rolledValue = Random.Shared.NextDouble();
67 | float chanceToRespawn = WarcraftPlayer.GetAbilityLevel(2) / 5 * 0.80f;
68 |
69 | if (Warcraft.RollDice(WarcraftPlayer.GetAbilityLevel(2), 80))
70 | {
71 | _hasCheatedDeath = true;
72 | WarcraftPlugin.Instance.AddTimer(2f, () =>
73 | {
74 | Player.PrintToChat(" " + Localizer["necromancer.cheatdeath"]);
75 | Player.Respawn();
76 | Player.SetHp(1);
77 | Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin, "particles/explosions_fx/explosion_smokegrenade_init.vpcf", 2);
78 | Player.EmitSound("Player.BecomeGhost", volume: 0.5f);
79 | });
80 | }
81 | }
82 | }
83 |
84 | private void SmokegrenadeDetonate(EventSmokegrenadeDetonate detonate)
85 | {
86 | if (WarcraftPlayer.GetAbilityLevel(1) > 0)
87 | {
88 | new PoisonCloudEffect(Player, 13, new Vector(detonate.X, detonate.Y, detonate.Z)).Start();
89 | }
90 | }
91 |
92 | private void GrenadeThrown(EventGrenadeThrown thrown)
93 | {
94 | if (WarcraftPlayer.GetAbilityLevel(1) > 0 && thrown.Weapon == "smokegrenade")
95 | {
96 | var smokeGrenade = Utilities.FindAllEntitiesByDesignerName("smokegrenade_projectile")
97 | .Where(x => x.Thrower.Index == Player.PlayerPawn.Index)
98 | .OrderByDescending(x => x.CreateTime).FirstOrDefault();
99 |
100 | if (smokeGrenade == null) return;
101 |
102 | var smokeColor = Color.FromArgb(100 - (int)((float)WarcraftPlayer.GetAbilityLevel(1) * (100 / 5)), 255, 0); //slight red shift
103 | smokeGrenade.SmokeColor.X = smokeColor.R;
104 | smokeGrenade.SmokeColor.Y = smokeColor.G;
105 | smokeGrenade.SmokeColor.Z = smokeColor.B;
106 | }
107 | }
108 |
109 | private void PlayerHurtOther(EventPlayerHurtOther hurt)
110 | {
111 | if (!hurt.Userid.IsAlive() || hurt.Userid.UserId == Player.UserId) return;
112 |
113 | if (Player.PlayerPawn.Value.Health < Player.PlayerPawn.Value.MaxHealth)
114 | {
115 | Warcraft.SpawnParticle(hurt.Userid.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 30), "particles/critters/chicken/chicken_impact_burst_zombie.vpcf");
116 | var healthDrained = hurt.DmgHealth * ((float)WarcraftPlayer.GetAbilityLevel(0) / WarcraftPlugin.MaxSkillLevel * 0.3f);
117 | var playerCalculatedHealth = Player.PlayerPawn.Value.Health + healthDrained;
118 | Player.SetHp((int)Math.Min(playerCalculatedHealth, Player.PlayerPawn.Value.MaxHealth));
119 | }
120 | }
121 |
122 | private void RoundStart(EventRoundStart start)
123 | {
124 | KillZombies();
125 | _hasCheatedDeath = false;
126 | }
127 |
128 | private void RoundEnd(EventRoundEnd end)
129 | {
130 | KillZombies();
131 | }
132 |
133 | private void KillZombies()
134 | {
135 | _zombieUpdateTimer?.Kill();
136 |
137 | foreach (var zombie in _zombies)
138 | {
139 | zombie.Kill();
140 | }
141 |
142 | _zombies.Clear();
143 | }
144 |
145 | private void Ultimate()
146 | {
147 | if (WarcraftPlayer.GetAbilityLevel(3) < 1 || !IsAbilityReady(3)) return;
148 |
149 | RaiseDead();
150 | StartCooldown(3);
151 | }
152 |
153 | private void RaiseDead()
154 | {
155 | Player.EmitSound("Player.BecomeGhost", volume: 0.5f);
156 |
157 | for (int i = 0; i < _maxZombies; i++)
158 | {
159 | _zombies.Add(new Zombie(Player));
160 | }
161 |
162 | _zombieUpdateTimer?.Kill();
163 |
164 | _zombieUpdateTimer = WarcraftPlugin.Instance.AddTimer(0.1f, () =>
165 | {
166 | var hasValidZombies = false;
167 | var zombieCount = _zombies.Count;
168 | var zombieIndex = 0;
169 | foreach (var zombie in _zombies)
170 | {
171 | if (zombie.Entity.IsValid)
172 | {
173 | zombieIndex++;
174 | zombie.FavouritePosition = (zombieIndex * 100) / zombieCount;
175 | zombie.Update();
176 | hasValidZombies = true;
177 | }
178 | }
179 |
180 | if (!hasValidZombies)
181 | {
182 | _zombieUpdateTimer?.Kill();
183 | _zombies.Clear();
184 | }
185 | }, TimerFlags.REPEAT);
186 | }
187 |
188 | private void SpottedPlayer(EventSpottedEnemy enemy)
189 | {
190 | foreach (var zombie in _zombies)
191 | {
192 | if (zombie.Entity.IsValid)
193 | zombie.SetEnemy(enemy.UserId);
194 | }
195 | }
196 |
197 | internal class PoisonCloudEffect(CCSPlayerController owner, float duration, Vector cloudPos) : WarcraftEffect(owner, duration)
198 | {
199 | readonly int _cloudHeight = 100;
200 | readonly int _cloudWidth = 260;
201 | private Box3d _hurtBox;
202 |
203 | public override void OnStart()
204 | {
205 | var hurtBoxPoint = cloudPos.With(z: cloudPos.Z + _cloudHeight / 2);
206 | _hurtBox = Warcraft.CreateBoxAroundPoint(hurtBoxPoint, _cloudWidth, _cloudWidth, _cloudHeight);
207 | //_hurtBox.Show(duration: Duration); //Debug
208 | }
209 |
210 | public override void OnTick()
211 | {
212 | //Find players within area
213 | var players = Utilities.GetPlayers();
214 | var playersInHurtZone = players.Where(x => x.PawnIsAlive && !x.AllyOf(Owner) && _hurtBox.Contains(x.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 20)));
215 | //small hurt
216 | if (playersInHurtZone.Any())
217 | {
218 | foreach (var player in playersInHurtZone)
219 | {
220 | player.TakeDamage(Owner.GetWarcraftPlayer().GetAbilityLevel(1) * 2, Owner, KillFeedIcon.prop_exploding_barrel);
221 | }
222 | }
223 | }
224 |
225 | public override void OnFinish(){}
226 | }
227 | }
228 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Classes/Paladin.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CounterStrikeSharp.API.Core;
3 | using CounterStrikeSharp.API.Modules.Utils;
4 | using WarcraftPlugin.Helpers;
5 | using CounterStrikeSharp.API;
6 | using System.Linq;
7 | using WarcraftPlugin.Models;
8 | using System.Drawing;
9 | using System.Collections.Generic;
10 | using WarcraftPlugin.Events.ExtendedEvents;
11 | using WarcraftPlugin.Core.Effects;
12 |
13 | namespace WarcraftPlugin.Classes
14 | {
15 | internal class Paladin : WarcraftClass
16 | {
17 | private bool _hasUsedDivineResurrection = false;
18 |
19 | public override string DisplayName => "Paladin";
20 | public override Color DefaultColor => Color.Yellow;
21 |
22 | public override List Abilities =>
23 | [
24 | new WarcraftAbility("Healing Aura", "Emit an aura that gradually heals nearby allies over time."),
25 | new WarcraftAbility("Holy Shield", "Gain an additional 20/40/60/80/100 armor."),
26 | new WarcraftAbility("Smite", "Infuse your attacks with divine energy, potentially stripping enemy armor."),
27 | new WarcraftAbility("Divine Resurrection", "Instantly revive a random fallen ally.")
28 | ];
29 |
30 | public override void Register()
31 | {
32 | HookEvent(PlayerSpawn);
33 | HookEvent(PlayerHurtOther);
34 | HookEvent(RoundStart);
35 |
36 | HookAbility(3, Ultimate);
37 | }
38 |
39 | private void RoundStart(EventRoundStart start)
40 | {
41 | _hasUsedDivineResurrection = false;
42 | }
43 |
44 | private void PlayerSpawn(EventPlayerSpawn spawn)
45 | {
46 | if (WarcraftPlayer.GetAbilityLevel(0) > 0)
47 | {
48 | new HealingAuraEffect(Player, 5f).Start();
49 | }
50 |
51 | if (WarcraftPlayer.GetAbilityLevel(1) > 0)
52 | {
53 | Player.GiveNamedItem("item_assaultsuit");
54 | Player.SetArmor(Player.PlayerPawn.Value.ArmorValue + WarcraftPlayer.GetAbilityLevel(1) * 20);
55 | }
56 | }
57 |
58 | private void Ultimate()
59 | {
60 | if (WarcraftPlayer.GetAbilityLevel(3) < 1) return;
61 |
62 | if (!_hasUsedDivineResurrection)
63 | {
64 | DivineResurrection();
65 | }
66 | else
67 | {
68 | Player.PrintToChat($"{ChatColors.Red}Divine resurrection already used this round.{ChatColors.Default}");
69 | }
70 | }
71 |
72 | private void DivineResurrection()
73 | {
74 | var deadTeamPlayers = Utilities.GetPlayers().Where(x => x.Team == Player.Team && !x.PawnIsAlive && Player.IsValid);
75 |
76 | // Check if there are any players on the same team
77 | if (deadTeamPlayers.Any())
78 | {
79 | _hasUsedDivineResurrection = true;
80 | // Generate a random index within the range of players
81 | int randomIndex = Random.Shared.Next(0, deadTeamPlayers.Count() - 1);
82 |
83 | // Get the random player
84 | var playerToRevive = deadTeamPlayers.ElementAt(randomIndex);
85 |
86 | //Revive
87 | playerToRevive.Respawn();
88 | playerToRevive.PlayerPawn.Value.Teleport(Player.CalculatePositionInFront(10, 60), Player.PlayerPawn.Value.EyeAngles, new Vector());
89 |
90 | playerToRevive.PrintToChat(" " + Localizer["paladin.revive"]);
91 | Utilities.GetPlayers().ForEach(x =>
92 | x.PrintToChat(" " + Localizer["paladin.revive.other", playerToRevive.PlayerName, Player.PlayerName]));
93 | }
94 | else
95 | {
96 | Player.PrintToChat(" " + Localizer["paladin.revive.none"]);
97 | }
98 | }
99 |
100 | private void PlayerHurtOther(EventPlayerHurtOther @event)
101 | {
102 | var victim = @event.Userid;
103 | if (!victim.IsAlive() || victim.UserId == Player.UserId) return;
104 |
105 | //Smite
106 | if (victim.PlayerPawn.Value.ArmorValue > 0 && Warcraft.RollDice(WarcraftPlayer.GetAbilityLevel(2), 75))
107 | {
108 | @event.AddBonusDamage(0, WarcraftPlayer.GetAbilityLevel(2) * 5);
109 | Warcraft.SpawnParticle(victim.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 40), "particles/survival_fx/gas_cannister_impact_child_flash.vpcf", 1);
110 | victim.EmitSound("Weapon_Taser.Hit", volume: 0.1f);
111 | }
112 | }
113 |
114 | internal class HealingAuraEffect(CCSPlayerController owner, float onTickInterval) : WarcraftEffect(owner, onTickInterval: onTickInterval)
115 | {
116 | public override void OnStart() {}
117 | public override void OnTick()
118 | {
119 | var currentAbilityLevel = Owner.GetWarcraftPlayer().GetAbilityLevel(0);
120 | var auraSize = currentAbilityLevel * 100;
121 | var healingZone = Warcraft.CreateBoxAroundPoint(Owner.PlayerPawn.Value.AbsOrigin, auraSize, auraSize, auraSize);
122 | //healingZone.Show(duration: 2); //Debug
123 | //Find players within area
124 | var playersToHeal = Utilities.GetPlayers().Where(x => x.AllyOf(Owner) && x.PawnIsAlive && Owner.IsValid &&
125 | healingZone.Contains(x.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 20)));
126 |
127 | if (playersToHeal.Any())
128 | {
129 | foreach (var player in playersToHeal)
130 | {
131 | if (player.PlayerPawn.Value.Health < player.PlayerPawn.Value.MaxHealth)
132 | {
133 | var healthAfterHeal = player.PlayerPawn.Value.Health + currentAbilityLevel;
134 | player.SetHp(Math.Min(healthAfterHeal, player.PlayerPawn.Value.MaxHealth));
135 | Warcraft.SpawnParticle(player.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 40), "particles/ui/ammohealthcenter/ui_hud_kill_burn_fire.vpcf", 1);
136 | }
137 | }
138 | }
139 | }
140 | public override void OnFinish(){}
141 | }
142 | }
143 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Classes/Rogue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Drawing;
3 | using CounterStrikeSharp.API.Core;
4 | using CounterStrikeSharp.API.Modules.Utils;
5 | using WarcraftPlugin.Helpers;
6 | using WarcraftPlugin.Models;
7 | using WarcraftPlugin.Core.Effects;
8 | using System.Collections.Generic;
9 | using WarcraftPlugin.Events.ExtendedEvents;
10 |
11 | namespace WarcraftPlugin.Classes
12 | {
13 | internal class Rogue : WarcraftClass
14 | {
15 | private bool _isPlayerInvulnerable;
16 |
17 | public override string DisplayName => "Rogue";
18 | public override Color DefaultColor => Color.DarkViolet;
19 |
20 | public override List Abilities =>
21 | [
22 | new WarcraftAbility("Stealth", "Become partially invisible for 1/2/3/4/5 seconds, when killing someone."),
23 | new WarcraftAbility("Sneak Attack", "When you hit an enemy in the back, you do an aditional 5/10/15/20/25 damage."),
24 | new WarcraftAbility("Blade Dance", "Increases movement speed and damage with knives."),
25 | new WarcraftCooldownAbility("Smokebomb", "When nearing death, you will automatically drop a smokebomb, letting you cheat death.", 50f)
26 | ];
27 |
28 | public override void Register()
29 | {
30 | HookEvent(PlayerHurtOther);
31 | HookEvent(PlayerHurt);
32 | HookEvent(PlayerKilledOther);
33 | HookEvent(PlayerItemEquip);
34 | }
35 |
36 | private void PlayerHurt(EventPlayerHurt @event)
37 | {
38 | if (_isPlayerInvulnerable)
39 | {
40 | Player.SetHp(1);
41 | return;
42 | }
43 |
44 | if (WarcraftPlayer.GetAbilityLevel(3) < 1 || !IsAbilityReady(3)) return;
45 |
46 | var pawn = Player.PlayerPawn.Value;
47 | if (pawn.Health < 0)
48 | {
49 | StartCooldown(3);
50 | _isPlayerInvulnerable = true;
51 | Player.SetHp(1);
52 | Player.Speed = 0;
53 | new InvisibleEffect(Player, 5).Start();
54 |
55 | //spawn smoke
56 | var smoke = Warcraft.SpawnSmoke(Player.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 5), Player.PlayerPawn.Value, Color.Black);
57 | smoke.SpawnTime = 0;
58 | smoke.Teleport(velocity: Vector.Zero);
59 |
60 | Player.ExecuteClientCommand("slot3"); //pull out knife
61 |
62 | var smokeEffect = Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 90), "particles/maps/de_house/house_fireplace.vpcf");
63 | smokeEffect.SetParent(Player.PlayerPawn.Value);
64 |
65 | WarcraftPlugin.Instance.AddTimer(2f, () => _isPlayerInvulnerable = false);
66 | }
67 | }
68 |
69 | private void PlayerItemEquip(EventItemEquip @event)
70 | {
71 | var pawn = Player.PlayerPawn.Value;
72 | var activeWeaponName = pawn.WeaponServices!.ActiveWeapon.Value.DesignerName;
73 | if (activeWeaponName == "weapon_knife")
74 | {
75 | pawn.VelocityModifier = 1 + 0.1f * WarcraftPlayer.GetAbilityLevel(2);
76 | }
77 | else
78 | {
79 | pawn.VelocityModifier = 1;
80 | }
81 | }
82 |
83 | private void SetInvisible()
84 | {
85 | if (Player.PlayerPawn.Value.Render.A != 0)
86 | {
87 | new InvisibleEffect(Player, WarcraftPlayer.GetAbilityLevel(0)).Start();
88 | }
89 | }
90 |
91 | private void PlayerKilledOther(EventPlayerKilledOther @event)
92 | {
93 | if (WarcraftPlayer.GetAbilityLevel(0) > 0)
94 | {
95 | SetInvisible();
96 | }
97 | }
98 |
99 | private void PlayerHurtOther(EventPlayerHurtOther @event)
100 | {
101 | if (!@event.Userid.IsAlive() || @event.Userid.UserId == Player.UserId) return;
102 |
103 | if (WarcraftPlayer.GetAbilityLevel(2) > 0) BladeDanceDamage(@event);
104 | if (WarcraftPlayer.GetAbilityLevel(1) > 0) Backstab(@event);
105 | }
106 |
107 | private void BladeDanceDamage(EventPlayerHurtOther @event)
108 | {
109 | if (@event.Weapon == "knife")
110 | {
111 | var damageBonus = WarcraftPlayer.GetAbilityLevel(2) * 12;
112 | @event.AddBonusDamage(damageBonus);
113 | Player.EmitSound("Player.GhostKnifeSwish", volume: 0.2f);
114 | }
115 | }
116 |
117 | private void Backstab(EventPlayerHurtOther eventPlayerHurt)
118 | {
119 | var attackerAngle = eventPlayerHurt.Attacker.PlayerPawn.Value.EyeAngles.Y;
120 | var victimAngle = eventPlayerHurt.Userid.PlayerPawn.Value.EyeAngles.Y;
121 |
122 | if (Math.Abs(attackerAngle - victimAngle) <= 50)
123 | {
124 | var damageBonus = WarcraftPlayer.GetAbilityLevel(1) * 5;
125 | eventPlayerHurt.AddBonusDamage(damageBonus);
126 | Player.PrintToChat(" " + Localizer["rogue.backstab", damageBonus]);
127 | Warcraft.SpawnParticle(eventPlayerHurt.Userid.PlayerPawn.Value.AbsOrigin.Clone().Add(z: 85), "particles/overhead_icon_fx/radio_voice_flash.vpcf", 1);
128 | }
129 | }
130 |
131 | internal class InvisibleEffect(CCSPlayerController owner, float duration) : WarcraftEffect(owner, duration)
132 | {
133 | public override void OnStart()
134 | {
135 | Owner.PrintToCenter(Localizer["rogue.invsible"]);
136 | Owner.PlayerPawn.Value.SetColor(Color.FromArgb(0, 255, 255, 255));
137 |
138 | Owner.AdrenalineSurgeEffect(Duration);
139 | }
140 | public override void OnTick() { }
141 | public override void OnFinish()
142 | {
143 | Owner.GetWarcraftPlayer().GetClass().SetDefaultAppearance();
144 | Owner.PrintToCenter(Localizer["rogue.visible"]);
145 | }
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Classes/Shadowblade.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
3 | using WarcraftPlugin.Helpers;
4 | using WarcraftPlugin.Models;
5 | using System.Drawing;
6 | using WarcraftPlugin.Core.Effects;
7 | using System.Collections.Generic;
8 | using WarcraftPlugin.Events.ExtendedEvents;
9 | using System.Linq;
10 | using System;
11 |
12 | namespace WarcraftPlugin.Classes
13 | {
14 | internal class Shadowblade : WarcraftClass
15 | {
16 | public override string DisplayName => "Shadowblade";
17 | public override DefaultClassModel DefaultModel => new()
18 | {
19 | TModel = "characters/models/ctm_st6/ctm_st6_variantn.vmdl",
20 | CTModel = "characters/models/ctm_st6/ctm_st6_variantn.vmdl"
21 | };
22 |
23 | public override Color DefaultColor => Color.Violet;
24 |
25 | public override List Abilities =>
26 | [
27 | new WarcraftAbility("Shadowstep", "Chance to teleport behind enemy when taking damage."),
28 | new WarcraftAbility("Evasion", "Chance to completely dodge incoming damage."),
29 | new WarcraftAbility("Venom Strike", "Your attacks poison enemies, dealing damage over time."),
30 | new WarcraftCooldownAbility("Cloak of Shadows", "Become invisible for a short duration.", 40f)
31 | ];
32 |
33 | private const float _venomDuration = 4f;
34 | private readonly float _cloakDuration = 6f;
35 |
36 | public override void Register()
37 | {
38 | HookEvent(PlayerHurt);
39 | HookEvent(PlayerHurtOther);
40 | HookAbility(3, Ultimate);
41 | }
42 |
43 | private void PlayerHurt(EventPlayerHurt @event)
44 | {
45 | var attacker = @event.Attacker;
46 | // Evasion: Chance to dodge damage
47 | if (Warcraft.RollDice(WarcraftPlayer.GetAbilityLevel(1), 30))
48 | {
49 | Player.PrintToChat(" " + Localizer["shadowblade.evaded", @event.DmgHealth]);
50 | attacker.PrintToChat(" " + Localizer["shadowblade.evaded", Player.GetRealPlayerName()]);
51 | @event.IgnoreDamage();
52 | Player.PlayerPawn.Value.EmitSound("BulletBy.Subsonic", volume: 0.2f);
53 | var particle = Warcraft.SpawnParticle(Player.EyePosition(-50), "particles/explosions_fx/explosion_hegrenade_dirt_ground.vpcf");
54 | particle.SetParent(Player.PlayerPawn.Value);
55 |
56 | return;
57 | }
58 |
59 | // Shadowstep
60 | if (Warcraft.RollDice(WarcraftPlayer.GetAbilityLevel(1), 20))
61 | {
62 | var posBehindEnemy = attacker.CalculatePositionInFront(-90, attacker.EyeHeight());
63 |
64 | // Check that we have visibility of the enemy
65 | var enemyPos = Warcraft.RayTrace(posBehindEnemy, attacker.EyePosition());
66 | var enemyVisible = enemyPos != null && attacker.PlayerPawn.Value.CollisionBox().Contains(enemyPos);
67 |
68 | // Check that we have ground to stand on!
69 | var groundDistance = posBehindEnemy.With().Add(z: -500);
70 | var groundPos = Warcraft.RayTrace(posBehindEnemy, groundDistance);
71 | var aboveGround = groundPos != null && groundPos.Z > groundDistance.Z;
72 |
73 | if (enemyVisible & aboveGround)
74 | {
75 | Player.PlayerPawn.Value.Teleport(posBehindEnemy, attacker.PlayerPawn.Value.EyeAngles, Vector.Zero);
76 | Warcraft.SpawnParticle(Player.PlayerPawn.Value.AbsOrigin, "particles/survival_fx/danger_zone_loop_black.vpcf", 2);
77 | Player.PlayerPawn.Value.EmitSound("UI.PlayerPingUrgent", volume: 0.2f);
78 | Player.PrintToChat(" " + Localizer["shadowblade.shadowstep"]);
79 | }
80 | }
81 | }
82 |
83 | private void PlayerHurtOther(EventPlayerHurtOther @event)
84 | {
85 | if (@event.Userid.AllyOf(Player)) return;
86 |
87 | // Venom Strike: Poison on knife hit
88 | var venomLevel = WarcraftPlayer.GetAbilityLevel(2);
89 | if (venomLevel > 0)
90 | {
91 | var isVictimPoisoned = WarcraftPlugin.Instance.EffectManager.GetEffectsByType()
92 | .Any(x => x.Victim.Handle == @event.Userid.Handle);
93 | if (!isVictimPoisoned)
94 | {
95 | new VenomStrikeEffect(Player, @event.Userid, _venomDuration, venomLevel).Start();
96 | }
97 | }
98 | }
99 |
100 | private void Ultimate()
101 | {
102 | if (WarcraftPlayer.GetAbilityLevel(3) < 1 || !IsAbilityReady(3)) return;
103 |
104 | new InvisibleEffect(Player, _cloakDuration).Start();
105 | StartCooldown(3);
106 | }
107 | }
108 |
109 | internal class VenomStrikeEffect(CCSPlayerController owner, CCSPlayerController victim, float duration, int damage) : WarcraftEffect(owner, duration, onTickInterval: 1f)
110 | {
111 | public CCSPlayerController Victim = victim;
112 |
113 | public override void OnStart()
114 | {
115 | if (!Victim.IsAlive()) return;
116 | Warcraft.SpawnParticle(Victim.EyePosition(-10), "particles/critters/chicken/chicken_impact_burst_zombie.vpcf", 1);
117 | }
118 |
119 | public override void OnTick()
120 | {
121 | if (!Victim.IsAlive()) return;
122 | Warcraft.SpawnParticle(Victim.EyePosition(-10), "particles/critters/chicken/chicken_impact_burst_zombie.vpcf", 1);
123 | Victim.PlayerPawn.Value.EmitSound("Player.DamageFall.Fem", volume: 0.2f);
124 | Victim.TakeDamage(damage, Owner, KillFeedIcon.bayonet);
125 | Victim.PrintToChat(" " + Localizer["shadowblade.venomstrike.victim", damage]);
126 | Owner.PrintToChat(" " + Localizer["shadowblade.venomstrike", Victim.GetRealPlayerName(), damage]);
127 | }
128 |
129 | public override void OnFinish()
130 | {
131 | }
132 | }
133 |
134 | internal class InvisibleEffect(CCSPlayerController owner, float duration) : WarcraftEffect(owner, duration)
135 | {
136 | public override void OnStart()
137 | {
138 | if (!Owner.IsAlive()) return;
139 | Owner.PrintToCenter(Localizer["rogue.invsible"]);
140 | Owner.PlayerPawn.Value.SetColor(Color.FromArgb(0, 255, 255, 255));
141 |
142 | Owner.AdrenalineSurgeEffect(Duration);
143 | Owner.PlayerPawn.Value.VelocityModifier = 2f;
144 | }
145 | public override void OnTick() { }
146 | public override void OnFinish()
147 | {
148 | if (!Owner.IsAlive()) return;
149 | Owner.GetWarcraftPlayer().GetClass().SetDefaultAppearance();
150 | Owner.PrintToCenter(Localizer["rogue.visible"]);
151 | Owner.PlayerPawn.Value.VelocityModifier = 1f;
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Compiler/CustomHero.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.CSharp;
2 | using Microsoft.CodeAnalysis.Emit;
3 | using Microsoft.CodeAnalysis;
4 | using System;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Reflection;
8 | using System.Reflection.Metadata;
9 | using System.Collections.Generic;
10 | using System.Runtime.Loader;
11 |
12 | namespace WarcraftPlugin.Compiler
13 | {
14 | internal class CustomHero
15 | {
16 | internal static Assembly CompileAndLoadAssemblies(string[] heroFiles)
17 | {
18 | // Parse the C# code into syntax trees
19 | var syntaxTrees = new List();
20 | foreach (var heroFile in heroFiles)
21 | {
22 | try
23 | {
24 | var syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(heroFile), path: heroFile);
25 | syntaxTrees.Add(syntaxTree);
26 | }
27 | catch (Exception ex)
28 | {
29 | Console.WriteLine($"Error loading custom hero {heroFile}: {ex.Message}");
30 | }
31 | }
32 |
33 | // Add all shared core references
34 | var references = new List();
35 | references.AddRange(Directory.GetFiles(Path.GetDirectoryName(typeof(object).Assembly.Location), "*.dll")
36 | .Where(dll =>
37 | {
38 | try
39 | {
40 | // Attempt to load the assembly and check if it's a valid managed assembly
41 | var assembly = Assembly.LoadFrom(dll);
42 | return true; // It's a managed assembly
43 | }
44 | catch
45 | {
46 | return false; // It's a native assembly (non-managed)
47 | }
48 | })
49 | .Select(dll => MetadataReference.CreateFromFile(dll))
50 | .ToList());
51 |
52 | // Add all loaded assemblies
53 | foreach (Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
54 | {
55 | if (!string.IsNullOrEmpty(loadedAssembly.Location))
56 | {
57 | // Adding references with location
58 | references.Add(MetadataReference.CreateFromFile(loadedAssembly.Location));
59 | }
60 | else
61 | {
62 | unsafe
63 | {
64 | if (loadedAssembly.TryGetRawMetadata(out byte* blob, out int length))
65 | {
66 | // Add in-memory references
67 | var moduleMetadata = ModuleMetadata.CreateFromMetadata((nint)blob, length);
68 | var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata);
69 | var metadataReference = assemblyMetadata.GetReference();
70 | references.Add(metadataReference);
71 | }
72 | }
73 | }
74 | }
75 |
76 | // Compilation options
77 | var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
78 | .WithGeneralDiagnosticOption(ReportDiagnostic.Suppress);
79 |
80 | // Create the compilation
81 | var compilation = CSharpCompilation.Create(
82 | $"W3CustomHeroes-{Guid.NewGuid()}",
83 | syntaxTrees: syntaxTrees,
84 | references: references,
85 | options: compilationOptions);
86 |
87 | // Emit the assembly to a memory stream
88 | using var memoryStream = new MemoryStream();
89 | EmitResult result = compilation.Emit(memoryStream);
90 |
91 | if (!result.Success)
92 | {
93 | foreach (var diagnostic in result.Diagnostics)
94 | {
95 | // Check if the diagnostic is associated with source code
96 | if (diagnostic.Location.IsInSource)
97 | {
98 | var lineSpan = diagnostic.Location.GetLineSpan();
99 | var fileName = lineSpan.Path; // This is the file name (or path) associated with the error.
100 | Console.WriteLine($"{fileName}({lineSpan.StartLinePosition.Line + 1},{lineSpan.StartLinePosition.Character + 1}):");
101 | Console.WriteLine($"{diagnostic.GetMessage()}");
102 | Console.WriteLine();
103 | }
104 | else
105 | {
106 | // For diagnostics without a source location, just output the message.
107 | Console.WriteLine(diagnostic.GetMessage());
108 | }
109 | }
110 | }
111 |
112 | Console.WriteLine("Loading custom heroes...");
113 | memoryStream.Seek(0, SeekOrigin.Begin);
114 | var customLoadContext = new CustomLoadContext();
115 | var assembly = customLoadContext.LoadFromStream(memoryStream);
116 |
117 | //Debug info
118 | //var allTypes = assembly.GetTypes();
119 | //foreach (var type in allTypes)
120 | //{
121 | // Console.WriteLine($"Type: {type.FullName}, Namespace: {type.Namespace}, IsClass: {type.IsClass}, IsAbstract: {type.IsAbstract}, IsAssignableFrom: {typeof(WarcraftClass).IsAssignableFrom(type)}");
122 | //}
123 | return assembly;
124 | }
125 |
126 | internal static void UnloadAssembly()
127 | {
128 | var previousAssembly = AppDomain.CurrentDomain.GetAssemblies()
129 | .FirstOrDefault(a => a.GetName().Name.Contains("W3CustomHeroes"));
130 |
131 | if (previousAssembly != null)
132 | AssemblyLoadContext.GetLoadContext(previousAssembly)?.Unload();
133 | }
134 |
135 | internal class CustomLoadContext : AssemblyLoadContext
136 | {
137 | internal CustomLoadContext() : base(isCollectible: true) { }
138 |
139 | protected override Assembly Load(AssemblyName assemblyName)
140 | {
141 | //Console.WriteLine($"Loading {assemblyName.Name}");
142 | if (assemblyName.Name == "WarcraftPlugin")
143 | {
144 | //Ensure the latest version of the assembly is loaded
145 | return AppDomain.CurrentDomain.GetAssemblies().Reverse().FirstOrDefault(a => a.GetName().Name == assemblyName.Name);
146 | }
147 | return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == assemblyName.Name);
148 | }
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Core/ClassManager.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Modules.Timers;
2 | using Microsoft.CodeAnalysis;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Reflection;
8 | using WarcraftPlugin.Compiler;
9 | using WarcraftPlugin.Models;
10 |
11 |
12 | namespace WarcraftPlugin.Core
13 | {
14 | internal class ClassManager
15 | {
16 | private readonly Dictionary _classes = [];
17 | private readonly Dictionary _classObjects = [];
18 |
19 | private DirectoryInfo _customHeroesFolder;
20 | private long _customHeroesFilesTimestamp = 0;
21 | private bool _checkingCustomHeroFiles;
22 | private Config _config;
23 |
24 | internal void Initialize(string moduleDirectory, Config config)
25 | {
26 | _config = config;
27 | RegisterDefaultClasses();
28 |
29 | _customHeroesFolder = Directory.CreateDirectory(Path.Combine(moduleDirectory, "CustomHeroes"));
30 | RegisterCustomClasses();
31 | TrackCustomClassesChanges();
32 | }
33 |
34 | private void RegisterCustomClasses()
35 | {
36 | var customHeroFiles = Directory.GetFiles(_customHeroesFolder.FullName, "*.cs");
37 |
38 | if (customHeroFiles.Length > 0)
39 | {
40 | _customHeroesFilesTimestamp = GetLatestTimestamp(customHeroFiles);
41 |
42 | try
43 | {
44 | Console.WriteLine();
45 | Console.ForegroundColor = ConsoleColor.Red;
46 | var assembly = CustomHero.CompileAndLoadAssemblies(customHeroFiles);
47 | Console.ResetColor();
48 | RegisterClasses(assembly);
49 | }
50 | catch
51 | {
52 | Console.ForegroundColor = ConsoleColor.White;
53 | Console.Error.WriteLine($"Error compiling and loading custom heroes");
54 | }
55 | finally
56 | {
57 | Console.WriteLine();
58 | Console.ResetColor();
59 | }
60 | }
61 | }
62 |
63 | private void RegisterDefaultClasses()
64 | {
65 | RegisterClasses(Assembly.GetExecutingAssembly());
66 | }
67 |
68 | private void RegisterClasses(Assembly assembly)
69 | {
70 | var heroClasses = assembly.GetTypes()
71 | .Where(t =>
72 | t.Namespace == "WarcraftPlugin.Classes" && // Ensure it’s in the right namespace
73 | t.IsClass && // Ensure it's a class
74 | !t.IsAbstract && // Exclude abstract classes
75 | typeof(WarcraftClass).IsAssignableFrom(t) // Ensure it derives from WarcraftClass
76 | );
77 |
78 | foreach (var heroClass in heroClasses)
79 | {
80 | RegisterClass(heroClass);
81 | }
82 | }
83 |
84 | private void RegisterClass(Type type)
85 | {
86 | if (_config.DeactivatedClasses.Contains(type.Name, StringComparer.InvariantCultureIgnoreCase))
87 | {
88 | Console.WriteLine($"Skipping deactivated class: {type.Name}");
89 | return;
90 | }
91 |
92 | WarcraftClass heroClass;
93 | try
94 | {
95 | heroClass = InstantiateClass(type);
96 | heroClass.Register();
97 | }
98 | catch (Exception)
99 | {
100 | Console.WriteLine($"Error registering class {type.Name}");
101 | throw;
102 | }
103 |
104 | if (_config.DeactivatedClasses.Contains(heroClass.InternalName, StringComparer.InvariantCultureIgnoreCase) ||
105 | _config.DeactivatedClasses.Contains(heroClass.DisplayName, StringComparer.InvariantCultureIgnoreCase) ||
106 | _config.DeactivatedClasses.Contains(heroClass.LocalizedDisplayName, StringComparer.InvariantCultureIgnoreCase)
107 | )
108 | {
109 | Console.WriteLine($"Skipping deactivated class: {heroClass.DisplayName}");
110 | return;
111 | }
112 |
113 | Console.WriteLine($"Registered class: {heroClass.DisplayName}");
114 | _classes[heroClass.InternalName] = type;
115 | _classObjects[heroClass.InternalName] = heroClass;
116 | }
117 |
118 | internal WarcraftClass InstantiateClassByName(string name)
119 | {
120 | if (!_classes.ContainsKey(name)) throw new Exception("Race not found: " + name);
121 |
122 | var heroClass = InstantiateClass(_classes[name]);
123 | heroClass.Register();
124 |
125 | return heroClass;
126 | }
127 |
128 | internal static WarcraftClass InstantiateClass(Type type)
129 | {
130 | WarcraftClass heroClass;
131 | heroClass = (WarcraftClass)Activator.CreateInstance(type);
132 |
133 | return heroClass;
134 | }
135 |
136 | private void TrackCustomClassesChanges()
137 | {
138 | WarcraftPlugin.Instance.AddTimer(5, () =>
139 | {
140 | if (_checkingCustomHeroFiles) return;
141 | _checkingCustomHeroFiles = true;
142 |
143 | try
144 | {
145 | var customHeroFiles = Directory.GetFiles(_customHeroesFolder.FullName, "*.cs");
146 | if (customHeroFiles.Length > 0 && _customHeroesFilesTimestamp != GetLatestTimestamp(customHeroFiles))
147 | {
148 | Console.WriteLine("Reloading custom hero files...");
149 | _classes.Clear();
150 | _classObjects.Clear();
151 | CustomHero.UnloadAssembly();
152 |
153 | //Trigger plugin reload
154 | File.SetLastWriteTime(Path.Combine(_customHeroesFolder.Parent.FullName, "WarcraftPlugin.dll"), DateTime.Now);
155 | }
156 | }
157 | finally
158 | {
159 | _checkingCustomHeroFiles = false;
160 | }
161 | }, TimerFlags.REPEAT);
162 | }
163 |
164 | internal WarcraftClass[] GetAllClasses()
165 | {
166 | if (_classObjects.Count == 0)
167 | {
168 | throw new Exception("No warcraft classes registered!!!");
169 | }
170 | return _classObjects.Values.ToArray();
171 | }
172 |
173 | internal WarcraftClass GetDefaultClass()
174 | {
175 | var allClasses = GetAllClasses();
176 | WarcraftClass defaultClass = null;
177 |
178 | if (_config.DefaultClass != null)
179 | {
180 | defaultClass = allClasses.FirstOrDefault(x =>
181 | x.InternalName.Equals(_config.DefaultClass, StringComparison.InvariantCultureIgnoreCase) ||
182 | x.DisplayName.Equals(_config.DefaultClass, StringComparison.InvariantCultureIgnoreCase) ||
183 | x.LocalizedDisplayName.Equals(_config.DefaultClass, StringComparison.InvariantCultureIgnoreCase));
184 | }
185 |
186 | defaultClass ??= allClasses.First();
187 |
188 | return defaultClass;
189 | }
190 |
191 | private static long GetLatestTimestamp(string[] files)
192 | {
193 | return files.Select(file => File.GetLastWriteTime(file).Ticks).Max();
194 | }
195 | }
196 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Core/CooldownManager.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API;
2 | using CounterStrikeSharp.API.Modules.Timers;
3 | using WarcraftPlugin.Helpers;
4 | using WarcraftPlugin.Models;
5 |
6 | namespace WarcraftPlugin.Core
7 | {
8 | internal class CooldownManager
9 | {
10 | private readonly float _tickRate = 0.25f;
11 |
12 | internal void Initialize()
13 | {
14 | WarcraftPlugin.Instance.AddTimer(_tickRate, CooldownTick, TimerFlags.REPEAT);
15 | }
16 |
17 | private void CooldownTick()
18 | {
19 | foreach (var player in Utilities.GetPlayers())
20 | {
21 | var warcraftPlayer = player?.GetWarcraftPlayer();
22 | if (warcraftPlayer == null) continue;
23 | for (int i = 0; i < warcraftPlayer.AbilityCooldowns.Count; i++)
24 | {
25 | if (warcraftPlayer.AbilityCooldowns[i] <= 0) continue;
26 |
27 | warcraftPlayer.AbilityCooldowns[i] -= 0.25f;
28 |
29 | if (warcraftPlayer.AbilityCooldowns[i] <= 0)
30 | {
31 | PlayEffects(warcraftPlayer, i);
32 | }
33 | }
34 | }
35 | }
36 |
37 | internal static bool IsAvailable(WarcraftPlayer player, int abilityIndex)
38 | {
39 | return player.AbilityCooldowns[abilityIndex] <= 0;
40 | }
41 |
42 | internal static float Remaining(WarcraftPlayer player, int abilityIndex)
43 | {
44 | return player.AbilityCooldowns[abilityIndex];
45 | }
46 |
47 | internal static void StartCooldown(WarcraftPlayer player, int abilityIndex, float abilityCooldown)
48 | {
49 | player.AbilityCooldowns[abilityIndex] = abilityCooldown;
50 | }
51 |
52 | internal static void ResetCooldowns(WarcraftPlayer player)
53 | {
54 | for (int abilityIndex = 0; abilityIndex < player.AbilityCooldowns.Count; abilityIndex++)
55 | {
56 | player.AbilityCooldowns[abilityIndex] = 0;
57 | }
58 | }
59 |
60 | private static void PlayEffects(WarcraftPlayer wcplayer, int abilityIndex)
61 | {
62 | var ability = wcplayer.GetClass().GetAbility(abilityIndex);
63 |
64 | wcplayer.GetPlayer().PlayLocalSound("sounds/weapons/taser/taser_charge_ready.vsnd");
65 | wcplayer.GetPlayer().PrintToCenter(WarcraftPlugin.Instance.Localizer["ability.ready", ability.DisplayName]);
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Core/Database.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using CounterStrikeSharp.API;
6 | using CounterStrikeSharp.API.Core;
7 | using Dapper;
8 | using Microsoft.Data.Sqlite;
9 | using WarcraftPlugin.Models;
10 |
11 | namespace WarcraftPlugin.Core
12 | {
13 | internal class Database
14 | {
15 | private SqliteConnection _connection;
16 |
17 | internal void Initialize(string directory)
18 | {
19 | _connection =
20 | new SqliteConnection(
21 | $"Data Source={Path.Join(directory, "database.db")}");
22 |
23 | _connection.Execute(@"
24 | CREATE TABLE IF NOT EXISTS `players` (
25 | `steamid` UNSIGNED BIG INT NOT NULL,
26 | `currentRace` VARCHAR(32) NOT NULL,
27 | `name` VARCHAR(64),
28 | PRIMARY KEY (`steamid`));");
29 |
30 | _connection.Execute(@"
31 | CREATE TABLE IF NOT EXISTS `raceinformation` (
32 | `steamid` UNSIGNED BIG INT NOT NULL,
33 | `racename` VARCHAR(32) NOT NULL,
34 | `currentXP` INT NULL DEFAULT 0,
35 | `currentLevel` INT NULL DEFAULT 1,
36 | `amountToLevel` INT NULL DEFAULT 100,
37 | `ability1level` TINYINT NULL DEFAULT 0,
38 | `ability2level` TINYINT NULL DEFAULT 0,
39 | `ability3level` TINYINT NULL DEFAULT 0,
40 | `ability4level` TINYINT NULL DEFAULT 0,
41 | PRIMARY KEY (`steamid`, `racename`));
42 | ");
43 | }
44 |
45 | internal bool PlayerExistsInDatabase(ulong steamid)
46 | {
47 | return _connection.ExecuteScalar("select count(*) from players where steamid = @steamid",
48 | new { steamid }) > 0;
49 | }
50 |
51 | internal void AddNewPlayerToDatabase(CCSPlayerController player)
52 | {
53 | var defaultClass = WarcraftPlugin.Instance.classManager.GetDefaultClass();
54 | Console.WriteLine($"Adding client to database {player.SteamID}");
55 | _connection.Execute(@"
56 | INSERT INTO players (`steamid`, `currentRace`)
57 | VALUES(@steamid, @className)",
58 | new { steamid = player.SteamID, className = defaultClass.InternalName });
59 | }
60 |
61 | internal WarcraftPlayer LoadPlayerFromDatabase(CCSPlayerController player, XpSystem xpSystem)
62 | {
63 | var dbPlayer = _connection.QueryFirstOrDefault(@"
64 | SELECT * FROM `players` WHERE `steamid` = @steamid",
65 | new { steamid = player.SteamID });
66 |
67 | if (dbPlayer == null)
68 | {
69 | AddNewPlayerToDatabase(player);
70 | dbPlayer = _connection.QueryFirstOrDefault(@"
71 | SELECT * FROM `players` WHERE `steamid` = @steamid",
72 | new { steamid = player.SteamID });
73 | }
74 |
75 | // If the class no longer exists, set it to the default class
76 | if (!WarcraftPlugin.Instance.classManager.GetAllClasses().Any(x => x.InternalName == dbPlayer.CurrentRace))
77 | {
78 | var defaultClass = WarcraftPlugin.Instance.classManager.GetDefaultClass();
79 | dbPlayer.CurrentRace = defaultClass.InternalName;
80 | player.PrintToChat(" "+ WarcraftPlugin.Instance.Localizer["class.disabled", defaultClass.LocalizedDisplayName]);
81 | }
82 |
83 | var raceInformationExists = _connection.ExecuteScalar(@"
84 | select count(*) from `raceinformation` where steamid = @steamid AND racename = @racename",
85 | new { steamid = player.SteamID, racename = dbPlayer.CurrentRace }
86 | ) > 0;
87 |
88 | if (!raceInformationExists)
89 | {
90 | _connection.Execute(@"
91 | insert into `raceinformation` (steamid, racename)
92 | values (@steamid, @racename);",
93 | new { steamid = player.SteamID, racename = dbPlayer.CurrentRace });
94 | }
95 |
96 | var raceInformation = _connection.QueryFirst(@"
97 | SELECT * from `raceinformation` where `steamid` = @steamid AND `racename` = @racename",
98 | new { steamid = player.SteamID, racename = dbPlayer.CurrentRace });
99 |
100 | var wcPlayer = new WarcraftPlayer(player);
101 | wcPlayer.LoadClassInformation(raceInformation, xpSystem);
102 | WarcraftPlugin.Instance.SetWcPlayer(player, wcPlayer);
103 |
104 | return wcPlayer;
105 | }
106 |
107 | internal List LoadClassInformationFromDatabase(CCSPlayerController player)
108 | {
109 | var raceInformation = _connection.Query(@"
110 | SELECT * from `raceinformation` where `steamid` = @steamid",
111 | new { steamid = player.SteamID });
112 |
113 | return raceInformation.AsList();
114 | }
115 |
116 | internal void SavePlayerToDatabase(CCSPlayerController player)
117 | {
118 | var wcPlayer = WarcraftPlugin.Instance.GetWcPlayer(player);
119 | Server.PrintToConsole($"Saving {player.PlayerName} to database...");
120 |
121 | var raceInformationExists = _connection.ExecuteScalar(@"
122 | select count(*) from `raceinformation` where steamid = @steamid AND racename = @racename",
123 | new { steamid = player.SteamID, racename = wcPlayer.className }
124 | ) > 0;
125 |
126 | if (!raceInformationExists)
127 | {
128 | _connection.Execute(@"
129 | insert into `raceinformation` (steamid, racename)
130 | values (@steamid, @racename);",
131 | new { steamid = player.SteamID, racename = wcPlayer.className });
132 | }
133 |
134 | _connection.Execute(@"
135 | UPDATE `raceinformation` SET `currentXP` = @currentXp,
136 | `currentLevel` = @currentLevel,
137 | `ability1level` = @ability1Level,
138 | `ability2level` = @ability2Level,
139 | `ability3level` = @ability3Level,
140 | `ability4level` = @ability4Level,
141 | `amountToLevel` = @amountToLevel WHERE `steamid` = @steamid AND `racename` = @racename;",
142 | new
143 | {
144 | wcPlayer.currentXp,
145 | wcPlayer.currentLevel,
146 | ability1Level = wcPlayer.GetAbilityLevel(0),
147 | ability2Level = wcPlayer.GetAbilityLevel(1),
148 | ability3Level = wcPlayer.GetAbilityLevel(2),
149 | ability4Level = wcPlayer.GetAbilityLevel(3),
150 | wcPlayer.amountToLevel,
151 | steamid = player.SteamID,
152 | racename = wcPlayer.className
153 | });
154 | }
155 |
156 | internal void SaveClients()
157 | {
158 | foreach (var player in Utilities.GetPlayers())
159 | {
160 | if (!player.IsValid) continue;
161 |
162 | var wcPlayer = WarcraftPlugin.Instance.GetWcPlayer(player);
163 | if (wcPlayer == null) continue;
164 |
165 | SavePlayerToDatabase(player);
166 | }
167 | }
168 |
169 | internal void SaveCurrentClass(CCSPlayerController player)
170 | {
171 | var wcPlayer = WarcraftPlugin.Instance.GetWcPlayer(player);
172 |
173 | _connection.Execute(@"
174 | UPDATE `players` SET `currentRace` = @currentRace, `name` = @name WHERE `steamid` = @steamid;",
175 | new
176 | {
177 | currentRace = wcPlayer.className,
178 | name = player.PlayerName,
179 | steamid = player.SteamID
180 | });
181 | }
182 |
183 | internal void ResetClients()
184 | {
185 | _connection.Execute(@"
186 | DELETE FROM `players`;");
187 |
188 | _connection.Execute(@"
189 | DELETE FROM `raceinformation`;");
190 | }
191 | }
192 |
193 | internal class DatabasePlayer
194 | {
195 | internal ulong SteamId { get; set; }
196 | internal string CurrentRace { get; set; }
197 | internal string Name { get; set; }
198 | }
199 |
200 | internal class ClassInformation
201 | {
202 | internal ulong SteamId { get; set; }
203 | internal string RaceName { get; set; }
204 | internal int CurrentXp { get; set; }
205 | internal int CurrentLevel { get; set; }
206 | internal int AmountToLevel { get; set; }
207 | internal int Ability1Level { get; set; }
208 | internal int Ability2Level { get; set; }
209 | internal int Ability3Level { get; set; }
210 | internal int Ability4Level { get; set; }
211 | }
212 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Core/Effects/EffectManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CounterStrikeSharp.API;
5 | using CounterStrikeSharp.API.Core;
6 | using CounterStrikeSharp.API.Modules.Timers;
7 | using static CounterStrikeSharp.API.Core.Listeners;
8 |
9 | namespace WarcraftPlugin.Core.Effects
10 | {
11 | public class EffectManager
12 | {
13 | private readonly List _effects = [];
14 | public static readonly float _tickRate = Server.TickInterval; //Lowest possible interval
15 |
16 | internal void Initialize()
17 | {
18 | WarcraftPlugin.Instance.RegisterListener(EffectTick);
19 | }
20 |
21 | internal void AddEffect(WarcraftEffect effect)
22 | {
23 | _effects.Add(effect);
24 | effect.OnStart();
25 | }
26 |
27 | private void EffectTick()
28 | {
29 | for (int i = _effects.Count - 1; i >= 0; i--)
30 | {
31 | var effect = _effects[i];
32 | effect.RemainingDuration -= _tickRate;
33 |
34 | if (effect.RemainingDuration <= 0) // Effect expired, remove it immediately
35 | {
36 | effect.OnFinish();
37 | _effects.RemoveAt(i);
38 | continue;
39 | }
40 |
41 | // Run OnTick only if the specified interval has passed
42 | float elapsedTime = effect.Duration - effect.RemainingDuration;
43 | if (elapsedTime - effect.LastTick >= effect.OnTickInterval)
44 | {
45 | effect.OnTick();
46 | effect.LastTick = elapsedTime;
47 | }
48 | }
49 | }
50 |
51 | public List GetEffects()
52 | {
53 | return _effects;
54 | }
55 |
56 | public List GetEffectsByType() where T : WarcraftEffect
57 | {
58 | return GetEffects().FindAll(x => x is T).Cast().ToList();
59 | }
60 |
61 | internal void DestroyEffects(CCSPlayerController player, EffectDestroyFlags flag)
62 | {
63 | for (int i = _effects.Count - 1; i >= 0; i--)
64 | {
65 | var effect = _effects[i];
66 | if (effect.Owner?.Handle == player.Handle && effect.ShouldDestroy(flag))
67 | {
68 | if (effect.FinishOnDestroy) effect.OnFinish();
69 | _effects.RemoveAt(i);
70 | }
71 | }
72 | }
73 |
74 | internal void DestroyEffect(WarcraftEffect effect)
75 | {
76 | if (_effects.Remove(effect) && effect.FinishOnDestroy)
77 | {
78 | effect.OnFinish();
79 | }
80 | }
81 |
82 | internal void DestroyAllEffects()
83 | {
84 | _effects.Clear();
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Core/Effects/WarcraftEffect.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using Microsoft.Extensions.Localization;
3 | using System;
4 |
5 | namespace WarcraftPlugin.Core.Effects
6 | {
7 | ///
8 | /// Represents an abstract base class for Warcraft effects.
9 | ///
10 | /// The player controller that owns this effect.
11 | /// The duration of the effect in seconds. If not supplied, effect will continue until destroyed.
12 | /// Indicates whether the effect should be destroyed on owner death. Defaults to true.
13 | /// Indicates whether the effect should be destroyed at the end of the round. Defaults to true.
14 | /// Indicates whether the effect should be destroyed when owner changes race. Defaults to true.
15 | /// Indicates whether the effect should be destroyed on owner disconnect. Defaults to true.
16 | /// Indicates whether the effect should be destroyed on owner spawn. Defaults to true.
17 | /// Indicates whether the effect should run the finish method when destroyed. Defaults to true.
18 | /// The interval in seconds at which the effect's OnTick method is called. Defaults to 0.25 seconds.
19 | ///
20 | /// All destroy events happen in the pre hook, before invoke.
21 | ///
22 | public abstract class WarcraftEffect(CCSPlayerController owner, float duration = 100000,
23 | bool destroyOnDeath = true, bool destroyOnRoundEnd = true,
24 | bool destroyOnChangingRace = true, bool destroyOnDisconnect = true,
25 | bool destroyOnSpawn = true, bool finishOnDestroy = true, float onTickInterval = 0.25f)
26 | {
27 | ///
28 | /// Gets the player controller that owns this effect.
29 | ///
30 | public CCSPlayerController Owner { get; } = owner;
31 |
32 | ///
33 | /// Gets the duration of the effect in seconds.
34 | ///
35 | public float Duration { get; set; } = duration;
36 |
37 | ///
38 | /// Gets or sets the remaining duration of the effect in seconds.
39 | ///
40 | public float RemainingDuration { get; set; } = duration;
41 |
42 | ///
43 | /// Gets or sets the time of the last tick in seconds.
44 | ///
45 | public float LastTick { get; set; } = 0;
46 |
47 | ///
48 | /// Gets a value indicating whether the effect should finish when destroyed.
49 | ///
50 | public bool FinishOnDestroy { get; set; } = finishOnDestroy;
51 |
52 | ///
53 | /// Gets the interval in seconds at which the effect's OnTick method is called.
54 | ///
55 | public float OnTickInterval { get; set; } = Math.Max(onTickInterval, EffectManager._tickRate);
56 |
57 | ///
58 | /// Gets the flags indicating the conditions under which the effect should be destroyed.
59 | ///
60 | public EffectDestroyFlags DestroyFlags = (destroyOnDeath ? EffectDestroyFlags.OnDeath : 0) |
61 | (destroyOnRoundEnd ? EffectDestroyFlags.OnRoundEnd : 0) |
62 | (destroyOnChangingRace ? EffectDestroyFlags.OnChangingRace : 0) |
63 | (destroyOnDisconnect ? EffectDestroyFlags.OnDisconnect : 0) |
64 | (destroyOnSpawn ? EffectDestroyFlags.OnSpawn : 0);
65 |
66 | public readonly IStringLocalizer Localizer = WarcraftPlugin.Instance.Localizer;
67 |
68 | ///
69 | /// Called when the effect starts.
70 | ///
71 | public abstract void OnStart();
72 |
73 | ///
74 | /// Called at regular intervals defined by OnTickInterval.
75 | ///
76 | public abstract void OnTick();
77 |
78 | ///
79 | /// Called when the effect finishes.
80 | ///
81 | public abstract void OnFinish();
82 |
83 | ///
84 | /// Determines whether the effect should be destroyed based on the specified condition.
85 | ///
86 | /// The condition to check against the destroy flags.
87 | /// True if the effect should be destroyed; otherwise, false.
88 | public bool ShouldDestroy(EffectDestroyFlags condition) => (DestroyFlags & condition) != 0;
89 |
90 | public void Destroy() => WarcraftPlugin.Instance.EffectManager.DestroyEffect(this);
91 | public void Start() => WarcraftPlugin.Instance.EffectManager.AddEffect(this);
92 | }
93 |
94 | ///
95 | /// Specifies the conditions under which a Warcraft effect should be destroyed.
96 | ///
97 | [Flags]
98 | public enum EffectDestroyFlags
99 | {
100 | ///
101 | /// No conditions.
102 | ///
103 | None = 0,
104 |
105 | ///
106 | /// Destroy on death.
107 | ///
108 | OnDeath = 1 << 0, // 1
109 |
110 | ///
111 | /// Destroy at the end of the round.
112 | ///
113 | OnRoundEnd = 1 << 1, // 2
114 |
115 | ///
116 | /// Destroy when changing race.
117 | ///
118 | OnChangingRace = 1 << 2, // 4
119 |
120 | ///
121 | /// Destroy on disconnect.
122 | ///
123 | OnDisconnect = 1 << 3, // 8
124 |
125 | ///
126 | /// Destroy on spawn.
127 | ///
128 | OnSpawn = 1 << 4, // 16
129 | }
130 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Core/XpSystem.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System;
3 | using System.Collections.Generic;
4 | using WarcraftPlugin.Helpers;
5 | using WarcraftPlugin.Models;
6 |
7 | namespace WarcraftPlugin.Core
8 | {
9 | internal class XpSystem
10 | {
11 | private readonly WarcraftPlugin _plugin;
12 |
13 | internal XpSystem(WarcraftPlugin plugin)
14 | {
15 | _plugin = plugin;
16 | }
17 |
18 | private readonly List _levelXpRequirement = new(new int[256]);
19 |
20 | internal void GenerateXpCurve(int initial, float modifier, int maxLevel)
21 | {
22 | for (int i = 1; i <= maxLevel; i++)
23 | {
24 | if (i == 1)
25 | _levelXpRequirement[i] = initial;
26 | else
27 | _levelXpRequirement[i] = Convert.ToInt32(_levelXpRequirement[i - 1] * modifier);
28 | }
29 | }
30 |
31 | internal int GetXpForLevel(int level)
32 | {
33 | return _levelXpRequirement[level];
34 | }
35 |
36 | internal void AddXp(CCSPlayerController player, int xpToAdd)
37 | {
38 | var wcPlayer = _plugin.GetWcPlayer(player);
39 | if (wcPlayer == null) return;
40 |
41 | if (wcPlayer.GetLevel() >= WarcraftPlugin.MaxLevel) return;
42 |
43 | wcPlayer.currentXp += xpToAdd;
44 |
45 | while (wcPlayer.currentXp >= wcPlayer.amountToLevel)
46 | {
47 | wcPlayer.currentXp = wcPlayer.currentXp - wcPlayer.amountToLevel;
48 | GrantLevel(wcPlayer);
49 |
50 | if (wcPlayer.GetLevel() >= WarcraftPlugin.MaxLevel) return;
51 | }
52 | }
53 |
54 | internal void GrantLevel(WarcraftPlayer wcPlayer)
55 | {
56 | if (wcPlayer.GetLevel() >= WarcraftPlugin.MaxLevel) return;
57 |
58 | wcPlayer.currentLevel += 1;
59 |
60 | RecalculateXpForLevel(wcPlayer);
61 | PerformLevelupEvents(wcPlayer);
62 | }
63 |
64 | private static void PerformLevelupEvents(WarcraftPlayer wcPlayer)
65 | {
66 | var player = wcPlayer.GetPlayer();
67 | if (player.IsAlive())
68 | {
69 | player.PlayLocalSound("play sounds/ui/achievement_earned.vsnd");
70 | Warcraft.SpawnParticle(player.PlayerPawn.Value.AbsOrigin, "particles/ui/ammohealthcenter/ui_hud_kill_streaks_glow_5.vpcf", 1);
71 | }
72 |
73 | WarcraftPlugin.RefreshPlayerName(player);
74 | }
75 |
76 | internal void RecalculateXpForLevel(WarcraftPlayer wcPlayer)
77 | {
78 | if (wcPlayer.currentLevel == WarcraftPlugin.MaxLevel)
79 | {
80 | wcPlayer.amountToLevel = 0;
81 | return;
82 | }
83 |
84 | wcPlayer.amountToLevel = GetXpForLevel(wcPlayer.currentLevel);
85 | }
86 |
87 | internal static int GetFreeSkillPoints(WarcraftPlayer wcPlayer)
88 | {
89 | int totalPointsUsed = 0;
90 |
91 | for (int i = 0; i < 4; i++)
92 | {
93 | totalPointsUsed += wcPlayer.GetAbilityLevel(i);
94 | }
95 |
96 | int level = wcPlayer.GetLevel();
97 | if (level > WarcraftPlugin.MaxLevel)
98 | level = WarcraftPlugin.MaxSkillLevel;
99 |
100 | return level - totalPointsUsed;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Events/ExtendedEvents/EventExtensions.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System;
3 | using WarcraftPlugin.Helpers;
4 | using WarcraftPlugin.Models;
5 |
6 | namespace WarcraftPlugin.Events.ExtendedEvents
7 | {
8 | public static class EventExtensions
9 | {
10 | ///
11 | /// Inflicts bonus damage to the player.
12 | ///
13 | /// The amount of damage to inflict.
14 | /// The amount of armor damage to inflict.
15 | /// Optional kill feed icon to display if the bonus damage results in a kill.
16 | /// Force client UI to update new health values.
17 | public static void AddBonusDamage(this EventPlayerHurtOther @event, int damageHealth, int damageArmor = 0, KillFeedIcon? killFeedIcon = null)
18 | {
19 | var victim = @event.Userid;
20 | var attacker = @event.Attacker;
21 | if (victim.IsAlive())
22 | {
23 | victim.PlayerPawn.Value.Health -= damageHealth;
24 | victim.PlayerPawn.Value.ArmorValue -= damageArmor;
25 | @event.DmgHealth += damageHealth;
26 | @event.DmgArmor += damageArmor;
27 |
28 | var attackerClass = attacker?.GetWarcraftPlayer()?.GetClass();
29 | if (killFeedIcon != null) attackerClass?.SetKillFeedIcon(killFeedIcon);
30 | }
31 | }
32 |
33 | ///
34 | /// Ignores all incoming damage to the player. Only works if the event is coming from Hookmode.Pre
35 | ///
36 | /// Optional amount of health damage to ignore.
37 | /// Optional amount of armor damage to ignore.
38 | public static void IgnoreDamage(this EventPlayerHurt @event, int? healthDamageToIgnore = null, int? armorDamageToIgnore = null)
39 | {
40 | var victim = @event.Userid;
41 | if (victim.IsAlive())
42 | {
43 | int ignoredHealthDamage = Math.Clamp(healthDamageToIgnore ?? @event.DmgHealth, 0, @event.DmgHealth);
44 | int ignoredArmorDamage = Math.Clamp(armorDamageToIgnore ?? @event.DmgArmor, 0, @event.DmgArmor);
45 | victim.PlayerPawn.Value.Health += ignoredHealthDamage;
46 | victim.PlayerPawn.Value.ArmorValue += ignoredArmorDamage;
47 | @event.DmgHealth -= ignoredHealthDamage;
48 | @event.DmgArmor -= ignoredArmorDamage;
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Events/ExtendedEvents/EventPlayerHurtOther.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 |
3 | namespace WarcraftPlugin.Events.ExtendedEvents
4 | {
5 | public class EventPlayerHurtOther(nint pointer) : EventPlayerHurt(pointer), ICustomGameEvent
6 | {
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Events/ExtendedEvents/EventPlayerKilledOther.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 |
3 | namespace WarcraftPlugin.Events.ExtendedEvents
4 | {
5 | public class EventPlayerKilledOther : EventPlayerDeath, ICustomGameEvent
6 | {
7 | public EventPlayerKilledOther(nint pointer) : base(pointer)
8 | {
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Events/ExtendedEvents/EventSpottedByEnemy.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Events;
3 |
4 | namespace WarcraftPlugin.Events.ExtendedEvents
5 | {
6 | public class EventSpottedByEnemy : GameEvent, ICustomGameEvent
7 | {
8 | public EventSpottedByEnemy() : base(0)
9 | {
10 | }
11 |
12 | public EventSpottedByEnemy(nint pointer) : base(pointer)
13 | {
14 | }
15 |
16 | public EventSpottedByEnemy(string name, bool force) : base(name, force)
17 | {
18 | }
19 |
20 | public CCSPlayerController UserId { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Events/ExtendedEvents/EventSpottedEnemy.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Events;
3 |
4 | namespace WarcraftPlugin.Events.ExtendedEvents
5 | {
6 | public class EventSpottedEnemy : GameEvent, ICustomGameEvent
7 | {
8 | public EventSpottedEnemy() : base(0)
9 | {
10 | }
11 |
12 | public EventSpottedEnemy(nint pointer) : base(pointer)
13 | {
14 | }
15 |
16 | public EventSpottedEnemy(string name, bool force) : base(name, force)
17 | {
18 | }
19 |
20 | public CCSPlayerController UserId { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Events/ExtendedEvents/ICustomGameEvent.cs:
--------------------------------------------------------------------------------
1 | namespace WarcraftPlugin.Events.ExtendedEvents
2 | {
3 | internal interface ICustomGameEvent
4 | {
5 | }
6 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Helpers/Geometry.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Utils;
3 | using g3;
4 | using MIConvexHull;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Drawing;
8 | using System.Linq;
9 |
10 | namespace WarcraftPlugin.Helpers
11 | {
12 | public static class Geometry
13 | {
14 | public static Box3d CreateBoxAroundPoint(Vector point, double sizeX, double sizeY, double heightZ)
15 | {
16 | Vector3d min = new(point.X - sizeX / 2, point.Y - sizeY / 2, point.Z - heightZ / 2);
17 | Vector3d max = new(point.X + sizeX / 2, point.Y + sizeY / 2, point.Z + heightZ / 2);
18 | return new Box3d(new AxisAlignedBox3d(min, max));
19 | }
20 |
21 | public static List CreateSphereAroundPoint(Vector point, double radius, int numLatitudeSegments = 10, int numLongitudeSegments = 10)
22 | {
23 | var vertices = new List();
24 |
25 | // Generate vertices
26 | for (int lat = 0; lat <= numLatitudeSegments; lat++)
27 | {
28 | double theta = lat * Math.PI / numLatitudeSegments;
29 | double sinTheta = Math.Sin(theta);
30 | double cosTheta = Math.Cos(theta);
31 |
32 | for (int lon = 0; lon <= numLongitudeSegments; lon++)
33 | {
34 | double phi = lon * 2 * Math.PI / numLongitudeSegments;
35 | double sinPhi = Math.Sin(phi);
36 | double cosPhi = Math.Cos(phi);
37 |
38 | double x = cosPhi * sinTheta;
39 | double y = cosTheta;
40 | double z = sinPhi * sinTheta;
41 |
42 | vertices.Add(new Vector3d(x * radius, y * radius, z * radius) + point.ToVector3d());
43 | }
44 | }
45 |
46 | return vertices;
47 | }
48 |
49 | public static Vector3d ToVector3d(this Vector vector)
50 | {
51 | return new Vector3d(vector.X, vector.Y, vector.Z);
52 | }
53 |
54 | public static Vector ToVector(this Vector3d vector)
55 | {
56 | return new Vector((float?)vector.x, (float?)vector.y, (float?)vector.z);
57 | }
58 |
59 | public static void DrawVertices(IEnumerable vertices, Color? color = null, float duration = 5, float width = 0.1f)
60 | {
61 | if (!vertices.Any())
62 | {
63 | Console.WriteLine("No vertices to draw");
64 | return;
65 | }
66 |
67 | var convexHull = ConvexHull.Create(vertices.Select(vertex => new double[] { vertex.x, vertex.y, vertex.z }).ToArray());
68 | int i = 0;
69 | float frequency = 0.1f;
70 | foreach (var face in convexHull.Result.Faces)
71 | {
72 | var facecolor = color ?? Color.FromArgb(255, (int)(Math.Sin(frequency * i + 0) * 127 + 128), (int)(Math.Sin(frequency * i + 2) * 127 + 128), (int)(Math.Sin(frequency * i + 4) * 127 + 128));
73 | Warcraft.DrawLaserBetween(new Vector((float)face.Vertices[0].Position[0], (float)face.Vertices[0].Position[1], (float)face.Vertices[0].Position[2]), new Vector((float)face.Vertices[1].Position[0], (float)face.Vertices[1].Position[1], (float)face.Vertices[1].Position[2]), facecolor, duration, width);
74 | Warcraft.DrawLaserBetween(new Vector((float)face.Vertices[1].Position[0], (float)face.Vertices[1].Position[1], (float)face.Vertices[1].Position[2]), new Vector((float)face.Vertices[2].Position[0], (float)face.Vertices[2].Position[1], (float)face.Vertices[2].Position[2]), facecolor, duration, width);
75 | Warcraft.DrawLaserBetween(new Vector((float)face.Vertices[2].Position[0], (float)face.Vertices[2].Position[1], (float)face.Vertices[2].Position[2]), new Vector((float)face.Vertices[0].Position[0], (float)face.Vertices[0].Position[1], (float)face.Vertices[0].Position[2]), facecolor, duration, width);
76 |
77 | i++;
78 | }
79 | }
80 |
81 | public static Vector GetRandomPoint(this Box3d box)
82 | {
83 | var random = Random.Shared;
84 | // Generate random coordinates within the range [-1, 1]
85 | double x = (2 * random.NextDouble() - 1) * box.Extent.x;
86 | double y = (2 * random.NextDouble() - 1) * box.Extent.y;
87 | double z = (2 * random.NextDouble() - 1) * box.Extent.z;
88 |
89 | // Transform coordinates to be relative to the box's center
90 | var randomPoint = box.Center + x * box.AxisX + y * box.AxisY + z * box.AxisZ;
91 |
92 | return randomPoint.ToVector();
93 | }
94 |
95 | public static Box3d ToBox(this CCollisionProperty collision, Vector worldPosition)
96 | {
97 | Vector worldCenter = worldPosition.Clone().Add(z: collision.Mins.Z + (collision.Maxs.Z - collision.Mins.Z) / 2);
98 | return CreateBoxAroundPoint(worldCenter, collision.Maxs.X * 2, collision.Maxs.Y * 2, collision.Maxs.Z);
99 | }
100 |
101 | public static Vector Add(this Vector vector, float x = 0, float y = 0, float z = 0)
102 | {
103 | vector.X += x;
104 | vector.Y += y;
105 | vector.Z += z;
106 | return vector;
107 | }
108 |
109 | public static Vector Multiply(this Vector vector, float x = 1, float y = 1, float z = 1)
110 | {
111 | vector.X *= x;
112 | vector.Y *= y;
113 | vector.Z *= z;
114 | return vector;
115 | }
116 |
117 | public static bool IsEqual(this Vector vector1, Vector vector2, bool floor = false)
118 | {
119 | if (floor)
120 | {
121 | return Math.Floor(vector1.X) == Math.Floor(vector2.X) &&
122 | Math.Floor(vector1.Y) == Math.Floor(vector2.Y) &&
123 | Math.Floor(vector1.Z) == Math.Floor(vector2.Z);
124 | }
125 | else
126 | {
127 | return vector1.X == vector2.X && vector1.Y == vector2.Y && vector1.Z == vector2.Z;
128 | }
129 | }
130 |
131 | ///
132 | /// Returns a copy of the vector with values replaced.
133 | ///
134 | public static Vector Clone(this Vector vector)
135 | {
136 | return vector.With();
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Helpers/Memory.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
3 | using CounterStrikeSharp.API.Modules.Utils;
4 | using System;
5 | using System.Runtime.InteropServices;
6 |
7 | namespace WarcraftPlugin.Helpers
8 | {
9 | internal static class Memory
10 | {
11 | internal static MemoryFunctionVoid CBaseEntity_EmitSoundParamsFunc = new(
12 | Environment.OSVersion.Platform == PlatformID.Unix
13 | ? @"\x48\xB8\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x2A\x55\x48\x89\xE5\x41\x55\x41\x54\x49\x89\xFC\x53\x48\x89\xF3"
14 | : @"\x48\x8B\xC4\x48\x89\x58\x10\x48\x89\x70\x18\x55\x57\x41\x56\x48\x8D\xA8\x08\xFF\xFF\xFF"
15 | );
16 |
17 | internal static MemoryFunctionWithReturn CSoundOpGameSystem_SetSoundEventParam_Windows =
18 | new("48 89 5C 24 08 48 89 6C 24 10 56 57 41 56 48 83 EC 40 48 8B B4 24 80 00 00 00");
19 | internal static MemoryFunctionWithReturn CSoundOpGameSystem_SetSoundEventParam_Linux =
20 | new("55 48 89 E5 41 57 41 56 49 89 F6 48 89 D6 41 55 41 89 CD");
21 |
22 | internal static MemoryFunctionVoid CBaseEntity_SetParent = new(
23 | Environment.OSVersion.Platform == PlatformID.Unix
24 | ? @"\x48\x85\xF6\x74\x2A\x48\x8B\x47\x10\xF6\x40\x31\x02\x75\x2A\x48\x8B\x46\x10\xF6\x40\x31\x02\x75\x2A\xB8\x2A\x2A\x2A\x2A"
25 | : @"\x4D\x8B\xD9\x48\x85\xD2\x74\x2A"
26 | );
27 |
28 | internal static MemoryFunctionWithReturn CSmokeGrenadeProjectile_CreateFunc = new(
29 | Environment.OSVersion.Platform == PlatformID.Unix
30 | ? @"\x55\x4C\x89\xC1\x48\x89\xE5\x41\x57\x41\x56\x49\x89\xD6"
31 | : @"\x48\x89\x5C\x24\x2A\x48\x89\x6C\x24\x2A\x48\x89\x74\x24\x2A\x57\x41\x56\x41\x57\x48\x83\xEC\x50\x4C\x8B\xB4\x24"
32 | );
33 | }
34 |
35 | internal class Struct
36 | {
37 | [StructLayout(LayoutKind.Explicit)]
38 | internal struct CAttackerInfo
39 | {
40 | internal CAttackerInfo(CEntityInstance attacker)
41 | {
42 | NeedInit = false;
43 | IsWorld = true;
44 | Attacker = attacker.EntityHandle.Raw;
45 | if (attacker.DesignerName != "cs_player_controller") return;
46 |
47 | var controller = attacker.As();
48 | IsWorld = false;
49 | IsPawn = true;
50 | AttackerUserId = (ushort)(controller.UserId ?? 0xFFFF);
51 | TeamNum = controller.TeamNum;
52 | TeamChecked = controller.TeamNum;
53 | }
54 |
55 | [FieldOffset(0x0)] internal bool NeedInit = true;
56 | [FieldOffset(0x1)] internal bool IsPawn = false;
57 | [FieldOffset(0x2)] internal bool IsWorld = false;
58 |
59 | [FieldOffset(0x4)]
60 | internal UInt32 Attacker;
61 |
62 | [FieldOffset(0x8)]
63 | internal ushort AttackerUserId;
64 |
65 | [FieldOffset(0x0C)] internal int TeamChecked = -1;
66 | [FieldOffset(0x10)] internal int TeamNum = -1;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Helpers/RayTracer.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Memory;
3 | using System.Drawing;
4 | using System.Numerics;
5 | using System.Runtime.InteropServices;
6 | using System;
7 | using CounterStrikeSharp.API.Modules.Utils;
8 | using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
9 |
10 | namespace WarcraftPlugin.Helpers
11 | {
12 | public static class RayTracer
13 | {
14 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
15 | private unsafe delegate bool TraceShapeDelegate(
16 | nint GameTraceManager,
17 | nint vecStart,
18 | nint vecEnd,
19 | nint skip,
20 | ulong mask,
21 | byte a6,
22 | GameTrace* pGameTrace
23 | );
24 |
25 | private static readonly nint TraceFunc = NativeAPI.FindSignature(Addresses.ServerPath, Environment.OSVersion.Platform == PlatformID.Unix
26 | ? "48 B8 ? ? ? ? ? ? ? ? 55 48 89 E5 41 57 41 56 49 89 D6 41 55"
27 | : "4C 8B DC 49 89 5B ? 49 89 6B ? 49 89 73 ? 57 41 56 41 57 48 81 EC ? ? ? ? 0F 57 C0");
28 |
29 | private static readonly nint GameTraceManager = NativeAPI.FindSignature(Addresses.ServerPath, Environment.OSVersion.Platform == PlatformID.Unix
30 | ? "48 8D 05 ? ? ? ? F3 0F 58 8D ? ? ? ? 31 FF"
31 | : "48 8B 0D ? ? ? ? 48 8D 45 ? 48 89 44 24 ? 4C 8D 44 24 ? C7 44 24 ? ? ? ? ? 48 8D 54 24 ? 4C 8B CB");
32 |
33 | public static unsafe Vector Trace(Vector _origin, QAngle _viewangles, bool drawResult = false, bool fromPlayer = false)
34 | {
35 | var _forward = new Vector();
36 |
37 | // Get forward vector from view angles
38 | NativeAPI.AngleVectors(_viewangles.Handle, _forward.Handle, 0, 0);
39 | var _endOrigin = new Vector(_origin.X + _forward.X * 8192, _origin.Y + _forward.Y * 8192, _origin.Z + _forward.Z * 8192);
40 |
41 | var d = 50;
42 |
43 | if (fromPlayer)
44 | {
45 | _origin.X += _forward.X * d;
46 | _origin.Y += _forward.Y * d;
47 | _origin.Z += _forward.Z * d;
48 | }
49 |
50 | return Trace(_origin, _endOrigin, drawResult);
51 | }
52 |
53 | public static unsafe Vector Trace(Vector _origin, Vector _endOrigin, bool drawResult = false)
54 | {
55 | var _gameTraceManagerAddress = Address.GetAbsoluteAddress(GameTraceManager, 3, 7);
56 |
57 | var traceShape = Marshal.GetDelegateForFunctionPointer(TraceFunc);
58 |
59 | // Console.WriteLine($"==== TraceFunc {TraceFunc} | GameTraceManager {GameTraceManager} | _gameTraceManagerAddress {_gameTraceManagerAddress} | _traceShape {_traceShape}");
60 |
61 | var _trace = stackalloc GameTrace[1];
62 |
63 | ulong mask = 0x1C1003;
64 | // var mask = 0xFFFFFFFF;
65 | var result = traceShape(*(nint*)_gameTraceManagerAddress, _origin.Handle, _endOrigin.Handle, 0, mask, 4, _trace);
66 |
67 | //Console.WriteLine($"RESULT {result}");
68 |
69 | //Console.WriteLine($"StartPos: {_trace->StartPos}");
70 | //Console.WriteLine($"EndPos: {_trace->EndPos}");
71 | //Console.WriteLine($"HitEntity: {(uint)_trace->HitEntity}");
72 | //Console.WriteLine($"Fraction: {_trace->Fraction}");
73 | //Console.WriteLine($"AllSolid: {_trace->AllSolid}");
74 | //Console.WriteLine($"ViewAngles: {_viewangles}");
75 |
76 | var endPos = new Vector(_trace->EndPos.X, _trace->EndPos.Y, _trace->EndPos.Z);
77 |
78 | if (drawResult)
79 | {
80 | Color color = Color.FromName("Green");
81 | if (result)
82 | {
83 | color = Color.FromName("Red");
84 | }
85 |
86 | Warcraft.DrawLaserBetween(_origin, endPos, color, 5);
87 | }
88 |
89 | if (result)
90 | {
91 | return endPos;
92 | }
93 |
94 | return null;
95 | }
96 | }
97 |
98 | internal static class Address
99 | {
100 | static unsafe internal nint GetAbsoluteAddress(nint addr, nint offset, int size)
101 | {
102 | if (addr == nint.Zero)
103 | {
104 | throw new Exception("Failed to find RayTrace signature.");
105 | }
106 |
107 | int code = *(int*)(addr + offset);
108 | return addr + code + size;
109 | }
110 |
111 | static internal nint GetCallAddress(nint a)
112 | {
113 | return GetAbsoluteAddress(a, 1, 5);
114 | }
115 | }
116 |
117 | [StructLayout(LayoutKind.Explicit, Size = 0x35)]
118 | internal unsafe struct Ray
119 | {
120 | [FieldOffset(0)] internal Vector3 Start;
121 | [FieldOffset(0xC)] internal Vector3 End;
122 | [FieldOffset(0x18)] internal Vector3 Mins;
123 | [FieldOffset(0x24)] internal Vector3 Maxs;
124 | [FieldOffset(0x34)] internal byte UnkType;
125 | }
126 |
127 | [StructLayout(LayoutKind.Explicit, Size = 0x44)]
128 | internal unsafe struct TraceHitboxData
129 | {
130 | [FieldOffset(0x38)] internal int HitGroup;
131 | [FieldOffset(0x40)] internal int HitboxId;
132 | }
133 |
134 | [StructLayout(LayoutKind.Explicit, Size = 0xB8)]
135 | internal unsafe struct GameTrace
136 | {
137 | [FieldOffset(0)] internal void* Surface;
138 | [FieldOffset(0x8)] internal void* HitEntity;
139 | [FieldOffset(0x10)] internal TraceHitboxData* HitboxData;
140 | [FieldOffset(0x50)] internal uint Contents;
141 | [FieldOffset(0x78)] internal Vector3 StartPos;
142 | [FieldOffset(0x84)] internal Vector3 EndPos;
143 | [FieldOffset(0x90)] internal Vector3 Normal;
144 | [FieldOffset(0x9C)] internal Vector3 Position;
145 | [FieldOffset(0xAC)] internal float Fraction;
146 | [FieldOffset(0xB6)] internal bool AllSolid;
147 | }
148 |
149 | [StructLayout(LayoutKind.Explicit, Size = 0x3a)]
150 | internal unsafe struct TraceFilter
151 | {
152 | [FieldOffset(0)] internal void* Vtable;
153 | [FieldOffset(0x8)] internal ulong Mask;
154 | [FieldOffset(0x20)] internal fixed uint SkipHandles[4];
155 | [FieldOffset(0x30)] internal fixed ushort arrCollisions[2];
156 | [FieldOffset(0x34)] internal uint Unk1;
157 | [FieldOffset(0x38)] internal byte Unk2;
158 | [FieldOffset(0x39)] internal byte Unk3;
159 | }
160 |
161 | internal unsafe struct TraceFilterV2
162 | {
163 | internal ulong Mask;
164 | internal fixed ulong V1[2];
165 | internal fixed uint SkipHandles[4];
166 | internal fixed ushort arrCollisions[2];
167 | internal short V2;
168 | internal byte V3;
169 | internal byte V4;
170 | internal byte V5;
171 | }
172 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Helpers/VolumeFix.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
3 | using System;
4 |
5 | namespace WarcraftPlugin.Helpers
6 | {
7 | internal class VolumeFix
8 | {
9 | internal static void Load()
10 | {
11 | if (Environment.OSVersion.Platform == PlatformID.Unix)
12 | {
13 | Memory.CSoundOpGameSystem_SetSoundEventParam_Linux.Hook(OnSetSoundEventParam, HookMode.Pre);
14 | }
15 | else
16 | {
17 | Memory.CSoundOpGameSystem_SetSoundEventParam_Windows.Hook(OnSetSoundEventParam, HookMode.Pre);
18 | }
19 | }
20 |
21 | internal static void Unload()
22 | {
23 | if (Environment.OSVersion.Platform == PlatformID.Unix)
24 | {
25 | Memory.CSoundOpGameSystem_SetSoundEventParam_Linux.Unhook(OnSetSoundEventParam, HookMode.Pre);
26 | }
27 | else
28 | {
29 | Memory.CSoundOpGameSystem_SetSoundEventParam_Windows.Unhook(OnSetSoundEventParam, HookMode.Pre);
30 | }
31 | }
32 |
33 | internal static HookResult OnSetSoundEventParam(DynamicHook hook)
34 | {
35 | var hash = hook.GetParam(3);
36 | if (hash == 0x2D8464AF)
37 | {
38 | hook.SetParam(3, 0xBD6054E9);
39 | }
40 | return HookResult.Continue;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Helpers/WeaponTypes.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace WarcraftPlugin.Helpers
4 | {
5 | public static class WeaponTypes
6 | {
7 | public static readonly List Shotguns =
8 | [
9 | "xm1014", "sawedoff", "nova", "mag7"
10 | ];
11 |
12 | public static readonly List Snipers =
13 | [
14 | "sg553", "scar20", "aug", "ssg08", "awp", "g3sg1"
15 | ];
16 |
17 | public static readonly List Rifles =
18 | [
19 | "ak47", "m4a1", "m4a1_silencer", "galilar", "famas"
20 | ];
21 |
22 | public static readonly List SMGs =
23 | [
24 | "mp9", "mac10", "mp7", "ump45", "p90", "bizon"
25 | ];
26 |
27 | public static readonly List Pistols =
28 | [
29 | "glock", "usp_silencer", "p2000", "dualberettas", "p250", "fiveseven", "tec9", "cz75a", "deagle", "revolver"
30 | ];
31 |
32 | public static readonly List Heavy =
33 | [
34 | "m249", "negev"
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/FontSizes.cs:
--------------------------------------------------------------------------------
1 | namespace WarcraftPlugin.Menu
2 | {
3 | internal static class FontSizes
4 | {
5 | internal const string FontSizeXs = "fontSize-xs"; // 8px
6 | internal const string FontSizeS = "fontSize-s"; // 12px
7 | internal const string FontSizeSm = "fontSize-sm"; // 16px
8 | internal const string FontSizeM = "fontSize-m"; // 18px
9 | internal const string FontSizeL = "fontSize-l"; // 24px
10 | internal const string FontSizeXl = "fontSize-xl"; // 32px
11 | internal const string FontSizeXxl = "fontSize-xxl"; // 40px
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/Menu.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace WarcraftPlugin.Menu;
6 |
7 | internal class Menu
8 | {
9 | internal string Title { get; set; } = "";
10 | internal int ResultsBeforePaging { get; set; }
11 | internal LinkedList Options { get; set; } = new();
12 | internal LinkedListNode Prev { get; set; } = null;
13 |
14 | internal LinkedListNode Add(string display, string subDisplay, Action onChoice, Action onSelect = null)
15 | {
16 | if (Options == null)
17 | Options = new();
18 | MenuOption newOption = new MenuOption
19 | {
20 | OptionDisplay = display,
21 | SubOptionDisplay = subDisplay,
22 | OnChoose = onChoice,
23 | OnSelect = onSelect,
24 | Index = Options.Count,
25 | Parent = this
26 | };
27 | return Options.AddLast(newOption);
28 | }
29 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/MenuAPI.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API;
2 | using CounterStrikeSharp.API.Core;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 |
6 | namespace WarcraftPlugin.Menu;
7 |
8 | internal static class MenuAPI
9 | {
10 |
11 | internal static readonly Dictionary Players = [];
12 |
13 | internal static void Load(BasePlugin plugin)
14 | {
15 | Server.NextFrame(() =>
16 | {
17 | foreach (var pl in Utilities.GetPlayers())
18 | {
19 | Players[pl.Slot] = new MenuPlayer
20 | {
21 | player = pl,
22 | Buttons = pl.Buttons
23 | };
24 | }
25 | });
26 |
27 | plugin.RegisterEventHandler((@event, info) =>
28 | {
29 | if (@event.Userid != null)
30 | Players[@event.Userid.Slot] = new MenuPlayer
31 | {
32 | player = @event.Userid,
33 | Buttons = 0
34 | };
35 | return HookResult.Continue;
36 | });
37 |
38 | plugin.RegisterEventHandler((@event, info) =>
39 | {
40 | if (@event.Userid != null) Players.Remove(@event.Userid.Slot);
41 | return HookResult.Continue;
42 | });
43 |
44 | plugin.RegisterListener(OnTick);
45 | }
46 |
47 | internal static void OnTick()
48 | {
49 | foreach (var player in Players.Values.Where(p => p.MainMenu != null))
50 | {
51 | if ((player.Buttons & PlayerButtons.Forward) == 0 && (player.player.Buttons & PlayerButtons.Forward) != 0)
52 | {
53 | player.ScrollUp();
54 | }
55 | else if ((player.Buttons & PlayerButtons.Back) == 0 && (player.player.Buttons & PlayerButtons.Back) != 0)
56 | {
57 | player.ScrollDown();
58 | }
59 | else if ((player.Buttons & PlayerButtons.Jump) == 0 && (player.player.Buttons & PlayerButtons.Jump) != 0)
60 | {
61 | player.Choose();
62 | }
63 | else if ((player.Buttons & PlayerButtons.Use) == 0 && (player.player.Buttons & PlayerButtons.Use) != 0)
64 | {
65 | player.Choose();
66 | }
67 |
68 | if (((long)player.player.Buttons & 8589934592) == 8589934592)
69 | {
70 | player.OpenMainMenu(null);
71 | }
72 |
73 | player.Buttons = player.player.Buttons;
74 | if (player.CenterHtml != "")
75 | Server.NextFrame(() =>
76 | player.player.PrintToCenterHtml(player.CenterHtml)
77 | );
78 | }
79 | }
80 |
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/MenuManager.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 |
3 | namespace WarcraftPlugin.Menu;
4 |
5 | internal static class MenuManager
6 | {
7 | internal static void OpenMainMenu(CCSPlayerController player, Menu menu, int selectedOptionIndex = 0)
8 | {
9 | if (player == null)
10 | return;
11 | MenuAPI.Players[player.Slot].OpenMainMenu(menu, selectedOptionIndex);
12 | }
13 |
14 | internal static void CloseMenu(CCSPlayerController player)
15 | {
16 | if (player == null)
17 | return;
18 | MenuAPI.Players[player.Slot].OpenMainMenu(null);
19 | }
20 |
21 | internal static void CloseSubMenu(CCSPlayerController player)
22 | {
23 | if (player == null)
24 | return;
25 | MenuAPI.Players[player.Slot].CloseSubMenu();
26 | }
27 |
28 | internal static void CloseAllSubMenus(CCSPlayerController player)
29 | {
30 | if (player == null)
31 | return;
32 | MenuAPI.Players[player.Slot].CloseAllSubMenus();
33 | }
34 |
35 | internal static void OpenSubMenu(CCSPlayerController player, Menu menu)
36 | {
37 | if (player == null)
38 | return;
39 | MenuAPI.Players[player.Slot].OpenSubMenu(menu);
40 | }
41 |
42 | internal static Menu CreateMenu(string title = "", int resultsBeforePaging = 4)
43 | {
44 | Menu menu = new()
45 | {
46 | Title = title,
47 | ResultsBeforePaging = resultsBeforePaging,
48 | };
49 | return menu;
50 | }
51 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/MenuOption.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System;
3 |
4 | namespace WarcraftPlugin.Menu;
5 |
6 | internal class MenuOption
7 | {
8 | internal Menu Parent { get; set; }
9 | internal string OptionDisplay { get; set; }
10 | internal string SubOptionDisplay { get; set; }
11 | internal Action OnChoose { get; set; }
12 | internal int Index { get; set; }
13 | internal Action OnSelect { get; set; }
14 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/MenuPlayer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text;
3 | using System.Text.RegularExpressions;
4 | using CounterStrikeSharp.API;
5 | using CounterStrikeSharp.API.Core;
6 | using Microsoft.Extensions.Localization;
7 | using WarcraftPlugin.Helpers;
8 |
9 | namespace WarcraftPlugin.Menu;
10 |
11 | internal class MenuPlayer
12 | {
13 | internal CCSPlayerController player { get; set; }
14 | internal Menu MainMenu = null;
15 | internal LinkedListNode CurrentChoice = null;
16 | internal LinkedListNode MenuStart = null;
17 | internal string CenterHtml = "";
18 | internal int VisibleOptions = 5;
19 | internal static IStringLocalizer Localizer = WarcraftPlugin.Instance.Localizer;
20 | internal PlayerButtons Buttons { get; set; }
21 |
22 | internal void OpenMainMenu(Menu menu, int selectedOptionIndex = 0)
23 | {
24 | player.DisableMovement();
25 |
26 | if (menu == null)
27 | {
28 | player.EnableMovement();
29 | MainMenu = null;
30 | CurrentChoice = null;
31 | CenterHtml = "";
32 | return;
33 | }
34 | MainMenu = menu;
35 | VisibleOptions = menu.ResultsBeforePaging;
36 | MenuStart = MainMenu.Options?.First;
37 | CurrentChoice = MenuStart;
38 |
39 | //Set the selected option based on index
40 | for (int i = 0; i < selectedOptionIndex; i++)
41 | {
42 | CurrentChoice = CurrentChoice.Next;
43 | }
44 |
45 | CurrentChoice?.Value.OnSelect?.Invoke(player, CurrentChoice.Value);
46 |
47 | UpdateCenterHtml();
48 | }
49 |
50 | internal void OpenSubMenu(Menu menu)
51 | {
52 | if (menu == null)
53 | {
54 | CurrentChoice = MainMenu?.Options?.First;
55 | MenuStart = CurrentChoice;
56 | UpdateCenterHtml();
57 | return;
58 | }
59 |
60 | VisibleOptions = menu.ResultsBeforePaging;
61 | CurrentChoice = menu.Options?.First;
62 | MenuStart = CurrentChoice;
63 | UpdateCenterHtml();
64 | }
65 | internal void GoBackToPrev(LinkedListNode menu)
66 | {
67 | if (menu == null)
68 | {
69 | CurrentChoice = MainMenu?.Options?.First;
70 | MenuStart = CurrentChoice;
71 | UpdateCenterHtml();
72 | return;
73 | }
74 |
75 | VisibleOptions = menu.Value.Parent?.ResultsBeforePaging ?? 4;
76 | CurrentChoice = menu;
77 | if (CurrentChoice.Value.Index >= 5)
78 | {
79 | MenuStart = CurrentChoice;
80 | for (int i = 0; i < 4; i++)
81 | {
82 | MenuStart = MenuStart?.Previous;
83 | }
84 | }
85 | else
86 | MenuStart = CurrentChoice.List?.First;
87 | UpdateCenterHtml();
88 | }
89 |
90 | internal void CloseSubMenu()
91 | {
92 | if (CurrentChoice?.Value.Parent?.Prev == null)
93 | {
94 | if (player.PlayerPawn.Value != null && player.PlayerPawn.Value.IsValid)
95 | {
96 | player.EnableMovement();
97 | }
98 |
99 | return;
100 | }
101 | GoBackToPrev(CurrentChoice?.Value.Parent.Prev);
102 | }
103 |
104 | internal void CloseAllSubMenus()
105 | {
106 | OpenSubMenu(null);
107 | }
108 |
109 | internal void Choose()
110 | {
111 | CurrentChoice?.Value.OnChoose?.Invoke(player, CurrentChoice.Value);
112 | }
113 |
114 | internal void ScrollDown()
115 | {
116 | if (CurrentChoice == null || MainMenu == null)
117 | return;
118 | CurrentChoice = CurrentChoice.Next ?? CurrentChoice.List?.First;
119 | MenuStart = CurrentChoice!.Value.Index >= VisibleOptions ? MenuStart!.Next : CurrentChoice.List?.First;
120 |
121 | CurrentChoice?.Value.OnSelect?.Invoke(player, CurrentChoice.Value);
122 |
123 | UpdateCenterHtml();
124 | }
125 |
126 | internal void ScrollUp()
127 | {
128 | if (CurrentChoice == null || MainMenu == null)
129 | return;
130 | CurrentChoice = CurrentChoice.Previous ?? CurrentChoice.List?.Last;
131 | if (CurrentChoice == CurrentChoice?.List?.Last && CurrentChoice?.Value.Index >= VisibleOptions)
132 | {
133 | MenuStart = CurrentChoice;
134 | for (int i = 0; i < VisibleOptions - 1; i++)
135 | MenuStart = MenuStart?.Previous;
136 | }
137 | else
138 | MenuStart = CurrentChoice!.Value.Index >= VisibleOptions ? MenuStart!.Previous : CurrentChoice.List?.First;
139 |
140 | CurrentChoice?.Value.OnSelect?.Invoke(player, CurrentChoice.Value);
141 |
142 | UpdateCenterHtml();
143 | }
144 |
145 | private void UpdateCenterHtml()
146 | {
147 | if (CurrentChoice == null || MainMenu == null)
148 | return;
149 |
150 | StringBuilder builder = new StringBuilder();
151 | int i = 0;
152 | LinkedListNode option = MenuStart!;
153 | builder.AppendLine($"{option.Value.Parent?.Title}
");
154 |
155 | while (i < VisibleOptions && option != null)
156 | {
157 | if (option == CurrentChoice)
158 | {
159 | builder.AppendLine($"{Localizer?["menu.selection.left"]} {option.Value.OptionDisplay} {Localizer?["menu.selection.right"]}
");
160 | if (option.Value.SubOptionDisplay != null) builder.AppendLine($"{option.Value.SubOptionDisplay}
");
161 | }
162 | else
163 | {
164 | builder.AppendLine($"{option.Value.OptionDisplay}
");
165 | }
166 | option = option.Next;
167 | i++;
168 | }
169 |
170 | if (option != null)
171 | {
172 | builder.AppendLine($"{Localizer?["menu.more.options.below"]}");
173 | }
174 | if (option == null && MenuStart.List.Count > VisibleOptions)
175 | {
176 | builder.AppendLine($"
");
177 | }
178 |
179 | if (CurrentChoice?.Value?.SubOptionDisplay != null)
180 | {
181 | var subOptionTextSpace = CalculateTextSpace(CurrentChoice?.Value?.SubOptionDisplay);
182 | if (subOptionTextSpace < 56)
183 | {
184 | builder.AppendLine($"ㅤ
");
185 | }
186 | else
187 | {
188 | builder.AppendLine($"ㅤ
");
189 | }
190 | }
191 |
192 | var selectKey = player.IsAlive() ? Localizer["menu.option.select"] : Localizer["menu.option.select.dead"];
193 | builder.AppendLine($"{Localizer["menu.navigate"]}: {Localizer["menu.option.up"]} {Localizer["menu.option.down"]} | {Localizer["menu.select"]}: {selectKey} | {Localizer["menu.exit"]}: {Localizer["menu.option.exit"]}");
194 | builder.AppendLine("
");
195 | CenterHtml = builder.ToString();
196 | }
197 |
198 | private static int CalculateTextSpace(string subOptionDisplay)
199 | {
200 | // Use a regular expression to remove all HTML-like tags
201 | string pattern = @"<[^>]+>";
202 | string cleanedString = Regex.Replace(subOptionDisplay, pattern, string.Empty);
203 |
204 | // Return the length of the cleaned string
205 | return cleanedString.Length;
206 | }
207 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/WarcraftMenu/ClassMenu.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Drawing;
5 | using System.Linq;
6 | using WarcraftPlugin.Core;
7 | using WarcraftPlugin.Helpers;
8 |
9 | namespace WarcraftPlugin.Menu.WarcraftMenu
10 | {
11 | internal static class ClassMenu
12 | {
13 | internal static void Show(CCSPlayerController player, List classInformations)
14 | {
15 | var plugin = WarcraftPlugin.Instance;
16 |
17 | // Create a dictionary for fast lookups by InternalName
18 | var classInfoDict = classInformations?.ToDictionary(x => x.RaceName, x => x);
19 |
20 | var warcraftClassInformations = new List();
21 | foreach (var warcraftClass in plugin.classManager.GetAllClasses())
22 | {
23 | // Try to get the class information from the dictionary
24 | classInfoDict.TryGetValue(warcraftClass.InternalName, out var classInformation);
25 |
26 | // Add to warcraftClassInformations with classInformation if found
27 | warcraftClassInformations.Add(new WarcraftClassInformation()
28 | {
29 | DisplayName = warcraftClass?.LocalizedDisplayName,
30 | InternalName = warcraftClass?.InternalName,
31 | CurrentLevel = classInformation != null ? classInformation.CurrentLevel : 1,
32 | CurrentXp = classInformation?.CurrentXp ?? 0,
33 | DefaultColor = warcraftClass.DefaultColor,
34 | TotalLevelRequired = WarcraftPlugin.Instance.Config.TotalLevelRequired
35 | .FirstOrDefault(x => x.Key.Equals(warcraftClass.InternalName, StringComparison.OrdinalIgnoreCase)
36 | || x.Key.Equals(warcraftClass.DisplayName, StringComparison.OrdinalIgnoreCase)
37 | || x.Key.Equals(warcraftClass.LocalizedDisplayName, StringComparison.OrdinalIgnoreCase)).Value
38 | });
39 | }
40 |
41 | var totalLevels = warcraftClassInformations.Sum(x => x.CurrentLevel);
42 |
43 | var classMenu = MenuManager.CreateMenu(@$"{plugin.Localizer["menu.class"]}
{plugin.Localizer["menu.class.total.levels"]} ({totalLevels})", 5);
44 |
45 | foreach (var warClassInformation in warcraftClassInformations
46 | .OrderByDescending(x => x.CurrentLevel)
47 | .ThenByDescending(x => x.CurrentXp)
48 | .ThenBy(x => x.TotalLevelRequired)
49 | .ThenBy(x => x.DisplayName))
50 | {
51 | if (!WarcraftPlugin.Instance.classManager.GetAllClasses().Any(x => x.InternalName == warClassInformation.InternalName))
52 | {
53 | continue;
54 | }
55 |
56 | var levelColor = TransitionToGold(warClassInformation.CurrentLevel / WarcraftPlugin.MaxLevel);
57 |
58 | var isCurrentClass = player.GetWarcraftPlayer().className == warClassInformation.InternalName;
59 | // Check if the class is locked based on total levels required
60 | var isLocked = warClassInformation.TotalLevelRequired >= totalLevels;
61 | var classDisplayColor = isLocked || isCurrentClass ? Color.Gray.Name : "white";
62 |
63 | var sb = new System.Text.StringBuilder();
64 | // Class colored bracket [
65 | sb.Append($"(");
66 | // Class colored name
67 | sb.Append($"{warClassInformation.DisplayName}");
68 | // Class colored bracket ]
69 | sb.Append($")");
70 | // Level information
71 | if (isLocked)
72 | sb.Append($" - {plugin.Localizer["menu.class.locked", $"{warClassInformation.TotalLevelRequired}"]}");
73 | else
74 | sb.Append($" - {plugin.Localizer["menu.class.level"]} {warClassInformation.CurrentLevel}");
75 |
76 | var displayString = sb.ToString();
77 |
78 | var classInternalName = warClassInformation.InternalName;
79 |
80 | classMenu.Add(displayString, null, (p, opt) =>
81 | {
82 | if (!isCurrentClass && !isLocked)
83 | {
84 | p.PlayLocalSound("sounds/buttons/button9.vsnd");
85 | MenuManager.CloseMenu(player);
86 |
87 | if (player.IsValid)
88 | {
89 | if (!player.PawnIsAlive)
90 | {
91 | plugin.ChangeClass(player, classInternalName);
92 | }
93 | else
94 | {
95 | player.GetWarcraftPlayer().DesiredClass = classInternalName;
96 | player.PrintToChat($" {plugin.Localizer["class.pending.change", warClassInformation.DisplayName]}");
97 | }
98 | }
99 |
100 | }
101 | else
102 | {
103 | p.PlayLocalSound("sounds/ui/menu_invalid.vsnd");
104 | }
105 | });
106 | }
107 |
108 | MenuManager.OpenMainMenu(player, classMenu);
109 | }
110 |
111 | internal static Color TransitionToGold(float t)
112 | {
113 | // Ensure t is clamped between 0 and 1
114 | t = Math.Clamp(t, 0.1f, 1.0f);
115 |
116 | // Define the light grey and gold colors
117 | Color lightGrey = Color.FromArgb(240, 240, 240); // Light grey (211, 211, 211)
118 | Color gold = Color.FromArgb(255, 215, 0); // Gold (255, 215, 0)
119 |
120 | // Linearly interpolate between the two colors
121 | int r = (int)(lightGrey.R + (gold.R - lightGrey.R) * t);
122 | int g = (int)(lightGrey.G + (gold.G - lightGrey.G) * t);
123 | int b = (int)(lightGrey.B + (gold.B - lightGrey.B) * t);
124 |
125 | // Return the new interpolated color
126 | return Color.FromArgb(r, g, b);
127 | }
128 | }
129 |
130 | internal class WarcraftClassInformation
131 | {
132 | internal string DisplayName { get; set; }
133 | internal string InternalName { get; set; }
134 | internal int CurrentLevel { get; set; }
135 | internal float CurrentXp { get; set; }
136 | internal Color DefaultColor { get; set; }
137 | public int TotalLevelRequired { get; set; }
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Menu/WarcraftMenu/SkillsMenu.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using WarcraftPlugin.Core;
3 | using WarcraftPlugin.Helpers;
4 | using WarcraftPlugin.Models;
5 |
6 | namespace WarcraftPlugin.Menu.WarcraftMenu
7 | {
8 | internal static class SkillsMenu
9 | {
10 | internal static void Show(WarcraftPlayer wcPlayer, int selectedOptionIndex = 0)
11 | {
12 | var plugin = WarcraftPlugin.Instance;
13 |
14 | var warcraftClass = wcPlayer.GetClass();
15 |
16 | var skillsMenu = MenuManager.CreateMenu(@$"{warcraftClass.LocalizedDisplayName} - {plugin.Localizer["menu.skills.level"]} {wcPlayer.GetLevel()}
17 | {plugin.Localizer["menu.skills.available", XpSystem.GetFreeSkillPoints(wcPlayer)]}");
18 |
19 | for (int i = 0; i < warcraftClass.Abilities.Count; i++)
20 | {
21 | var ability = warcraftClass.GetAbility(i);
22 | var abilityLevel = wcPlayer.GetAbilityLevel(i);
23 | var maxAbilityLevel = WarcraftPlayer.GetMaxAbilityLevel(i);
24 |
25 | var isUltimate = i == 3;
26 | var isDisabled = false;
27 |
28 | if (abilityLevel == maxAbilityLevel || XpSystem.GetFreeSkillPoints(wcPlayer) == 0)
29 | {
30 | isDisabled = true;
31 | }
32 |
33 | var color = isDisabled ? Color.Gray : Color.White;
34 |
35 | var abilityLevelColor = abilityLevel > 0 ? "#90EE90" : "white";
36 |
37 | var abilityProgressString = (abilityLevel == maxAbilityLevel)
38 | ? $"({abilityLevel}/{maxAbilityLevel})"
39 | : $"(" +
40 | $"{abilityLevel}" +
41 | $"/{maxAbilityLevel})";
42 |
43 | var displayString = $"{ability.DisplayName} {abilityProgressString}";
44 |
45 | if (isUltimate && abilityLevel != maxAbilityLevel) //Ultimate ability
46 | {
47 | if (wcPlayer.IsMaxLevel)
48 | {
49 | color = Color.MediumPurple;
50 | displayString = $"{ability.DisplayName} {abilityProgressString}";
51 | }
52 | else
53 | {
54 | isDisabled = true;
55 | color = Color.Gray;
56 | displayString = $"{ability.DisplayName} (level 16)";
57 | }
58 | }
59 |
60 | var subDisplayString = $"{ability.Description}";
61 |
62 | var abilityIndex = i;
63 | skillsMenu.Add(displayString, subDisplayString, (p, opt) =>
64 | {
65 | if (!isDisabled)
66 | {
67 | wcPlayer.GrantAbilityLevel(abilityIndex);
68 | }
69 | else
70 | {
71 | p.PlayLocalSound("sounds/ui/menu_invalid.vsnd");
72 | }
73 |
74 | Show(wcPlayer, opt.Index);
75 | });
76 | }
77 |
78 | MenuManager.OpenMainMenu(wcPlayer.Player, skillsMenu, selectedOptionIndex);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Models/DefaultClassModel.cs:
--------------------------------------------------------------------------------
1 | namespace WarcraftPlugin.Models
2 | {
3 | public class DefaultClassModel
4 | {
5 | public string TModel { get; set; }
6 | public string CTModel { get; set; }
7 | }
8 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Models/GameAction.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Events;
3 | using System;
4 |
5 | namespace WarcraftPlugin.Models
6 | {
7 | internal class GameAction
8 | {
9 | public Type EventType { get; set; }
10 | public Action Handler { get; set; }
11 | public HookMode HookMode { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Models/KillFeedIcon.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace WarcraftPlugin.Models
3 | {
4 | public enum KillFeedIcon
5 | {
6 | ammobox,
7 | ammobox_threepack,
8 | armor,
9 | armor_helmet,
10 | assaultsuit,
11 | assaultsuit_helmet_only,
12 | aug,
13 | awp,
14 | axe,
15 | bayonet,
16 | bizon,
17 | breachcharge,
18 | breachcharge_projectile,
19 | bumpmine,
20 | c4,
21 | clothing_hands,
22 | controldrone,
23 | customplayer,
24 | cz75a,
25 | deagle,
26 | decoy,
27 | defuser,
28 | disconnect,
29 | diversion,
30 | dronegun,
31 | elite,
32 | famas,
33 | firebomb,
34 | fists,
35 | fiveseven,
36 | flair0,
37 | flashbang,
38 | flashbang_assist,
39 | frag_grenade,
40 | g3sg1,
41 | galilar,
42 | glock,
43 | grenadepack,
44 | grenadepack2,
45 | hammer,
46 | healthshot,
47 | heavy_armor,
48 | hegrenade,
49 | helmet,
50 | hkp2000,
51 | incgrenade,
52 | inferno,
53 | kevlar,
54 | knife,
55 | knife_bowie,
56 | knife_butterfly,
57 | knife_canis,
58 | knife_cord,
59 | knife_css,
60 | knife_falchion,
61 | knife_flip,
62 | knife_gut,
63 | knife_gypsy_jackknife,
64 | knife_karambit,
65 | knife_kukri,
66 | knife_m9_bayonet,
67 | knife_outdoor,
68 | knife_push,
69 | knife_skeleton,
70 | knife_stiletto,
71 | knife_survival_bowie,
72 | knife_t,
73 | knife_tactical,
74 | knife_twinblade,
75 | knife_ursus,
76 | knife_widowmaker,
77 | knifegg,
78 | m4a1,
79 | m4a1_silencer,
80 | m4a1_silencer_off,
81 | m249,
82 | mac10,
83 | mag7,
84 | melee,
85 | molotov,
86 | mp5sd,
87 | mp7,
88 | mp9,
89 | negev,
90 | nova,
91 | p90,
92 | p250,
93 | p2000,
94 | planted_c4,
95 | planted_c4_survival,
96 | prop_exploding_barrel,
97 | radarjammer,
98 | revolver,
99 | sawedoff,
100 | scar20,
101 | sg556,
102 | shield,
103 | smokegrenade,
104 | snowball,
105 | spanner,
106 | spray0,
107 | ssg08,
108 | stomp_damage,
109 | tablet,
110 | tagrenade,
111 | taser,
112 | tec9,
113 | tripwirefire,
114 | tripwirefire_projectile,
115 | ump45,
116 | usp_silencer,
117 | usp_silencer_off,
118 | xm1014,
119 | zone_repulsor
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Models/WarcraftClass.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CounterStrikeSharp.API.Core;
4 | using CounterStrikeSharp.API.Modules.Events;
5 | using System.Drawing;
6 | using WarcraftPlugin.Helpers;
7 | using CounterStrikeSharp.API.Modules.Utils;
8 | using WarcraftPlugin.Core;
9 | using System.Linq;
10 | using CounterStrikeSharp.API;
11 | using Microsoft.Extensions.Localization;
12 | using WarcraftPlugin.lang;
13 |
14 | namespace WarcraftPlugin.Models
15 | {
16 | public interface IWarcraftAbility
17 | {
18 | public string InternalName { get; }
19 | public string DisplayName { get; }
20 | public string Description { get; }
21 | }
22 |
23 | public class WarcraftAbility : IWarcraftAbility
24 | {
25 | public WarcraftAbility(string displayName, string description)
26 | {
27 | DisplayName = displayName;
28 | Description = description;
29 | }
30 |
31 | public string InternalName => DisplayName.Replace(' ', '_').ToLowerInvariant();
32 | public string DisplayName { get; }
33 | public string Description { get; }
34 | }
35 |
36 | public class WarcraftCooldownAbility : WarcraftAbility
37 | {
38 | private readonly Func _cooldownProvider;
39 |
40 | public float Cooldown => _cooldownProvider();
41 |
42 | // Constructor for dynamic cooldown
43 | public WarcraftCooldownAbility(string displayName, string description,
44 | Func cooldownProvider)
45 | : base(displayName, description)
46 | {
47 | _cooldownProvider = cooldownProvider ?? (() => 0f);
48 | }
49 |
50 | // Constructor for static cooldown
51 | public WarcraftCooldownAbility(string displayName, string description,
52 | float cooldown)
53 | : this(displayName, description, () => cooldown)
54 | {
55 | }
56 | }
57 |
58 |
59 | public abstract class WarcraftClass
60 | {
61 | public string InternalName => DisplayName.Replace(' ', '_').ToLowerInvariant();
62 | public abstract string DisplayName { get; }
63 | public string LocalizedDisplayName => Localizer.Exists(InternalName) ? Localizer[InternalName] : DisplayName;
64 | public virtual DefaultClassModel DefaultModel { get; } = new DefaultClassModel();
65 | public abstract Color DefaultColor { get; }
66 | public WarcraftPlayer WarcraftPlayer { get; set; }
67 | public CCSPlayerController Player { get; set; }
68 |
69 | public abstract List Abilities { get; }
70 | private readonly Dictionary _eventHandlers = [];
71 | private readonly Dictionary _abilityHandlers = [];
72 |
73 | private float _killFeedIconTick;
74 | private KillFeedIcon? _killFeedIcon;
75 | public float LastHurtOther { get; set; } = 0;
76 |
77 | public abstract void Register();
78 |
79 | public virtual void PlayerChangingToAnotherRace() { SetDefaultAppearance(); }
80 |
81 | public virtual List PreloadResources { get; } = [];
82 | public readonly IStringLocalizer Localizer = WarcraftPlugin.Instance.Localizer;
83 |
84 | public void SetDefaultAppearance()
85 | {
86 | Player.PlayerPawn.Value.SetColor(GenerateShade(DefaultColor, Player.GetWarcraftPlayer().currentLevel));
87 |
88 | var model = Player.Team == CsTeam.CounterTerrorist ? DefaultModel?.CTModel : DefaultModel?.TModel;
89 |
90 | if (model != null && model != string.Empty)
91 | {
92 | Player.PlayerPawn.Value.SetModel(model);
93 | }
94 | }
95 |
96 | private static Color GenerateShade(Color baseColor, int shadeIndex)
97 | {
98 | if (shadeIndex < 1 || shadeIndex > 16)
99 | {
100 | return Color.White;
101 | }
102 |
103 | // Convert 1-based index to 0-based index
104 | int i = shadeIndex - 1;
105 |
106 | // Calculate the blend ratio
107 | double ratio = i / 15.0;
108 |
109 | // Interpolate between white and the base color
110 | int r = (int)(255 * (1 - ratio) + baseColor.R * ratio);
111 | int g = (int)(255 * (1 - ratio) + baseColor.G * ratio);
112 | int b = (int)(255 * (1 - ratio) + baseColor.B * ratio);
113 |
114 | // Ensure the values are within the valid range
115 | r = Math.Clamp(r, 0, 255);
116 | g = Math.Clamp(g, 0, 255);
117 | b = Math.Clamp(b, 0, 255);
118 |
119 | return Color.FromArgb(r, g, b);
120 | }
121 |
122 | public IWarcraftAbility GetAbility(int index)
123 | {
124 | var ability = Abilities[index];
125 | var abilityNameKey = $"{InternalName}.ability.{index}";
126 | var abilityDescriptionKey = $"{InternalName}.ability.{index}.description";
127 |
128 | return new WarcraftAbility(
129 | Localizer.Exists(abilityNameKey) ? Localizer[abilityNameKey] : ability.DisplayName,
130 | Localizer.Exists(abilityDescriptionKey) ? Localizer[abilityDescriptionKey] : ability.Description
131 | );
132 | }
133 |
134 | protected void HookEvent(Action handler, HookMode hookMode = HookMode.Pre) where T : GameEvent
135 | {
136 | _eventHandlers[typeof(T).Name + (hookMode == HookMode.Pre ? "-pre" : "")] = new GameAction
137 | {
138 | EventType = typeof(T),
139 | Handler = (evt) =>
140 | {
141 | if (evt is T typedEvent)
142 | {
143 | handler(typedEvent);
144 | }
145 | else
146 | {
147 | handler((T)evt);
148 | Console.WriteLine($"Handler for event expects an event of type {typeof(T).Name}.");
149 | }
150 | },
151 | HookMode = hookMode
152 | };
153 | }
154 |
155 | protected void HookAbility(int abilityIndex, Action handler)
156 | {
157 | _abilityHandlers[abilityIndex] = handler;
158 | }
159 |
160 | public void InvokeEvent(GameEvent @event, HookMode hookMode = HookMode.Post)
161 | {
162 | //Console.WriteLine($"Invoking event {@event.GetType().Name + (hookMode == HookMode.Pre ? "-pre" : "")}");
163 | if (_eventHandlers.TryGetValue(@event.GetType().Name + (hookMode == HookMode.Pre ? "-pre" : ""), out GameAction gameAction))
164 | {
165 | gameAction.Handler.Invoke(@event);
166 | }
167 | }
168 |
169 | internal List GetEventListeners()
170 | {
171 | return _eventHandlers.Values.ToList();
172 | }
173 |
174 | public bool IsAbilityReady(int abilityIndex)
175 | {
176 | return CooldownManager.IsAvailable(WarcraftPlayer, abilityIndex);
177 | }
178 |
179 | public float AbilityCooldownRemaining(int abilityIndex)
180 | {
181 | return CooldownManager.Remaining(WarcraftPlayer, abilityIndex);
182 | }
183 |
184 | public void StartCooldown(int abilityIndex, float? customCooldown = null)
185 | {
186 | var ability = Abilities[abilityIndex];
187 |
188 | if (ability is WarcraftCooldownAbility cooldownAbility)
189 | CooldownManager.StartCooldown(WarcraftPlayer, abilityIndex, customCooldown ?? cooldownAbility.Cooldown);
190 | }
191 |
192 | public void InvokeAbility(int abilityIndex)
193 | {
194 | if (_abilityHandlers.TryGetValue(abilityIndex, out Action value))
195 | {
196 | value.Invoke();
197 | }
198 | }
199 |
200 | public void ResetCooldowns()
201 | {
202 | CooldownManager.ResetCooldowns(WarcraftPlayer);
203 | }
204 |
205 | public void SetKillFeedIcon(KillFeedIcon? damageType)
206 | {
207 | _killFeedIconTick = Server.CurrentTime;
208 | _killFeedIcon = damageType;
209 | }
210 |
211 | public KillFeedIcon? GetKillFeedIcon()
212 | {
213 | return _killFeedIcon;
214 | }
215 |
216 | public void ResetKillFeedIcon()
217 | {
218 | _killFeedIcon = null;
219 | }
220 |
221 | public float GetKillFeedTick()
222 | {
223 | return _killFeedIconTick;
224 | }
225 | }
226 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Models/WarcraftPlayer.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using System.Collections.Generic;
3 | using WarcraftPlugin.Core;
4 | using WarcraftPlugin.Helpers;
5 |
6 | namespace WarcraftPlugin.Models
7 | {
8 | public class WarcraftPlayer
9 | {
10 | private int _playerIndex;
11 | internal int Index => _playerIndex;
12 | internal bool IsMaxLevel => currentLevel == WarcraftPlugin.MaxLevel;
13 | internal CCSPlayerController GetPlayer() => Player;
14 |
15 | internal CCSPlayerController Player { get; init; }
16 |
17 | internal string DesiredClass { get; set; }
18 |
19 | internal int currentXp;
20 | internal int currentLevel;
21 | internal int amountToLevel;
22 | internal string className;
23 |
24 | private readonly List _abilityLevels = new(new int[4]);
25 | internal List AbilityCooldowns = new(new float[4]);
26 |
27 | private WarcraftClass _class;
28 |
29 | internal WarcraftPlayer(CCSPlayerController player)
30 | {
31 | Player = player;
32 | }
33 |
34 | internal void LoadClassInformation(ClassInformation dbRace, XpSystem xpSystem)
35 | {
36 | currentLevel = dbRace.CurrentLevel;
37 | currentXp = dbRace.CurrentXp;
38 | className = dbRace.RaceName;
39 | amountToLevel = xpSystem.GetXpForLevel(currentLevel);
40 |
41 | _abilityLevels[0] = dbRace.Ability1Level;
42 | _abilityLevels[1] = dbRace.Ability2Level;
43 | _abilityLevels[2] = dbRace.Ability3Level;
44 | _abilityLevels[3] = dbRace.Ability4Level;
45 |
46 | _class = WarcraftPlugin.Instance.classManager.InstantiateClassByName(className);
47 | _class.WarcraftPlayer = this;
48 | _class.Player = Player;
49 | }
50 |
51 | public int GetLevel()
52 | {
53 | if (currentLevel > WarcraftPlugin.MaxLevel) return WarcraftPlugin.MaxLevel;
54 |
55 | return currentLevel;
56 | }
57 |
58 | public override string ToString()
59 | {
60 | return
61 | $"[{_playerIndex}]: {{raceName={className}, currentLevel={currentLevel}, currentXp={currentXp}, amountToLevel={amountToLevel}}}";
62 | }
63 |
64 | public int GetAbilityLevel(int abilityIndex)
65 | {
66 | return _abilityLevels[abilityIndex];
67 | }
68 |
69 | public static int GetMaxAbilityLevel(int abilityIndex)
70 | {
71 | return abilityIndex == 3 ? 1 : WarcraftPlugin.MaxSkillLevel;
72 | }
73 |
74 | public void SetAbilityLevel(int abilityIndex, int value)
75 | {
76 | _abilityLevels[abilityIndex] = value;
77 | }
78 |
79 | public WarcraftClass GetClass()
80 | {
81 | return _class;
82 | }
83 |
84 | public void GrantAbilityLevel(int abilityIndex)
85 | {
86 | Player.PlayLocalSound("sounds/buttons/button9.vsnd");
87 | _abilityLevels[abilityIndex] += 1;
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyTitle("WarcraftPlugin")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("WarcraftPlugin")]
12 | [assembly: AssemblyCopyright("Copyright © 2020")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Setting ComVisible to false makes the types in this assembly not visible
17 | // to COM components. If you need to access a type in this assembly from
18 | // COM, set the ComVisible attribute to true on that type.
19 | [assembly: ComVisible(false)]
20 |
21 | // The following GUID is for the ID of the typelib if this project is exposed to COM
22 | [assembly: Guid("207d464d-d53e-4054-a94c-7565351af8ff")]
23 |
24 | // Version information for an assembly consists of the following four values:
25 | //
26 | // Major Version
27 | // Minor Version
28 | // Build Number
29 | // Revision
30 | //
31 | // You can specify all the values or you can default the Build and Revision Numbers
32 | // by using the '*' as shown below:
33 | // [assembly: AssemblyVersion("1.0.*")]
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/WarcraftPlugin/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "WarcraftPlugin": {
4 | "commandName": "Project",
5 | "nativeDebugging": true
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Resources/arrow-down.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wngui/CS2WarcraftMod/cbcc4a622d453bbf3e735f64d4fb2edfc60feaa1/WarcraftPlugin/Resources/arrow-down.gif
--------------------------------------------------------------------------------
/WarcraftPlugin/Resources/nuget/Readme.md:
--------------------------------------------------------------------------------
1 | An open-source Warcraft mod for CS2 featuring a fully-fledged RPG system
--------------------------------------------------------------------------------
/WarcraftPlugin/Resources/nuget/wc-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wngui/CS2WarcraftMod/cbcc4a622d453bbf3e735f64d4fb2edfc60feaa1/WarcraftPlugin/Resources/nuget/wc-icon.png
--------------------------------------------------------------------------------
/WarcraftPlugin/Summons/Drone.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API.Core;
2 | using CounterStrikeSharp.API.Modules.Utils;
3 | using CounterStrikeSharp.API;
4 | using System.Drawing;
5 | using System;
6 | using WarcraftPlugin.Helpers;
7 | using CounterStrikeSharp.API.Modules.Memory;
8 | using CounterStrikeSharp.API.Modules.Timers;
9 | using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
10 | using WarcraftPlugin.Models;
11 |
12 | namespace WarcraftPlugin.Summons
13 | {
14 | internal class Drone
15 | {
16 | private CPhysicsPropMultiplayer _drone;
17 | private CDynamicProp _model;
18 | private CDynamicProp _turret;
19 | internal Vector Position { get; set; } = new(70, -70, 90);
20 |
21 | internal bool IsFireRateCooldown { get; set; } = false;
22 | private readonly float _fireRate = 2f;
23 | private Timer _fireRateTimer;
24 | private CBeam _lazerDot;
25 | private Vector _target;
26 | private readonly CCSPlayerController _owner;
27 |
28 | internal float Angle { get; set; } = 0f;
29 |
30 | internal Drone(CCSPlayerController owner, Vector position)
31 | {
32 | _owner = owner;
33 | Position = position;
34 | Activate();
35 | }
36 |
37 | private void Activate()
38 | {
39 | Deactivate();
40 |
41 | //Spawn animation
42 | var droneSpawnAnimation = Warcraft.SpawnParticle(_owner.CalculatePositionInFront(Position), "particles/ui/ui_electric_gold.vpcf");
43 | droneSpawnAnimation.SetParent(_owner.PlayerPawn.Value);
44 |
45 | //Create drone physics object
46 | _drone = Utilities.CreateEntityByName("prop_physics_multiplayer");
47 | _drone.SetColor(Color.FromArgb(0, 255, 255, 255));
48 | _drone.SetModel("models/props/de_dust/hr_dust/dust_soccerball/dust_soccer_ball001.vmdl");
49 | _drone.DispatchSpawn();
50 |
51 | //Create drone body
52 | _model = Utilities.CreateEntityByName("prop_dynamic");
53 | _model.SetModel("models/props/de_dust/hr_dust/dust_soccerball/dust_soccer_ball001.vmdl");
54 | _model.SetColor(Color.FromArgb(255, 0, 0, 0));
55 | _model.DispatchSpawn();
56 |
57 | //Create drone turret
58 | _turret = Utilities.CreateEntityByName("prop_dynamic");
59 | _turret.SetModel("models/tools/bullet_hit_marker.vmdl");
60 | _turret.DispatchSpawn();
61 |
62 | //Attach drone turret to body
63 | _turret.SetParent(_model, offset: new Vector(2, 2, 2), rotation: new QAngle(0, 310, 0));
64 | _turret.CBodyComponent.SceneNode.GetSkeletonInstance().Scale = 0.8f;
65 | _turret.SetColor(Color.FromArgb(255, 0, 0, 0));
66 |
67 | //Attach drone body to physics object
68 | _model.SetParent(_drone, rotation: new QAngle(175, 30, 0));
69 | _model.CBodyComponent.SceneNode.GetSkeletonInstance().Scale = 0.8f;
70 |
71 | _drone.Teleport(_owner.CalculatePositionInFront(Position), _owner.PlayerPawn.Value.V_angle, new Vector(nint.Zero));
72 | }
73 |
74 | internal void Deactivate()
75 | {
76 | _target = null;
77 |
78 | _turret?.RemoveIfValid();
79 | _model?.RemoveIfValid();
80 | _drone?.RemoveIfValid();
81 | _lazerDot?.RemoveIfValid();
82 |
83 | IsFireRateCooldown = false;
84 | }
85 |
86 | internal void Update()
87 | {
88 | if (!_owner.IsValid || !_drone.IsValid) return;
89 | var nextDronePosition = _owner.CalculatePositionInFront(Position);
90 | Vector velocity = Warcraft.CalculateTravelVelocity(_drone.AbsOrigin, nextDronePosition, 0.5f);
91 | _drone.Teleport(null, _owner.PlayerPawn.Value.V_angle, velocity);
92 |
93 | //Ensure drone is not stuck
94 | float droneDistanceToPlayer = (_owner.PlayerPawn.Value.AbsOrigin - _drone.AbsOrigin).Length();
95 | if (droneDistanceToPlayer > 500) _drone.Teleport(_owner.CalculatePositionInFront(Position), _owner.PlayerPawn.Value.V_angle, new Vector(nint.Zero));
96 |
97 | //Update laser to point at target
98 | if (_target != null)
99 | {
100 | _lazerDot = Warcraft.DrawLaserBetween(_turret.CalculatePositionInFront(new Vector(0, 30, 2)), _target, Color.FromArgb(15, 255, 0, 0), 0.2f, 0.2f);
101 | }
102 | }
103 |
104 | internal void EnemySpotted(CCSPlayerController enemy)
105 | {
106 | if (!IsFireRateCooldown)
107 | {
108 | var droneLevel = _owner.GetWarcraftPlayer().GetAbilityLevel(0);
109 | var timesToShoot = droneLevel + 3;
110 |
111 | TryShootTarget(enemy);
112 |
113 | for (var i = 0; i < timesToShoot; i++)
114 | {
115 | int rocketMaxChance = 20;
116 |
117 | WarcraftPlugin.Instance.AddTimer((float)(0.2 * i), () => TryShootTarget(enemy, Warcraft.RollDice(droneLevel, rocketMaxChance)));
118 | }
119 | }
120 | }
121 |
122 | private void TryShootTarget(CCSPlayerController target, bool isRocket = false)
123 | {
124 | if (_turret != null && _turret.IsValid)
125 | {
126 | var playerCollison = target.PlayerPawn.Value.CollisionBox();
127 | //playerCollison.Show(); //debug
128 |
129 | //check if we have a clear line of sight to target
130 | var turretMuzzle = _turret.CalculatePositionInFront(new Vector(0, 30, 2));
131 | var endPos = RayTracer.Trace(turretMuzzle, playerCollison.Center.ToVector(), false);
132 |
133 | //ensure trace has hit the players hitbox
134 | if (endPos != null && playerCollison.Contains(endPos))
135 | {
136 | _target = endPos;
137 |
138 | if (!IsFireRateCooldown)
139 | {
140 | //start fireing cooldown
141 | IsFireRateCooldown = true;
142 | _fireRateTimer = WarcraftPlugin.Instance.AddTimer(_fireRate, () =>
143 | {
144 | IsFireRateCooldown = false; _target = null;
145 | });
146 | }
147 |
148 | if (isRocket)
149 | {
150 | FireRocket(turretMuzzle, endPos);
151 | }
152 | else
153 | {
154 | Shoot(turretMuzzle, target);
155 | }
156 | }
157 | else
158 | {
159 | _target = null;
160 | }
161 | }
162 | }
163 |
164 | private void Shoot(Vector muzzle, CCSPlayerController target)
165 | {
166 | //particle effect from turret
167 | Warcraft.SpawnParticle(muzzle, "particles/weapons/cs_weapon_fx/weapon_muzzle_flash_assaultrifle.vpcf", 1);
168 | _turret.EmitSound("Weapon_M4A1.Silenced");
169 |
170 | //dodamage to target
171 | target.TakeDamage(_owner.GetWarcraftPlayer().GetAbilityLevel(0) * 1, _owner, KillFeedIcon.controldrone);
172 | }
173 |
174 | private void FireRocket(Vector muzzle, Vector endPos)
175 | {
176 | var rocket = Utilities.CreateEntityByName("hegrenade_projectile");
177 |
178 | Vector velocity = Warcraft.CalculateTravelVelocity(_turret.AbsOrigin, endPos, 1);
179 |
180 | rocket.Teleport(muzzle, rocket.AbsRotation, velocity);
181 | rocket.DispatchSpawn();
182 | Schema.SetSchemaValue(rocket.Handle, "CBaseGrenade", "m_hThrower", _owner.PlayerPawn.Raw); //Fixes killfeed
183 |
184 | //Rocket popping out the tube
185 | Warcraft.SpawnParticle(rocket.AbsOrigin, "particles/explosions_fx/explosion_hegrenade_smoketrails.vpcf", 1);
186 | rocket.EmitSound("Weapon_Nova.Pump", volume: 0.5f);
187 |
188 | rocket.AcceptInput("InitializeSpawnFromWorld");
189 |
190 | rocket.Damage = 40;
191 | rocket.DmgRadius = 200;
192 | }
193 | }
194 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/Summons/Zombie.cs:
--------------------------------------------------------------------------------
1 | using CounterStrikeSharp.API;
2 | using CounterStrikeSharp.API.Core;
3 | using CounterStrikeSharp.API.Modules.Utils;
4 | using System;
5 | using System.Drawing;
6 | using WarcraftPlugin.Helpers;
7 | using WarcraftPlugin.Models;
8 |
9 | namespace WarcraftPlugin.Summons
10 | {
11 | internal class Zombie : IDisposable
12 | {
13 | private const int _interestMax = 6;
14 | private int InterestScore = _interestMax;
15 | private readonly int _radius = 50;
16 | private readonly double _leapCooldown = 1;
17 | private readonly int _damage = 10;
18 | private readonly int _maxHealth = 100;
19 |
20 | internal int FavouritePosition { get; set; } = 1;
21 | internal CChicken Entity { get; set; }
22 | internal CCSPlayerController Owner { get; }
23 | internal bool IsFollowingLeader { get; private set; }
24 | internal CCSPlayerController Target { get; set; }
25 | internal double LastLeapTick { get; private set; } = 0;
26 | internal double LastAttackTick { get; private set; } = 0;
27 |
28 | internal Zombie(CCSPlayerController owner)
29 | {
30 | Owner = owner;
31 | Entity = Utilities.CreateEntityByName("chicken");
32 |
33 | Entity.Teleport(Owner.CalculatePositionInFront(new Vector(Random.Shared.Next(200), Random.Shared.Next(200), 5)), new QAngle(), new Vector());
34 | Entity.DispatchSpawn();
35 | Entity.SetColor(Color.GreenYellow);
36 | Entity.CBodyComponent.SceneNode.GetSkeletonInstance().Scale = 2f;
37 | Entity.Health = _maxHealth;
38 |
39 | Warcraft.SpawnParticle(Entity.AbsOrigin.Clone().Add(z: 5), "particles/entity/env_explosion/test_particle_composite_dark_outline_smoke.vpcf");
40 |
41 | Entity.OwnerEntity.Raw = Owner.PlayerPawn.Raw;
42 | FollowLeader();
43 | }
44 |
45 | internal void Update()
46 | {
47 | if (Entity == null || !Entity.IsValid) return;
48 | if (!Owner.IsAlive()) Kill();
49 |
50 | if(InterestScore <= 0)
51 | {
52 | FollowLeader();
53 | }
54 |
55 | if (Target.IsAlive())
56 | {
57 | if (LastLeapTick == 0 || LastLeapTick + _leapCooldown + Random.Shared.NextDouble() < Server.TickedTime)
58 | {
59 | AttackLeap();
60 | }
61 | }
62 | else if(IsFollowingLeader)
63 | {
64 | //Ensure chicken is not stuck
65 | float chickenDistanceToPlayer = (Owner.PlayerPawn.Value.AbsOrigin - Entity.AbsOrigin).Length();
66 | if (chickenDistanceToPlayer > 500)
67 | {
68 | var chickenResetPoint = Owner.CalculatePositionInFront(new Vector(Random.Shared.Next(100), Random.Shared.Next(100), 5));
69 | Entity.AbsOrigin.X = chickenResetPoint.X;
70 | Entity.AbsOrigin.Y = chickenResetPoint.Y;
71 | Entity.AbsOrigin.Z = Owner.PlayerPawn.Value.AbsOrigin.Z+5;
72 | Warcraft.SpawnParticle(Entity.AbsOrigin.Clone().Add(z: -50), "particles/entity/env_explosion/test_particle_composite_dark_outline_smoke.vpcf");
73 | return;
74 | }
75 | Vector velocity = CircularGetVelocityToPosition(Owner.PlayerPawn.Value.AbsOrigin, Entity.AbsOrigin);
76 |
77 | //Give them a boost so their little chicken feet can keep up
78 | Entity.AbsVelocity.X = Math.Clamp(velocity.X, -300, 300);
79 | Entity.AbsVelocity.Y = Math.Clamp(velocity.Y, -300, 300);
80 | Entity.AbsVelocity.Z = 10;
81 | }
82 | }
83 |
84 | private Vector CircularGetVelocityToPosition(Vector circleTarget, Vector zombie, int radius = 50)
85 | {
86 | // Calculate the angle in radians (map input 1-100 to 0-2π)
87 | double angle = (FavouritePosition - 1) / 99.0 * 2 * Math.PI;
88 |
89 | // Calculate x and y offsets based on the angle and radius
90 | float offsetX = (float)(_radius * Math.Cos(angle));
91 | float offsetY = (float)(_radius * Math.Sin(angle));
92 |
93 | // Add these offsets to the owner's position
94 | Vector targetPosition = circleTarget.Clone()
95 | .Add(x: offsetX, y: offsetY);
96 |
97 | // Calculate the travel velocity
98 | Vector velocity = Warcraft.CalculateTravelVelocity(zombie, targetPosition, 1);
99 | return velocity;
100 | }
101 |
102 | private void AttackLeap()
103 | {
104 | LastLeapTick = Server.TickedTime;
105 | Attack();
106 |
107 | //Leap logic
108 | Vector velocity = Warcraft.CalculateTravelVelocity(Entity.AbsOrigin, Target.PlayerPawn.Value.AbsOrigin, 1);
109 |
110 | Entity.AbsVelocity.Z = 400;
111 | Entity.AbsVelocity.X = Math.Clamp(velocity.X, -1000, 1000);
112 | Entity.AbsVelocity.Y = Math.Clamp(velocity.Y, -1000, 1000);
113 | }
114 |
115 | private void Attack()
116 | {
117 | var playerCollison = Target.PlayerPawn.Value.Collision.ToBox(Target.PlayerPawn.Value.AbsOrigin.Clone().Add(z: -60));
118 |
119 | //Check if zombie is inside targets collision box
120 | if (playerCollison.Contains(Entity.AbsOrigin))
121 | {
122 | //dodamage to target
123 | Target.TakeDamage(_damage, Owner, KillFeedIcon.fists);
124 | Entity.EmitSound("SprayCan.ShakeGhost", volume: 0.1f);
125 | InterestScore = _interestMax;
126 | }
127 | else
128 | {
129 | InterestScore--;
130 | }
131 | }
132 |
133 | internal void Kill()
134 | {
135 | Entity.RemoveIfValid();
136 | }
137 |
138 | internal void SetEnemy(CCSPlayerController enemy)
139 | {
140 | if (!enemy.IsAlive()) return;
141 |
142 | if (Target.IsAlive())
143 | {
144 | return;
145 | }
146 |
147 | if (Target == enemy) { return; }
148 | IsFollowingLeader = false;
149 | InterestScore = _interestMax;
150 | Target = enemy;
151 | Entity.Leader.Raw = enemy.PlayerPawn.Raw;
152 | }
153 |
154 | private void FollowLeader()
155 | {
156 | IsFollowingLeader = true;
157 | Target = null;
158 | Entity.Leader.Raw = Owner.PlayerPawn.Raw;
159 | }
160 |
161 | public void Dispose()
162 | {
163 | Kill();
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/WarcraftPlugin/WarcraftPlugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Library
5 | false
6 | true
7 | WarcraftPlugin
8 | True
9 | Warcraft CounterstrikeSharp Plugin
10 | WnGui
11 | An open-source Warcraft mod for CS2 featuring a fully-fledged RPG system
12 | https://github.com/Wngui/CS2WarcraftMod
13 | wc-icon.png
14 | warcraft;cs2;counterstrike;sharp
15 | $(OutputPath)
16 | Readme.md
17 | en
18 |
19 |
20 | true
21 |
22 |
23 | true
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Always
41 |
42 |
43 | Always
44 |
45 |
46 | Always
47 |
48 |
49 | Always
50 |
51 |
52 | True
53 | \
54 |
55 |
56 | True
57 | \
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/WarcraftPlugin/WarcraftPlugin.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.9.34616.47
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WarcraftPlugin", "WarcraftPlugin.csproj", "{8B2673A6-9754-419D-BA08-6AF351F19C9D}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {8B2673A6-9754-419D-BA08-6AF351F19C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {8B2673A6-9754-419D-BA08-6AF351F19C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {8B2673A6-9754-419D-BA08-6AF351F19C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {8B2673A6-9754-419D-BA08-6AF351F19C9D}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {CFAAC625-3622-4D84-9EC2-794D8BAEE421}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/WarcraftPlugin/lang/LocalizerMiddleware.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Localization;
2 | using System.Linq;
3 | using System.Collections.Generic;
4 | using System;
5 | using CounterStrikeSharp.API.Modules.Utils;
6 | using System.Text.RegularExpressions;
7 | using System.IO;
8 | using System.Globalization;
9 | using System.Text.Json;
10 | using System.Collections.Concurrent;
11 | using System.Threading.Tasks;
12 |
13 | namespace WarcraftPlugin.lang
14 | {
15 | public static class LocalizerMiddleware
16 | {
17 | internal static IStringLocalizer Load(IStringLocalizer localizer, string moduleDirectory)
18 | {
19 | var chatColors = GetChatColors();
20 |
21 | List customHeroLocalizerStrings = LoadCustomHeroLocalizations(moduleDirectory, chatColors);
22 |
23 | // Process the localizer strings
24 | var localizedStrings = localizer.GetAllStrings()
25 | .Select(ls => new LocalizedString(ls.Name, ReplaceChatColors(ls.Value, chatColors)))
26 | .Concat(customHeroLocalizerStrings)
27 | .ToList();
28 |
29 | return new WarcraftLocalizer(localizedStrings);
30 | }
31 |
32 | private static List LoadCustomHeroLocalizations(string moduleDirectory, Dictionary chatColors)
33 | {
34 | var searchPattern = $"*.{CultureInfo.CurrentUICulture.TwoLetterISOLanguageName}*.json";
35 | var fallbackSearchPattern = "*.en*.json";
36 |
37 | var customHeroLocalizations = Directory.EnumerateFiles(Path.Combine(moduleDirectory, "lang"), searchPattern);
38 | var fallbackLocalizations = Directory.EnumerateFiles(Path.Combine(moduleDirectory, "lang"), fallbackSearchPattern);
39 |
40 | // Use a thread-safe collection for parallel processing
41 | var concurrentLocalizerStrings = new ConcurrentBag();
42 |
43 | var jsonOptions = new JsonSerializerOptions { AllowTrailingCommas = true };
44 |
45 | Parallel.ForEach(customHeroLocalizations.Concat(fallbackLocalizations), file =>
46 | {
47 | var jsonContent = File.ReadAllText(file);
48 | var customHeroLocalizations = JsonSerializer.Deserialize>(jsonContent, jsonOptions);
49 |
50 | if (customHeroLocalizations != null)
51 | {
52 | foreach (var localization in customHeroLocalizations)
53 | {
54 | concurrentLocalizerStrings.Add(new LocalizedString(localization.Key, ReplaceChatColors(localization.Value, chatColors), false, searchedLocation: file));
55 | }
56 | }
57 | });
58 |
59 | // Use English as fallback
60 | var uniqueLocalizerStrings = concurrentLocalizerStrings
61 | .GroupBy(ls => ls.Name)
62 | .Select(g => g.FirstOrDefault(ls => !ls.SearchedLocation.Contains(".en.")) ?? g.First())
63 | .ToList();
64 |
65 | return uniqueLocalizerStrings;
66 | }
67 |
68 | private static Dictionary GetChatColors()
69 | {
70 | return typeof(ChatColors).GetProperties()
71 | .ToDictionary(prop => prop.Name.ToLower(), prop => prop.GetValue(null)?.ToString() ?? string.Empty, StringComparer.InvariantCultureIgnoreCase);
72 | }
73 |
74 | private static readonly Regex ChatColorRegex = new Regex(@"{(\D+?)}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
75 |
76 | private static string ReplaceChatColors(string input, Dictionary chatColors)
77 | {
78 | return ChatColorRegex.Replace(input, match =>
79 | {
80 | var key = match.Groups[1].Value.ToLower();
81 | return chatColors.TryGetValue(key, out var value) ? value : "{UNKNOWN-COLOR}";
82 | });
83 | }
84 | }
85 |
86 | public class WarcraftLocalizer(List localizedStrings) : IStringLocalizer
87 | {
88 | private readonly List _localizedStrings = localizedStrings;
89 |
90 | public LocalizedString this[string name] => _localizedStrings.FirstOrDefault(ls => ls.Name == name.ToLower()) ?? new LocalizedString(name.ToLower(), name.ToLower());
91 |
92 | public LocalizedString this[string name, params object[] arguments] =>
93 | new(name.ToLower(), string.Format(_localizedStrings.FirstOrDefault(ls => ls.Name == name.ToLower())?.Value ?? name.ToLower(), arguments));
94 |
95 | public IEnumerable GetAllStrings(bool includeParentCultures) => _localizedStrings;
96 |
97 | public IStringLocalizer WithCulture(CultureInfo culture) => this;
98 | }
99 |
100 | public static class StringLocalizerExtensions
101 | {
102 | public static bool Exists(this IStringLocalizer localizer, string name)
103 | {
104 | return localizer.GetAllStrings().Any(ls => ls.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase));
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/WarcraftPlugin/lang/en.json:
--------------------------------------------------------------------------------
1 | {
2 | //Menu + Core
3 | "menu.selection.left": "▶ ",
4 | "menu.selection.right": " ◀",
5 | "menu.option.select": "Space",
6 | "menu.option.select.dead": "E",
7 | "menu.option.exit": "Tab",
8 | "menu.option.up": "W↑",
9 | "menu.option.down": "S↓",
10 | "menu.exit": "Exit",
11 | "menu.select": "Select",
12 | "menu.navigate": "Navigate",
13 | "menu.more.options.below": "
",
14 | "menu.class": "Warcraft Class Menu",
15 | "menu.class.total.levels": "Total Levels",
16 | "menu.class.level": "level",
17 | "menu.class.locked": "Locked (lvl {0})",
18 | "menu.skills.level": "Level",
19 | "menu.skills.available": "Level up skills ({0} available)",
20 | "class.pending.change": "{Green} You will spawn as {Orange}{0}{Green} next round!",
21 | "class.disabled": "{Green}Current class is {Red}disabled{Green}! Now playing as {Orange}{0}{Default}.",
22 | "xp.current": "Experience",
23 | "xp.bonus.headshot": "HS bonus",
24 | "xp.bonus.knife": "knife bonus",
25 | "xp.kill": "{Gold}+{0} XP {Default}for killing {Green}{1} {Default}{2}{3}",
26 | "xp.add": "Adding {0} xp to player {1}",
27 | "ability.ready": "{0} is ready!",
28 | "ultimate.countdown": "Ultimate ready in {0}s",
29 | "no.ultimate": "No levels in ultimate",
30 |
31 | //Commands, used in-game by typing !
32 | "command.ultimate": "ultimate",
33 | "command.changeclass": "changeclass",
34 | "command.reset": "reset",
35 | "command.factoryreset": "factoryreset",
36 | "command.addxp": "addxp",
37 | "command.skills": "skills",
38 | "command.help": "commands", //"help" is a reserved keyword
39 | "command.help.description": "{Green}Type {Gold}!class{Green} to change classes, {Gold}!skills{Green} to level-up",
40 |
41 | //Adverts
42 | "advert.0": "{Green}Want to try a new class? Type {Gold}!class{Green} to change",
43 | "advert.1": "{Green}Unspent skill points? Type {Gold}!skills{Green} to level up abilities",
44 | "advert.2": "{Green}Want to try new abilities? Type {Gold}!reset{Green} to reassign",
45 | "advert.3": "{Green}Want to use your ultimate? Type 'bind ultimate' in console. Example: {Grey}bind x ultimate",
46 |
47 | //Stock class names
48 | "barbarian": "Barbarian",
49 | "mage": "Mage",
50 | "necromancer": "Necromancer",
51 | "paladin": "Paladin",
52 | "ranger": "Ranger",
53 | "rogue": "Rogue",
54 | "shapeshifter": "Shapeshifter",
55 | "tinker": "Tinker",
56 |
57 | //Stock class abilities
58 | "barbarian.ability.0": "Carnage",
59 | "barbarian.ability.0.description": "Increase damage dealt with shotguns.",
60 | "barbarian.ability.1": "Battle-Hardened",
61 | "barbarian.ability.1.description": "Increase your health by 20/40/60/80/100.",
62 | "barbarian.ability.2": "Exploding Barrel",
63 | "barbarian.ability.2.description": "Chance to throw an exploding barrel when firing.",
64 | "barbarian.ability.3": "Bloodlust",
65 | "barbarian.ability.3.description": "Grants infinite ammo, movement speed & health regeneration.",
66 |
67 | "mage.ability.0": "Fireball",
68 | "mage.ability.0.description": "Infuses molotovs with fire magic, causing a huge explosion on impact.",
69 | "mage.ability.1": "Ice Beam",
70 | "mage.ability.1.description": "Chance to freeze enemies in place.",
71 | "mage.ability.2": "Mana Shield",
72 | "mage.ability.2.description": "Passive magical shield, which regenerates armor over time.",
73 | "mage.ability.3": "Teleport",
74 | "mage.ability.3.description": "When you press your ultimate key, you will teleport to the spot you're aiming.",
75 |
76 | "necromancer.ability.0": "Life Drain",
77 | "necromancer.ability.0.description": "Harness dark magic to siphon health from foes and restore your own vitality.",
78 | "necromancer.ability.1": "Poison Cloud",
79 | "necromancer.ability.1.description": "Infuses smoke grenades with potent toxins, damaging enemies over time.",
80 | "necromancer.ability.2": "Splintered Soul",
81 | "necromancer.ability.2.description": "Chance to cheat death with a fraction of vitality.",
82 | "necromancer.ability.3": "Raise Dead",
83 | "necromancer.ability.3.description": "Summon a horde of undead chicken to fight for you.",
84 |
85 | "paladin.ability.0": "Healing Aura",
86 | "paladin.ability.0.description": "Emit an aura that gradually heals nearby allies over time.",
87 | "paladin.ability.1": "Holy Shield",
88 | "paladin.ability.1.description": "Gain an additional 20/40/60/80/100 armor.",
89 | "paladin.ability.2": "Smite",
90 | "paladin.ability.2.description": "Infuse your attacks with divine energy, potentially stripping enemy armor.",
91 | "paladin.ability.3": "Divine Resurrection",
92 | "paladin.ability.3.description": "Instantly revive a random fallen ally.",
93 |
94 | "ranger.ability.0": "Light footed",
95 | "ranger.ability.0.description": "Nimbly perform a dash in midair, by pressing jump.",
96 | "ranger.ability.1": "Ensnare trap",
97 | "ranger.ability.1.description": "Place a trap by throwing a decoy.",
98 | "ranger.ability.2": "Marksman",
99 | "ranger.ability.2.description": "Additional damage with scoped weapons.",
100 | "ranger.ability.3": "Arrowstorm",
101 | "ranger.ability.3.description": "Call down a deadly volley of arrows using the ultimate key.",
102 |
103 | "rogue.ability.0": "Stealth",
104 | "rogue.ability.0.description": "Become partially invisible for 1/2/3/4/5 seconds, when killing someone.",
105 | "rogue.ability.1": "Sneak Attack",
106 | "rogue.ability.1.description": "When you hit an enemy in the back, you do an additional 5/10/15/20/25 damage.",
107 | "rogue.ability.2": "Blade Dance",
108 | "rogue.ability.2.description": "Increases movement speed and damage with knives.",
109 | "rogue.ability.3": "Smokebomb",
110 | "rogue.ability.3.description": "When nearing death, you will automatically drop a smokebomb, letting you cheat death.",
111 |
112 | "shapeshifter.ability.0": "Adaptive Disguise",
113 | "shapeshifter.ability.0.description": "Chance to spawn with an enemy disguise, revealed upon attacking.",
114 | "shapeshifter.ability.1": "Doppelganger",
115 | "shapeshifter.ability.1.description": "Create a temporary inanimate clone of yourself, using a decoy grenade.",
116 | "shapeshifter.ability.2": "Imposter Syndrome",
117 | "shapeshifter.ability.2.description": "Chance to be notified when revealed by enemies on radar.",
118 | "shapeshifter.ability.3": "Morphling",
119 | "shapeshifter.ability.3.description": "Transform into an unassuming object.",
120 |
121 | "tinker.ability.0": "Attack Drone",
122 | "tinker.ability.0.description": "Deploy a gun drone that attacks nearby enemies.",
123 | "tinker.ability.1": "Spare Parts",
124 | "tinker.ability.1.description": "Chance to not lose ammo when firing.",
125 | "tinker.ability.2": "Spring Trap",
126 | "tinker.ability.2.description": "Deploy a trap which launches players into the air.",
127 | "tinker.ability.3": "Drone Swarm",
128 | "tinker.ability.3.description": "Summon a swarm of attack drones that damage all nearby enemies.",
129 |
130 | "shadowblade.ability.0": "Shadowstep",
131 | "shadowblade.ability.0.description": "Chance to teleport behind enemy when taking damage.",
132 | "shadowblade.ability.1": "Evasion",
133 | "shadowblade.ability.1.description": "Chance to completely dodge incoming damage.",
134 | "shadowblade.ability.2": "Venom Strike",
135 | "shadowblade.ability.2.description": "Your attacks poison enemies, dealing damage over time.",
136 | "shadowblade.ability.3": "Cloak of Shadows",
137 | "shadowblade.ability.3.description": "Become invisible for a short duration.",
138 |
139 | "dwarf_engineer.ability.0": "Build",
140 | "dwarf_engineer.ability.0.description": "Allows you to build using the builder tool.",
141 | "dwarf_engineer.ability.1": "Pickaxe",
142 | "dwarf_engineer.ability.1.description": "Chance to find grenades when stabbing surfaces.",
143 | "dwarf_engineer.ability.2": "Stone Skin",
144 | "dwarf_engineer.ability.2.description": "Increase armor.",
145 | "dwarf_engineer.ability.3": "Goldrush!",
146 | "dwarf_engineer.ability.3.description": "Grants 1000 HP for a short time.",
147 |
148 | //Stock class extras
149 | "mage.frozen": "{Blue}[FROZEN]{Default}",
150 | "necromancer.cheatdeath": "{DarkRed}You have cheated death, for now...",
151 | "paladin.revive": "{Green}You have been revived!",
152 | "paladin.revive.other": "{Green}{0}{Default} has been revived by {1}",
153 | "paladin.revive.none": "{Red}No fallen allies to revive!",
154 | "ranger.dash.cooldown": "{Red}Dash{Default} on cooldown!",
155 | "ranger.dash.ready": "{Green}Dash{Default} ready!",
156 | "rogue.backstab": "{Blue}[Backstab] {0} bonus damage",
157 | "rogue.invsible": "[Invisible]",
158 | "rogue.visible": "[Visible]",
159 | "shapeshifter.disguise.failed": "{Red}Disguise failed{Default}, no enemies to copy",
160 | "shapeshifter.disguise": "{Blue}Disguised{Default} as {0}",
161 | "shapeshifter.disguise.revealed": "{Red}Disguise{Default} broken!",
162 | "shapeshifter.spotted": "[Spotted]",
163 | "shadowblade.evaded": "{Green}Evaded {LightYellow}{0}{Green} damage!",
164 | "shadowblade.evaded.attacker": "{LightYellow}{0} {Green} Evaded your attack!",
165 | "shadowblade.shadowstep": "{DarkBlue}Shadowstep!",
166 | "shadowblade.venomstrike": "{Green}Poisoned {LightYellow}{0} {Green}for {DarkRed}{1} {Green}damage!",
167 | "shadowblade.venomstrike.victim": "{Green}Poisoned for {DarkRed}{0} {Green}damage!",
168 | "dwarf_engineer.buildtool": "Build Tool",
169 | "dwarf_engineer.pickaxe": "Pickaxe",
170 | "dwarf_engineer.build.tooltip": "{Green}[Left Click]{Default} Build {Grey}| {DarkBlue}[Right Click]{Default} Change {Grey}| {Red}[R]{Default} Rotate"
171 | }
--------------------------------------------------------------------------------
/WarcraftPlugin/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | 3.2.7
2 |
--------------------------------------------------------------------------------