{
+
+ // --- 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