Enhance combat action feedback

This commit is contained in:
2026-06-28 16:13:07 +09:00
parent 99843135c9
commit 30cac6c7ed
2 changed files with 363 additions and 26 deletions

View File

@@ -7529,35 +7529,183 @@ export class BattleScene extends Phaser.Scene {
depth: number,
defenderDirection: UnitDirection
) {
const direction = result.attacker.faction === 'ally' ? 1 : -1;
const direction = attackerX <= defenderX ? 1 : -1;
if (result.action === 'attack') {
soundDirector.playEffect('sword-slash', { volume: result.hit ? 0.52 : 0.38, stopAfterMs: 700 });
this.tweens.add({
targets: attackerSprite,
x: attackerX + direction * 84,
duration: 170,
yoyo: true,
ease: 'Sine.easeInOut'
});
await this.delay(180);
if (result.hit) {
defenderSprite.setTexture(this.unitActionTexture(result.defender), this.unitActionFrameIndex(defenderDirection, 'hurt'));
if (this.isRangedAttack(result)) {
await this.playRangedAttackMotion(result, attackerSprite, defenderSprite, attackerX, defenderX, groundY, depth, defenderDirection, direction);
return;
}
this.tweens.add({
targets: defenderSprite,
x: defenderX + direction * (result.hit ? 12 : 28),
y: result.hit ? defenderSprite.y : defenderSprite.y - 10,
duration: result.hit ? 70 : 120,
yoyo: true
});
await this.delay(230);
await this.playMeleeAttackMotion(result, attackerSprite, defenderSprite, attackerX, defenderX, groundY, depth, defenderDirection, direction);
return;
}
soundDirector.playEffect(result.action === 'item' ? 'cart-roll' : 'strategy-cast', {
volume: result.action === 'item' ? 0.34 : 0.44,
stopAfterMs: result.action === 'item' ? 720 : 950
await this.playSpecialActionMotion(result, attackerSprite, defenderSprite, attackerX, defenderX, groundY, depth, defenderDirection, direction);
}
private isRangedAttack(result: Pick<CombatResult, 'action' | 'attacker'>) {
return result.action === 'attack' && (result.attacker.classKey === 'archer' || this.attackRange(result.attacker) > 1);
}
private async playMeleeAttackMotion(
result: CombatResult,
attackerSprite: Phaser.GameObjects.Sprite,
defenderSprite: Phaser.GameObjects.Sprite,
attackerX: number,
defenderX: number,
groundY: number,
depth: number,
defenderDirection: UnitDirection,
direction: number
) {
const mounted = result.attacker.classKey === 'cavalry';
const windupX = attackerX - direction * 28;
const contactX = defenderX - direction * 126;
soundDirector.playMeleeRush(mounted);
this.createDustPuff(attackerX - direction * 18, groundY + 48, depth - 1, mounted ? 1.08 : 0.82);
this.tweens.add({
targets: attackerSprite,
x: windupX,
y: groundY + 5,
scaleX: 0.96,
duration: 110,
ease: 'Sine.easeIn'
});
await this.delay(115);
this.createDashTrail(attackerX - direction * 6, groundY - 30, contactX - direction * 18, groundY - 40, direction, depth);
this.createDustPuff(attackerX + direction * 18, groundY + 48, depth - 1, mounted ? 1.2 : 0.92);
this.tweens.add({
targets: attackerSprite,
x: contactX,
y: groundY - (mounted ? 4 : 2),
scaleX: 1.05,
duration: mounted ? 170 : 205,
ease: 'Cubic.easeIn'
});
await this.delay(mounted ? 170 : 205);
soundDirector.playMeleeSwing(result.hit, result.critical);
attackerSprite.setTexture(this.unitActionTexture(result.attacker), this.unitActionFrameIndex('east', 'attack'));
this.createSlashArc(defenderX - direction * 68, groundY - 48, direction, depth + 2, result.critical);
if (result.hit) {
defenderSprite.setTexture(this.unitActionTexture(result.defender), this.unitActionFrameIndex(defenderDirection, 'hurt'));
this.cameras.main.shake(result.critical ? 130 : 90, result.critical ? 0.006 : 0.0035);
this.shakeCombatSprite(defenderSprite, defenderX, groundY, direction, result.critical);
await this.delay(result.critical ? 130 : 92);
} else {
this.tweens.add({
targets: defenderSprite,
x: defenderX + direction * 36,
y: groundY - 16,
duration: 120,
yoyo: true,
ease: 'Sine.easeOut'
});
await this.delay(126);
}
this.tweens.add({
targets: attackerSprite,
x: attackerX,
y: groundY,
scaleX: 1,
duration: 180,
ease: 'Sine.easeOut'
});
await this.delay(190);
}
private async playRangedAttackMotion(
result: CombatResult,
attackerSprite: Phaser.GameObjects.Sprite,
defenderSprite: Phaser.GameObjects.Sprite,
attackerX: number,
defenderX: number,
groundY: number,
depth: number,
defenderDirection: UnitDirection,
direction: number
) {
attackerSprite.setTexture(this.unitActionTexture(result.attacker), this.unitActionFrameIndex('east', 'attack'));
this.createBowDrawEffect(attackerX + direction * 44, groundY - 52, direction, depth + 2);
this.tweens.add({
targets: attackerSprite,
x: attackerX - direction * 12,
y: groundY + 2,
duration: 150,
yoyo: true,
ease: 'Sine.easeInOut'
});
await this.delay(150);
soundDirector.playArrowShot();
const arrow = this.createArrowProjectile(attackerX + direction * 74, groundY - 62, direction, depth + 4);
const targetX = defenderX - direction * (result.hit ? 70 : 18);
const targetY = result.hit ? groundY - 62 : groundY - 104;
this.tweens.add({
targets: arrow,
x: targetX,
y: targetY,
angle: result.hit ? -4 * direction : -24 * direction,
duration: result.hit ? 330 : 380,
ease: result.hit ? 'Cubic.easeIn' : 'Sine.easeOut'
});
await this.delay(result.hit ? 332 : 382);
if (result.hit) {
defenderSprite.setTexture(this.unitActionTexture(result.defender), this.unitActionFrameIndex(defenderDirection, 'hurt'));
this.createArrowImpact(defenderX - direction * 58, groundY - 60, direction, depth + 5, result.critical);
this.shakeCombatSprite(defenderSprite, defenderX, groundY, direction, result.critical);
arrow.destroy();
await this.delay(result.critical ? 115 : 82);
return;
}
soundDirector.playArrowImpact(false, false);
this.tweens.add({
targets: arrow,
x: arrow.x + direction * 58,
y: arrow.y + 42,
alpha: 0,
angle: arrow.angle + direction * 36,
duration: 180,
ease: 'Sine.easeIn'
});
await this.delay(190);
arrow.destroy();
}
private async playSpecialActionMotion(
result: CombatResult,
attackerSprite: Phaser.GameObjects.Sprite,
defenderSprite: Phaser.GameObjects.Sprite,
attackerX: number,
defenderX: number,
groundY: number,
depth: number,
defenderDirection: UnitDirection,
direction: number
) {
if (result.action === 'item') {
soundDirector.playItemLaunch();
} else {
soundDirector.playStrategyPulse();
}
attackerSprite.setTexture(this.unitActionTexture(result.attacker), this.unitActionFrameIndex('east', this.actionPoseForCommand(result.action)));
this.createCasterPulse(attackerX + direction * 48, groundY - 50, depth + 2, result.action === 'item' ? 0xd8b15f : 0x6cc5ff);
this.tweens.add({
targets: attackerSprite,
scale: 1.08,
y: groundY - 4,
duration: 150,
yoyo: true,
ease: 'Sine.easeInOut'
});
await this.delay(155);
const projectile =
result.action === 'item'
@@ -7575,8 +7723,142 @@ export class BattleScene extends Phaser.Scene {
await this.delay(450);
if (result.hit) {
defenderSprite.setTexture(this.unitActionTexture(result.defender), this.unitActionFrameIndex(defenderDirection, 'hurt'));
this.createSpecialImpact(defenderX - direction * 68, result.action === 'item' ? groundY - 20 : groundY - 56, depth + 4, result.action);
this.shakeCombatSprite(defenderSprite, defenderX, groundY, direction, result.critical);
}
projectile.destroy();
await this.delay(110);
}
private createDustPuff(x: number, y: number, depth: number, scale = 1) {
const puff = this.trackCombatObject(this.add.container(x, y));
puff.setDepth(depth);
const left = this.add.circle(-12, 0, 9 * scale, 0xcdb48c, 0.34);
const center = this.add.circle(0, -4, 13 * scale, 0xd9c49a, 0.28);
const right = this.add.circle(12, 1, 8 * scale, 0xb99b72, 0.3);
puff.add([left, center, right]);
this.tweens.add({ targets: puff, y: y - 8, scale: 1.4, alpha: 0, duration: 360, ease: 'Sine.easeOut' });
return puff;
}
private createDashTrail(startX: number, startY: number, endX: number, endY: number, direction: number, depth: number) {
const trail = this.trackCombatObject(this.add.graphics());
trail.setDepth(depth);
trail.lineStyle(9, 0xffdf7b, 0.22);
trail.lineBetween(startX - direction * 16, startY + 8, endX - direction * 20, endY + 6);
trail.lineStyle(5, 0xf7f0d0, 0.36);
trail.lineBetween(startX, startY - 3, endX, endY - 8);
trail.lineStyle(3, 0x84c8ff, 0.18);
trail.lineBetween(startX - direction * 8, startY + 20, endX - direction * 12, endY + 16);
this.tweens.add({
targets: trail,
alpha: 0,
x: -direction * 18,
duration: 260,
ease: 'Sine.easeOut',
onComplete: () => trail.destroy()
});
return trail;
}
private createSlashArc(x: number, y: number, direction: number, depth: number, critical: boolean) {
const slash = this.trackCombatObject(this.add.graphics());
slash.setDepth(depth);
slash.lineStyle(critical ? 12 : 9, critical ? 0xff8f5f : 0xfff0b8, critical ? 0.94 : 0.86);
slash.beginPath();
slash.moveTo(x - direction * 42, y - 34);
slash.lineTo(x + direction * 8, y - 4);
slash.lineTo(x + direction * 46, y + 30);
slash.strokePath();
slash.lineStyle(critical ? 5 : 4, 0xffffff, 0.68);
slash.beginPath();
slash.moveTo(x - direction * 32, y - 26);
slash.lineTo(x + direction * 36, y + 24);
slash.strokePath();
const spark = this.trackCombatObject(this.add.star(x + direction * 14, y + 2, 7, 7, critical ? 34 : 25, critical ? 0xff6f4f : 0xffdf7b, 0.86));
spark.setDepth(depth + 1);
this.tweens.add({ targets: slash, alpha: 0, scale: 1.24, duration: 230, ease: 'Sine.easeOut', onComplete: () => slash.destroy() });
this.tweens.add({ targets: spark, alpha: 0, scale: critical ? 1.8 : 1.45, angle: 160 * direction, duration: 280, ease: 'Sine.easeOut' });
}
private shakeCombatSprite(sprite: Phaser.GameObjects.Sprite, baseX: number, baseY: number, direction: number, critical: boolean) {
this.tweens.add({
targets: sprite,
x: baseX + direction * (critical ? 24 : 15),
y: baseY - (critical ? 8 : 4),
duration: critical ? 46 : 54,
yoyo: true,
repeat: critical ? 2 : 1,
ease: 'Sine.easeInOut'
});
}
private createBowDrawEffect(x: number, y: number, direction: number, depth: number) {
const draw = this.trackCombatObject(this.add.graphics());
draw.setDepth(depth);
draw.lineStyle(4, 0xd8b15f, 0.86);
draw.beginPath();
draw.arc(x, y, 32, Phaser.Math.DegToRad(-62), Phaser.Math.DegToRad(62), false);
draw.strokePath();
draw.lineStyle(2, 0xf4e7c2, 0.74);
draw.lineBetween(x + direction * 9, y - 28, x - direction * 23, y);
draw.lineBetween(x - direction * 23, y, x + direction * 9, y + 28);
const notch = this.trackCombatObject(this.add.circle(x - direction * 25, y, 5, 0xffdf7b, 0.82));
notch.setDepth(depth + 1);
this.tweens.add({ targets: [draw, notch], alpha: 0, x: -direction * 10, duration: 260, ease: 'Sine.easeOut' });
}
private createArrowProjectile(x: number, y: number, direction: number, depth: number) {
const arrow = this.trackCombatObject(this.add.container(x, y));
arrow.setDepth(depth);
const shaft = this.add.rectangle(0, 0, 64, 4, 0xf2d7a1, 1);
shaft.setStrokeStyle(1, 0x6f5130, 0.8);
const head = this.add.triangle(direction * 39, 0, direction * 25, -8, direction * 45, 0, direction * 25, 8, 0xd8dfe6, 1);
head.setStrokeStyle(1, 0x45515c, 0.9);
const featherTop = this.add.triangle(-direction * 31, -4, -direction * 45, -11, -direction * 39, -1, -direction * 27, 3, 0xb84f3f, 0.96);
const featherBottom = this.add.triangle(-direction * 31, 4, -direction * 45, 11, -direction * 39, 1, -direction * 27, -3, 0xb84f3f, 0.96);
const glint = this.add.rectangle(direction * 10, -5, 32, 2, 0xffffff, 0.48);
arrow.add([shaft, head, featherTop, featherBottom, glint]);
arrow.setAngle(-5 * direction);
return arrow;
}
private createArrowImpact(x: number, y: number, direction: number, depth: number, critical: boolean) {
const burst = this.trackCombatObject(this.add.graphics());
burst.setDepth(depth);
burst.lineStyle(critical ? 5 : 4, critical ? 0xff8f5f : 0xffdf7b, 0.88);
for (let index = 0; index < 7; index += 1) {
const angle = -70 + index * 22;
const radians = Phaser.Math.DegToRad(angle);
const length = critical ? 38 : 28;
burst.lineBetween(x, y, x - direction * Math.cos(radians) * length, y + Math.sin(radians) * length);
}
const stuck = this.trackCombatObject(this.add.rectangle(x - direction * 10, y + 2, 42, 4, 0xf2d7a1, 0.92));
stuck.setDepth(depth + 1);
stuck.setAngle(-12 * direction);
this.tweens.add({ targets: burst, alpha: 0, scale: critical ? 1.5 : 1.25, duration: 260, ease: 'Sine.easeOut', onComplete: () => burst.destroy() });
this.tweens.add({ targets: stuck, alpha: 0, x: stuck.x - direction * 8, duration: 360, ease: 'Sine.easeOut' });
}
private createCasterPulse(x: number, y: number, depth: number, color: number) {
const ring = this.trackCombatObject(this.add.circle(x, y, 28));
ring.setDepth(depth);
ring.setStrokeStyle(3, color, 0.82);
const core = this.trackCombatObject(this.add.circle(x, y, 12, color, 0.28));
core.setDepth(depth - 1);
this.tweens.add({ targets: [ring, core], scale: 1.85, alpha: 0, duration: 420, ease: 'Sine.easeOut' });
}
private createSpecialImpact(x: number, y: number, depth: number, action: DamageCommand) {
const color = action === 'item' ? 0xffdf7b : 0x6cc5ff;
const impact = this.trackCombatObject(this.add.container(x, y));
impact.setDepth(depth);
const ring = this.add.circle(0, 0, action === 'item' ? 30 : 38);
ring.setStrokeStyle(action === 'item' ? 4 : 3, color, 0.9);
const core = this.add.star(0, 0, action === 'item' ? 7 : 6, 9, action === 'item' ? 34 : 28, color, 0.78);
const flash = this.add.circle(0, 0, 12, 0xffffff, 0.5);
impact.add([ring, core, flash]);
this.tweens.add({ targets: impact, scale: action === 'item' ? 1.38 : 1.65, alpha: 0, angle: action === 'item' ? 45 : 180, duration: 340, ease: 'Sine.easeOut' });
}
private createCombatCart(x: number, y: number, depth: number) {
@@ -7589,7 +7871,10 @@ export class BattleScene extends Phaser.Scene {
const rightWheel = this.add.circle(18, 15, 7, 0x1b1511, 1);
const blade = this.add.triangle(35, -4, 0, -10, 22, 0, 0, 10, 0xd8dfe6, 1);
blade.setStrokeStyle(1, 0x4d5960, 1);
container.add([body, cover, leftWheel, rightWheel, blade]);
const spark = this.add.star(34, -18, 5, 4, 12, 0xffdf7b, 0.72);
container.add([body, cover, leftWheel, rightWheel, blade, spark]);
this.tweens.add({ targets: [leftWheel, rightWheel], angle: 720, duration: 430 });
this.tweens.add({ targets: spark, angle: 180, alpha: 0.2, duration: 430, yoyo: true });
return container;
}
@@ -7600,7 +7885,10 @@ export class BattleScene extends Phaser.Scene {
const ring = this.add.circle(0, 0, 24);
ring.setStrokeStyle(3, 0xd8b15f, 0.9);
const spark = this.add.star(0, 0, 5, 7, 18, 0xf7e6a1, 0.9);
container.add([ring, core, spark]);
const outer = this.add.circle(0, 0, 38);
outer.setStrokeStyle(2, 0x6cc5ff, 0.58);
container.add([outer, ring, core, spark]);
this.tweens.add({ targets: outer, scale: 1.28, alpha: 0.18, duration: 430 });
this.tweens.add({ targets: ring, angle: 180, duration: 430 });
this.tweens.add({ targets: spark, angle: -240, duration: 430 });
return container;
@@ -7708,7 +7996,11 @@ export class BattleScene extends Phaser.Scene {
}
private showCombatImpact(result: CombatResult, x: number, y: number, depth: number) {
soundDirector.playEffect('combat-impact', { volume: result.critical ? 0.62 : result.action === 'strategy' ? 0.3 : 0.48, stopAfterMs: 650 });
if (this.isRangedAttack(result)) {
soundDirector.playArrowImpact(true, result.critical);
} else {
soundDirector.playEffect('combat-impact', { volume: result.critical ? 0.62 : result.action === 'strategy' ? 0.3 : 0.48, stopAfterMs: 650 });
}
const impact = this.trackCombatObject(
this.add.star(x, y, 8, 12, result.critical ? 54 : 42, result.critical ? 0xff8f5f : result.action === 'strategy' ? 0x6cc5ff : 0xffdf7b, 0.92)