├── .github └── workflows │ ├── foundry.yml │ └── rust.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── MIT-LICENSE ├── README.md ├── UNLICENSE ├── build.rs ├── build_resources ├── cached_build.ron └── weapon_formulas.json ├── output_definitions ├── d2_calculation_api.d.ts └── d2_calculation_api.pyi └── src ├── abilities └── mod.rs ├── activity ├── damage_calc.rs └── mod.rs ├── d2_enums.rs ├── enemies └── mod.rs ├── lib.rs ├── logging.rs ├── perks ├── buff_perks.rs ├── exotic_armor.rs ├── exotic_perks.rs ├── lib.rs ├── meta_perks.rs ├── mod.rs ├── origin_perks.rs ├── other_perks.rs ├── perk_options_handler.rs ├── year_1_perks.rs ├── year_2_perks.rs ├── year_3_perks.rs ├── year_4_perks.rs ├── year_5_perks.rs └── year_6_perks.rs ├── test.rs ├── types ├── js_types.rs ├── mod.rs ├── py_types.rs └── rs_types.rs └── weapons ├── dps_calc.rs ├── mod.rs ├── reserve_calc.rs ├── stat_calc.rs ├── ttk_calc.rs └── weapon_constructor.rs /.github/workflows/foundry.yml: -------------------------------------------------------------------------------- 1 | name: Foundry 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Install Rust 20 | uses: dtolnay/rust-toolchain@stable 21 | - name: Rust Cache 22 | uses: Swatinem/rust-cache@v2.2.1 23 | - name: Install Wasm-Pack 24 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 25 | - name: Build 26 | run: cargo build --features wasm,foundry 27 | - name: Test 28 | run: cargo test --features wasm,foundry 29 | - name: Build Wasm target 30 | run: wasm-pack build --target web --features wasm,foundry 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Install Rust 20 | uses: dtolnay/rust-toolchain@stable 21 | - name: Rust Cache 22 | uses: Swatinem/rust-cache@v2.2.1 23 | - name: Install Wasm-Pack 24 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 25 | - name: Build 26 | run: cargo build --verbose 27 | - name: Test 28 | run: cargo test 29 | - name: Build Wasm target 30 | run: wasm-pack build --target web --features wasm 31 | 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.pyd 3 | *.py 4 | *.js 5 | *.dll 6 | *.pyc 7 | *.old 8 | *.zip 9 | *.ts 10 | !*.d.ts 11 | database/weapon_formulas.rs 12 | !database/compactor.py 13 | !database/perk_rerouter.py 14 | zdata.json 15 | database/weapon.json 16 | database/weapon_test.json 17 | database/enhanced_handler.rs 18 | /zPythonStuff 19 | /zPveReverser 20 | data.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic", 3 | "rust-analyzer.cargo.extraEnv": { 4 | "IS_RA": "true" 5 | }, 6 | "rust-analyzer.cargo.features": [ 7 | "wasm", "foundry" 8 | ] 9 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "d2_calculation_api" 3 | edition = "2021" 4 | version = "1.0.2" 5 | rust-version = "1.65" 6 | repository = "https://github.com/oh-yes-0-fps/D2_Calculation_API" 7 | build = "build.rs" 8 | 9 | [features] 10 | wasm = ["serde-wasm-bindgen", "wasm-bindgen", "console_error_panic_hook"] 11 | python = ["pyo3", "pyo3-built"] 12 | foundry = [] 13 | 14 | [build-dependencies] 15 | built = { version = "0.6", features = ["git2", "chrono", "semver"] } 16 | serde = { version = "^1.0", features = ["derive"]} 17 | reqwest = { version = "^0.11", features = ["json", "blocking"] } 18 | serde_json = "^1.0" 19 | ron = "^0.8" 20 | 21 | [dev-dependencies] 22 | num-traits = "0.2" 23 | # wasm-bindgen-test = { version = "^0.3", optional = true } 24 | 25 | [dependencies] 26 | serde = { version = "^1.0", features = ["derive"]} 27 | built = { version = "0.6", features = ["chrono", "semver"] } 28 | 29 | serde-wasm-bindgen = { version = "^0.5", optional = true } 30 | console_error_panic_hook = { version = "0.1.7", optional = true} 31 | wasm-bindgen = { version = "^0.2", optional = true} 32 | 33 | pyo3 = { version = "^0.18", features = ["extension-module"], optional = true} 34 | pyo3-built = { version = "^0.4", optional = true} 35 | num_enum = "0.6.0" 36 | 37 | 38 | [lib] 39 | crate-type = ["cdylib"] 40 | 41 | [profile.release] 42 | lto = true 43 | opt-level = "z" 44 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bowan T. Foryt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # D2_Calculation_API 2 | 3 | [![Rust](https://github.com/oh-yes-0-fps/D2_Calculation_API/actions/workflows/rust.yml/badge.svg?branch=master)](https://github.com/oh-yes-0-fps/D2_Calculation_API/actions/workflows/rust.yml) 4 | 5 | Check the wiki section for more details on the API and how to use it. 6 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /output_definitions/d2_calculation_api.d.ts: -------------------------------------------------------------------------------- 1 | type Hash = number; 2 | /** 3 | */ 4 | export function start(): void; 5 | /** 6 | * @returns {MetaData} 7 | */ 8 | export function getMetadata(): MetaData; 9 | /** 10 | * @returns {string} 11 | */ 12 | export function stringifyWeapon(): string; 13 | /** 14 | *Returns the weapon as a JSON structure, snake case fields 15 | * @returns {any} 16 | */ 17 | export function weaponJSON(): any; 18 | /** 19 | * @param {Hash} _hash 20 | * @param {number} _weapon_type_id 21 | * @param {Hash} _intrinsic_hash 22 | * @param {number} _ammo_type_id 23 | * @param {number} _damage_type_id 24 | */ 25 | export function setWeapon(_hash: Hash, _weapon_type_id: number, _intrinsic_hash: Hash, _ammo_type_id: number, _damage_type_id: number): void; 26 | /** 27 | * @returns {Map} 28 | */ 29 | export function getStats(): Map; 30 | /** 31 | * @param {Map} _stats 32 | */ 33 | export function setStats(_stats: Map): void; 34 | /** 35 | * @param {Map} _stats 36 | * @param {number} _value 37 | * @param {number} _hash 38 | */ 39 | export function addTrait(_stats: Map, _value: number, _hash: number): void; 40 | /** 41 | * @returns {Array} 42 | */ 43 | export function getTraitHashes(): Array; 44 | /** 45 | * @param {number} perk_hash 46 | * @param {number} new_value 47 | */ 48 | export function setTraitValue(perk_hash: Hash, new_value: number): void; 49 | /** 50 | * @param {Uint32Array} _perks 51 | * @returns {Map} 52 | */ 53 | export function getTraitOptions(_perks: Uint32Array): Map; 54 | /** 55 | * @param {boolean} _dynamic_traits 56 | * @param {boolean} _pvp 57 | * @returns {RangeResponse} 58 | */ 59 | export function getWeaponRangeFalloff(_dynamic_traits: boolean, _pvp: boolean): RangeResponse; 60 | /** 61 | * @param {boolean} _dynamic_traits 62 | * @param {boolean} _pvp 63 | * @returns {HandlingResponse} 64 | */ 65 | export function getWeaponHandlingTimes(_dynamic_traits: boolean, _pvp: boolean): HandlingResponse; 66 | /** 67 | * @param {boolean} _dynamic_traits 68 | * @param {boolean} _pvp 69 | * @returns {ReloadResponse} 70 | */ 71 | export function getWeaponReloadTimes(_dynamic_traits: boolean, _pvp: boolean): ReloadResponse; 72 | /** 73 | * @param {boolean} _dynamic_traits 74 | * @param {boolean} _pvp 75 | * @returns {AmmoResponse} 76 | */ 77 | export function getWeaponAmmoSizes(_dynamic_traits: boolean, _pvp: boolean): AmmoResponse; 78 | /** 79 | * @param {number} _overhsield 80 | * @returns {TtkResponse} 81 | */ 82 | export function getWeaponTtk(_overhsield: number): Array; 83 | /** 84 | * @param {boolean} _use_rpl 85 | * @returns {DpsResponse} 86 | */ 87 | export function getWeaponDps(_use_rpl: boolean): DpsResponse; 88 | /** 89 | * @param {boolean} _dynamic_traits 90 | * @param {boolean} _pvp 91 | * @param {boolean} _use_rpl 92 | * @returns {FiringResponse} 93 | */ 94 | export function getWeaponFiringData(_dynamic_traits: boolean, _pvp: boolean, _use_rpl: boolean): FiringResponse; 95 | /** 96 | * @param {number} _rpl 97 | * @param {number} _override_cap 98 | * @param {number} _difficulty 99 | * @param {number} _enemy_type 100 | */ 101 | export function setEncounter(_rpl: number, _override_cap: number, _difficulty: number, _enemy_type: number): void; 102 | /** 103 | */ 104 | export enum DifficultyOptions { 105 | NORMAL, 106 | RAID, 107 | MASTER, 108 | } 109 | /** 110 | */ 111 | export enum EnemyType { 112 | MINOR, 113 | ELITE, 114 | MINIBOSS, 115 | BOSS, 116 | VEHICLE, 117 | ENCLAVE, 118 | PLAYER, 119 | CHAMPION, 120 | } 121 | /** 122 | */ 123 | export class AmmoResponse { 124 | /** 125 | ** Return copy of self without private attributes. 126 | */ 127 | toJSON(): Object; 128 | /** 129 | * Return stringified version of self. 130 | */ 131 | toString(): string; 132 | free(): void; 133 | /** 134 | */ 135 | readonly magSize: number; 136 | /** 137 | */ 138 | readonly reserveSize: number; 139 | } 140 | /** 141 | */ 142 | export class PerkOptionData { 143 | /** 144 | */ 145 | stacks: Array; 146 | /** 147 | */ 148 | options: Array; 149 | /** 150 | */ 151 | optionType: string; 152 | } 153 | /** 154 | */ 155 | export class OptimalKillData { 156 | /** 157 | ** Return copy of self without private attributes. 158 | */ 159 | toJSON(): Object; 160 | /** 161 | * Return stringified version of self. 162 | */ 163 | toString(): string; 164 | free(): void; 165 | /** 166 | */ 167 | readonly achievableRange: number; 168 | /** 169 | */ 170 | readonly bodyshots: number; 171 | /** 172 | */ 173 | readonly headshots: number; 174 | /** 175 | */ 176 | readonly timeTaken: number; 177 | } 178 | /** 179 | */ 180 | export class BodyKillData { 181 | /** 182 | ** Return copy of self without private attributes. 183 | */ 184 | toJSON(): Object; 185 | /** 186 | * Return stringified version of self. 187 | */ 188 | toString(): string; 189 | free(): void; 190 | /** 191 | */ 192 | readonly bodyshots: number; 193 | /** 194 | */ 195 | readonly timeTaken: number; 196 | } 197 | /** 198 | */ 199 | export class ResillienceTtkSummary { 200 | /** 201 | ** Return copy of self without private attributes. 202 | */ 203 | toJSON(): Object; 204 | /** 205 | * Return stringified version of self. 206 | */ 207 | toString(): string; 208 | free(): void; 209 | /** 210 | */ 211 | readonly bodyTtk: BodyKillData; 212 | /** 213 | */ 214 | readonly optimalTtk: OptimalKillData; 215 | /** 216 | */ 217 | readonly resillienceValue: number; 218 | } 219 | /** 220 | */ 221 | export class DpsResponse { 222 | free(): void; 223 | /** 224 | * Return stringified version of self. 225 | */ 226 | toString(): string; 227 | /** 228 | ** Return copy of self without private attributes. 229 | */ 230 | toJSON(): string; 231 | /** 232 | *Returns a list of dps values for each magazine 233 | */ 234 | readonly dpsPerMag: Array; 235 | /** 236 | *Returns a list of tuples of time and damage 237 | */ 238 | readonly timeDamageData: Array>; 239 | /** 240 | */ 241 | readonly totalDamage: number; 242 | /** 243 | */ 244 | readonly totalShots: number; 245 | /** 246 | */ 247 | readonly totalTime: number; 248 | } 249 | /** 250 | */ 251 | export class FiringResponse { 252 | /** 253 | ** Return copy of self without private attributes. 254 | */ 255 | toJSON(): Object; 256 | /** 257 | * Return stringified version of self. 258 | */ 259 | toString(): string; 260 | free(): void; 261 | /** 262 | */ 263 | readonly burstDelay: number; 264 | /** 265 | */ 266 | readonly burstSize: number; 267 | /** 268 | */ 269 | readonly innerBurstDelay: number; 270 | /** 271 | */ 272 | readonly pveCritMult: number; 273 | /** 274 | */ 275 | readonly pveExplosionDamage: number; 276 | /** 277 | */ 278 | readonly pveImpactDamage: number; 279 | /** 280 | */ 281 | readonly pvpCritMult: number; 282 | /** 283 | */ 284 | readonly pvpExplosionDamage: number; 285 | /** 286 | */ 287 | readonly pvpImpactDamage: number; 288 | /** 289 | */ 290 | readonly rpm: number; 291 | } 292 | /** 293 | */ 294 | export class HandlingResponse { 295 | /** 296 | ** Return copy of self without private attributes. 297 | */ 298 | toJSON(): Object; 299 | /** 300 | * Return stringified version of self. 301 | */ 302 | toString(): string; 303 | free(): void; 304 | /** 305 | */ 306 | readonly adsTime: number; 307 | /** 308 | */ 309 | readonly readyTime: number; 310 | /** 311 | */ 312 | readonly stowTime: number; 313 | } 314 | /** 315 | */ 316 | export class MetaData { 317 | /** 318 | ** Return copy of self without private attributes. 319 | */ 320 | toJSON(): Object; 321 | /** 322 | * Return stringified version of self. 323 | */ 324 | toString(): string; 325 | free(): void; 326 | /** 327 | */ 328 | readonly apiGitBranch: string; 329 | /** 330 | */ 331 | readonly apiGitCommit: string; 332 | /** 333 | */ 334 | readonly apiTimestamp: string; 335 | /** 336 | */ 337 | readonly apiVersion: string; 338 | /** 339 | */ 340 | readonly databaseTimestamp: bigint; 341 | } 342 | /** 343 | */ 344 | export class RangeResponse { 345 | /** 346 | ** Return copy of self without private attributes. 347 | */ 348 | toJSON(): Object; 349 | /** 350 | * Return stringified version of self. 351 | */ 352 | toString(): string; 353 | free(): void; 354 | /** 355 | */ 356 | readonly adsFalloffEnd: number; 357 | /** 358 | */ 359 | readonly adsFalloffStart: number; 360 | /** 361 | */ 362 | readonly floorPercent: number; 363 | /** 364 | */ 365 | readonly hipFalloffEnd: number; 366 | /** 367 | */ 368 | readonly hipFalloffStart: number; 369 | } 370 | /** 371 | */ 372 | export class ReloadResponse { 373 | /** 374 | ** Return copy of self without private attributes. 375 | */ 376 | toJSON(): Object; 377 | /** 378 | * Return stringified version of self. 379 | */ 380 | toString(): string; 381 | free(): void; 382 | /** 383 | */ 384 | readonly ammoTime: number; 385 | /** 386 | */ 387 | readonly reloadTime: number; 388 | } 389 | /** 390 | */ 391 | export class Stat { 392 | free(): void; 393 | /** 394 | * @returns {string} 395 | */ 396 | toString(): string; 397 | /** 398 | */ 399 | readonly baseValue: number; 400 | /** 401 | */ 402 | readonly partValue: number; 403 | /** 404 | */ 405 | readonly traitValue: number; 406 | } 407 | /** 408 | */ 409 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 410 | 411 | export interface InitOutput { 412 | readonly memory: WebAssembly.Memory; 413 | readonly __wbg_handlingresponse_free: (a: number) => void; 414 | readonly __wbg_reloadresponse_free: (a: number) => void; 415 | readonly __wbg_ammoresponse_free: (a: number) => void; 416 | readonly __wbg_get_ammoresponse_magSize: (a: number) => number; 417 | readonly __wbg_get_ammoresponse_reserveSize: (a: number) => number; 418 | readonly __wbg_dpsresponse_free: (a: number) => void; 419 | readonly __wbg_get_dpsresponse_totalTime: (a: number) => number; 420 | readonly __wbg_get_dpsresponse_totalShots: (a: number) => number; 421 | readonly dpsresponse_toString: (a: number, b: number) => void; 422 | readonly dpsresponse_toJSON: (a: number, b: number) => void; 423 | readonly dpsresponse_timeDamageData: (a: number) => number; 424 | readonly dpsresponse_dpsPerMag: (a: number) => number; 425 | readonly __wbg_optimalkilldata_free: (a: number) => void; 426 | readonly __wbg_get_optimalkilldata_headshots: (a: number) => number; 427 | readonly __wbg_set_optimalkilldata_headshots: (a: number, b: number) => void; 428 | readonly __wbg_get_optimalkilldata_bodyshots: (a: number) => number; 429 | readonly __wbg_set_optimalkilldata_bodyshots: (a: number, b: number) => void; 430 | readonly __wbg_set_optimalkilldata_achievableRange: (a: number, b: number) => void; 431 | readonly __wbg_bodykilldata_free: (a: number) => void; 432 | readonly __wbg_get_bodykilldata_bodyshots: (a: number) => number; 433 | readonly __wbg_set_bodykilldata_bodyshots: (a: number, b: number) => void; 434 | readonly __wbg_get_bodykilldata_timeTaken: (a: number) => number; 435 | readonly __wbg_set_bodykilldata_timeTaken: (a: number, b: number) => void; 436 | readonly __wbg_resilliencesummary_free: (a: number) => void; 437 | readonly __wbg_set_resilliencesummary_resillienceValue: (a: number, b: number) => void; 438 | readonly __wbg_get_resilliencesummary_bodyTtk: (a: number) => number; 439 | readonly __wbg_set_resilliencesummary_bodyTtk: (a: number, b: number) => void; 440 | readonly __wbg_get_resilliencesummary_optimalTtk: (a: number) => number; 441 | readonly __wbg_set_resilliencesummary_optimalTtk: (a: number, b: number) => void; 442 | readonly __wbg_firingresponse_free: (a: number) => void; 443 | readonly __wbg_get_firingresponse_pvpCritMult: (a: number) => number; 444 | readonly __wbg_get_firingresponse_pveImpactDamage: (a: number) => number; 445 | readonly __wbg_get_firingresponse_pveExplosionDamage: (a: number) => number; 446 | readonly __wbg_get_firingresponse_pveCritMult: (a: number) => number; 447 | readonly __wbg_get_firingresponse_burstDelay: (a: number) => number; 448 | readonly __wbg_get_firingresponse_innerBurstDelay: (a: number) => number; 449 | readonly __wbg_get_firingresponse_burstSize: (a: number) => number; 450 | readonly __wbg_get_firingresponse_rpm: (a: number) => number; 451 | readonly __wbg_set_firingresponse_rpm: (a: number, b: number) => void; 452 | readonly __wbg_stat_free: (a: number) => void; 453 | readonly __wbg_set_stat_baseValue: (a: number, b: number) => void; 454 | readonly __wbg_set_stat_partValue: (a: number, b: number) => void; 455 | readonly __wbg_get_stat_traitValue: (a: number) => number; 456 | readonly __wbg_set_stat_traitValue: (a: number, b: number) => void; 457 | readonly stat_toString: (a: number, b: number) => void; 458 | readonly __wbg_metadata_free: (a: number) => void; 459 | readonly __wbg_get_metadata_databaseTimestamp: (a: number) => number; 460 | readonly __wbg_get_metadata_apiVersion: (a: number, b: number) => void; 461 | readonly __wbg_get_metadata_apiTimestamp: (a: number, b: number) => void; 462 | readonly __wbg_get_metadata_apiGitCommit: (a: number, b: number) => void; 463 | readonly __wbg_get_metadata_apiGitBranch: (a: number, b: number) => void; 464 | readonly start: () => void; 465 | readonly getMetadata: (a: number) => void; 466 | readonly stringifyWeapon: (a: number) => void; 467 | readonly weaponJSON: (a: number) => void; 468 | readonly setWeapon: (a: number, b: number, c: number, d: number, e: number, f: number) => void; 469 | readonly getStats: (a: number) => void; 470 | readonly setStats: (a: number, b: number) => void; 471 | readonly addTrait: (a: number, b: number, c: number, d: number) => void; 472 | readonly getTraitHashes: (a: number) => void; 473 | readonly setTraitValue: (a: number, b: number) => void; 474 | readonly getTraitOptions: (a: number, b: number, c: number) => void; 475 | readonly getWeaponRangeFalloff: (a: number, b: number) => void; 476 | readonly getWeaponHandlingTimes: (a: number, b: number) => void; 477 | readonly getWeaponReloadTimes: (a: number, b: number) => void; 478 | readonly getWeaponAmmoSizes: (a: number, b: number) => void; 479 | readonly getWeaponTtk: (a: number, b: number) => void; 480 | readonly getWeaponDps: (a: number, b: number) => void; 481 | readonly getWeaponFiringData: (a: number, b: number, c: number) => void; 482 | readonly setEncounter: (a: number, b: number, c: number, d: number, e: number) => void; 483 | readonly __wbg_rangeresponse_free: (a: number) => void; 484 | readonly __wbg_get_rangeresponse_hipFalloffStart: (a: number) => number; 485 | readonly __wbg_get_reloadresponse_reloadTime: (a: number) => number; 486 | readonly __wbg_get_handlingresponse_readyTime: (a: number) => number; 487 | readonly __wbg_get_optimalkilldata_timeTaken: (a: number) => number; 488 | readonly __wbg_get_dpsresponse_totalDamage: (a: number) => number; 489 | readonly __wbg_get_firingresponse_pvpImpactDamage: (a: number) => number; 490 | readonly __wbg_get_stat_baseValue: (a: number) => number; 491 | readonly __wbg_set_optimalkilldata_timeTaken: (a: number, b: number) => void; 492 | readonly __wbg_get_rangeresponse_hipFalloffEnd: (a: number) => number; 493 | readonly __wbg_get_rangeresponse_adsFalloffStart: (a: number) => number; 494 | readonly __wbg_get_reloadresponse_ammoTime: (a: number) => number; 495 | readonly __wbg_get_handlingresponse_stowTime: (a: number) => number; 496 | readonly __wbg_get_optimalkilldata_achievableRange: (a: number) => number; 497 | readonly __wbg_get_resilliencesummary_resillienceValue: (a: number) => number; 498 | readonly __wbg_get_firingresponse_pvpExplosionDamage: (a: number) => number; 499 | readonly __wbg_get_handlingresponse_adsTime: (a: number) => number; 500 | readonly __wbg_get_rangeresponse_adsFalloffEnd: (a: number) => number; 501 | readonly __wbg_get_rangeresponse_floorPercent: (a: number) => number; 502 | readonly __wbg_get_stat_partValue: (a: number) => number; 503 | readonly __wbindgen_malloc: (a: number) => number; 504 | readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; 505 | readonly __wbindgen_free: (a: number, b: number) => void; 506 | readonly __wbindgen_add_to_stack_pointer: (a: number) => number; 507 | readonly __wbindgen_exn_store: (a: number) => void; 508 | readonly __wbindgen_start: () => void; 509 | } 510 | 511 | export type SyncInitInput = BufferSource | WebAssembly.Module; 512 | /** 513 | * Instantiates the given `module`, which can either be bytes or 514 | * a precompiled `WebAssembly.Module`. 515 | * 516 | * @param {SyncInitInput} module 517 | * 518 | * @returns {InitOutput} 519 | */ 520 | export function initSync(module: SyncInitInput): InitOutput; 521 | 522 | /** 523 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 524 | * for everything else, calls `WebAssembly.instantiate` directly. 525 | * 526 | * @param {InitInput | Promise} module_or_path 527 | * 528 | * @returns {Promise} 529 | */ 530 | export default function init(module_or_path?: InitInput | Promise): Promise; -------------------------------------------------------------------------------- /output_definitions/d2_calculation_api.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Self, Union# type: ignore 2 | from enum import Enum 3 | 4 | 5 | 6 | class WeaponInterface: 7 | class Trait: 8 | def __init__(self, _stat_buffs: dict[int, int], _enhanced: bool, _value: int, _hash: int): ... 9 | def __new__(cls: type[Self]) -> Self: ... 10 | def __repr__(self) -> str: ... 11 | @staticmethod 12 | def default() -> WeaponInterface.Trait: ... 13 | 14 | 15 | class HandlingResponse: 16 | @property 17 | def ready_time(self) -> float: ... 18 | @property 19 | def stow_time(self) -> float: ... 20 | @property 21 | def ads_time(self) -> float: ... 22 | def __repr__(self) -> str: ... 23 | 24 | class RangeResponse: 25 | @property 26 | def hip_falloff_start(self) -> float: ... 27 | @property 28 | def hip_falloff_end(self) -> float: ... 29 | @property 30 | def ads_falloff_start(self) -> float: ... 31 | @property 32 | def ads_falloff_end(self) -> float: ... 33 | @property 34 | def floor_percent(self) -> float: ... 35 | def __repr__(self) -> str: ... 36 | 37 | class DpsResponse: 38 | @property 39 | def dps_per_mag(self) -> list[float]: ... 40 | @property 41 | def time_damage_data(self) -> list[tuple[float, float]]: ... 42 | @property 43 | def time_dps_data(self) -> list[tuple[float, float]]: ... 44 | @property 45 | def total_damage(self) -> float: ... 46 | @property 47 | def total_time(self) -> float: ... 48 | @property 49 | def total_shots(self) -> int: ... 50 | def over_time_span(self, _start: float, _end: float) -> tuple[float, float]: ... 51 | def __repr__(self) -> str: ... 52 | 53 | class FiringResponse: 54 | @property 55 | def pvp_impact_damage(self) -> float: ... 56 | @property 57 | def pvp_explosion_damage(self) -> float: ... 58 | @property 59 | def pvp_crit_mult(self) -> float: ... 60 | @property 61 | def pve_impact_damage(self) -> float: ... 62 | @property 63 | def pve_explosion_damage(self) -> float: ... 64 | @property 65 | def pve_crit_mult(self) -> float: ... 66 | @property 67 | def burst_delay(self) -> float: ... 68 | @property 69 | def burst_duration(self) -> float: ... 70 | @property 71 | def burst_size(self) -> int: ... 72 | @property 73 | def rpm(self) -> float: ... 74 | def __repr__(self) -> str: ... 75 | 76 | class OptimalKillData: 77 | @property 78 | def headshots(self) -> int: ... 79 | @property 80 | def bodyshots(self) -> int: ... 81 | @property 82 | def time_taken(self) -> float: ... 83 | @property 84 | def achievable_range(self) -> float: ... 85 | def __repr__(self) -> str: ... 86 | 87 | class BodyKillData: 88 | @property 89 | def bodyshots(self) -> int: ... 90 | @property 91 | def time_taken(self) -> float: ... 92 | def __repr__(self) -> str: ... 93 | 94 | class ResillienceSummary: 95 | @property 96 | def value(self) -> int: ... 97 | @property 98 | def body_ttk(self) -> WeaponInterface.BodyKillData: ... 99 | @property 100 | def optimal_ttk(self) -> WeaponInterface.OptimalKillData: ... 101 | def __repr__(self) -> str: ... 102 | 103 | @staticmethod 104 | def set_weapon(_hash: int, _weapon_type_id: int, _intrinsic_hash: int, _ammo_type_id: int, _damage_type_id: int) -> None: ... 105 | @staticmethod 106 | def get_weapon_hash() -> bool: ... 107 | @staticmethod 108 | def stringify_weapon() -> str: ... 109 | @staticmethod 110 | def get_range_falloff(_use_traits: bool) -> WeaponInterface.RangeResponse: ... 111 | @staticmethod 112 | def get_handling_times(_use_traits: bool) -> WeaponInterface.HandlingResponse: ... 113 | @staticmethod 114 | def add_trait(_trait: Trait) -> None: ... 115 | @staticmethod 116 | def remove_trait(_trait: int) -> None: ... 117 | @staticmethod 118 | def get_dps(_do_rpl_mult: bool) -> WeaponInterface.DpsResponse: ... 119 | @staticmethod 120 | def get_ttk(_overshield: float) -> list[ResillienceSummary]: ... 121 | @staticmethod 122 | def set_stats(_stats: dict[int, int]) -> None: ... 123 | @staticmethod 124 | def get_firing_data(_use_traits: bool, _use_rpl: bool) -> WeaponInterface.FiringResponse: ... 125 | @staticmethod 126 | def reverse_pve_calc(_damage: float, _combatant_mult = 1.0, _pve_mult = 1.0) -> float: ... 127 | 128 | class ActivityInterface: 129 | class DifficultyOptions(Enum): 130 | NORMAL = 1, 131 | RAID = 2, 132 | MASTER = 3, 133 | 134 | class PlayerClass(Enum): 135 | UNKNOW = 0, 136 | Titan = 1, 137 | Hunter = 2, 138 | Warlock = 3, 139 | 140 | class Activity: 141 | def __init__(self, _name: str, _difficulty: ActivityInterface.DifficultyOptions, _rpl: int, _cap: int): ... 142 | def __new__(cls: type[Self]) -> Self: ... 143 | def __repr__(self) -> str: ... 144 | 145 | class Player: 146 | def __init__(self, _power_level: int, _class: ActivityInterface.PlayerClass): ... 147 | def __new__(cls: type[Self]) -> Self: ... 148 | def __repr__(self) -> str: ... 149 | 150 | @staticmethod 151 | def set_activity(_activity: Activity) -> None: ... 152 | @staticmethod 153 | def set_player(_player: Player) -> None: ... 154 | @staticmethod 155 | def get_activity() -> ActivityInterface.Activity: ... 156 | @staticmethod 157 | def get_player() -> ActivityInterface.Player: ... 158 | 159 | class EnemyInterface: 160 | class EnemyType(Enum): 161 | MINOR = 1, 162 | ELITE = 2, 163 | MINIBOSS = 3, 164 | BOSS = 4, 165 | VEHICLE = 5, 166 | ENCLAVE = 6, 167 | PLAYER = 7, 168 | CHAMPION = 8, 169 | 170 | class Enemy: 171 | def __init__(self, _health: float, _damage: float, _damage_resistance: float, _type: EnemyInterface.EnemyType, _tier: int): ... 172 | def __new__(cls: type[Self]) -> Self: ... 173 | def __repr__(self) -> str: ... 174 | 175 | @staticmethod 176 | def get_enemy() -> EnemyInterface.Enemy: ... 177 | @staticmethod 178 | def set_enemy(_enemy: Enemy) -> None: ... 179 | @staticmethod 180 | def set_enemy_type(_type: EnemyType) -> None: ... 181 | -------------------------------------------------------------------------------- /src/abilities/mod.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 2 | pub enum AbilityType { 3 | GRENADE, 4 | MELEE, 5 | CLASS, 6 | SUPER, 7 | 8 | //these will typically behave the same but are diff cuz i said so 9 | WEAPON, 10 | ARMOR, 11 | MISC, 12 | UNKNOWN, 13 | } 14 | impl Default for AbilityType { 15 | fn default() -> Self { 16 | AbilityType::UNKNOWN 17 | } 18 | } 19 | 20 | #[derive(Debug, Clone, Default)] 21 | pub struct AbilityDamageProfile { 22 | impact: f64, 23 | secondary: f64, 24 | sec_hit_count: u32, 25 | lin_hit_scalar: f64, 26 | crit_mult: f64, // if 1.0, no crit 27 | } 28 | 29 | #[derive(Debug, Clone, Default)] 30 | pub struct Ability { 31 | pub name: String, 32 | pub hash: u32, 33 | pub ability_type: AbilityType, 34 | pub damage_profile: AbilityDamageProfile, 35 | pub is_initialized: bool, 36 | } 37 | -------------------------------------------------------------------------------- /src/activity/damage_calc.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::Activity; 3 | use crate::{enemies::EnemyType, types::rs_types::DamageMods}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Clone, Copy)] 7 | struct TableKey { 8 | time: f64, 9 | value: f64, 10 | } 11 | #[derive(Debug, Clone)] 12 | struct LinearTable { 13 | table: Vec, 14 | } 15 | impl LinearTable { 16 | fn evaluate(&self, time: f64) -> f64 { 17 | if time > 0.0 { 18 | return self.table[self.table.len() - 1].value; 19 | } 20 | let mut index = 0; 21 | for i in 0..self.table.len() { 22 | if self.table[i].time > time { 23 | index = i; 24 | break; 25 | } 26 | } 27 | if index == 0 { 28 | return self.table[0].value; 29 | } 30 | if index == self.table.len() { 31 | return self.table[self.table.len() - 1].value; 32 | } 33 | let a = self.table[index - 1]; 34 | let b = self.table[index]; 35 | let t = (time - a.time) / (b.time - a.time); 36 | return a.value + (b.value - a.value) * t; 37 | } 38 | fn from_vecs(_times: [f64; 11], _values: [f64; 11]) -> LinearTable { 39 | let mut table = Vec::new(); 40 | let times = _times.clone().to_vec(); 41 | let values = _values.clone().to_vec(); 42 | for i in 0..times.len() { 43 | table.push(TableKey { 44 | time: times[i], 45 | value: values[i], 46 | }); 47 | } 48 | table.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap()); 49 | return LinearTable { table }; 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | pub struct DifficultyData { 55 | name: String, 56 | cap: i32, 57 | table: LinearTable, 58 | } 59 | 60 | const MASTER_VALUES: [f64; 11] = [ 61 | 0.85, 0.68, 0.58, 0.5336, 0.505, 0.485, 0.475, 0.46, 0.44, 0.42, 0.418, 62 | ]; 63 | const MASTER_TIMES: [f64; 11] = [ 64 | 0.0, -10.0, -20.0, -30.0, -40.0, -50.0, -60.0, -70.0, -80.0, -90.0, -99.0, 65 | ]; 66 | 67 | const NORMAL_VALUES: [f64; 11] = [ 68 | 1.0, 0.78, 0.66, 0.5914, 0.5405, 0.5, 0.475, 0.46, 0.44, 0.42, 0.418, 69 | ]; 70 | const NORMAL_TIMES: [f64; 11] = [ 71 | 0.0, -10.0, -20.0, -30.0, -40.0, -50.0, -60.0, -70.0, -80.0, -90.0, -99.0, 72 | ]; 73 | 74 | const RAID_VALUES: [f64; 11] = [ 75 | 0.925, 0.74, 0.62, 0.5623, 0.5225, 0.4925, 0.475, 0.46, 0.44, 0.42, 0.418, 76 | ]; 77 | const RAID_TIMES: [f64; 11] = [ 78 | 0.0, -10.0, -20.0, -30.0, -40.0, -50.0, -60.0, -70.0, -80.0, -90.0, -99.0, 79 | ]; 80 | 81 | const WEAPON_DELTA_EXPONENT: f64 = 1.006736; 82 | 83 | #[derive(Debug, Clone)] 84 | pub enum DifficultyOptions { 85 | NORMAL = 1, 86 | RAID = 2, 87 | MASTER = 3, 88 | } 89 | impl Default for DifficultyOptions { 90 | fn default() -> Self { 91 | DifficultyOptions::NORMAL 92 | } 93 | } 94 | impl DifficultyOptions { 95 | pub fn get_difficulty_data(&self) -> DifficultyData { 96 | match self { 97 | DifficultyOptions::NORMAL => DifficultyData { 98 | name: "Normal".to_string(), 99 | cap: 50, 100 | table: LinearTable::from_vecs(NORMAL_TIMES, NORMAL_VALUES), 101 | }, 102 | DifficultyOptions::MASTER => DifficultyData { 103 | name: "Master".to_string(), 104 | cap: 20, 105 | table: LinearTable::from_vecs(MASTER_TIMES, MASTER_VALUES), 106 | }, 107 | DifficultyOptions::RAID => DifficultyData { 108 | name: "Raid & Dungeon".to_string(), 109 | cap: 20, 110 | table: LinearTable::from_vecs(RAID_TIMES, RAID_VALUES), 111 | }, 112 | } 113 | } 114 | } 115 | impl From for DifficultyOptions { 116 | fn from(i: i32) -> Self { 117 | match i { 118 | 1 => DifficultyOptions::NORMAL, 119 | 2 => DifficultyOptions::RAID, 120 | 3 => DifficultyOptions::MASTER, 121 | _ => DifficultyOptions::NORMAL, 122 | } 123 | } 124 | } 125 | 126 | pub(super) fn rpl_mult(_rpl: f64) -> f64 { 127 | return (1.0 + ((1.0 / 30.0) * _rpl)) / (1.0 + 1.0 / 3.0); 128 | } 129 | 130 | pub(super) fn gpl_delta(_activity: &Activity) -> f64 { 131 | let difficulty_data = _activity.difficulty.get_difficulty_data(); 132 | let curve = difficulty_data.table; 133 | let rpl = _activity.rpl; 134 | let cap = if _activity.cap < difficulty_data.cap { 135 | _activity.cap 136 | } else { 137 | difficulty_data.cap 138 | }; 139 | let mut delta = _activity.player.pl as i32 - rpl as i32; 140 | if delta < -99 { 141 | return 0.0; 142 | } else if delta > cap { 143 | delta = cap; 144 | } 145 | let wep_delta_mult = WEAPON_DELTA_EXPONENT.powi(delta); 146 | let gear_delta_mult = curve.evaluate(delta as f64); 147 | wep_delta_mult * gear_delta_mult 148 | } 149 | 150 | // add_remove_pve_bonuses( 151 | // _rpl: f64, 152 | // _pl: u32, 153 | // _combatant_mult: f64, 154 | // _difficulty: DifficultyOptions, 155 | // _damage: f64, 156 | // ) -> f64 { 157 | // let rpl_mult = rpl_mult(_rpl); 158 | // let mut tmp_activity = Activity::default(); 159 | // tmp_activity.difficulty = _difficulty; 160 | // tmp_activity.rpl = _rpl as u32; 161 | // let gpl_delta = gpl_delta(tmp_activity, _pl); 162 | 163 | // _damage / (gpl_delta * rpl_mult * _combatant_mult) 164 | // } 165 | 166 | pub fn remove_pve_bonuses( 167 | _damage: f64, 168 | _combatant_mult: f64, 169 | _activity: &Activity, 170 | ) -> f64 { 171 | let rpl_mult = rpl_mult(_activity.rpl as f64); 172 | let gpl_delta = gpl_delta(_activity); 173 | 174 | _damage / (gpl_delta * rpl_mult * _combatant_mult) 175 | } 176 | -------------------------------------------------------------------------------- /src/activity/mod.rs: -------------------------------------------------------------------------------- 1 | use self::damage_calc::{gpl_delta, rpl_mult, DifficultyOptions}; 2 | 3 | pub mod damage_calc; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 6 | pub enum PlayerClass { 7 | Unknown = 0, 8 | Titan = 1, 9 | Hunter = 2, 10 | Warlock = 3, 11 | } 12 | impl Default for PlayerClass { 13 | fn default() -> Self { 14 | PlayerClass::Unknown 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone, Default)] 19 | pub struct Player { 20 | pub pl: u32, 21 | pub class: PlayerClass, 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct Activity { 26 | pub name: String, 27 | pub difficulty: DifficultyOptions, 28 | pub rpl: u32, 29 | pub cap: i32, 30 | pub player: Player, 31 | } 32 | impl Default for Activity { 33 | fn default() -> Self { 34 | let expansion_base = 1600; 35 | Activity { 36 | name: "Default".to_string(), 37 | difficulty: DifficultyOptions::default(), 38 | rpl: expansion_base, 39 | cap: 100, 40 | player: Player { 41 | pl: expansion_base + 210, 42 | class: PlayerClass::default(), 43 | }, 44 | } 45 | } 46 | } 47 | impl Activity { 48 | pub fn get_pl_delta(&self) -> f64 { 49 | gpl_delta(&self) 50 | } 51 | pub fn get_rpl_mult(&self) -> f64 { 52 | rpl_mult(self.rpl as f64) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/d2_enums.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] 6 | pub enum AmmoType { 7 | PRIMARY = 1, 8 | SPECIAL = 2, 9 | HEAVY = 3, 10 | UNKNOWN = 0, 11 | } 12 | impl From for AmmoType { 13 | fn from(_value: u32) -> AmmoType { 14 | match _value { 15 | 1 => AmmoType::PRIMARY, 16 | 2 => AmmoType::SPECIAL, 17 | 3 => AmmoType::HEAVY, 18 | _ => AmmoType::UNKNOWN, 19 | } 20 | } 21 | } 22 | 23 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] 24 | pub enum WeaponType { 25 | AUTORIFLE = 6, 26 | BOW = 31, 27 | FUSIONRIFLE = 11, 28 | GLAIVE = 33, 29 | GRENADELAUNCHER = 23, 30 | HANDCANNON = 9, 31 | LINEARFUSIONRIFLE = 22, 32 | MACHINEGUN = 8, 33 | PULSERIFLE = 13, 34 | ROCKET = 10, 35 | SCOUTRIFLE = 14, 36 | SHOTGUN = 7, 37 | SIDEARM = 17, 38 | SNIPER = 12, 39 | SUBMACHINEGUN = 24, 40 | SWORD = 18, 41 | TRACERIFLE = 25, 42 | UNKNOWN = 0, 43 | } 44 | impl From for WeaponType { 45 | fn from(_value: u32) -> WeaponType { 46 | match _value { 47 | 6 => WeaponType::AUTORIFLE, 48 | 31 => WeaponType::BOW, 49 | 11 => WeaponType::FUSIONRIFLE, 50 | 33 => WeaponType::GLAIVE, 51 | 23 => WeaponType::GRENADELAUNCHER, 52 | 9 => WeaponType::HANDCANNON, 53 | 22 => WeaponType::LINEARFUSIONRIFLE, 54 | 8 => WeaponType::MACHINEGUN, 55 | 13 => WeaponType::PULSERIFLE, 56 | 10 => WeaponType::ROCKET, 57 | 14 => WeaponType::SCOUTRIFLE, 58 | 7 => WeaponType::SHOTGUN, 59 | 17 => WeaponType::SIDEARM, 60 | 12 => WeaponType::SNIPER, 61 | 24 => WeaponType::SUBMACHINEGUN, 62 | 18 => WeaponType::SWORD, 63 | 25 => WeaponType::TRACERIFLE, 64 | _ => WeaponType::UNKNOWN, 65 | } 66 | } 67 | } 68 | 69 | #[allow(non_snake_case, non_camel_case_types)] 70 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 71 | pub enum StatHashes { 72 | ACCURACY, 73 | AIM_ASSIST, 74 | AIRBORNE, 75 | AMMO_CAPACITY, 76 | ATTACK, 77 | BLAST_RADIUS, 78 | CHARGE_RATE, 79 | CHARGE_TIME, 80 | DISCIPLINE, 81 | DRAW_TIME, 82 | GUARD_EFFICIENCY, 83 | GUARD_ENDURANCE, 84 | GUARD_RESISTANCE, 85 | HANDLING, 86 | IMPACT, 87 | INTELLECT, 88 | INVENTORY_SIZE, 89 | MAGAZINE, 90 | MOBILITY, 91 | POWER, 92 | RANGE, 93 | RECOIL_DIR, 94 | RECOVERY, 95 | RELOAD, 96 | RESILIENCE, 97 | RPM, 98 | SHIELD_DURATION, 99 | STABILITY, 100 | STRENGTH, 101 | SWING_SPEED, 102 | VELOCITY, 103 | ZOOM, 104 | UNKNOWN, 105 | } 106 | impl From for StatHashes { 107 | fn from(_value: u32) -> StatHashes { 108 | match _value { 109 | 1591432999 => StatHashes::ACCURACY, 110 | 1345609583 => StatHashes::AIM_ASSIST, 111 | 2714457168 => StatHashes::AIRBORNE, 112 | 925767036 => StatHashes::AMMO_CAPACITY, 113 | 1480404414 => StatHashes::ATTACK, 114 | 3614673599 => StatHashes::BLAST_RADIUS, 115 | 3022301683 => StatHashes::CHARGE_RATE, 116 | 2961396640 => StatHashes::CHARGE_TIME, 117 | 1735777505 => StatHashes::DISCIPLINE, 118 | 447667954 => StatHashes::DRAW_TIME, 119 | 2762071195 => StatHashes::GUARD_EFFICIENCY, 120 | 3736848092 => StatHashes::GUARD_ENDURANCE, 121 | 209426660 => StatHashes::GUARD_RESISTANCE, 122 | 943549884 => StatHashes::HANDLING, 123 | 4043523819 => StatHashes::IMPACT, 124 | 144602215 => StatHashes::INTELLECT, 125 | 1931675084 => StatHashes::INVENTORY_SIZE, 126 | 3871231066 => StatHashes::MAGAZINE, 127 | 2996146975 => StatHashes::MOBILITY, 128 | 1935470627 => StatHashes::POWER, 129 | 1240592695 => StatHashes::RANGE, 130 | 2715839340 => StatHashes::RECOIL_DIR, 131 | 1943323491 => StatHashes::RECOVERY, 132 | 4188031367 => StatHashes::RELOAD, 133 | 392767087 => StatHashes::RESILIENCE, 134 | 4284893193 => StatHashes::RPM, 135 | 1842278586 => StatHashes::SHIELD_DURATION, 136 | 155624089 => StatHashes::STABILITY, 137 | 4244567218 => StatHashes::STRENGTH, 138 | 2837207746 => StatHashes::SWING_SPEED, 139 | 2523465841 => StatHashes::VELOCITY, 140 | 3555269338 => StatHashes::ZOOM, 141 | _ => StatHashes::UNKNOWN, 142 | } 143 | } 144 | } 145 | impl Into for StatHashes { 146 | fn into(self) -> u32 { 147 | match self { 148 | StatHashes::ACCURACY => 1591432999, 149 | StatHashes::AIM_ASSIST => 1345609583, 150 | StatHashes::AIRBORNE => 2714457168, 151 | StatHashes::AMMO_CAPACITY => 925767036, 152 | StatHashes::ATTACK => 1480404414, 153 | StatHashes::BLAST_RADIUS => 3614673599, 154 | StatHashes::CHARGE_RATE => 3022301683, 155 | StatHashes::CHARGE_TIME => 2961396640, 156 | StatHashes::DISCIPLINE => 1735777505, 157 | StatHashes::DRAW_TIME => 447667954, 158 | StatHashes::GUARD_EFFICIENCY => 2762071195, 159 | StatHashes::GUARD_ENDURANCE => 3736848092, 160 | StatHashes::GUARD_RESISTANCE => 209426660, 161 | StatHashes::HANDLING => 943549884, 162 | StatHashes::IMPACT => 4043523819, 163 | StatHashes::INTELLECT => 144602215, 164 | StatHashes::INVENTORY_SIZE => 1931675084, 165 | StatHashes::MAGAZINE => 3871231066, 166 | StatHashes::MOBILITY => 2996146975, 167 | StatHashes::POWER => 1935470627, 168 | StatHashes::RANGE => 1240592695, 169 | StatHashes::RECOIL_DIR => 2715839340, 170 | StatHashes::RECOVERY => 1943323491, 171 | StatHashes::RELOAD => 4188031367, 172 | StatHashes::RESILIENCE => 392767087, 173 | StatHashes::RPM => 4284893193, 174 | StatHashes::SHIELD_DURATION => 1842278586, 175 | StatHashes::STABILITY => 155624089, 176 | StatHashes::STRENGTH => 4244567218, 177 | StatHashes::SWING_SPEED => 2837207746, 178 | StatHashes::VELOCITY => 2523465841, 179 | StatHashes::ZOOM => 3555269338, 180 | StatHashes::UNKNOWN => 0, 181 | } 182 | } 183 | } 184 | impl StatHashes { 185 | pub fn is_weapon_stat(&self) -> bool { 186 | match self { 187 | StatHashes::ACCURACY => true, 188 | StatHashes::AIM_ASSIST => true, 189 | StatHashes::AIRBORNE => true, 190 | StatHashes::AMMO_CAPACITY => true, 191 | StatHashes::ZOOM => true, 192 | StatHashes::RANGE => true, 193 | StatHashes::STABILITY => true, 194 | StatHashes::RELOAD => true, 195 | StatHashes::MAGAZINE => true, 196 | StatHashes::HANDLING => true, 197 | StatHashes::VELOCITY => true, 198 | StatHashes::BLAST_RADIUS => true, 199 | StatHashes::CHARGE_TIME => true, 200 | StatHashes::INVENTORY_SIZE => true, 201 | StatHashes::RECOIL_DIR => true, 202 | StatHashes::RPM => true, 203 | StatHashes::GUARD_EFFICIENCY => true, 204 | StatHashes::GUARD_ENDURANCE => true, 205 | StatHashes::GUARD_RESISTANCE => true, 206 | StatHashes::DRAW_TIME => true, 207 | StatHashes::SWING_SPEED => true, 208 | StatHashes::SHIELD_DURATION => true, 209 | StatHashes::IMPACT => true, 210 | StatHashes::CHARGE_RATE => true, 211 | _ => false, 212 | } 213 | } 214 | } 215 | 216 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] 217 | pub enum DamageType { 218 | ARC, 219 | VOID, 220 | SOLAR, 221 | STASIS, 222 | KINETIC, 223 | STRAND, 224 | UNKNOWN, 225 | } 226 | 227 | impl From for DamageType { 228 | fn from(_value: u32) -> DamageType { 229 | match _value { 230 | 2303181850 => DamageType::ARC, 231 | 3454344768 => DamageType::VOID, 232 | 1847026933 => DamageType::SOLAR, 233 | 151347233 => DamageType::STASIS, 234 | 3373582085 => DamageType::KINETIC, 235 | 3949783978 => DamageType::STRAND, 236 | _ => DamageType::UNKNOWN, 237 | } 238 | } 239 | } 240 | 241 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] 242 | pub enum DamageSource { 243 | SNIPER, 244 | MELEE, 245 | EXPLOSION, 246 | ENVIRONMENTAL, 247 | UNKNOWN, 248 | } 249 | 250 | pub type Seconds = f64; 251 | pub type MetersPerSecond = f64; 252 | pub type StatBump = i32; 253 | pub type BungieHash = u32; 254 | -------------------------------------------------------------------------------- /src/enemies/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::activity::Activity; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 4 | pub enum EnemyType { 5 | MINOR, 6 | ELITE, 7 | MINIBOSS, 8 | BOSS, 9 | VEHICLE, 10 | ENCLAVE, 11 | PLAYER, 12 | CHAMPION, 13 | } 14 | impl Default for EnemyType { 15 | fn default() -> Self { 16 | EnemyType::ENCLAVE 17 | } 18 | } 19 | 20 | #[derive(Debug, Clone, Default)] 21 | pub struct Enemy { 22 | pub health: f64, 23 | pub damage: f64, 24 | pub damage_resistance: f64, 25 | pub type_: EnemyType, 26 | pub tier: u8, 27 | } 28 | impl Enemy { 29 | pub fn get_adjusted_health(&self, _activity: Activity) -> f64 { 30 | self.health * (1.0 - self.damage_resistance) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 2 | pub enum LogLevel { 3 | Error, 4 | Warning, 5 | Info, 6 | Debug, 7 | } 8 | impl From for LogLevel { 9 | fn from(i: usize) -> Self { 10 | match i { 11 | 0 => LogLevel::Error, 12 | 1 => LogLevel::Warning, 13 | 2 => LogLevel::Info, 14 | 3 => LogLevel::Debug, 15 | _ => panic!("Invalid log level"), 16 | } 17 | } 18 | } 19 | impl From for usize { 20 | fn from(l: LogLevel) -> Self { 21 | match l { 22 | LogLevel::Error => 0, 23 | LogLevel::Warning => 1, 24 | LogLevel::Info => 2, 25 | LogLevel::Debug => 3, 26 | } 27 | } 28 | } 29 | impl Default for LogLevel { 30 | fn default() -> Self { 31 | LogLevel::Warning 32 | } 33 | } 34 | 35 | fn get_log_level() -> LogLevel { 36 | crate::PERS_DATA.with(|perm_data| perm_data.borrow().log_level) 37 | } 38 | 39 | pub fn extern_log(s: &str, log_level: LogLevel) { 40 | if log_level > get_log_level() { 41 | return; 42 | } 43 | #[cfg(feature = "wasm")] 44 | crate::console_log!("{}", s); 45 | #[cfg(not(feature = "wasm"))] 46 | println!("{}", s); 47 | } 48 | 49 | pub fn log(s: &str, log_level: usize) { 50 | extern_log(s, log_level.into()) 51 | } 52 | -------------------------------------------------------------------------------- /src/perks/buff_perks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::d2_enums::{AmmoType, DamageType, StatHashes, WeaponType}; 4 | 5 | use super::{ 6 | add_dmr, add_epr, add_fmr, add_hmr, add_mmr, add_rmr, add_rsmr, add_sbr, add_vmr, clamp, 7 | lib::{ 8 | CalculationInput, DamageModifierResponse, ExtraDamageResponse, FiringModifierResponse, 9 | HandlingModifierResponse, RangeModifierResponse, RefundResponse, ReloadModifierResponse, 10 | ReloadOverrideResponse, 11 | }, 12 | ModifierResponseInput, Perks, 13 | }; 14 | 15 | fn emp_buff(_cached_data: &mut HashMap, _desired_buff: f64) -> f64 { 16 | let current_buff = _cached_data.get("empowering").unwrap_or(&1.0).to_owned(); 17 | if current_buff >= _desired_buff { 18 | return 1.0; 19 | } else { 20 | _cached_data.insert("empowering".to_string(), _desired_buff); 21 | return _desired_buff / current_buff; 22 | } 23 | } 24 | 25 | fn srg_buff(_cached_data: &mut HashMap, _desired_tier: u32, _is_pvp: bool) -> f64 { 26 | let pve_buff = match _desired_tier { 27 | 0 => 1.0, 28 | 1 => 1.1, 29 | 2 => 1.17, 30 | 3 => 1.22, 31 | _ => 1.25, 32 | }; 33 | let pvp_buff = match _desired_tier { 34 | 0 => 1.0, 35 | 1 => 1.03, 36 | 2 => 1.045, 37 | 3 => 1.055, 38 | _ => 1.06, 39 | }; 40 | let buff = if _is_pvp { pvp_buff } else { pve_buff }; 41 | let current_buff = _cached_data.get("surge").unwrap_or(&1.0).to_owned(); 42 | if current_buff >= buff { 43 | return 1.0; 44 | } else { 45 | _cached_data.insert("surge".to_string(), buff); 46 | return buff / current_buff; 47 | } 48 | } 49 | 50 | fn gbl_debuff(_cached_data: &mut HashMap, _desired_buff: f64) -> f64 { 51 | let current_buff = _cached_data.get("debuff").unwrap_or(&1.0).to_owned(); 52 | if current_buff >= _desired_buff { 53 | return 1.0; 54 | } else { 55 | _cached_data.insert("debuff".to_string(), _desired_buff); 56 | return _desired_buff / current_buff; 57 | } 58 | } 59 | 60 | //surge mod dmr is in meta_perks.rs 61 | 62 | // 63 | // BUFFS 64 | // 65 | pub fn buff_perks() { 66 | add_dmr( 67 | Perks::WellOfRadiance, 68 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 69 | let buff = emp_buff(_input.cached_data, 1.25); 70 | DamageModifierResponse { 71 | impact_dmg_scale: buff, 72 | explosive_dmg_scale: buff, 73 | ..Default::default() 74 | } 75 | }), 76 | ); 77 | 78 | add_dmr( 79 | Perks::NobleRounds, 80 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 81 | if _input.value == 0 { 82 | return DamageModifierResponse::default(); 83 | } 84 | let des_buff = if _input.pvp { 1.15 } else { 1.35 }; 85 | let buff = emp_buff(_input.cached_data, des_buff); 86 | DamageModifierResponse { 87 | impact_dmg_scale: buff, 88 | explosive_dmg_scale: buff, 89 | ..Default::default() 90 | } 91 | }), 92 | ); 93 | 94 | add_dmr( 95 | Perks::Radiant, 96 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 97 | let des_buff = if _input.pvp { 1.1 } else { 1.25 }; 98 | let buff = emp_buff(_input.cached_data, des_buff); 99 | _input.cached_data.insert("radiant".to_string(), 1.0); 100 | DamageModifierResponse { 101 | impact_dmg_scale: buff, 102 | explosive_dmg_scale: buff, 103 | ..Default::default() 104 | } 105 | }), 106 | ); 107 | 108 | add_dmr( 109 | Perks::PathOfTheBurningSteps, 110 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 111 | let buff = srg_buff(_input.cached_data, _input.value, _input.pvp); 112 | DamageModifierResponse { 113 | impact_dmg_scale: if *_input.calc_data.damage_type == DamageType::SOLAR { 114 | buff 115 | } else { 116 | 1.0 117 | }, 118 | explosive_dmg_scale: if *_input.calc_data.damage_type == DamageType::SOLAR { 119 | buff 120 | } else { 121 | 1.0 122 | }, 123 | ..Default::default() 124 | } 125 | }), 126 | ); 127 | 128 | add_dmr( 129 | Perks::EternalWarrior, 130 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 131 | let buff = srg_buff(_input.cached_data, _input.value, _input.pvp); 132 | DamageModifierResponse { 133 | impact_dmg_scale: if *_input.calc_data.damage_type == DamageType::ARC { 134 | buff 135 | } else { 136 | 1.0 137 | }, 138 | explosive_dmg_scale: if *_input.calc_data.damage_type == DamageType::ARC { 139 | buff 140 | } else { 141 | 1.0 142 | }, 143 | ..Default::default() 144 | } 145 | }), 146 | ); 147 | 148 | add_dmr( 149 | Perks::BannerShield, 150 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 151 | let des_buff = if _input.pvp { 1.35 } else { 1.4 }; 152 | let buff = emp_buff(_input.cached_data, des_buff); 153 | DamageModifierResponse { 154 | impact_dmg_scale: buff, 155 | explosive_dmg_scale: buff, 156 | ..Default::default() 157 | } 158 | }), 159 | ); 160 | 161 | add_dmr( 162 | Perks::EmpRift, 163 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 164 | let des_buff = if _input.pvp { 1.15 } else { 1.2 }; 165 | let buff = emp_buff(_input.cached_data, des_buff); 166 | DamageModifierResponse { 167 | impact_dmg_scale: buff, 168 | explosive_dmg_scale: buff, 169 | ..Default::default() 170 | } 171 | }), 172 | ); 173 | 174 | add_dmr( 175 | Perks::MantleOfBattleHarmony, 176 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 177 | let tier = if _input.value > 0 { 4 } else { 0 }; 178 | let buff = srg_buff(_input.cached_data, tier, _input.pvp); 179 | DamageModifierResponse { 180 | impact_dmg_scale: buff, 181 | explosive_dmg_scale: buff, 182 | ..Default::default() 183 | } 184 | }), 185 | ); 186 | 187 | add_dmr( 188 | Perks::SanguineAlchemy, 189 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 190 | let tier = if _input.value > 0 { 2 } else { 0 }; 191 | let buff = srg_buff(_input.cached_data, tier, _input.pvp); 192 | DamageModifierResponse { 193 | impact_dmg_scale: buff, 194 | explosive_dmg_scale: buff, 195 | ..Default::default() 196 | } 197 | }), 198 | ); 199 | 200 | add_dmr( 201 | Perks::WardOfDawn, 202 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 203 | let buff = emp_buff(_input.cached_data, 1.25); 204 | DamageModifierResponse { 205 | impact_dmg_scale: buff, 206 | explosive_dmg_scale: buff, 207 | ..Default::default() 208 | } 209 | }), 210 | ); 211 | 212 | add_dmr( 213 | Perks::Gyrfalcon, 214 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 215 | let des_buff = if _input.pvp { 1.0 } else { 1.35 }; 216 | let buff = emp_buff(_input.cached_data, des_buff); 217 | DamageModifierResponse { 218 | impact_dmg_scale: buff, 219 | explosive_dmg_scale: buff, 220 | ..Default::default() 221 | } 222 | }), 223 | ); 224 | 225 | add_dmr( 226 | Perks::AeonInsight, 227 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 228 | let des_buff = if _input.pvp { 1.0 } else { 1.35 }; 229 | let buff = emp_buff(_input.cached_data, des_buff); 230 | DamageModifierResponse { 231 | impact_dmg_scale: buff, 232 | explosive_dmg_scale: buff, 233 | ..Default::default() 234 | } 235 | }), 236 | ); 237 | 238 | add_dmr( 239 | Perks::UmbralSharpening, 240 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 241 | let pve_values = [1.2, 1.25, 1.35, 1.4]; 242 | let des_buff = if _input.pvp { 243 | 1.0 244 | } else { 245 | pve_values[clamp(_input.value, 0, 3) as usize] 246 | }; 247 | let buff = emp_buff(_input.cached_data, des_buff); 248 | DamageModifierResponse { 249 | impact_dmg_scale: buff, 250 | explosive_dmg_scale: buff, 251 | ..Default::default() 252 | } 253 | }), 254 | ); 255 | 256 | // 257 | // DEBUFFS 258 | // 259 | 260 | add_dmr( 261 | Perks::Weaken, 262 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 263 | let des_debuff = if _input.pvp { 1.075 } else { 1.15 }; 264 | let debuff = gbl_debuff(_input.cached_data, des_debuff); 265 | DamageModifierResponse { 266 | impact_dmg_scale: debuff, 267 | explosive_dmg_scale: debuff, 268 | ..Default::default() 269 | } 270 | }), 271 | ); 272 | 273 | add_dmr( 274 | Perks::TractorCannon, 275 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 276 | let des_debuff = if _input.pvp { 1.5 } else { 1.3 }; 277 | let debuff = gbl_debuff(_input.cached_data, des_debuff); 278 | DamageModifierResponse { 279 | impact_dmg_scale: debuff, 280 | explosive_dmg_scale: debuff, 281 | ..Default::default() 282 | } 283 | }), 284 | ); 285 | 286 | add_dmr( 287 | Perks::MoebiusQuiver, 288 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 289 | let des_debuff = if _input.pvp { 1.5 } else { 1.3 }; 290 | let debuff = gbl_debuff(_input.cached_data, des_debuff); 291 | DamageModifierResponse { 292 | impact_dmg_scale: debuff, 293 | explosive_dmg_scale: debuff, 294 | ..Default::default() 295 | } 296 | }), 297 | ); 298 | add_dmr( 299 | Perks::DeadFall, 300 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 301 | let des_debuff = if _input.pvp { 1.5 } else { 1.3 }; 302 | let debuff = gbl_debuff(_input.cached_data, des_debuff); 303 | DamageModifierResponse { 304 | impact_dmg_scale: debuff, 305 | explosive_dmg_scale: debuff, 306 | ..Default::default() 307 | } 308 | }), 309 | ); 310 | add_dmr( 311 | Perks::Felwinters, 312 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 313 | let debuff = gbl_debuff(_input.cached_data, 1.3); 314 | DamageModifierResponse { 315 | impact_dmg_scale: debuff, 316 | explosive_dmg_scale: debuff, 317 | ..Default::default() 318 | } 319 | }), 320 | ); 321 | 322 | add_dmr( 323 | Perks::EnhancedScannerAugment, 324 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 325 | let pve_values = [1.08, 1.137, 1.173, 1.193, 1.2]; 326 | let des_debuff = if _input.pvp { 327 | 1.0 328 | } else { 329 | pve_values[clamp(_input.value, 0, 4) as usize] 330 | }; 331 | let debuff = gbl_debuff(_input.cached_data, des_debuff); 332 | DamageModifierResponse { 333 | impact_dmg_scale: debuff, 334 | explosive_dmg_scale: debuff, 335 | ..Default::default() 336 | } 337 | }), 338 | ); 339 | 340 | add_dmr( 341 | Perks::MaskOfBakris, 342 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 343 | let mut tier = if _input.value > 0 { 4 } else { 0 }; 344 | if *_input.calc_data.damage_type != DamageType::ARC && *_input.calc_data.damage_type != DamageType::STASIS { 345 | tier = 0; 346 | } 347 | let buff = srg_buff(_input.cached_data, tier, _input.pvp); 348 | DamageModifierResponse { 349 | impact_dmg_scale: buff, 350 | explosive_dmg_scale: buff, 351 | ..Default::default() 352 | } 353 | }), 354 | ); 355 | 356 | add_dmr( 357 | Perks::RaijusHarness, 358 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 359 | let buff = if _input.value > 0 && *_input.calc_data.damage_type == DamageType::ARC { 1.15 } else { 1.0 }; 360 | DamageModifierResponse { 361 | impact_dmg_scale: buff, 362 | explosive_dmg_scale: buff, 363 | ..Default::default() 364 | } 365 | }), 366 | ); 367 | } 368 | -------------------------------------------------------------------------------- /src/perks/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | d2_enums::{AmmoType, BungieHash, DamageSource, DamageType, StatBump, StatHashes, WeaponType}, 3 | enemies::EnemyType, 4 | types::rs_types::{FiringData, HandlingResponse}, 5 | weapons::Stat, 6 | }; 7 | use serde::Serialize; 8 | use std::{cell::RefCell, collections::HashMap, ops::Mul}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct CalculationInput<'a> { 12 | pub intrinsic_hash: u32, 13 | pub curr_firing_data: &'a FiringData, 14 | pub base_crit_mult: f64, 15 | pub shots_fired_this_mag: f64, 16 | pub total_shots_fired: f64, 17 | pub total_shots_hit: f64, 18 | pub base_mag: f64, 19 | pub curr_mag: f64, 20 | pub reserves_left: f64, 21 | pub time_total: f64, 22 | pub time_this_mag: f64, 23 | pub stats: &'a HashMap, 24 | pub weapon_type: &'a WeaponType, 25 | pub damage_type: &'a DamageType, 26 | pub ammo_type: &'a AmmoType, 27 | pub handling_data: HandlingResponse, 28 | pub num_reloads: f64, 29 | pub enemy_type: &'a EnemyType, 30 | pub perk_value_map: &'a HashMap, 31 | pub has_overshield: bool, 32 | } 33 | impl<'a> CalculationInput<'a> { 34 | //stuff like mag size can use this, not reload, damage, etc. 35 | pub fn construct_pve_sparse( 36 | _intrinsic_hash: u32, 37 | _firing_data: &'a FiringData, 38 | _stats: &'a HashMap, 39 | _perk_value_map: &'a HashMap, 40 | _weapon_type: &'a WeaponType, 41 | _ammo_type: &'a AmmoType, 42 | _damage_type: &'a DamageType, 43 | _base_damage: f64, 44 | _base_crit_mult: f64, 45 | _base_mag_size: i32, 46 | _total_shots_hit: i32, 47 | _total_time: f64, 48 | ) -> Self { 49 | Self { 50 | intrinsic_hash: _intrinsic_hash, 51 | curr_firing_data: &_firing_data, 52 | base_crit_mult: _base_crit_mult, 53 | shots_fired_this_mag: 0.0, 54 | total_shots_fired: _total_shots_hit as f64, 55 | total_shots_hit: _total_shots_hit as f64, 56 | base_mag: _base_mag_size as f64, 57 | curr_mag: _base_mag_size as f64, 58 | reserves_left: 100.0, 59 | time_total: _total_time, 60 | time_this_mag: -1.0, 61 | stats: &_stats, 62 | weapon_type: &_weapon_type, 63 | damage_type: _damage_type, 64 | ammo_type: &_ammo_type, 65 | handling_data: HandlingResponse::default(), 66 | num_reloads: 0.0, 67 | enemy_type: &EnemyType::BOSS, 68 | perk_value_map: _perk_value_map, 69 | has_overshield: false, 70 | } 71 | } 72 | pub fn construct_pvp( 73 | _intrinsic_hash: u32, 74 | _firing_data: &'a FiringData, 75 | _stats: &'a HashMap, 76 | _perk_value_map: &'a HashMap, 77 | _weapon_type: &'a WeaponType, 78 | _ammo_type: &'a AmmoType, 79 | _base_damage: f64, 80 | _base_crit_mult: f64, 81 | _mag_size: f64, 82 | _has_overshield: bool, 83 | _handling_data: HandlingResponse, 84 | ) -> Self { 85 | Self { 86 | intrinsic_hash: _intrinsic_hash, 87 | curr_firing_data: _firing_data, 88 | base_crit_mult: _base_crit_mult, 89 | shots_fired_this_mag: 0.0, 90 | total_shots_fired: 0.0, 91 | total_shots_hit: 0.0, 92 | base_mag: _mag_size, 93 | curr_mag: _mag_size, 94 | reserves_left: 999.0, 95 | time_total: 0.0, 96 | time_this_mag: 0.0, 97 | stats: _stats, 98 | weapon_type: _weapon_type, 99 | damage_type: &DamageType::STASIS, 100 | ammo_type: _ammo_type, 101 | handling_data: _handling_data, 102 | num_reloads: 0.0, 103 | enemy_type: &EnemyType::PLAYER, 104 | perk_value_map: _perk_value_map, 105 | has_overshield: _has_overshield, 106 | } 107 | } 108 | pub fn construct_static( 109 | _intrinsic_hash: u32, 110 | _firing_data: &'a FiringData, 111 | _stats: &'a HashMap, 112 | _perk_value_map: &'a HashMap, 113 | _weapon_type: &'a WeaponType, 114 | _ammo_type: &'a AmmoType, 115 | _damage_type: &'a DamageType, 116 | _crit_mult: f64, 117 | ) -> Self { 118 | Self { 119 | intrinsic_hash: _intrinsic_hash, 120 | curr_firing_data: _firing_data, 121 | base_crit_mult: _crit_mult, 122 | shots_fired_this_mag: 0.0, 123 | total_shots_fired: 0.0, 124 | total_shots_hit: 0.0, 125 | base_mag: 10.0, 126 | curr_mag: 10.0, 127 | reserves_left: 100.0, 128 | time_total: 0.0, 129 | time_this_mag: 0.0, 130 | stats: _stats, 131 | weapon_type: _weapon_type, 132 | damage_type: _damage_type, 133 | ammo_type: _ammo_type, 134 | handling_data: HandlingResponse::default(), 135 | num_reloads: 0.0, 136 | enemy_type: &EnemyType::ENCLAVE, 137 | perk_value_map: _perk_value_map, 138 | has_overshield: false, 139 | } 140 | } 141 | } 142 | 143 | #[derive(Debug, Clone, PartialEq, Serialize)] 144 | pub struct DamageModifierResponse { 145 | pub impact_dmg_scale: f64, 146 | pub explosive_dmg_scale: f64, 147 | pub crit_scale: f64, 148 | } 149 | impl Default for DamageModifierResponse { 150 | fn default() -> Self { 151 | Self { 152 | impact_dmg_scale: 1.0, 153 | explosive_dmg_scale: 1.0, 154 | crit_scale: 1.0, 155 | } 156 | } 157 | } 158 | 159 | #[derive(Debug, Clone, PartialEq)] 160 | pub struct ExtraDamageResponse { 161 | pub additive_damage: f64, 162 | pub time_for_additive_damage: f64, 163 | //basically is this happening concurrently with the main damage? 164 | pub increment_total_time: bool, 165 | // will increment shots hit but not shots fired, shots fired is what *most* 166 | // perks use for calculation EDR shouldn't mess with other perks in unwanted ways 167 | pub times_to_hit: i32, 168 | //is_dot takes priority; makes it put dmg*count at in-time+time_for_additive_damage 169 | //instead of adding time_for_additive_damage between each count 170 | pub hit_at_same_time: bool, 171 | //if its a dot the dps calculator will count backwards and apply the dmg 172 | pub is_dot: bool, 173 | //pl scalling will apply no matter what 174 | pub weapon_scale: bool, 175 | pub crit_scale: bool, 176 | pub combatant_scale: bool, 177 | } 178 | impl Default for ExtraDamageResponse { 179 | fn default() -> Self { 180 | Self { 181 | additive_damage: 0.0, 182 | time_for_additive_damage: 0.0, 183 | increment_total_time: false, 184 | times_to_hit: 0, 185 | hit_at_same_time: true, 186 | is_dot: false, 187 | weapon_scale: false, 188 | crit_scale: false, 189 | combatant_scale: false, 190 | } 191 | } 192 | } 193 | 194 | #[derive(Debug, Clone, PartialEq, Serialize)] 195 | pub struct ReloadModifierResponse { 196 | pub reload_stat_add: i32, 197 | pub reload_time_scale: f64, 198 | } 199 | impl Default for ReloadModifierResponse { 200 | fn default() -> Self { 201 | Self { 202 | reload_stat_add: 0, 203 | reload_time_scale: 1.0, 204 | } 205 | } 206 | } 207 | 208 | #[derive(Debug, Clone, PartialEq, Serialize)] 209 | pub struct FiringModifierResponse { 210 | pub burst_delay_scale: f64, 211 | pub burst_delay_add: f64, 212 | pub inner_burst_scale: f64, 213 | pub burst_size_add: f64, 214 | } 215 | impl Default for FiringModifierResponse { 216 | fn default() -> Self { 217 | Self { 218 | burst_delay_scale: 1.0, 219 | burst_delay_add: 0.0, 220 | inner_burst_scale: 1.0, 221 | burst_size_add: 0.0, 222 | } 223 | } 224 | } 225 | 226 | #[derive(Debug, Clone, PartialEq, Serialize)] 227 | pub struct HandlingModifierResponse { 228 | pub stat_add: i32, 229 | pub stow_scale: f64, 230 | pub draw_scale: f64, 231 | // pub handling_swap_scale: f64, 232 | pub ads_scale: f64, 233 | } 234 | impl Default for HandlingModifierResponse { 235 | fn default() -> Self { 236 | Self { 237 | stat_add: 0, 238 | stow_scale: 1.0, 239 | draw_scale: 1.0, 240 | ads_scale: 1.0, 241 | } 242 | } 243 | } 244 | 245 | #[derive(Debug, Clone, PartialEq, Serialize)] 246 | pub struct RangeModifierResponse { 247 | pub range_stat_add: i32, 248 | pub range_all_scale: f64, 249 | pub range_hip_scale: f64, 250 | pub range_zoom_scale: f64, 251 | } 252 | impl Default for RangeModifierResponse { 253 | fn default() -> Self { 254 | Self { 255 | range_stat_add: 0, 256 | range_all_scale: 1.0, 257 | range_hip_scale: 1.0, 258 | range_zoom_scale: 1.0, 259 | } 260 | } 261 | } 262 | 263 | #[derive(Debug, Clone, PartialEq)] 264 | pub struct RefundResponse { 265 | pub crit: bool, 266 | pub requirement: i32, 267 | pub refund_mag: i32, 268 | pub refund_reserves: i32, 269 | } 270 | impl Default for RefundResponse { 271 | fn default() -> Self { 272 | Self { 273 | crit: false, 274 | requirement: 0, 275 | refund_mag: 0, 276 | refund_reserves: 0, 277 | } 278 | } 279 | } 280 | #[derive(Debug, Clone, PartialEq, Serialize)] 281 | pub struct MagazineModifierResponse { 282 | pub magazine_stat_add: i32, 283 | pub magazine_scale: f64, 284 | pub magazine_add: f64, 285 | } 286 | impl Default for MagazineModifierResponse { 287 | fn default() -> Self { 288 | Self { 289 | magazine_stat_add: 0, 290 | magazine_scale: 1.0, 291 | magazine_add: 0.0, 292 | } 293 | } 294 | } 295 | 296 | #[derive(Debug, Clone, PartialEq, Serialize)] 297 | pub struct InventoryModifierResponse { 298 | pub inv_stat_add: i32, 299 | pub inv_scale: f64, 300 | pub inv_add: f64, 301 | } 302 | impl Default for InventoryModifierResponse { 303 | fn default() -> Self { 304 | Self { 305 | inv_stat_add: 0, 306 | inv_scale: 1.0, 307 | inv_add: 0.0, 308 | } 309 | } 310 | } 311 | 312 | #[derive(Debug, Clone, PartialEq, Serialize)] 313 | pub struct FlinchModifierResponse { 314 | pub flinch_scale: f64, 315 | } 316 | impl Default for FlinchModifierResponse { 317 | fn default() -> Self { 318 | Self { flinch_scale: 1.0 } 319 | } 320 | } 321 | 322 | #[derive(Debug, Clone, PartialEq)] 323 | pub struct VelocityModifierResponse { 324 | pub velocity_scaler: f64, 325 | } 326 | impl Default for VelocityModifierResponse { 327 | fn default() -> Self { 328 | Self { 329 | velocity_scaler: 1.0, 330 | } 331 | } 332 | } 333 | 334 | #[derive(Debug, Clone, PartialEq)] 335 | pub struct ReloadOverrideResponse { 336 | pub valid: bool, 337 | pub reload_time: f64, 338 | pub ammo_to_reload: i32, 339 | pub priority: i32, 340 | pub count_as_reload: bool, 341 | pub uses_ammo: bool, 342 | } 343 | impl ReloadOverrideResponse { 344 | pub fn invalid() -> Self { 345 | Self { 346 | //an easy way for dps calculator to throw out 347 | valid: false, 348 | reload_time: 0.0, 349 | ammo_to_reload: 0, 350 | priority: 0, 351 | //this will also reset mag stats 352 | count_as_reload: false, 353 | uses_ammo: false, 354 | } 355 | } 356 | } 357 | 358 | #[derive(Debug, Clone, PartialEq)] 359 | pub struct ExplosivePercentResponse { 360 | pub percent: f64, 361 | pub delyed: f64, 362 | pub retain_base_total: bool, 363 | } 364 | impl Default for ExplosivePercentResponse { 365 | fn default() -> Self { 366 | Self { 367 | percent: 0.0, 368 | delyed: 0.0, 369 | retain_base_total: false, 370 | } 371 | } 372 | } 373 | 374 | #[derive(Debug, Clone, PartialEq, Serialize)] 375 | pub struct DamageResistModifierResponse { 376 | pub body_shot_resist: f64, 377 | pub head_shot_resist: f64, 378 | pub element: Option, 379 | pub source: Option, 380 | } 381 | impl Default for DamageResistModifierResponse { 382 | fn default() -> Self { 383 | Self { 384 | body_shot_resist: 1.0, 385 | head_shot_resist: 1.0, 386 | element: None, 387 | source: None, 388 | } 389 | } 390 | } 391 | 392 | #[derive(Debug, Clone, PartialEq, Serialize)] 393 | pub struct ModifierResponseSummary { 394 | pub rmr: Option, 395 | pub dmr: Option, 396 | pub hmr: Option, 397 | pub fmr: Option, 398 | pub flmr: Option, 399 | pub rsmr: Option, 400 | pub mmr: Option, 401 | pub imr: Option, 402 | pub drmr: Option, 403 | pub statbump: Option>, 404 | } 405 | 406 | impl Default for ModifierResponseSummary { 407 | fn default() -> Self { 408 | Self { 409 | rmr: None, 410 | dmr: None, 411 | hmr: None, 412 | fmr: None, 413 | flmr: None, 414 | rsmr: None, 415 | mmr: None, 416 | imr: None, 417 | drmr: None, 418 | statbump: None, 419 | } 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/perks/meta_perks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | d2_enums::{AmmoType, DamageType, Seconds, StatHashes, WeaponType}, 5 | weapons::Stat, 6 | }; 7 | 8 | use super::{ 9 | add_dmr, add_epr, add_flmr, add_fmr, add_hmr, add_imr, add_mmr, add_rmr, add_rsmr, add_sbr, 10 | add_vmr, clamp, 11 | lib::{ 12 | CalculationInput, DamageModifierResponse, ExplosivePercentResponse, ExtraDamageResponse, 13 | FiringModifierResponse, FlinchModifierResponse, HandlingModifierResponse, 14 | InventoryModifierResponse, MagazineModifierResponse, RangeModifierResponse, RefundResponse, 15 | ReloadModifierResponse, 16 | }, 17 | ModifierResponseInput, Perks, 18 | }; 19 | 20 | pub fn meta_perks() { 21 | add_dmr( 22 | Perks::BuiltIn, 23 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 24 | let mut crit_scale = 1.0; 25 | let mut dmg_scale = 1.0; 26 | if *_input.calc_data.weapon_type == WeaponType::LINEARFUSIONRIFLE && !_input.pvp { 27 | crit_scale *= 1.15; 28 | }; 29 | if *_input.calc_data.damage_type == DamageType::KINETIC && !_input.pvp { 30 | if _input.calc_data.ammo_type == &AmmoType::PRIMARY { 31 | dmg_scale *= 1.1; 32 | } else if _input.calc_data.ammo_type == &AmmoType::SPECIAL { 33 | dmg_scale *= 1.15; 34 | }; 35 | }; 36 | if *_input 37 | .calc_data 38 | .perk_value_map 39 | .get(&_input.calc_data.intrinsic_hash) 40 | .unwrap_or(&0) 41 | > 1 42 | && _input.calc_data.intrinsic_hash < 1000 43 | { 44 | let stat_bump_id: StatHashes = _input 45 | .calc_data 46 | .perk_value_map 47 | .get(&_input.calc_data.intrinsic_hash) 48 | .unwrap() 49 | .to_owned() 50 | .into(); 51 | if stat_bump_id == StatHashes::CHARGE_TIME 52 | && _input.calc_data.weapon_type == &WeaponType::FUSIONRIFLE 53 | { 54 | // dmg_scale *= 55 | // dmr_chargetime_mw(_input, _input.value, is_enhanced, _pvp, _cached_data).impact_dmg_scale; 56 | } 57 | } 58 | DamageModifierResponse { 59 | crit_scale, 60 | impact_dmg_scale: dmg_scale, 61 | explosive_dmg_scale: dmg_scale, 62 | } 63 | }), 64 | ); 65 | 66 | add_fmr( 67 | Perks::BuiltIn, 68 | Box::new(|_input: ModifierResponseInput| -> FiringModifierResponse { 69 | #[allow(unused_mut)] 70 | let mut delay_add = 0.0; 71 | if *_input 72 | .calc_data 73 | .perk_value_map 74 | .get(&_input.calc_data.intrinsic_hash) 75 | .unwrap_or(&0) 76 | > 1 77 | && _input.calc_data.intrinsic_hash < 1000 78 | { 79 | let stat_bump_id: StatHashes = _input 80 | .calc_data 81 | .perk_value_map 82 | .get(&_input.calc_data.intrinsic_hash) 83 | .unwrap() 84 | .to_owned() 85 | .into(); 86 | if stat_bump_id == StatHashes::CHARGE_TIME { 87 | // delay_add += fmr_accelerated_coils(_input, _input.value, is_enhanced, _pvp, _cached_data) 88 | // .burst_delay_add; 89 | } 90 | } 91 | 92 | if _input.calc_data.weapon_type == &WeaponType::BOW { 93 | let draw_time = _input 94 | .calc_data 95 | .stats 96 | .get(&StatHashes::DRAW_TIME.into()) 97 | .unwrap() 98 | .clone(); 99 | delay_add += match _input.calc_data.intrinsic_hash { 100 | //Lightweights, Wishender, Ticcus, Verglas 101 | 905 | 1470121888 | 3239299468 | 2636679416 => { 102 | (draw_time.perk_val() as f64 * -4.0 + 900.0) / 1100.0 103 | } 104 | //Precisions, Lemon, Trinity, Hierarchy 105 | 906 | 2186532310 | 1573888036 | 2226793914 => { 106 | (draw_time.perk_val() as f64 * -3.6 + 900.0) / 1100.0 107 | } 108 | //Levi Breath lol 109 | 1699724249 => (draw_time.perk_val() as f64 * -5.0 + 1428.0) / 1100.0, 110 | _ => 0.0, 111 | }; 112 | } 113 | FiringModifierResponse { 114 | burst_delay_add: delay_add, 115 | ..Default::default() 116 | } 117 | }), 118 | ); 119 | 120 | add_epr( 121 | Perks::BuiltIn, 122 | Box::new( 123 | |_input: ModifierResponseInput| -> ExplosivePercentResponse { 124 | if *_input.calc_data.weapon_type == WeaponType::GRENADELAUNCHER { 125 | let blast_radius_struct = 126 | _input.calc_data.stats.get(&StatHashes::BLAST_RADIUS.into()); 127 | let blast_radius; 128 | if blast_radius_struct.is_none() { 129 | blast_radius = 0; 130 | } else { 131 | blast_radius = blast_radius_struct.unwrap().perk_val(); 132 | }; 133 | if _input.calc_data.ammo_type == &AmmoType::SPECIAL { 134 | return ExplosivePercentResponse { 135 | percent: 0.5 + 0.003 * blast_radius as f64, 136 | delyed: 0.0, 137 | retain_base_total: true, 138 | }; 139 | } else if _input.calc_data.ammo_type == &AmmoType::HEAVY { 140 | return ExplosivePercentResponse { 141 | percent: 0.7 + 0.00175 * blast_radius as f64, 142 | delyed: 0.0, 143 | retain_base_total: true, 144 | }; 145 | }; 146 | } 147 | if *_input.calc_data.weapon_type == WeaponType::ROCKET 148 | && _input.calc_data.intrinsic_hash < 1000 149 | //ensures not exotic 150 | { 151 | return ExplosivePercentResponse { 152 | percent: 0.28, 153 | delyed: 0.0, 154 | retain_base_total: true, 155 | }; 156 | } 157 | ExplosivePercentResponse { 158 | percent: 0.0, 159 | delyed: 0.0, 160 | retain_base_total: true, 161 | } 162 | }, 163 | ), 164 | ); 165 | 166 | add_hmr( 167 | Perks::DexterityMod, 168 | Box::new( 169 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 170 | let swap_scale = if _input.value > 0 { 171 | 0.85 - clamp(_input.value, 1, 3) as f64 * 0.05 172 | } else { 173 | 1.0 174 | }; 175 | HandlingModifierResponse { 176 | stow_scale: swap_scale, 177 | draw_scale: swap_scale, 178 | ..Default::default() 179 | } 180 | }, 181 | ), 182 | ); 183 | 184 | add_hmr( 185 | Perks::TargetingMod, 186 | Box::new( 187 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 188 | HandlingModifierResponse { 189 | ads_scale: if _input.value > 0 { 0.75 } else { 1.0 }, 190 | ..Default::default() 191 | } 192 | }, 193 | ), 194 | ); 195 | 196 | add_sbr( 197 | Perks::TargetingMod, 198 | Box::new(|_input: ModifierResponseInput| -> HashMap { 199 | let mut stats = HashMap::new(); 200 | if _input.value == 1 { 201 | stats.insert(StatHashes::AIM_ASSIST.into(), 5); 202 | } else if _input.value == 2 { 203 | stats.insert(StatHashes::AIM_ASSIST.into(), 8); 204 | } else if _input.value > 2 { 205 | stats.insert(StatHashes::AIM_ASSIST.into(), 10); 206 | } 207 | stats 208 | }), 209 | ); 210 | 211 | add_imr( 212 | Perks::ReserveMod, 213 | Box::new( 214 | |_input: ModifierResponseInput| -> InventoryModifierResponse { 215 | let mut inv_buff = if _input.value > 0 { 20 } else { 0 }; 216 | if _input.value == 2 { 217 | inv_buff += 20; 218 | } 219 | if _input.value > 2 { 220 | inv_buff += 30; 221 | } 222 | InventoryModifierResponse { 223 | inv_stat_add: inv_buff, 224 | inv_scale: 1.0, 225 | inv_add: 0.0, 226 | } 227 | }, 228 | ), 229 | ); 230 | 231 | add_sbr( 232 | Perks::ReserveMod, 233 | Box::new(|_input: ModifierResponseInput| -> HashMap { 234 | let mut inv_buff = if _input.value > 0 { 20 } else { 0 }; 235 | if _input.value == 2 { 236 | inv_buff += 15; 237 | } 238 | if _input.value > 2 { 239 | inv_buff += 20; 240 | } 241 | let mut stats = HashMap::new(); 242 | stats.insert(StatHashes::INVENTORY_SIZE.into(), inv_buff); 243 | stats 244 | }), 245 | ); 246 | 247 | add_rsmr( 248 | Perks::LoaderMod, 249 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 250 | if _input.value > 0 { 251 | let mut reload_stat_buff = 10; 252 | if _input.value > 1 { 253 | reload_stat_buff += 5; 254 | }; 255 | if _input.value > 2 { 256 | reload_stat_buff += 5; 257 | }; 258 | return ReloadModifierResponse { 259 | reload_stat_add: reload_stat_buff, 260 | reload_time_scale: 0.85, 261 | }; 262 | } else { 263 | return ReloadModifierResponse::default(); 264 | }; 265 | }), 266 | ); 267 | 268 | add_sbr( 269 | Perks::LoaderMod, 270 | Box::new(|_input: ModifierResponseInput| -> HashMap { 271 | let mut stats = HashMap::new(); 272 | if _input.value > 0 { 273 | let mut reload_stat_buff = 10; 274 | if _input.value > 1 { 275 | reload_stat_buff += 5; 276 | }; 277 | if _input.value > 2 { 278 | reload_stat_buff += 5; 279 | }; 280 | stats.insert(StatHashes::RELOAD.into(), reload_stat_buff); 281 | }; 282 | stats 283 | }), 284 | ); 285 | 286 | add_flmr( 287 | Perks::UnflinchingMod, 288 | Box::new(|_input: ModifierResponseInput| -> FlinchModifierResponse { 289 | if _input.value > 2 { 290 | FlinchModifierResponse { flinch_scale: 0.6 } 291 | } else if _input.value == 2 { 292 | FlinchModifierResponse { flinch_scale: 0.7 } 293 | } else if _input.value == 1 { 294 | FlinchModifierResponse { flinch_scale: 0.75 } 295 | } else { 296 | FlinchModifierResponse::default() 297 | } 298 | }), 299 | ); 300 | 301 | add_sbr( 302 | Perks::RallyBarricade, 303 | Box::new(|_input: ModifierResponseInput| -> HashMap { 304 | let mut stats = HashMap::new(); 305 | stats.insert(StatHashes::STABILITY.into(), 30); 306 | stats.insert(StatHashes::RELOAD.into(), 100); 307 | stats 308 | }), 309 | ); 310 | 311 | add_flmr( 312 | Perks::RallyBarricade, 313 | Box::new(|_input: ModifierResponseInput| -> FlinchModifierResponse { 314 | FlinchModifierResponse { flinch_scale: 0.5 } 315 | }), 316 | ); 317 | 318 | add_rsmr( 319 | Perks::RallyBarricade, 320 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 321 | ReloadModifierResponse { 322 | reload_stat_add: 100, 323 | reload_time_scale: 0.9, 324 | } 325 | }), 326 | ); 327 | 328 | add_rmr( 329 | Perks::RallyBarricade, 330 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 331 | RangeModifierResponse { 332 | range_all_scale: 1.1, 333 | ..Default::default() 334 | } 335 | }), 336 | ); 337 | 338 | add_dmr( 339 | Perks::ChargetimeMW, 340 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 341 | fn down5(x: i32) -> f64 { 342 | (x as f64 - 5.0) / x as f64 343 | } 344 | let damage_mod = match _input.calc_data.intrinsic_hash { 345 | 901 => down5(330), //high impact 346 | 906 => down5(280), 347 | 903 => down5(270), 348 | 902 => down5(245), //rapid fire 349 | _ => 1.0, 350 | }; 351 | DamageModifierResponse { 352 | explosive_dmg_scale: damage_mod, 353 | impact_dmg_scale: damage_mod, 354 | ..Default::default() 355 | } 356 | }), 357 | ); 358 | 359 | add_dmr( 360 | Perks::SurgeMod, 361 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 362 | let damage_mod; 363 | if _input.pvp { 364 | if _input.value == 1 { 365 | damage_mod = 1.03; 366 | } else if _input.value == 2 { 367 | damage_mod = 1.045; 368 | } else if _input.value > 2 { 369 | damage_mod = 1.055; 370 | } else { 371 | damage_mod = 1.0; 372 | } 373 | } else { 374 | if _input.value == 1 { 375 | damage_mod = 1.10; 376 | } else if _input.value == 2 { 377 | damage_mod = 1.17; 378 | } else if _input.value > 2 { 379 | damage_mod = 1.22; 380 | } else { 381 | damage_mod = 1.0; 382 | } 383 | } 384 | DamageModifierResponse { 385 | explosive_dmg_scale: damage_mod, 386 | impact_dmg_scale: damage_mod, 387 | ..Default::default() 388 | } 389 | }), 390 | ); 391 | } 392 | -------------------------------------------------------------------------------- /src/perks/origin_perks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::d2_enums::{StatHashes, WeaponType}; 4 | 5 | use super::{ 6 | add_dmr, add_epr, add_flmr, add_fmr, add_hmr, add_mmr, add_rmr, add_rr, add_rsmr, add_sbr, 7 | add_vmr, clamp, 8 | lib::{ 9 | CalculationInput, DamageModifierResponse, ExtraDamageResponse, FiringModifierResponse, 10 | FlinchModifierResponse, HandlingModifierResponse, MagazineModifierResponse, 11 | RangeModifierResponse, RefundResponse, ReloadModifierResponse, ReloadOverrideResponse, 12 | }, 13 | ModifierResponseInput, Perks, 14 | }; 15 | 16 | pub fn origin_perks() { 17 | add_rr( 18 | Perks::VeistStinger, 19 | Box::new(|_input: ModifierResponseInput| -> RefundResponse { 20 | if !(_input.value > 0) { 21 | return RefundResponse::default(); 22 | }; 23 | let data = _input.cached_data.get("veist_stinger"); 24 | let last_proc; 25 | if data.is_none() { 26 | last_proc = 0.0; 27 | } else { 28 | last_proc = *data.unwrap(); 29 | }; 30 | let time_since_last_proc = _input.calc_data.time_total - last_proc; 31 | if time_since_last_proc >= 4.0 && _input.value > 0 { 32 | let max_refund = _input.calc_data.base_mag - _input.calc_data.curr_mag; 33 | let refund_amount = (_input.calc_data.base_mag / 4.0).ceil() as i32; 34 | if max_refund > 0.0 { 35 | _input 36 | .cached_data 37 | .insert("veist_stinger".to_string(), _input.calc_data.time_total); 38 | let final_refund_ammount = clamp(refund_amount, 0, max_refund as i32); 39 | return RefundResponse { 40 | requirement: 1, 41 | crit: false, 42 | refund_mag: refund_amount, 43 | refund_reserves: -final_refund_ammount, 44 | }; 45 | } else { 46 | RefundResponse::default() 47 | } 48 | } else { 49 | RefundResponse::default() 50 | } 51 | }), 52 | ); 53 | 54 | add_fmr( 55 | Perks::VeistStinger, 56 | Box::new(|_input: ModifierResponseInput| -> FiringModifierResponse { 57 | FiringModifierResponse { 58 | burst_delay_scale: if _input.calc_data.weapon_type == &WeaponType::BOW 59 | && _input.value > 0 60 | { 61 | 0.85 62 | } else { 63 | 1.0 64 | }, 65 | ..Default::default() 66 | } 67 | }), 68 | ); 69 | 70 | add_dmr( 71 | Perks::HakkeBreach, 72 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 73 | let damage_mult = if _input.value > 0 { 0.3 } else { 0.0 }; 74 | DamageModifierResponse { 75 | impact_dmg_scale: 1.0 + damage_mult, 76 | explosive_dmg_scale: 1.0 + damage_mult, 77 | crit_scale: 1.0, 78 | } 79 | }), 80 | ); 81 | 82 | add_rmr( 83 | Perks::Alacrity, 84 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 85 | let range_add = if _input.value > 0 { 20 } else { 0 }; 86 | RangeModifierResponse { 87 | range_stat_add: range_add, 88 | ..Default::default() 89 | } 90 | }), 91 | ); 92 | 93 | add_rsmr( 94 | Perks::Alacrity, 95 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 96 | let reload_add = if _input.value > 0 { 50 } else { 0 }; 97 | ReloadModifierResponse { 98 | reload_stat_add: reload_add, 99 | ..Default::default() 100 | } 101 | }), 102 | ); 103 | 104 | add_sbr( 105 | Perks::Alacrity, 106 | Box::new(|_input: ModifierResponseInput| -> HashMap { 107 | let mut map = HashMap::new(); 108 | let range = if _input.value > 0 { 20 } else { 0 }; 109 | let reload = if _input.value > 0 { 50 } else { 0 }; 110 | let stability = if _input.value > 0 { 20 } else { 0 }; 111 | let aim_assist = if _input.value > 0 { 10 } else { 0 }; 112 | map.insert(StatHashes::RANGE.into(), range); 113 | map.insert(StatHashes::RELOAD.into(), reload); 114 | map.insert(StatHashes::STABILITY.into(), stability); 115 | map.insert(StatHashes::AIM_ASSIST.into(), aim_assist); 116 | map 117 | }), 118 | ); 119 | 120 | add_sbr( 121 | Perks::Ambush, 122 | Box::new(|_input: ModifierResponseInput| -> HashMap { 123 | let mut map = HashMap::new(); 124 | let range = if _input.is_enhanced { 30 } else { 20 }; 125 | let handling = if _input.is_enhanced { 40 } else { 20 }; 126 | if _input.calc_data.time_total < 2.0 && _input.value > 0 { 127 | map.insert(StatHashes::RANGE.into(), range); 128 | map.insert(StatHashes::HANDLING.into(), handling); 129 | } 130 | map 131 | }), 132 | ); 133 | 134 | add_rmr( 135 | Perks::Ambush, 136 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 137 | let range_add = if _input.is_enhanced { 30 } else { 20 }; 138 | if _input.calc_data.time_total < 2.0 && _input.value > 0 { 139 | RangeModifierResponse { 140 | range_stat_add: range_add, 141 | ..Default::default() 142 | } 143 | } else { 144 | RangeModifierResponse::default() 145 | } 146 | }), 147 | ); 148 | 149 | add_hmr( 150 | Perks::Ambush, 151 | Box::new(|_input: ModifierResponseInput| -> HandlingModifierResponse { 152 | let handling_add = if _input.is_enhanced { 40 } else { 20 }; 153 | if _input.calc_data.time_total < 2.0 && _input.value > 0 { 154 | HandlingModifierResponse { 155 | stat_add: handling_add, 156 | ..Default::default() 157 | } 158 | } else { 159 | HandlingModifierResponse::default() 160 | } 161 | }), 162 | ); 163 | 164 | add_dmr( 165 | Perks::Ambush, 166 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 167 | if _input.value == 0 || _input.pvp { 168 | return DamageModifierResponse::default(); 169 | } 170 | let damage_mult = if _input.calc_data.weapon_type == &WeaponType::LINEARFUSIONRIFLE { 171 | 1.0888 172 | } else { 173 | 1.1078 174 | }; 175 | 176 | DamageModifierResponse { 177 | impact_dmg_scale: damage_mult, 178 | explosive_dmg_scale: damage_mult, 179 | crit_scale: 1.0, 180 | } 181 | }), 182 | ); 183 | 184 | add_fmr( 185 | Perks::Ambush, 186 | Box::new(|_input: ModifierResponseInput| -> FiringModifierResponse { 187 | FiringModifierResponse { 188 | burst_delay_scale: if _input.calc_data.weapon_type == &WeaponType::BOW 189 | && _input.value > 0 190 | { 191 | 0.9 192 | } else { 193 | 1.0 194 | }, 195 | ..Default::default() 196 | } 197 | }), 198 | ); 199 | 200 | add_hmr( 201 | Perks::HotSwap, 202 | Box::new( 203 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 204 | let handling_add = if _input.is_enhanced { 60 } else { 30 }; 205 | if _input.value > 0 { 206 | HandlingModifierResponse { 207 | stat_add: handling_add, 208 | ..Default::default() 209 | } 210 | } else { 211 | HandlingModifierResponse::default() 212 | } 213 | }, 214 | ), 215 | ); 216 | 217 | add_rsmr( 218 | Perks::FluidDynamics, 219 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 220 | let reload_add = if _input.is_enhanced { 35 } else { 30 }; 221 | if _input.calc_data.shots_fired_this_mag <= _input.calc_data.base_mag / 2.0 { 222 | ReloadModifierResponse { 223 | reload_stat_add: reload_add, 224 | reload_time_scale: 1.0, 225 | } 226 | } else { 227 | ReloadModifierResponse::default() 228 | } 229 | }), 230 | ); 231 | 232 | add_sbr( 233 | Perks::FluidDynamics, 234 | Box::new(|_input: ModifierResponseInput| -> HashMap { 235 | let mut map = HashMap::new(); 236 | let reload = if _input.is_enhanced { 35 } else { 30 }; 237 | let stability = if _input.is_enhanced { 25 } else { 20 }; 238 | if _input.calc_data.shots_fired_this_mag <= _input.calc_data.base_mag / 2.0 239 | && _input.value > 0 240 | { 241 | map.insert(StatHashes::RELOAD.into(), reload); 242 | map.insert(StatHashes::STABILITY.into(), stability); 243 | } 244 | map 245 | }), 246 | ); 247 | 248 | add_rsmr( 249 | Perks::QuietMoment, 250 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 251 | if _input.value > 0 { 252 | ReloadModifierResponse { 253 | reload_stat_add: 40, 254 | reload_time_scale: 0.95, 255 | } 256 | } else { 257 | ReloadModifierResponse::default() 258 | } 259 | }), 260 | ); 261 | 262 | add_sbr( 263 | Perks::QuietMoment, 264 | Box::new(|_input: ModifierResponseInput| -> HashMap { 265 | let mut map = HashMap::new(); 266 | if _input.value > 0 { 267 | map.insert(StatHashes::RELOAD.into(), 40); 268 | } 269 | map 270 | }), 271 | ); 272 | 273 | add_rsmr( 274 | Perks::BitterSpite, 275 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 276 | let val = clamp(_input.value, 0, 5) as i32; 277 | let mult = match val { 278 | 0 => 1.0, 279 | 1 => 0.97, 280 | 2 => 0.96, 281 | 3 => 0.95, 282 | 4 => 0.92, 283 | 5 => 0.90, 284 | _ => 0.90, 285 | }; 286 | ReloadModifierResponse { 287 | reload_stat_add: val * 10, 288 | reload_time_scale: mult, 289 | } 290 | }), 291 | ); 292 | 293 | add_sbr( 294 | Perks::BitterSpite, 295 | Box::new(|_input: ModifierResponseInput| -> HashMap { 296 | let mut map = HashMap::new(); 297 | let val = clamp(_input.value, 0, 5) as i32; 298 | map.insert(StatHashes::RELOAD.into(), val * 10); 299 | map 300 | }), 301 | ); 302 | 303 | add_rmr( 304 | Perks::RightHook, 305 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 306 | let range_add = if _input.is_enhanced { 20 } else { 10 }; 307 | if _input.value > 0 { 308 | RangeModifierResponse { 309 | range_stat_add: range_add, 310 | ..Default::default() 311 | } 312 | } else { 313 | RangeModifierResponse::default() 314 | } 315 | }), 316 | ); 317 | 318 | add_sbr( 319 | Perks::RightHook, 320 | Box::new(|_input: ModifierResponseInput| -> HashMap { 321 | let mut map = HashMap::new(); 322 | let stat_bump = if _input.is_enhanced { 20 } else { 10 }; 323 | if _input.value > 0 { 324 | map.insert(StatHashes::AIM_ASSIST.into(), stat_bump); 325 | map.insert(StatHashes::RANGE.into(), stat_bump); 326 | } 327 | map 328 | }), 329 | ); 330 | 331 | add_hmr( 332 | Perks::SearchParty, 333 | Box::new( 334 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 335 | HandlingModifierResponse { 336 | ads_scale: 0.85, 337 | ..Default::default() 338 | } 339 | }, 340 | ), 341 | ); 342 | 343 | add_mmr( 344 | Perks::RunnethOver, 345 | Box::new( 346 | |_input: ModifierResponseInput| -> MagazineModifierResponse { 347 | let val = clamp(_input.value, 0, 5) as f64; 348 | MagazineModifierResponse { 349 | magazine_scale: 1.0 + val * 0.1, 350 | ..Default::default() 351 | } 352 | }, 353 | ), 354 | ); 355 | 356 | add_sbr( 357 | Perks::TexBalancedStock, 358 | Box::new(|_input: ModifierResponseInput| -> HashMap { 359 | let mut map = HashMap::new(); 360 | if _input.value > 0 { 361 | map.insert(StatHashes::HANDLING.into(), 20); 362 | map.insert(StatHashes::RELOAD.into(), 20); 363 | } 364 | map 365 | }), 366 | ); 367 | 368 | add_hmr( 369 | Perks::TexBalancedStock, 370 | Box::new( 371 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 372 | if _input.value > 0 { 373 | HandlingModifierResponse { 374 | stat_add: 50, 375 | ..Default::default() 376 | } 377 | } else { 378 | HandlingModifierResponse::default() 379 | } 380 | }, 381 | ), 382 | ); 383 | 384 | add_rsmr( 385 | Perks::TexBalancedStock, 386 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 387 | if _input.value > 0 { 388 | ReloadModifierResponse { 389 | reload_stat_add: 20, 390 | reload_time_scale: 0.9, 391 | ..Default::default() 392 | } 393 | } else { 394 | ReloadModifierResponse::default() 395 | } 396 | }), 397 | ); 398 | 399 | add_sbr( 400 | Perks::SurosSynergy, 401 | Box::new(|_input: ModifierResponseInput| -> HashMap { 402 | let mut out = HashMap::new(); 403 | if _input.value > 0 { 404 | out.insert(StatHashes::HANDLING.into(), 40); 405 | } 406 | out 407 | }), 408 | ); 409 | 410 | add_hmr( 411 | Perks::SurosSynergy, 412 | Box::new( 413 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 414 | if _input.value > 0 { 415 | HandlingModifierResponse { 416 | stat_add: 40, 417 | ..Default::default() 418 | } 419 | } else { 420 | HandlingModifierResponse::default() 421 | } 422 | }, 423 | ), 424 | ); 425 | 426 | add_flmr( 427 | Perks::SurosSynergy, 428 | Box::new(|_input: ModifierResponseInput| -> FlinchModifierResponse { 429 | if _input.value > 0 { 430 | FlinchModifierResponse { flinch_scale: 0.80 } 431 | } else { 432 | FlinchModifierResponse::default() 433 | } 434 | }), 435 | ); 436 | 437 | add_sbr( 438 | Perks::HarmonicResonance, 439 | Box::new(|_input: ModifierResponseInput| -> HashMap { 440 | let mut out = HashMap::new(); 441 | if _input.value == 1 { 442 | out.insert(StatHashes::HANDLING.into(), 10); 443 | } 444 | if _input.value > 1 { 445 | out.insert(StatHashes::RELOAD.into(), 20); 446 | out.insert(StatHashes::HANDLING.into(), 20); 447 | } 448 | out 449 | }), 450 | ); 451 | 452 | add_rsmr( 453 | Perks::HarmonicResonance, 454 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 455 | let stat_bump = if _input.value > 1 { 20 } else { 0 }; 456 | if _input.value > 0 { 457 | ReloadModifierResponse { 458 | reload_stat_add: stat_bump, 459 | reload_time_scale: 0.95, 460 | ..Default::default() 461 | } 462 | } else { 463 | ReloadModifierResponse::default() 464 | } 465 | }), 466 | ); 467 | 468 | add_hmr( 469 | Perks::HarmonicResonance, 470 | Box::new( 471 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 472 | let stat_bump = 10 * clamp(_input.value, 0, 2); 473 | HandlingModifierResponse { 474 | stat_add: stat_bump as i32, 475 | ..Default::default() 476 | } 477 | }, 478 | ), 479 | ); 480 | } 481 | -------------------------------------------------------------------------------- /src/perks/year_2_perks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::d2_enums::{AmmoType, StatHashes, WeaponType}; 4 | 5 | use super::{ 6 | add_dmr, add_epr, add_flmr, add_fmr, add_hmr, add_mmr, add_rmr, add_rr, add_rsmr, add_sbr, 7 | add_vmr, clamp, 8 | lib::{ 9 | CalculationInput, DamageModifierResponse, ExplosivePercentResponse, ExtraDamageResponse, 10 | FiringModifierResponse, FlinchModifierResponse, HandlingModifierResponse, 11 | MagazineModifierResponse, RangeModifierResponse, RefundResponse, ReloadModifierResponse, 12 | ReloadOverrideResponse, 13 | }, 14 | ModifierResponseInput, Perks, 15 | }; 16 | 17 | pub fn year_2_perks() { 18 | add_sbr( 19 | Perks::AirAssault, 20 | Box::new(|_input: ModifierResponseInput| -> HashMap { 21 | let mut stats = HashMap::new(); 22 | let ae_per_stack = if _input.is_enhanced { 35 } else { 30 }; 23 | let ae = ae_per_stack * _input.value as i32; 24 | stats.insert(StatHashes::AIRBORNE.into(), ae); 25 | stats 26 | }), 27 | ); 28 | 29 | add_fmr( 30 | Perks::ArchersTempo, 31 | Box::new(|_input: ModifierResponseInput| -> FiringModifierResponse { 32 | FiringModifierResponse { 33 | burst_delay_scale: if _input.value > 0 { 0.75 } else { 1.0 }, 34 | burst_delay_add: 0.0, 35 | inner_burst_scale: 1.0, 36 | burst_size_add: 0.0, 37 | } 38 | }), 39 | ); 40 | 41 | add_dmr( 42 | Perks::ExplosiveHead, 43 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 44 | if _input.pvp { 45 | DamageModifierResponse::default() 46 | } else { 47 | DamageModifierResponse { 48 | impact_dmg_scale: 1.0, 49 | explosive_dmg_scale: 1.3, 50 | crit_scale: 1.0, 51 | } 52 | } 53 | }), 54 | ); 55 | 56 | add_epr( 57 | Perks::ExplosiveHead, 58 | Box::new( 59 | |_input: ModifierResponseInput| -> ExplosivePercentResponse { 60 | ExplosivePercentResponse { 61 | percent: 0.5, 62 | delyed: if _input.pvp { 0.0 } else { 0.2 }, 63 | retain_base_total: true, 64 | } 65 | }, 66 | ), 67 | ); 68 | 69 | add_rsmr( 70 | Perks::FeedingFrenzy, 71 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 72 | let val = clamp(_input.value, 0, 5); 73 | let duration = 3.5; 74 | let mut reload_mult = 1.0; 75 | let mut reload = 0; 76 | if val == 1 { 77 | reload = 8; 78 | reload_mult = 0.975; 79 | } else if val == 2 { 80 | reload = 50; 81 | reload_mult = 0.9; 82 | } else if val == 3 { 83 | reload = 60; 84 | reload_mult = 0.868; 85 | } else if val == 4 { 86 | reload = 75; 87 | reload_mult = 0.837; 88 | } else if val == 5 { 89 | reload = 100; 90 | reload_mult = 0.8; 91 | }; 92 | if _input.calc_data.time_total > duration { 93 | reload = 0; 94 | reload_mult = 1.0; 95 | }; 96 | ReloadModifierResponse { 97 | reload_stat_add: reload, 98 | reload_time_scale: reload_mult, 99 | } 100 | }), 101 | ); 102 | 103 | add_sbr( 104 | Perks::FeedingFrenzy, 105 | Box::new(|_input: ModifierResponseInput| -> HashMap { 106 | let mut stats = HashMap::new(); 107 | let val = clamp(_input.value, 0, 5); 108 | let duration = 3.5; 109 | let mut reload = 0; 110 | if val == 1 { 111 | reload = 8; 112 | } else if val == 2 { 113 | reload = 50; 114 | } else if val == 3 { 115 | reload = 60; 116 | } else if val == 4 { 117 | reload = 75; 118 | } else if val == 5 { 119 | reload = 100; 120 | }; 121 | if _input.calc_data.time_total > duration { 122 | reload = 0; 123 | }; 124 | stats.insert(StatHashes::RELOAD.into(), reload); 125 | stats 126 | }), 127 | ); 128 | 129 | add_dmr( 130 | Perks::FiringLine, 131 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 132 | let mut crit_mult = 1.0; 133 | if _input.value > 0 { 134 | crit_mult = 1.2; 135 | } 136 | DamageModifierResponse { 137 | crit_scale: crit_mult, 138 | explosive_dmg_scale: 1.0, 139 | impact_dmg_scale: 1.0, 140 | } 141 | }), 142 | ); 143 | 144 | add_rr( 145 | Perks::FourthTimesTheCharm, 146 | Box::new(|_input: ModifierResponseInput| -> RefundResponse { 147 | RefundResponse { 148 | crit: true, 149 | requirement: 4, 150 | refund_mag: 2, 151 | refund_reserves: 0, 152 | } 153 | }), 154 | ); 155 | 156 | add_dmr( 157 | Perks::KillingTally, 158 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 159 | let val = clamp(_input.value, 0, 3); 160 | let mut damage_mult = 0.1 * val as f64; 161 | if _input.pvp { 162 | damage_mult *= 0.5; 163 | }; 164 | if _input.calc_data.num_reloads > 0.0 { 165 | damage_mult = 0.0; 166 | }; 167 | DamageModifierResponse { 168 | impact_dmg_scale: 1.0 + damage_mult, 169 | explosive_dmg_scale: 1.0 + damage_mult, 170 | crit_scale: 1.0, 171 | } 172 | }), 173 | ); 174 | 175 | add_mmr( 176 | Perks::OverFlow, 177 | Box::new( 178 | |_input: ModifierResponseInput| -> MagazineModifierResponse { 179 | let mut mag_scale = if _input.value > 0 { 2.0 } else { 1.0 }; 180 | if _input.is_enhanced && _input.value > 0 { 181 | mag_scale *= 1.1; 182 | }; 183 | if _input.calc_data.total_shots_fired > 0.0 { 184 | mag_scale = 1.0; 185 | }; 186 | MagazineModifierResponse { 187 | magazine_stat_add: 0, 188 | magazine_scale: mag_scale, 189 | magazine_add: 0.0, 190 | } 191 | }, 192 | ), 193 | ); 194 | 195 | add_rsmr( 196 | Perks::RapidHit, 197 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 198 | let values = vec![ 199 | (0, 1.0), 200 | (5, 0.99), 201 | (30, 0.97), 202 | (35, 0.96), 203 | (45, 0.94), 204 | (60, 0.93), 205 | ]; 206 | let entry_to_get = clamp( 207 | _input.value + _input.calc_data.shots_fired_this_mag as u32, 208 | 0, 209 | 5, 210 | ); 211 | ReloadModifierResponse { 212 | reload_stat_add: values[entry_to_get as usize].0, 213 | reload_time_scale: values[entry_to_get as usize].1, 214 | } 215 | }), 216 | ); 217 | 218 | add_sbr( 219 | Perks::RapidHit, 220 | Box::new(|_input: ModifierResponseInput| -> HashMap { 221 | let rel_values = vec![0, 5, 30, 35, 45, 60]; 222 | let stab_values = vec![0, 2, 12, 14, 18, 25]; 223 | let entry_to_get = clamp( 224 | _input.value + _input.calc_data.shots_fired_this_mag as u32, 225 | 0, 226 | 5, 227 | ); 228 | let mut stats = HashMap::new(); 229 | stats.insert(StatHashes::RELOAD.into(), rel_values[entry_to_get as usize]); 230 | stats.insert( 231 | StatHashes::STABILITY.into(), 232 | stab_values[entry_to_get as usize], 233 | ); 234 | stats 235 | }), 236 | ); 237 | 238 | add_dmr( 239 | Perks::ResevoirBurst, 240 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 241 | let mut damage_mult = 1.0; 242 | if _input.calc_data.curr_mag >= _input.calc_data.base_mag { 243 | damage_mult = 1.25; 244 | }; 245 | DamageModifierResponse { 246 | impact_dmg_scale: damage_mult, 247 | explosive_dmg_scale: damage_mult, 248 | crit_scale: 1.0, 249 | } 250 | }), 251 | ); 252 | 253 | add_dmr( 254 | Perks::Surrounded, 255 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 256 | let mut damage_mult = 1.0; 257 | if _input.value > 0 { 258 | damage_mult = if *_input.calc_data.weapon_type == WeaponType::SWORD { 259 | 1.35 260 | } else { 261 | 1.4 262 | }; 263 | if _input.is_enhanced { 264 | damage_mult *= 1.05; 265 | }; 266 | }; 267 | DamageModifierResponse { 268 | impact_dmg_scale: damage_mult, 269 | explosive_dmg_scale: damage_mult, 270 | crit_scale: 1.0, 271 | } 272 | }), 273 | ); 274 | 275 | add_dmr( 276 | Perks::FullCourt, 277 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 278 | let mut damage_mult = 1.0; 279 | if _input.value > 0 { 280 | damage_mult = 1.25; 281 | }; 282 | DamageModifierResponse { 283 | impact_dmg_scale: 1.0, 284 | explosive_dmg_scale: damage_mult, 285 | crit_scale: 1.0, 286 | } 287 | }), 288 | ); 289 | 290 | add_dmr( 291 | Perks::Swashbuckler, 292 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 293 | let val = clamp(_input.value, 0, 5); 294 | let duration = if _input.is_enhanced { 6.0 } else { 4.5 }; 295 | let mut dmg_boost = 0.067 * val as f64; 296 | if _input.calc_data.time_total > duration { 297 | dmg_boost = 0.0; 298 | }; 299 | DamageModifierResponse { 300 | impact_dmg_scale: 1.0 + dmg_boost, 301 | explosive_dmg_scale: 1.0 + dmg_boost, 302 | crit_scale: 1.0, 303 | } 304 | }), 305 | ); 306 | 307 | add_dmr( 308 | Perks::MultikillClip, 309 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 310 | let val = clamp(_input.value, 0, 5); 311 | let mut damage_mult = (1.0 / 6.0) * val as f64; 312 | if _input.calc_data.num_reloads > 0.0 { 313 | damage_mult = 0.0; 314 | }; 315 | DamageModifierResponse { 316 | impact_dmg_scale: 1.0 + damage_mult, 317 | explosive_dmg_scale: 1.0 + damage_mult, 318 | crit_scale: 1.0, 319 | } 320 | }), 321 | ); 322 | 323 | add_dmr( 324 | Perks::ExplosiveLight, 325 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 326 | let shots = if _input.is_enhanced { 7.0 } else { 6.0 }; 327 | let shots_left = _input.value as f64 * shots - _input.calc_data.total_shots_fired; 328 | if shots_left <= 0.0 { 329 | return DamageModifierResponse::default(); 330 | }; 331 | if _input.calc_data.weapon_type == &WeaponType::GRENADELAUNCHER { 332 | let blast_radius_struct = 333 | _input.calc_data.stats.get(&StatHashes::BLAST_RADIUS.into()); 334 | let blast_radius; 335 | if blast_radius_struct.is_none() { 336 | blast_radius = 0; 337 | } else { 338 | blast_radius = blast_radius_struct.unwrap().val(); 339 | }; 340 | if _input.calc_data.ammo_type == &AmmoType::HEAVY { 341 | let expl_percent = 0.7 + 0.00175 * blast_radius as f64; 342 | let impt_percent = 1.0 - expl_percent; 343 | let expl_mult = 0.875 / expl_percent * 1.6; 344 | let impt_mult = 0.125 / impt_percent; 345 | return DamageModifierResponse { 346 | impact_dmg_scale: impt_mult, 347 | explosive_dmg_scale: expl_mult, 348 | crit_scale: 1.0, 349 | }; 350 | } 351 | if _input.calc_data.ammo_type == &AmmoType::SPECIAL { 352 | let expl_percent = 0.5 + 0.0025 * blast_radius as f64; 353 | let impt_percent = 1.0 - expl_percent; 354 | let expl_mult = 0.75 / expl_percent * 1.6; 355 | let impt_mult = 0.25 / impt_percent; 356 | return DamageModifierResponse { 357 | impact_dmg_scale: impt_mult, 358 | explosive_dmg_scale: expl_mult, 359 | crit_scale: 1.0, 360 | }; 361 | } 362 | }; 363 | DamageModifierResponse { 364 | explosive_dmg_scale: 1.25, 365 | impact_dmg_scale: 1.25, 366 | crit_scale: 1.0, 367 | } 368 | }), 369 | ); 370 | 371 | add_sbr( 372 | Perks::ExplosiveLight, 373 | Box::new(|_input: ModifierResponseInput| -> HashMap { 374 | let mut out = HashMap::new(); 375 | if _input.value > 0 { 376 | out.insert(StatHashes::BLAST_RADIUS.into(), 100); 377 | }; 378 | out 379 | }), 380 | ); 381 | 382 | add_sbr( 383 | Perks::EyeOfTheStorm, 384 | Box::new(|_input: ModifierResponseInput| -> HashMap { 385 | let mut out = HashMap::new(); 386 | if _input.value > 0 { 387 | out.insert(StatHashes::HANDLING.into(), 30); 388 | }; 389 | out 390 | }), 391 | ); 392 | 393 | add_hmr( 394 | Perks::EyeOfTheStorm, 395 | Box::new( 396 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 397 | if _input.value > 0 { 398 | HandlingModifierResponse { 399 | stat_add: 30, 400 | ..Default::default() 401 | } 402 | } else { 403 | HandlingModifierResponse::default() 404 | } 405 | }, 406 | ), 407 | ); 408 | 409 | add_flmr( 410 | Perks::NoDistractions, 411 | Box::new(|_input: ModifierResponseInput| -> FlinchModifierResponse { 412 | if _input.value > 0 { 413 | FlinchModifierResponse { flinch_scale: 0.65 } 414 | } else { 415 | FlinchModifierResponse::default() 416 | } 417 | }), 418 | ); 419 | } 420 | -------------------------------------------------------------------------------- /src/perks/year_3_perks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | d2_enums::{AmmoType, StatHashes}, 5 | enemies::EnemyType, 6 | }; 7 | 8 | use super::{ 9 | add_dmr, add_epr, add_fmr, add_hmr, add_mmr, add_rmr, add_rsmr, add_sbr, add_vmr, 10 | lib::{ 11 | CalculationInput, DamageModifierResponse, ExtraDamageResponse, FiringModifierResponse, 12 | HandlingModifierResponse, MagazineModifierResponse, RangeModifierResponse, RefundResponse, 13 | ReloadModifierResponse, 14 | }, 15 | ModifierResponseInput, Perks, 16 | }; 17 | 18 | pub fn year_3_perks() { 19 | add_mmr( 20 | Perks::ClownCartridge, 21 | Box::new( 22 | |_input: ModifierResponseInput| -> MagazineModifierResponse { 23 | MagazineModifierResponse { 24 | magazine_add: 0.0, 25 | magazine_scale: 1.5, 26 | magazine_stat_add: 0, 27 | } 28 | }, 29 | ), 30 | ); 31 | 32 | add_sbr( 33 | Perks::ElementalCapacitor, 34 | Box::new(|_input: ModifierResponseInput| -> HashMap { 35 | let mut stats = HashMap::new(); 36 | let ev = if _input.is_enhanced { 5 } else { 0 }; 37 | if _input.value == 1 { 38 | stats.insert(StatHashes::STABILITY.into(), 20 + ev); 39 | } else if _input.value == 2 { 40 | stats.insert(StatHashes::RELOAD.into(), 50 + ev); 41 | } else if _input.value == 3 { 42 | stats.insert(StatHashes::HANDLING.into(), 50 + ev); 43 | } else if _input.value == 4 { 44 | stats.insert(StatHashes::RECOIL_DIR.into(), 20 + ev); 45 | } else if _input.value == 5 { 46 | stats.insert(StatHashes::AIRBORNE.into(), 20 + ev); 47 | }; 48 | stats 49 | }), 50 | ); 51 | 52 | add_hmr( 53 | Perks::ElementalCapacitor, 54 | Box::new( 55 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 56 | let mut handling = 0; 57 | if _input.value == 3 { 58 | handling = if _input.is_enhanced { 55 } else { 50 }; 59 | }; 60 | HandlingModifierResponse { 61 | stat_add: handling, 62 | ..Default::default() 63 | } 64 | }, 65 | ), 66 | ); 67 | 68 | add_rsmr( 69 | Perks::ElementalCapacitor, 70 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 71 | let mut reload = 0; 72 | if _input.value == 2 { 73 | reload = if _input.is_enhanced { 55 } else { 50 }; 74 | }; 75 | ReloadModifierResponse { 76 | reload_stat_add: reload, 77 | ..Default::default() 78 | } 79 | }), 80 | ); 81 | 82 | add_sbr( 83 | Perks::KillingWind, 84 | Box::new(|_input: ModifierResponseInput| -> HashMap { 85 | let mut stats = HashMap::new(); 86 | if _input.value > 0 { 87 | stats.insert(StatHashes::HANDLING.into(), 40); 88 | stats.insert(StatHashes::RANGE.into(), 20); 89 | }; 90 | stats 91 | }), 92 | ); 93 | 94 | add_rmr( 95 | Perks::KillingWind, 96 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 97 | if _input.value > 0 { 98 | RangeModifierResponse { 99 | range_stat_add: 20, 100 | range_all_scale: 1.05, 101 | range_zoom_scale: 1.0, 102 | range_hip_scale: 1.0, 103 | } 104 | } else { 105 | RangeModifierResponse { 106 | range_stat_add: 0, 107 | range_all_scale: 1.0, 108 | range_zoom_scale: 1.0, 109 | range_hip_scale: 1.0, 110 | } 111 | } 112 | }), 113 | ); 114 | 115 | add_hmr( 116 | Perks::KillingWind, 117 | Box::new( 118 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 119 | if _input.value > 0 { 120 | HandlingModifierResponse { 121 | stat_add: 40, 122 | ..Default::default() 123 | } 124 | } else { 125 | HandlingModifierResponse::default() 126 | } 127 | }, 128 | ), 129 | ); 130 | 131 | add_dmr( 132 | Perks::LastingImpression, 133 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 134 | DamageModifierResponse { 135 | impact_dmg_scale: 1.0, 136 | explosive_dmg_scale: 1.25, 137 | crit_scale: 1.0, 138 | } 139 | }), 140 | ); 141 | 142 | add_dmr( 143 | Perks::Vorpal, 144 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 145 | let mut buff = 1.0; 146 | if *_input.calc_data.enemy_type == EnemyType::BOSS 147 | || *_input.calc_data.enemy_type == EnemyType::MINIBOSS 148 | || *_input.calc_data.enemy_type == EnemyType::CHAMPION 149 | || *_input.calc_data.enemy_type == EnemyType::VEHICLE 150 | { 151 | if *_input.calc_data.ammo_type == AmmoType::PRIMARY { 152 | buff = 1.2; 153 | } else if *_input.calc_data.ammo_type == AmmoType::SPECIAL { 154 | buff = 1.15; 155 | } else if *_input.calc_data.ammo_type == AmmoType::HEAVY { 156 | buff = 1.1; 157 | } 158 | } 159 | DamageModifierResponse { 160 | impact_dmg_scale: buff, 161 | explosive_dmg_scale: buff, 162 | crit_scale: 1.0, 163 | } 164 | }), 165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /src/perks/year_6_perks.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::d2_enums::{AmmoType, BungieHash, DamageType, StatBump, StatHashes, WeaponType}; 4 | 5 | use super::{ 6 | add_dmr, add_epr, add_fmr, add_hmr, add_mmr, add_rmr, add_rsmr, add_sbr, add_vmr, clamp, 7 | lib::{ 8 | CalculationInput, DamageModifierResponse, ExplosivePercentResponse, ExtraDamageResponse, 9 | FiringModifierResponse, HandlingModifierResponse, InventoryModifierResponse, 10 | MagazineModifierResponse, RangeModifierResponse, RefundResponse, ReloadModifierResponse, 11 | VelocityModifierResponse, 12 | }, 13 | ModifierResponseInput, Perks, add_imr, 14 | }; 15 | 16 | pub fn year_6_perks() { 17 | add_sbr( 18 | Perks::KeepAway, 19 | Box::new(|_input: ModifierResponseInput| -> HashMap { 20 | let mut map = HashMap::new(); 21 | let mut range_bonus = 0; 22 | let mut reload_bonus = 0; 23 | if _input.value > 0 { 24 | range_bonus = 10; 25 | reload_bonus = 30; 26 | }; 27 | map.insert(StatHashes::RANGE.into(), range_bonus); 28 | map.insert(StatHashes::RELOAD.into(), reload_bonus); 29 | map 30 | }), 31 | ); 32 | 33 | add_rmr( 34 | Perks::KeepAway, 35 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 36 | let range_bonus = if _input.value > 0 { 10 } else { 0 }; 37 | RangeModifierResponse { 38 | range_stat_add: range_bonus, 39 | ..Default::default() 40 | } 41 | }), 42 | ); 43 | 44 | add_rsmr( 45 | Perks::KeepAway, 46 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 47 | let reload_bonus = if _input.value > 0 { 30 } else { 0 }; 48 | ReloadModifierResponse { 49 | reload_stat_add: reload_bonus, 50 | ..Default::default() 51 | } 52 | }), 53 | ); 54 | 55 | add_sbr( 56 | Perks::FieldTested, 57 | Box::new(|_input: ModifierResponseInput| -> HashMap { 58 | let mut map = HashMap::new(); 59 | if _input.value > 4 { 60 | map.insert(StatHashes::RANGE.into(), 20); 61 | map.insert(StatHashes::RELOAD.into(), 55); 62 | } else if _input.value == 4 { 63 | map.insert(StatHashes::RANGE.into(), 12); 64 | map.insert(StatHashes::RELOAD.into(), 35); 65 | } else if _input.value == 3 { 66 | map.insert(StatHashes::RANGE.into(), 9); 67 | map.insert(StatHashes::RELOAD.into(), 20); 68 | } else if _input.value == 2 { 69 | map.insert(StatHashes::RANGE.into(), 6); 70 | map.insert(StatHashes::RELOAD.into(), 10); 71 | } else if _input.value == 1 { 72 | map.insert(StatHashes::RELOAD.into(), 5); 73 | map.insert(StatHashes::RANGE.into(), 3); 74 | } 75 | map 76 | }), 77 | ); 78 | 79 | // add_hmr( 80 | // Perks::FieldTested, 81 | // Box::new( 82 | // |_input: ModifierResponseInput| -> HandlingModifierResponse { 83 | // let val = clamp(_input.value, 0, 5) as i32; 84 | // HandlingModifierResponse { 85 | // stat_add: val * 5, 86 | // ..Default::default() 87 | // } 88 | // }, 89 | // ), 90 | // ); 91 | 92 | add_rsmr( 93 | Perks::FieldTested, 94 | Box::new(|_input: ModifierResponseInput| -> ReloadModifierResponse { 95 | let reload_bump; 96 | if _input.value > 4 { 97 | reload_bump = 55; 98 | } else if _input.value == 4 { 99 | reload_bump = 35; 100 | } else if _input.value == 3 { 101 | reload_bump = 20; 102 | } else if _input.value == 2 { 103 | reload_bump = 10; 104 | } else if _input.value == 1 { 105 | reload_bump = 5; 106 | } else { 107 | reload_bump = 0; 108 | }; 109 | ReloadModifierResponse { 110 | reload_stat_add: reload_bump, 111 | ..Default::default() 112 | } 113 | }), 114 | ); 115 | 116 | add_rmr( 117 | Perks::FieldTested, 118 | Box::new(|_input: ModifierResponseInput| -> RangeModifierResponse { 119 | let range_bump; 120 | if _input.value > 4 { 121 | range_bump = 20; 122 | } else if _input.value == 4 { 123 | range_bump = 12; 124 | } else if _input.value == 3 { 125 | range_bump = 9; 126 | } else if _input.value == 2 { 127 | range_bump = 6; 128 | } else if _input.value == 1 { 129 | range_bump = 3; 130 | } else { 131 | range_bump = 0; 132 | }; 133 | RangeModifierResponse { 134 | range_stat_add: range_bump, 135 | ..Default::default() 136 | } 137 | }), 138 | ); 139 | 140 | add_dmr( 141 | Perks::ParacausalAffinity, 142 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 143 | if _input.value > 0 { 144 | DamageModifierResponse { 145 | explosive_dmg_scale: 1.2, 146 | impact_dmg_scale: 1.2, 147 | ..Default::default() 148 | } 149 | } else { 150 | DamageModifierResponse::default() 151 | } 152 | }), 153 | ); 154 | 155 | add_mmr( 156 | Perks::EnviousAssasin, 157 | Box::new( 158 | |_input: ModifierResponseInput| -> MagazineModifierResponse { 159 | let val = clamp(_input.value, 0, 15) as f64; 160 | if _input.calc_data.total_shots_fired == 0.0 { 161 | let mut mag_mult = 1.0; 162 | if *_input.calc_data.ammo_type == AmmoType::PRIMARY { 163 | mag_mult += 0.1 * val; 164 | } else { 165 | mag_mult += 0.2 * val; 166 | }; 167 | return MagazineModifierResponse { 168 | magazine_stat_add: 0, 169 | magazine_scale: clamp(mag_mult, 1.0, 2.5), 170 | magazine_add: 0.0, 171 | }; 172 | }; 173 | MagazineModifierResponse { 174 | magazine_stat_add: 0, 175 | magazine_scale: 1.0, 176 | magazine_add: 0.0, 177 | } 178 | }, 179 | ), 180 | ); 181 | 182 | add_dmr( 183 | Perks::CollectiveAction, 184 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 185 | let dmg_boost = if _input.value > 0 { 1.2 } else { 1.0 }; 186 | DamageModifierResponse { 187 | impact_dmg_scale: dmg_boost, 188 | explosive_dmg_scale: dmg_boost, 189 | crit_scale: 1.0, 190 | } 191 | }), 192 | ); 193 | 194 | add_sbr( 195 | Perks::Discord, 196 | Box::new(|_input: ModifierResponseInput| -> HashMap { 197 | let mut map = HashMap::new(); 198 | if _input.value > 0 { 199 | map.insert(StatHashes::AIRBORNE.into(), 30); 200 | } 201 | map 202 | }), 203 | ); 204 | 205 | add_hmr( 206 | Perks::Discord, 207 | Box::new( 208 | |_input: ModifierResponseInput| -> HandlingModifierResponse { 209 | let ads_mult = if _input.value > 0 { 0.75 } else { 1.0 }; 210 | HandlingModifierResponse { 211 | ads_scale: ads_mult, 212 | ..Default::default() 213 | } 214 | }, 215 | ), 216 | ); 217 | 218 | add_mmr( 219 | Perks::Bipod, 220 | Box::new( 221 | |_input: ModifierResponseInput| -> MagazineModifierResponse { 222 | MagazineModifierResponse { 223 | magazine_scale: 2.0, 224 | ..Default::default() 225 | } 226 | }, 227 | ), 228 | ); 229 | 230 | add_imr(Perks::Bipod, 231 | Box::new( 232 | |_input: ModifierResponseInput| -> InventoryModifierResponse { 233 | InventoryModifierResponse { 234 | inv_scale: 1.75, 235 | ..Default::default() 236 | } 237 | }, 238 | ), 239 | ); 240 | 241 | add_dmr( 242 | Perks::Bipod, 243 | Box::new(|_input: ModifierResponseInput| -> DamageModifierResponse { 244 | DamageModifierResponse { 245 | impact_dmg_scale: 0.6, 246 | explosive_dmg_scale: 0.6, 247 | crit_scale: 1.0, 248 | } 249 | }), 250 | ); 251 | } 252 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use num_traits::{Float, Zero}; 4 | 5 | use crate::{ 6 | d2_enums::{AmmoType, DamageType, StatHashes, WeaponType}, 7 | weapons::{Stat, Weapon}, 8 | PERS_DATA, 9 | }; 10 | 11 | const FLOAT_DELTA: f32 = 0.0001; 12 | fn cmp_floats(a: T, b: T) -> bool { 13 | let delta = T::from(FLOAT_DELTA).unwrap(); 14 | (a - b).abs() < delta 15 | } 16 | 17 | fn cmp_floats_delta(a: T, b: T, delta: T) -> bool { 18 | (a - b).abs() < delta 19 | } 20 | 21 | fn setup_pulse() { 22 | let vec = Vec::::from("bozo".to_string()); 23 | let mut hash = 0; 24 | for i in 0..vec.len() { 25 | hash += vec[i] as u32; 26 | if i < vec.len() - 1 { 27 | hash <<= 8; 28 | } 29 | } 30 | let mut new_weapon = Weapon::generate_weapon( 31 | hash, //bozo as u32 :) 32 | 13, //pulse 33 | 69420, //test pulse 34 | 1, //primary 35 | 3373582085, //kinetic 36 | ) 37 | .unwrap(); 38 | let mut stats = HashMap::new(); 39 | stats.insert(StatHashes::RELOAD.into(), Stat::from(50)); 40 | stats.insert(StatHashes::HANDLING.into(), Stat::from(50)); 41 | stats.insert(StatHashes::RANGE.into(), Stat::from(50)); 42 | stats.insert(StatHashes::ZOOM.into(), Stat::from(15)); 43 | new_weapon.set_stats(stats); 44 | PERS_DATA.with(|perm_data| { 45 | perm_data.borrow_mut().weapon = new_weapon; 46 | }); 47 | } 48 | 49 | #[test] 50 | fn test_pulse_setup() { 51 | setup_pulse(); 52 | PERS_DATA.with(|perm_data| { 53 | let mut weapon = perm_data.borrow().weapon.clone(); 54 | assert_eq!(weapon.damage_type, DamageType::KINETIC); 55 | assert_eq!(weapon.ammo_type, AmmoType::PRIMARY); 56 | assert_eq!(weapon.intrinsic_hash, 69420); 57 | assert_eq!(weapon.weapon_type, WeaponType::PULSERIFLE); 58 | let test_stat = weapon 59 | .get_stats() 60 | .get(&(StatHashes::HANDLING.into())) 61 | .unwrap() 62 | .val(); 63 | assert_eq!(test_stat, 50, "test_stat: {}", test_stat); 64 | }); 65 | } 66 | 67 | #[test] 68 | fn test_pulse_reload() { 69 | setup_pulse(); 70 | PERS_DATA.with(|perm_data| { 71 | let weapon = perm_data.borrow_mut().weapon.clone(); 72 | let response = weapon.calc_reload_time(None, None, true); 73 | assert!( 74 | cmp_floats(response.reload_time, 5.0), 75 | "reload time: {}", 76 | response.reload_time 77 | ); 78 | }); 79 | } 80 | 81 | #[test] 82 | fn test_pulse_handling() { 83 | setup_pulse(); 84 | PERS_DATA.with(|perm_data| { 85 | let weapon = perm_data.borrow_mut().weapon.clone(); 86 | let response = weapon.calc_handling_times(None, None, true); 87 | assert!( 88 | cmp_floats(response.ads_time, 5.0), 89 | "ads time: {}", 90 | response.ads_time 91 | ); 92 | assert!( 93 | cmp_floats(response.ready_time, 5.0), 94 | "ready time: {}", 95 | response.ready_time 96 | ); 97 | assert!( 98 | cmp_floats(response.stow_time, 5.0), 99 | "stow time: {}", 100 | response.stow_time 101 | ); 102 | }); 103 | } 104 | 105 | #[test] 106 | fn test_pulse_range() { 107 | setup_pulse(); 108 | PERS_DATA.with(|perm_data| { 109 | let weapon = perm_data.borrow_mut().weapon.clone(); 110 | let response = weapon.calc_range_falloff(None, None, true); 111 | assert!( 112 | cmp_floats(response.hip_falloff_start, 15.0), 113 | "hip falloff start: {}", 114 | response.hip_falloff_start 115 | ); 116 | assert!( 117 | cmp_floats(response.ads_falloff_start, 15.0 * (1.5 - 0.025)), 118 | "ads falloff start: {}", 119 | response.ads_falloff_start 120 | ); 121 | assert!( 122 | cmp_floats(response.hip_falloff_end, 30.0), 123 | "hip falloff end: {}", 124 | response.hip_falloff_end 125 | ); 126 | assert!( 127 | cmp_floats(response.ads_falloff_end, 30.0 * (1.5 - 0.025)), 128 | "ads falloff end: {}", 129 | response.ads_falloff_end 130 | ); 131 | }); 132 | } 133 | 134 | #[test] 135 | fn test_pulse_firing_data() { 136 | setup_pulse(); 137 | PERS_DATA.with(|perm_data| { 138 | let weapon = perm_data.borrow_mut().weapon.clone(); 139 | let mut response = weapon.calc_firing_data(None, None, true); 140 | PERS_DATA.with(|perm_data| { 141 | response.apply_pve_bonuses( 142 | perm_data.borrow().activity.get_rpl_mult(), 143 | perm_data.borrow().activity.get_pl_delta(), 144 | perm_data.borrow().weapon.damage_mods.pve, 145 | perm_data 146 | .borrow() 147 | .weapon 148 | .damage_mods 149 | .get_mod(&perm_data.borrow().enemy.type_), 150 | ) 151 | }); 152 | assert!( 153 | cmp_floats(response.pvp_impact_damage, 10.0), 154 | "impact damage: {}", 155 | response.pvp_impact_damage 156 | ); 157 | assert!( 158 | cmp_floats(response.pvp_explosion_damage, 0.0), 159 | "explosive damage: {}", 160 | response.pvp_explosion_damage 161 | ); 162 | assert!(cmp_floats(response.rpm, 900.0), "rpm: {}", response.rpm); 163 | assert!( 164 | cmp_floats(response.pvp_crit_mult, 2.0), 165 | "crit mult: {}", 166 | response.pvp_crit_mult 167 | ); 168 | }); 169 | } 170 | 171 | 172 | fn setup_bow() { 173 | let vec = Vec::::from("harm".to_string()); 174 | let mut hash = 0; 175 | for i in 0..vec.len() { 176 | hash += vec[i] as u32; 177 | if i < vec.len() - 1 { 178 | hash <<= 8; 179 | } 180 | } 181 | let mut new_weapon = Weapon::generate_weapon( 182 | hash, //harm turned himslf into a u32! Funniest shit I've ever seen 183 | 31, //bow 184 | 696969, //test bow 185 | 2, //special 186 | 3949783978, //strand 187 | ) 188 | .unwrap(); 189 | let mut stats = HashMap::new(); 190 | stats.insert(StatHashes::RELOAD.into(), Stat::from(50)); 191 | stats.insert(StatHashes::HANDLING.into(), Stat::from(50)); 192 | stats.insert(StatHashes::RANGE.into(), Stat::from(50)); 193 | stats.insert(StatHashes::ZOOM.into(), Stat::from(15)); 194 | new_weapon.set_stats(stats); 195 | PERS_DATA.with(|perm_data| { 196 | perm_data.borrow_mut().weapon = new_weapon; 197 | }); 198 | } 199 | 200 | #[test] 201 | fn test_bow_setup() { 202 | setup_bow(); 203 | PERS_DATA.with(|perm_data| { 204 | let mut weapon = perm_data.borrow().weapon.clone(); 205 | assert_eq!(weapon.damage_type, DamageType::STRAND); 206 | assert_eq!(weapon.ammo_type, AmmoType::SPECIAL); 207 | assert_eq!(weapon.intrinsic_hash, 696969); 208 | assert_eq!(weapon.weapon_type, WeaponType::BOW); 209 | let test_stat = weapon 210 | .get_stats() 211 | .get(&(StatHashes::HANDLING.into())) 212 | .unwrap() 213 | .val(); 214 | assert_eq!(test_stat, 50, "test_stat: {}", test_stat); 215 | }); 216 | } 217 | 218 | #[test] 219 | fn test_bow_reload() { 220 | setup_bow(); 221 | PERS_DATA.with(|perm_data| { 222 | let weapon = perm_data.borrow_mut().weapon.clone(); 223 | let response = weapon.calc_reload_time(None, None, true); 224 | assert!( 225 | cmp_floats(response.reload_time, 5.0), 226 | "reload time: {}", 227 | response.reload_time 228 | ); 229 | }); 230 | } 231 | 232 | #[test] 233 | fn test_bow_handling() { 234 | setup_bow(); 235 | PERS_DATA.with(|perm_data| { 236 | let weapon = perm_data.borrow_mut().weapon.clone(); 237 | let response = weapon.calc_handling_times(None, None, true); 238 | assert!( 239 | cmp_floats(response.ads_time, 5.0), 240 | "ads time: {}", 241 | response.ads_time 242 | ); 243 | assert!( 244 | cmp_floats(response.ready_time, 5.0), 245 | "ready time: {}", 246 | response.ready_time 247 | ); 248 | assert!( 249 | cmp_floats(response.stow_time, 5.0), 250 | "stow time: {}", 251 | response.stow_time 252 | ); 253 | }); 254 | } 255 | 256 | #[test] 257 | fn test_bow_range() { 258 | setup_bow(); 259 | PERS_DATA.with(|perm_data| { 260 | let weapon = perm_data.borrow_mut().weapon.clone(); 261 | let response = weapon.calc_range_falloff(None, None, true); 262 | assert!( 263 | response.ads_falloff_start > 998.0, 264 | "ads falloff start: {}", 265 | response.ads_falloff_start 266 | ); 267 | assert!( 268 | response.hip_falloff_end > 998.0, 269 | "hip falloff end: {}", 270 | response.hip_falloff_end 271 | ); 272 | }); 273 | } 274 | 275 | #[test] 276 | fn test_bow_firing_data() { 277 | setup_bow(); 278 | PERS_DATA.with(|perm_data| { 279 | let weapon = perm_data.borrow_mut().weapon.clone(); 280 | let mut response = weapon.calc_firing_data(None, None, true); 281 | PERS_DATA.with(|perm_data| { 282 | response.apply_pve_bonuses( 283 | perm_data.borrow().activity.get_rpl_mult(), 284 | perm_data.borrow().activity.get_pl_delta(), 285 | perm_data.borrow().weapon.damage_mods.pve, 286 | perm_data 287 | .borrow() 288 | .weapon 289 | .damage_mods 290 | .get_mod(&perm_data.borrow().enemy.type_), 291 | ) 292 | }); 293 | assert!( 294 | cmp_floats(response.pvp_impact_damage, 100.0), 295 | "impact damage: {}", 296 | response.pvp_impact_damage 297 | ); 298 | assert!( 299 | cmp_floats(response.pvp_explosion_damage, 0.0), 300 | "explosive damage: {}", 301 | response.pvp_explosion_damage 302 | ); 303 | assert!(cmp_floats(response.burst_delay, 20.0/30.0), "draw time: {}", response.burst_delay); 304 | assert!( 305 | cmp_floats(response.pvp_crit_mult, 1.5 + (2.0/51.0)), 306 | "crit mult: {}", 307 | response.pvp_crit_mult 308 | ); 309 | }); 310 | } 311 | -------------------------------------------------------------------------------- /src/types/js_types.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "wasm")] 2 | 3 | use std::collections::HashMap; 4 | 5 | use crate::{ 6 | activity::damage_calc::DifficultyOptions, 7 | enemies::EnemyType, 8 | perks::Perk, 9 | types::rs_types::StatQuadraticFormula, 10 | weapons::{ 11 | ttk_calc::{BodyKillData, OptimalKillData, ResillienceSummary}, 12 | Stat, 13 | }, 14 | }; 15 | use serde::{Deserialize, Serialize}; 16 | // use tsify::Tsify; 17 | use wasm_bindgen::{ 18 | convert::{IntoWasmAbi, WasmSlice}, 19 | prelude::wasm_bindgen, 20 | JsValue, 21 | }; 22 | 23 | use super::rs_types::{ 24 | AmmoFormula, AmmoResponse, DamageMods, DpsResponse, FiringData, FiringResponse, 25 | HandlingFormula, HandlingResponse, RangeFormula, RangeResponse, ReloadFormula, ReloadResponse, 26 | }; 27 | 28 | #[derive(Debug, Clone, Copy, Serialize)] 29 | #[wasm_bindgen(js_name = "HandlingResponse", inspectable)] 30 | pub struct JsHandlingResponse { 31 | #[wasm_bindgen(js_name = "readyTime", readonly)] 32 | pub ready_time: f64, 33 | #[wasm_bindgen(js_name = "stowTime", readonly)] 34 | pub stow_time: f64, 35 | #[wasm_bindgen(js_name = "adsTime", readonly)] 36 | pub ads_time: f64, 37 | #[wasm_bindgen(js_name = "timestamp", readonly)] 38 | pub timestamp: u32, 39 | } 40 | impl From for JsHandlingResponse { 41 | fn from(handling: HandlingResponse) -> Self { 42 | JsHandlingResponse { 43 | ready_time: handling.ready_time, 44 | stow_time: handling.stow_time, 45 | ads_time: handling.ads_time, 46 | timestamp: handling.timestamp as u32, 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone, Copy, Serialize)] 52 | #[wasm_bindgen(js_name = "RangeResponse", inspectable)] 53 | pub struct JsRangeResponse { 54 | #[wasm_bindgen(js_name = "hipFalloffStart", readonly)] 55 | pub hip_falloff_start: f64, 56 | #[wasm_bindgen(js_name = "hipFalloffEnd", readonly)] 57 | pub hip_falloff_end: f64, 58 | #[wasm_bindgen(js_name = "adsFalloffStart", readonly)] 59 | pub ads_falloff_start: f64, 60 | #[wasm_bindgen(js_name = "adsFalloffEnd", readonly)] 61 | pub ads_falloff_end: f64, 62 | #[wasm_bindgen(js_name = "floorPercent", readonly)] 63 | pub floor_percent: f64, 64 | #[wasm_bindgen(js_name = "timestamp", readonly)] 65 | pub timestamp: u32, 66 | } 67 | impl From for JsRangeResponse { 68 | fn from(range: RangeResponse) -> Self { 69 | JsRangeResponse { 70 | hip_falloff_start: range.hip_falloff_start, 71 | hip_falloff_end: range.hip_falloff_end, 72 | ads_falloff_start: range.ads_falloff_start, 73 | ads_falloff_end: range.ads_falloff_end, 74 | floor_percent: range.floor_percent, 75 | timestamp: range.timestamp as u32, 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, Clone, Copy, Serialize)] 81 | #[wasm_bindgen(js_name = "ReloadResponse", inspectable)] 82 | pub struct JsReloadResponse { 83 | #[wasm_bindgen(js_name = "reloadTime", readonly)] 84 | pub reload_time: f64, 85 | #[wasm_bindgen(js_name = "ammoTime", readonly)] 86 | pub ammo_time: f64, 87 | #[wasm_bindgen(js_name = "timestamp", readonly)] 88 | pub timestamp: u32, 89 | } 90 | impl From for JsReloadResponse { 91 | fn from(reload: ReloadResponse) -> Self { 92 | JsReloadResponse { 93 | reload_time: reload.reload_time, 94 | ammo_time: reload.ammo_time, 95 | timestamp: reload.timestamp as u32, 96 | } 97 | } 98 | } 99 | 100 | #[derive(Debug, Clone, Copy, Serialize)] 101 | #[wasm_bindgen(js_name = "AmmoResponse", inspectable)] 102 | pub struct JsAmmoResponse { 103 | #[wasm_bindgen(js_name = "magSize", readonly)] 104 | pub mag_size: i32, 105 | #[wasm_bindgen(js_name = "reserveSize", readonly)] 106 | pub reserve_size: i32, 107 | #[wasm_bindgen(js_name = "timestamp", readonly)] 108 | pub timestamp: u32, 109 | } 110 | impl From for JsAmmoResponse { 111 | fn from(ammo: AmmoResponse) -> Self { 112 | JsAmmoResponse { 113 | mag_size: ammo.mag_size, 114 | reserve_size: ammo.reserve_size, 115 | timestamp: ammo.timestamp as u32, 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug, Clone, Serialize)] 121 | #[wasm_bindgen(js_name = "DpsResponse")] 122 | pub struct JsDpsResponse { 123 | #[wasm_bindgen(skip)] 124 | pub dps_per_mag: Vec, 125 | #[wasm_bindgen(skip)] 126 | pub time_damage_data: Vec<(f64, f64)>, 127 | #[wasm_bindgen(js_name = "totalDamage", readonly)] 128 | pub total_damage: f64, 129 | #[wasm_bindgen(js_name = "totalTime", readonly)] 130 | pub total_time: f64, 131 | #[wasm_bindgen(js_name = "totalShots", readonly)] 132 | pub total_shots: i32, 133 | } 134 | #[wasm_bindgen(js_class = "DpsResponse")] 135 | impl JsDpsResponse { 136 | #[wasm_bindgen(js_name = "toString")] 137 | pub fn to_string(self) -> String { 138 | format!("{:?}", self) 139 | } 140 | #[wasm_bindgen(js_name = "toJSON")] 141 | pub fn to_json(self) -> String { 142 | serde_wasm_bindgen::to_value(&self) 143 | .unwrap() 144 | .as_string() 145 | .unwrap() 146 | } 147 | ///Returns a list of tuples of time and damage 148 | #[wasm_bindgen(getter, js_name = "timeDamageData")] 149 | pub fn time_damage_data(&self) -> JsValue { 150 | serde_wasm_bindgen::to_value(&self.time_damage_data).unwrap() 151 | } 152 | ///Returns a list of dps values for each magazine 153 | #[wasm_bindgen(getter, js_name = "dpsPerMag")] 154 | pub fn dps_per_mag(&self) -> JsValue { 155 | serde_wasm_bindgen::to_value(&self.dps_per_mag).unwrap() 156 | } 157 | } 158 | impl From for JsDpsResponse { 159 | fn from(dps: DpsResponse) -> Self { 160 | JsDpsResponse { 161 | dps_per_mag: dps.dps_per_mag, 162 | time_damage_data: dps.time_damage_data, 163 | total_damage: dps.total_damage, 164 | total_time: dps.total_time, 165 | total_shots: dps.total_shots, 166 | } 167 | } 168 | } 169 | 170 | #[derive(Debug, Clone, Copy, Serialize)] 171 | #[wasm_bindgen(js_name = "OptimalKillData", inspectable)] 172 | pub struct JsOptimalKillData { 173 | pub headshots: i32, 174 | pub bodyshots: i32, 175 | #[serde(rename = "timeTaken")] 176 | #[wasm_bindgen(js_name = "timeTaken")] 177 | pub time_taken: f64, 178 | } 179 | impl From for JsOptimalKillData { 180 | fn from(optimal: OptimalKillData) -> Self { 181 | JsOptimalKillData { 182 | headshots: optimal.headshots, 183 | bodyshots: optimal.bodyshots, 184 | time_taken: optimal.time_taken, 185 | } 186 | } 187 | } 188 | 189 | #[derive(Debug, Clone, Copy, Serialize)] 190 | #[wasm_bindgen(js_name = "BodyKillData", inspectable)] 191 | pub struct JsBodyKillData { 192 | pub bodyshots: i32, 193 | #[serde(rename = "timeTaken")] 194 | #[wasm_bindgen(js_name = "timeTaken")] 195 | pub time_taken: f64, 196 | } 197 | impl From for JsBodyKillData { 198 | fn from(body: BodyKillData) -> Self { 199 | JsBodyKillData { 200 | bodyshots: body.bodyshots, 201 | time_taken: body.time_taken, 202 | } 203 | } 204 | } 205 | 206 | #[derive(Debug, Clone, Serialize)] 207 | #[wasm_bindgen(js_name = "ResillienceSummary", inspectable)] 208 | pub struct JsResillienceSummary { 209 | #[serde(rename = "resillienceValue")] 210 | #[wasm_bindgen(js_name = "resillienceValue")] 211 | pub value: i32, 212 | #[serde(rename = "bodyTtk")] 213 | #[wasm_bindgen(js_name = "bodyTtk")] 214 | pub body_ttk: JsBodyKillData, 215 | #[serde(rename = "optimalTtk")] 216 | #[wasm_bindgen(js_name = "optimalTtk")] 217 | pub optimal_ttk: JsOptimalKillData, 218 | } 219 | impl From for JsResillienceSummary { 220 | fn from(resillience: ResillienceSummary) -> Self { 221 | JsResillienceSummary { 222 | value: resillience.value, 223 | body_ttk: resillience.body_ttk.into(), 224 | optimal_ttk: resillience.optimal_ttk.into(), 225 | } 226 | } 227 | } 228 | 229 | #[derive(Debug, Clone, Default, Serialize)] 230 | #[wasm_bindgen(js_name = "FiringResponse", inspectable)] 231 | pub struct JsFiringResponse { 232 | #[wasm_bindgen(js_name = "pvpImpactDamage", readonly)] 233 | pub pvp_impact_damage: f64, 234 | #[wasm_bindgen(js_name = "pvpExplosionDamage", readonly)] 235 | pub pvp_explosion_damage: f64, 236 | #[wasm_bindgen(js_name = "pvpCritMult", readonly)] 237 | pub pvp_crit_mult: f64, 238 | 239 | #[wasm_bindgen(js_name = "pveImpactDamage", readonly)] 240 | pub pve_impact_damage: f64, 241 | #[wasm_bindgen(js_name = "pveExplosionDamage", readonly)] 242 | pub pve_explosion_damage: f64, 243 | #[wasm_bindgen(js_name = "pveCritMult", readonly)] 244 | pub pve_crit_mult: f64, 245 | 246 | #[wasm_bindgen(js_name = "burstDelay", readonly)] 247 | pub burst_delay: f64, 248 | #[wasm_bindgen(js_name = "innerBurstDelay", readonly)] 249 | pub inner_burst_delay: f64, 250 | #[wasm_bindgen(js_name = "burstSize", readonly)] 251 | pub burst_size: i32, 252 | #[wasm_bindgen(js_name = "timestamp", readonly)] 253 | pub timestamp: u32, 254 | #[wasm_bindgen(js_name = "rpm", readonly)] 255 | pub rpm: f64, 256 | } 257 | impl From for JsFiringResponse { 258 | fn from(firing: FiringResponse) -> Self { 259 | JsFiringResponse { 260 | pvp_impact_damage: firing.pvp_impact_damage, 261 | pvp_explosion_damage: firing.pvp_explosion_damage, 262 | pvp_crit_mult: firing.pvp_crit_mult, 263 | pve_impact_damage: firing.pve_impact_damage, 264 | pve_explosion_damage: firing.pve_explosion_damage, 265 | pve_crit_mult: firing.pve_crit_mult, 266 | burst_delay: firing.burst_delay, 267 | inner_burst_delay: firing.inner_burst_delay, 268 | burst_size: firing.burst_size, 269 | rpm: firing.rpm, 270 | timestamp: firing.timestamp as u32, 271 | } 272 | } 273 | } 274 | 275 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] 276 | #[wasm_bindgen(js_name = "Stat")] 277 | pub struct JsStat { 278 | #[wasm_bindgen(js_name = "baseValue")] 279 | #[serde(rename = "baseValue")] 280 | pub base_value: i32, 281 | #[wasm_bindgen(js_name = "partValue")] 282 | #[serde(rename = "partValue")] 283 | pub part_value: i32, 284 | #[wasm_bindgen(js_name = "traitValue")] 285 | #[serde(rename = "traitValue")] 286 | pub trait_value: i32, 287 | } 288 | #[wasm_bindgen(js_class = "Stat")] 289 | impl JsStat { 290 | #[wasm_bindgen(js_name = "toString")] 291 | pub fn to_string(self) -> String { 292 | format!("{:?}", self) 293 | } 294 | } 295 | impl From for JsStat { 296 | fn from(stat: Stat) -> Self { 297 | JsStat { 298 | base_value: stat.base_value, 299 | part_value: stat.part_value, 300 | trait_value: stat.perk_value, 301 | } 302 | } 303 | } 304 | 305 | #[derive(Debug, Clone, Serialize)] 306 | #[wasm_bindgen(js_name = "MetaData", inspectable)] 307 | pub struct JsMetaData { 308 | #[wasm_bindgen(js_name = "apiVersion", readonly)] 309 | pub api_version: &'static str, 310 | #[wasm_bindgen(js_name = "apiTimestamp", readonly)] 311 | pub api_timestamp: &'static str, 312 | #[wasm_bindgen(js_name = "apiGitCommit", readonly)] 313 | pub api_commit: &'static str, 314 | #[wasm_bindgen(js_name = "apiGitBranch", readonly)] 315 | pub api_branch: &'static str, 316 | } 317 | 318 | #[derive(Debug, Clone, Default)] 319 | #[cfg(feature = "foundry")] 320 | #[wasm_bindgen(js_name = "ScalarResponseSummary", inspectable)] 321 | pub struct JsScalarResponse { 322 | #[wasm_bindgen(js_name = "reloadScalar", readonly)] 323 | pub reload_scalar: f64, 324 | #[wasm_bindgen(js_name = "drawScalar", readonly)] 325 | pub draw_scalar: f64, 326 | #[wasm_bindgen(js_name = "adsScalar", readonly)] 327 | pub ads_scalar: f64, 328 | #[wasm_bindgen(js_name = "stowScalar", readonly)] 329 | pub stow_scalar: f64, 330 | #[wasm_bindgen(js_name = "globalRangeScalar", readonly)] 331 | pub global_range_scalar: f64, 332 | #[wasm_bindgen(js_name = "hipfireRangeScalar", readonly)] 333 | pub hipfire_range_scalar: f64, 334 | #[wasm_bindgen(js_name = "adsRangeScalar", readonly)] 335 | pub ads_range_scalar: f64, 336 | #[wasm_bindgen(js_name = "magSizeScalar", readonly)] 337 | pub mag_size_scalar: f64, 338 | #[wasm_bindgen(js_name = "reserveSizeScalar", readonly)] 339 | pub reserve_size_scalar: f64, 340 | } 341 | 342 | #[derive(Debug, Clone)] 343 | #[wasm_bindgen(js_name = "DifficultyOptions")] 344 | pub enum JsDifficultyOptions { 345 | NORMAL = 1, 346 | RAID = 2, 347 | MASTER = 3, 348 | } 349 | impl Into for JsDifficultyOptions { 350 | fn into(self) -> DifficultyOptions { 351 | match self { 352 | JsDifficultyOptions::NORMAL => DifficultyOptions::NORMAL, 353 | JsDifficultyOptions::RAID => DifficultyOptions::RAID, 354 | JsDifficultyOptions::MASTER => DifficultyOptions::MASTER, 355 | } 356 | } 357 | } 358 | 359 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 360 | #[wasm_bindgen(js_name = "EnemyType")] 361 | pub enum JsEnemyType { 362 | MINOR, 363 | ELITE, 364 | MINIBOSS, 365 | BOSS, 366 | VEHICLE, 367 | ENCLAVE, 368 | PLAYER, 369 | CHAMPION, 370 | } 371 | impl Into for JsEnemyType { 372 | fn into(self) -> EnemyType { 373 | match self { 374 | JsEnemyType::MINOR => EnemyType::MINOR, 375 | JsEnemyType::ELITE => EnemyType::ELITE, 376 | JsEnemyType::MINIBOSS => EnemyType::MINIBOSS, 377 | JsEnemyType::BOSS => EnemyType::BOSS, 378 | JsEnemyType::VEHICLE => EnemyType::VEHICLE, 379 | JsEnemyType::ENCLAVE => EnemyType::ENCLAVE, 380 | JsEnemyType::PLAYER => EnemyType::PLAYER, 381 | JsEnemyType::CHAMPION => EnemyType::CHAMPION, 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "wasm")] 2 | pub mod js_types; 3 | #[cfg(feature = "python")] 4 | pub mod py_types; 5 | 6 | pub mod rs_types; 7 | -------------------------------------------------------------------------------- /src/types/rs_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::enemies::EnemyType; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct DataPointers { 9 | pub h: usize, 10 | pub r: usize, 11 | pub rl: usize, 12 | pub s: usize, 13 | pub f: usize, 14 | pub a: usize, 15 | } 16 | 17 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 18 | pub struct WeaponPath(pub u32, pub u32); 19 | 20 | #[derive(Debug, Clone, Default, Copy, Serialize)] 21 | pub struct FiringData { 22 | pub damage: f64, 23 | pub crit_mult: f64, 24 | pub burst_delay: f64, 25 | pub inner_burst_delay: f64, 26 | pub burst_size: i32, 27 | pub one_ammo: bool, 28 | pub charge: bool, 29 | pub timestamp: u64, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize)] 33 | pub struct DamageMods { 34 | pub pve: f64, 35 | pub minor: f64, 36 | pub elite: f64, 37 | pub miniboss: f64, 38 | pub champion: f64, 39 | pub boss: f64, 40 | pub vehicle: f64, 41 | pub timestamp: u64, 42 | } 43 | impl Default for DamageMods { 44 | fn default() -> Self { 45 | DamageMods { 46 | pve: 1.0, 47 | minor: 1.0, 48 | elite: 1.0, 49 | miniboss: 1.0, 50 | champion: 1.0, 51 | boss: 1.0, 52 | vehicle: 1.0, 53 | timestamp: 0, 54 | } 55 | } 56 | } 57 | impl DamageMods { 58 | pub fn get_mod(&self, _type: &EnemyType) -> f64 { 59 | let combatant_scale = match _type { 60 | &EnemyType::MINOR => self.minor, 61 | &EnemyType::ELITE => self.elite, 62 | &EnemyType::MINIBOSS => self.miniboss, 63 | &EnemyType::CHAMPION => self.champion, 64 | &EnemyType::BOSS => self.boss, 65 | &EnemyType::VEHICLE => self.vehicle, 66 | _ => 1.0, 67 | }; 68 | combatant_scale 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone, Default, Serialize)] 73 | pub struct RangeFormula { 74 | pub start: StatQuadraticFormula, 75 | pub end: StatQuadraticFormula, 76 | pub floor_percent: f64, 77 | pub fusion: bool, 78 | pub timestamp: u64, 79 | } 80 | 81 | //even if just linear use this 82 | #[derive(Debug, Clone, Default, Serialize)] 83 | pub struct StatQuadraticFormula { 84 | pub evpp: f64, 85 | pub vpp: f64, 86 | pub offset: f64, 87 | } 88 | impl StatQuadraticFormula { 89 | pub fn solve_at(&self, _x: f64) -> f64 { 90 | self.evpp * _x * _x + self.vpp * _x + self.offset 91 | } 92 | } 93 | 94 | #[derive(Debug, Clone, Default, Serialize)] 95 | pub struct ReloadFormula { 96 | pub reload_data: StatQuadraticFormula, 97 | pub ammo_percent: f64, 98 | pub timestamp: u64, 99 | } 100 | 101 | #[derive(Debug, Clone, Default, Serialize)] 102 | pub struct HandlingFormula { 103 | pub ready: StatQuadraticFormula, 104 | pub stow: StatQuadraticFormula, 105 | pub ads: StatQuadraticFormula, 106 | pub timestamp: u64, 107 | } 108 | 109 | #[derive(Debug, Clone, Default, Serialize)] 110 | pub struct AmmoFormula { 111 | pub mag: StatQuadraticFormula, 112 | pub round_to: i32, 113 | pub reserve_id: u32, 114 | pub timestamp: u64, 115 | } 116 | 117 | #[derive(Debug, Clone, Default)] 118 | pub struct RangeResponse { 119 | pub hip_falloff_start: f64, 120 | pub hip_falloff_end: f64, 121 | pub ads_falloff_start: f64, 122 | pub ads_falloff_end: f64, 123 | pub floor_percent: f64, 124 | pub timestamp: u64, 125 | } 126 | 127 | #[derive(Debug, Clone, Default, Copy)] 128 | pub struct HandlingResponse { 129 | pub ready_time: f64, 130 | pub stow_time: f64, 131 | pub ads_time: f64, 132 | pub timestamp: u64, 133 | } 134 | 135 | #[derive(Debug, Clone, Default)] 136 | pub struct AmmoResponse { 137 | pub mag_size: i32, 138 | pub reserve_size: i32, 139 | pub timestamp: u64, 140 | } 141 | 142 | #[derive(Debug, Clone, Default)] 143 | pub struct ReloadResponse { 144 | pub reload_time: f64, 145 | pub ammo_time: f64, 146 | pub timestamp: u64, 147 | } 148 | 149 | #[derive(Debug, Clone, Default)] 150 | pub struct DpsResponse { 151 | pub dps_per_mag: Vec, 152 | pub time_damage_data: Vec<(f64, f64)>, 153 | pub total_damage: f64, 154 | pub total_time: f64, 155 | pub total_shots: i32, 156 | } 157 | impl DpsResponse { 158 | pub fn apply_rpl(&mut self, rpl: f64) { 159 | for mag in self.dps_per_mag.iter_mut() { 160 | *mag *= rpl; 161 | } 162 | for (_, damage) in self.time_damage_data.iter_mut() { 163 | *damage *= rpl; 164 | } 165 | self.total_damage *= rpl; 166 | } 167 | pub fn get_dps_over_time(&self) -> Vec<(f64, f64)> { 168 | let dps_data = &self.time_damage_data; 169 | let mut damage_so_far = dps_data[0].1; 170 | let mut dps_lst = Vec::new(); 171 | for hit in dps_data { 172 | if hit.0 != 0.0 { 173 | dps_lst.push((hit.0, damage_so_far / hit.0)); 174 | } 175 | damage_so_far += hit.1; 176 | } 177 | dps_lst 178 | } 179 | } 180 | 181 | #[derive(Debug, Clone, Default)] 182 | pub struct FiringResponse { 183 | pub pvp_impact_damage: f64, 184 | pub pvp_explosion_damage: f64, 185 | pub pvp_crit_mult: f64, 186 | 187 | pub pve_impact_damage: f64, 188 | pub pve_explosion_damage: f64, 189 | pub pve_crit_mult: f64, 190 | 191 | pub burst_delay: f64, 192 | pub inner_burst_delay: f64, 193 | pub burst_size: i32, 194 | 195 | pub rpm: f64, 196 | 197 | pub timestamp: u64, 198 | } 199 | impl FiringResponse { 200 | pub fn apply_pve_bonuses( 201 | &mut self, 202 | _rpl_mult: f64, 203 | _gpl_mult: f64, 204 | _pve_mult: f64, 205 | _combatant_mult: f64, 206 | ) { 207 | self.pve_impact_damage *= _rpl_mult * _gpl_mult * _pve_mult * _combatant_mult; 208 | self.pve_explosion_damage *= _rpl_mult * _gpl_mult * _pve_mult * _combatant_mult; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/weapons/dps_calc.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::{cell::RefCell, rc::Rc}; 3 | 4 | use super::Weapon; 5 | use crate::d2_enums::{AmmoType, WeaponType}; 6 | use crate::enemies::Enemy; 7 | use crate::perks::lib::{ 8 | CalculationInput, ExtraDamageResponse, RefundResponse, ReloadOverrideResponse, 9 | }; 10 | use crate::perks::*; 11 | use crate::types::rs_types::DpsResponse; 12 | 13 | //first entry in tuple is refund to mag, second is too reserves 14 | pub fn calc_refund(_shots_hit_this_mag: i32, _refunds: Vec) -> (i32, i32) { 15 | let mut refund_ammount = (0, 0); 16 | for refund in _refunds { 17 | if _shots_hit_this_mag % refund.requirement == 0 { 18 | refund_ammount.0 += refund.refund_mag; 19 | refund_ammount.1 += refund.refund_reserves; 20 | } 21 | } 22 | return refund_ammount; 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct ExtraDamageResult { 27 | pub extra_time: f64, 28 | pub extra_dmg: f64, 29 | pub extra_hits: i32, 30 | pub extra_time_dmg: Vec<(f64, f64)>, 31 | } 32 | #[derive(Debug, Clone)] 33 | pub struct ExtraDamageBuffInfo { 34 | pub pl_buff: f64, 35 | pub pve_buff: f64, 36 | pub impact_buff: f64, 37 | pub explosive_buff: f64, 38 | pub crit_buff: f64, 39 | pub combatant_buff: f64, 40 | } 41 | impl ExtraDamageBuffInfo { 42 | pub fn get_buff_amount(&self, entry: &ExtraDamageResponse) -> f64 { 43 | let mut buff = self.pl_buff; 44 | if entry.weapon_scale { 45 | buff *= (self.impact_buff + self.explosive_buff) / 2.0; 46 | buff *= self.pve_buff 47 | }; 48 | if entry.crit_scale { 49 | buff *= self.crit_buff 50 | }; 51 | if entry.combatant_scale { 52 | buff *= self.combatant_buff 53 | }; 54 | buff 55 | } 56 | } 57 | pub fn calc_extra_dmg( 58 | _total_time: f64, 59 | _extra_dmg_entries: Vec, 60 | _dmg_buffs: ExtraDamageBuffInfo, 61 | ) -> ExtraDamageResult { 62 | let mut extra_time = 0.0; 63 | let mut extra_dmg = 0.0; 64 | let mut extra_hits = 0; 65 | let mut extra_time_dmg: Vec<(f64, f64)> = Vec::new(); 66 | for entry in _extra_dmg_entries { 67 | if entry.additive_damage > 0.0 { 68 | if entry.hit_at_same_time { 69 | let mut bonus_dmg = entry.additive_damage * entry.times_to_hit as f64; 70 | bonus_dmg *= _dmg_buffs.get_buff_amount(&entry); 71 | extra_dmg += bonus_dmg; 72 | if entry.increment_total_time { 73 | extra_time += entry.time_for_additive_damage 74 | }; 75 | extra_time_dmg.push((_total_time + entry.time_for_additive_damage, bonus_dmg)); 76 | extra_hits += entry.times_to_hit; 77 | } else if entry.is_dot == false { 78 | for i in 0..entry.times_to_hit { 79 | let mut bonus_dmg = entry.additive_damage; 80 | bonus_dmg *= _dmg_buffs.get_buff_amount(&entry); 81 | extra_dmg += bonus_dmg; 82 | if entry.increment_total_time { 83 | extra_time += entry.time_for_additive_damage 84 | }; 85 | extra_time_dmg.push(( 86 | _total_time + entry.time_for_additive_damage * i as f64, 87 | bonus_dmg, 88 | )); 89 | extra_hits += 1; 90 | } 91 | } else { 92 | //all dot does is increment time backwards 93 | for i in 0..entry.times_to_hit { 94 | let mut bonus_dmg = entry.additive_damage; 95 | bonus_dmg *= _dmg_buffs.get_buff_amount(&entry); 96 | extra_dmg += bonus_dmg; 97 | if entry.increment_total_time { 98 | extra_time += entry.time_for_additive_damage 99 | }; 100 | extra_time_dmg.push(( 101 | _total_time - entry.time_for_additive_damage * i as f64, 102 | bonus_dmg, 103 | )); 104 | extra_hits += 1; 105 | } 106 | } 107 | } 108 | } 109 | ExtraDamageResult { 110 | extra_time, 111 | extra_dmg, 112 | extra_hits, 113 | extra_time_dmg, 114 | } 115 | } 116 | 117 | pub fn complex_dps_calc(_weapon: Weapon, _enemy: Enemy, _pl_dmg_mult: f64) -> DpsResponse { 118 | let weapon = Rc::new(_weapon.clone()); 119 | let stats = weapon.stats.clone(); 120 | let weapon_type = weapon.weapon_type.clone(); 121 | let ammo_type = weapon.ammo_type.clone(); 122 | 123 | let tmp_dmg_prof = weapon.get_damage_profile(); 124 | let impact_dmg = tmp_dmg_prof.0; 125 | let explosion_dmg = tmp_dmg_prof.1; 126 | let crit_mult = tmp_dmg_prof.2; 127 | // let damage_delay = tmp_dmg_prof.3; 128 | 129 | let base_mag = weapon.calc_ammo_sizes(None, None, false).mag_size; 130 | let maximum_shots = if base_mag * 5 < 15 { 15 } else { base_mag * 5 }; 131 | 132 | let firing_settings = _weapon.firing_data.clone(); 133 | let perks = weapon.list_perks(); 134 | 135 | let burst_size = firing_settings.burst_size as f64; 136 | let burst_delay = firing_settings.burst_delay; 137 | let inner_burst_delay = firing_settings.inner_burst_delay; 138 | 139 | let mut total_damage = 0.0_f64; 140 | let mut total_time = 0.0_f64; 141 | 142 | let mut time_damage_data: Vec<(f64, f64)> = Vec::new(); //used for chart stuff 143 | let mut dps_per_mag: Vec = Vec::new(); //used for chart stuff 144 | 145 | let mut total_shots_fired = 0_i32; 146 | let mut total_shots_hit = 0_i32; 147 | let mut num_reloads = 0_i32; 148 | 149 | let mut pers_calc_data: HashMap = HashMap::new(); 150 | 151 | let mut reserve = weapon 152 | .calc_ammo_sizes( 153 | Some(weapon.static_calc_input()), 154 | Some(&mut pers_calc_data), 155 | false, 156 | ) 157 | .reserve_size; 158 | 159 | #[allow(unused_mut)] 160 | while reserve > 0 { 161 | let mut shots_this_mag = 0; 162 | //MAGAZINE///////////////////// 163 | let mag_calc_input = weapon.sparse_calc_input(total_shots_fired, total_time); 164 | let mut mag = weapon 165 | .calc_ammo_sizes(Some(mag_calc_input), Some(&mut pers_calc_data), false) 166 | .mag_size; 167 | if mag > reserve { 168 | mag = reserve 169 | } 170 | /////////////////////////////// 171 | 172 | //HANDLING///////////////////// 173 | //This is for stuff like weapon swapping, demo or trench barrel 174 | let handling_calc_input = weapon.sparse_calc_input(total_shots_fired, total_time); 175 | let handling_data = 176 | weapon.calc_handling_times(Some(handling_calc_input), Some(&mut pers_calc_data), false); 177 | /////////////////////////////// 178 | let mut start_time = total_time.clone(); 179 | while mag > 0 { 180 | //DMG MODIFIERS//////////////// 181 | let before_shot_input_data = CalculationInput { 182 | intrinsic_hash: weapon.intrinsic_hash, 183 | curr_firing_data: &firing_settings, 184 | base_crit_mult: crit_mult, 185 | base_mag: base_mag as f64, 186 | curr_mag: mag as f64, 187 | ammo_type: &ammo_type, 188 | weapon_type: &weapon_type, 189 | stats: &stats, 190 | perk_value_map: &weapon.perk_value_map_update(), 191 | enemy_type: &_enemy.type_, 192 | shots_fired_this_mag: shots_this_mag as f64, 193 | total_shots_fired: total_shots_fired as f64, 194 | total_shots_hit: total_shots_hit as f64, 195 | reserves_left: reserve as f64, 196 | time_total: total_time, 197 | time_this_mag: (total_time - start_time), 198 | damage_type: &weapon.damage_type, 199 | handling_data: handling_data, 200 | num_reloads: num_reloads as f64, 201 | has_overshield: false, 202 | }; 203 | let dmg_mods = get_dmg_modifier( 204 | perks.clone(), 205 | &before_shot_input_data, 206 | false, 207 | &mut pers_calc_data, 208 | ); 209 | /////////////////////////////// 210 | 211 | //FIRING MODIFIERS///////////// 212 | let firing_mods = get_firing_modifier( 213 | perks.clone(), 214 | &before_shot_input_data, 215 | false, 216 | &mut pers_calc_data, 217 | ); 218 | /////////////////////////////// 219 | 220 | let dmg = { 221 | ((impact_dmg * dmg_mods.impact_dmg_scale) * (crit_mult * dmg_mods.crit_scale) 222 | + (explosion_dmg * dmg_mods.explosive_dmg_scale)) 223 | * _pl_dmg_mult 224 | * weapon.damage_mods.get_mod(&_enemy.type_) 225 | * weapon.damage_mods.pve 226 | }; 227 | 228 | let shot_burst_delay = 229 | (burst_delay + firing_mods.burst_delay_add) * firing_mods.burst_delay_scale; 230 | let shot_inner_burst_delay = inner_burst_delay * firing_mods.inner_burst_scale; 231 | let shot_burst_size = burst_size + firing_mods.burst_size_add; 232 | 233 | // if total_shots_fired == 0 && firing_settings.is_charge { 234 | // total_time += shot_burst_delay*0.5; 235 | // } 236 | 237 | if firing_settings.one_ammo && burst_size > 1.0 { 238 | total_shots_fired += 1; 239 | shots_this_mag += 1; 240 | total_shots_hit += shot_burst_size as i32; 241 | total_damage += dmg * shot_burst_size; 242 | for i in 0..shot_burst_size as i32 { 243 | time_damage_data.push((total_time + shot_inner_burst_delay * i as f64, dmg)); 244 | } 245 | total_time += inner_burst_delay * (shot_burst_size - 1.0); 246 | } else { 247 | let spec_delay = if shots_this_mag % burst_size as i32 == 0 { 248 | shot_burst_delay 249 | } else { 250 | shot_inner_burst_delay 251 | }; 252 | total_shots_fired += 1; 253 | shots_this_mag += 1; 254 | total_shots_hit += 1; 255 | if inner_burst_delay == 0.0 { 256 | total_damage += dmg * burst_size; 257 | time_damage_data.push((total_time, dmg * burst_size)); 258 | } else { 259 | total_damage += dmg; 260 | time_damage_data.push((total_time, dmg)); 261 | } 262 | if total_shots_fired > 0 { 263 | total_time += spec_delay; 264 | } 265 | } 266 | mag -= 1; 267 | 268 | //REFUNDS////////////////////// 269 | let mut refund_calc_input = weapon.sparse_calc_input(total_shots_fired, total_time); 270 | refund_calc_input.shots_fired_this_mag = shots_this_mag as f64; 271 | let refunds = get_refund_modifier( 272 | perks.clone(), 273 | &refund_calc_input, 274 | false, 275 | &mut pers_calc_data, 276 | ); 277 | let ammo_to_refund = calc_refund(shots_this_mag, refunds); 278 | mag += ammo_to_refund.0; 279 | reserve += ammo_to_refund.1; 280 | /////////////////////////////// 281 | 282 | //COMPLEX CALC PRECURSOR////// 283 | let after_shot_input_data = CalculationInput { 284 | intrinsic_hash: weapon.intrinsic_hash, 285 | curr_firing_data: &firing_settings, 286 | base_crit_mult: crit_mult, 287 | base_mag: base_mag as f64, 288 | curr_mag: mag as f64, 289 | ammo_type: &ammo_type, 290 | weapon_type: &weapon_type, 291 | stats: &stats, 292 | perk_value_map: &weapon.perk_value_map_update(), 293 | enemy_type: &_enemy.type_, 294 | shots_fired_this_mag: shots_this_mag as f64, 295 | total_shots_fired: total_shots_fired as f64, 296 | total_shots_hit: total_shots_hit as f64, 297 | reserves_left: reserve as f64, 298 | time_total: total_time, 299 | time_this_mag: (total_time - start_time), 300 | damage_type: &weapon.damage_type, 301 | handling_data: handling_data, 302 | num_reloads: num_reloads as f64, 303 | has_overshield: false, 304 | }; 305 | /////////////////////////////// 306 | 307 | //EXTRA DMG//////////////////// 308 | let extra_dmg_responses = get_extra_damage( 309 | perks.clone(), 310 | &after_shot_input_data, 311 | false, 312 | &mut pers_calc_data, 313 | ); 314 | let buffs = ExtraDamageBuffInfo { 315 | pl_buff: _pl_dmg_mult, 316 | impact_buff: dmg_mods.impact_dmg_scale, 317 | explosive_buff: dmg_mods.explosive_dmg_scale, 318 | pve_buff: weapon.damage_mods.pve, 319 | crit_buff: crit_mult * dmg_mods.crit_scale, 320 | combatant_buff: weapon.damage_mods.get_mod(&_enemy.type_), 321 | }; 322 | let tmp_out_data = calc_extra_dmg(total_time, extra_dmg_responses, buffs); 323 | total_damage += tmp_out_data.extra_dmg; 324 | total_time += tmp_out_data.extra_time; 325 | total_shots_hit += tmp_out_data.extra_hits; 326 | time_damage_data.extend(tmp_out_data.extra_time_dmg); 327 | /////////////////////////////// 328 | 329 | //RELOAD OVERRIDE////////////// 330 | // if mag == 0 { 331 | // let reload_override_responses = get_reload_overrides( 332 | // perks.clone(), 333 | // &after_shot_input_data, 334 | // false, 335 | // &mut pers_calc_data, 336 | // ); 337 | // if reload_override_responses.len() > 0 { 338 | // let mut final_response = ReloadOverrideResponse::invalid(); 339 | // for response in reload_override_responses { 340 | // if response.priority > final_response.priority { 341 | // final_response = response; 342 | // } 343 | // } 344 | // if final_response.valid { 345 | // total_time += final_response.reload_time; 346 | // if final_response.uses_ammo { 347 | // let ammo_to_add = if final_response.ammo_to_reload > reserve { 348 | // reserve 349 | // } else { 350 | // final_response.ammo_to_reload 351 | // }; 352 | // mag = ammo_to_add; 353 | // reserve -= ammo_to_add; 354 | // } else { 355 | // mag = final_response.ammo_to_reload; 356 | // } 357 | // if final_response.count_as_reload { 358 | // num_reloads += 1; 359 | // start_time = total_time; 360 | // shots_this_mag = 0; 361 | // } 362 | // } 363 | // } 364 | // } 365 | if mag != 0 { 366 | if weapon.weapon_type == WeaponType::FUSIONRIFLE { 367 | total_time += shot_burst_delay * 0.45 368 | } else if weapon.weapon_type == WeaponType::LINEARFUSIONRIFLE { 369 | total_time += shot_burst_delay * 0.95 370 | } 371 | } 372 | /////////////////////////////// 373 | if weapon.ammo_type == AmmoType::PRIMARY { 374 | if total_shots_fired > maximum_shots { 375 | reserve = 0; 376 | break; 377 | } 378 | } else { 379 | if total_shots_fired > base_mag * 8 + 20 { 380 | reserve = 0; 381 | break; 382 | } 383 | } 384 | if reserve <= 0 { 385 | break; 386 | } 387 | } 388 | 389 | reserve -= base_mag; 390 | dps_per_mag.push(total_damage / total_time); 391 | 392 | //RELOAD/////////////////////// 393 | let reload_input_data = CalculationInput { 394 | intrinsic_hash: weapon.intrinsic_hash, 395 | curr_firing_data: &firing_settings, 396 | base_crit_mult: crit_mult, 397 | base_mag: base_mag as f64, 398 | curr_mag: mag as f64, 399 | ammo_type: &ammo_type, 400 | weapon_type: &weapon_type, 401 | stats: &stats, 402 | perk_value_map: &weapon.perk_value_map_update(), 403 | enemy_type: &_enemy.type_, 404 | shots_fired_this_mag: shots_this_mag as f64, 405 | total_shots_fired: total_shots_fired as f64, 406 | total_shots_hit: total_shots_hit as f64, 407 | reserves_left: reserve as f64, 408 | time_total: total_time, 409 | time_this_mag: (total_time - start_time), 410 | damage_type: &weapon.damage_type, 411 | handling_data, 412 | num_reloads: num_reloads as f64, 413 | has_overshield: false, 414 | }; 415 | let reload_responses = 416 | weapon.calc_reload_time(Some(reload_input_data), Some(&mut pers_calc_data), false); 417 | total_time += reload_responses.reload_time; 418 | /////////////////////////////// 419 | num_reloads += 1; 420 | } 421 | //sort time_damage_data by time 422 | time_damage_data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); 423 | DpsResponse { 424 | dps_per_mag, 425 | time_damage_data, 426 | total_damage, 427 | total_time, 428 | total_shots: total_shots_fired, 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /src/weapons/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dps_calc; 2 | pub mod reserve_calc; 3 | pub mod stat_calc; 4 | pub mod ttk_calc; 5 | pub mod weapon_constructor; 6 | 7 | use std::collections::HashMap; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::d2_enums::{AmmoType, DamageType, StatHashes, WeaponType}; 12 | use crate::enemies::Enemy; 13 | use crate::perks::{ 14 | get_magazine_modifier, get_reserve_modifier, get_stat_bumps, lib::CalculationInput, Perk, 15 | }; 16 | 17 | use crate::types::rs_types::{ 18 | AmmoFormula, DamageMods, DpsResponse, FiringData, HandlingFormula, RangeFormula, ReloadFormula, 19 | }; 20 | 21 | use self::dps_calc::complex_dps_calc; 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct PsuedoWeapon {} 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | pub struct Stat { 28 | pub base_value: i32, 29 | pub part_value: i32, 30 | pub perk_value: i32, 31 | } 32 | impl Stat { 33 | pub fn new() -> Stat { 34 | Stat { 35 | base_value: 0, 36 | part_value: 0, 37 | perk_value: 0, 38 | } 39 | } 40 | pub fn val(&self) -> i32 { 41 | (self.base_value + self.part_value).clamp(0, 100) 42 | } 43 | pub fn perk_val(&self) -> i32 { 44 | (self.base_value + self.part_value + self.perk_value).clamp(0, 100) 45 | } 46 | } 47 | impl From for Stat { 48 | fn from(_val: i32) -> Self { 49 | Stat { 50 | base_value: _val, 51 | part_value: 0, 52 | perk_value: 0, 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize)] 58 | pub struct Weapon { 59 | pub hash: u32, 60 | pub intrinsic_hash: u32, 61 | 62 | pub perks: HashMap, 63 | pub stats: HashMap, 64 | #[serde(skip)] 65 | pub perk_value_map: HashMap, 66 | 67 | pub damage_mods: DamageMods, 68 | pub firing_data: FiringData, 69 | pub range_formula: RangeFormula, 70 | pub ammo_formula: AmmoFormula, 71 | pub handling_formula: HandlingFormula, 72 | pub reload_formula: ReloadFormula, 73 | 74 | pub weapon_type: WeaponType, 75 | pub damage_type: DamageType, 76 | pub ammo_type: AmmoType, 77 | } 78 | impl Weapon { 79 | pub fn add_perk(&mut self, _perk: Perk) { 80 | self.perks.insert(_perk.hash, _perk); 81 | self.update_stats(); 82 | } 83 | pub fn remove_perk(&mut self, _perk_hash: u32) { 84 | self.perks.remove(&_perk_hash); 85 | self.update_stats(); 86 | } 87 | pub fn list_perk_ids(&self) -> Vec { 88 | self.perks.keys().cloned().collect() 89 | } 90 | pub fn list_perks(&self) -> Vec { 91 | let mut perk_list: Vec = Vec::new(); 92 | for (_key, perk) in &self.perks { 93 | perk_list.push(perk.clone()); 94 | } 95 | perk_list 96 | } 97 | pub fn perk_value_map_update(&self) -> HashMap { 98 | let mut perk_map: HashMap = HashMap::new(); 99 | for (_key, perk) in &self.perks { 100 | perk_map.insert(perk.hash, perk.value); 101 | } 102 | perk_map 103 | } 104 | pub fn change_perk_val(&mut self, _perk_hash: u32, _val: u32) { 105 | let perk_opt = self.perks.get_mut(&_perk_hash); 106 | if perk_opt.is_some() { 107 | perk_opt.unwrap().value = _val; 108 | } 109 | self.update_stats(); 110 | } 111 | pub fn get_stats(&mut self) -> HashMap { 112 | self.update_stats(); 113 | self.stats.clone() 114 | } 115 | pub fn set_stats(&mut self, _stats: HashMap) { 116 | self.stats = _stats; 117 | self.update_stats() 118 | } 119 | pub fn reset(&mut self) { 120 | self.perks = HashMap::new(); 121 | self.stats = HashMap::new(); 122 | self.hash = 0; 123 | self.damage_mods = DamageMods::default(); 124 | self.firing_data = FiringData::default(); 125 | self.range_formula = RangeFormula::default(); 126 | self.ammo_formula = AmmoFormula::default(); 127 | self.handling_formula = HandlingFormula::default(); 128 | self.reload_formula = ReloadFormula::default(); 129 | } 130 | 131 | pub fn static_calc_input(&self) -> CalculationInput { 132 | CalculationInput::construct_static( 133 | self.intrinsic_hash, 134 | &self.firing_data, 135 | &self.stats, 136 | &self.perk_value_map, 137 | &self.weapon_type, 138 | &self.ammo_type, 139 | &self.damage_type, 140 | self.firing_data.crit_mult, 141 | ) 142 | } 143 | 144 | pub fn sparse_calc_input(&self, _total_shots_fired: i32, _total_time: f64) -> CalculationInput { 145 | CalculationInput::construct_pve_sparse( 146 | self.intrinsic_hash, 147 | &self.firing_data, 148 | &self.stats, 149 | &self.perk_value_map, 150 | &self.weapon_type, 151 | &self.ammo_type, 152 | &self.damage_type, 153 | self.firing_data.damage, 154 | self.firing_data.crit_mult, 155 | self.calc_ammo_sizes(None, None, false).mag_size, 156 | _total_shots_fired, 157 | _total_time, 158 | ) 159 | } 160 | pub fn pvp_calc_input( 161 | &self, 162 | _total_shots_fired: f64, 163 | _total_shots_hit: f64, 164 | _total_time: f64, 165 | _overshield: bool, 166 | ) -> CalculationInput { 167 | let base_mag = self.calc_ammo_sizes(None, None, true).mag_size as f64; 168 | let mut tmp = CalculationInput::construct_pvp( 169 | self.intrinsic_hash, 170 | &self.firing_data, 171 | &self.stats, 172 | &self.perk_value_map, 173 | &self.weapon_type, 174 | &self.ammo_type, 175 | self.firing_data.damage, 176 | self.firing_data.crit_mult, 177 | base_mag, 178 | _overshield, 179 | self.calc_handling_times(None, None, true), 180 | ); 181 | tmp.time_this_mag = _total_time; 182 | tmp.time_total = _total_time; 183 | tmp.shots_fired_this_mag = _total_shots_fired; 184 | tmp.total_shots_fired = _total_shots_fired; 185 | tmp.total_shots_hit = _total_shots_hit; 186 | tmp 187 | } 188 | pub fn update_stats(&mut self) { 189 | self.perk_value_map = self.perk_value_map_update(); 190 | let input = CalculationInput::construct_static( 191 | self.intrinsic_hash, 192 | &self.firing_data, 193 | &self.stats, 194 | &self.perk_value_map, 195 | &self.weapon_type, 196 | &self.ammo_type, 197 | &self.damage_type, 198 | self.firing_data.crit_mult, 199 | ); 200 | let inter_var = get_stat_bumps(self.list_perks(), input, false, &mut HashMap::new()); 201 | let dynamic_stats = &inter_var[0]; 202 | let static_stats = &inter_var[1]; 203 | for (key, stat) in &mut self.stats { 204 | let a = static_stats.get(key); 205 | let b = dynamic_stats.get(key); 206 | if a.is_some() { 207 | stat.part_value = a.unwrap().clone(); 208 | } 209 | if b.is_some() { 210 | stat.perk_value = b.unwrap().clone(); 211 | } 212 | } 213 | } 214 | pub fn calc_dps(&self, _enemy: Enemy, _pl_dmg_mult: f64) -> DpsResponse { 215 | complex_dps_calc(self.clone(), _enemy, _pl_dmg_mult) 216 | } 217 | } 218 | impl Default for Weapon { 219 | fn default() -> Weapon { 220 | Weapon { 221 | intrinsic_hash: 0, 222 | hash: 0, 223 | 224 | perks: HashMap::new(), 225 | stats: HashMap::new(), 226 | perk_value_map: HashMap::new(), 227 | 228 | damage_mods: DamageMods::default(), 229 | firing_data: FiringData::default(), 230 | 231 | range_formula: RangeFormula::default(), 232 | ammo_formula: AmmoFormula::default(), 233 | handling_formula: HandlingFormula::default(), 234 | reload_formula: ReloadFormula::default(), 235 | 236 | weapon_type: WeaponType::UNKNOWN, 237 | damage_type: DamageType::UNKNOWN, 238 | ammo_type: AmmoType::UNKNOWN, 239 | } 240 | } 241 | } 242 | 243 | // //making this separate for organization 244 | // impl Weapon { 245 | // pub fn get_damage(&self) 246 | // } 247 | -------------------------------------------------------------------------------- /src/weapons/reserve_calc.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 2 | enum ReserveIDs { 3 | Primary, 4 | LeviathansBreath, 5 | Fusions, 6 | SpecialGrenadeLaunchers, 7 | SmallGrenadeLaunchers, 8 | LargeGrenadeLaunchers, 9 | ErianasVow, 10 | LinearFusions, 11 | SmallMachineGuns, 12 | LargeMachineGuns, 13 | Xenophage, 14 | Overture, 15 | RocketLaunchers, 16 | Shotguns, 17 | LordOfWolves, 18 | ForeRunner, 19 | SniperRifles, 20 | Glaive, 21 | TraceRifles, 22 | } 23 | impl From for ReserveIDs { 24 | fn from(id: u32) -> Self { 25 | match id { 26 | 0 => ReserveIDs::Primary, 27 | 1699724249 => ReserveIDs::LeviathansBreath, 28 | 111 => ReserveIDs::Fusions, 29 | 231 => ReserveIDs::LargeGrenadeLaunchers, 30 | 232 => ReserveIDs::SpecialGrenadeLaunchers, 31 | 233 => ReserveIDs::SmallGrenadeLaunchers, 32 | 3174300811 => ReserveIDs::ErianasVow, 33 | 221 => ReserveIDs::LinearFusions, 34 | 81 => ReserveIDs::SmallMachineGuns, 35 | 82 => ReserveIDs::LargeMachineGuns, 36 | 2261491232 => ReserveIDs::Xenophage, 37 | 2940035732 => ReserveIDs::Overture, 38 | 101 => ReserveIDs::RocketLaunchers, 39 | 71 => ReserveIDs::Shotguns, 40 | 481338655 => ReserveIDs::LordOfWolves, 41 | 2984682260 => ReserveIDs::ForeRunner, 42 | 121 => ReserveIDs::SniperRifles, 43 | 331 => ReserveIDs::Glaive, 44 | 251 => ReserveIDs::TraceRifles, 45 | _ => ReserveIDs::Primary, 46 | } 47 | } 48 | } 49 | 50 | pub fn calc_reserves(_mag_size: f64, _mag_stat: i32, _inv_stat: i32, _id: u32, _scale: f64) -> i32 { 51 | let id = ReserveIDs::from(_id); 52 | let raw_size: f64 = match id { 53 | ReserveIDs::Primary => 9999.0, 54 | ReserveIDs::SmallMachineGuns => small_machinegun(_mag_size, _mag_stat, _inv_stat), 55 | ReserveIDs::TraceRifles => trace_rifle(_mag_size, _mag_stat, _inv_stat), 56 | ReserveIDs::Glaive => glaives(_mag_size, _mag_stat, _inv_stat), 57 | ReserveIDs::SniperRifles => sniper_rifles(_mag_size, _mag_stat, _inv_stat), 58 | ReserveIDs::Shotguns => shotguns(_mag_size, _mag_stat, _inv_stat), 59 | ReserveIDs::Xenophage => xenophage(_mag_size, _mag_stat, _inv_stat), 60 | ReserveIDs::Overture => overture(_mag_size, _mag_stat, _inv_stat), 61 | ReserveIDs::ForeRunner => forerunner(_mag_size, _mag_stat, _inv_stat), 62 | ReserveIDs::ErianasVow => eriana_vow(_mag_size, _mag_stat, _inv_stat), 63 | ReserveIDs::RocketLaunchers => rockets(_mag_size, _mag_stat, _inv_stat), 64 | 65 | //placeholders 66 | ReserveIDs::LeviathansBreath => 8.0, 67 | ReserveIDs::Fusions => 21.0, 68 | ReserveIDs::SmallGrenadeLaunchers => 18.0, 69 | ReserveIDs::LargeGrenadeLaunchers => 20.0, 70 | ReserveIDs::SpecialGrenadeLaunchers => 21.0, 71 | ReserveIDs::LinearFusions => 21.0, 72 | ReserveIDs::LargeMachineGuns => 400.0, 73 | ReserveIDs::LordOfWolves => 120.0, 74 | }; 75 | let size = raw_size * _scale; 76 | size.ceil() as i32 77 | } 78 | 79 | fn small_machinegun(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 80 | let round_amount = _mag_size.ceil() - _mag_size; 81 | let offset = (-0.875 + round_amount * 2.0) * (2.0 - ((100.0 - _mag_stat as f64) / 100.0)); 82 | let reserves = 83 | 225.0 + offset + _inv_stat as f64 * ((225.0 + offset) * 2.0 - (225.0 + offset)) / 100.0; 84 | reserves 85 | } 86 | 87 | fn trace_rifle(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 88 | let mult = _inv_stat as f64 * 0.025 + 3.5; 89 | _mag_size * mult 90 | } 91 | 92 | fn glaives(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 93 | let vpp = if _mag_stat >= 100 { 0.1681 } else { 0.1792 }; 94 | let offset = if _mag_stat >= 100 { 13.44 } else { 14.44 }; 95 | vpp * _inv_stat as f64 + offset 96 | } 97 | 98 | fn sniper_rifles(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 99 | let vpp = if _mag_stat >= 100 { 0.14 } else { 0.12 }; 100 | let offset = if _mag_stat >= 100 { 14.0 } else { 12.0 }; 101 | vpp * _inv_stat as f64 + offset 102 | } 103 | 104 | fn shotguns(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 105 | let real_mag_size = _mag_size.ceil() as i32; 106 | let base_offset = match real_mag_size { 107 | 8 => 0.0, 108 | 7 => 4.0, 109 | 6 => 9.0, 110 | 5 => 17.0, 111 | 4 => 30.0, 112 | _ => 0.0, 113 | }; 114 | let base = (base_offset / 15.0) + 12.0; 115 | let mult_vpp = (2.0 / 3.0) / 100.0; 116 | base * (1.0 + mult_vpp * _inv_stat as f64) 117 | } 118 | 119 | fn forerunner(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 120 | _inv_stat as f64 * 0.325 + 53.45 121 | } 122 | 123 | fn overture(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 124 | let inv_stat = _inv_stat as f64; 125 | 0.005 * (inv_stat * inv_stat) + inv_stat * -0.4 + 67.375 126 | } 127 | 128 | fn xenophage(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 129 | let inv_stat = _inv_stat as f64; 130 | 0.01 * (inv_stat * inv_stat) + inv_stat * 0.56 + 25.91 131 | } 132 | 133 | fn eriana_vow(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 134 | let inv_stat = _inv_stat as f64; 135 | -0.00126 * (inv_stat * inv_stat) + inv_stat * 0.225 + 29.5 136 | } 137 | 138 | fn rockets(_mag_size: f64, _mag_stat: i32, _inv_stat: i32) -> f64 { 139 | _inv_stat as f64 * 0.05 + 4.5 140 | } 141 | -------------------------------------------------------------------------------- /src/weapons/ttk_calc.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::{ 6 | d2_enums::WeaponType, 7 | logging::extern_log, 8 | perks::{get_dmg_modifier, get_firing_modifier, lib::CalculationInput}, 9 | }; 10 | 11 | use super::{FiringData, Weapon}; 12 | 13 | //just to make code cleaner for now 14 | fn ceil(x: f64) -> f64 { 15 | x.ceil() 16 | } 17 | 18 | const RESILIENCE_VALUES: [f64; 11] = [ 19 | 185.001, 186.001, 187.001, 188.001, 189.001, 190.001, 192.001, 194.001, 196.001, 198.01, 200.00, 20 | ]; 21 | 22 | #[derive(Debug, Clone, Serialize)] 23 | pub struct OptimalKillData { 24 | pub headshots: i32, 25 | pub bodyshots: i32, 26 | #[serde(rename = "timeTaken")] 27 | pub time_taken: f64, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize)] 31 | pub struct BodyKillData { 32 | pub bodyshots: i32, 33 | #[serde(rename = "timeTaken")] 34 | pub time_taken: f64, 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize)] 38 | pub struct ResillienceSummary { 39 | pub value: i32, 40 | #[serde(rename = "bodyTtk")] 41 | pub body_ttk: BodyKillData, 42 | #[serde(rename = "optimalTtk")] 43 | pub optimal_ttk: OptimalKillData, 44 | } 45 | 46 | pub fn calc_ttk(_weapon: &Weapon, _overshield: f64) -> Vec { 47 | let mut ttk_data: Vec = Vec::new(); 48 | let mut persistent_data: HashMap = HashMap::new(); 49 | 50 | let tmp_dmg_prof = _weapon.get_damage_profile(); 51 | let impact_dmg = tmp_dmg_prof.0; 52 | let explosion_dmg = tmp_dmg_prof.1; 53 | let mut crit_mult = tmp_dmg_prof.2; 54 | // let damage_delay = tmp_dmg_prof.3; 55 | if _weapon.weapon_type == WeaponType::SHOTGUN && _weapon.firing_data.burst_size == 12 { 56 | crit_mult = 1.0; // shawty has no crits 57 | } 58 | 59 | for i in 0..RESILIENCE_VALUES.len() { 60 | let health = RESILIENCE_VALUES[i] + _overshield; 61 | 62 | let mut opt_damage_dealt = 0.0_f64; 63 | let mut opt_time_taken = 0.0_f64; 64 | let mut opt_bullets_fired = 0.0_f64; 65 | let mut opt_bullets_hit = 0.0_f64; 66 | let opt_bodyshots = 0; 67 | let mut opt_headshots = 0; 68 | let mut opt_bullet_timeline: Vec<(f64, f64)> = Vec::new(); 69 | 70 | //Optimal ttk 71 | while opt_bullets_hit < 50.0 { 72 | //PERK CALCULATIONS//////////// 73 | 74 | persistent_data.insert("health%".to_string(), (health - opt_damage_dealt) / 70.0); 75 | persistent_data.insert("empowering".to_string(), 1.0); 76 | persistent_data.insert("surge".to_string(), 1.0); 77 | persistent_data.insert("debuff".to_string(), 1.0); 78 | let calc_input = _weapon.pvp_calc_input( 79 | opt_bullets_fired, 80 | opt_bullets_hit, 81 | opt_time_taken, 82 | (_overshield - opt_damage_dealt) > 0.0, 83 | ); 84 | let dmg_mods = get_dmg_modifier( 85 | _weapon.list_perks().clone(), 86 | &calc_input, 87 | true, 88 | &mut persistent_data, 89 | ); 90 | let firing_mods = get_firing_modifier( 91 | _weapon.list_perks().clone(), 92 | &calc_input, 93 | true, 94 | &mut persistent_data, 95 | ); 96 | /////////////////////////////// 97 | 98 | let body_damage = (impact_dmg * dmg_mods.impact_dmg_scale) 99 | + (explosion_dmg * dmg_mods.explosive_dmg_scale); 100 | let critical_multiplier = crit_mult * dmg_mods.crit_scale; 101 | let head_diff = ((impact_dmg * dmg_mods.impact_dmg_scale) * critical_multiplier) 102 | - (impact_dmg * dmg_mods.impact_dmg_scale); 103 | 104 | let shot_burst_delay = (_weapon.firing_data.burst_delay + firing_mods.burst_delay_add) 105 | * firing_mods.burst_delay_scale; 106 | let shot_inner_burst_delay = 107 | _weapon.firing_data.inner_burst_delay * firing_mods.inner_burst_scale; 108 | let shot_burst_size = 109 | _weapon.firing_data.burst_size as f64 + firing_mods.burst_size_add; 110 | 111 | let mut shot_delay = if opt_bullets_hit % shot_burst_size > 0.0 && opt_bullets_hit > 0.0 112 | { 113 | shot_inner_burst_delay 114 | } else if opt_bullets_hit == 0.0 { 115 | 0.0 116 | } else { 117 | shot_burst_delay 118 | }; 119 | 120 | if _weapon.hash == 4289226715 { // vex mythoclast 121 | } else if _weapon.weapon_type == WeaponType::LINEARFUSIONRIFLE { 122 | shot_delay *= 1.95; 123 | } else if _weapon.weapon_type == WeaponType::FUSIONRIFLE { 124 | shot_delay *= 1.45; 125 | } 126 | 127 | let ammo_fired; 128 | if _weapon.firing_data.one_ammo { 129 | ammo_fired = opt_bullets_fired/shot_burst_size; 130 | } else { 131 | ammo_fired = opt_bullets_fired; 132 | } 133 | if ammo_fired 134 | >= _weapon 135 | .calc_ammo_sizes(Some(calc_input.clone()), Some(&mut persistent_data), true) 136 | .mag_size 137 | .into() 138 | { 139 | shot_delay += _weapon 140 | .calc_reload_time(Some(calc_input.clone()), Some(&mut persistent_data), true) 141 | .reload_time; 142 | } 143 | 144 | if opt_bullets_hit % shot_burst_size == 0.0 { 145 | opt_bullets_fired += 1.0; 146 | opt_bullets_hit += 1.0; 147 | } else { 148 | opt_bullets_hit += 1.0; 149 | }; 150 | 151 | opt_time_taken += shot_delay; 152 | 153 | opt_bullet_timeline.push((body_damage, head_diff)); 154 | 155 | // assume all headshots for first pass 156 | if (opt_damage_dealt + body_damage + head_diff) >= health { 157 | opt_headshots += 1; 158 | opt_damage_dealt += body_damage + head_diff; 159 | break; 160 | } else { 161 | opt_headshots += 1; 162 | opt_damage_dealt += body_damage + head_diff; 163 | } 164 | } 165 | 166 | let mut opt_timeline_damage_dealt = opt_damage_dealt; 167 | let mut opt_timeline_bodyshots = opt_bodyshots; 168 | let mut opt_timeline_headshots = opt_headshots; 169 | 170 | // walk back and turn headshots to bodyshots 171 | for timeline_snapshot in opt_bullet_timeline.iter().rev() { 172 | let _body_damage = timeline_snapshot.0; 173 | let headshot_diff = timeline_snapshot.1; 174 | 175 | if opt_timeline_damage_dealt - headshot_diff >= health { 176 | opt_timeline_bodyshots += 1; 177 | opt_timeline_headshots -= 1; 178 | opt_timeline_damage_dealt -= headshot_diff; 179 | } else { 180 | break; 181 | } 182 | } 183 | 184 | let optimal_ttk = OptimalKillData { 185 | headshots: opt_timeline_headshots, 186 | bodyshots: opt_timeline_bodyshots, 187 | time_taken: opt_time_taken 188 | }; 189 | 190 | let mut bdy_bullets_hit = 0.0; 191 | let mut bdy_bullets_fired = 0.0; 192 | let mut bdy_time_taken = 0.0; 193 | let mut bdy_damage_dealt = 0.0; 194 | while bdy_bullets_hit < 50.0 { 195 | //PERK CALCULATIONS//////////// 196 | persistent_data.insert("health%".to_string(), (health - bdy_damage_dealt) / 70.0); 197 | persistent_data.insert("empowering".to_string(), 1.0); 198 | persistent_data.insert("surge".to_string(), 1.0); 199 | persistent_data.insert("debuff".to_string(), 1.0); 200 | let calc_input = _weapon.pvp_calc_input( 201 | bdy_bullets_fired, 202 | bdy_bullets_hit, 203 | bdy_time_taken, 204 | (_overshield - bdy_damage_dealt) > 0.0, 205 | ); 206 | let dmg_mods = get_dmg_modifier( 207 | _weapon.list_perks().clone(), 208 | &calc_input, 209 | true, 210 | &mut persistent_data, 211 | ); 212 | let firing_mods = get_firing_modifier( 213 | _weapon.list_perks().clone(), 214 | &calc_input, 215 | true, 216 | &mut persistent_data, 217 | ); 218 | /////////////////////////////// 219 | 220 | let tmp_dmg_prof = _weapon.get_damage_profile(); 221 | let impact_dmg = tmp_dmg_prof.0; 222 | let explosion_dmg = tmp_dmg_prof.1; 223 | 224 | let body_damage = (impact_dmg * dmg_mods.impact_dmg_scale) 225 | + (explosion_dmg * dmg_mods.explosive_dmg_scale); 226 | 227 | let shot_burst_delay = (_weapon.firing_data.burst_delay + firing_mods.burst_delay_add) 228 | * firing_mods.burst_delay_scale; 229 | let shot_inner_burst_delay = 230 | _weapon.firing_data.inner_burst_delay * firing_mods.inner_burst_scale; 231 | let shot_burst_size = 232 | _weapon.firing_data.burst_size as f64 + firing_mods.burst_size_add; 233 | 234 | let mut shot_delay = if bdy_bullets_hit % shot_burst_size > 0.0 && bdy_bullets_hit > 0.0 235 | { 236 | shot_inner_burst_delay 237 | } else if bdy_bullets_hit == 0.0 { 238 | 0.0 239 | } else { 240 | shot_burst_delay 241 | }; 242 | 243 | if _weapon.hash == 4289226715 { //vex mythoclast 244 | } else if _weapon.weapon_type == WeaponType::LINEARFUSIONRIFLE { 245 | shot_delay *= 1.95; 246 | } else if _weapon.weapon_type == WeaponType::FUSIONRIFLE { 247 | shot_delay *= 1.45; 248 | } 249 | 250 | let ammo_fired; 251 | if _weapon.firing_data.one_ammo { 252 | ammo_fired = opt_bullets_fired/shot_burst_size; 253 | } else { 254 | ammo_fired = opt_bullets_fired; 255 | } 256 | if ammo_fired 257 | >= _weapon 258 | .calc_ammo_sizes(Some(calc_input.clone()), Some(&mut persistent_data), true) 259 | .mag_size 260 | .into() 261 | { 262 | shot_delay += _weapon 263 | .calc_reload_time(Some(calc_input.clone()), Some(&mut persistent_data), true) 264 | .reload_time; 265 | } 266 | 267 | bdy_time_taken += shot_delay; 268 | if bdy_bullets_hit % shot_burst_size == 0.0 { 269 | bdy_bullets_fired += 1.0; 270 | bdy_bullets_hit += 1.0; 271 | } else { 272 | bdy_bullets_hit += 1.0; 273 | }; 274 | 275 | if (bdy_damage_dealt + body_damage) >= health { 276 | break; 277 | } else { 278 | bdy_damage_dealt += body_damage; 279 | } 280 | } 281 | let body_ttk = BodyKillData { 282 | time_taken: bdy_time_taken, 283 | bodyshots: bdy_bullets_hit as i32, 284 | }; 285 | ttk_data.push(ResillienceSummary { 286 | value: i as i32, 287 | body_ttk, 288 | optimal_ttk, 289 | }); 290 | } 291 | ttk_data 292 | } 293 | 294 | impl Weapon { 295 | pub fn calc_ttk(&self, _overshield: f64) -> Vec { 296 | calc_ttk(self, _overshield) 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/weapons/weapon_constructor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | d2_enums::{AmmoType, DamageType, WeaponType}, 5 | database, 6 | perks::{enhanced_check, Perk}, 7 | types::rs_types::{ 8 | AmmoFormula, DamageMods, DataPointers, HandlingFormula, RangeFormula, ReloadFormula, 9 | StatQuadraticFormula, WeaponPath, 10 | }, 11 | }; 12 | 13 | use super::{FiringData, Weapon}; 14 | 15 | fn get_data_pointers(_weapon_type_id: u8, _intrinsic_hash: u32) -> Result { 16 | let pointer_map: HashMap = HashMap::from(database::DATA_POINTERS); 17 | let pointer_result = pointer_map.get(&WeaponPath(_weapon_type_id as u32, _intrinsic_hash)); 18 | if pointer_result.is_none() { 19 | return Err(format!( 20 | "No data pointers found for intrinsic hash: {}", 21 | _intrinsic_hash 22 | )); 23 | } 24 | let pointer = pointer_result.unwrap(); 25 | Ok(pointer.clone()) 26 | } 27 | 28 | impl Weapon { 29 | pub fn generate_weapon( 30 | _hash: u32, 31 | _weapon_type_id: u8, 32 | _intrinsic_hash: u32, 33 | _ammo_type_id: u32, 34 | _damage_type_id: u32, 35 | ) -> Result { 36 | let data_pointer_result = get_data_pointers(_weapon_type_id, _intrinsic_hash); 37 | if data_pointer_result.is_err() { 38 | return Err(data_pointer_result.unwrap_err()); 39 | } 40 | let data_pointer = data_pointer_result.unwrap(); 41 | 42 | let range_formula: RangeFormula = database::RANGE_DATA[data_pointer.r].clone(); 43 | 44 | let handling_formula: HandlingFormula = database::HANDLING_DATA[data_pointer.h].clone(); 45 | 46 | let reload_formula: ReloadFormula = database::RELOAD_DATA[data_pointer.rl].clone(); 47 | 48 | let damage_mods: DamageMods = database::SCALAR_DATA[data_pointer.s].clone(); 49 | 50 | let firing_data: FiringData = database::FIRING_DATA[data_pointer.f].clone(); 51 | 52 | let ammo_formula: AmmoFormula = database::AMMO_DATA[data_pointer.a].clone(); 53 | 54 | let weapon_type = WeaponType::from(_weapon_type_id as u32); 55 | let ammo_type = AmmoType::from(_ammo_type_id); 56 | let damage_type = DamageType::from(_damage_type_id); 57 | let intrinsic_alias = enhanced_check(_intrinsic_hash).0; 58 | Ok(Weapon { 59 | intrinsic_hash: intrinsic_alias, 60 | hash: _hash, 61 | perks: HashMap::from([ 62 | ( 63 | intrinsic_alias, 64 | Perk { 65 | stat_buffs: HashMap::new(), 66 | enhanced: false, 67 | value: 0, 68 | hash: intrinsic_alias, 69 | raw_hash: _intrinsic_hash, 70 | }, 71 | ), 72 | ( 73 | 0, 74 | Perk { 75 | stat_buffs: HashMap::new(), 76 | enhanced: false, 77 | value: 0, 78 | hash: 0, 79 | raw_hash: 0, 80 | }, 81 | ), 82 | ]), 83 | stats: HashMap::new(), 84 | perk_value_map: HashMap::from([(intrinsic_alias, 0), (0, 0)]), 85 | damage_mods, 86 | ammo_formula, 87 | firing_data, 88 | handling_formula, 89 | reload_formula, 90 | range_formula, 91 | ammo_type, 92 | damage_type, 93 | weapon_type, 94 | }) 95 | } 96 | } 97 | --------------------------------------------------------------------------------