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 = { 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; 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( 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 = explorationCharacterUnitTextureKeys ) { const assets: ExplorationCharacterAssetInfo[] = []; const seenTextureKeys = new Set(); 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 ) { 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 = [] ) { 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 ) { 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); } }); }); }