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:
82
assets/scripts/theme/ThemeBackground.ts
Normal file
82
assets/scripts/theme/ThemeBackground.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Sprite, UITransform, Node, view, Layers, director, find } from 'cc';
|
||||
import { normalizeTexturePath } from '../visual/EntityTextureResolver';
|
||||
import { VisualAssets } from '../visual/VisualAssets';
|
||||
import { getThemeBackground } from './ThemeRegistry';
|
||||
|
||||
const BG_NODE_NAME = 'LevelThemeBackground';
|
||||
/** 与主关卡 UI_2D、HUD UI_3D 分离,由 BgCamera 固定渲染 */
|
||||
const BG_LAYER = Layers.Enum.DEFAULT;
|
||||
|
||||
/** 按关卡 theme 应用背景图(BgOverlay + BgCamera,不随主相机缩放/拖拽) */
|
||||
export class ThemeBackground {
|
||||
static resolveHost(entrance: Node | null): Node | null {
|
||||
const scene = entrance?.scene ?? director.getScene();
|
||||
if (!scene) return null;
|
||||
return find('BgOverlay', scene);
|
||||
}
|
||||
|
||||
/** 移除误挂在 GameRoot / MainLevelEntrance 等处的旧背景节点 */
|
||||
static purgeStaleNodes(entrance: Node | null) {
|
||||
const scene = entrance?.scene ?? director.getScene();
|
||||
if (!scene) return;
|
||||
const host = find('BgOverlay', scene);
|
||||
const walk = (node: Node) => {
|
||||
for (const ch of [...node.children]) {
|
||||
if (ch.name === BG_NODE_NAME && ch.parent !== host) {
|
||||
ch.destroy();
|
||||
} else {
|
||||
walk(ch);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(scene);
|
||||
}
|
||||
|
||||
static async apply(entrance: Node | null, themeId: string | undefined): Promise<void> {
|
||||
const parent = this.resolveHost(entrance);
|
||||
if (!parent?.isValid) return;
|
||||
this.purgeStaleNodes(entrance);
|
||||
|
||||
const path = normalizeTexturePath(getThemeBackground(themeId));
|
||||
let bgNode = parent.getChildByName(BG_NODE_NAME);
|
||||
if (!path) {
|
||||
if (bgNode) bgNode.active = false;
|
||||
return;
|
||||
}
|
||||
if (!bgNode) {
|
||||
bgNode = new Node(BG_NODE_NAME);
|
||||
bgNode.parent = parent;
|
||||
bgNode.addComponent(UITransform);
|
||||
bgNode.addComponent(Sprite);
|
||||
}
|
||||
bgNode.layer = BG_LAYER;
|
||||
bgNode.active = true;
|
||||
bgNode.setSiblingIndex(0);
|
||||
bgNode.setPosition(0, 0, 0);
|
||||
bgNode.setScale(1, 1, 1);
|
||||
|
||||
const sf = await VisualAssets.loadTexturePath(path);
|
||||
if (!sf || !bgNode?.isValid) return;
|
||||
const ui = bgNode.getComponent(UITransform)!;
|
||||
const spr = bgNode.getComponent(Sprite)!;
|
||||
spr.spriteFrame = sf;
|
||||
spr.sizeMode = Sprite.SizeMode.CUSTOM;
|
||||
this.layoutFullScreen(bgNode, sf);
|
||||
}
|
||||
|
||||
private static layoutFullScreen(bgNode: Node, sf: NonNullable<Awaited<ReturnType<typeof VisualAssets.loadTexturePath>>>) {
|
||||
const ui = bgNode.getComponent(UITransform)!;
|
||||
const vs = view.getVisibleSize();
|
||||
const scale = Math.max(vs.width / sf.originalSize.width, vs.height / sf.originalSize.height);
|
||||
ui.setContentSize(sf.originalSize.width * scale, sf.originalSize.height * scale);
|
||||
ui.setAnchorPoint(0.5, 0.5);
|
||||
bgNode.setPosition(0, 0, 0);
|
||||
}
|
||||
|
||||
static clear(entrance: Node | null) {
|
||||
this.purgeStaleNodes(entrance);
|
||||
const parent = this.resolveHost(entrance);
|
||||
const bgNode = parent?.getChildByName(BG_NODE_NAME);
|
||||
if (bgNode) bgNode.active = false;
|
||||
}
|
||||
}
|
||||
9
assets/scripts/theme/ThemeBackground.ts.meta
Normal file
9
assets/scripts/theme/ThemeBackground.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "83a0c5ff-dabf-4448-9aa9-e0f95caee32d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
438
assets/scripts/theme/ThemeDatabase.ts
Normal file
438
assets/scripts/theme/ThemeDatabase.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import { JsonAsset, resources } from 'cc';
|
||||
import { CELL_PIXEL } from '../core/GridConstants';
|
||||
import type {
|
||||
ThemeConfig,
|
||||
ThemeDatabaseFile,
|
||||
ThemeEntityConfig,
|
||||
ThemeTileConfig,
|
||||
ThemeHudConfig,
|
||||
ThemeHudIconKey,
|
||||
EntityDisplayGlobalConfig,
|
||||
EntityDisplayCellBox,
|
||||
EntityDisplayKind,
|
||||
EntityDisplayScaleConfig,
|
||||
} from './ThemeTypes';
|
||||
|
||||
const DB_PATH = 'theme/themes-database';
|
||||
|
||||
/** scale=1 时的包围盒(相对 100px 格子),宽高比固定 */
|
||||
export const ENTITY_DISPLAY_BASE: Record<EntityDisplayKind, EntityDisplayCellBox> = {
|
||||
player: { w: 0.68, h: 0.9 },
|
||||
vehicle: { w: 0.96, h: 0.88 },
|
||||
/** 与 Unity Prop_kuai* 竖长比例一致(约 49×69) */
|
||||
prop: { w: 0.52, h: 0.69 },
|
||||
propGround: { w: 0.52, h: 0.69 },
|
||||
};
|
||||
|
||||
const HALF_H = CELL_PIXEL * 0.25;
|
||||
|
||||
/** Unity Prop_sanxing checkPoint collider ≈ +0.144 world (PPU 100) */
|
||||
export const DEFAULT_PROP_BLOCK_Y_OFFSET = 14;
|
||||
/** Unity nProp_sanxing checkPoint collider ≈ -0.115 world */
|
||||
export const DEFAULT_PROP_GROUND_Y_OFFSET = -11;
|
||||
/** 空地/载具格角色与载具:补齐无 Baseblock 时的视觉高度(约半格) */
|
||||
export const DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET = HALF_H;
|
||||
/** JumpBlock 自动补偿之上的主题微调(px) */
|
||||
export const DEFAULT_MOVER_JUMP_CELL_Y_OFFSET = 0;
|
||||
/** 骑乘时角色相对载具节点抬高,对齐载具甲板(约 vehicle 包围盒 40%) */
|
||||
export const DEFAULT_PLAYER_RIDE_Y_OFFSET = HALF_H * 0.88;
|
||||
export const DEFAULT_PLAYER_STAND_Y_OFFSET = 0;
|
||||
|
||||
export const DEFAULT_ENTITY_DISPLAY: EntityDisplayGlobalConfig = {
|
||||
player: { scale: 1 },
|
||||
vehicle: { scale: 1 },
|
||||
prop: { scale: 1 },
|
||||
propGround: { scale: 1 },
|
||||
propBlockYOffset: DEFAULT_PROP_BLOCK_Y_OFFSET,
|
||||
propGroundYOffset: DEFAULT_PROP_GROUND_Y_OFFSET,
|
||||
moverEmptyCellYOffset: DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET,
|
||||
moverJumpCellYOffset: DEFAULT_MOVER_JUMP_CELL_Y_OFFSET,
|
||||
playerRideYOffset: DEFAULT_PLAYER_RIDE_Y_OFFSET,
|
||||
playerStandYOffset: DEFAULT_PLAYER_STAND_Y_OFFSET,
|
||||
};
|
||||
|
||||
const DISPLAY_KINDS: EntityDisplayKind[] = ['player', 'vehicle', 'prop', 'propGround'];
|
||||
|
||||
const HUD_ICON_KEYS: ThemeHudIconKey[] = [
|
||||
'navigation', 'revert', 'speed1', 'speed2', 'speed4',
|
||||
'zoomIn', 'zoomOut', 'audioOn', 'audioOff',
|
||||
];
|
||||
|
||||
/** Unity 导入的内置 HUD 回退(themes-database 未配置 hud 时使用) */
|
||||
const FALLBACK_HUD: Record<string, ThemeHudConfig> = {
|
||||
silu: {
|
||||
navigation: 'textures/silu/anniu_03',
|
||||
revert: 'textures/silu/anniu_06',
|
||||
speed1: 'textures/silu/anniu_08',
|
||||
speed2: 'textures/silu/anniu_10',
|
||||
speed4: 'textures/silu/anniu_12',
|
||||
zoomIn: 'textures/silu/anniu_17',
|
||||
zoomOut: 'textures/silu/anniu_19',
|
||||
audioOn: 'textures/silu/anniu_22',
|
||||
audioOff: 'textures/silu/anniu_21',
|
||||
},
|
||||
};
|
||||
|
||||
const THEME_ID_ALIASES: Record<string, string> = {
|
||||
default: 'silu',
|
||||
SILU: 'silu',
|
||||
redArmy: 'redarmy',
|
||||
};
|
||||
|
||||
const FOLDER_TO_THEME_ID: Record<string, string> = {
|
||||
redArmy: 'redarmy',
|
||||
};
|
||||
|
||||
function asTrimmedString(v: unknown): string | undefined {
|
||||
if (v == null) return undefined;
|
||||
if (typeof v === 'string') {
|
||||
const s = v.trim();
|
||||
return s || undefined;
|
||||
}
|
||||
if (typeof v === 'number' || typeof v === 'boolean') {
|
||||
return String(v);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveThemeId(themeId: string | undefined): string {
|
||||
const trimmed = asTrimmedString(themeId);
|
||||
if (!trimmed) return 'silu';
|
||||
const raw = trimmed;
|
||||
if (themesMap[raw]) return raw;
|
||||
const alias = THEME_ID_ALIASES[raw];
|
||||
if (alias && themesMap[alias]) return alias;
|
||||
const fromFolder = FOLDER_TO_THEME_ID[raw];
|
||||
if (fromFolder && themesMap[fromFolder]) return fromFolder;
|
||||
const lower = raw.toLowerCase();
|
||||
if (themesMap[lower]) return lower;
|
||||
return raw;
|
||||
}
|
||||
|
||||
function mergeHudConfig(raw: ThemeHudConfig | undefined, themeId: string): ThemeHudConfig {
|
||||
const layers: ThemeHudConfig[] = [FALLBACK_HUD.silu];
|
||||
if (FALLBACK_HUD[themeId]) layers.push(FALLBACK_HUD[themeId]!);
|
||||
if (raw) layers.push(raw);
|
||||
return Object.assign({}, ...layers);
|
||||
}
|
||||
|
||||
export function getThemeHudConfig(themeId: string | undefined): ThemeHudConfig {
|
||||
const id = resolveThemeId(themeId);
|
||||
const cfg = getThemeConfig(id);
|
||||
return mergeHudConfig(cfg?.hud, themesMap[id] ? id : 'silu');
|
||||
}
|
||||
|
||||
export function getThemeHudIconPath(themeId: string | undefined, icon: ThemeHudIconKey): string | undefined {
|
||||
const hud = getThemeHudConfig(themeId);
|
||||
const path = asTrimmedString(hud[icon]);
|
||||
if (path) return path;
|
||||
return FALLBACK_HUD.silu[icon];
|
||||
}
|
||||
|
||||
export function getThemeHudIconScale(themeId: string | undefined): { x: number; y: number } {
|
||||
const hud = getThemeHudConfig(themeId);
|
||||
return {
|
||||
x: hud.iconScaleX ?? 1,
|
||||
y: hud.iconScaleY ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
/** 左上角肖像贴图(优先 entities.portrait) */
|
||||
export function getThemePortraitPath(themeId: string | undefined): string | undefined {
|
||||
const id = resolveThemeId(themeId);
|
||||
const ent = getThemeConfig(id)?.entities;
|
||||
const portrait = asTrimmedString(ent?.portrait);
|
||||
if (portrait) return portrait;
|
||||
return asTrimmedString(ent?.playerFront);
|
||||
}
|
||||
|
||||
/** 左上角肖像水平翻转(默认朝右) */
|
||||
export function getThemePortraitFlipX(themeId: string | undefined): boolean {
|
||||
const hud = getThemeHudConfig(themeId);
|
||||
return hud.portraitFlipX ?? true;
|
||||
}
|
||||
|
||||
const DEFAULT_PORTRAIT_SCALE = 1.35;
|
||||
|
||||
export function getThemePortraitScale(themeId: string | undefined): number {
|
||||
const hud = getThemeHudConfig(themeId);
|
||||
const raw = hud.portraitScale;
|
||||
if (raw != null && Number.isFinite(raw)) {
|
||||
return Math.max(0.5, Math.min(2.5, raw));
|
||||
}
|
||||
return DEFAULT_PORTRAIT_SCALE;
|
||||
}
|
||||
|
||||
export function getThemeHudIconCandidates(themeId: string | undefined, icon: ThemeHudIconKey): string[] {
|
||||
const primary = getThemeHudIconPath(themeId, icon);
|
||||
if (!primary) return [];
|
||||
// 不回退到 silu 碎图,避免非丝路主题下倍速/音量变成黄圆按钮
|
||||
return [primary];
|
||||
}
|
||||
|
||||
function clampScale(v: number, fallback = 1): number {
|
||||
if (!Number.isFinite(v)) return fallback;
|
||||
return Math.max(0.1, Math.min(2, v));
|
||||
}
|
||||
|
||||
function readScale(raw: EntityDisplayScaleConfig | EntityDisplayCellBox | undefined, base: EntityDisplayCellBox): number {
|
||||
if (raw && 'scale' in raw && raw.scale != null) {
|
||||
return clampScale(raw.scale);
|
||||
}
|
||||
const legacy = raw as EntityDisplayCellBox | undefined;
|
||||
if (legacy?.w != null && legacy?.h != null) {
|
||||
return clampScale(Math.min(legacy.w / base.w, legacy.h / base.h));
|
||||
}
|
||||
if (legacy?.w != null) return clampScale(legacy.w / base.w);
|
||||
if (legacy?.h != null) return clampScale(legacy.h / base.h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
function mergeScale(raw: EntityDisplayScaleConfig | EntityDisplayCellBox | undefined, kind: EntityDisplayKind): EntityDisplayScaleConfig {
|
||||
return { scale: readScale(raw, ENTITY_DISPLAY_BASE[kind]) };
|
||||
}
|
||||
|
||||
function readYOffset(raw: number | undefined, fallback: number): number {
|
||||
if (raw == null || !Number.isFinite(raw)) return fallback;
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** 读取 JSON 中的等比缩放配置(兼容旧版 w/h) */
|
||||
export function mergeEntityDisplay(
|
||||
raw: (Partial<Record<EntityDisplayKind, EntityDisplayScaleConfig | EntityDisplayCellBox>> & Pick<EntityDisplayGlobalConfig, 'propBlockYOffset' | 'propGroundYOffset' | 'moverEmptyCellYOffset' | 'playerRideYOffset' | 'playerStandYOffset'>) | undefined,
|
||||
): EntityDisplayGlobalConfig {
|
||||
const prop = mergeScale(raw?.prop, 'prop');
|
||||
const blockY = readYOffset(raw?.propBlockYOffset, DEFAULT_PROP_BLOCK_Y_OFFSET);
|
||||
return {
|
||||
player: mergeScale(raw?.player, 'player'),
|
||||
vehicle: mergeScale(raw?.vehicle, 'vehicle'),
|
||||
prop,
|
||||
propGround: mergeScale(raw?.propGround ?? raw?.prop, 'propGround'),
|
||||
propBlockYOffset: blockY,
|
||||
propGroundYOffset: readYOffset(raw?.propGroundYOffset, DEFAULT_PROP_GROUND_Y_OFFSET),
|
||||
moverEmptyCellYOffset: readYOffset(raw?.moverEmptyCellYOffset, DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET),
|
||||
moverJumpCellYOffset: readYOffset(raw?.moverJumpCellYOffset, DEFAULT_MOVER_JUMP_CELL_Y_OFFSET),
|
||||
playerRideYOffset: readYOffset(raw?.playerRideYOffset, DEFAULT_PLAYER_RIDE_Y_OFFSET),
|
||||
playerStandYOffset: readYOffset(raw?.playerStandYOffset, DEFAULT_PLAYER_STAND_Y_OFFSET),
|
||||
};
|
||||
}
|
||||
|
||||
/** 骑乘载具时角色视觉 Y 抬高(逻辑格点仍与载具一致) */
|
||||
export function getThemePlayerRideYOffset(themeId: string | undefined): number {
|
||||
const cfg = getThemeEntityDisplayScales(themeId);
|
||||
return cfg.playerRideYOffset ?? DEFAULT_PLAYER_RIDE_Y_OFFSET;
|
||||
}
|
||||
|
||||
/** 角色站立 Y 偏移(负值降低) */
|
||||
export function getThemePlayerStandYOffset(themeId: string | undefined): number {
|
||||
const cfg = getThemeEntityDisplayScales(themeId);
|
||||
return cfg.playerStandYOffset ?? DEFAULT_PLAYER_STAND_Y_OFFSET;
|
||||
}
|
||||
|
||||
/** 空地/载具格角色与载具 Y 补偿(有砖块格为 0) */
|
||||
export function getThemeMoverEmptyCellYOffset(themeId: string | undefined): number {
|
||||
const cfg = getThemeEntityDisplayScales(themeId);
|
||||
return cfg.moverEmptyCellYOffset ?? cfg.propBlockYOffset ?? DEFAULT_MOVER_EMPTY_CELL_Y_OFFSET;
|
||||
}
|
||||
|
||||
/** JumpBlock 额外 Y 抬高(在自动 pivot 差值之上) */
|
||||
export function getThemeMoverJumpCellYOffset(themeId: string | undefined): number {
|
||||
const cfg = getThemeEntityDisplayScales(themeId);
|
||||
return cfg.moverJumpCellYOffset ?? DEFAULT_MOVER_JUMP_CELL_Y_OFFSET;
|
||||
}
|
||||
|
||||
/** 可拾取物砖块/空地 Y 偏移(来自主题 entityDisplay,缺省为 Unity Prop/nProp 换算值) */
|
||||
export function getThemePropPlacementOffsets(themeId: string | undefined): { block: number; ground: number } {
|
||||
const cfg = getThemeEntityDisplayScales(themeId);
|
||||
return {
|
||||
block: cfg.propBlockYOffset ?? DEFAULT_PROP_BLOCK_Y_OFFSET,
|
||||
ground: cfg.propGroundYOffset ?? DEFAULT_PROP_GROUND_Y_OFFSET,
|
||||
};
|
||||
}
|
||||
|
||||
/** 由缩放算出运行时包围盒(保持默认宽高比) */
|
||||
export function entityDisplayCellBoxes(scales: EntityDisplayGlobalConfig): Record<EntityDisplayKind, EntityDisplayCellBox> {
|
||||
const out = {} as Record<EntityDisplayKind, EntityDisplayCellBox>;
|
||||
for (const kind of DISPLAY_KINDS) {
|
||||
const s = scales[kind].scale;
|
||||
const base = ENTITY_DISPLAY_BASE[kind];
|
||||
out[kind] = { w: base.w * s, h: base.h * s };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getGlobalEntityDisplay(): Record<EntityDisplayKind, EntityDisplayCellBox> {
|
||||
return entityDisplayCellBoxes(getGlobalEntityDisplayScales());
|
||||
}
|
||||
|
||||
/** @deprecated 使用 getThemeEntityDisplayScales(themeId) */
|
||||
export function getGlobalEntityDisplayScales(): EntityDisplayGlobalConfig {
|
||||
return mergeEntityDisplay(fileCache?.entityDisplay);
|
||||
}
|
||||
|
||||
export function getThemeEntityDisplayScales(themeId: string | undefined): EntityDisplayGlobalConfig {
|
||||
const cfg = getThemeConfig(resolveThemeId(themeId));
|
||||
if (cfg?.entityDisplay) {
|
||||
return mergeEntityDisplay(cfg.entityDisplay);
|
||||
}
|
||||
if (fileCache?.entityDisplay) {
|
||||
return mergeEntityDisplay(fileCache.entityDisplay);
|
||||
}
|
||||
return mergeEntityDisplay(undefined);
|
||||
}
|
||||
|
||||
export function getThemeEntityDisplayCellBoxes(themeId: string | undefined): Record<EntityDisplayKind, EntityDisplayCellBox> {
|
||||
return entityDisplayCellBoxes(getThemeEntityDisplayScales(themeId));
|
||||
}
|
||||
|
||||
let fileCache: ThemeDatabaseFile | null = null;
|
||||
let themesMap: Record<string, ThemeConfig> = {};
|
||||
let sortedIds: string[] = [];
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
|
||||
function rebuildIndex() {
|
||||
sortedIds = Object.keys(themesMap).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function ingestFile(data: ThemeDatabaseFile) {
|
||||
migrateLegacyEntityDisplay(data);
|
||||
fileCache = data;
|
||||
themesMap = { ...(data.themes ?? {}) };
|
||||
rebuildIndex();
|
||||
}
|
||||
|
||||
/** 将旧版根级 entityDisplay 迁移到各主题(仅补缺失项) */
|
||||
function migrateLegacyEntityDisplay(data: ThemeDatabaseFile) {
|
||||
const legacy = data.entityDisplay;
|
||||
if (!legacy || !data.themes) return;
|
||||
for (const id of Object.keys(data.themes)) {
|
||||
if (!data.themes[id].entityDisplay) {
|
||||
data.themes[id].entityDisplay = mergeEntityDisplay(legacy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadThemeDatabase(): Promise<void> {
|
||||
if (loadPromise) return loadPromise;
|
||||
loadPromise = new Promise((resolve, reject) => {
|
||||
resources.load(DB_PATH, JsonAsset, (err, asset) => {
|
||||
if (err || !asset?.json) {
|
||||
console.warn('[ThemeDatabase] 未找到 themes-database.json,将使用内置回退', err);
|
||||
ingestFile({ version: 1, themes: {} });
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
ingestFile(asset.json as ThemeDatabaseFile);
|
||||
console.log(`[ThemeDatabase] 已加载 ${sortedIds.length} 个主题: ${sortedIds.join(', ')}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/** 重新读取 themes-database.json(主题控制器保存全局尺寸后生效) */
|
||||
export function reloadThemeDatabase(): Promise<void> {
|
||||
fileCache = null;
|
||||
loadPromise = null;
|
||||
themesMap = {};
|
||||
sortedIds = [];
|
||||
return new Promise((resolve) => {
|
||||
resources.release(DB_PATH);
|
||||
loadThemeDatabase().then(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
export function isThemeDatabaseReady(): boolean {
|
||||
return fileCache !== null;
|
||||
}
|
||||
|
||||
export function getThemeIds(): string[] {
|
||||
return [...sortedIds];
|
||||
}
|
||||
|
||||
export function getThemeConfig(themeId: string | undefined): ThemeConfig | null {
|
||||
if (!themeId) return null;
|
||||
return themesMap[themeId] ?? null;
|
||||
}
|
||||
|
||||
export function hasTheme(themeId: string): boolean {
|
||||
return themeId in themesMap;
|
||||
}
|
||||
|
||||
export function getThemeTextureFolder(themeId: string | undefined): string {
|
||||
const cfg = getThemeConfig(themeId);
|
||||
if (cfg?.textureFolder) return cfg.textureFolder;
|
||||
if (themeId === 'redarmy') return 'redArmy';
|
||||
return themeId ?? 'silu';
|
||||
}
|
||||
|
||||
export function getThemeEntities(themeId: string | undefined): ThemeEntityConfig | null {
|
||||
const cfg = getThemeConfig(themeId);
|
||||
return cfg?.entities ?? null;
|
||||
}
|
||||
|
||||
export function getThemeBackground(themeId: string | undefined): string | undefined {
|
||||
const cfg = getThemeConfig(themeId);
|
||||
return asTrimmedString(cfg?.background);
|
||||
}
|
||||
|
||||
export function getThemeTiles(themeId: string | undefined): ThemeTileConfig | null {
|
||||
const cfg = getThemeConfig(themeId);
|
||||
return cfg?.tiles ?? null;
|
||||
}
|
||||
|
||||
export function getThemeBorderDecorKey(themeId: string | undefined): string | undefined {
|
||||
const cfg = getThemeConfig(resolveThemeId(themeId));
|
||||
return asTrimmedString(cfg?.borderDecorKey);
|
||||
}
|
||||
|
||||
/** 关卡 JSON / Unity 预制体里常见的装饰砖 tileKey,统一映射到 themes-database 的 borderDecor */
|
||||
const BORDER_DECOR_ALIASES = new Set([
|
||||
'kuai11',
|
||||
'Decor23',
|
||||
'borderDecor',
|
||||
'素材切图-23',
|
||||
'素材切图2-23',
|
||||
'小游戏素材红色_03',
|
||||
]);
|
||||
|
||||
/** 按 tileKey 解析贴图路径(含 borderDecorKey 与跨主题别名) */
|
||||
export function getThemeTilePath(themeId: string | undefined, tileKey: unknown): string | undefined {
|
||||
const id = resolveThemeId(themeId);
|
||||
const tiles = getThemeTiles(id);
|
||||
if (!tiles) return undefined;
|
||||
const key = asTrimmedString(tileKey);
|
||||
if (!key) return undefined;
|
||||
if (key === 'Baseblock' && tiles.Baseblock) return tiles.Baseblock;
|
||||
if (key === 'JumpBlock' && tiles.JumpBlock) return tiles.JumpBlock;
|
||||
if (key === 'WallBlock' && tiles.WallBlock) return tiles.WallBlock;
|
||||
const decorKey = getThemeBorderDecorKey(id);
|
||||
if (tiles.borderDecor) {
|
||||
if (key === decorKey || BORDER_DECOR_ALIASES.has(key)) {
|
||||
return tiles.borderDecor;
|
||||
}
|
||||
}
|
||||
if (tiles.borderDecor && key === 'borderDecor') return tiles.borderDecor;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setTheme(themeId: string, config: ThemeConfig): void {
|
||||
themesMap[themeId] = { ...config };
|
||||
if (fileCache) fileCache.themes[themeId] = themesMap[themeId];
|
||||
rebuildIndex();
|
||||
}
|
||||
|
||||
export function removeTheme(themeId: string): boolean {
|
||||
if (!(themeId in themesMap)) return false;
|
||||
delete themesMap[themeId];
|
||||
if (fileCache?.themes) delete fileCache.themes[themeId];
|
||||
rebuildIndex();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function exportDatabaseJson(): string {
|
||||
const payload: ThemeDatabaseFile = fileCache ?? { version: 1, themes: {} };
|
||||
payload.updatedAt = new Date().toISOString();
|
||||
payload.themes = { ...themesMap };
|
||||
return JSON.stringify(payload, null, 2);
|
||||
}
|
||||
9
assets/scripts/theme/ThemeDatabase.ts.meta
Normal file
9
assets/scripts/theme/ThemeDatabase.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ff990bd4-abf4-4e61-93a1-d602e00ea740",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
38
assets/scripts/theme/ThemeRegistry.ts
Normal file
38
assets/scripts/theme/ThemeRegistry.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export {
|
||||
loadThemeDatabase,
|
||||
reloadThemeDatabase,
|
||||
isThemeDatabaseReady,
|
||||
getThemeIds,
|
||||
getThemeConfig,
|
||||
hasTheme,
|
||||
getThemeTextureFolder,
|
||||
getThemeEntities,
|
||||
getThemeBackground,
|
||||
getThemeTiles,
|
||||
getThemeBorderDecorKey,
|
||||
getThemeTilePath,
|
||||
getGlobalEntityDisplay,
|
||||
getGlobalEntityDisplayScales,
|
||||
getThemeEntityDisplayScales,
|
||||
getThemeEntityDisplayCellBoxes,
|
||||
mergeEntityDisplay,
|
||||
entityDisplayCellBoxes,
|
||||
ENTITY_DISPLAY_BASE,
|
||||
DEFAULT_ENTITY_DISPLAY,
|
||||
DEFAULT_PROP_BLOCK_Y_OFFSET,
|
||||
DEFAULT_PROP_GROUND_Y_OFFSET,
|
||||
getThemePropPlacementOffsets,
|
||||
getThemeHudConfig,
|
||||
getThemeHudIconPath,
|
||||
getThemeHudIconScale,
|
||||
getThemeHudIconCandidates,
|
||||
getThemePortraitPath,
|
||||
getThemePortraitFlipX,
|
||||
getThemePortraitScale,
|
||||
resolveThemeId,
|
||||
setTheme,
|
||||
removeTheme,
|
||||
exportDatabaseJson,
|
||||
} from './ThemeDatabase';
|
||||
|
||||
export type { ThemeConfig, ThemeEntityConfig, ThemeTileConfig, ThemeHudConfig, ThemeHudIconKey, ThemeDatabaseFile, EntityDisplayGlobalConfig, EntityDisplayCellBox, EntityDisplayScaleConfig, EntityDisplayKind } from './ThemeTypes';
|
||||
9
assets/scripts/theme/ThemeRegistry.ts.meta
Normal file
9
assets/scripts/theme/ThemeRegistry.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "02caf4b6-3db1-455e-9293-6caad43db09f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
119
assets/scripts/theme/ThemeTypes.ts
Normal file
119
assets/scripts/theme/ThemeTypes.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/** 主题内实体贴图(resources 相对路径,无 .png) */
|
||||
export interface ThemeEntityConfig {
|
||||
/** 左上角 HUD 肖像(缺省用 playerFront;丝路对齐 Unity idel-反) */
|
||||
portrait?: string;
|
||||
playerFront?: string;
|
||||
playerBack?: string;
|
||||
/** 载具四向贴图(北/东/南/西);转向时直接切换,不做 flipX */
|
||||
vehicleNorth?: string;
|
||||
vehicleEast?: string;
|
||||
vehicleSouth?: string;
|
||||
vehicleWest?: string;
|
||||
/** @deprecated 使用 vehicleSouth / vehicleNorth */
|
||||
vehicleFront?: string;
|
||||
/** @deprecated 使用 vehicleNorth / vehicleSouth */
|
||||
vehicleBack?: string;
|
||||
prop?: string;
|
||||
/** 空地可拾取物(Unity nProp*) */
|
||||
propGround?: string;
|
||||
}
|
||||
|
||||
/** 四种砖块:地面×2 + 墙×1 + 装饰墙×1 */
|
||||
export interface ThemeTileConfig {
|
||||
Baseblock?: string;
|
||||
JumpBlock?: string;
|
||||
WallBlock?: string;
|
||||
borderDecor?: string;
|
||||
}
|
||||
|
||||
/** HUD 右侧功能按钮贴图(相对 resources/,无扩展名;对齐 Unity GameManager.changeIcon) */
|
||||
export type ThemeHudIconKey =
|
||||
| 'navigation'
|
||||
| 'revert'
|
||||
| 'speed1'
|
||||
| 'speed2'
|
||||
| 'speed4'
|
||||
| 'zoomIn'
|
||||
| 'zoomOut'
|
||||
| 'audioOn'
|
||||
| 'audioOff';
|
||||
|
||||
export interface ThemeHudConfig {
|
||||
navigation?: string;
|
||||
revert?: string;
|
||||
speed1?: string;
|
||||
speed2?: string;
|
||||
speed4?: string;
|
||||
zoomIn?: string;
|
||||
zoomOut?: string;
|
||||
audioOn?: string;
|
||||
audioOff?: string;
|
||||
/** Unity redArmy 等主题按钮图标缩放 */
|
||||
iconScaleX?: number;
|
||||
iconScaleY?: number;
|
||||
/** 左上角肖像是否水平翻转朝右 */
|
||||
portraitFlipX?: boolean;
|
||||
/** 左上角肖像相对 Unity 194 框的缩放(默认 1.35) */
|
||||
portraitScale?: number;
|
||||
}
|
||||
|
||||
export interface ThemeConfig {
|
||||
/** 显示名 */
|
||||
displayName: string;
|
||||
/** textures 下文件夹名(可与主题 id 不同,如 redarmy → redArmy) */
|
||||
textureFolder?: string;
|
||||
/** 关卡背景图 */
|
||||
background?: string;
|
||||
/** 右侧 HUD 按钮贴图 */
|
||||
hud?: ThemeHudConfig;
|
||||
entities?: ThemeEntityConfig;
|
||||
tiles?: ThemeTileConfig;
|
||||
/** border 层第 4 块砖在关卡 JSON 里使用的 tileKey(如 kuai11) */
|
||||
borderDecorKey?: string;
|
||||
/** 本主题实体等比缩放(scale=1 为默认包围盒) */
|
||||
entityDisplay?: EntityDisplayGlobalConfig;
|
||||
}
|
||||
|
||||
export interface ThemeDatabaseFile {
|
||||
version: number;
|
||||
updatedAt?: string;
|
||||
/** @deprecated 已迁移至各 theme.entityDisplay */
|
||||
entityDisplay?: EntityDisplayGlobalConfig;
|
||||
themes: Record<string, ThemeConfig>;
|
||||
}
|
||||
|
||||
/** 相对 CELL_PIXEL(100)的宽高比例(运行时由 scale × 基准盒计算) */
|
||||
export interface EntityDisplayCellBox {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
/** themes-database.json 中存储的等比缩放(1 = 内置默认包围盒) */
|
||||
export interface EntityDisplayScaleConfig {
|
||||
scale: number;
|
||||
}
|
||||
|
||||
export interface EntityDisplayGlobalConfig {
|
||||
player: EntityDisplayScaleConfig;
|
||||
vehicle: EntityDisplayScaleConfig;
|
||||
prop: EntityDisplayScaleConfig;
|
||||
/** 空地可拾取物(nProp)等比缩放,缺省与 prop 相同 */
|
||||
propGround?: EntityDisplayScaleConfig;
|
||||
/** 砖块上可拾取物世界 Y 偏移(px),默认 HALF_H×1.36≈34 */
|
||||
propBlockYOffset?: number;
|
||||
/** 空地上可拾取物世界 Y 偏移(px),默认 -12 */
|
||||
propGroundYOffset?: number;
|
||||
/**
|
||||
* 无砖块格子(空地/载具)上角色与载具额外 Y 偏移(px),
|
||||
* 使视觉高度与站在 Baseblock 上一致;默认与 propBlockYOffset 相同
|
||||
*/
|
||||
moverEmptyCellYOffset?: number;
|
||||
/** JumpBlock 上角色/载具额外 Y 抬高(px),在自动按贴图 pivot 差值之上叠加 */
|
||||
moverJumpCellYOffset?: number;
|
||||
/** 骑乘载具时角色相对载具的额外 Y 偏移(px),使角色站在载具甲板上 */
|
||||
playerRideYOffset?: number;
|
||||
/** 角色站立 Y 偏移(px,负值降低),按主题微调贴图与砖面对齐 */
|
||||
playerStandYOffset?: number;
|
||||
}
|
||||
|
||||
export type EntityDisplayKind = 'player' | 'vehicle' | 'prop' | 'propGround';
|
||||
9
assets/scripts/theme/ThemeTypes.ts.meta
Normal file
9
assets/scripts/theme/ThemeTypes.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8aa76f83-d412-4a87-9ac9-72080487110d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user