← back to Neon Baguette Battle
Weapon loadout: Space=poke, Return=throw baguette, R=rocket, B=bomb
e6c507144a080c7b6bf4432fbfd587f846be5208 · 2026-08-31 14:06:11 -0700 · Steve Abrams
- Generalized projectiles into 3 kinds: baguette (missile), rocket (fast+big, costs meter, nose/fins/flame art), bomb (lobbed arc, explodes with splash damage + boom)
- Per-weapon cooldowns for baguette/bomb; rocket gated on the special meter (bar relabeled ROCKET READY)
- P1 keys: Space/F=poke, G=whack, Return/H=throw baguette, R=rocket, B=bomb (+modifier fallbacks). P2: ,/./ / ; '
- AI throws baguettes/bombs at range, rockets when charged; new boom SFX + French voice lines
- Verified each key spawns the correct projectile (0 errors)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit e6c507144a080c7b6bf4432fbfd587f846be5208
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 31 14:06:11 2026 -0700
Weapon loadout: Space=poke, Return=throw baguette, R=rocket, B=bomb
- Generalized projectiles into 3 kinds: baguette (missile), rocket (fast+big, costs meter, nose/fins/flame art), bomb (lobbed arc, explodes with splash damage + boom)
- Per-weapon cooldowns for baguette/bomb; rocket gated on the special meter (bar relabeled ROCKET READY)
- P1 keys: Space/F=poke, G=whack, Return/H=throw baguette, R=rocket, B=bomb (+modifier fallbacks). P2: ,/./ / ; '
- AI throws baguettes/bombs at range, rockets when charged; new boom SFX + French voice lines
- Verified each key spawns the correct projectile (0 errors)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
README.md | 16 ++++---
index.html | 155 ++++++++++++++++++++++++++++++++++++++++++++-----------------
2 files changed, 122 insertions(+), 49 deletions(-)
diff --git a/README.md b/README.md
index 2e3e034..0edc58d 100644
--- a/README.md
+++ b/README.md
@@ -31,9 +31,11 @@ also click a card to choose P1's fighter. `Enter`/`Space` or **START FIGHT** beg
| Move | `A` / `D` | `←` / `→` |
| Jump | `W` | `↑` |
| Block | `S` | `↓` |
-| Poke (fast, weak) | `F` | `,` |
-| Swing (slow, strong, knockback) | `G` | `.` |
-| Crumb Blast (projectile, needs full meter) | `H` | `/` |
+| Poke with baguette (fast) | `Space` / `F` | `,` |
+| Whack (slow, strong) | `G` | `.` |
+| Throw baguette 🥖 (projectile) | `Return` / `H` | `/` |
+| Rocket 🚀 (fast + big, needs full meter) | `R` | `;` |
+| Bomb 💣 (lobbed, explodes) | `B` | `'` |
| Pause | `P` | |
| Mute (audio + voice) | `M` | |
@@ -45,9 +47,11 @@ otherwise it does a whack. (`J`/`K`/`L` also jab/whack/launch.) In 2-player the
arrows belong to Player 2.
## Features
-- **Real combat**: startup/active/recovery attack frames, blocking with chip
- damage, knockback, hit-stun, a build-up special meter, and a **launched
- baguette missile** (spinning projectile) once the meter is full.
+- **Real combat + a weapon loadout**: baguette **poke** and **whack** (melee),
+ plus three throwables — **throw baguette** (spinning missile, short cooldown),
+ **bomb** (lobbed, arcs and explodes with splash damage), and **rocket** (fast,
+ big, costs the full special meter). Startup/active/recovery frames, blocking
+ with chip damage, knockback, hit-stun.
- **CPU opponent** with an approach/attack/block/retreat/blast state machine.
- **Flying food hazards**: pies (cream splat, big damage), french rolls (fast),
and croissants arc in from off-screen and hit either fighter.
diff --git a/index.html b/index.html
index dd0d0d8..cefe7ed 100644
--- a/index.html
+++ b/index.html
@@ -110,9 +110,11 @@
<tr><td>Move</td><td><span class="key">A</span><span class="key">D</span></td></tr>
<tr><td>Jump</td><td><span class="key">W</span></td></tr>
<tr><td>Block</td><td><span class="key">S</span></td></tr>
- <tr><td>Jab (baguette)</td><td><span class="key">F</span></td></tr>
- <tr><td>Whack (baguette)</td><td><span class="key">G</span></td></tr>
- <tr><td>Launch baguette 🚀</td><td><span class="key">H</span></td></tr>
+ <tr><td>Poke (baguette)</td><td><span class="key">Space</span><span class="key">F</span></td></tr>
+ <tr><td>Whack</td><td><span class="key">G</span></td></tr>
+ <tr><td>Throw baguette 🥖</td><td><span class="key">Return</span><span class="key">H</span></td></tr>
+ <tr><td>Rocket 🚀</td><td><span class="key">R</span></td></tr>
+ <tr><td>Bomb 💣</td><td><span class="key">B</span></td></tr>
</table>
</div>
<div class="ctl p2"><h3>🥖 Player 2</h3>
@@ -120,13 +122,15 @@
<tr><td>Move</td><td><span class="key">←</span><span class="key">→</span></td></tr>
<tr><td>Jump</td><td><span class="key">↑</span></td></tr>
<tr><td>Block</td><td><span class="key">↓</span></td></tr>
- <tr><td>Jab (baguette)</td><td><span class="key">,</span></td></tr>
- <tr><td>Whack (baguette)</td><td><span class="key">.</span></td></tr>
- <tr><td>Launch baguette 🚀</td><td><span class="key">/</span></td></tr>
+ <tr><td>Poke (baguette)</td><td><span class="key">,</span></td></tr>
+ <tr><td>Whack</td><td><span class="key">.</span></td></tr>
+ <tr><td>Throw baguette 🥖</td><td><span class="key">/</span></td></tr>
+ <tr><td>Rocket 🚀</td><td><span class="key">;</span></td></tr>
+ <tr><td>Bomb 💣</td><td><span class="key">'</span></td></tr>
</table>
</div>
</div>
- <div class="tiny">1-Player also: <span class="key">←</span><span class="key">→</span> move · <span class="key">↑</span> jump · <span class="key">↓</span> block · <span class="key">Space</span> <span class="key">Return</span> <span class="key">⌘</span> <span class="key">⌥</span> <span class="key">Ctrl</span> <span class="key">⌫</span> fire · <span class="key">P</span> pause · <span class="key">M</span> mute</div>
+ <div class="tiny">1-Player also: <span class="key">←</span><span class="key">→</span> move · <span class="key">↑</span> jump · <span class="key">↓</span> block · <span class="key">Space</span> poke · <span class="key">Return</span> throw · <span class="key">R</span> rocket · <span class="key">B</span> bomb · <span class="key">P</span> pause · <span class="key">M</span> mute</div>
</div>
<!-- CHARACTER SELECT -->
@@ -242,6 +246,7 @@
block: () => tone(240, .1, 'triangle', .14),
hurt: () => { noise(.16,.22); tone(140,.15,'sawtooth',.12,70); },
blast: () => { tone(660,.25,'sawtooth',.16,120); noise(.2,.16); },
+ boom: () => { tone(90,.4,'sawtooth',.2,40); noise(.35,.3); },
jump: () => tone(300,.12,'sine',.1,540),
ko: () => { tone(120,.5,'sawtooth',.2,50); noise(.4,.2); },
bell: () => { tone(880,.5,'sine',.12); tone(1320,.5,'sine',.06); },
@@ -286,6 +291,9 @@
hurt: () => say(rnd(["Oh non!","Mon dieu!","Sacrebleu!","Ze pain!","Aïe!"]), {pitch:1.25, rate:1.05}),
block: () => say(rnd(["Non non non!","Not today!"]), {pitch:1.15}),
blast: () => say(rnd(["Baguette missile! Allez!","Fire ze baguette!","Incoming! Ha ha!"]), {rate:0.95, pitch:1.05, force:true}),
+ baguette: () => say(rnd(["Throw ze baguette!","Catch, mon ami!","Voilà!"]), {pitch:1.1, force:true}),
+ rocket: () => say(rnd(["Rocket! Allez!","Boom!","Fire ze rocket!"]), {rate:0.95, force:true}),
+ bomb: () => say(rnd(["Bombe!","Boom boom!","Ha! Ze bomb!"]), {pitch:1.15, force:true}),
round: () => say("Allez!", {force:true, pitch:1.1}),
fight: () => say("Fight, mon ami!", {force:true, pitch:1.15}),
ko: () => say("Oh la la! Magnifique!", {force:true, pitch:1.05}),
@@ -299,16 +307,19 @@
// ---------- Input ----------
const keys = {};
- // maps are arrays so an action can have several keys. 'fire' = launch missile if meter full, else whack.
+ // maps are arrays so an action can have several keys.
+ // poke = baguette jab · swing = whack · baguette = throw baguette · rocket = launch rocket (needs meter) · bomb = lob bomb
const P1_ATTACK = {
- poke: ['f','j'],
- swing: ['g','k'],
- blast: ['h','l'],
- fire: [' ','enter','backspace','delete','meta','control','alt','shift'] // Space/Return/Delete/⌘/Ctrl/⌥/⇧
+ poke: ['f','j',' ','shift'], // Space = poke with baguette
+ swing: ['g','k'], // whack
+ baguette: ['h','enter','backspace','delete'], // Return = throw baguette
+ rocket: ['r','meta','control'], // R = rocket
+ bomb: ['b','alt'], // B = bomb
};
const P1MAP_1P = { left:['a','arrowleft'], right:['d','arrowright'], jump:['w','arrowup'], block:['s','arrowdown'], ...P1_ATTACK };
const P1MAP_2P = { left:['a'], right:['d'], jump:['w'], block:['s'], ...P1_ATTACK }; // arrows belong to P2 in 2P
- const P2MAP = { left:['arrowleft'], right:['arrowright'], jump:['arrowup'], block:['arrowdown'], poke:[','], swing:['.'], blast:['/'] };
+ const P2MAP = { left:['arrowleft'], right:['arrowright'], jump:['arrowup'], block:['arrowdown'],
+ poke:[','], swing:['.'], baguette:['/'], rocket:[';'], bomb:["'"] };
let p1map = P1MAP_1P;
addEventListener('keydown', e => {
const k = e.key.toLowerCase();
@@ -375,6 +386,7 @@
this.animT = Math.random()*10;
this.aiTimer = 0; this.aiIntent = 'idle';
this.armSwing = 0; // visual arm extension 0..1
+ this.cd = { baguette:0, bomb:0 }; // per-weapon cooldowns (rocket uses the meter)
}
get top(){ return this.y - this.h; }
get cx(){ return this.x; }
@@ -395,12 +407,15 @@
startAttack(type){
if (this.attack || this.state==='hurt' || this.state==='ko' || !this.onGround) return;
- if (type === 'blast'){
- if (this.meter < 100) return;
- this.meter = 0;
- spawnBlast(this);
- Audio.blast(); Voice.blast();
- this.attack = { type:'blast', phase:'recover', t:0, recover:16 };
+ const THROW = { baguette:14, rocket:16, bomb:15 }; // recover frames per thrown weapon
+ if (type in THROW){
+ if (type === 'rocket'){ if (this.meter < 100) return; this.meter = 0; } // rocket costs the meter
+ else { if (this.cd[type] > 0) return; this.cd[type] = (type==='baguette' ? 48 : 90); } // cooldown
+ spawnProjectile(this, type);
+ if (type==='bomb'){ Audio.swing(); Voice.bomb(); }
+ else if (type==='rocket'){ Audio.blast(); Voice.rocket(); }
+ else { Audio.blast(); Voice.baguette(); }
+ this.attack = { type, phase:'recover', t:0, recover:THROW[type] };
this.state = 'poke'; this.stateTime = 0; this.armSwing = 1;
return;
}
@@ -453,10 +468,11 @@
if (down(map.block) && this.onGround && !this.attack){ this.state = 'block'; }
else if (this.state==='block'){ this.state = 'idle'; }
if (hit(map.jump) && this.onGround && !this.attack){ this.vy = -16; this.onGround = false; Audio.jump(); }
- if (hit(map.poke)) this.startAttack('poke');
- if (hit(map.swing)) this.startAttack('swing');
- if (hit(map.blast)) this.startAttack('blast');
- if (hit(map.fire)) this.startAttack(this.meter >= 100 ? 'blast' : 'swing'); // Space / ⌘ / Ctrl
+ if (hit(map.poke)) this.startAttack('poke');
+ if (hit(map.swing)) this.startAttack('swing');
+ if (hit(map.baguette)) this.startAttack('baguette');
+ if (hit(map.rocket)) this.startAttack('rocket');
+ if (hit(map.bomb)) this.startAttack('bomb');
}
controlAI(foe){
@@ -498,10 +514,11 @@
break;
case 'attack':
if (adist < 100) this.startAttack(Math.random()<0.55?'poke':'swing');
+ else if (adist < 300 && Math.random()<0.4) this.startAttack(Math.random()<0.6?'baguette':'bomb');
else if (!this.attack) this.vx = this.facing * speed;
break;
case 'blast':
- if (this.meter>=100) this.startAttack('blast');
+ if (this.meter>=100) this.startAttack('rocket'); else this.startAttack('baguette');
this.aiIntent='approach'; break;
}
}
@@ -509,11 +526,13 @@
update(foe){
this.animT += 0.16;
if (this.invuln>0) this.invuln--;
+ if (this.cd.baguette>0) this.cd.baguette--;
+ if (this.cd.bomb>0) this.cd.bomb--;
// attack phase machine
if (this.attack){
this.attack.t++;
const a = this.attack;
- if (a.type==='blast'){
+ if (a.type==='baguette' || a.type==='rocket' || a.type==='bomb'){
if (a.t >= a.recover){ this.attack=null; if(this.state!=='hurt') this.state='idle'; }
} else {
if (a.phase==='startup' && a.t>=a.startup){ a.phase='active'; a.t=0; }
@@ -624,7 +643,7 @@
if (this.state==='swing'){ // overhead whack that sweeps down
bang = -2.1 + armExt*2.7;
hx = sx + 6 + Math.cos(bang)*6; hy = sy + 2 + Math.sin(bang)*6;
- } else if (this.state==='poke' || (this.attack&&this.attack.type==='blast')){
+ } else if (this.state==='poke' || (this.attack && (this.attack.type==='baguette'||this.attack.type==='rocket'||this.attack.type==='bomb'))){
hx = sx + 12 + armExt*26; hy = sy + 8; bang = -0.12; // thrust / throw
} else if (this.state==='block'){
hx = sx - 2; hy = sy + 4; bang = -1.45; // raised guard
@@ -633,7 +652,7 @@
}
g.strokeStyle=shirt; g.lineWidth=9; g.lineCap='round';
g.beginPath(); g.moveTo(sx, sy); g.lineTo(hx, hy); g.stroke();
- const hideBag = this.attack && this.attack.type==='blast'; // baguette is airborne as a missile
+ const hideBag = this.attack && this.attack.type==='baguette'; // baguette left the hand as a missile
if (!hideBag) drawBaguetteWeapon(g, hx, hy, bang, 46, true);
g.fillStyle=skin; g.beginPath(); g.arc(hx, hy, 5, 0, Math.PI*2); g.fill();
@@ -703,6 +722,28 @@
g.restore();
}
+ // rocket drawn pointing +x (caller flips by facing)
+ function drawRocket(g, r){
+ g.fillStyle='#e6e6ee'; roundRectPath(g, -r, -8, r*1.7, 16, 6); g.fill();
+ g.lineWidth=1.5; g.strokeStyle='rgba(0,0,0,.25)'; roundRectPath(g, -r, -8, r*1.7, 16, 6); g.stroke();
+ g.fillStyle='#ff4d4d'; g.beginPath(); g.moveTo(r*0.7,-8); g.lineTo(r*0.7+18,0); g.lineTo(r*0.7,8); g.closePath(); g.fill(); // nose
+ g.fillStyle='#4d9dff';
+ g.beginPath(); g.moveTo(-r,-8); g.lineTo(-r-9,-15); g.lineTo(-r+7,-8); g.closePath(); g.fill(); // fins
+ g.beginPath(); g.moveTo(-r, 8); g.lineTo(-r-9, 15); g.lineTo(-r+7, 8); g.closePath(); g.fill();
+ g.fillStyle='#22e0ff'; g.beginPath(); g.arc(r*0.05,0,4,0,Math.PI*2); g.fill(); // window
+ }
+ // bomb drawn spinning; t drives the fuse-spark flicker
+ function drawBomb(g, r, t){
+ g.shadowColor='rgba(0,0,0,.5)'; g.shadowBlur=6;
+ g.fillStyle='#141414'; g.beginPath(); g.arc(0,0,r*0.72,0,Math.PI*2); g.fill();
+ g.shadowBlur=0;
+ g.fillStyle='rgba(255,255,255,.18)'; g.beginPath(); g.arc(-r*0.22,-r*0.22,r*0.22,0,Math.PI*2); g.fill();
+ g.fillStyle='#5a4a2a'; g.fillRect(-3,-r*0.82,6,5); // fuse cap
+ g.strokeStyle='#8a5a2a'; g.lineWidth=2; g.beginPath(); g.moveTo(0,-r*0.82); g.quadraticCurveTo(7,-r-2,12,-r*0.72); g.stroke();
+ g.fillStyle=(Math.floor(t/3)%2)?'#ffd166':'#ff6a3d'; g.shadowColor='#ffb347'; g.shadowBlur=8;
+ g.beginPath(); g.arc(12,-r*0.72,3.2,0,Math.PI*2); g.fill(); g.shadowBlur=0;
+ }
+
function roundRectPath(g,x,y,w,h,r){
r = Math.max(0, Math.min(r, w/2, h/2)); // never negative (thin cobble rows)
g.beginPath();
@@ -716,9 +757,15 @@
// ---------- Projectiles & particles ----------
let blasts = [], sparks = [], hazards = [], hazardTimer = 140, shake = 0;
- function spawnBlast(owner){
- blasts.push({ x: owner.x + owner.facing*owner.w*0.5, y: owner.y - owner.h*0.62,
- vx: owner.facing*19, r: 34, owner, life: 90, t:0, rot: 0 }); // fast + big
+ function spawnProjectile(owner, kind){
+ const f = owner.facing, base = { owner, kind, t:0, rot:0, hit:false };
+ if (kind === 'rocket'){
+ blasts.push({ ...base, x: owner.x + f*owner.w*0.5, y: owner.y - owner.h*0.55, vx: f*26, vy:0, grav:0, r:30, dmg:22, kb:18, life:80 });
+ } else if (kind === 'bomb'){
+ blasts.push({ ...base, x: owner.x + f*owner.w*0.4, y: owner.y - owner.h*0.9, vx: f*9, vy:-11, grav:0.45, spin:f*0.3, r:20, dmg:14, kb:14, life:220 });
+ } else { // baguette missile (fast + big)
+ blasts.push({ ...base, x: owner.x + f*owner.w*0.5, y: owner.y - owner.h*0.62, vx: f*19, vy:0, grav:0, r:34, dmg:16, kb:16, life:90 });
+ }
}
function spawnSpark(x,y,color,n){
for (let i=0;i<n;i++){
@@ -728,29 +775,51 @@
}
function updateProjectiles(f1,f2){
for (const b of blasts){
- b.t++; b.x += b.vx; b.rot += 0.7*Math.sign(b.vx); b.life--;
+ b.t++; b.x += b.vx;
+ if (b.grav){ b.vy += b.grav; b.y += b.vy; }
+ b.rot += (b.spin != null ? b.spin : 0.7*Math.sign(b.vx));
+ b.life--;
const foe = b.owner === f1 ? f2 : f1;
const hb = foe.hurtbox();
- if (b.x+b.r > hb.x && b.x-b.r < hb.x+hb.w && b.y+b.r>hb.y && b.y-b.r<hb.y+hb.h){
- if (foe.takeHit(18 * b.owner.powF, 16, Math.sign(b.vx))) { b.life = 0; spawnSpark(b.x,b.y,'#ffb347',18); }
+ const overlap = (b.x+b.r > hb.x && b.x-b.r < hb.x+hb.w && b.y+b.r>hb.y && b.y-b.r<hb.y+hb.h);
+ if (b.kind === 'bomb'){
+ if (overlap || b.y >= GROUND-1 || b.life<=0){ explodeBomb(b, foe); b.life=0; }
+ } else {
+ if (overlap){ if (foe.takeHit(b.dmg * b.owner.powF, b.kb, Math.sign(b.vx))){ b.life=0; spawnSpark(b.x,b.y,'#ffb347',18); } }
+ if (b.x < ARENA_L-60 || b.x > ARENA_R+60) b.life = 0;
}
- if (b.x < ARENA_L-40 || b.x > ARENA_R+40) b.life = 0;
}
blasts = blasts.filter(b => b.life>0);
for (const s of sparks){ s.x+=s.vx; s.y+=s.vy; s.vy+=0.25; s.life--; }
sparks = sparks.filter(s => s.life>0);
}
+ function explodeBomb(b, foe){
+ spawnSpark(b.x, b.y, '#ffd166', 26); spawnSpark(b.x, b.y, '#ff6a3d', 18); spawnSpark(b.x, b.y, '#ffffff', 8);
+ shake = Math.min(22, shake + 11); Audio.boom();
+ const d = Math.hypot(foe.x - b.x, (foe.y - foe.h*0.5) - b.y);
+ if (d < 96) foe.takeHit(b.dmg * b.owner.powF * (1 - d/150), 15, Math.sign(foe.x - b.x || b.vx));
+ }
function drawProjectiles(g){
for (const b of blasts){
const c = b.owner.colors;
g.save();
- // motion trail
- g.fillStyle = c.glow;
- for (let i=1;i<=6;i++){ g.globalAlpha = 0.30/i; g.beginPath(); g.arc(b.x-b.vx*i*0.9, b.y, 12-i, 0, Math.PI*2); g.fill(); }
- // the big spinning baguette missile
- g.globalAlpha = 1;
- g.translate(b.x, b.y); g.rotate(b.rot); g.scale(1.5, 1.5);
- drawBaguetteWeapon(g, -33, 0, 0, 66, true); // centered on flight path
+ if (b.kind === 'rocket'){
+ g.fillStyle = '#ff9a3d';
+ for (let i=1;i<=6;i++){ g.globalAlpha=0.32/i; g.beginPath(); g.arc(b.x-b.vx*i*0.7, b.y, 11-i, 0, Math.PI*2); g.fill(); }
+ g.globalAlpha=1; g.translate(b.x, b.y); g.scale(Math.sign(b.vx), 1);
+ drawRocket(g, b.r);
+ } else if (b.kind === 'bomb'){
+ g.translate(b.x, b.y);
+ g.fillStyle='rgba(130,130,140,.4)';
+ for (let i=1;i<=4;i++){ g.globalAlpha=0.32/i; g.beginPath(); g.arc(-b.vx*i*0.6, -b.vy*i*0.3, 7-i, 0, Math.PI*2); g.fill(); }
+ g.globalAlpha=1; g.rotate(b.rot);
+ drawBomb(g, b.r, b.t);
+ } else { // baguette missile
+ g.fillStyle = c.glow;
+ for (let i=1;i<=6;i++){ g.globalAlpha=0.30/i; g.beginPath(); g.arc(b.x-b.vx*i*0.9, b.y, 12-i, 0, Math.PI*2); g.fill(); }
+ g.globalAlpha=1; g.translate(b.x, b.y); g.rotate(b.rot); g.scale(1.5, 1.5);
+ drawBaguetteWeapon(g, -33, 0, 0, 66, true);
+ }
g.restore();
}
for (const s of sparks){
@@ -1022,7 +1091,7 @@
if (val>=100){ g.shadowColor=c.glow; g.shadowBlur=10; }
g.fillRect(bx,y,bw,h); g.shadowBlur=0;
if (val>=100){ g.fillStyle=c.accent; g.font='700 9px Georgia'; g.textAlign=flip?'right':'left';
- g.fillText('BAGUETTE MISSILE READY', flip?x+w:x, y+18); g.textAlign='left'; }
+ g.fillText('ROCKET READY', flip?x+w:x, y+18); g.textAlign='left'; }
}
function drawPips(g,x,y,n,flip){
for (let i=0;i<2;i++){
@@ -1304,7 +1373,7 @@
buildRoster();
// tiny test hook (only when the URL ends in #debug) — no effect on normal play
- if (location.hash === '#debug') window.__bb = { get f1(){ return f1; }, get f2(){ return f2; }, get state(){ return state; } };
+ if (location.hash === '#debug') window.__bb = { get f1(){ return f1; }, get f2(){ return f2; }, get state(){ return state; }, get blasts(){ return blasts; } };
// La Marseillaise on load — try immediately; browsers gate autoplay, so also fire on the first interaction
Audio.startAnthem();
const kickAnthem = () => Audio.startAnthem();
← a39d05a Broaden P1 controls: arrows move; Space/Return/Cmd/Option/Ct
·
back to Neon Baguette Battle
·
Apply graphic-designer review fixes #1-5 5e37c3a →