Compare commits

..

No commits in common. "051c47ef077221b046839bc4d1bfda028ed2bfd6" and "b0ef4f30a93c55eca98375ffe06d2ebb3ee9c159" have entirely different histories.

84 changed files with 301 additions and 4798 deletions

View file

@ -11,11 +11,8 @@
- Inspect project config (`package.json`, etc.) for available scripts. - Inspect project config (`package.json`, etc.) for available scripts.
- Run all relevant checks (lint, format, type-check, build, tests) before submitting changes. - Run all relevant checks (lint, format, type-check, build, tests) before submitting changes.
- Always use `npm test` or `npm run test` to run tests.
- Never claim checks passed unless they were actually run. - Never claim checks passed unless they were actually run.
- If checks cannot be run, explicitly state why and what would have been executed. - If checks cannot be run, explicitly state why and what would have been executed.
- when making changes to ui elements, consult the custom-elements.json file to ensure that the changes are compatible with the existing codebase.
- after changes are made to ui elements, run `npm run cem` to regenerate custom-elements.json
## SCM ## SCM

View file

@ -1,145 +0,0 @@
import os
path = "src/generation/CrystalSpiresGenerator.js"
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
start_idx = -1
for i, line in enumerate(lines):
if "ensureGlobalConnectivity(spires) {" in line:
start_idx = i
break
if start_idx == -1:
print("Error: ensureGlobalConnectivity not found")
exit(1)
# Find the LAST closing brace in the file (Class End)
class_end_idx = -1
for i in range(len(lines)-1, 0, -1):
if lines[i].strip() == "}":
class_end_idx = i
break
if class_end_idx == -1:
print("Error: Class end not found")
exit(1)
# New Content (With Logs AND robust logic)
new_code = """ ensureGlobalConnectivity(spires) {
console.log("Ensure Connectivity running...");
if (!spires || spires.length === 0) return;
const adj = new Map();
const platById = new Map();
const getPlatId = (sIdx, pIdx) => `${sIdx}:${pIdx}`;
let nodeCount = 0;
spires.forEach((s, sIdx) => {
s.platforms.forEach((p, pIdx) => {
const id = getPlatId(sIdx, pIdx);
adj.set(id, {id, p, neighbors: new Set()});
platById.set(id, p);
nodeCount++;
});
});
console.log(`Adjacency Graph Nodes: ${nodeCount}`);
let edgeCount = 0;
this.generatedAssets.bridges.forEach(b => {
if (b.fromPlatIdx !== undefined && b.toPlatIdx !== undefined) {
const idA = getPlatId(b.fromSpire, b.fromPlatIdx);
const idB = getPlatId(b.toSpire, b.toPlatIdx);
if(adj.has(idA) && adj.has(idB)) {
adj.get(idA).neighbors.add(idB);
adj.get(idB).neighbors.add(idA);
edgeCount++;
}
} else {
console.log("Bridge missing indices:", b);
}
});
console.log(`Adjacency Graph Edges: ${edgeCount}`);
const visited = new Set();
const components = [];
for(const [id, node] of adj) {
if(!visited.has(id)) {
const comp = [];
const q = [id];
visited.add(id);
while(q.length > 0) {
const curr = q.shift();
comp.push(curr);
adj.get(curr).neighbors.forEach(nId => {
if(!visited.has(nId)) {
visited.add(nId);
q.push(nId);
}
});
}
components.push(comp);
}
}
console.log(`Connected Components: ${components.length}`);
if (components.length > 1) {
components.sort((a,b) => b.length - a.length);
const targetComp = components[0];
const targetNodeId = targetComp[0];
const targetPlat = platById.get(targetNodeId);
for(let i=1; i<components.length; i++) {
const orphanComp = components[i];
const orphanNodeId = orphanComp[0];
const orphanPlat = platById.get(orphanNodeId);
console.log(`Linking Component ${i} (Size ${orphanComp.length}) to Main`);
this.placeTeleporter(targetPlat, orphanPlat);
}
}
}
placeTeleporter(pA, pB) {
const findSpot = (p) => {
const r = p.radius - 1;
for(let x=Math.round(p.x-r); x<=Math.round(p.x+r); x++) {
for(let z=Math.round(p.z-r); z<=Math.round(p.z+r); z++) {
const y = Math.round(p.y);
const floor = this.grid.getCell(x, y, z);
const air = this.grid.getCell(x, y+1, z);
if (floor !== 0 && air === 0) {
return {x, y: y+1, z};
}
}
}
return null;
};
const sA = findSpot(pA);
const sB = findSpot(pB);
if(sA && sB) {
this.grid.setCell(sA.x, sA.y, sA.z, 22);
this.grid.setCell(sB.x, sB.y, sB.z, 22);
console.log(`Placed Teleporter between (${sA.x},${sA.y},${sA.z}) and (${sB.x},${sB.y},${sB.z})`);
} else {
console.log("Failed to find spot for Teleporter");
}
}
}
"""
# Replace from start_idx to class_end_idx + 1 (since new_code includes })
final_lines = lines[:start_idx] + [new_code]
# Note: new_code ends with "}\n".
# We dropped `lines[class_end_idx]` (the old closing brace).
# We also dropped everything between start_idx and class_end_idx.
with open(path, "w", encoding="utf-8") as f:
f.writelines(final_lines)
print("Cleanup Complete")

View file

@ -1,903 +0,0 @@
{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/ui/combat-hud.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "CombatHUD",
"members": [
{
"kind": "method",
"name": "_handleGameStateChange",
"parameters": [
{
"name": "e"
}
]
},
{
"kind": "method",
"name": "_handleMissionVictory"
},
{
"kind": "method",
"name": "_handleSkillClick",
"parameters": [
{
"name": "skillId"
}
]
},
{
"kind": "method",
"name": "_handleEndTurn",
"parameters": [
{
"name": "event"
}
]
},
{
"kind": "method",
"name": "_handleMovementClick"
},
{
"kind": "method",
"name": "_handleSkillHover",
"parameters": [
{
"name": "skillId"
}
]
},
{
"kind": "method",
"name": "_handlePortraitClick",
"parameters": [
{
"name": "unit"
}
]
},
{
"kind": "method",
"name": "_getThreatLevel"
},
{
"kind": "method",
"name": "_renderBar",
"parameters": [
{
"name": "label"
},
{
"name": "current"
},
{
"name": "max"
},
{
"name": "type"
}
]
},
{
"kind": "field",
"name": "combatState",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "combatState"
},
{
"kind": "field",
"name": "hidden",
"privacy": "public",
"type": {
"text": "boolean"
},
"default": "false",
"attribute": "hidden",
"reflects": true
},
{
"kind": "field",
"name": "_missionVictoryHandler"
},
{
"kind": "field",
"name": "_gameStateChangeHandler"
}
],
"events": [
{
"name": "skill-click",
"type": {
"text": "CustomEvent"
}
},
{
"name": "end-turn",
"type": {
"text": "CustomEvent"
}
},
{
"name": "movement-click",
"type": {
"text": "CustomEvent"
}
},
{
"name": "hover-skill",
"type": {
"text": "CustomEvent"
}
}
],
"attributes": [
{
"name": "combatState",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "combatState"
},
{
"name": "hidden",
"type": {
"text": "boolean"
},
"default": "false",
"fieldName": "hidden"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "combat-hud",
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "CombatHUD",
"declaration": {
"name": "CombatHUD",
"module": "src/ui/combat-hud.js"
}
},
{
"kind": "custom-element-definition",
"name": "combat-hud",
"declaration": {
"name": "CombatHUD",
"module": "src/ui/combat-hud.js"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/ui/deployment-hud.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "DeploymentHUD",
"members": [
{
"kind": "method",
"name": "_updateDeployedIds",
"description": "Converts deployed indices to unit IDs and updates deployedIds",
"privacy": "private"
},
{
"kind": "method",
"name": "_formatClassName",
"parameters": [
{
"name": "classId",
"description": "The class identifier",
"type": {
"text": "string"
}
}
],
"description": "Formats a classId (e.g., \"CLASS_VANGUARD\") to a readable class name (e.g., \"Vanguard\")",
"return": {
"type": {
"text": "string"
}
},
"privacy": "private"
},
{
"kind": "method",
"name": "_selectUnit",
"parameters": [
{
"name": "unit"
}
]
},
{
"kind": "method",
"name": "_handleStartBattle"
},
{
"kind": "field",
"name": "squad",
"privacy": "public",
"type": {
"text": "array"
},
"default": "[]",
"attribute": "squad"
},
{
"kind": "field",
"name": "deployedIds",
"privacy": "public",
"type": {
"text": "array"
},
"default": "[]",
"attribute": "deployedIds"
},
{
"kind": "field",
"name": "deployedIndices",
"type": {
"text": "array"
},
"default": "[]"
},
{
"kind": "field",
"name": "selectedId",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "selectedId"
},
{
"kind": "field",
"name": "maxUnits",
"privacy": "public",
"type": {
"text": "number"
},
"default": "4",
"attribute": "maxUnits"
},
{
"kind": "field",
"name": "currentState",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "currentState"
},
{
"kind": "field",
"name": "missionDef",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "missionDef"
}
],
"events": [
{
"name": "recall-unit",
"type": {
"text": "CustomEvent"
}
},
{
"name": "unit-selected",
"type": {
"text": "CustomEvent"
}
},
{
"name": "start-battle",
"type": {
"text": "CustomEvent"
}
}
],
"attributes": [
{
"name": "squad",
"type": {
"text": "array"
},
"default": "[]",
"fieldName": "squad"
},
{
"name": "deployedIds",
"type": {
"text": "array"
},
"default": "[]",
"fieldName": "deployedIds"
},
{
"name": "selectedId",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "selectedId"
},
{
"name": "maxUnits",
"type": {
"text": "number"
},
"default": "4",
"fieldName": "maxUnits"
},
{
"name": "currentState",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "currentState"
},
{
"name": "missionDef",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "missionDef"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "deployment-hud",
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "DeploymentHUD",
"declaration": {
"name": "DeploymentHUD",
"module": "src/ui/deployment-hud.js"
}
},
{
"kind": "custom-element-definition",
"name": "deployment-hud",
"declaration": {
"name": "DeploymentHUD",
"module": "src/ui/deployment-hud.js"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/ui/dialogue-overlay.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "DialogueOverlay",
"members": [
{
"kind": "method",
"name": "_onUpdate",
"parameters": [
{
"name": "e"
}
]
},
{
"kind": "method",
"name": "_onEnd"
},
{
"kind": "method",
"name": "_handleInput",
"parameters": [
{
"name": "e"
}
]
},
{
"kind": "field",
"name": "activeNode",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "activeNode"
},
{
"kind": "field",
"name": "isVisible",
"privacy": "public",
"type": {
"text": "boolean"
},
"default": "false",
"attribute": "isVisible"
},
{
"kind": "field",
"name": "_isProcessingClick",
"type": {
"text": "boolean"
},
"default": "false"
}
],
"attributes": [
{
"name": "activeNode",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "activeNode"
},
{
"name": "isVisible",
"type": {
"text": "boolean"
},
"default": "false",
"fieldName": "isVisible"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "dialogue-overlay",
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "DialogueOverlay",
"declaration": {
"name": "DialogueOverlay",
"module": "src/ui/dialogue-overlay.js"
}
},
{
"kind": "custom-element-definition",
"name": "dialogue-overlay",
"declaration": {
"name": "DialogueOverlay",
"module": "src/ui/dialogue-overlay.js"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/ui/game-viewport.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "GameViewport",
"members": [
{
"kind": "method",
"name": "#handleUnitSelected",
"privacy": "private",
"parameters": [
{
"name": "event"
}
]
},
{
"kind": "method",
"name": "#handleStartBattle",
"privacy": "private"
},
{
"kind": "method",
"name": "#handleEndTurn",
"privacy": "private"
},
{
"kind": "method",
"name": "#handleSkillClick",
"privacy": "private",
"parameters": [
{
"name": "event"
}
]
},
{
"kind": "method",
"name": "#handleMovementClick",
"privacy": "private"
},
{
"kind": "method",
"name": "#setupCombatStateUpdates",
"privacy": "private"
},
{
"kind": "method",
"name": "#clearState",
"privacy": "private"
},
{
"kind": "method",
"name": "#updateSquad",
"privacy": "private"
},
{
"kind": "method",
"name": "#updateCombatState",
"privacy": "private"
},
{
"kind": "field",
"name": "squad",
"privacy": "public",
"type": {
"text": "array"
},
"default": "[]",
"attribute": "squad"
},
{
"kind": "field",
"name": "deployedIds",
"privacy": "public",
"type": {
"text": "array"
},
"default": "[]",
"attribute": "deployedIds"
},
{
"kind": "field",
"name": "combatState",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "combatState"
},
{
"kind": "field",
"name": "missionDef",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "missionDef"
},
{
"kind": "field",
"name": "debriefResult",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "debriefResult"
}
],
"attributes": [
{
"name": "squad",
"type": {
"text": "array"
},
"default": "[]",
"fieldName": "squad"
},
{
"name": "deployedIds",
"type": {
"text": "array"
},
"default": "[]",
"fieldName": "deployedIds"
},
{
"name": "combatState",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "combatState"
},
{
"name": "missionDef",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "missionDef"
},
{
"name": "debriefResult",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "debriefResult"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "game-viewport",
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "GameViewport",
"declaration": {
"name": "GameViewport",
"module": "src/ui/game-viewport.js"
}
},
{
"kind": "custom-element-definition",
"name": "game-viewport",
"declaration": {
"name": "GameViewport",
"module": "src/ui/game-viewport.js"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/ui/team-builder.js",
"declarations": [
{
"kind": "class",
"description": "",
"name": "TeamBuilder",
"members": [
{
"kind": "method",
"name": "_handleUnlocksChanged",
"description": "Handles unlock changes by refreshing the class list."
},
{
"kind": "method",
"name": "_initializeData",
"description": "Configures the component based on provided data."
},
{
"kind": "method",
"name": "_renderDetails"
},
{
"kind": "method",
"name": "_selectSlot",
"parameters": [
{
"name": "index"
}
]
},
{
"kind": "method",
"name": "_assignItem",
"parameters": [
{
"name": "item"
}
]
},
{
"kind": "method",
"name": "_removeUnit",
"parameters": [
{
"name": "index"
}
]
},
{
"kind": "method",
"name": "_handleEmbark"
},
{
"kind": "method",
"name": "_formatItemName",
"parameters": [
{
"name": "id"
}
]
},
{
"kind": "method",
"name": "_formatSkillName",
"parameters": [
{
"name": "id"
}
]
},
{
"kind": "field",
"name": "squad",
"privacy": "public",
"type": {
"text": "array"
},
"default": "[null, null, null, null]",
"attribute": "squad"
},
{
"kind": "field",
"name": "selectedSlotIndex",
"privacy": "public",
"type": {
"text": "number"
},
"default": "0",
"attribute": "selectedSlotIndex"
},
{
"kind": "field",
"name": "hoveredItem",
"privacy": "public",
"type": {
"text": "null"
},
"default": "null",
"attribute": "hoveredItem"
},
{
"kind": "field",
"name": "mode",
"privacy": "public",
"type": {
"text": "string"
},
"default": "\"DRAFT\"",
"attribute": "mode"
},
{
"kind": "field",
"name": "availablePool",
"privacy": "public",
"type": {
"text": "array"
},
"default": "[]",
"attribute": "availablePool"
},
{
"kind": "field",
"name": "_poolExplicitlySet",
"type": {
"text": "boolean"
},
"description": "Whether availablePool was explicitly set (vs default empty)",
"default": "false"
}
],
"events": [
{
"name": "embark",
"type": {
"text": "CustomEvent"
}
}
],
"attributes": [
{
"name": "mode",
"type": {
"text": "string"
},
"default": "\"DRAFT\"",
"fieldName": "mode"
},
{
"name": "availablePool",
"type": {
"text": "array"
},
"default": "[]",
"fieldName": "availablePool"
},
{
"name": "squad",
"type": {
"text": "array"
},
"default": "[null, null, null, null]",
"fieldName": "squad"
},
{
"name": "selectedSlotIndex",
"type": {
"text": "number"
},
"default": "0",
"fieldName": "selectedSlotIndex"
},
{
"name": "hoveredItem",
"type": {
"text": "null"
},
"default": "null",
"fieldName": "hoveredItem"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "team-builder",
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "TeamBuilder",
"declaration": {
"name": "TeamBuilder",
"module": "src/ui/team-builder.js"
}
},
{
"kind": "custom-element-definition",
"name": "team-builder",
"declaration": {
"name": "TeamBuilder",
"module": "src/ui/team-builder.js"
}
}
]
}
]
}

View file

@ -1,575 +0,0 @@
import { VoxelGrid } from "./src/grid/VoxelGrid.js";
import { CrystalSpiresGenerator } from "./src/generation/CrystalSpiresGenerator.js";
// 1. Get Seed from Args
const args = process.argv.slice(2);
const seed = args.length > 0 ? parseInt(args[0]) : 727852;
console.log(`Debug Run: Seed ${seed}`);
// Mock OffscreenCanvas for Node.js environment
global.OffscreenCanvas = class {
constructor(width, height) {
this.width = width;
this.height = height;
}
getContext() {
return {
fillStyle: "",
fillRect: () => {},
beginPath: () => {},
moveTo: () => {},
lineTo: () => {},
stroke: () => {},
fill: () => {},
arc: () => {},
save: () => {},
restore: () => {},
translate: () => {},
rotate: () => {},
scale: () => {},
createLinearGradient: () => ({ addColorStop: () => {} }),
createRadialGradient: () => ({ addColorStop: () => {} }),
createImageData: (w, h) => ({
width: w,
height: h,
data: new Uint8ClampedArray(w * h * 4),
}),
putImageData: () => {},
};
}
};
const grid = new VoxelGrid(20, 20, 20);
const generator = new CrystalSpiresGenerator(grid, seed);
generator.generate(4, 3);
// 2. Validate Bridges
const bridges = generator.generatedAssets.bridges || [];
const spires = generator.generatedAssets.spires || [];
console.log(`\n=== Bridge Validity Check ===`);
console.log(`Total Spires: ${spires.length}`);
console.log(`Total Bridges: ${bridges.length}`);
// Helper: Collision Check
function checkCylinderCollision(x1, z1, x2, z2, cx, cz, r) {
const dx = x2 - x1;
const dz = z2 - z1;
const fx = x1 - cx;
const fz = z1 - cz;
const a = dx * dx + dz * dz;
const b = 2 * (fx * dx + fz * dz);
const c = fx * fx + fz * fz - r * r;
let discrim = b * b - 4 * a * c;
if (discrim < 0) return false;
discrim = Math.sqrt(discrim);
const t1 = (-b - discrim) / (2 * a);
const t2 = (-b + discrim) / (2 * a);
if ((t1 > 0.05 && t1 < 0.95) || (t2 > 0.05 && t2 < 0.95)) return true;
return false;
}
// Helper: Check if a position is valid to stand on
function isValidStance(grid, x, y, z) {
if (
x < 0 ||
x >= grid.size.x ||
z < 0 ||
z >= grid.size.z ||
y < 0 ||
y >= grid.size.y
)
return false;
const ground = grid.getCell(x, y - 1, z);
const feet = grid.getCell(x, y, z);
const head = grid.getCell(x, y + 1, z);
if (ground === 0 || feet !== 0 || head !== 0) {
// console.log(`Stance Fail @ ${x},${y},${z}: G=${ground} F=${feet} H=${head}`);
return false;
}
return true;
}
// Helper: BFS Pathfinding
// Helper: Game-Accurate Walkability Check (Matches MovementSystem.js)
function checkWalkability(grid, start, end, spireCenter = null) {
const q = [];
const visited = new Set();
const cameFrom = new Map(); // Track path for reconstruction
// Find valid start Y (Unit stands ON TOP of the block)
// Generator `start` is the Bridge Block (Floor).
// So Unit Y = start.y + 1.
const sx = Math.round(start.x);
const sz = Math.round(start.z);
// Find Walkable Y logic from MovementSystem
// Checks: Cell(y) != 0 is FAIL. Cell(y-1) == 0 is FAIL. Cell(y+1) != 0 is FAIL.
const findWalkableY = (x, z, refY) => {
const levels = [refY, refY + 1, refY - 1, refY - 2];
for (const y of levels) {
if (
grid.getCell(x, y, z) === 0 && // Feet Clear
grid.getCell(x, y - 1, z) !== 0 && // Floor Solid
grid.getCell(x, y + 1, z) === 0
) {
// Head Clear
return y;
}
}
return null;
};
const sy = findWalkableY(sx, sz, Math.round(start.y) + 1);
if (sy === null) {
return {
success: false,
reason: `Invalid Start Stance near ${sx},${start.y},${sz}`,
};
}
const ex = Math.round(end.x);
const ez = Math.round(end.z);
// Target Y is flexible (logic finds it)
q.push({ x: sx, y: sy, z: sz, dist: 0 });
const startKey = `${sx},${sy},${sz}`;
visited.add(startKey);
cameFrom.set(startKey, null);
while (q.length > 0) {
const curr = q.shift();
// Target Reached? (Approximate X/Z matches)
// Note: We check if we are 'standing' on or near the target platform rim
if (Math.abs(curr.x - ex) <= 2 && Math.abs(curr.z - ez) <= 2) {
// Also check Y proximity?
if (Math.abs(curr.y - (end.y + 1)) <= 3) {
// Perform Outward Check if needed
if (spireCenter) {
// Reconstruct Path
let path = [];
let k = `${curr.x},${curr.y},${curr.z}`;
while (k) {
const [px, py, pz] = k.split(",").map(Number);
path.push({ x: px, y: py, z: pz });
k = cameFrom.get(k);
}
path.reverse();
// Check first 3 steps
if (path.length > 1) {
const p0 = path[0];
// Check a bit further steps to be sure
const checkIdx = Math.min(path.length - 1, 3);
const pCheck = path[checkIdx];
const d0 = Math.sqrt(
Math.pow(p0.x - spireCenter.x, 2) +
Math.pow(p0.z - spireCenter.z, 2)
);
const dN = Math.sqrt(
Math.pow(pCheck.x - spireCenter.x, 2) +
Math.pow(pCheck.z - spireCenter.z, 2)
);
if (dN < d0) {
// Check if we are walking on a BRIDGE (ID 20) or PLATFORM (ID != 20)
// If on Platform, Inward is valid (walking across).
// If on Bridge, Inward is invalid (curling back).
// `pCheck` is at feet level? No, pCheck has .y stance.
// Grid cell at y-1 is floor.
const floorID = grid.getCell(
Math.round(pCheck.x),
Math.round(pCheck.y - 1),
Math.round(pCheck.z)
);
if (floorID === 20) {
console.log(
`[DEBUG] Inward Fail (Bridge Voxel): Center(${spireCenter.x}, ${spireCenter.z})`
);
console.log(
`[DEBUG] Path[0]: (${p0.x}, ${p0.y}, ${p0.z}) D=${d0.toFixed(
2
)}`
);
console.log(
`[DEBUG] Path[${checkIdx}]: (${pCheck.x}, ${pCheck.y}, ${
pCheck.z
}) D=${dN.toFixed(2)} ID=${floorID}`
);
return {
success: false,
reason:
"Path goes INWARD towards spire center (Bridge Recurve)",
};
} else {
// console.log(`[DEBUG] Inward Move Allowed on Platform (ID ${floorID})`);
}
}
}
}
return { success: true };
}
}
if (curr.dist > 300) continue;
// Spec CoA Implementation
// 1. Neighbors (Horizontal Only)
const dirs = [
{ x: 1, z: 0 },
{ x: -1, z: 0 },
{ x: 0, z: 1 },
{ x: 0, z: -1 },
];
for (const d of dirs) {
const nx = curr.x + d.x;
const nz = curr.z + d.z;
if (!grid.isValidBounds(nx, 0, nz)) continue;
// CoA 1: Gradient <= 1
// Check Step Up (y+1), Level (y), Step Down (y-1)
const candidates = [curr.y, curr.y + 1, curr.y - 1];
for (const ny of candidates) {
if (!grid.isValidBounds(nx, ny, nz)) continue;
// CoA 3 & 4: Headroom Validation
// Floor(ny-1) must be solid.
// Feet(ny) can be Air, Bridge(20), Teleporter(22).
// Head1(ny+1) MUST be Air.
// Head2(ny+2) MUST be Air.
const floor = grid.getCell(nx, ny - 1, nz);
const feet = grid.getCell(nx, ny, nz);
const h1 = grid.getCell(nx, ny + 1, nz);
const h2 = grid.getCell(nx, ny + 2, nz);
const isFloorSolid = floor !== 0;
const isFeetPassable = feet === 0 || feet === 20 || feet === 22;
const isHeadClear = h1 === 0 && h2 === 0;
if (isFloorSolid && isFeetPassable && isHeadClear) {
const key = `${nx},${ny},${nz}`;
if (!visited.has(key)) {
visited.add(key);
cameFrom.set(key, `${curr.x},${curr.y},${curr.z}`);
q.push({ x: nx, y: ny, z: nz, dist: curr.dist + 1 });
}
}
}
}
}
return { success: false, reason: "No Path Found (Gap or Blockage)" };
}
let validBridges = 0;
bridges.forEach((b, idx) => {
let isValid = true;
let issues = [];
// Check 1: Pillar Collision
for (let i = 0; i < spires.length; i++) {
if (i === b.fromSpire || i === b.toSpire) continue;
const s = spires[i];
if (
checkCylinderCollision(
b.start.x,
b.start.z,
b.end.x,
b.end.z,
s.x,
s.z,
s.radius + 1.0
)
) {
isValid = false;
issues.push(`Clips Spire ${i}`);
}
}
// Check 2: Slope
const yDiff = Math.abs(b.end.y - b.start.y);
const rimDist = b.dist;
const reqYdMax = Math.max(rimDist, 3) * 3.0;
if (yDiff > 12) {
isValid = false;
issues.push(`Too steep (Y=${yDiff} > 12)`);
}
if (yDiff > reqYdMax) {
isValid = false;
issues.push(`Slope limit exceeded`);
}
// Bridge Validity Logic (CoA Compliance)
// 1. Walkability (Forward)
const walkA = checkWalkability(grid, b.start, b.end, { x: 0, y: 0, z: 0 }); // Centroid irrelevant now
// 2. Walkability (Reverse)
const walkB = checkWalkability(grid, b.end, b.start, { x: 0, y: 0, z: 0 });
if (!walkA.success) {
isValid = false;
issues.push(`Forward Unwalkable: ${walkA.reason}`);
}
if (isValid && !walkB.success) {
isValid = false;
issues.push(`Reverse Unwalkable: ${walkB.reason}`);
}
// Note: We removed strict "Inward" and "Tortuosity" checks as Bezier curves are organic.
// The primary constraints are now Gradient (in checkWalkability) and Headroom.
if (isValid) {
console.log(
`Bridge #${idx} [Spire ${b.fromSpire} -> ${b.toSpire}]: VALID (OK)`
);
validBridges++;
} else {
console.log(
`Bridge #${idx} [Spire ${b.fromSpire} -> ${
b.toSpire
}]: INVALID (${issues.join(", ")})`
);
}
});
console.log(`\n=== Orphan Platform Check ===`);
let orphans = 0;
let totalPlatforms = 0;
spires.forEach((s, sIdx) => {
s.platforms.forEach((p, pIdx) => {
totalPlatforms++;
// Check if this platform has any connections recorded in its object
// Note: connections array stores points, so strictly length > 0
if (!p.connections || p.connections.length === 0) {
orphans++;
console.log(
`Spire ${sIdx} Platform ${pIdx} (Y=${p.y}) is ORPHANED (0 connections)`
);
}
});
});
console.log(`Total Platforms: ${totalPlatforms}`);
console.log(`Orphan Platforms: ${orphans}`);
console.log(`\n=== Graph Connectivity Check ===`);
// 1. Gather all platforms and assign IDs
let allPlats = [];
spires.forEach((s, sIdx) => {
s.platforms.forEach((p, pIdx) => {
p.id = `${sIdx}:${pIdx}`;
allPlats.push(p);
});
});
allPlats.sort((a, b) => a.y - b.y);
// 2. Build Adjacency Graph from Bridges
// Bridge stores: { fromSpire, toSpire, start, end }
// We need to map start/end points back to platforms.
// Helper: Find platform containing point
function findPlat(pt, sIdx) {
const s = spires[sIdx];
// Find plat with closest Y and matching X/Z within radius
let best = null;
let minD = Infinity;
for (const p of s.platforms) {
const dy = Math.abs(p.y - pt.y);
if (dy < 2.0) return p; // Direct height match usually enough
}
return null;
}
const adj = new Map(); // ID -> Set<ID>
allPlats.forEach((p) => adj.set(p.id, new Set()));
bridges.forEach((b) => {
// Note: b.fromSpire/b.toSpire are indices
// b.start is on fromSpire, b.end is on toSpire
const pA = findPlat(b.start, b.fromSpire);
const pB = findPlat(b.end, b.toSpire);
if (pA && pB) {
adj.get(pA.id).add(pB.id);
adj.get(pB.id).add(pA.id);
// console.log(`[Graph] Linked ${pA.id} <--> ${pB.id} via Bridge ${bridges.indexOf(b)}`);
} else {
console.log(
`[Graph] Failed to map bridge ${bridges.indexOf(b)} to platforms!`
);
console.log(
` Bridge: Spire ${b.fromSpire} -> ${b.toSpire}, Y: ${b.start.y.toFixed(
1
)} -> ${b.end.y.toFixed(1)}`
);
console.log(` pA: ${pA ? pA.id : "NULL"}, pB: ${pB ? pB.id : "NULL"}`);
}
});
// 3. BFS from Lowest Platform
const startNode = allPlats[0];
const targetNode = allPlats[allPlats.length - 1]; // Highest
const qGraph = [startNode.id];
const visitedGraph = new Set([startNode.id]);
while (qGraph.length > 0) {
const curr = qGraph.shift();
const neighbors = adj.get(curr);
if (neighbors) {
for (const n of neighbors) {
if (!visitedGraph.has(n)) {
visitedGraph.add(n);
qGraph.push(n);
}
}
}
}
// 4. Report
const fullyConnected = visitedGraph.size === allPlats.length;
const topReachable = visitedGraph.has(targetNode.id);
console.log(
`Nodes Reachable from Bottom (Y=${startNode.y}): ${visitedGraph.size} / ${allPlats.length}`
);
console.log(
`Top Platform (Y=${targetNode.y}) Reachable: ${topReachable ? "YES" : "NO"}`
);
console.log(`Full Graph Interconnected: ${fullyConnected ? "YES" : "NO"}`);
// CoA 5: Global Reachability via Grid Flood Fill (True Walkability)
// Graph connectivity assumes bridges work. Flood Fill PROVES it.
console.log("\n=== Global Flood Fill (Spawn -> All) ===");
// DEBUG: Check specific coordinate reported by Generator logic
const checkVal = grid.getCell(12, 11, 5);
console.log(`DEBUG CHECK: Cell(12,11,5) = ${checkVal}`);
// 0. Find all Teleporters (ID 22)
const teleporters = [];
for (let x = 0; x < grid.width; x++) {
for (let z = 0; z < grid.depth; z++) {
for (let y = 0; y < grid.height; y++) {
if (grid.getCell(x, y, z) === 22) {
teleporters.push({ x, y, z });
}
}
}
}
console.log(`Found ${teleporters.length} Teleporter Nodes.`);
const spawnPlat = allPlats[0];
const reachableSet = new Set();
// Start slightly above spawn platform to ensure standing
const floodQ = [
{
x: Math.round(spawnPlat.x),
y: Math.round(spawnPlat.y + 1),
z: Math.round(spawnPlat.z),
},
];
const visitedFlood = new Set([`${floodQ[0].x},${floodQ[0].y},${floodQ[0].z}`]);
// Optimization: Limit flood fill iterations
let iterations = 0;
const MAX_ITER = 50000;
while (floodQ.length > 0 && iterations < MAX_ITER) {
iterations++;
const curr = floodQ.shift();
// Check Teleport Jump
const floorID = grid.getCell(curr.x, curr.y - 1, curr.z);
const feetID = grid.getCell(curr.x, curr.y, curr.z);
if (floorID === 22 || feetID === 22) {
for (const t of teleporters) {
const destKey = `${t.x},${t.y + 1},${t.z}`;
if (!visitedFlood.has(destKey)) {
visitedFlood.add(destKey);
floodQ.push({ x: t.x, y: t.y + 1, z: t.z });
}
}
}
// Neighbors (Same logic as checkWalkability)
const dirs = [
{ x: 1, z: 0 },
{ x: -1, z: 0 },
{ x: 0, z: 1 },
{ x: 0, z: -1 },
];
for (const d of dirs) {
const nx = curr.x + d.x;
const nz = curr.z + d.z;
if (!grid.isValidBounds(nx, 0, nz)) continue;
const candidates = [curr.y, curr.y + 1, curr.y - 1];
for (const ny of candidates) {
if (!grid.isValidBounds(nx, ny, nz)) continue;
const floor = grid.getCell(nx, ny - 1, nz);
const feet = grid.getCell(nx, ny, nz);
const h1 = grid.getCell(nx, ny + 1, nz);
const h2 = grid.getCell(nx, ny + 2, nz);
const isFloorSolid = floor !== 0;
// Allow ID 22 (Teleporter Node) as passable
const isPassable = feet === 0 || feet === 20 || feet === 22;
if (isFloorSolid && isPassable && h1 === 0 && h2 === 0) {
const key = `${nx},${ny},${nz}`;
if (!visitedFlood.has(key)) {
visitedFlood.add(key);
floodQ.push({ x: nx, y: ny, z: nz });
}
}
}
}
}
// Check Coverage
let floodReachableCount = 0;
allPlats.forEach((p, idx) => {
let hit = false;
const searchR = p.radius - 1;
for (let x = Math.round(p.x - searchR); x <= Math.round(p.x + searchR); x++) {
for (
let z = Math.round(p.z - searchR);
z <= Math.round(p.z + searchR);
z++
) {
// Check surface (y) or just above (y+1)
if (
visitedFlood.has(`${x},${Math.round(p.y + 1)},${z}`) ||
visitedFlood.has(`${x},${Math.round(p.y)},${z}`)
) {
hit = true;
break;
}
}
if (hit) break;
}
if (hit) floodReachableCount++;
else
console.log(`[FloodFail] Platform ${p.globalId} unreachable from Spawn.`);
});
console.log(
`Flood Fill Reachable Platforms: ${floodReachableCount} / ${allPlats.length}`
);
console.log(
`Global Reachability: ${
floodReachableCount === allPlats.length ? "PASS" : "FAIL"
}`
);
console.log(`\nGeneration Complete.`);

View file

@ -1,87 +0,0 @@
import os
path = "src/generation/CrystalSpiresGenerator.js"
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
# Locate connectVerticalLevels
start_idx = -1
for i, line in enumerate(lines):
if "connectVerticalLevels(spires) {" in line:
start_idx = i
break
if start_idx == -1:
print("Error: connectVerticalLevels not found")
exit(1)
# Inject logs UNCOMMENTED
new_code = """ connectVerticalLevels(spires) {
console.log("Running connectVerticalLevels...");
spires.forEach((s, sIdx) => {
const sortedPlats = [...s.platforms].sort((a, b) => a.y - b.y);
console.log(`Spire ${sIdx} has ${sortedPlats.length} platforms.`);
for (let i = 0; i < sortedPlats.length - 1; i++) {
const pA = sortedPlats[i];
const pB = sortedPlats[i + 1];
const dy = pB.y - pA.y;
console.log(`Spire ${sIdx} Pair ${i}->${i+1}: dy=${dy}`);
if (dy > 18) {
console.log(" Skipped: Too far");
continue;
}
const angleA = this.rng.next() * Math.PI * 2;
const angleB = angleA + Math.PI * 0.66; // 120 deg
const rA = pA.radius - 0.5; // Inset slightly
const rB = pB.radius - 0.5;
const start = {
x: pA.x + Math.cos(angleA) * rA,
y: pA.y,
z: pA.z + Math.sin(angleA) * rA,
};
const end = {
x: pB.x + Math.cos(angleB) * rB,
y: pB.y,
z: pB.z + Math.sin(angleB) * rB,
};
const success = this.buildBridge(start, end, { x: s.x, z: s.z }, 0.0);
console.log(` BuildBridge Result: ${success}`);
if (success) {
pA.connections.push(start);
pB.connections.push(end);
this.generatedAssets.bridges.push({
fromSpire: sIdx,
toSpire: sIdx, // Same Spire
fromPlatIdx: s.platforms.indexOf(pA),
toPlatIdx: s.platforms.indexOf(pB),
start,
end,
});
}
}
});
}
"""
end_idx = -1
for i in range(start_idx+1, len(lines)):
if "connectSpires(spires) {" in lines[i]:
end_idx = i - 1
break
final_lines = lines[:start_idx] + [new_code] + lines[end_idx+1:]
with open(path, "w", encoding="utf-8") as f:
f.writelines(final_lines)
print("Debug Instrumentation Complete")

770
package-lock.json generated
View file

@ -13,7 +13,6 @@
"three": "^0.182.0" "three": "^0.182.0"
}, },
"devDependencies": { "devDependencies": {
"@custom-elements-manifest/analyzer": "^0.11.0",
"@esm-bundle/chai": "^4.3.4-fix.0", "@esm-bundle/chai": "^4.3.4-fix.0",
"@web/dev-server": "^0.4.6", "@web/dev-server": "^0.4.6",
"@web/test-runner": "^0.20.2", "@web/test-runner": "^0.20.2",
@ -47,169 +46,6 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@custom-elements-manifest/analyzer": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.11.0.tgz",
"integrity": "sha512-F2jQFk6igV5vrTZYDBLVr0GDQF3cTJ2VwwzPdsmkzUE+SHiYAQNKYebIq8qphZdJeTcZtVhvw336FQVZsMqh4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@custom-elements-manifest/find-dependencies": "^0.0.7",
"@github/catalyst": "^1.6.0",
"@web/config-loader": "0.1.3",
"chokidar": "3.5.2",
"command-line-args": "5.1.2",
"comment-parser": "1.2.4",
"custom-elements-manifest": "1.0.0",
"debounce": "1.2.1",
"globby": "11.0.4",
"typescript": "~5.4.2"
},
"bin": {
"cem": "cem.js",
"custom-elements-manifest": "cem.js"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/@web/config-loader": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.1.3.tgz",
"integrity": "sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/array-back": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz",
"integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
"integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/command-line-args": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.2.tgz",
"integrity": "sha512-fytTsbndLbl+pPWtS0CxLV3BEWw9wJayB8NnU2cbQqVPsNdYezQeT+uIQv009m+GShnMNyuoBrRo8DTmuTfSCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-back": "^6.1.2",
"find-replace": "^3.0.0",
"lodash.camelcase": "^4.3.0",
"typical": "^4.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/globby": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
"integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.1.1",
"ignore": "^5.1.4",
"merge2": "^1.3.0",
"slash": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/@custom-elements-manifest/find-dependencies": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/find-dependencies/-/find-dependencies-0.0.7.tgz",
"integrity": "sha512-icHEBPHcOXSrpDOFkQhvM7vljEbqrEReW251RBxSzDctXzYWIL0Hk2yMDINn3d6mZ4KSsqLZlaFiGFp/2nn9rA==",
"dev": true,
"license": "ISC",
"dependencies": {
"oxc-resolver": "^11.9.0",
"rs-module-lexer": "^2.5.1"
}
},
"node_modules/@emnapi/core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
"integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.1", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
@ -662,13 +498,6 @@
"@types/chai": "^4.2.12" "@types/chai": "^4.2.12"
} }
}, },
"node_modules/@github/catalyst": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@github/catalyst/-/catalyst-1.7.0.tgz",
"integrity": "sha512-qOAxrDdRZz9+v4y2WoAfh11rpRY/x4FRofPNmJyZFzAjubtzE3sCa/tAycWWufmQGoYiwwzL/qJBBgyg7avxPw==",
"dev": true,
"license": "MIT"
},
"node_modules/@hapi/bourne": { "node_modules/@hapi/bourne": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
@ -719,23 +548,6 @@
"@lit-labs/ssr-dom-shim": "^1.4.0" "@lit-labs/ssr-dom-shim": "^1.4.0"
} }
}, },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -774,289 +586,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.16.2.tgz",
"integrity": "sha512-lVJbvydLQIDZHKUb6Zs9Rq80QVTQ9xdCQE30eC9/cjg4wsMoEOg65QZPymUAIVJotpUAWJD0XYcwE7ugfxx5kQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@oxc-resolver/binding-android-arm64": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.16.2.tgz",
"integrity": "sha512-fEk+g/g2rJ6LnBVPqeLcx+/alWZ/Db1UlXG+ZVivip0NdrnOzRL48PAmnxTMGOrLwsH1UDJkwY3wOjrrQltCqg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@oxc-resolver/binding-darwin-arm64": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.16.2.tgz",
"integrity": "sha512-Pkbp1qi7kdUX6k3Fk1PvAg6p7ruwaWKg1AhOlDgrg2vLXjtv9ZHo7IAQN6kLj0W771dPJZWqNxoqTPacp2oYWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxc-resolver/binding-darwin-x64": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.16.2.tgz",
"integrity": "sha512-FYCGcU1iSoPkADGLfQbuj0HWzS+0ItjDCt9PKtu2Hzy6T0dxO4Y1enKeCOxCweOlmLEkSxUlW5UPT4wvT3LnAg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxc-resolver/binding-freebsd-x64": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.16.2.tgz",
"integrity": "sha512-1zHCoK6fMcBjE54P2EG/z70rTjcRxvyKfvk4E/QVrWLxNahuGDFZIxoEoo4kGnnEcmPj41F0c2PkrQbqlpja5g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.16.2.tgz",
"integrity": "sha512-+ucLYz8EO5FDp6kZ4o1uDmhoP+M98ysqiUW4hI3NmfiOJQWLrAzQjqaTdPfIOzlCXBU9IHp5Cgxu6wPjVb8dbA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.16.2.tgz",
"integrity": "sha512-qq+TpNXyw1odDgoONRpMLzH4hzhwnEw55398dL8rhKGvvYbio71WrJ00jE+hGlEi7H1Gkl11KoPJRaPlRAVGPw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.16.2.tgz",
"integrity": "sha512-xlMh4gNtplNQEwuF5icm69udC7un0WyzT5ywOeHrPMEsghKnLjXok2wZgAA7ocTm9+JsI+nVXIQa5XO1x+HPQg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-arm64-musl": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.16.2.tgz",
"integrity": "sha512-OZs33QTMi0xmHv/4P0+RAKXJTBk7UcMH5tpTaCytWRXls/DGaJ48jOHmriQGK2YwUqXl+oneuNyPOUO0obJ+Hg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.16.2.tgz",
"integrity": "sha512-UVyuhaV32dJGtF6fDofOcBstg9JwB2Jfnjfb8jGlu3xcG+TsubHRhuTwQ6JZ1sColNT1nMxBiu7zdKUEZi1kwg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.16.2.tgz",
"integrity": "sha512-YZZS0yv2q5nE1uL/Fk4Y7m9018DSEmDNSG8oJzy1TJjA1jx5HL52hEPxi98XhU6OYhSO/vC1jdkJeE8TIHugug==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.16.2.tgz",
"integrity": "sha512-9VYuypwtx4kt1lUcwJAH4dPmgJySh4/KxtAPdRoX2BTaZxVm/yEXHq0mnl/8SEarjzMvXKbf7Cm6UBgptm3DZw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.16.2.tgz",
"integrity": "sha512-3gbwQ+xlL5gpyzgSDdC8B4qIM4mZaPDLaFOi3c/GV7CqIdVJc5EZXW4V3T6xwtPBOpXPXfqQLbhTnUD4SqwJtA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-x64-gnu": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.16.2.tgz",
"integrity": "sha512-m0WcK0j54tSwWa+hQaJMScZdWneqE7xixp/vpFqlkbhuKW9dRHykPAFvSYg1YJ3MJgu9ZzVNpYHhPKJiEQq57Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-linux-x64-musl": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.16.2.tgz",
"integrity": "sha512-ZjUm3w96P2t47nWywGwj1A2mAVBI/8IoS7XHhcogWCfXnEI3M6NPIRQPYAZW4s5/u3u6w1uPtgOwffj2XIOb/g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxc-resolver/binding-openharmony-arm64": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.16.2.tgz",
"integrity": "sha512-OFVQ2x3VenTp13nIl6HcQ/7dmhFmM9dg2EjKfHcOtYfrVLQdNR6THFU7GkMdmc8DdY1zLUeilHwBIsyxv5hkwQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@oxc-resolver/binding-wasm32-wasi": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.16.2.tgz",
"integrity": "sha512-+O1sY3RrGyA2AqDnd3yaDCsqZqCblSTEpY7TbbaOaw0X7iIbGjjRLdrQk9StG3QSiZuBy9FdFwotIiSXtwvbAQ==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.16.2.tgz",
"integrity": "sha512-jMrMJL+fkx6xoSMFPOeyQ1ctTFjavWPOSZEKUY5PebDwQmC9cqEr4LhdTnGsOtFrWYLXlEU4xWeMdBoc/XKkOA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxc-resolver/binding-win32-ia32-msvc": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.16.2.tgz",
"integrity": "sha512-tl0xDA5dcQplG2yg2ZhgVT578dhRFafaCfyqMEAXq8KNpor85nJ53C3PLpfxD2NKzPioFgWEexNsjqRi+kW2Mg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxc-resolver/binding-win32-x64-msvc": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.16.2.tgz",
"integrity": "sha512-M7z0xjYQq1HdJk2DxTSLMvRMyBSI4wn4FXGcVQBsbAihgXevAReqwMdb593nmCK/OiFwSNcOaGIzUvzyzQ+95w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@puppeteer/browsers": { "node_modules/@puppeteer/browsers": {
"version": "2.11.0", "version": "2.11.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.0.tgz", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.0.tgz",
@ -1496,17 +1025,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/accepts": { "node_modules/@types/accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz",
@ -2057,159 +1575,6 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@xn-sakina/rml-darwin-arm64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-darwin-arm64/-/rml-darwin-arm64-2.6.0.tgz",
"integrity": "sha512-RuFHj6ro6Q24gPqNQGvH4uxpsvbgqBBy+ZUK+jbMuMaw4wyti7F6klQWuikBJAxhWpmRbhAB/jrq0PC82qlh5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-darwin-x64": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-darwin-x64/-/rml-darwin-x64-2.6.0.tgz",
"integrity": "sha512-85bsP7viqtgw5nVYBdl8I4c2+q4sYFcBMTeFnTf4RqhUUwBLerP7D+XXnWwv3waO+aZ0Fe0ij9Fji3oTiREOCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-linux-arm-gnueabihf": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-linux-arm-gnueabihf/-/rml-linux-arm-gnueabihf-2.6.0.tgz",
"integrity": "sha512-ySI529TPraG1Mf/YiKhLLNGJ1js0Y3BnZRAihUpF4IlyFKmeL3slXEdvK2tVndyX2O21EYWv/DcSAmFMNOolfA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-linux-arm64-gnu": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-linux-arm64-gnu/-/rml-linux-arm64-gnu-2.6.0.tgz",
"integrity": "sha512-Ytzkmty4vVWAqe+mbu/ql5dqwUH49eVgPT38uJK78LTZRsdogxlQbuAoLKlb/N8CIXAE7BRoywz3lSEGToXylw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-linux-arm64-musl": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-linux-arm64-musl/-/rml-linux-arm64-musl-2.6.0.tgz",
"integrity": "sha512-DIBSDWlTmWk+r6Xp7mL9Cw8DdWNyJGg7YhOV1sSSRykdGs2TNtS3z0nbHRuUBMqrbtDk0IwqFSepLx12Bix/zw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-linux-x64-gnu": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-linux-x64-gnu/-/rml-linux-x64-gnu-2.6.0.tgz",
"integrity": "sha512-8Pks6hMicFGWYQmylKul7Gmn64pG4HkRL7skVWEPAF0LZHeI5yvV/EnQUnXXbxPp4Viy2H4420jl6BVS7Uetng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-linux-x64-musl": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-linux-x64-musl/-/rml-linux-x64-musl-2.6.0.tgz",
"integrity": "sha512-xHX/rNKcATVrJt2no0FdO6kqnV4P5cP/3MgHA0KwhD/YJmWa66JIfWtzrPv9n/s0beGSorLkh8PLt5lVLFGvlQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-win32-arm64-msvc": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-win32-arm64-msvc/-/rml-win32-arm64-msvc-2.6.0.tgz",
"integrity": "sha512-aIOu5frDsxRp5naN6YjBtbCHS4K2WHIx2EClGclv3wGFrOn1oSROxpVOV/MODUuWITj/26pWbZ/tnbvva6ZV8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14"
}
},
"node_modules/@xn-sakina/rml-win32-x64-msvc": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@xn-sakina/rml-win32-x64-msvc/-/rml-win32-x64-msvc-2.6.0.tgz",
"integrity": "sha512-XXbzy2gLEs6PpHdM2IUC5QujOIjz6LpSQpJ+ow43gVc7BhagIF5YlMyTFZCbJehjK9yNgPCzdrzsukCjsH5kIA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=14"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -2276,20 +1641,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -2469,19 +1820,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@ -2821,16 +2159,6 @@
"node": ">=12.17" "node": ">=12.17"
} }
}, },
"node_modules/comment-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz",
"integrity": "sha512-pm0b+qv+CkWNriSTMsfnjChF9kH0kxz55y44Wo5le9qLxMj5xDQAaEd9ZN1ovSuk9CsrncWaFwgpOMg7ClJwkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -2917,13 +2245,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/custom-elements-manifest": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",
"integrity": "sha512-j59k0ExGCKA8T6Mzaq+7axc+KVHwpEphEERU7VZ99260npu/p/9kd+Db+I3cGKxHkM5y6q5gnlXn00mzRQkX2A==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/data-uri-to-buffer": { "node_modules/data-uri-to-buffer": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
@ -3915,19 +3236,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@ -4575,16 +3883,6 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/npm-run-path": { "node_modules/npm-run-path": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@ -4674,38 +3972,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/oxc-resolver": {
"version": "11.16.2",
"resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.16.2.tgz",
"integrity": "sha512-Uy76u47vwhhF7VAmVY61Srn+ouiOobf45MU9vGct9GD2ARy6hKoqEElyHDB0L+4JOM6VLuZ431KiLwyjI/A21g==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxc-resolver/binding-android-arm-eabi": "11.16.2",
"@oxc-resolver/binding-android-arm64": "11.16.2",
"@oxc-resolver/binding-darwin-arm64": "11.16.2",
"@oxc-resolver/binding-darwin-x64": "11.16.2",
"@oxc-resolver/binding-freebsd-x64": "11.16.2",
"@oxc-resolver/binding-linux-arm-gnueabihf": "11.16.2",
"@oxc-resolver/binding-linux-arm-musleabihf": "11.16.2",
"@oxc-resolver/binding-linux-arm64-gnu": "11.16.2",
"@oxc-resolver/binding-linux-arm64-musl": "11.16.2",
"@oxc-resolver/binding-linux-ppc64-gnu": "11.16.2",
"@oxc-resolver/binding-linux-riscv64-gnu": "11.16.2",
"@oxc-resolver/binding-linux-riscv64-musl": "11.16.2",
"@oxc-resolver/binding-linux-s390x-gnu": "11.16.2",
"@oxc-resolver/binding-linux-x64-gnu": "11.16.2",
"@oxc-resolver/binding-linux-x64-musl": "11.16.2",
"@oxc-resolver/binding-openharmony-arm64": "11.16.2",
"@oxc-resolver/binding-wasm32-wasi": "11.16.2",
"@oxc-resolver/binding-win32-arm64-msvc": "11.16.2",
"@oxc-resolver/binding-win32-ia32-msvc": "11.16.2",
"@oxc-resolver/binding-win32-x64-msvc": "11.16.2"
}
},
"node_modules/p-event": { "node_modules/p-event": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz",
@ -5297,28 +4563,6 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rs-module-lexer": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/rs-module-lexer/-/rs-module-lexer-2.6.0.tgz",
"integrity": "sha512-aT0lO0icZ3Hq0IWvo+ORgVc6BJDoKfaDBdRIDQkL2PtBnFQJ0DuvExiiWI4GxjEjH8Yyro++NPArHFaD8bvS9w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"optionalDependencies": {
"@xn-sakina/rml-darwin-arm64": "2.6.0",
"@xn-sakina/rml-darwin-x64": "2.6.0",
"@xn-sakina/rml-linux-arm-gnueabihf": "2.6.0",
"@xn-sakina/rml-linux-arm64-gnu": "2.6.0",
"@xn-sakina/rml-linux-arm64-musl": "2.6.0",
"@xn-sakina/rml-linux-x64-gnu": "2.6.0",
"@xn-sakina/rml-linux-x64-musl": "2.6.0",
"@xn-sakina/rml-win32-arm64-msvc": "2.6.0",
"@xn-sakina/rml-win32-x64-msvc": "2.6.0"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -5872,20 +5116,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typical": { "node_modules/typical": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",

View file

@ -6,11 +6,11 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "node build.js", "build": "node build.js",
"cem": "cem analyze --litelement --globs src/**/*.js", "start": "web-dev-server --node-resolve --watch --root-dir --port 8000 dist",
"start": "web-dev-server --node-resolve --port 8000 --root-dir dist",
"test:all": "web-test-runner \"test/**/*.test.js\" --node-resolve", "test:all": "web-test-runner \"test/**/*.test.js\" --node-resolve",
"test": "web-test-runner --node-resolve", "test": "web-test-runner --node-resolve",
"visualizer": "web-dev-server --node-resolve --port 8001 --root-dir src/tools" "test:watch": "web-test-runner \"test/**/*.test.js\" --node-resolve --watch --config web-test-runner.config.js",
"visualizer": "web-dev-server --node-resolve --watch --port 8001 src/tools/map-visualizer.html"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -20,7 +20,6 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@custom-elements-manifest/analyzer": "^0.11.0",
"@esm-bundle/chai": "^4.3.4-fix.0", "@esm-bundle/chai": "^4.3.4-fix.0",
"@web/dev-server": "^0.4.6", "@web/dev-server": "^0.4.6",
"@web/test-runner": "^0.20.2", "@web/test-runner": "^0.20.2",
@ -31,6 +30,5 @@
"dependencies": { "dependencies": {
"lit": "^3.3.1", "lit": "^3.3.1",
"three": "^0.182.0" "three": "^0.182.0"
}, }
"customElements": "custom-elements.json"
} }

View file

@ -1,218 +0,0 @@
import os
path = "src/generation/CrystalSpiresGenerator.js"
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
# Line numbers (1-based) to 0-based indices
# remove 780-944, 1070-1170, 1172-1234, 1345-1450
# Note: Python slices are [start:end] where start is inclusive, end is exclusive.
# Line N -> Index N-1.
# Block 1: 1 to 779 (Index 0 to 779)
chunk1 = lines[:779]
# Block 2: Skip 780-944 (Index 779-944)
# Keep 945-1069 (Index 944-1069)
chunk2 = lines[944:1069]
# Block 3: Skip 1070-1170 (Index 1069-1170)
# Keep 1171 (Index 1170)
chunk3 = lines[1170:1171]
# Block 4: Skip 1172-1234 (Index 1171-1234)
# Keep 1235-1344 (Index 1234-1344)
chunk4 = lines[1234:1344]
# Block 5: Skip 1345-1450 (Index 1344-1450)
# Keep 1451-End (Index 1450:)
chunk5 = lines[1450:]
# New Content Strings
new_buildBridge = """ buildBridge(start, end, fromCenter = null, minDist = 2.0) {
// Wrapper for generateBezierBridge
// Simple Bezier logic for compatibility check
const p0 = start;
const p2 = end;
const midX = (p0.x + p2.x) / 2;
const midZ = (p0.z + p2.z) / 2;
const midY = (p0.y + p2.y) / 2;
const dx = p2.x - p0.x;
const dz = p2.z - p0.z;
const dist = Math.sqrt(dx*dx + dz*dz);
const nx = dx / dist;
const nz = dz / dist;
const arcFactor = dist < 5.0 ? 0.5 : (dist / 8.0);
const archHeight = 3.0 * arcFactor; // Simplified random
const curveMag = 0; // Straight arch for simple builds (or random if rng available?)
// We don't have RNG access easily here unless we use this.rng
// Assume this.rng exists.
if (this.rng) {
const cm = this.rng.range(-dist/4, dist/4);
const ah = this.rng.range(1, 4) * arcFactor;
// Recalc
}
// Re-use generateBezierBridge logic?
// "generateBezierBridge" requires pA, pB (Platforms).
// Here we have points.
// We'll duplicate logic briefly to ensure it works.
const p1 = {x: midX, y: midY + archHeight, z: midZ};
const steps = Math.ceil(dist * 2.5);
const pathVoxels = [];
const visited = new Set();
for(let i=0; i<=steps; i++) {
const t = i / steps;
const invT = 1 - t;
const x = invT*invT*p0.x + 2*invT*t*p1.x + t*t*p2.x;
const y = invT*invT*p0.y + 2*invT*t*p1.y + t*t*p2.y;
const z = invT*invT*p0.z + 2*invT*t*p1.z + t*t*p2.z;
const ix = Math.round(x);
const iy = Math.round(y);
const iz = Math.round(z);
const key = `${ix},${iy},${iz}`;
if (!visited.has(key)) {
visited.add(key);
pathVoxels.push({x: ix, y: iy, z: iz});
}
}
// VALIDATION
for (let i=0; i<pathVoxels.length; i++) {
const v = pathVoxels[i];
if (!this.grid.isValidBounds(v.x, v.y, v.z)) return false;
const id = this.grid.getCell(v.x, v.y, v.z);
if (id !== 0 && id !== 20 && id !== 22) {
const dStart = (v.x-p0.x)**2 + (v.y-p0.y)**2 + (v.z-p0.z)**2;
const dEnd = (v.x-p2.x)**2 + (v.y-p2.y)**2 + (v.z-p2.z)**2;
if (dStart > 4 && dEnd > 4) return false;
}
const h1 = this.grid.getCell(v.x, v.y+1, v.z);
const h2 = this.grid.getCell(v.x, v.y+2, v.z);
if (h1 !== 0 || h2 !== 0) return false;
if (i > 0) {
if (Math.abs(v.y - pathVoxels[i-1].y) > 1) return false;
}
}
// PLACEMENT
for (const v of pathVoxels) {
if (this.grid.getCell(v.x, v.y, v.z) === 0) {
this.grid.setCell(v.x, v.y, v.z, 20);
}
}
return true;
}\n\n"""
new_connectivity = """ /**
* Ensure Connectivity via Flood Fill & Teleporters.
*/
ensureGlobalConnectivity(spires) {
if (!spires || spires.length === 0) return;
let startPt = {x: spires[0].x, y: spires[0].platforms[0].y + 1, z: spires[0].z};
const visited = new Set();
const q = [{x: Math.round(startPt.x), y: Math.round(startPt.y), z: Math.round(startPt.z)}];
visited.add(`${q[0].x},${q[0].y},${q[0].z}`);
const MAX_ITER = 50000;
let iter = 0;
// Flood Fill
while(q.length > 0 && iter < MAX_ITER) {
iter++;
const curr = q.shift();
const dirs = [{x:1,z:0},{x:-1,z:0},{x:0,z:1},{x:0,z:-1}];
for(const d of dirs) {
const nx = curr.x+d.x;
const nz = curr.z+d.z;
// Y-scan
const candidates = [curr.y, curr.y+1, curr.y-1];
for(const ny of candidates) {
if (this.grid.isValidBounds(nx, ny, nz)) {
const floor = this.grid.getCell(nx, ny-1, nz);
const feet = this.grid.getCell(nx, ny, nz);
const h1 = this.grid.getCell(nx, ny+1, nz);
const h2 = this.grid.getCell(nx, ny+2, nz);
const pass = (floor!==0 && (feet===0||feet===20||feet===22) && h1===0 && h2===0);
if(pass) {
const key = `${nx},${ny},${nz}`;
if(!visited.has(key)) {
visited.add(key);
q.push({x:nx, y:ny, z:nz});
}
}
}
}
}
}
// Identify Orphans
const orphans = [];
spires.forEach(s => {
s.platforms.forEach(p => {
// Check if any point on/near platform is visited
let connected = false;
const r = p.radius - 1;
for(let x=p.x-r; x<=p.x+r; x++) {
for(let z=p.z-r; z<=p.z+r; z++) {
const key = `${Math.round(x)},${Math.round(p.y+1)},${Math.round(z)}`;
if(visited.has(key)) { connected = true; break; }
}
if(connected) break;
}
if (!connected) orphans.push(p);
});
});
// Fix Orphans with Teleporters
orphans.forEach(orp => {
const ox = Math.round(orp.x);
const oz = Math.round(orp.z);
const oy = Math.round(orp.y + 1); // Stand on top
// Find target (Spawn/Spire 0)
const target = spires[0].platforms[0];
const tx = Math.round(target.x);
const tz = Math.round(target.z);
const ty = Math.round(target.y + 1);
if(this.grid.isValidBounds(ox, oy, oz) && this.grid.isValidBounds(tx, ty, tz)) {
this.grid.setCell(ox, oy, oz, 22); // Teleporter
this.grid.setCell(tx, ty, tz, 22); // Teleporter
// Logic linkage would be separate asset, but visual ID 22 is enough for now.
// console.log("Teleporter Placed");
}
});
}\n\n"""
# Assemble
final_content = (
chunk1 +
[" // A* Removed\n"] +
chunk2 +
[new_buildBridge] +
chunk3 +
[" // Legacy Removed\n"] +
chunk4 +
[new_connectivity] +
chunk5
)
# Write back
with open(path, "w", encoding="utf-8") as f:
f.writelines(final_content)
print("Refactor Complete")

View file

@ -1,57 +0,0 @@
# **Crystal Spires: Light Bridge Specification**
This document details the generation logic for "Light Bridges" (ID 20) connecting floating islands in the Crystal Spires biome.
## **1. Visual & Gameplay Goals**
- **Visuals:** Translucent, glowing blue pathways projected between solid structures.
- **Gameplay:** Must be strictly walkable.
- **Max Step Height:** 1 Voxel (Units cannot jump 2 blocks up).
- **Width:** 1 voxel (Standard) or 2 voxels (Main thoroughfares).
- **Material:** "Hard Light" (ID 20). Unlike stone, it has no "thickness" below it (floating).
- **Trajectory:** Organic curves or arcs, not strict straight lines. Can defy gravity slightly (arcing up/down).
## **2. Generation Logic: Additive Bezier Arcs**
Bridges are constructed using **Quadratic Bezier Curves** that strictly adhere to an **Additive-Only** rule.
**Algorithm:**
1. **Anchor Selection (Edge-to-Edge):**
- Do not connect centers. Find the voxel on the _perimeter_ of Island A that is closest to Island B.
- Find the corresponding perimeter voxel on Island B (or the surface of a Spire).
2. **Control Point Calculation:**
- Calculate the midpoint between Start and End.
- Apply a randomized **Vertical Offset** (Arch) and **Lateral Offset** (Curve) to the midpoint to create the Bezier Control Point ($P_1$).
3. **Voxelization (Non-Destructive):**
- Interpolate the curve.
- **Collision Scan:** Before placing a voxel, verify that the target coordinate is Air (ID 0) AND the two voxels above it (Headroom) are Air (ID 0).
- **Placement:** If and ONLY if the scan is clear, set the target coordinate to Bridge (ID 20).
- **Abort:** If the scan finds any solid block in the path (Floor or Headroom), the bridge segment stops or the entire connection is recalculated.
## **3. Conditions of Acceptance (CoA)**
**CoA 1: Walkability (Gradient)**
- Between any two adjacent voxels on the bridge path, the Y-difference must be <= 1.
**CoA 2: Strictly Additive (No Destruction)**
- The generator must **never** set a Voxel ID to 0 (Air) if it was previously non-zero.
- The generator must **never** set a Voxel ID to 20 (Bridge) if it was previously non-zero.
- Bridges must weave around existing geometry or fail; they cannot tunnel.
**CoA 3: Headroom Validation**
- Instead of _carving_ headroom, the generator must _validate_ headroom.
- A bridge voxel is only valid if Grid[y+1] and Grid[y+2] are already 0.
**CoA 4: Connectivity**
- The bridge must connect the edge of the start platform to the edge of the end platform without leaving a gap.
**CoA 5: Global Reachability (No Orphans)**
- After all bridges are generated, a **Flood Fill** check from the player spawn zone is mandatory.
- Any platform that is not reached by this fill is considered an "Orphan."
- **Resolution:** Orphans must be explicitly connected via a fallback teleport node. The Orphan will have one teleport node randomly placed on it, and the other will be randomly on a non-Orphan platform

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 KiB

View file

@ -1131,7 +1131,6 @@ export class GameLoop {
*/ */
async startLevel(runData, options = {}) { async startLevel(runData, options = {}) {
console.log("GameLoop: Generating Level..."); console.log("GameLoop: Generating Level...");
console.log(`Level Seed: ${runData.seed}`);
this.runData = runData; this.runData = runData;
this.isRunning = true; this.isRunning = true;
@ -1172,7 +1171,7 @@ export class GameLoop {
// Restart the animation loop if it was stopped // Restart the animation loop if it was stopped
this.animate(); this.animate();
this.grid = new VoxelGrid(20, 20, 20); this.grid = new VoxelGrid(20, 10, 20);
let generator; let generator;
const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES"; const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES";

View file

@ -559,10 +559,11 @@ class GameStateManagerClass {
// Clear the active run (persistence and memory) // Clear the active run (persistence and memory)
await this.clearActiveRun(); await this.clearActiveRun();
// NOTE: We do NOT transition to Main Menu automatically here. // Transition to Main Menu (will show Hub if unlocking conditions met)
// The GameLoop (UI layer) is responsible for showing the Mission Debrief await this.transitionTo(GameStateManagerClass.STATES.MAIN_MENU);
// and then requesting title/hub transition when the user is done.
// If we transitioned here, we would rip the UI away before the user saw the results. // Force a refresh of the Hub screen if it was already open/cached?
// The state transition should handle visibility, but we check specific UI updates if needed.
}); });
} }

View file

@ -69,36 +69,23 @@ export class CaveGenerator extends BaseGenerator {
} }
generate(fillPercent = 0.45, iterations = 4) { generate(fillPercent = 0.45, iterations = 4) {
// 0. Decide Layout Topology
// "Tunnel" = Open Z-min and Z-max
// "Dead End" = Open Z-min only
this.isTunnel = this.rng.next() > 0.5;
// 1. Initialize 2D Map // 1. Initialize 2D Map
// 1 = Wall, 0 = Floor // 1 = Wall, 0 = Floor
let map = []; let map = [];
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
map[x] = []; map[x] = [];
for (let z = 0; z < this.depth; z++) { for (let z = 0; z < this.depth; z++) {
// Border Logic // Border is always wall
const isSideBorder = x === 0 || x === this.width - 1; if (
const isBackBorder = z === this.depth - 1; x === 0 ||
const isFrontBorder = z === 0; x === this.width - 1 ||
z === 0 ||
let isSolid = false; z === this.depth - 1
) {
if (isSideBorder) { map[x][z] = 1;
isSolid = true; // Sides are always walls (but low)
} else if (isFrontBorder) {
isSolid = false; // Entrance always open
} else if (isBackBorder) {
isSolid = !this.isTunnel; // Back open if tunnel, closed if dead end
} else { } else {
// Internal map[x][z] = this.rng.chance(fillPercent) ? 1 : 0;
isSolid = this.rng.chance(fillPercent);
} }
map[x][z] = isSolid ? 1 : 0;
} }
} }
@ -108,23 +95,22 @@ export class CaveGenerator extends BaseGenerator {
} }
// 3. Post-Process: Ensure Connectivity // 3. Post-Process: Ensure Connectivity
// Note: ensureConnectivity respects 0s, so open entrances remain open if connected
map = this.ensureConnectivity(map); map = this.ensureConnectivity(map);
// 4. Extrude to 3D Voxel Grid // 4. Extrude to 3D Voxel Grid
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
for (let z = 0; z < this.depth; z++) { for (let z = 0; z < this.depth; z++) {
if (map[x][z] === 1) { const isWall = map[x][z] === 1;
// Wall Logic
// Cap all walls to height 6 for visibility (cutoff view)
const MAX_WALL_HEIGHT = 6;
if (isWall) {
// Wall Logic
// Vary height slightly for "voxel aesthetic" // Vary height slightly for "voxel aesthetic"
// height-1 or height, but capped at MAX_WALL_HEIGHT // height-1 or height
const baseHeight = Math.min(this.height, MAX_WALL_HEIGHT); const wallHeight = this.height - (this.rng.next() > 0.5 ? 1 : 0);
const wallHeight = baseHeight - (this.rng.next() > 0.5 ? 1 : 0);
for (let y = 0; y < wallHeight; y++) { for (let y = 0; y < wallHeight; y++) {
// Use provisional ID 100 for walls, we will texture later or set directly here
// Just for consistency with previous flow, we'll use placeholder 100
this.grid.setCell(x, y, z, 100); this.grid.setCell(x, y, z, 100);
} }
} else { } else {
@ -299,23 +285,16 @@ export class CaveGenerator extends BaseGenerator {
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
newMap[x] = []; newMap[x] = [];
for (let z = 0; z < this.depth; z++) { for (let z = 0; z < this.depth; z++) {
// Border Logic (Must match generate loop to avoid closing entrances) // Borders stay walls
const isSideBorder = x === 0 || x === this.width - 1; if (
const isFrontBorder = z === 0; x === 0 ||
const isBackBorder = z === this.depth - 1; x === this.width - 1 ||
z === 0 ||
if (isSideBorder) { z === this.depth - 1
) {
newMap[x][z] = 1; newMap[x][z] = 1;
continue; continue;
} }
if (isFrontBorder) {
newMap[x][z] = 0;
continue;
}
if (isBackBorder) {
newMap[x][z] = this.isTunnel ? 0 : 1;
continue;
}
const neighbors = this.get2DNeighbors(map, x, z); const neighbors = this.get2DNeighbors(map, x, z);
if (neighbors > 4) newMap[x][z] = 1; if (neighbors > 4) newMap[x][z] = 1;

File diff suppressed because it is too large Load diff

View file

@ -37,16 +37,6 @@ export class VoxelManager {
color: 0x00ffff, color: 0x00ffff,
emissive: 0x004444, emissive: 0x004444,
}), // Crystal }), // Crystal
20: new THREE.MeshStandardMaterial({
color: 0x0088ff,
emissive: 0x002244,
transparent: true,
opacity: 0.8,
}), // Hard Light Bridge
22: new THREE.MeshStandardMaterial({
color: 0xff00ff,
emissive: 0x440044,
}), // Teleporter Node
}; };
// Shared Geometry // Shared Geometry

View file

@ -11,10 +11,7 @@ export class Item {
this.name = def.name; this.name = def.name;
this.type = def.type; // WEAPON, ARMOR, UTILITY, RELIC this.type = def.type; // WEAPON, ARMOR, UTILITY, RELIC
this.rarity = def.rarity || "COMMON"; this.rarity = def.rarity || "COMMON";
this.rarity = def.rarity || "COMMON";
this.tags = def.tags || []; this.tags = def.tags || [];
this.icon = def.icon || null;
this.description = def.description || "";
// Base Stats (e.g. { attack: 5, defense: 2 }) // Base Stats (e.g. { attack: 5, defense: 2 })
this.stats = def.stats || {}; this.stats = def.stats || {};

View file

@ -1,103 +0,0 @@
[
{
"id": "ITEM_STEEL_LONGSWORD",
"name": "Steel Longsword",
"type": "WEAPON",
"rarity": "UNCOMMON",
"tags": ["PHYSICAL", "MELEE"],
"stats": {
"attack": 6,
"accuracy": 5
},
"description": "A finely forged blade, balanced and sharp.",
"icon": "assets/icons/items/item_steel_longsword.png"
},
{
"id": "ITEM_REINFORCED_PLATE",
"name": "Reinforced Plate",
"type": "ARMOR",
"rarity": "UNCOMMON",
"tags": ["HEAVY"],
"stats": {
"defense": 6,
"health": 10,
"speed": -2
},
"description": "Heavy steel plates reinforced with chainmail.",
"icon": "assets/icons/items/item_reinforced_plate.png"
},
{
"id": "ITEM_CRYSTAL_STAFF",
"name": "Crystal Staff",
"type": "WEAPON",
"rarity": "UNCOMMON",
"tags": ["MAGIC", "RANGED", "TWO_HANDED"],
"stats": {
"magic": 8,
"willpower": 3
},
"description": "Embedded with a large mana crystal for focusing power.",
"icon": "assets/icons/items/item_crystal_staff.png"
},
{
"id": "ITEM_SILK_WEAVE_ROBES",
"name": "Silk Weave Robes",
"type": "ARMOR",
"rarity": "UNCOMMON",
"tags": ["LIGHT", "MAGIC"],
"stats": {
"willpower": 6,
"magic": 3,
"speed": 1
},
"description": "Enchanted silk that offers protection without weight.",
"icon": "assets/icons/items/item_silk_weave_robes.png"
},
{
"id": "ITEM_SERRATED_DAGGER",
"name": "Serrated Dagger",
"type": "WEAPON",
"rarity": "UNCOMMON",
"tags": ["PHYSICAL", "MELEE", "LIGHT"],
"stats": {
"attack": 5,
"crit_chance": 10,
"speed": 2
},
"description": "Designed to inflict bleeding wounds.",
"icon": "assets/icons/items/item_serrated_dagger.png"
},
{
"id": "ITEM_AMULET_VITALITY",
"name": "Amulet of Vitality",
"type": "ACCESSORY",
"rarity": "RARE",
"tags": ["MAGIC"],
"stats": {
"health": 20,
"regen": 2
},
"description": "Radiates a warm, life-giving energy.",
"icon": "assets/icons/items/item_amulet_vitality.png"
},
{
"id": "ITEM_ADVANCED_TOOLKIT",
"name": "Advanced Toolkit",
"type": "UTILITY",
"rarity": "UNCOMMON",
"tags": ["TECH"],
"stats": {
"tech": 5
},
"passive_effects": [
{
"trigger": "ON_INTERACT",
"condition": "mechanical",
"action": "REPAIR",
"value": 10
}
],
"description": "Contains precision tools for complex machinery.",
"icon": "assets/icons/items/item_advanced_toolkit.png"
}
]

View file

@ -35,13 +35,8 @@ export class ItemRegistry {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _doLoadAll() { async _doLoadAll() {
// Lazy-load item definitions // Lazy-load tier1_gear.json
const tier1Gear = await import("../items/tier1_gear.json", { const tier1Gear = await import("../items/tier1_gear.json", { with: { type: "json" } }).then(m => m.default);
with: { type: "json" },
}).then((m) => m.default);
const tier2Gear = await import("../items/tier2_gear.json", {
with: { type: "json" },
}).then((m) => m.default);
// Load tier1_gear.json // Load tier1_gear.json
for (const itemDef of tier1Gear) { for (const itemDef of tier1Gear) {
@ -51,14 +46,6 @@ export class ItemRegistry {
} }
} }
// Load tier2_gear.json
for (const itemDef of tier2Gear) {
if (itemDef && itemDef.id) {
const item = new Item(itemDef);
this.items.set(itemDef.id, item);
}
}
console.log(`Loaded ${this.items.size} items`); console.log(`Loaded ${this.items.size} items`);
} }
@ -82,3 +69,4 @@ export class ItemRegistry {
// Export singleton instance // Export singleton instance
export const itemRegistry = new ItemRegistry(); export const itemRegistry = new ItemRegistry();

View file

@ -78,6 +78,7 @@ export class MarketManager {
this.marketState = { this.marketState = {
generationId: `INIT_${Date.now()}`, generationId: `INIT_${Date.now()}`,
stock: [], stock: [],
buyback: [],
}; };
await this.generateStock(1); await this.generateStock(1);
} }
@ -129,26 +130,20 @@ export class MarketManager {
const newStock = []; const newStock = [];
if (tier === 1) { if (tier === 1) {
// Tier 1: Weapons, Armor, and basic Utility // Tier 1: Smith (5 Common Weapons, 3 Common Armor)
const weapons = this._generateMerchantStock( const smithWeapons = this._generateMerchantStock(
allItems, allItems,
["WEAPON"], ["WEAPON"],
{ COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 }, { COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 },
5 5
); );
const armor = this._generateMerchantStock( const smithArmor = this._generateMerchantStock(
allItems, allItems,
["ARMOR"], ["ARMOR"],
{ COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 }, { COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 },
3 3
); );
const utility = this._generateMerchantStock( newStock.push(...smithWeapons, ...smithArmor);
allItems,
["UTILITY"],
{ COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 },
2
);
newStock.push(...weapons, ...armor, ...utility);
// Alchemist (5 Potions, 2 Grenades) - simplified for now since we don't have potions in tier1_gear // Alchemist (5 Potions, 2 Grenades) - simplified for now since we don't have potions in tier1_gear
// Will add when consumables are available // Will add when consumables are available
@ -187,7 +182,8 @@ export class MarketManager {
const stockWithIds = newStock.map((item, index) => { const stockWithIds = newStock.map((item, index) => {
const itemDef = this.itemRegistry.get(item.defId); const itemDef = this.itemRegistry.get(item.defId);
const basePrice = this._calculateBasePrice(itemDef); const basePrice = this._calculateBasePrice(itemDef);
const price = basePrice; const variance = 1 + (Math.random() * 0.2 - 0.1); // ±10% variance
const price = Math.floor(basePrice * variance);
return { return {
id: `STOCK_${Date.now()}_${index}`, id: `STOCK_${Date.now()}_${index}`,
@ -299,9 +295,10 @@ export class MarketManager {
*/ */
async buyItem(stockId) { async buyItem(stockId) {
// Check both stock and buyback // Check both stock and buyback
const marketItem = this.marketState.stock.find( let marketItem = this.marketState.stock.find((item) => item.id === stockId);
(item) => item.id === stockId if (!marketItem) {
); marketItem = this.marketState.buyback.find((item) => item.id === stockId);
}
if (!marketItem || marketItem.purchased) { if (!marketItem || marketItem.purchased) {
return false; return false;
} }
@ -405,21 +402,23 @@ export class MarketManager {
}) })
); );
// 6. Add to Market Stock (at full price) // 6. Create buyback entry (limit 10)
const marketItem = { if (this.marketState.buyback.length >= 10) {
id: `STOCK_${Date.now()}_SOLD_${Math.random() this.marketState.buyback.shift(); // Remove oldest
.toString(36) }
.substr(2, 5)}`,
const buybackItem = {
id: `BUYBACK_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
defId: itemInstance.defId, defId: itemInstance.defId,
type: itemDef.type, type: itemDef.type,
rarity: itemDef.rarity, rarity: itemDef.rarity,
price: basePrice, // Resell at full market value price: sellPrice, // Buyback price = sell price
discount: 0, discount: 0,
purchased: false, purchased: false,
instanceData: { ...itemInstance }, // Store copy of original instance instanceData: { ...itemInstance }, // Store copy of original instance
}; };
this.marketState.stock.push(marketItem); this.marketState.buyback.push(buybackItem);
// 7. Save market state // 7. Save market state
await this.persistence.saveMarketState(this.marketState); await this.persistence.saveMarketState(this.marketState);
@ -446,16 +445,15 @@ export class MarketManager {
*/ */
getStockForMerchant(merchantType) { getStockForMerchant(merchantType) {
if (merchantType === "BUYBACK") { if (merchantType === "BUYBACK") {
return []; return this.marketState.buyback;
} }
// Filter stock by merchant type // Filter stock by merchant type
const typeMap = { const typeMap = {
WEAPONS: ["WEAPON"], SMITH: ["WEAPON", "ARMOR"],
ARMOR: ["ARMOR"], TAILOR: ["ARMOR"],
ENGINEER: ["UTILITY"], ALCHEMIST: ["CONSUMABLE", "UTILITY"],
ALCHEMIST: ["CONSUMABLE"], SCAVENGER: ["RELIC", "UTILITY"],
BAZAAR: ["RELIC"],
}; };
const allowedTypes = typeMap[merchantType] || []; const allowedTypes = typeMap[merchantType] || [];
@ -464,39 +462,6 @@ export class MarketManager {
); );
} }
/**
* Gets sellable items from player's hub stash.
* @returns {MarketItem[]}
*/
getSellableInventory() {
if (!this.inventoryManager?.hubStash) return [];
// Get all items from stash
const items = this.inventoryManager.hubStash.items || [];
// Format as MarketItems
return items
.map((itemInstance) => {
const itemDef = this.itemRegistry.get(itemInstance.defId);
if (!itemDef) return null;
const basePrice = this._calculateBasePrice(itemDef);
const sellPrice = Math.floor(basePrice * 0.25);
return {
id: itemInstance.uid, // Use UID for selling
defId: itemInstance.defId,
type: itemDef.type,
rarity: itemDef.rarity,
price: sellPrice,
discount: 0,
purchased: false, // Not relevant for selling, but keeps shape
isSellable: true, // Flag for UI
};
})
.filter((item) => item !== null);
}
/** /**
* Cleanup - remove event listeners. * Cleanup - remove event listeners.
*/ */

View file

@ -16,8 +16,6 @@ export class RosterManager {
this.roster = []; // List of active Explorer objects (Data only) this.roster = []; // List of active Explorer objects (Data only)
/** @type {ExplorerData[]} */ /** @type {ExplorerData[]} */
this.graveyard = []; // List of dead units this.graveyard = []; // List of dead units
/** @type {ExplorerData[]} */
this.candidates = []; // Pool of recruitable units
/** @type {number} */ /** @type {number} */
this.rosterLimit = 12; this.rosterLimit = 12;
} }
@ -29,20 +27,6 @@ export class RosterManager {
load(saveData) { load(saveData) {
this.roster = saveData.roster || []; this.roster = saveData.roster || [];
this.graveyard = saveData.graveyard || []; this.graveyard = saveData.graveyard || [];
this.candidates = saveData.candidates || [];
// DATA MIGRATION: Fix legacy portrait paths
const fixPortrait = (unit) => {
if (unit.portrait && unit.portrait.includes("class_")) {
// Replace "class_" with "" in the filename part
// e.g. assets/images/portraits/class_weaver.png -> assets/images/portraits/weaver.png
// Use regex to be safe: /class_([a-z]+)\.png/
unit.portrait = unit.portrait.replace(/class_/g, "");
}
};
this.roster.forEach(fixPortrait);
this.candidates.forEach(fixPortrait);
} }
/** /**
@ -53,7 +37,6 @@ export class RosterManager {
return { return {
roster: this.roster, roster: this.roster,
graveyard: this.graveyard, graveyard: this.graveyard,
candidates: this.candidates,
}; };
} }
@ -115,80 +98,7 @@ export class RosterManager {
* Clears the roster and graveyard. Used when starting a new game. * Clears the roster and graveyard. Used when starting a new game.
*/ */
clear() { clear() {
this.roster = [];
this.graveyard = []; this.graveyard = [];
this.candidates = [];
}
/**
* Generates a new set of candidates for recruitment.
* @param {number} count - Number of candidates to generate
*/
async generateCandidates(count = 3) {
this.candidates = [];
// Lazy import name generator
const { generateCharacterName } = await import("../utils/nameGenerator.js");
// Available classes (basic pool)
const classes = [
"CLASS_VANGUARD",
"CLASS_WEAVER",
"CLASS_SCAVENGER",
"CLASS_TINKER",
];
for (let i = 0; i < count; i++) {
const randomClass = classes[Math.floor(Math.random() * classes.length)];
const name = generateCharacterName();
const candidate = {
id: `CANDIDATE_${Date.now()}_${i}`,
name: name,
className: randomClass, // ID used for lookup
activeClassId: randomClass,
level: 1,
status: "READY",
hiringCost: 100, // Fixed cost for now
portrait: `assets/images/portraits/${randomClass
.replace("CLASS_", "")
.toLowerCase()}.png`,
};
this.candidates.push(candidate);
}
}
/**
* Hires a candidate, moving them to the roster.
* @param {string} candidateId
* @param {Object} wallet - Reference to wallet to deduct funds (if handled here, but manager acts on data)
* @returns {Promise<boolean>} success
*/
async hireCandidate(candidateId) {
if (this.roster.length >= this.rosterLimit) {
console.warn("Roster full.");
return false;
}
const index = this.candidates.findIndex((c) => c.id === candidateId);
if (index === -1) return false;
const candidate = this.candidates[index];
// Recruit logic specific to candidate -> unit conversion
// (We can reused recruitUnit but we need to ensure ID is unique/updated if needed, though candidate ID is fine usually)
// Actually, recruitUnit generates a new ID. Let's use that to be safe and consistent.
await this.recruitUnit({
name: candidate.name,
className: candidate.className,
activeClassId: candidate.activeClassId,
portrait: candidate.portrait,
});
// Remove from candidates
this.candidates.splice(index, 1);
return true;
} }
} }

View file

@ -109,11 +109,6 @@
Apply Textures Apply Textures
</label> </label>
<label style="cursor: pointer">
<input type="checkbox" id="zoneToggle" />
Show Spawn Zones
</label>
<button id="generateBtn">Generate Map</button> <button id="generateBtn">Generate Map</button>
<div style="margin-top: 10px; font-size: 0.8em; color: #888"> <div style="margin-top: 10px; font-size: 0.8em; color: #888">

View file

@ -54,7 +54,6 @@ class MapVisualizer {
this.fillInput = document.getElementById("fillInput"); this.fillInput = document.getElementById("fillInput");
this.iterInput = document.getElementById("iterInput"); this.iterInput = document.getElementById("iterInput");
this.textureToggle = document.getElementById("textureToggle"); this.textureToggle = document.getElementById("textureToggle");
this.zoneToggle = document.getElementById("zoneToggle");
this.genBtn = document.getElementById("generateBtn"); this.genBtn = document.getElementById("generateBtn");
// Populate Generator Options // Populate Generator Options
@ -76,9 +75,6 @@ class MapVisualizer {
this.textureToggle.addEventListener("change", () => { this.textureToggle.addEventListener("change", () => {
if (this.lastGrid) this.renderGrid(this.lastGrid); if (this.lastGrid) this.renderGrid(this.lastGrid);
}); });
this.zoneToggle.addEventListener("change", () => {
if (this.zoneGroup) this.zoneGroup.visible = this.zoneToggle.checked;
});
this.loadFromURL(); this.loadFromURL();
} }
@ -91,8 +87,6 @@ class MapVisualizer {
if (params.has("iter")) this.iterInput.value = params.get("iter"); if (params.has("iter")) this.iterInput.value = params.get("iter");
if (params.has("textures")) if (params.has("textures"))
this.textureToggle.checked = params.get("textures") === "true"; this.textureToggle.checked = params.get("textures") === "true";
if (params.has("zones"))
this.zoneToggle.checked = params.get("zones") === "true";
} }
updateURL(type, seed, fill, iter) { updateURL(type, seed, fill, iter) {
@ -102,7 +96,6 @@ class MapVisualizer {
params.set("fill", fill); params.set("fill", fill);
params.set("iter", iter); params.set("iter", iter);
params.set("textures", this.textureToggle.checked); params.set("textures", this.textureToggle.checked);
params.set("zones", this.zoneToggle.checked);
const newUrl = `${window.location.pathname}?${params.toString()}`; const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({}, "", newUrl); window.history.replaceState({}, "", newUrl);
@ -121,10 +114,10 @@ class MapVisualizer {
this.updateURL(type, seed, fill, iter); this.updateURL(type, seed, fill, iter);
// 1. Setup Grid // 1. Setup Grid
// Standard size (matches GameLoop default) // Standard size for testing (matches test cases roughly)
const width = 20; const width = 30;
const height = 20; const height = 40; // Increased verticality
const depth = 20; const depth = 30;
const grid = new VoxelGrid(width, height, depth); const grid = new VoxelGrid(width, height, depth);
// 2. Run Generator // 2. Run Generator
@ -146,8 +139,9 @@ class MapVisualizer {
} else if (type === "ContestedFrontierGenerator") { } else if (type === "ContestedFrontierGenerator") {
generator = new ContestedFrontierGenerator(grid, seed); generator = new ContestedFrontierGenerator(grid, seed);
generator.generate(); generator.generate();
} else if (type === "VoidSeepDepthsGenerator") {
generator = new VoidSeepDepthsGenerator(grid, seed); generator = new VoidSeepDepthsGenerator(grid, seed);
// fill -> difficulty?
// iter -> ignored?
generator.generate(iter || 1); generator.generate(iter || 1);
} else if (type === "RustingWastesGenerator") { } else if (type === "RustingWastesGenerator") {
generator = new RustingWastesGenerator(grid, seed); generator = new RustingWastesGenerator(grid, seed);
@ -167,20 +161,6 @@ class MapVisualizer {
// 3. Render // 3. Render
this.lastGrid = grid; this.lastGrid = grid;
this.renderGrid(grid); this.renderGrid(grid);
// 4. Render Spawn Zones (if available)
if (
generator &&
generator.generatedAssets &&
generator.generatedAssets.spawnZones
) {
this.renderZones(generator.generatedAssets.spawnZones);
} else {
if (this.zoneGroup) {
this.scene.remove(this.zoneGroup);
this.zoneGroup = null;
}
}
} }
generateMaterialsFromPalette(palette) { generateMaterialsFromPalette(palette) {
@ -231,20 +211,7 @@ class MapVisualizer {
const materialCover = new THREE.MeshStandardMaterial({ color: 0xaa4444 }); // Red cover const materialCover = new THREE.MeshStandardMaterial({ color: 0xaa4444 }); // Red cover
const materialDetail = new THREE.MeshStandardMaterial({ color: 0xaa44aa }); // Purple details const materialDetail = new THREE.MeshStandardMaterial({ color: 0xaa44aa }); // Purple details
const materialCrystal = new THREE.MeshStandardMaterial({ color: 0x00ffff }); // Cyan crystals const materialCrystal = new THREE.MeshStandardMaterial({ color: 0x00ffff }); // Cyan crystals
const materialStructure = new THREE.MeshStandardMaterial({ // Blue structure
color: 0x4444aa,
}); // Blue structure
const materialBridge = new THREE.MeshStandardMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.6,
});
const materialTeleporter = new THREE.MeshStandardMaterial({
color: 0xff00ff,
emissive: 0xff00ff,
emissiveIntensity: 1.0,
});
const materialMud = new THREE.MeshStandardMaterial({ color: 0x5c4033 }); // Dark Brown const materialMud = new THREE.MeshStandardMaterial({ color: 0x5c4033 }); // Dark Brown
const materialSandbag = new THREE.MeshStandardMaterial({ color: 0xc2b280 }); // Sand/Tan const materialSandbag = new THREE.MeshStandardMaterial({ color: 0xc2b280 }); // Sand/Tan
@ -281,9 +248,7 @@ class MapVisualizer {
mat = this.generatedMaterials[id]; mat = this.generatedMaterials[id];
} else { } else {
// Heuristic for coloring based on IDs in CaveGenerator // Heuristic for coloring based on IDs in CaveGenerator
if (id === 20) mat = materialBridge; if (id >= 200 && id < 300) mat = materialFloor;
else if (id === 22) mat = materialTeleporter;
else if (id >= 200 && id < 300) mat = materialFloor;
else if (id === 10) mat = materialCover; // Cover else if (id === 10) mat = materialCover; // Cover
else if (id === 15) mat = materialCrystal; // Crystal Scatter else if (id === 15) mat = materialCrystal; // Crystal Scatter
else if (id === 12) mat = materialSandbag; // Sandbags else if (id === 12) mat = materialSandbag; // Sandbags
@ -320,47 +285,6 @@ class MapVisualizer {
this.controls.target.set(grid.size.x / 2, 0, grid.size.z / 2); this.controls.target.set(grid.size.x / 2, 0, grid.size.z / 2);
} }
renderZones(spawnZones) {
if (this.zoneGroup) {
this.scene.remove(this.zoneGroup);
}
this.zoneGroup = new THREE.Group();
this.scene.add(this.zoneGroup);
// Player Zones (Blue)
const playerGeo = new THREE.BoxGeometry(0.8, 1, 0.8);
const playerMat = new THREE.MeshBasicMaterial({
color: 0x0000ff,
transparent: true,
opacity: 0.3,
});
// Enemy Zones (Red)
const enemyMat = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.3,
});
if (spawnZones.player) {
spawnZones.player.forEach((pos) => {
const mesh = new THREE.Mesh(playerGeo, playerMat);
mesh.position.set(pos.x, pos.y + 0.5, pos.z);
this.zoneGroup.add(mesh);
});
}
if (spawnZones.enemy) {
spawnZones.enemy.forEach((pos) => {
const mesh = new THREE.Mesh(playerGeo, enemyMat);
mesh.position.set(pos.x, pos.y + 0.5, pos.z);
this.zoneGroup.add(mesh);
});
}
this.zoneGroup.visible = this.zoneToggle.checked;
}
onWindowResize() { onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();

View file

@ -433,8 +433,8 @@ export class CharacterSheet extends LitElement {
} }
.equipment-slot { .equipment-slot {
width: 100%; width: clamp(50px, 7cqw, 70px);
height: 100%; height: clamp(50px, 7cqw, 70px);
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
border: 2px solid #555; border: 2px solid #555;
display: flex; display: flex;
@ -443,7 +443,6 @@ export class CharacterSheet extends LitElement {
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
position: relative; position: relative;
padding: 0;
} }
.equipment-slot:hover { .equipment-slot:hover {
@ -509,6 +508,7 @@ export class CharacterSheet extends LitElement {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
padding: 5px;
} }
.slot-label { .slot-label {
@ -591,7 +591,6 @@ export class CharacterSheet extends LitElement {
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
position: relative; position: relative;
padding: 0;
} }
.item-card:hover { .item-card:hover {
@ -631,6 +630,7 @@ export class CharacterSheet extends LitElement {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
padding: 5px;
} }
.skills-container { .skills-container {

View file

@ -33,12 +33,11 @@ export class BarracksScreen extends LitElement {
font-family: var(--font-family); font-family: var(--font-family);
display: grid; display: grid;
grid-template-columns: 60% 40%; grid-template-columns: 60% 40%;
grid-template-rows: auto auto 1fr; grid-template-rows: auto 1fr;
grid-template-areas: grid-template-areas:
"header header" "header header"
"tabs tabs" "roster detail";
"content content"; gap: var(--spacing-lg);
gap: var(--spacing-md);
} }
.header { .header {
@ -119,8 +118,9 @@ export class BarracksScreen extends LitElement {
color: var(--color-text-primary); color: var(--color-text-primary);
} }
/* Roster List */
.roster-list { .roster-list {
/* grid-area: roster; - Removed to allow nesting */ grid-area: roster;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-md); gap: var(--spacing-md);
@ -244,7 +244,7 @@ export class BarracksScreen extends LitElement {
/* Detail Sidebar */ /* Detail Sidebar */
.detail-sidebar { .detail-sidebar {
/* grid-area: detail; - Removed to allow nesting */ grid-area: detail;
background: var(--color-bg-panel); background: var(--color-bg-panel);
border: var(--border-width-medium) solid var(--color-border-default); border: var(--border-width-medium) solid var(--color-border-default);
padding: var(--spacing-lg); padding: var(--spacing-lg);
@ -339,9 +339,7 @@ export class BarracksScreen extends LitElement {
filter: { type: String }, filter: { type: String },
sort: { type: String }, sort: { type: String },
healingCostPerHp: { type: Number }, healingCostPerHp: { type: Number },
wallet: { type: Object }, wallet: { type: Object },
viewMode: { type: String }, // "ROSTER" | "RECRUIT"
}; };
} }
@ -353,23 +351,12 @@ export class BarracksScreen extends LitElement {
this.sort = "LEVEL_DESC"; this.sort = "LEVEL_DESC";
this.healingCostPerHp = 0.5; // 1 HP = 0.5 Shards this.healingCostPerHp = 0.5; // 1 HP = 0.5 Shards
this.wallet = { aetherShards: 0, ancientCores: 0 }; this.wallet = { aetherShards: 0, ancientCores: 0 };
this.viewMode = "ROSTER";
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._loadRoster(); this._loadRoster();
this._loadWallet(); this._loadWallet();
// Auto-generate candidates if none exist
if (
!gameStateManager.rosterManager.candidates ||
gameStateManager.rosterManager.candidates.length === 0
) {
gameStateManager.rosterManager
.generateCandidates(4)
.then(() => this.requestUpdate());
}
} }
_loadRoster() { _loadRoster() {
@ -468,9 +455,7 @@ export class BarracksScreen extends LitElement {
let portrait = unitData.portrait || unitData.image; let portrait = unitData.portrait || unitData.image;
if (!portrait) { if (!portrait) {
// Default portrait path based on class // Default portrait path based on class
portrait = `assets/images/portraits/${activeClassId portrait = `assets/images/portraits/${activeClassId.toLowerCase()}.png`;
.replace("CLASS_", "")
.toLowerCase()}.png`;
} }
return { return {
@ -703,9 +688,7 @@ export class BarracksScreen extends LitElement {
explorer.portrait = rosterData.portrait || rosterData.image; explorer.portrait = rosterData.portrait || rosterData.image;
} else { } else {
// Default portrait path // Default portrait path
explorer.portrait = `assets/images/portraits/${activeClassId explorer.portrait = `assets/images/portraits/${activeClassId.toLowerCase()}.png`;
.replace("CLASS_", "")
.toLowerCase()}.png`;
} }
// Generate skill tree if gameLoop isn't running // Generate skill tree if gameLoop isn't running
@ -861,232 +844,109 @@ export class BarracksScreen extends LitElement {
`; `;
} }
_renderRecruitView() { _renderDetailSidebar() {
const candidates = gameStateManager.rosterManager.candidates || []; const unit = this._getSelectedUnit();
if (!unit) {
if (candidates.length === 0) { return html`
return html`<div class="empty-state"> <div class="detail-sidebar">
No recruits available at this time. <div class="empty-state">
</div>`; <p>Select a unit to view details</p>
}
return html`
<div class="roster-list">
${candidates.map(
(candidate) => html`
<div class="unit-card ready">
<div class="unit-portrait">
<img
src="${candidate.portrait ||
"assets/images/portraits/default.png"}"
alt="Portrait"
/>
</div>
<div class="unit-info">
<div class="unit-name">${candidate.name}</div>
<div class="unit-meta">
<span class="unit-class"
>${candidate.className.replace("CLASS_", "")}</span
>
<span class="unit-level">Lvl 1</span>
</div>
</div>
<div class="unit-status">
<button
class="btn btn-primary"
?disabled=${this.wallet.aetherShards < candidate.hiringCost}
@click=${() => this._onHireClick(candidate)}
>
Hire (${candidate.hiringCost} 💎)
</button>
</div>
</div>
`
)}
</div>
<div class="detail-sidebar">
<div class="detail-header">
<h3>Recruitment Office</h3>
<p style="color: var(--color-text-secondary);">
New candidates arrive periodically. Hire them to expand your squad.
</p>
</div>
</div>
`;
}
async _onHireClick(candidate) {
if (this.wallet.aetherShards < candidate.hiringCost) {
alert("Insufficient funds.");
return;
}
// Deduct Funds
if (gameStateManager.hubStash && gameStateManager.hubStash.currency) {
gameStateManager.hubStash.currency.aetherShards -= candidate.hiringCost;
this.wallet.aetherShards =
gameStateManager.hubStash.currency.aetherShards;
// Calls RosterManager to move candidate to roster
const success = await gameStateManager.rosterManager.hireCandidate(
candidate.id
);
if (success) {
// Save everything
await gameStateManager.persistence.saveRoster(
gameStateManager.rosterManager.save()
);
await gameStateManager.persistence.saveHubStash(
gameStateManager.hubStash
);
// Update UI
this._loadRoster();
this.requestUpdate();
window.dispatchEvent(
new CustomEvent("wallet-updated", {
detail: { wallet: { ...this.wallet } },
bubbles: true,
composed: true,
})
);
} else {
alert("Roster is full!");
// Refund if failed (simple rollback)
gameStateManager.hubStash.currency.aetherShards += candidate.hiringCost;
this.wallet.aetherShards =
gameStateManager.hubStash.currency.aetherShards;
}
}
}
_renderRosterView() {
const filteredUnits = this._getFilteredUnits();
const selectedUnit = this._getSelectedUnit();
return html`
<div
style="grid-column: 1 / -1; display: grid; grid-template-columns: 60% 40%; gap: var(--spacing-lg); height: 100%;"
>
<div
class="roster-list-container"
style="display: flex; flex-direction: column; gap: 10px; overflow: hidden;"
>
<div class="filter-bar">
<button
class="filter-button ${this.filter === "ALL" ? "active" : ""}"
@click=${() => this._onFilterClick("ALL")}
>
All
</button>
<button
class="filter-button ${this.filter === "READY" ? "active" : ""}"
@click=${() => this._onFilterClick("READY")}
>
Ready
</button>
<button
class="filter-button ${this.filter === "INJURED" ? "active" : ""}"
@click=${() => this._onFilterClick("INJURED")}
>
Injured
</button>
</div>
<div class="roster-list" style="flex: 1; overflow-y: auto;">
${filteredUnits.length === 0
? html`<div class="empty-state">
No units found matching filter.
</div>`
: filteredUnits.map((unit) => this._renderUnitCard(unit))}
</div> </div>
</div> </div>
`;
}
<!-- Detail Sidebar --> const healCost = this._calculateHealCost(unit);
${selectedUnit
? this._renderDetailContent(selectedUnit)
: html`
<div class="detail-sidebar">
<div class="empty-state">Select a unit to view details</div>
</div>
`}
</div>
`;
}
_renderDetailContent(selectedUnit) {
const healCost = this._calculateHealCost(selectedUnit);
const canHeal = const canHeal =
selectedUnit.currentHp < selectedUnit.maxHp && unit.currentHp < unit.maxHp && this.wallet.aetherShards >= healCost;
this.wallet.aetherShards >= healCost; const isInjured = unit.currentHp < unit.maxHp;
const hpPercent = (unit.currentHp / unit.maxHp) * 100;
return html` return html`
<div class="detail-sidebar"> <div class="detail-sidebar">
<div class="detail-header"> <div class="detail-header">
<div class="detail-preview"> <div class="detail-preview">
${selectedUnit.portrait ${unit.portrait
? html`<img ? html`<img
src="${selectedUnit.portrait}" src="${unit.portrait}"
alt="${selectedUnit.name}" alt="${unit.name}"
/>` style="width: 100%; height: 100%; object-fit: cover;"
: html`👤`} @error=${(e) => {
e.target.style.display = "none";
}}
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"
/>
<span
style="display: none; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 2em;"
>
${unit.classId
? unit.classId.replace("CLASS_", "")[0]
: "?"}
</span>`
: html`<span
style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 2em;"
>
${unit.classId ? unit.classId.replace("CLASS_", "")[0] : "?"}
</span>`}
</div> </div>
<div class="unit-name">${selectedUnit.name}</div> <div>
<div class="unit-class"> <h3 style="margin: 0; color: var(--color-accent-cyan);">
${selectedUnit.classId.replace("CLASS_", "")} ${unit.name}
<span class="unit-level">Lv ${selectedUnit.level}</span> </h3>
<p
style="margin: var(--spacing-xs) 0; color: var(--color-text-secondary);"
>
${unit.classId?.replace("CLASS_", "") || "Unknown"} Level
${unit.level}
</p>
</div> </div>
</div> </div>
<div class="detail-stats"> <div class="detail-stats">
<div class="stat-row"> <div class="stat-row">
<span class="stat-label">Health</span> <span class="stat-label">Health</span>
<span class="stat-value" <span class="stat-value">${unit.currentHp} / ${unit.maxHp}</span>
>${selectedUnit.currentHp}/${selectedUnit.maxHp}</span
>
</div> </div>
<div class="unit-hp-bar"> <div class="progress-bar-container">
<div class="progress-bar-container"> <div
<div class="progress-bar-fill hp"
class="progress-bar-fill ${selectedUnit.currentHp < style="width: ${hpPercent}%"
selectedUnit.maxHp ></div>
? "injured" <div class="progress-bar-label">${Math.round(hpPercent)}%</div>
: "ready"}"
style="width: ${(selectedUnit.currentHp / selectedUnit.maxHp) *
100}%"
></div>
</div>
</div> </div>
<div class="stat-row"> <div class="stat-row">
<span class="stat-label">Status</span> <span class="stat-label">Status</span>
<span <span class="stat-value">${unit.status}</span>
class="stat-value status-badge ${selectedUnit.status.toLowerCase()}"
>${selectedUnit.status}</span
>
</div> </div>
</div> </div>
<div class="detail-actions"> <div class="detail-actions">
<button class="btn" @click=${this._handleInspect}> <button class="btn action-button" @click=${this._handleInspect}>
Inspect / Equip Inspect / Equip
</button> </button>
${selectedUnit.currentHp < selectedUnit.maxHp ${isInjured
? html` ? html`
<button <button
class="btn btn-primary" class="btn btn-primary action-button"
?disabled=${!canHeal} ?disabled=${!canHeal}
@click=${this._handleHeal} @click=${this._handleHeal}
> >
Heal (${healCost} 💎) Treat Wounds (${healCost} 💎)
</button> </button>
${!canHeal && healCost > 0
? html`
<div class="heal-cost">
Insufficient funds. Need ${healCost} Shards.
</div>
`
: ""}
` `
: ""} : html`
<button class="btn action-button" disabled>Full Health</button>
`}
<button <button
class="btn btn-danger" class="btn btn-danger action-button"
@click=${this._handleDismiss} @click=${this._handleDismiss}
style="margin-top: auto;"
> >
Dismiss Dismiss
</button> </button>
@ -1096,58 +956,81 @@ export class BarracksScreen extends LitElement {
} }
render() { render() {
const isRecruit = this.viewMode === "RECRUIT"; const filteredUnits = this._getFilteredUnits();
const rosterCount = this.units.length;
const rosterLimit = gameStateManager.rosterManager.rosterLimit || 12;
return html` return html`
<div class="header"> <div class="header">
<div class="roster-info"> <div>
<h2>Squad Quarters</h2> <h2>The Squad Quarters</h2>
<div class="roster-count"> <div class="roster-info">
${gameStateManager.rosterManager.roster.length} / <span class="roster-count"
${gameStateManager.rosterManager.rosterLimit} Units >Roster: ${rosterCount}/${rosterLimit}</span
>
<div class="filter-bar">
<button
class="filter-button ${this.filter === "ALL" ? "active" : ""}"
@click=${() => this._onFilterClick("ALL")}
>
All
</button>
<button
class="filter-button ${this.filter === "READY" ? "active" : ""}"
@click=${() => this._onFilterClick("READY")}
>
Ready
</button>
<button
class="filter-button ${this.filter === "INJURED"
? "active"
: ""}"
@click=${() => this._onFilterClick("INJURED")}
>
Injured
</button>
</div>
<div class="sort-bar">
<span
style="color: var(--color-text-secondary); font-size: var(--font-size-xs);"
>
Sort:
</span>
<button
class="sort-button"
@click=${() => this._onSortClick("LEVEL_DESC")}
>
Level
</button>
<button
class="sort-button"
@click=${() => this._onSortClick("NAME_ASC")}
>
Name
</button>
<button
class="sort-button"
@click=${() => this._onSortClick("HP_ASC")}
>
HP
</button>
</div>
</div> </div>
</div> </div>
<button class="btn btn-close" @click=${this._handleClose}></button>
<div
class="wallet-display"
style="display: flex; gap: 20px; align-items: center;"
>
<div
class="wallet-item"
style="color: var(--color-accent-gold); font-weight: bold;"
>
💎 ${this.wallet.aetherShards}
</div>
<button class="btn btn-close" @click=${this._handleClose}></button>
</div>
</div> </div>
<div <div class="roster-list">
style="grid-area: tabs; display: flex; gap: 10px; border-bottom: 1px solid var(--color-border-default); padding-bottom: 10px;" ${filteredUnits.length === 0
> ? html`
<button <div class="empty-state">
class="filter-button ${!isRecruit ? "active" : ""}" <p>No units found matching the current filter.</p>
@click=${() => { </div>
this.viewMode = "ROSTER"; `
this.requestUpdate(); : filteredUnits.map((unit) => this._renderUnitCard(unit))}
}}
>
Active Roster
</button>
<button
class="filter-button ${isRecruit ? "active" : ""}"
@click=${() => {
this.viewMode = "RECRUIT";
this.requestUpdate();
}}
>
Recruit
</button>
</div> </div>
<div style="grid-area: content; height: 100%; overflow: hidden;"> ${this._renderDetailSidebar()}
${isRecruit ? this._renderRecruitView() : this._renderRosterView()}
</div>
`; `;
} }
} }

View file

@ -207,13 +207,6 @@ export class MarketplaceScreen extends LitElement {
color: var(--color-text-primary); color: var(--color-text-primary);
} }
.item-icon {
width: 80px;
height: 80px;
object-fit: contain;
margin-bottom: var(--spacing-sm);
}
.item-type { .item-type {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-text-secondary); color: var(--color-text-secondary);
@ -339,7 +332,7 @@ export class MarketplaceScreen extends LitElement {
super(); super();
this.marketManager = null; this.marketManager = null;
this.wallet = { aetherShards: 0, ancientCores: 0 }; this.wallet = { aetherShards: 0, ancientCores: 0 };
this.activeMerchant = "WEAPONS"; this.activeMerchant = "SMITH";
this.activeFilter = "ALL"; this.activeFilter = "ALL";
this.selectedItem = null; this.selectedItem = null;
this.showModal = false; this.showModal = false;
@ -373,48 +366,13 @@ export class MarketplaceScreen extends LitElement {
_getStock() { _getStock() {
if (!this.marketManager) return []; if (!this.marketManager) return [];
const stock = this.marketManager.getStockForMerchant(this.activeMerchant);
let stock = []; // Apply filter
if (this.activeMerchant === "SELL") { if (this.activeFilter === "ALL") {
stock = this.marketManager.getSellableInventory(); return stock;
} else {
stock = this.marketManager.getStockForMerchant(this.activeMerchant);
} }
return stock.filter((item) => item.type === this.activeFilter);
// Filter out purchased items and apply type filter
let filteredStock = stock.filter((item) => !item.purchased);
if (this.activeFilter !== "ALL") {
filteredStock = filteredStock.filter(
(item) => item.type === this.activeFilter
);
}
// Sort: Type -> Rarity -> Name -> Price
const rarityWeight = {
ANCIENT: 4,
RARE: 3,
UNCOMMON: 2,
COMMON: 1,
};
return filteredStock.sort((a, b) => {
// 1. Type
if (a.type !== b.type) return a.type.localeCompare(b.type);
// 2. Rarity (High to Low)
const rarityA = rarityWeight[a.rarity] || 0;
const rarityB = rarityWeight[b.rarity] || 0;
if (rarityA !== rarityB) return rarityB - rarityA;
// 3. Name
const nameA = this._getItemName(a);
const nameB = this._getItemName(b);
if (nameA !== nameB) return nameA.localeCompare(nameB);
// 4. Price
return a.price - b.price;
});
} }
_onMerchantClick(merchant) { _onMerchantClick(merchant) {
@ -444,11 +402,6 @@ export class MarketplaceScreen extends LitElement {
async _confirmBuy() { async _confirmBuy() {
if (!this.selectedItem || !this.marketManager) return; if (!this.selectedItem || !this.marketManager) return;
if (this.activeMerchant === "SELL") {
await this._confirmSell();
return;
}
const success = await this.marketManager.buyItem(this.selectedItem.id); const success = await this.marketManager.buyItem(this.selectedItem.id);
if (success) { if (success) {
this._updateWallet(); this._updateWallet();
@ -459,19 +412,6 @@ export class MarketplaceScreen extends LitElement {
} }
} }
async _confirmSell() {
if (!this.selectedItem || !this.marketManager) return;
const success = await this.marketManager.sellItem(this.selectedItem.id);
if (success) {
this._updateWallet();
this._closeModal();
this.requestUpdate();
} else {
alert("Sell failed.");
}
}
_canAfford(item) { _canAfford(item) {
return this.wallet.aetherShards >= item.price; return this.wallet.aetherShards >= item.price;
} }
@ -489,30 +429,6 @@ export class MarketplaceScreen extends LitElement {
return item.defId; return item.defId;
} }
_getItemIcon(item) {
if (this.marketManager?.itemRegistry) {
const itemDef = this.marketManager.itemRegistry.get(item.defId);
return itemDef?.icon || null;
}
return null;
}
_getItemDescription(item) {
if (this.marketManager?.itemRegistry) {
const itemDef = this.marketManager.itemRegistry.get(item.defId);
return itemDef?.description || "No description available.";
}
return "";
}
_getItemStats(item) {
if (this.marketManager?.itemRegistry) {
const itemDef = this.marketManager.itemRegistry.get(item.defId);
return itemDef?.stats || {};
}
return {};
}
_dispatchClose() { _dispatchClose() {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("market-closed", { new CustomEvent("market-closed", {
@ -525,12 +441,10 @@ export class MarketplaceScreen extends LitElement {
render() { render() {
const stock = this._getStock(); const stock = this._getStock();
const merchants = [ const merchants = [
{ id: "WEAPONS", icon: "⚔️", label: "Weapons" }, { id: "SMITH", icon: "⚔️", label: "Smith" },
{ id: "ARMOR", icon: "🛡️", label: "Armor" }, { id: "TAILOR", icon: "🧥", label: "Tailor" },
{ id: "ENGINEER", icon: "🔧", label: "Engineer" },
{ id: "ALCHEMIST", icon: "⚗️", label: "Alchemist" }, { id: "ALCHEMIST", icon: "⚗️", label: "Alchemist" },
{ id: "BAZAAR", icon: "🏺", label: "Bazaar" }, { id: "BUYBACK", icon: "♻️", label: "Buyback" },
{ id: "SELL", icon: "💰", label: "Sell Items" },
]; ];
const filters = [ const filters = [
@ -603,13 +517,6 @@ export class MarketplaceScreen extends LitElement {
${item.purchased ${item.purchased
? html`<div class="sold-overlay">SOLD</div>` ? html`<div class="sold-overlay">SOLD</div>`
: ""} : ""}
${this._getItemIcon(item)
? html`<img
src="${this._getItemIcon(item)}"
class="item-icon"
alt="${this._getItemName(item)}"
/>`
: html`<div class="item-icon">📦</div>`}
<div class="item-name">${this._getItemName(item)}</div> <div class="item-name">${this._getItemName(item)}</div>
<div class="item-type">${item.type}</div> <div class="item-type">${item.type}</div>
<div class="item-price">${item.price} 💎</div> <div class="item-price">${item.price} 💎</div>
@ -628,11 +535,7 @@ export class MarketplaceScreen extends LitElement {
> >
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title"> <h3 class="modal-title">Confirm Purchase</h3>
${this.activeMerchant === "SELL"
? "Confirm Sell"
: "Confirm Purchase"}
</h3>
<button class="modal-close" @click=${this._closeModal}> <button class="modal-close" @click=${this._closeModal}>
</button> </button>
@ -644,36 +547,10 @@ export class MarketplaceScreen extends LitElement {
</div> </div>
<div class="item-type">${this.selectedItem.type}</div> <div class="item-type">${this.selectedItem.type}</div>
<div class="item-price"> <div class="item-price">
${this.activeMerchant === "SELL" Price: ${this.selectedItem.price} 💎
? "Sell Value:"
: "Price:"}
${this.selectedItem.price} 💎
</div>
<div
class="item-description"
style="margin-top: 10px; font-style: italic; color: #aaa;"
>
${this._getItemDescription(this.selectedItem)}
</div>
<div class="item-stats" style="margin-top: 10px;">
${Object.entries(
this._getItemStats(this.selectedItem)
).map(
([stat, value]) => html`
<div
style="display: flex; justify-content: space-between; font-size: 12px; color: #ccc;"
>
<span style="text-transform: capitalize;"
>${stat}</span
>
<span style="color: #00ffff;">${value}</span>
</div>
`
)}
</div> </div>
</div> </div>
${this.activeMerchant !== "SELL" && ${!this._canAfford(this.selectedItem)
!this._canAfford(this.selectedItem)
? html`<div style="color: var(--color-accent-red);"> ? html`<div style="color: var(--color-accent-red);">
Insufficient funds! Insufficient funds!
</div>` </div>`
@ -682,13 +559,10 @@ export class MarketplaceScreen extends LitElement {
<div class="modal-actions"> <div class="modal-actions">
<button <button
class="btn btn-primary" class="btn btn-primary"
?disabled=${this.activeMerchant !== "SELL" && ?disabled=${!this._canAfford(this.selectedItem)}
!this._canAfford(this.selectedItem)}
@click=${this._confirmBuy} @click=${this._confirmBuy}
> >
${this.activeMerchant === "SELL" Buy for ${this.selectedItem.price} 💎
? `Sell for ${this.selectedItem.price} 💎`
: `Buy for ${this.selectedItem.price} 💎`}
</button> </button>
<button class="btn" @click=${this._closeModal}>Cancel</button> <button class="btn" @click=${this._closeModal}>Cancel</button>
</div> </div>

View file

@ -29,7 +29,6 @@ describe("Core: GameStateManager (Singleton)", () => {
saveHubStash: sinon.stub().resolves(), saveHubStash: sinon.stub().resolves(),
loadUnlocks: sinon.stub().resolves([]), loadUnlocks: sinon.stub().resolves([]),
saveUnlocks: sinon.stub().resolves(), saveUnlocks: sinon.stub().resolves(),
clearRun: sinon.stub().resolves(),
}; };
// Inject Mock (replacing the real Persistence instance) // Inject Mock (replacing the real Persistence instance)
gameStateManager.persistence = mockPersistence; gameStateManager.persistence = mockPersistence;
@ -128,9 +127,7 @@ describe("Core: GameStateManager (Singleton)", () => {
className: "Vanguard", // Class name className: "Vanguard", // Class name
classId: "CLASS_VANGUARD", classId: "CLASS_VANGUARD",
}; };
gameStateManager.rosterManager.recruitUnit = sinon gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit);
.stub()
.resolves(mockRecruitedUnit);
gameStateManager.setGameLoop(mockGameLoop); gameStateManager.setGameLoop(mockGameLoop);
await gameStateManager.init(); await gameStateManager.init();
@ -140,9 +137,7 @@ describe("Core: GameStateManager (Singleton)", () => {
mockGameLoop.startLevel = sinon.stub().resolves(); mockGameLoop.startLevel = sinon.stub().resolves();
// Await the full async chain // Await the full async chain
await gameStateManager.handleEmbark({ await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "SELECT" } });
detail: { squad: mockSquad, mode: "SELECT" },
});
expect(gameStateManager.currentState).to.equal( expect(gameStateManager.currentState).to.equal(
GameStateManager.STATES.DEPLOYMENT GameStateManager.STATES.DEPLOYMENT
@ -161,15 +156,11 @@ describe("Core: GameStateManager (Singleton)", () => {
className: "Vanguard", className: "Vanguard",
classId: "CLASS_VANGUARD", classId: "CLASS_VANGUARD",
}; };
gameStateManager.rosterManager.recruitUnit = sinon gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit);
.stub()
.resolves(mockRecruitedUnit);
gameStateManager.setGameLoop(mockGameLoop); gameStateManager.setGameLoop(mockGameLoop);
await gameStateManager.init(); await gameStateManager.init();
const mockSquad = [ const mockSquad = [{ id: "u1", isNew: true, name: "Vanguard", classId: "CLASS_VANGUARD" }];
{ id: "u1", isNew: true, name: "Vanguard", classId: "CLASS_VANGUARD" },
];
mockGameLoop.startLevel = sinon.stub().resolves(); mockGameLoop.startLevel = sinon.stub().resolves();
let eventDispatched = false; let eventDispatched = false;
@ -179,9 +170,7 @@ describe("Core: GameStateManager (Singleton)", () => {
eventData = e.detail.runData; eventData = e.detail.runData;
}); });
await gameStateManager.handleEmbark({ await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "DRAFT" } });
detail: { squad: mockSquad, mode: "DRAFT" },
});
expect(eventDispatched).to.be.true; expect(eventDispatched).to.be.true;
expect(eventData).to.exist; expect(eventData).to.exist;
@ -213,8 +202,7 @@ describe("Core: GameStateManager (Singleton)", () => {
await gameStateManager.init(); await gameStateManager.init();
expect(mockPersistence.loadCampaign.called).to.be.true; expect(mockPersistence.loadCampaign.called).to.be.true;
expect(gameStateManager.missionManager.load.calledWith(savedCampaignData)) expect(gameStateManager.missionManager.load.calledWith(savedCampaignData)).to.be.true;
.to.be.true;
}); });
it("CoA 6: init should handle missing campaign data gracefully", async () => { it("CoA 6: init should handle missing campaign data gracefully", async () => {
@ -233,11 +221,9 @@ describe("Core: GameStateManager (Singleton)", () => {
mockPersistence.saveCampaign.resetHistory(); mockPersistence.saveCampaign.resetHistory();
// Dispatch campaign-data-changed event // Dispatch campaign-data-changed event
window.dispatchEvent( window.dispatchEvent(new CustomEvent("campaign-data-changed", {
new CustomEvent("campaign-data-changed", { detail: { missionCompleted: "MISSION_TUTORIAL_01" }
detail: { missionCompleted: "MISSION_TUTORIAL_01" }, }));
})
);
// Wait for async save (event listener is synchronous but save is async) // Wait for async save (event listener is synchronous but save is async)
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
@ -247,50 +233,4 @@ describe("Core: GameStateManager (Singleton)", () => {
expect(savedData).to.exist; expect(savedData).to.exist;
expect(savedData.completedMissions).to.be.an("array"); expect(savedData.completedMissions).to.be.an("array");
}); });
it("CoA 8: mission-sequence-complete should clear run but NOT transition to MAIN_MENU automatically", async () => {
// Setup
await gameStateManager.init();
gameStateManager.activeRunData = { id: "RUN_123" };
// Spy on transitionTo
const transitionSpy = sinon.spy(gameStateManager, "transitionTo");
// Dispatch event
window.dispatchEvent(
new CustomEvent("mission-sequence-complete", {
detail: { missionId: "MISSION_TEST" },
})
);
// Wait for async listeners
await new Promise((resolve) => setTimeout(resolve, 50));
// Assert: Run should be cleared
expect(mockPersistence.clearRun.called).to.be.true;
expect(gameStateManager.activeRunData).to.be.null;
// Assert: Should NOT have transitioned to MAIN_MENU (GameLoop handles this after debrief)
const callsToMainMenu = transitionSpy
.getCalls()
.filter((call) => call.args[0] === GameStateManager.STATES.MAIN_MENU);
// Initial init() calls it once, so we check if it was called AGAIN
// Actually, checking "called" might be tricky if init() called it.
// Let's check call count. init() makes 1 call.
// If mission-sequence-complete triggered it, we'd see 2.
// But better yet, let's reset history before dispatch.
transitionSpy.resetHistory();
// Re-dispatch to be sure we are testing the listener
window.dispatchEvent(
new CustomEvent("mission-sequence-complete", {
detail: { missionId: "MISSION_TEST" },
})
);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be
.false;
});
}); });

View file

@ -1,134 +0,0 @@
import os
path = "src/generation/CrystalSpiresGenerator.js"
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
start_idx = -1
for i, line in enumerate(lines):
if "ensureGlobalConnectivity(spires) {" in line:
start_idx = i
break
if start_idx == -1:
print("Error: ensureGlobalConnectivity not found")
exit(1)
# Find end of method. It's likely the second to last closing brace, or indented " }".
# The previous `view_file` showed it ending at line 1212/1213.
# We will scan for " }" starting from start_idx.
end_idx = -1
for i in range(start_idx + 1, len(lines)):
if lines[i].rstrip() == " }":
end_idx = i
break
if end_idx == -1:
print("Error: Closing brace not found")
exit(1)
# New Content
new_code = """ ensureGlobalConnectivity(spires) {
if (!spires || spires.length === 0) return;
// 1. Build Adjacency Graph
// Map platform ID "sIdx:pIdx" -> {id, p, neighbors: Set<id>}
const adj = new Map();
const platById = new Map();
const getPlatId = (sIdx, pIdx) => `${sIdx}:${pIdx}`;
spires.forEach((s, sIdx) => {
s.platforms.forEach((p, pIdx) => {
const id = getPlatId(sIdx, pIdx);
adj.set(id, {id, p, neighbors: new Set()});
platById.set(id, p);
});
});
// Populate Neighbors from Bridges
this.generatedAssets.bridges.forEach(b => {
if (b.fromPlatIdx !== undefined && b.toPlatIdx !== undefined) {
const idA = getPlatId(b.fromSpire, b.fromPlatIdx);
const idB = getPlatId(b.toSpire, b.toPlatIdx);
if(adj.has(idA) && adj.has(idB)) {
adj.get(idA).neighbors.add(idB);
adj.get(idB).neighbors.add(idA);
}
}
});
// 2. Find Connected Components (BFS)
const visited = new Set();
const components = [];
for(const [id, node] of adj) {
if(!visited.has(id)) {
const comp = [];
const q = [id];
visited.add(id);
while(q.length > 0) {
const curr = q.shift();
comp.push(curr);
adj.get(curr).neighbors.forEach(nId => {
if(!visited.has(nId)) {
visited.add(nId);
q.push(nId);
}
});
}
components.push(comp);
}
}
// 3. Link Components
if (components.length > 1) {
// Identify Main Component (Largest)
components.sort((a,b) => b.length - a.length);
const targetComp = components[0];
const targetNodeId = targetComp[0];
const targetPlat = platById.get(targetNodeId);
for(let i=1; i<components.length; i++) {
const orphanComp = components[i];
const orphanNodeId = orphanComp[0];
const orphanPlat = platById.get(orphanNodeId);
this.placeTeleporter(targetPlat, orphanPlat);
}
}
}
placeTeleporter(pA, pB) {
const findSpot = (p) => {
const r = p.radius - 1;
for(let x=Math.round(p.x-r); x<=Math.round(p.x+r); x++) {
for(let z=Math.round(p.z-r); z<=Math.round(p.z+r); z++) {
const y = Math.round(p.y);
const floor = this.grid.getCell(x, y, z);
const air = this.grid.getCell(x, y+1, z);
if (floor !== 0 && air === 0) {
return {x, y: y+1, z};
}
}
}
return null;
};
const sA = findSpot(pA);
const sB = findSpot(pB);
if(sA && sB) {
this.grid.setCell(sA.x, sA.y, sA.z, 22);
this.grid.setCell(sB.x, sB.y, sB.z, 22);
}
}
"""
# Replace
final_lines = lines[:start_idx] + [new_code] + lines[end_idx+1:]
with open(path, "w", encoding="utf-8") as f:
f.writelines(final_lines)
print("Update Complete")

View file

@ -1,139 +0,0 @@
import os
path = "src/generation/CrystalSpiresGenerator.js"
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
start_idx = -1
for i, line in enumerate(lines):
if "ensureGlobalConnectivity(spires) {" in line:
start_idx = i
break
if start_idx == -1:
print("Error: ensureGlobalConnectivity not found")
exit(1)
end_idx = -1
for i in range(start_idx + 1, len(lines)):
if lines[i].rstrip() == " }":
end_idx = i
break
if end_idx == -1:
print("Error: Closing brace not found")
exit(1)
# Debug Version
new_code = """ ensureGlobalConnectivity(spires) {
// console.log("Ensure Connectivity running...");
if (!spires || spires.length === 0) return;
const adj = new Map();
const platById = new Map();
const getPlatId = (sIdx, pIdx) => `${sIdx}:${pIdx}`;
let nodeCount = 0;
spires.forEach((s, sIdx) => {
s.platforms.forEach((p, pIdx) => {
const id = getPlatId(sIdx, pIdx);
adj.set(id, {id, p, neighbors: new Set()});
platById.set(id, p);
nodeCount++;
});
});
// console.log(`Adjacency Graph Nodes: ${nodeCount}`);
let edgeCount = 0;
this.generatedAssets.bridges.forEach(b => {
if (b.fromPlatIdx !== undefined && b.toPlatIdx !== undefined) {
const idA = getPlatId(b.fromSpire, b.fromPlatIdx);
const idB = getPlatId(b.toSpire, b.toPlatIdx);
if(adj.has(idA) && adj.has(idB)) {
adj.get(idA).neighbors.add(idB);
adj.get(idB).neighbors.add(idA);
edgeCount++;
}
} else {
// console.log("Bridge missing platform indices:", b);
}
});
// console.log(`Adjacency Graph Edges: ${edgeCount}`);
const visited = new Set();
const components = [];
for(const [id, node] of adj) {
if(!visited.has(id)) {
const comp = [];
const q = [id];
visited.add(id);
while(q.length > 0) {
const curr = q.shift();
comp.push(curr);
adj.get(curr).neighbors.forEach(nId => {
if(!visited.has(nId)) {
visited.add(nId);
q.push(nId);
}
});
}
components.push(comp);
}
}
// console.log(`Connected Components: ${components.length}`);
if (components.length > 1) {
components.sort((a,b) => b.length - a.length);
const targetComp = components[0];
const targetNodeId = targetComp[0];
const targetPlat = platById.get(targetNodeId);
for(let i=1; i<components.length; i++) {
const orphanComp = components[i];
const orphanNodeId = orphanComp[0];
const orphanPlat = platById.get(orphanNodeId);
// console.log(`Linking Component ${i} (Size ${orphanComp.length}) to Main`);
this.placeTeleporter(targetPlat, orphanPlat);
}
}
}
placeTeleporter(pA, pB) {
const findSpot = (p) => {
const r = p.radius - 1;
for(let x=Math.round(p.x-r); x<=Math.round(p.x+r); x++) {
for(let z=Math.round(p.z-r); z<=Math.round(p.z+r); z++) {
const y = Math.round(p.y);
const floor = this.grid.getCell(x, y, z);
const air = this.grid.getCell(x, y+1, z);
if (floor !== 0 && air === 0) {
return {x, y: y+1, z};
}
}
}
return null;
};
const sA = findSpot(pA);
const sB = findSpot(pB);
if(sA && sB) {
this.grid.setCell(sA.x, sA.y, sA.z, 22);
this.grid.setCell(sB.x, sB.y, sB.z, 22);
// console.log(`Placed Teleporter between (${sA.x},${sA.y},${sA.z}) and (${sB.x},${sB.y},${sB.z})`);
} else {
// console.log("Failed to find spot for Teleporter");
}
}
"""
final_lines = lines[:start_idx] + [new_code] + lines[end_idx+1:]
with open(path, "w", encoding="utf-8") as f:
f.writelines(final_lines)
print("Debug Update Complete")