[object Object]

← back to Games Agentabrams

Refine Baguette Brawl: combo system + damage scaling, impact juice, dizzy stars, smarter AI (re-inlined build)

4eb380db0ca07a032a235e3b963fb9458f4431c2 · 2026-08-31 13:16:50 -0700 · Steve Abrams

Files touched

Diff

commit 4eb380db0ca07a032a235e3b963fb9458f4431c2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 13:16:50 2026 -0700

    Refine Baguette Brawl: combo system + damage scaling, impact juice, dizzy stars, smarter AI (re-inlined build)
---
 games/baguette-brawl/BaguetteBrawl.aa | 81 +++++++++++++++++++++++++++++------
 games/baguette-brawl/index.html       | 81 +++++++++++++++++++++++++++++------
 2 files changed, 134 insertions(+), 28 deletions(-)

diff --git a/games/baguette-brawl/BaguetteBrawl.aa b/games/baguette-brawl/BaguetteBrawl.aa
index e8ac114..2ef86e1 100644
--- a/games/baguette-brawl/BaguetteBrawl.aa
+++ b/games/baguette-brawl/BaguetteBrawl.aa
@@ -310,13 +310,14 @@ function makeFighter(x, facing, cfg){
     swing:0, walkPhase:0,
     color:cfg.color, beret:cfg.beret, name:cfg.name,
     isAI:cfg.isAI||false, ctrl:cfg.ctrl,
-    hitFlash:0, wins:0, _t:0, _think:0
+    hitFlash:0, wins:0, _t:0, _think:0, combo:0, comboT:0
   };
 }
 
 let f1, f2, mode=1, difficulty='normal', state='menu';
 let roundNum=1, roundTime=60, timeLeft=60, roundPhase='intro';
 let freeze=0, shake=0, particles=[], clouds=[], stars=[], koTimer=null;
+let flash=0, flashCol='#ffffff', zoom=0, comboShow={n:0,x:W/2,t:0};
 let showAnnounce='', announceT=0, countdownT=0, lastCount=99;
 
 const AI_TUNE = {
@@ -342,14 +343,14 @@ function startGame(m){
 function resetFighter(f,x,facing){
   Object.assign(f,{x,y:GROUND,vx:0,vy:0,hp:100,meter:f.meter*0.5|0,
     state:'idle',atk:null,atkT:0,atkDur:0,atkHit:false,hurtT:0,blockT:0,
-    blocking:false,swing:0,facing,hitFlash:0,onGround:true});
+    blocking:false,swing:0,facing,hitFlash:0,onGround:true,combo:0,comboT:0});
 }
 function startRound(){
   clearTimeout(koTimer); koTimer=null;
   resEl.classList.add('hide'); menuEl.classList.add('hide'); pauseEl.classList.add('hide');
   resetFighter(f1,300,1);
   resetFighter(f2,660,-1);
-  timeLeft=roundTime; particles=[]; freeze=0; shake=0;
+  timeLeft=roundTime; particles=[]; freeze=0; shake=0; flash=0; zoom=0; comboShow={n:0,x:W/2,t:0};
   showAnnounce=`MANCHE ${roundNum}`; announceT=1.6; countdownT=3.2; lastCount=99;
   roundPhase='intro';
   state='fight';
@@ -443,6 +444,7 @@ function updateFighter(f, other, dt){
   if(f.state==='hurt'){ f.hurtT-=dt; if(f.hurtT<=0) f.state='idle'; }
   if(f.hitFlash>0) f.hitFlash-=dt;
   if(f.blockT>0 && !f.blocking) f.blockT-=dt*.5;
+  if(f.comboT>0){ f.comboT-=dt; if(f.comboT<=0) f.combo=0; }   // combo window lapses → reset
 }
 
 function swingCurve(p){
@@ -461,23 +463,34 @@ function landHit(attacker, target, a){
   if(blocked){
     dmg*=0.18; kb*=.5;
     addMeter(target,4);                    // reward defense a little
+    attacker.combo=0; attacker.comboT=0;   // string was guarded — pressure resets
     sfx.block();
     spawnParticles(target.x+target.facing*-20, target.y-70, 6, '#ffe7b0', 3);
     shake=Math.max(shake,4); target.blockT=.2;
   } else {
+    // combo scaling — each extra hit in a string does progressively less (curbs juggles)
+    attacker.combo++; attacker.comboT=1.1;
+    const scale = attacker.combo<=1 ? 1 : Math.max(0.5, 1-(attacker.combo-1)*0.12);
+    dmg = Math.round(dmg*scale);
+    if(attacker.combo>=2){ comboShow={n:attacker.combo, x:target.x, t:0.9}; }
     a.unblock ? sfx.crush() : sfx.hit();
     addMeter(target, a.unblock?0:6);
     target.state='hurt'; target.hurtT=a.stun;
     spawnParticles(target.x, target.y-70, a.unblock?26:16, attacker.color, a.unblock?9:6);
-    spawnParticles(target.x, target.y-80, 8, '#fff3dd', 5);
+    spawnBurst(target.x, target.y-72, a.unblock?12:7, '#fff3dd', a.unblock?11:8, attacker.facing);
     target.hitFlash=.18;
     shake=Math.max(shake, a.dmg>10?11:6);
     freeze=a.unblock?.11:(a.dmg>10?.07:.035);
+    if(a.unblock){ flash=Math.max(flash,.55); flashCol='#ffe7a0'; zoom=Math.max(zoom,.06); }
+    else if(a.dmg>10){ zoom=Math.max(zoom,.035); }
   }
   target.hp=Math.max(0,target.hp-dmg);
   target.vx = attacker.facing*kb;
   if(!blocked && a.dmg>10){ target.vy=-4-(a.unblock?3:0); target.onGround=false; }
-  if(target.hp<=0){ target.state='ko'; target.vx=attacker.facing*5; target.vy=-8; target.onGround=false; sfx.ko(); }
+  if(target.hp<=0){
+    target.state='ko'; target.vx=attacker.facing*5; target.vy=-8; target.onGround=false;
+    flash=Math.max(flash,.7); flashCol='#ffffff'; zoom=Math.max(zoom,.08); sfx.ko();
+  }
 }
 
 // ---------------------------------------------------------------------------
@@ -492,15 +505,22 @@ function aiControl(f, o, dt){
   let mv=0;
 
   if(f.state==='attack'||f.state==='hurt'){ f.vx*=.85; return; }
+  const nearWall = f.x<130 || f.x>W-130;
+
+  // the Crust Crusher is UNBLOCKABLE — don't guard it, RETREAT out of its reach
+  if(o.state==='attack' && o.atk==='super' && dist<170){
+    if(f.onGround && Math.random()<T.blockCh) { f.vy=-12.5; f.onGround=false; }  // hop away
+    f.vx=-dir*T.speed; return;
+  }
 
-  // anti-air: opponent airborne & close → block or heavy up-swing
-  if(!o.onGround && dist<120 && Math.random()<T.blockCh*0.5){ f.blocking=true; f.vx=0; return; }
+  // anti-air: opponent airborne & closing → guard
+  if(!o.onGround && o.vy>-2 && dist<120 && Math.random()<T.blockCh*0.6){ f.blocking=true; f.vx=0; return; }
 
-  // block when opponent commits an attack in range
-  if(o.state==='attack' && dist<130 && Math.random()<T.blockCh*0.18){ f.blocking=true; f.vx=0; return; }
+  // block a committed grounded heavy/jab in range (per-frame prob → reliable over startup)
+  if(o.state==='attack' && o.atk!=='super' && dist<130 && Math.random()<T.blockCh*0.22){ f.blocking=true; f.vx=0; return; }
 
-  // fire super when charged & in range
-  if(f.meter>=100 && dist<110 && Math.random()<T.superCh*0.1){ tryAttack(f,'super'); return; }
+  // fire super decisively: opponent stunned, or simply in range (scaled by difficulty)
+  if(f.meter>=100 && dist<115 && (o.state==='hurt' || Math.random()<T.superCh*0.14)){ tryAttack(f,'super'); return; }
 
   const ideal=90;
   if(dist>ideal+30) mv=dir;
@@ -519,7 +539,9 @@ function aiControl(f, o, dt){
   // punish a whiffed opponent attack: dash in
   if(o.state==='attack' && o.atkHit===false && dist<160 && dist>90 && Math.random()<T.whiff*0.1){ mv=dir; }
 
-  if(f.onGround && Math.random()<T.jumpCh && dist<180){ f.vy=-13; f.onGround=false; }
+  // cornered & pressured → hop over instead of walking into the wall
+  if(nearWall && dist<150 && f.onGround && Math.random()<0.04){ f.vy=-13; f.onGround=false; mv=dir; }
+  else if(f.onGround && Math.random()<T.jumpCh && dist<180){ f.vy=-13; f.onGround=false; }
   f.vx=mv*T.speed;
 }
 
@@ -532,6 +554,13 @@ function spawnParticles(x,y,n,color,spd){
     particles.push({x,y,vx:Math.cos(ang)*s,vy:Math.sin(ang)*s-2,life:1,color,r:Math.random()*3+1.5});
   }
 }
+// directional spark cone — sprays in the knockback direction (dir = attacker.facing)
+function spawnBurst(x,y,n,color,spd,dir){
+  for(let i=0;i<n;i++){
+    const ang=(-0.5+Math.random())*0.9, s=Math.random()*spd+2;
+    particles.push({x,y,vx:dir*Math.cos(ang)*s,vy:Math.sin(ang)*s-1.5,life:1,color,r:Math.random()*2.5+1.2});
+  }
+}
 function updateParticles(dt){
   for(let i=particles.length-1;i>=0;i--){
     const p=particles[i];
@@ -626,9 +655,10 @@ function drawScene(){
     ctx.strokeStyle='rgba(0,0,0,.18)'; ctx.stroke(); }
 }
 function draw(){
-  drawScene();
   ctx.save();
+  if(zoom>0){ const z=1+zoom; ctx.translate(W/2,H/2); ctx.scale(z,z); ctx.translate(-W/2,-H/2); }  // impact punch-in
   if(shake>0){ ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake); }
+  drawScene();
   if(f1&&f2){
     drawShadow(f1); drawShadow(f2);
     // draw the fighter that's further back (higher on screen / smaller x-depth) first
@@ -639,10 +669,22 @@ function draw(){
   ctx.globalAlpha=1;
   ctx.restore();
 
+  if(flash>0){ ctx.save(); ctx.globalAlpha=Math.min(.6,flash); ctx.fillStyle=flashCol; ctx.fillRect(0,0,W,H); ctx.restore(); }
   if(f1&&f2) drawHUD();
+  if(comboShow.t>0) drawCombo();
   if(announceT>0) drawAnnounce();
   if(countdownT>0 && countdownT<3.2 && roundPhase==='intro') drawCountdown();
 }
+function drawCombo(){
+  ctx.save();
+  ctx.globalAlpha=Math.min(1,comboShow.t*2);
+  const rise=(0.9-comboShow.t)*26;
+  ctx.textAlign='center'; ctx.textBaseline='middle';
+  ctx.fillStyle='#ffe7a0'; ctx.font="italic 800 30px Georgia,serif";
+  ctx.shadowColor='rgba(0,0,0,.6)'; ctx.shadowBlur=10;
+  ctx.fillText(`COMBO ×${comboShow.n}`, Math.max(90,Math.min(W-90,comboShow.x)), 150-rise);
+  ctx.restore();
+}
 
 function drawShadow(f){
   const sc=f.onGround?1:0.6;
@@ -727,10 +769,17 @@ function drawFighter(f){
     ctx.beginPath(); ctx.arc(21,eyY-1,1.1,0,7); ctx.fill();
   }
   ctx.strokeStyle='#5c3a1a'; ctx.lineWidth=2; ctx.beginPath();
-  if(f.state==='attack'){ ctx.arc(14,eyY+13,4,0,Math.PI); }
+  if(f.state==='hurt'){ ctx.arc(14,eyY+13,3.2,Math.PI,2*Math.PI); }   // "ow" mouth
+  else if(f.state==='attack'){ ctx.arc(14,eyY+13,4,0,Math.PI); }
   else { ctx.moveTo(9,eyY+12); ctx.quadraticCurveTo(14,eyY+15,20,eyY+12); }
   ctx.stroke();
 
+  // dizzy stars circling the head while stunned
+  if(f.state==='hurt'){
+    const t=performance.now()/220; ctx.fillStyle='#ffe7a0';
+    for(let i=0;i<3;i++){ const aa=t+i*2.094; spark(14+Math.cos(aa)*16, -bh-6+Math.sin(aa)*5, 3.4); }
+  }
+
   // arm + weapon baguette
   ctx.save();
   ctx.translate(16,-bh/2-4);
@@ -763,6 +812,7 @@ function drawFighter(f){
   ctx.restore();
 }
 function xeye(x,y){ ctx.beginPath(); ctx.moveTo(x-3,y-3); ctx.lineTo(x+3,y+3); ctx.moveTo(x+3,y-3); ctx.lineTo(x-3,y+3); ctx.stroke(); }
+function spark(x,y,r){ ctx.beginPath(); ctx.moveTo(x,y-r); ctx.lineTo(x+r*.3,y-r*.3); ctx.lineTo(x+r,y); ctx.lineTo(x+r*.3,y+r*.3); ctx.lineTo(x,y+r); ctx.lineTo(x-r*.3,y+r*.3); ctx.lineTo(x-r,y); ctx.lineTo(x-r*.3,y-r*.3); ctx.closePath(); ctx.fill(); }
 function roundedLoaf(cx,topY,w,h){
   const r=w/2; ctx.beginPath();
   ctx.moveTo(cx-r,topY+r); ctx.arc(cx,topY+r,r,Math.PI,0);
@@ -878,6 +928,9 @@ function loop(now){
       }
       updateParticles(dt);
       if(shake>0) shake=Math.max(0,shake-dt*40);
+      if(flash>0) flash=Math.max(0,flash-dt*2.2);
+      if(zoom>0)  zoom =Math.max(0,zoom -dt*0.34);
+      if(comboShow.t>0) comboShow.t-=dt;
     }
   }
 
diff --git a/games/baguette-brawl/index.html b/games/baguette-brawl/index.html
index e8ac114..2ef86e1 100644
--- a/games/baguette-brawl/index.html
+++ b/games/baguette-brawl/index.html
@@ -310,13 +310,14 @@ function makeFighter(x, facing, cfg){
     swing:0, walkPhase:0,
     color:cfg.color, beret:cfg.beret, name:cfg.name,
     isAI:cfg.isAI||false, ctrl:cfg.ctrl,
-    hitFlash:0, wins:0, _t:0, _think:0
+    hitFlash:0, wins:0, _t:0, _think:0, combo:0, comboT:0
   };
 }
 
 let f1, f2, mode=1, difficulty='normal', state='menu';
 let roundNum=1, roundTime=60, timeLeft=60, roundPhase='intro';
 let freeze=0, shake=0, particles=[], clouds=[], stars=[], koTimer=null;
+let flash=0, flashCol='#ffffff', zoom=0, comboShow={n:0,x:W/2,t:0};
 let showAnnounce='', announceT=0, countdownT=0, lastCount=99;
 
 const AI_TUNE = {
@@ -342,14 +343,14 @@ function startGame(m){
 function resetFighter(f,x,facing){
   Object.assign(f,{x,y:GROUND,vx:0,vy:0,hp:100,meter:f.meter*0.5|0,
     state:'idle',atk:null,atkT:0,atkDur:0,atkHit:false,hurtT:0,blockT:0,
-    blocking:false,swing:0,facing,hitFlash:0,onGround:true});
+    blocking:false,swing:0,facing,hitFlash:0,onGround:true,combo:0,comboT:0});
 }
 function startRound(){
   clearTimeout(koTimer); koTimer=null;
   resEl.classList.add('hide'); menuEl.classList.add('hide'); pauseEl.classList.add('hide');
   resetFighter(f1,300,1);
   resetFighter(f2,660,-1);
-  timeLeft=roundTime; particles=[]; freeze=0; shake=0;
+  timeLeft=roundTime; particles=[]; freeze=0; shake=0; flash=0; zoom=0; comboShow={n:0,x:W/2,t:0};
   showAnnounce=`MANCHE ${roundNum}`; announceT=1.6; countdownT=3.2; lastCount=99;
   roundPhase='intro';
   state='fight';
@@ -443,6 +444,7 @@ function updateFighter(f, other, dt){
   if(f.state==='hurt'){ f.hurtT-=dt; if(f.hurtT<=0) f.state='idle'; }
   if(f.hitFlash>0) f.hitFlash-=dt;
   if(f.blockT>0 && !f.blocking) f.blockT-=dt*.5;
+  if(f.comboT>0){ f.comboT-=dt; if(f.comboT<=0) f.combo=0; }   // combo window lapses → reset
 }
 
 function swingCurve(p){
@@ -461,23 +463,34 @@ function landHit(attacker, target, a){
   if(blocked){
     dmg*=0.18; kb*=.5;
     addMeter(target,4);                    // reward defense a little
+    attacker.combo=0; attacker.comboT=0;   // string was guarded — pressure resets
     sfx.block();
     spawnParticles(target.x+target.facing*-20, target.y-70, 6, '#ffe7b0', 3);
     shake=Math.max(shake,4); target.blockT=.2;
   } else {
+    // combo scaling — each extra hit in a string does progressively less (curbs juggles)
+    attacker.combo++; attacker.comboT=1.1;
+    const scale = attacker.combo<=1 ? 1 : Math.max(0.5, 1-(attacker.combo-1)*0.12);
+    dmg = Math.round(dmg*scale);
+    if(attacker.combo>=2){ comboShow={n:attacker.combo, x:target.x, t:0.9}; }
     a.unblock ? sfx.crush() : sfx.hit();
     addMeter(target, a.unblock?0:6);
     target.state='hurt'; target.hurtT=a.stun;
     spawnParticles(target.x, target.y-70, a.unblock?26:16, attacker.color, a.unblock?9:6);
-    spawnParticles(target.x, target.y-80, 8, '#fff3dd', 5);
+    spawnBurst(target.x, target.y-72, a.unblock?12:7, '#fff3dd', a.unblock?11:8, attacker.facing);
     target.hitFlash=.18;
     shake=Math.max(shake, a.dmg>10?11:6);
     freeze=a.unblock?.11:(a.dmg>10?.07:.035);
+    if(a.unblock){ flash=Math.max(flash,.55); flashCol='#ffe7a0'; zoom=Math.max(zoom,.06); }
+    else if(a.dmg>10){ zoom=Math.max(zoom,.035); }
   }
   target.hp=Math.max(0,target.hp-dmg);
   target.vx = attacker.facing*kb;
   if(!blocked && a.dmg>10){ target.vy=-4-(a.unblock?3:0); target.onGround=false; }
-  if(target.hp<=0){ target.state='ko'; target.vx=attacker.facing*5; target.vy=-8; target.onGround=false; sfx.ko(); }
+  if(target.hp<=0){
+    target.state='ko'; target.vx=attacker.facing*5; target.vy=-8; target.onGround=false;
+    flash=Math.max(flash,.7); flashCol='#ffffff'; zoom=Math.max(zoom,.08); sfx.ko();
+  }
 }
 
 // ---------------------------------------------------------------------------
@@ -492,15 +505,22 @@ function aiControl(f, o, dt){
   let mv=0;
 
   if(f.state==='attack'||f.state==='hurt'){ f.vx*=.85; return; }
+  const nearWall = f.x<130 || f.x>W-130;
+
+  // the Crust Crusher is UNBLOCKABLE — don't guard it, RETREAT out of its reach
+  if(o.state==='attack' && o.atk==='super' && dist<170){
+    if(f.onGround && Math.random()<T.blockCh) { f.vy=-12.5; f.onGround=false; }  // hop away
+    f.vx=-dir*T.speed; return;
+  }
 
-  // anti-air: opponent airborne & close → block or heavy up-swing
-  if(!o.onGround && dist<120 && Math.random()<T.blockCh*0.5){ f.blocking=true; f.vx=0; return; }
+  // anti-air: opponent airborne & closing → guard
+  if(!o.onGround && o.vy>-2 && dist<120 && Math.random()<T.blockCh*0.6){ f.blocking=true; f.vx=0; return; }
 
-  // block when opponent commits an attack in range
-  if(o.state==='attack' && dist<130 && Math.random()<T.blockCh*0.18){ f.blocking=true; f.vx=0; return; }
+  // block a committed grounded heavy/jab in range (per-frame prob → reliable over startup)
+  if(o.state==='attack' && o.atk!=='super' && dist<130 && Math.random()<T.blockCh*0.22){ f.blocking=true; f.vx=0; return; }
 
-  // fire super when charged & in range
-  if(f.meter>=100 && dist<110 && Math.random()<T.superCh*0.1){ tryAttack(f,'super'); return; }
+  // fire super decisively: opponent stunned, or simply in range (scaled by difficulty)
+  if(f.meter>=100 && dist<115 && (o.state==='hurt' || Math.random()<T.superCh*0.14)){ tryAttack(f,'super'); return; }
 
   const ideal=90;
   if(dist>ideal+30) mv=dir;
@@ -519,7 +539,9 @@ function aiControl(f, o, dt){
   // punish a whiffed opponent attack: dash in
   if(o.state==='attack' && o.atkHit===false && dist<160 && dist>90 && Math.random()<T.whiff*0.1){ mv=dir; }
 
-  if(f.onGround && Math.random()<T.jumpCh && dist<180){ f.vy=-13; f.onGround=false; }
+  // cornered & pressured → hop over instead of walking into the wall
+  if(nearWall && dist<150 && f.onGround && Math.random()<0.04){ f.vy=-13; f.onGround=false; mv=dir; }
+  else if(f.onGround && Math.random()<T.jumpCh && dist<180){ f.vy=-13; f.onGround=false; }
   f.vx=mv*T.speed;
 }
 
@@ -532,6 +554,13 @@ function spawnParticles(x,y,n,color,spd){
     particles.push({x,y,vx:Math.cos(ang)*s,vy:Math.sin(ang)*s-2,life:1,color,r:Math.random()*3+1.5});
   }
 }
+// directional spark cone — sprays in the knockback direction (dir = attacker.facing)
+function spawnBurst(x,y,n,color,spd,dir){
+  for(let i=0;i<n;i++){
+    const ang=(-0.5+Math.random())*0.9, s=Math.random()*spd+2;
+    particles.push({x,y,vx:dir*Math.cos(ang)*s,vy:Math.sin(ang)*s-1.5,life:1,color,r:Math.random()*2.5+1.2});
+  }
+}
 function updateParticles(dt){
   for(let i=particles.length-1;i>=0;i--){
     const p=particles[i];
@@ -626,9 +655,10 @@ function drawScene(){
     ctx.strokeStyle='rgba(0,0,0,.18)'; ctx.stroke(); }
 }
 function draw(){
-  drawScene();
   ctx.save();
+  if(zoom>0){ const z=1+zoom; ctx.translate(W/2,H/2); ctx.scale(z,z); ctx.translate(-W/2,-H/2); }  // impact punch-in
   if(shake>0){ ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake); }
+  drawScene();
   if(f1&&f2){
     drawShadow(f1); drawShadow(f2);
     // draw the fighter that's further back (higher on screen / smaller x-depth) first
@@ -639,10 +669,22 @@ function draw(){
   ctx.globalAlpha=1;
   ctx.restore();
 
+  if(flash>0){ ctx.save(); ctx.globalAlpha=Math.min(.6,flash); ctx.fillStyle=flashCol; ctx.fillRect(0,0,W,H); ctx.restore(); }
   if(f1&&f2) drawHUD();
+  if(comboShow.t>0) drawCombo();
   if(announceT>0) drawAnnounce();
   if(countdownT>0 && countdownT<3.2 && roundPhase==='intro') drawCountdown();
 }
+function drawCombo(){
+  ctx.save();
+  ctx.globalAlpha=Math.min(1,comboShow.t*2);
+  const rise=(0.9-comboShow.t)*26;
+  ctx.textAlign='center'; ctx.textBaseline='middle';
+  ctx.fillStyle='#ffe7a0'; ctx.font="italic 800 30px Georgia,serif";
+  ctx.shadowColor='rgba(0,0,0,.6)'; ctx.shadowBlur=10;
+  ctx.fillText(`COMBO ×${comboShow.n}`, Math.max(90,Math.min(W-90,comboShow.x)), 150-rise);
+  ctx.restore();
+}
 
 function drawShadow(f){
   const sc=f.onGround?1:0.6;
@@ -727,10 +769,17 @@ function drawFighter(f){
     ctx.beginPath(); ctx.arc(21,eyY-1,1.1,0,7); ctx.fill();
   }
   ctx.strokeStyle='#5c3a1a'; ctx.lineWidth=2; ctx.beginPath();
-  if(f.state==='attack'){ ctx.arc(14,eyY+13,4,0,Math.PI); }
+  if(f.state==='hurt'){ ctx.arc(14,eyY+13,3.2,Math.PI,2*Math.PI); }   // "ow" mouth
+  else if(f.state==='attack'){ ctx.arc(14,eyY+13,4,0,Math.PI); }
   else { ctx.moveTo(9,eyY+12); ctx.quadraticCurveTo(14,eyY+15,20,eyY+12); }
   ctx.stroke();
 
+  // dizzy stars circling the head while stunned
+  if(f.state==='hurt'){
+    const t=performance.now()/220; ctx.fillStyle='#ffe7a0';
+    for(let i=0;i<3;i++){ const aa=t+i*2.094; spark(14+Math.cos(aa)*16, -bh-6+Math.sin(aa)*5, 3.4); }
+  }
+
   // arm + weapon baguette
   ctx.save();
   ctx.translate(16,-bh/2-4);
@@ -763,6 +812,7 @@ function drawFighter(f){
   ctx.restore();
 }
 function xeye(x,y){ ctx.beginPath(); ctx.moveTo(x-3,y-3); ctx.lineTo(x+3,y+3); ctx.moveTo(x+3,y-3); ctx.lineTo(x-3,y+3); ctx.stroke(); }
+function spark(x,y,r){ ctx.beginPath(); ctx.moveTo(x,y-r); ctx.lineTo(x+r*.3,y-r*.3); ctx.lineTo(x+r,y); ctx.lineTo(x+r*.3,y+r*.3); ctx.lineTo(x,y+r); ctx.lineTo(x-r*.3,y+r*.3); ctx.lineTo(x-r,y); ctx.lineTo(x-r*.3,y-r*.3); ctx.closePath(); ctx.fill(); }
 function roundedLoaf(cx,topY,w,h){
   const r=w/2; ctx.beginPath();
   ctx.moveTo(cx-r,topY+r); ctx.arc(cx,topY+r,r,Math.PI,0);
@@ -878,6 +928,9 @@ function loop(now){
       }
       updateParticles(dt);
       if(shake>0) shake=Math.max(0,shake-dt*40);
+      if(flash>0) flash=Math.max(0,flash-dt*2.2);
+      if(zoom>0)  zoom =Math.max(0,zoom -dt*0.34);
+      if(comboShow.t>0) comboShow.t-=dt;
     }
   }
 

← 63c4a52 Add Baguette Brawl to the arcade (games/baguette-brawl, sing  ·  back to Games Agentabrams  ·  Baguette Brawl: add Training mode (re-inlined build) 0b14433 →