Seven verdicts from the test group
- opening CPU beatable in a game or two (softer cap, more whiffs, rubber band) - modals are for calm courts only; live-court news is a card now - the transition howto is gone: a ghost hand teaches the pull, the toss, the apex and the swing, each on the exact struggle it answers - a shot barrel keeps your weapon and glows its slot instead - switching weapons clicks, the live tile is unmistakable - barrels come faster the deeper the run goes - the umpire triple-tap cheat is deleted Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f13cbef28a
commit
a89e8ff419
10 files changed
+242
-79
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||
|
||||
import { cv, ctx } from './dom.js';
|
||||
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_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 } from './config.js';
|
||||
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_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 { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js';
|
||||
import { zombies, gore, piles, parts, pellets, extras, shots } from './zombies.js';
|
||||
@@ -1130,10 +1130,22 @@ function drawRack() {
|
||||
const sel = G.weapon === i, dim = stockI <= 0;
|
||||
const label = i === 0 && !temp ? 'BALL' : w ? w.label : '';
|
||||
const icon = i === 0 && !temp ? 'w_ball' : w ? w.icon : null;
|
||||
// rounded slots: five separate tiles read as five buttons, which is what they are
|
||||
// rounded slots: five separate tiles read as five buttons, which is what they are.
|
||||
// The LIVE one has to read at a glance ("the active weapon should be more pronounced"):
|
||||
// a visibly brighter well and a 2px gold frame, not the same hairline every other tile gets.
|
||||
if (sel) ctx.lineWidth = 2;
|
||||
panel(x + 1.5, y0 + 1.5, g.tw - 3, g.h - 3, 4,
|
||||
sel ? '#123a24' : '#0a2418', // sunk slot
|
||||
sel ? '#1c5230' : '#0a2418', // sunk slot — the live one sits visibly lit
|
||||
sel ? UI.pick : '#05160e'); // gold frame when live
|
||||
if (sel) ctx.lineWidth = 1;
|
||||
// a slot that just came out of a barrel announces itself: a pink pulse for a moment, WITHOUT
|
||||
// stealing the selection (grabbing the gun mid-plan was the complaint — see popBarrel)
|
||||
if (G.rackGlow && G.rackGlow.slot === i) {
|
||||
const p = 0.35 + 0.55 * Math.abs(Math.sin(G.rackGlow.t * 7));
|
||||
ctx.save(); ctx.lineWidth = 2;
|
||||
panel(x + 1.5, y0 + 1.5, g.tw - 3, g.h - 3, 4, null, hexA(UI.pink, p));
|
||||
ctx.restore();
|
||||
}
|
||||
// icon: left, vertically centred, padded off the edge
|
||||
const box = g.h - pad * 2, icx = x + pad + box / 2, icy = y0 + g.h / 2;
|
||||
if (!icon) { ctx.globalAlpha = 1; continue; } // an empty socket: nothing gathered for it yet
|
||||
@@ -1181,7 +1193,33 @@ function drawCard(title, sub1, sub2, color, icon) {
|
||||
if (sub2) shadowText(sub2, W / 2, H / 2 + 18, UI.cream);
|
||||
}
|
||||
|
||||
// Sit in the serve stance doing nothing and the gesture demonstrates itself: a ghost thumb slides
|
||||
// A GHOST HAND, fingertip at (x, y) — the tutorial finger every gesture hint shares. A drawn
|
||||
// pointing hand rather than the old abstract dot: testers did not read a sliding circle as "your
|
||||
// finger goes here". `press` (0..1) dips it toward the glass, which is what makes a mimed tap
|
||||
// read as a tap and not as a cursor hovering. Pure canvas paths, no sprite: it is HUD chrome in
|
||||
// the UI palette, same family as the arrowheads and the dotted track, not a body on the court.
|
||||
function drawGhostHand(x, y, press = 0) {
|
||||
ctx.save();
|
||||
ctx.translate(Math.round(x), Math.round(y) - 2 + press * 3);
|
||||
ctx.rotate(-0.18); // the slight tilt a right hand actually lands with
|
||||
ctx.fillStyle = UI.cream; ctx.strokeStyle = 'rgba(8,12,10,0.75)'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.roundRect(-2.2, 0, 4.4, 11, 2.2); ctx.fill(); ctx.stroke(); // index finger, tip at origin
|
||||
ctx.beginPath(); ctx.roundRect(-3.2, 8, 11.5, 10, 4); ctx.fill(); ctx.stroke(); // the curled fist behind it
|
||||
ctx.beginPath(); ctx.ellipse(-2.6, 12.5, 2.4, 3.6, 0.55, 0, 7); ctx.fill(); ctx.stroke(); // folded thumb
|
||||
ctx.strokeStyle = 'rgba(8,12,10,0.35)'; // two knuckle creases — what makes the blob read as fingers
|
||||
ctx.beginPath(); ctx.moveTo(2.4, 9.5); ctx.lineTo(2.4, 13.5); ctx.moveTo(5.4, 9.5); ctx.lineTo(5.4, 13.5); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
// the contact ring a mimed tap leaves on the glass: grows and fades under the fingertip
|
||||
function drawTapRing(x, y, p) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= Math.max(0, 0.9 * (1 - p));
|
||||
ctx.strokeStyle = UI.warm; ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.arc(x, y, 3 + p * 12, 0, 7); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Sit in the serve stance doing nothing and the gesture demonstrates itself: a ghost hand slides
|
||||
// BACK from the player while chevrons grow FORWARD up the court, then it releases and repeats.
|
||||
// THE APRON IS THIS DEMO'S ROOM, and now it is the only thing in it. The draw meter used to share
|
||||
// the space, which was backwards on both counts: a live readout down here is under your thumb the
|
||||
@@ -1223,12 +1261,9 @@ function demoSwipe(dx, dy, label) {
|
||||
// carry it off the edge of the screen, and clearing the full travel below a horizontal stroke
|
||||
// left it floating in the middle of an empty apron with nothing to attach itself to.
|
||||
shadowText(label, ax, ay + (dy ? travel + 24 : 18), UI.warm);
|
||||
// the ghost thumb
|
||||
// the ghost hand riding the stroke, pressed into the glass for the whole draw
|
||||
ctx.globalAlpha = gone ? 0 : 0.9;
|
||||
ctx.fillStyle = UI.cream;
|
||||
ctx.beginPath(); ctx.arc(ax + dx * len, ay + dy * len, 6, 0, 7); ctx.fill();
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.45)';
|
||||
ctx.beginPath(); ctx.arc(ax + dx * len, ay + dy * len, 2.5, 0, 7); ctx.fill();
|
||||
drawGhostHand(ax + dx * len, ay + dy * len, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@@ -1246,6 +1281,51 @@ function drawPullHint() {
|
||||
demoSwipe(dir, 0, 'SWIPE TO DODGE');
|
||||
}
|
||||
|
||||
// ---- the TENNIS lessons, same ghost hand. Tennis has no slingshot to mime, but the testteam
|
||||
// struggled to grasp the tap ritual too — so the hand TAPS: on the apron when the player sits idle
|
||||
// in the serve stance, riding the toss when they keep dropping it, and beside the player when
|
||||
// returns keep sailing past untouched. Each appears on the exact struggle it answers and goes
|
||||
// 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(8); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP TO TOSS', ax - 20, ay + 26, UI.warm);
|
||||
shadowText('TAP AGAIN AT THE TOP', ax - 20, ay + 38, 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(8); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AT THE TOP', b.x, b.y - 24, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
// two returns passed untouched: the hand presses harder as the ball closes on the player
|
||||
function drawHitHint() {
|
||||
const p0 = proj(P.x, 0, P.z);
|
||||
const d = Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8); // the same distance trySwing tests
|
||||
const press = Math.max(0, Math.min(1, 1 - (d - 1.6) / 2.2));
|
||||
const hx = Math.round(p0.x) + 26, hy = Math.round(p0.y) - 52; // over his shoulder, off the sprite
|
||||
ctx.save(); ctx.globalAlpha = 0.9;
|
||||
if (press >= 1) drawTapRing(hx, hy, (G.idleT * 2) % 1);
|
||||
drawGhostHand(hx, hy, press);
|
||||
ctx.font = F(8); ctx.textAlign = 'center'; ctx.textBaseline = 'top';
|
||||
shadowText('TAP AS IT ARRIVES', hx - 26, hy + 34, UI.warm);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// The modal: drawn LAST, over everything, and the world behind it is frozen (gameplay.update
|
||||
// early-returns). Unlike drawCard this is a wall — nothing moves until it is tapped away.
|
||||
// easeOutBack: overshoots past the target and settles. The arcade snap — nothing in a cabinet
|
||||
@@ -1682,6 +1762,7 @@ export function render() {
|
||||
// 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
|
||||
const demo = G.mode === 'splatter' && !G.interlude && !gunLike && G.idleHintT > TUNE.pullHintAfter;
|
||||
const tapDemo = G.mode === 'tennis' && G.serveNum !== 2 && G.idleHintT > TUNE.pullHintAfter;
|
||||
const txt = G.mode === 'splatter' && !G.interlude
|
||||
? (gunLike ? (touch ? `TAP TO UNLOAD THE ${sel}` : `SPACE UNLOADS THE ${sel}`)
|
||||
: G.aim ? 'PULL DOWN: FURTHER = HARDER'
|
||||
@@ -1689,10 +1770,17 @@ export function render() {
|
||||
: demo ? ''
|
||||
: touch ? 'PULL DOWN TO AIM · RELEASE TO FIRE' : 'DRAG DOWN TO AIM · SPACE SERVES')
|
||||
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
|
||||
: tapDemo ? '' // the ghost hand says it in mime and in its own words
|
||||
: (touch ? 'TAP TO TOSS, TAP TO SERVE' : 'SPACE TO TOSS, SPACE TO SERVE');
|
||||
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 (tapDemo) drawTapHint();
|
||||
}
|
||||
// the tennis struggle hints: each rides the exact failure it answers (see drawTapHint's block)
|
||||
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')
|
||||
drawHitHint();
|
||||
// The free-floating register: no panel, one fixed slot, shouted. PERFECT, GAME N, DOUBLE KILL,
|
||||
// RETURNED, CHANGEOVER, weapon labels — everything that used to be plain white 12px text sitting
|
||||
// directly on the crowd. It keeps its 1px jitter; the shadow is what makes it legible anywhere.
|
||||
|
||||
Reference in new issue
Block a user