diff --git a/index.html b/index.html index f7fd378..b9a0361 100644 --- a/index.html +++ b/index.html @@ -89,6 +89,20 @@ cursor: auto; } + /* Protect UI icons from being hijacked by the game board's Map CSS */ + .ui-svg-icon path, .ui-svg-icon polyline { + stroke: currentColor !important; + fill: none !important; + } + + /* Brute-force the SVG lines to turn dark when the button is hovered */ + button#btn-save-game:hover svg path, + button#btn-save-game:hover svg polyline, + button#btn-load-game:hover svg path, + button#btn-load-game:hover svg polyline { + stroke: #1a1a1a !important; + } + /* Standard pointer for interactive elements */ button, select, @@ -105,6 +119,53 @@ /* No cursor property here - it will be added dynamically */ } + /* GOAL 1: Leaderboard Pulsing Animation */ + @keyframes activeTurnPulse { + 0% { box-shadow: 0 0 5px var(--pip-color), inset 0 0 2px var(--pip-color); border-color: var(--pip-color); transform: scale(1); } + 100% { box-shadow: 0 0 25px var(--pip-color), inset 0 0 12px var(--pip-color); border-color: #ffffff; transform: scale(1.03); filter: brightness(1.2); } + } + + /* Apply pulse to the highlighted card */ + .highlight { + animation: activeTurnPulse 0.5s infinite alternate !important; + z-index: 10; + position: relative; + } + + + /* Kill the pulse instantly if Turbo is active */ + .turbo-active .highlight { + animation: none !important; + box-shadow: 0 0 5px var(--pip-color) !important; + border-color: var(--pip-color) !important; + } + + /* GOAL 2: Make the "_" and "█" Cursors Actually Blink */ + @keyframes blinkCursorAnim { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } + } + .blinking-cursor, .terminal-cursor { + animation: blinkCursorAnim 1s infinite !important; + font-weight: bold; + } + + /* GOAL 3: Force ALL Commander Text to Match the UI Theme */ + #cmdr-ui-container, + #cmdr-ui-container span, + #cmdr-ui-container div, + #cmdr-hp-text, + #cmdr-ap-text { + color: var(--pip-color) !important; + border-color: var(--pip-color) !important; + text-shadow: 0 0 5px var(--pip-color) !important; + } + + /* Keep the HP/AP fill bars solid (don't make them transparent text) */ + #cmdr-hp-fill, #cmdr-ap-fill { + text-shadow: none !important; + color: #1a1a1a !important; /* Dark text inside the bar so it's readable */ + } /* --- Modals --- */ .overlay { @@ -1399,16 +1460,24 @@ } } - /* --- Developer Menu Button Fixes --- */ - #dev-modal button { - /* Ensures text color does not change on hover */ - color: var(--pip-color) !important; - } + /* Admin/Dev Menu Unified Buttons */ + #dev-modal button { + width: 100%; + margin-bottom: 10px; + padding: 10px; + background-color: var(--pip-dark, #1a1a1a); + color: var(--pip-color); + border: 1px solid var(--pip-color); + font-family: inherit; + font-size: 16px; + cursor: pointer; + text-transform: uppercase; + } + #dev-modal button:hover { + background-color: var(--pip-color); + color: var(--pip-dark, #1a1a1a); + } - #dev-modal button:hover { - /* Sets a semi-transparent white background on hover for readability */ - background: rgba(255, 255, 255, 0.15); - } #dev-modal button:active { /* Pushes the button down slightly and darkens it for click feedback */ @@ -1870,7 +1939,14 @@
- + +
@@ -3228,6 +3304,8 @@ + + @@ -3506,7 +3584,13 @@
- +
@@ -3778,6 +3862,7 @@ const fileInput = document.getElementById('file-load-game'); if (loadBtn && fileInput) { + // 1. When the visible button is clicked, trigger the hidden file input loadBtn.addEventListener('click', (e) => { e.preventDefault(); @@ -4031,16 +4116,35 @@ const encounterData = { creatures: [ - { name: "Savage Dog", threat: 0.1 }, { name: "Mole Rat", threat: 0.3 }, { name: "Bloatfly", threat: 0.4 }, - { name: "Radroach", threat: 0.5 }, { name: "Yao Guai", threat: 1.0 }, { name: "Feral Ghoul", threat: 1.2 }, - { name: "Giant Ant", threat: 1.5 }, { name: "Scavenger's Dog", threat: 1.8 }, { name: "Giant Worker Ant", threat: 2.0 }, - { name: "Radscorpion", threat: 2.5 }, { name: "Giant Soldier Ant", threat: 3.0 }, { name: "Fire Ant Soldier", threat: 3.5 }, - { name: "Guard Dog", threat: 4.0 }, { name: "Fire Ant Warrior", threat: 4.5 }, { name: "Mirelurk", threat: 5.0 }, - { name: "Feral Ghoul Roamer", threat: 6.0 }, { name: "Giant Radscorpion", threat: 7.0 }, { name: "Mirelurk Hunter", threat: 8.0 }, - { name: "Vicious Dog", threat: 9.0 }, { name: "Centaur", threat: 10.0 }, { name: "Feral Ghoul Reaver", threat: 12.0 }, - { name: "Deathclaw", threat: 15.0 }, { name: "Enclave Deathclaw", threat: 20.0 }, { name: "Super Mutant", threat: 25.0 }, - { name: "Super Mutant Master", threat: 30.0 }, { name: "Super Mutant Overlord", threat: 40.0 }, { name: "Super Mutant Behemoth", threat: 50.0 } + { name: "Centaur", threat: 10.0, isHumanoid: true }, + { name: "Deathclaw", threat: 15.0, isHumanoid: false }, + { name: "Enclave Deathclaw", threat: 20.0, isHumanoid: false }, + { name: "Feral Ghoul", threat: 1.2, isHumanoid: true }, + { name: "Feral Ghoul Reaver", threat: 12.0, isHumanoid: true }, + { name: "Feral Ghoul Roamer", threat: 6.0, isHumanoid: true }, + { name: "Fire Ant Soldier", threat: 3.5, isHumanoid: false }, + { name: "Fire Ant Warrior", threat: 4.5, isHumanoid: false }, + { name: "Giant Ant", threat: 1.5, isHumanoid: false }, + { name: "Giant Radscorpion", threat: 7.0, isHumanoid: false }, + { name: "Giant Soldier Ant", threat: 3.0, isHumanoid: false }, + { name: "Giant Worker Ant", threat: 2.0, isHumanoid: false }, + { name: "Guard Dog", threat: 4.0, isHumanoid: false }, + { name: "Mirelurk", threat: 5.0, isHumanoid: false }, + { name: "Mirelurk Hunter", threat: 8.0, isHumanoid: false }, + { name: "Mole Rat", threat: 0.3, isHumanoid: false }, + { name: "Radroach", threat: 0.5, isHumanoid: false }, + { name: "Radscorpion", threat: 2.5, isHumanoid: false }, + { name: "Savage Dog", threat: 0.1, isHumanoid: false }, + { name: "Scavenger's Dog", threat: 1.8, isHumanoid: false }, + { name: "Super Mutant", threat: 25.0, isHumanoid: true }, + { name: "Super Mutant Behemoth", threat: 50.0, isHumanoid: true }, + { name: "Super Mutant Master", threat: 30.0, isHumanoid: true }, + { name: "Super Mutant Overlord", threat: 40.0, isHumanoid: true }, + { name: "Vicious Dog", threat: 9.0, isHumanoid: false }, + { name: "Yao Guai", threat: 1.0, isHumanoid: false }, + { name: "Bloatfly", threat: 0.4, isHumanoid: false } ], + genericLocations: ["a crashed Vertibird", "a ruined highway overpass", "an abandoned Red Rocket station", "a Pre-War police station", "a collapsed metro tunnel", "a flooded sewer system", "a ruined church", "an abandoned drive-in theater", "a Broadcasting Tower", "an Abandoned Shack"], subLocations: ["the manager's office", "the pharmacy counter", "the main server room", "the evidence locker", "the cockpit", "the projection booth", "the sacristy", "the underground pump station", "a hidden Refrigerator", "a locked Safe"], people: ["a Wasteland Doctor", "a scared Settler", "a shifty-eyed Mercenary", "a Ghoul scavenger", "a Brotherhood of Steel scribe", "a Fugitive Slave", "a Wounded Sheriff", "a Merchant", "Talon Company Mercs", "a Drunken Drifter", "a Lost Farmer", "a Mister Handy"], @@ -4108,7 +4212,8 @@ const infoIncome = Array.from(document.getElementsByClassName('income')); const areas = Array.from(document.getElementsByClassName('area')); const bar = Array.from(document.getElementsByClassName('bar')); - const map = document.querySelector('svg'); + const map = document.querySelector('.area').closest('svg'); + const vatsTooltip = document.createElement('div'); vatsTooltip.id = 'vats-tooltip'; @@ -4230,7 +4335,14 @@ if (targetCountry.isLockedDown) { infoLines.push(`TERRITORY LOCKED DOWN`); } + + // NEW: Searching status + if (targetCountry.isExploring) { + infoLines.push(`SEARCHING... (${targetCountry.exploreTurnsLeft} TURNS LEFT)`); + } + infoLines.push(`TERRITORY: ${formatTerritoryName(targetCountry.name)}`); + if (targetCountry.isCrater) { infoLines.push(`IRRADIATED CRATER (IMPASSABLE)`); } else { @@ -4911,6 +5023,24 @@ document.getElementById('dev-modal').style.display = 'none'; if (helpModal) helpModal.style.display = 'block'; }); + // --- NEW DEV GOD MODE BUTTON --- + const godModeBtn = document.getElementById('dev-godmode'); + if (godModeBtn) { + godModeBtn.addEventListener('click', (e) => { + Gamestate.godMode = !Gamestate.godMode; + + // Dynamically update the button text! + e.target.textContent = Gamestate.godMode ? "TURN GM OFF" : "TURN GM ON"; + + if (Gamestate.showToast) { + Gamestate.showToast(`Dev: AI Ignore is now ${Gamestate.godMode ? 'ON' : 'OFF'}`, Gamestate.godMode ? "#ff3333" : "grey"); + } + if (Gamestate.logAction) { + Gamestate.logAction(`[ OVERSEER ] AI Ignore Protocol: ${Gamestate.godMode ? 'ENGAGED' : 'DISABLED'}.`, true); + } + }); + } + // --- FIX: RESTORES THE PATCH NOTES BUTTONS --- @@ -4926,6 +5056,19 @@ document.getElementById('confirm-yes')?.addEventListener('click', () => { document.getElementById('confirm-restart-modal').style.display = 'none'; this.restart(); }); document.getElementById('confirm-no')?.addEventListener('click', () => { document.getElementById('confirm-restart-modal').style.display = 'none'; }); + // Instantly toggle the sliding/pulsing animations when Turbo is clicked + const turboToggleBtn = document.getElementById('turbo-toggle'); + if (turboToggleBtn) { + turboToggleBtn.addEventListener('change', (e) => { + if (e.target.checked) { + document.body.classList.add('turbo-active'); + } else { + document.body.classList.remove('turbo-active'); + } + }); + } + + document.getElementById('view-cards-btn')?.addEventListener('click', () => { if (this.wastelandEconomyActive) { this.showRecruitmentModal(); @@ -4943,7 +5086,15 @@ // DEV CONSOLE BUTTONS document.getElementById('close-dev-btn')?.addEventListener('click', () => { document.getElementById('dev-modal').style.display = 'none'; }); - document.getElementById('dev-caps')?.addEventListener('click', () => { for (let i = 0; i < 10; i++) this.player.cards.push({ country: "Cheat", type: "Wild" }); this.updateInfo(); this.showToast("Dev: +10 Caps"); }); + document.getElementById('dev-caps')?.addEventListener('click', () => { + if (this.wastelandEconomyActive) { + this.player.caps += 10; + } else { + for (let i = 0; i < 10; i++) this.player.cards.push({ country: "Cheat", type: "Wild" }); + } + this.updateInfo(); + this.showToast("Dev: +10 Caps/Cards"); + }); document.getElementById('dev-code')?.addEventListener('click', () => { this.player.codes++; this.updateInfo(); this.showToast("Dev: +1 Launch Code"); }); document.getElementById('dev-troops')?.addEventListener('click', () => { this.player.reserve += 100; @@ -5701,7 +5852,7 @@ Gamestate.openDiplomacy = function (targetName) { if (repScore >= 35) { repLabel = "IDOLIZED"; repColor = "#00ff00"; } else if (repScore >= 10) { repLabel = "LIKED"; repColor = "#88ff88"; } else if (repScore <= -35) { repLabel = "HATED"; repColor = "#ff0000"; } - else if (repScore <= -10) { repLabel = "HOSTILE"; repColor = "#ff8888"; } + else if (repScore <= -10) { repLabel = "VILIFIED"; repColor = "#ff8888"; } let repDisplay = document.getElementById('dip-rep-display'); repDisplay.textContent = `REP: ${repLabel}`; repDisplay.style.color = repColor; @@ -5716,13 +5867,32 @@ Gamestate.openDiplomacy = function (targetName) { let theirMaxCurrency = this.wastelandEconomyActive ? target.caps : target.cards.length; if (this.wastelandEconomyActive) { - baseCostPerTurn = 5 + (tax * 5); // Minimum 5 Caps - if (target.army > this.player.army) baseCostPerTurn = 10 + (tax * 5); - if (target.army > (this.player.army * 2)) baseCostPerTurn = 15 + (tax * 5); + baseCostPerTurn = 5 + (tax * 5); // Base cost 5 Caps + Betrayal Tax - if (repScore >= 35) baseCostPerTurn = Math.max(0, baseCostPerTurn - 10); - else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 5); + // Army Size Modifiers + if (target.army > (this.player.army * 2)) { + baseCostPerTurn += 5; + } else if (target.army > this.player.army) { + baseCostPerTurn += 2; + } else if (target.army < this.player.army) { + baseCostPerTurn -= 2; + } + + // Reputation Modifiers + if (repScore <= -35) { + baseCostPerTurn += 5; // Hated + } else if (repScore <= -10) { + baseCostPerTurn += 2; // Hostile + } else if (repScore >= 35) { + baseCostPerTurn -= 3; // Idolized + } else if (repScore >= 10) { + baseCostPerTurn -= 1; // Liked + } + + // Failsafe: Truce always costs at least 1 Cap + baseCostPerTurn = Math.max(1, baseCostPerTurn); } else { + baseCostPerTurn = 0.33 + (tax * 0.33); // Classic Cards if (target.army > this.player.army) baseCostPerTurn = 0.66 + (tax * 0.33); if (target.army > (this.player.army * 2)) baseCostPerTurn = 1.0 + (tax * 0.33); @@ -5766,17 +5936,20 @@ Gamestate.openDiplomacy = function (targetName) { reqTruceSlider.max = 0; reqTruceSlider.value = 0; reqTruceSlider.disabled = true; - // --- FIX: Changed text to match your requirement. --- - truceCostDisplay.textContent = `Requires at least ${Math.ceil(baseCostPerTurn)} ${currencyName} for a 1-turn Truce.`; - truceCostDisplay.style.color = "#ff3333"; + truceCostDisplay.textContent = `Requires at least ${Math.ceil(baseCostPerTurn)} ${currencyName} for a 1-day Truce.`; + truceCostDisplay.style.color = "var(--pip-color)"; + truceCostDisplay.style.opacity = "0.7"; // Faded to indicate it's locked } else { // Unlock the slider reqTruceSlider.max = maxAffordableTurns; reqTruceSlider.value = 0; reqTruceSlider.disabled = false; - truceCostDisplay.textContent = `Cost: 0 ${currencyName}`; - truceCostDisplay.style.color = "#ffcc00"; + truceCostDisplay.textContent = `Cost: 0 ${currencyName} (${Math.ceil(baseCostPerTurn)}/Day)`; + truceCostDisplay.style.color = "var(--pip-color)"; + truceCostDisplay.style.opacity = "1"; } + + reqTruceVal.textContent = "0"; const reqCapsSlider = document.getElementById('dip-req-caps'); @@ -5790,17 +5963,36 @@ Gamestate.openDiplomacy = function (targetName) { // 5. Update values on drag offerCapsSlider.oninput = function () { offerCapsVal.textContent = this.value; validateProposal(); }; offerTroopsSlider.oninput = function () { offerTroopsVal.textContent = this.value; validateProposal(); }; - reqCapsSlider.oninput = function () { reqCapsVal.textContent = this.value; validateProposal(); }; - // --- FIX: Wrapped the original oninput logic in a new function to keep it clean. --- + reqCapsSlider.oninput = function () { + reqCapsVal.textContent = this.value; + + // MUTUAL EXCLUSIVITY: If demanding caps, force Truce slider to 0 + if (parseInt(this.value) > 0) { + reqTruceSlider.value = 0; + reqTruceVal.textContent = "0"; + if (maxAffordableTurns >= 1) { + truceCostDisplay.textContent = `Cost: 0 ${currencyName}`; + } + } + validateProposal(); + }; + const handleTruceSliderInput = function() { reqTruceVal.textContent = this.value; + // MUTUAL EXCLUSIVITY: If requesting a truce, force Demand Caps slider to 0 + if (parseInt(this.value) > 0) { + reqCapsSlider.value = 0; + reqCapsVal.textContent = "0"; + } + let truceTurnsRequested = parseInt(this.value); let totalTruceCost = Math.ceil(baseCostPerTurn * truceTurnsRequested); if (maxAffordableTurns >= 1) { - truceCostDisplay.textContent = `Cost: ${totalTruceCost} ${currencyName}`; + // NEW: Keep the per-day rate visible while sliding + truceCostDisplay.textContent = `Cost: ${totalTruceCost} ${currencyName} (${Math.ceil(baseCostPerTurn)}/Day)`; } validateProposal(); @@ -5808,6 +6000,7 @@ Gamestate.openDiplomacy = function (targetName) { reqTruceSlider.oninput = handleTruceSliderInput; + // 6. The Validation Logic let validateProposal = () => { let capsOffered = parseInt(offerCapsSlider.value); @@ -5818,37 +6011,68 @@ Gamestate.openDiplomacy = function (targetName) { let analysisEl = document.getElementById('dip-analysis'); let sendBtn = document.getElementById('dip-send'); - if (repScore <= -35 && (capsRequested > 0 || truceTurnsRequested > 0)) { - analysisEl.innerHTML = `Refusal guaranteed. Target is hostile.`; + let isTerrified = (this.player.army >= target.army * 2); + if (repScore <= -35 && (capsRequested > 0 || truceTurnsRequested > 0) && !isTerrified) { + analysisEl.innerHTML = `Refusal guaranteed. Target is hostile and unyielding.`; sendBtn.disabled = true; return; } let offerValue = 0; - let requestValue = capsRequested; - - if (this.wastelandEconomyActive) { - offerValue = capsOffered + (troopsOffered * 5); // 1 Troop = 5 Caps - } else { - offerValue = capsOffered + (troopsOffered * 0.5); - } + let requestValue = capsRequested; // Removed the Truce cost from here! + let softPower = 0; let totalTruceCost = Math.ceil(baseCostPerTurn * truceTurnsRequested); - requestValue += totalTruceCost; + let totalCurrencyCost = capsOffered + totalTruceCost; // Track combined Cap spending + + // Failsafe: Prevent spending more than you have if combining Gifts + Truces + if (totalCurrencyCost > myMaxCurrency) { + analysisEl.innerHTML = `Insufficient funds. Combined cost exceeds your bank.`; + sendBtn.disabled = true; + return; + } + + if (this.wastelandEconomyActive) { + offerValue = capsOffered + (troopsOffered * 5); + if (this.player.army > target.army) softPower += Math.floor((this.player.army - target.army) * 1.5); + if (repScore >= 35) softPower += 10; + } else { + offerValue = capsOffered + (troopsOffered * 0.5); + if (this.player.army > target.army) softPower += (this.player.army - target.army) * 0.15; + if (repScore >= 35) softPower += 1.0; + } + let effectiveOffer = offerValue + softPower; + + // Auto-Pay Ceasefire Check + if (truceTurnsRequested > 0) { + if (offerValue > 0) { + analysisEl.innerHTML = `Purchasing Ceasefire + Generous Gift. Target will accept.`; + } else { + analysisEl.innerHTML = `Purchasing a ${truceTurnsRequested}-Turn Ceasefire for ${totalTruceCost} ${currencyName}.`; + } + sendBtn.disabled = false; + return; + } + // Is it a gift? if (offerValue > 0 && requestValue === 0) { - analysisEl.innerHTML = `Generous Gift. Significant Reputation increase expected.`; + analysisEl.innerHTML = `Generous Gift. Significant Reputation increase expected.`; sendBtn.disabled = false; return; } - // Is the trade fair? - if (offerValue >= requestValue && requestValue > 0) { - analysisEl.innerHTML = `Terms acceptable. Target likely to agree.`; + // Is the trade fair or forced? + if (effectiveOffer >= requestValue && requestValue > 0) { + if (offerValue < requestValue && softPower > 0) { + analysisEl.innerHTML = `Target will yield to your demands out of fear or respect.`; + } else { + analysisEl.innerHTML = `Terms acceptable. Target likely to agree.`; + } sendBtn.disabled = false; } else if (requestValue > 0) { - analysisEl.innerHTML = `Terms insufficient. Offer more ${currencyName} or Troops.`; + // Faded opacity used here for rejections to differentiate without breaking the theme color + analysisEl.innerHTML = `Target refuses. You must offer a fair trade, or possess a much larger army to force compliance.`; sendBtn.disabled = true; } else { analysisEl.innerHTML = `Awaiting terms...`; @@ -5856,6 +6080,9 @@ Gamestate.openDiplomacy = function (targetName) { } }; + + + validateProposal(); let modal = document.getElementById('diplomacy-modal'); @@ -5880,21 +6107,38 @@ Gamestate.openDiplomacy = function (targetName) { let offerValue = 0; let requestValue = proposal.capsRequested; let baseCostPerTurn = 0; + let softPower = 0; // NEW: Intimidation / Reputation leverage if (this.wastelandEconomyActive) { // CAPS ECONOMY offerValue = proposal.capsOffered + (proposal.troopsOffered * 5); - baseCostPerTurn = 2 + (tax * 2); - if (target.army > this.player.army) baseCostPerTurn = 4 + (tax * 2); - if (target.army > (this.player.army * 2)) baseCostPerTurn = 6 + (tax * 2); - - if (repScore >= 35) baseCostPerTurn = Math.max(0, baseCostPerTurn - 4); - else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 2); + if (this.player.army > target.army) { + softPower += Math.floor((this.player.army - target.army) * 1.5); + } + if (repScore >= 35) softPower += 10; + + // Synced Math from openDiplomacy UI! + baseCostPerTurn = 5 + (tax * 5); + if (target.army > (this.player.army * 2)) baseCostPerTurn += 5; + else if (target.army > this.player.army) baseCostPerTurn += 2; + else if (target.army < this.player.army) baseCostPerTurn -= 2; + + if (repScore <= -35) baseCostPerTurn += 5; + else if (repScore <= -10) baseCostPerTurn += 2; + else if (repScore >= 35) baseCostPerTurn -= 3; + else if (repScore >= 10) baseCostPerTurn -= 1; + + baseCostPerTurn = Math.max(1, baseCostPerTurn); } else { // CLASSIC CARD ECONOMY offerValue = proposal.capsOffered + (proposal.troopsOffered * 0.5); + if (this.player.army > target.army) { + softPower += (this.player.army - target.army) * 0.15; + } + if (repScore >= 35) softPower += 1.0; + baseCostPerTurn = 0.33 + (tax * 0.33); if (target.army > this.player.army) baseCostPerTurn = 0.66 + (tax * 0.33); if (target.army > (this.player.army * 2)) baseCostPerTurn = 1.0 + (tax * 0.33); @@ -5903,25 +6147,28 @@ Gamestate.openDiplomacy = function (targetName) { else if (repScore >= 10) baseCostPerTurn = Math.max(0, baseCostPerTurn - 0.33); } - // FIX: Check if truceRequested is greater than 0 (since it's a slider now) + let totalTruceCost = 0; if (proposal.truceRequested > 0) { - let totalTruceCost = Math.ceil(baseCostPerTurn * proposal.truceRequested); - requestValue += totalTruceCost; + totalTruceCost = Math.ceil(baseCostPerTurn * proposal.truceRequested); + // We no longer add this to requestValue, as it is auto-paid! } - // AI Acceptance Logic - if (offerValue >= requestValue || (offerValue > 0 && requestValue === 0)) { + let effectiveOffer = offerValue + softPower; + + // AI Acceptance Logic (Truce purchases are automatically accepted) + if (proposal.truceRequested > 0 || effectiveOffer >= requestValue || (offerValue > 0 && requestValue === 0)) { // 1 & 2. Transfer Currency if (this.wastelandEconomyActive) { - // Transfer Caps - this.player.caps -= proposal.capsOffered; - target.caps += proposal.capsOffered; + // Transfer Caps (Including auto-paid Truce cost) + this.player.caps -= (proposal.capsOffered + totalTruceCost); + target.caps += (proposal.capsOffered + totalTruceCost); + target.caps -= proposal.capsRequested; this.player.caps += proposal.capsRequested; } else { - // Transfer Cards - for (let i = 0; i < proposal.capsOffered; i++) { + // Transfer Cards (Including auto-paid Truce cost) + for (let i = 0; i < (proposal.capsOffered + totalTruceCost); i++) { if (this.player.cards.length > 0) target.cards.push(this.player.cards.pop()); } for (let i = 0; i < proposal.capsRequested; i++) { @@ -5950,7 +6197,6 @@ Gamestate.openDiplomacy = function (targetName) { } // 4. Enact Truce - // FIX: Enacts the truce for the exact number of turns selected on the slider if (proposal.truceRequested > 0) { this.diplomacy.truces.push({ f1: this.player.name, f2: target.name, turns: proposal.truceRequested }); } @@ -5960,6 +6206,12 @@ Gamestate.openDiplomacy = function (targetName) { let actionText = ""; let currencyName = this.wastelandEconomyActive ? "Caps" : "Cards"; + // Was it a Truce Purchase? + if (proposal.truceRequested > 0) { + repChange = 1; + actionText = `[ DIPLOMACY ] You purchased a ${proposal.truceRequested}-Turn Ceasefire with ${target.name} for ${totalTruceCost} ${currencyName}.`; + } + // Was it a pure gift? if (requestValue === 0 && offerValue > 0) { repChange = Math.floor(offerValue * 3); @@ -5970,6 +6222,11 @@ Gamestate.openDiplomacy = function (targetName) { repChange = Math.floor((offerValue - requestValue) * 2); actionText = `[ DIPLOMACY ] You concluded a highly favorable trade with ${target.name}.`; } + // Was it Extortion? (NEW) + else if (requestValue > 0 && offerValue < requestValue) { + repChange = -5; // Extorting damages your reputation with them + actionText = `[ DIPLOMACY ] You strong-armed ${target.name} into yielding to your demands.`; + } // Was it a standard, fair trade? else { repChange = 1; @@ -6337,12 +6594,22 @@ Gamestate.openDiplomacy = function (targetName) { Gamestate.handleClick = function (e) { + if (this.aiTurn || this.modalIsOpen) return; - if (this.aiTurn) return; + // --- NEW: Spontaneous Map Encounter Trigger (3% chance per click) --- + // Only allowed if encounters are enabled, we are NOT mid-attack, and we aren't targeting a nuke + if (this.encountersEnabled && Math.random() < 0.03) { + if (this.stage !== "Battle" || !this.prevCountry) { + if (this.stage !== "Nuke Targeting" && this.stage !== "Frenzy Targeting") { + this.resolveCreatureEncounter(); + return; // Stop the click action so the modal takes priority + } + } + } if (this.stage === "Fortify") { this.addArmy(e); - } else if (this.stage === "Battle" || this.stage === "Frenzy Targeting") { // --- THIS IS THE FIX --- + } else if (this.stage === "Battle" || this.stage === "Frenzy Targeting") { this.attack(e); } else if (this.stage === "Maneuver" || this.stage === "Commander Phase") { this.maneuver(e); @@ -6352,6 +6619,7 @@ Gamestate.openDiplomacy = function (targetName) { } + Gamestate.win = function (player) { if (winMessage) { winMessage.textContent = player.name; @@ -6410,11 +6678,11 @@ Gamestate.openDiplomacy = function (targetName) { let totalArmy = 0; this.players.forEach(player => { totalArmy += player.army }); // --- FINAL & DEFINITIVE PLAYER INFO LOGIC (REVISED) --- - let sortedPlayers = [...this.players].sort((a, b) => b.army - a.army); const iBobbleActive = this.bobbleheads && this.bobbleheads.find(b => b.key === 'i' && b.active); this.players.forEach((player, i) => { let infoBox = infoName[i] ? infoName[i].parentElement : null; + if (!infoBox) return; // --- Get UI Elements (instead of creating them) --- @@ -6426,6 +6694,13 @@ Gamestate.openDiplomacy = function (targetName) { if (player.alive) { infoBox.classList.remove('defeated'); + + // --- FIX: Force the player's card to highlight on their turn --- + if (player.isPlayer) { + if (!this.aiTurn) infoBox.classList.add('highlight'); + else infoBox.classList.remove('highlight'); + } + const fogEnabled = document.getElementById('opt-fog-of-war') && document.getElementById('opt-fog-of-war').checked; const hasIntel = !fogEnabled || player.name === this.player.name || iBobbleActive || (this.diplomacy.reputation[player.name] && this.diplomacy.reputation[player.name][this.player.name] >= 10); @@ -6435,7 +6710,18 @@ Gamestate.openDiplomacy = function (targetName) { // --- Faction Name & Reputation --- if (countryEl) { let countryHtml = player.country; - if (!player.isNeutral && player.name !== this.player.name) { + + // NEW: Add Text Label in Alliance Mode + if (this.isAllianceMode && !player.isNeutral) { + // Find who this player is allied with + let ally = this.players.find(p => p.team === player.team && p.name !== player.name && !p.isNeutral); + if (ally) { + // Append it on a new line, slightly smaller and faded + countryHtml += `
(Allied with ${ally.name})`; + } + } + // Standard Reputation Display (Hidden in Alliance Mode) + else if (!player.isNeutral && player.name !== this.player.name) { let rep = this.diplomacy.reputation[player.name]?.[this.player.name] || 0; let repText = "NEUTRAL"; if (rep >= 35) { repText = "IDOLIZED"; } @@ -6444,9 +6730,13 @@ Gamestate.openDiplomacy = function (targetName) { else if (rep <= -10) { repText = "HOSTILE"; } countryHtml += ` (${repText})`; } + countryEl.innerHTML = countryHtml; } + + + // Hide the old income-label if (incomeContainer) incomeContainer.style.display = "none"; @@ -6551,10 +6841,19 @@ Gamestate.openDiplomacy = function (targetName) { if (existingBtn) existingBtn.style.display = "none"; } - let rank = sortedPlayers.findIndex(p => p.name === player.name); - infoBox.style.order = rank; + // FIX: Order by turn (their index in the array) instead of army size + infoBox.style.order = i; }); + // NEW: Check Turbo status and apply a global class to control the sliding animations + let turbo = document.getElementById('turbo-toggle') && document.getElementById('turbo-toggle').checked; + if (turbo) { + document.body.classList.add('turbo-active'); + } else { + document.body.classList.remove('turbo-active'); + } + + // --- The rest of the original function remains the same... --- if (this.players.length === 6 && infoName[6] && infoName[6].parentElement) infoName[6].parentElement.style.display = "none"; let helpBtnEl = document.getElementById('help-btn'); if (helpBtnEl) helpBtnEl.style.order = "998"; @@ -6732,8 +7031,22 @@ Gamestate.openDiplomacy = function (targetName) { if (this.commandersEnabled && this.player.commander) { document.getElementById('cmdr-hp-text').textContent = `HP: ${this.player.commander.hp}/100`; document.getElementById('cmdr-ap-text').textContent = `AP: ${this.player.commander.ap}/2`; - document.getElementById('cmdr-hp-fill').style.width = `${this.player.commander.hp}%`; + + let hpFill = document.getElementById('cmdr-hp-fill'); + if (hpFill) { + hpFill.style.width = `${this.player.commander.hp}%`; + + // Dynamic HP Color (Red if <= 30, otherwise theme color) + if (this.player.commander.hp <= 30) { + hpFill.style.backgroundColor = "#ff3333"; + hpFill.style.boxShadow = "0 0 10px #ff3333"; + } else { + hpFill.style.backgroundColor = "var(--pip-color)"; + hpFill.style.boxShadow = "0 0 10px var(--pip-color)"; + } + } } + this.drawMapText(); this.lastStage = this.stage; // --- ACTIONABLE PERK BUTTON LOGIC (v6 - Chem Frenzy Integration) --- @@ -7022,6 +7335,36 @@ Gamestate.openDiplomacy = function (targetName) { } + if (!document.getElementById('ui-polish-styles')) { + let style = document.createElement('style'); + style.id = 'ui-polish-styles'; + style.innerHTML = ` + /* Leaderboard Pulsing Animation */ + @keyframes activeTurnPulse { + 0% { box-shadow: 0 0 5px var(--pip-color), inset 0 0 2px var(--pip-color); border-color: var(--pip-color); } + 100% { box-shadow: 0 0 15px var(--pip-color), inset 0 0 8px var(--pip-color); border-color: #ffffff; } + } + + /* When highlighted (and turbo is OFF), pulse the card */ + body:not(.turbo-active) .player-panel > div.highlight { + animation: activeTurnPulse 1.2s infinite alternate !important; + z-index: 10; + } + + /* Force Commander UI to match theme (including all text inside it) */ + #cmdr-ui-container, #cmdr-ui-container span, #cmdr-ui-container div { + border-color: var(--pip-color) !important; + color: var(--pip-color) !important; + text-shadow: 0 0 5px var(--pip-color) !important; + } + /* Ensure the health bar background itself doesn't turn transparent */ + #cmdr-hp-fill { + text-shadow: none !important; + } + `; + document.head.appendChild(style); + } + // --- Scrambled Text Intel Animation (All 2 Characters) --- @@ -8615,22 +8958,34 @@ Gamestate.openDiplomacy = function (targetName) { if (troopsToPlace > 0 && !turbo) await this.logAction(`${this.players[i].name} deployed ${troopsToPlace} troops to their sectors.`); } - let areaToFortify = ["", 0]; + let areaToFortify = ["", -1]; this.players[i].areas.forEach(area => { let country = this.countries.find(c => c.name === area); if (this.players[i].reserve > 0) { let ratio = 0; + + // NEW: Massive priority boost if the territory actually borders a valid enemy! + let hasEnemyNeighbor = country.neighbours.some(n => { + let nc = this.countries.find(x => x.name === n); + return nc && nc.owner !== this.players[i].name && !this.areAllies(this.players[i].name, nc.owner) && !nc.isCrater; + }); + if (hasEnemyNeighbor) ratio += 10.0; + if (this.nukesEnabled && country.isSilo) { - ratio = 5.0; + 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; + ratio += (count / continent.areas.length); } - if (ratio >= areaToFortify[1]) { + + // 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] } } @@ -8639,6 +8994,7 @@ Gamestate.openDiplomacy = function (targetName) { areaToFortify[0].army += this.players[i].reserve; this.players[i].reserve = 0; } + let currentAreas = [...this.players[i].areas]; for (let area of currentAreas) { let country = this.countries.find(c => c.name === area); @@ -8695,11 +9051,18 @@ Gamestate.openDiplomacy = function (targetName) { let opponent = this.countries.find(c => c.name === neighbourName); // --- FIX: The AI will consider attacking if the neighbor is an enemy, not a crater, AND NOT LOCKED DOWN. if (opponent && opponent.owner !== this.players[i].name && !opponent.isCrater && !opponent.isLockedDown) { + + // --- DEV GOD MODE: AI completely ignores Player territories --- + if (this.godMode && opponent.owner === this.player.name) { + return; // Skip adding the player to valid targets + } + possibleTargets.push(opponent); } }); + // --- CORRECTED ALLY FILTERING LOGIC --- // This will now correctly prevent the AI from attacking allies. possibleTargets = possibleTargets.filter(poss => { @@ -9674,80 +10037,217 @@ Gamestate.openDiplomacy = function (targetName) { Gamestate.resolveCreatureEncounter = async function () { - if (this.player.areas.length === 0) return; + if (this.player.areas.length === 0 || this.modalIsOpen) return; const randomAreaName = this.player.areas[Math.floor(Math.random() * this.player.areas.length)]; const territory = this.countries.find(c => c.name === randomAreaName); - if (!territory || territory.army <= 0) return; + if (!territory || territory.army <= 0 || territory.isExploring) return; // Don't attack exploring units const creature = encounterData.creatures[Math.floor(Math.random() * encounterData.creatures.length)]; - const title = "Creature Sighting"; - const message = `Your scouts report a wild ${creature.name} near your garrison of ${territory.army} troops at ${formatTerritoryName(territory.name)}.`; + const title = "Creature Sighting"; + + const army = territory.army; + const threat = creature.threat; + const tName = formatTerritoryName(territory.name); + const cName = `${creature.name}`; + + // Calculate Risk Assessment + let riskText, riskColor; + if (army >= threat * 1.5) { + riskText = "LOW RISK"; + riskColor = "var(--pip-color)"; // UI Theme Color + } else if (army >= threat) { + riskText = "MODERATE RISK"; + riskColor = "#ffcc00"; // Yellow + } else { + riskText = "HIGH RISK"; + riskColor = "#ff3333"; // Red + } + + // Flavor text variations + const flavors = [ + `Your scouts report a wild ${cName} near your garrison of ${army} troops at ${tName}.`, + `A terrifying ${cName} has been spotted stalking the perimeter of ${tName}, where ${army} of your troops are stationed.`, + `Frantic radio chatter from ${tName} indicates a ${cName} is encroaching on your encampment of ${army}.`, + `A dust cloud approaches ${tName}. Your ${army} defenders brace themselves as a ${cName} emerges from the wastes.`, + `Sentries at ${tName} have sounded the alarm! A ${cName} is prowling dangerously close to your ${army} soldiers.`, + `The local wildlife is getting restless. A ${cName} is currently menacing your ${army} troops garrisoned at ${tName}.`, + `A lone ${cName} has wandered into the patrol zone of your ${army} troops stationed in ${tName}.`, + `Your detachment of ${army} at ${tName} is requesting orders regarding a hostile ${cName} spotted nearby.`, + `Movement detected in the ruins of ${tName}. It's a ${cName}, and it's heading straight for your ${army} troops.`, + `The mutated fauna of ${tName} is acting up again. A ${cName} is threatening your garrison of ${army}.` + ]; + + const randomFlavor = flavors[Math.floor(Math.random() * flavors.length)]; + + const message = `${randomFlavor}

[ TACTICAL ASSESSMENT: ${riskText} ]`; + const choices = [ - { id: "attack", text: "[Attack] Try to eliminate the threat." }, - { id: "avoid", text: "[Avoid] The risk isn't worth it." } + { id: "attack", text: "[Attack] Engage and eliminate the threat." }, + { id: "avoid", text: "[Avoid] Attempt to hide and let it pass." } ]; - const onChoiceCallback = (decision) => { - if (decision === 'attack') { - // Log the action but DO NOT await it. - this.logAction(`You chose to engage the ${creature.name} at ${formatTerritoryName(territory.name)}!`, true); + this.modalIsOpen = true; + + const onChoiceCallback = (decision) => { + let forcedFightText = ""; + let isFighting = true; + + if (decision === 'avoid') { + // 80% chance to successfully hide + if (Math.random() < 0.80) { + 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.`; + } else { + forcedFightText = `Your troops tried to hide, but the ${creature.name} picked up their scent and ambushed them!

`; + this.logAction(`[ AMBUSH ] A ${creature.name} sniffed out your hiding troops at ${tName} and forced an engagement!`, true); + } + } + + if (isFighting) { const army = territory.army; const threat = creature.threat; - const damagePercent = threat / (army + threat); - let casualties = Math.floor(army * damagePercent); - - if (army > 1 && army - casualties < 1) { - casualties = army - 1; - } else if (army === 1 && casualties > 0) { - casualties = 1; - } - - if (casualties > 0) { - territory.army -= casualties; - this.player.army -= casualties; - // Log the action but DO NOT await it. - this.logAction(`Your soldiers fought bravely, but suffered ${casualties} casualties.`, true); - return `The ${creature.name} was driven off, but you lost ${casualties} troops in the fight.`; + + // Player wins ties (army >= threat) + if (army >= threat) { + let lootText = ""; + let logText = ""; + let roll = Math.random(); + + // Hostage Reward for Humanoids + if (creature.isHumanoid && roll < 0.40) { + let hostages = Math.floor(Math.random() * 3) + 1; + territory.army += hostages; + this.player.army += hostages; + lootText = `They rescued ${hostages} wastelanders being held captive, who have joined your ranks!`; + logText = `[ VICTORY ] The ${creature.name} was slain at ${tName}. Rescued ${hostages} wastelanders who joined the garrison!`; + } + // Stimpak Reward + else if (this.commandersEnabled && this.player.commander && this.player.commander.stimpaks < 3 && roll > 0.75) { + this.player.commander.stimpaks++; + let navInv = document.getElementById('nav-inv'); + if (navInv) navInv.classList.add('inv-pulse'); + lootText = `They salvaged a rare Stimpak from the creature's den!`; + logText = `[ VICTORY ] The ${creature.name} was slain at ${tName}. Salvaged a rare Stimpak from its den!`; + } + // Caps Reward + else { + let capsFound = Math.floor(Math.random() * 6) + 5; + if (this.wastelandEconomyActive) this.player.caps += capsFound; + lootText = `They recovered ${capsFound} Caps from the remains.`; + logText = `[ VICTORY ] The ${creature.name} was slain at ${tName}. Recovered ${capsFound} Caps from the area.`; + } + + // Log the exact victory outcome + this.logAction(logText, true); + return `${forcedFightText}A decisive victory! The ${creature.name} was eliminated with no casualties.

${lootText}`; } else { - // Log the action but DO NOT await it. - this.logAction(`Your garrison made short work of the ${creature.name} and suffered no losses.`); - return `A decisive victory! The ${creature.name} was eliminated with no casualties.`; + // Defeat/Casualties + let minRemaining = Math.max(1, Math.floor(army * 0.10)); // 10% or 1 unit minimum + let maxCasualties = army - minRemaining; + let casualties = Math.min(Math.ceil(threat), maxCasualties); + + if (casualties > 0) { + territory.army -= casualties; + this.player.army -= casualties; + // Log the exact losses + this.logAction(`[ CASUALTIES ] The ${creature.name} overwhelmed defenders at ${tName}! Lost ${casualties} troops before driving it off.`, true); + return `${forcedFightText}The ${creature.name} overwhelmed your forces! You suffered ${casualties} casualties before driving it away.`; + } else { + // Log battered survival + this.logAction(`[ SURVIVAL ] Defenders at ${tName} were battered by a ${creature.name} but suffered no permanent casualties.`); + return `${forcedFightText}The ${creature.name} battered your forces, but they managed to survive without permanent casualties.`; + } } - } else { // 'avoid' - // Log the action but DO NOT await it. - this.logAction(`You chose to avoid the ${creature.name}, leaving it to roam the wastes.`); - return `You wisely avoid the creature, and your troops remain safe.`; + } + }; + + Gamestate.resolveRadioEncounter = async function () { + if (this.player.areas.length === 0 || this.modalIsOpen) return; + + // Find a valid territory: not already exploring, army > 1 + const validTerritories = this.player.areas.map(a => this.countries.find(c => c.name === a)).filter(c => c && c.army > 1 && !c.isExploring); + if (validTerritories.length === 0) return; + + const territory = validTerritories[Math.floor(Math.random() * validTerritories.length)]; + + // Pick an encounter type + const types = ["location", "person", "container"]; + const type = types[Math.floor(Math.random() * types.length)]; + + let poiName = ""; + let actionVerb = ""; + + if (type === "location") { + poiName = encounterData.genericLocations[Math.floor(Math.random() * encounterData.genericLocations.length)]; + actionVerb = "explore the area"; + } else if (type === "person") { + poiName = encounterData.people[Math.floor(Math.random() * encounterData.people.length)]; + actionVerb = "approach and investigate"; + } else { + poiName = encounterData.containers[Math.floor(Math.random() * encounterData.containers.length)]; + actionVerb = "attempt to secure and open it"; + } + + const title = "Radio Transmission"; + const message = `>>> INCOMING TRANSMISSION <<<

Garrison forces at ${formatTerritoryName(territory.name)} report they have discovered ${poiName}.

Do you want to send a detachment to ${actionVerb}? This will lock the territory down for 2 turns and reduce their defenses by 15%.`; + + const choices = [ + { id: "explore", text: "[Investigate] Send the detachment." }, + { id: "ignore", text: "[Ignore] Hold the line. Do not engage." } + ]; + + this.modalIsOpen = true; + + const onChoiceCallback = (decision) => { + if (decision === 'explore') { + // Lock the troops into the exploring state + territory.isExploring = true; + territory.exploreTurnsLeft = 2; + territory.exploreType = type; + territory.explorePOI = poiName; + + this.logAction(`[ EXPEDITION ] Troops at ${formatTerritoryName(territory.name)} have begun investigating ${poiName}. They will report back in 2 turns.`, true); + return `Expedition launched. The garrison is now vulnerable.`; + } else { + this.logAction(`[ EXPEDITION ] You ordered the garrison at ${formatTerritoryName(territory.name)} to hold their position and ignore the ${poiName}.`); + return `Transmission ended. The garrison will maintain their post.`; } }; await this.showEncounterModal(title, message, choices, onChoiceCallback); + this.modalIsOpen = false; + this.updateInfo(); + this.drawMapText(); + }; - // Update the UI after the modal has closed + + await this.showEncounterModal(title, message, choices, onChoiceCallback); + this.modalIsOpen = false; this.updateInfo(); this.drawMapText(); }; Gamestate.triggerEncounterCheck = async function (triggerType) { - // --- THIS IS THE NEW CHECK --- // If encounters are disabled in the game settings, do nothing. if (!this.encountersEnabled) return; let chance = 0; if (triggerType === 'start_of_turn') { - chance = 0.08; // 8% chance at the start of a turn + chance = 1.0; // TEST MODE: 100% chance to trigger! } else if (triggerType === 'post_conquest') { - chance = 0.15; // 15% chance after conquering a territory + chance = 1.0; // TEST MODE: 100% chance to trigger! } if (Math.random() < chance) { if (triggerType === 'start_of_turn') { - await this.resolveCreatureEncounter(); + await this.resolveRadioEncounter(); } - // We will add post-conquest story logic here in a future step. + // Post-conquest will go here shortly! } }; @@ -9919,7 +10419,8 @@ Gamestate.openDiplomacy = function (targetName) { } // Log it for good measure - if (this.logAction) this.logAction("💾 SYSTEM: Game state successfully backed up to local memory.", true); + 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); @@ -9997,7 +10498,7 @@ Gamestate.openDiplomacy = function (targetName) { } // 10. Announce success! - if (this.logAction) this.logAction(`💾 SYSTEM: Successfully restored Save File (Day ${this.turn}).`, true); + 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); } @@ -10051,7 +10552,8 @@ Gamestate.openDiplomacy = function (targetName) { // --- BULLETPROOF SVG OVERLAP FIX (V2) --- Gamestate.fixMapTextOrder = function () { - let svg = document.querySelector('svg'); + let svg = document.querySelector('.area').closest('svg'); + // Create a master layer that sits on top of absolutely everything let topLayer = document.createElementNS("http://www.w3.org/2000/svg", "g");