In part 1 we built a station you can walk through: a game loop, input, a camera and rooms with exits. It moves, but it isn't a game yet. Nothing reacts to you.
In this part we make the world interactive, and finish with a complete, winnable puzzle chain:
- Find a wallet on the platform. It has a travel card inside.
- Find a staff keycard in the hallway.
- Use it to open the control room and switch the power back on.
- With power and a ticket, the ticket gates let you out.
Small, but it uses every system a point-and-click game needs: hotspots, items, an inventory, locked doors and story state.
Hotspots: one idea for everything
In part 1, exits were their own special thing. But an exit, a wallet on the floor, a locked door and a power lever all work the same way:
- they sit at a position in the room
- when you're close, a prompt appears
- pressing E does something
So we'll replace exits with a single list of hotspots. What a hotspot does is described by the properties it has:
{ id: 'wallet', x: 760, width: 30, label: 'Wallet', item: 'travel-card' }
{ id: 'platform-exit', x: 1700, width: 100, label: 'Hallway', exit: { to: 'hallway', spawnX: 120 } }
{ id: 'power-lever', x: 620, width: 40, label: 'Power lever', setFlag: 'powerOn' }
item: pick something upexit: go to another roomsetFlag: remember that something happenedsay: show a line of text
One hotspot can combine them. A locked door is just an exit with a requirement attached.
The bug in "first match"
Terminal's interaction code (simplified here) looped through every object in the room and picked the first one within range:
for (let i = 0; i < game.objects.length; i++) {
const dist = Math.abs(playerCenter - objCenter);
if (dist < interactionRange) {
targetIndex = i;
break; // Interact with one at a time
}
}
This works until two hotspots are close together. Stand right next to a door with a newspaper on the floor nearby, and pressing E might pick up the newspaper, because it happens to be earlier in the list, even though the door is right in front of you. Players experience that as "the game ignored me".
The fix is simple: check every hotspot in range and choose the nearest one. The prompt then always describes exactly what E will do.
Puzzles as data, not if-statements
Here's a shortened version of how Terminal handles a locked door:
if (obj.type === 'control_room_door') {
if (game.flags && game.flags.controlRoomUnlocked) {
switchRoom('control_room', 300);
return;
}
const hasKeycard = game.inventory.some(i => i.type === 'keycard');
if (hasKeycard) {
game.flags.controlRoomUnlocked = true;
triggerMonologue('TRY_CONTROL_ROOM_DOOR_WITH_KEYCARD');
setTimeout(() => {
switchRoom('control_room', 300);
}, 800);
} else {
triggerMonologue('TRY_CONTROL_ROOM_DOOR');
}
return;
}
It works. But every puzzle in the game is another block like this in one ever-growing function, and the ticket gates, bus stop and control desk all had their own. Adding a puzzle meant finding the right place in a long chain of special cases, and hoping nothing else broke.
In Last Train, the same locked door is data:
{
id: 'control-door',
x: 1400,
width: 100,
label: 'Control room',
requires: [{ item: 'keycard', otherwise: "Locked. There's a card reader." }],
exit: { to: 'control', spawnX: 120 },
}
And one small, generic function handles every hotspot in the game: check the requirements, and if any fail, say why; otherwise do what the hotspot describes. Adding a puzzle means adding an object to a room. No game code changes at all.
The ticket gates, which need two things, are just as short:
requires: [
{ flag: 'powerOn', otherwise: 'The gates are dead. No power.' },
{ item: 'travel-card', otherwise: 'I need a ticket to get through.' },
],
Requirements are checked in order, so the player always hears the most relevant reason first.
State that's easy to save
Everything the game needs to remember lives in three plain values:
inventory: an array of item IDs, like['travel-card', 'keycard']taken: an array of hotspot IDs that have been picked up, so they don't reappearflags: an object of things that have happened, like{ powerOn: true }
Notice what's not stored: we never delete hotspots from the room data, and we never store objects or functions in the state. The room data stays exactly as written, and the state is simple enough to turn into JSON in one line. That pays off in part 3, where saving the game takes about ten lines.
What's next
In the full part 2 we build all of this:
- the finished rooms, including the control room and the way out
- nearest-hotspot detection, requirements and the generic interaction function
- an inventory HUD and on-screen messages
- doors with lock lights that turn green when you can open them
- the complete, winnable part 2 source
Checking access…