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:
@@ -30,6 +30,35 @@ export enum Skin {
|
||||
sanxing = 5,
|
||||
}
|
||||
|
||||
const SKIN_TO_THEME: Record<Skin, string> = {
|
||||
[Skin.Silu]: 'silu',
|
||||
[Skin.Panda]: 'chinese',
|
||||
[Skin.RedArmy]: 'redArmy',
|
||||
[Skin.numMan]: 'numMan',
|
||||
[Skin.snow]: 'snow',
|
||||
[Skin.sanxing]: 'sanxing',
|
||||
};
|
||||
|
||||
const THEME_TO_SKIN: Record<string, Skin> = {
|
||||
silu: Skin.Silu,
|
||||
chinese: Skin.Panda,
|
||||
redarmy: Skin.RedArmy,
|
||||
redArmy: Skin.RedArmy,
|
||||
numMan: Skin.numMan,
|
||||
snow: Skin.snow,
|
||||
sanxing: Skin.sanxing,
|
||||
};
|
||||
|
||||
export function skinToTheme(skin: Skin): string {
|
||||
return SKIN_TO_THEME[skin] ?? 'silu';
|
||||
}
|
||||
|
||||
export function themeToSkin(theme: string | undefined): Skin {
|
||||
if (!theme) return Skin.Silu;
|
||||
const key = theme.trim();
|
||||
return THEME_TO_SKIN[key] ?? THEME_TO_SKIN[key.toLowerCase()] ?? Skin.Silu;
|
||||
}
|
||||
|
||||
export enum GameState {
|
||||
Run = 0,
|
||||
ResultWin = 1,
|
||||
@@ -68,7 +97,10 @@ function buildMoveTable(entries: [MoverRole, GridType, GridType, boolean, number
|
||||
return m;
|
||||
}
|
||||
|
||||
/** 单人 moveCondition */
|
||||
/**
|
||||
* 单人 moveCondition — 基于 Unity Define.cs,并扩展骑乘 → Jump 跳跃
|
||||
* 判定入口:gameplay/MoveRules.checkMoveStep
|
||||
*/
|
||||
export const moveCondition = buildMoveTable([
|
||||
['player', GridType.Across, GridType.Across, false, 1],
|
||||
['player', GridType.Jump, GridType.Across, false, 1],
|
||||
@@ -84,6 +116,8 @@ export const moveCondition = buildMoveTable([
|
||||
['player', GridType.Jump, GridType.Jump, true, 1],
|
||||
['player', GridType.Ride, GridType.None, false, 1],
|
||||
['player', GridType.Ride, GridType.None, true, 1],
|
||||
['player', GridType.Ride, GridType.Jump, true, 1],
|
||||
['player', GridType.None, GridType.Jump, true, 1],
|
||||
['player', GridType.None, GridType.None, false, 1],
|
||||
['player', GridType.None, GridType.None, true, 1],
|
||||
['player', GridType.None, GridType.Across, false, 1],
|
||||
@@ -93,7 +127,7 @@ export const moveCondition = buildMoveTable([
|
||||
['vehicle', GridType.Ride, GridType.None, false, 1],
|
||||
]);
|
||||
|
||||
/** 多人 moveConditionMult(与 Unity Define.moveConditionMult 一致) */
|
||||
/** 多人 moveConditionMult — 与 Unity Define.cs moveConditionMult 逐项一致 */
|
||||
const multEntries: [MoverRole, GridType, GridType, boolean, number][] = [
|
||||
['player', GridType.Across, GridType.Across, false, 1],
|
||||
['player', GridType.Across, GridType.Across, true, 1],
|
||||
@@ -111,6 +145,8 @@ const multEntries: [MoverRole, GridType, GridType, boolean, number][] = [
|
||||
['player', GridType.Jump, GridType.Jump, true, 1],
|
||||
['player', GridType.Ride, GridType.None, false, 1],
|
||||
['player', GridType.Ride, GridType.None, true, 1],
|
||||
['player', GridType.Ride, GridType.Jump, true, 1],
|
||||
['player', GridType.None, GridType.Jump, true, 1],
|
||||
['player', GridType.None, GridType.None, false, 1],
|
||||
['player', GridType.None, GridType.None, true, 1],
|
||||
['player', GridType.None, GridType.Across, false, 1],
|
||||
|
||||
43
assets/scripts/core/EmbeddedView.ts
Normal file
43
assets/scripts/core/EmbeddedView.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Camera, director, find, view, ResolutionPolicy } from 'cc';
|
||||
import { DESIGN_WIDTH, DESIGN_HEIGHT } from './GridConstants';
|
||||
|
||||
/**
|
||||
* 编辑器内嵌左栏:先 setFrameSize 铺满容器,再 FIXED_WIDTH 按宽适配(与 test.001code.com 一致)。
|
||||
* 须配合 loader 在 resize 时调用 view.setFrameSize(clientW, clientH)。
|
||||
*/
|
||||
export function applyEmbeddedDesignResolution(): void {
|
||||
view.resizeWithBrowserSize(true);
|
||||
const frame = view.getFrameSize();
|
||||
if (frame.width <= 0 || frame.height <= 0) {
|
||||
view.setDesignResolutionSize(DESIGN_WIDTH, DESIGN_HEIGHT, ResolutionPolicy.FIXED_WIDTH);
|
||||
syncEmbeddedCamerasOrtho();
|
||||
return;
|
||||
}
|
||||
view.setDesignResolutionSize(DESIGN_WIDTH, DESIGN_HEIGHT, ResolutionPolicy.FIXED_WIDTH);
|
||||
syncEmbeddedCamerasOrtho();
|
||||
}
|
||||
|
||||
/** 内嵌画布当前可视半高(设计坐标) */
|
||||
export function getEmbeddedOrthoHalf(): number {
|
||||
const vis = view.getVisibleSize();
|
||||
return (vis.height > 0 ? vis.height : DESIGN_HEIGHT) / 2;
|
||||
}
|
||||
|
||||
/** 主相机 / 背景 / HUD 相机正交高度须与可视高度一致,否则关卡与 HUD 错位 */
|
||||
export function syncEmbeddedCamerasOrtho(): void {
|
||||
const halfH = getEmbeddedOrthoHalf();
|
||||
const scene = director.getScene();
|
||||
if (!scene) return;
|
||||
for (const camName of ['UICamera', 'BgCamera', 'Main Camera']) {
|
||||
const cam = find(camName, scene)?.getComponent(Camera);
|
||||
if (cam) cam.orthoHeight = halfH;
|
||||
}
|
||||
const mainNode = find('Main Camera', scene);
|
||||
if (mainNode) mainNode.setPosition(0, 0, mainNode.position.z);
|
||||
(globalThis as { __tfrhSyncHudOrtho?: () => void }).__tfrhSyncHudOrtho = syncEmbeddedCamerasOrtho;
|
||||
}
|
||||
|
||||
/** @deprecated 使用 syncEmbeddedCamerasOrtho */
|
||||
export function syncHudCameraOrtho(): void {
|
||||
syncEmbeddedCamerasOrtho();
|
||||
}
|
||||
9
assets/scripts/core/EmbeddedView.ts.meta
Normal file
9
assets/scripts/core/EmbeddedView.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "44ae1fbb-1f7b-4afc-b4c7-66938cd25e5d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
33
assets/scripts/core/GridConstants.ts
Normal file
33
assets/scripts/core/GridConstants.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/** 等距网格常量(独立模块,避免 AppBootstrap ↔ GridCoords 循环依赖) */
|
||||
export const CELL_PIXEL = 100;
|
||||
|
||||
/** Unity Player.prefab moveSpeed=1.2、Vehicle.prefab=1,按格子像素等比换算 */
|
||||
export const UNITY_PLAYER_MOVE_SPEED = 1.2;
|
||||
export const UNITY_VEHICLE_MOVE_SPEED = 1;
|
||||
/** Unity PlayerController 跳上 JumpBlock 时 targetPosition.y += 0.15 */
|
||||
export const UNITY_JUMP_ARC_OFFSET = 0.15;
|
||||
|
||||
/** 移动中拾取:步进进度达到该比例且进入道具格后才消失(0–1) */
|
||||
export const PROP_COLLECT_MOVE_PROGRESS = 0.62;
|
||||
|
||||
/** 移动中拾取:角色与道具世界距离上限(相对 CELL_PIXEL) */
|
||||
export const PROP_COLLECT_TOUCH_RADIUS = 0.42;
|
||||
|
||||
export function scaledJumpArcOffset(): number {
|
||||
return UNITY_JUMP_ARC_OFFSET * CELL_PIXEL;
|
||||
}
|
||||
|
||||
export function scaledMoveSpeed(unitySpeed: number): number {
|
||||
return unitySpeed * CELL_PIXEL;
|
||||
}
|
||||
|
||||
/** 与 Unity Web 模板 canvas 一致(Template/index.html 960×600) */
|
||||
export const DESIGN_WIDTH = 960;
|
||||
export const DESIGN_HEIGHT = 600;
|
||||
|
||||
/** Unity Main Camera orthographicSize=5 → 半高 500px(世界尺度,不随设计分辨率变) */
|
||||
export const CAMERA_ORTHO_HALF = 500;
|
||||
|
||||
export function getHalfCellSize(): { halfW: number; halfH: number } {
|
||||
return { halfW: CELL_PIXEL * 0.5, halfH: CELL_PIXEL * 0.25 };
|
||||
}
|
||||
9
assets/scripts/core/GridConstants.ts.meta
Normal file
9
assets/scripts/core/GridConstants.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f0421cda-510d-4b04-9943-8ed38412858f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
88
assets/scripts/core/GridCoords.ts
Normal file
88
assets/scripts/core/GridCoords.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Vec3 } from 'cc';
|
||||
import { CELL_PIXEL } from './GridConstants';
|
||||
|
||||
/**
|
||||
* 与 Unity Grid 一致:CellLayout Isometric Z-as-Y,CellSize (1, 0.5, 1)。
|
||||
* 缩放后 halfW=CELL/2, halfH=CELL/4。
|
||||
*/
|
||||
const HALF_W = CELL_PIXEL * 0.5;
|
||||
const HALF_H = CELL_PIXEL * 0.25;
|
||||
|
||||
if (!Number.isFinite(HALF_W) || HALF_W <= 0) {
|
||||
console.error('[GridCoords] CELL_PIXEL 无效,请检查 GridConstants 模块加载');
|
||||
}
|
||||
|
||||
export function cellToWorld(cell: Vec3): Vec3 {
|
||||
const x = cell.x;
|
||||
const y = cell.y;
|
||||
return new Vec3((x - y) * HALF_W, (x + y) * HALF_H, 0);
|
||||
}
|
||||
|
||||
/** Unity Tilemap tileAnchor (0.5,0.5):精灵 pivot 对齐格子中心 */
|
||||
export function cellToWorldCenter(cell: Vec3): Vec3 {
|
||||
const w = cellToWorld(cell);
|
||||
return new Vec3(w.x, w.y + HALF_H, 0);
|
||||
}
|
||||
|
||||
export function worldToCell(world: Vec3): Vec3 {
|
||||
const cx = (world.y / HALF_H + world.x / HALF_W) * 0.5;
|
||||
const cy = (world.y / HALF_H - world.x / HALF_W) * 0.5;
|
||||
return new Vec3(Math.round(cx), Math.round(cy), 0);
|
||||
}
|
||||
|
||||
/** 输入为世界坐标下的格子中心(与 Tilemap tileAnchor 一致) */
|
||||
export function worldCenterToCell(world: Vec3): Vec3 {
|
||||
return worldToCell(new Vec3(world.x, world.y - HALF_H, world.z));
|
||||
}
|
||||
|
||||
/** 世界坐标吸附到最近格子中心 */
|
||||
export function snapWorldToCellCenter(world: Vec3, out?: Vec3): Vec3 {
|
||||
const cell = worldToCell(world);
|
||||
const snapped = cellToWorld(cell);
|
||||
if (out) {
|
||||
out.set(snapped);
|
||||
return out;
|
||||
}
|
||||
return snapped;
|
||||
}
|
||||
|
||||
export function formatCellKey(x: number, y: number): string {
|
||||
return `${x},${y}`;
|
||||
}
|
||||
|
||||
export function parseCellKey(key: string): { x: number; y: number } | null {
|
||||
const parts = key.split(',');
|
||||
if (parts.length !== 2) return null;
|
||||
const x = parseInt(parts[0], 10);
|
||||
const y = parseInt(parts[1], 10);
|
||||
if (Number.isNaN(x) || Number.isNaN(y)) return null;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/** 烘焙瓦片节点名 g_1_0 / b_-2_1 → 格子坐标 */
|
||||
export function parseTileNodeName(name: string | undefined | null): { layer: 'ground' | 'border'; x: number; y: number } | null {
|
||||
if (!name) return null;
|
||||
const m = name.match(/^([gb])_(-?\d+)_(-?\d+)$/);
|
||||
if (!m) return null;
|
||||
return {
|
||||
layer: m[1] === 'g' ? 'ground' : 'border',
|
||||
x: parseInt(m[2], 10),
|
||||
y: parseInt(m[3], 10),
|
||||
};
|
||||
}
|
||||
|
||||
export function tileNodeName(layer: 'ground' | 'border', x: number, y: number): string {
|
||||
const p = layer === 'ground' ? 'g' : 'b';
|
||||
return `${p}_${x}_${y}`;
|
||||
}
|
||||
|
||||
export function getHalfCellSize(): { halfW: number; halfH: number } {
|
||||
return { halfW: HALF_W, halfH: HALF_H };
|
||||
}
|
||||
|
||||
/** 等距绘制深度(连续值,移动插值用;与 compareIsoDrawOrder 配套) */
|
||||
export function isoDrawDepthFromWorld(world: Vec3): { x: number; y: number } {
|
||||
const cx = (world.y / HALF_H + world.x / HALF_W) * 0.5;
|
||||
const cy = (world.y / HALF_H - world.x / HALF_W) * 0.5;
|
||||
return { x: cx, y: cy };
|
||||
}
|
||||
9
assets/scripts/core/GridCoords.ts.meta
Normal file
9
assets/scripts/core/GridCoords.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4eaa5ece-8094-453e-a1e1-3b3bbe9162f0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
23
assets/scripts/core/ResourcesBundle.ts
Normal file
23
assets/scripts/core/ResourcesBundle.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { assetManager } from 'cc';
|
||||
|
||||
const RESOURCES_BUNDLE = 'resources';
|
||||
|
||||
let bundlePromise: Promise<assetManager.Bundle> | null = null;
|
||||
|
||||
/** 拆分分包后须先 loadBundle('resources'),resources.load 才可用 */
|
||||
export function ensureResourcesBundle(): Promise<assetManager.Bundle> {
|
||||
const existing = assetManager.getBundle(RESOURCES_BUNDLE);
|
||||
if (existing) return Promise.resolve(existing);
|
||||
if (bundlePromise) return bundlePromise;
|
||||
bundlePromise = new Promise((resolve, reject) => {
|
||||
assetManager.loadBundle(RESOURCES_BUNDLE, (err, bundle) => {
|
||||
bundlePromise = null;
|
||||
if (err || !bundle) {
|
||||
reject(err ?? new Error(`bundle "${RESOURCES_BUNDLE}" unavailable`));
|
||||
return;
|
||||
}
|
||||
resolve(bundle);
|
||||
});
|
||||
});
|
||||
return bundlePromise;
|
||||
}
|
||||
9
assets/scripts/core/ResourcesBundle.ts.meta
Normal file
9
assets/scripts/core/ResourcesBundle.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "db5967b6-5ed9-4793-9b53-486850d4efce",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user