Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
6.5 KiB
TypeScript
186 lines
6.5 KiB
TypeScript
import { Vec3 } from 'cc';
|
||
import { CELL_PIXEL } from '../core/GridConstants';
|
||
import { CommonDefine, MoverRole } from '../core/Define';
|
||
import { cellToWorldCenter, worldCenterToCell } from '../core/GridCoords';
|
||
import {
|
||
getThemePropPlacementOffsets,
|
||
getThemeMoverEmptyCellYOffset,
|
||
getThemeMoverJumpCellYOffset,
|
||
getThemePlayerStandYOffset,
|
||
DEFAULT_PROP_BLOCK_Y_OFFSET,
|
||
DEFAULT_PROP_GROUND_Y_OFFSET,
|
||
} from '../theme/ThemeDatabase';
|
||
import { getTilePivot } from '../visual/TilePivots';
|
||
import { getTileDrawSize } from '../visual/TileSizes';
|
||
import { LevelConfig, SpawnConfig } from './LevelTypes';
|
||
|
||
/** 与 Unity Prop / nProp 一致:砖块上偏高,空地偏低 */
|
||
export type PropPlacement = 'block' | 'ground';
|
||
|
||
const HALF_H = CELL_PIXEL * 0.25;
|
||
|
||
/** Unity checkPoint 偏移(PPU≈100):Prop ≈ +14px,nProp ≈ -11px */
|
||
export const PROP_BLOCK_Y_OFFSET = DEFAULT_PROP_BLOCK_Y_OFFSET;
|
||
export const PROP_GROUND_Y_OFFSET = DEFAULT_PROP_GROUND_Y_OFFSET;
|
||
|
||
export function cellHasTile(cellX: number, cellY: number, config: LevelConfig | undefined): boolean {
|
||
if (!config) return false;
|
||
const key = `${cellX},${cellY}`;
|
||
const hasGround = !!config.ground?.[key];
|
||
const hasBorder = config.border?.[key] !== undefined && config.border?.[key] !== false;
|
||
return hasGround || hasBorder;
|
||
}
|
||
|
||
/** 砖块 pivot 对齐格心后,行走面相对格心的抬高量(与 Baseblock 差值用于 JumpBlock) */
|
||
function tileWalkSurfaceAboveCenter(tileName: string, theme?: string): number {
|
||
const draw = getTileDrawSize(tileName, undefined, undefined, theme);
|
||
const pivot = getTilePivot(tileName, theme);
|
||
return draw.height * (1 - pivot.y);
|
||
}
|
||
|
||
/** JumpBlock 比 Baseblock 更高的行走面补偿(px) */
|
||
export function resolveMoverJumpCellYOffset(
|
||
cellX: number,
|
||
cellY: number,
|
||
config: LevelConfig | undefined,
|
||
theme?: string,
|
||
): number {
|
||
const key = `${cellX},${cellY}`;
|
||
const tile = config?.ground?.[key];
|
||
if (tile !== CommonDefine.BlockJump) return 0;
|
||
const auto = tileWalkSurfaceAboveCenter(CommonDefine.BlockJump, theme)
|
||
- tileWalkSurfaceAboveCenter(CommonDefine.BlockBase, theme);
|
||
return auto + getThemeMoverJumpCellYOffset(theme);
|
||
}
|
||
|
||
/** 角色/载具:JumpBlock 抬高;有普通砖为 0;空地/载具格补齐 Baseblock 高度 */
|
||
export function resolveMoverCellStandYOffset(
|
||
cellX: number,
|
||
cellY: number,
|
||
config: LevelConfig | undefined,
|
||
theme?: string,
|
||
): number {
|
||
if (!cellHasTile(cellX, cellY, config)) {
|
||
return getThemeMoverEmptyCellYOffset(theme);
|
||
}
|
||
return resolveMoverJumpCellYOffset(cellX, cellY, config, theme);
|
||
}
|
||
|
||
export function entityMoverWorldPosition(
|
||
cell: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme?: string,
|
||
): Vec3 {
|
||
const pos = cellToWorldCenter(cell);
|
||
pos.y += resolveMoverCellStandYOffset(cell.x, cell.y, config, theme);
|
||
return pos;
|
||
}
|
||
|
||
/**
|
||
* 从带站立/空地 Y 微调的世界坐标反推逻辑格(与 entityMoverWorldPosition 互逆)。
|
||
* 视觉 Y 会干扰等距 worldToCell,移动采样须先还原到格心平面。
|
||
*/
|
||
export function worldToMoverCell(
|
||
world: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme?: string,
|
||
forPlayer = false,
|
||
): Vec3 {
|
||
let cell = worldCenterToCell(world);
|
||
for (let i = 0; i < 4; i++) {
|
||
const off = resolveMoverCellStandYOffset(cell.x, cell.y, config, theme);
|
||
const stand = forPlayer ? getThemePlayerStandYOffset(theme) : 0;
|
||
const adjustedY = world.y - stand - off;
|
||
const next = worldCenterToCell(new Vec3(world.x, adjustedY, world.z));
|
||
if (next.x === cell.x && next.y === cell.y) break;
|
||
cell = next;
|
||
}
|
||
return cell;
|
||
}
|
||
|
||
/**
|
||
* 移动/格子判定用的逻辑采样点(格心,不含主题站立 Y)。
|
||
* @param levelLocal 关卡根节点本地坐标(与砖块 alignTileNode 同一空间)
|
||
*/
|
||
export function moverLogicalGridPosition(
|
||
levelLocal: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme?: string,
|
||
forPlayer = false,
|
||
): Vec3 {
|
||
return cellToWorldCenter(worldToMoverCell(levelLocal, config, theme, forPlayer));
|
||
}
|
||
|
||
/** 角色生成/移动位置(含主题站立 Y 微调) */
|
||
export function entityPlayerWorldPosition(
|
||
cell: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme?: string,
|
||
): Vec3 {
|
||
const pos = entityMoverWorldPosition(cell, config, theme);
|
||
pos.y += getThemePlayerStandYOffset(theme);
|
||
return pos;
|
||
}
|
||
|
||
/** 角色/载具是否按玩家站立 Y 反推逻辑格(与 worldToMoverCell / entityPlayerWorldPosition 一致) */
|
||
export function roleUsesPlayerStandOffset(role: MoverRole): boolean {
|
||
return role === 'player';
|
||
}
|
||
|
||
export function worldToMoverCellForRole(
|
||
world: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme: string | undefined,
|
||
role: MoverRole,
|
||
): Vec3 {
|
||
return worldToMoverCell(world, config, theme, roleUsesPlayerStandOffset(role));
|
||
}
|
||
|
||
export function moverLogicalGridPositionForRole(
|
||
levelLocal: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme: string | undefined,
|
||
role: MoverRole,
|
||
): Vec3 {
|
||
return moverLogicalGridPosition(
|
||
levelLocal,
|
||
config,
|
||
theme,
|
||
roleUsesPlayerStandOffset(role),
|
||
);
|
||
}
|
||
|
||
/** 逻辑格 → 世界站立点(player 含主题站立 Y,vehicle 为 mover 甲板) */
|
||
export function entityWorldPositionForRole(
|
||
cell: Vec3,
|
||
config: LevelConfig | undefined,
|
||
theme: string | undefined,
|
||
role: MoverRole,
|
||
): Vec3 {
|
||
return roleUsesPlayerStandOffset(role)
|
||
? entityPlayerWorldPosition(cell, config, theme)
|
||
: entityMoverWorldPosition(cell, config, theme);
|
||
}
|
||
|
||
export function propWorldYOffset(placement: PropPlacement, theme?: string): number {
|
||
const offsets = getThemePropPlacementOffsets(theme);
|
||
return placement === 'ground' ? offsets.ground : offsets.block;
|
||
}
|
||
|
||
/** 无 ground/border 视为空地(nProp);有砖块视为 Prop */
|
||
export function resolvePropPlacement(spawn: SpawnConfig, config: LevelConfig): PropPlacement {
|
||
const key = `${spawn.x},${spawn.y}`;
|
||
const hasGround = !!config.ground?.[key];
|
||
const hasBorder = config.border?.[key] !== undefined && config.border?.[key] !== false;
|
||
if (spawn.propPlacement === 'ground' || spawn.propPlacement === 'block') {
|
||
if (!hasGround && !hasBorder) return 'ground';
|
||
return spawn.propPlacement;
|
||
}
|
||
if (!hasGround && !hasBorder) return 'ground';
|
||
return 'block';
|
||
}
|
||
|
||
export function isGroundPropSpawn(spawn: SpawnConfig, config: LevelConfig): boolean {
|
||
return spawn.kind === 'prop' && resolvePropPlacement(spawn, config) === 'ground';
|
||
}
|