@ak--47/dungeon-master 1.3.0 → 1.4.0

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 (98) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dungeons/technical/array-of-object-lookup.js +0 -2
  3. package/dungeons/technical/hook-helpers-verify.js +89 -0
  4. package/dungeons/technical/identity-model-verify.js +47 -0
  5. package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
  6. package/dungeons/technical/pattern-attributed-by-source.js +42 -0
  7. package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
  8. package/dungeons/technical/pattern-funnel-frequency.js +54 -0
  9. package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
  10. package/dungeons/technical/simple.js +3 -4
  11. package/dungeons/technical/simplest-schema.json +5 -0
  12. package/dungeons/technical/text-generation.js +1 -1
  13. package/dungeons/vertical/ai-platform.js +216 -164
  14. package/dungeons/vertical/community.js +74 -41
  15. package/dungeons/vertical/crypto.js +343 -233
  16. package/dungeons/vertical/dating.js +246 -292
  17. package/dungeons/vertical/devtools.js +179 -70
  18. package/dungeons/vertical/ecommerce.js +275 -122
  19. package/dungeons/vertical/education.js +345 -312
  20. package/dungeons/vertical/fintech.js +445 -314
  21. package/dungeons/vertical/fitness.js +199 -165
  22. package/dungeons/vertical/food-delivery.js +309 -330
  23. package/dungeons/vertical/gaming.js +387 -241
  24. package/dungeons/vertical/healthcare.js +149 -66
  25. package/dungeons/vertical/insurance-application.js +173 -75
  26. package/dungeons/vertical/logistics.js +107 -12
  27. package/dungeons/vertical/marketplace.js +171 -76
  28. package/dungeons/vertical/media.js +259 -402
  29. package/dungeons/vertical/real-estate.js +336 -318
  30. package/dungeons/vertical/sass.js +294 -288
  31. package/dungeons/vertical/social.js +289 -234
  32. package/dungeons/vertical/travel.js +66 -20
  33. package/index.js +17 -17
  34. package/lib/core/config-validator.js +226 -175
  35. package/lib/core/context.js +10 -24
  36. package/lib/core/storage.js +6 -1
  37. package/lib/generators/events.js +57 -104
  38. package/lib/generators/funnels.js +212 -93
  39. package/lib/generators/mirror.js +3 -2
  40. package/lib/generators/product-names.js +1 -1
  41. package/lib/generators/scd.js +2 -1
  42. package/lib/generators/text.js +1 -1
  43. package/lib/hook-helpers/_internal.js +23 -0
  44. package/lib/hook-helpers/cohort.js +124 -0
  45. package/lib/hook-helpers/identity.js +56 -0
  46. package/lib/hook-helpers/index.js +44 -0
  47. package/lib/hook-helpers/inject.js +99 -0
  48. package/lib/hook-helpers/mutate.js +151 -0
  49. package/lib/hook-helpers/timing.js +99 -0
  50. package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
  51. package/lib/hook-patterns/attributed-by-source.js +72 -0
  52. package/lib/hook-patterns/frequency-by-frequency.js +46 -0
  53. package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
  54. package/lib/hook-patterns/index.js +14 -0
  55. package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
  56. package/lib/orchestrators/user-loop.js +143 -293
  57. package/lib/utils/utils.js +97 -52
  58. package/lib/verify/emulate-breakdown.js +281 -0
  59. package/lib/verify/index.js +12 -0
  60. package/lib/verify/verify-dungeon.js +61 -0
  61. package/package.json +6 -4
  62. package/scripts/smoke-test-all.mjs +162 -0
  63. package/scripts/verify-runner.mjs +72 -24
  64. package/types.d.ts +434 -225
  65. package/dungeons/technical/ad-spend-schema.json +0 -128
  66. package/dungeons/technical/anonymous-users-schema.json +0 -92
  67. package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
  68. package/dungeons/technical/experiments-schema.json +0 -203
  69. package/dungeons/technical/foobar-schema.json +0 -362
  70. package/dungeons/technical/group-analytics-schema.json +0 -241
  71. package/dungeons/technical/mirror-strategies-schema.json +0 -84
  72. package/dungeons/technical/nested-objects-schema.json +0 -145
  73. package/dungeons/technical/retention-cadence-schema.json +0 -37
  74. package/dungeons/technical/sanity-schema.json +0 -185
  75. package/dungeons/technical/scale-test-schema.json +0 -70
  76. package/dungeons/technical/scd-schema.json +0 -467
  77. package/dungeons/technical/simple-schema.json +0 -362
  78. package/dungeons/technical/text-generation-schema.json +0 -1062
  79. package/dungeons/vertical/ai-platform-schema.json +0 -617
  80. package/dungeons/vertical/community-schema.json +0 -579
  81. package/dungeons/vertical/crypto-schema.json +0 -546
  82. package/dungeons/vertical/dating-schema.json +0 -401
  83. package/dungeons/vertical/devtools-schema.json +0 -601
  84. package/dungeons/vertical/ecommerce-schema.json +0 -604
  85. package/dungeons/vertical/education-schema.json +0 -5686
  86. package/dungeons/vertical/fintech-schema.json +0 -630
  87. package/dungeons/vertical/fitness-schema.json +0 -530
  88. package/dungeons/vertical/food-delivery-schema.json +0 -36728
  89. package/dungeons/vertical/gaming-schema.json +0 -2703
  90. package/dungeons/vertical/healthcare-schema.json +0 -549
  91. package/dungeons/vertical/insurance-application-schema.json +0 -485
  92. package/dungeons/vertical/logistics-schema.json +0 -574
  93. package/dungeons/vertical/marketplace-schema.json +0 -533
  94. package/dungeons/vertical/media-schema.json +0 -4749
  95. package/dungeons/vertical/real-estate-schema.json +0 -527
  96. package/dungeons/vertical/sass-schema.json +0 -3128
  97. package/dungeons/vertical/social-schema.json +0 -620
  98. package/dungeons/vertical/travel-schema.json +0 -580
@@ -1,7 +1,7 @@
1
1
  // ── TWEAK THESE ──
2
2
  const SEED = "questforge";
3
- const num_days = 100;
4
- const num_users = 5_000;
3
+ const num_days = 120;
4
+ const num_users = 10_000;
5
5
  const avg_events_per_user_per_day = 1.2;
6
6
  let token = "your-mixpanel-token";
7
7
 
@@ -16,7 +16,6 @@ import * as v from "ak-tools";
16
16
 
17
17
  dayjs.extend(utc);
18
18
  const chance = u.initChance(SEED);
19
-
20
19
  /** @typedef {import("../../types").Dungeon} Config */
21
20
 
22
21
  /*
@@ -55,92 +54,254 @@ const chance = u.initChance(SEED);
55
54
 
56
55
  /*
57
56
  * ═══════════════════════════════════════════════════════════════════════════════
58
- * ANALYTICS HOOKS (11 architected patterns)
57
+ * ANALYTICS HOOKS (13 hooks)
59
58
  * ═══════════════════════════════════════════════════════════════════════════════
60
59
  *
60
+ * NOTE: All cohort effects are HIDDEN — no flag stamping. Discoverable via
61
+ * behavioral cohorts (count event per user), raw-prop breakdowns
62
+ * (treasure_type, subscription_tier, day-of-user-life), or funnel time-to-convert.
63
+ *
61
64
  * 1. CONVERSION: Ancient Compass Effect
62
- * Players who use the "Ancient Compass" item have 3x quest completion rate
63
- * and earn 1.5x more rewards.
64
- * → Mixpanel: Segment users by "use item" where item_type = "Ancient Compass"
65
- * → Compare quest completion rate and average reward_gold / reward_xp
66
- * → Look for compass_user = true on quest turned in events
65
+ * Players who use the "Ancient Compass" item earn 1.5x quest reward_gold
66
+ * and reward_xp on subsequent quest-turned-in events.
67
+ * → Mixpanel: Cohort A = users who fired "use item" with item_type="Ancient Compass"
68
+ * → Compare avg reward_gold / reward_xp on "quest turned in" between A and rest
69
+ * → Expected: A ~ 1.5x rest
67
70
  *
68
- * 2. TIME-BASED: Cursed Week (days 40-47)
69
- * Death rates spike 5x, dungeon completion plummets, resurrection usage 4x.
70
- * Mixpanel: Chart "player death" count by day — clear spike days 40-47
71
- * → Filter cause_of_death = "Curse" and cursed_week = true
71
+ * 2. TIME-BASED: Cursed Week (days 40-47 of user life)
72
+ * Death rates spike heavily during the user's days 40-47 (relative to their
73
+ * first event). Injected deaths carry cause_of_death="Curse".
74
+ * → Mixpanel: Chart "player death" count by day-of-user-life
75
+ * → Filter cause_of_death = "Curse" — clear cluster in user's days 40-47
72
76
  *
73
77
  * 3. RETENTION: Early Guild Joiners
74
- * Players who join a guild within first 3 days have 80% D30 retention vs 20%.
78
+ * Players who fire "guild joined" within first 3 days get extra cloned
79
+ * "combat completed" events; non-joiners with 3+ early deaths churn.
75
80
  * → Mixpanel: Cohort users who did "guild joined" within first 3 days
76
- * → Compare D30 retention rate; look for guild_member_retained = true
81
+ * → Compare D30 retention vs other users early joiners retain higher
77
82
  *
78
83
  * 4. CHURN: Death Spiral
79
- * Players with 3+ deaths in first week have 70% churn rate (events removed).
80
- * Mixpanel: Segment users by count of "player death" in first 7 days
81
- * → Bucket: 0-2, 3-4, 5+ deaths compare events after day 7
84
+ * Players with 3+ "player death" events in first 7 days lose 70% of their
85
+ * post-week-1 events.
86
+ * → Mixpanel: Bucket users by count of "player death" in first 7 days
87
+ * → Compare event volume after day 7 — 3+ deaths bucket has ~30% baseline
82
88
  *
83
89
  * 5. PURCHASE VALUE: Lucky Charm LTV
84
- * Lucky Charm Pack buyers become 5x higher LTV customers.
85
- * Mixpanel: Segment by "real money purchase" where product = "Lucky Charm Pack"
86
- * → Compare total revenue, purchase frequency; look for lucky_charm_effect = true
87
- *
88
- * 6. BEHAVIORS TOGETHER: Strategic Explorers
89
- * Players who both "inspect" AND "search for clues" before dungeons have
90
- * 85% dungeon completion (vs 45%) and 2x treasure value.
91
- * → Look for strategic_explorer = true on exit dungeon / find treasure events
92
- *
93
- * 7. TIMED RELEASE: Shadowmourne Legendary Weapon (day 45)
94
- * 2% of players find the legendary weapon after release. They get 90% combat
95
- * win rate (vs 60%) and complete dungeons 40% faster.
96
- * Filter "find treasure" where treasure_type = "Shadowmourne Legendary"
97
- * → Look for legendary_weapon_equipped = true
98
- *
99
- * 8. SUBSCRIPTION TIER: Premium/Elite Advantage
100
- * Premium: 50% better combat wins, 1.4x rewards, 45% higher dungeon completion
101
- * Elite: 70% better combat wins, 1.8x rewards, 65% higher dungeon completion,
102
- * bonus treasure events, reduced death rates.
103
- * Segment by subscription_tier super property
104
- * → Look for subscriber_advantage = "Premium" or "Elite"
105
- *
106
- * 9. PROGRESSION SCALING: Gold Reward by Level
107
- * Quest gold reward scales with player level: reward_gold *= (1 + level * 0.1).
108
- * A level-10 player earns 2x gold and a level-20 player earns 3x gold vs level-1.
109
- * Mixpanel: Insights on "quest turned in", avg of reward_gold, breakdown by level
110
- * → Expected: linear positive correlation between level and avg reward_gold
111
- *
112
- * 10. WHALE PURCHASES: Top-Spender Cohort
113
- * ~33% of users (deterministic via user_id hash) are "whales" who spend 1.8x
114
- * on real money purchases and are tagged is_whale: true.
115
- * → Mixpanel: Insights on "real money purchase", avg of price_usd, breakdown by is_whale
116
- * → Expected: is_whale=true ~1.8x higher avg purchase amount than is_whale=false
117
- *
118
- * 11. ALIGNMENT ARCHETYPE: Hero / Villain / Neutral
119
- * User profiles are enriched with an "archetype" derived from alignment:
120
- * Good "hero" (~22%), Evil → "villain" (~22%), other → "neutral" (~56%).
121
- * → Mixpanel: Insights, total unique users, breakdown by user profile "archetype"
90
+ * Users who purchase "Lucky Charm Pack" get 2x price_usd on subsequent
91
+ * real-money purchases under $49.99 + 35% chance of bonus cloned purchase.
92
+ * → Mixpanel: Cohort A = users with any "real money purchase" where product="Lucky Charm Pack"
93
+ * → Compare avg price_usd and total revenue between A and rest
94
+ * Expected: A ~ 1.5x avg purchase price
95
+ *
96
+ * 6. STRATEGIC EXPLORERS (BEHAVIORS TOGETHER everything)
97
+ *
98
+ * PATTERN: Players who fire BOTH "inspect" AND "search for clues" events
99
+ * have 85% dungeon completion (vs 45% baseline) and earn 2x treasure value.
100
+ * No flag derive cohort by joining users who did both event types.
101
+ *
102
+ * HOW TO FIND IT IN MIXPANEL:
103
+ *
104
+ * Report 1: Dungeon Completion by Strategic Cohort
105
+ * - Cohort A: users who fired BOTH "inspect" AND "search for clues"
106
+ * - Cohort B: rest
107
+ * - Event: "exit dungeon"
108
+ * - Filter: completion_status = "completed", divide by total exit dungeon
109
+ * - Expected: A ~ 85% completion vs B ~ 45%
110
+ *
111
+ * Report 2: Treasure Value by Strategic Cohort
112
+ * - Cohort A vs B (as above)
113
+ * - Event: "find treasure"
114
+ * - Measure: Average of "treasure_value"
115
+ * - Expected: A ~ 2x B
116
+ *
117
+ * REAL-WORLD ANALOGUE: Players who scout and prepare before encounters
118
+ * consistently outperform those who rush in.
119
+ *
120
+ * 7. SHADOWMOURNE LEGENDARY WEAPON (TIMED RELEASE event)
121
+ *
122
+ * PATTERN: At day 45, the "Shadowmourne Legendary" weapon releases.
123
+ * 2% of players find it after release. Equipped wielders get 90%
124
+ * combat win rate (vs 60%) and complete dungeons 40% faster.
125
+ *
126
+ * HOW TO FIND IT IN MIXPANEL:
127
+ *
128
+ * Report 1: Legendary Drop Adoption Over Time
129
+ * - Report type: Insights
130
+ * - Event: "find treasure"
131
+ * - Measure: Total
132
+ * - Filter: treasure_type = "Shadowmourne Legendary"
133
+ * - Line chart by day
134
+ * - Expected: zero before day 45, then small steady stream after
135
+ *
136
+ * Report 2: Combat Win Rate by Legendary Cohort
137
+ * - Cohort A: users who fired "find treasure" with treasure_type="Shadowmourne Legendary"
138
+ * - Cohort B: rest
139
+ * - Event: "combat completed"
140
+ * - Measure: count where outcome="Victory" / total combat completed
141
+ * - Expected: A ~ 90% wins vs B ~ baseline
142
+ *
143
+ * REAL-WORLD ANALOGUE: Patch-day legendary drops create a small power-
144
+ * user cohort that dominates leaderboards and PvP for weeks after.
145
+ *
146
+ * 8. PREMIUM / ELITE SUBSCRIBER ADVANTAGE (SUBSCRIPTION TIER — everything)
147
+ *
148
+ * PATTERN: Premium subscribers get 50% better combat wins, 1.4x rewards,
149
+ * 45% higher dungeon completion. Elite subscribers get 70% better combat
150
+ * wins, 1.8x rewards, 65% higher dungeon completion, bonus treasure events,
151
+ * and reduced death rates. No flag — discover via subscription_tier breakdown.
152
+ *
153
+ * HOW TO FIND IT IN MIXPANEL:
154
+ *
155
+ * Report 1: Quest Rewards by Subscription Tier
156
+ * - Report type: Insights
157
+ * - Event: "quest turned in"
158
+ * - Measure: Average of "reward_gold"
159
+ * - Breakdown: "subscription_tier"
160
+ * - Expected: Premium ~ 1.4x, Elite ~ 1.8x vs Free baseline
161
+ *
162
+ * Report 2: Dungeon Completion by Tier
163
+ * - Report type: Insights
164
+ * - Event: "exit dungeon"
165
+ * - Measure: completion_status="completed" / total exit dungeon
166
+ * - Breakdown: "subscription_tier"
167
+ * - Expected: Free ~ 45%, Premium ~ 65%, Elite ~ 75%
168
+ *
169
+ * REAL-WORLD ANALOGUE: Subscription tiers in live-service games confer
170
+ * XP/loot/rest bonuses that translate into measurable progress speed.
171
+ *
172
+ * 9. GOLD REWARD BY LEVEL (PROGRESSION SCALING — everything)
173
+ *
174
+ * PATTERN: Quest gold reward scales with player level using
175
+ * reward_gold *= (1 + level * 0.1). Level-10 earns 2x, level-20 earns
176
+ * 3x vs level-1. No flag — discover via user-property level breakdown.
177
+ *
178
+ * HOW TO FIND IT IN MIXPANEL:
179
+ *
180
+ * Report 1: Avg Gold per Quest by Player Level
181
+ * - Report type: Insights
182
+ * - Event: "quest turned in"
183
+ * - Measure: Average of "reward_gold"
184
+ * - Breakdown: user property "level" (bucketed)
185
+ * - Expected: linear ramp; level-10 ~ 2x, level-20 ~ 3x vs level-1
186
+ *
187
+ * REAL-WORLD ANALOGUE: Quest economies scale rewards with player level
188
+ * so high-level zones remain meaningfully lucrative.
189
+ *
190
+ * 10. WHALE PURCHASES (TOP-SPENDER COHORT — everything)
191
+ *
192
+ * PATTERN: ~33% of users (deterministic via user_id char % 3 hash) are
193
+ * "whales" who get 1.8x price_usd on real money purchases. No flag —
194
+ * derive cohort by ranking users by total purchase volume.
195
+ *
196
+ * HOW TO FIND IT IN MIXPANEL:
197
+ *
198
+ * Report 1: Avg Purchase by Spend Decile
199
+ * - Cohort A: top 33% of users by SUM(price_usd) on "real money purchase"
200
+ * - Cohort B: rest
201
+ * - Event: "real money purchase"
202
+ * - Measure: Average of "price_usd"
203
+ * - Expected: A ~ 1.8x B
204
+ *
205
+ * Report 2: Revenue Concentration
206
+ * - Sum price_usd by user, plot distribution
207
+ * - Expected: ~33% of users contribute the majority of revenue
208
+ *
209
+ * REAL-WORLD ANALOGUE: A small cohort of high-spending players
210
+ * ("whales") accounts for the majority of mobile-game revenue.
211
+ *
212
+ * 11. HERO / VILLAIN / NEUTRAL ARCHETYPE (USER ENRICHMENT — user)
213
+ *
214
+ * PATTERN: User profiles are enriched with an "archetype" derived
215
+ * from D&D alignment: Good -> "hero" (~22%), Evil -> "villain" (~22%),
216
+ * other -> "neutral" (~56%).
217
+ *
218
+ * HOW TO FIND IT IN MIXPANEL:
219
+ *
220
+ * Report 1: User Distribution by Archetype
221
+ * - Report type: Insights
222
+ * - Event: any event
223
+ * - Measure: Total unique users
224
+ * - Breakdown: "archetype" (user property)
225
+ * - Expected: hero ~ 22%, villain ~ 22%, neutral ~ 56%
226
+ *
227
+ * Report 2: Engagement by Archetype
228
+ * - Report type: Insights
229
+ * - Event: any event
230
+ * - Measure: Total per user (average)
231
+ * - Breakdown: "archetype"
232
+ * - Expected: similar volume per user across archetypes
233
+ *
234
+ * REAL-WORLD ANALOGUE: Player-character moral alignment is a useful
235
+ * audience segmentation lens for narrative design and content tuning.
236
+ *
237
+ * ───────────────────────────────────────────────────────────────────────────────
238
+ * 12. COMBAT FUNNEL TIME-TO-CONVERT (funnel-post)
239
+ *
240
+ * PATTERN: Elite tier completes the Combat funnel 1.4x faster (factor
241
+ * 0.71); Free tier 1.25x slower (factor 1.25). Mutates funnel timestamps.
242
+ *
243
+ * HOW TO FIND IT IN MIXPANEL:
244
+ *
245
+ * Report 1: Combat Funnel Median Time-to-Convert by Tier
246
+ * - Funnels > "combat initiated" -> "combat completed" -> "use item"
247
+ * - Measure: Median time to convert
248
+ * - Breakdown: subscription_tier
249
+ * - Expected: Elite ~ 0.71x; Free ~ 1.25x
250
+ *
251
+ * NOTE (funnel-post measurement): visible only via Mixpanel funnel
252
+ * median TTC. Cross-event MIN→MIN SQL queries on raw events do NOT
253
+ * show this — funnel-post adjusts gaps within funnel instances, not
254
+ * across the user's full event history. (This dungeon also has an
255
+ * everything-hook companion that compresses the cross-event Elite gap
256
+ * between earliest combat_initiated and earliest use_item.)
257
+ *
258
+ * ───────────────────────────────────────────────────────────────────────────────
259
+ * 13. COMBAT-PREP MAGIC NUMBER (in-funnel, everything)
260
+ *
261
+ * PATTERN: Sweet 3-6 inspect+search-for-clues events between
262
+ * quest-accepted and fight-boss → +30% on find-treasure
263
+ * treasure_value. Over 7+ → 25% of fight-boss victories flip to
264
+ * defeat (analysis paralysis). No flag.
265
+ *
266
+ * HOW TO FIND IT IN MIXPANEL:
267
+ *
268
+ * Report 1: Avg Treasure Value by Combat-Prep Bucket
269
+ * - Cohort A: users with 3-6 inspect+search between quest-accepted and fight-boss
270
+ * - Cohort B: users with 0-2
271
+ * - Event: "find treasure"
272
+ * - Measure: Average of "treasure_value"
273
+ * - Expected: A ~ 1.3x B
274
+ *
275
+ * Report 2: Boss Victory Rate on Heavy Preppers
276
+ * - Cohort C: users with >= 7 prep events
277
+ * - Cohort A: users with 3-6
278
+ * - Event: "fight boss"
279
+ * - Measure: Total filtered to victory=true / Total
280
+ * - Expected: C ~ 25% lower victory rate
281
+ *
282
+ * REAL-WORLD ANALOGUE: Calculated prep wins; over-prep is paralysis.
122
283
  *
123
284
  * ───────────────────────────────────────────────────────────────────────────────
124
285
  * EXPECTED METRICS SUMMARY
125
286
  * ───────────────────────────────────────────────────────────────────────────────
126
287
  *
127
288
  * Hook | Metric | Baseline | Hook Effect | Ratio
128
- * ──────────────────────|──────────────────────|──────────|─────────────|──────
129
- * Ancient Compass | Quest completion | 55% | 85-90% | ~1.6x
130
- * Cursed Week | Death rate | 8% | 40% | 5x
289
+ * ----------------------|----------------------|----------|-------------|------
290
+ * Ancient Compass | Quest reward (compass)| 1x | 1.5x | 1.5x
291
+ * Cursed Week | Death rate days 40-47| 1x | ~ 5x | 5x
131
292
  * Early Guild Join | D30 Retention | 20% | 80% | 4x
132
293
  * Death Spiral | Retention (3+ deaths)| 100% | 30% | 0.3x
133
- * Lucky Charm | LTV | $15 | $75 | 5x
134
- * Strategic Explorer | Dungeon completion | 45% | 85% | ~1.9x
294
+ * Lucky Charm | Avg purchase amt | 1x | 2x | 2x
295
+ * Strategic Explorer | Dungeon completion | 45% | 85% | ~ 1.9x
135
296
  * Legendary Weapon | Combat win rate | 60% | 90% | 1.5x
136
- * Premium Tier | Combat win rate | 60% | 90% | 1.5x
137
- * Elite Tier | Combat win rate | 60% | 102% | 1.7x
138
- * Gold Scaling (lvl 10) | Avg quest gold | ~100 | ~200 | 2.0x
139
- * Gold Scaling (lvl 20) | Avg quest gold | ~100 | ~300 | 3.0x
140
- * Whale Purchases | Avg real money spend | ~$15 | ~$27 | 1.8x
141
- * Hero archetype | User share | | ~22% | 2/9
142
- * Villain archetype | User share | | ~22% | 2/9
143
- * Neutral archetype | User share | | ~56% | 5/9
297
+ * Premium Tier | Quest reward | 1x | 1.4x | 1.4x
298
+ * Elite Tier | Quest reward | 1x | 1.8x | 1.8x
299
+ * Gold Scaling (lvl 10) | Avg quest gold | ~ 100 | ~ 200 | 2x
300
+ * Whale Purchases | Avg real money spend | 1x | 1.8x | 1.8x
301
+ * Hero/Villain/Neutral | User share | -- | 22/22/56% | n/a
302
+ * Combat T2C | median min by tier | 1x | 0.71/1.25x | ~ 1.8x range
303
+ * Combat-Prep Magic Num | sweet treasure_value | 1x | 1.3x | 1.3x
304
+ * Combat-Prep Magic Num | over boss victory | 1x | 0.75x | -25%
144
305
  */
145
306
 
146
307
  // Generate consistent item/location IDs for lookup tables
@@ -150,12 +311,16 @@ const itemIds = v.range(1, 301).map(n => `item_${v.uid(7)}`);
150
311
 
151
312
  /** @type {Config} */
152
313
  const config = {
314
+ version: 2,
153
315
  token,
154
316
  seed: SEED,
155
- numDays: num_days,
317
+ datasetStart: "2026-01-01T00:00:00Z",
318
+ datasetEnd: "2026-05-01T23:59:59Z",
319
+ // numDays: num_days,
156
320
  avgEventsPerUserPerDay: avg_events_per_user_per_day,
157
321
  numUsers: num_users,
158
- hasAnonIds: false,
322
+ hasAnonIds: true,
323
+ avgDevicePerUser: 2,
159
324
  hasSessionIds: true,
160
325
  format: "json",
161
326
  gzip: true,
@@ -235,6 +400,7 @@ const config = {
235
400
  event: "character created",
236
401
  weight: 1,
237
402
  isFirstEvent: true,
403
+ isAuthEvent: true,
238
404
  properties: {
239
405
  "character_class": [
240
406
  "Barbarian", "Bard", "Cleric", "Druid", "Fighter", "Monk",
@@ -278,9 +444,6 @@ const config = {
278
444
  "quest_id": questIds,
279
445
  "reward_gold": u.weighNumRange(10, 500, 0.5, 100),
280
446
  "reward_xp": u.weighNumRange(50, 2000, 0.5, 500),
281
- "compass_user": [false],
282
- "subscriber_advantage": ["Free"],
283
- "level_scaled": [false],
284
447
  }
285
448
  },
286
449
  {
@@ -299,9 +462,6 @@ const config = {
299
462
  "dungeon_id": dungeonIds,
300
463
  "time_spent_mins": u.weighNumRange(5, 120, 0.6, 30),
301
464
  "completion_status": ["completed", "abandoned", "died"],
302
- "strategic_explorer": [false],
303
- "legendary_weapon_equipped": [false],
304
- "subscriber_advantage": ["Free"],
305
465
  }
306
466
  },
307
467
  {
@@ -310,10 +470,6 @@ const config = {
310
470
  properties: {
311
471
  "treasure_type": ["Gold", "Weapon", "Armor", "Potion", "Scroll", "Rare Artifact"],
312
472
  "treasure_value": u.weighNumRange(5, 1000, 1.2, 50),
313
- "legendary_drop": [false],
314
- "strategic_explorer": [false],
315
- "subscriber_advantage": ["Free"],
316
- "elite_bonus": [false],
317
473
  }
318
474
  },
319
475
  {
@@ -323,9 +479,6 @@ const config = {
323
479
  "cause_of_death": ["Monster", "Trap", "Fall Damage", "Poison", "Friendly Fire"],
324
480
  "player_level": u.weighNumRange(1, 50),
325
481
  "resurrection_used": [false, false, false, true],
326
- "cursed_week": [false],
327
- "near_death_survival": [false],
328
- "subscriber_advantage": ["Free"],
329
482
  }
330
483
  },
331
484
  {
@@ -370,8 +523,6 @@ const config = {
370
523
  ],
371
524
  "price_usd": [4.99, 9.99, 19.99, 49.99, 99.99],
372
525
  "payment_method": ["Credit Card", "PayPal", "Apple Pay", "Google Pay"],
373
- "lucky_charm_effect": [false],
374
- "is_whale": [false],
375
526
  }
376
527
  },
377
528
  {
@@ -431,10 +582,6 @@ const config = {
431
582
  properties: {
432
583
  "outcome": ["Victory", "Defeat", "Fled"],
433
584
  "loot_gained": [false, false, false, true, true, true, true, true, true, true],
434
- "legendary_weapon_equipped": [false],
435
- "subscriber_advantage": ["Free"],
436
- "near_death_survival": [false],
437
- "guild_member_retained": [false],
438
585
  }
439
586
  },
440
587
  {
@@ -563,12 +710,6 @@ const config = {
563
710
  * 11. ALIGNMENT ARCHETYPE: Good=hero, Evil=villain, other=neutral (user hook)
564
711
  */
565
712
  hook: function (record, type, meta) {
566
- const NOW = dayjs();
567
- const DATASET_START = NOW.subtract(num_days, 'days');
568
- const CURSED_WEEK_START = DATASET_START.add(40, 'days');
569
- const CURSED_WEEK_END = DATASET_START.add(47, 'days');
570
- const LEGENDARY_WEAPON_RELEASE = DATASET_START.add(45, 'days');
571
-
572
713
  // Hook #11: ALIGNMENT ARCHETYPE — derive archetype on user profile
573
714
  if (type === "user") {
574
715
  if (record.alignment === "Chaotic Evil" || record.alignment === "Neutral Evil") {
@@ -583,16 +724,27 @@ const config = {
583
724
  // Hook #2 event-level part removed — cursed week now handled in everything hook
584
725
 
585
726
  // Hook #7: TIMED RELEASE — Legendary Weapon
586
- if (type === "event") {
587
- const EVENT_TIME = dayjs(record.time);
588
-
589
- if (record.event === "find treasure") {
590
- if (EVENT_TIME.isAfter(LEGENDARY_WEAPON_RELEASE) && chance.bool({ likelihood: 2 })) {
591
- record.treasure_type = "Shadowmourne Legendary";
592
- record.treasure_value = 50000;
593
- record.legendary_drop = true;
594
- } else {
595
- record.legendary_drop = false;
727
+ // (moved to everything hook — event hook fires before bunchIntoSessions
728
+ // reshuffles timestamps, causing tagged events to leak across the d45 boundary)
729
+
730
+ // HOOK 12 (T2C): COMBAT FUNNEL TIME-TO-CONVERT (funnel-post)
731
+ // Elite tier completes Combat funnel 1.4x faster (factor 0.71);
732
+ // Free tier 1.25x slower (factor 1.25).
733
+ if (type === "funnel-post") {
734
+ const segment = meta?.profile?.subscription_tier;
735
+ if (Array.isArray(record) && record.length > 1) {
736
+ const factor = (
737
+ segment === "Elite" ? 0.30 :
738
+ segment === "Premium" ? 0.70 :
739
+ segment === "Free" ? 1.40 :
740
+ 1.0
741
+ );
742
+ if (factor !== 1.0) {
743
+ for (let i = 1; i < record.length; i++) {
744
+ const prev = dayjs(record[i - 1].time);
745
+ const newGap = Math.round(dayjs(record[i].time).diff(prev) * factor);
746
+ record[i].time = prev.add(newGap, "milliseconds").toISOString();
747
+ }
596
748
  }
597
749
  }
598
750
  }
@@ -601,6 +753,8 @@ const config = {
601
753
  if (type === "everything") {
602
754
  const userEvents = record;
603
755
  const profile = meta.profile;
756
+ const datasetStart = dayjs.unix(meta.datasetStart);
757
+ const LEGENDARY_WEAPON_RELEASE = datasetStart.add(45, 'days');
604
758
 
605
759
  // Stamp superProps from profile for consistency
606
760
  userEvents.forEach(e => {
@@ -609,6 +763,47 @@ const config = {
609
763
  e.subscription_tier = profile.subscription_tier;
610
764
  });
611
765
 
766
+ // Hook #7: TIMED RELEASE — Shadowmourne Legendary post-d45.
767
+ // Runs in everything hook so timestamps are post-bunchIntoSessions.
768
+ userEvents.forEach(e => {
769
+ if (e.event === "find treasure" && dayjs(e.time).isAfter(LEGENDARY_WEAPON_RELEASE) && chance.bool({ likelihood: 2 })) {
770
+ e.treasure_type = "Shadowmourne Legendary";
771
+ e.treasure_value = 50000;
772
+ }
773
+ });
774
+
775
+ // Hook #12 (everything-hook companion): COMBAT T2C — also compress
776
+ // gap between earliest combat_initiated and earliest use_item across
777
+ // the user's full event history (the funnel-post hook only adjusts
778
+ // within-funnel-instance gaps; this catches cross-funnel measurement).
779
+ const tier = profile.subscription_tier;
780
+ const t2cFactor = (
781
+ tier === "Elite" ? 0.30 :
782
+ tier === "Premium" ? 0.70 :
783
+ tier === "Free" ? 1.40 :
784
+ 1.0
785
+ );
786
+ if (t2cFactor !== 1.0) {
787
+ let firstCombatTime = null;
788
+ let firstUseItemIdx = -1;
789
+ for (let i = 0; i < userEvents.length; i++) {
790
+ const e = userEvents[i];
791
+ if (e.event === "combat initiated" && firstCombatTime === null) {
792
+ firstCombatTime = dayjs(e.time);
793
+ }
794
+ if (e.event === "use item" && firstUseItemIdx === -1) {
795
+ firstUseItemIdx = i;
796
+ }
797
+ }
798
+ if (firstCombatTime !== null && firstUseItemIdx !== -1) {
799
+ const useItemTime = dayjs(userEvents[firstUseItemIdx].time);
800
+ if (useItemTime.isAfter(firstCombatTime)) {
801
+ const newGap = Math.round(useItemTime.diff(firstCombatTime) * t2cFactor);
802
+ userEvents[firstUseItemIdx].time = firstCombatTime.add(newGap, "milliseconds").toISOString();
803
+ }
804
+ }
805
+ }
806
+
612
807
  const firstEventTime = userEvents.length > 0 ? dayjs(userEvents[0].time) : null;
613
808
  const userLevel = profile.level || 1;
614
809
 
@@ -651,7 +846,7 @@ const config = {
651
846
  earlyDeaths++;
652
847
  }
653
848
 
654
- if (event.event === "find treasure" && event.legendary_drop) {
849
+ if (event.event === "find treasure" && event.treasure_type === "Shadowmourne Legendary") {
655
850
  hasLegendaryWeapon = true;
656
851
  }
657
852
 
@@ -663,168 +858,103 @@ const config = {
663
858
  }
664
859
  });
665
860
 
666
- // Second pass: modify events based on patterns
861
+ // Second pass: raw mutations + cloning, no flag stamping
667
862
  userEvents.forEach((event, idx) => {
668
863
  const eventTime = dayjs(event.time);
669
864
 
670
- // Set schema defaults for conditional properties
671
- if (event.event === "quest turned in") {
672
- event.compass_user = false;
673
- event.subscriber_advantage = "Free";
674
- event.level_scaled = false;
675
- }
676
- if (event.event === "exit dungeon") {
677
- event.strategic_explorer = false;
678
- event.legendary_weapon_equipped = false;
679
- event.subscriber_advantage = "Free";
680
- }
681
- if (event.event === "find treasure") {
682
- event.strategic_explorer = false;
683
- event.subscriber_advantage = "Free";
684
- }
685
- if (event.event === "combat completed") {
686
- event.legendary_weapon_equipped = false;
687
- event.subscriber_advantage = "Free";
688
- event.near_death_survival = false;
689
- }
690
- if (event.event === "player death") {
691
- event.near_death_survival = false;
692
- event.subscriber_advantage = "Free";
693
- }
694
- if (event.event === "real money purchase") {
695
- event.is_whale = false;
696
- }
697
-
698
- // Hook #9: PROGRESSION SCALING — Quest gold scales with level
865
+ // Hook 9: PROGRESSION SCALING Quest gold scales with level.
699
866
  if (event.event === "quest turned in") {
700
867
  const baseGold = event.reward_gold || 100;
701
868
  event.reward_gold = Math.floor(baseGold * (1 + userLevel * 0.1));
702
- event.level_scaled = true;
703
869
  }
704
870
 
705
- // Hook #1: CONVERSION — Ancient Compass users complete more quests
871
+ // Hook 1: CONVERSION — Ancient Compass users earn 1.5x quest
872
+ // rewards plus 40% chance of bonus cloned quest.
706
873
  if (usedAncientCompass && event.event === "quest turned in") {
707
874
  event.reward_gold = Math.floor((event.reward_gold || 100) * 1.5);
708
875
  event.reward_xp = Math.floor((event.reward_xp || 500) * 1.5);
709
- event.compass_user = true;
710
876
 
711
877
  if (chance.bool({ likelihood: 40 })) {
712
- const extraQuest = {
878
+ userEvents.splice(idx + 1, 0, {
713
879
  ...event,
714
880
  time: eventTime.add(chance.integer({ min: 10, max: 120 }), 'minutes').toISOString(),
715
881
  quest_id: chance.pickone(questIds),
716
882
  reward_gold: chance.integer({ min: 100, max: 500 }),
717
883
  reward_xp: chance.integer({ min: 500, max: 2000 }),
718
- compass_user: true,
719
- };
720
- userEvents.splice(idx + 1, 0, extraQuest);
884
+ });
721
885
  }
722
886
  }
723
887
 
724
- // Hook #5: PURCHASE VALUE — Lucky charm buyers spend 5x more
888
+ // Hook 5: PURCHASE VALUE — Lucky charm buyers see 2x prices
889
+ // and 35% chance of bonus high-value cloned purchase.
725
890
  if (boughtLuckyCharm) {
726
- if (event.event === "real money purchase") {
727
- if (event.price_usd) {
728
- const currentPrice = event.price_usd;
729
- if (currentPrice < 49.99) {
730
- event.price_usd = currentPrice * 2;
731
- }
732
- event.lucky_charm_effect = true;
733
- }
891
+ if (event.event === "real money purchase" && event.price_usd) {
892
+ event.price_usd = Math.round(event.price_usd * 2.5 * 100) / 100;
734
893
  }
735
-
736
894
  if (event.event === "item purchased" && chance.bool({ likelihood: 35 })) {
737
895
  const purchaseTemplate = userEvents.find(e => e.event === "real money purchase");
738
896
  if (purchaseTemplate) {
739
- const extraPurchase = {
897
+ userEvents.splice(idx + 1, 0, {
740
898
  ...purchaseTemplate,
741
899
  time: eventTime.add(chance.integer({ min: 1, max: 3 }), 'days').toISOString(),
742
900
  user_id: event.user_id,
743
- product: chance.pickone([
744
- "Premium Currency (5000)",
745
- "Legendary Weapon Chest",
746
- "Season Pass"
747
- ]),
901
+ product: chance.pickone(["Premium Currency (5000)", "Legendary Weapon Chest", "Season Pass"]),
748
902
  price_usd: chance.pickone([19.99, 49.99, 99.99]),
749
903
  payment_method: chance.pickone(["Credit Card", "PayPal"]),
750
- lucky_charm_effect: true,
751
- };
752
- userEvents.splice(idx + 1, 0, extraPurchase);
904
+ });
753
905
  }
754
906
  }
755
907
  }
756
908
 
757
- // Hook #10: WHALE PURCHASES — boost real money purchase amounts
758
- if (isWhale && event.event === "real money purchase") {
759
- if (event.price_usd) {
760
- event.price_usd = Math.round(event.price_usd * 1.8 * 100) / 100;
761
- }
762
- event.is_whale = true;
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;
763
912
  }
764
913
 
765
- // Hook #6: BEHAVIORS TOGETHER — Inspect + Search before dungeon
914
+ // Hook 6: BEHAVIORS TOGETHER — inspect+search dungeons get
915
+ // 85% completion rate + 2x treasure value.
766
916
  if (inspectedBeforeDungeon && searchedBeforeDungeon) {
767
- if (event.event === "exit dungeon") {
768
- if (event.completion_status !== "completed") {
769
- if (chance.bool({ likelihood: 85 })) {
770
- event.completion_status = "completed";
771
- event.strategic_explorer = true;
772
- }
773
- }
917
+ if (event.event === "exit dungeon" && event.completion_status !== "completed" && chance.bool({ likelihood: 85 })) {
918
+ event.completion_status = "completed";
774
919
  }
775
-
776
920
  if (event.event === "find treasure") {
777
921
  event.treasure_value = Math.floor((event.treasure_value || 50) * 2);
778
- event.strategic_explorer = true;
779
922
  }
780
923
  }
781
924
 
782
- // Hook #7: TIMED RELEASE — Legendary weapon owners dominate
925
+ // Hook 7: TIMED RELEASE — Legendary owners get 90% combat
926
+ // wins + 0.6x dungeon time.
783
927
  if (hasLegendaryWeapon) {
784
- if (event.event === "combat completed") {
785
- if (event.outcome !== "Victory") {
786
- if (chance.bool({ likelihood: 90 })) {
787
- event.outcome = "Victory";
788
- event.legendary_weapon_equipped = true;
789
- }
790
- }
928
+ if (event.event === "combat completed" && event.outcome !== "Victory" && chance.bool({ likelihood: 90 })) {
929
+ event.outcome = "Victory";
791
930
  }
792
-
793
931
  if (event.event === "exit dungeon") {
794
932
  event.completion_status = "completed";
795
933
  event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * 0.6);
796
- event.legendary_weapon_equipped = true;
797
934
  }
798
935
  }
799
936
 
800
- // Hook #8: SUBSCRIPTION TIER — Premium/Elite advantages
937
+ // Hook 8: SUBSCRIPTION TIER — Premium/Elite get win+reward+
938
+ // completion+treasure boosts. Reads tier from profile.
801
939
  if (subscriptionTier === "Premium" || subscriptionTier === "Elite") {
802
940
  const isElite = subscriptionTier === "Elite";
803
-
804
- if (event.event === "combat completed") {
805
- if (event.outcome !== "Victory") {
806
- const winBoost = isElite ? 70 : 50;
807
- if (Math.random() * 100 < winBoost) {
808
- event.outcome = "Victory";
809
- event.loot_gained = true;
810
- event.subscriber_advantage = subscriptionTier;
811
- }
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;
812
946
  }
813
947
  }
814
-
815
948
  if (event.event === "quest turned in") {
816
949
  const rewardMultiplier = isElite ? 1.8 : 1.4;
817
950
  event.reward_gold = Math.floor((event.reward_gold || 100) * rewardMultiplier);
818
951
  event.reward_xp = Math.floor((event.reward_xp || 500) * rewardMultiplier);
819
- event.subscriber_advantage = subscriptionTier;
820
952
  }
821
-
822
953
  if (event.event === "exit dungeon") {
823
954
  if (event.completion_status !== "completed") {
824
955
  const completionBoost = isElite ? 65 : 45;
825
- if (Math.random() * 100 < completionBoost) {
956
+ if (chance.bool({ likelihood: completionBoost })) {
826
957
  event.completion_status = "completed";
827
- event.subscriber_advantage = subscriptionTier;
828
958
  }
829
959
  }
830
960
  if (event.completion_status === "completed") {
@@ -832,46 +962,39 @@ const config = {
832
962
  event.time_spent_mins = Math.floor((event.time_spent_mins || 60) * speedBoost);
833
963
  }
834
964
  }
835
-
836
965
  if (event.event === "find treasure") {
837
966
  const treasureBoost = isElite ? 2.0 : 1.5;
838
967
  event.treasure_value = Math.floor((event.treasure_value || 50) * treasureBoost);
839
- event.subscriber_advantage = subscriptionTier;
840
968
  }
841
-
842
- if (event.event === "player death" && !event.cursed_week) {
843
- const survivalChance = isElite ? 50 : 30;
844
- if (Math.random() * 100 < survivalChance) {
969
+ if (event.event === "player death") {
970
+ const survivalLikelihood = isElite ? 50 : 30;
971
+ if (chance.bool({ likelihood: survivalLikelihood })) {
845
972
  event.event = "combat completed";
846
973
  event.outcome = "Victory";
847
974
  event.loot_gained = true;
848
- event.subscriber_advantage = subscriptionTier;
849
- event.near_death_survival = true;
850
975
  }
851
976
  }
852
-
853
- if (isElite && Math.random() * 100 < 15) {
977
+ if (isElite && chance.bool({ likelihood: 5 })) {
854
978
  if (event.event === "quest turned in" || event.event === "exit dungeon") {
855
979
  const treasureTemplate = userEvents.find(e => e.event === "find treasure");
856
980
  if (treasureTemplate) {
857
981
  const treasureTypes = ["Rare Artifact", "Gold", "Weapon", "Armor"];
858
- const bonusEvent = {
982
+ userEvents.splice(idx + 1, 0, {
859
983
  ...treasureTemplate,
860
- time: eventTime.add(Math.floor(Math.random() * 26) + 5, 'minutes').toISOString(),
984
+ time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
861
985
  user_id: event.user_id,
862
- treasure_type: treasureTypes[Math.floor(Math.random() * treasureTypes.length)],
863
- treasure_value: Math.floor(Math.random() * 601) + 200,
864
- subscriber_advantage: "Elite",
865
- elite_bonus: true,
866
- };
867
- userEvents.splice(idx + 1, 0, bonusEvent);
986
+ treasure_type: chance.pickone(treasureTypes),
987
+ treasure_value: chance.integer({ min: 200, max: 800 }),
988
+ });
868
989
  }
869
990
  }
870
991
  }
871
992
  }
872
993
  });
873
994
 
874
- // Hook #2: CURSED WEEK — inject death events for days 40-47 of each user's timeline
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.
875
998
  if (firstEventTime) {
876
999
  const deathTemplate = userEvents.find(e => e.event === "player death");
877
1000
  if (deathTemplate) {
@@ -884,7 +1007,7 @@ const config = {
884
1007
  const deathsToInject = Math.floor(cursedEvents.length * 0.6);
885
1008
  for (let d = 0; d < deathsToInject; d++) {
886
1009
  const sourceEvent = cursedEvents[d % cursedEvents.length];
887
- const injected = {
1010
+ userEvents.push({
888
1011
  ...deathTemplate,
889
1012
  time: dayjs(sourceEvent.time).add(chance.integer({ min: 1, max: 30 }), 'minutes').toISOString(),
890
1013
  user_id: sourceEvent.user_id,
@@ -892,41 +1015,64 @@ const config = {
892
1015
  cause_of_death: "Curse",
893
1016
  player_level: chance.integer({ min: 1, max: 50 }),
894
1017
  resurrection_used: chance.bool({ likelihood: 80 }),
895
- cursed_week: true,
896
- near_death_survival: false,
897
- subscriber_advantage: "Free",
898
- };
899
- userEvents.push(injected);
1018
+ });
900
1019
  }
901
1020
  }
902
1021
  }
903
1022
 
904
- // Hook #3 RETENTION + Hook #4 CHURN
905
- const shouldChurn = (!joinedGuildEarly && earlyDeaths >= 3) || (earlyDeaths >= 5);
906
-
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);
907
1027
  if (shouldChurn) {
908
1028
  const firstWeekEnd = firstEventTime ? firstEventTime.add(7, 'days') : null;
909
1029
  for (let i = userEvents.length - 1; i >= 0; i--) {
910
- const evt = userEvents[i];
911
- if (firstWeekEnd && dayjs(evt.time).isAfter(firstWeekEnd)) {
912
- if (chance.bool({ likelihood: 70 })) {
913
- userEvents.splice(i, 1);
914
- }
1030
+ if (firstWeekEnd && dayjs(userEvents[i].time).isAfter(firstWeekEnd) && chance.bool({ likelihood: 80 })) {
1031
+ userEvents.splice(i, 1);
915
1032
  }
916
1033
  }
917
1034
  } else if (joinedGuildEarly) {
918
1035
  const lastEvent = userEvents[userEvents.length - 1];
919
1036
  const combatTemplate = userEvents.find(e => e.event === "combat completed");
920
1037
  if (lastEvent && combatTemplate && chance.bool({ likelihood: 60 })) {
921
- const retentionEvent = {
1038
+ userEvents.push({
922
1039
  ...combatTemplate,
923
1040
  time: dayjs(lastEvent.time).add(chance.integer({ min: 1, max: 5 }), 'days').toISOString(),
924
1041
  user_id: lastEvent.user_id,
925
1042
  outcome: "Victory",
926
1043
  loot_gained: true,
927
- guild_member_retained: true,
928
- };
929
- userEvents.push(retentionEvent);
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
+ }
930
1076
  }
931
1077
  }
932
1078
  }