74 lines
2.9 KiB
JavaScript
74 lines
2.9 KiB
JavaScript
// ASCII-map parser for Wolfenstein levels — the tools/bloxorzMap.js role.
|
|
// Produces the exact level JSON schema the editor loads/saves/exports and
|
|
// the runtime loader consumes, so a level authored here and one exported
|
|
// from the in-browser editor are indistinguishable to WolfensteinLogic.
|
|
//
|
|
// Legend:
|
|
// # wall, type 1 (stone) 2 3 4 wall, type 2/3/4 (palette variety)
|
|
// . floor D door (closed by default)
|
|
// S player start (facing east / 0°) X level exit
|
|
// E guard enemy spawn (facing west)
|
|
// A ammo pickup H health pickup
|
|
//
|
|
// Every row must be the same length; the outer ring must be entirely wall/door
|
|
// (auditLevel below hard-rejects an open boundary or an unreachable exit —
|
|
// the same BFS-solvability gate tools/genBloxorz.js uses).
|
|
|
|
import { buildLevelModel, validateLevel } from '../src/games/wolfenstein/WolfensteinLogic.js';
|
|
|
|
const WALL_CHARS = { '#': 1, 2: 2, 3: 3, 4: 4 };
|
|
|
|
export function parseMap(id, name, art, opts = {}) {
|
|
const lines = art.split('\n').map((l) => l.replace(/\r$/, '')).filter((l) => l.length > 0);
|
|
const height = lines.length;
|
|
const width = lines[0]?.length ?? 0;
|
|
for (const l of lines) {
|
|
if (l.length !== width) throw new Error(`level ${id}: inconsistent row width (expected ${width}, got ${l.length}: "${l}")`);
|
|
}
|
|
|
|
const walls = [];
|
|
const doors = [];
|
|
const enemies = [];
|
|
const items = [];
|
|
let playerStart = null;
|
|
let exit = null;
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
const row = [];
|
|
for (let x = 0; x < width; x++) {
|
|
const ch = lines[y][x];
|
|
if (WALL_CHARS[ch]) { row.push(WALL_CHARS[ch]); continue; }
|
|
row.push(0);
|
|
if (ch === 'D') doors.push({ x, y, orientation: 'vertical' });
|
|
else if (ch === 'S') playerStart = { x: x + 0.5, y: y + 0.5, angle: 0 };
|
|
else if (ch === 'X') exit = { x: x + 0.5, y: y + 0.5, radius: 0.6 };
|
|
else if (ch === 'E') enemies.push({ type: 'guard', x: x + 0.5, y: y + 0.5, facing: 180 });
|
|
else if (ch === 'A') items.push({ type: 'ammo', x: x + 0.5, y: y + 0.5 });
|
|
else if (ch === 'H') items.push({ type: 'health', x: x + 0.5, y: y + 0.5 });
|
|
// '.' and anything else falls through as plain floor.
|
|
}
|
|
walls.push(row);
|
|
}
|
|
|
|
if (!playerStart) throw new Error(`level ${id}: missing S (player start)`);
|
|
if (!exit) throw new Error(`level ${id}: missing X (exit)`);
|
|
|
|
return {
|
|
version: 1, id, name,
|
|
campaignId: opts.campaignId ?? null, missionIndex: opts.missionIndex ?? 0,
|
|
width, height, cellSize: 64,
|
|
walls, playerStart, doors, enemies, items, exit,
|
|
briefing: opts.briefing ?? [],
|
|
};
|
|
}
|
|
|
|
/** Hard-rejects an invalid or unsolvable level — the build-time gate. */
|
|
export function auditLevel(level) {
|
|
const model = buildLevelModel(level);
|
|
const result = validateLevel(model);
|
|
if (!result.valid) {
|
|
throw new Error(`level ${level.id} failed validation:\n ${result.issues.join('\n ')}`);
|
|
}
|
|
return result;
|
|
}
|