Spaces:
Running
Running
Update index.html
Browse files- index.html +53 -44
index.html
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>Persistent Procedural World</title>
|
7 |
<style>
|
8 |
body { font-family: 'Courier New', monospace; background-color: #111; color: #eee; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
|
9 |
#game-container { display: flex; flex-grow: 1; overflow: hidden; }
|
@@ -36,7 +36,7 @@
|
|
36 |
.message-failure { color: #f88; border-left-color: #a44; }
|
37 |
.message-info { color: #aaa; border-left-color: #666; }
|
38 |
.message-item { color: #8bf; border-left-color: #46a; }
|
39 |
-
#action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: none; }
|
40 |
|
41 |
</style>
|
42 |
</head>
|
@@ -68,7 +68,7 @@
|
|
68 |
|
69 |
<script type="module">
|
70 |
import * as THREE from 'three';
|
71 |
-
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
72 |
|
73 |
const sceneContainer = document.getElementById('scene-container');
|
74 |
const storyTitleElement = document.getElementById('story-title');
|
@@ -78,12 +78,12 @@
|
|
78 |
const inventoryElement = document.getElementById('inventory-display');
|
79 |
const actionInfoElement = document.getElementById('action-info');
|
80 |
|
81 |
-
|
82 |
let scene, camera, renderer, clock, controls, raycaster, mouse;
|
83 |
-
let worldGroup = null;
|
84 |
-
let locationGroups = {};
|
85 |
-
let currentMessage = "";
|
86 |
-
let currentPlacingItem = null;
|
|
|
87 |
|
88 |
const MAT = {
|
89 |
stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }),
|
@@ -127,7 +127,7 @@
|
|
127 |
controls.enableDamping = true;
|
128 |
controls.dampingFactor = 0.1;
|
129 |
controls.target.set(0, 1, 0);
|
130 |
-
controls.maxPolarAngle = Math.PI / 2 - 0.05;
|
131 |
|
132 |
window.addEventListener('resize', onWindowResize, false);
|
133 |
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
|
@@ -146,8 +146,9 @@
|
|
146 |
}
|
147 |
|
148 |
function onMouseMove( event ) {
|
149 |
-
|
150 |
-
mouse.
|
|
|
151 |
}
|
152 |
|
153 |
function onMouseClick( event ) {
|
@@ -183,12 +184,14 @@
|
|
183 |
const ground = new THREE.Mesh(geo, material);
|
184 |
ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
|
185 |
ground.receiveShadow = true; ground.castShadow = false;
|
186 |
-
ground.userData.isGround = true;
|
187 |
return ground;
|
188 |
}
|
189 |
|
190 |
function setupLighting(type = 'default') {
|
191 |
-
currentLights.forEach(light =>
|
|
|
|
|
192 |
currentLights = [];
|
193 |
|
194 |
let ambientIntensity = 0.4;
|
@@ -231,14 +234,14 @@
|
|
231 |
}
|
232 |
}
|
233 |
|
234 |
-
|
235 |
const group = new THREE.Group();
|
236 |
group.add(createGround(MAT.dirt, 20));
|
237 |
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
|
238 |
const interactBox = createMesh(boxGeo, MAT.stone, {y: 0.5, x: 2, z: 2});
|
239 |
interactBox.userData = { isPickupable: true, itemName: "Mysterious Cube", description: "A plain stone cube."};
|
240 |
group.add(interactBox);
|
241 |
-
group.visible = false;
|
242 |
return { group, lighting: 'default', entryText: "You are in a default, featureless area.", cameraPos: {x:0, y:5, z:10}, targetPos: {x:0, y:1, z:0} };
|
243 |
}
|
244 |
|
@@ -287,7 +290,6 @@
|
|
287 |
return { group, lighting: 'cave', entryText: "It's dark and damp in here.", cameraPos: {x:0, y:4, z:8}, targetPos: {x:0, y:1, z:0} };
|
288 |
}
|
289 |
|
290 |
-
|
291 |
const locationData = {
|
292 |
'start': { creator: createDefaultZone },
|
293 |
'forest': { creator: createForestZone },
|
@@ -297,11 +299,11 @@
|
|
297 |
const pageGraph = {
|
298 |
'start': {
|
299 |
title: "The Crossroads",
|
300 |
-
options: [ { text: "Enter Forest", transitionTo: 'forest' } ]
|
301 |
},
|
302 |
'forest': {
|
303 |
title: "Dark Forest",
|
304 |
-
options: [ { text: "Return to Crossroads", transitionTo: 'start' }
|
305 |
},
|
306 |
'cave': {
|
307 |
title: "Dim Cave",
|
@@ -322,7 +324,7 @@
|
|
322 |
const defaultChar = {
|
323 |
name: "Player",
|
324 |
stats: { hp: 20, maxHp: 20, xp: 0 },
|
325 |
-
inventory: []
|
326 |
};
|
327 |
gameState = {
|
328 |
currentLocationId: null,
|
@@ -335,7 +337,7 @@
|
|
335 |
function transitionToLocation(newLocationId) {
|
336 |
console.log(`Transitioning from ${gameState.currentLocationId} to ${newLocationId}`);
|
337 |
currentMessage = "";
|
338 |
-
currentPlacingItem = null;
|
339 |
|
340 |
if (gameState.currentLocationId && locationGroups[gameState.currentLocationId]) {
|
341 |
locationGroups[gameState.currentLocationId].visible = false;
|
@@ -356,12 +358,14 @@
|
|
356 |
worldGroup.add(newGroup);
|
357 |
newGroup.visible = true;
|
358 |
} else {
|
359 |
-
console.error(`Location data or creator missing for ID: ${newLocationId}`);
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
|
|
|
|
365 |
newGroup.visible = true;
|
366 |
newLocationId = 'start';
|
367 |
currentMessage += `<p class="message message-failure">Error: Couldn't load target location, returned to start.</p>`;
|
@@ -390,6 +394,7 @@
|
|
390 |
choicesElement.innerHTML = `<button class="choice-button" onclick="handleTransition({transitionTo: 'start'})">Return to Start</button>`;
|
391 |
updateStatsDisplay();
|
392 |
updateInventoryDisplay();
|
|
|
393 |
return;
|
394 |
}
|
395 |
|
@@ -421,7 +426,7 @@
|
|
421 |
console.warn("Choice option has no transitionTo property:", option);
|
422 |
}
|
423 |
}
|
424 |
-
window.handleTransition = handleTransition;
|
425 |
|
426 |
function updateStatsDisplay() {
|
427 |
const { hp, maxHp, xp } = gameState.character.stats;
|
@@ -438,13 +443,16 @@
|
|
438 |
const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
|
439 |
const itemClass = `item-${itemDef.type || 'unknown'}`;
|
440 |
const placingClass = (item === currentPlacingItem) ? ' placing' : '';
|
441 |
-
invHtml += `<span class="item-tag ${itemClass}${placingClass}" title="${itemDef.description}" data-itemname="${item}">${item}</span>`;
|
442 |
});
|
443 |
}
|
444 |
inventoryElement.innerHTML = invHtml;
|
445 |
|
446 |
inventoryElement.querySelectorAll('.item-tag').forEach(tag => {
|
447 |
-
tag.onclick = () => {
|
|
|
|
|
|
|
448 |
});
|
449 |
}
|
450 |
|
@@ -453,19 +461,20 @@
|
|
453 |
actionInfoElement.textContent = `Placing: ${currentPlacingItem} (Click ground)`;
|
454 |
actionInfoElement.style.display = 'block';
|
455 |
} else {
|
456 |
-
actionInfoElement.textContent = `Mode: Explore (Click items)`;
|
457 |
-
actionInfoElement.style.display = 'block';
|
458 |
}
|
459 |
}
|
460 |
|
461 |
-
|
462 |
function pickupItem() {
|
|
|
|
|
463 |
raycaster.setFromCamera(mouse, camera);
|
464 |
const currentGroup = locationGroups[gameState.currentLocationId];
|
465 |
if (!currentGroup) return;
|
466 |
|
467 |
const pickupables = [];
|
468 |
-
currentGroup.
|
469 |
if (child.userData.isPickupable) {
|
470 |
pickupables.push(child);
|
471 |
}
|
@@ -487,8 +496,8 @@
|
|
487 |
currentMessage += `<p class="message message-info"><em>(You already had one)</em></p>`;
|
488 |
}
|
489 |
|
490 |
-
clickedObject.visible = false;
|
491 |
-
clickedObject.userData.isPickupable = false;
|
492 |
|
493 |
renderCurrentPageUI();
|
494 |
}
|
@@ -496,14 +505,15 @@
|
|
496 |
}
|
497 |
|
498 |
function togglePlacementMode(itemName) {
|
|
|
499 |
if (currentPlacingItem === itemName) {
|
500 |
-
currentPlacingItem = null;
|
501 |
console.log("Placement cancelled.");
|
502 |
} else {
|
503 |
currentPlacingItem = itemName;
|
504 |
console.log(`Ready to place: ${itemName}`);
|
505 |
}
|
506 |
-
updateInventoryDisplay();
|
507 |
updateActionInfo();
|
508 |
}
|
509 |
|
@@ -515,7 +525,7 @@
|
|
515 |
if (!currentGroup) return;
|
516 |
|
517 |
const grounds = [];
|
518 |
-
currentGroup.
|
519 |
|
520 |
const intersects = raycaster.intersectObjects(grounds);
|
521 |
|
@@ -526,30 +536,29 @@
|
|
526 |
|
527 |
console.log(`Placing ${itemName} at ${point.x.toFixed(1)}, ${point.z.toFixed(1)}`);
|
528 |
|
529 |
-
const itemGeo = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
530 |
const itemMat = MAT.simple.clone();
|
531 |
if(itemDef.type === 'weapon') itemMat.color.setHex(0xaa4444);
|
532 |
else if(itemDef.type === 'consumable') itemMat.color.setHex(0xaa7744);
|
533 |
else itemMat.color.setHex(0x8888aa);
|
534 |
|
535 |
-
const placedMesh = createMesh(itemGeo, itemMat, {x: point.x, y: 0.25, z: point.z});
|
536 |
-
placedMesh.userData = { isPlacedItem: true, itemName: itemName };
|
537 |
currentGroup.add(placedMesh);
|
538 |
|
539 |
gameState.character.inventory = gameState.character.inventory.filter(i => i !== itemName);
|
540 |
currentMessage = `<p class="message message-item"><em>Placed ${itemName}.</em></p>`;
|
541 |
-
currentPlacingItem = null;
|
542 |
renderCurrentPageUI();
|
543 |
|
544 |
} else {
|
545 |
console.log("Placement click missed ground.");
|
546 |
currentMessage = `<p class="message message-failure"><em>Cannot place item there.</em></p>`;
|
547 |
-
currentPlacingItem = null;
|
548 |
renderCurrentPageUI();
|
549 |
}
|
550 |
}
|
551 |
|
552 |
-
|
553 |
document.addEventListener('DOMContentLoaded', () => {
|
554 |
console.log("DOM Ready - Initializing Persistent World Adventure.");
|
555 |
try {
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Persistent Procedural World (Fixed)</title>
|
7 |
<style>
|
8 |
body { font-family: 'Courier New', monospace; background-color: #111; color: #eee; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; height: 100vh; }
|
9 |
#game-container { display: flex; flex-grow: 1; overflow: hidden; }
|
|
|
36 |
.message-failure { color: #f88; border-left-color: #a44; }
|
37 |
.message-info { color: #aaa; border-left-color: #666; }
|
38 |
.message-item { color: #8bf; border-left-color: #46a; }
|
39 |
+
#action-info { position: absolute; bottom: 10px; left: 10px; background-color: rgba(0,0,0,0.7); color: #ffcc66; padding: 5px 10px; border-radius: 3px; font-size: 0.9em; display: none; z-index: 10;}
|
40 |
|
41 |
</style>
|
42 |
</head>
|
|
|
68 |
|
69 |
<script type="module">
|
70 |
import * as THREE from 'three';
|
71 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
72 |
|
73 |
const sceneContainer = document.getElementById('scene-container');
|
74 |
const storyTitleElement = document.getElementById('story-title');
|
|
|
78 |
const inventoryElement = document.getElementById('inventory-display');
|
79 |
const actionInfoElement = document.getElementById('action-info');
|
80 |
|
|
|
81 |
let scene, camera, renderer, clock, controls, raycaster, mouse;
|
82 |
+
let worldGroup = null;
|
83 |
+
let locationGroups = {};
|
84 |
+
let currentMessage = "";
|
85 |
+
let currentPlacingItem = null;
|
86 |
+
let currentLights = []; // <<<< ENSURE THIS IS DECLARED GLOBALLY
|
87 |
|
88 |
const MAT = {
|
89 |
stone: new THREE.MeshStandardMaterial({ color: 0x777788, roughness: 0.85, metalness: 0.1 }),
|
|
|
127 |
controls.enableDamping = true;
|
128 |
controls.dampingFactor = 0.1;
|
129 |
controls.target.set(0, 1, 0);
|
130 |
+
controls.maxPolarAngle = Math.PI / 2 - 0.05;
|
131 |
|
132 |
window.addEventListener('resize', onWindowResize, false);
|
133 |
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
|
|
|
146 |
}
|
147 |
|
148 |
function onMouseMove( event ) {
|
149 |
+
const rect = renderer.domElement.getBoundingClientRect();
|
150 |
+
mouse.x = ( (event.clientX - rect.left) / rect.width ) * 2 - 1;
|
151 |
+
mouse.y = - ( (event.clientY - rect.top) / rect.height ) * 2 + 1;
|
152 |
}
|
153 |
|
154 |
function onMouseClick( event ) {
|
|
|
184 |
const ground = new THREE.Mesh(geo, material);
|
185 |
ground.rotation.x = -Math.PI / 2; ground.position.y = 0;
|
186 |
ground.receiveShadow = true; ground.castShadow = false;
|
187 |
+
ground.userData.isGround = true;
|
188 |
return ground;
|
189 |
}
|
190 |
|
191 |
function setupLighting(type = 'default') {
|
192 |
+
currentLights.forEach(light => {
|
193 |
+
if (light) scene.remove(light);
|
194 |
+
});
|
195 |
currentLights = [];
|
196 |
|
197 |
let ambientIntensity = 0.4;
|
|
|
234 |
}
|
235 |
}
|
236 |
|
237 |
+
function createDefaultZone() {
|
238 |
const group = new THREE.Group();
|
239 |
group.add(createGround(MAT.dirt, 20));
|
240 |
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
|
241 |
const interactBox = createMesh(boxGeo, MAT.stone, {y: 0.5, x: 2, z: 2});
|
242 |
interactBox.userData = { isPickupable: true, itemName: "Mysterious Cube", description: "A plain stone cube."};
|
243 |
group.add(interactBox);
|
244 |
+
group.visible = false;
|
245 |
return { group, lighting: 'default', entryText: "You are in a default, featureless area.", cameraPos: {x:0, y:5, z:10}, targetPos: {x:0, y:1, z:0} };
|
246 |
}
|
247 |
|
|
|
290 |
return { group, lighting: 'cave', entryText: "It's dark and damp in here.", cameraPos: {x:0, y:4, z:8}, targetPos: {x:0, y:1, z:0} };
|
291 |
}
|
292 |
|
|
|
293 |
const locationData = {
|
294 |
'start': { creator: createDefaultZone },
|
295 |
'forest': { creator: createForestZone },
|
|
|
299 |
const pageGraph = {
|
300 |
'start': {
|
301 |
title: "The Crossroads",
|
302 |
+
options: [ { text: "Enter Forest", transitionTo: 'forest' }, { text: "Enter Cave", transitionTo: 'cave'} ]
|
303 |
},
|
304 |
'forest': {
|
305 |
title: "Dark Forest",
|
306 |
+
options: [ { text: "Return to Crossroads", transitionTo: 'start' } ]
|
307 |
},
|
308 |
'cave': {
|
309 |
title: "Dim Cave",
|
|
|
324 |
const defaultChar = {
|
325 |
name: "Player",
|
326 |
stats: { hp: 20, maxHp: 20, xp: 0 },
|
327 |
+
inventory: ["Rusty Sword"] // Start with an item for testing placement
|
328 |
};
|
329 |
gameState = {
|
330 |
currentLocationId: null,
|
|
|
337 |
function transitionToLocation(newLocationId) {
|
338 |
console.log(`Transitioning from ${gameState.currentLocationId} to ${newLocationId}`);
|
339 |
currentMessage = "";
|
340 |
+
currentPlacingItem = null;
|
341 |
|
342 |
if (gameState.currentLocationId && locationGroups[gameState.currentLocationId]) {
|
343 |
locationGroups[gameState.currentLocationId].visible = false;
|
|
|
358 |
worldGroup.add(newGroup);
|
359 |
newGroup.visible = true;
|
360 |
} else {
|
361 |
+
console.error(`Location data or creator missing for ID: ${newLocationId}, attempting fallback to 'start'`);
|
362 |
+
if (!locationGroups['start']) {
|
363 |
+
locationData['start'].cachedInfo = locationData['start'].creator();
|
364 |
+
locationGroups['start'] = locationData['start'].cachedInfo.group;
|
365 |
+
worldGroup.add(locationGroups['start']);
|
366 |
+
}
|
367 |
+
locationInfo = locationData['start'].cachedInfo;
|
368 |
+
newGroup = locationGroups['start'];
|
369 |
newGroup.visible = true;
|
370 |
newLocationId = 'start';
|
371 |
currentMessage += `<p class="message message-failure">Error: Couldn't load target location, returned to start.</p>`;
|
|
|
394 |
choicesElement.innerHTML = `<button class="choice-button" onclick="handleTransition({transitionTo: 'start'})">Return to Start</button>`;
|
395 |
updateStatsDisplay();
|
396 |
updateInventoryDisplay();
|
397 |
+
updateActionInfo();
|
398 |
return;
|
399 |
}
|
400 |
|
|
|
426 |
console.warn("Choice option has no transitionTo property:", option);
|
427 |
}
|
428 |
}
|
429 |
+
window.handleTransition = handleTransition;
|
430 |
|
431 |
function updateStatsDisplay() {
|
432 |
const { hp, maxHp, xp } = gameState.character.stats;
|
|
|
443 |
const itemDef = itemsData[item] || { type: 'unknown', description: '???' };
|
444 |
const itemClass = `item-${itemDef.type || 'unknown'}`;
|
445 |
const placingClass = (item === currentPlacingItem) ? ' placing' : '';
|
446 |
+
invHtml += `<span class="item-tag ${itemClass}${placingClass}" title="Click to Place: ${itemDef.description}" data-itemname="${item}">${item}</span>`;
|
447 |
});
|
448 |
}
|
449 |
inventoryElement.innerHTML = invHtml;
|
450 |
|
451 |
inventoryElement.querySelectorAll('.item-tag').forEach(tag => {
|
452 |
+
tag.onclick = (event) => {
|
453 |
+
event.stopPropagation(); // Prevent click from triggering pickup
|
454 |
+
togglePlacementMode(tag.dataset.itemname);
|
455 |
+
};
|
456 |
});
|
457 |
}
|
458 |
|
|
|
461 |
actionInfoElement.textContent = `Placing: ${currentPlacingItem} (Click ground)`;
|
462 |
actionInfoElement.style.display = 'block';
|
463 |
} else {
|
464 |
+
actionInfoElement.textContent = `Mode: Explore (Click items/Use UI)`;
|
465 |
+
actionInfoElement.style.display = 'block';
|
466 |
}
|
467 |
}
|
468 |
|
|
|
469 |
function pickupItem() {
|
470 |
+
if (currentPlacingItem) return; // Don't pick up while placing
|
471 |
+
|
472 |
raycaster.setFromCamera(mouse, camera);
|
473 |
const currentGroup = locationGroups[gameState.currentLocationId];
|
474 |
if (!currentGroup) return;
|
475 |
|
476 |
const pickupables = [];
|
477 |
+
currentGroup.traverseVisible(child => { // Only check visible objects
|
478 |
if (child.userData.isPickupable) {
|
479 |
pickupables.push(child);
|
480 |
}
|
|
|
496 |
currentMessage += `<p class="message message-info"><em>(You already had one)</em></p>`;
|
497 |
}
|
498 |
|
499 |
+
clickedObject.visible = false;
|
500 |
+
clickedObject.userData.isPickupable = false;
|
501 |
|
502 |
renderCurrentPageUI();
|
503 |
}
|
|
|
505 |
}
|
506 |
|
507 |
function togglePlacementMode(itemName) {
|
508 |
+
if (!itemName) return;
|
509 |
if (currentPlacingItem === itemName) {
|
510 |
+
currentPlacingItem = null;
|
511 |
console.log("Placement cancelled.");
|
512 |
} else {
|
513 |
currentPlacingItem = itemName;
|
514 |
console.log(`Ready to place: ${itemName}`);
|
515 |
}
|
516 |
+
updateInventoryDisplay();
|
517 |
updateActionInfo();
|
518 |
}
|
519 |
|
|
|
525 |
if (!currentGroup) return;
|
526 |
|
527 |
const grounds = [];
|
528 |
+
currentGroup.traverseVisible(child => { if(child.userData.isGround) grounds.push(child); });
|
529 |
|
530 |
const intersects = raycaster.intersectObjects(grounds);
|
531 |
|
|
|
536 |
|
537 |
console.log(`Placing ${itemName} at ${point.x.toFixed(1)}, ${point.z.toFixed(1)}`);
|
538 |
|
539 |
+
const itemGeo = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
540 |
const itemMat = MAT.simple.clone();
|
541 |
if(itemDef.type === 'weapon') itemMat.color.setHex(0xaa4444);
|
542 |
else if(itemDef.type === 'consumable') itemMat.color.setHex(0xaa7744);
|
543 |
else itemMat.color.setHex(0x8888aa);
|
544 |
|
545 |
+
const placedMesh = createMesh(itemGeo, itemMat, {x: point.x, y: 0.25, z: point.z}); // Place slightly above ground
|
546 |
+
placedMesh.userData = { isPlacedItem: true, itemName: itemName, isPickupable: true, description: `Placed ${itemName}` }; // Make it pickupable again
|
547 |
currentGroup.add(placedMesh);
|
548 |
|
549 |
gameState.character.inventory = gameState.character.inventory.filter(i => i !== itemName);
|
550 |
currentMessage = `<p class="message message-item"><em>Placed ${itemName}.</em></p>`;
|
551 |
+
currentPlacingItem = null;
|
552 |
renderCurrentPageUI();
|
553 |
|
554 |
} else {
|
555 |
console.log("Placement click missed ground.");
|
556 |
currentMessage = `<p class="message message-failure"><em>Cannot place item there.</em></p>`;
|
557 |
+
currentPlacingItem = null;
|
558 |
renderCurrentPageUI();
|
559 |
}
|
560 |
}
|
561 |
|
|
|
562 |
document.addEventListener('DOMContentLoaded', () => {
|
563 |
console.log("DOM Ready - Initializing Persistent World Adventure.");
|
564 |
try {
|