diff --git a/index.html b/index.html
index 12dfbee..bab97a6 100644
--- a/index.html
+++ b/index.html
@@ -70,6 +70,8 @@
display: flex;
flex-direction: column;
overflow: hidden;
+ text-transform: uppercase; /* <-- THIS IS THE NEW LINE */
+
}
body::after {
@@ -186,13 +188,14 @@
left: 0;
width: 100vw;
height: 100vh;
- /* Replaced flat black with theme-aware vignette */
- background: radial-gradient(circle at center, transparent 40%, var(--vignette-shadow) 120%);
- backdrop-filter: blur(2px); /* Adds a premium subtle blur to the map behind */
+ /* Darkened the center so background is less distracting */
+ background: radial-gradient(circle at center, rgba(0, 0, 0, 0.8) 20%, var(--vignette-shadow) 100%);
+ backdrop-filter: blur(4px); /* Increased blur slightly for better focus */
z-index: 9000;
}
+
.start-modal,
.cards-modal,
.win-content,
@@ -722,6 +725,7 @@
justify-content: center;
align-items: center;
overflow: hidden;
+ background: linear-gradient(rgba(0,0,0,0.3), rgba(0,0,0,0.3)), var(--pip-panel-solid);
}
.map::before {
@@ -881,12 +885,15 @@
/* Force ALL log entries to be the same larger size */
.log-entry {
- opacity: 0.9;
- font-size: 18px !important;
- /* The !important ensures nothing overrides this base size */
+ opacity: 0.9; /* Restored to original for slight 'log' feel */
+ font-size: 18px !important; /* Match the directive's explicit size */
margin-bottom: 2px;
+ text-shadow: none !important; /* This removes the glow */
+ font-weight: normal !important; /* This removes the unwanted boldness */
}
+
+
/* Important messages just get bold and glow, no size difference */
.log-entry.important {
font-weight: bold;
@@ -930,35 +937,24 @@
.map-brackets {
position: absolute;
- left: 280px;
- right: 280px;
- top: 20px;
- bottom: 170px;
+ left: 260px;
+ right: 260px;
+ top: 0;
+ bottom: 150px;
pointer-events: none;
z-index: 40;
+ border-radius: 16px;
+ box-shadow: inset 0 0 100px 40px rgba(0, 0, 0, 0.95);
+
}
.map-brackets::before,
.map-brackets::after {
- content: '';
- position: absolute;
- width: 100%;
- height: 100%;
+ display: none;
}
- .map-brackets::before {
- border-top: 4px solid var(--pip-color);
- border-bottom: 4px solid var(--pip-color);
- transform: scaleX(0.95);
- opacity: 0.5;
- }
- .map-brackets::after {
- border-left: 4px solid var(--pip-color);
- border-right: 4px solid var(--pip-color);
- transform: scaleY(0.90);
- opacity: 0.5;
- }
+
.hp-bar {
width: 100%;
@@ -1262,6 +1258,7 @@
left: 140px;
right: 140px;
bottom: 0;
+
}
/* --- NEW: Hide brackets completely on mobile --- */
@@ -1906,6 +1903,24 @@
.faction-icon.default { background-color: transparent; }
+ .faction-icon.default { background-color: transparent; }
+
+ /* --- NEW: PURE CSS PAW PRINT FOR UI --- */
+ .paw-icon {
+ -webkit-mask-image: url('data:image/svg+xml;utf8,');
+ mask-image: url('data:image/svg+xml;utf8,');
+ background-color: var(--pip-color);
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 5px;
+ -webkit-mask-size: contain;
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: center;
+ filter: drop-shadow(0 0 3px var(--pip-color));
+ }
+
/* --- ROBCO OS TERMINAL STYLES --- */
.help-nav-item, .help-back-btn {
@@ -1980,34 +1995,15 @@
A RobCo Industries
Strategic Simulation
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2087,11 +2118,7 @@
Scorched Earth
-
+
@@ -3281,26 +3308,10 @@
-
-
-
-
-
-
- I n d i a n O c e a n
-
-
-
-
+
+
@@ -3331,7 +3342,7 @@
-
v2.3 [050926]
+
v2.4 [051326]
@@ -3575,7 +3586,6 @@
window.onerror = function (message, source, lineno, colno, error) {
var consoleDiv = document.getElementById('error-console');
var errorLog = document.getElementById('error-log');
-
if (consoleDiv) {
consoleDiv.style.display = 'block';
}
@@ -3776,10 +3786,21 @@
description: "At the start of your turn, you gain +10 Bottle Caps for every continent you fully control."
},
affinity: {}
+ },
+ "Maxson's Brotherhood": {
+ leader: "Elder Maxson",
+ color: "#e0e0e0",
+ perk: {
+ id: "prydwen_deployment",
+ name: "Prydwen Deployment",
+ description: "The Prydwen automatically dispatches 3 airborne troops per turn to contested borders."
+ },
+ affinity: { "The Institute": -30, "The Railroad": -20 }
}
};
+
// This is the data for any custom faction a player creates.
const CUSTOM_FACTION = {
leader: "Mysterious Stranger", // Placeholder
@@ -4056,7 +4077,16 @@
"Radio picked up a faint transmission: 'Almost heaven, West Virginia. Blue Ridge Mountains, Shenandoah River. Life is old there...' before fading to static.",
"Met a caravan guard who used to be a wasteland adventurer like you, until he took a bullet to the knee.",
"You ran into a hyper guy with a purple gun and an Australian accent talking about 'horde bases'. Whatever that is...",
- "A passing caravan handed me a sealed holotape. The label reads: 'To the sandbox. Keep your head down and come home soon. We're praying for you.'"
+ "A passing caravan handed me a sealed holotape. The label reads: 'To the sandbox. Keep your head down and come home soon. We're praying for you.'",
+ "Scouts report a Ghoul in a duster dragging a chained Vault Dweller across the desert.",
+ "A cheerful Vault Dweller was spotted repeatedly saying 'Okey dokey!' to a very confused trader.",
+ "A lone wanderer was seen dragging a severed head in a cooler. Just another Tuesday.",
+ "A Mr. Handy at a ruined Super Duper Mart politely offered to harvest a scout's organs for caps.",
+ "Rumors say an Enclave scientist smuggled a cold fusion core inside his own neck. Sounds ridiculous.",
+ "A stray dog is wandering the wastes carrying a severed finger in its mouth.",
+ "Intercepted an encrypted Vault-Tec memo regarding a 'Bud's Buds' executive management program.",
+ "Radio static briefly cleared to a quack doctor selling a miraculous healing elixir that regrows feet."
+
];
const combatFlavors = [
@@ -4080,10 +4110,51 @@
"by waiting patiently for their laser rifles to overheat.",
"while they were busy fending off a pack of rabid mole rats.",
"after a well-placed mini-nuke completely cleared the perimeter.",
+ "by dressing up as traveling merchants to bypass the main gate.",
+ "walking in the front door because the guards were asleep on Mentats.",
+ "after exploiting a fatal flaw in the welding below their T-60 chest plates.",
+
"by dressing up as traveling merchants to bypass the main gate.",
"walking in the front door because the guards were asleep on Mentats."
];
+ // --- NEW: DOGMEAT SPECIFIC FLAVORS ---
+ const dogmeatCombatFlavors = [
+ "while Dogmeat ruthlessly pinned their sniper to the ground.",
+ "after Dogmeat sniffed out their hidden flanking maneuver.",
+ "with Dogmeat tearing a piece of armor right off their captain.",
+ "while Dogmeat was busy chasing a terrified guard in circles.",
+ "after Dogmeat brought you an unpinned grenade he found... thankfully throwing it at them first.",
+ "while Dogmeat proudly sat on the defeated commander's chest.",
+ "with Dogmeat leading the charge, barking furiously.",
+ "after Dogmeat discovered a tunnel leading straight into their flank."
+ ];
+
+ const dogmeatEncounters = [
+ "Dogmeat is happily chewing on a strangely pristine Radroach leg.",
+ "Dogmeat started barking at a patch of empty dirt. Must be a mole rat deep underground.",
+ "Dogmeat dropped a slightly irradiated stick at your feet, waiting for you to throw it.",
+ "Dogmeat playfully chased a Bloatfly until it exploded.",
+ "Dogmeat fell asleep on top of a supply crate. Nobody has the heart to move him.",
+ "Dogmeat let out a low growl at the wind. Good boy.",
+ "Dogmeat found an old pre-war teddy bear and hasn't dropped it for hours.",
+ "Dogmeat is furiously digging a hole to bury a super mutant bone."
+ ];
+
+ const injuredDogmeatCombatFlavors = [
+ "while Dogmeat's whimpering nearly gave away your position.",
+ "even as Dogmeat's barking attracted a nearby pack of mole rats.",
+ "while your medics were busy trying to keep Dogmeat from bleeding out.",
+ "despite Dogmeat collapsing from exhaustion mid-battle."
+ ];
+
+ const injuredDogmeatEncounters = [
+ "Dogmeat lets out a painful whimper. His leg wound needs better treatment.",
+ "You have to stop the convoy to allow Dogmeat to rest. He's slowing you down.",
+ "Dogmeat is shivering, despite the heat. The infection might be getting worse.",
+ "You apply a crude bandage to Dogmeat's side. It's a temporary fix at best."
+ ];
+
const encounterData = {
creatures: [
{ name: "Centaur", threat: 10.0, isHumanoid: true },
@@ -4210,10 +4281,21 @@
// --- V.A.T.S. Calculation Logic ---
let baseChance = 0.50;
if (Gamestate.difficulty === "Easy") baseChance = 0.60;
- if (Gamestate.difficulty === "Hard") baseChance = 0.40;
+ else if (Gamestate.difficulty === "Hard") baseChance = 0.40;
+ else if (Gamestate.difficulty === "Normal") {
+ // --- NEW: Dynamic Rubber-Band Scaling ---
+ let totalPlayable = Gamestate.countries.filter(c => !c.isCrater).length;
+ let playerOwnedCount = Gamestate.countries.filter(c => c.owner === Gamestate.player.name).length;
+if (totalPlayable > 0 && (playerOwnedCount / totalPlayable) > 0.35) {
+
+ baseChance -= 0.05;
+ }
+ }
+
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 ---
let rangerBonus = 0;
let overdriveWarning = "";
@@ -4245,6 +4327,14 @@
}
}
// --- END BOBBLEHEAD CALCULATION ---
+
+ // --- NEW: DOGMEAT OFFENSE BUFF (HEALTHY ONLY) ---
+ if (Gamestate.player.dogmeatStatus === 'healthy' && Gamestate.player.name !== targetCountry.owner) {
+ baseChance += 0.10;
+ bobbleheadWarning += ` COMPANION: DOGMEAT (+10% OFFENSE)`;
+ }
+
+
if (Gamestate.perksEnabled) {
if (Gamestate.player.perk && Gamestate.player.perk.id === 'power_armor_infantry') {
@@ -4328,7 +4418,15 @@
cmdrWarning = ` COMMANDER PRESENT. +20% DEFENSE.`;
}
let hazardWarning = "";
+
+ // --- NEW: NUKE COUNTDOWN WARNING ---
+ let targetedNuke = Gamestate.activeNukes ? Gamestate.activeNukes.find(n => n.target === targetCountry.name) : null;
+ if (targetedNuke) {
+ hazardWarning += ` NUCLEAR STRIKE IN T-${targetedNuke.turns} TURNS`;
+ }
+
if (targetCountry.radDecay > 0) {
+
hazardWarning = ` TERRITORY IS IRRADIATED`;
} else if (e.target.classList.contains('radstorm-active')) {
hazardWarning = ` DANGER: ACTIVE RADSTORM`;
@@ -4341,6 +4439,18 @@
rangerWarning = ` RANGER NETWORK: +${Math.round(rangerBonus * 100)}% DEFENSE`;
}
+ // --- NEW: RELIC WARNINGS ---
+ let relicWarning = "";
+ if (targetCountry.hasMine) {
+ relicWarning += ` WARNING: BOTTLECAP MINE DETECTED`;
+ }
+ if (targetCountry.isFrozen > 0) {
+ relicWarning += ` STATUS: CRYOGENICALLY FROZEN (${targetCountry.isFrozen} T)`;
+ }
+ if (targetCountry.isBlockaded > 0) {
+ relicWarning += ` STATUS: BLOCKADED (${targetCountry.isBlockaded} T)`;
+ }
+
// --- NEW: V.A.T.S. UI forces 100% against empty territories ---
if (targetCountry.army === 0 && chancePercent !== "N/A") {
chancePercent = "100.0";
@@ -4349,7 +4459,7 @@
tooltipHTML = `
V.A.T.S. TARGETING
TARGET: ${formatTerritoryName(targetCountry.name)}
DEFENDERS: ${targetCountry.army}
- WIN CHANCE: ${chancePercent}${chancePercent !== "N/A" ? '%' : ''}${diplomacyWarning}${cmdrWarning}${rangerWarning}${overdriveWarning}${hazardWarning}${bobbleheadWarning}`;
+ WIN CHANCE: ${chancePercent}${chancePercent !== "N/A" ? '%' : ''}${diplomacyWarning}${cmdrWarning}${rangerWarning}${overdriveWarning}${hazardWarning}${bobbleheadWarning}${relicWarning}`;
}
else {
@@ -4359,13 +4469,42 @@
infoLines.push(`TERRITORY LOCKED DOWN`);
}
+ // --- NEW: NUKE COUNTDOWN WARNING ---
+ let targetedNukeInfo = Gamestate.activeNukes ? Gamestate.activeNukes.find(n => n.target === targetCountry.name) : null;
+ if (targetedNukeInfo) {
+ infoLines.push(`NUCLEAR STRIKE IN T-${targetedNukeInfo.turns} TURNS`);
+ }
+
// NEW: Searching status
if (targetCountry.isExploring) {
infoLines.push(`SEARCHING... (${targetCountry.exploreTurnsLeft} TURNS LEFT)`);
}
+ // --- NEW: DOGMEAT QUEST VATS WARNING ---
+ if (Gamestate.dogmeatQuest && Gamestate.dogmeatQuest.target === targetCountry.name) {
+ if (Gamestate.dogmeatQuest.active) {
+ infoLines.push(`[ QUEST: RESCUE DOGMEAT (${Gamestate.dogmeatQuest.timer} TURNS LEFT) ]`);
+ } else if (Gamestate.dogmeatQuest.siege > 0) {
+ infoLines.push(`[ SIEGE IN PROGRESS: ${Gamestate.dogmeatQuest.siege} DAYS LEFT ]`);
+ }
+ }
+
+
+ // --- NEW: RELIC STATUS (NON-COMBAT) ---
+
+ if (targetCountry.hasMine) {
+ infoLines.push(`[ BOTTLECAP MINE PLANTED ]`);
+ }
+ if (targetCountry.isFrozen > 0) {
+ infoLines.push(`[ FROZEN: ${targetCountry.isFrozen} TURNS ]`);
+ }
+ if (targetCountry.isBlockaded > 0) {
+ infoLines.push(`[ BLOCKADED: ${targetCountry.isBlockaded} TURNS ]`);
+ }
+
infoLines.push(`TERRITORY: ${formatTerritoryName(targetCountry.name)}`);
+
if (targetCountry.isCrater) {
infoLines.push(`IRRADIATED CRATER (IMPASSABLE)`);
} else {
@@ -4381,7 +4520,10 @@
// --- END OF NEW LOGIC ---
infoLines.push(`GARRISON: ${targetCountry.army}`);
if (targetCountry.radDecay && targetCountry.radDecay > 0) {
- let attrPercent = targetCountry.radDecay === 4 ? 50 : (targetCountry.radDecay === 3 ? 30 : 10);
+ let attrPercent = 10;
+ if (targetCountry.radDecay >= 8) attrPercent = 80;
+ else if (targetCountry.radDecay >= 5) attrPercent = 50;
+ else if (targetCountry.radDecay >= 3) attrPercent = 30;
infoLines.push(`☢ GLOWING SEA: -${attrPercent}% GARRISON NEXT TURN`);
}
if (Gamestate.nukesEnabled && targetCountry.isSilo) {
@@ -4396,13 +4538,15 @@
let isConverting = "";
if (p.commander.isConverting) {
let turnsLeft = 0;
+ let reqTurns = (Gamestate.levelingEnabled && p.activeBuffs && p.activeBuffs.infiltrator) ? 2 : 3;
if (targetCountry.army > 0) {
- turnsLeft = Math.ceil(targetCountry.army / 10) + 3;
+ turnsLeft = Math.ceil(targetCountry.army / 10) + reqTurns;
} else {
- turnsLeft = 3 - (p.commander.siegeTurns || 0);
+ turnsLeft = reqTurns - (p.commander.siegeTurns || 0);
}
isConverting = ` [CONVERTING: ${turnsLeft} T]`;
}
+
infoLines.push(`★ ${p.name} COMMANDER${isStranded}${isConverting} (${p.commander.hp} HP)`);
}
});
@@ -4413,9 +4557,57 @@
tooltipHTML = infoLines.join(' ');
}
}
+
+ // --- NEW: UNIVERSAL ACTION VALIDATOR FOR V.A.T.S. ---
+ let actionBlockedMsg = "";
+
+ if (Gamestate.targetingMode === 'relic' && Gamestate.pendingRelic) {
+ let rId = Gamestate.pendingRelic.id;
+ if (rId === 'fatman' || rId === 'cryolator') {
+ if (targetCountry.owner === Gamestate.player.name) actionBlockedMsg = "CANNOT TARGET OWN TERRITORY";
+ else if (Gamestate.getDistanceToEmpire(targetCountry.name) > 3) actionBlockedMsg = "OUT OF RANGE (MAX 3)";
+ } else if (rId === 'capmine' && targetCountry.owner !== Gamestate.player.name) {
+ actionBlockedMsg = "MUST TARGET FRIENDLY TERRITORY";
+ } else if (rId === 'shroudcard' && targetCountry.owner === Gamestate.player.name) {
+ actionBlockedMsg = "CANNOT TARGET OWN TERRITORY";
+ } else if (rId === 'geck') {
+ let isStormed = Gamestate.radstorm && Gamestate.radstorm.state !== 'none' && Gamestate.radstorm.areas.includes(targetCountry.name);
+ if (targetCountry.radDecay <= 0 && !isStormed) {
+ actionBlockedMsg = "MUST TARGET IRRADIATED OR STORM ZONE";
+ }
+ }
+
+ } else if (Gamestate.stage === "Nuke Targeting") {
+ if (targetCountry.owner === Gamestate.player.name) actionBlockedMsg = "CANNOT TARGET OWN TERRITORY";
+ } else if (Gamestate.stage === "Fortify") {
+ if (targetCountry.owner === Gamestate.player.name) {
+ if (targetCountry.isFrozen > 0) actionBlockedMsg = "TERRITORY IS FROZEN SOLID";
+ else if (targetCountry.isBlockaded > 0) actionBlockedMsg = "TERRITORY IS BLOCKADED";
+ }
+ } else if (Gamestate.stage === "Battle" && Gamestate.prevCountry && targetCountry.owner !== Gamestate.player.name) {
+ if (targetCountry.isLockedDown) actionBlockedMsg = "TARGET IS LOCKED DOWN";
+ else if (Gamestate.isAllianceMode && Gamestate.areAllies(Gamestate.player.name, targetCountry.owner)) actionBlockedMsg = "PERMANENT ALLY (FRIENDLY FIRE OFF)";
+ else {
+ let truce = Gamestate.getTruce ? Gamestate.getTruce(Gamestate.player.name, targetCountry.owner) : null;
+ if (truce && truce.locked) actionBlockedMsg = "FORCED TRUCE ACTIVE";
+ }
+ } else if (Gamestate.stage === "Maneuver" && Gamestate.prevCountry && targetCountry.owner === Gamestate.player.name) {
+ if (targetCountry.isLockedDown) actionBlockedMsg = "DESTINATION IS LOCKED DOWN";
+ } else if (Gamestate.stage === "Frenzy Targeting" && !Gamestate.prevCountry) {
+ if (targetCountry.owner === Gamestate.player.name && (targetCountry.army <= 1 || targetCountry.isExploring || targetCountry.isLockedDown)) {
+ actionBlockedMsg = "INVALID LAUNCH POINT";
+ }
+ }
+
+ if (actionBlockedMsg && !isShrouded) {
+ tooltipHTML = `
[ BLOCKED: ${actionBlockedMsg} ]
` + tooltipHTML;
+ }
+ // --- END VALIDATOR ---
+
vatsTooltip.innerHTML = tooltipHTML;
vatsTooltip.style.left = (e.clientX + 20) + "px";
vatsTooltip.style.top = (e.clientY + 20) + "px";
+
vatsTooltip.style.display = "block";
});
@@ -4671,8 +4863,9 @@
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"]
+ "fo4": ["The Minutemen", "The Institute", "The Railroad", "The Gunners", "Nuka-World Raiders", "Maxson's Brotherhood"]
};
+
const factions = factionThemes[selectedTheme] || [];
factions.forEach(factionName => {
@@ -4779,14 +4972,25 @@
Gamestate.logAction = function (message, isImportant = false, isNuke = false) {
return new Promise(resolve => {
let turbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked;
- // ---> NEW: Only skip non-debug logs during AI Turbo on mobile
- if (this.aiTurn && window.innerWidth <= 950 && turbo && !message.startsWith('> [DEBUG]')) {
- resolve();
- return;
- }
-
+
+ // --- FIX: Master AI Silencer for Turbo Mode ---
+ // If it is the AI's turn AND Turbo is checked, we silence the log.
+ // Exceptions: We NEVER silence the Tactical Backbrief, Debug logs, or actions involving the Human Player.
+ if (this.aiTurn && turbo) {
+ let isBackbrief = message.includes("[ TACTICAL BACKBRIEF ]");
+ let isDebug = message.startsWith('> [DEBUG]');
+ let involvesPlayer = message.includes(this.player.name);
+
+ // If it's a standard AI action and doesn't involve the player, throw it in the trash!
+ if (!isBackbrief && !isDebug && !involvesPlayer) {
+ resolve();
+ return;
+ }
+ }
+ // --- END SILENCER ---
this.logQueue.push({ message, isImportant, isNuke, resolve });
+
this.processLogQueue();
});
}
@@ -4840,10 +5044,13 @@
let i = 0;
entry.textContent = "";
- let typeSpeed = turboToggle && turboToggle.checked ? 0 : 8;
+ // --- FIX: Force instant text rendering during Initial Placement phases ---
+ let isSetupPhase = this.stage === "Initial Claim" || this.stage === "Initial Reinforce";
+ let typeSpeed = (turboToggle && turboToggle.checked) || isSetupPhase ? 0 : 8;
if (typeSpeed === 0) {
entry.textContent = fullText; combatLog.scrollTop = combatLog.scrollHeight; this.isLogging = false; logData.resolve(); this.processLogQueue();
+
} else {
let typeInterval = setInterval(() => {
entry.textContent += fullText.charAt(i); i++; combatLog.scrollTop = combatLog.scrollHeight;
@@ -4986,10 +5193,15 @@ Gamestate.renderInventory = function () {
// --- [ INJECTED PHASE LOCK LOGIC ] ---
if (item.owner === this.player.name) {
let isLocked = false;
- if (item.key === 'c' && (this.stage !== "Recruitment" && this.stage !== "Fortify")) isLocked = true;
- if ((item.key === 's' || item.key === 'l') && (this.stage === "Commander Phase" || this.stage === "Maneuver")) isLocked = true;
+ // Charisma: Recruitment & Fortify only
+ if (item.key === 'c' && this.stage !== "Recruitment" && this.stage !== "Fortify") isLocked = true;
+ // Agility: Maneuver & Commander Phase only
+ if (item.key === 'a' && this.stage !== "Maneuver" && this.stage !== "Commander Phase") isLocked = true;
+ // Strength, Endurance, Perception, Intel, Luck: Battle & Commander Phase only
+ if (['s', 'e', 'p', 'i', 'l'].includes(item.key) && this.stage !== "Battle" && this.stage !== "Commander Phase") isLocked = true;
if (item.cooldown > 0) {
+
btn.disabled = true;
btn.classList.add('cooldown');
innerHTML += ` COOLDOWN: ${item.cooldown}`;
@@ -5057,19 +5269,45 @@ Gamestate.renderInventory = function () {
container.appendChild(stimpakBtn);
}
+ // --- NEW: DOGMEAT HEALING BUTTON ---
+ if (this.player.dogmeatStatus === 'injured') {
+ const healBtn = document.createElement('button');
+ healBtn.className = 'bobblehead-btn';
+ let innerHTML = `TEND TO DOGMEAT'S WOUNDSHeal Dogmeat to remove his negative effects and unlock his powerful companion buffs.`;
+
+ let hasStimpak = this.commandersEnabled && this.player.commander && this.player.commander.stimpaks > 0;
+ let hasCaps = this.wastelandEconomyActive && this.player.caps >= 50;
+
+ if (hasStimpak || hasCaps) {
+ healBtn.disabled = false;
+ healBtn.onclick = () => { if (this.healDogmeat) this.healDogmeat(); };
+ innerHTML += ` [ HEAL (1 STIMPAK or 50 CAPS) ]`;
+ } else {
+ healBtn.disabled = true;
+ healBtn.style.opacity = '0.7';
+ innerHTML += ` [ REQUIRES 1 STIMPAK or 50 CAPS ]`;
+ }
+
+ healBtn.innerHTML = innerHTML;
+ container.appendChild(healBtn);
+ }
+
// --- [ UPDATED PULSE LOGIC ] ---
const navInv = document.getElementById('nav-inv');
+
if (navInv) {
const isStimpakReady = this.commandersEnabled && this.player.commander && this.player.commander.stimpaks > 0 && this.player.commander.hp < 100 && this.player.commander.ap > 0 && this.stage === 'Commander Phase';
// This line now correctly checks for phase-locks before pulsing the INV tab
const isBobbleheadReady = this.bobbleheads && this.bobbleheads.some(b => {
if (b.owner !== this.player.name || b.cooldown > 0) return false;
- if (b.key === 'c' && (this.stage !== "Recruitment" && this.stage !== "Fortify")) return false;
- if ((b.key === 's' || b.key === 'l') && (this.stage === "Commander Phase" || this.stage === "Maneuver")) return false;
+ if (b.key === 'c' && this.stage !== "Recruitment" && this.stage !== "Fortify") return false;
+ if (b.key === 'a' && this.stage !== "Maneuver" && this.stage !== "Commander Phase") return false;
+ if (['s', 'e', 'p', 'i', 'l'].includes(b.key) && this.stage !== "Battle" && this.stage !== "Commander Phase") return false;
return true;
});
+
if (isStimpakReady || isBobbleheadReady) {
navInv.classList.add('inv-pulse');
} else {
@@ -5131,9 +5369,64 @@ Gamestate.renderInventory = function () {
};
+ // --- DEV TOOLS: DOGMEAT INJECTION ---
+ Gamestate.devTriggerDogmeat = function() {
+ // --- FAILSAFE: If the variable is missing, build it right now! ---
+ if (!this.dogmeatQuest) {
+ this.dogmeatQuest = { active: false, target: null, timer: 0, resolved: false, cooldown: 0 };
+ }
+
+ // Simplified target filter just to make absolutely sure it finds someone to put the quest on
+ let validTargets = this.countries.filter(c => c.owner !== "none" && c.owner !== this.player.name && !c.isCrater && c.army > 0);
+
+ if (validTargets.length > 0) {
+ let randomTarget = validTargets[Math.floor(Math.random() * validTargets.length)];
+ this.dogmeatQuest.active = true;
+ this.dogmeatQuest.target = randomTarget.name;
+ this.dogmeatQuest.timer = 5;
+ this.dogmeatQuest.resolved = false;
+ this.dogmeatQuest.cooldown = 0;
+
+ if (this.queueToast) this.queueToast(`>>> NEW RUMOR <<< [ CX404 ] LONE DOG SIGHTED`, "var(--pip-color)", true);
+ if (this.logAction) this.logAction(`[ DEV OVERRIDE ] Scavengers spotted a dog defending a Red Rocket station at ${formatTerritoryName(randomTarget.name)}. You have ${this.dogmeatQuest.timer} turns to reach it!`, true);
+
+ this.updateInfo();
+ if (this.drawMapText) this.drawMapText();
+
+ // --- NEW: DEV MODE QUEST MODAL ---
+ this.modalIsOpen = true;
+ this.showEncounterModal(
+ "NEW QUEST: A BOY AND HIS DOG",
+ `Scavengers spotted a dog defending a Red Rocket station at ${formatTerritoryName(randomTarget.name)}.
Attack and conquer that territory within ${this.dogmeatQuest.timer} turns to trigger the rescue event!`,
+ [{id: "ok", text: "[Acknowledge] We are on our way."}],
+ () => { this.modalIsOpen = false; return null; }
+ );
+ } else {
+
+ if (this.showToast) this.showToast('No valid enemy targets for quest!', 'red');
+ }
+ };
+
+ Gamestate.devGrantDogmeat = function() {
+ if (!this.player) {
+ if (this.showToast) this.showToast('Game has not started yet!', 'red');
+ return;
+ }
+ if (!this.player.activeBuffs) this.player.activeBuffs = {};
+ this.player.activeBuffs.dogmeat = true;
+ this.player.dogmeatStatus = 'healthy';
+ if (this.dogmeatQuest) this.dogmeatQuest.resolved = true;
+
+ this.updateInfo();
+ if (this.renderInventory) this.renderInventory();
+ if (this.showToast) this.showToast('DOGMEAT GRANTED', 'var(--pip-color)');
+ };
+
Gamestate.init = function () {
+
if (winModal) winModal.style.display = "none";
if (!map) return;
+
if (modal) modal.style.display = "block";
this.injectHolidayEvents();
@@ -5402,8 +5695,20 @@ Gamestate.renderInventory = function () {
Gamestate.updateButtonText = function () {
if (!end) return;
+ end.disabled = false; // --- FIX: Release the Turbo Mode lock! ---
+
+ // --- FIX: Lock the End Cycle button during ALL Initial Setup phases ---
+ if (this.stage && this.stage.startsWith("Initial")) {
+ end.disabled = true;
+ end.textContent = "AWAITING DEPLOYMENT";
+ end.style.opacity = "0.5";
+ end.style.pointerEvents = "none";
+ return; // Stop here so it doesn't process other stages
+ }
+
if (this.stage === "Recruitment") {
end.textContent = "Skip Recruitment";
+
end.style.opacity = "1";
end.style.pointerEvents = "auto";
if (turnInfoMessage) turnInfoMessage.textContent = "Recruit new troops with your Caps, or Skip Recruitment to attack.";
@@ -5437,12 +5742,13 @@ Gamestate.renderInventory = function () {
end.style.opacity = "1";
end.style.pointerEvents = "auto";
} else if (this.stage === "AI Turn") {
- end.textContent = "AI is thinking...";
- end.style.opacity = "0.5";
+ // Let the dynamic loop handle the text
+ end.style.opacity = "0.8"; // Made slightly brighter
end.style.pointerEvents = "none";
}
}
+
// --- MASTER RELIC REGISTRY (Step 1 of Item System) ---
Gamestate.RelicDatabase = [
{ id: 'geck', name: 'G.E.C.K.', desc: 'Restores a Crater/Radstorm tile to lush land and spawns troops.', type: 'Targeted', consumed: true },
@@ -5528,21 +5834,31 @@ Gamestate.renderInventory = function () {
Gamestate.grantRelic = function (player, relic) {
if (!player.relics) player.relics = [];
- // Add to inventory
- player.relics.push({...relic});
-
- // Mark as "Found" globally so it doesn't drop again until used
- relic.found = true;
+ let newRelic = {...relic};
+ player.relics.push(newRelic);
+ relic.found = true; // Mark as "Found" globally
- if (player.isPlayer) {
- this.showToast(`RELIC RECOVERED: ${relic.name}`, "var(--pip-color)");
- if (this.renderInventory) this.renderInventory();
+ // --- FIX: Auto-Equip X-01 Power Armor ---
+ if (newRelic.id === 'x01armor') {
+ this.equipRelic(newRelic, player);
} else {
- this.logAction(`[ INTEL ] Reports indicate ${player.name} has secured a Pre-War artifact.`);
- // AI immediate use check (we will build this in Phase 4)
+ if (player.isPlayer) {
+ this.showToast(`RELIC RECOVERED: ${relic.name}`, "var(--pip-color)");
+ if (this.renderInventory) this.renderInventory();
+ } else {
+ let flavors = [
+ `[ INTEL ] Reports indicate ${player.name} has secured a Pre-War artifact.`,
+ `[ INTEL ] Scavengers working for ${player.name} unearthed a rare Wasteland Relic.`,
+ `[ INTEL ] Encrypted chatter suggests ${player.name} added a powerful artifact to their arsenal.`,
+ `[ INTEL ] Recon patrols spotted ${player.name}'s forces transporting a sealed Vault-Tec crate.`
+ ];
+ this.logAction(flavors[Math.floor(Math.random() * flavors.length)]);
+ }
+
}
};
+
Gamestate.activateRelic = function (relic) {
// Close the inventory modal first
document.getElementById('inventory-modal').style.display = 'none';
@@ -5566,34 +5882,80 @@ Gamestate.renderInventory = function () {
this.updateInfo();
};
- Gamestate.applyRelicEffect = async function (relic, target = null) {
+ // --- NEW: DISTANCE CALCULATOR FOR RELICS ---
+ Gamestate.getDistanceToEmpire = function(targetName, user = this.player) {
+ if (user.areas.includes(targetName)) return 0;
+ let queue = [...user.areas];
+ let visited = new Set(queue);
+ let distance = 0;
+
+ while (queue.length > 0) {
+ let nextQueue = [];
+ distance++;
+ for (let node of queue) {
+ let country = this.countries.find(c => c.name === node);
+ if (!country) continue;
+ for (let neighbor of country.neighbours) {
+ if (neighbor === targetName) return distance;
+ if (!visited.has(neighbor)) {
+ visited.add(neighbor);
+ nextQueue.push(neighbor);
+ }
+ }
+ }
+ queue = nextQueue;
+ }
+ return 999; // Unreachable
+ };
+
+ // --- FIX: Pass 'user' so AI gets its own rewards! ---
+ Gamestate.applyRelicEffect = async function (relic, target = null, user = this.player) {
+
let success = false;
switch (relic.id) {
case 'jet':
- this.player.extraTurn = true;
- await this.logAction("[ CHEMICALS ] Jet consumed. Reflexes heightened. Extra turn granted.", true);
+ user.extraTurn = true;
+ if (user.isPlayer) await this.logAction("[ CHEMICALS ] Jet consumed. Reflexes heightened. Extra turn granted.", true);
+ else await this.logAction(`[ INTEL ] ${user.name} consumed Jet! They are moving with impossible speed!`, true);
success = true;
break;
case 'stealthboy':
- this.player.stealthActive = 2; // Duration in turns
- await this.logAction("[ STEALTH ] Stealth Boy activated. You are invisible to enemy radar for 2 turns.", true);
+ user.stealthActive = 2; // Duration in turns
+ if (user.isPlayer) await this.logAction("[ STEALTH ] Stealth Boy activated. You are invisible to enemy radar for 2 turns.", true);
+ else await this.logAction(`[ INTEL ] ${user.name} activated a Stealth Boy. We've lost them on radar!`, true);
success = true;
break;
case 'lunchbox':
let troops = 10; let caps = 20;
- this.player.reserve += troops; this.player.caps += caps;
- await this.logAction(`[ LUNCHBOX ] You found ${troops} troops and ${caps} Caps! Wowie!`, true);
+ user.reserve += troops;
+ if (this.wastelandEconomyActive) user.caps += caps;
+ if (user.isPlayer) await this.logAction(`[ LUNCHBOX ] You found ${troops} troops and ${caps} Caps! Wowie!`, true);
+ else await this.logAction(`[ INTEL ] ${user.name} opened a Vault-Tec Lunchbox!`, true);
success = true;
break;
- case 'fatman':
- if (target.owner === this.player.name) {
- this.showToast("Cannot target own territory!", "red");
+ case 'fatman':
+ if (target.owner === user.name) {
+ if (user.isPlayer) this.showToast("Cannot target own territory!", "red");
return;
}
+ if (this.getDistanceToEmpire(target.name) > 3) {
+ this.showToast("Target out of range! Must be within 3 territories.", "red");
+ return;
+ }
+
+ // --- NEW: VISUAL FLASH ---
+
+ let fatmanEl = document.getElementById(target.name);
+ if (fatmanEl) {
+ fatmanEl.classList.add('flash');
+ setTimeout(() => fatmanEl.classList.remove('flash'), 1200);
+ }
+
const losses = Math.floor(target.army * 0.6);
+
target.army -= losses;
const targetPlayer = this.players.find(p => p.name === target.owner);
if (targetPlayer) targetPlayer.army -= losses;
@@ -5602,11 +5964,26 @@ Gamestate.renderInventory = function () {
break;
case 'geck':
- // Check if it's a Crater (from a previous Nuke)
- if (target.isCrater || target.hasRadstorm) {
+ let isStormed = this.radstorm && this.radstorm.state !== 'none' && this.radstorm.areas.includes(target.name);
+ if (target.radDecay > 0 || isStormed) {
+ // --- NEW: VISUAL FLASH ---
+ let geckEl = document.getElementById(target.name);
+ if (geckEl) {
+ geckEl.classList.add('radstorm-active');
+ setTimeout(() => geckEl.classList.remove('radstorm-active'), 1200);
+ }
+
target.isCrater = false;
- target.hasRadstorm = false;
+ target.radDecay = 0; // Cleanse radiation!
+
+ // Cleanse Radstorm
+ if (isStormed) {
+ this.radstorm.areas = this.radstorm.areas.filter(a => a !== target.name);
+ if (geckEl) geckEl.classList.remove('radstorm-warning', 'radstorm-active');
+ }
+
target.army += 15; this.player.army += 15;
+
target.owner = this.player.name;
target.color = this.player.color;
await this.logAction(`[ TERRAFORM ] G.E.C.K. deployed. ${formatTerritoryName(target.name)} has been restored to lush wasteland.`, true);
@@ -5621,10 +5998,30 @@ Gamestate.renderInventory = function () {
if (target.owner === this.player.name) {
this.showToast("Cannot target own territory!", "red"); return;
}
- target.isFrozen = 2; // Freeze for 2 turns
+ if (this.getDistanceToEmpire(target.name) > 3) {
+ this.showToast("Target out of range! Must be within 3 territories.", "red");
+ return;
+ }
+ target.isFrozen = 1; // FIX: Freeze for exactly 1 turn as intended
+
await this.logAction(`[ CRYOGENIC ] ${formatTerritoryName(target.name)} has been frozen solid. All actions blocked.`, true);
success = true;
break;
+
+ case 'superstimpak':
+ if (this.commandersEnabled && this.player.commander) {
+ // Calculate max HP including Life Giver and X-01 Armor
+ let maxHP = 100 + (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.lifeGiver ? this.player.activeBuffs.lifeGiver * 25 : 0);
+ if (this.player.relics && this.player.relics.some(r => r.id === 'x01armor' && r.isEquipped)) maxHP += 50;
+
+ this.player.commander.hp = maxHP;
+ await this.logAction(`[ MEDICAL ] Super Stimpak administered! Commander fully healed to ${maxHP} HP.`, true);
+ success = true;
+ } else {
+ this.showToast("You must have an active Commander to use this.", "red");
+ }
+ break;
+
case 'capmine':
if (target.owner !== this.player.name) {
@@ -5673,20 +6070,24 @@ Gamestate.renderInventory = function () {
this.updateInfo();
};
- Gamestate.equipRelic = function (relic) {
- if (!this.player.activeBuffs) this.player.activeBuffs = {};
+ Gamestate.equipRelic = function (relic, targetPlayer = this.player) {
+ if (!targetPlayer.activeBuffs) targetPlayer.activeBuffs = {};
if (relic.id === 'x01armor') {
- this.player.activeBuffs.x01armor = true;
- this.showToast("X-01 POWER ARMOR EQUIPPED", "var(--pip-color)");
- this.logAction("[ EQUIPMENT ] Commander has donned X-01 Power Armor. Armor and Defense boosted.", true);
-
- // Remove from inventory grid but keep in player.relics (as equipped)
+ targetPlayer.activeBuffs.x01armor = true;
relic.isEquipped = true;
- if (this.renderInventory) this.renderInventory();
+
+ if (targetPlayer.isPlayer) {
+ this.showToast("X-01 POWER ARMOR EQUIPPED", "var(--pip-color)");
+ this.logAction("[ EQUIPMENT ] Commander has donned X-01 Power Armor. Armor and Defense boosted.", true);
+ if (this.renderInventory) this.renderInventory();
+ } else {
+ this.logAction(`[ INTEL ] ${targetPlayer.name} has equipped the X-01 Power Armor!`);
+ }
}
};
+
Gamestate.aiRelicCheck = async function (aiPlayer) {
if (!aiPlayer.relics || aiPlayer.relics.length === 0) return;
@@ -5694,16 +6095,65 @@ Gamestate.renderInventory = function () {
let relic = aiPlayer.relics[i];
let used = false;
- // AI Logic for specific items
- if (relic.id === 'jet' || relic.id === 'lunchbox' || relic.id === 'x01armor') {
- // Instant use items
- await this.applyRelicEffect(relic);
+ // --- FIX: Pass aiPlayer as the 3rd parameter to all applyRelicEffect calls! ---
+
+ // 1. INSTANT ITEMS
+ if (relic.id === 'jet' || relic.id === 'lunchbox' || relic.id === 'x01armor' || relic.id === 'stealthboy') {
+ await this.applyRelicEffect(relic, null, aiPlayer);
used = true;
- } else if (relic.id === 'fatman') {
- // Target strongest enemy border
+ }
+ // 2. HEALING ITEMS
+ else if (relic.id === 'superstimpak') {
+ if (this.commandersEnabled && aiPlayer.commander && aiPlayer.commander.hp <= 50) {
+ await this.applyRelicEffect(relic, null, aiPlayer);
+ used = true;
+ }
+ }
+ // 3. HAZARD PROTECTION
+ else if (relic.id === 'radaway') {
+ if (this.hazardsEnabled && this.radstorm && this.radstorm.state !== 'none') {
+ await this.applyRelicEffect(relic, null, aiPlayer);
+ used = true;
+ }
+ }
+ // 4. EXPEDITION COMPLETION
+ else if (relic.id === 'survivalguide') {
+ let exploring = this.countries.some(c => c.owner === aiPlayer.name && c.isExploring);
+ if (exploring) {
+ await this.applyRelicEffect(relic, null, aiPlayer);
+ used = true;
+ }
+ }
+ // 5. OFFENSIVE TARGETING (Nukes, Freezes, Blockades)
+ else if (relic.id === 'fatman' || relic.id === 'cryolator' || relic.id === 'shroudcard') {
let target = this.getStrongestEnemyBorder(aiPlayer);
- if (target) {
- await this.applyRelicEffect(relic, target);
+ if (target && this.getDistanceToEmpire(target.name, aiPlayer) <= 3) {
+ await this.applyRelicEffect(relic, target, aiPlayer);
+ used = true;
+ }
+ }
+ // 6. DEFENSIVE TARGETING (Bottlecap Mines)
+ else if (relic.id === 'capmine') {
+ let borders = this.countries.filter(c => c.owner === aiPlayer.name && !c.hasMine && c.neighbours.some(n => {
+ let nc = this.countries.find(x => x.name === n);
+ return nc && nc.owner !== aiPlayer.name && !this.areAllies(aiPlayer.name, nc.owner);
+ }));
+ if (borders.length > 0) {
+ borders.sort((a, b) => a.army - b.army); // Protect the weakest border
+ await this.applyRelicEffect(relic, borders[0], aiPlayer);
+ used = true;
+ }
+ }
+ // 7. TERRAFORMING (G.E.C.K.)
+ else if (relic.id === 'geck') {
+ // --- FIX: AI now correctly identifies the central Radstorm array ---
+ let options = this.countries.filter(c => (c.owner === aiPlayer.name || c.neighbours.some(n => {
+ let nc = this.countries.find(x => x.name === n);
+ return nc && nc.owner === aiPlayer.name;
+ })) && (c.radDecay > 0 || (this.radstorm && this.radstorm.state !== 'none' && this.radstorm.areas.includes(c.name))));
+
+ if (options.length > 0) {
+ await this.applyRelicEffect(relic, options[0], aiPlayer);
used = true;
}
}
@@ -5747,13 +6197,25 @@ Gamestate.renderInventory = function () {
const perk = pool[Math.floor(Math.random() * pool.length)];
// Apply the perk to the AI's buffs
- if (!player.activeBuffs[perk.id]) player.activeBuffs[perk.id] = 0;
- player.activeBuffs[perk.id]++;
+ if (perk.type === 'Instant') {
+ this.applyInstantPerk(player, perk);
+ } else {
+ if (!player.activeBuffs[perk.id]) player.activeBuffs[perk.id] = 0;
+ player.activeBuffs[perk.id]++;
+ }
// Report the event in the Vault-Tec Action Log
- this.logAction(`[ INTEL ] Reports indicate ${player.name} has reached Level ${player.level} and acquired the '${perk.name}' perk.`);
+ let flavors = [
+ `[ INTEL ] Reports indicate ${player.name} has reached Level ${player.level} and acquired the '${perk.name}' perk.`,
+ `[ INTEL ] ${player.name}'s forces are growing more elite. They achieved Level ${player.level} ('${perk.name}').`,
+ `[ INTEL ] Field combat experience has pushed ${player.name} to Level ${player.level}. New doctrine: '${perk.name}'.`,
+ `[ INTEL ] Vault-Tec profiling confirms ${player.name} is now Level ${player.level}, utilizing '${perk.name}' tactics.`
+ ];
+ this.logAction(flavors[Math.floor(Math.random() * flavors.length)]);
+
};
+
Gamestate.updateXPBar = function() {
const bar = document.getElementById('xp-bar-container');
const label = document.getElementById('level-label');
@@ -5860,20 +6322,32 @@ Gamestate.renderInventory = function () {
// Filter out Ninja if Fog of War is off (since it hides movement)
if (perk.id === 'ninja' && !fogOfWar) return false;
+
+ // --- NEW: FILTER OWNED NON-STACKABLE PERKS ---
+ if (!perk.stackable && player.activeBuffs && player.activeBuffs[perk.id] > 0) return false;
+
return true;
});
// --- [ END FILTERING ] ---
+
// 2. Randomize and pick 3 unique options
let choices = allPerks.sort(() => 0.5 - Math.random()).slice(0, 3);
const title = `LEVEL UP: ${player.level}`;
let message = `You have reached a new level of wasteland expertise! Select a permanent perk to enhance your faction's capabilities.`;
- let modalChoices = choices.map(perk => ({
- id: perk.id,
- text: `
${factionName} is suffering heavy casualties and requests emergency reinforcements.
Transfer ${troopsRequested} Troops from your reserves?`;
} else {
+ headerEl.textContent = "[ INCOMING ENVOY ]";
+ headerEl.style.color = "var(--pip-color)";
+ headerEl.style.borderColor = "var(--pip-color)";
+ headerEl.style.textShadow = "0 0 5px var(--pip-color)";
+
+ acceptBtn.textContent = `ACCEPT (+${caps} CAPS)`;
+ rejectBtn.textContent = "REJECT";
+
msgEl.innerHTML = `${factionName} offers ${caps} Bottle Cap for a ${turns}-Round Ceasefire.
Do you accept the terms?`;
}
@@ -7271,8 +7927,6 @@ Gamestate.openDiplomacy = function (targetName) {
this.modalIsOpen = true;
- let acceptBtn = document.getElementById('envoy-accept');
- let rejectBtn = document.getElementById('envoy-reject');
let newAccept = acceptBtn.cloneNode(true);
let newReject = rejectBtn.cloneNode(true);
acceptBtn.parentNode.replaceChild(newAccept, acceptBtn);
@@ -7283,6 +7937,7 @@ Gamestate.openDiplomacy = function (targetName) {
});
}
+
Gamestate.showAsylumModal = function(aiName) {
return new Promise(resolve => {
let modal = document.getElementById('asylum-modal');
@@ -7792,11 +8447,74 @@ Gamestate.handleClick = function (e) {
}
}
+ // --- NEW: INITIAL PLACEMENT HANDLERS ---
+ if (this.stage === "Initial Claim") {
+ let country = this.countries.find(c => c.name === e.target.id);
+ if (!country || country.isCrater || country.isSilo || country.owner !== "none") {
+ if (this.showToast) this.showToast("Select an unowned territory.", "grey");
+ return;
+ }
+ country.owner = this.player.name;
+ country.color = this.player.color;
+ country.army = 1;
+ this.player.areas.push(country.name);
+ this.player.army++;
+ this.player.reserve--;
+
+ let el = document.getElementById(country.name);
+ if (el) el.style.fill = this.player.color;
+
+ this.logAction(`[ CLAIM ] You claimed ${formatTerritoryName(country.name)}.`);
+
+ this.setupPlayerIndex++; // Pass the turn to the next player
+ if (this.processInitialPlacement) this.processInitialPlacement();
+ return;
+
+ } else if (this.stage === "Initial Reinforce") {
+ let country = this.countries.find(c => c.name === e.target.id);
+ if (!country || country.isCrater || country.owner !== this.player.name) {
+ if (this.showToast) this.showToast("Select your own territory to reinforce.", "grey");
+ return;
+ }
+ if (this.player.reserve <= 0) return;
+
+ // Place 1 troop per click during setup
+ country.army++;
+ this.player.army++;
+ this.player.reserve--;
+
+ // --- FIX: Use drawMapText to respect fog ---
+ if (this.drawMapText) this.drawMapText();
+
+ // Do not await the log here so it doesn't block the UI
+ this.logAction(`[ DEPLOYMENT ] You deployed 1 troop to ${formatTerritoryName(country.name)}.`);
+
+ this.setupPlayerIndex++; // Pass the turn to the next player
+ if (this.processInitialPlacement) this.processInitialPlacement();
+ return;
+
+ } else if (this.stage === "Initial Commander") {
+ let country = this.countries.find(c => c.name === e.target.id);
+ if (!country || country.isCrater || country.owner !== this.player.name) {
+ if (this.showToast) this.showToast("Select your own territory to deploy your Commander.", "grey");
+ return;
+ }
+
+ this.player.commander.loc = country.name;
+ if (this.drawMapText) this.drawMapText();
+ this.logAction(`[ DEPLOYMENT ] You deployed your Commander to ${formatTerritoryName(country.name)}.`);
+
+ if (this.processInitialPlacement) this.processInitialPlacement();
+ return;
+ }
+
+ // --- STANDARD GAME HANDLERS ---
if (this.stage === "Fortify") {
this.addArmy(e);
} else if (this.stage === "Battle" || this.stage === "Frenzy Targeting") {
this.attack(e);
+
} else if (this.stage === "Maneuver" || this.stage === "Commander Phase") {
this.maneuver(e);
} else if (this.stage === "Nuke Targeting") {
@@ -7872,27 +8590,10 @@ Gamestate.handleClick = function (e) {
if (winModal) winModal.style.display = "block";
}
-Gamestate.restart = function () {
- if (modal) modal.style.display = "flex";
- if (winModal) winModal.style.display = "none";
-
- // --- NEW: COMPLETE RADIO SHUTDOWN ON REBOOT ---
- isRadioActive = false;
- let rBtn = document.getElementById('radio-toggle');
- if (rBtn) rBtn.classList.remove('radio-on');
-
- if (typeof pipboyAudio !== 'undefined') {
- pipboyAudio.pause();
- pipboyAudio.currentTime = 0;
- // This explicitly wipes the memory so the radio doesn't skip track 1
- // of your new theme when you turn it back on!
- pipboyAudio.removeAttribute('src');
- }
-
- if (typeof broadcastDelay !== 'undefined') {
- clearTimeout(broadcastDelay);
- }
+ Gamestate.restart = function () {
+ window.location.reload();
}
+
// --- INTEL ANIMATION LOGIC ---
Gamestate.intelAnimationTimer = null; // This will hold the animation timer
@@ -7966,10 +8667,17 @@ Gamestate.restart = function () {
// --- Player Name (Bold) ---
if (leaderEl) {
let levelSuffix = (this.levelingEnabled && player.level) ? ` (Lvl ${player.level})` : "";
- leaderEl.innerHTML = player.name + levelSuffix;
+ let dogmeatSuffix = "";
+ if (player.dogmeatStatus === 'healthy') {
+ dogmeatSuffix = ` `;
+ } else if (player.dogmeatStatus === 'injured') {
+ dogmeatSuffix = ` `;
+ }
+ leaderEl.innerHTML = player.name + dogmeatSuffix + levelSuffix;
}
+
// --- Faction Name & Reputation ---
if (countryEl) {
let countryHtml = player.country;
@@ -8091,11 +8799,12 @@ Gamestate.restart = function () {
// --- NEW: PLAYER COLOR IDENTIFIER & ALLIANCE STATUS ---
let colorBar = infoBox.querySelector('.player-color-bar');
if (!colorBar) {
- colorBar = document.createElement('div');
+ colorBar = document.createElement('button'); // --- FIX: Create as a button to perfectly match AI dimensions
// Give it the exact same CSS class as the standard buttons!
colorBar.className = 'btn-diplomacy player-color-bar';
infoBox.appendChild(colorBar);
}
+
if (player.name === this.player.name) {
colorBar.style.display = "block";
@@ -8132,8 +8841,8 @@ Gamestate.restart = function () {
// --- FINAL BUTTON LOGIC FOR DIPLOMACY & ALLIANCE ---
- // FIX: Specify 'button' so it doesn't accidentally grab our new color bar!
- let dipBtn = infoBox.querySelector('button.btn-diplomacy');
+ // FIX: Exclude the player-color-bar so we don't accidentally hide it!
+ let dipBtn = infoBox.querySelector('button.btn-diplomacy:not(.player-color-bar)');
// First, a universal rule: no buttons for the player, dead/neutral factions.
if ((player.name === this.player.name) || player.isNeutral || player.areas.length === 0) {
@@ -8141,6 +8850,7 @@ Gamestate.restart = function () {
}
+
// If we are in Alliance Mode...
else if (this.isAllianceMode) {
// And this player is our ally...
@@ -8277,29 +8987,39 @@ Gamestate.restart = function () {
}
};
} else {
- // Original logic for classic mode (remains untouched)
- if (this.getBestTrade(this.player.cards)) {
+ // --- FIX: Dynamic Button Text for Stash ---
+ let isValidPhase = (this.stage === "Fortify" || this.stage === "Recruitment");
+ let hasSet = this.getBestTrade(this.player.cards);
+ let canTrade = hasSet && !this.aiTurn && isValidPhase;
+
+ if (canTrade) {
viewCardsBtn.textContent = "OPEN STASH";
- viewCardsBtn.disabled = false; // <--- FIX: explicitly re-enable the button
+ viewCardsBtn.disabled = false;
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.disabled = true; // <--- FIX: explicitly disable the button
+ if (hasSet) {
+ viewCardsBtn.textContent = "WRONG PHASE";
+ } else {
+ viewCardsBtn.textContent = this.player.cards.length > 0 ? "NO ELIGIBLE SETS" : "STASH EMPTY";
+ }
+ viewCardsBtn.disabled = true;
viewCardsBtn.style.opacity = "0.5";
viewCardsBtn.style.pointerEvents = "none";
viewCardsBtn.classList.remove('ready-to-trade');
}
- // We also need to make sure the onclick is set for classic mode.
+ // We also need to make sure the onclick is securely locked.
viewCardsBtn.onclick = () => {
- if (this.aiTurn) return; // <--- BONUS FIX: Prevents opening stash during AI turns
+ if (this.aiTurn || !isValidPhase) return;
let cardsModal = document.getElementById('cards-modal');
if (cardsModal) cardsModal.style.display = 'flex';
this.renderCards();
};
}
+
+
}
@@ -8352,11 +9072,23 @@ Gamestate.restart = function () {
hpFill.style.width = "0%"; hpFill.style.opacity = "0"; hpFill.style.visibility = "hidden";
apFill.style.width = "0%"; apFill.style.opacity = "0"; apFill.style.visibility = "hidden";
}
+ // --- FIX: Lift fog during initial setup so players can see where to place troops ---
+ let isSetupPhase = this.stage === "Initial Claim" || this.stage === "Initial Reinforce";
+ // --- FIX: Fog of War applies during setup phases as requested ---
const fogEnabled = document.getElementById('opt-fog-of-war') && document.getElementById('opt-fog-of-war').checked && !Gamestate.devFogLifted;
+
const pBobbleActive = this.bobbleheads && this.bobbleheads.find(b => b.key === 'p' && b.active && b.owner === this.player.name);
const visibleTerritories = new Set();
if (fogEnabled && !pBobbleActive && this.player.alive) {
+ // --- NEW: Manual Mode Vision Exception ---
+ // During Manual Claiming, all empty grey lands are globally visible so players can draft them
+ if (this.stage === "Initial Claim") {
+ this.countries.forEach(c => {
+ if (c.owner === "none") visibleTerritories.add(c.name);
+ });
+ }
+
this.player.areas.forEach(a => visibleTerritories.add(a));
this.player.areas.forEach(areaName => {
const c = this.countries.find(x => x.name === areaName);
@@ -8402,7 +9134,9 @@ Gamestate.restart = function () {
const isVisible = !fogEnabled || pBobbleActive || visibleTerritories.has(country.name) || !this.player.alive;
if (isVisible) {
areaOnMap.classList.remove('fog-shroud');
+ // --- FIX: Restore faction color when visible ---
areaOnMap.style.fill = country.color;
+
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');
@@ -8410,7 +9144,9 @@ Gamestate.restart = function () {
} else {
areaOnMap.classList.add('fog-shroud');
- areaOnMap.style.fill = '';
+ // --- FIX: Strip faction color completely when shrouded ---
+ areaOnMap.style.fill = 'transparent';
+
areaOnMap.classList.remove('glowing-sea');
areaOnMap.classList.remove('allied-territory');
}
@@ -8437,21 +9173,29 @@ Gamestate.restart = function () {
document.getElementById('nuke-silo-status').style.color = hasSilo ? "#18ff62" : "#ff3333";
let nukeBtn = document.getElementById('btn-launch-nuke');
nukeBtn.textContent = `NUKES: ${this.player.codes}/4 CODES`;
- if (this.player.codes >= 4 && hasSilo && !this.activeNuke) {
+
+ let myActiveNuke = this.activeNukes ? this.activeNukes.find(n => n.launcher === this.player.name) : null;
+
+ if (this.player.codes >= 4 && hasSilo && !myActiveNuke) {
nukeBtn.disabled = false; nukeBtn.classList.add('nuke-ready'); nukeBtn.textContent = "INITIATE LAUNCH";
- } else if (this.activeNuke && this.activeNuke.launcher === this.player.name) {
- nukeBtn.disabled = true; nukeBtn.classList.remove('nuke-ready'); nukeBtn.textContent = `LAUNCH IN T-${this.activeNuke.turns}`;
+ } else if (myActiveNuke) {
+ nukeBtn.disabled = true; nukeBtn.classList.remove('nuke-ready'); nukeBtn.textContent = `LAUNCH IN T-${myActiveNuke.turns}`;
} else {
nukeBtn.disabled = true; nukeBtn.classList.remove('nuke-ready');
}
+
}
if (this.commandersEnabled && this.player.commander) {
- document.getElementById('cmdr-hp-text').textContent = `HP: ${this.player.commander.hp}/100`;
+ let maxHP = 100 + (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.lifeGiver ? this.player.activeBuffs.lifeGiver * 25 : 0);
+ if (this.player.relics && this.player.relics.some(r => r.id === 'x01armor' && r.isEquipped)) maxHP += 50;
+
+ document.getElementById('cmdr-hp-text').textContent = `HP: ${this.player.commander.hp}/${maxHP}`;
document.getElementById('cmdr-ap-text').textContent = `AP: ${this.player.commander.ap}/2`;
let hpFill = document.getElementById('cmdr-hp-fill');
if (hpFill) {
- hpFill.style.width = `${this.player.commander.hp}%`;
+ hpFill.style.width = `${(this.player.commander.hp / maxHP) * 100}%`;
+
// --- NEW: Add a pulse animation for critical health ---
if (!document.getElementById('cmdr-hp-pulse-style')) {
@@ -8485,13 +9229,15 @@ Gamestate.restart = function () {
if (this.player.commander.isConverting) {
let turnsLeft = 0;
+ let reqTurns = (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.infiltrator) ? 2 : 3;
if (locCountry.army > 0) {
- turnsLeft = Math.ceil(locCountry.army / 10) + 3; // Attrition turns + final turns
+ turnsLeft = Math.ceil(locCountry.army / 10) + reqTurns; // Attrition turns + final turns
} else {
- turnsLeft = 3 - (this.player.commander.siegeTurns || 0);
+ turnsLeft = reqTurns - (this.player.commander.siegeTurns || 0);
}
btnConvert.textContent = `CANCEL CONVERSION (${turnsLeft} T)`;
btnConvert.disabled = false;
+
} else {
btnConvert.textContent = "CONVERT TERRITORY";
btnConvert.disabled = (this.player.commander.ap < 1);
@@ -8544,24 +9290,26 @@ Gamestate.restart = function () {
};
}
- // --- PERK 2: The Gunners / Minutemen ---
+ // --- PERK 2: The Gunners / Minutemen ---
else if (this.player.perk.id === 'mercenary_contracts' || this.player.perk.id === 'minutemen_contracts' || this.player.perk.id === 'gunner_contracts') {
showButton = true;
const perkCooldown = this.player.perk.cooldown || 3;
- tooltipText = this.player.perk.description + ` (${perkCooldown} Turn Cooldown)`; // Use the dynamic description
+ tooltipText = this.player.perk.description + ` (${perkCooldown} Turn Cooldown)`;
+
+ let mercCost = (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.gunNut) ? 15 : 20;
if (this.player.mercenaryCooldown > 0) {
isButtonDisabled = true;
perkButton.innerHTML = "Contract on Cooldown (" + this.player.mercenaryCooldown + " Turns Left)";
- } else if (this.player.caps < 20) {
+ } else if (this.player.caps < mercCost) {
isButtonDisabled = true;
- perkButton.innerHTML = "Mercenary Contract (20 Caps)";
+ perkButton.innerHTML = "Mercenary Contract (" + mercCost + " Caps)";
} else {
isButtonDisabled = false;
- perkButton.innerHTML = "Mercenary Contract (20 Caps)";
+ perkButton.innerHTML = "Mercenary Contract (" + mercCost + " Caps)";
}
-
+
perkButton.onclick = () => {
if (!isButtonDisabled) this.useMercenaryContract();
};
@@ -8685,8 +9433,8 @@ Gamestate.restart = function () {
target.army += this.player.lastFailedAttack.defenderLosses; // --- NEW: Restore the defender
// 3. Update the map visually for both
- let sourceMapEl = document.getElementById(source.name);
- if (sourceMapEl && sourceMapEl.nextElementSibling) sourceMapEl.nextElementSibling.textContent = source.army;
+ let sourceMapEl = document.getEleme
+if (sourceMapEl && sourceMapEl.nextElementSibling) sourceMapEl.nextElementSibling.textContent = source.army;
let targetMapEl = document.getElementById(target.name);
if (targetMapEl && targetMapEl.nextElementSibling) targetMapEl.nextElementSibling.textContent = target.army;
@@ -8978,10 +9726,20 @@ Gamestate.restart = function () {
let c = this.countries.find(x => x.name === areaName);
if (c && c.army > 1 && !c.isCrater) {
let dmgPercent = (Math.random() * 0.15) + 0.10;
+ let owner = this.players.find(p => p.name === c.owner);
+
+ // RadAway Immunity & Adamantium Skeleton resistance
+ if (owner && owner.radImmunity > 0) {
+ dmgPercent = 0;
+ } else if (owner && this.levelingEnabled && owner.activeBuffs && owner.activeBuffs.adamantiumSkeleton) {
+ dmgPercent /= 2;
+ }
+
let dmg = Math.floor(c.army * dmgPercent);
+
if (dmg < 1) dmg = 1; if (c.army - dmg < 1) dmg = c.army - 1;
c.army -= dmg; totalKilled += dmg;
- let owner = this.players.find(p => p.name === c.owner); if (owner) owner.army -= dmg;
+ if (owner) owner.army -= dmg;
let areaOnMap = document.getElementById(c.name); if (areaOnMap && areaOnMap.nextElementSibling) areaOnMap.nextElementSibling.textContent = c.army;
}
});
@@ -9005,8 +9763,268 @@ Gamestate.restart = function () {
});
}
+ // --- NEW: INITIAL PLACEMENT ENGINE ---
+ Gamestate.processInitialPlacement = async function() {
+ let activePlayers = this.players.filter(p => !p.isNeutral);
+
+ // Keep wrapping the index around the table
+ if (this.setupPlayerIndex >= activePlayers.length) {
+ this.setupPlayerIndex = 0;
+ }
+
+ let currentPlayer = activePlayers[this.setupPlayerIndex];
+
+ // Are we in the Claiming phase?
+ if (this.stage === "Initial Claim") {
+ let unowned = this.countries.filter(c => c.owner === "none" && !c.isCrater && !c.isSilo);
+
+ if (unowned.length === 0) {
+ // The map is full! Switch to Reinforcement phase
+ this.stage = "Initial Reinforce";
+ await this.logAction("--- DEPLOYMENT: FORTIFY GARRISONS ---", true);
+ this.processInitialPlacement();
+ return;
+ }
+
+ if (currentPlayer.isPlayer) {
+ // Human Turn: Unlock map and wait for click
+ this.aiTurn = false;
+ if (map) map.style.pointerEvents = "auto";
+ let msg = "Select an empty grey territory to claim it.";
+ if (turnInfoMessage && turnInfoMessage.textContent !== msg) {
+ turnInfoMessage.textContent = msg;
+ this.updateInfo();
+ }
+ return;
+ } else {
+ // AI Turn: Claim a random territory
+ this.aiTurn = true;
+ if (map) map.style.pointerEvents = "none";
+
+ let randomCountry = unowned[Math.floor(Math.random() * unowned.length)];
+ randomCountry.owner = currentPlayer.name;
+ randomCountry.color = currentPlayer.color;
+ randomCountry.army = 1;
+ currentPlayer.areas.push(randomCountry.name);
+ currentPlayer.army++;
+ currentPlayer.reserve--;
+
+ let el = document.getElementById(randomCountry.name);
+
+ let isVis = this.isTerritoryVisible(randomCountry.name);
+
+ // --- FIX: Prevent color flashing before Fog kicks in ---
+ if (el) {
+ if (isVis) {
+ el.style.fill = currentPlayer.color;
+ } else {
+ // Instantly apply fog state to prevent the microsecond color flash
+ el.style.fill = 'transparent';
+ el.classList.add('fog-shroud');
+ }
+ }
+
+ // Ensure map text redraws to keep the '?' if shrouded
+ if (this.drawMapText) this.drawMapText();
+
+
+ // --- FIX: Dynamic AI Setup Speed & Flash ---
+ let turbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked;
+ if (!turbo && el && isVis) {
+ el.classList.add('flash');
+ setTimeout(() => el.classList.remove('flash'), 150);
+ }
+
+ let delay = turbo ? 10 : 150;
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ // Only log it if the player can see it!
+ if (isVis) {
+ await this.logAction(`[ CLAIM ] ${currentPlayer.name} claimed ${formatTerritoryName(randomCountry.name)}.`);
+ }
+
+ this.setupPlayerIndex++;
+
+
+ this.processInitialPlacement();
+ return;
+ }
+ }
+
+ // Are we in the Reinforce phase?
+ else if (this.stage === "Initial Reinforce") {
+ // Check if EVERYONE is completely out of troops
+ let totalReserves = 0;
+ activePlayers.forEach(p => totalReserves += p.reserve);
+
+ if (totalReserves <= 0) {
+ // If Commanders are enabled, switch to Commander placement!
+ if (this.commandersEnabled) {
+ this.stage = "Initial Commander";
+ this.processInitialPlacement();
+ return;
+ }
+
+ // Otherwise, Setup is completely finished! Start Day 1!
+ this.aiTurn = false;
+ if (map) map.style.pointerEvents = "auto";
+
+ // Grant Day 1 Income/Troops
+ let income = 0;
+ if (this.wastelandEconomyActive) {
+ this.stage = "Recruitment";
+ income = (this.player.areas.length * 2) + this.continentBonus(this.player);
+ this.player.caps += income;
+ } else {
+ this.stage = "Fortify";
+ this.player.reserve += this.unitBonus(this.player, 0);
+ }
+
+ this.updateButtonText();
+ this.updateInfo();
+
+ if (this.printMissionBriefing) await this.printMissionBriefing();
+
+ if (this.wastelandEconomyActive) {
+ await this.logAction(`TAXES COLLECTED: Gained ${income} Caps from territories and continent bonuses.`, true);
+ }
+ return;
+ }
+
+ // If this specific player is out of troops, skip them
+ if (currentPlayer.reserve <= 0) {
+ this.setupPlayerIndex++;
+ this.processInitialPlacement();
+ return;
+ }
+
+ if (currentPlayer.isPlayer) {
+ // Human Turn: Wait for click
+ this.aiTurn = false;
+ if (map) map.style.pointerEvents = "auto";
+
+ // FIX: Safely update text so typewriter effect doesn't glitch
+ let msg = `Deploy reserve troops. (${currentPlayer.reserve} remaining)`;
+ if (turnInfoMessage && turnInfoMessage.textContent !== msg) {
+ turnInfoMessage.textContent = msg;
+ this.updateInfo();
+ }
+ return;
+ } else {
+ // AI Turn: Drop 1 troop randomly on their own land
+ this.aiTurn = true;
+ if (map) map.style.pointerEvents = "none";
+
+ if (currentPlayer.areas.length > 0) {
+ let randomAreaName = currentPlayer.areas[Math.floor(Math.random() * currentPlayer.areas.length)];
+ let country = this.countries.find(c => c.name === randomAreaName);
+ if (country) {
+ country.army++;
+ currentPlayer.army++;
+ currentPlayer.reserve--;
+
+ let el = document.getElementById(country.name);
+
+ // --- FIX: Use drawMapText so we don't accidentally reveal troops under fog ---
+ if (this.drawMapText) this.drawMapText();
+
+ let isVis = this.isTerritoryVisible(country.name);
+
+ // --- FIX: Dynamic AI Setup Speed & Flash ---
+ let turbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked;
+ if (!turbo && el && isVis) {
+ el.classList.add('flash');
+ setTimeout(() => el.classList.remove('flash'), 150);
+ }
+
+ let delay = turbo ? 10 : 150;
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ // Only log it if the player can see it!
+ if (isVis) {
+ await this.logAction(`[ DEPLOYMENT ] ${currentPlayer.name} deployed 1 troop to ${formatTerritoryName(country.name)}.`);
+ }
+ }
+ }
+
+ this.setupPlayerIndex++;
+
+ this.setupPlayerIndex++;
+ this.processInitialPlacement();
+ return;
+ }
+ }
+
+ // --- NEW: Commander Placement Phase ---
+ else if (this.stage === "Initial Commander") {
+ let cmdrPlayers = activePlayers.filter(p => p.commander && !p.commander.loc);
+
+ // If everyone has placed their commander, Start Day 1!
+ if (cmdrPlayers.length === 0) {
+ this.aiTurn = false;
+ if (map) map.style.pointerEvents = "auto";
+
+ let income = 0;
+ if (this.wastelandEconomyActive) {
+ this.stage = "Recruitment";
+ income = (this.player.areas.length * 2) + this.continentBonus(this.player);
+ this.player.caps += income;
+ } else {
+ this.stage = "Fortify";
+ this.player.reserve += this.unitBonus(this.player, 0);
+ }
+
+ this.updateButtonText();
+ this.updateInfo();
+ if (this.printMissionBriefing) await this.printMissionBriefing();
+ if (this.wastelandEconomyActive) {
+ await this.logAction(`TAXES COLLECTED: Gained ${income} Caps from territories and continent bonuses.`, true);
+ }
+ return;
+ }
+
+ // Grab the next player who needs to place their commander
+ let curr = cmdrPlayers[0];
+
+ if (curr.isPlayer) {
+ this.aiTurn = false;
+ if (map) map.style.pointerEvents = "auto";
+ let msg = "Select a territory to deploy your Commander.";
+ if (turnInfoMessage && turnInfoMessage.textContent !== msg) {
+ turnInfoMessage.textContent = msg;
+ this.updateInfo();
+ }
+ return;
+ } else {
+ // AI Turn: Randomly drop commander on one of their owned territories
+ this.aiTurn = true;
+ if (map) map.style.pointerEvents = "none";
+
+ if (curr.areas.length > 0) {
+ curr.commander.loc = curr.areas[Math.floor(Math.random() * curr.areas.length)];
+ if (this.drawMapText) this.drawMapText();
+ } else {
+ curr.commander.loc = "none";
+ }
+
+ let turbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked;
+ let delay = turbo ? 10 : 150;
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ if (this.isTerritoryVisible(curr.commander.loc)) {
+ await this.logAction(`[ DEPLOYMENT ] ${curr.name} deployed their Commander to ${formatTerritoryName(curr.commander.loc)}.`);
+ }
+
+ this.processInitialPlacement();
+ return;
+ }
+ }
+ };
+
Gamestate.checkAutoPhaseAdvance = function() {
+
if (this.aiTurn || this.modalIsOpen) return;
+
let shouldAdvance = false;
let reason = "OUT OF AP";
@@ -9193,12 +10211,24 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
let aBobble = this.bobbleheads && this.bobbleheads.find(b => b.key === 'a');
let hasAgility = (aBobble && aBobble.active && aBobble.owner === this.player.name);
+ // --- NEW: DOGMEAT ENCUMBRANCE PENALTY ---
+ let dogmeatPenalty = (this.player.dogmeatStatus === 'injured') ? 1 : 0;
+ if (dogmeatPenalty > 0) {
+ if(this.logAction) this.logAction(`[ COMPANION ] Dogmeat's injuries are slowing you down. (-1 Maneuver Point)`, true);
+ }
+
+ // --- NEW: Add Action Boy/Girl Stacks ---
+ let actionBoyBonus = (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.actionBoy) ? this.player.activeBuffs.actionBoy : 0;
+
// Grant maneuver points to The Railroad player
if (this.perksEnabled && this.player.perk && this.player.perk.id === 'rapid_relocation') {
- this.player.maneuverPoints = hasAgility ? 6 : 5;
+ let basePoints = (hasAgility ? 6 : 5) + actionBoyBonus;
+ this.player.maneuverPoints = Math.max(0, basePoints - dogmeatPenalty);
if (turnInfoMessage) turnInfoMessage.textContent = `Move troops. You have ${this.player.maneuverPoints} moves remaining.`;
} else {
- this.player.maneuverPoints = hasAgility ? 2 : 1;
+ let basePoints = (hasAgility ? 2 : 1) + actionBoyBonus;
+ this.player.maneuverPoints = Math.max(0, basePoints - dogmeatPenalty);
+
if (turnInfoMessage) {
turnInfoMessage.textContent = hasAgility
? "Agility Active: Move troops twice this turn."
@@ -9206,6 +10236,7 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
}
}
+
this.updateButtonText();
if (turnInfo) turnInfo.textContent = "Maneuver Phase";
this.updateInfo();
@@ -9215,15 +10246,44 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
this.stage = "Maneuver"; // Set to Maneuver so the fall-through is clean
}
}
+
+ // --- FIX: JET EXTRA TURN ---
+ if (this.player.extraTurn) {
+ this.player.extraTurn = false;
+
+ // --- FIX: Hand the player their new turn income! ---
+ if (this.wastelandEconomyActive) {
+ this.stage = "Recruitment";
+ let income = (this.player.areas.length * 2) + this.continentBonus(this.player);
+ this.player.caps += income;
+ await this.logAction(`[ CHEMICALS ] Jet rushing through your veins. You take another turn instantly! (+${income} Caps)`, true);
+ } else {
+ this.stage = "Fortify";
+ let bonus = this.unitBonus(this.player, 0);
+ this.player.reserve += bonus;
+ await this.logAction(`[ CHEMICALS ] Jet rushing through your veins. You take another turn instantly! (+${bonus} Troops)`, true);
+ }
+
+ this.updateButtonText();
+ this.updateInfo();
+ return; // Bypass AI Turn completely
+ }
+
+
// FIX: Lock the AI Turn and Map IMMEDIATELY before any 'await' delays to prevent multi-click cloning!
this.aiTurn = true;
- if (map) map.style.pointerEvents = "none";
- if (this.player.conqueredThisTurn) {
+ if (map) map.style.pointerEvents = "none"; if (this.player.conqueredThisTurn) {
this.isDistributingLoot = true; // Set the flag to prevent INV flashing
let foundRareLoot = false;
// FIX: Make sure the luck item actually belongs to the player
const luckItem = this.bobbleheads && this.bobbleheads.find(i => i.key === 'l' && i.active && i.owner === this.player.name);
- const luckModifier = luckItem ? 0.15 : 0;
+ let luckModifier = luckItem ? 0.15 : 0;
+
+ // --- NEW: Add Scavenger Perk Bonus ---
+ if (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.scavenger) {
+ luckModifier += (0.10 * this.player.activeBuffs.scavenger);
+ }
+
// 1. Roll for Bobblehead (5% base + 15% Luck = 20% max)
@@ -9461,8 +10521,19 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
const clickedCountry = this.countries.find(c => c.name === e.target.id);
if (!clickedCountry || clickedCountry.owner !== this.player.name) return;
+
+ // --- NEW: CRYOLATOR & SHROUD CARD BLOCKS ---
+ if (clickedCountry.isFrozen > 0) {
+ if (this.showToast) this.showToast("Cannot deploy: Territory is frozen solid!", "red");
+ return;
+ }
+ if (clickedCountry.isBlockaded > 0) {
+ if (this.showToast) this.showToast("Cannot deploy: Territory is currently blockaded!", "red");
+ 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)) {
@@ -9535,8 +10606,16 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
this.prevCountry = null; this.prevTarget = null;
return;
}
+
+ // --- NEW: CRYOLATOR FREEZE CHECK ---
+ if (this.prevCountry && this.prevCountry.isFrozen > 0) {
+ if (this.showToast) this.showToast(`Cannot attack: ${formatTerritoryName(this.prevCountry.name)} is frozen solid!`, "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;
@@ -9657,8 +10736,15 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
if (map) map.style.pointerEvents = "none";
let atkForce = country.army;
let dmgToVip = Math.floor(atkForce * (Math.random() * 0.5 + 0.5));
+
+ // --- NEW: Refractor Perk ---
+ if (this.levelingEnabled && trespasser.activeBuffs && trespasser.activeBuffs.refractor) {
+ dmgToVip = Math.floor(dmgToVip / 2);
+ }
+
if (dmgToVip < 1) dmgToVip = 1;
let retaliation = Math.floor(Math.random() * 4) + 2;
+
trespasser.commander.hp -= dmgToVip;
country.army -= retaliation;
if (country.army < 1) country.army = 1;
@@ -9860,10 +10946,17 @@ if (this.stage === "Fortify" || this.stage === "Recruitment") {
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)}.`);
+
+ // --- NEW: Ninja Perk ---
+ if (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.ninja) {
+ await this.logAction(`COMMANDER MOVEMENT: Commander relocated to ${formatTerritoryName(country.name)}. (Hidden from sensors)`);
+ } else {
+ await this.logAction(`COMMANDER MOVEMENT: Commander relocated to ${formatTerritoryName(country.name)}.`);
+ }
this.checkAutoPhaseAdvance();
}
+
this.updateInfo();
}
return;
@@ -10215,9 +11308,12 @@ this.checkAutoPhaseAdvance();
Gamestate.useStimpak = async function () {
if (this.stage !== "Commander Phase" || !this.commandersEnabled || !this.player.commander) return;
- // Smart Max HP check
- const maxHP = 100 + (this.player.activeBuffs.lifeGiver || 0) * 25;
- const healAmount = this.player.activeBuffs.medic ? 40 : 20;
+ // Smart Max HP check (Includes Life Giver + X-01 Armor)
+ let maxHP = 100 + (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.lifeGiver ? this.player.activeBuffs.lifeGiver * 25 : 0);
+ if (this.player.relics && this.player.relics.some(r => r.id === 'x01armor' && r.isEquipped)) maxHP += 50;
+
+ const healAmount = (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.medic) ? 40 : 20;
+
if (this.player.commander.ap <= 0 || this.player.commander.stimpaks <= 0 || this.player.commander.hp >= maxHP) return;
@@ -10230,34 +11326,63 @@ this.checkAutoPhaseAdvance();
this.updateInfo();
this.checkAutoPhaseAdvance();
}
-
+
+ // --- NEW: HEAL DOGMEAT FROM INVENTORY ---
+ Gamestate.healDogmeat = async function() {
+ if (this.player.dogmeatStatus !== 'injured') return;
+
+ let hasStimpak = this.commandersEnabled && this.player.commander && this.player.commander.stimpaks > 0;
+ let hasCaps = this.wastelandEconomyActive && this.player.caps >= 50;
+
+ if (hasStimpak) {
+ this.player.commander.stimpaks--;
+ this.player.dogmeatStatus = 'healthy';
+ this.logAction(`[ COMPANION ] You used a Stimpak to treat Dogmeat's wounds. He's ready for action!`, true);
+ } else if (hasCaps) {
+ this.player.caps -= 50;
+ this.player.dogmeatStatus = 'healthy';
+ this.logAction(`[ COMPANION ] You spent 50 Caps on medical supplies for Dogmeat. He's ready for action!`, true);
+ }
+
+ // Close the inventory and re-render everything
+ let invModal = document.getElementById('inventory-modal');
+ if (invModal) invModal.style.display = 'none';
+
+ if (this.queueToast) this.queueToast(`>>> COMPANION HEALED <<<
DOGMEAT IS READY FOR ACTION`, "var(--pip-color)", true);
+
+ this.updateInfo();
+ if (this.renderInventory) this.renderInventory();
+ };
Gamestate.activateBobblehead = async function (itemKey) {
+
if (!this.bobbleheads) return;
const item = this.bobbleheads.find(i => i.key === itemKey);
if (!item || !item.found || item.cooldown > 0) return;
// --- FIX: SMART ACTIVATION LOCKOUT ---
- if (item.key === 's' || item.key === 'l') {
- if (this.stage === "Commander Phase" || this.stage === "Maneuver") {
- if (this.showToast) this.showToast(`Cannot activate: The Battle phase has already passed.`, "red");
- return;
- }
- } else if (item.key === 'c') {
- if (this.stage !== "Recruitment" && this.stage !== "Fortify") {
- if (this.showToast) this.showToast(`Cannot activate: Recruitment has already passed.`, "red");
- return;
- }
+ if (item.key === 'c' && this.stage !== "Recruitment" && this.stage !== "Fortify") {
+ if (this.showToast) this.showToast(`Cannot activate: Must be in Recruitment or Fortify phase.`, "red");
+ return;
+ }
+ if (item.key === 'a' && this.stage !== "Maneuver" && this.stage !== "Commander Phase") {
+ if (this.showToast) this.showToast(`Cannot activate: Must be in Maneuver or Commander phase.`, "red");
+ return;
+ }
+ if (['s', 'e', 'p', 'i', 'l'].includes(item.key) && this.stage !== "Battle" && this.stage !== "Commander Phase") {
+ if (this.showToast) this.showToast(`Cannot activate: Must be in Battle or Commander phase.`, "red");
+ return;
}
// --- END LOCKOUT ---
+
item.cooldown = item.totalCooldown; // Put on cooldown
// --- THIS IS THE NEW LOGIC ---
// Set the bobblehead to 'active' so the rest of the code knows it's on.
item.active = true;
- let logMessage = `\[ BOBBLEHEAD \] ${item.name} activated! ${item.desc}`;
+ let logMessage = `${item.name} activated! ${item.desc}`;
// Special handling for Agility Bobblehead
if (item.key === 'a') {
@@ -10290,8 +11415,9 @@ this.checkAutoPhaseAdvance();
Gamestate.initiateNukeSequence = function () {
- if (!this.nukesEnabled || this.player.codes < 4 || this.activeNuke) return;
+ if (!this.nukesEnabled || this.player.codes < 4 || (this.activeNukes && this.activeNukes.some(n => n.launcher === this.player.name))) return;
let silos = this.countries.filter(c => c.isSilo && c.owner === this.player.name);
+
if (silos.length === 0) { this.showToast("You must control a Silo to launch.", "red"); return; }
if (this.stage === "Nuke Targeting") {
@@ -10341,9 +11467,12 @@ this.checkAutoPhaseAdvance();
Gamestate.executeNukeLaunch = function (launcherData, targetName, launchSiloName) {
launcherData.codes -= 4;
this.globalCodes += 4;
- this.activeNuke = { launcher: launcherData.name, target: targetName, turns: 3, launchSilo: launchSiloName };
+
+ if (!this.activeNukes) this.activeNukes = [];
+ this.activeNukes.push({ launcher: launcherData.name, target: targetName, turns: 3, launchSilo: launchSiloName });
// Strip defenses from ALL silos the launcher owns
+
let silos = this.countries.filter(c => c.isSilo && c.owner === launcherData.name);
silos.forEach(s => s.siloTurns = 0);
@@ -10364,23 +11493,38 @@ this.checkAutoPhaseAdvance();
}
Gamestate.resolveNuke = async function () {
- if (!this.activeNuke) return;
- let launcher = this.players.find(p => p.name === this.activeNuke.launcher);
- if (!launcher || !launcher.alive || !this.countries.some(c => c.isSilo && c.owner === launcher.name)) {
- await this.logAction(`[ LAUNCH ABORTED ] ${this.activeNuke.launcher} lost Silo control. Sequence terminated.`, true);
- this.activeNuke = null; return;
+ // Failsafe migration for old save files
+ if (this.activeNuke && (!this.activeNukes || this.activeNukes.length === 0)) {
+ this.activeNukes = [this.activeNuke];
+ this.activeNuke = null;
}
+ if (!this.activeNukes || this.activeNukes.length === 0) return;
- this.activeNuke.turns -= 1;
- if (this.activeNuke.turns > 0) {
- await this.logAction(`[ NUCLEAR ALERT ] T-${this.activeNuke.turns} rounds until impact at ${formatTerritoryName(this.activeNuke.target)}.`, true, true);
- this.updateInfo(); return;
- }
+ // Loop backwards so we can remove nukes as they detonate/abort
+ for (let i = this.activeNukes.length - 1; i >= 0; i--) {
+ let currentNuke = this.activeNukes[i];
+ let launcher = this.players.find(p => p.name === currentNuke.launcher);
+
+ if (!launcher || !launcher.alive || !this.countries.some(c => c.isSilo && c.owner === launcher.name)) {
+ await this.logAction(`[ LAUNCH ABORTED ] ${currentNuke.launcher} lost Silo control. Sequence terminated.`, true);
+ this.activeNukes.splice(i, 1);
+ continue;
+ }
- let groundZero = this.countries.find(c => c.name === this.activeNuke.target);
+ currentNuke.turns -= 1;
+ if (currentNuke.turns > 0) {
+ await this.logAction(`[ NUCLEAR ALERT ] T-${currentNuke.turns} rounds until impact at ${formatTerritoryName(currentNuke.target)}.`, true, true);
+ continue; // Wait until next turn
+ }
+
+ let groundZero = this.countries.find(c => c.name === currentNuke.target);
await this.logAction(`[ DETONATION ] NUCLEAR IMPACT AT ${formatTerritoryName(groundZero.name)}!`, true, true);
- groundZero.isCrater = true; groundZero.isSilo = false;
+ // --- NEW: GROUND ZERO IS NOW HEAVILY IRRADIATED INSTEAD OF DEAD ---
+ groundZero.isCrater = false;
+ groundZero.isSilo = false;
+ groundZero.radDecay = 10; // 10 Turns of severe radiation!
+
let victim = this.players.find(p => p.name === groundZero.owner);
if (victim) {
victim.army -= groundZero.army; let idx = victim.areas.indexOf(groundZero.name); if (idx > -1) victim.areas.splice(idx, 1);
@@ -10399,11 +11543,17 @@ this.checkAutoPhaseAdvance();
for (let neighborName of groundZero.neighbours) {
let n = this.countries.find(c => c.name === neighborName);
if (n && !n.isCrater) {
- let dmg = Math.ceil(n.army * 0.50); n.army -= dmg;
- let nOwner = this.players.find(p => p.name === n.owner); if (nOwner) nOwner.army -= dmg;
- n.radDecay = 4;
+ // Sniper Immunity Check (Immune to friendly fire from own Nuke)
+ let isImmune = (n.owner === launcher.name && this.levelingEnabled && launcher.activeBuffs && launcher.activeBuffs.sniper);
+
+ if (!isImmune) {
+ let dmg = Math.ceil(n.army * 0.50); n.army -= dmg;
+ let nOwner = this.players.find(p => p.name === n.owner); if (nOwner) nOwner.army -= dmg;
+ }
+ n.radDecay = 5; // Increased neighbor radiation slightly
}
}
+
await this.logAction(`[ SCORCHED EARTH ] Adjacent territories caught in blast radius. 50% casualties. Glowing Sea expanding.`, true);
// --- REPUTATION PENALTY: NUCLEAR ATROCITY ---
@@ -10429,9 +11579,11 @@ this.checkAutoPhaseAdvance();
});
}
-
- this.activeNuke = null; this.updateInfo();
+ // Remove the detonated nuke from the array
+ this.activeNukes.splice(i, 1);
}
+ this.updateInfo();
+ }
Gamestate.killCommander = async function (player, killer = null) { // Added killer parameter
@@ -10500,8 +11652,7 @@ this.checkAutoPhaseAdvance();
winMessage.textContent = "YOU DIED.";
winMessage.style.color = "#ff0000";
let subMsg = winMessage.nextElementSibling;
- if (subMsg && subMsg.tagName === 'P') {
- subMsg.textContent = "YOUR COMMANDER WAS ASSASSINATED!";
+ if (subMsg && subMsg.tagName === 'P') { subMsg.textContent = "YOUR COMMANDER WAS ASSASSINATED!";
}
}
if (winModal) winModal.style.display = "block";
@@ -10509,6 +11660,11 @@ this.checkAutoPhaseAdvance();
}
Gamestate.processRadDecay = async function () {
+ // --- NEW: DECREMENT RADAWAY IMMUNITY ---
+ this.players.forEach(p => {
+ if (p.radImmunity && p.radImmunity > 0) p.radImmunity--;
+ });
+
let decayActive = false;
for (let c of this.countries) {
if (c.radDecay && c.radDecay > 0) {
@@ -10526,10 +11682,27 @@ this.checkAutoPhaseAdvance();
await this.logAction(`[ INFESTATION ] Radiation cleared at ${formatTerritoryName(c.name)}. A massive Feral Ghoul horde has moved in!`, true);
}
} else {
- let attrPercent = c.radDecay === 4 ? 0.50 : (c.radDecay === 3 ? 0.30 : 0.10);
- if (c.army > 1) {
- let dmg = Math.ceil(c.army * attrPercent); c.army -= dmg;
- let owner = this.players.find(p => p.name === c.owner); if (owner) owner.army -= dmg;
+ // --- NEW: DYNAMIC RADIATION SCALING & IMMUNITIES ---
+ let basePercent = 0.10;
+ if (c.radDecay >= 8) basePercent = 0.80; // Ground Zero (80% losses)
+ else if (c.radDecay >= 5) basePercent = 0.50; // Heavy Rads (50% losses)
+ else if (c.radDecay >= 3) basePercent = 0.30; // Medium Rads (30% losses)
+
+ let attrPercent = basePercent;
+ let owner = this.players.find(p => p.name === c.owner);
+
+ if (owner) {
+ if (owner.radImmunity && owner.radImmunity > 0) {
+ attrPercent = 0; // Immune from RadAway
+ } else if (owner.activeBuffs && owner.activeBuffs.adamantiumSkeleton) {
+ attrPercent = attrPercent / 2; // 50% resistance
+ }
+ }
+
+ if (c.army > 1 && attrPercent > 0) {
+ let dmg = Math.ceil(c.army * attrPercent);
+ c.army -= dmg;
+ if (owner) owner.army -= dmg;
}
c.radDecay--;
}
@@ -10546,6 +11719,8 @@ this.checkAutoPhaseAdvance();
this.stage = "AI Turn";
this.updateButtonText();
if (turnInfoMessage) turnInfoMessage.textContent = "";
+
+ let backbriefData = []; // --- NEW: Track stats for the Backbrief ---
for (let i = 1; i <= this.players.length; i++) {
if (i === this.players.length) {
@@ -10572,9 +11747,106 @@ this.checkAutoPhaseAdvance();
}
// --- END FIX ---
-
+ // --- NEW: DOGMEAT QUEST LOGIC ---
+ if (this.dogmeatQuest && this.dogmeatQuest.cooldown > 0) {
+ this.dogmeatQuest.cooldown--;
+ } else
+ if (this.dogmeatQuest && this.dogmeatQuest.active) {
+ this.dogmeatQuest.timer--;
+ if (this.dogmeatQuest.timer <= 0) {
+ this.dogmeatQuest.active = false;
+ this.dogmeatQuest.cooldown = 4; // Wait 4 turns before spawning again
+
+ // --- NEW: FAILURE MODAL ---
+ this.modalIsOpen = true;
+ await this.showEncounterModal(
+ "Signal Lost",
+ `The distress signal from the Red Rocket has faded. The dog has wandered off, and the trail has gone cold.`,
+ [{id: "ok", text: "[Acknowledge] A missed opportunity."}],
+ () => { this.modalIsOpen = false; return null; }
+ );
+ if (this.logAction) this.logAction(`[ QUEST FAILED ] The trail to the lone dog went cold.`, true);
+
+ } else if (this.dogmeatQuest.timer === 2) {
+
+ if (this.logAction) this.logAction(`[ QUEST UPDATE ] The Red Rocket distress signal is getting weak... (2 Turns Left)`, true);
+ }
+ } else if (this.dogmeatQuest && !this.dogmeatQuest.resolved && this.encountersEnabled && this.turn > 3 && (this.wastelandEconomyActive || this.commandersEnabled)) {
+ // 5% chance per turn to spawn the rumor
+ if (Math.random() < 0.05) {
+
+ let validTargets = this.countries.filter(c => c.owner !== "none" && c.owner !== this.player.name && !c.isCrater && c.army > 0 && !this.areAllies(this.player.name, c.owner));
+ if (validTargets.length > 0) {
+ let randomTarget = validTargets[Math.floor(Math.random() * validTargets.length)];
+ this.dogmeatQuest.active = true;
+ this.dogmeatQuest.target = randomTarget.name;
+ this.dogmeatQuest.timer = Math.floor(Math.random() * 3) + 4; // 4 to 6 turns
+
+ const rumors = [
+ `Scavengers spotted a dog defending a Red Rocket station at ${formatTerritoryName(randomTarget.name)}.`,
+ `Picked up a weak distress broadcast... a dog is barking furiously at ${formatTerritoryName(randomTarget.name)}.`,
+ `Caravan guards report a lone dog holding off raiders near ${formatTerritoryName(randomTarget.name)}.`,
+ `A traveling merchant claims to have seen a loyal dog waiting at ${formatTerritoryName(randomTarget.name)}.`
+ ];
+ const rText = rumors[Math.floor(Math.random() * rumors.length)];
+
+ if (this.queueToast) this.queueToast(`>>> NEW RUMOR <<< [ CX404 ] LONE DOG SIGHTED`, "var(--pip-color)", true);
+ if (this.logAction) this.logAction(`[ QUEST ] ${rText} You have ${this.dogmeatQuest.timer} turns to reach it!`, true);
+
+ // --- NEW: IN-GAME QUEST MODAL ---
+ this.modalIsOpen = true;
+ await this.showEncounterModal(
+ "NEW QUEST: A BOY AND HIS DOG",
+ `${rText}
Attack and conquer ${formatTerritoryName(randomTarget.name)} within ${this.dogmeatQuest.timer} turns to trigger a rescue operation!`,
+ [{id: "ok", text: "[Acknowledge] We are on our way."}],
+ () => { this.modalIsOpen = false; return null; }
+ );
+ }
+ }
+ }
+
+ // --- NEW: DOGMEAT SIEGE UPKEEP ---
+ if (this.dogmeatQuest && this.dogmeatQuest.siege > 0) {
+ let targetCountry = this.countries.find(c => c.name === this.dogmeatQuest.target);
+ // Ensure the player still owns the territory!
+ if (targetCountry && targetCountry.owner === this.player.name) {
+ this.dogmeatQuest.siege--;
+ if (this.dogmeatQuest.siege > 0) {
+ await this.resolveDogmeatSiege('defend', targetCountry);
+ } else {
+ this.dogmeatQuest.resolved = true;
+ await this.resolveDogmeatSiege('finish', targetCountry);
+ }
+ } else {
+ // Player lost the territory to an AI before finishing the siege!
+ this.dogmeatQuest.siege = 0;
+ this.dogmeatQuest.resolved = false;
+ this.dogmeatQuest.cooldown = 4;
+
+ const failureMsgs = [
+ "You lost control of the Red Rocket! The scavengers took advantage of the chaos and escaped with the dog.",
+ "The enemy forces overran your siege perimeter! In the confusion, the dog slipped away into the wasteland.",
+ "Your forces were pushed out of the territory! A passing caravan managed to rescue the dog before you could return."
+ ];
+ const randomFailMsg = failureMsgs[Math.floor(Math.random() * failureMsgs.length)];
+
+ this.modalIsOpen = true;
+ await this.showEncounterModal(
+ "Siege Broken",
+ randomFailMsg,
+ [{id: "ok", text: "[Acknowledge] We failed him."}],
+ () => { this.modalIsOpen = false; return null; }
+ );
+
+ if (this.logAction) this.logAction(`[ QUEST FAILED ] ${randomFailMsg}`, true);
+ }
+ }
+
+
+
// --- NEW: Process Exploration Countdowns FIRST ---
+
for (let c of this.countries) {
if (c.owner === this.player.name && c.isExploring) {
c.exploreTurnsLeft--;
@@ -10582,8 +11854,25 @@ this.checkAutoPhaseAdvance();
await this.resolveExplorationOutcome(c);
}
}
+
+ // --- NEW: RELIC TIMERS ---
+ if (c.isFrozen > 0) {
+ c.isFrozen--;
+ if (c.isFrozen <= 0) {
+ c.isFrozen = 0;
+ if (this.logAction) this.logAction(`[ THAW ] ${formatTerritoryName(c.name)} has thawed and resumed normal operations.`);
+ }
+ }
+ if (c.isBlockaded > 0) {
+ c.isBlockaded--;
+ if (c.isBlockaded <= 0) {
+ c.isBlockaded = 0;
+ if (this.logAction) this.logAction(`[ BLOCKADE LIFTED ] Supplies can now reach ${formatTerritoryName(c.name)}.`);
+ }
+ }
}
+
if (this.bobbleheads) {
this.bobbleheads.forEach(b => {
if (b.cooldown > 0) {
@@ -10621,8 +11910,10 @@ this.checkAutoPhaseAdvance();
}
// If the final siege is complete, capture the territory
- if (p.commander.siegeTurns >= 3) {
+ let requiredTurns = (this.levelingEnabled && p.activeBuffs && p.activeBuffs.infiltrator) ? 2 : 3;
+ if (p.commander.siegeTurns >= requiredTurns) {
let oldOwner = this.players.find(x => x.name === locCountry.owner);
+
if (oldOwner) {
let idx = oldOwner.areas.indexOf(locCountry.name);
if (idx > -1) oldOwner.areas.splice(idx, 1);
@@ -10654,63 +11945,54 @@ this.checkAutoPhaseAdvance();
}
}
if (this.perksEnabled) {
- if (this.player && this.player.perk) {
-
- // --- ADD THIS BLOCK: Chem Frenzy Cooldown ---
- if (this.player.perk.id === 'chem_frenzy' && this.player.chemFrenzyCooldown > 0) {
- this.player.chemFrenzyCooldown--;
- }
- // --- END OF ADDITION ---
- // --- NEW: Technology Overdrive Buff & Cooldown Countdown ---
- if (this.player.perk.id === 'tech_hoarders') {
- if (this.player.techOverdriveCooldown > 0) {
- this.player.techOverdriveCooldown--;
+ for (let p of this.players) {
+ if (p && p.perk) {
+ // --- Chem Frenzy Cooldown ---
+ if (p.perk.id === 'chem_frenzy' && p.chemFrenzyCooldown > 0) {
+ p.chemFrenzyCooldown--;
}
- }
-
-
- // --- END OF ADDITION ---
- // --- NEW: The Gunners/Minutemen Mercenary Cooldown ---
- if ((this.player.perk.id === 'mercenary_contracts' || this.player.perk.id === 'minutemen_contracts' || this.player.perk.id === 'gunner_contracts') && this.player.mercenaryCooldown > 0) {
- this.player.mercenaryCooldown--;
- if (this.player.mercenaryCooldown === 0) {
- this.logAction("Mercenary contacts have refreshed and are ready for hire.", true);
+ // --- Technology Overdrive Cooldown ---
+ if (p.perk.id === 'tech_hoarders') {
+ if (p.techOverdriveCooldown > 0) {
+ p.techOverdriveCooldown--;
+ }
}
- }
- // --- END OF ADDITION ---
-
-
- // --- NEW: Mr. House Predictive Cooldown ---
- if (this.player.perk.id === 'the_house_always_wins' && this.player.predictiveCooldown > 0) {
- this.player.predictiveCooldown--;
- if (this.player.predictiveCooldown === 0) {
- this.logAction("Predictive Simulation systems are fully recharged and ready.", true);
+ // --- Mercenary Cooldown ---
+ if ((p.perk.id === 'mercenary_contracts' || p.perk.id === 'minutemen_contracts' || p.perk.id === 'gunner_contracts') && p.mercenaryCooldown > 0) {
+ p.mercenaryCooldown--;
+ if (p.mercenaryCooldown === 0 && p.isPlayer) {
+ await this.logAction("Mercenary contacts have refreshed and are ready for hire.", true);
+ }
}
+ // --- Mr. House Predictive Cooldown ---
+ if (p.perk.id === 'the_house_always_wins' && p.predictiveCooldown > 0) {
+ p.predictiveCooldown--;
+ if (p.predictiveCooldown === 0 && p.isPlayer) {
+ await this.logAction("Predictive Simulation systems are fully recharged and ready.", true);
+ }
+ }
+ // --- Nuke Code Cooldown ---
+ if (p.nukeCodeCooldown > 0) {
+ p.nukeCodeCooldown--;
+ }
+ // --- End of Turn Resets/Triggers ---
+ if (p.perk.id === 'chem_frenzy') {
+ p.canUseChemFrenzy = true;
+ } else if (p.perk.id === 'prydwen_deployment') {
+ p.airborneTroops = 3;
+ let prefix = p.isPlayer ? "The" : `[ INTEL ] ${p.name}'s`;
+ await this.logAction(`${prefix} Prydwen has dispatched 3 airborne troops to contested borders.`, p.isPlayer);
+ }
+ if (p.perk.id === 'mysterious_stranger' && p.strangerCooldown > 0) {
+ p.strangerCooldown--;
+ }
+ if (p.perk.id === 'elders_edict' && p.lockdownCooldown > 0) {
+ p.lockdownCooldown--;
+ }
+ p.usedChemFrenzy = false;
+ p.usedMemoryDen = false;
}
- // --- END OF ADDITION ---
- // --- NEW: Nuke Code Cooldown ---
- if (this.player.nukeCodeCooldown > 0) {
- this.player.nukeCodeCooldown--;
- }
-
-
- 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) {
@@ -10737,17 +12019,123 @@ this.checkAutoPhaseAdvance();
b.active = false;
// Only log it if the human player owned it so it doesn't spam
if (this.logAction && b.owner === this.player.name) {
- this.logAction(`\\[ BOBBLEHEAD \\] The effect of the ${b.name} has worn off.`);
+ this.logAction(`The effect of the ${b.name} has worn off.`);
}
}
});
}
// --- END FIX ---
+
+ // --- FIX: STEALTH BOY DURATION DECREMENT ---
+ if (this.player.stealthActive && this.player.stealthActive > 0) {
+ this.player.stealthActive--;
+ if (this.player.stealthActive === 0 && this.logAction) {
+ this.logAction(`[ STEALTH ] Stealth Boy battery depleted. You are visible again.`, true);
+ }
+ }
+
+ // --- NEW: TACTICAL BACKBRIEF GENERATOR ---
+ let isTurbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked;
+ if (isTurbo && typeof backbriefData !== 'undefined' && backbriefData.length > 0) {
+ let bbText = `[ TACTICAL BACKBRIEF ] `;
+ let visibleAIs = backbriefData.filter(d => d.visible);
+ let ghostAIs = backbriefData.filter(d => !d.visible);
+
+ if (visibleAIs.length > 0) {
+ let repLines = [
+ `Scouts report shifting frontlines.`,
+ `Reconnaissance telemetry processed.`,
+ `Tactical map updated with latest enemy movements.`,
+ `Satellite imagery confirms major border shifts.`,
+ `Intercepted radio chatter paints a grim picture of the battlefield.`,
+ `Morning briefings indicate heavy nighttime maneuvers.`,
+ `Patrols have returned with updated sector assessments.`,
+ `Automated sensors have compiled the overnight battle data.`
+ ];
+ bbText += repLines[Math.floor(Math.random() * repLines.length)] + " ";
+
+ // Process EVERY visible AI individually
+ visibleAIs.forEach(ai => {
+ if (ai.net > 0) {
+ let winVerbs = [
+ "spearheaded a ruthless offensive, seizing",
+ "pushed the frontlines, capturing",
+ "went on the warpath, securing",
+ "overwhelmed local defenders, claiming",
+ "executed a calculated blitz, occupying",
+ "broke through enemy fortifications, annexing",
+ "dominated the battlefield, sweeping across",
+ "launched a massive incursion, taking control of"
+ ];
+ bbText += `${ai.name} ${winVerbs[Math.floor(Math.random() * winVerbs.length)]} ${ai.net} sector(s). `;
+ } else if (ai.net < 0) {
+ let loseVerbs = [
+ "suffered heavy casualties, losing",
+ "was severely beaten back, surrendering",
+ "failed to hold the line, dropping",
+ "crumbled under concentrated fire, abandoning",
+ "was forced into a desperate retreat, forfeiting",
+ "bled heavily on the defensive, yielding",
+ "lost significant ground, giving up"
+ ];
+ bbText += `${ai.name} ${loseVerbs[Math.floor(Math.random() * loseVerbs.length)]} ${Math.abs(ai.net)} region(s). `;
+ } else {
+ let neutralFlavors = [
+ "heavily fortified their current borders.",
+ "held their ground without gaining or losing territory.",
+ "entrenched their forces, waiting for an opening.",
+ "engaged in skirmishes but ended in a stalemate.",
+ "maintained their defensive perimeter."
+ ];
+ bbText += `${ai.name} ${neutralFlavors[Math.floor(Math.random() * neutralFlavors.length)]} `;
+ }
+ });
+
+ let diplos = visibleAIs.filter(d => d.truces > 0);
+ if (diplos.length > 0) {
+ let diploNames = diplos.map(d => d.name).join(" and ");
+ let diploFlavors = [
+ `Diplomatic channels indicate ${diploNames} brokered ceasefires. `,
+ `Envoys for ${diploNames} successfully negotiated temporary truces. `,
+ `Radio intercepts confirm ${diploNames} signed non-aggression pacts. `,
+ `Wasteland rumors suggest ${diploNames} paid heavily for peace. `
+ ];
+ bbText += diploFlavors[Math.floor(Math.random() * diploFlavors.length)];
+ }
+ } else {
+ let blindFlavors = [
+ "Radar is completely blind. All enemy movements occurred deep within the Fog of War. We have no visual on rival operations. ",
+ "Complete sensor blackout. Whatever happened out there is hidden by the fog. ",
+ "No visual contact with any faction. The wasteland is terrifyingly quiet. ",
+ "Satellite uplink failed. We are entirely blind to current enemy deployments. "
+ ];
+ bbText += blindFlavors[Math.floor(Math.random() * blindFlavors.length)];
+ }
+
+ if (ghostAIs.length > 0) {
+ let ghostNames = ghostAIs.map(g => g.name).join(", ");
+ let fogLines = [
+ `We have lost visual contact with ${ghostNames}. Their operations are shrouded in fog.`,
+ `Warning: ${ghostNames} operating undetected in the dark zones. Keep your guard up.`,
+ `Sensor anomalies suggest ${ghostNames} are maneuvering beyond our sightlines.`,
+ `Telemetry for ${ghostNames} is offline. Assume they are mobilizing in the shadows.`,
+ `Deep recon has lost the trail of ${ghostNames}. They could be anywhere.`,
+ `No confirmed sightings of ${ghostNames} this cycle. Stay vigilant.`,
+ `Radio static obscures the movements of ${ghostNames}. The dark zones remain quiet.`,
+ `Unverified reports hint that ${ghostNames} are amassing forces just outside our sensor range.`
+ ];
+ bbText += ` - ${fogLines[Math.floor(Math.random() * fogLines.length)]}`;
+ }
+ await this.logAction(bbText, false);
+ }
+ // --- END BACKBRIEF ---
+
this.turn += 1;
this.aiTurn = false;
await this.logAction(`--- DAY ${this.turn} BEGINS ---`, true);
+
// --- NEW: SUNDAY-ONLY ALTERNATING VERSE EASTER EGG ---
const today = new Date();
// 0 = Sunday. It will only run on real-world Sundays.
@@ -10830,9 +12218,15 @@ this.checkAutoPhaseAdvance();
if (this.perksEnabled && this.player.perk && this.player.perk.id === 'logistical_superiority') {
ncrBonusTroops += Math.ceil(continent.bonus * 0.5);
}
+ // Calculate Local Leader (+2 Troops, +2 Caps)
+ if (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.localLeader) {
+ continentIncome += (2 * this.player.activeBuffs.localLeader);
+ ncrBonusTroops += (2 * this.player.activeBuffs.localLeader);
+ }
}
});
+
let totalIncome = baseIncome + continentIncome;
let nukaBonus = 0;
@@ -10962,14 +12356,117 @@ this.checkAutoPhaseAdvance();
this.updateInfo();
continue;
}
- if (infoName[i]) infoName[i].parentElement.classList.add('highlight')
+ 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)];
+
+ // --- NEW: Dynamic Button Override & Tracking ---
+ let startAreasCount = this.players[i].areas.length;
+ let startTrucesCount = this.diplomacy.truces.length;
+ if (!this.players[i].isNeutral) {
+ let endBtn = document.getElementById('end');
+ if (endBtn) {
+ endBtn.disabled = true;
+ endBtn.style.opacity = "0.8";
+
+ let normalPhrases = [
+ "IS CALCULATING TACTICS...", "IS DEPLOYING REINFORCEMENTS...",
+ "IS REVIEWING SENSOR DATA...", "IS FORTIFYING THEIR PERIMETER...",
+ "IS ESTABLISHING SUPPLY LINES...", "IS RECALIBRATING TARGETING SENSORS...",
+ "IS DISPATCHING RECON PATROLS...", "IS SCAVENGING FOR AMMUNITION...",
+ "IS DISTRIBUTING RATIONS...", "IS CONDUCTING FIELD REPAIRS...",
+ "IS MONITORING HAM RADIO FREQUENCIES...", "IS CLEARING A RADROACH INFESTATION..."
+ ];
+
+ let funnyPhrases = [
+ "IS ARGUING WITH A MR. HANDY...", "IS DOWNING A NUKA-COLA QUANTUM...",
+ "IS WAITING FOR RADAWAY TO KICK IN...", "IS TRYING TO HACK A NOVICE TERMINAL...",
+ "IS KICKING A BROKEN JUKEBOX...", "IS HOARDING CANS OF CRAM...",
+ "IS RUNNING FROM A MUTATED RADSTAG...", "IS READING AN OVERDUE LIBRARY BOOK..."
+ ];
+
+ let d = new Date();
+ let month = d.getMonth() + 1; // 1-12
+ let day = d.getDate();
+ let holidayFlavors = [];
+
+ if (month === 4 && day === 1) holidayFlavors = ["WAS CAUGHT IN AN APRIL FOOLS TRAP...", "IS RIGGING A FAKE BOTTLECAP MINE..."];
+ else if (month === 2 && day === 14) holidayFlavors = ["IS WRITING A LOVE POEM FOR A DEATHCLAW...", "IS SHARING A HEART-SHAPED BOX OF MUTFRUIT..."];
+ else if (month === 3 && day === 17) holidayFlavors = ["IS SEARCHING FOR SHAMROCK GWINNETT ALE...", "IS PAINTING THEIR POWER ARMOR GREEN..."];
+ else if (month === 10 && day === 31) holidayFlavors = ["IS CARVING A GLOWING RAD-PUMPKIN...", "IS WEARING A FRIGHTENING PRE-WAR MASK..."];
+ else if (month === 11 && day === 11) holidayFlavors = ["IS SALUTING A RUINED PRE-WAR FLAG...", "IS HONORING THE FALLEN OF ANCHORAGE..."];
+ else if (month === 11 && day >= 22 && day <= 28) holidayFlavors = ["IS ROASTING A TWO-HEADED TURKEY...", "IS GIVING THANKS FOR NOT BEING EATEN..."];
+ else if (month === 12 && (day === 24 || day === 25)) holidayFlavors = ["IS DECORATING A SENTRY BOT WITH TINSEL...", "IS LEAVING MILK AND CRAM FOR SANTA..."];
+ else if ((month === 12 && day === 31) || (month === 1 && day === 1)) holidayFlavors = ["IS WRITING NEW YEAR'S RESOLUTIONS...", "IS DROPPING A HOLIDAY FAT MAN..."];
+ else if (month === 8 && day === 23) holidayFlavors = ["IS CELEBRATING A VERY SPECIAL BIRTHDAY...", "IS LIGHTING CANDLES ON A SWEET ROLL...", "IS WEARING A PRE-WAR PARTY HAT..."];
+
+ let chance = Math.random();
+ let phrase = "";
+
+ // 5% chance for a holiday phrase (if it is actually that holiday)
+ if (chance < 0.05 && holidayFlavors.length > 0) {
+ phrase = holidayFlavors[Math.floor(Math.random() * holidayFlavors.length)];
+ }
+ // 10% chance for a funny phrase (rolls between 0.05 and 0.15)
+ else if (chance < 0.15) {
+ phrase = funnyPhrases[Math.floor(Math.random() * funnyPhrases.length)];
+ }
+ // 85% chance for a normal phrase
+ else {
+ phrase = normalPhrases[Math.floor(Math.random() * normalPhrases.length)];
+ }
+
+ endBtn.textContent = `STANDBY: ${this.players[i].name.toUpperCase()} ${phrase}`;
+ }
+ }
+
+ if (turbo && !this.players[i].isNeutral) {
+ // Artificial "Thinking" Pause (0.8 to 1.2s)
+ await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 400));
+ }
+ // --- END OVERRIDE ---
+
+
+
+ // --- FIX: Suppress SYSTEM flavor logs during AI Turbo turns ---
+ if (!turbo && Math.random() < 0.20) {
+ let randomEvent;
+ // Check for Dogmeat events first
+ if (this.player.dogmeatStatus && Math.random() < 0.30) { // Increased to 30% to see them more
+ if (this.player.dogmeatStatus === 'healthy') {
+ randomEvent = dogmeatEncounters[Math.floor(Math.random() * dogmeatEncounters.length)];
+ } else if (this.player.dogmeatStatus === 'injured') {
+ randomEvent = injuredDogmeatEncounters[Math.floor(Math.random() * injuredDogmeatEncounters.length)];
+ }
+ }
+ // If no Dogmeat event, pick a standard one
+ if (!randomEvent) {
+ randomEvent = wastelandEncounters[Math.floor(Math.random() * wastelandEncounters.length)];
+ }
await this.logAction(`SYSTEM: ${randomEvent}`);
}
+
+
let aiPlayer = this.players[i];
+
+ // --- NEW: AI PASSIVE LEVELING PERKS (Turn-Start) ---
+ if (this.levelingEnabled && aiPlayer.activeBuffs) {
+ if (aiPlayer.activeBuffs.fortuneFinder) {
+ aiPlayer.caps += (aiPlayer.activeBuffs.fortuneFinder * 3);
+ }
+ if (aiPlayer.activeBuffs.scrounger) {
+ let scBonus = aiPlayer.activeBuffs.scrounger * 2;
+ aiPlayer.reserve += scBonus;
+ aiPlayer.army += scBonus;
+ }
+ if (aiPlayer.activeBuffs.solarPowered && this.commandersEnabled && aiPlayer.commander) {
+ let maxHP = 100 + (aiPlayer.activeBuffs.lifeGiver ? aiPlayer.activeBuffs.lifeGiver * 25 : 0);
+ if (aiPlayer.relics && aiPlayer.relics.some(r => r.id === 'x01armor' && r.isEquipped)) maxHP += 50;
+ aiPlayer.commander.hp = Math.min(maxHP, aiPlayer.commander.hp + (aiPlayer.activeBuffs.solarPowered * 10));
+ }
+ }
+
// --- FIX: EXILED ASYLUM REQUESTS ---
+
if (this.isAllianceMode && aiPlayer.areas.length === 0 && aiPlayer.commander && aiPlayer.commander.hp < 50 && !aiPlayer.hasRequestedAsylum) {
aiPlayer.hasRequestedAsylum = true; // Prevent spamming every turn
let ally = this.players.find(p => p.team === aiPlayer.team && p.name !== aiPlayer.name && p.alive && p.areas.length > 1);
@@ -11254,9 +12751,10 @@ if (this.bobbleheads) {
}
}
- if (this.nukesEnabled && this.players[i].codes >= 4 && !this.activeNuke) {
+ if (this.nukesEnabled && this.players[i].codes >= 4 && !(this.activeNukes && this.activeNukes.some(n => n.launcher === this.players[i].name))) {
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) {
let launchSite = silos.sort((a, b) => b.siloTurns - a.siloTurns)[0];
@@ -11281,8 +12779,14 @@ if (this.bobbleheads) {
if (this.perksEnabled && this.players[i].perk?.id === 'logistical_superiority') {
ncrBonusTroops += Math.ceil(continent.bonus * 0.5);
}
+ // --- AI PERK: Local Leader ---
+ if (this.levelingEnabled && this.players[i].activeBuffs && this.players[i].activeBuffs.localLeader) {
+ continentIncome += (2 * this.players[i].activeBuffs.localLeader);
+ ncrBonusTroops += (2 * this.players[i].activeBuffs.localLeader);
+ }
}
});
+
income += continentIncome;
// --- AI PERK: Nuka-World Tribute Chest ---
@@ -11294,6 +12798,16 @@ if (this.bobbleheads) {
this.players[i].caps += income;
+ // --- NEW: Dynamic Normal Difficulty Scaling (Economy) ---
+ let totalPlayable = this.countries.filter(c => !c.isCrater).length;
+ let playerOwnedCount = this.countries.filter(c => c.owner === this.player.name).length;
+if (this.difficulty === "Normal" && totalPlayable > 0 && (playerOwnedCount / totalPlayable) > 0.35) {
+
+ this.players[i].caps += 5;
+ this.players[i].reserve += 1;
+ this.players[i].army += 1;
+ }
+
if (!turbo) {
if (nukaBonus > 0) await this.logAction(`${this.players[i].name} collected ${income} Caps (including Tribute Chest bonuses).`);
else await this.logAction(`${this.players[i].name} collected ${income} Caps in taxes.`);
@@ -11323,7 +12837,17 @@ if (this.bobbleheads) {
} else {
// Original AI card trade-in and reinforcement logic
this.players[i].reserve = this.unitBonus(this.players[i], i);
+
+ // --- NEW: Dynamic Normal Difficulty Scaling (Classic) ---
+ let totalPlayable = this.countries.filter(c => !c.isCrater).length;
+ let playerOwnedCount = this.countries.filter(c => c.owner === this.player.name).length;
+if (this.difficulty === "Normal" && totalPlayable > 0 && (playerOwnedCount / totalPlayable) > 0.35) {
+
+ this.players[i].reserve += 2;
+ }
+
let troopsToPlace = this.players[i].reserve;
+
// --- AI PERK: Nuka-World Tribute Chest (Classic Mode) ---
if (this.perksEnabled && this.players[i].perk?.id === 'tribute_chest') {
@@ -11399,57 +12923,66 @@ if (this.bobbleheads) {
if (targetPlayer && targetActiveTruces < 1) {
this.logDebug(`${aiPlayer.name} chose to approach ${targetName}.`);
- const cost = 10; // New flat cost
+
+ // --- FIX: Dynamic Cost based on Economy Mode ---
+ const isEcon = this.wastelandEconomyActive;
+ const cost = isEcon ? 10 : 1; // 10 Caps or 1 Card
+ const currencyLabel = isEcon ? "CAPS" : "CARD";
+
+ let aiHasFunds = isEcon ? (aiPlayer.caps >= cost) : (aiPlayer.cards.length >= cost);
+ let targetHasFunds = isEcon ? (targetPlayer.caps >= cost) : (targetPlayer.cards.length >= cost);
// FIX ISSUE #6: AI Extortion Logic
- if (aiPlayer.army >= targetPlayer.army * 2 && targetPlayer.caps >= cost) {
+ if (aiPlayer.army >= targetPlayer.army * 2 && targetHasFunds) {
this.logDebug(`${aiPlayer.name} is overwhelmingly stronger than ${targetName} and will attempt extortion.`);
if (targetPlayer.isPlayer) {
let accepted = await this.showEnvoyModal(aiPlayer.name, cost, 3, aiPlayer.color, false, 0, true);
if (accepted) {
- targetPlayer.caps -= cost;
- aiPlayer.caps += cost;
- this.diplomacy.truces.push({ f1: aiPlayer.name, f2: targetPlayer.name, turns: 3, locked: true }); // LOCKED
+ if (isEcon) { targetPlayer.caps -= cost; aiPlayer.caps += cost; }
+ else { aiPlayer.cards.push(targetPlayer.cards.pop()); }
+ // FIX: Set turns to 4 secretly to account for immediate tick-down
+ this.diplomacy.truces.push({ f1: aiPlayer.name, f2: targetPlayer.name, turns: 4, locked: true });
this.updateInfo();
- if (this.queueToast) this.queueToast(`EXTORTION PAID: -${cost} CAPS`, "#ff3333");
+ if (this.queueToast) this.queueToast(`EXTORTION PAID: -${cost} ${currencyLabel}`, "#ff3333");
if (!turbo) await this.logAction(`[ DIPLOMACY ] You yielded to ${aiPlayer.name}'s extortion demand.`, true);
} else {
if (!turbo) await this.logAction(`[ DIPLOMACY ] You rejected ${aiPlayer.name}'s extortion attempt. Prepare for war.`, true);
}
} else {
// AI Extorts AI
- targetPlayer.caps -= cost;
- aiPlayer.caps += cost;
- this.diplomacy.truces.push({ f1: aiPlayer.name, f2: targetName, turns: 3, locked: true });
+ if (isEcon) { targetPlayer.caps -= cost; aiPlayer.caps += cost; }
+ else { aiPlayer.cards.push(targetPlayer.cards.pop()); }
+ this.diplomacy.truces.push({ f1: aiPlayer.name, f2: targetName, turns: 4, locked: true });
if (!turbo) await this.logAction(`[ DIPLOMACY ] ${targetName} was extorted into a Ceasefire by ${aiPlayer.name}.`);
}
}
// Standard Truce Offer
- else if (aiPlayer.caps >= cost) {
- this.logDebug(`${aiPlayer.name} can afford the truce cost of ${cost} caps.`);
- aiPlayer.caps -= cost;
+ else if (aiHasFunds) {
+ this.logDebug(`${aiPlayer.name} can afford the truce cost.`);
+ if (isEcon) aiPlayer.caps -= cost; else { aiPlayer.cards.pop(); }
if (targetPlayer.isPlayer) {
let accepted = await this.showEnvoyModal(aiPlayer.name, cost, 3, aiPlayer.color);
if (accepted) {
- this.player.caps += cost;
- this.diplomacy.truces.push({ f1: aiPlayer.name, f2: this.player.name, turns: 3 });
+ if (isEcon) { this.player.caps += cost; } else { this.player.cards.push({ country: "Envoy Gift", type: "Wild" }); }
+ // FIX: Set turns to 4 secretly to account for immediate tick-down
+ this.diplomacy.truces.push({ f1: aiPlayer.name, f2: this.player.name, turns: 4 });
this.updateInfo();
- if (this.queueToast) this.queueToast(`CEASEFIRE AGREED! +${cost} CAPS`, "var(--pip-color)");
+ if (this.queueToast) this.queueToast(`CEASEFIRE AGREED! +${cost} ${currencyLabel}`, "var(--pip-color)");
if (!turbo) await this.logAction(`[ DIPLOMACY ] You accepted a Ceasefire with ${aiPlayer.name}.`, true);
} else {
- aiPlayer.caps += cost; // Refund
+ if (isEcon) aiPlayer.caps += cost; else aiPlayer.cards.push({ country: "Refund", type: "Wild" }); // Refund
if (!turbo) await this.logAction(`[ DIPLOMACY ] You rejected ${aiPlayer.name}'s Ceasefire offer.`, true);
}
} else {
- // FIX: Restored AI-to-AI Negotiation
+ // Restored AI-to-AI Negotiation
let rep = this.diplomacy.reputation[targetName][aiPlayer.name] || 0;
if (rep >= -10) {
- targetPlayer.caps += cost;
- this.diplomacy.truces.push({ f1: aiPlayer.name, f2: targetName, turns: 3 });
+ if (isEcon) targetPlayer.caps += cost; else targetPlayer.cards.push({ country: "Envoy Gift", type: "Wild" });
+ this.diplomacy.truces.push({ f1: aiPlayer.name, f2: targetName, turns: 4 });
if (!turbo) await this.logAction(`[ DIPLOMACY ] ${aiPlayer.name} and ${targetName} have formed a Ceasefire agreement.`);
} else {
- aiPlayer.caps += cost; // Refund
+ if (isEcon) aiPlayer.caps += cost; else aiPlayer.cards.push({ country: "Refund", type: "Wild" }); // Refund
this.logDebug(`${targetName} rejected ${aiPlayer.name}'s offer due to low reputation.`);
}
}
@@ -11468,8 +13001,10 @@ if (this.bobbleheads) {
this.players[i].areas.forEach(area => {
let country = this.countries.find(c => c.name === area);
- if (this.players[i].reserve > 0) {
+ // --- FIX: AI respects Cryolator and Shroud Card ---
+ if (this.players[i].reserve > 0 && !(country.isFrozen > 0) && !(country.isBlockaded > 0)) {
let ratio = 0;
+
// NEW: Massive priority boost if the territory actually borders a valid enemy!
let hasEnemyNeighbor = country.neighbours.some(n => {
@@ -11478,23 +13013,57 @@ if (this.bobbleheads) {
});
if (hasEnemyNeighbor) ratio += 10.0;
+ // --- FIX: Desperation Fortify (Nuke Incoming) ---
+ if (this.activeNukes && this.activeNukes.length > 0) {
+ let incomingNuke = this.activeNukes.find(n => n.launcher !== this.players[i].name);
+ if (incomingNuke && country.neighbours.includes(incomingNuke.launchSilo)) {
+ ratio += 5000.0; // Drop all reinforcements near the silo to assault it!
+ }
+ }
+
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);
}
+ // --- FIX: Smart Continent Fortification ---
+ let continent = continents.find(x => x.name === country.continent);
+ let aiOwnedCount = 0;
+ continent.areas.forEach(x => {
+ if (this.players[i].areas.includes(x)) aiOwnedCount++;
+ });
+
+ // 1. Consolidate: If we own >50%, heavily fortify to push for the rest
+ if (aiOwnedCount >= continent.areas.length / 2 && aiOwnedCount < continent.areas.length) {
+ ratio += 20.0;
+ } else if (aiOwnedCount === continent.areas.length) {
+ ratio += 5.0; // Protect fully owned continent borders
+ } else {
+ ratio += (aiOwnedCount / continent.areas.length);
+ }
+
+ // 2. Disrupt: Fortify borders touching a continent monopolized by an enemy
+ country.neighbours.forEach(n => {
+ let nc = this.countries.find(x => x.name === n);
+ if (nc && nc.owner !== this.players[i].name && !nc.isCrater) {
+ let neighborCont = continents.find(x => x.name === nc.continent);
+ let enemyOwned = 0;
+ neighborCont.areas.forEach(nx => {
+ let nxc = this.countries.find(x => x.name === nx);
+ if (nxc && nxc.owner === nc.owner) enemyOwned++;
+ });
+ if (enemyOwned === neighborCont.areas.length) {
+ ratio += 15.0; // Big boost to prep an attack against the monopoly
+ }
+ }
+ });
+
// Add a tiny random factor so they don't always stack the exact same territory if tied
ratio += Math.random() * 0.5;
if (ratio > areaToFortify[1]) {
areaToFortify = [country, ratio]
}
+
}
});
if (areaToFortify[0]) {
@@ -11502,18 +13071,60 @@ if (this.bobbleheads) {
this.players[i].reserve = 0;
}
+ // --- FIX: AI Prydwen Deployment Logic ---
+ if (this.perksEnabled && this.players[i].perk && this.players[i].perk.id === 'prydwen_deployment' && this.players[i].airborneTroops > 0) {
+ let borderTiles = [];
+ this.players[i].areas.forEach(area => {
+ let country = this.countries.find(c => c.name === area);
+ if (country && !(country.isFrozen > 0) && !(country.isBlockaded > 0) && this.isContinentBorder(country.name)) {
+ borderTiles.push(country);
+ }
+ });
+
+ if (borderTiles.length > 0) {
+ // Find the weakest continent border
+ borderTiles.sort((a, b) => a.army - b.army);
+ let targetBorder = borderTiles[0];
+ targetBorder.army += this.players[i].airborneTroops;
+ this.players[i].army += this.players[i].airborneTroops;
+ this.players[i].airborneTroops = 0;
+ if (!turbo) await this.logAction(`[ INTEL ] The Prydwen reinforced ${formatTerritoryName(targetBorder.name)} with 3 airborne troops.`);
+ }
+ }
+ // --- END FIX ---
+
let currentAreas = [...this.players[i].areas];
for (let area of currentAreas) {
let country = this.countries.find(c => c.name === area);
- if (country && country.army > 1 && country.owner === this.players[i].name && (!this.nukesEnabled || !country.isSilo || Math.random() < 0.2)) {
+
+ // --- FIX: The Blitz Loop (Multi-Attack) ---
+ // The AI will keep attacking as long as it has overwhelming force (e.g. 5+ troops)
+ while (country && country.army > 4 && country.owner === this.players[i].name && (!this.nukesEnabled || !country.isSilo || Math.random() < 0.2)) {
+
+ // Before we attack, remember how many territories the AI owns
+ let previousAreaCount = this.players[i].areas.length;
+
await this.aiAttack(country, i, turbo);
- // NEW: If this attack ended the game, freeze the AI instantly!
- if (this.gameOver) return;
+ if (this.gameOver) return;
+
+ // Did the AI successfully conquer a territory during that attack?
+ if (this.players[i].areas.length > previousAreaCount) {
+ // If so, the AI's army just moved into the NEW territory.
+ // We need to update our 'country' variable to be the new front line!
+ // (The newest territory is always at the end of the array)
+ let newFrontLineName = this.players[i].areas[this.players[i].areas.length - 1];
+ country = this.countries.find(c => c.name === newFrontLineName);
+
+ // If for some reason the new front line doesn't have troops, break the blitz
+ if (!country || country.army <= 4) break;
+ } else {
+ // The attack failed, or was aborted by the suicide prevention. Break the blitz.
+ break;
+ }
}
-
-
}
this.aiManeuver(i);
+
if (this.players[i].conqueredThisTurn) {
if (this.wastelandEconomyActive) {
// --- NEW: AI receives raw Caps ---
@@ -11542,7 +13153,14 @@ if (this.bobbleheads) {
this.globalCodes--;
this.players[i].nukeChance = 0.15; // Reset
let refTile = this.players[i].areas[0];
- if (this.logFog) await this.logFog(refTile, `[ INTEL ] Radio intercepts indicate ${this.players[i].name} has secured a Nuclear Launch Code!`, true, "battle");
+ let flavors = [
+ `[ INTEL ] Radio intercepts indicate ${this.players[i].name} has secured a Nuclear Launch Code!`,
+ `[ INTEL ] A decrypted databank confirms ${this.players[i].name} extracted a Nuke Code Fragment.`,
+ `[ INTEL ] Panic in the wastes: ${this.players[i].name} is one step closer to nuclear launch capability.`,
+ `[ INTEL ] Black-box telemetry shows ${this.players[i].name} downloading silo launch protocols.`
+ ];
+ if (this.logFog) await this.logFog(refTile, flavors[Math.floor(Math.random() * flavors.length)], true, "battle");
+
} else {
this.players[i].nukeChance = Math.min(0.45, this.players[i].nukeChance + 0.05); // Escalate
}
@@ -11561,18 +13179,42 @@ if (this.bobbleheads) {
this.players[i].commander.ap = 2;
}
this.updateInfo();
- await this.aiRelicCheck(this.players[i]);
+ await this.aiRelicCheck(this.players[i]);
+
+ // --- NEW: Turbo Mode Summary Log (Silenced for Backbrief) ---
+ if (turbo && !this.players[i].isNeutral) {
+ let netAreas = this.players[i].areas.length - startAreasCount;
+ let newTruces = this.diplomacy.truces.length - startTrucesCount;
+
+ // Check visibility for Fog of War
+ let isVisible = false;
+ this.players[i].areas.forEach(a => { if (this.isTerritoryVisible(a)) isVisible = true; });
+
+ backbriefData.push({
+ name: this.players[i].name,
+ net: netAreas,
+ truces: newTruces,
+ visible: isVisible
+ });
+
+ // Artificial pause only. No individual logs!
+ await new Promise(resolve => setTimeout(resolve, 500));
+ }
+ // --- END SUMMARY ---
+
}
}
}
+
Gamestate.aiAttack = async function (country, i, turbo) {
this.logDebug(`AI ${this.players[i].name} is considering an attack FROM ${formatTerritoryName(country.name)} (${country.army} troops).`);
- // --- NEW: Prevent AI from attacking FROM a locked territory ---
- if (country.isLockedDown) return;
+ // --- NEW: Prevent AI from attacking FROM a locked or frozen territory ---
+ if (country.isLockedDown || country.isFrozen > 0) return;
+
// --- CORRECTED AI TARGETING LOGIC ---
// The AI can only see and target territories directly adjacent to the attacking country.
@@ -11642,19 +13284,52 @@ if (this.bobbleheads) {
// --- AI DESPERATION CHECK ---
let isDesperate = false;
- if (this.activeNuke && this.activeNuke.launcher !== this.players[i].name) {
- isDesperate = true;
+ let incomingNuke = null;
+ if (this.activeNukes) {
+ incomingNuke = this.activeNukes.find(n => n.launcher !== this.players[i].name);
+ if (incomingNuke) isDesperate = true;
}
let target = [possibleTargets[0], -999]; // This line is now safe.
+
possibleTargets.forEach(poss => {
let debugMsgs = [`Owner: ${poss.owner}`]; // Start logging
- let continent = continents.find(x => x.name === poss.continent); let count = 0;
- continent.areas.forEach(x => { if (this.players[i].areas.includes(x)) count++; });
- let ratio = count / continent.areas.length;
+ let continent = continents.find(x => x.name === poss.continent);
+
+ let aiOwnedCount = 0;
+ let enemyOwnedCount = 0;
+ continent.areas.forEach(x => {
+ if (this.players[i].areas.includes(x)) aiOwnedCount++;
+ else if (this.countries.find(c => c.name === x).owner === poss.owner) enemyOwnedCount++;
+ });
+
+ let ratio = aiOwnedCount / continent.areas.length;
debugMsgs.push(`Base score (continent): ${ratio.toFixed(2)}`); // Log base score
+ // --- FIX: Smart Combat (No Suicides, Consolidate, Disrupt) ---
+ // 1. Suicide Prevention (Unless desperate for a nuke)
+ if (!isDesperate && country.army <= poss.army + 1) {
+ ratio -= 1000.0;
+ debugMsgs.push('Suicide Prevention: -1000.0');
+ } else if (country.army > poss.army * 2) {
+ ratio += 15.0; // Go after weak targets!
+ debugMsgs.push('Overwhelming Force: +15.0');
+ }
+
+ // 2. Continent Consolidation
+ if (aiOwnedCount >= continent.areas.length / 2) {
+ ratio += 40.0;
+ debugMsgs.push('Continent Consolidation: +40.0');
+ }
+
+ // 3. Continent Disruption
+ if (enemyOwnedCount === continent.areas.length) {
+ ratio += 50.0;
+ debugMsgs.push('Continent Disruption: +50.0');
+ }
+
// --- NEW: REPUTATION MODIFIERS ---
+
let currentRep = 0;
if (poss.owner !== "none" && poss.owner !== "Wasteland Horrors") {
currentRep = this.diplomacy.reputation[this.players[i].name][poss.owner] || 0;
@@ -11668,8 +13343,9 @@ if (this.bobbleheads) {
// Log desperation effects
if (poss.owner === this.diplomacy.spiteTarget) { ratio += 100; debugMsgs.push('Spite Bonus: +100'); }
- if (isDesperate && poss.owner === this.activeNuke.launcher) { ratio += 500; debugMsgs.push('Desperation Bonus (Nuke Launcher): +500'); }
- if (isDesperate && this.activeNuke.launchSilo === poss.name) { ratio += 5000; debugMsgs.push('Desperation Bonus (Nuke Silo): +5000'); }
+ if (isDesperate && incomingNuke && poss.owner === incomingNuke.launcher) { ratio += 500; debugMsgs.push('Desperation Bonus (Nuke Launcher): +500'); }
+ if (isDesperate && incomingNuke && incomingNuke.launchSilo === poss.name) { ratio += 5000; debugMsgs.push('Desperation Bonus (Nuke Silo): +5000'); }
+
// --- FIX: AI Silo Prioritization ---
if (this.nukesEnabled && poss.isSilo) {
@@ -11717,8 +13393,13 @@ if (this.bobbleheads) {
- if (!target[0]) { return; }
+ // FIX: Abort the attack entirely if the best option is suicidal
+ if (!target[0] || target[1] < -500) {
+ this.logDebug(`AI ${this.players[i].name} aborted attack. All targets are too strong (Suicide Prevention).`);
+ return;
+ }
this.logDebug(`AI ${this.players[i].name} has chosen to attack ${formatTerritoryName(target[0].name)} with a score of ${target[1].toFixed(2)}.`);
+
// --- NEW: CLEAR ALLIANCE TARGET FLAG ---
// If the AI just fulfilled your request to attack this specific target, clear the flag
if (this.isAllianceMode && this.players[i].attackTarget === target[0].name) {
@@ -11788,13 +13469,15 @@ if (this.bobbleheads) {
const perkId = this.player.perk?.id;
if (!this.perksEnabled || !perkId || (perkId !== 'minutemen_contracts' && perkId !== 'gunner_contracts' && perkId !== 'mercenary_contracts') || this.aiTurn) return;
- if (this.player.caps < 20) {
- if (this.showToast) this.showToast("Not enough Caps! Contract requires 20.", "red");
+ let mercCost = (this.levelingEnabled && this.player.activeBuffs && this.player.activeBuffs.gunNut) ? 15 : 20;
+ if (this.player.caps < mercCost) {
+ if (this.showToast) this.showToast(`Not enough Caps! Contract requires ${mercCost}.`, "red");
return;
}
// Pay the cost
- this.player.caps -= 20;
+ this.player.caps -= mercCost;
+
// ---> CHANGED: 6 to 12 troops + 1 for every 2 territories owned
const reward = (Math.floor(Math.random() * 7) + 6) + Math.floor(this.player.areas.length / 2);
@@ -11838,9 +13521,15 @@ if (this.bobbleheads) {
player.commander.loc = friendlyNeighbors[0].name;
player.commander.ap -= 1; movedOrAction = true;
player.commander.siegeTurns = 0; // Reset subversion timer
- if (Gamestate.logFog) Gamestate.logFog(friendlyNeighbors[0].name, `Commander MOVEMENT: ${player.name}'s Commander retreats to ${formatTerritoryName(friendlyNeighbors[0].name)}.`, false, "move");
- else if (Gamestate.logAction) Gamestate.logAction(`Commander MOVEMENT: ${player.name}'s Commander retreats to ${formatTerritoryName(friendlyNeighbors[0].name)}.`);
+
+ // --- NEW: Ninja Perk ---
+ let isNinja = this.levelingEnabled && player.activeBuffs && player.activeBuffs.ninja;
+ if (!isNinja) {
+ if (Gamestate.logFog) Gamestate.logFog(friendlyNeighbors[0].name, `Commander MOVEMENT: ${player.name}'s Commander retreats to ${formatTerritoryName(friendlyNeighbors[0].name)}.`, false, "move");
+ else if (Gamestate.logAction) Gamestate.logAction(`Commander MOVEMENT: ${player.name}'s Commander retreats to ${formatTerritoryName(friendlyNeighbors[0].name)}.`);
+ }
}
+
}
@@ -11921,8 +13610,18 @@ if (this.bobbleheads) {
score += 50;
score += n.army; // Prefer tiles with more troops
if(this.nukesEnabled && n.isSilo) score += 100; // Strongly prefer silos
- } else if (n.owner !== 'none') {
- score -= 25; // Avoid enemy territory unless necessary
+ } else if (n.owner !== 'none' && !this.areAllies(player.name, n.owner) && !n.isCrater) {
+ // --- FIX: Aggressive Conversion Logic ---
+ // If Commander is healthy and the enemy tile is weak (or empty), step in to convert!
+ if (player.commander.hp > 70 && n.army === 0) {
+ score += 60; // Highly prioritize grabbing empty land!
+ } else if (player.commander.hp > 85 && n.army <= 15) {
+ score += 30; // Willing to siege moderately weak lands
+ } else {
+ score -= 25; // Avoid heavily fortified enemy territory
+ }
+ } else {
+ score -= 25; // Avoid craters or allied/neutral lands
}
if(score > bestMove.score) {
@@ -11953,6 +13652,88 @@ if (this.bobbleheads) {
}
+ // --- NEW: AI TROOP MANEUVERS & PERK PARITY ---
+ let maneuverPoints = (this.perksEnabled && player.perk && player.perk.id === 'rapid_relocation') ? 5 : 1;
+
+ // Allow Agility Bobblehead to add +1
+ let aBobble = this.bobbleheads && this.bobbleheads.find(b => b.key === 'a' && b.active && b.owner === player.name);
+ if (aBobble) maneuverPoints += 1;
+
+ while (maneuverPoints > 0) {
+ let borders = [];
+ let safeInteriors = [];
+
+ // Categorize territories
+ owned.forEach(t => {
+ if (t.isLockedDown || t.isExploring || t.isFrozen > 0) return;
+ let hasEnemyNeighbor = t.neighbours.some(n => {
+
+ let nc = this.countries.find(x => x.name === n);
+ return nc && nc.owner !== player.name && !this.areAllies(player.name, nc.owner) && !nc.isCrater;
+ });
+ if (hasEnemyNeighbor) borders.push(t);
+ else if (t.army > 1) safeInteriors.push(t);
+ });
+
+ let moved = false;
+
+ // 1. Great Khans (Guerrilla Tactics)
+ if (this.perksEnabled && player.perk && player.perk.id === 'guerrilla_tactics') {
+ let source = owned.find(t => t.army > 2 && !t.isLockedDown && !t.isExploring);
+ if (source) {
+ let enemyTargets = source.neighbours.map(n => this.countries.find(x => x.name === n)).filter(c => c && c.owner !== player.name && !this.areAllies(player.name, c.owner) && !c.isCrater);
+ for (let enemy of enemyTargets) {
+ let destination = enemy.neighbours.map(n => this.countries.find(x => x.name === n)).find(c => c && c.owner === player.name && c.name !== source.name);
+ if (destination) {
+ let moveAmt = source.army - 1;
+ destination.army += moveAmt;
+ source.army -= moveAmt;
+ let dmg = Math.max(1, Math.floor(enemy.army * 0.15));
+ enemy.army = Math.max(1, enemy.army - dmg);
+ if (this.logDebug) this.logDebug(`[AI MANEUVER] ${player.name} used Guerrilla Tactics through ${enemy.name}.`);
+ moved = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // 2. The Enclave (Vertibird Assault)
+ if (!moved && this.perksEnabled && player.perk && player.perk.id === 'vertibird_assault') {
+ let source = safeInteriors.slice().sort((a, b) => b.army - a.army)[0];
+ if (!source) source = owned.slice().sort((a, b) => b.army - a.army)[0]; // Fallback
+ let dest = borders.slice().sort((a, b) => a.army - b.army)[0];
+
+ if (source && dest && source.name !== dest.name && source.army > 1) {
+ let moveAmt = source.army - 1;
+ dest.army += moveAmt;
+ source.army -= moveAmt;
+ if (this.logDebug) this.logDebug(`[AI MANEUVER] ${player.name} used Vertibird Assault from ${source.name} to ${dest.name}.`);
+ moved = true;
+ }
+ }
+
+ // 3. Standard / Railroad Maneuver
+ if (!moved) {
+ safeInteriors.sort((a, b) => b.army - a.army);
+ borders.sort((a, b) => a.army - b.army);
+
+ for (let interior of safeInteriors) {
+ let validDest = borders.find(b => interior.neighbours.includes(b.name));
+ if (validDest) {
+ let moveAmt = interior.army - 1;
+ validDest.army += moveAmt;
+ interior.army -= moveAmt;
+ if (this.logDebug) this.logDebug(`[AI MANEUVER] ${player.name} standard maneuvered from ${interior.name} to ${validDest.name}.`);
+ moved = true;
+ break;
+ }
+ }
+ }
+
+ if (!moved) break; // No valid moves found
+ maneuverPoints--;
+ }
@@ -12017,7 +13798,17 @@ if (this.bobbleheads) {
} else if (this.difficulty === "Hard") {
if (player === this.player) attackerWinChance = 0.40;
else if (opp === this.player) attackerWinChance = 0.60;
+ } else if (this.difficulty === "Normal") {
+ // --- NEW: Dynamic Rubber-Band Scaling ---
+ let totalPlayable = this.countries.filter(c => !c.isCrater).length;
+ let playerOwnedCount = this.countries.filter(c => c.owner === this.player.name).length;
+if (totalPlayable > 0 && (playerOwnedCount / totalPlayable) > 0.35) {
+
+ if (player === this.player) attackerWinChance -= 0.05; // Player is dominating
+ else if (opp === this.player) attackerWinChance += 0.05;
+ }
}
+
if (opp.isNeutral) attackerWinChance -= 0.15;
// --- BOBBLEHEAD COMBAT MODIFIERS (STRENGTH & ENDURANCE) ---
@@ -12040,7 +13831,14 @@ if (this.bobbleheads) {
if (iBobble && iBobble.active && iBobble.owner === player.name) intelActive = true;
}
}
+
+ // --- NEW: DOGMEAT OFFENSE BUFF (HEALTHY ONLY) ---
+ if (player.dogmeatStatus === 'healthy' && player.name !== opp.name) {
+ attackerWinChance += 0.10;
+ }
+ // FIX: Declare this variable outside the block so the Silo logic can see it!
+ let ignoreDefenses = (this.levelingEnabled && player.activeBuffs && player.activeBuffs.demolitionExpert);
if (this.perksEnabled) {
// Power Armor Infantry bonus (Attacker)
@@ -12054,7 +13852,8 @@ if (this.bobbleheads) {
// --- NEW: Ranger Network Defensive Bonus ---
// Only apply if the territory has defenders and the attacker is not an ally
- if (opp && opp.perk && opp.perk.id === 'ranger_network' && opponent.army > 0 && !this.areAllies(player.name, opp.name)) {
+ if (!ignoreDefenses && opp && opp.perk && opp.perk.id === 'ranger_network' && opponent.army > 0 && !this.areAllies(player.name, opp.name)) {
+
// Calculate the size of the connected block the defending territory is in
const blockSize = this.getConnectedBlockSize(opp, opponent.name);
@@ -12074,7 +13873,7 @@ if (this.bobbleheads) {
}
- if (this.nukesEnabled && opponent.isSilo) {
+ if (!ignoreDefenses && 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;
@@ -12084,8 +13883,14 @@ if (this.bobbleheads) {
if (this.commandersEnabled && opp.commander && opp.commander.loc === opponent.name) {
attackerWinChance = attackerWinChance * 0.80;
+
+ // --- NEW: X-01 Power Armor Defense Bonus ---
+ if (opp.relics && opp.relics.some(r => r.id === 'x01armor' && r.isEquipped)) {
+ attackerWinChance -= 0.15;
+ }
}
if (attackerWinChance < 0.11) attackerWinChance = 0.11;
+
// --- NEW: 100% win chance against empty territories ---
if (opponent.army === 0) attackerWinChance = 1.0;
@@ -12093,9 +13898,34 @@ if (this.bobbleheads) {
if (player === this.player) attackerWinChance = Gamestate.devWinOverride;
if (opp === this.player) attackerWinChance = 1.0 - (Gamestate.devWinOverride);
}
- this.logDebug(`Battle: ${player.name} (${country.army}) vs ${opponent.owner} (${opponent.army}) in ${formatTerritoryName(opponent.name)}. Attacker win chance: ${(attackerWinChance * 100).toFixed(1)}%`);
+
+ // --- NEW: Calculate Overall Battle Probability for the Debug Log ---
+ let a_troops = country.army - 1;
+ let d_troops = opponent.army;
+ let overallWinProb = 0;
+
+ // We use the exact same calculation here that the hover tooltip uses to ensure parity
+ if (a_troops <= 0) {
+ overallWinProb = 0;
+ } else if (d_troops === 0) {
+ overallWinProb = 1;
+ } else if (attackerWinChance === 0.5) {
+ // Standard perfectly fair 50/50 dice odds (with Defender's Advantage)
+ overallWinProb = a_troops / (a_troops + d_troops);
+ } else {
+ let q = 1 - attackerWinChance;
+ let ratio = q / attackerWinChance;
+ overallWinProb = (1 - Math.pow(ratio, a_troops)) / (1 - Math.pow(ratio, a_troops + d_troops));
+ }
+
+ // Add the absolute floor/ceiling to match the tooltip
+ if (overallWinProb > 0.95) overallWinProb = 0.95;
+ if (overallWinProb < 0.01) overallWinProb = 0.01;
+
+ this.logDebug(`Battle: ${player.name} (${country.army}) vs ${opponent.owner} (${opponent.army}) in ${formatTerritoryName(opponent.name)}. Overall win prob: ${(overallWinProb * 100).toFixed(1)}% (Per-Roll: ${(attackerWinChance * 100).toFixed(1)}%)`);
let isVictory = false;
+
let flavor = "";
if (this.perksEnabled) {
// --- NEW: Apply variable Chem Frenzy bonus ---
@@ -12125,7 +13955,22 @@ if (this.bobbleheads) {
}
}
+ // --- FIX: Mysterious Stranger on Defense ---
+ if (this.perksEnabled && opp.perk?.id === 'mysterious_stranger') {
+ const wouldLose = (opponent.army <= country.army) && (Math.random() < attackerWinChance);
+ if ((opp.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 to defend!", true);
+ attackerWinChance -= 0.25; // Decrease attacker's chance
+ if (attackerWinChance < 0.11) attackerWinChance = 0.11; // Hard minimum
+ opp.strangerCooldown = Math.floor(Math.random() * 4) + 1;
+ }
+ }
+
// --- NEW: The Minutemen (Flare Gun Rescue) ---
+
if (opp.perk?.id === 'minutemen_contracts') {
// "Deemed to lose" check: Is the attacker bringing more troops than the defender has?
if (country.army > opponent.army) {
@@ -12152,9 +13997,59 @@ if (this.bobbleheads) {
}
const originalDefenderArmy = opponent.army;
const originalAttackerArmy = country.army;
+
+ // --- NEW: BOTTLECAP MINE DETONATION & DOGMEAT IMMUNITY ---
+ if (opponent.hasMine) {
+ opponent.hasMine = false; // Remove the mine so it doesn't trigger again
+
+ if (player.dogmeatStatus === 'healthy') {
+
+ await this.logAction(`[ BOMB DEFUSED ] Dogmeat sniffed out the Bottlecap Mine at ${formatTerritoryName(opponent.name)} before it could detonate!`, true);
+ } else {
+ let dmg = Math.min(country.army - 1, Math.floor(Math.random() * 8) + 4); // Deals 4 to 11 damage
+ if (dmg > 0) {
+ country.army -= dmg;
+ player.army -= dmg;
+ if (attacker && attacker.nextElementSibling) attacker.nextElementSibling.textContent = country.army;
+ if (this.queueToast) this.queueToast("BOMB DETONATED!", "red");
+ await this.logAction(`[ TRAP TRIGGERED ] Bottlecap Mine detonated at ${formatTerritoryName(opponent.name)}! Attacker lost ${dmg} troops.`, true);
+ }
+ }
+ }
+
+ // --- [ FIXED & RELOCATED LEVELING PERKS COMBAT MODIFIERS ] ---
+ if (this.levelingEnabled) {
+ if (player.activeBuffs) {
+ // 1. Bloody Mess: +5% per stack on all attacks
+ if (player.activeBuffs.bloodyMess) attackerWinChance += (player.activeBuffs.bloodyMess * 0.05);
+
+ // 3. Ghoul Slayer: Massive +15% against Wasteland Horrors
+ if (player.activeBuffs.ghoulSlayer && opp.name === "Wasteland Horrors") attackerWinChance += 0.15;
+
+ // 4. Commando: Pre-emptive strike
+ if (player.activeBuffs.commando && originalAttackerArmy >= 10 && opponent.army > 0) {
+ opponent.army -= 1;
+ this.logAction(`[ COMMANDO ] Pre-emptive strike eliminated 1 defender before the roll.`);
+ if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army;
+ }
+ }
+ if (opp.activeBuffs) {
+ // 2. Toughness: +5% per stack on all defense
+ if (opp.activeBuffs.toughness) attackerWinChance -= (opp.activeBuffs.toughness * 0.05);
+
+ // 5. Rooted: +10% defense if territory hasn't moved troops recently
+ if (opp.activeBuffs.rooted && !opponent.wasManeuveredThisTurn) {
+ attackerWinChance -= 0.10;
+ }
+ }
+ }
+ // --- END FIXED LEVELING PERKS ---
+
while (opponent.army > 0 && country.army > 1) {
+
+
let attackerRoll = Math.random();
- let attackerWins = (attackerRoll > attackerWinChance);
+ let attackerWins = (attackerRoll < attackerWinChance);
if (attackerWins) {
opponent.army -= 1;
@@ -12214,29 +14109,6 @@ if (opp && !opp.isNeutral) this.addXP(opp, 4);
}
}
- // --- [ COMPLETED LEVELING PERKS COMBAT MODIFIERS ] ---
- if (this.levelingEnabled) {
- // 1. Bloody Mess: +5% per stack on all attacks
- if (player.activeBuffs.bloodyMess) attackerWinChance += (player.activeBuffs.bloodyMess * 0.05);
-
- // 2. Toughness: +5% per stack on all defense
- if (opp.activeBuffs.toughness) attackerWinChance -= (opp.activeBuffs.toughness * 0.05);
-
- // 3. Ghoul Slayer: Massive +15% against Wasteland Horrors
- if (player.activeBuffs.ghoulSlayer && opp.name === "Wasteland Horrors") attackerWinChance += 0.15;
-
- // 4. Commando: Pre-emptive strike (Attacking with 10+ kills 1 defender instantly)
- if (player.activeBuffs.commando && attackerArmy >= 10) {
- opponent.army -= 1;
- this.logAction(`[ COMMANDO ] Pre-emptive strike eliminated 1 defender before the roll.`);
- }
-
- // 5. Rooted: +10% defense if territory hasn't moved troops recently
- // (Checks if the territory was 'locked down' or just not touched)
- if (opp.activeBuffs.rooted && !opponent.wasManeuveredThisTurn) {
- attackerWinChance -= 0.10;
- }
- }
@@ -12252,13 +14124,15 @@ if (opp && !opp.isNeutral) this.addXP(opp, 4);
}
if (opp.perk?.id === 'synth_replacements' && defenderLosses > 0) {
let synths = 0;
- for (let k = 0; k < defenderLosses; k++) { if (Math.random() < 0.10) synths++; }
+ // FIX: Corrected typo from 0.10 to 0.15
+ for (let k = 0; k < defenderLosses; k++) { if (Math.random() < 0.15) synths++; }
if (synths > 0) {
opp.reserve += synths;
await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed defending Synths to reserves!`);
}
}
// --- END SYNTH LOGIC ---
+
}
return;
}
@@ -12295,7 +14169,21 @@ this.addXP(player, xpReward);
opponent.explorePOI = null;
}
- flavor = (Math.random() < 0.10) ? (" " + combatFlavors[Math.floor(Math.random() * combatFlavors.length)]) : "!";
+ if (Math.random() < 0.15) { // 15% chance to see a flavor text
+ let flavorArray = combatFlavors; // Default to the standard list
+ if (player.isPlayer && player.dogmeatStatus && Math.random() < 0.40) { // 40% chance for a dog-related flavor
+ if (player.dogmeatStatus === 'healthy' && typeof dogmeatCombatFlavors !== 'undefined') {
+ flavorArray = dogmeatCombatFlavors;
+ } else if (player.dogmeatStatus === 'injured' && typeof injuredDogmeatCombatFlavors !== 'undefined') {
+ flavorArray = injuredDogmeatCombatFlavors;
+ }
+ }
+ flavor = " " + flavorArray[Math.floor(Math.random() * flavorArray.length)];
+
+ } else {
+ flavor = "!";
+ }
+
this.players.forEach(p => {
if (p.name === opponent.owner) {
@@ -12331,11 +14219,19 @@ this.addXP(player, xpReward);
// --- NEW ENCOUNTER TRIGGER IS NOW HERE (After troops have arrived!) ---
if (player.isPlayer) {
- await this.triggerEncounterCheck('post_conquest', opponent.name);
+ if (this.dogmeatQuest && this.dogmeatQuest.active && this.dogmeatQuest.target === opponent.name) {
+ this.dogmeatQuest.active = false;
+ this.dogmeatQuest.siege = 3; // Start the 3-day siege timer!
+ await this.resolveDogmeatSiege('start', opponent);
+ } else {
+ await this.triggerEncounterCheck('post_conquest', opponent.name);
+ }
}
+
if (this.perksEnabled && player.perk) {
+
if (player.perk.id === 'fev_infection') {
// (Bonus Fix: Made the victory FEV math match the repulse FEV math!)
const converted = Math.max(1, Math.floor(originalDefenderArmy * 0.25));
@@ -12389,9 +14285,10 @@ this.addXP(player, xpReward);
}
- if (this.activeNuke && this.activeNuke.launcher === originalOwner) {
+ if (this.activeNukes) {
// Check if the specific origin silo was just captured
- if (this.activeNuke.launchSilo === opponent.name) {
+ let abortedNukeIndex = this.activeNukes.findIndex(n => n.launcher === originalOwner && n.launchSilo === opponent.name);
+ if (abortedNukeIndex > -1) {
await this.logAction(`[ CRITICAL ] ${originalOwner} lost control of the launch silo at ${formatTerritoryName(opponent.name)}! Launch sequence aborted.`, true);
this.modalIsOpen = true;
@@ -12402,17 +14299,22 @@ this.addXP(player, xpReward);
);
this.modalIsOpen = false;
- this.activeNuke = null;
+ this.activeNukes.splice(abortedNukeIndex, 1);
}
}
+
// ==========================================
// 1. HUMAN PLAYER LOOTING (You naturally find items)
// ==========================================
if (player.isPlayer) {
// FIX: Must be active AND owned by the human player
let luckItem = this.bobbleheads && this.bobbleheads.find(i => i.key === 'l' && i.active && i.owner === this.player.name);
- const luckModifier = luckItem ? 0.15 : 0;
+ let luckModifier = luckItem ? 0.15 : 0;
+ if (this.levelingEnabled && player.activeBuffs && player.activeBuffs.scavenger) {
+ luckModifier += (0.10 * player.activeBuffs.scavenger);
+ }
+
// ---> CHANGED: Dynamic Battle Cap Rewards (Multiplied by Luck)
@@ -12456,7 +14358,11 @@ this.addXP(player, xpReward);
else if (!player.isPlayer) {
// FIX: Must be active AND owned by the AI attacker
let luckItem = this.bobbleheads && this.bobbleheads.find(i => i.key === 'l' && i.active && i.owner === player.name);
- const luckModifier = luckItem ? 0.15 : 0;
+ let luckModifier = luckItem ? 0.15 : 0;
+ if (this.levelingEnabled && player.activeBuffs && player.activeBuffs.scavenger) {
+ luckModifier += (0.10 * player.activeBuffs.scavenger);
+ }
+
let roll = Math.random();
if (this.bobbleheads && roll < (0.05 + luckModifier)) {
@@ -12545,7 +14451,8 @@ this.addXP(player, xpReward);
if (player.perk?.id === 'synth_replacements' && attackerLosses > 0) {
let synths = 0;
- for (let k = 0; k < attackerLosses; k++) { if (Math.random() < 0.10) synths++; }
+ // FIX: Corrected typo from 0.10 to 0.15
+ for (let k = 0; k < attackerLosses; k++) { if (Math.random() < 0.15) synths++; }
if (synths > 0) {
player.reserve += synths;
await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed attacking Synths to reserves!`);
@@ -12553,7 +14460,8 @@ this.addXP(player, xpReward);
}
if (opp.perk?.id === 'synth_replacements' && defenderLosses > 0) {
let synths = 0;
- for (let k = 0; k < defenderLosses; k++) { if (Math.random() < 0.10) synths++; }
+ // FIX: Corrected typo from 0.10 to 0.15
+ for (let k = 0; k < defenderLosses; k++) { if (Math.random() < 0.15) synths++; }
if (synths > 0) {
opp.reserve += synths;
await this.logAction(`MEMORY REPLACEMENT: The Institute recovered ${synths} destroyed defending Synths to reserves!`);
@@ -12562,6 +14470,7 @@ this.addXP(player, xpReward);
}
// --- END SYNTH LOGIC ---
+
if (isVictory) {
// --- NEW: Better logging for occupying empty land ---
@@ -12587,16 +14496,13 @@ this.addXP(player, xpReward);
return a === opponent.name;
});
if (matchedCountry) {
- // --- FIX: Dynamically build the popup text based on Perks! ---
let bonusText = `+${continent.bonus} TROOPS`;
if (this.perksEnabled && player.perk) {
if (player.perk.id === 'logistical_superiority') {
- // NCR gets +50% troops
let extraTroops = Math.ceil(continent.bonus * 0.5);
bonusText = `+${continent.bonus + extraTroops} TROOPS`;
} else if (player.perk.id === 'tribute_chest') {
- // Nuka-World gets +5 Caps (or +1 Card if classic mode)
if (this.wastelandEconomyActive) {
bonusText += ` & +5 CAPS`;
} else {
@@ -12605,10 +14511,20 @@ this.addXP(player, xpReward);
}
}
- if (this.queueToast) {
+ // --- FIX: Respect Turbo Mode and Fog of War for AI Captures ---
+ let turbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked;
+ let isVis = this.isTerritoryVisible ? this.isTerritoryVisible(opponent.name) : true;
+
+ let shouldShowAlert = false;
+ if (player.isPlayer) {
+ shouldShowAlert = true; // Always show for human player
+ } else if (!turbo && isVis) {
+ shouldShowAlert = true; // Show for AI only if visible and not fast-forwarding
+ }
+
+ if (shouldShowAlert && this.queueToast) {
this.queueToast(`>>> STRATEGIC ASSET SECURED <<<
${player.name.toUpperCase()} NOW CONTROLS ${continent.name.toUpperCase()} (${bonusText})`, player.color, false);
}
- // --- END OF FIX ---
}
}
})
@@ -12972,8 +14888,11 @@ this.addXP(player, xpReward);
let isFighting = true;
if (decision === 'avoid') {
- // 80% chance to successfully hide
- if (Math.random() < 0.80) {
+ // --- NEW: DOGMEAT STEALTH PENALTY ---
+ let hideChance = (this.player.activeBuffs && this.player.activeBuffs.dogmeat) ? 0.0 : 0.80; // 0% chance if Dogmeat barks!
+
+ if (Math.random() < hideChance) {
+
isFighting = false;
this.logAction(`[ SURVIVAL ] Garrison at ${tName} successfully stayed hidden from a roaming ${creature.name}.`);
return `Your garrison remained hidden until the ${creature.name} moved on.`;
@@ -13144,9 +15063,18 @@ this.addXP(player, xpReward);
}
+ // --- NEW: DOGMEAT LOOT & SURVIVAL BUFFS ---
+ let hasDogmeat = (this.player.dogmeatStatus === 'healthy');
+ let lootThreshold = hasDogmeat ? 0.45 : 0.30; // 45% chance for Caps
+
+ let stimThreshold = hasDogmeat ? 0.70 : 0.50; // 25% chance for Stimpak
+ let ambushThreshold = hasDogmeat ? 0.85 : 0.75; // 15% chance for Trap
+
// 30% Chance for High Value Loot (Caps or Units)
- if (roll < 0.30) {
+ if (roll < lootThreshold) {
let capsFound = Math.floor(Math.random() * 15) + 10;
+ if (hasDogmeat) capsFound += Math.floor(Math.random() * 10) + 5; // Bonus Caps
+
if (this.wastelandEconomyActive) this.player.caps += capsFound;
message += `They hit the jackpot! Your troops recovered ${capsFound} Caps from a pre-war stash.`;
logMsg = `[ EXPEDITION ] Success at ${formatTerritoryName(territory.name)}. Found ${capsFound} Caps!`;
@@ -13160,8 +15088,18 @@ this.addXP(player, xpReward);
logMsg = `[ EXPEDITION ] Success at ${formatTerritoryName(territory.name)}. Recovered a Stimpak!`;
}
// 25% Chance for an Ambush / Trap (Loss of units)
- else if (roll < 0.75) {
+ else if (roll < ambushThreshold) {
let casualties = Math.floor(territory.army * 0.20) + 1; // 20% casualties
+
+ // --- NEW: DOGMEAT SAVES THE DAY ---
+ if (hasDogmeat && Math.random() < 0.50) {
+ message += `Dogmeat started growling at a tripwire just in time. The detachment avoided the ambush entirely!`;
+ logMsg = `[ EXPEDITION ] Dogmeat sniffed out a deadly trap at ${formatTerritoryName(territory.name)}. Zero casualties.`;
+ casualties = 0;
+ }
+
+ if (territory.army - casualties < 1) casualties = territory.army - 1; // Never wipe out
+
if (territory.army - casualties < 1) casualties = territory.army - 1; // Never wipe out
if (casualties > 0) {
@@ -13193,9 +15131,146 @@ this.addXP(player, xpReward);
this.drawMapText();
};
+ // --- NEW: DOGMEAT QUEST EVENT ---
+ Gamestate.resolveDogmeatQuest = async function (territory) {
+ this.modalIsOpen = true;
+
+ let title = "Red Rocket Station";
+ let msg = `Your troops secure the area and locate the abandoned Red Rocket. A wounded dog is cornered by a pack of Vicious Dogs!
[ TACTICAL ASSESSMENT: HOSTILE INTERVENTION REQUIRED ]`;
+
+ // Force the player to fight the mini-battle
+ await new Promise(resolve => {
+ this.showEncounterModal(title, msg, [{id: 'fight', text: "[Engage] Save the dog!"}], (id) => resolve(id));
+ });
+
+ // Calculate casualties (3 to 7)
+ let casualties = Math.floor(Math.random() * 5) + 3;
+ // Failsafe: Don't wipe the garrison entirely to prevent ownership bugs
+ if (casualties >= territory.army) casualties = Math.max(0, territory.army - 1);
+
+ if (casualties > 0) {
+ territory.army -= casualties;
+ this.player.army -= casualties;
+ }
+
+ let postFightMsg = `You fought off the pack, but suffered ${casualties} casualties.
The dog is safe, but bleeding out. You can save him, but it will require medical supplies.`;
+
+ let hasStimpak = this.commandersEnabled && this.player.commander && this.player.commander.stimpaks > 0;
+ let hasCaps = this.wastelandEconomyActive && this.player.caps >= 15;
+
+ let adoptChoices = [];
+ if (hasStimpak) adoptChoices.push({id: 'stimpak', text: "[Adopt] Use 1 Stimpak to heal him."});
+ if (hasCaps) adoptChoices.push({id: 'caps', text: "[Adopt] Spend 15 Caps on medical supplies."});
+ adoptChoices.push({id: 'leave', text: "[Walk Away] We can't spare the resources."});
+
+ if (!hasStimpak && !hasCaps) {
+ postFightMsg += `
You lack the Stimpaks or Caps to save him...`;
+ }
+
+ let adoptChoice = await new Promise(resolve => {
+ this.showEncounterModal("A Boy and His Dog", postFightMsg, adoptChoices, (id) => resolve(id));
+ });
+
+ if (!this.player.activeBuffs) this.player.activeBuffs = {};
+
+ if (adoptChoice === 'stimpak') {
+ this.player.commander.stimpaks--;
+ this.player.activeBuffs.dogmeat = true;
+ this.player.dogmeatStatus = 'healthy';
+ this.logAction(`[ COMPANION ] You used a Stimpak to save CX404. Dogmeat has joined your faction!`, true);
+ if (this.queueToast) this.queueToast(`>>> COMPANION ACQUIRED <<< 🐾 DOGMEAT HAS JOINED YOU`, "var(--pip-color)", true);
+ } else if (adoptChoice === 'caps') {
+ this.player.caps -= 15;
+ this.player.activeBuffs.dogmeat = true;
+ this.player.dogmeatStatus = 'healthy';
+ this.logAction(`[ COMPANION ] You spent 15 Caps to save CX404. Dogmeat has joined your faction!`, true);
+ if (this.queueToast) this.queueToast(`>>> COMPANION ACQUIRED <<< 🐾 DOGMEAT HAS JOINED YOU`, "var(--pip-color)", true);
+ }
+ else {
+ this.dogmeatQuest.resolved = false; // Allow the quest to spawn again
+ this.dogmeatQuest.cooldown = 4; // Put it on a 4-turn cooldown
+ this.logAction(`[ TRAGEDY ] You were forced to leave the wounded dog behind. Hopefully someone else finds him...`, true);
+ }
+
+ this.modalIsOpen = false;
+ this.updateInfo();
+ if (this.renderInventory) this.renderInventory();
+ this.drawMapText();
+ };
+
+ // --- NEW: 3-DAY DOGMEAT SIEGE EVENT ---
+ Gamestate.resolveDogmeatSiege = async function(phase, territory) {
+ this.modalIsOpen = true;
+
+ if (phase === 'start') {
+ let msg = `Your troops secured the territory, but a heavily armed gang has barricaded themselves inside the Red Rocket with the dog.
You must hold this territory and lay siege for 3 DAYS to breach the heavy doors!`;
+ await new Promise(resolve => this.showEncounterModal("RED ROCKET SIEGE: DAY 1", msg, [{id:'ok', text:"[Hold the Line] We're not leaving."}], () => resolve(null)));
+ this.logAction(`[ SIEGE ] Red Rocket surrounded at ${formatTerritoryName(territory.name)}. 3 Days remaining.`, true);
+ }
+ else if (phase === 'defend') {
+ let casualties = Math.floor(Math.random() * 3) + 1;
+ if (territory.army - casualties < 1) casualties = territory.army - 1;
+ territory.army -= casualties;
+ this.player.army -= casualties;
+
+ let daysLeft = this.dogmeatQuest.siege;
+ let msg = `The scavengers inside the Red Rocket launched a desperate counter-attack! You suffered ${casualties} casualties repelling them.
${daysLeft} DAYS until the doors give way.`;
+ await new Promise(resolve => this.showEncounterModal(`RED ROCKET SIEGE: DAY ${4 - daysLeft}`, msg, [{id:'ok', text:"[Return Fire] Keep the pressure on!"}], () => resolve(null)));
+ this.logAction(`[ SIEGE ] Counter-attack at Red Rocket repelled. Lost ${casualties} troops. ${daysLeft} Days remaining.`, true);
+ }
+ else if (phase === 'finish') {
+ let msg = `You've breached the doors and wiped out the scavengers! The dog is safe, but he's gravely injured.
You can take him with you, but he'll slow your army down and attract trouble until his wounds are properly treated.`;
+
+ let hasStimpak = this.commandersEnabled && this.player.commander && this.player.commander.stimpaks > 0;
+ let hasCaps = this.wastelandEconomyActive && this.player.caps >= 50;
+
+ let adoptChoices = [];
+ if (hasStimpak) adoptChoices.push({id: 'heal_stimpak', text: "[Heal Him Now] Use 1 Stimpak."});
+ if (hasCaps) adoptChoices.push({id: 'heal_caps', text: "[Heal Him Now] Spend 50 Caps."});
+ adoptChoices.push({id: 'injured', text: "[Take Him With You (Injured)]", color: "#ffcc00"});
+ adoptChoices.push({id: 'leave', text: "[Walk Away] He's better off on his own."});
+
+ let adoptChoice = await new Promise(resolve => this.showEncounterModal("A Boy and His Dog", msg, adoptChoices, (id) => resolve(id)));
+
+ if (!this.player.activeBuffs) this.player.activeBuffs = {};
+
+ if (adoptChoice === 'heal_stimpak') {
+ this.player.commander.stimpaks--;
+ this.player.activeBuffs.dogmeat = true;
+ this.player.dogmeatStatus = 'healthy';
+ this.logAction(`[ COMPANION ] You used a Stimpak to save CX404. He's ready for action!`, true);
+ if (this.queueToast) this.queueToast(`>>> COMPANION ACQUIRED <<<
DOGMEAT HAS JOINED YOU`, "var(--pip-color)", true);
+ } else if (adoptChoice === 'heal_caps') {
+ this.player.caps -= 50;
+ this.player.activeBuffs.dogmeat = true;
+ this.player.dogmeatStatus = 'healthy';
+ this.logAction(`[ COMPANION ] You spent 50 Caps to save CX404. He's ready for action!`, true);
+ if (this.queueToast) this.queueToast(`>>> COMPANION ACQUIRED <<<
DOGMEAT HAS JOINED YOU`, "var(--pip-color)", true);
+ } else if (adoptChoice === 'injured') {
+ this.player.activeBuffs.dogmeat = true;
+ this.player.dogmeatStatus = 'injured';
+ this.logAction(`[ COMPANION ] You rescued Dogmeat, but he's injured. He will slow you down until treated.`, true);
+
+ if (this.queueToast) this.queueToast(`>>> COMPANION ACQUIRED <<<
DOGMEAT IS INJURED`, "#ffcc00", true);
+ } else { // Player chose to leave
+ this.dogmeatQuest.resolved = false; // Allow quest to spawn again
+ this.dogmeatQuest.cooldown = 4;
+ this.logAction(`[ TRAGEDY ] You were forced to leave the wounded dog behind.`, true);
+ }
+ }
+
+
+ this.modalIsOpen = false;
+ this.updateInfo();
+ if (this.renderInventory) this.renderInventory();
+ this.drawMapText();
+ };
+
+
Gamestate.resolveBattleEncounter = async function (territoryName) {
// Force unlock the modal state (in case the Move Troops slider confused it)
this.modalIsOpen = false;
+
const territory = this.countries.find(c => c.name === territoryName);
if (!territory || territory.army < 1) return; // FIX: Even 1 troop can discover a vault after a battle
@@ -13295,17 +15370,22 @@ Gamestate.triggerEncounterCheck = async function (triggerType, territoryName = n
}
// STANDARD RATE
else {
- chance = 0.08; // 8% chance per turn
+ chance = 0.15; // 15% chance per turn
+ // --- NEW: INJURED DOGMEAT ATTRACTS CREATURES ---
+ if (this.player.dogmeatStatus === 'injured') {
+ chance *= 2; // Double the encounter chance!
+ }
}
} else if (triggerType === 'post_conquest') {
// If on cooldown, no post-battle discoveries allowed
if (this.encounterCooldown > 0) {
chance = 0;
} else {
- chance = 0.15; // 15% chance per conquest
+ chance = 0.25; // 25% chance per conquest
}
}
+
// Roll the dice
if (Math.random() < chance) {
if (triggerType === 'start_of_turn') {
@@ -13434,23 +15514,31 @@ Gamestate.triggerEncounterCheck = async function (triggerType, territoryName = n
// ==========================================
Gamestate.saveGame = async function () {
try {
- // 1. Gather all critical game variables into one giant object
const saveData = {
turn: this.turn,
stage: this.stage,
difficulty: this.difficulty,
-
+ uiTheme: document.getElementById('chosen-theme')?.value || 'fo3', // ADDED: UI Theme
+
// Game Modes & Rules
wastelandEconomyActive: this.wastelandEconomyActive,
perksEnabled: this.perksEnabled,
nukesEnabled: this.nukesEnabled,
commandersEnabled: this.commandersEnabled,
+ encountersEnabled: this.encountersEnabled,
+ flatTrade: this.flatTrade,
+ isAllianceMode: this.isAllianceMode,
+ placementMode: this.placementMode,
+ setupPlayerIndex: this.setupPlayerIndex,
+
// Global Trackers
globalCodes: this.globalCodes,
- activeNuke: this.activeNuke,
+ activeNukes: this.activeNukes,
radstorm: this.radstorm,
diplomacy: this.diplomacy,
+ dogmeatQuest: this.dogmeatQuest,
+ activeRelicPool: this.activeRelicPool,
// The Main Entities
players: this.players,
@@ -13458,138 +15546,113 @@ Gamestate.triggerEncounterCheck = async function (triggerType, territoryName = n
bobbleheads: this.bobbleheads
};
- // 2. Convert that object into a JSON string
const jsonString = JSON.stringify(saveData);
- // 3. --- NEW: Create a downloadable file (Holotape) ---
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `WastelandConquest_Day${this.turn}_Save.json`; // Names the file automatically!
+ a.download = `WastelandConquest_Day${this.turn}_Save.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
- // --- END OF DOWNLOAD LOGIC ---
-
- // 4. Provide epic visual feedback
-
+
if (this.queueToast) {
this.queueToast(`>>> SYSTEM BACKUP COMPLETE <<<
Game progress saved to local memory.`, "var(--pip-color)", true);
- } else if (this.showToast) {
- this.showToast("Game Saved Successfully.", "green");
}
- // 5. Flash the button to confirm the click registered
- let saveBtn = document.getElementById('btn-save-game');
- if (saveBtn) {
- let oldColor = saveBtn.style.color;
- saveBtn.style.backgroundColor = "var(--pip-color)";
- saveBtn.style.color = "var(--pip-dark)";
- setTimeout(() => {
- saveBtn.style.backgroundColor = ""; // Reset to CSS default
- saveBtn.style.color = oldColor;
- }, 500);
- }
-
- // Log it for good measure
if (this.logAction) this.logAction("[ BACKUP ] SYSTEM: Game state successfully backed up to local memory.", true);
-
} catch (error) {
console.error("Failed to save game:", error);
if (this.showToast) this.showToast("CRITICAL ERROR: Failed to write to local memory.", "red");
}
};
-
// ==========================================
// --- LOAD GAME SYSTEM ---
// ==========================================
// --- FIX: Tell the function to accept the 'jsonString' we pass from the file button ---
Gamestate.loadGame = async function (jsonString) {
try {
- // --- NEW: REQUEST FULL SCREEN ON LOAD (MOBILE ONLY) ---
+ // Request fullscreen on mobile
if (window.innerWidth <= 950) {
- let elem = document.documentElement;
- if (elem.requestFullscreen) { elem.requestFullscreen().catch(e => console.log(e)); }
- else if (elem.webkitRequestFullscreen) { elem.webkitRequestFullscreen().catch(e => console.log(e)); }
+ document.documentElement.requestFullscreen().catch(e => console.log(e));
}
- // 1. Check if we actually received file data
-
if (!jsonString) {
if (this.showToast) this.showToast("Error: No save data found in file.", "red");
return false;
}
-
- // 2. Parse the string back into a JavaScript object
+
const loadedData = JSON.parse(jsonString);
- // 3. Overwrite all current game variables with the loaded data
+ // --- NEW: Restore UI Theme FIRST ---
+ if (loadedData.uiTheme) {
+ const themeDropdown = document.getElementById('chosen-theme');
+ if (themeDropdown) {
+ themeDropdown.value = loadedData.uiTheme;
+ }
+ if (this.applyUITheme) {
+ this.applyUITheme();
+ }
+ }
+
+ // Restore all game state variables
this.turn = loadedData.turn;
this.stage = loadedData.stage;
this.difficulty = loadedData.difficulty;
-
this.wastelandEconomyActive = loadedData.wastelandEconomyActive;
this.perksEnabled = loadedData.perksEnabled;
this.nukesEnabled = loadedData.nukesEnabled;
this.commandersEnabled = loadedData.commandersEnabled;
+ this.encountersEnabled = loadedData.encountersEnabled;
+
+ // --- FIX: Restore the new global modes and setup progress ---
+ this.flatTrade = loadedData.flatTrade || false;
+ this.isAllianceMode = loadedData.isAllianceMode || false;
+ this.placementMode = loadedData.placementMode || 'auto';
+ this.setupPlayerIndex = loadedData.setupPlayerIndex || 0;
this.globalCodes = loadedData.globalCodes;
- this.activeNuke = loadedData.activeNuke;
+ this.activeNukes = loadedData.activeNukes || (loadedData.activeNuke ? [loadedData.activeNuke] : []);
this.radstorm = loadedData.radstorm;
this.diplomacy = loadedData.diplomacy;
-
+ this.dogmeatQuest = loadedData.dogmeatQuest;
+ this.activeRelicPool = loadedData.activeRelicPool;
this.players = loadedData.players;
this.countries = loadedData.countries;
this.bobbleheads = loadedData.bobbleheads;
- // 4. Determine who "The Player" is (the human) and point 'this.player' back to them
+ // Re-establish player reference
this.player = this.players.find(p => p.isPlayer);
- // 5. Hide the start modal and show the game UI
- let startModal = document.getElementById('start-modal');
- if (startModal) startModal.style.display = 'none';
+ // Hide start modal and redraw UI
+ document.getElementById('start-modal').style.display = 'none';
- // 6. Completely redraw the map to match the loaded ownership and armies
this.countries.forEach(country => {
let areaOnMap = document.getElementById(country.name);
if (areaOnMap) {
areaOnMap.style.fill = country.color;
- if (country.isCrater) {
- areaOnMap.classList.add('crater');
- } else {
- areaOnMap.classList.remove('crater');
- }
+ areaOnMap.classList.toggle('crater', !!country.isCrater);
}
});
- // 7. Redraw the text numbers on the map
this.drawMapText();
-
- // 8. Re-initialize the UI to match the loaded stage
this.updateButtonText();
this.updateInfo();
+ this.updateXPBar();
+ if (this.renderInventory) this.renderInventory();
- // --- NEW: Refresh Leveling UI on Startup ---
- this.updateXPBar();
-
-
- // 9. Special rule for loading into the Recruitment stage
+ // Handle recruitment modal on load
if (this.wastelandEconomyActive && this.stage === 'Recruitment' && this.player.reserve === 0) {
const troopCost = 5;
- if (this.player.caps >= troopCost) {
- this.showRecruitmentModal();
- }
+ if (this.player.caps >= troopCost) this.showRecruitmentModal();
}
- // 10. Announce success!
if (this.logAction) this.logAction(`[ RESTORE ] SYSTEM: Successfully restored Save File (Day ${this.turn}).`, true);
- if (this.queueToast) {
- this.queueToast(`>>> SYSTEM RESTORE COMPLETE <<<
Welcome back, ${this.player.name}.`, "var(--pip-color)", true);
- }
+ if (this.queueToast) this.queueToast(`>>> SYSTEM RESTORE COMPLETE <<<
Welcome back, ${this.player.name}.`, "var(--pip-color)", true);
return true;
@@ -13600,8 +15663,6 @@ Gamestate.triggerEncounterCheck = async function (triggerType, territoryName = n
}
};
-
-
Gamestate.updateInfo = function () {
Gamestate.originalUpdateInfo.call(this);
// Run the normal game logic first
@@ -13660,10 +15721,16 @@ Gamestate.updateInfo = function () {
this.stage === 'Commander Phase';
// Condition 2: A Bobblehead is IMMEDIATELY usable right now.
- const isBobbleheadReady = this.bobbleheads &&
- this.bobbleheads.some(b => b.owner === this.player.name && b.cooldown <= 0);
+ const isBobbleheadReady = this.bobbleheads && this.bobbleheads.some(b => {
+ if (b.owner !== this.player.name || b.cooldown > 0) return false;
+ if (b.key === 'c' && this.stage !== "Recruitment" && this.stage !== "Fortify") return false;
+ if (b.key === 'a' && this.stage !== "Maneuver" && this.stage !== "Commander Phase") return false;
+ if (['s', 'e', 'p', 'i', 'l'].includes(b.key) && this.stage !== "Battle" && this.stage !== "Commander Phase") return false;
+ return true;
+ });
if (isStimpakReady || isBobbleheadReady) {
+
shouldFlash = true;
}
@@ -13707,29 +15774,30 @@ Gamestate.updateInfo = function () {
// The content for all the manual pages
const helpPages = {
- "page-root": () => {
- let adminLink = "";
- if (Gamestate.player && Gamestate.player.name.toUpperCase() === "OVERSEER") {
- adminLink = `> TERMINAL OVERRIDE (Access hidden developer tools to manipulate the simulation) `;
- }
- return `Welcome to the Wasteland Conquest Interface
` +
`> CURRENT MISSION DATA (Check active game settings, faction, and diplomacy) ` +
- `> HOW TO PLAY (Learn basics of claiming land, resources, and combat) ` +
- `> V.A.T.S. TARGETING (Understand how difficulty changes combat odds) ` +
- `> FACTIONS & PERKS (Discover unique abilities and advantages per faction) ` +
+ `> HOW TO PLAY (Learn basics of claiming land, resources, and combat) ` +
+ `> ITEM DATABASE (Stimpaks, Bobbleheads, Relics) ` +
+ `> V.A.T.S. TARGETING (Understand how difficulty changes combat odds) ` +
+ `> FACTIONS & PERKS (Discover unique abilities and advantages per faction) ` +
`> LEVELING & PROGRESSION (Earn XP and unlock powerful perks) ` +
- `> MAKING FRIENDS (DIPLOMACY) (How to form temporary truces or backstab rivals) ` +
- `> STORMS & GHOULS (Learn to survive hazards and fend off neutral hordes) ` +
- `> WASTELAND ENCOUNTERS (Learn about random events, ambushes, and hidden loot) ` +
- `> GAME PRESETS (Read about match types from Conquest to Survival Mode) ` +
- `> ADVANCED RULES (Learn complex features like Commanders, Fog, and Nukes) ` +
- `> MAP LEGEND (A visual guide explaining map colors, borders, and icons) ` +
- `> HOLOTAPE ARCHIVE (A look at the hit Fallout games that inspired this sim) ` +
- `> SAVE & LOAD (Protect progress by learning to backup and restore data) ` +
- `> SYSTEM CREDITS (See the latest system updates and developer information)
` +
- adminLink +
- `> LOG OFF THE GUIDE`;
- },
+ `> MAKING FRIENDS (DIPLOMACY) (How to form temporary truces or backstab rivals) ` +
+ `> STORMS & GHOULS (Learn to survive hazards and fend off neutral hordes) ` +
+ `> WASTELAND ENCOUNTERS (Learn about random events, ambushes, and hidden loot) ` +
+ `> GAME PRESETS (Read about match types from Conquest to Survival Mode) ` +
+ `> ADVANCED RULES (Learn complex features like Commanders, Fog, and Nukes) ` +
+ `> MAP LEGEND (A visual guide explaining map colors, borders, and icons) ` +
+ `> HOLOTAPE ARCHIVE (A look at the hit Fallout games that inspired this sim) ` +
+ `> SAVE & LOAD (Protect progress by learning to backup and restore data) ` +
+ `> SYSTEM CREDITS (See the latest system updates and developer information)
` +
+ adminLink +
+ `> LOG OFF THE GUIDE`;
+},
"page-status": () => {
// --- DYNAMIC PRESET MATCHER ---
let getMatchedPreset = () => {
@@ -13766,6 +15834,7 @@ Gamestate.updateInfo = function () {
const img3 = "data:image/png;base64,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"; // Injured (HP 40- / Strength 20%-)
let activeImg = img1;
+
if (Gamestate.commandersEnabled && p.commander) {
const hp = p.commander.hp;
if (hp <= 30) activeImg = img3;
@@ -13937,7 +16006,9 @@ html += `
const fogOn = document.getElementById('opt-fog-of-war') && document.getElementById('opt-fog-of-war').checked;
let n = 1;
let h = `> HOW TO PLAY: ${preset} MODE
Your goal is to eliminate all rival factions and control the map. Turn phases in order:
`;
+ h += `0. INITIAL DEPLOYMENT (Setup) Depending on your Boot Menu settings, you may begin with a manual drafting phase to claim territories and place your starting troops before the war officially begins.
`;
h += `${n++}. RECRUITMENT PHASE `;
+
if (econOn) {
h += `Spend Caps to hire troops via [RECRUITMENT]. Cost: 5 Caps per troop. * Cap income scales with territories and continents you control. * Deploy ALL reserves to your territories before advancing.
The Vault-Tec Assisted Targeting System processes your combat probabilities.
When engaging hostiles, 'Simulation Difficulty' dictates the base success rate of your initial strike: * EASY: 60% Hit Chance (Tactical Advantage) * NORMAL: 50% Hit Chance (Standard Parity) * HARD: 40% Hit Chance (Lethal Resistance)
Hover over an enemy territory during the Battle Phase to view the real-time V.A.T.S. attack vs. defend odds breakdown.",
-"page-vats": "> V.A.T.S. TARGETING (Combat Odds)
Wondering if you will win a fight before you start it? That is what V.A.T.S. is for.
1. HOW COMBAT WORKS: When you attack, the computer doesn't just subtract troops. It rolls a virtual \"Hit Chance\" for every soldier involved. The more troops you bring to a fight, the more chances you have to score a hit and destroy the enemy.
2. SIMULATION DIFFICULTY: Your base \"Hit Chance\" is permanently locked based on the difficulty you picked at the main menu: * EASY: 60% Hit Chance. You have a massive tactical advantage. * NORMAL: 50% Hit Chance. A completely fair, 50/50 fight. * HARD: 40% Hit Chance. Enemies are dug in and harder to kill.
3. REAL-TIME PREDICTIONS (Hovering): During your Attack phase, simply hover your mouse over an enemy territory that is next to your borders. The V.A.T.S. display will pop up on your screen and give you the exact mathematical probability of you winning that specific battle.
TIP: Always check V.A.T.S. before attacking! Enemy Faction Perks (like Brotherhood of Steel Power Armor) can secretly lower your win chances, making a seemingly easy fight very deadly.",
-"page-factions": "> FACTIONS & PERKS (Part 1/6)
Every faction has a unique advantage. (Note: Only active if 'Faction Perks' are enabled).
BROTHERHOOD OF STEEL [Power Armor Infantry] \"Grants a permanent +5% bonus to your win chance in all combat, both when attacking and defending.\" - EXPLANATION: Your troops are walking tanks. You get a flat boost to your V.A.T.S. hit chance in every single fight.
THE ENCLAVE [Vertibird Assault] \"During the Maneuver phase, you can move troops between any two territories you own, regardless of whether they are connected.\" - EXPLANATION: You don't need a connected land path to fortify your borders. You can fly troops straight across the map.
VAULT 87 MUTANTS [F.E.V. Infection] \"When you conquer a territory, 25% of the defeated enemy army (rounded down) is immediately converted into your own troops.\" - EXPLANATION: Every time you wipe out an enemy garrison, some of those defeated troops instantly join your army for free!
> NEXT PAGE (More Factions)",
+"page-vats": `> V.A.T.S. TARGETING
The Vault-Tec Assisted Targeting System processes your combat probabilities in real-time.
HOW TO USE: During the Battle Phase, click on one of your territories with 2+ troops, then hover your mouse over an adjacent enemy territory. The V.A.T.S. display will appear, showing your projected win chance.
TACTICAL BREAKDOWN: V.A.T.S. does more than show the odds. It provides a detailed breakdown of every factor affecting the battle, including: * Simulation Difficulty (Easy/Normal/Hard) * Active Faction Perks (yours and theirs) * Commander presence (+20% defense) * Nuclear Silo fortifications * Environmental hazards (Radstorms) * Active Bobblehead and Relic effects
PRO TIP: Always check the V.A.T.S. breakdown before committing to an attack. A 95% chance can drop to 20% if the enemy is in a fortified Silo with a Commander present.`,
- "page-factions-2": "> FACTIONS & PERKS (Part 2/6)
WASTELAND RAIDERS [Chem Frenzy] \"Sacrifice up to 50% of your attacking army for a massive combat bonus with diminishing returns. (3 Turn Cooldown)\" - EXPLANATION: You can choose to kill your own troops right before a battle to get a huge temporary boost to your hit chance.
BOS OUTCASTS [Technology Overdrive] \"Spend Caps equal to your total army size (Max 30) to grant all your attacking armies +10% win chance for 3 rounds. (3 Turn Cooldown)\" - EXPLANATION: If you have enough Caps, you can buy a massive 3-turn attack boost for your entire faction.
REILLY'S RANGERS [Ranger Network] \"Territories in a continuous block gain a cumulative +5% defensive bonus for each connected friendly territory, up to a maximum of +20%.\" - EXPLANATION: Keep your territories connected! The bigger your single \"blob\" of land, the harder it is for enemies to defeat you.
Every faction has a unique tactical advantage. (Note: Only active if 'Faction Perks' are enabled in the boot menu).
CUSTOM FACTION [Mysterious Stranger] \"When you are losing a battle, the Stranger may appear to grant a sudden +25% combat bonus.\" - EXPLANATION: Your custom player faction has a guardian angel. Sometimes, when you are projected to lose a fight, the Stranger bails you out and grants a massive hit chance boost.
Select a databank below to review specific regional factions:
> FALLOUT 3 (Capital Wasteland) > FALLOUT: NEW VEGAS (Mojave) > FALLOUT 4 (The Commonwealth)
< BACK TO MAIN MENU",
- "page-factions-3": "> FACTIONS & PERKS (Part 3/6)
NEW CALIFORNIA REPUBLIC [Logistical Superiority] \"Your vast supply lines increase the standard troop reinforcement bonus of any fully controlled Continent by 50% (rounded up).\" - EXPLANATION: If you own a whole continent, you get way more free troops at the start of your turn than anyone else would.
CAESAR'S LEGION [Scourge of the East] \"You are exempt from the rule requiring you to leave at least one troop behind after conquering a territory.\" - EXPLANATION: When you attack and win, you can move 100% of your surviving troops forward, leaving your old territory totally empty.
NEW VEGAS SECURITRONS [Predictive Simulation] \"If an attack completely fails, instantly abort the battle and restore all lost troops to both sides. (3 Turn Cooldown)\" - EXPLANATION: A literal \"Undo\" button. If an attack goes horribly wrong, use this perk to pretend it was just a simulation and get your dead troops back.
BROTHERHOOD OF STEEL [Power Armor Infantry] \"Grants a permanent +5% bonus to your win chance in all combat, both when attacking and defending.\" - EXPLANATION: Your troops are walking tanks. You get a flat boost to your V.A.T.S. hit chance in every single fight.
THE ENCLAVE [Vertibird Assault] \"During the Maneuver phase, you can move troops between any two territories you own, regardless of whether they are connected.\" - EXPLANATION: You don't need a connected land path to fortify your borders. You can fly troops straight across the map.
VAULT 87 MUTANTS [F.E.V. Infection] \"When you conquer a territory, 25% of the defeated enemy army (rounded down) is immediately converted into your own troops.\" - EXPLANATION: Every time you wipe out an enemy garrison, some of those defeated troops instantly join your army for free!
WASTELAND RAIDERS [Chem Frenzy] \"Sacrifice up to 50% of your attacking army for a massive combat bonus with diminishing returns. (3 Turn Cooldown)\" - EXPLANATION: You can choose to kill your own troops right before a battle to get a huge temporary boost to your hit chance.
BOS OUTCASTS [Technology Overdrive] \"Spend Caps equal to your total army size (Max 30) to grant all your attacking armies +10% win chance for 3 rounds. (3 Turn Cooldown)\" - EXPLANATION: If you have enough Caps, you can buy a massive 3-turn attack boost for your entire faction.
REILLY'S RANGERS [Ranger Network] \"Territories in a continuous block gain a cumulative +5% defensive bonus for each connected friendly territory, up to a maximum of +20%.\" - EXPLANATION: Keep your territories connected! The bigger your single \"blob\" of land, the harder it is for enemies to defeat you.
< BACK TO FACTIONS DIRECTORY",
- "page-factions-4": "> FACTIONS & PERKS (Part 4/6)
MOJAVE BROTHERHOOD [Elder's Edict] \"Lock down one of your territories for up to 3 turns. It cannot attack, maneuver, or be attacked. You may lift it early.\" - EXPLANATION: Turn a territory invincible. Great for protecting your Commander or stalling an enemy, but the troops inside can't do anything either.
GREAT KHANS [Guerrilla Tactics] \"During Maneuver, your troops can pass through exactly one enemy territory, inflicting 15% casualties on that territory's garrison as they pass.\" - EXPLANATION: You can move troops through an enemy line to connect your separated armies, dealing damage to the enemy as you run past them.
THE FIENDS [Chem-Fueled Raids] \"When you conquer a territory, there is a 30% chance to mug the defeated player! You may steal Caps, Stimpaks, or even Bobbleheads. If their pockets are empty, you enslave 1-2 survivors.\" - EXPLANATION: Taking land gives you a chance to randomly steal items straight out of the losing player's inventory!
> NEXT PAGE (More Factions) > PREVIOUS PAGE",
+"page-factions-fnv": "> FACTIONS & PERKS: NEW VEGAS
NEW CALIFORNIA REPUBLIC [Logistical Superiority] \"Your vast supply lines increase the standard troop reinforcement bonus of any fully controlled Continent by 50% (rounded up).\" - EXPLANATION: If you own a whole continent, you get way more free troops at the start of your turn than anyone else would.
CAESAR'S LEGION [Scourge of the East] \"You are exempt from the rule requiring you to leave at least one troop behind after conquering a territory.\" - EXPLANATION: When you attack and win, you can move 100% of your surviving troops forward, leaving your old territory totally empty.
NEW VEGAS SECURITRONS [Predictive Simulation] \"If an attack completely fails, instantly abort the battle and restore all lost troops to both sides. (3 Turn Cooldown)\" - EXPLANATION: A literal \"Undo\" button. If an attack goes horribly wrong, use this perk to pretend it was just a simulation and get your dead troops back.
MOJAVE BROTHERHOOD [Elder's Edict] \"Lock down one of your territories for up to 3 turns. It cannot attack, maneuver, or be attacked. You may lift it early.\" - EXPLANATION: Turn a territory invincible. Great for protecting your Commander or stalling an enemy, but the troops inside can't do anything either.
GREAT KHANS [Guerrilla Tactics] \"During Maneuver, your troops can pass through exactly one enemy territory, inflicting 15% casualties on that territory's garrison as they pass.\" - EXPLANATION: You can move troops through an enemy line to connect your separated armies, dealing damage to the enemy as you run past them.
THE FIENDS [Chem-Fueled Raids] \"When you conquer a territory, there is a 30% chance to mug the defeated player! You may steal Caps, Stimpaks, or even Bobbleheads. If their pockets are empty, you enslave 1-2 survivors.\" - EXPLANATION: Taking land gives you a chance to randomly steal items straight out of the losing player's inventory!
< BACK TO FACTIONS DIRECTORY",
-"page-factions-5": "> FACTIONS & PERKS (Part 5/6)
THE MINUTEMEN [Mercenary Contracts] \"Spend 20 Caps during your turn to instantly deploy 6-12 elite troops to your reserves, PLUS 1 extra troop for every 2 territories you own. (3 Turn Cooldown)\" - EXPLANATION: A powerful panic button. Spend Caps to instantly spawn a bunch of troops, scaling with how big your empire is.
THE INSTITUTE [Synth Replacements] \"Whenever you lose troops in any battle, there is a 10% chance per casualty that the Synth is recovered and instantly sent to your reserves.\" - EXPLANATION: A permanent passive buff. Whenever your troops die anywhere on the map, some of them instantly respawn back in your reserve pool.
THE RAILROAD [Rapid Relocation] \"Receive 5 maneuver points at the start of the Maneuver phase, allowing for up to five separate troop movements.\" - EXPLANATION: Normally you can only move troops once per turn. The Railroad can move troops up to 5 times, letting you perfectly balance your borders.
THE GUNNERS [Mercenary Contracts] \"Spend 20 Caps during your turn to instantly deploy 6-12 elite troops to your reserves, PLUS 1 extra troop for every 2 territories you own. (1 Turn Cooldown)\" - EXPLANATION: Very similar to the Minutemen, but with a drastically lower cooldown, letting you buy emergency troops almost every single turn.
NUKA-WORLD RAIDERS [Tribute Chest] \"At the start of your turn, you gain +10 Bottle Caps for every continent you fully control.\" - EXPLANATION: Hold entire continents to get incredibly rich, allowing you to buy more troops from the shop than anyone else.
CUSTOM FACTION [Mysterious Stranger] \"When you are losing a battle, the Stranger may appear to grant a sudden +25% combat bonus.\" - EXPLANATION: Your custom player faction has a guardian angel. Sometimes, when you are projected to lose a fight, the Stranger bails you out and grants a massive hit chance boost.
THE MINUTEMEN [Mercenary Contracts] \"Spend 20 Caps during your turn to instantly deploy 6-12 elite troops to your reserves, PLUS 1 extra troop for every 2 territories you own. (3 Turn Cooldown)\" - EXPLANATION: A powerful panic button. Spend Caps to instantly spawn a bunch of troops, scaling with how big your empire is.
THE INSTITUTE [Synth Replacements] \"Whenever you lose troops in any battle (attacking or defending), there is a 15% chance per casualty that the Synth is recovered and instantly sent to your reserves.\" - EXPLANATION: A permanent passive buff. Whenever your troops die anywhere on the map, some of them instantly respawn back in your reserve pool.
THE RAILROAD [Rapid Relocation] \"Receive 5 maneuver points at the start of the Maneuver phase, allowing for up to five separate troop movements.\" - EXPLANATION: Normally you can only move troops once per turn. The Railroad can move troops up to 5 times, letting you perfectly balance your borders.
THE GUNNERS [Mercenary Contracts] \"Spend 20 Caps during your turn to instantly deploy 6-12 elite troops to your reserves, PLUS 1 extra troop for every 2 territories you own. (1 Turn Cooldown)\" - EXPLANATION: Very similar to the Minutemen, but with a drastically lower cooldown, letting you buy emergency troops almost every single turn.
NUKA-WORLD RAIDERS [Tribute Chest] \"At the start of your turn, you gain +10 Bottle Caps for every continent you fully control.\" - EXPLANATION: Hold entire continents to get incredibly rich, allowing you to buy more troops from the shop than anyone else.
MAXSON'S BROTHERHOOD [Prydwen Deployment] \"The Prydwen automatically dispatches 3 airborne troops per turn to contested borders.\" - EXPLANATION: An aggressive variant of the Brotherhood. You receive 3 free elite troops every turn, automatically dropped onto your most vulnerable frontlines.
< BACK TO FACTIONS DIRECTORY",
"page-leveling": () => {
return `> LEVELING & PROGRESSION
RADSTORMS (Weather Hazards) Periodically, deadly radioactive storms will sweep across the map, usually lasting for several turns.
WARNINGS & DURATION: Keep an eye on your action log! The system will warn you of an incoming storm, track how many turns it will last, and report the region it is hitting.
IMPACT ZONE: A storm doesn't just hit one spot. It strikes a central territory and bleeds into the surrounding connected areas. You will see these active hazard zones glowing green on your map.
RADIATION SICKNESS: Any troops left standing in a green radiated zone at the start of the next turn will suffer massive casualties. The radiation will melt your army away.
STRATEGY: Evacuate! Use your Fortify phase to move your troops out of the green zones to a safe territory before you end your turn.
If 'Wasteland Encounters' are enabled, the simulation generates unpredictable events to test your adaptability. (Note: This system is somewhat modeled after the wasteland exploration mechanics found in Fallout Shelter).
1. CREATURE AMBUSHES: Your troops might accidentally disturb mutated wildlife.
THE THREAT: Creature strength scales wildly. Higher-level monsters have massive Threat Levels and are much deadlier than low-level pests.
THE CHOICE: You will be presented with a choice to [Attack] or [Avoid]. Hiding has a high success rate, but failing means you get ambushed!
THE OUTCOME: If you fight and your army is larger than the threat, you win and might scavenge Caps, Stimpaks, or even rescue captive troops!
If 'Wasteland Encounters' are enabled, the simulation generates unpredictable events to test your adaptability.
1. CREATURE AMBUSHES While holding territory, your garrisons may be ambushed by mutated wildlife. The system will provide a Tactical Assessment based on your garrison's size versus the creature's Threat Level.
THE CHOICE: You must choose to [Attack] or [Avoid]. Avoiding the encounter has a high chance of success, but if you fail, the creature will ambush you.
* COMPANION NOTE: Dogmeat is loyal, but not stealthy. His barking makes it impossible to hide from creatures, forcing an engagement.
THE OUTCOME: If your garrison's army size is greater than or equal to the creature's Threat Level, you will win the fight with no casualties and may scavenge Caps, Stimpaks, or even find rare Relics.
2. RADIO TRANSMISSIONS: While holding territory, your garrisons may pick up strange radio signals pointing to a person, container, or location of interest.
THE CHOICE: You must choose whether to [Investigate] the signal or [Ignore] it.
THE RISK (LOCKDOWN): Investigating puts the territory into \"Lockdown\" for 2 turns. It cannot attack, move troops, or defend itself efficiently while \"SEARCHING...\".
THE REWARD: If you brave the wastes, your troops might recruit wandering survivors, uncover hidden caches of Caps, or find rare items. But beware—some signals are just Raider traps!
3. POST-BATTLE DISCOVERIES: When clearing out enemy resistance and conquering a new territory, your troops might uncover an intact Pre-War Point of Interest (POI), or even a sealed Vault.
THE CHOICE: Just like radio signals, you can choose to [Investigate] the ruins (Locking down for 2 turns) or [Ignore] them.
THE REWARD: Exploring POIs and signals is the primary way to find rare loot! You might find extra Bottle Caps, Stimpaks, or permanent game-changing Bobbleheads. But beware... your choices might also trigger a deadly booby trap!
3. POST-BATTLE DISCOVERIES After conquering a territory, your troops may discover a sealed Vault or other Point of Interest. Investigating them locks the territory down for 2 turns but can yield high-value loot.
SPECIAL EVENT: A BOY AND HIS DOG A rare distress signal may be intercepted, revealing the location of a lone dog defending a Red Rocket station. This will mark a territory on your map with a Paw Icon ().
THE MISSION: You have a limited number of turns to attack and conquer the marked territory. If you succeed, you will trigger a multi-day siege to rescue the dog, CX404.
THE REWARD: If you rescue and heal him, Dogmeat will join your faction as a permanent companion, granting powerful buffs to combat, loot-finding, and mine-defusing.
> PREVIOUS PAGE`,
"page-presets": "> GAME PRESETS (Database Index)
The Overseer can load different simulation presets from the boot menu. Select a databank below to review its specific operational parameters:
> [01] CLASSIC CONQUEST > [02] WASTELAND SURVIVAL > [03] HEROES OF THE WASTELAND > [04] APOCALYPSE NOW > [05] ALLIANCE WARFARE > [06] COVERT WARFARE > [07] NUCLEAR OPTION > [08] CUSTOM RULESET",
@@ -14088,11 +16156,11 @@ html += `
"page-tactics-fog": "> ADVANCED TACTICS: FOG OF WAR
When Fog of War is active, your satellite uplink is degraded. Territories outside of your immediate visual range are shrouded, and enemy troop counts are hidden.
SIGHTLINES: You can only see the status of territories that are directly adjacent to a territory you own. If an enemy is two spaces away, their strength and ownership are unknown.
ACTION LOG: The Vault-Tec Action Log is also affected by the fog. Battles, troop movements, and events occurring in hidden sectors will not be detailed. Instead, you will only see vague reports of 'Sensor Anomalies' or 'Radio Static'.
SCOUTING: To reveal the map, you must physically attack and conquer neutral or enemy lands to push your borders forward.
PERCEPTION: If you find a Perception Bobblehead during a Wasteland Encounter, you can use it to temporarily lift the Fog of War and reveal the entire map for a few turns.
> BACK TO ADVANCED TACTICS",
-"page-tactics-commander": "> ADVANCED TACTICS: COMMANDER PROTOCOL
If Commanders are active, each faction is assigned a VIP leader marked by a ★ icon. This completely changes the win conditions of the simulation.
COMMANDER MOVE PHASE: Commanders are highly mobile. During this specific phase, your Commander can move up to two territories and does not have to stay within your own friendly territories.
CONVERTING TERRITORY: If your Commander is stationed in an enemy territory, you can click 'CONVERT TERRITORY' on their dashboard to initiate a 3-turn siege. The Commander cannot move while converting, but if successful, the territory flips to your control.
ENGAGING COMMANDERS: Regular units can attack an enemy Commander, but only if the enemy Commander is currently outside of their own territory. If you select a friendly territory that contains your Commander (★), you will be given the option to either attack an enemy Commander, or put your Commander on \"standby\" to issue standard attack orders elsewhere.
ENTRENCHED DEFENSE: Commanders provide a massive 20% defensive bonus to the territory they are stationed in, making them much harder to dislodge.
MEDICAL CARE & HEALING: Commanders have a finite health pool. If they take damage, they will slowly regain health over time as long as they remain stationed on your own territory. You can also use Stimpaks to restore their health instantly by opening the INV tab at the bottom of your screen.
ASSASSINATION: Your Commander is your king. If the territory holding your ★ icon is successfully attacked and defeated, your Commander is killed, and you are instantly eliminated from the game.
> BACK TO ADVANCED TACTICS",
+"page-tactics-commander": `> ADVANCED TACTICS: COMMANDER PROTOCOL
If Commanders are active, each faction is led by acommander marked by a ★ icon, which completely changes the win conditions.
KEY RULES: * Win/Loss: If your Commander is killed, you are eliminated. Be the last Commander standing to win. * Mobility & AP: Commanders have 2 Action Points (AP) per turn to Move, Duel other Commanders, or start a multi-turn 'Conversion' of enemy territory. * Defense: A stationed Commander provides a +20% defensive bonus to their territory's garrison. * Healing: Commanders slowly heal on friendly land. For instant healing, use Stimpaks.
For more information on Stimpaks and other items, see the ITEM DATABASE from the main menu.
> BACK TO ADVANCED TACTICS`,
-"page-tactics-nukes": "> ADVANCED TACTICS: SCORCHED EARTH
The ultimate trump card. When Scorched Earth is active, the nuclear arsenal of the old world is put back on the table.
THE SILO: To launch a nuke, you must first locate and conquer a territory containing a Nuclear Silo, marked by a ☢ icon. Silos also provide a defensive combat bonus to the troops stationed there.
THE CODES: A Silo is useless without launch codes. You must send troops on Expeditions (Encounters) to search ruins. If you successfully scavenge all 4 unique Launch Codes, the launch protocol is armed.
TOTAL ANNIHILATION: Once armed, you can select any territory on the map. A nuclear strike takes 3 turns to impact. When it lands, it will instantly vaporize every single unit at ground zero, including Commanders. It also inflicts 50% casualties and severe radiation on all adjacent territories.
DISRUPTION: If an enemy initiates a launch, you can abort the sequence by capturing the specific origin Silo they launched it from before the 3-turn timer expires.
> BACK TO ADVANCED TACTICS",
+"page-tactics-nukes": `> ADVANCED TACTICS: SCORCHED EARTH
When Scorched Earth is active, the nuclear arsenal of the old world is back on the table.
THE SILO & CODES: To launch a nuke, you must control a Nuclear Silo (☢) and possess all 4 Launch Codes, which can be found via conquest or Encounters.
THE IMPACT: A strike takes 3 turns to land. The initial impact is devastating and cannot be defended against: * Ground Zero: 100% of all units (including Commanders) are instantly vaporized. * Blast Radius: All adjacent territories suffer 50% casualties.
THE GLOWING SEA (ATTRITION): The territory is not permanently destroyed. Instead, it becomes a 'Glowing Sea' with severe radiation that lasts for 10 turns. Any army moving into this zone will suffer 80% attrition damage at the start of the next turn. This damage rate slowly decreases as the radiation cools.
PERK RESISTANCE: Perks like Adamantium Skeleton and items like RadAway will reduce or negate damage from the radiation attrition over time, but they offer NO protection from the initial blast damage.
DISRUPTION: You can abort an enemy's launch by capturing their specific origin Silo before the 3-turn timer expires.
> BACK TO ADVANCED TACTICS`,
-"page-legend": "> MAP LEGEND
The tactical map utilizes standard RobCo cartography visuals to denote high-value targets, hazards, and unknown variables.
★ (STAR): VIP Commander. Represents a faction leader. Highly lethal in combat. Target for assassination.
☢ (SILO): Nuclear Silo. Capture and hold this territory to enable Scorched Earth launch protocols.
⌖ (CROSSHAIR): Nuke Target. Indicates a territory currently targeted for an active nuclear strike.
? (QUESTION MARK): Fog of War. Denotes an area shrouded from your sensors. Its exact troop count and ownership are completely unknown.
GREEN GLOW: Active Radstorm. An environmental hazard zone. Any troops remaining in this zone at the end of the turn will suffer massive casualties.",
+"page-legend": `> MAP LEGEND
The tactical map utilizes standard RobCo cartography visuals to denote high-value targets, hazards, and unknown variables.
(PAW PRINT): Dogmeat Quest. Location of the "A Boy and His Dog" special encounter.
★ (STAR): Commander. Represents a faction leader. Highly lethal in combat. Target for assassination.
☢ (SILO): Nuclear Silo. Capture and hold this territory to enable Scorched Earth launch protocols.
⌖ (CROSSHAIR): Nuke Target. Indicates a territory currently targeted for an active nuclear strike.
? (QUESTION MARK): Fog of War. Denotes an area shrouded from your sensors. Its exact troop count and ownership are completely unknown.
GREEN GLOW: Active Radstorm. An environmental hazard zone. Any troops remaining in this zone at the end of the turn will suffer massive casualties.`,
"page-history": "> HOLOTAPE ARCHIVES
Select a holotape to load archival data on previous Wasteland simulations:
> BACK TO MAIN DIRECTORY",
@@ -14114,9 +16182,31 @@ html += `
"page-data": "> MISSION DATA: SAVE & LOAD PROTOCOLS
The simulation allows the Overseer to suspend current progress and resume operations at a later date by exporting data as a JSON file.
SAVING A SIMULATION: During your active turn, locate the Save button (marked with the icon) positioned next to the REBOOT GAME button. Clicking this will compile your complete session state—including map control, troop deployments, Bottle Cap reserves, Commander health levels, and active rulesets—and prompt you to download it as a JSON file to your local hardware.
LOADING A SIMULATION: You can only resume a suspended simulation during the initial RobCo boot sequence before a match begins. Select the LOAD GAME option from the boot menu and upload your previously saved JSON file to fully restore your session parameters.
DATA RETENTION WARNING: The terminal exports save data strictly as localized JSON files. There is no external cloud backup or browser auto-save. You are responsible for securing these files on your local operating system. If you delete or misplace your JSON file, your archived simulation cannot be recovered.",
-"page-about": "> SYSTEM CREDITS & LEGAL
ORIGINAL ENGINE ARCHITECTURE: This simulation was heavily modified from the original Risk framework created by Vinayak Vedantam (https://github.com/vvedanta).
PORTABLE DEPLOYMENT: Players can download the self-contained game as a .html file. To download: Save this webpage (Ctrl+S) as a single HTML file. (The playable music is not included).
SUPPORT THE DEVELOPER: Support my work by buying my book, SurvivalSOS: Fundamentals of SurvivalAvailable on Amazon
DISCLAIMER: This is an independent, fan-made project and is not affiliated with or endorsed by Bethesda Softworks, ZeniMax Media, or Microsoft. All Fallout-related intellectual property belongs to its respective owners. No copyright or trademark infringement is intended.",
+"page-items": `> ITEM DATABASE
The wasteland is full of valuable pre-war technology and chems. This section details the items you can find and use to gain a tactical advantage.
> STIMPAKS > BOBBLEHEADS > WASTELAND RELICS
< BACK TO MAIN DIRECTORY`,
+
+"page-stimpaks": `> ITEMS: STIMPAKS
A miraculous pre-war healing agent. Stimpaks are essential for keeping your Commander alive in the field.
FUNCTION: Instantly restores 20 HP to your Commander. If you have the 'Medic' perk, this is increased to 40 HP.
USAGE: Stimpaks can only be used during the Commander Phase. Activating one costs 1 Action Point (AP). You can use a Stimpak by clicking the button in the Commander UI or from the main Inventory screen.
ACQUISITION: Stimpaks are found randomly by completing Wasteland Encounters or by looting the supplies of a defeated rival Commander.
< BACK TO ITEM DATABASE`,
+
+"page-bobbleheads": `> ITEMS: BOBBLEHEADS
Rare, pre-war Vault-Tec collectibles that provide powerful, temporary buffs. Once found, they are permanently added to your inventory.
USAGE & PHASE LOCKING: Bobbleheads can only be activated during specific phases of your turn to prevent wasting their effects. An active Bobblehead lasts for one full turn and then goes on cooldown.
* (S)trength: +10% attack odds. (Battle/Commander Phase only) * (P)erception: Lifts Fog of War. (Battle/Commander Phase only) * (E)ndurance: +10% defense odds. (Battle/Commander Phase only) * (C)harisma: Improves troop trade-in value or reduces recruitment cost. (Recruitment/Fortify Phase only) * (I)ntelligence: Reveals enemy stats & halves Silo defense. (Battle/Commander Phase only) * (A)gility: Grants an extra maneuver action. (Maneuver/Commander Phase only) * (L)uck: Triples the chance to find loot. (Battle/Commander Phase only)
< BACK TO ITEM DATABASE`,
+
+"page-relics": `> ITEMS: WASTELAND RELICS
Extremely rare and powerful single-use artifacts. Six are randomly seeded into the loot pool each match.
* G.E.C.K.: Restores a Crater or Radstorm tile to lush land and spawns troops. * Fat Man: Devastating mini-nuke strike. Range is limited to 3 territories from your border. * Stealth Boy: Hide your territories from enemy intel for 2 turns. * Bottlecap Mine: Trap a friendly territory. Detonates on an incoming enemy. * Cryolator: Freeze an enemy territory, preventing all actions for 1 turn. Range is limited to 3 territories from your border. * Vault-Tec Lunchbox: A random assortment of Caps and Troops. * Super Stimpak: Auto-revives your Commander upon taking fatal damage. * Jet: Instantly take a second, consecutive turn. * RadAway: Grants total immunity to Radstorms and nuke fallout for 3 turns. * Silver Shroud Card: Blockade an enemy land, preventing reinforcements for 3 turns. * Wasteland Survival Guide: Instantly and successfully complete all active map expeditions.
< BACK TO ITEM DATABASE`,
+
+
+"page-about": "> SYSTEM CREDITS & LEGAL
ORIGINAL ENGINE ARCHITECTURE: This simulation was heavily modified from the original Risk framework created by Vinayak Vedantam (https://github.com/vvedanta).
PORTABLE DEPLOYMENT: Players can download the self-contained game as a .html file. To download: Save this webpage (Ctrl+S) as a single HTML file. (The playable music is not included).
SUPPORT THE DEVELOPER: Support my work by buying my book, SurvivalSOS: Fundamentals of SurvivalAvailable on Amazon
DISCLAIMER: This is an independent, fan-made project and is not affiliated with or endorsed by Bethesda Softworks, ZeniMax Media, or Microsoft. All Fallout-related intellectual property belongs to its respective owners. No copyright or trademark infringement is intended.",
"page-patch-notes": "> UPDATE HISTORY (PATCH NOTES)
" +
+"v2.4 [THE COMPANION & COGNITION UPDATE] " +
+"- New Companion System (Dogmeat): Added a rare, multi-stage quest to find and rescue Dogmeat. Players can choose to take him in an 'Injured' state (which incurs debuffs) or heal him with resources to unlock powerful combat, loot-finding, and mine-defusing buffs. " +
+"- Scorched Earth Overhaul: Ground Zero is no longer permanently destroyed. Nuked territories now suffer severe, 10-turn radiation attrition (80% initial losses) that slowly cools off. The launch engine now supports simultaneous nuclear strikes from multiple factions. " +
+"- V.A.T.S. Action Validator: V.A.T.S. now displays high-visibility warning banners explaining exactly why a tactical action is blocked (e.g., 'OUT OF RANGE', 'TERRITORY FROZEN SOLID'). It also tracks active nuclear impact countdowns. " +
+"- Advanced Relic Targeting: Fat Man and Cryolator strikes are now strictly limited to a range of 3 territories from your borders. Added theme-colored map icons for planted Mines, Frozen zones, Blockades, and active Expeditions. " +
+"- Progression Polish: The Level-Up system has been refined to prevent offering duplicate non-stackable perks, and now clearly labels upgrades for stackable abilities (e.g., Rank 2, Rank 3). " +
+"- Deployment Protocols: Introduced Classic Manual and Semi-Auto initial placement phases to the boot menu for deep strategic starts. " +
+"- Advanced AI Brain: Artificial Intelligence rewritten to prevent suicidal attacks, aggressively consolidate continents, perform continuous 'Blitz' sweeps, and utilize cooldown-based perks multiple times. " +
+"- Relic & Hazard Parity: Wasteland Relics now function as intended. Jet grants an extra turn, Stealth Boys hide your territories from AI radar, the G.E.C.K. clears Radstorms, and RadAway properly blocks weather attrition. " +
+"- Tactical Backbriefs: Turbo Mode now generates a dynamic, immersive intelligence summary at the end of the AI cycle instead of clogging the action log with AI-vs-AI combat spam. " +
+"- Wasteland Fixes: Added Maxson's Brotherhood (FO4), synchronized Dogmeat's status effects, resolved the 'Missing Caps' truce bug, and ensured AI respects Cryolator freezes and Silver Shroud blockades. " +
+"- Medical & Phase Locks: RadAway now explicitly grants immunity to Nuke Fallout in addition to weather Radstorms, complete with UI and action log feedback. Bobblehead activations are now strictly phase-locked to prevent accidental wasted uses. " +
+"- Wasteland Encounters: Increased the discovery rate of radio transmissions and post-battle expeditions. Cleaned up legacy SVG map text artifacts.
" +
"v2.3 [LEVELING & MOBILE UPDATE] " +
"- Leveling System: Implemented a new player progression system featuring uncapped XP for continuous character growth. " +
"- Enhanced Phone Mode: Completely overhauled the mobile interface with adaptive UI scaling and a dedicated mission log for small screens. " +
@@ -14198,7 +16288,7 @@ html += `
"- Performance: Turbo Mode toggle added to bypass V.A.T.S. rendering for accelerated AI processing.
" +
"Legacy v1.1 [PIP-BOY SCREEN AND UI] " +
"- Themes: Capital Wasteland and Mojave holographic overlays loaded.
" +
-"v1.0 [ROBCO OS INITIALIZED] " +
+"Legacy v1.0 [ROBCO OS INITIALIZED] " +
"- Baseline Strategic Simulation established. Faction telemetry and global map data synchronized.
" +
"> BACK TO SYSTEM CREDITS",
@@ -14276,11 +16366,22 @@ html += `
-
`;
+
+
+
+ COMPANION TESTING
+
+
+
+
+
+
+ `;
}
+
return `
> TERMINAL OVERRIDE: CORE LOGIC ACCESS Toggles lit in green are ACTIVE. Dimmed buttons require a disabled mechanic.