@ak--47/dungeon-master 1.5.0 → 1.5.2

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 (66) hide show
  1. package/.claude/skills/create-dungeon/SKILL.md +139 -46
  2. package/.claude/skills/verify-dungeon/references/counting-semantics.md +31 -6
  3. package/.claude/skills/verify-dungeon/references/sql-recipes.md +44 -25
  4. package/.claude/skills/write-hooks/SKILL.md +31 -3
  5. package/CHANGELOG.md +85 -0
  6. package/HOOKS.md +13 -0
  7. package/dungeons/technical/ad-spend.js +41 -49
  8. package/dungeons/technical/anonymous-users.js +38 -36
  9. package/dungeons/technical/array-of-object-lookup.js +136 -153
  10. package/dungeons/technical/datagen-v15-verify.js +24 -11
  11. package/dungeons/technical/experiments.js +42 -40
  12. package/dungeons/technical/foobar.js +114 -118
  13. package/dungeons/technical/group-analytics.js +42 -40
  14. package/dungeons/technical/hook-helpers-verify.js +69 -50
  15. package/dungeons/technical/identity-model-verify.js +22 -12
  16. package/dungeons/technical/mirror-strategies.js +37 -39
  17. package/dungeons/technical/nested-objects.js +119 -118
  18. package/dungeons/technical/pattern-aggregate-by-bin.js +21 -8
  19. package/dungeons/technical/pattern-attributed-by-source.js +23 -9
  20. package/dungeons/technical/pattern-frequency-by-frequency.js +21 -8
  21. package/dungeons/technical/pattern-funnel-frequency.js +30 -15
  22. package/dungeons/technical/pattern-ttc-by-segment.js +21 -8
  23. package/dungeons/technical/retention-cadence.js +115 -112
  24. package/dungeons/technical/sanity.js +86 -80
  25. package/dungeons/technical/scale-test.js +34 -38
  26. package/dungeons/technical/scd.js +111 -128
  27. package/dungeons/technical/simple.js +134 -141
  28. package/dungeons/technical/simplest.js +54 -62
  29. package/dungeons/technical/text-generation.js +110 -146
  30. package/dungeons/vertical/ai-platform.js +296 -333
  31. package/dungeons/vertical/community.js +284 -255
  32. package/dungeons/vertical/crypto.js +395 -391
  33. package/dungeons/vertical/dating.js +411 -378
  34. package/dungeons/vertical/devtools.js +336 -298
  35. package/dungeons/vertical/ecommerce.js +316 -394
  36. package/dungeons/vertical/education.js +369 -325
  37. package/dungeons/vertical/fintech.js +358 -325
  38. package/dungeons/vertical/fitness.js +335 -291
  39. package/dungeons/vertical/food-delivery.js +343 -307
  40. package/dungeons/vertical/gaming.js +480 -444
  41. package/dungeons/vertical/healthcare.js +306 -262
  42. package/dungeons/vertical/insurance-application.js +427 -409
  43. package/dungeons/vertical/logistics.js +271 -252
  44. package/dungeons/vertical/marketplace.js +333 -323
  45. package/dungeons/vertical/media.js +382 -335
  46. package/dungeons/vertical/real-estate.js +395 -346
  47. package/dungeons/vertical/sass.js +319 -333
  48. package/dungeons/vertical/social.js +368 -316
  49. package/dungeons/vertical/travel.js +297 -295
  50. package/index.js +46 -4
  51. package/lib/core/config-validator.js +126 -28
  52. package/lib/generators/funnels.js +4 -1
  53. package/lib/orchestrators/mixpanel-sender.js +7 -0
  54. package/lib/orchestrators/user-loop.js +132 -31
  55. package/lib/templates/defaults.js +59 -59
  56. package/lib/templates/macro-presets.js +14 -2
  57. package/lib/utils/dataset-context.js +103 -0
  58. package/lib/utils/retention-curve.js +140 -0
  59. package/lib/utils/utils.js +149 -38
  60. package/lib/verify/counting.js +40 -0
  61. package/lib/verify/emulate-breakdown.js +20 -1
  62. package/lib/verify/index.js +1 -0
  63. package/lib/verify/schema-validator.js +3 -1
  64. package/package.json +11 -2
  65. package/scripts/run-dungeon.mjs +12 -1
  66. package/types.d.ts +117 -1
@@ -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,
@@ -699,386 +1110,11 @@ const config = {
699
1110
 
700
1111
  lookupTables: [],
701
1112
 
702
- /**
703
- * 🎯 ARCHITECTED ANALYTICS HOOKS — 13 patterns
704
- *
705
- * 1. CONVERSION: Ancient Compass users have 3x quest completion + 1.5x rewards
706
- * 2. TIME-BASED: "Cursed Week" (days 40-47) has 5x death rates
707
- * 3. RETENTION: Early guild joiners (first 3 days) have 80% D30 retention vs 20%
708
- * 4. CHURN: Players with 3+ deaths in first week have 70% churn rate
709
- * 5. PURCHASE VALUE: Lucky Charm buyers spend 5x more (LTV pattern)
710
- * 6. BEHAVIORS TOGETHER: inspect + search before dungeon = 85% completion vs 45%
711
- * 7. TIMED RELEASE: Legendary weapon released day 45, early adopters dominate
712
- * 8. SUBSCRIPTION TIER: Premium/Elite users have higher engagement and success
713
- * 9. PROGRESSION SCALING: Quest gold scales with player level (1 + level * 0.15)
714
- * 10. WHALE PURCHASES: ~33% of users via deterministic hash spend 1.8x more
715
- * 11. ALIGNMENT ARCHETYPE: Good=hero, Evil=villain, other=neutral (user hook)
716
- * 12. COMBAT FUNNEL TTC: Elite 0.30x faster, Free 1.40x slower combat funnel T2C
717
- * 13. COMBAT-PREP MAGIC NUMBER: 3-6 prep events = +30% treasure; 7+ = -25% boss wins
718
- */
719
- hook: function (record, type, meta) {
720
- // Hook #11: ALIGNMENT ARCHETYPE — derive archetype on user profile
721
- if (type === "user") {
722
- if (record.alignment === "Chaotic Evil" || record.alignment === "Neutral Evil") {
723
- record.archetype = "villain";
724
- } else if (record.alignment === "Lawful Good" || record.alignment === "Neutral Good") {
725
- record.archetype = "hero";
726
- } else {
727
- record.archetype = "neutral";
728
- }
729
- }
730
-
731
- // Hook #2 event-level part removed — cursed week now handled in everything hook
732
-
733
- // Hook #7: TIMED RELEASE — Legendary Weapon
734
- // (moved to everything hook — event hook fires before bunchIntoSessions
735
- // reshuffles timestamps, causing tagged events to leak across the d45 boundary)
736
-
737
- // HOOK 12 (T2C): moved to everything hook — funnel-post effects
738
- // were invisible to SQL verification.
739
-
740
- // Hooks #1, #3, #4, #5, #6, #8, #9, #10: per-user behavioral patterns
741
- if (type === "everything") {
742
- const userEvents = record;
743
- const profile = meta.profile;
744
- const datasetStart = dayjs.unix(meta.datasetStart);
745
- const LEGENDARY_WEAPON_RELEASE = datasetStart.add(45, 'days');
746
-
747
- // Stamp superProps from profile for consistency
748
- userEvents.forEach(e => {
749
- e.Platform = profile.Platform;
750
- e.graphics_quality = profile.graphics_quality;
751
- e.subscription_tier = profile.subscription_tier;
752
- });
753
-
754
- // Hook #7: TIMED RELEASE — Shadowmourne Legendary post-d45.
755
- // Runs in everything hook so timestamps are post-bunchIntoSessions.
756
- userEvents.forEach(e => {
757
- if (e.event === "find treasure" && dayjs(e.time).isAfter(LEGENDARY_WEAPON_RELEASE) && chance.bool({ likelihood: 2 })) {
758
- e.treasure_type = "Shadowmourne Legendary";
759
- e.treasure_value = 50000;
760
- }
761
- });
762
-
763
- // Hook #12: COMBAT T2C — scale time gaps in combat funnel
764
- // sequences (combat initiated → combat completed → use item).
765
- // Elite ~0.30x (faster), Premium ~0.70x, Free ~1.40x (slower).
766
- // Finds all 3-step sequences and shifts step 2/3 timestamps.
767
- const tier = profile.subscription_tier;
768
- const t2cFactor = (
769
- tier === "Elite" ? 0.30 :
770
- tier === "Premium" ? 0.70 :
771
- tier === "Free" ? 1.40 :
772
- 1.0
773
- );
774
- if (t2cFactor !== 1.0) {
775
- // Collect indices for each combat funnel step
776
- const combatInitiated = [];
777
- const combatCompleted = [];
778
- const useItem = [];
779
- for (let i = 0; i < userEvents.length; i++) {
780
- const e = userEvents[i];
781
- if (e.event === "combat initiated") combatInitiated.push(i);
782
- else if (e.event === "combat completed") combatCompleted.push(i);
783
- else if (e.event === "use item") useItem.push(i);
784
- }
785
- // Match sequences: for each combat initiated, find next
786
- // combat completed after it, then next use item after that
787
- const matched = new Set();
788
- for (const ciIdx of combatInitiated) {
789
- const ccIdx = combatCompleted.find(j => j > ciIdx && !matched.has(j));
790
- if (ccIdx === undefined) continue;
791
- const uiIdx = useItem.find(j => j > ccIdx && !matched.has(j));
792
- if (uiIdx === undefined) continue;
793
- matched.add(ccIdx);
794
- matched.add(uiIdx);
795
- // Scale gap between step 1→2 and 2→3
796
- const t0 = dayjs(userEvents[ciIdx].time);
797
- const t1 = dayjs(userEvents[ccIdx].time);
798
- const t2 = dayjs(userEvents[uiIdx].time);
799
- const gap1 = t1.diff(t0);
800
- const gap2 = t2.diff(t1);
801
- userEvents[ccIdx].time = t0.add(Math.round(gap1 * t2cFactor), 'milliseconds').toISOString();
802
- userEvents[uiIdx].time = dayjs(userEvents[ccIdx].time).add(Math.round(gap2 * t2cFactor), 'milliseconds').toISOString();
803
- }
804
- }
805
-
806
- const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
807
- const userLevel = profile.level || 1;
808
-
809
- // Track user behaviors
810
- let usedAncientCompass = false;
811
- let boughtLuckyCharm = false;
812
- let joinedGuildEarly = false;
813
- let earlyDeaths = 0;
814
- let hasLegendaryWeapon = false;
815
- let inspectedBeforeDungeon = false;
816
- let searchedBeforeDungeon = false;
817
- let subscriptionTier = "Free";
818
-
819
- // Hook #10: Whale segmentation — deterministic via user_id hash (~33%)
820
- const userId = userEvents.length > 0 ? (userEvents[0].user_id || userEvents[0].distinct_id || "") : "";
821
- const isWhale = userId.length > 0 && userId.charCodeAt(0) % 3 === 0;
822
-
823
- // First pass: identify user patterns
824
- userEvents.forEach((event) => {
825
- const eventTime = dayjs(event.time);
826
- const daysSinceStart = firstEventTime ? eventTime.diff(firstEventTime, 'days', true) : 0;
827
-
828
- if (event.subscription_tier) {
829
- subscriptionTier = event.subscription_tier;
830
- }
831
-
832
- if (event.event === "use item" && event.item_type === "Ancient Compass") {
833
- usedAncientCompass = true;
834
- }
835
-
836
- if (event.event === "real money purchase" && event.product === "Lucky Charm Pack") {
837
- boughtLuckyCharm = true;
838
- }
839
-
840
- if (event.event === "guild joined" && daysSinceStart < 3) {
841
- joinedGuildEarly = true;
842
- }
843
-
844
- if (event.event === "player death" && daysSinceStart < 7) {
845
- earlyDeaths++;
846
- }
847
-
848
- if (event.event === "find treasure" && event.treasure_type === "Shadowmourne Legendary") {
849
- hasLegendaryWeapon = true;
850
- }
851
-
852
- if (event.event === "inspect") {
853
- inspectedBeforeDungeon = true;
854
- }
855
- if (event.event === "search for clues") {
856
- searchedBeforeDungeon = true;
857
- }
858
- });
859
-
860
- // Second pass: raw mutations + cloning, no flag stamping
861
- userEvents.forEach((event, idx) => {
862
- const eventTime = dayjs(event.time);
863
-
864
- // Hook 9: PROGRESSION SCALING — Quest gold scales with level.
865
- // Power curve so bucket-averaged high-level/low-level ratio >= 2.0x.
866
- if (event.event === "quest turned in") {
867
- const baseGold = event.reward_gold || 100;
868
- event.reward_gold = Math.floor(baseGold * (1 + userLevel * 0.15));
869
- }
870
-
871
- // Hook 1: CONVERSION — Ancient Compass users earn 1.5x quest
872
- // rewards plus 40% chance of bonus cloned quest.
873
- if (usedAncientCompass && event.event === "quest turned in") {
874
- event.reward_gold = Math.floor((event.reward_gold || 100) * 1.5);
875
- event.reward_xp = Math.floor((event.reward_xp || 500) * 1.5);
876
-
877
- if (chance.bool({ likelihood: 40 })) {
878
- userEvents.splice(idx + 1, 0, {
879
- ...event,
880
- time: eventTime.add(chance.integer({ min: 10, max: 120 }), 'minutes').toISOString(),
881
- quest_id: chance.pickone(questIds),
882
- reward_gold: chance.integer({ min: 100, max: 500 }),
883
- reward_xp: chance.integer({ min: 500, max: 2000 }),
884
- });
885
- }
886
- }
887
-
888
- // Hook 5: PURCHASE VALUE — Lucky charm buyers see 2x prices
889
- // and 35% chance of bonus high-value cloned purchase.
890
- if (boughtLuckyCharm) {
891
- if (event.event === "real money purchase" && event.price_usd) {
892
- event.price_usd = Math.round(event.price_usd * 2.5 * 100) / 100;
893
- }
894
- if (event.event === "item purchased" && chance.bool({ likelihood: 35 })) {
895
- const purchaseTemplate = userEvents.find(e => e.event === "real money purchase");
896
- if (purchaseTemplate) {
897
- userEvents.splice(idx + 1, 0, {
898
- ...purchaseTemplate,
899
- time: eventTime.add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
900
- user_id: event.user_id,
901
- product: chance.pickone(["Premium Currency (5000)", "Legendary Weapon Chest", "Season Pass"]),
902
- price_usd: chance.pickone([19.99, 49.99, 99.99]),
903
- payment_method: chance.pickone(["Credit Card", "PayPal"]),
904
- });
905
- }
906
- }
907
- }
908
-
909
- // Hook 10: WHALE PURCHASES — 1.8x price for whale cohort.
910
- if (isWhale && event.event === "real money purchase" && event.price_usd) {
911
- event.price_usd = Math.round(event.price_usd * 1.8 * 100) / 100;
912
- }
913
-
914
- // Hook 6: BEHAVIORS TOGETHER — inspect+search dungeons get
915
- // 85% completion rate + 2x treasure value.
916
- if (inspectedBeforeDungeon && searchedBeforeDungeon) {
917
- if (event.event === "exit dungeon" && event.completion_status !== "completed" && chance.bool({ likelihood: 85 })) {
918
- event.completion_status = "completed";
919
- }
920
- if (event.event === "find treasure") {
921
- event.treasure_value = Math.floor((event.treasure_value || 50) * 2);
922
- }
923
- }
924
-
925
- // Hook 7: TIMED RELEASE — Legendary owners get 90% combat
926
- // wins + 0.6x dungeon time.
927
- if (hasLegendaryWeapon) {
928
- if (event.event === "combat completed" && event.outcome !== "Victory" && chance.bool({ likelihood: 90 })) {
929
- event.outcome = "Victory";
930
- }
931
- if (event.event === "exit dungeon") {
932
- event.completion_status = "completed";
933
- event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * 0.6);
934
- }
935
- }
936
-
937
- // Hook 8: SUBSCRIPTION TIER — Premium/Elite get win+reward+
938
- // completion+treasure boosts. Reads tier from profile.
939
- if (subscriptionTier === "Premium" || subscriptionTier === "Elite") {
940
- const isElite = subscriptionTier === "Elite";
941
- if (event.event === "combat completed" && event.outcome !== "Victory") {
942
- const winBoost = isElite ? 70 : 50;
943
- if (chance.bool({ likelihood: winBoost })) {
944
- event.outcome = "Victory";
945
- event.loot_gained = true;
946
- }
947
- }
948
- if (event.event === "quest turned in") {
949
- const rewardMultiplier = isElite ? 1.8 : 1.4;
950
- event.reward_gold = Math.floor((event.reward_gold || 100) * rewardMultiplier);
951
- event.reward_xp = Math.floor((event.reward_xp || 500) * rewardMultiplier);
952
- }
953
- if (event.event === "exit dungeon") {
954
- if (event.completion_status !== "completed") {
955
- const completionBoost = isElite ? 65 : 45;
956
- if (chance.bool({ likelihood: completionBoost })) {
957
- event.completion_status = "completed";
958
- }
959
- }
960
- if (event.completion_status === "completed") {
961
- const speedBoost = isElite ? 0.7 : 0.85;
962
- event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * speedBoost);
963
- }
964
- }
965
- if (event.event === "find treasure") {
966
- const treasureBoost = isElite ? 2.0 : 1.5;
967
- event.treasure_value = Math.floor((event.treasure_value || 50) * treasureBoost);
968
- }
969
- if (event.event === "player death") {
970
- const survivalLikelihood = isElite ? 50 : 30;
971
- if (chance.bool({ likelihood: survivalLikelihood })) {
972
- event.event = "combat completed";
973
- event.outcome = "Victory";
974
- event.loot_gained = true;
975
- }
976
- }
977
- if (isElite && chance.bool({ likelihood: 5 })) {
978
- if (event.event === "quest turned in" || event.event === "exit dungeon") {
979
- const treasureTemplate = userEvents.find(e => e.event === "find treasure");
980
- if (treasureTemplate) {
981
- const treasureTypes = ["Rare Artifact", "Gold", "Weapon", "Armor"];
982
- userEvents.splice(idx + 1, 0, {
983
- ...treasureTemplate,
984
- time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
985
- user_id: event.user_id,
986
- treasure_type: chance.pickone(treasureTypes),
987
- treasure_value: chance.integer({ min: 200, max: 800 }),
988
- });
989
- }
990
- }
991
- }
992
- }
993
- });
994
-
995
- // Hook 2: CURSED WEEK — inject extra deaths in days 40-47 of
996
- // user's timeline. Cause_of_death set to "Curse" on injected.
997
- // Discover via line-chart of player-death by day-of-user-life.
998
- if (firstEventTime) {
999
- const deathTemplate = userEvents.find(e => e.event === "player death");
1000
- if (deathTemplate) {
1001
- const cursedStart = firstEventTime.add(40, 'days');
1002
- const cursedEnd = firstEventTime.add(47, 'days');
1003
- const cursedEvents = userEvents.filter(e => {
1004
- const t = dayjs(e.time);
1005
- return t.isAfter(cursedStart) && t.isBefore(cursedEnd);
1006
- });
1007
- const deathsToInject = Math.floor(cursedEvents.length * 0.6);
1008
- for (let d = 0; d < deathsToInject; d++) {
1009
- const sourceEvent = cursedEvents[d % cursedEvents.length];
1010
- userEvents.push({
1011
- ...deathTemplate,
1012
- time: dayjs(sourceEvent.time).add(chance.integer({ min: 1, max: 30 }), 'minutes').toISOString(),
1013
- user_id: sourceEvent.user_id,
1014
- event: "player death",
1015
- cause_of_death: "Curse",
1016
- player_level: chance.integer({ min: 1, max: 50 }),
1017
- resurrection_used: chance.bool({ likelihood: 80 }),
1018
- });
1019
- }
1020
- }
1021
- }
1022
-
1023
- // Hook 3 RETENTION + Hook 4 CHURN — early guild-joiners get
1024
- // extra cloned combat events; non-joiners with 3+ deaths in
1025
- // first week lose 70% of post-week events. No flag.
1026
- const shouldChurn = (!joinedGuildEarly && earlyDeaths >= 2) || (earlyDeaths >= 4);
1027
- if (shouldChurn) {
1028
- const firstWeekEnd = firstEventTime ? firstEventTime.add(7, 'days') : null;
1029
- for (let i = userEvents.length - 1; i >= 0; i--) {
1030
- if (firstWeekEnd && dayjs(userEvents[i].time).isAfter(firstWeekEnd) && chance.bool({ likelihood: 80 })) {
1031
- userEvents.splice(i, 1);
1032
- }
1033
- }
1034
- } else if (joinedGuildEarly) {
1035
- const lastEvent = userEvents[userEvents.length - 1];
1036
- const combatTemplate = userEvents.find(e => e.event === "combat completed");
1037
- if (lastEvent && combatTemplate && chance.bool({ likelihood: 60 })) {
1038
- userEvents.push({
1039
- ...combatTemplate,
1040
- time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
1041
- user_id: lastEvent.user_id,
1042
- outcome: "Victory",
1043
- loot_gained: true,
1044
- });
1045
- }
1046
- }
1047
-
1048
- // HOOK 13: COMBAT-PREP MAGIC NUMBER (in-funnel, no flags)
1049
- // Sweet 3-6 inspect+search events between quest accepted and
1050
- // fight boss → +30% loot/treasure_value on find-treasure events.
1051
- // Over 7+ → drop 25% of fight-boss completion (over-prep
1052
- // signals analysis paralysis). No flag.
1053
- const questAccept = userEvents.find(e => e.event === "quest accepted");
1054
- const bossFight = userEvents.find(e => e.event === "fight boss");
1055
- if (questAccept && bossFight) {
1056
- const aTime = dayjs(questAccept.time);
1057
- const bTime = dayjs(bossFight.time);
1058
- const prepCount = userEvents.filter(e =>
1059
- (e.event === "inspect" || e.event === "search for clues") &&
1060
- dayjs(e.time).isAfter(aTime) &&
1061
- dayjs(e.time).isBefore(bTime)
1062
- ).length;
1063
- if (prepCount >= 3 && prepCount <= 6) {
1064
- userEvents.forEach(e => {
1065
- if (e.event === "find treasure" && typeof e.treasure_value === "number") {
1066
- e.treasure_value = Math.round(e.treasure_value * 1.3);
1067
- }
1068
- });
1069
- } else if (prepCount >= 7) {
1070
- for (let i = userEvents.length - 1; i >= 0; i--) {
1071
- const ev = userEvents[i];
1072
- if (ev.event === "fight boss" && ev.victory === true && chance.bool({ likelihood: 25 })) {
1073
- ev.victory = false;
1074
- }
1075
- }
1076
- }
1077
- }
1078
- }
1079
-
1113
+ hook(record, type, meta) {
1114
+ if (type === "user") return handleUserHooks(record);
1115
+ if (type === "everything") return handleEverythingHooks(record, meta);
1080
1116
  return record;
1081
- }
1117
+ },
1082
1118
  };
1083
1119
 
1084
1120
  export default config;