Phase 12: the weapon ladder — every tin opens something nastier
Deliveries cycle BOMB -> TRIPLE -> FIRE -> GUN (G.deliveries indexes the
ladder); one tin at a time, a fresh delivery replaces unspent stock. All
weapons stay one-thumb: the serve auto-consumes, zero new inputs.
- Triple serve (x3): one real serve fans two ballistic clones +-6deg (extras[]
in zombies.js — NOT the reverted rapid-fire: each fan costs a proper serve).
Side balls respect variant rules (wound brutes, clank off chairs, pass the boss)
- Flame ball (x3): kills on ANY contact regardless of pow, chair ignored,
full pierce (splatter pace tax undone), ember trail, white-hot render
- Machine gun (8s): horde auto-fire as a pickup — no serving, drag steers,
no assist, spawner keeps running, ammo bar drains on the HUD, then the
serve loop resumes
- HUD pips per type (black/yellow/orange) at the bomb slot; flash names the
tin ('NEW BALLS: FIRE')
Verified: ladder cycles across 5 deliveries; triple spawns 2 extras and the
fan kills flanked targets; pow-0.1 flame ball clears shambler+shield+brute in
one pass with TRIPLE KILL chaining; GUN tin arms at changeover, blocks taps,
streams pellets (kill in 5 frames), expires back to pretoss; tennis 11/12;
no weapon-flag leaks into tennis; no console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bc26aec1c
commit
06bd24e6ea
6 files changed
+114
-22
No files matched your search
@@ -4,7 +4,7 @@ import { cv, ctx } from './dom.js';
|
||||
import { TUNE, COURT, PLAYER_H, BOY_H, UMP_H, BOUNCE_IDLE, SPRITE_H } from './config.js';
|
||||
import { P, O, ball, score, crowd, G } from './state.js';
|
||||
import { predictLanding } from './gameplay.js';
|
||||
import { zombies, gore, piles, parts, pellets } from './zombies.js';
|
||||
import { zombies, gore, piles, parts, pellets, extras } from './zombies.js';
|
||||
|
||||
// ---------------------------------------------------------------- palette ----
|
||||
// tennis palette -> drained splatter palette; the transition lerps between them
|
||||
@@ -91,7 +91,7 @@ function drawShadow(px, py, rx, ry, alpha = 0.28) {
|
||||
ctx.fillStyle = `rgba(0,0,0,${alpha})`;
|
||||
ctx.beginPath(); ctx.ellipse(px, py, rx, Math.max(1, ry), 0, 0, 7); ctx.fill();
|
||||
}
|
||||
function drawBall(x, y, z, glint = false, bomb = false) {
|
||||
function drawBall(x, y, z, glint = false, bomb = false, flame = false) {
|
||||
const sh = proj(x, 0, z);
|
||||
drawShadow(sh.x, sh.y, Math.max(1.5, sh.s * 0.09), sh.s * 0.04, 0.3);
|
||||
const bp = proj(x, y, z);
|
||||
@@ -101,6 +101,11 @@ function drawBall(x, y, z, glint = false, bomb = false) {
|
||||
if (Math.floor(G.idleT * 20) % 2) { ctx.fillStyle = '#ff8010'; ctx.fillRect(bp.x - 0.5, bp.y - r - 1, Math.max(1, r * 0.5), Math.max(1, r * 0.6)); }
|
||||
return;
|
||||
}
|
||||
if (flame) { // white-hot core in an orange shell, flickering
|
||||
ctx.fillStyle = '#ff8010'; ctx.beginPath(); ctx.arc(bp.x, bp.y, r + (Math.floor(G.idleT * 24) % 2 ? 1 : 0), 0, 7); ctx.fill();
|
||||
ctx.fillStyle = '#ffe8a0'; ctx.beginPath(); ctx.arc(bp.x, bp.y, Math.max(1, r * 0.5), 0, 7); ctx.fill();
|
||||
return;
|
||||
}
|
||||
ctx.fillStyle = '#e8f03a'; ctx.beginPath(); ctx.arc(bp.x, bp.y, r, 0, 7); ctx.fill();
|
||||
if (glint) { ctx.fillStyle = '#f8f8d0'; ctx.fillRect(bp.x - r * 0.3, bp.y - r * 0.5, Math.max(1, r * 0.5), Math.max(1, r * 0.5)); }
|
||||
}
|
||||
@@ -228,7 +233,8 @@ function drawMgRacket() { // horde: the player braces the racket forward and up-
|
||||
ctx.beginPath(); ctx.moveTo(hip.x, hip.y); ctx.lineTo(tip.x, tip.y); ctx.stroke();
|
||||
ctx.strokeStyle = '#b07030'; ctx.lineWidth = 1.5; // the head, aimed forward
|
||||
ctx.beginPath(); ctx.ellipse(tip.x, tip.y, Math.max(2, tip.s * 0.14), Math.max(3, tip.s * 0.22), 0, 0, 7); ctx.stroke();
|
||||
if (G.horde && G.horde.mgT > 1 / TUNE.mgRate - 0.06) { // muzzle flash right after a shot
|
||||
const cool = G.horde ? G.horde.mgT : G.mgCool; // horde or GUN-tin pickup — same racket
|
||||
if (cool > 1 / TUNE.mgRate - 0.06) { // muzzle flash right after a shot
|
||||
ctx.fillStyle = '#fff0a0'; ctx.beginPath(); ctx.arc(tip.x, tip.y, Math.max(2, tip.s * 0.12), 0, 7); ctx.fill();
|
||||
}
|
||||
}
|
||||
@@ -318,12 +324,14 @@ export function render() {
|
||||
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * BOUNCE_IDLE.apex;
|
||||
drawBall(bx, by, sv.z);
|
||||
}
|
||||
// the match ball
|
||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb);
|
||||
// splatter particles
|
||||
// the match ball (and any triple-serve side balls in flight)
|
||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb, ball.flame);
|
||||
for (const e of extras) drawBall(e.x, e.y, e.z);
|
||||
// splatter particles (fire embers ride the same array)
|
||||
for (const pt of parts) {
|
||||
const pp = proj(pt.x, pt.y, pt.z);
|
||||
ctx.fillStyle = pt.dark ? 'rgba(96,14,14,0.9)' : 'rgba(170,22,22,0.9)';
|
||||
ctx.fillStyle = pt.fire ? (pt.t > 0.12 ? '#ffc030' : '#ff8010')
|
||||
: pt.dark ? 'rgba(96,14,14,0.9)' : 'rgba(170,22,22,0.9)';
|
||||
ctx.fillRect(pp.x | 0, pp.y | 0, 2, 2);
|
||||
}
|
||||
// machine-gun pellets: flat tracers with a tiny ground shadow
|
||||
@@ -333,7 +341,7 @@ export function render() {
|
||||
ctx.fillStyle = '#f8e048'; ctx.fillRect((pp.x | 0) - 1, (pp.y | 0) - 1, 2, 2);
|
||||
}
|
||||
if (G.mode !== 'eaten') drawChar(P); // there is no P anymore
|
||||
if (G.horde && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player
|
||||
if ((G.horde || G.mgT > 0) && G.mode === 'splatter') drawMgRacket(); // rambo racket over the player
|
||||
|
||||
ctx.setTransform(RES, 0, 0, RES, 0, 0); // un-shake: the HUD and overlays stay rock steady
|
||||
if (G.boomFlash > 0) { ctx.fillStyle = `rgba(255,255,255,${Math.min(1, G.boomFlash / 0.08) * 0.85})`; ctx.fillRect(0, 0, W, H); }
|
||||
@@ -352,6 +360,18 @@ export function render() {
|
||||
ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.strokeStyle = '#ff8010'; ctx.lineWidth = 0.75; ctx.stroke();
|
||||
}
|
||||
for (let b = 0; b < G.triples; b++) { // triple serves: a yellow pip flanked by two dots
|
||||
ctx.fillStyle = '#e8f03a'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.fillRect(96 + b * 8 - 4, 13, 1.5, 1.5); ctx.fillRect(96 + b * 8 + 3, 13, 1.5, 1.5);
|
||||
}
|
||||
for (let b = 0; b < G.flames; b++) { // flame balls: orange pip, hot core
|
||||
ctx.fillStyle = '#ff8010'; ctx.beginPath(); ctx.arc(96 + b * 8, 14, 2.6, 0, 7); ctx.fill();
|
||||
ctx.fillStyle = '#ffe8a0'; ctx.fillRect(96 + b * 8 - 1, 13, 2, 2);
|
||||
}
|
||||
if (G.mgT > 0) { // machine-gun time: a draining ammo bar
|
||||
ctx.fillStyle = '#333'; ctx.fillRect(96, 11, 34, 5);
|
||||
ctx.fillStyle = '#f8e048'; ctx.fillRect(96, 11, 34 * Math.min(1, G.mgT / TUNE.mgPickupLen), 5);
|
||||
}
|
||||
ctx.fillStyle = score.server === 'O' ? '#f8e048' : '#c8d0dc';
|
||||
ctx.fillText((G.mode === 'tennis' ? 'CPU ' : 'THEM ') + setsTxt(1) + score.games[1] + ' ' + ptTxt(1), 12, 22);
|
||||
ctx.textAlign = 'center';
|
||||
@@ -367,7 +387,7 @@ export function render() {
|
||||
}
|
||||
if (G.eatenT > 0.7) { ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace'; ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 28); }
|
||||
}
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde) {
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0)) {
|
||||
ctx.fillStyle = '#f8e048'; ctx.font = 'bold 8px monospace';
|
||||
const touch = matchMedia('(pointer: coarse)').matches;
|
||||
const txt = G.mode === 'splatter'
|
||||
|
||||
Reference in new issue
Block a user