diff --git a/index.html b/index.html
index b9a0361..92380db 100644
--- a/index.html
+++ b/index.html
@@ -7914,8 +7914,16 @@ Gamestate.openDiplomacy = function (targetName) {
// STAGE 1: SELECTING THE ATTACKING TERRITORY
if (!this.prevCountry) {
if (country.owner === this.player.name && country.army > 1) {
+ // NEW: Block attacking if exploring
+ if (country.isExploring) {
+ if (Gamestate.showToast) Gamestate.showToast("Cannot attack: Troops are currently exploring.", "red");
+ return;
+ }
+
+
this.prevCountry = country; // Source selected
e.target.classList.add('flash');
+
this.prevTarget = e.target;
if (turnInfoMessage) turnInfoMessage.textContent = "Now, select an adjacent enemy territory to ATTACK.";
} else {
@@ -7994,7 +8002,15 @@ Gamestate.openDiplomacy = function (targetName) {
// WHEN YOU CLICK YOUR FIRST TERRITORY (Selecting the attacker)
if (!this.prevCountry) {
if (country.owner === this.player.name) {
+ // NEW: Block attacking if exploring
+ if (country.isExploring) {
+ if (Gamestate.showToast) Gamestate.showToast("Cannot attack: Troops are currently exploring.", "red");
+ return;
+ }
+
+
// --- INTERNAL PURGE LOGIC ---
+
let trespasser = this.players.find(p => p !== this.player && p.alive && !p.isNeutral && p.commander && p.commander.hp > 0 && p.commander.loc === country.name);
if (this.commandersEnabled && trespasser) {
if (country.army <= 1) {
@@ -8173,7 +8189,14 @@ Gamestate.openDiplomacy = function (targetName) {
if (this.showToast) this.showToast("Maneuver blocked: Territory is under Elder's Edict lockdown.", "red");
return;
}
+ // NEW: Block maneuvering if exploring
+ if (country.isExploring) {
+ if (Gamestate.showToast) Gamestate.showToast("Maneuver blocked: Troops are currently exploring.", "red");
+ return;
+ }
+
if (country.army <= 1) {
+
if (this.showToast) this.showToast("Maneuver blocked: Must have more than 1 troop to move.", "red");
return;
}
@@ -8593,10 +8616,23 @@ Gamestate.openDiplomacy = function (targetName) {
if (i === this.players.length) {
if (this.player.areas.length === 0 || (this.commandersEnabled && this.player.commander && this.player.commander.hp <= 0)) {
this.player.alive = false;
- setTimeout(() => this.aiMove(), 100);
+ this.checkWinCondition(); // This triggers the defeat modal instantly
return;
}
+
+
+ // --- NEW: Process Exploration Countdowns FIRST ---
+ for (let c of this.countries) {
+ if (c.owner === this.player.name && c.isExploring) {
+ c.exploreTurnsLeft--;
+ if (c.exploreTurnsLeft <= 0) {
+ await this.resolveExplorationOutcome(c);
+ }
+ }
+ }
+
if (this.bobbleheads) {
+
this.bobbleheads.forEach(b => {
if (b.active) {
b.active = false;
@@ -9000,7 +9036,11 @@ Gamestate.openDiplomacy = function (targetName) {
let country = this.countries.find(c => c.name === area);
if (country && country.army > 1 && country.owner === this.players[i].name && (!this.nukesEnabled || !country.isSilo || Math.random() < 0.2)) {
await this.aiAttack(country, i, turbo);
+ // NEW: If this attack ended the game, freeze the AI instantly!
+ if (this.gameOver) return;
}
+
+
}
this.aiManeuver(i);
if (this.players[i].conqueredThisTurn) {
@@ -9641,11 +9681,31 @@ Gamestate.openDiplomacy = function (targetName) {
isVictory = true;
player.conqueredThisTurn = true;
- // --- THIS IS THE NEW ENCOUNTER TRIGGER ---
- await this.triggerEncounterCheck('post_conquest');
+ // --- NEW: EXPEDITION WIPED OUT CHECK ---
+ if (opponent.isExploring) {
+ // Only show the modal if the HUMAN player's expedition was destroyed
+ if (originalOwner === this.player.name) {
+ const title = "Signal Lost";
+ const msg = `>>> CONNECTION SEVERED <<<
The expedition force searching the ${opponent.explorePOI || 'area'} at ${formatTerritoryName(opponent.name)} has been completely wiped out by an attack from ${player.name}.
All hands and recovered assets are lost.`;
+ const choices = [{ id: "acknowledge", text: "[Acknowledge] A tragic loss." }];
+
+ this.modalIsOpen = true;
+ await this.showEncounterModal(title, msg, choices, () => {
+ this.logAction(`[ TRAGEDY ] The expedition at ${formatTerritoryName(opponent.name)} was slaughtered by ${player.name}.`, true);
+ return null; // Skip outcome delay screen
+ });
+ this.modalIsOpen = false;
+ }
+ // Clear the exploring flags so the new owner doesn't inherit them
+ opponent.isExploring = false;
+ opponent.exploreTurnsLeft = 0;
+ opponent.exploreType = null;
+ opponent.explorePOI = null;
+ }
flavor = (Math.random() < 0.10) ? (" " + combatFlavors[Math.floor(Math.random() * combatFlavors.length)]) : "!";
this.players.forEach(p => {
+
if (p.name === opponent.owner) {
let index = p.areas.indexOf(opponent.name);
if (index > -1) {
@@ -9677,7 +9737,13 @@ Gamestate.openDiplomacy = function (targetName) {
country.army -= movedTroops;
if (defender) defender.style.fill = opponent.color;
+ // --- NEW ENCOUNTER TRIGGER IS NOW HERE (After troops have arrived!) ---
+ if (player.isPlayer) {
+ await this.triggerEncounterCheck('post_conquest', opponent.name);
+ }
+
if (this.perksEnabled && player.perk) {
+
if (player.perk.id === 'fev_infection') {
// (Bonus Fix: Made the victory FEV math match the repulse FEV math!)
const converted = Math.max(1, Math.floor(originalDefenderArmy * 0.25));
@@ -9866,6 +9932,39 @@ Gamestate.openDiplomacy = function (targetName) {
Gamestate.checkWinCondition = function () {
if (this.gameOver) return;
+ let winModal = document.querySelector('#win-modal');
+ let winMessage = document.querySelector('.win-message');
+ if (!winModal || !winMessage) return;
+
+ // --- CHECK 1: PLAYER DEFEAT ---
+ let playerDead = false;
+ if (this.commandersEnabled && (!this.player.commander || this.player.commander.hp <= 0)) {
+ playerDead = true;
+ } else if (this.player.areas.length === 0) {
+ playerDead = true;
+ }
+
+ if (playerDead) {
+ this.gameOver = true;
+ this.player.alive = false;
+
+ let defeatFlavors = [
+ "Your faction is nothing but a footnote in the wasteland's bloody history.",
+ "Your forces have been scattered, your settlements burned. The wastes claim another victim.",
+ "War never changes. And this time, you were on the losing side."
+ ];
+ let cmdrFlavor = "Your Commander has fallen. Without leadership, your forces scattered into the irradiated winds.";
+
+ winMessage.textContent = "YOU DIED.";
+ winMessage.style.color = "#ff3333";
+ let subMsg = winMessage.nextElementSibling;
+ if (subMsg && subMsg.tagName === 'P') {
+ subMsg.textContent = this.commandersEnabled ? cmdrFlavor : defeatFlavors[Math.floor(Math.random() * defeatFlavors.length)];
+ }
+ winModal.style.display = "block";
+ return;
+ }
+
let totalPlayableLand = this.countries.filter(c => !c.isCrater).length;
let playerLand = this.countries.filter(c => c.owner === this.player.name).length;
let ownsAllLand = (playerLand >= totalPlayableLand);
@@ -9879,48 +9978,52 @@ Gamestate.openDiplomacy = function (targetName) {
if (livingRivals.length > 0) allRivalsDead = false;
}
- // --- CHECK 1: TOTAL DOMINATION ---
+ // --- CHECK 2: TOTAL DOMINATION ---
if (ownsAllLand && allRivalsDead) {
this.gameOver = true;
- let winModal = document.querySelector('#win-modal');
- let winMessage = document.querySelector('.win-message');
-
- if (winMessage) {
- winMessage.textContent = "TOTAL DOMINATION!";
- winMessage.style.color = "var(--pip-color)";
- let subMsg = winMessage.nextElementSibling;
- if (subMsg && subMsg.tagName === 'P') {
- if (this.commandersEnabled) {
- subMsg.textContent = "All rival commanders have been executed and the wasteland is entirely under your control.";
- } else {
- subMsg.textContent = "You have conquered all territories in the wasteland.";
- }
- }
+ let domFlavors = [
+ "You have conquered the wastes. All who opposed you are dust in the wind.",
+ "From the ashes of the old world, your empire rises absolute.",
+ "No rivals remain. The wasteland belongs to you alone."
+ ];
+ winMessage.textContent = "TOTAL DOMINATION!";
+ winMessage.style.color = "var(--pip-color)";
+ let subMsg = winMessage.nextElementSibling;
+ if (subMsg && subMsg.tagName === 'P') {
+ subMsg.textContent = domFlavors[Math.floor(Math.random() * domFlavors.length)];
}
- if (winModal) winModal.style.display = "block";
+ winModal.style.display = "block";
return;
}
- // --- CHECK 2: SHARED ALLIED VICTORY ---
+ // --- CHECK 3: SHARED ALLIED VICTORY ---
let livingFactions = this.players.filter(p => p.alive && !p.isNeutral && p.name !== this.player.name);
if (livingFactions.length > 0) {
- let allLivingAreIdolized = livingFactions.every(p => this.diplomacy.reputation[p.name] && this.diplomacy.reputation[p.name][this.player.name] >= 35);
+ let allLivingAreAllies = false;
+
+ if (this.isAllianceMode) {
+ // NEW: In Alliance mode, check if everyone alive shares your Team ID
+ allLivingAreAllies = livingFactions.every(p => p.team === this.player.team);
+ } else {
+ // Classic Mode: Require Idolized diplomacy reputation
+ allLivingAreAllies = livingFactions.every(p => this.diplomacy.reputation[p.name] && this.diplomacy.reputation[p.name][this.player.name] >= 35);
+ }
- if (allLivingAreIdolized) {
+ if (allLivingAreAllies) {
this.gameOver = true;
- let winModal = document.querySelector('#win-modal');
- let winMessage = document.querySelector('.win-message');
-
- if (winMessage) {
- winMessage.textContent = "ALLIED VICTORY!";
- winMessage.style.color = "#0088ff"; // A distinct "peace" color
- let subMsg = winMessage.nextElementSibling;
- if (subMsg && subMsg.tagName === 'P') {
- subMsg.textContent = "Through masterful diplomacy and shared trust, the remaining factions have united under your leadership.";
- }
+ let allyFlavors = [
+ "Through diplomacy and shared blood, you and your allies have united the wastes.",
+ "A coalition of power now rules the wasteland. Together, you brought order from chaos.",
+ "The wars are over. Your alliance stands victorious over the irradiated ashes."
+ ];
+ winMessage.textContent = "ALLIED VICTORY!";
+ winMessage.style.color = "#39ff14"; // Distinct green/alliance color
+ let subMsg = winMessage.nextElementSibling;
+ if (subMsg && subMsg.tagName === 'P') {
+ subMsg.textContent = allyFlavors[Math.floor(Math.random() * allyFlavors.length)];
}
- if (winModal) winModal.style.display = "block";
+ winModal.style.display = "block";
}
}
};
@@ -9945,21 +10048,30 @@ Gamestate.openDiplomacy = function (targetName) {
// If an onChoice function is provided, execute it and get the result message
if (onChoice) {
const resultMessage = await onChoice(choice.id);
- titleEl.textContent = "Outcome";
- messageEl.innerHTML = resultMessage; // Display the result
- choicesContainer.innerHTML = ''; // Clear the buttons
+
+ // NEW: If the event returned text, show the 2.5s delay screen.
+ // If it returned null, close the modal instantly!
+ if (resultMessage) {
+ titleEl.textContent = "Outcome";
+ messageEl.innerHTML = resultMessage; // Display the result
+ choicesContainer.innerHTML = ''; // Clear the buttons
- // Wait 2.5 seconds before closing the modal
- setTimeout(() => {
+ // Wait 2.5 seconds before closing the modal
+ setTimeout(() => {
+ modal.style.display = 'none';
+ resolve(choice.id);
+ }, 2500);
+ } else {
modal.style.display = 'none';
resolve(choice.id);
- }, 2500);
+ }
} else {
// Original behavior if no onChoice is provided
modal.style.display = 'none';
resolve(choice.id);
}
};
+
choicesContainer.appendChild(button);
});
@@ -10050,7 +10162,7 @@ Gamestate.openDiplomacy = function (targetName) {
const army = territory.army;
const threat = creature.threat;
const tName = formatTerritoryName(territory.name);
- const cName = `${creature.name}`;
+ const cName = `${creature.name}`;
// Calculate Risk Assessment
let riskText, riskColor;
@@ -10163,15 +10275,20 @@ Gamestate.openDiplomacy = function (targetName) {
}
}
}
- };
+ }; // <-- This closes the callback properly!
+
+ // These lines properly close the Creature Encounter function!
+ await this.showEncounterModal(title, message, choices, onChoiceCallback);
+ this.modalIsOpen = false;
+ this.updateInfo();
+ this.drawMapText();
+ };
Gamestate.resolveRadioEncounter = async function () {
if (this.player.areas.length === 0 || this.modalIsOpen) return;
-
// Find a valid territory: not already exploring, army > 1
const validTerritories = this.player.areas.map(a => this.countries.find(c => c.name === a)).filter(c => c && c.army > 1 && !c.isExploring);
if (validTerritories.length === 0) return;
-
const territory = validTerritories[Math.floor(Math.random() * validTerritories.length)];
// Pick an encounter type
@@ -10193,7 +10310,7 @@ Gamestate.openDiplomacy = function (targetName) {
}
const title = "Radio Transmission";
- const message = `>>> INCOMING TRANSMISSION <<<
Garrison forces at ${formatTerritoryName(territory.name)} report they have discovered ${poiName}.
Do you want to send a detachment to ${actionVerb}? This will lock the territory down for 2 turns and reduce their defenses by 15%.`;
+ const message = `>>> INCOMING TRANSMISSION <<<
Garrison forces at ${formatTerritoryName(territory.name)} report they have discovered ${poiName}.
Do you want to send a detachment to ${actionVerb}? This will lock the territory down for 2 turns and reduce their defenses by 15%.`;
const choices = [
{ id: "explore", text: "[Investigate] Send the detachment." },
@@ -10211,10 +10328,10 @@ Gamestate.openDiplomacy = function (targetName) {
territory.explorePOI = poiName;
this.logAction(`[ EXPEDITION ] Troops at ${formatTerritoryName(territory.name)} have begun investigating ${poiName}. They will report back in 2 turns.`, true);
- return `Expedition launched. The garrison is now vulnerable.`;
+ return null; // Skip the outcome screen
} else {
this.logAction(`[ EXPEDITION ] You ordered the garrison at ${formatTerritoryName(territory.name)} to hold their position and ignore the ${poiName}.`);
- return `Transmission ended. The garrison will maintain their post.`;
+ return null; // Skip the outcome screen
}
};
@@ -10224,6 +10341,128 @@ Gamestate.openDiplomacy = function (targetName) {
this.drawMapText();
};
+ Gamestate.resolveExplorationOutcome = async function (territory) {
+ if (this.modalIsOpen) return;
+ this.modalIsOpen = true;
+
+ territory.isExploring = false; // Remove the searching status
+
+ const title = "Expedition Returned";
+ let message = `Your detachment has returned from exploring the ${territory.explorePOI} at ${formatTerritoryName(territory.name)}.
`;
+ let logMsg = "";
+
+ const roll = Math.random();
+
+ // 30% Chance for High Value Loot (Caps or Units)
+ if (roll < 0.30) {
+ let capsFound = Math.floor(Math.random() * 15) + 10;
+ if (this.wastelandEconomyActive) this.player.caps += capsFound;
+ message += `They hit the jackpot! Your troops recovered ${capsFound} Caps from a pre-war stash.`;
+ logMsg = `[ EXPEDITION ] Success at ${formatTerritoryName(territory.name)}. Found ${capsFound} Caps!`;
+ }
+ // 20% Chance for Stimpak
+ else if (roll < 0.50 && this.commandersEnabled && this.player.commander && this.player.commander.stimpaks < 3) {
+ this.player.commander.stimpaks++;
+ let navInv = document.getElementById('nav-inv');
+ if (navInv) navInv.classList.add('inv-pulse');
+ message += `They discovered a sealed medical kit containing a rare Stimpak!`;
+ logMsg = `[ EXPEDITION ] Success at ${formatTerritoryName(territory.name)}. Recovered a Stimpak!`;
+ }
+ // 25% Chance for an Ambush / Trap (Loss of units)
+ else if (roll < 0.75) {
+ let casualties = Math.floor(territory.army * 0.20) + 1; // 20% casualties
+ if (territory.army - casualties < 1) casualties = territory.army - 1; // Never wipe out
+
+ if (casualties > 0) {
+ territory.army -= casualties;
+ this.player.army -= casualties;
+ message += `It was a trap! The detachment was ambushed, suffering ${casualties} casualties before retreating.`;
+ logMsg = `[ EXPEDITION ] Disaster at ${formatTerritoryName(territory.name)}. Lost ${casualties} troops to a trap.`;
+ } else {
+ message += `They triggered an ancient security system but narrowly escaped without casualties.`;
+ logMsg = `[ EXPEDITION ] Troops at ${formatTerritoryName(territory.name)} triggered a trap but escaped unharmed.`;
+ }
+ }
+ // 25% Chance for Nothing
+ else {
+ message += `The area had already been stripped clean by scavengers. They returned empty-handed.`;
+ logMsg = `[ EXPEDITION ] The search at ${formatTerritoryName(territory.name)} turned up nothing.`;
+ }
+
+ const choices = [{ id: "acknowledge", text: "[Acknowledge] Resume normal patrols." }];
+
+ const onChoiceCallback = () => {
+ this.logAction(logMsg, true);
+ return null; // Skip the outcome screen
+ };
+
+ await this.showEncounterModal(title, message, choices, onChoiceCallback);
+ this.modalIsOpen = false;
+ this.updateInfo();
+ this.drawMapText();
+ };
+
+ Gamestate.resolveBattleEncounter = async function (territoryName) {
+ // Force unlock the modal state (in case the Move Troops slider confused it)
+ this.modalIsOpen = false;
+
+ const territory = this.countries.find(c => c.name === territoryName);
+ if (!territory || territory.army < 1) return; // FIX: Even 1 troop can discover a vault after a battle
+
+ // First, check if this territory has a designated Vault
+
+ let poiName = "";
+ let type = "";
+ let actionVerb = "investigate the ruins";
+ let isVault = false;
+
+ const possibleVaults = encounterData.vaults.filter(v => v.territory === territoryName);
+
+ if (possibleVaults.length > 0) {
+ // Pick a vault if there are multiple in this territory
+ poiName = possibleVaults[Math.floor(Math.random() * possibleVaults.length)].name;
+ type = "vault";
+ actionVerb = "breach the vault door";
+ isVault = true;
+ } else {
+ // If no vault, pick a generic location or container
+ const types = ["location", "container"];
+ type = types[Math.floor(Math.random() * types.length)];
+
+ if (type === "location") {
+ poiName = encounterData.genericLocations[Math.floor(Math.random() * encounterData.genericLocations.length)];
+ actionVerb = "secure the area";
+ } else {
+ poiName = encounterData.containers[Math.floor(Math.random() * encounterData.containers.length)];
+ actionVerb = "attempt to crack it open";
+ }
+ }
+
+ const title = isVault ? "Vault Discovered" : "Post-Battle Discovery";
+ const message = `>>> SECTOR SECURED <<<
While clearing out the last of the enemy resistance at ${formatTerritoryName(territory.name)}, your troops uncovered ${poiName}.
Do you want to order the garrison to ${actionVerb}? This will lock the territory down for 2 turns and reduce their defenses by 15%.`;
+
+ const choices = [
+ { id: "explore", text: "[Investigate] Send the detachment." },
+ { id: "ignore", text: "[Ignore] Focus on fortifications." }
+ ];
+
+ this.modalIsOpen = true;
+
+ const onChoiceCallback = (decision) => {
+ if (decision === 'explore') {
+ territory.isExploring = true;
+ territory.exploreTurnsLeft = 2;
+ territory.exploreType = type;
+ territory.explorePOI = poiName;
+
+ this.logAction(`[ EXPEDITION ] Conquerors at ${formatTerritoryName(territory.name)} are breaching ${poiName}. Reports expected in 2 turns.`, true);
+ return null; // Skip the outcome screen
+ } else {
+ this.logAction(`[ TACTICAL ] The garrison at ${formatTerritoryName(territory.name)} ignored ${poiName} to focus on defense.`);
+ return null; // Skip the outcome screen
+ }
+ };
+
await this.showEncounterModal(title, message, choices, onChoiceCallback);
this.modalIsOpen = false;
@@ -10232,27 +10471,69 @@ Gamestate.openDiplomacy = function (targetName) {
};
- Gamestate.triggerEncounterCheck = async function (triggerType) {
+ Gamestate.triggerEncounterCheck = async function (triggerType, territoryName = null) {
// If encounters are disabled in the game settings, do nothing.
if (!this.encountersEnabled) return;
+ // 1. EXPLORATION LOCKOUT: Cannot trigger new events if already exploring
+ let isCurrentlyExploring = this.countries.some(c => c.owner === this.player.name && c.isExploring);
+ if (isCurrentlyExploring) return;
+
+ // Initialize trackers if they don't exist
+ if (this.turnsSinceLastEncounter === undefined) this.turnsSinceLastEncounter = 0;
+ if (this.encounterCooldown === undefined) this.encounterCooldown = 0;
+
let chance = 0;
+
if (triggerType === 'start_of_turn') {
- chance = 1.0; // TEST MODE: 100% chance to trigger!
+ // Update timers at the start of the turn
+ if (this.encounterCooldown > 0) this.encounterCooldown--;
+ this.turnsSinceLastEncounter++;
+
+ // 2. THE 3-TURN COOLDOWN: 0% chance while resting
+ if (this.encounterCooldown > 0) {
+ chance = 0;
+ }
+ // 3. THE 9-TURN PITY TIMER: 100% chance if starved
+ else if (this.turnsSinceLastEncounter >= 9) {
+ chance = 1.0;
+ }
+ // STANDARD RATE
+ else {
+ chance = 0.08; // 8% chance per turn
+ }
} else if (triggerType === 'post_conquest') {
- chance = 1.0; // TEST MODE: 100% chance to trigger!
+ // If on cooldown, no post-battle discoveries allowed
+ if (this.encounterCooldown > 0) {
+ chance = 0;
+ } else {
+ chance = 0.15; // 15% chance per conquest
+ }
}
+ // Roll the dice
if (Math.random() < chance) {
if (triggerType === 'start_of_turn') {
+ // Reset timers!
+ this.turnsSinceLastEncounter = 0;
+ this.encounterCooldown = 3; // Enforce 3-turn break
+
await this.resolveRadioEncounter();
+ } else if (triggerType === 'post_conquest' && territoryName) {
+ // Reset timers!
+ this.turnsSinceLastEncounter = 0;
+ this.encounterCooldown = 3; // Enforce 3-turn break
+
+ await this.resolveBattleEncounter(territoryName);
}
- // Post-conquest will go here shortly!
}
};
+
+
+
/**
* Sets the initial reputation values between all players based on their faction affinities.
* This function should be called once at the start of a new game.