Fix strategy motion effects by tactic type

This commit is contained in:
2026-07-09 15:44:59 +09:00
parent d257499769
commit 77b5db7770
4 changed files with 202 additions and 16 deletions

View File

@@ -0,0 +1,26 @@
# Battle Strategy Motion Audit
## Scope
Audited all battle strategy motions defined in `src/game/scenes/BattleScene.ts`.
## Findings
- `aid`: support/heal strategy. Uses the support cut-in, green healing effect, and does not use a projectile.
- `encourage`: support/focus strategy. Uses the support cut-in, gold buff effect, and does not use a projectile.
- `fireTactic`: enemy-target damage strategy named `화계`. It applies `burn`, but previously reused the generic blue-white strategy orb projectile.
- `roar`: enemy-target damage strategy named `고함`. It applies `confusion`, but also reused the same generic strategy orb projectile.
## Fix
- `fireTactic` now uses an orange/red moving flame projectile, fire-colored caster pulse, and flame burst impact.
- `roar` now uses a short-range shockwave projectile and impact ring instead of a magic orb.
- The generic blue strategy orb remains only as a fallback for future damage strategies without a specific motion.
- The final combat impact flash now matches the selected strategy motion color, so fire tactics no longer end with a blue-white impact star.
## Users Covered
- Player fire tactic users: Guan Yu, Jian Yong, Sun Qian, Zhuge Liang, Ma Liang, Pang Tong, Fa Zheng, Jiang Wei.
- Player roar users: Zhang Fei, Wei Yan, Ma Chao.
- Enemy AI fire tactic users: strategist, quartermaster, advisor, shaman, Sima Yi, and Chen Gong style units selected by enemy behavior.
- Enemy AI roar users: leader and officer style units selected by enemy behavior.

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View File

@@ -1119,6 +1119,7 @@ type DamageCommand = Exclude<BattleCommand, 'wait'>;
type UsableCommand = Extract<BattleCommand, 'strategy' | 'item'>;
type UsableEffect = 'damage' | 'heal' | 'focus';
type UsableTarget = 'enemy' | 'ally' | 'self';
type SpecialDamageMotionKind = 'item' | 'arcane' | 'fire' | 'roar';
type RosterTab = 'ally' | 'enemy';
type ActiveFaction = 'ally' | 'enemy';
const battleSpeedOptions = ['normal', 'fast', 'very-fast'] as const;
@@ -13805,6 +13806,7 @@ export class BattleScene extends Phaser.Scene {
defenderDirection: UnitDirection,
direction: number
) {
const motionKind = this.specialDamageMotionKind(result);
if (result.action === 'item') {
soundDirector.playItemLaunch();
} else {
@@ -13814,7 +13816,7 @@ export class BattleScene extends Phaser.Scene {
void this.playUnitActionFrames(attackerSprite, result.attacker, 'east', this.actionPoseForCommand(result.action), 92);
const baseScaleX = attackerSprite.scaleX;
const baseScaleY = attackerSprite.scaleY;
this.createCasterPulse(attackerX + direction * 48, groundY - 50, depth + 2, result.action === 'item' ? 0xd8b15f : 0x6cc5ff);
this.createCasterPulse(attackerX + direction * 48, groundY - 50, depth + 2, this.specialCasterColor(motionKind));
this.tweens.add({
targets: attackerSprite,
scaleX: baseScaleX * 1.08,
@@ -13826,29 +13828,89 @@ export class BattleScene extends Phaser.Scene {
});
await this.delay(155);
const projectile =
result.action === 'item'
? this.createCombatCart(attackerX + direction * 78, groundY - 10, depth)
: this.createStrategyEffect(attackerX + direction * 78, groundY - 48, depth);
const projectile = this.createSpecialProjectile(motionKind, attackerX + direction * 78, this.specialProjectileY(motionKind, groundY), direction, depth);
projectile.setDepth(depth);
this.combatCutInObjects.push(projectile);
this.tweens.add({
targets: projectile,
x: defenderX - direction * (result.hit ? 72 : 12),
y: result.hit ? projectile.y : projectile.y - 24,
y: result.hit ? projectile.y : projectile.y - this.specialProjectileMissLift(motionKind),
duration: this.scaledBattleDuration(430, 100),
ease: 'Cubic.easeIn'
});
await this.delay(450);
if (result.hit) {
void this.playUnitActionFrames(defenderSprite, result.defender, defenderDirection, 'hurt', 70);
this.createSpecialImpact(defenderX - direction * 68, result.action === 'item' ? groundY - 20 : groundY - 56, depth + 4, result.action);
this.createSpecialImpact(defenderX - direction * 68, this.specialImpactY(motionKind, groundY), depth + 4, motionKind);
this.shakeCombatSprite(defenderSprite, defenderX, groundY, direction, result.critical);
}
projectile.destroy();
await this.delay(110);
}
private specialDamageMotionKind(result: Pick<CombatResult, 'action' | 'usable'>): SpecialDamageMotionKind {
if (result.action === 'item') {
return 'item';
}
if (result.usable?.id === 'fireTactic' || result.usable?.statusEffect === 'burn') {
return 'fire';
}
if (result.usable?.id === 'roar' || result.usable?.statusEffect === 'confusion') {
return 'roar';
}
return 'arcane';
}
private specialCasterColor(kind: SpecialDamageMotionKind) {
if (kind === 'item') {
return 0xd8b15f;
}
if (kind === 'fire') {
return 0xff7a2a;
}
if (kind === 'roar') {
return 0xd8b15f;
}
return 0x6cc5ff;
}
private specialProjectileY(kind: SpecialDamageMotionKind, groundY: number) {
if (kind === 'item') {
return groundY - 10;
}
if (kind === 'roar') {
return groundY - 58;
}
return groundY - 48;
}
private specialImpactY(kind: SpecialDamageMotionKind, groundY: number) {
if (kind === 'item') {
return groundY - 20;
}
if (kind === 'roar') {
return groundY - 50;
}
return groundY - 56;
}
private specialProjectileMissLift(kind: SpecialDamageMotionKind) {
return kind === 'item' ? 18 : kind === 'roar' ? 6 : 24;
}
private createSpecialProjectile(kind: SpecialDamageMotionKind, x: number, y: number, direction: number, depth: number) {
if (kind === 'item') {
return this.createCombatCart(x, y, depth);
}
if (kind === 'fire') {
return this.createFireTacticProjectile(x, y, direction, depth);
}
if (kind === 'roar') {
return this.createRoarWaveProjectile(x, y, direction, depth);
}
return this.createStrategyEffect(x, y, depth);
}
private createDustPuff(x: number, y: number, depth: number, scale = 1) {
const puff = this.trackCombatObject(this.add.container(x, y));
puff.setDepth(depth);
@@ -13968,16 +14030,59 @@ export class BattleScene extends Phaser.Scene {
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;
private createSpecialImpact(x: number, y: number, depth: number, kind: SpecialDamageMotionKind) {
if (kind === 'fire') {
this.createFireTacticImpact(x, y, depth);
return;
}
if (kind === 'roar') {
this.createRoarImpact(x, y, depth);
return;
}
const color = kind === '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 ring = this.add.circle(0, 0, kind === 'item' ? 30 : 38);
ring.setStrokeStyle(kind === 'item' ? 4 : 3, color, 0.9);
const core = this.add.star(0, 0, kind === 'item' ? 7 : 6, 9, kind === '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' });
this.tweens.add({ targets: impact, scale: kind === 'item' ? 1.38 : 1.65, alpha: 0, angle: kind === 'item' ? 45 : 180, duration: 340, ease: 'Sine.easeOut' });
}
private createFireTacticImpact(x: number, y: number, depth: number) {
const impact = this.trackCombatObject(this.add.container(x, y));
impact.setDepth(depth);
const smokeBack = this.add.circle(-9, 10, 27, 0x3c2b24, 0.2);
const smokeFront = this.add.circle(15, 7, 21, 0x5a3a26, 0.18);
const outer = this.add.circle(0, 0, 42);
outer.setStrokeStyle(4, 0xff6a2f, 0.82);
const flameA = this.add.triangle(-13, 4, 0, 22, 16, -18, 30, 18, 0xe24a1e, 0.88);
const flameB = this.add.triangle(12, -1, 0, 18, 11, -25, 24, 16, 0xff8a2a, 0.94);
const core = this.add.star(0, 0, 7, 10, 31, 0xffcf5a, 0.92);
const flash = this.add.circle(0, -2, 12, 0xfff2b8, 0.7);
impact.add([smokeBack, smokeFront, outer, flameA, flameB, core, flash]);
this.tweens.add({ targets: impact, scale: 1.55, alpha: 0, angle: 18, duration: 360, ease: 'Sine.easeOut' });
this.tweens.add({ targets: [smokeBack, smokeFront], scale: 1.9, y: '+=12', duration: 420, ease: 'Sine.easeOut' });
}
private createRoarImpact(x: number, y: number, depth: number) {
const impact = this.trackCombatObject(this.add.container(x, y));
impact.setDepth(depth);
const outer = this.add.ellipse(0, 0, 92, 54);
outer.setStrokeStyle(5, 0xd8b15f, 0.8);
const middle = this.add.ellipse(0, 0, 62, 36);
middle.setStrokeStyle(3, 0xfff0b8, 0.72);
const inner = this.add.ellipse(0, 0, 32, 22, 0xb36dff, 0.18);
const burst = this.add.graphics();
burst.lineStyle(4, 0xffdf7b, 0.78);
for (let index = 0; index < 8; index += 1) {
const angle = Phaser.Math.DegToRad(index * 45);
burst.lineBetween(Math.cos(angle) * 15, Math.sin(angle) * 10, Math.cos(angle) * 46, Math.sin(angle) * 27);
}
impact.add([outer, middle, inner, burst]);
this.tweens.add({ targets: impact, scaleX: 1.5, scaleY: 1.2, alpha: 0, duration: 320, ease: 'Sine.easeOut' });
}
private createCombatCart(x: number, y: number, depth: number) {
@@ -13997,6 +14102,46 @@ export class BattleScene extends Phaser.Scene {
return container;
}
private createFireTacticProjectile(x: number, y: number, direction: number, depth: number) {
const container = this.add.container(x, y);
container.setDepth(depth);
const smoke = this.add.circle(-direction * 31, 7, 12, 0x4a3328, 0.24);
const backFlame = this.add.triangle(-direction * 8, 3, -direction * 38, -16, direction * 23, -2, -direction * 35, 18, 0xd9471f, 0.72);
const hotFlame = this.add.triangle(direction * 2, -1, -direction * 25, -11, direction * 29, -2, -direction * 22, 13, 0xff8a2a, 0.86);
const tail = this.add.circle(-direction * 19, 4, 17, 0xd9471f, 0.7);
const mid = this.add.circle(-direction * 6, -1, 15, 0xff7a25, 0.92);
const core = this.add.circle(direction * 7, -4, 10, 0xffd36a, 0.98);
const heart = this.add.circle(direction * 12, -5, 5, 0xfff2b8, 0.96);
const emberTop = this.add.star(-direction * 25, -13, 5, 3, 8, 0xffb341, 0.78);
const emberBottom = this.add.star(-direction * 34, 14, 5, 3, 7, 0xff6a2f, 0.68);
container.add([smoke, backFlame, hotFlame, tail, mid, core, heart, emberTop, emberBottom]);
this.tweens.add({ targets: [backFlame, hotFlame, tail, mid, core], scale: 1.12, duration: 120, yoyo: true, repeat: 2, ease: 'Sine.easeInOut' });
this.tweens.add({ targets: [emberTop, emberBottom], x: `-=${direction * 12}`, alpha: 0.22, angle: 160 * direction, duration: 430, ease: 'Sine.easeOut' });
this.tweens.add({ targets: smoke, scale: 1.55, alpha: 0.04, duration: 430, ease: 'Sine.easeOut' });
return container;
}
private createRoarWaveProjectile(x: number, y: number, direction: number, depth: number) {
const container = this.add.container(x, y);
container.setDepth(depth);
const outer = this.add.ellipse(0, 0, 64, 42);
outer.setStrokeStyle(4, 0xd8b15f, 0.74);
const middle = this.add.ellipse(-direction * 12, 0, 42, 28);
middle.setStrokeStyle(3, 0xf2e0a8, 0.62);
const inner = this.add.ellipse(-direction * 24, 0, 23, 16);
inner.setStrokeStyle(2, 0xb36dff, 0.52);
const pulse = this.add.circle(direction * 16, 0, 8, 0xffdf7b, 0.46);
const slash = this.add.graphics();
slash.lineStyle(4, 0xfff0b8, 0.66);
slash.lineBetween(-direction * 44, -16, direction * 22, -5);
slash.lineBetween(-direction * 42, 16, direction * 26, 5);
container.add([outer, middle, inner, pulse, slash]);
this.tweens.add({ targets: [outer, middle, inner], scaleX: 1.28, scaleY: 1.08, alpha: 0.22, duration: 430, ease: 'Sine.easeOut' });
this.tweens.add({ targets: pulse, scale: 1.8, alpha: 0.12, duration: 430, ease: 'Sine.easeOut' });
this.tweens.add({ targets: slash, x: direction * 18, alpha: 0.1, duration: 430, ease: 'Sine.easeOut' });
return container;
}
private createStrategyEffect(x: number, y: number, depth: number) {
const container = this.add.container(x, y);
container.setDepth(depth);
@@ -14122,9 +14267,7 @@ export class BattleScene extends Phaser.Scene {
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)
);
const impact = this.trackCombatObject(this.add.star(x, y, 8, 12, result.critical ? 54 : 42, this.combatImpactColor(result), 0.92));
impact.setDepth(depth);
this.tweens.add({
targets: impact,
@@ -14136,6 +14279,23 @@ export class BattleScene extends Phaser.Scene {
});
}
private combatImpactColor(result: Pick<CombatResult, 'action' | 'usable' | 'critical'>) {
if (result.critical) {
return 0xff8f5f;
}
if (result.action === 'strategy') {
const motionKind = this.specialDamageMotionKind(result);
if (motionKind === 'fire') {
return 0xff6a2f;
}
if (motionKind === 'roar') {
return 0xd8b15f;
}
return 0x6cc5ff;
}
return 0xffdf7b;
}
private showCombatMiss(x: number, y: number, depth: number) {
const miss = this.trackCombatObject(this.add.text(x, y, '회피', {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',