diff --git a/assets/scripts/pages/archive-world-data.js b/assets/scripts/pages/archive-world-data.js index d981869..343666d 100755 --- a/assets/scripts/pages/archive-world-data.js +++ b/assets/scripts/pages/archive-world-data.js @@ -120,12 +120,12 @@ const AREA_FRAMES = Object.freeze({ vault: 0, workshop: 1, playroom: 2, boss: 3 }); const AREAS = 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 }), safeSpawns: Object.freeze({ - gate: { x: 724, y: 965 }, square: { x: 724, y: 555 }, library: { x: 270, y: 350 }, - study: { x: 720, y: 330 }, inn: { x: 1110, y: 350 }, workshop: { x: 1060, y: 615 }, - playroom: { x: 1060, y: 840 }, museum: { x: 330, y: 835 }, garden: { x: 330, y: 545 } + gate: { x: 724, y: 1015 }, square: { x: 724, y: 555 }, library: { x: 270, y: 370 }, + study: { x: 720, y: 350 }, inn: { x: 1110, y: 370 }, workshop: { x: 1060, y: 625 }, + playroom: { x: 1120, y: 835 }, museum: { x: 270, y: 830 }, garden: { x: 370, y: 545 } }), safeZones: Object.freeze([ rect(650, 845, 150, 220), rect(585, 455, 280, 190) diff --git a/assets/scripts/pages/archive-world-scenes.js b/assets/scripts/pages/archive-world-scenes.js index 942e6d2..688bc09 100755 --- a/assets/scripts/pages/archive-world-scenes.js +++ b/assets/scripts/pages/archive-world-scenes.js @@ -84,7 +84,7 @@ }); this.createAnimations(); - this.npcSprites = []; + this.npcCollisions = this.physics.add.staticGroup(); const state = controller.getState(); const safe = Systems.nearestSafeSpawn(this.areaId, state.position.area === this.areaId ? state.position.x : area.spawn.x, @@ -100,6 +100,7 @@ this.interactables = []; this.markers = []; this.createWorldObjects(); + this.physics.add.collider(this.player, this.npcCollisions); this.createEnemies(); 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 }); }); 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 }); - this.npcSprites.push(npc); 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-wall", 1235, 840, "ink_vial", 2); @@ -215,6 +217,7 @@ createEnemies() { this.enemies = this.physics.add.group(); 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.area.enemies.forEach((entry) => { const spawn = Array.isArray(entry) @@ -289,7 +292,6 @@ if (!moving) this.stepDistance = 0; } this.player.setDepth(this.player.y + 60); - this.npcSprites.forEach((npc) => npc.setFlipX(this.player.x < npc.x)); this.updateEnemies(time, delta); this.updateNearest(); this.handleKeys(time); @@ -311,13 +313,7 @@ } updateNearest() { - let nearest = null; - 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; + this.nearest = Systems.nearestInteraction(this.interactables, this.player.x, this.player.y); controller.setPrompt(this.nearest ? promptFor(this.nearest) : ""); } diff --git a/assets/scripts/pages/archive-world-state.js b/assets/scripts/pages/archive-world-state.js index da4c037..8339506 100755 --- a/assets/scripts/pages/archive-world-state.js +++ b/assets/scripts/pages/archive-world-state.js @@ -12,7 +12,7 @@ const LEGACY_KEY = "zxh_archive_world_v1"; const PALETTES = Object.freeze(["brass", "moss", "berry"]); 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 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"]); diff --git a/assets/scripts/pages/archive-world-systems.js b/assets/scripts/pages/archive-world-systems.js index 4140e36..e95c938 100755 --- a/assets/scripts/pages/archive-world-systems.js +++ b/assets/scripts/pages/archive-world-systems.js @@ -193,9 +193,47 @@ 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) { const area = Data.AREAS[areaId] || Data.AREAS.town; - const points = Object.entries(area.safeSpawns || { entrance: area.spawn }); const safeCurrent = isSafe(areaId, x, y); if (safeCurrent) return { area: areaId, spawn: "saved", x, y }; return nearestNamedSafeSpawn(areaId, x, y); @@ -223,6 +261,7 @@ return Object.freeze({ normalizedVector, approachVelocity, discover, startQuest, progressQuest, completeQuest, updateStory, addItem, itemQuantity, consumeItem, grantXp, takeDamage, respawn, recordBoss, isSafe, isCombatSafe, + interactionRadius, nearestInteraction, safeSpawnForState, nearestSafeSpawn, nearestNamedSafeSpawn, currentObjective }); })); diff --git a/assets/scripts/pages/archive-world.js b/assets/scripts/pages/archive-world.js index 8aef8ee..75be00f 100755 --- a/assets/scripts/pages/archive-world.js +++ b/assets/scripts/pages/archive-world.js @@ -95,7 +95,7 @@ function ensureSafePosition(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); } diff --git a/build-logs/gitea-build-monitor.log b/build-logs/gitea-build-monitor.log index 3d8dda0..a045037 100755 --- a/build-logs/gitea-build-monitor.log +++ b/build-logs/gitea-build-monitor.log @@ -778,3 +778,9 @@ at , /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.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-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. diff --git a/play/rpg.org b/play/rpg.org index 17afc49..5caee86 100755 --- a/play/rpg.org +++ b/play/rpg.org @@ -142,12 +142,12 @@ - - - - - - - - + + + + + + + + #+END_EXPORT diff --git a/posts/posts-list.org b/posts/posts-list.org index 5b2118f..44b6acc 100755 --- a/posts/posts-list.org +++ b/posts/posts-list.org @@ -4,7 +4,7 @@ See the categories: @@html:Categories@@ * Posts: -- [[file:career/career-list.org][Career List]] @@html:30-07-2026 01:00@@ +- [[file:career/career-list.org][Career List]] @@html:30-07-2026 10:03@@ - [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:01-06-2026 10:21@@ @@html:@@ @@html:@@ - [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:11-03-2026 17:18@@ @@html:@@ @@html:@@ - [[file:career/javascript.org][Understands the Javascript language]] @@html:11-03-2026 16:52@@ @@html:@@ @@html:@@ diff --git a/sitemap.org b/sitemap.org index 7c9041d..8995544 100755 --- a/sitemap.org +++ b/sitemap.org @@ -112,9 +112,9 @@ flowchart TD n43 --> n53 n54["Tag: education"] n43 --> n54 - n55["Tag: reading"] + n55["Tag: emacs"] n43 --> n55 - n56["Tag: emacs"] + n56["Tag: reading"] n43 --> n56 n57["Tag: maths"] n43 --> n57 @@ -228,8 +228,8 @@ flowchart TD click n52 "tags/update.html" "Tag: update" click n53 "tags/insights.html" "Tag: insights" click n54 "tags/education.html" "Tag: education" - click n55 "tags/reading.html" "Tag: reading" - click n56 "tags/emacs.html" "Tag: emacs" + click n55 "tags/emacs.html" "Tag: emacs" + click n56 "tags/reading.html" "Tag: reading" click n57 "tags/maths.html" "Tag: maths" click n59 "play/sigil.html" "Sigil Press" click n60 "play/ink.html" "Ink Pond" @@ -322,8 +322,8 @@ flowchart TD - [[file:tags/update.org][Tag: update]] - [[file:tags/insights.org][Tag: insights]] - [[file:tags/education.org][Tag: education]] - - [[file:tags/reading.org][Tag: reading]] - [[file:tags/emacs.org][Tag: emacs]] + - [[file:tags/reading.org][Tag: reading]] - [[file:tags/maths.org][Tag: maths]] - play - [[file:play/sigil.org][Sigil Press]] diff --git a/tests/archive-world-state.test.cjs b/tests/archive-world-state.test.cjs index b0238fc..ac2bc91 100755 --- a/tests/archive-world-state.test.cjs +++ b/tests/archive-world-state.test.cjs @@ -9,6 +9,11 @@ test("fresh v2 state applies safe defaults", () => { assert.equal(fresh.name, "Ada"); assert.equal(fresh.palette, "moss"); 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.settings.soundEnabled, false); assert.deepEqual(fresh.sigils, []); diff --git a/tests/archive-world-systems.test.cjs b/tests/archive-world-systems.test.cjs index 13b4469..6ed135f 100755 --- a/tests/archive-world-systems.test.cjs +++ b/tests/archive-world-systems.test.cjs @@ -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)); }); +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", () => { Object.entries(data.AREAS).forEach(([areaId, area]) => { 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}`); }); }); + +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}`); + }); + }); +});