Improve battle unit immersion

This commit is contained in:
2026-06-28 23:15:45 +09:00
parent 66fc9984e5
commit 501a97706f
185 changed files with 196 additions and 65 deletions

View File

@@ -1058,6 +1058,11 @@ type UnitView = {
label: Phaser.GameObjects.Text;
textureBase: string;
direction: UnitDirection;
baseX: number;
baseY: number;
baseScaleX: number;
baseScaleY: number;
idleTween?: Phaser.Tweens.Tween;
};
type UnitDirection = 'south' | 'east' | 'north' | 'west';
@@ -3183,8 +3188,18 @@ export class BattleScene extends Phaser.Scene {
if (this.mapMask) {
label.setMask(this.mapMask);
}
const view = { sprite, label, textureBase, direction: 'south' as UnitDirection };
const view = {
sprite,
label,
textureBase,
direction: 'south' as UnitDirection,
baseX: sprite.x,
baseY: sprite.y,
baseScaleX: sprite.scaleX,
baseScaleY: sprite.scaleY
};
this.unitViews.set(unit.id, view);
this.applyUnitTone(view);
this.syncUnitMotion(unit, view);
});
}
@@ -3422,11 +3437,12 @@ export class BattleScene extends Phaser.Scene {
return;
}
view.sprite.setPosition(this.tileCenterX(unit.x), this.tileCenterY(unit.y));
view.label.setPosition(view.sprite.x, view.sprite.y + this.layout.tileSize * 0.52);
this.setUnitBasePosition(view, this.tileCenterX(unit.x), this.tileCenterY(unit.y));
view.label.setPosition(view.baseX, view.baseY + this.layout.tileSize * 0.52);
const visible = this.isTileVisible(unit.x, unit.y);
view.sprite.setVisible(visible);
view.label.setVisible(visible);
this.syncUnitMotion(unit, view);
}
private isTileVisible(x: number, y: number) {
@@ -3580,7 +3596,7 @@ export class BattleScene extends Phaser.Scene {
const targetX = this.tileCenterX(x);
const targetY = this.tileCenterY(y);
if (!stayingInPlace) {
this.moveUnitView(unit, x, y, view);
this.moveUnitView(unit, x, y, view, 260, fromX, fromY);
}
this.showCommandMenu(unit, pointer?.x ?? targetX, pointer?.y ?? targetY);
const isFinalAlly = this.remainingAllyCount() === 1;
@@ -6630,8 +6646,9 @@ export class BattleScene extends Phaser.Scene {
this.tweens.killTweensOf([view.sprite, view.label]);
view.direction = direction;
view.sprite.setPosition(this.tileCenterX(unit.x), this.tileCenterY(unit.y));
view.label.setPosition(view.sprite.x, view.sprite.y + this.layout.tileSize * 0.52);
this.setUnitBasePosition(view, this.tileCenterX(unit.x), this.tileCenterY(unit.y));
this.resetUnitSpritePose(view);
view.label.setPosition(view.baseX, view.baseY + this.layout.tileSize * 0.52);
if (unit.hp > 0) {
view.sprite.setInteractive({ useHandCursor: true });
}
@@ -6763,10 +6780,12 @@ export class BattleScene extends Phaser.Scene {
if (!this.canAttack(enemy, target) && behavior !== 'hold') {
const destination = this.chooseApproachTile(enemy, target);
if (destination && (destination.x !== enemy.x || destination.y !== enemy.y)) {
const fromX = enemy.x;
const fromY = enemy.y;
enemy.x = destination.x;
enemy.y = destination.y;
this.centerCameraOnTile(destination.x, destination.y);
await this.moveUnitViewAsync(enemy, destination.x, destination.y, this.unitViews.get(enemy.id), 260);
await this.moveUnitViewAsync(enemy, destination.x, destination.y, this.unitViews.get(enemy.id), 260, fromX, fromY);
}
}
@@ -8423,7 +8442,9 @@ export class BattleScene extends Phaser.Scene {
x: number,
y: number,
view = this.unitViews.get(unit.id),
duration = 260
duration = 260,
fromX = unit.x,
fromY = unit.y
) {
return new Promise<void>((resolve) => {
if (!view) {
@@ -8431,12 +8452,17 @@ export class BattleScene extends Phaser.Scene {
return;
}
const startX = this.tileCenterX(fromX);
const startY = this.tileCenterY(fromY);
const targetX = this.tileCenterX(x);
const targetY = this.tileCenterY(y);
const direction = this.directionFromDelta(targetX - view.sprite.x, targetY - view.sprite.y);
const movementDistance = this.movementTileDistance(unit, x, y);
const direction = this.directionFromDelta(x - fromX, y - fromY);
const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y);
const movementDuration = this.movementDuration(duration, movementDistance);
this.tweens.killTweensOf([view.sprite, view.label]);
this.setUnitBasePosition(view, startX, startY);
this.resetUnitSpritePose(view);
view.label.setPosition(startX, startY + this.layout.tileSize * 0.52);
this.playMovementSound(unit, movementDuration, movementDistance);
this.playUnitWalk(view, direction);
this.tweens.add({
@@ -8446,6 +8472,7 @@ export class BattleScene extends Phaser.Scene {
duration: movementDuration,
ease: 'Sine.easeInOut',
onComplete: () => {
this.setUnitBasePosition(view, targetX, targetY);
this.syncUnitMotion(unit, view, direction);
this.updateMiniMap();
resolve();
@@ -8591,6 +8618,7 @@ export class BattleScene extends Phaser.Scene {
return;
}
view.sprite.postFX.clear();
this.applyUnitTone(view);
view.sprite.clearTint();
view.sprite.setAlpha(1);
if (unit) {
@@ -8935,7 +8963,7 @@ export class BattleScene extends Phaser.Scene {
this.updateMiniMap();
soundDirector.playSelect();
this.moveUnitView(unit, fromX, fromY, view, 180);
this.moveUnitView(unit, fromX, fromY, view, 180, toX, toY);
this.clearMarkers();
this.showMoveRange(unit);
this.renderUnitDetail(
@@ -8950,18 +8978,25 @@ export class BattleScene extends Phaser.Scene {
x: number,
y: number,
view = this.unitViews.get(unit.id),
duration = 260
duration = 260,
fromX = unit.x,
fromY = unit.y
) {
if (!view) {
return;
}
const startX = this.tileCenterX(fromX);
const startY = this.tileCenterY(fromY);
const targetX = this.tileCenterX(x);
const targetY = this.tileCenterY(y);
const direction = this.directionFromDelta(targetX - view.sprite.x, targetY - view.sprite.y);
const movementDistance = this.movementTileDistance(unit, x, y);
const direction = this.directionFromDelta(x - fromX, y - fromY);
const movementDistance = this.movementTileDistanceFrom(fromX, fromY, x, y);
const movementDuration = this.movementDuration(duration, movementDistance);
this.tweens.killTweensOf([view.sprite, view.label]);
this.setUnitBasePosition(view, startX, startY);
this.resetUnitSpritePose(view);
view.label.setPosition(startX, startY + this.layout.tileSize * 0.52);
this.playMovementSound(unit, movementDuration, movementDistance);
this.playUnitWalk(view, direction);
this.tweens.add({
@@ -8971,6 +9006,7 @@ export class BattleScene extends Phaser.Scene {
duration: movementDuration,
ease: 'Sine.easeInOut',
onComplete: () => {
this.setUnitBasePosition(view, targetX, targetY);
this.syncUnitMotion(unit, view, direction);
this.updateMiniMap();
}
@@ -8985,7 +9021,11 @@ export class BattleScene extends Phaser.Scene {
}
private movementTileDistance(unit: UnitData, x: number, y: number) {
return Math.max(1, Math.abs(x - unit.x) + Math.abs(y - unit.y));
return this.movementTileDistanceFrom(unit.x, unit.y, x, y);
}
private movementTileDistanceFrom(fromX: number, fromY: number, x: number, y: number) {
return Math.max(1, Math.abs(x - fromX) + Math.abs(y - fromY));
}
private movementDuration(baseDuration: number, tileDistance: number) {
@@ -8996,19 +9036,18 @@ export class BattleScene extends Phaser.Scene {
}
private playMovementSound(unit: UnitData, duration: number, tileDistance = 1) {
const mountedClasses: UnitClassKey[] = ['cavalry'];
const isMounted = mountedClasses.includes(unit.classKey);
const isMounted = this.isMountedUnit(unit);
const key = isMounted ? 'horse-gallop' : 'footstep-walk';
const pulseCount = isMounted
? Math.max(2, Math.min(9, tileDistance * 2))
? Math.max(3, Math.min(10, tileDistance * 2 + 1))
: Math.max(2, Math.min(8, tileDistance + 1));
const interval = Math.max(isMounted ? 74 : 96, duration / Math.max(1, pulseCount - 0.25));
const interval = Math.max(isMounted ? 72 : 108, duration / Math.max(1, pulseCount - 0.25));
for (let index = 0; index < pulseCount; index += 1) {
this.time.delayedCall(Math.round(index * interval), () => {
soundDirector.playEffect(key, {
volume: isMounted ? 0.36 : 0.32,
rate: isMounted ? 1.02 + (index % 2) * 0.1 : 0.95 + (index % 2) * 0.12,
volume: isMounted ? 0.24 : 0.18,
rate: isMounted ? 0.98 + (index % 2) * 0.12 : 0.9 + (index % 2) * 0.12,
stopAfterMs: Math.round(Math.min(isMounted ? 210 : 260, interval + 80))
});
});
@@ -9024,18 +9063,22 @@ export class BattleScene extends Phaser.Scene {
private playUnitWalk(view: UnitView, direction: UnitDirection) {
view.direction = direction;
this.stopUnitIdle(view);
view.sprite.setFlipX(false);
view.sprite.play(`${view.textureBase}-walk-${direction}`, true);
}
private playUnitIdle(view: UnitView, direction: UnitDirection) {
private playUnitIdle(unit: UnitData, view: UnitView, direction: UnitDirection) {
view.direction = direction;
this.stopUnitIdle(view);
view.sprite.setFlipX(false);
view.sprite.play(`${view.textureBase}-idle-${direction}`, true);
this.startUnitIdleMotion(unit, view, direction);
}
private stopUnitWalk(view: UnitView, direction: UnitDirection) {
view.direction = direction;
this.stopUnitIdle(view);
view.sprite.stop();
this.setUnitDirectionFrame(view, direction);
}
@@ -9051,7 +9094,7 @@ export class BattleScene extends Phaser.Scene {
return;
}
this.playUnitIdle(view, direction);
this.playUnitIdle(unit, view, direction);
}
private setUnitDirectionFrame(view: UnitView, direction: UnitDirection) {
@@ -9059,6 +9102,60 @@ export class BattleScene extends Phaser.Scene {
view.sprite.setFlipX(false);
}
private setUnitBasePosition(view: UnitView, x: number, y: number) {
view.baseX = x;
view.baseY = y;
view.sprite.setPosition(x, y);
}
private resetUnitSpritePose(view: UnitView) {
view.sprite.setPosition(view.baseX, view.baseY);
view.sprite.setScale(view.baseScaleX, view.baseScaleY);
view.sprite.setAngle(0);
}
private stopUnitIdle(view: UnitView) {
view.idleTween?.stop();
view.idleTween = undefined;
this.tweens.killTweensOf(view.sprite);
this.resetUnitSpritePose(view);
}
private startUnitIdleMotion(unit: UnitData, view: UnitView, direction: UnitDirection) {
const mounted = this.isMountedUnit(unit);
const seed = this.hashString(unit.id) % 160;
const sway = direction === 'west' ? -1 : 1;
const bob = mounted ? 2.1 : 1.35;
const duration = mounted ? 520 + seed * 0.45 : 880 + seed * 0.8;
const angle = mounted ? 0.45 * sway : 0.65 * sway;
const scaleX = view.baseScaleX * (mounted ? 1.012 : 0.992);
const scaleY = view.baseScaleY * (mounted ? 0.988 : 1.014);
view.idleTween = this.tweens.add({
targets: view.sprite,
y: view.baseY - bob,
angle,
scaleX,
scaleY,
duration,
ease: 'Sine.easeInOut',
yoyo: true,
repeat: -1,
delay: seed
});
}
private applyUnitTone(view: UnitView) {
const tone = view.sprite.postFX.addColorMatrix();
tone.saturate(-0.32);
tone.contrast(0.03, true);
tone.brightness(0.97, true);
}
private isMountedUnit(unit: UnitData) {
return unit.classKey === 'cavalry';
}
private unitFrameIndex(direction: UnitDirection, frame = 0) {
return unitFrameRows[direction] * 4 + frame;
}
@@ -9240,6 +9337,7 @@ export class BattleScene extends Phaser.Scene {
}
view.sprite.postFX.clear();
this.applyUnitTone(view);
view.sprite.clearTint();
view.sprite.setAlpha(1);
this.syncUnitMotion(unit, view);