chore: Add python debug and utility scripts
This commit is contained in:
parent
5c5a030bbb
commit
b363d0850a
6 changed files with 1298 additions and 0 deletions
145
cleanup_and_debug.py
Normal file
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")
|
||||||
575
debug_spires.js
Normal file
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
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")
|
||||||
218
refactor_gen.py
Normal file
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")
|
||||||
134
update_connectivity.py
Normal file
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
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")
|
||||||
Loading…
Reference in a new issue