Spaces:
Running
Running
// --- Game State (Modify Existing) --- | |
let gameState = { | |
currentPageId: 1, | |
// π Encapsulate character data | |
character: { | |
name: "Hero", | |
race: "Human", | |
alignment: "Neutral Good", | |
class: "Fighter", | |
level: 1, | |
xp: 0, | |
xpToNextLevel: 100, // Experience needed for level 2 | |
statPointsPerLevel: 1, // How many points earned on level up (optional) | |
availableStatPoints: 0, // Points available to spend | |
stats: { | |
strength: 7, | |
intelligence: 5, | |
wisdom: 5, // Corrected spelling from before | |
dexterity: 6, | |
constitution: 6, // Added constitution | |
charisma: 5, // Added charisma | |
hp: 30, | |
maxHp: 30 | |
}, | |
inventory: [] // Will mirror items collected in game | |
} | |
// Note: We removed the top-level 'stats' and 'inventory' as they are now inside character | |
}; | |
// --- DOM Element Getters (Add New) --- | |
const charNameInput = document.getElementById('char-name'); | |
const charRaceSpan = document.getElementById('char-race'); | |
const charAlignmentSpan = document.getElementById('char-alignment'); | |
const charClassSpan = document.getElementById('char-class'); | |
const charLevelSpan = document.getElementById('char-level'); | |
const charXPSpan = document.getElementById('char-xp'); | |
const charXPNextSpan = document.getElementById('char-xp-next'); | |
const charHPSpan = document.getElementById('char-hp'); | |
const charMaxHPSpan = document.getElementById('char-max-hp'); | |
const charInventoryList = document.getElementById('char-inventory-list'); | |
const statSpans = { // Map stat names to their display elements | |
strength: document.getElementById('stat-strength'), | |
intelligence: document.getElementById('stat-intelligence'), | |
wisdom: document.getElementById('stat-wisdom'), | |
dexterity: document.getElementById('stat-dexterity'), | |
constitution: document.getElementById('stat-constitution'), | |
charisma: document.getElementById('stat-charisma'), | |
}; | |
const statIncreaseButtons = document.querySelectorAll('.stat-increase'); | |
const levelUpButton = document.getElementById('levelup-btn'); | |
const saveCharButton = document.getElementById('save-char-btn'); | |
const exportCharButton = document.getElementById('export-char-btn'); | |
const statIncreaseCostSpan = document.getElementById('stat-increase-cost'); | |
// --- NEW Character Sheet Functions --- | |
/** | |
* Renders the entire character sheet based on gameState.character | |
*/ | |
function renderCharacterSheet() { | |
const char = gameState.character; | |
charNameInput.value = char.name; | |
charRaceSpan.textContent = char.race; | |
charAlignmentSpan.textContent = char.alignment; | |
charClassSpan.textContent = char.class; | |
charLevelSpan.textContent = char.level; | |
charXPSpan.textContent = char.xp; | |
charXPNextSpan.textContent = char.xpToNextLevel; | |
// Update HP (ensure it doesn't exceed maxHP) | |
char.stats.hp = Math.min(char.stats.hp, char.stats.maxHp); | |
charHPSpan.textContent = char.stats.hp; | |
charMaxHPSpan.textContent = char.stats.maxHp; | |
// Update core stats display | |
for (const stat in statSpans) { | |
if (statSpans.hasOwnProperty(stat) && char.stats.hasOwnProperty(stat)) { | |
statSpans[stat].textContent = char.stats[stat]; | |
} | |
} | |
// Update inventory list (up to 15 slots) | |
charInventoryList.innerHTML = ''; // Clear previous list | |
const maxSlots = 15; | |
for (let i = 0; i < maxSlots; i++) { | |
const li = document.createElement('li'); | |
if (i < char.inventory.length) { | |
const item = char.inventory[i]; | |
const itemInfo = itemsData[item] || { type: 'unknown', description: '???' }; | |
const itemSpan = document.createElement('span'); | |
itemSpan.classList.add(`item-${itemInfo.type || 'unknown'}`); | |
itemSpan.title = itemInfo.description; | |
itemSpan.textContent = item; | |
li.appendChild(itemSpan); | |
} else { | |
// Add placeholder for empty slot | |
const emptySlotSpan = document.createElement('span'); | |
emptySlotSpan.classList.add('item-slot'); | |
li.appendChild(emptySlotSpan); | |
} | |
charInventoryList.appendChild(li); | |
} | |
// Update level up / stat increase buttons state | |
updateLevelUpAvailability(); | |
// Display cost to increase stat (example: level * 10) | |
statIncreaseCostSpan.textContent = calculateStatIncreaseCost(); | |
} | |
/** | |
* Calculates the XP cost to increase a stat (example logic) | |
*/ | |
function calculateStatIncreaseCost() { | |
// Cost could depend on current stat value or level | |
return (gameState.character.level * 10) + 5; // Example: 15 XP at level 1, 25 at level 2 etc. | |
} | |
/** | |
* Enables/disables level up and stat increase buttons based on XP/Points | |
*/ | |
function updateLevelUpAvailability() { | |
const char = gameState.character; | |
const canLevelUp = char.xp >= char.xpToNextLevel; | |
levelUpButton.disabled = !canLevelUp; | |
const canIncreaseStat = char.availableStatPoints > 0 || (char.xp >= calculateStatIncreaseCost()); // Can spend points OR XP | |
statIncreaseButtons.forEach(button => { | |
// Enable if points available OR if enough XP (and not leveling up) | |
button.disabled = !(char.availableStatPoints > 0 || (char.xp >= calculateStatIncreaseCost())); | |
// Optionally disable if level up is pending to force level up first? | |
// button.disabled = button.disabled || canLevelUp; | |
}); | |
// Enable spending stat points ONLY if available > 0 | |
if (char.availableStatPoints > 0) { | |
statIncreaseCostSpan.parentElement.innerHTML = `<small>Available points: ${char.availableStatPoints} / Cost per point: 1</small>`; | |
statIncreaseButtons.forEach(button => button.disabled = false); | |
} else { | |
statIncreaseCostSpan.parentElement.innerHTML = `<small>Cost to increase stat: <span id="stat-increase-cost">${calculateStatIncreaseCost()}</span> XP</small>`; | |
// Disable based on XP check done above | |
} | |
} | |
/** | |
* Handles leveling up the character | |
*/ | |
function handleLevelUp() { | |
const char = gameState.character; | |
if (char.xp >= char.xpToNextLevel) { | |
char.level++; | |
char.xp -= char.xpToNextLevel; // Subtract cost | |
char.xpToNextLevel = Math.floor(char.xpToNextLevel * 1.6); // Increase next level cost (adjust multiplier) | |
char.availableStatPoints += char.statPointsPerLevel; // Grant stat points | |
// Increase max HP based on Constitution (example: + half CON modifier) | |
const conModifier = Math.floor((char.stats.constitution - 10) / 2); | |
const hpGain = Math.max(1, Math.floor(Math.random() * 6) + 1 + conModifier); // Roll d6 + CON mod (like D&D) | |
char.stats.maxHp += hpGain; | |
char.stats.hp = char.stats.maxHp; // Full heal on level up | |
console.log(`π Leveled Up to ${char.level}! Gained ${char.statPointsPerLevel} stat point(s) and ${hpGain} HP.`); | |
renderCharacterSheet(); // Update display | |
} else { | |
console.warn("Not enough XP to level up yet."); | |
} | |
} | |
/** | |
* Handles increasing a specific stat | |
*/ | |
function handleStatIncrease(statName) { | |
const char = gameState.character; | |
const cost = calculateStatIncreaseCost(); | |
// Priority 1: Spend available stat points | |
if (char.availableStatPoints > 0) { | |
char.stats[statName]++; | |
char.availableStatPoints--; | |
console.log(`π Increased ${statName} using a point. ${char.availableStatPoints} points remaining.`); | |
// Update derived stats if needed (e.g., CON affects maxHP) | |
if (statName === 'constitution') { | |
const oldModifier = Math.floor((char.stats.constitution - 1 - 10) / 2); | |
const newModifier = Math.floor((char.stats.constitution - 10) / 2); | |
const hpBonusPerLevel = Math.max(0, newModifier - oldModifier) * char.level; // Gain HP retroactively? Or just going forward? Simpler: just add difference. | |
if(hpBonusPerLevel > 0) { | |
console.log(`Increased max HP by ${hpBonusPerLevel} due to CON increase.`); | |
char.stats.maxHp += hpBonusPerLevel; | |
char.stats.hp += hpBonusPerLevel; // Also increase current HP | |
} | |
} | |
renderCharacterSheet(); | |
return; // Exit after spending a point | |
} | |
// Priority 2: Spend XP if no points are available | |
if (char.xp >= cost) { | |
char.stats[statName]++; | |
char.xp -= cost; | |
console.log(`πͺ Increased ${statName} for ${cost} XP.`); | |
// Update derived stats (same as above) | |
if (statName === 'constitution') { | |
const oldModifier = Math.floor((char.stats.constitution - 1 - 10) / 2); | |
const newModifier = Math.floor((char.stats.constitution - 10) / 2); | |
const hpBonusPerLevel = Math.max(0, newModifier - oldModifier) * char.level; | |
if(hpBonusPerLevel > 0) { | |
console.log(`Increased max HP by ${hpBonusPerLevel} due to CON increase.`); | |
char.stats.maxHp += hpBonusPerLevel; | |
char.stats.hp += hpBonusPerLevel; | |
} | |
} | |
renderCharacterSheet(); | |
} else { | |
console.warn(`Not enough XP (${char.xp}/${cost}) or stat points to increase ${statName}.`); | |
} | |
} | |
/** | |
* Saves character data to localStorage | |
*/ | |
function saveCharacter() { | |
try { | |
localStorage.setItem('textAdventureCharacter', JSON.stringify(gameState.character)); | |
console.log('πΎ Character saved locally.'); | |
// Optional: Add brief visual confirmation | |
saveCharButton.textContent = 'πΎ Saved!'; | |
setTimeout(() => { saveCharButton.innerHTML = 'πΎ<span class="btn-label">Save</span>'; }, 1500); | |
} catch (e) { | |
console.error('Error saving character to localStorage:', e); | |
alert('Failed to save character. Local storage might be full or disabled.'); | |
} | |
} | |
/** | |
* Loads character data from localStorage | |
*/ | |
function loadCharacter() { | |
try { | |
const savedData = localStorage.getItem('textAdventureCharacter'); | |
if (savedData) { | |
const loadedChar = JSON.parse(savedData); | |
// Basic validation / merging with default structure | |
gameState.character = { | |
...gameState.character, // Start with defaults | |
...loadedChar, // Override with loaded data | |
stats: { // Ensure stats object exists and merge | |
...gameState.character.stats, | |
...(loadedChar.stats || {}) | |
}, | |
inventory: loadedChar.inventory || [] // Ensure inventory array exists | |
}; | |
console.log('πΎ Character loaded from local storage.'); | |
return true; // Indicate success | |
} | |
} catch (e) { | |
console.error('Error loading character from localStorage:', e); | |
// Don't overwrite gameState if loading fails | |
} | |
return false; // Indicate nothing loaded or error | |
} | |
/** | |
* Exports character data as a JSON file download | |
*/ | |
function exportCharacter() { | |
try { | |
const charJson = JSON.stringify(gameState.character, null, 2); // Pretty print JSON | |
const blob = new Blob([charJson], { type: 'application/json' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
// Sanitize name for filename | |
const filename = `${gameState.character.name.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'character'}_save.json`; | |
a.download = filename; | |
document.body.appendChild(a); // Required for Firefox | |
a.click(); | |
document.body.removeChild(a); | |
URL.revokeObjectURL(url); // Clean up | |
console.log(`π€ Character exported as ${filename}`); | |
} catch (e) { | |
console.error('Error exporting character:', e); | |
alert('Failed to export character data.'); | |
} | |
} | |
// --- Event Listeners (Add New) --- | |
charNameInput.addEventListener('change', () => { | |
gameState.character.name = charNameInput.value.trim() || "Hero"; | |
// No need to re-render just for name change unless displaying it elsewhere | |
console.log(`π€ Name changed to: ${gameState.character.name}`); | |
// Maybe save automatically on name change? | |
// saveCharacter(); | |
}); | |
levelUpButton.addEventListener('click', handleLevelUp); | |
statIncreaseButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
const statToIncrease = button.dataset.stat; | |
if (statToIncrease) { | |
handleStatIncrease(statToIncrease); | |
} | |
}); | |
}); | |
saveCharButton.addEventListener('click', saveCharacter); | |
exportCharButton.addEventListener('click', exportCharacter); | |
// --- Modify Existing Functions --- | |
function startGame() { | |
// Try loading character first | |
if (!loadCharacter()) { | |
// If no save found, initialize with defaults (already done by gameState definition) | |
console.log("No saved character found, starting new."); | |
} | |
// Ensure compatibility if loaded save is old/missing fields | |
gameState.character = { | |
...{ // Define ALL default fields here | |
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: [] | |
}, | |
...gameState.character // Loaded data overrides defaults | |
}; | |
// Ensure stats object has all keys after loading potentially partial data | |
gameState.character.stats = { | |
strength: 7, intelligence: 5, wisdom: 5, dexterity: 6, constitution: 6, charisma: 5, hp: 30, maxHp: 30, // Defaults first | |
...(gameState.character.stats || {}) // Loaded stats override defaults | |
} | |
gameState.currentPageId = 1; // Always start at page 1 | |
renderCharacterSheet(); // Initial render of the sheet | |
renderPage(gameState.currentPageId); // Render the story page | |
} | |
function handleChoiceClick(choiceData) { | |
const nextPageId = parseInt(choiceData.nextPage); | |
const itemToAdd = choiceData.addItem; | |
if (isNaN(nextPageId)) { | |
console.error("Invalid nextPageId:", choiceData.nextPage); return; | |
} | |
// Process Choice Effects | |
if (itemToAdd && !gameState.character.inventory.includes(itemToAdd)) { | |
gameState.character.inventory.push(itemToAdd); // Add to character inventory | |
console.log("Added item:", itemToAdd); | |
// Limit inventory size? | |
// if (gameState.character.inventory.length > 15) { /* Handle overflow */ } | |
} | |
// Process Landing Page Effects | |
gameState.currentPageId = nextPageId; | |
const nextPageData = gameData[nextPageId]; | |
if (nextPageData) { | |
// Apply HP loss defined on the *landing* page | |
if (nextPageData.hpLoss) { | |
gameState.character.stats.hp -= nextPageData.hpLoss; // Update character HP | |
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(); // Update sheet before showing game over | |
renderPage(99); // Go to game over page | |
return; | |
} | |
} | |
// --- Apply Rewards (New) --- | |
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}!`); | |
// Update derived stats if needed (e.g., CON -> HP) | |
if (stat === 'constitution') { /* ... update maxHP ... */ } | |
} | |
} | |
// Add other reward types here (e.g., items, stat points) | |
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}`); | |
} | |
} | |
// Check if landing page is game over | |
if (nextPageData.gameOver) { | |
console.log("Reached Game Over page."); | |
renderCharacterSheet(); // Update sheet one last time | |
renderPage(nextPageId); | |
return; | |
} | |
} else { | |
console.error(`Data for page ${nextPageId} not found!`); | |
renderCharacterSheet(); | |
renderPage(99); // Fallback to game over | |
return; | |
} | |
// Render the character sheet (updates XP, stats, inventory) BEFORE rendering page | |
renderCharacterSheet(); | |
// Render the new story page | |
renderPage(nextPageId); | |
} | |
// --- REMOVE/REPLACE Old UI Updates --- | |
// Remove the old updateStatsDisplay() and updateInventoryDisplay() functions | |
// as renderCharacterSheet() now handles this. Make sure no code is still calling them. |