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>
This commit is contained in:
318
assets/scripts/visual/EntityTextureResolver.ts
Normal file
318
assets/scripts/visual/EntityTextureResolver.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
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_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<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;
|
||||
}
|
||||
Reference in New Issue
Block a user