@ak--47/dungeon-master 1.4.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude/skills/analyze-soup/SKILL.md +158 -0
  2. package/.claude/skills/create-dungeon/SKILL.md +464 -0
  3. package/.claude/skills/verify-dungeon/SKILL.md +157 -0
  4. package/.claude/skills/verify-dungeon/references/counting-semantics.md +161 -0
  5. package/.claude/skills/verify-dungeon/references/report-format.md +216 -0
  6. package/.claude/skills/verify-dungeon/references/sql-recipes.md +857 -0
  7. package/.claude/skills/write-hooks/SKILL.md +468 -0
  8. package/CHANGELOG.md +182 -0
  9. package/HOOKS.md +1256 -597
  10. package/README.md +140 -5
  11. package/dungeons/technical/ad-spend.js +41 -49
  12. package/dungeons/technical/anonymous-users.js +38 -36
  13. package/dungeons/technical/array-of-object-lookup.js +136 -153
  14. package/dungeons/technical/datagen-v15-verify.js +87 -0
  15. package/dungeons/technical/experiments.js +42 -40
  16. package/dungeons/technical/foobar.js +114 -118
  17. package/dungeons/technical/group-analytics.js +42 -40
  18. package/dungeons/technical/hook-helpers-verify.js +69 -50
  19. package/dungeons/technical/identity-model-verify.js +22 -12
  20. package/dungeons/technical/mirror-strategies.js +37 -39
  21. package/dungeons/technical/nested-objects.js +119 -118
  22. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  23. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  24. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  25. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  26. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  27. package/dungeons/technical/retention-cadence.js +115 -112
  28. package/dungeons/technical/sanity.js +86 -80
  29. package/dungeons/technical/scale-test.js +34 -38
  30. package/dungeons/technical/scd.js +111 -128
  31. package/dungeons/technical/simple.js +134 -141
  32. package/dungeons/technical/simplest.js +111 -65
  33. package/dungeons/technical/text-generation.js +110 -146
  34. package/dungeons/vertical/ai-platform.js +300 -333
  35. package/dungeons/vertical/community.js +290 -255
  36. package/dungeons/vertical/crypto.js +400 -391
  37. package/dungeons/vertical/dating.js +421 -375
  38. package/dungeons/vertical/devtools.js +346 -298
  39. package/dungeons/vertical/ecommerce.js +322 -394
  40. package/dungeons/vertical/education.js +380 -325
  41. package/dungeons/vertical/fintech.js +371 -325
  42. package/dungeons/vertical/fitness.js +345 -291
  43. package/dungeons/vertical/food-delivery.js +352 -307
  44. package/dungeons/vertical/gaming.js +490 -444
  45. package/dungeons/vertical/healthcare.js +311 -262
  46. package/dungeons/vertical/insurance-application.js +437 -409
  47. package/dungeons/vertical/logistics.js +278 -252
  48. package/dungeons/vertical/marketplace.js +340 -323
  49. package/dungeons/vertical/media.js +390 -335
  50. package/dungeons/vertical/real-estate.js +402 -347
  51. package/dungeons/vertical/sass.js +331 -333
  52. package/dungeons/vertical/social.js +377 -316
  53. package/dungeons/vertical/travel.js +302 -295
  54. package/index.js +64 -7
  55. package/lib/core/config-validator.js +378 -17
  56. package/lib/core/dungeon-loader.js +2 -5
  57. package/lib/generators/events.js +12 -13
  58. package/lib/generators/funnels.js +76 -2
  59. package/lib/hook-helpers/index.js +1 -0
  60. package/lib/hook-helpers/inject.js +95 -0
  61. package/lib/orchestrators/mixpanel-sender.js +7 -0
  62. package/lib/orchestrators/user-loop.js +598 -48
  63. package/lib/templates/defaults.js +59 -59
  64. package/lib/templates/macro-presets.js +53 -11
  65. package/lib/utils/dataset-context.js +103 -0
  66. package/lib/utils/retention-curve.js +140 -0
  67. package/lib/utils/utils.js +157 -109
  68. package/lib/verify/counting.js +360 -0
  69. package/lib/verify/emulate-breakdown.js +531 -108
  70. package/lib/verify/funnel-engine.js +539 -0
  71. package/lib/verify/identity.js +78 -0
  72. package/lib/verify/index.js +20 -0
  73. package/lib/verify/schema-validator.js +3 -1
  74. package/lib/verify/verify-dungeon.js +58 -0
  75. package/package.json +14 -3
  76. package/scripts/run-dungeon.mjs +12 -1
  77. package/types.d.ts +353 -4
  78. package/scripts/smoke-test-all.mjs +0 -162
@@ -1,62 +1,49 @@
1
- // ── TWEAK THESE ──
2
- const SEED = "questforge";
3
- const num_days = 120;
4
- const num_users = 10_000;
5
- const avg_events_per_user_per_day = 1.2;
6
- let token = "your-mixpanel-token";
7
-
8
- // ── env overrides ──
9
- if (process.env.MP_TOKEN) token = process.env.MP_TOKEN;
10
-
1
+ // ── IMPORTS ──
11
2
  import dayjs from "dayjs";
12
3
  import utc from "dayjs/plugin/utc.js";
4
+ dayjs.extend(utc);
13
5
  import "dotenv/config";
14
6
  import * as u from "../../lib/utils/utils.js";
15
7
  import * as v from "ak-tools";
16
-
17
- dayjs.extend(utc);
18
- const chance = u.initChance(SEED);
19
8
  /** @typedef {import("../../types").Dungeon} Config */
20
9
 
10
+ // ── OVERVIEW ──
21
11
  /*
22
- * ═══════════════════════════════════════════════════════════════════════════════
23
- * DATASET OVERVIEW
24
- * ═══════════════════════════════════════════════════════════════════════════════
12
+ * NAME: QuestForge
13
+ * APP: D&D-inspired action RPG with a deep character system, party-based
14
+ * dungeon crawls, boss fights, a player-driven economy, guilds, and
15
+ * subscription tiers. Combines tabletop-RPG strategic depth with the
16
+ * monetization and engagement loops of a modern live-service game.
17
+ * SCALE: 10,000 users, ~1.4M events, 121 days (2026-01-01 → 2026-05-01)
18
+ * CORE LOOP: character created → tutorial → quest → dungeon crawl → combat → loot → level up
25
19
  *
26
- * QuestForge — a D&D-inspired action RPG with a deep character system, party-
27
- * based dungeon crawls, boss fights, a player-driven economy, guilds, and
28
- * subscription tiers. Combines the strategic depth of a tabletop RPG with the
29
- * monetization and engagement loops of a modern live-service game.
20
+ * EVENTS (24):
21
+ * combat initiated (20) > enter dungeon (18) > combat completed (18) > find treasure (16)
22
+ * > quest accepted (15) > exit dungeon (14) > use item (14) > quest objective completed (12)
23
+ * > item purchased (11) > quest turned in (10) > inspect (9) > player death (8)
24
+ * > search for clues (8) > item sold (7) > gameplay summary (6) > level up (5)
25
+ * > attack (5) > guild joined (4) > real money purchase (3) > fight boss (3)
26
+ * > defend (3) > tutorial completed (2) > character created (1) > guild left (1)
30
27
  *
31
- * CORE LOOP: Players create characters (12 classes, 9 races, 9 alignments,
32
- * 13 backgrounds) and progress through quests, dungeon crawls, boss fights,
33
- * and combat encounters. Strategic preparation (inspect, search for clues,
34
- * Ancient Compass) creates skill gaps. Guild membership drives social
35
- * retention. Monetization via premium currency, lucky charms, season pass,
36
- * and subscription tiers (Free / Premium / Elite).
28
+ * FUNNELS (7):
29
+ * - Onboarding: character created tutorial completed quest accepted (75%)
30
+ * - Combat Loop: combat initiated combat completed use item (75%)
31
+ * - Dungeon Crawl: enter dungeon find treasure exit dungeon (60%)
32
+ * - Quest Lifecycle: quest accepted quest objective completed quest turned in (55%)
33
+ * - Prep Funnel: inspect search for clues enter dungeon (50%)
34
+ * - Economy: item purchased → use item → item sold (45%)
35
+ * - Social/Progression: guild joined → level up → real money purchase (25%)
37
36
  *
38
- * SCALE: 5,000 users × 120 events = 600K events over 100 days
39
- * 24 event types, 7 funnels, guild group analytics, subscription tiers
40
- *
41
- * KEY SYSTEMS:
42
- * - Character: 12 D&D classes, 9 races, 9 alignments, 13 backgrounds
43
- * - Quests: accept objective turn in (5 quest types, gold/XP rewards)
44
- * - Dungeons: enter → find treasure → exit (50 dungeons, party-based, 3 outcomes)
45
- * - Combat: initiate → complete (6 enemy types), boss fights, attack/defend
46
- * - Economy: item purchase/sell, real money purchases, vendor types
47
- * - Progression: level up (1-50), stat points, level-scaled rewards
48
- * - Social: guild join/leave (5-100 members, guild levels 1-20)
49
- * - Monetization: premium currency, lucky charms, legendary chests, season pass
50
- * - Subscriptions: Free / Premium ($9.99/mo) / Elite ($19.99/mo)
51
- * - Experiments: A/B/C/Control variants across "fast leveling", "tension
52
- * economy", "free trial"
37
+ * USER PROPS: preferred_playstyle, total_playtime_hours, achievement_points, favorite_class,
38
+ * Platform, graphics_quality, subscription_tier, race, class, alignment,
39
+ * background, level, archetype, experiment, variant
40
+ * SUPER PROPS: Platform, graphics_quality, subscription_tier
41
+ * SCD PROPS: player_rank (recruit/veteran/elite/legend, weekly fuzzy, max 20)
42
+ * GROUPS: guild_id (cap 500; on guild joined / guild left / quest turned in / combat completed)
53
43
  */
54
44
 
45
+ // ── HOOK STORIES ──
55
46
  /*
56
- * ═══════════════════════════════════════════════════════════════════════════════
57
- * ANALYTICS HOOKS (13 hooks)
58
- * ═══════════════════════════════════════════════════════════════════════════════
59
- *
60
47
  * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
61
48
  * behavioral cohorts (count event per user), raw-prop breakdowns
62
49
  * (treasure_type, subscription_tier, day-of-user-life), or funnel time-to-convert.
@@ -299,37 +286,461 @@ const chance = u.initChance(SEED);
299
286
  * Combat-Prep Magic Num | over boss victory | 1x | 0.75x | -25%
300
287
  */
301
288
 
289
+ // ── SCALE ──
290
+ const SEED = "questforge";
291
+ const NUM_USERS = 10_000;
292
+ const DATASET_START = "2026-01-01T00:00:00Z";
293
+ const DATASET_END = "2026-05-01T23:59:59Z";
294
+ const EVENTS_PER_DAY = 1.2;
295
+ const token = process.env.MP_TOKEN || "your-mixpanel-token";
296
+
297
+ const chance = u.initChance(SEED);
298
+
299
+ // ── KNOBS (tweak these to reshape stories) ──
300
+ const COMPASS_REWARD_MULT = 1.5;
301
+ const COMPASS_BONUS_QUEST_LIKELIHOOD = 40;
302
+
303
+ const CURSED_WEEK_START_DAY = 40;
304
+ const CURSED_WEEK_END_DAY = 47;
305
+ const CURSED_DEATH_INJECTION_FACTOR = 0.6;
306
+
307
+ const EARLY_GUILD_DAYS = 3;
308
+ const EARLY_GUILD_COMBAT_CLONE_LIKELIHOOD = 60;
309
+
310
+ const DEATH_SPIRAL_EARLY_DAYS = 7;
311
+ const DEATH_SPIRAL_DROP_LIKELIHOOD = 80;
312
+
313
+ const LUCKY_CHARM_PRICE_MULT = 2.5;
314
+ const LUCKY_CHARM_BONUS_PURCHASE_LIKELIHOOD = 35;
315
+
316
+ const STRATEGIC_COMPLETION_LIKELIHOOD = 85;
317
+ const STRATEGIC_TREASURE_MULT = 2;
318
+
319
+ const LEGENDARY_RELEASE_DAY = 45;
320
+ const LEGENDARY_DROP_LIKELIHOOD = 2;
321
+ const LEGENDARY_TREASURE_VALUE = 50000;
322
+ const LEGENDARY_WIN_LIKELIHOOD = 90;
323
+ const LEGENDARY_DUNGEON_SPEED_MULT = 0.6;
324
+
325
+ const PREMIUM_WIN_LIKELIHOOD = 50;
326
+ const PREMIUM_REWARD_MULT = 1.4;
327
+ const PREMIUM_COMPLETION_LIKELIHOOD = 45;
328
+ const PREMIUM_DUNGEON_SPEED_MULT = 0.85;
329
+ const PREMIUM_TREASURE_MULT = 1.5;
330
+ const PREMIUM_SURVIVAL_LIKELIHOOD = 30;
331
+
332
+ const ELITE_WIN_LIKELIHOOD = 70;
333
+ const ELITE_REWARD_MULT = 1.8;
334
+ const ELITE_COMPLETION_LIKELIHOOD = 65;
335
+ const ELITE_DUNGEON_SPEED_MULT = 0.7;
336
+ const ELITE_TREASURE_MULT = 2.0;
337
+ const ELITE_SURVIVAL_LIKELIHOOD = 50;
338
+ const ELITE_BONUS_TREASURE_LIKELIHOOD = 5;
339
+
340
+ const LEVEL_GOLD_SCALING = 0.15;
341
+
342
+ const WHALE_PRICE_MULT = 1.8;
343
+
344
+ const TTC_ELITE_FACTOR = 0.30;
345
+ const TTC_PREMIUM_FACTOR = 0.70;
346
+ const TTC_FREE_FACTOR = 1.40;
347
+
348
+ const PREP_SWEET_MIN = 3;
349
+ const PREP_SWEET_MAX = 6;
350
+ const PREP_OVER_THRESHOLD = 7;
351
+ const PREP_TREASURE_BOOST = 1.3;
352
+ const PREP_BOSS_FLIP_LIKELIHOOD = 25;
353
+
354
+ // ── DATA ARRAYS ──
302
355
  // Generate consistent item/location IDs for lookup tables
303
356
  const dungeonIds = v.range(1, 51).map(n => `dungeon_${v.uid(6)}`);
304
357
  const questIds = v.range(1, 201).map(n => `quest_${v.uid(8)}`);
305
358
  const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
306
359
 
360
+ // ── HELPER FUNCTIONS ──
361
+ function handleUserHooks(record) {
362
+ // Hook #11: ALIGNMENT ARCHETYPE — derive archetype on user profile
363
+ if (record.alignment === "Chaotic Evil" || record.alignment === "Neutral Evil") {
364
+ record.archetype = "villain";
365
+ } else if (record.alignment === "Lawful Good" || record.alignment === "Neutral Good") {
366
+ record.archetype = "hero";
367
+ } else {
368
+ record.archetype = "neutral";
369
+ }
370
+ return record;
371
+ }
372
+
373
+ function handleEverythingHooks(record, meta) {
374
+ const userEvents = record;
375
+ const profile = meta.profile;
376
+ const datasetStart = dayjs.unix(meta.datasetStart);
377
+ const LEGENDARY_WEAPON_RELEASE = datasetStart.add(LEGENDARY_RELEASE_DAY, 'days');
378
+
379
+ // Stamp superProps from profile for consistency
380
+ userEvents.forEach(e => {
381
+ e.Platform = profile.Platform;
382
+ e.graphics_quality = profile.graphics_quality;
383
+ e.subscription_tier = profile.subscription_tier;
384
+ });
385
+
386
+ // Hook #7: TIMED RELEASE — Shadowmourne Legendary post-d45.
387
+ // Runs in everything hook so timestamps are post-bunchIntoSessions.
388
+ userEvents.forEach(e => {
389
+ if (e.event === "find treasure" && dayjs(e.time).isAfter(LEGENDARY_WEAPON_RELEASE) && chance.bool({ likelihood: LEGENDARY_DROP_LIKELIHOOD })) {
390
+ e.treasure_type = "Shadowmourne Legendary";
391
+ e.treasure_value = LEGENDARY_TREASURE_VALUE;
392
+ }
393
+ });
394
+
395
+ // Hook #12: COMBAT T2C — scale time gaps in combat funnel
396
+ // sequences (combat initiated → combat completed → use item).
397
+ // Elite ~0.30x (faster), Premium ~0.70x, Free ~1.40x (slower).
398
+ // Finds all 3-step sequences and shifts step 2/3 timestamps.
399
+ const tier = profile.subscription_tier;
400
+ const t2cFactor = (
401
+ tier === "Elite" ? TTC_ELITE_FACTOR :
402
+ tier === "Premium" ? TTC_PREMIUM_FACTOR :
403
+ tier === "Free" ? TTC_FREE_FACTOR :
404
+ 1.0
405
+ );
406
+ if (t2cFactor !== 1.0) {
407
+ // Collect indices for each combat funnel step
408
+ const combatInitiated = [];
409
+ const combatCompleted = [];
410
+ const useItem = [];
411
+ for (let i = 0; i < userEvents.length; i++) {
412
+ const e = userEvents[i];
413
+ if (e.event === "combat initiated") combatInitiated.push(i);
414
+ else if (e.event === "combat completed") combatCompleted.push(i);
415
+ else if (e.event === "use item") useItem.push(i);
416
+ }
417
+ // Match sequences: for each combat initiated, find next
418
+ // combat completed after it, then next use item after that
419
+ const matched = new Set();
420
+ for (const ciIdx of combatInitiated) {
421
+ const ccIdx = combatCompleted.find(j => j > ciIdx && !matched.has(j));
422
+ if (ccIdx === undefined) continue;
423
+ const uiIdx = useItem.find(j => j > ccIdx && !matched.has(j));
424
+ if (uiIdx === undefined) continue;
425
+ matched.add(ccIdx);
426
+ matched.add(uiIdx);
427
+ // Scale gap between step 1→2 and 2→3
428
+ const t0 = dayjs(userEvents[ciIdx].time);
429
+ const t1 = dayjs(userEvents[ccIdx].time);
430
+ const t2 = dayjs(userEvents[uiIdx].time);
431
+ const gap1 = t1.diff(t0);
432
+ const gap2 = t2.diff(t1);
433
+ userEvents[ccIdx].time = t0.add(Math.round(gap1 * t2cFactor), 'milliseconds').toISOString();
434
+ userEvents[uiIdx].time = dayjs(userEvents[ccIdx].time).add(Math.round(gap2 * t2cFactor), 'milliseconds').toISOString();
435
+ }
436
+ }
437
+
438
+ const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
439
+ const userLevel = profile.level || 1;
440
+
441
+ // Track user behaviors
442
+ let usedAncientCompass = false;
443
+ let boughtLuckyCharm = false;
444
+ let joinedGuildEarly = false;
445
+ let earlyDeaths = 0;
446
+ let hasLegendaryWeapon = false;
447
+ let inspectedBeforeDungeon = false;
448
+ let searchedBeforeDungeon = false;
449
+ let subscriptionTier = "Free";
450
+
451
+ // Hook #10: Whale segmentation — deterministic via user_id hash (~33%)
452
+ const userId = userEvents.length > 0 ? (userEvents[0].user_id || userEvents[0].distinct_id || "") : "";
453
+ const isWhale = userId.length > 0 && userId.charCodeAt(0) % 3 === 0;
454
+
455
+ // First pass: identify user patterns
456
+ userEvents.forEach((event) => {
457
+ const eventTime = dayjs(event.time);
458
+ const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
459
+
460
+ if (event.subscription_tier) {
461
+ subscriptionTier = event.subscription_tier;
462
+ }
463
+
464
+ if (event.event === "use item" && event.item_type === "Ancient Compass") {
465
+ usedAncientCompass = true;
466
+ }
467
+
468
+ if (event.event === "real money purchase" && event.product === "Lucky Charm Pack") {
469
+ boughtLuckyCharm = true;
470
+ }
471
+
472
+ if (event.event === "guild joined" && daysSinceStart < EARLY_GUILD_DAYS) {
473
+ joinedGuildEarly = true;
474
+ }
475
+
476
+ if (event.event === "player death" && daysSinceStart < DEATH_SPIRAL_EARLY_DAYS) {
477
+ earlyDeaths++;
478
+ }
479
+
480
+ if (event.event === "find treasure" && event.treasure_type === "Shadowmourne Legendary") {
481
+ hasLegendaryWeapon = true;
482
+ }
483
+
484
+ if (event.event === "inspect") {
485
+ inspectedBeforeDungeon = true;
486
+ }
487
+ if (event.event === "search for clues") {
488
+ searchedBeforeDungeon = true;
489
+ }
490
+ });
491
+
492
+ // Second pass: raw mutations + cloning, no flag stamping
493
+ userEvents.forEach((event, idx) => {
494
+ const eventTime = dayjs(event.time);
495
+
496
+ // Hook 9: PROGRESSION SCALING — Quest gold scales with level.
497
+ // Power curve so bucket-averaged high-level/low-level ratio >= 2.0x.
498
+ if (event.event === "quest turned in") {
499
+ const baseGold = event.reward_gold || 100;
500
+ event.reward_gold = Math.floor(baseGold * (1 + userLevel * LEVEL_GOLD_SCALING));
501
+ }
502
+
503
+ // Hook 1: CONVERSION — Ancient Compass users earn 1.5x quest
504
+ // rewards plus 40% chance of bonus cloned quest.
505
+ if (usedAncientCompass && event.event === "quest turned in") {
506
+ event.reward_gold = Math.floor((event.reward_gold || 100) * COMPASS_REWARD_MULT);
507
+ event.reward_xp = Math.floor((event.reward_xp || 500) * COMPASS_REWARD_MULT);
508
+
509
+ if (chance.bool({ likelihood: COMPASS_BONUS_QUEST_LIKELIHOOD })) {
510
+ userEvents.splice(idx + 1, 0, {
511
+ ...event,
512
+ time: eventTime.add(chance.integer({ min: 10, max: 120 }), 'minutes').toISOString(),
513
+ quest_id: chance.pickone(questIds),
514
+ reward_gold: chance.integer({ min: 100, max: 500 }),
515
+ reward_xp: chance.integer({ min: 500, max: 2000 }),
516
+ });
517
+ }
518
+ }
519
+
520
+ // Hook 5: PURCHASE VALUE — Lucky charm buyers see 2x prices
521
+ // and 35% chance of bonus high-value cloned purchase.
522
+ if (boughtLuckyCharm) {
523
+ if (event.event === "real money purchase" && event.price_usd) {
524
+ event.price_usd = Math.round(event.price_usd * LUCKY_CHARM_PRICE_MULT * 100) / 100;
525
+ }
526
+ if (event.event === "item purchased" && chance.bool({ likelihood: LUCKY_CHARM_BONUS_PURCHASE_LIKELIHOOD })) {
527
+ const purchaseTemplate = userEvents.find(e => e.event === "real money purchase");
528
+ if (purchaseTemplate) {
529
+ userEvents.splice(idx + 1, 0, {
530
+ ...purchaseTemplate,
531
+ time: eventTime.add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
532
+ user_id: event.user_id,
533
+ product: chance.pickone(["Premium Currency (5000)", "Legendary Weapon Chest", "Season Pass"]),
534
+ price_usd: chance.pickone([19.99, 49.99, 99.99]),
535
+ payment_method: chance.pickone(["Credit Card", "PayPal"]),
536
+ });
537
+ }
538
+ }
539
+ }
540
+
541
+ // Hook 10: WHALE PURCHASES — 1.8x price for whale cohort.
542
+ if (isWhale && event.event === "real money purchase" && event.price_usd) {
543
+ event.price_usd = Math.round(event.price_usd * WHALE_PRICE_MULT * 100) / 100;
544
+ }
545
+
546
+ // Hook 6: BEHAVIORS TOGETHER — inspect+search dungeons get
547
+ // 85% completion rate + 2x treasure value.
548
+ if (inspectedBeforeDungeon && searchedBeforeDungeon) {
549
+ if (event.event === "exit dungeon" && event.completion_status !== "completed" && chance.bool({ likelihood: STRATEGIC_COMPLETION_LIKELIHOOD })) {
550
+ event.completion_status = "completed";
551
+ }
552
+ if (event.event === "find treasure") {
553
+ event.treasure_value = Math.floor((event.treasure_value || 50) * STRATEGIC_TREASURE_MULT);
554
+ }
555
+ }
556
+
557
+ // Hook 7: TIMED RELEASE — Legendary owners get 90% combat
558
+ // wins + 0.6x dungeon time.
559
+ if (hasLegendaryWeapon) {
560
+ if (event.event === "combat completed" && event.outcome !== "Victory" && chance.bool({ likelihood: LEGENDARY_WIN_LIKELIHOOD })) {
561
+ event.outcome = "Victory";
562
+ }
563
+ if (event.event === "exit dungeon") {
564
+ event.completion_status = "completed";
565
+ event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * LEGENDARY_DUNGEON_SPEED_MULT);
566
+ }
567
+ }
568
+
569
+ // Hook 8: SUBSCRIPTION TIER — Premium/Elite get win+reward+
570
+ // completion+treasure boosts. Reads tier from profile.
571
+ if (subscriptionTier === "Premium" || subscriptionTier === "Elite") {
572
+ const isElite = subscriptionTier === "Elite";
573
+ if (event.event === "combat completed" && event.outcome !== "Victory") {
574
+ const winBoost = isElite ? ELITE_WIN_LIKELIHOOD : PREMIUM_WIN_LIKELIHOOD;
575
+ if (chance.bool({ likelihood: winBoost })) {
576
+ event.outcome = "Victory";
577
+ event.loot_gained = true;
578
+ }
579
+ }
580
+ if (event.event === "quest turned in") {
581
+ const rewardMultiplier = isElite ? ELITE_REWARD_MULT : PREMIUM_REWARD_MULT;
582
+ event.reward_gold = Math.floor((event.reward_gold || 100) * rewardMultiplier);
583
+ event.reward_xp = Math.floor((event.reward_xp || 500) * rewardMultiplier);
584
+ }
585
+ if (event.event === "exit dungeon") {
586
+ if (event.completion_status !== "completed") {
587
+ const completionBoost = isElite ? ELITE_COMPLETION_LIKELIHOOD : PREMIUM_COMPLETION_LIKELIHOOD;
588
+ if (chance.bool({ likelihood: completionBoost })) {
589
+ event.completion_status = "completed";
590
+ }
591
+ }
592
+ if (event.completion_status === "completed") {
593
+ const speedBoost = isElite ? ELITE_DUNGEON_SPEED_MULT : PREMIUM_DUNGEON_SPEED_MULT;
594
+ event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * speedBoost);
595
+ }
596
+ }
597
+ if (event.event === "find treasure") {
598
+ const treasureBoost = isElite ? ELITE_TREASURE_MULT : PREMIUM_TREASURE_MULT;
599
+ event.treasure_value = Math.floor((event.treasure_value || 50) * treasureBoost);
600
+ }
601
+ if (event.event === "player death") {
602
+ const survivalLikelihood = isElite ? ELITE_SURVIVAL_LIKELIHOOD : PREMIUM_SURVIVAL_LIKELIHOOD;
603
+ if (chance.bool({ likelihood: survivalLikelihood })) {
604
+ event.event = "combat completed";
605
+ event.outcome = "Victory";
606
+ event.loot_gained = true;
607
+ }
608
+ }
609
+ if (isElite && chance.bool({ likelihood: ELITE_BONUS_TREASURE_LIKELIHOOD })) {
610
+ if (event.event === "quest turned in" || event.event === "exit dungeon") {
611
+ const treasureTemplate = userEvents.find(e => e.event === "find treasure");
612
+ if (treasureTemplate) {
613
+ const treasureTypes = ["Rare Artifact", "Gold", "Weapon", "Armor"];
614
+ userEvents.splice(idx + 1, 0, {
615
+ ...treasureTemplate,
616
+ time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
617
+ user_id: event.user_id,
618
+ treasure_type: chance.pickone(treasureTypes),
619
+ treasure_value: chance.integer({ min: 200, max: 800 }),
620
+ });
621
+ }
622
+ }
623
+ }
624
+ }
625
+ });
626
+
627
+ // Hook 2: CURSED WEEK — inject extra deaths in days 40-47 of
628
+ // user's timeline. Cause_of_death set to "Curse" on injected.
629
+ // Discover via line-chart of player-death by day-of-user-life.
630
+ if (firstEventTime) {
631
+ const deathTemplate = userEvents.find(e => e.event === "player death");
632
+ if (deathTemplate) {
633
+ const cursedStart = firstEventTime.add(CURSED_WEEK_START_DAY, 'days');
634
+ const cursedEnd = firstEventTime.add(CURSED_WEEK_END_DAY, 'days');
635
+ const cursedEvents = userEvents.filter(e => {
636
+ const t = dayjs(e.time);
637
+ return t.isAfter(cursedStart) && t.isBefore(cursedEnd);
638
+ });
639
+ const deathsToInject = Math.floor(cursedEvents.length * CURSED_DEATH_INJECTION_FACTOR);
640
+ for (let d = 0; d < deathsToInject; d++) {
641
+ const sourceEvent = cursedEvents[d % cursedEvents.length];
642
+ userEvents.push({
643
+ ...deathTemplate,
644
+ time: dayjs(sourceEvent.time).add(chance.integer({ min: 1, max: 30 }), 'minutes').toISOString(),
645
+ user_id: sourceEvent.user_id,
646
+ event: "player death",
647
+ cause_of_death: "Curse",
648
+ player_level: chance.integer({ min: 1, max: 50 }),
649
+ resurrection_used: chance.bool({ likelihood: 80 }),
650
+ });
651
+ }
652
+ }
653
+ }
654
+
655
+ // Hook 3 RETENTION + Hook 4 CHURN — early guild-joiners get
656
+ // extra cloned combat events; non-joiners with 3+ deaths in
657
+ // first week lose 70% of post-week events. No flag.
658
+ const shouldChurn = (!joinedGuildEarly && earlyDeaths >= 2) || (earlyDeaths >= 4);
659
+ if (shouldChurn) {
660
+ const firstWeekEnd = firstEventTime ? firstEventTime.add(DEATH_SPIRAL_EARLY_DAYS, 'days') : null;
661
+ for (let i = userEvents.length - 1; i >= 0; i--) {
662
+ if (firstWeekEnd && dayjs(userEvents[i].time).isAfter(firstWeekEnd) && chance.bool({ likelihood: DEATH_SPIRAL_DROP_LIKELIHOOD })) {
663
+ userEvents.splice(i, 1);
664
+ }
665
+ }
666
+ } else if (joinedGuildEarly) {
667
+ const lastEvent = userEvents[userEvents.length - 1];
668
+ const combatTemplate = userEvents.find(e => e.event === "combat completed");
669
+ if (lastEvent && combatTemplate && chance.bool({ likelihood: EARLY_GUILD_COMBAT_CLONE_LIKELIHOOD })) {
670
+ userEvents.push({
671
+ ...combatTemplate,
672
+ time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
673
+ user_id: lastEvent.user_id,
674
+ outcome: "Victory",
675
+ loot_gained: true,
676
+ });
677
+ }
678
+ }
679
+
680
+ // HOOK 13: COMBAT-PREP MAGIC NUMBER (in-funnel, no flags)
681
+ // Sweet 3-6 inspect+search events between quest accepted and
682
+ // fight boss → +30% loot/treasure_value on find-treasure events.
683
+ // Over 7+ → drop 25% of fight-boss completion (over-prep
684
+ // signals analysis paralysis). No flag.
685
+ const questAccept = userEvents.find(e => e.event === "quest accepted");
686
+ const bossFight = userEvents.find(e => e.event === "fight boss");
687
+ if (questAccept && bossFight) {
688
+ const aTime = dayjs(questAccept.time);
689
+ const bTime = dayjs(bossFight.time);
690
+ const prepCount = userEvents.filter(e =>
691
+ (e.event === "inspect" || e.event === "search for clues") &&
692
+ dayjs(e.time).isAfter(aTime) &&
693
+ dayjs(e.time).isBefore(bTime)
694
+ ).length;
695
+ if (prepCount >= PREP_SWEET_MIN && prepCount <= PREP_SWEET_MAX) {
696
+ userEvents.forEach(e => {
697
+ if (e.event === "find treasure" && typeof e.treasure_value === "number") {
698
+ e.treasure_value = Math.round(e.treasure_value * PREP_TREASURE_BOOST);
699
+ }
700
+ });
701
+ } else if (prepCount >= PREP_OVER_THRESHOLD) {
702
+ for (let i = userEvents.length - 1; i >= 0; i--) {
703
+ const ev = userEvents[i];
704
+ if (ev.event === "fight boss" && ev.victory === true && chance.bool({ likelihood: PREP_BOSS_FLIP_LIKELIHOOD })) {
705
+ ev.victory = false;
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ return record;
712
+ }
713
+
714
+ // ── CONFIG ──
307
715
  /** @type {Config} */
308
716
  const config = {
309
717
  version: 2,
310
- token,
311
718
  seed: SEED,
312
- datasetStart: "2026-01-01T00:00:00Z",
313
- datasetEnd: "2026-05-01T23:59:59Z",
314
- // numDays: num_days,
315
- avgEventsPerUserPerDay: avg_events_per_user_per_day,
316
- numUsers: num_users,
317
- hasAnonIds: true,
318
- avgDevicePerUser: 2,
319
- hasSessionIds: true,
719
+ datasetStart: DATASET_START,
720
+ datasetEnd: DATASET_END,
721
+ avgEventsPerUserPerDay: EVENTS_PER_DAY,
722
+ numUsers: NUM_USERS,
320
723
  format: "json",
321
724
  gzip: true,
322
- alsoInferFunnels: false,
323
- hasLocation: true,
324
- hasAndroidDevices: true,
325
- hasIOSDevices: true,
326
- hasDesktopDevices: true,
327
- hasBrowser: false,
328
- hasCampaigns: false,
329
- isAnonymous: false,
330
- hasAdSpend: false,
331
-
332
- hasAvatar: true,
725
+ credentials: {
726
+ token,
727
+ },
728
+ switches: {
729
+ hasSessionIds: true,
730
+ alsoInferFunnels: false,
731
+ hasLocation: true,
732
+ hasAndroidDevices: true,
733
+ hasIOSDevices: true,
734
+ hasDesktopDevices: true,
735
+ hasBrowser: false,
736
+ hasCampaigns: false,
737
+ isAnonymous: false,
738
+ hasAdSpend: false,
739
+ hasAvatar: true,
740
+ },
741
+ identity: {
742
+ avgDevicePerUser: 2,
743
+ },
333
744
 
334
745
  concurrency: 1,
335
746
  writeToDisk: false,
@@ -435,6 +846,7 @@ const config = {
435
846
  {
436
847
  event: "quest turned in",
437
848
  weight: 10,
849
+ isStrictEvent: false,
438
850
  properties: {
439
851
  "quest_id": questIds,
440
852
  "reward_gold": u.weighNumRange(10, 500, 0.5, 100),
@@ -453,6 +865,7 @@ const config = {
453
865
  {
454
866
  event: "exit dungeon",
455
867
  weight: 14,
868
+ isStrictEvent: false,
456
869
  properties: {
457
870
  "dungeon_id": dungeonIds,
458
871
  "time_spent_mins": u.weighNumRange(5, 120, 0.6, 30),
@@ -462,6 +875,7 @@ const config = {
462
875
  {
463
876
  event: "find treasure",
464
877
  weight: 16,
878
+ isStrictEvent: false,
465
879
  properties: {
466
880
  "treasure_type": ["Gold", "Weapon", "Armor", "Potion", "Scroll", "Rare Artifact"],
467
881
  "treasure_value": u.weighNumRange(5, 1000, 1.2, 50),
@@ -488,6 +902,7 @@ const config = {
488
902
  {
489
903
  event: "item purchased",
490
904
  weight: 11,
905
+ isStrictEvent: false,
491
906
  properties: {
492
907
  "item_id": itemIds,
493
908
  "item_type": ["Weapon", "Armor", "Potion", "Scroll", "Mount", "Cosmetic"],
@@ -507,6 +922,7 @@ const config = {
507
922
  {
508
923
  event: "real money purchase",
509
924
  weight: 3,
925
+ isStrictEvent: false,
510
926
  properties: {
511
927
  "product": [
512
928
  "Premium Currency (1000)",
@@ -523,6 +939,7 @@ const config = {
523
939
  {
524
940
  event: "guild joined",
525
941
  weight: 4,
942
+ isStrictEvent: false,
526
943
  properties: {
527
944
  "guild_size": u.weighNumRange(5, 100),
528
945
  "guild_level": u.weighNumRange(1, 20),
@@ -538,6 +955,7 @@ const config = {
538
955
  {
539
956
  event: "inspect",
540
957
  weight: 9,
958
+ isStrictEvent: false,
541
959
  properties: {
542
960
  "inspect_target": ["NPC", "Monster", "Treasure Chest", "Door", "Statue", "Bookshelf"],
543
961
  }
@@ -545,6 +963,7 @@ const config = {
545
963
  {
546
964
  event: "search for clues",
547
965
  weight: 8,
966
+ isStrictEvent: false,
548
967
  properties: {
549
968
  "location_type": ["Dungeon Entrance", "Hidden Room", "Quest Location", "Town Square"],
550
969
  "clue_found": [false, false, true, true, true],
@@ -553,6 +972,7 @@ const config = {
553
972
  {
554
973
  event: "use item",
555
974
  weight: 14,
975
+ isStrictEvent: false,
556
976
  properties: {
557
977
  "item_id": itemIds,
558
978
  "item_type": [
@@ -574,6 +994,7 @@ const config = {
574
994
  {
575
995
  event: "combat completed",
576
996
  weight: 18,
997
+ isStrictEvent: false,
577
998
  properties: {
578
999
  "outcome": ["Victory", "Defeat", "Fled"],
579
1000
  "loot_gained": [false, false, false, true, true, true, true, true, true, true],
@@ -689,386 +1110,11 @@ const config = {
689
1110
 
690
1111
  lookupTables: [],
691
1112
 
692
- /**
693
- * 🎯 ARCHITECTED ANALYTICS HOOKS — 13 patterns
694
- *
695
- * 1. CONVERSION: Ancient Compass users have 3x quest completion + 1.5x rewards
696
- * 2. TIME-BASED: "Cursed Week" (days 40-47) has 5x death rates
697
- * 3. RETENTION: Early guild joiners (first 3 days) have 80% D30 retention vs 20%
698
- * 4. CHURN: Players with 3+ deaths in first week have 70% churn rate
699
- * 5. PURCHASE VALUE: Lucky Charm buyers spend 5x more (LTV pattern)
700
- * 6. BEHAVIORS TOGETHER: inspect + search before dungeon = 85% completion vs 45%
701
- * 7. TIMED RELEASE: Legendary weapon released day 45, early adopters dominate
702
- * 8. SUBSCRIPTION TIER: Premium/Elite users have higher engagement and success
703
- * 9. PROGRESSION SCALING: Quest gold scales with player level (1 + level * 0.15)
704
- * 10. WHALE PURCHASES: ~33% of users via deterministic hash spend 1.8x more
705
- * 11. ALIGNMENT ARCHETYPE: Good=hero, Evil=villain, other=neutral (user hook)
706
- * 12. COMBAT FUNNEL TTC: Elite 0.30x faster, Free 1.40x slower combat funnel T2C
707
- * 13. COMBAT-PREP MAGIC NUMBER: 3-6 prep events = +30% treasure; 7+ = -25% boss wins
708
- */
709
- hook: function (record, type, meta) {
710
- // Hook #11: ALIGNMENT ARCHETYPE — derive archetype on user profile
711
- if (type === "user") {
712
- if (record.alignment === "Chaotic Evil" || record.alignment === "Neutral Evil") {
713
- record.archetype = "villain";
714
- } else if (record.alignment === "Lawful Good" || record.alignment === "Neutral Good") {
715
- record.archetype = "hero";
716
- } else {
717
- record.archetype = "neutral";
718
- }
719
- }
720
-
721
- // Hook #2 event-level part removed — cursed week now handled in everything hook
722
-
723
- // Hook #7: TIMED RELEASE — Legendary Weapon
724
- // (moved to everything hook — event hook fires before bunchIntoSessions
725
- // reshuffles timestamps, causing tagged events to leak across the d45 boundary)
726
-
727
- // HOOK 12 (T2C): moved to everything hook — funnel-post effects
728
- // were invisible to SQL verification.
729
-
730
- // Hooks #1, #3, #4, #5, #6, #8, #9, #10: per-user behavioral patterns
731
- if (type === "everything") {
732
- const userEvents = record;
733
- const profile = meta.profile;
734
- const datasetStart = dayjs.unix(meta.datasetStart);
735
- const LEGENDARY_WEAPON_RELEASE = datasetStart.add(45, 'days');
736
-
737
- // Stamp superProps from profile for consistency
738
- userEvents.forEach(e => {
739
- e.Platform = profile.Platform;
740
- e.graphics_quality = profile.graphics_quality;
741
- e.subscription_tier = profile.subscription_tier;
742
- });
743
-
744
- // Hook #7: TIMED RELEASE — Shadowmourne Legendary post-d45.
745
- // Runs in everything hook so timestamps are post-bunchIntoSessions.
746
- userEvents.forEach(e => {
747
- if (e.event === "find treasure" && dayjs(e.time).isAfter(LEGENDARY_WEAPON_RELEASE) && chance.bool({ likelihood: 2 })) {
748
- e.treasure_type = "Shadowmourne Legendary";
749
- e.treasure_value = 50000;
750
- }
751
- });
752
-
753
- // Hook #12: COMBAT T2C — scale time gaps in combat funnel
754
- // sequences (combat initiated → combat completed → use item).
755
- // Elite ~0.30x (faster), Premium ~0.70x, Free ~1.40x (slower).
756
- // Finds all 3-step sequences and shifts step 2/3 timestamps.
757
- const tier = profile.subscription_tier;
758
- const t2cFactor = (
759
- tier === "Elite" ? 0.30 :
760
- tier === "Premium" ? 0.70 :
761
- tier === "Free" ? 1.40 :
762
- 1.0
763
- );
764
- if (t2cFactor !== 1.0) {
765
- // Collect indices for each combat funnel step
766
- const combatInitiated = [];
767
- const combatCompleted = [];
768
- const useItem = [];
769
- for (let i = 0; i < userEvents.length; i++) {
770
- const e = userEvents[i];
771
- if (e.event === "combat initiated") combatInitiated.push(i);
772
- else if (e.event === "combat completed") combatCompleted.push(i);
773
- else if (e.event === "use item") useItem.push(i);
774
- }
775
- // Match sequences: for each combat initiated, find next
776
- // combat completed after it, then next use item after that
777
- const matched = new Set();
778
- for (const ciIdx of combatInitiated) {
779
- const ccIdx = combatCompleted.find(j => j > ciIdx && !matched.has(j));
780
- if (ccIdx === undefined) continue;
781
- const uiIdx = useItem.find(j => j > ccIdx && !matched.has(j));
782
- if (uiIdx === undefined) continue;
783
- matched.add(ccIdx);
784
- matched.add(uiIdx);
785
- // Scale gap between step 1→2 and 2→3
786
- const t0 = dayjs(userEvents[ciIdx].time);
787
- const t1 = dayjs(userEvents[ccIdx].time);
788
- const t2 = dayjs(userEvents[uiIdx].time);
789
- const gap1 = t1.diff(t0);
790
- const gap2 = t2.diff(t1);
791
- userEvents[ccIdx].time = t0.add(Math.round(gap1 * t2cFactor), 'milliseconds').toISOString();
792
- userEvents[uiIdx].time = dayjs(userEvents[ccIdx].time).add(Math.round(gap2 * t2cFactor), 'milliseconds').toISOString();
793
- }
794
- }
795
-
796
- const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
797
- const userLevel = profile.level || 1;
798
-
799
- // Track user behaviors
800
- let usedAncientCompass = false;
801
- let boughtLuckyCharm = false;
802
- let joinedGuildEarly = false;
803
- let earlyDeaths = 0;
804
- let hasLegendaryWeapon = false;
805
- let inspectedBeforeDungeon = false;
806
- let searchedBeforeDungeon = false;
807
- let subscriptionTier = "Free";
808
-
809
- // Hook #10: Whale segmentation — deterministic via user_id hash (~33%)
810
- const userId = userEvents.length > 0 ? (userEvents[0].user_id || userEvents[0].distinct_id || "") : "";
811
- const isWhale = userId.length > 0 && userId.charCodeAt(0) % 3 === 0;
812
-
813
- // First pass: identify user patterns
814
- userEvents.forEach((event) => {
815
- const eventTime = dayjs(event.time);
816
- const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
817
-
818
- if (event.subscription_tier) {
819
- subscriptionTier = event.subscription_tier;
820
- }
821
-
822
- if (event.event === "use item" && event.item_type === "Ancient Compass") {
823
- usedAncientCompass = true;
824
- }
825
-
826
- if (event.event === "real money purchase" && event.product === "Lucky Charm Pack") {
827
- boughtLuckyCharm = true;
828
- }
829
-
830
- if (event.event === "guild joined" && daysSinceStart < 3) {
831
- joinedGuildEarly = true;
832
- }
833
-
834
- if (event.event === "player death" && daysSinceStart < 7) {
835
- earlyDeaths++;
836
- }
837
-
838
- if (event.event === "find treasure" && event.treasure_type === "Shadowmourne Legendary") {
839
- hasLegendaryWeapon = true;
840
- }
841
-
842
- if (event.event === "inspect") {
843
- inspectedBeforeDungeon = true;
844
- }
845
- if (event.event === "search for clues") {
846
- searchedBeforeDungeon = true;
847
- }
848
- });
849
-
850
- // Second pass: raw mutations + cloning, no flag stamping
851
- userEvents.forEach((event, idx) => {
852
- const eventTime = dayjs(event.time);
853
-
854
- // Hook 9: PROGRESSION SCALING — Quest gold scales with level.
855
- // Power curve so bucket-averaged high-level/low-level ratio >= 2.0x.
856
- if (event.event === "quest turned in") {
857
- const baseGold = event.reward_gold || 100;
858
- event.reward_gold = Math.floor(baseGold * (1 + userLevel * 0.15));
859
- }
860
-
861
- // Hook 1: CONVERSION — Ancient Compass users earn 1.5x quest
862
- // rewards plus 40% chance of bonus cloned quest.
863
- if (usedAncientCompass && event.event === "quest turned in") {
864
- event.reward_gold = Math.floor((event.reward_gold || 100) * 1.5);
865
- event.reward_xp = Math.floor((event.reward_xp || 500) * 1.5);
866
-
867
- if (chance.bool({ likelihood: 40 })) {
868
- userEvents.splice(idx + 1, 0, {
869
- ...event,
870
- time: eventTime.add(chance.integer({ min: 10, max: 120 }), 'minutes').toISOString(),
871
- quest_id: chance.pickone(questIds),
872
- reward_gold: chance.integer({ min: 100, max: 500 }),
873
- reward_xp: chance.integer({ min: 500, max: 2000 }),
874
- });
875
- }
876
- }
877
-
878
- // Hook 5: PURCHASE VALUE — Lucky charm buyers see 2x prices
879
- // and 35% chance of bonus high-value cloned purchase.
880
- if (boughtLuckyCharm) {
881
- if (event.event === "real money purchase" && event.price_usd) {
882
- event.price_usd = Math.round(event.price_usd * 2.5 * 100) / 100;
883
- }
884
- if (event.event === "item purchased" && chance.bool({ likelihood: 35 })) {
885
- const purchaseTemplate = userEvents.find(e => e.event === "real money purchase");
886
- if (purchaseTemplate) {
887
- userEvents.splice(idx + 1, 0, {
888
- ...purchaseTemplate,
889
- time: eventTime.add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
890
- user_id: event.user_id,
891
- product: chance.pickone(["Premium Currency (5000)", "Legendary Weapon Chest", "Season Pass"]),
892
- price_usd: chance.pickone([19.99, 49.99, 99.99]),
893
- payment_method: chance.pickone(["Credit Card", "PayPal"]),
894
- });
895
- }
896
- }
897
- }
898
-
899
- // Hook 10: WHALE PURCHASES — 1.8x price for whale cohort.
900
- if (isWhale && event.event === "real money purchase" && event.price_usd) {
901
- event.price_usd = Math.round(event.price_usd * 1.8 * 100) / 100;
902
- }
903
-
904
- // Hook 6: BEHAVIORS TOGETHER — inspect+search dungeons get
905
- // 85% completion rate + 2x treasure value.
906
- if (inspectedBeforeDungeon && searchedBeforeDungeon) {
907
- if (event.event === "exit dungeon" && event.completion_status !== "completed" && chance.bool({ likelihood: 85 })) {
908
- event.completion_status = "completed";
909
- }
910
- if (event.event === "find treasure") {
911
- event.treasure_value = Math.floor((event.treasure_value || 50) * 2);
912
- }
913
- }
914
-
915
- // Hook 7: TIMED RELEASE — Legendary owners get 90% combat
916
- // wins + 0.6x dungeon time.
917
- if (hasLegendaryWeapon) {
918
- if (event.event === "combat completed" && event.outcome !== "Victory" && chance.bool({ likelihood: 90 })) {
919
- event.outcome = "Victory";
920
- }
921
- if (event.event === "exit dungeon") {
922
- event.completion_status = "completed";
923
- event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * 0.6);
924
- }
925
- }
926
-
927
- // Hook 8: SUBSCRIPTION TIER — Premium/Elite get win+reward+
928
- // completion+treasure boosts. Reads tier from profile.
929
- if (subscriptionTier === "Premium" || subscriptionTier === "Elite") {
930
- const isElite = subscriptionTier === "Elite";
931
- if (event.event === "combat completed" && event.outcome !== "Victory") {
932
- const winBoost = isElite ? 70 : 50;
933
- if (chance.bool({ likelihood: winBoost })) {
934
- event.outcome = "Victory";
935
- event.loot_gained = true;
936
- }
937
- }
938
- if (event.event === "quest turned in") {
939
- const rewardMultiplier = isElite ? 1.8 : 1.4;
940
- event.reward_gold = Math.floor((event.reward_gold || 100) * rewardMultiplier);
941
- event.reward_xp = Math.floor((event.reward_xp || 500) * rewardMultiplier);
942
- }
943
- if (event.event === "exit dungeon") {
944
- if (event.completion_status !== "completed") {
945
- const completionBoost = isElite ? 65 : 45;
946
- if (chance.bool({ likelihood: completionBoost })) {
947
- event.completion_status = "completed";
948
- }
949
- }
950
- if (event.completion_status === "completed") {
951
- const speedBoost = isElite ? 0.7 : 0.85;
952
- event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * speedBoost);
953
- }
954
- }
955
- if (event.event === "find treasure") {
956
- const treasureBoost = isElite ? 2.0 : 1.5;
957
- event.treasure_value = Math.floor((event.treasure_value || 50) * treasureBoost);
958
- }
959
- if (event.event === "player death") {
960
- const survivalLikelihood = isElite ? 50 : 30;
961
- if (chance.bool({ likelihood: survivalLikelihood })) {
962
- event.event = "combat completed";
963
- event.outcome = "Victory";
964
- event.loot_gained = true;
965
- }
966
- }
967
- if (isElite && chance.bool({ likelihood: 5 })) {
968
- if (event.event === "quest turned in" || event.event === "exit dungeon") {
969
- const treasureTemplate = userEvents.find(e => e.event === "find treasure");
970
- if (treasureTemplate) {
971
- const treasureTypes = ["Rare Artifact", "Gold", "Weapon", "Armor"];
972
- userEvents.splice(idx + 1, 0, {
973
- ...treasureTemplate,
974
- time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
975
- user_id: event.user_id,
976
- treasure_type: chance.pickone(treasureTypes),
977
- treasure_value: chance.integer({ min: 200, max: 800 }),
978
- });
979
- }
980
- }
981
- }
982
- }
983
- });
984
-
985
- // Hook 2: CURSED WEEK — inject extra deaths in days 40-47 of
986
- // user's timeline. Cause_of_death set to "Curse" on injected.
987
- // Discover via line-chart of player-death by day-of-user-life.
988
- if (firstEventTime) {
989
- const deathTemplate = userEvents.find(e => e.event === "player death");
990
- if (deathTemplate) {
991
- const cursedStart = firstEventTime.add(40, 'days');
992
- const cursedEnd = firstEventTime.add(47, 'days');
993
- const cursedEvents = userEvents.filter(e => {
994
- const t = dayjs(e.time);
995
- return t.isAfter(cursedStart) && t.isBefore(cursedEnd);
996
- });
997
- const deathsToInject = Math.floor(cursedEvents.length * 0.6);
998
- for (let d = 0; d < deathsToInject; d++) {
999
- const sourceEvent = cursedEvents[d % cursedEvents.length];
1000
- userEvents.push({
1001
- ...deathTemplate,
1002
- time: dayjs(sourceEvent.time).add(chance.integer({ min: 1, max: 30 }), 'minutes').toISOString(),
1003
- user_id: sourceEvent.user_id,
1004
- event: "player death",
1005
- cause_of_death: "Curse",
1006
- player_level: chance.integer({ min: 1, max: 50 }),
1007
- resurrection_used: chance.bool({ likelihood: 80 }),
1008
- });
1009
- }
1010
- }
1011
- }
1012
-
1013
- // Hook 3 RETENTION + Hook 4 CHURN — early guild-joiners get
1014
- // extra cloned combat events; non-joiners with 3+ deaths in
1015
- // first week lose 70% of post-week events. No flag.
1016
- const shouldChurn = (!joinedGuildEarly && earlyDeaths >= 2) || (earlyDeaths >= 4);
1017
- if (shouldChurn) {
1018
- const firstWeekEnd = firstEventTime ? firstEventTime.add(7, 'days') : null;
1019
- for (let i = userEvents.length - 1; i >= 0; i--) {
1020
- if (firstWeekEnd && dayjs(userEvents[i].time).isAfter(firstWeekEnd) && chance.bool({ likelihood: 80 })) {
1021
- userEvents.splice(i, 1);
1022
- }
1023
- }
1024
- } else if (joinedGuildEarly) {
1025
- const lastEvent = userEvents[userEvents.length - 1];
1026
- const combatTemplate = userEvents.find(e => e.event === "combat completed");
1027
- if (lastEvent && combatTemplate && chance.bool({ likelihood: 60 })) {
1028
- userEvents.push({
1029
- ...combatTemplate,
1030
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
1031
- user_id: lastEvent.user_id,
1032
- outcome: "Victory",
1033
- loot_gained: true,
1034
- });
1035
- }
1036
- }
1037
-
1038
- // HOOK 13: COMBAT-PREP MAGIC NUMBER (in-funnel, no flags)
1039
- // Sweet 3-6 inspect+search events between quest accepted and
1040
- // fight boss → +30% loot/treasure_value on find-treasure events.
1041
- // Over 7+ → drop 25% of fight-boss completion (over-prep
1042
- // signals analysis paralysis). No flag.
1043
- const questAccept = userEvents.find(e => e.event === "quest accepted");
1044
- const bossFight = userEvents.find(e => e.event === "fight boss");
1045
- if (questAccept && bossFight) {
1046
- const aTime = dayjs(questAccept.time);
1047
- const bTime = dayjs(bossFight.time);
1048
- const prepCount = userEvents.filter(e =>
1049
- (e.event === "inspect" || e.event === "search for clues") &&
1050
- dayjs(e.time).isAfter(aTime) &&
1051
- dayjs(e.time).isBefore(bTime)
1052
- ).length;
1053
- if (prepCount >= 3 && prepCount <= 6) {
1054
- userEvents.forEach(e => {
1055
- if (e.event === "find treasure" && typeof e.treasure_value === "number") {
1056
- e.treasure_value = Math.round(e.treasure_value * 1.3);
1057
- }
1058
- });
1059
- } else if (prepCount >= 7) {
1060
- for (let i = userEvents.length - 1; i >= 0; i--) {
1061
- const ev = userEvents[i];
1062
- if (ev.event === "fight boss" && ev.victory === true && chance.bool({ likelihood: 25 })) {
1063
- ev.victory = false;
1064
- }
1065
- }
1066
- }
1067
- }
1068
- }
1069
-
1113
+ hook(record, type, meta) {
1114
+ if (type === "user") return handleUserHooks(record);
1115
+ if (type === "everything") return handleEverythingHooks(record, meta);
1070
1116
  return record;
1071
- }
1117
+ },
1072
1118
  };
1073
1119
 
1074
1120
  export default config;