Improve battle unit immersion
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user