@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.
- package/CHANGELOG.md +58 -0
- package/dungeons/technical/array-of-object-lookup.js +0 -2
- package/dungeons/technical/hook-helpers-verify.js +89 -0
- package/dungeons/technical/identity-model-verify.js +47 -0
- package/dungeons/technical/pattern-aggregate-by-bin.js +41 -0
- package/dungeons/technical/pattern-attributed-by-source.js +42 -0
- package/dungeons/technical/pattern-frequency-by-frequency.js +40 -0
- package/dungeons/technical/pattern-funnel-frequency.js +54 -0
- package/dungeons/technical/pattern-ttc-by-segment.js +45 -0
- package/dungeons/technical/simple.js +3 -4
- package/dungeons/technical/simplest-schema.json +5 -0
- package/dungeons/technical/text-generation.js +1 -1
- package/dungeons/vertical/ai-platform.js +216 -164
- package/dungeons/vertical/community.js +74 -41
- package/dungeons/vertical/crypto.js +343 -233
- package/dungeons/vertical/dating.js +246 -292
- package/dungeons/vertical/devtools.js +179 -70
- package/dungeons/vertical/ecommerce.js +275 -122
- package/dungeons/vertical/education.js +345 -312
- package/dungeons/vertical/fintech.js +445 -314
- package/dungeons/vertical/fitness.js +199 -165
- package/dungeons/vertical/food-delivery.js +309 -330
- package/dungeons/vertical/gaming.js +387 -241
- package/dungeons/vertical/healthcare.js +149 -66
- package/dungeons/vertical/insurance-application.js +173 -75
- package/dungeons/vertical/logistics.js +107 -12
- package/dungeons/vertical/marketplace.js +171 -76
- package/dungeons/vertical/media.js +259 -402
- package/dungeons/vertical/real-estate.js +336 -318
- package/dungeons/vertical/sass.js +294 -288
- package/dungeons/vertical/social.js +289 -234
- package/dungeons/vertical/travel.js +66 -20
- package/index.js +17 -17
- package/lib/core/config-validator.js +226 -175
- package/lib/core/context.js +10 -24
- package/lib/core/storage.js +6 -1
- package/lib/generators/events.js +57 -104
- package/lib/generators/funnels.js +212 -93
- package/lib/generators/mirror.js +3 -2
- package/lib/generators/product-names.js +1 -1
- package/lib/generators/scd.js +2 -1
- package/lib/generators/text.js +1 -1
- package/lib/hook-helpers/_internal.js +23 -0
- package/lib/hook-helpers/cohort.js +124 -0
- package/lib/hook-helpers/identity.js +56 -0
- package/lib/hook-helpers/index.js +44 -0
- package/lib/hook-helpers/inject.js +99 -0
- package/lib/hook-helpers/mutate.js +151 -0
- package/lib/hook-helpers/timing.js +99 -0
- package/lib/hook-patterns/aggregate-per-user-by-bin.js +38 -0
- package/lib/hook-patterns/attributed-by-source.js +72 -0
- package/lib/hook-patterns/frequency-by-frequency.js +46 -0
- package/lib/hook-patterns/funnel-frequency-breakdown.js +73 -0
- package/lib/hook-patterns/index.js +14 -0
- package/lib/hook-patterns/time-to-convert-by-segment.js +41 -0
- package/lib/orchestrators/user-loop.js +143 -293
- package/lib/utils/utils.js +97 -52
- package/lib/verify/emulate-breakdown.js +281 -0
- package/lib/verify/index.js +12 -0
- package/lib/verify/verify-dungeon.js +61 -0
- package/package.json +6 -4
- package/scripts/smoke-test-all.mjs +162 -0
- package/scripts/verify-runner.mjs +72 -24
- package/types.d.ts +434 -225
- package/dungeons/technical/ad-spend-schema.json +0 -128
- package/dungeons/technical/anonymous-users-schema.json +0 -92
- package/dungeons/technical/array-of-object-lookup-schema.json +0 -191
- package/dungeons/technical/experiments-schema.json +0 -203
- package/dungeons/technical/foobar-schema.json +0 -362
- package/dungeons/technical/group-analytics-schema.json +0 -241
- package/dungeons/technical/mirror-strategies-schema.json +0 -84
- package/dungeons/technical/nested-objects-schema.json +0 -145
- package/dungeons/technical/retention-cadence-schema.json +0 -37
- package/dungeons/technical/sanity-schema.json +0 -185
- package/dungeons/technical/scale-test-schema.json +0 -70
- package/dungeons/technical/scd-schema.json +0 -467
- package/dungeons/technical/simple-schema.json +0 -362
- package/dungeons/technical/text-generation-schema.json +0 -1062
- package/dungeons/vertical/ai-platform-schema.json +0 -617
- package/dungeons/vertical/community-schema.json +0 -579
- package/dungeons/vertical/crypto-schema.json +0 -546
- package/dungeons/vertical/dating-schema.json +0 -401
- package/dungeons/vertical/devtools-schema.json +0 -601
- package/dungeons/vertical/ecommerce-schema.json +0 -604
- package/dungeons/vertical/education-schema.json +0 -5686
- package/dungeons/vertical/fintech-schema.json +0 -630
- package/dungeons/vertical/fitness-schema.json +0 -530
- package/dungeons/vertical/food-delivery-schema.json +0 -36728
- package/dungeons/vertical/gaming-schema.json +0 -2703
- package/dungeons/vertical/healthcare-schema.json +0 -549
- package/dungeons/vertical/insurance-application-schema.json +0 -485
- package/dungeons/vertical/logistics-schema.json +0 -574
- package/dungeons/vertical/marketplace-schema.json +0 -533
- package/dungeons/vertical/media-schema.json +0 -4749
- package/dungeons/vertical/real-estate-schema.json +0 -527
- package/dungeons/vertical/sass-schema.json +0 -3128
- package/dungeons/vertical/social-schema.json +0 -620
- 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 =
|
|
4
|
-
const num_users =
|
|
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 (
|
|
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
|
|
63
|
-
* and
|
|
64
|
-
* → Mixpanel:
|
|
65
|
-
* → Compare quest
|
|
66
|
-
* →
|
|
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
|
|
70
|
-
*
|
|
71
|
-
* →
|
|
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
|
|
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
|
|
81
|
+
* → Compare D30 retention vs other users — early joiners retain higher
|
|
77
82
|
*
|
|
78
83
|
* 4. CHURN: Death Spiral
|
|
79
|
-
* Players with 3+
|
|
80
|
-
*
|
|
81
|
-
* →
|
|
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
|
|
85
|
-
*
|
|
86
|
-
* →
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
130
|
-
* Cursed Week | Death rate
|
|
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 |
|
|
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 |
|
|
137
|
-
* Elite Tier |
|
|
138
|
-
* Gold Scaling (lvl 10) | Avg quest gold | ~100
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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.
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
719
|
-
};
|
|
720
|
-
userEvents.splice(idx + 1, 0, extraQuest);
|
|
884
|
+
});
|
|
721
885
|
}
|
|
722
886
|
}
|
|
723
887
|
|
|
724
|
-
// Hook
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
751
|
-
};
|
|
752
|
-
userEvents.splice(idx + 1, 0, extraPurchase);
|
|
904
|
+
});
|
|
753
905
|
}
|
|
754
906
|
}
|
|
755
907
|
}
|
|
756
908
|
|
|
757
|
-
// Hook
|
|
758
|
-
if (isWhale && event.event === "real money purchase") {
|
|
759
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
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 (
|
|
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
|
-
|
|
843
|
-
|
|
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
|
-
|
|
982
|
+
userEvents.splice(idx + 1, 0, {
|
|
859
983
|
...treasureTemplate,
|
|
860
|
-
time: eventTime.add(
|
|
984
|
+
time: eventTime.add(chance.integer({ min: 5, max: 30 }), 'minutes').toISOString(),
|
|
861
985
|
user_id: event.user_id,
|
|
862
|
-
treasure_type:
|
|
863
|
-
treasure_value:
|
|
864
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
905
|
-
|
|
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
|
-
|
|
911
|
-
|
|
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
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
}
|