Refine Archive World navigation and gate progression
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:
@@ -5,6 +5,12 @@
|
|||||||
return Object.freeze({ x, y, width, height, depth: depth || 2 });
|
return Object.freeze({ x, y, width, height, depth: depth || 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enemy(type, x, y, options) {
|
||||||
|
return Object.freeze(Object.assign({
|
||||||
|
type, x, y, leash: 150, engage: 245, requiresSigil: null, questId: null
|
||||||
|
}, options || {}));
|
||||||
|
}
|
||||||
|
|
||||||
function landmark(id, title, eyebrow, x, y, npc, description, links, questId, area) {
|
function landmark(id, title, eyebrow, x, y, npc, description, links, questId, area) {
|
||||||
return Object.freeze({ id, title, eyebrow, x, y, npc, description, links, questId, area: area || null });
|
return Object.freeze({ id, title, eyebrow, x, y, npc, description, links, questId, area: area || null });
|
||||||
}
|
}
|
||||||
@@ -13,22 +19,22 @@
|
|||||||
gate: landmark("gate", "Town Gate", "Arrivals", 724, 920, "orin",
|
gate: landmark("gate", "Town Gate", "Arrivals", 724, 920, "orin",
|
||||||
"The only opening in Archive Town's old wall. Gatekeeper Orin is trying to restore the erased arrival plaques.",
|
"The only opening in Archive Town's old wall. Gatekeeper Orin is trying to restore the erased arrival plaques.",
|
||||||
[["Home", "/"], ["Recently updated", "/recently-updated.html"], ["Contact", "/home/contact.html"]], "gate"),
|
[["Home", "/"], ["Recently updated", "/recently-updated.html"], ["Contact", "/home/contact.html"]], "gate"),
|
||||||
library: landmark("library", "Grand Library", "Knowledge", 265, 280, "sera",
|
library: landmark("library", "Grand Library", "Knowledge", 265, 305, "sera",
|
||||||
"Head Archivist Sera guards the sealed vault and a shelf whose symbols no longer remember their order.",
|
"Head Archivist Sera guards the sealed vault and a shelf whose symbols no longer remember their order.",
|
||||||
[["Posts", "/posts/posts-list.html"], ["Categories", "/home/categories.html"], ["Posts introduction", "/posts/posts-intro.html"]], "library", "vault"),
|
[["Posts", "/posts/posts-list.html"], ["Categories", "/home/categories.html"], ["Posts introduction", "/posts/posts-intro.html"]], "library", "vault"),
|
||||||
study: landmark("study", "Guild Study", "Work", 720, 250, "vale",
|
study: landmark("study", "Guild Study", "Work", 720, 300, "vale",
|
||||||
"Instructor Vale keeps the guild calm while Nulls gather beyond the practice banners.",
|
"Instructor Vale keeps the guild calm while Nulls gather beyond the practice banners.",
|
||||||
[["Career", "/posts/career/career-list.html"], ["Competency status", "/home/status.html"], ["Probation objectives", "/posts/career/probation-objectives.html"]], "study"),
|
[["Career", "/posts/career/career-list.html"], ["Competency status", "/home/status.html"], ["Probation objectives", "/posts/career/probation-objectives.html"]], "study"),
|
||||||
inn: landmark("inn", "Kitchen Inn", "Daily life", 1110, 285, "mara",
|
inn: landmark("inn", "Kitchen Inn", "Daily life", 1110, 305, "mara",
|
||||||
"Innkeeper Mara holds three borrowed memories and cannot recall which traveller entrusted each one.",
|
"Innkeeper Mara holds three borrowed memories and cannot recall which traveller entrusted each one.",
|
||||||
[["Blogs", "/blogs/blogs-list.html"], ["Weekly reviews", "/tags/review.html"], ["Blogs introduction", "/blogs/blogs-intro.html"]], "inn"),
|
[["Blogs", "/blogs/blogs-list.html"], ["Weekly reviews", "/tags/review.html"], ["Blogs introduction", "/blogs/blogs-intro.html"]], "inn"),
|
||||||
workshop: landmark("workshop", "Workshop", "Living systems", 1125, 545, "pell",
|
workshop: landmark("workshop", "Workshop", "Living systems", 1125, 590, "pell",
|
||||||
"Inventor Pell's recall engine has lost its cogs. A Redacted Sentinel occupies the repair yard.",
|
"Inventor Pell's recall engine has lost its cogs. A Redacted Sentinel occupies the repair yard.",
|
||||||
[["Services", "/home/services.html"], ["Wird tracker", "/home/wird-tracker.html"], ["Countdowns", "/home/countdown.html"], ["Backlog", "/home/backlog.html"]], "workshop", "workshop"),
|
[["Services", "/home/services.html"], ["Wird tracker", "/home/wird-tracker.html"], ["Countdowns", "/home/countdown.html"], ["Backlog", "/home/backlog.html"]], "workshop", "workshop"),
|
||||||
playroom: landmark("playroom", "Playroom", "Experiments", 1120, 805, "pip",
|
playroom: landmark("playroom", "Playroom", "Experiments", 1120, 835, "pip",
|
||||||
"Gamekeeper Pip's lantern maze changes whenever a name disappears. Six patient lights still know the route.",
|
"Gamekeeper Pip's lantern maze changes whenever a name disappears. Six patient lights still know the route.",
|
||||||
[["Play hub", "/play/play.html"], ["The Rain Index", "/play/the-rain-index.html"], ["House of Pages", "/play/house.html"]], "playroom", "playroom"),
|
[["Play hub", "/play/play.html"], ["The Rain Index", "/play/the-rain-index.html"], ["House of Pages", "/play/house.html"]], "playroom", "playroom"),
|
||||||
museum: landmark("museum", "Lima Museum", "Keepsakes", 270, 805, "lima",
|
museum: landmark("museum", "Lima Museum", "Keepsakes", 270, 830, "lima",
|
||||||
"Curator Lima can reveal what the Redactor was before the ink, but the Museum Lens is fractured.",
|
"Curator Lima can reveal what the Redactor was before the ink, but the Museum Lens is fractured.",
|
||||||
[["Lima archive", "/lima/index.html"], ["Older writing", "/blogs/2025/2025-list.html"], ["Memory Cabinet", "/play/memory.html"]], "museum"),
|
[["Lima archive", "/lima/index.html"], ["Older writing", "/blogs/2025/2025-list.html"], ["Memory Cabinet", "/play/memory.html"]], "museum"),
|
||||||
garden: landmark("garden", "Notes Garden", "Connections", 255, 545, "rowan",
|
garden: landmark("garden", "Notes Garden", "Connections", 255, 545, "rowan",
|
||||||
@@ -115,56 +121,110 @@
|
|||||||
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: 965 },
|
||||||
|
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: 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: 550 },
|
study: { x: 720, y: 330 }, inn: { x: 1110, y: 350 }, workshop: { x: 1060, y: 615 },
|
||||||
playroom: { x: 1060, y: 805 }, museum: { x: 330, y: 805 }, garden: { x: 330, y: 545 }
|
playroom: { x: 1060, y: 840 }, museum: { x: 330, y: 835 }, garden: { x: 330, y: 545 }
|
||||||
}),
|
}),
|
||||||
|
safeZones: Object.freeze([
|
||||||
|
rect(650, 845, 150, 220), rect(585, 455, 280, 190)
|
||||||
|
]),
|
||||||
|
gates: Object.freeze([
|
||||||
|
Object.freeze({ id: "town-gate", x: 672, y: 850, width: 104, height: 102, requiresSigil: "gate" })
|
||||||
|
]),
|
||||||
collisions: Object.freeze([
|
collisions: Object.freeze([
|
||||||
rect(0, 0, 1448, 58), rect(0, 0, 58, 1086), rect(1390, 0, 58, 1086),
|
/* Outer town wall, with a deliberate walkable opening through the gate. */
|
||||||
rect(0, 930, 645, 156), rect(803, 930, 645, 156),
|
rect(0, 0, 1448, 62), rect(0, 0, 54, 1086), rect(1394, 0, 54, 1086),
|
||||||
rect(125, 68, 330, 230, 7), rect(580, 70, 285, 210, 7), rect(960, 70, 355, 230, 7),
|
rect(0, 862, 590, 98), rect(590, 840, 82, 132), rect(776, 840, 86, 132), rect(862, 862, 586, 98),
|
||||||
rect(1010, 420, 335, 210, 7), rect(1000, 690, 345, 225, 7),
|
/* Major buildings. Their steps and front doors remain approachable. */
|
||||||
rect(105, 685, 340, 230, 7), rect(85, 390, 350, 225, 7),
|
rect(188, 62, 270, 212, 7), rect(606, 55, 238, 218, 7), rect(964, 58, 292, 218, 7),
|
||||||
rect(470, 75, 95, 350), rect(885, 75, 88, 350),
|
rect(1010, 418, 330, 142, 7), rect(1002, 655, 340, 155, 7), rect(218, 620, 220, 178, 7),
|
||||||
rect(465, 615, 100, 275), rect(885, 615, 98, 275)
|
/* Rivers. Breaks align with the two east/west bridge routes. */
|
||||||
|
rect(468, 88, 112, 255), rect(468, 432, 112, 93), rect(468, 610, 112, 250),
|
||||||
|
rect(868, 88, 112, 255), rect(868, 432, 112, 93), rect(868, 610, 112, 250),
|
||||||
|
/* Garden pond, dense hedges and corner ink pockets. */
|
||||||
|
rect(56, 360, 105, 292), rect(160, 382, 286, 45), rect(78, 675, 105, 175),
|
||||||
|
rect(1260, 310, 130, 108), rect(1270, 572, 120, 92), rect(1270, 812, 120, 80)
|
||||||
]),
|
]),
|
||||||
npcs: Object.freeze([
|
npcs: Object.freeze([
|
||||||
["orin", 724, 875], ["sera", 270, 335], ["vale", 720, 315], ["mara", 1110, 335],
|
["orin", 724, 875], ["sera", 270, 335], ["vale", 720, 315], ["mara", 1110, 335],
|
||||||
["pell", 1045, 545], ["pip", 1045, 800], ["lima", 335, 800], ["rowan", 335, 545],
|
["pell", 1060, 590], ["pip", 1060, 835], ["lima", 335, 830], ["rowan", 335, 545],
|
||||||
["nell", 760, 720], ["otho", 675, 520], ["remy", 790, 585]
|
["nell", 760, 720], ["otho", 675, 520], ["remy", 790, 585]
|
||||||
]),
|
]),
|
||||||
enemies: Object.freeze([
|
enemies: Object.freeze([
|
||||||
["ink_blot", 510, 490], ["lost_footnote", 930, 640], ["null_scribe", 520, 780],
|
enemy("ink_blot", 635, 350, { leash: 95, engage: 210, requiresSigil: "gate", questId: "study" }),
|
||||||
["ink_blot", 1320, 380], ["lost_footnote", 120, 640]
|
enemy("lost_footnote", 805, 350, { leash: 95, engage: 210, requiresSigil: "gate", questId: "study" }),
|
||||||
|
enemy("null_scribe", 720, 405, { leash: 105, engage: 280, requiresSigil: "gate", questId: "study" }),
|
||||||
|
enemy("ink_blot", 1110, 625, { leash: 105, engage: 205, requiresSigil: "gate" }),
|
||||||
|
enemy("lost_footnote", 340, 590, { leash: 75, engage: 175, requiresSigil: "gate" })
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
vault: Object.freeze({
|
vault: Object.freeze({
|
||||||
width: 724, height: 543, background: "areas", frame: 0, spawn: { x: 362, y: 485 },
|
width: 724, height: 543, background: "areas", frame: 0, spawn: { x: 362, y: 485 },
|
||||||
|
movement: Object.freeze({ speed: 155, acceleration: 1300, deceleration: 1900, bootAcceleration: 1740 }),
|
||||||
safeSpawns: Object.freeze({ entrance: { x: 362, y: 485 } }),
|
safeSpawns: Object.freeze({ entrance: { x: 362, y: 485 } }),
|
||||||
collisions: Object.freeze([rect(0, 0, 724, 55), rect(0, 0, 45, 543), rect(679, 0, 45, 543), rect(0, 500, 300, 43), rect(424, 500, 300, 43)]),
|
safeZones: Object.freeze([rect(300, 430, 124, 95)]),
|
||||||
|
collisions: Object.freeze([
|
||||||
|
rect(0, 0, 724, 32), rect(0, 0, 34, 543), rect(690, 0, 34, 543),
|
||||||
|
rect(0, 32, 278, 130), rect(446, 32, 278, 130), rect(278, 32, 168, 60),
|
||||||
|
rect(0, 472, 300, 71), rect(424, 472, 300, 71),
|
||||||
|
rect(145, 150, 100, 72), rect(479, 150, 100, 72),
|
||||||
|
rect(72, 272, 95, 70), rect(557, 272, 95, 70),
|
||||||
|
rect(145, 345, 100, 72), rect(479, 345, 100, 72)
|
||||||
|
]),
|
||||||
npcs: Object.freeze([]), enemies: Object.freeze([])
|
npcs: Object.freeze([]), enemies: Object.freeze([])
|
||||||
}),
|
}),
|
||||||
workshop: Object.freeze({
|
workshop: Object.freeze({
|
||||||
width: 724, height: 543, background: "areas", frame: 1, spawn: { x: 362, y: 475 },
|
width: 724, height: 543, background: "areas", frame: 1, spawn: { x: 362, y: 475 },
|
||||||
|
movement: Object.freeze({ speed: 155, acceleration: 1300, deceleration: 1900, bootAcceleration: 1740 }),
|
||||||
safeSpawns: Object.freeze({ entrance: { x: 362, y: 475 } }),
|
safeSpawns: Object.freeze({ entrance: { x: 362, y: 475 } }),
|
||||||
collisions: Object.freeze([rect(0, 0, 724, 48), rect(0, 0, 42, 543), rect(682, 0, 42, 543), rect(0, 500, 300, 43), rect(424, 500, 300, 43), rect(50, 50, 220, 105), rect(470, 70, 190, 95)]),
|
safeZones: Object.freeze([rect(300, 430, 124, 95)]),
|
||||||
npcs: Object.freeze([]), enemies: Object.freeze([["sentinel", 365, 210]])
|
collisions: Object.freeze([
|
||||||
|
rect(0, 0, 724, 32), rect(0, 0, 34, 543), rect(690, 0, 34, 543),
|
||||||
|
rect(34, 32, 656, 115), rect(0, 472, 300, 71), rect(424, 472, 300, 71),
|
||||||
|
rect(34, 145, 120, 118), rect(570, 145, 120, 125),
|
||||||
|
rect(34, 342, 218, 130), rect(520, 360, 170, 112)
|
||||||
|
]),
|
||||||
|
npcs: Object.freeze([]), enemies: Object.freeze([
|
||||||
|
enemy("sentinel", 362, 245, { leash: 205, engage: 360 })
|
||||||
|
])
|
||||||
}),
|
}),
|
||||||
playroom: Object.freeze({
|
playroom: Object.freeze({
|
||||||
width: 724, height: 543, background: "areas", frame: 2, spawn: { x: 362, y: 485 },
|
width: 724, height: 543, background: "areas", frame: 2, spawn: { x: 362, y: 485 },
|
||||||
|
movement: Object.freeze({ speed: 150, acceleration: 1250, deceleration: 1850, bootAcceleration: 1680 }),
|
||||||
safeSpawns: Object.freeze({ entrance: { x: 362, y: 485 } }),
|
safeSpawns: Object.freeze({ entrance: { x: 362, y: 485 } }),
|
||||||
|
safeZones: Object.freeze([rect(300, 445, 124, 80)]),
|
||||||
collisions: Object.freeze([
|
collisions: Object.freeze([
|
||||||
rect(0, 0, 724, 45), rect(0, 0, 42, 543), rect(682, 0, 42, 543), rect(0, 500, 300, 43), rect(424, 500, 300, 43),
|
rect(0, 0, 724, 38), rect(0, 0, 28, 543), rect(696, 0, 28, 543),
|
||||||
rect(100, 95, 210, 42), rect(405, 95, 210, 42), rect(200, 190, 320, 42), rect(90, 285, 220, 42), rect(410, 285, 220, 42), rect(220, 380, 285, 42)
|
rect(0, 488, 300, 55), rect(424, 488, 300, 55),
|
||||||
|
rect(28, 38, 210, 58), rect(294, 38, 186, 58), rect(522, 38, 174, 58),
|
||||||
|
rect(147, 104, 92, 92), rect(345, 96, 72, 118), rect(520, 106, 96, 94),
|
||||||
|
rect(28, 220, 128, 72), rect(217, 214, 118, 78), rect(416, 210, 116, 82), rect(615, 220, 81, 72),
|
||||||
|
rect(112, 315, 126, 78), rect(302, 300, 94, 102), rect(492, 315, 126, 78),
|
||||||
|
rect(28, 408, 118, 80), rect(205, 408, 105, 80), rect(414, 408, 105, 80), rect(578, 408, 118, 80)
|
||||||
]),
|
]),
|
||||||
npcs: Object.freeze([]), enemies: Object.freeze([["lost_footnote", 120, 180], ["ink_blot", 590, 360]])
|
npcs: Object.freeze([]), enemies: Object.freeze([
|
||||||
|
enemy("lost_footnote", 90, 180, { leash: 60, engage: 150 }),
|
||||||
|
enemy("ink_blot", 650, 350, { leash: 55, engage: 145 })
|
||||||
|
])
|
||||||
}),
|
}),
|
||||||
boss: Object.freeze({
|
boss: Object.freeze({
|
||||||
width: 724, height: 543, background: "areas", frame: 3, spawn: { x: 362, y: 470 },
|
width: 724, height: 543, background: "areas", frame: 3, spawn: { x: 362, y: 470 },
|
||||||
|
movement: Object.freeze({ speed: 158, acceleration: 1380, deceleration: 1950, bootAcceleration: 1780 }),
|
||||||
safeSpawns: Object.freeze({ entrance: { x: 362, y: 470 } }),
|
safeSpawns: Object.freeze({ entrance: { x: 362, y: 470 } }),
|
||||||
collisions: Object.freeze([rect(0, 0, 724, 35), rect(0, 0, 35, 543), rect(689, 0, 35, 543), rect(0, 508, 285, 35), rect(439, 508, 285, 35)]),
|
safeZones: Object.freeze([rect(310, 438, 104, 70)]),
|
||||||
npcs: Object.freeze([]), enemies: Object.freeze([["redactor", 362, 190]])
|
collisions: Object.freeze([
|
||||||
|
rect(0, 0, 724, 42), rect(0, 0, 34, 543), rect(690, 0, 34, 543),
|
||||||
|
rect(34, 42, 145, 55), rect(545, 42, 145, 55),
|
||||||
|
rect(34, 97, 48, 350), rect(642, 97, 48, 350),
|
||||||
|
rect(34, 447, 245, 64), rect(445, 447, 245, 64),
|
||||||
|
rect(196, 146, 48, 48), rect(480, 146, 48, 48),
|
||||||
|
rect(196, 326, 48, 48), rect(480, 326, 48, 48)
|
||||||
|
]),
|
||||||
|
npcs: Object.freeze([]), enemies: Object.freeze([
|
||||||
|
enemy("redactor", 362, 190, { leash: 250, engage: 500 })
|
||||||
|
])
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@
|
|||||||
this.enemySerial = 0;
|
this.enemySerial = 0;
|
||||||
this.bossPuzzle = [];
|
this.bossPuzzle = [];
|
||||||
this.bossPuzzleSolved = false;
|
this.bossPuzzleSolved = false;
|
||||||
|
this.controlLockedUntil = 0;
|
||||||
|
this.stepDistance = 0;
|
||||||
|
this.previousPlayerPosition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(data) {
|
init(data) {
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
const area = Data.AREAS[this.areaId];
|
const area = Data.AREAS[this.areaId];
|
||||||
this.area = area;
|
this.area = area;
|
||||||
this.physics.world.setBounds(0, 0, area.width, area.height);
|
this.physics.world.setBounds(0, 0, area.width, area.height);
|
||||||
|
this.cameras.main.setBackgroundColor("#0b1518");
|
||||||
if (area.background === "town") this.add.image(0, 0, "town").setOrigin(0).setDepth(0);
|
if (area.background === "town") this.add.image(0, 0, "town").setOrigin(0).setDepth(0);
|
||||||
else this.add.image(0, 0, "areas", area.frame).setOrigin(0).setDepth(0);
|
else this.add.image(0, 0, "areas", area.frame).setOrigin(0).setDepth(0);
|
||||||
|
|
||||||
@@ -67,6 +71,17 @@
|
|||||||
this.physics.add.existing(block, true);
|
this.physics.add.existing(block, true);
|
||||||
this.collisions.add(block);
|
this.collisions.add(block);
|
||||||
});
|
});
|
||||||
|
this.gateBlocks = [];
|
||||||
|
(area.gates || []).forEach((gate) => {
|
||||||
|
if (controller.getState().sigils.includes(gate.requiresSigil)) return;
|
||||||
|
const block = this.add.rectangle(
|
||||||
|
gate.x + gate.width / 2, gate.y + gate.height / 2,
|
||||||
|
gate.width, gate.height, 0xf4c96b, debug ? 0.2 : 0
|
||||||
|
).setDepth(debug ? 51 : 2);
|
||||||
|
this.physics.add.existing(block, true);
|
||||||
|
this.collisions.add(block);
|
||||||
|
this.gateBlocks.push({ gate, block });
|
||||||
|
});
|
||||||
|
|
||||||
this.createAnimations();
|
this.createAnimations();
|
||||||
this.npcSprites = [];
|
this.npcSprites = [];
|
||||||
@@ -78,7 +93,8 @@
|
|||||||
this.player = this.physics.add.sprite(safe.x, safe.y, `traveler-${state.palette}`, frameFor(this.lastFacing))
|
this.player = this.physics.add.sprite(safe.x, safe.y, `traveler-${state.palette}`, frameFor(this.lastFacing))
|
||||||
.setScale(0.3).setDepth(20);
|
.setScale(0.3).setDepth(20);
|
||||||
this.player.body.setSize(72, 42).setOffset(92, 190);
|
this.player.body.setSize(72, 42).setOffset(92, 190);
|
||||||
this.player.setCollideWorldBounds(true).setDrag(900, 900).setMaxVelocity(190, 190);
|
this.player.setCollideWorldBounds(true).setDrag(0, 0).setMaxVelocity(230, 230);
|
||||||
|
this.previousPlayerPosition = { x: this.player.x, y: this.player.y };
|
||||||
this.physics.add.collider(this.player, this.collisions);
|
this.physics.add.collider(this.player, this.collisions);
|
||||||
|
|
||||||
this.interactables = [];
|
this.interactables = [];
|
||||||
@@ -98,8 +114,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.cameras.main.setBounds(0, 0, area.width, area.height);
|
this.cameras.main.setBounds(0, 0, area.width, area.height);
|
||||||
|
this.cameras.main.setZoom(this.areaId === "town" ? 1 : 1.32);
|
||||||
this.cameras.main.startFollow(this.player, true,
|
this.cameras.main.startFollow(this.player, true,
|
||||||
controller.reducedMotion() ? 1 : 0.14, controller.reducedMotion() ? 1 : 0.14);
|
controller.reducedMotion() ? 1 : 0.24, controller.reducedMotion() ? 1 : 0.24);
|
||||||
|
this.cameras.main.setDeadzone(72, 48);
|
||||||
controller.audio.attach(this, this.areaId);
|
controller.audio.attach(this, this.areaId);
|
||||||
controller.renderHud();
|
controller.renderHud();
|
||||||
controller.status(this.areaId === "town"
|
controller.status(this.areaId === "town"
|
||||||
@@ -152,17 +170,17 @@
|
|||||||
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 });
|
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", 115, 550, "memory_fragment", 2);
|
this.addChest("town-garden", 205, 565, "memory_fragment", 2);
|
||||||
this.addChest("town-wall", 1280, 870, "ink_vial", 2);
|
this.addChest("town-wall", 1235, 840, "ink_vial", 2);
|
||||||
} else {
|
} else {
|
||||||
this.addPortal("town", this.area.width / 2, this.area.height - 38, "Return to Archive Town");
|
this.addPortal("town", this.area.width / 2, this.area.height - 38, "Return to Archive Town");
|
||||||
if (this.areaId === "vault") {
|
if (this.areaId === "vault") {
|
||||||
this.addPortal("boss", this.area.width / 2, 95, controller.getState().story.ending ? "Visit the restored core" : "Confront The Redactor");
|
this.addPortal("boss", this.area.width / 2, 120, controller.getState().story.ending ? "Visit the restored core" : "Confront The Redactor");
|
||||||
this.addChest("vault-lore", 100, 420, "lore_page", 2);
|
this.addChest("vault-lore", 100, 420, "lore_page", 2);
|
||||||
}
|
}
|
||||||
if (this.areaId === "playroom") {
|
if (this.areaId === "playroom") {
|
||||||
this.interactables.push({ type: "challenge", id: "playroom", x: this.area.width / 2, y: 110 });
|
this.interactables.push({ type: "challenge", id: "playroom", x: this.area.width / 2, y: 255 });
|
||||||
this.add.circle(this.area.width / 2, 110, 22, 0xf8d879, 0.45).setDepth(5);
|
this.add.circle(this.area.width / 2, 255, 22, 0xf8d879, 0.45).setDepth(5);
|
||||||
}
|
}
|
||||||
if (this.areaId === "boss") this.createBossPedestals();
|
if (this.areaId === "boss") this.createBossPedestals();
|
||||||
}
|
}
|
||||||
@@ -197,19 +215,26 @@
|
|||||||
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.area.enemies.forEach(([type, x, y]) => {
|
this.physics.add.collider(this.enemies, this.enemies);
|
||||||
|
this.area.enemies.forEach((entry) => {
|
||||||
|
const spawn = Array.isArray(entry)
|
||||||
|
? { type: entry[0], x: entry[1], y: entry[2] }
|
||||||
|
: entry;
|
||||||
|
const { type, x, y } = spawn;
|
||||||
if ((type === "sentinel" || type === "redactor") && controller.getState().defeatedBosses.includes(type)) return;
|
if ((type === "sentinel" || type === "redactor") && controller.getState().defeatedBosses.includes(type)) return;
|
||||||
this.spawnEnemy(type, x, y);
|
this.spawnEnemy(type, x, y, spawn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnEnemy(type, x, y) {
|
spawnEnemy(type, x, y, spawn) {
|
||||||
const spec = Data.ENEMIES[type];
|
const spec = Data.ENEMIES[type];
|
||||||
const enemy = this.physics.add.sprite(x, y, "cast", spec.frame).setScale(spec.boss ? 0.31 : 0.21).setDepth(y + 30);
|
const enemy = this.physics.add.sprite(x, y, "cast", spec.frame).setScale(spec.boss ? 0.31 : 0.21).setDepth(y + 30);
|
||||||
enemy.body.setSize(spec.boss ? 155 : 115, spec.boss ? 85 : 65).setOffset(spec.boss ? 79 : 99, spec.boss ? 196 : 200);
|
enemy.body.setSize(spec.boss ? 155 : 115, spec.boss ? 85 : 65).setOffset(spec.boss ? 79 : 99, spec.boss ? 196 : 200);
|
||||||
enemy.setData({
|
enemy.setData({
|
||||||
id: `${type}-${++this.enemySerial}`, type, spec, health: spec.health,
|
id: `${type}-${++this.enemySerial}`, type, spec, health: spec.health,
|
||||||
originX: x, originY: y, nextAction: this.time.now + 900, phase: 1
|
originX: x, originY: y, leash: spawn && spawn.leash || 150,
|
||||||
|
engage: spawn && spawn.engage || 245, requiresSigil: spawn && spawn.requiresSigil || null,
|
||||||
|
questId: spawn && spawn.questId || null, nextAction: this.time.now + 900, phase: 1
|
||||||
});
|
});
|
||||||
this.enemies.add(enemy);
|
this.enemies.add(enemy);
|
||||||
return enemy;
|
return enemy;
|
||||||
@@ -218,10 +243,15 @@
|
|||||||
update(time, delta) {
|
update(time, delta) {
|
||||||
if (!this.player) return;
|
if (!this.player) return;
|
||||||
if (controller.locked()) {
|
if (controller.locked()) {
|
||||||
this.player.setAcceleration(0).setVelocity(0);
|
this.player.setVelocity(0);
|
||||||
this.player.anims.stop();
|
this.player.anims.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const traveled = Math.hypot(
|
||||||
|
this.player.x - this.previousPlayerPosition.x,
|
||||||
|
this.player.y - this.previousPlayerPosition.y
|
||||||
|
);
|
||||||
|
this.previousPlayerPosition = { x: this.player.x, y: this.player.y };
|
||||||
let dx = 0;
|
let dx = 0;
|
||||||
let dy = 0;
|
let dy = 0;
|
||||||
if (this.cursors.left.isDown || this.keys.left.isDown || controller.move.left) dx -= 1;
|
if (this.cursors.left.isDown || this.keys.left.isDown || controller.move.left) dx -= 1;
|
||||||
@@ -230,20 +260,33 @@
|
|||||||
if (this.cursors.down.isDown || this.keys.down.isDown || controller.move.down) dy += 1;
|
if (this.cursors.down.isDown || this.keys.down.isDown || controller.move.down) dy += 1;
|
||||||
const moving = dx !== 0 || dy !== 0;
|
const moving = dx !== 0 || dy !== 0;
|
||||||
const state = controller.getState();
|
const state = controller.getState();
|
||||||
const acceleration = state.equipment.boots ? 980 : 760;
|
this.gateBlocks = this.gateBlocks.filter((entry) => {
|
||||||
if (moving) {
|
if (!state.sigils.includes(entry.gate.requiresSigil)) return true;
|
||||||
const vector = Systems.normalizedVector(dx, dy, acceleration);
|
entry.block.destroy();
|
||||||
this.player.setAcceleration(vector.x, vector.y);
|
controller.status("The Town Gate opens. Archive Town is ready to be explored.");
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const acceptingMovement = time >= this.controlLockedUntil;
|
||||||
|
if (acceptingMovement) {
|
||||||
|
const velocity = Systems.approachVelocity(
|
||||||
|
this.player.body.velocity.x, this.player.body.velocity.y,
|
||||||
|
dx, dy, this.area.movement, delta, state.equipment.boots
|
||||||
|
);
|
||||||
|
this.player.setVelocity(velocity.x, velocity.y);
|
||||||
|
}
|
||||||
|
if (moving && acceptingMovement) {
|
||||||
this.lastFacing = Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? "west" : "east") : (dy < 0 ? "north" : "south");
|
this.lastFacing = Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? "west" : "east") : (dy < 0 ? "north" : "south");
|
||||||
this.player.anims.play(`traveler-${state.palette}-walk-${this.lastFacing}`, true);
|
this.player.anims.play(`traveler-${state.palette}-walk-${this.lastFacing}`, true);
|
||||||
if (state.settings.soundEnabled && time - this.lastStepAt > 350) {
|
this.stepDistance += traveled;
|
||||||
|
if (state.settings.soundEnabled && this.stepDistance >= 44) {
|
||||||
controller.audio.play("step", { rate: 0.96 + Math.random() * 0.08 });
|
controller.audio.play("step", { rate: 0.96 + Math.random() * 0.08 });
|
||||||
this.lastStepAt = time;
|
this.lastStepAt = time;
|
||||||
|
this.stepDistance = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.player.setAcceleration(0);
|
|
||||||
this.player.anims.stop();
|
this.player.anims.stop();
|
||||||
this.player.setFrame(frameFor(this.lastFacing));
|
this.player.setFrame(frameFor(this.lastFacing));
|
||||||
|
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.npcSprites.forEach((npc) => npc.setFlipX(this.player.x < npc.x));
|
||||||
@@ -351,7 +394,7 @@
|
|||||||
controller.audio.play(type === "redactor" ? "victory" : "defeat");
|
controller.audio.play(type === "redactor" ? "victory" : "defeat");
|
||||||
let state = Systems.grantXp(controller.getState(), spec.xp);
|
let state = Systems.grantXp(controller.getState(), spec.xp);
|
||||||
if (type === "sentinel" || type === "redactor") state = Systems.recordBoss(state, type);
|
if (type === "sentinel" || type === "redactor") state = Systems.recordBoss(state, type);
|
||||||
if (state.quests.study.status === "active" && this.areaId === "town") {
|
if (enemy.getData("questId") === "study" && state.quests.study.status === "active") {
|
||||||
state = Systems.progressQuest(state, "study", 1);
|
state = Systems.progressQuest(state, "study", 1);
|
||||||
if (state.quests.study.count >= Data.QUESTS.study.target) state = Systems.completeQuest(state, "study");
|
if (state.quests.study.count >= Data.QUESTS.study.target) state = Systems.completeQuest(state, "study");
|
||||||
}
|
}
|
||||||
@@ -368,22 +411,40 @@
|
|||||||
this.enemies.getChildren().forEach((enemy) => {
|
this.enemies.getChildren().forEach((enemy) => {
|
||||||
if (!enemy.active) return;
|
if (!enemy.active) return;
|
||||||
const spec = enemy.getData("spec");
|
const spec = enemy.getData("spec");
|
||||||
|
const required = enemy.getData("requiresSigil");
|
||||||
|
const available = !required || controller.getState().sigils.includes(required);
|
||||||
|
enemy.setVisible(available);
|
||||||
|
enemy.body.enable = available;
|
||||||
|
if (!available) return;
|
||||||
const distance = Phaser.Math.Distance.Between(enemy.x, enemy.y, this.player.x, this.player.y);
|
const distance = Phaser.Math.Distance.Between(enemy.x, enemy.y, this.player.x, this.player.y);
|
||||||
|
const homeDistance = Phaser.Math.Distance.Between(
|
||||||
|
enemy.x, enemy.y, enemy.getData("originX"), enemy.getData("originY")
|
||||||
|
);
|
||||||
|
const playerInSafeZone = Systems.isCombatSafe(this.areaId, this.player.x, this.player.y);
|
||||||
|
if (playerInSafeZone || homeDistance > enemy.getData("leash")) {
|
||||||
|
if (homeDistance > 8) {
|
||||||
|
this.physics.moveTo(enemy, enemy.getData("originX"), enemy.getData("originY"), spec.speed);
|
||||||
|
} else {
|
||||||
|
enemy.setPosition(enemy.getData("originX"), enemy.getData("originY")).setVelocity(0);
|
||||||
|
}
|
||||||
|
enemy.setDepth(enemy.y + 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (spec.behaviour === "chase") {
|
if (spec.behaviour === "chase") {
|
||||||
if (distance < 280) this.physics.moveToObject(enemy, this.player, spec.speed);
|
if (distance < enemy.getData("engage")) this.physics.moveToObject(enemy, this.player, spec.speed);
|
||||||
else enemy.setVelocity(0);
|
else this.returnEnemyHome(enemy, spec.speed);
|
||||||
} else if (spec.behaviour === "wander") {
|
} else if (spec.behaviour === "wander") {
|
||||||
if (time > enemy.getData("nextAction")) {
|
if (distance < enemy.getData("engage") && time > enemy.getData("nextAction")) {
|
||||||
enemy.setData("nextAction", time + 650 + Math.random() * 800);
|
enemy.setData("nextAction", time + 650 + Math.random() * 800);
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
enemy.setVelocity(Math.cos(angle) * spec.speed, Math.sin(angle) * spec.speed);
|
enemy.setVelocity(Math.cos(angle) * spec.speed, Math.sin(angle) * spec.speed);
|
||||||
}
|
} else if (distance >= enemy.getData("engage")) this.returnEnemyHome(enemy, spec.speed);
|
||||||
} else if (spec.behaviour === "ranged") {
|
} else if (spec.behaviour === "ranged") {
|
||||||
if (distance < 340 && time > enemy.getData("nextAction")) {
|
if (distance < enemy.getData("engage") && time > enemy.getData("nextAction")) {
|
||||||
enemy.setData("nextAction", time + (controller.getState().settings.assist ? 1900 : 1350));
|
enemy.setData("nextAction", time + (controller.getState().settings.assist ? 1900 : 1350));
|
||||||
this.fireProjectile(enemy, spec.damage);
|
this.fireProjectile(enemy, spec.damage);
|
||||||
} else if (distance < 170) this.physics.moveToObject(enemy, this.player, -spec.speed);
|
} else if (distance < 170) this.physics.moveToObject(enemy, this.player, -spec.speed);
|
||||||
else enemy.setVelocity(0);
|
else this.returnEnemyHome(enemy, spec.speed);
|
||||||
} else if (spec.behaviour === "charge") {
|
} else if (spec.behaviour === "charge") {
|
||||||
if (time > enemy.getData("nextAction")) {
|
if (time > enemy.getData("nextAction")) {
|
||||||
enemy.setTint(0xffd36e);
|
enemy.setTint(0xffd36e);
|
||||||
@@ -403,6 +464,14 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
returnEnemyHome(enemy, speed) {
|
||||||
|
const distance = Phaser.Math.Distance.Between(
|
||||||
|
enemy.x, enemy.y, enemy.getData("originX"), enemy.getData("originY")
|
||||||
|
);
|
||||||
|
if (distance > 8) this.physics.moveTo(enemy, enemy.getData("originX"), enemy.getData("originY"), speed);
|
||||||
|
else enemy.setVelocity(0);
|
||||||
|
}
|
||||||
|
|
||||||
updateRedactor(enemy, time, distance) {
|
updateRedactor(enemy, time, distance) {
|
||||||
const health = enemy.getData("health");
|
const health = enemy.getData("health");
|
||||||
const phase = health > 20 ? 1 : health > 10 ? 2 : 3;
|
const phase = health > 20 ? 1 : health > 10 ? 2 : 3;
|
||||||
@@ -477,7 +546,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
damagePlayer(amount, fromX, fromY) {
|
damagePlayer(amount, fromX, fromY) {
|
||||||
if (this.areaId === "town" && isTownSafe(this.player.x, this.player.y)) return;
|
if (Systems.isCombatSafe(this.areaId, this.player.x, this.player.y)) return;
|
||||||
const result = Systems.takeDamage(controller.getState(), amount, this.time.now, this.lastHitAt);
|
const result = Systems.takeDamage(controller.getState(), amount, this.time.now, this.lastHitAt);
|
||||||
if (!result.hit) return;
|
if (!result.hit) return;
|
||||||
this.lastHitAt = this.time.now;
|
this.lastHitAt = this.time.now;
|
||||||
@@ -485,6 +554,7 @@
|
|||||||
controller.audio.play("damage");
|
controller.audio.play("damage");
|
||||||
const direction = Systems.normalizedVector(this.player.x - fromX, this.player.y - fromY, 220);
|
const direction = Systems.normalizedVector(this.player.x - fromX, this.player.y - fromY, 220);
|
||||||
this.player.setVelocity(direction.x, direction.y).setTint(0xffb2b2);
|
this.player.setVelocity(direction.x, direction.y).setTint(0xffb2b2);
|
||||||
|
this.controlLockedUntil = this.time.now + 180;
|
||||||
this.time.delayedCall(160, () => { if (this.player.active) this.player.clearTint(); });
|
this.time.delayedCall(160, () => { if (this.player.active) this.player.clearTint(); });
|
||||||
if (!controller.reducedMotion()) this.cameras.main.shake(90, 0.003);
|
if (!controller.reducedMotion()) this.cameras.main.shake(90, 0.003);
|
||||||
if (result.defeated) {
|
if (result.defeated) {
|
||||||
@@ -501,8 +571,8 @@
|
|||||||
persistPosition(checkpoint) {
|
persistPosition(checkpoint) {
|
||||||
if (!this.player) return;
|
if (!this.player) return;
|
||||||
let state = StateWithSafe(controller.getState(), this.areaId, this.player.x, this.player.y, this.lastFacing);
|
let state = StateWithSafe(controller.getState(), this.areaId, this.player.x, this.player.y, this.lastFacing);
|
||||||
if (checkpoint || this.areaId === "town") {
|
if (checkpoint || Systems.isCombatSafe(this.areaId, this.player.x, this.player.y)) {
|
||||||
const safe = Systems.nearestSafeSpawn(this.areaId, this.player.x, this.player.y);
|
const safe = Systems.nearestNamedSafeSpawn(this.areaId, this.player.x, this.player.y);
|
||||||
state = root.ArchiveWorldState.withCheckpoint(state, safe.area, safe.spawn, safe.x, safe.y);
|
state = root.ArchiveWorldState.withCheckpoint(state, safe.area, safe.spawn, safe.x, safe.y);
|
||||||
}
|
}
|
||||||
controller.setState(state, null, true);
|
controller.setState(state, null, true);
|
||||||
@@ -538,9 +608,5 @@
|
|||||||
return String(value).replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
return String(value).replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTownSafe(x, y) {
|
|
||||||
return y > 835 || (x > 570 && x < 875 && y > 405 && y < 690);
|
|
||||||
}
|
|
||||||
|
|
||||||
root.ArchiveWorldScenes = Object.freeze({ createSceneClasses });
|
root.ArchiveWorldScenes = Object.freeze({ createSceneClasses });
|
||||||
}(typeof globalThis !== "undefined" ? globalThis : this));
|
}(typeof globalThis !== "undefined" ? globalThis : this));
|
||||||
|
|||||||
@@ -18,6 +18,30 @@
|
|||||||
return { x: (x / length) * speed, y: (y / length) * speed };
|
return { x: (x / length) * speed, y: (y / length) * speed };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveToward(value, target, maximumDelta) {
|
||||||
|
if (Math.abs(target - value) <= maximumDelta) return target;
|
||||||
|
return value + Math.sign(target - value) * maximumDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
function approachVelocity(currentX, currentY, inputX, inputY, movement, deltaMs, hasBoots) {
|
||||||
|
const config = movement || {};
|
||||||
|
const speed = Math.max(1, Number(config.speed) || 160);
|
||||||
|
const moving = Boolean(Number(inputX) || Number(inputY));
|
||||||
|
const target = moving ? normalizedVector(inputX, inputY, speed) : { x: 0, y: 0 };
|
||||||
|
const rate = moving
|
||||||
|
? (hasBoots ? Number(config.bootAcceleration) || 1700 : Number(config.acceleration) || 1300)
|
||||||
|
: Number(config.deceleration) || 1900;
|
||||||
|
const step = Math.max(0, Math.min(50, Number(deltaMs) || 0)) / 1000 * rate;
|
||||||
|
let x = moveToward(Number(currentX) || 0, target.x, step);
|
||||||
|
let y = moveToward(Number(currentY) || 0, target.y, step);
|
||||||
|
const length = Math.hypot(x, y);
|
||||||
|
if (length > speed) {
|
||||||
|
x = x / length * speed;
|
||||||
|
y = y / length * speed;
|
||||||
|
}
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
function discover(state, landmarkId) {
|
function discover(state, landmarkId) {
|
||||||
const next = copy(state);
|
const next = copy(state);
|
||||||
if (!next || !Data.LANDMARK_IDS.includes(landmarkId)) return next;
|
if (!next || !Data.LANDMARK_IDS.includes(landmarkId)) return next;
|
||||||
@@ -164,11 +188,22 @@
|
|||||||
return !area.collisions.some((item) => pointInRect(x, y, item, 10));
|
return !area.collisions.some((item) => pointInRect(x, y, item, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCombatSafe(areaId, x, y) {
|
||||||
|
const area = Data.AREAS[areaId];
|
||||||
|
return Boolean(area && (area.safeZones || []).some((item) => pointInRect(x, y, item, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestNamedSafeSpawn(areaId, x, y) {
|
||||||
|
const area = Data.AREAS[areaId] || Data.AREAS.town;
|
||||||
|
const points = Object.entries(area.safeSpawns || { entrance: area.spawn });
|
||||||
const best = points.sort((a, b) =>
|
const best = points.sort((a, b) =>
|
||||||
Math.hypot(a[1].x - x, a[1].y - y) - Math.hypot(b[1].x - x, b[1].y - y))[0];
|
Math.hypot(a[1].x - x, a[1].y - y) - Math.hypot(b[1].x - x, b[1].y - y))[0];
|
||||||
return { area: areaId, spawn: best[0], x: best[1].x, y: best[1].y };
|
return { area: areaId, spawn: best[0], x: best[1].x, y: best[1].y };
|
||||||
@@ -186,7 +221,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
normalizedVector, discover, startQuest, progressQuest, completeQuest, updateStory, addItem, itemQuantity,
|
normalizedVector, approachVelocity, discover, startQuest, progressQuest, completeQuest, updateStory, addItem, itemQuantity,
|
||||||
consumeItem, grantXp, takeDamage, respawn, recordBoss, isSafe, nearestSafeSpawn, currentObjective
|
consumeItem, grantXp, takeDamage, respawn, recordBoss, isSafe, isCombatSafe,
|
||||||
|
nearestSafeSpawn, nearestNamedSafeSpawn, currentObjective
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -80,6 +80,8 @@
|
|||||||
else if (questState.status === "active") {
|
else if (questState.status === "active") {
|
||||||
action = quest.kind === "combat"
|
action = quest.kind === "combat"
|
||||||
? `<p class="rpg-panel__note">Combat progress: ${questState.count} / ${quest.target}</p><button type="button" data-action="return">Return to the fight</button>`
|
? `<p class="rpg-panel__note">Combat progress: ${questState.count} / ${quest.target}</p><button type="button" data-action="return">Return to the fight</button>`
|
||||||
|
: id === "playroom"
|
||||||
|
? `<button type="button" data-action="travel" data-area="playroom">Enter the lantern maze</button>`
|
||||||
: quest.kind === "boss"
|
: quest.kind === "boss"
|
||||||
? `<button type="button" data-action="travel" data-area="workshop">Enter the repair yard</button>`
|
? `<button type="button" data-action="travel" data-area="workshop">Enter the repair yard</button>`
|
||||||
: `<button type="button" data-action="challenge" data-id="${place.questId}">Continue challenge</button>`;
|
: `<button type="button" data-action="challenge" data-id="${place.questId}">Continue challenge</button>`;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
const loaded = loadState();
|
const loaded = loadState();
|
||||||
if (loaded.state) {
|
if (loaded.state) {
|
||||||
controller.state = loaded.state;
|
controller.state = developmentAreaOverride(loaded.state);
|
||||||
renderHud();
|
renderHud();
|
||||||
startGame();
|
startGame();
|
||||||
if (loaded.message) status(loaded.message);
|
if (loaded.message) status(loaded.message);
|
||||||
@@ -60,6 +60,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function developmentAreaOverride(state) {
|
||||||
|
const local = ["localhost", "127.0.0.1"].includes(window.location.hostname);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const areaId = params.get("collisionDebug") === "1" ? params.get("area") : null;
|
||||||
|
if (!local || !Data.AREAS[areaId]) return state;
|
||||||
|
const spawn = Data.AREAS[areaId].spawn;
|
||||||
|
return State.withPosition(state, areaId, spawn.x, spawn.y, "north", "entrance");
|
||||||
|
}
|
||||||
|
|
||||||
function loadState() {
|
function loadState() {
|
||||||
try {
|
try {
|
||||||
const rawV2 = localStorage.getItem(State.STORAGE_KEY);
|
const rawV2 = localStorage.getItem(State.STORAGE_KEY);
|
||||||
|
|||||||
@@ -772,3 +772,9 @@ at <ScriptBlock>, /home/zaine/master-folder/org-platform/org_web/build-logs/gite
|
|||||||
2026-07-30T01:01:00.6776757+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
2026-07-30T01:01:00.6776757+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
2026-07-30T01:01:00.7510165+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
2026-07-30T01:01:00.7510165+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
2026-07-30T01:01:01.0172871+01:00 [INFO] Sent authoring server test notification.
|
2026-07-30T01:01:01.0172871+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-30T09:08:53.9348501+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-30T09:08:53.9423657+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-30T09:08:55.0348149+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
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.
|
||||||
|
|||||||
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>
|
<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.0.1" defer></script>
|
<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.0.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.0.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.0.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.0.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.0.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.0.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.0.1" defer></script>
|
<script src="/assets/scripts/pages/archive-world.js?v=archive-world-2.1.1" defer></script>
|
||||||
#+END_EXPORT
|
#+END_EXPORT
|
||||||
|
|||||||
@@ -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">29-07-2026 21:47</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">30-07-2026 01:00</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>@@
|
||||||
|
|||||||
20
sitemap.org
20
sitemap.org
@@ -104,13 +104,13 @@ flowchart TD
|
|||||||
n43 --> n49
|
n43 --> n49
|
||||||
n50["Tag: website"]
|
n50["Tag: website"]
|
||||||
n43 --> n50
|
n43 --> n50
|
||||||
n51["Tag: update"]
|
n51["Tag: life"]
|
||||||
n43 --> n51
|
n43 --> n51
|
||||||
n52["Tag: life"]
|
n52["Tag: update"]
|
||||||
n43 --> n52
|
n43 --> n52
|
||||||
n53["Tag: education"]
|
n53["Tag: insights"]
|
||||||
n43 --> n53
|
n43 --> n53
|
||||||
n54["Tag: insights"]
|
n54["Tag: education"]
|
||||||
n43 --> n54
|
n43 --> n54
|
||||||
n55["Tag: reading"]
|
n55["Tag: reading"]
|
||||||
n43 --> n55
|
n43 --> n55
|
||||||
@@ -224,10 +224,10 @@ flowchart TD
|
|||||||
click n48 "tags/notes.html" "Tag: notes"
|
click n48 "tags/notes.html" "Tag: notes"
|
||||||
click n49 "tags/review.html" "Tag: review"
|
click n49 "tags/review.html" "Tag: review"
|
||||||
click n50 "tags/website.html" "Tag: website"
|
click n50 "tags/website.html" "Tag: website"
|
||||||
click n51 "tags/update.html" "Tag: update"
|
click n51 "tags/life.html" "Tag: life"
|
||||||
click n52 "tags/life.html" "Tag: life"
|
click n52 "tags/update.html" "Tag: update"
|
||||||
click n53 "tags/education.html" "Tag: education"
|
click n53 "tags/insights.html" "Tag: insights"
|
||||||
click n54 "tags/insights.html" "Tag: insights"
|
click n54 "tags/education.html" "Tag: education"
|
||||||
click n55 "tags/reading.html" "Tag: reading"
|
click n55 "tags/reading.html" "Tag: reading"
|
||||||
click n56 "tags/emacs.html" "Tag: emacs"
|
click n56 "tags/emacs.html" "Tag: emacs"
|
||||||
click n57 "tags/maths.html" "Tag: maths"
|
click n57 "tags/maths.html" "Tag: maths"
|
||||||
@@ -318,10 +318,10 @@ flowchart TD
|
|||||||
- [[file:tags/notes.org][Tag: notes]]
|
- [[file:tags/notes.org][Tag: notes]]
|
||||||
- [[file:tags/review.org][Tag: review]]
|
- [[file:tags/review.org][Tag: review]]
|
||||||
- [[file:tags/website.org][Tag: website]]
|
- [[file:tags/website.org][Tag: website]]
|
||||||
- [[file:tags/update.org][Tag: update]]
|
|
||||||
- [[file:tags/life.org][Tag: life]]
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[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/reading.org][Tag: reading]]
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
|
|||||||
@@ -11,6 +11,24 @@ test("movement normalization prevents faster diagonals", () => {
|
|||||||
assert.equal(Math.round(Math.hypot(diagonal.x, diagonal.y)), 175);
|
assert.equal(Math.round(Math.hypot(diagonal.x, diagonal.y)), 175);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("movement acceleration is frame-rate aware, responsive, and speed capped", () => {
|
||||||
|
const movement = data.AREAS.town.movement;
|
||||||
|
const first = systems.approachVelocity(0, 0, 1, 1, movement, 16, false);
|
||||||
|
assert.ok(Math.hypot(first.x, first.y) > 0);
|
||||||
|
assert.ok(Math.hypot(first.x, first.y) < movement.speed);
|
||||||
|
|
||||||
|
let velocity = { x: 0, y: 0 };
|
||||||
|
for (let frame = 0; frame < 120; frame += 1) {
|
||||||
|
velocity = systems.approachVelocity(velocity.x, velocity.y, 1, 1, movement, 16, false);
|
||||||
|
}
|
||||||
|
assert.equal(Math.round(Math.hypot(velocity.x, velocity.y)), movement.speed);
|
||||||
|
|
||||||
|
for (let frame = 0; frame < 20; frame += 1) {
|
||||||
|
velocity = systems.approachVelocity(velocity.x, velocity.y, 0, 0, movement, 16, false);
|
||||||
|
}
|
||||||
|
assert.deepEqual(velocity, { x: 0, y: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
test("landmark discoveries deduplicate and complete at eight", () => {
|
test("landmark discoveries deduplicate and complete at eight", () => {
|
||||||
let current = state.fresh("Ada", "brass");
|
let current = state.fresh("Ada", "brass");
|
||||||
current = systems.discover(current, "gate");
|
current = systems.discover(current, "gate");
|
||||||
@@ -83,3 +101,27 @@ test("invalid collision positions resolve to a named safe spawn", () => {
|
|||||||
assert.equal(systems.isSafe(safe.area, safe.x, safe.y), true);
|
assert.equal(systems.isSafe(safe.area, safe.x, safe.y), true);
|
||||||
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("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]) => {
|
||||||
|
assert.equal(systems.isSafe(areaId, spawn.x, spawn.y), true, `${areaId}:${spawnId}`);
|
||||||
|
});
|
||||||
|
area.enemies.forEach((entry) => {
|
||||||
|
const spawn = Array.isArray(entry)
|
||||||
|
? { type: entry[0], x: entry[1], y: entry[2] }
|
||||||
|
: entry;
|
||||||
|
assert.equal(systems.isSafe(areaId, spawn.x, spawn.y), true, `${areaId}:${spawn.type}`);
|
||||||
|
assert.equal(systems.isCombatSafe(areaId, spawn.x, spawn.y), false, `${areaId}:${spawn.type} is in a safe zone`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("town landmarks and NPC interaction points remain reachable", () => {
|
||||||
|
Object.values(data.LANDMARKS).forEach((landmark) => {
|
||||||
|
assert.equal(systems.isSafe("town", landmark.x, landmark.y), true, `landmark:${landmark.id}`);
|
||||||
|
});
|
||||||
|
data.AREAS.town.npcs.forEach(([npcId, x, y]) => {
|
||||||
|
assert.equal(systems.isSafe("town", x, y), true, `npc:${npcId}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user