'use strict'; /** 与 Unity Levels*.cs / levels-database.json spawns 一致 */ const { normalizeSpawnScale, clampScale, DEFAULT_SPAWN_SCALE } = require('./entity-spawn-defaults'); const { normalizeTexturePath } = require('./entity-texture-presets'); const SPAWN_KIND_LABELS = { player: '玩家', prop: '可拾取物', vehicle: '载具', }; const DIRECTIONS = [ 'Direction.North', 'Direction.East', 'Direction.South', 'Direction.West', ]; function ensureSpawns(state) { if (!state.config) return []; if (!Array.isArray(state.config.spawns)) state.config.spawns = []; return state.config.spawns; } function spawnsAt(state, x, y) { return ensureSpawns(state).filter((s) => s.x === x && s.y === y); } function findSpawnAt(state, x, y) { return spawnsAt(state, x, y)[0] || null; } function countByKind(state, kind) { return ensureSpawns(state).filter((s) => s.kind === kind).length; } function getPlayerSpawn(state) { return ensureSpawns(state).find((s) => s.kind === 'player') || null; } function getVehicleSpawn(state) { return ensureSpawns(state).find((s) => s.kind === 'vehicle') || null; } function applyScaleField(entry, scale) { const s = normalizeSpawnScale(scale); if (s !== undefined) entry.scale = s; else delete entry.scale; } function setPlayerSpawn(state, x, y, direction, scale) { const prev = getPlayerSpawn(state); const rest = ensureSpawns(state).filter((s) => s.kind !== 'player'); const entry = { x, y, kind: 'player', playerDirection: direction || 'Direction.South', }; const sc = scale !== undefined ? scale : prev?.scale ?? DEFAULT_SPAWN_SCALE.player; applyScaleField(entry, sc); rest.push(entry); state.config.spawns = rest; return entry; } function setVehicleSpawn(state, x, y, direction, scale) { const prev = getVehicleSpawn(state); const rest = ensureSpawns(state).filter((s) => s.kind !== 'vehicle'); const entry = { x, y, kind: 'vehicle', vehicleDirection: direction || 'Direction.North', }; const sc = scale !== undefined ? scale : prev?.scale ?? DEFAULT_SPAWN_SCALE.vehicle; applyScaleField(entry, sc); rest.push(entry); state.config.spawns = rest; return entry; } function clearVehicleSpawn(state) { const before = countByKind(state, 'vehicle'); state.config.spawns = ensureSpawns(state).filter((s) => s.kind !== 'vehicle'); return before > 0; } /** 载具 0 或 1:再次点击已有载具格则清除 */ function vehicleToggleInternal(state, x, y, direction, scale) { const existing = getVehicleSpawn(state); if (existing && existing.x === x && existing.y === y) { clearVehicleSpawn(state); return { changed: false, spawn: null, removed: true }; } const spawn = setVehicleSpawn(state, x, y, direction, scale); return { changed: true, spawn, removed: false }; } function toggleVehicleSpawn(state, x, y, direction, scale) { const r = vehicleToggleInternal(state, x, y, direction, scale); return r.changed; } function toggleVehicleSpawnDetailed(state, x, y, direction, scale) { return vehicleToggleInternal(state, x, y, direction, scale); } function togglePropSpawn(state, x, y, scale, propPlacement) { const spawns = ensureSpawns(state); const idx = spawns.findIndex((s) => s.x === x && s.y === y && s.kind === 'prop'); if (idx >= 0) { spawns.splice(idx, 1); return { changed: false, spawn: null, removed: true }; } const entry = { x, y, kind: 'prop' }; if (propPlacement === 'ground') entry.propPlacement = 'ground'; const sc = scale !== undefined ? scale : DEFAULT_SPAWN_SCALE.prop; applyScaleField(entry, sc); spawns.push(entry); return { changed: true, spawn: entry, removed: false }; } function removeSpawnAt(state, x, y) { const spawns = ensureSpawns(state); const before = spawns.length; state.config.spawns = spawns.filter((s) => !(s.x === x && s.y === y)); return state.config.spawns.length < before; } function removeSpawnRef(state, spawnRef) { if (!spawnRef) return false; const spawns = ensureSpawns(state); const idx = spawns.indexOf(spawnRef); if (idx < 0) return false; spawns.splice(idx, 1); return true; } function pickSpawnAtCell(state, x, y, preferKind) { const hits = spawnsAt(state, x, y); if (!hits.length) return null; if (preferKind) { const m = hits.find((s) => s.kind === preferKind); if (m) return m; } return hits[0]; } function spawnStillExists(state, spawnRef) { if (!spawnRef) return false; return ensureSpawns(state).includes(spawnRef); } function updateSpawnEntry(state, spawnRef, patch) { const spawns = ensureSpawns(state); const idx = spawns.indexOf(spawnRef); if (idx < 0) return false; const cur = { ...spawns[idx] }; if (patch.x !== undefined) cur.x = parseInt(patch.x, 10); if (patch.y !== undefined) cur.y = parseInt(patch.y, 10); if (patch.playerDirection !== undefined) cur.playerDirection = patch.playerDirection; if (patch.vehicleDirection !== undefined) cur.vehicleDirection = patch.vehicleDirection; if (patch.scale !== undefined) applyScaleField(cur, patch.scale); if (patch.texture !== undefined) { const t = normalizeTexturePath(patch.texture); if (t) cur.texture = t; else delete cur.texture; } spawns[idx] = cur; return true; } function formatSpawnScale(spawn) { if (spawn?.scale !== undefined && spawn.scale !== null) return spawn.scale; return 1; } function validateSpawns(state) { const spawns = ensureSpawns(state); const players = spawns.filter((s) => s.kind === 'player'); const vehicles = spawns.filter((s) => s.kind === 'vehicle'); const props = spawns.filter((s) => s.kind === 'prop'); const errors = []; if (players.length !== 1) { errors.push(`玩家必须恰好 1 个(当前 ${players.length})`); } if (vehicles.length > 1) { errors.push(`载具最多 1 个(当前 ${vehicles.length})`); } if (props.length < 1) { errors.push(`可拾取物至少 1 个(当前 ${props.length})`); } return { ok: errors.length === 0, errors, players, vehicles, props }; } function spawnSummary(state) { const v = validateSpawns(state); const player = v.players[0]; const vehicle = v.vehicles[0]; const parts = []; if (player) { const sc = formatSpawnScale(player); parts.push(`玩家 (${player.x},${player.y}) ${player.playerDirection || ''}${sc !== 1 ? ` ×${sc}` : ''}`); } else { parts.push('玩家 未设置'); } parts.push(`载具 ${v.vehicles.length}/1`); if (vehicle) { const sc = formatSpawnScale(vehicle); parts.push(`@(${vehicle.x},${vehicle.y}) ${vehicle.vehicleDirection || ''}${sc !== 1 ? ` ×${sc}` : ''}`); } parts.push(`可拾取物 ${v.props.length} 个`); if (!v.ok) parts.push('[待完善]'); return parts.join(' · '); } function validationHint(state) { const v = validateSpawns(state); if (v.ok) return ''; return v.errors.join(';'); } module.exports = { SPAWN_KIND_LABELS, DIRECTIONS, DEFAULT_SPAWN_SCALE, clampScale, ensureSpawns, spawnsAt, findSpawnAt, countByKind, getPlayerSpawn, getVehicleSpawn, setPlayerSpawn, setVehicleSpawn, clearVehicleSpawn, toggleVehicleSpawn, toggleVehicleSpawnDetailed, togglePropSpawn, removeSpawnAt, removeSpawnRef, pickSpawnAtCell, spawnStillExists, updateSpawnEntry, formatSpawnScale, validateSpawns, spawnSummary, validationHint, };