348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import type Phaser from 'phaser';
|
|
import { releaseTextureSource } from '../render/loaderLifecycle';
|
|
|
|
export type ExplorationCharacterDirection = 'south' | 'east' | 'north' | 'west';
|
|
export type ExplorationCharacterMotion = 'idle' | 'walk';
|
|
|
|
export const explorationCharacterFrameSize = 192;
|
|
export const explorationCharacterIdleFrameCount = 8;
|
|
export const explorationCharacterWalkFrameCount = 8;
|
|
export const explorationCharacterFramesPerDirection =
|
|
explorationCharacterIdleFrameCount + explorationCharacterWalkFrameCount;
|
|
|
|
export const explorationCharacterTextureKeyByUnitTextureKey = {
|
|
'unit-liu-bei': 'exploration-liu-bei',
|
|
'unit-guan-yu': 'exploration-guan-yu',
|
|
'unit-zhang-fei': 'exploration-zhang-fei',
|
|
'unit-shu-officer': 'exploration-shu-officer',
|
|
'unit-shu-infantry': 'exploration-shu-infantry'
|
|
} as const;
|
|
|
|
export const explorationCharacterNamedTextureKeyFallbacks = {
|
|
'exploration-jian-yong': 'unit-shu-officer',
|
|
'exploration-mi-zhu': 'unit-shu-officer',
|
|
'exploration-zhuge-liang': 'unit-shu-officer',
|
|
'exploration-huang-quan': 'unit-shu-officer',
|
|
'exploration-zhuo-recruiting-clerk': 'unit-shu-officer',
|
|
'exploration-zhuo-villager': 'unit-shu-infantry',
|
|
'exploration-zhuo-quartermaster': 'unit-shu-officer',
|
|
'exploration-zou-jing': 'unit-shu-officer'
|
|
} as const;
|
|
|
|
export type ExplorationCharacterUnitTextureKey =
|
|
keyof typeof explorationCharacterTextureKeyByUnitTextureKey;
|
|
export type ExplorationCharacterBaseTextureKey =
|
|
(typeof explorationCharacterTextureKeyByUnitTextureKey)[ExplorationCharacterUnitTextureKey];
|
|
export type ExplorationCharacterNamedTextureKey =
|
|
keyof typeof explorationCharacterNamedTextureKeyFallbacks;
|
|
export type ExplorationCharacterTextureKey =
|
|
| ExplorationCharacterBaseTextureKey
|
|
| ExplorationCharacterNamedTextureKey;
|
|
|
|
export type ExplorationCharacterAssetInfo = {
|
|
unitTextureKey: ExplorationCharacterUnitTextureKey;
|
|
key: ExplorationCharacterTextureKey;
|
|
url: string;
|
|
frameSize: number;
|
|
frameWidth: number;
|
|
frameHeight: number;
|
|
format: 'webp';
|
|
};
|
|
|
|
export const explorationCharacterUnitTextureKeys = Object.freeze(
|
|
Object.keys(
|
|
explorationCharacterTextureKeyByUnitTextureKey
|
|
) as ExplorationCharacterUnitTextureKey[]
|
|
);
|
|
|
|
export const explorationCharacterTextureKeys = Object.freeze(
|
|
[
|
|
...Object.values(explorationCharacterTextureKeyByUnitTextureKey),
|
|
...Object.keys(explorationCharacterNamedTextureKeyFallbacks)
|
|
] as ExplorationCharacterTextureKey[]
|
|
);
|
|
|
|
const explorationCharacterDirections = [
|
|
'south',
|
|
'east',
|
|
'north',
|
|
'west'
|
|
] as const satisfies readonly ExplorationCharacterDirection[];
|
|
const explorationCharacterSheetRows: Record<ExplorationCharacterDirection, number> = {
|
|
south: 0,
|
|
east: 1,
|
|
north: 2,
|
|
west: 3
|
|
};
|
|
const explorationCharacterIdleFrameRate = 6;
|
|
const explorationCharacterWalkFrameRate = 10;
|
|
|
|
const explorationCharacterSheetModules = import.meta.glob(
|
|
'../../assets/images/exploration/characters/exploration-*.webp',
|
|
{
|
|
eager: true,
|
|
import: 'default'
|
|
}
|
|
) as Record<string, string>;
|
|
|
|
function textureKeyFromPath(path: string) {
|
|
const fileName = path.split('/').pop() ?? path;
|
|
return fileName.replace(/\.webp$/i, '');
|
|
}
|
|
|
|
const explorationCharacterSheetUrlsByKey = new Map(
|
|
Object.entries(explorationCharacterSheetModules).map(([path, url]) => [
|
|
textureKeyFromPath(path),
|
|
url
|
|
])
|
|
);
|
|
const explorationCharacterTextureKeySet = new Set<ExplorationCharacterTextureKey>(
|
|
explorationCharacterTextureKeys
|
|
);
|
|
const unitTextureKeyByExplorationCharacterTextureKey = new Map<
|
|
ExplorationCharacterBaseTextureKey,
|
|
ExplorationCharacterUnitTextureKey
|
|
>(
|
|
Object.entries(explorationCharacterTextureKeyByUnitTextureKey).map(
|
|
([unitTextureKey, textureKey]) => [
|
|
textureKey,
|
|
unitTextureKey as ExplorationCharacterUnitTextureKey
|
|
]
|
|
)
|
|
);
|
|
|
|
export function explorationCharacterTextureKeyForUnitTextureKey(
|
|
unitTextureKey: string
|
|
): ExplorationCharacterBaseTextureKey | undefined {
|
|
if (!(unitTextureKey in explorationCharacterTextureKeyByUnitTextureKey)) {
|
|
return undefined;
|
|
}
|
|
return explorationCharacterTextureKeyByUnitTextureKey[
|
|
unitTextureKey as ExplorationCharacterUnitTextureKey
|
|
];
|
|
}
|
|
|
|
export function explorationCharacterAssetInfo(
|
|
textureKeyOrUnitTextureKey: string
|
|
): ExplorationCharacterAssetInfo | undefined {
|
|
const key = explorationCharacterTextureKeyForSourceKey(textureKeyOrUnitTextureKey);
|
|
const url = key ? explorationCharacterSheetUrlsByKey.get(key) : undefined;
|
|
const unitTextureKey = key ? fallbackUnitTextureKeyForExplorationTextureKey(key) : undefined;
|
|
if (!key || !url || !unitTextureKey) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
unitTextureKey,
|
|
key,
|
|
url,
|
|
frameSize: explorationCharacterFrameSize,
|
|
frameWidth: explorationCharacterFrameSize,
|
|
frameHeight: explorationCharacterFrameSize,
|
|
format: 'webp'
|
|
};
|
|
}
|
|
|
|
export function explorationCharacterAssetInfos(
|
|
textureKeysOrUnitTextureKeys: Iterable<string> = explorationCharacterUnitTextureKeys
|
|
) {
|
|
const assets: ExplorationCharacterAssetInfo[] = [];
|
|
const seenTextureKeys = new Set<ExplorationCharacterTextureKey>();
|
|
Array.from(new Set(textureKeysOrUnitTextureKeys)).forEach((textureKeyOrUnitTextureKey) => {
|
|
const asset = explorationCharacterAssetInfo(textureKeyOrUnitTextureKey);
|
|
if (!asset || seenTextureKeys.has(asset.key)) {
|
|
return;
|
|
}
|
|
seenTextureKeys.add(asset.key);
|
|
assets.push(asset);
|
|
});
|
|
return assets;
|
|
}
|
|
|
|
export function hasExplorationCharacterAsset(textureKeyOrUnitTextureKey: string) {
|
|
return explorationCharacterAssetInfo(textureKeyOrUnitTextureKey) !== undefined;
|
|
}
|
|
|
|
function explorationCharacterTextureKeyForSourceKey(
|
|
textureKeyOrUnitTextureKey: string
|
|
): ExplorationCharacterTextureKey | undefined {
|
|
return explorationCharacterTextureKeyForUnitTextureKey(textureKeyOrUnitTextureKey) ??
|
|
(
|
|
explorationCharacterTextureKeySet.has(
|
|
textureKeyOrUnitTextureKey as ExplorationCharacterTextureKey
|
|
)
|
|
? textureKeyOrUnitTextureKey as ExplorationCharacterTextureKey
|
|
: undefined
|
|
);
|
|
}
|
|
|
|
function fallbackUnitTextureKeyForExplorationTextureKey(
|
|
textureKey: ExplorationCharacterTextureKey
|
|
): ExplorationCharacterUnitTextureKey | undefined {
|
|
const baseUnitTextureKey = unitTextureKeyByExplorationCharacterTextureKey.get(
|
|
textureKey as ExplorationCharacterBaseTextureKey
|
|
);
|
|
if (baseUnitTextureKey) {
|
|
return baseUnitTextureKey;
|
|
}
|
|
if (textureKey in explorationCharacterNamedTextureKeyFallbacks) {
|
|
return explorationCharacterNamedTextureKeyFallbacks[
|
|
textureKey as ExplorationCharacterNamedTextureKey
|
|
];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function explorationCharacterAnimationKey(
|
|
textureKey: ExplorationCharacterTextureKey,
|
|
motion: ExplorationCharacterMotion,
|
|
direction: ExplorationCharacterDirection
|
|
) {
|
|
return `${textureKey}-${motion}-${direction}`;
|
|
}
|
|
|
|
export function explorationCharacterFrameFor(
|
|
motion: ExplorationCharacterMotion,
|
|
direction: ExplorationCharacterDirection,
|
|
frameIndex = 0
|
|
) {
|
|
const rowStart =
|
|
explorationCharacterSheetRows[direction] * explorationCharacterFramesPerDirection;
|
|
const motionStart = motion === 'walk' ? explorationCharacterIdleFrameCount : 0;
|
|
const motionFrameCount = motion === 'walk'
|
|
? explorationCharacterWalkFrameCount
|
|
: explorationCharacterIdleFrameCount;
|
|
const normalizedFrameIndex = Math.max(
|
|
0,
|
|
Math.min(motionFrameCount - 1, Math.trunc(frameIndex))
|
|
);
|
|
return rowStart + motionStart + normalizedFrameIndex;
|
|
}
|
|
|
|
export function applyExplorationCharacterMotion(
|
|
sprite: Phaser.GameObjects.Sprite,
|
|
textureKey: ExplorationCharacterTextureKey,
|
|
motion: ExplorationCharacterMotion,
|
|
direction: ExplorationCharacterDirection,
|
|
reducedMotion = false
|
|
) {
|
|
if (!sprite.scene.textures.exists(textureKey)) {
|
|
return false;
|
|
}
|
|
|
|
if (reducedMotion && motion === 'idle') {
|
|
sprite.anims.stop();
|
|
sprite.setFrame(explorationCharacterFrameFor('idle', direction));
|
|
return true;
|
|
}
|
|
|
|
const animationKey = explorationCharacterAnimationKey(
|
|
textureKey,
|
|
motion,
|
|
direction
|
|
);
|
|
if (
|
|
sprite.anims.currentAnim?.key !== animationKey ||
|
|
!sprite.anims.isPlaying
|
|
) {
|
|
sprite.play(animationKey);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function ensureExplorationCharacterAnimations(
|
|
scene: Phaser.Scene,
|
|
textureKeysOrUnitTextureKeys: Iterable<string>
|
|
) {
|
|
explorationCharacterAssetInfos(textureKeysOrUnitTextureKeys).forEach(({ key }) => {
|
|
if (!scene.textures.exists(key)) {
|
|
return;
|
|
}
|
|
|
|
explorationCharacterDirections.forEach((direction) => {
|
|
const idleFrames = Array.from(
|
|
{ length: explorationCharacterIdleFrameCount },
|
|
(_, frameIndex) => ({
|
|
key,
|
|
frame: explorationCharacterFrameFor('idle', direction, frameIndex)
|
|
})
|
|
);
|
|
const walkFrames = Array.from(
|
|
{ length: explorationCharacterWalkFrameCount },
|
|
(_, frameIndex) => ({
|
|
key,
|
|
frame: explorationCharacterFrameFor('walk', direction, frameIndex)
|
|
})
|
|
);
|
|
const idleAnimationKey = explorationCharacterAnimationKey(key, 'idle', direction);
|
|
const walkAnimationKey = explorationCharacterAnimationKey(key, 'walk', direction);
|
|
|
|
if (!scene.anims.exists(idleAnimationKey)) {
|
|
scene.anims.create({
|
|
key: idleAnimationKey,
|
|
frames: idleFrames,
|
|
frameRate: explorationCharacterIdleFrameRate,
|
|
repeat: -1
|
|
});
|
|
}
|
|
if (!scene.anims.exists(walkAnimationKey)) {
|
|
scene.anims.create({
|
|
key: walkAnimationKey,
|
|
frames: walkFrames,
|
|
frameRate: explorationCharacterWalkFrameRate,
|
|
repeat: -1
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
export function releaseExplorationCharacterTextures(
|
|
scene: Phaser.Scene,
|
|
keepTextureKeysOrUnitTextureKeys: Iterable<string> = []
|
|
) {
|
|
const keepTextureKeys = new Set(
|
|
Array.from(keepTextureKeysOrUnitTextureKeys)
|
|
.map((textureKey) => explorationCharacterTextureKeyForSourceKey(textureKey))
|
|
.filter((key): key is ExplorationCharacterTextureKey => key !== undefined)
|
|
);
|
|
return releaseExplorationCharacterTextureKeys(
|
|
scene,
|
|
explorationCharacterTextureKeys.filter((textureKey) => !keepTextureKeys.has(textureKey))
|
|
);
|
|
}
|
|
|
|
export function releaseExplorationCharacterTextureKeys(
|
|
scene: Phaser.Scene,
|
|
textureKeysOrUnitTextureKeys: Iterable<string>
|
|
) {
|
|
let releasedTextureCount = 0;
|
|
const textureKeys = new Set(
|
|
Array.from(textureKeysOrUnitTextureKeys)
|
|
.map((textureKey) => explorationCharacterTextureKeyForSourceKey(textureKey))
|
|
.filter((key): key is ExplorationCharacterTextureKey => key !== undefined)
|
|
);
|
|
|
|
textureKeys.forEach((textureKey) => {
|
|
releaseExplorationCharacterAnimations(scene, textureKey);
|
|
if (releaseTextureSource(scene, textureKey)) {
|
|
releasedTextureCount += 1;
|
|
}
|
|
});
|
|
|
|
return releasedTextureCount;
|
|
}
|
|
|
|
function releaseExplorationCharacterAnimations(
|
|
scene: Phaser.Scene,
|
|
textureKey: ExplorationCharacterTextureKey
|
|
) {
|
|
explorationCharacterDirections.forEach((direction) => {
|
|
(['idle', 'walk'] as ExplorationCharacterMotion[]).forEach((motion) => {
|
|
const animationKey = explorationCharacterAnimationKey(textureKey, motion, direction);
|
|
if (scene.anims.exists(animationKey)) {
|
|
scene.anims.remove(animationKey);
|
|
}
|
|
});
|
|
});
|
|
}
|