2026-04-14

Kunal chat — points shop, armor, sets

Exchange with Kunal last night and this morning. He’s reacting warmly to the leveling system and independently landed on the same design pressure we’re already feeling: points should trade for things people actually want, and so should member perks — so they need to be reconciled, not run as parallel programs.

His framing: “make every business an FYE”

Points-only item shop — items tradeable for points with no other acquisition path. The scarcity is the mechanic. This matches what we’re already doing with local-artist limited-run tees sold as armor in the points shop. He flagged the clothing-line-called-armor as a strong brand surface on its own.

What I shared back

  • Artist-collab tees as armor, points-shop only — confirmed direction
  • Exploring promo items where buying 1 or a set grants bonus stats; set completion and combo discovery as a progression loop
  • Current reconciliation:
    • WithFriends members can convert points → tangible items
    • Free users currently top out at a Discord role
    • Members earn points faster on purchases (tier-scaled)
    • Check-ins, events, and quests earn at a flat rate for everyone

His counter

Consider making item shop access itself a membership perk, rather than (or in addition to) the earn-rate multiplier. Different lever: gates the outlet, not the rate.

Thinking out loud

Two levers for member vs. free differentiation

Right now we pull the rate lever (members earn faster). Kunal’s suggestion is the access lever (members unlock the shop at all). These aren’t mutually exclusive and they mean different things:

  • Rate lever says: everyone plays the same game, members play it faster. Good for onboarding — free users see progress and can imagine the upgrade.
  • Access lever says: the good stuff is behind the wall. Stronger conversion pressure, weaker free-tier loop.

The current design (rate + Discord-role-only for free) is the softer of the two. Worth considering a hybrid: keep the flat earn rate on check-ins/events/quests (community participation is egalitarian), but gate some item-shop tiers or drops behind membership. Rare armor = member-only; common consumables = open. Matches how actual MMOs handle F2P vs. sub.

Sets and combos are the right gamification shape

Set completion is a known-good loop (Diablo, Destiny, Pokémon) because it rewards exploration of the catalog rather than raw spend. A customer chasing a 3-piece armor set is being pulled through three different SKUs they might not otherwise have seen. Combos let us design intentional cross-sells without them feeling like cross-sells — “this cookbook pairs with this apron for +2 hearth” is merchandising in RPG clothing.

The risk: sets that require specific items force inventory discipline we don’t have yet. Start with soft sets (any 3 items tagged “local-artist” = bonus) before hard sets (these exact 3 SKUs). Soft sets tolerate inventory churn.

Kunal as a signal, not a collaborator

Post-contract, he’s in ally/referral mode. This conversation is useful because he’s seeing the same design problems from his side of the indie-retail membership space and converging on similar answers — that’s validation the direction is right, not that we should coordinate roadmaps. Keep sharing the interesting stuff; don’t fold his suggestions into plans without independent reason.

Open

  • Decide: keep rate-only differentiation, or add tiered item-shop gating for members
  • Draft plan for sets/combos → gear-sets-and-combos
  • Catalog the current armor tees and confirm points-only pricing is documented somewhere durable
  • Flag to Carrie: Kunal’s FYE framing, see if it shifts how we think about the points-shop surface

Kunal follow-up — staff personalities, paid-vs-volunteer staffing

Second exchange same day. Kunal’s state: shipping nothing, doing 15 onboardings/week. Only feature built recently is an event scraper so customers can use the WithFriends upsell without manual event entry. He’s treading water on product.

His idea: staff personalities in-app

Pitch: the WithFriends “places to hang out” app surfaces staff personalities, not just venues. You walk into a bubble tea shop today because the person working the counter also plays Counter-Strike. Personalization in both directions — icebreaker-as-a-feature.

This maps cleanly onto the guildmaster identity we’re already building. Our staff wear identifiable tees, run named events, and get promoted by name on the event listings. The reason people sign up for a given game isn’t the game — it’s the GM. That’s the shape Kunal is describing, we just got there from the RPG side instead of the social side.

If we take his framing seriously, the product implication is: staff profiles as first-class surfaces in Guild. Bio, interests, what they run, what they’re into outside the shop. Not an org chart — a roster. Low-lift to prototype; the content already exists in people’s heads and on Discord.

His staffing claim: tips/upsell make volunteering work

Kunal’s hard prior: pure volunteer staffing is brutal, even $30-50/event via tips or upsell donations unlocks a very different willingness. He credits transparent money flow — not the amount, the clarity. His evidence is 2010 Babycastles.

I pushed back implicitly by describing our actual model:

  • Paid events: staff/creatives are billed as the draw, they get paid, event is promoted under their name. This is not the volunteer-tip problem he’s solving.
  • Volunteer layer: Discord moderation, and participants helping host things they were already attending (e.g., board game night at a member hangout). That’s not “volunteer staffing” in Kunal’s sense — it’s participants doing participant things.

So we’re not ignoring his point, we’ve routed around it. The open question is whether there’s a third tier between “paid creative GM” and “member helping out because they’re there anyway” — something like a stipend-tier for recurring volunteer mods who put in real hours. Worth revisiting if the volunteer-mod program scales past a few people.

First member hangout happened

Logged: first member hangout produced signups, I demo’d the NFC kiosk to members there, and we taught new members to play Magic. This is the first real proof the hangout format pulls conversions, not just retention. Worth keeping as a data point before we over-invest in the format — one event isn’t a pattern.

Upcoming physical presence: big events at Liberty Science Center and the library. These are the tests for whether the guildmaster-in-uniform thing reads outside the shop.

Take

Kunal’s two ideas this round are different quality:

  • Staff personalities in-app: aligned with where we’re going, product-shaped, actionable. Good signal.
  • Pay volunteers small amounts: right answer for his 2010 model, wrong framing for ours. Our paid-event staff are already paid; our “volunteers” aren’t staff. Don’t let the anecdote pull us toward building a stipend system for a problem we don’t have.

Same pattern as this morning: he’s useful as a convergent second opinion on things we’re already chewing on, misleading when he’s pattern-matching to his own past.

Open

  • Guild: scope staff profile pages (bio, interests, events they run) — low-lift prototype
  • Document volunteer-mod program as it stands; revisit stipend question only if it scales
  • Track conversion/retention signal from member hangouts — need >1 data point before drawing conclusions
  • Liberty Science Center + library events: post-mortem template for whether guildmaster identity reads outside the shop

Dispatched to Carrie (3:31 PM)

Pitched the staff-personalization idea in my own voice, framed around the existing presence feature:

kunal suggested the idea that personalization should also include staff. like when a GM is running an event at the shop, we update their profile on guild — who they are, what they run, what they’re into. privacy features will still work here. but it’s stuff like — how do ppl know i’m going to be at the shop today

Framing choice worth noting: I led with the member discovery problem (“how do ppl know I’m going to be at the shop today”), not the product spec. That’s the right entry point for Carrie — she operates the floor, she feels the gap between “Nachi is here” and “anyone knows Nachi is here.” Presence-for-staff is the answer to a question she already has.

Privacy caveat was explicit — not every staff member will want to broadcast. Opt-in, same model as member presence.

Carrie’s response

Immediate validation and she sharpened it:

oh yeah maybe they wanna chat with u about a certain thing cuz they know you’re into it versus like lan it’s kinda a letdown if they come to the shop excited about something but ur not there

This is the clearer articulation of the feature’s value. Not “staff have personalities” — staff presence sets expectations. If a member comes in expecting to talk to me about something I’m into and I’m not on shift, that’s a worse experience than if they never expected me at all. Presence-for-staff is the fix for a bad-surprise failure mode Carrie has observed on the floor.

Also a data point: Carrie shops at the store, Lan rarely does. So Carrie has firsthand read on the member-shopping experience; Lan doesn’t. Weight her opinion accordingly on retail-floor questions.

Side thread: Staff vs Member account separation

Came up naturally from the staff-presence discussion. Right now Carrie and I each have a single account that’s both a member account and a staff account. Carrie flagged the privacy issue immediately:

hmmm that’s something to think about i guess there’s some personal shopping stuff id wanna keep private

Agreed direction: separate Staff accounts from Member accounts. Staff role carries admin-dashboard access; member role is personal/retail.

The GM wrinkle

The model isn’t two-tier, it’s three:

  1. Staff: paid, admin dashboard access. MMO analogue = actual employees.
  2. GM: runs events, identified in Guild, not staff. MMO analogue = volunteer GMs (the old-school kind, before they all got outsourced).
  3. Member: participant, no role privileges.

Example I gave: Tom. Tom runs events. Tom is not an employee. Tom is a GM. He needs a role that surfaces his presence/identity to members without granting admin access. This is distinct from the staff-presence feature Kunal pitched — that was about employees on shift. GMs are a separate role that overlaps in presence surfacing but not in permissions.

Implication for account model

If roles are separable (Staff, GM, Member) and Carrie/Nachi currently conflate Staff+Member in one account, the account model needs to support either:

  • One user, multiple role-scoped views (member mode vs. staff mode, with privacy isolation), or
  • Separate accounts entirely

Carrie’s privacy concern (“personal shopping stuff id wanna keep private”) is the real requirement. Whichever architecture solves that is fine. Multi-account is simpler to reason about; role-scoped views are better UX but harder to get privacy-correct.

Side thread: email transition

Moving primary email to panat@dungeonbooks.com. Spinning down scriptwizards. Flagged to Carrie in passing, not a discussion point yet. Implications for anything bound to the scriptwizards email (accounts, receipts, contracts) — separate audit task, not for this journal.

Open

  • Design: Staff vs GM vs Member role model — write up in a plan note
  • Decide: separate accounts vs. role-scoped views for privacy isolation
  • Audit: services/accounts currently bound to scriptwizards email before shutdown
  • Reframe the staff-presence pitch around Carrie’s “bad-surprise” framing — that’s the real value prop, not “personalities”

Discord: created dungeon-club channel

Decision: created a dedicated channel for dungeon.club in the 277-member bookstore Discord. Initial reasoning was wrong — I was thinking about it as a support-channel-for-12-users problem. Real frame: the 265 Discord members who haven’t seen Guild yet. A channel is a passive recruitment surface that general chat can’t be.

Dropped 4 invite codes in the opening post. Drafted the intro around:

  • What it is (plain language, not jargon)
  • Why (members deserve a place, not just a mailing list)
  • What this channel is for (announcements, bugs, suggestions)
  • Invite codes
  • How to get one if those are gone (visit the shop — invite-only is on purpose)

Framing choice: invite-only as a feature, not an apology. Matches our BBS / validation-message frame.

Pinned-message content (evergreen: what Guild is, how to get an invite, known issues) was left as a follow-up. First post is an announcement; the pin should be the durable doc. Writing the pin separately lets it be updated without editing a post that scrolls away.

Actual posted message (3:56 PM)

hey y’all. spinning up a channel for dungeon.club

if you’ve visited the shop sometime in the past week, you may have seen a vertical monitor with a leaderboard and events board.

what it is: our member portal. sign up with an invite code (be sure to use the same email you gave us at the shop). once you’re in, check in at the shop, earn points, see upcoming events and the leaderboard.

why a portal: members of a third place should have an online space that feels like yours. long term I hope this can grow into a network of other indie shops too.

this channel is for: announcement when we ship major stuff bug reports (stuff not working, weird stuff) suggestions and feedback. I’m trying to turn this into a game with quests, skills, items, etc.

Framings I hadn’t used in the draft that are sharper:

  • “Vertical monitor with a leaderboard and events board” — physical-world anchor for the 265 browsers who’ve been in the shop recently. “Oh, that thing” is a stronger hook than any abstract pitch.
  • “Same email you gave us at the shop” — concrete onboarding instruction that heads off the confusion about which email account to use. Should go in the pin too.
  • “Third place” — replaces my “place that’s yours” with the Oldenburg concept. Richer, and the Discord audience is the kind of audience who’ll recognize the term.
  • “Turn this into a game with quests, skills, items, etc.” — explicit game framing. My draft implied it; this states it. Sets expectations that this is deliberately gamified, not accidentally gamified.

Activity feed: scoping decision pending

Ran through the scoping options for the next build target. Three shapes:

  1. Global guild activity — everyone sees everyone’s check-ins, level-ups, events, purchases (anonymized/flavored). High social-proof, leaderboard-adjacent. Privacy-sensitive: becomes a public historical log of when people were in the shop.
  2. Personal activity — just your own history. Safer, less social.
  3. Hybrid — personal journal (private to member) + curated “hall feed” of opt-in public events (level-ups, achievements, big milestones) with timing specificity stripped.

Leaning option 3. Global raw check-in history broadcasts “Nachi was in the shop Thursday 4:47 PM” which is creepier than member-presence (which shows who’s currently there, not when they were there). Stripping out a milestone-only public feed preserves the social proof without becoming a stalker log.

Event types in v1: check-ins, level-ups, achievements (don’t exist yet as a system but level-ups alone justify the feed). Narrow is good.

Reuse SSE infra from PR #96 (presence). Don’t build new plumbing.

Retention window: undecided. 30/90/infinite all have privacy and UI implications.

Flavored vs. literal text: probably flavored for public feed (“Nachi entered the hall”) and literal for personal history (“checked in at 4:47 PM”).

Ship order: personal history first (lower stakes, validates the data model), global milestone feed second (after seeing what the data looks like). Not both at once.

Name level: two members about to cross

Kris (Mage) and Allan (Rogue) are about to hit level 9. They are our top 2 lifetime spenders: ~1.4K. This creates a real deadline — whatever happens at name level needs to exist before they reach it, or the moment is anti-climactic.

Reward framing correction

Initial instinct was “GM eligibility is the name-level reward.” Corrected after realizing these are top-spending customers, not aspiring volunteer staff. “You’ve earned the right to run Magic nights for us” reads as asking them to work, which misreads what engaged spenders actually want. They want recognition as patrons, not deputization into labor.

Better framing: name level unlocks access, not labor. Titles, a stronghold, pre-access, curation input, input on the shop — none of which are work.

3D print ritual (MVP)

Proposed to Carrie: 3D-print a small building for each as their stronghold. Kris gets a wizard tower, Allan gets a thieves’ hideout (class-specific per BECMI). Carrie’s response: “that would be so cute actually.” Validated.

Why this beats a flat digital badge:

  • Tangible, persistent, not pixels
  • Unique per class (trivially possible with 3D printing)
  • Scales to a shelf-collection — every name-level member going forward gets one. Five years from now the shop has a miniature town built by its most engaged members. That’s a permanent artifact and a better marketing surface than any landing page.

Naming decisions

  • Avoid “Maga” (BECMI-canonical female Mage title). Political collision is unavoidable regardless of gender.
  • “Wizard” as single gender-neutral title for Mage, matching how “Master Thief” already works. Offer alternatives (Sorcerer, Thaumaturge, Conjurer) as a menu at ceremony — member picks.
  • “Rogue” base class is kept (modern D&D convention). At name level, Rogue who settles becomes Guildmaster; Rogue who travels remains Rogue. Clean.

Settle vs. travel is the real moment

Name level in BECMI isn’t just a title bump — the player chooses settle or travel, each with different consequences. The choice itself is the ritual. Present the choice at the in-person ceremony.

Design captured

Full design written up in name-level. Captures: threshold, settle/travel, class-specific strongholds, 3D print ritual, apprentice mechanic (deferred to V2), construction-as-GP-sink (deferred to V3), title handling, timeline.

Immediate action

  • Audit Kris and Allan’s lifetime spend vs. current XP this week. If under-credited from pre-Guild purchases, backfill. They may already be past 9 by real contribution.
  • Source STLs and decide in-house print vs. commission this week. 8–20 hours per detailed building print. Tight timeline.
  • Decide scale (28mm D&D standard recommended) and lock it for shelf coherence.
  • Design the in-person ceremony script so Carrie and other staff can run it when I’m not there.

XP design inspiration from Rules Cyclopedia

Pulled RC’s experience chapter as reference. Most of it doesn’t transfer (monster XP, combat mechanics, immortality paths). Three things do:

  1. Exceptional Action staff bonus. Staff-discretionary, capped, one per session, size = 1/20 of next-level gap. Solves “how to reward cultural contribution that the automated system can’t see.” This is the single most valuable import — gives staff a tool for recognizing qualitative engagement (teaching a newcomer Magic, bringing a friend in, handling a service moment well). Ties into the Staff/GM/Member role model work (different roles could have different bonus award authority).
  2. Not all money is XP. RC: “salaries don’t count, only money from dangerous or challenging experiences.” Magical item sales cap at 10% of creation cost. Translation: be explicit about what does and doesn’t earn XP — subscription renewals, store credit redemptions, gift cards, membership fees. Every ambiguity is a future complaint.
  3. Retroactive progression. RC Step 4–6 (high-level character creation) is for players joining existing campaigns. We have the exact equivalent problem: customers who shopped for years before the Guild existed. If their lifetime spend converts to XP, they may be under-credited right now. Directly applicable to the Kris/Allan question above.

Skipped:

  • Hard caps (max XP per day, max level per month). Worth doing eventually, not urgent.
  • Immortality paths (Dynast/Hero/Paragon/Polymath). Interesting as V5 trajectory framework; notes-only for now.

Capture location: these belong as extensions to rpg-loyalty-system-design (already has the canonical XP curve and earn rates). Deferred to a focused edit pass — grafting on mid-brainstorm would be sloppy.

Evening: WithFriends migration reconciliation + issue #66

Ran prod data audits and writes in one session. All commits on branch fix/refund-handling-66.

WithFriends → Guild migration reconciliations

Three members migrated from WithFriends to Guild in the last ~month. The old backfill awarded their full Square history at 1×, ignoring that they were paying loyalty members (WF Bronze / WF Silver) during the WF-active window. Wrote scripts/reconcile-member.ts to:

  • Fetch the member’s full Square orders,
  • Segment by tier-history dates,
  • Update existing Purchase rows with the correct earnMultiplier and pointsEarned,
  • Create any missing Purchase rows,
  • Issue one adjustment LoyaltyTransaction for the XP/points delta,
  • PATCH the member record.

Idempotent via metadata.kind: 'member-reconciliation'.

Applied:

  • Brian Long (Bronze, WF-signup 2026-03-24): +1,151 XP. 20,070 → 21,221.
  • Sean Buckley (Silver, WF Bronze 2026-03-12 → WF Silver 2026-03-20): +7,634 XP. 51,419 → 59,053.
  • Allan Abanilla (Bronze, WF-signup 2026-03-26): +15,673 XP. 139,434 → 155,107. This corrected his Square customer ID drift (merge on 2026-04-09 had detached his old customer record from Payload) — user had fixed the ID to XFB1744ASH7ZHBQ5TWX1FH5ZCC before the reconciliation ran.

No refunds found across the three during this pass.

Issue #66: refunds still award loyalty points — fixed

Nathaniel flagged this after his 2025-10-01 95.95 across 4 refund events:

  • Kris (2, 21.32)
  • Sean (1, $15.98)
  • Nathaniel (1, $26.66)

Allan and Brian had zero refunds.

Multiple layers of bugs uncovered en route

Each of these had to be fixed separately.

  1. Square’s order.refunds is never populated for card payments. Card refunds live on the Payment object, not the Order. The old backfillSquarePurchases and the initial reconcile script both read order.refunds and got empty arrays — silently under-correcting. Fix: loadRefundsByPaymentId(locationId) pulls the Refunds API once and builds a payment_id → refund_cents map; backfill now subtracts using tender payment_ids per order.

  2. refund.order_id ≠ the originating order’s ID. Square creates a separate “refund order” for each card refund, and refund.order_id points to that, not the original purchase order. Only payment.order_id (fetched via payments.get(refund.payment_id)) reliably maps back. Fixed in both the webhook and the historical-refunds script.

  3. My first pass at applyRefundReversal only reversed points, not XP. I’d read the LoyaltyTransactions xp field comment (“always ≥ 0, XP never decreases”) as a business rule and wrote an explanation to match. Panat called it out: big-purchase-then-refund is a trivial exploit to level up for free. Reworked to reverse XP too — scaling the original purchase transaction’s xp (which may include buff contribution) proportionally to the refunded fraction. For backfilled purchases with no per-purchase tx, XP reversal equals points reversal.

  4. Historical reversal required two passes. The first run of reverse-historical-refunds.ts (before the XP fix) only reversed points on the 4 refunds. Second pass used --apply-xp-correction to append an xp-only adjustment per refund.

Post-fix state

MemberXPPointsLevel
Kris219,992219,9928
Sean55,85754,3776
Nathaniel7,1236,2833 (dropped from 4)

Nathaniel’s level drop is the correct behavior — his $26.66 refund was a full refund and the XP for that purchase had crossed the level-4 threshold. System now reflects his real earned-and-retained contribution.

Kris’s name-level gap widened slightly (from 14,677 to 20,008 XP from name level at 240K), because the two refunds he didn’t have credit for are now properly un-credited. Still the imminent name-level candidate but the threshold is a bit further than I’d previously quoted.

Known exploit captured for later

Even with XP reversal in place, there’s a residual exploit: buy big → redeem points for store credit → refund before the refund window closes → keep the store credit while the points balance goes negative. The negative-balance guard in redeemPoints prevents recurring abuse (member can’t redeem again until the debt is repaid) but doesn’t recover the already-issued store credit.

Dungeon Books has a 7-day refund window, so the fix is a 7-day redemption hold: points earned from a purchase become redeemable only after 7 days have elapsed since the purchase. Captured in known-exploit-redemption-refund as a known vulnerability. Scope: hard blocker before scaling invites past ~50 members and before any on-chain / Solana redemption path, because on-chain the negative-balance recovery property doesn’t hold cleanly.

Panat told Carrie about the exploit so operational awareness is in place.

Commits on fix/refund-handling-66

  1. chore(scripts): add reconcile-member and sync-member-to-square
  2. fix(loyalty): reverse points on Square refunds — initial webhook + backfill refund handling
  3. chore(scripts): add reverse-historical-refunds
  4. fix(loyalty): reverse XP on refunds, fetch card refunds via Refunds API — corrects the above when XP-reversal gap surfaced, and when order.refunds limitation surfaced
  5. fix(loyalty): address copilot review comments — partial-refund proportional scaling in historical script, rollback restores from pre-update snapshots instead of inverse deltas, reconcile-member.ts allows negative xpDelta, dead code removed

PR #104 + review

Shipped as PR #104 with Copilot as reviewer. 5 inline comments, all legitimate. 4 fixed in c1c546c; 1 (wrap multi-write sequence in a DB transaction) deferred to issue #105 because it’s architectural and overlaps with a broader direction we want anyway: move off raw Drizzle access onto Payload’s local-API transactions, narrowing prod DB access to just Payload itself. The pre-value snapshot + rollback in this PR is the stopgap.

Issue #105 — “Migrate from raw DB access to Payload local API (with transactions)”: captures the broader migration. Scope: awardPoints, redeemPoints, applyRefundReversal, backfillSquarePurchases, and any scripts that currently reach into payload.db. Acceptance: no new getDb(payload) calls outside migrations and explicit admin-only tools.

Files changed

  • src/lib/loyalty.tsapplyRefundReversal (new), reverses points + xp + recomputes level
  • src/lib/square-customer.ts — backfill loads refund map from Refunds API, handles card refunds
  • src/app/api/webhooks/square/route.ts — routes refund.updated / refund.created to handleCompletedRefund
  • src/collections/LoyaltyTransactions.ts — xp field description updated to acknowledge negative values
  • scripts/reconcile-member.ts — new, generic WF → Guild migration tool
  • scripts/sync-member-to-square.ts — new, manual member-state sync utility
  • scripts/reverse-historical-refunds.ts — new, historical refund reversal tool with --apply-xp-correction
  • tests/unit/loyalty.test.ts — +5 refund reversal tests + signature update
  • tests/unit/square-customer.test.ts — +4 backfill refund tests

Open questions / follow-ups

  • PR on fix/refund-handling-66 branch — write description, list the 4 commits, link to this journal entry
  • Redemption-hold mechanic — ticket / plan note before invites scale
  • Fulvio, Chris, John, Ximena — when they sign up on Guild, same WF migration template applies (use reconcile-member.ts with their WF signup dates)
  • Consider: should reconcile-member.ts ALSO subtract refunds during reconciliation? Currently doesn’t; relies on reverse-historical-refunds.ts being run as a companion. Easy to forget. Could fold both into one tool.

Dungeon Crawler Carl RPG — BackerKit launched

Renegade Game Studios launched the DCC RPG BackerKit campaign (“Unstoppable”) today. Already at $4M day one.

https://www.backerkit.com/c/projects/renegade-game-studios/dungeon-crawler-carl-rpg-unstoppable

  • Backed personally at $490 tier
  • Also went in as a retailer

Implications

  • Retailer backing = DCC RPG stock for the shop at fulfillment. Track fulfillment date when it’s posted; folds into inventory/receiving planning.
  • DCC is adjacent to our core audience (LitRPG / dungeon-crawler-curious readers). The books already sell. The RPG expands the handsell → event → demo-table pipeline.
  • $4M day one is a signal: there’s a live audience willing to buy into the DCC-adjacent ecosystem right now. Worth thinking about whether we run a DCC-themed event or display window while the campaign is hot (launch-week attention is the cheap marketing window, not fulfillment day).
  • Retailer tier specifics (MOQ, margin, promo kit, release-date exclusivity) — need to capture from the retailer portal/pledge details and put in a references note when confirmed.

Open

  • Log retailer tier details (MOQ, wholesale terms, estimated fulfillment) in a references note once confirmed
  • Decide: DCC-themed event or display while campaign is live — cheap attention window
  • Confirm DCC book stock is current; campaign backers are the exact audience who’ll walk in asking for the novels

Guild aesthetic: leaning 1-bit roguelike

Shift (or narrowing) from the cassette-futurism direction captured in guild-visual-direction toward a 1-bit pixel art treatment — two-color tiles in the Nethack / Brogue / Caves of Qud lineage. Trigger was a Dicegoblin blog post on pixel art for VTTs; asset pack candidates from itch.io captured in guild-1bit-aesthetic.

The honest tension

Cassette futurism and 1-bit roguelike are adjacent, not identical. Cassette futurism is pen-and-ink module art + CRT phosphor + printed-manual density. 1-bit roguelike is pixel-grid sprites + bitmap fonts. Both can read as “retro,” but they produce different artifacts and require different skills. Drifting between them will look incoherent.

Decision owed: pick one as primary, or define clean surface ownership (print surfaces = module art, screen surfaces = 1-bit). Don’t mix ad-hoc. Captured as an open question in the aesthetic note.

Why 1-bit is attractive right now

  • Off-the-shelf asset packs cover the whole visual domain (goblins, tiles, towns, UI) — no commissioning needed to launch.
  • Survives every output medium the shop uses (receipt printer, thermal label, B&W zine photocopy, vertical monitor, NFC card art).
  • Roguelike lineage maps onto the RPG framing of Guild (classes, dungeons, name-level strongholds as pixel towns).
  • 3D-printed strongholds from name-level get a natural digital twin: 1-bit pixel buildings populating a shared “guild town” on the portal as members hit name level. Physical + digital parallel.

Risk

Pixel art looks great in promos and terrible at wrong DPI. Needs a readability test at the actual kiosk viewing distance and the actual vertical-monitor DPI before it’s locked in. Asset-pack mixing across authors is also stylistically fragile — grid sizes and sprite proportions must match or the whole thing reads as a ransom note.

Open

  • Reconcile 1-bit lean with cassette-futurism direction in guild-visual-direction — pick primary, or define surface ownership
  • Pick 1–2 asset packs and commit (see guild-1bit-aesthetic for the evaluation list)
  • License audit on chosen packs (commercial use: kiosk, NFC cards, in-shop signage)
  • Readability test at actual kiosk DPI before locking

Reframe: top-down pixel as the OutsideRPG world

Later in the day: the aesthetic direction is no longer an abstract “retro vibe” question. It’s the rendering of outsiderpg-platform-vision — the game world in “real world = game world” has to actually look like something, and what it should look like is top-down retro pixel in the Pokemon Gold / Earthbound / Stardew lineage.

Ship targets in phases:

  1. Phase 1 — top-down view of Dungeon Books interior. One composed scene, renders on kiosk + portal, members drop in as sprites on check-in.
  2. Phase 2 — JC streetscape. Each participating shop = a building on the map. Tap a shop, enter its Phase 1 interior.
  3. Phase 3 — multi-city, deferred.

This is the correct framing because it converts aesthetic debate into a concrete deliverable. Phase 1 is tractable with off-the-shelf assets (Demonic Dungeon Building Interior is a near-perfect fit). Phase 2 has a real gap — no pack does modern urban top-down well.

Correction on the 1-bit framing from this morning: strict 1-bit is the wrong treatment for the world map. 1-bit rendering of a real modern city reads as hostile/alienating. Colored limited-palette pixel (Mystic-16 / Cartoon-Candy / Fruitpunch24 range) matches the Pokemon/Earthbound lineage that makes “real place as game place” feel welcoming instead of brutal. 1-bit stays valid for print (receipts, labels, zines, signage) where color doesn’t help and stark graphic treatment reads better.

Clean split captured in guild-1bit-aesthetic: colored top-down for the product world; 1-bit for print and merchandise; cassette futurism (guild-visual-direction) for manual-style collateral.

Open

  • Pick UI kit: Demonic Dungeon UI (matches scene, recommended) vs. Chroma Noir UI (graphic contrast)
  • Phase 1 scene composition — list fixed shop elements (counter, shelves, event table, kiosk, bathroom, back room, entryway) and map Demonic Dungeon Interior tiles to each
  • Phase 2 stylization decision (fantasy-village JC / hunt modern pack / commission storefronts) — defer until Phase 1 ships
  • Update outsiderpg-platform-vision to reference rendering direction done

Stack decision for the game layer: PixiJS + @pixi/react

Worked through Haxe, Heaps, HaxeFlixel, MelonJS, Phaser, PixiJS, Godot, Bevy, Defold, Excalibur.js with Claude across a long thread. The decision landed on PixiJS + @pixi/react after a creative-direction reframe (see next section).

Constraints that narrowed it:

  • Minimal and fast. First game. Not trying to be cross-platform yet.
  • One language in the codebase (TypeScript). Open source. Live in code, not an external editor.
  • Tight interop with Guild (Next.js / Medusa) because the game and the commerce platform are the same product, not separate.

What got eliminated and why:

  • Haxe/Heaps, Bevy, Defold, Godot: all pull in non-TS languages or external editors. Break the “one language” and “live in code” constraints.
  • bracket-lib, rot.js, malwoden: all dormant (last activity 2022). bracket-lib also optimized for ASCII/CP437 roguelike, wrong aesthetic register.
  • Phaser: strong contender; lost to PixiJS + @pixi/react because the product is an interactive UI (not a game with physics/combat) and @pixi/react gives us first-class React-composition of pixi elements. Phaser’s Scene/Game lifecycle is a seam we don’t need.
  • Excalibur.js: pre-1.0 (breaking changes expected) + smaller community. Ruled out despite being TS-native.

Full analysis in game-engine-choice.

Creative reframe: “a wizard shopping on eBay”

Mid-discussion I stopped thinking about “a game” and reframed the whole aesthetic. The vision is ambient fantasy-pixel layer across the entire app — not a discrete game view sitting next to the dashboard. Web development in a fantasy world. Magic and technology intersecting at every surface.

Concrete examples:

  • Pixel sprites drifting through page headers as you scroll
  • Cursor trails with sparks
  • Loading states as arcane sigils
  • Empty states showing a sleeping dragon on a pile of inventory
  • Transitions between pages that feel like moving between rooms

The top-down shop interior is ONE strong instance of this treatment. The treatment itself is app-wide.

Why this matters for the stack

@pixi/react is designed to mix Pixi-rendered elements with React DOM at any level of the tree. Ambient pixel layers alongside product grids is literally what it’s for. Phaser expects to own a canvas; it doesn’t fit “pixel sprites drifting through a product listing header” as cleanly.

Reference lineage

  • Duolingo owl — character pervades the product without always being on-screen
  • Animal Crossing UI — menus, cursors, transitions carry the world’s character
  • Stardew Valley shop menus — inventory UI rendered in the world register

The risk I flagged back to myself

Delight-as-friction. Every animated sprite is attention the user spends not-shopping. Working constraint going forward: every animation must serve feedback, continuity, or identity. No decoration-only sprites. If it doesn’t do one of those three jobs, cut it.

Also: GPU budget on mobile. Dev-only perf overlays on from day one. Respect prefers-reduced-motion.

Captured in guild-1bit-aesthetic (updated) and game-engine-choice.

Open

  • First prototype surface: pick ONE place in the Guild portal to drop an ambient pixel sprite (header? cursor? loading state?) to validate the treatment end-to-end before scoping the shop-interior scene
  • Establish perf overlay + motion-reduce pattern before the first sprite ships
  • Articulate the “feedback/continuity/identity” rule as a design-review checklist for future animations