End Cycle
+
+
+
+
+
STAT
INV
@@ -1611,7 +1682,8 @@ button:active {
+1 Launch Code
Trigger Radstorm
- ADD RANDOM BOBBLEHEAD
+ ADD RANDOM BOBBLEHEAD
+ Force Creature Encounter
@@ -1809,6 +1881,201 @@ button:active {
return true; // Prevents the browser's default error handling
};
+const FACTIONS = {
+ // --- Fallout 3 Factions ---
+ "Brotherhood of Steel": {
+ leader: "Elder Lyons",
+ color: "#3a8dcb",
+ perk: {
+ id: "power_armor_infantry",
+ name: "Power Armor Infantry",
+ description: "Grants a permanent +5% bonus to your win chance in all combat, both when attacking and defending."
+ },
+ affinity: { "The Enclave": -30, "Vault 87 Mutants": -15, "Wasteland Raiders": -10, "BOS Outcasts": -15 }
+ },
+
+ "The Enclave": {
+ leader: "President Eden",
+ color: "#b74545",
+ perk: {
+ id: "vertibird_assault",
+ name: "Vertibird Assault",
+ description: "During the Maneuver phase, you can move troops between any two territories you own, regardless of whether they are connected."
+ },
+ affinity: { "Brotherhood of Steel": -30, "BOS Outcasts": -30 }
+ },
+ "Vault 87 Mutants": {
+ leader: "Overlord",
+ color: "#d19a4f",
+ perk: {
+ id: "fev_infection",
+ name: "F.E.V. Infection",
+ description: "When you conquer a territory, 25% of the defeated enemy army (rounded down) is immediately converted into your own troops."
+ },
+ affinity: { "Brotherhood of Steel": -15, "The Enclave": -15 }
+ },
+ "Wasteland Raiders": {
+ leader: "Flak",
+ color: "#8e6aa5",
+ perk: {
+ id: "chem_frenzy",
+ name: "Chem Frenzy",
+ description: "Once per turn, before attacking, you can sacrifice 1 troop to gain a +10% win chance for that single battle."
+ },
+ affinity: { "Brotherhood of Steel": -10, "New California Republic": -10, "The Minutemen": -10, "Reilly's Rangers": -10 }
+ },
+ "BOS Outcasts": {
+ leader: "Protector Casdin",
+ color: "#5a8b5c",
+ perk: {
+ id: "tech_hoarders",
+ name: "Tech Hoarders",
+ description: "Trading in Bottle Cap cards always yields +2 additional troops on top of the standard trade value."
+ },
+ affinity: { "Brotherhood of Steel": -15, "The Enclave": -30 }
+ },
+ "Reilly's Rangers": {
+ leader: "Reilly",
+ color: "#bdb862",
+ perk: {
+ id: "geomapper_module",
+ name: "Geomapper Module",
+ description: "Once per turn, you can spend 1 Bottle Cap to reveal the exact troop count of any single territory on the map."
+ },
+ affinity: { "Wasteland Raiders": -10 }
+ },
+
+ // --- Fallout: New Vegas Factions ---
+ "New California Republic": {
+ leader: "General Oliver",
+ color: "#c9a46a", // Assigned a representative color
+ perk: {
+ id: "logistical_superiority",
+ name: "Logistical Superiority",
+ description: "Receive +1 bonus troop during deployment for every Continent you fully control."
+ },
+ affinity: { "Caesar's Legion": -30, "Great Khans": -15, "Mojave Brotherhood": -5 }
+ },
+ "Caesar's Legion": {
+ leader: "Caesar",
+ color: "#e04f4f", // Assigned a representative color
+ perk: {
+ id: "scourge_of_the_east",
+ name: "Scourge of the East",
+ description: "You are exempt from the rule requiring you to leave at least one troop behind after conquering a territory."
+ },
+ affinity: { "New California Republic": -30 }
+ },
+ "New Vegas Securitrons": {
+ leader: "Mr. House",
+ color: "#7aabac", // Assigned a representative color
+ perk: {
+ id: "the_house_always_wins",
+ name: "The House Always Wins",
+ description: "Gain +1 Bottle Cap for every 3 Bottle Cap cards an opponent turns in."
+ },
+ affinity: { "New California Republic": 5, "Caesar's Legion": 5 }
+ },
+ "Mojave Brotherhood": {
+ leader: "Elder McNamara",
+ color: "#556b2f", // Assigned a representative color
+ 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."
+ },
+ affinity: { "New California Republic": -5 }
+ },
+ "Great Khans": {
+ leader: "Papa Khan",
+ color: "#6b4a3a", // Assigned a representative color
+ perk: {
+ id: "guerrilla_tactics",
+ name: "Guerrilla Tactics",
+ description: "During Maneuver, your troops can pass through exactly one enemy territory, inflicting 1 casualty on that territory as they pass."
+ },
+ affinity: { "New California Republic": -15 }
+ },
+ "The Fiends": {
+ leader: "Motor-Runner",
+ color: "#a0522d", // Assigned a representative color
+ 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."
+ },
+ affinity: { "New California Republic": -10, "Caesar's Legion": -10 }
+ },
+
+ // --- Fallout 4 Factions ---
+ "The Minutemen": {
+ leader: "Preston Garvey",
+ color: "#0077b6", // Assigned a representative color
+ 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."
+ },
+ affinity: { "The Railroad": 15, "The Institute": -10, "The Gunners": -10 }
+ },
+ "The Institute": {
+ leader: "Father",
+ color: "#e0e0e0", // Assigned a representative color
+ perk: {
+ id: "synth_replacements",
+ name: "Synth Replacements",
+ description: "When you lose a battle while defending, each lost defender has a 50% chance to be a 'Synth' and is not removed."
+ },
+ affinity: { "Brotherhood of Steel": -20, "The Railroad": -20 }
+ },
+ "The Railroad": {
+ leader: "Desdemona",
+ color: "#888888", // Assigned a representative color
+ perk: {
+ id: "rapid_relocation",
+ name: "Rapid Relocation",
+ description: "Receive 5 maneuver points at the start of the Maneuver phase, allowing for up to five separate troop movements."
+ },
+ affinity: { "The Institute": -20, "Brotherhood of Steel": -10, "The Minutemen": 15 }
+ },
+ "The Gunners": {
+ leader: "Captain Wes",
+ color: "#4b5320", // Assigned a representative color
+ perk: {
+ id: "mercenary_contracts",
+ name: "Mercenary Contracts",
+ description: "You can spend 2 Bottle Caps at any time during your turn to instantly spawn 3 troops on your Commander's location."
+ },
+ affinity: { "The Minutemen": -10 }
+ },
+ "Nuka-World Raiders": {
+ leader: "Colter",
+ color: "#d22b2b", // Assigned a representative color
+ perk: {
+ id: "tribute_chest",
+ name: "Tribute Chest",
+ description: "At the start of your turn, you gain +1 Bottle Cap for every continent you fully control."
+ },
+ affinity: {}
+ },
+ // Note: I had to merge "Brotherhood of Steel" (FO4) with the FO3 version.
+ // If you want Maxson's BoS to be a separate faction from Lyons', we will need to add a new entry.
+ // For now, I've kept them as one BoS with the "Technology Salvage" perk. Let me know if you want to change this.
+};
+
+// This is the data for any custom faction a player creates.
+const CUSTOM_FACTION = {
+ leader: "Mysterious Stranger", // Placeholder
+ color: "#cccccc", // Default color
+ perk: {
+ id: "mysterious_stranger",
+ name: "Mysterious Stranger",
+ description: "Triggers on a losing battle roll after a 1-4 round cooldown. Negates opponent's highest die. If your roll had no successes, you automatically re-roll your dice."
+ },
+ affinity: {} // Starts neutral with everyone
+};
+
+
// --- DYNAMIC CURSOR FUNCTION ---
function updateDynamicCursor(hexColor) {
const encodedColor = hexColor.replace('#', '%23');
@@ -1828,9 +2095,61 @@ function updateDynamicCursor(hexColor) {
}
-/*===============================
- ROBCO RISK
-===============================*/
+document.addEventListener('DOMContentLoaded', () => {
+ const factionInput = document.getElementById('chosen-country');
+ const tooltip = document.getElementById('vats-tooltip'); // WE ARE USING YOUR EXISTING TOOLTIP ELEMENT
+
+ if (!factionInput || !tooltip) {
+ console.error("Tooltip or Faction Input not found!");
+ return;
+ }
+
+ // This function will show and position the tooltip
+ const showPerkTooltip = (e) => {
+ const factionName = factionInput.value;
+ let factionData;
+
+ // Determine which faction data to show (canon or custom)
+ if (FACTIONS && FACTIONS[factionName]) {
+ factionData = FACTIONS[factionName];
+ } else if (CUSTOM_FACTION) {
+ factionData = CUSTOM_FACTION;
+ }
+
+ // If we have valid data, build and show the tooltip
+ if (factionData && factionData.perk) {
+ tooltip.innerHTML = `
${factionData.perk.name}: ${factionData.perk.description}`;
+ tooltip.style.display = 'block';
+ tooltip.style.left = (e.pageX + 15) + 'px';
+ tooltip.style.top = (e.pageY + 15) + 'px';
+ }
+ };
+
+ // This function will hide the tooltip
+ const hidePerkTooltip = () => {
+ tooltip.style.display = 'none';
+ };
+
+ // This function will move the tooltip with the mouse
+ const movePerkTooltip = (e) => {
+ if (tooltip.style.display === 'block') {
+ tooltip.style.left = (e.pageX + 15) + 'px';
+ tooltip.style.top = (e.pageY + 15) + 'px';
+ }
+ };
+
+ // --- Assign events to the faction input box ---
+ // When the mouse enters the input box...
+ factionInput.addEventListener('mouseenter', showPerkTooltip);
+ // When the mouse leaves the input box...
+ factionInput.addEventListener('mouseleave', hidePerkTooltip);
+ // As the mouse moves over the input box...
+ factionInput.addEventListener('mousemove', movePerkTooltip);
+ // When the text inside changes (e.g. user types or selects from dropdown)...
+ factionInput.addEventListener('input', showPerkTooltip);
+
+});
+
if (!document.getElementById('radstorm-styles')) {
let style = document.createElement('style');
@@ -1991,35 +2310,50 @@ const combatFlavors = [
"walking in the front door because the guards were asleep on Mentats."
];
-const basePlayers = [
- { name: "Elder Lyons", country: "Brotherhood of Steel", color: "#3a8dcb", army: 10, reserve: 10, areas: [], bonus: 2, alive: true, cards: [], conqueredThisTurn: false, isNeutral: false, codes: 0 },
- { name: "President Eden", country: "Enclave", color: "#b74545", army: 20, reserve: 20, areas: [], bonus: 2, alive: true, cards: [], conqueredThisTurn: false, isNeutral: false, codes: 0 },
- { name: "Overlord", country: "Super Mutants", color: "#d19a4f", army: 20, reserve: 20, areas: [], bonus: 2, alive: true, cards: [], conqueredThisTurn: false, isNeutral: false, codes: 0 },
- { name: "Boss", country: "Raiders", color: "#8e6aa5", army: 20, reserve: 20, areas: [], bonus: 2, alive: true, cards: [], conqueredThisTurn: false, isNeutral: false, codes: 0 },
- { name: "Protector Casdin", country: "Outcasts", color: "#5a8b5c", army: 20, reserve: 20, areas: [], bonus: 2, alive: true, cards: [], conqueredThisTurn: false, isNeutral: false, codes: 0 },
- { name: "Sonora Cruz", country: "Regulators", color: "#bdb862", army: 20, reserve: 20, areas: [], bonus: 2, alive: true, cards: [], conqueredThisTurn: false, isNeutral: false, codes: 0 }
-];
-
-
-const themeFactions = {
- "fo3": [
- { name: "Elder Lyons", country: "Brotherhood of Steel" }, { name: "President Eden", country: "The Enclave" },
- { name: "Overlord", country: "Vault 87 Mutants" }, { name: "Flak", country: "Wasteland Raiders" },
- { name: "Protector Casdin", country: "BOS Outcasts" }, { name: "Reilly", country: "Reilly's Rangers" }
+const encounterData = {
+ creatures: [
+ { name: "Savage Dog", threat: 0.1 }, { name: "Mole Rat", threat: 0.3 }, { name: "Bloatfly", threat: 0.4 },
+ { name: "Radroach", threat: 0.5 }, { name: "Yao Guai", threat: 1.0 }, { name: "Feral Ghoul", threat: 1.2 },
+ { name: "Giant Ant", threat: 1.5 }, { name: "Scavenger's Dog", threat: 1.8 }, { name: "Giant Worker Ant", threat: 2.0 },
+ { name: "Radscorpion", threat: 2.5 }, { name: "Giant Soldier Ant", threat: 3.0 }, { name: "Fire Ant Soldier", threat: 3.5 },
+ { name: "Guard Dog", threat: 4.0 }, { name: "Fire Ant Warrior", threat: 4.5 }, { name: "Mirelurk", threat: 5.0 },
+ { name: "Feral Ghoul Roamer", threat: 6.0 }, { name: "Giant Radscorpion", threat: 7.0 }, { name: "Mirelurk Hunter", threat: 8.0 },
+ { name: "Vicious Dog", threat: 9.0 }, { name: "Centaur", threat: 10.0 }, { name: "Feral Ghoul Reaver", threat: 12.0 },
+ { name: "Deathclaw", threat: 15.0 }, { name: "Enclave Deathclaw", threat: 20.0 }, { name: "Super Mutant", threat: 25.0 },
+ { name: "Super Mutant Master", threat: 30.0 }, { name: "Super Mutant Overlord", threat: 40.0 }, { name: "Super Mutant Behemoth", threat: 50.0 }
],
- "fnv": [
- { name: "General Oliver", country: "New California Republic" }, { name: "Caesar", country: "Caesar's Legion" },
- { name: "Mr. House", country: "New Vegas Securitrons" }, { name: "Elder McNamara", country: "Mojave Brotherhood" },
- { name: "Papa Khan", country: "Great Khans" }, { name: "Motor-Runner", country: "The Fiends" }
+ genericLocations: ["a crashed Vertibird", "a ruined highway overpass", "an abandoned Red Rocket station", "a Pre-War police station", "a collapsed metro tunnel", "a flooded sewer system", "a ruined church", "an abandoned drive-in theater", "a Broadcasting Tower", "an Abandoned Shack"],
+ subLocations: ["the manager's office", "the pharmacy counter", "the main server room", "the evidence locker", "the cockpit", "the projection booth", "the sacristy", "the underground pump station", "a hidden Refrigerator", "a locked Safe"],
+ people: ["a Wasteland Doctor", "a scared Settler", "a shifty-eyed Mercenary", "a Ghoul scavenger", "a Brotherhood of Steel scribe", "a Fugitive Slave", "a Wounded Sheriff", "a Merchant", "Talon Company Mercs", "a Drunken Drifter", "a Lost Farmer", "a Mister Handy"],
+ containers: ["a weapons locker", "a footlocker", "a first aid box", "a locked safe", "a steamer trunk", "a doctor's bag", "an ammo box"],
+ namedLocations: [
+ { name: "Super Duper Mart", subLocations: ["the pharmacy counter", "the manager's office", "a locked safe"] },
+ { name: "Slave Camp", people: ["Fugitive Slave", "shifty-eyed Mercenary"] },
+ { name: "Gas Station", subLocations: ["abandoned Red Rocket station", "a hidden Refrigerator"] },
+ { name: "Diner", people: ["a Mister Handy", "a Drunken Drifter"] },
+ { name: "C.I.T. Ruins", people: ["an escaped Synth"], subLocations: ["the main server room"] },
+ { name: "Water Treatment Plant", subLocations: ["the underground pump station", "a locked safe"] },
+ { name: "Hubris Comics Store", people: ["a Ghoul scavenger"] },
+ { name: "Gwinnett Brewery", people: ["a Mister Handy", "a Drunken Drifter"] },
+ { name: "Robot Factory", people: ["an escaped Synth"], subLocations: ["the main server room"] }
],
- "fo4": [
- { name: "Preston Garvey", country: "The Minutemen" }, { name: "Father", country: "The Institute" },
- { name: "Elder Maxson", country: "Brotherhood of Steel" }, { name: "Desdemona", country: "The Railroad" },
- { name: "Captain Wes", country: "The Gunners" }, { name: "Colter", country: "Nuka-World Raiders" }
+ vaults: [
+ { territory: "eastern_us", name: "Vault 101" }, { territory: "western_us", name: "Vault 13" },
+ { territory: "alberta", name: "Vault 15" }, { territory: "ontario", name: "Vault 112" },
+ { territory: "china", name: "Vault 118" }, { territory: "quebec", name: "Vault 81" },
+ { territory: "quebec", name: "Vault 111" }, { territory: "quebec", name: "Vault 114" },
+ { territory: "quebec", name: "Vault 95" }, { territory: "central_america", name: "Vault 3" },
+ { territory: "central_america", name: "Vault 21" }, { territory: "central_america", name: "Vault 34" },
+ { territory: "ural", name: "Vault 77" }
]
};
+
+// This new constant replaces both basePlayers and themeFactions.
+// It contains all faction data, including perks and affinities, in one place.
+
+
const cardTypes = ["Nuka-Cola Cap", "Sunset Sarsaparilla Cap", "Quantum Cap"];
let deck = [];
let tradeCount = 0;
@@ -2063,49 +2397,47 @@ document.body.appendChild(vatsTooltip);
map.addEventListener('mousemove', (e) => {
let targetId = e.target.id;
-
- if (!Gamestate.countries) return;
-
+ if (!Gamestate.countries) return;
let targetCountry = Gamestate.countries.find(c => c.name === targetId);
- if (!targetCountry) { vatsTooltip.style.display = "none"; return; }
-
+ if (!targetCountry) {
+ vatsTooltip.style.display = "none";
+ return;
+ }
let tooltipHTML = "";
-
- // --- FOG OF WAR TOOLTIP CHECK (NEW) ---
- // First, check if the territory is shrouded.
const isShrouded = e.target.classList.contains('fog-shroud');
-
if (isShrouded) {
- // If it's shrouded, only show "UNKNOWN".
tooltipHTML = "TERRITORY: UNKNOWN";
-
} else {
- // If it's VISIBLE, proceed with the normal detailed tooltip logic.
- let isBattleHover = (Gamestate.stage === "Battle" || Gamestate.stage === "Commander Phase") &&
- Gamestate.prevCountry && Gamestate.prevCountry.neighbours.includes(targetCountry.name) &&
- targetCountry.owner !== Gamestate.player.name && Gamestate.prevCountry.army > 1 &&
- !targetCountry.isCrater && !Gamestate.aiTurn;
-
+ let isBattleHover = (Gamestate.stage === "Battle" || Gamestate.stage === "Commander Phase") &&
+ Gamestate.prevCountry && Gamestate.prevCountry.neighbours.includes(targetCountry.name) &&
+ targetCountry.owner !== Gamestate.player.name && Gamestate.prevCountry.army > 1 &&
+ !targetCountry.isCrater && !Gamestate.aiTurn;
if (isBattleHover) {
- // --- This is your original, unchanged V.A.T.S. logic ---
+ // --- V.A.T.S. Calculation Logic ---
let baseChance = 0.50;
if (Gamestate.difficulty === "Easy") baseChance = 0.60;
if (Gamestate.difficulty === "Hard") baseChance = 0.40;
let owner = Gamestate.players.find(p => p.name === targetCountry.owner);
if (owner && owner.isNeutral) baseChance -= 0.15;
+
+ // --- THIS IS THE NEWLY ADDED PERK LOGIC FOR THE TOOLTIP ---
+ if (Gamestate.perksEnabled) {
+ if (Gamestate.player.perk && Gamestate.player.perk.id === 'power_armor_infantry') {
+ baseChance += 0.05;
+ }
+ if (owner && owner.perk && owner.perk.id === 'power_armor_infantry') {
+ baseChance -= 0.05;
+ }
+ }
+
if (Gamestate.nukesEnabled && targetCountry.isSilo) {
let buff = Math.min(0.80, targetCountry.siloTurns * 0.20);
- if(Gamestate.activeNuke && Gamestate.activeNuke.launcher === targetCountry.owner) buff = 0;
- if(targetCountry.owner === "Wasteland Horrors") buff = 0;
+ if (Gamestate.activeNuke && Gamestate.activeNuke.launcher === targetCountry.owner) buff = 0;
+ if (targetCountry.owner === "Wasteland Horrors") buff = 0;
baseChance = baseChance * (1 - buff);
}
if (Gamestate.commandersEnabled && owner && owner.commander && owner.commander.loc === targetCountry.name) {
- baseChance = baseChance * 0.80;
- }
- if(Gamestate.nukesEnabled && targetCountry.isSilo) {
- let cont = continents.find(c => c.name === targetCountry.continent);
- let ownsCont = cont.areas.every(area => owner && owner.areas.includes(area));
- if(ownsCont) baseChance = baseChance * 0.85;
+ baseChance = baseChance * 0.80;
}
if (baseChance < 0.11) baseChance = 0.11;
if (Gamestate.devWinOverride !== undefined && Gamestate.devWinOverride >= 0) {
@@ -2114,15 +2446,18 @@ map.addEventListener('mousemove', (e) => {
let a = Gamestate.prevCountry.army - 1;
let d = targetCountry.army;
let winProb = 0;
- if (baseChance === 0.5) { winProb = a / (a + d); }
- else {
- let q = 1 - baseChance; let ratio = q / baseChance; winProb = (1 - Math.pow(ratio, a)) / (1 - Math.pow(ratio, a + d));
+ if (baseChance === 0.5) {
+ winProb = a / (a + d);
+ } else {
+ let q = 1 - baseChance;
+ let ratio = q / baseChance;
+ winProb = (1 - Math.pow(ratio, a)) / (1 - Math.pow(ratio, a + d));
}
let chancePercent = Math.round(winProb * 100);
if (chancePercent > 95) chancePercent = 95;
if (chancePercent < 1) chancePercent = 1;
- if(Gamestate.nukesEnabled && targetCountry.isSilo && targetCountry.siloTurns >= 4 && (!Gamestate.activeNuke || Gamestate.activeNuke.launcher !== targetCountry.owner)) {
- if(chancePercent > 20) chancePercent = 20;
+ if (Gamestate.nukesEnabled && targetCountry.isSilo && targetCountry.siloTurns >= 4 && (!Gamestate.activeNuke || Gamestate.activeNuke.launcher !== targetCountry.owner)) {
+ if (chancePercent > 20) chancePercent = 20;
}
let diplomacyWarning = "";
if (Gamestate.areAllies(Gamestate.player.name, targetCountry.owner)) {
@@ -2137,9 +2472,11 @@ map.addEventListener('mousemove', (e) => {
DEFENDERS: ${targetCountry.army}
WIN CHANCE: ${chancePercent}${chancePercent !== "N/A" ? '%' : ''}${diplomacyWarning}${cmdrWarning}`;
} else {
- // --- This is your original, unchanged passive info hover logic ---
let infoLines = [];
let owner = Gamestate.players.find(p => p.name === targetCountry.owner);
+ if (targetCountry.isLockedDown) {
+ infoLines.push(`
TERRITORY LOCKED DOWN `);
+ }
infoLines.push(`TERRITORY: ${formatTerritoryName(targetCountry.name)}`);
if (targetCountry.isCrater) {
infoLines.push(`
IRRADIATED CRATER (IMPASSABLE) `);
@@ -2152,7 +2489,7 @@ map.addEventListener('mousemove', (e) => {
}
if (Gamestate.nukesEnabled && targetCountry.isSilo) {
let defBuff = targetCountry.siloTurns > 0 ? Math.min(80, targetCountry.siloTurns * 20) : 0;
- if (targetCountry.owner === "Wasteland Horrors") defBuff = 0;
+ if (targetCountry.owner === "Wasteland Horrors") defBuff = 0;
infoLines.push(`
COMMAND SILO (+${defBuff}% DEFENSE) `);
}
if (Gamestate.commandersEnabled) {
@@ -2167,14 +2504,13 @@ map.addEventListener('mousemove', (e) => {
tooltipHTML = infoLines.join('
');
}
}
-
- // This part remains the same, it just displays whatever HTML we built.
vatsTooltip.innerHTML = tooltipHTML;
vatsTooltip.style.left = (e.clientX + 20) + "px";
vatsTooltip.style.top = (e.clientY + 20) + "px";
vatsTooltip.style.display = "block";
});
+
map.addEventListener('mouseleave', () => { vatsTooltip.style.display = "none"; });
const modal = document.querySelector('#start-modal');
@@ -2208,40 +2544,150 @@ const themeSelector = document.getElementById('chosen-theme');
const leaderInput = document.getElementById('chosen-leader');
const factionInput = document.getElementById('chosen-country');
-if (themeSelector) {
- themeSelector.addEventListener('change', function(e) {
- let theme = e.target.value;
-
- // --- ADD THIS BLOCK to update the cursor color ---
- if (theme === 'fnv') {
- updateDynamicCursor('#ffb642'); // Amber
- } else if (theme === 'fo4') {
- updateDynamicCursor('#22ccff'); // Blue
- } else {
- updateDynamicCursor('#18ff62'); // Default Green
- }
- // --- END OF ADDED BLOCK ---
+// =================================================================
+// NEW CUSTOM DROPDOWN & TOOLTIP LOGIC
+// =================================================================
+document.addEventListener('DOMContentLoaded', () => {
+ const factionInput = document.getElementById('chosen-country-input');
+ const optionsContainer = document.getElementById('custom-faction-options');
+ const tooltip = document.getElementById('vats-tooltip');
- // 1. Swap CSS
- document.body.classList.remove('theme-fo3', 'theme-fnv', 'theme-fo4');
- if (theme !== 'fo3') document.body.classList.add('theme-' + theme);
-
- // 2. Auto-fill names and adjust input width
- if (theme === 'fo3') {
- if(leaderInput) { leaderInput.value = "Lone Wanderer"; }
- if(factionInput) { factionInput.value = "Brotherhood of Steel"; }
- } else if (theme === 'fnv') {
- if(leaderInput) { leaderInput.value = "Courier Six"; }
- if(factionInput) { factionInput.value = "New California Republic"; }
- } else if (theme === 'fo4') {
- if(leaderInput) { leaderInput.value = "Sole Survivor"; }
- if(factionInput) { factionInput.value = "The Minutemen"; }
+ const perksEnabledCheckbox = document.getElementById('opt-perks');
+
+ // Function to enable or disable the custom dropdown based on the checkbox
+ const toggleFactionInput = () => {
+ const perksAreOn = perksEnabledCheckbox.checked;
+ if (perksAreOn) {
+ factionInput.disabled = false;
+ factionInput.style.pointerEvents = 'auto';
+ factionInput.style.opacity = '1';
+ } else {
+ factionInput.disabled = true;
+ factionInput.style.pointerEvents = 'none';
+ factionInput.style.opacity = '0.5';
+ tooltip.style.display = 'none'; // Also hide the tooltip
+ }
+ };
+
+ // Add an event listener to the checkbox itself
+ perksEnabledCheckbox.addEventListener('change', toggleFactionInput);
+
+ // Call it once on load to set the initial state
+ toggleFactionInput();
+
+ if (!factionInput || !optionsContainer || !tooltip) return;
+
+ // --- Function to populate the options ---
+ const populateCustomDropdown = () => {
+ const themeDropdown = document.getElementById('chosen-theme');
+ const selectedTheme = themeDropdown ? themeDropdown.value : "fo3";
+ optionsContainer.innerHTML = ''; // Clear old options
+
+ const factionThemes = {
+ "fo3": ["Brotherhood of Steel", "The Enclave", "Vault 87 Mutants", "Wasteland Raiders", "BOS Outcasts", "Reilly's Rangers"],
+ "fnv": ["New California Republic", "Caesar's Legion", "New Vegas Securitrons", "Mojave Brotherhood", "Great Khans", "The Fiends"],
+ "fo4": ["The Minutemen", "The Institute", "The Railroad", "The Gunners", "Nuka-World Raiders", "Brotherhood of Steel"]
+ };
+ const factions = factionThemes[selectedTheme] || [];
+
+ factions.forEach(factionName => {
+ const optionDiv = document.createElement('div');
+ optionDiv.textContent = factionName;
+ optionDiv.style.padding = '8px 12px';
+ optionDiv.style.cursor = 'pointer';
+
+ // THIS IS THE KEY: We attach mouse events to each custom option DIV
+ optionDiv.addEventListener('mouseenter', (e) => {
+ optionDiv.style.background = 'var(--pip-color)';
+ optionDiv.style.color = 'var(--pip-dark)';
+ const factionData = FACTIONS[factionName];
+ if (factionData && factionData.perk) {
+ tooltip.innerHTML = `
${factionData.perk.name}: ${factionData.perk.description}`;
+ tooltip.style.display = 'block';
+ }
+ });
+ optionDiv.addEventListener('mouseleave', () => {
+ optionDiv.style.background = '';
+ optionDiv.style.color = '';
+ tooltip.style.display = 'none';
+ });
+ optionDiv.addEventListener('mousemove', (e) => {
+ tooltip.style.left = (e.pageX + 15) + 'px';
+ tooltip.style.top = (e.pageY + 15) + 'px';
+ });
+ optionDiv.addEventListener('mousedown', () => {
+ factionInput.value = factionName;
+ optionsContainer.style.display = 'none';
+ });
+
+ optionsContainer.appendChild(optionDiv);
+ });
+ };
+
+ // --- Event Listeners to control the dropdown ---
+ factionInput.addEventListener('focus', () => {
+ populateCustomDropdown();
+ optionsContainer.style.display = 'block';
+ });
+
+ // Hide dropdown if you click anywhere else
+ document.addEventListener('click', (e) => {
+ if (!factionInput.contains(e.target) && !optionsContainer.contains(e.target)) {
+ optionsContainer.style.display = 'none';
}
});
- // Trigger on boot to apply the default theme immediately
- themeSelector.dispatchEvent(new Event('change'));
-}
+ // Also attach populate AND theme-switching logic to theme selector
+ const themeSelector = document.getElementById('chosen-theme');
+ if (themeSelector) {
+ themeSelector.addEventListener('change', (e) => {
+ // First, populate the faction list for the new theme
+ populateCustomDropdown();
+
+ // Now, perform all the theme-switching actions
+ const theme = e.target.value;
+ const leaderInput = document.getElementById('chosen-leader');
+ const factionInput = document.getElementById('chosen-country-input');
+
+ // 1. Update cursor color
+ if (typeof updateDynamicCursor === 'function') {
+ if (theme === 'fnv') {
+ updateDynamicCursor('#ffb642');
+ } else if (theme === 'fo4') {
+ updateDynamicCursor('#22ccff');
+ } else {
+ updateDynamicCursor('#18ff62');
+ }
+ }
+
+ // 2. Swap CSS body class
+ document.body.classList.remove('theme-fo3', 'theme-fnv', 'theme-fo4');
+ if (theme !== 'fo3') {
+ document.body.classList.add('theme-' + theme);
+ }
+
+ // 3. Auto-fill default names
+ if (theme === 'fo3') {
+ if (leaderInput) { leaderInput.value = "Lone Wanderer"; }
+ if (factionInput) { factionInput.value = "Brotherhood of Steel"; }
+ } else if (theme === 'fnv') {
+ if (leaderInput) { leaderInput.value = "Courier Six"; }
+ if (factionInput) { factionInput.value = "New California Republic"; }
+ } else if (theme === 'fo4') {
+ if (leaderInput) { leaderInput.value = "Sole Survivor"; }
+ if (factionInput) { factionInput.value = "The Minutemen"; }
+ }
+ });
+
+ // Trigger on boot to apply the default theme immediately
+ themeSelector.dispatchEvent(new Event('change'));
+ }
+
+
+ // Initial population
+ populateCustomDropdown();
+});
+
Gamestate.logQueue = [];
Gamestate.isLogging = false;
@@ -2511,7 +2957,34 @@ Gamestate.renderInventory = function() {
}
};
+Gamestate.populateFactionDropdown = function() {
+ const themeDropdown = document.getElementById('chosen-theme');
+ const selectedTheme = themeDropdown ? themeDropdown.value : "fo3";
+ const dataList = document.getElementById('faction-list');
+
+ if (!dataList) return; // Exit if the element doesn't exist
+ // Clear any old options from a previous selection
+ dataList.innerHTML = "";
+
+ // This object defines which factions belong to which theme
+ const factionThemes = {
+ "fo3": ["Brotherhood of Steel", "The Enclave", "Vault 87 Mutants", "Wasteland Raiders", "BOS Outcasts", "Reilly's Rangers"],
+ "fnv": ["New California Republic", "Caesar's Legion", "New Vegas Securitrons", "Mojave Brotherhood", "Great Khans", "The Fiends"],
+ "fo4": ["The Minutemen", "The Institute", "The Railroad", "The Gunners", "Nuka-World Raiders", "Brotherhood of Steel"]
+ };
+
+ const factionsForTheme = factionThemes[selectedTheme];
+
+ // Create a new
element for each faction and add it to the datalist
+ if (factionsForTheme) {
+ factionsForTheme.forEach(factionName => {
+ const option = document.createElement('option');
+ option.value = factionName;
+ dataList.appendChild(option);
+ });
+ }
+};
Gamestate.init = function(){
@@ -2566,6 +3039,7 @@ document.getElementById('secret-dev-key')?.addEventListener('click', (e) => {
if (helpModal) helpModal.style.display = 'block';
});
+
// --- FIX: RESTORES THE PATCH NOTES BUTTONS ---
document.getElementById('open-updates-btn')?.addEventListener('click', (e) => { e.preventDefault(); document.getElementById('updates-modal').style.display = 'block'; });
document.getElementById('close-updates-btn')?.addEventListener('click', () => { document.getElementById('updates-modal').style.display = 'none'; });
@@ -2619,6 +3093,12 @@ document.getElementById('secret-dev-key')?.addEventListener('click', (e) => {
document.getElementById('dev-modal').style.display = 'none';
});
+document.getElementById('dev-encounter')?.addEventListener('click', () => {
+ this.resolveCreatureEncounter();
+ document.getElementById('dev-modal').style.display = 'none';
+});
+
+
// --- NEW DEV PERK BUTTON ---
document.getElementById('dev-perk')?.addEventListener('click', () => {
if (!Gamestate.bobbleheads) return;
@@ -2669,270 +3149,493 @@ document.getElementById('secret-dev-key')?.addEventListener('click', (e) => {
}
+// Paste this entire new function into your JavaScript file.
+// A good spot is right after your Gamestate.init function.
+
Gamestate.updateButtonText = function() {
if (!end) return;
- if (this.stage === "Fortify") {
- end.textContent = "Deploy Troops"; end.style.opacity = "0.5"; end.style.pointerEvents = "none";
- }
- else if (this.stage === "Battle") {
- end.textContent = "End Attack Phase"; end.style.opacity = "1"; end.style.pointerEvents = "auto";
- }
- else if (this.stage === "Maneuver") {
- if(this.commandersEnabled && this.player.alive && this.player.commander) {
+ if (this.stage === "Fortify") {
+ end.textContent = "Deploy Troops";
+ end.style.opacity = "0.5";
+ end.style.pointerEvents = "none";
+ } else if (this.stage === "Battle") {
+ end.textContent = "End Attack Phase";
+ end.style.opacity = "1";
+ end.style.pointerEvents = "auto";
+ } else if (this.stage === "Maneuver") {
+ // --- THIS IS THE NEW PERK LOGIC ---
+ // For The Railroad, the button should always say "End Turn" during their special maneuver phase.
+ if (this.perksEnabled && this.player.perk && this.player.perk.id === 'rapid_relocation') {
+ end.textContent = "End Turn";
+ } else if (this.commandersEnabled && this.player.alive && this.player.commander) {
end.textContent = this.maneuverSource ? "Next Phase" : "Skip Move";
} else {
end.textContent = "End Turn";
}
- end.style.opacity = "1"; end.style.pointerEvents = "auto";
- }
- else if (this.stage === "Commander Phase") {
- end.textContent = "End Turn"; end.style.opacity = "1"; end.style.pointerEvents = "auto";
- }
- else if (this.stage === "AI Turn") {
- end.textContent = "AI is thinking..."; end.style.opacity = "0.5"; end.style.pointerEvents = "none";
+ end.style.opacity = "1";
+ end.style.pointerEvents = "auto";
+ } else if (this.stage === "Commander Phase") {
+ end.textContent = "End Turn";
+ end.style.opacity = "1";
+ end.style.pointerEvents = "auto";
+ } else if (this.stage === "AI Turn") {
+ end.textContent = "AI is thinking...";
+ end.style.opacity = "0.5";
+ end.style.pointerEvents = "none";
}
}
-Gamestate.start = async function(){
- if(combatLog) combatLog.innerHTML = "";
- this.logQueue = []; this.isLogging = false;
- this.diplomacy = { truces: [], betrayalTax: {}, grudges: {}, spiteTarget: null, spiteTurns: 0, reputation: {} };
- // --- RESET INVENTORY ON REBOOT ---
- if (this.bobbleheads) {
- this.bobbleheads.forEach(b => {
- b.found = false;
- b.cooldown = 0;
- b.active = false;
- });
- }
+Gamestate.start = async function() {
+// --- Read Game Options from UI First ---
+this.perksEnabled = document.getElementById('opt-perks')?.checked;
+this.nukesEnabled = document.getElementById('opt-nukes')?.checked;
+this.commandersEnabled = document.getElementById('opt-commander')?.checked;
+this.hazardsEnabled = document.getElementById('opt-radstorms')?.checked;
+this.encountersEnabled = document.getElementById('opt-encounters')?.checked;
+
+// If any major gameplay modifier is active, enable Wasteland Economy.
+this.wastelandEconomyActive = this.perksEnabled || this.nukesEnabled || this.commandersEnabled || this.hazardsEnabled || this.encountersEnabled;
+
+
+ if (combatLog) combatLog.innerHTML = "";
+ this.logQueue = [];
+ this.isLogging = false;
+ // --- Core Game State Initialization ---
+ this.diplomacy = {
+ truces: [],
+ betrayalTax: {},
+ grudges: {},
+ spiteTarget: null,
+ spiteTurns: 0,
+ reputation: {}
+ };
+ if (this.bobbleheads) this.bobbleheads.forEach(b => {
+ b.found = false;
+ b.cooldown = 0;
+ b.active = false;
+ });
let navInv = document.getElementById('nav-inv');
if (navInv) navInv.classList.remove('inv-pulse');
-
if (end) end.style.pointerEvents = "auto";
if (map) map.style.pointerEvents = "auto";
if (modal) modal.style.display = "none";
if (playerPanel) playerPanel.style.display = "flex";
if (infoPanel) infoPanel.style.display = "flex";
-
- let optRadstorms = document.getElementById('opt-radstorms') && document.getElementById('opt-radstorms').checked;
- let optHorrors = document.getElementById('opt-horrors') && document.getElementById('opt-horrors').checked;
- this.nukesEnabled = document.getElementById('opt-nukes') && document.getElementById('opt-nukes').checked;
- this.commandersEnabled = document.getElementById('opt-commander') && document.getElementById('opt-commander').checked;
- this.flatTrade = document.getElementById('opt-flat-trade') && document.getElementById('opt-flat-trade').checked;
-
- this.hazardsEnabled = optRadstorms;
- this.radstorm = { state: 'none', timer: 0, cooldown: Math.floor(Math.random() * 11) + 5, areas: [] };
-
- this.globalCodes = 10;
- this.activeNuke = null;
-this.devWinOverride = -1;
-
+ // --- Read Game Options from UI ---
+ this.perksEnabled = document.getElementById('opt-perks')?.checked;
+ this.nukesEnabled = document.getElementById('opt-nukes')?.checked;
+ this.commandersEnabled = document.getElementById('opt-commander')?.checked;
+ this.flatTrade = document.getElementById('opt-flat-trade')?.checked;
+ this.hazardsEnabled = document.getElementById('opt-radstorms')?.checked;
+ // --- THIS IS THE NEW LINE FOR ENCOUNTERS ---
+ this.encountersEnabled = document.getElementById('opt-encounters')?.checked;
+ this.radstorm = {
+ state: 'none',
+ timer: 0,
+ cooldown: Math.floor(Math.random() * 11) + 5,
+ areas: []
+ };
+ // Initialize map
this.countries = JSON.parse(JSON.stringify(countries));
this.countries.forEach(c => {
let el = document.getElementById(c.name);
- if (el) { el.classList.remove('radstorm-warning', 'radstorm-active', 'allied-territory', 'crater', 'glowing-sea'); c.isCrater = false; c.radDecay = 0; c.isSilo = false; c.siloTurns = 0; }
+ if (el) {
+ el.classList.remove('radstorm-warning', 'radstorm-active', 'allied-territory', 'crater', 'glowing-sea');
+ c.isCrater = false;
+ c.radDecay = 0;
+ c.isSilo = false;
+ c.siloTurns = 0;
+ }
});
+ // --- PLAYER SETUP LOGIC (with Perks checkbox) ---
+ this.players = [];
+ if (this.perksEnabled) {
+ // --- ADVANCED SETUP: Faction Perks Enabled ---
+ const themeDropdown = document.getElementById('chosen-theme');
+ const selectedTheme = themeDropdown ? themeDropdown.value : "fo3";
+ const factionThemes = {
+ "fo3": ["Brotherhood of Steel", "The Enclave", "Vault 87 Mutants", "Wasteland Raiders", "BOS Outcasts", "Reilly's Rangers"],
+ "fnv": ["New California Republic", "Caesar's Legion", "New Vegas Securitrons", "Mojave Brotherhood", "Great Khans", "The Fiends"],
+ "fo4": ["The Minutemen", "The Institute", "The Railroad", "The Gunners", "Nuka-World Raiders", "Brotherhood of Steel"]
+ };
+ const selectedFactionNames = [...factionThemes[selectedTheme]];
+ const playerCountryInput = document.getElementById('chosen-country-input');
+ const playerCountryChoice = sanitizeInput(playerCountryInput.value) || selectedFactionNames[0];
+ const playerLeaderChoice = sanitizeInput(chosenLeader.value) || "Player";
+ const playerColorChoice = chosenColor.value || "#3a8dcb";
+ const isCustomFaction = !FACTIONS[playerCountryChoice];
+ const playerFactionData = isCustomFaction ? CUSTOM_FACTION : FACTIONS[playerCountryChoice];
+ this.players.push({
+ name: playerLeaderChoice,
+ country: playerCountryChoice,
+ color: playerColorChoice,
+ perk: playerFactionData.perk,
+ army: 0,
+ reserve: 20,
+ areas: [],
+ bonus: 2,
+ alive: true,
+ cards: [],
+ conqueredThisTurn: false,
+ isNeutral: false,
+ isPlayer: true,
+ caps: 20
- let themeDropdown = document.getElementById('chosen-theme');
- let selectedTheme = themeDropdown ? themeDropdown.value : "fo3";
- document.body.classList.remove('theme-fnv');
- if (selectedTheme === "fnv") document.body.classList.add('theme-fnv');
-
- this.players = JSON.parse(JSON.stringify(basePlayers));
-
- let factionList = themeFactions[selectedTheme];
- for(let i = 0; i < 6; i++) {
- // Preserve existing properties and only update name and country
- this.players[i].name = factionList[i].name;
- this.players[i].country = factionList[i].country;
- // This ensures .codes and other properties are not erased
- }
-
-
- let horrors = this.players.find(p => p.isNeutral && p.name === "Wasteland Horrors");
- if ((optHorrors || this.nukesEnabled) && !horrors) {
- this.players.push({ "name": "Wasteland Horrors", "country": "Feral Ghouls & Deathclaws", "color": "#333333", "army": 0, "reserve": 0, "areas": [], "bonus": 0, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": true });
- horrors = this.players[this.players.length - 1];
- }
-
- // Set up standard diplomacy trackers AND the new Reputation Matrix
- this.players.forEach(p1 => {
- this.diplomacy.betrayalTax[p1.name] = 0;
- this.diplomacy.grudges[p1.name] = [];
- this.diplomacy.reputation[p1.name] = {};
-
- // Initialize everyone at Neutral (0) with everyone else
- this.players.forEach(p2 => {
- if (p1.name !== p2.name) {
- this.diplomacy.reputation[p1.name][p2.name] = 0;
- }
});
- });
+ const playerFactionIndex = selectedFactionNames.indexOf(playerCountryChoice);
+ if (playerFactionIndex > -1) {
+ selectedFactionNames.splice(playerFactionIndex, 1);
+ } else {
+ selectedFactionNames.pop();
+ }
+ selectedFactionNames.forEach(factionName => {
+ let factionData = FACTIONS[factionName];
+ let aiColor = (factionData.color === playerColorChoice) ? "#CCCCCC" : factionData.color;
+ this.players.push({
+ name: factionData.leader,
+ country: factionName,
+ color: aiColor,
+ perk: factionData.perk,
+ army: 0,
+ reserve: 20,
+ areas: [],
+ bonus: 2,
+ alive: true,
+ cards: [],
+ conqueredThisTurn: false,
+ isNeutral: false,
+ caps: 20
+ });
+ });
+ } else {
+ // --- CLASSIC SETUP: Faction Perks Disabled (Corrected) ---
+ const playerLeaderInput = document.getElementById('chosen-leader');
+ const playerCountryInput = document.getElementById('chosen-country-input');
+ const playerColorInput = document.getElementById('chosen-color');
+ const playerLeaderChoice = sanitizeInput(playerLeaderInput.value) || "Player 1";
+ const playerCountryChoice = sanitizeInput(playerCountryInput.value) || "Wastelanders";
+ const playerColorChoice = playerColorInput.value || "#0088CC";
+ this.players.push({
+ name: playerLeaderChoice,
+ country: playerCountryChoice,
+ color: playerColorChoice,
+ perk: {
+ id: "none"
+ },
+ army: 0,
+ reserve: 20,
+ areas: [],
+ bonus: 2,
+ alive: true,
+ cards: [],
+ conqueredThisTurn: false,
+ isNeutral: false,
+ isPlayer: true,
+ caps: 20
- this.aiTurn = false; this.gameOver = false; this.prevCountry = null; this.prevTarget = null; this.turn = 1; this.stage = "Fortify"; this.playerTroopsPlaced = 0; this.player = this.players[0];
-
- let diffSelect = document.getElementById('chosen-difficulty'); this.difficulty = diffSelect ? diffSelect.value : "Normal";
+ });
+ const defaultColors = ["#CC0000", "#00CC00", "#CCCC00", "#CC00CC", "#00CCCC"];
+ for (let i = 1; i < 6; i++) {
+ let aiColor = (defaultColors[i - 1] === playerColorChoice) ? "#888888" : defaultColors[i - 1];
+ this.players.push({
+ name: `AI Player ${i}`,
+ country: `Faction ${i}`,
+ color: aiColor,
+ perk: {
+ id: "none"
+ },
+ army: 0,
+ reserve: 20,
+ areas: [],
+ bonus: 2,
+ alive: true,
+ cards: [],
+ conqueredThisTurn: false,
+ isNeutral: false,
+ caps: 20
- if (chosenLeader && chosenCountry) {
- let safeLeader = sanitizeInput(chosenLeader.value); let safeCountry = sanitizeInput(chosenCountry.value);
- this.players[0].name = safeLeader; this.players[0].country = safeCountry;
- if (playerName) playerName.textContent = safeLeader; if (playerCountry) playerCountry.textContent = safeCountry;
+ });
+ }
}
-
-// --- OVERSEER KEY REVEAL ---
+ // Finalize player UI text
+ if (playerName) playerName.textContent = this.players.find(p => p.isPlayer).name;
+ if (playerCountry) playerCountry.textContent = this.players.find(p => p.isPlayer).country;
+ // --- CORRECTED OVERSEER KEY REVEAL ---
let devKey = document.getElementById('secret-dev-key');
if (devKey) {
- if (this.players[0].name.toLowerCase() === "overseer") devKey.style.display = "block";
- else devKey.style.display = "none";
+ // Check the name property of the human player object.
+ const humanPlayer = this.players.find(p => p.isPlayer);
+ if (humanPlayer && humanPlayer.name.toLowerCase() === "overseer") {
+ devKey.style.display = "block";
+ } else {
+ devKey.style.display = "none";
+ }
}
+ // Add neutral Horrors faction if needed
+ if (document.getElementById('opt-horrors')?.checked || this.nukesEnabled) {
+ this.players.push({
+ "name": "Wasteland Horrors",
+ "country": "Feral Ghouls & Deathclaws",
+ "color": "#333333",
+ perk: {
+ id: "none"
+ },
+ army: 0,
+ reserve: 0,
+ areas: [],
+ "bonus": 0,
+ "alive": true,
+ "cards": [],
+ "conqueredThisTurn": false,
+ "isNeutral": true,
+ caps: 20
- if (chosenColor) {
- let selectedHex = chosenColor.value; let defaultPlayerColor = this.players[0].color;
- let colorConflictAI = this.players.find((p, index) => index !== 0 && p.color === selectedHex);
- if (colorConflictAI) colorConflictAI.color = defaultPlayerColor;
- this.players[0].color = selectedHex;
+ });
}
-
- generateDeck(); tradeCount = 0; this.players.forEach(p => { p.cards = []; p.conqueredThisTurn = false; });
- let cardCount = document.getElementById('card-count'); if (cardCount) cardCount.textContent = "0"; if(this.prevTarget) this.prevTarget.classList.remove('flash');
-
- for(let j = 0; j < this.players.length; j++){
- if(infoName[j]) infoName[j].innerHTML = this.players[j].country; if(infoLeader[j]) infoLeader[j].innerHTML = this.players[j].name;
- if(infoName[j]) infoName[j].parentElement.classList.remove('defeated'); if(bar[j]) bar[j].style.background = this.players[j].color;
+ // --- REPUTATION SETUP ---
+ this.players.forEach(p1 => {
+ this.diplomacy.betrayalTax[p1.name] = 0;
+ this.diplomacy.grudges[p1.name] = [];
+ this.diplomacy.reputation[p1.name] = {};
+ this.players.forEach(p2 => {
+ this.diplomacy.reputation[p1.name][p2.name] = 0;
+ });
+ });
+ if (this.perksEnabled) {
+ setInitialReputations();
}
+// --- Initialize Core Game Variables ---
+this.aiTurn = false;
+this.gameOver = false;
+this.turn = 1;
- this.stage = "Fortify"; this.updateButtonText(); if (turnInfoMessage) turnInfoMessage.textContent = "Click on your own areas to place reinforcements";
-
- // --- MAP DISTRIBUTION ---
+if (this.wastelandEconomyActive) {
+ this.stage = "Income";
+} else {
+ this.stage = "Fortify";
+}
+
+ this.player = this.players.find(p => p.isPlayer);
+// --- FULL MAP & UI POPULATION LOGIC (from your working code) ---
+if (!this.wastelandEconomyActive) {
+ generateDeck();
+ tradeCount = 0;
+ this.players.forEach(p => { p.cards = []; });
+}
+this.players.forEach(p => { p.conqueredThisTurn = false; });
+
+ let cardCount = document.getElementById('card-count');
+ if (cardCount) cardCount.textContent = "0";
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ for (let j = 0; j < this.players.length; j++) {
+ if (infoName[j]) infoName[j].innerHTML = this.players[j].country;
+ if (infoLeader[j]) infoLeader[j].innerHTML = this.players[j].name;
+ if (infoName[j]) infoName[j].parentElement.classList.remove('defeated');
+ if (bar[j]) bar[j].style.background = this.players[j].color;
+ }
+ this.updateButtonText();
+ if (turnInfoMessage) turnInfoMessage.textContent = "Click on your own areas to place reinforcements";
+ let horrors = this.players.find(p => p.isNeutral);
let siloAreas = [];
- if(this.nukesEnabled) {
+ if (this.nukesEnabled && horrors) {
continents.forEach(cont => {
let randArea = cont.areas[Math.floor(Math.random() * cont.areas.length)];
let c = this.countries.find(x => x.name === randArea);
- c.isSilo = true; c.owner = "Wasteland Horrors"; c.color = "#333333"; c.army = 15;
- siloAreas.push(c.name); horrors.areas.push(c.name); horrors.army += 15;
+ c.isSilo = true;
+ c.owner = "Wasteland Horrors";
+ c.color = "#333333";
+ c.army = 15;
+ siloAreas.push(c.name);
+ horrors.areas.push(c.name);
+ horrors.army += 15;
});
document.getElementById('nuke-ui-container').style.display = "block";
- } else { document.getElementById('nuke-ui-container').style.display = "none"; }
-
- this.players.forEach(p => { p.reserve = p.isNeutral ? 0 : 20; if(p.name !== "Wasteland Horrors") { p.army = 0; p.areas = []; }});
-
-let validAreasForPlayers = areas.filter(a => !siloAreas.includes(a.id));
+ } else {
+ document.getElementById('nuke-ui-container').style.display = "none";
+ }
+ this.players.forEach(p => {
+ if (p !== horrors) {
+ p.army = 0;
+ p.areas = [];
+ p.reserve = 20;
+ }
+ });
+ let validAreasForPlayers = areas.filter(a => !siloAreas.includes(a.id));
let shuffledAreas = shuffle([...validAreasForPlayers]);
-
- // --- WILD GHOULS STARTING SPAWN ---
- if (optHorrors && horrors) {
- let ghoulStartCount = 4; // Give them 4 random territories to start
+ if (document.getElementById('opt-horrors')?.checked && horrors) {
+ let ghoulStartCount = 4;
for (let g = 0; g < ghoulStartCount; g++) {
- let area = shuffledAreas.pop(); // Remove it from the available player pool
+ let area = shuffledAreas.pop();
let country = this.countries.find(c => c.name === area.id);
- country.owner = horrors.name;
- country.color = horrors.color;
- country.army = Math.floor(Math.random() * 3) + 3; // Start with 3 to 5 ghouls
- horrors.areas.push(country.name);
- horrors.army += country.army;
+ if (country) {
+ country.owner = horrors.name;
+ country.color = horrors.color;
+ country.army = Math.floor(Math.random() * 3) + 3;
+ horrors.areas.push(country.name);
+ horrors.army += country.army;
+ }
}
}
-
let activePlayers = this.players.filter(p => !p.isNeutral);
-
shuffledAreas.forEach((area, i) => {
- let player = activePlayers[i % activePlayers.length];
+ let player = activePlayers[i % activePlayers.length];
let country = this.countries.find(c => c.name === area.id);
- country.owner = player.name; country.color = player.color; country.army = 1;
- player.areas.push(country.name); player.army += country.army; player.reserve -= 1;
+ if (country) {
+ country.owner = player.name;
+ country.color = player.color;
+ country.army = 1;
+ player.areas.push(country.name);
+ player.army++;
+ player.reserve--;
+ }
});
-
this.players.forEach(player => {
- while(player.reserve > 0 && !player.isNeutral && player.areas.length > 0) {
+ while (player.reserve > 0 && !player.isNeutral && player.areas.length > 0) {
let randomArea = player.areas[Math.floor(Math.random() * player.areas.length)];
let country = this.countries.find(c => c.name === randomArea);
- country.army += 1; player.army += 1; player.reserve -= 1;
+ if (country) {
+ country.army++;
+ player.army++;
+ player.reserve--;
+ }
+ }
+ });
+ if (this.commandersEnabled) {
+ this.players.forEach(p => {
+ if (p.alive && !p.isNeutral && p.areas.length > 0) {
+ p.commander = {
+ hp: 100,
+ ap: 2,
+ loc: p.areas[Math.floor(Math.random() * p.areas.length)],
+ stimpaks: 0,
+ siegeTurns: 0,
+ wasAttacked: false
+ };
+ }
+ });
+ document.getElementById('cmdr-ui-container').style.display = "block";
+ } else {
+ document.getElementById('cmdr-ui-container').style.display = "none";
+ }
+ this.countries.forEach(country => {
+ let areaOnMap = document.getElementById(country.name);
+ if (areaOnMap) areaOnMap.style.fill = country.color;
+ });
+if (!this.wastelandEconomyActive) {
+ this.player.reserve = this.unitBonus(this.player, 0);
+ this.player.army += this.player.reserve;
+}
+
+this.drawMapText();
+this.updateInfo();
+this.startIntelAnimation();
+await this.logAction("RobCo OS Booted. Wasteland Simulation Online.", true);
+if (this.wastelandEconomyActive) await this.logAction(">>> SYSTEM: WASTELAND ECONOMY ENGAGED. CAPS ARE KING.", true);
+
+ if (document.getElementById('opt-horrors')?.checked) await this.logAction(">>> SYSTEM: NEUTRAL THREAT SCANNER ACTIVE.");
+ if (this.nukesEnabled) await this.logAction(">>> DEFCON ALERT: 6 Pre-War Command Silos detected. Arms race initiated.", true);
+ if (this.commandersEnabled) await this.logAction(">>> COMMANDER PROTOCOL: VIPs deployed to the field. Protect them at all costs.", true);
+ if (this.perksEnabled) await this.logAction(">>> WARNING: Faction-specific combat doctrines are active. Expect asymmetrical warfare.", true);
+ await this.logAction("--- DAY 1 BEGINS ---", true);
+};
+
+
+
+Gamestate.toggleLockdown = async function(countryName) {
+ const clickedCountry = this.countries.find(c => c.name === countryName);
+ if (!clickedCountry || clickedCountry.owner !== this.player.name) return;
+
+ // 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;
+ }
+
+ const isAlreadyLocked = clickedCountry.isLockedDown;
+
+ // First, clear any existing lockdown from all other territories.
+ this.countries.forEach(c => {
+ if (c.name !== countryName) {
+ c.isLockedDown = false;
+ let el = document.getElementById(c.name);
+ if (el) el.classList.remove('lockdown-territory');
}
});
- if(this.commandersEnabled) {
-this.players.forEach(p => { if(p.alive && !p.isNeutral && p.areas.length > 0) { p.commander = { hp: 100, ap: 2, loc: p.areas[Math.floor(Math.random() * p.areas.length)], stimpaks: 0, siegeTurns: 0, wasAttacked: false }; }});
- document.getElementById('cmdr-ui-container').style.display = "block";
- } else { document.getElementById('cmdr-ui-container').style.display = "none"; }
+ // 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
+ 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)}.`);
+ }
+
+ this.updateInfo();
+};
- this.countries.forEach(country => { let areaOnMap = document.getElementById(country.name); if (areaOnMap) areaOnMap.style.fill = country.color; });
-
- this.player.reserve = this.unitBonus(this.player, 0); this.player.army += this.player.reserve;
- this.drawMapText(); this.updateInfo();
-
- this.startIntelAnimation();
- await this.logAction("RobCo OS Booted. Wasteland Simulation Online.", true);
- if (optRadstorms) await this.logAction(">>> SYSTEM: METEOROLOGICAL HAZARD SUBSYSTEM LOADED.");
- if (optHorrors) await this.logAction(">>> SYSTEM: NEUTRAL THREAT SCANNER ACTIVE.");
- if (this.nukesEnabled) await this.logAction(">>> DEFCON ALERT: 6 Pre-War Command Silos detected. Arms race initiated.", true);
- if (this.commandersEnabled) await this.logAction(">>> COMMANDER PROTOCOL: VIPs deployed to the field. Protect them at all costs.", true);
-await this.logAction("--- DAY 1 BEGINS ---", true);
-}
Gamestate.drawMapText = function() {
this.countries.forEach(country => {
let areaOnMap = document.getElementById(country.name);
let textNode = country.textNode || (areaOnMap ? areaOnMap.nextElementSibling : null);
-
if (areaOnMap && textNode) {
-
- if(country.isCrater) {
- textNode.innerHTML = "";
- return;
+ if (country.isCrater) {
+ textNode.innerHTML = "";
+ return;
}
-
// --- FOG OF WAR CHECK ---
const isShrouded = areaOnMap.classList.contains('fog-shroud');
-
let finalHtml;
-
if (!isShrouded) {
// --- VISIBLE TERRITORY ---
- // Your original, working logic for visible territories.
let text = `${country.army}`;
let iconHtml = "";
- if(this.nukesEnabled && country.isSilo) {
+ // --- THIS IS THE NEW LOGIC FOR THE LOCKDOWN ICON ---
+ if (country.isLockedDown) {
+ iconHtml += `🔒 `;
+ }
+
+ if (this.nukesEnabled && country.isSilo) {
let isLaunchSite = (this.activeNuke && this.activeNuke.launchSilo === country.name);
let siloColor = isLaunchSite ? "#ff3333" : "#ffcc00";
let pulse = isLaunchSite ? `filter="drop-shadow(0 0 8px #ff3333)"` : ``;
iconHtml += `☢ `;
}
-
- if(this.commandersEnabled) {
+ if (this.commandersEnabled) {
this.players.forEach(p => {
if (p.alive && !p.isNeutral && p.commander && p.commander.loc === country.name) {
iconHtml += `★ `;
}
});
}
-
- if (iconHtml !== "") {
- text += `${iconHtml} `;
+ if (iconHtml !== "") {
+ text += `${iconHtml} `;
}
finalHtml = text;
-
} else {
// --- SHROUDED TERRITORY ---
- // Show a question mark.
let text = '?';
-
// SILO MEMORY: If we know a silo is here, show the icon.
if (this.nukesEnabled && country.isSilo && country.knownSilo) {
- // CORRECTED LINE: This creates a valid icon without the problematic 'dy' attribute.
- // The icon will appear next to the question mark.
text += ` ☢ `;
}
finalHtml = text;
}
-
// Update the map text.
textNode.innerHTML = finalHtml;
}
@@ -3195,6 +3898,33 @@ Gamestate.showEnvoyModal = function(factionName, caps, turns, color, isRequestin
});
}
+Gamestate.showFrenzyModal = function(targetName) {
+ return new Promise(resolve => {
+ const modal = document.getElementById('frenzy-modal');
+ const targetNameSpan = document.getElementById('frenzy-target-name');
+ const yesBtn = document.getElementById('frenzy-yes');
+ const noBtn = document.getElementById('frenzy-no');
+
+ if (!modal || !targetNameSpan || !yesBtn || !noBtn) {
+ resolve(false); // Failsafe if elements don't exist
+ return;
+ }
+
+ targetNameSpan.textContent = targetName;
+ modal.style.display = 'flex';
+
+ const close = (decision) => {
+ modal.style.display = 'none';
+ yesBtn.onclick = null; // Clean up listeners
+ noBtn.onclick = null;
+ resolve(decision);
+ };
+
+ yesBtn.onclick = () => close(true);
+ noBtn.onclick = () => close(false);
+ });
+};
+
Gamestate.showBetrayalModal = function(targetName) {
return new Promise((resolve) => {
@@ -3560,15 +4290,25 @@ Gamestate.updateInfo = function(){
combinedStats += ` | Codes: ${nukesVal}`;
}
- // The rest of your code, which correctly puts the stats in the container
- if (incomeContainer) {
- incomeContainer.innerHTML = combinedStats;
- incomeContainer.style.display = "block";
- }
-
- if (capsEl) {
- capsEl.style.display = "none";
- }
+// --- THIS IS THE NEW ECONOMY/CLASSIC DISPLAY LOGIC ---
+if (this.wastelandEconomyActive) {
+ let capsDisplay = hasIntel ? `${player.caps} Caps ` : `?? `;
+ if (incomeContainer) {
+ incomeContainer.innerHTML = `Treasury: ${capsDisplay}`;
+ incomeContainer.style.display = "block";
+ }
+ if (capsEl) capsEl.style.display = "none";
+} else {
+ // Original logic for classic mode
+ if (incomeContainer) {
+ incomeContainer.innerHTML = combinedStats;
+ incomeContainer.style.display = "block";
+ }
+ if (capsEl) {
+ capsEl.style.display = "none";
+ }
+}
+
}
@@ -3622,13 +4362,39 @@ Gamestate.updateInfo = function(){
if (this.players.length === 6 && infoName[6] && infoName[6].parentElement) infoName[6].parentElement.style.display = "none";
let helpBtnEl = document.getElementById('help-btn'); if (helpBtnEl) helpBtnEl.style.order = "998";
let restartBtn = document.getElementById('restart'); if (restartBtn) restartBtn.style.order = "999";
- let cardCount = document.getElementById('card-count'); if (cardCount) cardCount.textContent = this.player.cards.length;
- if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve;
- let viewCardsBtn = document.getElementById('view-cards-btn');
- if (viewCardsBtn) {
- if (this.getBestTrade(this.player.cards)) { viewCardsBtn.textContent = "OPEN STASH"; viewCardsBtn.style.opacity = "1"; viewCardsBtn.style.pointerEvents = "auto"; viewCardsBtn.classList.add('ready-to-trade'); }
- else { viewCardsBtn.textContent = this.player.cards.length > 0 ? "NO ELIGIBLE SETS" : "STASH EMPTY"; viewCardsBtn.style.opacity = "0.5"; viewCardsBtn.style.pointerEvents = "none"; viewCardsBtn.classList.remove('ready-to-trade'); }
+ let cardCount = document.getElementById('card-count');
+if (cardCount) {
+ if (this.wastelandEconomyActive) {
+ cardCount.textContent = this.player.caps; // Show currency
+ } else {
+ cardCount.textContent = this.player.cards.length; // Show card count
}
+}
+
+ if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve;
+let viewCardsBtn = document.getElementById('view-cards-btn');
+if (viewCardsBtn) {
+ if (this.wastelandEconomyActive) {
+ viewCardsBtn.textContent = "VIEW STASH";
+ viewCardsBtn.style.opacity = "1";
+ viewCardsBtn.style.pointerEvents = "auto";
+ viewCardsBtn.classList.remove('ready-to-trade');
+ } else {
+ // Original logic for classic mode
+ if (this.getBestTrade(this.player.cards)) {
+ viewCardsBtn.textContent = "OPEN STASH";
+ viewCardsBtn.style.opacity = "1";
+ viewCardsBtn.style.pointerEvents = "auto";
+ viewCardsBtn.classList.add('ready-to-trade');
+ } else {
+ viewCardsBtn.textContent = this.player.cards.length > 0 ? "NO ELIGIBLE SETS" : "STASH EMPTY";
+ viewCardsBtn.style.opacity = "0.5";
+ viewCardsBtn.style.pointerEvents = "none";
+ viewCardsBtn.classList.remove('ready-to-trade');
+ }
+ }
+}
+
let hpFill = document.getElementById('hp-fill'); let apFill = document.getElementById('ap-fill');
if (hpFill && apFill && totalArmy > 0 && this.player.alive) {
let hpPercentage = Math.min(100, (this.player.areas.length / 24) * 100); hpFill.style.width = hpPercentage + "%";
@@ -3693,6 +4459,8 @@ Gamestate.updateInfo = function(){
if (this.nukesEnabled && country.isSilo) { country.knownSilo = true; }
if (country.radDecay > 0) areaOnMap.classList.add('glowing-sea'); else areaOnMap.classList.remove('glowing-sea');
if (this.areAllies(this.player.name, country.owner)) areaOnMap.classList.add('allied-territory'); else areaOnMap.classList.remove('allied-territory');
+ if (country.isLockedDown) areaOnMap.classList.add('lockdown-territory');
+
} else {
areaOnMap.classList.add('fog-shroud');
areaOnMap.style.fill = '';
@@ -3736,6 +4504,79 @@ Gamestate.updateInfo = function(){
document.getElementById('cmdr-hp-fill').style.width = `${this.player.commander.hp}%`;
}
this.drawMapText(); this.lastStage = this.stage;
+
+// --- ACTIONABLE PERK BUTTON LOGIC (v5 - Final) ---
+const perkButtonWrapper = document.getElementById('perk-button-wrapper');
+const perkButton = document.getElementById('btn-perk-action');
+const tooltip = document.getElementById('vats-tooltip');
+if (perkButtonWrapper && perkButton && tooltip) {
+ let showButton = false;
+ let tooltipText = "";
+ if (this.perksEnabled && this.player && this.player.perk && !this.aiTurn) {
+
+ // PERK 1: The Gunners
+ if (this.player.perk.id === 'mercenary_contracts') {
+ showButton = true;
+ perkButton.textContent = "Mercenary Contract (2 Caps)";
+ perkButton.onclick = () => this.useMercenaryContract();
+ perkButton.disabled = this.player.cards.length < 2;
+ tooltipText = perkButton.disabled ? "Requires at least 2 Bottle Caps." : "Spend 2 Caps to deploy troops to your reserves, scaling with territory owned.";
+ }
+ // PERK 2: Mojave Brotherhood (Corrected v3)
+ 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";
+ perkButton.onclick = () => {
+ // Check if a territory has been selected
+ 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");
+ }
+ };
+ // --- NEW Disabling Logic ---
+ // Disable the button if it's the Fortify phase AND there are troops to place.
+ if (this.stage === "Fortify" && this.player.reserve > 0) {
+ perkButton.disabled = true;
+ tooltipText = "Deploy all reserve troops to enable the Elder's Edict.";
+ }
+ // Disable if the perk is on cooldown
+ else if (this.player.lockdownCooldown > 0) {
+ perkButton.disabled = true;
+ tooltipText = `Edict is on cooldown for ${this.player.lockdownCooldown} more turn(s).`;
+ }
+ // Otherwise, enable the button
+ else {
+ perkButton.disabled = false;
+ tooltipText = "Select one of your territories and then click this button to apply or remove the Elder's Edict.";
+ }
+ }
+ // PERK 3: The Railroad (No button needed for this passive perk)
+ // The logic for 'rapid_relocation' is handled in the maneuver and end turn functions.
+ // We no longer show a button here.
+ }
+ // Final visibility decision
+ perkButtonWrapper.style.display = showButton ? 'block' : 'none';
+
+ // --- Event Listeners for the Custom Tooltip ---
+ perkButtonWrapper.onmouseenter = (e) => {
+ if (tooltipText && perkButtonWrapper.style.display === 'block') {
+ tooltip.innerHTML = tooltipText;
+ tooltip.style.display = 'block';
+ tooltip.style.left = (e.pageX + 15) + 'px';
+ tooltip.style.top = (e.pageY + 15) + 'px';
+ }
+ };
+ perkButtonWrapper.onmousemove = (e) => {
+ tooltip.style.left = (e.pageX + 15) + 'px';
+ tooltip.style.top = (e.pageY + 15) + 'px';
+ };
+ perkButtonWrapper.onmouseleave = () => {
+ tooltip.style.display = 'none';
+ };
+}
+
}
@@ -3822,6 +4663,12 @@ Gamestate.executeTrade = async function() {
if (this.aiTurn) return;
if (this.isValidTrade()) {
let bonus = getTradeBonus();
+ // --- PERK LOGIC: BOS Outcasts ('tech_hoarders') ---
+ if (this.perksEnabled && this.player.perk && this.player.perk.id === 'tech_hoarders') {
+ bonus += 2;
+ await this.logAction("⚙️ Tech Hoarders perk yielded +2 additional troops from the trade.");
+ }
+
this.selectedCards.sort((a,b) => b-a).forEach(index => { deck.unshift(this.player.cards[index]); this.player.cards.splice(index, 1); });
this.player.reserve += bonus; this.player.army += bonus;
@@ -3908,68 +4755,75 @@ Gamestate.processRadstorm = async function() {
});
}
-Gamestate.handleEndTurn = async function(){
- if(this.aiTurn || this.player.reserve > 0){ return; }
-
-// --- BATTLE -> MANEUVER BRIDGE ---
+Gamestate.handleEndTurn = async function() {
+ if (this.aiTurn || this.player.reserve > 0) {
+ return;
+ }
+ // --- BATTLE -> MANEUVER BRIDGE ---
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.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(this.prevTarget) this.prevTarget.classList.remove('flash');
- this.prevCountry = null; this.prevTarget = null;
-
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ this.prevCountry = null;
+ this.prevTarget = null;
if (canManeuver) {
- this.stage = "Maneuver";
- this.maneuverSource = null; this.maneuverDest = null;
- this.updateButtonText();
- if (turnInfo) turnInfo.textContent = "Maneuver Phase";
- if (turnInfoMessage) turnInfoMessage.textContent = "Move troops between adjacent territories. [SHIFT] moves all.";
- this.updateInfo();
- return;
+ this.stage = "Maneuver";
+ this.maneuverSource = null;
+ this.maneuverDest = null;
+
+ // --- THIS IS THE NEW PERK LOGIC ---
+ // Grant maneuver points to The Railroad player
+ if (this.perksEnabled && this.player.perk && this.player.perk.id === 'rapid_relocation') {
+ this.player.maneuverPoints = 5;
+ if (turnInfoMessage) turnInfoMessage.textContent = `Move troops. You have ${this.player.maneuverPoints} moves remaining.`;
+ } else {
+ this.player.maneuverPoints = 1; // Standard players get 1 point
+ if (turnInfoMessage) turnInfoMessage.textContent = "Move troops between adjacent territories. [SHIFT] moves all.";
+ }
+
+ this.updateButtonText();
+ if (turnInfo) turnInfo.textContent = "Maneuver Phase";
+ this.updateInfo();
+ return;
} else {
// No maneuvers possible? Jump straight to Commander Phase instead of ending turn!
this.stage = "Maneuver"; // Set to maneuver temporarily so the next block catches it
}
}
-
// --- MANEUVER -> COMMANDER BRIDGE ---
if (this.stage === "Maneuver" && this.commandersEnabled && this.player.alive && this.player.commander) {
- if(this.prevTarget) this.prevTarget.classList.remove('flash');
- this.prevCountry = null; this.prevTarget = null;
-
- this.stage = "Commander Phase";
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ this.prevCountry = null;
+ this.prevTarget = null;
+ this.stage = "Commander Phase";
this.player.commander.ap = 2;
this.player.commander.hasFought = false; // Reset attack limit
this.aiTurn = false; // Guard against AI hijacking
-
- this.updateButtonText();
- if(turnInfo) turnInfo.textContent = "Commander Phase";
- if(turnInfoMessage) turnInfoMessage.textContent = "Move your Commander or use a Stimpak. (Costs 1 AP)";
- this.updateInfo();
+ this.updateButtonText();
+ if (turnInfo) turnInfo.textContent = "Commander Phase";
+ if (turnInfoMessage) turnInfoMessage.textContent = "Move your Commander or use a Stimpak. (Costs 1 AP)";
+ this.updateInfo();
return;
}
-
// FIX: Lock the AI Turn and Map IMMEDIATELY before any 'await' delays to prevent multi-click cloning!
- this.aiTurn = true;
+ this.aiTurn = true;
if (map) map.style.pointerEvents = "none";
-
if (this.player.conqueredThisTurn) {
-
let luckItem = this.bobbleheads && this.bobbleheads.find(i => i.key === 'l' && i.cooldown > 0);
// Luck perk now adds a flat +15% to all rare loot drops
- const luckModifier = luckItem ? 0.15 : 0;
-
+ const luckModifier = luckItem ? 0.15 : 0;
let roll = Math.random();
-
// 1. Roll for Bobblehead (5% base + 15% Luck = 20% max)
- if (this.bobbleheads && roll < (0.05 + luckModifier)) {
+ if (this.bobbleheads && roll < (0.05 + luckModifier)) {
let unfoundItems = this.bobbleheads.filter(item => !item.found);
if (unfoundItems.length > 0) {
let foundItem = unfoundItems[Math.floor(Math.random() * unfoundItems.length)];
@@ -3977,83 +4831,98 @@ Gamestate.handleEndTurn = async function(){
let navInv = document.getElementById('nav-inv');
if (navInv) navInv.classList.add('inv-pulse');
this.justFoundLoot = true;
-
-
- // CRITICAL: Call queueToast for the Bobblehead
if (this.queueToast) {
this.queueToast(`>>> SYSTEM OVERRIDE <<< ★ ASSET ACQUIRED: ${foundItem.name.toUpperCase()}`, "var(--pip-color)", true);
}
await this.logAction(`[ EPIC LOOT ] You discovered a '${foundItem.name}' in the ruins!`, true);
} else {
- // If all bobbleheads are found, fallback to giving a Stimpak
if (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');
this.justFoundLoot = true;
-
- // CRITICAL: Call queueToast for the Stimpak (Fallback)
if (this.queueToast) {
- this.queueToast(`>>> MEDICAL ALERT <<< ✚ STIMPAK INVENTORY UPDATED`, "var(--pip-color)", true);
+ this.queueToast(`>>> MEDICAL ALERT <<< + STIMPAK INVENTORY UPDATED`, "var(--pip-color)", true);
}
await this.logAction("[ MEDICAL ] Found a rare Stimpak hidden in the rubble!", true);
}
}
- }
+ }
// 2. Roll for Stimpak (25% base + 15% Luck = 40% max)
- else if (this.commandersEnabled && this.player.commander && this.player.commander.stimpaks < 3 && roll < (0.40 + luckModifier)) {
+ else if (this.commandersEnabled && this.player.commander && this.player.commander.stimpaks < 3 && roll < (0.40 + luckModifier)) {
this.player.commander.stimpaks++;
let navInv = document.getElementById('nav-inv');
if (navInv) navInv.classList.add('inv-pulse');
- this.justFoundLoot = true;
-
- // CRITICAL: Call queueToast for the Stimpak
+ this.justFoundLoot = true;
if (this.queueToast) {
- this.queueToast(`>>> MEDICAL ALERT <<< ✚ STIMPAK INVENTORY UPDATED`, "var(--pip-color)", true);
+ this.queueToast(`>>> MEDICAL ALERT <<< + STIMPAK INVENTORY UPDATED`, "var(--pip-color)", true);
}
await this.logAction("[ MEDICAL ] Scavenged a rare Stimpak from the battlefield!", true);
- }
+ }
// 3. Roll for Nuke Codes (15% base + 15% Luck = 30% max)
- else if (this.nukesEnabled && this.player.codes < 4 && this.globalCodes > 0 && roll < (0.55 + luckModifier)) {
- this.player.codes++; this.globalCodes--;
+ else if (this.nukesEnabled && this.player.codes < 4 && this.globalCodes > 0 && roll < (0.55 + luckModifier)) {
+ this.player.codes++;
+ this.globalCodes--;
this.justFoundLoot = true;
-
-
- // Use the standard terminal color for finding fragments
if (this.queueToast) {
this.queueToast(`>>> ENCRYPTED DATA ACQUIRED <<< ☢ LAUNCH CODE FRAGMENT RECOVERED`, "var(--pip-color)", true);
}
await this.logAction("[ INTEL ] Recovered a heavily encrypted Launch Code Fragment!", true);
- }
-
+ }
// 4. Default: Always get a Bottle Cap if nothing rare is found
else {
- let newCap = deck.length > 0 ? deck.pop() : { country: "Wasteland Salvage", type: "Wild" };
+ let newCap = deck.length > 0 ? deck.pop() : {
+ country: "Wasteland Salvage",
+ type: "Wild"
+ };
this.player.cards.push(newCap);
-
if (this.getBestTrade(this.player.cards)) {
await this.logAction("STASH FULL: Enough Caps collected to hire more troops.");
let navInv = document.getElementById('nav-inv');
- if (navInv) navInv.classList.add('inv-pulse');
+ if (navInv) navInv.classList.add('inv-pulse');
} else {
await this.logAction("SCAVENGED: Found a Bottle Cap after securing enemy territory.");
}
- this.justFoundLoot = false; // Reset the flag if only a cap was found
-
+ this.justFoundLoot = false;
}
this.updateInfo();
}
this.player.conqueredThisTurn = false;
+ this.updateButtonText();
+ this.aiMove();
+};
+
+
+
- this.updateButtonText(); this.aiMove();
-}
Gamestate.unitBonus = function(player, i){
if (!player.alive || player.isNeutral) return 0;
- player.bonus = 0; player.bonus += Math.floor(player.areas.length / 3); player.bonus += this.continentBonus(player);
- if(player.bonus < 3){ player.bonus = 3; } if (infoIncome[i]) infoIncome[i].innerHTML = player.bonus;
+
+ player.bonus = 0;
+ // Standard reinforcement bonus
+ player.bonus += Math.floor(player.areas.length / 3);
+ player.bonus += this.continentBonus(player);
+
+ // --- PERK LOGIC: New California Republic ('logistical_superiority') ---
+ if (this.perksEnabled && player.perk && player.perk.id === 'logistical_superiority') {
+ let continentsOwned = 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++;
+ }
+ });
+ player.bonus += continentsOwned; // Add +1 troop for each continent controlled.
+ }
+
+ // Minimum reinforcement rule
+ if(player.bonus < 3){ player.bonus = 3; }
+
+ if (infoIncome[i]) infoIncome[i].innerHTML = player.bonus;
return player.bonus;
}
+
Gamestate.continentBonus = function(player){
if (player.isNeutral) return 0;
let bonus = 0; continents.forEach( continent => {
@@ -4063,39 +4932,107 @@ Gamestate.continentBonus = function(player){
return bonus;
}
+Gamestate.findShortestPath = function(startNodeName, endNodeName) {
+ let queue = [
+ [startNodeName]
+ ];
+ let visited = new Set([startNodeName]);
+
+ while (queue.length > 0) {
+ let path = queue.shift();
+ let nodeName = path[path.length - 1];
+
+ if (nodeName === endNodeName) {
+ return path;
+ }
+
+ let country = this.countries.find(c => c.name === nodeName);
+ if (country) {
+ for (let neighborName of country.neighbours) {
+ if (!visited.has(neighborName)) {
+ visited.add(neighborName);
+ let newPath = [...path, neighborName];
+ queue.push(newPath);
+ }
+ }
+ }
+ }
+ return null; // No path found
+};
+
+
+// Helper function to check if a territory is on a continent border
+Gamestate.isContinentBorder = function(countryName) {
+ const country = this.countries.find(c => c.name === countryName);
+ if (!country) return false;
+
+ const ownContinent = country.continent;
+ return country.neighbours.some(neighbourName => {
+ const neighbour = this.countries.find(n => n.name === neighbourName);
+ return neighbour && neighbour.continent !== ownContinent;
+ });
+};
+
+
Gamestate.addArmy = async function(e){
let actionFired = false;
- this.countries.forEach(country => {
- if(e.target.id === country.name && this.player.reserve > 0 && country.owner === this.player.name){
- if(e.shiftKey){ this.playerTroopsPlaced += this.player.reserve; country.army += this.player.reserve; this.player.reserve = 0; }
- else { this.playerTroopsPlaced += 1; country.army += 1; this.player.reserve -= 1; }
- if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve;
- if (e.target.nextElementSibling) e.target.nextElementSibling.textContent = country.army;
- actionFired = true;
- }
- })
+ const clickedCountry = this.countries.find(c => c.name === e.target.id);
- if(actionFired){
+ if (!clickedCountry || clickedCountry.owner !== this.player.name) return;
+
+ // --- PERK LOGIC: Brotherhood of Steel ('prydwen_deployment') ---
+ if (this.perksEnabled && this.player.perk?.id === 'prydwen_deployment' && this.player.airborneTroops > 0) {
+ // Check if the clicked territory is a valid border
+ if (this.isContinentBorder(clickedCountry.name)) {
+ const troopsToDeploy = e.shiftKey ? this.player.airborneTroops : 1;
+
+ this.player.airborneTroops -= troopsToDeploy;
+ clickedCountry.army += troopsToDeploy;
+
+ await this.logAction(`🚁 Deployed ${troopsToDeploy} airborne troop(s) to the contested border at ${formatTerritoryName(clickedCountry.name)}.`);
+ actionFired = true;
+ } else {
+ if(this.showToast) this.showToast("Airborne troops can only be deployed to territories on a continent border.", "red");
+ return; // Stop if the deployment is invalid
+ }
+ }
+ // --- Standard Deployment Logic ---
+ else if (this.player.reserve > 0) {
+ const troopsToDeploy = e.shiftKey ? this.player.reserve : 1;
+
+ this.playerTroopsPlaced += troopsToDeploy;
+ clickedCountry.army += troopsToDeploy;
+ this.player.reserve -= troopsToDeploy;
+
+ actionFired = true;
+ }
+
+ if (actionFired) {
+ if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve;
+ if (e.target.nextElementSibling) e.target.nextElementSibling.textContent = clickedCountry.army;
+
this.updateInfo();
- if(this.player.reserve === 0){
- await this.logAction(`${this.player.name} deployed ${this.playerTroopsPlaced} fresh troops across their sectors.`);
+
+ // Check if all troops (both reserve and airborne) are deployed
+ const allTroopsDeployed = this.player.reserve === 0 && (this.player.airborneTroops === 0 || !this.player.airborneTroops);
+
+ if (allTroopsDeployed) {
+ await this.logAction(`${this.player.name} deployed all available reinforcements.`);
this.playerTroopsPlaced = 0;
this.stage = "Battle";
- // CRITICAL FIX: Unlock the End button
let endBtn = document.getElementById('end');
if (endBtn) {
endBtn.style.opacity = "1";
endBtn.style.pointerEvents = "auto";
}
-
if (turnInfo) turnInfo.textContent = "Combat Phase";
if (turnInfoMessage) turnInfoMessage.textContent = "Select staging territory, then target an enemy.";
this.updateButtonText();
this.updateInfo();
}
}
-}
+};
Gamestate.vatsTargeting = async function(attackerEl, defenderEl) {
let turbo = turboToggle && turboToggle.checked; if (turbo) return;
@@ -4109,6 +5046,13 @@ Gamestate.vatsTargeting = async function(attackerEl, defenderEl) {
}
Gamestate.attack = async function(e){
+ // --- PERK: Check for Mojave Brotherhood Lockdown ---
+ if (this.perksEnabled && this.prevCountry && this.prevCountry.isLockedDown) {
+ if(this.showToast) this.showToast("Cannot attack: Territory is under Elder's Edict lockdown.", "red");
+ this.prevCountry = null; this.prevTarget = null;
+ return;
+ }
+
if(this.prevTarget) this.prevTarget.classList.remove('flash');
let country = this.countries.find(c => c.name === e.target.id);
if (!country || country.isCrater) return;
@@ -4116,10 +5060,8 @@ Gamestate.attack = async function(e){
// WHEN YOU CLICK YOUR FIRST TERRITORY (Selecting the attacker)
if (!this.prevCountry) {
if (country.owner === this.player.name) {
-
// --- NEW: THE INTERNAL PURGE (Hunting trespassers on your land) ---
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) {
if (this.showToast) this.showToast("Garrison too weak! Need at least 2 troops to execute a Purge.", "red");
@@ -4129,69 +5071,55 @@ Gamestate.attack = async function(e){
if (this.showToast) this.showToast("Target has already evaded a tactical strike this turn!", "red");
e.target.classList.add('flash'); this.prevTarget = e.target; this.prevCountry = country; return;
}
-
let choice = await this.showTacticalModal(trespasser.name, formatTerritoryName(country.name), true);
if (choice === "ambush") {
trespasser.commander.wasAttacked = true;
trespasser.commander.hasBeenAmbushed = true; // LIMIT TO 1 PER ROUND
if (map) map.style.pointerEvents = "none";
-
let atkForce = country.army;
-
// MASSIVELY NERFED DAMAGE TO VIP: Troops only do 0.5 to 1.0 damage each
let dmgToVip = Math.floor(atkForce * (Math.random() * 0.5 + 0.5));
if (dmgToVip < 1) dmgToVip = 1; // Always do at least 1 damage
-
// MASSIVE RETALIATION: Commander always kills 2 to 5 troops in self-defense
- let retaliation = Math.floor(Math.random() * 4) + 2;
-
+ let retaliation = Math.floor(Math.random() * 4) + 2;
trespasser.commander.hp -= dmgToVip;
country.army -= retaliation;
if(country.army < 1) country.army = 1; // Minimum garrison remains
-
let mapEl = document.getElementById(country.name);
if(mapEl && mapEl.nextElementSibling) mapEl.nextElementSibling.textContent = country.army;
-
await this.logAction(`[ INTERNAL PURGE ] Garrison hunted down ${trespasser.name}'s Commander! Dealt ${dmgToVip} DMG. Lost ${retaliation} troops to return fire.`, true);
if (trespasser.commander.hp <= 0) await this.killCommander(trespasser);
-
// CRITICAL LOOP FIX: Clear the selection so the player can't spam click!
- this.prevCountry = null;
+ this.prevCountry = null;
this.prevTarget = null;
if (e.target) e.target.classList.remove('flash');
-
this.updateInfo();
if (map) map.style.pointerEvents = "auto";
- return;
+ return;
} else if (choice === "cancel") {
return;
}
}
-
e.target.classList.add('flash'); this.prevTarget = e.target; this.prevCountry = country;
}
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) ---
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"; // Defaults to normal combat if no VIP is present
-
// If an enemy Commander is hiding here, enforce the new territory rules!
if (this.commandersEnabled && strandedCmdr) {
let isCmdrOnOwnLand = (country.owner === strandedCmdr.name);
let isCmdrOnAllyLand = this.areAllies(strandedCmdr.name, country.owner);
if (isCmdrOnOwnLand || isCmdrOnAllyLand) {
// Protected by territory or allies! Must assault territory first.
- attackChoice = "assault";
+ attackChoice = "assault";
} else if (strandedCmdr.commander.hasBeenAmbushed) {
// Protected by the once-per-round limit!
- attackChoice = "assault";
+ attackChoice = "assault";
} else {
attackChoice = await this.showTacticalModal(strandedCmdr.name, formatTerritoryName(country.name));
if (attackChoice === "cancel") {
@@ -4199,7 +5127,6 @@ Gamestate.attack = async function(e){
}
}
}
-
// BRANCH 1: THE ASSASSINATION (Suppressive Fire)
if (attackChoice === "ambush") {
if (this.prevCountry.army <= 1) {
@@ -4207,58 +5134,66 @@ Gamestate.attack = async function(e){
this.prevCountry = null; this.prevTarget = null; return;
}
strandedCmdr.commander.wasAttacked = true;
+ // --- PERK LOGIC: Wasteland Raiders ('chem_frenzy') ---
+ if (this.perksEnabled && this.player.perk?.id === 'chem_frenzy' && this.player.canUseChemFrenzy && this.prevCountry.army > 1) {
+ const useFrenzy = await this.showFrenzyModal(formatTerritoryName(country.name));
+ if (useFrenzy) {
+ this.prevCountry.army -= 1;
+ this.player.army -= 1;
+ player.usedChemFrenzy = true;
+ }
+ }
strandedCmdr.commander.hasBeenAmbushed = true; // LIMIT TO 1 PER ROUND
let attackerMap = document.getElementById(this.prevCountry.name);
let defenderMap = document.getElementById(country.name);
if (map) map.style.pointerEvents = "none";
await this.vatsTargeting(attackerMap, defenderMap);
-
let atkForce = this.prevCountry.army - 1;
-
// MASSIVELY NERFED DAMAGE TO VIP: Troops only do 1 to 2 damage each
let dmgToVip = Math.floor(atkForce * (Math.random() * 1 + 2));
if (dmgToVip < 1) dmgToVip = 1; // Always do at least 1 damage
-
// MASSIVE RETALIATION: Commander always kills 2 to 5 troops in self-defense
- let retaliation = Math.floor(Math.random() * 4) + 2;
-
+ let retaliation = Math.floor(Math.random() * 4) + 2;
strandedCmdr.commander.hp -= dmgToVip;
this.prevCountry.army -= retaliation;
if(this.prevCountry.army < 1) this.prevCountry.army = 1;
-
if(attackerMap && attackerMap.nextElementSibling) attackerMap.nextElementSibling.textContent = this.prevCountry.army;
-
await this.logAction(`[ SUPPRESSIVE FIRE ] ${this.player.name} ambushed ${strandedCmdr.name}'s stranded Commander! Dealt ${dmgToVip} DMG. Lost ${retaliation} troops to return fire.`, true);
-
if (strandedCmdr.commander.hp <= 0) await this.killCommander(strandedCmdr);
this.updateInfo();
if (map) map.style.pointerEvents = "auto";
-
// Clear selection here to force user to re-select
- this.prevCountry = null;
+ this.prevCountry = null;
this.prevTarget = null;
if (attackerMap) attackerMap.classList.remove('flash');
return; // Ends the attack early so the territory isn't flipped!
}
-
// BRANCH 2: THE INVASION (Standard Combat)
if (this.areAllies(this.player.name, country.owner)) {
let confirmBetrayal = await this.showBetrayalModal(country.owner);
- if(!confirmBetrayal) { this.prevTarget = null; this.prevCountry = null; return; }
+ if(!confirmBetrayal) { this.prevTarget = null; this.prevCountry = null; return; }
else { this.breakTruce(this.player.name, country.owner); }
}
-
+ // --- PERK LOGIC: Wasteland Raiders ('chem_frenzy') ---
+ if (this.perksEnabled && this.player.perk?.id === 'chem_frenzy' && this.player.canUseChemFrenzy && this.prevCountry.army > 1) {
+ const useFrenzy = await this.showFrenzyModal(formatTerritoryName(country.name));
+ if (useFrenzy) {
+ // Activate the perk for the upcoming battle
+ this.prevCountry.army -= 1; // Sacrifice one troop
+ this.player.army -= 1;
+ // The 'canUseChemFrenzy' flag will be checked in the battle function
+ }
+ // If they cancel, the attack proceeds as normal.
+ }
let attackerMap = document.getElementById(this.prevCountry.name); let defenderMap = document.getElementById(country.name);
if (map) map.style.pointerEvents = "none";
-
// Meat Grinder pre-strike
if(this.nukesEnabled && country.isSilo && country.siloTurns > 0) {
let defBuff = Math.min(0.80, country.siloTurns * 0.20);
- if(this.activeNuke && this.activeNuke.launcher === country.owner) defBuff = 0;
+ if(this.activeNuke && this.activeNuke.launcher === country.owner) defBuff = 0;
if(country.owner === "Wasteland Horrors") defBuff = 0; /* GHOUL NERF */
-
if(defBuff > 0) {
- let meatGrinder = Math.floor((this.prevCountry.army - 1) * (defBuff / 2));
+ let meatGrinder = Math.floor((this.prevCountry.army - 1) * (defBuff / 2));
if(meatGrinder > 0) {
this.prevCountry.army -= meatGrinder; this.player.army -= meatGrinder;
if(attackerMap && attackerMap.nextElementSibling) attackerMap.nextElementSibling.textContent = this.prevCountry.army;
@@ -4266,127 +5201,161 @@ Gamestate.attack = async function(e){
}
}
}
-
if(this.prevCountry.army > 1) {
- await this.vatsTargeting(attackerMap, defenderMap); await this.battle(this.prevCountry, country, this.player, 0);
+ await this.vatsTargeting(attackerMap, defenderMap); await this.battle(this.prevCountry, country, this.player, 0);
} else { await this.logAction(`ASSAULT FAILED: Attacking force decimated by automated defenses.`); }
-
if (map) map.style.pointerEvents = "auto";
-
} else if (this.prevCountry.army === 1) {
this.updateInfo(); await this.logAction("Cannot attack: A minimum garrison of 1 troop must remain in the territory.", true);
}
}
-
this.prevCountry = null; this.prevTarget = null;
}
-Gamestate.maneuver = async function(e){
- if(this.prevTarget){ this.prevTarget.classList.remove('flash'); }
+
+Gamestate.maneuver = async function(e) {
+ if (this.prevTarget) {
+ this.prevTarget.classList.remove('flash');
+ }
let country = this.countries.find(c => c.name === e.target.id);
- if(!country || country.isCrater) return;
+ if (!country || country.isCrater) return;
- if(this.stage === "Commander Phase") {
- if(!this.player.commander || this.player.commander.ap <= 0) return;
+ // --- Commander Phase Logic (No changes here) ---
+ if (this.stage === "Commander Phase") {
+ if (!this.player.commander || this.player.commander.ap <= 0) return;
let cmdrLoc = this.countries.find(c => c.name === this.player.commander.loc);
- if(!cmdrLoc) return;
-
- if(cmdrLoc.neighbours.includes(country.name)) {
+ if (!cmdrLoc) return;
+ if (cmdrLoc.neighbours.includes(country.name)) {
let enemyCmdr = this.players.find(p => p !== this.player && p.alive && !p.isNeutral && p.commander && p.commander.loc === country.name);
-
- if(enemyCmdr) {
+ if (enemyCmdr) {
if (this.player.commander.hasFought) {
- let msg = "Commander is exhausted and cannot duel again this turn!";
- if(typeof this.showToast === 'function') { this.showToast(msg, "red"); } else { console.log(msg); }
+ if (this.showToast) this.showToast("Commander is exhausted and cannot duel again this turn!", "red");
return;
}
-
this.player.commander.ap -= 1;
await this.logAction(`[ REGICIDE DUEL ] Commander initiated direct combat with ${enemyCmdr.name}'s Commander!`, true);
-
- // 3. Combat Math (Safe Home Advantage: 5-15 DMG Max)
- let rawDmgToEnemy = Math.floor(Math.random() * 16) + 10;
- let rawDmgToSelf = Math.floor(Math.random() * 11) + 10;
-
- if (cmdrLoc.owner === this.player.name) { rawDmgToSelf = Math.floor(Math.random() * 11) + 5; }
-
- let cappedDmgToEnemy = Math.min(25, rawDmgToEnemy);
+ let rawDmgToEnemy = Math.floor(Math.random() * 16) + 10;
+ let rawDmgToSelf = Math.floor(Math.random() * 11) + 10;
+ if (cmdrLoc.owner === this.player.name) rawDmgToSelf = Math.floor(Math.random() * 11) + 5;
+ let cappedDmgToEnemy = Math.min(25, rawDmgToEnemy);
let cappedDmgToSelf = Math.min(25, rawDmgToSelf);
-
- enemyCmdr.commander.hp -= cappedDmgToEnemy;
+ enemyCmdr.commander.hp -= cappedDmgToEnemy;
this.player.commander.hp -= cappedDmgToSelf;
- this.player.commander.hasFought = true;
+ this.player.commander.hasFought = true;
enemyCmdr.commander.wasAttacked = true;
this.player.commander.wasAttacked = true;
-
await this.logAction(`DUEL: Dealt ${cappedDmgToEnemy} DMG. Received ${cappedDmgToSelf} DMG in return.`);
-
- if(enemyCmdr.commander.hp <= 0) await this.killCommander(enemyCmdr);
- if(this.player.commander.hp <= 0) await this.killCommander(this.player);
-
- this.updateInfo();
- } else {
- this.player.commander.loc = country.name; this.player.commander.ap -= 1;
- this.player.commander.siegeTurns = 0; // Reset subversion timer on move
-
- let moveFlavor = country.owner === this.player.name ? "relocated to" : "infiltrated enemy lines at";
- if (country.owner === "none" || country.owner === "Wasteland Horrors") moveFlavor = "ventured into the wild wastes of";
-
- await this.logAction(`COMMANDER MOVEMENT: Commander ${moveFlavor} ${formatTerritoryName(country.name)}.`);
- this.updateInfo();
+ if (enemyCmdr.commander.hp <= 0) await this.killCommander(enemyCmdr);
+ if (this.player.commander.hp <= 0) await this.killCommander(this.player);
+ } else {
+ this.player.commander.loc = country.name;
+ this.player.commander.ap -= 1;
+ this.player.commander.siegeTurns = 0;
+ await this.logAction(`COMMANDER MOVEMENT: Commander relocated to ${formatTerritoryName(country.name)}.`);
}
+ this.updateInfo();
}
return;
}
- if(country.owner !== this.player.name) return;
- let targetEl = document.getElementById(country.name); if(targetEl) { targetEl.classList.add('flash'); this.prevTarget = targetEl; }
+ // --- NEW UNIFIED MANEUVER LOGIC ---
- if(this.prevCountry){
- if(this.prevCountry.name !== country.name && this.prevCountry.owner === this.player.name){
- if (this.maneuverSource && this.maneuverDest) {
- let valid1 = (this.prevCountry.name === this.maneuverSource && country.name === this.maneuverDest);
- let valid2 = (this.prevCountry.name === this.maneuverDest && country.name === this.maneuverSource);
- if (!valid1 && !valid2) { return; }
- }
+ // 1. SELECTING A SOURCE TERRITORY
+ if (!this.prevCountry) {
+ if (country.owner !== this.player.name) return; // Can't start from a non-player territory
+ if (country.isLockedDown) {
+ if (this.showToast) this.showToast("Maneuver blocked: Territory is under Elder's Edict lockdown.", "red");
+ return;
+ }
+ if (country.army <= 1) {
+ if (this.showToast) this.showToast("Maneuver blocked: Must have more than 1 troop to move.", "red");
+ return;
+ }
+ // If all checks pass, select this as the source
+ this.prevCountry = country;
+ let targetEl = document.getElementById(country.name);
+ if (targetEl) {
+ targetEl.classList.add('flash');
+ this.prevTarget = targetEl;
+ }
+ return; // Wait for player to select a destination
+ }
- if(this.prevCountry.neighbours.includes(country.name) && this.prevCountry.army > 1){
- this.maneuverSource = this.prevCountry.name;
- this.maneuverDest = country.name;
+ // 2. A SOURCE IS ALREADY SELECTED, NOW SELECTING A DESTINATION
+ if (this.prevCountry.name === country.name) {
+ // Player clicked the same territory again, de-select it
+ this.prevCountry = null;
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ this.prevTarget = null;
+ return;
+ }
- let maxMove = this.prevCountry.army - 1; let moveAmount = 1;
-
- if (e.shiftKey) { moveAmount = maxMove; }
- else {
- let input = await this.showManeuverModal(1, maxMove, formatTerritoryName(country.name));
- if (input === null) {
- if (targetEl) targetEl.classList.remove('flash');
- this.prevTarget = document.getElementById(this.prevCountry.name);
- if (this.prevTarget) this.prevTarget.classList.add('flash');
- this.maneuverSource = null; this.maneuverDest = null;
- return;
+ // 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");
+ return;
+ }
+
+ let path = this.findShortestPath(this.prevCountry.name, country.name);
+ if (path) {
+ let cost = path.length - 1;
+ if (this.player.maneuverPoints >= cost) {
+ let maxMove = this.prevCountry.army - 1;
+ let moveAmount = e.shiftKey ? maxMove : (await this.showManeuverModal(1, maxMove, formatTerritoryName(country.name)));
+
+ if (moveAmount !== null) {
+ country.army += moveAmount;
+ this.prevCountry.army -= moveAmount;
+ this.player.maneuverPoints -= cost;
+
+ await this.logAction(`Relocated ${moveAmount} troops to ${formatTerritoryName(country.name)}. (${cost} moves used).`);
+
+ if (this.player.maneuverPoints <= 0) {
+ this.handleEndTurn();
+ } else {
+ if (turnInfoMessage) turnInfoMessage.textContent = `Move troops. You have ${this.player.maneuverPoints} moves remaining.`;
+ this.updateInfo();
}
- moveAmount = input;
- }
+ }
+ } else {
+ if (this.showToast) this.showToast(`Path requires ${cost} moves, but you only have ${this.player.maneuverPoints}.`, "red");
+ }
+ }
+ // Reset selection after every attempt for The Railroad
+ this.prevCountry = null;
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ this.prevTarget = null;
+ return;
+ }
+
+ // STANDARD PLAYER LOGIC (Non-Railroad)
+ if (country.owner === this.player.name && this.prevCountry.neighbours.includes(country.name)) {
+ if (this.prevCountry.army > 1) {
+ let maxMove = this.prevCountry.army - 1;
+ let moveAmount = e.shiftKey ? maxMove : (await this.showManeuverModal(1, maxMove, formatTerritoryName(country.name)));
+
+ if (moveAmount !== null) {
country.army += moveAmount;
this.prevCountry.army -= moveAmount;
-
- let sourceMap = document.getElementById(`${this.prevCountry.name}`); let destMap = document.getElementById(`${country.name}`);
- if (sourceMap && sourceMap.nextElementSibling) sourceMap.nextElementSibling.textContent = this.prevCountry.army;
- if (destMap && destMap.nextElementSibling) destMap.nextElementSibling.textContent = country.army;
-
- if(this.commandersEnabled && this.player.commander && this.player.commander.loc === this.prevCountry.name && e.shiftKey) {
- this.player.commander.loc = country.name;
- this.player.commander.siegeTurns = 0; // Reset subversion timer
- this.logAction(`Commander escorted to ${formatTerritoryName(country.name)}.`);
- }
- this.updateInfo();
- }
- }
+ this.player.maneuverPoints = 0; // Standard players only get one move
+ this.handleEndTurn();
+ } else {
+ // If they cancel the modal, reset selection
+ this.prevCountry = null;
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ this.prevTarget = null;
+ }
+ }
+ } else {
+ // Invalid move for standard players, so reset selection
+ this.prevCountry = null;
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
+ this.prevTarget = null;
}
- this.prevCountry = country;
-}
+};
+
Gamestate.useStimpak = async function() {
if(this.stage !== "Commander Phase" || !this.commandersEnabled || !this.player.commander) return;
@@ -4646,41 +5615,33 @@ Gamestate.processRadDecay = async function() {
if(decayActive) this.updateInfo();
}
-Gamestate.aiMove = async function(){
- if(this.gameOver) return;
- if(this.prevTarget) this.prevTarget.classList.remove('flash');
-
- let hasAiMadeADeal = false; // <-- ADD THIS LINE
+Gamestate.aiMove = async function() {
+ if (this.gameOver) return;
+ if (this.prevTarget) this.prevTarget.classList.remove('flash');
- this.stage = "AI Turn"; this.updateButtonText(); if (turnInfoMessage) turnInfoMessage.textContent = "";
-
-for(let i = 1; i <= this.players.length; i++){
- // FIX: The end-of-day logic must check if i matches length, then STOP.
- 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;
- // Wait for the next frame to avoid recursion depth issues
- setTimeout(() => this.aiMove(), 100);
+ let hasAiMadeADeal = false;
+ this.stage = "AI Turn";
+ this.updateButtonText();
+ if (turnInfoMessage) turnInfoMessage.textContent = "";
+
+ for (let i = 1; i <= this.players.length; i++) {
+ 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);
return;
}
-
- // At the start of the new day, handle all Bobblehead status updates.
if (this.bobbleheads) {
this.bobbleheads.forEach(b => {
- // 1. Deactivate any that were active.
if (b.active) {
b.active = false;
if (this.logAction) this.logAction(`\[ BOBBLEHEAD \] The effect of the ${b.name} has worn off.`);
}
- // 2. Decrease the cooldown timer for any item that is on cooldown.
if (b.cooldown > 0) {
b.cooldown--;
}
});
}
-
-
-// --- COMMANDER SUBVERSION (SIEGE) LOGIC ---
for (let p of this.players) {
if (this.commandersEnabled && p.alive && p.commander && p.commander.hp > 0) {
let locCountry = this.countries.find(c => c.name === p.commander.loc);
@@ -4694,245 +5655,294 @@ for(let i = 1; i <= this.players.length; i++){
if (idx > -1) oldOwner.areas.splice(idx, 1);
oldOwner.army -= locCountry.army;
}
- locCountry.owner = p.name; locCountry.color = p.color;
- p.areas.push(locCountry.name); p.army += locCountry.army;
+ locCountry.owner = p.name;
+ locCountry.color = p.color;
+ p.areas.push(locCountry.name);
+ p.army += locCountry.army;
p.commander.siegeTurns = 0;
- if (Gamestate.logAction) Gamestate.logAction(`[ SUBVERSION ] ${p.name}'s Commander incited a rebellion! ${formatTerritoryName(locCountry.name)} has flipped!`, true);
+ if (Gamestate.logAction) Gamestate.logAction(`\[ SUBVERSION \] ${p.name}'s Commander incited a rebellion! ${formatTerritoryName(locCountry.name)} has flipped!`, true);
}
}
- } else { p.commander.siegeTurns = 0; }
- p.commander.wasAttacked = false;
+ } else {
+ p.commander.siegeTurns = 0;
+ }
+ p.commander.wasAttacked = false;
}
}
-
-this.turn += 1;
+ if (this.perksEnabled) {
+ if (this.player && this.player.perk) {
+ if (this.player.perk.id === 'chem_frenzy') {
+ this.player.canUseChemFrenzy = true;
+ } else if (this.player.perk.id === 'prydwen_deployment') {
+ this.player.airborneTroops = 3;
+ await this.logAction("The Prydwen has dispatched 3 airborne troops. They can only be deployed to continent borders.");
+ }
+ if (this.player.perk.id === 'mysterious_stranger' && this.player.strangerCooldown > 0) {
+ this.player.strangerCooldown--;
+ }
+ this.player.usedChemFrenzy = false;
+ this.player.usedMemoryDen = false;
+ }
+ this.players.forEach(p => {
+ if (p.perk?.id === 'elders_edict' && p.lockdownCooldown > 0) {
+ p.lockdownCooldown--;
+ }
+ });
+ const lockedCountry = this.countries.find(c => c.isLockedDown);
+ if (lockedCountry) {
+ if (lockedCountry.lockdownTimer === undefined) {
+ lockedCountry.lockdownTimer = 3;
+ } else {
+ lockedCountry.lockdownTimer--;
+ }
+ if (lockedCountry.lockdownTimer <= 0) {
+ lockedCountry.isLockedDown = false;
+ let owner = this.players.find(p => p.name === lockedCountry.owner);
+ if (owner) {
+ owner.lockdownCooldown = 3;
+ }
+ await this.logAction(`LOCKDOWN EXPIRED: The Elder's Edict has expired for ${formatTerritoryName(lockedCountry.name)}.`);
+ let el = document.getElementById(lockedCountry.name);
+ if (el) el.classList.remove('lockdown-territory');
+ }
+ }
+ }
+ this.turn += 1;
this.aiTurn = false;
await this.logAction(`--- DAY ${this.turn} BEGINS ---`, true);
-
- if(this.diplomacy.spiteTurns > 0) {
- this.diplomacy.spiteTurns--;
- if(this.diplomacy.spiteTurns <= 0) {
- this.diplomacy.spiteTarget = null; await this.logAction("[ DIPLOMACY ] The Spite Alliance has dissolved. Borders return to normal.", true);
- }
- }
-
- for(let t = this.diplomacy.truces.length - 1; t >= 0; t--) {
- this.diplomacy.truces[t].turns--;
- if(this.diplomacy.truces[t].turns <= 0) {
- let truce = this.diplomacy.truces[t]; await this.logAction(`[ DIPLOMACY ] Ceasefire expired between ${truce.f1} and ${truce.f2}. Borders are hot.`, true); this.diplomacy.truces.splice(t, 1);
- }
- }
-
- await this.processRadstorm();
- if(this.nukesEnabled) { await this.resolveNuke(); await this.processRadDecay(); this.countries.forEach(c => { if(c.isSilo && c.owner !== "none") c.siloTurns++; }); }
- if(this.commandersEnabled && this.player.alive && this.player.commander) {
- let loc = this.countries.find(c => c.name === this.player.commander.loc);
- if(loc && loc.owner === this.player.name) { let regen = loc.isSilo ? 4 : 2; this.player.commander.hp = Math.min(100, this.player.commander.hp + regen); }
- }
-
- this.stage = "Fortify"; this.updateButtonText(); if (turnInfoMessage) turnInfoMessage.textContent = "Deploy reserve troops to your territories.";
- let bonus = this.unitBonus(this.player, 0); this.player.reserve += bonus; this.player.army += bonus;
- if (map) map.style.pointerEvents = "auto";
- if (infoName[i-1]) infoName[i-1].parentElement.classList.remove('highlight'); if (infoName[0]) infoName[0].parentElement.classList.add('highlight');
- if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve; this.updateInfo(); return;
- }
-
-if (infoName[i-1]) infoName[i-1].parentElement.classList.remove('highlight');
- if(this.players[i] && this.players[i].alive){
+ if (this.diplomacy.spiteTurns > 0) {
+ this.diplomacy.spiteTurns--;
+ if (this.diplomacy.spiteTurns <= 0) {
+ this.diplomacy.spiteTarget = null;
+ await this.logAction("\[ DIPLOMACY \] The Spite Alliance has dissolved. Borders return to normal.", true);
+ }
+ }
+ for (let t = this.diplomacy.truces.length - 1; t >= 0; t--) {
+ this.diplomacy.truces[t].turns--;
+ if (this.diplomacy.truces[t].turns <= 0) {
+ let truce = this.diplomacy.truces[t];
+ await this.logAction(`\[ DIPLOMACY \] Ceasefire expired between ${truce.f1} and ${truce.f2}. Borders are hot.`, true);
+ this.diplomacy.truces.splice(t, 1);
+ }
+ }
+ await this.processRadstorm();
+ if (this.nukesEnabled) {
+ await this.resolveNuke();
+ await this.processRadDecay();
+ this.countries.forEach(c => {
+ if (c.isSilo && c.owner !== "none") c.siloTurns++;
+ });
+ }
+ if (this.commandersEnabled && this.player.alive && this.player.commander) {
+ let loc = this.countries.find(c => c.name === this.player.commander.loc);
+ if (loc && loc.owner === this.player.name) {
+ let regen = loc.isSilo ? 4 : 2;
+ this.player.commander.hp = Math.min(100, this.player.commander.hp + regen);
+ }
+ }
+if (this.wastelandEconomyActive) {
+ this.stage = "Income";
+ this.updateButtonText();
+ if (turnInfoMessage) turnInfoMessage.textContent = "Collect your taxes from the wasteland.";
+
+ let income = this.player.areas.length * 2;
+ let continentIncome = 0;
+ 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; }
+ });
+ income += continentIncome;
+ this.player.caps += income;
+
+ await this.logAction(`TAXES COLLECTED: Gained ${income} Caps from territories and continent bonuses.`, true);
+ await this.showRecruitmentModal();
+ this.stage = "Fortify";
+} else {
+ this.stage = "Fortify";
+ let bonus = this.unitBonus(this.player, 0);
+ this.player.reserve += bonus;
+ this.player.army += bonus;
+}
+
+this.updateButtonText();
+if (turnInfoMessage) turnInfoMessage.textContent = "Deploy reserve troops to your territories.";
+
+ if (this.perksEnabled && this.player.perk && this.player.perk.id === 'tribute_chest') {
+ let continentsOwned = 0;
+ continents.forEach(continent => {
+ if (continent.areas.every(area => this.player.areas.includes(area) && !this.countries.find(c => c.name === area).isCrater)) {
+ continentsOwned++;
+ }
+ });
+ if (continentsOwned > 0) {
+ for (let k = 0; k < continentsOwned; k++) {
+ this.player.cards.push({
+ country: "Vassal Tribute",
+ type: "Wild"
+ });
+ }
+ await this.logAction(`Nuka-Raiders demand tribute! Gained +${continentsOwned} Bottle Cap(s) from controlled continents.`);
+ }
+ }
+ if (map) map.style.pointerEvents = "auto";
+ if (infoName[i - 1]) infoName[i - 1].parentElement.classList.remove('highlight');
+ if (infoName[0]) infoName[0].parentElement.classList.add('highlight');
+ if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve;
+
+ // --- CORRECTED PLACEMENT FOR ENCOUNTER TRIGGER ---
+ await this.triggerEncounterCheck('start_of_turn');
+
+ this.updateInfo();
+ return;
+ }
+
+ if (infoName[i - 1]) infoName[i - 1].parentElement.classList.remove('highlight');
+ if (this.players[i] && this.players[i].alive) {
if (this.players[i].isNeutral) {
if (this.players[i].name === "Wasteland Horrors") {
let ownedAreas = this.countries.filter(c => c.owner === this.players[i].name);
if (ownedAreas.length > 0) {
let totalSpawned = 0;
ownedAreas.forEach(c => {
- if (this.difficulty === "Hard") { let spawn = Math.floor(Math.random() * 5) + 2; c.army += spawn; this.players[i].army += spawn; totalSpawned += spawn; }
- else if (this.difficulty === "Normal" && Math.random() < 0.30) { let spawn = Math.floor(Math.random() * 3) + 1; c.army += spawn; this.players[i].army += spawn; totalSpawned += spawn; }
+ if (this.difficulty === "Hard") {
+ let spawn = Math.floor(Math.random() * 5) + 2;
+ c.army += spawn;
+ this.players[i].army += spawn;
+ totalSpawned += spawn;
+ } else if (this.difficulty === "Normal" && Math.random() < 0.30) {
+ let spawn = Math.floor(Math.random() * 3) + 1;
+ c.army += spawn;
+ this.players[i].army += spawn;
+ totalSpawned += spawn;
+ }
});
- if (totalSpawned > 0 && Gamestate.logAction) { Gamestate.logAction(`[ WARNING ] The Wasteland Horrors are multiplying... (+${totalSpawned} hostiles detected).`, true); }
+ if (totalSpawned > 0 && Gamestate.logAction) {
+ Gamestate.logAction(`\[ WARNING \] The Wasteland Horrors are multiplying... (+${totalSpawned} hostiles detected).`, true);
+ }
}
}
- this.updateInfo(); continue;
+ this.updateInfo();
+ continue;
}
-
if (infoName[i]) infoName[i].parentElement.classList.add('highlight')
let turbo = turboToggle && turboToggle.checked;
-
- if (Math.random() < 0.20) { let randomEvent = wastelandEncounters[Math.floor(Math.random() * wastelandEncounters.length)]; await this.logAction(`SYSTEM: ${randomEvent}`); }
-
- // --- NEW: AI ITEM USAGE LOGIC ---
-
+ if (Math.random() < 0.20) {
+ let randomEvent = wastelandEncounters[Math.floor(Math.random() * wastelandEncounters.length)];
+ await this.logAction(`SYSTEM: ${randomEvent}`);
+ }
let aiPlayer = this.players[i];
-
- // 1. STIMPAK USAGE: If commander is below 60% health and has a stimpak, use it.
if (this.commandersEnabled && aiPlayer.commander && aiPlayer.commander.hp < 60 && aiPlayer.commander.stimpaks > 0) {
aiPlayer.commander.stimpaks--;
aiPlayer.commander.hp = Math.min(100, aiPlayer.commander.hp + 20);
if (!turbo) await this.logAction(`\[ MEDICAL \] ${aiPlayer.name}'s Commander injected a Stimpak in the field.`);
}
-
- // 2. BOBBLEHEAD USAGE: AI decides if using a Bobblehead is a good idea.
if (this.bobbleheads) {
- // Check for STRENGTH bobblehead: Use if planning a major attack.
let strengthBobble = this.bobbleheads.find(b => b.key === 's');
if (strengthBobble && strengthBobble.found && strengthBobble.cooldown === 0) {
- // Is the AI feeling aggressive and stronger than its weakest neighbor?
let weakestNeighbor = this.getWeakestNeighbor(aiPlayer);
if (weakestNeighbor && aiPlayer.army > weakestNeighbor.army) {
strengthBobble.cooldown = strengthBobble.totalCooldown;
- strengthBobble.active = true; // This will be used by the battle logic
+ strengthBobble.active = true;
if (!turbo) await this.logAction(`\[ INTEL \] ${aiPlayer.name} is preparing a major offensive, activating their STRENGTH BOBBLEHEAD.`);
}
}
-
- // Check for INTELLIGENCE bobblehead: Use if reputation with player is low.
let intelBobble = this.bobbleheads.find(b => b.key === 'i');
if (intelBobble && intelBobble.found && intelBobble.cooldown === 0) {
let repWithPlayer = this.diplomacy.reputation[aiPlayer.name]?.[this.player.name] || 0;
- if (repWithPlayer < 10) { // If they don't like the player, they want intel.
+ if (repWithPlayer < 10) {
intelBobble.cooldown = intelBobble.totalCooldown;
- // No 'active' flag needed, this is for the AI's internal logic only.
if (!turbo) await this.logAction(`\[ INTEL \] ${aiPlayer.name} is gathering intelligence on global power levels.`);
}
}
}
- // --- END OF AI ITEM USAGE ---
-
-
-// AI Nuke Logic
- if(this.nukesEnabled && this.players[i].codes >= 4 && !this.activeNuke) {
+ if (this.nukesEnabled && this.players[i].codes >= 4 && !this.activeNuke) {
let silos = this.countries.filter(c => c.isSilo && c.owner === this.players[i].name);
- if(silos.length > 0) {
- let target = this.countries.slice().sort((a,b) => b.army - a.army).find(c => c.owner !== this.players[i].name && !c.isCrater);
- if(target) {
- // FIX: AI calculates its best silo and triggers the centralized launch function
- let launchSite = silos.sort((a,b) => b.siloTurns - a.siloTurns)[0];
+ if (silos.length > 0) {
+ let target = this.countries.slice().sort((a, b) => b.army - a.army).find(c => c.owner !== this.players[i].name && !c.isCrater);
+ if (target) {
+ let launchSite = silos.sort((a, b) => b.siloTurns - a.siloTurns)[0];
this.executeNukeLaunch(this.players[i], target.name, launchSite.name);
}
}
}
+if (this.wastelandEconomyActive) {
+ // AI Wasteland 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; }
+ });
+ income += continentIncome;
+ this.players[i].caps += income;
+ if (!turbo) await this.logAction(`${this.players[i].name} collected ${income} Caps in taxes.`);
- // --- EXPANDED AI DIPLOMACY LOGIC ---
- // --- CORRECTED DIPLOMACY CHANCE LOGIC ---
- // --- REPLACEMENT CODE STARTS HERE ---
+ // AI decides how many troops to buy (they will spend between 50% and 100% of their caps)
+ const troopCost = 5;
+ const spendingPercentage = (Math.random() * 0.5) + 0.5;
+ const affordableTroops = Math.floor(this.players[i].caps / troopCost);
+ const troopsToBuy = Math.floor(affordableTroops * spendingPercentage);
+ const totalCost = troopsToBuy * troopCost;
- // --- PART 1: SMART PLAYER-FOCUSED DIPLOMACY ---
- // This block contains the new tactical logic for when an AI decides to contact YOU.
- // It only runs ONCE per AI round.
- if (Math.random() < 0.33 && !hasAiMadeADeal && !this.justFoundLoot) {
-
- let aiPlayer = this.players[i];
- let repWithPlayer = this.diplomacy.reputation[aiPlayer.name][this.player.name] || 0;
- let hatesPlayer = repWithPlayer <= -35 || (this.diplomacy.grudges[this.player.name] && this.diplomacy.grudges[this.player.name].includes(aiPlayer.name));
+ 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.`);
+ }
- // TACTICAL CHECK 1: Is the AI weak and being threatened by a neighbor?
- let isThreatened = aiPlayer.areas.some(areaName => {
- let country = this.countries.find(c => c.name === areaName);
- return country.neighbours.some(nName => {
- let neighbor = this.countries.find(n => n.name === nName);
- // Is the neighbor a powerful enemy?
- return neighbor && neighbor.owner !== aiPlayer.name && neighbor.owner !== this.player.name && neighbor.army > (country.army * 1.5);
- });
- });
-
- // TACTICAL CHECK 2: Do the AI and Player share a powerful common enemy?
- const aiBorders = new Set(aiPlayer.areas.flatMap(a => this.countries.find(c => c.name === a).neighbours));
- const playerBorders = new Set(this.player.areas.flatMap(a => this.countries.find(c => c.name === a).neighbours));
- const commonBorders = [...aiBorders].filter(b => playerBorders.has(b));
- let hasPowerfulCommonEnemy = commonBorders.some(bName => {
- let borderCountry = this.countries.find(c => c.name === bName);
- return borderCountry && borderCountry.owner !== aiPlayer.name && borderCountry.owner !== this.player.name && borderCountry.army > 10;
- });
-
- // If they don't hate the player AND have a tactical reason, they will contact you.
- if (!hatesPlayer && (isThreatened || hasPowerfulCommonEnemy)) {
- hasAiMadeADeal = true; // Set the flag so no other AI contacts the player this round.
-
- // This is your existing "Desperate Plea" or "Bribe for Truce" logic.
- // It will now only trigger for good reasons.
- if (aiPlayer.areas.length <= 3 && repWithPlayer >= 25 && this.player.reserve >= 5) {
- let granted = await this.showEnvoyModal(aiPlayer.name, 0, 0, aiPlayer.color, true, 5);
- if (granted) {
- this.player.reserve -= 5; aiPlayer.reserve += 5; aiPlayer.army += 5;
- this.diplomacy.reputation[aiPlayer.name][this.player.name] = Math.min(50, repWithPlayer + 15);
- if(!turbo) await this.logAction(`\[ DIPLOMACY \] You sent emergency reinforcements to ${aiPlayer.name}. They will not forget this.`, true);
- } else {
- this.diplomacy.reputation[aiPlayer.name][this.player.name] = Math.max(-50, repWithPlayer - 10);
- if(!turbo) await this.logAction(`\[ DIPLOMACY \] You ignored ${aiPlayer.name}'s plea for help. Their loyalty wavers.`, true);
- }
- } else if (aiPlayer.cards.length > 0 && !this.areAllies(aiPlayer.name, this.player.name)) {
- let offerAmount = Math.min(aiPlayer.cards.length, Math.floor(Math.random() * 3) + 1);
- let offeredCaps = [];
- for (let c = 0; c < offerAmount; c++) { if(aiPlayer.cards.length > 0) offeredCaps.push(aiPlayer.cards.pop()); }
-
- let accept = await this.showEnvoyModal(aiPlayer.name, offerAmount, offerAmount, aiPlayer.color, false, 0);
- if (accept) {
- this.player.cards.push(...offeredCaps);
- this.diplomacy.truces.push({f1: aiPlayer.name, f2: this.player.name, turns: offerAmount});
- this.diplomacy.reputation[aiPlayer.name][this.player.name] = Math.min(50, repWithPlayer + 2);
- if(!turbo) await this.logAction(`\[ DIPLOMACY \] You signed a Ceasefire with ${aiPlayer.name}.`, true);
- } else {
- aiPlayer.cards.push(...offeredCaps);
- this.diplomacy.reputation[aiPlayer.name][this.player.name] = Math.max(-50, repWithPlayer - 2);
- if(!turbo) await this.logAction(`\[ DIPLOMACY \] You rejected ${aiPlayer.name}'s envoy.`, true);
- }
- }
- }
+} 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);
+ const mrHousePlayer = this.players.find(p => this.perksEnabled && p.perk && p.perk.id === 'the_house_always_wins');
+ if (mrHousePlayer && tradeIndices) {
+ const cardsTraded = 3;
+ const houseBonus = Math.floor(cardsTraded / 3);
+ if (houseBonus > 0) {
+ mrHousePlayer.cards.push({ country: "House Always Wins", type: "Wild" });
+ if (!turbo) await this.logAction(`"The House Always Wins..." Mr. House takes a cut of the trade.`);
}
+ }
+ 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.`);
+}
- // --- PART 2: AI-to-AI DIPLOMACY ---
- // This logic runs separately and more frequently, allowing AI to form pacts in the background.
- // Each AI gets a small (10%) chance to try this each turn.
- if (Math.random() < 0.10) {
- let aiPlayer = this.players[i];
- if (aiPlayer.cards.length > 0) {
- let potentialAllies = this.players.filter(p => p !== aiPlayer && p.alive && !p.isNeutral && p.name !== this.player.name && !this.areAllies(aiPlayer.name, p.name) && (!this.diplomacy.grudges[p.name] || !this.diplomacy.grudges[p.name].includes(aiPlayer.name)));
- if(potentialAllies.length > 0) {
- let targetAlly = potentialAllies[Math.floor(Math.random() * potentialAllies.length)];
-
- if ((this.diplomacy.reputation[aiPlayer.name][targetAlly.name] || 0) > -35) {
- let offerAmount = Math.min(aiPlayer.cards.length, Math.floor(Math.random() * 3) + 1);
- for (let c = 0; c < offerAmount; c++) { if(aiPlayer.cards.length > 0) targetAlly.cards.push(aiPlayer.cards.pop()); }
- this.diplomacy.truces.push({f1: aiPlayer.name, f2: targetAlly.name, turns: offerAmount});
- if(!turbo) await this.logAction(`\[ DIPLOMACY \] ${aiPlayer.name} and ${targetAlly.name} signed a ${offerAmount}-Round Pact.`, true);
- }
- }
- }
- }
- // --- REPLACEMENT CODE ENDS HERE ---
-
-
-
-
- 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.`);
-
- let areaToFortify = ["", 0];
+ let areaToFortify = ["", 0];
this.players[i].areas.forEach(area => {
let country = this.countries.find(c => c.name === area);
- if(this.players[i].reserve > 0){
+ if (this.players[i].reserve > 0) {
let ratio = 0;
- if(this.nukesEnabled && country.isSilo) { ratio = 5.0; }
- else {
- let continent = continents.find(x => x.name === country.continent); let count = 0;
- continent.areas.forEach(x => { if (this.players[i].areas.includes(x)) count++; });
+ if (this.nukesEnabled && country.isSilo) {
+ ratio = 5.0;
+ } else {
+ let continent = continents.find(x => x.name === country.continent);
+ let count = 0;
+ continent.areas.forEach(x => {
+ if (this.players[i].areas.includes(x)) count++;
+ });
ratio = count / continent.areas.length;
}
- if (ratio >= areaToFortify[1]){ areaToFortify = [country, ratio] }
+ if (ratio >= areaToFortify[1]) {
+ areaToFortify = [country, ratio]
+ }
}
});
-
- if (areaToFortify[0]) { areaToFortify[0].army += this.players[i].reserve; this.players[i].reserve = 0; }
-
+ if (areaToFortify[0]) {
+ areaToFortify[0].army += this.players[i].reserve;
+ this.players[i].reserve = 0;
+ }
let currentAreas = [...this.players[i].areas];
for (let area of currentAreas) {
let country = this.countries.find(c => c.name === area);
@@ -4940,29 +5950,36 @@ if (infoName[i-1]) infoName[i-1].parentElement.classList.remove('highlight');
await this.aiAttack(country, i, turbo);
}
}
-
this.aiManeuver(i);
-
if (this.players[i].conqueredThisTurn) {
- let newCap = deck.length > 0 ? deck.pop() : { country: "Wasteland Salvage", type: "Wild" };
- this.players[i].cards.push(newCap); this.players[i].conqueredThisTurn = false;
- if (this.nukesEnabled && this.players[i].codes < 4 && this.globalCodes > 0 && Math.random() < 0.15) { this.players[i].codes++; this.globalCodes--; }
- if (this.commandersEnabled && this.players[i].commander && this.players[i].commander.stimpaks < 1 && Math.random() < 0.15) { this.players[i].commander.stimpaks++; }
+ let newCap = deck.length > 0 ? deck.pop() : {
+ country: "Wasteland Salvage",
+ type: "Wild"
+ };
+ this.players[i].cards.push(newCap);
+ this.players[i].conqueredThisTurn = false;
+ if (this.nukesEnabled && this.players[i].codes < 4 && this.globalCodes > 0 && Math.random() < 0.15) {
+ this.players[i].codes++;
+ this.globalCodes--;
+ }
+ if (this.commandersEnabled && this.players[i].commander && this.players[i].commander.stimpaks < 1 && Math.random() < 0.15) {
+ this.players[i].commander.stimpaks++;
+ }
}
-
-if(this.commandersEnabled && this.players[i].commander) {
+ if (this.commandersEnabled && this.players[i].commander) {
let loc = this.countries.find(c => c.name === this.players[i].commander.loc);
- if(loc && loc.owner === this.players[i].name) { let regen = loc.isSilo ? 4 : 2; this.players[i].commander.hp = Math.min(100, this.players[i].commander.hp + regen); }
-
- // FIX: Refill the AI Commander's Action Points every turn!
- this.players[i].commander.ap = 2;
+ if (loc && loc.owner === this.players[i].name) {
+ let regen = loc.isSilo ? 4 : 2;
+ this.players[i].commander.hp = Math.min(100, this.players[i].commander.hp + regen);
+ }
+ this.players[i].commander.ap = 2;
}
-
this.updateInfo();
- }
- }
+ }
+ }
}
+
Gamestate.aiAttack = async function(country, i, turbo){
// --- CORRECTED AI TARGETING LOGIC ---
// The AI can only see and target territories directly adjacent to the attacking country.
@@ -4976,23 +5993,50 @@ Gamestate.aiAttack = async function(country, i, turbo){
}
});
- // This is the beginning of your existing logic for filtering allies, which is still correct.
+ // --- CORRECTED ALLY FILTERING LOGIC ---
+ // This will now correctly prevent the AI from attacking allies.
possibleTargets = possibleTargets.filter(poss => {
- if(!this.areAllies(this.players[i].name, poss.owner)) return true;
- let isWildcard = ["Raiders", "Super Mutants", "Great Khans", "The Fiends"].includes(this.players[i].country);
- let backstabChance = isWildcard ? 0.15 : 0.02;
- if(Math.random() < backstabChance) return true;
- return false;
+ // If they are not allies, they are a valid target.
+ if (!this.areAllies(this.players[i].name, poss.owner)) {
+ return true;
+ }
+
+ // --- Backstab Logic ---
+ // If they ARE allies, only certain factions have a chance to betray.
+ let isChaotic = ["Raiders", "Super Mutants", "Great Khans", "The Fiends"].includes(this.players[i].country);
+ let backstabChance = 0.02; // A very low base chance
+ if (isChaotic) {
+ backstabChance = 0.10; // Chaotic factions are more likely to betray
+ }
+
+ // If the random roll succeeds, they will betray their ally and attack.
+ if (Math.random() < backstabChance) {
+ this.breakTruce(this.players[i].name, poss.owner); // Announce the betrayal
+ return true; // The ally is now a valid target.
+ }
+
+ // Otherwise, they are an ally and not a valid target.
+ return false;
});
+
// --- AI DESPERATION CHECK ---
// GLOBAL THREAT: If ANY nuke is in the air, the entire wasteland unites against the launcher!
+ // ... code that filters possibleTargets ...
+
+ // --- FAILSAFE CHECK ---
+ // If, after filtering for allies, there are no valid targets left, the AI cannot attack from this territory.
+ if (possibleTargets.length === 0) {
+ return; // Exit the function.
+ }
+
+ // --- AI DESPERATION CHECK ---
let isDesperate = false;
if (this.activeNuke && this.activeNuke.launcher !== this.players[i].name) {
isDesperate = true;
}
+ let target = [possibleTargets[0], -999]; // This line is now safe.
- let target = [possibleTargets[0], -999]; // Start lower to allow for negative ratios
possibleTargets.forEach(poss => {
let continent = continents.find(x => x.name === poss.continent); let count = 0;
continent.areas.forEach(x => { if(this.players[i].areas.includes(x)) count++; });
@@ -5024,6 +6068,13 @@ Gamestate.aiAttack = async function(country, i, turbo){
if(!target[0]){ return; }
+ // --- FINAL TRUCE ENFORCEMENT ---
+ // This is an explicit final check. If the chosen target is an ally, abort the attack.
+ if (this.areAllies(this.players[i].name, target[0].owner)) {
+ // We log this for debugging, but it won't be visible to the player.
+ console.log(`AI ${this.players[i].name} aborted attack on ally ${target[0].owner}.`);
+ return; // Exit the function, preventing the attack.
+ }
// --- THIS IS THE CODE THAT GOT DELETED ---
if(this.areAllies(this.players[i].name, target[0].owner)) { this.breakTruce(this.players[i].name, target[0].owner); }
@@ -5053,6 +6104,36 @@ Gamestate.getWeakestNeighbor = function(player) {
}
// --- END OF NEW FUNCTION ---
+Gamestate.useMercenaryContract = async function() {
+ if (!this.perksEnabled || !this.player.perk || this.player.perk.id !== 'mercenary_contracts' || this.aiTurn) return;
+
+ if (this.player.cards.length < 2) {
+ if(this.showToast) this.showToast("Not enough Bottle Caps! Contract requires 2.", "red");
+ return;
+ }
+
+ // Pay the cost
+ this.player.cards.splice(0, 2);
+
+ // --- SCALING REWARD ---
+ // 1 troop per territory owned, with a minimum of 3.
+ const reward = Math.max(3, this.player.areas.length);
+
+ // --- UNIFIED DEPLOYMENT LOGIC ---
+ // Troops are always added to the general reserve pool, forcing a deployment phase.
+ this.player.reserve += reward;
+ this.player.army += reward;
+ this.stage = "Fortify"; // Force the game into deployment phase
+ this.updateButtonText();
+ await this.logAction(`Mercenary contract fulfilled! +${reward} Gunners have been added to your reserves.`);
+
+ this.updateInfo();
+};
+
+
+
+
+
Gamestate.aiManeuver = function(i){
let player = this.players[i];
let owned = this.countries.filter(c => c.owner === player.name);
@@ -5242,155 +6323,265 @@ Gamestate.refreshDevMenuStatus = function() {
}
}
}
-Gamestate.battle = async function(country, opponent, player, i){
- let defender = document.getElementById(`${opponent.name}`); let attacker = document.getElementById(`${country.name}`);
-
+
+Gamestate.battle = async function(country, opponent, player, i) {
+ let defender = document.getElementById(`${opponent.name}`);
+ let attacker = document.getElementById(`${country.name}`);
// Find the opponent
- let opp; this.players.forEach(p => { if(p.name === opponent.owner){ opp = p; } });
-
+ let opp;
+ this.players.forEach(p => {
+ if (p.name === opponent.owner) {
+ opp = p;
+ }
+ });
// --- EMPTY TERRITORY FAIL-SAFE ---
- // If a faction was eliminated but territories remain, create a ghost opponent so combat math doesn't crash.
- if (!opp) { opp = { name: "none", isNeutral: true, alive: false, army: 0, cards: [], areas: [] }; }
+ if (!opp) {
+ opp = {
+ name: "none",
+ isNeutral: true,
+ alive: false,
+ army: 0,
+ cards: [],
+ areas: []
+ };
+ }
+ const originalOwner = opponent.owner;
+ let attackerWinChance = 0.5;
+ if (this.difficulty === "Easy") {
+ if (player === this.player) attackerWinChance = 0.60;
+ else if (opp === this.player) attackerWinChance = 0.40;
+ } else if (this.difficulty === "Hard") {
+ if (player === this.player) attackerWinChance = 0.40;
+ else if (opp === this.player) attackerWinChance = 0.60;
+ }
+ if (opp.isNeutral) attackerWinChance -= 0.15;
- const originalOwner = opponent.owner;
+ if (this.perksEnabled) {
+ if (player.perk && player.perk.id === 'power_armor_infantry') {
+ attackerWinChance += 0.05;
+ }
+ if (opp.perk && opp.perk.id === 'power_armor_infantry') {
+ attackerWinChance -= 0.05;
+ }
+ }
- let attackerWinChance = 0.5;
- if (this.difficulty === "Easy") { if (player === this.player) attackerWinChance = 0.60; else if (opp === this.player) attackerWinChance = 0.40; }
- else if (this.difficulty === "Hard") { if (player === this.player) attackerWinChance = 0.40; else if (opp === this.player) attackerWinChance = 0.60; }
- if (opp.isNeutral) attackerWinChance -= 0.15;
-
if (this.nukesEnabled && opponent.isSilo) {
let buff = Math.min(0.80, opponent.siloTurns * 0.20);
- if(this.activeNuke && this.activeNuke.launcher === opponent.owner) buff = 0;
- if(opponent.owner === "Wasteland Horrors") buff = 0;
+ if (this.activeNuke && this.activeNuke.launcher === opponent.owner) buff = 0;
+ if (opponent.owner === "Wasteland Horrors") buff = 0;
attackerWinChance = attackerWinChance * (1 - buff);
}
-
- // Only buff defenses if the Commander actually OWNS this territory
- if (this.commandersEnabled && opp.commander && opp.commander.loc === opponent.name) {
- attackerWinChance = attackerWinChance * 0.80;
+ if (this.commandersEnabled && opp.commander && opp.commander.loc === opponent.name) {
+ attackerWinChance = attackerWinChance * 0.80;
}
-
- // CAP DEFENSE AT 89% (Attacker always has at least an 11% chance)
if (attackerWinChance < 0.11) attackerWinChance = 0.11;
-
- // --- DEV MODE COMBAT OVERRIDE ---
if (Gamestate.devWinOverride !== undefined && Gamestate.devWinOverride >= 0) {
- if (player === this.player) attackerWinChance = Gamestate.devWinOverride; // When Player attacks
- if (opp === this.player) attackerWinChance = 1.0 - Gamestate.devWinOverride; // When Player defends
+ if (player === this.player) attackerWinChance = Gamestate.devWinOverride;
+ if (opp === this.player) attackerWinChance = 1.0 - Gamestate.devWinOverride;
}
-
- let isVictory = false; let flavor = "";
-
- // Standard troops wipe each other out to 0 (Ignoring the Commander's presence)
- while(opponent.army > 0 && country.army > 1){
- if(Math.random() > attackerWinChance){ country.army -= 1; }
- else { opponent.army -= 1; }
+ let isVictory = false;
+ let flavor = "";
+ if (this.perksEnabled) {
+ if (player.usedChemFrenzy) {
+ attackerWinChance += 0.10;
+ player.usedChemFrenzy = false;
+ await this.logAction("Chem Frenzy! The Raiders fight with unnatural ferocity.");
+ }
+ if (this.perksEnabled && player.perk?.id === 'mysterious_stranger') {
+ const wouldLose = (country.army <= opponent.army) && (Math.random() > attackerWinChance);
+ if ((player.strangerCooldown || 0) === 0 && wouldLose) {
+ if (this.queueToast) {
+ this.queueToast(`>>> UNKNOWN VARIABLE DETECTED <<< ? THE MYSTERIOUS STRANGER APPEARS`, "var(--pip-color)", true);
+ }
+ await this.logAction("A familiar tune plays... The Mysterious Stranger steps from the shadows!", true);
+ attackerWinChance += 0.25;
+ player.strangerCooldown = Math.floor(Math.random() * 4) + 1;
+ }
+ }
+ 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!`);
+ }
+ }
}
-
- if(country.army <= 1 && opponent.army > 0){
+ 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;
+ }
+ }
+ if (country.army <= 1 && opponent.army > 0) {
if (attacker && attacker.nextElementSibling) attacker.nextElementSibling.textContent = country.army;
if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army;
- this.updateInfo(); await this.logAction(`REPULSED: ${player.name} assaulted ${originalOwner} in ${formatTerritoryName(opponent.name)} but failed.`);
+ 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 attackerLosses = originalAttackerArmy - country.army;
+ 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!`);
+ }
+ }
return;
}
-
- if(opponent.army <= 0 ){
- isVictory = true;
- player.conqueredThisTurn = true;
- flavor = (Math.random() < 0.10) ? (" " + combatFlavors[Math.floor(Math.random() * combatFlavors.length)]) : "!";
+ if (opponent.army <= 0) {
+ isVictory = true;
+ player.conqueredThisTurn = true;
+ // --- THIS IS THE NEW ENCOUNTER TRIGGER ---
+ await this.triggerEncounterCheck('post_conquest');
+
+ 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) { p.areas.splice(index, 1); } }
+ if (p.name === opponent.owner) {
+ let index = p.areas.indexOf(opponent.name);
+ if (index > -1) {
+ p.areas.splice(index, 1);
+ }
+ }
});
-
- opponent.owner = player.name; opponent.color = player.color; opponent.siloTurns = 0; player.areas.push(opponent.name);
-
- let maxMove = country.army - 1; let minMove = 1; let movedTroops = minMove;
-
+ opponent.owner = player.name;
+ opponent.color = player.color;
+ opponent.siloTurns = 0;
+ player.areas.push(opponent.name);
+ let maxMove = country.army - 1;
+ let minMove = 1;
+ let movedTroops = minMove;
if (player === this.player && maxMove > minMove) {
- movedTroops = await this.showMoveModal(minMove, maxMove, formatTerritoryName(opponent.name));
- } else if (player !== this.player) { movedTroops = maxMove; }
-
- opponent.army = movedTroops; country.army -= movedTroops;
- if (defender) defender.style.fill = opponent.color;
-
- if(this.activeNuke && this.activeNuke.launcher === originalOwner) {
- let stillHasSilo = this.countries.some(c => c.isSilo && c.owner === originalOwner);
- if(!stillHasSilo) { await this.logAction(`[ CRITICAL ] ${originalOwner} lost Silo control! Launch sequence aborted.`, true); this.activeNuke = null; }
+ movedTroops = await this.showMoveModal(minMove, maxMove, formatTerritoryName(opponent.name));
+ } else if (player !== this.player) {
+ movedTroops = maxMove;
+ }
+ 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!`);
+ }
+ } else if (player.perk.id === 'scourge_of_the_east') {
+ movedTroops = country.army;
+ country.army = 0;
+ }
+ }
+ if (this.activeNuke && this.activeNuke.launcher === originalOwner) {
+ let stillHasSilo = this.countries.some(c => c.isSilo && c.owner === originalOwner);
+ if (!stillHasSilo) {
+ await this.logAction(`[ CRITICAL ] ${originalOwner} lost Silo control! Launch sequence aborted.`, true);
+ this.activeNuke = null;
+ }
}
-
- // --- NEW: AI LOOT ROLL ---
- // If the attacker is an AI, give them a chance to find loot.
if (!player.isPlayer) {
let luckItem = this.bobbleheads && this.bobbleheads.find(i => i.key === 'l' && i.cooldown > 0);
- const luckModifier = luckItem ? 0.15 : 0; // Luck Bobblehead bonus applies to AI too!
-
+ const luckModifier = luckItem ? 0.15 : 0;
let roll = Math.random();
-
- // 1. Chance to find a Bobblehead (5% base chance)
if (this.bobbleheads && roll < (0.05 + luckModifier)) {
let unfoundItems = this.bobbleheads.filter(item => !item.found);
if (unfoundItems.length > 0) {
let foundItem = unfoundItems[Math.floor(Math.random() * unfoundItems.length)];
- foundItem.found = true; // Mark as found globally so player can't find it
+ foundItem.found = true;
if (this.logAction) await this.logAction(`\[ INTEL \] Radio intercepts indicate ${player.name} has located a '${foundItem.name}'.`);
}
- }
- // 2. Chance to find a Stimpak if commanders are enabled
- else if (this.commandersEnabled && player.commander && player.commander.stimpaks < 3 && roll < (0.40 + luckModifier)) {
+ } else if (this.commandersEnabled && player.commander && player.commander.stimpaks < 3 && roll < (0.40 + luckModifier)) {
player.commander.stimpaks++;
if (this.logAction) await this.logAction(`\[ INTEL \] Scouts report ${player.name}'s Commander salvaged a Stimpak from the battlefield.`);
}
}
- // --- END OF NEW AI LOOT ROLL ---
-
- // FACTION ELIMINATION CHECK
- if(opp.areas && opp.areas.length === 0 && (!this.commandersEnabled || !opp.commander || opp.commander.hp <= 0)){
+ if (opp.areas && opp.areas.length === 0 && (!this.commandersEnabled || !opp.commander || opp.commander.hp <= 0)) {
if (opp.name !== "none") {
- opp.alive = false; let index = this.players.indexOf(opp); if (infoName[index]) infoName[index].parentElement.classList.add('defeated');
- if (opp.cards.length > 0) { player.cards.push(...opp.cards); if (Gamestate.logAction) Gamestate.logAction(`[ LOOT ] ${player.name} looted the dead. (+${opp.cards.length} Caps)`, true); opp.cards = []; }
- if (this.nukesEnabled && opp.codes > 0) { player.codes += opp.codes; opp.codes = 0; if (Gamestate.logAction) Gamestate.logAction(`[ INTEL ] Recovered Launch Codes from the defeated faction!`, true); }
+ opp.alive = false;
+ let index = this.players.indexOf(opp);
+ if (infoName[index]) infoName[index].parentElement.classList.add('defeated');
+ if (opp.cards.length > 0) {
+ player.cards.push(...opp.cards);
+ if (Gamestate.logAction) Gamestate.logAction(`[ LOOT ] ${player.name} looted the dead. (+${opp.cards.length} Caps)`, true);
+ opp.cards = [];
+ }
+ if (this.nukesEnabled && opp.codes > 0) {
+ player.codes += opp.codes;
+ opp.codes = 0;
+ if (Gamestate.logAction) Gamestate.logAction(`[ INTEL ] Recovered Launch Codes from the defeated faction!`, true);
+ }
}
} else if (opp.areas && opp.areas.length === 0 && this.commandersEnabled && opp.commander && opp.commander.hp > 0) {
await this.logAction(`[ EXILED ] ${opp.name} lost their final territory, but their Commander is still alive behind enemy lines!`, true);
}
}
-
- player.army = 0; opp.army = 0;
+ player.army = 0;
+ opp.army = 0;
this.countries.forEach(c => {
- player.areas.forEach(area => { if(area === c.name){ player.army += c.army } })
- if(opp.areas) { opp.areas.forEach(area => { if(area === c.name){ opp.army += c.army } }) }
+ player.areas.forEach(area => {
+ if (area === c.name) {
+ player.army += c.army
+ }
+ })
+ if (opp.areas) {
+ opp.areas.forEach(area => {
+ if (area === c.name) {
+ opp.army += c.army
+ }
+ })
+ }
});
-
this.updateInfo();
- if (isVictory) {
- await this.logAction(`VICTORY: ${player.name} wiped out ${originalOwner} in ${formatTerritoryName(opponent.name)}${flavor}`, true);
-
- // --- REPUTATION PENALTY: UNPROVOKED ATTACK ---
- // The defender likes the attacker -5 less for this aggression.
- if (originalOwner !== "none" && originalOwner !== "Wasteland Horrors") {
- // Check if the reputation object exists for this interaction before modifying it
- if (this.diplomacy.reputation[originalOwner] && this.diplomacy.reputation[originalOwner][player.name] !== undefined) {
- this.diplomacy.reputation[originalOwner][player.name] = Math.max(-50, this.diplomacy.reputation[originalOwner][player.name] - 5);
-
+ if (this.perksEnabled && opp.perk?.id === 'synth_replacements' && country.army <= 1) {
+ const lossesIncurred = originalDefenderArmy - opponent.army;
+ let synthsReplaced = 0;
+ for (let k = 0; k < lossesIncurred; k++) {
+ if (Math.random() < 0.50) {
+ opponent.army++;
+ synthsReplaced++;
+ }
+ }
+ if (synthsReplaced > 0) {
+ await this.logAction(`MEMORY REPLACEMENT: The Institute replaced ${synthsReplaced} fallen defenders with Synths!`);
+ if (opponent.army > 0 && country.army <= 1) {
+ isVictory = false;
+ await this.logAction(`REPULSED: Synth replacements successfully held the line!`);
}
}
}
-
- if(this.player.alive){
+ 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) {
+ this.diplomacy.reputation[originalOwner][player.name] = Math.max(-50, this.diplomacy.reputation[originalOwner][player.name] - 5);
+ }
+ }
+ }
+ if (this.player.alive) {
continents.forEach(continent => {
- let ownsContinent = continent.areas.every(area => player.areas.includes(area) && !this.countries.find(x=>x.name===area).isCrater);
- if(ownsContinent){
- let matchedCountry = continent.areas.some(a => { return a === opponent.name; });
- if(matchedCountry){
- if (this.queueToast) this.queueToast(`>>> STRATEGIC ASSET SECURED <<< ${player.name.toUpperCase()} NOW CONTROLS ${continent.name.toUpperCase()} (+${continent.bonus} TROOPS)`, player.color, false);
+ let ownsContinent = continent.areas.every(area => player.areas.includes(area) && !this.countries.find(x => x.name === area).isCrater);
+ if (ownsContinent) {
+ let matchedCountry = continent.areas.some(a => {
+ return a === opponent.name;
+ });
+ if (matchedCountry) {
+ if (this.queueToast) this.queueToast(`>>> STRATEGIC ASSET SECURED <<< ${player.name.toUpperCase()} NOW CONTROLS ${continent.name.toUpperCase()} (+${continent.bonus} TROOPS)`, player.color, false);
}
}
- })
+ })
}
-
- // Call the new centralized win condition function
this.checkWinCondition();
}
@@ -5457,6 +6648,214 @@ Gamestate.checkWinCondition = function() {
};
+
+Gamestate.showEncounterModal = function(title, message, choices, onChoice = null) {
+ return new Promise((resolve) => {
+ const modal = document.getElementById('encounter-modal');
+ const titleEl = document.getElementById('encounter-title');
+ const messageEl = document.getElementById('encounter-message');
+ const choicesContainer = document.getElementById('encounter-choices');
+
+ titleEl.textContent = title;
+ messageEl.innerHTML = message;
+ choicesContainer.innerHTML = '';
+
+ choices.forEach(choice => {
+ const button = document.createElement('button');
+ button.textContent = choice.text;
+ button.onclick = async () => {
+ // 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
+
+ // Wait 2.5 seconds before closing the modal
+ setTimeout(() => {
+ 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);
+ });
+
+ modal.style.display = 'flex';
+ });
+};
+
+Gamestate.showRecruitmentModal = function() {
+ return new Promise((resolve) => {
+ const modal = document.getElementById('recruitment-modal');
+ const capsDisplay = document.getElementById('recruitment-caps');
+ const slider = document.getElementById('recruitment-slider');
+ const valDisplay = document.getElementById('recruitment-val');
+ const costDisplay = document.getElementById('recruitment-cost');
+ const confirmBtn = document.getElementById('recruitment-confirm');
+
+ const troopCost = 5;
+ const maxTroops = Math.floor(this.player.caps / troopCost);
+
+ capsDisplay.textContent = this.player.caps;
+ slider.max = maxTroops;
+ slider.value = 0;
+ valDisplay.textContent = 0;
+ costDisplay.textContent = 0;
+
+ slider.oninput = function() {
+ valDisplay.textContent = this.value;
+ costDisplay.textContent = this.value * troopCost;
+ };
+
+ const newConfirmBtn = confirmBtn.cloneNode(true);
+ confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
+
+ newConfirmBtn.onclick = () => {
+ const troopsToBuy = parseInt(slider.value);
+ const totalCost = troopsToBuy * troopCost;
+
+ if (this.player.caps >= totalCost) {
+ this.player.caps -= totalCost;
+ this.player.reserve += troopsToBuy;
+ this.player.army += troopsToBuy;
+ this.logAction(`RECRUITMENT: Purchased +${troopsToBuy} troops for ${totalCost} Caps.`, true);
+ }
+ modal.style.display = 'none';
+ resolve();
+ };
+
+ modal.style.display = 'flex';
+ });
+};
+
+
+Gamestate.resolveCreatureEncounter = async function() {
+ if (this.player.areas.length === 0) return;
+
+ const randomAreaName = this.player.areas[Math.floor(Math.random() * this.player.areas.length)];
+ const territory = this.countries.find(c => c.name === randomAreaName);
+ if (!territory || territory.army <= 0) return;
+
+ const creature = encounterData.creatures[Math.floor(Math.random() * encounterData.creatures.length)];
+
+ const title = "Creature Sighting";
+ const message = `Your scouts report a wild ${creature.name} near your garrison of ${territory.army} troops at ${formatTerritoryName(territory.name)} .`;
+ const choices = [
+ { id: "attack", text: "[Attack] Try to eliminate the threat." },
+ { id: "avoid", text: "[Avoid] The risk isn't worth it." }
+ ];
+
+ const onChoiceCallback = (decision) => {
+ if (decision === 'attack') {
+ // Log the action but DO NOT await it.
+ this.logAction(`You chose to engage the ${creature.name} at ${formatTerritoryName(territory.name)}!`, true);
+
+ const army = territory.army;
+ const threat = creature.threat;
+ const damagePercent = threat / (army + threat);
+ let casualties = Math.floor(army * damagePercent);
+
+ if (army > 1 && army - casualties < 1) {
+ casualties = army - 1;
+ } else if (army === 1 && casualties > 0) {
+ casualties = 1;
+ }
+
+ if (casualties > 0) {
+ territory.army -= casualties;
+ this.player.army -= casualties;
+ // Log the action but DO NOT await it.
+ this.logAction(`Your soldiers fought bravely, but suffered ${casualties} casualties.`, true);
+ return `The ${creature.name} was driven off, but you lost ${casualties} troops in the fight.`;
+ } else {
+ // Log the action but DO NOT await it.
+ this.logAction(`Your garrison made short work of the ${creature.name} and suffered no losses.`);
+ return `A decisive victory! The ${creature.name} was eliminated with no casualties .`;
+ }
+ } else { // 'avoid'
+ // Log the action but DO NOT await it.
+ this.logAction(`You chose to avoid the ${creature.name}, leaving it to roam the wastes.`);
+ return `You wisely avoid the creature, and your troops remain safe.`;
+ }
+ };
+
+ await this.showEncounterModal(title, message, choices, onChoiceCallback);
+
+ // Update the UI after the modal has closed
+ this.updateInfo();
+ this.drawMapText();
+};
+
+
+Gamestate.triggerEncounterCheck = async function(triggerType) {
+ // --- THIS IS THE NEW CHECK ---
+ // If encounters are disabled in the game settings, do nothing.
+ if (!this.encountersEnabled) return;
+
+ let chance = 0;
+ if (triggerType === 'start_of_turn') {
+ chance = 0.08; // 8% chance at the start of a turn
+ } else if (triggerType === 'post_conquest') {
+ chance = 0.15; // 15% chance after conquering a territory
+ }
+
+ if (Math.random() < chance) {
+ if (triggerType === 'start_of_turn') {
+ await this.resolveCreatureEncounter();
+ }
+ // We will add post-conquest story logic here in a future step.
+ }
+};
+
+
+
+/**
+ * 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.
+ *
+ * I am assuming:
+ * - You have a global 'players' array where each player object has a 'country' property (e.g., players[0].country = "The Enclave").
+ * - You have a global 'reputation' matrix (e.g., reputation[playerIndex1][playerIndex2]).
+ */
+
+function setInitialReputations() {
+ console.log("Setting initial faction reputations...");
+
+ // The ONLY change is on the next line: "players.length" becomes "Gamestate.players.length"
+ for (let i = 0; i < Gamestate.players.length; i++) {
+
+ for (let j = 0; j < Gamestate.players.length; j++) {
+ if (i === j) {
+ // Also needs to be changed here:
+ Gamestate.diplomacy.reputation[Gamestate.players[i].name][Gamestate.players[j].name] = 0;
+ continue;
+ }
+
+ const faction1_name = Gamestate.players[i].country;
+ const faction2_name = Gamestate.players[j].country;
+
+ if (FACTIONS[faction1_name] && FACTIONS[faction1_name].affinity && FACTIONS[faction1_name].affinity[faction2_name]) {
+ const affinityValue = FACTIONS[faction1_name].affinity[faction2_name];
+
+ // And here:
+ Gamestate.diplomacy.reputation[Gamestate.players[i].name][Gamestate.players[j].name] = affinityValue;
+
+ console.log(`Affinity found: ${faction1_name} -> ${faction2_name} = ${affinityValue}`);
+ } else {
+ // And finally, here:
+ Gamestate.diplomacy.reputation[Gamestate.players[i].name][Gamestate.players[j].name] = 0;
+ }
+ }
+ }
+ console.log("Initial reputations set.", Gamestate.diplomacy.reputation);
+}
+
+
const wastelandRadio = [
{ file: "Fallout3.mp3", title: "Fallout 3 Theme - Inon Zur" }, { file: "IDontWantToSeeTomorrow.mp3", title: "Nat King Cole - I Don't Want to See Tomorrow" },
{ file: "LetsGoSunning.mp3", title: "Jack Shaindlin - Lets Go Sunning" }, { file: "TheendoftheWorld.mp3", title: "Skeeter Davis - The End of the World" },
@@ -5550,7 +6949,6 @@ Gamestate.updateInfo = function() {
}
}
}
-// ---------------------------------------------
// --- BULLETPROOF SVG OVERLAP FIX (V2) ---
Gamestate.fixMapTextOrder = function() {
@@ -5570,7 +6968,6 @@ Gamestate.fixMapTextOrder = function() {
}
});
}
-// -----------------------------------