Compare commits
7 commits
b0ef4f30a9
...
051c47ef07
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
051c47ef07 | ||
|
|
bf40a0f788 | ||
|
|
234ce4b5f3 | ||
|
|
dbfa9929dd | ||
|
|
b363d0850a | ||
|
|
5c5a030bbb | ||
|
|
25c9d47587 |
|
|
@ -11,8 +11,11 @@
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
|
|
|
||||||
145
cleanup_and_debug.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
|
||||||
|
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")
|
||||||
903
custom-elements.json
Normal file
|
|
@ -0,0 +1,903 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
575
debug_spires.js
Normal file
|
|
@ -0,0 +1,575 @@
|
||||||
|
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.`);
|
||||||
87
instrument_vertical.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
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
|
|
@ -13,6 +13,7 @@
|
||||||
"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",
|
||||||
|
|
@ -46,6 +47,169 @@
|
||||||
"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",
|
||||||
|
|
@ -498,6 +662,13 @@
|
||||||
"@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",
|
||||||
|
|
@ -548,6 +719,23 @@
|
||||||
"@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",
|
||||||
|
|
@ -586,6 +774,289 @@
|
||||||
"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",
|
||||||
|
|
@ -1025,6 +1496,17 @@
|
||||||
"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",
|
||||||
|
|
@ -1575,6 +2057,159 @@
|
||||||
"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",
|
||||||
|
|
@ -1641,6 +2276,20 @@
|
||||||
"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",
|
||||||
|
|
@ -1820,6 +2469,19 @@
|
||||||
"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",
|
||||||
|
|
@ -2159,6 +2821,16 @@
|
||||||
"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",
|
||||||
|
|
@ -2245,6 +2917,13 @@
|
||||||
"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",
|
||||||
|
|
@ -3236,6 +3915,19 @@
|
||||||
"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",
|
||||||
|
|
@ -3883,6 +4575,16 @@
|
||||||
"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",
|
||||||
|
|
@ -3972,6 +4674,38 @@
|
||||||
"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",
|
||||||
|
|
@ -4563,6 +5297,28 @@
|
||||||
"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",
|
||||||
|
|
@ -5116,6 +5872,20 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
10
package.json
|
|
@ -6,11 +6,11 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"start": "web-dev-server --node-resolve --watch --root-dir --port 8000 dist",
|
"cem": "cem analyze --litelement --globs src/**/*.js",
|
||||||
|
"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",
|
||||||
"test:watch": "web-test-runner \"test/**/*.test.js\" --node-resolve --watch --config web-test-runner.config.js",
|
"visualizer": "web-dev-server --node-resolve --port 8001 --root-dir src/tools"
|
||||||
"visualizer": "web-dev-server --node-resolve --watch --port 8001 src/tools/map-visualizer.html"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"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",
|
||||||
|
|
@ -30,5 +31,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lit": "^3.3.1",
|
"lit": "^3.3.1",
|
||||||
"three": "^0.182.0"
|
"three": "^0.182.0"
|
||||||
}
|
},
|
||||||
|
"customElements": "custom-elements.json"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
218
refactor_gen.py
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
|
||||||
|
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")
|
||||||
57
specs/BridgeCreation.spec.md
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# **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
|
||||||
BIN
src/assets/icons/items/item_advanced_toolkit.png
Normal file
|
After Width: | Height: | Size: 903 KiB |
BIN
src/assets/icons/items/item_amulet_vitality.png
Normal file
|
After Width: | Height: | Size: 771 KiB |
BIN
src/assets/icons/items/item_apprentice_wand.png
Normal file
|
After Width: | Height: | Size: 446 KiB |
BIN
src/assets/icons/items/item_crystal_staff.png
Normal file
|
After Width: | Height: | Size: 707 KiB |
BIN
src/assets/icons/items/item_dagger.png
Normal file
|
After Width: | Height: | Size: 361 KiB |
BIN
src/assets/icons/items/item_leaf_robes.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
src/assets/icons/items/item_leather_apron.png
Normal file
|
After Width: | Height: | Size: 508 KiB |
BIN
src/assets/icons/items/item_lockpick_set.png
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
src/assets/icons/items/item_padded_vest.png
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
src/assets/icons/items/item_reinforced_plate.png
Normal file
|
After Width: | Height: | Size: 742 KiB |
BIN
src/assets/icons/items/item_robes.png
Normal file
|
After Width: | Height: | Size: 660 KiB |
BIN
src/assets/icons/items/item_rusty_blade.png
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
src/assets/icons/items/item_scrap_plate.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
src/assets/icons/items/item_serrated_dagger.png
Normal file
|
After Width: | Height: | Size: 697 KiB |
BIN
src/assets/icons/items/item_silk_weave_robes.png
Normal file
|
After Width: | Height: | Size: 751 KiB |
BIN
src/assets/icons/items/item_staff.png
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
src/assets/icons/items/item_steel_longsword.png
Normal file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/assets/icons/items/item_turret_kit.png
Normal file
|
After Width: | Height: | Size: 817 KiB |
BIN
src/assets/icons/items/item_wrench.png
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
src/assets/icons/mission_bomb.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
src/assets/icons/mission_boss_final.png
Normal file
|
After Width: | Height: | Size: 627 KiB |
BIN
src/assets/icons/mission_boss_gate.png
Normal file
|
After Width: | Height: | Size: 580 KiB |
BIN
src/assets/icons/mission_boss_plant.png
Normal file
|
After Width: | Height: | Size: 714 KiB |
BIN
src/assets/icons/mission_climb.png
Normal file
|
After Width: | Height: | Size: 699 KiB |
BIN
src/assets/icons/mission_coin.png
Normal file
|
After Width: | Height: | Size: 678 KiB |
BIN
src/assets/icons/mission_core.png
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
src/assets/icons/mission_diplomat.png
Normal file
|
After Width: | Height: | Size: 596 KiB |
BIN
src/assets/icons/mission_elevator.png
Normal file
|
After Width: | Height: | Size: 597 KiB |
BIN
src/assets/icons/mission_escort.png
Normal file
|
After Width: | Height: | Size: 587 KiB |
BIN
src/assets/icons/mission_flag.png
Normal file
|
After Width: | Height: | Size: 568 KiB |
BIN
src/assets/icons/mission_leaf.png
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
src/assets/icons/mission_run.png
Normal file
|
After Width: | Height: | Size: 576 KiB |
BIN
src/assets/icons/mission_search.png
Normal file
|
After Width: | Height: | Size: 578 KiB |
BIN
src/assets/icons/mission_shadow.png
Normal file
|
After Width: | Height: | Size: 557 KiB |
BIN
src/assets/icons/mission_shield.png
Normal file
|
After Width: | Height: | Size: 725 KiB |
BIN
src/assets/icons/mission_skull.png
Normal file
|
After Width: | Height: | Size: 648 KiB |
BIN
src/assets/icons/mission_skull_poison.png
Normal file
|
After Width: | Height: | Size: 763 KiB |
BIN
src/assets/icons/mission_target.png
Normal file
|
After Width: | Height: | Size: 661 KiB |
BIN
src/assets/icons/mission_vip.png
Normal file
|
After Width: | Height: | Size: 594 KiB |
|
|
@ -1131,6 +1131,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -1171,7 +1172,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, 10, 20);
|
this.grid = new VoxelGrid(20, 20, 20);
|
||||||
|
|
||||||
let generator;
|
let generator;
|
||||||
const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES";
|
const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES";
|
||||||
|
|
|
||||||
|
|
@ -559,11 +559,10 @@ class GameStateManagerClass {
|
||||||
// Clear the active run (persistence and memory)
|
// Clear the active run (persistence and memory)
|
||||||
await this.clearActiveRun();
|
await this.clearActiveRun();
|
||||||
|
|
||||||
// Transition to Main Menu (will show Hub if unlocking conditions met)
|
// NOTE: We do NOT transition to Main Menu automatically here.
|
||||||
await this.transitionTo(GameStateManagerClass.STATES.MAIN_MENU);
|
// The GameLoop (UI layer) is responsible for showing the Mission Debrief
|
||||||
|
// and then requesting title/hub transition when the user is done.
|
||||||
// Force a refresh of the Hub screen if it was already open/cached?
|
// If we transitioned here, we would rip the UI away before the user saw the results.
|
||||||
// The state transition should handle visibility, but we check specific UI updates if needed.
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,23 +69,36 @@ 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 is always wall
|
// Border Logic
|
||||||
if (
|
const isSideBorder = x === 0 || x === this.width - 1;
|
||||||
x === 0 ||
|
const isBackBorder = z === this.depth - 1;
|
||||||
x === this.width - 1 ||
|
const isFrontBorder = z === 0;
|
||||||
z === 0 ||
|
|
||||||
z === this.depth - 1
|
let isSolid = false;
|
||||||
) {
|
|
||||||
map[x][z] = 1;
|
if (isSideBorder) {
|
||||||
|
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 {
|
||||||
map[x][z] = this.rng.chance(fillPercent) ? 1 : 0;
|
// Internal
|
||||||
|
isSolid = this.rng.chance(fillPercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map[x][z] = isSolid ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,22 +108,23 @@ 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++) {
|
||||||
const isWall = map[x][z] === 1;
|
if (map[x][z] === 1) {
|
||||||
|
|
||||||
if (isWall) {
|
|
||||||
// Wall Logic
|
// Wall Logic
|
||||||
|
// Cap all walls to height 6 for visibility (cutoff view)
|
||||||
|
const MAX_WALL_HEIGHT = 6;
|
||||||
|
|
||||||
// Vary height slightly for "voxel aesthetic"
|
// Vary height slightly for "voxel aesthetic"
|
||||||
// height-1 or height
|
// height-1 or height, but capped at MAX_WALL_HEIGHT
|
||||||
const wallHeight = this.height - (this.rng.next() > 0.5 ? 1 : 0);
|
const baseHeight = Math.min(this.height, MAX_WALL_HEIGHT);
|
||||||
|
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 {
|
||||||
|
|
@ -285,16 +299,23 @@ 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++) {
|
||||||
// Borders stay walls
|
// Border Logic (Must match generate loop to avoid closing entrances)
|
||||||
if (
|
const isSideBorder = x === 0 || x === this.width - 1;
|
||||||
x === 0 ||
|
const isFrontBorder = z === 0;
|
||||||
x === this.width - 1 ||
|
const isBackBorder = z === this.depth - 1;
|
||||||
z === 0 ||
|
|
||||||
z === this.depth - 1
|
if (isSideBorder) {
|
||||||
) {
|
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,16 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ 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 || {};
|
||||||
|
|
|
||||||
103
src/items/tier2_gear.json
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -35,9 +35,14 @@ export class ItemRegistry {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async _doLoadAll() {
|
async _doLoadAll() {
|
||||||
// Lazy-load tier1_gear.json
|
// Lazy-load item definitions
|
||||||
const tier1Gear = await import("../items/tier1_gear.json", { with: { type: "json" } }).then(m => m.default);
|
const tier1Gear = await import("../items/tier1_gear.json", {
|
||||||
|
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) {
|
||||||
if (itemDef && itemDef.id) {
|
if (itemDef && itemDef.id) {
|
||||||
|
|
@ -46,6 +51,14 @@ 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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,4 +82,3 @@ export class ItemRegistry {
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const itemRegistry = new ItemRegistry();
|
export const itemRegistry = new ItemRegistry();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -130,20 +129,26 @@ export class MarketManager {
|
||||||
const newStock = [];
|
const newStock = [];
|
||||||
|
|
||||||
if (tier === 1) {
|
if (tier === 1) {
|
||||||
// Tier 1: Smith (5 Common Weapons, 3 Common Armor)
|
// Tier 1: Weapons, Armor, and basic Utility
|
||||||
const smithWeapons = this._generateMerchantStock(
|
const weapons = 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 smithArmor = this._generateMerchantStock(
|
const armor = this._generateMerchantStock(
|
||||||
allItems,
|
allItems,
|
||||||
["ARMOR"],
|
["ARMOR"],
|
||||||
{ COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 },
|
{ COMMON: 1, UNCOMMON: 0, RARE: 0, ANCIENT: 0 },
|
||||||
3
|
3
|
||||||
);
|
);
|
||||||
newStock.push(...smithWeapons, ...smithArmor);
|
const utility = this._generateMerchantStock(
|
||||||
|
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
|
||||||
|
|
@ -182,8 +187,7 @@ 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 variance = 1 + (Math.random() * 0.2 - 0.1); // ±10% variance
|
const price = basePrice;
|
||||||
const price = Math.floor(basePrice * variance);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `STOCK_${Date.now()}_${index}`,
|
id: `STOCK_${Date.now()}_${index}`,
|
||||||
|
|
@ -295,10 +299,9 @@ export class MarketManager {
|
||||||
*/
|
*/
|
||||||
async buyItem(stockId) {
|
async buyItem(stockId) {
|
||||||
// Check both stock and buyback
|
// Check both stock and buyback
|
||||||
let marketItem = this.marketState.stock.find((item) => item.id === stockId);
|
const marketItem = this.marketState.stock.find(
|
||||||
if (!marketItem) {
|
(item) => item.id === stockId
|
||||||
marketItem = this.marketState.buyback.find((item) => item.id === stockId);
|
);
|
||||||
}
|
|
||||||
if (!marketItem || marketItem.purchased) {
|
if (!marketItem || marketItem.purchased) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -402,23 +405,21 @@ export class MarketManager {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 6. Create buyback entry (limit 10)
|
// 6. Add to Market Stock (at full price)
|
||||||
if (this.marketState.buyback.length >= 10) {
|
const marketItem = {
|
||||||
this.marketState.buyback.shift(); // Remove oldest
|
id: `STOCK_${Date.now()}_SOLD_${Math.random()
|
||||||
}
|
.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: sellPrice, // Buyback price = sell price
|
price: basePrice, // Resell at full market value
|
||||||
discount: 0,
|
discount: 0,
|
||||||
purchased: false,
|
purchased: false,
|
||||||
instanceData: { ...itemInstance }, // Store copy of original instance
|
instanceData: { ...itemInstance }, // Store copy of original instance
|
||||||
};
|
};
|
||||||
|
|
||||||
this.marketState.buyback.push(buybackItem);
|
this.marketState.stock.push(marketItem);
|
||||||
|
|
||||||
// 7. Save market state
|
// 7. Save market state
|
||||||
await this.persistence.saveMarketState(this.marketState);
|
await this.persistence.saveMarketState(this.marketState);
|
||||||
|
|
@ -445,15 +446,16 @@ export class MarketManager {
|
||||||
*/
|
*/
|
||||||
getStockForMerchant(merchantType) {
|
getStockForMerchant(merchantType) {
|
||||||
if (merchantType === "BUYBACK") {
|
if (merchantType === "BUYBACK") {
|
||||||
return this.marketState.buyback;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter stock by merchant type
|
// Filter stock by merchant type
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
SMITH: ["WEAPON", "ARMOR"],
|
WEAPONS: ["WEAPON"],
|
||||||
TAILOR: ["ARMOR"],
|
ARMOR: ["ARMOR"],
|
||||||
ALCHEMIST: ["CONSUMABLE", "UTILITY"],
|
ENGINEER: ["UTILITY"],
|
||||||
SCAVENGER: ["RELIC", "UTILITY"],
|
ALCHEMIST: ["CONSUMABLE"],
|
||||||
|
BAZAAR: ["RELIC"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const allowedTypes = typeMap[merchantType] || [];
|
const allowedTypes = typeMap[merchantType] || [];
|
||||||
|
|
@ -462,6 +464,39 @@ 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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +29,20 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -37,6 +53,7 @@ export class RosterManager {
|
||||||
return {
|
return {
|
||||||
roster: this.roster,
|
roster: this.roster,
|
||||||
graveyard: this.graveyard,
|
graveyard: this.graveyard,
|
||||||
|
candidates: this.candidates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +115,80 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,11 @@
|
||||||
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">
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ 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
|
||||||
|
|
@ -75,6 +76,9 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +91,8 @@ 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) {
|
||||||
|
|
@ -96,6 +102,7 @@ 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);
|
||||||
|
|
@ -114,10 +121,10 @@ class MapVisualizer {
|
||||||
this.updateURL(type, seed, fill, iter);
|
this.updateURL(type, seed, fill, iter);
|
||||||
|
|
||||||
// 1. Setup Grid
|
// 1. Setup Grid
|
||||||
// Standard size for testing (matches test cases roughly)
|
// Standard size (matches GameLoop default)
|
||||||
const width = 30;
|
const width = 20;
|
||||||
const height = 40; // Increased verticality
|
const height = 20;
|
||||||
const depth = 30;
|
const depth = 20;
|
||||||
const grid = new VoxelGrid(width, height, depth);
|
const grid = new VoxelGrid(width, height, depth);
|
||||||
|
|
||||||
// 2. Run Generator
|
// 2. Run Generator
|
||||||
|
|
@ -139,9 +146,8 @@ 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);
|
||||||
|
|
@ -161,6 +167,20 @@ 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) {
|
||||||
|
|
@ -211,7 +231,20 @@ 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
|
||||||
// Blue structure
|
const materialStructure = new THREE.MeshStandardMaterial({
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -248,7 +281,9 @@ 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 >= 200 && id < 300) mat = materialFloor;
|
if (id === 20) mat = materialBridge;
|
||||||
|
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
|
||||||
|
|
@ -285,6 +320,47 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -433,8 +433,8 @@ export class CharacterSheet extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-slot {
|
.equipment-slot {
|
||||||
width: clamp(50px, 7cqw, 70px);
|
width: 100%;
|
||||||
height: clamp(50px, 7cqw, 70px);
|
height: 100%;
|
||||||
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,6 +443,7 @@ 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 {
|
||||||
|
|
@ -508,7 +509,6 @@ 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,6 +591,7 @@ 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 {
|
||||||
|
|
@ -630,7 +631,6 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,12 @@ 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 1fr;
|
grid-template-rows: auto auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"header header"
|
"header header"
|
||||||
"roster detail";
|
"tabs tabs"
|
||||||
gap: var(--spacing-lg);
|
"content content";
|
||||||
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
@ -118,9 +119,8 @@ export class BarracksScreen extends LitElement {
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Roster List */
|
|
||||||
.roster-list {
|
.roster-list {
|
||||||
grid-area: roster;
|
/* grid-area: roster; - Removed to allow nesting */
|
||||||
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;
|
/* grid-area: detail; - Removed to allow nesting */
|
||||||
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,7 +339,9 @@ 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"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,12 +353,23 @@ 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() {
|
||||||
|
|
@ -455,7 +468,9 @@ 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.toLowerCase()}.png`;
|
portrait = `assets/images/portraits/${activeClassId
|
||||||
|
.replace("CLASS_", "")
|
||||||
|
.toLowerCase()}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -688,7 +703,9 @@ 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.toLowerCase()}.png`;
|
explorer.portrait = `assets/images/portraits/${activeClassId
|
||||||
|
.replace("CLASS_", "")
|
||||||
|
.toLowerCase()}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate skill tree if gameLoop isn't running
|
// Generate skill tree if gameLoop isn't running
|
||||||
|
|
@ -844,109 +861,232 @@ export class BarracksScreen extends LitElement {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderDetailSidebar() {
|
_renderRecruitView() {
|
||||||
const unit = this._getSelectedUnit();
|
const candidates = gameStateManager.rosterManager.candidates || [];
|
||||||
if (!unit) {
|
|
||||||
return html`
|
if (candidates.length === 0) {
|
||||||
<div class="detail-sidebar">
|
return html`<div class="empty-state">
|
||||||
<div class="empty-state">
|
No recruits available at this time.
|
||||||
<p>Select a unit to view details</p>
|
</div>`;
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const healCost = this._calculateHealCost(unit);
|
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>
|
||||||
|
|
||||||
|
<!-- Detail Sidebar -->
|
||||||
|
${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 =
|
||||||
unit.currentHp < unit.maxHp && this.wallet.aetherShards >= healCost;
|
selectedUnit.currentHp < selectedUnit.maxHp &&
|
||||||
const isInjured = unit.currentHp < unit.maxHp;
|
this.wallet.aetherShards >= healCost;
|
||||||
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">
|
||||||
${unit.portrait
|
${selectedUnit.portrait
|
||||||
? html`<img
|
? html`<img
|
||||||
src="${unit.portrait}"
|
src="${selectedUnit.portrait}"
|
||||||
alt="${unit.name}"
|
alt="${selectedUnit.name}"
|
||||||
style="width: 100%; height: 100%; object-fit: cover;"
|
/>`
|
||||||
@error=${(e) => {
|
: html`👤`}
|
||||||
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>
|
<div class="unit-name">${selectedUnit.name}</div>
|
||||||
<h3 style="margin: 0; color: var(--color-accent-cyan);">
|
<div class="unit-class">
|
||||||
${unit.name}
|
${selectedUnit.classId.replace("CLASS_", "")}
|
||||||
</h3>
|
<span class="unit-level">Lv ${selectedUnit.level}</span>
|
||||||
<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">${unit.currentHp} / ${unit.maxHp}</span>
|
<span class="stat-value"
|
||||||
|
>${selectedUnit.currentHp}/${selectedUnit.maxHp}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar-container">
|
<div class="unit-hp-bar">
|
||||||
<div
|
<div class="progress-bar-container">
|
||||||
class="progress-bar-fill hp"
|
<div
|
||||||
style="width: ${hpPercent}%"
|
class="progress-bar-fill ${selectedUnit.currentHp <
|
||||||
></div>
|
selectedUnit.maxHp
|
||||||
<div class="progress-bar-label">${Math.round(hpPercent)}%</div>
|
? "injured"
|
||||||
|
: "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 class="stat-value">${unit.status}</span>
|
<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 action-button" @click=${this._handleInspect}>
|
<button class="btn" @click=${this._handleInspect}>
|
||||||
Inspect / Equip
|
Inspect / Equip
|
||||||
</button>
|
</button>
|
||||||
${isInjured
|
${selectedUnit.currentHp < selectedUnit.maxHp
|
||||||
? html`
|
? html`
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary action-button"
|
class="btn btn-primary"
|
||||||
?disabled=${!canHeal}
|
?disabled=${!canHeal}
|
||||||
@click=${this._handleHeal}
|
@click=${this._handleHeal}
|
||||||
>
|
>
|
||||||
Treat Wounds (${healCost} 💎)
|
Heal (${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 action-button"
|
class="btn btn-danger"
|
||||||
@click=${this._handleDismiss}
|
@click=${this._handleDismiss}
|
||||||
|
style="margin-top: auto;"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -956,81 +1096,58 @@ export class BarracksScreen extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const filteredUnits = this._getFilteredUnits();
|
const isRecruit = this.viewMode === "RECRUIT";
|
||||||
const rosterCount = this.units.length;
|
|
||||||
const rosterLimit = gameStateManager.rosterManager.rosterLimit || 12;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div>
|
<div class="roster-info">
|
||||||
<h2>The Squad Quarters</h2>
|
<h2>Squad Quarters</h2>
|
||||||
<div class="roster-info">
|
<div class="roster-count">
|
||||||
<span class="roster-count"
|
${gameStateManager.rosterManager.roster.length} /
|
||||||
>Roster: ${rosterCount}/${rosterLimit}</span
|
${gameStateManager.rosterManager.rosterLimit} Units
|
||||||
>
|
|
||||||
<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 class="roster-list">
|
<div
|
||||||
${filteredUnits.length === 0
|
style="grid-area: tabs; display: flex; gap: 10px; border-bottom: 1px solid var(--color-border-default); padding-bottom: 10px;"
|
||||||
? html`
|
>
|
||||||
<div class="empty-state">
|
<button
|
||||||
<p>No units found matching the current filter.</p>
|
class="filter-button ${!isRecruit ? "active" : ""}"
|
||||||
</div>
|
@click=${() => {
|
||||||
`
|
this.viewMode = "ROSTER";
|
||||||
: filteredUnits.map((unit) => this._renderUnitCard(unit))}
|
this.requestUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Active Roster
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-button ${isRecruit ? "active" : ""}"
|
||||||
|
@click=${() => {
|
||||||
|
this.viewMode = "RECRUIT";
|
||||||
|
this.requestUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Recruit
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this._renderDetailSidebar()}
|
<div style="grid-area: content; height: 100%; overflow: hidden;">
|
||||||
|
${isRecruit ? this._renderRecruitView() : this._renderRosterView()}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,13 @@ 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);
|
||||||
|
|
@ -332,7 +339,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 = "SMITH";
|
this.activeMerchant = "WEAPONS";
|
||||||
this.activeFilter = "ALL";
|
this.activeFilter = "ALL";
|
||||||
this.selectedItem = null;
|
this.selectedItem = null;
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
|
|
@ -366,13 +373,48 @@ export class MarketplaceScreen extends LitElement {
|
||||||
|
|
||||||
_getStock() {
|
_getStock() {
|
||||||
if (!this.marketManager) return [];
|
if (!this.marketManager) return [];
|
||||||
const stock = this.marketManager.getStockForMerchant(this.activeMerchant);
|
|
||||||
|
|
||||||
// Apply filter
|
let stock = [];
|
||||||
if (this.activeFilter === "ALL") {
|
if (this.activeMerchant === "SELL") {
|
||||||
return stock;
|
stock = this.marketManager.getSellableInventory();
|
||||||
|
} 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) {
|
||||||
|
|
@ -402,6 +444,11 @@ 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();
|
||||||
|
|
@ -412,6 +459,19 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
@ -429,6 +489,30 @@ 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", {
|
||||||
|
|
@ -441,10 +525,12 @@ export class MarketplaceScreen extends LitElement {
|
||||||
render() {
|
render() {
|
||||||
const stock = this._getStock();
|
const stock = this._getStock();
|
||||||
const merchants = [
|
const merchants = [
|
||||||
{ id: "SMITH", icon: "⚔️", label: "Smith" },
|
{ id: "WEAPONS", icon: "⚔️", label: "Weapons" },
|
||||||
{ id: "TAILOR", icon: "🧥", label: "Tailor" },
|
{ id: "ARMOR", icon: "🛡️", label: "Armor" },
|
||||||
|
{ id: "ENGINEER", icon: "🔧", label: "Engineer" },
|
||||||
{ id: "ALCHEMIST", icon: "⚗️", label: "Alchemist" },
|
{ id: "ALCHEMIST", icon: "⚗️", label: "Alchemist" },
|
||||||
{ id: "BUYBACK", icon: "♻️", label: "Buyback" },
|
{ id: "BAZAAR", icon: "🏺", label: "Bazaar" },
|
||||||
|
{ id: "SELL", icon: "💰", label: "Sell Items" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const filters = [
|
const filters = [
|
||||||
|
|
@ -517,6 +603,13 @@ 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>
|
||||||
|
|
@ -535,7 +628,11 @@ 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">Confirm Purchase</h3>
|
<h3 class="modal-title">
|
||||||
|
${this.activeMerchant === "SELL"
|
||||||
|
? "Confirm Sell"
|
||||||
|
: "Confirm Purchase"}
|
||||||
|
</h3>
|
||||||
<button class="modal-close" @click=${this._closeModal}>
|
<button class="modal-close" @click=${this._closeModal}>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -547,10 +644,36 @@ 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">
|
||||||
Price: ${this.selectedItem.price} 💎
|
${this.activeMerchant === "SELL"
|
||||||
|
? "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._canAfford(this.selectedItem)
|
${this.activeMerchant !== "SELL" &&
|
||||||
|
!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>`
|
||||||
|
|
@ -559,10 +682,13 @@ 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._canAfford(this.selectedItem)}
|
?disabled=${this.activeMerchant !== "SELL" &&
|
||||||
|
!this._canAfford(this.selectedItem)}
|
||||||
@click=${this._confirmBuy}
|
@click=${this._confirmBuy}
|
||||||
>
|
>
|
||||||
Buy for ${this.selectedItem.price} 💎
|
${this.activeMerchant === "SELL"
|
||||||
|
? `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>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ 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;
|
||||||
|
|
@ -127,7 +128,9 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
className: "Vanguard", // Class name
|
className: "Vanguard", // Class name
|
||||||
classId: "CLASS_VANGUARD",
|
classId: "CLASS_VANGUARD",
|
||||||
};
|
};
|
||||||
gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit);
|
gameStateManager.rosterManager.recruitUnit = sinon
|
||||||
|
.stub()
|
||||||
|
.resolves(mockRecruitedUnit);
|
||||||
gameStateManager.setGameLoop(mockGameLoop);
|
gameStateManager.setGameLoop(mockGameLoop);
|
||||||
await gameStateManager.init();
|
await gameStateManager.init();
|
||||||
|
|
||||||
|
|
@ -137,7 +140,9 @@ 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({ detail: { squad: mockSquad, mode: "SELECT" } });
|
await gameStateManager.handleEmbark({
|
||||||
|
detail: { squad: mockSquad, mode: "SELECT" },
|
||||||
|
});
|
||||||
|
|
||||||
expect(gameStateManager.currentState).to.equal(
|
expect(gameStateManager.currentState).to.equal(
|
||||||
GameStateManager.STATES.DEPLOYMENT
|
GameStateManager.STATES.DEPLOYMENT
|
||||||
|
|
@ -156,11 +161,15 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
className: "Vanguard",
|
className: "Vanguard",
|
||||||
classId: "CLASS_VANGUARD",
|
classId: "CLASS_VANGUARD",
|
||||||
};
|
};
|
||||||
gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit);
|
gameStateManager.rosterManager.recruitUnit = sinon
|
||||||
|
.stub()
|
||||||
|
.resolves(mockRecruitedUnit);
|
||||||
gameStateManager.setGameLoop(mockGameLoop);
|
gameStateManager.setGameLoop(mockGameLoop);
|
||||||
await gameStateManager.init();
|
await gameStateManager.init();
|
||||||
|
|
||||||
const mockSquad = [{ id: "u1", isNew: true, name: "Vanguard", classId: "CLASS_VANGUARD" }];
|
const mockSquad = [
|
||||||
|
{ 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;
|
||||||
|
|
@ -170,7 +179,9 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
eventData = e.detail.runData;
|
eventData = e.detail.runData;
|
||||||
});
|
});
|
||||||
|
|
||||||
await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "DRAFT" } });
|
await gameStateManager.handleEmbark({
|
||||||
|
detail: { squad: mockSquad, mode: "DRAFT" },
|
||||||
|
});
|
||||||
|
|
||||||
expect(eventDispatched).to.be.true;
|
expect(eventDispatched).to.be.true;
|
||||||
expect(eventData).to.exist;
|
expect(eventData).to.exist;
|
||||||
|
|
@ -202,7 +213,8 @@ 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)).to.be.true;
|
expect(gameStateManager.missionManager.load.calledWith(savedCampaignData))
|
||||||
|
.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 () => {
|
||||||
|
|
@ -221,9 +233,11 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
mockPersistence.saveCampaign.resetHistory();
|
mockPersistence.saveCampaign.resetHistory();
|
||||||
|
|
||||||
// Dispatch campaign-data-changed event
|
// Dispatch campaign-data-changed event
|
||||||
window.dispatchEvent(new CustomEvent("campaign-data-changed", {
|
window.dispatchEvent(
|
||||||
detail: { missionCompleted: "MISSION_TUTORIAL_01" }
|
new CustomEvent("campaign-data-changed", {
|
||||||
}));
|
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));
|
||||||
|
|
@ -233,4 +247,50 @@ 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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
134
update_connectivity.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
|
||||||
|
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")
|
||||||
139
update_gen_debug.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
|
||||||
|
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")
|
||||||