diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..554a999 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.sass-cache \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..cb8c8eb --- /dev/null +++ b/index.html @@ -0,0 +1,877 @@ + + + + + + RobCo Strategic Simulation + + + +
+ +
+
+

ROBCO OS v1.2

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
+
+

ROBCO STRAT-COMv1.2

+
+
+ +
+ HP
+ AP
+
+
+ +
+
+

Awaiting Deployment

+
2
+
+
+
+
+ +
+
+
Bottle Caps 0
+ +
+
+ + +
+
+ +
+ +
+ STAT + INV + DATA + MAP + RADIO +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + style="overflow:visible"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + I n d i a n O c e a n + + + +
+ +
+
Vault-Tec Action Log
+
+
+ + + + + +
+
Caps Yield:
+
Caps Yield:
+
Caps Yield:
+
Caps Yield:
+
Caps Yield:
+
Caps Yield:
+
Caps Yield:
+ + + +
+
+ + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..737f1d6 --- /dev/null +++ b/index.js @@ -0,0 +1,1411 @@ +/*=============================== + 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(); \ No newline at end of file