Spaces:
Running
Running
Update game.js
Browse files
game.js
CHANGED
@@ -1,33 +1,18 @@
|
|
1 |
-
// --- Game State
|
|
|
2 |
let gameState = {
|
3 |
currentPageId: 1,
|
4 |
-
// π Encapsulate character data
|
5 |
character: {
|
6 |
-
name: "Hero",
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
level: 1,
|
11 |
-
xp: 0,
|
12 |
-
xpToNextLevel: 100, // Experience needed for level 2
|
13 |
-
statPointsPerLevel: 1, // How many points earned on level up (optional)
|
14 |
-
availableStatPoints: 0, // Points available to spend
|
15 |
-
stats: {
|
16 |
-
strength: 7,
|
17 |
-
intelligence: 5,
|
18 |
-
wisdom: 5, // Corrected spelling from before
|
19 |
-
dexterity: 6,
|
20 |
-
constitution: 6, // Added constitution
|
21 |
-
charisma: 5, // Added charisma
|
22 |
-
hp: 30,
|
23 |
-
maxHp: 30
|
24 |
-
},
|
25 |
-
inventory: [] // Will mirror items collected in game
|
26 |
}
|
27 |
-
// Note: We removed the top-level 'stats' and 'inventory' as they are now inside character
|
28 |
};
|
29 |
|
30 |
-
|
|
|
|
|
31 |
const charNameInput = document.getElementById('char-name');
|
32 |
const charRaceSpan = document.getElementById('char-race');
|
33 |
const charAlignmentSpan = document.getElementById('char-alignment');
|
@@ -38,26 +23,23 @@ const charXPNextSpan = document.getElementById('char-xp-next');
|
|
38 |
const charHPSpan = document.getElementById('char-hp');
|
39 |
const charMaxHPSpan = document.getElementById('char-max-hp');
|
40 |
const charInventoryList = document.getElementById('char-inventory-list');
|
41 |
-
const statSpans = {
|
42 |
-
strength: document.getElementById('stat-strength'),
|
43 |
-
|
44 |
-
|
45 |
-
dexterity: document.getElementById('stat-dexterity'),
|
46 |
-
constitution: document.getElementById('stat-constitution'),
|
47 |
-
charisma: document.getElementById('stat-charisma'),
|
48 |
};
|
49 |
const statIncreaseButtons = document.querySelectorAll('.stat-increase');
|
50 |
const levelUpButton = document.getElementById('levelup-btn');
|
51 |
const saveCharButton = document.getElementById('save-char-btn');
|
52 |
const exportCharButton = document.getElementById('export-char-btn');
|
53 |
const statIncreaseCostSpan = document.getElementById('stat-increase-cost');
|
|
|
54 |
|
55 |
-
// ---
|
56 |
|
57 |
-
/**
|
58 |
-
* Renders the entire character sheet based on gameState.character
|
59 |
-
*/
|
60 |
function renderCharacterSheet() {
|
|
|
|
|
61 |
const char = gameState.character;
|
62 |
|
63 |
charNameInput.value = char.name;
|
@@ -68,20 +50,17 @@ function renderCharacterSheet() {
|
|
68 |
charXPSpan.textContent = char.xp;
|
69 |
charXPNextSpan.textContent = char.xpToNextLevel;
|
70 |
|
71 |
-
// Update HP (ensure it doesn't exceed maxHP)
|
72 |
char.stats.hp = Math.min(char.stats.hp, char.stats.maxHp);
|
73 |
charHPSpan.textContent = char.stats.hp;
|
74 |
charMaxHPSpan.textContent = char.stats.maxHp;
|
75 |
|
76 |
-
// Update core stats display
|
77 |
for (const stat in statSpans) {
|
78 |
if (statSpans.hasOwnProperty(stat) && char.stats.hasOwnProperty(stat)) {
|
79 |
statSpans[stat].textContent = char.stats[stat];
|
80 |
}
|
81 |
}
|
82 |
|
83 |
-
|
84 |
-
charInventoryList.innerHTML = ''; // Clear previous list
|
85 |
const maxSlots = 15;
|
86 |
for (let i = 0; i < maxSlots; i++) {
|
87 |
const li = document.createElement('li');
|
@@ -94,221 +73,148 @@ function renderCharacterSheet() {
|
|
94 |
itemSpan.textContent = item;
|
95 |
li.appendChild(itemSpan);
|
96 |
} else {
|
97 |
-
// Add placeholder for empty slot
|
98 |
const emptySlotSpan = document.createElement('span');
|
99 |
emptySlotSpan.classList.add('item-slot');
|
|
|
100 |
li.appendChild(emptySlotSpan);
|
101 |
}
|
102 |
charInventoryList.appendChild(li);
|
103 |
}
|
104 |
|
105 |
-
//
|
106 |
-
updateLevelUpAvailability();
|
107 |
-
|
108 |
-
// Display cost to increase stat (example: level * 10)
|
109 |
-
statIncreaseCostSpan.textContent = calculateStatIncreaseCost();
|
110 |
}
|
111 |
|
112 |
-
/**
|
113 |
-
* Calculates the XP cost to increase a stat (example logic)
|
114 |
-
*/
|
115 |
function calculateStatIncreaseCost() {
|
116 |
-
//
|
117 |
-
return (gameState.character.level * 10) + 5;
|
118 |
}
|
119 |
|
120 |
-
/**
|
121 |
-
* Enables/disables level up and stat increase buttons based on XP/Points
|
122 |
-
*/
|
123 |
function updateLevelUpAvailability() {
|
124 |
const char = gameState.character;
|
125 |
const canLevelUp = char.xp >= char.xpToNextLevel;
|
126 |
levelUpButton.disabled = !canLevelUp;
|
127 |
|
128 |
-
const
|
|
|
|
|
|
|
129 |
statIncreaseButtons.forEach(button => {
|
130 |
-
|
131 |
-
|
132 |
-
// Optionally disable if level up is pending to force level up first?
|
133 |
// button.disabled = button.disabled || canLevelUp;
|
134 |
});
|
135 |
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
statIncreaseButtons.forEach(button => button.disabled = false);
|
140 |
-
} else {
|
141 |
-
statIncreaseCostSpan.parentElement.innerHTML = `<small>Cost to increase stat: <span id="stat-increase-cost">${calculateStatIncreaseCost()}</span> XP</small>`;
|
142 |
-
// Disable based on XP check done above
|
143 |
-
}
|
144 |
}
|
145 |
|
146 |
-
|
147 |
-
* Handles leveling up the character
|
148 |
-
*/
|
149 |
function handleLevelUp() {
|
150 |
-
|
|
|
151 |
if (char.xp >= char.xpToNextLevel) {
|
152 |
char.level++;
|
153 |
-
char.xp -= char.xpToNextLevel;
|
154 |
-
char.xpToNextLevel = Math.floor(char.xpToNextLevel * 1.6);
|
155 |
-
char.availableStatPoints += char.statPointsPerLevel;
|
156 |
|
157 |
-
// Increase max HP based on Constitution (example: + half CON modifier)
|
158 |
const conModifier = Math.floor((char.stats.constitution - 10) / 2);
|
159 |
-
const hpGain = Math.max(1, Math.floor(Math.random() * 6) + 1 + conModifier);
|
160 |
char.stats.maxHp += hpGain;
|
161 |
-
char.stats.hp = char.stats.maxHp;
|
162 |
|
163 |
-
console.log(
|
164 |
-
renderCharacterSheet();
|
165 |
} else {
|
166 |
console.warn("Not enough XP to level up yet.");
|
167 |
}
|
168 |
}
|
169 |
|
170 |
-
/**
|
171 |
-
* Handles increasing a specific stat
|
172 |
-
*/
|
173 |
function handleStatIncrease(statName) {
|
|
|
174 |
const char = gameState.character;
|
175 |
const cost = calculateStatIncreaseCost();
|
176 |
|
177 |
-
// Priority 1: Spend available stat points
|
178 |
if (char.availableStatPoints > 0) {
|
179 |
char.stats[statName]++;
|
180 |
char.availableStatPoints--;
|
181 |
-
console.log(
|
182 |
-
|
183 |
-
// Update derived stats if needed (e.g., CON affects maxHP)
|
184 |
-
if (statName === 'constitution') {
|
185 |
-
const oldModifier = Math.floor((char.stats.constitution - 1 - 10) / 2);
|
186 |
-
const newModifier = Math.floor((char.stats.constitution - 10) / 2);
|
187 |
-
const hpBonusPerLevel = Math.max(0, newModifier - oldModifier) * char.level; // Gain HP retroactively? Or just going forward? Simpler: just add difference.
|
188 |
-
if(hpBonusPerLevel > 0) {
|
189 |
-
console.log(`Increased max HP by ${hpBonusPerLevel} due to CON increase.`);
|
190 |
-
char.stats.maxHp += hpBonusPerLevel;
|
191 |
-
char.stats.hp += hpBonusPerLevel; // Also increase current HP
|
192 |
-
}
|
193 |
-
}
|
194 |
-
|
195 |
renderCharacterSheet();
|
196 |
-
return;
|
197 |
}
|
198 |
|
199 |
-
// Priority 2: Spend XP if no points are available
|
200 |
if (char.xp >= cost) {
|
201 |
char.stats[statName]++;
|
202 |
char.xp -= cost;
|
203 |
-
console.log(
|
204 |
-
|
205 |
-
// Update derived stats (same as above)
|
206 |
-
if (statName === 'constitution') {
|
207 |
-
const oldModifier = Math.floor((char.stats.constitution - 1 - 10) / 2);
|
208 |
-
const newModifier = Math.floor((char.stats.constitution - 10) / 2);
|
209 |
-
const hpBonusPerLevel = Math.max(0, newModifier - oldModifier) * char.level;
|
210 |
-
if(hpBonusPerLevel > 0) {
|
211 |
-
console.log(`Increased max HP by ${hpBonusPerLevel} due to CON increase.`);
|
212 |
-
char.stats.maxHp += hpBonusPerLevel;
|
213 |
-
char.stats.hp += hpBonusPerLevel;
|
214 |
-
}
|
215 |
-
}
|
216 |
-
|
217 |
renderCharacterSheet();
|
218 |
} else {
|
219 |
-
console.warn(`Not enough XP
|
220 |
}
|
221 |
}
|
222 |
|
223 |
|
224 |
-
/**
|
225 |
-
* Saves character data to localStorage
|
226 |
-
*/
|
227 |
function saveCharacter() {
|
228 |
try {
|
229 |
localStorage.setItem('textAdventureCharacter', JSON.stringify(gameState.character));
|
230 |
-
console.log('
|
231 |
-
//
|
232 |
-
saveCharButton.textContent = '
|
233 |
-
|
|
|
|
|
|
|
|
|
234 |
} catch (e) {
|
235 |
-
console.error('Error saving character
|
236 |
-
alert('Failed to save character.
|
237 |
}
|
238 |
}
|
239 |
|
240 |
-
/**
|
241 |
-
* Loads character data from localStorage
|
242 |
-
*/
|
243 |
function loadCharacter() {
|
244 |
-
|
|
|
245 |
const savedData = localStorage.getItem('textAdventureCharacter');
|
246 |
if (savedData) {
|
247 |
const loadedChar = JSON.parse(savedData);
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
...loadedChar, // Override with loaded data
|
252 |
-
stats: { // Ensure stats object exists and merge
|
253 |
-
...gameState.character.stats,
|
254 |
-
...(loadedChar.stats || {})
|
255 |
-
},
|
256 |
-
inventory: loadedChar.inventory || [] // Ensure inventory array exists
|
257 |
-
};
|
258 |
-
console.log('πΎ Character loaded from local storage.');
|
259 |
-
return true; // Indicate success
|
260 |
}
|
261 |
-
} catch (e) {
|
262 |
-
|
263 |
-
// Don't overwrite gameState if loading fails
|
264 |
-
}
|
265 |
-
return false; // Indicate nothing loaded or error
|
266 |
}
|
267 |
|
268 |
-
/**
|
269 |
-
* Exports character data as a JSON file download
|
270 |
-
*/
|
271 |
function exportCharacter() {
|
272 |
-
|
273 |
-
|
|
|
274 |
const blob = new Blob([charJson], { type: 'application/json' });
|
275 |
const url = URL.createObjectURL(blob);
|
276 |
-
const a = document.createElement('a');
|
277 |
-
a.href = url;
|
278 |
-
// Sanitize name for filename
|
279 |
const filename = `${gameState.character.name.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'character'}_save.json`;
|
280 |
-
a.download = filename;
|
281 |
-
|
282 |
-
|
283 |
-
document.body.removeChild(a);
|
284 |
-
URL.revokeObjectURL(url); // Clean up
|
285 |
-
console.log(`π€ Character exported as ${filename}`);
|
286 |
-
} catch (e) {
|
287 |
-
console.error('Error exporting character:', e);
|
288 |
-
alert('Failed to export character data.');
|
289 |
-
}
|
290 |
}
|
291 |
|
292 |
-
|
|
|
|
|
293 |
charNameInput.addEventListener('change', () => {
|
294 |
gameState.character.name = charNameInput.value.trim() || "Hero";
|
295 |
-
|
296 |
-
console.log(`π€ Name changed to: ${gameState.character.name}`);
|
297 |
-
// Maybe save automatically on name change?
|
298 |
-
// saveCharacter();
|
299 |
});
|
300 |
-
|
301 |
levelUpButton.addEventListener('click', handleLevelUp);
|
302 |
-
|
303 |
statIncreaseButtons.forEach(button => {
|
304 |
button.addEventListener('click', () => {
|
305 |
const statToIncrease = button.dataset.stat;
|
306 |
-
if (statToIncrease) {
|
307 |
-
handleStatIncrease(statToIncrease);
|
308 |
-
}
|
309 |
});
|
310 |
});
|
311 |
-
|
312 |
saveCharButton.addEventListener('click', saveCharacter);
|
313 |
exportCharButton.addEventListener('click', exportCharacter);
|
314 |
|
@@ -316,110 +222,38 @@ exportCharButton.addEventListener('click', exportCharacter);
|
|
316 |
// --- Modify Existing Functions ---
|
317 |
|
318 |
function startGame() {
|
319 |
-
//
|
320 |
-
if (!loadCharacter()) {
|
321 |
-
// If no save found, initialize with defaults (already done by gameState definition)
|
322 |
-
console.log("No saved character found, starting new.");
|
323 |
-
}
|
324 |
-
// Ensure compatibility if loaded save is old/missing fields
|
325 |
gameState.character = {
|
326 |
-
...{
|
327 |
-
|
328 |
-
level: 1, xp: 0, xpToNextLevel: 100, statPointsPerLevel: 1, availableStatPoints: 0,
|
329 |
-
stats: { strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30 },
|
330 |
-
inventory: []
|
331 |
-
},
|
332 |
-
...gameState.character // Loaded data overrides defaults
|
333 |
};
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
gameState.currentPageId
|
341 |
-
renderCharacterSheet(); // Initial render of the sheet
|
342 |
-
renderPage(gameState.currentPageId); // Render the story page
|
343 |
}
|
344 |
|
345 |
function handleChoiceClick(choiceData) {
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
console.log("Added item:", itemToAdd);
|
357 |
-
// Limit inventory size?
|
358 |
-
// if (gameState.character.inventory.length > 15) { /* Handle overflow */ }
|
359 |
-
}
|
360 |
-
|
361 |
-
// Process Landing Page Effects
|
362 |
-
gameState.currentPageId = nextPageId;
|
363 |
-
const nextPageData = gameData[nextPageId];
|
364 |
-
|
365 |
-
if (nextPageData) {
|
366 |
-
// Apply HP loss defined on the *landing* page
|
367 |
-
if (nextPageData.hpLoss) {
|
368 |
-
gameState.character.stats.hp -= nextPageData.hpLoss; // Update character HP
|
369 |
-
console.log(`Lost ${nextPageData.hpLoss} HP.`);
|
370 |
-
if (gameState.character.stats.hp <= 0) {
|
371 |
-
gameState.character.stats.hp = 0;
|
372 |
-
console.log("Player died from HP loss!");
|
373 |
-
renderCharacterSheet(); // Update sheet before showing game over
|
374 |
-
renderPage(99); // Go to game over page
|
375 |
-
return;
|
376 |
-
}
|
377 |
-
}
|
378 |
-
|
379 |
-
// --- Apply Rewards (New) ---
|
380 |
-
if (nextPageData.reward) {
|
381 |
-
if (nextPageData.reward.xp) {
|
382 |
-
gameState.character.xp += nextPageData.reward.xp;
|
383 |
-
console.log(`β¨ Gained ${nextPageData.reward.xp} XP!`);
|
384 |
-
}
|
385 |
-
if (nextPageData.reward.statIncrease) {
|
386 |
-
const stat = nextPageData.reward.statIncrease.stat;
|
387 |
-
const amount = nextPageData.reward.statIncrease.amount;
|
388 |
-
if (gameState.character.stats.hasOwnProperty(stat)) {
|
389 |
-
gameState.character.stats[stat] += amount;
|
390 |
-
console.log(`π Stat ${stat} increased by ${amount}!`);
|
391 |
-
// Update derived stats if needed (e.g., CON -> HP)
|
392 |
-
if (stat === 'constitution') { /* ... update maxHP ... */ }
|
393 |
-
}
|
394 |
-
}
|
395 |
-
// Add other reward types here (e.g., items, stat points)
|
396 |
-
if(nextPageData.reward.addItem && !gameState.character.inventory.includes(nextPageData.reward.addItem)){
|
397 |
-
gameState.character.inventory.push(nextPageData.reward.addItem);
|
398 |
-
console.log(`π Found item: ${nextPageData.reward.addItem}`);
|
399 |
-
}
|
400 |
-
}
|
401 |
-
|
402 |
-
// Check if landing page is game over
|
403 |
-
if (nextPageData.gameOver) {
|
404 |
-
console.log("Reached Game Over page.");
|
405 |
-
renderCharacterSheet(); // Update sheet one last time
|
406 |
-
renderPage(nextPageId);
|
407 |
-
return;
|
408 |
-
}
|
409 |
|
410 |
-
} else {
|
411 |
-
console.error(`Data for page ${nextPageId} not found!`);
|
412 |
-
renderCharacterSheet();
|
413 |
-
renderPage(99); // Fallback to game over
|
414 |
-
return;
|
415 |
-
}
|
416 |
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
renderPage(nextPageId);
|
421 |
-
}
|
422 |
|
423 |
-
// ---
|
424 |
-
//
|
425 |
-
|
|
|
1 |
+
// --- Game State ---
|
2 |
+
// (Keep the gameState definition from the previous step, including the nested 'character' object)
|
3 |
let gameState = {
|
4 |
currentPageId: 1,
|
|
|
5 |
character: {
|
6 |
+
name: "Hero", race: "Human", alignment: "Neutral Good", class: "Fighter",
|
7 |
+
level: 1, xp: 0, xpToNextLevel: 100, statPointsPerLevel: 1, availableStatPoints: 0,
|
8 |
+
stats: { strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30 },
|
9 |
+
inventory: []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
}
|
|
|
11 |
};
|
12 |
|
13 |
+
|
14 |
+
// --- DOM Element Getters ---
|
15 |
+
// (Keep all the getters from the previous step: charNameInput, charRaceSpan, etc.)
|
16 |
const charNameInput = document.getElementById('char-name');
|
17 |
const charRaceSpan = document.getElementById('char-race');
|
18 |
const charAlignmentSpan = document.getElementById('char-alignment');
|
|
|
23 |
const charHPSpan = document.getElementById('char-hp');
|
24 |
const charMaxHPSpan = document.getElementById('char-max-hp');
|
25 |
const charInventoryList = document.getElementById('char-inventory-list');
|
26 |
+
const statSpans = {
|
27 |
+
strength: document.getElementById('stat-strength'), intelligence: document.getElementById('stat-intelligence'),
|
28 |
+
wisdom: document.getElementById('stat-wisdom'), dexterity: document.getElementById('stat-dexterity'),
|
29 |
+
constitution: document.getElementById('stat-constitution'), charisma: document.getElementById('stat-charisma'),
|
|
|
|
|
|
|
30 |
};
|
31 |
const statIncreaseButtons = document.querySelectorAll('.stat-increase');
|
32 |
const levelUpButton = document.getElementById('levelup-btn');
|
33 |
const saveCharButton = document.getElementById('save-char-btn');
|
34 |
const exportCharButton = document.getElementById('export-char-btn');
|
35 |
const statIncreaseCostSpan = document.getElementById('stat-increase-cost');
|
36 |
+
const statPointsAvailableSpan = document.getElementById('stat-points-available'); // Added span for clarity
|
37 |
|
38 |
+
// --- Character Sheet Functions ---
|
39 |
|
|
|
|
|
|
|
40 |
function renderCharacterSheet() {
|
41 |
+
// (Keep the logic from the previous renderCharacterSheet function)
|
42 |
+
// ...
|
43 |
const char = gameState.character;
|
44 |
|
45 |
charNameInput.value = char.name;
|
|
|
50 |
charXPSpan.textContent = char.xp;
|
51 |
charXPNextSpan.textContent = char.xpToNextLevel;
|
52 |
|
|
|
53 |
char.stats.hp = Math.min(char.stats.hp, char.stats.maxHp);
|
54 |
charHPSpan.textContent = char.stats.hp;
|
55 |
charMaxHPSpan.textContent = char.stats.maxHp;
|
56 |
|
|
|
57 |
for (const stat in statSpans) {
|
58 |
if (statSpans.hasOwnProperty(stat) && char.stats.hasOwnProperty(stat)) {
|
59 |
statSpans[stat].textContent = char.stats[stat];
|
60 |
}
|
61 |
}
|
62 |
|
63 |
+
charInventoryList.innerHTML = '';
|
|
|
64 |
const maxSlots = 15;
|
65 |
for (let i = 0; i < maxSlots; i++) {
|
66 |
const li = document.createElement('li');
|
|
|
73 |
itemSpan.textContent = item;
|
74 |
li.appendChild(itemSpan);
|
75 |
} else {
|
|
|
76 |
const emptySlotSpan = document.createElement('span');
|
77 |
emptySlotSpan.classList.add('item-slot');
|
78 |
+
emptySlotSpan.textContent = '[Empty]'; // Set text directly
|
79 |
li.appendChild(emptySlotSpan);
|
80 |
}
|
81 |
charInventoryList.appendChild(li);
|
82 |
}
|
83 |
|
84 |
+
updateLevelUpAvailability(); // Handles button disabling logic
|
|
|
|
|
|
|
|
|
85 |
}
|
86 |
|
|
|
|
|
|
|
87 |
function calculateStatIncreaseCost() {
|
88 |
+
// (Keep the same logic)
|
89 |
+
return (gameState.character.level * 10) + 5;
|
90 |
}
|
91 |
|
|
|
|
|
|
|
92 |
function updateLevelUpAvailability() {
|
93 |
const char = gameState.character;
|
94 |
const canLevelUp = char.xp >= char.xpToNextLevel;
|
95 |
levelUpButton.disabled = !canLevelUp;
|
96 |
|
97 |
+
const cost = calculateStatIncreaseCost();
|
98 |
+
const canIncreaseWithXP = char.xp >= cost;
|
99 |
+
const canIncreaseWithPoints = char.availableStatPoints > 0;
|
100 |
+
|
101 |
statIncreaseButtons.forEach(button => {
|
102 |
+
button.disabled = !(canIncreaseWithPoints || canIncreaseWithXP);
|
103 |
+
// Optionally disable if level up is pending
|
|
|
104 |
// button.disabled = button.disabled || canLevelUp;
|
105 |
});
|
106 |
|
107 |
+
// Update cost/points display text
|
108 |
+
statIncreaseCostSpan.textContent = cost;
|
109 |
+
statPointsAvailableSpan.textContent = char.availableStatPoints;
|
|
|
|
|
|
|
|
|
|
|
110 |
}
|
111 |
|
112 |
+
|
|
|
|
|
113 |
function handleLevelUp() {
|
114 |
+
// (Keep the same logic)
|
115 |
+
const char = gameState.character;
|
116 |
if (char.xp >= char.xpToNextLevel) {
|
117 |
char.level++;
|
118 |
+
char.xp -= char.xpToNextLevel;
|
119 |
+
char.xpToNextLevel = Math.floor(char.xpToNextLevel * 1.6);
|
120 |
+
char.availableStatPoints += char.statPointsPerLevel;
|
121 |
|
|
|
122 |
const conModifier = Math.floor((char.stats.constitution - 10) / 2);
|
123 |
+
const hpGain = Math.max(1, Math.floor(Math.random() * 6) + 1 + conModifier);
|
124 |
char.stats.maxHp += hpGain;
|
125 |
+
char.stats.hp = char.stats.maxHp;
|
126 |
|
127 |
+
console.log(`Leveled Up to ${char.level}! Gained ${char.statPointsPerLevel} stat point(s) and ${hpGain} HP.`);
|
128 |
+
renderCharacterSheet();
|
129 |
} else {
|
130 |
console.warn("Not enough XP to level up yet.");
|
131 |
}
|
132 |
}
|
133 |
|
|
|
|
|
|
|
134 |
function handleStatIncrease(statName) {
|
135 |
+
// (Keep the same logic, including point spending priority)
|
136 |
const char = gameState.character;
|
137 |
const cost = calculateStatIncreaseCost();
|
138 |
|
|
|
139 |
if (char.availableStatPoints > 0) {
|
140 |
char.stats[statName]++;
|
141 |
char.availableStatPoints--;
|
142 |
+
console.log(`Increased ${statName} using a point. ${char.availableStatPoints} points remaining.`);
|
143 |
+
if (statName === 'constitution') { /* ... update maxHP ... */ } // Add HP update logic here if needed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
renderCharacterSheet();
|
145 |
+
return;
|
146 |
}
|
147 |
|
|
|
148 |
if (char.xp >= cost) {
|
149 |
char.stats[statName]++;
|
150 |
char.xp -= cost;
|
151 |
+
console.log(`Increased ${statName} for ${cost} XP.`);
|
152 |
+
if (statName === 'constitution') { /* ... update maxHP ... */ } // Add HP update logic here if needed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
renderCharacterSheet();
|
154 |
} else {
|
155 |
+
console.warn(`Not enough XP or points to increase ${statName}.`);
|
156 |
}
|
157 |
}
|
158 |
|
159 |
|
|
|
|
|
|
|
160 |
function saveCharacter() {
|
161 |
try {
|
162 |
localStorage.setItem('textAdventureCharacter', JSON.stringify(gameState.character));
|
163 |
+
console.log('Character saved locally.');
|
164 |
+
// Update button text for confirmation - NO EMOJI
|
165 |
+
saveCharButton.textContent = 'Saved!';
|
166 |
+
saveCharButton.disabled = true; // Briefly disable
|
167 |
+
setTimeout(() => {
|
168 |
+
saveCharButton.textContent = 'Save'; // Restore original text
|
169 |
+
saveCharButton.disabled = false;
|
170 |
+
}, 1500);
|
171 |
} catch (e) {
|
172 |
+
console.error('Error saving character:', e);
|
173 |
+
alert('Failed to save character.');
|
174 |
}
|
175 |
}
|
176 |
|
|
|
|
|
|
|
177 |
function loadCharacter() {
|
178 |
+
// (Keep the same logic)
|
179 |
+
try {
|
180 |
const savedData = localStorage.getItem('textAdventureCharacter');
|
181 |
if (savedData) {
|
182 |
const loadedChar = JSON.parse(savedData);
|
183 |
+
gameState.character = { ...gameState.character, ...loadedChar, stats: { ...gameState.character.stats, ...(loadedChar.stats || {}) }, inventory: loadedChar.inventory || [] };
|
184 |
+
console.log('Character loaded from local storage.');
|
185 |
+
return true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
}
|
187 |
+
} catch (e) { console.error('Error loading character:', e); }
|
188 |
+
return false;
|
|
|
|
|
|
|
189 |
}
|
190 |
|
|
|
|
|
|
|
191 |
function exportCharacter() {
|
192 |
+
// (Keep the same logic)
|
193 |
+
try {
|
194 |
+
const charJson = JSON.stringify(gameState.character, null, 2);
|
195 |
const blob = new Blob([charJson], { type: 'application/json' });
|
196 |
const url = URL.createObjectURL(blob);
|
197 |
+
const a = document.createElement('a'); a.href = url;
|
|
|
|
|
198 |
const filename = `${gameState.character.name.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'character'}_save.json`;
|
199 |
+
a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
200 |
+
console.log(`Character exported as ${filename}`);
|
201 |
+
} catch (e) { console.error('Error exporting character:', e); alert('Failed to export character data.'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
202 |
}
|
203 |
|
204 |
+
|
205 |
+
// --- Event Listeners ---
|
206 |
+
// (Keep the same event listeners, they reference elements by ID)
|
207 |
charNameInput.addEventListener('change', () => {
|
208 |
gameState.character.name = charNameInput.value.trim() || "Hero";
|
209 |
+
console.log(`Name changed to: ${gameState.character.name}`);
|
|
|
|
|
|
|
210 |
});
|
|
|
211 |
levelUpButton.addEventListener('click', handleLevelUp);
|
|
|
212 |
statIncreaseButtons.forEach(button => {
|
213 |
button.addEventListener('click', () => {
|
214 |
const statToIncrease = button.dataset.stat;
|
215 |
+
if (statToIncrease) { handleStatIncrease(statToIncrease); }
|
|
|
|
|
216 |
});
|
217 |
});
|
|
|
218 |
saveCharButton.addEventListener('click', saveCharacter);
|
219 |
exportCharButton.addEventListener('click', exportCharacter);
|
220 |
|
|
|
222 |
// --- Modify Existing Functions ---
|
223 |
|
224 |
function startGame() {
|
225 |
+
// (Keep the same logic, including loadCharacter and default merging)
|
226 |
+
if (!loadCharacter()) { console.log("No saved character found, starting new."); }
|
|
|
|
|
|
|
|
|
227 |
gameState.character = {
|
228 |
+
...{ name: "Hero", race: "Human", alignment: "Neutral Good", class: "Fighter", level: 1, xp: 0, xpToNextLevel: 100, statPointsPerLevel: 1, availableStatPoints: 0, stats: { strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30 }, inventory: [] },
|
229 |
+
...gameState.character
|
|
|
|
|
|
|
|
|
|
|
230 |
};
|
231 |
+
gameState.character.stats = {
|
232 |
+
strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30,
|
233 |
+
...(gameState.character.stats || {})
|
234 |
+
}
|
235 |
+
gameState.currentPageId = 1;
|
236 |
+
renderCharacterSheet();
|
237 |
+
renderPage(gameState.currentPageId);
|
|
|
|
|
238 |
}
|
239 |
|
240 |
function handleChoiceClick(choiceData) {
|
241 |
+
// (Keep the same logic, including reward processing)
|
242 |
+
const nextPageId = parseInt(choiceData.nextPage); const itemToAdd = choiceData.addItem; if (isNaN(nextPageId)) { console.error("Invalid nextPageId:", choiceData.nextPage); return; }
|
243 |
+
if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { gameState.character.inventory.push(itemToAdd); console.log("Added item:", itemToAdd); }
|
244 |
+
gameState.currentPageId = nextPageId; const nextPageData = gameData[nextPageId];
|
245 |
+
if (nextPageData) { if (nextPageData.hpLoss) { gameState.character.stats.hp -= nextPageData.hpLoss; console.log(`Lost ${nextPageData.hpLoss} HP.`); if (gameState.character.stats.hp <= 0) { gameState.character.stats.hp = 0; console.log("Player died from HP loss!"); renderCharacterSheet(); renderPage(99); return; } }
|
246 |
+
if (nextPageData.reward) { if (nextPageData.reward.xp) { gameState.character.xp += nextPageData.reward.xp; console.log(`Gained ${nextPageData.reward.xp} XP!`); } if (nextPageData.reward.statIncrease) { const stat = nextPageData.reward.statIncrease.stat; const amount = nextPageData.reward.statIncrease.amount; if (gameState.character.stats.hasOwnProperty(stat)) { gameState.character.stats[stat] += amount; console.log(`Stat ${stat} increased by ${amount}!`); if (stat === 'constitution') { /* ... update maxHP ... */ } } } if(nextPageData.reward.addItem && !gameState.character.inventory.includes(nextPageData.reward.addItem)){ gameState.character.inventory.push(nextPageData.reward.addItem); console.log(`Found item: ${nextPageData.reward.addItem}`); } }
|
247 |
+
if (nextPageData.gameOver) { console.log("Reached Game Over page."); renderCharacterSheet(); renderPage(nextPageId); return; }
|
248 |
+
} else { console.error(`Data for page ${nextPageId} not found!`); renderCharacterSheet(); renderPage(99); return; }
|
249 |
+
renderCharacterSheet(); renderPage(nextPageId);
|
250 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
|
253 |
+
// --- REMEMBER ---
|
254 |
+
// Make sure to remove the OLD #stats-display and #inventory-display elements from your HTML
|
255 |
+
// and the old updateStatsDisplay() and updateInventoryDisplay() functions from your JS.
|
|
|
|
|
256 |
|
257 |
+
// --- Initialization ---
|
258 |
+
// initThreeJS(); // Keep your Three.js initialization
|
259 |
+
startGame();
|