diff --git a/src/games/tetrisattack/TetrisAttackGame.js b/src/games/tetrisattack/TetrisAttackGame.js index bbdc1b8..217b148 100644 --- a/src/games/tetrisattack/TetrisAttackGame.js +++ b/src/games/tetrisattack/TetrisAttackGame.js @@ -20,6 +20,7 @@ const BOARD_LEFT = 744; const BOARD_TOP = 116; const STEP_MS = 1000 / 60; // logic tick rate +const POSE_HOLD_MS = 5000; // how long a reaction pose holds before resting export const D = { bg: -10, boardBg: 0, panels: 5, incoming: 4, cursor: 12, fx: 16, hud: 24, portrait: 20, overlay: 60, overlayUI: 62 }; @@ -288,8 +289,8 @@ export default class TetrisAttackGame extends Phaser.Scene { fontFamily: FONT, fontSize: '34px', color, stroke: '#101018', strokeThickness: 6, }).setOrigin(0.5).setDepth(D.fx); this.tweens.add({ targets: t, y: t.y - 70, alpha: 0, duration: 900, ease: 'Cubic.easeOut', onComplete: () => t.destroy() }); - // host reacts: player doing well → worried - this.hostPortrait?.setPose('worried'); + // host reacts: player doing well → worried for a beat, then back to rest + this.hostPortrait?.setPose('worried', POSE_HOLD_MS); } // floating score if (n && e.score) { @@ -419,7 +420,7 @@ export default class TetrisAttackGame extends Phaser.Scene { stroke: '#3a2a00', strokeThickness: 9, lineSpacing: 10, }).setOrigin(0.5).setDepth(D.fx); this.tweens.add({ targets: banner, scale: { from: 0.4, to: 1 }, duration: 340, ease: 'Back.easeOut' }); - this.hostPortrait?.setPose('worried'); + this.hostPortrait?.setPose('worried', POSE_HOLD_MS); this.time.delayedCall(1400, () => { this.tweens.add({ diff --git a/src/games/tetrisattack/TetrisAttackScreens.js b/src/games/tetrisattack/TetrisAttackScreens.js index ba9c850..2ce5dcd 100644 --- a/src/games/tetrisattack/TetrisAttackScreens.js +++ b/src/games/tetrisattack/TetrisAttackScreens.js @@ -288,19 +288,37 @@ export function createHostPortrait(scene, x, y, round, index = scene.roundIndex) const frameBg = scene.add.rectangle(x, y, 300, 380, 0x0e1526, 0.5).setStrokeStyle(5, 0x4a6ea8).setDepth(20); const img = buildPortraitImage(scene, round, index, 'neutral', x, y, 1.0); img.setDepth(21); + + const apply = (pose) => { + if (scene.textures.exists('tetrisattack-characters')) { + img.setFrame(index * 3 + (POSE_OFFSET[pose] ?? 0)); + } else { + // opponents fallback has no poses — pulse instead + const tint = pose === 'happy' ? 0xffd0d0 : pose === 'worried' ? 0xd0ffe0 : 0xffffff; + img.setTint(tint); + scene.tweens.add({ targets: img, scale: img.scaleX * 1.06, duration: 120, yoyo: true }); + } + }; + + // basePose is the pose the host rests in (driven by the danger flag); a held + // pose is a momentary reaction that reverts to it once holdMs elapses. + let basePose = 'neutral'; + let holdTimer = null; + const clearHold = () => { if (holdTimer) { holdTimer.remove(false); holdTimer = null; } }; + return { sprite: img, - setPose(pose) { - if (scene.textures.exists('tetrisattack-characters')) { - img.setFrame(index * 3 + (POSE_OFFSET[pose] ?? 0)); + setPose(pose, holdMs = 0) { + clearHold(); + if (holdMs > 0) { + apply(pose); + holdTimer = scene.time.delayedCall(holdMs, () => { holdTimer = null; apply(basePose); }); } else { - // opponents fallback has no poses — pulse instead - const tint = pose === 'happy' ? 0xffd0d0 : pose === 'worried' ? 0xd0ffe0 : 0xffffff; - img.setTint(tint); - scene.tweens.add({ targets: img, scale: img.scaleX * 1.06, duration: 120, yoyo: true }); + basePose = pose; + apply(pose); } }, - destroy() { frameBg.destroy(); img.destroy(); }, + destroy() { clearHold(); frameBg.destroy(); img.destroy(); }, }; }