Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
253 lines
7.6 KiB
JavaScript
253 lines
7.6 KiB
JavaScript
'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,
|
||
};
|