Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5dd5efc
plan: ramble phase 3, the egg supply overhaul
Sep 9, 2026
a54b891
plan: fix nine issues from the first adversarial review
Sep 9, 2026
336af48
plan: fix thirteen issues from the second adversarial review
Sep 9, 2026
2bb2c68
plan: fix six issues from the third adversarial review
Sep 9, 2026
ea94b25
plan: revert the trades promote, offer the waiting egg instead
Sep 9, 2026
3262a3a
plan: fix four issues from the fifth review, close the review
Sep 9, 2026
ad6e78c
ramble: move the egg-lock predicate to a leaf module
Sep 9, 2026
ec64d0c
ramble: looking at a screen no longer mints an egg
Sep 9, 2026
9d71056
ramble: absent egg reads as null, not a phantom 0% egg
Sep 9, 2026
b1a22c0
ramble: the shelf refills the incubating slot, and hatching mints not…
Sep 9, 2026
92d92db
ramble: an eggless bird lays after sustained care
Sep 9, 2026
32c351e
ramble: one starter egg, once ever, and the two prologue flags
Sep 9, 2026
561c21a
ramble: the routes answer for a player with no egg
Sep 9, 2026
55be2aa
ramble: the panel answers for a player with no egg
Sep 9, 2026
1ef28fb
ramble: the shelf-waiting card no longer contradicts itself
Sep 9, 2026
fc09eba
ramble: the prologue, in the game's own voice
Sep 9, 2026
815b675
ramble: structurally test the prologue gate and hook point
Sep 9, 2026
e238c33
ramble 0.11.0: eggs are earned
Sep 9, 2026
b1e4f85
ramble: fix Warm it re-enable, sticky error text, and two stale comments
Sep 9, 2026
548267f
docs(ramble): correct guides on shelf refill, both languages
Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bundles/ramble/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ramble",
"name": "Ramble",
"version": "0.10.0",
"version": "0.11.0",
"type": "mcp-server",
"author": "Crow",
"category": "social",
Expand Down
2 changes: 1 addition & 1 deletion bundles/ramble/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "crow-ramble",
"version": "0.10.0",
"version": "0.11.0",
"description": "Ramble MCP server — proximity marks, caws, privacy grid, egg and bird companion, gifts and swaps",
"type": "module",
"main": "server/index.js",
Expand Down
34 changes: 31 additions & 3 deletions bundles/ramble/panel/ramble.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export default {

<p class="rb-big" id="rb-egg-percent">&mdash;</p>
<p class="rb-muted rb-fine" id="rb-egg-line">Reading the warmth&hellip;</p>
<p class="rb-muted rb-fine" id="rb-egg-empty" hidden>Nests hold them. So do friends.</p>
</section>

<section class="rb-card rb-hatch-reveal" id="rb-hatch-reveal" hidden>
Expand Down Expand Up @@ -307,9 +308,9 @@ export default {
</div>
</section>

<!-- The successor egg is minted the moment one hatches, and the perch
swaps to the bird for good — so without this the egg view (and
its daily check-in) would be unreachable after the first hatch. -->
<!-- Once a bird hatches the perch swaps to it for good — this card
is the only route back to the egg view (and its daily
check-in), so it must never be hidden. -->
<section class="rb-card" id="rb-pet-nextegg">
<p class="rb-eyebrow">Next egg</p>
<div class="rb-row">
Expand All @@ -324,6 +325,10 @@ export default {
<strong class="rb-big rb-grow" id="rb-nextegg-percent">&mdash;</strong>
<button class="rb-btn rb-btn-ghost" id="rb-see-egg" type="button">See egg</button>
</div>
<p class="rb-muted rb-fine" id="rb-nextegg-empty" hidden>Nothing warming just now.</p>
<p class="rb-muted rb-fine" id="rb-nextegg-lay" hidden></p>
<p class="rb-muted rb-fine" id="rb-nextegg-waiting" hidden>One&rsquo;s waiting on your shelf.</p>
<button class="rb-btn rb-btn-ghost" id="rb-nextegg-warm" type="button" hidden>Warm it</button>
</section>

<button class="rb-btn" id="rb-my-flock" type="button">${icon("flock")}My flock</button>
Expand Down Expand Up @@ -462,6 +467,29 @@ export default {
<div id="rb-ar-sheet-body"></div>
</div>
</div>

<!-- ─────────────────────────────────────────────── the prologue -->
<!-- Ramble's first narrative content (spec §4.4). Written from inside
the premise: you ARE the egg. Skippable; both beats set a flag
that replicates, and a game-state wipe clears them so the
prologue genuinely replays. -->
<div class="rb-prologue" id="rb-prologue" hidden>
<div class="rb-prologue-card">
<div id="rb-prologue-intro" hidden>
<p class="rb-prologue-lead">You are an egg.</p>
<p>You wandered off from your nest. Nobody is coming to look for you &mdash; that&rsquo;s alright. It happens more than you&rsquo;d think.</p>
<p>Nobody knows what&rsquo;s inside you yet. Not even you.</p>
<p>Go somewhere. That&rsquo;s how eggs get warm.</p>
<button class="rb-btn" id="rb-prologue-go" type="button">Go</button>
</div>
<div id="rb-prologue-hatch" hidden>
<p class="rb-prologue-lead" id="rb-prologue-hatch-lead">You&rsquo;re out.</p>
<p>You live on what you do: new streets, new faces, the small daily things. Keep moving and you stay bright. Go still long enough and you droop.</p>
<p>That&rsquo;s all that happens. Nothing here is ever lost.</p>
<button class="rb-btn" id="rb-prologue-seen" type="button">Have a look at yourself</button>
</div>
</div>
</div>
</div>

<script src="/ramble/static/leaflet/leaflet.js"></script>
Expand Down
46 changes: 42 additions & 4 deletions bundles/ramble/panel/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -805,15 +805,24 @@ export default function rambleRouter(dashboardAuth, options = {}) {
router.get("/api/ramble/pet", handle(async (req, res) => {
const now = Date.now();
// One read for the whole companion strip: mood/energy/chores, the bird
// that hatched (null until the first one does), and just the egg's
// progress percent — the panel's full egg card reads /api/ramble/egg.
// that hatched (null until the first one does), and the incubating egg
// (null if none exists) — the panel's full egg card reads /api/ramble/egg.
const pet = await mods.petMod.petState(db, { now });
const bird = await mods.eggsMod.activeBird(db);
const egg = await mods.eggsMod.eggState(db, { now });
const lay = await mods.eggsMod.layProgress(db);
// The egg sitting on the shelf that a tap would incubate, or null. Nothing
// auto-promotes it (see promoteFromShelf's note), so the card offers it.
const waiting = await mods.eggsMod.nextPromotable(db);
res.json({
...pet,
bird,
egg: { percent: egg.egg.percent },
// Task 2 (spec 2026-09-08 §4.1): no egg is a valid state now — a read
// must not crash for it, so an absent egg is reported as `null`, not
// dereferenced.
egg: egg.egg ?? null,
shelf_waiting: waiting ? waiting.egg_id : null,
lay,
seed: await mods.walletMod.seedBalance(db),
hearts: await mods.heartsMod.heartsBalance(db),
energy_max_cap: (await mods.heartsMod.readHeartSettings(db)).cap,
Expand All @@ -833,7 +842,32 @@ export default function rambleRouter(dashboardAuth, options = {}) {
// `eggState` and `creditWarmth` have NO internal `now` default (the db layer
// refuses an undefined argument), so every call from here passes one.
router.get("/api/ramble/egg", handle(async (req, res) => {
res.json(await mods.eggsMod.eggState(db, { now: Date.now() }));
// No promote here. A GET must never queue a sync op, and see
// promoteFromShelf's note: a read-path promote both races the drain and
// launders shelf_origin. hatchIfReady is the only local emptier.
const state = await mods.eggsMod.eggState(db, { now: Date.now() });
const lay = await mods.eggsMod.layProgress(db);
res.json({ ...state, lay });
// NOTE: no `shelf_waiting` here. The waiting egg is offered on the PET
// card (Task 7), which reads GET /api/ramble/pet; adding it to this route
// too would give the egg view a field it never paints, and the two
// surfaces would then disagree — the egg view saying "No one on the way"
// while the pet card offers one. One surface owns this affordance.
}));

router.get("/api/ramble/prologue", handle(async (req, res) => {
res.json(await mods.eggsMod.readPrologue(db));
}));

router.post("/api/ramble/prologue/intro", handle(async (req, res) => {
const egg = await mods.eggsMod.grantStarterEgg(db, { now: Date.now(), emit });
await mods.eggsMod.setPrologueSeen(db, "intro", { emit });
res.json({ egg: egg ? { egg_id: egg.egg_id, warmth: egg.warmth } : null, intro_seen: true });
}));

router.post("/api/ramble/prologue/hatch", handle(async (req, res) => {
await mods.eggsMod.setPrologueSeen(db, "hatch", { emit });
res.json({ hatch_seen: true });
}));

router.post("/api/ramble/egg/checkin", handle(async (req, res) => {
Expand All @@ -846,6 +880,10 @@ export default function rambleRouter(dashboardAuth, options = {}) {
credited,
warmth,
hatched: hatched ? { egg_id: hatched.egg_id, species: hatched.species, seed: hatched.seed } : null,
// Read AFTER feedAll deliberately: a check-in that hatches the last egg
// with an empty shelf reports false, and "nothing to warm yet" is then
// the true statement about what comes next.
egg: !!(await mods.eggsMod.getIncubatingEgg(db)),
});
}));

Expand Down
11 changes: 7 additions & 4 deletions bundles/ramble/panel/static/ramble-ar.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* every label is built with createElement + textContent, and the only markup
* this file ever mounts is the bird; a caller-built art element is appended, never parsed.
*
* Contract: renderAr({ anchors, pose, bird, camera }) -> frame. It knows
* Contract: renderAr({ anchors, pose, bird, camera, hasEgg }) -> frame. It knows
* nothing about maps, marks or nests. An anchor is
* { id, kind, lat, lon, accuracy_m, approx_m, locked, title, reach_m?, art? }
* and the frame says where each label goes as FRACTIONS of the viewport, so
Expand Down Expand Up @@ -226,6 +226,7 @@
visible: visible.map(function (it) { return it.id; }),
say: sayFor(mode, reason, items, visible, coarse.length),
bird: s.bird || null,
hasEgg: !!s.hasEgg,
};
}

Expand Down Expand Up @@ -372,10 +373,12 @@
}
}

function paintBird(bird) {
function paintBird(bird, hasEgg) {
var valid = !!(engine && bird && typeof engine.isValidBird === "function" && engine.isValidBird({ species: bird.species, seed: bird.seed }));
if (e.bird) setHidden(e.bird, !valid);
if (e.egg) setHidden(e.egg, valid);
// Phase 3: with no bird AND no egg there is nothing to draw. Without the
// hasEgg term this un-hides a phantom seed-0 egg on every frame.
if (e.egg) setHidden(e.egg, valid || !hasEgg);
if (!valid) { birdKey = null; return; }
var key = bird.species + ":" + bird.seed + ":" + (bird.mood || "happy");
if (key === birdKey) return;
Expand Down Expand Up @@ -432,7 +435,7 @@
e.more.textContent = more.join(" · ");
}
paintRadar(frame);
paintBird(frame.bird);
paintBird(frame.bird, frame.hasEgg);
if (e.say) e.say.textContent = frame.say;
var entered = false;
var nowVisible = {};
Expand Down
32 changes: 32 additions & 0 deletions bundles/ramble/panel/static/ramble.css
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,34 @@
#ramble .rb-meter-bar i { transition: none; }
}

/* ------------------------------------------------------------- the prologue */

#ramble .rb-prologue {
position: fixed;
inset: 0;
z-index: 1200;
display: grid;
place-items: center;
background: rgba(20, 22, 36, .5);
padding: 24px;
}
#ramble .rb-prologue[hidden] { display: none; }
#ramble .rb-prologue-card {
max-width: 30rem;
border-radius: var(--rb-radius);
padding: 22px 20px;
background: var(--rb-surface);
color: var(--rb-text);
border: var(--rb-line-w) solid var(--rb-line);
box-shadow: var(--rb-pop);
animation: rb-sheet-up .22s ease-out;
}
#ramble .rb-prologue-card p { margin: 0 0 12px; line-height: 1.5; }
#ramble .rb-prologue-lead { font: 800 1.15rem var(--rb-font-display); }
@media (prefers-reduced-motion: reduce) {
#ramble .rb-prologue-card { animation: none; }
}

/* ------------------------------------------------------------- narrow phones */

@media (max-width: 420px) {
Expand Down Expand Up @@ -1011,3 +1039,7 @@
#ramble .rb-ar-label.rb-ar-fx-collect,
#ramble .rb-ar-row.rb-ar-fx-collect { box-shadow: 0 0 0 6px color-mix(in oklab, var(--rb-accent-2) 55%, transparent); }
}

#ramble #rb-nextegg-empty,
#ramble #rb-nextegg-lay,
#ramble #rb-egg-empty { margin-top: 6px; }
Loading
Loading