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) {