From 2c6cfccc4012cc7628a33c19ddb23530ad4e992e Mon Sep 17 00:00:00 2001 From: threememories Date: Thu, 30 Apr 2026 20:34:57 +0000 Subject: [PATCH] Upload files to "/" --- index.html | 381 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 331 insertions(+), 50 deletions(-) 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.