Refine archive world spawn and interaction handling
All checks were successful
Build Org Website / build (push) Successful in 38s

This commit is contained in:
gitea-actions
2026-07-30 10:22:23 +01:00
parent 2f125f32ae
commit dc1ca5c00e
11 changed files with 112 additions and 33 deletions

View File

@@ -120,12 +120,12 @@
const AREA_FRAMES = Object.freeze({ vault: 0, workshop: 1, playroom: 2, boss: 3 }); const AREA_FRAMES = Object.freeze({ vault: 0, workshop: 1, playroom: 2, boss: 3 });
const AREAS = Object.freeze({ const AREAS = Object.freeze({
town: Object.freeze({ town: Object.freeze({
width: 1448, height: 1086, background: "town", spawn: { x: 724, y: 965 }, width: 1448, height: 1086, background: "town", spawn: { x: 724, y: 1015 },
movement: Object.freeze({ speed: 162, acceleration: 1280, deceleration: 1850, bootAcceleration: 1720 }), movement: Object.freeze({ speed: 162, acceleration: 1280, deceleration: 1850, bootAcceleration: 1720 }),
safeSpawns: Object.freeze({ safeSpawns: Object.freeze({
gate: { x: 724, y: 965 }, square: { x: 724, y: 555 }, library: { x: 270, y: 350 }, gate: { x: 724, y: 1015 }, square: { x: 724, y: 555 }, library: { x: 270, y: 370 },
study: { x: 720, y: 330 }, inn: { x: 1110, y: 350 }, workshop: { x: 1060, y: 615 }, study: { x: 720, y: 350 }, inn: { x: 1110, y: 370 }, workshop: { x: 1060, y: 625 },
playroom: { x: 1060, y: 840 }, museum: { x: 330, y: 835 }, garden: { x: 330, y: 545 } playroom: { x: 1120, y: 835 }, museum: { x: 270, y: 830 }, garden: { x: 370, y: 545 }
}), }),
safeZones: Object.freeze([ safeZones: Object.freeze([
rect(650, 845, 150, 220), rect(585, 455, 280, 190) rect(650, 845, 150, 220), rect(585, 455, 280, 190)

View File

@@ -84,7 +84,7 @@
}); });
this.createAnimations(); this.createAnimations();
this.npcSprites = []; this.npcCollisions = this.physics.add.staticGroup();
const state = controller.getState(); const state = controller.getState();
const safe = Systems.nearestSafeSpawn(this.areaId, const safe = Systems.nearestSafeSpawn(this.areaId,
state.position.area === this.areaId ? state.position.x : area.spawn.x, state.position.area === this.areaId ? state.position.x : area.spawn.x,
@@ -100,6 +100,7 @@
this.interactables = []; this.interactables = [];
this.markers = []; this.markers = [];
this.createWorldObjects(); this.createWorldObjects();
this.physics.add.collider(this.player, this.npcCollisions);
this.createEnemies(); this.createEnemies();
this.createInput(); this.createInput();
@@ -164,11 +165,12 @@
if (!controller.reducedMotion()) this.tweens.add({ targets: marker, alpha: 0.45, scale: 1.14, yoyo: true, repeat: -1, duration: 1100 }); if (!controller.reducedMotion()) this.tweens.add({ targets: marker, alpha: 0.45, scale: 1.14, yoyo: true, repeat: -1, duration: 1100 });
}); });
this.area.npcs.forEach(([id, x, y]) => { this.area.npcs.forEach(([id, x, y]) => {
const npc = this.add.sprite(x, y, "cast", Data.NPCS[id].frame).setScale(0.23).setDepth(y + 60); const npc = this.npcCollisions.create(x, y, "cast", Data.NPCS[id].frame)
.setScale(0.23).setDepth(y + 60);
npc.refreshBody();
npc.body.setSize(82, 44).setOffset(115, 214);
npc.setData("interaction", { type: "npc", id, x, y }); npc.setData("interaction", { type: "npc", id, x, y });
this.npcSprites.push(npc);
this.interactables.push(npc.getData("interaction")); this.interactables.push(npc.getData("interaction"));
if (!controller.reducedMotion()) this.tweens.add({ targets: npc, y: y - 2, duration: 1200 + Data.NPCS[id].frame * 30, yoyo: true, repeat: -1 });
}); });
this.addChest("town-garden", 205, 565, "memory_fragment", 2); this.addChest("town-garden", 205, 565, "memory_fragment", 2);
this.addChest("town-wall", 1235, 840, "ink_vial", 2); this.addChest("town-wall", 1235, 840, "ink_vial", 2);
@@ -215,6 +217,7 @@
createEnemies() { createEnemies() {
this.enemies = this.physics.add.group(); this.enemies = this.physics.add.group();
this.physics.add.collider(this.enemies, this.collisions); this.physics.add.collider(this.enemies, this.collisions);
this.physics.add.collider(this.enemies, this.npcCollisions);
this.physics.add.collider(this.enemies, this.enemies); this.physics.add.collider(this.enemies, this.enemies);
this.area.enemies.forEach((entry) => { this.area.enemies.forEach((entry) => {
const spawn = Array.isArray(entry) const spawn = Array.isArray(entry)
@@ -289,7 +292,6 @@
if (!moving) this.stepDistance = 0; if (!moving) this.stepDistance = 0;
} }
this.player.setDepth(this.player.y + 60); this.player.setDepth(this.player.y + 60);
this.npcSprites.forEach((npc) => npc.setFlipX(this.player.x < npc.x));
this.updateEnemies(time, delta); this.updateEnemies(time, delta);
this.updateNearest(); this.updateNearest();
this.handleKeys(time); this.handleKeys(time);
@@ -311,13 +313,7 @@
} }
updateNearest() { updateNearest() {
let nearest = null; this.nearest = Systems.nearestInteraction(this.interactables, this.player.x, this.player.y);
let best = Infinity;
this.interactables.forEach((item) => {
const distance = Phaser.Math.Distance.Between(this.player.x, this.player.y, item.x, item.y);
if (distance < best) { best = distance; nearest = item; }
});
this.nearest = best < 92 ? nearest : null;
controller.setPrompt(this.nearest ? promptFor(this.nearest) : ""); controller.setPrompt(this.nearest ? promptFor(this.nearest) : "");
} }

View File

@@ -12,7 +12,7 @@
const LEGACY_KEY = "zxh_archive_world_v1"; const LEGACY_KEY = "zxh_archive_world_v1";
const PALETTES = Object.freeze(["brass", "moss", "berry"]); const PALETTES = Object.freeze(["brass", "moss", "berry"]);
const FACES = Object.freeze(["north", "south", "east", "west"]); const FACES = Object.freeze(["north", "south", "east", "west"]);
const SPAWN = Object.freeze({ area: "town", spawn: "gate", x: 724, y: 965 }); const SPAWN = Object.freeze({ area: "town", spawn: "gate", x: 724, y: 1015 });
const LANDMARK_IDS = Data ? Data.LANDMARK_IDS : Object.freeze(["gate", "library", "study", "inn", "workshop", "playroom", "museum", "garden"]); const LANDMARK_IDS = Data ? Data.LANDMARK_IDS : Object.freeze(["gate", "library", "study", "inn", "workshop", "playroom", "museum", "garden"]);
const QUEST_IDS = Data ? Data.QUEST_IDS : Object.freeze(LANDMARK_IDS.concat("lost_letter")); const QUEST_IDS = Data ? Data.QUEST_IDS : Object.freeze(LANDMARK_IDS.concat("lost_letter"));
const ITEM_IDS = Data ? Data.ITEM_IDS : Object.freeze(["living_bookmark", "lantern", "archive_key", "swiftstep_boots", "ink_vial", "memory_fragment", "garden_seed", "workshop_cog", "museum_lens", "recall_stew", "lore_page", "archive_sigil"]); const ITEM_IDS = Data ? Data.ITEM_IDS : Object.freeze(["living_bookmark", "lantern", "archive_key", "swiftstep_boots", "ink_vial", "memory_fragment", "garden_seed", "workshop_cog", "museum_lens", "recall_stew", "lore_page", "archive_sigil"]);

View File

@@ -193,9 +193,47 @@
return Boolean(area && (area.safeZones || []).some((item) => pointInRect(x, y, item, 0))); return Boolean(area && (area.safeZones || []).some((item) => pointInRect(x, y, item, 0)));
} }
function interactionRadius(item) {
return {
npc: 46,
landmark: 64,
portal: 58,
chest: 48,
challenge: 52,
"boss-memory": 50
}[item && item.type] || 44;
}
function nearestInteraction(items, x, y) {
let nearest = null;
let best = Infinity;
(Array.isArray(items) ? items : []).forEach((item) => {
if (!item || !Number.isFinite(item.x) || !Number.isFinite(item.y)) return;
const distance = Math.hypot(item.x - x, item.y - y);
if (distance <= interactionRadius(item) && distance < best) {
nearest = item;
best = distance;
}
});
return nearest;
}
function safeSpawnForState(state) {
const position = state && state.position;
const gate = Data.AREAS.town.gates && Data.AREAS.town.gates[0];
const gateLocked = state && Array.isArray(state.sigils) && !state.sigils.includes("gate");
if (position && position.area === "town" && gate && gateLocked && position.y < gate.y + gate.height) {
return nearestNamedSafeSpawn("town", Data.AREAS.town.safeSpawns.gate.x, Data.AREAS.town.safeSpawns.gate.y);
}
return nearestSafeSpawn(
position && position.area || "town",
position && position.x,
position && position.y
);
}
function nearestSafeSpawn(areaId, x, y) { function nearestSafeSpawn(areaId, x, y) {
const area = Data.AREAS[areaId] || Data.AREAS.town; const area = Data.AREAS[areaId] || Data.AREAS.town;
const points = Object.entries(area.safeSpawns || { entrance: area.spawn });
const safeCurrent = isSafe(areaId, x, y); const safeCurrent = isSafe(areaId, x, y);
if (safeCurrent) return { area: areaId, spawn: "saved", x, y }; if (safeCurrent) return { area: areaId, spawn: "saved", x, y };
return nearestNamedSafeSpawn(areaId, x, y); return nearestNamedSafeSpawn(areaId, x, y);
@@ -223,6 +261,7 @@
return Object.freeze({ return Object.freeze({
normalizedVector, approachVelocity, discover, startQuest, progressQuest, completeQuest, updateStory, addItem, itemQuantity, normalizedVector, approachVelocity, discover, startQuest, progressQuest, completeQuest, updateStory, addItem, itemQuantity,
consumeItem, grantXp, takeDamage, respawn, recordBoss, isSafe, isCombatSafe, consumeItem, grantXp, takeDamage, respawn, recordBoss, isSafe, isCombatSafe,
interactionRadius, nearestInteraction, safeSpawnForState,
nearestSafeSpawn, nearestNamedSafeSpawn, currentObjective nearestSafeSpawn, nearestNamedSafeSpawn, currentObjective
}); });
})); }));

View File

@@ -95,7 +95,7 @@
function ensureSafePosition(state) { function ensureSafePosition(state) {
if (state.health <= 0) state = Systems.respawn(state); if (state.health <= 0) state = Systems.respawn(state);
const safe = Systems.nearestSafeSpawn(state.position.area, state.position.x, state.position.y); const safe = Systems.safeSpawnForState(state);
return State.withPosition(state, safe.area, safe.x, safe.y, state.position.facing, safe.spawn); return State.withPosition(state, safe.area, safe.x, safe.y, state.position.facing, safe.spawn);
} }

View File

@@ -778,3 +778,9 @@ at <ScriptBlock>, /home/zaine/master-folder/org-platform/org_web/build-logs/gite
2026-07-30T09:08:55.0548108+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python. 2026-07-30T09:08:55.0548108+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
2026-07-30T09:08:55.1356472+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0 2026-07-30T09:08:55.1356472+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
2026-07-30T09:08:55.4335778+01:00 [INFO] Sent authoring server test notification. 2026-07-30T09:08:55.4335778+01:00 [INFO] Sent authoring server test notification.
2026-07-30T10:03:40.8579861+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
2026-07-30T10:03:40.8658341+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
2026-07-30T10:03:41.6706793+01:00 [INFO] Sent build status notification with 1 embed(s).
2026-07-30T10:03:41.6923349+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
2026-07-30T10:03:41.7745044+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
2026-07-30T10:03:42.0626713+01:00 [INFO] Sent authoring server test notification.

View File

@@ -142,12 +142,12 @@
<noscript><p class="archive-world__noscript">The RPG needs JavaScript, but every destination remains available in the plain directory above.</p></noscript> <noscript><p class="archive-world__noscript">The RPG needs JavaScript, but every destination remains available in the plain directory above.</p></noscript>
</main> </main>
<script src="/assets/scripts/vendor/phaser-4.1.0.min.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/vendor/phaser-4.1.0.min.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world-data.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world-data.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world-state.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world-state.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world-systems.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world-systems.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world-audio.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world-audio.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world-ui.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world-ui.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world-scenes.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world-scenes.js?v=archive-world-2.1.2" defer></script>
<script src="/assets/scripts/pages/archive-world.js?v=archive-world-2.1.1" defer></script> <script src="/assets/scripts/pages/archive-world.js?v=archive-world-2.1.2" defer></script>
#+END_EXPORT #+END_EXPORT

View File

@@ -4,7 +4,7 @@
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@ See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
* Posts: * Posts:
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">30-07-2026 01:00</span>@@ - [[file:career/career-list.org][Career List]] @@html:<span class="post-date">30-07-2026 10:03</span>@@
- [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@ - [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@ - [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@ - [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@

View File

@@ -112,9 +112,9 @@ flowchart TD
n43 --> n53 n43 --> n53
n54["Tag: education"] n54["Tag: education"]
n43 --> n54 n43 --> n54
n55["Tag: reading"] n55["Tag: emacs"]
n43 --> n55 n43 --> n55
n56["Tag: emacs"] n56["Tag: reading"]
n43 --> n56 n43 --> n56
n57["Tag: maths"] n57["Tag: maths"]
n43 --> n57 n43 --> n57
@@ -228,8 +228,8 @@ flowchart TD
click n52 "tags/update.html" "Tag: update" click n52 "tags/update.html" "Tag: update"
click n53 "tags/insights.html" "Tag: insights" click n53 "tags/insights.html" "Tag: insights"
click n54 "tags/education.html" "Tag: education" click n54 "tags/education.html" "Tag: education"
click n55 "tags/reading.html" "Tag: reading" click n55 "tags/emacs.html" "Tag: emacs"
click n56 "tags/emacs.html" "Tag: emacs" click n56 "tags/reading.html" "Tag: reading"
click n57 "tags/maths.html" "Tag: maths" click n57 "tags/maths.html" "Tag: maths"
click n59 "play/sigil.html" "Sigil Press" click n59 "play/sigil.html" "Sigil Press"
click n60 "play/ink.html" "Ink Pond" click n60 "play/ink.html" "Ink Pond"
@@ -322,8 +322,8 @@ flowchart TD
- [[file:tags/update.org][Tag: update]] - [[file:tags/update.org][Tag: update]]
- [[file:tags/insights.org][Tag: insights]] - [[file:tags/insights.org][Tag: insights]]
- [[file:tags/education.org][Tag: education]] - [[file:tags/education.org][Tag: education]]
- [[file:tags/reading.org][Tag: reading]]
- [[file:tags/emacs.org][Tag: emacs]] - [[file:tags/emacs.org][Tag: emacs]]
- [[file:tags/reading.org][Tag: reading]]
- [[file:tags/maths.org][Tag: maths]] - [[file:tags/maths.org][Tag: maths]]
- play - play
- [[file:play/sigil.org][Sigil Press]] - [[file:play/sigil.org][Sigil Press]]

View File

@@ -9,6 +9,11 @@ test("fresh v2 state applies safe defaults", () => {
assert.equal(fresh.name, "Ada"); assert.equal(fresh.name, "Ada");
assert.equal(fresh.palette, "moss"); assert.equal(fresh.palette, "moss");
assert.equal(fresh.position.area, "town"); assert.equal(fresh.position.area, "town");
assert.deepEqual(
{ x: fresh.position.x, y: fresh.position.y },
data.AREAS.town.safeSpawns.gate
);
assert.ok(fresh.position.y > data.AREAS.town.gates[0].y + data.AREAS.town.gates[0].height);
assert.equal(fresh.health, 100); assert.equal(fresh.health, 100);
assert.equal(fresh.settings.soundEnabled, false); assert.equal(fresh.settings.soundEnabled, false);
assert.deepEqual(fresh.sigils, []); assert.deepEqual(fresh.sigils, []);

View File

@@ -102,6 +102,31 @@ test("invalid collision positions resolve to a named safe spawn", () => {
assert.ok(["gate", "square", "library", "study", "inn", "workshop", "playroom", "museum", "garden"].includes(safe.spawn)); assert.ok(["gate", "square", "library", "study", "inn", "workshop", "playroom", "museum", "garden"].includes(safe.spawn));
}); });
test("locked-gate saves inside town recover to the outside opening spawn", () => {
const current = state.fresh("Ada", "brass");
current.position = { area: "town", spawn: "square", x: 724, y: 555, facing: "south" };
const safe = systems.safeSpawnForState(current);
assert.deepEqual(
{ area: safe.area, spawn: safe.spawn, x: safe.x, y: safe.y },
{ area: "town", spawn: "gate", ...data.AREAS.town.safeSpawns.gate }
);
current.sigils.push("gate");
const unlocked = systems.safeSpawnForState(current);
assert.equal(unlocked.spawn, "saved");
assert.equal(unlocked.y, 555);
});
test("interaction ranges are type-specific and NPC conversations require close approach", () => {
const npc = { type: "npc", id: "orin", x: 0, y: 0 };
const landmark = { type: "landmark", id: "gate", x: 56, y: 0 };
assert.equal(systems.interactionRadius(npc), 46);
assert.equal(systems.nearestInteraction([npc], 47, 0), null);
assert.equal(systems.nearestInteraction([npc], 45, 0), npc);
assert.equal(systems.nearestInteraction([npc, landmark], 0, 0), npc);
assert.equal(systems.nearestInteraction([landmark], 0, 0), landmark);
});
test("all declared safe spawns and enemy homes are outside collision geometry", () => { test("all declared safe spawns and enemy homes are outside collision geometry", () => {
Object.entries(data.AREAS).forEach(([areaId, area]) => { Object.entries(data.AREAS).forEach(([areaId, area]) => {
Object.entries(area.safeSpawns).forEach(([spawnId, spawn]) => { Object.entries(area.safeSpawns).forEach(([spawnId, spawn]) => {
@@ -125,3 +150,11 @@ test("town landmarks and NPC interaction points remain reachable", () => {
assert.equal(systems.isSafe("town", x, y), true, `npc:${npcId}`); assert.equal(systems.isSafe("town", x, y), true, `npc:${npcId}`);
}); });
}); });
test("town safe spawns do not place the player inside an NPC body", () => {
Object.entries(data.AREAS.town.safeSpawns).forEach(([spawnId, spawn]) => {
data.AREAS.town.npcs.forEach(([npcId, x, y]) => {
assert.ok(Math.hypot(spawn.x - x, spawn.y - y) >= 30, `${spawnId} overlaps npc:${npcId}`);
});
});
});