Migration: consolidate Echo6 docs to cortex with full infrastructure cleanup sync
- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup) - Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing - Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack - Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure - Removes 4 deprecated runbook duplicates (canonical versions live in projects/) - Adds .gitignore for binary archives and editor temp files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
89834796ff
commit
e9231ac24a
93 changed files with 51223 additions and 254 deletions
823
projects/mmud/last-ember-chronicle.html
Normal file
823
projects/mmud/last-ember-chronicle.html
Normal file
|
|
@ -0,0 +1,823 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Last Ember — Chronicle</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ember: #e8713a;
|
||||
--ember-glow: #ff9d5c;
|
||||
--ember-deep: #c44e1a;
|
||||
--ash: #1a1714;
|
||||
--charcoal: #0d0b09;
|
||||
--smoke: #2a2520;
|
||||
--smoke-light: #3d3630;
|
||||
--parchment: #d4c4a8;
|
||||
--parchment-dark: #b8a88c;
|
||||
--parchment-faded: #a89878;
|
||||
--bone: #c8b898;
|
||||
--blood: #8b2020;
|
||||
--blood-bright: #cc3333;
|
||||
--gold: #c4a44a;
|
||||
--gold-dim: #8a7a3a;
|
||||
--frost: #7a9ab0;
|
||||
--poison: #5a8a4a;
|
||||
--text-bright: #e8dcc8;
|
||||
--text-dim: #9a8e78;
|
||||
--text-ghost: #5a5244;
|
||||
--victory: #5a8a4a;
|
||||
--defeat: #8b2020;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--charcoal);
|
||||
color: var(--text-bright);
|
||||
font-family: 'Crimson Text', Georgia, serif;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#ember-canvas {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.page-wrap {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ═══ NAV ═══ */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid rgba(90,82,68,0.15);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--text-ghost);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-link:hover { color: var(--parchment-faded); }
|
||||
.nav-link.active {
|
||||
color: var(--parchment);
|
||||
border-bottom-color: var(--ember);
|
||||
}
|
||||
|
||||
.nav-home {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 14px;
|
||||
color: var(--parchment-faded);
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.1em;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav-home:hover { color: var(--ember-glow); }
|
||||
|
||||
/* ═══ PAGE HEADER ═══ */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
padding: 40px 0 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: clamp(22px, 4vw, 32px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--parchment);
|
||||
text-shadow: 0 0 30px rgba(232,113,58,0.2);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
color: var(--text-ghost);
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin: 28px 0;
|
||||
color: var(--text-ghost);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2em;
|
||||
font-family: 'Cinzel', serif;
|
||||
}
|
||||
|
||||
.divider::before, .divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--smoke-light), transparent);
|
||||
}
|
||||
|
||||
/* ═══ CHRONICLE — EPOCH CARDS ═══ */
|
||||
.epoch-card {
|
||||
position: relative;
|
||||
margin-bottom: 40px;
|
||||
padding: 28px 32px;
|
||||
background: linear-gradient(180deg, rgba(26,23,20,0.95), rgba(13,11,9,0.95));
|
||||
border: 1px solid var(--smoke-light);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.epoch-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.epoch-card.victory::before {
|
||||
background: linear-gradient(90deg, transparent, var(--victory), transparent);
|
||||
}
|
||||
|
||||
.epoch-card.defeat::before {
|
||||
background: linear-gradient(90deg, transparent, var(--defeat), transparent);
|
||||
}
|
||||
|
||||
.epoch-card.current::before {
|
||||
background: linear-gradient(90deg, transparent, var(--ember), transparent);
|
||||
animation: current-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes current-pulse {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.epoch-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.epoch-number {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--text-ghost);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.epoch-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--parchment);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.epoch-outcome {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 14px;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
border-radius: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.epoch-outcome.victory {
|
||||
border: 1px solid var(--victory);
|
||||
color: var(--victory);
|
||||
background: rgba(90,138,74,0.08);
|
||||
}
|
||||
|
||||
.epoch-outcome.defeat {
|
||||
border: 1px solid var(--defeat);
|
||||
color: var(--blood-bright);
|
||||
background: rgba(139,32,32,0.08);
|
||||
}
|
||||
|
||||
.epoch-outcome.ongoing {
|
||||
border: 1px solid var(--ember);
|
||||
color: var(--ember-glow);
|
||||
background: rgba(232,113,58,0.08);
|
||||
animation: ongoing-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes ongoing-pulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.epoch-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.epoch-meta-item {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.epoch-meta-item .meta-val {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.epoch-summary {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.epoch-summary p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.epoch-summary p:last-child { margin-bottom: 0; }
|
||||
|
||||
.epoch-summary .name { color: var(--parchment); font-weight: 600; }
|
||||
.epoch-summary .place { color: var(--ember-glow); font-style: italic; }
|
||||
.epoch-summary .item { color: var(--gold); }
|
||||
|
||||
.epoch-roster {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(90,82,68,0.12);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.epoch-roster .roster-names {
|
||||
color: var(--text-dim);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* ═══ JOURNALS — TAB SYSTEM ═══ */
|
||||
.journal-section { display: none; }
|
||||
.journal-section.active { display: block; }
|
||||
.chronicle-section { display: none; }
|
||||
.chronicle-section.active { display: block; }
|
||||
|
||||
.npc-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid var(--smoke-light);
|
||||
}
|
||||
|
||||
.npc-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 14px 8px;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--text-ghost);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.npc-tab:hover { color: var(--parchment-faded); }
|
||||
|
||||
.npc-tab.active {
|
||||
color: var(--parchment);
|
||||
border-bottom-color: var(--ember);
|
||||
}
|
||||
|
||||
.npc-tab .tab-icon {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.npc-tab.active .tab-icon { opacity: 0.9; }
|
||||
|
||||
/* Journal entries */
|
||||
.journal-feed { display: none; }
|
||||
.journal-feed.active { display: block; }
|
||||
|
||||
.journal-entry {
|
||||
margin-bottom: 32px;
|
||||
padding: 24px 28px;
|
||||
background: linear-gradient(180deg, rgba(26,23,20,0.9), rgba(13,11,9,0.9));
|
||||
border: 1px solid var(--smoke-light);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.journal-entry::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 3px; height: 100%;
|
||||
}
|
||||
|
||||
.journal-entry.grist::before { background: var(--ember); opacity: 0.4; }
|
||||
.journal-entry.maren::before { background: var(--blood-bright); opacity: 0.4; }
|
||||
.journal-entry.torval::before { background: var(--gold); opacity: 0.4; }
|
||||
.journal-entry.whisper::before { background: var(--frost); opacity: 0.4; }
|
||||
|
||||
.journal-date {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.15em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.journal-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.journal-text p {
|
||||
margin-bottom: 10px;
|
||||
text-indent: 1.5em;
|
||||
}
|
||||
|
||||
.journal-text p:first-child { text-indent: 0; }
|
||||
.journal-text p:last-child { margin-bottom: 0; }
|
||||
|
||||
/* Voice-specific styling */
|
||||
.journal-entry.grist .journal-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.journal-entry.whisper .journal-text {
|
||||
font-style: italic;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.journal-entry.maren .journal-text {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.journal-entry.torval .journal-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.journal-npc-sig {
|
||||
margin-top: 14px;
|
||||
text-align: right;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 11px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ═══ FOOTER ═══ */
|
||||
.page-footer {
|
||||
text-align: center;
|
||||
padding: 32px 0 48px;
|
||||
border-top: 1px solid rgba(90,82,68,0.15);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.page-footer a {
|
||||
color: var(--text-ghost);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.page-footer a:hover { color: var(--ember); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.epoch-header { flex-direction: column; gap: 10px; }
|
||||
.epoch-card { padding: 20px; }
|
||||
.journal-entry { padding: 18px 20px; }
|
||||
.npc-tab { font-size: 9px; padding: 10px 4px; }
|
||||
.npc-tab .tab-icon { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="ember-canvas"></canvas>
|
||||
|
||||
<div class="page-wrap">
|
||||
|
||||
<!-- NAV -->
|
||||
<nav class="nav-bar">
|
||||
<a class="nav-home" href="#">The Last Ember</a>
|
||||
<span style="color:var(--smoke-light)">·</span>
|
||||
<a class="nav-link active" data-page="chronicle" onclick="showPage('chronicle')">Chronicle</a>
|
||||
<a class="nav-link" data-page="journals" onclick="showPage('journals')">Journals</a>
|
||||
<a class="nav-link" href="#">Board</a>
|
||||
</nav>
|
||||
|
||||
<!-- ════════════════════════════════════ -->
|
||||
<!-- CHRONICLE PAGE -->
|
||||
<!-- ════════════════════════════════════ -->
|
||||
<div class="chronicle-section active" id="page-chronicle">
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Chronicle</h1>
|
||||
<p class="page-subtitle">Every epoch leaves its mark. The dungeon forgets. We do not.</p>
|
||||
</div>
|
||||
|
||||
<div class="divider">CURRENT EPOCH</div>
|
||||
|
||||
<!-- CURRENT EPOCH -->
|
||||
<div class="epoch-card current">
|
||||
<div class="epoch-header">
|
||||
<div>
|
||||
<div class="epoch-number">EPOCH VII · DAY 17 OF 30</div>
|
||||
<div class="epoch-title">The Siege of the Drowned Mines</div>
|
||||
</div>
|
||||
<span class="epoch-outcome ongoing">In Progress</span>
|
||||
</div>
|
||||
<div class="epoch-meta">
|
||||
<span class="epoch-meta-item">MODE: <span class="meta-val">Hold the Line</span></span>
|
||||
<span class="epoch-meta-item">BREACH: <span class="meta-val">The Emergence (open)</span></span>
|
||||
<span class="epoch-meta-item">PLAYERS: <span class="meta-val">7</span></span>
|
||||
<span class="epoch-meta-item">SECRETS: <span class="meta-val">11/20</span></span>
|
||||
</div>
|
||||
<div class="epoch-summary">
|
||||
<p>Seventeen days in and the water still rises. <span class="name">Kael</span> has led the push through the second depth, establishing <span class="place">Checkpoint Alpha</span> through sheer attrition — three deaths, two retreats, and a final dawn push that cleared the cluster in a single session. The Bounty Troll that haunted <span class="place">the Sunken Gallery</span> for nine days fell to a combined effort, its last breath echoing through flooded corridors that have already begun to reclaim the rooms behind the front line.</p>
|
||||
<p><span class="name">Mira</span> has proven the epoch's quiet weapon — twelve secrets uncovered, including the <span class="item">Ancient Ward</span> that halved the second floor's regen and gave the fighters a window they desperately needed. The Breach opened two days ago and something massive stirs within. Floor 3 is barely mapped. Floor 4 is a rumor. Thirteen days remain, and the mines are not finished with them yet.</p>
|
||||
</div>
|
||||
<div class="epoch-roster">
|
||||
ROSTER: <span class="roster-names">Kael · Mira · Torr · Sable · Dren · Ash · Vex</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">PAST EPOCHS</div>
|
||||
|
||||
<!-- EPOCH VI — VICTORY -->
|
||||
<div class="epoch-card victory">
|
||||
<div class="epoch-header">
|
||||
<div>
|
||||
<div class="epoch-number">EPOCH VI · 30 DAYS · FEB 2026</div>
|
||||
<div class="epoch-title">The Crown of the Ember Wyrm</div>
|
||||
</div>
|
||||
<span class="epoch-outcome victory">Victory</span>
|
||||
</div>
|
||||
<div class="epoch-meta">
|
||||
<span class="epoch-meta-item">MODE: <span class="meta-val">Retrieve & Escape</span></span>
|
||||
<span class="epoch-meta-item">BREACH: <span class="meta-val">The Resonance</span></span>
|
||||
<span class="epoch-meta-item">PLAYERS: <span class="meta-val">9</span></span>
|
||||
<span class="epoch-meta-item">SECRETS: <span class="meta-val">18/20</span></span>
|
||||
</div>
|
||||
<div class="epoch-summary">
|
||||
<p>They called it the impossible run. <span class="name">Torr</span> claimed the <span class="item">Crown of the Ember Wyrm</span> on the twenty-second day, four floors deep in chambers that burned with a heat that had no source. The Pursuer awakened three rooms behind — an eyeless thing that moved without sound and killed without hesitation. <span class="name">Mira</span> had spent six days warding the third floor, and <span class="name">Sable</span> held the chokepoint between the second and third depths for eleven hours before the Pursuer caught her. She died on her feet. The Crown passed to <span class="name">Ash</span> through the relay, and the final sprint through the first floor took four minutes of real time and a year off everyone's nerves.</p>
|
||||
<p>The Resonance Breach had been the epoch's turning point — a puzzle dungeon between floors two and three that <span class="name">Dren</span> solved alone over three quiet days while the rest of the server fought for every room. The shortcut it opened shaved two floors off the escape route and made the impossible merely improbable. Eighteen of twenty secrets fell. The last two died with the epoch, their locations known to no one. <span class="name">Kael</span> finished at level ten — the first to cap since Epoch III. The Crown rests in the Hall. The Wyrm's chambers have already begun to reshape.</p>
|
||||
</div>
|
||||
<div class="epoch-roster">
|
||||
ROSTER: <span class="roster-names">Kael · Mira · Torr · Sable · Dren · Ash · Vex · Lira · Puck</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EPOCH V — DEFEAT -->
|
||||
<div class="epoch-card defeat">
|
||||
<div class="epoch-header">
|
||||
<div>
|
||||
<div class="epoch-number">EPOCH V · 30 DAYS · JAN 2026</div>
|
||||
<div class="epoch-title">The Warden of the Bone Pits</div>
|
||||
</div>
|
||||
<span class="epoch-outcome defeat">Defeat</span>
|
||||
</div>
|
||||
<div class="epoch-meta">
|
||||
<span class="epoch-meta-item">MODE: <span class="meta-val">Hold the Line</span></span>
|
||||
<span class="epoch-meta-item">BREACH: <span class="meta-val">The Incursion</span></span>
|
||||
<span class="epoch-meta-item">PLAYERS: <span class="meta-val">5</span></span>
|
||||
<span class="epoch-meta-item">SECRETS: <span class="meta-val">13/20</span></span>
|
||||
</div>
|
||||
<div class="epoch-summary">
|
||||
<p>Five adventurers against a dungeon that fought back with everything it had. The <span class="place">Bone Pits</span> earned their name — floors slick with calcite, walls studded with things that used to be alive, and a regen rate on the third depth that three players simply could not outpace. <span class="name">Kael</span> and <span class="name">Mira</span> held the second floor for twelve consecutive days, a feat of endurance that the barkeep still recounts to anyone who'll listen, but the third floor's checkpoints required a coordination window that never came. The Incursion Breach on day fifteen made it worse — monsters pouring upward through the new passage, forcing <span class="name">Torr</span> to abandon the push and defend cleared ground.</p>
|
||||
<p>The Warden never spawned. They never reached it. On day twenty-eight, the front line collapsed back to <span class="place">Checkpoint Beta</span> on floor two and held there, grim and exhausted, while the last rooms fell dark around them. <span class="name">Dren</span> joined on day nineteen — too late to turn the tide, but early enough to witness the slow retreat. Thirteen secrets found, seven left buried. The epoch ended not with a killing blow but with a long silence, the dungeon reclaiming what it had never truly lost. Grist poured five drinks that night. Nobody ordered them.</p>
|
||||
</div>
|
||||
<div class="epoch-roster">
|
||||
ROSTER: <span class="roster-names">Kael · Mira · Torr · Dren · Sable</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EPOCH IV — VICTORY -->
|
||||
<div class="epoch-card victory">
|
||||
<div class="epoch-header">
|
||||
<div>
|
||||
<div class="epoch-number">EPOCH IV · 30 DAYS · DEC 2025</div>
|
||||
<div class="epoch-title">The Fall of the Iron Colossus</div>
|
||||
</div>
|
||||
<span class="epoch-outcome victory">Victory</span>
|
||||
</div>
|
||||
<div class="epoch-meta">
|
||||
<span class="epoch-meta-item">MODE: <span class="meta-val">Raid Boss</span></span>
|
||||
<span class="epoch-meta-item">BREACH: <span class="meta-val">The Heist</span></span>
|
||||
<span class="epoch-meta-item">PLAYERS: <span class="meta-val">11</span></span>
|
||||
<span class="epoch-meta-item">SECRETS: <span class="meta-val">20/20</span></span>
|
||||
</div>
|
||||
<div class="epoch-summary">
|
||||
<p>Eleven adventurers. Three thousand three hundred hit points of ancient iron and malice squatting in the deepest chamber of the fourth floor. The <span class="place">Iron Colossus</span> rolled Armor Phase and No Escape — a combination that meant once you committed below twenty-five percent, you were finishing the fight or dying in it. The first week was pure scouting. <span class="name">Lira</span> lost two days' gold learning what the phase transitions looked like. <span class="name">Puck</span> discovered the armor weakness on day nine — a ritual hidden behind a stat-gated secret on floor three that permanently stripped the Colossus's defenses. The tide turned.</p>
|
||||
<p>By day twenty, every player on the server had contributed damage. The final phase began on a Tuesday morning when <span class="name">Kael</span> pushed it below the threshold and the exits sealed. He died. <span class="name">Vex</span> went in next and died. <span class="name">Ash</span> went in third with stacked discovery buffs, two consumables, and a borrowed <span class="item">Runed Maul</span> from Torval's back shelf. The Colossus fell in six rounds. The only epoch where every secret was found. <span class="name">Mira</span> found the twentieth on day twenty-nine — a lore secret hidden in something Whisper had said on day three that nobody thought to write down.</p>
|
||||
</div>
|
||||
<div class="epoch-roster">
|
||||
ROSTER: <span class="roster-names">Kael · Mira · Torr · Sable · Dren · Ash · Vex · Lira · Puck · Strand · Wick</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════════════ -->
|
||||
<!-- JOURNALS PAGE -->
|
||||
<!-- ════════════════════════════════════ -->
|
||||
<div class="journal-section" id="page-journals">
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Journals</h1>
|
||||
<p class="page-subtitle">Four voices. Same day. Different truths.</p>
|
||||
</div>
|
||||
|
||||
<!-- NPC TABS -->
|
||||
<div class="npc-tabs">
|
||||
<div class="npc-tab active" data-npc="grist" onclick="showJournal('grist')">
|
||||
<span class="tab-icon">🍺</span>
|
||||
Grist
|
||||
</div>
|
||||
<div class="npc-tab" data-npc="maren" onclick="showJournal('maren')">
|
||||
<span class="tab-icon">🩸</span>
|
||||
Maren
|
||||
</div>
|
||||
<div class="npc-tab" data-npc="torval" onclick="showJournal('torval')">
|
||||
<span class="tab-icon">⚖</span>
|
||||
Torval
|
||||
</div>
|
||||
<div class="npc-tab" data-npc="whisper" onclick="showJournal('whisper')">
|
||||
<span class="tab-icon">👁</span>
|
||||
Whisper
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GRIST'S JOURNAL -->
|
||||
<div class="journal-feed active" id="journal-grist">
|
||||
|
||||
<div class="journal-entry grist">
|
||||
<div class="journal-date">EPOCH VII · DAY 17</div>
|
||||
<div class="journal-text">
|
||||
<p>Kael came in bloody again. Wouldn't say from what. Ordered two drinks, finished one, stared at the wall for ten minutes, then asked about the bounty board. I told him the troll was done. He already knew. He's the one who killed it.</p>
|
||||
<p>Mira stopped by after. She found something on the second floor — wouldn't say what exactly, but she had that look. The one where she knows something the dungeon doesn't want her to know. Traded a token for a hint about floor three. I gave her what I had. She'll figure out the rest.</p>
|
||||
<p>Sable came in late. Died again. Third time this epoch. Didn't want to talk about it. I poured her something warm and told her the front line held. It did. Barely. Floor two lost two rooms overnight but Alpha's holding. That checkpoint isn't going anywhere.</p>
|
||||
<p>Seven of them now. Seven against whatever's down there. Thirteen days left. The Breach opened yesterday and something's moving inside it. Big. They can feel it through the floor when it shifts. I can feel it through the bar.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Grist</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry grist">
|
||||
<div class="journal-date">EPOCH VII · DAY 16</div>
|
||||
<div class="journal-text">
|
||||
<p>The troll died today. Took nine days. Kael landed the killing blow but Mira and Torr chipped it down to nothing over the past week. That thing regenerated every night and every morning someone went back in. That's what this place does to people. It makes them stubborn.</p>
|
||||
<p>New bounty went up. Spiders on the eastern branch of floor two. Six of them. Torr's already on it. He likes the quiet work — finds the nest, clears what he can, gets out. No glory, just progress. Good kid.</p>
|
||||
<p>The Breach cracked open sometime after midnight. I heard it. Everyone heard it. The lanterns flickered for the first time in longer than I can remember. Something poured through that crack that wasn't light and wasn't dark. Dren was the first one down to look. Hasn't come back to report yet.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Grist</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry grist">
|
||||
<div class="journal-date">EPOCH VII · DAY 15</div>
|
||||
<div class="journal-text">
|
||||
<p>Told them. Three days I've been saying the walls were getting thin. Nobody listens to the barkeep until the ground starts shaking. The Breach is open. The passage sits between the second and third depths, and whatever's inside it is not from either floor.</p>
|
||||
<p>Quiet day otherwise. Everyone's saving their actions for tomorrow. Smart. The dungeon doesn't care about smart, but it helps.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Grist</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- MAREN'S JOURNAL -->
|
||||
<div class="journal-feed" id="journal-maren">
|
||||
|
||||
<div class="journal-entry maren">
|
||||
<div class="journal-date">EPOCH VII · DAY 17</div>
|
||||
<div class="journal-text">
|
||||
<p>Three today. Kael first — deep lacerations across the forearms, consistent with something that grabs before it bites. He sat still while I worked. Didn't flinch. That's not bravery. That's numbness. I've seen the difference.</p>
|
||||
<p>Sable second. Blunt force trauma to the ribs, probably from a charging attack she didn't sidestep. I asked her why she rushed the room. She said she thought she could make it. They always think they can make it. I set the rib and told her to stay above floor one for two days. She won't.</p>
|
||||
<p>Torr came in for a routine patch. Minor cuts, nothing structural. He's careful. Moves like someone who's been hurt enough times to know exactly how much it costs. I appreciate that. More of them should learn it before they learn it the hard way.</p>
|
||||
<p>The Breach is open. I can smell it from here — ozone and something older. I know what's on the other side of cracks like that. I know what lives in the spaces between floors. I went there once. I'm not going back. But they will. And I'll be here when they crawl out.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Maren</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry maren">
|
||||
<div class="journal-date">EPOCH VII · DAY 16</div>
|
||||
<div class="journal-text">
|
||||
<p>Sable again. That's twice in three days. This time it was the Gallery — took a hit from the troll's replacement spawn that she wasn't expecting. The original was stronger, she said. As if that's an excuse for not respecting the weaker one. The weaker ones still kill you. I've stitched enough of them to know.</p>
|
||||
<p>No other patients. The troll's death seems to have given them confidence. Confidence is when I get busy.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Maren</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry maren">
|
||||
<div class="journal-date">EPOCH VII · DAY 15</div>
|
||||
<div class="journal-text">
|
||||
<p>No injuries today. Unusual. They're all resting, saving themselves for whatever the Breach brings. The smart ones prepare. The others will be my patients tomorrow.</p>
|
||||
<p>The scar on my palm aches when the dungeon shifts. It ached all night.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Maren</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TORVAL'S JOURNAL -->
|
||||
<div class="journal-feed" id="journal-torval">
|
||||
|
||||
<div class="journal-entry torval">
|
||||
<div class="journal-date">EPOCH VII · DAY 17</div>
|
||||
<div class="journal-text">
|
||||
<p>Good day! Sold a reinforced buckler to Sable — she needed it after, well, you know. Third death this run. I didn't mention that. Just told her the buckler was "lightly used, deeply reliable." She didn't laugh. They never do. But she bought it, and that's what matters. For her, I mean. Protection. Very important.</p>
|
||||
<p>Kael came in to appraise something from the second floor. Tapped it on the counter. Listened. Heavy, good ring, slight harmonic on the follow-through. Tier four, easily. Named a fair price. He sold it back for the upgrade fund. Practical man, Kael. No sentiment about gear. I respect that. I also profit from it, which I respect slightly more.</p>
|
||||
<p>Dren bought three smoke bombs. Three. For one person. I asked if he was planning something specific. He said "the Breach." I said "ah." I wrapped them carefully. Something about the way he said it made me think he might actually need all three.</p>
|
||||
<p>The ledger gains another page. The pages at the front are still unreadable. I've stopped trying.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Torval</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry torval">
|
||||
<div class="journal-date">EPOCH VII · DAY 16</div>
|
||||
<div class="journal-text">
|
||||
<p>Inventory refresh day! Somehow the stock always matches what they'll need. I've stopped questioning it. New shipment includes tier three weapons appropriate for the second floor push and a few trinkets I haven't seen before. One of them hums. Not loudly. Not unpleasantly. But it hums. Priced it accordingly.</p>
|
||||
<p>The troll is dead. Good for morale, bad for my potion sales. When the big threat goes away, they get brave and stop buying healing supplies. I'll give it two days before Sable's back at my counter buying bandages.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Torval</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry torval">
|
||||
<div class="journal-date">EPOCH VII · DAY 15</div>
|
||||
<div class="journal-text">
|
||||
<p>The ground cracked. Stock fell off two shelves. Nothing broke — I pack carefully, because I know where I work. The Breach is open. New territory means new drops means new customers means new pages in the ledger. I love this job.</p>
|
||||
<p>Restocked the smoke bombs. I have a feeling.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Torval</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- WHISPER'S JOURNAL -->
|
||||
<div class="journal-feed" id="journal-whisper">
|
||||
|
||||
<div class="journal-entry whisper">
|
||||
<div class="journal-date">EPOCH VII · DAY 17</div>
|
||||
<div class="journal-text">
|
||||
<p>The second floor remembers being whole. It pushes back at night — not the monsters, the stone itself. Rooms seal shut like wounds closing. Two lost since dawn. Alpha holds because something older than the mine agreed it should. I don't know what. I heard it once, through the wall between the second and third depths. It was counting.</p>
|
||||
<p>Mira came to the corner today. She found the ward — I could see it on her, the residue of old mechanisms waking up. She asked about the eastern branch. I told her what I could. The words come in pieces. A door. A serpent that isn't a serpent. The sound of water where no water runs. She wrote it down. Good. I can't always say it twice.</p>
|
||||
<p>The Breach breathes. I can hear it from here. Two floors away and I can hear it like it's sitting next to me. Something large. Something that was here before the mines. Before the bar. Before the lanterns. Not before me. I was here first. I think. The memory is thin today.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Whisper</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry whisper">
|
||||
<div class="journal-date">EPOCH VII · DAY 16</div>
|
||||
<div class="journal-text">
|
||||
<p>The troll stopped. Its voice left the stone. A small silence where there used to be weight. Kael ended it but the dungeon let it end. Some things are allowed to die. Others aren't. The replacement is weaker — a shadow of a shadow. It serves the room but the room doesn't respect it.</p>
|
||||
<p>Three secrets on the eastern branch. I can feel them like teeth in a jaw. The first is behind something carved. The second requires a key that isn't a key. The third — I lose the third when I try to look at it directly. It moves. Or I move. One of us does.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Whisper</div>
|
||||
</div>
|
||||
|
||||
<div class="journal-entry whisper">
|
||||
<div class="journal-date">EPOCH VII · DAY 15</div>
|
||||
<div class="journal-text">
|
||||
<p>It opened. The thin place between. I told Grist three days ago. He listens, in his way. He told them. They listened, in theirs.</p>
|
||||
<p>What came through the crack is not new. It has been waiting underneath the underneath, patient as geology. The rooms between the floors are not rooms. They are the dungeon dreaming about itself. The secrets in there are different — not hidden, just not yet decided. They will become what they need to become when someone looks at them long enough.</p>
|
||||
<p>The lanterns flickered. They have never flickered. I watched them very carefully afterward to make sure they were still the same lanterns. They are. But they noticed too.</p>
|
||||
</div>
|
||||
<div class="journal-npc-sig">— Whisper</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="page-footer">
|
||||
<a href="#">The Last Ember</a> · meshMUD
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═══ EMBER PARTICLES (same as main page) ═══
|
||||
const canvas = document.getElementById('ember-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let embers = [];
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
class Ember {
|
||||
constructor() { this.reset(); }
|
||||
reset() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = canvas.height + 10;
|
||||
this.size = Math.random() * 2 + 0.5;
|
||||
this.speedY = -(Math.random() * 0.3 + 0.08);
|
||||
this.speedX = (Math.random() - 0.5) * 0.2;
|
||||
this.opacity = Math.random() * 0.4 + 0.15;
|
||||
this.decay = Math.random() * 0.0008 + 0.0003;
|
||||
this.wobble = Math.random() * Math.PI * 2;
|
||||
this.wobbleSpeed = Math.random() * 0.015 + 0.003;
|
||||
const t = Math.random();
|
||||
this.r = Math.floor(200 + t * 55);
|
||||
this.g = Math.floor(80 + t * 80);
|
||||
this.b = Math.floor(20 + t * 30);
|
||||
}
|
||||
update() {
|
||||
this.wobble += this.wobbleSpeed;
|
||||
this.x += this.speedX + Math.sin(this.wobble) * 0.12;
|
||||
this.y += this.speedY;
|
||||
this.opacity -= this.decay;
|
||||
if (this.opacity <= 0 || this.y < -20) this.reset();
|
||||
}
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${this.r},${this.g},${this.b},${this.opacity})`;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size * 2.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${this.r},${this.g},${this.b},${this.opacity * 0.12})`;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const e = new Ember();
|
||||
e.y = Math.random() * canvas.height;
|
||||
embers.push(e);
|
||||
}
|
||||
|
||||
function animateEmbers() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
embers.forEach(e => { e.update(); e.draw(); });
|
||||
requestAnimationFrame(animateEmbers);
|
||||
}
|
||||
animateEmbers();
|
||||
|
||||
// ═══ PAGE SWITCHING ═══
|
||||
function showPage(page) {
|
||||
document.querySelectorAll('.chronicle-section, .journal-section').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById('page-' + page).classList.add('active');
|
||||
document.querySelectorAll('.nav-link[data-page]').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.page === page);
|
||||
});
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// ═══ NPC JOURNAL TABS ═══
|
||||
function showJournal(npc) {
|
||||
document.querySelectorAll('.npc-tab').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.npc === npc);
|
||||
});
|
||||
document.querySelectorAll('.journal-feed').forEach(el => {
|
||||
el.classList.toggle('active', el.id === 'journal-' + npc);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
915
projects/mmud/last-ember-howto.html
Normal file
915
projects/mmud/last-ember-howto.html
Normal file
|
|
@ -0,0 +1,915 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Last Ember — How to Play</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ember: #e8713a;
|
||||
--ember-glow: #ff9d5c;
|
||||
--ember-deep: #c44e1a;
|
||||
--ash: #1a1714;
|
||||
--charcoal: #0d0b09;
|
||||
--smoke: #2a2520;
|
||||
--smoke-light: #3d3630;
|
||||
--parchment: #d4c4a8;
|
||||
--parchment-dark: #b8a88c;
|
||||
--parchment-faded: #a89878;
|
||||
--bone: #c8b898;
|
||||
--blood: #8b2020;
|
||||
--blood-bright: #cc3333;
|
||||
--gold: #c4a44a;
|
||||
--gold-dim: #8a7a3a;
|
||||
--frost: #7a9ab0;
|
||||
--poison: #5a8a4a;
|
||||
--text-bright: #e8dcc8;
|
||||
--text-dim: #9a8e78;
|
||||
--text-ghost: #5a5244;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--charcoal);
|
||||
color: var(--text-bright);
|
||||
font-family: 'Crimson Text', Georgia, serif;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#ember-canvas {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.page-wrap {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ═══ NAV ═══ */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid rgba(90,82,68,0.15);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--text-ghost);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.nav-link:hover { color: var(--parchment-faded); }
|
||||
.nav-link.active { color: var(--parchment); border-bottom-color: var(--ember); }
|
||||
|
||||
.nav-home {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 14px;
|
||||
color: var(--parchment-faded);
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.1em;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav-home:hover { color: var(--ember-glow); }
|
||||
|
||||
/* ═══ HEADER ═══ */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
padding: 48px 0 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: clamp(22px, 4vw, 32px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--parchment);
|
||||
text-shadow: 0 0 30px rgba(232,113,58,0.2);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
color: var(--text-ghost);
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin: 36px 0 28px;
|
||||
color: var(--text-ghost);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.25em;
|
||||
font-family: 'Cinzel', serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.divider::before, .divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--smoke-light), transparent);
|
||||
}
|
||||
|
||||
/* ═══ PROSE SECTIONS ═══ */
|
||||
.prose {
|
||||
font-size: 17px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.prose strong {
|
||||
color: var(--parchment);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose em.place {
|
||||
color: var(--ember-glow);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prose em.npc {
|
||||
color: var(--parchment-dark);
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose em.cmd {
|
||||
color: var(--gold);
|
||||
font-style: normal;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
background: rgba(196,164,74,0.08);
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(196,164,74,0.15);
|
||||
}
|
||||
|
||||
.prose em.item {
|
||||
color: var(--gold);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ═══ CALLOUT BOXES ═══ */
|
||||
.callout {
|
||||
padding: 20px 24px;
|
||||
margin: 24px 0;
|
||||
background: linear-gradient(135deg, rgba(26,23,20,0.95), rgba(42,37,32,0.7));
|
||||
border: 1px solid var(--smoke-light);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.callout::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.callout.ember::before { background: var(--ember); opacity: 0.5; }
|
||||
.callout.gold::before { background: var(--gold); opacity: 0.5; }
|
||||
.callout.frost::before { background: var(--frost); opacity: 0.5; }
|
||||
.callout.blood::before { background: var(--blood-bright); opacity: 0.5; }
|
||||
|
||||
.callout-label {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--text-ghost);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.callout .prose { margin-bottom: 0; }
|
||||
.callout .prose:not(:last-child) { margin-bottom: 12px; }
|
||||
|
||||
/* ═══ COMMAND REFERENCE ═══ */
|
||||
.cmd-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 4px 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.cmd-key {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--gold);
|
||||
padding: 3px 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cmd-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-dim);
|
||||
padding: 3px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cmd-unlock {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--text-ghost);
|
||||
background: rgba(90,82,68,0.15);
|
||||
padding: 1px 6px;
|
||||
border-radius: 1px;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ═══ CLASS CARDS ═══ */
|
||||
.class-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.class-cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.class-card {
|
||||
padding: 20px 16px;
|
||||
background: linear-gradient(180deg, rgba(26,23,20,0.95), rgba(13,11,9,0.95));
|
||||
border: 1px solid var(--smoke-light);
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.class-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.class-card.fighter::before { background: linear-gradient(90deg, transparent, var(--blood-bright), transparent); }
|
||||
.class-card.caster::before { background: linear-gradient(90deg, transparent, var(--poison), transparent); }
|
||||
.class-card.rogue::before { background: linear-gradient(90deg, transparent, var(--frost), transparent); }
|
||||
|
||||
.class-card-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.class-card-name {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--parchment);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.class-card-stat {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.class-card.fighter .class-card-stat { color: var(--blood-bright); }
|
||||
.class-card.caster .class-card-stat { color: var(--poison); }
|
||||
.class-card.rogue .class-card-stat { color: var(--frost); }
|
||||
|
||||
.class-card-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ═══ FLOW DIAGRAM ═══ */
|
||||
.flow-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.flow-step-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--smoke-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-ghost);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-step:not(:last-child) .flow-step-num::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
height: calc(100% + 16px);
|
||||
background: var(--smoke-light);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.flow-step-content {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.flow-step-label {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--parchment-faded);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.flow-step-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.flow-step-text code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--gold);
|
||||
background: rgba(196,164,74,0.08);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(196,164,74,0.12);
|
||||
}
|
||||
|
||||
/* ═══ MESSAGE EXAMPLE ═══ */
|
||||
.msg-example {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 2;
|
||||
padding: 16px 20px;
|
||||
background: rgba(13,11,9,0.9);
|
||||
border: 1px solid var(--smoke-light);
|
||||
border-radius: 2px;
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.msg-server { color: var(--parchment-faded); }
|
||||
.msg-player { color: var(--frost); }
|
||||
.msg-system { color: var(--text-ghost); font-style: italic; }
|
||||
.msg-broadcast { color: var(--ember-glow); }
|
||||
.msg-gold { color: var(--gold); }
|
||||
|
||||
/* ═══ TIP STRIP ═══ */
|
||||
.tip-strip {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.tip-strip { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.tip-card {
|
||||
padding: 16px 18px;
|
||||
background: linear-gradient(135deg, rgba(26,23,20,0.9), rgba(42,37,32,0.5));
|
||||
border: 1px solid rgba(90,82,68,0.15);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tip-card-label {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--text-ghost);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tip-card-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ═══ NPC GUIDE ═══ */
|
||||
.npc-guide {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid rgba(90,82,68,0.1);
|
||||
}
|
||||
|
||||
.npc-guide:last-child { border-bottom: none; }
|
||||
|
||||
.npc-guide-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.npc-guide-name {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--parchment);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.npc-guide-role {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 9px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.npc-guide-desc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ═══ FOOTER ═══ */
|
||||
.page-footer {
|
||||
text-align: center;
|
||||
padding: 36px 0 48px;
|
||||
border-top: 1px solid rgba(90,82,68,0.15);
|
||||
margin-top: 20px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-ghost);
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.page-footer a {
|
||||
color: var(--text-ghost);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.page-footer a:hover { color: var(--ember); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="ember-canvas"></canvas>
|
||||
|
||||
<div class="page-wrap">
|
||||
|
||||
<!-- NAV -->
|
||||
<nav class="nav-bar">
|
||||
<a class="nav-home" href="#">The Last Ember</a>
|
||||
<span style="color:var(--smoke-light)">·</span>
|
||||
<a class="nav-link" href="#">Board</a>
|
||||
<a class="nav-link" href="#">Chronicle</a>
|
||||
<a class="nav-link" href="#">Journals</a>
|
||||
<a class="nav-link active">How to Play</a>
|
||||
</nav>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">How to Play</h1>
|
||||
<p class="page-subtitle">A text adventure played over radio. Five minutes a day. Thirty days an epoch. No internet required.</p>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- WHAT IS THIS -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">What is meshMUD</div>
|
||||
|
||||
<p class="prose">meshMUD is a multiplayer text adventure that runs over <strong>Meshtastic</strong> — a long-range radio mesh network. There is no internet connection, no app store, no account creation. You play by sending short text messages from your Meshtastic node. The game responds. Everything happens in 150 characters or less.</p>
|
||||
|
||||
<p class="prose">It plays like the BBS door games of the early '90s — <em class="place">Legend of the Red Dragon</em>, <em class="place">TradeWars 2002</em> — adapted for radio. Short daily sessions. Asynchronous multiplayer. A shared world where you see evidence of other players without needing to be online at the same time. A dungeon that resets every 30 days.</p>
|
||||
|
||||
<p class="prose">You don't need to be a gamer. You don't need to be fast. You need a Meshtastic radio and five minutes.</p>
|
||||
|
||||
<div class="callout ember">
|
||||
<div class="callout-label">The basics</div>
|
||||
<p class="prose">You wake up in a tavern called <em class="place">The Last Ember</em>. Below it is a dungeon that changes every 30 days. You explore it, fight monsters, find secrets, and help other players push deeper — all by typing short commands over your radio. When the 30 days end, the dungeon resets. Your character persists. The stories stay.</p>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- GETTING STARTED -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">Getting Started</div>
|
||||
|
||||
<p class="prose">If your mesh network is running meshMUD, the game server listens for direct messages from any node. Send it a DM and it responds. That's it.</p>
|
||||
|
||||
<div class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">1</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Send a DM to the game node</div>
|
||||
<div class="flow-step-text">Find the meshMUD node on your Meshtastic client and send any message. The server responds with a welcome and asks you to pick a class.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">2</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Pick your class</div>
|
||||
<div class="flow-step-text">One letter. <code>F</code> for Fighter, <code>C</code> for Caster, <code>R</code> for Rogue. That's your only creation choice — everything else emerges through play.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">3</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">You're in</div>
|
||||
<div class="flow-step-text">The server drops you in <em class="place">The Last Ember</em> with starting gear and a handful of gold. Type <code>L</code> to look around. Type <code>H</code> for help. You're playing.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg-example">
|
||||
<span class="msg-player">You → meshMUD:</span> <span class="msg-gold">hello</span><br>
|
||||
<span class="msg-server">meshMUD:</span> Welcome to The Last Ember. Pick a class: (F)ighter (C)aster (R)ogue<br>
|
||||
<span class="msg-player">You:</span> <span class="msg-gold">F</span><br>
|
||||
<span class="msg-server">meshMUD:</span> Kael the Fighter. POW:5 DEF:4 SPD:3 HP:30. You stand in the tavern. Type L.<br>
|
||||
<span class="msg-player">You:</span> <span class="msg-gold">L</span><br>
|
||||
<span class="msg-server">meshMUD:</span> The Last Ember. Lanterns burn without oil. Grist polishes a glass. Exits: dungeon.<br>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- CLASSES -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">The Three Classes</div>
|
||||
|
||||
<p class="prose">Three stats govern everything: <strong>POW</strong> (offense), <strong>DEF</strong> (survivability), and <strong>SPD</strong> (evasion, initiative, spellcasting). Each class leans into one. You earn 2 stat points per level to allocate however you want — that's where your build takes shape.</p>
|
||||
|
||||
<div class="class-cards">
|
||||
<div class="class-card fighter">
|
||||
<div class="class-card-icon">⚔</div>
|
||||
<div class="class-card-name">Fighter</div>
|
||||
<div class="class-card-stat">POW-FOCUSED</div>
|
||||
<div class="class-card-desc">High HP. Hits hard. Takes hits. Abilities like Strike, Bash, Rally, Cleave. Passive damage reduction. The front line.</div>
|
||||
</div>
|
||||
<div class="class-card caster">
|
||||
<div class="class-card-icon">✦</div>
|
||||
<div class="class-card-name">Caster</div>
|
||||
<div class="class-card-stat">SPD-FOCUSED</div>
|
||||
<div class="class-card-desc">Low HP. Spells scale on SPD. Bolt, Ward, Blast, Drain. Passive: see enemy stats. Knowledge is power. Fragile is the cost.</div>
|
||||
</div>
|
||||
<div class="class-card rogue">
|
||||
<div class="class-card-icon">◈</div>
|
||||
<div class="class-card-name">Rogue</div>
|
||||
<div class="class-card-stat">SPD / BALANCED</div>
|
||||
<div class="class-card-desc">Stealth and utility. Stab, Dodge, Ambush, Steal. Passive evasion chance. Thrives in the spaces between fights.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- A TYPICAL DAY -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">A Typical Day</div>
|
||||
|
||||
<p class="prose">A session takes <strong>five to fifteen minutes</strong>. You get 12 dungeon actions per day — enough to explore a few rooms, fight a few monsters, and make progress without burning out. Town actions are always free.</p>
|
||||
|
||||
<div class="flow-steps">
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">1</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Visit Grist</div>
|
||||
<div class="flow-step-text">The barkeep tells you what happened while you were gone. Who died, what fell, what the front line looks like. Always free. This is how the world stays alive between sessions.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">2</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Check the bounty board</div>
|
||||
<div class="flow-step-text">Shared objectives the whole server works toward. A monster with a communal HP pool. An exploration target. You chip away at it — so does everyone else.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">3</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Gear up</div>
|
||||
<div class="flow-step-text">Buy supplies from Torval, heal up with Maren if you need it, spend a bard token at the bar for a hint or buff. All free actions.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">4</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Enter the dungeon</div>
|
||||
<div class="flow-step-text">Move room to room, fight what you find, look for secrets, leave messages for other players. Each move or fight costs an action. Twelve per day — spend them wisely.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-step-num">5</div>
|
||||
<div class="flow-step-content">
|
||||
<div class="flow-step-label">Return to town</div>
|
||||
<div class="flow-step-text">Bank your gold before the dungeon takes it. Tomorrow the rooms may have changed, the bounty may be weaker, and someone may have left you a message you need to read.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- THE TOWN -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">The Last Ember — Your Town</div>
|
||||
|
||||
<p class="prose">The tavern is the one room that never changes. Epochs wipe the dungeon, reshuffle everything, reshape the world — but <em class="place">The Last Ember</em> stays. Same bar. Same people. Same lanterns that burn without oil and nobody questions anymore.</p>
|
||||
|
||||
<p class="prose">Four people live here. They remember you across every wipe.</p>
|
||||
|
||||
<div class="npc-guide">
|
||||
<span class="npc-guide-icon">🍺</span>
|
||||
<div>
|
||||
<div class="npc-guide-name">Grist</div>
|
||||
<div class="npc-guide-role">Barkeep</div>
|
||||
<div class="npc-guide-desc">Knows everything that happens in the dungeon because everyone tells him and he never forgets. Visit him first every session — he'll catch you up on what you missed. He also runs the bounty board, handles the epoch vote, and trades bard tokens for hints, buffs, and secrets. He doesn't trade because he's kind. He trades because he collects.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="npc-guide">
|
||||
<span class="npc-guide-icon">🩸</span>
|
||||
<div>
|
||||
<div class="npc-guide-name">Maren</div>
|
||||
<div class="npc-guide-role">Healer</div>
|
||||
<div class="npc-guide-desc">Used to be an adventurer. Went deeper than anyone. Came back done. Heals with her hands, not magic, and it hurts. She charges gold because free healing breeds carelessness. She's the reason you survive long enough to learn from your mistakes.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="npc-guide">
|
||||
<span class="npc-guide-icon">⚖</span>
|
||||
<div>
|
||||
<div class="npc-guide-name">Torval</div>
|
||||
<div class="npc-guide-role">Merchant</div>
|
||||
<div class="npc-guide-desc">Buys and sells gear. Appraises items by weight and sound. His inventory somehow matches what's in the dungeon each epoch. Nobody asks how. His prices are fair and his stock is real, which is more than you can say for most people in a town built around a hole full of monsters.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="npc-guide">
|
||||
<span class="npc-guide-icon">👁</span>
|
||||
<div>
|
||||
<div class="npc-guide-name">Whisper</div>
|
||||
<div class="npc-guide-role">Sage</div>
|
||||
<div class="npc-guide-desc">Sits in the same corner. Knows things about the dungeon that change each epoch — lore, connections, what the symbols mean. Speaks in fragments because that's how the information comes to her. Pay attention to her exact words. Players who dismiss her as flavor text miss half the game.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- THE DUNGEON -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">The Dungeon</div>
|
||||
|
||||
<p class="prose">Four floors. Each one deeper, harder, and stranger than the last. Monsters get meaner. Secrets get subtler. The rooms change every epoch but the structure holds — floor one is where you learn, floor four is where legends are made.</p>
|
||||
|
||||
<p class="prose">You carry three pieces of gear: a <strong>weapon</strong>, <strong>armor</strong>, and a <strong>trinket</strong>. The trinket is the wildcard — it might grant a passive ability, boost an unexpected stat, or do something no other slot can. Six tiers of gear across the dungeon. The best stuff doesn't come from shops.</p>
|
||||
|
||||
<div class="callout frost">
|
||||
<div class="callout-label">Death</div>
|
||||
<p class="prose">Death costs you all the gold you're carrying. Not your gear. Not your level. Just your gold. The question is always the same: do you bank it before you go in, or carry it and risk losing everything? The dungeon teaches you the answer. Usually the hard way.</p>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- MULTIPLAYER -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">Playing Together</div>
|
||||
|
||||
<p class="prose">meshMUD is <strong>asynchronous multiplayer</strong>. You don't need to be online at the same time as anyone else. You see other players through what they leave behind — messages scratched on dungeon walls, bounty progress that wasn't there yesterday, broadcasts announcing who found what and who fell where.</p>
|
||||
|
||||
<div class="tip-strip">
|
||||
<div class="tip-card">
|
||||
<div class="tip-card-label">Bounties</div>
|
||||
<div class="tip-card-text">Shared objectives with communal HP pools. You chip away at a target over days. Everyone who contributes shares the reward when it falls.</div>
|
||||
</div>
|
||||
<div class="tip-card">
|
||||
<div class="tip-card-label">Messages</div>
|
||||
<div class="tip-card-text">Leave 15-character notes in dungeon rooms for others to find. Warnings, tips, coordinates. Dark Souls soapstone, over LoRa.</div>
|
||||
</div>
|
||||
<div class="tip-card">
|
||||
<div class="tip-card-label">Mail</div>
|
||||
<div class="tip-card-text">Send direct messages to specific players through the barkeep. Coordinate strategy, share secrets, warn someone about what's ahead.</div>
|
||||
</div>
|
||||
<div class="tip-card">
|
||||
<div class="tip-card-label">Broadcasts</div>
|
||||
<div class="tip-card-text">Major events announce to the whole mesh. Boss kills, rare finds, deaths, front line changes. The world narrates itself.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="prose">There is no PvP. All competition runs through leaderboards, bounty races, and endgame objectives. On a small mesh network where everyone knows each other, cooperation is the game.</p>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- EPOCHS -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">The 30-Day Epoch</div>
|
||||
|
||||
<p class="prose">Every 30 days, the dungeon resets. New rooms, new monsters, new secrets, new narrative. Your character keeps their name and their history, but gear and gold start fresh. Each epoch has an <strong>endgame mode</strong> — a shared objective the whole server works toward. On day 30, players vote on the next epoch's mode.</p>
|
||||
|
||||
<div class="callout gold">
|
||||
<div class="callout-label">Three endgame modes</div>
|
||||
<p class="prose"><strong>Hold the Line</strong> — the dungeon regenerates rooms. Push the front line deeper, establish checkpoints that lock in progress. The whole server descends together.</p>
|
||||
<p class="prose"><strong>Raid Boss</strong> — a massive enemy with thousands of HP squats on the lowest floor. The server chips away over days. Discover its weaknesses. Coordinate the kill.</p>
|
||||
<p class="prose"><strong>Retrieve & Escape</strong> — an artifact on floor four. Grab it, carry it to the surface. Something unkillable chases the carrier. Other players clear the path, block the pursuer, relay the objective hand-to-hand.</p>
|
||||
</div>
|
||||
|
||||
<p class="prose">On day 15, the <strong>Breach</strong> opens — a surprise mini-zone between floors two and three with its own challenge, its own loot, and its own secrets. You don't know what's inside until it opens.</p>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- SECRETS -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">Secrets & Discovery</div>
|
||||
|
||||
<p class="prose">Twenty secrets hide in the dungeon each epoch. Some are behind walls that need a strong arm to break. Some are puzzles spread across multiple rooms. Some are hidden in things Whisper says that nobody thinks to write down. Finding them isn't required — but every secret you uncover gives a real mechanical advantage, and some of them benefit the entire server.</p>
|
||||
|
||||
<p class="prose"><strong>Read the room descriptions carefully.</strong> The dungeon tells you where its secrets are. It just doesn't tell you plainly.</p>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- COMMANDS -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">Quick Command Reference</div>
|
||||
|
||||
<p class="prose">Every command fits in a short message. Most have single-letter shortcuts. New commands unlock as you level up — the game teaches you as you go.</p>
|
||||
|
||||
<div class="callout ember">
|
||||
<div class="callout-label">Movement & Awareness</div>
|
||||
<div class="cmd-grid">
|
||||
<span class="cmd-key">n s e w</span><span class="cmd-desc">Move north, south, east, west</span>
|
||||
<span class="cmd-key">l</span><span class="cmd-desc">Look — describe current room, show exits</span>
|
||||
<span class="cmd-key">x [thing]</span><span class="cmd-desc">Examine something in the room <span class="cmd-unlock">LV5</span></span>
|
||||
<span class="cmd-key">who</span><span class="cmd-desc">List active players <span class="cmd-unlock">LV3</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout blood">
|
||||
<div class="callout-label">Combat</div>
|
||||
<div class="cmd-grid">
|
||||
<span class="cmd-key">f</span><span class="cmd-desc">Fight — engage the monster in this room</span>
|
||||
<span class="cmd-key">a</span><span class="cmd-desc">Attack — basic melee/spell attack</span>
|
||||
<span class="cmd-key">flee</span><span class="cmd-desc">Attempt to escape combat (SPD-based chance)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout gold">
|
||||
<div class="callout-label">Town & NPCs</div>
|
||||
<div class="cmd-grid">
|
||||
<span class="cmd-key">barkeep</span><span class="cmd-desc">Talk to Grist — recap, tokens, bounties <span class="cmd-unlock">LV3</span></span>
|
||||
<span class="cmd-key">heal</span><span class="cmd-desc">Visit Maren — restore HP for gold <span class="cmd-unlock">LV3</span></span>
|
||||
<span class="cmd-key">shop</span><span class="cmd-desc">Browse Torval's inventory <span class="cmd-unlock">LV3</span></span>
|
||||
<span class="cmd-key">bank</span><span class="cmd-desc">Deposit gold safely <span class="cmd-unlock">LV3</span></span>
|
||||
<span class="cmd-key">board</span><span class="cmd-desc">View active bounties <span class="cmd-unlock">LV3</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout frost">
|
||||
<div class="callout-label">Inventory & Character</div>
|
||||
<div class="cmd-grid">
|
||||
<span class="cmd-key">i</span><span class="cmd-desc">Inventory — show gear and backpack <span class="cmd-unlock">LV2</span></span>
|
||||
<span class="cmd-key">st</span><span class="cmd-desc">Stats — show POW, DEF, SPD, HP, gold, level</span>
|
||||
<span class="cmd-key">equip [item]</span><span class="cmd-desc">Equip an item from your backpack <span class="cmd-unlock">LV2</span></span>
|
||||
<span class="cmd-key">use [item]</span><span class="cmd-desc">Use a consumable <span class="cmd-unlock">LV2</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout ember">
|
||||
<div class="callout-label">Social</div>
|
||||
<div class="cmd-grid">
|
||||
<span class="cmd-key">msg [text]</span><span class="cmd-desc">Leave a 15-char message in this room <span class="cmd-unlock">LV5</span></span>
|
||||
<span class="cmd-key">read</span><span class="cmd-desc">Read messages in this room</span>
|
||||
<span class="cmd-key">rate</span><span class="cmd-desc">Mark a message as helpful</span>
|
||||
<span class="cmd-key">mail</span><span class="cmd-desc">Check your inbox <span class="cmd-unlock">LV3</span></span>
|
||||
<span class="cmd-key">mail [who] [text]</span><span class="cmd-desc">Send mail to a player <span class="cmd-unlock">LV3</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout gold">
|
||||
<div class="callout-label">Meta</div>
|
||||
<div class="cmd-grid">
|
||||
<span class="cmd-key">h</span><span class="cmd-desc">Help — list all available commands</span>
|
||||
<span class="cmd-key">h [cmd]</span><span class="cmd-desc">Help on a specific command</span>
|
||||
<span class="cmd-key">vote</span><span class="cmd-desc">Vote for next epoch's mode (day 30 only) <span class="cmd-unlock">LV1</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════════════════════════════ -->
|
||||
<!-- TIPS -->
|
||||
<!-- ════════════════════════════ -->
|
||||
<div class="divider">Grist's Advice for the New Arrival</div>
|
||||
|
||||
<div class="callout ember">
|
||||
<div class="callout-label">Things nobody tells you</div>
|
||||
<p class="prose"><strong>Bank before you descend.</strong> Death takes everything you're carrying. Not your gear, not your level — just your gold. The bank is free. Use it.</p>
|
||||
<p class="prose"><strong>Visit Grist every session.</strong> His recap costs nothing and tells you everything you missed. The bounty board is there too. Five seconds of reading saves you from walking into something that killed Sable yesterday.</p>
|
||||
<p class="prose"><strong>Leave messages.</strong> A 15-character note in a dangerous room saves someone's life tomorrow. This is a small network. Help each other.</p>
|
||||
<p class="prose"><strong>Read room descriptions.</strong> The dungeon hides things in plain sight. If the text mentions scratches on a wall, there's a reason. If Whisper mumbles about the eastern branch, there's a reason. The game rewards attention.</p>
|
||||
<p class="prose"><strong>You don't have to fight everything.</strong> Twelve actions is enough for a good day, not enough for a reckless one. Know when to push and when to walk away. The dungeon will be here tomorrow.</p>
|
||||
<p class="prose"><strong>Bard tokens accrue whether you log in or not.</strong> One per day, cap at five. Spend them at the barkeep for things gold can't buy — hints, buffs, intel. A patient player who saves five tokens gets information that changes everything.</p>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="page-footer">
|
||||
<a href="#">The Last Ember</a> · meshMUD
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═══ EMBER PARTICLES ═══
|
||||
const canvas = document.getElementById('ember-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let embers = [];
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
class Ember {
|
||||
constructor() { this.reset(); }
|
||||
reset() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = canvas.height + 10;
|
||||
this.size = Math.random() * 2 + 0.5;
|
||||
this.speedY = -(Math.random() * 0.3 + 0.08);
|
||||
this.speedX = (Math.random() - 0.5) * 0.2;
|
||||
this.opacity = Math.random() * 0.35 + 0.1;
|
||||
this.decay = Math.random() * 0.0007 + 0.0003;
|
||||
this.wobble = Math.random() * Math.PI * 2;
|
||||
this.wobbleSpeed = Math.random() * 0.015 + 0.003;
|
||||
const t = Math.random();
|
||||
this.r = Math.floor(200 + t * 55);
|
||||
this.g = Math.floor(80 + t * 80);
|
||||
this.b = Math.floor(20 + t * 30);
|
||||
}
|
||||
update() {
|
||||
this.wobble += this.wobbleSpeed;
|
||||
this.x += this.speedX + Math.sin(this.wobble) * 0.1;
|
||||
this.y += this.speedY;
|
||||
this.opacity -= this.decay;
|
||||
if (this.opacity <= 0 || this.y < -20) this.reset();
|
||||
}
|
||||
draw() {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${this.r},${this.g},${this.b},${this.opacity})`;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size * 2.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${this.r},${this.g},${this.b},${this.opacity * 0.1})`;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const e = new Ember();
|
||||
e.y = Math.random() * canvas.height;
|
||||
embers.push(e);
|
||||
}
|
||||
|
||||
function animateEmbers() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
embers.forEach(e => { e.update(); e.draw(); });
|
||||
requestAnimationFrame(animateEmbers);
|
||||
}
|
||||
animateEmbers();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1442
projects/mmud/last-ember.html
Normal file
1442
projects/mmud/last-ember.html
Normal file
File diff suppressed because it is too large
Load diff
215
projects/mmud/mmud-phase5-prompt.md
Normal file
215
projects/mmud/mmud-phase5-prompt.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Task: MMUD Phase 5 — Endgame Modes
|
||||
|
||||
## Before Writing Any Code
|
||||
|
||||
Re-read these sections of `/home/zvx/projects/mmud/docs/planned.md`:
|
||||
- Endgame: Three Rotating Modes (all of it — mode selection, R&E with Pursuer + support roles, Raid Boss with mechanic tables + phases, Hold the Line with regen + checkpoints + floor bosses)
|
||||
- The Darkcragg Depths (dungeon name context)
|
||||
- Floor Boss Mechanic Tables (all 4 floor tables)
|
||||
- Bounties During Hold the Line
|
||||
|
||||
Also re-read `config.py` for: HTL_REGEN_ROOMS_PER_DAY, FLOOR_BOSS_MECHANICS, WARDEN_HP/REGEN, RAID_BOSS_HP_PER_PLAYER/CAP/REGEN/MECHANIC_TABLE/PHASES, PURSUER_ADVANCE_RATE/SPAWN_DISTANCE/RELAY_RESET_DISTANCE, WARD/LURE constants, ENDGAME_MODES.
|
||||
|
||||
Phase 4 already generates floor bosses, raid boss pre-config, and bounties. This phase wires the runtime game logic for all three modes.
|
||||
|
||||
## Phase 5 Deliverables
|
||||
|
||||
All three endgame modes are playable. The epoch vote selects the mode. Each mode has its own win condition, progression mechanics, and broadcasts.
|
||||
|
||||
### 1. Epoch Vote System
|
||||
|
||||
`src/systems/vote.py` (new file)
|
||||
|
||||
- Day 30 trigger (already in daytick.py — wire it to the vote system)
|
||||
- `vote <mode>` command at barkeep — free action. Modes: `retrieve`, `raid`, `hold` (or numbers 1/2/3).
|
||||
- Votes are public — broadcast on cast: "🗳 {name} voted {mode}."
|
||||
- Votes can be changed up until epoch ends. UPSERT into epoch_votes table.
|
||||
- Tally on epoch end: most votes wins. Tiebreak: longest-unplayed mode. No quorum — 1 vote decides if only 1 player votes. Zero votes → longest-unplayed auto-selected.
|
||||
- `epoch_generate.py` already accepts endgame mode — wire the vote result into the next epoch's generation.
|
||||
|
||||
### 2. Hold the Line — Runtime Logic
|
||||
|
||||
`src/systems/endgame_htl.py` (new file)
|
||||
|
||||
**Room clearing:**
|
||||
- All dungeon rooms start hostile (`htl_cleared = 0`)
|
||||
- Killing all monsters in a room sets `htl_cleared = 1` with timestamp
|
||||
- Regen ticks (already in daytick.py) revert rooms: pick N random cleared non-checkpoint rooms per floor per day, set `htl_cleared = 0`, respawn monsters. Spread ticks across the day (floor 2 at 5/day = 1 room every ~5 hours).
|
||||
- Cleared rooms are safe — no random encounters. Reverted rooms respawn monsters.
|
||||
|
||||
**Checkpoints:**
|
||||
- Checkpoint rooms are defined during worldgen (is_checkpoint = 1 in rooms table). 3 per floor (hub, midpoint, stairway), 1 on floor 4 (Warden).
|
||||
- Establishment condition: all rooms in the checkpoint cluster (checkpoint room + all adjacent rooms) must be cleared within one regen window. Check on each room clear — if the cluster is complete, spawn the floor boss.
|
||||
- Once the floor boss dies, checkpoint locks permanently. `htl_checkpoints.established = 1`. Regen can never revert rooms behind an established checkpoint.
|
||||
- Final checkpoint on each floor (stairway) unlocks the next floor for all players.
|
||||
|
||||
**Floor bosses:**
|
||||
- Already generated with rolled mechanics (from Phase 4 bossgen).
|
||||
- Floor boss spawns in the checkpoint room when cluster is cleared. Uses the same shared HP pool / chip-and-run combat as bounties.
|
||||
- Boss mechanic implementation — each mechanic modifies combat behavior:
|
||||
- `armored` — damage halved until boss below 50% HP
|
||||
- `enraged` — boss deals double damage below 50% HP, takes 25% more
|
||||
- `regenerator` — boss heals 10% HP between sessions (check on engagement, apply since last fight)
|
||||
- `stalwart` — first flee attempt per engagement always fails
|
||||
- `warded` — boss has +50% DEF until a discovery secret on the same floor is found (check secret_progress)
|
||||
- `phasing` — boss is immune to damage on even-numbered epoch days (check epoch.day_number)
|
||||
- `draining` — boss steals 10% of damage dealt as HP from the attacker
|
||||
- `splitting` — at 50% HP, boss splits into two half-HP monsters in adjacent rooms. Both must die.
|
||||
- `rotating_resistance` — immune to the highest stat used by the last player who fought it. Track in DB.
|
||||
- `retaliator` — reflects 20% of damage back to attacker
|
||||
- `summoner` — spawns 1 add at start of each engagement. Add must die before boss can be damaged.
|
||||
- `cursed` — player who dealt most damage last session gets -2 to a random stat next login
|
||||
|
||||
- Floor 4 Warden: shared HP pool 300-500, regen at 3%/8h, rolls 2 mechanics from the full table.
|
||||
- Warden kill = epoch win. Broadcast: "🏆 The Warden has fallen! The Darkcragg Depths are conquered!"
|
||||
|
||||
**Broadcasts from DCRG:**
|
||||
- "🏰 Floor {n} Checkpoint {name} established!"
|
||||
- "⚠ Floor {n} lost {x} rooms. Frontline at {room}."
|
||||
- "🏰 Floor {n} unlocked! The descent continues."
|
||||
- "💀 Floor {n} frontline collapsed to Checkpoint {name}. Rally!"
|
||||
- Floor boss spawned, floor boss killed, Warden progress.
|
||||
|
||||
**Barkeep integration:**
|
||||
- Grist's recap includes HtL status: floors cleared percentage, checkpoint status, frontline position.
|
||||
|
||||
### 3. Raid Boss — Runtime Logic
|
||||
|
||||
`src/systems/endgame_raid.py` (new file)
|
||||
|
||||
**Activation:**
|
||||
- On epoch start (if mode is raid_boss), calculate active player count (anyone who entered the dungeon in first 3 days).
|
||||
- Set raid_boss.hp = 300 × active players, cap 6000. Set raid_boss.hp_max to same value.
|
||||
- Place boss in a room on floor 3-4 (from pre-generated config in Phase 4).
|
||||
|
||||
**Combat:**
|
||||
- Same chip-and-run as bounties — shared HP pool, engage/damage/flee.
|
||||
- Regen: 3%/8h (lazy-evaluated like bounties — calculate on engagement).
|
||||
- Track contributions in raid_boss_contributors.
|
||||
|
||||
**Mechanic implementation (2-3 rolled):**
|
||||
- `windup_strike` — every 3rd combat round, next round deals triple damage unless player uses `defend` or `dodge` action. Add `defend` and `dodge` as combat commands (cost 1 action, negate the windup).
|
||||
- `flat_damage_boost` — boss damage multiplied by 1.5x
|
||||
- `retribution` — at 75%/50%/25% HP thresholds, burst damage (2x normal) to the player who pushed it past the threshold
|
||||
- `aura_damage` — player takes 5% max HP unavoidable damage each combat round regardless of DEF
|
||||
- `extra_regen` — regen rate becomes 5%/8h instead of 3%/8h
|
||||
- `armor_phase` — boss takes half damage until: a discovery secret on the floor is found, OR 5+ unique players have contributed damage
|
||||
- `boss_flees` — at 75%/50%/25% HP, boss relocates to random room on same floor. Broadcast from DCRG: "🐉 The {boss} has fled to somewhere on Floor {n}!" Players must find it.
|
||||
- `regen_burst` — once per day at a random hour, boss heals 15% max HP in one tick. Trackable through observation.
|
||||
- `no_escape` — below 25% HP, all flee attempts fail. Fight to the death.
|
||||
- `summoner` — 1-2 adds spawn per engagement, must be killed before boss takes damage
|
||||
- `lockout` — after engaging, player can't reengage for 24 hours. Store lockout_until in raid_boss_contributors.
|
||||
- `enrage_timer` — after 5 combat rounds in a single engagement, boss damage doubles each subsequent round
|
||||
|
||||
**Phases:**
|
||||
- Phase transitions at 66% and 33% HP (`RAID_BOSS_PHASES`).
|
||||
- At each threshold, rolled mechanics intensify. Implementation: each mechanic has a `phase_modifier(phase_num)` that scales its effect. E.g., summoner spawns 1 add in phase 1, 2 in phase 2, 3 in phase 3. Windup goes from every 3rd round to every 2nd.
|
||||
- Phase transition broadcasts from DCRG: "🐉 The {boss} enters its second phase!"
|
||||
|
||||
**Win condition:** Boss HP reaches 0. All contributors rewarded. Killing blow gets bonus. Broadcast: "🏆 The {boss} has been slain! Victory belongs to the Darkcragg!"
|
||||
|
||||
### 4. Retrieve and Escape — Runtime Logic
|
||||
|
||||
`src/systems/endgame_rne.py` (new file)
|
||||
|
||||
**Setup:**
|
||||
- Guardian monster placed on floor 4 during epoch generation (add to bossgen if not already there — a strong but non-boss monster guarding the objective).
|
||||
- `escape_run` table tracks run state.
|
||||
|
||||
**Claiming the objective:**
|
||||
- Player defeats the guardian on floor 4 → objective claimed. `escape_run.active = 1`, carrier set, pursuer spawns.
|
||||
- Broadcast from DCRG: "👑 {name} claimed the {objective}! The Pursuer stirs."
|
||||
- Monster spawn rates double on all floors (multiply spawn chance by `ESCAPE_SPAWN_RATE_MULTIPLIER`).
|
||||
|
||||
**Pursuer:**
|
||||
- Tracks carrier. Advances 1 room toward carrier every 2 carrier actions (`PURSUER_ADVANCE_RATE`).
|
||||
- Spawns 3 rooms behind carrier (`PURSUER_SPAWN_DISTANCE`).
|
||||
- Track pursuer position in `escape_run.pursuer_room_id`. Track fractional ticks in `pursuer_ticks`.
|
||||
- On every carrier action: increment pursuer_ticks. When pursuer_ticks >= PURSUER_ADVANCE_RATE, advance pursuer 1 room toward carrier (pathfind shortest route), reset ticks.
|
||||
- When pursuer enters carrier's room: forced combat. Pursuer is invulnerable (takes no damage). Hits hard. Carrier can only flee. Flee uses normal SPD check. Success = carrier moves 1 room. Failure = take damage + try again next action.
|
||||
|
||||
**Carrier death and relay:**
|
||||
- Carrier dies → objective drops at death room. Broadcast from DCRG: "💀 The carrier has fallen on Floor {n}. The {objective} lies unguarded."
|
||||
- `escape_run.objective_dropped = 1`, `dropped_room_id` set.
|
||||
- Any player can `pickup` the objective in that room.
|
||||
- On pickup: pursuer resets to 5 rooms behind new carrier (`PURSUER_RELAY_RESET_DISTANCE`). Broadcast: "👑 {name} picks up the {objective}! The Pursuer resets."
|
||||
- Death penalty still applies to the dead carrier (gold loss, respawn in town).
|
||||
|
||||
**Three support roles:**
|
||||
|
||||
**Blockers:**
|
||||
- Non-carrier in a room between pursuer and carrier. When pursuer reaches a blocker's room, forced combat with the blocker instead of advancing.
|
||||
- Blocker can't kill pursuer (invulnerable). Each round blocker survives = 1 round pursuer isn't moving.
|
||||
- Blocker can flee (normal SPD check). Blocker can die.
|
||||
- Broadcast: "🛡 {name} is blocking the Pursuer on Floor {n}!" and "💀 {name} fell holding the line. The Pursuer advances."
|
||||
- Implementation: on pursuer advance, check if any player is in the target room. If yes, pursuer enters combat with them instead of continuing.
|
||||
|
||||
**Warders:**
|
||||
- `ward` command in a cleared dungeon room (1 extra action after clearing = `WARD_ACTION_COST`). Sets `rooms.ward_active = 1`.
|
||||
- Warded room slows pursuer — takes 2 advance ticks to pass through instead of 1 (`WARD_PURSUER_SLOWDOWN`).
|
||||
- Ward breaks after one use (reset to 0 when pursuer passes through).
|
||||
- No broadcast on warding — silent preparation.
|
||||
|
||||
**Lures:**
|
||||
- `lure` command when on same floor as pursuer. Costs 2 actions (`LURE_ACTION_COST`).
|
||||
- Pursuer diverts toward lure player for 3 ticks (`LURE_DIVERT_TICKS`), then snaps back to carrier tracking. Total delay ~6 ticks (`LURE_TOTAL_DELAY_TICKS`) including backtrack.
|
||||
- Broadcast: "🎯 {name} lured the Pursuer into {room}! It diverts."
|
||||
- After divert expires: "👁 The Pursuer has reacquired the carrier."
|
||||
|
||||
**Pursuer distance broadcasts (from DCRG):**
|
||||
- "👁 The Pursuer is {n} rooms behind the carrier." (every 5 carrier actions)
|
||||
- "👁 The Pursuer is 3 rooms behind. It's closing."
|
||||
- "⚠ The Pursuer has reached the carrier!"
|
||||
|
||||
**Win condition:** Any player delivers objective to town (The Last Ember). Broadcast: "🏆 The {objective} has reached the surface! Victory belongs to the Darkcragg!"
|
||||
- All participants get epoch win credit (tracked in escape_participants by role).
|
||||
|
||||
### 5. Mode Activation in Engine
|
||||
|
||||
Update `src/core/engine.py` and `src/core/actions.py`:
|
||||
- On game start, check epoch.endgame_mode. Load the appropriate endgame system.
|
||||
- Mode-specific commands only available when that mode is active:
|
||||
- HtL: checkpoint status command, floor control display
|
||||
- Raid: raid boss status command (`boss` — show HP, phase, mechanics discovered so far)
|
||||
- R&E: `pickup`, `ward`, `lure`, `block` commands. Carrier status. Pursuer distance.
|
||||
- Combat system needs to dispatch to endgame boss combat (floor boss, raid boss, pursuer) when the target is a special entity. Same chip-and-run framework but with mechanic overlays.
|
||||
- Endgame status integrated into barkeep recap and stats display.
|
||||
|
||||
### 6. New Combat Commands
|
||||
|
||||
For raid boss mechanics:
|
||||
- `defend` / `def` — defensive stance. Negates windup strike. Costs 1 dungeon action. Does no damage that round.
|
||||
- `dodge` / `dge` — evasion. Negates windup strike. Costs 1 dungeon action. Does no damage that round.
|
||||
|
||||
For R&E:
|
||||
- `pickup` — pick up dropped objective in current room. Free action.
|
||||
- `ward` — ward current room after clearing it. 1 dungeon action.
|
||||
- `lure` — lure the Pursuer. 2 dungeon actions.
|
||||
- `block` — (passive) just being in the pursuer's path triggers blocking. No explicit command needed — the system detects it. But add a `block` info command that shows: "Stand in the Pursuer's path to block. It will fight you instead of advancing."
|
||||
|
||||
## Rules
|
||||
|
||||
- All responses under 150 chars. Test this.
|
||||
- All broadcasts route through DCRG node, not EMBR.
|
||||
- Endgame mode commands are only available when that mode is active. Other mode commands return: "That doesn't apply this epoch."
|
||||
- Boss combat uses the same chip-and-run framework as bounties — shared HP pool, damage persists, flee to disengage.
|
||||
- Floor boss and raid boss regen is lazy-evaluated (calculate accumulated regen on engagement).
|
||||
- Use constants from `config.py`.
|
||||
- Raw parameterized SQL, no ORM.
|
||||
- Commit after each mode is working (3 major commits minimum).
|
||||
|
||||
## Testing
|
||||
|
||||
Add to `tests/`:
|
||||
- `tests/test_vote.py` — vote casting, changing, public broadcast, tally, tiebreak, zero-vote fallback
|
||||
- `tests/test_htl.py` — room clearing, regen ticks, checkpoint cluster detection, checkpoint establishment, floor boss spawn on cluster clear, floor unlock, Warden kill = win, rooms behind checkpoint immune to regen
|
||||
- `tests/test_boss_mechanics.py` — test each of the 12 mechanic implementations: armored, enraged, regenerator, stalwart, warded, phasing, draining, splitting, rotating_resistance, retaliator, summoner, cursed. Test phase scaling for raid boss.
|
||||
- `tests/test_raid.py` — HP scaling from active players, cap at 6000, regen, phase transitions at 66%/33%, contribution tracking, lockout mechanic, completion + rewards
|
||||
- `tests/test_rne.py` — objective claim, pursuer advancement (2:1 ratio), pursuer in carrier room triggers combat, carrier death drops objective, relay pickup resets pursuer, ward slows pursuer, lure diverts pursuer, blocker intercepts pursuer, win condition on town delivery
|
||||
- `tests/test_rne_broadcasts.py` — all R&E broadcasts fire correctly (claim, distance, blocker, lure, death, relay, victory)
|
||||
|
||||
Use in-memory SQLite for tests. All endgame tests should generate a proper epoch first (use epoch_generate with DummyBackend).
|
||||
|
||||
## Done When
|
||||
|
||||
All three endgame modes are playable end-to-end. A Hold the Line epoch can be won by clearing all floors and killing the Warden. A Raid Boss epoch can be won by depleting the boss HP pool through coordinated chip-and-run combat with mechanic discovery. A Retrieve and Escape epoch can be won through a relay of carriers with blockers, warders, and lures supporting. The epoch vote selects the next mode. All broadcasts route through DCRG. All responses under 150 chars, all tests passing. Commit and report.
|
||||
120
projects/mmud/mmud-phase6-prompt.md
Normal file
120
projects/mmud/mmud-phase6-prompt.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Task: MMUD Phase 6 — The Breach
|
||||
|
||||
## Before Writing Any Code
|
||||
|
||||
Re-read these sections of `/home/zvx/projects/mmud/docs/planned.md`:
|
||||
- The Breach — Mid-Epoch Event (Day 15) (all of it — 4 mini-events, endgame interaction, design rationale)
|
||||
- Breach secrets (the 3 breach-type secrets)
|
||||
|
||||
Also re-read `config.py` for: BREACH_ROOMS_MIN/MAX, BREACH_CONNECTS_FLOORS, BREACH_SECRETS, BREACH_MINI_EVENTS, EMERGENCE_HP, INCURSION_REGEN/HOLD_HOURS.
|
||||
|
||||
Phase 4 already generates the Breach zone (breachgen.py) and Phase 4's daytick.py already handles the day 15 trigger and days 12-13 foreshadowing. Phase 4's breach.py has basic state management. This phase wires the full runtime logic for all 4 mini-events.
|
||||
|
||||
## Phase 6 Deliverables
|
||||
|
||||
The Breach opens on day 15 with a random mini-event. Each of the 4 types plays differently. The Breach interacts with whichever endgame mode is active.
|
||||
|
||||
### 1. Breach Activation (verify/extend existing)
|
||||
|
||||
The day 15 trigger should already be in daytick.py. Verify it:
|
||||
- Day 12-13: barkeep foreshadowing broadcasts from DCRG: "The walls grow thin between the second and third depths. Something stirs."
|
||||
- Day 15: Breach opens. Set `breach.active = 1`. Open the room exits connecting Breach zone to floors 2 and 3. Broadcast from DCRG: "⚡ The ground splits. A new passage has opened between Floors 2 and 3. Strange light pours from within."
|
||||
- Players can now enter Breach rooms via the new exits from floors 2 and 3.
|
||||
- The permanent shortcut between floors 2 and 3 persists for the rest of the epoch.
|
||||
|
||||
### 2. Mini-Event: The Heist (mini Retrieve & Escape)
|
||||
|
||||
`src/systems/breach_heist.py` (new file)
|
||||
|
||||
- Artifact in the deepest Breach room, guarded by the Breach mini-boss.
|
||||
- Kill mini-boss → claim artifact. Carrier must bring it back to town.
|
||||
- Pursuer spawns (slower, Breach-only — only operates within the 5-8 Breach rooms + the floors 2-3 connection).
|
||||
- If carrier dies, artifact drops. Any player can pick up.
|
||||
- Relay mechanics same as R&E but compressed — 5-8 rooms, not 4 floors.
|
||||
- 3 Breach secrets scattered along the escape route. Found under pressure.
|
||||
- Completion: artifact delivered to town. Breach rewards distributed. Broadcast from DCRG: "🏆 The artifact has been extracted from the Breach!"
|
||||
|
||||
Reuse as much R&E logic from Phase 5 as possible — shared carrier/pursuer/relay patterns.
|
||||
|
||||
### 3. Mini-Event: The Emergence (mini Raid Boss)
|
||||
|
||||
`src/systems/breach_emergence.py` (new file)
|
||||
|
||||
- Creature with shared HP pool (500-800 HP, `EMERGENCE_HP_MIN/MAX`) sits in central Breach room.
|
||||
- Surrounding rooms spawn minions on a timer (respawn every 8 hours).
|
||||
- Same chip-and-run combat as bounties/raid boss. Regen at 3%/8h.
|
||||
- 3 Breach secrets are in the minion rooms — discovered while contributing to the kill.
|
||||
- Completion: creature HP reaches 0. Broadcast: "🏆 The Breach creature has been destroyed!"
|
||||
|
||||
Reuse raid boss combat framework from Phase 5.
|
||||
|
||||
### 4. Mini-Event: The Incursion (mini Hold the Line)
|
||||
|
||||
`src/systems/breach_incursion.py` (new file)
|
||||
|
||||
- Breach rooms start fully hostile. Regen at 2 rooms/day (`INCURSION_REGEN_ROOMS_PER_DAY`) within just 5-8 rooms.
|
||||
- Players must clear ALL Breach rooms and hold them all for 48 hours (`INCURSION_HOLD_HOURS`).
|
||||
- If any room reverts during the hold timer, the clock resets.
|
||||
- 3 Breach secrets behind the hardest rooms, found as part of the push.
|
||||
- Track hold start time in `breach.incursion_hold_started_at`. On each regen tick, check if any Breach room reverted — if so, reset the timer.
|
||||
- Completion: 48 hours with all rooms held. Broadcast: "🏆 The Breach has been secured! The incursion is contained."
|
||||
|
||||
Reuse HtL room clearing/regen logic from Phase 5.
|
||||
|
||||
### 5. Mini-Event: The Resonance (puzzle dungeon)
|
||||
|
||||
`src/systems/breach_resonance.py` (new file)
|
||||
|
||||
- No combat focus. Breach rooms contain environmental puzzles.
|
||||
- 3 Breach secrets ARE the puzzle rewards. Finding all 3 unlocks a bonus cache in the deepest room.
|
||||
- Puzzles are generated in Phase 4 (breachgen already places Breach secrets). This phase adds the interaction logic:
|
||||
- `examine` objects in Breach rooms triggers puzzle checks
|
||||
- Puzzle state tracked per-player in secret_progress
|
||||
- Sequence puzzles, item-interaction puzzles, cross-room clue puzzles (use the same multi-room puzzle archetypes from the main dungeon)
|
||||
- Completion: all 3 Breach secrets found by any player(s). Bonus cache unlocked. Broadcast: "🏆 The Resonance has been understood. The Breach yields its secrets."
|
||||
|
||||
Soloable by nature — knowledge not stats.
|
||||
|
||||
### 6. Breach Interaction with Endgame Modes
|
||||
|
||||
Regardless of which mini-event is running, the Breach benefits the active endgame mode:
|
||||
|
||||
- **Retrieve & Escape:** The Breach shortcut (floors 2↔3) becomes an alternate escape route. Carrier can path through it. Shorter but Breach content (mini-boss, minions, etc.) may still be there.
|
||||
- **Raid Boss:** Breach completion (any mini-event) drops a buff item granting +20% damage vs the raid boss for the rest of the epoch. Add to player inventory on Breach completion.
|
||||
- **Hold the Line:** Breach rooms count as bonus territory toward checkpoint progress on both floors 2 and 3. Cleared Breach rooms contribute to the cleared room count for both floor 2 and floor 3 checkpoints.
|
||||
|
||||
### 7. Breach Secret Integration
|
||||
|
||||
Verify that the 3 Breach secrets work with the existing discovery system:
|
||||
- `secrets` command includes Breach secrets in the count after day 15
|
||||
- Secret milestones (5/10/15/20) fire correctly with Breach secrets included
|
||||
- Barkeep hints for Breach secrets only available after day 15
|
||||
- Breach secrets contribute to the completionist reward (all 20 found)
|
||||
|
||||
## Rules
|
||||
|
||||
- All responses under 150 chars.
|
||||
- All Breach broadcasts route through DCRG.
|
||||
- Breach mini-event is always random (selected at epoch gen, never voted).
|
||||
- Reuse combat/territory frameworks from Phase 5 — don't duplicate code.
|
||||
- Breach content is inaccessible before day 15. Exits to Breach rooms don't exist until activation.
|
||||
- Use constants from `config.py`.
|
||||
- Commit after each mini-event works.
|
||||
|
||||
## Testing
|
||||
|
||||
Add to `tests/`:
|
||||
- `tests/test_breach_activation.py` — day 15 trigger, foreshadowing on days 12-13, exits open, Breach accessible, inaccessible before day 15
|
||||
- `tests/test_breach_heist.py` — mini-boss, artifact claim, mini-pursuer, relay, completion, secrets under pressure
|
||||
- `tests/test_breach_emergence.py` — shared HP pool, minion respawn, chip-and-run, completion, secrets in minion rooms
|
||||
- `tests/test_breach_incursion.py` — room clearing, regen within Breach, 48h hold timer, timer reset on revert, completion
|
||||
- `tests/test_breach_resonance.py` — puzzle interaction, secret discovery, bonus cache unlock, no combat required
|
||||
- `tests/test_breach_endgame.py` — R&E shortcut, raid boss damage buff, HtL bonus territory
|
||||
|
||||
Use in-memory SQLite for tests. Generate full epoch with DummyBackend for each test.
|
||||
|
||||
## Done When
|
||||
|
||||
The Breach opens on day 15 with one of four randomly selected mini-events. Each mini-event is playable end-to-end with its own win condition. Breach secrets integrate cleanly with the discovery system. The Breach interacts with whichever endgame mode is active. All broadcasts route through DCRG. All responses under 150 chars, all tests passing. Commit and report.
|
||||
|
||||
This is the final gameplay phase. After this, the full 30-day epoch loop is complete: epoch generates → players explore and progress → Breach opens day 15 → endgame mode pushes through days 20-30 → epoch vote → wipe → new epoch.
|
||||
152
projects/mmud/mmud-prompts/mmud-prompts/01-update-planned.md
Normal file
152
projects/mmud/mmud-prompts/mmud-prompts/01-update-planned.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Task: Update planned.md with new design sections
|
||||
|
||||
Edit `/home/zvx/projects/mmud/docs/planned.md` in place. Three additions plus resolved decisions updates.
|
||||
|
||||
---
|
||||
|
||||
## Addition 1: The Last Ember — Town Hub
|
||||
|
||||
Find the `## Atmosphere & Writing` section. **BEFORE** the `---` divider that separates Atmosphere from `## New Player Onboarding`, insert this new section:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## The Last Ember — Town Hub
|
||||
|
||||
The Last Ember is the one room that never changes. Epochs wipe the dungeon, reskin the narrative, randomize everything — but players always wake up in the same bar, with the same people, who remember them. The lanterns don't burn oil — they just burn. Nobody lights them. Nobody replaces them. The dungeon reshapes itself every 30 days but the Last Ember sits at the mouth of it like a tooth that won't come loose.
|
||||
|
||||
The Last Ember is the constant across every epoch, every server, every wipe. It is the frame for the entire game.
|
||||
|
||||
### Grist — The Barkeep
|
||||
|
||||
Has never left the bar. Not once. Players who've been around for dozens of epochs start to wonder if he *can*. He knows everything that happens in the dungeon — not because he goes there, but because everyone who comes back tells him, and he never forgets. He speaks in short, deliberate sentences. Never wastes a word. He pours drinks that are always exactly what you needed, even if you didn't order.
|
||||
|
||||
His recap isn't a service — it's a compulsion. He *has* to tell you what happened. Like the information would burn him if he held it.
|
||||
|
||||
He's the bard token system. He trades in stories, not gold. Bring him something interesting — a secret, a discovery, something nobody else knows — and he gives you something back. Information, a temporary edge, a nudge in the right direction. He doesn't trade because he's kind. He trades because he *collects*.
|
||||
|
||||
**Mechanical role:** Recap (free), bard token exchange, hints, epoch vote ballot, bounty board.
|
||||
|
||||
### Maren — The Healer
|
||||
|
||||
Used to be an adventurer. Went deeper than anyone. Came back wrong — not injured, just *done*. She won't say what she saw on the lowest floor. She heals with her hands, not magic, and it hurts. She's efficient, not gentle. She charges gold because she says free healing breeds carelessness, and she's tired of patching people up who didn't respect the dungeon.
|
||||
|
||||
She's the only NPC who will occasionally refuse to talk to you if you died doing something stupid — but she still heals you.
|
||||
|
||||
She has a scar across her left palm that she got "the last time." She won't say the last time of what.
|
||||
|
||||
**Mechanical role:** HP restoration for gold.
|
||||
|
||||
### Torval — The Merchant
|
||||
|
||||
Doesn't go into the dungeon either, but somehow his inventory matches what's down there each epoch. Nobody asks how. He appraises items by weight and sound — taps gear on the counter, listens, names a price. He's cheerful in a way that feels slightly wrong given where he operates. He tells bad jokes. He calls everyone "friend" and means it exactly zero percent. He'd sell you a cursed sword and sleep fine.
|
||||
|
||||
But his prices are fair and his stock is real, which is more than you can say for most people in a town built around a hole full of monsters.
|
||||
|
||||
He keeps a ledger that goes back further than the bar. The pages at the front are in a language nobody can read.
|
||||
|
||||
**Mechanical role:** Buy, sell, item appraisal.
|
||||
|
||||
### Whisper — The Sage
|
||||
|
||||
Nobody knows if Whisper is her name or a description of how she talks. She sits in the corner of the Last Ember, always the same corner, and she knows things about the dungeon that change each epoch — lore, history, connections between rooms, what the symbols mean. She speaks in fragments and riddles not because she's trying to be mysterious but because that's how the information comes to her. She describes it like listening to a conversation through a wall.
|
||||
|
||||
Her clues are genuine but filtered through whatever broke her ability to just *say things plainly*. Players who pay attention to her exact phrasing find secrets faster. Players who dismiss her as flavor text miss half the game.
|
||||
|
||||
She has been the same age for as long as anyone can remember.
|
||||
|
||||
**Mechanical role:** Lore hints, secret clues, puzzle guidance (via bard tokens).
|
||||
|
||||
### NPC Live Conversations — LLM at Runtime
|
||||
|
||||
The "zero LLM at runtime" rule has one exception: talking to NPCs in the Last Ember. Walking up to Grist and having an actual conversation, asking Maren about her scar, trying to get Whisper to speak plainly — these interactions use a live LLM call.
|
||||
|
||||
The 150-character limit IS the NPC's personality. Grist is terse by nature. Maren doesn't waste words. Whisper speaks in fragments. Torval talks fast. The constraint is the flavor.
|
||||
|
||||
**Command:** `talk <npc>` or `talk <npc> <message>` — free action (in town only). Opens or continues a conversation.
|
||||
|
||||
**System prompt per NPC includes:**
|
||||
- Full backstory and personality card
|
||||
- Current game state injection: active bounties, recent deaths, Breach status, epoch day, floor control percentages, raid boss HP — whatever is relevant. The NPC *knows what's happening.*
|
||||
- Hard rules: respond in character, NEVER break character, response MUST be under 150 characters, never reveal exact secret locations or puzzle solutions (hints only), never acknowledge being an AI, never discuss anything outside the game world.
|
||||
|
||||
**What each NPC brings:**
|
||||
- **Grist** — gossip and world state. Knows everything from broadcast logs. Ask about another player and he'll tell you what they've been up to. Dry, factual, slightly unsettling in how much he knows.
|
||||
- **Maren** — the human element. Comments on your injuries, your play pattern, your stubbornness. Has opinions about the dungeon. Will never talk about what she saw on the lowest floor no matter how hard you try.
|
||||
- **Torval** — comic relief and commerce. Banter about items, terrible jokes, comments on your gear. "You're wearing THAT to floor 3? Bold." Embellished sales pitches.
|
||||
- **Whisper** — lore oracle. High-skill conversation. Speaks in fragments. Ask the right questions and get real, useful information about secrets. Her cryptic style is the LLM prompt, not a gimmick — talking to Whisper IS a puzzle.
|
||||
|
||||
**Guardrails:**
|
||||
- Conversation memory is session-only — NPCs don't remember yesterday's chat. Keeps context windows small and prevents exploit accumulation.
|
||||
- If the LLM fails or times out, fall back to a random pre-generated dialogue snippet from the batch pipeline (20 per NPC already generated at epoch start).
|
||||
- No rate limit on NPC conversations. Players can talk as long as they want. The NPCs are storytellers and historians — extended conversation is a feature, not abuse.
|
||||
- Uses the same pluggable LLM backend as the epoch generation pipeline (Anthropic, OpenAI, Google, or Dummy).
|
||||
|
||||
**Server History Seed — 2 Years of Lore:**
|
||||
|
||||
Before the server goes live, generate 24 epochs (2 years) of simulated history. Each epoch gets: number, endgame mode, Breach type, narrative theme, win/loss result, 3-5 notable players (generated names, classes, what they did), 1-2 memorable moments, hall of fame entries, titles earned. Stored in the persistent tables. When the real server starts on epoch 25, the NPCs have 24 epochs of stories to tell. A compressed lore packet (20-30 sentences of highlights) is injected into every NPC system prompt and regenerated each epoch as real player history accumulates and blends with seeded history.
|
||||
|
||||
**Cost math:** At Haiku-tier pricing, ~500 tokens per turn. Even heavy usage (50+ turns/day across all players) is ~$0.006/day. Unlimited conversation is essentially free.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Addition 2: Command Discovery
|
||||
|
||||
Find the `## New Player Onboarding` section. After the "Daily Tips" subsection and before the `---` divider that separates it from `## Resolved Decisions`, insert:
|
||||
|
||||
```markdown
|
||||
### Command Discovery — No Guessing on Slow Radio
|
||||
|
||||
On a 45-60 second radio round-trip, guessing a command and getting "Unknown command" is unacceptable. Every interaction point should make available commands visible.
|
||||
|
||||
**First connect message:** Include core commands explicitly. Not "type H for help" — actually list them. `Move:N/S/E/W Fight:F Look:L Flee:FL Stats:ST Help:H` fits in 150 chars and gives a new player everything for their first session.
|
||||
|
||||
**Smart error responses:** Never just "Unknown command." Always suggest valid commands based on current player state:
|
||||
- In town: `Unknown. Try: BAR SHOP HEAL BANK TRAIN ENTER H(elp)`
|
||||
- In dungeon: `Unknown. Try: F(ight) FL(ee) L(ook) N/S/E/W H(elp)`
|
||||
- In combat: `Unknown. Try: F(ight) FL(ee) STATS`
|
||||
- Dead: `Unknown. You're dead. Type RESPAWN.`
|
||||
|
||||
**Context-sensitive help (H command):** `H` alone shows commands available in current state. `H <cmd>` gives specific help. All fits 150 chars. Help output changes based on player level — only shows unlocked commands.
|
||||
|
||||
**Barkeep nudges:** When a player visits Grist but hasn't used a system yet, the recap appends a tip: "Tip: try BOUNTY to see active hunts" or "Tip: use MSG to leave notes in rooms." One tip per visit, rotating through unused systems. Stops once the player has tried everything.
|
||||
|
||||
**Progressive unlock announcements:** When a command unlocks at a new level, announce it explicitly with usage: "⬆ Level 3! New: SHOP(buy gear) BANK(save gold) MAIL(send messages)"
|
||||
|
||||
**Last Ember quick reference:** The spectator web page includes a printable command cheat sheet — a one-page reference players can keep next to their Meshtastic device. Physical reference for a physical radio game.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Addition 3: Resolved Decisions
|
||||
|
||||
Find the `## Resolved Decisions` section. Add these lines at the end of the list:
|
||||
|
||||
```markdown
|
||||
- Town hub: The Last Ember — persistent bar across all epochs, all servers. Four permanent NPCs: Grist (barkeep), Maren (healer), Torval (merchant), Whisper (sage).
|
||||
- NPC live conversations: NPCs are sim nodes on the mesh (GRST, MRN, TRVL, WSPR). Players DM them directly. Three rule layers: unknown node gets static onboarding, known player not in bar gets static rejection, known player in bar gets full LLM conversation. No rate limit. Session-only memory. Falls back to pre-generated dialogue on failure. 24-epoch history seed provides 2 years of lore.
|
||||
- Command discovery: smart error responses show valid commands for current state, barkeep nudges for unused systems, explicit command listing on first connect.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Addition 4: LLM Content Pipeline update
|
||||
|
||||
Find the `## LLM Content Pipeline` section. Find the line that says `### Decision Rule` and the text `Use LLMs for content that can be validated offline. Use deterministic templates for anything that must be correct in real-time.`
|
||||
|
||||
Replace that with:
|
||||
|
||||
```markdown
|
||||
### Decision Rule
|
||||
|
||||
Use LLMs for content that can be validated offline. Use deterministic templates for anything that must be correct in real-time. **One exception:** NPC conversations in The Last Ember use live LLM calls — the 150-char response constraint, personality cards, and session-only memory make this safe, cheap, and in-character. See The Last Ember section for details.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "Design doc: add Last Ember NPCs, live NPC conversations, command discovery"
|
||||
```
|
||||
102
projects/mmud/mmud-prompts/mmud-prompts/02-npc-nodes.md
Normal file
102
projects/mmud/mmud-prompts/mmud-prompts/02-npc-nodes.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Task: Add NPC sim node architecture and rules to planned.md
|
||||
|
||||
Edit `/home/zvx/projects/mmud/docs/planned.md` in place.
|
||||
|
||||
---
|
||||
|
||||
## Edit 1: NPC Sim Nodes section
|
||||
|
||||
Find the `### NPC Live Conversations — LLM at Runtime` subsection inside `## The Last Ember — Town Hub`. Replace the **Command** line and everything after it in that subsection (from `**Command:**` through the end of `**Cost math:**`) with the following:
|
||||
|
||||
```markdown
|
||||
**Network Architecture — NPCs as Mesh Nodes:**
|
||||
|
||||
The NPCs are literal Meshtastic nodes on the mesh network. Five sim nodes, all backed by the same game database:
|
||||
|
||||
- **EMBR** — The Last Ember. The game server. All game commands go here.
|
||||
- **GRST** — Grist. DM this node to talk to the barkeep.
|
||||
- **MRN** — Maren. DM this node to talk to the healer.
|
||||
- **TRVL** — Torval. DM this node to talk to the merchant.
|
||||
- **WSPR** — Whisper. DM this node to talk to the sage.
|
||||
|
||||
Players don't issue a `talk` command — they DM the NPC's node directly. The game server sees inbound on the NPC node ID, checks the rules below, and routes the response back through that NPC's node. The NPCs are *people on the network*, not menu options.
|
||||
|
||||
**Three rule layers (checked in order):**
|
||||
|
||||
**Rule 1 — Unknown node (not in the game):** Static in-character rejection with onboarding instructions. No LLM call. Each NPC has a fixed response:
|
||||
- Grist: `"Don't know you. DM EMBR to start. Then we'll talk."`
|
||||
- Maren: `"I only patch up adventurers. DM EMBR to become one."`
|
||||
- Torval: `"No account, no credit, friend. DM EMBR to join up."`
|
||||
- Whisper: `"...not yet. EMBR. Begin there."`
|
||||
|
||||
**Rule 2 — Known player, not in the bar:** Static in-character refusal. Player is in the dungeon, dead, or otherwise not in town. No LLM call.
|
||||
- Grist: `"You're not here, {name}. Come back to the bar first."`
|
||||
- Maren: `"I can hear you're still down there. Come back alive."`
|
||||
- Torval: `"I don't do deliveries. Get back to the Ember."`
|
||||
- Whisper: `"...too far. Return."`
|
||||
|
||||
**Rule 3 — Known player, in the bar:** Full LLM conversation. This is the only case that triggers a live LLM call.
|
||||
|
||||
**System prompt per NPC includes:**
|
||||
- Full backstory and personality card
|
||||
- Current game state injection: active bounties, recent deaths, Breach status, epoch day, floor control percentages, raid boss HP — whatever is relevant. The NPC *knows what's happening.*
|
||||
- Hard rules: respond in character, NEVER break character, response MUST be under 150 characters, never reveal exact secret locations or puzzle solutions (hints only), never acknowledge being an AI, never discuss anything outside the game world.
|
||||
|
||||
**What each NPC brings:**
|
||||
- **Grist** — gossip and world state. Knows everything from broadcast logs. Ask about another player and he'll tell you what they've been up to. Dry, factual, slightly unsettling in how much he knows.
|
||||
- **Maren** — the human element. Comments on your injuries, your play pattern, your stubbornness. Has opinions about the dungeon. Will never talk about what she saw on the lowest floor no matter how hard you try.
|
||||
- **Torval** — comic relief and commerce. Banter about items, terrible jokes, comments on your gear. "You're wearing THAT to floor 3? Bold." Embellished sales pitches.
|
||||
- **Whisper** — lore oracle. High-skill conversation. Speaks in fragments. Ask the right questions and get real, useful information about secrets. Her cryptic style is the LLM prompt, not a gimmick — talking to Whisper IS a puzzle.
|
||||
|
||||
**Guardrails:**
|
||||
- Conversation memory is session-only — NPCs don't remember yesterday's chat. Keeps context windows small and prevents exploit accumulation.
|
||||
- If the LLM fails or times out, fall back to a random pre-generated dialogue snippet from the batch pipeline (20 per NPC already generated at epoch start).
|
||||
- No rate limit on NPC conversations. Players can talk as long as they want. The NPCs are storytellers, historians, and characters — extended conversation is a feature, not abuse.
|
||||
- Uses the same pluggable LLM backend as the epoch generation pipeline (Anthropic, OpenAI, Google, or Dummy).
|
||||
|
||||
**Server History Seed — 2 Years of Lore:**
|
||||
|
||||
Before the server goes live, generate 24 epochs (2 years) of simulated history. For each epoch:
|
||||
- Epoch number, endgame mode, Breach type, narrative theme
|
||||
- Whether the server won or lost (mix of both — some epic victories, some heartbreaking failures)
|
||||
- 3-5 notable players per epoch (generated names, classes, levels reached, what they did)
|
||||
- 1-2 memorable moments per epoch ("Kira carried the Crown from floor 4 to floor 1 with 3 HP", "The Warden stood for 28 days — the server failed on the final push", "Epoch 11's Raid Boss had No Escape + Enraged — three players died on the killing blow")
|
||||
- Hall of fame entries, titles earned
|
||||
|
||||
Stored in the persistent tables (accounts, hall_of_fame, hall_of_fame_participants, titles). When the real server starts on epoch 25, the NPCs have 24 epochs of history to draw from. Grist drops names of old champions. Maren compares your injuries to legends. Torval mentions gear from epochs past. Whisper sees patterns across cycles that nobody else notices.
|
||||
|
||||
**NPC context injection includes a lore packet:** A compressed 20-30 sentence summary of server history highlights pulled from the hall of fame tables. Regenerated at each epoch start so it stays current as real player history accumulates and blends with the seed history. The NPCs don't distinguish between seeded and real history — it's all the same to them.
|
||||
|
||||
**Cost math:** At Haiku-tier pricing, ~500 tokens per conversation turn. Even heavy usage (50 turns/day across all players) is ~25,000 tokens/day ≈ $0.006/day. Unlimited conversation is essentially free.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 2: Update Resolved Decisions
|
||||
|
||||
Find the resolved decision line that says:
|
||||
```
|
||||
- NPC live conversations: LLM at runtime exception for talk command in town. Session-only memory, 5/day rate limit per NPC, falls back to pre-generated dialogue on failure.
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
```
|
||||
- NPC live conversations: NPCs are sim nodes on the mesh (GRST, MRN, TRVL, WSPR). Players DM them directly. Three rule layers: unknown node gets static onboarding response, known player not in bar gets static rejection, known player in bar gets full LLM conversation. No rate limit. Session-only memory. Falls back to pre-generated dialogue on failure. 24-epoch history seed provides 2 years of lore for NPCs to draw from.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 3: Add to Open Questions
|
||||
|
||||
Add this to the end of the `## Open Questions` list:
|
||||
```
|
||||
- NPC sim node deployment — which host runs meshtasticd with 5 identities, TCP routing to game LXC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "Design doc: NPCs as mesh sim nodes, three-layer access rules, onboarding funnel"
|
||||
```
|
||||
61
projects/mmud/mmud-prompts/mmud-prompts/03-darkcragg.md
Normal file
61
projects/mmud/mmud-prompts/mmud-prompts/03-darkcragg.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Task: Add Darkcragg Depths dungeon name to planned.md
|
||||
|
||||
Edit `/home/zvx/projects/mmud/docs/planned.md` in place.
|
||||
|
||||
---
|
||||
|
||||
## Edit 1: Dungeon section
|
||||
|
||||
Find the `## Dungeon` section. Find the line `### Hub-Spoke Layout with Loops`. Insert a new subsection BEFORE it:
|
||||
|
||||
```markdown
|
||||
### The Darkcragg Depths
|
||||
|
||||
The dungeon is always the Darkcragg Depths. Like the Last Ember, the name is a constant — it persists across every epoch, every server, every wipe. The floors reskin, the layout regenerates, the monsters change, but the Depths are always the Depths. Players descend into the Darkcragg. They talk about the Darkcragg. It's a proper noun, not a generic dungeon.
|
||||
|
||||
The four floors are narratively re-skinned each epoch (Sunken Halls, Fungal Depths, Ember Caverns, Void Reach are defaults — the LLM pipeline may rename them) but the Darkcragg Depths is the name on the door every time.
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 2: Update The Last Ember section
|
||||
|
||||
Find the paragraph in `## The Last Ember — Town Hub` that starts with "The Last Ember is the constant across every epoch". Replace that single line with:
|
||||
|
||||
```markdown
|
||||
The Last Ember and the Darkcragg Depths are the two constants across every epoch, every server, every wipe. The bar and the hole it sits on top of. Everything else changes. These don't.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 3: Update NPC rejection messages
|
||||
|
||||
Find the Rule 2 static responses for known players not in the bar. Update Maren's line:
|
||||
|
||||
Replace:
|
||||
```
|
||||
- Maren: `"I can hear you're still down there. Come back alive."`
|
||||
```
|
||||
With:
|
||||
```
|
||||
- Maren: `"I can hear you're still in the Darkcragg. Come back alive."`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 4: Resolved Decisions
|
||||
|
||||
Add to the end of the `## Resolved Decisions` list:
|
||||
|
||||
```markdown
|
||||
- Dungeon name: The Darkcragg Depths — persistent across all epochs like the Last Ember. Floor names reskin per epoch but the Darkcragg is always the Darkcragg.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "Design doc: the dungeon is the Darkcragg Depths"
|
||||
```
|
||||
75
projects/mmud/mmud-prompts/mmud-prompts/04-dcrg-node.md
Normal file
75
projects/mmud/mmud-prompts/mmud-prompts/04-dcrg-node.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Task: Add Darkcragg Depths broadcast node to planned.md
|
||||
|
||||
Edit `/home/zvx/projects/mmud/docs/planned.md` in place.
|
||||
|
||||
---
|
||||
|
||||
## Edit 1: Update NPC Sim Nodes section
|
||||
|
||||
Find the `**Network Architecture — NPCs as Mesh Nodes:**` block inside `## The Last Ember — Town Hub`. Replace the node list (the 5-item bullet list starting with `- **EMBR**` through `- **WSPR**`) with:
|
||||
|
||||
```markdown
|
||||
- **EMBR** — The Last Ember. The game server. All game commands go here. Responds with direct action results only.
|
||||
- **DCRG** — The Darkcragg Depths. One-way broadcast node. All dungeon events come from here — deaths, bounty progress, Breach opening, regen ticks, boss phase transitions, discoveries, level-ups. Does not accept commands. The dungeon is alive on the network.
|
||||
- **GRST** — Grist. DM this node to talk to the barkeep.
|
||||
- **MRN** — Maren. DM this node to talk to the healer.
|
||||
- **TRVL** — Torval. DM this node to talk to the merchant.
|
||||
- **WSPR** — Whisper. DM this node to talk to the sage.
|
||||
|
||||
This splits two distinct streams: EMBR only sends direct responses to your actions. DCRG is the ambient feed of what's happening in the world. The NPCs are people you talk to. Six nodes total, one game DB backing all of them.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 2: Add DCRG rules to the rule layers
|
||||
|
||||
Find `**Three rule layers (checked in order):**`. Insert a new section BEFORE Rule 1:
|
||||
|
||||
```markdown
|
||||
**DCRG rules (broadcast node):**
|
||||
- DCRG never accepts inbound messages. If a player or unknown node DMs DCRG, it responds with a static message: `"The Darkcragg does not answer. It only speaks. DM EMBR to play."`
|
||||
- All tier 1 and tier 2 broadcasts are sent FROM the DCRG node, not EMBR.
|
||||
- Targeted broadcasts (multi-room puzzle feedback) are also sent from DCRG as DMs to qualifying players.
|
||||
- DCRG is the voice of the dungeon. When someone dies, when the Breach opens, when a bounty falls — it comes from the Darkcragg.
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 3: Update Broadcast System section
|
||||
|
||||
Find `## Broadcast System`. Find the first paragraph or description of how broadcasts work. Add this line at the end of the introductory text, before any subsections:
|
||||
|
||||
```markdown
|
||||
All broadcasts are sent from the DCRG (Darkcragg Depths) sim node, not the main EMBR game node. This separates the ambient world feed from direct command responses. EMBR talks to you. The Darkcragg talks about everyone.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 4: Update Resolved Decisions
|
||||
|
||||
Find the resolved decision about NPC live conversations that starts with `- NPC live conversations: NPCs are sim nodes`. Replace it with:
|
||||
|
||||
```markdown
|
||||
- Mesh node architecture: 6 sim nodes — EMBR (game commands + responses), DCRG (one-way dungeon broadcasts), GRST/MRN/TRVL/WSPR (NPC conversations). One game DB backs all of them.
|
||||
- NPC conversations: Players DM NPC nodes directly. Three rule layers: unknown node gets static onboarding, known player not in bar gets static rejection, known player in bar gets full LLM conversation. Session-only memory, 5/day rate limit per NPC, falls back to pre-generated dialogue on failure.
|
||||
- DCRG is broadcast-only — does not accept commands. All tier 1/2 and targeted broadcasts route through DCRG.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edit 5: Update Open Questions
|
||||
|
||||
Find the open question `- NPC sim node deployment`. Replace it with:
|
||||
|
||||
```markdown
|
||||
- Sim node deployment — which host runs meshtasticd with 6 identities (EMBR, DCRG, GRST, MRN, TRVL, WSPR), TCP routing to game LXC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "Design doc: DCRG broadcast node — the dungeon speaks on the mesh"
|
||||
```
|
||||
215
projects/mmud/mmud-prompts/mmud-prompts/05-phase5.md
Normal file
215
projects/mmud/mmud-prompts/mmud-prompts/05-phase5.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Task: MMUD Phase 5 — Endgame Modes
|
||||
|
||||
## Before Writing Any Code
|
||||
|
||||
Re-read these sections of `/home/zvx/projects/mmud/docs/planned.md`:
|
||||
- Endgame: Three Rotating Modes (all of it — mode selection, R&E with Pursuer + support roles, Raid Boss with mechanic tables + phases, Hold the Line with regen + checkpoints + floor bosses)
|
||||
- The Darkcragg Depths (dungeon name context)
|
||||
- Floor Boss Mechanic Tables (all 4 floor tables)
|
||||
- Bounties During Hold the Line
|
||||
|
||||
Also re-read `config.py` for: HTL_REGEN_ROOMS_PER_DAY, FLOOR_BOSS_MECHANICS, WARDEN_HP/REGEN, RAID_BOSS_HP_PER_PLAYER/CAP/REGEN/MECHANIC_TABLE/PHASES, PURSUER_ADVANCE_RATE/SPAWN_DISTANCE/RELAY_RESET_DISTANCE, WARD/LURE constants, ENDGAME_MODES.
|
||||
|
||||
Phase 4 already generates floor bosses, raid boss pre-config, and bounties. This phase wires the runtime game logic for all three modes.
|
||||
|
||||
## Phase 5 Deliverables
|
||||
|
||||
All three endgame modes are playable. The epoch vote selects the mode. Each mode has its own win condition, progression mechanics, and broadcasts.
|
||||
|
||||
### 1. Epoch Vote System
|
||||
|
||||
`src/systems/vote.py` (new file)
|
||||
|
||||
- Day 30 trigger (already in daytick.py — wire it to the vote system)
|
||||
- `vote <mode>` command at barkeep — free action. Modes: `retrieve`, `raid`, `hold` (or numbers 1/2/3).
|
||||
- Votes are public — broadcast on cast: "🗳 {name} voted {mode}."
|
||||
- Votes can be changed up until epoch ends. UPSERT into epoch_votes table.
|
||||
- Tally on epoch end: most votes wins. Tiebreak: longest-unplayed mode. No quorum — 1 vote decides if only 1 player votes. Zero votes → longest-unplayed auto-selected.
|
||||
- `epoch_generate.py` already accepts endgame mode — wire the vote result into the next epoch's generation.
|
||||
|
||||
### 2. Hold the Line — Runtime Logic
|
||||
|
||||
`src/systems/endgame_htl.py` (new file)
|
||||
|
||||
**Room clearing:**
|
||||
- All dungeon rooms start hostile (`htl_cleared = 0`)
|
||||
- Killing all monsters in a room sets `htl_cleared = 1` with timestamp
|
||||
- Regen ticks (already in daytick.py) revert rooms: pick N random cleared non-checkpoint rooms per floor per day, set `htl_cleared = 0`, respawn monsters. Spread ticks across the day (floor 2 at 5/day = 1 room every ~5 hours).
|
||||
- Cleared rooms are safe — no random encounters. Reverted rooms respawn monsters.
|
||||
|
||||
**Checkpoints:**
|
||||
- Checkpoint rooms are defined during worldgen (is_checkpoint = 1 in rooms table). 3 per floor (hub, midpoint, stairway), 1 on floor 4 (Warden).
|
||||
- Establishment condition: all rooms in the checkpoint cluster (checkpoint room + all adjacent rooms) must be cleared within one regen window. Check on each room clear — if the cluster is complete, spawn the floor boss.
|
||||
- Once the floor boss dies, checkpoint locks permanently. `htl_checkpoints.established = 1`. Regen can never revert rooms behind an established checkpoint.
|
||||
- Final checkpoint on each floor (stairway) unlocks the next floor for all players.
|
||||
|
||||
**Floor bosses:**
|
||||
- Already generated with rolled mechanics (from Phase 4 bossgen).
|
||||
- Floor boss spawns in the checkpoint room when cluster is cleared. Uses the same shared HP pool / chip-and-run combat as bounties.
|
||||
- Boss mechanic implementation — each mechanic modifies combat behavior:
|
||||
- `armored` — damage halved until boss below 50% HP
|
||||
- `enraged` — boss deals double damage below 50% HP, takes 25% more
|
||||
- `regenerator` — boss heals 10% HP between sessions (check on engagement, apply since last fight)
|
||||
- `stalwart` — first flee attempt per engagement always fails
|
||||
- `warded` — boss has +50% DEF until a discovery secret on the same floor is found (check secret_progress)
|
||||
- `phasing` — boss is immune to damage on even-numbered epoch days (check epoch.day_number)
|
||||
- `draining` — boss steals 10% of damage dealt as HP from the attacker
|
||||
- `splitting` — at 50% HP, boss splits into two half-HP monsters in adjacent rooms. Both must die.
|
||||
- `rotating_resistance` — immune to the highest stat used by the last player who fought it. Track in DB.
|
||||
- `retaliator` — reflects 20% of damage back to attacker
|
||||
- `summoner` — spawns 1 add at start of each engagement. Add must die before boss can be damaged.
|
||||
- `cursed` — player who dealt most damage last session gets -2 to a random stat next login
|
||||
|
||||
- Floor 4 Warden: shared HP pool 300-500, regen at 3%/8h, rolls 2 mechanics from the full table.
|
||||
- Warden kill = epoch win. Broadcast: "🏆 The Warden has fallen! The Darkcragg Depths are conquered!"
|
||||
|
||||
**Broadcasts from DCRG:**
|
||||
- "🏰 Floor {n} Checkpoint {name} established!"
|
||||
- "⚠ Floor {n} lost {x} rooms. Frontline at {room}."
|
||||
- "🏰 Floor {n} unlocked! The descent continues."
|
||||
- "💀 Floor {n} frontline collapsed to Checkpoint {name}. Rally!"
|
||||
- Floor boss spawned, floor boss killed, Warden progress.
|
||||
|
||||
**Barkeep integration:**
|
||||
- Grist's recap includes HtL status: floors cleared percentage, checkpoint status, frontline position.
|
||||
|
||||
### 3. Raid Boss — Runtime Logic
|
||||
|
||||
`src/systems/endgame_raid.py` (new file)
|
||||
|
||||
**Activation:**
|
||||
- On epoch start (if mode is raid_boss), calculate active player count (anyone who entered the dungeon in first 3 days).
|
||||
- Set raid_boss.hp = 300 × active players, cap 6000. Set raid_boss.hp_max to same value.
|
||||
- Place boss in a room on floor 3-4 (from pre-generated config in Phase 4).
|
||||
|
||||
**Combat:**
|
||||
- Same chip-and-run as bounties — shared HP pool, engage/damage/flee.
|
||||
- Regen: 3%/8h (lazy-evaluated like bounties — calculate on engagement).
|
||||
- Track contributions in raid_boss_contributors.
|
||||
|
||||
**Mechanic implementation (2-3 rolled):**
|
||||
- `windup_strike` — every 3rd combat round, next round deals triple damage unless player uses `defend` or `dodge` action. Add `defend` and `dodge` as combat commands (cost 1 action, negate the windup).
|
||||
- `flat_damage_boost` — boss damage multiplied by 1.5x
|
||||
- `retribution` — at 75%/50%/25% HP thresholds, burst damage (2x normal) to the player who pushed it past the threshold
|
||||
- `aura_damage` — player takes 5% max HP unavoidable damage each combat round regardless of DEF
|
||||
- `extra_regen` — regen rate becomes 5%/8h instead of 3%/8h
|
||||
- `armor_phase` — boss takes half damage until: a discovery secret on the floor is found, OR 5+ unique players have contributed damage
|
||||
- `boss_flees` — at 75%/50%/25% HP, boss relocates to random room on same floor. Broadcast from DCRG: "🐉 The {boss} has fled to somewhere on Floor {n}!" Players must find it.
|
||||
- `regen_burst` — once per day at a random hour, boss heals 15% max HP in one tick. Trackable through observation.
|
||||
- `no_escape` — below 25% HP, all flee attempts fail. Fight to the death.
|
||||
- `summoner` — 1-2 adds spawn per engagement, must be killed before boss takes damage
|
||||
- `lockout` — after engaging, player can't reengage for 24 hours. Store lockout_until in raid_boss_contributors.
|
||||
- `enrage_timer` — after 5 combat rounds in a single engagement, boss damage doubles each subsequent round
|
||||
|
||||
**Phases:**
|
||||
- Phase transitions at 66% and 33% HP (`RAID_BOSS_PHASES`).
|
||||
- At each threshold, rolled mechanics intensify. Implementation: each mechanic has a `phase_modifier(phase_num)` that scales its effect. E.g., summoner spawns 1 add in phase 1, 2 in phase 2, 3 in phase 3. Windup goes from every 3rd round to every 2nd.
|
||||
- Phase transition broadcasts from DCRG: "🐉 The {boss} enters its second phase!"
|
||||
|
||||
**Win condition:** Boss HP reaches 0. All contributors rewarded. Killing blow gets bonus. Broadcast: "🏆 The {boss} has been slain! Victory belongs to the Darkcragg!"
|
||||
|
||||
### 4. Retrieve and Escape — Runtime Logic
|
||||
|
||||
`src/systems/endgame_rne.py` (new file)
|
||||
|
||||
**Setup:**
|
||||
- Guardian monster placed on floor 4 during epoch generation (add to bossgen if not already there — a strong but non-boss monster guarding the objective).
|
||||
- `escape_run` table tracks run state.
|
||||
|
||||
**Claiming the objective:**
|
||||
- Player defeats the guardian on floor 4 → objective claimed. `escape_run.active = 1`, carrier set, pursuer spawns.
|
||||
- Broadcast from DCRG: "👑 {name} claimed the {objective}! The Pursuer stirs."
|
||||
- Monster spawn rates double on all floors (multiply spawn chance by `ESCAPE_SPAWN_RATE_MULTIPLIER`).
|
||||
|
||||
**Pursuer:**
|
||||
- Tracks carrier. Advances 1 room toward carrier every 2 carrier actions (`PURSUER_ADVANCE_RATE`).
|
||||
- Spawns 3 rooms behind carrier (`PURSUER_SPAWN_DISTANCE`).
|
||||
- Track pursuer position in `escape_run.pursuer_room_id`. Track fractional ticks in `pursuer_ticks`.
|
||||
- On every carrier action: increment pursuer_ticks. When pursuer_ticks >= PURSUER_ADVANCE_RATE, advance pursuer 1 room toward carrier (pathfind shortest route), reset ticks.
|
||||
- When pursuer enters carrier's room: forced combat. Pursuer is invulnerable (takes no damage). Hits hard. Carrier can only flee. Flee uses normal SPD check. Success = carrier moves 1 room. Failure = take damage + try again next action.
|
||||
|
||||
**Carrier death and relay:**
|
||||
- Carrier dies → objective drops at death room. Broadcast from DCRG: "💀 The carrier has fallen on Floor {n}. The {objective} lies unguarded."
|
||||
- `escape_run.objective_dropped = 1`, `dropped_room_id` set.
|
||||
- Any player can `pickup` the objective in that room.
|
||||
- On pickup: pursuer resets to 5 rooms behind new carrier (`PURSUER_RELAY_RESET_DISTANCE`). Broadcast: "👑 {name} picks up the {objective}! The Pursuer resets."
|
||||
- Death penalty still applies to the dead carrier (gold loss, respawn in town).
|
||||
|
||||
**Three support roles:**
|
||||
|
||||
**Blockers:**
|
||||
- Non-carrier in a room between pursuer and carrier. When pursuer reaches a blocker's room, forced combat with the blocker instead of advancing.
|
||||
- Blocker can't kill pursuer (invulnerable). Each round blocker survives = 1 round pursuer isn't moving.
|
||||
- Blocker can flee (normal SPD check). Blocker can die.
|
||||
- Broadcast: "🛡 {name} is blocking the Pursuer on Floor {n}!" and "💀 {name} fell holding the line. The Pursuer advances."
|
||||
- Implementation: on pursuer advance, check if any player is in the target room. If yes, pursuer enters combat with them instead of continuing.
|
||||
|
||||
**Warders:**
|
||||
- `ward` command in a cleared dungeon room (1 extra action after clearing = `WARD_ACTION_COST`). Sets `rooms.ward_active = 1`.
|
||||
- Warded room slows pursuer — takes 2 advance ticks to pass through instead of 1 (`WARD_PURSUER_SLOWDOWN`).
|
||||
- Ward breaks after one use (reset to 0 when pursuer passes through).
|
||||
- No broadcast on warding — silent preparation.
|
||||
|
||||
**Lures:**
|
||||
- `lure` command when on same floor as pursuer. Costs 2 actions (`LURE_ACTION_COST`).
|
||||
- Pursuer diverts toward lure player for 3 ticks (`LURE_DIVERT_TICKS`), then snaps back to carrier tracking. Total delay ~6 ticks (`LURE_TOTAL_DELAY_TICKS`) including backtrack.
|
||||
- Broadcast: "🎯 {name} lured the Pursuer into {room}! It diverts."
|
||||
- After divert expires: "👁 The Pursuer has reacquired the carrier."
|
||||
|
||||
**Pursuer distance broadcasts (from DCRG):**
|
||||
- "👁 The Pursuer is {n} rooms behind the carrier." (every 5 carrier actions)
|
||||
- "👁 The Pursuer is 3 rooms behind. It's closing."
|
||||
- "⚠ The Pursuer has reached the carrier!"
|
||||
|
||||
**Win condition:** Any player delivers objective to town (The Last Ember). Broadcast: "🏆 The {objective} has reached the surface! Victory belongs to the Darkcragg!"
|
||||
- All participants get epoch win credit (tracked in escape_participants by role).
|
||||
|
||||
### 5. Mode Activation in Engine
|
||||
|
||||
Update `src/core/engine.py` and `src/core/actions.py`:
|
||||
- On game start, check epoch.endgame_mode. Load the appropriate endgame system.
|
||||
- Mode-specific commands only available when that mode is active:
|
||||
- HtL: checkpoint status command, floor control display
|
||||
- Raid: raid boss status command (`boss` — show HP, phase, mechanics discovered so far)
|
||||
- R&E: `pickup`, `ward`, `lure`, `block` commands. Carrier status. Pursuer distance.
|
||||
- Combat system needs to dispatch to endgame boss combat (floor boss, raid boss, pursuer) when the target is a special entity. Same chip-and-run framework but with mechanic overlays.
|
||||
- Endgame status integrated into barkeep recap and stats display.
|
||||
|
||||
### 6. New Combat Commands
|
||||
|
||||
For raid boss mechanics:
|
||||
- `defend` / `def` — defensive stance. Negates windup strike. Costs 1 dungeon action. Does no damage that round.
|
||||
- `dodge` / `dge` — evasion. Negates windup strike. Costs 1 dungeon action. Does no damage that round.
|
||||
|
||||
For R&E:
|
||||
- `pickup` — pick up dropped objective in current room. Free action.
|
||||
- `ward` — ward current room after clearing it. 1 dungeon action.
|
||||
- `lure` — lure the Pursuer. 2 dungeon actions.
|
||||
- `block` — (passive) just being in the pursuer's path triggers blocking. No explicit command needed — the system detects it. But add a `block` info command that shows: "Stand in the Pursuer's path to block. It will fight you instead of advancing."
|
||||
|
||||
## Rules
|
||||
|
||||
- All responses under 150 chars. Test this.
|
||||
- All broadcasts route through DCRG node, not EMBR.
|
||||
- Endgame mode commands are only available when that mode is active. Other mode commands return: "That doesn't apply this epoch."
|
||||
- Boss combat uses the same chip-and-run framework as bounties — shared HP pool, damage persists, flee to disengage.
|
||||
- Floor boss and raid boss regen is lazy-evaluated (calculate accumulated regen on engagement).
|
||||
- Use constants from `config.py`.
|
||||
- Raw parameterized SQL, no ORM.
|
||||
- Commit after each mode is working (3 major commits minimum).
|
||||
|
||||
## Testing
|
||||
|
||||
Add to `tests/`:
|
||||
- `tests/test_vote.py` — vote casting, changing, public broadcast, tally, tiebreak, zero-vote fallback
|
||||
- `tests/test_htl.py` — room clearing, regen ticks, checkpoint cluster detection, checkpoint establishment, floor boss spawn on cluster clear, floor unlock, Warden kill = win, rooms behind checkpoint immune to regen
|
||||
- `tests/test_boss_mechanics.py` — test each of the 12 mechanic implementations: armored, enraged, regenerator, stalwart, warded, phasing, draining, splitting, rotating_resistance, retaliator, summoner, cursed. Test phase scaling for raid boss.
|
||||
- `tests/test_raid.py` — HP scaling from active players, cap at 6000, regen, phase transitions at 66%/33%, contribution tracking, lockout mechanic, completion + rewards
|
||||
- `tests/test_rne.py` — objective claim, pursuer advancement (2:1 ratio), pursuer in carrier room triggers combat, carrier death drops objective, relay pickup resets pursuer, ward slows pursuer, lure diverts pursuer, blocker intercepts pursuer, win condition on town delivery
|
||||
- `tests/test_rne_broadcasts.py` — all R&E broadcasts fire correctly (claim, distance, blocker, lure, death, relay, victory)
|
||||
|
||||
Use in-memory SQLite for tests. All endgame tests should generate a proper epoch first (use epoch_generate with DummyBackend).
|
||||
|
||||
## Done When
|
||||
|
||||
All three endgame modes are playable end-to-end. A Hold the Line epoch can be won by clearing all floors and killing the Warden. A Raid Boss epoch can be won by depleting the boss HP pool through coordinated chip-and-run combat with mechanic discovery. A Retrieve and Escape epoch can be won through a relay of carriers with blockers, warders, and lures supporting. The epoch vote selects the next mode. All broadcasts route through DCRG. All responses under 150 chars, all tests passing. Commit and report.
|
||||
120
projects/mmud/mmud-prompts/mmud-prompts/06-phase6.md
Normal file
120
projects/mmud/mmud-prompts/mmud-prompts/06-phase6.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Task: MMUD Phase 6 — The Breach
|
||||
|
||||
## Before Writing Any Code
|
||||
|
||||
Re-read these sections of `/home/zvx/projects/mmud/docs/planned.md`:
|
||||
- The Breach — Mid-Epoch Event (Day 15) (all of it — 4 mini-events, endgame interaction, design rationale)
|
||||
- Breach secrets (the 3 breach-type secrets)
|
||||
|
||||
Also re-read `config.py` for: BREACH_ROOMS_MIN/MAX, BREACH_CONNECTS_FLOORS, BREACH_SECRETS, BREACH_MINI_EVENTS, EMERGENCE_HP, INCURSION_REGEN/HOLD_HOURS.
|
||||
|
||||
Phase 4 already generates the Breach zone (breachgen.py) and Phase 4's daytick.py already handles the day 15 trigger and days 12-13 foreshadowing. Phase 4's breach.py has basic state management. This phase wires the full runtime logic for all 4 mini-events.
|
||||
|
||||
## Phase 6 Deliverables
|
||||
|
||||
The Breach opens on day 15 with a random mini-event. Each of the 4 types plays differently. The Breach interacts with whichever endgame mode is active.
|
||||
|
||||
### 1. Breach Activation (verify/extend existing)
|
||||
|
||||
The day 15 trigger should already be in daytick.py. Verify it:
|
||||
- Day 12-13: barkeep foreshadowing broadcasts from DCRG: "The walls grow thin between the second and third depths. Something stirs."
|
||||
- Day 15: Breach opens. Set `breach.active = 1`. Open the room exits connecting Breach zone to floors 2 and 3. Broadcast from DCRG: "⚡ The ground splits. A new passage has opened between Floors 2 and 3. Strange light pours from within."
|
||||
- Players can now enter Breach rooms via the new exits from floors 2 and 3.
|
||||
- The permanent shortcut between floors 2 and 3 persists for the rest of the epoch.
|
||||
|
||||
### 2. Mini-Event: The Heist (mini Retrieve & Escape)
|
||||
|
||||
`src/systems/breach_heist.py` (new file)
|
||||
|
||||
- Artifact in the deepest Breach room, guarded by the Breach mini-boss.
|
||||
- Kill mini-boss → claim artifact. Carrier must bring it back to town.
|
||||
- Pursuer spawns (slower, Breach-only — only operates within the 5-8 Breach rooms + the floors 2-3 connection).
|
||||
- If carrier dies, artifact drops. Any player can pick up.
|
||||
- Relay mechanics same as R&E but compressed — 5-8 rooms, not 4 floors.
|
||||
- 3 Breach secrets scattered along the escape route. Found under pressure.
|
||||
- Completion: artifact delivered to town. Breach rewards distributed. Broadcast from DCRG: "🏆 The artifact has been extracted from the Breach!"
|
||||
|
||||
Reuse as much R&E logic from Phase 5 as possible — shared carrier/pursuer/relay patterns.
|
||||
|
||||
### 3. Mini-Event: The Emergence (mini Raid Boss)
|
||||
|
||||
`src/systems/breach_emergence.py` (new file)
|
||||
|
||||
- Creature with shared HP pool (500-800 HP, `EMERGENCE_HP_MIN/MAX`) sits in central Breach room.
|
||||
- Surrounding rooms spawn minions on a timer (respawn every 8 hours).
|
||||
- Same chip-and-run combat as bounties/raid boss. Regen at 3%/8h.
|
||||
- 3 Breach secrets are in the minion rooms — discovered while contributing to the kill.
|
||||
- Completion: creature HP reaches 0. Broadcast: "🏆 The Breach creature has been destroyed!"
|
||||
|
||||
Reuse raid boss combat framework from Phase 5.
|
||||
|
||||
### 4. Mini-Event: The Incursion (mini Hold the Line)
|
||||
|
||||
`src/systems/breach_incursion.py` (new file)
|
||||
|
||||
- Breach rooms start fully hostile. Regen at 2 rooms/day (`INCURSION_REGEN_ROOMS_PER_DAY`) within just 5-8 rooms.
|
||||
- Players must clear ALL Breach rooms and hold them all for 48 hours (`INCURSION_HOLD_HOURS`).
|
||||
- If any room reverts during the hold timer, the clock resets.
|
||||
- 3 Breach secrets behind the hardest rooms, found as part of the push.
|
||||
- Track hold start time in `breach.incursion_hold_started_at`. On each regen tick, check if any Breach room reverted — if so, reset the timer.
|
||||
- Completion: 48 hours with all rooms held. Broadcast: "🏆 The Breach has been secured! The incursion is contained."
|
||||
|
||||
Reuse HtL room clearing/regen logic from Phase 5.
|
||||
|
||||
### 5. Mini-Event: The Resonance (puzzle dungeon)
|
||||
|
||||
`src/systems/breach_resonance.py` (new file)
|
||||
|
||||
- No combat focus. Breach rooms contain environmental puzzles.
|
||||
- 3 Breach secrets ARE the puzzle rewards. Finding all 3 unlocks a bonus cache in the deepest room.
|
||||
- Puzzles are generated in Phase 4 (breachgen already places Breach secrets). This phase adds the interaction logic:
|
||||
- `examine` objects in Breach rooms triggers puzzle checks
|
||||
- Puzzle state tracked per-player in secret_progress
|
||||
- Sequence puzzles, item-interaction puzzles, cross-room clue puzzles (use the same multi-room puzzle archetypes from the main dungeon)
|
||||
- Completion: all 3 Breach secrets found by any player(s). Bonus cache unlocked. Broadcast: "🏆 The Resonance has been understood. The Breach yields its secrets."
|
||||
|
||||
Soloable by nature — knowledge not stats.
|
||||
|
||||
### 6. Breach Interaction with Endgame Modes
|
||||
|
||||
Regardless of which mini-event is running, the Breach benefits the active endgame mode:
|
||||
|
||||
- **Retrieve & Escape:** The Breach shortcut (floors 2↔3) becomes an alternate escape route. Carrier can path through it. Shorter but Breach content (mini-boss, minions, etc.) may still be there.
|
||||
- **Raid Boss:** Breach completion (any mini-event) drops a buff item granting +20% damage vs the raid boss for the rest of the epoch. Add to player inventory on Breach completion.
|
||||
- **Hold the Line:** Breach rooms count as bonus territory toward checkpoint progress on both floors 2 and 3. Cleared Breach rooms contribute to the cleared room count for both floor 2 and floor 3 checkpoints.
|
||||
|
||||
### 7. Breach Secret Integration
|
||||
|
||||
Verify that the 3 Breach secrets work with the existing discovery system:
|
||||
- `secrets` command includes Breach secrets in the count after day 15
|
||||
- Secret milestones (5/10/15/20) fire correctly with Breach secrets included
|
||||
- Barkeep hints for Breach secrets only available after day 15
|
||||
- Breach secrets contribute to the completionist reward (all 20 found)
|
||||
|
||||
## Rules
|
||||
|
||||
- All responses under 150 chars.
|
||||
- All Breach broadcasts route through DCRG.
|
||||
- Breach mini-event is always random (selected at epoch gen, never voted).
|
||||
- Reuse combat/territory frameworks from Phase 5 — don't duplicate code.
|
||||
- Breach content is inaccessible before day 15. Exits to Breach rooms don't exist until activation.
|
||||
- Use constants from `config.py`.
|
||||
- Commit after each mini-event works.
|
||||
|
||||
## Testing
|
||||
|
||||
Add to `tests/`:
|
||||
- `tests/test_breach_activation.py` — day 15 trigger, foreshadowing on days 12-13, exits open, Breach accessible, inaccessible before day 15
|
||||
- `tests/test_breach_heist.py` — mini-boss, artifact claim, mini-pursuer, relay, completion, secrets under pressure
|
||||
- `tests/test_breach_emergence.py` — shared HP pool, minion respawn, chip-and-run, completion, secrets in minion rooms
|
||||
- `tests/test_breach_incursion.py` — room clearing, regen within Breach, 48h hold timer, timer reset on revert, completion
|
||||
- `tests/test_breach_resonance.py` — puzzle interaction, secret discovery, bonus cache unlock, no combat required
|
||||
- `tests/test_breach_endgame.py` — R&E shortcut, raid boss damage buff, HtL bonus territory
|
||||
|
||||
Use in-memory SQLite for tests. Generate full epoch with DummyBackend for each test.
|
||||
|
||||
## Done When
|
||||
|
||||
The Breach opens on day 15 with one of four randomly selected mini-events. Each mini-event is playable end-to-end with its own win condition. Breach secrets integrate cleanly with the discovery system. The Breach interacts with whichever endgame mode is active. All broadcasts route through DCRG. All responses under 150 chars, all tests passing. Commit and report.
|
||||
|
||||
This is the final gameplay phase. After this, the full 30-day epoch loop is complete: epoch generates → players explore and progress → Breach opens day 15 → endgame mode pushes through days 20-30 → epoch vote → wipe → new epoch.
|
||||
32
projects/mmud/mmud-prompts/mmud-prompts/README.md
Normal file
32
projects/mmud/mmud-prompts/mmud-prompts/README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# MMUD Prompt Bundle
|
||||
|
||||
## Status
|
||||
- Phases 1-4: COMPLETE (265 tests passing)
|
||||
- Phases 5-6: Prompts ready
|
||||
- Design doc: NEEDS UPDATES (4 prompts below, run before Phase 5)
|
||||
|
||||
## Run Order
|
||||
|
||||
### Step 1: Design Doc Updates (run in CC in this exact order)
|
||||
These update `/home/zvx/projects/mmud/docs/planned.md` in place:
|
||||
|
||||
1. `01-update-planned.md` — Adds The Last Ember (bar + 4 NPC bios), NPC live conversations, command discovery
|
||||
2. `02-npc-nodes.md` — Replaces talk command with sim node architecture (6 nodes), three rule layers, onboarding funnel
|
||||
3. `03-darkcragg.md` — Names the dungeon "The Darkcragg Depths"
|
||||
4. `04-dcrg-node.md` — Adds DCRG as one-way broadcast node, separates broadcast stream from EMBR
|
||||
|
||||
### Step 2: Build Phases
|
||||
5. `05-phase5.md` — Endgame modes: Hold the Line (regen, checkpoints, floor bosses, 12 mechanics), Raid Boss (HP scaling, 12 mechanics, 3 phases), Retrieve & Escape (Pursuer, blockers, warders, lures), epoch vote
|
||||
6. `06-phase6.md` — The Breach: 4 mini-events (Heist, Emergence, Incursion, Resonance), endgame interaction, day 15 trigger. Final gameplay phase.
|
||||
|
||||
### If Needed
|
||||
- `mmud-project.md` — Drop into `/home/zvx/projects/.ref/projects/` if not already there
|
||||
|
||||
## What's Complete After Phase 6
|
||||
The full 30-day epoch loop: generate → explore → Breach day 15 → endgame push days 20-30 → vote → wipe → new epoch. All three endgame modes, all four Breach mini-events, 20 secrets, 40 bounties, floor bosses, raid boss, Pursuer + support roles.
|
||||
|
||||
## What Comes After Phase 6
|
||||
- NPC live conversations (LLM runtime for talk via sim nodes) — needs implementation prompt
|
||||
- Sim node deployment (meshtasticd with 6 identities) — needs infrastructure work
|
||||
- Last Ember spectator web dashboard — separate project, parallel track
|
||||
- Playtesting and number tuning
|
||||
48
projects/mmud/mmud-prompts/mmud-prompts/mmud-project.md
Normal file
48
projects/mmud/mmud-prompts/mmud-prompts/mmud-project.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# MMUD — Mesh Multi-User Dungeon
|
||||
|
||||
Text-based multiplayer dungeon crawler for Meshtastic LoRa mesh networks. BBS door games (LORD, TradeWars) adapted for 150-char mesh radio constraints, async play, 30-day wipe cycles.
|
||||
|
||||
## Status
|
||||
|
||||
**Phase:** Pre-development — design complete, repo scaffolded, implementation not started.
|
||||
|
||||
## Repo
|
||||
|
||||
`/home/zvx/projects/mmud`
|
||||
|
||||
The repo contains a `CLAUDE.md` with full architecture, directory structure, development phases, and implementation guidance. **Read it first before any implementation work.**
|
||||
|
||||
## Key Files
|
||||
|
||||
- `CLAUDE.md` — Architecture, patterns, dev phases, gotchas
|
||||
- `docs/planned.md` — Complete game design document (~950 lines). Source of truth for all mechanics. If code contradicts this, code is wrong.
|
||||
- `config.py` — All game constants with rationale
|
||||
- `src/db/schema.sql` — Full database schema
|
||||
|
||||
## Design Constraints
|
||||
|
||||
- 150 characters per Meshtastic LoRa message (hard ceiling)
|
||||
- Zero runtime LLM calls — all text batch-generated at epoch start
|
||||
- Async-first — all multiplayer through shared DB state
|
||||
- 12 dungeon actions/day, 30-day epochs
|
||||
- Python 3.11+, SQLite, Meshtastic Python API
|
||||
|
||||
## Development Phases
|
||||
|
||||
1. **Core Loop** — Meshtastic message handling, command parser, player creation, room navigation, basic combat, death, action budget
|
||||
2. **Economy & Progression** — XP, leveling, gold, shops, gear (weapon/armor/trinket), bank, healer
|
||||
3. **Social Systems** — Broadcasts (tier 1/2/targeted), barkeep (recap, tokens, hints), bounty board, player messages, mail
|
||||
4. **Epoch Generation** — World gen, LLM narrative pipeline (batch + validation), secret placement, bounty pool generation
|
||||
5. **Endgame Modes** — Hold the Line (regen, checkpoints, floor bosses), Raid Boss (HP scaling, mechanic tables, phases), Retrieve & Escape (Pursuer, blockers, warders, lures), epoch vote
|
||||
6. **The Breach** — Breach zone gen, 4 mini-event types (Heist, Emergence, Incursion, Resonance), day 15 trigger
|
||||
|
||||
## No Runbooks Needed
|
||||
|
||||
This is a pure software project — no LXC provisioning, no Caddy config, no Authentik integration. Runs as a Python daemon connected to a Meshtastic device via USB/serial or TCP. No infrastructure runbooks apply.
|
||||
|
||||
## Notes
|
||||
|
||||
- All regen/HP/damage numbers in the design doc are targets, not validated — will need playtesting
|
||||
- The game runs on a Meshtastic mesh network, not a web server
|
||||
- SQLite single file DB, no ORM, raw parameterized SQL
|
||||
- Every outbound message must fit 150 chars — the formatter is the final gate
|
||||
Loading…
Add table
Add a link
Reference in a new issue