mudlet-map-renderer 0.0.25 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ [{"envId":1,"colors":[128,0,0]},{"envId":2,"colors":[0,128,0]},{"envId":3,"colors":[128,128,0]},{"envId":4,"colors":[0,0,128]},{"envId":5,"colors":[128,0,128]},{"envId":6,"colors":[0,128,128]},{"envId":7,"colors":[192,192,192]},{"envId":8,"colors":[0,0,0]},{"envId":9,"colors":[255,0,0]},{"envId":10,"colors":[0,255,0]},{"envId":11,"colors":[255,255,0]},{"envId":12,"colors":[0,0,255]},{"envId":13,"colors":[255,0,255]},{"envId":14,"colors":[0,255,255]},{"envId":15,"colors":[255,255,255]},{"envId":16,"colors":[128,128,128]},{"envId":17,"colors":[0,0,95]},{"envId":18,"colors":[0,0,135]},{"envId":19,"colors":[0,0,175]},{"envId":20,"colors":[0,0,215]},{"envId":21,"colors":[0,0,255]},{"envId":22,"colors":[0,95,0]},{"envId":23,"colors":[0,95,95]},{"envId":24,"colors":[0,95,135]},{"envId":25,"colors":[0,95,175]},{"envId":26,"colors":[0,95,215]},{"envId":27,"colors":[0,95,255]},{"envId":28,"colors":[0,135,0]},{"envId":29,"colors":[0,135,95]},{"envId":30,"colors":[0,135,135]},{"envId":31,"colors":[0,135,175]},{"envId":32,"colors":[0,135,215]},{"envId":33,"colors":[0,135,255]},{"envId":34,"colors":[0,175,0]},{"envId":35,"colors":[0,175,95]},{"envId":36,"colors":[0,175,135]},{"envId":37,"colors":[0,175,175]},{"envId":38,"colors":[0,175,215]},{"envId":39,"colors":[0,175,255]},{"envId":40,"colors":[0,215,0]},{"envId":41,"colors":[0,215,95]},{"envId":42,"colors":[0,215,135]},{"envId":43,"colors":[0,215,175]},{"envId":44,"colors":[0,215,215]},{"envId":45,"colors":[0,215,255]},{"envId":46,"colors":[0,255,0]},{"envId":47,"colors":[0,255,95]},{"envId":48,"colors":[0,255,135]},{"envId":49,"colors":[0,255,175]},{"envId":50,"colors":[0,255,215]},{"envId":51,"colors":[0,255,255]},{"envId":52,"colors":[95,0,0]},{"envId":53,"colors":[95,0,95]},{"envId":54,"colors":[95,0,135]},{"envId":55,"colors":[95,0,175]},{"envId":56,"colors":[95,0,215]},{"envId":57,"colors":[95,0,255]},{"envId":58,"colors":[95,95,0]},{"envId":59,"colors":[95,95,95]},{"envId":60,"colors":[95,95,135]},{"envId":61,"colors":[95,95,175]},{"envId":62,"colors":[95,95,215]},{"envId":63,"colors":[95,95,255]},{"envId":64,"colors":[95,135,0]},{"envId":65,"colors":[95,135,95]},{"envId":66,"colors":[95,135,135]},{"envId":67,"colors":[95,135,175]},{"envId":68,"colors":[95,135,215]},{"envId":69,"colors":[95,135,255]},{"envId":70,"colors":[95,175,0]},{"envId":71,"colors":[95,175,95]},{"envId":72,"colors":[95,175,135]},{"envId":73,"colors":[95,175,175]},{"envId":74,"colors":[95,175,215]},{"envId":75,"colors":[95,175,255]},{"envId":76,"colors":[95,215,0]},{"envId":77,"colors":[95,215,95]},{"envId":78,"colors":[95,215,135]},{"envId":79,"colors":[95,215,175]},{"envId":80,"colors":[95,215,215]},{"envId":81,"colors":[95,215,255]},{"envId":82,"colors":[95,255,0]},{"envId":83,"colors":[95,255,95]},{"envId":84,"colors":[95,255,135]},{"envId":85,"colors":[95,255,175]},{"envId":86,"colors":[95,255,215]},{"envId":87,"colors":[95,255,255]},{"envId":88,"colors":[135,0,0]},{"envId":89,"colors":[135,0,95]},{"envId":90,"colors":[135,0,135]},{"envId":91,"colors":[135,0,175]},{"envId":92,"colors":[135,0,215]},{"envId":93,"colors":[135,0,255]},{"envId":94,"colors":[135,95,0]},{"envId":95,"colors":[135,95,95]},{"envId":96,"colors":[135,95,135]},{"envId":97,"colors":[135,95,175]},{"envId":98,"colors":[135,95,215]},{"envId":99,"colors":[135,95,255]},{"envId":100,"colors":[135,135,0]},{"envId":101,"colors":[135,135,95]},{"envId":102,"colors":[135,135,135]},{"envId":103,"colors":[135,135,175]},{"envId":104,"colors":[135,135,215]},{"envId":105,"colors":[135,135,255]},{"envId":106,"colors":[135,175,0]},{"envId":107,"colors":[135,175,95]},{"envId":108,"colors":[135,175,135]},{"envId":109,"colors":[135,175,175]},{"envId":110,"colors":[135,175,215]},{"envId":111,"colors":[135,175,255]},{"envId":112,"colors":[135,215,0]},{"envId":113,"colors":[135,215,95]},{"envId":114,"colors":[135,215,135]},{"envId":115,"colors":[135,215,175]},{"envId":116,"colors":[135,215,215]},{"envId":117,"colors":[135,215,255]},{"envId":118,"colors":[135,255,0]},{"envId":119,"colors":[135,255,95]},{"envId":120,"colors":[135,255,135]},{"envId":121,"colors":[135,255,175]},{"envId":122,"colors":[135,255,215]},{"envId":123,"colors":[135,255,255]},{"envId":124,"colors":[175,0,0]},{"envId":125,"colors":[175,0,95]},{"envId":126,"colors":[175,0,135]},{"envId":127,"colors":[175,0,175]},{"envId":128,"colors":[175,0,215]},{"envId":129,"colors":[175,0,255]},{"envId":130,"colors":[175,95,0]},{"envId":131,"colors":[175,95,95]},{"envId":132,"colors":[175,95,135]},{"envId":133,"colors":[175,95,175]},{"envId":134,"colors":[175,95,215]},{"envId":135,"colors":[175,95,255]},{"envId":136,"colors":[175,135,0]},{"envId":137,"colors":[175,135,95]},{"envId":138,"colors":[175,135,135]},{"envId":139,"colors":[175,135,175]},{"envId":140,"colors":[175,135,215]},{"envId":141,"colors":[175,135,255]},{"envId":142,"colors":[175,175,0]},{"envId":143,"colors":[175,175,95]},{"envId":144,"colors":[175,175,135]},{"envId":145,"colors":[175,175,175]},{"envId":146,"colors":[175,175,215]},{"envId":147,"colors":[175,175,255]},{"envId":148,"colors":[175,215,0]},{"envId":149,"colors":[175,215,95]},{"envId":150,"colors":[175,215,135]},{"envId":151,"colors":[175,215,175]},{"envId":152,"colors":[175,215,215]},{"envId":153,"colors":[175,215,255]},{"envId":154,"colors":[175,255,0]},{"envId":155,"colors":[175,255,95]},{"envId":156,"colors":[175,255,135]},{"envId":157,"colors":[175,255,175]},{"envId":158,"colors":[175,255,215]},{"envId":159,"colors":[175,255,255]},{"envId":160,"colors":[215,0,0]},{"envId":161,"colors":[215,0,95]},{"envId":162,"colors":[215,0,135]},{"envId":163,"colors":[215,0,175]},{"envId":164,"colors":[215,0,215]},{"envId":165,"colors":[215,0,255]},{"envId":166,"colors":[215,95,0]},{"envId":167,"colors":[215,95,95]},{"envId":168,"colors":[215,95,135]},{"envId":169,"colors":[215,95,175]},{"envId":170,"colors":[215,95,215]},{"envId":171,"colors":[215,95,255]},{"envId":172,"colors":[215,135,0]},{"envId":173,"colors":[215,135,95]},{"envId":174,"colors":[215,135,135]},{"envId":175,"colors":[215,135,175]},{"envId":176,"colors":[215,135,215]},{"envId":177,"colors":[215,135,255]},{"envId":178,"colors":[215,175,0]},{"envId":179,"colors":[215,175,95]},{"envId":180,"colors":[215,175,135]},{"envId":181,"colors":[215,175,175]},{"envId":182,"colors":[215,175,215]},{"envId":183,"colors":[215,175,255]},{"envId":184,"colors":[215,215,0]},{"envId":185,"colors":[215,215,95]},{"envId":186,"colors":[215,215,135]},{"envId":187,"colors":[215,215,175]},{"envId":188,"colors":[215,215,215]},{"envId":189,"colors":[215,215,255]},{"envId":190,"colors":[215,255,0]},{"envId":191,"colors":[215,255,95]},{"envId":192,"colors":[215,255,135]},{"envId":193,"colors":[215,255,175]},{"envId":194,"colors":[215,255,215]},{"envId":195,"colors":[215,255,255]},{"envId":196,"colors":[255,0,0]},{"envId":197,"colors":[255,0,95]},{"envId":198,"colors":[255,0,135]},{"envId":199,"colors":[255,0,175]},{"envId":200,"colors":[0,0,255]},{"envId":201,"colors":[0,255,0]},{"envId":202,"colors":[255,0,0]},{"envId":203,"colors":[165,42,42]},{"envId":204,"colors":[255,95,135]},{"envId":205,"colors":[255,95,175]},{"envId":206,"colors":[255,95,215]},{"envId":207,"colors":[255,95,255]},{"envId":208,"colors":[255,135,0]},{"envId":209,"colors":[255,135,95]},{"envId":210,"colors":[255,135,135]},{"envId":211,"colors":[255,135,175]},{"envId":212,"colors":[255,135,215]},{"envId":213,"colors":[255,135,255]},{"envId":214,"colors":[255,175,0]},{"envId":215,"colors":[255,175,95]},{"envId":216,"colors":[255,175,135]},{"envId":217,"colors":[255,175,175]},{"envId":218,"colors":[255,175,215]},{"envId":219,"colors":[255,175,255]},{"envId":220,"colors":[255,215,0]},{"envId":221,"colors":[255,215,95]},{"envId":222,"colors":[255,215,135]},{"envId":223,"colors":[255,215,175]},{"envId":224,"colors":[255,215,215]},{"envId":225,"colors":[255,215,255]},{"envId":226,"colors":[255,255,0]},{"envId":227,"colors":[255,255,95]},{"envId":228,"colors":[255,255,135]},{"envId":229,"colors":[255,255,175]},{"envId":230,"colors":[255,255,215]},{"envId":231,"colors":[255,255,255]},{"envId":232,"colors":[8,8,8]},{"envId":233,"colors":[18,18,18]},{"envId":234,"colors":[28,28,28]},{"envId":235,"colors":[38,38,38]},{"envId":236,"colors":[48,48,48]},{"envId":237,"colors":[58,58,58]},{"envId":238,"colors":[68,68,68]},{"envId":239,"colors":[78,78,78]},{"envId":240,"colors":[88,88,88]},{"envId":241,"colors":[98,98,98]},{"envId":242,"colors":[108,108,108]},{"envId":243,"colors":[118,118,118]},{"envId":244,"colors":[128,128,128]},{"envId":245,"colors":[138,138,138]},{"envId":246,"colors":[148,148,148]},{"envId":247,"colors":[158,158,158]},{"envId":248,"colors":[168,168,168]},{"envId":249,"colors":[178,178,178]},{"envId":250,"colors":[188,188,188]},{"envId":251,"colors":[198,198,198]},{"envId":252,"colors":[208,208,208]},{"envId":253,"colors":[218,218,218]},{"envId":254,"colors":[228,228,228]},{"envId":255,"colors":[238,238,238]},{"envId":257,"colors":[128,0,0]},{"envId":258,"colors":[0,128,0]},{"envId":259,"colors":[128,128,0]},{"envId":260,"colors":[0,0,128]},{"envId":261,"colors":[128,0,128]},{"envId":262,"colors":[0,128,128]},{"envId":263,"colors":[192,192,192]},{"envId":264,"colors":[0,0,0]},{"envId":265,"colors":[255,0,0]},{"envId":266,"colors":[0,255,0]},{"envId":267,"colors":[255,255,0]},{"envId":268,"colors":[0,0,255]},{"envId":269,"colors":[255,0,255]},{"envId":270,"colors":[0,255,255]},{"envId":271,"colors":[255,255,255]},{"envId":272,"colors":[128,128,128]},{"envId":293,"colors":[0,171,192]},{"envId":294,"colors":[139,255,0]},{"envId":295,"colors":[239,176,73]},{"envId":296,"colors":[163,151,235]},{"envId":297,"colors":[236,167,236]},{"envId":298,"colors":[22,232,196]},{"envId":299,"colors":[0,170,255]},{"envId":300,"colors":[0,196,255]},{"envId":301,"colors":[255,114,14]},{"envId":303,"colors":[170,139,89]},{"envId":400,"colors":[186,112,74]},{"envId":798,"colors":[255,140,0]},{"envId":799,"colors":[230,230,250]},{"envId":800,"colors":[30,144,255]},{"envId":801,"colors":[154,205,50]},{"envId":802,"colors":[85,107,47]},{"envId":803,"colors":[0,255,255]},{"envId":805,"colors":[107,142,35]},{"envId":806,"colors":[0,100,0]},{"envId":810,"colors":[255,0,0]},{"envId":811,"colors":[139,69,19]},{"envId":812,"colors":[230,230,250]},{"envId":813,"colors":[255,255,255]},{"envId":814,"colors":[34,139,34]},{"envId":823,"colors":[255,215,0]},{"envId":824,"colors":[47,79,79]},{"envId":825,"colors":[189,183,107]},{"envId":826,"colors":[184,134,11]},{"envId":830,"colors":[255,69,0]},{"envId":855,"colors":[160,82,45]}]
@@ -0,0 +1,102 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Konva Universal Lib — Demo</title>
7
+ <style>
8
+ html, body {
9
+ width: 100%;
10
+ height: 100%;
11
+ margin: 0;
12
+ font-family: Inter, system-ui, sans-serif;
13
+ background-color: #000;
14
+ color: #fff;
15
+ }
16
+
17
+ body {
18
+ position: relative;
19
+ overflow: hidden;
20
+ }
21
+
22
+ #stage {
23
+ width: 100%;
24
+ height: 100%;
25
+ }
26
+
27
+ #hud {
28
+ position: absolute;
29
+ top: 1rem;
30
+ left: 1rem;
31
+ padding: 0.5rem 0.75rem;
32
+ background: rgba(0, 0, 0, 0.6);
33
+ border-radius: 0.5rem;
34
+ font-size: 0.9rem;
35
+ line-height: 1.4;
36
+ pointer-events: auto;
37
+ }
38
+
39
+ #controls {
40
+ margin-top: 0.5rem;
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 0.5rem;
44
+ }
45
+
46
+ #controls label {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.4rem;
50
+ font-weight: 500;
51
+ }
52
+
53
+ #destination-form {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 0.35rem;
57
+ }
58
+
59
+ #destination-inputs {
60
+ display: flex;
61
+ gap: 0.35rem;
62
+ }
63
+
64
+ #destination-input {
65
+ width: 6rem;
66
+ }
67
+
68
+ #destination-status {
69
+ font-size: 0.8rem;
70
+ color: #c8f7ff;
71
+ }
72
+
73
+ #hud strong {
74
+ font-weight: 600;
75
+ }
76
+ </style>
77
+ <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
78
+ </head>
79
+ <body>
80
+ <div id="stage"></div>
81
+ <div id="hud">
82
+ <div id="status">Loading map…</div>
83
+ <div id="walker-status"></div>
84
+ <div id="controls">
85
+ <label>
86
+ <input type="checkbox" id="exploration-toggle" checked>
87
+ Exploration overlay
88
+ </label>
89
+ <form id="destination-form">
90
+ <label for="destination-input">Destination room</label>
91
+ <div id="destination-inputs">
92
+ <input type="number" id="destination-input" min="1" placeholder="Room ID" />
93
+ <button type="submit">Set</button>
94
+ <button type="button" id="destination-clear">Clear</button>
95
+ </div>
96
+ <div id="destination-status"></div>
97
+ </form>
98
+ </div>
99
+ </div>
100
+ <script type="module" src="/main.ts"></script>
101
+ </body>
102
+ </html>
package/demo/main.ts ADDED
@@ -0,0 +1,389 @@
1
+ import data from "./mapExport.json";
2
+ import colors from "./colors.json";
3
+ import {Renderer} from "@src";
4
+ import MapReader from "@src/reader/MapReader";
5
+
6
+ const stageElement = document.getElementById("stage") as HTMLDivElement;
7
+ const statusElement = document.getElementById("status") as HTMLDivElement;
8
+ const walkerStatusElement = document.getElementById("walker-status") as HTMLDivElement;
9
+ const explorationToggle = document.getElementById("exploration-toggle") as HTMLInputElement | null;
10
+ const destinationForm = document.getElementById("destination-form") as HTMLFormElement | null;
11
+ const destinationInput = document.getElementById("destination-input") as HTMLInputElement | null;
12
+ const destinationClearButton = document.getElementById("destination-clear") as HTMLButtonElement | null;
13
+ const destinationStatusElement = document.getElementById("destination-status") as HTMLDivElement | null;
14
+
15
+ const mapReader = new MapReader(data as MapData.Map, colors as MapData.Env[]);
16
+ const startingRoomId = 1;
17
+
18
+ const visitedRooms = mapReader.decorateWithExploration([startingRoomId]);
19
+
20
+ const renderer = new Renderer(stageElement, mapReader);
21
+ const startingRoom = mapReader.getRoom(startingRoomId);
22
+ let currentRoomId = startingRoomId;
23
+ const walkerState: { timeoutId: number | undefined } = { timeoutId: undefined };
24
+ let destinationRoomId: number | undefined;
25
+ let currentDestinationPath: number[] | undefined;
26
+
27
+ if (startingRoom) {
28
+ const startingArea = mapReader.getExplorationArea(startingRoom.area);
29
+ startingArea?.addVisitedRoom(startingRoom.id);
30
+
31
+ renderer.setPosition(startingRoomId);
32
+ updateAreaStatus(startingRoom.area);
33
+ updateDestinationStatus("No destination set.");
34
+
35
+ walkerStatusElement.textContent = "Walker preparing first step…";
36
+ scheduleNextStep(600);
37
+ } else {
38
+ statusElement.textContent = "Starting room not found.";
39
+ walkerStatusElement.textContent = "Walker is idle.";
40
+ }
41
+
42
+ explorationToggle?.addEventListener("change", () => {
43
+ if (explorationToggle.checked) {
44
+ mapReader.decorateWithExploration(visitedRooms);
45
+ } else {
46
+ mapReader.clearExplorationDecoration();
47
+ }
48
+ renderer.setPosition(currentRoomId);
49
+ const currentRoom = mapReader.getRoom(currentRoomId);
50
+ if (currentRoom) {
51
+ updateAreaStatus(currentRoom.area);
52
+ }
53
+ updateDestinationGuidance();
54
+ });
55
+
56
+ if (explorationToggle) {
57
+ explorationToggle.checked = mapReader.isExplorationEnabled();
58
+ }
59
+
60
+ destinationForm?.addEventListener("submit", event => {
61
+ event.preventDefault();
62
+ if (!destinationInput) {
63
+ return;
64
+ }
65
+ const roomId = Number.parseInt(destinationInput.value, 10);
66
+ if (Number.isNaN(roomId)) {
67
+ updateDestinationStatus("Enter a valid room id.");
68
+ return;
69
+ }
70
+
71
+ const room = mapReader.getRoom(roomId);
72
+ if (!room) {
73
+ updateDestinationStatus(`Room ${roomId} not found.`);
74
+ return;
75
+ }
76
+
77
+ destinationRoomId = roomId;
78
+ destinationInput.value = roomId.toString();
79
+ updateDestinationGuidance();
80
+ });
81
+
82
+ destinationClearButton?.addEventListener("click", () => {
83
+ destinationRoomId = undefined;
84
+ currentDestinationPath = undefined;
85
+ updateDestinationStatus("Destination cleared. Walking freely.");
86
+ renderer.clearPaths();
87
+ if (destinationInput) {
88
+ destinationInput.value = "";
89
+ }
90
+ });
91
+
92
+ const exitNumberToDirection: Record<number, MapData.direction> = {
93
+ 1: "north",
94
+ 2: "northeast",
95
+ 3: "northwest",
96
+ 4: "east",
97
+ 5: "west",
98
+ 6: "south",
99
+ 7: "southeast",
100
+ 8: "southwest",
101
+ 9: "up",
102
+ 10: "down",
103
+ 11: "in",
104
+ 12: "out",
105
+ };
106
+
107
+ function getRoomExits(room: MapData.Room) {
108
+ const lockedDirections = new Set(
109
+ (room.exitLocks ?? [])
110
+ .map(lockId => exitNumberToDirection[lockId])
111
+ .filter((direction): direction is MapData.direction => Boolean(direction)),
112
+ );
113
+ const lockedSpecialTargets = new Set(room.mSpecialExitLocks ?? []);
114
+
115
+ const exits: number[] = [];
116
+
117
+ Object.entries(room.exits ?? {}).forEach(([direction, exitId]) => {
118
+ if (lockedDirections.has(direction as MapData.direction)) {
119
+ return;
120
+ }
121
+ if (exitId > 0) {
122
+ exits.push(exitId);
123
+ }
124
+ });
125
+
126
+ Object.values(room.specialExits ?? {}).forEach(exitId => {
127
+ if (typeof exitId !== "number" || exitId <= 0) {
128
+ return;
129
+ }
130
+ if (lockedSpecialTargets.has(exitId)) {
131
+ return;
132
+ }
133
+ exits.push(exitId);
134
+ });
135
+
136
+ return exits;
137
+ }
138
+
139
+ function updateAreaStatus(areaId: number) {
140
+ if (!mapReader.isExplorationEnabled()) {
141
+ statusElement.textContent = `Area ${areaId}`;
142
+ return;
143
+ }
144
+
145
+ const area = mapReader.getExplorationArea(areaId);
146
+ if (!area) {
147
+ statusElement.textContent = `Area ${areaId}`;
148
+ return;
149
+ }
150
+
151
+ const visited = area.getVisitedRoomCount();
152
+ const total = area.getTotalRoomCount();
153
+ statusElement.innerHTML = `<strong>Area ${areaId}</strong><br/>Visited ${visited} of ${total} rooms`;
154
+ }
155
+
156
+ function randomDelay() {
157
+ return 800 + Math.random() * 1200;
158
+ }
159
+
160
+ const PREFERRED_PATH_PROBABILITY = 0.7;
161
+
162
+ function findPreferredRoomId(room: MapData.Room) {
163
+ const queue: number[] = [room.id];
164
+ const cameFrom = new Map<number, number | null>([[room.id, null]]);
165
+
166
+ while (queue.length) {
167
+ const currentId = queue.shift()!;
168
+ const currentRoom = mapReader.getRoom(currentId);
169
+ if (!currentRoom) {
170
+ continue;
171
+ }
172
+
173
+ const area = mapReader.getExplorationArea(currentRoom.area);
174
+ const isVisited = area?.hasVisitedRoom(currentId) ?? false;
175
+ const isStartRoom = currentId === room.id;
176
+ if (!isVisited && !isStartRoom) {
177
+ let stepId = currentId;
178
+ let parentId = cameFrom.get(stepId) ?? null;
179
+ while (parentId !== null && parentId !== room.id) {
180
+ stepId = parentId;
181
+ parentId = cameFrom.get(stepId) ?? null;
182
+ }
183
+ return stepId;
184
+ }
185
+
186
+ for (const neighbourId of getRoomExits(currentRoom)) {
187
+ if (!cameFrom.has(neighbourId)) {
188
+ cameFrom.set(neighbourId, currentId);
189
+ queue.push(neighbourId);
190
+ }
191
+ }
192
+ }
193
+
194
+ return undefined;
195
+ }
196
+
197
+ function pickNextRoom(room: MapData.Room) {
198
+ const exits = getRoomExits(room)
199
+ .map(exitRoomId => mapReader.getRoom(exitRoomId))
200
+ .filter((candidate): candidate is MapData.Room => Boolean(candidate));
201
+
202
+ if (!exits.length) {
203
+ return undefined;
204
+ }
205
+
206
+ const preferredExplorationRoomId = findPreferredRoomId(room);
207
+ if (preferredExplorationRoomId !== undefined) {
208
+ const preferredRoom = exits.find(candidate => candidate.id === preferredExplorationRoomId);
209
+ if (preferredRoom && Math.random() < PREFERRED_PATH_PROBABILITY) {
210
+ return preferredRoom;
211
+ }
212
+ }
213
+
214
+ const unvisited = exits.filter(candidate => {
215
+ const area = mapReader.getExplorationArea(candidate.area);
216
+ if (area) {
217
+ return !area.hasVisitedRoom(candidate.id);
218
+ }
219
+ return !visitedRooms?.has(candidate.id);
220
+ });
221
+
222
+ const preferredDestinationRoomId = getNextStepTowardsDestination(room.id);
223
+ const preferredRoom = preferredDestinationRoomId !== undefined
224
+ ? exits.find(candidate => candidate.id === preferredDestinationRoomId)
225
+ : undefined;
226
+
227
+ if (preferredRoom) {
228
+ const bias = unvisited.length ? 0.75 : 0.55;
229
+ if (Math.random() < bias) {
230
+ return preferredRoom;
231
+ }
232
+ }
233
+
234
+ const choices = unvisited.length ? unvisited : exits;
235
+
236
+ return choices[Math.floor(Math.random() * choices.length)];
237
+ }
238
+
239
+ function scheduleNextStep(delay = randomDelay()) {
240
+ const {timeoutId} = walkerState;
241
+ if (timeoutId !== undefined) {
242
+ window.clearTimeout(timeoutId);
243
+ }
244
+ walkerState.timeoutId = window.setTimeout(walkStep, delay);
245
+ walkerStatusElement.textContent = `Next step in ${(delay / 1000).toFixed(1)}s`;
246
+ }
247
+
248
+ function walkStep() {
249
+ const room = mapReader.getRoom(currentRoomId);
250
+ if (!room) {
251
+ walkerStatusElement.textContent = "Walker lost its position.";
252
+ return;
253
+ }
254
+
255
+ const nextRoom = pickNextRoom(room);
256
+ if (!nextRoom) {
257
+ walkerStatusElement.textContent = "Walker reached a dead end.";
258
+ scheduleNextStep();
259
+ return;
260
+ }
261
+
262
+ const explorationArea = mapReader.getExplorationArea(nextRoom.area);
263
+ if (explorationArea) {
264
+ explorationArea.addVisitedRoom(nextRoom.id);
265
+ } else {
266
+ visitedRooms?.add(nextRoom.id);
267
+ }
268
+
269
+ currentRoomId = nextRoom.id;
270
+
271
+ renderer.setPosition(nextRoom.id);
272
+ updateAreaStatus(nextRoom.area);
273
+ updateDestinationGuidance();
274
+
275
+ walkerStatusElement.textContent = `Walker moved to room ${nextRoom.id}`;
276
+ scheduleNextStep();
277
+ }
278
+
279
+ function getNextStepTowardsDestination(fromRoomId: number) {
280
+ if (!destinationRoomId || destinationRoomId === fromRoomId) {
281
+ return undefined;
282
+ }
283
+ if (currentDestinationPath && currentDestinationPath[0] === fromRoomId) {
284
+ if (currentDestinationPath.length >= 2) {
285
+ return currentDestinationPath[1];
286
+ }
287
+ return undefined;
288
+ }
289
+ const path = findPathBetweenRooms(fromRoomId, destinationRoomId);
290
+ if (!path || path.length < 2) {
291
+ return undefined;
292
+ }
293
+ currentDestinationPath = path;
294
+ return path[1];
295
+ }
296
+
297
+ function findPathBetweenRooms(startRoomId: number, targetRoomId: number) {
298
+ const startRoom = mapReader.getRoom(startRoomId);
299
+ const targetRoom = mapReader.getRoom(targetRoomId);
300
+ if (!startRoom || !targetRoom) {
301
+ return undefined;
302
+ }
303
+
304
+ if (startRoomId === targetRoomId) {
305
+ return [startRoomId];
306
+ }
307
+
308
+ const queue: number[] = [startRoomId];
309
+ const visited = new Set<number>([startRoomId]);
310
+ const parents = new Map<number, number>();
311
+
312
+ while (queue.length) {
313
+ const currentId = queue.shift();
314
+ if (currentId === undefined) {
315
+ break;
316
+ }
317
+ const currentRoom = mapReader.getRoom(currentId);
318
+ if (!currentRoom) {
319
+ continue;
320
+ }
321
+
322
+ for (const neighborId of getRoomExits(currentRoom)) {
323
+ if (visited.has(neighborId)) {
324
+ continue;
325
+ }
326
+ visited.add(neighborId);
327
+ parents.set(neighborId, currentId);
328
+
329
+ if (neighborId === targetRoomId) {
330
+ return buildPathFromParents(targetRoomId, parents, startRoomId);
331
+ }
332
+
333
+ queue.push(neighborId);
334
+ }
335
+ }
336
+
337
+ return undefined;
338
+ }
339
+
340
+ function buildPathFromParents(targetId: number, parents: Map<number, number>, startId: number) {
341
+ const path = [targetId];
342
+ let current = targetId;
343
+
344
+ while (current !== startId) {
345
+ const parent = parents.get(current);
346
+ if (parent === undefined) {
347
+ return undefined;
348
+ }
349
+ path.push(parent);
350
+ current = parent;
351
+ }
352
+
353
+ path.reverse();
354
+ return path;
355
+ }
356
+
357
+ function updateDestinationStatus(message: string) {
358
+ if (!destinationStatusElement) {
359
+ return;
360
+ }
361
+ destinationStatusElement.textContent = message;
362
+ }
363
+
364
+ function updateDestinationGuidance() {
365
+ if (!destinationRoomId) {
366
+ updateDestinationStatus("No destination set.");
367
+ currentDestinationPath = undefined;
368
+ return;
369
+ }
370
+
371
+ const path = findPathBetweenRooms(currentRoomId, destinationRoomId);
372
+ renderer.clearPaths();
373
+
374
+ if (!path) {
375
+ updateDestinationStatus(`No route to room ${destinationRoomId}. Wandering randomly.`);
376
+ currentDestinationPath = undefined;
377
+ return;
378
+ }
379
+
380
+ if (path.length < 2) {
381
+ updateDestinationStatus(`Already at destination room ${destinationRoomId}.`);
382
+ currentDestinationPath = path;
383
+ return;
384
+ }
385
+
386
+ renderer.renderPath(path);
387
+ updateDestinationStatus(`Biasing towards room ${destinationRoomId} (${path.length - 1} steps away).`);
388
+ currentDestinationPath = path;
389
+ }