├── .prettierignore ├── .eslintignore ├── test ├── __mocks__ │ ├── assetMock.js │ └── cssMock.js ├── setupFiles.ts └── simulations │ └── ZukLineOfSight.test.ts ├── src ├── assets │ ├── images │ │ └── webappicon.png │ └── fonts │ │ ├── RuneScape-UF.woff │ │ ├── RuneScape-UF.woff2 │ │ ├── RuneScape-Plain-11.woff │ │ ├── RuneScape-Plain-12.woff │ │ ├── RuneScape-Plain-11.woff2 │ │ └── RuneScape-Plain-12.woff2 ├── content │ └── inferno │ │ ├── assets │ │ ├── images │ │ │ ├── bat.png │ │ │ ├── blob.png │ │ │ ├── map.png │ │ │ ├── nib.png │ │ │ ├── mager.png │ │ │ ├── meleer.png │ │ │ ├── ranger.png │ │ │ ├── TzKal-Zuk.png │ │ │ ├── Yt-HurKot.png │ │ │ ├── Jal-MejJak.png │ │ │ ├── zuk_attack.png │ │ │ ├── Jal-AkRek-Ket.png │ │ │ ├── Jal-AkRek-Mej.png │ │ │ ├── Jal-AkRek-Xil.png │ │ │ └── jad │ │ │ │ ├── jad_mage_1.png │ │ │ │ ├── jad_mage_2.png │ │ │ │ ├── jad_mage_3.png │ │ │ │ ├── jad_mage_4.png │ │ │ │ ├── jad_mage_5.png │ │ │ │ ├── jad_mage_6.png │ │ │ │ ├── jad_mage_7.png │ │ │ │ ├── jad_mage_8.png │ │ │ │ ├── jad_mage_9.png │ │ │ │ ├── jad_mage_10.png │ │ │ │ ├── jad_mage_11.png │ │ │ │ ├── jad_mage_12.png │ │ │ │ ├── jad_mage_13.png │ │ │ │ ├── jad_mage_14.png │ │ │ │ ├── jad_mage_15.png │ │ │ │ ├── jad_mage_16.png │ │ │ │ ├── jad_mage_17.png │ │ │ │ ├── jad_mage_18.png │ │ │ │ ├── jad_mage_19.png │ │ │ │ ├── jad_mage_20.png │ │ │ │ ├── jad_mage_21.png │ │ │ │ ├── jad_mage_22.png │ │ │ │ ├── jad_mage_23.png │ │ │ │ ├── jad_mage_24.png │ │ │ │ ├── jad_mage_25.png │ │ │ │ ├── jad_mage_26.png │ │ │ │ ├── jad_mage_27.png │ │ │ │ ├── jad_mage_28.png │ │ │ │ ├── jad_mage_29.png │ │ │ │ ├── jad_mage_30.png │ │ │ │ ├── jad_mage_31.png │ │ │ │ ├── jad_mage_32.png │ │ │ │ ├── jad_range_1.png │ │ │ │ ├── jad_range_10.png │ │ │ │ ├── jad_range_11.png │ │ │ │ ├── jad_range_12.png │ │ │ │ ├── jad_range_13.png │ │ │ │ ├── jad_range_2.png │ │ │ │ ├── jad_range_3.png │ │ │ │ ├── jad_range_4.png │ │ │ │ ├── jad_range_5.png │ │ │ │ ├── jad_range_6.png │ │ │ │ ├── jad_range_7.png │ │ │ │ ├── jad_range_8.png │ │ │ │ └── jad_range_9.png │ │ └── sounds │ │ │ ├── bat.ogg │ │ │ ├── blob.ogg │ │ │ ├── meleer.ogg │ │ │ ├── firebreath_159.ogg │ │ │ ├── firewave_hit_163.ogg │ │ │ ├── mage_ranger_598.ogg │ │ │ ├── fireblast_cast_and_fire_155.ogg │ │ │ └── firewave_cast_and_fire_162.ogg │ │ ├── js │ │ ├── Wall.ts │ │ ├── InfernoMobDeathStore.ts │ │ ├── InfernoSettings.ts │ │ ├── InfernoScene.ts │ │ ├── mobs │ │ │ ├── JalAkRekMej.ts │ │ │ ├── JalAkRekXil.ts │ │ │ ├── JalAkRekKet.ts │ │ │ ├── JalMejRah.ts │ │ │ ├── YtHurKot.ts │ │ │ ├── JalXil.ts │ │ │ ├── JalTokJadAnim.ts │ │ │ ├── JalNib.ts │ │ │ ├── JalImKot.ts │ │ │ ├── JalAk.ts │ │ │ ├── JalMejJak.ts │ │ │ ├── JalZek.ts │ │ │ ├── JalTokJad.ts │ │ │ └── TzKalZuk.ts │ │ ├── InfernoHealerSpark.ts │ │ ├── JalZekModelWithLight.ts │ │ ├── ZukShield.ts │ │ ├── InfernoPillar.ts │ │ ├── InfernoWaves.ts │ │ ├── InfernoLoadout.ts │ │ └── InfernoRegion.ts │ │ └── sidebar.html ├── @types │ └── chebyshev │ │ └── index.d.ts ├── manifest.json └── index.ts ├── .prettierrc ├── tsconfig.json ├── .vscode ├── tasks.json └── launch.json ├── .eslintrc.js ├── index.d.ts ├── jest.config.js ├── package.json ├── README.md ├── .gitignore ├── webpack.config.js └── assets.md /.prettierignore: -------------------------------------------------------------------------------- 1 | src/index.html -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /test/__mocks__/assetMock.js: -------------------------------------------------------------------------------- 1 | module.exports = "image"; 2 | -------------------------------------------------------------------------------- /test/__mocks__/cssMock.js: -------------------------------------------------------------------------------- 1 | module.exports = "image"; 2 | -------------------------------------------------------------------------------- /src/assets/images/webappicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/images/webappicon.png -------------------------------------------------------------------------------- /src/assets/fonts/RuneScape-UF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/fonts/RuneScape-UF.woff -------------------------------------------------------------------------------- /src/assets/fonts/RuneScape-UF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/fonts/RuneScape-UF.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/RuneScape-Plain-11.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/fonts/RuneScape-Plain-11.woff -------------------------------------------------------------------------------- /src/assets/fonts/RuneScape-Plain-12.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/fonts/RuneScape-Plain-12.woff -------------------------------------------------------------------------------- /src/assets/fonts/RuneScape-Plain-11.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/fonts/RuneScape-Plain-11.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/RuneScape-Plain-12.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/assets/fonts/RuneScape-Plain-12.woff2 -------------------------------------------------------------------------------- /src/content/inferno/assets/images/bat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/bat.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/blob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/blob.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/map.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/nib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/nib.png -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/bat.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/bat.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/blob.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/blob.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/images/mager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/mager.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/meleer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/meleer.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/ranger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/ranger.png -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/meleer.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/meleer.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/images/TzKal-Zuk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/TzKal-Zuk.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/Yt-HurKot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/Yt-HurKot.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/Jal-MejJak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/Jal-MejJak.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/zuk_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/zuk_attack.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/Jal-AkRek-Ket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/Jal-AkRek-Ket.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/Jal-AkRek-Mej.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/Jal-AkRek-Mej.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/Jal-AkRek-Xil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/Jal-AkRek-Xil.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_1.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_2.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_3.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_4.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_5.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_6.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_7.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_8.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_9.png -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/firebreath_159.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/firebreath_159.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_10.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_11.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_12.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_13.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_14.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_15.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_16.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_17.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_18.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_19.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_20.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_21.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_22.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_23.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_24.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_25.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_26.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_27.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_28.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_29.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_30.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_31.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_mage_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_mage_32.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_1.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_10.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_11.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_12.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_13.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_2.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_3.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_4.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_5.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_6.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_7.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_8.png -------------------------------------------------------------------------------- /src/content/inferno/assets/images/jad/jad_range_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/images/jad/jad_range_9.png -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/firewave_hit_163.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/firewave_hit_163.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/mage_ranger_598.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/mage_ranger_598.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/fireblast_cast_and_fire_155.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/fireblast_cast_and_fire_155.ogg -------------------------------------------------------------------------------- /src/content/inferno/assets/sounds/firewave_cast_and_fire_162.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OldSchoolSDK/InfernoTrainer/HEAD/src/content/inferno/assets/sounds/firewave_cast_and_fire_162.ogg -------------------------------------------------------------------------------- /src/@types/chebyshev/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "chebyshev" { 4 | export default function chebyshev(pointOne: number[], pointTwo: number[]): number; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "arrowParens": "always", 5 | "printWidth": 120, 6 | "bracketSpacing": true, 7 | "semi": true, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Inferno", 3 | "name": "Inferno Trainer", 4 | "icons": [ 5 | { 6 | "src": "/webappicon.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "fullscreen", 13 | "orientation": "landscape" 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": false, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "jsx": "react", 9 | "allowJs": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true 12 | }, 13 | "include": ["src", "index.d.ts", "test"] 14 | } 15 | -------------------------------------------------------------------------------- /src/content/inferno/js/Wall.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Entity, CollisionType } from "osrs-sdk"; 4 | 5 | export class Wall extends Entity { 6 | get collisionType() { 7 | return CollisionType.BLOCK_MOVEMENT; 8 | } 9 | 10 | get size() { 11 | return 1; 12 | } 13 | draw() { 14 | // force empty draw 15 | } 16 | 17 | get color() { 18 | return "#222222"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "(Start) Webpack Dev Server", 8 | "identifier": "start_server_task", 9 | "type": "npm", 10 | "script": "start_server", 11 | "promptOnClose": true, 12 | "problemMatcher": [] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | rules: { 7 | "@typescript-eslint/no-unused-vars": "off", 8 | }, 9 | root: true, 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["@typescript-eslint"], 12 | extends: [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome Debugger", 8 | "port": 9222, 9 | "url": "http://localhost:8000/", 10 | 11 | "webRoot": "${workspaceFolder}", 12 | "sourceMaps": true, 13 | "trace": true, 14 | "timeout": 15000 15 | //"preLaunchTask": "start_server_task" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | declare module "*.gif"; 6 | declare module "*.ogg" { 7 | const value: string; 8 | export default value; 9 | } 10 | declare module "*.glb" { 11 | const value: string; 12 | export default value; 13 | } 14 | declare module "*.gltf" { 15 | const value: string; 16 | export default value; 17 | } 18 | declare module "*.html" { 19 | const value: string; 20 | export default value; 21 | } 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "jsdom", 5 | extensionsToTreatAsEsm: [".ts"], 6 | globals: { 7 | "ts-jest": { 8 | useESM: true, 9 | }, 10 | }, 11 | setupFiles: ["./test/setupFiles.ts"], 12 | moduleNameMapper: { 13 | "^(\\.{1,2}/.*)\\.js$": "$1", 14 | "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|ogg|glb|html)$": 15 | "/test/__mocks__/assetMock.js", 16 | "\\.(css|less)$": "/test/__mocks__/cssMock.js", 17 | three: require.resolve("three"), 18 | //"osrs-sdk": require.resolve("osrs-sdk"), 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoMobDeathStore.ts: -------------------------------------------------------------------------------- 1 | import { Mob, Region } from "osrs-sdk"; 2 | 3 | import { shuffle, remove } from "lodash"; 4 | 5 | export class InfernoMobDeathStore { 6 | static mobDeathStore = new InfernoMobDeathStore(); 7 | static deadMobs: Mob[] = []; 8 | static npcDied(mob: Mob) { 9 | if (!mob.hasResurrected) { 10 | InfernoMobDeathStore.deadMobs.push(mob); 11 | } 12 | } 13 | 14 | static selectMobToResurect(_region: Region) { 15 | if (InfernoMobDeathStore.deadMobs.length) { 16 | const mobToResurrect = shuffle(InfernoMobDeathStore.deadMobs)[0]; 17 | mobToResurrect.hasResurrected = true; 18 | remove(InfernoMobDeathStore.deadMobs, mobToResurrect); 19 | return mobToResurrect; 20 | } 21 | return null; 22 | } 23 | 24 | static clearDeadMobs() { 25 | InfernoMobDeathStore.deadMobs = []; 26 | } 27 | } -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoSettings.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export class InfernoSettings { 4 | static waveProgression = false; 5 | static spawnIndicators = false; 6 | static displaySetTimer = false; 7 | 8 | static persistToStorage() { 9 | window.localStorage.setItem("waveProgression", String(InfernoSettings.waveProgression)); 10 | window.localStorage.setItem("spawnIndicators", String(InfernoSettings.spawnIndicators)); 11 | window.localStorage.setItem("displaySetTimer", String(InfernoSettings.displaySetTimer)); 12 | } 13 | 14 | static readFromStorage() { 15 | InfernoSettings.waveProgression = window.localStorage.getItem("waveProgression") === "true" || false; 16 | InfernoSettings.spawnIndicators = window.localStorage.getItem("spawnIndicators") === "true" || false; 17 | InfernoSettings.displaySetTimer = window.localStorage.getItem("displaySetTimer") === "true" || false; 18 | } 19 | } -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoScene.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, Entity, CollisionType, LineOfSightMask, Model, GLTFModel } from "osrs-sdk"; 4 | 5 | 6 | // note: v1 has the rocks where zuk should be - we could use that in the future 7 | export const InfernoSceneModel = Assets.getAssetUrl("models/scene-v3.glb"); 8 | 9 | export class InfernoScene extends Entity { 10 | get collisionType() { 11 | return CollisionType.NONE; 12 | } 13 | 14 | get size() { 15 | return 1; 16 | } 17 | 18 | draw() { 19 | // force empty draw 20 | } 21 | 22 | get color() { 23 | return "#222222"; 24 | } 25 | 26 | get lineOfSight() { 27 | return LineOfSightMask.NONE; 28 | } 29 | 30 | getPerceivedRotation() { 31 | return -Math.PI / 2; 32 | } 33 | 34 | create3dModel(): Model { 35 | return new GLTFModel(this, [InfernoSceneModel], { scale: 1, verticalOffset: -2.5, originOffset: { 36 | x: -6.5, 37 | y: 12.5, 38 | }}); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inferno-trainer", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npx webpack serve", 8 | "build": "webpack", 9 | "lint": "eslint . --ext .ts --fix", 10 | "prettier": "npx prettier --write ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/OldSchoolSDK/InfernoTrainer.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/OldSchoolSDK/InfernoTrainer/issues" 21 | }, 22 | "homepage": "https://github.com/OldSchoolSDK/InfernoTrainer#readme", 23 | "devDependencies": { 24 | "@types/jest": "^27.4.1", 25 | "@types/lodash": "^4.14.171", 26 | "@types/three": "^0.163.0", 27 | "@typescript-eslint/eslint-plugin": "^5.17.0", 28 | "@typescript-eslint/parser": "^5.17.0", 29 | "copy-webpack-plugin": "^9.0.1", 30 | "eslint": "^7.32.0", 31 | "eslint-config-standard": "^16.0.3", 32 | "eslint-plugin-import": "^2.23.4", 33 | "eslint-plugin-node": "^11.1.0", 34 | "eslint-plugin-promise": "^5.1.0", 35 | "html-loader": "^4.2.0", 36 | "jest": "^27.5.1", 37 | "lodash": "^4.17.21", 38 | "prettier": "^3.2.5", 39 | "ts-jest": "^27.1.4", 40 | "ts-loader": "^9.5.1", 41 | "typescript": "^4.6.3", 42 | "webpack": "^5.91.0", 43 | "webpack-cli": "^4.9.2", 44 | "webpack-dev-server": "^3.11.2" 45 | }, 46 | "dependencies": { 47 | "osrs-sdk": "0.1.4", 48 | "three": "^0.163.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inferno Trainer 2 | 3 | - [Click here to try the Inferno Trainer](https://www.infernotrainer.com/) 4 | - [Click here to beta test the Inferno Trainer](https://beta.infernotrainer.com/) 5 | - [Join our Discord](https://discord.gg/Z3ZyY7Yzt5) 6 | 7 | ## What is this project? 8 | 9 | This project stemmed from my interest in Old School Runescape's Inferno, and my desire for an open source, relatively clean re-implementation of the Old School Runescape engine. The underlying code is designed closer to a true game engine compared to any other trainer or simulator. The goal is for there to be a clean, well-defined API between all "Game Content" code and any underlying "Engine" code 10 | 11 | ## How do I use it? 12 | 13 | ### Pick your own waves 14 | 15 | If you want to practice a wave, click one of the links above. You can type in a wave and it will produce a random spawn, and you can re-play the exact spawn if you wish. 16 | 17 | ### Practice a wave I failed in-game 18 | 19 | Alternatively, if you are practicing the Inferno and have the Inferno Stats plugin (Available on RuneLite's Plugin Hub), you can click a wave in the panel and it will load the simulation with the exact spawn. I would recommend you disable the "Hide when outside of the Inferno" feature for when you plank. 20 | 21 | ## I found a bug! 22 | 23 | Likely. Please open a issue above. Videos, screenshots, proof of OSRS science, etc is appreciated. I want this to be a faithful re-implementation of OSRS and all bugs are appreciated. 24 | 25 | ## Can I contribute? 26 | 27 | Sure. Right now the code is undergoing rapid development and the API is not stable. I am open to pull requests but I suggest you start small and let me talk to you first to make sure we're aligned. 28 | 29 | ## Development notes 30 | 31 | Use Node 16 for now. There's an SSL error on version >= 18. 32 | 33 | npm run start 34 | 35 | Running test 36 | 37 | npx jest 38 | -------------------------------------------------------------------------------- /test/setupFiles.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | 3 | global.OffscreenCanvas = jest.fn().mockImplementation((width: number, height: number) => { 4 | return { 5 | height, 6 | width, 7 | oncontextlost: jest.fn(), 8 | oncontextrestored: jest.fn(), 9 | getContext: jest.fn(() => undefined), 10 | convertToBlob: jest.fn(), 11 | transferToImageBitmap: jest.fn(), 12 | addEventListener: jest.fn(), 13 | removeEventListener: jest.fn(), 14 | dispatchEvent: jest.fn(), 15 | }; 16 | }); 17 | 18 | global.fetch = jest.fn().mockImplementation(() => ({ 19 | arrayBuffer: () => null, 20 | })); 21 | 22 | import { Random, Settings } from "osrs-sdk"; 23 | 24 | jest.mock("osrs-sdk", () => { 25 | const originalModule = jest.requireActual( 26 | "osrs-sdk", 27 | ); 28 | return { 29 | ...originalModule, 30 | Assets: { 31 | getAssetUrl(x: any) { 32 | return x; 33 | } 34 | }, 35 | SoundCache: { 36 | preload() {}, 37 | play() {} 38 | } 39 | }; 40 | }); 41 | 42 | jest.mock("three", () => ({ 43 | Scene: class Scene { 44 | public add(): void { 45 | return; 46 | } 47 | }, 48 | WebGLRenderer: class WebGlRenderer { 49 | public render(): void { 50 | return; 51 | } 52 | public setSize(): void { 53 | return; 54 | } 55 | }, 56 | GLTFLoader: class GLTFLoader { 57 | constructor() {} 58 | setMeshoptDecoder() {} 59 | }, 60 | })); 61 | jest.spyOn(document, "getElementById").mockImplementation((elementId: string) => { 62 | const c = document.createElement("canvas"); 63 | c.ariaLabel = elementId; 64 | return c; 65 | }); 66 | 67 | const nextRandom = []; 68 | 69 | Random.setRandom(() => { 70 | if (nextRandom.length > 0) { 71 | return nextRandom.shift(); 72 | } 73 | Random.memory = (Random.memory + 13.37) % 180; 74 | return Math.abs(Math.sin(Random.memory * 0.0174533)); 75 | }); 76 | 77 | Settings.readFromStorage(); 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | # MacOSX 3 | .DS_Store 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalAkRekMej.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Mob, EntityNames, Settings, MagicWeapon, UnitBonuses } from "osrs-sdk"; 4 | 5 | import JalAkRekMejImage from "../../assets/images/Jal-AkRek-Mej.png"; 6 | 7 | export class JalAkRekMej extends Mob { 8 | mobName() { 9 | return EntityNames.JAL_AK_REK_MEJ; 10 | } 11 | 12 | get combatLevel() { 13 | return 70; 14 | } 15 | drawUnderTile() { 16 | if (this.dying > -1) { 17 | this.region.context.fillStyle = "#964B0073"; 18 | } 19 | { 20 | this.region.context.fillStyle = "#0000FF"; 21 | } 22 | 23 | // Draw mob 24 | this.region.context.fillRect( 25 | -(this.size * Settings.tileSize) / 2, 26 | -(this.size * Settings.tileSize) / 2, 27 | this.size * Settings.tileSize, 28 | this.size * Settings.tileSize, 29 | ); 30 | } 31 | 32 | setStats() { 33 | this.weapons = { 34 | magic: new MagicWeapon(), 35 | }; 36 | 37 | // non boosted numbers 38 | this.stats = { 39 | attack: 1, 40 | strength: 1, 41 | defence: 95, 42 | range: 1, 43 | magic: 120, 44 | hitpoint: 15, 45 | }; 46 | 47 | // with boosts 48 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 49 | } 50 | 51 | get bonuses(): UnitBonuses { 52 | return { 53 | attack: { 54 | stab: 0, 55 | slash: 0, 56 | crush: 0, 57 | magic: 25, 58 | range: 0, 59 | }, 60 | defence: { 61 | stab: 0, 62 | slash: 0, 63 | crush: 0, 64 | magic: 25, 65 | range: 0, 66 | }, 67 | other: { 68 | meleeStrength: 0, 69 | rangedStrength: 0, 70 | magicDamage: 1.25, 71 | prayer: 0, 72 | }, 73 | }; 74 | } 75 | 76 | get attackSpeed() { 77 | return 4; 78 | } 79 | 80 | get attackRange() { 81 | return 15; 82 | } 83 | 84 | get size() { 85 | return 1; 86 | } 87 | 88 | get image() { 89 | return JalAkRekMejImage; 90 | } 91 | 92 | attackStyleForNewAttack() { 93 | return "magic"; 94 | } 95 | 96 | attackAnimation(tickPercent: number, context) { 97 | context.translate(Math.sin(tickPercent * Math.PI * 4) * 2, Math.sin(tickPercent * Math.PI * -2)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalAkRekXil.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Mob, EntityNames, Settings, RangedWeapon, UnitBonuses } from "osrs-sdk"; 4 | 5 | import JalAkRekMejImage from "../../assets/images/Jal-AkRek-Xil.png"; 6 | 7 | export class JalAkRekXil extends Mob { 8 | mobName() { 9 | return EntityNames.JAL_AK_REK_XIL; 10 | } 11 | 12 | get combatLevel() { 13 | return 70; 14 | } 15 | 16 | drawUnderTile() { 17 | if (this.dying > -1) { 18 | this.region.context.fillStyle = "#964B0073"; 19 | } 20 | { 21 | this.region.context.fillStyle = "#00FF00"; 22 | } 23 | 24 | // Draw mob 25 | this.region.context.fillRect( 26 | -(this.size * Settings.tileSize) / 2, 27 | -(this.size * Settings.tileSize) / 2, 28 | this.size * Settings.tileSize, 29 | this.size * Settings.tileSize, 30 | ); 31 | } 32 | setStats() { 33 | this.weapons = { 34 | range: new RangedWeapon(), 35 | }; 36 | 37 | // non boosted numbers 38 | this.stats = { 39 | attack: 1, 40 | strength: 1, 41 | defence: 95, 42 | range: 120, 43 | magic: 1, 44 | hitpoint: 15, 45 | }; 46 | 47 | // with boosts 48 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 49 | } 50 | 51 | get bonuses(): UnitBonuses { 52 | return { 53 | attack: { 54 | stab: 0, 55 | slash: 0, 56 | crush: 0, 57 | magic: 0, 58 | range: 25, 59 | }, 60 | defence: { 61 | stab: 0, 62 | slash: 0, 63 | crush: 0, 64 | magic: 0, 65 | range: 25, 66 | }, 67 | other: { 68 | meleeStrength: 0, 69 | rangedStrength: 25, 70 | magicDamage: 0, 71 | prayer: 0, 72 | }, 73 | }; 74 | } 75 | 76 | get attackSpeed() { 77 | return 4; 78 | } 79 | 80 | get attackRange() { 81 | return 15; 82 | } 83 | 84 | get size() { 85 | return 1; 86 | } 87 | 88 | get image() { 89 | return JalAkRekMejImage; 90 | } 91 | 92 | attackStyleForNewAttack() { 93 | return "range"; 94 | } 95 | 96 | attackAnimation(tickPercent: number, context) { 97 | context.translate(Math.sin(tickPercent * Math.PI * 4) * 2, Math.sin(tickPercent * Math.PI * -2)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalAkRekKet.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Mob, EntityNames, Settings, MeleeWeapon, UnitBonuses } from "osrs-sdk"; 4 | 5 | import JalAkRekKetImage from "../../assets/images/Jal-AkRek-Ket.png"; 6 | 7 | export class JalAkRekKet extends Mob { 8 | mobName() { 9 | return EntityNames.JAL_AK_REK_KET; 10 | } 11 | 12 | get combatLevel() { 13 | return 70; 14 | } 15 | 16 | drawUnderTile() { 17 | if (this.dying > -1) { 18 | this.region.context.fillStyle = "#964B0073"; 19 | } 20 | { 21 | this.region.context.fillStyle = "#FF0000"; 22 | } 23 | 24 | // Draw mob 25 | this.region.context.fillRect( 26 | -(this.size * Settings.tileSize) / 2, 27 | -(this.size * Settings.tileSize) / 2, 28 | this.size * Settings.tileSize, 29 | this.size * Settings.tileSize, 30 | ); 31 | } 32 | 33 | setStats() { 34 | this.weapons = { 35 | crush: new MeleeWeapon(), 36 | }; 37 | 38 | // non boosted numbers 39 | this.stats = { 40 | attack: 120, 41 | strength: 120, 42 | defence: 95, 43 | range: 1, 44 | magic: 1, 45 | hitpoint: 15, 46 | }; 47 | 48 | // with boosts 49 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 50 | } 51 | 52 | get bonuses(): UnitBonuses { 53 | return { 54 | attack: { 55 | stab: 0, 56 | slash: 0, 57 | crush: 0, 58 | magic: 0, 59 | range: 25, 60 | }, 61 | defence: { 62 | stab: 25, 63 | slash: 25, 64 | crush: 25, 65 | magic: 0, 66 | range: 0, 67 | }, 68 | other: { 69 | meleeStrength: 25, 70 | rangedStrength: 0, 71 | magicDamage: 0, 72 | prayer: 0, 73 | }, 74 | }; 75 | } 76 | 77 | get attackSpeed() { 78 | return 4; 79 | } 80 | 81 | get attackRange() { 82 | return 1; 83 | } 84 | 85 | get size() { 86 | return 1; 87 | } 88 | 89 | get image() { 90 | return JalAkRekKetImage; 91 | } 92 | 93 | attackStyleForNewAttack() { 94 | return "crush"; 95 | } 96 | 97 | attackAnimation(tickPercent: number, context) { 98 | context.translate(Math.sin(tickPercent * Math.PI * 4) * 2, Math.sin(tickPercent * Math.PI * -2)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const webpack = require("webpack"); 4 | 5 | let isDevBuild = false; 6 | if (!process.env.COMMIT_REF) { 7 | isDevBuild = true; 8 | process.env.COMMIT_REF = "local build"; 9 | } 10 | if (!process.env.BUILD_DATE) { 11 | isDevBuild = true; 12 | process.env.BUILD_DATE = ""; 13 | } 14 | if (!process.env.DEPLOY_URL) { 15 | isDevBuild = true; 16 | process.env.DEPLOY_URL = "http://localhost:8000/"; 17 | } 18 | const config = { 19 | mode: isDevBuild ? "development" : "production", 20 | entry: "./src/index.ts", 21 | output: { 22 | filename: "main.js", 23 | path: path.resolve(__dirname, "dist"), 24 | publicPath: '', 25 | }, 26 | devtool: "source-map", 27 | devServer: { 28 | contentBase: path.join(__dirname, "dist"), 29 | compress: true, 30 | port: 8000, 31 | }, 32 | resolve: { 33 | extensions: [".tsx", ".ts", ".js"], 34 | }, 35 | // url(https://assets-soltrainer.netlify.app/assets/fonts/RuneScape-UF.woff) format("woff"); 36 | plugins: [ 37 | new CopyPlugin({ 38 | patterns: [ 39 | { from: `index.html`, to: "", context: `src/` }, 40 | { from: `index.html`, to: "colosseum.html", context: `src/` }, 41 | { from: `manifest.json`, to: "", context: `src/` }, 42 | { 43 | from: `assets/images/webappicon.png`, 44 | to: "webappicon.png", 45 | context: `src/`, 46 | }, 47 | { from: '*.png', to: "", context: "node_modules/osrs-sdk/_bundles/", noErrorOnMissing: true }, 48 | { from: '*.gif', to: "", context: "node_modules/osrs-sdk/_bundles/", noErrorOnMissing: true }, 49 | { from: '*.ogg', to: "", context: "node_modules/osrs-sdk/_bundles/", noErrorOnMissing: true }, 50 | { from: `assets/fonts/*.woff`, to: "", context: `src/` }, 51 | { from: `assets/fonts/*.woff2`, to: "", context: `src/` }, 52 | ], 53 | }), 54 | new webpack.EnvironmentPlugin(["COMMIT_REF", "BUILD_DATE", "DEPLOY_URL"]), 55 | ], 56 | module: { 57 | rules: [ 58 | { 59 | test: /\.tsx?$/, 60 | use: "ts-loader", 61 | exclude: /node_modules/, 62 | }, 63 | { 64 | test: /\.(png|svg|jpg|jpeg|gif|ogg|gltf|glb)$/i, 65 | type: "asset/resource", 66 | }, 67 | { 68 | test: /\.html$/i, 69 | loader: "html-loader", 70 | }, 71 | ], 72 | }, 73 | }; 74 | 75 | module.exports = config; 76 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalMejRah.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, RangedWeapon, Unit, AttackBonuses, ProjectileOptions, Player, Mob, Sound, UnitBonuses, GLTFModel, EntityNames } from "osrs-sdk"; 4 | 5 | import BatImage from "../../assets/images/bat.png"; 6 | import BatSound from "../../assets/sounds/bat.ogg"; 7 | import { InfernoMobDeathStore } from "../InfernoMobDeathStore"; 8 | 9 | const BatModel = Assets.getAssetUrl("models/7692_33018.glb"); 10 | 11 | class JalMejRahWeapon extends RangedWeapon { 12 | attack(from: Unit, to: Unit, bonuses: AttackBonuses = {}, options: ProjectileOptions = {}): boolean { 13 | super.attack(from, to, bonuses, options); 14 | const player = to as Player; 15 | player.currentStats.run -= 300; 16 | return true; 17 | } 18 | } 19 | export class JalMejRah extends Mob { 20 | mobName() { 21 | return EntityNames.JAL_MEJ_RAJ; 22 | } 23 | 24 | get combatLevel() { 25 | return 85; 26 | } 27 | 28 | dead() { 29 | super.dead(); 30 | InfernoMobDeathStore.npcDied(this); 31 | } 32 | 33 | setStats() { 34 | this.stunned = 1; 35 | 36 | this.weapons = { 37 | range: new JalMejRahWeapon({ sound: new Sound(BatSound, 0.5) }), 38 | }; 39 | 40 | // non boosted numbers 41 | this.stats = { 42 | attack: 0, 43 | strength: 0, 44 | defence: 55, 45 | range: 120, 46 | magic: 120, 47 | hitpoint: 25, 48 | }; 49 | 50 | // with boosts 51 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 52 | } 53 | 54 | get bonuses(): UnitBonuses { 55 | return { 56 | attack: { 57 | stab: 0, 58 | slash: 0, 59 | crush: 0, 60 | magic: 0, 61 | range: 25, 62 | }, 63 | defence: { 64 | stab: 30, 65 | slash: 30, 66 | crush: 30, 67 | magic: -20, 68 | range: 45, 69 | }, 70 | other: { 71 | meleeStrength: 0, 72 | rangedStrength: 30, 73 | magicDamage: 0, 74 | prayer: 0, 75 | }, 76 | }; 77 | } 78 | get attackSpeed() { 79 | return 3; 80 | } 81 | 82 | get attackRange() { 83 | return 4; 84 | } 85 | 86 | get size() { 87 | return 2; 88 | } 89 | 90 | get image() { 91 | return BatImage; 92 | } 93 | 94 | attackStyleForNewAttack() { 95 | return "range"; 96 | } 97 | 98 | attackAnimation(tickPercent: number, context) { 99 | context.translate(Math.sin(tickPercent * Math.PI * 4) * 2, Math.sin(tickPercent * Math.PI * -2)); 100 | } 101 | 102 | create3dModel() { 103 | return GLTFModel.forRenderable(this, BatModel); 104 | } 105 | 106 | override get attackAnimationId() { 107 | return 1; 108 | } 109 | 110 | override get deathAnimationId() { 111 | return 3; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/content/inferno/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
3:30
4 | 5 |
6 | 7 | 8 | 14 | Replay Wave 15 | 16 | 17 | 27 |
35 | 36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
54 | 55 | 56 |
57 | 58 |
64 | 65 | 66 |
67 | 68 |
74 | 75 | 76 |
77 | 78 | Custom Mob Mode 79 | 80 | 81 |
82 | 83 | Explore Source on GitHub 86 | Join Our Discord 89 | Mob Explanations 90 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/YtHurKot.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Weapon, Unit, AttackBonuses, ProjectileOptions, Random, Mob, Location, Region, UnitOptions, Projectile, MeleeWeapon, UnitBonuses, UnitTypes, EntityNames } from "osrs-sdk"; 4 | 5 | import HurKotImage from "../../assets/images/Yt-HurKot.png"; 6 | 7 | class HealWeapon extends Weapon { 8 | calculateHitDelay(distance: number) { 9 | return 3; 10 | } 11 | static isMeleeAttackStyle(style: string) { 12 | return true; 13 | } 14 | 15 | attack(from: Unit, to: Unit, bonuses: AttackBonuses = {}, options: ProjectileOptions): boolean { 16 | this.damage = -Math.floor(Random.get() * 20); 17 | this.registerProjectile(from, to, bonuses, options); 18 | return true; 19 | } 20 | } 21 | 22 | export class YtHurKot extends Mob { 23 | myJad: Unit; 24 | 25 | constructor(region: Region, location: Location, options: UnitOptions) { 26 | super(region, location, options); 27 | this.myJad = this.aggro as Unit; 28 | } 29 | mobName() { 30 | return EntityNames.YT_HUR_KOT; 31 | } 32 | 33 | attackStep() { 34 | super.attackStep(); 35 | 36 | if (this.myJad.isDying()) { 37 | this.dead(); 38 | } 39 | } 40 | 41 | shouldChangeAggro(projectile: Projectile) { 42 | return this.aggro != projectile.from && this.autoRetaliate; 43 | } 44 | 45 | get combatLevel() { 46 | return 141; 47 | } 48 | 49 | setStats() { 50 | this.stunned = 1; 51 | 52 | this.weapons = { 53 | heal: new HealWeapon(), 54 | crush: new MeleeWeapon(), 55 | }; 56 | 57 | // non boosted numbers 58 | this.stats = { 59 | attack: 165, 60 | strength: 125, 61 | defence: 100, 62 | range: 150, 63 | magic: 150, 64 | hitpoint: 90, 65 | }; 66 | 67 | // with boosts 68 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 69 | } 70 | 71 | get bonuses(): UnitBonuses { 72 | return { 73 | attack: { 74 | stab: 0, 75 | slash: 0, 76 | crush: 0, 77 | magic: 100, 78 | range: 80, 79 | }, 80 | defence: { 81 | stab: 0, 82 | slash: 0, 83 | crush: 0, 84 | magic: 130, 85 | range: 130, 86 | }, 87 | other: { 88 | meleeStrength: 0, 89 | rangedStrength: 0, 90 | magicDamage: 0, 91 | prayer: 0, 92 | }, 93 | }; 94 | } 95 | get attackSpeed() { 96 | return 4; 97 | } 98 | 99 | attackStyleForNewAttack() { 100 | return this.aggro?.type === UnitTypes.PLAYER ? "crush" : "heal"; 101 | } 102 | 103 | get attackRange() { 104 | return 1; 105 | } 106 | 107 | get size() { 108 | return 1; 109 | } 110 | 111 | get image() { 112 | return HurKotImage; 113 | } 114 | 115 | get color() { 116 | return "#ACFF5633"; 117 | } 118 | 119 | attackAnimation(tickPercent: number, context) { 120 | context.transform(1, 0, Math.sin(-tickPercent * Math.PI * 2) / 2, 1, 0, 0); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoHealerSpark.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, Weapon, Unit, AttackBonuses, ProjectileOptions, Random, Projectile, Entity, Region, GLTFModel, Location, CollisionType, LineOfSightMask, Pathing, Viewport, SoundCache, Sound, Collision, Settings, Trainer } from "osrs-sdk"; 4 | 5 | import FireWaveHit from "../assets/sounds/firewave_hit_163.ogg"; 6 | 7 | const Splat = Assets.getAssetUrl("models/tekton_meteor_splat.glb"); 8 | 9 | class InfernoSparkWeapon extends Weapon { 10 | calculateHitDelay(distance: number) { 11 | return 1; 12 | } 13 | 14 | static isMeleeAttackStyle(style: string) { 15 | // fun way to make the attack instantaneous 16 | return true; 17 | } 18 | 19 | attack(from: Unit, to: Unit, bonuses: AttackBonuses = {}, options: ProjectileOptions = {}): boolean { 20 | this.damage = 5 + Math.floor(Random.get() * 6); 21 | to.addProjectile(new Projectile(this, this.damage, from, to, bonuses.attackStyle, options)); 22 | return true; 23 | } 24 | } 25 | 26 | export class InfernoHealerSpark extends Entity { 27 | from: Unit; 28 | to: Unit; 29 | weapon: InfernoSparkWeapon = new InfernoSparkWeapon(); 30 | 31 | age = 0; 32 | 33 | constructor(region: Region, location: Location, from: Unit, to: Unit) { 34 | super(region, location); 35 | this.from = from; 36 | this.to = to; 37 | } 38 | 39 | create3dModel() { 40 | return GLTFModel.forRenderable(this, Splat, { verticalOffset: -1 }); 41 | } 42 | 43 | get animationIndex() { 44 | return 0; 45 | } 46 | 47 | get color() { 48 | return "#FFFF00"; 49 | } 50 | 51 | get collisionType() { 52 | return CollisionType.NONE; 53 | } 54 | 55 | get lineOfSight() { 56 | return LineOfSightMask.NONE; 57 | } 58 | 59 | shouldDestroy() { 60 | return this.dying === 0; 61 | } 62 | 63 | get drawOutline() { 64 | return false; 65 | } 66 | 67 | visible() { 68 | return this.dying < 0 && this.age >= 1; 69 | } 70 | 71 | tick() { 72 | ++this.age; 73 | if (this.age == 1) { 74 | let attemptedVolume = 75 | 1 / 76 | Pathing.dist( 77 | Trainer.player.location.x, 78 | Trainer.player.location.y, 79 | this.location.x, 80 | this.location.y, 81 | ); 82 | attemptedVolume = Math.min(1, Math.max(0, Math.sqrt(attemptedVolume))); 83 | SoundCache.play(new Sound(FireWaveHit, 0.025 * attemptedVolume), true); 84 | if ( 85 | Collision.collisionMath(this.location.x - 1, this.location.y + 1, 3, this.to.location.x, this.to.location.y, 1) 86 | ) { 87 | this.weapon.attack(this.from, this.to as Unit, {}); 88 | } 89 | } else if (this.age == 3) { 90 | this.playAnimation(0).then(() => { 91 | this.dying = 0; 92 | }) 93 | } 94 | } 95 | 96 | draw() { 97 | this.region.context.fillStyle = "#FF0000"; 98 | 99 | this.region.context.fillRect( 100 | this.location.x * Settings.tileSize, 101 | (this.location.y - this.size + 1) * Settings.tileSize, 102 | this.size * Settings.tileSize, 103 | this.size * Settings.tileSize, 104 | ); 105 | } 106 | 107 | get size() { 108 | return 1; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/content/inferno/js/JalZekModelWithLight.ts: -------------------------------------------------------------------------------- 1 | import { Assets, GLTFModel, Model, Location3, Renderable, GLTFModelOptions } from "osrs-sdk"; 2 | import * as THREE from "three"; 3 | import { JalZek } from "./mobs/JalZek"; 4 | 5 | export class JalZekModelWithLight extends GLTFModel { 6 | underglowLight: THREE.PointLight | null = null; 7 | jalZekRenderable: JalZek; 8 | lightParented = false; 9 | primaryModelPath: string; 10 | 11 | // light props 12 | readonly NORMAL_UNDERGLOW_INTENSITY: number = 32.0; 13 | readonly FLICKER_OFF_INTENSITY: number = 0.05; 14 | readonly FLICKER_ON_INTENSITY: number = 50.0; 15 | 16 | constructor(renderable: JalZek, modelPath: string, options?: GLTFModelOptions) { 17 | super(renderable, [modelPath], options || {}); 18 | this.jalZekRenderable = renderable; 19 | this.primaryModelPath = modelPath; 20 | this.createLight(); 21 | } 22 | 23 | private createLight() { 24 | this.underglowLight = new THREE.PointLight( 25 | 0xff0000, // red 26 | this.NORMAL_UNDERGLOW_INTENSITY, 27 | this.jalZekRenderable.size * 3.0, // reduced radius 28 | 7, // increased decay for sharper falloff 29 | ); 30 | this.underglowLight.position.set(0, -1.0, 0); 31 | this.underglowLight.visible = true; 32 | this.underglowLight.name = `JalZekUnderglow_${this.jalZekRenderable.mobId}`; 33 | } 34 | 35 | override async preload(): Promise { 36 | await super.preload(); 37 | } 38 | 39 | override draw( 40 | scene: THREE.Scene, 41 | clockDelta: number, 42 | tickPercent: number, 43 | location: Location3, 44 | rotation: number, 45 | pitch: number, 46 | modelIsVisible: boolean, 47 | modelOffsets: Location3[], 48 | ): void { 49 | super.draw(scene, clockDelta, tickPercent, location, rotation, pitch, modelIsVisible, modelOffsets); 50 | 51 | if (modelIsVisible && this.underglowLight && !this.lightParented) { 52 | if (this.primaryModelPath) { 53 | const sdkModelObject = scene.getObjectByName(this.primaryModelPath); 54 | if (sdkModelObject) { 55 | sdkModelObject.add(this.underglowLight); 56 | this.lightParented = true; 57 | } 58 | } 59 | } 60 | if (this.underglowLight) { 61 | this.underglowLight.visible = modelIsVisible; 62 | } 63 | } 64 | 65 | public setFlickerVisualState(isFlickering: boolean): void { 66 | if (this.underglowLight) { 67 | if (isFlickering) { 68 | // Double flicker: ON -> OFF -> ON -> OFF -> normal 69 | this.underglowLight.intensity = this.FLICKER_ON_INTENSITY; 70 | setTimeout(() => { 71 | if (this.underglowLight) { 72 | this.underglowLight.intensity = this.FLICKER_OFF_INTENSITY; 73 | setTimeout(() => { 74 | if (this.underglowLight) { 75 | this.underglowLight.intensity = this.FLICKER_ON_INTENSITY; 76 | setTimeout(() => { 77 | if (this.underglowLight) { 78 | this.underglowLight.intensity = this.FLICKER_OFF_INTENSITY; 79 | setTimeout(() => { 80 | if (this.underglowLight) { 81 | this.underglowLight.intensity = this.NORMAL_UNDERGLOW_INTENSITY; 82 | } 83 | }, 50); 84 | } 85 | }, 50); 86 | } 87 | }, 50); 88 | } 89 | }, 50); 90 | } else { 91 | this.underglowLight.intensity = this.NORMAL_UNDERGLOW_INTENSITY; 92 | } 93 | } 94 | } 95 | 96 | override destroy(scene: THREE.Scene): void { 97 | if (this.underglowLight) { 98 | if (this.underglowLight.parent) { 99 | this.underglowLight.parent.remove(this.underglowLight); 100 | } 101 | this.underglowLight.dispose(); 102 | this.underglowLight = null; 103 | } 104 | this.lightParented = false; 105 | super.destroy(scene); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalXil.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, MultiModelProjectileOffsetInterpolator, Location3, Mob, MeleeWeapon, RangedWeapon, Sound, UnitBonuses, Projectile, GLTFModel, EntityNames } from "osrs-sdk"; 4 | 5 | import RangeImage from "../../assets/images/ranger.png"; 6 | import RangerSound from "../../assets/sounds/mage_ranger_598.ogg"; 7 | import { InfernoMobDeathStore } from "../InfernoMobDeathStore"; 8 | 9 | const HitSound = Assets.getAssetUrl("assets/sounds/inferno_rangermager_dmg.ogg"); 10 | 11 | export const RangerModel = Assets.getAssetUrl("models/7698_33014.glb"); 12 | export const RangeProjectileModel = Assets.getAssetUrl("models/range_projectile.glb"); 13 | 14 | // draw the projectiles coming from the shoulders but converging on the target. 15 | // the projectile is already rotated towards the target so we only need to offset on the x direction 16 | const JalXilOffsetsInterpolator: MultiModelProjectileOffsetInterpolator ={ 17 | interpolateOffsets: function (from: Location3, to: Location3, percent: number): Location3[] { 18 | const r = 0.6 * (1.0 - percent); 19 | const res = [ 20 | { x: r, y: 0, z: 0}, 21 | { x: -r, y: 0, z: 0} 22 | ]; 23 | return res; 24 | } 25 | } 26 | 27 | export class JalXil extends Mob { 28 | mobName() { 29 | return EntityNames.JAL_XIL; 30 | } 31 | 32 | get combatLevel() { 33 | return 370; 34 | } 35 | 36 | override get height() { 37 | return 4; 38 | } 39 | 40 | dead() { 41 | super.dead(); 42 | InfernoMobDeathStore.npcDied(this); 43 | } 44 | 45 | setStats() { 46 | this.stunned = 1; 47 | `` 48 | this.weapons = { 49 | crush: new MeleeWeapon(), 50 | range: new RangedWeapon({ 51 | models: [RangeProjectileModel, RangeProjectileModel], 52 | offsetsInterpolator: JalXilOffsetsInterpolator, 53 | modelScale: 1/128, 54 | projectileSound: new Sound(RangerSound, 0.1), 55 | verticalOffset: -1, 56 | reduceDelay: -2, 57 | visualDelayTicks: 3, 58 | }), 59 | }; 60 | 61 | // non boosted numbers 62 | this.stats = { 63 | attack: 140, 64 | strength: 180, 65 | defence: 60, 66 | range: 250, 67 | magic: 90, 68 | hitpoint: 125, 69 | }; 70 | 71 | // with boosts 72 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 73 | } 74 | 75 | get bonuses(): UnitBonuses { 76 | return { 77 | attack: { 78 | stab: 0, 79 | slash: 0, 80 | crush: 0, 81 | magic: 0, 82 | range: 40, 83 | }, 84 | defence: { 85 | stab: 0, 86 | slash: 0, 87 | crush: 0, 88 | magic: 0, 89 | range: 0, 90 | }, 91 | other: { 92 | meleeStrength: 0, 93 | rangedStrength: 50, 94 | magicDamage: 0, 95 | prayer: 0, 96 | }, 97 | }; 98 | } 99 | 100 | get attackSpeed() { 101 | return 4; 102 | } 103 | 104 | get attackRange() { 105 | return 15; 106 | } 107 | 108 | get size() { 109 | return 3; 110 | } 111 | 112 | get image() { 113 | return RangeImage; 114 | } 115 | 116 | hitSound(damaged) { 117 | return new Sound(HitSound, 0.25); 118 | } 119 | 120 | shouldChangeAggro(projectile: Projectile) { 121 | return this.aggro != projectile.from && this.autoRetaliate; 122 | } 123 | 124 | attackStyleForNewAttack() { 125 | return "range"; 126 | } 127 | 128 | canMeleeIfClose() { 129 | return "crush" as const; 130 | } 131 | 132 | attackAnimation(tickPercent: number, context) { 133 | context.rotate(Math.sin(-tickPercent * Math.PI)); 134 | } 135 | 136 | override create3dModel() { 137 | return GLTFModel.forRenderable(this, RangerModel); 138 | } 139 | 140 | override get deathAnimationLength() { 141 | return 5; 142 | } 143 | 144 | override get deathAnimationId() { 145 | return 4; 146 | } 147 | 148 | override get attackAnimationId() { 149 | return 2; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalTokJadAnim.ts: -------------------------------------------------------------------------------- 1 | import JadMage1 from "../../assets/images/jad/jad_mage_1.png"; 2 | import JadMage2 from "../../assets/images/jad/jad_mage_2.png"; 3 | import JadMage3 from "../../assets/images/jad/jad_mage_3.png"; 4 | import JadMage4 from "../../assets/images/jad/jad_mage_4.png"; 5 | import JadMage5 from "../../assets/images/jad/jad_mage_5.png"; 6 | import JadMage6 from "../../assets/images/jad/jad_mage_6.png"; 7 | import JadMage7 from "../../assets/images/jad/jad_mage_7.png"; 8 | import JadMage8 from "../../assets/images/jad/jad_mage_8.png"; 9 | import JadMage9 from "../../assets/images/jad/jad_mage_9.png"; 10 | import JadMage10 from "../../assets/images/jad/jad_mage_10.png"; 11 | import JadMage11 from "../../assets/images/jad/jad_mage_11.png"; 12 | import JadMage12 from "../../assets/images/jad/jad_mage_12.png"; 13 | import JadMage13 from "../../assets/images/jad/jad_mage_13.png"; 14 | import JadMage14 from "../../assets/images/jad/jad_mage_14.png"; 15 | import JadMage15 from "../../assets/images/jad/jad_mage_15.png"; 16 | import JadMage16 from "../../assets/images/jad/jad_mage_16.png"; 17 | import JadMage17 from "../../assets/images/jad/jad_mage_17.png"; 18 | import JadMage18 from "../../assets/images/jad/jad_mage_18.png"; 19 | import JadMage19 from "../../assets/images/jad/jad_mage_19.png"; 20 | import JadMage20 from "../../assets/images/jad/jad_mage_20.png"; 21 | import JadMage21 from "../../assets/images/jad/jad_mage_21.png"; 22 | import JadMage22 from "../../assets/images/jad/jad_mage_22.png"; 23 | import JadMage23 from "../../assets/images/jad/jad_mage_23.png"; 24 | import JadMage24 from "../../assets/images/jad/jad_mage_24.png"; 25 | import JadMage25 from "../../assets/images/jad/jad_mage_25.png"; 26 | import JadMage26 from "../../assets/images/jad/jad_mage_26.png"; 27 | import JadMage27 from "../../assets/images/jad/jad_mage_27.png"; 28 | import JadMage28 from "../../assets/images/jad/jad_mage_28.png"; 29 | import JadMage29 from "../../assets/images/jad/jad_mage_29.png"; 30 | import JadMage30 from "../../assets/images/jad/jad_mage_30.png"; 31 | import JadMage31 from "../../assets/images/jad/jad_mage_31.png"; 32 | import JadMage32 from "../../assets/images/jad/jad_mage_32.png"; 33 | 34 | import JadRange1 from "../../assets/images/jad/jad_range_1.png"; 35 | import JadRange2 from "../../assets/images/jad/jad_range_2.png"; 36 | import JadRange3 from "../../assets/images/jad/jad_range_3.png"; 37 | import JadRange4 from "../../assets/images/jad/jad_range_4.png"; 38 | import JadRange5 from "../../assets/images/jad/jad_range_5.png"; 39 | import JadRange6 from "../../assets/images/jad/jad_range_6.png"; 40 | import JadRange7 from "../../assets/images/jad/jad_range_7.png"; 41 | import JadRange8 from "../../assets/images/jad/jad_range_8.png"; 42 | import JadRange9 from "../../assets/images/jad/jad_range_9.png"; 43 | import JadRange10 from "../../assets/images/jad/jad_range_10.png"; 44 | import JadRange11 from "../../assets/images/jad/jad_range_11.png"; 45 | import JadRange12 from "../../assets/images/jad/jad_range_12.png"; 46 | import JadRange13 from "../../assets/images/jad/jad_range_13.png"; 47 | 48 | // These were captured at 10fps 49 | export const JAD_FRAMES_PER_TICK = 6; 50 | 51 | export const JAD_MAGE_FRAMES = [ 52 | JadMage1, 53 | JadMage2, 54 | JadMage3, 55 | JadMage4, 56 | JadMage5, 57 | JadMage6, 58 | JadMage7, 59 | JadMage8, 60 | JadMage9, 61 | JadMage10, 62 | JadMage11, 63 | JadMage12, 64 | JadMage13, 65 | JadMage14, 66 | JadMage15, 67 | JadMage16, 68 | JadMage17, 69 | JadMage18, 70 | JadMage19, 71 | JadMage20, 72 | JadMage21, 73 | JadMage22, 74 | JadMage23, 75 | JadMage24, 76 | JadMage25, 77 | JadMage26, 78 | JadMage27, 79 | JadMage28, 80 | JadMage29, 81 | JadMage30, 82 | JadMage31, 83 | JadMage32, 84 | ]; 85 | 86 | export const JAD_RANGE_FRAMES = [ 87 | JadRange1, 88 | JadRange2, 89 | JadRange3, 90 | JadRange4, 91 | JadRange5, 92 | JadRange6, 93 | JadRange7, 94 | JadRange8, 95 | JadRange9, 96 | JadRange10, 97 | JadRange11, 98 | JadRange12, 99 | JadRange13, 100 | ]; 101 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalNib.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, MeleeWeapon, Unit, AttackBonuses, ProjectileOptions, Random, Projectile, Location, Mob, Region, UnitOptions, Sound, UnitBonuses, Collision, AttackIndicators, Pathing, GLTFModel, EntityNames, LocationUtils } from "osrs-sdk"; 4 | 5 | import NibblerImage from "../../assets/images/nib.png"; 6 | import NibblerSound from "../../assets/sounds/meleer.ogg"; 7 | 8 | const NibblerModel = Assets.getAssetUrl("models/7691_33005.glb"); 9 | 10 | class NibblerWeapon extends MeleeWeapon { 11 | attack(from: Unit, to: Unit, bonuses: AttackBonuses, options: ProjectileOptions = {}): boolean { 12 | const damage = Math.floor(Random.get() * 5); 13 | this.damage = damage; 14 | to.addProjectile(new Projectile(this, this.damage, from, to, "crush", { 15 | ...this.projectileOptions, 16 | ...options 17 | })); 18 | return true; 19 | } 20 | } 21 | 22 | export class JalNib extends Mob { 23 | constructor(region: Region, location: Location, options: UnitOptions) { 24 | super(region, location, options); 25 | this.autoRetaliate = false; 26 | } 27 | 28 | mobName() { 29 | return EntityNames.JAL_NIB; 30 | } 31 | 32 | get combatLevel() { 33 | return 32; 34 | } 35 | 36 | setStats() { 37 | this.stunned = 1; 38 | this.autoRetaliate = false; 39 | this.weapons = { 40 | crush: new NibblerWeapon({ 41 | sound: new Sound(NibblerSound, 0.2) 42 | }), 43 | }; 44 | 45 | // non boosted numbers 46 | this.stats = { 47 | attack: 1, 48 | strength: 1, 49 | defence: 15, 50 | range: 1, 51 | magic: 15, 52 | hitpoint: 10, 53 | }; 54 | 55 | // with boosts 56 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 57 | } 58 | 59 | get bonuses(): UnitBonuses { 60 | return { 61 | attack: { 62 | stab: 0, 63 | slash: 0, 64 | crush: 0, 65 | magic: 0, 66 | range: 0, 67 | }, 68 | defence: { 69 | stab: -20, 70 | slash: -20, 71 | crush: -20, 72 | magic: -20, 73 | range: -20, 74 | }, 75 | other: { 76 | meleeStrength: 0, 77 | rangedStrength: 0, 78 | magicDamage: 0, 79 | prayer: 0, 80 | }, 81 | }; 82 | } 83 | 84 | get consumesSpace(): Unit { 85 | return null; 86 | } 87 | 88 | get attackSpeed() { 89 | return 4; 90 | } 91 | 92 | get attackRange() { 93 | return 1; 94 | } 95 | 96 | get size() { 97 | return 1; 98 | } 99 | 100 | get image() { 101 | return NibblerImage; 102 | } 103 | 104 | attackStyleForNewAttack() { 105 | return "crush"; 106 | } 107 | 108 | attackAnimation(tickPercent: number, context) { 109 | context.translate(Math.sin(tickPercent * Math.PI * 4) * 2, Math.sin(tickPercent * Math.PI * -2)); 110 | } 111 | 112 | attackIfPossible() { 113 | this.attackStyle = this.attackStyleForNewAttack(); 114 | 115 | if (this.dying === -1 && this.aggro.dying > -1) { 116 | this.dead(); // cheat way for now. pillar should AOE 117 | } 118 | if (this.canAttack() === false) { 119 | return; 120 | } 121 | const isUnderAggro = Collision.collisionMath( 122 | this.location.x, 123 | this.location.y, 124 | this.size, 125 | this.aggro.location.x, 126 | this.aggro.location.y, 127 | 1, 128 | ); 129 | this.attackFeedback = AttackIndicators.NONE; 130 | 131 | const aggroPoint = LocationUtils.closestPointTo(this.location.x, this.location.y, this.aggro); 132 | if ( 133 | !isUnderAggro && 134 | Pathing.dist(this.location.x, this.location.y, aggroPoint.x, aggroPoint.y) <= this.attackRange && 135 | this.attackDelay <= 0 136 | ) { 137 | this.attack() && this.didAttack(); 138 | } 139 | } 140 | 141 | create3dModel() { 142 | return GLTFModel.forRenderable(this, NibblerModel); 143 | } 144 | 145 | override get attackAnimationId() { 146 | return 2; 147 | } 148 | 149 | override get deathAnimationId() { 150 | return 4; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /assets.md: -------------------------------------------------------------------------------- 1 | ## Asset Pipeline 2 | 3 | **All assets are property of Jagex.** This tool is designed to assist players in overcoming difficult PVM encounters and as part of that, being faithful visually to the game is important. 4 | 5 | ## Sounds 6 | 7 | Currently using https://github.com/lequietriot/Old-School-RuneScape-Cache-Tools. Sound IDs are just grabbed using Visual Sound Plugin or Runelite dev mode. 8 | 9 | ## Optimising Models 10 | 11 | Install the [gltf-transform CLI](https://gltf-transform.dev/) using: 12 | 13 | npm install --global @gltf-transform/cli 14 | 15 | Then in the directory that contains GLTF files: 16 | 17 | for file in *.gltf; do 18 | gltf-transform optimize --compress meshopt $file $(echo $file | sed 's/\.gltf$/\.glb/') 19 | done 20 | 21 | ## Scene models 22 | 23 | Currently using a branch of [OSRS-Environment-Exporter](https://github.com/Supalosa/OSRS-Environment-Exporter/pull/1) with hardcoded overrides for the Inferno region to remove ground clutter and clear the space around Zuk. 24 | 25 | ## Other models 26 | 27 | Using Dezinator's `osrscachereader` at https://github.com/Dezinater/osrscachereader: 28 | 29 | ### Player models 30 | 31 | npm run cmd modelBuilder item 26684,27235,27238,27241,26235,28902,13237,22249,12926,20997,11959,25865,23975,23979,23971,7462,22109,21021,21024 maleModel0,maleModel1 anim 808,819,824,820,822,821,426,5061,7618 name player split 32 | 33 | where: 34 | 35 | - 26684 # tzkal slayer helmet 36 | - 27235 # masori mask (f) 37 | - 27238 # masori body (f) 38 | - 27241 # masori legs (f) 39 | - 26235 # zaryte vambracess 40 | - 28902 # dizana's max cape (l) 41 | - 13237 # pegasian boots 42 | - 22249 # anguish (or) 43 | - 20997 # twisted bow 44 | - 12926 # toxic blowpipe 45 | - 11959 # black chinchompa 46 | - 25865 # bow of faerdhinen 47 | - 23975 # crystal body 48 | - 23979 # crystal legs 49 | - 23971 # crystal helm 50 | - 7462 # barrows gloves 51 | - 22109 # ava's assembler 52 | - 21021 # ancestral top (buggy) 53 | - 21024 # ancestral bottom (buggy) 54 | 55 | 56 | - 808 # idle 57 | - 819 # walk 58 | - 824 # run 59 | - 820 # rotate 180 60 | - 822 # strafe left 61 | - 821 # strafe right 62 | - 426 # fire bow 63 | - 5061 # fire blowpipe 64 | - 7618 # throw chinchompa 65 | 66 | ### NPC models 67 | 68 | # Zuk: Idle, Fire, Flinch, Die 69 | npm run cmd modelBuilder npc 7706 anim 7564,7566,7565,7562 name zuk 70 | 71 | # Ranger: Idle, Walk, Range, Melee, Die, Flinch 72 | npm run cmd modelBuilder npc 7698 anim 7602,7603,7605,7604,7606,7607 name ranger 73 | 74 | # Mager: Idle, Walk, Mage, Revive Melee, Die 75 | npm run cmd modelBuilder npc 7699 anim 7609,7608,7610,7611,7612,7613 name mager 76 | 77 | # Nibbler: Idle, Walk, Attack, Flinch, Die 78 | npm run cmd modelBuilder npc 7691 anim 7573,7572,7574,7575,7676 name nibbler 79 | 80 | # Bat: Idle/Walk, Attack, Flinch, Die 81 | npm run cmd modelBuilder npc 7692 anim 7577,7578,7579,7580 name bat 82 | 83 | # Blob: Idle/Walk, Attack, Flinch, Die 84 | npm run cmd modelBuilder npc 7693 anim 7586,7586,7581,7582,7583,7585,7584 name blob 85 | 86 | # Meleer: Idle, Walk, Attack, Down, Up, Flinch, Die 87 | npm run cmd modelBuilder npc 7697 anim 7595,7596,7597,7600,7601,7598,7599 name meleer 88 | 89 | # Jad: Idle, Walk, Mage, Range, Melee, Flinch, Die 90 | npm run cmd modelBuilder npc 7700 anim 7589,7588,7592,7593,7590,7591,7594 name jad 91 | 92 | # Shield: Idle, Die 93 | npm run cmd modelBuilder npc 7707 anim 7567,7569 name shield 94 | 95 | ### Spotanim models 96 | 97 | npm run cmd modelBuilder spotanim 448 name jad_mage_front 98 | npm run cmd modelBuilder spotanim 449 name jad_mage_middle 99 | npm run cmd modelBuilder spotanim 450 name jad_mage_rear 100 | 101 | npm run cmd modelBuilder spotanim 451 name jad_range 102 | 103 | npm run cmd modelBuilder spotanim 1120 name dragon_arrow 104 | npm run cmd modelBuilder spotanim 1122 name dragon_dart 105 | npm run cmd modelBuilder spotanim 1272 name black_chinchompa_projectile 106 | 107 | npm run cmd modelBuilder spotanim 1382 name bat_projectile 108 | npm run cmd modelBuilder spotanim 1378 name blob_range_projectile 109 | npm run cmd modelBuilder spotanim 1380 name blob_mage_projectile 110 | 111 | npm run cmd modelBuilder spotanim 1376 name mage_projectile 112 | npm run cmd modelBuilder spotanim 1377 name range_projectile 113 | npm run cmd modelBuilder spotanim 1375 name zuk_projectile 114 | 115 | # these look terrible with normal optimisation so we do this 116 | for file in tekton_meteor*.gltf; do 117 | gltf-transform optimize --simplify false --compress meshopt $file $(echo $file | sed 's/\.gltf$/\.glb/') 118 | done 119 | npm run cmd modelBuilder spotanim 660 name tekton_meteor 120 | npm run cmd modelBuilder spotanim 659 name tekton_meteor_splat 121 | 122 | sounds 123 | range and mage ATTACK sound 598 124 | death 598 125 | 126 | zanik rez 1095 sound -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalImKot.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, Mob, EntityNames, MeleeWeapon, Sound, UnitBonuses, Location, Random, Collision, UnitTypes, Player, GLTFModel } from "osrs-sdk"; 4 | 5 | import MeleerImage from "../../assets/images/meleer.png"; 6 | import MeleerSound from "../../assets/sounds/meleer.ogg"; 7 | import { InfernoMobDeathStore } from "../InfernoMobDeathStore"; 8 | 9 | const MeleerModel = Assets.getAssetUrl("models/7697_33010.glb"); 10 | const HitSound = Assets.getAssetUrl("assets/sounds/inferno_melee_dmg.ogg"); 11 | 12 | export class JalImKot extends Mob { 13 | private digSequenceTime = 0; 14 | private digLocation: Location = { x: 0, y: 0 }; 15 | private digCount = 0; 16 | 17 | mobName() { 18 | return EntityNames.JAL_IM_KOT; 19 | } 20 | 21 | get combatLevel() { 22 | return 240; 23 | } 24 | 25 | dead() { 26 | super.dead(); 27 | InfernoMobDeathStore.npcDied(this); 28 | } 29 | 30 | setStats() { 31 | this.stunned = 1; 32 | 33 | this.weapons = { 34 | slash: new MeleeWeapon({ 35 | sound: new Sound(MeleerSound, 0.6) 36 | }), 37 | }; 38 | 39 | // non boosted numbers 40 | this.stats = { 41 | attack: 210, 42 | strength: 290, 43 | defence: 120, 44 | range: 220, 45 | magic: 120, 46 | hitpoint: 75, 47 | }; 48 | 49 | // with boosts 50 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 51 | } 52 | 53 | get bonuses(): UnitBonuses { 54 | return { 55 | attack: { 56 | stab: 0, 57 | slash: 0, 58 | crush: 0, 59 | magic: 0, 60 | range: 0, 61 | }, 62 | defence: { 63 | stab: 65, 64 | slash: 65, 65 | crush: 65, 66 | magic: 30, 67 | range: 5, 68 | }, 69 | other: { 70 | meleeStrength: 40, 71 | rangedStrength: 0, 72 | magicDamage: 0, 73 | prayer: 0, 74 | }, 75 | }; 76 | } 77 | 78 | hitSound(damaged) { 79 | return new Sound(HitSound, 0.25); 80 | } 81 | 82 | get attackSpeed() { 83 | return 4; 84 | } 85 | 86 | attackStyleForNewAttack() { 87 | return "slash"; 88 | } 89 | 90 | get attackRange() { 91 | return 1; 92 | } 93 | 94 | get size() { 95 | return 4; 96 | } 97 | 98 | get image() { 99 | return MeleerImage; 100 | } 101 | 102 | get color() { 103 | return "#ACFF5633"; 104 | } 105 | 106 | attackAnimation(tickPercent: number, context) { 107 | context.transform(1, 0, Math.sin(-tickPercent * Math.PI * 2) / 2, 1, 0, 0); 108 | } 109 | 110 | movementStep() { 111 | super.movementStep(); 112 | if (!this.hasLOS && !this.digSequenceTime) { 113 | if ((this.attackDelay <= -38 && Random.get() < 0.1) || this.attackDelay <= -50) { 114 | this.startDig(); 115 | this.playAnimation(3); 116 | } 117 | } 118 | if (this.digSequenceTime && --this.digSequenceTime === 0) { 119 | this.endDig(); 120 | this.playAnimation(4); 121 | } 122 | } 123 | 124 | startDig() { 125 | if (!this.aggro) { 126 | return; 127 | } 128 | this.freeze(6); 129 | this.digSequenceTime = 6; 130 | this.digCount++; 131 | if ( 132 | !Collision.collidesWithAnyEntities(this.region, this.aggro.location.x - 3, this.aggro.location.y + 3, this.size) 133 | ) { 134 | this.digLocation = { 135 | x: this.aggro.location.x - this.size + 1, 136 | y: this.aggro.location.y + this.size - 1, 137 | }; 138 | } else if ( 139 | !Collision.collidesWithAnyEntities(this.region, this.aggro.location.x, this.aggro.location.y, this.size) 140 | ) { 141 | this.digLocation = { 142 | x: this.aggro.location.x, 143 | y: this.aggro.location.y, 144 | }; 145 | } else if ( 146 | !Collision.collidesWithAnyEntities(this.region, this.aggro.location.x - 3, this.aggro.location.y, this.size) 147 | ) { 148 | this.digLocation = { 149 | x: this.aggro.location.x - this.size + 1, 150 | y: this.aggro.location.y, 151 | }; 152 | } else if ( 153 | !Collision.collidesWithAnyEntities(this.region, this.aggro.location.x, this.aggro.location.y + 3, this.size) 154 | ) { 155 | this.digLocation = { 156 | x: this.aggro.location.x, 157 | y: this.aggro.location.y + this.size - 1, 158 | }; 159 | } else { 160 | this.digLocation = { 161 | x: this.aggro.location.x - 1, 162 | y: this.aggro.location.y + 1, 163 | }; 164 | } 165 | this.perceivedLocation = this.location; 166 | } 167 | 168 | endDig() { 169 | if (this.aggro.type === UnitTypes.PLAYER) { 170 | const player = this.aggro as Player; 171 | if (player.aggro === this) { 172 | player.interruptCombat(); 173 | } 174 | } 175 | this.attackDelay = 6; 176 | this.freeze(2); 177 | this.location = this.digLocation; 178 | this.perceivedLocation = this.location; 179 | } 180 | 181 | create3dModel() { 182 | return GLTFModel.forRenderable(this, MeleerModel); 183 | } 184 | 185 | get deathAnimationLength() { 186 | return 6; 187 | } 188 | 189 | get attackAnimationId() { 190 | return 2; 191 | } 192 | 193 | override get deathAnimationId() { 194 | return 6; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalAk.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Assets, Mob, MeleeWeapon, MagicWeapon, Sound, RangedWeapon, UnitBonuses, Random, AttackIndicators, Unit, Viewport, GLTFModel, EntityNames, Trainer } from "osrs-sdk"; 3 | 4 | import BlobImage from "../../assets/images/blob.png"; 5 | import BlobSound from "../../assets/sounds/blob.ogg"; 6 | 7 | import { JalAkRekKet } from "./JalAkRekKet"; 8 | import { JalAkRekMej } from "./JalAkRekMej"; 9 | import { JalAkRekXil } from "./JalAkRekXil"; 10 | import { InfernoMobDeathStore } from "../InfernoMobDeathStore"; 11 | 12 | const BlobModel = Assets.getAssetUrl("models/7693_33001.glb"); 13 | 14 | export class JalAk extends Mob { 15 | playerPrayerScan?: string = null; 16 | 17 | mobName() { 18 | return EntityNames.JAL_AK; 19 | } 20 | 21 | get combatLevel() { 22 | return 165; 23 | } 24 | 25 | dead() { 26 | super.dead(); 27 | InfernoMobDeathStore.npcDied(this); 28 | } 29 | 30 | setStats() { 31 | this.stunned = 1; 32 | 33 | this.weapons = { 34 | crush: new MeleeWeapon(), 35 | magic: new MagicWeapon({ 36 | sound: new Sound(BlobSound) 37 | }), 38 | range: new RangedWeapon({ 39 | sound: new Sound(BlobSound) 40 | }), 41 | }; 42 | 43 | // non boosted numbers 44 | this.stats = { 45 | attack: 160, 46 | strength: 160, 47 | defence: 95, 48 | range: 160, 49 | magic: 160, 50 | hitpoint: 40, 51 | }; 52 | 53 | // with boosts 54 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 55 | } 56 | 57 | get bonuses(): UnitBonuses { 58 | return { 59 | attack: { 60 | stab: 0, 61 | slash: 0, 62 | crush: 0, 63 | magic: 45, 64 | range: 40, 65 | }, 66 | defence: { 67 | stab: 25, 68 | slash: 25, 69 | crush: 25, 70 | magic: 25, 71 | range: 25, 72 | }, 73 | other: { 74 | meleeStrength: 45, 75 | rangedStrength: 45, 76 | magicDamage: 1.0, 77 | prayer: 0, 78 | }, 79 | }; 80 | } 81 | // Since blobs attack on a 6 tick cycle, but these mechanics are odd, i set the 82 | // attack speed to 3. The attack code exits early during a scan, so it always is 83 | // double the cooldown between actual attacks. 84 | get attackSpeed() { 85 | return 3; 86 | } 87 | 88 | get attackRange() { 89 | return 15; 90 | } 91 | 92 | get size() { 93 | return 3; 94 | } 95 | 96 | get height() { 97 | return 2; 98 | } 99 | 100 | get image() { 101 | return BlobImage; 102 | } 103 | 104 | attackAnimation(tickPercent: number, context) { 105 | context.scale(1 + Math.sin(tickPercent * Math.PI) / 4, 1 - Math.sin(tickPercent * Math.PI) / 4); 106 | } 107 | 108 | shouldShowAttackAnimation() { 109 | return this.attackDelay === this.attackSpeed && this.playerPrayerScan === null; 110 | } 111 | 112 | attackStyleForNewAttack() { 113 | if (this.playerPrayerScan !== "magic" && this.playerPrayerScan !== "range") { 114 | return Random.get() < 0.5 ? "magic" : "range"; 115 | } 116 | return this.playerPrayerScan === "magic" ? "range" : "magic"; 117 | } 118 | 119 | canMeleeIfClose() { 120 | return "crush" as const; 121 | } 122 | 123 | magicMaxHit() { 124 | return 29; 125 | } 126 | 127 | attackIfPossible() { 128 | this.attackFeedback = AttackIndicators.NONE; 129 | 130 | this.hadLOS = this.hasLOS; 131 | this.setHasLOS(); 132 | 133 | if (this.canAttack() === false) { 134 | return; 135 | } 136 | 137 | this.attackStyle = this.attackStyleForNewAttack(); 138 | 139 | // Scan when appropriate 140 | if (this.hasLOS && (!this.hadLOS || (!this.playerPrayerScan && this.attackDelay <= 0))) { 141 | // we JUST gained LoS, or we are properly queued up for the next scan 142 | const unit = this.aggro as Unit; 143 | const overhead = unit.prayerController.overhead(); 144 | this.playerPrayerScan = overhead ? overhead.feature() : "none"; 145 | this.attackFeedback = AttackIndicators.SCAN; 146 | 147 | this.attackDelay = this.attackSpeed; 148 | return; 149 | } 150 | 151 | // Perform attack. Blobs can hit through LoS if they got a scan. 152 | if (this.playerPrayerScan && this.attackDelay <= 0) { 153 | this.attack() && this.didAttack(); 154 | this.playerPrayerScan = null; 155 | } 156 | } 157 | 158 | removedFromWorld() { 159 | const player = Trainer.player; 160 | const xil = new JalAkRekXil( 161 | this.region, 162 | { x: this.location.x + 1, y: this.location.y - 1 }, 163 | { aggro: player, cooldown: 4 }, 164 | ); 165 | this.region.addMob(xil as Mob); 166 | 167 | const ket = new JalAkRekKet(this.region, this.location, { 168 | aggro: player, 169 | cooldown: 4, 170 | }); 171 | this.region.addMob(ket as Mob); 172 | 173 | const mej = new JalAkRekMej( 174 | this.region, 175 | { x: this.location.x + 2, y: this.location.y - 2 }, 176 | { aggro: player, cooldown: 4 }, 177 | ); 178 | this.region.addMob(mej as Mob); 179 | } 180 | 181 | create3dModel() { 182 | return GLTFModel.forRenderable(this, BlobModel); 183 | } 184 | 185 | override get attackAnimationId() { 186 | return this.attackStyle === "magic" ? 2 : 4; 187 | } 188 | 189 | override get deathAnimationId() { 190 | return 3; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/content/inferno/js/ZukShield.ts: -------------------------------------------------------------------------------- 1 | import { Assets, Mob, ImageLoader, Location, Projectile, Random, LineOfSightMask, Region, UnitOptions, UnitBonuses, DelayedAction, Unit, CollisionType, Settings, GLTFModel, EntityNames } from "osrs-sdk"; 2 | import { UnitStats } from "osrs-sdk/lib/src/sdk/UnitStats"; 3 | 4 | import { find } from "lodash"; 5 | import { JalXil } from "./mobs/JalXil"; 6 | 7 | 8 | const MissSplat = Assets.getAssetUrl("assets/images/hitsplats/miss.png"); 9 | const DamageSplat = Assets.getAssetUrl("assets/images/hitsplats/damage.png"); 10 | 11 | const ShieldModel = Assets.getAssetUrl("models/7707_33036.glb"); 12 | 13 | export class ZukShield extends Mob { 14 | incomingProjectiles: Projectile[] = []; 15 | missedHitsplatImage: HTMLImageElement; 16 | damageHitsplatImage: HTMLImageElement; 17 | stats: UnitStats; 18 | currentStats: UnitStats; 19 | 20 | movementDirection: boolean = Random.get() < 0.5 ? true : false; 21 | 22 | get lineOfSight() { 23 | return LineOfSightMask.NONE; 24 | } 25 | 26 | constructor(region: Region, location: Location, options: UnitOptions) { 27 | super(region, location, options); 28 | 29 | this.freeze(1); 30 | this.missedHitsplatImage = ImageLoader.createImage(MissSplat); 31 | this.damageHitsplatImage = ImageLoader.createImage(DamageSplat); 32 | 33 | // non boosted numbers 34 | this.stats = { 35 | attack: 0, 36 | strength: 0, 37 | defence: 0, 38 | range: 0, 39 | magic: 0, 40 | hitpoint: 600, 41 | }; 42 | 43 | // with boosts 44 | this.currentStats = { 45 | attack: 0, 46 | strength: 0, 47 | defence: 0, 48 | range: 0, 49 | magic: 0, 50 | hitpoint: 600, 51 | }; 52 | } 53 | 54 | get bonuses(): UnitBonuses { 55 | return { 56 | attack: { 57 | stab: 0, 58 | slash: 0, 59 | crush: 0, 60 | magic: 0, 61 | range: 0, 62 | }, 63 | defence: { 64 | stab: 0, 65 | slash: 0, 66 | crush: 0, 67 | magic: 0, 68 | range: 0, 69 | }, 70 | other: { 71 | meleeStrength: 0, 72 | rangedStrength: 0, 73 | magicDamage: 0, 74 | prayer: 0, 75 | }, 76 | targetSpecific: { 77 | undead: 0, 78 | slayer: 0, 79 | }, 80 | }; 81 | } 82 | 83 | dead() { 84 | super.dead(); 85 | this.dying = 3; 86 | DelayedAction.registerDelayedAction( 87 | new DelayedAction(() => { 88 | this.region.removeMob(this); 89 | const ranger = find(this.region.mobs, (mob: Mob) => { 90 | return mob.mobName() === EntityNames.JAL_XIL; 91 | }) as JalXil; 92 | if (ranger) { 93 | ranger.setAggro(this.aggro as Unit); 94 | } 95 | const mager = find(this.region.mobs, (mob: Mob) => { 96 | return mob.mobName() === EntityNames.JAL_ZEK; 97 | }) as JalXil; 98 | if (mager) { 99 | mager.setAggro(this.aggro as Unit); 100 | } 101 | }, 2), 102 | ); 103 | } 104 | 105 | override visible() { 106 | // always visible, even during countdown 107 | return true; 108 | } 109 | 110 | get drawOutline() { 111 | return false; 112 | } 113 | 114 | contextActions() { 115 | return []; 116 | } 117 | mobName() { 118 | return EntityNames.INFERNO_SHIELD; 119 | } 120 | 121 | get selectable() { 122 | return false; 123 | } 124 | 125 | canBeAttacked() { 126 | return false; 127 | } 128 | movementStep() { 129 | this.processIncomingAttacks(); 130 | 131 | this.perceivedLocation = { x: this.location.x, y: this.location.y }; 132 | 133 | if (this.frozen <= 0) { 134 | if (this.movementDirection) { 135 | this.location.x++; 136 | } else { 137 | this.location.x--; 138 | } 139 | if (this.location.x < 11) { 140 | this.freeze(5); 141 | this.movementDirection = !this.movementDirection; 142 | } 143 | if (this.location.x > 35) { 144 | this.freeze(5); 145 | this.movementDirection = !this.movementDirection; 146 | } 147 | } 148 | 149 | if (this.currentStats.hitpoint <= 0) { 150 | return this.dead(); 151 | } 152 | } 153 | 154 | drawHitsplat(projectile: Projectile): boolean { 155 | return projectile.attackStyle !== "typeless"; 156 | } 157 | 158 | get size() { 159 | return 5; 160 | } 161 | 162 | get color() { 163 | return "#FF7300"; 164 | } 165 | 166 | get collisionType() { 167 | return CollisionType.NONE; 168 | } 169 | 170 | entityName() { 171 | return EntityNames.INFERNO_SHIELD; 172 | } 173 | 174 | canMove() { 175 | return false; 176 | } 177 | 178 | attackIfPossible() { 179 | // Shield can't attack. 180 | } 181 | 182 | getPerceivedRotation(tickPercent: any) { 183 | return -Math.PI / 2; 184 | } 185 | 186 | drawUnderTile() { 187 | this.region.context.fillStyle = this.color; 188 | // Draw mob 189 | this.region.context.fillRect( 190 | -(3 * Settings.tileSize) / 2, 191 | -(3 * Settings.tileSize) / 2, 192 | 3 * Settings.tileSize, 193 | 3 * Settings.tileSize, 194 | ); 195 | } 196 | 197 | create3dModel() { 198 | return GLTFModel.forRenderable(this, ShieldModel); 199 | } 200 | 201 | get animationIndex() { 202 | return 0; // idle 203 | } 204 | 205 | override get deathAnimationId() { 206 | return 1; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Settings, Region, World, Viewport, MapController, TileMarker, Assets, Location, Chrome, ImageLoader, Trainer, ControlPanelController } from "osrs-sdk"; 4 | 5 | import NewRelicBrowser from "new-relic-browser"; 6 | import { InfernoRegion } from "./content/inferno/js/InfernoRegion"; 7 | import { InfernoSettings } from "./content/inferno/js/InfernoSettings"; 8 | 9 | const SpecialAttackBarBackground = Assets.getAssetUrl("assets/images/attackstyles/interface/special_attack_background.png"); 10 | 11 | declare global { 12 | interface Window { 13 | newrelic: typeof NewRelicBrowser; 14 | } 15 | } 16 | 17 | Settings.readFromStorage(); 18 | InfernoSettings.readFromStorage(); 19 | 20 | // Choose the region based on the URL. 21 | const AVAILABLE_REGIONS = { 22 | 'inferno.html': new InfernoRegion(), 23 | }; 24 | const DEFAULT_REGION_PATH = 'inferno.html'; 25 | 26 | const regionName = window.location.pathname.split('/').pop(); 27 | const selectedRegion: Region = (regionName in AVAILABLE_REGIONS) ? AVAILABLE_REGIONS[regionName] : AVAILABLE_REGIONS[DEFAULT_REGION_PATH]; 28 | 29 | // Create world 30 | const world = new World(); 31 | world.getReadyTimer = 6; 32 | selectedRegion.world = world; 33 | world.addRegion(selectedRegion); 34 | 35 | // Initialise UI 36 | document.getElementById('sidebar_content').innerHTML = selectedRegion.getSidebarContent(); 37 | 38 | document.getElementById("reset").addEventListener("click", () => { 39 | Trainer.reset(); 40 | }); 41 | 42 | document.getElementById("settings").addEventListener("click", () => { 43 | ControlPanelController.controller.setActiveControl('SETTINGS'); 44 | }); 45 | 46 | const tileMarkerColor = document.getElementById("tileMarkerColor") as HTMLInputElement; 47 | tileMarkerColor.addEventListener("input", () => { 48 | Settings.tileMarkerColor = tileMarkerColor.value; 49 | TileMarker.onSetColor(Settings.tileMarkerColor); 50 | Settings.persistToStorage(); 51 | }, false); 52 | tileMarkerColor.value = Settings.tileMarkerColor; 53 | 54 | const { player } = selectedRegion.initialiseRegion(); 55 | 56 | Viewport.setupViewport(selectedRegion); 57 | Viewport.viewport.setPlayer(player); 58 | 59 | ImageLoader.onAllImagesLoaded(() => { 60 | MapController.controller.updateOrbsMask(player.currentStats, player.stats); 61 | }); 62 | TileMarker.loadAll(selectedRegion); 63 | 64 | 65 | player.perceivedLocation = player.location; 66 | player.destinationLocation = player.location; 67 | /// ///////////////////////////////////////////////////////// 68 | // UI controls 69 | 70 | ImageLoader.onAllImagesLoaded(() => 71 | MapController.controller.updateOrbsMask(Trainer.player.currentStats, Trainer.player.stats), 72 | ); 73 | 74 | ImageLoader.onAllImagesLoaded(() => { 75 | drawAssetLoadingBar(loadingAssetProgress); 76 | imagesReady = true; 77 | checkStart(); 78 | }); 79 | 80 | const interval = setInterval(() => { 81 | ImageLoader.checkImagesLoaded(interval); 82 | }, 50); 83 | 84 | Assets.onAllAssetsLoaded(() => { 85 | // renders a single frame 86 | Viewport.viewport.initialise().then(() => { 87 | console.log("assets are preloaded"); 88 | assetsPreloaded = true; 89 | checkStart(); 90 | }); 91 | }); 92 | 93 | function drawAssetLoadingBar(loadingProgress: number) { 94 | const specialAttackBarBackground = ImageLoader.createImage(SpecialAttackBarBackground); 95 | const { width: canvasWidth, height: canvasHeight } = Chrome.size(); 96 | const canvas = document.getElementById("world") as HTMLCanvasElement; 97 | canvas.width = canvasWidth; 98 | canvas.height = canvasHeight; 99 | const context = canvas.getContext("2d"); 100 | context.clearRect(0, 0, canvas.width, canvas.height); 101 | context.fillStyle = "#FFFF00"; 102 | context.font = "32px OSRS"; 103 | context.textAlign = "center"; 104 | context.fillText(`Loading models: ${Math.floor(loadingProgress * 100)}%`, canvas.width / 2, canvas.height / 2); 105 | const scale = 2; 106 | const left = canvasWidth / 2 - (specialAttackBarBackground.width * scale) / 2; 107 | const top = canvasHeight / 2 + 20; 108 | const width = specialAttackBarBackground.width * scale; 109 | const height = specialAttackBarBackground.height * scale; 110 | context.drawImage(specialAttackBarBackground, left, top, width, height); 111 | context.fillStyle = "#730606"; 112 | context.fillRect(left + 2 * scale, top + 6 * scale, width - 4 * scale, height - 12 * scale); 113 | context.fillStyle = "#397d3b"; 114 | context.fillRect(left + 2 * scale, top + 6 * scale, (width - 4 * scale) * loadingProgress, height - 12 * scale); 115 | context.fillStyle = "#000000"; 116 | context.globalAlpha = 0.5; 117 | context.strokeRect(left + 2 * scale, top + 6 * scale, width - 4 * scale, height - 12 * scale); 118 | context.globalAlpha = 1; 119 | } 120 | 121 | let loadingAssetProgress = 0.0; 122 | drawAssetLoadingBar(loadingAssetProgress); 123 | 124 | Assets.onAssetProgress((loaded, total) => { 125 | loadingAssetProgress = loaded / total; 126 | drawAssetLoadingBar(loadingAssetProgress); 127 | }); 128 | 129 | const assets2 = setInterval(() => { 130 | Assets.checkAssetsLoaded(assets2); 131 | }, 50); 132 | 133 | let imagesReady = false; 134 | let assetsPreloaded = false; 135 | let started = false; 136 | 137 | function checkStart() { 138 | if (!started && imagesReady && assetsPreloaded) { 139 | started = true; 140 | // Start the engine 141 | world.startTicking(); 142 | } 143 | } 144 | 145 | /// ///////////////////////////////////////////////////////// 146 | 147 | window.newrelic.addRelease("inferno-trainer", process.env.COMMIT_REF); 148 | 149 | // UI disclaimer 150 | const topHeaderContainer = document.getElementById("disclaimer_panel"); 151 | topHeaderContainer.innerHTML = 152 | 'Work in progress.
' + 153 | topHeaderContainer.innerHTML; 154 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalMejJak.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, ProjectileOptions, Weapon, Unit, AttackBonuses, Random, ArcProjectileMotionInterpolator, Projectile, DelayedAction, Mob, UnitBonuses, UnitTypes, Model, GLTFModel, EntityNames } from "osrs-sdk"; 4 | 5 | import JalMejJakImage from "../../assets/images/Jal-MejJak.png"; 6 | import { InfernoHealerSpark } from "../InfernoHealerSpark"; 7 | 8 | const HealerModel = Assets.getAssetUrl("models/zuk_healer.glb"); 9 | const Spark = Assets.getAssetUrl("models/tekton_meteor.glb"); 10 | 11 | const HEAL_PROJECTILE_SETTINGS: ProjectileOptions = { 12 | model: Spark, 13 | modelScale: 1 / 128, 14 | setDelay: 3, 15 | } 16 | class HealWeapon extends Weapon { 17 | calculateHitDelay(distance: number) { 18 | return 3; 19 | } 20 | 21 | attack(from: Unit, to: Unit, bonuses: AttackBonuses = {}, options: ProjectileOptions): boolean { 22 | this.damage = -Math.floor(Random.get() * 25); 23 | this.registerProjectile(from, to, bonuses, HEAL_PROJECTILE_SETTINGS); 24 | return true; 25 | } 26 | } 27 | 28 | class InfernoSparkWeapon extends Weapon { 29 | // dummy weapon for the projectile (that this needs to exist means we need to fix the Projectile api) 30 | } 31 | 32 | const AOE_PROJECTILE_SETTINGS: ProjectileOptions = { 33 | model: Spark, 34 | modelScale: 1 / 128, 35 | motionInterpolator: new ArcProjectileMotionInterpolator(3), 36 | setDelay: 4, 37 | visualDelayTicks: 0, 38 | } 39 | 40 | class AoeWeapon extends Weapon { 41 | calculateHitDelay(distance: number) { 42 | return 1; 43 | } 44 | 45 | attack(from: Unit, to: Unit): boolean { 46 | const playerLocation = from.aggro.location; 47 | // make splat in 2 random spots and where the player is 48 | const limitedPlayerLocation = { 49 | x: Math.min(Math.max(from.location.x - 5, playerLocation.x), from.location.x + 4), 50 | y: playerLocation.y, 51 | z: 0, 52 | }; 53 | const spark2Location = { 54 | x: from.location.x + (Math.floor(Random.get() * 11) - 5), 55 | y: 14 + Math.floor(Random.get() * 4), 56 | z: 0, 57 | }; 58 | const spark3Location = { 59 | x: from.location.x + (Math.floor(Random.get() * 11) - 5), 60 | y: 14 + Math.floor(Random.get() * 4), 61 | z: 0, 62 | }; 63 | from.region.addProjectile(new Projectile(new InfernoSparkWeapon(), 0, from, limitedPlayerLocation, "magic", AOE_PROJECTILE_SETTINGS)) 64 | from.region.addProjectile(new Projectile(new InfernoSparkWeapon(), 0, from, spark2Location, "magic", AOE_PROJECTILE_SETTINGS)) 65 | from.region.addProjectile(new Projectile(new InfernoSparkWeapon(), 0, from, spark3Location, "magic", AOE_PROJECTILE_SETTINGS)) 66 | 67 | DelayedAction.registerDelayedAction( 68 | new DelayedAction(() => { 69 | const spark1 = new InfernoHealerSpark(from.region, limitedPlayerLocation, from, to); 70 | from.region.addEntity(spark1); 71 | const spark2 = new InfernoHealerSpark(from.region, spark2Location, from, to); 72 | from.region.addEntity(spark2); 73 | const spark3 = new InfernoHealerSpark(from.region, spark3Location, from, to); 74 | from.region.addEntity(spark3); 75 | }, 2), 76 | ); 77 | return true; 78 | } 79 | 80 | get isAreaAttack() { 81 | return true; 82 | } 83 | } 84 | 85 | const SPAWN_DELAY = 1; 86 | export class JalMejJak extends Mob { 87 | private lastAggro: Unit = null; 88 | 89 | mobName() { 90 | return EntityNames.JAL_MEJ_JAK; 91 | } 92 | 93 | get combatLevel() { 94 | return 250; 95 | } 96 | 97 | setHasLOS() { 98 | this.hasLOS = true; 99 | } 100 | 101 | setStats() { 102 | this.stunned = SPAWN_DELAY; 103 | this.weapons = { 104 | heal: new HealWeapon(), 105 | aoe: new AoeWeapon(), 106 | }; 107 | 108 | // non boosted numbers 109 | this.stats = { 110 | attack: 1, 111 | strength: 1, 112 | defence: 100, 113 | range: 1, 114 | magic: 1, 115 | hitpoint: 75, 116 | }; 117 | 118 | // with boosts 119 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 120 | } 121 | 122 | override addedToWorld() { 123 | this.playAnimation(2); 124 | } 125 | 126 | get bonuses(): UnitBonuses { 127 | return { 128 | attack: { 129 | stab: 0, 130 | slash: 0, 131 | crush: 0, 132 | magic: 0, 133 | range: 40, 134 | }, 135 | defence: { 136 | stab: 0, 137 | slash: 0, 138 | crush: 0, 139 | magic: 0, 140 | range: 0, 141 | }, 142 | other: { 143 | meleeStrength: 0, 144 | rangedStrength: 0, 145 | magicDamage: 1.0, 146 | prayer: 0, 147 | }, 148 | }; 149 | } 150 | 151 | get attackSpeed() { 152 | return 3; 153 | } 154 | 155 | get attackRange() { 156 | return 0; 157 | } 158 | 159 | get size() { 160 | return 1; 161 | } 162 | 163 | get image() { 164 | return JalMejJakImage; 165 | } 166 | 167 | shouldChangeAggro(projectile: Projectile) { 168 | return this.aggro != projectile.from && this.autoRetaliate; 169 | } 170 | 171 | attackStyleForNewAttack() { 172 | return this.aggro.type === UnitTypes.PLAYER ? "aoe" : "heal"; 173 | } 174 | 175 | canMove() { 176 | return false; 177 | } 178 | 179 | create3dModel(): Model { 180 | return GLTFModel.forRenderable(this, HealerModel, { scale: 1 / 128, verticalOffset: 1.0 }); 181 | } 182 | 183 | get outlineRenderOrder() { 184 | // to allow it to draw in front of the shield 185 | return 999; 186 | } 187 | 188 | get idlePoseId() { 189 | return 0; 190 | } 191 | 192 | get attackAnimationId() { 193 | return 1; 194 | } 195 | 196 | get deathAnimationId() { 197 | return 3; 198 | } 199 | 200 | override attackStep() { 201 | super.attackStep(); 202 | if (this.lastAggro && this.lastAggro != this.aggro) { 203 | this.nulledTicks = 2; 204 | } 205 | this.lastAggro = this.aggro; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoPillar.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Entity, Projectile, UnitBonuses, Region, Settings, DelayedAction, Model, ImageLoader, Location, EntityNames, UnitStats, BasicModel, Assets } from "osrs-sdk"; 3 | 4 | 5 | import { filter, remove } from "lodash"; 6 | 7 | const MissSplat = Assets.getAssetUrl("assets/images/hitsplats/miss.png"); 8 | const DamageSplat = Assets.getAssetUrl("assets/images/hitsplats/damage.png"); 9 | 10 | 11 | export class InfernoPillar extends Entity { 12 | incomingProjectiles: Projectile[] = []; 13 | missedHitsplatImage: HTMLImageElement; 14 | damageHitsplatImage: HTMLImageElement; 15 | stats: UnitStats; 16 | currentStats: UnitStats; 17 | bonuses: UnitBonuses; 18 | 19 | constructor(region: Region, point: Location) { 20 | super(region, point); 21 | 22 | this.missedHitsplatImage = ImageLoader.createImage(MissSplat); 23 | this.damageHitsplatImage = ImageLoader.createImage(DamageSplat); 24 | 25 | // non boosted numbers 26 | this.stats = { 27 | attack: 0, 28 | strength: 0, 29 | defence: 0, 30 | range: 0, 31 | magic: 0, 32 | hitpoint: 255, 33 | }; 34 | 35 | // with boosts 36 | this.currentStats = { 37 | attack: 0, 38 | strength: 0, 39 | defence: 0, 40 | range: 0, 41 | magic: 0, 42 | hitpoint: 255, 43 | }; 44 | 45 | this.bonuses = { 46 | attack: { 47 | stab: 0, 48 | slash: 0, 49 | crush: 0, 50 | magic: 0, 51 | range: 0, 52 | }, 53 | defence: { 54 | stab: 0, 55 | slash: 0, 56 | crush: 0, 57 | magic: 0, 58 | range: 0, 59 | }, 60 | other: { 61 | meleeStrength: 0, 62 | rangedStrength: 0, 63 | magicDamage: 0, 64 | prayer: 0, 65 | }, 66 | targetSpecific: { 67 | undead: 0, 68 | slayer: 0, 69 | }, 70 | }; 71 | } 72 | 73 | tick() { 74 | this.incomingProjectiles = filter( 75 | this.incomingProjectiles, 76 | (projectile: Projectile) => projectile.remainingDelay > -1, 77 | ); 78 | 79 | this.incomingProjectiles.forEach((projectile) => { 80 | projectile.remainingDelay--; 81 | if (projectile.remainingDelay <= 0) { 82 | this.currentStats.hitpoint -= projectile.damage; 83 | } 84 | }); 85 | this.currentStats.hitpoint = Math.max(0, this.currentStats.hitpoint); 86 | 87 | if (this.currentStats.hitpoint <= 0) { 88 | return this.dead(); 89 | } 90 | } 91 | 92 | draw() { 93 | this.region.context.fillStyle = "#000073"; 94 | 95 | this.region.context.fillRect( 96 | this.location.x * Settings.tileSize, 97 | (this.location.y - this.size + 1) * Settings.tileSize, 98 | this.size * Settings.tileSize, 99 | this.size * Settings.tileSize, 100 | ); 101 | } 102 | 103 | drawUILayer( 104 | tickPercent: number, 105 | screenPosition: Location, 106 | context: OffscreenCanvasRenderingContext2D, 107 | scale: number, 108 | hitsplatAbove) { 109 | context.save(); 110 | 111 | context.translate(screenPosition.x, screenPosition.y); 112 | 113 | if (Settings.rotated === "south") { 114 | context.rotate(Math.PI); 115 | } 116 | 117 | context.fillStyle = "red"; 118 | context.fillRect( 119 | (-this.size / 2) * Settings.tileSize, 120 | hitsplatAbove ? (-this.size / 2) * Settings.tileSize : 0, 121 | Settings.tileSize * this.size, 122 | 5, 123 | ); 124 | 125 | context.fillStyle = "lime"; 126 | const w = (this.currentStats.hitpoint / this.stats.hitpoint) * (Settings.tileSize * this.size); 127 | context.fillRect((-this.size / 2) * Settings.tileSize, hitsplatAbove ? (-this.size / 2) * Settings.tileSize : 0, w, 5); 128 | 129 | let projectileOffsets: number[][] = [ 130 | [0, 0], 131 | [0, -16], 132 | [-12, -8], 133 | [12, -8], 134 | ]; 135 | 136 | let projectileCounter = 0; 137 | this.incomingProjectiles.forEach((projectile) => { 138 | if (projectile.remainingDelay > 0) { 139 | return; 140 | } 141 | if (projectileCounter > 3) { 142 | return; 143 | } 144 | projectileCounter++; 145 | const image = projectile.damage === 0 ? this.missedHitsplatImage : this.damageHitsplatImage; 146 | if (!projectile.offsetX && !projectile.offsetY) { 147 | projectile.offsetX = projectileOffsets[0][0]; 148 | projectile.offsetY = projectileOffsets[0][1]; 149 | } 150 | 151 | projectileOffsets = remove(projectileOffsets, (offset: number[]) => { 152 | return offset[0] !== projectile.offsetX || offset[1] !== projectile.offsetY; 153 | }); 154 | 155 | const posMult = hitsplatAbove ? -1 : 1; 156 | 157 | context.drawImage( 158 | image, 159 | projectile.offsetX - 12, 160 | posMult * ((this.size + 1) * Settings.tileSize) / 2 - projectile.offsetY, 161 | 24, 162 | 23, 163 | ); 164 | context.fillStyle = "#FFFFFF"; 165 | context.font = "16px Stats_11"; 166 | context.textAlign = "center"; 167 | context.fillText( 168 | String(projectile.damage), 169 | projectile.offsetX, 170 | posMult * ((this.size + 1) * Settings.tileSize) / 2 - projectile.offsetY + 15, 171 | ); 172 | context.textAlign = "left"; 173 | }); 174 | context.restore(); 175 | } 176 | 177 | entityName() { 178 | return EntityNames.PILLAR; 179 | } 180 | 181 | get size() { 182 | return 3; 183 | } 184 | 185 | get height() { 186 | return 6; 187 | } 188 | 189 | get color() { 190 | return "#333333"; 191 | } 192 | 193 | dead() { 194 | this.dying = 2; 195 | DelayedAction.registerDelayedAction( 196 | new DelayedAction(() => { 197 | this.region.removeEntity(this); 198 | }, 2), 199 | ); 200 | // TODO: needs to AOE the nibblers around it 201 | } 202 | 203 | addProjectile(projectile: Projectile) { 204 | this.incomingProjectiles.push(projectile); 205 | } 206 | 207 | static addPillarsToWorld(region: Region, southPillar: boolean, westPillar: boolean, northPillar: boolean) { 208 | if (southPillar) { 209 | region.addEntity(new InfernoPillar(region, { x: 21, y: 37 })); 210 | } 211 | if (westPillar) { 212 | region.addEntity(new InfernoPillar(region, { x: 11, y: 23 })); 213 | } 214 | if (northPillar) { 215 | region.addEntity(new InfernoPillar(region, { x: 28, y: 21 })); 216 | } 217 | } 218 | 219 | create3dModel(): Model { 220 | return BasicModel.forRenderable(this); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /test/simulations/ZukLineOfSight.test.ts: -------------------------------------------------------------------------------- 1 | import { World, Mob, InvisibleMovementBlocker, Viewport, Player, TwistedBow, TestRegion } from "osrs-sdk"; 2 | 3 | import { Wall } from "../../src/content/inferno/js/Wall"; 4 | import { TzKalZuk } from "../../src/content/inferno/js/mobs/TzKalZuk"; 5 | 6 | // Zuk LOS (dragging behaviour) tests 7 | describe("player LOS in zuk fight", () => { 8 | let region: TestRegion; 9 | let world: World; 10 | 11 | let zuk: Mob; 12 | 13 | beforeEach(() => { 14 | region = new TestRegion(51, 57); 15 | world = new World(); 16 | region.world = world; 17 | world.addRegion(region); 18 | 19 | zuk = new TzKalZuk(region, { x: 22, y: 8 }, {}); 20 | region.addMob(zuk); 21 | 22 | // The arena still needs to be closed off to prevent the player from pathing "around" the walls. 23 | // Seems like this is most accurate if it's the exact size of the real arena. 24 | for (let x = 10; x < 41; x++) { 25 | region.addEntity(new InvisibleMovementBlocker(region, { x, y: 13 })); 26 | region.addEntity(new InvisibleMovementBlocker(region, { x, y: 44 })); 27 | } 28 | for (let y = 14; y < 44; y++) { 29 | region.addEntity(new InvisibleMovementBlocker(region, { x: 10, y })); 30 | region.addEntity(new InvisibleMovementBlocker(region, { x: 40, y })); 31 | } 32 | 33 | region.addEntity(new Wall(region, { x: 21, y: 8 })); 34 | region.addEntity(new Wall(region, { x: 21, y: 7 })); 35 | region.addEntity(new Wall(region, { x: 21, y: 6 })); 36 | region.addEntity(new Wall(region, { x: 21, y: 5 })); 37 | region.addEntity(new Wall(region, { x: 21, y: 4 })); 38 | region.addEntity(new Wall(region, { x: 21, y: 3 })); 39 | region.addEntity(new Wall(region, { x: 21, y: 2 })); 40 | region.addEntity(new Wall(region, { x: 21, y: 1 })); 41 | region.addEntity(new Wall(region, { x: 21, y: 0 })); 42 | region.addEntity(new Wall(region, { x: 29, y: 8 })); 43 | region.addEntity(new Wall(region, { x: 29, y: 7 })); 44 | region.addEntity(new Wall(region, { x: 29, y: 6 })); 45 | region.addEntity(new Wall(region, { x: 29, y: 5 })); 46 | region.addEntity(new Wall(region, { x: 29, y: 4 })); 47 | region.addEntity(new Wall(region, { x: 29, y: 3 })); 48 | region.addEntity(new Wall(region, { x: 29, y: 2 })); 49 | region.addEntity(new Wall(region, { x: 29, y: 1 })); 50 | region.addEntity(new Wall(region, { x: 29, y: 0 })); 51 | 52 | Viewport.setupViewport(region, true); 53 | }); 54 | 55 | test("player has line of sight at left safespot", () => { 56 | const player = new Player(region, { x: 14, y: 14 }); 57 | region.addPlayer(player); 58 | Viewport.viewport.setPlayer(player); 59 | 60 | const twistedBow = new TwistedBow(); 61 | twistedBow.inventoryLeftClick(player); 62 | 63 | player.setAggro(zuk); 64 | 65 | expect(player.location).toEqual({ x: 14, y: 14 }); 66 | expect(player.attackDelay).toBe(0); 67 | world.tickWorld(1); 68 | expect(player.attackDelay).toBe(twistedBow.attackSpeed); 69 | expect(player.location).toEqual({ x: 14, y: 14 }); 70 | }); 71 | 72 | test("player has line of sight at middle-left safespot", () => { 73 | const player = new Player(region, { x: 20, y: 14 }); 74 | region.addPlayer(player); 75 | Viewport.viewport.setPlayer(player); 76 | 77 | const twistedBow = new TwistedBow(); 78 | twistedBow.inventoryLeftClick(player); 79 | 80 | player.setAggro(zuk); 81 | 82 | expect(player.location).toEqual({ x: 20, y: 14 }); 83 | expect(player.attackDelay).toBe(0); 84 | world.tickWorld(1); 85 | expect(player.attackDelay).toBe(twistedBow.attackSpeed); 86 | expect(player.location).toEqual({ x: 20, y: 14 }); 87 | }); 88 | 89 | test("player has line of sight at middle-right safespot", () => { 90 | const player = new Player(region, { x: 30, y: 14 }); 91 | region.addPlayer(player); 92 | Viewport.viewport.setPlayer(player); 93 | 94 | const twistedBow = new TwistedBow(); 95 | twistedBow.inventoryLeftClick(player); 96 | 97 | player.setAggro(zuk); 98 | 99 | expect(player.location).toEqual({ x: 30, y: 14 }); 100 | expect(player.attackDelay).toBe(0); 101 | world.tickWorld(1); 102 | expect(player.attackDelay).toBe(twistedBow.attackSpeed); 103 | expect(player.location).toEqual({ x: 30, y: 14 }); 104 | }); 105 | 106 | test("player has line of sight at right safespot", () => { 107 | const player = new Player(region, { x: 36, y: 14 }); 108 | region.addPlayer(player); 109 | Viewport.viewport.setPlayer(player); 110 | 111 | const twistedBow = new TwistedBow(); 112 | twistedBow.inventoryLeftClick(player); 113 | 114 | player.setAggro(zuk); 115 | 116 | expect(player.location).toEqual({ x: 36, y: 14 }); 117 | expect(player.attackDelay).toBe(0); 118 | world.tickWorld(1); 119 | expect(player.attackDelay).toBe(twistedBow.attackSpeed); 120 | expect(player.location).toEqual({ x: 36, y: 14 }); 121 | }); 122 | 123 | test("player is dragged from dead tiles on left side", () => { 124 | const player = new Player(region, { x: 16, y: 14 }); 125 | region.addPlayer(player); 126 | Viewport.viewport.setPlayer(player); 127 | 128 | const twistedBow = new TwistedBow(); 129 | twistedBow.inventoryLeftClick(player); 130 | 131 | player.setAggro(zuk); 132 | 133 | expect(player.location).toEqual({ x: 16, y: 14 }); 134 | expect(player.attackDelay).toBe(0); 135 | world.tickWorld(); 136 | // The player should look like they are pathing towards this position. In reality 137 | // they gain LOS at x = 20. 138 | expect(player.pathTargetLocation).toEqual({ x: 22, y: 14 }); 139 | expect(player.attackDelay).toBe(-1); 140 | expect(player.location).toEqual({ x: 18, y: 14 }); 141 | world.tickWorld(); 142 | expect(player.attackDelay).toBe(twistedBow.attackSpeed); 143 | expect(player.location).toEqual({ x: 20, y: 14 }); 144 | }); 145 | 146 | test("player is dragged from dead tiles on right side", () => { 147 | const player = new Player(region, { x: 34, y: 14 }); 148 | region.addPlayer(player); 149 | Viewport.viewport.setPlayer(player); 150 | 151 | const twistedBow = new TwistedBow(); 152 | twistedBow.inventoryLeftClick(player); 153 | 154 | player.setAggro(zuk); 155 | 156 | expect(player.location).toEqual({ x: 34, y: 14 }); 157 | expect(player.attackDelay).toBe(0); 158 | world.tickWorld(); 159 | expect(player.pathTargetLocation).toEqual({ x: 28, y: 14 }); 160 | expect(player.attackDelay).toBe(-1); 161 | expect(player.location).toEqual({ x: 32, y: 14 }); 162 | world.tickWorld(); 163 | expect(player.attackDelay).toBe(twistedBow.attackSpeed); 164 | expect(player.location).toEqual({ x: 30, y: 14 }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoWaves.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Random, Region, Player, Entity, Mob, Collision, UnitOptions, Location, Unit } from "osrs-sdk"; 4 | 5 | import { shuffle } from "lodash"; 6 | 7 | import { JalMejRah } from "./mobs/JalMejRah"; 8 | import { JalAk } from "./mobs/JalAk"; 9 | import { JalZek } from "./mobs/JalZek"; 10 | import { JalImKot } from "./mobs/JalImKot"; 11 | import { JalNib } from "./mobs/JalNib"; 12 | import { JalXil } from "./mobs/JalXil"; 13 | 14 | export class InfernoWaves { 15 | static spawns = [ 16 | { x: 12, y: 19 }, 17 | { x: 33, y: 19 }, 18 | { x: 14, y: 25 }, 19 | { x: 34, y: 26 }, 20 | { x: 27, y: 31 }, 21 | { x: 16, y: 37 }, 22 | { x: 34, y: 39 }, 23 | { x: 12, y: 42 }, 24 | { x: 26, y: 42 }, 25 | ]; 26 | static shuffle(array) { 27 | let currentIndex = array.length, 28 | randomIndex; 29 | 30 | // While there remain elements to shuffle... 31 | while (currentIndex != 0) { 32 | // Pick a remaining element... 33 | randomIndex = Math.floor((Random.get() || Math.random()) * currentIndex); 34 | currentIndex--; 35 | 36 | // And swap it with the current element. 37 | [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; 38 | } 39 | 40 | return array; 41 | } 42 | 43 | static getRandomSpawns() { 44 | // Deep copy to prevent shared object references from corrupting the static array 45 | const originalSpawns = InfernoWaves.spawns.map(spawn => ({ x: spawn.x, y: spawn.y })); 46 | return InfernoWaves.shuffle(originalSpawns); 47 | } 48 | 49 | static spawn(region: Region, player: Player, randomPillar: Entity, spawns: Location[], wave: number) { 50 | const mobCounts = InfernoWaves.waves[wave - 1]; 51 | let mobs: Mob[] = []; 52 | let i = 0; 53 | Array(mobCounts[5]) 54 | .fill(0) 55 | .forEach(() => { 56 | const spawnLocation = spawns[i++]; 57 | mobs.push(new JalZek(region, spawnLocation, { aggro: player })); 58 | }); 59 | Array(mobCounts[4]) 60 | .fill(0) 61 | .forEach(() => { 62 | const spawnLocation = spawns[i++]; 63 | mobs.push(new JalXil(region, spawnLocation, { aggro: player })); 64 | }); 65 | Array(mobCounts[3]) 66 | .fill(0) 67 | .forEach(() => { 68 | const spawnLocation = spawns[i++]; 69 | mobs.push(new JalImKot(region, spawnLocation, { aggro: player })); 70 | }); 71 | Array(mobCounts[2]) 72 | .fill(0) 73 | .forEach(() => { 74 | const spawnLocation = spawns[i++]; 75 | mobs.push(new JalAk(region, spawnLocation, { aggro: player })); 76 | }); 77 | Array(mobCounts[1]) 78 | .fill(0) 79 | .forEach(() => { 80 | const spawnLocation = spawns[i++]; 81 | mobs.push(new JalMejRah(region, spawnLocation, { aggro: player })); 82 | }); 83 | 84 | mobs = mobs.concat(InfernoWaves.spawnNibblers(mobCounts[0], region, randomPillar)); 85 | return mobs; 86 | } 87 | 88 | static spawnEnduranceMode(region: Region, player: Player, concurrentSpawns: number, check = false) { 89 | let j = 0; 90 | 91 | const mobs: Mob[] = []; 92 | let randomSpawns = [ 93 | { x: 12, y: 19 }, 94 | { x: 33, y: 19 }, 95 | { x: 14, y: 25 }, 96 | { x: 34, y: 26 }, 97 | { x: 27, y: 31 }, 98 | { x: 16, y: 37 }, 99 | { x: 34, y: 39 }, 100 | { x: 12, y: 42 }, 101 | { x: 26, y: 42 }, 102 | ]; 103 | 104 | randomSpawns = shuffle(randomSpawns); 105 | 106 | for (let i = 0; i < concurrentSpawns; i++) { 107 | const mobTypes: (typeof Mob)[] = [JalZek, JalXil, JalImKot, JalAk, JalMejRah]; 108 | 109 | const randomType = mobTypes[Math.floor(Random.get() * 5)]; 110 | 111 | let randomSpawn = null; 112 | if (check) { 113 | do { 114 | randomSpawn = randomSpawns[j++]; 115 | } while (j < randomSpawns.length && Collision.collidesWithAnyMobs(region, randomSpawn.x, randomSpawn.y, 4)); 116 | } else { 117 | randomSpawn = randomSpawns[j++]; 118 | } 119 | if (randomSpawn) { 120 | mobs.push(new randomType(region, randomSpawn, { aggro: player })); 121 | } 122 | } 123 | 124 | return mobs; 125 | } 126 | 127 | static spawnNibblers(n: number, region: Region, pillar: Entity) { 128 | const mobs: Mob[] = []; 129 | const nibblerSpawns = shuffle([ 130 | { x: 8 + 11, y: 13 + 14 }, 131 | { x: 9 + 11, y: 13 + 14 }, 132 | { x: 10 + 11, y: 13 + 14 }, 133 | { x: 8 + 11, y: 12 + 14 }, 134 | { x: 9 + 11, y: 12 + 14 }, 135 | { x: 10 + 11, y: 12 + 14 }, 136 | { x: 8 + 11, y: 11 + 14 }, 137 | { x: 9 + 11, y: 11 + 14 }, 138 | { x: 10 + 11, y: 11 + 14 }, 139 | ]); 140 | 141 | const unknownPillar = pillar as unknown; 142 | 143 | // hack hack hack 144 | const options: UnitOptions = { aggro: unknownPillar as Unit /* TODO: || world.player */ }; 145 | 146 | Array(n) 147 | .fill(0) 148 | .forEach(() => mobs.push(new JalNib(region, nibblerSpawns.shift(), options))); 149 | return mobs; 150 | } 151 | 152 | // cba to convert this to any other format 153 | // nibblers, bats, blobs, melee, ranger, mager 154 | static waves = [ 155 | [3, 1, 0, 0, 0, 0], // 1 156 | [3, 2, 0, 0, 0, 0], 157 | [6, 0, 0, 0, 0, 0], 158 | [3, 0, 1, 0, 0, 0], 159 | [3, 1, 1, 0, 0, 0], 160 | [3, 2, 1, 0, 0, 0], 161 | [3, 0, 2, 0, 0, 0], // 7 162 | [6, 0, 0, 0, 0, 0], 163 | [3, 0, 0, 1, 0, 0], 164 | [3, 1, 0, 1, 0, 0], 165 | [3, 2, 0, 1, 0, 0], 166 | [3, 0, 1, 1, 0, 0], 167 | [3, 1, 1, 1, 0, 0], 168 | [3, 2, 1, 1, 0, 0], 169 | [3, 0, 2, 1, 0, 0], // 15 170 | [3, 0, 0, 2, 0, 0], 171 | [6, 0, 0, 0, 0, 0], // 17 172 | [3, 0, 0, 0, 1, 0], 173 | [3, 1, 0, 0, 1, 0], 174 | [3, 2, 0, 0, 1, 0], 175 | [3, 0, 1, 0, 1, 0], // 21 176 | [3, 1, 1, 0, 1, 0], 177 | [3, 2, 1, 0, 1, 0], // 23 178 | [3, 0, 2, 0, 1, 0], // 24 179 | [3, 0, 0, 1, 1, 0], // 25 180 | [3, 1, 0, 1, 1, 0], // 26 181 | [3, 2, 0, 1, 1, 0], // 27 182 | [3, 0, 1, 1, 1, 0], // 28 183 | [3, 1, 1, 1, 1, 0], // 29 184 | [3, 2, 1, 1, 1, 0], // 30 185 | [3, 0, 2, 1, 1, 0], // 31 186 | [3, 0, 0, 2, 1, 0], // 32 187 | [3, 0, 0, 0, 2, 0], // 33 188 | [6, 0, 0, 0, 0, 0], // 34 189 | [3, 0, 0, 0, 0, 1], // 35 190 | [3, 1, 0, 0, 0, 1], // 36 191 | [3, 2, 0, 0, 0, 1], // 37 192 | [3, 0, 1, 0, 0, 1], // 38 193 | [3, 1, 1, 0, 0, 1], // 39 194 | [3, 2, 1, 0, 0, 1], // 40 195 | [3, 0, 2, 0, 0, 1], // 41 double blob mage 196 | [3, 0, 0, 1, 0, 1], // 42 197 | [3, 1, 0, 1, 0, 1], // 43 198 | [3, 2, 0, 1, 0, 1], // 44 199 | [3, 0, 1, 1, 0, 1], // 45 200 | [3, 1, 1, 1, 0, 1], // 46 201 | [3, 2, 1, 1, 0, 1], // 47 202 | [3, 0, 2, 1, 0, 1], // 48 double blob melee mage 203 | [3, 0, 0, 2, 0, 1], // 49 2 melee mage 204 | [3, 0, 0, 0, 1, 1], // 50 205 | [3, 1, 0, 0, 1, 1], // 51 206 | [3, 2, 0, 0, 1, 1], // 52 207 | [3, 0, 1, 0, 1, 1], // 53 208 | [3, 1, 1, 0, 1, 1], // 54 209 | [3, 2, 1, 0, 1, 1], // 55 210 | [3, 0, 2, 0, 1, 1], // 56 211 | [3, 0, 0, 1, 1, 1], // 57 212 | [3, 1, 0, 1, 1, 1], // 58 213 | [3, 2, 0, 1, 1, 1], // 59 214 | [3, 0, 1, 1, 1, 1], // 60 215 | [3, 1, 1, 1, 1, 1], // 61 216 | [3, 2, 1, 1, 1, 1], // 62 217 | [3, 0, 2, 1, 1, 1], // 63 218 | [3, 0, 0, 2, 1, 1], // 64 219 | [3, 0, 0, 0, 2, 1], // 65 220 | [3, 0, 0, 0, 0, 2], // 66 221 | ]; 222 | } 223 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalZek.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { 4 | Assets, 5 | Mob, 6 | Projectile, 7 | MeleeWeapon, 8 | MagicWeapon, 9 | Sound, 10 | UnitBonuses, 11 | Collision, 12 | AttackIndicators, 13 | Random, 14 | Viewport, 15 | GLTFModel, 16 | EntityNames, 17 | Trainer, 18 | Model, 19 | } from "osrs-sdk"; 20 | 21 | import { InfernoMobDeathStore } from "../InfernoMobDeathStore"; 22 | import { InfernoRegion } from "../InfernoRegion"; 23 | 24 | import MagerImage from "../../assets/images/mager.png"; 25 | import MagerSound from "../../assets/sounds/mage_ranger_598.ogg"; 26 | import { JalZekModelWithLight } from "../JalZekModelWithLight"; 27 | 28 | const HitSound = Assets.getAssetUrl("assets/sounds/inferno_rangermager_dmg.ogg"); 29 | 30 | export const MagerModel = Assets.getAssetUrl("models/7699_33000.glb"); 31 | export const MageProjectileModel = Assets.getAssetUrl("models/mage_projectile.glb"); 32 | 33 | export class JalZek extends Mob { 34 | shouldRespawnMobs: boolean; 35 | isFlickering = false; 36 | // flicker only the tick before the attack animation happns 37 | flickerDurationTicks = 1; 38 | flickerTicksRemaining = 0; 39 | extendedGltfModelInstance: JalZekModelWithLight | null = null; 40 | 41 | mobName() { 42 | return EntityNames.JAL_ZEK; 43 | } 44 | 45 | shouldChangeAggro(projectile: Projectile) { 46 | return this.aggro != projectile.from && this.autoRetaliate; 47 | } 48 | 49 | get combatLevel() { 50 | return 490; 51 | } 52 | 53 | dead() { 54 | super.dead(); 55 | InfernoMobDeathStore.npcDied(this); 56 | if (this.isFlickering) { 57 | this.isFlickering = false; 58 | this.updateUnderglowVisuals(); 59 | } 60 | } 61 | 62 | setStats() { 63 | const region = this.region as InfernoRegion; 64 | this.shouldRespawnMobs = region.wave >= 69; 65 | 66 | this.stunned = 1; 67 | 68 | this.weapons = { 69 | stab: new MeleeWeapon(), 70 | magic: new MagicWeapon({ 71 | model: MageProjectileModel, 72 | modelScale: 1 / 128, 73 | visualDelayTicks: 2, 74 | visualHitEarlyTicks: -1, // hits after landing 75 | sound: new Sound(MagerSound, 0.1), 76 | }), 77 | }; 78 | 79 | // non boosted numbers 80 | this.stats = { 81 | attack: 370, 82 | strength: 510, 83 | defence: 260, 84 | range: 510, 85 | magic: 300, 86 | hitpoint: 220, 87 | }; 88 | 89 | // with boosts 90 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 91 | } 92 | 93 | get bonuses(): UnitBonuses { 94 | return { 95 | attack: { 96 | stab: 0, 97 | slash: 0, 98 | crush: 0, 99 | magic: 80, 100 | range: 0, 101 | }, 102 | defence: { 103 | stab: 0, 104 | slash: 0, 105 | crush: 0, 106 | magic: 0, 107 | range: 0, 108 | }, 109 | other: { 110 | meleeStrength: 0, 111 | rangedStrength: 0, 112 | magicDamage: 1.0, 113 | prayer: 0, 114 | }, 115 | }; 116 | } 117 | 118 | get attackSpeed() { 119 | return 4; 120 | } 121 | 122 | get attackRange() { 123 | return 15; 124 | } 125 | 126 | get size() { 127 | return 4; 128 | } 129 | 130 | get image() { 131 | return MagerImage; 132 | } 133 | 134 | hitSound(damaged) { 135 | return new Sound(HitSound, 0.25); 136 | } 137 | 138 | attackStyleForNewAttack() { 139 | return "magic"; 140 | } 141 | 142 | canMeleeIfClose() { 143 | return "stab" as const; 144 | } 145 | 146 | magicMaxHit() { 147 | return 70; 148 | } 149 | 150 | get maxHit() { 151 | return 70; 152 | } 153 | 154 | attackAnimation(tickPercent: number, context) { 155 | context.rotate(tickPercent * Math.PI * 2); 156 | } 157 | 158 | respawnLocation(mobToResurrect: Mob) { 159 | for (let x = 15 + 11; x < 22 + 11; x++) { 160 | for (let y = 10 + 14; y < 23 + 14; y++) { 161 | if (!Collision.collidesWithAnyMobs(this.region, x, y, mobToResurrect.size)) { 162 | if (!Collision.collidesWithAnyEntities(this.region, x, y, mobToResurrect.size)) { 163 | return { x, y }; 164 | } 165 | } 166 | } 167 | } 168 | 169 | return { x: 21, y: 22 }; 170 | } 171 | 172 | create3dModel() { 173 | if (!this.extendedGltfModelInstance) { 174 | this.extendedGltfModelInstance = new JalZekModelWithLight(this, MagerModel); 175 | } 176 | return this.extendedGltfModelInstance; 177 | } 178 | 179 | updateUnderglowVisuals() { 180 | if (this.extendedGltfModelInstance) { 181 | this.extendedGltfModelInstance.setFlickerVisualState(this.isFlickering); 182 | } 183 | } 184 | 185 | attackStep() { 186 | super.attackStep(); 187 | 188 | if (this.isFlickering) { 189 | this.flickerTicksRemaining--; 190 | // double flicker on the flicker tick 191 | this.extendedGltfModelInstance?.setFlickerVisualState(true); 192 | if (this.flickerTicksRemaining <= 0) { 193 | this.isFlickering = false; 194 | // reset to normal 195 | this.extendedGltfModelInstance?.setFlickerVisualState(false); 196 | // Set attack style before attacking 197 | this.attackStyle = this.attackStyleForNewAttack(); 198 | this.attackFeedback = AttackIndicators.NONE; 199 | if (Random.get() < 0.1 && !this.shouldRespawnMobs) { 200 | const mobToResurrect = InfernoMobDeathStore.selectMobToResurect(this.region); 201 | if (!mobToResurrect) { 202 | this.attack() && this.didAttack(); 203 | } else { 204 | // Set to 50% health 205 | mobToResurrect.currentStats.hitpoint = Math.floor(mobToResurrect.stats.hitpoint / 2); 206 | mobToResurrect.dying = -1; 207 | mobToResurrect.attackDelay = mobToResurrect.attackSpeed; 208 | 209 | mobToResurrect.setLocation(this.respawnLocation(mobToResurrect)); 210 | mobToResurrect.playAnimation(mobToResurrect.idlePoseId); 211 | mobToResurrect.cancelDeath(); 212 | mobToResurrect.aggro = Trainer.player; 213 | 214 | mobToResurrect.perceivedLocation = mobToResurrect.location; 215 | this.region.addMob(mobToResurrect); 216 | // (15, 10) to (21 , 22 217 | this.attackDelay = 8; 218 | this.playAnimation(3); 219 | } 220 | } else { 221 | this.attack() && this.didAttack(); 222 | } 223 | this.attackDelay = this.attackSpeed; 224 | } 225 | return; 226 | } 227 | this.attackIfPossible(); 228 | } 229 | 230 | attackIfPossible() { 231 | this.hadLOS = this.hasLOS; 232 | this.setHasLOS(); 233 | 234 | if (!this.aggro || this.stunned > 0 || this.frozen > 0 || this.attackDelay > 0 || this.isDying()) { 235 | return; 236 | } 237 | 238 | const isUnderAggro = Collision.collisionMath( 239 | this.location.x, 240 | this.location.y, 241 | this.size, 242 | this.aggro.location.x, 243 | this.aggro.location.y, 244 | 1, 245 | ); 246 | 247 | if (!isUnderAggro && this.hasLOS) { 248 | // start flicker BEFORE initiating attack 249 | this.isFlickering = true; 250 | this.flickerTicksRemaining = this.flickerDurationTicks; 251 | //resetting visual state 252 | this.extendedGltfModelInstance?.setFlickerVisualState(false); 253 | // wait for flicker to finish before attacking 254 | this.attackDelay = this.flickerDurationTicks; 255 | return; 256 | } 257 | } 258 | 259 | override get attackAnimationId() { 260 | return 2; 261 | } 262 | 263 | override get deathAnimationId() { 264 | return 5; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/JalTokJad.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, UnitOptions, ImageLoader, Location, MultiModelProjectileOffsetInterpolator, Location3, MagicWeapon, Mob, Unit, AttackBonuses, DelayedAction, AttackIndicators, SoundCache, Projectile, ArcProjectileMotionInterpolator, RangedWeapon, FollowTargetInterpolator, Region, MeleeWeapon, Collision, Random, UnitBonuses, Sound, GLTFModel, EntityNames } from "osrs-sdk"; 4 | 5 | import JadImage from "../../assets/images/jad/jad_mage_1.png"; 6 | import { YtHurKot } from "./YtHurKot"; 7 | 8 | import FireBreath from "../../assets/sounds/firebreath_159.ogg"; 9 | import FireWaveCastAndFire from "../../assets/sounds/firewave_cast_and_fire_162.ogg"; 10 | import FireWaveHit from "../../assets/sounds/firewave_hit_163.ogg"; 11 | 12 | import { JAD_FRAMES_PER_TICK, JAD_MAGE_FRAMES, JAD_RANGE_FRAMES } from "./JalTokJadAnim"; 13 | 14 | const HitSound = Assets.getAssetUrl("assets/sounds/dragon_hit_410.ogg"); 15 | 16 | export const JadModel = Assets.getAssetUrl("models/7700_33012.glb"); 17 | export const JadRangeProjectileModel = Assets.getAssetUrl("models/jad_range.glb"); 18 | export const JadMageProjectileModel1 = Assets.getAssetUrl("models/jad_mage_front.glb"); 19 | export const JadMageProjectileModel2 = Assets.getAssetUrl("models/jad_mage_middle.glb"); 20 | export const JadMageProjectileModel3 = Assets.getAssetUrl("models/jad_mage_rear.glb"); 21 | 22 | interface JadUnitOptions extends UnitOptions { 23 | attackSpeed: number; 24 | stun: number; 25 | healers: number; 26 | isZukWave: boolean; 27 | } 28 | 29 | const MageStartSound = { src: FireBreath, volume: 0.1 }; 30 | const RangeProjectileSound = { src: FireWaveHit, volume: 0.075 }; 31 | const MageProjectileSound = { src: FireWaveCastAndFire, volume: 0.075 }; 32 | 33 | const JAD_PROJECTILE_DELAY = 3; 34 | 35 | // draw the projectiles forward to back 36 | const MageOffsetInterpolator: MultiModelProjectileOffsetInterpolator ={ 37 | interpolateOffsets: function (from, to, percent: number): Location3[] { 38 | const res = [ 39 | { x: 0, y: 1.0, z: 0}, 40 | { x: 0, y: 0.5, z: 0}, 41 | { x: 0, y: 0, z: 0} 42 | ]; 43 | return res; 44 | } 45 | } 46 | 47 | class JadMagicWeapon extends MagicWeapon { 48 | override attack(from: Mob, to: Unit, bonuses: AttackBonuses = {}): boolean { 49 | DelayedAction.registerDelayedAction( 50 | new DelayedAction(() => { 51 | const overhead = to.prayerController?.matchFeature("magic"); 52 | from.attackFeedback = AttackIndicators.HIT; 53 | if (overhead) { 54 | from.attackFeedback = AttackIndicators.BLOCKED; 55 | } 56 | super.attack(from, to, bonuses); 57 | }, JAD_PROJECTILE_DELAY), 58 | ); 59 | SoundCache.play(MageStartSound); 60 | return true; 61 | } 62 | 63 | registerProjectile(from: Unit, to: Unit) { 64 | to.addProjectile( 65 | new Projectile(this, this.damage, from, to, "magic", { 66 | reduceDelay: JAD_PROJECTILE_DELAY, 67 | motionInterpolator: new ArcProjectileMotionInterpolator(1), 68 | color: "#FFAA00", 69 | size: 2, 70 | visualHitEarlyTicks: -1, 71 | projectileSound: MageProjectileSound, 72 | models: [ 73 | JadMageProjectileModel1, 74 | JadMageProjectileModel2, 75 | JadMageProjectileModel3, 76 | ], 77 | modelScale: 1 / 128, 78 | offsetsInterpolator: MageOffsetInterpolator 79 | }), 80 | ); 81 | } 82 | } 83 | class JadRangeWeapon extends RangedWeapon { 84 | attack(from: Mob, to: Unit, bonuses: AttackBonuses = {}): boolean { 85 | DelayedAction.registerDelayedAction( 86 | new DelayedAction(() => { 87 | const overhead = to.prayerController?.matchFeature("range"); 88 | from.attackFeedback = AttackIndicators.HIT; 89 | if (overhead) { 90 | from.attackFeedback = AttackIndicators.BLOCKED; 91 | } 92 | super.attack(from, to, bonuses); 93 | }, JAD_PROJECTILE_DELAY), 94 | ); 95 | return true; 96 | } 97 | 98 | registerProjectile(from: Unit, to: Unit) { 99 | to.addProjectile( 100 | new Projectile(this, this.damage, from, to, "range", { 101 | reduceDelay: JAD_PROJECTILE_DELAY, 102 | model: JadRangeProjectileModel, 103 | modelScale: 1 / 128, 104 | // allows the animation to play out even after hitting 105 | visualHitEarlyTicks: -1, 106 | motionInterpolator: new FollowTargetInterpolator(), 107 | sound: RangeProjectileSound, 108 | }), 109 | ); 110 | } 111 | } 112 | 113 | const jadMageFrames = JAD_MAGE_FRAMES.map((frame) => ImageLoader.createImage(frame)); 114 | const jadRangeFrames = JAD_RANGE_FRAMES.map((frame) => ImageLoader.createImage(frame)); 115 | 116 | export class JalTokJad extends Mob { 117 | playerPrayerScan?: string = null; 118 | waveCooldown: number; 119 | hasProccedHealers = false; 120 | healers: number; 121 | isZukWave: boolean; 122 | 123 | currentAnimationTick = 0; 124 | currentAnimationFrame = 0; 125 | currentAnimation: string[] | null = null; 126 | 127 | constructor(region: Region, location: Location, options: JadUnitOptions) { 128 | super(region, location, options); 129 | this.waveCooldown = options.attackSpeed; 130 | this.stunned = options.stun; 131 | this.healers = options.healers; 132 | this.autoRetaliate = true; 133 | this.isZukWave = options.isZukWave; 134 | } 135 | 136 | mobName() { 137 | return EntityNames.JAL_TOK_JAD; 138 | } 139 | 140 | get combatLevel() { 141 | return 900; 142 | } 143 | 144 | shouldChangeAggro(projectile: Projectile) { 145 | return this.aggro != projectile.from && this.autoRetaliate; 146 | } 147 | 148 | setStats() { 149 | this.weapons = { 150 | stab: new MeleeWeapon(), 151 | magic: new JadMagicWeapon(), 152 | range: new JadRangeWeapon(), 153 | }; 154 | 155 | // non boosted numbers 156 | this.stats = { 157 | hitpoint: 350, 158 | attack: 750, 159 | strength: 1020, 160 | defence: 480, 161 | range: 1020, 162 | magic: 510, 163 | }; 164 | 165 | // with boosts 166 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 167 | } 168 | 169 | damageTaken() { 170 | if (this.currentStats.hitpoint < this.stats.hitpoint / 2) { 171 | if (this.hasProccedHealers === false) { 172 | this.autoRetaliate = false; 173 | this.hasProccedHealers = true; 174 | for (let i = 0; i < this.healers; i++) { 175 | // Spawn healer 176 | 177 | let xOff = 0; 178 | let yOff = 0; 179 | 180 | while (Collision.collidesWithMob(this.region, this.location.x + xOff, this.location.y + yOff, 1, this)) { 181 | if (this.isZukWave) { 182 | xOff = Math.floor(Random.get() * 6); 183 | yOff = -Math.floor(Random.get() * 4) - this.size; 184 | } else { 185 | xOff = Math.floor(Random.get() * 11) - 5; 186 | yOff = Math.floor(Random.get() * 15) - 5 - this.size; 187 | } 188 | } 189 | 190 | const healer = new YtHurKot( 191 | this.region, 192 | { x: this.location.x + xOff, y: this.location.y + yOff }, 193 | { aggro: this }, 194 | ); 195 | this.region.addMob(healer); 196 | } 197 | } 198 | } 199 | } 200 | get bonuses(): UnitBonuses { 201 | return { 202 | attack: { 203 | stab: 0, 204 | slash: 0, 205 | crush: 0, 206 | magic: 100, 207 | range: 80, 208 | }, 209 | defence: { 210 | stab: 0, 211 | slash: 0, 212 | crush: 0, 213 | magic: 0, 214 | range: 0, 215 | }, 216 | other: { 217 | meleeStrength: 0, 218 | rangedStrength: 80, 219 | magicDamage: 1.75, 220 | prayer: 0, 221 | }, 222 | }; 223 | } 224 | 225 | get attackSpeed() { 226 | return this.waveCooldown; 227 | } 228 | 229 | get flinchDelay() { 230 | return 2; 231 | } 232 | 233 | get attackRange() { 234 | return 50; 235 | } 236 | 237 | get size() { 238 | return 5; 239 | } 240 | 241 | get clickboxRadius() { 242 | return 2.5; 243 | } 244 | 245 | get clickboxHeight() { 246 | return 4; 247 | } 248 | 249 | get image() { 250 | if (!this.currentAnimation) { 251 | return JadImage; 252 | } 253 | const animationLength = this.currentAnimation.length; 254 | return this.currentAnimation[Math.floor(this.currentAnimationFrame % animationLength)]; 255 | } 256 | 257 | get isAnimated() { 258 | return !!this.currentAnimation; 259 | } 260 | 261 | attackStyleForNewAttack() { 262 | return Random.get() < 0.5 ? "range" : "magic"; 263 | } 264 | 265 | shouldShowAttackAnimation() { 266 | return this.attackDelay === this.attackSpeed && this.playerPrayerScan === null; 267 | } 268 | 269 | canMeleeIfClose() { 270 | return "stab" as const; 271 | } 272 | 273 | magicMaxHit() { 274 | return 113; 275 | } 276 | 277 | attackStep() { 278 | super.attackStep(); 279 | this.currentAnimationTick++; 280 | } 281 | 282 | hitSound(damaged) { 283 | return new Sound(HitSound, 0.1); 284 | } 285 | 286 | attack() { 287 | super.attack(); 288 | this.attackFeedback = AttackIndicators.NONE; 289 | if (this.attackStyle === "magic") { 290 | this.currentAnimation = JAD_MAGE_FRAMES; 291 | } else if (this.attackStyle === "range") { 292 | this.currentAnimation = JAD_RANGE_FRAMES; 293 | } 294 | this.currentAnimationFrame = 0; 295 | this.currentAnimationTick = 0; 296 | return true; 297 | } 298 | 299 | draw(tickPercent, context, offset, scale, drawUnderTile) { 300 | if (this.currentAnimation) { 301 | this.currentAnimationFrame = 302 | (this.currentAnimationTick - 1) * JAD_FRAMES_PER_TICK + tickPercent * JAD_FRAMES_PER_TICK; 303 | if (this.currentAnimationFrame >= this.currentAnimation.length) { 304 | this.currentAnimation = null; 305 | this.currentAnimationFrame = 0; 306 | this.currentAnimationTick = 0; 307 | } 308 | } 309 | super.draw(tickPercent, context, offset, scale, drawUnderTile); 310 | } 311 | 312 | create3dModel() { 313 | return GLTFModel.forRenderable(this, JadModel); 314 | } 315 | 316 | get attackAnimationId() { 317 | switch (this.attackStyle) { 318 | case "magic": 319 | return 2; 320 | case "range": 321 | return 3; 322 | default: 323 | return 4; 324 | } 325 | } 326 | 327 | override get deathAnimationId() { 328 | return 6; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/content/inferno/js/mobs/TzKalZuk.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Assets, MagicWeapon, Unit, Sound, Projectile, Mob, Region, UnitOptions, ImageLoader, Location, Viewport, UnitTypes, UnitBonuses, Model, GLTFModel, EntityNames, Trainer, Settings } from "osrs-sdk"; 4 | 5 | import ZukImage from "../../assets/images/TzKal-Zuk.png"; 6 | import { InfernoSettings } from "../InfernoSettings"; 7 | import { ZukShield } from "../ZukShield"; 8 | import { find } from "lodash"; 9 | import ZukAttackImage from "../../assets/images/zuk_attack.png"; 10 | import { JalZek, MagerModel } from "./JalZek"; 11 | import { JalXil, RangerModel } from "./JalXil"; 12 | import { JalMejJak } from "./JalMejJak"; 13 | import { JadModel, JalTokJad } from "./JalTokJad"; 14 | 15 | const HitSound = Assets.getAssetUrl("assets/sounds/dragon_hit_410.ogg"); 16 | 17 | import ZukAttackSound from "../../assets/sounds/fireblast_cast_and_fire_155.ogg"; 18 | 19 | const ZukModel = Assets.getAssetUrl("models/7706_33011.glb"); 20 | const ZukBall = Assets.getAssetUrl("models/zuk_projectile.glb"); 21 | 22 | /* eslint-disable @typescript-eslint/no-explicit-any */ 23 | 24 | const zukWeaponImage = ImageLoader.createImage(ZukAttackImage); 25 | 26 | class ZukWeapon extends MagicWeapon { 27 | get image(): HTMLImageElement { 28 | return zukWeaponImage; 29 | } 30 | 31 | isBlockable() { 32 | return false; 33 | } 34 | registerProjectile(from: Unit, to: Unit) { 35 | const sound = new Sound(ZukAttackSound, 0.03); 36 | if (to.isPlayer) { 37 | // louder! 38 | sound.volume = 0.1; 39 | } 40 | to.addProjectile( 41 | new ZukProjectile(this, this.damage, from, to, "range", { 42 | model: ZukBall, 43 | modelScale: 1 / 128, 44 | setDelay: 4, 45 | visualDelayTicks: 2, 46 | sound, 47 | }), 48 | ); 49 | } 50 | } 51 | 52 | class ZukProjectile extends Projectile { 53 | get size() { 54 | return 2; 55 | } 56 | 57 | get color() { 58 | return "#FFAA00"; 59 | } 60 | } 61 | 62 | export class TzKalZuk extends Mob { 63 | shield: ZukShield; 64 | enraged = false; 65 | 66 | setTimer = 72; 67 | timerPaused = false; 68 | hasPaused = false; 69 | 70 | constructor(region: Region, location: Location, options: UnitOptions) { 71 | super(region, location, options); 72 | this.attackDelay = 14; 73 | 74 | this.shield = find(region.mobs.concat(region.newMobs), (mob: Unit) => { 75 | return mob.mobName() === EntityNames.INFERNO_SHIELD; 76 | }) as ZukShield; 77 | } 78 | 79 | contextActions(region: Region, x: number, y: number) { 80 | return super.contextActions(region, x, y).concat([ 81 | { 82 | text: [ 83 | { text: "Spawn ", fillStyle: "white" }, 84 | { text: ` Jad`, fillStyle: "yellow" }, 85 | ], 86 | action: () => { 87 | Trainer.clickController.redClick(); 88 | this.setTimer = 400; 89 | 90 | this.currentStats.hitpoint = 479; 91 | this.timerPaused = true; 92 | this.hasPaused = true; 93 | this.damageTaken(); 94 | }, 95 | }, 96 | { 97 | text: [ 98 | { text: "Spawn ", fillStyle: "white" }, 99 | { text: ` Healers`, fillStyle: "yellow" }, 100 | ], 101 | action: () => { 102 | Trainer.clickController.redClick(); 103 | this.setTimer = 400; 104 | 105 | this.currentStats.hitpoint = 239; 106 | this.damageTaken(); 107 | this.timerPaused = false; 108 | this.hasPaused = true; 109 | }, 110 | }, 111 | ]); 112 | } 113 | 114 | mobName() { 115 | return EntityNames.TZ_KAL_ZUK; 116 | } 117 | 118 | attackIfPossible() { 119 | this.attackStyle = this.attackStyleForNewAttack(); 120 | 121 | if (this.timerPaused === false) { 122 | this.setTimer--; 123 | 124 | if (this.setTimer === 0) { 125 | this.setTimer = 350; 126 | 127 | const mager = new JalZek(this.region, { x: 20, y: 21 }, { aggro: this.shield, spawnDelay: 7 }); 128 | this.region.addMob(mager); 129 | const ranger = new JalXil(this.region, { x: 29, y: 21 }, { aggro: this.shield, spawnDelay: 9 }); 130 | this.region.addMob(ranger); 131 | } 132 | } 133 | 134 | if (this.canAttack() && this.attackDelay <= 0) { 135 | this.attack() && this.didAttack(); 136 | } 137 | } 138 | damageTaken() { 139 | if (this.timerPaused === false) { 140 | if (this.currentStats.hitpoint < 600 && this.hasPaused === false) { 141 | this.timerPaused = true; 142 | this.hasPaused = true; 143 | } 144 | } else { 145 | if (this.currentStats.hitpoint < 480) { 146 | this.setTimer += 175; 147 | this.timerPaused = false; 148 | // Spawn Jad 149 | const jad = new JalTokJad( 150 | this.region, 151 | { x: 24, y: 25 }, 152 | { 153 | aggro: this.shield, 154 | attackSpeed: 8, 155 | stun: 1, 156 | healers: 3, 157 | isZukWave: true, 158 | spawnDelay: 7, 159 | }, 160 | ); 161 | this.region.addMob(jad); 162 | } 163 | } 164 | 165 | if (this.currentStats.hitpoint < 240 && this.enraged === false) { 166 | this.enraged = true; 167 | 168 | const healer1 = new JalMejJak(this.region, { x: 16, y: 9 }, { aggro: this, spawnDelay: 2 }); 169 | this.region.addMob(healer1); 170 | 171 | const healer2 = new JalMejJak(this.region, { x: 20, y: 9 }, { aggro: this, spawnDelay: 2 }); 172 | this.region.addMob(healer2); 173 | 174 | const healer3 = new JalMejJak(this.region, { x: 30, y: 9 }, { aggro: this, spawnDelay: 2 }); 175 | this.region.addMob(healer3); 176 | 177 | const healer4 = new JalMejJak(this.region, { x: 34, y: 9 }, { aggro: this, spawnDelay: 2 }); 178 | this.region.addMob(healer4); 179 | } 180 | 181 | if (this.currentStats.hitpoint <= 0) { 182 | this.region.mobs.forEach((mob: Mob) => { 183 | if ((mob as any) !== this) { 184 | mob.dying = 0; 185 | } 186 | }); 187 | } 188 | } 189 | 190 | override visible() { 191 | // always visible, even during countdown 192 | return true; 193 | } 194 | 195 | attack() { 196 | if (!this.aggro || this.aggro.dying >= 0) { 197 | return false; 198 | } 199 | let shieldOrPlayer: Unit = this.shield; 200 | 201 | if (this.aggro.location.x < this.shield.location.x || this.aggro.location.x >= this.shield.location.x + 5) { 202 | shieldOrPlayer = this.aggro as Unit; 203 | } 204 | if (this.aggro.location.y > 16) { 205 | shieldOrPlayer = this.aggro as Unit; 206 | } 207 | this.weapons["typeless"].attack(this, shieldOrPlayer, { 208 | attackStyle: "typeless", 209 | magicBaseSpellDamage: shieldOrPlayer.type === UnitTypes.PLAYER ? this.magicMaxHit() : 0, 210 | }); 211 | return true; 212 | } 213 | 214 | get combatLevel() { 215 | return 1400; 216 | } 217 | 218 | canMove() { 219 | return false; 220 | } 221 | 222 | magicMaxHit() { 223 | return 251; 224 | } 225 | 226 | override get xpBonusMultiplier() { 227 | return 1.575; 228 | } 229 | 230 | setStats() { 231 | this.stunned = 8; 232 | 233 | this.weapons = { 234 | // sound is handled internally in ZukWeapon 235 | typeless: new ZukWeapon(), 236 | }; 237 | 238 | // non boosted numbers 239 | this.stats = { 240 | attack: 350, 241 | strength: 600, 242 | defence: 234, 243 | range: 400, 244 | magic: 150, 245 | hitpoint: 1200, 246 | }; 247 | 248 | // with boosts 249 | this.currentStats = JSON.parse(JSON.stringify(this.stats)); 250 | } 251 | 252 | get bonuses(): UnitBonuses { 253 | return { 254 | attack: { 255 | stab: 0, 256 | slash: 0, 257 | crush: 0, 258 | magic: 550, 259 | range: 550, 260 | }, 261 | defence: { 262 | stab: 0, 263 | slash: 0, 264 | crush: 0, 265 | magic: 350, 266 | range: 100, 267 | }, 268 | other: { 269 | meleeStrength: 200, 270 | rangedStrength: 200, 271 | magicDamage: 4.5, 272 | prayer: 0, 273 | }, 274 | }; 275 | } 276 | get attackSpeed() { 277 | if (this.enraged) { 278 | return 7; 279 | } 280 | return 10; 281 | } 282 | 283 | attackStyleForNewAttack() { 284 | return "magic"; 285 | } 286 | 287 | get attackRange() { 288 | return 0; 289 | } 290 | 291 | get size() { 292 | return 7; 293 | } 294 | 295 | get height() { 296 | return 4; 297 | } 298 | 299 | get image() { 300 | return ZukImage; 301 | } 302 | 303 | hitSound(damaged) { 304 | return new Sound(HitSound, 0.1); 305 | } 306 | 307 | attackAnimation(tickPercent: number, context: OffscreenCanvasRenderingContext2D) { 308 | context.transform(1, 0, Math.sin(-tickPercent * Math.PI * 2) / 2, 1, 0, 0); 309 | } 310 | 311 | drawOverTile(tickPercent: number, context: OffscreenCanvasRenderingContext2D, scale: number) { 312 | super.drawOverTile(tickPercent, context, scale); 313 | // Draw mob 314 | } 315 | 316 | create3dModel(): Model { 317 | return GLTFModel.forRenderable(this, ZukModel); 318 | } 319 | 320 | getPerceivedRotation(tickPercent: any) { 321 | // zuk doesn't rotate for some reason 322 | return -Math.PI / 2; 323 | } 324 | 325 | drawUILayer(tickPercent, offset, context, scale, hitsplatsAbove) { 326 | super.drawUILayer(tickPercent, offset, context, scale, hitsplatsAbove); 327 | 328 | context.fillStyle = "#FFFF00"; 329 | context.font = "24px OSRS"; 330 | 331 | context.fillText(String(this.currentStats.hitpoint), offset.x, offset.y + 120); 332 | 333 | // Display set timer if the setting is enabled 334 | if (InfernoSettings.displaySetTimer) { 335 | // Set color based on timer state: red when running, green when paused 336 | context.fillStyle = this.timerPaused ? "#999999" : "#ffffff"; 337 | context.font = "20px OSRS"; 338 | 339 | // Convert ticks to MM:SS format (1 tick = 0.6 seconds) 340 | const totalSeconds = Math.round(this.setTimer * 0.6); 341 | const minutes = Math.floor(totalSeconds / 60); 342 | const seconds = totalSeconds % 60; 343 | const timerDisplay = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; 344 | 345 | // Draw timer below the health 346 | context.fillText(timerDisplay, offset.x, offset.y + 150); 347 | } 348 | } 349 | 350 | async preload() { 351 | await super.preload(); 352 | await GLTFModel.preload(RangerModel); 353 | await GLTFModel.preload(MagerModel); 354 | await GLTFModel.preload(JadModel); 355 | } 356 | 357 | get deathAnimationLength() { 358 | return 6; 359 | } 360 | 361 | get attackAnimationId() { 362 | return 1; 363 | } 364 | 365 | override get deathAnimationId() { 366 | return 3; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoLoadout.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AhrimsRobeskirt, 3 | AhrimsRobetop, 4 | AncestralRobebottom, 5 | AncestralRobetop, 6 | AncientStaff, 7 | AvasAccumulator, 8 | AvasAssembler, 9 | BarrowsGloves, 10 | BastionPotion, 11 | BlackChinchompa, 12 | BlackDhideChaps, 13 | BlackDhideVambraces, 14 | Blowpipe, 15 | BowOfFaerdhinen, 16 | BrowserUtils, 17 | Chest, 18 | CrystalBody, 19 | CrystalHelm, 20 | CrystalLegs, 21 | CrystalShield, 22 | DagonhaiRobeTop, 23 | DevoutBoots, 24 | DiamondBoltsE, 25 | DizanasQuiver, 26 | DragonArrows, 27 | GuthixRobeTop, 28 | HolyBlessing, 29 | InfernalCape, 30 | Item, 31 | ItemName, 32 | JusticiarChestguard, 33 | JusticiarFaceguard, 34 | JusticiarLegguards, 35 | KodaiWand, 36 | Legs, 37 | MagesBook, 38 | MasoriBodyF, 39 | MasoriChapsF, 40 | MasoriMaskF, 41 | NecklaceOfAnguish, 42 | OccultNecklace, 43 | PegasianBoots, 44 | Player, 45 | PrimordialBoots, 46 | RangerBoots, 47 | RingOfSufferingImbued, 48 | RobinHoodHat, 49 | RubyBoltsE, 50 | RuneCrossbow, 51 | RuneKiteshield, 52 | SaradominBody, 53 | SaradominBrew, 54 | SaradominChaps, 55 | SaradominCoif, 56 | ScytheOfVitur, 57 | SlayerHelmet, 58 | StaminaPotion, 59 | SuperRestore, 60 | TorvaFullhelm, 61 | TorvaPlatebody, 62 | TorvaPlatelegs, 63 | TwistedBow, 64 | UnitOptions, 65 | Weapon, 66 | ZaryteVambraces, 67 | } from "osrs-sdk"; 68 | import { filter, indexOf, map } from "lodash"; 69 | 70 | export class InfernoLoadout { 71 | wave: number; 72 | loadoutType: string; 73 | onTask: boolean; 74 | 75 | constructor(wave: number, loadoutType: string, onTask: boolean) { 76 | this.wave = wave; 77 | this.loadoutType = loadoutType; 78 | this.onTask = onTask; 79 | } 80 | 81 | loadoutMaxMelee() { 82 | return { 83 | equipment: { 84 | weapon: new ScytheOfVitur(), 85 | offhand: null, 86 | helmet: new TorvaFullhelm(), 87 | necklace: new OccultNecklace(), // TODO 88 | cape: new InfernalCape(), 89 | ammo: new DragonArrows(), 90 | chest: new TorvaPlatebody(), 91 | legs: new TorvaPlatelegs(), 92 | feet: new PrimordialBoots(), 93 | gloves: new ZaryteVambraces(), // TODO 94 | ring: new RingOfSufferingImbued(), // TODO 95 | }, 96 | inventory: [ 97 | new TwistedBow(), 98 | new MasoriBodyF(), 99 | new DizanasQuiver(), 100 | new PegasianBoots(), 101 | new NecklaceOfAnguish(), 102 | new MasoriChapsF(), 103 | new MasoriMaskF(), 104 | new SaradominBrew(), 105 | new SaradominBrew(), 106 | new SaradominBrew(), 107 | new SuperRestore(), 108 | new SuperRestore(), 109 | new SaradominBrew(), 110 | new SaradominBrew(), 111 | new SuperRestore(), 112 | new SuperRestore(), 113 | new SaradominBrew(), 114 | new SaradominBrew(), 115 | new SuperRestore(), 116 | new SuperRestore(), 117 | new SaradominBrew(), 118 | new SaradominBrew(), 119 | new SuperRestore(), 120 | new SuperRestore(), 121 | new BastionPotion(), 122 | new StaminaPotion(), 123 | new SuperRestore(), 124 | new SuperRestore(), 125 | ], 126 | }; 127 | } 128 | 129 | loadoutMaxTbowSpeedrunner() { 130 | return { 131 | ...this.loadoutMaxTbow(), 132 | inventory: [ 133 | new BlackChinchompa(), 134 | new Blowpipe(), 135 | new MasoriBodyF(), 136 | new TwistedBow(!!BrowserUtils.getQueryVar("geno")), 137 | new BastionPotion(), 138 | new NecklaceOfAnguish(), 139 | new MasoriChapsF(), 140 | null, 141 | new BastionPotion(), 142 | new SaradominBrew(), 143 | new SaradominBrew(), 144 | new SuperRestore(), 145 | new SuperRestore(), 146 | new SaradominBrew(), 147 | new SaradominBrew(), 148 | new SuperRestore(), 149 | new SuperRestore(), 150 | new SaradominBrew(), 151 | new SaradominBrew(), 152 | new SuperRestore(), 153 | new SuperRestore(), 154 | new SaradominBrew(), 155 | new SaradominBrew(), 156 | new SuperRestore(), 157 | new SuperRestore(), 158 | new BastionPotion(), 159 | new StaminaPotion(), 160 | new BastionPotion(), 161 | ], 162 | }; 163 | } 164 | 165 | loadoutMaxTbow() { 166 | return { 167 | equipment: { 168 | weapon: new KodaiWand(), 169 | offhand: new CrystalShield(), 170 | helmet: new MasoriMaskF(), 171 | necklace: new OccultNecklace(), 172 | cape: new DizanasQuiver(), 173 | ammo: new DragonArrows(), 174 | chest: new AncestralRobetop(), 175 | legs: new AncestralRobebottom(), 176 | feet: new PegasianBoots(), 177 | gloves: new ZaryteVambraces(), 178 | ring: new RingOfSufferingImbued(), 179 | }, 180 | inventory: [ 181 | new Blowpipe(), 182 | new MasoriBodyF(), 183 | new TwistedBow(), 184 | new JusticiarChestguard(), 185 | new NecklaceOfAnguish(), 186 | new MasoriChapsF(), 187 | null, 188 | new JusticiarLegguards(), 189 | new SaradominBrew(), 190 | new SaradominBrew(), 191 | new SuperRestore(), 192 | new SuperRestore(), 193 | new SaradominBrew(), 194 | new SaradominBrew(), 195 | new SuperRestore(), 196 | new SuperRestore(), 197 | new SaradominBrew(), 198 | new SaradominBrew(), 199 | new SuperRestore(), 200 | new SuperRestore(), 201 | new SaradominBrew(), 202 | new SaradominBrew(), 203 | new SuperRestore(), 204 | new SuperRestore(), 205 | new BastionPotion(), 206 | new StaminaPotion(), 207 | new SuperRestore(), 208 | new SuperRestore(), 209 | ], 210 | }; 211 | } 212 | 213 | loadoutMaxFbow() { 214 | return { 215 | equipment: { 216 | weapon: new KodaiWand(), 217 | offhand: new CrystalShield(), 218 | helmet: new CrystalHelm(), 219 | necklace: new OccultNecklace(), 220 | cape: new AvasAssembler(), 221 | ammo: new HolyBlessing(), 222 | chest: new AncestralRobetop(), 223 | legs: new AncestralRobebottom(), 224 | feet: new PegasianBoots(), 225 | gloves: new BarrowsGloves(), 226 | ring: new RingOfSufferingImbued(), 227 | }, 228 | inventory: [ 229 | new Blowpipe(), 230 | new CrystalBody(), 231 | new BowOfFaerdhinen(), 232 | new JusticiarChestguard(), 233 | new NecklaceOfAnguish(), 234 | new CrystalLegs(), 235 | null, 236 | new JusticiarLegguards(), 237 | new SaradominBrew(), 238 | new SaradominBrew(), 239 | new SuperRestore(), 240 | new SuperRestore(), 241 | new SaradominBrew(), 242 | new SaradominBrew(), 243 | new SuperRestore(), 244 | new SuperRestore(), 245 | new SaradominBrew(), 246 | new SaradominBrew(), 247 | new SuperRestore(), 248 | new SuperRestore(), 249 | new SaradominBrew(), 250 | new SaradominBrew(), 251 | new SuperRestore(), 252 | new SuperRestore(), 253 | new BastionPotion(), 254 | new StaminaPotion(), 255 | new SuperRestore(), 256 | new SuperRestore(), 257 | ], 258 | }; 259 | } 260 | 261 | 262 | loadoutBudgetFbow() { 263 | return { 264 | equipment: { 265 | weapon: new AncientStaff(), 266 | offhand: new CrystalShield(), 267 | helmet: new CrystalHelm(), 268 | necklace: new OccultNecklace(), 269 | cape: new AvasAssembler(), 270 | ammo: new HolyBlessing(), 271 | chest: new AhrimsRobetop(), 272 | legs: new CrystalLegs(), 273 | feet: new DevoutBoots(), 274 | gloves: new BarrowsGloves(), 275 | ring: new RingOfSufferingImbued(), 276 | }, 277 | inventory: [ 278 | new Blowpipe(), 279 | new CrystalBody(), 280 | new BowOfFaerdhinen(), 281 | new SuperRestore(), 282 | new NecklaceOfAnguish(), 283 | null, 284 | new SuperRestore(), 285 | new SuperRestore(), 286 | new SaradominBrew(), 287 | new SaradominBrew(), 288 | new SuperRestore(), 289 | new SuperRestore(), 290 | new SaradominBrew(), 291 | new SaradominBrew(), 292 | new SuperRestore(), 293 | new SuperRestore(), 294 | new SaradominBrew(), 295 | new SaradominBrew(), 296 | new SuperRestore(), 297 | new SuperRestore(), 298 | new SaradominBrew(), 299 | new SaradominBrew(), 300 | new SuperRestore(), 301 | new SuperRestore(), 302 | new BastionPotion(), 303 | new StaminaPotion(), 304 | new SuperRestore(), 305 | new SuperRestore(), 306 | ], 307 | }; 308 | } 309 | 310 | 311 | loadoutRcb() { 312 | return { 313 | equipment: { 314 | weapon: new AncientStaff(), 315 | offhand: new CrystalShield(), 316 | helmet: new SaradominCoif(), 317 | necklace: new OccultNecklace(), 318 | cape: new AvasAssembler(), 319 | ammo: new RubyBoltsE(), 320 | chest: new AhrimsRobetop(), 321 | legs: new AhrimsRobeskirt(), 322 | feet: new PegasianBoots(), 323 | gloves: new BarrowsGloves(), 324 | ring: new RingOfSufferingImbued(), 325 | }, 326 | inventory: [ 327 | new Blowpipe(), 328 | new RuneCrossbow(), 329 | new DiamondBoltsE(), 330 | new JusticiarFaceguard(), 331 | new NecklaceOfAnguish(), 332 | new SaradominBody(), 333 | new SaradominChaps(), 334 | new JusticiarChestguard(), 335 | null, 336 | new SaradominBrew(), 337 | new SuperRestore(), 338 | new JusticiarLegguards(), 339 | new SaradominBrew(), 340 | new SaradominBrew(), 341 | new SuperRestore(), 342 | new SuperRestore(), 343 | new SaradominBrew(), 344 | new SaradominBrew(), 345 | new SuperRestore(), 346 | new SuperRestore(), 347 | new BastionPotion(), 348 | new StaminaPotion(), 349 | new SuperRestore(), 350 | new SuperRestore(), 351 | new BastionPotion(), 352 | new StaminaPotion(), 353 | new SuperRestore(), 354 | new SuperRestore(), 355 | ], 356 | }; 357 | } 358 | 359 | loadoutZerker() { 360 | return { 361 | equipment: { 362 | weapon: new KodaiWand(), 363 | offhand: new RuneKiteshield(), 364 | helmet: new SaradominCoif(), 365 | necklace: new OccultNecklace(), 366 | cape: new AvasAssembler(), 367 | ammo: new DragonArrows(), 368 | chest: new DagonhaiRobeTop(), 369 | legs: new SaradominChaps(), 370 | feet: new RangerBoots(), 371 | gloves: new BarrowsGloves(), 372 | ring: new RingOfSufferingImbued(), 373 | }, 374 | inventory: [ 375 | new Blowpipe(), 376 | new TwistedBow(), 377 | null, 378 | null, 379 | new NecklaceOfAnguish(), 380 | new SaradominBody(), 381 | null, 382 | null, 383 | new SaradominBrew(), 384 | new SaradominBrew(), 385 | new SuperRestore(), 386 | new SuperRestore(), 387 | new SaradominBrew(), 388 | new SaradominBrew(), 389 | new SuperRestore(), 390 | new SuperRestore(), 391 | new SaradominBrew(), 392 | new SaradominBrew(), 393 | new SuperRestore(), 394 | new SuperRestore(), 395 | new SaradominBrew(), 396 | new SaradominBrew(), 397 | new SuperRestore(), 398 | new SuperRestore(), 399 | new BastionPotion(), 400 | new StaminaPotion(), 401 | new SuperRestore(), 402 | new SuperRestore(), 403 | ], 404 | }; 405 | } 406 | 407 | loadoutPure() { 408 | return { 409 | equipment: { 410 | weapon: new KodaiWand(), 411 | offhand: new MagesBook(), 412 | helmet: new RobinHoodHat(), 413 | necklace: new OccultNecklace(), 414 | cape: new AvasAccumulator(), 415 | ammo: new DragonArrows(), 416 | chest: new GuthixRobeTop(), 417 | legs: new BlackDhideChaps(), 418 | feet: new RangerBoots(), 419 | gloves: new BlackDhideVambraces(), 420 | ring: new RingOfSufferingImbued(), 421 | }, 422 | inventory: [ 423 | new Blowpipe(), 424 | new TwistedBow(), 425 | null, 426 | null, 427 | new NecklaceOfAnguish(), 428 | null, 429 | null, 430 | null, 431 | new SaradominBrew(), 432 | new SaradominBrew(), 433 | new SuperRestore(), 434 | new SuperRestore(), 435 | new SaradominBrew(), 436 | new SaradominBrew(), 437 | new SuperRestore(), 438 | new SuperRestore(), 439 | new SaradominBrew(), 440 | new SaradominBrew(), 441 | new SuperRestore(), 442 | new SuperRestore(), 443 | new BastionPotion(), 444 | new StaminaPotion(), 445 | new SuperRestore(), 446 | new SuperRestore(), 447 | new BastionPotion(), 448 | new StaminaPotion(), 449 | new SuperRestore(), 450 | new SuperRestore(), 451 | ], 452 | }; 453 | } 454 | 455 | findItemByName(list: Item[], name: ItemName) { 456 | return indexOf(map(list, "itemName"), name); 457 | } 458 | 459 | findAnyItemWithName(list: Item[], names: ItemName[]) { 460 | return ( 461 | filter( 462 | names.map((name: ItemName) => { 463 | return this.findItemByName(list, name); 464 | }), 465 | (index: number) => index !== -1, 466 | )[0] || -1 467 | ); 468 | } 469 | 470 | setStats(player: Player) { 471 | player.stats.prayer = 99; 472 | player.currentStats.prayer = 99; 473 | player.stats.defence = 99; 474 | player.currentStats.defence = 99; 475 | switch (this.loadoutType) { 476 | case "zerker": 477 | player.stats.prayer = 52; 478 | player.currentStats.prayer = 52; 479 | player.stats.defence = 45; 480 | player.currentStats.defence = 45; 481 | break; 482 | case "pure": 483 | player.stats.prayer = 52; 484 | player.currentStats.prayer = 52; 485 | player.stats.defence = 1; 486 | player.currentStats.defence = 1; 487 | break; 488 | } 489 | } 490 | 491 | getLoadout(): UnitOptions { 492 | let loadout: UnitOptions; 493 | switch (this.loadoutType) { 494 | case "max_tbow_speed": 495 | loadout = this.loadoutMaxTbowSpeedrunner(); 496 | break; 497 | case "max_tbow": 498 | loadout = this.loadoutMaxTbow(); 499 | break; 500 | case "max_fbow": 501 | loadout = this.loadoutMaxFbow(); 502 | break; 503 | case "budget_fbow": 504 | loadout = this.loadoutBudgetFbow(); 505 | break; 506 | case "zerker": 507 | loadout = this.loadoutZerker(); 508 | break; 509 | case "pure": 510 | loadout = this.loadoutPure(); 511 | break; 512 | case "rcb": 513 | loadout = this.loadoutRcb(); 514 | break; 515 | case "max_melee": 516 | loadout = this.loadoutMaxMelee(); 517 | break; 518 | } 519 | 520 | if (this.wave > 66 && this.wave <= 69) { 521 | // switch necklace to range dps necklace 522 | loadout.inventory[this.findItemByName(loadout.inventory, ItemName.NECKLACE_OF_ANGUISH)] = new OccultNecklace(); 523 | loadout.equipment.necklace = new NecklaceOfAnguish(); 524 | 525 | // Swap out staff with zuk/jad dps weapon 526 | const staff = loadout.equipment.weapon; 527 | const bow = this.findAnyItemWithName(loadout.inventory, [ 528 | ItemName.TWISTED_BOW, 529 | ItemName.BOWFA, 530 | ItemName.RUNE_CROSSBOW, 531 | ]); 532 | loadout.equipment.weapon = loadout.inventory[bow] as Weapon; 533 | loadout.inventory[bow] = staff; 534 | if (loadout.equipment.offhand && loadout.equipment.weapon.isTwoHander) { 535 | loadout.inventory[loadout.inventory.indexOf(null)] = loadout.equipment.offhand; 536 | loadout.equipment.offhand = null; 537 | } 538 | 539 | // Swap out chest 540 | const mageChest = loadout.equipment.chest; 541 | const rangeChest = this.findAnyItemWithName(loadout.inventory, [ 542 | ItemName.MASORI_BODY_F, 543 | ItemName.ARMADYL_CHESTPLATE, 544 | ItemName.SARADOMIN_D_HIDE_BODY, 545 | ItemName.CRYSTAL_BODY, 546 | ]); 547 | if (rangeChest !== -1) { 548 | loadout.equipment.chest = loadout.inventory[rangeChest] as Chest; 549 | loadout.inventory[rangeChest] = mageChest; 550 | } 551 | 552 | // Swap out body 553 | const mageLegs = loadout.equipment.legs; 554 | const rangeLegs = this.findAnyItemWithName(loadout.inventory, [ 555 | ItemName.MASORI_CHAPS_F, 556 | ItemName.ARMADYL_CHAINSKIRT, 557 | ItemName.SARADOMIN_D_HIDE_CHAPS, 558 | ItemName.CRYSTAL_LEGS, 559 | ]); 560 | if (rangeLegs !== -1) { 561 | loadout.equipment.legs = loadout.inventory[rangeLegs] as Legs; 562 | loadout.inventory[rangeLegs] = mageLegs; 563 | } 564 | } 565 | 566 | if (this.onTask && this.loadoutType !== "pure") { 567 | loadout.equipment.helmet = new SlayerHelmet(); 568 | } 569 | 570 | return loadout; 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /src/content/inferno/js/InfernoRegion.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { BrowserUtils, CardinalDirection, ControlPanelController, Entity, EntityNames, ImageLoader, InvisibleMovementBlocker, Location, Mob, Player, Region, Settings, TileMarker, Trainer, Viewport } from "osrs-sdk"; 3 | 4 | import InfernoMapImage from "../assets/images/map.png"; 5 | 6 | import { filter, shuffle } from "lodash"; 7 | import { InfernoLoadout } from "./InfernoLoadout"; 8 | import { InfernoMobDeathStore } from "./InfernoMobDeathStore"; 9 | import { InfernoPillar } from "./InfernoPillar"; 10 | import { InfernoScene } from "./InfernoScene"; 11 | import { InfernoSettings } from "./InfernoSettings"; 12 | import { InfernoWaves } from "./InfernoWaves"; 13 | import { JalAk } from "./mobs/JalAk"; 14 | import { JalImKot } from "./mobs/JalImKot"; 15 | import { JalMejRah } from "./mobs/JalMejRah"; 16 | import { JalTokJad } from "./mobs/JalTokJad"; 17 | import { JalXil } from "./mobs/JalXil"; 18 | import { JalZek } from "./mobs/JalZek"; 19 | import { TzKalZuk } from "./mobs/TzKalZuk"; 20 | import { Wall } from "./Wall"; 21 | import { ZukShield } from "./ZukShield"; 22 | 23 | import SidebarContent from "../sidebar.html"; 24 | 25 | /* eslint-disable @typescript-eslint/no-explicit-any */ 26 | 27 | export class InfernoRegion extends Region { 28 | wave: number; 29 | mapImage: HTMLImageElement = ImageLoader.createImage(InfernoMapImage); 30 | 31 | // Wave progression properties 32 | private waveCompleteTimer = -1; // -1 = not triggered, 0-7 = countdown to next wave 33 | private lastMobCount = 0; 34 | private waveProgressionEnabled = false; 35 | 36 | // Spawn indicator entities 37 | private spawnIndicators: Entity[] = []; 38 | 39 | get initialFacing() { 40 | return this.wave === 69 ? CardinalDirection.NORTH : CardinalDirection.SOUTH; 41 | } 42 | 43 | getName() { 44 | return "Inferno"; 45 | } 46 | 47 | get width(): number { 48 | return 51; 49 | } 50 | 51 | get height(): number { 52 | return 57; 53 | } 54 | 55 | rightClickActions(): any[] { 56 | if (this.wave !== 0) { 57 | return []; 58 | } 59 | 60 | return [ 61 | { 62 | text: [ 63 | { text: "Spawn ", fillStyle: "white" }, 64 | { text: "Bat", fillStyle: "blue" }, 65 | ], 66 | action: () => { 67 | Trainer.clickController.yellowClick(); 68 | const x = Viewport.viewport.contextMenu.destinationLocation.x; 69 | const y = Viewport.viewport.contextMenu.destinationLocation.y; 70 | const mob = new JalMejRah(this, { x, y }, { aggro: Trainer.player }); 71 | mob.removableWithRightClick = true; 72 | this.addMob(mob); 73 | }, 74 | }, 75 | 76 | { 77 | text: [ 78 | { text: "Spawn ", fillStyle: "white" }, 79 | { text: "Blob", fillStyle: "green" }, 80 | ], 81 | action: () => { 82 | Trainer.clickController.yellowClick(); 83 | const x = Viewport.viewport.contextMenu.destinationLocation.x; 84 | const y = Viewport.viewport.contextMenu.destinationLocation.y; 85 | const mob = new JalAk(this, { x, y }, { aggro: Trainer.player }); 86 | mob.removableWithRightClick = true; 87 | this.addMob(mob); 88 | }, 89 | }, 90 | 91 | { 92 | text: [ 93 | { text: "Spawn ", fillStyle: "white" }, 94 | { text: "Meleer", fillStyle: "yellow" }, 95 | ], 96 | action: () => { 97 | Trainer.clickController.yellowClick(); 98 | const x = Viewport.viewport.contextMenu.destinationLocation.x; 99 | const y = Viewport.viewport.contextMenu.destinationLocation.y; 100 | const mob = new JalImKot(this, { x, y }, { aggro: Trainer.player }); 101 | mob.removableWithRightClick = true; 102 | this.addMob(mob); 103 | }, 104 | }, 105 | 106 | { 107 | text: [ 108 | { text: "Spawn ", fillStyle: "white" }, 109 | { text: "Ranger", fillStyle: "orange" }, 110 | ], 111 | action: () => { 112 | Trainer.clickController.yellowClick(); 113 | const x = Viewport.viewport.contextMenu.destinationLocation.x; 114 | const y = Viewport.viewport.contextMenu.destinationLocation.y; 115 | const mob = new JalXil(this, { x, y }, { aggro: Trainer.player }); 116 | mob.removableWithRightClick = true; 117 | this.addMob(mob); 118 | }, 119 | }, 120 | 121 | { 122 | text: [ 123 | { text: "Spawn ", fillStyle: "white" }, 124 | { text: "Mager", fillStyle: "red" }, 125 | ], 126 | action: () => { 127 | Trainer.clickController.yellowClick(); 128 | const x = Viewport.viewport.contextMenu.destinationLocation.x; 129 | const y = Viewport.viewport.contextMenu.destinationLocation.y; 130 | const mob = new JalZek(this, { x, y }, { aggro: Trainer.player }); 131 | mob.removableWithRightClick = true; 132 | this.addMob(mob); 133 | }, 134 | }, 135 | ]; 136 | } 137 | 138 | initializeAndGetLoadoutType() { 139 | const loadoutSelector = document.getElementById("loadouts") as HTMLInputElement; 140 | loadoutSelector.value = Settings.loadout; 141 | loadoutSelector.addEventListener("change", () => { 142 | Settings.loadout = loadoutSelector.value; 143 | Settings.persistToStorage(); 144 | }); 145 | 146 | return loadoutSelector.value; 147 | } 148 | 149 | initializeAndGetOnTask() { 150 | const onTaskCheckbox = document.getElementById("onTask") as HTMLInputElement; 151 | onTaskCheckbox.checked = Settings.onTask; 152 | onTaskCheckbox.addEventListener("change", () => { 153 | Settings.onTask = onTaskCheckbox.checked; 154 | Settings.persistToStorage(); 155 | }); 156 | return onTaskCheckbox.checked; 157 | } 158 | 159 | initializeAndGetSouthPillar() { 160 | const southPillarCheckbox = document.getElementById("southPillar") as HTMLInputElement; 161 | southPillarCheckbox.checked = Settings.southPillar; 162 | southPillarCheckbox.addEventListener("change", () => { 163 | Settings.southPillar = southPillarCheckbox.checked; 164 | Settings.persistToStorage(); 165 | }); 166 | return southPillarCheckbox.checked; 167 | } 168 | 169 | initializeAndGetWestPillar() { 170 | const westPillarCheckbox = document.getElementById("westPillar") as HTMLInputElement; 171 | westPillarCheckbox.checked = Settings.westPillar; 172 | westPillarCheckbox.addEventListener("change", () => { 173 | Settings.westPillar = westPillarCheckbox.checked; 174 | Settings.persistToStorage(); 175 | }); 176 | return westPillarCheckbox.checked; 177 | } 178 | 179 | initializeAndGetNorthPillar() { 180 | const northPillarCheckbox = document.getElementById("northPillar") as HTMLInputElement; 181 | northPillarCheckbox.checked = Settings.northPillar; 182 | northPillarCheckbox.addEventListener("change", () => { 183 | Settings.northPillar = northPillarCheckbox.checked; 184 | Settings.persistToStorage(); 185 | }); 186 | return northPillarCheckbox.checked; 187 | } 188 | 189 | initializeAndGetUse3dView() { 190 | const use3dViewCheckbox = document.getElementById("use3dView") as HTMLInputElement; 191 | use3dViewCheckbox.checked = Settings.use3dView; 192 | use3dViewCheckbox.addEventListener("change", () => { 193 | Settings.use3dView = use3dViewCheckbox.checked; 194 | Settings.persistToStorage(); 195 | window.location.reload(); 196 | }); 197 | return use3dViewCheckbox.checked; 198 | } 199 | 200 | initializeWaveProgressionToggle() { 201 | const waveProgressionCheckbox = document.getElementById("waveProgression") as HTMLInputElement; 202 | waveProgressionCheckbox.checked = InfernoSettings.waveProgression === true; 203 | waveProgressionCheckbox.addEventListener("change", () => { 204 | InfernoSettings.waveProgression = waveProgressionCheckbox.checked; 205 | InfernoSettings.persistToStorage(); 206 | }); 207 | } 208 | 209 | initializeSpawnIndicatorsToggle() { 210 | const spawnIndicatorsCheckbox = document.getElementById("spawnIndicators") as HTMLInputElement; 211 | spawnIndicatorsCheckbox.checked = InfernoSettings.spawnIndicators === true; 212 | spawnIndicatorsCheckbox.addEventListener("change", () => { 213 | InfernoSettings.spawnIndicators = spawnIndicatorsCheckbox.checked; 214 | InfernoSettings.persistToStorage(); 215 | // Update current spawn indicators visibility 216 | if (!InfernoSettings.spawnIndicators) { 217 | this.clearSpawnIndicators(); 218 | } else { 219 | // Refresh spawn indicators if enabled 220 | const spawns = InfernoWaves.getRandomSpawns(); 221 | this.updateSpawnIndicators(spawns); 222 | } 223 | }); 224 | } 225 | 226 | initializeDisplaySetTimerToggle() { 227 | const displaySetTimerCheckbox = document.getElementById("displaySetTimer") as HTMLInputElement; 228 | displaySetTimerCheckbox.checked = InfernoSettings.displaySetTimer === true; 229 | displaySetTimerCheckbox.addEventListener("change", () => { 230 | InfernoSettings.displaySetTimer = displaySetTimerCheckbox.checked; 231 | InfernoSettings.persistToStorage(); 232 | }); 233 | } 234 | 235 | initialiseRegion() { 236 | const waveInput: HTMLInputElement = document.getElementById("waveinput") as HTMLInputElement; 237 | 238 | 239 | const exportWaveInput: HTMLButtonElement = document.getElementById("exportCustomWave") as HTMLButtonElement; 240 | const editWaveInput: HTMLButtonElement = document.getElementById("editWave") as HTMLButtonElement; 241 | 242 | editWaveInput.addEventListener("click", () => { 243 | const magers = filter(this.mobs, (mob: Mob) => { 244 | return mob.mobName() === EntityNames.JAL_ZEK; 245 | }).map((mob: Mob) => { 246 | return [mob.location.x - 11, mob.location.y - 14]; 247 | }); 248 | 249 | const rangers = filter(this.mobs, (mob: Mob) => { 250 | return mob.mobName() === EntityNames.JAL_XIL; 251 | }).map((mob: Mob) => { 252 | return [mob.location.x - 11, mob.location.y - 14]; 253 | }); 254 | 255 | const meleers = filter(this.mobs, (mob: Mob) => { 256 | return mob.mobName() === EntityNames.JAL_IM_KOT; 257 | }).map((mob: Mob) => { 258 | return [mob.location.x - 11, mob.location.y - 14]; 259 | }); 260 | 261 | const blobs = filter(this.mobs, (mob: Mob) => { 262 | return mob.mobName() === EntityNames.JAL_AK; 263 | }).map((mob: Mob) => { 264 | return [mob.location.x - 11, mob.location.y - 14]; 265 | }); 266 | 267 | const bats = filter(this.mobs, (mob: Mob) => { 268 | return mob.mobName() === EntityNames.JAL_MEJ_RAJ; 269 | }).map((mob: Mob) => { 270 | return [mob.location.x - 11, mob.location.y - 14]; 271 | }); 272 | 273 | const url = `/?wave=0&mager=${JSON.stringify(magers)}&ranger=${JSON.stringify( 274 | rangers, 275 | )}&melee=${JSON.stringify(meleers)}&blob=${JSON.stringify(blobs)}&bat=${JSON.stringify(bats)}©able`; 276 | window.location.href = url; 277 | }); 278 | exportWaveInput.addEventListener("click", () => { 279 | const magers = filter(this.mobs, (mob: Mob) => { 280 | return mob.mobName() === EntityNames.JAL_ZEK; 281 | }).map((mob: Mob) => { 282 | return [mob.location.x - 11, mob.location.y - 14]; 283 | }); 284 | 285 | const rangers = filter(this.mobs, (mob: Mob) => { 286 | return mob.mobName() === EntityNames.JAL_XIL; 287 | }).map((mob: Mob) => { 288 | return [mob.location.x - 11, mob.location.y - 14]; 289 | }); 290 | 291 | const meleers = filter(this.mobs, (mob: Mob) => { 292 | return mob.mobName() === EntityNames.JAL_IM_KOT; 293 | }).map((mob: Mob) => { 294 | return [mob.location.x - 11, mob.location.y - 14]; 295 | }); 296 | 297 | const blobs = filter(this.mobs, (mob: Mob) => { 298 | return mob.mobName() === EntityNames.JAL_AK; 299 | }).map((mob: Mob) => { 300 | return [mob.location.x - 11, mob.location.y - 14]; 301 | }); 302 | 303 | const bats = filter(this.mobs, (mob: Mob) => { 304 | return mob.mobName() === EntityNames.JAL_MEJ_RAJ; 305 | }).map((mob: Mob) => { 306 | return [mob.location.x - 11, mob.location.y - 14]; 307 | }); 308 | 309 | const url = `/?wave=74&mager=${JSON.stringify(magers)}&ranger=${JSON.stringify(rangers)}&melee=${JSON.stringify( 310 | meleers, 311 | )}&blob=${JSON.stringify(blobs)}&bat=${JSON.stringify(bats)}©able`; 312 | window.location.href = url; 313 | }); 314 | 315 | // create player 316 | const player = new Player(this, { 317 | x: parseInt(BrowserUtils.getQueryVar("x")) || 25, 318 | y: parseInt(BrowserUtils.getQueryVar("y")) || 25, 319 | }); 320 | 321 | this.addPlayer(player); 322 | 323 | const loadoutType = this.initializeAndGetLoadoutType(); 324 | const onTask = this.initializeAndGetOnTask(); 325 | const southPillar = this.initializeAndGetSouthPillar(); 326 | const westPillar = this.initializeAndGetWestPillar(); 327 | const northPillar = this.initializeAndGetNorthPillar(); 328 | 329 | this.initializeAndGetUse3dView(); 330 | this.initializeWaveProgressionToggle(); 331 | this.initializeSpawnIndicatorsToggle(); 332 | this.initializeDisplaySetTimerToggle(); 333 | this.wave = parseInt(BrowserUtils.getQueryVar("wave")); 334 | 335 | if (isNaN(this.wave)) { 336 | this.wave = 62; 337 | } 338 | if (this.wave < 0) { 339 | this.wave = 0; 340 | } 341 | if (this.wave > InfernoWaves.waves.length + 8) { 342 | this.wave = InfernoWaves.waves.length + 8; 343 | } 344 | 345 | const loadout = new InfernoLoadout(this.wave, loadoutType, onTask); 346 | loadout.setStats(player); // flip this around one day 347 | player.setUnitOptions(loadout.getLoadout()); 348 | 349 | if (this.wave < 67 || this.wave >= 70) { 350 | // Add pillars 351 | InfernoPillar.addPillarsToWorld(this, southPillar, westPillar, northPillar); 352 | } 353 | 354 | const randomPillar = (shuffle(this.entities.filter((entity) => entity.entityName() === EntityNames.PILLAR)) || [ 355 | null, 356 | ])[0]; // Since we've only added pillars this is safe. Do not move to after movement blockers. 357 | 358 | for (let x = 10; x < 41; x++) { 359 | this.addEntity(new InvisibleMovementBlocker(this, { x, y: 13 })); 360 | this.addEntity(new InvisibleMovementBlocker(this, { x, y: 44 })); 361 | } 362 | for (let y = 14; y < 44; y++) { 363 | this.addEntity(new InvisibleMovementBlocker(this, { x: 10, y })); 364 | this.addEntity(new InvisibleMovementBlocker(this, { x: 40, y })); 365 | } 366 | 367 | const bat = BrowserUtils.getQueryVar("bat") || "[]"; 368 | const blob = BrowserUtils.getQueryVar("blob") || "[]"; 369 | const melee = BrowserUtils.getQueryVar("melee") || "[]"; 370 | const ranger = BrowserUtils.getQueryVar("ranger") || "[]"; 371 | const mager = BrowserUtils.getQueryVar("mager") || "[]"; 372 | const replayLink = document.getElementById("replayLink") as HTMLLinkElement; 373 | 374 | function importSpawn(region: Region) { 375 | try { 376 | JSON.parse(mager).forEach((spawn: number[]) => 377 | region.addMob(new JalZek(region, { x: spawn[0] + 11, y: spawn[1] + 14 }, { aggro: player })), 378 | ); 379 | JSON.parse(ranger).forEach((spawn: number[]) => 380 | region.addMob(new JalXil(region, { x: spawn[0] + 11, y: spawn[1] + 14 }, { aggro: player })), 381 | ); 382 | JSON.parse(melee).forEach((spawn: number[]) => 383 | region.addMob(new JalImKot(region, { x: spawn[0] + 11, y: spawn[1] + 14 }, { aggro: player })), 384 | ); 385 | JSON.parse(blob).forEach((spawn: number[]) => 386 | region.addMob(new JalAk(region, { x: spawn[0] + 11, y: spawn[1] + 14 }, { aggro: player })), 387 | ); 388 | JSON.parse(bat).forEach((spawn: number[]) => 389 | region.addMob(new JalMejRah(region, { x: spawn[0] + 11, y: spawn[1] + 14 }, { aggro: player })), 390 | ); 391 | 392 | InfernoWaves.spawnNibblers(3, region, randomPillar).forEach(region.addMob.bind(region)); 393 | 394 | replayLink.href = `/${window.location.search}`; 395 | } catch (ex) { 396 | console.log("failed to import wave from inferno stats", ex); 397 | } 398 | } 399 | // Add mobs 400 | if (this.wave === 0) { 401 | // world.getReadyTimer = 0; 402 | player.location = { x: 28, y: 17 }; 403 | this.world.getReadyTimer = -1; 404 | 405 | // Clear death store when starting any wave 406 | InfernoMobDeathStore.clearDeadMobs(); 407 | 408 | // Use our spawn indicator system instead of manual tile markers 409 | const spawns = InfernoWaves.getRandomSpawns(); 410 | this.updateSpawnIndicators(spawns); 411 | 412 | importSpawn(this); 413 | } else if (this.wave < 67) { 414 | player.location = { x: 28, y: 17 }; 415 | if (bat != "[]" || blob != "[]" || melee != "[]" || ranger != "[]" || mager != "[]") { 416 | // Backwards compatibility layer for runelite plugin 417 | this.wave = 1; 418 | 419 | // Clear death store when starting any wave 420 | InfernoMobDeathStore.clearDeadMobs(); 421 | 422 | importSpawn(this); 423 | } else { 424 | // Native approach 425 | const customSpawns = BrowserUtils.getQueryVar("spawns") 426 | ? JSON.parse(decodeURIComponent(BrowserUtils.getQueryVar("spawns"))) 427 | : undefined; 428 | 429 | this.spawnRegularWave(player, randomPillar, customSpawns); 430 | } 431 | } else if (this.wave === 67) { 432 | // Clear death store when starting special waves 433 | InfernoMobDeathStore.clearDeadMobs(); 434 | 435 | player.location = { x: 18, y: 25 }; 436 | const jad = new JalTokJad( 437 | this, 438 | { x: 23, y: 27 }, 439 | { aggro: player, attackSpeed: 8, stun: 1, healers: 5, isZukWave: false }, 440 | ); 441 | this.addMob(jad); 442 | } else if (this.wave === 68) { 443 | // Clear death store when starting special waves 444 | InfernoMobDeathStore.clearDeadMobs(); 445 | 446 | player.location = { x: 25, y: 27 }; 447 | 448 | const jad1 = new JalTokJad( 449 | this, 450 | { x: 18, y: 24 }, 451 | { aggro: player, attackSpeed: 9, stun: 1, healers: 3, isZukWave: false }, 452 | ); 453 | this.addMob(jad1); 454 | 455 | const jad2 = new JalTokJad( 456 | this, 457 | { x: 28, y: 24 }, 458 | { aggro: player, attackSpeed: 9, stun: 7, healers: 3, isZukWave: false }, 459 | ); 460 | this.addMob(jad2); 461 | 462 | const jad3 = new JalTokJad( 463 | this, 464 | { x: 23, y: 35 }, 465 | { aggro: player, attackSpeed: 9, stun: 4, healers: 3, isZukWave: false }, 466 | ); 467 | this.addMob(jad3); 468 | } else if (this.wave === 69) { 469 | // Clear death store when starting special waves 470 | InfernoMobDeathStore.clearDeadMobs(); 471 | 472 | player.location = { x: 25, y: 15 }; 473 | 474 | // spawn zuk 475 | const shield = new ZukShield(this, { x: 23, y: 13 }, { aggro: player }); 476 | this.addMob(shield); 477 | 478 | this.addMob(new TzKalZuk(this, { x: 22, y: 8 }, { aggro: player })); 479 | 480 | this.addEntity(new Wall(this, { x: 21, y: 8 })); 481 | this.addEntity(new Wall(this, { x: 21, y: 7 })); 482 | this.addEntity(new Wall(this, { x: 21, y: 6 })); 483 | this.addEntity(new Wall(this, { x: 21, y: 5 })); 484 | this.addEntity(new Wall(this, { x: 21, y: 4 })); 485 | this.addEntity(new Wall(this, { x: 21, y: 3 })); 486 | this.addEntity(new Wall(this, { x: 21, y: 2 })); 487 | this.addEntity(new Wall(this, { x: 21, y: 1 })); 488 | this.addEntity(new Wall(this, { x: 21, y: 0 })); 489 | this.addEntity(new Wall(this, { x: 29, y: 8 })); 490 | this.addEntity(new Wall(this, { x: 29, y: 7 })); 491 | this.addEntity(new Wall(this, { x: 29, y: 6 })); 492 | this.addEntity(new Wall(this, { x: 29, y: 5 })); 493 | this.addEntity(new Wall(this, { x: 29, y: 4 })); 494 | this.addEntity(new Wall(this, { x: 29, y: 3 })); 495 | this.addEntity(new Wall(this, { x: 29, y: 2 })); 496 | this.addEntity(new Wall(this, { x: 29, y: 1 })); 497 | this.addEntity(new Wall(this, { x: 29, y: 0 })); 498 | 499 | this.addEntity(new TileMarker(this, { x: 14, y: 14 }, "#00FF00", 1, false)); 500 | 501 | this.addEntity(new TileMarker(this, { x: 16, y: 14 }, "#FF0000", 1, false)); 502 | this.addEntity(new TileMarker(this, { x: 17, y: 14 }, "#FF0000", 1, false)); 503 | this.addEntity(new TileMarker(this, { x: 18, y: 14 }, "#FF0000", 1, false)); 504 | 505 | this.addEntity(new TileMarker(this, { x: 20, y: 14 }, "#00FF00", 1, false)); 506 | 507 | this.addEntity(new TileMarker(this, { x: 30, y: 14 }, "#00FF00", 1, false)); 508 | 509 | this.addEntity(new TileMarker(this, { x: 32, y: 14 }, "#FF0000", 1, false)); 510 | this.addEntity(new TileMarker(this, { x: 33, y: 14 }, "#FF0000", 1, false)); 511 | this.addEntity(new TileMarker(this, { x: 34, y: 14 }, "#FF0000", 1, false)); 512 | 513 | this.addEntity(new TileMarker(this, { x: 36, y: 14 }, "#00FF00", 1, false)); 514 | } else if (this.wave === 74) { 515 | player.location = { x: 28, y: 17 }; 516 | importSpawn(this); 517 | } 518 | 519 | document.getElementById("playWaveNum").addEventListener("click", () => { 520 | window.location.href = `/?wave=${waveInput.value || this.wave}`; 521 | }); 522 | 523 | document 524 | .getElementById("pauseResumeLink") 525 | .addEventListener("click", () => (this.world.isPaused ? this.world.startTicking() : this.world.stopTicking())); 526 | 527 | waveInput.addEventListener("focus", () => (ControlPanelController.controller.isUsingExternalUI = true)); 528 | waveInput.addEventListener("focusout", () => (ControlPanelController.controller.isUsingExternalUI = false)); 529 | 530 | // set timer 531 | let timer_mode = "Start Set Timer"; 532 | let timer_time = 210; 533 | 534 | setInterval(() => { 535 | if ( 536 | timer_mode === "Start Set Timer" || 537 | timer_mode === "Resume" 538 | ) { 539 | return; 540 | } 541 | timer_time--; 542 | if (timer_time <= 0) { 543 | timer_time = 210; 544 | timer_mode = "Start Set Timer"; 545 | } 546 | document.getElementById("set_timer_time").innerText = 547 | String(Math.floor(timer_time / 60)) + 548 | ":" + 549 | String(timer_time % 60).padStart(2, "0"); 550 | document.getElementById("set_timer_button").innerText = 551 | timer_mode; 552 | }, 1000); 553 | document 554 | .getElementById("set_timer_button") 555 | .addEventListener("click", () => { 556 | if (timer_mode === "Start Set Timer") { 557 | timer_mode = "Pause"; 558 | } else if (timer_mode === "Pause") { 559 | timer_mode = "Resume"; 560 | } else if (timer_mode === "Resume") { 561 | timer_mode = "Reset"; 562 | timer_time += 105; 563 | } else if (timer_mode === "Reset") { 564 | timer_time = 210; 565 | timer_mode = "Start Set Timer"; 566 | } 567 | document.getElementById("set_timer_time").innerText = 568 | String(Math.floor(timer_time / 60)) + 569 | ":" + 570 | String(timer_time % 60).padStart(2, "0"); 571 | document.getElementById("set_timer_button").innerText = 572 | timer_mode; 573 | }); 574 | 575 | 576 | // Add 3d scene 577 | if (Settings.use3dView) { 578 | this.addEntity(new InfernoScene(this, { x: 0, y: 48 })); 579 | } 580 | 581 | player.perceivedLocation = player.location; 582 | player.destinationLocation = player.location; 583 | 584 | return { player }; 585 | } 586 | 587 | drawWorldBackground(context: OffscreenCanvasRenderingContext2D, scale: number) { 588 | context.fillStyle = "black"; 589 | context.fillRect(0, 0, 10000000, 10000000); 590 | if (this.mapImage) { 591 | const ctx = context as any; 592 | ctx.webkitImageSmoothingEnabled = false; 593 | ctx.mozImageSmoothingEnabled = false; 594 | context.imageSmoothingEnabled = false; 595 | 596 | context.fillStyle = "white"; 597 | 598 | context.drawImage(this.mapImage, 0, 0, this.width * scale, this.height * scale); 599 | 600 | ctx.webkitImageSmoothingEnabled = true; 601 | ctx.mozImageSmoothingEnabled = true; 602 | context.imageSmoothingEnabled = true; 603 | } 604 | } 605 | 606 | drawDefaultFloor() { 607 | // replaced by an Entity in 3d view 608 | return !Settings.use3dView; 609 | } 610 | 611 | // Spawn indicator management methods 612 | private clearSpawnIndicators() { 613 | this.spawnIndicators.forEach(indicator => { 614 | this.removeEntity(indicator); 615 | }); 616 | this.spawnIndicators = []; 617 | } 618 | 619 | private updateSpawnIndicators(spawns: Location[]) { 620 | // Clear existing indicators 621 | this.clearSpawnIndicators(); 622 | 623 | // Only show spawn indicators if the setting is enabled 624 | if (!InfernoSettings.spawnIndicators) { 625 | return; 626 | } 627 | 628 | console.log("Updating spawn indicators for spawns:", spawns); 629 | 630 | // Add new indicators for current spawn points 631 | spawns.forEach((spawn: Location, index: number) => { 632 | // Create multiple size indicators with completely isolated location objects 633 | [2, 3, 4].forEach((size: number) => { 634 | const color = index < 9 ? "#00FF0050" : "#FF000050"; // Green for valid spawns, red for overflow 635 | // Create a completely isolated copy of the location to prevent any reference sharing 636 | const isolatedLocation = { x: spawn.x, y: spawn.y }; 637 | const tileMarker = new TileMarker(this, isolatedLocation, color, size, false); 638 | this.addEntity(tileMarker); 639 | this.spawnIndicators.push(tileMarker); 640 | }); 641 | }); 642 | 643 | console.log(`Added ${this.spawnIndicators.length} spawn indicator entities`); 644 | } 645 | 646 | postTick() { 647 | super.postTick(); 648 | this.handleWaveProgression(); 649 | } 650 | 651 | private handleWaveProgression() { 652 | // Only enable wave progression for waves 1-69 and if the setting is enabled 653 | if (this.wave >= 1 && this.wave <= 69 && InfernoSettings.waveProgression) { 654 | this.waveProgressionEnabled = true; 655 | } else { 656 | this.waveProgressionEnabled = false; 657 | } 658 | 659 | if (!this.waveProgressionEnabled) { 660 | return; 661 | } 662 | 663 | // Count current alive mobs (with special handling for nibblers) 664 | const aliveMobs = this.mobs.filter(mob => { 665 | return mob.dying === -1; 666 | }); 667 | 668 | const currentMobCount = aliveMobs.length; 669 | 670 | // Check if wave just completed (all relevant mobs dead) 671 | if (currentMobCount === 0 && this.lastMobCount > 0 && this.waveCompleteTimer === -1) { 672 | // Wave completed! Start 9-tick timer (1 extra tick to allow for bloblet spawning) 673 | this.waveCompleteTimer = 9; 674 | console.log(`Wave ${this.wave} completed! Next wave spawning in 9 ticks (allowing for bloblets)...`); 675 | 676 | // Update wave display 677 | const waveInput = document.getElementById("waveinput") as HTMLInputElement; 678 | if (waveInput) { 679 | waveInput.value = String(this.wave + 1); 680 | } 681 | } 682 | 683 | // Cancel wave completion if mobs spawned during countdown (e.g., bloblets) 684 | if (this.waveCompleteTimer > 0 && currentMobCount > 0) { 685 | console.log(`Wave ${this.wave} completion cancelled - new mobs detected (likely bloblets)`); 686 | this.waveCompleteTimer = -1; 687 | 688 | // Revert wave display 689 | const waveInput = document.getElementById("waveinput") as HTMLInputElement; 690 | if (waveInput) { 691 | waveInput.value = String(this.wave); 692 | } 693 | } 694 | 695 | // Handle countdown timer 696 | if (this.waveCompleteTimer > 0) { 697 | this.waveCompleteTimer--; 698 | 699 | if (this.waveCompleteTimer === 0) { 700 | // Timer finished, spawn next wave 701 | this.spawnNextWave(); 702 | this.waveCompleteTimer = -1; 703 | } 704 | } 705 | 706 | this.lastMobCount = currentMobCount; 707 | } 708 | 709 | private spawnRegularWave(player: any, randomPillar: any, customSpawns?: Location[]) { 710 | // Common logic for spawning regular waves (1-66) 711 | const spawns = customSpawns || InfernoWaves.getRandomSpawns(); 712 | 713 | // Clear death store to prevent resurrection of mobs from previous waves 714 | InfernoMobDeathStore.clearDeadMobs(); 715 | 716 | // Add spawn indicators before spawning mobs 717 | this.updateSpawnIndicators(spawns); 718 | 719 | // Spawn the mobs 720 | InfernoWaves.spawn(this, player, randomPillar, spawns, this.wave).forEach(this.addMob.bind(this)); 721 | 722 | // Update replay link and wave input 723 | const encodedSpawn = encodeURIComponent(JSON.stringify(spawns)); 724 | const replayLink = document.getElementById("replayLink") as HTMLLinkElement; 725 | if (replayLink) { 726 | replayLink.href = `/?wave=${this.wave}&x=${player.location.x}&y=${player.location.y}&spawns=${encodedSpawn}`; 727 | } 728 | 729 | const waveInput = document.getElementById("waveinput") as HTMLInputElement; 730 | if (waveInput) { 731 | waveInput.value = String(this.wave); 732 | } 733 | } 734 | 735 | private spawnNextWave() { 736 | if (this.wave >= 69) { 737 | // Don't spawn anything after wave 69 738 | console.log("Inferno completed! No more waves to spawn."); 739 | this.waveProgressionEnabled = false; 740 | return; 741 | } 742 | 743 | // Increment to next wave 744 | this.wave++; 745 | console.log(`Spawning wave ${this.wave}...`); 746 | 747 | // Get player reference 748 | const player = this.players[0]; 749 | if (!player) { 750 | console.error("No player found for wave progression"); 751 | return; 752 | } 753 | 754 | // Get random pillar for nibblers 755 | const randomPillar = (shuffle(this.entities.filter((entity) => entity.entityName() === EntityNames.PILLAR)) || [null])[0]; 756 | 757 | // Spawn the next wave based on wave number 758 | if (this.wave >= 1 && this.wave <= 66) { 759 | // Regular waves (1-66) 760 | this.spawnRegularWave(player, randomPillar); 761 | } else if (this.wave === 67) { 762 | // Jad wave - clear spawn indicators since it's a special spawn 763 | this.clearSpawnIndicators(); 764 | 765 | // Clear death store for special waves 766 | InfernoMobDeathStore.clearDeadMobs(); 767 | 768 | player.location = { x: 18, y: 25 }; 769 | const jad = new JalTokJad( 770 | this, 771 | { x: 23, y: 27 }, 772 | { aggro: player, attackSpeed: 8, stun: 1, healers: 5, isZukWave: false }, 773 | ); 774 | this.addMob(jad); 775 | } else if (this.wave === 68) { 776 | // Triple Jad wave - clear spawn indicators since it's a special spawn 777 | this.clearSpawnIndicators(); 778 | 779 | // Clear death store for special waves 780 | InfernoMobDeathStore.clearDeadMobs(); 781 | 782 | player.location = { x: 25, y: 27 }; 783 | 784 | const jad1 = new JalTokJad( 785 | this, 786 | { x: 18, y: 24 }, 787 | { aggro: player, attackSpeed: 9, stun: 1, healers: 3, isZukWave: false }, 788 | ); 789 | this.addMob(jad1); 790 | 791 | const jad2 = new JalTokJad( 792 | this, 793 | { x: 28, y: 24 }, 794 | { aggro: player, attackSpeed: 9, stun: 7, healers: 3, isZukWave: false }, 795 | ); 796 | this.addMob(jad2); 797 | 798 | const jad3 = new JalTokJad( 799 | this, 800 | { x: 23, y: 35 }, 801 | { aggro: player, attackSpeed: 9, stun: 4, healers: 3, isZukWave: false }, 802 | ); 803 | this.addMob(jad3); 804 | } else if (this.wave === 69) { 805 | // Zuk wave 806 | // Clear death store for special waves 807 | InfernoMobDeathStore.clearDeadMobs(); 808 | 809 | player.location = { x: 25, y: 15 }; 810 | 811 | // Remove pillars for Zuk wave 812 | this.entities = this.entities.filter(entity => entity.entityName() !== EntityNames.PILLAR); 813 | 814 | // Spawn zuk 815 | const shield = new ZukShield(this, { x: 23, y: 13 }, { aggro: player }); 816 | this.addMob(shield); 817 | 818 | this.addMob(new TzKalZuk(this, { x: 22, y: 8 }, { aggro: player })); 819 | 820 | // Add walls 821 | for (let y = 0; y <= 8; y++) { 822 | this.addEntity(new Wall(this, { x: 21, y })); 823 | this.addEntity(new Wall(this, { x: 29, y })); 824 | } 825 | 826 | // Add tile markers 827 | this.addEntity(new TileMarker(this, { x: 14, y: 14 }, "#00FF00", 1, false)); 828 | this.addEntity(new TileMarker(this, { x: 16, y: 14 }, "#FF0000", 1, false)); 829 | this.addEntity(new TileMarker(this, { x: 17, y: 14 }, "#FF0000", 1, false)); 830 | this.addEntity(new TileMarker(this, { x: 18, y: 14 }, "#FF0000", 1, false)); 831 | this.addEntity(new TileMarker(this, { x: 20, y: 14 }, "#00FF00", 1, false)); 832 | this.addEntity(new TileMarker(this, { x: 30, y: 14 }, "#00FF00", 1, false)); 833 | this.addEntity(new TileMarker(this, { x: 32, y: 14 }, "#FF0000", 1, false)); 834 | this.addEntity(new TileMarker(this, { x: 33, y: 14 }, "#FF0000", 1, false)); 835 | this.addEntity(new TileMarker(this, { x: 34, y: 14 }, "#FF0000", 1, false)); 836 | this.addEntity(new TileMarker(this, { x: 36, y: 14 }, "#00FF00", 1, false)); 837 | } 838 | 839 | // Update wave input display 840 | const waveInput = document.getElementById("waveinput") as HTMLInputElement; 841 | if (waveInput) { 842 | waveInput.value = String(this.wave); 843 | } 844 | } 845 | 846 | getSidebarContent() { 847 | return SidebarContent; 848 | } 849 | } 850 | --------------------------------------------------------------------------------