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:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

View 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_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;
}