Serve reads, the mop beat, the head-icon card, and Daniel benched
Oliver's batch, the code half: - The tennis serve lost its ghost fingers (idle tap demo + apex rider deleted with their counters; the rally-return hint survives). - The toss rises over the tossing HAND, measured off the toss frames' raised fingertips (TOSS.dx/oDx, women's tour override) — not over the head. - An uncaught toss returns to the bounce stance instead of flashing the smash frame the player never earned. - Between match games the court belongs to the mop boy: 5 s (TUNE.matchMopT), full job allowance, a hurry speed between stains, and the far-court reach limit lifted for the match (his stains are exactly where opponents die). The next opponent walks on only after — d.ending guards the empty court. - The walk-on card wears the opponent's own head: config.HEAD_CROP, straight crops measured off the family sheets, no new art. - The match intro is DEATH MATCH, counting the real blood pool. - Daniel is benched: every ump_* personality line routes through aside()'s early return; officiating calls keep speaking. Personality pass later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e01894f4d
commit
c084d53497
8 files changed
+141
-70
No files matched your search
+1
-1
@@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=211"></script>
|
<script type="module" src="src/main.js?v=212"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+36
-9
@@ -93,7 +93,7 @@ export function startBossFight(chapter) {
|
|||||||
ball.active = false; setupPoint();
|
ball.active = false; setupPoint();
|
||||||
spawnChapterBoss(chapter);
|
spawnChapterBoss(chapter);
|
||||||
zombieDuck(true); // the music drops out for it
|
zombieDuck(true); // the music drops out for it
|
||||||
voice('ump_boss_arrives', 0, 0.9); voice('sudden_death', 2.6, 0.85);
|
aside('ump_boss_arrives'); voice('sudden_death', 2.6, 0.85); // the stake is an event; the quip is benched (see aside())
|
||||||
const art = bossArt(chapter);
|
const art = bossArt(chapter);
|
||||||
// THE WALK-ON IS AN OVERLAY, not a band card. A card announced the biggest fight in the game in
|
// THE WALK-ON IS AN OVERLAY, not a band card. A card announced the biggest fight in the game in
|
||||||
// the same slot and size as a routine set, and it read as the boss simply being there. This is on
|
// the same slot and size as a routine set, and it read as the boss simply being there. This is on
|
||||||
@@ -337,8 +337,11 @@ export function startMatch() {
|
|||||||
voice('play_resume', 0.2);
|
voice('play_resume', 0.2);
|
||||||
if (!G.seen.match) {
|
if (!G.seen.match) {
|
||||||
G.seen.match = true;
|
G.seen.match = true;
|
||||||
showModal({ title: 'EXHIBITION MATCH', color: UI.hot,
|
// Oliver's copy direction: name the stake, count the blood. TUNE.matchPlayerHp is the real
|
||||||
body: ['WIN THE GAME — EVERY POINT WOUNDS IT', 'ITS POINTS DRAW YOUR BLOOD'] });
|
// number, so the modal can never drift from the pool it describes.
|
||||||
|
showModal({ title: 'DEATH MATCH', color: UI.hot,
|
||||||
|
body: ['WIN THE POINT — WOUND THE ZOMBIE',
|
||||||
|
'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'] });
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -409,6 +412,17 @@ function startFall(from) {
|
|||||||
// OPPONENT'S SERVE POSES, because both are the same job — naming the frame while the body is doing
|
// OPPONENT'S SERVE POSES, because both are the same job — naming the frame while the body is doing
|
||||||
// something the walk cycle has no picture for.
|
// something the walk cycle has no picture for.
|
||||||
export function tickMatchFall(dt) {
|
export function tickMatchFall(dt) {
|
||||||
|
// THE MOP BEAT runs here, on the same clock discipline as the fall: its own timer, never the
|
||||||
|
// 'over' beat (see the section note above). When the boy's time is up the next opponent walks
|
||||||
|
// on with its own intro card, exactly as it used to on the kill frame.
|
||||||
|
const d = G.match;
|
||||||
|
if (d && d.mopT > 0 && (d.mopT -= dt) <= 0) {
|
||||||
|
d.mopT = 0;
|
||||||
|
matchOpponent();
|
||||||
|
d.ending = false; // play re-opens only now that a body is on court
|
||||||
|
matchReady();
|
||||||
|
G.phase = 'over'; G.overT = 2.2; // matchReady's setupPoint re-arms pretoss; keep the old beat
|
||||||
|
}
|
||||||
if (!G.matchFall) return tickMatchServe(dt);
|
if (!G.matchFall) return tickMatchServe(dt);
|
||||||
const f = G.matchFall;
|
const f = G.matchFall;
|
||||||
const zb = zombies.find(z => z.variant === 'r');
|
const zb = zombies.find(z => z.variant === 'r');
|
||||||
@@ -492,7 +506,8 @@ function matchOpponent() {
|
|||||||
// so killZombie can file the kill under it without asking G which opponent we were on.
|
// so killZombie can file the kill under it without asking G which opponent we were on.
|
||||||
zb.foe = matchFoe(G.match.opp);
|
zb.foe = matchFoe(G.match.opp);
|
||||||
zb.fam = zb.foe.fam; // ...and the body it wears (config.ZFAMS)
|
zb.fam = zb.foe.fam; // ...and the body it wears (config.ZFAMS)
|
||||||
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 2.4 };
|
// the walk-on card wears the opponent's own head (config.HEAD_CROP — a crop, not new art)
|
||||||
|
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 2.4, head: zb.fam };
|
||||||
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
|
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
|
||||||
// scoreboard (a quarter per point), so a deeper pool would
|
// scoreboard (a quarter per point), so a deeper pool would
|
||||||
// change nothing — the ladder escalates in racketAI instead
|
// change nothing — the ladder escalates in racketAI instead
|
||||||
@@ -569,15 +584,27 @@ function matchGameWon(zb, banked = false) {
|
|||||||
// the FAR side: that is where the body has been standing and bleeding, and it keeps him out of the
|
// the FAR side: that is where the body has been standing and bleeding, and it keeps him out of the
|
||||||
// near court where the player is about to serve. Between games only — see sendBoy's match note.
|
// near court where the player is about to serve. Between games only — see sendBoy's match note.
|
||||||
sendBoy(0, 4.5, true);
|
sendBoy(0, 4.5, true);
|
||||||
|
// THE MOP BEAT (Oliver: "mopboy must mop for 5 seconds and really remove some gore visibly
|
||||||
|
// before the new opponent arrives"). The fresh opponent used to spawn on the same frame as the
|
||||||
|
// kill, so the boy always mopped behind the next game's back. Now the court belongs to him
|
||||||
|
// first: the full job allowance (a between-games sendBoy books one stain), and matchOpponent
|
||||||
|
// waits on d.mopT (ticked in tickMatchFall). A court with nothing worth mopping skips the wait —
|
||||||
|
// an empty beat is a stall, not a show.
|
||||||
|
if (G.boy && G.boy.mop) { G.boy.jobs = TUNE.boyMaxJobs; d.mopT = TUNE.matchMopT; }
|
||||||
|
else d.mopT = 0.6;
|
||||||
|
// Serving stays blocked for the whole beat: the match's permanent interlude carries it, and
|
||||||
|
// keyPressed's introT bar sits ABOVE its over-beat re-arm, so a tap cannot serve at an empty
|
||||||
|
// court. matchOpponent replaces this object and re-arms the normal 1.6 s intro when it runs.
|
||||||
|
G.interlude = { introT: d.mopT + 1.6 };
|
||||||
d.opp++;
|
d.opp++;
|
||||||
// ALTERNATING SERVICE, as the sport has it. The match calls advancePoint directly, which banks
|
// ALTERNATING SERVICE, as the sport has it. The match calls advancePoint directly, which banks
|
||||||
// the game but never touches the server — that flip lives in tennis's own pointTo, which the
|
// the game but never touches the server — that flip lives in tennis's own pointTo, which the
|
||||||
// match bypasses (the same shape as the G.serveNum bug this file already carries a note about).
|
// match bypasses (the same shape as the G.serveNum bug this file already carries a note about).
|
||||||
score.server = other(score.server);
|
score.server = other(score.server);
|
||||||
matchOpponent(); // fresh body, its own intro beat
|
// d.ending STAYS TRUE for the whole beat — the empty-court check in resolveInterlude reads
|
||||||
d.ending = false;
|
// "no body + not ending" as regicide, so a stray resolve during the beat would bank a phantom
|
||||||
matchReady();
|
// game. The walk-on tick below is what re-opens play, exactly where the old code did.
|
||||||
G.phase = 'over'; G.overT = 2.2;
|
G.phase = 'over'; G.overT = d.mopT + 2.2; // ticks down through the beat; reset at the walk-on
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchGameLost() {
|
function matchGameLost() {
|
||||||
@@ -610,7 +637,7 @@ export function resolveInterlude(side) {
|
|||||||
// of body text under a weapon icon it read as a footnote to the tin rather than as the prize.
|
// of body text under a weapon icon it read as a footnote to the tin rather than as the prize.
|
||||||
if (G.hp < G.hpMax) {
|
if (G.hp < G.hpMax) {
|
||||||
G.hp = Math.min(G.hpMax, G.hp + TUNE.healPerDuel);
|
G.hp = Math.min(G.hpMax, G.hp + TUNE.healPerDuel);
|
||||||
voice('ump_trainer', 0.2, 0.95);
|
aside('ump_trainer', 0.2); // benched with the rest of Daniel's personality — see aside()
|
||||||
showModal({ title: 'WOUNDS TREATED', icon: 'x_cross', iconH: 52, color: UI.pick,
|
showModal({ title: 'WOUNDS TREATED', icon: 'x_cross', iconH: 52, color: UI.pick,
|
||||||
body: ['THE TRAINER PATCHED YOU UP', G.hp + ' OF ' + G.hpMax + ' BACK'] });
|
body: ['THE TRAINER PATCHED YOU UP', G.hp + ' OF ' + G.hpMax + ' BACK'] });
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-3
@@ -222,7 +222,11 @@ export const TUNE = {
|
|||||||
changeoverT: 5.0, // s the changeover breather lasts (after odd games, as the rules demand)
|
changeoverT: 5.0, // s the changeover breather lasts (after odd games, as the rules demand)
|
||||||
boyMopSpeed: 2.2, // m/s a MOPPING ball boy crosses at (a fetching one still sprints at 7.5)
|
boyMopSpeed: 2.2, // m/s a MOPPING ball boy crosses at (a fetching one still sprints at 7.5)
|
||||||
boyMopDwell: 1.8, // s he stands on a stain pushing the mop before moving to the next one
|
boyMopDwell: 1.8, // s he stands on a stain pushing the mop before moving to the next one
|
||||||
|
boyHurry: 4.6, // m/s he covers ground during the MATCH mop beat — brisk, so the 5 s window
|
||||||
|
// is spent scrubbing rather than walking into shot
|
||||||
boyMaxJobs: 4, // stains one boy will clean in a round — a kill streak must not employ him forever
|
boyMaxJobs: 4, // stains one boy will clean in a round — a kill streak must not employ him forever
|
||||||
|
matchMopT: 5, // s the mop beat holds the court between match games — the boy visibly cleans
|
||||||
|
// before the next opponent walks on (skipped when there is nothing to mop)
|
||||||
boyStride: 0.28, // m of travel per animation frame. Frames are driven by DISTANCE, not time, so
|
boyStride: 0.28, // m of travel per animation frame. Frames are driven by DISTANCE, not time, so
|
||||||
// the feet cannot slide: change a boy's speed and his legs keep up on their own.
|
// the feet cannot slide: change a boy's speed and his legs keep up on their own.
|
||||||
gameBeatT: 1.6, // s the game-end beat lasts on even (non-changeover) games
|
gameBeatT: 1.6, // s the game-end beat lasts on even (non-changeover) games
|
||||||
@@ -613,7 +617,7 @@ export const MODES = [
|
|||||||
|
|
||||||
export const TOURS = [
|
export const TOURS = [
|
||||||
{ id: 'men', name: "MEN'S SINGLES", tile: 'tour_m', sfx: null },
|
{ id: 'men', name: "MEN'S SINGLES", tile: 'tour_m', sfx: null },
|
||||||
{ id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', dx: 0.46, apex: 0.78 },
|
{ id: 'women', name: "WOMEN'S SINGLES", tile: 'tour_w', sfx: '_f', dx: 0.46, apex: 0.78, tossDx: -0.59 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// netW was the singles-stick width (10.06) while the court is drawn to the DOUBLES lines, so the
|
// netW was the singles-stick width (10.06) while the court is drawn to the DOUBLES lines, so the
|
||||||
@@ -806,6 +810,23 @@ export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
|
|||||||
// the top of _master.py for the fix its next attempt needs.
|
// the top of _master.py for the fix its next attempt needs.
|
||||||
export const ZFAMS = ['r', 'br', 'sh', 'bb', 'st', 'ru', 'gk', 'lj'];
|
export const ZFAMS = ['r', 'br', 'sh', 'bb', 'st', 'ru', 'gk', 'lj'];
|
||||||
|
|
||||||
|
// HEAD CROPS for the opponent intro card — a straight source-pixel crop out of each family's own
|
||||||
|
// healthy frame (z_<fam>1.png), so the card's icon is the body on court, never separate art.
|
||||||
|
// MEASURED offline (silhouette-width profile: the head band ends where the shoulders widen; br and
|
||||||
|
// gk hand-extended past the rule — the brute's shoulders start early, the keeper's hat brim wider
|
||||||
|
// than his face). Re-run the measurement when a family sheet is regenerated. A family missing here
|
||||||
|
// simply gets a text-only card, so a new roster row never breaks the walk-on.
|
||||||
|
export const HEAD_CROP = {
|
||||||
|
r: { x: 3, y: 0, w: 71, h: 73 },
|
||||||
|
br: { x: 0, y: 0, w: 87, h: 61 },
|
||||||
|
sh: { x: 8, y: 0, w: 62, h: 65 },
|
||||||
|
bb: { x: 7, y: 0, w: 62, h: 65 },
|
||||||
|
st: { x: 14, y: 0, w: 57, h: 55 },
|
||||||
|
ru: { x: 14, y: 0, w: 57, h: 55 },
|
||||||
|
gk: { x: 6, y: 0, w: 73, h: 65 },
|
||||||
|
lj: { x: 6, y: 0, w: 74, h: 56 },
|
||||||
|
};
|
||||||
|
|
||||||
// THE RACKET FAMILY'S THREE STAGES, as a FRACTION of the pool — never against literal hp, the rule
|
// THE RACKET FAMILY'S THREE STAGES, as a FRACTION of the pool — never against literal hp, the rule
|
||||||
// this whole file already holds (see woundStage). It lived inline in drawZombie; the FALL BEAT needs
|
// this whole file already holds (see woundStage). It lived inline in drawZombie; the FALL BEAT needs
|
||||||
// the same answer to know which body collapses and which one gets back up, and two copies of a
|
// the same answer to know which body collapses and which one gets back up, and two copies of a
|
||||||
@@ -1005,8 +1026,13 @@ export const HOME = { P: { x: 0, z: 22.8 }, O: { x: 0, z: 2.5 } };
|
|||||||
// where the near player is allowed to move
|
// where the near player is allowed to move
|
||||||
export const P_BOUNDS = { x: 6.5, zMin: COURT.netZ + 1.2, zMax: 26 };
|
export const P_BOUNDS = { x: 6.5, zMin: COURT.netZ + 1.2, zMax: 26 };
|
||||||
|
|
||||||
// serve toss: release height and upward speed (apex derives from these + gravity)
|
// serve toss: release height and upward speed (apex derives from these + gravity).
|
||||||
export const TOSS = { y0: 1.8, vy: 4.6 };
|
// dx/oDx: the tossing HAND's x offset from the frame centre, in metres, at facing +1 — the ball
|
||||||
|
// must rise over the raised palm, not the head (Oliver's report). MEASURED off the toss frames
|
||||||
|
// (topmost opaque row = the fingertips, × PLAYER_H/SPRITE_H): p_toss −52 px → −0.44 m,
|
||||||
|
// o_toss +50 px → +0.42 m (he is drawn from the front, so his left hand is the viewer's right).
|
||||||
|
// The women's frame reaches further (p_toss_f −70 px → −0.59 m) — TOURS overrides tossDx.
|
||||||
|
export const TOSS = { y0: 1.8, vy: 4.6, dx: -0.44, oDx: 0.42 };
|
||||||
|
|
||||||
// shot target depth from the receiving baseline, by aim (up = deep, none = mid, down = short)
|
// shot target depth from the receiving baseline, by aim (up = deep, none = mid, down = short)
|
||||||
export const SHOT_DEPTH = { deep: 3.0, mid: 5.4, short: 8.6 };
|
export const SHOT_DEPTH = { deep: 3.0, mid: 5.4, short: 8.6 };
|
||||||
|
|||||||
+27
-12
@@ -1,7 +1,7 @@
|
|||||||
// Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update.
|
// Game logic: ballistics, swings, serves, rules/scoring, AI, and the per-frame update.
|
||||||
|
|
||||||
import { TUNE, UI, bossArt, 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, bossArt, 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, court, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
import { P, O, ball, score, crowd, G, court, tour, other, deuceSide, setupPoint, sendBoy, resetState } from './state.js';
|
||||||
import { audio, pock, swish, whoosh, applause, voice, sfx, aww, zombieMusic, zombieDuck, menuDuck, splat, shriek, boom } 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
|
||||||
@@ -232,11 +232,11 @@ function doHit(ch, quality, aim) {
|
|||||||
// ----------------------------------------------------------------- serve ----
|
// ----------------------------------------------------------------- serve ----
|
||||||
function tossBall(sv) {
|
function tossBall(sv) {
|
||||||
ball.active = true; ball.bounces = 0; ball.collected = false;
|
ball.active = true; ball.bounces = 0; ball.collected = false;
|
||||||
// The player is right-handed: he tosses with his LEFT hand (viewer's left) and hits with the
|
// The ball rises over the tossing HAND, not the head — TOSS.dx/oDx are measured off the toss
|
||||||
// racket on the viewer's right, so the ball goes up almost over his head — a big offset either way
|
// frames' raised fingertips (see config). `* facing` because drawChar mirrors the whole sprite
|
||||||
// lands it on the wrong side of one of those two frames. The opponent is drawn from the front and
|
// when the server faces left, exactly like the idle bounce's offset.
|
||||||
// keeps the reach he was drawn with.
|
ball.x = sv.x + sv.facing * (sv.side === 'P' ? (tour.tossDx ?? TOSS.dx) : TOSS.oDx);
|
||||||
ball.x = sv.x + (sv.side === 'P' ? 0.12 : -0.35); ball.z = sv.z; ball.y = TOSS.y0;
|
ball.z = sv.z; ball.y = TOSS.y0;
|
||||||
ball.vx = 0; ball.vz = 0; ball.vy = TOSS.vy; ball.g = TUNE.gravity;
|
ball.vx = 0; ball.vz = 0; ball.vy = TOSS.vy; ball.g = TUNE.gravity;
|
||||||
// THE TOSS IS THE SERVER'S BALL. lastHit was left stale from the previous rally, and if the
|
// THE TOSS IS THE SERVER'S BALL. lastHit was left stale from the previous rally, and if the
|
||||||
// OPPONENT had touched it last, the freshly tossed ball read as *their* ball in flight — which
|
// OPPONENT had touched it last, the freshly tossed ball read as *their* ball in flight — which
|
||||||
@@ -424,7 +424,6 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
|||||||
// (spendSlot already hands you back plain balls when the last one goes)
|
// (spendSlot already hands you back plain balls when the last one goes)
|
||||||
G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded serve strike, per side
|
G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true }); // recorded serve strike, per side
|
||||||
sv.pose = 'serve'; sv.poseT = 0;
|
sv.pose = 'serve'; sv.poseT = 0;
|
||||||
if (sv.side === 'P') G.tossDrops = 0; // a served toss ends the apex lesson
|
|
||||||
if (perfect && sv.side === 'P') {
|
if (perfect && sv.side === 'P') {
|
||||||
G.hitstop = 0.05;
|
G.hitstop = 0.05;
|
||||||
// the word is ARMED here and pops at the IMPACT — first body the ball touches, or its first
|
// the word is ARMED here and pops at the IMPACT — first body the ball touches, or its first
|
||||||
@@ -474,6 +473,13 @@ export function dismissModal() {
|
|||||||
// and not situation aware.. they seem random". `aside()` is the single entry point and it enforces
|
// and not situation aware.. they seem random". `aside()` is the single entry point and it enforces
|
||||||
// one shared cooldown (TUNE.asideEvery), so several triggers firing at once still yields one remark.
|
// one shared cooldown (TUNE.asideEvery), so several triggers firing at once still yields one remark.
|
||||||
export function aside(name, delay = 0.2) {
|
export function aside(name, delay = 0.2) {
|
||||||
|
// DANIEL IS BENCHED (2026-08-13). Oliver: "daniel is very repetetive. please let him just
|
||||||
|
// announce the normal tennis game events for now.. this is a task for another time." So the
|
||||||
|
// umpire keeps every OFFICIATING call (score, game/set, fault, out, let, time, play) and loses
|
||||||
|
// ALL personality asides — this early return is the whole cut, since every ump_* remark routes
|
||||||
|
// through here. The situation triggers and the cooldown model below survive intact for the
|
||||||
|
// personality pass that will re-open him; do not delete them.
|
||||||
|
return;
|
||||||
// THE MATCH IS THE EXCEPTION to the interlude bar, exactly as sendBoy is: the match runs on a
|
// THE MATCH IS THE EXCEPTION to the interlude bar, exactly as sendBoy is: the match runs on a
|
||||||
// PERMANENT interlude, so this line silently muted the umpire for an entire mode — the one mode
|
// PERMANENT interlude, so this line silently muted the umpire for an entire mode — the one mode
|
||||||
// that is literally a tennis match he is sitting over. The shared asideEvery cooldown still holds,
|
// that is literally a tennis match he is sitting over. The shared asideEvery cooldown still holds,
|
||||||
@@ -1294,7 +1300,10 @@ function mopRound(b, dt) {
|
|||||||
if (b.tx === undefined && b.jobs > 0) {
|
if (b.tx === undefined && b.jobs > 0) {
|
||||||
let best = null, bs = -1;
|
let best = null, bs = -1;
|
||||||
for (const d of gore) {
|
for (const d of gore) {
|
||||||
if (d.a < 0.15 || d.z < COURT.netZ - 2) continue; // faded, or up the far end where he can't work
|
// faded, or up the far end where he can't work — EXCEPT in the match, where the far end is
|
||||||
|
// exactly where every opponent dies: the mop beat holds an empty court for him, so the
|
||||||
|
// gore-mode reach limit would leave him jobless two steps from the mess he was sent for
|
||||||
|
if (d.a < 0.15 || (!G.match && d.z < COURT.netZ - 2)) continue;
|
||||||
const score = d.a * d.r - Math.hypot(d.x - b.x, d.z - b.z) * 0.04; // big and close first
|
const score = d.a * d.r - Math.hypot(d.x - b.x, d.z - b.z) * 0.04; // big and close first
|
||||||
if (score > bs) { bs = score; best = d; }
|
if (score > bs) { bs = score; best = d; }
|
||||||
}
|
}
|
||||||
@@ -1304,7 +1313,10 @@ function mopRound(b, dt) {
|
|||||||
const working = b.tx !== undefined && b.jobs > 0;
|
const working = b.tx !== undefined && b.jobs > 0;
|
||||||
const gx = working ? b.tx : b.dir * 14, gz = working ? b.tz : b.z; // no target left: walk off the wing
|
const gx = working ? b.tx : b.dir * 14, gz = working ? b.tz : b.z; // no target left: walk off the wing
|
||||||
const dx = gx - b.x, dz = gz - b.z, d = Math.hypot(dx, dz);
|
const dx = gx - b.x, dz = gz - b.z, d = Math.hypot(dx, dz);
|
||||||
const step = Math.min(TUNE.boyMopSpeed * dt, d);
|
// during the match's mop beat he HURRIES between stains (the beat is his stage and it is 5 s
|
||||||
|
// long; at walking pace he spent 4 of them getting into shot) — the push itself keeps its pace
|
||||||
|
const spd = G.match && G.match.mopT > 0 ? TUNE.boyHurry : TUNE.boyMopSpeed;
|
||||||
|
const step = Math.min(spd * dt, d);
|
||||||
if (d > 0.001) { b.x += dx / d * step; b.z += dz / d * step; b.dist = (b.dist || 0) + step; }
|
if (d > 0.001) { b.x += dx / d * step; b.z += dz / d * step; b.dist = (b.dist || 0) + step; }
|
||||||
if (Math.abs(dx) > 0.05) b.dir = dx > 0 ? 1 : -1; // face the way he's walking
|
if (Math.abs(dx) > 0.05) b.dir = dx > 0 ? 1 : -1; // face the way he's walking
|
||||||
if (working && d <= 0.12) { b.mopT = TUNE.boyMopDwell; b.tx = undefined; b.tz = undefined; } // arrived: get to work
|
if (working && d <= 0.12) { b.mopT = TUNE.boyMopDwell; b.tx = undefined; b.tz = undefined; } // arrived: get to work
|
||||||
@@ -1671,10 +1683,13 @@ export function update(dt) {
|
|||||||
}
|
}
|
||||||
if (G.phase === 'toss' && ball.y <= 1.4 && ball.vy < 0 && !G.aim) { // uncaught toss → retoss (a held slingshot aim keeps the toss up)
|
if (G.phase === 'toss' && ball.y <= 1.4 && ball.vy < 0 && !G.aim) { // uncaught toss → retoss (a held slingshot aim keeps the toss up)
|
||||||
ball.active = false; G.phase = 'pretoss';
|
ball.active = false; G.phase = 'pretoss';
|
||||||
|
// an uncaught toss is NOT a swing. The 'serve' pose used to survive into pretoss, and
|
||||||
|
// drawChar's pick (phase !== 'toss' → smash) flashed the SMASH frame at a ball the player
|
||||||
|
// never hit — Oliver: "the smash frame is always played even if I did not tap a second
|
||||||
|
// time". Back to idle, where the bounce stance picks him up.
|
||||||
|
const sv = score.server === 'P' ? P : O;
|
||||||
|
if (sv.pose === 'serve') { sv.pose = 'idle'; sv.poseT = 0; }
|
||||||
if (score.server === 'O') G.aiServeT = 0.8;
|
if (score.server === 'O') G.aiServeT = 0.8;
|
||||||
// dropping the toss twice running means the second tap isn't landing — the ghost hand then
|
|
||||||
// rides the next toss and presses at the apex (render.drawApexHint)
|
|
||||||
else if (G.mode === 'tennis') G.tossDrops++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-40
@@ -1,7 +1,7 @@
|
|||||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||||
|
|
||||||
import { cv, ctx } from './dom.js';
|
import { cv, ctx } from './dom.js';
|
||||||
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS } from './config.js';
|
import { BENCH, BENCH_STAGES, ZFAMS, famFrames, racketStage, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS, HEAD_CROP } from './config.js';
|
||||||
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
||||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY } from './gameplay.js';
|
import { predictLanding, previewServe, slotId, slotStock, serveApexY } from './gameplay.js';
|
||||||
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
||||||
@@ -1708,16 +1708,31 @@ function cardBody(c, dx) {
|
|||||||
// ...and the title FITS. Cards carry whole sentences now ("SWING TO RETURN — OR SWISH IT AWAY")
|
// ...and the title FITS. Cards carry whole sentences now ("SWING TO RETURN — OR SWISH IT AWAY")
|
||||||
// and boss names of any length, and Bangers 22 ran a long one off both edges of a phone. Measure
|
// and boss names of any length, and Bangers 22 ran a long one off both edges of a phone. Measure
|
||||||
// once and shrink to the band; 22 stays 22 for everything that already fit.
|
// once and shrink to the band; 22 stays 22 for everything that already fit.
|
||||||
|
// THE OPPONENT CARD CARRIES ITS HEAD (Oliver: the punchline plus a picture of his head). The
|
||||||
|
// icon is a straight crop out of the family's own healthy frame — config.HEAD_CROP, measured,
|
||||||
|
// never separate art — drawn left of the text block, which shifts right to keep the whole
|
||||||
|
// group centred as one. A family with no crop row degrades to the plain text card.
|
||||||
|
const hd = c.head && HEAD_CROP[c.head] && SPR['z_' + c.head + '1'] ? HEAD_CROP[c.head] : null;
|
||||||
|
const ih = hd ? CARD_H - 14 : 0, iw = hd ? Math.round(hd.w * ih / hd.h) : 0;
|
||||||
|
const room = W - 24 - (hd ? iw + 8 : 0);
|
||||||
ctx.font = B(22);
|
ctx.font = B(22);
|
||||||
const tw = ctx.measureText(title).width;
|
const tw = ctx.measureText(title).width;
|
||||||
const tSize = tw > W - 24 ? Math.max(13, Math.floor(22 * (W - 24) / tw)) : 22;
|
const tSize = tw > room ? Math.max(13, Math.floor(22 * room / tw)) : 22;
|
||||||
const lines = [[title, B(tSize), 0, color]];
|
const lines = [[title, B(tSize), 0, color]];
|
||||||
if (sub1) lines.push([sub1, F(8), 30, UI.cream]);
|
if (sub1) lines.push([sub1, F(8), 30, UI.cream]);
|
||||||
if (sub2) lines.push([sub2, F(8), 44, UI.cream]);
|
if (sub2) lines.push([sub2, F(8), 44, UI.cream]);
|
||||||
const s = inkSpan(lines);
|
const s = inkSpan(lines);
|
||||||
plate(s.bot - s.top);
|
plate(Math.max(s.bot - s.top, ih));
|
||||||
const y = Math.round(CARD_MID - (s.top + s.bot) / 2);
|
const y = Math.round(CARD_MID - (s.top + s.bot) / 2);
|
||||||
for (const [t, f, rel, col] of lines) { ctx.font = f; shadowText(t, W / 2, y + rel, col); }
|
const cx = Math.round(W / 2 + (hd ? (iw + 8) / 2 : 0));
|
||||||
|
if (hd) {
|
||||||
|
let blockW = 0;
|
||||||
|
for (const [t, f] of lines) { ctx.font = f; blockW = Math.max(blockW, ctx.measureText(t).width); }
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(SPR['z_' + c.head + '1'], hd.x, hd.y, hd.w, hd.h,
|
||||||
|
Math.round(cx - blockW / 2 - 8 - iw), Math.round(CARD_MID - ih / 2), iw, ih);
|
||||||
|
}
|
||||||
|
for (const [t, f, rel, col] of lines) { ctx.font = f; shadowText(t, cx, y + rel, col); }
|
||||||
ctx.textAlign = 'center'; ctx.restore(); // the band's neighbours (flash, countdown) assume centred
|
ctx.textAlign = 'center'; ctx.restore(); // the band's neighbours (flash, countdown) assume centred
|
||||||
}
|
}
|
||||||
// The plate, sized to the `h` px of content it carries and centred on the axis. Capped at CARD_H:
|
// The plate, sized to the `h` px of content it carries and centred on the axis. Capped at CARD_H:
|
||||||
@@ -1841,37 +1856,10 @@ function drawPullHint() {
|
|||||||
demoSwipe(dir, 0, 'SWIPE TO DODGE');
|
demoSwipe(dir, 0, 'SWIPE TO DODGE');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- the TENNIS lessons, same ghost hand. Tennis has no slingshot to mime, but the testteam
|
// ---- the TENNIS lesson, same ghost hand. The serve hints (an idle tap demo on the apron, an
|
||||||
// struggled to grasp the tap ritual too — so the hand TAPS: on the apron when the player sits idle
|
// apex rider on a dropped toss) lived here too and were DELETED on Oliver's report — a ghost
|
||||||
// in the serve stance, riding the toss when they keep dropping it, and beside the player when
|
// finger over the running serve animation read as clutter, and the zombie duel serves fine with
|
||||||
// returns keep sailing past untouched. Each appears on the exact struggle it answers and goes
|
// no teacher at all. Only the return lesson below survives.
|
||||||
// away the moment the player does the thing (the counters live in state.G, reset in gameplay).
|
|
||||||
function drawTapHint() {
|
|
||||||
const p0 = proj(P.x, 0, P.z);
|
|
||||||
const ax = Math.round(p0.x) + 20, ay = Math.round(p0.y) + 14; // beside his feet, on the apron
|
|
||||||
const t = (G.idleHintT - TUNE.pullHintAfter) % 1.6; // hover · press · hold · lift
|
|
||||||
const press = t < 0.6 ? 0 : t < 0.75 ? (t - 0.6) / 0.15 : t < 0.95 ? 1 : Math.max(0, 1 - (t - 0.95) / 0.2);
|
|
||||||
ctx.save(); ctx.globalAlpha = 0.9;
|
|
||||||
if (t >= 0.72 && t < 1.32) drawTapRing(ax, ay, (t - 0.72) / 0.6);
|
|
||||||
drawGhostHand(ax, ay, press);
|
|
||||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
|
||||||
shadowText('TAP TO TOSS', ax - 20, ay + 40, UI.warm);
|
|
||||||
shadowText('TAP AGAIN AT THE TOP', ax - 20, ay + 54, UI.warm);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
// two dropped tosses in a row: the hand rides the ball itself and presses exactly at the apex,
|
|
||||||
// so the TIMING is the thing being shown, not just the verb
|
|
||||||
function drawApexHint() {
|
|
||||||
const b = proj(ball.x, ball.y, ball.z);
|
|
||||||
const apex = TOSS.y0 + TOSS.vy * TOSS.vy / (2 * TUNE.gravity);
|
|
||||||
const press = Math.max(0, 1 - Math.abs(ball.y - apex) / 0.45);
|
|
||||||
ctx.save(); ctx.globalAlpha = 0.9;
|
|
||||||
if (press > 0.55) drawTapRing(b.x + 4, b.y + 4, 1 - press); // tight at the apex, blooming as it passes
|
|
||||||
drawGhostHand(b.x + 4, b.y + 4, press);
|
|
||||||
ctx.font = F(10); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
|
||||||
shadowText('TAP AT THE TOP', b.x, b.y - 28, UI.warm);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
// two returns passed untouched: the hand presses harder as the ball closes on the player
|
// two returns passed untouched: the hand presses harder as the ball closes on the player
|
||||||
function drawHitHint() {
|
function drawHitHint() {
|
||||||
const p0 = proj(P.x, 0, P.z);
|
const p0 = proj(P.x, 0, P.z);
|
||||||
@@ -2423,7 +2411,11 @@ export function render() {
|
|||||||
// after a beat of inaction the apron demo takes over, and it says PULL DOWN in its own words —
|
// after a beat of inaction the apron demo takes over, and it says PULL DOWN in its own words —
|
||||||
// so this slot shuts up rather than printing the same instruction a second time 40 px below it
|
// so this slot shuts up rather than printing the same instruction a second time 40 px below it
|
||||||
const demo = G.mode === 'splatter' && !G.interlude && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
const demo = G.mode === 'splatter' && !G.interlude && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
||||||
const tapDemo = G.mode === 'tennis' && G.serveNum !== 2 && G.idleHintT > TUNE.pullHintAfter;
|
// NO GHOST HAND ON THE TENNIS SERVE — not the idle tap demo, not the apex rider. Oliver: "this
|
||||||
|
// ghost finger over the running serve animation is distracting. this must be removed. it is
|
||||||
|
// not in zombie tennis mode" — and the duel serving fine without a teacher is the argument.
|
||||||
|
// The rally-return hint (drawHitHint) stays: it answers a different struggle and he did not
|
||||||
|
// flag it. drawTapHint/drawApexHint were deleted with their counters.
|
||||||
// The gesture-teaching strings ('PULL DOWN TO AIM…', 'TAP TO TOSS…') are GONE — the ghost hand
|
// The gesture-teaching strings ('PULL DOWN TO AIM…', 'TAP TO TOSS…') are GONE — the ghost hand
|
||||||
// is the teacher now, and saying the same thing twice in two registers was clutter. This slot
|
// is the teacher now, and saying the same thing twice in two registers was clutter. This slot
|
||||||
// only carries STATE the mime cannot: which weapon the serve fires, and a second serve.
|
// only carries STATE the mime cannot: which weapon the serve fires, and a second serve.
|
||||||
@@ -2435,11 +2427,8 @@ export function render() {
|
|||||||
: '';
|
: '';
|
||||||
if (txt) shadowText(txt, W / 2, METER_Y(), UI.warm); // same slot the draw meter uses
|
if (txt) shadowText(txt, W / 2, METER_Y(), UI.warm); // same slot the draw meter uses
|
||||||
if (demo) drawPullHint(); // show the gesture instead of only naming it
|
if (demo) drawPullHint(); // show the gesture instead of only naming it
|
||||||
if (tapDemo) drawTapHint();
|
|
||||||
}
|
}
|
||||||
// the tennis struggle hints: each rides the exact failure it answers (see drawTapHint's block)
|
// the one surviving tennis struggle hint: rides the exact failure it answers
|
||||||
if (G.mode === 'tennis' && score.server === 'P' && G.phase === 'toss' && G.tossDrops >= 2 && ball.active)
|
|
||||||
drawApexHint();
|
|
||||||
if (G.mode === 'tennis' && G.phase === 'rally' && G.passStreak >= 2 && ball.active && ball.lastHit === 'O')
|
if (G.mode === 'tennis' && G.phase === 'rally' && G.passStreak >= 2 && ball.active && ball.lastHit === 'O')
|
||||||
drawHitHint();
|
drawHitHint();
|
||||||
// The free-floating register: no panel, one fixed slot, shouted. PERFECT, GAME N, DOUBLE KILL,
|
// The free-floating register: no panel, one fixed slot, shouted. PERFECT, GAME N, DOUBLE KILL,
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export const G = {
|
|||||||
meleeT: 0, // s of racket-swish cooldown left
|
meleeT: 0, // s of racket-swish cooldown left
|
||||||
rackGlow: null, // {slot, t} — a freshly gathered barrel weapon pulses its rack tile briefly
|
rackGlow: null, // {slot, t} — a freshly gathered barrel weapon pulses its rack tile briefly
|
||||||
// instead of equipping itself (popBarrel keeps your current selection)
|
// instead of equipping itself (popBarrel keeps your current selection)
|
||||||
tossDrops: 0, // consecutive DROPPED tosses (tennis) — ≥2 shows the ghost-hand apex hint
|
|
||||||
passStreak: 0, // consecutive rally balls that passed untouched (tennis) — ≥2 shows TAP TO HIT
|
passStreak: 0, // consecutive rally balls that passed untouched (tennis) — ≥2 shows TAP TO HIT
|
||||||
// ---- the sidestep. The movement block is 'rally'-only on purpose, so this integrates OUTSIDE it
|
// ---- the sidestep. The movement block is 'rally'-only on purpose, so this integrates OUTSIDE it
|
||||||
// and is the only way to move in the serve stance (which is where a boss fight is spent).
|
// and is the only way to move in the serve stance (which is where a boss fight is spent).
|
||||||
|
|||||||
+2
-2
@@ -113,7 +113,7 @@ export function spawnBarrel() {
|
|||||||
// shoots at you until you deal with it.
|
// shoots at you until you deal with it.
|
||||||
export function spawnMachine() {
|
export function spawnMachine() {
|
||||||
spawnZombie((Math.random() * 2 - 1) * 3.6, 2.2 + Math.random() * 3, 'm');
|
spawnZombie((Math.random() * 2 - 1) * 3.6, 2.2 + Math.random() * 3, 'm');
|
||||||
voice('ump_machine', 0.4, 0.95);
|
aside('ump_machine', 0.4); // benched with the rest of Daniel's personality — see aside()
|
||||||
if (!G.seen.mach) {
|
if (!G.seen.mach) {
|
||||||
G.seen.mach = true;
|
G.seen.mach = true;
|
||||||
G.card = { title: 'SWING TO RETURN — OR SWISH IT AWAY', color: UI.warm, t: 2.6 };
|
G.card = { title: 'SWING TO RETURN — OR SWISH IT AWAY', color: UI.warm, t: 2.6 };
|
||||||
@@ -273,7 +273,7 @@ export function hurtPlayer(n) {
|
|||||||
vx: (Math.random() * 2 - 1) * 2.2, vy: 0.8 + Math.random() * 2, vz: (Math.random() * 2 - 1) * 2.2,
|
vx: (Math.random() * 2 - 1) * 2.2, vy: 0.8 + Math.random() * 2, vz: (Math.random() * 2 - 1) * 2.2,
|
||||||
t: 0.35 + Math.random() * 0.35, dark: Math.random() < 0.5 });
|
t: 0.35 + Math.random() * 0.35, dark: Math.random() < 0.5 });
|
||||||
if (G.hp <= 0) killPlayer();
|
if (G.hp <= 0) killPlayer();
|
||||||
else voice('ump_quiet_bleeding', 0.3, 0.95); // the umpire objects on your behalf. He is still watching.
|
else aside('ump_quiet_bleeding', 0.3); // benched with the rest of Daniel's personality — see aside()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Death, from EITHER channel: a shambler reaching you, or your hp running out. Extracted so the two
|
// Death, from EITHER channel: a shambler reaching you, or your hp running out. Extracted so the two
|
||||||
|
|||||||
@@ -505,6 +505,9 @@ test('endDrill hands the court back', () => {
|
|||||||
|
|
||||||
const startMatch = () => { const r = _startMatch(); G.modal = null; G.modalQ.length = 0; return r; };
|
const startMatch = () => { const r = _startMatch(); G.modal = null; G.modalQ.length = 0; return r; };
|
||||||
const oppOf = () => zombies.find(z => z.variant === 'r');
|
const oppOf = () => zombies.find(z => z.variant === 'r');
|
||||||
|
// a won game now opens the MOP BEAT (TUNE.matchMopT) before the next opponent walks on —
|
||||||
|
// tests that play across a game boundary step through it here
|
||||||
|
const passMopBeat = () => { for (let i = 0; i < (TUNE.matchMopT + 1) * 60; i++) update(DT); };
|
||||||
|
|
||||||
test('a match owns the court like every other mode', () => {
|
test('a match owns the court like every other mode', () => {
|
||||||
assert.ok(startMatch(), 'a match starts on a clean court');
|
assert.ok(startMatch(), 'a match starts on a clean court');
|
||||||
@@ -532,8 +535,14 @@ test('a point won tears a wound stage off the body; only the game point fells it
|
|||||||
assert.equal(zb.hp, 1, 'three points in: one stage left');
|
assert.equal(zb.hp, 1, 'three points in: one stage left');
|
||||||
resolveInterlude('P'); // the game point
|
resolveInterlude('P'); // the game point
|
||||||
assert.ok(!zombies.includes(zb), 'the game-winning point IS the kill');
|
assert.ok(!zombies.includes(zb), 'the game-winning point IS the kill');
|
||||||
|
// THE MOP BEAT: the corpse's gore holds the court first — no fresh body until the boy has had
|
||||||
|
// his time (TUNE.matchMopT, skipped only on a clean court; the explode() above guarantees gore).
|
||||||
|
assert.ok(!oppOf(), 'no fresh opponent on the kill frame — the mop beat owns the court');
|
||||||
|
assert.ok(G.match.mopT > 0, 'the beat is armed');
|
||||||
|
assert.ok(G.interlude && G.interlude.introT > 0, 'and serving is barred for its whole length');
|
||||||
|
for (let i = 0; i < (TUNE.matchMopT + 1) * 10; i++) update(0.1);
|
||||||
const next = oppOf();
|
const next = oppOf();
|
||||||
assert.ok(next && next !== zb, 'a fresh opponent shambled on');
|
assert.ok(next && next !== zb, 'a fresh opponent shambled on after the mop beat');
|
||||||
assert.equal(next.hpMax, TUNE.matchOppHp, 'same flat pool — the ladder escalates in speed/flub, not hp');
|
assert.equal(next.hpMax, TUNE.matchOppHp, 'same flat pool — the ladder escalates in speed/flub, not hp');
|
||||||
assert.equal(G.runGames, 1, 'the ladder banked the game');
|
assert.equal(G.runGames, 1, 'the ladder banked the game');
|
||||||
assert.deepEqual(score.pts, [0, 0], 'and the points reset');
|
assert.deepEqual(score.pts, [0, 0], 'and the points reset');
|
||||||
@@ -753,6 +762,7 @@ test('service alternates by game, and the opponent actually serves', () => {
|
|||||||
for (let i = 0; i < 2 * 60 && G.matchFall; i++) update(DT); // let each collapse finish
|
for (let i = 0; i < 2 * 60 && G.matchFall; i++) update(DT); // let each collapse finish
|
||||||
}
|
}
|
||||||
assert.equal(score.server, 'O', 'their serve next game');
|
assert.equal(score.server, 'O', 'their serve next game');
|
||||||
|
for (let i = 0; i < (TUNE.matchMopT + 1) * 60; i++) update(DT); // the mop beat holds the court first
|
||||||
// the opponent stands behind ITS baseline to serve, not on the receiving mark
|
// the opponent stands behind ITS baseline to serve, not on the receiving mark
|
||||||
const rz = zombies.find(z => z.variant === 'r');
|
const rz = zombies.find(z => z.variant === 'r');
|
||||||
assert.ok(rz.z < 1.0, 'it steps back to serve: z=' + rz.z.toFixed(2));
|
assert.ok(rz.z < 1.0, 'it steps back to serve: z=' + rz.z.toFixed(2));
|
||||||
@@ -879,13 +889,17 @@ test('regicide: killing the body mid-game wins the game outright', () => {
|
|||||||
resolveInterlude('P'); // killZombie's exit lands here with the court empty
|
resolveInterlude('P'); // killZombie's exit lands here with the court empty
|
||||||
assert.equal(score.games[0], games + 1, 'the game banks');
|
assert.equal(score.games[0], games + 1, 'the game banks');
|
||||||
assert.deepEqual(score.pts, [0, 0], 'the part-played points reset');
|
assert.deepEqual(score.pts, [0, 0], 'the part-played points reset');
|
||||||
|
passMopBeat();
|
||||||
assert.ok(oppOf(), 'and the next opponent is on');
|
assert.ok(oppOf(), 'and the next opponent is on');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the match measures OPPONENTS FELLED, banked as its own kind of run', () => {
|
test('the match measures OPPONENTS FELLED, banked as its own kind of run', () => {
|
||||||
localStorage.removeItem('splatterRuns');
|
localStorage.removeItem('splatterRuns');
|
||||||
startMatch();
|
startMatch();
|
||||||
for (let k = 0; k < 8; k++) resolveInterlude('P'); // two games = two bodies down
|
for (let g = 0; g < 2; g++) { // two games = two bodies down
|
||||||
|
for (let k = 0; k < 4; k++) resolveInterlude('P');
|
||||||
|
passMopBeat(); // each corpse gets its mop beat
|
||||||
|
}
|
||||||
assert.equal(G.match.opp, 3, 'on the third rung');
|
assert.equal(G.match.opp, 3, 'on the third rung');
|
||||||
G.hp = 1; resolveInterlude('O');
|
G.hp = 1; resolveInterlude('O');
|
||||||
assert.equal(G.mode, 'eaten', 'bled out');
|
assert.equal(G.mode, 'eaten', 'bled out');
|
||||||
@@ -894,6 +908,7 @@ test('the match measures OPPONENTS FELLED, banked as its own kind of run', () =>
|
|||||||
// a shorter second run must NOT overwrite the mark, and must know what it is chasing
|
// a shorter second run must NOT overwrite the mark, and must know what it is chasing
|
||||||
G.mode = 'splatter'; endMatch(); startMatch();
|
G.mode = 'splatter'; endMatch(); startMatch();
|
||||||
for (let k = 0; k < 4; k++) resolveInterlude('P');
|
for (let k = 0; k < 4; k++) resolveInterlude('P');
|
||||||
|
passMopBeat(); // no points settle while the boy owns the court
|
||||||
G.hp = 1; resolveInterlude('O');
|
G.hp = 1; resolveInterlude('O');
|
||||||
assert.equal(G.match.felled, 1);
|
assert.equal(G.match.felled, 1);
|
||||||
assert.equal(G.match.best, 2, 'the mark to beat is shown');
|
assert.equal(G.match.best, 2, 'the mark to beat is shown');
|
||||||
|
|||||||
Reference in new issue
Block a user