Enhance combat action feedback
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user