From f8aa4e2eab914d3ccc77338627d40513b49c7d3e Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Fri, 20 Mar 2026 17:20:19 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20make=20spawn=20optional=20=E2=80=94=20on?= =?UTF-8?q?ly=20required=20for=20initial=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- editor.html | 65 ++++++++++++++++++++++++++++++++---------------- js/world/maps.js | 4 +-- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/editor.html b/editor.html index 91bb2a7..65fa30e 100644 --- a/editor.html +++ b/editor.html @@ -224,7 +224,7 @@ function init() { // Init map data for all maps 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 @@ -283,7 +283,7 @@ function loadCurrentGameData() { 11: [0,1,2,3,6,7,8,9] }; 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 = [ { 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) { currentMapId = id; selectedEntity = null; - // Center camera on spawn + // Center camera on spawn or map center const md = mapData[id]; const cfg = mapConfigs[id]; if (md && cfg) { - camera.x = -(md.spawn.x * TILE_PX - canvas.width / 2 + TILE_PX / 2); - camera.y = -(md.spawn.y * TILE_PX - canvas.height / 2 + TILE_PX / 2); + const cx = md.spawn ? md.spawn.x : Math.floor(cfg.widthTiles / 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(); updateProps(); @@ -419,14 +421,17 @@ function render() { ctx.stroke(); } - // Spawn - const sp = md.spawn; - ctx.fillStyle = 'rgba(0, 229, 153, 0.4)'; - ctx.fillRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX); - ctx.strokeStyle = '#00e599'; - ctx.lineWidth = 2; - ctx.strokeRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX); - drawLabel(ctx, '🏠', sp.x, sp.y); + // Spawn (optional β€” only needed on the starting map) + if (md.spawn) { + const sp = md.spawn; + const spSel = selectedEntity?.type === 'spawn'; + ctx.fillStyle = 'rgba(0, 229, 153, 0.4)'; + ctx.fillRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX); + ctx.strokeStyle = spSel ? '#fff' : '#00e599'; + ctx.lineWidth = spSel ? 2.5 : 2; + ctx.strokeRect(sp.x * TILE_PX, sp.y * TILE_PX, TILE_PX, TILE_PX); + drawLabel(ctx, '🏠', sp.x, sp.y); + } // Exits md.exits.forEach((e, i) => { @@ -548,8 +553,15 @@ function onMouseDown(e) { render(); break; case 'spawn': - md.spawn = { x: tile.x, y: tile.y }; - updateEntityList(); render(); + // 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 }; + selectedEntity = { type: 'spawn', index: 0 }; + } + updateEntityList(); updateProps(); render(); break; case 'npc': 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}`; let info = ''; 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.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} `; }); @@ -655,7 +667,8 @@ function onKeyDown(e) { e.preventDefault(); const md = getData(); 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 === 'interaction') md.interactions.splice(index, 1); selectedEntity = null; @@ -731,13 +744,15 @@ function selectEntityAt(tx, ty) { idx = md.interactions.findIndex(i => i.x === tx && i.y === ty); if (idx >= 0) { selectedEntity = { type: 'interaction', index: idx }; updateEntityList(); updateProps(); return; } // 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; updateEntityList(); updateProps(); } function deleteEntityAt(tx, ty) { 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); if (idx >= 0) { md.npcs.splice(idx, 1); selectedEntity = null; updateEntityList(); updateProps(); return; } idx = md.exits.findIndex(e => e.x === tx && e.y === ty); @@ -751,7 +766,9 @@ function deleteEntityAt(tx, ty) { function updateEntityList() { 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) + : '
None (use 🏠 tool to place)
'; document.getElementById('npc-list').innerHTML = md.npcs.map((n, i) => makeEntityItem('npc', i, 'πŸ‘€', n.id, n.x, n.y, '#cc55ff') @@ -799,7 +816,7 @@ function getSelectedEntityData() { if (!selectedEntity) return null; const md = getData(); switch (selectedEntity.type) { - case 'spawn': return md.spawn; + case 'spawn': return md.spawn || null; case 'npc': return md.npcs[selectedEntity.index]; case 'exit': return md.exits[selectedEntity.index]; case 'interaction': return md.interactions[selectedEntity.index]; @@ -954,7 +971,9 @@ function generateMapsJS() { out += ` image: '${imageKey}',\n`; out += ` widthTiles: ${cfg.widthTiles},\n`; out += ` heightTiles: ${cfg.heightTiles},\n`; - out += ` spawn: { x: ${md.spawn.x}, y: ${md.spawn.y} },\n`; + if (md.spawn) { + out += ` spawn: { x: ${md.spawn.x}, y: ${md.spawn.y} },\n`; + } out += ` wallSet: buildWallSet(${varName(mapId)}Walls),\n\n`; // Exits @@ -1154,10 +1173,12 @@ function parseAndLoadMapsJS(src) { 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+)`)); if (spawnMatch) { mapData[mapId].spawn = { x: parseInt(spawnMatch[1]), y: parseInt(spawnMatch[2]) }; + } else { + mapData[mapId].spawn = null; } // Parse NPCs - find the npcs array diff --git a/js/world/maps.js b/js/world/maps.js index 9428cca..33ef076 100644 --- a/js/world/maps.js +++ b/js/world/maps.js @@ -28,7 +28,7 @@ const labMap = { image: 'map:lab', widthTiles: 10, heightTiles: 12, - spawn: { x: 4, y: 10 }, + // No spawn β€” player enters via door from town wallSet: buildWallSet(labWalls), exits: [ @@ -110,7 +110,6 @@ const houseA1fMap = { image: 'map:house-a-1f', widthTiles: 8, heightTiles: 8, - spawn: { x: 0, y: 0 }, wallSet: buildWallSet(houseA1fWalls), exits: [ @@ -134,7 +133,6 @@ const route1Map = { image: 'map:route-1', widthTiles: 20, heightTiles: 36, - spawn: { x: 0, y: 0 }, wallSet: buildWallSet(route1Walls), exits: [