fix: make spawn optional — only required for initial map
Spawn can now be deleted in the editor (click same tile with Spawn tool, use Delete tool, or press Delete key). Interior maps no longer have spawn objects. The editor shows "None" when no spawn is set, and the generated maps.js omits the spawn field for maps without one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
editor.html
51
editor.html
@@ -224,7 +224,7 @@ function init() {
|
|||||||
|
|
||||||
// Init map data for all maps
|
// Init map data for all maps
|
||||||
for (const id of Object.keys(mapConfigs)) {
|
for (const id of Object.keys(mapConfigs)) {
|
||||||
mapData[id] = { walls: new Set(), spawn: { x: 0, y: 0 }, npcs: [], exits: [], interactions: [] };
|
mapData[id] = { walls: new Set(), spawn: null, npcs: [], exits: [], interactions: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load current game data
|
// Load current game data
|
||||||
@@ -283,7 +283,7 @@ function loadCurrentGameData() {
|
|||||||
11: [0,1,2,3,6,7,8,9]
|
11: [0,1,2,3,6,7,8,9]
|
||||||
};
|
};
|
||||||
mapData.lab.walls = wallDataToSet(labWalls);
|
mapData.lab.walls = wallDataToSet(labWalls);
|
||||||
mapData.lab.spawn = { x: 4, y: 10 };
|
mapData.lab.spawn = null; // No spawn — player enters via door from town
|
||||||
mapData.lab.npcs = [
|
mapData.lab.npcs = [
|
||||||
{ id: 'professor', x: 5, y: 1, facing: 'down', dialog: ['Welcome to the Circuit Lab!','I\'m the Professor. We study logic gates here.','Use the workshop tables to design circuits.','Press TAB to open the Workshop anytime!'] }
|
{ id: 'professor', x: 5, y: 1, facing: 'down', dialog: ['Welcome to the Circuit Lab!','I\'m the Professor. We study logic gates here.','Use the workshop tables to design circuits.','Press TAB to open the Workshop anytime!'] }
|
||||||
];
|
];
|
||||||
@@ -345,12 +345,14 @@ function wallDataToSet(data) {
|
|||||||
function switchMap(id) {
|
function switchMap(id) {
|
||||||
currentMapId = id;
|
currentMapId = id;
|
||||||
selectedEntity = null;
|
selectedEntity = null;
|
||||||
// Center camera on spawn
|
// Center camera on spawn or map center
|
||||||
const md = mapData[id];
|
const md = mapData[id];
|
||||||
const cfg = mapConfigs[id];
|
const cfg = mapConfigs[id];
|
||||||
if (md && cfg) {
|
if (md && cfg) {
|
||||||
camera.x = -(md.spawn.x * TILE_PX - canvas.width / 2 + TILE_PX / 2);
|
const cx = md.spawn ? md.spawn.x : Math.floor(cfg.widthTiles / 2);
|
||||||
camera.y = -(md.spawn.y * TILE_PX - canvas.height / 2 + TILE_PX / 2);
|
const cy = md.spawn ? md.spawn.y : Math.floor(cfg.heightTiles / 2);
|
||||||
|
camera.x = -(cx * TILE_PX - canvas.width / 2 + TILE_PX / 2);
|
||||||
|
camera.y = -(cy * TILE_PX - canvas.height / 2 + TILE_PX / 2);
|
||||||
}
|
}
|
||||||
updateEntityList();
|
updateEntityList();
|
||||||
updateProps();
|
updateProps();
|
||||||
@@ -419,14 +421,17 @@ function render() {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn
|
// Spawn (optional — only needed on the starting map)
|
||||||
|
if (md.spawn) {
|
||||||
const sp = md.spawn;
|
const sp = md.spawn;
|
||||||
|
const spSel = selectedEntity?.type === 'spawn';
|
||||||
ctx.fillStyle = 'rgba(0, 229, 153, 0.4)';
|
ctx.fillStyle = 'rgba(0, 229, 153, 0.4)';
|
||||||
ctx.fillRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX);
|
ctx.fillRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX);
|
||||||
ctx.strokeStyle = '#00e599';
|
ctx.strokeStyle = spSel ? '#fff' : '#00e599';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = spSel ? 2.5 : 2;
|
||||||
ctx.strokeRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX);
|
ctx.strokeRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX);
|
||||||
drawLabel(ctx, '🏠', sp.x, sp.y);
|
drawLabel(ctx, '🏠', sp.x, sp.y);
|
||||||
|
}
|
||||||
|
|
||||||
// Exits
|
// Exits
|
||||||
md.exits.forEach((e, i) => {
|
md.exits.forEach((e, i) => {
|
||||||
@@ -548,8 +553,15 @@ function onMouseDown(e) {
|
|||||||
render();
|
render();
|
||||||
break;
|
break;
|
||||||
case 'spawn':
|
case 'spawn':
|
||||||
|
// Toggle: if spawn already at this tile, remove it; otherwise place/move
|
||||||
|
if (md.spawn && md.spawn.x === tile.x && md.spawn.y === tile.y) {
|
||||||
|
md.spawn = null;
|
||||||
|
selectedEntity = null;
|
||||||
|
} else {
|
||||||
md.spawn = { x: tile.x, y: tile.y };
|
md.spawn = { x: tile.x, y: tile.y };
|
||||||
updateEntityList(); render();
|
selectedEntity = { type: 'spawn', index: 0 };
|
||||||
|
}
|
||||||
|
updateEntityList(); updateProps(); render();
|
||||||
break;
|
break;
|
||||||
case 'npc':
|
case 'npc':
|
||||||
md.npcs.push({ id: `npc_${Date.now()}`, x: tile.x, y: tile.y, facing: 'down', dialog: ['Hello!'] });
|
md.npcs.push({ id: `npc_${Date.now()}`, x: tile.x, y: tile.y, facing: 'down', dialog: ['Hello!'] });
|
||||||
@@ -597,7 +609,7 @@ function onMouseMove(e) {
|
|||||||
const key = `${tile.x},${tile.y}`;
|
const key = `${tile.x},${tile.y}`;
|
||||||
let info = '';
|
let info = '';
|
||||||
if (md.walls.has(key)) info += 'Wall ';
|
if (md.walls.has(key)) info += 'Wall ';
|
||||||
if (md.spawn.x === tile.x && md.spawn.y === tile.y) info += 'Spawn ';
|
if (md.spawn && md.spawn.x === tile.x && md.spawn.y === tile.y) info += 'Spawn ';
|
||||||
md.npcs.forEach(n => { if (n.x === tile.x && n.y === tile.y) info += `NPC:${n.id} `; });
|
md.npcs.forEach(n => { if (n.x === tile.x && n.y === tile.y) info += `NPC:${n.id} `; });
|
||||||
md.exits.forEach(e => { if (e.x === tile.x && e.y === tile.y) info += `Exit→${e.targetMap}(${e.targetX ?? '?'},${e.targetY ?? '?'}) `; });
|
md.exits.forEach(e => { if (e.x === tile.x && e.y === tile.y) info += `Exit→${e.targetMap}(${e.targetX ?? '?'},${e.targetY ?? '?'}) `; });
|
||||||
md.interactions.forEach(i => { if (i.x === tile.x && i.y === tile.y) info += `${i.type}:${i.label} `; });
|
md.interactions.forEach(i => { if (i.x === tile.x && i.y === tile.y) info += `${i.type}:${i.label} `; });
|
||||||
@@ -655,7 +667,8 @@ function onKeyDown(e) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const md = getData();
|
const md = getData();
|
||||||
const { type, index } = selectedEntity;
|
const { type, index } = selectedEntity;
|
||||||
if (type === 'npc') md.npcs.splice(index, 1);
|
if (type === 'spawn') md.spawn = null;
|
||||||
|
else if (type === 'npc') md.npcs.splice(index, 1);
|
||||||
else if (type === 'exit') md.exits.splice(index, 1);
|
else if (type === 'exit') md.exits.splice(index, 1);
|
||||||
else if (type === 'interaction') md.interactions.splice(index, 1);
|
else if (type === 'interaction') md.interactions.splice(index, 1);
|
||||||
selectedEntity = null;
|
selectedEntity = null;
|
||||||
@@ -731,13 +744,15 @@ function selectEntityAt(tx, ty) {
|
|||||||
idx = md.interactions.findIndex(i => i.x === tx && i.y === ty);
|
idx = md.interactions.findIndex(i => i.x === tx && i.y === ty);
|
||||||
if (idx >= 0) { selectedEntity = { type: 'interaction', index: idx }; updateEntityList(); updateProps(); return; }
|
if (idx >= 0) { selectedEntity = { type: 'interaction', index: idx }; updateEntityList(); updateProps(); return; }
|
||||||
// Spawn?
|
// Spawn?
|
||||||
if (md.spawn.x === tx && md.spawn.y === ty) { selectedEntity = { type: 'spawn', index: 0 }; updateEntityList(); updateProps(); return; }
|
if (md.spawn && md.spawn.x === tx && md.spawn.y === ty) { selectedEntity = { type: 'spawn', index: 0 }; updateEntityList(); updateProps(); return; }
|
||||||
selectedEntity = null;
|
selectedEntity = null;
|
||||||
updateEntityList(); updateProps();
|
updateEntityList(); updateProps();
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEntityAt(tx, ty) {
|
function deleteEntityAt(tx, ty) {
|
||||||
const md = getData();
|
const md = getData();
|
||||||
|
// Spawn
|
||||||
|
if (md.spawn && md.spawn.x === tx && md.spawn.y === ty) { md.spawn = null; selectedEntity = null; updateEntityList(); updateProps(); return; }
|
||||||
let idx = md.npcs.findIndex(n => n.x === tx && n.y === ty);
|
let idx = md.npcs.findIndex(n => n.x === tx && n.y === ty);
|
||||||
if (idx >= 0) { md.npcs.splice(idx, 1); selectedEntity = null; updateEntityList(); updateProps(); return; }
|
if (idx >= 0) { md.npcs.splice(idx, 1); selectedEntity = null; updateEntityList(); updateProps(); return; }
|
||||||
idx = md.exits.findIndex(e => e.x === tx && e.y === ty);
|
idx = md.exits.findIndex(e => e.x === tx && e.y === ty);
|
||||||
@@ -751,7 +766,9 @@ function deleteEntityAt(tx, ty) {
|
|||||||
function updateEntityList() {
|
function updateEntityList() {
|
||||||
const md = getData();
|
const md = getData();
|
||||||
|
|
||||||
document.getElementById('spawn-list').innerHTML = makeEntityItem('spawn', 0, '🏠', `Spawn`, md.spawn.x, md.spawn.y);
|
document.getElementById('spawn-list').innerHTML = md.spawn
|
||||||
|
? makeEntityItem('spawn', 0, '🏠', `Spawn`, md.spawn.x, md.spawn.y)
|
||||||
|
: '<div style="padding:4px 16px;color:var(--text2);font-size:11px;">None (use 🏠 tool to place)</div>';
|
||||||
|
|
||||||
document.getElementById('npc-list').innerHTML = md.npcs.map((n, i) =>
|
document.getElementById('npc-list').innerHTML = md.npcs.map((n, i) =>
|
||||||
makeEntityItem('npc', i, '👤', n.id, n.x, n.y, '#cc55ff')
|
makeEntityItem('npc', i, '👤', n.id, n.x, n.y, '#cc55ff')
|
||||||
@@ -799,7 +816,7 @@ function getSelectedEntityData() {
|
|||||||
if (!selectedEntity) return null;
|
if (!selectedEntity) return null;
|
||||||
const md = getData();
|
const md = getData();
|
||||||
switch (selectedEntity.type) {
|
switch (selectedEntity.type) {
|
||||||
case 'spawn': return md.spawn;
|
case 'spawn': return md.spawn || null;
|
||||||
case 'npc': return md.npcs[selectedEntity.index];
|
case 'npc': return md.npcs[selectedEntity.index];
|
||||||
case 'exit': return md.exits[selectedEntity.index];
|
case 'exit': return md.exits[selectedEntity.index];
|
||||||
case 'interaction': return md.interactions[selectedEntity.index];
|
case 'interaction': return md.interactions[selectedEntity.index];
|
||||||
@@ -954,7 +971,9 @@ function generateMapsJS() {
|
|||||||
out += ` image: '${imageKey}',\n`;
|
out += ` image: '${imageKey}',\n`;
|
||||||
out += ` widthTiles: ${cfg.widthTiles},\n`;
|
out += ` widthTiles: ${cfg.widthTiles},\n`;
|
||||||
out += ` heightTiles: ${cfg.heightTiles},\n`;
|
out += ` heightTiles: ${cfg.heightTiles},\n`;
|
||||||
|
if (md.spawn) {
|
||||||
out += ` spawn: { x: ${md.spawn.x}, y: ${md.spawn.y} },\n`;
|
out += ` spawn: { x: ${md.spawn.x}, y: ${md.spawn.y} },\n`;
|
||||||
|
}
|
||||||
out += ` wallSet: buildWallSet(${varName(mapId)}Walls),\n\n`;
|
out += ` wallSet: buildWallSet(${varName(mapId)}Walls),\n\n`;
|
||||||
|
|
||||||
// Exits
|
// Exits
|
||||||
@@ -1154,10 +1173,12 @@ function parseAndLoadMapsJS(src) {
|
|||||||
mapData[mapId].walls = wallDataToSet(wallData);
|
mapData[mapId].walls = wallDataToSet(wallData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse spawn
|
// Parse spawn (optional — only present on initial map)
|
||||||
const spawnMatch = src.match(new RegExp(`${vName}Map[\\s\\S]*?spawn:\\s*\\{\\s*x:\\s*(\\d+)\\s*,\\s*y:\\s*(\\d+)`));
|
const spawnMatch = src.match(new RegExp(`${vName}Map[\\s\\S]*?spawn:\\s*\\{\\s*x:\\s*(\\d+)\\s*,\\s*y:\\s*(\\d+)`));
|
||||||
if (spawnMatch) {
|
if (spawnMatch) {
|
||||||
mapData[mapId].spawn = { x: parseInt(spawnMatch[1]), y: parseInt(spawnMatch[2]) };
|
mapData[mapId].spawn = { x: parseInt(spawnMatch[1]), y: parseInt(spawnMatch[2]) };
|
||||||
|
} else {
|
||||||
|
mapData[mapId].spawn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse NPCs - find the npcs array
|
// Parse NPCs - find the npcs array
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const labMap = {
|
|||||||
image: 'map:lab',
|
image: 'map:lab',
|
||||||
widthTiles: 10,
|
widthTiles: 10,
|
||||||
heightTiles: 12,
|
heightTiles: 12,
|
||||||
spawn: { x: 4, y: 10 },
|
// No spawn — player enters via door from town
|
||||||
wallSet: buildWallSet(labWalls),
|
wallSet: buildWallSet(labWalls),
|
||||||
|
|
||||||
exits: [
|
exits: [
|
||||||
@@ -110,7 +110,6 @@ const houseA1fMap = {
|
|||||||
image: 'map:house-a-1f',
|
image: 'map:house-a-1f',
|
||||||
widthTiles: 8,
|
widthTiles: 8,
|
||||||
heightTiles: 8,
|
heightTiles: 8,
|
||||||
spawn: { x: 0, y: 0 },
|
|
||||||
wallSet: buildWallSet(houseA1fWalls),
|
wallSet: buildWallSet(houseA1fWalls),
|
||||||
|
|
||||||
exits: [
|
exits: [
|
||||||
@@ -134,7 +133,6 @@ const route1Map = {
|
|||||||
image: 'map:route-1',
|
image: 'map:route-1',
|
||||||
widthTiles: 20,
|
widthTiles: 20,
|
||||||
heightTiles: 36,
|
heightTiles: 36,
|
||||||
spawn: { x: 0, y: 0 },
|
|
||||||
wallSet: buildWallSet(route1Walls),
|
wallSet: buildWallSet(route1Walls),
|
||||||
|
|
||||||
exits: [
|
exits: [
|
||||||
|
|||||||
Reference in New Issue
Block a user