← Creature Collection results

Creature Collection

Claude Opus 5 · typescript

Initial view of Creature Collection

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 248,776 bytes · SHA-256 359c91aa261f
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aetherbind — Chronicles of the Seven Resonances</title>
<style>
/* ── css: base ── */
:root{
  --ink:#e8f0ff; --ink-dim:#93a4c6; --void:#05060d; --void2:#0b0e1c;
  --glass:rgba(18,24,48,.55); --glass2:rgba(38,48,88,.42);
  --edge:rgba(150,190,255,.22); --edge2:rgba(180,220,255,.45);
  --gold:#ffd479; --rose:#ff7a9c; --cyan:#6ff0ff; --lime:#9dff8f; --violet:#c39bff;
  --t-ember:#ff8a4c; --t-tide:#54c8ff; --t-loam:#a9e06a; --t-gale:#c6b7ff;
  --t-glim:#ffe066; --t-hush:#ff6fa8; --t-stone:#9fb2c9;
  --ok:#7fe7a2; --bad:#ff6b6b;
  --font: "Trebuchet MS", "Segoe UI", system-ui, sans-serif;
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%;overflow:hidden;background:var(--void);color:var(--ink);font-family:var(--font)}
body{user-select:none}
#app{position:fixed;inset:0;overflow:hidden}
#scene{position:absolute;inset:0;width:100%;height:100%;display:block}
.hidden{display:none !important}
#vignette{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(ellipse at 50% 50%, transparent 45%, rgba(0,0,0,.55) 100%);
  transition:background-color .6s ease; mix-blend-mode:normal}
#vignette.tinted{box-shadow:inset 0 0 220px 60px var(--tint,transparent)}
button{font-family:var(--font);cursor:pointer;color:var(--ink)}
::-webkit-scrollbar{width:8px}
::-webkit-scrollbar-thumb{background:rgba(140,180,255,.28);border-radius:8px}
.shake{animation:shk .32s cubic-bezier(.36,.07,.19,.97)}
@keyframes shk{10%,90%{transform:translate(-2px,1px)}30%{transform:translate(5px,-3px)}50%{transform:translate(-6px,2px)}70%{transform:translate(4px,3px)}}
/* ── css: title ── */
#title-screen{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  background:radial-gradient(circle at 50% 32%, rgba(23,32,74,.72) 0%, rgba(8,10,24,.85) 55%, rgba(3,4,10,.95) 100%);z-index:60}
#title-glow{position:absolute;inset:-20%;
  background:
   radial-gradient(circle at 30% 40%, rgba(110,200,255,.20), transparent 40%),
   radial-gradient(circle at 70% 60%, rgba(200,140,255,.18), transparent 42%),
   radial-gradient(circle at 50% 85%, rgba(255,210,120,.14), transparent 45%);
  animation:drift 22s ease-in-out infinite alternate;filter:blur(8px)}
@keyframes drift{0%{transform:translate(-3%,-2%) scale(1)}100%{transform:translate(3%,2%) scale(1.12)}}
#title-inner{position:relative;text-align:center;padding:32px;max-width:min(94vw,760px)}
#title-inner h1{margin:0;font-size:clamp(34px,8.2vw,80px);letter-spacing:.2em;text-indent:.2em;font-weight:800;
  background:linear-gradient(180deg,#fff,#a9d8ff 45%,#8f8cff);-webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 0 26px rgba(120,180,255,.55));animation:pulseGlow 5s ease-in-out infinite}
@keyframes pulseGlow{50%{filter:drop-shadow(0 0 44px rgba(180,140,255,.75))}}
#title-inner h2{margin:8px 0 30px;font-size:clamp(10px,2.2vw,15px);letter-spacing:.4em;text-indent:.4em;color:var(--ink-dim);
  font-weight:400;text-transform:uppercase}
#title-buttons{display:flex;gap:16px;justify-content:center;flex-wrap:wrap}
.bigbtn{padding:14px 30px;font-size:16px;letter-spacing:.16em;text-transform:uppercase;border-radius:14px;
  border:1px solid var(--edge2);background:linear-gradient(180deg,rgba(120,170,255,.28),rgba(60,90,190,.20));
  backdrop-filter:blur(10px);box-shadow:0 8px 30px rgba(60,110,255,.25), inset 0 1px 0 rgba(255,255,255,.2);
  transition:transform .15s, box-shadow .2s, background .2s}
.bigbtn:hover{transform:translateY(-2px);box-shadow:0 14px 40px rgba(90,150,255,.45), inset 0 1px 0 rgba(255,255,255,.3)}
.bigbtn.ghost{background:rgba(255,255,255,.05)}
.bigbtn:disabled{opacity:.35;cursor:not-allowed;transform:none}
#title-foot{margin-top:34px;color:#8b98b8;font-size:13px;font-style:italic;line-height:1.6}
/* ── css: hud ── */
#hud{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;
  padding:12px 14px;gap:10px;z-index:20;pointer-events:none}
#hud-left{pointer-events:none;display:flex;flex-direction:column;gap:6px}
#hud-zone{font-size:15px;letter-spacing:.22em;text-transform:uppercase;font-weight:700;
  padding:7px 14px;border-radius:10px;background:var(--glass);border:1px solid var(--edge);backdrop-filter:blur(8px);
  box-shadow:0 4px 18px rgba(0,0,0,.4)}
#hud-quest{font-size:12px;color:var(--ink-dim);padding:5px 12px;border-radius:8px;background:rgba(8,12,26,.5);
  border:1px solid rgba(150,190,255,.12);max-width:46vw}
#hud-right{display:flex;gap:8px;pointer-events:auto}
.chip{padding:8px 13px;font-size:12px;letter-spacing:.14em;border-radius:10px;border:1px solid var(--edge);
  background:var(--glass);backdrop-filter:blur(8px);transition:background .18s,transform .12s}
.chip:hover{background:var(--glass2);transform:translateY(-1px)}
#touch{position:absolute;left:0;right:0;bottom:0;padding:18px;display:flex;justify-content:space-between;
  align-items:flex-end;z-index:21;pointer-events:none}
#dpad{display:grid;grid-template-columns:repeat(3,52px);grid-template-rows:repeat(3,52px);pointer-events:auto;opacity:.9}
#dpad .tbtn:nth-child(1){grid-area:1/2}
#dpad .tbtn:nth-child(2){grid-area:2/1}
#dpad .tbtn:nth-child(3){grid-area:3/2}
#dpad .tbtn:nth-child(4){grid-area:2/3}
#abtn{display:flex;gap:12px;pointer-events:auto}
.tbtn{width:52px;height:52px;border-radius:14px;border:1px solid var(--edge2);background:var(--glass);
  backdrop-filter:blur(8px);font-size:16px;color:var(--ink);display:flex;align-items:center;justify-content:center}
.tbtn:active{background:rgba(120,180,255,.45)}
.tbtn.round{border-radius:50%;width:60px;height:60px;font-weight:700}
#toast-root{position:absolute;top:64px;left:50%;transform:translateX(-50%);z-index:70;display:flex;flex-direction:column;
  gap:8px;align-items:center;pointer-events:none}
.toast{padding:9px 18px;border-radius:999px;background:rgba(10,16,34,.82);border:1px solid var(--edge2);
  font-size:13px;letter-spacing:.06em;backdrop-filter:blur(10px);animation:toastIn .3s ease, toastOut .4s ease 2.1s forwards;
  box-shadow:0 6px 24px rgba(0,0,0,.5)}
@keyframes toastIn{from{opacity:0;transform:translateY(-12px)}}
@keyframes toastOut{to{opacity:0;transform:translateY(-10px)}}
/* ── css: dialogue ── */
#dialogue{position:absolute;left:50%;transform:translateX(-50%);bottom:24px;width:min(92vw,780px);z-index:40;
  padding:18px 22px 16px;border-radius:16px;background:linear-gradient(180deg,rgba(16,22,46,.86),rgba(8,11,26,.9));
  border:1px solid var(--edge2);backdrop-filter:blur(14px);box-shadow:0 18px 60px rgba(0,0,0,.6);cursor:pointer}
#dlg-name{font-size:12px;letter-spacing:.28em;text-transform:uppercase;color:var(--gold);margin-bottom:8px}
#dlg-text{font-size:16px;line-height:1.65;min-height:3.3em}
#dlg-hint{text-align:right;font-size:11px;color:var(--ink-dim);animation:blink 1.4s infinite}
@keyframes blink{50%{opacity:.25}}
#cutin{position:absolute;inset:0;z-index:55;display:flex;align-items:center;justify-content:center;
  background:radial-gradient(ellipse at 50% 50%, rgba(0,0,0,.35), rgba(0,0,0,.9));animation:fadeIn .5s ease}
@keyframes fadeIn{from{opacity:0}}
#cutin-inner{max-width:min(90vw,700px);text-align:center;padding:36px;border-radius:20px;
  border:1px solid var(--cut,rgba(180,220,255,.5));background:rgba(8,12,28,.6);backdrop-filter:blur(12px);
  box-shadow:0 0 90px 10px var(--cutglow,rgba(90,150,255,.28))}
#cutin-title{font-size:clamp(20px,4.4vw,34px);letter-spacing:.2em;text-transform:uppercase;margin-bottom:18px;
  color:var(--cut,#bfe0ff);text-shadow:0 0 24px var(--cutglow,rgba(120,180,255,.7))}
#cutin-body{font-size:16px;line-height:1.8;color:#dbe6ff;font-style:italic}
#cutin-inner::after{content:"click to continue";display:block;margin-top:22px;font-size:11px;letter-spacing:.24em;color:#7f8dae}
/* ── css: modal ── */
#modal-root{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;
  background:rgba(3,5,12,.62);backdrop-filter:blur(3px);animation:fadeIn .18s ease;padding:16px}
.panel{width:min(96vw,940px);max-height:min(90vh,760px);display:flex;flex-direction:column;border-radius:20px;
  background:linear-gradient(160deg,rgba(24,32,62,.86),rgba(10,14,30,.92));border:1px solid var(--edge2);
  box-shadow:0 30px 90px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.12);backdrop-filter:blur(18px);overflow:hidden;
  animation:panelIn .22s cubic-bezier(.2,.9,.3,1)}
@keyframes panelIn{from{opacity:0;transform:translateY(18px) scale(.97)}}
.panel-head{display:flex;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid rgba(150,190,255,.16);
  background:linear-gradient(180deg,rgba(90,130,230,.16),transparent)}
.panel-head h3{margin:0;font-size:15px;letter-spacing:.26em;text-transform:uppercase;flex:1}
.panel-head .sub{font-size:12px;color:var(--ink-dim);letter-spacing:.1em}
.panel-body{padding:18px 20px;overflow-y:auto;flex:1}
.panel-foot{padding:12px 20px;border-top:1px solid rgba(150,190,255,.14);display:flex;gap:10px;justify-content:flex-end;
  flex-wrap:wrap;align-items:center}
.tabs{display:flex;gap:6px;padding:10px 16px 0;flex-wrap:wrap}
.tab{padding:8px 16px;font-size:12px;letter-spacing:.14em;text-transform:uppercase;border-radius:10px 10px 0 0;
  border:1px solid transparent;background:rgba(255,255,255,.04);color:var(--ink-dim)}
.tab.on{background:rgba(120,170,255,.20);border-color:var(--edge);color:var(--ink)}
.btn{padding:9px 17px;border-radius:10px;border:1px solid var(--edge);background:rgba(120,170,255,.14);
  font-size:12.5px;letter-spacing:.1em;transition:background .16s,transform .12s}
.btn:hover{background:rgba(140,190,255,.3);transform:translateY(-1px)}
.btn:disabled{opacity:.35;cursor:not-allowed;transform:none}
.btn.warn{background:rgba(255,110,110,.16);border-color:rgba(255,140,140,.4)}
.btn.good{background:rgba(120,255,180,.16);border-color:rgba(140,255,190,.4)}
.grid{display:grid;gap:12px}
.g2{grid-template-columns:repeat(auto-fill,minmax(260px,1fr))}
.g3{grid-template-columns:repeat(auto-fill,minmax(190px,1fr))}
.card{padding:12px 14px;border-radius:14px;background:rgba(255,255,255,.045);border:1px solid rgba(150,190,255,.14);
  transition:background .16s,border-color .16s,transform .12s;cursor:pointer;position:relative;overflow:hidden}
.card:hover{background:rgba(130,180,255,.12);border-color:var(--edge2);transform:translateY(-2px)}
.card.sel{background:rgba(150,200,255,.18);border-color:var(--edge2);box-shadow:0 0 0 1px var(--edge2) inset}
.card.dead{opacity:.5}
.card.locked{opacity:.4;cursor:default}
.card.locked:hover{transform:none;background:rgba(255,255,255,.045)}
.row{display:flex;align-items:center;gap:12px}
.spacer{flex:1}
.ico{width:52px;height:52px;border-radius:12px;background:radial-gradient(circle at 50% 45%,rgba(255,255,255,.1),rgba(0,0,0,.25));
  border:1px solid rgba(160,200,255,.16);flex:0 0 auto;image-rendering:auto}
.nm{font-size:14.5px;font-weight:700;letter-spacing:.04em}
.mt{font-size:11.5px;color:var(--ink-dim);letter-spacing:.06em}
.pill{display:inline-block;padding:2.5px 9px;border-radius:999px;font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;font-weight:700;color:#06080f}
.bar{height:7px;border-radius:99px;background:rgba(255,255,255,.1);overflow:hidden;margin-top:6px}
.bar > i{display:block;height:100%;border-radius:99px;transition:width .5s cubic-bezier(.2,.9,.3,1)}
.bar.thin{height:4px}
.hpfill{background:linear-gradient(90deg,#66e39a,#a8f0b6)}
.hpfill.mid{background:linear-gradient(90deg,#ffd166,#ffe9a0)}
.hpfill.low{background:linear-gradient(90deg,#ff6b6b,#ff9d9d)}
.xpfill{background:linear-gradient(90deg,#6fb2ff,#b0d8ff)}
.stat-line{display:flex;justify-content:space-between;font-size:12px;padding:3px 0;border-bottom:1px dashed rgba(150,190,255,.1)}
.stat-line b{color:var(--gold);font-weight:600}
.lore{font-size:12.5px;line-height:1.65;color:#b9c6e4;font-style:italic;margin:8px 0}
.mvcard{padding:9px 12px;border-radius:11px;background:rgba(255,255,255,.05);border:1px solid rgba(150,190,255,.13);
  font-size:12px;cursor:pointer}
.mvcard:hover{background:rgba(130,180,255,.14)}
.kv{font-size:11px;color:var(--ink-dim);display:flex;gap:10px;flex-wrap:wrap;margin-top:4px}
.empty{text-align:center;padding:36px;color:var(--ink-dim);font-style:italic;font-size:13px}
.sectitle{font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--gold);margin:18px 0 10px}
.sectitle:first-child{margin-top:0}
.miniмap{display:block}
#dexmap{width:100%;border-radius:14px;border:1px solid var(--edge);background:rgba(0,0,0,.3)}
.chartgrid{display:grid;grid-template-columns:auto repeat(7,1fr);gap:2px;font-size:10px}
.chartgrid div{padding:5px 2px;text-align:center;border-radius:4px;background:rgba(255,255,255,.04)}
.chartgrid .hd{font-weight:700;letter-spacing:.04em;font-size:9px;text-transform:uppercase}
/* ── css: battleui ── */
#battle-ui{position:absolute;inset:0;z-index:30;pointer-events:none}
#battle-ui .layer{position:absolute;inset:0}
.bstatus{position:absolute;width:min(38vw,290px);padding:9px 12px;border-radius:13px;
  background:linear-gradient(160deg,rgba(18,24,50,.82),rgba(8,11,26,.86));border:1px solid var(--edge);
  backdrop-filter:blur(10px);box-shadow:0 8px 26px rgba(0,0,0,.5);pointer-events:none;transition:opacity .25s}
.bstatus .r1{display:flex;align-items:baseline;gap:8px}
.bstatus .nmx{font-size:13.5px;font-weight:700;letter-spacing:.05em;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bstatus .lv{font-size:11px;color:var(--gold)}
.bstatus .hpnum{font-size:10.5px;color:var(--ink-dim);text-align:right;margin-top:3px;letter-spacing:.06em}
.stchips{display:flex;gap:4px;margin-top:5px;flex-wrap:wrap}
.stchip{font-size:9px;letter-spacing:.1em;padding:1.5px 6px;border-radius:99px;font-weight:700;text-transform:uppercase}
.st-burn{background:#ff7043;color:#210800}.st-frost{background:#7fd8ff;color:#00212e}
.st-shock{background:#ffe066;color:#2b2200}.st-regen{background:#8ff0a8;color:#012713}
.st-sap{background:#c58cff;color:#22003a}.st-barrier{background:#ffffff;color:#0b1226}
#bcmd{position:absolute;left:0;right:0;bottom:0;padding:12px;pointer-events:auto;
  display:flex;flex-direction:column;gap:9px;align-items:center}
#blog{width:min(96vw,880px);min-height:52px;padding:11px 16px;border-radius:14px;
  background:linear-gradient(180deg,rgba(14,20,42,.9),rgba(6,9,22,.94));border:1px solid var(--edge2);
  font-size:14px;line-height:1.5;backdrop-filter:blur(12px);box-shadow:0 10px 34px rgba(0,0,0,.55)}
#bmenu{width:min(96vw,880px);display:grid;gap:9px}
#bmenu.main{grid-template-columns:repeat(4,1fr)}
#bmenu.moves{grid-template-columns:repeat(2,1fr)}
#bmenu.list{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
.bbtn{padding:12px 10px;border-radius:13px;border:1px solid var(--edge2);text-align:left;
  background:linear-gradient(180deg,rgba(70,110,220,.24),rgba(30,44,96,.3));backdrop-filter:blur(8px);
  transition:transform .12s,box-shadow .18s,background .18s;position:relative;overflow:hidden}
.bbtn:hover:not(:disabled){transform:translateY(-2px);background:linear-gradient(180deg,rgba(110,160,255,.36),rgba(40,60,130,.4));
  box-shadow:0 8px 22px rgba(70,130,255,.32)}
.bbtn:disabled{opacity:.38;cursor:not-allowed}
.bbtn .t{font-size:13.5px;font-weight:700;letter-spacing:.05em;display:flex;align-items:center;gap:7px}
.bbtn .d{font-size:10.5px;color:#a9b8da;margin-top:3px;letter-spacing:.05em}
.bbtn.center{text-align:center}
.bbtn.center .t{justify-content:center;font-size:14px;letter-spacing:.16em;text-transform:uppercase}
.affdot{width:9px;height:9px;border-radius:3px;flex:0 0 auto;box-shadow:0 0 8px currentColor}
/* rhythm mini-game */
#bindgame{position:absolute;inset:0;z-index:45;display:flex;align-items:center;justify-content:center;
  background:radial-gradient(ellipse at 50% 50%,rgba(10,14,32,.5),rgba(2,3,8,.88));pointer-events:auto;animation:fadeIn .2s}
#bindgame .bg-inner{width:min(94vw,620px);text-align:center;padding:26px;border-radius:20px;
  background:rgba(12,17,36,.82);border:1px solid var(--edge2);backdrop-filter:blur(14px);box-shadow:0 0 70px rgba(120,180,255,.25)}
#bindgame h4{margin:0 0 6px;font-size:13px;letter-spacing:.3em;text-transform:uppercase;color:var(--gold)}
#bindgame .hint{font-size:12px;color:var(--ink-dim);margin-bottom:18px;line-height:1.6}
#bindtrack{position:relative;height:64px;border-radius:14px;overflow:hidden;
  background:linear-gradient(90deg,rgba(255,90,90,.2),rgba(255,220,120,.2) 34%,rgba(120,255,180,.35) 50%,rgba(255,220,120,.2) 66%,rgba(255,90,90,.2));
  border:1px solid var(--edge)}
#bindzone{position:absolute;top:0;bottom:0;left:42%;width:16%;background:rgba(140,255,190,.25);
  border-left:2px solid #8ff0b0;border-right:2px solid #8ff0b0;box-shadow:0 0 24px rgba(140,255,190,.5) inset}
#bindneedle{position:absolute;top:-4px;bottom:-4px;width:4px;background:#fff;box-shadow:0 0 14px #fff,0 0 30px #9fd8ff;border-radius:2px}
#bindtaps{display:flex;gap:8px;justify-content:center;margin-top:16px}
.tapdot{width:14px;height:14px;border-radius:50%;background:rgba(255,255,255,.14);border:1px solid var(--edge)}
.tapdot.hit{background:#8ff0b0;box-shadow:0 0 14px #8ff0b0}
.tapdot.miss{background:#ff7a7a;box-shadow:0 0 14px #ff7a7a}
#bindmeter{margin-top:16px;font-size:12px;letter-spacing:.16em;color:var(--ink-dim)}
#bindbtn{margin-top:18px;padding:14px 34px;font-size:15px;letter-spacing:.2em;text-transform:uppercase;border-radius:14px;
  border:1px solid var(--edge2);background:linear-gradient(180deg,rgba(140,200,255,.3),rgba(50,80,180,.3))}
</style>
</head>
<body>
<div id="app">
  <canvas id="scene"></canvas>
  <div id="vignette"></div>
  <div id="hud" class="hidden">
    <div id="hud-left">
      <div id="hud-zone">—</div>
      <div id="hud-quest">—</div>
    </div>
    <div id="hud-right">
      <button id="btn-menu" class="chip">MENU</button>
      <button id="btn-mute" class="chip">♪</button>
    </div>
  </div>
  <div id="touch" class="hidden">
    <div id="dpad">
      <button class="tbtn" data-dir="up">▲</button>
      <button class="tbtn" data-dir="left">◀</button>
      <button class="tbtn" data-dir="down">▼</button>
      <button class="tbtn" data-dir="right">▶</button>
    </div>
    <div id="abtn">
      <button class="tbtn round" data-act="a">A</button>
      <button class="tbtn round" data-act="b">B</button>
    </div>
  </div>
  <div id="battle-ui" class="hidden"></div>
  <div id="dialogue" class="hidden"><div id="dlg-name"></div><div id="dlg-text"></div><div id="dlg-hint">▼ space / tap</div></div>
  <div id="modal-root" class="hidden"></div>
  <div id="cutin" class="hidden"><div id="cutin-inner"><div id="cutin-title"></div><div id="cutin-body"></div></div></div>
  <div id="toast-root"></div>
  <div id="title-screen">
    <div id="title-glow"></div>
    <div id="title-inner">
      <h1>AETHERBIND</h1>
      <h2>Chronicles of the Seven Resonances</h2>
      <div id="title-buttons">
        <button id="btn-new" class="bigbtn">New Binding</button>
        <button id="btn-continue" class="bigbtn ghost">Continue</button>
      </div>
      <p id="title-foot">The Song cracked. What answers you now must be bound, or it will unbind you.</p>
    </div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./runtime":"src/runtime.ts","./fx":"src/fx.ts","./audio":"src/audio.ts","./render":"src/render.ts","./battleui":"src/battleui.ts","./game":"src/game.ts","./ui":"src/ui.ts","./menus":"src/menus.ts"},"src/typechart.ts":{"./types":"src/types.ts"},"src/abilities.ts":{"./types":"src/types.ts"},"src/species.ts":{"./types":"src/types.ts"},"src/items.ts":{"./types":"src/types.ts"},"src/zones.ts":{"./types":"src/types.ts"},"src/creature.ts":{"./types":"src/types.ts","./species":"src/species.ts","./abilities":"src/abilities.ts"},"src/state.ts":{"./types":"src/types.ts","./creature":"src/creature.ts","./zones":"src/zones.ts"},"src/sprites.ts":{"./types":"src/types.ts","./util":"src/util.ts"},"src/battle.ts":{"./types":"src/types.ts","./abilities":"src/abilities.ts","./species":"src/species.ts","./state":"src/state.ts","./creature":"src/creature.ts","./typechart":"src/typechart.ts","./util":"src/util.ts"},"src/world.ts":{"./zones":"src/zones.ts","./util":"src/util.ts"},"src/fx.ts":{"./util":"src/util.ts"},"src/runtime.ts":{"./battle":"src/battle.ts","./types":"src/types.ts"},"src/render.ts":{"./state":"src/state.ts","./runtime":"src/runtime.ts","./world":"src/world.ts","./fx":"src/fx.ts","./util":"src/util.ts","./sprites":"src/sprites.ts","./species":"src/species.ts","./creature":"src/creature.ts","./battle":"src/battle.ts","./typechart":"src/typechart.ts"},"src/ui.ts":{"./audio":"src/audio.ts","./util":"src/util.ts"},"src/menus.ts":{"./state":"src/state.ts","./types":"src/types.ts","./species":"src/species.ts","./abilities":"src/abilities.ts","./items":"src/items.ts","./zones":"src/zones.ts","./typechart":"src/typechart.ts","./sprites":"src/sprites.ts","./creature":"src/creature.ts","./ui":"src/ui.ts","./util":"src/util.ts","./audio":"src/audio.ts","./battle":"src/battle.ts"},"src/battleui.ts":{"./types":"src/types.ts","./abilities":"src/abilities.ts","./species":"src/species.ts","./state":"src/state.ts","./creature":"src/creature.ts","./battle":"src/battle.ts","./runtime":"src/runtime.ts","./ui":"src/ui.ts","./menus":"src/menus.ts","./fx":"src/fx.ts","./render":"src/render.ts","./typechart":"src/typechart.ts","./audio":"src/audio.ts","./util":"src/util.ts"},"src/game.ts":{"./state":"src/state.ts","./types":"src/types.ts","./zones":"src/zones.ts","./species":"src/species.ts","./creature":"src/creature.ts","./world":"src/world.ts","./runtime":"src/runtime.ts","./fx":"src/fx.ts","./audio":"src/audio.ts","./ui":"src/ui.ts","./menus":"src/menus.ts","./battleui":"src/battleui.ts","./items":"src/items.ts","./util":"src/util.ts","./typechart":"src/typechart.ts","./sprites":"src/sprites.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const state_1 = require("./state");
const runtime_1 = require("./runtime");
const fx_1 = require("./fx");
const audio_1 = require("./audio");
const render_1 = require("./render");
const battleui_1 = require("./battleui");
const game_1 = require("./game");
const ui_1 = require("./ui");
const menus_1 = require("./menus");
// ------------------------------------------------------------------ input
const held = new Set();
let repeatT = 0;
const DIRS = {
    ArrowUp: [0, -1], KeyW: [0, -1],
    ArrowDown: [0, 1], KeyS: [0, 1],
    ArrowLeft: [-1, 0], KeyA: [-1, 0],
    ArrowRight: [1, 0], KeyD: [1, 0],
};
window.addEventListener('keydown', (e) => {
    if (e.repeat)
        return;
    const code = e.code;
    if (DIRS[code]) {
        e.preventDefault();
        held.add(code);
        if (runtime_1.RT.mode === 'field') {
            const d = DIRS[code];
            (0, game_1.tryMove)(d[0], d[1]);
            repeatT = 0.19;
        }
        return;
    }
    if (code === 'Space' || code === 'Enter' || code === 'KeyE') {
        if ((0, ui_1.dialogueOpen)()) {
            e.preventDefault();
            (0, ui_1.advanceDialogue)();
            return;
        }
        if (runtime_1.RT.mode === 'title') {
            e.preventDefault();
            startNew();
            return;
        }
        if (runtime_1.RT.mode === 'field' && !(0, ui_1.modalOpen)()) {
            e.preventDefault();
            (0, game_1.interact)();
            return;
        }
        return;
    }
    if (code === 'Escape') {
        e.preventDefault();
        if ((0, ui_1.modalOpen)()) {
            (0, ui_1.closeModal)();
            return;
        }
        if (runtime_1.RT.mode === 'field')
            (0, game_1.openPauseMenu)();
        return;
    }
    if (runtime_1.RT.mode !== 'field' || (0, ui_1.modalOpen)() || (0, ui_1.dialogueOpen)())
        return;
    if (code === 'KeyT')
        (0, menus_1.openTeam)();
    else if (code === 'KeyB')
        (0, menus_1.openBag)();
    else if (code === 'KeyC')
        (0, menus_1.openDex)();
    else if (code === 'KeyM')
        (0, menus_1.openMap)();
    else if (code === 'KeyN') {
        if ((0, state_1.saveGame)())
            (0, ui_1.toast)('Inscribed.');
    }
    else if (code === 'KeyP')
        (0, game_1.openPauseMenu)();
    else if (code === 'KeyK')
        toggleMute();
});
window.addEventListener('keyup', (e) => { held.delete(e.code); });
window.addEventListener('blur', () => held.clear());
function toggleMute() {
    state_1.G.muted = !state_1.G.muted;
    audio_1.AUD.setMuted(state_1.G.muted);
    (0, ui_1.$)('#btn-mute').textContent = state_1.G.muted ? '♪̶' : '♪';
    (0, ui_1.toast)(state_1.G.muted ? 'Silence held' : 'The Song resumes');
}
// touch pad
let touchDir = null;
document.querySelectorAll('#dpad .tbtn').forEach((b) => {
    const dir = b.dataset.dir;
    const map = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] };
    const start = (e) => { e.preventDefault(); touchDir = map[dir]; (0, game_1.tryMove)(map[dir][0], map[dir][1]); repeatT = 0.19; };
    const end = (e) => { e.preventDefault(); touchDir = null; };
    b.addEventListener('touchstart', start, { passive: false });
    b.addEventListener('touchend', end);
    b.addEventListener('mousedown', start);
    b.addEventListener('mouseup', end);
    b.addEventListener('mouseleave', end);
});
document.querySelectorAll('#abtn .tbtn').forEach((b) => {
    const act = b.dataset.act;
    b.addEventListener('click', (e) => {
        e.preventDefault();
        audio_1.AUD.ensure();
        if (act === 'a') {
            if ((0, ui_1.dialogueOpen)())
                (0, ui_1.advanceDialogue)();
            else
                (0, game_1.interact)();
        }
        else {
            if ((0, ui_1.modalOpen)())
                (0, ui_1.closeModal)();
            else
                (0, game_1.openPauseMenu)();
        }
    });
});
(0, ui_1.$)('#btn-menu').addEventListener('click', () => { audio_1.AUD.ensure(); (0, game_1.openPauseMenu)(); });
(0, ui_1.$)('#btn-mute').addEventListener('click', () => { audio_1.AUD.ensure(); toggleMute(); });
function startNew() {
    audio_1.AUD.ensure();
    audio_1.AUD.play('field');
    (0, game_1.beginNewGame)();
}
(0, ui_1.$)('#btn-new').addEventListener('click', () => startNew());
(0, ui_1.$)('#btn-continue').addEventListener('click', () => {
    audio_1.AUD.ensure();
    if (!(0, game_1.continueGame)())
        (0, ui_1.toast)('No inscription found.');
});
// ------------------------------------------------------------------ loop
let last = performance.now();
function frame(now) {
    const dt = Math.min(0.05, (now - last) / 1000);
    last = now;
    runtime_1.RT.time += dt;
    // held-key repeat movement
    if (runtime_1.RT.mode === 'field') {
        repeatT -= dt;
        if (repeatT <= 0) {
            let d = touchDir;
            if (!d) {
                for (const code of held) {
                    if (DIRS[code]) {
                        d = DIRS[code];
                        break;
                    }
                }
            }
            if (d) {
                (0, game_1.tryMove)(d[0], d[1]);
                repeatT = 0.145;
            }
        }
        if (runtime_1.RT.moveT < 1) {
            runtime_1.RT.moveT = Math.min(1, runtime_1.RT.moveT + dt * 7.2);
            if (runtime_1.RT.moveT >= 1)
                (0, game_1.onStepComplete)();
        }
        (0, render_1.spawnAmbient)(dt);
    }
    // battle anim decay
    const ba = runtime_1.RT.battleAnim;
    ba.allyFlash = Math.max(0, ba.allyFlash - dt * 3.2);
    ba.foeFlash = Math.max(0, ba.foeFlash - dt * 3.2);
    ba.allyShakeT = Math.max(0, ba.allyShakeT - dt);
    ba.foeShakeT = Math.max(0, ba.foeShakeT - dt);
    fx_1.fx.update(dt);
    state_1.G.playtime += dt;
    if (runtime_1.RT.mode === 'title')
        (0, render_1.drawTitleScene)(runtime_1.RT.time);
    else if (runtime_1.RT.mode === 'battle') {
        (0, render_1.drawBattle)(runtime_1.RT.time);
        (0, battleui_1.syncBars)();
    }
    else
        (0, render_1.drawField)(runtime_1.RT.time);
    requestAnimationFrame(frame);
}
// expose a small debug surface (useful for automated checks)
window.AB = { G: state_1.G, RT: runtime_1.RT, AUD: audio_1.AUD, fx: fx_1.fx };
function start() {
    (0, render_1.resize)();
    (0, game_1.refreshTitleButtons)();
    (0, ui_1.hide)((0, ui_1.$)('#hud'));
    (0, ui_1.hide)((0, ui_1.$)('#touch'));
    requestAnimationFrame(frame);
    // resume audio on first gesture
    const kick = () => { audio_1.AUD.ensure(); if (runtime_1.RT.mode === 'title')
        audio_1.AUD.play('title'); window.removeEventListener('pointerdown', kick); };
    window.addEventListener('pointerdown', kick);
    window.addEventListener('beforeunload', () => { if (runtime_1.RT.mode === 'field')
        (0, state_1.saveGame)(); });
    if ((0, state_1.hasSave)())
        (0, ui_1.$)('#title-foot').textContent = 'An inscription waits on the stele. Continue, or begin a new binding.';
}
start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ---- Core type declarations for the Aetherbind engine ----
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/typechart.ts ──
__mods["src/typechart.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AFFINITY_LORE = exports.AFFINITY_COLOR = exports.AFFINITIES = void 0;
exports.typeMult = typeMult;
exports.multLabel = multLabel;
exports.AFFINITIES = ['Ember', 'Briar', 'Tide', 'Gale', 'Lumen', 'Hush', 'Aeon'];
exports.AFFINITY_COLOR = {
    Ember: '#ff8a4c', Briar: '#a9e06a', Tide: '#54c8ff', Gale: '#c6b7ff',
    Lumen: '#ffe066', Hush: '#ff6fa8', Aeon: '#9fb2c9',
};
exports.AFFINITY_LORE = {
    Ember: 'The Resonance of appetite — heat that remembers what it consumed.',
    Briar: 'The Resonance of patience — growth that outlives the wound.',
    Tide: 'The Resonance of erosion — a soft voice that unmakes stone.',
    Gale: 'The Resonance of severance — it cuts flame from fuel and wave from shore.',
    Lumen: 'The Resonance of witness — nothing hidden survives being seen.',
    Hush: 'The Resonance of the crack in the Song — it answers every note with silence.',
    Aeon: 'The Resonance of duration — indifferent to all things but silence.',
};
// Ember > Briar > Tide > Ember ; Gale > Ember & Tide ; Lumen > Gale & Briar ;
// Hush beats everything slightly ; Aeon resists all but Hush.
const SUPER = 2.0, WEAK = 0.5, SLIGHT = 1.25, RESIST = 0.5, HUSH_ON_AEON = 2.0;
function typeMult(atk, def, def2) {
    let m = pair(atk, def);
    if (def2 && def2 !== def)
        m *= pair(atk, def2);
    return m;
}
function pair(a, d) {
    if (d === 'Aeon')
        return a === 'Hush' ? HUSH_ON_AEON : RESIST;
    if (a === 'Hush')
        return SLIGHT;
    if (a === 'Aeon')
        return 1;
    if (d === 'Hush')
        return 1;
    const wins = {
        Ember: ['Briar'],
        Briar: ['Tide'],
        Tide: ['Ember'],
        Gale: ['Ember', 'Tide'],
        Lumen: ['Gale', 'Briar'],
    };
    if ((wins[a] || []).includes(d))
        return SUPER;
    if ((wins[d] || []).includes(a))
        return WEAK;
    return 1;
}
function multLabel(m) {
    if (m >= 2)
        return 'Resonant!';
    if (m > 1)
        return 'Attuned';
    if (m === 1)
        return '';
    if (m > 0)
        return 'Dampened';
    return 'Null';
}
};

// ── module: src/abilities.ts ──
__mods["src/abilities.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ABILITY_MAP = exports.ABILITIES = void 0;
exports.ab = ab;
function A(id, name, affinity, power, accuracy, cost, maxPP, effect, blurb) {
    return { id, name, affinity, power, accuracy, cost, maxPP, effect, blurb };
}
exports.ABILITIES = [
    // ---------- Ember ----------
    A('cinderspit', 'Cinderspit', 'Ember', 38, 100, 1, 30, { kind: 'burn', chance: 0.18, turns: 3 }, 'A cough of live grit that sticks and smoulders.'),
    A('emberlash', 'Emberlash', 'Ember', 55, 95, 2, 22, { kind: 'burn', chance: 0.3, turns: 3 }, 'A whipcord of heat drawn along the ribs.'),
    A('kilnbreath', 'Kilnbreath', 'Ember', 78, 90, 3, 14, { kind: 'burn', chance: 0.45, turns: 4 }, 'Exhaled air hot enough to glaze sand.'),
    A('cauterize', 'Cauterize', 'Ember', 0, 100, 3, 10, { kind: 'heal', value: 0.45, onSelf: true }, 'Sealing a wound with the same fire that opened it.'),
    A('pyreknell', 'Pyre Knell', 'Ember', 118, 85, 5, 6, { kind: 'recoil', value: 0.22 }, 'A single ringing detonation of stored heat.'),
    A('sparkflurry', 'Sparkflurry', 'Ember', 22, 95, 2, 18, { kind: 'multi', value: 3 }, 'Three quick sparks, none of them polite.'),
    A('forgestance', 'Forgestance', 'Ember', 0, 100, 2, 14, { kind: 'atkUp', value: 2, onSelf: true }, 'The body treated as an anvil, then as a hammer.'),
    A('ashveil', 'Ashveil', 'Ember', 44, 100, 2, 16, { kind: 'defDown', chance: 0.6, value: 1 }, 'Fine ash grinds away a creature\u2019s guard.'),
    // ---------- Briar ----------
    A('thornflick', 'Thornflick', 'Briar', 36, 100, 1, 30, { kind: 'none' }, 'A barb loosed with idle precision.'),
    A('rootbind', 'Rootbind', 'Briar', 0, 95, 2, 16, { kind: 'spdDown', value: 2 }, 'Something below the floor takes an interest in ankles.'),
    A('sapleech', 'Sapleech', 'Briar', 52, 95, 3, 16, { kind: 'drain', value: 0.5 }, 'Borrowing vitality, with no plan to return it.'),
    A('bramblewave', 'Bramblewave', 'Briar', 74, 90, 3, 14, { kind: 'defDown', chance: 0.3, value: 1 }, 'A rolling swell of thicket and splinter.'),
    A('greenvigil', 'Green Vigil', 'Briar', 0, 100, 2, 12, { kind: 'regen', turns: 4, onSelf: true }, 'A slow patient hum that knits what was torn.'),
    A('seedvolley', 'Seedvolley', 'Briar', 20, 100, 2, 18, { kind: 'multi', value: 4 }, 'Four hard little futures fired at speed.'),
    A('barkmantle', 'Barkmantle', 'Briar', 0, 100, 2, 14, { kind: 'defUp', value: 2, onSelf: true }, 'The skin remembers it was once heartwood.'),
    A('heartwoodfell', 'Heartwood Fell', 'Briar', 112, 85, 5, 6, { kind: 'sap', chance: 0.4, turns: 3 }, 'The full weight of an old growth, dropped at once.'),
    // ---------- Tide ----------
    A('brinespit', 'Brinespit', 'Tide', 38, 100, 1, 30, { kind: 'none' }, 'Salt water delivered with prejudice.'),
    A('undertow', 'Undertow', 'Tide', 56, 95, 2, 20, { kind: 'spdDown', chance: 0.4, value: 1 }, 'The pull beneath the pull.'),
    A('rimefall', 'Rimefall', 'Tide', 62, 95, 3, 16, { kind: 'frost', chance: 0.5, turns: 4 }, 'Water taught to stop moving.'),
    A('glasscurrent', 'Glasscurrent', 'Tide', 80, 90, 3, 12, { kind: 'frost', chance: 0.25, turns: 3 }, 'A smooth cold flow that shears as it passes.'),
    A('tidalvow', 'Tidal Vow', 'Tide', 0, 100, 3, 10, { kind: 'heal', value: 0.5, onSelf: true }, 'Promising the sea a return, later.'),
    A('mistcoil', 'Mistcoil', 'Tide', 0, 100, 2, 14, { kind: 'spdUp', value: 2, onSelf: true }, 'Body loosened into vapour and momentum.'),
    A('deepknell', 'Deep Knell', 'Tide', 116, 85, 5, 6, { kind: 'defDown', chance: 0.35, value: 2 }, 'Pressure from a depth nothing should reach.'),
    A('spraylash', 'Spraylash', 'Tide', 24, 100, 2, 18, { kind: 'multi', value: 3 }, 'Three stinging arcs of fine cold spray.'),
    // ---------- Gale ----------
    A('cutwind', 'Cutwind', 'Gale', 40, 100, 1, 30, { kind: 'none' }, 'Air folded until it has an edge.'),
    A('quickveer', 'Quickveer', 'Gale', 34, 100, 2, 20, { kind: 'priority', value: 1 }, 'Arriving before the intent to arrive.'),
    A('shearsong', 'Shearsong', 'Gale', 60, 95, 2, 18, { kind: 'flinch', chance: 0.3 }, 'A note so thin it startles the nerve.'),
    A('stormspiral', 'Stormspiral', 'Gale', 82, 88, 3, 12, { kind: 'shock', chance: 0.35, turns: 3 }, 'A tight ascending coil of charged air.'),
    A('hollowdraft', 'Hollowdraft', 'Gale', 0, 95, 2, 14, { kind: 'atkDown', value: 2 }, 'Stealing the breath that a blow needs.'),
    A('featherstep', 'Featherstep', 'Gale', 0, 100, 2, 12, { kind: 'spdUp', value: 2, onSelf: true }, 'Weight politely declined.'),
    A('skysunder', 'Skysunder', 'Gale', 120, 82, 5, 6, { kind: 'critUp', value: 1 }, 'The seam of the sky, pulled apart briefly.'),
    A('gustvolley', 'Gustvolley', 'Gale', 21, 100, 2, 18, { kind: 'multi', value: 4 }, 'Four small pressures in fast sequence.'),
    // ---------- Lumen ----------
    A('glintray', 'Glintray', 'Lumen', 40, 100, 1, 30, { kind: 'none' }, 'A needle of noticing.'),
    A('revealbrand', 'Reveal Brand', 'Lumen', 54, 100, 2, 18, { kind: 'pierce' }, 'Light that ignores whatever was raised against it.'),
    A('sunveil', 'Sunveil', 'Lumen', 0, 100, 3, 10, { kind: 'barrier', turns: 3, onSelf: true }, 'A pane of gathered brightness, good for one blow.'),
    A('coronabloom', 'Coronabloom', 'Lumen', 76, 92, 3, 14, { kind: 'shock', chance: 0.3, turns: 3 }, 'Brightness opening like a slow flower.'),
    A('daybreakhymn', 'Daybreak Hymn', 'Lumen', 0, 100, 3, 8, { kind: 'cleanse', onSelf: true, value: 0.25 }, 'Sung once, and the ailment forgets its name.'),
    A('mirrorlance', 'Mirrorlance', 'Lumen', 66, 95, 3, 14, { kind: 'critUp', value: 1 }, 'Your own shape, sharpened and returned.'),
    A('zenithfall', 'Zenithfall', 'Lumen', 122, 84, 5, 6, { kind: 'defDown', chance: 0.3, value: 1 }, 'Noon arriving all at once, from directly above.'),
    A('prismguard', 'Prismguard', 'Lumen', 0, 100, 3, 10, { kind: 'shieldAll', turns: 2 }, 'Light split across the whole binding for a moment.'),
    // ---------- Hush ----------
    A('nullnip', 'Nullnip', 'Hush', 42, 100, 1, 26, { kind: 'none' }, 'A small bite out of the world\u2019s noise.'),
    A('quietbleed', 'Quietbleed', 'Hush', 50, 95, 2, 18, { kind: 'sap', chance: 0.5, turns: 3 }, 'The wound makes no sound, which is worse.'),
    A('unsong', 'Unsong', 'Hush', 70, 92, 3, 14, { kind: 'atkDown', chance: 0.5, value: 1 }, 'A phrase played backwards until it hurts.'),
    A('voidfold', 'Voidfold', 'Hush', 88, 88, 4, 10, { kind: 'pierce' }, 'Space creased so the blow skips the armour.'),
    A('silencecrown', 'Silence Crown', 'Hush', 0, 100, 3, 10, { kind: 'atkUp', value: 3, onSelf: true }, 'Wearing the gap in the Song like a diadem.'),
    A('lastchord', 'Last Chord', 'Hush', 130, 80, 6, 5, { kind: 'recoil', value: 0.3 }, 'The note the Song ends on, borrowed early.'),
    A('echofracture', 'Echofracture', 'Hush', 32, 100, 3, 12, { kind: 'echo', value: 2 }, 'One strike that insists on happening twice.'),
    A('dimmingtouch', 'Dimming Touch', 'Hush', 46, 95, 2, 16, { kind: 'shock', chance: 0.4, turns: 3 }, 'Contact that briefly unplugs the will.'),
    // ---------- Aeon ----------
    A('gravechime', 'Gravechime', 'Aeon', 40, 100, 1, 30, { kind: 'none' }, 'A slow ring from something that has waited.'),
    A('stoneoath', 'Stone Oath', 'Aeon', 0, 100, 2, 14, { kind: 'defUp', value: 3, onSelf: true }, 'A promise made in a material that keeps them.'),
    A('agecrush', 'Agecrush', 'Aeon', 84, 90, 3, 12, { kind: 'spdDown', chance: 0.5, value: 1 }, 'Centuries of accumulated weight, applied.'),
    A('slowsiege', 'Slowsiege', 'Aeon', 58, 100, 2, 18, { kind: 'defDown', chance: 0.45, value: 1 }, 'Patient pressure at a single seam.'),
    A('memorywell', 'Memory Well', 'Aeon', 0, 100, 3, 10, { kind: 'heal', value: 0.55, onSelf: true }, 'Drinking from a version of yourself that was whole.'),
    A('duststorm', 'Duststorm', 'Aeon', 26, 95, 2, 16, { kind: 'multi', value: 3 }, 'Three passes of grit that has outlived mountains.'),
    A('epochbreak', 'Epochbreak', 'Aeon', 124, 82, 5, 6, { kind: 'flinch', chance: 0.3 }, 'An era ending against a single body.'),
    A('warddawn', 'Warddawn', 'Aeon', 0, 100, 3, 10, { kind: 'barrier', turns: 4, onSelf: true }, 'A stillness that deflects exactly one intention.'),
    A('tetherpull', 'Tetherpull', 'Aeon', 48, 100, 2, 16, { kind: 'drain', value: 0.4 }, 'Time siphoned back along the binding thread.'),
];
exports.ABILITY_MAP = Object.fromEntries(exports.ABILITIES.map((a) => [a.id, a]));
function ab(id) {
    const a = exports.ABILITY_MAP[id];
    if (!a)
        throw new Error('unknown ability ' + id);
    return a;
}
};

// ── module: src/species.ts ──
__mods["src/species.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STARTERS = exports.SPECIES_MAP = exports.SPECIES = void 0;
exports.sp = sp;
function S(s) { return s; }
exports.SPECIES = [
    // ================= EMBER =================
    S({
        id: 'wickling', name: 'Wickling', affinity: 'Ember', stage: 1, rarity: 2,
        base: { hp: 44, atk: 30, def: 24, spd: 28 }, yieldExp: 62, glyph: 'beast', hueA: 22, hueB: 44,
        lore: 'Hatched from a candle that refused to be snuffed. It sleeps curled around whatever it most wants to keep warm.',
        learn: [
            { lv: 1, ability: 'cinderspit' }, { lv: 1, ability: 'forgestance' }, { lv: 6, ability: 'sparkflurry' },
            { lv: 11, ability: 'emberlash' }, { lv: 16, ability: 'ashveil' }, { lv: 22, ability: 'cauterize' },
            { lv: 28, ability: 'kilnbreath' }, { lv: 38, ability: 'pyreknell' },
        ],
        form: { into: 'pyrarch', level: 16 },
    }),
    S({
        id: 'pyrarch', name: 'Pyrarch', affinity: 'Ember', affinity2: 'Lumen', stage: 2, rarity: 4,
        base: { hp: 76, atk: 58, def: 46, spd: 52 }, yieldExp: 158, glyph: 'beast', hueA: 14, hueB: 52,
        lore: 'A Wickling that finally chose a thing worth burning for. Its mane throws light that reads as approval.',
        learn: [
            { lv: 1, ability: 'emberlash' }, { lv: 1, ability: 'glintray' }, { lv: 18, ability: 'kilnbreath' },
            { lv: 24, ability: 'coronabloom' }, { lv: 30, ability: 'cauterize' }, { lv: 36, ability: 'mirrorlance' },
            { lv: 42, ability: 'pyreknell' }, { lv: 48, ability: 'zenithfall' },
        ],
    }),
    S({
        id: 'coalmaw', name: 'Coalmaw', affinity: 'Ember', stage: 1, rarity: 2,
        base: { hp: 52, atk: 34, def: 32, spd: 18 }, yieldExp: 70, glyph: 'quad', hueA: 8, hueB: 30,
        lore: 'It eats spent embers and stores the heat in a gut of packed slag. Prospectors follow its trail to find old fires.',
        learn: [
            { lv: 1, ability: 'cinderspit' }, { lv: 1, ability: 'gravechime' }, { lv: 8, ability: 'ashveil' },
            { lv: 14, ability: 'slowsiege' }, { lv: 20, ability: 'emberlash' }, { lv: 26, ability: 'stoneoath' },
            { lv: 32, ability: 'kilnbreath' }, { lv: 40, ability: 'agecrush' },
        ],
        form: { into: 'slagthane', level: 22, catalyst: 'cat_emberglass' },
    }),
    S({
        id: 'slagthane', name: 'Slagthane', affinity: 'Ember', affinity2: 'Aeon', stage: 2, rarity: 4,
        base: { hp: 92, atk: 62, def: 70, spd: 30 }, yieldExp: 172, glyph: 'quad', hueA: 4, hueB: 36,
        lore: 'Its shell cooled into a map of every furnace it has outlasted. It moves as though the ground owes it money.',
        learn: [
            { lv: 1, ability: 'kilnbreath' }, { lv: 1, ability: 'stoneoath' }, { lv: 26, ability: 'agecrush' },
            { lv: 32, ability: 'warddawn' }, { lv: 38, ability: 'pyreknell' }, { lv: 44, ability: 'epochbreak' },
        ],
    }),
    S({
        id: 'fennaught', name: 'Fennaught', affinity: 'Ember', affinity2: 'Hush', stage: 1, rarity: 3,
        base: { hp: 58, atk: 52, def: 40, spd: 58 }, yieldExp: 128, glyph: 'beast', hueA: 350, hueB: 30,
        lore: 'A heat that arrived from the wrong side of the Song. Where it walks, the crackling sound comes a moment late.',
        learn: [
            { lv: 1, ability: 'nullnip' }, { lv: 1, ability: 'emberlash' }, { lv: 12, ability: 'sparkflurry' },
            { lv: 18, ability: 'dimmingtouch' }, { lv: 25, ability: 'unsong' }, { lv: 33, ability: 'kilnbreath' },
            { lv: 41, ability: 'lastchord' },
        ],
    }),
    // ================= TIDE =================
    S({
        id: 'brinelet', name: 'Brinelet', affinity: 'Tide', stage: 1, rarity: 2,
        base: { hp: 46, atk: 26, def: 30, spd: 26 }, yieldExp: 62, glyph: 'serp', hueA: 190, hueB: 210,
        lore: 'Born in a tidepool that held a single note for a century. It hums that note when content.',
        learn: [
            { lv: 1, ability: 'brinespit' }, { lv: 1, ability: 'mistcoil' }, { lv: 6, ability: 'spraylash' },
            { lv: 11, ability: 'undertow' }, { lv: 16, ability: 'rimefall' }, { lv: 22, ability: 'tidalvow' },
            { lv: 29, ability: 'glasscurrent' }, { lv: 38, ability: 'deepknell' },
        ],
        form: { into: 'thalassine', level: 16 },
    }),
    S({
        id: 'thalassine', name: 'Thalassine', affinity: 'Tide', affinity2: 'Briar', stage: 2, rarity: 4,
        base: { hp: 80, atk: 50, def: 56, spd: 48 }, yieldExp: 158, glyph: 'serp', hueA: 178, hueB: 130,
        lore: 'Its trailing fronds are older than its body, seeded from drowned gardens. It grows a little every tide.',
        learn: [
            { lv: 1, ability: 'undertow' }, { lv: 1, ability: 'thornflick' }, { lv: 18, ability: 'sapleech' },
            { lv: 24, ability: 'glasscurrent' }, { lv: 30, ability: 'greenvigil' }, { lv: 36, ability: 'bramblewave' },
            { lv: 43, ability: 'deepknell' }, { lv: 49, ability: 'heartwoodfell' },
        ],
    }),
    S({
        id: 'frostmurmur', name: 'Frostmurmur', affinity: 'Tide', stage: 1, rarity: 3,
        base: { hp: 48, atk: 40, def: 34, spd: 44 }, yieldExp: 88, glyph: 'wisp', hueA: 200, hueB: 240,
        lore: 'A rumour of cold that learned to hold a shape. It repeats the last word spoken near it, slowly.',
        learn: [
            { lv: 1, ability: 'brinespit' }, { lv: 1, ability: 'rimefall' }, { lv: 10, ability: 'undertow' },
            { lv: 16, ability: 'mistcoil' }, { lv: 23, ability: 'glasscurrent' }, { lv: 30, ability: 'tidalvow' },
            { lv: 39, ability: 'deepknell' },
        ],
        form: { into: 'glaciarch', level: 24, catalyst: 'cat_rimewater' },
    }),
    S({
        id: 'glaciarch', name: 'Glaciarch', affinity: 'Tide', affinity2: 'Aeon', stage: 2, rarity: 4,
        base: { hp: 86, atk: 60, def: 66, spd: 52 }, yieldExp: 176, glyph: 'wisp', hueA: 196, hueB: 258,
        lore: 'The rumour, confirmed. Inside its chest a single drop of water has been falling for nine hundred years.',
        learn: [
            { lv: 1, ability: 'glasscurrent' }, { lv: 1, ability: 'stoneoath' }, { lv: 28, ability: 'agecrush' },
            { lv: 34, ability: 'warddawn' }, { lv: 40, ability: 'deepknell' }, { lv: 46, ability: 'epochbreak' },
        ],
    }),
    S({
        id: 'kelpwhisper', name: 'Kelpwhisper', affinity: 'Tide', affinity2: 'Briar', stage: 1, rarity: 2,
        base: { hp: 62, atk: 34, def: 48, spd: 22 }, yieldExp: 84, glyph: 'bulb', hueA: 160, hueB: 100,
        lore: 'A drifting mat of listening fronds. Sailors leave it questions and return years later for the answers.',
        learn: [
            { lv: 1, ability: 'thornflick' }, { lv: 1, ability: 'brinespit' }, { lv: 9, ability: 'rootbind' },
            { lv: 15, ability: 'sapleech' }, { lv: 21, ability: 'barkmantle' }, { lv: 28, ability: 'undertow' },
            { lv: 35, ability: 'bramblewave' }, { lv: 44, ability: 'heartwoodfell' },
        ],
    }),
    // ================= BRIAR =================
    S({
        id: 'sprigling', name: 'Sprigling', affinity: 'Briar', stage: 1, rarity: 2,
        base: { hp: 50, atk: 28, def: 30, spd: 22 }, yieldExp: 62, glyph: 'bulb', hueA: 96, hueB: 76,
        lore: 'It carries a seed it will never plant, in case the world needs a spare.',
        learn: [
            { lv: 1, ability: 'thornflick' }, { lv: 1, ability: 'barkmantle' }, { lv: 6, ability: 'seedvolley' },
            { lv: 11, ability: 'rootbind' }, { lv: 16, ability: 'sapleech' }, { lv: 22, ability: 'greenvigil' },
            { lv: 29, ability: 'bramblewave' }, { lv: 38, ability: 'heartwoodfell' },
        ],
        form: { into: 'thornwarden', level: 16 },
    }),
    S({
        id: 'thornwarden', name: 'Thornwarden', affinity: 'Briar', affinity2: 'Aeon', stage: 2, rarity: 4,
        base: { hp: 84, atk: 54, def: 62, spd: 40 }, yieldExp: 158, glyph: 'bulb', hueA: 88, hueB: 40,
        lore: 'The spare seed germinated inside its ribs. Now it guards a garden only it can see.',
        learn: [
            { lv: 1, ability: 'bramblewave' }, { lv: 1, ability: 'stoneoath' }, { lv: 20, ability: 'greenvigil' },
            { lv: 26, ability: 'slowsiege' }, { lv: 33, ability: 'warddawn' }, { lv: 40, ability: 'heartwoodfell' },
            { lv: 47, ability: 'epochbreak' },
        ],
    }),
    S({
        id: 'mossgrub', name: 'Mossgrub', affinity: 'Briar', stage: 1, rarity: 1,
        base: { hp: 56, atk: 30, def: 40, spd: 16 }, yieldExp: 54, glyph: 'serp', hueA: 110, hueB: 84,
        lore: 'It digests stone into soil at the rate of a fingernail per year. Utterly unhurried, utterly certain.',
        learn: [
            { lv: 1, ability: 'thornflick' }, { lv: 1, ability: 'rootbind' }, { lv: 8, ability: 'seedvolley' },
            { lv: 14, ability: 'barkmantle' }, { lv: 20, ability: 'sapleech' }, { lv: 27, ability: 'greenvigil' },
            { lv: 34, ability: 'bramblewave' },
        ],
    }),
    S({
        id: 'petalvane', name: 'Petalvane', affinity: 'Briar', affinity2: 'Gale', stage: 1, rarity: 3,
        base: { hp: 52, atk: 46, def: 34, spd: 62 }, yieldExp: 118, glyph: 'moth', hueA: 320, hueB: 96,
        lore: 'Blossoms arranged as a weathervane. It always faces the direction the Song is thinnest.',
        learn: [
            { lv: 1, ability: 'cutwind' }, { lv: 1, ability: 'thornflick' }, { lv: 10, ability: 'featherstep' },
            { lv: 16, ability: 'seedvolley' }, { lv: 22, ability: 'shearsong' }, { lv: 30, ability: 'bramblewave' },
            { lv: 38, ability: 'skysunder' },
        ],
    }),
    // ================= GALE =================
    S({
        id: 'zephrit', name: 'Zephrit', affinity: 'Gale', stage: 1, rarity: 2,
        base: { hp: 42, atk: 36, def: 24, spd: 50 }, yieldExp: 74, glyph: 'avian', hueA: 260, hueB: 210,
        lore: 'A draught with opinions. It nests in doorways and resents being closed.',
        learn: [
            { lv: 1, ability: 'cutwind' }, { lv: 1, ability: 'quickveer' }, { lv: 7, ability: 'gustvolley' },
            { lv: 13, ability: 'featherstep' }, { lv: 20, ability: 'shearsong' }, { lv: 26, ability: 'hollowdraft' },
            { lv: 33, ability: 'stormspiral' }, { lv: 42, ability: 'skysunder' },
        ],
        form: { into: 'skyrend', level: 20 },
    }),
    S({
        id: 'skyrend', name: 'Skyrend', affinity: 'Gale', affinity2: 'Hush', stage: 2, rarity: 4,
        base: { hp: 70, atk: 66, def: 42, spd: 82 }, yieldExp: 170, glyph: 'avian', hueA: 272, hueB: 330,
        lore: 'It outgrew doorways and took up the horizon instead. Its wake carries a silence the ear mistakes for altitude.',
        learn: [
            { lv: 1, ability: 'shearsong' }, { lv: 1, ability: 'nullnip' }, { lv: 22, ability: 'stormspiral' },
            { lv: 28, ability: 'echofracture' }, { lv: 34, ability: 'voidfold' }, { lv: 41, ability: 'skysunder' },
            { lv: 48, ability: 'lastchord' },
        ],
    }),
    S({
        id: 'ventmote', name: 'Ventmote', affinity: 'Gale', stage: 1, rarity: 1,
        base: { hp: 38, atk: 32, def: 22, spd: 54 }, yieldExp: 50, glyph: 'wisp', hueA: 230, hueB: 190,
        lore: 'Barely a creature — a knot of moving air that has decided to persist. Persistence counts for a lot.',
        learn: [
            { lv: 1, ability: 'cutwind' }, { lv: 1, ability: 'featherstep' }, { lv: 8, ability: 'quickveer' },
            { lv: 15, ability: 'gustvolley' }, { lv: 22, ability: 'hollowdraft' }, { lv: 30, ability: 'stormspiral' },
        ],
    }),
    // ================= LUMEN =================
    S({
        id: 'glimmerfawn', name: 'Glimmerfawn', affinity: 'Lumen', stage: 1, rarity: 2,
        base: { hp: 46, atk: 32, def: 28, spd: 40 }, yieldExp: 76, glyph: 'quad', hueA: 50, hueB: 60,
        lore: 'It grazes on reflections. Where it has fed, mirrors stay honest for a week.',
        learn: [
            { lv: 1, ability: 'glintray' }, { lv: 1, ability: 'daybreakhymn' }, { lv: 9, ability: 'revealbrand' },
            { lv: 15, ability: 'sunveil' }, { lv: 22, ability: 'mirrorlance' }, { lv: 29, ability: 'coronabloom' },
            { lv: 36, ability: 'prismguard' }, { lv: 44, ability: 'zenithfall' },
        ],
        form: { into: 'aurelith', level: 22, catalyst: 'cat_prismcore' },
    }),
    S({
        id: 'aurelith', name: 'Aurelith', affinity: 'Lumen', affinity2: 'Aeon', stage: 2, rarity: 4,
        base: { hp: 82, atk: 60, def: 58, spd: 62 }, yieldExp: 180, glyph: 'quad', hueA: 44, hueB: 30,
        lore: 'Its antlers hold a standing wave of noon. Binders use it as a lantern and a lie detector both.',
        learn: [
            { lv: 1, ability: 'coronabloom' }, { lv: 1, ability: 'warddawn' }, { lv: 26, ability: 'mirrorlance' },
            { lv: 32, ability: 'prismguard' }, { lv: 38, ability: 'agecrush' }, { lv: 45, ability: 'zenithfall' },
        ],
    }),
    S({
        id: 'lampwing', name: 'Lampwing', affinity: 'Lumen', affinity2: 'Gale', stage: 1, rarity: 3,
        base: { hp: 44, atk: 48, def: 30, spd: 66 }, yieldExp: 122, glyph: 'moth', hueA: 56, hueB: 262,
        lore: 'Moth-shaped and faintly ceremonial. It escorts the lost, though not always toward safety.',
        learn: [
            { lv: 1, ability: 'glintray' }, { lv: 1, ability: 'cutwind' }, { lv: 11, ability: 'quickveer' },
            { lv: 17, ability: 'revealbrand' }, { lv: 24, ability: 'shearsong' }, { lv: 32, ability: 'mirrorlance' },
            { lv: 40, ability: 'zenithfall' },
        ],
    }),
    // ================= HUSH =================
    S({
        id: 'nullkin', name: 'Nullkin', affinity: 'Hush', stage: 1, rarity: 3,
        base: { hp: 44, atk: 44, def: 28, spd: 46 }, yieldExp: 96, glyph: 'wisp', hueA: 330, hueB: 290,
        lore: 'A child of the crack. It is polite, curious, and cannot be recorded by any means.',
        learn: [
            { lv: 1, ability: 'nullnip' }, { lv: 1, ability: 'quietbleed' }, { lv: 10, ability: 'dimmingtouch' },
            { lv: 17, ability: 'echofracture' }, { lv: 24, ability: 'unsong' }, { lv: 31, ability: 'voidfold' },
            { lv: 39, ability: 'silencecrown' }, { lv: 46, ability: 'lastchord' },
        ],
        form: { into: 'voidchoir', level: 26, catalyst: 'cat_silence' },
    }),
    S({
        id: 'voidchoir', name: 'Voidchoir', affinity: 'Hush', stage: 2, rarity: 4,
        base: { hp: 78, atk: 78, def: 48, spd: 68 }, yieldExp: 196, glyph: 'wisp', hueA: 315, hueB: 268,
        lore: 'Many Nullkin, agreeing. What they agree on has never been translated and probably should not be.',
        learn: [
            { lv: 1, ability: 'voidfold' }, { lv: 1, ability: 'silencecrown' }, { lv: 30, ability: 'echofracture' },
            { lv: 36, ability: 'unsong' }, { lv: 42, ability: 'quietbleed' }, { lv: 48, ability: 'lastchord' },
        ],
    }),
    S({
        id: 'mutewisp', name: 'Mutewisp', affinity: 'Hush', affinity2: 'Tide', stage: 1, rarity: 2,
        base: { hp: 48, atk: 38, def: 34, spd: 42 }, yieldExp: 82, glyph: 'wisp', hueA: 300, hueB: 200,
        lore: 'It drifts where sound has pooled and drinks it. Rooms it has visited feel newly furnished with quiet.',
        learn: [
            { lv: 1, ability: 'nullnip' }, { lv: 1, ability: 'brinespit' }, { lv: 9, ability: 'rimefall' },
            { lv: 16, ability: 'quietbleed' }, { lv: 23, ability: 'dimmingtouch' }, { lv: 31, ability: 'unsong' },
            { lv: 40, ability: 'voidfold' },
        ],
    }),
    // ================= AEON =================
    S({
        id: 'runeshell', name: 'Runeshell', affinity: 'Aeon', stage: 1, rarity: 2,
        base: { hp: 58, atk: 26, def: 50, spd: 14 }, yieldExp: 72, glyph: 'shell', hueA: 210, hueB: 40,
        lore: 'Its plates are inscribed with a language that predates mouths. It has never been in a hurry.',
        learn: [
            { lv: 1, ability: 'gravechime' }, { lv: 1, ability: 'stoneoath' }, { lv: 8, ability: 'slowsiege' },
            { lv: 14, ability: 'duststorm' }, { lv: 21, ability: 'warddawn' }, { lv: 28, ability: 'agecrush' },
            { lv: 35, ability: 'memorywell' }, { lv: 43, ability: 'epochbreak' },
        ],
        form: { into: 'monolithe', level: 24, catalyst: 'cat_fossil' },
    }),
    S({
        id: 'monolithe', name: 'Monolithe', affinity: 'Aeon', stage: 2, rarity: 4,
        base: { hp: 104, atk: 56, def: 88, spd: 26 }, yieldExp: 190, glyph: 'shell', hueA: 216, hueB: 26,
        lore: 'The inscription finished itself. Reading the last line aloud is discouraged in seven provinces.',
        learn: [
            { lv: 1, ability: 'agecrush' }, { lv: 1, ability: 'warddawn' }, { lv: 28, ability: 'memorywell' },
            { lv: 34, ability: 'tetherpull' }, { lv: 40, ability: 'slowsiege' }, { lv: 47, ability: 'epochbreak' },
        ],
    }),
    S({
        id: 'dustsage', name: 'Dustsage', affinity: 'Aeon', affinity2: 'Lumen', stage: 1, rarity: 3,
        base: { hp: 54, atk: 42, def: 46, spd: 34 }, yieldExp: 116, glyph: 'orb', hueA: 40, hueB: 200,
        lore: 'A slow orbit of grit around a lit centre. It answers questions with the year they were first asked.',
        learn: [
            { lv: 1, ability: 'gravechime' }, { lv: 1, ability: 'glintray' }, { lv: 10, ability: 'duststorm' },
            { lv: 17, ability: 'revealbrand' }, { lv: 24, ability: 'memorywell' }, { lv: 32, ability: 'prismguard' },
            { lv: 40, ability: 'epochbreak' },
        ],
    }),
    S({
        id: 'cragfoal', name: 'Cragfoal', affinity: 'Aeon', affinity2: 'Ember', stage: 1, rarity: 2,
        base: { hp: 56, atk: 44, def: 44, spd: 36 }, yieldExp: 100, glyph: 'quad', hueA: 20, hueB: 210,
        lore: 'Volcanic scree given four legs and a stubborn temper. Its hoofprints cool into small black coins.',
        learn: [
            { lv: 1, ability: 'gravechime' }, { lv: 1, ability: 'cinderspit' }, { lv: 10, ability: 'slowsiege' },
            { lv: 17, ability: 'ashveil' }, { lv: 24, ability: 'agecrush' }, { lv: 31, ability: 'kilnbreath' },
            { lv: 40, ability: 'epochbreak' },
        ],
    }),
];
exports.SPECIES_MAP = Object.fromEntries(exports.SPECIES.map((s) => [s.id, s]));
function sp(id) {
    const s = exports.SPECIES_MAP[id];
    if (!s)
        throw new Error('unknown species ' + id);
    return s;
}
exports.STARTERS = ['wickling', 'brinelet', 'sprigling'];
};

// ── module: src/items.ts ──
__mods["src/items.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SHOP_STOCK_BY_ZONE = exports.ITEM_MAP = exports.ITEMS = void 0;
exports.it = it;
exports.ITEMS = [
    // --- Binding vessels (3 tiers) ---
    { id: 'bind_clay', name: 'Clay Reliquary', kind: 'bind', power: 1.0, price: 180, battle: true,
        desc: 'A fired vessel with a hairline seam. Cheap, honest, and only occasionally sufficient.' },
    { id: 'bind_glass', name: 'Glass Reliquary', kind: 'bind', power: 1.6, price: 520, battle: true,
        desc: 'Blown from lens-sand. The creature can see out, which apparently helps.' },
    { id: 'bind_choir', name: 'Choirshell Reliquary', kind: 'bind', power: 2.6, price: 1400, battle: true,
        desc: 'Grown, not made. It sings the target\u2019s own resonance back at it until agreement occurs.' },
    // --- Restoratives ---
    { id: 'res_salve', name: 'Sunmoss Salve', kind: 'restore', power: 45, price: 120, battle: true,
        desc: 'Restores 45 Vitality to one bound creature.' },
    { id: 'res_draught', name: 'Deepwell Draught', kind: 'restore', power: 130, price: 380, battle: true,
        desc: 'Restores 130 Vitality. Tastes of cold stone and old rain.' },
    { id: 'res_hymnal', name: 'Whole Hymnal', kind: 'restore', power: 9999, price: 1100, battle: true,
        desc: 'Restores a creature completely by reminding it of its intact self.' },
    { id: 'res_tonic', name: 'Clearnote Tonic', kind: 'restore', power: 0, price: 200, battle: true,
        desc: 'Clears Scorch, Rime, Stutter and Sapping from one creature.' },
    { id: 'res_ember', name: 'Second Kindling', kind: 'restore', power: -1, price: 900, battle: true,
        desc: 'Rouses one unbound-out creature to half Vitality.' },
    // --- Catalysts ---
    { id: 'cat_emberglass', name: 'Emberglass Shard', kind: 'catalyst', price: 950,
        desc: 'Slag cooled too fast. Held by an Ember-kin of sufficient age, it forces a reshaping.' },
    { id: 'cat_rimewater', name: 'Rimewater Vial', kind: 'catalyst', price: 950,
        desc: 'Water that froze while falling and never noticed. A catalyst for Tide-kin.' },
    { id: 'cat_prismcore', name: 'Prismcore', kind: 'catalyst', price: 950,
        desc: 'A splinter of standing noon. Lumen-kin reshape around it eagerly.' },
    { id: 'cat_fossil', name: 'Long Fossil', kind: 'catalyst', price: 950,
        desc: 'A bone with a date carved by nobody. Aeon-kin recognise it as family.' },
    { id: 'cat_silence', name: 'Bottled Silence', kind: 'catalyst', price: 1250,
        desc: 'An absence, corked. Hush-kin will do almost anything to be near it.' },
    // --- Field tools ---
    { id: 'tool_lure', name: 'Resonance Lure', kind: 'tool', price: 260,
        desc: 'Doubles wild encounter frequency for 120 paces.' },
    { id: 'tool_ward', name: 'Quietstep Ward', kind: 'tool', price: 260,
        desc: 'Suppresses wild encounters for 120 paces.' },
    { id: 'tool_tuner', name: 'Field Tuner', kind: 'tool', price: 340, battle: true,
        desc: 'Widens the binding window on the next attempt this battle.' },
    { id: 'tool_chart', name: 'Wayfarer\u2019s Chart', kind: 'tool', price: 0,
        desc: 'Shows the Threadwork of every zone you have entered.' },
];
exports.ITEM_MAP = Object.fromEntries(exports.ITEMS.map((i) => [i.id, i]));
function it(id) {
    const i = exports.ITEM_MAP[id];
    if (!i)
        throw new Error('unknown item ' + id);
    return i;
}
exports.SHOP_STOCK_BY_ZONE = {
    0: ['bind_clay', 'res_salve', 'res_tonic'],
    1: ['bind_clay', 'res_salve', 'res_tonic', 'tool_lure', 'tool_ward'],
    2: ['bind_clay', 'bind_glass', 'res_salve', 'res_draught', 'res_tonic', 'tool_lure', 'tool_ward', 'tool_tuner'],
    3: ['bind_glass', 'res_draught', 'res_tonic', 'res_ember', 'cat_emberglass', 'cat_rimewater', 'tool_tuner'],
    4: ['bind_glass', 'bind_choir', 'res_draught', 'res_hymnal', 'res_ember', 'cat_prismcore', 'cat_fossil', 'tool_tuner'],
    5: ['bind_choir', 'res_hymnal', 'res_ember', 'res_draught', 'cat_silence', 'cat_emberglass', 'cat_rimewater', 'cat_prismcore', 'cat_fossil'],
};
};

// ── module: src/zones.ts ──
__mods["src/zones.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STORY_STAGES = exports.ZONES = void 0;
exports.ZONES = [
    {
        id: 0, name: 'Hushfall Hollow', epithet: 'where the first note went missing',
        gradA: '#1b2a3f', gradB: '#243d4e', gradC: '#0b1220', tint: 'rgba(90,170,220,.22)',
        encounterRate: 0.055, levels: [3, 6], particle: 'motes',
        ground: '#2c4a52', accent: '#3f6b6b', prop: '#7fd0c4',
        spawns: ['ventmote', 'mossgrub', 'sprigling', 'brinelet', 'wickling'],
        width: 34, height: 26,
        guardian: {
            name: 'Warden Coil', anchor: false, boon: 'Warden\u2019s Ear', boonDesc: '+8% binding success, always.',
            team: [{ id: 'mossgrub', lv: 8 }, { id: 'kelpwhisper', lv: 10 }],
        },
        storyNode: {
            name: 'The Hollow Stele',
            text: [
                'A stele stands where the ground forgot to finish. Its face is scored with seven grooves; six of them still hum.',
                'BINDER, reads the carving, THE SONG IS NOT A SOUND. IT IS AN AGREEMENT. HOLD YOUR END OF IT.',
                'Something small watches you from the reeds and does not run.',
            ],
        },
    },
    {
        id: 1, name: 'Emberscar Flats', epithet: 'a burn that kept its shape',
        gradA: '#3d1a12', gradB: '#7a2f16', gradC: '#180608', tint: 'rgba(255,120,60,.24)',
        encounterRate: 0.075, levels: [8, 14], particle: 'ash',
        ground: '#4a2418', accent: '#7c3a1e', prop: '#ffa552',
        spawns: ['wickling', 'coalmaw', 'cragfoal', 'ventmote', 'fennaught'],
        width: 38, height: 28,
        guardian: {
            name: 'Anchor of Ash', anchor: true, boon: 'Cinderheart', boonDesc: 'Party gains +6% ATK permanently.',
            team: [{ id: 'coalmaw', lv: 15 }, { id: 'cragfoal', lv: 16 }, { id: 'slagthane', lv: 18 }],
        },
        storyNode: {
            name: 'The First Anchor',
            text: [
                'The Flats do not smoke. They simply refuse to cool — a wound held open by something buried and patient.',
                'Four Anchors were driven into the world to keep the Song from unravelling. They have begun to hum in the wrong key.',
                'Whatever is pulling on them from the Hush side is getting closer.',
            ],
        },
    },
    {
        id: 2, name: 'Glasswater Shelf', epithet: 'a tide that stopped mid-sentence',
        gradA: '#0e2a3d', gradB: '#1d5f7a', gradC: '#04121c', tint: 'rgba(90,210,255,.24)',
        encounterRate: 0.07, levels: [14, 20], particle: 'spray',
        ground: '#1b4552', accent: '#2b7086', prop: '#8ce8ff',
        spawns: ['brinelet', 'frostmurmur', 'kelpwhisper', 'mutewisp', 'thalassine'],
        width: 40, height: 28,
        guardian: {
            name: 'Anchor of Rime', anchor: true, boon: 'Stillwater Lung', boonDesc: 'Party gains +8% max Vitality permanently.',
            team: [{ id: 'frostmurmur', lv: 20 }, { id: 'kelpwhisper', lv: 21 }, { id: 'glaciarch', lv: 24 }],
        },
        storyNode: {
            name: 'The Frozen Question',
            text: [
                'A wave stands upright here, clear as a pane, holding a shoal of fish that have not aged in two centuries.',
                'A binder\u2019s coat floats inside it. The pockets are full of unsent letters addressed to the Hush.',
                '"WE ASKED IT WHAT IT WANTED," says the last one. "IT SAID: EVERYTHING, EVENTUALLY, QUIETLY."',
            ],
        },
    },
    {
        id: 3, name: 'Thornwrit Deep', epithet: 'a library that grew back',
        gradA: '#152a19', gradB: '#2f5c2a', gradC: '#08130b', tint: 'rgba(140,230,110,.22)',
        encounterRate: 0.08, levels: [20, 27], particle: 'pollen',
        ground: '#23412a', accent: '#376b3c', prop: '#b6f07a',
        spawns: ['sprigling', 'mossgrub', 'petalvane', 'thornwarden', 'kelpwhisper', 'nullkin'],
        width: 42, height: 30,
        guardian: {
            name: 'Anchor of Root', anchor: true, boon: 'Deepbound Weave', boonDesc: 'Party gains +8% DEF permanently.',
            team: [{ id: 'petalvane', lv: 26 }, { id: 'mossgrub', lv: 27 }, { id: 'thornwarden', lv: 30 }],
        },
        storyNode: {
            name: 'The Regrown Archive',
            text: [
                'Every trunk here is a spine, and every spine holds a bark-page account of a binding that failed.',
                'The oldest page is the shortest: SEVEN RESONANCES. SIX WILL ARGUE. THE SEVENTH ONLY LISTENS, AND SUBTRACTS.',
                'Somewhere deeper, a page turns itself.',
            ],
        },
    },
    {
        id: 4, name: 'Ventspire Reach', epithet: 'where noon was nailed to the sky',
        gradA: '#2a1f4a', gradB: '#6a4fa8', gradC: '#0c0820', tint: 'rgba(200,160,255,.26)',
        encounterRate: 0.075, levels: [27, 35], particle: 'gust',
        ground: '#33305e', accent: '#554c8c', prop: '#ffe89a',
        spawns: ['zephrit', 'lampwing', 'glimmerfawn', 'dustsage', 'skyrend', 'petalvane'],
        width: 44, height: 30,
        guardian: {
            name: 'Anchor of Noon', anchor: true, boon: 'Standing Zenith', boonDesc: 'Party gains +8% SPD and +5% crit permanently.',
            team: [{ id: 'lampwing', lv: 33 }, { id: 'skyrend', lv: 34 }, { id: 'aurelith', lv: 37 }],
        },
        storyNode: {
            name: 'The Nailed Hour',
            text: [
                'The spires hold up a sky that has been noon for as long as anyone can prove. Nothing here casts a shadow it agreed to.',
                'From this height you can see the Hush: not darkness, but a place where the world has been carefully, courteously deleted.',
                'It is not spreading. It is being pulled — and something on the far side is doing the pulling.',
            ],
        },
    },
    {
        id: 5, name: 'The Silent Coda', epithet: 'the last measure of the Song',
        gradA: '#241028', gradB: '#3d1740', gradC: '#060309', tint: 'rgba(255,110,180,.26)',
        encounterRate: 0.1, levels: [33, 44], particle: 'null',
        ground: '#2a1830', accent: '#48254e', prop: '#ff9ecb',
        spawns: ['nullkin', 'mutewisp', 'fennaught', 'voidchoir', 'skyrend', 'dustsage'],
        width: 40, height: 28,
        guardian: {
            name: 'The Unsong', anchor: false, boon: 'Coda Keeper', boonDesc: 'Post-Coda roaming unlocked; wild kin grow bolder.',
            team: [{ id: 'mutewisp', lv: 38 }, { id: 'fennaught', lv: 40 }, { id: 'skyrend', lv: 42 }, { id: 'voidchoir', lv: 45 }],
        },
        storyNode: {
            name: 'The Seventh Groove',
            text: [
                'The seventh groove on the Hollow Stele was not empty. It was full of a thing that makes no sound.',
                'THE SONG IS AN AGREEMENT, says the Unsong, in the shape of your own voice. I NEVER SIGNED.',
                'Hold your end of it, Binder. Everything else here has let go.',
            ],
        },
    },
];
exports.STORY_STAGES = [
    { name: 'I — The Loose Thread', text: 'A note has gone missing from the Song, and the world is quietly balancing its books.' },
    { name: 'II — The Humming Anchors', text: 'Two Anchors quieted. The other two have begun to hum in a key that hurts to hear.' },
    { name: 'III — The Courteous Deletion', text: 'The Hush does not destroy. It negotiates, and it always wins the second round.' },
    { name: 'IV — Hold Your End', text: 'Four Anchors stilled. The Coda opens. Bring everything you have bound.' },
];
};

// ── module: src/creature.ts ──
__mods["src/creature.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NO_BONUS = exports.LEVEL_CAP = void 0;
exports.nextUid = nextUid;
exports.seedUid = seedUid;
exports.expForLevel = expForLevel;
exports.expToNext = expToNext;
exports.expProgress = expProgress;
exports.statAt = statAt;
exports.stats = stats;
exports.maxHp = maxHp;
exports.learnsetUpTo = learnsetUpTo;
exports.makeCreature = makeCreature;
exports.ppMaxOf = ppMaxOf;
exports.healFull = healFull;
exports.isFainted = isFainted;
exports.displayName = displayName;
exports.newLearns = newLearns;
exports.canTransform = canTransform;
exports.transform = transform;
const species_1 = require("./species");
exports.LEVEL_CAP = 50;
let uidSeq = 1;
function nextUid() { return uidSeq++; }
function seedUid(n) { uidSeq = Math.max(uidSeq, n + 1); }
function expForLevel(lv) {
    // cumulative exp required to BE level lv
    return Math.floor(0.8 * Math.pow(lv - 1, 3) + 6 * Math.pow(lv - 1, 2) + 12 * (lv - 1));
}
function expToNext(c) {
    if (c.level >= exports.LEVEL_CAP)
        return 0;
    return Math.max(1, expForLevel(c.level + 1) - expForLevel(c.level));
}
function expProgress(c) {
    if (c.level >= exports.LEVEL_CAP)
        return 1;
    const base = expForLevel(c.level);
    return Math.min(1, Math.max(0, (c.exp - base) / Math.max(1, expForLevel(c.level + 1) - base)));
}
exports.NO_BONUS = { atk: 0, def: 0, spd: 0, hp: 0, crit: 0, bind: 0 };
function statAt(base, level, kind) {
    if (kind === 'hp')
        return Math.floor((base * level) / 22) + level + 10;
    return Math.floor((base * level) / 45 + level * 0.4) + 5;
}
function stats(c, b = exports.NO_BONUS) {
    const s = (0, species_1.sp)(c.speciesId).base;
    const bondF = 1 + c.bond * 0.0004;
    return {
        hp: Math.floor(statAt(s.hp, c.level, 'hp') * (1 + b.hp)),
        atk: Math.floor(statAt(s.atk, c.level, 'other') * (1 + b.atk) * bondF),
        def: Math.floor(statAt(s.def, c.level, 'other') * (1 + b.def) * bondF),
        spd: Math.floor(statAt(s.spd, c.level, 'other') * (1 + b.spd) * bondF),
    };
}
function maxHp(c, b = exports.NO_BONUS) { return stats(c, b).hp; }
function learnsetUpTo(species, level) {
    return species.learn.filter((l) => l.lv <= level).map((l) => l.ability);
}
function makeCreature(speciesId, level) {
    const s = (0, species_1.sp)(speciesId);
    const known = learnsetUpTo(s, level);
    const abilities = known.slice(-4);
    const c = {
        uid: nextUid(), speciesId, level, exp: expForLevel(level), hp: 0,
        abilities, pp: [], statuses: [], bond: 0,
    };
    c.pp = abilities.map((id) => ppMaxOf(id));
    c.hp = maxHp(c);
    return c;
}
const abilities_1 = require("./abilities");
function ppMaxOf(id) { return (0, abilities_1.ab)(id).maxPP; }
function healFull(c, b = exports.NO_BONUS) {
    c.hp = maxHp(c, b);
    c.statuses = [];
    c.pp = c.abilities.map((id) => ppMaxOf(id));
}
function isFainted(c) { return c.hp <= 0; }
function displayName(c) { return c.nickname || (0, species_1.sp)(c.speciesId).name; }
/** Returns list of newly learnable abilities gained crossing from oldLv to newLv. */
function newLearns(speciesId, oldLv, newLv) {
    return (0, species_1.sp)(speciesId).learn.filter((l) => l.lv > oldLv && l.lv <= newLv).map((l) => l.ability);
}
function canTransform(c, hasCatalyst) {
    const f = (0, species_1.sp)(c.speciesId).form;
    if (!f)
        return false;
    if (c.level < f.level)
        return false;
    if (f.catalyst && !hasCatalyst(f.catalyst))
        return false;
    return true;
}
function transform(c, b = exports.NO_BONUS) {
    const f = (0, species_1.sp)(c.speciesId).form;
    if (!f)
        return null;
    const ratio = c.hp / maxHp(c, b);
    c.speciesId = f.into;
    const known = learnsetUpTo((0, species_1.sp)(f.into), c.level);
    for (const id of known.slice(-2)) {
        if (!c.abilities.includes(id)) {
            if (c.abilities.length < 4) {
                c.abilities.push(id);
                c.pp.push(ppMaxOf(id));
            }
            else {
                c.abilities[3] = id;
                c.pp[3] = ppMaxOf(id);
            }
        }
    }
    c.hp = Math.max(1, Math.round(maxHp(c, b) * ratio));
    return f.into;
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.G = exports.SAVE_KEY = void 0;
exports.freshState = freshState;
exports.setState = setState;
exports.resetState = resetState;
exports.addItem = addItem;
exports.takeItem = takeItem;
exports.countItem = countItem;
exports.markSeen = markSeen;
exports.markBound = markBound;
exports.zoneDef = zoneDef;
exports.saveGame = saveGame;
exports.loadGame = loadGame;
exports.hasSave = hasSave;
exports.partyAlive = partyAlive;
exports.totalGoalProgress = totalGoalProgress;
exports.applyBoon = applyBoon;
const creature_1 = require("./creature");
const zones_1 = require("./zones");
exports.SAVE_KEY = 'aetherbind.save.v1';
function freshState() {
    return {
        version: 1, playerName: 'Binder', party: [], vault: [], bag: {}, shards: 600,
        zone: 0, px: 8, py: 18, unlockedZones: [0], bonuses: { ...creature_1.NO_BONUS }, boons: [],
        guardiansBeaten: [], anchorsStilled: 0, storyStage: 0, seenStory: [], seenCutins: [],
        dex: {}, uniqueBound: [], tutorial: 0, steps: 0, lureSteps: 0, wardSteps: 0,
        postgame: false, muted: false, lastShrine: { zone: 0, x: 8, y: 18 }, uidSeq: 1, playtime: 0,
    };
}
exports.G = freshState();
function setState(s) {
    // mutate in place so every module's imported binding stays valid
    for (const k of Object.keys(exports.G))
        delete exports.G[k];
    Object.assign(exports.G, s);
    (0, creature_1.seedUid)(s.uidSeq);
}
function resetState() { setState(freshState()); }
function addItem(id, n = 1) { exports.G.bag[id] = (exports.G.bag[id] || 0) + n; }
function takeItem(id, n = 1) {
    if ((exports.G.bag[id] || 0) < n)
        return false;
    exports.G.bag[id] -= n;
    if (exports.G.bag[id] <= 0)
        delete exports.G.bag[id];
    return true;
}
function countItem(id) { return exports.G.bag[id] || 0; }
function markSeen(speciesId) {
    const e = exports.G.dex[speciesId] || { seen: false, bound: false };
    e.seen = true;
    exports.G.dex[speciesId] = e;
}
function markBound(speciesId) {
    const e = exports.G.dex[speciesId] || { seen: false, bound: false };
    e.seen = true;
    e.bound = true;
    exports.G.dex[speciesId] = e;
    if (!exports.G.uniqueBound.includes(speciesId))
        exports.G.uniqueBound.push(speciesId);
}
function zoneDef(i = exports.G.zone) { return zones_1.ZONES[Math.max(0, Math.min(zones_1.ZONES.length - 1, i))]; }
function saveGame(slot = 1) {
    try {
        exports.G.uidSeq = Math.max(...[1, ...exports.G.party.map((c) => c.uid), ...exports.G.vault.map((c) => c.uid)]) + 1;
        localStorage.setItem(exports.SAVE_KEY + '.' + slot, JSON.stringify(exports.G));
        return true;
    }
    catch {
        return false;
    }
}
function loadGame(slot = 1) {
    try {
        const raw = localStorage.getItem(exports.SAVE_KEY + '.' + slot);
        if (!raw)
            return false;
        const parsed = JSON.parse(raw);
        if (!parsed || !Array.isArray(parsed.party))
            return false;
        const base = freshState();
        setState({ ...base, ...parsed, bonuses: { ...base.bonuses, ...(parsed.bonuses || {}) } });
        return true;
    }
    catch {
        return false;
    }
}
function hasSave(slot = 1) {
    try {
        return !!localStorage.getItem(exports.SAVE_KEY + '.' + slot);
    }
    catch {
        return false;
    }
}
function partyAlive() { return exports.G.party.filter((c) => c.hp > 0); }
function totalGoalProgress() {
    return { bound: exports.G.uniqueBound.length, anchors: exports.G.anchorsStilled };
}
function applyBoon(kind, amt) {
    exports.G.bonuses[kind] += amt;
}
};

// ── module: src/util.ts ──
__mods["src/util.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rnd = rnd;
exports.ri = ri;
exports.pick = pick;
exports.chance = chance;
exports.clamp = clamp;
exports.lerp = lerp;
exports.shuffle = shuffle;
exports.hash2 = hash2;
exports.valueNoise = valueNoise;
exports.fbm = fbm;
exports.wait = wait;
exports.esc = esc;
function rnd(a = 1, b) {
    if (b === undefined)
        return Math.random() * a;
    return a + Math.random() * (b - a);
}
function ri(a, b) { return Math.floor(rnd(a, b + 1)); }
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function chance(p) { return Math.random() < p; }
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function lerp(a, b, t) { return a + (b - a) * t; }
function shuffle(a) {
    const r = a.slice();
    for (let i = r.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [r[i], r[j]] = [r[j], r[i]];
    }
    return r;
}
function hash2(x, y, seed = 1337) {
    let h = x * 374761393 + y * 668265263 + seed * 1442695040888963407;
    h = (h ^ (h >> 13)) * 1274126177;
    return ((h ^ (h >> 16)) & 0x7fffffff) / 0x7fffffff;
}
function valueNoise(x, y, seed = 7) {
    const xi = Math.floor(x), yi = Math.floor(y);
    const xf = x - xi, yf = y - yi;
    const s = (t) => t * t * (3 - 2 * t);
    const u = s(xf), v = s(yf);
    const a = hash2(xi, yi, seed), b = hash2(xi + 1, yi, seed);
    const c = hash2(xi, yi + 1, seed), d = hash2(xi + 1, yi + 1, seed);
    return lerp(lerp(a, b, u), lerp(c, d, u), v);
}
function fbm(x, y, seed = 7, oct = 3) {
    let sum = 0, amp = 0.5, f = 1;
    for (let i = 0; i < oct; i++) {
        sum += valueNoise(x * f, y * f, seed + i * 31) * amp;
        amp *= 0.5;
        f *= 2;
    }
    return sum * 1.6;
}
function wait(ms) { return new Promise((r) => setTimeout(r, ms)); }
function esc(s) {
    return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AUD = void 0;
class Audio {
    constructor() {
        this.ctx = null;
        this.master = null;
        this.musicGain = null;
        this.sfxGain = null;
        this.muted = false;
        this.mode = 'none';
        this.timer = null;
        this.step = 0;
        // ---- generative loop ----
        this.scales = {
            title: [0, 3, 5, 7, 10, 12, 15],
            field: [0, 2, 4, 7, 9, 12, 14],
            battle: [0, 2, 3, 7, 8, 12, 14],
            boss: [0, 1, 3, 6, 7, 10, 13],
            none: [0],
        };
        this.roots = { title: 220, field: 196, battle: 165, boss: 138, none: 220 };
    }
    ensure() {
        if (this.ctx) {
            if (this.ctx.state === 'suspended')
                void this.ctx.resume();
            return true;
        }
        try {
            const AC = window.AudioContext
                || window.webkitAudioContext;
            this.ctx = new AC();
            this.master = this.ctx.createGain();
            this.master.gain.value = this.muted ? 0 : 0.55;
            this.master.connect(this.ctx.destination);
            this.musicGain = this.ctx.createGain();
            this.musicGain.gain.value = 0.36;
            this.musicGain.connect(this.master);
            this.sfxGain = this.ctx.createGain();
            this.sfxGain.gain.value = 0.7;
            this.sfxGain.connect(this.master);
            return true;
        }
        catch {
            return false;
        }
    }
    setMuted(m) {
        this.muted = m;
        if (this.master && this.ctx) {
            this.master.gain.cancelScheduledValues(this.ctx.currentTime);
            this.master.gain.linearRampToValueAtTime(m ? 0 : 0.55, this.ctx.currentTime + 0.2);
        }
    }
    // ---- one-shot tone ----
    tone(freq, dur, type = 'square', gain = 0.2, glide) {
        if (!this.ensure() || !this.ctx || !this.sfxGain)
            return;
        const t = this.ctx.currentTime;
        const o = this.ctx.createOscillator();
        const g = this.ctx.createGain();
        o.type = type;
        o.frequency.setValueAtTime(freq, t);
        if (glide !== undefined)
            o.frequency.exponentialRampToValueAtTime(Math.max(30, glide), t + dur);
        g.gain.setValueAtTime(0, t);
        g.gain.linearRampToValueAtTime(gain, t + 0.008);
        g.gain.exponentialRampToValueAtTime(0.0008, t + dur);
        o.connect(g);
        g.connect(this.sfxGain);
        o.start(t);
        o.stop(t + dur + 0.02);
    }
    noise(dur, gain = 0.16, hp = 400) {
        if (!this.ensure() || !this.ctx || !this.sfxGain)
            return;
        const t = this.ctx.currentTime;
        const len = Math.max(1, Math.floor(this.ctx.sampleRate * dur));
        const buf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < len; i++)
            d[i] = (Math.random() * 2 - 1) * (1 - i / len);
        const src = this.ctx.createBufferSource();
        src.buffer = buf;
        const f = this.ctx.createBiquadFilter();
        f.type = 'highpass';
        f.frequency.value = hp;
        const g = this.ctx.createGain();
        g.gain.value = gain;
        src.connect(f);
        f.connect(g);
        g.connect(this.sfxGain);
        src.start(t);
    }
    sfx(name) {
        switch (name) {
            case 'ui':
                this.tone(660, 0.06, 'square', 0.12);
                break;
            case 'back':
                this.tone(330, 0.07, 'square', 0.1);
                break;
            case 'step':
                this.tone(160, 0.035, 'triangle', 0.05);
                break;
            case 'hit':
                this.noise(0.13, 0.2, 900);
                this.tone(180, 0.1, 'sawtooth', 0.14, 90);
                break;
            case 'crit':
                this.noise(0.2, 0.28, 1400);
                this.tone(880, 0.16, 'square', 0.16, 220);
                break;
            case 'super':
                this.tone(1200, 0.2, 'square', 0.14, 400);
                this.noise(0.22, 0.2, 1600);
                break;
            case 'weak':
                this.tone(200, 0.14, 'sine', 0.12, 140);
                break;
            case 'heal':
                this.tone(520, 0.1, 'sine', 0.15);
                setTimeout(() => this.tone(780, 0.14, 'sine', 0.13), 90);
                break;
            case 'faint':
                this.tone(300, 0.5, 'triangle', 0.16, 60);
                break;
            case 'levelup':
                [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => this.tone(f, 0.15, 'square', 0.14), i * 90));
                break;
            case 'bind':
                [440, 550, 660].forEach((f, i) => setTimeout(() => this.tone(f, 0.1, 'sine', 0.14), i * 70));
                break;
            case 'bound':
                [659, 784, 988, 1319].forEach((f, i) => setTimeout(() => this.tone(f, 0.18, 'triangle', 0.16), i * 110));
                break;
            case 'fail':
                this.tone(300, 0.22, 'sawtooth', 0.14, 120);
                break;
            case 'encounter':
                [220, 330, 220, 440].forEach((f, i) => setTimeout(() => this.tone(f, 0.12, 'square', 0.16), i * 80));
                break;
            case 'tick':
                this.tone(1400, 0.03, 'square', 0.08);
                break;
            case 'transform':
                [392, 523, 659, 784, 1047, 1319].forEach((f, i) => setTimeout(() => this.tone(f, 0.22, 'triangle', 0.15), i * 130));
                break;
        }
    }
    play(mode) {
        if (mode === this.mode)
            return;
        this.mode = mode;
        if (this.timer !== null) {
            clearInterval(this.timer);
            this.timer = null;
        }
        if (mode === 'none')
            return;
        if (!this.ensure())
            return;
        this.step = 0;
        const bpm = mode === 'battle' ? 132 : mode === 'boss' ? 148 : mode === 'title' ? 74 : 96;
        const iv = (60 / bpm) * 1000 / 2;
        this.timer = window.setInterval(() => this.tick(), iv);
    }
    tick() {
        if (!this.ctx || !this.musicGain)
            return;
        const m = this.mode;
        const sc = this.scales[m], root = this.roots[m];
        const s = this.step++;
        const t = this.ctx.currentTime;
        const semi = (n) => root * Math.pow(2, n / 12);
        // bass
        if (s % 4 === 0) {
            const bn = sc[(Math.floor(s / 16) * 2) % sc.length];
            this.voice(semi(bn) / 2, 0.42, 'triangle', 0.16, t);
        }
        // pad chord
        if (s % 16 === 0) {
            const base = sc[(Math.floor(s / 16) * 2) % sc.length];
            [0, 4, 7].forEach((iv2, i) => this.voice(semi(base + iv2), 2.2, 'sine', 0.055, t + i * 0.02, true));
        }
        // arp / melody
        const density = m === 'battle' || m === 'boss' ? 0.85 : m === 'title' ? 0.4 : 0.55;
        if (Math.random() < density) {
            const n = sc[Math.floor(Math.random() * sc.length)] + (Math.random() < 0.3 ? 12 : 0);
            this.voice(semi(n) * 2, m === 'title' ? 0.55 : 0.2, m === 'boss' ? 'square' : 'square', 0.05, t);
        }
        // percussion
        if (m === 'battle' || m === 'boss') {
            if (s % 4 === 0)
                this.perc(0.09, 0.14, 60);
            if (s % 4 === 2)
                this.perc(0.05, 0.09, 3000);
        }
    }
    voice(freq, dur, type, gain, t, pad = false) {
        if (!this.ctx || !this.musicGain)
            return;
        const o = this.ctx.createOscillator();
        const g = this.ctx.createGain();
        const f = this.ctx.createBiquadFilter();
        f.type = 'lowpass';
        f.frequency.value = pad ? 900 : 2600;
        o.type = type;
        o.frequency.value = freq;
        g.gain.setValueAtTime(0, t);
        g.gain.linearRampToValueAtTime(gain, t + (pad ? 0.4 : 0.02));
        g.gain.exponentialRampToValueAtTime(0.0006, t + dur);
        o.connect(f);
        f.connect(g);
        g.connect(this.musicGain);
        o.start(t);
        o.stop(t + dur + 0.05);
    }
    perc(dur, gain, hp) {
        if (!this.ctx || !this.musicGain)
            return;
        const t = this.ctx.currentTime;
        const len = Math.max(1, Math.floor(this.ctx.sampleRate * dur));
        const buf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < len; i++)
            d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2);
        const src = this.ctx.createBufferSource();
        src.buffer = buf;
        const f = this.ctx.createBiquadFilter();
        f.type = hp > 500 ? 'highpass' : 'lowpass';
        f.frequency.value = hp;
        const g = this.ctx.createGain();
        g.gain.value = gain;
        src.connect(f);
        f.connect(g);
        g.connect(this.musicGain);
        src.start(t);
    }
}
exports.AUD = new Audio();
};

// ── module: src/sprites.ts ──
__mods["src/sprites.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.drawCreature = drawCreature;
exports.creatureIcon = creatureIcon;
const util_1 = require("./util");
/** Deterministic procedural creature rendering. All shapes are drawn in a
 *  unit box (-1..1 both axes) then scaled — so the same species always looks the same. */
function grad(ctx, s, r) {
    const g = ctx.createLinearGradient(-r, -r, r, r);
    g.addColorStop(0, `hsl(${s.hueA} 85% 68%)`);
    g.addColorStop(0.55, `hsl(${(s.hueA + s.hueB) / 2} 72% 52%)`);
    g.addColorStop(1, `hsl(${s.hueB} 68% 34%)`);
    return g;
}
function eye(ctx, x, y, r, hue) {
    ctx.fillStyle = '#0a0a14';
    ctx.beginPath();
    ctx.ellipse(x, y, r, r * 1.15, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = `hsl(${hue} 100% 82%)`;
    ctx.beginPath();
    ctx.arc(x + r * 0.28, y - r * 0.3, r * 0.42, 0, Math.PI * 2);
    ctx.fill();
}
/** draw creature centered at (cx,cy) with radius R. facing: 1 right, -1 left. */
function drawCreature(ctx, s, cx, cy, R, facing, t, alpha = 1) {
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.translate(cx, cy);
    ctx.scale(facing, 1);
    const bob = Math.sin(t * 2.2 + s.hueA) * R * 0.045;
    ctx.translate(0, bob);
    const body = grad(ctx, s, R);
    const outline = `hsl(${s.hueB} 60% 16%)`;
    ctx.lineWidth = Math.max(1.4, R * 0.055);
    ctx.strokeStyle = outline;
    ctx.lineJoin = 'round';
    // soft aura
    const aura = ctx.createRadialGradient(0, 0, R * 0.2, 0, 0, R * 1.5);
    aura.addColorStop(0, `hsla(${s.hueA},90%,65%,0.28)`);
    aura.addColorStop(1, 'hsla(0,0%,0%,0)');
    ctx.fillStyle = aura;
    ctx.beginPath();
    ctx.arc(0, 0, R * 1.5, 0, Math.PI * 2);
    ctx.fill();
    const sd = (0, util_1.hash2)(s.hueA, s.hueB, s.name.length * 17);
    switch (s.glyph) {
        case 'beast': {
            // haunches + head + mane
            ctx.fillStyle = body;
            ctx.beginPath();
            ctx.ellipse(-R * 0.12, R * 0.18, R * 0.62, R * 0.44, -0.08, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            // legs
            ctx.beginPath();
            for (const lx of [-R * 0.5, -R * 0.1, R * 0.28]) {
                ctx.moveTo(lx, R * 0.5);
                ctx.lineTo(lx + R * 0.05, R * 0.92);
            }
            ctx.lineWidth = R * 0.14;
            ctx.strokeStyle = `hsl(${s.hueB} 55% 30%)`;
            ctx.stroke();
            ctx.lineWidth = Math.max(1.4, R * 0.055);
            ctx.strokeStyle = outline;
            // tail flame
            ctx.beginPath();
            ctx.moveTo(-R * 0.7, R * 0.05);
            ctx.quadraticCurveTo(-R * (1.05 + Math.sin(t * 3) * 0.06), -R * 0.35, -R * 0.5, -R * 0.62);
            ctx.quadraticCurveTo(-R * 0.62, -R * 0.15, -R * 0.7, R * 0.05);
            ctx.fillStyle = `hsl(${s.hueA} 95% 62%)`;
            ctx.fill();
            // head
            ctx.beginPath();
            ctx.ellipse(R * 0.5, -R * 0.22, R * 0.33, R * 0.3, 0.1, 0, Math.PI * 2);
            ctx.fillStyle = body;
            ctx.fill();
            ctx.stroke();
            // ears
            ctx.beginPath();
            ctx.moveTo(R * 0.38, -R * 0.46);
            ctx.lineTo(R * 0.3, -R * 0.85);
            ctx.lineTo(R * 0.6, -R * 0.5);
            ctx.moveTo(R * 0.66, -R * 0.42);
            ctx.lineTo(R * 0.82, -R * 0.78);
            ctx.lineTo(R * 0.8, -R * 0.34);
            ctx.fillStyle = `hsl(${s.hueA} 80% 50%)`;
            ctx.fill();
            ctx.stroke();
            eye(ctx, R * 0.62, -R * 0.26, R * 0.075, s.hueA);
            break;
        }
        case 'quad': {
            ctx.fillStyle = body;
            ctx.beginPath();
            ctx.ellipse(0, R * 0.1, R * 0.58, R * 0.36, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            ctx.lineWidth = R * 0.11;
            ctx.strokeStyle = `hsl(${s.hueB} 55% 28%)`;
            ctx.beginPath();
            for (const lx of [-R * 0.42, -R * 0.14, R * 0.16, R * 0.42]) {
                ctx.moveTo(lx, R * 0.38);
                ctx.lineTo(lx, R * 0.9);
            }
            ctx.stroke();
            ctx.lineWidth = Math.max(1.4, R * 0.055);
            ctx.strokeStyle = outline;
            // neck + head
            ctx.beginPath();
            ctx.moveTo(R * 0.32, -R * 0.05);
            ctx.quadraticCurveTo(R * 0.72, -R * 0.3, R * 0.68, -R * 0.62);
            ctx.lineTo(R * 0.42, -R * 0.5);
            ctx.quadraticCurveTo(R * 0.44, -R * 0.2, R * 0.2, R * 0.02);
            ctx.fillStyle = body;
            ctx.fill();
            ctx.stroke();
            ctx.beginPath();
            ctx.ellipse(R * 0.62, -R * 0.68, R * 0.24, R * 0.19, 0.25, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            // antlers / horns
            ctx.lineWidth = R * 0.06;
            ctx.strokeStyle = `hsl(${s.hueA} 90% 70%)`;
            ctx.beginPath();
            ctx.moveTo(R * 0.55, -R * 0.84);
            ctx.lineTo(R * 0.4, -R * 1.16);
            ctx.moveTo(R * 0.47, -R * 1.0);
            ctx.lineTo(R * 0.22, -R * 1.04);
            ctx.moveTo(R * 0.7, -R * 0.84);
            ctx.lineTo(R * 0.82, -R * 1.14);
            ctx.moveTo(R * 0.77, -R * 1.0);
            ctx.lineTo(R * 1.0, -R * 1.0);
            ctx.stroke();
            ctx.lineWidth = Math.max(1.4, R * 0.055);
            ctx.strokeStyle = outline;
            eye(ctx, R * 0.71, -R * 0.7, R * 0.06, s.hueA);
            break;
        }
        case 'serp': {
            ctx.strokeStyle = body;
            ctx.lineWidth = R * 0.34;
            ctx.lineCap = 'round';
            ctx.beginPath();
            ctx.moveTo(-R * 0.85, R * 0.6);
            for (let i = 0; i <= 20; i++) {
                const p = i / 20;
                const x = -R * 0.85 + p * R * 1.5;
                const y = R * 0.6 - p * R * 1.0 + Math.sin(p * 6 + t * 2.4) * R * 0.2;
                ctx.lineTo(x, y);
            }
            ctx.stroke();
            ctx.strokeStyle = outline;
            ctx.lineWidth = Math.max(1.4, R * 0.05);
            // head
            ctx.beginPath();
            ctx.ellipse(R * 0.66, -R * 0.44 + Math.sin(6 + t * 2.4) * R * 0.2, R * 0.28, R * 0.21, -0.35, 0, Math.PI * 2);
            ctx.fillStyle = `hsl(${s.hueA} 85% 62%)`;
            ctx.fill();
            ctx.stroke();
            // fins
            ctx.beginPath();
            ctx.moveTo(R * 0.42, -R * 0.62);
            ctx.lineTo(R * 0.28, -R * 0.98);
            ctx.lineTo(R * 0.6, -R * 0.72);
            ctx.fillStyle = `hsl(${s.hueB} 80% 55%)`;
            ctx.fill();
            ctx.stroke();
            eye(ctx, R * 0.74, -R * 0.48 + Math.sin(6 + t * 2.4) * R * 0.2, R * 0.06, s.hueA);
            break;
        }
        case 'avian': {
            const flap = Math.sin(t * 5) * 0.5;
            ctx.fillStyle = `hsl(${s.hueB} 70% 46%)`;
            for (const dir of [-1, 1]) {
                ctx.beginPath();
                ctx.moveTo(0, -R * 0.1);
                ctx.quadraticCurveTo(dir * R * 0.8, -R * (0.7 + flap * dir * 0.3), dir * R * 1.15, -R * (0.15 + flap * 0.25));
                ctx.quadraticCurveTo(dir * R * 0.6, R * 0.18, 0, R * 0.12);
                ctx.fill();
                ctx.stroke();
            }
            ctx.fillStyle = body;
            ctx.beginPath();
            ctx.ellipse(0, R * 0.05, R * 0.3, R * 0.42, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(R * 0.06, -R * 0.42, R * 0.24, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(R * 0.26, -R * 0.44);
            ctx.lineTo(R * 0.58, -R * 0.36);
            ctx.lineTo(R * 0.26, -R * 0.28);
            ctx.fillStyle = `hsl(${s.hueA} 95% 66%)`;
            ctx.fill();
            ctx.stroke();
            eye(ctx, R * 0.14, -R * 0.48, R * 0.06, s.hueA);
            break;
        }
        case 'moth': {
            const flap = 0.7 + Math.sin(t * 4.4) * 0.3;
            for (const dir of [-1, 1]) {
                ctx.save();
                ctx.scale(dir, 1);
                ctx.fillStyle = `hsla(${s.hueA},90%,66%,.85)`;
                ctx.beginPath();
                ctx.ellipse(R * 0.5 * flap, -R * 0.3, R * 0.5 * flap, R * 0.34, -0.35, 0, Math.PI * 2);
                ctx.fill();
                ctx.stroke();
                ctx.fillStyle = `hsla(${s.hueB},85%,58%,.85)`;
                ctx.beginPath();
                ctx.ellipse(R * 0.42 * flap, R * 0.3, R * 0.38 * flap, R * 0.26, 0.3, 0, Math.PI * 2);
                ctx.fill();
                ctx.stroke();
                ctx.restore();
            }
            ctx.fillStyle = `hsl(${s.hueB} 55% 30%)`;
            ctx.beginPath();
            ctx.ellipse(0, 0, R * 0.13, R * 0.46, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            ctx.lineWidth = R * 0.035;
            ctx.strokeStyle = `hsl(${s.hueA} 90% 70%)`;
            ctx.beginPath();
            ctx.moveTo(-R * 0.05, -R * 0.44);
            ctx.quadraticCurveTo(-R * 0.35, -R * 0.8, -R * 0.5, -R * 0.7);
            ctx.moveTo(R * 0.05, -R * 0.44);
            ctx.quadraticCurveTo(R * 0.35, -R * 0.8, R * 0.5, -R * 0.7);
            ctx.stroke();
            eye(ctx, -R * 0.06, -R * 0.36, R * 0.05, s.hueA);
            eye(ctx, R * 0.08, -R * 0.36, R * 0.05, s.hueA);
            break;
        }
        case 'bulb': {
            ctx.fillStyle = body;
            ctx.beginPath();
            ctx.ellipse(0, R * 0.22, R * 0.52, R * 0.5, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            // leaves
            for (let i = 0; i < 5; i++) {
                const a = -Math.PI / 2 + (i - 2) * 0.42 + Math.sin(t * 1.6 + i) * 0.06;
                ctx.save();
                ctx.rotate(a);
                ctx.fillStyle = `hsl(${s.hueA + i * 4} 78% ${52 - i * 3}%)`;
                ctx.beginPath();
                ctx.ellipse(0, -R * 0.72, R * 0.14, R * 0.44, 0, 0, Math.PI * 2);
                ctx.fill();
                ctx.stroke();
                ctx.restore();
            }
            ctx.fillStyle = `hsla(${s.hueB},80%,72%,.9)`;
            ctx.beginPath();
            ctx.arc(0, R * 0.12, R * 0.16, 0, Math.PI * 2);
            ctx.fill();
            eye(ctx, -R * 0.16, R * 0.16, R * 0.07, s.hueA);
            eye(ctx, R * 0.2, R * 0.16, R * 0.07, s.hueA);
            break;
        }
        case 'shell': {
            ctx.fillStyle = body;
            ctx.beginPath();
            ctx.moveTo(-R * 0.8, R * 0.42);
            ctx.quadraticCurveTo(-R * 0.72, -R * 0.85, 0, -R * 0.8);
            ctx.quadraticCurveTo(R * 0.76, -R * 0.82, R * 0.82, R * 0.42);
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
            // runes
            ctx.strokeStyle = `hsla(${s.hueA},95%,74%,.85)`;
            ctx.lineWidth = R * 0.04;
            for (let i = 0; i < 5; i++) {
                const yy = -R * 0.6 + i * R * 0.24;
                ctx.beginPath();
                ctx.moveTo(-R * 0.5 + (i % 2) * R * 0.15, yy);
                ctx.lineTo(R * 0.3 - (i % 3) * R * 0.14, yy);
                ctx.stroke();
            }
            ctx.strokeStyle = outline;
            ctx.lineWidth = Math.max(1.4, R * 0.055);
            ctx.fillStyle = `hsl(${s.hueB} 40% 44%)`;
            ctx.beginPath();
            ctx.ellipse(R * 0.62, R * 0.5, R * 0.26, R * 0.18, 0.1, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(-R * 0.8, R * 0.42);
            ctx.lineTo(R * 0.82, R * 0.42);
            ctx.lineTo(R * 0.7, R * 0.68);
            ctx.lineTo(-R * 0.68, R * 0.68);
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
            eye(ctx, R * 0.72, R * 0.46, R * 0.055, s.hueA);
            break;
        }
        case 'orb': {
            const pulse = 1 + Math.sin(t * 2.6) * 0.05;
            ctx.fillStyle = `hsl(${s.hueB} 60% 26%)`;
            for (let i = 0; i < 3; i++) {
                ctx.save();
                ctx.rotate(t * (0.4 + i * 0.22) + i * 2);
                ctx.beginPath();
                ctx.ellipse(0, 0, R * (0.95 - i * 0.13) * pulse, R * (0.34 - i * 0.06), 0, 0, Math.PI * 2);
                ctx.lineWidth = R * 0.05;
                ctx.strokeStyle = `hsla(${s.hueA},85%,${66 - i * 8}%,.8)`;
                ctx.stroke();
                ctx.restore();
            }
            const core = ctx.createRadialGradient(0, 0, 0, 0, 0, R * 0.5);
            core.addColorStop(0, '#ffffff');
            core.addColorStop(0.4, `hsl(${s.hueA} 95% 72%)`);
            core.addColorStop(1, `hsl(${s.hueB} 70% 34%)`);
            ctx.fillStyle = core;
            ctx.beginPath();
            ctx.arc(0, 0, R * 0.42 * pulse, 0, Math.PI * 2);
            ctx.fill();
            ctx.strokeStyle = outline;
            ctx.lineWidth = Math.max(1.4, R * 0.05);
            ctx.stroke();
            eye(ctx, -R * 0.1, -R * 0.04, R * 0.07, s.hueB);
            eye(ctx, R * 0.14, -R * 0.04, R * 0.07, s.hueB);
            break;
        }
        default: { // wisp
            const n = 7;
            ctx.fillStyle = body;
            ctx.beginPath();
            for (let i = 0; i <= n * 4; i++) {
                const p = i / (n * 4), a = p * Math.PI * 2;
                const wob = 1 + Math.sin(a * 3 + t * 2.6 + sd * 6) * 0.14 + Math.cos(a * 5 - t * 1.7) * 0.08;
                const rr = R * 0.62 * wob;
                const x = Math.cos(a) * rr, y = Math.sin(a) * rr * 1.08;
                if (i === 0)
                    ctx.moveTo(x, y);
                else
                    ctx.lineTo(x, y);
            }
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
            // trailing ribbons
            ctx.lineWidth = R * 0.06;
            ctx.strokeStyle = `hsla(${s.hueA},90%,68%,.7)`;
            for (let i = 0; i < 3; i++) {
                ctx.beginPath();
                ctx.moveTo(-R * 0.2 + i * R * 0.2, R * 0.55);
                ctx.quadraticCurveTo(-R * 0.4 + i * R * 0.24 + Math.sin(t * 3 + i) * R * 0.2, R * 0.9, -R * 0.1 + i * R * 0.22, R * 1.18);
                ctx.stroke();
            }
            ctx.lineWidth = Math.max(1.4, R * 0.05);
            ctx.strokeStyle = outline;
            eye(ctx, -R * 0.16, -R * 0.08, R * 0.085, s.hueA);
            eye(ctx, R * 0.2, -R * 0.08, R * 0.085, s.hueA);
            break;
        }
    }
    ctx.restore();
}
/** Small icon for menus — renders to a data URL cache. */
const iconCache = new Map();
function creatureIcon(s, size = 64) {
    const key = s.id + ':' + size;
    const c = iconCache.get(key);
    if (c)
        return c;
    const cv = document.createElement('canvas');
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    cv.width = size * dpr;
    cv.height = size * dpr;
    const ctx = cv.getContext('2d');
    if (!ctx)
        return '';
    ctx.scale(dpr, dpr);
    drawCreature(ctx, s, size / 2, size / 2 + size * 0.02, size * 0.36, 1, 0.4);
    const url = cv.toDataURL();
    iconCache.set(key, url);
    return url;
}
};

// ── module: src/battle.ts ──
__mods["src/battle.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STATUS_LABEL = void 0;
exports.stageMul = stageMul;
exports.makeCombatant = makeCombatant;
exports.startBattle = startBattle;
exports.activeAlly = activeAlly;
exports.liveFoes = liveFoes;
exports.liveAllies = liveAllies;
exports.effSpd = effSpd;
exports.buildOrder = buildOrder;
exports.nextActor = nextActor;
exports.advance = advance;
exports.computeDamage = computeDamage;
exports.bonusesFor = bonusesFor;
exports.useAbility = useAbility;
exports.endOfTurn = endOfTurn;
exports.foeChoose = foeChoose;
exports.awardExp = awardExp;
exports.bindChance = bindChance;
const abilities_1 = require("./abilities");
const species_1 = require("./species");
const state_1 = require("./state");
const creature_1 = require("./creature");
const typechart_1 = require("./typechart");
const util_1 = require("./util");
const STAGE_MULT = [0.4, 0.5, 0.6, 0.75, 0.86, 1, 1.16, 1.33, 1.5, 1.7, 1.9, 2.2, 2.5];
function stageMul(s) { return STAGE_MULT[(0, util_1.clamp)(s + 6, 0, 12)]; }
function makeCombatant(c, side, slot) {
    return { side, slot, c, stages: { atk: 0, def: 0, spd: 0 }, barrier: 0, flinch: false, acted: false };
}
function startBattle(foes, opts = {}) {
    const allies = state_1.G.party.map((c, i) => makeCombatant(c, 'ally', i));
    const b = {
        allies,
        foes: foes.map((c, i) => makeCombatant(c, 'foe', i)),
        active: Math.max(0, allies.findIndex((a) => !(0, creature_1.isFainted)(a.c))),
        order: [], orderIdx: 0, round: 0,
        boss: !!opts.boss, wild: !!opts.wild, guardianName: opts.guardianName,
        bindBonus: 0, over: null, expPool: 0, escapeTries: 0,
    };
    buildOrder(b);
    return b;
}
function activeAlly(b) { return b.allies[b.active]; }
function liveFoes(b) { return b.foes.filter((f) => !(0, creature_1.isFainted)(f.c)); }
function liveAllies(b) { return b.allies.filter((a) => !(0, creature_1.isFainted)(a.c)); }
function effSpd(k) {
    let s = (0, creature_1.stats)(k.c, k.side === 'ally' ? state_1.G.bonuses : undefined).spd * stageMul(k.stages.spd);
    if (k.c.statuses.some((x) => x.kind === 'frost'))
        s *= 0.5;
    return s;
}
function buildOrder(b) {
    const parts = [activeAlly(b), ...liveFoes(b)].filter((k) => k && !(0, creature_1.isFainted)(k.c));
    parts.sort((x, y) => effSpd(y) - effSpd(x) + (Math.random() - 0.5) * 0.001);
    b.order = parts;
    b.orderIdx = 0;
    b.round++;
    for (const k of parts)
        k.acted = false;
}
/** Who acts next; null when the round is exhausted. */
function nextActor(b) {
    while (b.orderIdx < b.order.length) {
        const k = b.order[b.orderIdx];
        if ((0, creature_1.isFainted)(k.c)) {
            b.orderIdx++;
            continue;
        }
        if (k.side === 'ally' && k !== activeAlly(b)) {
            b.orderIdx++;
            continue;
        }
        return k;
    }
    return null;
}
function advance(b) { b.orderIdx++; }
// ---------------- damage ----------------
function computeDamage(atkC, defC, a) {
    const atkSpecies = (0, species_1.sp)(atkC.c.speciesId);
    const defSpecies = (0, species_1.sp)(defC.c.speciesId);
    const aStats = (0, creature_1.stats)(atkC.c, atkC.side === 'ally' ? state_1.G.bonuses : undefined);
    const dStats = (0, creature_1.stats)(defC.c, defC.side === 'ally' ? state_1.G.bonuses : undefined);
    let atk = aStats.atk * stageMul(atkC.stages.atk);
    if (atkC.c.statuses.some((s) => s.kind === 'burn'))
        atk *= 0.5;
    let def = dStats.def * stageMul(defC.stages.def);
    if (a.effect.kind === 'pierce')
        def = dStats.def; // ignore buffs
    def = Math.max(1, def);
    const tm = (0, typechart_1.typeMult)(a.affinity, defSpecies.affinity, defSpecies.affinity2);
    const stab = (a.affinity === atkSpecies.affinity || a.affinity === atkSpecies.affinity2) ? 1.25 : 1;
    const critChance = 0.055 + (state_1.G.bonuses.crit || 0) + (atkC.side === 'ally' ? atkC.c.bond * 0.00015 : 0)
        + (a.effect.kind === 'critUp' ? 0.2 : 0);
    const crit = (0, util_1.chance)(critChance);
    const powFactor = a.power / 42;
    let mult = powFactor * tm * stab * (crit ? 1.7 : 1) * (0, util_1.rnd)(0.92, 1.06);
    if (atkC.side === 'foe' && !state_1.G.postgame)
        mult *= 0.96;
    const dmg = Math.max(1, Math.floor(0.4 * atkC.c.level * (atk / def) * mult) + (0, util_1.ri)(1, 3));
    return { dmg, crit, mult: tm, label: (0, typechart_1.multLabel)(tm) };
}
function addStatus(k, kind, turns) {
    if (k.c.statuses.some((s) => s.kind === kind))
        return false;
    const spec = (0, species_1.sp)(k.c.speciesId);
    // affinity immunities
    if (kind === 'burn' && (spec.affinity === 'Ember' || spec.affinity2 === 'Ember'))
        return false;
    if (kind === 'frost' && (spec.affinity === 'Tide' || spec.affinity2 === 'Tide'))
        return false;
    if (kind === 'shock' && (spec.affinity === 'Gale' || spec.affinity2 === 'Gale'))
        return false;
    k.c.statuses.push({ kind, turns });
    return true;
}
const STATUS_TEXT = {
    burn: 'is Scorched — heat gnaws at it', frost: 'is Rimed — its motion drags',
    shock: 'is Stuttering — its will skips', regen: 'begins Mending', sap: 'is Sapped — vitality leaks away',
    barrier: 'raises a Deflection',
};
exports.STATUS_LABEL = {
    burn: 'Scorch', frost: 'Rime', shock: 'Stutter', regen: 'Mend', sap: 'Sap', barrier: 'Deflect',
};
function bonusesFor(k) { return k.side === 'ally' ? state_1.G.bonuses : undefined; }
function dealDamage(b, target, amount, ev, meta) {
    if (target.barrier > 0) {
        target.barrier = 0;
        ev.push({ t: 'barrier', who: target });
        ev.push({ t: 'msg', text: `${(0, creature_1.displayName)(target.c)}'s Deflection shatters — no harm done.` });
        return;
    }
    target.c.hp = Math.max(0, target.c.hp - amount);
    ev.push({ t: 'dmg', who: target, amount, crit: meta?.crit || false, mult: meta?.mult ?? 1, label: meta?.label || '' });
    if (target.c.hp <= 0)
        ev.push({ t: 'faint', who: target });
}
function healUp(target, amount, ev) {
    const mx = (0, creature_1.maxHp)(target.c, bonusesFor(target));
    const before = target.c.hp;
    target.c.hp = Math.min(mx, target.c.hp + amount);
    ev.push({ t: 'heal', who: target, amount: target.c.hp - before });
}
const STAT_NAME = { atk: 'Force', def: 'Ward', spd: 'Tempo' };
function shiftStage(k, key, delta, ev) {
    const before = k.stages[key];
    k.stages[key] = (0, util_1.clamp)(before + delta, -6, 6);
    if (k.stages[key] === before) {
        ev.push({ t: 'stat', who: k, text: `${STAT_NAME[key]} cannot shift further` });
        return;
    }
    const word = delta > 0 ? (delta > 1 ? 'surges' : 'rises') : (delta < -1 ? 'collapses' : 'falls');
    ev.push({ t: 'stat', who: k, text: `${STAT_NAME[key]} ${word}` });
}
/** Execute one ability use. Mutates state, returns events for the UI. */
function useAbility(b, user, abilityId, target) {
    const ev = [];
    const a = (0, abilities_1.ab)(abilityId);
    const idx = user.c.abilities.indexOf(abilityId);
    if (user.flinch) {
        user.flinch = false;
        ev.push({ t: 'msg', text: `${(0, creature_1.displayName)(user.c)} hesitates, the note lost.` });
        return ev;
    }
    if (user.c.statuses.some((s) => s.kind === 'shock') && (0, util_1.chance)(0.28)) {
        ev.push({ t: 'msg', text: `${(0, creature_1.displayName)(user.c)} stutters — the Resonance will not come.` });
        return ev;
    }
    if (idx >= 0) {
        if (user.c.pp[idx] < a.cost) {
            ev.push({ t: 'msg', text: `${(0, creature_1.displayName)(user.c)} has no Energy left for ${a.name}.` });
            return ev;
        }
        user.c.pp[idx] -= a.cost;
    }
    ev.push({ t: 'use', who: user, ability: a });
    let tgt = target;
    if ((0, creature_1.isFainted)(tgt.c)) {
        const alt = tgt.side === 'foe' ? liveFoes(b)[0] : liveAllies(b)[0];
        if (!alt)
            return ev;
        tgt = alt;
    }
    const e = a.effect;
    const self = e.onSelf ? user : tgt;
    // pure status / support moves
    if (a.power === 0) {
        applyEffect(b, user, self, a, ev, true);
        return ev;
    }
    // accuracy
    const accMod = user.c.statuses.some((s) => s.kind === 'frost') ? 0.96 : 1;
    if (!(0, util_1.chance)(Math.min(1, (a.accuracy / 100) * accMod))) {
        ev.push({ t: 'miss', who: tgt });
        return ev;
    }
    const hits = e.kind === 'multi' ? (e.value || 2) : e.kind === 'echo' ? (e.value || 2) : 1;
    let total = 0;
    for (let h = 0; h < hits; h++) {
        if ((0, creature_1.isFainted)(tgt.c))
            break;
        const d = computeDamage(user, tgt, a);
        total += d.dmg;
        dealDamage(b, tgt, d.dmg, ev, d);
    }
    if (hits > 1)
        ev.push({ t: 'msg', text: `Struck ${hits} times!` });
    applyEffect(b, user, e.onSelf ? user : tgt, a, ev, false, total);
    return ev;
}
function applyEffect(b, user, tgt, a, ev, isStatusMove, dmgDealt = 0) {
    const e = a.effect;
    const roll = e.chance === undefined ? true : (0, util_1.chance)(e.chance);
    const mx = (k) => (0, creature_1.maxHp)(k.c, bonusesFor(k));
    switch (e.kind) {
        case 'burn':
        case 'frost':
        case 'shock':
        case 'sap': {
            if (!roll || (0, creature_1.isFainted)(tgt.c))
                break;
            if (addStatus(tgt, e.kind, e.turns || 3)) {
                ev.push({ t: 'status', who: tgt, kind: e.kind, text: `${(0, creature_1.displayName)(tgt.c)} ${STATUS_TEXT[e.kind]}.` });
            }
            break;
        }
        case 'regen': {
            if (addStatus(tgt, 'regen', e.turns || 4)) {
                ev.push({ t: 'status', who: tgt, kind: 'regen', text: `${(0, creature_1.displayName)(tgt.c)} ${STATUS_TEXT.regen}.` });
            }
            break;
        }
        case 'barrier': {
            tgt.barrier = e.turns || 3;
            ev.push({ t: 'status', who: tgt, kind: 'barrier', text: `${(0, creature_1.displayName)(tgt.c)} ${STATUS_TEXT.barrier}.` });
            break;
        }
        case 'shieldAll': {
            for (const k of liveAllies(b))
                k.barrier = e.turns || 2;
            ev.push({ t: 'msg', text: 'Light splits across the whole binding — Deflection for all.' });
            break;
        }
        case 'heal': {
            healUp(tgt, Math.round(mx(tgt) * (e.value || 0.4)), ev);
            break;
        }
        case 'cleanse': {
            const had = tgt.c.statuses.length > 0;
            tgt.c.statuses = tgt.c.statuses.filter((s) => s.kind === 'regen');
            healUp(tgt, Math.round(mx(tgt) * (e.value || 0.2)), ev);
            ev.push({ t: 'msg', text: had ? `${(0, creature_1.displayName)(tgt.c)} is cleared of ailment.` : 'The hymn rings clean.' });
            break;
        }
        case 'drain': {
            if (dmgDealt > 0)
                healUp(user, Math.round(dmgDealt * (e.value || 0.5)), ev);
            break;
        }
        case 'recoil': {
            if (dmgDealt > 0) {
                const r = Math.max(1, Math.round(dmgDealt * (e.value || 0.2)));
                user.c.hp = Math.max(0, user.c.hp - r);
                ev.push({ t: 'dmg', who: user, amount: r, crit: false, mult: 1, label: 'recoil' });
                if (user.c.hp <= 0)
                    ev.push({ t: 'faint', who: user });
            }
            break;
        }
        case 'flinch': {
            if (roll && !(0, creature_1.isFainted)(tgt.c)) {
                tgt.flinch = true;
                ev.push({ t: 'stat', who: tgt, text: 'flinches' });
            }
            break;
        }
        case 'atkUp':
            if (roll)
                shiftStage(tgt, 'atk', e.value || 1, ev);
            break;
        case 'defUp':
            if (roll)
                shiftStage(tgt, 'def', e.value || 1, ev);
            break;
        case 'spdUp':
            if (roll)
                shiftStage(tgt, 'spd', e.value || 1, ev);
            break;
        case 'atkDown':
            if (roll && !(0, creature_1.isFainted)(tgt.c))
                shiftStage(tgt, 'atk', -(e.value || 1), ev);
            break;
        case 'defDown':
            if (roll && !(0, creature_1.isFainted)(tgt.c))
                shiftStage(tgt, 'def', -(e.value || 1), ev);
            break;
        case 'spdDown':
            if (roll && !(0, creature_1.isFainted)(tgt.c))
                shiftStage(tgt, 'spd', -(e.value || 1), ev);
            break;
        default: {
            if (isStatusMove)
                ev.push({ t: 'msg', text: `${a.name} settles into the air.` });
            break;
        }
    }
}
/** End-of-turn ticks for one combatant. */
function endOfTurn(k) {
    const ev = [];
    if ((0, creature_1.isFainted)(k.c))
        return ev;
    const mx = (0, creature_1.maxHp)(k.c, bonusesFor(k));
    for (const s of [...k.c.statuses]) {
        if (s.kind === 'burn') {
            const d = Math.max(1, Math.round(mx * 0.0625));
            k.c.hp = Math.max(0, k.c.hp - d);
            ev.push({ t: 'dmg', who: k, amount: d, crit: false, mult: 1, label: 'Scorch' });
        }
        else if (s.kind === 'sap') {
            const d = Math.max(1, Math.round(mx * 0.05));
            k.c.hp = Math.max(0, k.c.hp - d);
            ev.push({ t: 'dmg', who: k, amount: d, crit: false, mult: 1, label: 'Sap' });
        }
        else if (s.kind === 'regen') {
            const h = Math.max(1, Math.round(mx * 0.09));
            const before = k.c.hp;
            k.c.hp = Math.min(mx, k.c.hp + h);
            ev.push({ t: 'heal', who: k, amount: k.c.hp - before });
        }
        s.turns--;
        if (s.turns <= 0) {
            k.c.statuses = k.c.statuses.filter((x) => x !== s);
            ev.push({ t: 'msg', text: `${(0, creature_1.displayName)(k.c)} shakes off ${exports.STATUS_LABEL[s.kind]}.` });
        }
    }
    if (k.c.hp <= 0)
        ev.push({ t: 'faint', who: k });
    return ev;
}
// ---------------- foe AI ----------------
function foeChoose(b, foe) {
    const targets = liveAllies(b).filter((x) => x === activeAlly(b));
    const target = targets[0] || liveAllies(b)[0];
    const usable = foe.c.abilities.filter((id, i) => foe.c.pp[i] >= (0, abilities_1.ab)(id).cost);
    if (usable.length === 0)
        return { ability: foe.c.abilities[0], target };
    // score each: damage estimate, heal when low, buff sometimes
    let best = usable[0], bestScore = -1;
    const mx = (0, creature_1.maxHp)(foe.c, undefined);
    for (const id of usable) {
        const a = (0, abilities_1.ab)(id);
        let score;
        if (a.power === 0) {
            const k = a.effect.kind;
            if (k === 'heal' || k === 'cleanse')
                score = foe.c.hp < mx * 0.42 ? 120 : 4;
            else
                score = b.round <= 2 ? 55 : 14;
        }
        else {
            const d = computeDamage(foe, target, a);
            score = d.dmg * (a.accuracy / 100);
            if (d.dmg >= target.c.hp)
                score *= 2.4;
        }
        score *= (0, util_1.rnd)(0.82, 1.18);
        if (score > bestScore) {
            bestScore = score;
            best = id;
        }
    }
    if (b.boss && (0, util_1.chance)(0.12))
        best = (0, util_1.pick)(usable);
    return { ability: best, target };
}
function awardExp(defeated, participants) {
    const alive = participants.filter((c) => c.hp > 0);
    if (alive.length === 0)
        return { gained: 0, levels: [] };
    const base = (0, species_1.sp)(defeated.speciesId).yieldExp;
    const results = [];
    let gained = 0;
    for (const c of alive) {
        const rel = (0, util_1.clamp)(defeated.level / Math.max(1, c.level), 0.45, 2.2);
        const g = Math.max(4, Math.floor((base * defeated.level / 6) * rel / alive.length));
        gained = g;
        c.exp += g;
        c.bond = Math.min(255, c.bond + 2);
        const from = c.level;
        const learned = [];
        while (c.level < creature_1.LEVEL_CAP && c.exp >= (0, creature_1.expForLevel)(c.level + 1)) {
            c.level++;
            const gains = (0, creature_1.newLearns)(c.speciesId, c.level - 1, c.level);
            for (const id of gains)
                learned.push(id);
            c.hp = Math.min((0, creature_1.maxHp)(c, state_1.G.bonuses), c.hp + 6);
        }
        if (c.level > from)
            results.push({ uid: c.uid, name: (0, creature_1.displayName)(c), from, to: c.level, learned });
    }
    return { gained, levels: results };
}
/** Base binding chance 0..1 before the rhythm mini-game multiplier. */
function bindChance(target, itemPower, bonus) {
    const mx = (0, creature_1.maxHp)(target, undefined);
    const hpFactor = (3 * mx - 2 * target.hp) / (3 * mx); // 1/3 at full HP, ~1 at 1 HP
    const rarity = (0, species_1.sp)(target.speciesId).rarity;
    const rarityF = [1, 1.1, 0.9, 0.72, 0.5][rarity] ?? 0.8;
    const lvF = (0, util_1.clamp)(1.25 - target.level / 90, 0.55, 1.2);
    const statusF = target.statuses.some((s) => s.kind === 'frost' || s.kind === 'shock') ? 1.2 : 1;
    return (0, util_1.clamp)(hpFactor * itemPower * rarityF * lvF * statusF * (0.62 + bonus), 0.03, 0.97);
}
};

// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.T_BACK = exports.T_DECO = exports.T_GATE = exports.T_GUARD = exports.T_STORY = exports.T_SHRINE = exports.T_GRASS = exports.T_WALL = exports.T_FLOOR = void 0;
exports.tileAt = tileAt;
exports.setTile = setTile;
exports.walkable = walkable;
exports.getMap = getMap;
exports.findWalkableNear = findWalkableNear;
const zones_1 = require("./zones");
const util_1 = require("./util");
exports.T_FLOOR = 0, exports.T_WALL = 1, exports.T_GRASS = 2, exports.T_SHRINE = 3, exports.T_STORY = 4, exports.T_GUARD = 5, exports.T_GATE = 6, exports.T_DECO = 7, exports.T_BACK = 8;
const maps = {};
function tileAt(m, x, y) {
    if (x < 0 || y < 0 || x >= m.w || y >= m.h)
        return exports.T_WALL;
    return m.tiles[y * m.w + x];
}
function setTile(m, x, y, v) {
    if (x < 0 || y < 0 || x >= m.w || y >= m.h)
        return;
    m.tiles[y * m.w + x] = v;
}
function walkable(m, x, y) {
    const t = tileAt(m, x, y);
    return t !== exports.T_WALL && t !== exports.T_DECO;
}
function carveRoom(m, cx, cy, r) {
    for (let y = cy - r; y <= cy + r; y++) {
        for (let x = cx - r; x <= cx + r; x++) {
            const d = Math.hypot(x - cx, (y - cy) * 1.25);
            if (d <= r)
                setTile(m, x, y, exports.T_FLOOR);
        }
    }
}
function carvePath(m, x0, y0, x1, y1, w = 1) {
    let x = x0, y = y0, guard = 0;
    while ((x !== x1 || y !== y1) && guard++ < 600) {
        for (let dy = -w; dy <= w; dy++)
            for (let dx = -w; dx <= w; dx++)
                setTile(m, x + dx, y + dy, exports.T_FLOOR);
        if (x !== x1 && (y === y1 || (0, util_1.hash2)(x, y, 91) > 0.5))
            x += Math.sign(x1 - x);
        else
            y += Math.sign(y1 - y);
    }
    for (let dy = -w; dy <= w; dy++)
        for (let dx = -w; dx <= w; dx++)
            setTile(m, x1 + dx, y1 + dy, exports.T_FLOOR);
}
function getMap(zoneId) {
    const cached = maps[zoneId];
    if (cached)
        return cached;
    const z = zones_1.ZONES[zoneId];
    const w = z.width, h = z.height;
    const m = {
        w, h, tiles: new Uint8Array(w * h).fill(exports.T_WALL),
        shrine: { x: 0, y: 0 }, story: { x: 0, y: 0 }, guard: null, gate: null, back: null,
        entry: { x: 0, y: 0 },
    };
    const seed = 400 + zoneId * 77;
    // Landmark anchor points laid out left→right
    const entry = { x: 4, y: Math.floor(h / 2) };
    const shrine = { x: Math.floor(w * 0.26), y: Math.floor(h * 0.32) + (zoneId % 2 ? 3 : -1) };
    const story = { x: Math.floor(w * 0.52), y: Math.floor(h * 0.72) };
    const guard = { x: w - 6, y: Math.floor(h / 2) + (zoneId % 2 ? -3 : 3) };
    const nodes = [entry, shrine, story, guard];
    for (const n of nodes) {
        n.x = Math.max(3, Math.min(w - 4, n.x));
        n.y = Math.max(3, Math.min(h - 4, n.y));
        carveRoom(m, n.x, n.y, 4);
    }
    // extra chambers
    for (let i = 0; i < 6; i++) {
        const rx = 5 + Math.floor((0, util_1.hash2)(i, zoneId, seed) * (w - 10));
        const ry = 4 + Math.floor((0, util_1.hash2)(i + 50, zoneId, seed) * (h - 8));
        carveRoom(m, rx, ry, 3 + Math.floor((0, util_1.hash2)(i, i, seed) * 2));
        nodes.push({ x: rx, y: ry });
    }
    for (let i = 1; i < nodes.length; i++)
        carvePath(m, nodes[i - 1].x, nodes[i - 1].y, nodes[i].x, nodes[i].y, 1);
    carvePath(m, entry.x, entry.y, story.x, story.y, 1);
    carvePath(m, shrine.x, shrine.y, guard.x, guard.y, 1);
    // grass patches from noise on floor
    for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
            if (tileAt(m, x, y) !== exports.T_FLOOR)
                continue;
            const n = (0, util_1.fbm)(x * 0.24, y * 0.24, seed, 3);
            if (n > 0.78)
                setTile(m, x, y, exports.T_GRASS);
            else if (n < 0.26 && (0, util_1.hash2)(x, y, seed + 5) > 0.72)
                setTile(m, x, y, exports.T_DECO);
        }
    }
    // landmarks (clear surroundings first)
    const place = (p, t) => {
        for (let dy = -1; dy <= 1; dy++)
            for (let dx = -1; dx <= 1; dx++) {
                if (tileAt(m, p.x + dx, p.y + dy) !== exports.T_WALL)
                    setTile(m, p.x + dx, p.y + dy, exports.T_FLOOR);
            }
        setTile(m, p.x, p.y, t);
    };
    place(shrine, exports.T_SHRINE);
    place(story, exports.T_STORY);
    place(guard, exports.T_GUARD);
    m.shrine = shrine;
    m.story = story;
    m.guard = zones_1.ZONES[zoneId].guardian ? guard : null;
    if (!zones_1.ZONES[zoneId].guardian)
        setTile(m, guard.x, guard.y, exports.T_FLOOR);
    // gate forward (beyond guardian) / gate back (at entry)
    if (zoneId < zones_1.ZONES.length - 1) {
        const gate = { x: w - 2, y: guard.y };
        carvePath(m, guard.x, guard.y, gate.x, gate.y, 0);
        place(gate, exports.T_GATE);
        m.gate = gate;
    }
    if (zoneId > 0) {
        const back = { x: 1, y: entry.y };
        carvePath(m, entry.x, entry.y, back.x, back.y, 0);
        place(back, exports.T_BACK);
        m.back = back;
    }
    for (let dy = -1; dy <= 1; dy++)
        for (let dx = -1; dx <= 1; dx++)
            setTile(m, entry.x + dx, entry.y + dy, exports.T_FLOOR);
    m.entry = entry;
    maps[zoneId] = m;
    return m;
}
function findWalkableNear(m, x, y) {
    if (walkable(m, x, y))
        return { x, y };
    for (let r = 1; r < 12; r++) {
        for (let dy = -r; dy <= r; dy++)
            for (let dx = -r; dx <= r; dx++) {
                if (walkable(m, x + dx, y + dy))
                    return { x: x + dx, y: y + dy };
            }
    }
    return { x: m.entry.x, y: m.entry.y };
}
};

// ── module: src/fx.ts ──
__mods["src/fx.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fx = exports.FX = void 0;
const util_1 = require("./util");
class FX {
    constructor() {
        this.particles = [];
        this.floats = [];
        this.shake = 0;
        this.flash = 0;
        this.flashColor = '#fff';
    }
    burst(x, y, color, n = 14, speed = 110, kind = 'spark') {
        for (let i = 0; i < n; i++) {
            const a = (0, util_1.rnd)(0, Math.PI * 2), s = (0, util_1.rnd)(speed * 0.35, speed);
            this.particles.push({
                x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 20,
                life: 0, max: (0, util_1.rnd)(0.4, 0.95), r: (0, util_1.rnd)(1.6, 4.2), color, grav: 130, kind,
                rot: (0, util_1.rnd)(0, 6.28), vr: (0, util_1.rnd)(-6, 6),
            });
        }
    }
    ring(x, y, color, r = 8) {
        this.particles.push({ x, y, vx: 0, vy: 0, life: 0, max: 0.5, r, color, grav: 0, kind: 'ring' });
    }
    ambient(x, y, vx, vy, color, r, life) {
        this.particles.push({ x, y, vx, vy, life: 0, max: life, r, color, grav: 0, kind: 'dot' });
    }
    float(x, y, text, color = '#fff', size = 22, crit = false) {
        this.floats.push({ x, y, vy: -46, life: 0, max: 1.05, text, color, size, crit });
    }
    kick(amount) { this.shake = Math.min(24, this.shake + amount); }
    bang(color, amount = 0.7) { this.flash = amount; this.flashColor = color; }
    update(dt) {
        for (let i = this.particles.length - 1; i >= 0; i--) {
            const p = this.particles[i];
            p.life += dt;
            if (p.life >= p.max) {
                this.particles.splice(i, 1);
                continue;
            }
            p.x += p.vx * dt;
            p.y += p.vy * dt;
            p.vy += p.grav * dt;
            p.vx *= 1 - 1.4 * dt;
            if (p.rot !== undefined && p.vr !== undefined)
                p.rot += p.vr * dt;
        }
        for (let i = this.floats.length - 1; i >= 0; i--) {
            const f = this.floats[i];
            f.life += dt;
            if (f.life >= f.max) {
                this.floats.splice(i, 1);
                continue;
            }
            f.y += f.vy * dt;
            f.vy += 60 * dt;
        }
        this.shake *= Math.pow(0.0025, dt);
        if (this.shake < 0.2)
            this.shake = 0;
        this.flash = Math.max(0, this.flash - dt * 2.4);
    }
    drawParticles(ctx) {
        for (const p of this.particles) {
            const t = 1 - p.life / p.max;
            ctx.globalAlpha = Math.max(0, t);
            if (p.kind === 'ring') {
                ctx.strokeStyle = p.color;
                ctx.lineWidth = 3 * t + 0.5;
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.r * (1 + (1 - t) * 4), 0, Math.PI * 2);
                ctx.stroke();
            }
            else if (p.kind === 'shard') {
                ctx.save();
                ctx.translate(p.x, p.y);
                ctx.rotate(p.rot || 0);
                ctx.fillStyle = p.color;
                ctx.fillRect(-p.r, -p.r * 0.4, p.r * 2, p.r * 0.8);
                ctx.restore();
            }
            else if (p.kind === 'spark') {
                ctx.strokeStyle = p.color;
                ctx.lineWidth = p.r * 0.7;
                ctx.beginPath();
                ctx.moveTo(p.x, p.y);
                ctx.lineTo(p.x - p.vx * 0.02, p.y - p.vy * 0.02);
                ctx.stroke();
            }
            else {
                ctx.fillStyle = p.color;
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.r * (0.5 + t * 0.8), 0, Math.PI * 2);
                ctx.fill();
            }
        }
        ctx.globalAlpha = 1;
    }
    drawFloats(ctx) {
        ctx.textAlign = 'center';
        for (const f of this.floats) {
            const t = f.life / f.max;
            ctx.globalAlpha = t < 0.12 ? t / 0.12 : Math.max(0, 1 - Math.pow((t - 0.12) / 0.88, 2.4));
            const sz = f.size * (f.crit ? 1 + Math.max(0, 0.4 - t) : 1);
            ctx.font = `800 ${sz}px "Trebuchet MS", sans-serif`;
            ctx.lineWidth = Math.max(3, sz * 0.18);
            ctx.strokeStyle = 'rgba(4,6,14,.9)';
            ctx.strokeText(f.text, f.x, f.y);
            ctx.fillStyle = f.color;
            ctx.fillText(f.text, f.x, f.y);
        }
        ctx.globalAlpha = 1;
        ctx.textAlign = 'left';
    }
    drawFlash(ctx, w, h) {
        if (this.flash <= 0)
            return;
        ctx.globalAlpha = Math.min(0.85, this.flash);
        ctx.fillStyle = this.flashColor;
        ctx.fillRect(0, 0, w, h);
        ctx.globalAlpha = 1;
    }
    shakeOffset() {
        if (this.shake <= 0)
            return { x: 0, y: 0 };
        return { x: (0, util_1.rnd)(-this.shake, this.shake), y: (0, util_1.rnd)(-this.shake, this.shake) };
    }
    clear() { this.particles.length = 0; this.floats.length = 0; this.shake = 0; this.flash = 0; }
}
exports.FX = FX;
exports.fx = new FX();
};

// ── module: src/runtime.ts ──
__mods["src/runtime.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RT = void 0;
exports.hpAnimOf = hpAnimOf;
exports.setHpAnim = setHpAnim;
exports.RT = {
    mode: 'title',
    player: { x: 8, y: 18, fx: 8, fy: 18, facing: 1, moving: false },
    moveT: 1, fromX: 8, fromY: 18,
    battle: null,
    battleAnim: {
        allyOffset: 0, foeOffset: 0, allyFlash: 0, foeFlash: 0,
        allyShakeT: 0, foeShakeT: 0, focus: null, hpAnim: new Map(),
    },
    busy: false,
    encounterFlash: 0,
    transition: 0,
    transitionDir: 1,
    bindingCreature: null,
    time: 0,
    paused: false,
};
function hpAnimOf(uid, actual) {
    const v = exports.RT.battleAnim.hpAnim.get(uid);
    if (v === undefined) {
        exports.RT.battleAnim.hpAnim.set(uid, actual);
        return actual;
    }
    return v;
}
function setHpAnim(uid, v) { exports.RT.battleAnim.hpAnim.set(uid, v); }
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ctx = exports.LAYOUT = exports.DPR = exports.VH = exports.VW = exports.cv = exports.TILE = void 0;
exports.resize = resize;
exports.spawnAmbient = spawnAmbient;
exports.drawField = drawField;
exports.drawBattle = drawBattle;
exports.screenPosFor = screenPosFor;
exports.drawTitleScene = drawTitleScene;
exports.statusUid = statusUid;
exports.nameOf = nameOf;
exports.mhp = mhp;
exports.affColor = affColor;
const state_1 = require("./state");
const runtime_1 = require("./runtime");
const world_1 = require("./world");
const fx_1 = require("./fx");
const util_1 = require("./util");
const sprites_1 = require("./sprites");
const species_1 = require("./species");
const creature_1 = require("./creature");
const battle_1 = require("./battle");
const typechart_1 = require("./typechart");
exports.TILE = 44;
exports.cv = document.getElementById('scene');
const ctx = exports.cv.getContext('2d');
exports.ctx = ctx;
exports.VW = 1280, exports.VH = 720, exports.DPR = 1;
function resize() {
    exports.DPR = Math.min(2, window.devicePixelRatio || 1);
    exports.VW = exports.cv.clientWidth || window.innerWidth;
    exports.VH = exports.cv.clientHeight || window.innerHeight;
    exports.cv.width = Math.floor(exports.VW * exports.DPR);
    exports.cv.height = Math.floor(exports.VH * exports.DPR);
    ctx.setTransform(exports.DPR, 0, 0, exports.DPR, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ---------- ambient particle spawner ----------
let ambT = 0;
function spawnAmbient(dt) {
    const z = (0, state_1.zoneDef)();
    ambT += dt;
    const rate = z.particle === 'null' ? 0.055 : 0.03;
    while (ambT > rate) {
        ambT -= rate;
        const p = z.prop;
        switch (z.particle) {
            case 'ash':
                fx_1.fx.ambient((0, util_1.rnd)(0, exports.VW), exports.VH + 8, (0, util_1.rnd)(-16, 16), (0, util_1.rnd)(-42, -18), '#ffb26b', (0, util_1.rnd)(1, 2.6), (0, util_1.rnd)(2.4, 5));
                break;
            case 'pollen':
                fx_1.fx.ambient((0, util_1.rnd)(0, exports.VW), (0, util_1.rnd)(0, exports.VH), (0, util_1.rnd)(-14, 14), (0, util_1.rnd)(-10, 10), '#d8ff9a', (0, util_1.rnd)(1, 2.4), (0, util_1.rnd)(3, 6));
                break;
            case 'spray':
                fx_1.fx.ambient((0, util_1.rnd)(0, exports.VW), (0, util_1.rnd)(exports.VH * 0.3, exports.VH), (0, util_1.rnd)(-30, 30), (0, util_1.rnd)(-60, -20), '#b6f0ff', (0, util_1.rnd)(1, 2.2), (0, util_1.rnd)(1.4, 3));
                break;
            case 'gust':
                fx_1.fx.ambient(-10, (0, util_1.rnd)(0, exports.VH), (0, util_1.rnd)(70, 150), (0, util_1.rnd)(-16, 16), '#e0d6ff', (0, util_1.rnd)(1, 2.4), (0, util_1.rnd)(2.4, 4.6));
                break;
            case 'motes':
                fx_1.fx.ambient((0, util_1.rnd)(0, exports.VW), (0, util_1.rnd)(0, exports.VH), (0, util_1.rnd)(-8, 8), (0, util_1.rnd)(-16, -4), '#9fe8e0', (0, util_1.rnd)(1, 2.6), (0, util_1.rnd)(3, 7));
                break;
            default:
                fx_1.fx.ambient((0, util_1.rnd)(0, exports.VW), (0, util_1.rnd)(0, exports.VH), (0, util_1.rnd)(-6, 6), (0, util_1.rnd)(-6, 6), p, (0, util_1.rnd)(1.4, 3.4), (0, util_1.rnd)(1.6, 3.4));
                break;
        }
    }
}
function bgGradient() {
    const z = (0, state_1.zoneDef)();
    const g = ctx.createLinearGradient(0, 0, exports.VW * 0.35, exports.VH);
    g.addColorStop(0, z.gradB);
    g.addColorStop(0.55, z.gradA);
    g.addColorStop(1, z.gradC);
    return g;
}
// ---------- field ----------
function drawTile(t, sx, sy, tx, ty, time) {
    const z = (0, state_1.zoneDef)();
    const n = (0, util_1.hash2)(tx, ty, 21);
    if (t === world_1.T_WALL) {
        ctx.fillStyle = '#05070e';
        ctx.fillRect(sx, sy, exports.TILE, exports.TILE);
        // chunky rock block with a lit top edge
        ctx.fillStyle = z.gradC;
        ctx.fillRect(sx + 1, sy + 1, exports.TILE - 2, exports.TILE - 2);
        ctx.fillStyle = `rgba(255,255,255,${0.05 + n * 0.05})`;
        ctx.fillRect(sx + 1, sy + 1, exports.TILE - 2, exports.TILE - 2);
        ctx.fillStyle = `rgba(${180},${210},${255},${0.14 + n * 0.1})`;
        ctx.fillRect(sx + 1, sy + 1, exports.TILE - 2, 5);
        ctx.fillStyle = 'rgba(0,0,0,.4)';
        ctx.fillRect(sx + 1, sy + exports.TILE - 8, exports.TILE - 2, 7);
        return;
    }
    // floor base
    ctx.fillStyle = z.ground;
    ctx.fillRect(sx, sy, exports.TILE, exports.TILE);
    ctx.fillStyle = `rgba(255,255,255,${0.03 + n * 0.06})`;
    ctx.fillRect(sx, sy, exports.TILE, exports.TILE);
    // scattered flecks
    if (n > 0.7) {
        ctx.fillStyle = `rgba(255,255,255,.10)`;
        ctx.fillRect(sx + 6 + n * 18, sy + 8 + n * 14, 3, 3);
    }
    // subtle grid seams
    ctx.strokeStyle = 'rgba(0,0,0,.07)';
    ctx.lineWidth = 1;
    ctx.strokeRect(sx + 0.5, sy + 0.5, exports.TILE - 1, exports.TILE - 1);
    if (t === world_1.T_GRASS) {
        ctx.fillStyle = z.accent;
        ctx.fillRect(sx, sy, exports.TILE, exports.TILE);
        ctx.fillStyle = z.prop;
        ctx.globalAlpha = 0.14 + Math.sin(time * 1.4 + tx * 0.5 + ty * 0.3) * 0.05;
        ctx.fillRect(sx, sy, exports.TILE, exports.TILE);
        ctx.globalAlpha = 1;
        ctx.strokeStyle = z.prop;
        ctx.lineWidth = 1.8;
        ctx.globalAlpha = 0.8;
        for (let i = 0; i < 5; i++) {
            const gx = sx + 5 + ((i * 8 + n * 12) % (exports.TILE - 10));
            const sway = Math.sin(time * 1.6 + tx * 0.7 + i) * 3;
            ctx.beginPath();
            ctx.moveTo(gx, sy + exports.TILE - 4);
            ctx.quadraticCurveTo(gx + sway, sy + exports.TILE - 14, gx + sway * 1.6, sy + exports.TILE - 24 - (i % 3) * 4);
            ctx.stroke();
        }
        ctx.globalAlpha = 1;
    }
    else if (t === world_1.T_DECO) {
        // a standing shard, faintly lit
        const gg = ctx.createLinearGradient(sx, sy, sx, sy + exports.TILE);
        gg.addColorStop(0, z.prop);
        gg.addColorStop(1, z.gradC);
        ctx.globalAlpha = 0.62;
        ctx.fillStyle = gg;
        ctx.beginPath();
        ctx.moveTo(sx + exports.TILE * 0.5, sy + 3);
        ctx.lineTo(sx + exports.TILE - 7, sy + exports.TILE * 0.62);
        ctx.lineTo(sx + exports.TILE * 0.62, sy + exports.TILE - 5);
        ctx.lineTo(sx + exports.TILE * 0.32, sy + exports.TILE - 5);
        ctx.lineTo(sx + 7, sy + exports.TILE * 0.58);
        ctx.closePath();
        ctx.fill();
        ctx.globalAlpha = 0.5;
        ctx.strokeStyle = z.prop;
        ctx.lineWidth = 1.3;
        ctx.stroke();
        ctx.globalAlpha = 1;
    }
    else if (t === world_1.T_SHRINE) {
        const pulse = 0.6 + Math.sin(time * 2) * 0.25;
        const g = ctx.createRadialGradient(sx + exports.TILE / 2, sy + exports.TILE / 2, 2, sx + exports.TILE / 2, sy + exports.TILE / 2, exports.TILE);
        g.addColorStop(0, `rgba(180,255,230,${0.5 * pulse})`);
        g.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = g;
        ctx.fillRect(sx - exports.TILE / 2, sy - exports.TILE / 2, exports.TILE * 2, exports.TILE * 2);
        ctx.fillStyle = '#dff7ee';
        ctx.beginPath();
        ctx.moveTo(sx + exports.TILE / 2, sy + 6);
        ctx.lineTo(sx + exports.TILE - 10, sy + exports.TILE / 2);
        ctx.lineTo(sx + exports.TILE / 2, sy + exports.TILE - 6);
        ctx.lineTo(sx + 10, sy + exports.TILE / 2);
        ctx.closePath();
        ctx.fill();
        ctx.strokeStyle = '#7fe8c8';
        ctx.lineWidth = 2;
        ctx.stroke();
    }
    else if (t === world_1.T_STORY) {
        ctx.fillStyle = '#c9d6f0';
        ctx.fillRect(sx + exports.TILE * 0.28, sy + 6, exports.TILE * 0.44, exports.TILE - 12);
        ctx.strokeStyle = '#8fa8d8';
        ctx.lineWidth = 2;
        ctx.strokeRect(sx + exports.TILE * 0.28, sy + 6, exports.TILE * 0.44, exports.TILE - 12);
        ctx.fillStyle = 'rgba(60,80,130,.7)';
        for (let i = 0; i < 4; i++)
            ctx.fillRect(sx + exports.TILE * 0.34, sy + 13 + i * 6, exports.TILE * 0.32, 2);
        const gl = 0.35 + Math.sin(time * 1.5) * 0.2;
        ctx.fillStyle = `rgba(200,220,255,${gl})`;
        ctx.beginPath();
        ctx.arc(sx + exports.TILE / 2, sy + exports.TILE / 2, exports.TILE * 0.42, 0, Math.PI * 2);
        ctx.fill();
    }
    else if (t === world_1.T_GUARD) {
        const pulse = 0.5 + Math.sin(time * 3) * 0.3;
        const g = ctx.createRadialGradient(sx + exports.TILE / 2, sy + exports.TILE / 2, 2, sx + exports.TILE / 2, sy + exports.TILE / 2, exports.TILE * 1.3);
        g.addColorStop(0, `rgba(255,120,150,${0.65 * pulse})`);
        g.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = g;
        ctx.fillRect(sx - exports.TILE, sy - exports.TILE, exports.TILE * 3, exports.TILE * 3);
        ctx.save();
        ctx.translate(sx + exports.TILE / 2, sy + exports.TILE / 2);
        ctx.rotate(time * 0.6);
        ctx.strokeStyle = '#ffd0dd';
        ctx.lineWidth = 2.5;
        for (let i = 0; i < 3; i++) {
            ctx.beginPath();
            ctx.arc(0, 0, exports.TILE * (0.18 + i * 0.11), i * 2, i * 2 + 4.2);
            ctx.stroke();
        }
        ctx.restore();
    }
    else if (t === world_1.T_GATE || t === world_1.T_BACK) {
        const up = t === world_1.T_GATE;
        ctx.fillStyle = up ? 'rgba(150,220,255,.2)' : 'rgba(255,220,150,.18)';
        ctx.fillRect(sx + 2, sy + 2, exports.TILE - 4, exports.TILE - 4);
        ctx.strokeStyle = up ? '#9fe0ff' : '#ffd89a';
        ctx.lineWidth = 2;
        ctx.strokeRect(sx + 2, sy + 2, exports.TILE - 4, exports.TILE - 4);
        ctx.fillStyle = up ? '#c9f0ff' : '#ffe9c0';
        ctx.font = '700 20px "Trebuchet MS", sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText(up ? '»' : '«', sx + exports.TILE / 2, sy + exports.TILE / 2 + 7);
        ctx.textAlign = 'left';
    }
}
function drawPlayer(sx, sy, time) {
    const bob = Math.sin(time * 6) * (runtime_1.RT.player.moving ? 2.4 : 0.8);
    const cx = sx + exports.TILE / 2, cy = sy + exports.TILE / 2 + bob;
    // shadow
    ctx.fillStyle = 'rgba(0,0,0,.35)';
    ctx.beginPath();
    ctx.ellipse(cx, sy + exports.TILE - 5, exports.TILE * 0.3, exports.TILE * 0.11, 0, 0, Math.PI * 2);
    ctx.fill();
    // aura
    const g = ctx.createRadialGradient(cx, cy, 2, cx, cy, exports.TILE * 0.85);
    g.addColorStop(0, 'rgba(170,220,255,.35)');
    g.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.arc(cx, cy, exports.TILE * 0.85, 0, Math.PI * 2);
    ctx.fill();
    ctx.save();
    ctx.translate(cx, cy);
    ctx.scale(runtime_1.RT.player.facing, 1);
    // cloak
    ctx.fillStyle = '#2b3f7a';
    ctx.beginPath();
    ctx.moveTo(0, -exports.TILE * 0.06);
    ctx.quadraticCurveTo(exports.TILE * 0.26, exports.TILE * 0.1, exports.TILE * 0.17, exports.TILE * 0.36);
    ctx.lineTo(-exports.TILE * 0.17, exports.TILE * 0.36);
    ctx.quadraticCurveTo(-exports.TILE * 0.26, exports.TILE * 0.1, 0, -exports.TILE * 0.06);
    ctx.fill();
    ctx.strokeStyle = '#8fb6ff';
    ctx.lineWidth = 1.6;
    ctx.stroke();
    // head
    ctx.fillStyle = '#f0d8bd';
    ctx.beginPath();
    ctx.arc(exports.TILE * 0.02, -exports.TILE * 0.22, exports.TILE * 0.13, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#5a4a3a';
    ctx.lineWidth = 1.2;
    ctx.stroke();
    // hood
    ctx.fillStyle = '#3d569c';
    ctx.beginPath();
    ctx.arc(exports.TILE * 0.02, -exports.TILE * 0.25, exports.TILE * 0.155, Math.PI * 1.05, Math.PI * 2.1);
    ctx.fill();
    // lantern
    const lg = 0.6 + Math.sin(time * 4) * 0.3;
    ctx.fillStyle = `rgba(255,220,140,${lg})`;
    ctx.beginPath();
    ctx.arc(exports.TILE * 0.24, exports.TILE * 0.05, exports.TILE * 0.075, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
}
function drawField(time) {
    const m = (0, world_1.getMap)(state_1.G.zone);
    ctx.fillStyle = bgGradient();
    ctx.fillRect(0, 0, exports.VW, exports.VH);
    const sh = fx_1.fx.shakeOffset();
    ctx.save();
    ctx.translate(sh.x, sh.y);
    const pxf = (0, util_1.lerp)(runtime_1.RT.fromX, runtime_1.RT.player.x, runtime_1.RT.moveT);
    const pyf = (0, util_1.lerp)(runtime_1.RT.fromY, runtime_1.RT.player.y, runtime_1.RT.moveT);
    const camX = pxf * exports.TILE + exports.TILE / 2 - exports.VW / 2;
    const camY = pyf * exports.TILE + exports.TILE / 2 - exports.VH / 2;
    const cx = (0, util_1.clamp)(camX, -exports.TILE * 2, m.w * exports.TILE - exports.VW + exports.TILE * 2);
    const cy = (0, util_1.clamp)(camY, -exports.TILE * 2, m.h * exports.TILE - exports.VH + exports.TILE * 2);
    const x0 = Math.floor(cx / exports.TILE) - 1, x1 = Math.ceil((cx + exports.VW) / exports.TILE) + 1;
    const y0 = Math.floor(cy / exports.TILE) - 1, y1 = Math.ceil((cy + exports.VH) / exports.TILE) + 1;
    for (let ty = y0; ty <= y1; ty++) {
        for (let tx = x0; tx <= x1; tx++) {
            const t = (0, world_1.tileAt)(m, tx, ty);
            drawTile(t, Math.round(tx * exports.TILE - cx), Math.round(ty * exports.TILE - cy), tx, ty, time);
        }
    }
    drawPlayer(Math.round(pxf * exports.TILE - cx), Math.round(pyf * exports.TILE - cy), time);
    fx_1.fx.drawParticles(ctx);
    fx_1.fx.drawFloats(ctx);
    ctx.restore();
    // edge darkening
    const vg = ctx.createRadialGradient(exports.VW / 2, exports.VH / 2, Math.min(exports.VW, exports.VH) * 0.32, exports.VW / 2, exports.VH / 2, Math.max(exports.VW, exports.VH) * 0.78);
    vg.addColorStop(0, 'rgba(0,0,0,0)');
    vg.addColorStop(1, 'rgba(0,0,0,.55)');
    ctx.fillStyle = vg;
    ctx.fillRect(0, 0, exports.VW, exports.VH);
    fx_1.fx.drawFlash(ctx, exports.VW, exports.VH);
    drawTransition();
}
// ---------- battle scene ----------
function drawPlatform(x, y, rw, color) {
    ctx.save();
    const g = ctx.createRadialGradient(x, y, 4, x, y, rw);
    g.addColorStop(0, color);
    g.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.globalAlpha = 0.55;
    ctx.fillStyle = g;
    ctx.beginPath();
    ctx.ellipse(x, y, rw, rw * 0.28, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.globalAlpha = 0.9;
    ctx.strokeStyle = color;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.ellipse(x, y, rw * 0.72, rw * 0.2, 0, 0, Math.PI * 2);
    ctx.stroke();
    ctx.restore();
}
exports.LAYOUT = { allyPos: { x: 0, y: 0, r: 1 }, foePos: [] };
function drawBattle(time) {
    const b = runtime_1.RT.battle;
    const z = (0, state_1.zoneDef)();
    const g = ctx.createLinearGradient(0, 0, exports.VW * 0.4, exports.VH);
    g.addColorStop(0, z.gradB);
    g.addColorStop(0.5, z.gradA);
    g.addColorStop(1, z.gradC);
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, exports.VW, exports.VH);
    // radial arena glow
    const rg = ctx.createRadialGradient(exports.VW * 0.5, exports.VH * 0.42, 10, exports.VW * 0.5, exports.VH * 0.42, Math.max(exports.VW, exports.VH) * 0.6);
    rg.addColorStop(0, 'rgba(255,255,255,.10)');
    rg.addColorStop(1, 'rgba(0,0,0,.5)');
    ctx.fillStyle = rg;
    ctx.fillRect(0, 0, exports.VW, exports.VH);
    // moving resonance rings
    ctx.save();
    ctx.globalAlpha = 0.12;
    ctx.strokeStyle = z.prop;
    ctx.lineWidth = 2;
    for (let i = 0; i < 5; i++) {
        const r = ((time * 40 + i * 90) % 460) + 40;
        ctx.beginPath();
        ctx.ellipse(exports.VW * 0.5, exports.VH * 0.62, r, r * 0.3, 0, 0, Math.PI * 2);
        ctx.stroke();
    }
    ctx.restore();
    if (!b) {
        fx_1.fx.drawParticles(ctx);
        drawTransition();
        return;
    }
    const sh = fx_1.fx.shakeOffset();
    ctx.save();
    ctx.translate(sh.x, sh.y);
    const scale = Math.min(exports.VW / 1280, exports.VH / 720);
    const R = 88 * Math.max(0.62, scale);
    const foes = (0, battle_1.liveFoes)(b);
    const nf = Math.max(1, foes.length);
    exports.LAYOUT = { allyPos: { x: exports.VW * 0.28, y: exports.VH * 0.66, r: R * 1.05 }, foePos: [] };
    // foes upper right
    for (let i = 0; i < nf; i++) {
        const spread = nf === 1 ? 0 : (i - (nf - 1) / 2);
        const fxp = exports.VW * 0.72 + spread * R * 1.4;
        const fyp = exports.VH * 0.34 + spread * R * 0.24;
        exports.LAYOUT.foePos.push({ x: fxp, y: fyp, r: R * 0.9 });
    }
    // platforms
    drawPlatform(exports.LAYOUT.allyPos.x, exports.LAYOUT.allyPos.y + R * 0.9, R * 1.7, z.prop);
    for (const p of exports.LAYOUT.foePos)
        drawPlatform(p.x, p.y + p.r * 0.9, p.r * 1.5, z.accent);
    const ba = runtime_1.RT.battleAnim;
    // foes
    foes.forEach((f, i) => {
        const p = exports.LAYOUT.foePos[i];
        if (!p)
            return;
        const off = ba.focus === 'foe' ? ba.foeOffset : 0;
        const shk = ba.foeShakeT > 0 ? Math.sin(ba.foeShakeT * 60) * 8 : 0;
        ctx.save();
        if (ba.foeFlash > 0) {
            ctx.shadowColor = '#fff';
            ctx.shadowBlur = 30 * ba.foeFlash;
        }
        (0, sprites_1.drawCreature)(ctx, (0, species_1.sp)(f.c.speciesId), p.x - off + shk, p.y, p.r, -1, time, 1);
        if (ba.foeFlash > 0) {
            ctx.globalCompositeOperation = 'lighter';
            ctx.globalAlpha = ba.foeFlash * 0.5;
            (0, sprites_1.drawCreature)(ctx, (0, species_1.sp)(f.c.speciesId), p.x - off + shk, p.y, p.r, -1, time, 1);
        }
        ctx.restore();
        if (f.barrier > 0)
            drawBarrier(p.x - off, p.y, p.r * 1.25, time);
    });
    // active ally
    const al = (0, battle_1.activeAlly)(b);
    if (al && al.c.hp > 0) {
        const off = ba.focus === 'ally' ? ba.allyOffset : 0;
        const shk = ba.allyShakeT > 0 ? Math.sin(ba.allyShakeT * 60) * 8 : 0;
        ctx.save();
        if (ba.allyFlash > 0) {
            ctx.shadowColor = '#fff';
            ctx.shadowBlur = 30 * ba.allyFlash;
        }
        (0, sprites_1.drawCreature)(ctx, (0, species_1.sp)(al.c.speciesId), exports.LAYOUT.allyPos.x + off + shk, exports.LAYOUT.allyPos.y, exports.LAYOUT.allyPos.r, 1, time, 1);
        ctx.restore();
        if (al.barrier > 0)
            drawBarrier(exports.LAYOUT.allyPos.x + off, exports.LAYOUT.allyPos.y, exports.LAYOUT.allyPos.r * 1.25, time);
    }
    fx_1.fx.drawParticles(ctx);
    fx_1.fx.drawFloats(ctx);
    ctx.restore();
    fx_1.fx.drawFlash(ctx, exports.VW, exports.VH);
    drawTransition();
}
function drawBarrier(x, y, r, time) {
    ctx.save();
    ctx.globalAlpha = 0.45 + Math.sin(time * 4) * 0.12;
    ctx.strokeStyle = '#dff4ff';
    ctx.lineWidth = 2.5;
    ctx.beginPath();
    ctx.ellipse(x, y, r * 0.82, r, 0, 0, Math.PI * 2);
    ctx.stroke();
    ctx.globalAlpha = 0.12;
    ctx.fillStyle = '#bfe6ff';
    ctx.fill();
    ctx.restore();
}
function drawTransition() {
    if (runtime_1.RT.transition <= 0)
        return;
    const t = runtime_1.RT.transition;
    ctx.save();
    ctx.fillStyle = '#03040a';
    const bands = 12;
    for (let i = 0; i < bands; i++) {
        const bh = exports.VH / bands;
        const w = exports.VW * (0, util_1.clamp)(t * 1.6 - (i % 2) * 0.25, 0, 1);
        const x = i % 2 === 0 ? 0 : exports.VW - w;
        ctx.fillRect(x, i * bh, w, bh + 1);
    }
    ctx.restore();
}
function screenPosFor(side, slot = 0) {
    if (side === 'ally')
        return exports.LAYOUT.allyPos;
    return exports.LAYOUT.foePos[slot] || exports.LAYOUT.allyPos;
}
function drawTitleScene(time) {
    ctx.clearRect(0, 0, exports.VW, exports.VH);
    const g = ctx.createLinearGradient(0, 0, 0, exports.VH);
    g.addColorStop(0, '#0d1330');
    g.addColorStop(1, '#03040c');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, exports.VW, exports.VH);
    ctx.save();
    for (let i = 0; i < 70; i++) {
        const x = ((0, util_1.hash2)(i, 3, 11) * exports.VW + time * (8 + (0, util_1.hash2)(i, 9, 5) * 22)) % exports.VW;
        const y = (0, util_1.hash2)(i, 7, 19) * exports.VH;
        const a = 0.2 + Math.sin(time * 2 + i) * 0.18;
        ctx.globalAlpha = Math.max(0.03, a);
        ctx.fillStyle = i % 3 === 0 ? '#9fd8ff' : '#e6c9ff';
        ctx.beginPath();
        ctx.arc(x, y, 1 + (0, util_1.hash2)(i, 1, 3) * 1.8, 0, Math.PI * 2);
        ctx.fill();
    }
    ctx.restore();
}
function statusUid(uid, hp) { return (0, runtime_1.hpAnimOf)(uid, hp); }
function nameOf(c) { return (0, creature_1.displayName)(c); }
function mhp(c) { return (0, creature_1.maxHp)(c, state_1.G.bonuses); }
function affColor(a) { return typechart_1.AFFINITY_COLOR[a]; }
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.$$ = exports.$ = void 0;
exports.show = show;
exports.hide = hide;
exports.toast = toast;
exports.say = say;
exports.dialogueOpen = dialogueOpen;
exports.advanceDialogue = advanceDialogue;
exports.cutin = cutin;
exports.setVignette = setVignette;
exports.modalOpen = modalOpen;
exports.openModal = openModal;
exports.replaceModal = replaceModal;
exports.closeModal = closeModal;
exports.closeAllModals = closeAllModals;
exports.panel = panel;
exports.confirmBox = confirmBox;
exports.bindClicks = bindClicks;
const audio_1 = require("./audio");
const util_1 = require("./util");
const $ = (sel) => document.querySelector(sel);
exports.$ = $;
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
exports.$$ = $$;
function show(el) { el.classList.remove('hidden'); }
function hide(el) { el.classList.add('hidden'); }
// ---------- toasts ----------
function toast(text) {
    const root = (0, exports.$)('#toast-root');
    const d = document.createElement('div');
    d.className = 'toast';
    d.textContent = text;
    root.appendChild(d);
    setTimeout(() => d.remove(), 2600);
}
// ---------- dialogue ----------
const dlg = () => (0, exports.$)('#dialogue');
let dlgQueue = [];
let dlgResolve = null;
let typing = null;
function renderLine() {
    const cur = dlgQueue[0];
    if (!cur)
        return;
    (0, exports.$)('#dlg-name').textContent = cur.name;
    const target = (0, exports.$)('#dlg-text');
    target.textContent = '';
    let i = 0;
    if (typing !== null)
        clearInterval(typing);
    typing = window.setInterval(() => {
        i += 2;
        target.textContent = cur.text.slice(0, i);
        if (i % 6 === 0)
            audio_1.AUD.tone(880 + (i % 3) * 60, 0.015, 'square', 0.03);
        if (i >= cur.text.length) {
            if (typing !== null)
                clearInterval(typing);
            typing = null;
        }
    }, 16);
}
function say(name, lines) {
    dlgQueue = lines.map((t) => ({ name, text: t }));
    show(dlg());
    renderLine();
    return new Promise((res) => { dlgResolve = res; });
}
function dialogueOpen() { return !dlg().classList.contains('hidden'); }
function advanceDialogue() {
    if (!dialogueOpen())
        return;
    const cur = dlgQueue[0];
    if (typing !== null && cur) {
        clearInterval(typing);
        typing = null;
        (0, exports.$)('#dlg-text').textContent = cur.text;
        return;
    }
    dlgQueue.shift();
    audio_1.AUD.sfx('ui');
    if (dlgQueue.length === 0) {
        hide(dlg());
        const r = dlgResolve;
        dlgResolve = null;
        if (r)
            r();
    }
    else
        renderLine();
}
dlg().addEventListener('click', () => advanceDialogue());
// ---------- cut-ins ----------
function cutin(title, body, color, glow) {
    const el = (0, exports.$)('#cutin');
    (0, exports.$)('#cutin-title').textContent = title;
    (0, exports.$)('#cutin-body').textContent = body;
    el.style.setProperty('--cut', color);
    el.style.setProperty('--cutglow', glow);
    show(el);
    audio_1.AUD.sfx('bound');
    return new Promise((res) => {
        const done = () => { el.removeEventListener('click', done); hide(el); res(); };
        el.addEventListener('click', done);
        setTimeout(() => { if (!el.classList.contains('hidden')) { /* keep until click */ } }, 100);
    });
}
function setVignette(tint) {
    const v = (0, exports.$)('#vignette');
    v.style.setProperty('--tint', tint);
    v.classList.add('tinted');
}
const modalStack = [];
function modalOpen() { return modalStack.length > 0; }
function openModal(html, after, onClose) {
    modalStack.push({ html, after, onClose });
    paintModal();
}
function replaceModal(html, after) {
    if (modalStack.length === 0) {
        openModal(html, after);
        return;
    }
    const top = modalStack[modalStack.length - 1];
    top.html = html;
    top.after = after;
    paintModal();
}
function closeModal() {
    const m = modalStack.pop();
    audio_1.AUD.sfx('back');
    if (m?.onClose)
        m.onClose();
    paintModal();
}
function closeAllModals() {
    while (modalStack.length) {
        const m = modalStack.pop();
        if (m?.onClose)
            m.onClose();
    }
    paintModal();
}
function paintModal() {
    const root = (0, exports.$)('#modal-root');
    if (modalStack.length === 0) {
        root.innerHTML = '';
        hide(root);
        return;
    }
    const top = modalStack[modalStack.length - 1];
    root.innerHTML = top.html;
    show(root);
    root.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => closeModal()));
    if (top.after)
        top.after(root);
}
function panel(title, sub, body, foot) {
    return `<div class="panel"><div class="panel-head"><h3>${(0, util_1.esc)(title)}</h3><span class="sub">${(0, util_1.esc)(sub)}</span></div>
  <div class="panel-body">${body}</div><div class="panel-foot">${foot}</div></div>`;
}
function confirmBox(text, onYes, yesLabel = 'Confirm') {
    openModal(panel('Confirm', '', `<div style="font-size:14px;line-height:1.7;padding:8px 2px">${(0, util_1.esc)(text)}</div>`, `<button class="btn" data-close>Cancel</button><button class="btn good" id="cf-yes">${(0, util_1.esc)(yesLabel)}</button>`), (root) => {
        const y = root.querySelector('#cf-yes');
        if (y)
            y.addEventListener('click', () => { closeModal(); onYes(); });
    });
}
function bindClicks(root, sel, fn) {
    Array.from(root.querySelectorAll(sel)).forEach((el, i) => {
        el.addEventListener('click', (e) => { e.stopPropagation(); audio_1.AUD.sfx('ui'); fn(el, i); });
    });
}
};

// ── module: src/menus.ts ──
__mods["src/menus.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.affPill = affPill;
exports.creatureCard = creatureCard;
exports.openTeam = openTeam;
exports.openCreatureDetail = openCreatureDetail;
exports.teachAbility = teachAbility;
exports.doTransform = doTransform;
exports.openBag = openBag;
exports.applyRestore = applyRestore;
exports.openShop = openShop;
exports.openDex = openDex;
exports.openMap = openMap;
exports.openPause = openPause;
exports.healParty = healParty;
const state_1 = require("./state");
const species_1 = require("./species");
const abilities_1 = require("./abilities");
const items_1 = require("./items");
const zones_1 = require("./zones");
const typechart_1 = require("./typechart");
const sprites_1 = require("./sprites");
const creature_1 = require("./creature");
const ui_1 = require("./ui");
const util_1 = require("./util");
const audio_1 = require("./audio");
const battle_1 = require("./battle");
function affPill(a) {
    return `<span class="pill" style="background:${typechart_1.AFFINITY_COLOR[a]}">${(0, util_1.esc)(a)}</span>`;
}
function hpClass(f) { return f > 0.5 ? '' : f > 0.22 ? 'mid' : 'low'; }
function creatureCard(c, extra = '', cls = '') {
    const s = (0, species_1.sp)(c.speciesId);
    const mx = (0, creature_1.maxHp)(c, state_1.G.bonuses);
    const f = Math.max(0, c.hp / mx);
    const st = c.statuses.map((x) => `<span class="stchip st-${x.kind}">${battle_1.STATUS_LABEL[x.kind]}</span>`).join('');
    return `<div class="card ${c.hp <= 0 ? 'dead' : ''} ${cls}" data-uid="${c.uid}">
    <div class="row">
      <img class="ico" src="${(0, sprites_1.creatureIcon)(s, 56)}" alt="">
      <div style="flex:1;min-width:0">
        <div class="row"><span class="nm">${(0, util_1.esc)((0, creature_1.displayName)(c))}</span><span class="spacer"></span><span class="mt">Lv ${c.level}</span></div>
        <div style="margin-top:3px">${affPill(s.affinity)}${s.affinity2 ? ' ' + affPill(s.affinity2) : ''}</div>
        <div class="bar"><i class="hpfill ${hpClass(f)}" style="width:${(f * 100).toFixed(1)}%"></i></div>
        <div class="mt" style="margin-top:4px">${c.hp} / ${mx} Vitality</div>
        <div class="bar thin"><i class="xpfill" style="width:${((0, creature_1.expProgress)(c) * 100).toFixed(1)}%"></i></div>
        <div class="stchips">${st}</div>
      </div>
    </div>${extra}</div>`;
}
// =============== TEAM ===============
function openTeam(mode = 'view') {
    const body = state_1.G.party.length === 0
        ? '<div class="empty">No kin bound yet.</div>'
        : `<div class="grid g2">${state_1.G.party.map((c) => creatureCard(c)).join('')}</div>
       ${state_1.G.vault.length ? `<div class="sectitle">Reliquary Vault (${state_1.G.vault.length})</div><div class="grid g2">${state_1.G.vault.map((c) => creatureCard(c, '', 'vault')).join('')}</div>` : ''}`;
    (0, ui_1.replaceModal)((0, ui_1.panel)('Binding — Your Kin', `${state_1.G.party.length}/6 carried · ${state_1.G.uniqueBound.length}/8 unique bound`, body, `<span class="mt">Tap a creature for details</span><span class="spacer"></span><button class="btn" data-close>Close</button>`), (root) => {
        (0, ui_1.bindClicks)(root, '.card', (el) => {
            const uid = Number(el.dataset.uid);
            const c = state_1.G.party.find((x) => x.uid === uid) || state_1.G.vault.find((x) => x.uid === uid);
            if (c)
                openCreatureDetail(c, mode);
        });
    });
}
function statRow(label, v) {
    return `<div class="stat-line"><span>${label}</span><b>${v}</b></div>`;
}
function openCreatureDetail(c, mode = 'view') {
    const s = (0, species_1.sp)(c.speciesId);
    const st = (0, creature_1.stats)(c, state_1.G.bonuses);
    const inVault = state_1.G.vault.includes(c);
    const moves = c.abilities.map((id, i) => {
        const a = (0, abilities_1.ab)(id);
        return `<div class="mvcard" data-mv="${i}">
      <div class="row"><span class="affdot" style="background:${typechart_1.AFFINITY_COLOR[a.affinity]};color:${typechart_1.AFFINITY_COLOR[a.affinity]}"></span>
      <b>${(0, util_1.esc)(a.name)}</b><span class="spacer"></span><span class="mt">${c.pp[i]}/${(0, creature_1.ppMaxOf)(id)} E</span></div>
      <div class="kv"><span>PWR ${a.power || '—'}</span><span>ACC ${a.accuracy}%</span><span>Cost ${a.cost}</span><span>${(0, util_1.esc)(a.affinity)}</span></div>
      <div class="mt" style="margin-top:4px">${(0, util_1.esc)(a.blurb)}</div></div>`;
    }).join('');
    const canT = (0, creature_1.canTransform)(c, (id) => (0, state_1.countItem)(id) > 0);
    const formInfo = s.form
        ? `<div class="mt" style="margin-top:8px">Reshaping: <b style="color:var(--gold)">${(0, util_1.esc)((0, species_1.sp)(s.form.into).name)}</b> at Lv ${s.form.level}${s.form.catalyst ? ` with ${(0, util_1.esc)((0, items_1.it)(s.form.catalyst).name)}` : ''}</div>`
        : '<div class="mt" style="margin-top:8px">This kin has reached its final shape.</div>';
    const teachable = (0, creature_1.learnsetUpTo)(s, c.level).filter((id) => !c.abilities.includes(id));
    const body = `<div class="grid g2">
    <div>
      <div class="row"><img class="ico" style="width:76px;height:76px" src="${(0, sprites_1.creatureIcon)(s, 84)}" alt="">
        <div><div class="nm" style="font-size:18px">${(0, util_1.esc)((0, creature_1.displayName)(c))}</div>
        <div class="mt">${(0, util_1.esc)(s.name)} · Lv ${c.level}${c.level >= creature_1.LEVEL_CAP ? ' (cap)' : ''}</div>
        <div style="margin-top:5px">${affPill(s.affinity)}${s.affinity2 ? ' ' + affPill(s.affinity2) : ''}</div></div></div>
      <div class="lore">${(0, util_1.esc)(s.lore)}</div>
      ${statRow('Vitality', `${c.hp} / ${(0, creature_1.maxHp)(c, state_1.G.bonuses)}`)}
      ${statRow('Force (ATK)', st.atk)}
      ${statRow('Ward (DEF)', st.def)}
      ${statRow('Tempo (SPD)', st.spd)}
      ${statRow('Attunement (Bond)', c.bond)}
      ${formInfo}
    </div>
    <div><div class="sectitle">Resonances Known</div>${moves}
    ${teachable.length ? `<div class="sectitle">Available to Teach</div><div class="grid" style="gap:8px">${teachable.map((id) => `<div class="mvcard" data-teach="${id}"><b>${(0, util_1.esc)((0, abilities_1.ab)(id).name)}</b><div class="kv"><span>PWR ${(0, abilities_1.ab)(id).power || '—'}</span><span>${(0, util_1.esc)((0, abilities_1.ab)(id).affinity)}</span><span>Cost ${(0, abilities_1.ab)(id).cost}</span></div></div>`).join('')}</div>` : ''}
    </div></div>`;
    const foot = `${canT ? '<button class="btn good" id="do-form">Reshape</button>' : ''}
    ${state_1.G.party.length > 1 || inVault ? `<button class="btn" id="do-move">${inVault ? 'Carry' : 'To Vault'}</button>` : ''}
    <button class="btn" id="do-name">Rename</button>
    <span class="spacer"></span><button class="btn" data-close>Back</button>`;
    (0, ui_1.openModal)((0, ui_1.panel)('Kin Record', (0, util_1.esc)(s.name), body, foot), (root) => {
        const t = root.querySelector('#do-form');
        if (t)
            t.addEventListener('click', () => doTransform(c));
        const mv = root.querySelector('#do-move');
        if (mv)
            mv.addEventListener('click', () => {
                if (inVault) {
                    if (state_1.G.party.length >= 6) {
                        (0, ui_1.toast)('Your binding already carries six.');
                        return;
                    }
                    state_1.G.vault.splice(state_1.G.vault.indexOf(c), 1);
                    state_1.G.party.push(c);
                }
                else {
                    if (state_1.G.party.filter((x) => x.hp > 0).length <= 1 && c.hp > 0) {
                        (0, ui_1.toast)('You must carry at least one waking kin.');
                        return;
                    }
                    state_1.G.party.splice(state_1.G.party.indexOf(c), 1);
                    state_1.G.vault.push(c);
                }
                (0, ui_1.closeModal)();
                openTeam(mode);
            });
        const nm = root.querySelector('#do-name');
        if (nm)
            nm.addEventListener('click', () => {
                const v = window.prompt('Name this kin:', (0, creature_1.displayName)(c));
                if (v && v.trim()) {
                    c.nickname = v.trim().slice(0, 14);
                    (0, ui_1.closeModal)();
                    openCreatureDetail(c, mode);
                }
            });
        (0, ui_1.bindClicks)(root, '[data-teach]', (el) => {
            const id = el.dataset.teach;
            teachAbility(c, id);
        });
    });
}
function teachAbility(c, id) {
    const a = (0, abilities_1.ab)(id);
    if (c.abilities.length < 4) {
        c.abilities.push(id);
        c.pp.push((0, creature_1.ppMaxOf)(id));
        (0, ui_1.toast)(`${(0, creature_1.displayName)(c)} learned ${a.name}.`);
        audio_1.AUD.sfx('levelup');
        (0, ui_1.closeModal)();
        openCreatureDetail(c);
        return;
    }
    const body = `<div style="font-size:14px;line-height:1.7;margin-bottom:12px">${(0, util_1.esc)((0, creature_1.displayName)(c))} already holds four Resonances. Which is released to make room for <b style="color:var(--gold)">${(0, util_1.esc)(a.name)}</b>?</div>
    <div class="grid g2">${c.abilities.map((x, i) => `<div class="mvcard" data-drop="${i}"><b>${(0, util_1.esc)((0, abilities_1.ab)(x).name)}</b><div class="kv"><span>PWR ${(0, abilities_1.ab)(x).power || '—'}</span><span>${(0, util_1.esc)((0, abilities_1.ab)(x).affinity)}</span></div></div>`).join('')}</div>`;
    (0, ui_1.openModal)((0, ui_1.panel)('Release a Resonance', (0, util_1.esc)(a.name), body, '<button class="btn" data-close>Keep all four</button>'), (root) => {
        (0, ui_1.bindClicks)(root, '[data-drop]', (el) => {
            const i = Number(el.dataset.drop);
            c.abilities[i] = id;
            c.pp[i] = (0, creature_1.ppMaxOf)(id);
            (0, ui_1.toast)(`${(0, creature_1.displayName)(c)} learned ${a.name}.`);
            audio_1.AUD.sfx('levelup');
            (0, ui_1.closeAllModals)();
            openTeam();
        });
    });
}
async function doTransform(c) {
    const s = (0, species_1.sp)(c.speciesId);
    if (!s.form)
        return;
    if (s.form.catalyst) {
        if (!(0, state_1.takeItem)(s.form.catalyst)) {
            (0, ui_1.toast)('The catalyst is missing.');
            return;
        }
    }
    (0, ui_1.closeAllModals)();
    audio_1.AUD.sfx('transform');
    const before = s.name;
    const into = (0, creature_1.transform)(c, state_1.G.bonuses);
    if (!into)
        return;
    const ns = (0, species_1.sp)(into);
    await (0, ui_1.cutin)('Reshaping', `${before} unbinds its own outline and re-forms. It answers now as ${ns.name}.`, typechart_1.AFFINITY_COLOR[ns.affinity], typechart_1.AFFINITY_COLOR[ns.affinity] + '99');
    state_1.G.dex[into] = { seen: true, bound: true };
    if (!state_1.G.uniqueBound.includes(into))
        state_1.G.uniqueBound.push(into);
    (0, ui_1.toast)(`${before} became ${ns.name}!`);
}
// =============== BAG ===============
const KIND_LABEL = {
    bind: 'Reliquaries', restore: 'Restoratives', catalyst: 'Catalysts', tool: 'Field Tools', lore: 'Records',
};
function openBag(onUseInBattle) {
    const ids = Object.keys(state_1.G.bag).filter((k) => state_1.G.bag[k] > 0);
    const groups = {};
    for (const id of ids) {
        const item = items_1.ITEM_MAP[id];
        if (!item)
            continue;
        (groups[item.kind] = groups[item.kind] || []).push(id);
    }
    const inBattle = !!onUseInBattle;
    const body = ids.length === 0 ? '<div class="empty">Your satchel is empty.</div>'
        : Object.keys(groups).map((k) => `<div class="sectitle">${KIND_LABEL[k] || k}</div><div class="grid g2">${groups[k].map((id) => {
            const item = (0, items_1.it)(id);
            const usable = !inBattle || item.battle;
            return `<div class="card ${usable ? '' : 'locked'}" data-item="${id}">
        <div class="row"><span class="nm">${(0, util_1.esc)(item.name)}</span><span class="spacer"></span><span class="mt">×${state_1.G.bag[id]}</span></div>
        <div class="mt" style="margin-top:5px;line-height:1.5">${(0, util_1.esc)(item.desc)}</div></div>`;
        }).join('')}</div>`).join('');
    const html = (0, ui_1.panel)('Satchel', `${state_1.G.shards} Resonance Shards`, body, '<button class="btn" data-close>Close</button>');
    const wire = (root) => {
        (0, ui_1.bindClicks)(root, '.card[data-item]:not(.locked)', (el) => {
            const id = el.dataset.item;
            if (onUseInBattle) {
                onUseInBattle(id);
                return;
            }
            useItemField(id);
        });
    };
    if (inBattle)
        (0, ui_1.openModal)(html, wire);
    else
        (0, ui_1.replaceModal)(html, wire);
}
function useItemField(id) {
    const item = (0, items_1.it)(id);
    if (item.kind === 'restore') {
        pickTargetForRestore(id);
        return;
    }
    if (item.kind === 'tool') {
        if (id === 'tool_lure') {
            (0, state_1.takeItem)(id);
            state_1.G.lureSteps = 120;
            state_1.G.wardSteps = 0;
            (0, ui_1.toast)('Resonance Lure hums. Kin draw near.');
            (0, ui_1.closeModal)();
            return;
        }
        if (id === 'tool_ward') {
            (0, state_1.takeItem)(id);
            state_1.G.wardSteps = 120;
            state_1.G.lureSteps = 0;
            (0, ui_1.toast)('Quietstep Ward settles. The wilds ignore you.');
            (0, ui_1.closeModal)();
            return;
        }
        if (id === 'tool_chart') {
            openMap();
            return;
        }
        (0, ui_1.toast)('That tool only works in a binding contest.');
        return;
    }
    if (item.kind === 'catalyst') {
        const eligible = [...state_1.G.party, ...state_1.G.vault].filter((c) => {
            const f = (0, species_1.sp)(c.speciesId).form;
            return f && f.catalyst === id && c.level >= f.level;
        });
        if (eligible.length === 0) {
            (0, ui_1.toast)('No carried kin is ready for this catalyst.');
            return;
        }
        const body = `<div class="grid g2">${eligible.map((c) => creatureCard(c)).join('')}</div>`;
        (0, ui_1.openModal)((0, ui_1.panel)('Apply Catalyst', (0, util_1.esc)(item.name), body, '<button class="btn" data-close>Back</button>'), (root) => {
            (0, ui_1.bindClicks)(root, '.card', (el) => {
                const c = eligible.find((x) => x.uid === Number(el.dataset.uid));
                if (c)
                    void doTransform(c);
            });
        });
        return;
    }
    (0, ui_1.toast)('Nothing happens here.');
}
function pickTargetForRestore(id) {
    const item = (0, items_1.it)(id);
    const targets = [...state_1.G.party];
    const body = `<div class="grid g2">${targets.map((c) => creatureCard(c)).join('')}</div>`;
    (0, ui_1.openModal)((0, ui_1.panel)('Use ' + item.name, 'Choose a kin', body, '<button class="btn" data-close>Back</button>'), (root) => {
        (0, ui_1.bindClicks)(root, '.card', (el) => {
            const c = targets.find((x) => x.uid === Number(el.dataset.uid));
            if (!c)
                return;
            const r = applyRestore(id, c);
            if (r) {
                (0, ui_1.closeModal)();
                openBag();
            }
        });
    });
}
/** Returns true if the item was consumed. */
function applyRestore(id, c) {
    const item = (0, items_1.it)(id);
    const mx = (0, creature_1.maxHp)(c, state_1.G.bonuses);
    if (id === 'res_tonic') {
        if (c.statuses.filter((s) => s.kind !== 'regen').length === 0) {
            (0, ui_1.toast)('No ailment to clear.');
            return false;
        }
        c.statuses = c.statuses.filter((s) => s.kind === 'regen');
        (0, state_1.takeItem)(id);
        audio_1.AUD.sfx('heal');
        (0, ui_1.toast)(`${(0, creature_1.displayName)(c)} is cleared.`);
        return true;
    }
    if (id === 'res_ember') {
        if (c.hp > 0) {
            (0, ui_1.toast)('That kin is already awake.');
            return false;
        }
        c.hp = Math.floor(mx / 2);
        (0, state_1.takeItem)(id);
        audio_1.AUD.sfx('bound');
        (0, ui_1.toast)(`${(0, creature_1.displayName)(c)} is roused.`);
        return true;
    }
    if (c.hp <= 0) {
        (0, ui_1.toast)('That kin cannot be mended while unbound.');
        return false;
    }
    if (c.hp >= mx) {
        (0, ui_1.toast)('Already at full Vitality.');
        return false;
    }
    c.hp = Math.min(mx, c.hp + (item.power || 0));
    (0, state_1.takeItem)(id);
    audio_1.AUD.sfx('heal');
    (0, ui_1.toast)(`${(0, creature_1.displayName)(c)} recovers.`);
    return true;
}
// =============== SHOP ===============
function openShop(stock) {
    const body = `<div class="grid g2">${stock.map((id) => {
        const item = (0, items_1.it)(id);
        const afford = state_1.G.shards >= item.price;
        return `<div class="card ${afford ? '' : 'locked'}" data-buy="${id}">
      <div class="row"><span class="nm">${(0, util_1.esc)(item.name)}</span><span class="spacer"></span><span class="mt" style="color:${afford ? 'var(--gold)' : 'var(--bad)'}">${item.price} ◈</span></div>
      <div class="mt" style="margin-top:5px;line-height:1.5">${(0, util_1.esc)(item.desc)}</div>
      <div class="mt" style="margin-top:4px">Carried: ${(0, state_1.countItem)(id)}</div></div>`;
    }).join('')}</div>`;
    (0, ui_1.replaceModal)((0, ui_1.panel)('Shrine Exchange', `${state_1.G.shards} Resonance Shards`, body, '<span class="mt">Shards are cut from stilled resonance.</span><span class="spacer"></span><button class="btn" data-close>Leave</button>'), (root) => {
        (0, ui_1.bindClicks)(root, '.card[data-buy]:not(.locked)', (el) => {
            const id = el.dataset.buy;
            const item = (0, items_1.it)(id);
            if (state_1.G.shards < item.price)
                return;
            state_1.G.shards -= item.price;
            (0, state_1.addItem)(id);
            (0, ui_1.toast)(`Bought ${item.name}.`);
            openShop(stock);
        });
    });
}
// =============== ENCYCLOPEDIA ===============
function openDex() {
    const seenCount = Object.values(state_1.G.dex).filter((d) => d.seen).length;
    const boundCount = Object.values(state_1.G.dex).filter((d) => d.bound).length;
    const body = `<div class="tabs" style="padding:0 0 12px">
      <button class="tab on" data-tab="kin">Kin</button>
      <button class="tab" data-tab="res">Resonances</button>
      <button class="tab" data-tab="chart">Concordance</button>
      <button class="tab" data-tab="lore">World</button>
    </div><div id="dexbody"></div>`;
    (0, ui_1.replaceModal)((0, ui_1.panel)('Codex of Bindings', `${seenCount} witnessed · ${boundCount} bound of ${species_1.SPECIES.length}`, body, '<button class="btn" data-close>Close</button>'), (root) => {
        const paint = (tab) => {
            const el = root.querySelector('#dexbody');
            el.innerHTML = dexTab(tab);
            if (tab === 'kin') {
                (0, ui_1.bindClicks)(el, '.card:not(.locked)', (c) => {
                    const id = c.dataset.spid;
                    openSpeciesDetail(id);
                });
            }
        };
        (0, ui_1.bindClicks)(root, '.tab', (el) => {
            root.querySelectorAll('.tab').forEach((t) => t.classList.remove('on'));
            el.classList.add('on');
            paint(el.dataset.tab);
        });
        paint('kin');
    });
}
function dexTab(tab) {
    if (tab === 'kin') {
        return `<div class="grid g3">${species_1.SPECIES.map((s) => {
            const d = state_1.G.dex[s.id];
            if (!d || !d.seen)
                return `<div class="card locked"><div class="row"><div class="ico"></div><div><div class="nm">— — —</div><div class="mt">Unwitnessed</div></div></div></div>`;
            return `<div class="card" data-spid="${s.id}"><div class="row"><img class="ico" src="${(0, sprites_1.creatureIcon)(s, 56)}" alt="">
        <div><div class="nm">${(0, util_1.esc)(s.name)}</div><div style="margin-top:4px">${affPill(s.affinity)}</div>
        <div class="mt" style="margin-top:4px">${d.bound ? '◈ Bound' : 'Witnessed'}</div></div></div></div>`;
        }).join('')}</div>`;
    }
    if (tab === 'res') {
        const byAff = typechart_1.AFFINITIES.map((a) => {
            const list = abilities_1.ABILITIES.filter((x) => x.affinity === a);
            return `<div class="sectitle">${a} — ${list.length}</div><div class="grid g2">${list.map((x) => `<div class="mvcard">
        <div class="row"><span class="affdot" style="background:${typechart_1.AFFINITY_COLOR[a]};color:${typechart_1.AFFINITY_COLOR[a]}"></span><b>${(0, util_1.esc)(x.name)}</b></div>
        <div class="kv"><span>PWR ${x.power || '—'}</span><span>ACC ${x.accuracy}%</span><span>Cost ${x.cost}</span><span>Effect: ${(0, util_1.esc)(x.effect.kind)}</span></div>
        <div class="mt" style="margin-top:4px">${(0, util_1.esc)(x.blurb)}</div></div>`).join('')}</div>`;
        }).join('');
        return byAff;
    }
    if (tab === 'chart') {
        const head = `<div class="hd"></div>` + typechart_1.AFFINITIES.map((a) => `<div class="hd" style="color:${typechart_1.AFFINITY_COLOR[a]}">${a.slice(0, 4)}</div>`).join('');
        const rows = typechart_1.AFFINITIES.map((atk) => `<div class="hd" style="color:${typechart_1.AFFINITY_COLOR[atk]}">${atk.slice(0, 4)}</div>` + typechart_1.AFFINITIES.map((def) => {
            const m = (0, typechart_1.typeMult)(atk, def);
            const col = m >= 2 ? 'rgba(120,255,170,.28)' : m > 1 ? 'rgba(200,255,140,.18)' : m === 1 ? 'rgba(255,255,255,.05)' : 'rgba(255,120,120,.22)';
            return `<div style="background:${col}">${m === 1 ? '·' : '×' + m}</div>`;
        }).join('')).join('');
        return `<div class="mt" style="margin-bottom:12px;line-height:1.7">Rows attack, columns defend. Ember consumes Briar; Briar drinks Tide; Tide drowns Ember. Gale severs both Ember and Tide. Lumen exposes Gale and Briar. Hush gains a slight edge on all. Aeon dulls everything — except Hush, which it cannot dull at all.</div>
      <div class="chartgrid">${head}${rows}</div>
      <div class="sectitle">The Seven Resonances</div>
      ${typechart_1.AFFINITIES.map((a) => `<div class="stat-line"><span>${affPill(a)}</span><span style="max-width:70%;text-align:right;font-style:italic;color:#b9c6e4">${(0, util_1.esc)(typechart_1.AFFINITY_LORE[a])}</span></div>`).join('')}`;
    }
    return `<div class="lore" style="font-size:13.5px">The Song is not a sound. It is an agreement between everything that exists and the fact of its existing.</div>
    <div class="sectitle">The Binding Craft</div>
    <div class="mt" style="line-height:1.8">A Binder carries reliquaries — vessels tuned to hold a creature's resonance without silencing it. A well-made binding is a contract: the kin lends you its note, and you promise to keep singing.</div>
    <div class="sectitle">The Four Anchors</div>
    <div class="mt" style="line-height:1.8">Ash, Rime, Root and Noon. Driven into the world where the Song runs thinnest. Still all four and the Coda will open — which is either a repair or an invitation, depending on who you ask.</div>
    <div class="sectitle">Progress</div>
    <div class="stat-line"><span>Unique kin bound</span><b>${state_1.G.uniqueBound.length} / 8</b></div>
    <div class="stat-line"><span>Anchors stilled</span><b>${state_1.G.anchorsStilled} / 4</b></div>
    <div class="stat-line"><span>Zones opened</span><b>${state_1.G.unlockedZones.length} / ${zones_1.ZONES.length}</b></div>
    <div class="stat-line"><span>Paces walked</span><b>${state_1.G.steps}</b></div>
    ${state_1.G.boons.length ? `<div class="sectitle">Boons Earned</div>${state_1.G.boons.map((b) => `<div class="stat-line"><span>${(0, util_1.esc)(b)}</span><b>◈</b></div>`).join('')}` : ''}`;
}
function openSpeciesDetail(id) {
    const s = (0, species_1.sp)(id);
    const body = `<div class="row"><img class="ico" style="width:88px;height:88px" src="${(0, sprites_1.creatureIcon)(s, 96)}" alt="">
    <div><div class="nm" style="font-size:19px">${(0, util_1.esc)(s.name)}</div>
    <div style="margin-top:5px">${affPill(s.affinity)}${s.affinity2 ? ' ' + affPill(s.affinity2) : ''}</div>
    <div class="mt" style="margin-top:5px">Stage ${s.stage} · Rarity ${'★'.repeat(s.rarity)}</div></div></div>
    <div class="lore">${(0, util_1.esc)(s.lore)}</div>
    <div class="sectitle">Base Resonance Profile</div>
    <div class="stat-line"><span>Vitality</span><b>${s.base.hp}</b></div>
    <div class="stat-line"><span>Force</span><b>${s.base.atk}</b></div>
    <div class="stat-line"><span>Ward</span><b>${s.base.def}</b></div>
    <div class="stat-line"><span>Tempo</span><b>${s.base.spd}</b></div>
    ${s.form ? `<div class="sectitle">Reshaping</div><div class="mt">Becomes <b style="color:var(--gold)">${(0, util_1.esc)((0, species_1.sp)(s.form.into).name)}</b> at Lv ${s.form.level}${s.form.catalyst ? ` in the presence of ${(0, util_1.esc)((0, items_1.it)(s.form.catalyst).name)}` : ''}.</div>` : ''}
    <div class="sectitle">Learnset</div>
    ${s.learn.map((l) => `<div class="stat-line"><span>Lv ${l.lv}</span><b>${(0, util_1.esc)((0, abilities_1.ab)(l.ability).name)}</b></div>`).join('')}`;
    (0, ui_1.openModal)((0, ui_1.panel)('Codex Entry', (0, util_1.esc)(s.name), body, '<button class="btn" data-close>Back</button>'));
}
// =============== MAP ===============
function openMap() {
    const body = `<div class="grid g2">${zones_1.ZONES.map((z, i) => {
        const open = state_1.G.unlockedZones.includes(i);
        const here = state_1.G.zone === i;
        return `<div class="card ${open ? '' : 'locked'}" style="${here ? 'border-color:var(--gold)' : ''}">
      <div class="row"><span class="nm">${open ? (0, util_1.esc)(z.name) : 'Sealed Threadwork'}</span><span class="spacer"></span>
      <span class="mt">${here ? 'HERE' : open ? 'Open' : 'Sealed'}</span></div>
      ${open ? `<div class="lore" style="margin:6px 0 2px">${(0, util_1.esc)(z.epithet)}</div>
      <div class="mt">Wild kin Lv ${z.levels[0]}–${z.levels[1]}${z.guardian ? ` · ${state_1.G.guardiansBeaten.includes(z.guardian.name) ? 'Guardian stilled' : 'Guardian: ' + (0, util_1.esc)(z.guardian.name)}` : ''}</div>` : '<div class="mt" style="margin-top:6px">Still the guardian before to unravel this seal.</div>'}
      </div>`;
    }).join('')}</div>`;
    (0, ui_1.replaceModal)((0, ui_1.panel)('Wayfarer\u2019s Chart', (0, util_1.esc)((0, state_1.zoneDef)().name), body, '<button class="btn" data-close>Close</button>'));
}
// =============== PAUSE ===============
function openPause(onQuit) {
    const z = (0, state_1.zoneDef)();
    const body = `<div class="grid g3">
      <div class="card" data-go="team"><div class="nm">Binding</div><div class="mt">Your kin, their resonances, reshaping</div></div>
      <div class="card" data-go="bag"><div class="nm">Satchel</div><div class="mt">Reliquaries, restoratives, catalysts</div></div>
      <div class="card" data-go="dex"><div class="nm">Codex</div><div class="mt">Kin, resonances, concordance, lore</div></div>
      <div class="card" data-go="map"><div class="nm">Chart</div><div class="mt">Threadwork of the six zones</div></div>
      <div class="card" data-go="save"><div class="nm">Inscribe</div><div class="mt">Write your progress to the stele</div></div>
      <div class="card" data-go="quit"><div class="nm">Abandon</div><div class="mt">Return to the title</div></div>
    </div>
    <div class="sectitle">Standing</div>
    <div class="stat-line"><span>Zone</span><b>${(0, util_1.esc)(z.name)}</b></div>
    <div class="stat-line"><span>Unique kin bound</span><b>${state_1.G.uniqueBound.length} / 8</b></div>
    <div class="stat-line"><span>Anchors stilled</span><b>${state_1.G.anchorsStilled} / 4</b></div>
    <div class="stat-line"><span>Shards</span><b>${state_1.G.shards} ◈</b></div>
    ${state_1.G.postgame ? '<div class="stat-line"><span>Coda</span><b style="color:var(--rose)">Silenced — free roam</b></div>' : ''}`;
    (0, ui_1.openModal)((0, ui_1.panel)('Interlude', (0, util_1.esc)(state_1.G.playerName) + ' the Binder', body, '<button class="btn" data-close>Resume</button>'), (root) => {
        (0, ui_1.bindClicks)(root, '[data-go]', (el) => {
            const go = el.dataset.go;
            if (go === 'team')
                openTeam();
            else if (go === 'bag')
                openBag();
            else if (go === 'dex')
                openDex();
            else if (go === 'map')
                openMap();
            else if (go === 'save') {
                if ((0, state_1.saveGame)())
                    (0, ui_1.toast)('Inscribed. The stele remembers.');
                else
                    (0, ui_1.toast)('The stele refuses your mark.');
            }
            else if (go === 'quit')
                (0, ui_1.confirmBox)('Abandon this walk and return to the title? Unsaved progress is lost.', () => { (0, ui_1.closeAllModals)(); onQuit(); }, 'Abandon');
        });
    });
}
function healParty() {
    for (const c of [...state_1.G.party, ...state_1.G.vault])
        (0, creature_1.healFull)(c, state_1.G.bonuses);
}
};

// ── module: src/battleui.ts ──
__mods["src/battleui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderPanels = renderPanels;
exports.syncBars = syncBars;
exports.enterBattle = enterBattle;
exports.battleActive = battleActive;
const abilities_1 = require("./abilities");
const species_1 = require("./species");
const state_1 = require("./state");
const creature_1 = require("./creature");
const battle_1 = require("./battle");
const runtime_1 = require("./runtime");
const ui_1 = require("./ui");
const menus_1 = require("./menus");
const ui_2 = require("./ui");
const fx_1 = require("./fx");
const render_1 = require("./render");
const typechart_1 = require("./typechart");
const audio_1 = require("./audio");
const util_1 = require("./util");
const ui = () => (0, ui_1.$)('#battle-ui');
let resolveChoice = null;
let onBattleEnd = null;
let participants = [];
// ---------------------------------------------------------------- scaffolding
function buildUI() {
    ui().innerHTML = `<div class="layer" id="bpanels"></div>
    <div id="bcmd"><div id="blog">…</div><div id="bmenu" class="main"></div></div>`;
    (0, ui_1.show)(ui());
}
function log(text) { (0, ui_1.$)('#blog').innerHTML = text; }
function panelHtml(k, side, idx, total) {
    const mx = (0, creature_1.maxHp)(k.c, k.side === 'ally' ? state_1.G.bonuses : undefined);
    const st = k.c.statuses.map((s) => `<span class="stchip st-${s.kind}">${battle_1.STATUS_LABEL[s.kind]}</span>`).join('')
        + (k.barrier > 0 ? '<span class="stchip st-barrier">Deflect</span>' : '');
    const pos = side === 'ally'
        ? `left:3.5%;bottom:calc(22px + 150px)`
        : `right:3.5%;top:${72 + idx * 84}px`;
    const s = (0, species_1.sp)(k.c.speciesId);
    return `<div class="bstatus" style="${pos}" data-uid="${k.c.uid}">
    <div class="r1"><span class="nmx">${(0, util_1.esc)((0, creature_1.displayName)(k.c))}</span><span class="lv">Lv ${k.c.level}</span></div>
    <div style="margin-top:3px">${(0, menus_1.affPill)(s.affinity)}${s.affinity2 ? ' ' + (0, menus_1.affPill)(s.affinity2) : ''}</div>
    <div class="bar"><i class="hpfill" data-bar="${k.c.uid}" style="width:100%"></i></div>
    <div class="hpnum" data-num="${k.c.uid}">${k.c.hp} / ${mx}</div>
    <div class="stchips" data-st="${k.c.uid}">${st}</div></div>`;
}
function renderPanels() {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const foes = b.foes.filter((f) => !(0, creature_1.isFainted)(f.c));
    const al = (0, battle_1.activeAlly)(b);
    const parts = [];
    if (al)
        parts.push(panelHtml(al, 'ally', 0, 1));
    foes.forEach((f, i) => parts.push(panelHtml(f, 'foe', i, foes.length)));
    (0, ui_1.$)('#bpanels').innerHTML = parts.join('');
    syncBars(true);
}
/** Called every frame from the main loop: tween HP bars. */
function syncBars(instant = false) {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const all = [...b.allies, ...b.foes];
    for (const k of all) {
        const bar = document.querySelector(`[data-bar="${k.c.uid}"]`);
        if (!bar)
            continue;
        const mx = (0, creature_1.maxHp)(k.c, k.side === 'ally' ? state_1.G.bonuses : undefined);
        let cur = (0, runtime_1.hpAnimOf)(k.c.uid, k.c.hp);
        if (instant)
            cur = k.c.hp;
        else
            cur += (k.c.hp - cur) * 0.16;
        if (Math.abs(cur - k.c.hp) < 0.6)
            cur = k.c.hp;
        (0, runtime_1.setHpAnim)(k.c.uid, cur);
        const f = (0, util_1.clamp)(cur / mx, 0, 1);
        bar.style.width = (f * 100).toFixed(1) + '%';
        bar.className = 'hpfill ' + (f > 0.5 ? '' : f > 0.22 ? 'mid' : 'low');
        const num = document.querySelector(`[data-num="${k.c.uid}"]`);
        if (num)
            num.textContent = `${Math.max(0, Math.round(cur))} / ${mx}`;
        const stc = document.querySelector(`[data-st="${k.c.uid}"]`);
        if (stc) {
            stc.innerHTML = k.c.statuses.map((s) => `<span class="stchip st-${s.kind}">${battle_1.STATUS_LABEL[s.kind]}</span>`).join('')
                + (k.barrier > 0 ? '<span class="stchip st-barrier">Deflect</span>' : '');
        }
    }
}
// ---------------------------------------------------------------- menus
function menu(cls, html) {
    const m = (0, ui_1.$)('#bmenu');
    m.className = cls;
    m.innerHTML = html;
    return m;
}
function btn(t, d, attr, disabled = false, dotColor) {
    return `<button class="bbtn${d ? '' : ' center'}" ${attr} ${disabled ? 'disabled' : ''}>
    <span class="t">${dotColor ? `<span class="affdot" style="background:${dotColor};color:${dotColor}"></span>` : ''}${(0, util_1.esc)(t)}</span>
    ${d ? `<span class="d">${d}</span>` : ''}</button>`;
}
function showMain() {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const hasBind = Object.keys(state_1.G.bag).some((k) => k.startsWith('bind_') && state_1.G.bag[k] > 0);
    const m = menu('main', [
        btn('Resonate', '', 'data-a="move"'),
        btn('Bind', '', 'data-a="bind"', !b.wild || !hasBind),
        btn('Satchel', '', 'data-a="item"'),
        btn(b.wild ? 'Withdraw' : 'Swap', '', 'data-a="' + (b.wild ? 'flee' : 'swap') + '"'),
    ].join(''));
    (0, ui_2.bindClicks)(m, '[data-a]', (el) => {
        const a = el.dataset.a;
        if (a === 'move')
            showMoves();
        else if (a === 'bind')
            showBindItems();
        else if (a === 'item')
            showItems();
        else if (a === 'swap')
            showSwap();
        else if (a === 'flee')
            choose({ kind: 'flee' });
    });
    log(`<b>${(0, util_1.esc)((0, creature_1.displayName)((0, battle_1.activeAlly)(b).c))}</b> awaits your call. <span style="color:var(--ink-dim)">Round ${b.round}</span>`);
}
function showMoves() {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const c = (0, battle_1.activeAlly)(b).c;
    const items = c.abilities.map((id, i) => {
        const a = (0, abilities_1.ab)(id);
        const canUse = c.pp[i] >= a.cost;
        return btn(a.name, `PWR ${a.power || '—'} · ACC ${a.accuracy}% · E ${c.pp[i]}/${a.maxPP} · ${a.effect.kind !== 'none' ? (0, util_1.esc)(a.effect.kind) : 'plain'}`, `data-mv="${i}"`, !canUse, typechart_1.AFFINITY_COLOR[a.affinity]);
    }).join('');
    const m = menu('moves', items + btn('Back', '', 'data-back="1"'));
    (0, ui_2.bindClicks)(m, '[data-mv]', (el) => {
        const i = Number(el.dataset.mv);
        const id = c.abilities[i];
        const foes = (0, battle_1.liveFoes)(b);
        const a = (0, abilities_1.ab)(id);
        if (a.effect.onSelf || a.power === 0 || foes.length === 1) {
            choose({ kind: 'move', ability: id, target: foes[0] || (0, battle_1.activeAlly)(b) });
        }
        else
            showTargets(id);
    });
    (0, ui_2.bindClicks)(m, '[data-back]', () => showMain());
    log('Choose a Resonance.');
}
function showTargets(abilityId) {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const foes = (0, battle_1.liveFoes)(b);
    const m = menu('list', foes.map((f, i) => btn((0, creature_1.displayName)(f.c), `Lv ${f.c.level} · ${f.c.hp}/${(0, creature_1.maxHp)(f.c)} · ${(0, species_1.sp)(f.c.speciesId).affinity}`, `data-tg="${i}"`))
        .join('') + btn('Back', '', 'data-back="1"'));
    (0, ui_2.bindClicks)(m, '[data-tg]', (el) => choose({ kind: 'move', ability: abilityId, target: foes[Number(el.dataset.tg)] }));
    (0, ui_2.bindClicks)(m, '[data-back]', () => showMoves());
    log(`Aim <b>${(0, util_1.esc)((0, abilities_1.ab)(abilityId).name)}</b> at which kin?`);
}
function showBindItems() {
    const ids = Object.keys(state_1.G.bag).filter((k) => k.startsWith('bind_') && state_1.G.bag[k] > 0);
    const m = menu('list', ids.map((id) => {
        const item = ITEMDESC(id);
        return btn(item.name, `×${state_1.G.bag[id]} · quality ${item.power}`, `data-bi="${id}"`);
    }).join('') + btn('Back', '', 'data-back="1"'));
    (0, ui_2.bindClicks)(m, '[data-bi]', (el) => choose({ kind: 'bind', item: el.dataset.bi }));
    (0, ui_2.bindClicks)(m, '[data-back]', () => showMain());
    log('Which reliquary?');
}
function ITEMDESC(id) {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const map = {
        bind_clay: { name: 'Clay Reliquary', power: 1.0 },
        bind_glass: { name: 'Glass Reliquary', power: 1.6 },
        bind_choir: { name: 'Choirshell Reliquary', power: 2.6 },
    };
    return map[id] || { name: id, power: 1 };
}
function showItems() {
    (0, menus_1.openBag)((id) => {
        const item = id;
        (0, ui_2.closeModal)();
        if (item === 'tool_tuner') {
            choose({ kind: 'item', item });
            return;
        }
        if (item.startsWith('bind_')) {
            choose({ kind: 'bind', item });
            return;
        }
        // restorative → choose target
        const targets = state_1.G.party;
        (0, ui_2.openModal)((0, ui_2.panel)('Use', 'Choose a kin', `<div class="grid g2">${targets.map((c) => (0, menus_1.creatureCard)(c)).join('')}</div>`, '<button class="btn" data-close>Back</button>'), (root) => {
            (0, ui_2.bindClicks)(root, '.card', (el) => {
                const c = targets.find((x) => x.uid === Number(el.dataset.uid));
                if (!c)
                    return;
                (0, ui_2.closeModal)();
                choose({ kind: 'item', item, target: c });
            });
        });
    });
}
function showSwap() {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const opts = b.allies.map((a, i) => ({ a, i })).filter((x) => !(0, creature_1.isFainted)(x.a.c) && x.i !== b.active);
    if (opts.length === 0) {
        log('No other waking kin to call.');
        setTimeout(() => showMain(), 700);
        return;
    }
    const m = menu('list', opts.map((o) => btn((0, creature_1.displayName)(o.a.c), `Lv ${o.a.c.level} · ${o.a.c.hp}/${(0, creature_1.maxHp)(o.a.c, state_1.G.bonuses)}`, `data-sw="${o.i}"`)).join('') + btn('Back', '', 'data-back="1"'));
    (0, ui_2.bindClicks)(m, '[data-sw]', (el) => choose({ kind: 'swap', to: Number(el.dataset.sw) }));
    (0, ui_2.bindClicks)(m, '[data-back]', () => showMain());
    log('Call which kin forward?');
}
function choose(a) {
    const r = resolveChoice;
    resolveChoice = null;
    menu('main', '');
    if (r)
        r(a);
}
function awaitChoice() {
    showMain();
    return new Promise((res) => { resolveChoice = res; });
}
// ---------------------------------------------------------------- fx helpers
function posOf(k) {
    const b = runtime_1.RT.battle;
    if (!b)
        return { x: 0, y: 0, r: 40 };
    if (k.side === 'ally')
        return (0, render_1.screenPosFor)('ally');
    const idx = (0, battle_1.liveFoes)(b).indexOf(k);
    return (0, render_1.screenPosFor)('foe', Math.max(0, idx));
}
async function playEvents(evs) {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    for (const e of evs) {
        switch (e.t) {
            case 'use': {
                const p = posOf(e.who);
                runtime_1.RT.battleAnim.focus = e.who.side;
                if (e.who.side === 'ally')
                    runtime_1.RT.battleAnim.allyOffset = -26;
                else
                    runtime_1.RT.battleAnim.foeOffset = -26;
                log(`<b>${(0, util_1.esc)((0, creature_1.displayName)(e.who.c))}</b> calls <span style="color:${typechart_1.AFFINITY_COLOR[e.ability.affinity]}">${(0, util_1.esc)(e.ability.name)}</span>!`);
                fx_1.fx.burst(p.x, p.y, typechart_1.AFFINITY_COLOR[e.ability.affinity], 16, 150, 'spark');
                fx_1.fx.ring(p.x, p.y, typechart_1.AFFINITY_COLOR[e.ability.affinity], 14);
                audio_1.AUD.tone(520, 0.09, 'square', 0.1, 760);
                await (0, util_1.wait)(230);
                runtime_1.RT.battleAnim.allyOffset = 0;
                runtime_1.RT.battleAnim.foeOffset = 0;
                break;
            }
            case 'dmg': {
                const p = posOf(e.who);
                const col = e.crit ? '#fff2a0' : e.mult >= 2 ? '#9dffc4' : e.mult < 1 ? '#a9b8da' : '#ffd9d9';
                fx_1.fx.float(p.x + (Math.random() - 0.5) * 30, p.y - p.r * 0.5, (e.crit ? '✦ ' : '') + String(e.amount), col, e.crit ? 32 : 24, e.crit);
                fx_1.fx.burst(p.x, p.y, e.mult >= 2 ? '#b8ffd8' : '#ffc9a0', e.mult >= 2 ? 26 : 12, e.mult >= 2 ? 200 : 120, 'shard');
                if (e.who.side === 'ally') {
                    runtime_1.RT.battleAnim.allyFlash = 1;
                    runtime_1.RT.battleAnim.allyShakeT = 0.3;
                }
                else {
                    runtime_1.RT.battleAnim.foeFlash = 1;
                    runtime_1.RT.battleAnim.foeShakeT = 0.3;
                }
                if (e.crit) {
                    fx_1.fx.kick(11);
                    fx_1.fx.bang('#fff6c9', 0.5);
                    audio_1.AUD.sfx('crit');
                }
                else if (e.mult >= 2) {
                    fx_1.fx.kick(8);
                    fx_1.fx.bang('#c9ffe0', 0.35);
                    audio_1.AUD.sfx('super');
                }
                else if (e.mult < 1) {
                    audio_1.AUD.sfx('weak');
                }
                else {
                    fx_1.fx.kick(4);
                    audio_1.AUD.sfx('hit');
                }
                if (e.label && e.label !== 'recoil' && e.mult !== 1) {
                    fx_1.fx.float(p.x, p.y - p.r * 1.05, e.label, e.mult >= 2 ? '#9dffc4' : '#a9b8da', 17);
                }
                if (e.label === 'recoil' || e.label === 'Scorch' || e.label === 'Sap') {
                    log(`${(0, util_1.esc)((0, creature_1.displayName)(e.who.c))} takes ${e.amount} from ${(0, util_1.esc)(e.label)}.`);
                }
                await (0, util_1.wait)(300);
                break;
            }
            case 'heal': {
                if (e.amount <= 0)
                    break;
                const p = posOf(e.who);
                fx_1.fx.float(p.x, p.y - p.r * 0.6, '+' + e.amount, '#9dffb8', 24);
                fx_1.fx.burst(p.x, p.y, '#a6ffcc', 16, 90, 'dot');
                audio_1.AUD.sfx('heal');
                log(`${(0, util_1.esc)((0, creature_1.displayName)(e.who.c))} recovers ${e.amount} Vitality.`);
                await (0, util_1.wait)(320);
                break;
            }
            case 'miss': {
                const p = posOf(e.who);
                fx_1.fx.float(p.x, p.y - p.r * 0.6, 'unheard', '#a9b8da', 20);
                audio_1.AUD.sfx('fail');
                log('The Resonance goes unheard.');
                await (0, util_1.wait)(340);
                break;
            }
            case 'status': {
                const p = posOf(e.who);
                fx_1.fx.burst(p.x, p.y, e.kind === 'burn' ? '#ff8a4c' : e.kind === 'frost' ? '#7fd8ff' : e.kind === 'shock' ? '#ffe066' : e.kind === 'regen' ? '#8ff0a8' : '#c58cff', 20, 110, 'dot');
                log(e.text);
                audio_1.AUD.tone(300, 0.14, 'triangle', 0.12, 520);
                await (0, util_1.wait)(420);
                break;
            }
            case 'stat': {
                const p = posOf(e.who);
                fx_1.fx.float(p.x, p.y - p.r * 0.85, e.text, '#cfe3ff', 18);
                log(`${(0, util_1.esc)((0, creature_1.displayName)(e.who.c))}'s ${e.text}.`);
                audio_1.AUD.tone(420, 0.1, 'sine', 0.1, 640);
                await (0, util_1.wait)(300);
                break;
            }
            case 'barrier': {
                const p = posOf(e.who);
                fx_1.fx.ring(p.x, p.y, '#ffffff', 34);
                audio_1.AUD.tone(900, 0.16, 'sine', 0.14, 300);
                await (0, util_1.wait)(260);
                break;
            }
            case 'faint': {
                const p = posOf(e.who);
                fx_1.fx.burst(p.x, p.y, '#8899bb', 30, 160, 'dot');
                fx_1.fx.kick(9);
                audio_1.AUD.sfx('faint');
                log(`<b>${(0, util_1.esc)((0, creature_1.displayName)(e.who.c))}</b> is unbound and falls quiet.`);
                await (0, util_1.wait)(520);
                renderPanels();
                break;
            }
            case 'msg': {
                log(e.text);
                await (0, util_1.wait)(400);
                break;
            }
            case 'switch': {
                log(`${(0, util_1.esc)((0, creature_1.displayName)(e.to.c))} steps forward.`);
                await (0, util_1.wait)(300);
                break;
            }
        }
        syncBars();
    }
    runtime_1.RT.battleAnim.focus = null;
}
// ---------------------------------------------------------------- binding minigame
function bindMinigame(target, baseChance, quality) {
    return new Promise((resolve) => {
        const host = document.createElement('div');
        host.id = 'bindgame';
        host.innerHTML = `<div class="bg-inner">
      <h4>Binding Contest</h4>
      <div class="hint">Tap <b>SPACE</b> or the seal three times as the needle crosses the resonant band.<br>
        ${(0, util_1.esc)(quality)} · base agreement <b>${Math.round(baseChance * 100)}%</b> · target: ${(0, util_1.esc)((0, creature_1.displayName)(target))}</div>
      <div id="bindtrack"><div id="bindzone"></div><div id="bindneedle" style="left:0%"></div></div>
      <div id="bindtaps"><span class="tapdot"></span><span class="tapdot"></span><span class="tapdot"></span></div>
      <div id="bindmeter">Attunement 0%</div>
      <button id="bindbtn">Seal</button></div>`;
        document.body.appendChild(host);
        const needle = host.querySelector('#bindneedle');
        const dots = Array.from(host.querySelectorAll('.tapdot'));
        const meter = host.querySelector('#bindmeter');
        let taps = 0, score = 0, t = 0, dir = 1, done = false;
        const speed = 0.85 + Math.min(0.9, target.level / 44);
        let raf = 0;
        const tick = () => {
            if (done)
                return;
            t += dir * speed * 0.016;
            if (t > 1) {
                t = 1;
                dir = -1;
            }
            else if (t < 0) {
                t = 0;
                dir = 1;
            }
            needle.style.left = (t * 100).toFixed(2) + '%';
            raf = requestAnimationFrame(tick);
        };
        raf = requestAnimationFrame(tick);
        const tap = () => {
            if (done || taps >= 3)
                return;
            const d = Math.abs(t - 0.5);
            let v = 0;
            if (d < 0.05) {
                v = 1;
                audio_1.AUD.tone(1200, 0.1, 'square', 0.16);
            }
            else if (d < 0.11) {
                v = 0.72;
                audio_1.AUD.tone(900, 0.08, 'square', 0.13);
            }
            else if (d < 0.2) {
                v = 0.4;
                audio_1.AUD.tone(600, 0.07, 'square', 0.1);
            }
            else {
                v = 0;
                audio_1.AUD.sfx('fail');
            }
            score += v;
            dots[taps].classList.add(v > 0.6 ? 'hit' : v > 0 ? 'hit' : 'miss');
            if (v > 0.6)
                dots[taps].style.boxShadow = '0 0 18px #8ff0b0';
            taps++;
            meter.textContent = `Attunement ${Math.round((score / 3) * 100)}%`;
            if (taps >= 3)
                finish();
        };
        const keyH = (e) => {
            if (e.code === 'Space' || e.code === 'Enter') {
                e.preventDefault();
                tap();
            }
        };
        host.querySelector('#bindbtn').addEventListener('click', tap);
        host.addEventListener('click', (e) => { if (e.target === host)
            tap(); });
        window.addEventListener('keydown', keyH);
        const finish = () => {
            done = true;
            cancelAnimationFrame(raf);
            window.removeEventListener('keydown', keyH);
            setTimeout(() => {
                host.remove();
                // multiplier from 0.55 (all misses) to 1.5 (perfect)
                resolve(0.55 + (score / 3) * 0.95);
            }, 420);
        };
    });
}
// ---------------------------------------------------------------- main loop
function enterBattle(foes, opts, done) {
    runtime_1.RT.battle = (0, battle_1.startBattle)(foes, opts);
    runtime_1.RT.mode = 'battle';
    runtime_1.RT.battleAnim.hpAnim.clear();
    onBattleEnd = done;
    participants = state_1.G.party.filter((c) => c.hp > 0);
    for (const f of foes)
        (0, state_1.markSeen)(f.speciesId);
    buildUI();
    renderPanels();
    audio_1.AUD.play(opts.boss ? 'boss' : 'battle');
    audio_1.AUD.sfx('encounter');
    fx_1.fx.bang('#ffffff', 0.6);
    void runBattle();
}
async function runBattle() {
    const b = runtime_1.RT.battle;
    if (!b)
        return;
    const intro = b.wild
        ? `A wild <b>${(0, util_1.esc)((0, creature_1.displayName)(b.foes[0].c))}</b> answers your presence!`
        : `<b>${(0, util_1.esc)(b.guardianName || 'A Guardian')}</b> bars the way with ${b.foes.length} kin!`;
    log(intro);
    await (0, util_1.wait)(900);
    while (b.over === null) {
        const actor = (0, battle_1.nextActor)(b);
        if (!actor) {
            // end of round: status ticks
            for (const k of [...b.allies, ...b.foes]) {
                if ((0, creature_1.isFainted)(k.c))
                    continue;
                const evs = (0, battle_1.endOfTurn)(k);
                if (evs.length)
                    await playEvents(evs);
                if (checkEnd(b))
                    break;
            }
            if (b.over !== null)
                break;
            if (await handleFaints(b))
                break;
            (0, battle_1.buildOrder)(b);
            renderPanels();
            continue;
        }
        if (actor.side === 'ally') {
            const act = await awaitChoice();
            const handled = await doPlayerAction(b, actor, act);
            if (!handled)
                continue;
        }
        else {
            log(`${(0, util_1.esc)((0, creature_1.displayName)(actor.c))} gathers itself…`);
            await (0, util_1.wait)(420);
            const pickAct = (0, battle_1.foeChoose)(b, actor);
            await playEvents((0, battle_1.useAbility)(b, actor, pickAct.ability, pickAct.target));
        }
        (0, battle_1.advance)(b);
        if (checkEnd(b))
            break;
        if (await handleFaints(b))
            break;
        renderPanels();
    }
    await finishBattle(b);
}
async function doPlayerAction(b, me, act) {
    switch (act.kind) {
        case 'move': {
            await playEvents((0, battle_1.useAbility)(b, me, act.ability, act.target));
            return true;
        }
        case 'swap': {
            const to = b.allies[act.to];
            log(`${(0, util_1.esc)((0, creature_1.displayName)(to.c))} steps forward.`);
            fx_1.fx.burst((0, render_1.screenPosFor)('ally').x, (0, render_1.screenPosFor)('ally').y, '#bfe0ff', 20, 130, 'dot');
            audio_1.AUD.tone(600, 0.12, 'sine', 0.12, 900);
            b.active = act.to;
            await (0, util_1.wait)(420);
            renderPanels();
            return true; // costs the turn
        }
        case 'item': {
            if (act.item === 'tool_tuner') {
                if ((0, state_1.takeItem)('tool_tuner')) {
                    b.bindBonus += 0.22;
                    log('The Field Tuner widens the binding window.');
                    audio_1.AUD.sfx('bind');
                }
                await (0, util_1.wait)(520);
                return true;
            }
            if (act.target) {
                const ok = (0, menus_1.applyRestore)(act.item, act.target);
                if (!ok)
                    return false;
                const p = act.target === me.c ? (0, render_1.screenPosFor)('ally') : (0, render_1.screenPosFor)('ally');
                fx_1.fx.burst(p.x, p.y, '#a6ffcc', 18, 100, 'dot');
                fx_1.fx.float(p.x, p.y - p.r * 0.6, 'mended', '#9dffb8', 20);
                log(`You use ${(0, util_1.esc)(act.item.startsWith('res_') ? act.item : act.item)} on ${(0, util_1.esc)((0, creature_1.displayName)(act.target))}.`);
                await (0, util_1.wait)(520);
                renderPanels();
                return true;
            }
            return false;
        }
        case 'bind': {
            const ok = await attemptBind(b, act.item);
            if (ok)
                return true;
            return true;
        }
        case 'flee': {
            b.escapeTries++;
            const mySpd = (0, battle_1.effSpd)(me);
            const theirs = Math.max(1, Math.max(...(0, battle_1.liveFoes)(b).map((f) => (0, battle_1.effSpd)(f))));
            const p = (0, util_1.clamp)(0.36 + (mySpd / theirs) * 0.34 + b.escapeTries * 0.1, 0.2, 0.95);
            if ((0, util_1.chance)(p)) {
                log('You withdraw, letting the note fade.');
                audio_1.AUD.sfx('back');
                await (0, util_1.wait)(600);
                b.over = 'fled';
                return true;
            }
            log('The resonance holds you — no withdrawal.');
            audio_1.AUD.sfx('fail');
            await (0, util_1.wait)(600);
            return true;
        }
    }
}
async function attemptBind(b, itemId) {
    const foes = (0, battle_1.liveFoes)(b);
    if (foes.length === 0)
        return false;
    const target = foes.length === 1 ? foes[0] : foes.reduce((a, c) => (c.c.hp / (0, creature_1.maxHp)(c.c) < a.c.hp / (0, creature_1.maxHp)(a.c) ? c : a));
    if (!b.wild) {
        log('A Guardian\u2019s kin cannot be bound.');
        await (0, util_1.wait)(700);
        return false;
    }
    if (state_1.G.party.length >= 6 && state_1.G.vault.length >= 24) {
        log('You have nowhere to keep another kin.');
        await (0, util_1.wait)(700);
        return false;
    }
    if (!(0, state_1.takeItem)(itemId))
        return false;
    const q = ITEMDESC(itemId);
    const base = (0, battle_1.bindChance)(target.c, q.power, state_1.G.bonuses.bind + b.bindBonus);
    log(`You loose the ${(0, util_1.esc)(q.name)}. It opens around ${(0, util_1.esc)((0, creature_1.displayName)(target.c))}…`);
    const p = posOf(target);
    fx_1.fx.ring(p.x, p.y, '#ffe9a0', 20);
    fx_1.fx.burst(p.x, p.y, '#ffe9a0', 22, 130, 'spark');
    audio_1.AUD.sfx('bind');
    await (0, util_1.wait)(560);
    const mult = await bindMinigame(target.c, base, q.name);
    const finalP = (0, util_1.clamp)(base * mult, 0.02, 0.985);
    const success = (0, util_1.chance)(finalP);
    fx_1.fx.ring(p.x, p.y, success ? '#a8ffc9' : '#ff9a9a', 26);
    await (0, util_1.wait)(200);
    if (success) {
        fx_1.fx.burst(p.x, p.y, '#ffe9a0', 46, 220, 'spark');
        fx_1.fx.bang('#fff3c4', 0.75);
        audio_1.AUD.sfx('bound');
        const bound = target.c;
        target.c.hp = Math.max(1, target.c.hp);
        b.over = 'bound';
        runtime_1.RT.bindingCreature = bound;
        log(`<b>${(0, util_1.esc)((0, creature_1.displayName)(bound))}</b> agrees. The binding holds!`);
        await (0, util_1.wait)(800);
    }
    else {
        audio_1.AUD.sfx('fail');
        log(`${(0, util_1.esc)((0, creature_1.displayName)(target.c))} slips the seal — ${Math.round(finalP * 100)}% was not enough.`);
        await (0, util_1.wait)(750);
    }
    return true;
}
function checkEnd(b) {
    if (b.over !== null)
        return true;
    if ((0, battle_1.liveFoes)(b).length === 0) {
        b.over = 'win';
        return true;
    }
    if ((0, battle_1.liveAllies)(b).length === 0) {
        b.over = 'lose';
        return true;
    }
    return false;
}
/** If the active ally fainted, force a switch. Returns true if the battle ended. */
async function handleFaints(b) {
    if (checkEnd(b))
        return true;
    const al = (0, battle_1.activeAlly)(b);
    if (al && (0, creature_1.isFainted)(al.c)) {
        const alive = b.allies.map((a, i) => ({ a, i })).filter((x) => !(0, creature_1.isFainted)(x.a.c));
        if (alive.length === 0) {
            b.over = 'lose';
            return true;
        }
        log('Call another kin forward.');
        const chosen = await new Promise((res) => {
            const m = menu('list', alive.map((o) => btn((0, creature_1.displayName)(o.a.c), `Lv ${o.a.c.level} · ${o.a.c.hp}/${(0, creature_1.maxHp)(o.a.c, state_1.G.bonuses)}`, `data-fw="${o.i}"`)).join(''));
            (0, ui_2.bindClicks)(m, '[data-fw]', (el) => res(Number(el.dataset.fw)));
        });
        b.active = chosen;
        menu('main', '');
        fx_1.fx.burst((0, render_1.screenPosFor)('ally').x, (0, render_1.screenPosFor)('ally').y, '#bfe0ff', 20, 130, 'dot');
        renderPanels();
        await (0, util_1.wait)(420);
        (0, battle_1.buildOrder)(b);
    }
    return false;
}
async function finishBattle(b) {
    const result = b.over || 'fled';
    menu('main', '');
    if (result === 'win' || result === 'bound') {
        let totalExp = 0;
        const defeated = b.foes.filter((f) => (0, creature_1.isFainted)(f.c) || result === 'bound');
        const levelMsgs = [];
        const learnQueue = [];
        for (const f of b.foes) {
            if (!(0, creature_1.isFainted)(f.c) && result !== 'bound')
                continue;
            const r = (0, battle_1.awardExp)(f.c, participants);
            totalExp += r.gained;
            for (const lv of r.levels) {
                levelMsgs.push(`${lv.name} rose to Lv ${lv.to}.`);
                const c = participants.find((x) => x.uid === lv.uid);
                if (c)
                    for (const id of lv.learned)
                        learnQueue.push({ c, id });
            }
        }
        const shards = Math.max(8, Math.round(defeated.reduce((s, f) => s + f.c.level * 5 + (0, species_1.sp)(f.c.speciesId).rarity * 9, 0) * (b.boss ? 3.2 : 1)));
        state_1.G.shards += shards;
        log(`The resonance stills. <b>${totalExp}</b> Attunement gained · <b>${shards} ◈</b>`);
        if (levelMsgs.length)
            audio_1.AUD.sfx('levelup');
        await (0, util_1.wait)(1100);
        for (const m of levelMsgs) {
            log(m);
            await (0, util_1.wait)(700);
        }
        runtime_1.RT.battle = null;
        (0, ui_1.hide)(ui());
        if (result === 'bound' && runtime_1.RT.bindingCreature) {
            const bound = runtime_1.RT.bindingCreature;
            runtime_1.RT.bindingCreature = null;
            const s = (0, species_1.sp)(bound.speciesId);
            (0, state_1.markBound)(s.id);
            healFullIfNeeded(bound);
            if (state_1.G.party.length < 6)
                state_1.G.party.push(bound);
            else
                state_1.G.vault.push(bound);
            await (0, ui_1.cutin)('Binding Sealed', `${s.name} lends you its note. ${s.lore}`, typechart_1.AFFINITY_COLOR[s.affinity], typechart_1.AFFINITY_COLOR[s.affinity] + '99');
            if (state_1.G.party.length >= 6)
                (0, ui_1.toast)(`${s.name} waits in the Reliquary Vault.`);
        }
        for (const q of learnQueue) {
            if (q.c.abilities.includes(q.id))
                continue;
            await new Promise((res) => {
                if (q.c.abilities.length < 4) {
                    q.c.abilities.push(q.id);
                    q.c.pp.push((0, abilities_1.ab)(q.id).maxPP);
                    (0, ui_1.toast)(`${(0, creature_1.displayName)(q.c)} learned ${(0, abilities_1.ab)(q.id).name}.`);
                    res();
                }
                else {
                    (0, ui_2.openModal)((0, ui_2.panel)('New Resonance', (0, util_1.esc)((0, creature_1.displayName)(q.c)), `<div style="font-size:14px;line-height:1.7;margin-bottom:12px">${(0, util_1.esc)((0, creature_1.displayName)(q.c))} can hold <b style="color:var(--gold)">${(0, util_1.esc)((0, abilities_1.ab)(q.id).name)}</b> — but must release one.</div>
            <div class="grid g2">${q.c.abilities.map((x, i) => `<div class="mvcard" data-drop="${i}"><b>${(0, util_1.esc)((0, abilities_1.ab)(x).name)}</b><div class="kv"><span>PWR ${(0, abilities_1.ab)(x).power || '—'}</span><span>${(0, util_1.esc)((0, abilities_1.ab)(x).affinity)}</span></div></div>`).join('')}</div>`, '<button class="btn" id="skip-learn">Decline</button>'), (root) => {
                        (0, ui_2.bindClicks)(root, '[data-drop]', (el) => {
                            const i = Number(el.dataset.drop);
                            q.c.abilities[i] = q.id;
                            q.c.pp[i] = (0, abilities_1.ab)(q.id).maxPP;
                            (0, ui_1.toast)(`${(0, creature_1.displayName)(q.c)} learned ${(0, abilities_1.ab)(q.id).name}.`);
                            (0, ui_2.closeModal)();
                            res();
                        });
                        const sk = root.querySelector('#skip-learn');
                        if (sk)
                            sk.addEventListener('click', () => { (0, ui_2.closeModal)(); res(); });
                    });
                }
            });
        }
    }
    else if (result === 'lose') {
        log('Your binding is spent. The Song carries you back.');
        audio_1.AUD.sfx('faint');
        await (0, util_1.wait)(1400);
        runtime_1.RT.battle = null;
        (0, ui_1.hide)(ui());
    }
    else {
        runtime_1.RT.battle = null;
        (0, ui_1.hide)(ui());
    }
    (0, ui_1.closeAllModals)();
    const cb = onBattleEnd;
    onBattleEnd = null;
    if (cb)
        cb({ result });
}
function healFullIfNeeded(c) {
    if (c.hp <= 0)
        (0, creature_1.healFull)(c, state_1.G.bonuses);
}
function battleActive() { return runtime_1.RT.battle !== null; }
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.openMap = exports.openDex = exports.openBag = exports.openTeam = void 0;
exports.updateHud = updateHud;
exports.tryMove = tryMove;
exports.onStepComplete = onStepComplete;
exports.transitionOut = transitionOut;
exports.transitionIn = transitionIn;
exports.interact = interact;
exports.beginNewGame = beginNewGame;
exports.continueGame = continueGame;
exports.quitToTitle = quitToTitle;
exports.refreshTitleButtons = refreshTitleButtons;
exports.openPauseMenu = openPauseMenu;
const state_1 = require("./state");
const zones_1 = require("./zones");
const species_1 = require("./species");
const creature_1 = require("./creature");
const world_1 = require("./world");
const runtime_1 = require("./runtime");
const fx_1 = require("./fx");
const audio_1 = require("./audio");
const ui_1 = require("./ui");
const menus_1 = require("./menus");
Object.defineProperty(exports, "openTeam", { enumerable: true, get: function () { return menus_1.openTeam; } });
Object.defineProperty(exports, "openBag", { enumerable: true, get: function () { return menus_1.openBag; } });
Object.defineProperty(exports, "openDex", { enumerable: true, get: function () { return menus_1.openDex; } });
Object.defineProperty(exports, "openMap", { enumerable: true, get: function () { return menus_1.openMap; } });
const battleui_1 = require("./battleui");
const items_1 = require("./items");
const util_1 = require("./util");
const typechart_1 = require("./typechart");
const sprites_1 = require("./sprites");
let interactLock = false;
// ---------------------------------------------------------------- HUD
function updateHud() {
    const z = (0, state_1.zoneDef)();
    (0, ui_1.$)('#hud-zone').textContent = z.name;
    const q = questText();
    (0, ui_1.$)('#hud-quest').textContent = q;
    (0, ui_1.setVignette)(z.tint);
    const mb = (0, ui_1.$)('#btn-mute');
    mb.textContent = state_1.G.muted ? '♪̶' : '♪';
}
function questText() {
    if (state_1.G.tutorial < 4) {
        const steps = [
            'Move with WASD / arrows. Reach the glowing shrine.',
            'Step into tall growth to draw out wild kin.',
            'Win the contest — then try binding a wild kin.',
            'Speak to the stele, then still the zone Guardian.',
        ];
        return steps[state_1.G.tutorial] || '';
    }
    if (state_1.G.postgame)
        return `Coda silenced · ${state_1.G.uniqueBound.length} unique kin bound · free roam`;
    const z = (0, state_1.zoneDef)();
    if (z.guardian && !state_1.G.guardiansBeaten.includes(z.guardian.name)) {
        return `Bind ${Math.max(0, 8 - state_1.G.uniqueBound.length)} more unique kin · still ${z.guardian.name}`;
    }
    return `Anchors stilled ${state_1.G.anchorsStilled}/4 · unique kin ${state_1.G.uniqueBound.length}/8 · press » to travel on`;
}
// ---------------------------------------------------------------- movement
function tryMove(dx, dy) {
    if (runtime_1.RT.mode !== 'field' || runtime_1.RT.busy || runtime_1.RT.moveT < 1 || (0, ui_1.modalOpen)() || (0, ui_1.dialogueOpen)())
        return;
    const m = (0, world_1.getMap)(state_1.G.zone);
    runtime_1.RT.player.facing = dx < 0 ? -1 : dx > 0 ? 1 : runtime_1.RT.player.facing;
    const nx = runtime_1.RT.player.x + dx, ny = runtime_1.RT.player.y + dy;
    if (!(0, world_1.walkable)(m, nx, ny)) {
        audio_1.AUD.tone(120, 0.05, 'square', 0.05);
        return;
    }
    runtime_1.RT.fromX = runtime_1.RT.player.x;
    runtime_1.RT.fromY = runtime_1.RT.player.y;
    runtime_1.RT.player.x = nx;
    runtime_1.RT.player.y = ny;
    runtime_1.RT.moveT = 0;
    runtime_1.RT.player.moving = true;
    state_1.G.px = nx;
    state_1.G.py = ny;
    state_1.G.steps++;
    if (state_1.G.lureSteps > 0)
        state_1.G.lureSteps--;
    if (state_1.G.wardSteps > 0)
        state_1.G.wardSteps--;
    audio_1.AUD.sfx('step');
}
function onStepComplete() {
    runtime_1.RT.player.moving = false;
    const m = (0, world_1.getMap)(state_1.G.zone);
    const t = (0, world_1.tileAt)(m, runtime_1.RT.player.x, runtime_1.RT.player.y);
    if (t === world_1.T_GRASS) {
        if (state_1.G.tutorial === 1) {
            state_1.G.tutorial = 2;
            updateHud();
        }
        maybeEncounter();
    }
    autoInteract(t);
}
function autoInteract(t) {
    if (t === world_1.T_GATE)
        void useGate(1);
    else if (t === world_1.T_BACK)
        void useGate(-1);
}
// ---------------------------------------------------------------- encounters
function maybeEncounter() {
    const z = (0, state_1.zoneDef)();
    if (state_1.G.wardSteps > 0)
        return;
    if (state_1.G.party.filter((c) => c.hp > 0).length === 0)
        return;
    let rate = z.encounterRate * (state_1.G.lureSteps > 0 ? 2 : 1);
    if (state_1.G.tutorial === 2)
        rate = 1; // guarantee the tutorial battle
    if (state_1.G.postgame)
        rate *= 1.25;
    if (!(0, util_1.chance)(rate))
        return;
    const speciesId = pickSpawn(z.spawns);
    const [lo, hi] = z.levels;
    const bump = state_1.G.postgame ? 6 : 0;
    const lv = (0, util_1.clamp)((0, util_1.ri)(lo, hi) + bump, 2, 50);
    const foes = [(0, creature_1.makeCreature)(speciesId, lv)];
    // packs in later zones
    if ((state_1.G.zone >= 3 || state_1.G.postgame) && (0, util_1.chance)(0.28))
        foes.push((0, creature_1.makeCreature)(pickSpawn(z.spawns), (0, util_1.clamp)(lv - 1, 2, 50)));
    if ((state_1.G.zone >= 4 || state_1.G.postgame) && (0, util_1.chance)(0.12))
        foes.push((0, creature_1.makeCreature)(pickSpawn(z.spawns), (0, util_1.clamp)(lv - 2, 2, 50)));
    void startEncounter(foes, { wild: true });
}
function pickSpawn(list) {
    // weight by inverse rarity
    const weighted = [];
    for (const id of list) {
        const r = species_1.SPECIES_MAP[id] ? (0, species_1.sp)(id).rarity : 2;
        const w = r >= 4 ? 1 : r === 3 ? 2 : 4;
        for (let i = 0; i < w; i++)
            weighted.push(id);
    }
    return (0, util_1.pick)(weighted);
}
async function startEncounter(foes, opts) {
    runtime_1.RT.busy = true;
    await transitionOut();
    (0, battleui_1.enterBattle)(foes, opts, (o) => { void afterBattle(o.result, opts); });
}
async function afterBattle(result, opts) {
    runtime_1.RT.mode = 'field';
    runtime_1.RT.battle = null;
    await transitionIn();
    runtime_1.RT.busy = false;
    audio_1.AUD.play('field');
    updateHud();
    if (state_1.G.tutorial === 2 && (result === 'win' || result === 'bound' || result === 'fled')) {
        state_1.G.tutorial = 3;
        updateHud();
        await (0, ui_1.say)('Threadwarden Vell', [
            'Well struck. That is the whole craft, in miniature: listen, answer, and do not overplay your hand.',
            'Now the harder half. Wear a wild kin down, then loose a reliquary — and match the resonant band when the seal opens.',
            'Bind eight unique kin and still four Anchors, and the Coda will open for you. Or for it. We shall see.',
        ]);
    }
    if (result === 'lose') {
        (0, menus_1.healParty)();
        const s = state_1.G.lastShrine;
        state_1.G.zone = s.zone;
        const m = (0, world_1.getMap)(state_1.G.zone);
        const p = (0, world_1.findWalkableNear)(m, s.x, s.y);
        runtime_1.RT.player.x = p.x;
        runtime_1.RT.player.y = p.y;
        runtime_1.RT.fromX = p.x;
        runtime_1.RT.fromY = p.y;
        runtime_1.RT.moveT = 1;
        state_1.G.px = p.x;
        state_1.G.py = p.y;
        updateHud();
        await (0, ui_1.say)('The Song', [
            'You wake at a shrine with the taste of copper and half-remembered chords.',
            'Your kin are mended. The Song does not punish; it simply resumes.',
        ]);
        (0, state_1.saveGame)();
    }
    if (result === 'win' && opts.boss && opts.guardianName)
        await onGuardianDefeated(opts.guardianName);
}
// ---------------------------------------------------------------- transitions
async function transitionOut() {
    runtime_1.RT.transitionDir = 1;
    for (let i = 0; i <= 10; i++) {
        runtime_1.RT.transition = i / 10;
        await (0, util_1.wait)(22);
    }
}
async function transitionIn() {
    for (let i = 10; i >= 0; i--) {
        runtime_1.RT.transition = i / 10;
        await (0, util_1.wait)(22);
    }
    runtime_1.RT.transition = 0;
}
// ---------------------------------------------------------------- interactions
function interact() {
    if (runtime_1.RT.mode !== 'field' || runtime_1.RT.busy || interactLock || (0, ui_1.modalOpen)())
        return;
    if ((0, ui_1.dialogueOpen)()) {
        (0, ui_1.advanceDialogue)();
        return;
    }
    const m = (0, world_1.getMap)(state_1.G.zone);
    const t = (0, world_1.tileAt)(m, runtime_1.RT.player.x, runtime_1.RT.player.y);
    const ahead = (0, world_1.tileAt)(m, runtime_1.RT.player.x + runtime_1.RT.player.facing, runtime_1.RT.player.y);
    if (t === world_1.T_SHRINE) {
        void useShrine();
        return;
    }
    if (ahead === world_1.T_SHRINE) {
        void useShrine();
        return;
    }
    if (t === world_1.T_STORY || ahead === world_1.T_STORY) {
        void useStory();
        return;
    }
    if (t === world_1.T_GUARD || ahead === world_1.T_GUARD) {
        void useGuardian();
        return;
    }
    if (t === world_1.T_GATE || ahead === world_1.T_GATE) {
        void useGate(1);
        return;
    }
    if (t === world_1.T_BACK || ahead === world_1.T_BACK) {
        void useGate(-1);
        return;
    }
    (0, ui_1.toast)('Nothing answers here.');
}
async function useShrine() {
    interactLock = true;
    const z = (0, state_1.zoneDef)();
    state_1.G.lastShrine = { zone: state_1.G.zone, x: runtime_1.RT.player.x, y: runtime_1.RT.player.y };
    fx_1.fx.burst(0, 0, '#aef0dd', 0);
    (0, menus_1.healParty)();
    audio_1.AUD.sfx('heal');
    (0, state_1.saveGame)();
    if (state_1.G.tutorial === 0) {
        state_1.G.tutorial = 1;
        updateHud();
        await (0, ui_1.say)('Resonance Shrine', [
            'The shrine drinks a little of your tiredness and gives back a chord you did not know you were missing.',
            'Your kin are whole. Your progress is inscribed.',
        ]);
        await (0, ui_1.say)('Threadwarden Vell', [
            'Shrines are the only honest architecture left. Rest here, trade here, and let the stele hold your place in the Song.',
            'Now walk into the tall growth. Something in Hushfall Hollow has been waiting to be noticed.',
        ]);
        interactLock = false;
        return;
    }
    const body = `<div class="mt" style="line-height:1.8;margin-bottom:14px">Your kin are mended and your place in the Song inscribed. The shrine of ${(0, util_1.esc)(z.name)} keeps a small exchange.</div>
    <div class="grid g3">
      <div class="card" data-go="shop"><div class="nm">Exchange</div><div class="mt">Spend Resonance Shards</div></div>
      <div class="card" data-go="team"><div class="nm">Binding</div><div class="mt">Review and reshape your kin</div></div>
      <div class="card" data-go="bag"><div class="nm">Satchel</div><div class="mt">Sort what you carry</div></div>
    </div>`;
    (0, ui_1.openModal)((0, ui_1.panel)('Resonance Shrine', `${state_1.G.shards} ◈ · progress inscribed`, body, '<button class="btn" data-close>Step away</button>'), (root) => {
        (0, ui_1.bindClicks)(root, '[data-go]', (el) => {
            const go = el.dataset.go;
            if (go === 'shop')
                (0, menus_1.openShop)(items_1.SHOP_STOCK_BY_ZONE[state_1.G.zone] || items_1.SHOP_STOCK_BY_ZONE[0]);
            else if (go === 'team')
                (0, menus_1.openTeam)();
            else if (go === 'bag')
                (0, menus_1.openBag)();
        });
    }, () => { interactLock = false; });
    (0, ui_1.toast)('Kin mended · progress inscribed');
}
async function useStory() {
    interactLock = true;
    const z = (0, state_1.zoneDef)();
    const first = !state_1.G.seenStory.includes(z.id);
    if (first)
        state_1.G.seenStory.push(z.id);
    if (state_1.G.tutorial === 3) {
        state_1.G.tutorial = 4;
        updateHud();
    }
    await (0, ui_1.say)(z.storyNode.name, z.storyNode.text);
    if (first) {
        const stage = Math.min(zones_1.STORY_STAGES.length - 1, Math.floor(state_1.G.anchorsStilled));
        if (state_1.G.storyStage < stage)
            state_1.G.storyStage = stage;
        const st = zones_1.STORY_STAGES[state_1.G.storyStage];
        await (0, ui_1.cutin)(st.name, st.text, z.prop, z.tint);
        state_1.G.shards += 120;
        (0, ui_1.toast)('Record inscribed · +120 ◈');
        (0, state_1.saveGame)();
    }
    interactLock = false;
}
async function useGuardian() {
    const z = (0, state_1.zoneDef)();
    if (!z.guardian) {
        (0, ui_1.toast)('The dais is empty.');
        return;
    }
    const gd = z.guardian;
    if (state_1.G.guardiansBeaten.includes(gd.name)) {
        (0, ui_1.toast)(`${gd.name} is already stilled.`);
        return;
    }
    interactLock = true;
    await (0, ui_1.say)(gd.name, [
        gd.anchor
            ? `I am the ${gd.name}. I have held this seam since before your grandmother's name was chosen.`
            : `${gd.name} regards you the way weather regards a roof.`,
        'Show me a binding worth the noise you are making.',
    ]);
    interactLock = false;
    const foes = gd.team.map((t) => (0, creature_1.makeCreature)(t.id, t.lv));
    void startEncounter(foes, { boss: true, guardianName: gd.name });
}
async function onGuardianDefeated(name) {
    const z = zones_1.ZONES.find((x) => x.guardian && x.guardian.name === name);
    if (!z || !z.guardian)
        return;
    const gd = z.guardian;
    if (state_1.G.guardiansBeaten.includes(name))
        return;
    state_1.G.guardiansBeaten.push(name);
    state_1.G.boons.push(`${gd.boon} — ${gd.boonDesc}`);
    applyBoonEffect(gd.boon);
    if (gd.anchor) {
        state_1.G.anchorsStilled++;
        state_1.G.storyStage = Math.min(zones_1.STORY_STAGES.length - 1, state_1.G.anchorsStilled);
    }
    if (state_1.G.zone + 1 < zones_1.ZONES.length && !state_1.G.unlockedZones.includes(state_1.G.zone + 1))
        state_1.G.unlockedZones.push(state_1.G.zone + 1);
    audio_1.AUD.sfx('transform');
    await (0, ui_1.cutin)(gd.boon, `${gd.boonDesc}  ${gd.anchor ? `The ${name} goes quiet; the seam knits. ${4 - state_1.G.anchorsStilled} Anchors still hum.` : `${name} yields the way onward.`}`, z.prop, z.tint);
    (0, state_1.addItem)('res_draught', 2);
    if (gd.anchor)
        (0, state_1.addItem)('bind_glass', 3);
    (0, ui_1.toast)('Boon earned · the way onward unravels');
    updateHud();
    (0, state_1.saveGame)();
    if (state_1.G.anchorsStilled >= 4 && !state_1.G.unlockedZones.includes(5))
        state_1.G.unlockedZones.push(5);
    // final victory
    if (name === 'The Unsong')
        void finalVictory();
}
function applyBoonEffect(boon) {
    switch (boon) {
        case 'Warden\u2019s Ear':
            state_1.G.bonuses.bind += 0.08;
            break;
        case 'Cinderheart':
            state_1.G.bonuses.atk += 0.06;
            break;
        case 'Stillwater Lung':
            state_1.G.bonuses.hp += 0.08;
            break;
        case 'Deepbound Weave':
            state_1.G.bonuses.def += 0.08;
            break;
        case 'Standing Zenith':
            state_1.G.bonuses.spd += 0.08;
            state_1.G.bonuses.crit += 0.05;
            break;
        case 'Coda Keeper':
            state_1.G.postgame = true;
            break;
    }
    for (const c of [...state_1.G.party, ...state_1.G.vault])
        c.hp = Math.min((0, creature_1.maxHp)(c, state_1.G.bonuses), c.hp);
}
async function finalVictory() {
    state_1.G.postgame = true;
    const boundEnough = state_1.G.uniqueBound.length >= 8;
    await (0, ui_1.cutin)('The Agreement Holds', boundEnough
        ? 'Eight kin lend you their notes and four Anchors stand quiet. The Unsong does not vanish — it simply agrees to wait, which is all anything has ever done. The Song resumes, with a seam you can hear if you listen sideways.'
        : 'The Unsong falls quiet, but your binding is thin: fewer than eight kin answer your name. The seam holds — barely. Bind more, and the repair will hold cleanly.', '#ff9ecb', 'rgba(255,110,180,.5)');
    await (0, ui_1.say)('Threadwarden Vell', [
        'You held your end of it. That is rarer than talent.',
        'The wilds are bolder now — kin walk at greater strength everywhere. Go and finish your Codex, Binder.',
    ]);
    (0, ui_1.toast)('Post-Coda free roam unlocked');
    (0, state_1.saveGame)();
}
// ---------------------------------------------------------------- gates
async function useGate(dir) {
    const target = state_1.G.zone + dir;
    if (target < 0 || target >= zones_1.ZONES.length) {
        (0, ui_1.toast)('The Threadwork ends here.');
        return;
    }
    if (dir === 1) {
        const z = (0, state_1.zoneDef)();
        if (z.guardian && !state_1.G.guardiansBeaten.includes(z.guardian.name)) {
            (0, ui_1.toast)(`${z.guardian.name} still holds this seam.`);
            // nudge player back
            runtime_1.RT.player.x = (0, util_1.clamp)(runtime_1.RT.player.x - 1, 0, 999);
            runtime_1.RT.fromX = runtime_1.RT.player.x;
            return;
        }
        if (target === 5 && state_1.G.anchorsStilled < 4) {
            (0, ui_1.toast)('The Coda will not open until four Anchors are stilled.');
            runtime_1.RT.player.x = (0, util_1.clamp)(runtime_1.RT.player.x - 1, 0, 999);
            runtime_1.RT.fromX = runtime_1.RT.player.x;
            return;
        }
        if (!state_1.G.unlockedZones.includes(target))
            state_1.G.unlockedZones.push(target);
    }
    runtime_1.RT.busy = true;
    await transitionOut();
    state_1.G.zone = target;
    const m = (0, world_1.getMap)(target);
    const spot = dir === 1 ? m.entry : (m.gate ? { x: m.gate.x - 1, y: m.gate.y } : m.entry);
    const p = (0, world_1.findWalkableNear)(m, spot.x, spot.y);
    runtime_1.RT.player.x = p.x;
    runtime_1.RT.player.y = p.y;
    runtime_1.RT.fromX = p.x;
    runtime_1.RT.fromY = p.y;
    runtime_1.RT.moveT = 1;
    state_1.G.px = p.x;
    state_1.G.py = p.y;
    updateHud();
    fx_1.fx.clear();
    await transitionIn();
    runtime_1.RT.busy = false;
    const z = (0, state_1.zoneDef)();
    (0, ui_1.toast)(`${z.name} — ${z.epithet}`);
    audio_1.AUD.play('field');
    (0, state_1.saveGame)();
}
// ---------------------------------------------------------------- new game / starters
function beginNewGame() {
    (0, state_1.setState)((0, state_1.freshState)());
    (0, state_1.addItem)('bind_clay', 5);
    (0, state_1.addItem)('res_salve', 3);
    (0, state_1.addItem)('tool_chart', 1);
    runtime_1.RT.mode = 'field';
    (0, ui_1.hide)((0, ui_1.$)('#title-screen'));
    chooseStarter();
}
function chooseStarter() {
    const cards = species_1.STARTERS.map((id) => {
        const s = (0, species_1.sp)(id);
        return `<div class="card" data-st="${id}">
      <div class="row"><img class="ico" style="width:66px;height:66px" src="${(0, sprites_1.creatureIcon)(s, 72)}" alt="">
        <div><div class="nm" style="font-size:16px">${(0, util_1.esc)(s.name)}</div>
        <div style="margin-top:4px">${(0, menus_1.affPill)(s.affinity)}</div></div></div>
      <div class="lore">${(0, util_1.esc)(s.lore)}</div>
      <div class="stat-line"><span>Vitality</span><b>${s.base.hp}</b></div>
      <div class="stat-line"><span>Force</span><b>${s.base.atk}</b></div>
      <div class="stat-line"><span>Ward</span><b>${s.base.def}</b></div>
      <div class="stat-line"><span>Tempo</span><b>${s.base.spd}</b></div>
      <div class="mt" style="margin-top:8px">Reshapes at Lv ${s.form ? s.form.level : '—'}</div></div>`;
    }).join('');
    (0, ui_1.openModal)((0, ui_1.panel)('Choose Your First Binding', 'Three kin answered the shrine tonight', `<div class="mt" style="line-height:1.8;margin-bottom:14px">Threadwarden Vell sets three reliquaries on the stone. Each already holds an agreement — it only needs a second signature.</div>
     <div class="grid g2">${cards}</div>`, '<span class="mt">This choice shapes your early Concordance.</span>'), (root) => {
        (0, ui_1.bindClicks)(root, '[data-st]', (el) => {
            const id = el.dataset.st;
            (0, ui_1.closeAllModals)();
            void startWithStarter(id);
        });
    });
}
async function startWithStarter(id) {
    const c = (0, creature_1.makeCreature)(id, 5);
    (0, creature_1.healFull)(c, state_1.G.bonuses);
    state_1.G.party.push(c);
    (0, state_1.markBound)(id);
    (0, state_1.markSeen)(id);
    const m = (0, world_1.getMap)(0);
    const p = (0, world_1.findWalkableNear)(m, m.entry.x, m.entry.y);
    runtime_1.RT.player.x = p.x;
    runtime_1.RT.player.y = p.y;
    runtime_1.RT.fromX = p.x;
    runtime_1.RT.fromY = p.y;
    runtime_1.RT.moveT = 1;
    state_1.G.px = p.x;
    state_1.G.py = p.y;
    state_1.G.zone = 0;
    state_1.G.lastShrine = { zone: 0, x: m.shrine.x, y: m.shrine.y };
    runtime_1.RT.mode = 'field';
    (0, ui_1.show)((0, ui_1.$)('#hud'));
    (0, ui_1.show)((0, ui_1.$)('#touch'));
    (0, ui_1.hide)((0, ui_1.$)('#title-screen'));
    updateHud();
    audio_1.AUD.play('field');
    const s = (0, species_1.sp)(id);
    await (0, ui_1.cutin)('First Binding', `${s.name} answers to your name. The agreement is signed on both sides.`, typechart_1.AFFINITY_COLOR[s.affinity], typechart_1.AFFINITY_COLOR[s.affinity] + '99');
    await (0, ui_1.say)('Threadwarden Vell', [
        'There. You are a Binder now, which mostly means you are responsible for things that were previously nobody\u2019s fault.',
        'Walk with WASD or the arrows — or the pad, if your hands prefer glass. Press E or A to speak to what stands before you.',
        'The shrine glows to the north-west of here. Reach it and rest: that is your first lesson.',
    ]);
    (0, state_1.saveGame)();
}
function continueGame() {
    if (!(0, state_1.loadGame)())
        return false;
    const m = (0, world_1.getMap)(state_1.G.zone);
    const p = (0, world_1.findWalkableNear)(m, state_1.G.px, state_1.G.py);
    runtime_1.RT.player.x = p.x;
    runtime_1.RT.player.y = p.y;
    runtime_1.RT.fromX = p.x;
    runtime_1.RT.fromY = p.y;
    runtime_1.RT.moveT = 1;
    runtime_1.RT.mode = 'field';
    (0, ui_1.show)((0, ui_1.$)('#hud'));
    (0, ui_1.show)((0, ui_1.$)('#touch'));
    (0, ui_1.hide)((0, ui_1.$)('#title-screen'));
    updateHud();
    audio_1.AUD.setMuted(state_1.G.muted);
    audio_1.AUD.play('field');
    (0, ui_1.toast)(`Resumed in ${(0, state_1.zoneDef)().name}`);
    return true;
}
function quitToTitle() {
    runtime_1.RT.mode = 'title';
    runtime_1.RT.battle = null;
    (0, ui_1.hide)((0, ui_1.$)('#hud'));
    (0, ui_1.hide)((0, ui_1.$)('#touch'));
    (0, ui_1.hide)((0, ui_1.$)('#battle-ui'));
    (0, ui_1.show)((0, ui_1.$)('#title-screen'));
    refreshTitleButtons();
    audio_1.AUD.play('title');
}
function refreshTitleButtons() {
    const c = (0, ui_1.$)('#btn-continue');
    c.disabled = !(0, state_1.hasSave)();
}
function openPauseMenu() {
    if (runtime_1.RT.mode !== 'field' || runtime_1.RT.busy)
        return;
    (0, menus_1.openPause)(() => quitToTitle());
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>