(
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 |
--------------------------------------------------------------------------------