mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
feat(catalog): graduated cosmetic ladder + per-session test-unlock
Step 1 — real tiered unlocks (was flat: any credit unlocked skins+iso+3D):
- entitlements DEFAULT_CATALOG + migration 025 graduate the ladder — unlock by
lifetime credit spend OR subscription tier, whichever first:
Fancy (generated-skins) 1 credit / supporter (tier 1)
Iso (iso-dungeon) 10 credits / premium (tier 2)
3D (kenney-3d-characters) 25 credits / operator (tier 3)
- Migration 025 UPDATEs the already-seeded 024 rows (idempotent).
- Tests updated to assert the ladder (1cr -> Fancy only; 10 -> +Iso; 25 -> all).
Step 2 — test the premium modes without paying:
- renderModes: ?unlock=1 (sticky via localStorage) flips RENDER_MODE_TEST_UNLOCKS
for THAT browser only, so Fancy/Iso/3D are all clickable; ?unlock=0 turns it off.
Suite 57 suites / 414 tests green.
This commit is contained in:
@@ -16,9 +16,16 @@
|
||||
];
|
||||
|
||||
RK.entitlements = RK.entitlements || { premium: false, level: 'free', packs: {} };
|
||||
// Production QA switch: set to true to temporarily bypass premium render-mode gating for
|
||||
// testing. Default false — premium modes (Fancy/Iso/3D) gate behind pack entitlements.
|
||||
// Per-session QA bypass for premium render-mode gating (Fancy/Iso/3D). Default OFF. A tester
|
||||
// opts IN for their own browser only via ?unlock=1 (persisted) — nobody else is affected:
|
||||
// ?unlock=1 -> on (sticky) ?unlock=0 -> off
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = false;
|
||||
try {
|
||||
var q = (root.location && root.location.search) || '';
|
||||
var m = /[?&]unlock=([01])/.exec(q);
|
||||
if (m) { try { root.localStorage.setItem('rk_unlock_all', m[1]); } catch (_) {} }
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = (root.localStorage && root.localStorage.getItem('rk_unlock_all') === '1');
|
||||
} catch (_) { /* no location/localStorage */ }
|
||||
RK.renderModeTestUnlocks = function () { return RK.RENDER_MODE_TEST_UNLOCKS === true; };
|
||||
|
||||
RK.modeMeta = function (id) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- 025: graduate the cosmetic ladder.
|
||||
--
|
||||
-- Migration 024 seeded every premium pack at tier 1 / unlock_min_credits 1, so buying ANY credit
|
||||
-- unlocked skins + iso + 3D at once (flat, not tiered). Turn it into a real ladder — unlock by
|
||||
-- lifetime credit spend OR by subscription tier, whichever comes first:
|
||||
-- Fancy (generated-skins) 1 credit / supporter (tier 1)
|
||||
-- Iso (iso-dungeon) 10 credits / premium (tier 2)
|
||||
-- 3D (kenney-3d-characters) 25 credits / operator (tier 3)
|
||||
-- Idempotent (plain UPDATEs); the free roguelike-interior pack is untouched.
|
||||
|
||||
UPDATE cosmetic_catalog SET tier = 1, unlock_min_credits = 1 WHERE pack_id = 'generated-skins';
|
||||
UPDATE cosmetic_catalog SET tier = 2, unlock_min_credits = 10 WHERE pack_id = 'iso-dungeon';
|
||||
UPDATE cosmetic_catalog SET tier = 3, unlock_min_credits = 25 WHERE pack_id = 'kenney-3d-characters';
|
||||
@@ -15,17 +15,19 @@ const DEFAULT_CATALOG = Object.freeze({
|
||||
id: 'roguelike-interior', label: 'Kenney Roguelike Interior', kind: 'render-pack',
|
||||
projection: 'topdown', tier: 0, unlockMinCredits: null, grantOnly: false, premium: false
|
||||
}),
|
||||
// Graduated ladder — unlock by lifetime credit spend OR by subscription tier, whichever comes
|
||||
// first: Fancy(skins) at 1 credit / supporter, Iso at 10 / premium, 3D at 25 / operator.
|
||||
'generated-skins': Object.freeze({
|
||||
id: 'generated-skins', label: 'Premium Generated Skins', kind: 'render-pack',
|
||||
projection: 'topdown', tier: 1, unlockMinCredits: 1, grantOnly: false, premium: true
|
||||
}),
|
||||
'iso-dungeon': Object.freeze({
|
||||
id: 'iso-dungeon', label: 'Kenney Isometric Dungeon', kind: 'render-pack',
|
||||
projection: 'iso', tier: 1, unlockMinCredits: 1, grantOnly: false, premium: true
|
||||
projection: 'iso', tier: 2, unlockMinCredits: 10, grantOnly: false, premium: true
|
||||
}),
|
||||
'kenney-3d-characters': Object.freeze({
|
||||
id: 'kenney-3d-characters', label: 'Kenney Animated 3D Characters', kind: 'render-pack',
|
||||
projection: '3d', tier: 1, unlockMinCredits: 1, grantOnly: false, premium: true
|
||||
projection: '3d', tier: 3, unlockMinCredits: 25, grantOnly: false, premium: true
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -22,12 +22,19 @@ describe('three-way unlock — default (seed) catalog', () => {
|
||||
expect(s.tier).toBe(0);
|
||||
});
|
||||
|
||||
test('backward-compat: buying any credit unlocks the seed packs (all threshold 1)', () => {
|
||||
test('graduated ladder: 1 credit unlocks Fancy (skins) only, not Iso or 3D', () => {
|
||||
const s = snapshotForUser({ credits: 5, total_credits_purchased: 1 });
|
||||
expect(s.packs['generated-skins']).toBe(true); // threshold 1
|
||||
expect(s.packs['iso-dungeon']).toBe(false); // threshold 10
|
||||
expect(s.packs['kenney-3d-characters']).toBe(false); // threshold 25
|
||||
expect(s.premium).toBe(true); // a premium pack IS unlocked
|
||||
});
|
||||
|
||||
test('graduated ladder: 25 credits unlocks the whole ladder', () => {
|
||||
const s = snapshotForUser({ credits: 0, total_credits_purchased: 25 });
|
||||
expect(s.packs['generated-skins']).toBe(true);
|
||||
expect(s.packs['iso-dungeon']).toBe(true);
|
||||
expect(s.packs['kenney-3d-characters']).toBe(true);
|
||||
expect(s.premium).toBe(true);
|
||||
});
|
||||
|
||||
test('a credit buyer is NOT auto-promoted to a premium tier', () => {
|
||||
@@ -91,7 +98,7 @@ describe('canUsePack reads the snapshot', () => {
|
||||
const locked = snapshotForUser({ total_credits_purchased: 0 });
|
||||
expect(Entitlements.canUsePack(locked, 'roguelike-interior')).toBe(true); // free
|
||||
expect(Entitlements.canUsePack(locked, 'iso-dungeon')).toBe(false); // locked
|
||||
const unlocked = snapshotForUser({ total_credits_purchased: 1 });
|
||||
const unlocked = snapshotForUser({ total_credits_purchased: 10 }); // iso threshold is 10
|
||||
expect(Entitlements.canUsePack(unlocked, 'iso-dungeon')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,9 +47,10 @@ describe('recordDirectEntryPurchase', () => {
|
||||
expect(insert[0]).toMatch(/'direct_entry'/); // audits the purchase leg
|
||||
expect(insert[0]).toMatch(/'game_entry'/); // audits the spend leg
|
||||
|
||||
// The seed premium packs (threshold 1) are now unlocked, exactly like a credit purchase.
|
||||
expect(rec.entitlements.packs['iso-dungeon']).toBe(true);
|
||||
// A direct entry = 1 credit, which unlocks Fancy (generated-skins, threshold 1) but not the
|
||||
// pricier Iso (10) / 3D (25) tiers.
|
||||
expect(rec.entitlements.packs['generated-skins']).toBe(true);
|
||||
expect(rec.entitlements.packs['iso-dungeon']).toBe(false);
|
||||
expect(rec.entitlements.premium).toBe(true);
|
||||
// ...but a direct buyer is still NOT a premium tier.
|
||||
expect(rec.entitlements.level).toBe('free');
|
||||
|
||||
@@ -11,14 +11,14 @@ describe('cosmetic entitlements', () => {
|
||||
expect(Entitlements.normalizeAppearance({ avatar: 'kenney-survivor-male' }, entitlements).avatar).toBe('default');
|
||||
});
|
||||
|
||||
test('any credits purchase unlocks the preview premium packs', () => {
|
||||
test('graduated ladder: credit spend unlocks packs by threshold', () => {
|
||||
const entitlements = Entitlements.snapshotForUser({ credits: 7, total_credits_purchased: 10 });
|
||||
|
||||
expect(entitlements.premium).toBe(true);
|
||||
expect(entitlements.level).toBe('free'); // spend unlocks packs by threshold, but isn't a premium tier
|
||||
expect(entitlements.packs['generated-skins']).toBe(true);
|
||||
expect(entitlements.packs['iso-dungeon']).toBe(true);
|
||||
expect(entitlements.packs['kenney-3d-characters']).toBe(true);
|
||||
expect(entitlements.packs['generated-skins']).toBe(true); // threshold 1
|
||||
expect(entitlements.packs['iso-dungeon']).toBe(true); // threshold 10 — just reached
|
||||
expect(entitlements.packs['kenney-3d-characters']).toBe(false); // threshold 25 — not yet
|
||||
expect(Entitlements.normalizeAppearance({ avatar: 'monero-knight' }, entitlements).avatar).toBe('monero-knight');
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,8 @@ describe('IdentityService', () => {
|
||||
|
||||
expect(snapshot.appearance).toEqual(db.state.user.appearance);
|
||||
expect(snapshot.entitlements.premium).toBe(true);
|
||||
expect(snapshot.entitlements.packs['iso-dungeon']).toBe(true);
|
||||
expect(snapshot.entitlements.packs['generated-skins']).toBe(true); // 5 credits >= threshold 1
|
||||
expect(snapshot.entitlements.packs['iso-dungeon']).toBe(false); // < threshold 10
|
||||
});
|
||||
|
||||
test('saves normalized appearance and refreshes the session cache', async () => {
|
||||
|
||||
Reference in New Issue
Block a user