JavaScript must be enabled to play.
Browser lacks capabilities required to play.
Upgrade or switch to another browser.
Loading…
<<nobr>> <<script>> window.AVStart = window.AVStart || {}; AVStart.slug = function (text) { return String(text || "unknown") .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, ""); }; AVStart.characterFolderName = function (girl) { if (!girl) return "placeholder"; const raw = girl.folder || girl.characterFolder || girl.characterName || girl.id || girl.name || "placeholder"; return AVStart.slug(raw); }; AVStart.faceImage = function (girl) { if (!girl) return "data/characters/placeholder.png"; const folder = AVStart.characterFolderName(girl); return girl.face || girl.faceImage || girl.image || ("data/characters/" + folder + "/face.png"); }; AVStart.clonePlain = function (value) { try { return JSON.parse(JSON.stringify(value || [])); } catch (error) { if (Array.isArray(value)) return value.slice(); return value; } }; AVStart.defaultRegions = function () { return [ { id: "cinder", name: "Cinder Hold", owned: true, ruler: "You", power: 35, basePower: 35, influence: 8, x: 50, y: 58, color: "rgba(223,255,93,.35)", harem: [] }, { id: "iron_veil", name: "Iron Veil", owned: false, ruler: "Marshal Kade", power: 32, basePower: 32, influence: 8, x: 23, y: 26, color: "rgba(255,91,32,.35)", harem: [] }, { id: "red_market", name: "Red Market", owned: false, ruler: "Madam Sable", power: 30, basePower: 30, influence: 8, x: 72, y: 25, color: "rgba(255,91,32,.35)", harem: [] }, { id: "salt_crown", name: "Salt Crown", owned: false, ruler: "Captain Oro", power: 30, basePower: 30, influence: 8, x: 84, y: 55, color: "rgba(255,91,32,.35)", harem: [] }, { id: "grave_radio", name: "Grave Radio", owned: false, ruler: "Saint Vox", power: 28, basePower: 28, influence: 8, x: 67, y: 78, color: "rgba(255,91,32,.35)", harem: [] }, { id: "black_lotus", name: "Black Lotus", owned: false, ruler: "Lady Nyx", power: 31, basePower: 31, influence: 8, x: 35, y: 80, color: "rgba(255,91,32,.35)", harem: [] }, { id: "rust_gate", name: "Rust Gate", owned: false, ruler: "Baron Rusk", power: 29, basePower: 29, influence: 8, x: 15, y: 57, color: "rgba(255,91,32,.35)", harem: [] }, { id: "white_spire", name: "White Spire", owned: false, ruler: "The Pale Court", power: 33, basePower: 33, influence: 8, x: 51, y: 17, color: "rgba(255,91,32,.35)", harem: [] } ]; }; AVStart.getRegionPool = function () { const v = State.variables || {}; if (v._avStartOriginalRegions && v._avStartOriginalRegions.length > 0) { return AVStart.clonePlain(v._avStartOriginalRegions); } if (v.regions && v.regions.length > 0) { v._avStartOriginalRegions = AVStart.clonePlain(v.regions); return AVStart.clonePlain(v._avStartOriginalRegions); } v._avStartOriginalRegions = AVStart.defaultRegions(); return AVStart.clonePlain(v._avStartOriginalRegions); }; AVStart.getWomenPool = function () { const v = State.variables || {}; if (v._avStartOriginalWomen && v._avStartOriginalWomen.length > 0) { return v._avStartOriginalWomen.slice(); } if (v.startingWomenPool && v.startingWomenPool.length > 0) { v._avStartOriginalWomen = v.startingWomenPool.slice(); return v._avStartOriginalWomen.slice(); } if (v.women && v.women.length > 0) { v._avStartOriginalWomen = v.women.slice(); return v._avStartOriginalWomen.slice(); } return []; }; AVStart.resetNewRun = function () { const v = State.variables || {}; const originalWomen = AVStart.getWomenPool(); const originalRegions = AVStart.getRegionPool(); v.startingWomenPool = originalWomen.slice(); v.women = originalWomen.slice(); v.regions = AVStart.clonePlain(originalRegions); v.introStep = 0; v.introDone = false; v.savedGirlName = ""; v.savedGirlKey = ""; v.girlsDistributed = false; v.day = 1; v.hour = 7; v.actionsLeft = 5; v.pendingPlayerActions = []; v.dayRecap = []; v.recapEvents = []; v.recapIndex = 0; v.showRecapPopup = false; v.gameOver = false; v.gameOverReason = ""; v.victory = false; v.victoryCreated = false; v.tab = "base"; v.freeWomenPool = []; v.protectedRegionId = null; v.protectedByEnemy = {}; v.pendingEnemyProtections = {}; v.protectionBlocksShown = {}; v.playerMovedGirlsThisRecap = {}; v.girlsTakenThisRecap = {}; v.playerHaremRaidedThisRecap = false; v.incursionUsedThisRecap = false; v.enemyActionTrace = []; v.enemyWarSupports = []; v.enemyFullDayActionUsed = {}; v.extractionFrontsAtRecapStart = {}; v.isResolvingDay = false; v.playerWorkedPowerToday = false; }; AVStart.getFacePool = function () { const source = AVStart.getWomenPool(); const faces = []; for (const girl of source) { if (!girl) continue; faces.push({ name: girl.name || "Unknown", src: AVStart.faceImage(girl) }); } if (faces.length < 1) { faces.push({ name: "Unknown", src: "data/characters/placeholder.png" }); } return faces; }; AVStart.randomBetween = function (min, max) { return min + Math.random() * (max - min); }; AVStart.randomizeFaceTile = function (item) { if (!item) return; const size = Math.round(84 + Math.random() * 128); item.style.setProperty("--x", AVStart.randomBetween(48, 96).toFixed(2) + "vw"); item.style.setProperty("--y", AVStart.randomBetween(-4, 90).toFixed(2) + "vh"); item.style.setProperty("--s", size + "px"); item.style.setProperty("--d", AVStart.randomBetween(18, 34).toFixed(2) + "s"); item.style.setProperty("--delay", (-AVStart.randomBetween(0, 30)).toFixed(2) + "s"); item.style.setProperty("--r", AVStart.randomBetween(-13, 13).toFixed(2) + "deg"); item.style.setProperty("--o", AVStart.randomBetween(.055, .145).toFixed(3)); item.style.setProperty("--dx", AVStart.randomBetween(-8, 8).toFixed(2) + "px"); item.style.setProperty("--dy", AVStart.randomBetween(-10, 10).toFixed(2) + "px"); }; AVStart.renderFaceRain = function () { const layer = document.getElementById("av-face-rain"); if (!layer) return; const faces = AVStart.getFacePool(); layer.innerHTML = ""; const count = Math.max(18, Math.min(42, faces.length * 4)); for (let i = 0; i < count; i++) { const face = faces[Math.floor(Math.random() * faces.length)]; const item = document.createElement("div"); const img = document.createElement("img"); item.className = "av-face-tile av-face-tile-" + ((i % 8) + 1); AVStart.randomizeFaceTile(item); img.src = face.src; img.alt = face.name; item.appendChild(img); layer.appendChild(item); } }; AVStart.openSaves = function () { try { if (typeof UI !== "undefined" && UI && typeof UI.saves === "function") { UI.saves(); return; } if (window.SugarCube && SugarCube.UI && typeof SugarCube.UI.saves === "function") { SugarCube.UI.saves(); return; } if (typeof Dialog !== "undefined" && typeof Save !== "undefined") { Dialog.setup("Recover a save"); if (typeof Saves !== "undefined" && Saves.dialog) { Saves.dialog(); } else if (typeof Save.browser === "function") { Save.browser(); } Dialog.open(); return; } alert("Save menu unavailable."); } catch (error) { alert("Save menu failed: " + (error.message || error)); } }; AVStart.ageGateKey = "av_age_gate_21_ok"; AVStart.ageGateAccepted = function () { try { return window.localStorage && localStorage.getItem(AVStart.ageGateKey) === "1"; } catch (error) { return State.variables && State.variables._avAgeGateAccepted === true; } }; AVStart.acceptAgeGate = function () { try { if (window.localStorage) localStorage.setItem(AVStart.ageGateKey, "1"); } catch (error) { if (State.variables) State.variables._avAgeGateAccepted = true; } const gate = document.getElementById("av-age-gate"); if (gate) { gate.classList.remove("active"); gate.classList.add("accepted"); gate.setAttribute("aria-hidden", "true"); } }; AVStart.rejectAgeGate = function () { window.location.href = "https://www.google.com/"; }; AVStart.showAgeGate = function () { if (AVStart.ageGateAccepted()) return; const gate = document.getElementById("av-age-gate"); if (!gate) return; gate.classList.remove("accepted"); gate.classList.add("active"); gate.setAttribute("aria-hidden", "false"); const yes = gate.querySelector(".av-age-yes"); const no = gate.querySelector(".av-age-no"); if (yes && !yes.dataset.bound) { yes.dataset.bound = "1"; yes.addEventListener("click", function (event) { event.preventDefault(); AVStart.acceptAgeGate(); }); } if (no && !no.dataset.bound) { no.dataset.bound = "1"; no.addEventListener("click", function (event) { event.preventDefault(); AVStart.rejectAgeGate(); }); } if (yes) yes.focus(); }; AVStart.playIntro = function () { if (!AVStart.ageGateAccepted()) { AVStart.showAgeGate(); return; } AVStart.resetNewRun(); if (typeof Engine !== "undefined" && Engine && typeof Engine.play === "function") { Engine.play("Intro"); return; } if (window.SugarCube && SugarCube.Engine && typeof SugarCube.Engine.play === "function") { SugarCube.Engine.play("Intro"); return; } alert("Engine unavailable."); }; AVStart.bindMenu = function () { const enter = document.querySelector(".av-enter-map"); const load = document.querySelector(".av-recover-save"); if (enter && !enter.dataset.bound) { enter.dataset.bound = "1"; enter.addEventListener("click", function (event) { event.preventDefault(); AVStart.playIntro(); }); } if (load && !load.dataset.bound) { load.dataset.bound = "1"; load.addEventListener("click", function (event) { event.preventDefault(); AVStart.openSaves(); }); } }; setTimeout(function () { AVStart.getWomenPool(); AVStart.renderFaceRain(); AVStart.bindMenu(); AVStart.showAgeGate(); }, 0); <</script>> <style> #ui-bar,#ui-bar-toggle{display:none!important} html,body{margin:0!important;padding:0!important;overflow:hidden!important;background:#020303!important} #story{margin:0!important;width:100vw!important;max-width:none!important} #passages{margin:0!important;padding:0!important;width:100vw!important;max-width:none!important} .passage{margin:0!important;padding:0!important} .av-age-gate{ position:fixed; inset:0; z-index:999; display:none; align-items:center; justify-content:center; padding:24px; box-sizing:border-box; background:rgba(2,3,3,.86); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); } .av-age-gate.active{ display:flex; } .av-age-gate.accepted{ display:none!important; } .av-age-card{ width:min(560px,100%); border:1px solid rgba(223,255,93,.62); background: linear-gradient(135deg,rgba(15,18,15,.98),rgba(3,4,4,.96)), linear-gradient(90deg,rgba(223,255,93,.10),rgba(255,91,32,.10)); box-shadow:0 24px 80px rgba(0,0,0,.72),0 0 34px rgba(223,255,93,.12); padding:30px; color:#e9efe4; font-family:Inter,Arial,Helvetica,sans-serif; position:relative; overflow:hidden; } .av-age-card::before{ content:""; position:absolute; left:0; top:0; width:7px; height:100%; background:#dfff5d; box-shadow:0 0 24px rgba(223,255,93,.55); } .av-age-kicker{ color:#dfff5d; font-size:.66rem; font-weight:950; letter-spacing:.22em; text-transform:uppercase; margin-bottom:14px; } .av-age-card h2{ margin:0 0 14px; color:#ff5b20; font-size:clamp(2rem,5vw,3.4rem); line-height:.88; letter-spacing:-.05em; text-transform:uppercase; font-weight:950; } .av-age-card p{ margin:0 0 12px; color:rgba(232,237,222,.82); font-size:.95rem; font-weight:700; line-height:1.5; } .av-age-actions{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:24px; } .av-age-btn{ min-height:50px; border:1px solid rgba(232,237,222,.24); background:linear-gradient(90deg,rgba(12,15,13,.98),rgba(4,5,5,.86)); color:#e9efe4; font-size:.76rem; font-weight:950; letter-spacing:.16em; text-transform:uppercase; cursor:pointer; } .av-age-btn:hover, .av-age-btn:focus{ outline:none; border-color:rgba(223,255,93,.86); color:#fff; } .av-age-no:hover, .av-age-no:focus{ border-color:rgba(255,91,32,.88); } .av-age-yes{ border-color:rgba(223,255,93,.58); } .av-face-rain{ position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; filter:saturate(.78) contrast(1.05); } .av-face-tile{ position:absolute; left:var(--x); top:var(--y); width:var(--s); height:calc(var(--s) * 1.32); transform:translate3d(0,0,0) rotate(var(--r)); border:0; background:transparent; box-shadow:none; opacity:0; overflow:hidden; animation:avFaceBreathe var(--d) ease-in-out var(--delay) infinite; mix-blend-mode:screen; will-change:opacity,transform,filter; } .av-face-tile img{ width:100%; height:100%; object-fit:cover; object-position:center top; display:block; filter:grayscale(.55) sepia(.12) brightness(.52) contrast(1.18) blur(.15px); -webkit-mask-image:linear-gradient(180deg,transparent 0%,black 13%,black 74%,transparent 100%); mask-image:linear-gradient(180deg,transparent 0%,black 13%,black 74%,transparent 100%); } .av-face-tile::after{ content:""; position:absolute; inset:0; background: linear-gradient(135deg,rgba(223,255,93,.10),transparent 38%,rgba(255,91,32,.12)), linear-gradient(180deg,rgba(0,0,0,.05),rgba(0,0,0,.55)); mix-blend-mode:screen; } .av-face-tile-2, .av-face-tile-5{ mix-blend-mode:lighten; } .av-face-tile-3, .av-face-tile-7{ filter:blur(.5px) saturate(.74); } @keyframes avFaceBreathe{ 0%{ opacity:0; transform:translate3d(0,0,0) rotate(var(--r)) scale(.985); } 18%{ opacity:calc(var(--o) * .55); } 48%{ opacity:var(--o); transform:translate3d(var(--dx),var(--dy),0) rotate(var(--r)) scale(1.015); } 74%{ opacity:calc(var(--o) * .72); } 100%{ opacity:0; transform:translate3d(calc(var(--dx) * .45),calc(var(--dy) * .45),0) rotate(var(--r)) scale(.995); } } .av-screen{ position:fixed; inset:0; width:100vw; height:100vh; overflow:hidden; background: linear-gradient(90deg,rgba(2,3,3,.96) 0%,rgba(2,3,3,.88) 30%,rgba(2,3,3,.48) 58%,rgba(2,3,3,.18) 100%), url("assets/map-night.png"); background-size:cover; background-position:center; color:#e9efe4; font-family:Inter,Arial,Helvetica,sans-serif; isolation:isolate; } .av-screen::before{ content:""; position:absolute; inset:0; z-index:1; background: radial-gradient(circle at 74% 45%,rgba(255,92,33,.16),transparent 22%), radial-gradient(circle at 52% 55%,transparent 0%,rgba(0,0,0,.25) 58%,rgba(0,0,0,.88) 100%); pointer-events:none; } .av-screen::after{ content:""; position:absolute; inset:0; z-index:2; background-image: linear-gradient(rgba(255,255,255,.045) 1px,transparent 1px), linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px), linear-gradient(rgba(255,255,255,.028),rgba(255,255,255,0) 2px); background-size:64px 64px,64px 64px,100% 3px; opacity:.20; mix-blend-mode:overlay; pointer-events:none; } .av-noise{ position:absolute; inset:0; z-index:3; pointer-events:none; background: repeating-linear-gradient( 0deg, rgba(255,255,255,.018) 0px, rgba(255,255,255,.018) 1px, transparent 1px, transparent 5px ); opacity:.20; } .av-cut-a, .av-cut-b, .av-cut-c{ position:absolute; z-index:4; pointer-events:none; border:1px solid rgba(222,255,94,.28); background:rgba(222,255,94,.045); mix-blend-mode:screen; } .av-cut-a{ width:32vw; height:13vh; right:-4vw; top:12vh; transform:skewX(-18deg); } .av-cut-b{ width:22vw; height:34vh; right:14vw; bottom:-13vh; transform:skewX(14deg); border-color:rgba(255,91,32,.28); background:rgba(255,91,32,.05); } .av-cut-c{ width:8vw; height:100vh; left:0; top:0; background:rgba(0,0,0,.55); border:0; border-right:1px solid rgba(222,255,94,.22); } .av-layout{ position:relative; z-index:10; width:100vw; height:100vh; display:grid; grid-template-columns:minmax(72px,8vw) minmax(560px,720px) 1fr; align-items:center; box-sizing:border-box; } .av-panel{ grid-column:2; width:100%; max-width:720px; position:relative; } .av-topline{ display:flex; align-items:center; gap:12px; margin-bottom:22px; color:#dfff5d; font-size:.66rem; font-weight:800; letter-spacing:.22em; text-transform:uppercase; } .av-dot{ width:9px; height:9px; background:#dfff5d; box-shadow:0 0 18px #dfff5d; animation:avBlink 1.25s steps(2,end) infinite; } @keyframes avBlink{ 50%{opacity:.18} } .av-topline::after{ content:""; height:1px; width:180px; background:linear-gradient(90deg,rgba(223,255,93,.8),transparent); } .av-title{ margin:0; font-size:clamp(4.2rem,7.3vw,9rem); line-height:.78; letter-spacing:-.06em; text-transform:uppercase; font-weight:950; color:#f1f4e7; text-shadow: 0 0 26px rgba(223,255,93,.14), 4px 4px 0 rgba(0,0,0,.78); } .av-title span{ display:block; } .av-title .vice{ color:#ff5b20; text-shadow: 0 0 30px rgba(255,91,32,.28), 4px 4px 0 rgba(0,0,0,.78); } .av-title-wrap{ position:relative; display:inline-block; } .av-title-wrap::after{ content:""; position:absolute; right:-34px; top:10px; width:18px; height:78%; background:#dfff5d; clip-path:polygon(0 0,100% 0,55% 100%,0 100%); box-shadow:0 0 22px rgba(223,255,93,.36); } .av-copy{ margin:26px 0 28px; max-width:610px; display:grid; gap:10px; } .av-copy-main{ margin:0; color:#e8edde; font-size:1rem; font-weight:650; line-height:1.38; letter-spacing:.01em; } .av-copy-sub{ margin:0; color:rgba(232,237,222,.62); font-size:.76rem; font-weight:850; line-height:1.55; letter-spacing:.10em; text-transform:uppercase; } .av-menu{ display:grid; grid-template-columns:1fr; gap:10px; width:460px; max-width:100%; } .av-btn{ position:relative; display:grid; grid-template-columns:46px 1fr 74px; align-items:center; min-height:52px; border:1px solid rgba(232,237,222,.22); background: linear-gradient(90deg,rgba(12,15,13,.92),rgba(4,5,5,.76)); color:#e9efe4!important; text-decoration:none!important; text-transform:uppercase; font-size:.78rem; font-weight:900; letter-spacing:.16em; cursor:pointer; overflow:hidden; box-shadow:0 12px 28px rgba(0,0,0,.32); transition:transform .12s ease,border-color .12s ease,background .12s ease,color .12s ease; } .av-btn::before{ content:""; position:absolute; inset:0 auto 0 0; width:5px; background:#dfff5d; opacity:.95; } .av-btn::after{ content:""; position:absolute; right:-40px; top:0; width:80px; height:100%; background:rgba(223,255,93,.08); transform:skewX(-22deg); transition:right .15s ease,background .15s ease; } .av-btn:hover{ transform:translateX(10px); border-color:rgba(223,255,93,.86); background: linear-gradient(90deg,rgba(31,38,28,.98),rgba(8,9,8,.86)); color:#ffffff!important; } .av-btn:hover::after{ right:-18px; background:rgba(223,255,93,.16); } .av-btn.red::before{ background:#ff5b20; } .av-btn.red:hover{ border-color:rgba(255,91,32,.88); } .av-btn.red::after{ background:rgba(255,91,32,.09); } .av-btn.red:hover::after{ background:rgba(255,91,32,.18); } .av-icon{ display:flex; align-items:center; justify-content:center; color:currentColor; opacity:.92; } .av-icon svg{ width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; } .av-label{ position:relative; z-index:2; } .av-meta{ position:relative; z-index:2; text-align:center; color:rgba(232,237,222,.42); font-size:.62rem; letter-spacing:.13em; } .av-footer{ margin-top:20px; color:rgba(232,237,222,.45); font-size:.62rem; font-weight:800; letter-spacing:.13em; text-transform:uppercase; } .av-corner{ position:absolute; right:46px; bottom:34px; z-index:12; display:grid; gap:7px; text-align:right; color:rgba(232,237,222,.38); font-size:.62rem; font-weight:850; letter-spacing:.16em; text-transform:uppercase; } .av-corner strong{ color:#dfff5d; font-size:.78rem; font-weight:950; } .av-corner::before{ content:""; width:110px; height:1px; justify-self:end; background:linear-gradient(90deg,transparent,#dfff5d); opacity:.65; } .av-sidecode{ position:absolute; left:24px; bottom:12vh; z-index:12; writing-mode:vertical-rl; text-orientation:mixed; color:rgba(223,255,93,.48); font-size:.62rem; font-weight:950; letter-spacing:.20em; text-transform:uppercase; } @media(max-width:1100px){ .av-layout{ grid-template-columns:minmax(52px,7vw) minmax(480px,620px) 1fr; } .av-title{ font-size:clamp(3.7rem,8vw,7rem); } .av-copy-main{ font-size:.92rem; } .av-menu{ width:420px; } } @media(max-width:780px){ .av-layout{ display:flex; align-items:center; padding:0 30px; box-sizing:border-box; } .av-panel{ max-width:520px; } .av-title{ font-size:clamp(3.2rem,14vw,5.3rem); } .av-title-wrap::after{ display:none; } .av-menu{ width:100%; max-width:390px; } .av-copy-main{ font-size:.84rem; } .av-copy-sub{ font-size:.60rem; } .av-corner, .av-sidecode{ display:none; } } @media(max-height:700px){ .av-topline{ margin-bottom:14px; } .av-title{ font-size:clamp(3.1rem,6.4vw,6.2rem); } .av-copy{ margin:18px 0 18px; } .av-copy-main{ font-size:.82rem; } .av-copy-sub{ display:none; } .av-btn{ min-height:43px; font-size:.68rem; } .av-footer{ margin-top:13px; } } @media(max-height:540px){ .av-copy, .av-footer, .av-corner{ display:none; } } </style><div class="av-screen"><div id="av-age-gate" class="av-age-gate" role="dialog" aria-modal="true" aria-hidden="true"><div class="av-age-card"><div class="av-age-kicker">Warning</div><h2>Adults only +21</h2><p>All characters are fictional and are 21 years old or older.</p><p>To play, you must be 21 years old or older. Are you 21+?</p><div class="av-age-actions"><button type="button" class="av-age-btn av-age-no">NO</button><button type="button" class="av-age-btn av-age-yes">Yes</button></div></div></div><div id="av-face-rain" class="av-face-rain"></div><div class="av-noise"></div><div class="av-cut-a"></div><div class="av-cut-b"></div><div class="av-cut-c"></div><div class="av-sidecode">BLACKLINE OPEN / THE ROAD WANTS A PRICE</div><div class="av-layout"><div class="av-panel"><div class="av-topline"><i class="av-dot"></i> Night channel open</div><div class="av-title-wrap"><h1 class="av-title"><span>Apocalyptic</span><span class="vice">Vice</span></h1></div><div class="av-copy"><p class="av-copy-main">Eight capitals keep their lights low. Behind the walls, power trades hands in whispers, in debts, in beds, and in doors that do not open twice.</p><p class="av-copy-sub">Come back with a crown.</p></div><div class="av-menu"><a class="av-btn av-enter-map" href="#"><span class="av-icon"><svg viewBox="0 0 24 24"><path d="M8 5l10 7-10 7V5z"/></svg></span><span class="av-label">Enter the map</span><span class="av-meta">RUN</span></a><a class="av-btn av-recover-save" href="#"><span class="av-icon"><svg viewBox="0 0 24 24"><path d="M4 6h16v12H4z"/><path d="M8 6V4h8v2"/></svg></span><span class="av-label">Recover a save</span><span class="av-meta">OLD</span></a><a class="av-btn red" href="https://discord.gg/uVsWK9MJgT" target="_blank" rel="noopener"><span class="av-icon"><svg viewBox="0 0 24 24"><path d="M18 13v6H5V6h6"/><path d="M14 5h5v5"/><path d="M10 14L19 5"/></svg></span><span class="av-label">Discord</span><span class="av-meta">WEB</span></a><a class="av-btn red" href="https://ko-fi.com/sbnstudio" target="_blank" rel="noopener"><span class="av-icon"><svg viewBox="0 0 24 24"><path d="M12 21s-7-4.4-9-9.5C1.5 7.5 3.8 4 7.3 4c2 0 3.4 1.1 4.7 2.8C13.3 5.1 14.7 4 16.7 4c3.5 0 5.8 3.5 4.3 7.5C19 16.6 12 21 12 21z"/></svg></span><span class="av-label">Support</span><span class="av-meta">WEB</span></a></div><div class="av-footer">Adults only +21</div></div></div><div class="av-corner"><strong>Black Archipelago</strong><span>No one owns the island yet.</span></div></div><<endnobr>>
<<set $day = 1>> <<set $hour = 10>> <<set $tab = "base">> <<set $selectedRegion = "cinder">> <<set $selectedWoman = 0>> <<set $actionsLeft = 5>> <<set $pendingPlayerActions = []>> <<set $dayRecap = []>> <<set $recapEvents = []>> <<set $recapIndex = 0>> <<set $showRecapPopup = false>> <<set $gameOver = false>> <<set $gameOverReason = "">> <<set $girlsDistributed = false>> <<set $player = { }>> <<set $women = []>> <<set $freeWomenPool = []>> <<set $girlsDistributed = false>> <<set $startingWomenPool = [ { name:"Jane", place:"Unassigned", face:"data/characters/jane/jane_face.png", body:"data/characters/jane/jane_body.png", image:"data/characters/jane/jane_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"You", vaginal:0, anal:0, oral:0 }, { name:"Mara", place:"Unassigned", face:"data/characters/mara/mara_face.png", body:"data/characters/mara/mara_body.png", image:"data/characters/mara/mara_face.png", virgin:false, pregnant:false, pregnantBy:"None", firstTime:"Dagan", favoritePerson:"None", vaginal:100, anal:0, oral:252 }, { name:"Lena", place:"Unassigned", face:"data/characters/lena/lena_face.png", body:"data/characters/lena/lena_body.png", image:"data/characters/lena/lena_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"None", vaginal:0, anal:0, oral:0 }, { name:"Lu", place:"Unassigned", face:"data/characters/lu/lu_face.png", body:"data/characters/lu/lu_body.png", image:"data/characters/lu/lu_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"None", vaginal:0, anal:0, oral:0 }, { name:"Ema", place:"Unassigned", face:"data/characters/ema/ema_face.png", body:"data/characters/ema/ema_body.png", image:"data/characters/ema/ema_face.png", virgin:false, pregnant:false, pregnantBy:"None", firstTime:"Unknown", favoritePerson:"None", vaginal:120, anal:0, oral:15 }, { name:"Teha", place:"Unassigned", face:"data/characters/teha/teha_face.png", body:"data/characters/teha/teha_body.png", image:"data/characters/teha/teha_face.png", virgin:false, pregnant:false, pregnantBy:"None", firstTime:"Unknown", favoritePerson:"None", vaginal:12, anal:0, oral:105 }, { name:"Xi", place:"Unassigned", face:"data/characters/xi/xi_face.png", body:"data/characters/xi/xi_body.png", image:"data/characters/xi/xi_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"None", vaginal:0, anal:0, oral:1010 }, { name:"Ana", place:"Unassigned", face:"data/characters/ana/ana_face.png", body:"data/characters/ana/ana_body.png", image:"data/characters/ana/ana_face.png", virgin:false, pregnant:false, pregnantBy:"None", firstTime:"Unknown", favoritePerson:"None", vaginal:31, anal:12, oral:75 }, { name:"Etsu", place:"Unassigned", face:"data/characters/etsu/etsu_face.png", body:"data/characters/etsu/etsu_body.png", image:"data/characters/etsu/etsu_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"None", vaginal:0, anal:0, oral:0 }, { name:"Yurt", place:"Unassigned", face:"data/characters/yurt/yurt_face.png", body:"data/characters/yurt/yurt_body.png", image:"data/characters/yurt/yurt_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"None", vaginal:0, anal:0, oral:0 }, { name:"Jenna", place:"Unassigned", face:"data/characters/jenna/jenna_face.png", body:"data/characters/jenna/jenna_body.png", image:"data/characters/jenna/jenna_face.png", virgin:false, pregnant:false, pregnantBy:"None", firstTime:"Unknown", favoritePerson:"None", vaginal:801, anal:22, oral:5709 }, { name:"Eva", place:"Unassigned", face:"data/characters/eva/eva_face.png", body:"data/characters/eva/eva_body.png", image:"data/characters/eva/eva_face.png", virgin:true, pregnant:false, pregnantBy:"None", firstTime:"None", favoritePerson:"You", vaginal:0, anal:0, oral:0 } ]>> <<set $regions = [ {id:"cinder", name:"Cinder Hold", ruler:"You", owned:true, x:49.5, y:50, power:10, influence:1, color:"rgba(223,255,93,0.30)"}, {id:"red", name:"Red Bastion", ruler:"Kael", owned:false, x:17.5, y:19, power:10, influence:1, color:"rgba(255,91,32,0.30)"}, {id:"garden", name:"White Gardens", ruler:"Maera", owned:false, x:88.5, y:17, power:10, influence:1, color:"rgba(230,245,255,0.24)"}, {id:"iron", name:"Iron Road", ruler:"Dagan", owned:false, x:83.5, y:50, power:10, influence:1, color:"rgba(190,190,190,0.26)"}, {id:"market", name:"Grey Market", ruler:"Noma", owned:false, x:88.5, y:84.5, power:10, influence:1, color:"rgba(220,170,80,0.28)"}, {id:"marsh", name:"Black Marsh", ruler:"Silex", owned:false, x:51, y:84, power:10, influence:1, color:"rgba(80,190,120,0.24)"}, {id:"coast", name:"Split Coast", ruler:"Orska", owned:false, x:15.5, y:50, power:10, influence:1, color:"rgba(90,160,255,0.25)"}, {id:"dome", name:"Blue Dome", ruler:"Veyr", owned:false, x:52.5, y:15, power:10, influence:1, color:"rgba(90,220,255,0.25)"}, {id:"debauchery", name:"Debauchery City", ruler:"Econ", owned:false, x:10.5, y:84, power:10, influence:1, color:"rgba(30,220,255,0.25)"} ]>>
<<nobr>> <<script>> if (!setup.AV) setup.AV = {}; setup.AV.defaultRegionSpots = [ { x: 50, y: 52 }, { x: 17, y: 22 }, { x: 74, y: 20 }, { x: 86, y: 47 }, { x: 69, y: 78 }, { x: 30, y: 80 }, { x: 14, y: 55 }, { x: 49, y: 27 }, { x: 10.5, y: 84 } ]; setup.AV.PROTECT_POWER_COST = 4; setup.AV.PROTECT_INFLUENCE_COST = 1; setup.AV.WORK_POWER_GAIN = 5; setup.AV.ENEMY_PROTECT_FRONT_CHANCE = 80; setup.AV.ENEMY_PROTECT_BIG_HAREM_CHANCE = 60; setup.AV.ENEMY_PROTECT_SMALL_HAREM_CHANCE = 40; setup.AV.play = function (passage) { if (window.Engine && Engine.play) { Engine.play(passage); return; } if (window.SugarCube && SugarCube.Engine && SugarCube.Engine.play) { SugarCube.Engine.play(passage); } }; setup.AV.notify = function (message) { try { if (typeof UI !== "undefined" && UI && typeof UI.alert === "function") { UI.alert(String(message)); return; } } catch (error) {} window.alert(String(message)); }; setup.AV.openSaveMenu = function () { try { if (typeof UI !== "undefined" && UI && typeof UI.saves === "function") { UI.saves(); return; } if (typeof SugarCube !== "undefined" && SugarCube.UI && typeof SugarCube.UI.saves === "function") { SugarCube.UI.saves(); return; } const saveButton = document.querySelector("#menu-item-saves a, #menu-item-saves button"); if (saveButton) { saveButton.click(); return; } setup.AV.notify("Could not open the save menu."); } catch (error) { setup.AV.notify("Save menu failed: " + (error.message || error)); } }; setup.AV.roll = function (min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }; setup.AV.pick = function (arr) { if (!arr || arr.length < 1) return null; return arr[Math.floor(Math.random() * arr.length)]; }; setup.AV.shuffle = function (arr) { const copy = arr.slice(); for (let i = copy.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const temp = copy[i]; copy[i] = copy[j]; copy[j] = temp; } return copy; }; setup.AV.chance = function (value) { value = Math.max(0, Math.min(100, Number(value || 0))); return Math.random() * 100 <= value; }; setup.AV.slug = function (text) { return String(text || "unknown") .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, ""); }; setup.AV.characterFolderName = function (girl) { if (!girl) return "placeholder"; const raw = girl.folder || girl.characterFolder || girl.characterName || girl.id || girl.name || "placeholder"; return setup.AV.slug(raw); }; setup.AV.warImage = function () { return "data/misc/war.png"; }; setup.AV.nightMapImage = function () { return "data/misc/MAP_NIGHT.png"; }; setup.AV.actorKeyForName = function (actorName, side) { if (String(side || "") === "self") return "self"; return "enemy:" + setup.AV.slug(actorName || "unknown"); }; setup.AV.isPlayerRegion = function (region) { return !!region && !!region.owned && String(region.ruler || "") === "You"; }; setup.AV.actorSideForRegion = function (region) { return setup.AV.isPlayerRegion(region) ? "self" : "enemy"; }; setup.AV.actorNameForRegion = function (region) { if (!region) return "Unknown"; return setup.AV.isPlayerRegion(region) ? "You" : (region.ruler || region.name || "Unknown"); }; setup.AV.actorKeyForRegion = function (region) { return setup.AV.actorKeyForName(setup.AV.actorNameForRegion(region), setup.AV.actorSideForRegion(region)); }; setup.AV.ensureBondData = function (girl) { if (!girl) return; if (!girl.bondByActor) girl.bondByActor = {}; }; setup.AV.getBondData = function (girl, actorKey) { if (!girl) return null; setup.AV.ensureBondData(girl); const key = actorKey || "self"; if (!girl.bondByActor[key]) { girl.bondByActor[key] = { resistance: 0, actions: 0, conquered: false }; } if (girl.bondByActor[key].resistance == null) girl.bondByActor[key].resistance = 0; if (girl.bondByActor[key].actions == null) girl.bondByActor[key].actions = 0; if (girl.bondByActor[key].conquered == null) girl.bondByActor[key].conquered = false; return girl.bondByActor[key]; }; setup.AV.isConqueredGirl = function (girl) { if (!girl) return false; if (girl.conqueredBy) return true; const bonds = girl.bondByActor || {}; for (const key in bonds) { if (!Object.prototype.hasOwnProperty.call(bonds, key)) continue; if (bonds[key] && bonds[key].conquered) return true; } return false; }; setup.AV.girlTakenKey = function (girl) { return setup.AV.characterFolderName(girl); }; setup.AV.ensureNightSystems = function () { const v = State.variables; if (!v.protectedTonight) v.protectedTonight = {}; if (!v.girlsTakenThisRecap) v.girlsTakenThisRecap = {}; if (!v.enemySpentProtection) v.enemySpentProtection = {}; if (!v.protectionBlocksShown) v.protectionBlocksShown = {}; if (!v.enemyFullDayActionUsed) v.enemyFullDayActionUsed = {}; if (!v.extractionFrontsAtRecapStart) v.extractionFrontsAtRecapStart = {}; if (v.isResolvingDay == null) v.isResolvingDay = false; if (v.incursionUsedThisRecap == null) v.incursionUsedThisRecap = false; if (v.playerHaremRaidUsedThisRecap == null) v.playerHaremRaidUsedThisRecap = false; if (v.playerGirlTakenThisRecap == null) v.playerGirlTakenThisRecap = false; if (v.playerWorkedPowerToday == null) v.playerWorkedPowerToday = false; }; setup.AV.isRecapMovementLockActive = function () { return !!State.variables.isResolvingDay; }; setup.AV.hasAnyQueuedPlayerIncursion = function () { const actions = State.variables.pendingPlayerActions || []; for (const action of actions) { if (!action) continue; if (action.type === "incursion") return true; } return false; }; setup.AV.canStartIncursionThisRecap = function () { setup.AV.ensureNightSystems(); setup.AV.ensureRegionsReady(); return !State.variables.incursionUsedThisRecap; }; setup.AV.markIncursionUsedThisRecap = function () { setup.AV.ensureNightSystems(); State.variables.incursionUsedThisRecap = true; }; setup.AV.canTargetPlayerHaremThisRecap = function (target) { setup.AV.ensureNightSystems(); if (!setup.AV.isPlayerRegion(target)) return true; if (!setup.AV.isRecapMovementLockActive()) return true; if (State.variables.playerHaremRaidUsedThisRecap) return false; if (State.variables.playerGirlTakenThisRecap) return false; return true; }; setup.AV.markPlayerHaremRaidThisRecap = function () { setup.AV.ensureNightSystems(); if (!setup.AV.isRecapMovementLockActive()) return; State.variables.playerHaremRaidUsedThisRecap = true; }; setup.AV.markPlayerGirlTakenThisRecap = function () { setup.AV.ensureNightSystems(); if (!setup.AV.isRecapMovementLockActive()) return; State.variables.playerGirlTakenThisRecap = true; State.variables.playerHaremRaidUsedThisRecap = true; }; setup.AV.wasGirlTakenThisRecap = function (girl) { setup.AV.ensureNightSystems(); if (!setup.AV.isRecapMovementLockActive()) return false; const key = setup.AV.girlTakenKey(girl); if (!key) return false; return !!State.variables.girlsTakenThisRecap[key]; }; setup.AV.markGirlTakenThisRecap = function (girl) { setup.AV.ensureNightSystems(); const key = setup.AV.girlTakenKey(girl); if (!key) return; State.variables.girlsTakenThisRecap[key] = true; }; setup.AV.canLoseGirlFromRegionThisRecap = function (region) { setup.AV.ensureNightSystems(); if (!region) return false; if (!setup.AV.isRecapMovementLockActive()) return true; if (setup.AV.isPlayerRegion(region) && State.variables.playerGirlTakenThisRecap) return false; return true; }; setup.AV.isGirlStealable = function (girl) { if (!girl) return false; if (setup.AV.isConqueredGirl(girl)) return false; if (setup.AV.wasGirlTakenThisRecap(girl)) return false; return true; }; setup.AV.getEligibleHaremGirls = function (region) { const harem = setup.AV.getHarem(region); const eligible = []; if (!setup.AV.canLoseGirlFromRegionThisRecap(region)) return eligible; for (const girl of harem) { if (!girl) continue; if (!setup.AV.isGirlStealable(girl)) continue; eligible.push(girl); } return eligible; }; setup.AV.removeGirlFromRegion = function (region, girl) { if (!region || !girl) return null; if (!setup.AV.canLoseGirlFromRegionThisRecap(region)) return null; if (!setup.AV.isGirlStealable(girl)) return null; const harem = setup.AV.getHarem(region); const index = harem.indexOf(girl); if (index < 0) return null; const removed = harem.splice(index, 1)[0]; setup.AV.markGirlTakenThisRecap(removed); if (setup.AV.isPlayerRegion(region)) { setup.AV.markPlayerGirlTakenThisRecap(); } return removed; }; setup.AV.takeGirlFromRegion = function (region) { if (!region) return null; const eligible = setup.AV.getEligibleHaremGirls(region); if (eligible.length < 1) return null; return setup.AV.removeGirlFromRegion(region, setup.AV.pick(eligible)); }; setup.AV.isRegionProtected = function (region) { setup.AV.ensureNightSystems(); if (!region) return false; return !!State.variables.protectedTonight[region.id]; }; setup.AV.canPayProtection = function (region) { if (!region) return false; const power = Number(region.power || 0); const influence = Number(region.influence || 0); if (power <= setup.AV.PROTECT_POWER_COST) return false; if (influence <= setup.AV.PROTECT_INFLUENCE_COST) return false; return true; }; setup.AV.payProtectionCost = function (region) { if (!region) return false; if (!setup.AV.canPayProtection(region)) return false; region.power = Math.max(1, Number(region.power || 1) - setup.AV.PROTECT_POWER_COST); region.influence = Math.max(1, Number(region.influence || 1) - setup.AV.PROTECT_INFLUENCE_COST); return true; }; setup.AV.protectRegionTonight = function (region) { setup.AV.ensureNightSystems(); if (!region) return; State.variables.protectedTonight[region.id] = true; }; setup.AV.conqueredFaceImage = function (girl, side) { const folder = setup.AV.characterFolderName(girl); const owner = String(side || "self").toLowerCase(); return "data/characters/" + folder + "/" + owner + "/conquered.png"; }; setup.AV.resistanceImageForGirl = function (girl) { const folder = setup.AV.characterFolderName(girl); return "data/characters/" + folder + "/resist.png"; }; setup.AV.takenImageForGirl = function (girl) { const folder = setup.AV.characterFolderName(girl); return "data/characters/" + folder + "/taken.png"; }; setup.AV.riteImageForGirl = function (girl, side) { const folder = setup.AV.characterFolderName(girl); const owner = String(side || "self").toLowerCase(); return "data/characters/" + folder + "/rite/" + owner + "/passagerite.png"; }; setup.AV.tauntImageForGirlFolder = function (girlFolder) { return "data/characters/" + setup.AV.slug(girlFolder || "unknown") + "/taunt.png"; }; setup.AV.faceImage = function (girl) { if (!girl) return "data/characters/placeholder.png"; const folder = setup.AV.characterFolderName(girl); return girl.face || girl.faceImage || girl.image || ("data/characters/" + folder + "/face.png"); }; setup.AV.bodyImage = function (girl) { if (!girl) return "data/characters/placeholder.png"; const folder = setup.AV.characterFolderName(girl); if (girl.pregnant) { return girl.bodyPregnant || girl.pregnantBody || ("data/characters/" + folder + "/body_pregant.png"); } return girl.body || girl.bodyImage || ("data/characters/" + folder + "/body.png"); }; setup.AV.actionImageForGirl = function (girl, actionType, side, stage) { const key = String(actionType || "").toLowerCase(); const owner = String(side || "self").toLowerCase(); const step = String(stage || "action").toLowerCase(); const suffix = step === "finish" ? "_finish" : ""; const folder = setup.AV.characterFolderName(girl); return "data/characters/" + folder + "/" + owner + "/" + key + suffix + ".png"; }; setup.AV.ensureTauntHistory = function () { if (!State.variables.tauntHistory) State.variables.tauntHistory = {}; return State.variables.tauntHistory; }; setup.AV.ensurePossessionHistory = function () { if (!State.variables.possessionHistory) State.variables.possessionHistory = {}; return State.variables.possessionHistory; }; setup.AV.girlKey = function (girl) { if (!girl) return ""; return setup.AV.characterFolderName(girl); }; setup.AV.grudgeKey = function (a, b) { if (!a || !b) return ""; return [a.id, b.id].sort().join("__"); }; setup.AV.markGrudge = function (a, b, reason, girlData) { if (!a || !b) return; if (a.id === b.id) return; const history = setup.AV.ensureTauntHistory(); const key = setup.AV.grudgeKey(a, b); if (!key) return; let girlName = ""; let girlFolder = ""; if (girlData && typeof girlData === "object") { girlName = girlData.name || ""; girlFolder = setup.AV.characterFolderName(girlData); } else { girlName = String(girlData || ""); girlFolder = setup.AV.slug(girlName || "unknown"); } if (!history[key]) { history[key] = { aId: a.id, bId: b.id, count: 0, lastReason: "", girlName: "", girlFolder: "", day: 0 }; } history[key].count = Number(history[key].count || 0) + 1; history[key].lastReason = reason || "Old passive"; history[key].girlName = girlName || history[key].girlName || ""; history[key].girlFolder = girlFolder || history[key].girlFolder || ""; history[key].day = Number(State.variables.day || 1); }; setup.AV.recordPossession = function (region, girl, reason) { if (!region || !girl) return; const history = setup.AV.ensurePossessionHistory(); const key = setup.AV.girlKey(girl); if (!key) return; if (!history[key]) { history[key] = { girlName: girl.name || key, girlFolder: key, currentRegionId: "", regions: {} }; } if (!history[key].regions) history[key].regions = {}; if (!history[key].regions[region.id]) { history[key].regions[region.id] = { regionId: region.id, regionName: region.name || region.id, ruler: region.ruler || "", count: 0, firstDay: Number(State.variables.day || 1), lastDay: Number(State.variables.day || 1), lastReason: "" }; } history[key].regions[region.id].count = Number(history[key].regions[region.id].count || 0) + 1; history[key].regions[region.id].regionName = region.name || region.id; history[key].regions[region.id].ruler = region.ruler || ""; history[key].regions[region.id].lastDay = Number(State.variables.day || 1); history[key].regions[region.id].lastReason = reason || "Possession"; history[key].girlName = girl.name || history[key].girlName || key; history[key].girlFolder = key; history[key].currentRegionId = region.id; if (setup.AV.registerGirlForFailsafe) { setup.AV.registerGirlForFailsafe(girl, "possession recorded at " + (region.name || region.id || "Unknown")); } }; setup.AV.region = function (id) { const regions = State.variables.regions || []; for (const r of regions) { if (r && r.id === id) return r; } return null; }; setup.AV.home = function () { const regions = State.variables.regions || []; for (const r of regions) { if (r && r.owned && String(r.ruler || "") === "You") return r; } for (const r of regions) { if (r && r.id === "cinder") return r; } return regions[0] || null; }; setup.AV.getHarem = function (region) { if (!region) return []; if (setup.AV.isPlayerRegion(region)) { return State.variables.women || []; } if (!region.harem) region.harem = []; return region.harem; }; setup.AV.syncCurrentPossessions = function () { const home = setup.AV.home(); const regions = State.variables.regions || []; if (home) { const women = State.variables.women || []; for (const girl of women) { if (!girl) continue; setup.AV.recordPossession(home, girl, "Current Cinder possession"); } } for (const region of regions) { if (!region || setup.AV.isPlayerRegion(region)) continue; const harem = region.harem || []; for (const girl of harem) { if (!girl) continue; setup.AV.recordPossession(region, girl, "Current city possession"); } } }; setup.AV.repairStrandedGirls = function () { const v = State.variables; const regions = v.regions || []; if (!v.women) v.women = []; for (const region of regions) { if (!region || !region.harem || region.harem.length < 1) continue; if (!setup.AV.isPlayerRegion(region)) continue; const stranded = region.harem.slice(); region.harem = []; for (const girl of stranded) { if (!girl) continue; setup.AV.normalizeGirl(girl, "Cinder Hold"); girl.place = "Cinder Hold"; setup.AV.setCurrentCaptor(girl, "You", "self", "self"); if (v.women.indexOf(girl) < 0) { v.women.push(girl); } const home = setup.AV.home(); if (home) { setup.AV.recordPossession(home, girl, "Recovered from hidden player-owned region harem"); } } } }; setup.AV.ensureGirlFailsafeData = function () { const v = State.variables; if (!v.women) v.women = []; if (!v.freeWomenPool) v.freeWomenPool = []; if (!v.girlFailsafeRegistry) v.girlFailsafeRegistry = {}; if (!v.girlFailsafeLog) v.girlFailsafeLog = []; return v.girlFailsafeRegistry; }; setup.AV.registerGirlForFailsafe = function (girl, reason) { if (!girl) return ""; setup.AV.ensureGirlFailsafeData(); const key = setup.AV.girlKey(girl); if (!key) return ""; try { const snapshot = setup.AV.clonePlain(girl); if (snapshot) { snapshot._failsafeKey = key; snapshot._failsafeLastSeenDay = Number(State.variables.day || 1); snapshot._failsafeLastSeenReason = reason || "registered"; State.variables.girlFailsafeRegistry[key] = snapshot; } } catch (error) { State.variables.girlFailsafeRegistry[key] = girl; } return key; }; setup.AV.registerGirlsForFailsafe = function (girls, reason) { if (!girls || girls.length < 1) return; for (const girl of girls) { if (!girl) continue; setup.AV.registerGirlForFailsafe(girl, reason || "batch registered"); } }; setup.AV.registerAllKnownGirlsForFailsafe = function (reason) { const v = State.variables; const regions = v.regions || []; setup.AV.ensureGirlFailsafeData(); setup.AV.registerGirlsForFailsafe(v.startingWomenPool || [], reason || "starting pool"); setup.AV.registerGirlsForFailsafe(v.women || [], reason || "Cinder harem"); setup.AV.registerGirlsForFailsafe(v.freeWomenPool || [], reason || "scavenge pool"); for (const region of regions) { if (!region || !region.harem) continue; setup.AV.registerGirlsForFailsafe(region.harem, reason || ("city harem: " + (region.name || region.id || "unknown"))); } }; setup.AV.removeGirlFromArrayByKey = function (arr, girlKey) { if (!arr || !girlKey) return 0; let removed = 0; for (let i = arr.length - 1; i >= 0; i--) { const girl = arr[i]; if (!girl) continue; if (setup.AV.girlKey(girl) !== girlKey) continue; arr.splice(i, 1); removed += 1; } return removed; }; setup.AV.removeGirlFromAllContainers = function (girlOrKey) { const v = State.variables; const regions = v.regions || []; const girlKey = typeof girlOrKey === "string" ? setup.AV.slug(girlOrKey) : setup.AV.girlKey(girlOrKey); if (!girlKey) return 0; let removed = 0; if (!v.women) v.women = []; if (!v.freeWomenPool) v.freeWomenPool = []; removed += setup.AV.removeGirlFromArrayByKey(v.women, girlKey); removed += setup.AV.removeGirlFromArrayByKey(v.freeWomenPool, girlKey); for (const region of regions) { if (!region) continue; if (!region.harem) region.harem = []; removed += setup.AV.removeGirlFromArrayByKey(region.harem, girlKey); } return removed; }; setup.AV.hasGirlInArrayByKey = function (arr, girlKey) { if (!arr || !girlKey) return false; for (const girl of arr) { if (!girl) continue; if (setup.AV.girlKey(girl) === girlKey) return true; } return false; }; setup.AV.isGirlInAnyContainer = function (girlOrKey) { const v = State.variables; const regions = v.regions || []; const girlKey = typeof girlOrKey === "string" ? setup.AV.slug(girlOrKey) : setup.AV.girlKey(girlOrKey); if (!girlKey) return false; if (setup.AV.hasGirlInArrayByKey(v.women || [], girlKey)) return true; if (setup.AV.hasGirlInArrayByKey(v.freeWomenPool || [], girlKey)) return true; for (const region of regions) { if (!region || !region.harem) continue; if (setup.AV.hasGirlInArrayByKey(region.harem, girlKey)) return true; } return false; }; setup.AV.putGirlInScavengePool = function (girl, reason, logs) { if (!girl) return false; const v = State.variables; setup.AV.ensureGirlFailsafeData(); const girlKey = setup.AV.girlKey(girl); if (!girlKey) return false; setup.AV.removeGirlFromAllContainers(girlKey); setup.AV.normalizeGirl(girl, "Ruins"); girl.place = "Ruins"; girl.currentCaptorName = "No city"; girl.currentCaptorKey = "free"; girl.currentCaptorSide = "free"; if (!setup.AV.hasGirlInArrayByKey(v.freeWomenPool, girlKey)) { v.freeWomenPool.push(girl); } setup.AV.registerGirlForFailsafe(girl, reason || "moved to scavenge pool"); v.girlFailsafeLog.push({ day: Number(v.day || 1), girlKey: girlKey, girlName: girl.name || girlKey, reason: reason || "missing from every city", result: "freeWomenPool" }); if (logs) { logs.push("Failsafe: " + (girl.name || girlKey) + " was in no city, so she was moved to the scavenge pool."); } return true; }; setup.AV.repairMissingGirlsToScavengePool = function (reason, logs) { const v = State.variables; let repaired = 0; setup.AV.ensureGirlFailsafeData(); setup.AV.registerAllKnownGirlsForFailsafe(reason || "failsafe scan"); const registry = v.girlFailsafeRegistry || {}; for (const key in registry) { if (!Object.prototype.hasOwnProperty.call(registry, key)) continue; if (setup.AV.isGirlInAnyContainer(key)) continue; const snapshot = registry[key]; if (!snapshot) continue; const recovered = setup.AV.clonePlain(snapshot); if (!recovered) continue; delete recovered._failsafeKey; delete recovered._failsafeLastSeenDay; delete recovered._failsafeLastSeenReason; if (setup.AV.putGirlInScavengePool(recovered, reason || "missing from every city", logs)) { repaired += 1; } } return repaired; }; setup.AV.getCurrentGirlFromRegionByFolder = function (region, girlFolder) { if (!region || !girlFolder) return null; const harem = setup.AV.getHarem(region) || []; const wanted = setup.AV.slug(girlFolder); for (const girl of harem) { if (!girl) continue; if (setup.AV.characterFolderName(girl) === wanted) return girl; } return null; }; setup.AV.getTauntGirlForPair = function (taunter, target) { if (!taunter || !target) { return { girlName: "unknown", girlFolder: "unknown", source: "none", valid: false }; } const direct = setup.AV.ensureTauntHistory(); const key = setup.AV.grudgeKey(taunter, target); const directItem = direct[key]; if (directItem && (directItem.girlFolder || directItem.girlName)) { const directFolder = directItem.girlFolder || setup.AV.slug(directItem.girlName || "unknown"); const currentGirl = setup.AV.getCurrentGirlFromRegionByFolder(taunter, directFolder); if (currentGirl) { return { girlName: currentGirl.name || directItem.girlName || directFolder, girlFolder: setup.AV.characterFolderName(currentGirl), source: "direct passive", valid: true }; } } const possession = setup.AV.ensurePossessionHistory(); for (const girlKey in possession) { if (!Object.prototype.hasOwnProperty.call(possession, girlKey)) continue; const item = possession[girlKey]; if (!item || !item.regions) continue; if (!item.regions[taunter.id]) continue; if (!item.regions[target.id]) continue; const currentGirl = setup.AV.getCurrentGirlFromRegionByFolder(taunter, item.girlFolder || girlKey); if (currentGirl) { return { girlName: currentGirl.name || item.girlName || girlKey, girlFolder: setup.AV.characterFolderName(currentGirl), source: "shared possession", valid: true }; } } return { girlName: "unknown", girlFolder: "unknown", source: "none", valid: false }; }; setup.AV.tauntImageForPair = function (taunter, target) { const girl = setup.AV.getTauntGirlForPair(taunter, target); return setup.AV.tauntImageForGirlFolder(girl.girlFolder || girl.girlName || "unknown"); }; setup.AV.chooseEnemyTauntTarget = function (enemy) { if (!enemy || enemy.owned) return null; const regions = State.variables.regions || []; let best = null; let bestScore = -9999; for (const target of regions) { if (!target) continue; if (target.id === enemy.id) continue; if (String(target.ruler || "") === String(enemy.ruler || "")) continue; const tauntGirl = setup.AV.getTauntGirlForPair(enemy, target); if (!tauntGirl.valid) continue; const isPlayer = setup.AV.isPlayerRegion(target); const score = Number(target.influence || 0) + (isPlayer ? 5 : 0) + setup.AV.roll(0, 24); if (score > bestScore) { bestScore = score; best = target; } } return best; }; setup.AV.pushRecapEvent = function (events, event) { if (!events) return; events.push({ type: event.type || "generic", title: event.title || "Report", subtitle: event.subtitle || "", image: event.image || setup.AV.nightMapImage(), image2: event.image2 || "", imageLabel: event.imageLabel || "Image", image2Label: event.image2Label || "Second image", intro: event.intro || "", body: event.body || "", finish: event.finish || "", stats: event.stats || [] }); }; setup.AV.cloneGirl = function (girl) { return JSON.parse(JSON.stringify(girl)); }; setup.AV.normalizeGirl = function (girl, defaultPlace) { if (!girl) return null; if (!girl.name) girl.name = "Unknown Girl"; if (!girl.place) girl.place = defaultPlace || "Unassigned"; const folder = setup.AV.characterFolderName(girl); if (!girl.face && !girl.faceImage && !girl.image) girl.face = "data/characters/" + folder + "/face.png"; if (!girl.body && !girl.bodyImage) girl.body = "data/characters/" + folder + "/body.png"; if (!girl.image) girl.image = girl.face || girl.faceImage || "data/characters/" + folder + "/face.png"; if (!girl.face) girl.face = girl.faceImage || girl.image || "data/characters/" + folder + "/face.png"; if (!girl.body) girl.body = girl.bodyImage || "data/characters/" + folder + "/body.png"; if (girl.pregnant == null) girl.pregnant = false; if (!girl.pregnantBy) girl.pregnantBy = "None"; if (!girl.pregnant) girl.pregnantBy = "None"; if (!girl.firstTime) girl.firstTime = "None"; if (!girl.favoritePerson) girl.favoritePerson = "None"; if (girl.vaginal == null) girl.vaginal = 0; if (girl.anal == null) girl.anal = 0; if (girl.oral == null) girl.oral = 0; if (girl.virgin == null) girl.virgin = true; setup.AV.ensureBondData(girl); return girl; }; setup.AV.setCurrentCaptor = function (girl, actorName, actorKey, side) { if (!girl) return; const finalSide = side || "enemy"; const finalName = actorName || "Unknown"; const finalKey = actorKey || setup.AV.actorKeyForName(finalName, finalSide); girl.currentCaptorName = finalName; girl.currentCaptorKey = finalKey; girl.currentCaptorSide = finalSide; setup.AV.getBondData(girl, finalKey); }; setup.AV.distributeStartingWomen = function () { const v = State.variables; if (v.girlsDistributed) return; const regions = v.regions || []; const enemies = regions.filter(function (r) { return r && !setup.AV.isPlayerRegion(r); }); let pool = []; if (v.startingWomenPool && v.startingWomenPool.length > 0) { pool = v.startingWomenPool; } else if (v.women && v.women.length > 0) { pool = v.women; } v.women = []; v.freeWomenPool = []; for (const r of enemies) { r.harem = []; } const clonedPool = []; for (let i = 0; i < pool.length; i++) { const girl = setup.AV.cloneGirl(pool[i]); setup.AV.normalizeGirl(girl, "Unassigned"); clonedPool.push(girl); } const savedKey = setup.AV.slug(v.savedGirlKey || ""); const savedName = String(v.savedGirlName || "").trim().toLowerCase(); let savedIndex = -1; if (savedKey) { for (let i = 0; i < clonedPool.length; i++) { if (setup.AV.characterFolderName(clonedPool[i]) === savedKey) { savedIndex = i; break; } } } if (savedIndex < 0 && savedName) { for (let i = 0; i < clonedPool.length; i++) { if (String(clonedPool[i].name || "").trim().toLowerCase() === savedName) { savedIndex = i; break; } } } if (savedIndex < 0) { for (let i = 0; i < clonedPool.length; i++) { if (String(clonedPool[i].name || "").toLowerCase() === "jane") { savedIndex = i; break; } } } if (savedIndex < 0 && clonedPool.length > 0) savedIndex = 0; const remaining = []; for (let i = 0; i < clonedPool.length; i++) { const girl = clonedPool[i]; if (i === savedIndex) { girl.place = "Cinder Hold"; setup.AV.setCurrentCaptor(girl, "You", "self", "self"); v.women.push(girl); const home = setup.AV.home(); if (home) setup.AV.recordPossession(home, girl, "Saved during the university collapse"); } else { remaining.push(girl); } } const shuffled = setup.AV.shuffle(remaining); for (const enemy of enemies) { if (!enemy.harem) enemy.harem = []; if (shuffled.length < 1) break; const girl = shuffled.shift(); girl.place = enemy.name || "Unknown"; setup.AV.setCurrentCaptor(girl, setup.AV.actorNameForRegion(enemy), setup.AV.actorKeyForRegion(enemy), setup.AV.actorSideForRegion(enemy)); enemy.harem.push(girl); setup.AV.recordPossession(enemy, girl, "Scattered after the university collapse"); } for (const girl of shuffled) { girl.place = "Unassigned"; v.freeWomenPool.push(girl); } v.girlsDistributed = true; }; setup.AV.clonePlain = setup.AV.clonePlain || function (value) { try { return JSON.parse(JSON.stringify(value || [])); } catch (error) { if (Array.isArray(value)) return value.slice(); return value; } }; setup.AV.defaultRegions = setup.AV.defaultRegions || function () { return [ { id: "cinder", name: "Cinder Hold", owned: true, ruler: "You", power: 35, basePower: 35, influence: 8, x: 50, y: 58, color: "rgba(223,255,93,.35)", harem: [] }, { id: "iron_veil", name: "Iron Veil", owned: false, ruler: "Marshal Kade", power: 32, basePower: 32, influence: 8, x: 23, y: 26, color: "rgba(255,91,32,.35)", harem: [] }, { id: "red_market", name: "Red Market", owned: false, ruler: "Madam Sable", power: 30, basePower: 30, influence: 8, x: 72, y: 25, color: "rgba(255,91,32,.35)", harem: [] }, { id: "salt_crown", name: "Salt Crown", owned: false, ruler: "Captain Oro", power: 30, basePower: 30, influence: 8, x: 84, y: 55, color: "rgba(255,91,32,.35)", harem: [] }, { id: "grave_radio", name: "Grave Radio", owned: false, ruler: "Saint Vox", power: 28, basePower: 28, influence: 8, x: 67, y: 78, color: "rgba(255,91,32,.35)", harem: [] }, { id: "black_lotus", name: "Black Lotus", owned: false, ruler: "Lady Nyx", power: 31, basePower: 31, influence: 8, x: 35, y: 80, color: "rgba(255,91,32,.35)", harem: [] }, { id: "rust_gate", name: "Rust Gate", owned: false, ruler: "Baron Rusk", power: 29, basePower: 29, influence: 8, x: 15, y: 57, color: "rgba(255,91,32,.35)", harem: [] }, { id: "white_spire", name: "White Spire", owned: false, ruler: "The Pale Court", power: 33, basePower: 33, influence: 8, x: 51, y: 17, color: "rgba(255,91,32,.35)", harem: [] } ]; }; setup.AV.ensureRegionsReady = function () { const v = State.variables; if (!Array.isArray(v.regions) || v.regions.length < 1) { if (Array.isArray(v._avStartOriginalRegions) && v._avStartOriginalRegions.length > 0) { v.regions = setup.AV.clonePlain(v._avStartOriginalRegions); } else { v.regions = setup.AV.defaultRegions(); } } let hasHome = false; for (const region of v.regions) { if (!region) continue; if (region.id === "cinder" || (region.owned && String(region.ruler || "") === "You")) { hasHome = true; region.id = region.id || "cinder"; region.name = region.name || "Cinder Hold"; region.owned = true; region.ruler = "You"; region.color = "rgba(223,255,93,.35)"; if (!Number.isFinite(Number(region.power))) region.power = 35; if (!Number.isFinite(Number(region.basePower))) region.basePower = Number(region.power || 35); if (!Number.isFinite(Number(region.influence))) region.influence = 8; if (!region.harem) region.harem = []; break; } } if (!hasHome) { v.regions.unshift(setup.AV.defaultRegions()[0]); } }; setup.AV.ensureRegionPositions = function () { setup.AV.ensureRegionsReady(); const regions = State.variables.regions; const spots = setup.AV.defaultRegionSpots; const usedIds = {}; for (let i = 0; i < regions.length; i++) { const r = regions[i]; if (!r) continue; let rawId = String(r.id || "").trim(); if (!rawId) rawId = setup.AV.slug(r.name || ("region_" + i)); if (usedIds[rawId]) { let newId = setup.AV.slug(r.name || rawId || ("region_" + i)); if (!newId) newId = "region_" + i; if (usedIds[newId]) newId = newId + "_" + i; r.id = newId; usedIds[newId] = true; } else { r.id = rawId; usedIds[rawId] = true; } const spot = spots[i % spots.length]; if (!Number.isFinite(Number(r.x))) r.x = spot.x; if (!Number.isFinite(Number(r.y))) r.y = spot.y; if (!Number.isFinite(Number(r.influence))) r.influence = 8; if (!Number.isFinite(Number(r.power))) r.power = 30; if (!Number.isFinite(Number(r.basePower))) r.basePower = Number(r.power || 30); if (!r.color) r.color = r.owned ? "rgba(223,255,93,.35)" : "rgba(255,91,32,.35)"; if (!r.harem) r.harem = []; } }; setup.AV.normalizeWomen = function () { const v = State.variables; const regions = v.regions || []; if (!v.women) v.women = []; if (!v.freeWomenPool) v.freeWomenPool = []; for (const w of v.women) { setup.AV.normalizeGirl(w, "Cinder Hold"); setup.AV.registerGirlForFailsafe(w, "normalized in Cinder Hold"); } for (const w of v.freeWomenPool) { setup.AV.normalizeGirl(w, "Ruins"); setup.AV.registerGirlForFailsafe(w, "normalized in scavenge pool"); } for (const r of regions) { if (!r) continue; if (!r.harem) r.harem = []; for (const w of r.harem) { setup.AV.normalizeGirl(w, r.name || "Unknown"); setup.AV.registerGirlForFailsafe(w, "normalized in " + (r.name || "Unknown")); } } setup.AV.registerAllKnownGirlsForFailsafe("normalizeWomen full scan"); }; setup.AV.ensureGame = function () { const v = State.variables; if (v.actionsLeft == null) v.actionsLeft = 5; if (!v.pendingPlayerActions) v.pendingPlayerActions = []; if (!v.dayRecap) v.dayRecap = []; if (!v.recapEvents) v.recapEvents = []; if (!v.enemyWarSupports) v.enemyWarSupports = []; if (!v.tauntHistory) v.tauntHistory = {}; if (!v.possessionHistory) v.possessionHistory = {}; if (v.recapIndex == null) v.recapIndex = 0; if (v.showRecapPopup == null) v.showRecapPopup = false; if (v.gameOver == null) v.gameOver = false; if (!v.gameOverReason) v.gameOverReason = ""; if (!v.freeWomenPool) v.freeWomenPool = []; if (v.girlsDistributed == null) v.girlsDistributed = false; setup.AV.ensureNightSystems(); setup.AV.ensureGirlFailsafeData(); delete v.player; delete v.food; delete v.scrap; delete v.fuel; setup.AV.ensureRegionPositions(); const home = setup.AV.home(); if (home) { home.owned = true; home.ruler = "You"; home.color = "rgba(223,255,93,.35)"; } setup.AV.distributeStartingWomen(); setup.AV.normalizeWomen(); setup.AV.repairStrandedGirls(); setup.AV.registerAllKnownGirlsForFailsafe("ensureGame scan"); setup.AV.repairMissingGirlsToScavengePool("ensureGame missing girl recovery", null); setup.AV.syncCurrentPossessions(); }; setup.AV.effectiveInfluence = function (region) { if (!region) return 5; const raw = Number(region.influence || 0); if (!Number.isFinite(raw)) return 5; return Math.max(5, Math.min(150, raw)); }; setup.AV.warOverlapData = function (a, b) { if (!a || !b) return { active: false, x: 50, y: 50, depth: 0, distance: 999, rA: 0, rB: 0 }; if (a.id === b.id || String(a.ruler || "") === String(b.ruler || "")) { return { active: false, x: (Number(a.x || 50) + Number(b.x || 50)) / 2, y: (Number(a.y || 50) + Number(b.y || 50)) / 2, depth: 0, distance: 999, rA: 0, rB: 0 }; } const ax = Number(a.x || 50); const ay = Number(a.y || 50); const bx = Number(b.x || 50); const by = Number(b.y || 50); const yScale = 9 / 16; const aPhys = { x: ax, y: ay * yScale }; const bPhys = { x: bx, y: by * yScale }; const dx = bPhys.x - aPhys.x; const dy = bPhys.y - aPhys.y; const distance = Math.sqrt((dx * dx) + (dy * dy)); const rA = setup.AV.effectiveInfluence(a); const rB = setup.AV.effectiveInfluence(b); const depth = (rA + rB) - distance; if (depth <= 0) return { active: false, x: (ax + bx) / 2, y: (ay + by) / 2, depth: depth, distance: distance, rA: rA, rB: rB }; let t = 0.5; if (distance > 0.001) { const pointOnLine = ((distance * distance) + (rA * rA) - (rB * rB)) / (2 * distance); t = pointOnLine / distance; t = Math.max(0.18, Math.min(0.82, t)); } const overlapPhysX = aPhys.x + (dx * t); const overlapPhysY = aPhys.y + (dy * t); return { active: true, x: Math.max(2, Math.min(98, overlapPhysX)), y: Math.max(2, Math.min(98, overlapPhysY / yScale)), depth: depth, distance: distance, rA: rA, rB: rB }; }; setup.AV.overlapPower = function (a, b) { const data = setup.AV.warOverlapData(a, b); return data.depth; }; setup.AV.hasWarFront = function (a, b) { if (!a || !b) return false; if (a.id === b.id) return false; if (String(a.ruler || "") === String(b.ruler || "")) return false; return setup.AV.warOverlapData(a, b).active; }; setup.AV.hasWarOnPlayerTerritory = function () { const home = setup.AV.region("cinder") || setup.AV.home(); const regions = State.variables.regions || []; if (!home) return false; for (const r of regions) { if (!r || r.id === home.id) continue; if (setup.AV.isPlayerRegion(r)) continue; if (setup.AV.hasWarFront(home, r)) return true; } return false; }; setup.AV.getCinderThreat = function () { const home = setup.AV.region("cinder") || setup.AV.home(); const regions = State.variables.regions || []; let best = null; let bestScore = -9999; if (!home) return null; for (const enemy of regions) { if (!enemy || enemy.id === home.id) continue; if (setup.AV.isPlayerRegion(enemy)) continue; if (!setup.AV.hasWarFront(home, enemy)) continue; const score = Number(enemy.power || 0) + Number(enemy.influence || 0) + Math.max(0, Math.round(setup.AV.overlapPower(home, enemy) * 5)); if (score > bestScore) { bestScore = score; best = enemy; } } return best; }; setup.AV.getEnemyTargets = function (enemy) { const regions = State.variables.regions || []; const targets = []; for (const target of regions) { if (!target || !enemy) continue; if (target.id === enemy.id) continue; if (String(target.ruler || "") === String(enemy.ruler || "")) continue; targets.push(target); } return targets; }; setup.AV.chooseEnemyWarTarget = function (enemy) { const targets = setup.AV.getEnemyTargets(enemy); let best = null; let bestScore = -9999; for (const target of targets) { if (!setup.AV.hasWarFront(enemy, target)) continue; const enemyPower = Number(enemy.power || 0); const targetPower = Number(target.power || 0); const isPlayer = setup.AV.isPlayerRegion(target); const score = (enemyPower - targetPower) + (targetPower < enemyPower ? 22 : 0) + (targetPower < 14 ? 24 : 0) + (setup.AV.getEligibleHaremGirls(target).length > 0 ? 10 : 0) + Math.max(0, Math.round(setup.AV.overlapPower(enemy, target) * 3)) + (isPlayer ? 8 : 0) + setup.AV.roll(0, 28); if (score > bestScore) { bestScore = score; best = target; } } return best; }; setup.AV.chooseEnemyExtractionTarget = function (enemy) { const targets = setup.AV.getEnemyTargets(enemy); let best = null; let bestScore = -9999; for (const target of targets) { if (!setup.AV.hasExtractionRoute(enemy, target)) continue; if (!setup.AV.canTargetPlayerHaremThisRecap(target)) continue; if (setup.AV.isRegionProtected(target)) continue; const harem = setup.AV.getEligibleHaremGirls(target); if (!harem || harem.length < 1) continue; const enemyPower = Number(enemy.power || 0); const targetPower = Number(target.power || 0); const isPlayer = setup.AV.isPlayerRegion(target); const overlap = setup.AV.overlapPower(enemy, target); const score = Math.min(30, harem.length * 6) + (targetPower < enemyPower ? 18 : 0) + 18 + (overlap > 0 ? Math.min(22, Math.round(overlap * 3)) : 0) + (isPlayer ? 10 : 0) + setup.AV.roll(0, 32); if (score > bestScore) { bestScore = score; best = target; } } return best; }; setup.AV.chooseEnemyIncursionTarget = function (enemy) { if (!setup.AV.canStartIncursionThisRecap()) return null; const targets = setup.AV.getEnemyTargets(enemy); let best = null; let bestScore = -9999; for (const target of targets) { if (!target) continue; const isPlayer = setup.AV.isPlayerRegion(target); const targetPower = Number(target.power || 0); const targetInfluence = Number(target.influence || 0); const enemyPower = Number(enemy.power || 0); const enemyInfluence = Number(enemy.influence || 0); const frontBonus = setup.AV.hasWarFront(enemy, target) ? 10 : 0; const score = (enemyPower - targetPower) + ((enemyInfluence - targetInfluence) * 1.2) + (targetPower < 18 ? 12 : 0) + (targetInfluence < 10 ? 8 : 0) + frontBonus + (isPlayer ? 6 : 0) + setup.AV.roll(0, 36); if (score > bestScore) { bestScore = score; best = target; } } return best; }; setup.AV.chooseEnemyExpandTarget = function (enemy) { const targets = setup.AV.getEnemyTargets(enemy); let best = null; let bestScore = -9999; for (const target of targets) { const overlap = setup.AV.overlapPower(enemy, target); const score = (overlap > -10 ? 15 : 0) + (setup.AV.isPlayerRegion(target) ? -4 : 0) + setup.AV.roll(0, 12); if (score > bestScore) { bestScore = score; best = target; } } return best; }; setup.AV.warKey = function (a, b) { if (!a || !b) return ""; return [a.id, b.id].sort().join("__"); }; setup.AV.buildWarFrontSnapshot = function () { const regions = State.variables.regions || []; const snapshot = {}; for (let i = 0; i < regions.length; i++) { for (let j = i + 1; j < regions.length; j++) { const a = regions[i]; const b = regions[j]; if (!a || !b) continue; if (!setup.AV.hasWarFront(a, b)) continue; snapshot[setup.AV.warKey(a, b)] = true; } } return snapshot; }; setup.AV.hasExtractionRoute = function (a, b) { if (!a || !b) return false; if (State.variables.isResolvingDay) { const snapshot = State.variables.extractionFrontsAtRecapStart || {}; return !!snapshot[setup.AV.warKey(a, b)]; } return setup.AV.hasWarFront(a, b); }; setup.AV.getActiveWarFronts = function () { const regions = State.variables.regions || []; const fronts = []; for (let i = 0; i < regions.length; i++) { for (let j = i + 1; j < regions.length; j++) { const a = regions[i]; const b = regions[j]; if (!a || !b) continue; if (!setup.AV.hasWarFront(a, b)) continue; fronts.push({ key: setup.AV.warKey(a, b), aId: a.id, bId: b.id }); } } return fronts; }; setup.AV.chooseEnemySupportWar = function (enemy) { if (!enemy || enemy.owned) return null; if (Number(enemy.power || 0) <= 5) return null; const fronts = setup.AV.getActiveWarFronts(); let best = null; let bestScore = -9999; for (const front of fronts) { const a = setup.AV.region(front.aId); const b = setup.AV.region(front.bId); if (!a || !b) continue; if (a.id === enemy.id || b.id === enemy.id) continue; if (String(a.ruler || "") === String(enemy.ruler || "")) continue; if (String(b.ruler || "") === String(enemy.ruler || "")) continue; const scoreA = (Number(a.power || 0) < Number(b.power || 0) ? 8 : 0) + Math.min(18, setup.AV.getEligibleHaremGirls(b).length * 4) + (!setup.AV.isPlayerRegion(a) ? 8 : -2) + setup.AV.roll(0, 14); const scoreB = (Number(b.power || 0) < Number(a.power || 0) ? 8 : 0) + Math.min(18, setup.AV.getEligibleHaremGirls(a).length * 4) + (!setup.AV.isPlayerRegion(b) ? 8 : -2) + setup.AV.roll(0, 14); const side = scoreA >= scoreB ? a : b; const other = side.id === a.id ? b : a; const finalScore = Math.max(scoreA, scoreB); if (finalScore > bestScore) { bestScore = finalScore; best = { key: front.key, aId: a.id, bId: b.id, sideId: side.id, otherId: other.id }; } } return best; }; setup.AV.queueEnemyWarSupport = function (enemy, plan, logs) { if (!enemy || !plan) return; if (!State.variables.enemyWarSupports) State.variables.enemyWarSupports = []; const side = setup.AV.region(plan.sideId); const other = setup.AV.region(plan.otherId); if (!side || !other) return; const supportPower = Math.max(1, Math.min(10, Math.floor(Number(enemy.power || 0) * 0.12) + setup.AV.roll(1, 3))); const cost = Math.max(1, Math.min(Number(enemy.power || 1) - 1, Math.ceil(supportPower / 2))); enemy.power = Math.max(1, Number(enemy.power || 1) - cost); State.variables.enemyWarSupports.push({ key: plan.key, aId: plan.aId, bId: plan.bId, sideId: plan.sideId, otherId: plan.otherId, supporterId: enemy.id, supporterName: enemy.name, supportPower: supportPower, cost: cost }); if (logs) logs.push(enemy.name + " sided with " + side.name + " against " + other.name + " with +" + supportPower + " support power."); }; setup.AV.drawMap = function () { const regions = State.variables.regions || []; const zonesLayer = document.getElementById("av-zones-layer"); const warLayer = document.getElementById("av-war-layer"); const pointsLayer = document.getElementById("av-points-layer"); if (!zonesLayer || !warLayer || !pointsLayer) return; zonesLayer.innerHTML = ""; warLayer.innerHTML = ""; pointsLayer.innerHTML = ""; setup.AV.ensureRegionPositions(); const mapRegions = []; for (let i = 0; i < regions.length; i++) { const r = regions[i]; if (!r) continue; let x = Number(r.x); let y = Number(r.y); const influence = setup.AV.effectiveInfluence(r); if (!Number.isFinite(x)) x = 50; if (!Number.isFinite(y)) y = 52; x = Math.max(2, Math.min(98, x)); y = Math.max(2, Math.min(98, y)); const color = r.color || (r.owned ? "rgba(223,255,93,.35)" : "rgba(255,91,32,.35)"); const stateClass = setup.AV.isPlayerRegion(r) ? "owned" : "enemy"; mapRegions.push({ region: r, x: x, y: y, influence: influence, color: color, stateClass: stateClass }); const zone = document.createElement("div"); zone.className = "influence-zone " + stateClass; zone.style.left = x + "%"; zone.style.top = y + "%"; zone.style.setProperty("--d", (influence * 2) + "%"); zone.style.setProperty("--c", color); zonesLayer.appendChild(zone); } for (let a = 0; a < mapRegions.length; a++) { for (let b = a + 1; b < mapRegions.length; b++) { const first = mapRegions[a]; const second = mapRegions[b]; const r1 = first.region; const r2 = second.region; const warData = setup.AV.warOverlapData(r1, r2); if (!warData.active) continue; const warSize = Math.max(6, Math.min(18, 5 + (warData.depth * 1.35))); const danger = Math.max(10, Math.min(100, Math.round(warData.depth * 14))); const warBtn = document.createElement("button"); warBtn.type = "button"; warBtn.className = "war-zone-btn"; warBtn.style.left = warData.x + "%"; warBtn.style.top = warData.y + "%"; warBtn.style.setProperty("--s", warSize + "%"); warBtn.title = "War zone: " + (r1.name || "Region") + " / " + (r2.name || "Region"); const warText = document.createElement("span"); warText.textContent = "WAR"; warBtn.appendChild(warText); warBtn.addEventListener("click", function () { setup.AV.openWarZone(r1.id, r2.id, danger); }); warLayer.appendChild(warBtn); } } for (let i = 0; i < mapRegions.length; i++) { const item = mapRegions[i]; const r = item.region; const btn = document.createElement("button"); btn.type = "button"; btn.className = "region-btn " + item.stateClass; btn.style.left = item.x + "%"; btn.style.top = item.y + "%"; btn.title = r.name || "Region"; btn.addEventListener("click", function () { State.variables.selectedRegion = r.id; State.variables.tab = "region"; setup.AV.play("Main"); }); pointsLayer.appendChild(btn); const label = document.createElement("div"); label.className = "region-label"; label.style.left = item.x + "%"; label.style.top = item.y + "%"; label.textContent = r.name || "Region"; pointsLayer.appendChild(label); } }; setup.AV.consumeAction = function (hours, powerCost) { const v = State.variables; setup.AV.ensureGame(); if (v.actionsLeft <= 0) return false; v.actionsLeft -= 1; v.hour = Math.min(23, Number(v.hour || 10) + Number(hours || 1)); const home = setup.AV.home(); if (home && powerCost > 0) home.power = Math.max(1, Number(home.power || 1) - Number(powerCost || 0)); return true; }; setup.AV.hasQueued = function (type, targetId, girlName) { const actions = State.variables.pendingPlayerActions || []; for (const a of actions) { if (!a) continue; if (a.type !== type) continue; if (targetId && a.targetId !== targetId) continue; if (girlName && a.girlName !== girlName) continue; return true; } return false; }; setup.AV.canQueueFullDayAction = function () { setup.AV.ensureGame(); const v = State.variables; if (Number(v.actionsLeft || 0) !== 5) return false; if ((v.pendingPlayerActions || []).length > 0) return false; return true; }; setup.AV.queueWorkPower = function () { const v = State.variables; if (!setup.AV.canQueueFullDayAction()) { return; } v.actionsLeft = 0; v.hour = 23; v.playerWorkedPowerToday = true; v.pendingPlayerActions.push({ type: "workPower", label: "Work on power", hours: 5, powerGain: setup.AV.WORK_POWER_GAIN }); }; setup.AV.queueProtectHome = function () { const v = State.variables; const home = setup.AV.home(); if (!home) return; if (setup.AV.hasQueued("protectHome", "", "")) return; if (!setup.AV.canPayProtection(home)) { setup.AV.notify("Not enough Power or Influence to protect Cinder tonight."); return; } if (!setup.AV.consumeAction(2, 0)) return; setup.AV.payProtectionCost(home); v.pendingPlayerActions.push({ type: "protectHome", label: "Protect Cinder tonight", hours: 2 }); }; setup.AV.queueScavenge = function () { const v = State.variables; if (!setup.AV.consumeAction(3, 1)) return; v.pendingPlayerActions.push({ type: "scavenge", label: "Scavenge", hours: 3 }); }; setup.AV.queueInfluenceWork = function () { const v = State.variables; if (!setup.AV.consumeAction(3, 0)) return; v.pendingPlayerActions.push({ type: "workInfluence", label: "Work on influence", hours: 3 }); }; setup.AV.queueSelfHaremAction = function (girlIndex, actionType) { const v = State.variables; const girl = (v.women || [])[girlIndex]; const action = String(actionType || "").toLowerCase(); if (!girl) return; if (["vaginal", "anal", "oral"].indexOf(action) < 0) return; if (setup.AV.hasQueued("selfHarem", "", girl.name + "_" + action)) return; if (!setup.AV.consumeAction(2, 0)) return; v.pendingPlayerActions.push({ type: "selfHarem", girlIndex: girlIndex, girlName: girl.name + "_" + action, realGirlName: girl.name, actionType: action, label: "Harem step: " + girl.name + " / " + action, hours: 2 }); }; setup.AV.queueIncursion = function (targetId) { const v = State.variables; const target = setup.AV.region(targetId); if (!target || target.owned) return; if (setup.AV.hasAnyQueuedPlayerIncursion()) { setup.AV.notify("Only one incursion can be prepared per night."); return; } if (setup.AV.hasQueued("incursion", targetId, "")) return; if (!setup.AV.consumeAction(4, 2)) return; v.pendingPlayerActions.push({ type: "incursion", targetId: targetId, targetName: target.name, label: "Incursion against " + target.name, hours: 4 }); }; setup.AV.queueSteal = function (targetId, girlIndex) { const v = State.variables; const home = setup.AV.home(); const target = setup.AV.region(targetId); if (!home || !target || target.owned) return; if (!setup.AV.hasExtractionRoute(home, target)) { setup.AV.notify("Extraction impossible: Cinder's influence does not touch " + target.name + "."); return; } const harem = setup.AV.getHarem(target); const girl = harem[girlIndex]; if (!girl) return; if (!setup.AV.isGirlStealable(girl)) return; if (setup.AV.hasQueued("stealGirl", targetId, girl.name)) return; if (!setup.AV.consumeAction(4, 2)) return; v.pendingPlayerActions.push({ type: "stealGirl", targetId: targetId, targetName: target.name, girlIndex: girlIndex, girlName: girl.name, label: "Extraction from " + target.name + ": " + girl.name, hours: 4 }); }; setup.AV.openWarZone = function (regionAId, regionBId, danger) { const a = setup.AV.region(regionAId); const b = setup.AV.region(regionBId); State.variables.selectedWarZone = { aId: regionAId, bId: regionBId, aName: a && a.name ? a.name : "Unknown", bName: b && b.name ? b.name : "Unknown", danger: Number(danger || 10) }; State.variables.tab = "war"; setup.AV.play("Main"); }; setup.AV.queueWarFight = function () { const v = State.variables; const war = v.selectedWarZone; if (!war) return; if (!setup.AV.consumeAction(5, 3)) return; v.pendingPlayerActions.push({ type: "warFight", aId: war.aId, bId: war.bId, label: "Fight war zone: " + war.aName + " / " + war.bName, hours: 5 }); }; setup.AV.queueSupportWar = function (sideId) { const v = State.variables; const war = v.selectedWarZone; if (!war || !sideId) return; if (!setup.AV.consumeAction(4, 2)) return; const side = setup.AV.region(sideId); v.pendingPlayerActions.push({ type: "warSupport", aId: war.aId, bId: war.bId, sideId: sideId, sideName: side && side.name ? side.name : "Unknown", label: "Support war side: " + (side && side.name ? side.name : "Unknown"), hours: 4 }); }; setup.AV.giveGirlToPlayer = function (girl) { if (!girl) return; setup.AV.ensureGirlFailsafeData(); setup.AV.registerGirlForFailsafe(girl, "before moving to Cinder Hold"); setup.AV.removeGirlFromAllContainers(girl); if (!State.variables.women) State.variables.women = []; setup.AV.normalizeGirl(girl, "Cinder Hold"); girl.place = "Cinder Hold"; setup.AV.setCurrentCaptor(girl, "You", "self", "self"); if (!setup.AV.hasGirlInArrayByKey(State.variables.women, setup.AV.girlKey(girl))) { State.variables.women.push(girl); } setup.AV.registerGirlForFailsafe(girl, "moved to Cinder Hold"); const home = setup.AV.home(); if (home) setup.AV.recordPossession(home, girl, "Moved to Cinder Hold"); }; setup.AV.giveGirlToRegion = function (girl, region) { if (!girl || !region) return; if (setup.AV.isPlayerRegion(region)) { setup.AV.giveGirlToPlayer(girl); return; } setup.AV.ensureGirlFailsafeData(); setup.AV.registerGirlForFailsafe(girl, "before moving to city"); setup.AV.removeGirlFromAllContainers(girl); if (!region.harem) region.harem = []; setup.AV.normalizeGirl(girl, region.name || "Unknown"); girl.place = region.name || "Unknown"; const actorName = setup.AV.actorNameForRegion(region); const actorKey = setup.AV.actorKeyForRegion(region); const side = setup.AV.actorSideForRegion(region); setup.AV.setCurrentCaptor(girl, actorName, actorKey, side); if (!setup.AV.hasGirlInArrayByKey(region.harem, setup.AV.girlKey(girl))) { region.harem.push(girl); } setup.AV.registerGirlForFailsafe(girl, "moved to " + (region.name || "Unknown")); setup.AV.recordPossession(region, girl, "Moved to " + (region.name || "Unknown")); }; setup.AV.applyHaremAction = function (girl, actorName, actionType, options) { options = options || {}; const action = String(actionType || "").toLowerCase(); const side = options.side || (String(actorName || "") === "You" ? "self" : "enemy"); const actorKey = options.actorKey || setup.AV.actorKeyForName(actorName, side); const finalActorName = actorName || "Unknown"; if (!girl) return { mode: "failed", title: "Action failed", subtitle: "No target", image: setup.AV.nightMapImage(), body: "No one was found for this order.", stats: ["No target"] }; setup.AV.normalizeGirl(girl, girl.place || "Unknown"); setup.AV.setCurrentCaptor(girl, finalActorName, actorKey, side); const bond = setup.AV.getBondData(girl, actorKey); if (!bond.conquered && Number(bond.resistance || 0) < 3) { bond.resistance = Math.min(3, Number(bond.resistance || 0) + 1); if (side === "self") { return { mode: "resistance", title: "Resistance lowered", subtitle: girl.name + " / You", image: setup.AV.resistanceImageForGirl(girl), imageLabel: "Resistance", intro: "Cinder does not try to break " + girl.name + " in one night.", body: "\"You do not have to trust this place today,\" you tell her. \"Just breathe. Stay alive. Let the walls protect you first.\"", finish: bond.resistance >= 3 ? girl.name + " no longer sees every room in Cinder as a trap." : girl.name + " is still guarded, but Cinder feels a little less like the end of the world.", stats: ["Target: " + girl.name, "Resistance: " + bond.resistance + " / 3"] }; } return { mode: "resistance", title: "Resistance lowered", subtitle: girl.name + " / " + finalActorName, image: setup.AV.resistanceImageForGirl(girl), imageLabel: "Resistance", intro: girl.name + " wakes under foreign walls, under a ruler who does not speak like home.", body: "\"" + finalActorName + " rules this city,\" the guards remind her. The place itself begins teaching her where she is.", finish: bond.resistance >= 3 ? girl.name + " has learned enough of the foreign city to stop fighting every shadow." : girl.name + " still resists, but the foreign city has started becoming familiar.", stats: ["Target: " + girl.name, "Actor: " + finalActorName, "Resistance: " + bond.resistance + " / 3"] }; } if (["vaginal", "anal", "oral"].indexOf(action) < 0) return { mode: "failed", title: "Action failed", subtitle: girl.name, image: setup.AV.faceImage(girl), body: "The order breaks apart before it can become a private scene.", stats: ["Unknown action: " + action] }; const stats = []; const outcomes = []; bond.actions = Number(bond.actions || 0) + 1; stats.push("Action: " + action); stats.push("Actor: " + finalActorName); stats.push("Resistance: " + bond.resistance + " / 3"); stats.push("Bond weight: " + bond.actions + " / 10"); if (action === "vaginal") { girl.vaginal = Number(girl.vaginal || 0) + 1; stats.push("Vaginal +1"); if (girl.virgin) { girl.virgin = false; girl.firstTime = finalActorName; stats.push("First time: " + finalActorName); outcomes.push("Her first time is now tied to " + finalActorName + "."); } if (!girl.pregnant && setup.AV.chance(50)) { girl.pregnant = true; girl.pregnantBy = finalActorName; stats.push("Pregnant: Yes"); stats.push("Pregnant by: " + finalActorName); outcomes.push("Pregnancy now ties part of her future to " + finalActorName + "."); } } if (action === "anal") { girl.anal = Number(girl.anal || 0) + 1; stats.push("Anal +1"); } if (action === "oral") { girl.oral = Number(girl.oral || 0) + 1; stats.push("Oral +1"); } if (setup.AV.chance(22)) { girl.favoritePerson = finalActorName; stats.push("Favorite: " + girl.favoritePerson); } if (!bond.conquered && bond.actions >= 10) { bond.conquered = true; girl.conqueredBy = finalActorName; girl.conqueredByKey = actorKey; girl.conqueredSide = side; const conqueredFace = setup.AV.conqueredFaceImage(girl, side); girl.face = conqueredFace; girl.faceImage = conqueredFace; girl.image = conqueredFace; stats.push("Conquered: Yes"); stats.push("Public face changed"); outcomes.push(girl.name + " becomes part of " + finalActorName + "'s legend."); } if (side === "self") { return { mode: "action", title: bond.conquered ? "Conquered bond" : "Private bond", subtitle: "You / " + girl.name, image: setup.AV.actionImageForGirl(girl, action, side, "action"), image2: setup.AV.actionImageForGirl(girl, action, side, "finish"), imageLabel: "Scene", image2Label: "Finish", intro: "Cinder Hold is quiet around " + girl.name + ". You keep the room warm, close, and protected from the ash outside.", body: "\"" + girl.name + ", stay with me,\" you say. Not as a battlefield order, but as a promise that the walls will hold tonight.", finish: outcomes.length > 0 ? outcomes.join(" ") : "The bond with " + girl.name + " grows heavier and harder to ignore.", stats: stats }; } return { mode: "action", title: bond.conquered ? "Foreign claim" : "Foreign harem scene", subtitle: finalActorName + " / " + girl.name, image: setup.AV.actionImageForGirl(girl, action, side, "action"), image2: setup.AV.actionImageForGirl(girl, action, side, "finish"), imageLabel: "Scene", image2Label: "Finish", intro: girl.name + " is inside a foreign city now. The doors, guards, language, and laws all belong to " + finalActorName + ".", body: "\"" + girl.name + ", this is where your life is now,\" " + finalActorName + " says. His city answers before she can.", finish: outcomes.length > 0 ? outcomes.join(" ") : girl.name + " returns to the foreign corridors carrying another piece of that city with her.", stats: stats }; }; setup.AV.applyPassageRiteStats = function (girl, actorName, actorKey, side) { const stats = []; const outcomes = []; if (!girl) return { stats: ["No girl"], outcomes: [] }; const finalSide = side || "enemy"; const finalActorName = actorName || "Unknown"; const finalActorKey = actorKey || setup.AV.actorKeyForName(finalActorName, finalSide); setup.AV.normalizeGirl(girl, girl.place || "Captured city"); setup.AV.setCurrentCaptor(girl, finalActorName, finalActorKey, finalSide); const bond = setup.AV.getBondData(girl, finalActorKey); if (Number(bond.resistance || 0) < 3) { bond.resistance = 3; stats.push("Resistance forced open: 3 / 3"); } else stats.push("Resistance: " + bond.resistance + " / 3"); bond.actions = Number(bond.actions || 0) + 1; girl.vaginal = Number(girl.vaginal || 0) + 1; stats.push("Passage rite: completed"); stats.push("Vaginal +1"); stats.push("Bond weight +1"); stats.push("Bond weight: " + bond.actions + " / 10"); if (girl.virgin) { girl.virgin = false; girl.firstTime = finalActorName; stats.push("First time changed"); stats.push("First time: " + finalActorName); outcomes.push(girl.name + "'s first time is now tied to " + finalActorName + "."); } if (!girl.pregnant && setup.AV.chance(50)) { girl.pregnant = true; girl.pregnantBy = finalActorName; stats.push("Pregnant: Yes"); stats.push("Pregnant by: " + finalActorName); outcomes.push("Pregnancy now ties part of her future to " + finalActorName + "."); } else stats.push("Pregnant: " + (girl.pregnant ? "Already" : "No")); if (setup.AV.chance(22)) { girl.favoritePerson = finalActorName; stats.push("Favorite: " + finalActorName); outcomes.push("Something in her private loyalty shifts toward " + finalActorName + "."); } if (!bond.conquered && bond.actions >= 10) { bond.conquered = true; girl.conqueredBy = finalActorName; girl.conqueredByKey = finalActorKey; girl.conqueredSide = finalSide; const conqueredFace = setup.AV.conqueredFaceImage(girl, finalSide); girl.face = conqueredFace; girl.faceImage = conqueredFace; girl.image = conqueredFace; stats.push("Conquered: Yes"); stats.push("Public face changed"); outcomes.push(girl.name + " becomes part of " + finalActorName + "'s public legend."); } else stats.push("Conquered: " + (bond.conquered ? "Already" : "No")); return { stats: stats, outcomes: outcomes }; }; setup.AV.showBlockedExtraction = function (attacker, target, logs, events) { if (!attacker || !target) return; setup.AV.ensureNightSystems(); const key = attacker.id + "__" + target.id; if (State.variables.protectionBlocksShown[key]) return; State.variables.protectionBlocksShown[key] = true; const attackerName = attacker.name || attacker.ruler || "Unknown"; const targetName = target.name || target.ruler || "Unknown"; if (logs) logs.push("Protection held: " + attackerName + " tried to extract a girl from " + targetName + ", but the night guard stopped it."); setup.AV.pushRecapEvent(events, { type: "protection", title: "Protection held", subtitle: targetName + " resisted an extraction", image: setup.AV.nightMapImage(), imageLabel: "Night guard", intro: targetName + " did not sleep unguarded.", body: attackerName + " sent a quiet team toward the harem, but the protected corridors were already watched.", finish: "No one is taken from " + targetName + " tonight.", stats: ["Attacker: " + attackerName, "Protected city: " + targetName, "Result: extraction blocked"] }); }; setup.AV.applyCaptureRite = function (winner, loser, capturedGirls, logs, events) { if (!winner || !loser || !capturedGirls || capturedGirls.length < 1) return; setup.AV.registerGirlsForFailsafe(capturedGirls, "before capture rite from " + (loser.name || "fallen city")); const side = setup.AV.actorSideForRegion(winner); const actorName = setup.AV.actorNameForRegion(winner); const actorKey = setup.AV.actorKeyForRegion(winner); for (const girl of capturedGirls) { if (!girl) continue; setup.AV.normalizeGirl(girl, loser.name || "Captured city"); if (side === "self") { setup.AV.giveGirlToPlayer(girl); } else { setup.AV.giveGirlToRegion(girl, winner); } setup.AV.setCurrentCaptor(girl, actorName, actorKey, side); setup.AV.markGirlTakenThisRecap(girl); setup.AV.recordPossession(winner, girl, "Capture rite under " + actorName); setup.AV.registerGirlForFailsafe(girl, "capture rite under " + actorName); const rite = setup.AV.applyPassageRiteStats(girl, actorName, actorKey, side); const finishExtra = rite.outcomes.length > 0 ? " " + rite.outcomes.join(" ") : ""; if (logs) { logs.push(girl.name + " passed the passage rite under " + actorName + "."); } setup.AV.pushRecapEvent(events, { type: "captureRite", title: "Passage rite", subtitle: girl.name + " / " + actorName, image: setup.AV.riteImageForGirl(girl, side), imageLabel: "Rite", intro: loser.name + " has fallen, and " + girl.name + " is no longer protected by the old ruler.", body: "The rite is done in the shadow of the new banner. The old city name loses its grip, and " + actorName + " becomes the power standing over her future.", finish: girl.name + " now belongs to the new order of " + (winner.name || actorName) + "." + finishExtra, stats: [ "Girl: " + girl.name, "Captor: " + actorName, "Side: " + side, "Preserved in game: yes" ].concat(rite.stats) }); } }; setup.AV.captureRegion = function (winner, loser, logs, events) { if (!winner || !loser) return; const wasPlayerHome = setup.AV.isPlayerRegion(loser); const winnerIsPlayer = setup.AV.isPlayerRegion(winner); const loserOldPower = Number(loser.power || 0); const winnerName = winner.name || winner.ruler || "Unknown"; const loserName = loser.name || "Unknown"; const realWarFront = setup.AV.hasWarFront(winner, loser); if (wasPlayerHome) { if (!realWarFront) { if (logs) { logs.push("Blocked invalid collapse: " + winnerName + " tried to take Cinder without an active war front."); } return; } if (loserOldPower < 10) { if (logs) { logs.push("Cinder is under collapse pressure from " + winnerName + ", but final game over will only be checked after night recovery."); } setup.AV.pushRecapEvent(events, { type: "cinderPressure", title: "Cinder under pressure", subtitle: winnerName + " is at the border", image: setup.AV.warImage(), imageLabel: "War", intro: winnerName + " has pushed Cinder Hold close to collapse.", body: "The Hold does not fall from fear alone. It falls only if weakness remains visible after the night and an enemy front is still touching the territory.", finish: "Cinder survives for now, but the border has learned the smell of weakness.", stats: [ "Cinder power before recovery: " + loserOldPower, "War front: active", "Immediate capture blocked", "Final check pending" ] }); } return; } const oldLoserHarem = loser.harem || []; const capturedGirls = []; for (const girl of oldLoserHarem) { if (!girl) continue; setup.AV.registerGirlForFailsafe(girl, "inside " + loserName + " before city capture"); capturedGirls.push(girl); } if (capturedGirls.length > 0) { setup.AV.markGrudge(winner, loser, winnerName + " captured a city with girls inside", capturedGirls[0]); } loser.harem = []; loser.owned = winnerIsPlayer; loser.ruler = winner.ruler; loser.color = winner.color || (loser.owned ? "rgba(223,255,93,.35)" : "rgba(255,91,32,.35)"); loser.power = Math.max(10, Math.floor(Number(winner.power || 20) * 0.45)); loser.influence = Math.max(6, Math.floor(Number(loser.influence || 8) * 0.6)); if (logs) { logs.push(winnerName + " captured " + loserName + "."); } setup.AV.pushRecapEvent(events, { type: "capture", title: "City captured", subtitle: winnerName + " took " + loserName, image: setup.AV.warImage(), imageLabel: "War", intro: loserName + " could not hold the line anymore.", body: winnerName + " now controls the city. Its old ruler loses authority, its banners lose meaning, and every captive inside hears the new order before dawn.", finish: loserName + " now answers to " + (winner.ruler || winnerName) + ".", stats: [ "Captured: " + loserName, "New ruler: " + (winner.ruler || winnerName), "Girls in passage rite: " + capturedGirls.length, "Girls preserved in game: " + capturedGirls.length ] }); setup.AV.applyCaptureRite(winner, loser, capturedGirls, logs, events); const repaired = setup.AV.repairMissingGirlsToScavengePool("city capture failsafe after " + loserName + " fell", logs); if (repaired > 0) { setup.AV.pushRecapEvent(events, { type: "failsafe", title: "Missing survivor recovered", subtitle: "Scavenge pool updated", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "The city changed hands and the failsafe checked every known girl.", body: "Someone had no active city, harem, or scavenge entry after the collapse. She was not deleted from the game.", finish: "She can now be found later through scavenge.", stats: [ "Recovered to scavenge pool: " + repaired, "Cause: city capture cleanup", "Result: no depop" ] }); } }; setup.AV.resolvePlayerProtectActions = function (logs, events) { const actions = State.variables.pendingPlayerActions || []; const home = setup.AV.home(); if (!home) return; for (const action of actions) { if (!action || action.type !== "protectHome") continue; setup.AV.protectRegionTonight(home); logs.push("Cinder protected the harem tonight."); setup.AV.pushRecapEvent(events, { type: "protection", title: "Cinder protected", subtitle: "Night guard active", image: setup.AV.nightMapImage(), imageLabel: "Protection", intro: "Cinder spends power and influence before nightfall.", body: "The doors are watched, the corridors are locked, and the harem is not left exposed to enemy extraction teams.", finish: "No enemy can take a girl from Cinder tonight unless the city itself falls.", stats: ["Power cost: -" + setup.AV.PROTECT_POWER_COST, "Influence cost: -" + setup.AV.PROTECT_INFLUENCE_COST, "Protected region: Cinder Hold"] }); } }; setup.AV.resolveEnemyProtectionPlans = function (logs, events) { setup.AV.ensureNightSystems(); const regions = State.variables.regions || []; const enemies = regions.filter(function (r) { return r && !setup.AV.isPlayerRegion(r); }); for (const enemy of enemies) { if (!enemy) continue; if (setup.AV.getEligibleHaremGirls(enemy).length < 1) continue; if (!setup.AV.canPayProtection(enemy)) continue; const hasPlayerFront = setup.AV.hasWarFront(enemy, setup.AV.home()); const valuableHarem = setup.AV.getEligibleHaremGirls(enemy).length >= 2; const chance = hasPlayerFront ? setup.AV.ENEMY_PROTECT_FRONT_CHANCE : (valuableHarem ? setup.AV.ENEMY_PROTECT_BIG_HAREM_CHANCE : setup.AV.ENEMY_PROTECT_SMALL_HAREM_CHANCE); if (!setup.AV.chance(chance)) continue; setup.AV.payProtectionCost(enemy); setup.AV.protectRegionTonight(enemy); State.variables.enemySpentProtection[enemy.id] = true; logs.push(enemy.name + " protected its harem tonight."); } }; setup.AV.resolveWorkPower = function (action, logs, events) { const home = setup.AV.home(); if (!home) return; const before = Number(home.power || 0); const gain = Number(action.powerGain || setup.AV.WORK_POWER_GAIN || 5); home.power = before + gain; logs.push("Cinder worked on power for the full day: +" + gain + " power."); setup.AV.pushRecapEvent(events, { type: "workPower", title: "Power work", subtitle: "Cinder Hold", image: setup.AV.nightMapImage(), imageLabel: "Full day", intro: "Cinder shuts down every other operation and spends the full day reinforcing the Hold.", body: "No raid, no harem order, no influence work. The day goes into walls, patrol gear, weapons, engines, locked gates, and the tired hands needed to keep them working.", finish: "Cinder gains power now, then still receives the normal night recovery after the day ends.", stats: ["Full day action: yes", "Action points spent: 5 / 5", "Cinder power: " + before + " -> " + home.power, "Immediate gain: +" + gain, "Night recovery still applies: +5", "No fatigue penalty for this action"] }); }; setup.AV.resolveSelfHaremAction = function (action, logs, events) { const girl = (State.variables.women || [])[action.girlIndex]; if (!girl) { logs.push("Harem action failed: target not found."); return; } const result = setup.AV.applyHaremAction(girl, "You", action.actionType, { actorKey: "self", side: "self" }); logs.push("Cinder harem step logged for " + girl.name + " [" + action.actionType + "]."); setup.AV.pushRecapEvent(events, { type: result.mode || "selfHarem", title: result.title || "Cinder Hold", subtitle: result.subtitle || ("You with " + girl.name), image: result.image || setup.AV.faceImage(girl), image2: result.image2 || "", imageLabel: result.imageLabel || "Image", image2Label: result.image2Label || "Second image", intro: result.intro || "", body: result.body || "", finish: result.finish || "", stats: result.stats || [] }); }; setup.AV.resolveWorkInfluence = function (action, logs, events) { const home = setup.AV.home(); if (!home) return; const before = Number(home.influence || 0); const totalGain = 2 + (setup.AV.chance(35) ? 1 : 0); home.influence = Math.min(150, before + totalGain); logs.push("Cinder worked on influence: +" + totalGain + " influence."); setup.AV.pushRecapEvent(events, { type: "workInfluence", title: "Influence work", subtitle: "Cinder Hold", image: setup.AV.nightMapImage(), imageLabel: "City", intro: "Cinder does not grow only through raids.", body: "Messengers are sent through the safe alleys. Protection promises are repeated. Small disputes are settled publicly. The goal is credibility.", finish: "By nightfall, Cinder's name carries farther.", stats: ["Cinder influence: " + before + " -> " + home.influence, "Total gain: +" + totalGain] }); }; setup.AV.resolveScavenge = function (action, logs, events) { const home = setup.AV.home(); setup.AV.repairMissingGirlsToScavengePool("before player scavenge", logs); const roll = setup.AV.roll(1, 100); if (!home) return; if (roll <= 22) { logs.push("Scavenge failed: nothing useful was found."); setup.AV.pushRecapEvent(events, { type: "scavenge", title: "Scavenge failed", subtitle: "Outside the walls", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "The crew left before sunrise.", body: "The ruins were already stripped clean.", finish: "Cinder gains nothing from the road today.", stats: ["No power gained", "No influence gained"] }); return; } if (roll <= 55) { const powerGain = setup.AV.roll(2, 5); home.power = Number(home.power || 0) + powerGain; logs.push("Scavenge returned with usable systems: Cinder power +" + powerGain + "."); setup.AV.pushRecapEvent(events, { type: "scavenge", title: "Scavenge success", subtitle: "Ruined supply road", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "The crew found a route that had not been fully picked apart.", body: "They returned with batteries, wire, old cells, weapon parts, and enough usable fragments to make the Hold stronger.", finish: "Cinder converts the haul into direct power.", stats: ["Cinder power +" + powerGain] }); return; } if (roll <= 72) { const powerGain = setup.AV.roll(3, 6); const influenceGain = setup.AV.chance(35) ? 1 : 0; home.power = Number(home.power || 0) + powerGain; home.influence = Math.min(150, Number(home.influence || 5) + influenceGain); logs.push("Scavenge found a strong haul: Cinder power +" + powerGain + ", influence +" + influenceGain + "."); setup.AV.pushRecapEvent(events, { type: "scavenge", title: "Strong haul", subtitle: "Abandoned checkpoint", image: setup.AV.nightMapImage(), imageLabel: "Checkpoint", intro: "The checkpoint looked dead from the road, but its buried systems still had value.", body: "Cinder's crew returned with working cells, old ammunition, and visible success.", finish: "The Hold gains power. The story may also push Cinder's name farther.", stats: ["Cinder power +" + powerGain, "Cinder influence +" + influenceGain] }); return; } if (roll <= 86) { const freePool = State.variables.freeWomenPool || []; if (freePool.length > 0) { const index = setup.AV.roll(0, freePool.length - 1); const foundGirl = freePool.splice(index, 1)[0]; const influenceGain = 1; setup.AV.normalizeGirl(foundGirl, "Cinder Hold"); setup.AV.giveGirlToPlayer(foundGirl); setup.AV.markGirlTakenThisRecap(foundGirl); home.influence = Math.min(150, Number(home.influence || 5) + influenceGain); logs.push("Scavenge encounter: " + foundGirl.name + " was found and brought to Cinder Hold."); setup.AV.pushRecapEvent(events, { type: "scavengeGirl", title: "Road encounter", subtitle: "A survivor was found", image: setup.AV.faceImage(foundGirl), imageLabel: "Survivor", intro: "The crew found signs of life where the road bent under collapsed concrete.", body: foundGirl.name + " was not yet written into any city harem.", finish: foundGirl.name + " was brought back to Cinder Hold.", stats: ["Gained: " + foundGirl.name, "Cinder influence +" + influenceGain, "Free pool remaining: " + freePool.length, "Protected from chain movement this recap: yes"] }); return; } const powerGain = setup.AV.roll(1, 3); home.power = Number(home.power || 0) + powerGain; logs.push("Scavenge found no survivor, but recovered power +" + powerGain + "."); setup.AV.pushRecapEvent(events, { type: "scavengeGirlEmpty", title: "Empty trail", subtitle: "No survivor found", image: setup.AV.nightMapImage(), imageLabel: "Road", intro: "The crew followed signs of movement through the ruins.", body: "There were blankets, ashes, and old tracks, but nobody remained.", finish: "The trail ended cold, though the crew still recovered enough to strengthen the Hold.", stats: ["No girl gained", "Cinder power +" + powerGain] }); return; } const eligibleWomen = setup.AV.getEligibleHaremGirls(home); if (eligibleWomen.length > 0 && !setup.AV.isRegionProtected(home)) { const enemies = (State.variables.regions || []).filter(function (r) { return r && !setup.AV.isPlayerRegion(r) && setup.AV.hasExtractionRoute(r, home); }); const thiefRegion = setup.AV.pick(enemies); const stolen = setup.AV.pick(eligibleWomen); const influenceLoss = 1; if (!thiefRegion) { const powerLoss = setup.AV.roll(1, 3); home.power = Math.max(1, Number(home.power || 1) - powerLoss); logs.push("Scavenge ambush: no enemy influence front touched Cinder. Cinder lost " + powerLoss + " power."); setup.AV.pushRecapEvent(events, { type: "ambush", title: "Scavenge ambush", subtitle: "No extraction route", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "The crew was followed, but the raiders had no stable route back to a hostile city.", body: "Without touching influence, a real harem abduction cannot hold. The ambush becomes damage, not extraction.", finish: "No girl is taken, but Cinder loses power.", stats: ["No girl lost", "Cinder power -" + powerLoss, "Reason: no enemy influence contact"] }); return; } const removed = setup.AV.removeGirlFromRegion(home, stolen); if (!removed) { const powerLoss = setup.AV.roll(1, 3); home.power = Math.max(1, Number(home.power || 1) - powerLoss); logs.push("Scavenge ambush: raiders tried to take someone from Cinder, but the harem raid limit was already used tonight."); setup.AV.pushRecapEvent(events, { type: "ambush", title: "Scavenge ambush", subtitle: "No captive lost", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "Raiders followed the crew back toward Cinder.", body: "Cinder had already suffered or blocked one harem raid tonight. The second attempt cannot chain into another loss.", finish: "No girl is taken, but Cinder still loses power.", stats: ["No girl lost", "Cinder power -" + powerLoss, "Cinder harem raid limit: already used"] }); return; } home.influence = Math.max(1, Number(home.influence || 5) - influenceLoss); setup.AV.giveGirlToRegion(removed, thiefRegion); setup.AV.markGrudge(thiefRegion, home, thiefRegion.name + " took a girl during a scavenge ambush", removed); thiefRegion.influence = Math.min(150, Number(thiefRegion.influence || 5) + 1); logs.push("Scavenge ambush: " + removed.name + " was taken during the route."); setup.AV.pushRecapEvent(events, { type: "ambush", title: "Scavenge ambush", subtitle: thiefRegion.name, image: setup.AV.takenImageForGirl(removed), imageLabel: "Taken", intro: "Someone had been watching the route.", body: "Because enemy influence touched Cinder's edge, the abduction route could hold.", finish: removed.name + " is now in " + thiefRegion.name + ". Cinder loses face.", stats: ["Lost: " + removed.name, "Taken by: " + thiefRegion.name, "Cinder influence -" + influenceLoss, "Cinder harem raid limit: used tonight", thiefRegion.name + " influence +1"] }); } else { const powerLoss = setup.AV.roll(1, 3); home.power = Math.max(1, Number(home.power || 1) - powerLoss); logs.push("Scavenge ambush: no eligible girl could be taken, but Cinder lost " + powerLoss + " power."); setup.AV.pushRecapEvent(events, { type: "ambush", title: "Scavenge ambush", subtitle: "No captive lost", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "Raiders tried to follow the crew back through the ruins.", body: "They found nobody they could take. Protection, conquest, previous movement, or the harem raid limit closed every useful route.", finish: "No girl was lost, but the Hold is weaker.", stats: ["No girl lost", "Cinder power -" + powerLoss] }); } }; setup.AV.resolveIncursion = function (action, logs, events) { const home = setup.AV.home(); const target = setup.AV.region(action.targetId); if (!home || !target) return; if (!setup.AV.canStartIncursionThisRecap()) { logs.push("Incursion cancelled: one incursion has already happened tonight."); return; } setup.AV.markIncursionUsedThisRecap(); const chance = 45 + ((Number(home.power || 0) - Number(target.power || 0)) * 1.25) + ((Number(home.influence || 0) - Number(target.influence || 0)) * 2); if (setup.AV.chance(chance)) { const powerDamage = setup.AV.roll(4, 8); const influenceDamage = 1; const influenceGain = 2; target.power = Math.max(1, Number(target.power || 1) - powerDamage); target.influence = Math.max(1, Number(target.influence || 5) - influenceDamage); home.influence = Math.min(150, Number(home.influence || 5) + influenceGain); logs.push("Incursion success against " + target.name + "."); setup.AV.pushRecapEvent(events, { type: "incursion", title: "Incursion success", subtitle: target.name, image: setup.AV.warImage(), imageLabel: "Incursion", intro: "Cinder does not need its influence to touch " + target.name + " for a raid.", body: "An incursion is shallow, violent, and temporary. It damages a target without opening a true extraction route.", finish: "Cinder's reputation grows because the operation was visible and successful.", stats: [target.name + " power -" + powerDamage, target.name + " influence -" + influenceDamage, "Cinder influence +" + influenceGain, "Influence contact needed: no", "Incursion limit: used tonight"] }); if (target.power < 10 && setup.AV.hasWarFront(home, target)) setup.AV.captureRegion(home, target, logs, events); } else { const loss = setup.AV.roll(2, 5); home.power = Math.max(1, Number(home.power || 1) - loss); logs.push("Incursion failed against " + target.name + "."); setup.AV.pushRecapEvent(events, { type: "incursion", title: "Incursion failed", subtitle: target.name, image: setup.AV.warImage(), imageLabel: "Failed strike", intro: "The plan looked clean until the first signal failed.", body: target.name + " reacted faster than expected.", finish: "Cinder loses power and gains no glory from the border.", stats: ["Cinder power -" + loss, "Influence contact needed: no", "Incursion limit: used tonight"] }); } }; setup.AV.resolveStealGirl = function (action, logs, events) { const home = setup.AV.home(); const target = setup.AV.region(action.targetId); if (!home || !target) return; const harem = setup.AV.getHarem(target); const girl = harem[action.girlIndex] || harem[0]; if (!girl) { logs.push("Extraction failed at " + target.name + ": no target found."); return; } if (!setup.AV.hasExtractionRoute(home, target)) { logs.push("Extraction impossible at " + target.name + ": the extraction route did not exist before tonight's moves."); setup.AV.pushRecapEvent(events, { type: "failed", title: "Extraction impossible", subtitle: target.name, image: setup.AV.nightMapImage(), imageLabel: "No route", intro: "Cinder knows where " + girl.name + " is, but knowing is not reaching.", body: "A harem extraction needs a stable route through touching influence.", finish: "Cinder must expand influence or create a war front before stealing from " + target.name + ".", stats: ["Target: " + girl.name, "Reason: no pre-existing extraction route", "Result: no extraction"] }); return; } if (setup.AV.isRegionProtected(target)) { logs.push("Extraction blocked at " + target.name + ": the city protected its harem tonight."); setup.AV.pushRecapEvent(events, { type: "protection", title: "Extraction blocked", subtitle: target.name, image: setup.AV.nightMapImage(), imageLabel: "Protected", intro: "Cinder's route into " + target.name + " was clean, but the harem was not exposed tonight.", body: target.name + " spent power and influence before dark.", finish: "No one is taken from " + target.name + ".", stats: ["Target: " + girl.name, "Protected city: " + target.name, "Result: blocked by protection"] }); return; } if (setup.AV.isConqueredGirl(girl)) { logs.push("Extraction cancelled: " + girl.name + " is conquered and cannot be taken."); setup.AV.pushRecapEvent(events, { type: "failed", title: "Extraction impossible", subtitle: girl.name, image: setup.AV.faceImage(girl), imageLabel: "Locked", intro: girl.name + " is too deeply bound to her current ruler.", body: "A conquered girl cannot be extracted by normal routes.", finish: "Cinder cannot take her tonight.", stats: ["Target: " + girl.name, "Reason: conquered", "Result: no extraction"] }); return; } setup.AV.markGrudge(home, target, "Cinder extracted a girl", girl); const stolen = setup.AV.removeGirlFromRegion(target, girl); if (!stolen) return; setup.AV.giveGirlToPlayer(stolen); target.power = Math.max(1, Number(target.power || 1) - setup.AV.roll(2, 5)); home.influence = Math.min(150, Number(home.influence || 5) + 1); logs.push("Extraction success: " + stolen.name + " was taken from " + target.name + "."); setup.AV.pushRecapEvent(events, { type: "extraction", title: "Extraction success", subtitle: target.name, image: setup.AV.takenImageForGirl(stolen), imageLabel: "Taken", intro: "Cinder sends a small crew into " + target.name + " for one purpose: bring " + stolen.name + " back alive.", body: "If the influence fronts touch and the harem is exposed, the route closes around her and Cinder succeeds.", finish: stolen.name + " is now in Cinder Hold.", stats: ["Gained: " + stolen.name, "Cinder influence +1", target.name + " power damaged", "Protected from chain movement this recap: yes"] }); }; setup.AV.resolveWarFight = function (action, logs, events) { const home = setup.AV.home(); const a = setup.AV.region(action.aId); const b = setup.AV.region(action.bId); if (!home || !a || !b) return; if (!setup.AV.hasWarFront(a, b)) { logs.push("War action cancelled: no active war front exists between " + a.name + " and " + b.name + "."); return; } let enemy = null; if (a.id === home.id) enemy = b; if (b.id === home.id) enemy = a; if (!enemy) { if (a.owned) enemy = b; if (b.owned) enemy = a; } if (!enemy) return; const homeScore = Number(home.power || 0) + Number(home.influence || 0) + setup.AV.roll(1, 20); const enemyScore = Number(enemy.power || 0) + Number(enemy.influence || 0) + setup.AV.roll(1, 20); if (homeScore >= enemyScore) { const pDmg = setup.AV.roll(5, 10); const iDmg = 1; const influenceGain = setup.AV.chance(55) ? 1 : 0; enemy.power = Math.max(1, Number(enemy.power || 1) - pDmg); enemy.influence = Math.max(5, Number(enemy.influence || 5) - iDmg); home.influence = Math.min(150, Number(home.influence || 5) + influenceGain); let girl = null; if (!setup.AV.isRegionProtected(enemy)) girl = setup.AV.takeGirlFromRegion(enemy); if (girl) { setup.AV.giveGirlToPlayer(girl); setup.AV.markGrudge(home, enemy, "Cinder took a girl after a war victory", girl); logs.push("War victory: " + enemy.name + " lost " + pDmg + " power and " + girl.name + " was captured."); setup.AV.pushRecapEvent(events, { type: "war", title: "War victory", subtitle: enemy.name, image: setup.AV.takenImageForGirl(girl), imageLabel: "Taken", intro: "Cinder wins the front against " + enemy.name + ".", body: girl.name + " is taken during the collapse.", finish: girl.name + " is moved to Cinder Hold.", stats: [enemy.name + " power -" + pDmg, enemy.name + " influence -" + iDmg, "Cinder influence +" + influenceGain, "Gained: " + girl.name] }); } else { logs.push("War victory: " + enemy.name + " lost " + pDmg + " power."); setup.AV.pushRecapEvent(events, { type: "war", title: "War victory", subtitle: enemy.name, image: setup.AV.warImage(), imageLabel: "War", intro: "Cinder wins the front against " + enemy.name + ".", body: "The enemy loses ground, weapons, and confidence. No eligible girl is exposed tonight.", finish: enemy.name + " is weaker after the battle.", stats: [enemy.name + " power -" + pDmg, enemy.name + " influence -" + iDmg, "Cinder influence +" + influenceGain] }); } if (enemy.power < 10 && setup.AV.hasWarFront(home, enemy)) setup.AV.captureRegion(home, enemy, logs, events); } else { const loss = setup.AV.roll(5, 10); const influenceLoss = 1; home.power = Math.max(1, Number(home.power || 1) - loss); home.influence = Math.max(5, Number(home.influence || 5) - influenceLoss); let girl = null; if (!setup.AV.isRegionProtected(home)) girl = setup.AV.takeGirlFromRegion(home); if (girl) { setup.AV.giveGirlToRegion(girl, enemy); setup.AV.markGrudge(enemy, home, enemy.name + " took a girl after a war defeat", girl); logs.push("War defeat: Cinder lost " + loss + " power and " + girl.name + " was taken by " + enemy.name + "."); setup.AV.pushRecapEvent(events, { type: "war", title: "War defeat", subtitle: enemy.name, image: setup.AV.takenImageForGirl(girl), imageLabel: "Taken", intro: "Cinder loses the front.", body: enemy.name + " takes " + girl.name + " during the collapse.", finish: girl.name + " is now in " + enemy.name + ".", stats: ["Cinder power -" + loss, "Cinder influence -" + influenceLoss, "Lost: " + girl.name, "Taken by: " + enemy.name, "Cinder harem raid limit: used tonight"] }); } else { logs.push("War defeat: Cinder lost " + loss + " power."); setup.AV.pushRecapEvent(events, { type: "war", title: "War defeat", subtitle: enemy.name, image: setup.AV.warImage(), imageLabel: "War", intro: "Cinder loses the front against " + enemy.name + ".", body: "The Hold pays in power and reputation. No eligible girl is exposed tonight.", finish: "Cinder survives the defeat, but weaker.", stats: ["Cinder power -" + loss, "Cinder influence -" + influenceLoss] }); } } }; setup.AV.resolveSupportWar = function (action, logs, events) { const home = setup.AV.home(); const a = setup.AV.region(action.aId); const b = setup.AV.region(action.bId); const side = setup.AV.region(action.sideId); if (!home || !a || !b || !side) return; if (!setup.AV.hasWarFront(a, b)) { logs.push("Support war cancelled: no active war front exists between " + a.name + " and " + b.name + "."); return; } const other = side.id === a.id ? b : a; const sideScore = Number(side.power || 0) + Number(side.influence || 0) + setup.AV.roll(1, 20) + 8; const otherScore = Number(other.power || 0) + Number(other.influence || 0) + setup.AV.roll(1, 20); if (sideScore >= otherScore) { const dmg = setup.AV.roll(4, 9); const influenceDamage = 1; other.power = Math.max(1, Number(other.power || 1) - dmg); other.influence = Math.max(5, Number(other.influence || 5) - influenceDamage); let girl = null; if (!setup.AV.isRegionProtected(other)) girl = setup.AV.takeGirlFromRegion(other); if (girl && setup.AV.chance(70)) { setup.AV.giveGirlToPlayer(girl); setup.AV.markGrudge(home, other, "Cinder received a girl from a supported war", girl); setup.AV.markGrudge(side, other, side.name + " took a girl during a supported war", girl); logs.push("War support paid off: " + side.name + " won and delivered " + girl.name + " as a reward."); setup.AV.pushRecapEvent(events, { type: "warSupport", title: "Support paid off", subtitle: side.name, image: setup.AV.takenImageForGirl(girl), imageLabel: "Reward", intro: "Cinder backs the right side of the war.", body: side.name + " wins the front and sends proof that the support mattered: " + girl.name + ".", finish: girl.name + " is delivered to Cinder Hold.", stats: ["Gained: " + girl.name, other.name + " power -" + dmg, other.name + " influence -" + influenceDamage] }); } else { const powerGain = setup.AV.roll(3, 7); const influenceGain = setup.AV.chance(45) ? 1 : 0; home.power = Number(home.power || 0) + powerGain; home.influence = Math.min(150, Number(home.influence || 5) + influenceGain); logs.push("War support paid off: " + side.name + " won and paid Cinder with power +" + powerGain + " and influence +" + influenceGain + "."); setup.AV.pushRecapEvent(events, { type: "warSupport", title: "Support paid off", subtitle: side.name, image: setup.AV.warImage(), imageLabel: "War", intro: "Cinder's support helps " + side.name + " win the front.", body: "No girl is delivered this time, but the winning side pays in engines, armed hands, batteries, patrol rights, and public credit.", finish: "Cinder turns the favor into power, and sometimes into reputation.", stats: ["Cinder power +" + powerGain, "Cinder influence +" + influenceGain, other.name + " power -" + dmg, other.name + " influence -" + influenceDamage] }); } if (other.power < 10 && setup.AV.hasWarFront(side, other)) setup.AV.captureRegion(side, other, logs, events); } else { const loss = setup.AV.roll(2, 5); home.power = Math.max(1, Number(home.power || 1) - loss); logs.push("War support failed: " + side.name + " lost the front."); setup.AV.pushRecapEvent(events, { type: "warSupport", title: "Support failed", subtitle: side.name, image: setup.AV.warImage(), imageLabel: "War", intro: "Cinder chooses a side, but the chosen side fails.", body: side.name + " loses the front despite the support.", finish: "Cinder loses power and gains nothing.", stats: ["Cinder power -" + loss] }); } }; setup.AV.resolveTaunt = function (taunter, target, logs, events) { if (!taunter || !target) return false; const tauntGirl = setup.AV.getTauntGirlForPair(taunter, target); if (!tauntGirl.valid) { if (logs) logs.push((taunter.name || "Unknown") + " tried to taunt " + (target.name || "Unknown") + ", but the needed girl was not currently held."); return false; } const beforeTargetInfluence = Number(target.influence || 0); const beforeTaunterInfluence = Number(taunter.influence || 0); target.influence = Math.max(1, beforeTargetInfluence - 1); taunter.influence = Math.min(150, beforeTaunterInfluence + 1); const taunterName = setup.AV.isPlayerRegion(taunter) ? "Cinder" : (taunter.name || taunter.ruler || "Unknown"); const targetName = target.name || target.ruler || "Unknown"; logs.push(taunterName + " taunted " + targetName + " with " + tauntGirl.girlName + "'s image."); setup.AV.pushRecapEvent(events, { type: "taunt", title: "Taunt sent", subtitle: taunterName + " -> " + targetName, image: setup.AV.tauntImageForPair(taunter, target), imageLabel: "Taunt", intro: taunterName + " does not send a random insult.", body: "The taunt uses " + tauntGirl.girlName + ", because her name already cuts through the history between both powers.", finish: targetName + " loses face. " + taunterName + " looks stronger for turning private history into a public weapon.", stats: ["Sender: " + taunterName, "Target: " + targetName, "Girl used: " + tauntGirl.girlName, "Target influence: " + beforeTargetInfluence + " -> " + target.influence, "Sender influence: " + beforeTaunterInfluence + " -> " + taunter.influence] }); return true; }; setup.AV.queueTaunt = function (targetId) { const home = setup.AV.home(); const target = setup.AV.region(targetId); if (!home || !target || target.id === home.id) return; const tauntGirl = setup.AV.getTauntGirlForPair(home, target); if (!tauntGirl.valid) return; if (!setup.AV.consumeAction(1, 0)) return; State.variables.pendingPlayerActions.push({ type: "taunt", targetId: target.id, targetName: target.name, girlName: tauntGirl.girlName, girlFolder: tauntGirl.girlFolder, label: "Taunt " + target.name + " with " + tauntGirl.girlName, hours: 1 }); }; setup.AV.resolvePlayerTaunt = function (action, logs, events) { const home = setup.AV.home(); const target = setup.AV.region(action.targetId); if (!home || !target) return; setup.AV.resolveTaunt(home, target, logs, events); }; setup.AV.enemyHaremAction = function (region, logs, events) { if (!region || setup.AV.isPlayerRegion(region)) return; const harem = setup.AV.getHarem(region); if (!harem || harem.length < 1) return; const girl = setup.AV.pick(harem); if (!girl) return; const actorName = setup.AV.actorNameForRegion(region); const actorKey = setup.AV.actorKeyForRegion(region); const side = setup.AV.actorSideForRegion(region); const action = setup.AV.pick(["vaginal", "oral", "anal"]); const result = setup.AV.applyHaremAction(girl, actorName, action, { actorKey: actorKey, side: side }); logs.push(region.name + ": harem step logged for " + girl.name + " [" + action + "]."); setup.AV.pushRecapEvent(events, { type: result.mode || "harem", title: result.title || region.name, subtitle: result.subtitle || (actorName + " with " + girl.name), image: result.image || setup.AV.faceImage(girl), image2: result.image2 || "", imageLabel: result.imageLabel || "Image", image2Label: result.image2Label || "Second image", intro: result.intro || "", body: result.body || "", finish: result.finish || "", stats: result.stats || [] }); }; setup.AV.enemyScavenge = function (enemy, logs, events) { if (!enemy || setup.AV.isPlayerRegion(enemy)) return; setup.AV.repairMissingGirlsToScavengePool("before enemy scavenge", logs); const roll = setup.AV.roll(1, 100); if (roll <= 28) { logs.push(enemy.name + " scavenged but found nothing useful."); return; } if (roll <= 62) { const gain = setup.AV.roll(1, 4); enemy.power = Number(enemy.power || 0) + gain; logs.push(enemy.name + " scavenged systems and gained +" + gain + " power."); return; } if (roll <= 80) { const gain = setup.AV.roll(1, 3); const influenceGain = setup.AV.chance(35) ? 1 : 0; enemy.power = Number(enemy.power || 0) + gain; enemy.influence = Math.min(150, Number(enemy.influence || 5) + influenceGain); logs.push(enemy.name + " found patrol routes: +" + gain + " power, +" + influenceGain + " influence."); return; } const freePool = State.variables.freeWomenPool || []; if (freePool.length > 0) { const index = setup.AV.roll(0, freePool.length - 1); const girl = freePool.splice(index, 1)[0]; setup.AV.markGirlTakenThisRecap(girl); setup.AV.giveGirlToRegion(girl, enemy); logs.push(enemy.name + " found " + girl.name + " while scavenging."); setup.AV.pushRecapEvent(events, { type: "enemyScavengeGirl", title: enemy.name + " scavenging", subtitle: "A survivor was found", image: setup.AV.takenImageForGirl(girl), imageLabel: "Taken", intro: enemy.name + " sends scouts into the ruins.", body: "They return with " + girl.name + ". Another survivor becomes another reason for that city to stand taller.", finish: girl.name + " is now in " + enemy.name + ".", stats: ["Gained: " + girl.name, "New place: " + enemy.name, "Protected from chain movement this recap: yes"] }); return; } const gain = 1; enemy.power = Number(enemy.power || 0) + gain; logs.push(enemy.name + " scavenged old systems: +" + gain + " power."); }; setup.AV.enemyWorkInfluence = function (enemy, logs, events) { if (!enemy || setup.AV.isPlayerRegion(enemy)) return; const before = Number(enemy.influence || 0); const totalGain = 2 + (setup.AV.chance(40) ? 1 : 0); enemy.influence = Math.min(150, before + totalGain); logs.push(enemy.name + " worked on influence: +" + totalGain + " influence."); setup.AV.pushRecapEvent(events, { type: "enemyWorkInfluence", title: enemy.name + " influence work", subtitle: enemy.ruler || enemy.name, image: setup.AV.nightMapImage(), imageLabel: "City", intro: enemy.name + " does not only survive through raids.", body: "The ruler pays patrols, repairs public promises, settles disputes, and reminds the people that obedience is safer than doubt.", finish: enemy.name + " gains influence. Its borders and reputation become harder to ignore.", stats: [enemy.name + " influence: " + before + " -> " + enemy.influence, "Total gain: +" + totalGain] }); }; setup.AV.enemyCanWorkPower = function (enemy) { setup.AV.ensureNightSystems(); if (!enemy) return false; if (setup.AV.isPlayerRegion(enemy)) return false; if (State.variables.enemySpentProtection && State.variables.enemySpentProtection[enemy.id]) return false; if (State.variables.enemyFullDayActionUsed && State.variables.enemyFullDayActionUsed[enemy.id]) return false; return true; }; setup.AV.enemyWorkPower = function (enemy, logs, events) { if (!setup.AV.enemyCanWorkPower(enemy)) return false; const before = Number(enemy.power || 0); const gain = setup.AV.WORK_POWER_GAIN || 5; enemy.power = before + gain; State.variables.enemyFullDayActionUsed[enemy.id] = true; if (logs) logs.push(enemy.name + " worked on power for the full day: +" + gain + " power."); setup.AV.pushRecapEvent(events, { type: "enemyWorkPower", title: enemy.name + " power work", subtitle: enemy.ruler || enemy.name, image: setup.AV.nightMapImage(), imageLabel: "Full day", intro: enemy.name + " shuts down raiding and spends the day reinforcing itself.", body: "The ruler chooses walls, patrol gear, engines, locked gates, and armed hands over immediate aggression.", finish: enemy.name + " gains power now, then still receives the normal night recovery.", stats: [enemy.name + " power: " + before + " -> " + enemy.power, "Immediate gain: +" + gain, "Night recovery still applies: +5", "Enemy full day action: used"] }); return true; }; setup.AV.enemyIncursionAction = function (enemy, target, logs, events) { if (!enemy || !target) return false; if (!setup.AV.canStartIncursionThisRecap()) { logs.push(enemy.name + " considered an incursion, but one incursion has already happened tonight."); return false; } setup.AV.markIncursionUsedThisRecap(); const enemyName = enemy.name || enemy.ruler || "Enemy"; const targetName = target.name || target.ruler || "Target"; const isAgainstPlayer = setup.AV.isPlayerRegion(target); const frontBonus = setup.AV.hasWarFront(enemy, target) ? 10 : 0; const chance = 42 + ((Number(enemy.power || 0) - Number(target.power || 0)) * 1.1) + ((Number(enemy.influence || 0) - Number(target.influence || 0)) * 1.4) + frontBonus + (isAgainstPlayer ? 4 : 0); if (setup.AV.chance(chance)) { const powerDamage = setup.AV.roll(3, 7); const influenceDamage = setup.AV.chance(55) ? 1 : 0; const influenceGain = 1 + (setup.AV.chance(35) ? 1 : 0); target.power = Math.max(1, Number(target.power || 1) - powerDamage); target.influence = Math.max(1, Number(target.influence || 1) - influenceDamage); enemy.influence = Math.min(150, Number(enemy.influence || 5) + influenceGain); logs.push(enemyName + " made an incursion against " + targetName + "."); setup.AV.pushRecapEvent(events, { type: "enemyIncursion", title: enemyName + " incursion", subtitle: targetName, image: setup.AV.warImage(), imageLabel: "Incursion", intro: enemyName + " sends a shallow raid beyond its walls.", body: "This is not an extraction and not a conquest. The raiders do not need a stable influence border because they are not trying to hold the road.", finish: targetName + " is weaker by morning. " + enemyName + " gains reputation for proving it can still reach outward.", stats: [targetName + " power -" + powerDamage, targetName + " influence -" + influenceDamage, enemyName + " influence +" + influenceGain, "Influence contact needed: no", "Incursion limit: used tonight"] }); if (target.power < 10 && setup.AV.hasWarFront(enemy, target)) setup.AV.captureRegion(enemy, target, logs, events); return true; } const loss = setup.AV.roll(1, 4); enemy.power = Math.max(1, Number(enemy.power || 1) - loss); logs.push(enemyName + " failed an incursion against " + targetName + "."); setup.AV.pushRecapEvent(events, { type: "enemyIncursion", title: enemyName + " incursion failed", subtitle: targetName, image: setup.AV.warImage(), imageLabel: "Failed raid", intro: enemyName + " tries to cut into " + targetName + " with a shallow raid.", body: "The route breaks before the strike becomes useful.", finish: enemyName + " loses power and gains nothing from the attempt.", stats: [enemyName + " power -" + loss, "Target: " + targetName, "Influence contact needed: no", "Incursion limit: used tonight"] }); return true; }; setup.AV.enemyExtractionAction = function (enemy, target, logs, events) { if (!enemy || !target) return false; if (!setup.AV.hasExtractionRoute(enemy, target)) { if (logs) { logs.push(enemy.name + " wanted to extract from " + target.name + ", but the extraction route did not exist before tonight's moves."); } return false; } if (!setup.AV.canTargetPlayerHaremThisRecap(target)) { if (logs) logs.push(enemy.name + " wanted to raid Cinder's harem, but Cinder has already suffered or blocked one harem raid tonight."); return false; } if (setup.AV.isPlayerRegion(target)) { setup.AV.markPlayerHaremRaidThisRecap(); } if (setup.AV.isRegionProtected(target)) { if (setup.AV.isPlayerRegion(target)) { setup.AV.showBlockedExtraction(enemy, target, logs, events); } return false; } const eligible = setup.AV.getEligibleHaremGirls(target); if (!eligible || eligible.length < 1) { return false; } const girl = setup.AV.pick(eligible); if (!girl) return false; setup.AV.markGrudge(enemy, target, enemy.name + " extracted a girl", girl); const stolen = setup.AV.removeGirlFromRegion(target, girl); if (!stolen) return false; setup.AV.giveGirlToRegion(stolen, enemy); target.power = Math.max(1, Number(target.power || 1) - setup.AV.roll(1, 3)); enemy.influence = Math.min(150, Number(enemy.influence || 5) + 1); logs.push(enemy.name + " extracted " + stolen.name + " from " + target.name + "."); setup.AV.pushRecapEvent(events, { type: "enemyExtraction", title: enemy.name + " extraction", subtitle: target.name, image: setup.AV.takenImageForGirl(stolen), imageLabel: "Taken", intro: enemy.name + " sends a quiet team across the border and pulls " + stolen.name + " out of " + target.name + ".", body: "By the time she understands the route, the language around her has changed. The road is foreign, and the city waiting ahead belongs to another ruler.", finish: stolen.name + " is now in " + enemy.name + ".", stats: [ "Taken: " + stolen.name, "From: " + target.name, "New place: " + enemy.name, "Route existed before recap: yes", enemy.name + " influence +1", setup.AV.isPlayerRegion(target) ? "Cinder harem raid limit: used tonight" : "Cinder harem raid limit: not used" ] }); return true; }; setup.AV.enemyWarAction = function (enemy, target, logs, events) { if (!enemy || !target) return; if (!setup.AV.hasWarFront(enemy, target)) { logs.push(enemy.name + " wanted to attack " + target.name + ", but there was no active war front."); setup.AV.enemyExpandAction(enemy, target, logs); return; } const enemyScore = Number(enemy.power || 0) + Number(enemy.influence || 0) + setup.AV.roll(1, 20); const targetScore = Number(target.power || 0) + Number(target.influence || 0) + setup.AV.roll(1, 20); if (enemyScore >= targetScore) { const powerDamage = setup.AV.roll(2, 6); const influenceDamage = 1; const influenceGain = setup.AV.chance(35) ? 1 : 0; target.power = Math.max(1, Number(target.power || 1) - powerDamage); target.influence = Math.max(5, Number(target.influence || 5) - influenceDamage); enemy.influence = Math.min(150, Number(enemy.influence || 5) + influenceGain); let stolenGirl = null; if (!setup.AV.isRegionProtected(target) && setup.AV.chance(45)) { stolenGirl = setup.AV.takeGirlFromRegion(target); if (stolenGirl) { setup.AV.giveGirlToRegion(stolenGirl, enemy); setup.AV.markGrudge(enemy, target, enemy.name + " took a girl after a war victory", stolenGirl); logs.push(enemy.name + " took " + stolenGirl.name + " from " + target.name + " after the battle."); } } if (setup.AV.isRegionProtected(target) && setup.AV.isPlayerRegion(target)) setup.AV.showBlockedExtraction(enemy, target, logs, events); logs.push(enemy.name + " won a war against " + target.name + "."); setup.AV.pushRecapEvent(events, { type: "enemyWar", title: enemy.name + " war victory", subtitle: target.name, image: stolenGirl ? setup.AV.takenImageForGirl(stolenGirl) : setup.AV.warImage(), imageLabel: stolenGirl ? "Taken" : "War", intro: enemy.name + " attacks " + target.name + " along an active influence front.", body: enemy.name + " wins the battle and makes the rival city look smaller by morning.", finish: stolenGirl ? stolenGirl.name + " is taken during the collapse and moved to " + enemy.name + "." : "No eligible girl is taken.", stats: [target.name + " power -" + powerDamage, target.name + " influence -" + influenceDamage, enemy.name + " influence +" + influenceGain, stolenGirl ? "Taken: " + stolenGirl.name : "No harem reward", stolenGirl && setup.AV.isPlayerRegion(target) ? "Cinder harem raid limit: used tonight" : "Cinder harem raid limit: not used"] }); if (target.power < 10 && setup.AV.hasWarFront(enemy, target)) setup.AV.captureRegion(enemy, target, logs, events); } else { const loss = setup.AV.roll(1, 4); const targetGain = setup.AV.chance(20) ? 1 : 0; enemy.power = Math.max(1, Number(enemy.power || 1) - loss); target.influence = Math.min(150, Number(target.influence || 5) + targetGain); logs.push(enemy.name + " failed a war against " + target.name + "."); setup.AV.pushRecapEvent(events, { type: "enemyWarFailed", title: enemy.name + " war failed", subtitle: target.name, image: setup.AV.warImage(), imageLabel: "War", intro: enemy.name + " tries to break " + target.name + ".", body: target.name + " holds the line and turns the failed attack into local proof of strength.", finish: enemy.name + " loses face and power.", stats: [enemy.name + " power -" + loss, target.name + " influence +" + targetGain] }); } }; setup.AV.enemyExpandAction = function (enemy, target, logs) { if (!enemy || !target) return; if (!setup.AV.chance(25)) { logs.push(enemy.name + " patrolled toward " + target.name + " but failed to expand influence."); return; } const growth = 1; enemy.influence = Math.min(150, Number(enemy.influence || 5) + growth); enemy.power = Math.max(1, Number(enemy.power || 1) - 1); logs.push(enemy.name + " slowly expanded influence by +" + growth + " toward " + target.name + "."); }; setup.AV.resolveEnemyWarSupportBattles = function (logs, events) { const supports = State.variables.enemyWarSupports || []; if (supports.length < 1) return; const grouped = {}; for (const support of supports) { if (!support || !support.key) continue; if (!grouped[support.key]) grouped[support.key] = []; grouped[support.key].push(support); } for (const key in grouped) { const list = grouped[key]; const a = setup.AV.region(list[0].aId); const b = setup.AV.region(list[0].bId); if (!a || !b) continue; if (!setup.AV.hasWarFront(a, b)) { logs.push("Supported war cancelled: no active front remains between " + a.name + " and " + b.name + "."); continue; } const supportsA = list.filter(function (s) { return s.sideId === a.id; }); const supportsB = list.filter(function (s) { return s.sideId === b.id; }); const powerA = supportsA.reduce(function (sum, s) { return sum + Number(s.supportPower || 0); }, 0); const powerB = supportsB.reduce(function (sum, s) { return sum + Number(s.supportPower || 0); }, 0); const scoreA = Number(a.power || 0) + Number(a.influence || 0) + setup.AV.roll(1, 20) + powerA; const scoreB = Number(b.power || 0) + Number(b.influence || 0) + setup.AV.roll(1, 20) + powerB; const winner = scoreA >= scoreB ? a : b; const loser = winner.id === a.id ? b : a; const damage = setup.AV.roll(2, 6) + Math.floor(Math.max(powerA, powerB) / 3); const influenceDamage = 1; const influenceGain = setup.AV.chance(45) ? 1 : 0; loser.power = Math.max(1, Number(loser.power || 1) - damage); loser.influence = Math.max(5, Number(loser.influence || 5) - influenceDamage); winner.influence = Math.min(150, Number(winner.influence || 5) + influenceGain); let stolenGirl = null; if (!setup.AV.isRegionProtected(loser)) stolenGirl = setup.AV.takeGirlFromRegion(loser); if (stolenGirl) { setup.AV.giveGirlToRegion(stolenGirl, winner); setup.AV.markGrudge(winner, loser, winner.name + " took a girl during a supported war", stolenGirl); } logs.push("Supported war resolved: " + winner.name + " defeated " + loser.name + "."); setup.AV.pushRecapEvent(events, { type: "enemySupportedWar", title: "Supported war", subtitle: winner.name + " defeated " + loser.name, image: stolenGirl ? setup.AV.takenImageForGirl(stolenGirl) : setup.AV.warImage(), imageLabel: stolenGirl ? "Taken" : "War", intro: winner.name + " did not win alone. The front turned because outside supporters changed the balance.", body: "The war became a public test of who could attract allies and turn violence into debt.", finish: stolenGirl ? stolenGirl.name + " is taken from " + loser.name + " and moved to " + winner.name + "." : "No girl is taken, but the losing city looks weaker by morning.", stats: [loser.name + " power -" + damage, loser.name + " influence -" + influenceDamage, winner.name + " influence +" + influenceGain, stolenGirl ? "Taken: " + stolenGirl.name : "No girl taken"] }); if (loser.power < 10 && setup.AV.hasWarFront(winner, loser)) setup.AV.captureRegion(winner, loser, logs, events); } State.variables.enemyWarSupports = []; }; setup.AV.enemyStrategicAction = function (enemy, logs, events) { if (!enemy || setup.AV.isPlayerRegion(enemy)) return; if (State.variables.enemyFullDayActionUsed && State.variables.enemyFullDayActionUsed[enemy.id]) { if (logs) logs.push(enemy.name + " already spent the full day working on power."); return; } if (State.variables.enemySpentProtection && State.variables.enemySpentProtection[enemy.id]) { if (logs) logs.push(enemy.name + " spent the night protecting its harem and skipped a strategic raid."); return; } const warTarget = setup.AV.chooseEnemyWarTarget(enemy); const extractionTarget = setup.AV.chooseEnemyExtractionTarget(enemy); const incursionTarget = setup.AV.chooseEnemyIncursionTarget(enemy); const expandTarget = setup.AV.chooseEnemyExpandTarget(enemy); const supportPlan = setup.AV.chooseEnemySupportWar(enemy); const tauntTarget = setup.AV.chooseEnemyTauntTarget(enemy); const enemyPower = Number(enemy.power || 0); const enemyInfluence = Number(enemy.influence || 0); const enemyBase = Number(enemy.basePower || 30); const freePoolAvailable = (State.variables.freeWomenPool || []).length > 0; let possibleActions = []; if (setup.AV.enemyCanWorkPower(enemy)) { if (enemyPower < enemyBase) possibleActions.push("workPower", "workPower", "workPower"); if (enemyPower < 18) possibleActions.push("workPower", "workPower", "workPower"); if (setup.AV.chance(20)) possibleActions.push("workPower"); } if (warTarget) { const targetPower = Number(warTarget.power || 0); const isPlayerWar = setup.AV.isPlayerRegion(warTarget); if (enemyPower >= targetPower - 12) possibleActions.push("war", "war"); if (enemyPower > targetPower + 4) possibleActions.push("war"); if (isPlayerWar) possibleActions.push("war"); } if (extractionTarget) { const isPlayerExtraction = setup.AV.isPlayerRegion(extractionTarget); possibleActions.push("extraction", "extraction"); if (!isPlayerExtraction) possibleActions.push("extraction"); } if (incursionTarget && setup.AV.canStartIncursionThisRecap()) { const isPlayerIncursion = setup.AV.isPlayerRegion(incursionTarget); possibleActions.push("incursion"); if (!isPlayerIncursion) possibleActions.push("incursion"); if (enemyPower > Number(incursionTarget.power || 0) - 8) possibleActions.push("incursion"); } if (supportPlan) possibleActions.push("supportWar", "supportWar"); if (tauntTarget) possibleActions.push("taunt"); if (enemyInfluence < 10) possibleActions.push("workInfluence", "workInfluence", "workInfluence"); if (enemyInfluence < 18) possibleActions.push("workInfluence", "workInfluence"); if (setup.AV.chance(35)) possibleActions.push("workInfluence"); if (freePoolAvailable) possibleActions.push("scavenge", "scavenge", "scavenge"); if (enemyPower < enemyBase) possibleActions.push("scavenge", "scavenge"); if (setup.AV.chance(50)) possibleActions.push("scavenge"); if (expandTarget && setup.AV.chance(14)) possibleActions.push("expand"); if (possibleActions.length < 1) possibleActions.push("workInfluence", "scavenge"); const choice = setup.AV.pick(possibleActions); if (choice === "workPower") { setup.AV.enemyWorkPower(enemy, logs, events); return; } if (choice === "workInfluence") { setup.AV.enemyWorkInfluence(enemy, logs, events); return; } if (choice === "incursion" && incursionTarget && setup.AV.canStartIncursionThisRecap()) { setup.AV.enemyIncursionAction(enemy, incursionTarget, logs, events); return; } if (choice === "taunt" && tauntTarget) { setup.AV.resolveTaunt(enemy, tauntTarget, logs, events); return; } if (choice === "supportWar" && supportPlan) { setup.AV.queueEnemyWarSupport(enemy, supportPlan, logs); return; } if (choice === "war" && warTarget) { setup.AV.enemyWarAction(enemy, warTarget, logs, events); return; } if (choice === "extraction" && extractionTarget) { setup.AV.enemyExtractionAction(enemy, extractionTarget, logs, events); return; } if (choice === "expand" && expandTarget) { setup.AV.enemyExpandAction(enemy, expandTarget, logs); return; } setup.AV.enemyScavenge(enemy, logs, events); }; setup.AV.resolvePlayerActions = function (logs, events) { const actions = State.variables.pendingPlayerActions || []; for (const action of actions) { if (!action) continue; if (action.type === "protectHome") continue; if (action.type === "workPower") setup.AV.resolveWorkPower(action, logs, events); if (action.type === "workInfluence") setup.AV.resolveWorkInfluence(action, logs, events); if (action.type === "selfHarem") setup.AV.resolveSelfHaremAction(action, logs, events); if (action.type === "scavenge") setup.AV.resolveScavenge(action, logs, events); if (action.type === "incursion") setup.AV.resolveIncursion(action, logs, events); if (action.type === "stealGirl") setup.AV.resolveStealGirl(action, logs, events); if (action.type === "warFight") setup.AV.resolveWarFight(action, logs, events); if (action.type === "warSupport") setup.AV.resolveSupportWar(action, logs, events); if (action.type === "taunt") setup.AV.resolvePlayerTaunt(action, logs, events); } }; setup.AV.resolveEnemies = function (logs, events) { const regions = State.variables.regions || []; const enemies = setup.AV.shuffle(regions.filter(function (r) { return r && !setup.AV.isPlayerRegion(r); })); State.variables.enemyWarSupports = []; for (const enemy of enemies) { if (!enemy || setup.AV.isPlayerRegion(enemy)) continue; setup.AV.enemyStrategicAction(enemy, logs, events); if (State.variables.enemyFullDayActionUsed && State.variables.enemyFullDayActionUsed[enemy.id]) continue; if (setup.AV.chance(38)) setup.AV.enemyStrategicAction(enemy, logs, events); } setup.AV.resolveEnemyWarSupportBattles(logs, events); for (const enemy of enemies) { if (!enemy || setup.AV.isPlayerRegion(enemy)) continue; if (State.variables.enemySpentProtection && State.variables.enemySpentProtection[enemy.id]) continue; if (State.variables.enemyFullDayActionUsed && State.variables.enemyFullDayActionUsed[enemy.id]) continue; if (setup.AV.chance(70)) setup.AV.enemyHaremAction(enemy, logs, events); } }; setup.AV.nightRecovery = function (logs) { const regions = State.variables.regions || []; const actions = State.variables.pendingPlayerActions || []; const usedActions = 5 - Number(State.variables.actionsLeft || 0); const home = setup.AV.home(); const playerWorkedPower = actions.some(function (a) { return a && a.type === "workPower"; }) || !!State.variables.playerWorkedPowerToday; for (const r of regions) { if (!r) continue; const beforePower = Number(r.power || 0); r.power = beforePower + 5; if (logs) logs.push((r.name || "Unknown") + " night recovery: +5 power."); } if (home && usedActions >= 4 && !playerWorkedPower) { const fatigue = usedActions - 3; const beforeFatigue = Number(home.power || 1); home.power = Math.max(1, beforeFatigue - fatigue); if (logs) logs.push("Cinder fatigue: -" + fatigue + " power after heavy daily operations."); } if (home && playerWorkedPower && logs) logs.push("Cinder fatigue skipped: Work on power was a full-day reinforcement action."); }; setup.AV.createGameOverEvent = function (threat, logs, events) { const home = setup.AV.home(); const enemy = threat || setup.AV.getCinderThreat(); const enemyName = enemy ? (enemy.name || enemy.ruler || "Enemy") : "Enemy"; const women = (State.variables.women || []).slice(); let riteImage = ""; let riteGirlCount = 0; const stats = [ "Cinder power: " + (home ? Number(home.power || 0) : 0), "Enemy at border: " + enemyName, "Reason: power below 10 while an enemy influence front touched Cinder", "Result: Game over" ]; if (enemy && women.length > 0) { for (const girl of women) { if (!girl) continue; const index = State.variables.women.indexOf(girl); if (index >= 0) { State.variables.women.splice(index, 1); } const enemyActorName = setup.AV.actorNameForRegion(enemy); const enemyActorKey = setup.AV.actorKeyForRegion(enemy); const enemySide = setup.AV.actorSideForRegion(enemy); setup.AV.giveGirlToRegion(girl, enemy); setup.AV.markGirlTakenThisRecap(girl); const rite = setup.AV.applyPassageRiteStats(girl, enemyActorName, enemyActorKey, enemySide); const finishExtra = rite.outcomes.length > 0 ? " " + rite.outcomes.join(" ") : ""; if (!riteImage) { riteImage = setup.AV.riteImageForGirl(girl, "enemy"); } riteGirlCount += 1; setup.AV.pushRecapEvent(events, { type: "gameOverRite", title: "Enemy passage rite", subtitle: girl.name + " / " + enemyActorName, image: setup.AV.riteImageForGirl(girl, "enemy"), imageLabel: "Passage rite", intro: "Cinder has fallen, and " + girl.name + " is pulled out of the broken Hold.", body: "The enemy rite is not a side event. It is the proof that Cinder failed to protect its own harem when the walls stopped meaning safety.", finish: girl.name + " now belongs to " + enemyActorName + "'s victorious order." + finishExtra, stats: [ "Girl: " + girl.name, "Captor: " + enemyActorName, "Side: enemy", "Cinder harem lost: yes" ].concat(rite.stats) }); } stats.push("Enemy rite girls: " + riteGirlCount); stats.push("Enemy rites: completed for every Cinder girl"); } else { stats.push("Enemy rite girls: none available"); } if (logs) { logs.push("Game over: Cinder collapsed under " + enemyName + "."); logs.push("Game over rites completed for " + riteGirlCount + " Cinder girls."); } setup.AV.pushRecapEvent(events, { type: "gameOver", title: "Cinder collapsed", subtitle: enemyName + " broke the Hold", image: setup.AV.warImage(), image2: riteImage, imageLabel: "Collapse", image2Label: "Enemy rites", intro: "Cinder does not fall because a number is low. It falls because that weakness becomes visible while a hostile influence front is already touching the Hold.", body: enemyName + " reaches the border when Cinder no longer has enough power to make fear useful. The walls still exist, but people stop believing they can hold.", finish: "The Hold falls. Every girl who was still in Cinder's harem is taken through the enemy passage rite. No separate passage opens; the end is written inside this recap.", stats: stats }); }; setup.AV.getAllGirlsInGame = function () { const seen = []; const result = []; const v = State.variables; const pushGirl = function (girl) { if (!girl) return; if (seen.indexOf(girl) >= 0) return; seen.push(girl); result.push(girl); }; for (const girl of (v.women || [])) { pushGirl(girl); } for (const girl of (v.freeWomenPool || [])) { pushGirl(girl); } for (const region of (v.regions || [])) { if (!region || !region.harem) continue; for (const girl of region.harem) { pushGirl(girl); } } return result; }; setup.AV.hasConqueredAllCities = function () { const regions = State.variables.regions || []; if (regions.length < 1) return false; for (const region of regions) { if (!region) continue; if (!setup.AV.isPlayerRegion(region)) return false; } return true; }; setup.AV.moveAllGirlsToCinderForVictory = function () { const v = State.variables; const allGirls = setup.AV.getAllGirlsInGame(); v.women = []; v.freeWomenPool = []; for (const region of (v.regions || [])) { if (!region) continue; region.harem = []; } for (const girl of allGirls) { setup.AV.giveGirlToPlayer(girl); } return allGirls; }; setup.AV.applyVictoryKingAction = function (girl) { const stats = []; const outcomes = []; if (!girl) { return { stats: ["No girl"], outcomes: [] }; } setup.AV.normalizeGirl(girl, "Cinder Hold"); setup.AV.setCurrentCaptor(girl, "You", "self", "self"); const bond = setup.AV.getBondData(girl, "self"); if (Number(bond.resistance || 0) < 3) { bond.resistance = 3; stats.push("Resistance resolved: 3 / 3"); } else { stats.push("Resistance: " + bond.resistance + " / 3"); } bond.actions = Number(bond.actions || 0) + 1; girl.vaginal = Number(girl.vaginal || 0) + 1; stats.push("Vaginal +1"); stats.push("Bond weight +1"); stats.push("Bond weight: " + bond.actions + " / 10"); if (girl.virgin) { girl.virgin = false; girl.firstTime = "You"; stats.push("First time: You"); outcomes.push(girl.name + "'s first time is now tied to Cinder's king."); } if (!girl.pregnant && setup.AV.chance(50)) { girl.pregnant = true; girl.pregnantBy = "You"; stats.push("Pregnant: Yes"); stats.push("Pregnant by: You"); outcomes.push("Pregnancy now ties part of her future to Cinder."); } else { stats.push("Pregnant: " + (girl.pregnant ? "Already" : "No")); } girl.favoritePerson = "You"; stats.push("Favorite: You"); if (!bond.conquered && bond.actions >= 10) { bond.conquered = true; girl.conqueredBy = "You"; girl.conqueredByKey = "self"; girl.conqueredSide = "self"; const conqueredFace = setup.AV.conqueredFaceImage(girl, "self"); girl.face = conqueredFace; girl.faceImage = conqueredFace; girl.image = conqueredFace; stats.push("Conquered: Yes"); stats.push("Public face changed"); outcomes.push(girl.name + " becomes part of the final Cinder legend."); } else { stats.push("Conquered: " + (bond.conquered ? "Already" : "No")); } return { stats: stats, outcomes: outcomes }; }; setup.AV.createVictoryEvents = function (logs, events) { const v = State.variables; if (v.victoryCreated) return true; v.victoryCreated = true; v.victory = true; v.gameOver = true; v.gameOverReason = "Cinder conquered every city."; const allGirls = setup.AV.moveAllGirlsToCinderForVictory(); if (logs) { logs.push("Victory: Cinder conquered every city."); logs.push("Victory harem secured: " + allGirls.length + " girls moved to Cinder."); } setup.AV.pushRecapEvent(events, { type: "victory", title: "Total conquest", subtitle: "Every city belongs to Cinder", image: setup.AV.warImage(), imageLabel: "Victory", intro: "The last hostile banner falls.", body: "No rival ruler remains. No foreign law still cuts the map away from Cinder. The roads, the ruins, the guarded rooms, and the old enemy cities now answer to one throne.", finish: "The final procession begins. Every girl still in the world is brought under Cinder's roof before the king's ending is written.", stats: [ "Cities conquered: all", "Girls gathered: " + allGirls.length, "Final sequence: started" ] }); for (const girl of allGirls) { if (!girl) continue; const result = setup.AV.applyVictoryKingAction(girl); const finishExtra = result.outcomes.length > 0 ? " " + result.outcomes.join(" ") : ""; setup.AV.pushRecapEvent(events, { type: "victoryGirl", title: "King's claim", subtitle: girl.name, image: setup.AV.actionImageForGirl(girl, "vaginal", "self", "action"), image2: setup.AV.actionImageForGirl(girl, "vaginal", "self", "finish"), imageLabel: "Vaginal", image2Label: "Finish", intro: girl.name + " is brought into the final Cinder procession.", body: "This is not a raid anymore. There is no border left to cross and no foreign city left to take her back.", finish: girl.name + " is counted under the king of Cinder." + finishExtra, stats: [ "Girl: " + girl.name, "Final action: Vaginal", "King: You" ].concat(result.stats) }); } setup.AV.pushRecapEvent(events, { type: "victoryEnd", title: "You are the king", subtitle: "Cinder owns the whole map", image: setup.AV.nightMapImage(), imageLabel: "Kingdom", intro: "The war map has no hostile color left.", body: "Every rival ruler has lost. Every city is inside Cinder's reach. Every road leads back to your walls.", finish: "You are the king of the ruined world. End.", stats: [ "Victory: complete", "All cities conquered", "All girls secured", "End" ] }); return true; }; setup.AV.checkGameOver = function (logs, events) { const v = State.variables; const cinder = setup.AV.region("cinder") || setup.AV.home(); v.gameOver = false; v.gameOverReason = ""; if (setup.AV.hasConqueredAllCities && setup.AV.hasConqueredAllCities()) { return setup.AV.createVictoryEvents(logs, events); } if (!cinder) { v.gameOver = true; v.gameOverReason = "Cinder Hold no longer exists."; setup.AV.createGameOverEvent(null, logs, events); return true; } const cinderPower = Number(cinder.power || 0); const threat = setup.AV.getCinderThreat(); const hasFront = !!threat; if (!setup.AV.isPlayerRegion(cinder)) { if (cinderPower < 10 && hasFront) { v.gameOver = true; v.gameOverReason = "Cinder Hold has been captured under active collapse conditions."; setup.AV.createGameOverEvent(threat, logs, events); return true; } cinder.owned = true; cinder.ruler = "You"; cinder.color = "rgba(223,255,93,.35)"; logs.push("Stale Cinder ownership fixed: Cinder was marked captured, but final collapse conditions were not met."); return false; } if (cinderPower >= 10) { logs.push("Game over check passed: Cinder power is " + cinderPower + ", so the Hold survives."); return false; } if (!hasFront) { logs.push("Game over blocked: Cinder power is below 10, but no active enemy war front touches your territory."); return false; } v.gameOver = true; v.gameOverReason = "Cinder Hold collapsed because your power was below 10 while an active enemy war front touched your territory."; setup.AV.createGameOverEvent(threat, logs, events); return true; }; setup.AV.endDay = function () { setup.AV.ensureGame(); setup.AV.normalizeWomen(); setup.AV.repairMissingGirlsToScavengePool("before day resolution", null); setup.AV.syncCurrentPossessions(); const logs = []; const events = []; const v = State.variables; v.protectedTonight = {}; v.girlsTakenThisRecap = {}; v.enemySpentProtection = {}; v.protectionBlocksShown = {}; v.enemyWarSupports = []; v.enemyFullDayActionUsed = {}; v.extractionFrontsAtRecapStart = setup.AV.buildWarFrontSnapshot(); v.incursionUsedThisRecap = false; v.playerHaremRaidUsedThisRecap = false; v.playerGirlTakenThisRecap = false; v.isResolvingDay = true; v.gameOver = false; v.gameOverReason = ""; logs.push("Day " + Number(v.day || 1) + " report started."); logs.push("Player actions queued: " + (v.pendingPlayerActions || []).length + "."); setup.AV.resolveEnemyProtectionPlans(logs, events); setup.AV.resolvePlayerProtectActions(logs, events); setup.AV.resolvePlayerActions(logs, events); setup.AV.resolveEnemies(logs, events); const repaired = setup.AV.repairMissingGirlsToScavengePool("after day resolution", logs); if (repaired > 0) { setup.AV.pushRecapEvent(events, { type: "failsafe", title: "Missing survivor recovered", subtitle: "Scavenge pool updated", image: setup.AV.nightMapImage(), imageLabel: "Ruins", intro: "The night cleanup checked every known girl after all wars, raids, captures, and scavenges.", body: "One or more girls had no active city, no Cinder slot, and no scavenge entry. The failsafe moved them into the scavenge pool instead of letting them depop.", finish: "They can be found later by player or enemy scavenge.", stats: [ "Recovered to scavenge pool: " + repaired, "Result: no depop", "Recovery timing: end of day" ] }); } setup.AV.nightRecovery(logs); setup.AV.clamp(); setup.AV.ensureRegionPositions(); setup.AV.repairMissingGirlsToScavengePool("after night cleanup", logs); const isGameOver = setup.AV.checkGameOver(logs, events); v.dayRecap = logs; v.recapEvents = events; v.recapIndex = 0; v.showRecapPopup = events.length > 0; v.pendingPlayerActions = []; v.actionsLeft = 5; v.isResolvingDay = false; v.girlsTakenThisRecap = {}; v.protectedTonight = {}; v.enemySpentProtection = {}; v.protectionBlocksShown = {}; v.enemyFullDayActionUsed = {}; v.extractionFrontsAtRecapStart = {}; v.incursionUsedThisRecap = false; v.playerHaremRaidUsedThisRecap = false; v.playerGirlTakenThisRecap = false; v.playerWorkedPowerToday = false; if (!isGameOver) { v.day = Number(v.day || 1) + 1; v.hour = 7; v.tab = "recap"; } else { v.tab = "recap"; } }; setup.AV.clamp = function () { delete State.variables.player; delete State.variables.food; delete State.variables.scrap; delete State.variables.fuel; const regions = State.variables.regions || []; for (const r of regions) { if (!r) continue; r.power = Math.max(1, Number(r.power || 1)); r.influence = Math.max(1, Math.min(150, Number(r.influence || 1))); } }; <</script>> <<run setup.AV.ensureGame()>> <<if $regions is undefined>> <<run setup.AV.ensureGame()>> <</if>> <<if $regions and $regions.length lt 1>> <<run setup.AV.ensureGame()>> <</if>> <<if $women is undefined>><<set $women = []>><</if>> <<if $freeWomenPool is undefined>><<set $freeWomenPool = []>><</if>> <<if $enemyWarSupports is undefined>><<set $enemyWarSupports = []>><</if>> <<if $tauntHistory is undefined>><<set $tauntHistory = {}>><</if>> <<if $possessionHistory is undefined>><<set $possessionHistory = {}>><</if>> <<script>> delete State.variables.player; delete State.variables.food; delete State.variables.scrap; delete State.variables.fuel; <</script>> <<if $day is undefined>><<set $day = 1>><</if>> <<if $hour is undefined>><<set $hour = 10>><</if>> <<if $actionsLeft is undefined>><<set $actionsLeft = 5>><</if>> <<if $pendingPlayerActions is undefined>><<set $pendingPlayerActions = []>><</if>> <<if $dayRecap is undefined>><<set $dayRecap = []>><</if>> <<if $recapEvents is undefined>><<set $recapEvents = []>><</if>> <<if $recapIndex is undefined>><<set $recapIndex = 0>><</if>> <<if $showRecapPopup is undefined>><<set $showRecapPopup = false>><</if>> <<if $gameOver is undefined>><<set $gameOver = false>><</if>> <<if $gameOverReason is undefined>><<set $gameOverReason = "">><</if>> <<if $girlsDistributed is undefined>><<set $girlsDistributed = false>><</if>> <<if $protectedTonight is undefined>><<set $protectedTonight = {}>><</if>> <<if $girlsTakenThisRecap is undefined>><<set $girlsTakenThisRecap = {}>><</if>> <<if $enemySpentProtection is undefined>><<set $enemySpentProtection = {}>><</if>> <<if $protectionBlocksShown is undefined>><<set $protectionBlocksShown = {}>><</if>> <<if $isResolvingDay is undefined>><<set $isResolvingDay = false>><</if>> <<if $incursionUsedThisRecap is undefined>><<set $incursionUsedThisRecap = false>><</if>> <<if $playerHaremRaidUsedThisRecap is undefined>><<set $playerHaremRaidUsedThisRecap = false>><</if>> <<if $playerGirlTakenThisRecap is undefined>><<set $playerGirlTakenThisRecap = false>><</if>> <<if $enemyFullDayActionUsed is undefined>><<set $enemyFullDayActionUsed = {}>><</if>> <<if $extractionFrontsAtRecapStart is undefined>><<set $extractionFrontsAtRecapStart = {}>><</if>> <<if $playerWorkedPowerToday is undefined>><<set $playerWorkedPowerToday = false>><</if>> <<if !$selectedRegion>><<set $selectedRegion = $regions[0].id>><</if>> <<if !$tab>><<set $tab = "base">><</if>> <<if $tab is "army">><<set $tab = "base">><</if>> <<if $selectedWoman is undefined>><<set $selectedWoman = 0>><</if>> <<run setup.AV.ensureGame()>> <<run setup.AV.normalizeWomen()>> <<run setup.AV.ensureRegionPositions()>> <<run setup.AV.syncCurrentPossessions()>> <<run setup.AV.clamp()>> <<set _hourSafe = Number($hour)>> <<set _isNight = false>> <<if _hourSafe >= 19>><<set _isNight = true>><</if>> <<if _hourSafe < 6>><<set _isNight = true>><</if>> <<set _selected = $regions[0]>> <<for _r range $regions>> <<if _r>> <<if _r.id is $selectedRegion>> <<set _selected = _r>> <</if>> <</if>> <</for>> <<if !_selected>> <<set $selectedRegion = $regions[0].id>> <<set _selected = $regions[0]>> <</if>> <<set _home = setup.AV.home()>> <<if !_home>> <<run setup.AV.ensureRegionsReady()>> <<run setup.AV.ensureGame()>> <<set _home = setup.AV.home()>> <</if>> <<if !_selected>> <<set _selected = _home>> <</if>> <<set _selectedHarem = setup.AV.getHarem(_selected)>> <<set _homeProtectedQueued = setup.AV.hasQueued("protectHome", "", "")>> <<if $gameOver and !$victory>> <<set _staleGameOverCheck = _home && Number(_home.power || 0) < 10 && setup.AV.hasWarOnPlayerTerritory()>> <<if !_staleGameOverCheck>> <<set $gameOver = false>> <<set $gameOverReason = "">> <</if>> <</if>> <style> #ui-bar,#ui-bar-toggle{display:none!important} html,body{margin:0!important;padding:0!important;overflow:hidden!important;background:#020303!important} #story{margin:0!important;width:100vw!important;max-width:none!important} #passages{margin:0!important;padding:0!important;width:100vw!important;max-width:none!important} .passage{margin:0!important;padding:0!important} .game-screen{position:fixed;inset:0;width:100vw;height:100vh;background:#020303;color:#e9efe4;font-family:Inter,Arial,Helvetica,sans-serif;overflow:hidden} .game-screen::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.94),rgba(0,0,0,.78) 36%,rgba(0,0,0,.20)),radial-gradient(circle at 77% 50%,rgba(255,91,32,.12),transparent 26%);z-index:1;pointer-events:none} .game-screen::after{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.034) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);background-size:64px 64px;opacity:.16;mix-blend-mode:overlay;z-index:2;pointer-events:none} .game-layout{position:relative;z-index:5;height:100vh;display:grid;grid-template-columns:430px 1fr;gap:18px;padding:18px;box-sizing:border-box} .left-panel{height:calc(100vh - 36px);border:1px solid rgba(223,255,93,.22);background:rgba(4,7,6,.92);box-shadow:0 18px 42px rgba(0,0,0,.42);display:grid;grid-template-rows:auto auto 1fr auto;overflow:hidden} .panel-head{padding:17px 19px 15px;border-bottom:1px solid rgba(223,255,93,.16)} .panel-kicker{color:#dfff5d;font-size:.62rem;font-weight:950;letter-spacing:.18em;text-transform:uppercase;margin-bottom:8px} .panel-title{margin:0;font-size:1.85rem;font-weight:950;letter-spacing:-.06em;line-height:.9;text-transform:uppercase}.panel-title span{color:#ff5b20} .panel-sub{margin-top:10px;color:rgba(232,237,222,.54);font-size:.68rem;font-weight:850;letter-spacing:.12em;text-transform:uppercase} .tabs{display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid rgba(223,255,93,.16)} .tabs a,.tabs .link-internal{display:block;padding:11px 4px;text-align:center;color:rgba(232,237,222,.62)!important;text-decoration:none!important;font-size:.55rem;font-weight:950;letter-spacing:.08em;text-transform:uppercase;border-right:1px solid rgba(223,255,93,.12);background:rgba(0,0,0,.18)} .tabs a:hover,.tabs .link-internal:hover{color:#fff!important;background:rgba(223,255,93,.08)} .panel-body{padding:16px 18px;overflow:auto}.block-title{color:#dfff5d;font-size:.66rem;font-weight:950;letter-spacing:.16em;text-transform:uppercase;margin:0 0 12px} .stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:18px}.stat{border:1px solid rgba(232,237,222,.14);background:rgba(255,255,255,.035);padding:10px 11px}.stat label{display:block;color:rgba(232,237,222,.48);font-size:.58rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase;margin-bottom:5px}.stat b{font-size:1rem;color:#e9efe4}.stat.good b{color:#dfff5d} .actions{display:grid;gap:9px}.actions a,.actions .link-internal{position:relative;display:block;padding:12px 14px;border:1px solid rgba(232,237,222,.18);background:linear-gradient(90deg,rgba(16,20,17,.94),rgba(5,7,6,.82));color:#e9efe4!important;text-decoration:none!important;text-transform:uppercase;font-size:.68rem;font-weight:950;letter-spacing:.13em}.actions a::before,.actions .link-internal::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:#dfff5d}.actions a:hover,.actions .link-internal:hover{border-color:rgba(223,255,93,.75);background:linear-gradient(90deg,rgba(31,38,28,.95),rgba(5,7,6,.90))} .action-disabled{position:relative;display:block;padding:12px 14px;border:1px solid rgba(232,237,222,.10);background:linear-gradient(90deg,rgba(80,84,78,.24),rgba(10,12,11,.42));color:rgba(232,237,222,.34)!important;text-decoration:none!important;text-transform:uppercase;font-size:.68rem;font-weight:950;letter-spacing:.13em;cursor:not-allowed;filter:grayscale(1);opacity:.62} .action-disabled::before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:rgba(232,237,222,.20)} .action-disabled small{display:block;margin-top:5px;color:rgba(232,237,222,.30);font-size:.50rem;font-weight:850;letter-spacing:.10em;text-transform:uppercase} .card{border:1px solid rgba(232,237,222,.14);background:rgba(0,0,0,.28);padding:12px;margin-bottom:10px}.card-top{display:flex;justify-content:space-between;gap:10px;margin-bottom:8px}.card-name{font-weight:950;text-transform:uppercase;letter-spacing:.08em}.card-state{color:#ff5b20;font-size:.61rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase}.card-data{color:rgba(232,237,222,.68);font-size:.74rem;line-height:1.55} .harem-grid{display:grid;grid-template-columns:1fr;gap:16px}.girl-card{display:grid;grid-template-columns:1fr;gap:12px;border:1px solid rgba(232,237,222,.16);background:linear-gradient(180deg,rgba(7,9,8,.98),rgba(12,16,13,.82));padding:12px;color:#e9efe4!important;box-shadow:0 12px 28px rgba(0,0,0,.30);position:relative;overflow:hidden}.girl-card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:5px;background:#ff5b20} .girl-media{display:grid;grid-template-columns:1fr 1fr;gap:10px;position:relative;z-index:2}.girl-shot{position:relative;height:260px;overflow:hidden;border:1px solid rgba(232,237,222,.18);background:#101410;box-shadow:0 12px 24px rgba(0,0,0,.34)}.girl-shot span{position:absolute;left:8px;top:8px;z-index:3;padding:4px 7px;background:rgba(0,0,0,.70);border:1px solid rgba(223,255,93,.22);color:#dfff5d;font-size:.50rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase}.girl-face,.girl-body{width:100%;height:100%;object-fit:cover;display:block;filter:contrast(1.04) saturate(.96)}.girl-face{object-position:center top}.girl-body{object-position:center center} .girl-shot.locked-body{background:#050706}.girl-shot.locked-body .girl-body{filter:blur(30px) brightness(.50) saturate(.45) contrast(.75);transform:scale(1.26)}.girl-shot.locked-body::after{content:"";position:absolute;inset:0;z-index:4;background:radial-gradient(circle at 50% 50%,rgba(0,0,0,.05),rgba(0,0,0,.42)),repeating-linear-gradient(45deg,rgba(255,255,255,.025) 0 2px,rgba(0,0,0,.025) 2px 5px);pointer-events:none}.girl-shot.locked-body::before{content:"BODY LOCKED";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:5;padding:8px 10px;border:1px solid rgba(223,255,93,.38);background:rgba(0,0,0,.76);color:#dfff5d;font-size:.58rem;font-weight:950;letter-spacing:.14em;text-transform:uppercase;pointer-events:none;white-space:nowrap} .girl-info{position:relative;z-index:2;display:flex;flex-direction:column;gap:8px;min-width:0}.girl-name{font-size:1rem;font-weight:950;letter-spacing:-.03em;text-transform:uppercase;color:#fff}.girl-place{color:#ff5b20;font-size:.56rem;font-weight:950;letter-spacing:.13em;text-transform:uppercase}.girl-stats{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:auto}.girl-stat{border:1px solid rgba(232,237,222,.12);background:rgba(255,255,255,.035);padding:7px 8px}.girl-stat label{display:block;color:rgba(232,237,222,.42);font-size:.48rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase;margin-bottom:3px}.girl-stat b{color:#e9efe4;font-size:.68rem;text-transform:uppercase}.mini-actions{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-top:8px}.mini-actions a,.mini-actions .link-internal{display:block;text-align:center;padding:8px 6px;border:1px solid rgba(232,237,222,.16);background:rgba(255,255,255,.04);color:#e9efe4!important;text-decoration:none!important;font-size:.54rem;font-weight:950;text-transform:uppercase;letter-spacing:.08em} .queue-list{display:grid;gap:8px;margin-bottom:16px}.queue-item{border:1px solid rgba(223,255,93,.18);background:rgba(223,255,93,.05);padding:9px 10px;color:rgba(232,237,222,.78);font-size:.68rem;font-weight:850;text-transform:uppercase;letter-spacing:.08em}.recap-list{display:grid;gap:8px}.recap-item{border-left:4px solid #ff5b20;background:rgba(255,255,255,.04);padding:10px 12px;color:rgba(232,237,222,.78);font-size:.74rem;line-height:1.45}.bottom{padding:14px 18px;border-top:1px solid rgba(223,255,93,.16);display:grid;grid-template-columns:1fr 1fr;gap:10px}.bottom.save-row{grid-template-columns:1fr 1fr 1fr}.bottom a,.bottom .link-internal{display:block;text-align:center;padding:10px;border:1px solid rgba(223,255,93,.24);background:rgba(223,255,93,.07);color:#e9efe4!important;text-decoration:none!important;font-size:.65rem;font-weight:950;letter-spacing:.14em;text-transform:uppercase}.bottom a:hover,.bottom .link-internal:hover{background:rgba(223,255,93,.15)} .map-holder{position:relative;height:calc(100vh - 36px);border:1px solid rgba(223,255,93,.20);background:#050706;box-shadow:0 18px 42px rgba(0,0,0,.42);overflow:hidden}.map-stage{position:absolute;inset:0;overflow:hidden;background:#020303}.map-board{position:absolute;left:50%;top:50%;width:min(100%,calc((100vh - 36px) * 1.777777));aspect-ratio:16/9;transform:translate(-50%,-50%);overflow:hidden;background:#050706;border-left:1px solid rgba(223,255,93,.12);border-right:1px solid rgba(223,255,93,.12)}.map-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;filter:contrast(1.05) saturate(.95);z-index:1}.map-fade{position:absolute;inset:0;background:linear-gradient(rgba(0,0,0,.04),rgba(0,0,0,.24)),radial-gradient(circle at 50% 55%,transparent 0%,rgba(0,0,0,.32) 78%,rgba(0,0,0,.62) 100%);z-index:2;pointer-events:none}.map-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(223,255,93,.055) 1px,transparent 1px),linear-gradient(90deg,rgba(223,255,93,.035) 1px,transparent 1px);background-size:80px 80px;mix-blend-mode:screen;opacity:.30;z-index:3;pointer-events:none}.zones-layer{position:absolute;inset:0;z-index:4;pointer-events:none}.war-layer{position:absolute;inset:0;z-index:7;pointer-events:none}.points-layer{position:absolute;inset:0;z-index:8;pointer-events:none}.influence-zone{position:absolute;width:var(--d,18%);aspect-ratio:1/1;transform:translate(-50%,-50%);border-radius:50%;background:radial-gradient(circle,var(--c,rgba(255,91,32,.35)) 0%,var(--c,rgba(255,91,32,.35)) 43%,rgba(0,0,0,0) 72%);border:2px solid rgba(232,237,222,.20);box-shadow:0 0 28px rgba(0,0,0,.38),inset 0 0 24px rgba(255,255,255,.05);mix-blend-mode:screen;opacity:.96}.influence-zone::after{content:"";position:absolute;inset:8%;border-radius:50%;border:1px dashed rgba(232,237,222,.22)}.influence-zone.owned{border-color:rgba(223,255,93,.58);box-shadow:0 0 24px rgba(223,255,93,.18),inset 0 0 24px rgba(223,255,93,.06)}.influence-zone.enemy{border-color:rgba(255,91,32,.42);box-shadow:0 0 24px rgba(255,91,32,.16),inset 0 0 24px rgba(255,91,32,.05)}.war-zone-btn{position:absolute;width:var(--s,10%);aspect-ratio:1/1;transform:translate(-50%,-50%);border-radius:50%;border:2px solid rgba(255,91,32,.95);background:radial-gradient(circle,rgba(255,91,32,.52) 0%,rgba(255,91,32,.26) 44%,rgba(255,91,32,.08) 70%,rgba(0,0,0,0) 100%);box-shadow:0 0 28px rgba(255,91,32,.72),inset 0 0 26px rgba(255,91,32,.30);color:#fff;font-size:.56rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase;cursor:pointer;pointer-events:auto;appearance:none;padding:0;margin:0;z-index:20;animation:warPulse 1.4s infinite}.war-zone-btn span{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.78);border:1px solid rgba(255,91,32,.75);padding:5px 7px;color:#ff5b20;text-shadow:0 2px 4px #000;pointer-events:none}@keyframes warPulse{0%{opacity:.72;transform:translate(-50%,-50%) scale(.96)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.04)}100%{opacity:.72;transform:translate(-50%,-50%) scale(.96)}}.region-btn{position:absolute;z-index:6;width:24px;height:24px;border:2px solid #e9efe4;background:rgba(0,0,0,.82);transform:translate(-50%,-50%) rotate(45deg);box-shadow:0 0 14px rgba(255,255,255,.52);cursor:pointer;pointer-events:auto;appearance:none;padding:0;margin:0}.region-btn.owned{border-color:#dfff5d;background:#dfff5d;box-shadow:0 0 20px rgba(223,255,93,.9)}.region-btn.enemy{border-color:#ff5b20;box-shadow:0 0 18px rgba(255,91,32,.75)}.region-label{position:absolute;z-index:7;transform:translate(-50%,20px);white-space:nowrap;color:#e9efe4;font-size:.58rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase;text-shadow:0 2px 4px #000;pointer-events:none}.map-hud{position:absolute;left:18px;top:18px;z-index:8;display:flex;gap:10px;align-items:center}.map-chip{border:1px solid rgba(232,237,222,.18);background:rgba(0,0,0,.58);padding:8px 10px;color:rgba(232,237,222,.72);font-size:.60rem;font-weight:950;letter-spacing:.14em;text-transform:uppercase}.map-chip strong{color:#dfff5d;font-weight:950} .recap-popup-bg{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.76);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}.recap-popup{width:min(820px,94vw);max-height:92vh;overflow:auto;border:1px solid rgba(255,91,32,.55);background:rgba(4,7,6,.97);box-shadow:0 24px 60px rgba(0,0,0,.7);padding:18px;color:#e9efe4}.recap-popup-kicker{color:#dfff5d;font-size:.58rem;font-weight:950;letter-spacing:.16em;text-transform:uppercase;margin-bottom:7px}.recap-popup-title{font-size:1.7rem;font-weight:950;letter-spacing:-.05em;text-transform:uppercase;color:#fff}.recap-popup-sub{color:#ff5b20;font-size:.68rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase;margin-top:5px}.recap-popup-images{display:grid;gap:10px;margin:16px 0}.recap-popup-images.single{grid-template-columns:1fr}.recap-popup-shot{position:relative;height:360px;overflow:hidden;border:1px solid rgba(232,237,222,.16);background:#050706;box-shadow:0 12px 28px rgba(0,0,0,.38);display:flex;align-items:center;justify-content:center}.recap-popup-images.single .recap-popup-shot{height:min(560px,62vh)}.recap-popup-shot span{position:absolute;left:10px;top:10px;z-index:2;padding:5px 8px;background:rgba(0,0,0,.72);border:1px solid rgba(255,91,32,.55);color:#ff5b20;font-size:.55rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase}.recap-popup-img{width:100%;height:100%;object-fit:contain;object-position:center center;display:block;background:#050706}.recap-popup-body{color:rgba(232,237,222,.76);font-size:.86rem;line-height:1.55;margin:14px 0}.recap-popup-finish{border-left:4px solid #dfff5d;background:rgba(223,255,93,.055);padding:11px 12px;color:rgba(232,237,222,.82);font-size:.82rem;line-height:1.5;margin:14px 0}.recap-popup-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}.recap-popup-stat{border:1px solid rgba(232,237,222,.14);background:rgba(255,255,255,.04);padding:9px 10px;color:#e9efe4;font-size:.72rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em}.recap-popup-actions{margin:14px 0 0}.recap-popup-actions-top{position:sticky;top:0;z-index:40;margin:14px 0 18px;padding:8px 0;background:linear-gradient(180deg,rgba(4,7,6,.98),rgba(4,7,6,.88));backdrop-filter:blur(5px)}.recap-popup-actions a,.recap-popup-actions .link-internal{display:block;text-align:center;padding:13px 15px;border:1px solid rgba(223,255,93,.28);background:rgba(223,255,93,.08);color:#e9efe4!important;text-decoration:none!important;font-size:.72rem;font-weight:950;letter-spacing:.14em;text-transform:uppercase}.recap-popup-actions a:hover,.recap-popup-actions .link-internal:hover{background:rgba(223,255,93,.16);border-color:rgba(223,255,93,.55)} @media(max-width:980px){.game-layout{grid-template-columns:1fr;grid-template-rows:48vh 1fr;padding:12px}.map-holder{height:48vh}.left-panel{height:auto;min-height:0}.map-board{width:100%}} @media(max-width:720px){.girl-media{grid-template-columns:1fr}.girl-shot{height:300px}.recap-popup-shot{height:300px}.recap-popup-images.single .recap-popup-shot{height:380px}.recap-popup-stats{grid-template-columns:1fr}} </style> <div class="game-screen"> <div class="game-layout"> <div class="left-panel"> <div class="panel-head"> <div class="panel-kicker">Apocalyptic Vice</div> <h1 class="panel-title">Command <span>status</span></h1> <div class="panel-sub">Day $day · Hour $hour · Actions $actionsLeft / 5</div> </div> <div class="tabs"> <<link "Base">><<set $tab = "base">><<goto "Main">><</link>> <<link "Women">><<set $tab = "women">><<goto "Main">><</link>> <<link "Region">><<set $tab = "region">><<goto "Main">><</link>> <<link "Recap">><<set $tab = "recap">><<goto "Main">><</link>> </div> <div class="panel-body"> <<if $pendingPlayerActions.length > 0>> <h2 class="block-title">Queued actions</h2> <div class="queue-list"> <<for _a range $pendingPlayerActions>> <div class="queue-item"><<print _a.label>> · <<print _a.hours>>h</div> <</for>> </div> <</if>> <<if $tab is "base">> <h2 class="block-title">Hold status</h2> <div class="stat-grid"> <div class="stat good"><label>Cinder power</label><b><<print _home.power>></b></div> <div class="stat good"><label>Cinder influence</label><b><<print Math.round(_home.influence)>></b></div> <div class="stat"><label>Actions</label><b>$actionsLeft / 5</b></div> <div class="stat"><label>War fronts</label><b><<print setup.AV.getActiveWarFronts().length>></b></div> <div class="stat"><label>Protection cost</label><b>-4 P / -1 I</b></div> <div class="stat"><label>Protection queued</label><b><<if _homeProtectedQueued>>Yes<<else>>No<</if>></b></div> <div class="stat"><label>Incursion tonight</label><b><<if $incursionUsedThisRecap or setup.AV.hasAnyQueuedPlayerIncursion()>>Used<<else>>Free<</if>></b></div> <div class="stat"><label>Harem raid limit</label><b><<if $playerHaremRaidUsedThisRecap>>Used<<else>>Free<</if>></b></div> </div> <h2 class="block-title">Orders</h2> <div class="actions"> <<if $actionsLeft > 0>> <<if setup.AV.canQueueFullDayAction()>><<link "Work on power">><<run setup.AV.queueWorkPower()>><<goto "Main">><</link>><<else>><div class="action-disabled">Work on power<small>Full day only · needs 5 / 5 actions and no queued order</small></div><</if>> <<link "Protect Cinder tonight">><<run setup.AV.queueProtectHome()>><<goto "Main">><</link>> <<link "Work on influence">><<run setup.AV.queueInfluenceWork()>><<goto "Main">><</link>> <<link "Queue scavenge">><<run setup.AV.queueScavenge()>><<goto "Main">><</link>> <<else>> <div class="card"><div class="card-data">No actions left today.</div></div> <</if>> </div> <</if>> <<if $tab is "women">> <h2 class="block-title">Your harem</h2> <div class="harem-grid"> <<for _i = 0; _i lt $women.length; _i++>> <<set _w = $women[_i]>> <<if _w>> <<set _bond = setup.AV.getBondData(_w, "self")>> <div class="girl-card"> <div class="girl-media"> <div class="girl-shot"><img class="girl-face" @src="setup.AV.faceImage(_w)"><span>Face</span></div> <div class="girl-shot"><img class="girl-body" @src="setup.AV.bodyImage(_w)"><span>Body</span></div> </div> <div class="girl-info"> <div><div class="girl-name"><<print _w.name>></div><div class="girl-place"><<print _w.place>></div></div> <div class="girl-stats"> <div class="girl-stat"><label>Virgin</label><b><<if _w.virgin>>Yes<<else>>No<</if>></b></div> <div class="girl-stat"><label>Pregnant</label><b><<if _w.pregnant>>Yes<<else>>No<</if>></b></div> <div class="girl-stat"><label>Pregnant by</label><b><<print _w.pregnantBy>></b></div> <div class="girl-stat"><label>First time</label><b><<print _w.firstTime>></b></div> <div class="girl-stat"><label>Favorite</label><b><<print _w.favoritePerson>></b></div> <div class="girl-stat"><label>Resistance</label><b><<print _bond.resistance>> / 3</b></div> <div class="girl-stat"><label>Bond weight</label><b><<print _bond.actions>> / 10</b></div> <div class="girl-stat"><label>Conquered</label><b><<if _bond.conquered>>Yes<<else>>No<</if>></b></div> <div class="girl-stat"><label>Stealable</label><b><<if setup.AV.isGirlStealable(_w)>>Yes<<else>>No<</if>></b></div> <div class="girl-stat"><label>Vaginal</label><b><<print _w.vaginal>></b></div> <div class="girl-stat"><label>Anal</label><b><<print _w.anal>></b></div> <div class="girl-stat"><label>Oral</label><b><<print _w.oral>></b></div> </div> <<if $actionsLeft > 0>> <div class="mini-actions"> <<capture _i>><<link "Vaginal">><<run setup.AV.queueSelfHaremAction(_i, "vaginal")>><<goto "Main">><</link>><</capture>> <<capture _i>><<link "Anal">><<run setup.AV.queueSelfHaremAction(_i, "anal")>><<goto "Main">><</link>><</capture>> <<capture _i>><<link "Oral">><<run setup.AV.queueSelfHaremAction(_i, "oral")>><<goto "Main">><</link>><</capture>> </div> <</if>> </div> </div> <</if>> <</for>> </div> <</if>> <<if $tab is "region">> <<set _tauntGirl = setup.AV.getTauntGirlForPair(_home, _selected)>> <<set _touchesCinder = setup.AV.hasWarFront(_home, _selected)>> <h2 class="block-title">Selected region</h2> <div class="card"> <div class="card-top"><div class="card-name"><<print _selected.name>></div><div class="card-state"><<if _selected.owned>>Owned<<else>>Enemy<</if>></div></div> <div class="card-data">Ruler: <<print _selected.ruler>><br>Power: <<print _selected.power>><br>Influence: <<print Math.round(_selected.influence)>><br>Harem size: <<print _selectedHarem.length>><br>Eligible for extraction: <<print setup.AV.getEligibleHaremGirls(_selected).length>><br>Influence touches Cinder: <<if _touchesCinder>>Yes<<else>>No<</if>><br>Taunt valid: <<if _tauntGirl.valid>>Yes — <<print _tauntGirl.girlName>><<else>>No<</if>></div> </div> <<if !_selected.owned>> <h2 class="block-title">Enemy orders</h2> <div class="actions"> <<if $actionsLeft > 0>> <<link "Queue incursion">><<run setup.AV.queueIncursion(_selected.id)>><<goto "Main">><</link>> <<if _tauntGirl.valid>> <<link "Queue taunt">><<run setup.AV.queueTaunt(_selected.id)>><<goto "Main">><</link>> <div class="card"><div class="card-data">Taunt ready with: <<print _tauntGirl.girlName>><br>Source: <<print _tauntGirl.source>></div></div> <<else>> <div class="card"><div class="card-data">Taunt locked. Cinder needs history with this city and must currently have the related girl in the harem.</div></div> <</if>> <<else>> <div class="card"><div class="card-data">No actions left today.</div></div> <</if>> </div> <h2 class="block-title">Enemy harem</h2> <div class="harem-grid"> <<for _gi = 0; _gi lt _selectedHarem.length; _gi++>> <<set _g = _selectedHarem[_gi]>> <<set _rid = _selected.id>> <<set _enemyBond = setup.AV.getBondData(_g, setup.AV.actorKeyForRegion(_selected))>> <div class="girl-card"> <div class="girl-media"><div class="girl-shot"><img class="girl-face" @src="setup.AV.faceImage(_g)"><span>Face</span></div><div class="girl-shot locked-body"><img class="girl-body" @src="setup.AV.bodyImage(_g)"><span>Body</span></div></div> <div class="girl-info"> <div><div class="girl-name"><<print _g.name>></div><div class="girl-place"><<print _g.place>></div></div> <div class="girl-stats"> <div class="girl-stat"><label>Virgin</label><b><<if _g.virgin>>Yes<<else>>No<</if>></b></div><div class="girl-stat"><label>Pregnant</label><b><<if _g.pregnant>>Yes<<else>>No<</if>></b></div><div class="girl-stat"><label>Pregnant by</label><b><<print _g.pregnantBy>></b></div><div class="girl-stat"><label>Current captor</label><b><<print _g.currentCaptorName || "Unknown">></b></div><div class="girl-stat"><label>Resistance</label><b><<print _enemyBond.resistance>> / 3</b></div><div class="girl-stat"><label>Bond weight</label><b><<print _enemyBond.actions>> / 10</b></div><div class="girl-stat"><label>Conquered</label><b><<if _enemyBond.conquered>>Yes<<else>>No<</if>></b></div><div class="girl-stat"><label>Stealable</label><b><<if setup.AV.isGirlStealable(_g)>>Yes<<else>>No<</if>></b></div><div class="girl-stat"><label>Favorite</label><b><<print _g.favoritePerson>></b></div><div class="girl-stat"><label>Vaginal</label><b><<print _g.vaginal>></b></div><div class="girl-stat"><label>Anal</label><b><<print _g.anal>></b></div><div class="girl-stat"><label>Oral</label><b><<print _g.oral>></b></div> </div> <div class="actions"> <<if $actionsLeft > 0>> <<if setup.AV.isGirlStealable(_g) and _touchesCinder>> <<capture _rid _gi>><<link "Queue extraction">><<run setup.AV.queueSteal(_rid, _gi)>><<goto "Main">><</link>><</capture>> <<elseif setup.AV.isGirlStealable(_g) and !_touchesCinder>> <div class="card"><div class="card-data">Extraction locked: influence zones do not touch. Incursion is still possible.</div></div> <<else>> <div class="card"><div class="card-data">Extraction locked: conquered or already moved this recap.</div></div> <</if>> <</if>> </div> </div> </div> <</for>> </div> <</if>> <</if>> <<if $tab is "war">> <h2 class="block-title">War zone</h2> <<if !$selectedWarZone>> <div class="card"><div class="card-top"><div class="card-name">No active front</div><div class="card-state">Empty</div></div><div class="card-data">No overlapping conflict selected.</div></div> <<else>> <<set _wa = setup.AV.region($selectedWarZone.aId)>> <<set _wb = setup.AV.region($selectedWarZone.bId)>> <div class="card"><div class="card-top"><div class="card-name"><<print $selectedWarZone.aName>> / <<print $selectedWarZone.bName>></div><div class="card-state">Conflict</div></div><div class="card-data">Danger: <<print $selectedWarZone.danger>> / 100<br>Side A: <<print _wa.name>> · Power <<print _wa.power>><br>Side B: <<print _wb.name>> · Power <<print _wb.power>><br>Status: Active overlap</div></div> <h2 class="block-title">War orders</h2> <div class="actions"> <<if $actionsLeft > 0>> <<if _wa.owned || _wb.owned>> <<link "Queue fight this war">><<run setup.AV.queueWarFight()>><<goto "Main">><</link>> <<else>> <<link "Support side A">><<run setup.AV.queueSupportWar($selectedWarZone.aId)>><<goto "Main">><</link>> <<link "Support side B">><<run setup.AV.queueSupportWar($selectedWarZone.bId)>><<goto "Main">><</link>> <</if>> <<else>> <div class="card"><div class="card-data">No actions left today.</div></div> <</if>> </div> <</if>> <</if>> <<if $tab is "recap">> <h2 class="block-title">Daily recap</h2> <<if $dayRecap.length < 1>> <div class="card"><div class="card-data">No recap yet. End the day to generate one.</div></div> <<else>> <div class="recap-list"> <<for _log range $dayRecap>> <div class="recap-item"><<print _log>></div> <</for>> </div> <</if>> <</if>> </div> <div class="bottom save-row"> <<link "End day">><<run setup.AV.endDay()>><<goto "Main">><</link>> <<link "Save menu">><<run setup.AV.openSaveMenu()>><</link>> <<link "Lobby">><<goto "Start">><</link>> </div> </div> <div class="map-holder"> <div class="map-stage"> <div class="map-board"> <<if _isNight>><img class="map-img" src="data/misc/MAP_NIGHT.png"><<else>><img class="map-img" src="data/misc/MAP.png"><</if>> <div class="map-fade"></div><div class="map-grid"></div><div id="av-zones-layer" class="zones-layer"></div><div id="av-war-layer" class="war-layer"></div><div id="av-points-layer" class="points-layer"></div> <div class="map-hud"><div class="map-chip">Day <strong>$day</strong></div><div class="map-chip">Hour <strong>$hour</strong></div><div class="map-chip">Actions <strong>$actionsLeft / 5</strong></div></div> </div> </div> </div> <<if $showRecapPopup and $recapEvents and $recapEvents.length > 0>> <<set _event = $recapEvents[$recapIndex]>> <<set _nextRecapIndex = $recapIndex + 1>> <div class="recap-popup-bg"> <div class="recap-popup"> <div class="recap-popup-kicker">Daily event <<print $recapIndex + 1>> / <<print $recapEvents.length>></div> <div class="recap-popup-title"><<print _event.title>></div> <div class="recap-popup-sub"><<print _event.subtitle>></div> <div class="recap-popup-actions recap-popup-actions-top"> <<if $recapIndex > 0>> <<link "Previous event">><<set $recapIndex -= 1>><<goto "Main">><</link>> <</if>> <<if $gameOver and _nextRecapIndex >= $recapEvents.length>> <<link "Restart">><<set $gameOver = false>><<set $gameOverReason = "">><<set $victory = false>><<set $victoryCreated = false>><<goto "Start">><</link>> <<elseif _nextRecapIndex lt $recapEvents.length>> <<link "Next event">><<set $recapIndex += 1>><<goto "Main">><</link>> <<elseif $gameOver>> <<link "Restart">><<set $gameOver = false>><<set $gameOverReason = "">><<set $victory = false>><<set $victoryCreated = false>><<goto "Start">><</link>> <<else>> <<link "Close recap">><<set $showRecapPopup = false>><<goto "Main">><</link>> <</if>> </div> <<if _event.intro>><div class="recap-popup-body"><<print _event.intro>></div><</if>> <div class="recap-popup-images single"><div class="recap-popup-shot"><img class="recap-popup-img" @src="_event.image"><span><<print _event.imageLabel>></span></div></div> <<if _event.body>><div class="recap-popup-body"><<print _event.body>></div><</if>> <<if _event.image2>><div class="recap-popup-images single"><div class="recap-popup-shot"><img class="recap-popup-img" @src="_event.image2"><span><<print _event.image2Label>></span></div></div><</if>> <<if _event.finish>><div class="recap-popup-finish"><<print _event.finish>></div><</if>> <div class="recap-popup-stats"> <<for _s range _event.stats>> <div class="recap-popup-stat"><<print _s>></div> <</for>> </div> </div> </div> <</if>> </div> </div> <<script>> setup.AV.drawMap(); setTimeout(function () { setup.AV.drawMap(); }, 50); setTimeout(function () { setup.AV.drawMap(); }, 150); <</script>> <<endnobr>>
:: Intro <<nobr>> <<script>> if (!setup.AVIntro) setup.AVIntro = {}; setup.AVIntro.slug = function (text) { return String(text || "unknown") .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, ""); }; setup.AVIntro.characterFolderName = function (girl) { if (!girl) return "placeholder"; const raw = girl.folder || girl.characterFolder || girl.characterName || girl.id || girl.name || "placeholder"; return setup.AVIntro.slug(raw); }; setup.AVIntro.faceImage = function (girl) { if (!girl) return "data/characters/placeholder.png"; const folder = setup.AVIntro.characterFolderName(girl); return girl.face || girl.faceImage || girl.image || ("data/characters/" + folder + "/face.png"); }; setup.AVIntro.getGirlText = function (girl) { if (!girl) return ""; return String( girl.introLine || girl.introDescription || girl.bio || girl.description || "" ).trim(); }; setup.AVIntro.getGirlRole = function (girl) { if (!girl) return ""; return String( girl.origin || girl.major || girl.role || girl.place || "" ).trim(); }; setup.AVIntro.yesNo = function (value) { return value ? "Yes" : "No"; }; setup.AVIntro.cleanGirlStats = function (girl) { if (!girl) return; if (girl.virgin == null) girl.virgin = true; if (girl.pregnant == null) girl.pregnant = false; if (!girl.pregnantBy) girl.pregnantBy = "None"; if (!girl.firstTime) girl.firstTime = "None"; if (!girl.favoritePerson) girl.favoritePerson = "None"; if (girl.vaginal == null) girl.vaginal = 0; if (girl.anal == null) girl.anal = 0; if (girl.oral == null) girl.oral = 0; }; setup.AVIntro.chooseGirl = function (index) { const v = State.variables; const sourcePool = (v.startingWomenPool && v.startingWomenPool.length > 0) ? v.startingWomenPool : (v.women || []); const girl = sourcePool[index]; if (!girl) return; if (!v.startingWomenPool || v.startingWomenPool.length < 1) { v.startingWomenPool = sourcePool.slice(); } v.savedGirlName = girl.name || "Unknown Girl"; v.savedGirlKey = setup.AVIntro.characterFolderName(girl); v.introDone = true; v.girlsDistributed = false; v.day = 1; v.hour = 7; v.actionsLeft = 5; v.pendingPlayerActions = []; v.dayRecap = []; v.recapEvents = []; v.recapIndex = 0; v.showRecapPopup = false; v.gameOver = false; v.gameOverReason = ""; v.victory = false; v.victoryCreated = false; v.tab = "base"; }; setup.AVIntro.resetIntro = function () { State.variables.introStep = 0; }; <</script>> <<if $introStep is undefined>> <<set $introStep = 0>> <</if>> <<if $women is undefined>> <<set $women = []>> <</if>> <<if $startingWomenPool is undefined>> <<set $startingWomenPool = []>> <</if>> <<set $playerName = "the foreign student">> <<set $introCountry = "Kavanda">> <<set $universityName = "N'Dara National University">> <<set $prophecyTitle = "The Cinder King">> <<set $prophecyLine = "When the old flags burn, the stranger from the lecture halls will choose one life from the fire, then rule what remains.">> <style> #ui-bar,#ui-bar-toggle{display:none!important} html,body{margin:0!important;padding:0!important;overflow:hidden!important;background:#020303!important} #story{margin:0!important;width:100vw!important;max-width:none!important} #passages{margin:0!important;padding:0!important;width:100vw!important;max-width:none!important} .passage{margin:0!important;padding:0!important} .intro-screen{position:fixed;inset:0;width:100vw;height:100vh;background:#020303;color:#e9efe4;font-family:Inter,Arial,Helvetica,sans-serif;overflow:hidden} .intro-screen::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.97),rgba(0,0,0,.78) 45%,rgba(0,0,0,.42) 72%,rgba(0,0,0,.78)),radial-gradient(circle at 76% 28%,rgba(255,91,32,.18),transparent 28%),radial-gradient(circle at 34% 82%,rgba(223,255,93,.08),transparent 28%);z-index:1;pointer-events:none} .intro-screen::after{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(223,255,93,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(223,255,93,.025) 1px,transparent 1px),linear-gradient(rgba(255,255,255,.020),rgba(255,255,255,0) 2px);background-size:74px 74px,74px 74px,100% 3px;opacity:.26;z-index:2;pointer-events:none} .intro-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;filter:contrast(1.08) saturate(.88) brightness(.58);z-index:0} .intro-wrap{position:relative;z-index:5;height:100vh;display:grid;grid-template-columns:minmax(390px,650px) 1fr;gap:24px;padding:32px;box-sizing:border-box} .intro-panel{align-self:center;border:1px solid rgba(223,255,93,.24);background:rgba(4,7,6,.95);box-shadow:0 24px 70px rgba(0,0,0,.68);padding:24px;max-height:calc(100vh - 64px);overflow:auto} .intro-kicker{color:#dfff5d;font-size:.62rem;font-weight:950;letter-spacing:.18em;text-transform:uppercase;margin-bottom:10px} .intro-title{font-size:2.55rem;line-height:.88;margin:0 0 14px;font-weight:950;letter-spacing:-.07em;text-transform:uppercase;color:#fff} .intro-title span{color:#ff5b20} .intro-text{color:rgba(232,237,222,.78);font-size:.93rem;line-height:1.70;margin:14px 0} .intro-text strong{color:#fff} .intro-quote{border-left:4px solid #ff5b20;background:rgba(255,91,32,.07);padding:13px 14px;margin:18px 0;color:#fff;font-size:.94rem;line-height:1.58} .intro-warning{border-left:4px solid #dfff5d;background:rgba(223,255,93,.055);padding:13px 14px;margin:18px 0;color:rgba(232,237,222,.86);font-size:.86rem;line-height:1.55} .intro-actions{display:grid;gap:10px;margin-top:18px} .intro-actions a,.intro-actions .link-internal{display:block;padding:13px 15px;border:1px solid rgba(223,255,93,.28);background:linear-gradient(90deg,rgba(223,255,93,.12),rgba(255,91,32,.08));color:#e9efe4!important;text-decoration:none!important;text-transform:uppercase;font-size:.72rem;font-weight:950;letter-spacing:.14em} .intro-actions a:hover,.intro-actions .link-internal:hover{border-color:rgba(223,255,93,.72);background:rgba(223,255,93,.16)} .intro-side{align-self:end;justify-self:end;max-width:520px;border:1px solid rgba(255,91,32,.22);background:rgba(0,0,0,.44);padding:18px;color:rgba(232,237,222,.70);font-size:.78rem;line-height:1.58} .intro-side b{color:#dfff5d;text-transform:uppercase;letter-spacing:.10em;font-size:.66rem} .girl-choice-grid{display:grid;grid-template-columns:1fr;gap:16px;margin-top:16px} .girl-choice{display:grid;grid-template-columns:220px 1fr;gap:14px;border:1px solid rgba(232,237,222,.16);background:linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,.025));padding:12px;min-height:250px} .girl-choice-img{position:relative;height:250px;border:1px solid rgba(232,237,222,.14);background:#050706;overflow:hidden;display:flex;align-items:center;justify-content:center} .girl-choice-img img{max-width:100%;max-height:100%;width:100%;height:100%;object-fit:contain;object-position:center center;display:block;background:#050706} .girl-choice-img span{position:absolute;left:8px;top:8px;padding:4px 7px;background:rgba(0,0,0,.75);border:1px solid rgba(223,255,93,.22);color:#dfff5d;font-size:.49rem;font-weight:950;letter-spacing:.13em;text-transform:uppercase} .girl-choice-info{display:flex;flex-direction:column;min-width:0} .girl-choice-name{font-size:1.12rem;font-weight:950;letter-spacing:-.03em;text-transform:uppercase;color:#fff;margin-bottom:5px} .girl-choice-origin{color:#ff5b20;font-size:.58rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase;margin-bottom:10px} .girl-choice-text{color:rgba(232,237,222,.73);font-size:.82rem;line-height:1.55;margin-bottom:12px} .girl-choice-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:auto;margin-bottom:10px} .girl-choice-stat{border:1px solid rgba(232,237,222,.12);background:rgba(0,0,0,.22);padding:7px 8px} .girl-choice-stat label{display:block;color:rgba(232,237,222,.45);font-size:.48rem;font-weight:950;letter-spacing:.13em;text-transform:uppercase;margin-bottom:3px} .girl-choice-stat b{color:#e9efe4;font-size:.68rem;text-transform:uppercase} .girl-choice a,.girl-choice .link-internal{display:block;text-align:center;padding:10px;border:1px solid rgba(255,91,32,.36);background:rgba(255,91,32,.10);color:#fff!important;text-decoration:none!important;font-size:.62rem;font-weight:950;letter-spacing:.12em;text-transform:uppercase} .girl-choice a:hover,.girl-choice .link-internal:hover{border-color:rgba(255,91,32,.72);background:rgba(255,91,32,.18)} @media(max-width:980px){ .intro-wrap{grid-template-columns:1fr;padding:16px} .intro-panel{max-height:calc(100vh - 32px)} .intro-side{display:none} } @media(max-width:720px){ .girl-choice{grid-template-columns:1fr} .girl-choice-img{height:320px} .girl-choice-stats{grid-template-columns:1fr 1fr} } </style> <div class="intro-screen"> <img class="intro-bg" src="data/misc/MAP_NIGHT.png"> <div class="intro-wrap"> <div class="intro-panel"> <<if $introStep is 0>> <div class="intro-kicker">Prologue / locked story</div> <h1 class="intro-title">Before <span>Cinder</span></h1> <div class="intro-text"> Before the Hold, before the war map, before anyone called him king, he was only <strong>a foreign student</strong> in the fictional African state of <strong>Kavanda</strong>. </div> <div class="intro-text"> He came to <strong>N'Dara National University</strong> for an exchange semester: politics, anthropology, dead languages, and the kind of campus life that makes danger feel far away because everyone is young enough to think history is already finished. </div> <div class="intro-text"> Kavanda was not peaceful. Everyone knew that. Soldiers stood at fuel stations. Convoys crossed the capital at night. Old separatist flags appeared on walls and vanished before morning. But the university still opened its gates every day, and that made the illusion strong enough to live inside. </div> <div class="intro-actions"> <<link "Continue">> <<set $introStep = 1>> <<goto "Intro">> <</link>> </div> <</if>> <<if $introStep is 1>> <div class="intro-kicker">The archive below the campus</div> <h1 class="intro-title">The buried <span>prophecy</span></h1> <div class="intro-text"> The discovery happened under the anthropology building, in a sealed archive that officially did not exist. </div> <div class="intro-text"> A professor had brought him there after midnight, not because he trusted him, but because the old tablet reacted to him. The symbols along the stone warmed under his hand. The power failed across the campus. Somewhere above them, dogs began screaming at the dark. </div> <div class="intro-quote"> “$prophecyLine” </div> <div class="intro-text"> The professor called it a royal omen. A survival curse. A succession text from a dead kingdom that had predicted the end of states, the fall of cities, and a foreign student choosing one life from the first fire. </div> <div class="intro-text"> He laughed because prophecy sounded ridiculous. Then every phone in the archive received the same emergency alert. </div> <div class="intro-actions"> <<link "The sirens start">> <<set $introStep = 2>> <<goto "Intro">> <</link>> </div> <</if>> <<if $introStep is 2>> <div class="intro-kicker">The first attack</div> <h1 class="intro-title">The city <span>breaks</span></h1> <div class="intro-text"> The apocalypse did not arrive as a meteor, a plague, or one clean explosion. It arrived as a coordinated attack. </div> <div class="intro-text"> The northern military district fired on the presidential convoy. The airport burned. A rebel column crossed the student quarter. Government drones answered too late and hit the wrong streets. By sunrise, every faction claimed it was restoring order, and every faction was killing civilians to prove it. </div> <div class="intro-text"> N'Dara National University became a trap. The dormitories locked down. The clinic filled with smoke. The library became a shelter until armed men opened the doors from outside. </div> <div class="intro-warning"> He found the evacuation tunnel beneath the old lecture hall. It was narrow, flooding, and already collapsing. There was time to pull only one adult survivor through before the corridor sealed behind him. </div> <div class="intro-actions"> <<link "Choose who survives with you">> <<set $introStep = 3>> <<goto "Intro">> <</link>> </div> <</if>> <<if $introStep is 3>> <div class="intro-kicker">The first survivor</div> <h1 class="intro-title">Save <span>one</span></h1> <<set _pool = []>> <<if $startingWomenPool and $startingWomenPool.length > 0>> <<set _pool = $startingWomenPool>> <<else>> <<set _pool = $women>> <</if>> <div class="intro-text"> Only one reaches the tunnel with him. She becomes the first person inside Cinder's future. The others are scattered into the ruined world, enemy cities, roads, rumors, and locked rooms. </div> <<if !_pool or _pool.length < 1>> <div class="intro-warning"> No girls found in <code>$women</code> or <code>$startingWomenPool</code>. Add your girl list before this passage. </div> <div class="intro-actions"> <<link "Return to Start">> <<goto "Start">> <</link>> </div> <<else>> <div class="girl-choice-grid"> <<for _i = 0; _i lt _pool.length; _i++>> <<set _girl = _pool[_i]>> <<run setup.AVIntro.cleanGirlStats(_girl)>> <<set _girlText = setup.AVIntro.getGirlText(_girl)>> <<set _girlRole = setup.AVIntro.getGirlRole(_girl)>> <div class="girl-choice"> <div class="girl-choice-img"> <img @src="setup.AVIntro.faceImage(_girl)"> <span>Face</span> </div> <div class="girl-choice-info"> <div class="girl-choice-name"><<print _girl.name>></div> <<if _girlRole>> <div class="girl-choice-origin"><<print _girlRole>></div> <</if>> <<if _girlText>> <div class="girl-choice-text"><<print _girlText>></div> <</if>> <div class="girl-choice-stats"> <div class="girl-choice-stat"> <label>Virgin</label> <b><<print setup.AVIntro.yesNo(_girl.virgin)>></b> </div> <div class="girl-choice-stat"> <label>Pregnant</label> <b><<print setup.AVIntro.yesNo(_girl.pregnant)>></b> </div> <div class="girl-choice-stat"> <label>First time</label> <b><<print _girl.firstTime>></b> </div> <div class="girl-choice-stat"> <label>Vaginal</label> <b><<print _girl.vaginal>></b> </div> <div class="girl-choice-stat"> <label>Anal</label> <b><<print _girl.anal>></b> </div> <div class="girl-choice-stat"> <label>Oral</label> <b><<print _girl.oral>></b> </div> </div> <<capture _i>> <<link "Save her">> <<run setup.AVIntro.chooseGirl(_i)>> <<goto "Main">> <</link>> <</capture>> </div> </div> <</for>> </div> <</if>> <</if>> </div> <div class="intro-side"> <b>$prophecyTitle</b><br> Locked story: a foreign student, Kavanda, N'Dara National University, a buried prophecy, a coordinated attack, one adult survivor saved, then Cinder begins. </div> </div> </div> <<endnobr>>