feat: return-to-door system for map exits
When entering an interior (e.g. lab from town), the game saves the player's current position as a return point. When exiting, if the exit has no explicit targetX/targetY, the system pops the stored return point and warps back to that exact position. This means interior exits just need targetMap — the player always returns to the specific door they entered from, not a hardcoded position. Falls back to the destination map's spawn if no return point is stored. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,8 +113,38 @@ function handleInteraction(event) {
|
||||
|
||||
case 'mapExit': {
|
||||
const { targetMap, targetX, targetY } = event.data;
|
||||
warpToMap(targetMap, targetX, targetY);
|
||||
console.log(`[gameMode] warped to ${targetMap} (${targetX}, ${targetY})`);
|
||||
const p = worldState.player;
|
||||
|
||||
// Save return point: where the player is NOW (one tile back from the exit)
|
||||
// so when they leave the target map, they return here
|
||||
worldState.returnPoints.push({
|
||||
fromMap: worldState.currentMap,
|
||||
fromX: p.x,
|
||||
fromY: p.y
|
||||
});
|
||||
|
||||
// If exit has explicit coordinates, use them
|
||||
// Otherwise, check for a stored return point for the target map
|
||||
if (targetX != null && targetY != null) {
|
||||
warpToMap(targetMap, targetX, targetY);
|
||||
} else {
|
||||
// Pop the most recent return point for this map
|
||||
const retIdx = worldState.returnPoints.findLastIndex(
|
||||
rp => rp.fromMap === targetMap
|
||||
);
|
||||
if (retIdx >= 0) {
|
||||
const ret = worldState.returnPoints[retIdx];
|
||||
worldState.returnPoints.splice(retIdx, 1);
|
||||
warpToMap(ret.fromMap, ret.fromX, ret.fromY);
|
||||
} else {
|
||||
// Fallback: use map spawn point
|
||||
const destMap = getMap(targetMap);
|
||||
const sx = destMap?.spawn?.x ?? 0;
|
||||
const sy = destMap?.spawn?.y ?? 0;
|
||||
warpToMap(targetMap, sx, sy);
|
||||
}
|
||||
}
|
||||
console.log(`[gameMode] warped to ${worldState.currentMap} (${worldState.player.x}, ${worldState.player.y})`);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
292
js/world/maps.js
292
js/world/maps.js
@@ -1,60 +1,25 @@
|
||||
/**
|
||||
* maps.js - PNG-based world maps with wall coordinate arrays
|
||||
*
|
||||
* Each map has a pre-rendered PNG background image and defines:
|
||||
* - walls: Set of "x,y" strings for collision (built from coordinate data)
|
||||
* - npcs, interactions, exits as position-based objects
|
||||
*
|
||||
* Map images are drawn at 3x scale (16px native → 48px on screen)
|
||||
* maps.js - PNG-based world maps (auto-generated by Level Editor)
|
||||
*/
|
||||
|
||||
// ==================== Wall builder helpers ====================
|
||||
|
||||
function buildWallSet(widthTiles, heightTiles, wallData) {
|
||||
function buildWallSet(wallData) {
|
||||
const set = new Set();
|
||||
for (const [row, cols] of Object.entries(wallData)) {
|
||||
for (const col of cols) {
|
||||
set.add(`${col},${row}`);
|
||||
}
|
||||
for (const col of cols) set.add(col + ',' + row);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
function wallRange(from, to) {
|
||||
const cols = [];
|
||||
for (let c = from; c <= to; c++) cols.push(c);
|
||||
return cols;
|
||||
}
|
||||
function r(a, b) { const arr = []; for (let i = a; i <= b; i++) arr.push(i); return arr; }
|
||||
|
||||
// ==================== Map definitions ====================
|
||||
// ==================== Circuit Lab ====================
|
||||
|
||||
/**
|
||||
* LAB (10×12 tiles — lab.png 160×192)
|
||||
*
|
||||
* Visual layout from PNG:
|
||||
* Row 0: Top wall — machinery/equipment
|
||||
* Row 1: Machines on left, big computer right
|
||||
* Row 2: Open area, desk/machine on far right
|
||||
* Row 3-4: Table in center-left area
|
||||
* Row 5: Open corridor
|
||||
* Row 6-7: Two rows of bookshelves with gap in middle (col 4)
|
||||
* Row 8-9: Open floor
|
||||
* Row 10: Open floor near exit
|
||||
* Row 11: Bottom wall, door at cols 4-5
|
||||
*/
|
||||
const labWalls = {
|
||||
0: wallRange(0, 9), // solid top wall
|
||||
1: [0, 1, 7, 8, 9], // machines on sides
|
||||
2: [0, 7, 8, 9], // desk/machine right
|
||||
3: [0, 2, 3, 9], // table center-left
|
||||
4: [0, 2, 3, 9], // table continues
|
||||
5: [0, 9], // open corridor
|
||||
6: [0, 1, 2, 6, 7, 8, 9], // bookshelves, gap at 3-5
|
||||
7: [0, 1, 2, 6, 7, 8, 9], // bookshelves, gap at 3-5
|
||||
8: [0, 9], // open
|
||||
9: [0, 9], // open
|
||||
10: [0, 9], // open
|
||||
11: [0, 1, 2, 3, 6, 7, 8, 9] // bottom wall, door at 4-5
|
||||
0: [...r(0,9)],
|
||||
1: [0,1,2,3,6,7,8,9],
|
||||
3: [6,7,8],
|
||||
6: [0,1,2,3,6,7,8,9],
|
||||
7: [0,1,2,3,6,7,8,9],
|
||||
};
|
||||
|
||||
const labMap = {
|
||||
@@ -64,182 +29,142 @@ const labMap = {
|
||||
widthTiles: 10,
|
||||
heightTiles: 12,
|
||||
spawn: { x: 4, y: 10 },
|
||||
wallSet: buildWallSet(10, 12, labWalls),
|
||||
wallSet: buildWallSet(labWalls),
|
||||
|
||||
exits: [
|
||||
// Exit door — appear one tile BELOW the town door (not ON it, to avoid re-trigger)
|
||||
{ x: 4, y: 11, targetMap: 'town', targetX: 10, targetY: 8 },
|
||||
{ x: 5, y: 11, targetMap: 'town', targetX: 10, targetY: 8 }
|
||||
// No targetX/targetY — uses stored return point (the door the player entered from)
|
||||
{ x: 4, y: 11, targetMap: 'town' },
|
||||
{ x: 5, y: 11, targetMap: 'town' },
|
||||
],
|
||||
|
||||
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!"] },
|
||||
],
|
||||
|
||||
interactions: [
|
||||
// Workshop tables (the table at rows 3-4)
|
||||
{ x: 2, y: 3, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 3, y: 3, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 2, y: 4, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 3, y: 4, type: 'workshop', label: 'Workshop Table' },
|
||||
// Bookshelves (interact from adjacent)
|
||||
{ x: 1, y: 6, type: 'sign', label: 'Bookshelf',
|
||||
dialog: ['A collection of logic circuit manuals.'] },
|
||||
{ x: 7, y: 6, type: 'sign', label: 'Bookshelf',
|
||||
dialog: ['Advanced boolean algebra textbooks.'] },
|
||||
// Machines at top
|
||||
{ x: 8, y: 1, type: 'terminal', label: 'Terminal',
|
||||
dialog: ['Circuit analysis terminal.', 'Connect components to solve puzzles.'] },
|
||||
// Puzzle door (machine at top-right)
|
||||
{ x: 8, y: 2, type: 'puzzle_door', puzzleId: 'lab_door_1',
|
||||
requiredOutputs: [1, 0, 1, 1], label: 'Locked Door' }
|
||||
{ x: 7, y: 3, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 8, y: 3, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 6, y: 3, type: 'workshop', label: 'Workshop Table' },
|
||||
{ x: 1, y: 7, type: 'sign', label: 'Bookshelf', dialog: ["A collection of logic circuit manuals."] },
|
||||
{ x: 7, y: 7, type: 'sign', label: 'Bookshelf', dialog: ["Advanced boolean algebra textbooks."] },
|
||||
{ x: 0, y: 1, type: 'terminal', label: 'Terminal', dialog: ["Circuit analysis terminal.","Connect components to solve puzzles."] },
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* TOWN (20×18 tiles — pallet-town.png 320×288)
|
||||
*
|
||||
* Visual layout from PNG:
|
||||
* Row 0: Top border — trees
|
||||
* Row 1: Trees, fence
|
||||
* Row 2-5: Left house (cols 1-5), Right big building (cols 12-18)
|
||||
* Row 6: Open area, house doors
|
||||
* Row 7-8: Sign, fences, open path area
|
||||
* Row 9-10: Garden/flowers left, path center, fence right
|
||||
* Row 11-13: Open area, another building right side
|
||||
* Row 14-16: Water (left), trees (right), path center
|
||||
* Row 17: Bottom border — trees/fence
|
||||
*/
|
||||
const townWalls = (() => {
|
||||
const w = {};
|
||||
function add(row, cols) {
|
||||
if (!w[row]) w[row] = [];
|
||||
w[row].push(...cols);
|
||||
}
|
||||
// ==================== Neon Town ====================
|
||||
|
||||
// Top border
|
||||
add(0, wallRange(0, 19));
|
||||
add(1, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 18, 19]);
|
||||
const palletTownWalls = {
|
||||
0: [...r(0,19)],
|
||||
1: [0,1,2,3,4,5,6,7,8,9,10,11,18,19],
|
||||
2: [0,19],
|
||||
3: [0,4,5,6,7,12,13,14,15,19],
|
||||
4: [0,4,5,6,7,12,13,14,15,19],
|
||||
5: [0,3,4,5,6,7,11,12,13,14,15,19],
|
||||
6: [0,19],
|
||||
7: [0,19],
|
||||
8: [0,10,11,12,13,14,15,19],
|
||||
9: [0,4,5,6,7,10,11,12,13,14,15,19],
|
||||
10: [0,10,11,12,13,14,15,19],
|
||||
11: [0,10,11,13,14,15,19],
|
||||
12: [0,19],
|
||||
13: [0,10,11,12,13,14,15,19],
|
||||
14: [0,19],
|
||||
15: [0,19],
|
||||
16: [0,19],
|
||||
17: [0,1,8,9,10,11,12,13,14,15,16,17,18,19],
|
||||
};
|
||||
|
||||
// Left house (rows 2-5, cols 1-5)
|
||||
for (let r = 2; r <= 4; r++) add(r, [0, 1, 2, 3, 4, 5, 19]);
|
||||
add(5, [0, 1, 2, 4, 5, 19]); // door gap at col 3
|
||||
|
||||
// Right big building (rows 2-5, cols 12-18)
|
||||
for (let r = 2; r <= 5; r++) add(r, [0, 12, 13, 14, 15, 16, 17, 18, 19]);
|
||||
|
||||
// Open path rows with side borders
|
||||
add(6, [0, 19]);
|
||||
add(7, [0, 19]);
|
||||
add(8, [0, 1, 18, 19]);
|
||||
|
||||
// Garden/flowers left side + fence right
|
||||
add(9, [0, 1, 2, 3, 4, 5, 18, 19]);
|
||||
add(10, [0, 1, 2, 3, 4, 5, 18, 19]);
|
||||
|
||||
// Open area
|
||||
add(11, [0, 1, 18, 19]);
|
||||
add(12, [0, 1, 18, 19]);
|
||||
|
||||
// Building right + water left (rows 13-16)
|
||||
add(13, [0, 1, 14, 15, 16, 17, 18, 19]);
|
||||
add(14, [0, 1, 2, 3, 14, 15, 16, 17, 18, 19]);
|
||||
add(15, [0, 1, 2, 3, 14, 15, 16, 17, 18, 19]);
|
||||
add(16, [0, 1, 2, 3, 14, 15, 16, 17, 18, 19]);
|
||||
|
||||
// Bottom border
|
||||
add(17, wallRange(0, 19));
|
||||
|
||||
return w;
|
||||
})();
|
||||
|
||||
const townMap = {
|
||||
const palletTownMap = {
|
||||
id: 'town',
|
||||
name: 'Neon Town',
|
||||
image: 'map:pallet-town',
|
||||
widthTiles: 20,
|
||||
heightTiles: 18,
|
||||
spawn: { x: 10, y: 8 },
|
||||
wallSet: buildWallSet(20, 18, townWalls),
|
||||
spawn: { x: 12, y: 12 },
|
||||
wallSet: buildWallSet(palletTownWalls),
|
||||
|
||||
exits: [
|
||||
// Door into lab — appear one tile ABOVE the lab exit (not ON it, to avoid re-trigger)
|
||||
{ x: 10, y: 7, targetMap: 'lab', targetX: 4, targetY: 10 }
|
||||
{ x: 12, y: 11, targetMap: 'lab', targetX: 4, targetY: 10 },
|
||||
],
|
||||
|
||||
npcs: [
|
||||
{
|
||||
id: 'merchant',
|
||||
x: 8, y: 10,
|
||||
facing: 'right',
|
||||
dialog: [
|
||||
'Welcome to Neon Town!',
|
||||
'I trade in rare logic components.',
|
||||
'Craft some circuits in the Lab workshop!',
|
||||
'Some doors need special output patterns to open.'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'guide',
|
||||
x: 11, y: 12,
|
||||
facing: 'down',
|
||||
dialog: [
|
||||
'The Circuit Lab is in the big building up north.',
|
||||
'Talk to the Professor — he\'ll teach you about logic gates!',
|
||||
'Press TAB anytime to open your Workshop.'
|
||||
]
|
||||
}
|
||||
{ id: 'merchant', x: 8, y: 10, facing: 'right', dialog: ["Welcome to Neon Town!","I trade in rare logic components."] },
|
||||
{ id: 'guide', x: 11, y: 12, facing: 'down', dialog: ["The Circuit Lab is in the big building up north.","Press TAB anytime to open your Workshop."] },
|
||||
],
|
||||
|
||||
interactions: [
|
||||
// Left house door
|
||||
{ x: 3, y: 5, type: 'door', label: 'House',
|
||||
dialog: ['The door is locked.', 'Nobody seems to be home.'] },
|
||||
// Sign in town center
|
||||
{ x: 10, y: 9, type: 'sign', label: 'Sign',
|
||||
dialog: ['Welcome to Neon Town!', 'Circuit Lab ↑'] },
|
||||
// Right building sign
|
||||
{ x: 12, y: 6, type: 'sign', label: 'Sign',
|
||||
dialog: ['CIRCUIT LAB', 'Open for research!'] }
|
||||
{ x: 3, y: 5, type: 'door', label: 'House', dialog: ["The door is locked."] },
|
||||
{ x: 7, y: 9, type: 'sign', label: 'Sign', dialog: ["Welcome to Neon Town!","Circuit Lab ↑"] },
|
||||
{ x: 11, y: 5, type: 'sign', label: 'Sign', dialog: ["CIRCUIT LAB","Open for research!"] },
|
||||
]
|
||||
};
|
||||
|
||||
// ==================== Map registry ====================
|
||||
// ==================== House Interior ====================
|
||||
|
||||
const maps = {
|
||||
lab: labMap,
|
||||
town: townMap
|
||||
const houseA1fWalls = {
|
||||
};
|
||||
|
||||
// ==================== Public API ====================
|
||||
const houseA1fMap = {
|
||||
id: 'house-a-1f',
|
||||
name: 'House Interior',
|
||||
image: 'map:house-a-1f',
|
||||
widthTiles: 8,
|
||||
heightTiles: 8,
|
||||
spawn: { x: 0, y: 0 },
|
||||
wallSet: buildWallSet(houseA1fWalls),
|
||||
|
||||
export function getMap(id) {
|
||||
return maps[id] || null;
|
||||
}
|
||||
exits: [
|
||||
],
|
||||
|
||||
npcs: [
|
||||
],
|
||||
|
||||
interactions: [
|
||||
]
|
||||
};
|
||||
|
||||
// ==================== Route 1 ====================
|
||||
|
||||
const route1Walls = {
|
||||
};
|
||||
|
||||
const route1Map = {
|
||||
id: 'route-1',
|
||||
name: 'Route 1',
|
||||
image: 'map:route-1',
|
||||
widthTiles: 20,
|
||||
heightTiles: 36,
|
||||
spawn: { x: 0, y: 0 },
|
||||
wallSet: buildWallSet(route1Walls),
|
||||
|
||||
exits: [
|
||||
],
|
||||
|
||||
npcs: [
|
||||
],
|
||||
|
||||
interactions: [
|
||||
]
|
||||
};
|
||||
|
||||
// ==================== Registry ====================
|
||||
|
||||
const maps = {
|
||||
'lab': labMap,
|
||||
'town': palletTownMap,
|
||||
'house-a-1f': houseA1fMap,
|
||||
'route-1': route1Map,
|
||||
};
|
||||
|
||||
export function getMap(id) { return maps[id] || null; }
|
||||
|
||||
/**
|
||||
* Check if a tile position is a wall (using Set for O(1) lookup)
|
||||
*/
|
||||
export function isWall(mapId, x, y) {
|
||||
const map = maps[mapId];
|
||||
if (!map) return true;
|
||||
if (x < 0 || x >= map.widthTiles || y < 0 || y >= map.heightTiles) return true;
|
||||
return map.wallSet.has(`${x},${y}`);
|
||||
return map.wallSet.has(x + ',' + y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tile is walkable (not a wall and no NPC blocking)
|
||||
*/
|
||||
export function isWalkable(mapId, x, y) {
|
||||
if (isWall(mapId, x, y)) return false;
|
||||
if (getNPC(mapId, x, y)) return false;
|
||||
@@ -264,9 +189,6 @@ export function getExit(mapId, x, y) {
|
||||
return map.exits.find(e => e.x === x && e.y === y) || null;
|
||||
}
|
||||
|
||||
// Backward compat
|
||||
export function getTile(mapId, x, y) {
|
||||
return isWall(mapId, x, y) ? 1 : 0;
|
||||
}
|
||||
export function getTile(mapId, x, y) { return isWall(mapId, x, y) ? 1 : 0; }
|
||||
|
||||
export { maps };
|
||||
|
||||
@@ -40,6 +40,10 @@ export const worldState = {
|
||||
solvedPuzzles: [], // array of puzzleIds that have been solved
|
||||
activePuzzle: null, // { puzzleId, requiredOutputs, doorX, doorY } or null when no puzzle active
|
||||
|
||||
// Return points — remembers where the player entered each map from
|
||||
// Stack of { fromMap, fromX, fromY } — push on enter, pop on exit
|
||||
returnPoints: [],
|
||||
|
||||
// Game flags
|
||||
flags: {
|
||||
// Examples:
|
||||
|
||||
Reference in New Issue
Block a user