diff --git a/index.html b/index.html index af7047e..f7fd378 100644 --- a/index.html +++ b/index.html @@ -1866,11 +1866,20 @@ - - + +
+ + + +
+
+ + + +
@@ -1890,20 +1899,19 @@ style="font-size: 20px; font-weight: bold; padding: 5px 10px; border: 1px solid currentColor;"> REP: NEUTRAL -

- +
- YOUR OFFER
+ OFFER (Bribe for Reputation)
-
+
@@ -1924,23 +1932,25 @@
- +
- YOUR REQUEST
+ DEMAND (Pay for Truce)
-
-
-
@@ -1964,11 +1973,12 @@ style="margin-top: 0; border-color: var(--pip-color); color: var(--pip-color); font-size: 20px; padding: 10px;">SUBMIT PROPOSAL + style="margin-top: 0; border-color: #ffcc00; color: #ffcc00; font-size: 20px; padding: 10px;">CANCEL + + + + +
+ + +
+ + + +
{ + + // --- NEW: Hook up the Save Game button --- + const saveBtn = document.getElementById('btn-save-game'); + if (saveBtn) { + saveBtn.addEventListener('click', () => { + // Only allow saving if the game has actually started and it is the player's turn! + if (Gamestate.turn > 0 && !Gamestate.aiTurn && !Gamestate.gameOver) { + Gamestate.saveGame(); + } else { + if (Gamestate.showToast) Gamestate.showToast("Cannot save during AI turn or game over.", "red"); + } + }); + } + + // --- NEW: Hook up the Load Game (Holotape) button & Hidden Input --- + const loadBtn = document.getElementById('btn-load-game'); + const fileInput = document.getElementById('file-load-game'); + + if (loadBtn && fileInput) { + // 1. When the visible button is clicked, trigger the hidden file input + loadBtn.addEventListener('click', (e) => { + e.preventDefault(); + fileInput.click(); + }); + + // 2. When a file is selected by the player, read it! + fileInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (!file) return; // User cancelled + + const reader = new FileReader(); + reader.onload = function(e) { + // Extract the text content from the file + const savedDataString = e.target.result; + + // Send it to the game to be rebuilt! + Gamestate.loadGame(savedDataString); + + // Clear the input so you can load the same file again later if needed + fileInput.value = ''; + }; + // Read the file as raw text + reader.readAsText(file); + }); + } + // --- END OF NEW LOGIC --- + const factionInput = document.getElementById('chosen-country-input'); const tooltip = document.getElementById('vats-tooltip'); // WE ARE USING YOUR EXISTING TOOLTIP ELEMENT - if (!factionInput || !tooltip) { console.error("Tooltip or Faction Input not found!"); return; @@ -4138,8 +4203,13 @@ let diplomacyWarning = ""; if (Gamestate.areAllies(Gamestate.player.name, targetCountry.owner)) { - diplomacyWarning = `
WARNING: CEASEFIRE ACTIVE.
ATTACKING INCURS BETRAYAL TAX.
`; + if (Gamestate.isAllianceMode) { + diplomacyWarning = `
PERMANENT ALLY.
FRIENDLY FIRE DISABLED.
`; + } else { + diplomacyWarning = `
WARNING: CEASEFIRE ACTIVE.
ATTACKING INCURS BETRAYAL TAX.
`; + } } + let cmdrWarning = ""; if (Gamestate.commandersEnabled && owner && owner.commander && owner.commander.loc === targetCountry.name) { cmdrWarning = `
COMMANDER GUARDED. +20% DEFENSE BUFF.`; @@ -4164,7 +4234,16 @@ if (targetCountry.isCrater) { infoLines.push(`IRRADIATED CRATER (IMPASSABLE)`); } else { - infoLines.push(`OWNER: ${owner ? (owner.name === "Wasteland Horrors" ? "FERAL GHOULS" : owner.name) : "UNKNOWN"}`); + // --- NEW: Add (ALLY) tag to the Owner line --- + let ownerNameDisplay = owner ? (owner.name === "Wasteland Horrors" ? "FERAL GHOULS" : owner.name) : "UNKNOWN"; + + // If the territory is owned by your ally (and not you), tag it! + if (owner && owner.name !== Gamestate.player.name && Gamestate.areAllies(Gamestate.player.name, owner.name)) { + ownerNameDisplay += ` (ALLY)`; + } + + infoLines.push(`OWNER: ${ownerNameDisplay}`); + // --- END OF NEW LOGIC --- infoLines.push(`GARRISON: ${targetCountry.army}`); if (targetCountry.radDecay && targetCountry.radDecay > 0) { let attrPercent = targetCountry.radDecay === 4 ? 50 : (targetCountry.radDecay === 3 ? 30 : 10); @@ -5230,11 +5309,75 @@ this.diplomacy.reputation[p1.name][p2.name] = 0; }); }); + if (this.perksEnabled) { setInitialReputations(); } + + // --- NEW: ALLIANCE WARFARE TEAM SETUP --- + // Read the dropdown directly from your HTML! + const presetDropdown = document.getElementById('game-mode-preset'); + this.isAllianceMode = (presetDropdown && presetDropdown.value === 'alliance'); + + let activeFactions = this.players.filter(p => !p.isNeutral); + + if (this.isAllianceMode) { + // LORE-FRIENDLY PAIRINGS + const loreTeams = [ + ["Brotherhood of Steel", "Reilly's Rangers"], + ["The Enclave", "BOS Outcasts"], + ["Vault 87 Mutants", "Wasteland Raiders"], + ["New California Republic", "New Vegas Securitrons"], + ["Caesar's Legion", "Great Khans"], + ["Mojave Brotherhood", "The Fiends"], + ["The Minutemen", "The Railroad"], + ["The Institute", "Brotherhood of Steel"], // FO4 High-Tech Juggernauts + ["The Gunners", "Nuka-World Raiders"] + ]; + + let teamCounter = 1; + + // 1. Assign teams based on lore pairs + activeFactions.forEach(p1 => { + if (p1.team) return; // Skip if already assigned a team + + // Find if p1 belongs to a lore pair + let pair = loreTeams.find(t => t.includes(p1.country)); + if (pair) { + let allyName = pair[0] === p1.country ? pair[1] : pair[0]; + let p2 = activeFactions.find(p => p.country === allyName); + + // If both factions are in the game, pair them up! + if (p2 && !p2.team) { + p1.team = teamCounter; + p2.team = teamCounter; + teamCounter++; + } + } + }); + + // 2. Fallback: Pair up any leftovers (e.g. Custom Factions) + let unassigned = activeFactions.filter(p => !p.team); + for (let i = 0; i < unassigned.length; i += 2) { + unassigned[i].team = teamCounter; + if (unassigned[i+1]) unassigned[i+1].team = teamCounter; + teamCounter++; + } + } else { + // Free-For-All: Everyone gets a unique team ID + activeFactions.forEach((p, index) => p.team = index + 10); + } + + // Neutral factions aren't on a team (Safe Assignment) + this.players.forEach(p => { + if (p.isNeutral) p.team = 99; + }); + // --- END ALLIANCE SETUP --- + + // --- Initialize Core Game Variables --- this.aiTurn = false; + this.gameOver = false; this.turn = 1; @@ -5373,17 +5516,51 @@ this.startIntelAnimation(); await this.logAction("> ROBCO INDUSTRIES (TM) Termlink Protocol active.", true); await this.logAction("> Loading Wasteland Conquest Module... COMPLETE.", true); - if (this.wastelandEconomyActive) await this.logAction(">>> SYSTEM: WASTELAND ECONOMY ENGAGED. CAPS ARE KING.", true); - if (document.getElementById('opt-horrors')?.checked) await this.logAction(">>> SYSTEM: NEUTRAL THREAT SCANNER ACTIVE."); - if (this.nukesEnabled) await this.logAction(">>> DEFCON ALERT: 6 Pre-War Command Silos detected. Arms race initiated.", true); - if (this.commandersEnabled) await this.logAction(">>> COMMANDER PROTOCOL: VIPs deployed to the field. Protect them at all costs.", true); - if (this.perksEnabled) await this.logAction(">>> WARNING: Faction-specific combat doctrines are active. Expect asymmetrical warfare.", true); + // --- NEW: LORE-FRIENDLY BOOT SEQUENCES (Safe Scope Version) --- + // FIX: Grab the value directly into a newly named variable to avoid crashes! + const activePresetValue = document.getElementById('game-mode-preset') ? document.getElementById('game-mode-preset').value : 'custom'; + + if (activePresetValue === 'custom') { + // Custom Mode: Show the detailed mechanical breakdown + if (this.wastelandEconomyActive) await this.logAction(">>> SYSTEM: WASTELAND ECONOMY ENGAGED. CAPS ARE KING.", true); + if (document.getElementById('opt-horrors')?.checked) await this.logAction(">>> SYSTEM: NEUTRAL THREAT SCANNER ACTIVE.", true); + if (this.nukesEnabled) await this.logAction(">>> DEFCON ALERT: 6 Pre-War Command Silos detected. Arms race initiated.", true); + if (this.commandersEnabled) await this.logAction(">>> COMMANDER PROTOCOL: VIPs deployed to the field. Protect them at all costs.", true); + if (this.perksEnabled) await this.logAction(">>> WARNING: Faction-specific combat doctrines are active. Expect asymmetrical warfare.", true); + } + else if (activePresetValue === 'classic') { + await this.logAction(">>> INITIATING CLASSIC CONQUEST. Standard rules of engagement apply. No advanced tech detected.", true); + } + else if (activePresetValue === 'survival') { + await this.logAction(">>> INITIATING WASTELAND SURVIVAL. Extreme weather and hostile fauna warnings in effect. Manage your resources carefully.", true); + } + else if (activePresetValue === 'heroes') { + await this.logAction(">>> INITIATING HEROES OF THE WASTELAND. Legendary figures have stepped onto the battlefield. Protect your VIP.", true); + } + else if (activePresetValue === 'apocalypse') { + await this.logAction(">>> INITIATING APOCALYPSE NOW. All safeties disabled. Extreme hazards, commanders, and WMDs authorized. God have mercy.", true); + } + else if (activePresetValue === 'alliance') { + // Safely find out who the player's ally is + let myAlly = this.players.find(p => p.name !== this.player.name && p.team === this.player.team && !p.isNeutral); + let allyText = myAlly ? `${myAlly.name} (${myAlly.country})` : "Unknown Forces"; + await this.logAction(`>>> INITIATING ALLIANCE WARFARE. Joint-operation treaty confirmed. You are permanently allied with ${allyText}. Fight together, or die together.`, true); + } + else if (activePresetValue === 'covert') { + await this.logAction(">>> INITIATING COVERT WARFARE. Satellites offline. Fog of War obscures the battlefield. Trust no one.", true); + } + else if (activePresetValue === 'nuclear') { + await this.logAction(">>> INITIATING NUCLEAR OPTION. Silos are hot. Launch codes have been scattered across the wasteland. Total annihilation authorized.", true); + } + // --- END OF BOOT SEQUENCES --- + await this.logAction("--- DAY 1 BEGINS ---", true); }; + Gamestate.toggleLockdown = async function (countryName) { const clickedCountry = this.countries.find(c => c.name === countryName); if (!clickedCountry || clickedCountry.owner !== this.player.name) return; @@ -5486,204 +5663,307 @@ } - Gamestate.areAllies = function (f1, f2) { return this.diplomacy.truces.some(t => (t.f1 === f1 && t.f2 === f2) || (t.f1 === f2 && t.f2 === f1)); } + Gamestate.areAllies = function (name1, name2) { + // --- NEW: Alliance Warfare Mode --- + // If Alliance mode is active, check their permanent team IDs. + if (this.isAllianceMode) { + let p1 = this.players.find(p => p.name === name1); + let p2 = this.players.find(p => p.name === name2); + if (p1 && p2 && p1.team === p2.team) { + return true; // They are permanently on the same team! + } + return false; // Everyone else is a permanent enemy. + } + + // --- CLASSIC MODE: Temporary Truces --- + // If playing standard Free-For-All, use the normal truce array. + for (let truce of this.diplomacy.truces) { + if ((truce.f1 === name1 && truce.f2 === name2) || (truce.f1 === name2 && truce.f2 === name1)) { + return true; + } + } + return false; + }; Gamestate.getTruce = function (f1, f2) { return this.diplomacy.truces.find(t => (t.f1 === f1 && t.f2 === f2) || (t.f1 === f2 && t.f2 === f1)); } - Gamestate.openDiplomacy = function (targetName) { - let target = this.players.find(p => p.name === targetName); - if (!target || !target.alive || target.isNeutral) return; +Gamestate.openDiplomacy = function (targetName) { + let target = this.players.find(p => p.name === targetName); + if (!target || !target.alive || target.isNeutral) return; + if (this.areAllies(this.player.name, targetName)) { this.showToast(`You already have a Ceasefire with ${targetName}.`); return; } + if (this.diplomacy.grudges[targetName] && this.diplomacy.grudges[targetName].includes(this.player.name)) { this.showToast(`REJECTED: ${targetName} refuses to speak with a backstabber.`, "red"); return; } - if (this.areAllies(this.player.name, targetName)) { this.showToast(`You already have a Ceasefire with ${targetName}.`); return; } - if (this.diplomacy.grudges[targetName] && this.diplomacy.grudges[targetName].includes(this.player.name)) { this.showToast(`REJECTED: ${targetName} refuses to speak with a backstabber.`, "red"); return; } + // 1. Setup the UI text + document.getElementById('dip-target-name').textContent = `Target: ${targetName}`; - // 1. Setup the UI text - document.getElementById('dip-target-name').textContent = `Target: ${targetName}`; + // 2. Fetch and display Reputation & Tax + let repScore = this.diplomacy.reputation[target.name][this.player.name] || 0; + let repLabel = "NEUTRAL"; let repColor = "var(--pip-color)"; + if (repScore >= 35) { repLabel = "IDOLIZED"; repColor = "#00ff00"; } + else if (repScore >= 10) { repLabel = "LIKED"; repColor = "#88ff88"; } + else if (repScore <= -35) { repLabel = "HATED"; repColor = "#ff0000"; } + else if (repScore <= -10) { repLabel = "HOSTILE"; repColor = "#ff8888"; } + let repDisplay = document.getElementById('dip-rep-display'); + repDisplay.textContent = `REP: ${repLabel}`; + repDisplay.style.color = repColor; + let tax = this.diplomacy.betrayalTax[this.player.name] || 0; + let warningEl = document.getElementById('dip-tax-warning'); + warningEl.innerHTML = tax > 0 ? `WASTELAND PARIAH: +${tax} penalty applies to all negotiations.` : ""; - // 2. Fetch and display Reputation - let repScore = this.diplomacy.reputation[target.name][this.player.name] || 0; - let repLabel = "NEUTRAL"; let repColor = "var(--pip-color)"; - if (repScore >= 35) { repLabel = "IDOLIZED"; repColor = "#00ff00"; } - else if (repScore >= 10) { repLabel = "LIKED"; repColor = "#88ff88"; } - else if (repScore <= -35) { repLabel = "HATED"; repColor = "#ff0000"; } - else if (repScore <= -10) { repLabel = "HOSTILE"; repColor = "#ff8888"; } + // --- PRE-CALCULATE BASE TRUCE COST PER TURN --- + let baseCostPerTurn = 0; + let currencyName = this.wastelandEconomyActive ? "Caps" : "Cards"; + let myMaxCurrency = this.wastelandEconomyActive ? this.player.caps : this.player.cards.length; + let theirMaxCurrency = this.wastelandEconomyActive ? target.caps : target.cards.length; - let repDisplay = document.getElementById('dip-rep-display'); - repDisplay.textContent = `REP: ${repLabel}`; - repDisplay.style.color = repColor; + if (this.wastelandEconomyActive) { + baseCostPerTurn = 5 + (tax * 5); // Minimum 5 Caps + if (target.army > this.player.army) baseCostPerTurn = 10 + (tax * 5); + if (target.army > (this.player.army * 2)) baseCostPerTurn = 15 + (tax * 5); + + if (repScore >= 35) baseCostPerTurn = Math.max(0, baseCostPerTurn - 10); + else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 5); + } else { + baseCostPerTurn = 0.33 + (tax * 0.33); // Classic Cards + if (target.army > this.player.army) baseCostPerTurn = 0.66 + (tax * 0.33); + if (target.army > (this.player.army * 2)) baseCostPerTurn = 1.0 + (tax * 0.33); + + if (repScore >= 35) baseCostPerTurn = Math.max(0, baseCostPerTurn - 0.66); + else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 0.33); + } - let tax = this.diplomacy.betrayalTax[this.player.name] || 0; - let warningEl = document.getElementById('dip-tax-warning'); - warningEl.innerHTML = tax > 0 ? `WASTELAND PARIAH: +${tax} Cap penalty applies to negotiations.` : ""; + // 3. Setup the Offer Sliders (Bribes) + const offerCapsSlider = document.getElementById('dip-offer-caps'); + const offerCapsVal = document.getElementById('dip-offer-caps-val'); + // --- FIX: The value span was being replaced. Let's make sure it stays. --- + offerCapsSlider.previousElementSibling.querySelector('#dip-my-caps-max').textContent = myMaxCurrency; + offerCapsSlider.max = myMaxCurrency; + offerCapsSlider.value = 0; + offerCapsVal.textContent = "0"; - // 3. Setup the Offer Sliders - const offerCapsSlider = document.getElementById('dip-offer-caps'); - const offerCapsVal = document.getElementById('dip-offer-caps-val'); - document.getElementById('dip-my-caps-max').textContent = this.player.cards.length; - offerCapsSlider.max = this.player.cards.length; - offerCapsSlider.value = 0; offerCapsVal.textContent = "0"; + const offerTroopsSlider = document.getElementById('dip-offer-troops'); + const offerTroopsVal = document.getElementById('dip-offer-troops-val'); + // --- FIX: Same fix as above for the troops slider. --- + offerTroopsSlider.previousElementSibling.querySelector('#dip-my-troops-max').textContent = this.player.reserve; + offerTroopsSlider.max = this.player.reserve; + offerTroopsSlider.value = 0; + offerTroopsVal.textContent = "0"; - const offerTroopsSlider = document.getElementById('dip-offer-troops'); - const offerTroopsVal = document.getElementById('dip-offer-troops-val'); - document.getElementById('dip-my-troops-max').textContent = this.player.reserve; - offerTroopsSlider.max = this.player.reserve; - offerTroopsSlider.value = 0; offerTroopsVal.textContent = "0"; + // 4. Setup the Request Sliders (Demands) + const reqTruceSlider = document.getElementById('dip-req-truce'); + const reqTruceVal = document.getElementById('dip-req-truce-val'); + const truceCostDisplay = document.getElementById('dip-truce-cost'); - // 4. Setup the Request Sliders - const reqTruce = document.getElementById('dip-req-truce'); - reqTruce.checked = false; // Default to unchecked + // --- DYNAMIC TRUCE SLIDER LOGIC --- + let maxAffordableTurns = 0; + if (baseCostPerTurn === 0) { + maxAffordableTurns = 10; // Free truce! + } else { + maxAffordableTurns = Math.floor(myMaxCurrency / baseCostPerTurn); + } - const reqCapsSlider = document.getElementById('dip-req-caps'); - const reqCapsVal = document.getElementById('dip-req-caps-val'); - document.getElementById('dip-their-caps-max').textContent = target.cards.length; - reqCapsSlider.max = target.cards.length; - reqCapsSlider.value = 0; reqCapsVal.textContent = "0"; + if (maxAffordableTurns < 1) { + // Lock the slider + reqTruceSlider.max = 0; + reqTruceSlider.value = 0; + reqTruceSlider.disabled = true; + // --- FIX: Changed text to match your requirement. --- + truceCostDisplay.textContent = `Requires at least ${Math.ceil(baseCostPerTurn)} ${currencyName} for a 1-turn Truce.`; + truceCostDisplay.style.color = "#ff3333"; + } else { + // Unlock the slider + reqTruceSlider.max = maxAffordableTurns; + reqTruceSlider.value = 0; + reqTruceSlider.disabled = false; + truceCostDisplay.textContent = `Cost: 0 ${currencyName}`; + truceCostDisplay.style.color = "#ffcc00"; + } + reqTruceVal.textContent = "0"; - // 5. Update values on drag - offerCapsSlider.oninput = function () { offerCapsVal.textContent = this.value; validateProposal(); }; - offerTroopsSlider.oninput = function () { offerTroopsVal.textContent = this.value; validateProposal(); }; - reqCapsSlider.oninput = function () { reqCapsVal.textContent = this.value; validateProposal(); }; - reqTruce.onchange = function () { validateProposal(); }; + const reqCapsSlider = document.getElementById('dip-req-caps'); + const reqCapsVal = document.getElementById('dip-req-caps-val'); + // --- FIX: The value span was being replaced here too. --- + reqCapsSlider.previousElementSibling.querySelector('#dip-their-caps-max').textContent = theirMaxCurrency; + reqCapsSlider.max = theirMaxCurrency; + reqCapsSlider.value = 0; + reqCapsVal.textContent = "0"; - // 6. The Validation Logic (Checks if the AI will accept) - let validateProposal = () => { - let capsOffered = parseInt(offerCapsSlider.value); - let troopsOffered = parseInt(offerTroopsSlider.value); - let capsRequested = parseInt(reqCapsSlider.value); - let truceRequested = reqTruce.checked; + // 5. Update values on drag + offerCapsSlider.oninput = function () { offerCapsVal.textContent = this.value; validateProposal(); }; + offerTroopsSlider.oninput = function () { offerTroopsVal.textContent = this.value; validateProposal(); }; + reqCapsSlider.oninput = function () { reqCapsVal.textContent = this.value; validateProposal(); }; + + // --- FIX: Wrapped the original oninput logic in a new function to keep it clean. --- + const handleTruceSliderInput = function() { + reqTruceVal.textContent = this.value; + + let truceTurnsRequested = parseInt(this.value); + let totalTruceCost = Math.ceil(baseCostPerTurn * truceTurnsRequested); - let analysisEl = document.getElementById('dip-analysis'); - let sendBtn = document.getElementById('dip-send'); + if (maxAffordableTurns >= 1) { + truceCostDisplay.textContent = `Cost: ${totalTruceCost} ${currencyName}`; + } + + validateProposal(); + }; + reqTruceSlider.oninput = handleTruceSliderInput; - // AI hates you = Instant refusal of any request - if (repScore <= -35 && (capsRequested > 0 || truceRequested)) { - analysisEl.innerHTML = `Refusal guaranteed. Target is hostile.`; - sendBtn.disabled = true; - return; - } - // Calculate Trade Value - let offerValue = capsOffered + (troopsOffered * 0.5); // Caps are worth more than troops - let requestValue = capsRequested; - if (truceRequested) { - let baseTruceCost = 1 + tax; - if (target.army > this.player.army) baseTruceCost = 2 + tax; - if (target.army > (this.player.army * 2)) baseTruceCost = 3 + tax; + // 6. The Validation Logic + let validateProposal = () => { + let capsOffered = parseInt(offerCapsSlider.value); + let troopsOffered = parseInt(offerTroopsSlider.value); + let capsRequested = parseInt(reqCapsSlider.value); + let truceTurnsRequested = parseInt(reqTruceSlider.value); + + let analysisEl = document.getElementById('dip-analysis'); + let sendBtn = document.getElementById('dip-send'); + + if (repScore <= -35 && (capsRequested > 0 || truceTurnsRequested > 0)) { + analysisEl.innerHTML = `Refusal guaranteed. Target is hostile.`; + sendBtn.disabled = true; + return; + } - // Reputation Discounts! - if (repScore >= 35) baseTruceCost = Math.max(0, baseTruceCost - 2); - else if (repScore >= 10) baseTruceCost = Math.max(0, baseTruceCost - 1); + let offerValue = 0; + let requestValue = capsRequested; - requestValue += baseTruceCost; - } + if (this.wastelandEconomyActive) { + offerValue = capsOffered + (troopsOffered * 5); // 1 Troop = 5 Caps + } else { + offerValue = capsOffered + (troopsOffered * 0.5); + } + + let totalTruceCost = Math.ceil(baseCostPerTurn * truceTurnsRequested); + requestValue += totalTruceCost; + + // Is it a gift? + if (offerValue > 0 && requestValue === 0) { + analysisEl.innerHTML = `Generous Gift. Significant Reputation increase expected.`; + sendBtn.disabled = false; + return; + } + + // Is the trade fair? + if (offerValue >= requestValue && requestValue > 0) { + analysisEl.innerHTML = `Terms acceptable. Target likely to agree.`; + sendBtn.disabled = false; + } else if (requestValue > 0) { + analysisEl.innerHTML = `Terms insufficient. Offer more ${currencyName} or Troops.`; + sendBtn.disabled = true; + } else { + analysisEl.innerHTML = `Awaiting terms...`; + sendBtn.disabled = true; + } + }; - // Is it a gift? - if (offerValue > 0 && requestValue === 0) { - analysisEl.innerHTML = `Generous Gift. Significant Reputation increase expected.`; - sendBtn.disabled = false; - return; - } + validateProposal(); - // Is the trade fair? - if (offerValue >= requestValue && requestValue > 0) { - analysisEl.innerHTML = `Terms acceptable. Target likely to agree.`; - sendBtn.disabled = false; - } else if (requestValue > 0) { - analysisEl.innerHTML = `Terms insufficient. Target demands more value.`; - sendBtn.disabled = true; - } else { - analysisEl.innerHTML = `Awaiting terms...`; - sendBtn.disabled = true; // Nothing offered or requested - } - }; - - validateProposal(); // Run once to set initial state - - let modal = document.getElementById('diplomacy-modal'); - modal.style.display = 'flex'; - - document.getElementById('dip-send').onclick = () => { - // Build the proposal object - let proposal = { - capsOffered: parseInt(offerCapsSlider.value), - troopsOffered: parseInt(offerTroopsSlider.value), - capsRequested: parseInt(reqCapsSlider.value), - truceRequested: reqTruce.checked - }; - this.sendEnvoy(target, proposal, tax); - modal.style.display = 'none'; - }; - document.getElementById('dip-cancel').onclick = () => { modal.style.display = 'none'; }; - }; + let modal = document.getElementById('diplomacy-modal'); + modal.style.display = 'flex'; + document.getElementById('dip-send').onclick = () => { + let proposal = { + capsOffered: parseInt(offerCapsSlider.value), + troopsOffered: parseInt(offerTroopsSlider.value), + capsRequested: parseInt(reqCapsSlider.value), + truceRequested: parseInt(reqTruceSlider.value) + }; + this.sendEnvoy(target, proposal, tax); + modal.style.display = 'none'; + }; + document.getElementById('dip-cancel').onclick = () => { modal.style.display = 'none'; }; +}; Gamestate.sendEnvoy = function (target, proposal, tax) { let repScore = this.diplomacy.reputation[target.name][this.player.name] || 0; - // Calculate Trade Value - let offerValue = proposal.capsOffered + (proposal.troopsOffered * 0.5); + // Calculate Trade Value based on Economy Type + let offerValue = 0; let requestValue = proposal.capsRequested; + let baseCostPerTurn = 0; - let baseTruceCost = 1 + tax; - if (target.army > this.player.army) baseTruceCost = 2 + tax; - if (target.army > (this.player.army * 2)) baseTruceCost = 3 + tax; - if (repScore >= 75) baseTruceCost = Math.max(0, baseTruceCost - 2); - else if (repScore >= 25) baseTruceCost = Math.max(0, baseTruceCost - 1); + if (this.wastelandEconomyActive) { + // CAPS ECONOMY + offerValue = proposal.capsOffered + (proposal.troopsOffered * 5); + + baseCostPerTurn = 2 + (tax * 2); + if (target.army > this.player.army) baseCostPerTurn = 4 + (tax * 2); + if (target.army > (this.player.army * 2)) baseCostPerTurn = 6 + (tax * 2); + + if (repScore >= 35) baseCostPerTurn = Math.max(0, baseCostPerTurn - 4); + else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 2); + } else { + // CLASSIC CARD ECONOMY + offerValue = proposal.capsOffered + (proposal.troopsOffered * 0.5); - if (proposal.truceRequested) { - requestValue += baseTruceCost; + baseCostPerTurn = 0.33 + (tax * 0.33); + if (target.army > this.player.army) baseCostPerTurn = 0.66 + (tax * 0.33); + if (target.army > (this.player.army * 2)) baseCostPerTurn = 1.0 + (tax * 0.33); + + if (repScore >= 35) baseCostPerTurn = Math.max(0, baseCostPerTurn - 0.66); + else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 0.33); + } + + // FIX: Check if truceRequested is greater than 0 (since it's a slider now) + if (proposal.truceRequested > 0) { + let totalTruceCost = Math.ceil(baseCostPerTurn * proposal.truceRequested); + requestValue += totalTruceCost; } // AI Acceptance Logic - // If the offer is greater than or equal to the request, OR it's a pure gift, they accept. if (offerValue >= requestValue || (offerValue > 0 && requestValue === 0)) { - // --- EXECUTE THE TRADE --- - // 1. Transfer Caps (Player to AI) - for (let i = 0; i < proposal.capsOffered; i++) { - if (this.player.cards.length > 0) target.cards.push(this.player.cards.pop()); - } - // 2. Transfer Caps (AI to Player) - for (let i = 0; i < proposal.capsRequested; i++) { - if (target.cards.length > 0) this.player.cards.push(target.cards.pop()); + // 1 & 2. Transfer Currency + if (this.wastelandEconomyActive) { + // Transfer Caps + this.player.caps -= proposal.capsOffered; + target.caps += proposal.capsOffered; + target.caps -= proposal.capsRequested; + this.player.caps += proposal.capsRequested; + } else { + // Transfer Cards + for (let i = 0; i < proposal.capsOffered; i++) { + if (this.player.cards.length > 0) target.cards.push(this.player.cards.pop()); + } + for (let i = 0; i < proposal.capsRequested; i++) { + if (target.cards.length > 0) this.player.cards.push(target.cards.pop()); + } } + // 3. Transfer Troops (Player to AI Reserve) if (proposal.troopsOffered > 0) { this.player.reserve = Math.max(0, this.player.reserve - proposal.troopsOffered); target.reserve += proposal.troopsOffered; - // Instantly deploy them to the AI's army so they can use them target.army += proposal.troopsOffered; - // --- NEW: PREVENT DEPLOYMENT SOFTLOCK --- - // If the player traded away their last reserve troop during the Fortify phase, - // instantly advance them to the Battle phase. + // PREVENT DEPLOYMENT SOFTLOCK if (this.stage === "Fortify" && this.player.reserve === 0) { this.stage = "Battle"; - let endBtn = document.getElementById('end'); if (endBtn) { endBtn.style.opacity = "1"; endBtn.style.pointerEvents = "auto"; } - if (turnInfo) turnInfo.textContent = "Combat Phase"; if (turnInfoMessage) turnInfoMessage.textContent = "Select staging territory, then target an enemy."; this.updateButtonText(); } - // ---------------------------------------- } // 4. Enact Truce - if (proposal.truceRequested) { - this.diplomacy.truces.push({ f1: this.player.name, f2: target.name, turns: 3 }); + // FIX: Enacts the truce for the exact number of turns selected on the slider + if (proposal.truceRequested > 0) { + this.diplomacy.truces.push({ f1: this.player.name, f2: target.name, turns: proposal.truceRequested }); } // --- CALCULATE REPUTATION CHANGES --- let repChange = 0; let actionText = ""; + let currencyName = this.wastelandEconomyActive ? "Caps" : "Cards"; // Was it a pure gift? if (requestValue === 0 && offerValue > 0) { - repChange = Math.floor(offerValue * 3); // Gifts are highly valued (+3 to +15 approx) - actionText = `[ DIPLOMACY ] You sent a generous gift of ${proposal.capsOffered} Caps and ${proposal.troopsOffered} Troops to ${target.name}.`; + repChange = Math.floor(offerValue * 3); + actionText = `[ DIPLOMACY ] You sent a generous gift of ${proposal.capsOffered} ${currencyName} and ${proposal.troopsOffered} Troops to ${target.name}.`; } // Was it an exceptionally generous trade? else if (offerValue > requestValue * 2) { @@ -5692,7 +5972,7 @@ } // Was it a standard, fair trade? else { - repChange = 1; // Small bump just for doing business + repChange = 1; actionText = `[ DIPLOMACY ] Trade agreement reached with ${target.name}.`; } @@ -5712,7 +5992,7 @@ } this.updateInfo(); - } + }; Gamestate.showEnvoyModal = function (factionName, caps, turns, color, isRequestingTroops = false, troopsRequested = 0) { @@ -6231,16 +6511,22 @@ } let dipBtn = infoBox.querySelector('.btn-diplomacy'); - if (player.name !== this.player.name && !player.isNeutral && player.areas.length > 0) { + // --- NEW: Do not show Diplomacy buttons if Alliance Mode is active --- + if (this.isAllianceMode || (player.name === this.player.name) || player.isNeutral || player.areas.length === 0) { + if (dipBtn) dipBtn.style.display = "none"; + } else { if (!dipBtn) { dipBtn = document.createElement('button'); dipBtn.className = 'btn-diplomacy'; infoBox.appendChild(dipBtn); } dipBtn.style.display = "block"; + if (this.areAllies(this.player.name, player.name)) { let t = this.getTruce(this.player.name, player.name); - dipBtn.textContent = `ALLIED: ${t.turns} TURNS`; + // Fallback in case getTruce returns undefined (which it shouldn't, but safety first) + let turnsLeft = t ? t.turns : "?"; + dipBtn.textContent = `ALLIED: ${turnsLeft} TURNS`; dipBtn.disabled = true; dipBtn.style.background = "#0088ff"; } else { @@ -6250,10 +6536,9 @@ dipBtn.style.color = "#000"; dipBtn.onclick = () => this.openDiplomacy(player.name); } - } else { - if (dipBtn) dipBtn.style.display = "none"; } + } else { // --- Dead Player Logic --- infoBox.classList.add('defeated'); @@ -6288,27 +6573,25 @@ if (viewCardsBtn) { if (this.wastelandEconomyActive) { viewCardsBtn.textContent = "RECRUITMENT"; - const troopCost = 5; // Assumes troop cost is 5, must match modal + const troopCost = 5; const canAfford = this.player.caps >= troopCost; - // --- THIS IS THE FIX --- - // Button is only enabled if it's the recruitment stage AND player can afford it. - const isEnabled = (this.stage === 'Recruitment' && canAfford && !this.aiTurn); + // --- FIX: Allow recruitment during BOTH Recruitment and Fortify phases! --- + const isEnabled = ((this.stage === 'Recruitment' || this.stage === 'Fortify') && canAfford && !this.aiTurn); viewCardsBtn.disabled = !isEnabled; viewCardsBtn.style.opacity = isEnabled ? "1" : "0.5"; viewCardsBtn.style.pointerEvents = isEnabled ? "auto" : "none"; - // --- END OF FIX --- viewCardsBtn.classList.remove('ready-to-trade'); viewCardsBtn.onclick = () => { - if (this.stage === 'Recruitment' && !this.aiTurn) { + if ((this.stage === 'Recruitment' || this.stage === 'Fortify') && !this.aiTurn) { this.showRecruitmentModal(); } }; - - } else { + } + else { // Original logic for classic mode (remains untouched) if (this.getBestTrade(this.player.cards)) { viewCardsBtn.textContent = "OPEN STASH"; @@ -6370,20 +6653,32 @@ const fogEnabled = document.getElementById('opt-fog-of-war') && document.getElementById('opt-fog-of-war').checked; const pBobbleActive = this.bobbleheads && this.bobbleheads.find(b => b.key === 'p' && b.active); const visibleTerritories = new Set(); + if (fogEnabled && !pBobbleActive && this.player.alive) { this.player.areas.forEach(a => visibleTerritories.add(a)); this.player.areas.forEach(areaName => { const c = this.countries.find(x => x.name === areaName); if (c) c.neighbours.forEach(n => visibleTerritories.add(n)); }); - const idolizedAllies = this.players.filter(p => p.alive && !p.isNeutral && p.name !== this.player.name && this.diplomacy.reputation[p.name] && this.diplomacy.reputation[p.name][this.player.name] >= 35); - idolizedAllies.forEach(ally => { + + // --- NEW: Add vision for permanent Alliance Mode teammates --- + let alliesWithVision = []; + if (this.isAllianceMode) { + alliesWithVision = this.players.filter(p => p.alive && !p.isNeutral && p.name !== this.player.name && p.team === this.player.team); + } else { + // Classic Mode: Only share vision if Idolized + alliesWithVision = this.players.filter(p => p.alive && !p.isNeutral && p.name !== this.player.name && this.diplomacy.reputation[p.name] && this.diplomacy.reputation[p.name][this.player.name] >= 35); + } + + // Loop through all vision-sharing allies and add their borders to your map + alliesWithVision.forEach(ally => { ally.areas.forEach(a => visibleTerritories.add(a)); ally.areas.forEach(areaName => { const c = this.countries.find(x => x.name === areaName); if (c) c.neighbours.forEach(n => visibleTerritories.add(n)); }); }); + // --- END OF VISION LOGIC --- } this.countries.forEach(country => { let areaOnMap = document.getElementById(country.name); @@ -6482,13 +6777,31 @@ // --- PERK 2: The Gunners --- else if (this.player.perk.id === 'mercenary_contracts') { showButton = true; - perkButton.textContent = "Mercenary Contract (20 Caps)"; - isButtonDisabled = this.player.caps < 20; - tooltipText = isButtonDisabled ? "Insufficient Caps. Requires 20 Caps." : "Spend 20 Caps to instantly add 5 elite troops to your reserves."; + + // Calculate current scaling reward for the tooltip + const currentReward = 3 + Math.floor(this.player.areas.length / 2); + + if (this.player.mercenaryCooldown > 0) { + isButtonDisabled = true; + perkButton.innerHTML = "Contract on Cooldown
(" + this.player.mercenaryCooldown + " Turns Left)"; + tooltipText = "Mercenary contacts require " + this.player.mercenaryCooldown + " more turn(s) to refresh."; + } else if (this.player.caps < 20) { + isButtonDisabled = true; + perkButton.innerHTML = "Mercenary Contract
(20 Caps)"; + tooltipText = "Insufficient Caps. Requires 20 Caps."; + } else { + isButtonDisabled = false; + perkButton.innerHTML = "Mercenary Contract
(20 Caps)"; + // --- FIX: Updated tooltip to say 3 Turn Cooldown + tooltipText = "Spend 20 Caps to instantly add " + currentReward + " elite troops to your reserves. (3 Turn Cooldown)"; + } + perkButton.onclick = () => { if (!isButtonDisabled) this.useMercenaryContract(); }; } + + // --- PERK 3: BOS Outcasts --- else if (this.player.perk.id === 'tech_hoarders') { @@ -6886,12 +7199,18 @@ } Gamestate.handleEndTurn = async function () { - if (this.aiTurn || this.player.reserve > 0) { + if (this.aiTurn) { // --- FIX: Removed the global reserve lock! return; } // --- FORTIFY -> BATTLE / RECRUITMENT BRIDGE (Corrected Logic) --- if (this.stage === "Fortify" || this.stage === "Recruitment") { + // Apply the reserve lock ONLY when leaving the Fortify/Recruitment phase + if (this.player.reserve > 0) { + if (this.showToast) this.showToast("You must deploy all your reserve troops before ending the phase.", "red"); + return; + } + // This now handles transitions from both Fortify and our new Recruitment stage this.stage = "Battle"; // The next stage is always Battle let endBtn = document.getElementById('end'); @@ -7372,6 +7691,15 @@ // WHEN YOU CLICK THE SECOND TERRITORY (The Target) if (this.prevCountry.name !== country.name && country.owner !== this.player.name && this.prevCountry.owner === this.player.name) { + // --- FIX: Prevent attacking Permanent Allies (Alliance Mode) --- + if (this.isAllianceMode && this.areAllies(this.player.name, country.owner)) { + if (this.showToast) this.showToast(`Cannot attack ${formatTerritoryName(country.name)}: Friendly Fire disabled.`, "red"); + this.prevCountry = null; + if (this.prevTarget) this.prevTarget.classList.remove('flash'); + this.prevTarget = null; + return; + } + // --- NEW: Prevent attacking INTO a locked territory --- if (country.isLockedDown) { if (this.showToast) this.showToast(`Cannot attack: ${formatTerritoryName(country.name)} is under Elder's Edict lockdown.`, "red"); @@ -7455,6 +7783,13 @@ if (!cmdrLoc) return; if (cmdrLoc.neighbours.includes(country.name)) { let enemyCmdr = this.players.find(p => p !== this.player && p.alive && !p.isNeutral && p.commander && p.commander.loc === country.name); + + // --- NEW: Prevent dueling an allied Commander --- + if (enemyCmdr && this.areAllies(this.player.name, enemyCmdr.name)) { + if (this.showToast) this.showToast("Friendly Fire: Cannot duel an allied Commander!", "red"); + return; + } + if (enemyCmdr) { if (this.player.commander.hasFought) { if (this.showToast) this.showToast("Commander is exhausted and cannot duel again this turn!", "red"); @@ -7932,7 +8267,8 @@ for (let p of this.players) { if (this.commandersEnabled && p.alive && p.commander && p.commander.hp > 0) { let locCountry = this.countries.find(c => c.name === p.commander.loc); - if (locCountry && locCountry.owner !== p.name && !locCountry.isCrater) { + // --- FIX: Treat allied territory as a safe haven, not a siege! --- + if (locCountry && locCountry.owner !== p.name && !locCountry.isCrater && !this.areAllies(p.name, locCountry.owner)) { if (!p.commander.wasAttacked) { p.commander.siegeTurns = (p.commander.siegeTurns || 0) + 1; if (p.commander.siegeTurns >= 10) { @@ -7971,7 +8307,7 @@ if (this.player.techOverdriveActive === 0) { // When the buff ends, start the cooldown this.player.techOverdriveCooldown = 3; - this.logAction("⚙️ Technology Overdrive has worn off. Systems require 3 turns to recharge."); + this.logAction("Technology Overdrive has worn off. Systems require 3 turns to recharge."); } } else if (this.player.techOverdriveCooldown > 0) { this.player.techOverdriveCooldown--; @@ -7979,13 +8315,21 @@ } // --- END OF ADDITION --- + // --- NEW: The Gunners Mercenary Cooldown --- + if (this.player.perk.id === 'mercenary_contracts' && this.player.mercenaryCooldown > 0) { + this.player.mercenaryCooldown--; + if (this.player.mercenaryCooldown === 0) { + this.logAction("Mercenary contacts have refreshed and are ready for hire.", true); + } + } + // --- END OF ADDITION --- // --- NEW: Mr. House Predictive Cooldown --- if (this.player.perk.id === 'the_house_always_wins' && this.player.predictiveCooldown > 0) { this.player.predictiveCooldown--; if (this.player.predictiveCooldown === 0) { - this.logAction("💻 Predictive Simulation systems are fully recharged and ready.", true); + this.logAction("Predictive Simulation systems are fully recharged and ready.", true); } } // --- END OF ADDITION --- @@ -8086,10 +8430,11 @@ // Calculate Nuka-World Caps if (this.perksEnabled && this.player.perk && this.player.perk.id === 'tribute_chest') { - nukaBonus = continentsOwned; + nukaBonus = continentsOwned * 5; // --- BUFFED: +5 Caps per continent! totalIncome += nukaBonus; } + // Apply Caps this.player.caps += totalIncome; @@ -8126,9 +8471,8 @@ this.updateButtonText(); - - - if (this.perksEnabled && this.player.perk && this.player.perk.id === 'tribute_chest') { + // --- FIX: Only give Tribute CARDS if the Wasteland Economy (Caps) is turned OFF --- + if (!this.wastelandEconomyActive && this.perksEnabled && this.player.perk && this.player.perk.id === 'tribute_chest') { let continentsOwned = 0; continents.forEach(continent => { if (continent.areas.every(area => this.player.areas.includes(area) && !this.countries.find(c => c.name === area).isCrater)) { @@ -8142,9 +8486,10 @@ type: "Wild" }); } - await this.logAction(`Nuka-Raiders demand tribute! Gained +${continentsOwned} Bottle Cap(s) from controlled continents.`); + await this.logAction(`Nuka-Raiders demand tribute! Gained +${continentsOwned} Bottle Cap card(s) from controlled continents.`); } } + if (map) map.style.pointerEvents = "auto"; if (infoName[i - 1]) infoName[i - 1].parentElement.classList.remove('highlight'); if (infoName[0]) infoName[0].parentElement.classList.add('highlight'); @@ -8363,7 +8708,12 @@ return true; } - // --- Backstab Logic --- + // --- NEW: NO BETRAYALS IN ALLIANCE WARFARE --- + if (this.isAllianceMode) { + return false; // Permanently safe! + } + + // --- Backstab Logic (Classic Mode Only) --- // If they ARE allies, only certain factions have a chance to betray. let isChaotic = ["Raiders", "Super Mutants", "Great Khans", "The Fiends"].includes(this.players[i].country); let backstabChance = 0.02; // A very low base chance @@ -8382,6 +8732,7 @@ }); + // --- AI DESPERATION CHECK --- // GLOBAL THREAT: If ANY nuke is in the air, the entire wasteland unites against the launcher! // ... code that filters possibleTargets ... @@ -8477,10 +8828,12 @@ // Pay the cost this.player.caps -= 20; - // --- NEW: Flat reward of 5 elite troops --- - const reward = 5; + // 3 base troops + 1 for every 2 territories owned + const reward = 3 + Math.floor(this.player.areas.length / 2); + + // --- FIX: Increased to 4 (3 Turn Cooldown + Current Turn) --- + this.player.mercenaryCooldown = 4; - // --- UNIFIED DEPLOYMENT LOGIC --- // Troops are always added to the general reserve pool, forcing a deployment phase. this.player.reserve += reward; this.player.army += reward; @@ -8494,9 +8847,6 @@ - - - Gamestate.aiManeuver = function (i) { let player = this.players[i]; let owned = this.countries.filter(c => c.owner === player.name); @@ -8769,7 +9119,7 @@ if (bonus > 0) { attackerWinChance -= bonus; // Decrease attacker's chance by the defensive bonus - this.logAction(`🛡️ Ranger Network provided a +${Math.round(bonus * 100)}% defensive bonus.`); + this.logAction(`Ranger Network provided a +${Math.round(bonus * 100)}% defensive bonus.`); } } @@ -9120,11 +9470,33 @@ return a === opponent.name; }); if (matchedCountry) { - if (this.queueToast) this.queueToast(`>>> STRATEGIC ASSET SECURED <<<

${player.name.toUpperCase()} NOW CONTROLS ${continent.name.toUpperCase()}
(+${continent.bonus} TROOPS)`, player.color, false); + // --- FIX: Dynamically build the popup text based on Perks! --- + let bonusText = `+${continent.bonus} TROOPS`; + + if (this.perksEnabled && player.perk) { + if (player.perk.id === 'logistical_superiority') { + // NCR gets +50% troops + let extraTroops = Math.ceil(continent.bonus * 0.5); + bonusText = `+${continent.bonus + extraTroops} TROOPS`; + } else if (player.perk.id === 'tribute_chest') { + // Nuka-World gets +5 Caps (or +1 Card if classic mode) + if (this.wastelandEconomyActive) { + bonusText += ` & +5 CAPS`; + } else { + bonusText += ` & +1 CARD`; + } + } + } + + if (this.queueToast) { + this.queueToast(`>>> STRATEGIC ASSET SECURED <<<

${player.name.toUpperCase()} NOW CONTROLS ${continent.name.toUpperCase()}
(${bonusText})`, player.color, false); + } + // --- END OF FIX --- } } }) } + this.checkWinCondition(); } @@ -9481,6 +9853,166 @@ } Gamestate.typewriterInterval = null; + // ========================================== + // --- SAVE GAME SYSTEM --- + // ========================================== + Gamestate.saveGame = async function () { + try { + // 1. Gather all critical game variables into one giant object + const saveData = { + turn: this.turn, + stage: this.stage, + difficulty: this.difficulty, + + // Game Modes & Rules + wastelandEconomyActive: this.wastelandEconomyActive, + perksEnabled: this.perksEnabled, + nukesEnabled: this.nukesEnabled, + commandersEnabled: this.commandersEnabled, + + // Global Trackers + globalCodes: this.globalCodes, + activeNuke: this.activeNuke, + radstorm: this.radstorm, + diplomacy: this.diplomacy, + + // The Main Entities + players: this.players, + countries: this.countries, + bobbleheads: this.bobbleheads + }; + + // 2. Convert that object into a JSON string + const jsonString = JSON.stringify(saveData); + + // 3. --- NEW: Create a downloadable file (Holotape) --- + const blob = new Blob([jsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `WastelandConquest_Day${this.turn}_Save.json`; // Names the file automatically! + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + // --- END OF DOWNLOAD LOGIC --- + + // 4. Provide epic visual feedback + + if (this.queueToast) { + this.queueToast(`>>> SYSTEM BACKUP COMPLETE <<<

Game progress saved to local memory.`, "var(--pip-color)", true); + } else if (this.showToast) { + this.showToast("Game Saved Successfully.", "green"); + } + + // 5. Flash the button to confirm the click registered + let saveBtn = document.getElementById('btn-save-game'); + if (saveBtn) { + let oldColor = saveBtn.style.color; + saveBtn.style.backgroundColor = "var(--pip-color)"; + saveBtn.style.color = "var(--pip-dark)"; + setTimeout(() => { + saveBtn.style.backgroundColor = ""; // Reset to CSS default + saveBtn.style.color = oldColor; + }, 500); + } + + // Log it for good measure + if (this.logAction) this.logAction("💾 SYSTEM: Game state successfully backed up to local memory.", true); + + } catch (error) { + console.error("Failed to save game:", error); + if (this.showToast) this.showToast("CRITICAL ERROR: Failed to write to local memory.", "red"); + } + }; + + // ========================================== + // --- LOAD GAME SYSTEM --- + // ========================================== + // --- FIX: Tell the function to accept the 'jsonString' we pass from the file button --- + Gamestate.loadGame = async function (jsonString) { + try { + // 1. Check if we actually received file data + if (!jsonString) { + if (this.showToast) this.showToast("Error: No save data found in file.", "red"); + return false; + } + + // 2. Parse the string back into a JavaScript object + const loadedData = JSON.parse(jsonString); + + // 3. Overwrite all current game variables with the loaded data + this.turn = loadedData.turn; + this.stage = loadedData.stage; + this.difficulty = loadedData.difficulty; + + this.wastelandEconomyActive = loadedData.wastelandEconomyActive; + this.perksEnabled = loadedData.perksEnabled; + this.nukesEnabled = loadedData.nukesEnabled; + this.commandersEnabled = loadedData.commandersEnabled; + + this.globalCodes = loadedData.globalCodes; + this.activeNuke = loadedData.activeNuke; + this.radstorm = loadedData.radstorm; + this.diplomacy = loadedData.diplomacy; + + this.players = loadedData.players; + this.countries = loadedData.countries; + this.bobbleheads = loadedData.bobbleheads; + + // 4. Determine who "The Player" is (the human) and point 'this.player' back to them + this.player = this.players.find(p => p.isPlayer); + + // 5. Hide the start modal and show the game UI + let startModal = document.getElementById('start-modal'); + if (startModal) startModal.style.display = 'none'; + + // 6. Completely redraw the map to match the loaded ownership and armies + this.countries.forEach(country => { + let areaOnMap = document.getElementById(country.name); + if (areaOnMap) { + areaOnMap.style.fill = country.color; + if (country.isCrater) { + areaOnMap.classList.add('crater'); + } else { + areaOnMap.classList.remove('crater'); + } + } + }); + + // 7. Redraw the text numbers on the map + this.drawMapText(); + + // 8. Re-initialize the UI to match the loaded stage + this.updateButtonText(); + this.updateInfo(); + + // 9. Special rule for loading into the Recruitment stage + if (this.wastelandEconomyActive && this.stage === 'Recruitment' && this.player.reserve === 0) { + const troopCost = 5; + if (this.player.caps >= troopCost) { + this.showRecruitmentModal(); + } + } + + // 10. Announce success! + if (this.logAction) this.logAction(`💾 SYSTEM: Successfully restored Save File (Day ${this.turn}).`, true); + if (this.queueToast) { + this.queueToast(`>>> SYSTEM RESTORE COMPLETE <<<

Welcome back, ${this.player.name}.`, "var(--pip-color)", true); + } + + return true; + + } catch (error) { + console.error("Failed to load game:", error); + if (this.showToast) this.showToast("CRITICAL ERROR: Save file corrupted or incompatible.", "red"); + return false; + } + }; + + + Gamestate.updateInfo = function () { Gamestate.originalUpdateInfo.call(this); // Run the normal game logic first