feat(monetization): lock the cosmetic ladder (loyalty model) + free Original pack

Nails down the premium unlock system per the design pass:
- FREE `original` topdown pack (NEW): the game's bare tiles — floor/wall/door/
  torch/features only, no furniture, so the free tavern reads plain (the intended
  "reason to unlock"). Registered in assetPacks (RK.THEMES.original) as the free
  default topdown pack; plain ASCII stays free (accessibility).
- Loyalty ladder (lifetime credits PURCHASED, cumulative, NOT deducted → buying to
  play also unlocks cosmetics): Interior 1, skins 5, iso 10, +tilepack 20, +iso 40,
  3D 50. Each row is data (tilepack OR skin) → operator appends more at any rung.
  DEFAULT_CATALOG (entitlements.js) + migration 028 + MONETIZATION.md ladder table.
- Tests updated to the new thresholds (catalogEntitlements_ultra, directEntryCredit_ultra).

Note: this reprices roguelike-interior from free → 1cr, so once deployed the free
tavern becomes bare (by design). Deploy is bundled with the single-player render-kit
migration so the free-tier change lands with its payoff. Suite 61/432 green.
This commit is contained in:
jwinterm
2026-07-12 09:52:13 -04:00
parent d8abe89cb7
commit 42ed51af69
6 changed files with 101 additions and 23 deletions
+22 -1
View File
@@ -27,7 +27,7 @@ owns a parallel catalog.
```
1. it's free (tier 0, no gate)
2. an explicit grant (products → grants)
3. lifetime spend ≥ the pack's unlock_min_credits threshold
3. lifetime credits PURCHASED ≥ the pack's unlock_min_credits threshold
4. the user's premium TIER ≥ the pack's tier
```
@@ -35,6 +35,27 @@ owns a parallel catalog.
**Buying credits does NOT put you on the premium ladder** (`credits` → tier 0) — that was the old bug where
any purchase unlocked every premium pack.
### The ladder (loyalty model)
Clause 3 keys off **lifetime credits _purchased_**, cumulatively — it is **not deducted**, so buying credits
to *play* also walks you up the cosmetic ladder (cosmetics never compete with the core loop). Each catalog
row is just data — a **tilepack OR a char-skin**, at any credit rung — so the operator adds more of either by
appending a row (+ assets + `registerPack` for a client pack). This IS the "infinite packs" system.
| Rung (lifetime credits) | Unlocks |
|---|---|
| **free** | `original` bare tiles (no furniture → the "ugly tavern", by design) · plain ASCII |
| 1 | `roguelike-interior` (first premium tilepack) |
| 5 | `generated-skins` (character skins) |
| 10 | `iso-dungeon` — unlocks the **Iso** technique |
| 20 | `roguelike-dungeon` (another tilepack) |
| 40 | `iso-medieval` (second iso pack) |
| 50 | `kenney-3d-characters` — unlocks **3D** |
A render **technique** (Iso/3D) is available once the user has *any* unlocked pack for its projection, so the
cheapest pack in a projection is effectively that technique's gate. Plain ASCII stays free (accessibility).
Source of truth: `cosmetic_catalog` table (migration 028) mirrored by `DEFAULT_CATALOG` in `entitlements.js`.
## Premium subscription → cosmetics
`levelForUser(user)` returns the **higher** of the stored `premium_level` and an active