import { Direction } from '../core/Define'; import { LevelConfig, LevelEntityTextures, SpawnConfig, SpawnKind } from '../level/LevelTypes'; import { resolvePropPlacement } from '../level/EntitySpawnPlacement'; import type { PropPlacement } from '../level/EntitySpawnPlacement'; import { getThemeBackground, getThemeEntities, getThemeTextureFolder, isThemeDatabaseReady } from '../theme/ThemeRegistry'; import { entityTextureCandidates, vehicleSpriteRoleForDirection, vehicleThemeFieldForDirection, allVehicleDirectionTextureCandidates, type EntitySpriteRole, } from './ThemeEntityTextures'; const THEME_TEXTURE_FOLDER: Record = { default: 'default', silu: 'silu', SILU: 'silu', chinese: 'chinese', redArmy: 'redArmy', redarmy: 'redArmy', numMan: 'numMan', snow: 'snow', sanxing: 'sanxing', }; function resolveThemeFolder(theme: string | undefined): string { if (!theme) return 'silu'; const trimmed = String(theme).trim(); if (isThemeDatabaseReady()) return getThemeTextureFolder(trimmed); return THEME_TEXTURE_FOLDER[trimmed] ?? THEME_TEXTURE_FOLDER[trimmed.toLowerCase()] ?? trimmed; } export interface EntityVisualOptions { theme?: string; entityTextures?: LevelEntityTextures; /** 单个 spawn 的贴图覆盖(可拾取物常用) */ spawnTexture?: string; /** 可拾取物放置高度(Unity Prop / nProp) */ propPlacement?: PropPlacement; } /** 统一为 resources 相对路径,无 .png 后缀 */ export function normalizeTexturePath(raw: string | undefined): string | undefined { if (!raw) return undefined; let p = String(raw).trim().replace(/\\/g, '/'); if (!p) return undefined; if (p.startsWith('assets/resources/')) p = p.slice('assets/resources/'.length); if (p.startsWith('resources/')) p = p.slice('resources/'.length); if (p.startsWith('/')) p = p.slice(1); if (p.endsWith('.png')) p = p.slice(0, -4); return p; } function themePropCandidates(theme: string | undefined): string[] { const folder = resolveThemeFolder(theme); return [ `textures/${folder}/Prop_kuai1`, 'textures/silu/Prop_kuai1', ]; } function themeGroundPropCandidates(theme: string | undefined): string[] { const folder = resolveThemeFolder(theme); return [ `textures/${folder}/nProp_kuai1`, 'textures/silu/nProp_kuai1', ]; } /** Prop_kuai2 → nProp_kuai2;Prop → nProp */ export function toGroundPropTexturePath(blockPath: string | undefined): string | undefined { const norm = normalizeTexturePath(blockPath); if (!norm) return undefined; const slash = norm.lastIndexOf('/'); const dir = slash >= 0 ? norm.slice(0, slash + 1) : ''; const file = slash >= 0 ? norm.slice(slash + 1) : norm; if (file.startsWith('Prop_')) return `${dir}n${file}`; if (file === 'Prop') return `${dir}nProp`; if (file.startsWith('nProp')) return norm; return norm.replace(/\/Prop([^/]*)$/, '/nProp$1'); } function themeDbEntityPath( theme: string | undefined, field: keyof NonNullable>, ): string | undefined { const ent = getThemeEntities(theme); if (!ent) return undefined; return normalizeTexturePath(ent[field]); } function themeDbEntityCandidates( theme: string | undefined, field: keyof NonNullable>, role: EntitySpriteRole, ): string[] { const db = themeDbEntityPath(theme, field); const legacy = entityTextureCandidates(theme, role); return withFallback(db, legacy); } function levelVehicleTextureOverride( direction: Direction, entityTextures: LevelEntityTextures | undefined, ): string | undefined { if (!entityTextures) return undefined; const field = vehicleThemeFieldForDirection(direction); const direct = normalizeTexturePath(entityTextures[field]); if (direct) return direct; const front = isEntityFront(direction); return normalizeTexturePath( front ? entityTextures.vehicleFront : entityTextures.vehicleBack, ); } function withFallback(primary: string | undefined, fallbacks: string[]): string[] { const out: string[] = []; const norm = normalizeTexturePath(primary); if (norm) out.push(norm); for (const p of fallbacks) { if (!out.includes(p)) out.push(p); } return out; } function themePropCandidatesWithDb(theme: string | undefined): string[] { const db = themeDbEntityPath(theme, 'prop'); if (db) return [db]; return themePropCandidates(theme); } function themeGroundPropCandidatesWithDb( theme: string | undefined, blockPropPath?: string, ): string[] { const dbGround = themeDbEntityPath(theme, 'propGround'); if (dbGround) return [dbGround]; const derived = toGroundPropTexturePath(blockPropPath ?? themeDbEntityPath(theme, 'prop')); if (derived) return [derived]; return themeGroundPropCandidates(theme); } export function isEntityFront(direction: Direction | undefined): boolean { return direction === Direction.South || direction === Direction.East; } export function entityFlipX(direction: Direction | undefined): boolean { return direction === Direction.West || direction === Direction.East; } /** @deprecated 载具已改用四向贴图;玩家仍用 IsFront + flipX */ export function resolveVehicleOrientation(direction: Direction | undefined): { front: boolean; flipX: boolean; } { const dir = direction ?? Direction.North; return { front: isEntityFront(dir), flipX: entityFlipX(dir), }; } export function resolvePlayerTexturePaths( direction: Direction | undefined, options: EntityVisualOptions = {}, ): string[] { const front = isEntityFront(direction); const custom = front ? normalizeTexturePath(options.entityTextures?.playerFront) : normalizeTexturePath(options.entityTextures?.playerBack); const spawnCustom = normalizeTexturePath(options.spawnTexture); const themePaths = themeDbEntityCandidates( options.theme, front ? 'playerFront' : 'playerBack', front ? 'playerFront' : 'playerBack', ); if (spawnCustom) return withFallback(spawnCustom, themePaths); if (custom) return withFallback(custom, themePaths); return themePaths; } /** 载具四向贴图路径(北/东/南/西各一张,转向时直接切换,无 flipX) */ export function resolveVehicleTexturePaths( direction: Direction | undefined, options: EntityVisualOptions = {}, ): string[] { const dir = direction ?? Direction.North; const field = vehicleThemeFieldForDirection(dir); const role = vehicleSpriteRoleForDirection(dir); const custom = levelVehicleTextureOverride(dir, options.entityTextures); const spawnCustom = normalizeTexturePath(options.spawnTexture); const themePaths = themeDbEntityCandidates(options.theme, field, role); if (spawnCustom) return withFallback(spawnCustom, themePaths); if (custom) return withFallback(custom, themePaths); return themePaths; } /** 预加载主题载具四向贴图 */ export function collectThemeVehicleTexturePaths( theme: string | undefined, entityTextures?: LevelEntityTextures, ): string[] { const set = new Set(); const add = (p: string | undefined) => { const n = normalizeTexturePath(p); if (n) set.add(n); }; for (const p of allVehicleDirectionTextureCandidates(theme)) add(p); if (entityTextures) { for (let d = Direction.North; d <= Direction.West; d++) { add(levelVehicleTextureOverride(d, entityTextures)); } } return Array.from(set); } export function resolvePropTexturePaths(options: EntityVisualOptions = {}): string[] { const spawnCustom = normalizeTexturePath(options.spawnTexture); const levelBlock = normalizeTexturePath(options.entityTextures?.prop); const levelGround = normalizeTexturePath(options.entityTextures?.propGround); const onGround = options.propPlacement === 'ground'; if (spawnCustom) { const custom = onGround ? toGroundPropTexturePath(spawnCustom) ?? spawnCustom : spawnCustom; const fallback = onGround ? themeGroundPropCandidatesWithDb(options.theme, levelBlock) : themePropCandidatesWithDb(options.theme); return withFallback(custom, fallback); } if (onGround && levelGround) { return withFallback(levelGround, themeGroundPropCandidatesWithDb(options.theme, levelBlock)); } if (levelBlock) { const custom = onGround ? toGroundPropTexturePath(levelBlock) ?? levelBlock : levelBlock; return withFallback(custom, onGround ? themeGroundPropCandidatesWithDb(options.theme, levelBlock) : themePropCandidatesWithDb(options.theme)); } return onGround ? themeGroundPropCandidatesWithDb(options.theme) : themePropCandidatesWithDb(options.theme); } export function resolveEntityTexturePaths( kind: SpawnKind, direction: Direction | undefined, options: EntityVisualOptions = {}, ): string[] { if (kind === 'player') return resolvePlayerTexturePaths(direction, options); if (kind === 'vehicle') return resolveVehicleTexturePaths(direction, options); if (kind === 'prop' || kind === 'prop_decor') return resolvePropTexturePaths(options); return themePropCandidates(options.theme); } /** 预加载关卡用到的实体贴图(仅主路径,不扫全量 fallback) */ export function collectLevelEntityTexturePaths( theme: string | undefined, entityTextures?: LevelEntityTextures, spawns?: SpawnConfig[], levelConfig?: Pick, ): string[] { const set = new Set(); const add = (p: string | undefined) => { const n = normalizeTexturePath(p); if (n) set.add(n); }; const addPrimary = (paths: string[]) => { for (const p of paths) add(p); }; add(getThemeBackground(theme)); const visualBase: EntityVisualOptions = { theme, entityTextures }; const list = spawns ?? []; for (const s of list) { if (s.kind === 'player') { addPrimary(resolvePlayerTexturePaths( parseSpawnDirection(s.playerDirection) ?? Direction.South, { ...visualBase, spawnTexture: s.texture }, )); } else if (s.kind === 'vehicle') { for (let d = Direction.North; d <= Direction.West; d++) { addPrimary(resolveVehicleTexturePaths(d, { ...visualBase, spawnTexture: s.texture })); } } else if (s.kind === 'prop') { const placement: PropPlacement = levelConfig ? resolvePropPlacement(s, levelConfig as LevelConfig) : (s.propPlacement ?? 'block'); addPrimary(resolvePropTexturePaths({ ...visualBase, spawnTexture: s.texture, propPlacement: placement, })); } else if (s.kind === 'prop_decor') { addPrimary(resolvePropTexturePaths(visualBase)); } } if (!list.length) { addPrimary(resolvePlayerTexturePaths(Direction.South, visualBase)); for (const p of collectThemeVehicleTexturePaths(theme, entityTextures)) add(p); addPrimary(resolvePropTexturePaths(visualBase)); } return Array.from(set); } function parseSpawnDirection(raw: unknown): Direction | undefined { if (typeof raw === 'number') return raw as Direction; if (typeof raw !== 'string') return undefined; const name = raw.replace(/^Direction\./, ''); const key = name as keyof typeof Direction; if (Object.prototype.hasOwnProperty.call(Direction, key)) { const v = Direction[key]; if (typeof v === 'number') return v as Direction; } return undefined; }