Back to Shiftfield
SOURCE / PINNED RELEASE

Made of little things.

Shiftfield

Release
5a170c505f6b…
Author-recorded commit
a2e23e0171aa…
License
LICENSE
Author’s source reference
nostr://npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc/wss%3A%2F%2Fgit.napplet.soy%2F/n-e4fb6341ccd

Archive hash verified: 66de7623efd91da1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Shiftfield</title>
    <meta name="description" content="— the Roguelike Minesweeper —
Clear sectors, bank scrap, die rich. Explore deeper depths for more riches, but watch out for hazards, and the ground that shifts beneath your feet.

Built 100% locally and offline with StrataLLM and Qwen3.8 Flash Next" />
  </head>
  <body>
    <!--
      Shiftfield — playable game shell (Phase 2.5 integration).
      Boots the real GameEngine (chaos layer over the Phase 1 world).
      Phase 3 replaces the cleared/dead overlays with the decision,
      summary, and workshop screens.
    -->
    <main id="app" class="app">
      <header class="hud" aria-label="Run status">
        <span class="stat">D<span class="stat-key">epth</span> <b id="depth">0</b></span>
        <span class="stat">T<span class="stat-key">urn</span> <b id="turn">0</b></span>
        <span class="stat">F<span class="stat-key">low</span> <b id="flow">0</b></span>
        <span class="stat stat-runscrap" id="runScrapStat" title="Scrap earned this run (lost on death unless banked)">◈<span class="stat-key"> Scrap</span> <b id="runScrap">0</b></span>
        <span class="stat stat-scrap" title="Banked scrap (safe)">⛃<span class="stat-key"> Banked</span> <b id="scrap">0</b></span>
        <span class="stat stat-shift" id="shiftStat" hidden>
          ⇄ <b id="shiftArrow">—</b> in <b id="shiftIn">—</b>
        </span>
        <span class="stat stat-shield" id="shieldStat" hidden>⛨ <b id="shieldCount">0</b></span>
        <output id="status" class="status status-idle" aria-live="polite">Ready</output>
        <button type="button" id="sfxToggle" class="icon-btn" aria-label="Toggle sound" aria-pressed="false" title="Sound on — click to mute">🔊</button>
        <!-- Field Manual mid-run entry (ruling 2026-10-28, how_to_play_menu_ruling):
             opens the manual landed on Tools & Workshop, without disturbing the run. -->
        <button type="button" id="helpToggle" class="icon-btn" aria-label="Field Manual" title="Field Manual — tools & workshop reference">?</button>
      </header>

      <section id="boardHost" class="board-host" aria-label="Shiftfield grid"></section>

      <!-- Depth-6 Shifter awakening (ruling 2026-10-28): one-shot per run.
           BLOCKING modal — the run cannot continue until acknowledged (ruling
           2026-10-28 follow-up). Full-viewport backdrop intercepts board
           clicks; panel sits centered so the board and its wind stay visible
           behind the dim. -->
      <div id="shifterIntro" class="intro-modal" hidden>
        <div class="intro-panel" role="dialog" aria-modal="true" aria-labelledby="shifterIntroTitle">
          <b id="shifterIntroTitle">You sense a tremble beneath the earth</b>
          <span>The mines are shifting. Watch the telegraphed movement &mdash;
          and know that your actions aren't the only thing that wraps around
          the board...</span>
          <button type="button" id="shifterIntroOk">Understood</button>
        </div>
      </div>

      <!-- ONE-TIME runner-name gate (first-run name gate ruling 2026-10-29):
           interrupts the FIRST run only — while the board is available and the
           player has never been asked (save.name_prompted). Blank + confirm is
           a valid choice (run unnamed); the gate never interrupts again. The
           summary field stays as the later rename path. -->
      <div id="nameGate" class="intro-modal" hidden>
        <div class="intro-panel" role="dialog" aria-modal="true" aria-labelledby="nameGateTitle">
          <b id="nameGateTitle">Choose your runner name</b>
          <span>The leaderboard remembers your deepest depth. Your name rides
          every score you post &mdash; leave it blank to run unnamed.</span>
          <label class="summary-name" for="nameGateInput">Runner name
            <input id="nameGateInput" type="text" maxlength="24" placeholder="unnamed-runner" autocomplete="off" spellcheck="false">
          </label>
          <button type="button" id="nameGateOk">Enter The Depths</button>
        </div>
      </div>

      <footer class="abilitybar" role="toolbar" aria-label="Abilities">
        <button type="button" data-ability="radar" title="Radar Pulse — select, then click a tile: reveals safe tiles in a sweep around it">
          <span class="label">Radar</span> <span class="cost">50f</span><span class="cd" data-cd="radar"></span>
        </button>
        <button type="button" data-ability="seeker" title="Seeker Drone — flags one mine per Workshop level">
          <span class="label">Seeker</span> <span class="cost">50f</span><span class="cd" data-cd="seeker"></span>
        </button>
        <button type="button" data-ability="stabilizer" title="Stabilizer — freezes the Shifter countdown (buy levels in the Workshop)">
          <span class="label">Stabilize</span> <span class="cost">30f</span><span class="cd" data-cd="stabilizer"></span>
        </button>
      </footer>

      <!-- MENU — first screen of the §4.1 loop; Enter The Depths → RUNNING, Workshop → SHOP -->
      <div id="menuScreen" class="overlay" hidden>
        <div class="panel panel-anim" role="dialog" aria-modal="true" aria-labelledby="menuTitle">
          <h2 id="menuTitle">Shiftfield</h2>
          <p class="menu-tag">Roguelike minefield — clear sectors, bank scrap, die rich.</p>
          <p class="menu-stats" id="menuStats"></p>
          <div class="panel-actions">
            <button id="startRunBtn" type="button">Enter The Depths</button>
            <button id="menuWorkshopBtn" class="btn-secondary" type="button">Workshop</button>
            <!-- Field Manual entry (ruling 2026-10-28, how_to_play_menu_ruling). -->
            <button id="menuFieldManualBtn" class="btn-secondary" type="button">Field Manual</button>
            <button id="menuLeaderboardBtn" class="btn-secondary" type="button" hidden>Leaderboard</button>
          </div>
          <p class="stub-note" id="saveNote" hidden></p>
        </div>
      </div>

      <!-- DECISION — bank vs continue after a sector clear (§4.1 step 2) -->
      <div id="decisionScreen" class="overlay" hidden>
        <div class="panel panel-anim" role="dialog" aria-modal="true" aria-labelledby="decisionTitle">
          <h2 id="decisionTitle">Sector cleared</h2>
          <p id="decisionBody"></p>
          <div class="panel-actions">
            <button id="bankBtn" type="button">Bank &amp; Exit</button>
            <button id="continueBtn" class="btn-secondary" type="button">Go Deeper</button>
          </div>
          <p class="stub-note">Scrap in the vault is safe. An explosion destroys some of what you carry.</p>
        </div>
      </div>

      <!-- SUMMARY — run over (§4.1 steps 3/5): banked exit or 50% death salvage -->
      <div id="summaryScreen" class="overlay" hidden>
        <div class="panel panel-anim" role="dialog" aria-modal="true" aria-labelledby="summaryTitle">
          <h2 id="summaryTitle">Boom</h2>
          <p id="summaryCause"></p>
          <dl class="summary-ledger" id="summaryLedger"></dl>
          <label class="summary-name" for="playerNameInput">Runner name
            <input id="playerNameInput" type="text" maxlength="24" placeholder="unnamed-runner" autocomplete="off" spellcheck="false">
          </label>
          <p class="stub-note" id="summaryBoardNote" hidden></p>
          <div class="panel-actions">
            <button id="summaryWorkshopBtn" type="button">Workshop</button>
            <button id="summaryBoardBtn" class="btn-secondary" type="button" hidden>Leaderboard</button>
            <button id="summaryMenuBtn" class="btn-secondary" type="button">Menu</button>
          </div>
        </div>
      </div>

      <!-- LEADERBOARD — shared depth-v1 board (ruling 2026-10-28). Row names are
           untrusted public data: rendered with textContent, never innerHTML. -->
      <div id="boardScreen" class="overlay" hidden>
        <div class="panel panel-anim" role="dialog" aria-modal="true" aria-labelledby="boardTitle">
          <h2 id="boardTitle">Deepest Depth</h2>
          <p id="boardNote"></p>
          <dl class="summary-ledger board-rows" id="boardRows"></dl>
          <p class="stub-note">Scores are player-reported; each runner's deepest board counts. Your identity lives in this browser — clearing site data starts a fresh runner.</p>
          <div class="panel-actions">
            <button id="boardRefreshBtn" type="button">Refresh</button>
            <button id="boardCloseBtn" class="btn-secondary" type="button">Back</button>
          </div>
        </div>
      </div>

      <!-- FIELD MANUAL — how-to-play overlay (ruling 2026-10-28,
           how_to_play_menu_ruling): 3 merged tabs, static approved copy only
           (no engine reads). MENU opens Basics; the HUD `?` lands on Tools &
           Workshop mid-run; Back hands the underlying screen straight back —
           no §4.1 state change, the overlay never alters the phase.
           The three explosion causes reuse the ratified player-word labels
           (ui_status_copy_ruling); engine enums must never appear here —
           pinned by the field-manual-tabs browser check. -->
      <div id="helpScreen" class="overlay" hidden>
        <div class="panel panel-help panel-anim" role="dialog" aria-modal="true" aria-labelledby="helpTitle">
          <h2 id="helpTitle">Field Manual</h2>
          <div class="help-tabs" role="tablist" aria-label="Manual sections">
            <button type="button" class="help-tab active" role="tab" id="helpTabBasics" aria-selected="true">Basics</button>
            <button type="button" class="help-tab" role="tab" id="helpTabHazards" aria-selected="false">Hazards &amp; The Shift</button>
            <button type="button" class="help-tab" role="tab" id="helpTabTools" aria-selected="false">Tools &amp; Workshop</button>
          </div>
          <div class="help-body">
            <section class="help-tab-panel" id="helpPanelBasics" role="tabpanel" aria-labelledby="helpTabBasics">
              <h3>The Loop</h3>
              <p>Your task is to clear a mine field one sector at a time, and maybe get some loot while you're at it.</p>
              <ul>
                <li><b>Left-click / tap</b> — Reveals a tile. A safe move rewards you with <b>Flow</b> and <b>Scrap</b>.</li>
                <li><b>Right-click / long-press</b> — Places a flag which locks down the tile. A locked tile prevents anything from moving in, out, or through it.</li>
                <li>Open every non-mine tile and the sector clears.</li>
                <li>After each sector clear, you may choose to <b>Bank &amp; Exit</b> to save your scrap, or <b>Go Deeper</b> to risk the depths for higher rewards.</li>
                <li><b>Death destroys half of what you carry.</b> Bank your scrap to protect it.</li>
                <li>Between runs, the <b>Workshop</b> turns banked scrap into upgrades and abilities.</li>
              </ul>
              <p>Controls: left-click reveal &middot; right-click flag &middot; tap reveal &middot; long-press flag.</p>
              <h3>Flow &amp; Scrap</h3>
              <p><b>Flow</b> powers abilities.</p>
              <ul>
                <li>Ceiling 100 by default. Raise the cap with upgrades in the Workshop.</li>
              </ul>
              <p><b>Scrap</b> is the run economy.</p>
              <ul>
                <li>Deeper sectors have more scrap, at the risk of more hazards.</li>
                <li><b>Bank</b> at any decision screen to move your haul to the vault.</li>
                <li>Scrap is spent in the Workshop on upgrades and abilities to aid later runs.</li>
              </ul>
              <h3>Death &amp; Banking</h3>
              <p>A run ends when something explodes. You, specifically. There are a few ways an explosion can occur:</p>
              <ol>
                <li><b>A mine detonated</b> — you opened a mine, or a shifted mine landed on ground you had already opened.</li>
                <li><b>Mines collided</b> — a shift forced two mines into one tile.</li>
                <li><b>A mine ignited</b> — fire spread to a mine, or a mine shifted beneath the flames.</li>
              </ol>
              <p>A shield charge absorbs any one of these (see Tools &amp; Workshop). Without one — <b>Boom</b>: half of your unbanked scrap burns with you.</p>
              <p><b>Turn law:</b> only <i>effective</i> actions advance the world. Blocked clicks, rejected flags and refused abilities cost nothing and tick nothing.</p>
            </section>
            <section class="help-tab-panel" id="helpPanelHazards" role="tabpanel" aria-labelledby="helpTabHazards" hidden>
              <h3>The Shift</h3>
              <p>From <b>depth 6</b> the mines themselves start moving.</p>
              <ul>
                <li>Every shift is <b>telegraphed</b>: the HUD chip shows <b>&#8644; direction &mdash; in N turns</b>, and amber wind streaks blow across the board in that direction. The last three turns warn loudly.</li>
                <li>The board <b>wraps</b>: mines leaving one edge re-enter at the opposite edge. The amber edge chevrons and corner brackets are permanent — <b>wrap is always on</b>, at every depth.</li>
                <li>A <b>flag is an anchor</b>. Mines can never move into or out of a flagged tile — a mine whose path is blocked by your flag simply stays put.</li>
                <li>Shifting can force two mines into the same tile if they are blocked by a flag. This will trigger an explosion unless a <b>shield charge</b> eats it and protects you.</li>
                <li>Ice does <b>not</b> stop mines. Only flags do.</li>
                <li>Shift cadence tightens with depth; the <b>Stabilizer</b> freezes the countdown.</li>
              </ul>
              <h3>Hazards</h3>
              <p>Sectors past depth 10 come with additional hazards.</p>
              <p><b>Ice</b> — frozen ground that is too hard to dig through for a time. It blocks <i>you</i>, not the mines.</p>
              <p><b>Fire</b> — burns on hidden ground.</p>
              <ul>
                <li>On <b>odd turns</b>, each fire has a chance to ignite some neighboring tiles. Clear it before it spreads too far.</li>
                <li>Fire is only deadly when it shares a tile <b>with a mine</b> — fire next to a mine does nothing, forever.</li>
                <li>Revealing a fire tile puts it out. <b>Flagging</b> a fire tile puts it out as well (and anchors the ground).</li>
                <li>Ice blocks fire spread.</li>
              </ul>
                          </section>
            <section class="help-tab-panel" id="helpPanelTools" role="tabpanel" aria-labelledby="helpTabTools" hidden>
              <p>Abilities start <b>locked</b> — buy upgrades in the Workshop to use them.</p>
              <ul>
                <li><b>Radar Pulse</b> <i>(50f, 2T cooldown)</i> — select, then click: sweeps a patch of safe tiles open around the target. Grows with Radar Array levels: 3&times;3 &rarr; 21-tile circle &rarr; full 5&times;5. Radar pays <b>no Flow</b> and never floods its own sweep.</li>
                <li><b>Seeker Drone</b> <i>(50f, 3T cooldown)</i> — auto-flags 1 mine (2, then 3 at higher levels).</li>
                <li><b>Stabilizer</b> <i>(30f, 10T cooldown)</i> — freezes the shift countdown for 3 turns (5 / 7 / 10 at higher levels). At max, recast on cooldown and the Shifter never fires.</li>
                <li><b>Shield Generator</b> — increases the number of <b>shield charges the run starts with</b>. A charge absorbs one mine hit, then it's gone until the next run. Charges carry across sectors.</li>
                <li><b>Data Ledger</b> — +25% run scrap per level.</li>
                <li><b>Flow Capacitor</b> — +100 Flow ceiling per level.</li>
              </ul>
            </section>
          </div>
          <div class="panel-actions">
            <button id="helpBackBtn" type="button">Back</button>
          </div>
        </div>
      </div>

      <!-- SHOP / Workshop — tiered permanent upgrades (§4.1 step 6) -->
      <div id="shopScreen" class="overlay" hidden>
        <div class="panel panel-shop panel-anim" role="dialog" aria-modal="true" aria-labelledby="shopTitle">
          <h2 id="shopTitle">Workshop</h2>
          <p class="shop-scrap">⛃ <b id="shopScrap">0</b> scrap</p>
          <div class="upgrade-list" id="upgradeList"></div>
          <div class="panel-actions">
            <button id="shopStartBtn" type="button">Enter The Depths</button>
            <button id="shopBackBtn" class="btn-secondary" type="button">Back</button>
          </div>
        </div>
      </div>
    </main>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>