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'; }