Created
July 4, 2026 22:20
-
-
Save atondwal/ebf143d77c8cf9b3050a2c726b0721dd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <title>Crowns of Eldervale</title> | |
| <style> | |
| :root{ | |
| --bg:#12161f; --sea:#0d1118; --panel:#1b212d; --panel2:#232a38; | |
| --ink:#e8ddc3; --dim:#9a917a; --gold:#c9a24b; --bad:#b0563c; --good:#5e9c78; | |
| --line:#333c4d; | |
| } | |
| *{box-sizing:border-box;margin:0} | |
| html,body{height:100%} | |
| body{ | |
| background:var(--bg); color:var(--ink); | |
| font-family:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif; | |
| font-size:15px; | |
| } | |
| #app{display:flex;flex-direction:column;height:100vh} | |
| /* ---- top bar ---- */ | |
| #top{display:flex;align-items:center;gap:20px;padding:10px 16px;background:var(--panel);border-bottom:1px solid var(--line);flex-wrap:wrap} | |
| #crest{font-variant:small-caps;letter-spacing:3px;color:var(--gold);font-weight:700;font-size:17px} | |
| .stat{display:flex;flex-direction:column;line-height:1.2} | |
| .stat small{color:var(--dim);font-variant:small-caps;letter-spacing:1px;font-size:11px} | |
| .stat b{font-variant-numeric:tabular-nums;font-size:15px} | |
| #endturn{margin-left:auto;background:var(--gold);color:#1a1408;border:0;padding:9px 20px;font:inherit;font-weight:700;cursor:pointer;border-radius:3px;letter-spacing:.5px} | |
| #endturn:hover{filter:brightness(1.1)} | |
| #restart{background:none;border:1px solid var(--line);color:var(--dim);padding:8px 12px;font:inherit;cursor:pointer;border-radius:3px} | |
| #restart:hover{border-color:var(--gold);color:var(--gold)} | |
| /* ---- main layout ---- */ | |
| main{flex:1;display:flex;min-height:0} | |
| #left{flex:1.5;display:flex;flex-direction:column;min-width:0} | |
| #mapwrap{flex:1;background:var(--sea);min-height:0;display:flex;padding:6px} | |
| svg{width:100%;height:100%} | |
| #log{height:150px;overflow-y:auto;padding:8px 14px;border-top:1px solid var(--line);font-size:13px;line-height:1.6;color:var(--dim)} | |
| #log b{color:var(--gold);font-weight:400;font-variant:small-caps} | |
| #panel{width:370px;border-left:1px solid var(--line);background:var(--panel);overflow-y:auto;padding:16px} | |
| /* ---- map ---- */ | |
| polygon{stroke:var(--sea);stroke-width:2.5;cursor:pointer;opacity:.9} | |
| polygon:hover{opacity:1} | |
| polygon.sel{stroke:var(--gold);stroke-width:3;opacity:1} | |
| polygon.dead{opacity:.5} | |
| .mlabel{fill:#f4edd8;font-size:15px;text-anchor:middle;pointer-events:none;paint-order:stroke;stroke:rgba(0,0,0,.55);stroke-width:3px;font-family:inherit} | |
| .mpow{font-size:11.5px;opacity:.85} | |
| text.dead{opacity:.5} | |
| /* ---- panel ---- */ | |
| #panel h3{font-variant:small-caps;letter-spacing:2px;color:var(--gold);font-weight:400;margin-bottom:10px;text-wrap:balance} | |
| #panel h4{font-variant:small-caps;letter-spacing:1.5px;color:var(--gold);font-weight:400;margin:14px 0 6px} | |
| .ccard{background:var(--panel2);border:1px solid var(--line);padding:12px;border-radius:3px} | |
| .cname{font-size:17px} | |
| .csub{color:var(--dim);font-size:13px;margin-top:3px} | |
| .stats{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;font-size:13px;font-variant-numeric:tabular-nums} | |
| .chips{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px} | |
| .chip{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:2px 9px;font-size:12px;color:var(--dim);font-variant:small-caps;letter-spacing:.5px} | |
| .chip.rel{color:var(--ink);border-color:var(--gold)} | |
| .opwrap{margin:12px 0 4px} | |
| .oplabel{font-size:13px;color:var(--dim);margin-bottom:4px} | |
| .oplabel b{font-variant-numeric:tabular-nums} | |
| .pos{color:var(--good)} .neg{color:var(--bad)} | |
| .opbar{height:7px;background:var(--bg);border:1px solid var(--line);border-radius:4px;overflow:hidden} | |
| .opfill{height:100%} | |
| .opfill.pos{background:var(--good)} .opfill.neg{background:var(--bad)} | |
| #actions{display:flex;flex-direction:column;gap:6px} | |
| .abtn{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;text-align:left;background:var(--panel2);border:1px solid var(--line);color:var(--ink);padding:8px 11px;font:inherit;font-size:14px;cursor:pointer;border-radius:3px} | |
| .abtn small{color:var(--dim);font-size:11.5px;line-height:1.35} | |
| .abtn:hover:not(:disabled){border-color:var(--gold)} | |
| .abtn:disabled{opacity:.45;cursor:default} | |
| .objbox{background:var(--panel2);border-left:2px solid var(--gold);padding:9px 12px;margin:8px 0;font-size:13.5px;line-height:1.5} | |
| .objbox.war{border-left-color:var(--bad)} | |
| .objbox p{margin:2px 0} | |
| .hint{color:var(--dim);font-style:italic;font-size:13px;margin-top:12px;line-height:1.5} | |
| /* ---- modal ---- */ | |
| #modal{position:fixed;inset:0;background:rgba(5,7,12,.75);display:flex;align-items:center;justify-content:center;z-index:9;padding:16px} | |
| #mbox{background:var(--panel);border:1px solid var(--gold);max-width:680px;width:100%;max-height:88vh;overflow-y:auto;padding:24px;border-radius:4px} | |
| #mtitle{font-variant:small-caps;letter-spacing:2px;color:var(--gold);font-weight:400;margin-bottom:10px;text-wrap:balance} | |
| .mint{line-height:1.6;margin-bottom:8px} | |
| #mbtns{display:flex;flex-direction:column;gap:8px;margin-top:14px} | |
| #mbtns button{background:var(--panel2);border:1px solid var(--line);color:var(--ink);padding:10px 14px;font:inherit;text-align:left;cursor:pointer;border-radius:3px} | |
| #mbtns button:hover:not(:disabled){border-color:var(--gold)} | |
| #mbtns button:disabled{opacity:.45;cursor:default} | |
| #pickgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px} | |
| .pickcard{background:var(--panel2);border:1px solid var(--line);padding:11px;cursor:pointer;border-radius:3px;display:flex;flex-direction:column;gap:3px} | |
| .pickcard:hover{border-color:var(--gold)} | |
| .pickcard small{color:var(--dim);font-size:12px} | |
| .pickcard .traits{font-variant:small-caps;letter-spacing:.5px} | |
| .dot{width:11px;height:11px;border-radius:50%;display:inline-block;margin-bottom:2px} | |
| .hidden{display:none!important} | |
| button:focus-visible,.pickcard:focus-visible{outline:2px solid var(--gold);outline-offset:1px} | |
| @media (prefers-reduced-motion:no-preference){polygon{transition:opacity .15s,stroke .15s}} | |
| @media (max-width:900px){ | |
| main{flex-direction:column} | |
| #panel{width:100%;border-left:0;border-top:1px solid var(--line)} | |
| #mapwrap{min-height:340px} | |
| #pickgrid{grid-template-columns:1fr} | |
| } | |
| </style> | |
| <div id="app"> | |
| <header id="top"> | |
| <div id="crest">β¦ Crowns of Eldervale</div> | |
| <div class="stat"><small>Season</small><b id="date">β</b></div> | |
| <div class="stat"><small>Gold</small><b id="gold">β</b></div> | |
| <div class="stat"><small>Prestige</small><b id="prest">β</b></div> | |
| <div class="stat"><small>Realm power</small><b id="power">β</b></div> | |
| <div class="stat"><small>Ruler</small><b id="myrealm">β</b></div> | |
| <button id="endturn">End Season βΈ</button> | |
| <button id="restart" title="Begin a new saga">βΊ New saga</button> | |
| </header> | |
| <main> | |
| <div id="left"> | |
| <div id="mapwrap"><svg id="map" viewBox="0 0 800 560" role="img" aria-label="Map of Eldervale"></svg></div> | |
| <div id="log"></div> | |
| </div> | |
| <aside id="panel"><p class="hint">Select a realm on the map to open its court.</p></aside> | |
| </main> | |
| </div> | |
| <div id="modal" class="hidden"><div id="mbox"><h2 id="mtitle"></h2><div id="mbody"></div><div id="mbtns"></div></div></div> | |
| <script> | |
| 'use strict'; | |
| /* ============ helpers ============ */ | |
| const $ = id => document.getElementById(id); | |
| const rnd = (a,b) => a + Math.floor(Math.random()*(b-a+1)); | |
| const pick = arr => arr[Math.floor(Math.random()*arr.length)]; | |
| const chance = p => Math.random() < p; | |
| const clamp = (v,a,b) => Math.max(a, Math.min(b, v)); | |
| const pairKey = (a,b) => a < b ? a+'|'+b : b+'|'+a; | |
| /* ============ static data ============ */ | |
| const SEASONS = ['Spring','Summer','Autumn','Winter']; | |
| const RACES = {Human:{life:70}, Elf:{life:300}, Dwarf:{life:130}, Orc:{life:55}, Dragonkin:{life:160}, Fae:{life:220}}; | |
| const TITLES = {Human:['King','Queen'], Elf:['Highlord','Highlady'], Dwarf:['Thane','Thane'], | |
| Orc:['Warchief','Warchief'], Dragonkin:['Ascendant','Ascendant'], Fae:['Sovereign','Sovereign']}; | |
| const REL = { | |
| 'Human|Human':10,'Elf|Human':0,'Dwarf|Human':5,'Human|Orc':-25,'Dragonkin|Human':-10,'Fae|Human':-5, | |
| 'Elf|Elf':15,'Dwarf|Elf':-20,'Elf|Orc':-35,'Dragonkin|Elf':0,'Elf|Fae':15, | |
| 'Dwarf|Dwarf':15,'Dwarf|Orc':-30,'Dragonkin|Dwarf':-15,'Dwarf|Fae':-10, | |
| 'Orc|Orc':10,'Dragonkin|Orc':5,'Fae|Orc':-15, | |
| 'Dragonkin|Dragonkin':10,'Dragonkin|Fae':0,'Fae|Fae':10 | |
| }; | |
| const TRAITS = { | |
| brave:{opp:'craven',mod:{m:3}}, craven:{opp:'brave',mod:{i:1,m:-2}}, | |
| ambitious:{opp:'content',mod:{d:1,i:1,m:1}}, content:{opp:'ambitious',mod:{d:1}}, | |
| honest:{opp:'deceitful',mod:{d:2}}, deceitful:{opp:'honest',mod:{i:3}}, | |
| kind:{opp:'cruel',mod:{d:2}}, cruel:{opp:'kind',mod:{i:1,m:1}}, | |
| gregarious:{opp:'shy',mod:{d:3}}, shy:{opp:'gregarious',mod:{i:1,d:-1}}, | |
| wrathful:{opp:'calm',mod:{m:2,d:-1}}, calm:{opp:'wrathful',mod:{d:2}}, | |
| greedy:{opp:'generous',mod:{i:1}}, generous:{opp:'greedy',mod:{d:2}}, | |
| }; | |
| const NAMES = { | |
| Human:{m:['Aldric','Bram','Cedric','Doran','Edmund','Gareth','Hale','Osric','Roland','Theo'], | |
| f:['Adela','Brienne','Clara','Elin','Gwen','Isolde','Maren','Rosalind','Sable','Ysra'], | |
| dyn:['Blackwood','Harrow','Valemont','Ashford','Crane']}, | |
| Elf:{m:['Aerendil','Caladrel','Faelar','Ilthuryn','Sylvaris','Thandriel'], | |
| f:['Aelene','Ithilwen','Lyriel','Naivara','Sariel','Vaelis'], | |
| dyn:['Moonshadow','Dawnwhisper','Silverbough','Starweaver']}, | |
| Dwarf:{m:['Borin','Dagur','Grimnir','Keldan','Thorvald','Ulfgar'], | |
| f:['Astrid','Brunhild','Dagny','Hertha','Sigrun','Torva'], | |
| dyn:['Ironvein','Stonehelm','Deepdelver','Goldbeard']}, | |
| Orc:{m:['Drakh','Ghorbash','Karguk','Morzul','Ragash','Urzoth'], | |
| f:['Bashuk','Ghra','Lazgar','Shel','Urzul','Vola'], | |
| dyn:['Bloodfang','Skullrend','Ashmaw','Ironhide']}, | |
| Dragonkin:{m:['Arjhan','Balasar','Kriv','Medrash','Rhogar','Torinn'], | |
| f:['Akra','Farideh','Jheri','Kava','Sora','Thava'], | |
| dyn:['Emberscale','Cindwyrm','Goldcrest','Stormtalon']}, | |
| Fae:{m:['Corin','Finvarra','Oberic','Puck','Rowan','Thistle'], | |
| f:['Aine','Fionna','Maeve','Nimue','Saoirse','Titania'], | |
| dyn:['Gloamthorn','Dewlight','Nightbell','Willowisp']}, | |
| }; | |
| const REALM_DEFS = [ | |
| {name:'Frostgard', race:'Human', color:'#6f96b8', lx:140, ly:95, pts:'0,0 300,0 285,70 305,130 290,190 140,178 0,192'}, | |
| {name:'Silverwood', race:'Elf', color:'#5e9c66', lx:425, ly:95, pts:'300,0 560,0 545,80 565,140 550,190 400,203 290,190 305,130 285,70'}, | |
| {name:'KragdΓ»n', race:'Dwarf', color:'#a3874f', lx:680, ly:90, pts:'560,0 800,0 800,198 660,190 550,190 565,140 545,80'}, | |
| {name:'Tidehold', race:'Human', color:'#4f9ea6', lx:100, ly:285, pts:'0,192 140,178 210,190 196,270 214,330 200,385 92,372 0,382'}, | |
| {name:'Aldermark', race:'Human', color:'#b3812e', lx:320, ly:290, pts:'210,190 290,190 400,203 430,196 444,270 426,330 440,390 300,382 200,385 214,330 196,270'}, | |
| {name:'Duskmere', race:'Elf', color:'#7b6aa6', lx:540, ly:288, pts:'430,196 550,190 660,190 632,270 654,340 636,390 520,382 440,390 426,330 444,270'}, | |
| {name:'Emberpeak', race:'Dragonkin', color:'#b0563c', lx:720, ly:292, pts:'660,190 800,198 800,390 636,390 654,340 632,270'}, | |
| {name:'Thornvale', race:'Fae', color:'#8a9a4a', lx:140, ly:470, pts:'0,382 92,372 200,385 300,382 282,470 296,560 0,560'}, | |
| {name:'Sunspire', race:'Human', color:'#c2a24b', lx:410, ly:472, pts:'300,382 440,390 520,382 542,470 526,560 296,560 282,470'}, | |
| {name:'Ashfall', race:'Orc', color:'#7d6d63', lx:665, ly:475, pts:'520,382 636,390 800,390 800,560 526,560 542,470'}, | |
| ]; | |
| /* ============ state ============ */ | |
| let S = null; // pure data β everything saved to localStorage | |
| const els = {}; // realm id -> {poly, name, pow} SVG nodes | |
| const title = c => TITLES[c.race][c.g === 'm' ? 0 : 1]; | |
| const alive = () => S.realms.filter(r => r.alive); | |
| const others = id => S.realms.filter(r => r.alive && r.id !== id).map(r => r.id); | |
| const allied = (a,b) => !!S.allies[pairKey(a,b)]; | |
| const tied = (a,b) => !!S.ties[pairKey(a,b)]; | |
| const coun = (rid, role) => S.realms[rid].court.find(c => c.role === role); | |
| const kin = rid => S.realms[rid].court.filter(c => c.role === 'kin'); | |
| const unwedKin = rid => kin(rid).filter(c => !c.married); | |
| const eldest = cs => cs.reduce((a,b) => b.age > a.age ? b : a); | |
| const dipl = rid => Math.round((S.realms[rid].ruler.d + coun(rid,'chancellor').d) / 2); // the court speaking | |
| const intr = rid => Math.round((S.realms[rid].ruler.i + coun(rid,'spymaster').i) / 2); // the court scheming | |
| const warBetween = (a,b) => S.wars.find(w => (w.a===a && w.d===b) || (w.a===b && w.d===a)); | |
| const atWarAny = id => S.wars.some(w => w.a===id || w.d===id); | |
| const rn = R => S.realms[R].ruler.name + ' of ' + S.realms[R].name; | |
| const other2 = R => { const p = others(S.p).filter(x => x !== R); return p.length ? pick(p) : R; }; | |
| const totalPower = () => alive().reduce((s,r) => s + r.power, 0); | |
| function opAdd(who, about, amt){ | |
| if (S.op[who] && S.op[who][about] !== undefined) | |
| S.op[who][about] = clamp(S.op[who][about] + amt, -100, 100); | |
| } | |
| function newChar(race, dyn){ | |
| const g = chance(0.5) ? 'm' : 'f'; | |
| const life = RACES[race].life; | |
| const keys = Object.keys(TRAITS); | |
| const traits = []; | |
| while (traits.length < 3){ | |
| const t = pick(keys); | |
| if (!traits.includes(t) && !traits.includes(TRAITS[t].opp)) traits.push(t); | |
| } | |
| const c = {cid:S.cid++, name:pick(NAMES[race][g]), dyn:dyn || pick(NAMES[race].dyn), race, g, | |
| age:rnd(Math.round(life*0.3), Math.round(life*0.75)), | |
| d:rnd(4,14), i:rnd(4,14), m:rnd(4,14), | |
| traits, married:chance(0.6)}; | |
| for (const t of traits){ | |
| const mod = TRAITS[t].mod; | |
| c.d = clamp(c.d + (mod.d||0), 1, 20); | |
| c.i = clamp(c.i + (mod.i||0), 1, 20); | |
| c.m = clamp(c.m + (mod.m||0), 1, 20); | |
| } | |
| return c; | |
| } | |
| function newCourtier(race, role, dyn){ | |
| const c = newChar(race, dyn), life = RACES[race].life; | |
| c.role = role; // 'chancellor'|'spymaster'|'marshal'|'kin' | |
| c.age = role === 'kin' ? rnd(16, Math.round(life*0.35)) | |
| : rnd(Math.round(life*0.25), Math.round(life*0.6)); | |
| if (role === 'kin') c.married = false; // kin are the marriage pool | |
| return c; | |
| } | |
| function newCourt(r){ | |
| const court = ['chancellor','spymaster','marshal'].map(role => newCourtier(r.race, role, null)); | |
| for (let n = rnd(1,2); n > 0; n--) court.push(newCourtier(r.race, 'kin', r.ruler.dyn)); | |
| return court; | |
| } | |
| function baseOpinion(a, b){ | |
| const A = S.realms[a].ruler, B = S.realms[b].ruler; | |
| let v = REL[pairKey(A.race, B.race)] || 0; | |
| for (const ta of A.traits) for (const tb of B.traits){ | |
| if (ta === tb) v += 8; | |
| else if (TRAITS[ta].opp === tb) v -= 12; | |
| } | |
| if (B.traits.includes('kind')) v += 5; | |
| if (B.traits.includes('cruel')) v -= 5; | |
| return clamp(v + rnd(-12,12), -70, 70); | |
| } | |
| function newGame(){ | |
| S = {v:2, cid:1, realms:REALM_DEFS.map((d,i) => ({id:i, name:d.name, race:d.race, color:d.color, | |
| alive:true, absorbedBy:null, power:rnd(60,100), ruler:null, court:[]})), | |
| base:{}, op:{}, allies:{}, ties:{}, wars:[], sway:[], cds:{}, | |
| season:0, year:1123, p:null, gold:120, prestige:0, sel:null, over:false, log:[]}; | |
| for (const r of S.realms){ r.ruler = newChar(r.race); r.court = newCourt(r); } | |
| for (let a = 0; a < S.realms.length; a++){ | |
| S.base[a] = {}; S.op[a] = {}; | |
| for (let b = 0; b < S.realms.length; b++) if (a !== b){ | |
| S.base[a][b] = baseOpinion(a,b); | |
| S.op[a][b] = S.base[a][b]; | |
| } | |
| } | |
| } | |
| function save(){ try{ localStorage.setItem('eldervale', JSON.stringify(S)); }catch(e){} } | |
| function load(){ | |
| try{ | |
| const s = localStorage.getItem('eldervale'); | |
| if (s){ const d = JSON.parse(s); if (d && d.v === 2){ S = d; return true; } } | |
| }catch(e){} | |
| return false; | |
| } | |
| /* ============ log & modal ============ */ | |
| function addLog(txt){ | |
| S.log.unshift(`<b>${SEASONS[S.season]} ${S.year}</b> β ${txt}`); | |
| if (S.log.length > 150) S.log.pop(); | |
| $('log').innerHTML = S.log.map(l => `<div>${l}</div>`).join(''); | |
| } | |
| function showModal(t, bodyHTML, btns){ | |
| $('mtitle').textContent = t; | |
| $('mbody').innerHTML = bodyHTML; | |
| const bb = $('mbtns'); bb.innerHTML = ''; | |
| for (const b of btns){ | |
| const el = document.createElement('button'); | |
| el.textContent = b.label; | |
| el.disabled = !!b.disabled; | |
| el.onclick = () => { hideModal(); if (b.fn) b.fn(); }; | |
| bb.appendChild(el); | |
| } | |
| $('modal').classList.remove('hidden'); | |
| } | |
| function hideModal(){ $('modal').classList.add('hidden'); } | |
| /* ============ wars ============ */ | |
| function sidePower(id, enemy){ | |
| let p = S.realms[id].power; | |
| for (const b of others(id)) | |
| if (allied(id,b) && !allied(b,enemy)) p += S.realms[b].power * 0.5; | |
| return p; | |
| } | |
| function declareWar(a, d){ | |
| S.wars.push({a, d, score:0}); | |
| delete S.allies[pairKey(a,d)]; | |
| opAdd(d, a, -40); | |
| S.base[d][a] = clamp((S.base[d][a]||0) - 25, -70, 70); | |
| if (a === S.p) S.prestige -= 3; | |
| addLog(`β ${S.realms[a].name} declares war on ${S.realms[d].name}!`); | |
| } | |
| function transfer(from, to, frac){ | |
| const F = S.realms[from], T = S.realms[to]; | |
| const amt = Math.round(F.power * frac); | |
| F.power -= amt; T.power += amt; | |
| if (F.power < 18) annex(from, to); | |
| } | |
| function severTies(cid, name){ | |
| for (const k in S.ties){ | |
| const tie = S.ties[k]; | |
| if (tie.ac !== cid && tie.bc !== cid) continue; | |
| delete S.ties[k]; | |
| const [a, b] = k.split('|').map(Number); | |
| opAdd(a, b, -5); opAdd(b, a, -5); | |
| addLog(`π The pact binding ${S.realms[a].name} and ${S.realms[b].name} lapses with the death of ${name}.`); | |
| } | |
| } | |
| function annex(from, to){ | |
| const F = S.realms[from], T = S.realms[to]; | |
| T.power += F.power; F.power = 0; | |
| F.alive = false; F.absorbedBy = to; | |
| S.wars = S.wars.filter(w => w.a !== from && w.d !== from); | |
| S.sway = S.sway.filter(s => s.t !== from); | |
| for (const k in S.ties) if (k.split('|').map(Number).includes(from)) delete S.ties[k]; | |
| if (S.sel === from) S.sel = null; | |
| addLog(`β ${F.name} is no more β annexed by ${T.name}.`); | |
| if (from === S.p) endGame(false, `Your realm has been conquered by ${T.name}. The chronicle closes.`); | |
| } | |
| function endWar(w, win, lose){ | |
| S.wars = S.wars.filter(x => x !== w); | |
| addLog(`β ${S.realms[win].name} triumphs over ${S.realms[lose].name}.`); | |
| opAdd(lose, win, -30); | |
| if (win === S.p) S.prestige += 15; | |
| if (lose === S.p) S.prestige -= 10; | |
| transfer(lose, win, 0.33); | |
| } | |
| function tickWars(){ | |
| for (const w of [...S.wars]){ | |
| const pa = sidePower(w.a, w.d), pd = sidePower(w.d, w.a); | |
| w.score = clamp(w.score + (pa - pd)/12 + rnd(-7,7), -110, 110); | |
| if (w.score >= 100) endWar(w, w.a, w.d); | |
| else if (w.score <= -100) endWar(w, w.d, w.a); | |
| } | |
| } | |
| /* ============ succession & aging ============ */ | |
| function succession(rid, cause){ | |
| const r = S.realms[rid]; | |
| const old = r.ruler; | |
| if (rid === S.p && !old.heir){ | |
| addLog(`${title(old)} ${old.name} ${cause}.`); | |
| endGame(false, `${title(old)} ${old.name} ${cause} with no heir. House ${old.dyn} is ended, and the chronicle with it.`); | |
| return; | |
| } | |
| const c = newChar(r.race, old.heir ? old.dyn : null); | |
| c.age = rnd(16, Math.round(RACES[r.race].life * 0.3)); | |
| c.heir = false; | |
| r.ruler = c; | |
| for (const b of others(rid)){ | |
| S.base[rid][b] = baseOpinion(rid,b); S.op[rid][b] = S.base[rid][b]; | |
| S.base[b][rid] = baseOpinion(b,rid); S.op[b][rid] = S.base[b][rid]; | |
| } | |
| if (rid === S.p) | |
| addLog(`${old.name} ${cause}. You now rule as ${title(c)} ${c.name} ${c.dyn}. Old friendships must be won anew.`); | |
| else | |
| addLog(`${old.name} of ${r.name} ${cause}. ${old.heir ? c.name + ' ' + c.dyn + ' inherits the throne.' : 'A new dynasty, House ' + c.dyn + ', seizes the throne.'}`); | |
| } | |
| function yearly(){ | |
| for (const r of [...alive()]){ | |
| const c = r.ruler, life = RACES[c.race].life; | |
| c.age++; | |
| let dp = 0.004; | |
| if (c.age > life * 0.7) dp += Math.pow((c.age - life*0.7)/(life*0.35), 2) * 0.5; | |
| if (chance(dp)){ succession(r.id, 'has died'); continue; } | |
| if (!c.heir && chance(c.married ? 0.18 : 0.07)){ | |
| c.heir = true; | |
| if (r.id === S.p) addLog(c.married ? 'Joy in your hall β an heir is born to your line.' | |
| : 'A kinsman comes of age at your court. You have an heir.'); | |
| } | |
| } | |
| } | |
| /* ============ player actions ============ */ | |
| const ACTIONS = [ | |
| {id:'praise', n:'Praise', g:0, cd:3, tip:'Flattering words from your envoy. A small, free warmth.'}, | |
| {id:'gift', n:'Send Gift', g:60, cd:4, tip:'Gold speaks. +20 opinion.'}, | |
| {id:'sway', n:'Sway Court', g:30, cd:8, tip:'Your envoy lingers at their court: +3 opinion each season for 6 seasons.'}, | |
| {id:'marry', n:'Marriage Pact', g:100, cd:0, tip:'Bind your dynasties. +35 opinion and unlocks an alliance.'}, | |
| {id:'ally', n:'Form Alliance', g:0, cd:0, tip:'They fight beside you. Needs opinion 30+, or a marriage pact.'}, | |
| {id:'insult', n:'Insult', g:0, cd:3, tip:'A public slight. β25 opinion, +5 prestige (+10 against a rival).'}, | |
| {id:'rumor', n:'Spread Rumor', g:40, cd:5, tip:'Poison another court against them. Success rests on your intrigue.'}, | |
| {id:'plot', n:'Plot Assassination', g:150, cd:12, tip:'A knife in the dark. Discovery means disgrace β or war.'}, | |
| {id:'war', n:'Declare War', g:0, cd:0, tip:'Press your claim by force. The victor seizes a third of the loser\'s power.'}, | |
| {id:'peace', n:'Sue for Peace', g:0, cd:0, tip:'End the war on current terms: victory at +60, surrender at β60, else white peace.'}, | |
| ]; | |
| // Returns null if allowed, 'hide' to omit the button, or a reason string to disable it. | |
| function why(aid, t){ | |
| const act = ACTIONS.find(x => x.id === aid); | |
| const w = warBetween(S.p, t); | |
| if (aid === 'peace') return w ? null : 'hide'; | |
| if (aid === 'war'){ | |
| if (w) return 'hide'; | |
| return allied(S.p, t) ? 'You are sworn allies' : null; | |
| } | |
| if (w && ['praise','gift','sway','marry','ally'].includes(aid)) return 'Not while at war'; | |
| if (aid === 'ally' && allied(S.p, t)) return 'hide'; | |
| if (act.g > S.gold) return 'Needs ' + act.g + ' gold'; | |
| if (S.cds[aid + ':' + t] > 0) return 'Ready in ' + S.cds[aid + ':' + t] + ' season(s)'; | |
| if (aid === 'marry' && tied(S.p, t)) return 'Dynasties already bound'; | |
| if (aid === 'ally' && !(S.op[t][S.p] >= 30 || tied(S.p, t))) | |
| return 'They think too little of you (needs opinion 30, or a marriage pact)'; | |
| return null; | |
| } | |
| function act(aid, t){ | |
| if (S.over || why(aid, t)) return; | |
| const a = ACTIONS.find(x => x.id === aid); | |
| S.gold -= a.g; | |
| if (a.cd) S.cds[aid + ':' + t] = a.cd; | |
| const T = S.realms[t], me = S.realms[S.p], pc = me.ruler, tc = T.ruler; | |
| switch (aid){ | |
| case 'praise': | |
| opAdd(t, S.p, 6 + Math.floor(pc.d/4)); | |
| addLog(`Your envoy heaps praise on ${tc.name} of ${T.name}.`); | |
| break; | |
| case 'gift': | |
| opAdd(t, S.p, 20); | |
| addLog(`A caravan of gifts arrives in ${T.name}. ${tc.name} is pleased.`); | |
| break; | |
| case 'sway': | |
| S.sway.push({t, left:6}); | |
| addLog(`Your envoy settles into the court of ${T.name} to sway ${tc.name}.`); | |
| break; | |
| case 'marry': | |
| S.ties[pairKey(S.p,t)] = 1; | |
| opAdd(t, S.p, 35); | |
| S.base[t][S.p] = clamp(S.base[t][S.p] + 15, -70, 70); | |
| S.prestige += 5; | |
| addLog(`Wedding bells across the marches β your dynasty is bound to House ${tc.dyn} of ${T.name}.`); | |
| break; | |
| case 'ally': | |
| S.allies[pairKey(S.p,t)] = 1; | |
| S.base[t][S.p] = clamp(S.base[t][S.p] + 10, -70, 70); | |
| S.prestige += 10; | |
| addLog(`π€ ${T.name} swears an alliance with you.`); | |
| break; | |
| case 'insult': | |
| S.prestige += S.op[t][S.p] < -40 ? 10 : 5; | |
| opAdd(t, S.p, -25); | |
| addLog(`Your herald delivers a stinging insult to ${tc.name}. The courts titter.`); | |
| break; | |
| case 'rumor': { | |
| const pool = others(S.p).filter(x => x !== t); | |
| if (!pool.length){ addLog('There is no court left to whisper in.'); break; } | |
| const r = pick(pool); | |
| const ch = clamp(40 + 3*(pc.i - tc.i), 10, 85); | |
| if (rnd(1,100) <= ch){ | |
| opAdd(r, t, -20); | |
| addLog(`Whispers take root in ${S.realms[r].name}: ${S.realms[r].ruler.name} now looks coldly on ${tc.name}.`); | |
| } else if (chance(0.5)){ | |
| opAdd(t, S.p, -20); | |
| addLog(`Your rumor is traced back to your court. ${tc.name} is furious.`); | |
| } else addLog('Your rumor dies quietly, unheard.'); | |
| break; | |
| } | |
| case 'plot': { | |
| const ch = clamp(15 + 4*(pc.i - tc.i), 5, 70); | |
| if (rnd(1,100) <= ch){ | |
| addLog(`${tc.name} of ${T.name} is found dead in the night. None can prove a thing.`); | |
| if (chance(0.35)){ | |
| for (const b of others(S.p)) opAdd(b, S.p, -10); | |
| addLog('β¦yet whispers in every court name you the hand behind it.'); | |
| } | |
| succession(t, 'was slain by an unknown hand'); | |
| } else if (chance(0.6)){ | |
| opAdd(t, S.p, -60); | |
| S.prestige -= 15; | |
| addLog(`Your assassin is caught! ${tc.name} knows whose coin bought the knife.`); | |
| if (T.power > me.power * 0.8 && !warBetween(S.p, t)) declareWar(t, S.p); | |
| } else addLog('Your assassin loses nerve and slips away. No one is the wiser.'); | |
| break; | |
| } | |
| case 'war': | |
| declareWar(S.p, t); | |
| break; | |
| case 'peace': { | |
| const w = warBetween(S.p, t); | |
| const my = w.a === S.p ? w.score : -w.score; | |
| S.wars = S.wars.filter(x => x !== w); | |
| if (my >= 60){ S.prestige += 10; addLog(`${T.name} concedes β you dictate a victor's peace.`); transfer(t, S.p, 0.2); } | |
| else if (my <= -60){ S.prestige -= 10; addLog(`You bend the knee. A costly peace with ${T.name}.`); transfer(S.p, t, 0.2); } | |
| else { S.prestige -= 5; opAdd(t, S.p, 10); addLog(`White peace with ${T.name}. The matter rests, for now.`); } | |
| break; | |
| } | |
| } | |
| save(); render(); | |
| } | |
| /* ============ AI ============ */ | |
| function aiTurn(){ | |
| for (const r of alive()){ | |
| if (r.id === S.p || !chance(0.55)) continue; | |
| const a = r.id, pool = others(a); | |
| if (!pool.length) continue; | |
| const t = pick(pool), o = S.op[a][t]; | |
| const hawk = r.ruler.traits.includes('ambitious') || r.ruler.traits.includes('wrathful'); | |
| if (o < -45 && !allied(a,t) && !warBetween(a,t) && !atWarAny(a) | |
| && r.power > S.realms[t].power * 1.15 && chance(hawk ? 0.2 : 0.1)){ | |
| declareWar(a, t); | |
| } else if (o > 35 && S.op[t][a] > 25 && !allied(a,t) && !warBetween(a,t) && chance(0.25)){ | |
| S.allies[pairKey(a,t)] = 1; | |
| addLog(`π€ ${r.name} and ${S.realms[t].name} swear an alliance.`); | |
| } else if (o > 5 && chance(0.15)){ | |
| opAdd(t, a, 12); | |
| if (t === S.p) addLog(`${r.ruler.name} of ${r.name} sends you a generous gift of ${r.race === 'Dwarf' ? 'wrought gold' : 'fine horses'}.`); | |
| } else if (o < -25 && chance(0.15)){ | |
| opAdd(t, a, -12); | |
| if (t === S.p) addLog(`${r.ruler.name} of ${r.name} mocks you before the assembled courts.`); | |
| } | |
| } | |
| } | |
| /* ============ events ============ */ | |
| const EVENTS = [ | |
| {t:'A Grand Tourney', b:R=>`${rn(R)} proclaims a grand tourney and invites champions from every realm.`, ch:[ | |
| {l:'Attend with a lavish retinue (β40 gold)', g:40, f:R=>{S.gold-=40; opAdd(R,S.p,15); S.prestige+=5; return 'Your champions take the laurels. The courts speak your name warmly.';}}, | |
| {l:'Decline politely', f:R=>{opAdd(R,S.p,-5); return `${rn(R)} marks your empty seat.`;}}]}, | |
| {t:'Feast of the Long Night', b:()=>'The longest night of the year approaches, when crowns traditionally open their halls.', ch:[ | |
| {l:'Host a great feast (β60 gold)', g:60, f:R=>{S.gold-=60; opAdd(R,S.p,12); opAdd(other2(R),S.p,12); S.prestige+=4; return 'Your hall blazes with light; two courts leave as friends.';}}, | |
| {l:'Keep your hall dark', f:R=>{opAdd(R,S.p,-4); return 'Travelers carry word of your cold hearth.';}}]}, | |
| {t:'A Dragon Over the Marches', b:()=>'A young dragon has taken to burning granaries along your border.', ch:[ | |
| {l:'Ride against it yourself', f:()=>{const c=S.realms[S.p].ruler; if(c.m>=12){S.prestige+=12; return 'You slay the wyrm. The songs are already spreading.';} S.prestige-=5; return 'The beast drives you back, singed and humbled.';}}, | |
| {l:'Hire dragonslayers (β50 gold)', g:50, f:()=>{S.gold-=50; S.prestige+=6; return 'The slayers mount the head above your gate.';}}]}, | |
| {t:'The Wandering Bard', b:R=>`A famed bard offers to carry a ballad to the court of ${rn(R)}.`, ch:[ | |
| {l:'Pay for a flattering ballad (β25 gold)', g:25, f:R=>{S.gold-=25; opAdd(R,S.p,12); return `The ballad delights the court of ${S.realms[R].name}.`;}}, | |
| {l:'Have him sing of your own glory', f:()=>{S.prestige+=3; return 'Your name rings in taverns across Eldervale.';}}]}, | |
| {t:'Border Skirmish', b:R=>`Your riders clash with hunters sworn to ${rn(R)}. Blood is spilled.`, ch:[ | |
| {l:'Pay weregild (β30 gold)', g:30, f:R=>{S.gold-=30; opAdd(R,S.p,10); return 'The dead are paid for; tempers cool.';}}, | |
| {l:'Stand firm', f:R=>{opAdd(R,S.p,-15); S.prestige+=5; return 'You yield nothing. Their envoy departs in fury.';}}]}, | |
| {t:'A Suspicious Gift', b:R=>`An unmarked cask of elderwine arrives, said to come from ${rn(R)}.`, ch:[ | |
| {l:'Drink deep', f:R=>{if(chance(0.25)){opAdd(R,S.p,-20); S.prestige-=5; return 'Poison! You survive, barely. Someone will answer for this.';} S.prestige+=3; return 'A superb vintage. The rumor was baseless.';}}, | |
| {l:'Pour it into the sea', f:R=>{opAdd(R,S.p,-5); return 'If the gift was honest, word of the insult will travel.';}}]}, | |
| {t:'Refugees of War', b:()=>'Ragged columns from the war-torn marches beg leave to settle in your lands.', ch:[ | |
| {l:'Shelter them (β30 gold)', g:30, f:R=>{S.gold-=30; S.prestige+=6; opAdd(R,S.p,8); return 'They bless your name. The courts take note of your mercy.';}}, | |
| {l:'Bar the gates', f:R=>{opAdd(R,S.p,-8); return 'They trudge on. Minstrels make of it a sad song.';}}]}, | |
| {t:'The Hedge-Wizard', b:()=>'A ragged wizard at your gate offers blessings β for a price.', ch:[ | |
| {l:'Pay for a blessing (β20 gold)', g:20, f:R=>{S.gold-=20; opAdd(R,S.p,10); return `He carries kind words of you to the court of ${S.realms[R].name}.`;}}, | |
| {l:'Turn him away', f:()=>{if(chance(0.3)){opAdd(pick(others(S.p)),S.p,-5); opAdd(pick(others(S.p)),S.p,-5); return 'He curses your name at every crossroads.';} return 'He shuffles off, muttering.';}}]}, | |
| {t:'Trade Delegation', b:R=>`Merchants under the banner of ${rn(R)} seek a charter in your ports.`, ch:[ | |
| {l:'Grant the charter', f:R=>{S.gold+=40; opAdd(R,S.p,8); return 'Trade flows; both courts profit.';}}, | |
| {l:'Levy heavy tariffs', f:R=>{S.gold+=80; opAdd(R,S.p,-12); return 'Your coffers swell. Their merchants seethe.';}}]}, | |
| {t:'A Faerie Bargain', b:()=>'A creature of the twilight courts offers a chest of faerie gold β for nothing at all, it says.', ch:[ | |
| {l:'Take the gold', f:R=>{S.gold+=80; opAdd(R,S.p,-6); opAdd(other2(R),S.p,-6); return 'The gold is real. So are the uneasy whispers that follow it.';}}, | |
| {l:'Refuse politely', f:()=>{S.prestige+=4; return 'It smiles too widely and departs. You feel wiser.';}}]}, | |
| ]; | |
| function maybeEvent(){ | |
| if (!chance(0.3)) return; | |
| const pool = others(S.p); | |
| if (!pool.length) return; | |
| const R = pick(pool), ev = pick(EVENTS); | |
| showModal(ev.t, `<p class="mint">${ev.b(R)}</p>`, ev.ch.map(c => ({ | |
| label:c.l, disabled:(c.g||0) > S.gold, | |
| fn:() => { const res = c.f(R); if (res) addLog(res); save(); render(); } | |
| }))); | |
| } | |
| /* ============ turn loop ============ */ | |
| function endTurn(){ | |
| if (S.p === null || S.over) return; | |
| for (const s of S.sway){ opAdd(s.t, S.p, 3); s.left--; } | |
| S.sway = S.sway.filter(s => s.left > 0 && S.realms[s.t].alive); | |
| aiTurn(); | |
| tickWars(); | |
| if (!S.over) maybeEvent(); | |
| // opinions drift back toward their base, one point a season | |
| for (const a of others(-1)) for (const b of others(a)){ | |
| if (S.op[a][b] < S.base[a][b]) S.op[a][b]++; | |
| else if (S.op[a][b] > S.base[a][b]) S.op[a][b]--; | |
| } | |
| for (const k in S.cds) if (S.cds[k] > 0) S.cds[k]--; | |
| S.season++; | |
| if (S.season > 3){ S.season = 0; S.year++; if (!S.over) yearly(); } | |
| S.gold += 15 + Math.floor(S.realms[S.p].power / 5); | |
| checkVictory(); | |
| save(); render(); | |
| } | |
| function checkVictory(){ | |
| if (S.over) return; | |
| const mine = S.realms[S.p].power; | |
| const rest = others(S.p); | |
| if (mine / totalPower() >= 0.5) | |
| return endGame(true, 'Half of Eldervale bows to your banner. The High Crown is yours by conquest.'); | |
| if (rest.length && rest.every(b => allied(S.p, b))) | |
| return endGame(true, 'Every living crown is sworn to you. The High Crown is yours by friendship β a thing no conqueror ever held.'); | |
| } | |
| function endGame(win, msg){ | |
| S.over = true; save(); | |
| showModal(win ? 'π The High Crown' : 'The Crown Falls', `<p class="mint">${msg}</p>`, | |
| [{label:'Begin a new saga', fn:restart}]); | |
| } | |
| function restart(){ | |
| try{ localStorage.removeItem('eldervale'); }catch(e){} | |
| newGame(); | |
| showStart(); | |
| } | |
| /* ============ rendering ============ */ | |
| function buildMap(){ | |
| const svg = $('map'), NS = 'http://www.w3.org/2000/svg'; | |
| REALM_DEFS.forEach((d, i) => { | |
| const poly = document.createElementNS(NS, 'polygon'); | |
| poly.setAttribute('points', d.pts); | |
| poly.setAttribute('fill', d.color); | |
| poly.addEventListener('click', () => { S.sel = i; render(); }); | |
| svg.appendChild(poly); | |
| const name = document.createElementNS(NS, 'text'); | |
| name.setAttribute('x', d.lx); name.setAttribute('y', d.ly); | |
| name.setAttribute('class', 'mlabel'); | |
| name.textContent = d.name; | |
| const pow = document.createElementNS(NS, 'text'); | |
| pow.setAttribute('x', d.lx); pow.setAttribute('y', d.ly + 17); | |
| pow.setAttribute('class', 'mlabel mpow'); | |
| svg.appendChild(name); svg.appendChild(pow); | |
| els[i] = {poly, name, pow}; | |
| }); | |
| } | |
| function charCard(r){ | |
| const c = r.ruler; | |
| return `<div class="ccard"><div class="cname">${title(c)} ${c.name} ${c.dyn}</div> | |
| <div class="csub">${r.name} Β· ${c.race} Β· ${c.age} years</div> | |
| <div class="stats"><span>Diplomacy ${c.d}</span><span>Intrigue ${c.i}</span><span>Martial ${c.m}</span><span>Power ${r.power}</span></div> | |
| <div class="chips">${c.traits.map(t => `<span class="chip">${t}</span>`).join('')}</div> | |
| <div class="csub">${c.married ? 'Married' : 'Unwed'} Β· ${c.heir ? 'Has an heir' : 'No heir'}</div></div>`; | |
| } | |
| function renderPanel(){ | |
| const el = $('panel'); | |
| if (S.p === null) return; | |
| const t = S.sel; | |
| if (t === null || t === S.p){ | |
| const me = S.realms[S.p]; | |
| const allies = others(S.p).filter(b => allied(S.p, b)); | |
| const myWars = S.wars.filter(w => w.a === S.p || w.d === S.p); | |
| el.innerHTML = `<h3>Your Court</h3>${charCard(me)} | |
| <div class="objbox"><h4>Paths to the High Crown</h4> | |
| <p>β Conquest β hold half of Eldervale's power: <b>${Math.round(100*me.power/totalPower())}%</b> of 50%</p> | |
| <p>π€ Friendship β ally every living crown: <b>${allies.length}</b> of ${others(S.p).length}</p></div> | |
| ${allies.length ? `<div class="objbox"><h4>Allies</h4><p>${allies.map(b => S.realms[b].name).join(', ')}</p></div>` : ''} | |
| ${myWars.map(w => { const e2 = w.a === S.p ? w.d : w.a; const my = w.a === S.p ? w.score : -w.score; | |
| return `<div class="objbox war"><h4>War with ${S.realms[e2].name}</h4><p>War score <b>${Math.round(my)}</b> β the war ends at Β±100, or sue for peace.</p></div>`; }).join('')} | |
| <p class="hint">Select another realm on the map to open its court.</p>`; | |
| return; | |
| } | |
| const r = S.realms[t]; | |
| if (!r.alive){ | |
| el.innerHTML = `<h3>${r.name}</h3><p class="hint">These lands now answer to ${S.realms[r.absorbedBy].name}.</p>`; | |
| return; | |
| } | |
| const o = S.op[t][S.p]; | |
| const rel = []; | |
| if (allied(S.p, t)) rel.push('π€ Ally'); | |
| if (tied(S.p, t)) rel.push('π Bound by marriage'); | |
| if (warBetween(S.p, t)) rel.push('β At war'); | |
| const theirAllies = others(t).filter(b => b !== S.p && allied(t, b)).map(b => S.realms[b].name); | |
| el.innerHTML = `<h3>Court of ${r.name}</h3>${charCard(r)} | |
| <div class="opwrap"><div class="oplabel">Their opinion of you <b class="${o >= 0 ? 'pos' : 'neg'}">${o > 0 ? '+' : ''}${o}</b></div> | |
| <div class="opbar"><div class="opfill ${o >= 0 ? 'pos' : 'neg'}" style="width:${(o+100)/2}%"></div></div></div> | |
| ${rel.length ? `<div class="chips">${rel.map(x => `<span class="chip rel">${x}</span>`).join('')}</div>` : ''} | |
| ${theirAllies.length ? `<p class="hint">Sworn to: ${theirAllies.join(', ')}</p>` : ''} | |
| <h4>Diplomacy</h4> | |
| <div id="actions">${ACTIONS.map(a => { | |
| const reason = why(a.id, t); | |
| if (reason === 'hide') return ''; | |
| return `<button class="abtn" data-act="${a.id}" data-t="${t}" ${reason ? 'disabled' : ''}> | |
| <span>${a.n}${a.g ? ' Β· ' + a.g + ' gold' : ''}</span><small>${reason || a.tip}</small></button>`; | |
| }).join('')}</div>`; | |
| } | |
| function render(){ | |
| if (S.p === null) return; | |
| const me = S.realms[S.p]; | |
| $('date').textContent = SEASONS[S.season] + ' ' + S.year; | |
| $('gold').textContent = S.gold; | |
| $('prest').textContent = S.prestige; | |
| $('power').textContent = Math.round(100 * me.power / totalPower()) + '%'; | |
| $('myrealm').textContent = title(me.ruler) + ' ' + me.ruler.name + ' of ' + me.name; | |
| for (const r of S.realms){ | |
| const e = els[r.id]; | |
| const owner = r.alive ? r : S.realms[r.absorbedBy]; | |
| e.poly.setAttribute('fill', owner.color); | |
| e.poly.classList.toggle('sel', S.sel === r.id); | |
| e.poly.classList.toggle('dead', !r.alive); | |
| let tag = ''; | |
| if (r.id === S.p) tag = ' π'; | |
| else if (r.alive){ | |
| if (warBetween(S.p, r.id)) tag = ' β'; | |
| else if (allied(S.p, r.id)) tag = ' π€'; | |
| else if (tied(S.p, r.id)) tag = ' π'; | |
| } | |
| e.name.textContent = r.name + tag; | |
| e.pow.textContent = r.alive ? r.power : ''; | |
| e.name.classList.toggle('dead', !r.alive); | |
| } | |
| $('log').innerHTML = S.log.map(l => `<div>${l}</div>`).join(''); | |
| renderPanel(); | |
| } | |
| /* ============ start screen & boot ============ */ | |
| function showStart(){ | |
| const body = `<p class="mint">Eldervale has no High Sovereign. Ten crowns scheme, feast, marry and murder for the honor. | |
| Win by holding <b>half the land's power</b> β or by making an <b>ally of every living crown</b>. | |
| Mind your heirs: a dynasty without one dies with you.</p> | |
| <div id="pickgrid">${S.realms.map(r => { const c = r.ruler; | |
| return `<div class="pickcard" data-pick="${r.id}" tabindex="0"> | |
| <span class="dot" style="background:${r.color}"></span><b>${r.name}</b> | |
| <small>${c.race} Β· ${title(c)} ${c.name} ${c.dyn}, ${c.age}</small> | |
| <small>Diplomacy ${c.d} Β· Intrigue ${c.i} Β· Martial ${c.m} Β· Power ${r.power}</small> | |
| <small class="traits">${c.traits.join(' Β· ')}</small></div>`; }).join('')}</div>`; | |
| showModal('Choose Your Crown', body, []); | |
| document.querySelectorAll('.pickcard').forEach(el => { | |
| const go = () => { | |
| S.p = +el.dataset.pick; S.sel = null; | |
| hideModal(); | |
| addLog(`You take up the crown of ${S.realms[S.p].name}. May your reign be long.`); | |
| save(); render(); | |
| }; | |
| el.onclick = go; | |
| el.onkeydown = ev => { if (ev.key === 'Enter' || ev.key === ' ') go(); }; | |
| }); | |
| } | |
| $('endturn').onclick = endTurn; | |
| $('restart').onclick = () => showModal('Begin anew?', '<p class="mint">Abandon this saga and start a new one?</p>', | |
| [{label:'Yes β begin a new saga', fn:restart}, {label:'No β continue', fn:null}]); | |
| $('panel').addEventListener('click', e => { | |
| const b = e.target.closest('[data-act]'); | |
| if (b && !b.disabled) act(b.dataset.act, +b.dataset.t); | |
| }); | |
| const hasSave = load(); | |
| buildMap(); | |
| if (hasSave && S.p !== null && !S.over) render(); | |
| else { newGame(); showStart(); } | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment