Spaces:
Running
Running
Update index.html
Browse files- index.html +226 -79
index.html
CHANGED
@@ -70,11 +70,11 @@
|
|
70 |
border-bottom: 1px solid #555;
|
71 |
font-size: 0.9em;
|
72 |
}
|
73 |
-
#stats-display, #inventory-display {
|
74 |
margin-bottom: 10px;
|
75 |
line-height: 1.8;
|
76 |
}
|
77 |
-
#stats-display span, #inventory-display span {
|
78 |
display: inline-block;
|
79 |
background-color: #444;
|
80 |
padding: 3px 8px;
|
@@ -84,8 +84,10 @@
|
|
84 |
border: 1px solid #666;
|
85 |
white-space: nowrap;
|
86 |
}
|
87 |
-
#stats-display strong, #inventory-display strong { color: #aaa; margin-right: 5px; }
|
88 |
#inventory-display em { color: #888; font-style: normal; }
|
|
|
|
|
89 |
|
90 |
#inventory-display .item-quest { background-color: #666030; border-color: #999048;}
|
91 |
#inventory-display .item-weapon { background-color: #663030; border-color: #994848;}
|
@@ -114,6 +116,7 @@
|
|
114 |
|
115 |
.roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
116 |
.roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
|
|
117 |
</style>
|
118 |
</head>
|
119 |
<body>
|
@@ -127,6 +130,7 @@
|
|
127 |
<div id="stats-inventory-container">
|
128 |
<div id="stats-display"></div>
|
129 |
<div id="inventory-display"></div>
|
|
|
130 |
</div>
|
131 |
<div id="choices-container">
|
132 |
<h3>What will you do?</h3>
|
@@ -153,6 +157,7 @@
|
|
153 |
const choicesElement = document.getElementById('choices');
|
154 |
const statsElement = document.getElementById('stats-display');
|
155 |
const inventoryElement = document.getElementById('inventory-display');
|
|
|
156 |
|
157 |
let scene, camera, renderer;
|
158 |
let currentAssemblyGroup = null;
|
@@ -173,6 +178,9 @@
|
|
173 |
const sandMaterial = new THREE.MeshStandardMaterial({ color: 0xF4A460, roughness: 0.9 });
|
174 |
const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
|
175 |
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
|
|
|
|
|
|
|
176 |
|
177 |
function initThreeJS() {
|
178 |
if (!sceneContainer) { console.error("Scene container not found!"); return; }
|
@@ -472,19 +480,25 @@
|
|
472 |
return group;
|
473 |
}
|
474 |
|
475 |
-
function createOvergrownPathAssembly() {
|
476 |
const group = new THREE.Group();
|
477 |
group.add(createGroundPlane(dirtMaterial, 15));
|
478 |
-
const forest = createForestAssembly(
|
479 |
group.add(forest);
|
480 |
const fungiGeo = new THREE.SphereGeometry(0.1, 8, 8);
|
481 |
-
for (let i = 0; i <
|
482 |
group.add(createMesh(fungiGeo, glowMaterial, { x: (Math.random() - 0.5) * 8, y: 0.1, z: (Math.random() - 0.5) * 8 }));
|
483 |
}
|
484 |
const vineGeo = new THREE.CylinderGeometry(0.05, 0.05, 2, 8);
|
485 |
-
for (let i = 0; i <
|
486 |
group.add(createMesh(vineGeo, leafMaterial, { x: (Math.random() - 0.5) * 6, y: 2, z: (Math.random() - 0.5) * 6 }, { z: Math.random() * Math.PI }));
|
487 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
488 |
return group;
|
489 |
}
|
490 |
|
@@ -505,7 +519,7 @@
|
|
505 |
}
|
506 |
|
507 |
function createGoblinAmbushAssembly() {
|
508 |
-
const group = createOvergrownPathAssembly();
|
509 |
const bodyGeo = new THREE.CylinderGeometry(0.3, 0.3, 1, 8);
|
510 |
const headGeo = new THREE.SphereGeometry(0.2, 8, 8);
|
511 |
const goblinMat = new THREE.MeshStandardMaterial({ color: 0x556B2F });
|
@@ -556,6 +570,77 @@
|
|
556 |
return group;
|
557 |
}
|
558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
559 |
// Game Data
|
560 |
const itemsData = {
|
561 |
"Flaming Sword":{type:"weapon", description:"A fiery blade"},
|
@@ -582,7 +667,7 @@
|
|
582 |
"8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail... It leads towards a ravine spanned by a rickety rope bridge.</p><p>(+20 XP)</p>`, options: [ { text: "Risk crossing the rope bridge (Dexterity Check)", check: { stat: 'dexterity', dc: 10, onFailure: 81 }, next: 80 }, { text: "Search for another way across the ravine", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
|
583 |
"10": { title: "Goblin Ambush!", content: `<p>Two scraggly goblins leap out, brandishing crude spears!</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 10 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" },
|
584 |
"11": { title: "Hidden Evasion", content: `<p>You melt into the shadows as the goblins blunder past.</p><p>(+30 XP)</p>`, options: [ { text: "Continue cautiously", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
|
585 |
-
"12": { title: "Ambush Victory!", content: `<p>You defeat the goblins! Found a Crude Dagger.</p><p>(+50 XP)</p>`, options: [ { text: "Press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger" } },
|
586 |
"13": { title: "Daring Escape", content: `<p>With surprising agility, you tumble past the goblins!</p><p>(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
|
587 |
"14": { title: "Forest Stream Crossing", content: `<p>The path leads to a clear, shallow stream...</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?)", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones" },
|
588 |
"15": { title: "Log Bridge", content: `<p>Further upstream, a large, mossy log spans the stream.</p>`, options: [ { text: "Cross carefully on the log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Go back and wade instead", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
|
@@ -612,20 +697,77 @@
|
|
612 |
let gameState = {
|
613 |
currentPageId: 1,
|
614 |
character: {
|
615 |
-
name: "Hero",
|
616 |
-
|
617 |
-
|
618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
619 |
}
|
620 |
};
|
621 |
|
622 |
// Game Logic Functions
|
623 |
function startGame() {
|
624 |
-
const defaultChar = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
625 |
gameState = { currentPageId: 1, character: { ...defaultChar } };
|
626 |
renderPage(gameState.currentPageId);
|
627 |
}
|
628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
629 |
function handleChoiceClick(choiceData) {
|
630 |
const optionNextPageId = parseInt(choiceData.nextPage);
|
631 |
const itemToAdd = choiceData.addItem;
|
@@ -638,13 +780,15 @@
|
|
638 |
if (check) {
|
639 |
const statValue = gameState.character.stats[check.stat] || 10;
|
640 |
const modifier = Math.floor((statValue - 10) / 2);
|
|
|
641 |
const roll = Math.floor(Math.random() * 20) + 1;
|
642 |
-
const totalResult = roll + modifier;
|
643 |
const dc = check.dc;
|
644 |
-
console.log(`Check: ${check.stat} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} = ${
|
645 |
if (totalResult >= dc) {
|
646 |
nextPageId = optionNextPageId;
|
647 |
rollResultMessage = `<p class="roll-success"><em>Check Success! (${totalResult} vs DC ${dc})</em></p>`;
|
|
|
648 |
} else {
|
649 |
nextPageId = parseInt(check.onFailure);
|
650 |
rollResultMessage = `<p class="roll-failure"><em>Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
|
@@ -660,6 +804,7 @@
|
|
660 |
gameState.currentPageId = nextPageId;
|
661 |
const nextPageData = gameData[nextPageId];
|
662 |
|
|
|
663 |
if (nextPageData) {
|
664 |
if (nextPageData.hpLoss) {
|
665 |
gameState.character.stats.hp -= nextPageData.hpLoss;
|
@@ -667,7 +812,15 @@
|
|
667 |
if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died!"); nextPageId = 99; }
|
668 |
}
|
669 |
if (nextPageData.reward) {
|
670 |
-
if (nextPageData.reward.xp) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
671 |
if (nextPageData.reward.statIncrease) {
|
672 |
const stat = nextPageData.reward.statIncrease.stat;
|
673 |
const amount = nextPageData.reward.statIncrease.amount;
|
@@ -676,24 +829,28 @@
|
|
676 |
console.log(`Stat ${stat} increased by ${amount}.`);
|
677 |
}
|
678 |
}
|
679 |
-
if(nextPageData.reward.addItem && !gameState.character.inventory.includes(nextPageData.reward.addItem)){
|
680 |
gameState.character.inventory.push(nextPageData.reward.addItem);
|
681 |
console.log(`Found item: ${nextPageData.reward.addItem}`);
|
682 |
}
|
|
|
|
|
|
|
|
|
683 |
}
|
684 |
const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
|
685 |
gameState.character.stats.maxHp = 10 + (conModifier * gameState.character.level);
|
686 |
gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
|
687 |
if (nextPageId === 99 && gameState.character.stats.hp <= 0) {
|
688 |
-
renderPageInternal(99, gameData[99], rollResultMessage);
|
689 |
return;
|
690 |
}
|
691 |
} else {
|
692 |
console.error(`Data for page ${nextPageId} not found!`);
|
693 |
-
renderPageInternal(99, gameData[99], "<p><em>Error: Next page data missing!</em></p>");
|
694 |
return;
|
695 |
}
|
696 |
-
renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage);
|
697 |
}
|
698 |
|
699 |
function renderPageInternal(pageId, pageData, message = "") {
|
@@ -702,6 +859,7 @@
|
|
702 |
storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
|
703 |
updateStatsDisplay();
|
704 |
updateInventoryDisplay();
|
|
|
705 |
choicesElement.innerHTML = '';
|
706 |
if (pageData.options && pageData.options.length > 0) {
|
707 |
pageData.options.forEach(option => {
|
@@ -726,7 +884,7 @@
|
|
726 |
const button = document.createElement('button');
|
727 |
button.classList.add('choice-button');
|
728 |
button.textContent = pageData.gameOver ? "Restart Adventure" : "The End";
|
729 |
-
button.onclick = () => handleChoiceClick({ nextPage: pageData.gameOver ?
|
730 |
choicesElement.appendChild(button);
|
731 |
if (!pageData.gameOver) choicesElement.insertAdjacentHTML('afterbegin', '<p><i>The path ends here.</i></p>');
|
732 |
}
|
@@ -736,24 +894,47 @@
|
|
736 |
function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
|
737 |
|
738 |
function updateStatsDisplay() {
|
739 |
-
const char=gameState.character;
|
740 |
statsElement.innerHTML = `<strong>Stats:</strong> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span>Str: ${char.stats.strength}</span> <span>Int: ${char.stats.intelligence}</span> <span>Wis: ${char.stats.wisdom}</span> <span>Dex: ${char.stats.dexterity}</span> <span>Con: ${char.stats.constitution}</span> <span>Cha: ${char.stats.charisma}</span>`;
|
741 |
}
|
742 |
|
743 |
function updateInventoryDisplay() {
|
744 |
-
let h='<strong>Inventory:</strong> ';
|
745 |
-
if(gameState.character.inventory.length === 0){
|
746 |
-
h+='<em>Empty</em>';
|
747 |
} else {
|
748 |
-
gameState.character.inventory.forEach(i=>{
|
749 |
-
const d=itemsData[i]||{type:'unknown',description:'???'};
|
750 |
-
const c
|
751 |
-
h
|
752 |
});
|
753 |
}
|
754 |
inventoryElement.innerHTML = h;
|
755 |
}
|
756 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
757 |
function updateScene(illustrationKey) {
|
758 |
if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); }
|
759 |
scene.fog = null;
|
@@ -813,6 +994,20 @@
|
|
813 |
scene.background = new THREE.Color(0x1A1A1A);
|
814 |
camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0);
|
815 |
assemblyFunction = createDarkCaveAssembly; break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
816 |
default:
|
817 |
console.warn(`Unknown illustration key: "${illustrationKey}". Using default.`);
|
818 |
assemblyFunction = createDefaultAssembly; break;
|
@@ -834,52 +1029,4 @@
|
|
834 |
scene.remove(child);
|
835 |
}
|
836 |
});
|
837 |
-
const ambient
|
838 |
-
let directionalLight;
|
839 |
-
switch (illustrationKey) {
|
840 |
-
case 'crossroads-signpost-sunny':
|
841 |
-
ambient.intensity = 0.8;
|
842 |
-
directionalLight = new THREE.DirectionalLight(0xFFF8E1, 1.5);
|
843 |
-
directionalLight.position.set(10, 15, 10);
|
844 |
-
break;
|
845 |
-
case 'dark-forest-entrance-gnarled-roots-filtered-light':
|
846 |
-
case 'overgrown-forest-path-glowing-fungi-vines':
|
847 |
-
ambient.intensity = 0.3;
|
848 |
-
directionalLight = new THREE.DirectionalLight(0xA8E4A0, 0.6);
|
849 |
-
directionalLight.position.set(5, 10, 5);
|
850 |
-
break;
|
851 |
-
case 'dark-cave-entrance-dripping-water':
|
852 |
-
ambient.intensity = 0.1;
|
853 |
-
directionalLight = new THREE.DirectionalLight(0x666666, 0.2);
|
854 |
-
directionalLight.position.set(2, 5, 2);
|
855 |
-
break;
|
856 |
-
default:
|
857 |
-
ambient.intensity = 0.5;
|
858 |
-
directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
859 |
-
directionalLight.position.set(8, 15, 10);
|
860 |
-
}
|
861 |
-
directionalLight.castShadow = true;
|
862 |
-
directionalLight.shadow.mapSize.set(1024, 1024);
|
863 |
-
directionalLight.shadow.camera.near = 0.5;
|
864 |
-
directionalLight.shadow.camera.far = 50;
|
865 |
-
directionalLight.shadow.camera.left = -15;
|
866 |
-
directionalLight.shadow.camera.right = 15;
|
867 |
-
directionalLight.shadow.camera.top = 15;
|
868 |
-
directionalLight.shadow.camera.bottom = -15;
|
869 |
-
scene.add(directionalLight);
|
870 |
-
}
|
871 |
-
|
872 |
-
document.addEventListener('DOMContentLoaded', () => {
|
873 |
-
console.log("DOM Ready.");
|
874 |
-
try {
|
875 |
-
initThreeJS();
|
876 |
-
startGame();
|
877 |
-
} catch (error) {
|
878 |
-
console.error("Init failed:", error);
|
879 |
-
storyTitleElement.textContent = "Error";
|
880 |
-
storyContentElement.innerHTML = `<p>Init Error. Check console.</p><pre>${error}</pre>`;
|
881 |
-
}
|
882 |
-
});
|
883 |
-
</script>
|
884 |
-
</body>
|
885 |
-
</html>
|
|
|
70 |
border-bottom: 1px solid #555;
|
71 |
font-size: 0.9em;
|
72 |
}
|
73 |
+
#stats-display, #inventory-display, #character-sheet {
|
74 |
margin-bottom: 10px;
|
75 |
line-height: 1.8;
|
76 |
}
|
77 |
+
#stats-display span, #inventory-display span, #character-sheet span {
|
78 |
display: inline-block;
|
79 |
background-color: #444;
|
80 |
padding: 3px 8px;
|
|
|
84 |
border: 1px solid #666;
|
85 |
white-space: nowrap;
|
86 |
}
|
87 |
+
#stats-display strong, #inventory-display strong, #character-sheet strong { color: #aaa; margin-right: 5px; }
|
88 |
#inventory-display em { color: #888; font-style: normal; }
|
89 |
+
#character-sheet .accomplishment { background-color: #665533; border-color: #998866; }
|
90 |
+
#character-sheet .attribute { background-color: #553366; border-color: #886699; }
|
91 |
|
92 |
#inventory-display .item-quest { background-color: #666030; border-color: #999048;}
|
93 |
#inventory-display .item-weapon { background-color: #663030; border-color: #994848;}
|
|
|
116 |
|
117 |
.roll-success { color: #7f7; border-left: 3px solid #4a4; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
118 |
.roll-failure { color: #f77; border-left: 3px solid #a44; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
119 |
+
.level-up { color: #ffcc66; border-left: 3px solid #cc9933; padding-left: 8px; margin-bottom: 1em; font-size: 0.9em; }
|
120 |
</style>
|
121 |
</head>
|
122 |
<body>
|
|
|
130 |
<div id="stats-inventory-container">
|
131 |
<div id="stats-display"></div>
|
132 |
<div id="inventory-display"></div>
|
133 |
+
<div id="character-sheet"></div>
|
134 |
</div>
|
135 |
<div id="choices-container">
|
136 |
<h3>What will you do?</h3>
|
|
|
157 |
const choicesElement = document.getElementById('choices');
|
158 |
const statsElement = document.getElementById('stats-display');
|
159 |
const inventoryElement = document.getElementById('inventory-display');
|
160 |
+
const characterSheetElement = document.getElementById('character-sheet');
|
161 |
|
162 |
let scene, camera, renderer;
|
163 |
let currentAssemblyGroup = null;
|
|
|
178 |
const sandMaterial = new THREE.MeshStandardMaterial({ color: 0xF4A460, roughness: 0.9 });
|
179 |
const wetStoneMaterial = new THREE.MeshStandardMaterial({ color: 0x2F4F4F, roughness: 0.7 });
|
180 |
const glowMaterial = new THREE.MeshStandardMaterial({ color: 0x00FFAA, emissive: 0x00FFAA, emissiveIntensity: 0.5 });
|
181 |
+
const crackedEarthMaterial = new THREE.MeshStandardMaterial({ color: 0x663300, roughness: 0.9 });
|
182 |
+
const birdMaterial = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.7 });
|
183 |
+
const scorpionMaterial = new THREE.MeshStandardMaterial({ color: 0x4A2F1A, roughness: 0.8 });
|
184 |
|
185 |
function initThreeJS() {
|
186 |
if (!sceneContainer) { console.error("Scene container not found!"); return; }
|
|
|
480 |
return group;
|
481 |
}
|
482 |
|
483 |
+
function createOvergrownPathAssembly(treeCount=15, objectCount=30, creatureCount=0, lightIntensity=0.6) {
|
484 |
const group = new THREE.Group();
|
485 |
group.add(createGroundPlane(dirtMaterial, 15));
|
486 |
+
const forest = createForestAssembly(treeCount, 10);
|
487 |
group.add(forest);
|
488 |
const fungiGeo = new THREE.SphereGeometry(0.1, 8, 8);
|
489 |
+
for (let i = 0; i < objectCount; i++) {
|
490 |
group.add(createMesh(fungiGeo, glowMaterial, { x: (Math.random() - 0.5) * 8, y: 0.1, z: (Math.random() - 0.5) * 8 }));
|
491 |
}
|
492 |
const vineGeo = new THREE.CylinderGeometry(0.05, 0.05, 2, 8);
|
493 |
+
for (let i = 0; i < Math.floor(objectCount / 3); i++) {
|
494 |
group.add(createMesh(vineGeo, leafMaterial, { x: (Math.random() - 0.5) * 6, y: 2, z: (Math.random() - 0.5) * 6 }, { z: Math.random() * Math.PI }));
|
495 |
}
|
496 |
+
if (creatureCount > 0) {
|
497 |
+
const creatureGeo = new THREE.SphereGeometry(0.2, 8, 8);
|
498 |
+
for (let i = 0; i < creatureCount; i++) {
|
499 |
+
group.add(createMesh(creatureGeo, birdMaterial, { x: (Math.random() - 0.5) * 8, y: 2 + Math.random(), z: (Math.random() - 0.5) * 8 }));
|
500 |
+
}
|
501 |
+
}
|
502 |
return group;
|
503 |
}
|
504 |
|
|
|
519 |
}
|
520 |
|
521 |
function createGoblinAmbushAssembly() {
|
522 |
+
const group = createOvergrownPathAssembly(15, 30, 2);
|
523 |
const bodyGeo = new THREE.CylinderGeometry(0.3, 0.3, 1, 8);
|
524 |
const headGeo = new THREE.SphereGeometry(0.2, 8, 8);
|
525 |
const goblinMat = new THREE.MeshStandardMaterial({ color: 0x556B2F });
|
|
|
570 |
return group;
|
571 |
}
|
572 |
|
573 |
+
function createMossyRavineAssembly(treeCount=10, objectCount=20, creatureCount=3, lightIntensity=0.5) {
|
574 |
+
const group = new THREE.Group();
|
575 |
+
group.add(createGroundPlane(groundMaterial, 20));
|
576 |
+
const forest = createForestAssembly(treeCount, 15);
|
577 |
+
group.add(forest);
|
578 |
+
const bridgeGeo = new THREE.BoxGeometry(5, 0.1, 1);
|
579 |
+
group.add(createMesh(bridgeGeo, woodMaterial, { y: 1, z: 2 }));
|
580 |
+
const ropeGeo = new THREE.CylinderGeometry(0.05, 0.05, 5, 8);
|
581 |
+
group.add(createMesh(ropeGeo, woodMaterial, { x: -2.5, y: 1.5, z: 2 }, { z: Math.PI / 2 }));
|
582 |
+
group.add(createMesh(ropeGeo, woodMaterial, { x: 2.5, y: 1.5, z: 2 }, { z: Math.PI / 2 }));
|
583 |
+
const rockGeo = new THREE.SphereGeometry(0.4, 6, 6);
|
584 |
+
for (let i = 0; i < objectCount; i++) {
|
585 |
+
group.add(createMesh(rockGeo, stoneMaterial, { x: (Math.random() - 0.5) * 10, y: 0.2, z: (Math.random() - 0.5) * 10 }));
|
586 |
+
}
|
587 |
+
if (creatureCount > 0) {
|
588 |
+
const birdGeo = new THREE.SphereGeometry(0.15, 8, 8);
|
589 |
+
for (let i = 0; i < creatureCount; i++) {
|
590 |
+
const bird = createMesh(birdGeo, birdMaterial, { x: (Math.random() - 0.5) * 8, y: 3 + Math.random(), z: (Math.random() - 0.5) * 8 });
|
591 |
+
bird.userData.update = (time) => {
|
592 |
+
bird.position.y += Math.sin(time + i) * 0.05;
|
593 |
+
};
|
594 |
+
group.add(bird);
|
595 |
+
}
|
596 |
+
}
|
597 |
+
return group;
|
598 |
+
}
|
599 |
+
|
600 |
+
function createRockyBadlandsAssembly(treeCount=5, objectCount=15, creatureCount=2, lightIntensity=1.2) {
|
601 |
+
const group = new THREE.Group();
|
602 |
+
group.add(createGroundPlane(crackedEarthMaterial, 25));
|
603 |
+
const sparseVeg = createForestAssembly(treeCount, 20);
|
604 |
+
sparseVeg.children.forEach(c => {
|
605 |
+
if (c.type === 'Group') c.scale.set(0.5, 0.5, 0.5);
|
606 |
+
});
|
607 |
+
group.add(sparseVeg);
|
608 |
+
const rockGeo = new THREE.SphereGeometry(0.6, 6, 6);
|
609 |
+
for (let i = 0; i < objectCount; i++) {
|
610 |
+
group.add(createMesh(rockGeo, stoneMaterial, { x: (Math.random() - 0.5) * 15, y: 0.3, z: (Math.random() - 0.5) * 15 }));
|
611 |
+
}
|
612 |
+
if (creatureCount > 0) {
|
613 |
+
const scorpionGeo = new THREE.BoxGeometry(0.3, 0.1, 0.5);
|
614 |
+
for (let i = 0; i < creatureCount; i++) {
|
615 |
+
group.add(createMesh(scorpionGeo, scorpionMaterial, { x: (Math.random() - 0.5) * 10, y: 0.05, z: (Math.random() - 0.5) * 10 }));
|
616 |
+
}
|
617 |
+
}
|
618 |
+
return group;
|
619 |
+
}
|
620 |
+
|
621 |
+
function createMountainFortressAssembly(treeCount=3, objectCount=10, creatureCount=2, lightIntensity=0.8) {
|
622 |
+
const group = new THREE.Group();
|
623 |
+
group.add(createGroundPlane(stoneMaterial, 20));
|
624 |
+
const wallGeo = new THREE.BoxGeometry(15, 3, 0.5);
|
625 |
+
group.add(createMesh(wallGeo, stoneMaterial, { y: 1.5, z: -5 }));
|
626 |
+
const towerGeo = new THREE.CylinderGeometry(1, 1, 4, 12);
|
627 |
+
group.add(createMesh(towerGeo, stoneMaterial, { x: -6, y: 2, z: -5 }));
|
628 |
+
group.add(createMesh(towerGeo, stoneMaterial, { x: 6, y: 2, z: -5 }));
|
629 |
+
const crateGeo = new THREE.BoxGeometry(0.8, 0.8, 0.8);
|
630 |
+
for (let i = 0; i < objectCount; i++) {
|
631 |
+
group.add(createMesh(crateGeo, woodMaterial, { x: (Math.random() - 0.5) * 8, y: 0.4, z: (Math.random() - 0.5) * 4 + 2 }));
|
632 |
+
}
|
633 |
+
if (creatureCount > 0) {
|
634 |
+
const guardGeo = new THREE.CylinderGeometry(0.3, 0.3, 1.5, 8);
|
635 |
+
for (let i = 0; i < creatureCount; i++) {
|
636 |
+
group.add(createMesh(guardGeo, darkWoodMaterial, { x: -4 + i * 8, y: 0.75, z: -5 }));
|
637 |
+
}
|
638 |
+
}
|
639 |
+
const sparseTrees = createForestAssembly(treeCount, 15);
|
640 |
+
group.add(sparseTrees);
|
641 |
+
return group;
|
642 |
+
}
|
643 |
+
|
644 |
// Game Data
|
645 |
const itemsData = {
|
646 |
"Flaming Sword":{type:"weapon", description:"A fiery blade"},
|
|
|
667 |
"8": { title: "Hidden Game Trail", content: `<p>Your sharp eyes spot a faint trail... It leads towards a ravine spanned by a rickety rope bridge.</p><p>(+20 XP)</p>`, options: [ { text: "Risk crossing the rope bridge (Dexterity Check)", check: { stat: 'dexterity', dc: 10, onFailure: 81 }, next: 80 }, { text: "Search for another way across the ravine", next: 82 } ], illustration: "narrow-game-trail-forest-rope-bridge-ravine", reward: { xp: 20 } },
|
668 |
"10": { title: "Goblin Ambush!", content: `<p>Two scraggly goblins leap out, brandishing crude spears!</p>`, options: [ { text: "Fight the goblins!", next: 12 }, { text: "Attempt to dodge past them (Dexterity Check)", check: { stat: 'dexterity', dc: 13, onFailure: 10 }, next: 13 } ], illustration: "two-goblins-ambush-forest-path-spears" },
|
669 |
"11": { title: "Hidden Evasion", content: `<p>You melt into the shadows as the goblins blunder past.</p><p>(+30 XP)</p>`, options: [ { text: "Continue cautiously", next: 14 } ], illustration: "forest-shadows-hiding-goblins-walking-past", reward: { xp: 30 } },
|
670 |
+
"12": { title: "Ambush Victory!", content: `<p>You defeat the goblins! Found a Crude Dagger.</p><p>(+50 XP)</p>`, options: [ { text: "Press onward", next: 14 } ], illustration: "defeated-goblins-forest-path-loot", reward: { xp: 50, addItem: "Crude Dagger", accomplishment: "Defeated Goblins" } },
|
671 |
"13": { title: "Daring Escape", content: `<p>With surprising agility, you tumble past the goblins!</p><p>(+25 XP)</p>`, options: [ { text: "Keep running!", next: 14 } ], illustration: "blurred-motion-running-past-goblins-forest", reward: { xp: 25 } },
|
672 |
"14": { title: "Forest Stream Crossing", content: `<p>The path leads to a clear, shallow stream...</p>`, options: [ { text: "Wade across the stream", next: 16 }, { text: "Look for a drier crossing point (fallen log?)", next: 15 } ], illustration: "forest-stream-crossing-dappled-sunlight-stones" },
|
673 |
"15": { title: "Log Bridge", content: `<p>Further upstream, a large, mossy log spans the stream.</p>`, options: [ { text: "Cross carefully on the log (Dexterity Check)", check: { stat: 'dexterity', dc: 9, onFailure: 151 }, next: 16 }, { text: "Go back and wade instead", next: 14 } ], illustration: "mossy-log-bridge-over-forest-stream" },
|
|
|
697 |
let gameState = {
|
698 |
currentPageId: 1,
|
699 |
character: {
|
700 |
+
name: "Hero",
|
701 |
+
race: "Human",
|
702 |
+
alignment: "Neutral Good",
|
703 |
+
class: "Adventurer",
|
704 |
+
level: 1,
|
705 |
+
xp: 0,
|
706 |
+
xpToNextLevel: 100,
|
707 |
+
stats: {
|
708 |
+
strength: 8,
|
709 |
+
intelligence: 10,
|
710 |
+
wisdom: 10,
|
711 |
+
dexterity: 10,
|
712 |
+
constitution: 10,
|
713 |
+
charisma: 8,
|
714 |
+
hp: 12,
|
715 |
+
maxHp: 12
|
716 |
+
},
|
717 |
+
inventory: [],
|
718 |
+
wins: {
|
719 |
+
strength: 0,
|
720 |
+
intelligence: 0,
|
721 |
+
wisdom: 0,
|
722 |
+
dexterity: 0,
|
723 |
+
constitution: 0,
|
724 |
+
charisma: 0
|
725 |
+
},
|
726 |
+
accomplishments: []
|
727 |
}
|
728 |
};
|
729 |
|
730 |
// Game Logic Functions
|
731 |
function startGame() {
|
732 |
+
const defaultChar = {
|
733 |
+
name: "Hero",
|
734 |
+
race: "Human",
|
735 |
+
alignment: "Neutral Good",
|
736 |
+
class: "Adventurer",
|
737 |
+
level: 1,
|
738 |
+
xp: 0,
|
739 |
+
xpToNextLevel: 100,
|
740 |
+
stats: {
|
741 |
+
strength: 8,
|
742 |
+
intelligence: 10,
|
743 |
+
wisdom: 10,
|
744 |
+
dexterity: 10,
|
745 |
+
constitution: 10,
|
746 |
+
charisma: 8,
|
747 |
+
hp: 12,
|
748 |
+
maxHp: 12
|
749 |
+
},
|
750 |
+
inventory: [],
|
751 |
+
wins: {
|
752 |
+
strength: 0,
|
753 |
+
intelligence: 0,
|
754 |
+
wisdom: 0,
|
755 |
+
dexterity: 0,
|
756 |
+
constitution: 0,
|
757 |
+
charisma: 0
|
758 |
+
},
|
759 |
+
accomplishments: []
|
760 |
+
};
|
761 |
gameState = { currentPageId: 1, character: { ...defaultChar } };
|
762 |
renderPage(gameState.currentPageId);
|
763 |
}
|
764 |
|
765 |
+
function getAttributeModifier(statValue) {
|
766 |
+
if (statValue >= 14) return 3;
|
767 |
+
if (statValue >= 12) return 2;
|
768 |
+
return 1;
|
769 |
+
}
|
770 |
+
|
771 |
function handleChoiceClick(choiceData) {
|
772 |
const optionNextPageId = parseInt(choiceData.nextPage);
|
773 |
const itemToAdd = choiceData.addItem;
|
|
|
780 |
if (check) {
|
781 |
const statValue = gameState.character.stats[check.stat] || 10;
|
782 |
const modifier = Math.floor((statValue - 10) / 2);
|
783 |
+
const accomplishmentBonus = gameState.character.accomplishments.includes("Defeated Goblins") && check.stat === 'dexterity' ? 1 : 0;
|
784 |
const roll = Math.floor(Math.random() * 20) + 1;
|
785 |
+
const totalResult = roll + modifier + accomplishmentBonus;
|
786 |
const dc = check.dc;
|
787 |
+
console.log(`Check: ${check.stat} (DC ${dc}) | Roll: ${roll} + Mod: ${modifier} + Acc: ${accomplishmentBonus} = ${rollResultMessage}`);
|
788 |
if (totalResult >= dc) {
|
789 |
nextPageId = optionNextPageId;
|
790 |
rollResultMessage = `<p class="roll-success"><em>Check Success! (${totalResult} vs DC ${dc})</em></p>`;
|
791 |
+
gameState.character.wins[check.stat]++;
|
792 |
} else {
|
793 |
nextPageId = parseInt(check.onFailure);
|
794 |
rollResultMessage = `<p class="roll-failure"><em>Check Failed! (${totalResult} vs DC ${dc})</em></p>`;
|
|
|
804 |
gameState.currentPageId = nextPageId;
|
805 |
const nextPageData = gameData[nextPageId];
|
806 |
|
807 |
+
let levelUpMessage = "";
|
808 |
if (nextPageData) {
|
809 |
if (nextPageData.hpLoss) {
|
810 |
gameState.character.stats.hp -= nextPageData.hpLoss;
|
|
|
812 |
if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died!"); nextPageId = 99; }
|
813 |
}
|
814 |
if (nextPageData.reward) {
|
815 |
+
if (nextPageData.reward.xp) {
|
816 |
+
gameState.character.xp += nextPageData.reward.xp;
|
817 |
+
console.log(`Gained ${nextPageData.reward.xp} XP! Total: ${gameState.character.xp}`);
|
818 |
+
if (gameState.character.xp >= gameState.character.xpToNextLevel && gameState.character.level === 1) {
|
819 |
+
gameState.character.level = 2;
|
820 |
+
gameState.character.xpToNextLevel = 200;
|
821 |
+
levelUpMessage = `<p class="level-up"><em>Congratulations! You've reached Level 2! New XP goal: 200.</em></p>`;
|
822 |
+
}
|
823 |
+
}
|
824 |
if (nextPageData.reward.statIncrease) {
|
825 |
const stat = nextPageData.reward.statIncrease.stat;
|
826 |
const amount = nextPageData.reward.statIncrease.amount;
|
|
|
829 |
console.log(`Stat ${stat} increased by ${amount}.`);
|
830 |
}
|
831 |
}
|
832 |
+
if (nextPageData.reward.addItem && !gameState.character.inventory.includes(nextPageData.reward.addItem)) {
|
833 |
gameState.character.inventory.push(nextPageData.reward.addItem);
|
834 |
console.log(`Found item: ${nextPageData.reward.addItem}`);
|
835 |
}
|
836 |
+
if (nextPageData.reward.accomplishment && !gameState.character.accomplishments.includes(nextPageData.reward.accomplishment)) {
|
837 |
+
gameState.character.accomplishments.push(nextPageData.reward.accomplishment);
|
838 |
+
console.log(`Accomplishment earned: ${nextPageData.reward.accomplishment}`);
|
839 |
+
}
|
840 |
}
|
841 |
const conModifier = Math.floor((gameState.character.stats.constitution - 10) / 2);
|
842 |
gameState.character.stats.maxHp = 10 + (conModifier * gameState.character.level);
|
843 |
gameState.character.stats.hp = Math.min(gameState.character.stats.hp, gameState.character.stats.maxHp);
|
844 |
if (nextPageId === 99 && gameState.character.stats.hp <= 0) {
|
845 |
+
renderPageInternal(99, gameData[99], rollResultMessage + levelUpMessage);
|
846 |
return;
|
847 |
}
|
848 |
} else {
|
849 |
console.error(`Data for page ${nextPageId} not found!`);
|
850 |
+
renderPageInternal(99, gameData[99], "<p><em>Error: Next page data missing!</em></p>" + levelUpMessage);
|
851 |
return;
|
852 |
}
|
853 |
+
renderPageInternal(nextPageId, gameData[nextPageId], rollResultMessage + levelUpMessage);
|
854 |
}
|
855 |
|
856 |
function renderPageInternal(pageId, pageData, message = "") {
|
|
|
859 |
storyContentElement.innerHTML = message + (pageData.content || "<p>...</p>");
|
860 |
updateStatsDisplay();
|
861 |
updateInventoryDisplay();
|
862 |
+
updateCharacterSheet();
|
863 |
choicesElement.innerHTML = '';
|
864 |
if (pageData.options && pageData.options.length > 0) {
|
865 |
pageData.options.forEach(option => {
|
|
|
884 |
const button = document.createElement('button');
|
885 |
button.classList.add('choice-button');
|
886 |
button.textContent = pageData.gameOver ? "Restart Adventure" : "The End";
|
887 |
+
button.onclick = () => handleChoiceClick({ nextPage: pageData.gameOver ? Ascendancy : 99 });
|
888 |
choicesElement.appendChild(button);
|
889 |
if (!pageData.gameOver) choicesElement.insertAdjacentHTML('afterbegin', '<p><i>The path ends here.</i></p>');
|
890 |
}
|
|
|
894 |
function renderPage(pageId) { renderPageInternal(pageId, gameData[pageId]); }
|
895 |
|
896 |
function updateStatsDisplay() {
|
897 |
+
const char = gameState.character;
|
898 |
statsElement.innerHTML = `<strong>Stats:</strong> <span>Lvl: ${char.level}</span> <span>XP: ${char.xp}/${char.xpToNextLevel}</span> <span>HP: ${char.stats.hp}/${char.stats.maxHp}</span> <span>Str: ${char.stats.strength}</span> <span>Int: ${char.stats.intelligence}</span> <span>Wis: ${char.stats.wisdom}</span> <span>Dex: ${char.stats.dexterity}</span> <span>Con: ${char.stats.constitution}</span> <span>Cha: ${char.stats.charisma}</span>`;
|
899 |
}
|
900 |
|
901 |
function updateInventoryDisplay() {
|
902 |
+
let h = '<strong>Inventory:</strong> ';
|
903 |
+
if (gameState.character.inventory.length === 0) {
|
904 |
+
h += '<em>Empty</em>';
|
905 |
} else {
|
906 |
+
gameState.character.inventory.forEach(i => {
|
907 |
+
const d = itemsData[i] || { type: 'unknown', description: '???' };
|
908 |
+
const c = `item-${d.type || 'unknown'}`;
|
909 |
+
h += `<span class="${c}" title="${d.description}">${i}</span>`;
|
910 |
});
|
911 |
}
|
912 |
inventoryElement.innerHTML = h;
|
913 |
}
|
914 |
|
915 |
+
function updateCharacterSheet() {
|
916 |
+
const char = gameState.character;
|
917 |
+
let h = '<strong>Character Sheet:</strong> ';
|
918 |
+
const attributes = ['strength', 'intelligence', 'wisdom', 'dexterity', 'constitution', 'charisma'];
|
919 |
+
let totalScore = 0;
|
920 |
+
attributes.forEach(attr => {
|
921 |
+
const value = char.stats[attr];
|
922 |
+
const wins = char.wins[attr];
|
923 |
+
const modifier = getAttributeModifier(value);
|
924 |
+
const score = (value + wins) * char.level;
|
925 |
+
totalScore += score;
|
926 |
+
h += `<span class="attribute">${attr.charAt(0).toUpperCase() + attr.slice(1)}: ${value} (Wins: ${wins}, Mod: +${modifier}, Score: ${score})</span>`;
|
927 |
+
});
|
928 |
+
h += `<span>Total Score: ${totalScore}</span>`;
|
929 |
+
if (char.accomplishments.length > 0) {
|
930 |
+
h += '<br><strong>Accomplishments:</strong> ';
|
931 |
+
char.accomplishments.forEach(acc => {
|
932 |
+
h += `<span class="accomplishment">${acc}</span>`;
|
933 |
+
});
|
934 |
+
}
|
935 |
+
characterSheetElement.innerHTML = h;
|
936 |
+
}
|
937 |
+
|
938 |
function updateScene(illustrationKey) {
|
939 |
if (currentAssemblyGroup) { scene.remove(currentAssemblyGroup); }
|
940 |
scene.fog = null;
|
|
|
994 |
scene.background = new THREE.Color(0x1A1A1A);
|
995 |
camera.position.set(0, 1.5, 4); camera.lookAt(0, 1, 0);
|
996 |
assemblyFunction = createDarkCaveAssembly; break;
|
997 |
+
case 'narrow-game-trail-forest-rope-bridge-ravine':
|
998 |
+
scene.fog = new THREE.Fog(0x2E4F3A, 5, 20);
|
999 |
+
camera.position.set(0, 2, 8); camera.lookAt(0, 1, 0);
|
1000 |
+
assemblyFunction = createMossyRavineAssembly; break;
|
1001 |
+
case 'rocky-badlands-cracked-earth-harsh-sun':
|
1002 |
+
scene.fog = new THREE.Fog(0xCC9966, 10, 40);
|
1003 |
+
scene.background = new THREE.Color(0xFFCC99);
|
1004 |
+
camera.position.set(0, 3, 12); camera.lookAt(0, 1, 0);
|
1005 |
+
assemblyFunction = createRockyBadlandsAssembly; break;
|
1006 |
+
case 'approaching-dark-fortress-walls-guards':
|
1007 |
+
scene.fog = new THREE.Fog(0x666666, 5, 30);
|
1008 |
+
scene.background = new THREE.Color(0x666666);
|
1009 |
+
camera.position.set(0, 3, 10); camera.lookAt(0, 2, -2);
|
1010 |
+
assemblyFunction = createMountainFortressAssembly; break;
|
1011 |
default:
|
1012 |
console.warn(`Unknown illustration key: "${illustrationKey}". Using default.`);
|
1013 |
assemblyFunction = createDefaultAssembly; break;
|
|
|
1029 |
scene.remove(child);
|
1030 |
}
|
1031 |
});
|
1032 |
+
const ambient
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|