/*=============================== ROBCO RISK ===============================*/ // --- DYNAMIC HAZARD CSS INJECTION --- if (!document.getElementById('radstorm-styles')) { let style = document.createElement('style'); style.id = 'radstorm-styles'; style.innerHTML = ` @keyframes radstormWarn { 0% { fill: #ffcc00; filter: drop-shadow(0 0 5px #ffcc00); } 100% { fill: #ff8800; filter: drop-shadow(0 0 15px #ff8800); } } @keyframes radstormAct { 0% { fill: #39ff14; filter: drop-shadow(0 0 10px #39ff14); } 100% { fill: #00ff00; filter: drop-shadow(0 0 25px #00ff00); } } .radstorm-warning { animation: radstormWarn 1s infinite alternate !important; opacity: 0.85; } .radstorm-active { animation: radstormAct 0.4s infinite alternate !important; opacity: 0.95; } `; document.head.appendChild(style); } const continents = [ { areas: ["indonesia", "new_guinea", "eastern_australia", "western_australia"], name: "The Southern Wastes", bonus: 2 }, { areas: ["brazil", "peru", "venezuela", "argentina"], name: "Amazonian Wastes", bonus: 2 }, { areas: ["egypt", "north_africa", "east_africa", "congo", "south_africa", "madagascar"], name: "Saharan Wastes", bonus: 3 }, { areas: ["iceland", "uk", "scandinavia", "northern_europe", "western_europe", "ukraine", "southern_europe"], name: "European Commonwealth", bonus: 5 }, { areas: ["central_america", "eastern_us", "western_us", "quebec", "ontario", "alberta", "northwest_territory", "alaska", "greenland"], name: "Capital Wasteland", bonus: 5 }, { areas: ["middle_east", "afghanistan", "ural", "siberia", "irkutsk", "yakutsk", "kamchatka", "mongolia", "japan", "china", "siam", "india"], name: "The Great Wastes", bonus: 7 } ]; const countries = [ {name: "indonesia", continent: "The Southern Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["siam", "western_australia", "new_guinea"]}, {name: "new_guinea", continent: "The Southern Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["indonesia", "eastern_australia", "western_australia"]}, {name: "eastern_australia", continent: "The Southern Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["western_australia", "new_guinea"]}, {name: "western_australia", continent: "The Southern Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["eastern_australia", "new_guinea", "indonesia"]}, {name: "ural", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["ukraine", "siberia", "afghanistan", "china"]}, {name: "siberia", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["ural", "mongolia", "yakutsk", "irkutsk", "china"]}, {name: "afghanistan", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["ukraine", "ural", "middle_east", "china", "india"]}, {name: "irkutsk", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["yakutsk", "siberia", "kamchatka", "mongolia"]}, {name: "yakutsk", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["irkutsk", "siberia", "kamchatka"]}, {name: "kamchatka", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["alaska", "yakutsk", "japan", "irkutsk", "mongolia"]}, {name: "middle_east", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["ukraine", "afghanistan", "india", "egypt", "east_africa", "southern_europe"]}, {name: "india", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["middle_east", "siam", "afghanistan", "china"]}, {name: "siam", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["indonesia", "india", "china"]}, {name: "china", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["ural", "siberia", "afghanistan", "mongolia", "siam", "india"]}, {name: "mongolia", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["irkutsk", "siberia", "kamchatka", "china", "japan"]}, {name: "japan", continent: "The Great Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["kamchatka", "mongolia"]}, {name: "egypt", continent: "Saharan Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["middle_east", "southern_europe", "north_africa", "east_africa"]}, {name: "north_africa", continent: "Saharan Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["egypt", "southern_europe", "western_europe", "east_africa", "congo", "brazil"]}, {name: "east_africa", continent: "Saharan Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["middle_east", "egypt", "north_africa", "congo", "madagascar", "south_africa"]}, {name: "congo", continent: "Saharan Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["south_africa", "north_africa", "east_africa"]}, {name: "south_africa", continent: "Saharan Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["congo", "madagascar", "east_africa"]}, {name: "madagascar", continent: "Saharan Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["south_africa", "east_africa"]}, {name: "brazil", continent: "Amazonian Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["peru", "argentina", "north_africa", "venezuela"]}, {name: "peru", continent: "Amazonian Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["brazil", "argentina", "venezuela"]}, {name: "argentina", continent: "Amazonian Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["brazil", "peru"]}, {name: "venezuela", continent: "Amazonian Wastes", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["brazil", "peru", "central_america"]}, {name: "iceland", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["greenland", "uk", "scandinavia"]}, {name: "scandinavia", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["iceland", "uk", "ukraine", "northern_europe"]}, {name: "northern_europe", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["ukraine", "uk", "scandinavia", "southern_europe", "western_europe"]}, {name: "western_europe", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["north_africa", "uk", "northern_europe", "southern_europe"]}, {name: "southern_europe", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["north_africa", "egypt", "northern_europe", "western_europe", "middle_east", "ukraine"]}, {name: "uk", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["western_europe", "iceland", "northern_europe", "scandinavia"]}, {name: "ukraine", continent: "European Commonwealth", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["scandinavia", "ural", "northern_europe", "southern_europe", "afghanistan", "middle_east"]}, {name: "greenland", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["iceland", "quebec", "ontario", "northwest_territory"]}, {name: "central_america", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["venezuela", "eastern_us", "western_us"]}, {name: "eastern_us", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["central_america", "western_us", "ontario", "quebec"]}, {name: "western_us", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["central_america", "eastern_us", "ontario", "alberta"]}, {name: "quebec", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["eastern_us", "ontario", "greenland"]}, {name: "ontario", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["eastern_us", "western_us", "quebec", "alberta", "northwest_territory", "greenland"]}, {name: "alberta", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["western_us", "ontario", "northwest_territory", "alaska"]}, {name: "northwest_territory", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["alberta", "ontario", "greenland", "alaska"]}, {name: "alaska", continent: "Capital Wasteland", owner: "none", color:"#1a1a1a", "army": 0, neighbours: ["alberta", "northwest_territory", "kamchatka"]} ]; const wastelandEncounters = [ "Spotted a Vertibird flying low over the wastes.", "Radio picked up a strange distress signal. Probably a raider trap.", "Caravan guards chased off a pack of wild Mongrels.", "Another settlement needs our help. Marking it on the global map.", "Traded a handful of mutfruit for some polished 5.56 rounds.", "Super Mutants spotted dragging captives into a metro tunnel.", "Avoided a Radscorpion nest near the old irradiated crater.", "Brahmin stampede delayed supply lines by three hours.", "A wandering Eyebot is blasting patriotic music nearby.", "Dust storm rolling in. Visibility dropping to near zero.", "Picked up a HAM radio broadcast from KQ4JZM... Static approaching.", "Scavenged an abandoned pickup truck. The tonneau cover was surprisingly intact.", "Found an old pre-war safe containing detailed, multi-tiered bug-out lists.", "Spotted survivors establishing a security perimeter around a ruined church.", "Hacked a terminal and found a manuscript for 'SurvivalSOS Fundamentals of Survival' by Joseph Howard." ]; const combatFlavors = [ "while they were complaining about the radioactive heat.", "after distracting their guards with a loaded Brahmin.", "by unleashing a swarm of Cazadores on their flank.", "while they were trying to hack a Novice terminal.", "after hitting them with a Fat Man. Overkill, but effective.", "catching them mid-game of Caravan and flipping the table.", "storming the base while the commander was literally in the shower.", "distracting the guards with a holotape of 'Butcher Pete'.", "finding the leader trying to repair a toaster and hitting them with it.", "walking in the front door because the guards were asleep on Mentats." ]; const basePlayers = [ { "name": "Elder Lyons", "country": "Brotherhood of Steel", "color": "#0088ff", "army": 10, "reserve": 10, "areas": [], "bonus": 2, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": false }, { "name": "President Eden", "country": "Enclave", "color": "#ff003c", "army": 20, "reserve": 20, "areas": [], "bonus": 2, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": false }, { "name": "Overlord", "country": "Super Mutants", "color": "#ffaa00", "army": 20, "reserve": 20, "areas": [], "bonus": 2, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": false }, { "name": "Boss", "country": "Raiders", "color": "#ff00ff", "army": 20, "reserve": 20, "areas": [], "bonus": 2, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": false }, { "name": "Protector Casdin", "country": "Outcasts", "color": "#00ff00", "army": 20, "reserve": 20, "areas": [], "bonus": 2, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": false }, { "name": "Sonora Cruz", "country": "Regulators", "color": "#ffff00", "army": 20, "reserve": 20, "areas": [], "bonus": 2, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": false } ]; const themeFactions = { "fo3": [ { name: "Elder Lyons", country: "Brotherhood of Steel" }, { name: "President Eden", country: "The Enclave" }, { name: "Overlord", country: "Vault 87 Mutants" }, { name: "Flak", country: "Wasteland Raiders" }, { name: "Protector Casdin", country: "BOS Outcasts" }, { name: "Reilly", country: "Reilly's Rangers" } ], "fnv": [ { name: "General Oliver", country: "New California Republic" }, { name: "Caesar", country: "Caesar's Legion" }, { name: "Mr. House", country: "New Vegas Securitrons" }, { name: "Elder McNamara", country: "Mojave Brotherhood" }, { name: "Papa Khan", country: "Great Khans" }, { name: "Motor-Runner", country: "The Fiends" } ] }; const cardTypes = ["Nuka-Cola Cap", "Sunset Sarsaparilla Cap", "Quantum Cap"]; let deck = []; let tradeCount = 0; function generateDeck() { deck = []; countries.forEach((country, index) => { deck.push({ country: country.name, type: cardTypes[index % 3] }); }); deck.push({ country: "Wild", type: "Wild" }); deck.push({ country: "Wild", type: "Wild" }); shuffle(deck); } function getTradeBonus() { tradeCount++; if (tradeCount <= 5) return 2 + (tradeCount * 2); return 15 + ((tradeCount - 6) * 5); } function shuffle(array) { var currentIndex = array.length, temporaryValue, randomIndex; while (0 !== currentIndex) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } const infoName = Array.from(document.getElementsByClassName('country')); const infoLeader = Array.from(document.getElementsByClassName('leader')); 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 modal = document.querySelector('#start-modal'); const reserveDisplay = document.querySelector('#reserve'); const chosenLeader = document.querySelector('#chosen-leader'); const chosenCountry = document.querySelector('#chosen-country'); const submitName = document.querySelector('#submit-name'); const winModal = document.querySelector('#win-modal'); const winMessage = document.querySelector('.win-message'); const playAgain = document.querySelector('#play-again'); const playerName = document.querySelector('.player-name'); const playerCountry = document.querySelector('.player-country'); const restart = document.querySelector('#restart'); const playerPanel = document.querySelector('.player-panel'); const infoPanel = document.querySelector('.info'); const turnInfo = document.querySelector('.turn-info'); const turnInfoMessage = document.querySelector('.turn-info-message'); const end = document.querySelector('#end'); const combatLog = document.getElementById('combat-log'); const turboToggle = document.getElementById('turbo-toggle'); const helpBtn = document.querySelector('#help-btn'); const helpModal = document.querySelector('#help-modal'); const closeHelpBtn = document.querySelector('#close-help-btn'); let Gamestate = {}; Gamestate.logQueue = []; Gamestate.isLogging = false; Gamestate.logAction = function(message, isImportant = false) { return new Promise(resolve => { this.logQueue.push({message, isImportant, resolve}); this.processLogQueue(); }); } Gamestate.processLogQueue = function() { if(this.isLogging || this.logQueue.length === 0) return; this.isLogging = true; let logData = this.logQueue.shift(); if(!combatLog) { this.isLogging = false; logData.resolve(); return; } let entry = document.createElement('div'); entry.className = 'log-entry' + (logData.isImportant ? ' important' : ''); let now = new Date(); let timeString = now.toLocaleTimeString([], {hour12: false}); let fullText = `> [${timeString}] ${logData.message}`; combatLog.appendChild(entry); if (combatLog.children.length > 50) combatLog.removeChild(combatLog.firstChild); let i = 0; entry.textContent = ""; let typeSpeed = turboToggle && turboToggle.checked ? 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; if (i >= fullText.length) { clearInterval(typeInterval); setTimeout(() => { this.isLogging = false; logData.resolve(); this.processLogQueue(); }, 200); } }, typeSpeed); } } Gamestate.injectHolidayEvents = function() { let today = new Date(); let currentYear = today.getFullYear(); let todayTime = today.getTime(); const daysToMs = (days) => days * 24 * 60 * 60 * 1000; function getFloatingDate(year, month, occurrence, dayOfWeek) { let d = new Date(year, month, 1); let day = d.getDay(); let diff = dayOfWeek - day; if (diff < 0) diff += 7; let date = diff + 1 + (occurrence - 1) * 7; if (occurrence === 5) { let dTemp = new Date(year, month, date); if (dTemp.getMonth() !== month) date -= 7; } return new Date(year, month, date); } function getEaster(year) { let f = Math.floor, G = year % 19, C = f(year / 100); let H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30; let I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)); let J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7; let L = I - J, month = 3 + f((L + 40) / 44), day = L + 28 - 31 * f(month / 4); return new Date(year, month - 1, day); } [currentYear - 1, currentYear, currentYear + 1].forEach(year => { let holidays = [ { name: "New Year's Day", date: new Date(year, 0, 1), msgs: ["Resolutions for the New Year: Survive. Don't mutate.", "Someone set off a Fat Man to celebrate the New Year. Typical."] }, { name: "MLK Day", date: getFloatingDate(year, 0, 3, 1), msgs: ["A cracked terminal displays an old speech about equality. Rare sentiment these days.", "Scavenged a pre-war holotape preaching peace and brotherhood."] }, { name: "Valentine's Day", date: new Date(year, 1, 14), msgs: ["Found a skeleton clutching a faded box of Cram and a diamond ring.", "Raiders were spotted sharing a heart-shaped box of... questionable meat."] }, { name: "St. Patrick's Day", date: new Date(year, 2, 17), msgs: ["Discovered a hidden stash of perfectly preserved Shamrock Gwinnett Ale.", "Someone painted a Power Armor helmet bright green. Subtle."] }, { name: "Easter", date: getEaster(year), msgs: ["Found a clutch of colorful Deathclaw eggs. Best to leave them alone.", "Spotted a wastelander in a pre-war rabbit mascot suit. Terrifying."] }, { name: "Mother's Day", date: getFloatingDate(year, 4, 2, 0), msgs: ["Unearthed a heartfelt pre-war letter addressed to 'Mom'.", "Found a faded holotape of a family's last Mother's day dinner."] }, { name: "Memorial Day", date: getFloatingDate(year, 4, 5, 1), msgs: ["A moment of silence for the Anchorage veterans at a ruined memorial.", "Found a rusted set of dog tags and a folded flag."] }, { name: "Juneteenth", date: new Date(year, 5, 19), msgs: ["A settlement radio is broadcasting songs of freedom and emancipation.", "Found a pre-war terminal documenting the end of slavery. A reminder of better ideals."] }, { name: "Father's Day", date: getFloatingDate(year, 5, 3, 0), msgs: ["Found a dusty 'World's Best Dad' mug next to a hunting rifle.", "Unearthed a broken pocket watch with 'To Dad' engraved on the back."] }, { name: "Independence Day", date: new Date(year, 6, 4), msgs: ["Boomers are launching fireworks. Or artillery. Hard to tell.", "A stray eyebot is blasting the Star-Spangled Banner at max volume."] }, { name: "Labor Day", date: getFloatingDate(year, 8, 1, 1), msgs: ["Protectrons are stuck in a holiday loop, demanding fair wages.", "Found an old union strike poster in a ruined factory."] }, { name: "Columbus Day", date: getFloatingDate(year, 9, 2, 1), msgs: ["Raiders claim they 'discovered' a settlement that was already occupied.", "Found a pre-war globe. So much of it is just glowing craters now."] }, { name: "The Great War", date: new Date(year, 9, 23), msgs: ["October 23rd. The day the world ended. The air feels heavier today.", "Found a stopped watch. 9:47 AM. Never forget."] }, { name: "Halloween", date: new Date(year, 9, 31), msgs: ["Feral ghouls are looking extra festive today. Still deadly, though.", "Found a pristine plastic pumpkin bucket full of bottle caps."] }, { name: "Veterans Day", date: new Date(year, 10, 11), msgs: ["Found a pristine pre-war military uniform folded neatly in a footlocker.", "A lone bugler is playing Taps somewhere in the ruins."] }, { name: "Thanksgiving Day", date: getFloatingDate(year, 10, 4, 4), msgs: ["Traders are roasting a two-headed Radturkey. Smells like radiation and sage.", "Settlers are giving thanks for another year of not being eaten."] }, { name: "Christmas Eve", date: new Date(year, 11, 24), msgs: ["Spotted a sleigh pulled by Radstags in the distance. Probably hallucinations.", "A lone radio station is playing a static-filled version of Silent Night."] }, { name: "Christmas Day", date: new Date(year, 11, 25), msgs: ["A protectron wrapped in tinsel is wishing everyone a 'Merry Christmas' before firing.", "Found a pristine snow globe. A tiny, un-nuked world in glass."] }, { name: "New Year's Eve", date: new Date(year, 11, 31), msgs: ["Raiders are stockpiling explosives for midnight. Standard procedure.", "The last hours of the year. Let's hope the next one is less irradiated."] } ]; holidays.forEach(holiday => { let hTime = holiday.date.getTime(); let startWindow = hTime - daysToMs(4); let endWindow = hTime + daysToMs(2) + (daysToMs(1) - 1); if (todayTime >= startWindow && todayTime <= endWindow) { holiday.msgs.forEach(msg => { if (!wastelandEncounters.includes(msg)) { wastelandEncounters.push(msg); } }); } }); }); } Gamestate.init = function(){ if (winModal) winModal.style.display = "none"; if (!map) return; if (modal) modal.style.display = "block"; helpBtn?.addEventListener('click', (e) => { e.preventDefault(); if (helpModal) helpModal.style.display = 'block'; }); closeHelpBtn?.addEventListener('click', () => { if (helpModal) helpModal.style.display = 'none'; }); this.injectHolidayEvents(); submitName?.addEventListener('click', this.start.bind(this)); map?.addEventListener('mousedown', this.handleClick.bind(this)); end?.addEventListener('click', this.handleEndTurn.bind(this)); playAgain?.addEventListener('click', this.restart.bind(this)); restart?.addEventListener('click', () => { let confirmModal = document.getElementById('confirm-restart-modal'); if (confirmModal) confirmModal.style.display = 'flex'; }); 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'; }); document.getElementById('view-cards-btn')?.addEventListener('click', () => { let cardsModal = document.getElementById('cards-modal'); if (cardsModal) cardsModal.style.display = 'flex'; this.renderCards(); }); document.getElementById('close-cards-btn')?.addEventListener('click', () => { let cardsModal = document.getElementById('cards-modal'); if (cardsModal) cardsModal.style.display = 'none'; }); document.getElementById('trade-btn')?.addEventListener('click', () => { this.executeTrade(); }); } Gamestate.updateButtonText = function() { if (!end) return; if (this.stage === "Fortify") { end.textContent = "Deploy Troops"; end.style.opacity = "0.5"; end.style.pointerEvents = "none"; } else if (this.stage === "Battle") { end.textContent = "End Attack Phase"; end.style.opacity = "1"; end.style.pointerEvents = "auto"; } else if (this.stage === "Maneuver") { end.textContent = "End Turn"; end.style.opacity = "1"; end.style.pointerEvents = "auto"; } else if (this.stage === "AI Turn") { end.textContent = "AI is thinking..."; end.style.opacity = "0.5"; end.style.pointerEvents = "none"; } } Gamestate.start = async function(){ if(combatLog) combatLog.innerHTML = ""; this.logQueue = []; this.isLogging = false; if (end) end.style.pointerEvents = "auto"; if (map) map.style.pointerEvents = "auto"; if (modal) modal.style.display = "none"; if (playerPanel) playerPanel.style.display = "flex"; if (infoPanel) infoPanel.style.display = "flex"; let optRadstorms = document.getElementById('opt-radstorms') && document.getElementById('opt-radstorms').checked; let optHorrors = document.getElementById('opt-horrors') && document.getElementById('opt-horrors').checked; this.hazardsEnabled = optRadstorms; this.radstorm = { state: 'none', timer: 0, cooldown: Math.floor(Math.random() * 11) + 5, areas: [] }; this.countries = JSON.parse(JSON.stringify(countries)); this.countries.forEach(c => { let el = document.getElementById(c.name); if (el) { el.classList.remove('radstorm-warning'); el.classList.remove('radstorm-active'); } }); let themeDropdown = document.getElementById('chosen-theme'); let selectedTheme = themeDropdown ? themeDropdown.value : "fo3"; document.body.classList.remove('theme-fnv'); if (selectedTheme === "fnv") document.body.classList.add('theme-fnv'); this.players = JSON.parse(JSON.stringify(basePlayers)); let factionList = themeFactions[selectedTheme]; for(let i = 0; i < 6; i++) { this.players[i].name = factionList[i].name; this.players[i].country = factionList[i].country; } if (optHorrors) { this.players.push({ "name": "Wasteland Horrors", "country": "Feral Ghouls & Deathclaws", "color": "#333333", "army": 0, "reserve": 0, "areas": [], "bonus": 0, "alive": true, "cards": [], "conqueredThisTurn": false, "isNeutral": true }); } this.aiTurn = false; this.gameOver = false; this.prevCountry = null; this.prevTarget = null; this.turn = 1; this.stage = "Fortify"; this.playerTroopsPlaced = 0; this.player = this.players[0]; let diffSelect = document.getElementById('chosen-difficulty'); this.difficulty = diffSelect ? diffSelect.value : "Normal"; if (chosenLeader && chosenCountry) { this.players[0].name = chosenLeader.value; this.players[0].country = chosenCountry.value; if (playerName) playerName.textContent = chosenLeader.value; if (playerCountry) playerCountry.textContent = chosenCountry.value; } generateDeck(); tradeCount = 0; this.players.forEach(p => { p.cards = []; p.conqueredThisTurn = false; }); let cardCount = document.getElementById('card-count'); if (cardCount) cardCount.textContent = "0"; if(this.prevTarget) this.prevTarget.classList.remove('flash'); for(let j = 0; j < this.players.length; j++){ if(infoName[j]) infoName[j].innerHTML = this.players[j].country; if(infoLeader[j]) infoLeader[j].innerHTML = this.players[j].name; if(infoName[j]) infoName[j].parentElement.classList.remove('defeated'); if(bar[j]) bar[j].style.background = this.players[j].color; } this.stage = "Fortify"; this.updateButtonText(); if (turnInfoMessage) turnInfoMessage.textContent = "Click on your own areas to place reserve troops"; this.players.forEach(p => { p.reserve = p.isNeutral ? 0 : 20; p.army = 0; p.areas = []; }); let shuffledAreas = shuffle([...areas]); shuffledAreas.forEach((area, i) => { let player = this.players[i % this.players.length]; let country = this.countries.find(c => c.name === area.id); country.owner = player.name; country.color = player.color; country.army = player.isNeutral ? 8 : 1; player.areas.push(country.name); player.army += country.army; if (!player.isNeutral) player.reserve -= 1; }); this.players.forEach(player => { while(player.reserve > 0 && !player.isNeutral) { let randomArea = player.areas[Math.floor(Math.random() * player.areas.length)]; let country = this.countries.find(c => c.name === randomArea); country.army += 1; player.army += 1; player.reserve -= 1; } }); this.countries.forEach(country => { let areaOnMap = document.getElementById(country.name); if (areaOnMap) { areaOnMap.style.fill = country.color; if (areaOnMap.nextElementSibling) areaOnMap.nextElementSibling.textContent = country.army; } }); this.player.reserve = this.unitBonus(this.player, 0); this.player.army += this.player.reserve; this.updateInfo(); await this.logAction("RobCo OS Booted. Wasteland Simulation Online.", true); if (optRadstorms) await this.logAction(">>> SYSTEM: METEOROLOGICAL HAZARD SUBSYSTEM LOADED."); if (optHorrors) await this.logAction(">>> SYSTEM: NEUTRAL THREAT SCANNER ACTIVE."); } Gamestate.showToast = function(message, bgColor = "#222") { let toast = document.getElementById("toast"); if(toast) { toast.textContent = message; toast.style.backgroundColor = bgColor; toast.className = "toast show"; if (this.toastTimeout) clearTimeout(this.toastTimeout); this.toastTimeout = setTimeout(function(){ toast.className = toast.className.replace("show", ""); }, 3500); } } Gamestate.handleClick = function(e){ if (this.aiTurn) return; if(this.stage === "Fortify"){ this.addArmy(e); } else if(this.stage === "Battle"){ this.attack(e); } else if(this.stage === "Maneuver"){ this.maneuver(e); } } Gamestate.win = function(player){ if (winMessage) { winMessage.textContent = player.name; winMessage.style.color = player.color; } if (winModal) winModal.style.display = "block"; } Gamestate.restart = function(){ if (modal) modal.style.display = "flex"; if (winModal) winModal.style.display = "none"; } Gamestate.updateInfo = function(){ if (turnInfo) turnInfo.textContent = this.stage; let totalArmy = 0; this.players.forEach(player => { totalArmy += player.army }); let sortedPlayers = [...this.players].sort((a, b) => b.army - a.army); this.players.forEach((player, i) => { let infoBox = infoName[i] ? infoName[i].parentElement : null; if (!infoBox) return; if (player.alive) { if (player.isNeutral) { if (infoIncome[i]) infoIncome[i].parentElement.style.display = "none"; } else { if (infoIncome[i]) { infoIncome[i].innerHTML = player.bonus; infoIncome[i].parentElement.style.display = "block"; } } if (bar[i]) bar[i].style.width = (player.army / totalArmy) * 230 + 'px'; } else { if (infoIncome[i]) { infoIncome[i].innerHTML = "OFFLINE"; infoIncome[i].parentElement.style.display = "block"; } if (bar[i]) bar[i].style.width = "0px"; } let rank = sortedPlayers.findIndex(p => p.name === player.name); infoBox.style.display = "block"; infoBox.style.order = rank; }); if (this.players.length === 6 && infoName[6] && infoName[6].parentElement) { infoName[6].parentElement.style.display = "none"; } let helpBtnEl = document.getElementById('help-btn'); if (helpBtnEl) helpBtnEl.style.order = "998"; let restartBtn = document.getElementById('restart'); if (restartBtn) restartBtn.style.order = "999"; let cardCount = document.getElementById('card-count'); if (cardCount) cardCount.textContent = this.player.cards.length; if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve; let viewCardsBtn = document.getElementById('view-cards-btn'); if (viewCardsBtn) { if (this.getBestTrade(this.player.cards)) { viewCardsBtn.textContent = "OPEN STASH"; viewCardsBtn.style.opacity = "1"; viewCardsBtn.style.pointerEvents = "auto"; viewCardsBtn.classList.add('ready-to-trade'); } else { viewCardsBtn.textContent = this.player.cards.length > 0 ? "NO ELIGIBLE SETS" : "STASH EMPTY"; viewCardsBtn.style.opacity = "0.5"; viewCardsBtn.style.pointerEvents = "none"; viewCardsBtn.classList.remove('ready-to-trade'); } } let hpFill = document.getElementById('hp-fill'); let apFill = document.getElementById('ap-fill'); if (hpFill && apFill && totalArmy > 0 && this.player.alive) { let hpPercentage = Math.min(100, (this.player.areas.length / 24) * 100); hpFill.style.width = hpPercentage + "%"; if (this.player.areas.length <= 5) { hpFill.style.background = "#ff0000"; hpFill.style.boxShadow = "0 0 10px rgba(255, 0, 0, 0.8)"; } else { hpFill.style.background = "var(--pip-color)"; hpFill.style.boxShadow = "var(--pip-glow)"; } let apPercentage = 0; if (this.stage === "Fortify") { let maxReserve = Math.max(this.player.reserve + this.playerTroopsPlaced, 1); apPercentage = (this.player.reserve / maxReserve) * 100; } else if (this.stage === "Battle") { let currentStrikeForce = 0; let validAttacks = 0; let ownedTerritories = this.countries.filter(c => c.owner === this.player.name); ownedTerritories.forEach(t => { if (t.army > 1) { let hasEnemyNeighbor = t.neighbours.some(n => { let neighborCountry = this.countries.find(x => x.name === n); return neighborCountry && neighborCountry.owner !== this.player.name; }); if (hasEnemyNeighbor) { currentStrikeForce += (t.army - 1); validAttacks++; } } }); if (validAttacks === 0) { apPercentage = 0; } else { if (this.lastStage !== "Battle") { this.initialStrikeForce = currentStrikeForce; } if (currentStrikeForce > (this.initialStrikeForce || 1)) { this.initialStrikeForce = currentStrikeForce; } apPercentage = Math.min(100, (currentStrikeForce / Math.max(this.initialStrikeForce, 1)) * 100); } } else if (this.stage === "Maneuver") { let canManeuver = false; let ownedTerritories = this.countries.filter(c => c.owner === this.player.name); for (let t of ownedTerritories) { if (t.army > 1 && t.neighbours.some(n => { let neighborCountry = this.countries.find(x => x.name === n); return neighborCountry && neighborCountry.owner === this.player.name; })) { canManeuver = true; break; } } if (canManeuver) { apPercentage = this.maneuverSource ? 0 : 100; } else { apPercentage = 0; } } else { apPercentage = 0; } apFill.style.width = apPercentage + "%"; if (apPercentage <= 0) { apFill.style.opacity = "0"; apFill.style.visibility = "hidden"; } else { apFill.style.opacity = "1"; apFill.style.visibility = "visible"; } if (hpPercentage <= 0) { hpFill.style.opacity = "0"; hpFill.style.visibility = "hidden"; } else { hpFill.style.opacity = "1"; hpFill.style.visibility = "visible"; } } else if (!this.player.alive && hpFill && apFill) { hpFill.style.width = "0%"; hpFill.style.opacity = "0"; hpFill.style.visibility = "hidden"; apFill.style.width = "0%"; apFill.style.opacity = "0"; apFill.style.visibility = "hidden"; } if (this.radstorm && this.radstorm.areas) { this.radstorm.areas.forEach(areaName => { let el = document.getElementById(areaName); if (el) { if (this.radstorm.state === 'warning') el.classList.add('radstorm-warning'); if (this.radstorm.state === 'active') el.classList.add('radstorm-active'); } }); } this.lastStage = this.stage; } Gamestate.renderCards = function() { const list = document.getElementById('card-list'); if (!list) return; list.innerHTML = ''; this.selectedCards = []; this.player.cards.forEach((card, index) => { let cardEl = document.createElement('div'); cardEl.textContent = `${card.country} (${card.type})`; cardEl.className = 'risk-card'; cardEl.onclick = () => this.toggleCardSelection(cardEl, index); list.appendChild(cardEl); }); let tradeBtn = document.getElementById('trade-btn'); if (tradeBtn) { tradeBtn.disabled = true; tradeBtn.textContent = "SELECT 3 ELIGIBLE CAPS"; tradeBtn.classList.remove('ready-to-trade'); } } Gamestate.toggleCardSelection = function(element, index) { if (this.selectedCards.includes(index)) { this.selectedCards = this.selectedCards.filter(i => i !== index); element.classList.remove('selected'); } else if (this.selectedCards.length < 3) { this.selectedCards.push(index); element.classList.add('selected'); } let tradeBtn = document.getElementById('trade-btn'); if (tradeBtn) { let isValid = this.isValidTrade(); tradeBtn.disabled = !isValid; tradeBtn.textContent = isValid ? "SPEND CAPS FOR TROOPS" : "SELECT 3 ELIGIBLE CAPS"; if (isValid) { tradeBtn.classList.add('ready-to-trade'); } else { tradeBtn.classList.remove('ready-to-trade'); } } } Gamestate.isValidTrade = function() { if (this.selectedCards.length !== 3) return false; let types = this.selectedCards.map(i => this.player.cards[i].type); let wilds = types.filter(t => t === "Wild").length; let regularTypes = types.filter(t => t !== "Wild"); let uniqueTypes = new Set(regularTypes).size; return (uniqueTypes === 1 || uniqueTypes === 3 || wilds > 0); } Gamestate.getBestTrade = function(cards) { if (cards.length < 3) return null; for (let i=0; i c.type); let wilds = types.filter(t => t === "Wild").length; let regularTypes = types.filter(t => t !== "Wild"); let uniqueTypes = new Set(regularTypes).size; if (uniqueTypes === 1 || uniqueTypes === 3 || wilds > 0) return [i, j, k]; } } } return null; } Gamestate.executeTrade = async function() { if (this.aiTurn) return; // Prevent trading while AI is calculating if (this.isValidTrade()) { let bonus = getTradeBonus(); this.selectedCards.sort((a,b) => b-a).forEach(index => { deck.unshift(this.player.cards[index]); this.player.cards.splice(index, 1); }); this.player.reserve += bonus; this.player.army += bonus; if (this.stage === "Battle" || this.stage === "Maneuver" || this.stage === "AI Turn") { this.stage = "Fortify"; if (turnInfo) turnInfo.textContent = "Combat Phase"; if (turnInfoMessage) turnInfoMessage.textContent = "Bonus Received! Place your new troops."; this.updateButtonText(); } if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve; this.updateInfo(); await this.logAction(`SUPPLY DROP: ${this.player.name} spent Caps for +${bonus} troops!`, true); this.renderCards(); let tradeBtn = document.getElementById('trade-btn'); if (tradeBtn) { tradeBtn.disabled = true; tradeBtn.classList.remove('ready-to-trade'); } let cardsModal = document.getElementById('cards-modal'); if (cardsModal) cardsModal.style.display = 'none'; } } Gamestate.processRadstorm = async function() { if (!this.hazardsEnabled) return; if (!this.radstorm) { this.radstorm = { state: 'none', timer: 0, cooldown: Math.floor(Math.random() * 11) + 5, areas: [] }; } this.countries.forEach(c => { let el = document.getElementById(c.name); if (el) { el.classList.remove('radstorm-warning'); el.classList.remove('radstorm-active'); } }); if (this.radstorm.state === 'none') { this.radstorm.cooldown--; if (this.radstorm.cooldown <= 0) { this.radstorm.state = 'warning'; this.radstorm.timer = 2; let startCountry = this.countries[Math.floor(Math.random() * this.countries.length)]; this.radstorm.areas = [startCountry.name]; let targetCount = Math.floor(Math.random() * 4) + 1; while(this.radstorm.areas.length < targetCount) { let potential = []; this.radstorm.areas.forEach(a => { let c = this.countries.find(x => x.name === a); c.neighbours.forEach(n => { let neighborExists = this.countries.some(x => x.name === n); if (neighborExists && !this.radstorm.areas.includes(n) && !potential.includes(n)) potential.push(n); }); }); if (potential.length === 0) break; let nextArea = potential[Math.floor(Math.random() * potential.length)]; this.radstorm.areas.push(nextArea); } await this.logAction(`[ S.O.S ] WARNING: Severe Radstorm forming over ${this.radstorm.areas.length} territories! Evacuate immediately.`, true); } } else if (this.radstorm.state === 'warning') { this.radstorm.timer--; if (this.radstorm.timer <= 0) { this.radstorm.state = 'active'; this.radstorm.timer = Math.floor(Math.random() * 4) + 2; await this.logAction(`[ HAZARD ] The Radstorm has touched down! It will rage for ${this.radstorm.timer} days.`, true); } else { await this.logAction(`[ S.O.S ] Radstorm arriving in ${this.radstorm.timer} day(s)...`, true); } } if (this.radstorm.state === 'active') { let totalKilled = 0; this.radstorm.areas.forEach(areaName => { let c = this.countries.find(x => x.name === areaName); if (c.army > 1) { let dmgPercent = (Math.random() * 0.15) + 0.10; 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; let areaOnMap = document.getElementById(c.name); if (areaOnMap && areaOnMap.nextElementSibling) areaOnMap.nextElementSibling.textContent = c.army; } }); if (totalKilled > 0) { await this.logAction(`[ HAZARD ] Radstorm killed ${totalKilled} troops in the irradiated zone.`, true); } else { await this.logAction(`[ HAZARD ] Radstorm rages, but garrisons are already at minimum capacity.`); } this.radstorm.timer--; if (this.radstorm.timer <= 0) { this.radstorm.state = 'none'; this.radstorm.cooldown = Math.floor(Math.random() * 11) + 5; await this.logAction(`[ CLEAR ] The Radstorm has dissipated. Skies are clear for now.`, true); } } this.radstorm.areas.forEach(areaName => { let el = document.getElementById(areaName); if (el) { if (this.radstorm.state === 'warning') el.classList.add('radstorm-warning'); if (this.radstorm.state === 'active') el.classList.add('radstorm-active'); } }); } Gamestate.handleEndTurn = async function(){ if(this.aiTurn || this.player.reserve > 0){ return; } if (this.stage === "Battle") { let canManeuver = false; let owned = this.countries.filter(c => c.owner === this.player.name); for (let t of owned) { if (t.army > 1 && t.neighbours.some(n => this.countries.find(x => x.name === n).owner === this.player.name)) { canManeuver = true; break; } } if(this.prevTarget) this.prevTarget.classList.remove('flash'); this.prevCountry = null; this.prevTarget = null; if (canManeuver) { this.stage = "Maneuver"; this.maneuverSource = null; this.maneuverDest = null; this.updateButtonText(); if (turnInfo) turnInfo.textContent = "Maneuver Phase"; if (turnInfoMessage) turnInfoMessage.textContent = "Move troops between adjacent territories. Hold SHIFT to move all."; this.updateInfo(); return; } } if (this.player.conqueredThisTurn && deck.length > 0) { this.player.cards.push(deck.pop()); this.updateInfo(); if (this.getBestTrade(this.player.cards)) { await this.logAction("STASH FULL: Enough Caps collected to hire more troops."); } else { await this.logAction("SCAVENGED: Found a Bottle Cap after securing enemy territory."); } } this.player.conqueredThisTurn = false; this.aiTurn = true; this.updateButtonText(); if (map) map.style.pointerEvents = "none"; this.aiMove(); } Gamestate.unitBonus = function(player, i){ if (!player.alive || player.isNeutral) return 0; player.bonus = 0; player.bonus += Math.floor(player.areas.length / 3); player.bonus += this.continentBonus(player); if(player.bonus < 3){ player.bonus = 3; } if (infoIncome[i]) infoIncome[i].innerHTML = player.bonus; return player.bonus; } Gamestate.continentBonus = function(player){ if (player.isNeutral) return 0; let bonus = 0; continents.forEach( continent => { let ownsContinent = continent.areas.every(area => player.areas.includes(area)); if(ownsContinent){ bonus += continent.bonus; } }) return bonus; } Gamestate.addArmy = async function(e){ let actionFired = false; this.countries.forEach(country => { if(e.target.id === country.name && this.player.reserve > 0 && country.owner === this.player.name){ if(e.shiftKey){ this.playerTroopsPlaced += this.player.reserve; country.army += this.player.reserve; this.player.reserve = 0; } else{ this.playerTroopsPlaced += 1; country.army += 1; this.player.reserve -= 1; } if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve; if (e.target.nextElementSibling) e.target.nextElementSibling.textContent = country.army; actionFired = true; } }) if(actionFired){ this.updateInfo(); if(this.player.reserve === 0){ await this.logAction(`${this.player.name} deployed ${this.playerTroopsPlaced} fresh troops across their sectors.`); this.playerTroopsPlaced = 0; this.stage = "Battle"; if (turnInfo) turnInfo.textContent = "Combat Phase"; if (turnInfoMessage) turnInfoMessage.textContent = "Select staging territory, then target an enemy."; this.updateButtonText(); this.updateInfo(); } } } Gamestate.vatsTargeting = async function(attackerEl, defenderEl) { let turbo = turboToggle && turboToggle.checked; if (turbo) return; await this.logAction(`[ V.A.T.S. TARGETING SEQUENCE ENGAGED ]`); for(let i=0; i<3; i++) { if(attackerEl) attackerEl.classList.add('vats-flash'); if(defenderEl) defenderEl.classList.add('vats-flash'); await new Promise(r => setTimeout(r, 80)); if(attackerEl) attackerEl.classList.remove('vats-flash'); if(defenderEl) defenderEl.classList.remove('vats-flash'); await new Promise(r => setTimeout(r, 60)); } } Gamestate.attack = async function(e){ if(this.prevTarget) this.prevTarget.classList.remove('flash'); let country = this.countries.find(c => c.name === e.target.id); if (!country) return; if (!this.prevCountry) { if (country.owner === this.player.name) { e.target.classList.add('flash'); this.prevTarget = e.target; this.prevCountry = country; } return; } if (this.prevCountry.name !== country.name && country.owner !== this.player.name && this.prevCountry.owner === this.player.name) { if (this.prevCountry.neighbours.includes(country.name) && this.prevCountry.army > 1) { let attackerMap = document.getElementById(this.prevCountry.name); let defenderMap = document.getElementById(country.name); if (map) map.style.pointerEvents = "none"; await this.vatsTargeting(attackerMap, defenderMap); await this.battle(this.prevCountry, country, this.player, 0); if (map) map.style.pointerEvents = "auto"; } else if (this.prevCountry.army === 1) { this.updateInfo(); await this.logAction("Cannot attack: A minimum garrison of 1 troop must remain in the territory.", true); } } this.prevCountry = null; this.prevTarget = null; } Gamestate.maneuver = function(e){ if(this.prevTarget){ this.prevTarget.classList.remove('flash'); } this.countries.forEach(country => { if(e.target.id === country.name){ if (country.owner !== this.player.name) return; e.target.classList.add('flash'); this.prevTarget = e.target; if(this.prevCountry){ if(this.prevCountry.name !== country.name && this.prevCountry.owner === this.player.name){ if (this.maneuverSource && this.maneuverDest) { let valid1 = (this.prevCountry.name === this.maneuverSource && country.name === this.maneuverDest); let valid2 = (this.prevCountry.name === this.maneuverDest && country.name === this.maneuverSource); if (!valid1 && !valid2) { return; } } if(this.prevCountry.neighbours.includes(country.name) && this.prevCountry.army > 1){ this.maneuverSource = this.prevCountry.name; this.maneuverDest = country.name; let moveAmount = e.shiftKey ? (this.prevCountry.army - 1) : 1; country.army += moveAmount; this.prevCountry.army -= moveAmount; let sourceMap = document.getElementById(`${this.prevCountry.name}`); let destMap = document.getElementById(`${country.name}`); if (sourceMap && sourceMap.nextElementSibling) sourceMap.nextElementSibling.textContent = this.prevCountry.army; if (destMap && destMap.nextElementSibling) destMap.nextElementSibling.textContent = country.army; this.updateInfo(); } } } this.prevCountry = country; } }); } Gamestate.aiMove = async function(){ if(this.gameOver) return; if(this.prevTarget) this.prevTarget.classList.remove('flash'); this.stage = "AI Turn"; this.updateButtonText(); if (turnInfoMessage) turnInfoMessage.textContent = ""; for(let i = 1; i <= this.players.length; i++){ if(i === this.players.length){ if(this.player.areas.length === 0){ this.player.alive = false; return this.aiMove(); } this.turn += 1; this.aiTurn = false; await this.processRadstorm(); this.stage = "Fortify"; this.updateButtonText(); if (turnInfoMessage) turnInfoMessage.textContent = "Deploy reserve troops to your territories."; let bonus = this.unitBonus(this.player, 0); this.player.reserve += bonus; this.player.army += bonus; if (map) map.style.pointerEvents = "auto"; if (infoName[i-1]) infoName[i-1].parentElement.classList.remove('highlight'); if (infoName[0]) infoName[0].parentElement.classList.add('highlight') if (reserveDisplay) reserveDisplay.innerHTML = this.player.reserve; this.updateInfo(); return; } if (infoName[i-1]) infoName[i-1].parentElement.classList.remove('highlight'); if(this.players[i].alive){ if (this.players[i].isNeutral) { continue; } if (infoName[i]) infoName[i].parentElement.classList.add('highlight') let turbo = turboToggle && turboToggle.checked; if (Math.random() < 0.20) { let randomEvent = wastelandEncounters[Math.floor(Math.random() * wastelandEncounters.length)]; await this.logAction(`SYSTEM: ${randomEvent}`); } this.players[i].reserve = this.unitBonus(this.players[i], i); let troopsToPlace = this.players[i].reserve; let tradeIndices = this.getBestTrade(this.players[i].cards); if (tradeIndices) { let bonus = getTradeBonus(); tradeIndices.sort((a,b)=>b-a).forEach(index => { deck.unshift(this.players[i].cards[index]); this.players[i].cards.splice(index, 1); }); this.players[i].reserve += bonus; troopsToPlace += bonus; await this.logAction(`SUPPLY: ${this.players[i].name} spent Caps for +${bonus} troops!`, true); } this.players[i].army += this.players[i].reserve; if (troopsToPlace > 0) { await this.logAction(`${this.players[i].name} deployed ${troopsToPlace} troops to their sectors.`); } let areaToFortify = ["", 0]; this.players[i].areas.forEach(area => { this.countries.forEach(country => { if(country.name === area && this.players[i].reserve > 0){ country.neighbours.forEach(neighbour => { this.countries.forEach(c => { if(c.name === neighbour && c.owner !== this.players[i].name){ let continent = continents.find(x => x.name === country.continent); let count = 0; continent.areas.forEach(x => { if (this.players[i].areas.includes(x)) count++; }); let ratio = count / continent.areas.length; if (ratio >= areaToFortify[1]){ areaToFortify = [country, ratio] } } }) }) } }) }) if (areaToFortify[0]) { areaToFortify[0].army += this.players[i].reserve; this.players[i].reserve = 0; let areaOnMap = document.getElementById(`${areaToFortify[0].name}`); if (areaOnMap && areaOnMap.nextElementSibling) areaOnMap.nextElementSibling.textContent = areaToFortify[0].army; } 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) { await this.aiAttack(country, i, turbo); } } this.aiManeuver(i); if (this.players[i].conqueredThisTurn && deck.length > 0) { this.players[i].cards.push(deck.pop()); this.players[i].conqueredThisTurn = false; } this.updateInfo(); } } } Gamestate.aiAttack = async function(country, i, turbo){ let possibleTargets = []; country.neighbours.forEach(neighbour => { this.countries.forEach(opponent => { if(neighbour === opponent.name && opponent.army + 1 < country.army && country.owner !== opponent.owner ){ possibleTargets.push(opponent); } }) }) let target = [possibleTargets[0], 0]; possibleTargets.forEach(poss => { let continent = continents.find(x => x.name === poss.continent); let count = 0; continent.areas.forEach(x => { if(this.players[i].areas.includes(x)) count++; }); let ratio = count / continent.areas.length; if(ratio >= target[1]){ target = [poss, ratio] } }) if(!target[0]){ return; } let attackerMap = document.getElementById(country.name); let defenderMap = document.getElementById(target[0].name); await this.vatsTargeting(attackerMap, defenderMap); await this.battle(country, target[0], this.players[i], i); } Gamestate.aiManeuver = function(i){ let player = this.players[i]; let owned = this.countries.filter(c => c.owner === player.name); let internal = owned.filter(c => c.army > 1 && c.neighbours.every(n => { let neighborCountry = this.countries.find(x => x.name === n); return neighborCountry && neighborCountry.owner === player.name; })); if(internal.length > 0) { internal.sort((a,b) => b.army - a.army); let source = internal[0]; let destName = source.neighbours.find(n => this.countries.find(x => x.name === n).owner === player.name); if(destName) { let dest = this.countries.find(x => x.name === destName); let moveAmount = source.army - 1; dest.army += moveAmount; source.army -= moveAmount; let sourceMap = document.getElementById(`${source.name}`); let destMap = document.getElementById(`${dest.name}`); if(sourceMap && sourceMap.nextElementSibling) sourceMap.nextElementSibling.textContent = source.army; if(destMap && destMap.nextElementSibling) destMap.nextElementSibling.textContent = dest.army; } } } Gamestate.battle = async function(country, opponent, player, i){ let defender = document.getElementById(`${opponent.name}`) let attacker = document.getElementById(`${country.name}`) let opp; this.players.forEach(p => { if(p.name === opponent.owner){ opp = p; } }); const originalOwner = opponent.owner; let attackerWinChance = 0.5; if (this.difficulty === "Easy") { if (player === this.player) attackerWinChance = 0.60; else if (opp === this.player) attackerWinChance = 0.40; } else if (this.difficulty === "Hard") { if (player === this.player) attackerWinChance = 0.40; else if (opp === this.player) attackerWinChance = 0.60; } if (opp.isNeutral) { attackerWinChance -= 0.15; } let isVictory = false; let flavor = ""; while(opponent.army > 0){ if(country.army <= 1){ if (attacker && attacker.nextElementSibling) attacker.nextElementSibling.textContent = country.army; if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; this.updateInfo(); await this.logAction(`REPULSED: ${player.name} assaulted ${originalOwner} in ${opponent.name} but failed.`); return; } if(Math.random() > attackerWinChance){ country.army -= 1; } else { opponent.army -= 1; } } if(opponent.army <= 0 ){ isVictory = true; player.conqueredThisTurn = true; flavor = (Math.random() < 0.10) ? (" " + combatFlavors[Math.floor(Math.random() * combatFlavors.length)]) : "!"; this.players.forEach(p => { if(p.name === opponent.owner){ let index = p.areas.indexOf(opponent.name); if (index > -1) { p.areas.splice(index, 1); } } }); opponent.owner = player.name; opponent.color = player.color; player.areas.push(opponent.name); let movedTroops = Math.max(1, Math.floor(country.army / 2)); opponent.army = movedTroops; country.army -= movedTroops; if (defender) defender.style.fill = opponent.color; if (defender && defender.nextElementSibling) defender.nextElementSibling.textContent = opponent.army; if (attacker && attacker.nextElementSibling) attacker.nextElementSibling.textContent = country.army; if(opp.areas.length === 0){ opp.alive = false; let index = this.players.indexOf(opp) if (infoName[index]) infoName[index].parentElement.classList.add('defeated'); } } player.army = 0; opp.army = 0; this.countries.forEach(c => { player.areas.forEach(area => { if(area === c.name){ player.army += c.army } }) opp.areas.forEach(area => { if(area === c.name){ opp.army += c.army } }) }); this.updateInfo(); if (isVictory) { await this.logAction(`VICTORY: ${player.name} wiped out ${originalOwner} in ${opponent.name}${flavor}`, true); } if(this.player.alive){ continents.forEach(continent => { let ownsContinent = continent.areas.every(area => player.areas.includes(area)); if(ownsContinent){ let matchedCountry = continent.areas.some(a => { return a === opponent.name; }); if(matchedCountry){ this.showToast(`${player.name} controls ${continent.name}! (+${continent.bonus} troops)`, player.color); } } }) } if(player.areas.length === 42){ this.gameOver = true; this.win(player); } } Gamestate.init();