awacke1's picture
Update game.js
e34e138 verified
raw
history blame
17.6 kB
// --- 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.