PageSourceSearch

https://the-torchbearer.netlify.app/src/systems/fury.js?v=27Jul-TB8

js the-torchbearer.netlify.app collected 2026-10-03 11:29:57 UTC 9,524 bytes, 263 lines download raw bytes

1/* The Torchbearer — Fury and the weapon Ultimates.
2 *
3 * The combat loop had no crescendo: every fight was the same three verbs at
4 * the same intensity. Fury fixes that. You build it by fighting well — hits
5 * feed it a little, finishers and parries feed it a lot, taking damage burns
6 * it — and when it fills you spend it on a weapon-specific Ultimate.
7 *
8 * The meter is the reward for aggression; the Ultimate is the release.
9 */
10window.SK = window.SK || {};
11
12SK.FURY_MAX = 100;
13
14SK.FURY_GAIN = {
15  hit: 4,
16  finisher: 10,
17  charged: 12,
18  kill: 7,
19  elite: 15,
20  parry: 25,        // parrying is the fastest route to an Ultimate
21  hurt: -20         // and getting hit is the fastest way to lose one
22};
23
24/* Each weapon spends Fury differently — same button, three fantasies. */
25SK.ULTIMATES = {
26  sword: {
27    name: 'BLADE STORM',
28    descr: 'Seven whirlwinds in four seconds.',
29    kind: 'storm',
30    waves: 7, interval: 190, radius: 150, damage: 34, knock: 260
31  },
32  spear: {
33    name: 'THOUSAND LANCES',
34    descr: 'A spear for every direction, three times over.',
35    kind: 'lances',
36    volleys: 3, interval: 260, spears: 10, range: 300, damage: 62, knock: 200
37  },
38  axe: {
39    name: 'CATACLYSM',
40    descr: 'Three quakes, each wider than the last.',
41    kind: 'cataclysm',
42    quakes: 3, interval: 420, radius: [180, 260, 340], damage: 70, knock: 520
43  },
44  bow: {
45    name: 'ARROW STORM',
46    descr: 'Five volleys of piercing shafts, sweeping the hall.',
47    kind: 'volley',
48    volleys: 5, interval: 220, shots: 9, spread: 0.5,
49    damage: 38, knock: 180, speed: 780
50  },
51  staff: {
52    name: 'STARFALL',
53    descr: 'Four expanding rings of bursting light.',
54    kind: 'rings',
55    rings: 4, interval: 300, bolts: 14, damage: 44, knock: 220,
56    speed: 430, burst: 60
57  },
58
59  /* These three shipped without an Ultimate at all, so pressing E while
60   * carrying one did nothing AND still emptied the meter. Every weapon in
61   * SK.WEAPON_IDS must have an entry here; there is a test for it now. */
62  dagger: {
63    name: 'THOUSAND CUTS',
64    descr: 'Twelve strikes in three seconds. Nothing survives the middle.',
65    kind: 'storm',
66    waves: 12, interval: 110, radius: 118, damage: 22, knock: 170
67  },
68  crossbow: {
69    name: 'SIEGE VOLLEY',
70    descr: 'Four salvos of bolts that punch through everything.',
71    kind: 'volley',
72    volleys: 4, interval: 300, shots: 7, spread: 0.42,
73    damage: 60, knock: 300, speed: 900
74  },
75  froststaff: {
76    name: 'WHITEOUT',
77    descr: 'Five rings of killing frost. The hall stops moving.',
78    kind: 'rings',
79    rings: 5, interval: 260, bolts: 12, damage: 36, knock: 180,
80    speed: 400, burst: 44
81  }
82};
83
84SK.Fury = class Fury {
85  constructor(scene) {
86    this.scene = scene;
87    this.value = 0;
88    this.active = false;      // an Ultimate is currently playing
89    this.readyPulsed = false;
90  }
91
92  add(kind) {
93    if (this.active) return;
94    let g = SK.FURY_GAIN[kind] || 0;
95    // The Magister burns hotter.
96    const fm = this.scene.run.mods.furyMult || 1;
97    if (g > 0) g *= fm;
98    const before = this.value;
99    this.value = Phaser.Math.Clamp(this.value + g, 0, SK.FURY_MAX);
100
101    // Announce the moment it fills — the player must know it is available.
102    if (before < SK.FURY_MAX && this.value >= SK.FURY_MAX) {
103      this.readyPulsed = true;
104      const p = this.scene.player;
105      SK.juice.popup(this.scene, p.x, p.y - 60, 'FURY READY  [E]', SK.CSS.gold, 20);
106      SK.juice.shockwave(this.scene, p.x, p.y, 0xffcc33, 140);
107      SK.synth.chargeReady();
108      if (this.scene.lighting) {
109        this.scene.lighting.flash(p.x, p.y, 260, 0xffcc33, 300);
110      }
111    }
112  }
113
114  get ready() { return this.value >= SK.FURY_MAX && !this.active; }
115
116  /* Spend the meter. Returns false if it was not full. */
117  unleash() {
118    if (!this.ready) return false;
119
120    const scene = this.scene;
121    const w = SK.currentWeapon(scene.run);
122    const ult = SK.ULTIMATES[w.id];
123
124    /* Check BEFORE spending. The meter used to be zeroed on the line above
125     * this guard, so carrying a weapon with no Ultimate meant filling Fury to
126     * 100, pressing E, and watching the whole bar vanish for nothing. */
127    if (!ult || typeof this['_' + ult.kind] !== 'function') {
128      SK.juice.popup(scene, scene.player.x, scene.player.y - 40,
129        'NO ULTIMATE FOR THIS ARM', SK.CSS.red, 15);
130      return false;
131    }
132
133    this.value = 0;
134    this.active = true;
135
136    scene.run.stats.ultimates = (scene.run.stats.ultimates || 0) + 1;
137
138    // The announcement beat.
139    scene.cameras.main.flash(260, 255, 220, 150);
140    SK.shake(scene, 14, 400);
141    SK.shock(scene, 1);
142    SK.juice.slowmo(scene, 0.4, 420);
143    SK.juice.popup(scene, scene.player.x, scene.player.y - 70, ult.name,
144      SK.CSS.gold, 28);
145    SK.synth.victory();
146    scene.player.iframesUntil = scene.time.now + 900;   // untouchable mid-ult
147
148    this['_' + ult.kind](scene, ult, w);
149    return true;
150  }
151
152  _storm(scene, ult, w) {
153    for (let i = 0; i < ult.waves; i++) {
154      scene.time.delayedCall(i * ult.interval, () => {
155        if (!scene.player || !scene.player.sprite) return;
156        scene.ultStrike({
157          x: scene.player.x, y: scene.player.y, angle: 0,
158          range: ult.radius, arc: Math.PI * 2,
159          dmg: ult.damage, knock: ult.knock, weapon: w,
160          stagger: true, charged: true, finisher: true
161        });
162        SK.juice.shockwave(scene, scene.player.x, scene.player.y, w.glow, ult.radius);
163        if (SK.synth.shoot) SK.synth.shoot(w.kind, w.pitch, true);
164        else SK.synth.swing(w.pitch, true);
165        if (i === ult.waves - 1) this.active = false;
166      });
167    }
168  }
169
170  _lances(scene, ult, w) {
171    for (let v = 0; v < ult.volleys; v++) {
172      scene.time.delayedCall(v * ult.interval, () => {
173        if (!scene.player || !scene.player.sprite) return;
174        const off = (v * Math.PI) / ult.spears;
175        // One damage instance per target for the whole volley: ten spears
176        // used to each hit the boss, which deleted it outright.
177        const volleyHits = new Set();
178        for (let i = 0; i < ult.spears; i++) {
179          const a = off + (i / ult.spears) * Math.PI * 2;
180          scene.ultStrike({
181            hitSet: volleyHits,
182            x: scene.player.x, y: scene.player.y, angle: a,
183            range: ult.range, arc: 0.42,
184            dmg: ult.damage, knock: ult.knock, weapon: w,
185            stagger: true, charged: true, finisher: true
186          });
187          scene.spawnLance(scene.player.x, scene.player.y, a, ult.range, w.glow);
188        }
189        SK.synth.swing(w.pitch, true);
190        if (v === ult.volleys - 1) this.active = false;
191      });
192    }
193  }
194
195  /* Ranged Ultimates fire real projectiles, so they inherit pierce and burst
196   * from the weapon rather than needing their own hit resolution. */
197  _volley(scene, ult, w) {
198    for (let v = 0; v < ult.volleys; v++) {
199      scene.time.delayedCall(v * ult.interval, () => {
200        if (!scene.player || !scene.player.sprite) return;
201        const base = scene.player.aimAngle;
202        const shared = new Set();          // the volley may hit a target once
203        for (let i = 0; i < ult.shots; i++) {
204          const a = base + (i - (ult.shots - 1) / 2) * (ult.spread / ult.shots) * 2;
205          scene.spawnPlayerBolt(scene.player.x, scene.player.y, a, {
206            speed: ult.speed, dmg: ult.damage, weapon: w, range: 700,
207            knock: ult.knock, pierce: true, finisher: true, charged: true,
208            // The volley has ONE report, played below. Without this a 5-shot
209            // lance volley fires five stacked samples and clips the master.
210            silent: true,
211            hitSet: shared
212          });
213        }
214        SK.synth.swing(w.pitch, true);
215        if (v === ult.volleys - 1) this.active = false;
216      });
217    }
218  }
219
220  _rings(scene, ult, w) {
221    for (let r = 0; r < ult.rings; r++) {
222      scene.time.delayedCall(r * ult.interval, () => {
223        if (!scene.player || !scene.player.sprite) return;
224        const off = r * 0.22;
225        const shared = new Set();
226        for (let i = 0; i < ult.bolts; i++) {
227          scene.spawnPlayerBolt(scene.player.x, scene.player.y,
228            off + (i / ult.bolts) * Math.PI * 2, {
229              speed: ult.speed, dmg: ult.damage, weapon: w, range: 520,
230              knock: ult.knock, burst: ult.burst, finisher: true, charged: true,
231              silent: true,          // a 12-bolt ring is one sound, not twelve
232              hitSet: shared
233            });
234        }
235        SK.shake(scene, 8, 200);
236        SK.synth.swing(w.pitch, true);
237        if (r === ult.rings - 1) this.active = false;
238      });
239    }
240  }
241
242  _cataclysm(scene, ult, w) {
243    for (let i = 0; i < ult.quakes; i++) {
244      scene.time.delayedCall(i * ult.interval, () => {
245        if (!scene.player || !scene.player.sprite) return;
246        const r = ult.radius[i] || ult.radius[ult.radius.length - 1];
247        scene.ultStrike({
248          x: scene.player.x, y: scene.player.y, angle: 0,
249          range: r, arc: Math.PI * 2,
250          dmg: ult.damage, knock: ult.knock, weapon: w,
251          stagger: true, charged: true, finisher: true, slam: true
252        });
253        SK.juice.shockwave(scene, scene.player.x, scene.player.y, w.glow, r);
254        SK.shake(scene, 10 + i * 4, 260);
255        if (scene.lighting) {
256          scene.lighting.flash(scene.player.x, scene.player.y, r * 1.5, w.glow, 260);
257        }
258        SK.synth.swing(w.pitch * 0.8, true);
259        if (i === ult.quakes - 1) this.active = false;
260      });
261    }
262  }
263};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.