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();
|