From cc74d8e45cb01301f1c489c55202803bfb7c4ceb Mon Sep 17 00:00:00 2001 From: threememories Date: Tue, 28 Apr 2026 21:07:10 +0000 Subject: [PATCH] Upload files to "/" --- index.html | 687 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 480 insertions(+), 207 deletions(-) diff --git a/index.html b/index.html index b322f07..af7047e 100644 --- a/index.html +++ b/index.html @@ -3525,7 +3525,7 @@ // --- Fallout 3 Factions --- "Brotherhood of Steel": { leader: "Elder Lyons", - color: "#3a8dcb", + color: "#4d0099", perk: { id: "power_armor_infantry", name: "Power Armor Infantry", @@ -3591,13 +3591,14 @@ perk: { id: "logistical_superiority", name: "Logistical Superiority", - description: "Receive +1 bonus troop during deployment for every Continent you fully control." + description: "Your vast supply lines increase the standard troop reinforcement bonus of any fully controlled Continent by 50% (rounded up)." }, affinity: { "Caesar's Legion": -30, "Great Khans": -15, "Mojave Brotherhood": -5 } }, + "Caesar's Legion": { leader: "Caesar", - color: "#e04f4f", + color: "#ad1f1f", perk: { id: "scourge_of_the_east", name: "Scourge of the East", @@ -3607,24 +3608,28 @@ }, "New Vegas Securitrons": { leader: "Mr. House", - color: "#00ff00", + color: "#c2b236", perk: { id: "the_house_always_wins", name: "Predictive Simulation", - description: "If an attack completely fails, instantly abort the battle and restore all lost attacking troops. (2 Turn Cooldown)" + // --- FIX: Changed text to "3 Turn" and "all lost troops" (since it now restores defenders too) + description: "If an attack completely fails, instantly abort the battle and restore all lost troops to both sides. (3 Turn Cooldown)" }, affinity: { "New California Republic": -10, "Caesar's Legion": -20 } }, + "Mojave Brotherhood": { leader: "Elder McNamara", color: "#556b2f", perk: { id: "elders_edict", name: "Elder's Edict", - description: "Once per turn, 'lock down' one territory. It cannot be attacked for one full round, but its troops also cannot move or attack." + // --- FIX: Updated description to accurately reflect 3-turn mechanics + description: "Lock down one of your territories for up to 3 turns. It cannot attack, maneuver, or be attacked. You may lift it early. (3 Turn Cooldown starts when lifted)" }, affinity: { "New California Republic": -5 } }, + "Great Khans": { leader: "Papa Khan", color: "#6b4a3a", @@ -3640,33 +3645,36 @@ color: "#a0522d", perk: { id: "psycho_rush", - name: "Psycho Rush", - description: "Your Commander unit provides an attack bonus of +2 to the highest attack die, instead of the standard +1." + name: "Chem-Fueled Raids", + description: "When you conquer a territory, there is a 30% chance to mug the defeated player! You may steal Caps, Stimpaks, or even Bobbleheads. If their pockets are empty, you enslave 1-2 survivors into your army." }, affinity: { "New California Republic": -10, "Caesar's Legion": -10 } }, + // --- Fallout 4 Factions --- "The Minutemen": { leader: "Preston Garvey", - color: "#0077b6", + color: "#b2976b", perk: { id: "another_settlement", name: "Another Settlement...", - description: "If an enemy attacks one of your territories, you may instantly move up to 3 troops from an adjacent friendly territory to reinforce it before the battle begins." + description: "When an enemy attacks you with a larger force, there is a 40% chance the local militia answers your flare, instantly spawning 2-4 defenders and granting a +15% defense bonus." }, affinity: { "The Railroad": 15, "The Institute": -10, "The Gunners": -10 } }, + "The Institute": { leader: "Father", color: "#e0e0e0", perk: { id: "synth_replacements", name: "Synth Replacements", - description: "When you lose a battle while defending, 30% of your destroyed Synths are recovered and sent back to your reserves." + description: "Whenever you lose troops in any battle (attacking or defending), there is a 10% chance per casualty that the Synth is recovered and instantly sent to your reserves." }, affinity: { "Brotherhood of Steel": -20, "The Railroad": -20 } }, + "The Railroad": { leader: "Desdemona", color: "#888888", @@ -5225,12 +5233,21 @@ if (this.perksEnabled) { setInitialReputations(); } - // --- Initialize Core Game Variables --- + // --- Initialize Core Game Variables --- this.aiTurn = false; this.gameOver = false; this.turn = 1; + // --- NEW: SCRUB LEFTOVER LOCKDOWNS FROM PREVIOUS GAME --- + this.countries.forEach(c => { + c.isLockedDown = false; + let mapEl = document.getElementById(c.name); + if (mapEl) mapEl.classList.remove('lockdown-territory'); + }); + // --- END OF CLEANUP --- + // --- CORRECTED INITIAL STAGE LOGIC --- + if (this.wastelandEconomyActive) { // If economy is active, the first phase is always for recruiting this.stage = "Recruitment"; @@ -5371,7 +5388,6 @@ const clickedCountry = this.countries.find(c => c.name === countryName); if (!clickedCountry || clickedCountry.owner !== this.player.name) return; - // Check if the perk is on cooldown for the player. if (this.player.lockdownCooldown > 0) { if (this.showToast) this.showToast(`Edict is on cooldown for ${this.player.lockdownCooldown} more turn(s).`, "red"); return; @@ -5391,19 +5407,22 @@ // Now, toggle the state of the clicked country. clickedCountry.isLockedDown = !isAlreadyLocked; let clickedEl = document.getElementById(clickedCountry.name); + if (clickedCountry.isLockedDown) { // If we are ADDING the lockdown if (clickedEl) clickedEl.classList.add('lockdown-territory'); - clickedCountry.lockdownTimer = 3; // Set the 3-turn timer + clickedCountry.lockdownTimer = 3; // FIX: Ensure timer starts at 3 await this.logAction(`LOCKDOWN ENACTED: ${formatTerritoryName(clickedCountry.name)} is now under Elder's Edict for 3 turns.`); } else { // If we are REMOVING the lockdown if (clickedEl) clickedEl.classList.remove('lockdown-territory'); - clickedCountry.lockdownTimer = 0; // Clear the timer - this.player.lockdownCooldown = 4; // Start the cooldown (3 turns + current turn) - await this.logAction(`LOCKDOWN LIFTED: The edict has been lifted from ${formatTerritoryName(clickedCountry.name)}.`); + clickedCountry.lockdownTimer = 0; + + // FIX: Start the cooldown exactly at 3 turns when lifted manually! + this.player.lockdownCooldown = 3; + + await this.logAction(`LOCKDOWN LIFTED: The edict has been lifted from ${formatTerritoryName(clickedCountry.name)}. Cooldown started.`); } - this.updateInfo(); }; @@ -5411,6 +5430,7 @@ + Gamestate.drawMapText = function () { this.countries.forEach(country => { let areaOnMap = document.getElementById(country.name); @@ -6011,7 +6031,7 @@ this.isToastActive = false; this.processToastQueue(); }, 400); - }, 3500); // Alert stays on screen for 3.5 seconds + }, 2700); // Alert stays on screen for 2.7 seconds }; @@ -6430,29 +6450,31 @@ let showButton = false; let tooltipText = ""; let isButtonDisabled = false; + let isBuffActive = false; // --- NEW: Tracks if an active buff is running so the button keeps glowing if (this.perksEnabled && this.player && this.player.perk && !this.aiTurn) { // --- PERK 1: Wasteland Raiders --- if (this.player.perk.id === 'chem_frenzy') { showButton = true; - perkButton.textContent = "Use Chem Frenzy"; - tooltipText = "Activate to select an attacking territory and sacrifice troops for a massive, one-time combat bonus. (3-turn cooldown)"; - - // Disable button if in the wrong phase or on cooldown - if (this.stage !== 'Battle' || (this.player.chemFrenzyCooldown > 0)) { + if (this.stage !== 'Battle') { isButtonDisabled = true; - if (this.player.chemFrenzyCooldown > 0) { - perkButton.textContent = `Frenzy on Cooldown (${this.player.chemFrenzyCooldown})`; - tooltipText = `You must wait ${this.player.chemFrenzyCooldown} more turn(s) before using Chem Frenzy again.`; - } + perkButton.textContent = "Use Chem Frenzy"; + tooltipText = "Can only be used during the Battle Phase."; + } else if (this.player.chemFrenzyCooldown > 0) { + isButtonDisabled = true; + perkButton.textContent = "Frenzy on Cooldown (" + this.player.chemFrenzyCooldown + ")"; + tooltipText = "You must wait " + this.player.chemFrenzyCooldown + " more turn(s) before using Chem Frenzy again."; + } else { + isButtonDisabled = false; + perkButton.textContent = "Use Chem Frenzy"; + tooltipText = "Activate to select an attacking territory and sacrifice troops for a massive, one-time combat bonus. (3-turn cooldown)"; } - perkButton.onclick = () => { if (!isButtonDisabled) { this.stage = 'Frenzy Targeting'; if (turnInfoMessage) turnInfoMessage.textContent = "Select a friendly territory to launch your frenzied attack FROM."; - this.updateInfo(); // Re-run to update UI + this.updateInfo(); } }; } @@ -6460,59 +6482,101 @@ // --- PERK 2: The Gunners --- else if (this.player.perk.id === 'mercenary_contracts') { showButton = true; - perkButton.textContent = "Mercenary Contract (2 Caps)"; - isButtonDisabled = this.player.cards.length < 2; - tooltipText = isButtonDisabled ? "Requires at least 2 Bottle Caps." : "Spend 2 Caps to deploy troops to your reserves, scaling with territory owned."; - perkButton.onclick = () => this.useMercenaryContract(); + 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."; + perkButton.onclick = () => { + if (!isButtonDisabled) this.useMercenaryContract(); + }; } - // --- NEW: PERK - BOS Outcasts --- + // --- PERK 3: BOS Outcasts --- else if (this.player.perk.id === 'tech_hoarders') { showButton = true; + const overdriveCost = this.player.army; - // --- NEW: DYNAMIC COST CALCULATION --- - const overdriveCost = this.player.army; // Cost is 1 cap per unit on the board - tooltipText = "Spend " + overdriveCost + " Caps to grant all your attacking armies +10% win chance for 3 turns. (Once per game)"; - // --- END OF NEW LOGIC --- - if (this.player.techOverdriveActive > 0) { isButtonDisabled = true; - perkButton.textContent = "Overdrive Active (" + this.player.techOverdriveActive + ")"; + isBuffActive = true; // --- NEW: Let the game know the buff is running so it glows! + perkButton.innerHTML = "BUFF ACTIVE
(" + this.player.techOverdriveActive + " Turns Left)"; tooltipText = "Your armies have a global +10% attack bonus for " + this.player.techOverdriveActive + " more turn(s)."; - } - // --- NEW: Check for cooldown instead of one-time use --- - else if (this.player.techOverdriveCooldown > 0) { + } else if (this.player.techOverdriveCooldown > 0) { isButtonDisabled = true; - perkButton.textContent = "Overdrive on Cooldown (" + this.player.techOverdriveCooldown + ")"; + perkButton.innerHTML = "RECHARGING
(" + this.player.techOverdriveCooldown + " Turns Left)"; tooltipText = "The advanced systems need " + this.player.techOverdriveCooldown + " more turn(s) to recharge."; - } - - // --- MODIFIED: Check against dynamic cost --- - else if (this.player.caps < overdriveCost) { + } else if (this.player.caps < overdriveCost) { isButtonDisabled = true; - perkButton.textContent = "Technology Overdrive (" + overdriveCost + " Caps)"; + perkButton.innerHTML = "Technology Overdrive
(" + overdriveCost + " Caps)"; tooltipText = "Insufficient Caps. Requires " + overdriveCost + "."; - } - else { + } else { isButtonDisabled = false; - perkButton.textContent = "Technology Overdrive (" + overdriveCost + " Caps)"; + perkButton.innerHTML = "Technology Overdrive
(" + overdriveCost + " Caps)"; + tooltipText = "Spend " + overdriveCost + " Caps to grant all your attacking armies +10% win chance for 3 turns. (3 Turn Cooldown)"; } perkButton.onclick = () => { if (!isButtonDisabled) { - // --- MODIFIED: Deduct dynamic cost --- this.player.caps -= overdriveCost; this.player.techOverdriveActive = 3; this.logAction("⚙️ TECHNOLOGY OVERDRIVE ENGAGED! All attacking forces receive +10% win chance for 3 rounds.", true); - this.updateInfo(); + this.updateInfo(); } }; } - // --- NEW: PERK 5: New Vegas Securitrons --- + + // --- PERK 4: Mojave Brotherhood --- + else if (this.player.perk.id === 'elders_edict') { + showButton = true; + const lockedCountry = this.countries.find(c => c.isLockedDown); + const isAnyTerritoryLocked = !!lockedCountry; + + // 1. Safely determine the button text (Fixes Bug 1 & 2) + if (isAnyTerritoryLocked) { + perkButton.innerHTML = "Lift Lockdown
(" + (lockedCountry.lockdownTimer || 3) + " Turns Left)"; + } else if (this.player.lockdownCooldown > 0) { + perkButton.innerHTML = "Edict on Cooldown
(" + this.player.lockdownCooldown + " Turns Left)"; + } else { + perkButton.textContent = "Enact Lockdown"; + } + + // 2. Safely determine if disabled & set Tooltip (Fixes Bug 2) + if (isAnyTerritoryLocked) { + isButtonDisabled = false; + tooltipText = "Click to manually lift the lockdown early and begin the 3-turn cooldown."; + } else if (this.player.lockdownCooldown > 0) { + isButtonDisabled = true; + tooltipText = "Edict requires " + this.player.lockdownCooldown + " more turn(s) to recharge."; + } else if (this.stage === "Recruitment") { + isButtonDisabled = true; + tooltipText = "Cannot enact Elder's Edict during the Recruitment phase."; + } else if (this.stage === "Fortify" && this.player.reserve > 0) { + isButtonDisabled = true; + tooltipText = "Deploy all reserve troops to enable the Elder's Edict."; + } else { + isButtonDisabled = false; + tooltipText = "Select one of your territories and then click this button to apply the Elder's Edict."; + } + + perkButton.onclick = () => { + if (!isButtonDisabled) { + if (isAnyTerritoryLocked) { + this.toggleLockdown(lockedCountry.name); + this.updateInfo(); + } else if (this.prevCountry && this.prevCountry.owner === this.player.name) { + this.toggleLockdown(this.prevCountry.name); + this.updateInfo(); + } else { + if (this.showToast) this.showToast("Select one of your territories first to use the Edict.", "grey"); + } + } + }; + } + + + + // --- PERK 5: New Vegas Securitrons --- else if (this.player.perk.id === 'the_house_always_wins') { showButton = true; - - // Check if there is a valid failed attack from THIS turn to undo const hasValidUndo = this.player.lastFailedAttack && this.player.lastFailedAttack.turnNumber === this.turn && this.player.lastFailedAttack.losses > 0; if (this.player.predictiveCooldown > 0) { @@ -6531,60 +6595,98 @@ perkButton.onclick = async () => { if (!isButtonDisabled && hasValidUndo) { - // 1. Find the territory to restore troops to let source = this.countries.find(c => c.name === this.player.lastFailedAttack.sourceTerritory); - if (source) { - // 2. Restore the troops + let target = this.countries.find(c => c.name === this.player.lastFailedAttack.targetTerritory); // --- NEW: Find the target territory + + if (source && target) { + // 2. Restore the troops to BOTH sides source.army += this.player.lastFailedAttack.losses; + target.army += this.player.lastFailedAttack.defenderLosses; // --- NEW: Restore the defender - // 3. Update the map visually + // 3. Update the map visually for both let sourceMapEl = document.getElementById(source.name); if (sourceMapEl && sourceMapEl.nextElementSibling) sourceMapEl.nextElementSibling.textContent = source.army; - // 4. Set Cooldown & Clear the stored attack - this.player.predictiveCooldown = 3; // 2 turns + current - const restoredAmount = this.player.lastFailedAttack.losses; - this.player.lastFailedAttack = null; // Clear it so they can't click it twice + let targetMapEl = document.getElementById(target.name); + if (targetMapEl && targetMapEl.nextElementSibling) targetMapEl.nextElementSibling.textContent = target.army; - await this.logAction(`[ PREDICTIVE SIMULATION ] Attack declared a simulation! ${restoredAmount} Securitrons restored to ${formatTerritoryName(source.name)}.`, true); + // 4. Set Cooldown & Clear the stored attack + this.player.predictiveCooldown = 3; + const restoredAmount = this.player.lastFailedAttack.losses; + const restoredDefenders = this.player.lastFailedAttack.defenderLosses; + this.player.lastFailedAttack = null; + + await this.logAction(`[ PREDICTIVE SIMULATION ] Attack declared a simulation! ${restoredAmount} Securitrons and ${restoredDefenders} defenders restored.`, true); this.updateInfo(); } } }; } - - // --- PERK 3: Mojave Brotherhood --- - else if (this.player.perk.id === 'elders_edict') { - showButton = true; - const isAnyTerritoryLocked = this.countries.some(c => c.isLockedDown); - perkButton.textContent = isAnyTerritoryLocked ? "Lift Lockdown" : "Enact Lockdown"; - tooltipText = "Select one of your territories and then click this button to apply or remove the Elder's Edict."; - - if (this.stage === "Fortify" && this.player.reserve > 0) { - isButtonDisabled = true; - tooltipText = "Deploy all reserve troops to enable the Elder's Edict."; - } else if (this.player.lockdownCooldown > 0) { - isButtonDisabled = true; - tooltipText = `Edict is on cooldown for ${this.player.lockdownCooldown} more turn(s).`; - } - - perkButton.onclick = () => { - if (this.prevCountry && this.prevCountry.owner === this.player.name) { - this.toggleLockdown(this.prevCountry.name); - } else { - if (this.showToast) this.showToast("Select one of your territories first to use the Edict.", "grey"); - } - }; - } } // --- Final visibility and state decision --- perkButtonWrapper.style.display = showButton ? 'block' : 'none'; perkButton.disabled = isButtonDisabled; + // --- THIS IS THE FIX --- - if (!isButtonDisabled) perkButton.classList.add('ready-to-trade'); - else perkButton.classList.remove('ready-to-trade'); + if (!document.getElementById('perk-custom-styles')) { + let style = document.createElement('style'); + style.id = 'perk-custom-styles'; + style.innerHTML = ` + /* STATE 1: READY */ + @keyframes borderOnlyPulse { + 0% { border-color: var(--pip-color); box-shadow: inset 0 0 5px var(--pip-color), 0 0 5px var(--pip-color); } + 100% { border-color: #ffffff; box-shadow: inset 0 0 5px var(--pip-color), 0 0 15px var(--pip-color); } + } + .perk-ready-state { + animation: borderOnlyPulse 1s infinite alternate !important; + filter: none !important; + background-color: var(--pip-dark) !important; + } + /* FIX: Tell the button how to correctly handle a mouse hover! */ + .perk-ready-state:hover { + background-color: var(--pip-color) !important; + color: var(--pip-dark) !important; + } + + /* STATE 2: RUNNING */ + @keyframes textOnlyPulse { + 0% { color: rgba(128, 128, 128, 0.5); text-shadow: none; border-color: rgba(128, 128, 128, 0.3); } + 100% { color: var(--pip-color); text-shadow: 0 0 8px var(--pip-color); border-color: rgba(128, 128, 128, 0.3); } + } + .perk-running-state { + animation: textOnlyPulse 1.5s infinite alternate !important; + background-color: var(--pip-dark) !important; + box-shadow: inset 0 0 5px rgba(0,0,0,0.5) !important; + } + + /* STATE 3: DISABLED */ + .perk-disabled-state { + background-color: var(--pip-dark) !important; + opacity: 0.5 !important; + box-shadow: inset 0 0 5px var(--pip-color), 0 0 5px var(--pip-color) !important; + } + `; + document.head.appendChild(style); + } + + // 2. Wipe out any rogue inline JavaScript styles! + perkButton.style.animation = ""; + perkButton.style.backgroundColor = ""; + perkButton.style.opacity = ""; + perkButton.style.boxShadow = ""; + perkButton.classList.remove('ready-to-trade', 'perk-ready-state', 'perk-running-state', 'perk-disabled-state'); + + // 3. Apply the states purely via CSS classes + if (!isButtonDisabled) { + perkButton.classList.add('perk-ready-state'); // State 1 + } else if (typeof isBuffActive !== 'undefined' && isBuffActive) { + perkButton.classList.add('perk-running-state'); // State 2 + } else { + perkButton.classList.add('perk-disabled-state'); // State 3 + } // --- END OF FIX --- + // --- Event Listeners for the Custom Tooltip --- perkButtonWrapper.onmouseenter = (e) => { if (tooltipText && perkButtonWrapper.style.display === 'block') { @@ -6604,6 +6706,7 @@ } + } @@ -6807,15 +6910,49 @@ if (this.stage === "Battle") { let canManeuver = false; let owned = this.countries.filter(c => c.owner === this.player.name); + for (let t of owned) { - if (t.army > 1 && t.neighbours.some(n => { - let nc = this.countries.find(x => x.name === n); - return nc && nc.owner === this.player.name && !nc.isCrater; - })) { - canManeuver = true; - break; + if (t.army > 1 && !t.isLockedDown) { + + // 1. Standard Check: Adjacent friendly territory + let standardMove = t.neighbours.some(n => { + let nc = this.countries.find(x => x.name === n); + return nc && nc.owner === this.player.name && !nc.isCrater; + }); + + // 2. The Enclave Check: Vertibird Assault bypasses adjacency + let enclaveMove = false; + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'vertibird_assault') { + if (owned.length > 1) enclaveMove = true; + } + + // 3. Great Khans Check: Guerrilla Tactics jump over enemy + let khansMove = false; + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'guerrilla_tactics') { + let enemyNeighbors = t.neighbours.filter(n => { + let nc = this.countries.find(x => x.name === n); + return nc && nc.owner !== this.player.name && !nc.isCrater; + }); + for (let en of enemyNeighbors) { + let enemyCountry = this.countries.find(c => c.name === en); + if (enemyCountry && enemyCountry.neighbours.some(enn => { + let dest = this.countries.find(x => x.name === enn); + return dest && dest.owner === this.player.name && dest.name !== t.name; + })) { + khansMove = true; + break; + } + } + } + + // If ANY of these move types are legal, the player is allowed to enter the Maneuver phase! + if (standardMove || enclaveMove || khansMove) { + canManeuver = true; + break; + } } } + if (this.prevTarget) this.prevTarget.classList.remove('flash'); this.prevCountry = null; this.prevTarget = null; @@ -6954,16 +7091,18 @@ // --- PERK LOGIC: New California Republic ('logistical_superiority') --- if (this.perksEnabled && player.perk && player.perk.id === 'logistical_superiority') { - let continentsOwned = 0; + let extraContinentBonus = 0; continents.forEach(continent => { let ownsContinent = continent.areas.every(area => player.areas.includes(area) && !this.countries.find(c => c.name === area).isCrater); if (ownsContinent) { - continentsOwned++; + // NCR gets an extra 50% troops from the continent's base value, rounded up. + extraContinentBonus += Math.ceil(continent.bonus * 0.5); } }); - player.bonus += continentsOwned; // Add +1 troop for each continent controlled. + player.bonus += extraContinentBonus; } + // Minimum reinforcement rule if (player.bonus < 3) { player.bonus = 3; } @@ -7230,10 +7369,22 @@ return; } - // 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) { - if (this.prevCountry.neighbours.includes(country.name) && this.prevCountry.army > 1) { - // --- TACTICAL DECISION (AMBUSH OR ASSAULT) --- + // 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) { + + // --- 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"); + this.prevCountry = null; + if (this.prevTarget) this.prevTarget.classList.remove('flash'); + this.prevTarget = null; + return; + } + // --- END OF NEW LOGIC --- + + if (this.prevCountry.neighbours.includes(country.name) && this.prevCountry.army > 1) { + // --- TACTICAL DECISION (AMBUSH OR ASSAULT) --- + let strandedCmdr = this.players.find(p => p !== this.player && p.alive && !p.isNeutral && p.commander && p.commander.hp > 0 && p.commander.loc === country.name && p.name !== country.owner); let attackChoice = "assault"; if (this.commandersEnabled && strandedCmdr) { @@ -7367,7 +7518,18 @@ return; } + // --- NEW: Prevent maneuvering INTO a locked-down territory --- + if (country.isLockedDown) { + if (this.showToast) this.showToast("Maneuver blocked: Destination territory is under Elder's Edict lockdown.", "red"); + this.prevCountry = null; + if (this.prevTarget) this.prevTarget.classList.remove('flash'); + this.prevTarget = null; + return; + } + // --- END OF NEW LOGIC --- + // RAILROAD PERK LOGIC: "Rapid Relocation" + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'rapid_relocation') { if (country.owner !== this.player.name) { if (this.showToast) this.showToast("Invalid Destination: Must target a friendly territory.", "red"); @@ -7406,10 +7568,18 @@ return; } - // STANDARD & GREAT KHANS PLAYER LOGIC (Non-Railroad) + // STANDARD, GREAT KHANS, & ENCLAVE PLAYER LOGIC (Non-Railroad) if (country.owner === this.player.name) { let isDirectNeighbor = this.prevCountry.neighbours.includes(country.name); + + // --- NEW: The Enclave "Vertibird Assault" --- + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'vertibird_assault') { + isDirectNeighbor = true; // Vertibirds bypass the adjacency rule! + } + // --- END ENCLAVE LOGIC --- + let validEnemyPath = null; + // --- NEW: Great Khans "Guerrilla Tactics" Routing --- if (!isDirectNeighbor && this.perksEnabled && this.player.perk && this.player.perk.id === 'guerrilla_tactics') { @@ -7893,30 +8063,62 @@ } if (this.wastelandEconomyActive) { // --- FINAL ECONOMY & RECRUITMENT LOGIC --- - // 1. Calculate Income - let income = this.player.areas.length * 2; + let baseIncome = this.player.areas.length * 2; let continentIncome = 0; + let continentsOwned = 0; + let ncrBonusTroops = 0; // Track NCR free troops + continents.forEach(continent => { let ownsContinent = continent.areas.every(area => this.player.areas.includes(area) && !this.countries.find(c => c.name === area).isCrater); - if (ownsContinent) { continentIncome += 5; } + if (ownsContinent) { + continentIncome += 5; + continentsOwned++; + + // Calculate NCR troops immediately if they own the continent + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'logistical_superiority') { + ncrBonusTroops += Math.ceil(continent.bonus * 0.5); + } + } }); - income += continentIncome; - this.player.caps += income; - if (income > 0) { - await this.logAction(`TAXES COLLECTED: Gained ${income} Caps from territories and continent bonuses.`, true); + + let totalIncome = baseIncome + continentIncome; + let nukaBonus = 0; + + // Calculate Nuka-World Caps + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'tribute_chest') { + nukaBonus = continentsOwned; + totalIncome += nukaBonus; + } + + // Apply Caps + this.player.caps += totalIncome; + + // --- NEW: EXPLICIT BEFORE/AFTER LOGGING --- + if (totalIncome > 0) { + if (nukaBonus > 0) { + await this.logAction(`TAXES COLLECTED: Base income was ${baseIncome + continentIncome} Caps. Tribute Chest perk added +${nukaBonus} Caps. Total: ${totalIncome} Caps.`, true); + } else { + await this.logAction(`TAXES COLLECTED: Gained ${totalIncome} Caps from territories and continent bonuses.`, true); + } + } + + // Apply and log NCR Troops + if (ncrBonusTroops > 0) { + this.player.reserve += ncrBonusTroops; + await this.logAction(`LOGISTICAL SUPERIORITY: NCR drafted +${ncrBonusTroops} reinforcements from controlled continents!`, true); } // 2. Set the Correct Stage for the Player's Turn if (this.player.reserve > 0) { - this.stage = "Fortify"; // Must deploy troops + this.stage = "Fortify"; // Must deploy troops (NCR will trigger this if they got free troops!) } else { this.stage = "Recruitment"; // Can recruit or skip } - // --- END OF FINAL LOGIC --- - } else { -//... + } else { + +//... this.stage = "Fortify"; let bonus = this.unitBonus(this.player, 0); this.player.reserve += bonus; @@ -7925,6 +8127,7 @@ this.updateButtonText(); + if (this.perksEnabled && this.player.perk && this.player.perk.id === 'tribute_chest') { let continentsOwned = 0; continents.forEach(continent => { @@ -8048,52 +8251,23 @@ this.players[i].army += troopsToBuy; if (!turbo) await this.logAction(`${this.players[i].name} recruited ${troopsToBuy} new troops.`); } - } else { // Original AI card trade-in and reinforcement logic - if (this.wastelandEconomyActive) { - // New AI Economy Logic - let income = this.players[i].areas.length * 2; - let continentIncome = 0; - continents.forEach(continent => { - let ownsContinent = continent.areas.every(area => this.players[i].areas.includes(area) && !this.countries.find(c => c.name === area).isCrater); - if (ownsContinent) { continentIncome += 5; } + this.players[i].reserve = this.unitBonus(this.players[i], i); + let troopsToPlace = this.players[i].reserve; + let tradeIndices = this.getBestTrade(this.players[i].cards); + + if (tradeIndices) { + let bonus = getTradeBonus(); + tradeIndices.sort((a, b) => b - a).forEach(index => { + deck.unshift(this.players[i].cards[index]); + this.players[i].cards.splice(index, 1); }); - income += continentIncome; - this.players[i].caps += income; - if (!turbo) await this.logAction(`${this.players[i].name} collected ${income} Caps in taxes.`); - - const troopCost = 5; - const spendingPercentage = (Math.random() * 0.5) + 0.5; // AI will spend 50-100% of their caps - const affordableTroops = Math.floor(this.players[i].caps / troopCost); - const troopsToBuy = Math.floor(affordableTroops * spendingPercentage); - const totalCost = troopsToBuy * troopCost; - - if (troopsToBuy > 0) { - this.players[i].caps -= totalCost; - this.players[i].reserve += troopsToBuy; - this.players[i].army += troopsToBuy; - if (!turbo) await this.logAction(`${this.players[i].name} recruited ${troopsToBuy} new troops.`); - } - - } else { - // Original AI card trade-in and reinforcement logic - this.players[i].reserve = this.unitBonus(this.players[i], i); - let troopsToPlace = this.players[i].reserve; - let tradeIndices = this.getBestTrade(this.players[i].cards); - if (tradeIndices) { - let bonus = getTradeBonus(); - tradeIndices.sort((a, b) => b - a).forEach(index => { - deck.unshift(this.players[i].cards[index]); - this.players[i].cards.splice(index, 1); - }); - this.players[i].reserve += bonus; - troopsToPlace += bonus; - } - this.players[i].army += this.players[i].reserve; - if (troopsToPlace > 0 && !turbo) await this.logAction(`${this.players[i].name} deployed ${troopsToPlace} troops to their sectors.`); + this.players[i].reserve += bonus; + troopsToPlace += bonus; } - + this.players[i].army += this.players[i].reserve; + if (troopsToPlace > 0 && !turbo) await this.logAction(`${this.players[i].name} deployed ${troopsToPlace} troops to their sectors.`); } let areaToFortify = ["", 0]; @@ -8166,18 +8340,21 @@ Gamestate.aiAttack = async function (country, i, turbo) { + // --- NEW: Prevent AI from attacking FROM a locked territory --- + if (country.isLockedDown) return; + // --- CORRECTED AI TARGETING LOGIC --- // The AI can only see and target territories directly adjacent to the attacking country. let possibleTargets = []; country.neighbours.forEach(neighbourName => { let opponent = this.countries.find(c => c.name === neighbourName); - - // The AI will consider attacking if the neighbor is an enemy and not a crater. - if (opponent && opponent.owner !== this.players[i].name && !opponent.isCrater) { + // --- FIX: The AI will consider attacking if the neighbor is an enemy, not a crater, AND NOT LOCKED DOWN. + if (opponent && opponent.owner !== this.players[i].name && !opponent.isCrater && !opponent.isLockedDown) { possibleTargets.push(opponent); } }); + // --- CORRECTED ALLY FILTERING LOGIC --- // This will now correctly prevent the AI from attacking allies. possibleTargets = possibleTargets.filter(poss => { @@ -8643,27 +8820,37 @@ player.strangerCooldown = Math.floor(Math.random() * 4) + 1; } } + // --- NEW: The Minutemen (Flare Gun Rescue) --- if (opp.perk?.id === 'another_settlement') { - const reinforcementTerritory = opp.areas - .map(areaName => this.countries.find(c => c.name === areaName)) - .find(c => c && c.army > 1 && c.neighbours.includes(opponent.name)); - if (reinforcementTerritory) { - const troopsToMove = Math.min(3, reinforcementTerritory.army - 1); - reinforcementTerritory.army -= troopsToMove; - opponent.army += troopsToMove; - await this.logAction(`"Another settlement needs our help!" The Minutemen reinforced ${formatTerritoryName(opponent.name)} with ${troopsToMove} troops!`); + // "Deemed to lose" check: Is the attacker bringing more troops than the defender has? + if (country.army > opponent.army) { + // RNG Check: 40% chance help arrives in time + if (Math.random() < 0.40) { + // Spawn 2 to 4 free troops out of thin air + const spawnedTroops = Math.floor(Math.random() * 3) + 2; + opponent.army += spawnedTroops; + + // Temporary +15% defense boost (lowers attacker's win chance) + attackerWinChance -= 0.15; + if (attackerWinChance < 0.11) attackerWinChance = 0.11; // Hard minimum failsafe + + // Instantly update the map visual so the player sees the reinforcements + if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; + + await this.logAction(`"Another settlement needs our help!" Flare fired! ${spawnedTroops} Minutemen Militia arrived just in time, granting a +15% defense bonus!`, true); + } } } + // --- END MINUTEMEN LOGIC --- + } const originalDefenderArmy = opponent.army; const originalAttackerArmy = country.army; while (opponent.army > 0 && country.army > 1) { let attackerRoll = Math.random(); - if (this.perksEnabled && player.perk?.id === 'psycho_rush' && this.commandersEnabled && player.commander?.loc === country.name) { - attackerRoll += 0.1; - } let attackerWins = (attackerRoll > attackerWinChance); if (attackerWins) { + opponent.army -= 1; } else { country.army -= 1; @@ -8671,22 +8858,29 @@ } if (country.army <= 1 && opponent.army > 0) { const attackerLosses = originalAttackerArmy - country.army; + const defenderLosses = originalDefenderArmy - opponent.army; // --- NEW: Track defender losses // --- NEW: Record Failure for Mr. House --- if (this.perksEnabled && player.perk && player.perk.id === 'the_house_always_wins') { // We store the data of the failed attack so the button can undo it later player.lastFailedAttack = { sourceTerritory: country.name, + targetTerritory: opponent.name, // --- NEW: Track target territory losses: attackerLosses, - turnNumber: this.turn // Ensure they can only undo battles from THIS turn + defenderLosses: defenderLosses, // --- NEW: Track defender losses + turnNumber: this.turn }; // AI Logic: Mr. House AI will instantly auto-undo if he loses 3 or more troops and it's off cooldown if (!player.isPlayer && (player.predictiveCooldown || 0) === 0 && attackerLosses >= 3) { - country.army += attackerLosses; // Restore troops + country.army += attackerLosses; + opponent.army += defenderLosses; // --- NEW: Restore defenders for AI player.predictiveCooldown = 3; - await this.logAction(`[ PREDICTIVE SIMULATION ] ${player.name} foresaw unacceptable losses. The attack was aborted and ${attackerLosses} Securitrons were preserved!`, true); + + await this.logAction(`[ PREDICTIVE SIMULATION ] ${player.name} foresaw unacceptable losses. The attack was aborted! ${attackerLosses} attackers and ${defenderLosses} defenders restored.`, true); + if (attacker && attacker.nextElementSibling) attacker.nextElementSibling.textContent = country.army; + if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; this.updateInfo(); return; // Bypass normal defeat logic } @@ -8698,16 +8892,38 @@ this.updateInfo(); await this.logAction(`REPULSED: ${player.name} assaulted ${originalOwner} in ${formatTerritoryName(opponent.name)} but failed.`); - if (this.perksEnabled && opp && opp.perk?.id === 'fev_infection') { - const converted = Math.floor(attackerLosses * 0.25); - if (converted > 0) { - opponent.army += converted; - if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; - await this.logAction(`MUTATION!: F.E.V. converted ${converted} defeated attackers into Super Mutants!`); + if (this.perksEnabled) { + if (opp && opp.perk?.id === 'fev_infection') { + const converted = Math.floor(attackerLosses * 0.25); + if (converted > 0) { + opponent.army += converted; + if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; + await this.logAction(`MUTATION!: F.E.V. converted ${converted} defeated attackers into Super Mutants!`); + } } + + // --- NEW: Universal Synth Recovery (Failed Attack) --- + if (player.perk?.id === 'synth_replacements' && attackerLosses > 0) { + let synths = 0; + for (let k = 0; k < attackerLosses; k++) { if (Math.random() < 0.10) synths++; } + if (synths > 0) { + player.reserve += synths; + await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed attacking Synths to reserves!`); + } + } + if (opp.perk?.id === 'synth_replacements' && defenderLosses > 0) { + let synths = 0; + for (let k = 0; k < defenderLosses; k++) { if (Math.random() < 0.10) synths++; } + if (synths > 0) { + opp.reserve += synths; + await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed defending Synths to reserves!`); + } + } + // --- END SYNTH LOGIC --- } return; } + if (opponent.army <= 0) { isVictory = true; player.conqueredThisTurn = true; @@ -8728,30 +8944,80 @@ opponent.color = player.color; opponent.siloTurns = 0; player.areas.push(opponent.name); - let maxMove = country.army - 1; + // --- NEW: Calculate Max Movement BEFORE the modal --- + let maxMove = country.army - 1; let minMove = 1; + + if (this.perksEnabled && player.perk && player.perk.id === 'scourge_of_the_east') { + maxMove = country.army; // Caesar's Legion can move 100% of their troops! + } + let movedTroops = minMove; if (player === this.player && maxMove > minMove) { + // The modal will now correctly reflect the higher maximum movedTroops = await this.showMoveModal(minMove, maxMove, formatTerritoryName(opponent.name)); } else if (player !== this.player) { - movedTroops = maxMove; + movedTroops = maxMove; // AI always moves the maximum allowed } + opponent.army = movedTroops; country.army -= movedTroops; if (defender) defender.style.fill = opponent.color; + if (this.perksEnabled && player.perk) { if (player.perk.id === 'fev_infection') { - const defeatedTroops = country.army + opponent.army; - const converted = Math.floor(defeatedTroops * 0.25); - if (converted > 0) { - opponent.army += converted; - await this.logAction(`MUTATION!: F.E.V. converted ${converted} defeated combatants into Super Mutants!`); + // (Bonus Fix: Made the victory FEV math match the repulse FEV math!) + const converted = Math.max(1, Math.floor(originalDefenderArmy * 0.25)); + opponent.army += converted; + // Force visual update + if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; + await this.logAction(`MUTATION!: F.E.V. converted ${converted} defeated defenders into Super Mutants!`); + } + // --- NEW: The Fiends (Chem-Fueled Raids) --- + else if (player.perk.id === 'psycho_rush') { + // 30% chance to trigger a raid on conquest + if (Math.random() < 0.30) { + let gotLoot = false; + let lootRoll = Math.random(); + + // 60% chance to try stealing Caps / Cards + if (lootRoll < 0.60) { + if (this.wastelandEconomyActive && opp.caps > 0) { + let stolenCaps = Math.min(opp.caps, Math.floor(Math.random() * 4) + 2); // 2 to 5 caps + opp.caps -= stolenCaps; + player.caps += stolenCaps; + gotLoot = true; + await this.logAction(`[ RAID ] The Fiends ransacked the survivors and stole ${stolenCaps} Caps!`, true); + } else if (!this.wastelandEconomyActive && opp.cards && opp.cards.length > 0) { + let stolenCard = opp.cards.pop(); + player.cards.push(stolenCard); + gotLoot = true; + await this.logAction(`[ RAID ] The Fiends ransacked the survivors and stole a Bottle Cap card!`, true); + } + } + // 40% chance to try stealing a Stimpak + else { + if (this.commandersEnabled && opp.commander && opp.commander.stimpaks > 0 && player.commander) { + opp.commander.stimpaks--; + player.commander.stimpaks = Math.min(3, player.commander.stimpaks + 1); + gotLoot = true; + await this.logAction(`[ RAID ] The Fiends mugged the enemy medics and stole a Stimpak!`, true); + } + } + + // FAILSAFE: If the enemy was broke or didn't have the item, take slaves! + if (!gotLoot) { + let enslaved = Math.floor(Math.random() * 2) + 1; // 1 or 2 troops + opponent.army += enslaved; + if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; + await this.logAction(`[ ENSLAVED ] Finding no loot, The Fiends forced ${enslaved} captives to join their army!`, true); + } } - } else if (player.perk.id === 'scourge_of_the_east') { - movedTroops = country.army; - country.army = 0; } + // --- END FIENDS LOGIC --- } + + if (this.activeNuke && this.activeNuke.launcher === originalOwner) { let stillHasSilo = this.countries.some(c => c.isSilo && c.owner === originalOwner); if (!stillHasSilo) { @@ -8812,26 +9078,33 @@ } }); this.updateInfo(); - if (this.perksEnabled && opp.perk?.id === 'synth_replacements') { - // If the territory was conquered, all original defenders were lost. - // Otherwise, subtract the remaining defenders from the original amount. - const lossesIncurred = isVictory ? originalDefenderArmy : (originalDefenderArmy - opponent.army); - let synthsReplaced = 0; - for (let k = 0; k < lossesIncurred; k++) { - // 30% chance to recover a lost troop - if (Math.random() < 0.30) { - synthsReplaced++; + // --- NEW: Universal Synth Recovery (Successful Attack) --- + if (this.perksEnabled && isVictory) { + const attackerLosses = originalAttackerArmy - country.army; + const defenderLosses = originalDefenderArmy; + + if (player.perk?.id === 'synth_replacements' && attackerLosses > 0) { + let synths = 0; + for (let k = 0; k < attackerLosses; k++) { if (Math.random() < 0.10) synths++; } + if (synths > 0) { + player.reserve += synths; + await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed attacking Synths to reserves!`); } } - - if (synthsReplaced > 0) { - // Send recovered synths to the Institute's reserves for their next turn - opp.reserve += synthsReplaced; - await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synthsReplaced} destroyed Synths to their reserves!`); + if (opp.perk?.id === 'synth_replacements' && defenderLosses > 0) { + let synths = 0; + for (let k = 0; k < defenderLosses; k++) { if (Math.random() < 0.10) synths++; } + if (synths > 0) { + opp.reserve += synths; + await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed defending Synths to reserves!`); + } } } + // --- END SYNTH LOGIC --- + if (isVictory) { + await this.logAction(`VICTORY: ${player.name} wiped out ${originalOwner} in ${formatTerritoryName(opponent.name)}${flavor}`, true); if (originalOwner !== "none" && originalOwner !== "Wasteland Horrors") { if (this.diplomacy.reputation[originalOwner] && this.diplomacy.reputation[originalOwner][player.name] !== undefined) {