Refine archive world spawn and interaction handling
All checks were successful
Build Org Website / build (push) Successful in 38s
All checks were successful
Build Org Website / build (push) Successful in 38s
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) : "");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.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.
|
||||
|
||||
16
play/rpg.org
16
play/rpg.org
@@ -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>
|
||||
</main>
|
||||
|
||||
<script src="/assets/scripts/vendor/phaser-4.1.0.min.js?v=archive-world-2.1.1" 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-state.js?v=archive-world-2.1.1" 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-audio.js?v=archive-world-2.1.1" 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-scenes.js?v=archive-world-2.1.1" defer></script>
|
||||
<script src="/assets/scripts/pages/archive-world.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.2" 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.2" 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.2" 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.2" defer></script>
|
||||
#+END_EXPORT
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||
|
||||
* 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/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>@@
|
||||
|
||||
10
sitemap.org
10
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]]
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user