├── .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 | Gameplay Video 7 | Developer Wiki 8 | Discord 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 | ![image](https://github.com/user-attachments/assets/3a96b1ba-0173-4b3e-8e2a-43b1ac091247) 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 | --------------------------------------------------------------------------------