diff --git a/cleanup_and_debug.py b/cleanup_and_debug.py new file mode 100644 index 0000000..d88d330 --- /dev/null +++ b/cleanup_and_debug.py @@ -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 { + 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") diff --git a/debug_spires.js b/debug_spires.js new file mode 100644 index 0000000..5272c34 --- /dev/null +++ b/debug_spires.js @@ -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 +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.`); diff --git a/instrument_vertical.py b/instrument_vertical.py new file mode 100644 index 0000000..096f42d --- /dev/null +++ b/instrument_vertical.py @@ -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") diff --git a/refactor_gen.py b/refactor_gen.py new file mode 100644 index 0000000..c381e47 --- /dev/null +++ b/refactor_gen.py @@ -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 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") diff --git a/update_connectivity.py b/update_connectivity.py new file mode 100644 index 0000000..6e5a126 --- /dev/null +++ b/update_connectivity.py @@ -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} + 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 { + 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") diff --git a/update_gen_debug.py b/update_gen_debug.py new file mode 100644 index 0000000..702e983 --- /dev/null +++ b/update_gen_debug.py @@ -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 { + 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")