Answer the playtest: a net for the lock, and a duel that pays
The hang: I could not reproduce it or pin it on one latch, so this is two things. A root-cause fix — iOS does not guarantee a touchend when you switch apps mid-drag, and the held G.aim it leaves behind blocks the uncaught-toss retoss forever — and a watchdog for whatever else is out there. The watchdog does not time how long you have been blocked, because a horde blocks you for fifteen honest seconds; it watches whether the clock that should release you is moving, and logs the frozen state before letting go. The duel: past about game 20 all four tins sit at wCap and a won duel handed over literally nothing. It falls through to the barrel weapons now. The prize also stops borrowing the praise channel — arriving in the same slot as PERFECT made it read as another compliment. It gets a card with its own icon. The turn into zombie mode was unwatchable because the zoom started ON it, and the curve render applies is zero at t=0, so the camera only arrived half a second after he had changed. It peaks on the turn now, and the swap lands on a hitstop, a flash and a burst so it reads as an event rather than a glitch. Laser down from 0.55 to 0.22 — zap() sums four voices, so its vol was never one voice's level. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
20a742fbf3
commit
a1a8a61422
7 files changed
+130
-17
No files matched your search
+27
@@ -1035,6 +1035,33 @@ period is a filter, not a migration. It replaced `splatterBest`, which it migrat
|
|||||||
- `T.stats.check()` is the runnable check: it asserts the period filter and the aggregation against
|
- `T.stats.check()` is the runnable check: it asserts the period filter and the aggregation against
|
||||||
a fixture, and `read()` takes an injected array precisely so it never touches a real save.
|
a fixture, and `read()` takes an injected array precisely so it never touches a real save.
|
||||||
|
|
||||||
|
**THE DEADLOCK WATCHDOG** (`watchdog()` in gameplay.js) exists because a run that hard-locks costs
|
||||||
|
the player everything, and this file has form: `mgT` was once left above zero by a beat that moved
|
||||||
|
the phase underneath it, and `mgT > 0` blocks serving. It does NOT time how long the player has been
|
||||||
|
blocked — a horde blocks you for fifteen honest seconds. It watches whether the CLOCK that should
|
||||||
|
release you is moving: every legitimate wait (horde, burst, changeover, duel intro, an 'over' beat)
|
||||||
|
counts down, so a frozen signature for `TUNE.stuckT` seconds means the state is dead. It logs the
|
||||||
|
frozen signature into the miss log (scoreboard tap) before letting go — **a lock that unsticks
|
||||||
|
itself and leaves no evidence is worse than one that does not.** Soaked five game-minutes to game 23
|
||||||
|
through a horde: zero firings.
|
||||||
|
|
||||||
|
- **Anything new that blocks serving must either tick a clock or be added to `watchdog`'s release.**
|
||||||
|
- The matching root-cause fix is in input.js: `visibilitychange`/`blur`/`pagehide` drop the in-flight
|
||||||
|
gesture. `stick` and `G.aim` are only cleared by the touchend for the matching identifier, and iOS
|
||||||
|
does not guarantee that event when you switch apps mid-drag — a held `G.aim` then blocks the
|
||||||
|
uncaught-toss retoss (`!G.aim`) forever.
|
||||||
|
|
||||||
|
**A won duel must always pay.** `deliverTin` walks the four-tin ladder, and when every rung is at
|
||||||
|
`wCap` — which is the state from about game 20 — it now falls through to the **barrel weapons**
|
||||||
|
(unlocked ones only, so no rocket in game three). Before that it printed RACK FULL and handed over
|
||||||
|
nothing, which is the one thing the duel exists for. `RACK FULL` now means literally everything is
|
||||||
|
capped.
|
||||||
|
|
||||||
|
**The tin gets a CARD, not a flash.** `G.flash` is the praise channel (PERFECT, DOUBLE KILL), so a
|
||||||
|
weapon arriving there read as another compliment. `G.card` takes an optional `icon` now and draws it
|
||||||
|
above the title; the card does not wait for a tap. `weaponModal` still fires first-sight-only,
|
||||||
|
because that one has rules to teach.
|
||||||
|
|
||||||
**The world board ranks KILLS IN A SINGLE RUN.** Games survived is the same information at a tenth
|
**The world board ranks KILLS IN A SINGLE RUN.** Games survived is the same information at a tenth
|
||||||
the resolution — most players land between 5 and 20, so a games board is a wall of ties broken by
|
the resolution — most players land between 5 and 20, so a games board is a wall of ties broken by
|
||||||
who got there first. Games ride along as the tiebreak and as a column. The prompt to sign it appears
|
who got there first. Games ride along as the tiebreak and as a column. The prompt to sign it appears
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ export const TUNE = {
|
|||||||
zSplatQ: 0.5, // min shot quality to splatter; below = stagger pushback
|
zSplatQ: 0.5, // min shot quality to splatter; below = stagger pushback
|
||||||
zPushback: 1.4, // m a staggered zombie is knocked back
|
zPushback: 1.4, // m a staggered zombie is knocked back
|
||||||
splatterOverT: 0.1, // s the spent-ball 'over' beat lasts before the next serve is armed
|
splatterOverT: 0.1, // s the spent-ball 'over' beat lasts before the next serve is armed
|
||||||
|
stuckT: 6, // s the deadlock watchdog waits before letting go of a frozen latch (see update)
|
||||||
|
tinCardT: 2.0, // s the won-duel weapon card holds (it does NOT wait for a tap)
|
||||||
// the slingshot: the pull's DIRECTION picks a lane, its LENGTH is power. Deliberately DECOUPLED —
|
// the slingshot: the pull's DIRECTION picks a lane, its LENGTH is power. Deliberately DECOUPLED —
|
||||||
// reaching a far shambler must not cost a longer drag than the screen has room for. The old
|
// reaching a far shambler must not cost a longer drag than the screen has room for. The old
|
||||||
// mapping (pull length = reach) is why you had to drag off the bottom of the phone.
|
// mapping (pull length = reach) is why you had to drag off the bottom of the phone.
|
||||||
|
|||||||
+73
-12
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { TUNE, UI, BOSS_ART, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS } from './config.js';
|
import { TUNE, UI, BOSS_ART, APPLAUSE_T, EATEN_BURST, EATEN_SUMMARY, COURT, CONTACT, VOLLEY, ZONE_DEPTH, HOME, P_BOUNDS, TOSS, SHOT_DEPTH, POSE_TIME, BOUNCE_IDLE, zombieH, Z_CHEST , WEAPONS, RACK_MAX , BARREL_WEAPONS } from './config.js';
|
||||||
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
import { P, O, ball, score, crowd, G, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck } from './audio.js';
|
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } from './audio.js';
|
||||||
import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
import { axisX, axisZ, takeFlickAim, touchPX, resetInput } from './input.js';
|
||||||
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
import { W } from './render.js'; // runtime-only cycle (render imports gameplay), same pattern as input/zombies
|
||||||
import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam } from './zombies.js';
|
import { updateZombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam } from './zombies.js';
|
||||||
@@ -574,17 +574,32 @@ export function deliverTin() {
|
|||||||
const cand = TINS[(G.deliveries + k) % TINS.length];
|
const cand = TINS[(G.deliveries + k) % TINS.length];
|
||||||
if (stock(cand) < TUNE.wCap) { id = cand; G.deliveries += k + 1; break; }
|
if (stock(cand) < TUNE.wCap) { id = cand; G.deliveries += k + 1; break; }
|
||||||
}
|
}
|
||||||
// Every tin full is a real state and it is not a bug, so say so plainly instead of miming a
|
// ...and when the whole four-rung ladder is capped, PAY OUT OF THE BARREL RACK instead. Past
|
||||||
// handover that cannot happen. No boy, no umpire line — nothing was delivered.
|
// about game 20 all four tins sit at wCap and the duel — the one thing in the run you play for —
|
||||||
|
// handed you nothing at all: "the duel does not always give you new weapons". The barrel weapons
|
||||||
|
// are the natural next rung, they already exist, and you cannot otherwise win one without
|
||||||
|
// shooting a barrel. Unlocked ones only, so this can never hand you a rocket in game three.
|
||||||
|
if (!id) {
|
||||||
|
const late = Object.keys(BARREL_WEAPONS)
|
||||||
|
.filter(w => G.runGames >= BARREL_WEAPONS[w].from && stock(w) < TUNE.wCap);
|
||||||
|
if (late.length) id = late[G.deliveries++ % late.length];
|
||||||
|
}
|
||||||
|
// Every tin AND every barrel weapon full is a real state and not a bug, so say so plainly instead
|
||||||
|
// of miming a handover that cannot happen. No boy, no umpire line — nothing was delivered.
|
||||||
if (!id) { G.flash = { text: 'RACK FULL', t: 1.1 }; return; }
|
if (!id) { G.flash = { text: 'RACK FULL', t: 1.1 }; return; }
|
||||||
const wasPlain = G.weapon === 0 && !G.temp; // never yank you off a deliberate PLAIN (banking)
|
const wasPlain = G.weapon === 0 && !G.temp; // never yank you off a deliberate PLAIN (banking)
|
||||||
grantWeapon(id);
|
grantWeapon(id);
|
||||||
if (wasPlain) G.weapon = 0;
|
if (wasPlain) G.weapon = 0;
|
||||||
sendBoy(); voice('new_balls', 0.6);
|
sendBoy(); voice('new_balls', 0.6);
|
||||||
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
||||||
// From the fifth delivery on, weaponModal has nothing new to explain and stays quiet — so the tin
|
// THE PRIZE GETS ITS OWN CARD, with the tin's own icon on it. It used to borrow G.flash — the
|
||||||
// needs the same free-floating label a barrel already gets, or a won duel pays out in silence.
|
// same channel as PERFECT and DOUBLE KILL — so the one thing you played the duel FOR arrived as a
|
||||||
G.flash = { text: WEAPONS[id].label, t: 1.1 };
|
// word in the praise slot and read as another compliment. weaponModal still fires the first time
|
||||||
|
// a tin is seen (it has rules to teach); every time after, this card is the announcement, and it
|
||||||
|
// does not want a tap: you have just won a point, not been asked a question.
|
||||||
|
G.card = { title: WEAPONS[id].label, sub1: 'DELIVERED TO THE RACK',
|
||||||
|
sub2: (WEAPONS[id].desc && WEAPONS[id].desc[0]) || '',
|
||||||
|
icon: WEAPONS[id].icon, color: UI.warm, t: TUNE.tinCardT };
|
||||||
weaponModal(id);
|
weaponModal(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,6 +711,38 @@ export function endBossFight() {
|
|||||||
if (G.afterDuel) { G.afterClear = G.afterDuel; G.afterDuel = null; proceedAfterClear(); }
|
if (G.afterDuel) { G.afterClear = G.afterDuel; G.afterDuel = null; proceedAfterClear(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- THE DEADLOCK WATCHDOG.
|
||||||
|
// A run that hard-locks costs the player everything, and this file has form: `mgT` was once left
|
||||||
|
// above zero by a beat that moved the phase underneath it, and `mgT > 0` blocks serving, so the
|
||||||
|
// game sat there with the gun racket on screen. This is the net.
|
||||||
|
//
|
||||||
|
// It cannot misfire on a legitimate wait, and that is the whole design: every legitimate reason you
|
||||||
|
// cannot serve — a horde, a burst, a changeover, the duel's announcement, an 'over' beat — has a
|
||||||
|
// CLOCK counting down toward letting you act. So the test is not "how long has the player been
|
||||||
|
// blocked" (a horde blocks you for fifteen honest seconds); it is "is the thing that should be
|
||||||
|
// releasing me actually moving". If none of those numbers changes for stuckT seconds while the
|
||||||
|
// player is blocked and no popup is waiting on them, the state is dead and nothing will revive it.
|
||||||
|
//
|
||||||
|
// It also LOGS what was frozen, into the same miss log the scoreboard tap already shows — a lock
|
||||||
|
// that unsticks itself and leaves no evidence is worse than one that does not.
|
||||||
|
let stuckT = 0, stuckSig = '';
|
||||||
|
function watchdog(dt) {
|
||||||
|
if (G.mode !== 'splatter' || G.modal) { stuckT = 0; return; } // a popup waits on the PLAYER: legitimate
|
||||||
|
const blocked = G.horde || G.mgT > 0 || G.changeover || G.phase === 'over'
|
||||||
|
|| (G.interlude && G.interlude.introT > 0)
|
||||||
|
|| (G.phase === 'toss' && ball.y < 2.0); // a toss on the floor: only a held aim keeps it there
|
||||||
|
if (!blocked) { stuckT = 0; return; }
|
||||||
|
const sig = [G.horde && G.horde.t, G.mgT, G.changeover, G.interlude && G.interlude.introT,
|
||||||
|
G.phase === 'over' && G.overT, ball.y].map(v => (+v || 0).toFixed(2)).join('|');
|
||||||
|
if (sig !== stuckSig) { stuckSig = sig; stuckT = 0; return; } // something is still moving — fine
|
||||||
|
if ((stuckT += dt) < TUNE.stuckT) return;
|
||||||
|
logSwing({ result: 'UNSTUCK', reason: `nothing moved for ${TUNE.stuckT}s — released a dead latch`,
|
||||||
|
phase: G.phase, sig, aim: !!G.aim });
|
||||||
|
G.horde = null; G.mgT = 0; G.changeover = false; G.interlude = false;
|
||||||
|
G.aim = null; endBurst(); setupPoint();
|
||||||
|
stuckT = 0; stuckSig = '';
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveInterlude(side) {
|
export function resolveInterlude(side) {
|
||||||
if (!G.interlude) return;
|
if (!G.interlude) return;
|
||||||
G.interlude = false;
|
G.interlude = false;
|
||||||
@@ -806,13 +853,26 @@ function transition(dt) {
|
|||||||
const t = (G.transT += dt);
|
const t = (G.transT += dt);
|
||||||
const once = (key, at, fn) => { if (t >= at && !G.tFlags[key]) { G.tFlags[key] = true; fn(); } };
|
const once = (key, at, fn) => { if (t >= at && !G.tFlags[key]) { G.tFlags[key] = true; fn(); } };
|
||||||
once('quiet', 1.8, () => voice('quiet_dark', 0, 0.82));
|
once('quiet', 1.8, () => voice('quiet_dark', 0, 0.82));
|
||||||
// The turn is the moment the game tells you what it now is, and it used to happen in a wide
|
// THE CAMERA HAS TO BE THERE BEFORE IT HAPPENS. The zoom used to start ON the turn — and the
|
||||||
// static shot at the far baseline where it was three pixels tall. The camera PUNCHES IN on him
|
// curve render applies is `sin(p*PI)`, which is ZERO at p=0, so the punch-in only reached full
|
||||||
// for it (a flat 2D zoom, see render), and the ground goes with it. Each later beat shakes harder.
|
// magnification half a second AFTER he had already changed. You watched the aftermath every time.
|
||||||
|
// It starts at 1.8 with T 3.2 so the peak (p=0.5) lands exactly on the turn at 3.4, holds through
|
||||||
|
// it, and pulls back out as the mob walks on — which becomes the reveal of what else is coming.
|
||||||
|
once('closein', 1.8, () => { G.zoom = { x: O.x, z: O.z, k: 2.1, t: 0, T: 3.2 }; });
|
||||||
|
// ...and the turn itself is a sprite SWAP, one frame to the next, with nothing to sell it. So it
|
||||||
|
// lands on a beat: the world stops dead for a moment (hitstop), the screen flashes, he comes
|
||||||
|
// apart and puts himself back together wrong. The freeze is what makes a swap read as an EVENT
|
||||||
|
// rather than as a glitch — the eye is given something to blame the change on.
|
||||||
once('turn', 3.4, () => {
|
once('turn', 3.4, () => {
|
||||||
spawnZombie(O.x, O.z, 'c'); G.hideO = true; // the opponent... turns
|
G.hitstop = 0.16; // the world holds its breath
|
||||||
G.zoom = { x: O.x, z: O.z, k: 1.9, t: 0, T: 1.15 };
|
G.boomFlash = 0.16; G.shake = { t: 0.7, mag: 7 };
|
||||||
G.shake = { t: 0.45, mag: 4 };
|
shriek(); splat(0.9); boom();
|
||||||
|
for (let k = 0; k < 40; k++) parts.push({ // he bursts, and what stands up is not him
|
||||||
|
x: O.x, y: 0.5 + Math.random() * 1.5, z: O.z,
|
||||||
|
vx: (Math.random() * 2 - 1) * 3.5, vy: 1 + Math.random() * 4, vz: (Math.random() * 2 - 1) * 3.5,
|
||||||
|
t: 0.5 + Math.random() * 0.7, dark: Math.random() < 0.45 });
|
||||||
|
gore.push({ x: O.x, z: O.z, r: 1.1, a: 0.9 });
|
||||||
|
spawnZombie(O.x, O.z, 'c'); G.hideO = true; // the opponent... turns
|
||||||
});
|
});
|
||||||
once('mob', 4.8, () => { spawnZombie(-3.5, -1.6, 'a'); spawnZombie(2.5, -2.1, 'b'); G.shake = { t: 0.5, mag: 6 }; });
|
once('mob', 4.8, () => { spawnZombie(-3.5, -1.6, 'a'); spawnZombie(2.5, -2.1, 'b'); G.shake = { t: 0.5, mag: 6 }; });
|
||||||
once('seated', 5.4, () => voice('remain_seated', 0, 0.95));
|
once('seated', 5.4, () => voice('remain_seated', 0, 0.95));
|
||||||
@@ -1359,6 +1419,7 @@ export function update(dt) {
|
|||||||
if (G.mode !== 'tennis') updateZombies(dt);
|
if (G.mode !== 'tennis') updateZombies(dt);
|
||||||
tickTestMode(dt); // keeps the sandbox stocked and rolls it a barrel now and then
|
tickTestMode(dt); // keeps the sandbox stocked and rolls it a barrel now and then
|
||||||
if (G.meleeT > 0) G.meleeT -= dt;
|
if (G.meleeT > 0) G.meleeT -= dt;
|
||||||
|
watchdog(dt);
|
||||||
if (G.clearing && !zombies.some(blocksClear)) proceedAfterClear(); // the won-game wave is mopped up — advance
|
if (G.clearing && !zombies.some(blocksClear)) proceedAfterClear(); // the won-game wave is mopped up — advance
|
||||||
if (G.mode === 'transition') transition(dt);
|
if (G.mode === 'transition') transition(dt);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -341,6 +341,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=124"></script>
|
<script type="module" src="main.js?v=125"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -151,6 +151,18 @@ const endTouch = e => {
|
|||||||
cv.addEventListener('touchend', endTouch, { passive: false });
|
cv.addEventListener('touchend', endTouch, { passive: false });
|
||||||
cv.addEventListener('touchcancel', endTouch, { passive: false });
|
cv.addEventListener('touchcancel', endTouch, { passive: false });
|
||||||
|
|
||||||
|
// A DRAG THAT NEVER ENDS LOCKS THE GAME. `stick` and `G.aim` are only cleared by the touchend for
|
||||||
|
// the matching identifier — and that event is not guaranteed: switch apps, pull the notification
|
||||||
|
// shade, take a call, and iOS tears the touch down without always delivering touchend OR
|
||||||
|
// touchcancel. What is left behind is a held slingshot: `G.aim` set forever, which keeps the
|
||||||
|
// uncaught-toss retoss from firing (see the `!G.aim` guard in update) and leaves the player in the
|
||||||
|
// toss phase with the ball on the floor, unable to serve or step. Dropping the gesture whenever the
|
||||||
|
// page stops being visible costs nothing — you cannot be mid-pull while looking at another app.
|
||||||
|
const dropGesture = () => { resetInput(); G.aim = null; };
|
||||||
|
addEventListener('visibilitychange', () => { if (document.hidden) dropGesture(); });
|
||||||
|
addEventListener('blur', dropGesture);
|
||||||
|
addEventListener('pagehide', dropGesture);
|
||||||
|
|
||||||
// Mouse/desktop dismissal. Registered ONLY on fine pointers: on a touch screen pointerdown
|
// Mouse/desktop dismissal. Registered ONLY on fine pointers: on a touch screen pointerdown
|
||||||
// fires before touchstart, so a shared handler would clear the modal and let the touchstart
|
// fires before touchstart, so a shared handler would clear the modal and let the touchstart
|
||||||
// below it fall through into a serve — exactly the swallowed-gesture bug.
|
// below it fall through into a serve — exactly the swallowed-gesture bug.
|
||||||
|
|||||||
@@ -1050,11 +1050,19 @@ function drawRack() {
|
|||||||
|
|
||||||
// the centered milestone 'block': a real pause, front and center. Shared by the sudden-death duel
|
// the centered milestone 'block': a real pause, front and center. Shared by the sudden-death duel
|
||||||
// and the SET announcement so both read as the same interruption. Assumes textAlign is already set.
|
// and the SET announcement so both read as the same interruption. Assumes textAlign is already set.
|
||||||
function drawCard(title, sub1, sub2, color) {
|
function drawCard(title, sub1, sub2, color, icon) {
|
||||||
// 80 tall, not 72: the second sub line ends at H/2+26 and the band used to stop at +30, so it sat
|
// 80 tall, not 72: the second sub line ends at H/2+26 and the band used to stop at +30, so it sat
|
||||||
// all but touching the edge. The band now clears it by 10 px, the same air it has at the top.
|
// all but touching the edge. The band now clears it by 10 px, the same air it has at the top.
|
||||||
ctx.fillStyle = 'rgba(10,6,8,0.66)'; ctx.fillRect(0, H / 2 - 44, W, 80);
|
// An ICON (the won-duel tin) needs its own room above the title, so the band grows for it rather
|
||||||
|
// than the art crowding the type.
|
||||||
|
const img = icon && SPR[icon], ih = img ? 34 : 0;
|
||||||
|
ctx.fillStyle = 'rgba(10,6,8,0.66)'; ctx.fillRect(0, H / 2 - 44 - ih, W, 80 + ih);
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
if (img) { // the tin itself, at its own aspect, sat on the title
|
||||||
|
const w = Math.round(img.width * (ih - 6) / img.height);
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(img, Math.round(W / 2 - w / 2), Math.round(H / 2 - 44 - ih + 3), w, ih - 6);
|
||||||
|
}
|
||||||
// the title SHOUTS (Bangers, twice the old size); the subs stay Silkscreen, because they are the
|
// the title SHOUTS (Bangers, twice the old size); the subs stay Silkscreen, because they are the
|
||||||
// detail you read second and a whole card in the loud face is just noise
|
// detail you read second and a whole card in the loud face is just noise
|
||||||
ctx.font = B(30); shadowText(title, W / 2, H / 2 - 34, color);
|
ctx.font = B(30); shadowText(title, W / 2, H / 2 - 34, color);
|
||||||
@@ -1623,7 +1631,7 @@ export function render() {
|
|||||||
}
|
}
|
||||||
// the SET milestone card (the duel's own announcement is a real modal now, not a timed card)
|
// the SET milestone card (the duel's own announcement is a real modal now, not a timed card)
|
||||||
if (G.card && G.card.t > 0 && !(G.card.wait > 0)) // `wait` holds it OFF the screen (a boss dying under it)
|
if (G.card && G.card.t > 0 && !(G.card.wait > 0)) // `wait` holds it OFF the screen (a boss dying under it)
|
||||||
drawCard(G.card.title, G.card.sub1, G.card.sub2, G.card.color);
|
drawCard(G.card.title, G.card.sub1, G.card.sub2, G.card.color, G.card.icon);
|
||||||
if (TUNE.missHud && G.lastMiss && G.idleT - G.lastMiss.at < 3.5) { // last miss reason, live
|
if (TUNE.missHud && G.lastMiss && G.idleT - G.lastMiss.at < 3.5) { // last miss reason, live
|
||||||
ctx.font = F(8); ctx.fillStyle = '#ff9060';
|
ctx.font = F(8); ctx.fillStyle = '#ff9060';
|
||||||
ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28);
|
ctx.fillText(G.lastMiss.text.toUpperCase(), W / 2, H - 28);
|
||||||
|
|||||||
+4
-1
@@ -313,7 +313,10 @@ export function fireBeam(dirX, dirZ) {
|
|||||||
// what it kills are one geometry
|
// what it kills are one geometry
|
||||||
CAUSE = 'laser';
|
CAUSE = 'laser';
|
||||||
G.beam = { x: P.x, y: TUNE.beamY + TUNE.beamOriginLift, z: P.z, ux, uz, t: TUNE.beamT };
|
G.beam = { x: P.x, y: TUNE.beamY + TUNE.beamOriginLift, z: P.z, ux, uz, t: TUNE.beamT };
|
||||||
zap(0.55); G.shake = { t: 0.18, mag: 3 }; G.boomFlash = 0.08;
|
// 0.22, was 0.55: zap() sums FOUR voices (crack, two detuned saws, a sub) straight into the
|
||||||
|
// destination, so its `vol` is not one voice's level — it was coming out several times louder
|
||||||
|
// than anything else in the game. Oliver: "laser sound is way too loud."
|
||||||
|
zap(0.22); G.shake = { t: 0.18, mag: 3 }; G.boomFlash = 0.08;
|
||||||
for (let i = zombies.length - 1; i >= 0; i--) {
|
for (let i = zombies.length - 1; i >= 0; i--) {
|
||||||
const zb = zombies[i];
|
const zb = zombies[i];
|
||||||
if (!zb) continue;
|
if (!zb) continue;
|
||||||
|
|||||||
Reference in new issue
Block a user