feat: add timed pose transitions to host portrait reactions
Introduce temporary emotional reactions for the host portrait that hold for a duration before reverting to a resting pose. The setPose method now accepts an optional holdMs parameter; when provided, the pose is applied temporarily and transitions back to the base (danger-driven) pose once the timer elapses. This prevents the host from remaining in a reaction pose indefinitely after events like scoring or opponent clears.
This commit is contained in:
parent
ecfdf4c59b
commit
962466a0a3
|
|
@ -20,6 +20,7 @@ const BOARD_LEFT = 744;
|
||||||
const BOARD_TOP = 116;
|
const BOARD_TOP = 116;
|
||||||
|
|
||||||
const STEP_MS = 1000 / 60; // logic tick rate
|
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 };
|
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,
|
fontFamily: FONT, fontSize: '34px', color, stroke: '#101018', strokeThickness: 6,
|
||||||
}).setOrigin(0.5).setDepth(D.fx);
|
}).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() });
|
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
|
// host reacts: player doing well → worried for a beat, then back to rest
|
||||||
this.hostPortrait?.setPose('worried');
|
this.hostPortrait?.setPose('worried', POSE_HOLD_MS);
|
||||||
}
|
}
|
||||||
// floating score
|
// floating score
|
||||||
if (n && e.score) {
|
if (n && e.score) {
|
||||||
|
|
@ -419,7 +420,7 @@ export default class TetrisAttackGame extends Phaser.Scene {
|
||||||
stroke: '#3a2a00', strokeThickness: 9, lineSpacing: 10,
|
stroke: '#3a2a00', strokeThickness: 9, lineSpacing: 10,
|
||||||
}).setOrigin(0.5).setDepth(D.fx);
|
}).setOrigin(0.5).setDepth(D.fx);
|
||||||
this.tweens.add({ targets: banner, scale: { from: 0.4, to: 1 }, duration: 340, ease: 'Back.easeOut' });
|
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.time.delayedCall(1400, () => {
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
|
|
|
||||||
|
|
@ -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 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);
|
const img = buildPortraitImage(scene, round, index, 'neutral', x, y, 1.0);
|
||||||
img.setDepth(21);
|
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 {
|
return {
|
||||||
sprite: img,
|
sprite: img,
|
||||||
setPose(pose) {
|
setPose(pose, holdMs = 0) {
|
||||||
if (scene.textures.exists('tetrisattack-characters')) {
|
clearHold();
|
||||||
img.setFrame(index * 3 + (POSE_OFFSET[pose] ?? 0));
|
if (holdMs > 0) {
|
||||||
|
apply(pose);
|
||||||
|
holdTimer = scene.time.delayedCall(holdMs, () => { holdTimer = null; apply(basePose); });
|
||||||
} else {
|
} else {
|
||||||
// opponents fallback has no poses — pulse instead
|
basePose = pose;
|
||||||
const tint = pose === 'happy' ? 0xffd0d0 : pose === 'worried' ? 0xd0ffe0 : 0xffffff;
|
apply(pose);
|
||||||
img.setTint(tint);
|
|
||||||
scene.tweens.add({ targets: img, scale: img.scaleX * 1.06, duration: 120, yoyo: true });
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroy() { frameBg.destroy(); img.destroy(); },
|
destroy() { clearHold(); frameBg.destroy(); img.destroy(); },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue