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:
185
assets/scripts/level/EntitySpawnPlacement.ts
Normal file
185
assets/scripts/level/EntitySpawnPlacement.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user