sovereign-in-violet 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/game.js +174 -21
- package/package.json +1 -1
package/dist/game.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/* sovereign-in-violet 1.0.
|
|
1
|
+
/* sovereign-in-violet 1.0.4, built from violet-hour.html */
|
|
2
2
|
(function(){
|
|
3
|
-
document.head.insertAdjacentHTML("beforeend", "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=Zen+Antique&family=Italianno&family=Caveat:wght@500&family=Yuji+Syuku&family=Zen+Kurenaido&family=VT323&family=Shippori+Mincho:wght@400;600&display=swap\" rel=\"stylesheet\">" + "<style>" + "\n/* paranoia: whisperling margin notes */\n.note.whisperling{opacity:.85;letter-spacing:.04em}\n.note.whisperling .g{filter:blur(1px);opacity:.8}\n.note.whisperling .clear{text-shadow:0 0 6px rgba(201,162,74,.6)}\n:root{\n --bg:#EFE8F3; --surface:#F8F4FA; --raised:#E4D9EC; --ink:#2A1B38; --muted:#6A5680;\n --gold:#83620C; --gold-soft:#C9A33A; --violet:#5B3A82; --line:#D3C4DF; --danger:#8E2F4A;\n --display:\"IM Fell English\", \"Iowan Old Style\", \"Palatino Linotype\", Georgia, serif;\n --body:\"Spectral\", \"Iowan Old Style\", Georgia, \"Times New Roman\", serif;\n box-sizing:border-box;\n padding-top:env(safe-area-inset-top,0px);\n padding-bottom:env(safe-area-inset-bottom,0px);\n}\n@media (prefers-color-scheme: dark){\n :root:not([data-theme=\"light\"]){\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n }\n}\n:root[data-theme=\"dark\"]{\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n}\n*,*::before,*::after{box-sizing:inherit}\nhtml{scroll-padding-top:env(safe-area-inset-top,0px)}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--body);font-size:17px;line-height:1.62;-webkit-font-smoothing:antialiased}\nmain{max-width:640px;margin:0 auto;padding:20px 20px 120px}\nh1,h2,h3{font-family:var(--display);font-weight:400;line-height:1.15;margin:0}\nh1{font-size:2.3rem}\nh2{font-size:1.65rem;margin-bottom:.35rem}\nh3{font-size:1.2rem;color:var(--violet);margin:1.4rem 0 .5rem}\np{margin:0 0 1rem}\n.muted{color:var(--muted)}\n.small{font-size:.9rem}\nbutton{font:inherit;color:inherit;cursor:pointer}\nbutton:focus-visible,textarea:focus-visible,input:focus-visible{outline:2px solid var(--gold);outline-offset:2px}\n.btn{background:var(--violet);color:var(--surface);border:0;border-radius:3px;padding:.7rem 1.1rem;font-size:1rem}\n.btn:disabled{opacity:.5;cursor:default}\n.btn-quiet{background:transparent;border:1px solid var(--line);border-radius:3px;padding:.55rem .9rem;color:var(--ink);font-size:.95rem}\n.btn-link{background:none;border:0;padding:0;color:var(--violet);text-decoration:underline;text-underline-offset:3px;font-size:.95rem}\n.row{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}\n\n/* Title */\n.title{min-height:70vh;display:flex;flex-direction:column;justify-content:center;gap:1.2rem;padding-top:2rem}\n.sigil{width:92px;height:92px;color:var(--gold)}\n.title h1{font-size:3rem;letter-spacing:.01em}\n.title .lede{font-style:italic;color:var(--muted);max-width:30ch;font-size:1.1rem}\n.saves{margin-top:1rem;display:grid;gap:.5rem}\n.save{display:flex;justify-content:space-between;align-items:center;gap:.5rem;border-left:3px solid var(--gold-soft);background:var(--surface);padding:.7rem .9rem}\n.save button.open{background:none;border:0;text-align:left;padding:0;flex:1}\n.save .name{font-family:var(--display);font-size:1.2rem;display:block}\n\n/* Creation */\n.progress{display:flex;gap:4px;margin:0 0 1.4rem}\n.progress span{flex:1;height:3px;background:var(--line)}\n.progress span.done{background:var(--gold)}\n.step-kicker{font-style:italic;color:var(--gold);margin-bottom:.2rem}\n.picked{border-left:2px solid var(--line);padding-left:.8rem;margin:1.2rem 0;color:var(--muted);font-size:.92rem}\n.picked b{color:var(--ink);font-weight:500}\n\n/* Choices */\n.choices{display:grid;gap:.55rem;margin:1.2rem 0 .6rem}\n.choice{display:block;width:100%;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.8rem 1rem;line-height:1.4}\n.choice:hover{border-color:var(--gold-soft)}\n.choice[aria-pressed=\"true\"]{border-color:var(--gold);box-shadow:inset 3px 0 0 var(--gold)}\n.choice .hint{display:block;font-size:.82rem;color:var(--muted);font-style:italic;margin-top:.2rem}\n.extra{display:grid;grid-template-columns:1fr 1fr;gap:.55rem}\n.extra button{background:transparent;border:1px dashed var(--line);border-radius:3px;padding:.65rem;color:var(--muted)}\n.custom{margin-top:.6rem;display:grid;gap:.5rem}\n[hidden]{display:none !important}\ntextarea,input[type=text]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem;resize:vertical}\n\n/* Story */\n.scene{font-size:1.08rem;line-height:1.72}\n.scene p{margin-bottom:1.05rem}\n.action{font-style:italic;color:var(--muted);border-top:1px solid var(--line);padding-top:.8rem;margin:1.4rem 0 .4rem}\n.whisper{display:none}\n.scale.main .ends{color:var(--ink);font-size:.95rem}\n.scale.main .bar{height:3px}\n.omen{font-family:var(--display);font-style:italic;color:var(--gold);margin:0 0 1rem}\n.thinking{display:flex;gap:.8rem;align-items:center;color:var(--muted);font-style:italic;margin:1.5rem 0}\n.thinking svg{width:34px;height:34px;color:var(--gold);flex:none}\n@media (prefers-reduced-motion:no-preference){ .thinking svg{animation:breathe 2.6s ease-in-out infinite} }\n@keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}\n.card{background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:1rem 1.1rem;margin:1.2rem 0}\n.card.gold{border-color:var(--gold-soft);box-shadow:inset 3px 0 0 var(--gold)}\n.card.door{border-color:var(--danger);box-shadow:inset 3px 0 0 var(--danger)}\n.card h3{margin-top:0}\n.error{color:var(--danger)}\n\n/* Sheet */\n.traits{display:grid;grid-template-columns:1fr 1fr;gap:.4rem 1.2rem}\n.trait{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:.35rem 0}\n.pips{display:flex;gap:3px}\n.pips i{width:9px;height:9px;border-radius:50%;border:1px solid var(--gold);display:block}\n.pips i.on{background:var(--gold)}\n.track{display:flex;gap:5px;align-items:center}\n.track i{width:18px;height:18px;border:1.5px solid var(--muted);display:block;border-radius:2px}\n.track i.on{background:var(--danger);border-color:var(--danger)}\n.track.stab i.on{background:var(--violet);border-color:var(--violet)}\n.scale{margin:.7rem 0 1rem}\n.scale .ends{display:flex;justify-content:space-between;font-size:.85rem;color:var(--muted);gap:1rem}\n.scale .ends span:last-child{text-align:right}\n.scale .bar{position:relative;height:2px;background:var(--line);margin:.55rem 0 .2rem}\n.scale .bar::after{content:\"\";position:absolute;left:50%;top:-4px;width:1px;height:10px;background:var(--muted)}\n.scale .dot{position:absolute;top:-6px;width:14px;height:14px;border-radius:50%;background:var(--gold);transform:translateX(-50%);border:2px solid var(--surface)}\nul.plain{list-style:none;padding:0;margin:0}\nul.plain li{padding:.45rem 0;border-bottom:1px solid var(--line)}\nul.plain li:last-child{border-bottom:0}\n.empty{color:var(--muted);font-style:italic}\n\n/* Nav */\nnav.tabs{position:fixed;left:0;right:0;bottom:0;background:var(--surface);border-top:1px solid var(--line);padding-bottom:env(safe-area-inset-bottom,0px);z-index:10}\nnav.tabs .inner{max-width:640px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr)}\nnav.tabs button{background:none;border:0;padding:.75rem .2rem;font-family:var(--display);font-size:1.02rem;color:var(--muted);border-top:2px solid transparent}\nnav.tabs button[aria-current=\"page\"]{color:var(--gold);border-top-color:var(--gold)}\n.seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden}\n.seg button{background:none;border:0;padding:.45rem .8rem;font-size:.92rem;color:var(--muted)}\n.seg button[aria-pressed=\"true\"]{background:var(--violet);color:var(--surface)}\n\n/* Japanese: mincho faces matching the English serif look, no fake italics, Japanese line-breaking */\nhtml[lang=\"ja\"]{\n --display:\"Zen Antique\",\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",serif;\n --body:\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",\"Noto Serif JP\",serif;\n}\nhtml[lang=\"ja\"] body{line-height:1.75;line-break:strict;overflow-wrap:anywhere}\nhtml[lang=\"ja\"] *{font-style:normal !important}\nhtml[lang=\"ja\"] h1,html[lang=\"ja\"] h2,html[lang=\"ja\"] h3{line-height:1.35;letter-spacing:.04em}\nhtml[lang=\"ja\"] .title .lede{max-width:16em}\nhtml[lang=\"ja\"] .step-kicker,html[lang=\"ja\"] .omen,html[lang=\"ja\"] .whisper,html[lang=\"ja\"] .action{letter-spacing:.03em}\n\n/* Page effects: always marked by his eye, always brief, never change what you chose */\n#pfxEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity .5s ease;z-index:60}\n#pfxEye svg{width:100%;height:100%}\n#pfxEye.on{opacity:.9}\n.pfx-flip{transform:scaleX(-1);transition:transform .3s ease}\n.pfx-unflip{transition:transform .3s ease}\n.pfx-mirror{display:block;transform:scaleX(-1)}\n.pfx-glow{border-color:var(--violet) !important;box-shadow:inset 0 0 0 2px var(--violet) !important}\n.pfx-voice{color:var(--violet) !important;font-family:var(--display);font-style:italic}\n.pfx-line{color:var(--violet);font-family:var(--display);font-style:italic;opacity:0;transition:opacity .8s ease;margin:.6rem 0 0}\n.pfx-line.on{opacity:1}\n\n/* Hidden race hints: a faint watching eye and a violet tint that deepen with his Manifestation; your resolve as a small steady light */\nbody{background-image:linear-gradient(rgba(106,64,160,var(--taint,0)),rgba(106,64,160,var(--taint,0)));background-attachment:fixed}\n#watchEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity 2s ease;z-index:55}\n#watchEye svg{width:100%;height:100%}\n.resolve-light{display:inline-block;width:.55rem;height:.55rem;border-radius:50%;background:var(--gold);vertical-align:middle;margin-left:.5rem;opacity:calc(.15 + var(--r,0)*.85);box-shadow:0 0 calc(2px + var(--r,0)*10px) var(--gold)}\n\n.faded-end{animation:fadeAway 9s ease forwards}\n@keyframes fadeAway{from{opacity:1}to{opacity:.45}}\n@media (prefers-reduced-motion:reduce){.faded-end{animation:none;opacity:.6}}\n.hint.pull{color:var(--gold);font-style:normal;margin-bottom:.2rem}\n.learned{margin-top:1rem;padding-top:.8rem;border-top:1px solid var(--line)}\n.learned li{margin:.25rem 0}\n\n/* Pictures: VHS-tape stills */\n.vhs{position:relative;margin:0 0 1rem;border-radius:6px;overflow:hidden;background:#0b0714;box-shadow:0 0 0 1px var(--line)}\n.vhs canvas{display:block;width:100%;height:auto;aspect-ratio:4/3}\n.vhs .osd{position:absolute;top:.5rem;left:.7rem;font-family:\"VT323\",ui-monospace,monospace;font-size:1.25rem;color:#f1ecf8;text-shadow:1px 1px 0 #000,-1px 0 0 rgba(255,0,80,.5);letter-spacing:.06em;font-style:normal !important}\n.vhs .osd.ts{top:auto;bottom:.4rem;left:auto;right:.7rem}\n.vhs .osd.rec{color:#e0a0ff}\n.title .vhs{max-width:26rem}\n.more-pulls summary{cursor:pointer;color:var(--violet);margin:.6rem 0}\n.choice.gift-choice{font-family:var(--his);font-size:1.45rem;line-height:1.1;color:var(--gold);border-color:var(--gold-soft);background:linear-gradient(90deg,rgba(201,162,74,.16),rgba(255,250,238,.6));box-shadow:0 0 14px rgba(201,162,74,.3);animation:giftGlow 3.2s ease-in-out infinite}\n.choice.gift-choice::before{content:\"✦ \";color:var(--gold)}\n@keyframes giftGlow{0%,100%{box-shadow:0 0 10px rgba(201,162,74,.2)}50%{box-shadow:0 0 18px rgba(201,162,74,.45)}}\n@media (prefers-reduced-motion:reduce){.choice.gift-choice{animation:none}}\n.choice.gift-own{font-family:var(--display);font-style:italic;color:var(--ink);border:1px dashed var(--gold-soft)}\n.choice.gift-own::before{content:\"✧ \";color:var(--gold)}\n\n/* ---------- The book ---------- */\n:root, :root[data-theme=\"dark\"], :root:not([data-theme=\"light\"]){\n --bg:#1b1320; --surface:#f1e6cf; --raised:#e6d7b8; --ink:#2b1f2a; --muted:#6b5a55;\n --gold:#8a6a1c; --gold-soft:#b8923a; --violet:#5b3a82; --line:#cbb894; --danger:#8e2f4a;\n --his:\"Italianno\",\"Yuji Syuku\",cursive; --reader:\"Caveat\",\"Zen Kurenaido\",cursive;\n}\n@media (prefers-color-scheme: dark){ :root:not([data-theme=\"light\"]){ --surface:#ddd0b2; --raised:#d0c1a0; } }\nbody{background:radial-gradient(ellipse at 50% 0%, #2a1d33 0%, #140e18 70%) fixed}\nmain{background:var(--surface);background-image:radial-gradient(ellipse at 50% 40%, rgba(255,250,235,.55), rgba(255,250,235,0) 65%),radial-gradient(circle at 20% 80%, rgba(120,90,50,.08), rgba(0,0,0,0) 40%),radial-gradient(circle at 85% 15%, rgba(120,90,50,.07), rgba(0,0,0,0) 35%);\n margin:16px auto 110px;padding:28px 26px 40px;box-shadow:0 0 0 1px rgba(90,60,30,.25), 0 18px 50px rgba(0,0,0,.55), inset 0 0 60px rgba(120,90,50,.18);border-radius:2px 6px 6px 2px;min-height:80vh;position:relative}\nmain::before{content:\"\";position:absolute;inset:0;pointer-events:none;border-left:6px solid rgba(90,60,30,.12);border-radius:2px 0 0 2px}\n.tabs{background:#e6d7b8 !important;border-top:1px solid #b8a37a !important}\n.card{background:rgba(255,250,238,.55)}\n.choice{background:rgba(255,250,238,.6)}\n/* drop cap on each page's first paragraph */\n.scene p:first-child::first-letter{float:left;font-family:var(--display);font-size:3.3em;line-height:.82;padding:.08em .08em 0 0;color:var(--gold)}\nhtml[lang=\"ja\"] .scene p:first-child::first-letter{float:none;font-size:inherit;color:inherit;padding:0}\n.folio{text-align:center;font-family:var(--display);color:var(--muted);font-size:.9rem;margin-top:1.6rem;letter-spacing:.2em}\n/* illustration plates */\n.vhs{background:#f7eedb;padding:7px 7px 0;border:1px solid #6b5436;box-shadow:0 0 0 3px #f7eedb, 0 0 0 4px #6b5436, 0 6px 16px rgba(60,40,20,.25);border-radius:1px;margin:.4rem 4px 1.3rem}\n.vhs canvas{border:1px solid #6b5436}\n.vhs figcaption{font-family:var(--display);font-style:italic;text-align:center;color:#5a4630;font-size:.92rem;padding:.35rem .4rem .45rem}\n/* margin notes */\n.note{display:block;margin:.2rem 0 1rem;max-width:88%;line-height:1.15}\n.note.gold{font-family:var(--his);color:var(--gold);font-size:1.75rem;transform:rotate(-1.6deg);margin-left:12%}\n.note.reader{font-family:var(--reader);color:var(--violet);font-size:1.3rem;transform:rotate(1.2deg);margin-left:2%}\n.note.reader .sig{display:block;font-size:1.05rem;opacity:.8;margin-left:40%}\nhtml[lang=\"ja\"] .note.gold{font-size:1.2rem} html[lang=\"ja\"] .choice.gift-choice{font-size:1.05rem} html[lang=\"ja\"] .note.reader{font-size:1.05rem}\nu.vu{text-decoration:underline wavy var(--violet);text-decoration-thickness:1.5px;text-underline-offset:4px}\n@media (min-width: 1000px){ main{max-width:900px;padding-right:270px} .note{float:right;clear:right;width:215px;margin:0 -250px .8rem 0} .note.gold{margin-left:0} .note.reader{margin-left:0} }\n/* his voice everywhere is his own hand, in tarnished gold */\n.pfx-voice{color:var(--gold) !important;font-family:var(--his) !important;font-style:normal !important;font-size:1.45em;line-height:1.1}\nhtml[lang=\"ja\"] .pfx-voice{font-size:1.05em}\n\n/* ---------- The cover ---------- */\n.cover{min-height:88vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;cursor:pointer;text-align:center;margin:-28px -26px -40px;padding:18px 12px 30px;background:#0b0712;border-radius:2px 6px 6px 2px}\n.cover-art{position:relative;width:min(100%,420px)}\n.cover-art canvas{display:block;width:100%;height:auto;aspect-ratio:2/3;border-radius:3px;box-shadow:0 0 0 1px #6b5436, 0 0 40px rgba(154,99,224,.25)}\n.cover .stars{display:none}\n.cover .stars i{position:absolute;background:#fff6dc;border-radius:50%;opacity:.2;animation:twinkle 2.8s ease-in-out infinite}\n@keyframes twinkle{0%,100%{opacity:.15;transform:scale(.8)}50%{opacity:1;transform:scale(1.4)}}\n.cover h1{color:#e9dcb8;font-size:2.4rem;text-shadow:0 2px 12px rgba(0,0,0,.8)}\n.cover-tap{color:var(--gold-soft);font-family:var(--display);font-style:italic;letter-spacing:.12em;animation:tapPulse 2.4s ease-in-out infinite;margin:0}\n@keyframes tapPulse{0%,100%{opacity:.45}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){ .cover .stars i,.cover-tap{animation:none;opacity:.7} }\n\n/* tarot plates */\n.tarot{margin:.4rem auto 1.2rem;max-width:300px}\n.tarot-card{position:relative;background:#1a1024;padding:58px 16px 64px;border-radius:10px;box-shadow:0 0 0 2px #b8923a, 0 0 0 5px #1a1024, 0 0 0 6px #b8923a, 0 10px 26px rgba(0,0,0,.45);background-image:radial-gradient(circle at 12px 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at 12px calc(100% - 12px),#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) calc(100% - 12px),#b8923a 1.5px,transparent 2px)}\n.tarot-card canvas{display:block;width:100%;height:auto;aspect-ratio:4/3;border:1px solid #b8923a;border-radius:2px}\n.tarot .arc-num,.tarot .arc-name{position:absolute;left:0;right:0;text-align:center;font-family:var(--display);color:#e0c070;letter-spacing:.14em}\n.tarot .arc-num{top:14px;font-size:1.15rem}\n.tarot .arc-num::after,.tarot .arc-name::before{content:\"✧ ✦ ✧\";display:block;font-size:.6rem;letter-spacing:.5em;color:#b8923a;margin:.35rem 0 0}\n.tarot .arc-name::before{margin:0 0 .35rem}\n.tarot .arc-name{bottom:12px;font-size:1rem;text-transform:uppercase}\n.tarot.reversed .tarot-card{transform:rotate(180deg)}\n.tarot figcaption{text-align:center;font-family:var(--display);font-style:italic;color:var(--muted);font-size:.9rem;margin-top:.5rem}\nhtml[lang=\"ja\"] .tarot .arc-name{text-transform:none;letter-spacing:.06em}\n/* his offer: a card held out to you */\n.choice.gift-choice{border-radius:9px !important;border:1px solid var(--gold-soft) !important;box-shadow:0 0 0 3px rgba(255,250,238,.6), 0 0 0 4px var(--gold-soft), 0 6px 16px rgba(120,80,20,.25) !important;transform:rotate(-1.2deg);position:relative;padding-left:2.2rem !important}\n.choice.gift-choice::before{content:\"✦\";position:absolute;left:.7rem;top:.35rem;font-size:.9rem;color:var(--gold)}\n.choice.gift-choice::after{content:\"✦\";position:absolute;right:.7rem;bottom:.3rem;font-size:.9rem;color:var(--gold);transform:rotate(180deg)}\n/* epigraphs, recap, engine */\n.epi{margin:0 0 1.5rem;padding:.1rem 0 .1rem 1rem;border-left:2px solid var(--gold-soft)}\n.epi-q{font-family:var(--display);font-style:italic;font-size:1.12rem;line-height:1.4;margin:0}\n.epi-q::before{content:\"\\201C\"} .epi-q::after{content:\"\\201D\"}\n.epi-by{margin:.35rem 0 0;color:var(--muted);font-size:.88rem;letter-spacing:.03em}\n.card .epi{margin:1rem 0}\n.recap{border:1px dashed var(--line);border-radius:3px;padding:.6rem .9rem;margin:0 0 1.2rem;color:var(--muted);font-size:.95rem}\n.recap summary{cursor:pointer;font-family:var(--display);color:var(--violet)}\n.recap p{margin:.4rem 0 0}\ninput[type=password]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem}\n" + "</style>");
|
|
3
|
+
document.head.insertAdjacentHTML("beforeend", "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=Zen+Antique&family=Italianno&family=Caveat:wght@500&family=Yuji+Syuku&family=Zen+Kurenaido&family=VT323&family=Shippori+Mincho:wght@400;600&display=swap\" rel=\"stylesheet\">" + "<style>" + "\n/* paranoia: whisperling margin notes */\n.note.whisperling{opacity:.85;letter-spacing:.04em}\n.note.whisperling .g{filter:blur(1px);opacity:.8}\n.note.whisperling .clear{text-shadow:0 0 6px rgba(201,162,74,.6)}\n:root{\n --bg:#EFE8F3; --surface:#F8F4FA; --raised:#E4D9EC; --ink:#2A1B38; --muted:#6A5680;\n --gold:#83620C; --gold-soft:#C9A33A; --violet:#5B3A82; --line:#D3C4DF; --danger:#8E2F4A;\n --display:\"IM Fell English\", \"Iowan Old Style\", \"Palatino Linotype\", Georgia, serif;\n --body:\"Spectral\", \"Iowan Old Style\", Georgia, \"Times New Roman\", serif;\n box-sizing:border-box;\n padding-top:env(safe-area-inset-top,0px);\n padding-bottom:env(safe-area-inset-bottom,0px);\n}\n@media (prefers-color-scheme: dark){\n :root:not([data-theme=\"light\"]){\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n }\n}\n:root[data-theme=\"dark\"]{\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n}\n*,*::before,*::after{box-sizing:inherit}\nhtml{scroll-padding-top:env(safe-area-inset-top,0px)}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--body);font-size:17px;line-height:1.62;-webkit-font-smoothing:antialiased}\nmain{max-width:640px;margin:0 auto;padding:20px 20px 120px}\nh1,h2,h3{font-family:var(--display);font-weight:400;line-height:1.15;margin:0}\nh1{font-size:2.3rem}\nh2{font-size:1.65rem;margin-bottom:.35rem}\nh3{font-size:1.2rem;color:var(--violet);margin:1.4rem 0 .5rem}\np{margin:0 0 1rem}\n.muted{color:var(--muted)}\n.small{font-size:.9rem}\nbutton{font:inherit;color:inherit;cursor:pointer}\nbutton:focus-visible,textarea:focus-visible,input:focus-visible{outline:2px solid var(--gold);outline-offset:2px}\n.btn{background:var(--violet);color:var(--surface);border:0;border-radius:3px;padding:.7rem 1.1rem;font-size:1rem}\n.btn:disabled{opacity:.5;cursor:default}\n.btn-quiet{background:transparent;border:1px solid var(--line);border-radius:3px;padding:.55rem .9rem;color:var(--ink);font-size:.95rem}\n.btn-link{background:none;border:0;padding:0;color:var(--violet);text-decoration:underline;text-underline-offset:3px;font-size:.95rem}\n.row{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}\n\n/* Title */\n.title{min-height:70vh;display:flex;flex-direction:column;justify-content:center;gap:1.2rem;padding-top:2rem}\n.sigil{width:92px;height:92px;color:var(--gold)}\n.title h1{font-size:3rem;letter-spacing:.01em}\n.title .lede{font-style:italic;color:var(--muted);max-width:30ch;font-size:1.1rem}\n.saves{margin-top:1rem;display:grid;gap:.5rem}\n.save{display:flex;justify-content:space-between;align-items:center;gap:.5rem;border-left:3px solid var(--gold-soft);background:var(--surface);padding:.7rem .9rem}\n.save button.open{background:none;border:0;text-align:left;padding:0;flex:1}\n.save .name{font-family:var(--display);font-size:1.2rem;display:block}\n\n/* Creation */\n.progress{display:flex;gap:4px;margin:0 0 1.4rem}\n.progress span{flex:1;height:3px;background:var(--line)}\n.progress span.done{background:var(--gold)}\n.step-kicker{font-style:italic;color:var(--gold);margin-bottom:.2rem}\n.picked{border-left:2px solid var(--line);padding-left:.8rem;margin:1.2rem 0;color:var(--muted);font-size:.92rem}\n.picked b{color:var(--ink);font-weight:500}\n\n/* Choices */\n.choices{display:grid;gap:.55rem;margin:1.2rem 0 .6rem}\n.choice{display:block;width:100%;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.8rem 1rem;line-height:1.4}\n.choice:hover{border-color:var(--gold-soft)}\n.choice[aria-pressed=\"true\"]{border-color:var(--gold);box-shadow:inset 3px 0 0 var(--gold)}\n.choice .hint{display:block;font-size:.82rem;color:var(--muted);font-style:italic;margin-top:.2rem}\n.extra{display:grid;grid-template-columns:1fr 1fr;gap:.55rem}\n.extra button{background:transparent;border:1px dashed var(--line);border-radius:3px;padding:.65rem;color:var(--muted)}\n.custom{margin-top:.6rem;display:grid;gap:.5rem}\n[hidden]{display:none !important}\ntextarea,input[type=text]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem;resize:vertical}\n\n/* Story */\n.scene{font-size:1.08rem;line-height:1.72}\n.scene p{margin-bottom:1.05rem}\n.action{font-style:italic;color:var(--muted);border-top:1px solid var(--line);padding-top:.8rem;margin:1.4rem 0 .4rem}\n.whisper{display:none}\n.scale.main .ends{color:var(--ink);font-size:.95rem}\n.scale.main .bar{height:3px}\n.omen{font-family:var(--display);font-style:italic;color:var(--gold);margin:0 0 1rem}\n.thinking{display:flex;gap:.8rem;align-items:center;color:var(--muted);font-style:italic;margin:1.5rem 0}\n.thinking svg{width:34px;height:34px;color:var(--gold);flex:none}\n@media (prefers-reduced-motion:no-preference){ .thinking svg{animation:breathe 2.6s ease-in-out infinite} }\n@keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}\n.card{background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:1rem 1.1rem;margin:1.2rem 0}\n.card.gold{border-color:var(--gold-soft);box-shadow:inset 3px 0 0 var(--gold)}\n.card.door{border-color:var(--danger);box-shadow:inset 3px 0 0 var(--danger)}\n.card h3{margin-top:0}\n.error{color:var(--danger)}\n\n/* Sheet */\n.traits{display:grid;grid-template-columns:1fr 1fr;gap:.4rem 1.2rem}\n.trait{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:.35rem 0}\n.pips{display:flex;gap:3px}\n.pips i{width:9px;height:9px;border-radius:50%;border:1px solid var(--gold);display:block}\n.pips i.on{background:var(--gold)}\n.track{display:flex;gap:5px;align-items:center}\n.track i{width:18px;height:18px;border:1.5px solid var(--muted);display:block;border-radius:2px}\n.track i.on{background:var(--danger);border-color:var(--danger)}\n.track.stab i.on{background:var(--violet);border-color:var(--violet)}\n.scale{margin:.7rem 0 1rem}\n.scale .ends{display:flex;justify-content:space-between;font-size:.85rem;color:var(--muted);gap:1rem}\n.scale .ends span:last-child{text-align:right}\n.scale .bar{position:relative;height:2px;background:var(--line);margin:.55rem 0 .2rem}\n.scale .bar::after{content:\"\";position:absolute;left:50%;top:-4px;width:1px;height:10px;background:var(--muted)}\n.scale .dot{position:absolute;top:-6px;width:14px;height:14px;border-radius:50%;background:var(--gold);transform:translateX(-50%);border:2px solid var(--surface)}\nul.plain{list-style:none;padding:0;margin:0}\nul.plain li{padding:.45rem 0;border-bottom:1px solid var(--line)}\nul.plain li:last-child{border-bottom:0}\n.empty{color:var(--muted);font-style:italic}\n\n/* Nav */\nnav.tabs{position:fixed;left:0;right:0;bottom:0;background:var(--surface);border-top:1px solid var(--line);padding-bottom:env(safe-area-inset-bottom,0px);z-index:10}\nnav.tabs .inner{max-width:640px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr)}\nnav.tabs button{background:none;border:0;padding:.75rem .2rem;font-family:var(--display);font-size:1.02rem;color:var(--muted);border-top:2px solid transparent}\nnav.tabs button[aria-current=\"page\"]{color:var(--gold);border-top-color:var(--gold)}\n.seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden}\n.seg button{background:none;border:0;padding:.45rem .8rem;font-size:.92rem;color:var(--muted)}\n.seg button[aria-pressed=\"true\"]{background:var(--violet);color:var(--surface)}\n\n/* Japanese: mincho faces matching the English serif look, no fake italics, Japanese line-breaking */\nhtml[lang=\"ja\"]{\n --display:\"Zen Antique\",\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",serif;\n --body:\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",\"Noto Serif JP\",serif;\n}\nhtml[lang=\"ja\"] body{line-height:1.75;line-break:strict;overflow-wrap:anywhere}\nhtml[lang=\"ja\"] *{font-style:normal !important}\nhtml[lang=\"ja\"] h1,html[lang=\"ja\"] h2,html[lang=\"ja\"] h3{line-height:1.35;letter-spacing:.04em}\nhtml[lang=\"ja\"] .title .lede{max-width:16em}\nhtml[lang=\"ja\"] .step-kicker,html[lang=\"ja\"] .omen,html[lang=\"ja\"] .whisper,html[lang=\"ja\"] .action{letter-spacing:.03em}\n\n/* Page effects: always marked by his eye, always brief, never change what you chose */\n#pfxEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity .5s ease;z-index:60}\n#pfxEye svg{width:100%;height:100%}\n#pfxEye.on{opacity:.9}\n.pfx-flip{transform:scaleX(-1);transition:transform .3s ease}\n.pfx-unflip{transition:transform .3s ease}\n.pfx-mirror{display:block;transform:scaleX(-1)}\n.pfx-glow{border-color:var(--violet) !important;box-shadow:inset 0 0 0 2px var(--violet) !important}\n.pfx-voice{color:var(--violet) !important;font-family:var(--display);font-style:italic}\n.pfx-line{color:var(--violet);font-family:var(--display);font-style:italic;opacity:0;transition:opacity .8s ease;margin:.6rem 0 0}\n.pfx-line.on{opacity:1}\n\n/* Hidden race hints: a faint watching eye and a violet tint that deepen with his Manifestation; your resolve as a small steady light */\nbody{background-image:linear-gradient(rgba(106,64,160,var(--taint,0)),rgba(106,64,160,var(--taint,0)));background-attachment:fixed}\n#watchEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity 2s ease;z-index:55}\n#watchEye svg{width:100%;height:100%}\n.resolve-light{display:inline-block;width:.55rem;height:.55rem;border-radius:50%;background:var(--gold);vertical-align:middle;margin-left:.5rem;opacity:calc(.15 + var(--r,0)*.85);box-shadow:0 0 calc(2px + var(--r,0)*10px) var(--gold)}\n\n.faded-end{animation:fadeAway 9s ease forwards}\n@keyframes fadeAway{from{opacity:1}to{opacity:.45}}\n@media (prefers-reduced-motion:reduce){.faded-end{animation:none;opacity:.6}}\n.hint.pull{color:var(--gold);font-style:normal;margin-bottom:.2rem}\n.learned{margin-top:1rem;padding-top:.8rem;border-top:1px solid var(--line)}\n.learned li{margin:.25rem 0}\n\n/* Pictures: VHS-tape stills */\n.vhs{position:relative;margin:0 0 1rem;border-radius:6px;overflow:hidden;background:#0b0714;box-shadow:0 0 0 1px var(--line)}\n.vhs canvas{display:block;width:100%;height:auto;aspect-ratio:4/3}\n.vhs .osd{position:absolute;top:.5rem;left:.7rem;font-family:\"VT323\",ui-monospace,monospace;font-size:1.25rem;color:#f1ecf8;text-shadow:1px 1px 0 #000,-1px 0 0 rgba(255,0,80,.5);letter-spacing:.06em;font-style:normal !important}\n.vhs .osd.ts{top:auto;bottom:.4rem;left:auto;right:.7rem}\n.vhs .osd.rec{color:#e0a0ff}\n.title .vhs{max-width:26rem}\n.more-pulls summary{cursor:pointer;color:var(--violet);margin:.6rem 0}\n.choice.gift-choice{font-family:var(--his);font-size:1.45rem;line-height:1.1;color:var(--gold);border-color:var(--gold-soft);background:linear-gradient(90deg,rgba(201,162,74,.16),rgba(255,250,238,.6));box-shadow:0 0 14px rgba(201,162,74,.3);animation:giftGlow 3.2s ease-in-out infinite}\n.choice.gift-choice::before{content:\"✦ \";color:var(--gold)}\n@keyframes giftGlow{0%,100%{box-shadow:0 0 10px rgba(201,162,74,.2)}50%{box-shadow:0 0 18px rgba(201,162,74,.45)}}\n@media (prefers-reduced-motion:reduce){.choice.gift-choice{animation:none}}\n.choice.gift-own{font-family:var(--display);font-style:italic;color:var(--ink);border:1px dashed var(--gold-soft)}\n.choice.gift-own::before{content:\"✧ \";color:var(--gold)}\n\n/* ---------- The book ---------- */\n:root, :root[data-theme=\"dark\"], :root:not([data-theme=\"light\"]){\n --bg:#1b1320; --surface:#f1e6cf; --raised:#e6d7b8; --ink:#2b1f2a; --muted:#6b5a55;\n --gold:#8a6a1c; --gold-soft:#b8923a; --violet:#5b3a82; --line:#cbb894; --danger:#8e2f4a;\n --his:\"Italianno\",\"Yuji Syuku\",cursive; --reader:\"Caveat\",\"Zen Kurenaido\",cursive;\n}\n@media (prefers-color-scheme: dark){ :root:not([data-theme=\"light\"]){ --surface:#ddd0b2; --raised:#d0c1a0; } }\nbody{background:radial-gradient(ellipse at 50% 0%, #2a1d33 0%, #140e18 70%) fixed}\nmain{background:var(--surface);background-image:radial-gradient(ellipse at 50% 40%, rgba(255,250,235,.55), rgba(255,250,235,0) 65%),radial-gradient(circle at 20% 80%, rgba(120,90,50,.08), rgba(0,0,0,0) 40%),radial-gradient(circle at 85% 15%, rgba(120,90,50,.07), rgba(0,0,0,0) 35%);\n margin:16px auto 110px;padding:28px 26px 40px;box-shadow:0 0 0 1px rgba(90,60,30,.25), 0 18px 50px rgba(0,0,0,.55), inset 0 0 60px rgba(120,90,50,.18);border-radius:2px 6px 6px 2px;min-height:80vh;position:relative}\nmain::before{content:\"\";position:absolute;inset:0;pointer-events:none;border-left:6px solid rgba(90,60,30,.12);border-radius:2px 0 0 2px}\n.tabs{background:#e6d7b8 !important;border-top:1px solid #b8a37a !important}\n.card{background:rgba(255,250,238,.55)}\n.choice{background:rgba(255,250,238,.6)}\n/* drop cap on each page's first paragraph */\n.scene p:first-child::first-letter{float:left;font-family:var(--display);font-size:3.3em;line-height:.82;padding:.08em .08em 0 0;color:var(--gold)}\nhtml[lang=\"ja\"] .scene p:first-child::first-letter{float:none;font-size:inherit;color:inherit;padding:0}\n.folio{text-align:center;font-family:var(--display);color:var(--muted);font-size:.9rem;margin-top:1.6rem;letter-spacing:.2em}\n/* illustration plates */\n.vhs{background:#f7eedb;padding:7px 7px 0;border:1px solid #6b5436;box-shadow:0 0 0 3px #f7eedb, 0 0 0 4px #6b5436, 0 6px 16px rgba(60,40,20,.25);border-radius:1px;margin:.4rem 4px 1.3rem}\n.vhs canvas{border:1px solid #6b5436}\n.vhs figcaption{font-family:var(--display);font-style:italic;text-align:center;color:#5a4630;font-size:.92rem;padding:.35rem .4rem .45rem}\n/* margin notes */\n.note{display:block;margin:.2rem 0 1rem;max-width:88%;line-height:1.15}\n.note.gold{font-family:var(--his);color:var(--gold);font-size:1.75rem;transform:rotate(-1.6deg);margin-left:12%}\n.note.reader{font-family:var(--reader);color:var(--violet);font-size:1.3rem;transform:rotate(1.2deg);margin-left:2%}\n.note.reader .sig{display:block;font-size:1.05rem;opacity:.8;margin-left:40%}\nhtml[lang=\"ja\"] .note.gold{font-size:1.2rem} html[lang=\"ja\"] .choice.gift-choice{font-size:1.05rem} html[lang=\"ja\"] .note.reader{font-size:1.05rem}\nu.vu{text-decoration:underline wavy var(--violet);text-decoration-thickness:1.5px;text-underline-offset:4px}\n@media (min-width: 1000px){ main{max-width:900px;padding-right:270px} .note{float:right;clear:right;width:215px;margin:0 -250px .8rem 0} .note.gold{margin-left:0} .note.reader{margin-left:0} }\n/* his voice everywhere is his own hand, in tarnished gold */\n.pfx-voice{color:var(--gold) !important;font-family:var(--his) !important;font-style:normal !important;font-size:1.45em;line-height:1.1}\nhtml[lang=\"ja\"] .pfx-voice{font-size:1.05em}\n\n/* ---------- The cover ---------- */\n.cover{min-height:88vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;cursor:pointer;text-align:center;margin:-28px -26px -40px;padding:18px 12px 30px;background:#0b0712;border-radius:2px 6px 6px 2px}\n.cover-art{position:relative;width:min(100%,420px)}\n.cover-art canvas{display:block;width:100%;height:auto;aspect-ratio:2/3;border-radius:3px;box-shadow:0 0 0 1px #6b5436, 0 0 40px rgba(154,99,224,.25)}\n.cover .stars{display:none}\n.cover .stars i{position:absolute;background:#fff6dc;border-radius:50%;opacity:.2;animation:twinkle 2.8s ease-in-out infinite}\n@keyframes twinkle{0%,100%{opacity:.15;transform:scale(.8)}50%{opacity:1;transform:scale(1.4)}}\n.cover h1{color:#e9dcb8;font-size:2.4rem;text-shadow:0 2px 12px rgba(0,0,0,.8)}\n.cover-tap{color:var(--gold-soft);font-family:var(--display);font-style:italic;letter-spacing:.12em;animation:tapPulse 2.4s ease-in-out infinite;margin:0}\n@keyframes tapPulse{0%,100%{opacity:.45}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){ .cover .stars i,.cover-tap{animation:none;opacity:.7} }\n\n/* tarot plates */\n.tarot{margin:.4rem auto 1.2rem;max-width:300px}\n.tarot-card{position:relative;background:#1a1024;padding:58px 16px 64px;border-radius:10px;box-shadow:0 0 0 2px #b8923a, 0 0 0 5px #1a1024, 0 0 0 6px #b8923a, 0 10px 26px rgba(0,0,0,.45);background-image:radial-gradient(circle at 12px 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at 12px calc(100% - 12px),#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) calc(100% - 12px),#b8923a 1.5px,transparent 2px)}\n.tarot-card canvas{display:block;width:100%;height:auto;aspect-ratio:4/3;border:1px solid #b8923a;border-radius:2px}\n.tarot .arc-num,.tarot .arc-name{position:absolute;left:0;right:0;text-align:center;font-family:var(--display);color:#e0c070;letter-spacing:.14em}\n.tarot .arc-num{top:14px;font-size:1.15rem}\n.tarot .arc-num::after,.tarot .arc-name::before{content:\"✧ ✦ ✧\";display:block;font-size:.6rem;letter-spacing:.5em;color:#b8923a;margin:.35rem 0 0}\n.tarot .arc-name::before{margin:0 0 .35rem}\n.tarot .arc-name{bottom:12px;font-size:1rem;text-transform:uppercase}\n.tarot.reversed .tarot-card{transform:rotate(180deg)}\n.tarot figcaption{text-align:center;font-family:var(--display);font-style:italic;color:var(--muted);font-size:.9rem;margin-top:.5rem}\nhtml[lang=\"ja\"] .tarot .arc-name{text-transform:none;letter-spacing:.06em}\n/* his offer: a card held out to you */\n.choice.gift-choice{border-radius:9px !important;border:1px solid var(--gold-soft) !important;box-shadow:0 0 0 3px rgba(255,250,238,.6), 0 0 0 4px var(--gold-soft), 0 6px 16px rgba(120,80,20,.25) !important;transform:rotate(-1.2deg);position:relative;padding-left:2.2rem !important}\n.choice.gift-choice::before{content:\"✦\";position:absolute;left:.7rem;top:.35rem;font-size:.9rem;color:var(--gold)}\n.choice.gift-choice::after{content:\"✦\";position:absolute;right:.7rem;bottom:.3rem;font-size:.9rem;color:var(--gold);transform:rotate(180deg)}\n/* epigraphs, recap, engine */\n.epi{margin:0 0 1.5rem;padding:.1rem 0 .1rem 1rem;border-left:2px solid var(--gold-soft)}\n.epi-q{font-family:var(--display);font-style:italic;font-size:1.12rem;line-height:1.4;margin:0}\n.epi-q::before{content:\"\\201C\"} .epi-q::after{content:\"\\201D\"}\n.epi-by{margin:.35rem 0 0;color:var(--muted);font-size:.88rem;letter-spacing:.03em}\n.card .epi{margin:1rem 0}\n.recap{border:1px dashed var(--line);border-radius:3px;padding:.6rem .9rem;margin:0 0 1.2rem;color:var(--muted);font-size:.95rem}\n.recap summary{cursor:pointer;font-family:var(--display);color:var(--violet)}\n.recap p{margin:.4rem 0 0}\ninput[type=password]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem}\n/* scene header, choice icons and portraits */\n.story-head{display:flex;justify-content:space-between;align-items:center;gap:.8rem;margin-bottom:.4rem}\n.story-head .step-kicker{margin:0}\n.status-strip{display:flex;gap:2px;flex:none}\n.status-strip .st{width:28px;height:28px;display:block}\n.cicon{width:1.15em;height:1.15em;vertical-align:-.2em;margin-right:.55rem;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:.75}\n.choice.gift-choice .cicon{opacity:1}\n.cast{display:flex;flex-wrap:wrap;gap:.9rem;margin:.2rem 0 1rem}\n.por{margin:0;text-align:center;width:64px}\n.por canvas{display:block;width:56px;height:56px;margin:0 auto;border-radius:50%;box-shadow:0 0 0 1.5px #6b5436}\n.por.sovereign canvas{box-shadow:0 0 0 1.5px var(--gold-soft),0 0 10px rgba(201,162,74,.45)}\n.por.servant canvas{box-shadow:0 0 0 1.5px var(--violet)}\n.por figcaption{font-family:var(--display);font-size:.82rem;color:var(--muted);margin-top:.25rem;line-height:1.2;overflow-wrap:anywhere}\n/* picture first: the plate sketches in as faint ink, then the colour washes in; a gold drop cap blooms, then the words */\n@keyframes sketchIn{0%{opacity:0;filter:grayscale(1) contrast(2.4) brightness(1.55) sepia(.35)}22%{opacity:.75;filter:grayscale(1) contrast(2.4) brightness(1.55) sepia(.35)}100%{opacity:1;filter:none}}\n.vhs.sketch-in canvas{animation:sketchIn 1.5s ease-out both}\n@keyframes dcBloom{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:none}}\n.scene .dc-bloom{animation:dcBloom .5s ease-out both;min-height:3em}\n@keyframes writeIn{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}\n.scene.write-in > *{animation:writeIn .6s ease-out both;animation-delay:1.5s}\n.scene.write-in > *:nth-child(2){animation-delay:1.9s} .scene.write-in > *:nth-child(3){animation-delay:2.3s} .scene.write-in > *:nth-child(n+4){animation-delay:2.7s}\n/* his signs */\n.sv-shadow-slide{position:fixed;left:0;bottom:12vh;width:60px;height:170px;pointer-events:none;z-index:70;color:#120a18;opacity:0;animation:svSlide 2.4s ease-in-out forwards}\n.sv-shadow-slide svg,.sv-shadow-still svg{width:100%;height:100%;fill:currentColor}\n@keyframes svSlide{0%{transform:translateX(-80px) skewX(-8deg);opacity:0}15%{opacity:.32}85%{opacity:.32}100%{transform:translateX(calc(100vw + 40px)) skewX(-8deg);opacity:0}}\n.sv-shadow-still{width:22px;height:62px;color:#120a18;opacity:.16;margin:-2.2rem 0 0 auto}\n.sv-rw{white-space:nowrap}\n.sv-rw s{text-decoration:none;background:linear-gradient(var(--violet),var(--violet)) no-repeat 0 55%/100% 2px}\n.sv-rw .sv-new{font-family:var(--his);color:var(--violet);font-size:1.45em;line-height:.6;margin-left:.15em;display:inline-block;transform:rotate(-3deg)}\n.sv-rw.anim s{animation:svStrike .7s ease-in 2.2s both}\n.sv-rw.anim .sv-new{animation:svInk .9s ease-out 2.9s both}\n@keyframes svStrike{from{background-size:0 2px}to{background-size:100% 2px}}\n@keyframes svInk{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:none}}\n.note.sv-reply{margin-left:22%;transform:rotate(-.8deg);opacity:.9}\n.choice.sv-took{color:var(--violet);border-color:var(--violet) !important;box-shadow:inset 3px 0 0 var(--violet);transition:color .35s,border-color .35s}\n.sv-half{font-style:italic;color:var(--violet);opacity:.45;margin:.9rem 0 0;border-top:1px dashed var(--line);padding-top:.7rem}\n.sv-half.anim{animation:svHalf 1.6s ease-out 3s both}\n@keyframes svHalf{from{opacity:0}to{opacity:.45}}\n.por.sv-behind{position:relative}\n.por.sv-behind canvas{position:relative;z-index:1}\n.por.sv-behind::before{content:\"\";position:absolute;z-index:0;left:26px;top:-4px;width:40px;height:82px;opacity:.7;background:no-repeat center/contain url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 170'%3E%3Cpath fill='%23120a18' d='M17 4h26v26h9v5H8v-5h9z'/%3E%3Cellipse fill='%23120a18' cx='30' cy='48' rx='13' ry='14'/%3E%3Cpath fill='%23120a18' d='M14 64q16-6 32 0l9 100q-25 8-50 0z'/%3E%3C/svg%3E\")}\nmain::after{content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;transition:box-shadow 2s ease;box-shadow:inset 0 0 calc(var(--creep,0) * 90px) calc(var(--creep,0) * 14px) rgba(52,18,82,calc(var(--creep,0) * .6))}\n@media (prefers-reduced-motion:reduce){ .vhs.sketch-in canvas,.scene .dc-bloom,.scene.write-in > *,.sv-rw.anim s,.sv-rw.anim .sv-new,.sv-half.anim{animation:none} }\n" + "</style>");
|
|
4
4
|
var wait = document.getElementById("svLoading"); if (wait) wait.remove();
|
|
5
5
|
document.body.insertAdjacentHTML("afterbegin", "<main id=\"app\" aria-live=\"polite\"></main>\n<div id=\"watchEye\" aria-hidden=\"true\"></div>\n<div id=\"pfxEye\" aria-hidden=\"true\"></div>\n<nav class=\"tabs\" id=\"tabs\" hidden><div class=\"inner\">\n <button data-tab=\"story\">Story</button>\n <button data-tab=\"sheet\">Sheet</button>\n <button data-tab=\"journal\">Journal</button>\n <button data-tab=\"world\">World</button>\n</div></nav>");
|
|
6
6
|
})();
|
|
7
|
-
window.SOVEREIGN_VERSION = "1.0.
|
|
7
|
+
window.SOVEREIGN_VERSION = "1.0.4";
|
|
8
8
|
|
|
9
9
|
/* ---------- Sigil (original mark) ---------- */
|
|
10
10
|
const SIGIL = `<svg class="sigil" viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">
|
|
@@ -444,11 +444,98 @@ async function pfxEcho(chosenIdx){
|
|
|
444
444
|
const bs=[...app.querySelectorAll(".choices [data-choice]")].filter(b=>+b.dataset.choice!==chosenIdx); if(!bs.length) return;
|
|
445
445
|
const b=pick(bs); pfxBusy = true; b.classList.add("pfx-glow"); pfxEye(900); await pfxWait(650); b.classList.remove("pfx-glow"); pfxBusy = false;
|
|
446
446
|
}
|
|
447
|
+
/* ---------- The Sovereign shows himself: "one is deniable, two is weird" ----------
|
|
448
|
+
Chapter I: a top-hatted shadow slides across a page turn (deniable), then a few pages later a violet stroke
|
|
449
|
+
rewrites a word (unmistakable). Later acts escalate: a second hand answers his margin notes, choices turn violet
|
|
450
|
+
when tapped, ink creeps in from the page edges with his Manifestation, his shadow stands behind a portrait,
|
|
451
|
+
and the next page is half written before the player chooses. Each sign is decided once per page and saved,
|
|
452
|
+
so re-rendering never replays it. With page effects off or reduced motion, signs appear still: nothing moves. */
|
|
453
|
+
const svMotion = () => pfxOn && !pfxReduced();
|
|
454
|
+
const svPlayed = new Set(); // signs already animated this session (per page), so switching tabs never replays them
|
|
455
|
+
const SV_SHADOW = `<svg viewBox="0 0 60 170" aria-hidden="true"><path d="M17 4h26v26h9v5H8v-5h9z"/><ellipse cx="30" cy="48" rx="13" ry="14"/><path d="M14 64q16-6 32 0l9 100q-25 8-50 0z"/></svg>`;
|
|
456
|
+
const SV_REPLIES = {
|
|
457
|
+
en:["don't answer him","he wrote that to me too","who are you talking to?","stop reading. please.","he's lying. he's not.","I can see you reading this","close the book"],
|
|
458
|
+
ja:["返事をしないで","私にも同じことを書いた","誰と話してるの?","読むのをやめて。お願い。","嘘よ。嘘じゃない。","読んでいるのが見える","本を閉じて"]
|
|
459
|
+
};
|
|
460
|
+
function svRewriteFor(text){ // the first word he can claim, preferring a middle paragraph: your→our, You→We, I→we
|
|
461
|
+
const ps = String(text||"").trim().split(/\n\s*\n|\n/).filter(Boolean);
|
|
462
|
+
const order = ps.map((_,i)=>i).sort((a,b)=>(a===0)-(b===0));
|
|
463
|
+
for (const re of [[/\b(your)\b/,"our"],[/\b(Your)\b/,"Our"],[/(?:^|>|[.!?]\s+)(You)\b/,"We"],[/\b(I)\b/,"we"]])
|
|
464
|
+
for (const i of order){ if (re[0].test(ps[i])) return {p:i, re:re[0].source, to:re[1]}; }
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
function svDecide(cur){
|
|
468
|
+
if (cur.sg) return cur.sg;
|
|
469
|
+
if (G.ended || G.intermission || cur.priceScene || (cur.reflection && !cur.reflectionDone) || (G.state.deathsDoor && !cur.priceScene)) return {};
|
|
470
|
+
const sg = cur.sg = {}, S = G.signs = G.signs || {};
|
|
471
|
+
const n = G.sceneCount, cs = n - (G.chapterStart||0), act = actOf(), h = G.state.hold||0, m = G.state.manifest ?? 2;
|
|
472
|
+
const later = act >= 2 || h >= 5, deep = act >= 3 || h >= 7;
|
|
473
|
+
if (S.slide == null ? cs >= 3 : (later && n - S.slide >= 6 && Math.random() < 0.12)){ sg.slide = true; S.slide = n; }
|
|
474
|
+
else if (S.slide != null && (S.rewrite == null ? n - S.slide >= 3 : (later && n - S.rewrite >= 5 && Math.random() < 0.15))){
|
|
475
|
+
const rw = svRewriteFor(cur.text); if (rw){ sg.rewrite = rw; S.rewrite = n; } }
|
|
476
|
+
const last = G.history[G.history.length-1];
|
|
477
|
+
if (later && last && last.whisper && Math.random() < 0.5) sg.reply = Math.floor(Math.random()*SV_REPLIES.en.length);
|
|
478
|
+
if (later) sg.violet = true;
|
|
479
|
+
if (deep && (cur.speakers||[]).some(x=>x.kind!=="sovereign")) sg.behind = cur.speakers.findIndex(x=>x.kind!=="sovereign");
|
|
480
|
+
if (deep && cur.choices && cur.choices.length > 1 && n - (S.half||0) >= 4 && Math.random() < 0.5){
|
|
481
|
+
const c = cur.choices.find(x=>x.gift || x.price) || pick(cur.choices);
|
|
482
|
+
sg.half = c.text; S.half = n; }
|
|
483
|
+
return sg;
|
|
484
|
+
}
|
|
485
|
+
function svRewriteHtml(paraHtml, rw, anim){
|
|
486
|
+
const re = new RegExp(rw.re);
|
|
487
|
+
return paraHtml.replace(re, (all, word) => all.replace(word, `<span class="sv-rw${anim?" anim":""}"><s>${word}</s><span class="sv-new">${esc(rw.to)}</span></span>`));
|
|
488
|
+
}
|
|
489
|
+
function svHalfText(t){
|
|
490
|
+
t = String(t||"").trim(); if (!t) return "";
|
|
491
|
+
const line = lang === "ja" ? t : "You " + t.charAt(0).toLowerCase() + t.slice(1).replace(/[.!?]$/,"");
|
|
492
|
+
const sp = line.lastIndexOf(" "), word = line.length - sp - 1;
|
|
493
|
+
return word >= 3 ? line.slice(0, sp + 1 + Math.ceil(word / 2)) : line.slice(0, -1); // stops mid-word, as if the pen was lifted
|
|
494
|
+
}
|
|
495
|
+
function svAfterRender(cur){ // play this page's moving signs once
|
|
496
|
+
const sg = cur.sg || {}, key = n => G.id + ":" + G.sceneCount + ":" + n, motion = svMotion();
|
|
497
|
+
if (sg.slide && motion && !svPlayed.has(key("slide"))){
|
|
498
|
+
svPlayed.add(key("slide"));
|
|
499
|
+
const d = document.createElement("div"); d.className = "sv-shadow-slide"; d.innerHTML = SV_SHADOW; document.body.appendChild(d);
|
|
500
|
+
setTimeout(()=>d.remove(), 2600);
|
|
501
|
+
}
|
|
502
|
+
if (sg.rewrite && motion && !svPlayed.has(key("rw"))){ svPlayed.add(key("rw")); setTimeout(()=>pfxEye(1600), 2600); }
|
|
503
|
+
}
|
|
447
504
|
function thinking(msg){ return `<div class="thinking">${SIGIL.replace('class="sigil"','')}<span>${esc(msg)}</span></div>`; }
|
|
448
505
|
function progressBar(done, total){ let s='<div class="progress" aria-hidden="true">'; for(let i=0;i<total;i++) s+=`<span class="${i<done?'done':''}"></span>`; return s+'</div>'; }
|
|
449
506
|
|
|
507
|
+
const ICONS = {
|
|
508
|
+
talk:'<path d="M4 6h16v9H10l-4 4v-4H4z"/>', look:'<ellipse cx="12" cy="12" rx="9" ry="5.5"/><circle cx="12" cy="12" r="2.4"/>',
|
|
509
|
+
go:'<path d="M5 12h12M13 7l5 5-5 5"/>', run:'<circle cx="14" cy="5" r="2"/><path d="M8 21l3-6 3 2v4M6 12l4-3 4 2 3 3"/>',
|
|
510
|
+
hide:'<path d="M3 12s3-5 9-5 9 5 9 5M4 20L20 4"/>', help:'<path d="M12 20s-7-4.5-7-10a4 4 0 0 1 7-2 4 4 0 0 1 7 2c0 5.5-7 10-7 10z"/>',
|
|
511
|
+
fight:'<path d="M5 19l9-9M14 4l6 6-3 1-4-4zM4 15l5 5"/>', make:'<path d="M4 20l4-1 11-11-3-3L5 16zM14 6l3 3"/>',
|
|
512
|
+
wait:'<circle cx="12" cy="12" r="8"/><path d="M12 8v4l3 2"/>', take:'<path d="M6 12V7a2 2 0 0 1 4 0v4M10 11V5a2 2 0 0 1 4 0v6M14 11V7a2 2 0 0 1 4 0v7a6 6 0 0 1-12 0v-2"/>',
|
|
513
|
+
refuse:'<circle cx="12" cy="12" r="8"/><path d="M6.5 6.5l11 11"/>', call:'<path d="M5 4h4l2 5-3 2a11 11 0 0 0 5 5l2-3 5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 6a2 2 0 0 1 2-2z"/>'
|
|
514
|
+
};
|
|
515
|
+
const iconSvg = k => ICONS[k] ? `<svg class="cicon" viewBox="0 0 24 24" aria-hidden="true">${ICONS[k]}</svg>` : "";
|
|
516
|
+
function portraitFor(canvas, name, kind){
|
|
517
|
+
canvas.width = 96; canvas.height = 96; const c = canvas.getContext("2d"); let h = 0; for (const ch of name) h = (h*31 + ch.charCodeAt(0)) >>> 0; const r = inkRng(h || 1);
|
|
518
|
+
const bg = c.createRadialGradient(48,40,6,48,48,60); bg.addColorStop(0, kind==="sovereign" ? "#3a2050" : "#efe3cc"); bg.addColorStop(1, kind==="sovereign" ? "#0c0614" : "#c9b48e"); c.fillStyle = bg; c.fillRect(0,0,96,96);
|
|
519
|
+
if (kind === "sovereign"){ c.fillStyle="#07030e"; c.beginPath(); c.ellipse(48,62,30,38,0,0,7); c.fill(); c.fillStyle="#f5d98a"; [36,60].forEach(x=>{ c.beginPath(); c.moveTo(x-8,52); c.quadraticCurveTo(x,44,x+8,52); c.quadraticCurveTo(x,49,x-8,52); c.fill(); }); c.strokeStyle="#f3ead8"; c.lineWidth=2; c.beginPath(); c.moveTo(32,66); c.quadraticCurveTo(48,80,64,66); c.stroke(); return; }
|
|
520
|
+
const skins = ["#f3dcc4","#e6c2a0","#c99a74","#9a6a4a","#f6e2d0"], hairs = ["#2a1a14","#5a3a22","#8a5a2a","#c89a4a","#3a3a44","#7a2a2a"], coats = ["#6a5a8a","#4a6a72","#8a5a6a","#5a6a4a","#7a6050"];
|
|
521
|
+
c.fillStyle = coats[Math.floor(r()*coats.length)]; c.beginPath(); c.ellipse(48,100,32,26,0,0,7); c.fill();
|
|
522
|
+
c.fillStyle = skins[Math.floor(r()*skins.length)]; c.beginPath(); c.arc(48,48,24,0,7); c.fill();
|
|
523
|
+
c.fillStyle = hairs[Math.floor(r()*hairs.length)]; c.beginPath(); c.arc(48,42,25, Math.PI*1.05, Math.PI*1.95 + r()*0.3); c.fill();
|
|
524
|
+
const marked = kind === "servant"; c.fillStyle = marked ? "#9a63e0" : "#2a1f2a"; [40,56].forEach(x=>{ c.beginPath(); c.arc(x,50,2.6,0,7); c.fill(); });
|
|
525
|
+
c.fillStyle="rgba(230,140,140,.4)"; [34,62].forEach(x=>{ c.beginPath(); c.arc(x,57,4,0,7); c.fill(); });
|
|
526
|
+
c.strokeStyle="#2a1f2a"; c.lineWidth=1.6; c.beginPath(); c.arc(48,58,5, .2, Math.PI-.2); c.stroke();
|
|
527
|
+
c.strokeStyle="rgba(60,40,50,.6)"; c.lineWidth=1.4; c.beginPath(); c.arc(48,48,24,0,7); c.stroke();
|
|
528
|
+
}
|
|
529
|
+
function statusStrip(){ // the hidden race, shown only as pictures: his moon, your candle, your shadow
|
|
530
|
+
const s = G.state || {}, m = s.manifest ?? 2, r = s.resolve || 0, h = s.hold || 0, f = s.fade || 0;
|
|
531
|
+
const bite = 10 - Math.min(10, m) * 0.9; // the more he manifests, the fuller (and more golden) his moon
|
|
532
|
+
const moon = `<svg viewBox="0 0 40 40" class="st"><circle cx="20" cy="20" r="13" fill="${m>=6?"#e8c060":"#e9dcb8"}"/><circle cx="${20+bite*1.6}" cy="16" r="12" fill="var(--surface)"/></svg>`;
|
|
533
|
+
const candle = `<svg viewBox="0 0 40 40" class="st"><rect x="16" y="20" width="8" height="15" rx="1" fill="#f1e6cf" stroke="#6b5436"/><path d="M20 ${18-r*0.9}c-${1+r*0.3} 3-${1+r*0.3} 6 0 6s${1+r*0.3}-3 0-6z" fill="#f2b640" opacity="${0.35+r*0.065}"/></svg>`;
|
|
534
|
+
const shadow = `<svg viewBox="0 0 40 40" class="st" style="opacity:${(1-f*0.06).toFixed(2)}"><circle cx="12" cy="12" r="4" fill="#4a3a5a"/><path d="M9 16h6l1 12h-8z" fill="#4a3a5a"/><path d="M8 29 L${14+h*2.2} ${31+h*0.5} L${16+h*2.2} ${33+h*0.5} L10 30z" fill="${h>=7?"#5a2a8a":"#1a1224"}" opacity=".7"/></svg>`;
|
|
535
|
+
return `<div class="status-strip" aria-hidden="true">${moon}${candle}${shadow}</div>`;
|
|
536
|
+
}
|
|
450
537
|
function choicePanel(opts, {hints=false, customLabel="Write your own", placeholder="Describe your own choice"}={}){
|
|
451
|
-
return `<div class="choices">${opts.map((o,i)=>`<button class="choice${o.gift==="own"?" gift-own":o.gift?" gift-choice":""}" data-choice="${i}">${esc(o.text)}${hints&&o.hint?`<span class="hint">${esc(o.hint)}</span>`:""}</button>`).join("")}</div>
|
|
538
|
+
return `<div class="choices">${opts.map((o,i)=>`<button class="choice${o.gift==="own"?" gift-own":o.gift?" gift-choice":""}${o.icon?" has-icon":""}" data-choice="${i}">${iconSvg(o.icon)}${esc(o.text)}${hints&&o.hint?`<span class="hint">${esc(o.hint)}</span>`:""}</button>`).join("")}</div>
|
|
452
539
|
<div class="extra"><button data-random>Random</button><button data-custom-toggle>${esc(customLabel)}</button></div>
|
|
453
540
|
<div class="custom" id="customBox" hidden><textarea id="customText" rows="3" placeholder="${esc(placeholder)}"></textarea><div class="row"><button class="btn" data-custom-go>Choose this</button></div></div>`;
|
|
454
541
|
}
|
|
@@ -523,7 +610,7 @@ function renderCreation(){
|
|
|
523
610
|
document.getElementById("backBtn").onclick=renderTitle;
|
|
524
611
|
inp.focus(); return;
|
|
525
612
|
}
|
|
526
|
-
if (s === 1){ renderTensionPick(back); return; }
|
|
613
|
+
if (s === 1){ if (C.skipPick){ C.step = 0; return renderCreation(); } renderTensionPick(back); return; }
|
|
527
614
|
if (s <= PILLARS.length + 1){
|
|
528
615
|
const p = PILLARS[s-2];
|
|
529
616
|
if (!C.options[p.key]){
|
|
@@ -650,6 +737,13 @@ const STRENGTH_HINT = v => v>=4 ? "Pulls hard both ways" : v>=3 ? "Pulls both wa
|
|
|
650
737
|
function renderTensionPick(back){
|
|
651
738
|
const hasSaved = C.profile && Array.isArray(C.profile.tset) && C.profile.tset.length;
|
|
652
739
|
if (!C.quizDone && (!hasSaved || C.retake)) return renderPullQuiz(back);
|
|
740
|
+
// The game decides the pulls from the answers; players no longer pick them. Quiet moments keep refining them later.
|
|
741
|
+
if (!C.skipPick){ C.skipPick = true;
|
|
742
|
+
if (!C.quizDone && hasSaved) C.tset = C.profile.tset.filter(k=>TENSIONS.some(t=>t.key===k));
|
|
743
|
+
C.tensionSet = TENSIONS.filter(t=>C.tset.includes(t.key) || t.key==="devotion");
|
|
744
|
+
C.options = {}; C.step = 2;
|
|
745
|
+
Store.putProfile({...(C.profile||{}), tset:C.tset, own:C.own, personal:C.personal});
|
|
746
|
+
return renderCreation(); }
|
|
653
747
|
const opts = TENSIONS.filter(t=>t.key!=="devotion");
|
|
654
748
|
const draw = () => {
|
|
655
749
|
const firstKeys = C.quizDone ? (C.quizKeys||[]) : C.tset;
|
|
@@ -1046,7 +1140,7 @@ function scenePromptRaw(action){
|
|
|
1046
1140
|
const refl = (reachOn ? G.reflections : []).filter(r=>r.a).slice(-4).map(r=> r.intruded ? `(An entry the player did NOT write; the Sovereign put it in their journal: ${r.a})` : `Q: ${r.q} A: ${r.a}`).join("\n");
|
|
1047
1141
|
const edits = G.edits.map(e=>"- "+e.canon).join("\n");
|
|
1048
1142
|
const rollLine = action.roll ? `HIDDEN ROLL (the player never sees numbers): they attempted this with ${action.roll.trait} at ${action.roll.risk} risk. Outcome: ${action.roll.outcome.toUpperCase()}. Narrate that outcome faithfully.` : `No roll was needed for this action.`;
|
|
1049
|
-
return `${SETTING}\n\n${themesFor(tset(), G && G.personal)}\n\nYou are the narrator of a solo roleplay.\n${pacing}\n${door}\n\nCURRENT STATE: ${stateBrief()}\n\nSTORY SO FAR:\n${(G.chronicle.length > 20 ? [...G.chronicle.slice(0,4), "(…)", ...G.chronicle.slice(-16)] : G.chronicle).join(" ") || "(nothing yet: this is the opening scene. Start in the player's ordinary day, and let the Sovereign's first real intrusion arrive by the end.)"}\n\n${recent ? "MOST RECENT SCENES:\n"+recent+"\n\n" : ""}WHAT THE CHARACTER KNOWS ABOUT THE HIDDEN WORLD: ${(G.known||[]).join("; ") || "nothing yet"}\nCONTINUITY: The character only knows what is listed above or has plainly happened on-page. Choices must never assume knowledge they lack (for example, no choice to enter the mirror world until they have discovered it exists). Never refer back to a past detail (\"this time\", \"again\", \"as before\") unless that detail actually appears in the story so far or the recent scenes.\n\n${edits ? "WORLD FACTS THE PLAYER HAS SET (always honor these):\n"+edits+"\n\n" : ""}${refl ? "THE PLAYER'S OWN REFLECTIONS (let these subtly inform the story; never quote them back):\n"+refl+"\n\n" : ""}PLAYER'S ACTION: ${action.text}\n${rollLine}\n\nWRITE: second person, present tense,
|
|
1143
|
+
return `${SETTING}\n\n${themesFor(tset(), G && G.personal)}\n\nYou are the narrator of a solo roleplay.\n${pacing}\n${door}\n\nCURRENT STATE: ${stateBrief()}\n\nSTORY SO FAR:\n${(G.chronicle.length > 20 ? [...G.chronicle.slice(0,4), "(…)", ...G.chronicle.slice(-16)] : G.chronicle).join(" ") || "(nothing yet: this is the opening scene. Start in the player's ordinary day, and let the Sovereign's first real intrusion arrive by the end.)"}\n\n${recent ? "MOST RECENT SCENES:\n"+recent+"\n\n" : ""}WHAT THE CHARACTER KNOWS ABOUT THE HIDDEN WORLD: ${(G.known||[]).join("; ") || "nothing yet"}\nCONTINUITY: The character only knows what is listed above or has plainly happened on-page. Choices must never assume knowledge they lack (for example, no choice to enter the mirror world until they have discovered it exists). Never refer back to a past detail (\"this time\", \"again\", \"as before\") unless that detail actually appears in the story so far or the recent scenes.\n\n${edits ? "WORLD FACTS THE PLAYER HAS SET (always honor these):\n"+edits+"\n\n" : ""}${refl ? "THE PLAYER'S OWN REFLECTIONS (let these subtly inform the story; never quote them back):\n"+refl+"\n\n" : ""}PLAYER'S ACTION: ${action.text}\n${rollLine}\n\nFIRST LINE: begin your reply with one line in exactly this form, naming this scene's illustration: @pic SETTING TIME WEATHER, where SETTING is one of street, alley, room, station, train, cafe, office, rooftop, park, shop, stage, mirror; TIME is dawn, day, dusk or night; WEATHER is clear, rain, fog or snow. For example: @pic cafe night rain. Then a new line, then the story.\nWRITE: second person, present tense, 80 to 140 words in 2 to 4 short paragraphs. Short, clear sentences and common words, so readers whose first language is not English can follow easily; keep the voice, lose the padding. Let the picture carry the setting. End on a moment that asks for a decision. Plain paragraphs, no headings, no markdown. Avoid the obvious next beat; at least once a chapter, subvert a genre expectation.\nThen write a line containing only @@@ and then ONLY this JSON:\n{"speakers":[{"name":"anyone who speaks or matters in this scene, max 3","kind":"sovereign|friend|stranger|servant"}],\n"choices":[{"icon":"talk|look|go|run|hide|help|fight|make|wait|take|refuse|call","text":"under 10 words","trait":"Nerve|Wits|Grit|Charm","risk":"none|low|medium|high"${action.kind==="face"?',"price":"short phrase"':(offerAllowed()?',"price":"only on the one choice that accepts an offer; omit otherwise"':'')}${giftOfferNow()?',"gift":"${giftOfferKind()} (only on the one choice that uses his power)"':''}${G.nextProbe?(G.nextProbe.regret?',"regret":"no, yes or unsure (see THE QUIET MOMENT)"':',"probe":"2, 1 or 0 (see THE QUIET MOMENT)"'):''}}],\n"changes":{"wounds":0,"stability":0,"hold":0,"resolve":0,"manifest":0,"fade":0,"tensions":{${tset().map(t=>`"${t.key}":0`).join(",")}},"daysPassed":0,"addItems":[],"removeItems":[],"addBonds":[{"name":"","note":""}],"removeBonds":[],"addGifts":[{"name":"","effect":"","cost":""}],"addScars":[]},\n"threads":["up to 6 short open threads, replacing the old list"],\n"npcs":[{"name":"","note":"up to 8 people who matter, replacing the old list"}],\n"aims":["the player's current aims, 2 to 4, replacing the old list; keep them unless the story changes them"],
|
|
1050
1144
|
"picture":{"setting":"street|alley|room|station|train|cafe|office|rooftop|park|shop|stage|mirror (closest match to where this scene happens)","time":"dawn|day|dusk|night","weather":"clear|rain|fog|snow","figures":"0 to 4 other people visible","caption":"a short, evocative title for this illustration, like a plate in an old storybook"},
|
|
1051
1145
|
"whisper":"optional: one line the Sovereign writes in the margin in his own hand from their shadow, in character with his current mask, or null",
|
|
1052
1146
|
"chronicle":"one sentence summarizing what happened this scene",\n"learned":["short facts about the hidden world the character newly and clearly learned this scene, e.g. the mirror world exists; usually empty"],\n"reflection":null,\n"fourthWall":false,\n"offer":false,\n"knew":false,\n"echo":false,\n"haunt":false,\n"readerNote":null,\n"readerUnderline":null,\n"giftUsed":false,\n"giftActed":false,\n"observed":null,\n"intrusion":null,\n"goal":null,\n"work":null,\n"lasting":null,\n"ending":false}\nRULES FOR THE JSON: exactly 4 choices unless ending. Make choices distinct and concrete; where natural, let them pull toward different sides of the tensions. Risk "none" for safe actions. wounds change 0 to +2 for real harm (negative only for healing); stability change 0 to -2 for horror or loss (positive only for real rest or connection). Tension changes -2 to +2 only for tensions this scene's action truly touched, mostly 0. Gifts are rare, always cost something, and are strongest in the mirror world. "daysPassed": 0 within a day, 1 or more when time moves on. ${G.history.length && G.history[G.history.length-1].whisper ? "\"whisper\" must be null this scene (he spoke last scene)." : "Include a \"whisper\" in roughly one scene in three or four, never two in a row, and never lecturing or giving advice."} Empty arrays when nothing changes. ${reflRule()}\n${fwRule()}\n${offerRule(action)}\n${hauntRules(action)}`;
|
|
@@ -1065,13 +1159,10 @@ async function takeTurn(action, isRetry){
|
|
|
1065
1159
|
ctl = new AbortController();
|
|
1066
1160
|
const res = await sample(scenePrompt(action), {
|
|
1067
1161
|
modelTier: tier, cache:false, signal: ctl.signal,
|
|
1068
|
-
onText: ({text})=>{
|
|
1069
|
-
const narr = text.split("@@@")[0];
|
|
1070
|
-
const live = document.getElementById("liveScene");
|
|
1071
|
-
if (live) { const th = document.getElementById("liveThinking"); if (th) th.remove(); live.innerHTML = paras(narr); }
|
|
1072
|
-
}
|
|
1162
|
+
onText: ({text})=>{ try { liveReveal(text); } catch(e){} }
|
|
1073
1163
|
});
|
|
1074
|
-
const
|
|
1164
|
+
const sp = splitPic(res.text);
|
|
1165
|
+
const parts = sp.rest.split("@@@");
|
|
1075
1166
|
const narrative = parts[0].trim();
|
|
1076
1167
|
let data = null;
|
|
1077
1168
|
if (parts[1]) data = parseLoose(parts[1]);
|
|
@@ -1080,6 +1171,7 @@ async function takeTurn(action, isRetry){
|
|
|
1080
1171
|
data = { choices: [] }; // keep the scene; stats just don't change this turn
|
|
1081
1172
|
}
|
|
1082
1173
|
if (!Array.isArray(data.choices) || !data.choices.length) data.choices = [{text:"Continue", trait:"Wits", risk:"none"}];
|
|
1174
|
+
if (sp.pic) data.picture = {...((data.picture && typeof data.picture === "object") ? data.picture : {}), ...sp.pic}; // keep the picture that already sketched in
|
|
1083
1175
|
applyScene(action, narrative, data);
|
|
1084
1176
|
delete G.pending;
|
|
1085
1177
|
await Store.put(G);
|
|
@@ -1144,13 +1236,14 @@ function applyScene(action, narrative, d){
|
|
|
1144
1236
|
if (G.history.length > 8) G.history = G.history.slice(-8);
|
|
1145
1237
|
let refl = null;
|
|
1146
1238
|
if (reflAllowed() && d.reflection && typeof d.reflection === "string" && d.reflection.trim()){ refl = d.reflection.trim(); G.lastReflection = G.sceneCount; }
|
|
1147
|
-
const choices = (Array.isArray(d.choices)?d.choices:[]).slice(0,4).map(c=>({text:String(c.text||""), trait:TRAITS.includes(c.trait)?c.trait:"Wits", risk:["none","low","medium","high"].includes(c.risk)?c.risk:"none", price:c.price?String(c.price):null, probe:[0,1,2].includes(parseInt(c.probe)) ? parseInt(c.probe) : null, regret:["no","yes","unsure"].includes(c.regret) ? c.regret : null, gift: c.gift === "own" ? "own" : (c.gift === true || c.gift === "true" || c.gift === "shortcut") ? "shortcut" : false}));
|
|
1239
|
+
const choices = (Array.isArray(d.choices)?d.choices:[]).slice(0,4).map(c=>({text:String(c.text||""), trait:TRAITS.includes(c.trait)?c.trait:"Wits", risk:["none","low","medium","high"].includes(c.risk)?c.risk:"none", price:c.price?String(c.price):null, probe:[0,1,2].includes(parseInt(c.probe)) ? parseInt(c.probe) : null, regret:["no","yes","unsure"].includes(c.regret) ? c.regret : null, icon: ICONS[c.icon] ? c.icon : null, gift: c.gift === "own" ? "own" : (c.gift === true || c.gift === "true" || c.gift === "shortcut") ? "shortcut" : false}));
|
|
1148
1240
|
if (choices.filter(c=>c.gift).length > 1) choices.forEach((c,i)=>{ if (c.gift && i !== choices.findIndex(x=>x.gift)) c.gift = false; });
|
|
1149
1241
|
if (choices.some(c=>c.gift)) G.lastGiftOffer = G.sceneCount;
|
|
1150
1242
|
const rn = (typeof d.readerNote === "string" && d.readerNote.trim()) ? d.readerNote.trim().slice(0,140) : null;
|
|
1151
1243
|
if (rn) G.lastReaderNote = G.sceneCount;
|
|
1152
1244
|
const rAuthor = rn ? readerAuthor() : null;
|
|
1153
|
-
|
|
1245
|
+
const speakers = (Array.isArray(d.speakers)?d.speakers:[]).filter(x=>x && x.name).slice(0,3).map(x=>({name:String(x.name).slice(0,30), kind:["sovereign","friend","stranger","servant"].includes(x.kind)?x.kind:"stranger"}));
|
|
1246
|
+
G.current = {speakers, readerNote: rn, readerBy: rAuthor ? rAuthor.name : "", underline: (rn && typeof d.readerUnderline === "string") ? d.readerUnderline.trim().slice(0,80) : "", text:narrative, choices, reflection:refl, reflectionDone:false, priceScene: action.kind==="face", picture: cleanPicture(d.picture, G.sceneCount*97 + (G.chapter||1)*13)};
|
|
1154
1247
|
if (d.offer === true) G.lastOffer = G.sceneCount;
|
|
1155
1248
|
if (d.knew === true) G.lastKnew = G.sceneCount;
|
|
1156
1249
|
if (d.echo === true) G.lastEcho = G.sceneCount;
|
|
@@ -1257,10 +1350,11 @@ function endChapter(type, lasting){
|
|
|
1257
1350
|
function raceHints(){
|
|
1258
1351
|
const m = G && G.state ? (G.state.manifest ?? 2) : 0;
|
|
1259
1352
|
document.documentElement.style.setProperty("--taint", String((m*0.013).toFixed(3)));
|
|
1353
|
+
document.documentElement.style.setProperty("--creep", String(Math.max(0, Math.min(1, (m - 3) / 7)).toFixed(2))); // ink creeps in from the page edges
|
|
1260
1354
|
const w = document.getElementById("watchEye");
|
|
1261
1355
|
if (w){ if (!w.innerHTML) w.innerHTML = SIGIL; w.style.opacity = G ? String(0.05 + m*0.035) : "0"; }
|
|
1262
1356
|
}
|
|
1263
|
-
function clearHints(){ document.documentElement.style.setProperty("--taint","0"); const w=document.getElementById("watchEye"); if (w) w.style.opacity="0"; }
|
|
1357
|
+
function clearHints(){ document.documentElement.style.setProperty("--taint","0"); document.documentElement.style.setProperty("--creep","0"); const w=document.getElementById("watchEye"); if (w) w.style.opacity="0"; }
|
|
1264
1358
|
|
|
1265
1359
|
const ENDINGS = {
|
|
1266
1360
|
free: "You resisted his call and sealed him away, perhaps with a touch of his own power. But an idea is never truly gone. You'll always be glancing over your shoulder: at a flash of violet or tarnished gold, a shadow that moves a beat too late, a laugh that sounds almost like his.",
|
|
@@ -1607,6 +1701,51 @@ function cleanPicture(p, seed){
|
|
|
1607
1701
|
weather: ["clear","rain","fog","snow"].includes(p.weather) ? p.weather : "clear",
|
|
1608
1702
|
figures: Math.max(0, Math.min(4, parseInt(p.figures)||0)), seed, caption: typeof p.caption === "string" ? p.caption.trim().slice(0,70) : "" };
|
|
1609
1703
|
}
|
|
1704
|
+
function splitPic(text){ // reads the "@pic setting time weather" first line; while it is still arriving, nothing is shown
|
|
1705
|
+
const t = String(text||""), lead = t.replace(/^\s+/, "");
|
|
1706
|
+
const nl = lead.indexOf("\n"), first = nl < 0 ? lead : lead.slice(0, nl);
|
|
1707
|
+
if (nl < 0 && /^@(p(i(c)?)?)?$/i.test(first)) return {pic:null, rest:"", waiting:true}; // the picture line is still arriving
|
|
1708
|
+
if (!/^@pic\b/i.test(first)) return {pic:null, rest:t};
|
|
1709
|
+
if (nl < 0) return {pic:null, rest:"", waiting:true};
|
|
1710
|
+
const pic = {};
|
|
1711
|
+
lead.slice(0, nl).replace(/^@pic\b/i, "").toLowerCase().split(/[\s|,]+/).forEach(w=>{
|
|
1712
|
+
if (PIC_SETTINGS.includes(w)) pic.setting = w;
|
|
1713
|
+
else if (["dawn","day","dusk","night"].includes(w)) pic.time = w;
|
|
1714
|
+
else if (["clear","rain","fog","snow"].includes(w)) pic.weather = w; });
|
|
1715
|
+
return {pic: pic.setting ? pic : null, rest: lead.slice(nl + 1)};
|
|
1716
|
+
}
|
|
1717
|
+
let revealedScene = null, shownTextScene = null; // scenes whose picture already sketched in / whose text already appeared live
|
|
1718
|
+
const REVEAL_MS = 1500, DROPCAP_MS = 550;
|
|
1719
|
+
function liveReveal(text){ // painting first, then the gold drop cap, then the words as they arrive
|
|
1720
|
+
const L = liveReveal.s || (liveReveal.s = {}), sp = splitPic(text);
|
|
1721
|
+
if (L.scene !== G.sceneCount + 1){ Object.assign(L, {scene:G.sceneCount + 1, pic:false, t0:0, cap:0, timer:null}); }
|
|
1722
|
+
const live = document.getElementById("liveScene"), box = document.getElementById("livePic");
|
|
1723
|
+
if (!live) return;
|
|
1724
|
+
if (sp.waiting) return;
|
|
1725
|
+
if (sp.pic && !L.pic && box){
|
|
1726
|
+
L.pic = true; L.t0 = Date.now(); revealedScene = G.sceneCount + 1;
|
|
1727
|
+
const th = document.getElementById("liveThinking"); if (th) th.remove();
|
|
1728
|
+
box.innerHTML = picHtml("livePicC", G.sceneCount + 1, "");
|
|
1729
|
+
box.firstElementChild.classList.add("sketch-in");
|
|
1730
|
+
paintPicture("livePicC", cleanPicture(sp.pic, (G.sceneCount + 1)*97 + (G.chapter||1)*13));
|
|
1731
|
+
}
|
|
1732
|
+
const narr = sp.rest.split("@@@")[0];
|
|
1733
|
+
L.text = narr;
|
|
1734
|
+
const wait = L.pic && !pfxReduced() ? L.t0 + REVEAL_MS - Date.now() : 0;
|
|
1735
|
+
const show = () => {
|
|
1736
|
+
const th = document.getElementById("liveThinking"); if (th && L.text.trim()) th.remove();
|
|
1737
|
+
const el = document.getElementById("liveScene"); if (!el || !L.text.trim()) return;
|
|
1738
|
+
if (!L.cap){ // the drop cap blooms alone before the words follow
|
|
1739
|
+
L.cap = Date.now(); el.innerHTML = `<p class="dc-bloom">${esc(L.text.trim().charAt(0))}</p>`;
|
|
1740
|
+
if (!pfxReduced()) { clearTimeout(L.timer); L.timer = setTimeout(show, DROPCAP_MS); return; }
|
|
1741
|
+
}
|
|
1742
|
+
const left = L.cap + DROPCAP_MS - Date.now();
|
|
1743
|
+
if (left > 0 && !pfxReduced()) { clearTimeout(L.timer); L.timer = setTimeout(show, left); return; }
|
|
1744
|
+
el.innerHTML = paras(L.text); shownTextScene = L.scene;
|
|
1745
|
+
};
|
|
1746
|
+
clearTimeout(L.timer);
|
|
1747
|
+
if (wait > 0) L.timer = setTimeout(show, wait); else show();
|
|
1748
|
+
}
|
|
1610
1749
|
function tapeClock(n){ const t = 14*3600 + (n||0)*97; const h = Math.floor(t/3600)%24, m = Math.floor(t/60)%60, sec = t%60; return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(sec).padStart(2,"0")}`; }
|
|
1611
1750
|
function picHtml(id, n, caption){
|
|
1612
1751
|
return `<figure class="vhs" aria-hidden="true"><canvas id="${id}" width="320" height="240"></canvas>${caption ? `<figcaption>${esc(caption)}</figcaption>` : ""}</figure>`;
|
|
@@ -1709,9 +1848,11 @@ function renderStoryShell(action, inner){
|
|
|
1709
1848
|
setTimeout(pfxLoading, 0);
|
|
1710
1849
|
tabs.hidden = false; setTabs();
|
|
1711
1850
|
const last = G.current;
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1851
|
+
const opening = action && (action.kind==="start" || action.kind==="chapter") && G.mode !== "oneshot"; // same header and chapter quote as the finished page, so nothing jumps
|
|
1852
|
+
app.innerHTML = `<div class="story-head"><p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?", one-shot":`, chapter ${ROMAN[G.chapter||1]||G.chapter}`}</p>${G.state ? statusStrip() : ""}</div>
|
|
1853
|
+
${opening ? epigraphHtml(quoteFor("open", G, G.chapter||1)) : ""}
|
|
1854
|
+
${action && action.kind!=="start" && action.kind!=="chapter" ? `<p class="action">${esc(action.label||action.text)}</p>` : ""}
|
|
1855
|
+
<div id="storyBody"><div id="livePic"></div><div class="scene" id="liveScene"></div><div id="liveThinking">${inner}</div><div class="row"><button class="btn-link" id="stopBtn">Stop</button></div></div>`;
|
|
1715
1856
|
const sb = document.getElementById("stopBtn"); if (sb) sb.onclick=()=>ctl?.abort();
|
|
1716
1857
|
}
|
|
1717
1858
|
function setTabs(){ tabs.querySelectorAll("[data-tab]").forEach(b=>{ if(b.dataset.tab===view.tab) b.setAttribute("aria-current","page"); else b.removeAttribute("aria-current"); }); }
|
|
@@ -1730,12 +1871,15 @@ function renderStory(){
|
|
|
1730
1871
|
const cur = G.current;
|
|
1731
1872
|
if (!cur){ if (G.pending) return takeTurn(G.pending); return takeTurn({text:"Begin the story.", kind:"start"}); }
|
|
1732
1873
|
const last = G.history[G.history.length-1];
|
|
1733
|
-
let html = `<p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?`, one-shot, scene ${G.sceneCount}`:`, chapter ${ROMAN[G.chapter||1]||G.chapter}`}</p>`;
|
|
1874
|
+
let html = `<div class="story-head"><p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?`, one-shot, scene ${G.sceneCount}`:`, chapter ${ROMAN[G.chapter||1]||G.chapter}`}</p>${statusStrip()}</div>`;
|
|
1734
1875
|
if (resumeRecap){ resumeRecap = false; const rc = (G.chronicle||[]).slice(-3); if (rc.length && G.sceneCount > 1) html += `<details class="recap" open><summary>Previously</summary>${rc.map(x=>`<p>${esc(x)}</p>`).join("")}</details>`; }
|
|
1735
1876
|
if (G.mode !== "oneshot" && G.sceneCount - (G.chapterStart||0) === 1 && !G.ended && !G.intermission) html += epigraphHtml(quoteFor("open", G, G.chapter||1));
|
|
1736
1877
|
if (last && last.action!=="Began the story." && last.action!=="A new chapter.") html += `<p class="action">${esc(last.action)}</p>`;
|
|
1737
1878
|
if (last && last.omen) html += `<p class="omen">${esc(last.omen)}</p>`;
|
|
1738
|
-
|
|
1879
|
+
const fresh = revealedScene !== G.sceneCount && !G.ended && !G.intermission, freshText = fresh && shownTextScene !== G.sceneCount;
|
|
1880
|
+
revealedScene = shownTextScene = G.sceneCount;
|
|
1881
|
+
if (cur.picture) html += picHtml("scenePic", G.sceneCount, cur.picture.caption ? `Plate ${G.sceneCount}. ${cur.picture.caption}` : "").replace('<figure class="vhs"', `<figure class="vhs${fresh?" sketch-in":""}"`);
|
|
1882
|
+
const sg = svDecide(cur), svAnim = n => svMotion() && !svPlayed.has(G.id + ":" + G.sceneCount + ":" + n);
|
|
1739
1883
|
let sceneHtml = paras(cur.text);
|
|
1740
1884
|
if (cur.underline){ const u = esc(cur.underline); const at = sceneHtml.indexOf(u); if (u.length > 3 && at >= 0) sceneHtml = sceneHtml.slice(0,at) + `<u class="vu">${u}</u>` + sceneHtml.slice(at+u.length); }
|
|
1741
1885
|
let noteGold = (last && last.whisper) ? `<aside class="note gold">${esc(last.whisper)}</aside>` : "";
|
|
@@ -1746,11 +1890,15 @@ function renderStory(){
|
|
|
1746
1890
|
noteGold = `<aside class="note gold whisperling" aria-hidden="true">${words.join(" ")}</aside>`; } }
|
|
1747
1891
|
const noteReader = cur.readerNote ? `<aside class="note reader">${esc(cur.readerNote)}${cur.readerBy ? `<span class="sig">— ${esc(cur.readerBy)}</span>` : ""}</aside>` : "";
|
|
1748
1892
|
const ps = sceneHtml.split("</p>").filter(x=>x.trim()).map(x=>x+"</p>");
|
|
1893
|
+
if (sg.rewrite && ps[sg.rewrite.p]) ps[sg.rewrite.p] = svRewriteHtml(ps[sg.rewrite.p], sg.rewrite, svAnim("rw"));
|
|
1894
|
+
if (noteGold && sg.reply != null) noteGold += `<aside class="note reader sv-reply" aria-hidden="true">${esc((SV_REPLIES[lang] || SV_REPLIES.en)[sg.reply])}</aside>`;
|
|
1749
1895
|
if (noteGold) ps.splice(Math.min(1, ps.length), 0, noteGold);
|
|
1750
1896
|
if (noteReader) ps.splice(Math.min(ps.length, noteGold ? 3 : 2), 0, noteReader);
|
|
1751
1897
|
sceneHtml = ps.join("");
|
|
1752
|
-
html += `<div class="
|
|
1898
|
+
if (cur.speakers && cur.speakers.length) html += `<div class="cast">${cur.speakers.map((sp,i)=>`<figure class="por ${sp.kind}${sg.behind === i ? " sv-behind" : ""}"><canvas id="por${i}"></canvas><figcaption>${esc(sp.kind==="sovereign"?"???":sp.name)}</figcaption></figure>`).join("")}</div>`;
|
|
1899
|
+
html += `<div class="scene${freshText ? " write-in" : ""}">${sceneHtml}</div>`;
|
|
1753
1900
|
html += `<p class="folio">— ${G.sceneCount} —</p>`;
|
|
1901
|
+
if (sg.slide && !svMotion()) html += `<div class="sv-shadow-still" aria-hidden="true">${SV_SHADOW}</div>`; // gentle version: printed on the page, not moving
|
|
1754
1902
|
|
|
1755
1903
|
if (cur.reflection && !cur.reflectionDone){
|
|
1756
1904
|
html += `<div class="card gold"><h3>A quiet moment</h3><p>${esc(cur.reflection)}</p>
|
|
@@ -1806,8 +1954,12 @@ function renderStory(){
|
|
|
1806
1954
|
const isPrice = cur.priceScene && G.state.deathsDoor;
|
|
1807
1955
|
const shown = isPrice ? opts : opts.map(o=>o.price ? {...o, hint:"Price: "+o.price} : o);
|
|
1808
1956
|
html += choicePanel(shown, {hints:true, customLabel: isPrice?"Name your own price":"Write your own", placeholder: isPrice?"What will you give up?":"What do you do? (Start with /edit to change the world instead)"});
|
|
1957
|
+
if (sg.half) html += `<p class="sv-half${svAnim("half") ? " anim" : ""}" aria-hidden="true">${esc(svHalfText(sg.half))}</p>`;
|
|
1809
1958
|
app.innerHTML = html;
|
|
1959
|
+
if (sg.half) svPlayed.add(G.id + ":" + G.sceneCount + ":half");
|
|
1960
|
+
svAfterRender(cur);
|
|
1810
1961
|
paintPicture("scenePic", cur.picture);
|
|
1962
|
+
(cur.speakers||[]).forEach((sp,i)=>{ const cv = document.getElementById("por"+i); if (cv) try { portraitFor(cv, sp.name, sp.kind); } catch(e){} });
|
|
1811
1963
|
raceHints();
|
|
1812
1964
|
pfxStory();
|
|
1813
1965
|
wireChoices(shown, async (o, how)=>{
|
|
@@ -1831,6 +1983,7 @@ function renderStory(){
|
|
|
1831
1983
|
return takeTurn({text:o.text, kind:"custom", gift: o.giftKind || false, regretOf: (pr && pr.regret) ? pr : null, regret: (pr && pr.regret) ? "instead" : null,
|
|
1832
1984
|
probeOf: (pr && !pr.regret) ? pr : null, probe: (pr && !pr.regret && o.probeRead != null) ? o.probeRead : null, quietOwn: pr ? o.text : null, roll: secretRoll(trait, risk)});
|
|
1833
1985
|
}
|
|
1986
|
+
if (sg.violet && how === "choice"){ const b = app.querySelector(`[data-choice="${shown.indexOf(o)}"]`); if (b){ b.classList.add("sv-took"); if (svMotion()) await pfxWait(480); } }
|
|
1834
1987
|
if (how === "choice") await pfxEcho(shown.indexOf(o));
|
|
1835
1988
|
const pp = G.pendingProbe;
|
|
1836
1989
|
const probeOf = (how === "choice" && o.probe != null && pp && !pp.regret) ? pp : null;
|
package/package.json
CHANGED