Files
cocos/assets/scripts/visual/EntityTextureResolver.ts
刘宇飞 d393302388 Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:30:58 +08:00

319 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string> = {
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_kuai2Prop → 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<ReturnType<typeof getThemeEntities>>,
): string | undefined {
const ent = getThemeEntities(theme);
if (!ent) return undefined;
return normalizeTexturePath(ent[field]);
}
function themeDbEntityCandidates(
theme: string | undefined,
field: keyof NonNullable<ReturnType<typeof getThemeEntities>>,
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<string>();
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<LevelConfig, 'ground' | 'border'>,
): string[] {
const set = new Set<string>();
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;
}