1/* The Torchbearer â the juice toolkit. 2 * 3 * Everything that makes a hit FEEL like a hit: hitstop freezes, floating 4 * score popups, shockwave rings, burst particles. All procedural, all cheap. 5 * 6 * Hitstop note: gameplay runs on delta-accumulated physics, so a true freeze 7 * is done by pausing physics + tweens for a few tens of ms of real time. 8 * Scenes must check SK.juice.isFrozen(scene) nowhere â the freeze is fully 9 * self-contained. 10 */ 11window.SK = window.SK || {}; 12 13SK.juice = { 14 /* A coaching line: a framed strip low on the screen that slides in, holds, 15 * and leaves. Deliberately not a modal â the game keeps running while you 16 * read it, because stopping play to teach play is how tutorials get skipped. */ 17 coach: function (scene, text) { 18 var w = scene.scale.width, h = scene.scale.height; 19 var y = h - 132; 20 var c = scene.add.container(w / 2, y + 16).setDepth(1200).setScrollFactor(0); 21 var t = scene.add.text(0, 0, text, { 22 fontFamily: SK.FONT, fontSize: '15px', color: SK.CSS.bone 23 }).setOrigin(0.5); 24 var pw = t.width + 44, ph = 40; 25 var g = scene.add.graphics(); 26 g.fillStyle(0x12100c, 0.9); 27 g.fillRoundedRect(-pw / 2, -ph / 2, pw, ph, 6); 28 g.lineStyle(1, SK.COLORS.gold, 0.85); 29 g.strokeRoundedRect(-pw / 2, -ph / 2, pw, ph, 6); 30 g.fillStyle(SK.COLORS.gold, 1); 31 g.fillRect(-pw / 2 + 1, -ph / 2 + 1, 3, ph - 2); 32 c.add([g, t]); 33 c.setAlpha(0); 34 scene.tweens.add({ targets: c, alpha: 1, y: y, duration: 260, ease: 'Quad.easeOut' }); 35 scene.tweens.add({ 36 targets: c, alpha: 0, y: y - 14, delay: 3600, duration: 340, 37 onComplete: function () { c.destroy(true); } 38 }); 39 return c; 40 }, 41 42 43 /* ---- time control ----------------------------------------------------- 44 * Hard rules, learned the hard way: 45 * 1. A freeze is ALWAYS bounded (MAX_FREEZE). Extensions can never push 46 * it past that, so the world can never stay stopped. 47 * 2. thaw() and unslow() each restore EVERY field either one can touch â 48 * pause state, clock scale, tween scale and physics timeScale â so no 49 * interleaving of the two can leave a residue behind. 50 * 3. Release timers re-arm rather than bail out when a deadline moves. 51 */ 52 53 MAX_FREEZE: 140, // ms; nothing may stop the world longer than this 54 MAX_SLOWMO: 1200, 55 56 hitstop: function (scene, ms) { 57 var now = performance.now(); 58 59 if (scene._hitstopUntil && now < scene._hitstopUntil) { 60 // Extend, but never beyond the absolute cap measured from the START of 61 // the freeze. Continuous hits used to push the deadline forever. 62 var cap = (scene._hitstopStart || now) + SK.juice.MAX_FREEZE; 63 scene._hitstopUntil = Math.min(Math.max(scene._hitstopUntil, now + ms), cap); 64 return; 65 } 66 67 scene._hitstopStart = now; 68 scene._hitstopUntil = now + Math.min(ms, SK.juice.MAX_FREEZE); 69 70 if (scene.physics && scene.physics.world) scene.physics.world.pause(); 71 scene.tweens.timeScale = 0.05; 72 scene.time.timeScale = 0.05; 73 74 var release = function () { 75 if (!scene.sys || !scene.sys.isActive()) { SK.juice.thaw(scene); return; } 76 var left = scene._hitstopUntil - performance.now(); 77 if (left > 1) { setTimeout(release, left); return; } 78 SK.juice.thaw(scene); 79 }; 80 setTimeout(release, Math.min(ms, SK.juice.MAX_FREEZE)); 81 }, 82 83 /* Brief slow-motion â parries, boss deaths, final blows. */ 84 slowmo: function (scene, scale, ms) { 85 if (!scene.physics || !scene.physics.world) return; 86 var now = performance.now(); 87 ms = Math.min(ms, SK.juice.MAX_SLOWMO); 88 89 if (scene._slowmoUntil && now < scene._slowmoUntil) { 90 var cap = (scene._slowmoStart || now) + SK.juice.MAX_SLOWMO; 91 scene._slowmoUntil = Math.min(Math.max(scene._slowmoUntil, now + ms), cap); 92 return; 93 } 94 95 scene._slowmoStart = now; 96 scene._slowmoUntil = now + ms; 97 98 scene.physics.world.timeScale = 1 / scale; // arcade: >1 slows the sim 99 scene.tweens.timeScale = scale; 100 scene.time.timeScale = scale; 101 102 var release = function () { 103 if (!scene.sys || !scene.sys.isActive()) { SK.juice.unslow(scene); return; } 104 var left = scene._slowmoUntil - performance.now(); 105 if (left > 1) { setTimeout(release, left); return; } 106 SK.juice.unslow(scene); 107 }; 108 setTimeout(release, ms); 109 }, 110 111 /* Restore everything. Both of these reset ALL four fields on purpose. */ 112 thaw: function (scene) { 113 scene._hitstopUntil = 0; 114 scene._hitstopStart = 0; 115 if (scene.physics && scene.physics.world) { 116 if (scene.physics.world.isPaused) scene.physics.world.resume();
117 if (!scene._slowmoUntil) scene.physics.world.timeScale = 1; 118 } 119 if (scene.tweens) scene.tweens.timeScale = scene._slowmoUntil ? scene.tweens.timeScale : 1; 120 if (scene.time) scene.time.timeScale = scene._slowmoUntil ? scene.time.timeScale : 1; 121 }, 122 123 unslow: function (scene) { 124 scene._slowmoUntil = 0; 125 scene._slowmoStart = 0; 126 if (scene.physics && scene.physics.world) { 127 scene.physics.world.timeScale = 1; 128 if (scene.physics.world.isPaused && !scene._hitstopUntil) { 129 scene.physics.world.resume(); 130 } 131 } 132 if (scene.tweens) scene.tweens.timeScale = 1; 133 if (scene.time) scene.time.timeScale = 1; 134 }, 135 136 /* Belt-and-braces: called every frame. A stopped or slowed world that has 137 * outlived its deadline is unrecoverable for the player, so force it back. */ 138 policeTime: function (scene) { 139 var now = performance.now(); 140 if (scene._hitstopUntil && now > scene._hitstopUntil + 80) SK.juice.thaw(scene); 141 if (scene._slowmoUntil && now > scene._slowmoUntil + 150) SK.juice.unslow(scene); 142 143 var w = scene.physics && scene.physics.world; 144 if (!w) return; 145 if (w.isPaused && !scene._hitstopUntil) w.resume(); 146 if (w.timeScale !== 1 && !scene._slowmoUntil) w.timeScale = 1; 147 if (scene.time.timeScale !== 1 && !scene._hitstopUntil && !scene._slowmoUntil) { 148 scene.time.timeScale = 1; 149 scene.tweens.timeScale = 1; 150 } 151 }, 152 153 /* Floating text: "+30", "PERFECT", "FEVER". */ 154 popup: function (scene, x, y, text, color, size) { 155 // Numeric feedback is a preference; labels like PERFECT always show. 156 if (SK.settings && SK.settings.damageNumbers === false && 157 /^[-+]?\d/.test(String(text))) { 158 return null; 159 } 160 var t = scene.add.text(x, y, text, { 161 fontFamily: SK.FONT, 162 fontSize: (size || 16) + 'px', 163 color: color || '#ffffff', 164 stroke: '#120e0a', 165 strokeThickness: 3 166 }).setOrigin(0.5).setDepth(850); 167 168 scene.tweens.add({ 169 targets: t, 170 y: y - 44, 171 alpha: { from: 1, to: 0 }, 172 scale: { from: 1, to: 1.15 }, 173 duration: 700, 174 ease: 'Quad.easeOut', 175 onComplete: function () { t.destroy(); } 176 }); 177 return t; 178 }, 179 180 /* Expanding ring â kills, explosions, door opens. */ 181 shockwave: function (scene, x, y, color, maxR) { 182 var g = scene.add.graphics().setDepth(450); 183 var state = { r: 10, a: 0.9 }; 184 scene.tweens.add({ 185 targets: state, 186 r: maxR || 120, 187 a: 0, 188 duration: 380, 189 ease: 'Quad.easeOut', 190 onUpdate: function () { 191 g.clear(); 192 g.lineStyle(4, color, state.a); 193 g.strokeCircle(x, y, state.r); 194 g.lineStyle(10, color, state.a * 0.25); 195 g.strokeCircle(x, y, state.r); 196 }, 197 onComplete: function () { g.destroy(); } 198 }); 199 }, 200 201 /* Directional burst of squares â enemy deaths, slash impacts. */ 202 burst: function (scene, x, y, colors, count, force) { 203 var n = count || 14; 204 var f = force || 180; 205 for (var i = 0; i < n; i++) { 206 var size = Phaser.Math.Between(3, 8); 207 var rect = scene.add.rectangle( 208 x, y, size, size, colors[i % colors.length] 209 ).setDepth(440); 210 rect.setAngle(Phaser.Math.Between(0, 360)); 211 var ang = Math.random() * Math.PI * 2; 212 var dist = f * (0.4 + Math.random() * 0.8); 213 scene.tweens.add({ 214 targets: rect, 215 x: x + Math.cos(ang) * dist, 216 y: y + Math.sin(ang) * dist, 217 angle: rect.angle + Phaser.Math.Between(-260, 260), 218 alpha: 0, 219 scale: 0.3, 220 duration: Phaser.Math.Between(300, 520), 221 ease: 'Quad.easeOut', 222 onComplete: function (tw, targets) { targets[0].destroy(); } 223 }); 224 } 225 }, 226 227 /* Ground indicator that fills over an attack's wind-up. The contract for 228 * this game: nothing may damage the player without one of these first. 229 * shape 'arc' - a wedge (melee swings) 230 * shape 'line' - a lane (charges, beams) 231 * shape 'circle' - a radius (slams, rings) 232 * Returns a handle with .cancel() so a stagger can abort the tell. */ 233 telegraph: function (scene, opts) { 234 /* Two layers on purpose. 235 * 236 * `g` is the floor stain at depth 6: under the actors, and therefore under 237 * the lightmap at depth 800, so torchlight dims it along with the stone. 238 * That looks right but it is why the wind-up used to be almost invisible in 239 * an unlit corner â a warning you cannot see is not a warning. 240 * 241 * `edge` is the same shape drawn as a bright outline ABOVE the lightmap, so 242 * the tell survives any lighting. It is the only part of the world that is 243 * deliberately exempt from the dungeon's own darkness. */ 244 var g = scene.add.graphics().setDepth(6); 245 var edge = scene.add.graphics().setDepth(810); 246 var state = { p: 0 };
247 var color = opts.color || 0xff4a3d; 248 var alive = true; 249 250 var draw = function () { 251 if (!alive) return; 252 g.clear(); 253 edge.clear(); 254 var t = state.p; 255 // Outline appears immediately; the fill sweeps in as time runs out. 256 g.lineStyle(2, color, 0.35 + t * 0.4); 257 g.fillStyle(color, 0.22 + t * 0.38); 258 // The readable layer: thickens and brightens as the blow approaches, and 259 // pulses once it is nearly out so the release is impossible to miss. 260 var urgent = t > 0.72 ? (Math.sin(t * 60) * 0.25 + 0.75) : 1; 261 edge.lineStyle(t > 0.72 ? 4 : 3, color, (0.55 + t * 0.45) * urgent); 262 263 var x = opts.follow ? opts.follow.x : opts.x; 264 var y = opts.follow ? opts.follow.y : opts.y; 265 266 if (opts.shape === 'line') { 267 var a = opts.angle, half = (opts.width || 40) / 2; 268 var px = Math.cos(a + Math.PI / 2) * half; 269 var py = Math.sin(a + Math.PI / 2) * half; 270 var ex = x + Math.cos(a) * opts.length; 271 var ey = y + Math.sin(a) * opts.length; 272 g.beginPath(); 273 g.moveTo(x + px, y + py); 274 g.lineTo(ex + px, ey + py); 275 g.lineTo(ex - px, ey - py); 276 g.lineTo(x - px, y - py); 277 g.closePath(); 278 g.fillPath(); 279 g.strokePath(); 280 edge.beginPath(); 281 edge.moveTo(x + px, y + py); 282 edge.lineTo(ex + px, ey + py); 283 edge.lineTo(ex - px, ey - py); 284 edge.lineTo(x - px, y - py); 285 edge.closePath(); 286 edge.strokePath(); 287 } else if (opts.shape === 'circle') { 288 g.fillCircle(x, y, opts.radius * (0.35 + t * 0.65)); 289 g.strokeCircle(x, y, opts.radius); 290 edge.strokeCircle(x, y, opts.radius); 291 } else { 292 // The arc was the ONLY shape that never stroked its outline, which is 293 // why the commonest tell in the game â the overhead slash â was the 294 // hardest one to read. 295 var arc = opts.arc || Math.PI / 2; 296 g.slice(x, y, opts.radius, opts.angle - arc / 2, opts.angle + arc / 2, false); 297 g.fillPath(); 298 edge.slice(x, y, opts.radius, 299 opts.angle - arc / 2, opts.angle + arc / 2, false); 300 edge.strokePath(); 301 } 302 }; 303 304 var tw = scene.tweens.add({ 305 targets: state, p: 1, duration: opts.ms || 500, 306 onUpdate: draw, 307 onComplete: function () { alive = false; g.destroy(); edge.destroy(); } 308 }); 309 draw(); 310 311 return { 312 cancel: function () { 313 if (!alive) return; 314 alive = false; 315 tw.stop(); 316 g.destroy(); 317 edge.destroy(); 318 } 319 }; 320 }, 321 322 /* One camera kick in a direction â subtler than a shake for slashes. 323 * Works on a static (room-locked) camera by nudging scroll and restoring. */ 324 kick: function (scene, angle, px) { 325 var cam = scene.cameras.main; 326 var bx = cam.scrollX; 327 var by = cam.scrollY; 328 cam.setScroll(bx + Math.cos(angle) * px, by + Math.sin(angle) * px); 329 var state = { t: 0 }; 330 scene.tweens.add({ 331 targets: state, 332 t: 1, 333 duration: 110, 334 ease: 'Quad.easeOut', 335 onUpdate: function () { 336 cam.setScroll( 337 bx + Math.cos(angle) * px * (1 - state.t), 338 by + Math.sin(angle) * px * (1 - state.t) 339 ); 340 }, 341 onComplete: function () { cam.setScroll(bx, by); } 342 }); 343 } 344};
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.