diff --git "a/index.html" "b/index.html" --- "a/index.html" +++ "b/index.html" @@ -38,7 +38,7 @@ padding: 20px; overflow-y: auto; background-color: #333; - min-width: 280px; + min-width: 280px; /* Increased min-width slightly */ height: 100%; box-sizing: border-box; display: flex; @@ -59,10 +59,11 @@ #story-content { margin-bottom: 20px; line-height: 1.6; - flex-grow: 1; + flex-grow: 1; /* Let story content grow */ } #story-content p { margin-bottom: 1em; } #story-content p:last-child { margin-bottom: 0; } + #story-content .sell-feedback { color: #9f9; font-style: italic; margin-top: 1em;} /* Feedback style */ #stats-inventory-container { margin-bottom: 20px; @@ -84,6 +85,8 @@ border: 1px solid #666; white-space: nowrap; } + #stats-display .stat-gold { background-color: #666030; border-color: #999048;} /* Style gold */ + #stats-display strong, #inventory-display strong { color: #aaa; margin-right: 5px; } #inventory-display em { color: #888; font-style: normal; } @@ -94,7 +97,7 @@ #inventory-display .item-unknown { background-color: #555; border-color: #777;} #choices-container { - margin-top: auto; + margin-top: auto; /* Push choices to bottom */ padding-top: 15px; border-top: 1px solid #555; } @@ -112,6 +115,16 @@ .choice-button:hover:not(:disabled) { background-color: #d4a017; color: #222; border-color: #b8860b; } .choice-button:disabled { background-color: #444; color: #888; cursor: not-allowed; border-color: #666; opacity: 0.7; } + /* Specific style for Sell buttons */ + .sell-button { + background-color: #4a4a4a; + border-color: #6a6a6a; + } + .sell-button:hover:not(:disabled) { + background-color: #a07017; /* Different hover for sell */ + border-color: #80500b; + } + .roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; } .roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; } @@ -157,7 +170,7 @@ let scene, camera, renderer; let currentAssemblyGroup = null; - // Materials + // --- Materials --- (Unchanged) const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.1 }); const woodMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.7, metalness: 0 }); const darkWoodMaterial = new THREE.MeshStandardMaterial({ color: 0x5C3D20, roughness: 0.7, metalness: 0 }); @@ -174,6 +187,7 @@ const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 }); const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 }); + // --- Three.js Setup --- (Unchanged initThreeJS, onWindowResize, animate, createMesh, createGroundPlane) function initThreeJS() { if (!sceneContainer) { console.error("Scene container not found!"); return; } scene = new THREE.Scene(); @@ -234,529 +248,48 @@ return ground; } - // ======================================== - // Procedural Generation Functions - // ======================================== - function createDefaultAssembly() { - const group = new THREE.Group(); - const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); - group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); - group.add(createGroundPlane()); - return group; - } - - function createCityGatesAssembly() { - const group = new THREE.Group(); - const gh=4, gw=1.5, gd=0.8, ah=1, aw=3; // Gate Height, Gate Width, Gate Depth, Arch Height, Arch Width - // Towers - const tlGeo = new THREE.BoxGeometry(gw, gh, gd); - group.add(createMesh(tlGeo, stoneMaterial, { x:-(aw/2+gw/2), y:gh/2, z:0 })); - const trGeo = new THREE.BoxGeometry(gw, gh, gd); - group.add(createMesh(trGeo, stoneMaterial, { x:(aw/2+gw/2), y:gh/2, z:0 })); - // Arch - const aGeo = new THREE.BoxGeometry(aw, ah, gd); - group.add(createMesh(aGeo, stoneMaterial, { x:0, y:gh-ah/2, z:0 })); - // Crenellations - const cs=0.4; // Crenellation size - const cg = new THREE.BoxGeometry(cs, cs, gd*1.1); - for(let i=-1; i<=1; i+=2){ // Left/Right on towers - group.add(createMesh(cg.clone(), stoneMaterial, { x:-(aw/2+gw/2)+i*cs*0.7, y:gh+cs/2, z:0 })); - group.add(createMesh(cg.clone(), stoneMaterial, { x:(aw/2+gw/2)+i*cs*0.7, y:gh+cs/2, z:0 })); - } - // Center crenellation on arch - group.add(createMesh(cg.clone(), stoneMaterial, { x:0, y:gh+ah-cs/2, z:0 })); // Adjusted Y pos - group.add(createGroundPlane(stoneMaterial)); // Stone ground - return group; - } - - function createWeaponsmithAssembly() { - const group = new THREE.Group(); - // Building - const bw=3, bh=2.5, bd=3.5; // Building Width, Height, Depth - const bGeo = new THREE.BoxGeometry(bw, bh, bd); - group.add(createMesh(bGeo, darkWoodMaterial, { x:0, y:bh/2, z:0 })); - // Chimney - const ch=3.5; // Chimney Height - const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); // Slightly tapered - group.add(createMesh(cGeo, stoneMaterial, { x:bw*0.3, y:ch/2, z:-bd*0.3 })); - // TODO: Add anvil, forge visual? - group.add(createGroundPlane(dirtMaterial)); // Dirt ground - return group; - } - - function createTempleAssembly() { - const group = new THREE.Group(); - const bs=5, bsh=0.5, ch=3, cr=0.25, rh=0.5; // Base Size, Base Height, Column Height, Column Radius, Roof Height - // Base platform - const bGeo = new THREE.BoxGeometry(bs, bsh, bs); - group.add(createMesh(bGeo, templeMaterial, { x:0, y:bsh/2, z:0 })); - // Columns - const cGeo = new THREE.CylinderGeometry(cr, cr, ch, 12); - const cPos = [{x:-bs/3, z:-bs/3}, {x:bs/3, z:-bs/3}, {x:-bs/3, z:bs/3}, {x:bs/3, z:bs/3}]; // Column positions - cPos.forEach(p=>group.add(createMesh(cGeo.clone(), templeMaterial, { x:p.x, y:bsh+ch/2, z:p.z }))); - // Roof - const rGeo = new THREE.BoxGeometry(bs*0.9, rh, bs*0.9); // Slightly smaller than base - group.add(createMesh(rGeo, templeMaterial, { x:0, y:bsh+ch+rh/2, z:0 })); - // TODO: Add altar? Steps? - group.add(createGroundPlane(stoneMaterial)); // Stone ground - return group; - } - - function createResistanceMeetingAssembly() { - const group = new THREE.Group(); - // Table - const tw=2, th=0.8, td=1, tt=0.1; // Table Width, Height, Depth, Thickness - const ttg = new THREE.BoxGeometry(tw, tt, td); // Table Top Geometry - group.add(createMesh(ttg, woodMaterial, { x:0, y:th-tt/2, z:0 })); - // Table Legs - const lh=th-tt, ls=0.1; // Leg Height, Size - const lg=new THREE.BoxGeometry(ls, lh, ls); // Leg Geometry - const lofW=tw/2-ls*1.5; // Leg Offset Width - const lofD=td/2-ls*1.5; // Leg Offset Depth - group.add(createMesh(lg, woodMaterial, { x:-lofW, y:lh/2, z:-lofD })); - group.add(createMesh(lg.clone(), woodMaterial, { x:lofW, y:lh/2, z:-lofD })); - group.add(createMesh(lg.clone(), woodMaterial, { x:-lofW, y:lh/2, z:lofD })); - group.add(createMesh(lg.clone(), woodMaterial, { x:lofW, y:lh/2, z:lofD })); - // Stools/Crates - const ss=0.4; // Stool Size - const sg=new THREE.BoxGeometry(ss, ss*0.8, ss); // Stool Geometry - group.add(createMesh(sg, darkWoodMaterial, { x:-tw*0.6, y:ss*0.4, z:0 })); - group.add(createMesh(sg.clone(), darkWoodMaterial, { x:tw*0.6, y:ss*0.4, z:0 })); - // TODO: Add map/papers on table? Dim lighting? - group.add(createGroundPlane(stoneMaterial)); // Stone floor - return group; - } - - function createForestAssembly(tc=10, a=10) { // Tree Count, Area Size - const group = new THREE.Group(); - // Tree creation helper function - const createTree=(x,z)=>{ - const treeGroup=new THREE.Group(); - const trunkHeight=Math.random()*1.5+2; // Random height - const trunkRadius=Math.random()*0.1+0.1; // Random radius - const trunkGeo = new THREE.CylinderGeometry(trunkRadius*0.7, trunkRadius, trunkHeight, 8); // Tapered trunk - treeGroup.add(createMesh(trunkGeo, woodMaterial, {x:0, y:trunkHeight/2, z:0})); - // Foliage (simple sphere) - const foliageRadius=trunkHeight*0.4+0.2; // Foliage size based on height - const foliageGeo=new THREE.SphereGeometry(foliageRadius, 8, 6); // Low poly - treeGroup.add(createMesh(foliageGeo, leafMaterial, {x:0, y:trunkHeight*0.9, z:0})); // Position foliage near top - treeGroup.position.set(x,0,z); - return treeGroup; - }; - // Place trees randomly - for(let i=0; i1.0) group.add(createTree(x,z)); - } - group.add(createGroundPlane(groundMaterial, a*1.1)); // Ground slightly larger than tree area - return group; - } - - function createRoadAmbushAssembly() { - const group = new THREE.Group(); - const areaSize=12; - // Add forest base - const forestGroup = createForestAssembly(8, areaSize); // Fewer trees for ambush visibility - group.add(forestGroup); - // Add Road - const roadWidth=3, roadLength=areaSize*1.2; - const roadGeo=new THREE.PlaneGeometry(roadWidth, roadLength); - const roadMat=new THREE.MeshStandardMaterial({color:0x966F33, roughness:0.9}); // Dirt road color - const road=createMesh(roadGeo, roadMat, {x:0, y:0.01, z:0}, {x:-Math.PI/2}); // Lay flat on ground - road.receiveShadow=true; // Road receives shadows - group.add(road); - // Add some rocks/bushes near road for cover - const rockGeo=new THREE.SphereGeometry(0.5, 5, 4); // Low poly rock - const rockMat=new THREE.MeshStandardMaterial({color:0x666666, roughness:0.8}); - group.add(createMesh(rockGeo, rockMat, {x:roadWidth*0.7, y:0.25, z:1}, {y:Math.random()*Math.PI})); - group.add(createMesh(rockGeo.clone().scale(0.8,0.8,0.8), rockMat, {x:-roadWidth*0.8, y:0.2, z:-2}, {y:Math.random()*Math.PI})); - // Note: Goblins/enemies added separately if needed by game logic/specific scene key - return group; - } - - function createForestEdgeAssembly() { - const group = new THREE.Group(); - const areaSize=15; - const forestGroup = createForestAssembly(15, areaSize); // Denser forest - // Remove trees from one side to create the "edge" - const treesToRemove=[]; - forestGroup.children.forEach(child => { - // Assuming trees are added as Groups directly to forestGroup - if(child.type === 'Group' && child.position.x > 0) { // Remove trees on the positive X side - treesToRemove.push(child); - } - }); - treesToRemove.forEach(tree => forestGroup.remove(tree)); - group.add(forestGroup); - // TODO: Could add foothills/different terrain visual beyond the edge - return group; - } - - function createPrisonerCellAssembly() { - const group = new THREE.Group(); - const cellSize=3, wallHeight=2.5, wallThickness=0.2, barRadius=0.05, barSpacing=0.25; - // Floor - const cellFloorMat=stoneMaterial.clone(); - cellFloorMat.color.setHex(0x555555); // Darker floor - group.add(createGroundPlane(cellFloorMat, cellSize)); - // Walls (Back and Sides) - const wallBackGeo=new THREE.BoxGeometry(cellSize, wallHeight, wallThickness); - group.add(createMesh(wallBackGeo, stoneMaterial, {x:0, y:wallHeight/2, z:-cellSize/2})); // Back wall - const wallSideGeo=new THREE.BoxGeometry(wallThickness, wallHeight, cellSize); - group.add(createMesh(wallSideGeo, stoneMaterial, {x:-cellSize/2, y:wallHeight/2, z:0})); // Left wall - group.add(createMesh(wallSideGeo.clone(), stoneMaterial, {x:cellSize/2, y:wallHeight/2, z:0})); // Right wall - // Bars (Front) - const barGeo=new THREE.CylinderGeometry(barRadius, barRadius, wallHeight, 8); - const numBars=Math.floor(cellSize/barSpacing); - for(let i=0; i 2 || Math.abs(z) > 2) { // Avoid center - group.add(createMesh(grassGeo, grassMaterial, { x, y: 0.25, z }, { y: Math.random() * Math.PI })); - } - } - const rockGeo = new THREE.SphereGeometry(0.3, 6, 6); // Simple rock - for (let i = 0; i < 10; i++) { - group.add(createMesh(rockGeo, stoneMaterial, { x: (Math.random() - 0.5) * 20, y: 0.15, z: (Math.random() - 0.5) * 20 }, { y: Math.random() * Math.PI })); - } - return group; - } - - function createRollingHillsAssembly() { - const group = new THREE.Group(); - // Use a PlaneGeometry and modify vertex heights - const hillGeo = new THREE.PlaneGeometry(50, 50, 20, 20); // More segments for smoother hills - const hillMat = grassMaterial.clone(); - const hill = new THREE.Mesh(hillGeo, hillMat); - hill.rotation.x = -Math.PI / 2; // Lay flat initially - hill.receiveShadow = true; - - const positions = hillGeo.attributes.position; - for (let i = 0; i < positions.count; i++) { - const x = positions.getX(i); - const z = positions.getY(i); // Note: Before rotation, Y is depth (becomes Z) - // Use sine waves to create rolling hills effect - const y = (Math.sin(x * 0.1 + z * 0.05) + Math.sin(x * 0.05 + z * 0.2)) * 1.5; // Adjust frequency/amplitude - positions.setZ(i, y); // Set the height (Z becomes Y after rotation) - } - hillGeo.computeVertexNormals(); // Important for correct lighting - group.add(hill); - - // Add distant shepherd figure (simple cylinder) - const shepherdGeo = new THREE.CylinderGeometry(0.15, 0.1, 1.2, 8); // Simple figure - group.add(createMesh(shepherdGeo, darkWoodMaterial, { x: 15, y: 1.5 + Math.sin(15 * 0.1 - 15 * 0.05) * 1.5, z: -15 })); // Place on hill - - // Add some grass tufts - const grassGeo = new THREE.ConeGeometry(0.3, 0.7, 6); - for (let i = 0; i < 50; i++) { - const x = (Math.random() - 0.5) * 40; - const z = (Math.random() - 0.5) * 40; - // Calculate height at this point - const y = (Math.sin(x * 0.1 + z * 0.05) + Math.sin(x * 0.05 + z * 0.2)) * 1.5 + 0.35; - group.add(createMesh(grassGeo, grassMaterial, { x: x, y: y, z: z }, { y: Math.random() * Math.PI })); - } - return group; - } - - function createCoastalCliffsAssembly() { - const group = new THREE.Group(); - // Main cliff block (simple for now) - const cliffGeo = new THREE.BoxGeometry(15, 8, 15); // Taller, wider cliffs - // Displace vertices for more natural look - const positions = cliffGeo.attributes.position; - const normals = cliffGeo.attributes.normal; - for (let i = 0; i < positions.count; i++) { - // Push vertices outwards slightly based on normals, more randomness for cliffs - const displaceFactor = 1 + (Math.random() - 0.5) * 0.3; - if (Math.abs(normals.getX(i)) > 0.5 || Math.abs(normals.getZ(i)) > 0.5) { // Affect sides more - positions.setX(i, positions.getX(i) * displaceFactor); - positions.setZ(i, positions.getZ(i) * displaceFactor); - } - positions.setY(i, positions.getY(i) * (1 + (Math.random()-0.5)*0.1)); // Vary height slightly - } - cliffGeo.computeVertexNormals(); - group.add(createMesh(cliffGeo, stoneMaterial, { y: 4 })); // Center cliff higher - - // Path down cliff (using a plane deformed along a curve would be better) - const pathGeo = new THREE.PlaneGeometry(1, 10, 1, 10); // More segments - const path = createMesh(pathGeo, dirtMaterial, { x: -4, y: 4, z: 0 }, { x: -Math.PI / 2, z: -Math.PI / 6 }); - // TODO: Deform path vertices to follow cliffside better - group.add(path); - - // Ocean plane - const oceanGeo = new THREE.PlaneGeometry(100, 100, 20, 20); // More segments for waves - const ocean = createMesh(oceanGeo, oceanMaterial, { y: -1 }, { x: -Math.PI / 2 }); // Lower ocean level - ocean.receiveShadow = false; // Ocean doesn't receive shadows - // Simple wave animation - const clock = new THREE.Clock(); - ocean.userData.update = (time) => { - const delta = clock.getDelta(); // Use clock delta if available - const oceanPositions = ocean.geometry.attributes.position; - for (let i = 0; i < oceanPositions.count; i++) { - const x = oceanPositions.getX(i); - const z = oceanPositions.getY(i); // Y before rotation - const y = Math.sin(x * 0.1 + time * 0.5) * 0.2 + Math.sin(z * 0.1 + time * 0.3) * 0.1; - oceanPositions.setZ(i, y); // Set height (Z becomes Y after rotation) - } - ocean.geometry.attributes.position.needsUpdate = true; - ocean.geometry.computeVertexNormals(); - }; - group.add(ocean); - return group; - } - - function createForestEntranceAssembly() { - const group = createForestAssembly(25, 15); // Denser forest at entrance - // Add gnarled roots near the center - const rootGeo = new THREE.TorusGeometry(0.8, 0.15, 8, 16); // Thicker roots - for (let i = 0; i < 10; i++) { - group.add(createMesh(rootGeo, woodMaterial, - { x: (Math.random() - 0.5) * 5, y: 0.1, z: (Math.random() - 0.5) * 5 + 2 }, // Near front - { x: Math.PI / 2 + (Math.random()-0.5)*0.5, y: Math.random()*Math.PI, z:(Math.random()-0.5)*0.5 } // More random rotation - )); - } - return group; - } - - function createOvergrownPathAssembly() { - const group = new THREE.Group(); - group.add(createGroundPlane(dirtMaterial, 15)); - const forest = createForestAssembly(20, 12); // Dense forest - group.add(forest); - // Glowing Fungi - const fungiGeo = new THREE.SphereGeometry(0.1, 8, 8); - for (let i = 0; i < 30; i++) { - group.add(createMesh(fungiGeo, glowMaterial, { x: (Math.random() - 0.5) * 10, y: 0.1, z: (Math.random() - 0.5) * 10 })); - } - // Vines (simple cylinders for now) - const vineGeo = new THREE.CylinderGeometry(0.05, 0.05, 3, 8); // Longer vines - for (let i = 0; i < 15; i++) { - // Find a tree to attach to (simplistic search) - let targetTree = null; - for(const child of forest.children) { - if(child.type === 'Group' && Math.random() > 0.5) { - targetTree = child; - break; - } - } - let vinePos = { x: (Math.random() - 0.5) * 8, y: 1.5, z: (Math.random() - 0.5) * 8 }; - if(targetTree) { - vinePos = {x: targetTree.position.x, y: 1.5, z: targetTree.position.z}; - } - group.add(createMesh(vineGeo, leafMaterial, vinePos, - { x:(Math.random()-0.5)*Math.PI*0.5, y:Math.random()*Math.PI, z: (Math.random()-0.5)*Math.PI*0.5 + Math.PI/2 } // More hanging rotation - )); - } - return group; - } + // --- Procedural Generation Functions --- (Unchanged create...Assembly functions) + // [Keep all the create...Assembly functions from the previous listing here] + // ... (createDefaultAssembly, createCityGatesAssembly, ..., createDarkCaveAssembly) ... + function createDefaultAssembly() { const group = new THREE.Group(); const sphereGeo = new THREE.SphereGeometry(0.5, 16, 16); group.add(createMesh(sphereGeo, stoneMaterial, { x: 0, y: 0.5, z: 0 })); group.add(createGroundPlane()); return group; } + function createCityGatesAssembly() { const group = new THREE.Group(); const gh = 4, gw = 1.5, gd = 0.8, ah = 1, aw = 3; const tlGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(tlGeo, stoneMaterial, { x: -(aw / 2 + gw / 2), y: gh / 2, z: 0 })); const trGeo = new THREE.BoxGeometry(gw, gh, gd); group.add(createMesh(trGeo, stoneMaterial, { x: (aw / 2 + gw / 2), y: gh / 2, z: 0 })); const aGeo = new THREE.BoxGeometry(aw, ah, gd); group.add(createMesh(aGeo, stoneMaterial, { x: 0, y: gh - ah / 2, z: 0 })); const cs = 0.4; const cg = new THREE.BoxGeometry(cs, cs, gd * 1.1); for (let i = -1; i <= 1; i += 2) { group.add(createMesh(cg.clone(), stoneMaterial, { x: -(aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); group.add(createMesh(cg.clone(), stoneMaterial, { x: (aw / 2 + gw / 2) + i * cs * 0.7, y: gh + cs / 2, z: 0 })); } group.add(createMesh(cg.clone(), stoneMaterial, { x: 0, y: gh + ah - cs / 2, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; } + function createWeaponsmithAssembly() { const group = new THREE.Group(); const bw = 3, bh = 2.5, bd = 3.5; const bGeo = new THREE.BoxGeometry(bw, bh, bd); group.add(createMesh(bGeo, darkWoodMaterial, { x: 0, y: bh / 2, z: 0 })); const ch = 3.5; const cGeo = new THREE.CylinderGeometry(0.3, 0.4, ch, 8); group.add(createMesh(cGeo, stoneMaterial, { x: bw * 0.3, y: ch / 2, z: -bd * 0.3 })); group.add(createGroundPlane(dirtMaterial)); return group; } + function createTempleAssembly() { const group = new THREE.Group(); const bs = 5, bsh = 0.5, ch = 3, cr = 0.25, rh = 0.5; const bGeo = new THREE.BoxGeometry(bs, bsh, bs); group.add(createMesh(bGeo, templeMaterial, { x: 0, y: bsh / 2, z: 0 })); const cGeo = new THREE.CylinderGeometry(cr, cr, ch, 12); const cPos = [{ x: -bs / 3, z: -bs / 3 }, { x: bs / 3, z: -bs / 3 }, { x: -bs / 3, z: bs / 3 }, { x: bs / 3, z: bs / 3 }]; cPos.forEach(p => group.add(createMesh(cGeo.clone(), templeMaterial, { x: p.x, y: bsh + ch / 2, z: p.z }))); const rGeo = new THREE.BoxGeometry(bs * 0.9, rh, bs * 0.9); group.add(createMesh(rGeo, templeMaterial, { x: 0, y: bsh + ch + rh / 2, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; } + function createResistanceMeetingAssembly() { const group = new THREE.Group(); const tw = 2, th = 0.8, td = 1, tt = 0.1; const ttg = new THREE.BoxGeometry(tw, tt, td); group.add(createMesh(ttg, woodMaterial, { x: 0, y: th - tt / 2, z: 0 })); const lh = th - tt, ls = 0.1; const lg = new THREE.BoxGeometry(ls, lh, ls); const lofW = tw / 2 - ls * 1.5; const lofD = td / 2 - ls * 1.5; group.add(createMesh(lg, woodMaterial, { x: -lofW, y: lh / 2, z: -lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x: lofW, y: lh / 2, z: -lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x: -lofW, y: lh / 2, z: lofD })); group.add(createMesh(lg.clone(), woodMaterial, { x: lofW, y: lh / 2, z: lofD })); const ss = 0.4; const sg = new THREE.BoxGeometry(ss, ss * 0.8, ss); group.add(createMesh(sg, darkWoodMaterial, { x: -tw * 0.6, y: ss * 0.4, z: 0 })); group.add(createMesh(sg.clone(), darkWoodMaterial, { x: tw * 0.6, y: ss * 0.4, z: 0 })); group.add(createGroundPlane(stoneMaterial)); return group; } + function createForestAssembly(tc = 10, a = 10) { const group = new THREE.Group(); const createTree = (x, z) => { const treeGroup = new THREE.Group(); const trunkHeight = Math.random() * 1.5 + 2; const trunkRadius = Math.random() * 0.1 + 0.1; const trunkGeo = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8); treeGroup.add(createMesh(trunkGeo, woodMaterial, { x: 0, y: trunkHeight / 2, z: 0 })); const foliageRadius = trunkHeight * 0.4 + 0.2; const foliageGeo = new THREE.SphereGeometry(foliageRadius, 8, 6); treeGroup.add(createMesh(foliageGeo, leafMaterial, { x: 0, y: trunkHeight * 0.9, z: 0 })); treeGroup.position.set(x, 0, z); return treeGroup; }; for (let i = 0; i < tc; i++) { const x = (Math.random() - 0.5) * a; const z = (Math.random() - 0.5) * a; if (Math.sqrt(x * x + z * z) > 1.0) group.add(createTree(x, z)); } group.add(createGroundPlane(groundMaterial, a * 1.1)); return group; } + function createRoadAmbushAssembly() { const group = new THREE.Group(); const areaSize = 12; const forestGroup = createForestAssembly(8, areaSize); group.add(forestGroup); const roadWidth = 3, roadLength = areaSize * 1.2; const roadGeo = new THREE.PlaneGeometry(roadWidth, roadLength); const roadMat = new THREE.MeshStandardMaterial({ color: 0x966F33, roughness: 0.9 }); const road = createMesh(roadGeo, roadMat, { x: 0, y: 0.01, z: 0 }, { x: -Math.PI / 2 }); road.receiveShadow = true; group.add(road); const rockGeo = new THREE.SphereGeometry(0.5, 5, 4); const rockMat = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.8 }); group.add(createMesh(rockGeo, rockMat, { x: roadWidth * 0.7, y: 0.25, z: 1 }, { y: Math.random() * Math.PI })); group.add(createMesh(rockGeo.clone().scale(0.8, 0.8, 0.8), rockMat, { x: -roadWidth * 0.8, y: 0.2, z: -2 }, { y: Math.random() * Math.PI })); return group; } + function createForestEdgeAssembly() { const group = new THREE.Group(); const areaSize = 15; const forestGroup = createForestAssembly(15, areaSize); const treesToRemove = []; forestGroup.children.forEach(child => { if (child.type === 'Group' && child.position.x > 0) { treesToRemove.push(child); } }); treesToRemove.forEach(tree => forestGroup.remove(tree)); group.add(forestGroup); return group; } + function createPrisonerCellAssembly() { const group = new THREE.Group(); const cellSize = 3, wallHeight = 2.5, wallThickness = 0.2, barRadius = 0.05, barSpacing = 0.25; const cellFloorMat = stoneMaterial.clone(); cellFloorMat.color.setHex(0x555555); group.add(createGroundPlane(cellFloorMat, cellSize)); const wallBackGeo = new THREE.BoxGeometry(cellSize, wallHeight, wallThickness); group.add(createMesh(wallBackGeo, stoneMaterial, { x: 0, y: wallHeight / 2, z: -cellSize / 2 })); const wallSideGeo = new THREE.BoxGeometry(wallThickness, wallHeight, cellSize); group.add(createMesh(wallSideGeo, stoneMaterial, { x: -cellSize / 2, y: wallHeight / 2, z: 0 })); group.add(createMesh(wallSideGeo.clone(), stoneMaterial, { x: cellSize / 2, y: wallHeight / 2, z: 0 })); const barGeo = new THREE.CylinderGeometry(barRadius, barRadius, wallHeight, 8); const numBars = Math.floor(cellSize / barSpacing); for (let i = 0; i < numBars; i++) { const xPos = -cellSize / 2 + (i + 0.5) * barSpacing; group.add(createMesh(barGeo.clone(), metalMaterial, { x: xPos, y: wallHeight / 2, z: cellSize / 2 })); } return group; } + function createGameOverAssembly() { const group = new THREE.Group(); const boxGeo = new THREE.BoxGeometry(2, 2, 2); group.add(createMesh(boxGeo, gameOverMaterial, { x: 0, y: 1, z: 0 })); group.add(createGroundPlane(stoneMaterial.clone().set({ color: 0x333333 }))); return group; } + function createErrorAssembly() { const group = new THREE.Group(); const coneGeo = new THREE.ConeGeometry(0.8, 1.5, 8); group.add(createMesh(coneGeo, errorMaterial, { x: 0, y: 0.75, z: 0 })); group.add(createGroundPlane()); return group; } + function createCrossroadsAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(dirtMaterial, 30)); const poleGeo = new THREE.CylinderGeometry(0.1, 0.1, 3, 8); group.add(createMesh(poleGeo, woodMaterial, { y: 1.5 })); const signGeo = new THREE.BoxGeometry(1.5, 0.3, 0.05); group.add(createMesh(signGeo, woodMaterial, { y: 2.5, z: 0.2 }, { y: Math.PI / 4 })); group.add(createMesh(signGeo, woodMaterial, { y: 2.2, x: -0.2 }, { y: -Math.PI / 4 })); const grassGeo = new THREE.ConeGeometry(0.2, 0.5, 6); for (let i = 0; i < 20; i++) { const x = (Math.random() - 0.5) * 25; const z = (Math.random() - 0.5) * 25; if (Math.abs(x) > 2 || Math.abs(z) > 2) { group.add(createMesh(grassGeo, grassMaterial, { x, y: 0.25, z }, { y: Math.random() * Math.PI })); } } const rockGeo = new THREE.SphereGeometry(0.3, 6, 6); for (let i = 0; i < 10; i++) { group.add(createMesh(rockGeo, stoneMaterial, { x: (Math.random() - 0.5) * 20, y: 0.15, z: (Math.random() - 0.5) * 20 }, { y: Math.random() * Math.PI })); } return group; } + function createRollingHillsAssembly() { const group = new THREE.Group(); const hillGeo = new THREE.PlaneGeometry(50, 50, 20, 20); const hillMat = grassMaterial.clone(); const hill = new THREE.Mesh(hillGeo, hillMat); hill.rotation.x = -Math.PI / 2; hill.receiveShadow = true; const positions = hillGeo.attributes.position; for (let i = 0; i < positions.count; i++) { const x = positions.getX(i); const z = positions.getY(i); const y = (Math.sin(x * 0.1 + z * 0.05) + Math.sin(x * 0.05 + z * 0.2)) * 1.5; positions.setZ(i, y); } hillGeo.computeVertexNormals(); group.add(hill); const shepherdGeo = new THREE.CylinderGeometry(0.15, 0.1, 1.2, 8); group.add(createMesh(shepherdGeo, darkWoodMaterial, { x: 15, y: 1.5 + Math.sin(15 * 0.1 - 15 * 0.05) * 1.5, z: -15 })); const grassGeo = new THREE.ConeGeometry(0.3, 0.7, 6); for (let i = 0; i < 50; i++) { const x = (Math.random() - 0.5) * 40; const z = (Math.random() - 0.5) * 40; const y = (Math.sin(x * 0.1 + z * 0.05) + Math.sin(x * 0.05 + z * 0.2)) * 1.5 + 0.35; group.add(createMesh(grassGeo, grassMaterial, { x: x, y: y, z: z }, { y: Math.random() * Math.PI })); } return group; } + function createCoastalCliffsAssembly() { const group = new THREE.Group(); const cliffGeo = new THREE.BoxGeometry(15, 8, 15); const positions = cliffGeo.attributes.position; const normals = cliffGeo.attributes.normal; for (let i = 0; i < positions.count; i++) { const displaceFactor = 1 + (Math.random() - 0.5) * 0.3; if (Math.abs(normals.getX(i)) > 0.5 || Math.abs(normals.getZ(i)) > 0.5) { positions.setX(i, positions.getX(i) * displaceFactor); positions.setZ(i, positions.getZ(i) * displaceFactor); } positions.setY(i, positions.getY(i) * (1 + (Math.random() - 0.5) * 0.1)); } cliffGeo.computeVertexNormals(); group.add(createMesh(cliffGeo, stoneMaterial, { y: 4 })); const pathGeo = new THREE.PlaneGeometry(1, 10, 1, 10); const path = createMesh(pathGeo, dirtMaterial, { x: -4, y: 4, z: 0 }, { x: -Math.PI / 2, z: -Math.PI / 6 }); group.add(path); const oceanGeo = new THREE.PlaneGeometry(100, 100, 20, 20); const ocean = createMesh(oceanGeo, oceanMaterial, { y: -1 }, { x: -Math.PI / 2 }); ocean.receiveShadow = false; const clock = new THREE.Clock(); ocean.userData.update = (time) => { const delta = clock.getDelta(); const oceanPositions = ocean.geometry.attributes.position; for (let i = 0; i < oceanPositions.count; i++) { const x = oceanPositions.getX(i); const z = oceanPositions.getY(i); const y = Math.sin(x * 0.1 + time * 0.5) * 0.2 + Math.sin(z * 0.1 + time * 0.3) * 0.1; oceanPositions.setZ(i, y); } ocean.geometry.attributes.position.needsUpdate = true; ocean.geometry.computeVertexNormals(); }; group.add(ocean); return group; } + function createForestEntranceAssembly() { const group = createForestAssembly(25, 15); const rootGeo = new THREE.TorusGeometry(0.8, 0.15, 8, 16); for (let i = 0; i < 10; i++) { group.add(createMesh(rootGeo, woodMaterial, { x: (Math.random() - 0.5) * 5, y: 0.1, z: (Math.random() - 0.5) * 5 + 2 }, { x: Math.PI / 2 + (Math.random() - 0.5) * 0.5, y: Math.random() * Math.PI, z: (Math.random() - 0.5) * 0.5 })); } return group; } + function createOvergrownPathAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(dirtMaterial, 15)); const forest = createForestAssembly(20, 12); group.add(forest); const fungiGeo = new THREE.SphereGeometry(0.1, 8, 8); for (let i = 0; i < 30; i++) { group.add(createMesh(fungiGeo, glowMaterial, { x: (Math.random() - 0.5) * 10, y: 0.1, z: (Math.random() - 0.5) * 10 })); } const vineGeo = new THREE.CylinderGeometry(0.05, 0.05, 3, 8); for (let i = 0; i < 15; i++) { let targetTree = null; for (const child of forest.children) { if (child.type === 'Group' && Math.random() > 0.5) { targetTree = child; break; } } let vinePos = { x: (Math.random() - 0.5) * 8, y: 1.5, z: (Math.random() - 0.5) * 8 }; if (targetTree) { vinePos = { x: targetTree.position.x, y: 1.5, z: targetTree.position.z }; } group.add(createMesh(vineGeo, leafMaterial, vinePos, { x: (Math.random() - 0.5) * Math.PI * 0.5, y: Math.random() * Math.PI, z: (Math.random() - 0.5) * Math.PI * 0.5 + Math.PI / 2 })); } return group; } + function createClearingStatueAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(grassMaterial, 10)); const baseGeo = new THREE.CylinderGeometry(0.6, 0.8, 0.3, 12); group.add(createMesh(baseGeo, stoneMaterial, { y: 0.15 })); const statueGeo = new THREE.BoxGeometry(0.8, 2, 0.8); const sPos = statueGeo.attributes.position; for (let i = 0; i < sPos.count; i++) { sPos.setX(i, sPos.getX(i) * (1 + (Math.random() - 0.5) * 0.05)); sPos.setY(i, sPos.getY(i) * (1 + (Math.random() - 0.5) * 0.05)); sPos.setZ(i, sPos.getZ(i) * (1 + (Math.random() - 0.5) * 0.05)); } statueGeo.computeVertexNormals(); const statue = createMesh(statueGeo, stoneMaterial, { y: 0.3 + 1 }); group.add(statue); const mossGeo = new THREE.SphereGeometry(0.1, 6, 6); for (let i = 0; i < 20; i++) { const faceIndex = Math.floor(Math.random() * statueGeo.index.count / 3); const pA = new THREE.Vector3().fromBufferAttribute(sPos, statueGeo.index.getX(faceIndex * 3)); const pB = new THREE.Vector3().fromBufferAttribute(sPos, statueGeo.index.getY(faceIndex * 3)); const pC = new THREE.Vector3().fromBufferAttribute(sPos, statueGeo.index.getZ(faceIndex * 3)); const point = new THREE.Triangle(pA, pB, pC).getMidpoint(new THREE.Vector3()); point.add(statue.position); group.add(createMesh(mossGeo, grassMaterial, point)); } const leafGeo = new THREE.PlaneGeometry(0.15, 0.1); for (let i = 0; i < 30; i++) { group.add(createMesh(leafGeo, leafMaterial, { x: (Math.random() - 0.5) * 8, y: 0.01, z: (Math.random() - 0.5) * 8 }, { x: -Math.PI / 2, y: 0, z: Math.random() * Math.PI })); } return group; } + function createGoblinAmbushAssembly() { const group = createOvergrownPathAssembly(); const createGoblin = () => { const goblinGroup = new THREE.Group(); const bodyGeo = new THREE.CylinderGeometry(0.2, 0.3, 1, 8); const headGeo = new THREE.SphereGeometry(0.25, 8, 8); const goblinMat = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.8 }); goblinGroup.add(createMesh(bodyGeo, goblinMat, { y: 0.5 })); goblinGroup.add(createMesh(headGeo, goblinMat, { y: 1.2 })); const spearGeo = new THREE.CylinderGeometry(0.03, 0.03, 1.5, 6); const spearTipGeo = new THREE.ConeGeometry(0.05, 0.15, 6); const spear = new THREE.Group(); spear.add(createMesh(spearGeo, woodMaterial, { y: 0.75 })); spear.add(createMesh(spearTipGeo, metalMaterial.clone().set({ color: 0x777777 }), { y: 1.5 + 0.075 })); spear.rotation.z = Math.PI / 5; spear.position.x = 0.3; goblinGroup.add(spear); return goblinGroup; } const goblin1 = createGoblin(); goblin1.position.set(-1.5, 0, 2); goblin1.rotation.y = Math.PI / 4; group.add(goblin1); const goblin2 = createGoblin(); goblin2.position.set(1.5, 0, 2.5); goblin2.rotation.y = -Math.PI / 6; group.add(goblin2); return group; } + function createHiddenCoveAssembly() { const group = new THREE.Group(); group.add(createGroundPlane(sandMaterial, 15)); const caveGeo = new THREE.BoxGeometry(3, 2.5, 3); const caveMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); group.add(createMesh(caveGeo, caveMat, { z: -6, y: 1.25 })); const rockGeo = new THREE.SphereGeometry(0.5, 6, 6); const rockMat = wetStoneMaterial.clone(); for (let i = 0; i < 15; i++) { group.add(createMesh(rockGeo, rockMat, { x: (Math.random() - 0.5) * 12, y: 0.25, z: (Math.random() - 0.5) * 12 }, { y: Math.random() * Math.PI })); } const seaweedGeo = new THREE.ConeGeometry(0.2, 1.2, 6); const seaweedMat = leafMaterial.clone().set({ color: 0x1E4D2B }); for (let i = 0; i < 10; i++) { group.add(createMesh(seaweedGeo, seaweedMat, { x: (Math.random() - 0.5) * 10, y: 0.6, z: (Math.random() - 0.5) * 10 + 2 }, { x: (Math.random() - 0.5) * 0.2, z: (Math.random() - 0.5) * 0.2 })); } return group; } + function createDarkCaveAssembly() { const group = new THREE.Group(); const caveRadius = 5; const caveHeight = 4; group.add(createGroundPlane(wetStoneMaterial, caveRadius * 2)); const wallGeo = new THREE.SphereGeometry(caveRadius, 32, 16, 0, Math.PI * 2, 0, Math.PI / 1.5); const wallMat = wetStoneMaterial.clone(); wallMat.side = THREE.BackSide; const wall = new THREE.Mesh(wallGeo, wallMat); wall.position.y = caveHeight * 0.6; group.add(wall); const stalactiteGeo = new THREE.ConeGeometry(0.1, 0.8, 8); const stalagmiteGeo = new THREE.ConeGeometry(0.15, 0.5, 8); for (let i = 0; i < 15; i++) { const x = (Math.random() - 0.5) * caveRadius * 1.5; const z = (Math.random() - 0.5) * caveRadius * 1.5; if (Math.random() > 0.5) { group.add(createMesh(stalactiteGeo, wetStoneMaterial, { x: x, y: caveHeight - 0.4, z: z })) } else { group.add(createMesh(stalagmiteGeo, wetStoneMaterial, { x: x, y: 0.25, z: z })) } } const dripGeo = new THREE.SphereGeometry(0.05, 8, 8); for (let i = 0; i < 5; i++) { const drip = createMesh(dripGeo, oceanMaterial, { x: (Math.random() - 0.5) * caveRadius, y: caveHeight - 0.2, z: (Math.random() - 0.5) * caveRadius }); drip.userData.startY = caveHeight - 0.2; drip.userData.update = (time) => { drip.position.y -= 0.1; if (drip.position.y < 0) { drip.position.y = drip.userData.startY; drip.position.x = (Math.random() - 0.5) * caveRadius; drip.position.z = (Math.random() - 0.5) * caveRadius; } }; group.add(drip); } return group; } - function createClearingStatueAssembly() { - const group = new THREE.Group(); - group.add(createGroundPlane(grassMaterial, 10)); // Grassy clearing - // Statue Base - const baseGeo = new THREE.CylinderGeometry(0.6, 0.8, 0.3, 12); - group.add(createMesh(baseGeo, stoneMaterial, { y: 0.15 })); - // Statue Figure (abstract) - const statueGeo = new THREE.BoxGeometry(0.8, 2, 0.8); - // Slightly deform statue geometry - const sPos = statueGeo.attributes.position; - for (let i = 0; i < sPos.count; i++) { - sPos.setX(i, sPos.getX(i) * (1 + (Math.random()-0.5)*0.05)); - sPos.setY(i, sPos.getY(i) * (1 + (Math.random()-0.5)*0.05)); - sPos.setZ(i, sPos.getZ(i) * (1 + (Math.random()-0.5)*0.05)); - } - statueGeo.computeVertexNormals(); - const statue = createMesh(statueGeo, stoneMaterial, { y: 0.3 + 1 }); // On base - group.add(statue); - // Moss patches (using small spheres) - const mossGeo = new THREE.SphereGeometry(0.1, 6, 6); // Smaller moss - for (let i = 0; i < 20; i++) { - // Place moss on statue surface (approximate) - const faceIndex = Math.floor(Math.random() * statueGeo.index.count / 3); - const pA = new THREE.Vector3().fromBufferAttribute(sPos, statueGeo.index.getX(faceIndex*3)); - const pB = new THREE.Vector3().fromBufferAttribute(sPos, statueGeo.index.getY(faceIndex*3)); - const pC = new THREE.Vector3().fromBufferAttribute(sPos, statueGeo.index.getZ(faceIndex*3)); - const point = new THREE.Triangle(pA, pB, pC).getMidpoint(new THREE.Vector3()); - point.add(statue.position); // Add statue's position - group.add(createMesh(mossGeo, grassMaterial, point)); - } - // Fallen Leaves - const leafGeo = new THREE.PlaneGeometry(0.15, 0.1); // Slightly larger leaves - for (let i = 0; i < 30; i++) { - group.add(createMesh(leafGeo, leafMaterial, - { x: (Math.random() - 0.5) * 8, y: 0.01, z: (Math.random() - 0.5) * 8 }, // On ground - { x: -Math.PI/2, y: 0, z: Math.random() * Math.PI } // Random rotation flat on ground - )); - } - return group; - } - - function createGoblinAmbushAssembly() { - // Base scene: overgrown path - const group = createOvergrownPathAssembly(); - // Goblin Figure creation helper - const createGoblin = () => { - const goblinGroup = new THREE.Group(); - const bodyGeo = new THREE.CylinderGeometry(0.2, 0.3, 1, 8); // Smaller body - const headGeo = new THREE.SphereGeometry(0.25, 8, 8); // Slightly larger head - const goblinMat = new THREE.MeshStandardMaterial({ color: 0x556B2F, roughness: 0.8 }); // Dark olive green - goblinGroup.add(createMesh(bodyGeo, goblinMat, { y: 0.5 })); - goblinGroup.add(createMesh(headGeo, goblinMat, { y: 1.2 })); - // Spear - const spearGeo = new THREE.CylinderGeometry(0.03, 0.03, 1.5, 6); // Thinner spear - const spearTipGeo = new THREE.ConeGeometry(0.05, 0.15, 6); - const spear = new THREE.Group(); - spear.add(createMesh(spearGeo, woodMaterial, {y:0.75})); - spear.add(createMesh(spearTipGeo, metalMaterial.clone().set({color:0x777777}), {y:1.5+0.075})); - spear.rotation.z = Math.PI / 5; // Angled spear - spear.position.x = 0.3; - goblinGroup.add(spear); - return goblinGroup; - } - // Add two goblins near the path - const goblin1 = createGoblin(); - goblin1.position.set(-1.5, 0, 2); // Hiding near path edge - goblin1.rotation.y = Math.PI / 4; // Facing towards path - group.add(goblin1); - - const goblin2 = createGoblin(); - goblin2.position.set(1.5, 0, 2.5); // Further back, other side - goblin2.rotation.y = -Math.PI / 6; // Facing towards path - group.add(goblin2); - return group; - } - - function createHiddenCoveAssembly() { - const group = new THREE.Group(); - group.add(createGroundPlane(sandMaterial, 15)); // Sandy ground - // Cave Entrance (simple dark box as placeholder) - const caveGeo = new THREE.BoxGeometry(3, 2.5, 3); - const caveMat = new THREE.MeshStandardMaterial({color: 0x111111}); // Very dark material - group.add(createMesh(caveGeo, caveMat, { z: -6, y: 1.25 })); // Positioned at back - // Rocks scattered around - const rockGeo = new THREE.SphereGeometry(0.5, 6, 6); - const rockMat = wetStoneMaterial.clone(); // Use wet stone for cove rocks - for (let i = 0; i < 15; i++) { - group.add(createMesh(rockGeo, rockMat, - { x: (Math.random() - 0.5) * 12, y: 0.25, z: (Math.random() - 0.5) * 12 }, - { y: Math.random()*Math.PI} - )); - } - // Seaweed (simple cones) - const seaweedGeo = new THREE.ConeGeometry(0.2, 1.2, 6); - const seaweedMat = leafMaterial.clone().set({color: 0x1E4D2B}); // Darker green - for (let i = 0; i < 10; i++) { - group.add(createMesh(seaweedGeo, seaweedMat, - { x: (Math.random() - 0.5) * 10, y: 0.6, z: (Math.random() - 0.5) * 10 + 2 }, // Closer to front/water edge - {x: (Math.random()-0.5)*0.2, z: (Math.random()-0.5)*0.2} // Slight tilt - )); - } - // TODO: Add driftwood? Shells? Simple boat wreck? - return group; - } - - function createDarkCaveAssembly() { - const group = new THREE.Group(); - const caveRadius = 5; - const caveHeight = 4; - // Cave floor - group.add(createGroundPlane(wetStoneMaterial, caveRadius * 2)); - // Cave Walls (Inverted cylinder or sphere) - // Using Sphere for more natural shape - const wallGeo = new THREE.SphereGeometry(caveRadius, 32, 16, 0, Math.PI*2, 0, Math.PI/1.5); // Partial sphere for walls/ceiling - const wallMat = wetStoneMaterial.clone(); - wallMat.side = THREE.BackSide; // Render interior - const wall = new THREE.Mesh(wallGeo, wallMat); - wall.position.y = caveHeight * 0.6; // Adjust vertical position - group.add(wall); - - // Stalactites/Stalagmites (cones) - const stalactiteGeo = new THREE.ConeGeometry(0.1, 0.8, 8); - const stalagmiteGeo = new THREE.ConeGeometry(0.15, 0.5, 8); - for(let i=0; i<15; i++){ - const x = (Math.random()-0.5)*caveRadius*1.5; - const z = (Math.random()-0.5)*caveRadius*1.5; - if(Math.random() > 0.5) { // Stalactite - group.add(createMesh(stalactiteGeo, wetStoneMaterial, {x:x, y: caveHeight - 0.4, z:z})) - } else { // Stalagmite - group.add(createMesh(stalagmiteGeo, wetStoneMaterial, {x:x, y: 0.25, z:z})) - } - } - - // Dripping water animation (simple sphere falling) - const dripGeo = new THREE.SphereGeometry(0.05, 8, 8); - for (let i = 0; i < 5; i++) { - const drip = createMesh(dripGeo, oceanMaterial, { x: (Math.random() - 0.5) * caveRadius, y: caveHeight - 0.2, z: (Math.random() - 0.5) * caveRadius }); - drip.userData.startY = caveHeight - 0.2; - drip.userData.update = (time) => { - drip.position.y -= 0.1; // Simple constant fall speed - if (drip.position.y < 0) { - drip.position.y = drip.userData.startY; // Reset drip - drip.position.x = (Math.random() - 0.5) * caveRadius; // Reset horizontal position too - drip.position.z = (Math.random() - 0.5) * caveRadius; - } - }; - group.add(drip); - } - // TODO: Add crystals? Water pool? Specific entrance visual? - return group; - } // ======================================== // Game Data // ======================================== const itemsData = { - "Flaming Sword":{type:"weapon", description:"A legendary blade, wreathed in magical fire."}, - "Whispering Bow":{type:"weapon", description:"Crafted by elves, its arrows fly almost silently."}, - "Guardian Shield":{type:"armor", description:"A sturdy shield imbued with protective enchantments."}, - "Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds."}, - "Shield of Faith Spell":{type:"spell", description:"A scroll containing a prayer that grants temporary magical protection."}, - "Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe."}, - "Secret Tunnel Map":{type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?"}, - "Poison Daggers":{type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin."}, - "Master Key":{type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all."}, - "Crude Dagger":{type:"weapon", description:"A roughly made dagger, chipped and stained."}, - "Scout's Pouch":{type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins."} - // TODO: Add more items (potions, armor pieces, quest items) + "Flaming Sword": {type:"weapon", description:"A legendary blade, wreathed in magical fire.", goldValue: 500}, + "Whispering Bow": {type:"weapon", description:"Crafted by elves, its arrows fly almost silently.", goldValue: 350}, + "Guardian Shield": {type:"armor", description:"A sturdy shield imbued with protective enchantments.", goldValue: 250}, + "Healing Light Spell":{type:"spell", description:"A scroll containing the incantation to mend minor wounds.", goldValue: 50}, + "Shield of Faith Spell":{type:"spell",description:"A scroll containing a prayer that grants temporary magical protection.", goldValue: 75}, + "Binding Runes Scroll":{type:"spell", description:"Complex runes scribbled on parchment, said to temporarily immobilize a foe.", goldValue: 100}, + "Secret Tunnel Map": {type:"quest", description:"A crudely drawn map showing a hidden path, perhaps into the fortress?", goldValue: 5}, // Quest items low value? + "Poison Daggers": {type:"weapon", description:"A pair of wicked-looking daggers coated in a fast-acting toxin.", goldValue: 150}, + "Master Key": {type:"quest", description:"An ornate key rumored to unlock many doors, though perhaps not all.", goldValue: 10}, + "Crude Dagger": {type:"weapon", description:"A roughly made dagger, chipped and stained.", goldValue: 10}, + "Scout's Pouch": {type:"quest", description:"A small leather pouch containing flint & steel, jerky, and some odd coins.", goldValue: 20} // Value includes contents + // TODO: Add more items }; const gameData = { @@ -829,95 +362,121 @@ "224": { title: "Sneak Success?", content:"

Moving like a shadow, you manage to slip past the gate guards unnoticed!

", options: [{text:"Enter the fortress courtyard", next: 99}], illustration:"approaching-dark-fortress-walls-guards"}, // TODO: Expand fortress interior // --- Game Over / Error State --- - "99": { title: "Game Over / To Be Continued...", content: "

Your adventure ends here (for now). Thanks for playing!

", options: [{ text: "Restart Adventure", next: 1 }], illustration: "game-over-generic", gameOver: true } + "99": { + title: "Game Over / To Be Continued...", + content: "

Your adventure ends here... for now. You can sell unwanted items for gold before starting again.

", // Modified content + // Options will be dynamically generated in renderPageInternal for selling + options: [ + // The restart button will be added last by renderPageInternal logic + ], + illustration: "game-over-generic", + gameOver: true, // Mark this page specifically for game over logic + allowSell: true // Add a flag to enable selling on this page + } }; // ======================================== // Game State // ======================================== + // Define default character state - used for first launch + const defaultCharacterState = { + name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", + level: 1, xp: 0, xpToNextLevel: 100, gold: 0, // Added gold + stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, + inventory: [] + }; + + // Initialize gameState let gameState = { currentPageId: 1, - character: { - name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", - level: 1, xp: 0, xpToNextLevel: 100, - stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, - inventory: [] - // TODO: Add equipment slots (weapon, armor, etc.) - // TODO: Add status effects array - } + // Deep copy default state initially to avoid modification issues + character: JSON.parse(JSON.stringify(defaultCharacterState)), + // Store sell feedback message temporarily + lastSellMessage: "" }; + // ======================================== // Game Logic Functions // ======================================== - function startGame() { - // Reset state if restarting - const defaultChar = { - name: "Hero", race: "Human", alignment: "Neutral Good", class: "Adventurer", - level: 1, xp: 0, xpToNextLevel: 100, - stats: { strength: 8, intelligence: 10, wisdom: 10, dexterity: 10, constitution: 10, charisma: 8, hp: 12, maxHp: 12 }, - inventory: [] - }; - // Deep copy necessary for nested objects like stats - gameState = { currentPageId: 1, character: JSON.parse(JSON.stringify(defaultChar)) }; - console.log("Starting new game with state:", JSON.stringify(gameState)); + + // Function to start a brand new game (resets everything) + function startNewGame() { + console.log("Starting brand new game..."); + // Reset state completely using the default + gameState = { + currentPageId: 1, + character: JSON.parse(JSON.stringify(defaultCharacterState)), + lastSellMessage: "" + }; + renderPage(gameState.currentPageId); + } + + // Function to restart, keeping character progress ("New Game Plus") + function restartGamePlus() { + console.log("Restarting game (keeping progress)..."); + gameState.currentPageId = 1; // Only reset the page ID + gameState.lastSellMessage = ""; // Clear any sell messages renderPage(gameState.currentPageId); } function handleChoiceClick(choiceData) { console.log("Choice clicked:", choiceData); + // --- Special Actions (Sell, Restart) --- + if (choiceData.action === 'restart_plus') { + restartGamePlus(); + return; + } + if (choiceData.action === 'sell_item') { + handleSellItem(choiceData.item); + return; + } + + // --- Standard Page Navigation --- const optionNextPageId = parseInt(choiceData.next); - const itemToAdd = choiceData.addItem; // Item from direct choice property (less common now) + const itemToAdd = choiceData.addItem; let nextPageId = optionNextPageId; - let rollResultMessage = ""; - const check = choiceData.check; // Skill check object { stat, dc, onFailure } - - // --- Input Validation --- - // Handle explicit restart command (typically from page 99) - if (choiceData.next === 1 && (pageData = gameData[gameState.currentPageId]) && pageData.gameOver) { - console.log("Restarting game..."); - startGame(); - return; - } - // Basic validation for normal choices + let rollResultMessage = gameState.lastSellMessage || ""; // Carry over sell message if any + gameState.lastSellMessage = ""; // Clear sell message after use + const check = choiceData.check; + + // --- Basic Input Validation --- if (isNaN(optionNextPageId) && !check) { console.error("Invalid choice data: Missing 'next' page ID and no check defined.", choiceData); - // Attempt to render current page again with an error, or go to game over const currentPageData = gameData[gameState.currentPageId] || gameData[99]; renderPageInternal(gameState.currentPageId, currentPageData , "

Error: Invalid choice data encountered! Cannot proceed.

"); - // Make choices inactive? choicesElement.querySelectorAll('button').forEach(b => b.disabled = true); return; } // --- Skill Check Logic --- if (check) { - const statValue = gameState.character.stats[check.stat] || 10; // Default to 10 if stat missing + const statValue = gameState.character.stats[check.stat] || 10; const modifier = Math.floor((statValue - 10) / 2); const roll = Math.floor(Math.random() * 20) + 1; const totalResult = roll + modifier; const dc = check.dc; - const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1); // Capitalize stat name + const statName = check.stat.charAt(0).toUpperCase() + check.stat.slice(1); console.log(`Check: ${statName} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${totalResult}`); if (totalResult >= dc) { // Success - nextPageId = optionNextPageId; // Proceed to the 'next' page defined in the option - rollResultMessage = `

${statName} Check Success! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})

`; + nextPageId = optionNextPageId; + rollResultMessage += `

${statName} Check Success! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})

`; } else { // Failure - nextPageId = parseInt(check.onFailure); // Go to the 'onFailure' page ID - rollResultMessage = `

${statName} Check Failed! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})

`; + nextPageId = parseInt(check.onFailure); + rollResultMessage += `

${statName} Check Failed! (Rolled ${roll} + ${modifier} = ${totalResult} vs DC ${dc})

`; if (isNaN(nextPageId)) { console.error("Invalid onFailure ID:", check.onFailure); - nextPageId = 99; // Default to game over on invalid failure ID + nextPageId = 99; rollResultMessage += "

Error: Invalid failure path defined!

"; } } } // --- Page Transition & Consequences --- - const targetPageData = gameData[nextPageId]; // Get data for the *next* page + const targetPageData = gameData[nextPageId]; if (!targetPageData) { console.error(`Data for target page ${nextPageId} not found!`); renderPageInternal(99, gameData[99] || { title: "Error", content: "

Page Data Missing!

", illustration: "error", gameOver: true }, "

Error: Next page data missing! Cannot continue.

"); @@ -931,33 +490,29 @@ gameState.character.stats.hp -= hpLostThisTurn; console.log(`Lost ${hpLostThisTurn} HP.`); } - // TODO: Implement hpGain (similar to hpLoss but adding HP) if (targetPageData.reward && targetPageData.reward.hpGain) { const hpGained = targetPageData.reward.hpGain; gameState.character.stats.hp += hpGained; console.log(`Gained ${hpGained} HP.`); - // Clamp HP to maxHP later } - // Check for death *after* applying HP changes for this turn + // Check for death *after* applying HP changes if (gameState.character.stats.hp <= 0) { - gameState.character.stats.hp = 0; // Don't go below 0 + gameState.character.stats.hp = 0; console.log("Player died!"); - nextPageId = 99; // Override navigation to game over page - rollResultMessage += `

You have succumbed to your injuries! (-${hpLostThisTurn} HP)

`; - // Ensure we use game over page data for rendering - const gameOverPageData = gameData[nextPageId]; - renderPageInternal(nextPageId, gameOverPageData, rollResultMessage); - return; // Stop further processing for this choice + nextPageId = 99; // Force navigation to game over page + rollResultMessage += `

You have succumbed to your injuries!${hpLostThisTurn > 0 ? ` (-${hpLostThisTurn} HP)` : ''}

`; + const gameOverPageData = gameData[nextPageId]; + renderPageInternal(nextPageId, gameOverPageData, rollResultMessage); + return; // Stop processing } - // Apply other rewards if the player is still alive + // Apply other rewards if alive if (targetPageData.reward) { if (targetPageData.reward.xp) { gameState.character.xp += targetPageData.reward.xp; console.log(`Gained ${targetPageData.reward.xp} XP! Total: ${gameState.character.xp}`); - // TODO: Check for Level Up - // checkLevelUp(); + // checkLevelUp(); // TODO } if (targetPageData.reward.statIncrease) { const stat = targetPageData.reward.statIncrease.stat; @@ -965,20 +520,15 @@ if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}. New value: ${gameState.character.stats[stat]}`); - // Recalculate Max HP immediately if Constitution changes - if (stat === 'constitution') { - recalculateMaxHp(); - } + if (stat === 'constitution') recalculateMaxHp(); } } - // Add item from reward property if (targetPageData.reward.addItem && !gameState.character.inventory.includes(targetPageData.reward.addItem)) { gameState.character.inventory.push(targetPageData.reward.addItem); console.log(`Found item: ${targetPageData.reward.addItem}`); - rollResultMessage += `

Item acquired: ${targetPageData.reward.addItem}

`; // Add feedback + rollResultMessage += `

Item acquired: ${targetPageData.reward.addItem}

`; } } - // Add item from direct choice property (less common, but handle for compatibility) if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); @@ -986,122 +536,139 @@ } // --- Update Game State --- - gameState.currentPageId = nextPageId; // Update current page *after* processing consequences - - // Recalculate derived stats (like Max HP) and clamp current values + gameState.currentPageId = nextPageId; recalculateMaxHp(); - gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp); // Clamp current HP + gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp); console.log("Transitioning to page:", nextPageId, " New state:", JSON.stringify(gameState)); - // Render the determined next page - renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage); // Use potentially overridden nextPageId + renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage); + } + + function handleSellItem(itemName) { + console.log("Attempting to sell:", itemName); + const itemIndex = gameState.character.inventory.indexOf(itemName); + const itemInfo = itemsData[itemName]; + + if (itemIndex !== -1 && itemInfo && itemInfo.goldValue > 0) { + const value = itemInfo.goldValue; + gameState.character.gold += value; + gameState.character.inventory.splice(itemIndex, 1); // Remove item + gameState.lastSellMessage = ``; + console.log(`Sold ${itemName} for ${value} gold. Current gold: ${gameState.character.gold}`); + } else { + console.warn("Could not sell item:", itemName, " - Item not found, no value, or invalid."); + gameState.lastSellMessage = `

Cannot sell ${itemName}.

`; // Use failure style for error feedback + } + // Re-render the current page (which should be the Game Over page '99') + renderPageInternal(gameState.currentPageId, gameData[gameState.currentPageId], gameState.lastSellMessage); } + function recalculateMaxHp() { const baseHp = 10; // Base HP for level 1 adventurer const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2); - // TODO: Incorporate level into HP calculation, e.g., baseHp + (level * conModifier) + other bonuses - gameState.character.stats.maxHp = baseHp + conModifier * gameState.character.level; - // Ensure HP is at least 1? - gameState.character.stats.maxHp = Math.max(1, gameState.character.stats.maxHp); + gameState.character.stats.maxHp = Math.max(1, baseHp + conModifier * gameState.character.level); // Ensure HP is at least 1 } - // TODO: function checkLevelUp() { ... } - function renderPageInternal(pageId, pageData, message = "") { - // --- Page Data Validation --- if (!pageData) { console.error(`Render Error: No data for page ${pageId}`); - pageData = gameData[99] || { title: "Error", content: "

Render Error! Critical page data missing.

", illustration: "error", gameOver: true }; // Fallback to error/game over + pageData = gameData[99] || { title: "Error", content: "

Render Error! Critical page data missing.

", illustration: "error", gameOver: true }; message += "

Render Error: Page data was missing! Cannot proceed.

"; - pageId = 99; // Ensure we treat this as the game over page + pageId = 99; } - console.log(`Rendering page ${pageId}: "${pageData.title}"`); - // --- Update UI Elements --- storyTitleElement.textContent = pageData.title || "Untitled Page"; - storyContentElement.innerHTML = message + (pageData.content || "

...

"); // Prepend messages (like roll results) + // Inject message first, then page content + storyContentElement.innerHTML = message + (pageData.content || "

...

"); updateStatsDisplay(); updateInventoryDisplay(); - choicesElement.innerHTML = ''; // Clear previous choices - // --- Generate Choices --- const options = pageData.options || []; - const isGameOverOrEnd = pageData.gameOver || (options.length === 0 && pageId !== 99); // Check if it's an end state + const isGameOverPage = pageData.gameOver === true; // Specifically check the gameOver flag + + // --- Generate Sell Buttons (Only on Game Over page if allowSell is true) --- + if (isGameOverPage && pageData.allowSell === true) { + const sellableItems = gameState.character.inventory.filter(itemName => { + const itemInfo = itemsData[itemName]; + return itemInfo && itemInfo.goldValue > 0 && itemInfo.type !== 'quest'; // Only sell non-quest items with value + }); + + if (sellableItems.length > 0) { + choicesElement.innerHTML += `

Sell Items:

`; // Add a header for sell options + sellableItems.forEach(itemName => { + const itemInfo = itemsData[itemName]; + const sellButton = document.createElement('button'); + sellButton.classList.add('choice-button', 'sell-button'); // Add specific class + sellButton.textContent = `Sell ${itemName} (${itemInfo.goldValue} Gold)`; + sellButton.onclick = () => handleChoiceClick({ action: 'sell_item', item: itemName }); + choicesElement.appendChild(sellButton); + }); + choicesElement.innerHTML += `
`; // Separator + } + } - if (!isGameOverOrEnd && pageId !== 99) { // Normal page with choices + + // --- Generate Standard Choices / Restart Button --- + if (!isGameOverPage && options.length > 0) { // Normal page with navigation choices options.forEach(option => { const button = document.createElement('button'); button.classList.add('choice-button'); button.textContent = option.text; let requirementMet = true; - let requirementText = []; // Store unmet requirements text + let requirementText = []; - // --- Requirement Checks --- - // Example: Check for required item if (option.requireItem) { if (!gameState.character.inventory.includes(option.requireItem)) { - requirementMet = false; - requirementText.push(`Requires: ${option.requireItem}`); + requirementMet = false; requirementText.push(`Requires: ${option.requireItem}`); } } - // Example: Check for required stat value if (option.requireStat) { const currentStat = gameState.character.stats[option.requireStat.stat] || 0; if (currentStat < option.requireStat.value) { - requirementMet = false; - requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`); + requirementMet = false; requirementText.push(`Requires: ${option.requireStat.stat.charAt(0).toUpperCase() + option.requireStat.stat.slice(1)} ${option.requireStat.value}`); } } - // TODO: Add checks for light source, specific quest flags etc. - // --- Button State and Click Handler --- button.disabled = !requirementMet; - if (!requirementMet) { - button.title = requirementText.join(', '); // Show all unmet requirements on hover - button.classList.add('disabled'); - } else { - // Attach click handler only if requirements met - const choiceData = { - next: option.next, - addItem: option.addItem, // If defined directly on option - check: option.check // Skill check associated with option - }; + if (!requirementMet) button.title = requirementText.join(', '); + else { + const choiceData = { next: option.next, addItem: option.addItem, check: option.check }; button.onclick = () => handleChoiceClick(choiceData); } choicesElement.appendChild(button); }); - } else { // Game Over or defined end of branch - const button = document.createElement('button'); - button.classList.add('choice-button'); - // Use specific text for game over page, different for other end pages - button.textContent = (pageId === 99 && pageData.gameOver) ? "Restart Adventure" : "The Path Ends Here (Restart)"; - // The 'next: 1' ensures it triggers the restart logic in handleChoiceClick - button.onclick = () => handleChoiceClick({ next: 1 }); - choicesElement.appendChild(button); - if (pageId !== 99) { // Add message only if it's not the explicit game over page - choicesElement.insertAdjacentHTML('afterbegin', '

There are no further paths from here.

'); - } + } else if (isGameOverPage) { // Game Over page needs restart button + const restartButton = document.createElement('button'); + restartButton.classList.add('choice-button'); + restartButton.textContent = "Restart Adventure (Keep Progress)"; + // Use the specific restart_plus action + restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); + choicesElement.appendChild(restartButton); + } else { // End of a branch (not game over page 99), offer restart + choicesElement.insertAdjacentHTML('beforeend', '

There are no further paths from here.

'); + const restartButton = document.createElement('button'); + restartButton.classList.add('choice-button'); + restartButton.textContent = "Restart Adventure (Keep Progress)"; + restartButton.onclick = () => handleChoiceClick({ action: 'restart_plus' }); + choicesElement.appendChild(restartButton); } - // --- Update 3D Scene --- updateScene(pageData.illustration || 'default'); } - function renderPage(pageId) { - // Simple wrapper, could add pre/post render logic here if needed - renderPageInternal(pageId, gameData[pageId]); - } + function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); } function updateStatsDisplay() { const char=gameState.character; - statsElement.innerHTML = `Stats: Lvl: ${char.level} XP: ${char.xp}/${char.xpToNextLevel} HP: ${char.stats.hp}/${char.stats.maxHp} Str: ${char.stats.strength} Int: ${char.stats.intelligence} Wis: ${char.stats.wisdom} Dex: ${char.stats.dexterity} Con: ${char.stats.constitution} Cha: ${char.stats.charisma}`; + // Added Gold display + statsElement.innerHTML = `Stats: Gold: ${char.gold} Lvl: ${char.level} XP: ${char.xp}/${char.xpToNextLevel} HP: ${char.stats.hp}/${char.stats.maxHp} Str: ${char.stats.strength} Int: ${char.stats.intelligence} Wis: ${char.stats.wisdom} Dex: ${char.stats.dexterity} Con: ${char.stats.constitution} Cha: ${char.stats.charisma}`; } - function updateInventoryDisplay() { + function updateInventoryDisplay() { // Unchanged let h='Inventory: '; if(gameState.character.inventory.length === 0){ h+='Empty'; @@ -1109,285 +676,19 @@ gameState.character.inventory.forEach(itemName=>{ const item = itemsData[itemName] || {type:'unknown',description:'An unknown item.'}; const itemClass = `item-${item.type || 'unknown'}`; - // Ensure description is a string const descriptionText = typeof item.description === 'string' ? item.description : 'No description available.'; - h += `${itemName}`; // Escape quotes in title + h += `${itemName}`; }); } inventoryElement.innerHTML = h; } - function updateScene(illustrationKey) { - if (!scene) { - console.warn("Scene not initialized, cannot update visual."); - return; - } - console.log("Updating scene for illustration key:", illustrationKey); - - // --- Cleanup --- - if (currentAssemblyGroup) { - scene.remove(currentAssemblyGroup); - // Basic disposal - more complex scenes might need deeper disposal - currentAssemblyGroup.traverse(child => { - if (child.isMesh) { - child.geometry.dispose(); - // Dispose materials if they are unique to this assembly and not reused - // if (Array.isArray(child.material)) { - // child.material.forEach(m => m.dispose()); - // } else { - // child.material.dispose(); - // } - } - }); - currentAssemblyGroup = null; - } - scene.fog = null; // Reset fog - scene.background = new THREE.Color(0x222222); // Reset background - // Reset camera to default unless overridden by case - camera.position.set(0, 2.5, 7); - camera.lookAt(0, 0.5, 0); - - // --- Select Assembly --- - let assemblyFunction; - switch (illustrationKey) { - // Basic Structures - case 'city-gates': assemblyFunction = createCityGatesAssembly; break; - case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break; - case 'temple': assemblyFunction = createTempleAssembly; break; - case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break; - case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break; - case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; - case 'error': assemblyFunction = createErrorAssembly; break; - - // Landscape / Outdoor Scenes - case 'crossroads-signpost-sunny': - scene.fog = new THREE.Fog(0x87CEEB, 10, 35); camera.position.set(0, 3, 10); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x87CEEB); - assemblyFunction = createCrossroadsAssembly; break; - case 'rolling-green-hills-shepherd-distance': - case 'hilltop-view-overgrown-shrine-wildflowers': // Base visual is hills - case 'overgrown-stone-shrine-wildflowers-close': // Base visual is hills - scene.fog = new THREE.Fog(0xA8E4A0, 15, 50); camera.position.set(0, 5, 15); camera.lookAt(0, 2, -5); scene.background = new THREE.Color(0x90EE90); - // Adjust camera for close-up shrine views specifically - if (illustrationKey === 'overgrown-stone-shrine-wildflowers-close') camera.position.set(1, 2, 4); - if (illustrationKey === 'hilltop-view-overgrown-shrine-wildflowers') camera.position.set(3, 4, 8); - assemblyFunction = createRollingHillsAssembly; break; - case 'windy-sea-cliffs-crashing-waves-path-down': - case 'scanning-sea-cliffs-no-other-paths-visible': // Reuse visual - case 'close-up-handholds-carved-in-cliff-face': // Reuse visual - scene.fog = new THREE.Fog(0x6699CC, 10, 40); camera.position.set(5, 5, 10); camera.lookAt(-2, 0, -5); scene.background = new THREE.Color(0x6699CC); - assemblyFunction = createCoastalCliffsAssembly; break; - case 'hidden-cove-beach-dark-cave-entrance': - case 'character-fallen-at-bottom-of-cliff-path-cove': // Reuse visual - scene.fog = new THREE.Fog(0x336699, 5, 30); camera.position.set(0, 2, 8); camera.lookAt(0, 1, -2); scene.background = new THREE.Color(0x336699); - assemblyFunction = createHiddenCoveAssembly; break; - case 'rocky-badlands-cracked-earth-harsh-sun': - scene.fog = new THREE.Fog(0xD2B48C, 15, 40); camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0xCD853F); - assemblyFunction = createDefaultAssembly; break; // Placeholder - Needs badlands assembly - - // Forest Scenes - case 'shadowwood-forest': // Generic forest, use if needed - scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); - assemblyFunction = createForestAssembly; break; - case 'dark-forest-entrance-gnarled-roots-filtered-light': - scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); - assemblyFunction = createForestEntranceAssembly; break; - case 'overgrown-forest-path-glowing-fungi-vines': - case 'pushing-through-forest-undergrowth': // Reuse visual - scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x112211); - assemblyFunction = createOvergrownPathAssembly; break; - case 'forest-clearing-mossy-statue-weathered-stone': - case 'forest-clearing-mossy-statue-hidden-compartment': // Reuse visual - case 'forest-clearing-mossy-statue-offering': // Reuse visual - scene.fog = new THREE.Fog(0x2E4F3A, 5, 25); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x223322); - assemblyFunction = createClearingStatueAssembly; break; - case 'narrow-game-trail-forest-rope-bridge-ravine': - case 'character-crossing-rope-bridge-safely': // Reuse visual - case 'rope-bridge-snapping-character-falling': // Reuse visual - case 'fallen-log-crossing-ravine': // Reuse visual (needs log added?) - scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(2, 3, 6); camera.lookAt(0, -1, -2); scene.background = new THREE.Color(0x1A1A1A); - assemblyFunction = createForestAssembly; break; // TODO: Needs bridge/ravine elements - case 'two-goblins-ambush-forest-path-spears': - case 'forest-shadows-hiding-goblins-walking-past': // Reuse visual, maybe different camera? - case 'defeated-goblins-forest-path-loot': // Reuse visual, remove goblins? - case 'blurred-motion-running-past-goblins-forest': // Reuse visual - scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 2, 7); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x112211); - assemblyFunction = createGoblinAmbushAssembly; break; // TODO: Modify based on state - case 'forest-stream-crossing-dappled-sunlight-stones': - case 'mossy-log-bridge-over-forest-stream': // Needs log added? - case 'character-splashing-into-stream-from-log': // Reuse visual - scene.fog = new THREE.Fog(0x668866, 8, 25); camera.position.set(0, 2, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x446644); - if (illustrationKey === 'mossy-log-bridge-over-forest-stream') camera.position.set(1, 2, 5); - assemblyFunction = createForestAssembly; break; // TODO: Needs stream/log elements - case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': - case 'forest-edge': // Explicit key for this scene - scene.fog = new THREE.Fog(0xAAAAAA, 10, 40); camera.position.set(0, 3, 10); camera.lookAt(0, 1, -5); scene.background = new THREE.Color(0x888888); - assemblyFunction = createForestEdgeAssembly; break; - - // Mountain / Fortress Scenes - case 'climbing-rocky-foothills-path-fortress-closer': - case 'rockslide-blocking-mountain-path-boulders': - case 'character-climbing-over-boulders': - case 'character-slipping-on-rockslide-boulders': - case 'rough-detour-path-around-rockslide': - scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(0, 4, 9); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x708090); - assemblyFunction = createDefaultAssembly; break; // Placeholder - Needs rocky foothills assembly - case 'zoomed-view-mountain-fortress-western-ridge': - scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(5, 6, 12); camera.lookAt(-2, 3, -5); scene.background = new THREE.Color(0x708090); - assemblyFunction = createDefaultAssembly; break; // Placeholder - case 'narrow-goat-trail-mountainside-fortress-view': - scene.fog = new THREE.Fog(0x778899, 5, 30); camera.position.set(1, 3, 6); camera.lookAt(0, 2, -2); scene.background = new THREE.Color(0x708090); - assemblyFunction = createDefaultAssembly; break; // Placeholder - case 'narrow-windy-mountain-ridge-path': - case 'character-falling-off-windy-ridge': - scene.fog = new THREE.Fog(0x8899AA, 6, 25); camera.position.set(2, 5, 7); camera.lookAt(0, 3, -3); scene.background = new THREE.Color(0x778899); - assemblyFunction = createDefaultAssembly; break; // Placeholder - case 'approaching-dark-fortress-walls-guards': - scene.fog = new THREE.Fog(0x444455, 5, 20); camera.position.set(0, 3, 8); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x333344); - assemblyFunction = createDefaultAssembly; break; // Placeholder - Needs fortress walls assembly - - // Indoor Scenes - case 'dark-cave-entrance-dripping-water': - scene.fog = new THREE.Fog(0x1A1A1A, 2, 10); camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x111111); - assemblyFunction = createDarkCaveAssembly; break; - - // Default / Fallback - default: - console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); - assemblyFunction = createDefaultAssembly; break; - } + // --- Scene Update and Lighting --- (Unchanged updateScene, adjustLighting) + function updateScene(illustrationKey) { if (!scene) { console.warn("Scene not initialized, cannot update visual."); return; } console.log("Updating scene for illustration key:", illustrationKey); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); currentAssemblyGroup.traverse(child => { if (child.isMesh) { child.geometry.dispose(); } }); currentAssemblyGroup = null; } scene.fog = null; scene.background = new THREE.Color(0x222222); camera.position.set(0, 2.5, 7); camera.lookAt(0, 0.5, 0); let assemblyFunction; switch (illustrationKey) { case 'city-gates': assemblyFunction = createCityGatesAssembly; break; case 'weaponsmith': assemblyFunction = createWeaponsmithAssembly; break; case 'temple': assemblyFunction = createTempleAssembly; break; case 'resistance-meeting': assemblyFunction = createResistanceMeetingAssembly; break; case 'prisoner-cell': assemblyFunction = createPrisonerCellAssembly; break; case 'game-over': case 'game-over-generic': assemblyFunction = createGameOverAssembly; break; case 'error': assemblyFunction = createErrorAssembly; break; case 'crossroads-signpost-sunny': scene.fog = new THREE.Fog(0x87CEEB, 10, 35); camera.position.set(0, 3, 10); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x87CEEB); assemblyFunction = createCrossroadsAssembly; break; case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': scene.fog = new THREE.Fog(0xA8E4A0, 15, 50); camera.position.set(0, 5, 15); camera.lookAt(0, 2, -5); scene.background = new THREE.Color(0x90EE90); if (illustrationKey === 'overgrown-stone-shrine-wildflowers-close') camera.position.set(1, 2, 4); if (illustrationKey === 'hilltop-view-overgrown-shrine-wildflowers') camera.position.set(3, 4, 8); assemblyFunction = createRollingHillsAssembly; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'scanning-sea-cliffs-no-other-paths-visible': case 'close-up-handholds-carved-in-cliff-face': scene.fog = new THREE.Fog(0x6699CC, 10, 40); camera.position.set(5, 5, 10); camera.lookAt(-2, 0, -5); scene.background = new THREE.Color(0x6699CC); assemblyFunction = createCoastalCliffsAssembly; break; case 'hidden-cove-beach-dark-cave-entrance': case 'character-fallen-at-bottom-of-cliff-path-cove': scene.fog = new THREE.Fog(0x336699, 5, 30); camera.position.set(0, 2, 8); camera.lookAt(0, 1, -2); scene.background = new THREE.Color(0x336699); assemblyFunction = createHiddenCoveAssembly; break; case 'rocky-badlands-cracked-earth-harsh-sun': scene.fog = new THREE.Fog(0xD2B48C, 15, 40); camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0xCD853F); assemblyFunction = createDefaultAssembly; break; case 'shadowwood-forest': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'dark-forest-entrance-gnarled-roots-filtered-light': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestEntranceAssembly; break; case 'overgrown-forest-path-glowing-fungi-vines': case 'pushing-through-forest-undergrowth': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 1.5, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createOvergrownPathAssembly; break; case 'forest-clearing-mossy-statue-weathered-stone': case 'forest-clearing-mossy-statue-hidden-compartment': case 'forest-clearing-mossy-statue-offering': scene.fog = new THREE.Fog(0x2E4F3A, 5, 25); camera.position.set(0, 2, 5); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x223322); assemblyFunction = createClearingStatueAssembly; break; case 'narrow-game-trail-forest-rope-bridge-ravine': case 'character-crossing-rope-bridge-safely': case 'rope-bridge-snapping-character-falling': case 'fallen-log-crossing-ravine': scene.fog = new THREE.Fog(0x2E2E2E, 5, 20); camera.position.set(2, 3, 6); camera.lookAt(0, -1, -2); scene.background = new THREE.Color(0x1A1A1A); assemblyFunction = createForestAssembly; break; case 'two-goblins-ambush-forest-path-spears': case 'forest-shadows-hiding-goblins-walking-past': case 'defeated-goblins-forest-path-loot': case 'blurred-motion-running-past-goblins-forest': scene.fog = new THREE.Fog(0x1A2F2A, 3, 15); camera.position.set(0, 2, 7); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x112211); assemblyFunction = createGoblinAmbushAssembly; break; case 'forest-stream-crossing-dappled-sunlight-stones': case 'mossy-log-bridge-over-forest-stream': case 'character-splashing-into-stream-from-log': scene.fog = new THREE.Fog(0x668866, 8, 25); camera.position.set(0, 2, 6); camera.lookAt(0, 0.5, 0); scene.background = new THREE.Color(0x446644); if (illustrationKey === 'mossy-log-bridge-over-forest-stream') camera.position.set(1, 2, 5); assemblyFunction = createForestAssembly; break; case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': case 'forest-edge': scene.fog = new THREE.Fog(0xAAAAAA, 10, 40); camera.position.set(0, 3, 10); camera.lookAt(0, 1, -5); scene.background = new THREE.Color(0x888888); assemblyFunction = createForestEdgeAssembly; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'rockslide-blocking-mountain-path-boulders': case 'character-climbing-over-boulders': case 'character-slipping-on-rockslide-boulders': case 'rough-detour-path-around-rockslide': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(0, 4, 9); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'zoomed-view-mountain-fortress-western-ridge': scene.fog = new THREE.Fog(0x778899, 8, 35); camera.position.set(5, 6, 12); camera.lookAt(-2, 3, -5); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-goat-trail-mountainside-fortress-view': scene.fog = new THREE.Fog(0x778899, 5, 30); camera.position.set(1, 3, 6); camera.lookAt(0, 2, -2); scene.background = new THREE.Color(0x708090); assemblyFunction = createDefaultAssembly; break; case 'narrow-windy-mountain-ridge-path': case 'character-falling-off-windy-ridge': scene.fog = new THREE.Fog(0x8899AA, 6, 25); camera.position.set(2, 5, 7); camera.lookAt(0, 3, -3); scene.background = new THREE.Color(0x778899); assemblyFunction = createDefaultAssembly; break; case 'approaching-dark-fortress-walls-guards': scene.fog = new THREE.Fog(0x444455, 5, 20); camera.position.set(0, 3, 8); camera.lookAt(0, 2, 0); scene.background = new THREE.Color(0x333344); assemblyFunction = createDefaultAssembly; break; case 'dark-cave-entrance-dripping-water': scene.fog = new THREE.Fog(0x1A1A1A, 2, 10); camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0); scene.background = new THREE.Color(0x111111); assemblyFunction = createDarkCaveAssembly; break; default: console.warn(`Unknown illustration key: "${illustrationKey}". Using default scene.`); assemblyFunction = createDefaultAssembly; break; } try { currentAssemblyGroup = assemblyFunction(); if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { scene.add(currentAssemblyGroup); adjustLighting(illustrationKey); } else { throw new Error("Assembly function did not return a valid THREE.Group."); } } catch (error) { console.error(`Error creating assembly for ${illustrationKey}:`, error); if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); } currentAssemblyGroup = createErrorAssembly(); scene.add(currentAssemblyGroup); adjustLighting('error'); } onWindowResize(); } + function adjustLighting(illustrationKey) { if (!scene) return; const lightsToRemove = scene.children.filter(child => child.isLight && !child.isAmbientLight); lightsToRemove.forEach(light => scene.remove(light)); const ambient = scene.children.find(c => c.isAmbientLight); if (!ambient) { console.warn("No ambient light found, adding default."); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); } let directionalLight; let lightIntensity = 1.2; let ambientIntensity = 0.5; let lightColor = 0xffffff; let lightPosition = { x: 8, y: 15, z: 10 }; switch (illustrationKey) { case 'crossroads-signpost-sunny': case 'rolling-green-hills-shepherd-distance': case 'hilltop-view-overgrown-shrine-wildflowers': case 'overgrown-stone-shrine-wildflowers-close': ambientIntensity = 0.7; lightIntensity = 1.5; lightColor = 0xFFF8E1; lightPosition = { x: 10, y: 15, z: 10 }; break; case 'shadowwood-forest': case 'dark-forest-entrance-gnarled-roots-filtered-light': case 'overgrown-forest-path-glowing-fungi-vines': case 'forest-clearing-mossy-statue-weathered-stone': case 'narrow-game-trail-forest-rope-bridge-ravine': case 'two-goblins-ambush-forest-path-spears': case 'forest-stream-crossing-dappled-sunlight-stones': case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = { x: 5, y: 12, z: 5 }; break; case 'dark-cave-entrance-dripping-water': ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = { x: 0, y: 5, z: 3 }; break; case 'prisoner-cell': ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = { x: 0, y: 10, z: 5 }; break; case 'windy-sea-cliffs-crashing-waves-path-down': case 'hidden-cove-beach-dark-cave-entrance': ambientIntensity = 0.6; lightIntensity = 1.0; lightColor = 0xCCDDFF; lightPosition = { x: -10, y: 12, z: 8 }; break; case 'rocky-badlands-cracked-earth-harsh-sun': ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = { x: 5, y: 20, z: 5 }; break; case 'climbing-rocky-foothills-path-fortress-closer': case 'zoomed-view-mountain-fortress-western-ridge': case 'narrow-goat-trail-mountainside-fortress-view': case 'narrow-windy-mountain-ridge-path': case 'approaching-dark-fortress-walls-guards': ambientIntensity = 0.5; lightIntensity = 1.3; lightColor = 0xDDEEFF; lightPosition = { x: 10, y: 18, z: 15 }; break; case 'game-over': case 'game-over-generic': ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = { x: 0, y: 5, z: 5 }; break; case 'error': ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = { x: 0, y: 5, z: 5 }; break; default: ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = { x: 8, y: 15, z: 10 }; break; } const currentAmbient = scene.children.find(c => c.isAmbientLight); if (currentAmbient) { currentAmbient.intensity = ambientIntensity; } directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); directionalLight.castShadow = true; directionalLight.shadow.mapSize.set(1024, 1024); directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; directionalLight.shadow.bias = -0.001; scene.add(directionalLight); } - // --- Create and Add Assembly --- - try { - currentAssemblyGroup = assemblyFunction(); - if (currentAssemblyGroup && currentAssemblyGroup.isGroup) { // Check if it's a valid group - scene.add(currentAssemblyGroup); - adjustLighting(illustrationKey); // Adjust lighting based on the final scene key - } else { - throw new Error("Assembly function did not return a valid THREE.Group."); - } - } catch (error) { - console.error(`Error creating assembly for ${illustrationKey}:`, error); - if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); } // Clean up potential partial assembly - currentAssemblyGroup = createErrorAssembly(); // Display error cone - scene.add(currentAssemblyGroup); - adjustLighting('error'); // Use default/error lighting - } - onWindowResize(); // Ensure camera aspect is correct after potential changes - } - - function adjustLighting(illustrationKey) { - if (!scene) return; - // Remove existing non-ambient lights first - const lightsToRemove = scene.children.filter(child => child.isLight && !child.isAmbientLight); - lightsToRemove.forEach(light => scene.remove(light)); - - const ambient = scene.children.find(c => c.isAmbientLight); - if (!ambient) { - console.warn("No ambient light found in scene, adding default."); - scene.add(new THREE.AmbientLight(0xffffff, 0.5)); // Add default if missing - } - - let directionalLight; - let lightIntensity = 1.2; - let ambientIntensity = 0.5; - let lightColor = 0xffffff; - let lightPosition = {x: 8, y: 15, z: 10}; - - // Adjust lighting based on scene type (match cases in updateScene) - switch (illustrationKey) { - // Sunny / Open Areas - case 'crossroads-signpost-sunny': - case 'rolling-green-hills-shepherd-distance': - case 'hilltop-view-overgrown-shrine-wildflowers': - case 'overgrown-stone-shrine-wildflowers-close': - ambientIntensity = 0.7; lightIntensity = 1.5; lightColor = 0xFFF8E1; lightPosition = {x: 10, y: 15, z: 10}; break; // Bright, warm light - // Forest / Dim Light - case 'shadowwood-forest': - case 'dark-forest-entrance-gnarled-roots-filtered-light': - case 'overgrown-forest-path-glowing-fungi-vines': - case 'forest-clearing-mossy-statue-weathered-stone': - case 'narrow-game-trail-forest-rope-bridge-ravine': - case 'two-goblins-ambush-forest-path-spears': - case 'forest-stream-crossing-dappled-sunlight-stones': - case 'forest-edge-view-rocky-foothills-distant-mountain-fortress': // Edge might be brighter - ambientIntensity = 0.4; lightIntensity = 0.8; lightColor = 0xB0C4DE; lightPosition = {x: 5, y: 12, z: 5}; break; // Dimmer, slightly blue/green filtered light - // Caves / Dark Indoors - case 'dark-cave-entrance-dripping-water': - ambientIntensity = 0.1; lightIntensity = 0.3; lightColor = 0x667799; lightPosition = {x: 0, y: 5, z: 3}; break; // Very dim, cool light from entrance - case 'prisoner-cell': - ambientIntensity = 0.2; lightIntensity = 0.5; lightColor = 0x7777AA; lightPosition = {x: 0, y: 10, z: 5}; break; // Gloomy, cold light, maybe top-down - // Coastal / Overcast - case 'windy-sea-cliffs-crashing-waves-path-down': - case 'hidden-cove-beach-dark-cave-entrance': - ambientIntensity = 0.6; lightIntensity = 1.0; lightColor = 0xCCDDFF; lightPosition = {x: -10, y: 12, z: 8}; break; // Bright but cool/diffused coastal light - // Badlands / Harsh Light - case 'rocky-badlands-cracked-earth-harsh-sun': - ambientIntensity = 0.7; lightIntensity = 1.8; lightColor = 0xFFFFDD; lightPosition = {x: 5, y: 20, z: 5}; break; // Very bright, slightly yellow harsh sunlight - // Mountains / Fortress Approach - case 'climbing-rocky-foothills-path-fortress-closer': - case 'zoomed-view-mountain-fortress-western-ridge': - case 'narrow-goat-trail-mountainside-fortress-view': - case 'narrow-windy-mountain-ridge-path': - case 'approaching-dark-fortress-walls-guards': - ambientIntensity = 0.5; lightIntensity = 1.3; lightColor = 0xDDEEFF; lightPosition = {x: 10, y: 18, z: 15}; break; // Clear mountain air light, slightly cool - // Special States - case 'game-over': case 'game-over-generic': - ambientIntensity = 0.2; lightIntensity = 0.8; lightColor = 0xFF6666; lightPosition = {x: 0, y: 5, z: 5}; break; // Reddish tint for game over - case 'error': - ambientIntensity = 0.4; lightIntensity = 1.0; lightColor = 0xFFCC00; lightPosition = {x: 0, y: 5, z: 5}; break; // Orange/Yellow tint for error - default: // Default lighting fallback - ambientIntensity = 0.5; lightIntensity = 1.2; lightColor = 0xffffff; lightPosition = {x: 8, y: 15, z: 10}; break; - } - - // Update ambient light - const currentAmbient = scene.children.find(c => c.isAmbientLight); - if (currentAmbient) { - currentAmbient.intensity = ambientIntensity; - } - - // Add new directional light - directionalLight = new THREE.DirectionalLight(lightColor, lightIntensity); - directionalLight.position.set(lightPosition.x, lightPosition.y, lightPosition.z); - directionalLight.castShadow = true; - // Configure shadow properties (adjust map size and camera frustum as needed) - directionalLight.shadow.mapSize.set(1024, 1024); // Or 2048 for better quality - directionalLight.shadow.camera.near = 0.5; - directionalLight.shadow.camera.far = 50; // Adjust based on typical scene scale - directionalLight.shadow.camera.left = -20; // Adjust frustum size based on scene scale - directionalLight.shadow.camera.right = 20; - directionalLight.shadow.camera.top = 20; - directionalLight.shadow.camera.bottom = -20; - directionalLight.shadow.bias = -0.001; // Adjust shadow bias to prevent artifacts - scene.add(directionalLight); - - // Optional: Add a light helper for debugging - // const helper = new THREE.DirectionalLightHelper( directionalLight, 5 ); - // scene.add( helper ); - // const shadowHelper = new THREE.CameraHelper( directionalLight.shadow.camera ); - // scene.add( shadowHelper ); - } - - // ======================================== - // Potential Future Improvements (Comment Block) - // ======================================== - /* - Potential Areas for Expansion/Improvement: - - * More Scene Variety: Add more create...Assembly functions and corresponding illustration keys for greater visual diversity (e.g., villages, ruins, different cave types, interiors). - * More Complex Scenes: The current scenes are quite abstract. More detail could be added, potentially involving more complex geometry generation (e.g., procedural buildings, terrain algorithms) or even simple pre-made models loaded for key elements (like the statue, specific monsters). - * Interaction: Add interaction with the 3D scene (e.g., clicking on objects using raycasting to examine them, pick up items visually, or activate levers). - * Combat System: Implement a more detailed combat mechanic instead of abstract resolution (e.g., turn-based, display enemy models, track enemy HP, use stats for attack/damage rolls, visual feedback for hits/misses). - * Character Progression: Implement a level-up system based on XP (increase stats, HP, unlock abilities/spells). Check for level up after gaining XP. - * Save/Load: Add functionality to save and load game progress (perhaps using `localStorage` to store the `gameState` object as a JSON string). Add save/load buttons to the UI. - * Data Management: For a larger game, storing `gameData` and `itemsData` in external JSON files and fetching them (`Workspace` API) would be more manageable than keeping them inline. - * Error Handling: Add more robust checks for missing data (items, pages) or potential errors during scene generation and game logic. Improve feedback on errors (e.g., specific messages instead of just going to page 99). - * Code Organization: Split the JavaScript into modules (e.g., `three-setup.js`, `game-logic.js`, `ui-manager.js`, `scene-generator.js`, `data.js`) for better maintainability using ES6 modules and imports. - * Refine Scene Logic: Improve how scene elements correspond to game state (e.g., actually remove defeated goblin models from the scene, visually represent items found on the ground before pickup, change scene lighting based on time of day if implemented). - * Add More Mechanics: Implement status effects (poison, bless), equipment slots (weapon, armor, shield) with stat modifiers, spells with costs (mana/charges), currency/shops, more complex skill checks (e.g., opposed checks, checks with advantage/disadvantage based on items/status), quest tracking system. - * Accessibility: Review and improve accessibility (ARIA attributes for dynamic content updates, keyboard navigation for choices). - * Performance: For very complex scenes or many objects, consider optimizing geometry (instancing similar objects like trees/rocks), using lower-poly models, texture atlases, and optimizing the render loop. Dispose of unused Three.js objects properly. - * Sound/Music: Add background music loops and sound effects for actions, ambiance, and feedback. - */ + // --- Potential Future Improvements Comment --- (Unchanged) + /* [Keep comment block here] */ // ======================================== // Initialization @@ -1397,23 +698,20 @@ try { initThreeJS(); if (scene && camera && renderer) { - startGame(); // Start the game logic only after successful Three.js init + // Use startNewGame for the very first load + startNewGame(); console.log("Game Started Successfully."); } else { - // If initThreeJS failed but didn't throw, or refs are null throw new Error("Three.js initialization failed or did not complete."); } } catch (error) { console.error("Initialization failed:", error); - // Display user-friendly error message in the UI storyTitleElement.textContent = "Initialization Error"; storyContentElement.innerHTML = `

A critical error occurred during setup. The adventure cannot begin. Please check the console (F12) for technical details.

${error.stack || error}
`; choicesElement.innerHTML = '

Cannot proceed due to initialization error.

'; - // Attempt to clear or show error in the 3D view area if (sceneContainer) { sceneContainer.innerHTML = '

3D Scene Failed to Load

'; } - // Prevent further Three.js operations if it failed scene = null; camera = null; renderer = null; } });