make-mp-data 2.0.21 → 2.0.22

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.
@@ -21,185 +21,216 @@ import { makeSCD } from "../generators/scd.js";
21
21
  * @returns {Promise<void>}
22
22
  */
23
23
  export async function userLoop(context) {
24
- const { config, storage, defaults } = context;
25
- const chance = u.getChance();
26
- const concurrency = config?.concurrency || Math.min(os.cpus().length * 2, 16);
27
- const USER_CONN = pLimit(concurrency);
28
-
29
- const {
30
- verbose,
31
- numUsers,
32
- numEvents,
33
- isAnonymous,
34
- hasAvatar,
35
- hasAnonIds,
36
- hasSessionIds,
37
- hasLocation,
38
- funnels,
39
- userProps,
40
- scdProps,
41
- numDays,
42
- percentUsersBornInDataset = 5,
43
- } = config;
44
-
45
- const { eventData, userProfilesData, scdTableData } = storage;
46
- const avgEvPerUser = numEvents / numUsers;
47
- const startTime = Date.now();
48
-
49
- // Create batches for parallel processing
50
- const batchSize = Math.max(1, Math.ceil(numUsers / concurrency));
51
- const userPromises = [];
52
-
53
- for (let i = 0; i < numUsers; i++) {
54
- const userPromise = USER_CONN(async () => {
55
- context.incrementUserCount();
56
- const eps = Math.floor(context.getEventCount() / ((Date.now() - startTime) / 1000));
57
-
58
- if (verbose) {
59
- u.progress([
60
- ["users", context.getUserCount()],
61
- ["events", context.getEventCount()],
62
- ["eps", eps]
63
- ]);
64
- }
65
-
66
- const userId = chance.guid();
67
- const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds });
68
- const { distinct_id, created } = user;
69
- const userIsBornInDataset = chance.bool({ likelihood: percentUsersBornInDataset });
70
- let numEventsPreformed = 0;
71
-
72
- if (!userIsBornInDataset) delete user.created;
73
-
74
- // Calculate time adjustments
75
- const daysShift = context.getDaysShift();
76
- const adjustedCreated = userIsBornInDataset
77
- ? dayjs(created).subtract(daysShift, 'd')
78
- : dayjs.unix(global.FIXED_BEGIN);
79
-
80
- if (hasLocation) {
81
- const location = u.pickRandom(u.choose(defaults.locationsUsers));
82
- for (const key in location) {
83
- user[key] = location[key];
84
- }
85
- }
86
-
87
- // Profile creation
88
- const profile = await makeUserProfile(context, userProps, user);
89
-
90
- // SCD creation
91
- // @ts-ignore
92
- const scdUserTables = t.objFilter(scdProps, (scd) => scd.type === 'user' || !scd.type);
93
- const scdTableKeys = Object.keys(scdUserTables);
94
-
95
- const userSCD = {};
96
- for (const [index, key] of scdTableKeys.entries()) {
97
- const { max = 100 } = scdProps[key];
98
- const mutations = chance.integer({ min: 1, max });
99
- const changes = await makeSCD(context, scdProps[key], key, distinct_id, mutations, created);
100
- userSCD[key] = changes;
101
-
102
- await config.hook(changes, "scd-pre", {
103
- profile,
104
- type: 'user',
105
- scd: { [key]: scdProps[key] },
106
- config,
107
- allSCDs: userSCD
108
- });
109
- }
110
-
111
- let numEventsThisUserWillPreform = Math.floor(chance.normal({
112
- mean: avgEvPerUser,
113
- dev: avgEvPerUser / u.integer(u.integer(2, 5), u.integer(2, 7))
114
- }) * 0.714159265359);
115
-
116
- // Power users and low-activity users logic
117
- chance.bool({ likelihood: 20 }) ? numEventsThisUserWillPreform *= 5 : null;
118
- chance.bool({ likelihood: 15 }) ? numEventsThisUserWillPreform *= 0.333 : null;
119
- numEventsThisUserWillPreform = Math.round(numEventsThisUserWillPreform);
120
-
121
- let userFirstEventTime;
122
-
123
- const firstFunnels = funnels.filter((f) => f.isFirstFunnel).reduce(u.weighFunnels, []);
124
- const usageFunnels = funnels.filter((f) => !f.isFirstFunnel).reduce(u.weighFunnels, []);
125
-
126
- const secondsInDay = 86400;
127
- const noise = () => chance.integer({ min: 0, max: secondsInDay });
128
- let usersEvents = [];
129
-
130
- if (firstFunnels.length && userIsBornInDataset) {
131
- const firstFunnel = chance.pickone(firstFunnels, user);
132
- const firstTime = adjustedCreated.subtract(noise(), 'seconds').unix();
133
- const [data, userConverted] = await makeFunnel(context, firstFunnel, user, firstTime, profile, userSCD);
134
-
135
- const timeShift = context.getTimeShift();
136
- userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
137
- numEventsPreformed += data.length;
138
- usersEvents = usersEvents.concat(data);
139
-
140
- if (!userConverted) {
141
- // if (verbose) {
142
- // u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
143
- // }
144
- return;
145
- }
146
- } else {
147
- userFirstEventTime = adjustedCreated.subtract(noise(), 'seconds').unix();
148
- }
149
-
150
- while (numEventsPreformed < numEventsThisUserWillPreform) {
151
- if (usageFunnels.length) {
152
- const currentFunnel = chance.pickone(usageFunnels);
153
- const [data, userConverted] = await makeFunnel(context, currentFunnel, user, userFirstEventTime, profile, userSCD);
154
- numEventsPreformed += data.length;
155
- usersEvents = usersEvents.concat(data);
156
- } else {
157
- const data = await makeEvent(context, distinct_id, userFirstEventTime, u.pick(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
158
- numEventsPreformed++;
159
- usersEvents = usersEvents.concat(data);
160
- }
161
- }
162
-
163
- // Hook for processing all user events
164
- if (config.hook) {
165
- const newEvents = await config.hook(usersEvents, "everything", {
166
- profile,
167
- scd: userSCD,
168
- config,
169
- userIsBornInDataset
170
- });
171
- if (Array.isArray(newEvents)) usersEvents = newEvents;
172
- }
173
-
174
- // Store all user data
175
- await userProfilesData.hookPush(profile);
176
-
177
- if (Object.keys(userSCD).length) {
178
- for (const [key, changesArray] of Object.entries(userSCD)) {
179
- for (const changes of changesArray) {
180
- try {
181
- const target = scdTableData.filter(arr => arr.scdKey === key).pop();
182
- await target.hookPush(changes, { profile, type: 'user' });
183
- }
184
- catch (e) {
185
- // This is probably a test
186
- const target = scdTableData[0];
187
- await target.hookPush(changes, { profile, type: 'user' });
188
- }
189
- }
190
- }
191
- }
192
-
193
- await eventData.hookPush(usersEvents, { profile });
194
-
195
- if (verbose) {
196
- // u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
197
- }
198
- });
199
-
200
- userPromises.push(userPromise);
201
- }
202
-
203
- // Wait for all users to complete
204
- await Promise.all(userPromises);
24
+ const { config, storage, defaults } = context;
25
+ const chance = u.getChance();
26
+ const concurrency = config?.concurrency || Math.min(os.cpus().length * 2, 16);
27
+ const USER_CONN = pLimit(concurrency);
28
+
29
+ const {
30
+ verbose,
31
+ numUsers,
32
+ numEvents,
33
+ isAnonymous,
34
+ hasAvatar,
35
+ hasAnonIds,
36
+ hasSessionIds,
37
+ hasLocation,
38
+ funnels,
39
+ userProps,
40
+ scdProps,
41
+ numDays,
42
+ percentUsersBornInDataset = 5,
43
+ } = config;
44
+
45
+ const { eventData, userProfilesData, scdTableData } = storage;
46
+ const avgEvPerUser = numEvents / numUsers;
47
+ const startTime = Date.now();
48
+
49
+ // Create batches for parallel processing
50
+ const batchSize = Math.max(1, Math.ceil(numUsers / concurrency));
51
+ const userPromises = [];
52
+
53
+ for (let i = 0; i < numUsers; i++) {
54
+ const userPromise = USER_CONN(async () => {
55
+ context.incrementUserCount();
56
+ const eps = Math.floor(context.getEventCount() / ((Date.now() - startTime) / 1000));
57
+
58
+ if (verbose) {
59
+ u.progress([
60
+ ["users", context.getUserCount()],
61
+ ["events", context.getEventCount()],
62
+ ["eps", eps]
63
+ ]);
64
+ }
65
+
66
+ const userId = chance.guid();
67
+ const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds });
68
+ const { distinct_id, created } = user;
69
+ const userIsBornInDataset = chance.bool({ likelihood: percentUsersBornInDataset });
70
+ let numEventsPreformed = 0;
71
+
72
+ if (!userIsBornInDataset) delete user.created;
73
+
74
+ // Calculate time adjustments
75
+ const daysShift = context.getDaysShift();
76
+ const adjustedCreated = userIsBornInDataset
77
+ ? dayjs(created).subtract(daysShift, 'd')
78
+ : dayjs.unix(global.FIXED_BEGIN);
79
+
80
+ if (hasLocation) {
81
+ const location = u.pickRandom(u.choose(defaults.locationsUsers));
82
+ for (const key in location) {
83
+ user[key] = location[key];
84
+ }
85
+ }
86
+
87
+ // Profile creation
88
+ const profile = await makeUserProfile(context, userProps, user);
89
+
90
+ // Call user hook after profile creation
91
+ if (config.hook) {
92
+ await config.hook(profile, "user", {
93
+ user,
94
+ config,
95
+ userIsBornInDataset
96
+ });
97
+ }
98
+
99
+ // SCD creation
100
+ // @ts-ignore
101
+ const scdUserTables = t.objFilter(scdProps, (scd) => scd.type === 'user' || !scd.type);
102
+ const scdTableKeys = Object.keys(scdUserTables);
103
+
104
+ const userSCD = {};
105
+ for (const [index, key] of scdTableKeys.entries()) {
106
+ const { max = 100 } = scdProps[key];
107
+ const mutations = chance.integer({ min: 1, max });
108
+ const changes = await makeSCD(context, scdProps[key], key, distinct_id, mutations, created);
109
+ userSCD[key] = changes;
110
+
111
+ await config.hook(changes, "scd-pre", {
112
+ profile,
113
+ type: 'user',
114
+ scd: { [key]: scdProps[key] },
115
+ config,
116
+ allSCDs: userSCD
117
+ });
118
+ }
119
+
120
+ let numEventsThisUserWillPreform = Math.floor(chance.normal({
121
+ mean: avgEvPerUser,
122
+ dev: avgEvPerUser / u.integer(u.integer(2, 5), u.integer(2, 7))
123
+ }) * 0.714159265359);
124
+
125
+ // Power users and low-activity users logic
126
+ chance.bool({ likelihood: 20 }) ? numEventsThisUserWillPreform *= 5 : null;
127
+ chance.bool({ likelihood: 15 }) ? numEventsThisUserWillPreform *= 0.333 : null;
128
+ numEventsThisUserWillPreform = Math.round(numEventsThisUserWillPreform);
129
+
130
+ let userFirstEventTime;
131
+
132
+ const firstFunnels = funnels.filter((f) => f.isFirstFunnel)
133
+ .filter((f) => !f.conditions || matchConditions(profile, f.conditions))
134
+ .reduce(weighFunnels, []);
135
+ const usageFunnels = funnels.filter((f) => !f.isFirstFunnel)
136
+ .filter((f) => !f.conditions || matchConditions(profile, f.conditions))
137
+ .reduce(weighFunnels, []);
138
+
139
+ const secondsInDay = 86400;
140
+ const noise = () => chance.integer({ min: 0, max: secondsInDay });
141
+ let usersEvents = [];
142
+
143
+ // PATH FOR USERS BORN IN DATASET AND PERFORMING FIRST FUNNEL
144
+ if (firstFunnels.length && userIsBornInDataset) {
145
+ const firstFunnel = chance.pickone(firstFunnels, user);
146
+ const firstTime = adjustedCreated.subtract(noise(), 'seconds').unix();
147
+ const [data, userConverted] = await makeFunnel(context, firstFunnel, user, firstTime, profile, userSCD);
148
+
149
+ const timeShift = context.getTimeShift();
150
+ userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
151
+ numEventsPreformed += data.length;
152
+ usersEvents = usersEvents.concat(data);
153
+
154
+ if (!userConverted) {
155
+ // if (verbose) {
156
+ // u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
157
+ // }
158
+ return;
159
+ }
160
+ } else {
161
+ userFirstEventTime = adjustedCreated.subtract(noise(), 'seconds').unix();
162
+ }
163
+
164
+ // ALL SUBSEQUENT FUNNELS
165
+ while (numEventsPreformed < numEventsThisUserWillPreform) {
166
+ if (usageFunnels.length) {
167
+ const currentFunnel = chance.pickone(usageFunnels);
168
+ const [data, userConverted] = await makeFunnel(context, currentFunnel, user, userFirstEventTime, profile, userSCD);
169
+ numEventsPreformed += data.length;
170
+ usersEvents = usersEvents.concat(data);
171
+ } else {
172
+ const data = await makeEvent(context, distinct_id, userFirstEventTime, u.pick(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
173
+ numEventsPreformed++;
174
+ usersEvents = usersEvents.concat(data);
175
+ }
176
+ }
177
+
178
+ // Hook for processing all user events
179
+ if (config.hook) {
180
+ const newEvents = await config.hook(usersEvents, "everything", {
181
+ profile,
182
+ scd: userSCD,
183
+ config,
184
+ userIsBornInDataset
185
+ });
186
+ if (Array.isArray(newEvents)) usersEvents = newEvents;
187
+ }
188
+
189
+ // Store all user data
190
+ await userProfilesData.hookPush(profile);
191
+
192
+ if (Object.keys(userSCD).length) {
193
+ for (const [key, changesArray] of Object.entries(userSCD)) {
194
+ for (const changes of changesArray) {
195
+ try {
196
+ const target = scdTableData.filter(arr => arr.scdKey === key).pop();
197
+ await target.hookPush(changes, { profile, type: 'user' });
198
+ }
199
+ catch (e) {
200
+ // This is probably a test
201
+ const target = scdTableData[0];
202
+ await target.hookPush(changes, { profile, type: 'user' });
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ await eventData.hookPush(usersEvents, { profile });
209
+
210
+ if (verbose) {
211
+ // u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
212
+ }
213
+ });
214
+
215
+ userPromises.push(userPromise);
216
+ }
217
+
218
+ // Wait for all users to complete
219
+ await Promise.all(userPromises);
220
+ }
221
+
222
+
223
+ export function weighFunnels(acc, funnel) {
224
+ const weight = funnel?.weight || 1;
225
+ for (let i = 0; i < weight; i++) {
226
+ acc.push(funnel);
227
+ }
228
+ return acc;
229
+ }
230
+
231
+ export function matchConditions(profile, conditions) {
232
+ for (const [key, value] of Object.entries(conditions)) {
233
+ if (profile[key] !== value) return false;
234
+ }
235
+ return true;
205
236
  }
@@ -94,6 +94,9 @@ interface Funnel {
94
94
 
95
95
  /** Properties that will be attached to every event generated within this funnel journey. */
96
96
  props?: Record<string, ValueValid>;
97
+
98
+ /** User property conditions that determine which users are eligible for this funnel. Only users whose properties match these conditions will be considered for this funnel. */
99
+ conditions?: Record<string, ValueValid>;
97
100
  }
98
101
 
99
102
 
@@ -153,6 +156,4 @@ interface LookupTableSchema {
153
156
 
154
157
  /** A dictionary of attributes (columns) for the table and their possible values. */
155
158
  attributes: Record<string, ValueValid>;
156
- }
157
- /**
158
-
159
+ }
@@ -56,6 +56,7 @@ Core Requirements:
56
56
  - Use groups if the prompt involves B2B, teams, companies, or organizations.
57
57
  - Use SCD if user or group traits change over time (e.g., subscription tier).
58
58
  - Use lookup tables if events reference external entities with their own attributes (e.g., product_id, video_id).
59
+ - Use funnel conditions when different user segments or cohorts should have different behavioral patterns (e.g., premium vs free users, students vs teachers, rider vs driver, doctor vs patient).
59
60
 
60
61
  4. Available Functions: You have access to these built-in functions: date, weighNumRange, range, and the chance library.
61
62
 
@@ -1,9 +1,15 @@
1
+ /**
2
+ * this file is NOT currently used
3
+ */
4
+
1
5
  import dayjs from "dayjs";
2
6
  import utc from "dayjs/plugin/utc.js";
7
+ dayjs.extend(utc);
3
8
  import "dotenv/config";
4
- import { weighNumRange, range, date, initChance, exhaust, choose, integer, decimal } from "../../lib/utils/utils.js";
9
+ import { weighNumRange, range, date, initChance, exhaust, choose, integer, decimal, odds } from "../utils/utils.js";
10
+ const { NODE_ENV = "unknown" } = process.env;
11
+
5
12
 
6
- dayjs.extend(utc);
7
13
 
8
14
  /**
9
15
  * Dungeon template for AI-generated configurations
@@ -29,7 +35,7 @@ export function createDungeonTemplate({
29
35
  const chance = u.initChance(seed);
30
36
  const numEvents = numUsers * 100;
31
37
 
32
- /** @typedef {import("../../types.d.ts").Dungeon} Config */
38
+ /** @typedef {import("../../types.js").Dungeon} Config */
33
39
 
34
40
  /** @type {Config} */
35
41
  const config = {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @fileoverview this is a highly verbose schema for a dungeon that shows all the options available
3
+ * and how they might be implemented with extensive comments so an AI can understand it
4
+ * it is not meant to be used as a template, but rather as a reference for how to create a dungeon
5
+ * it is also used as a test for the AI to see if it can generate a dungeon with the same structure
6
+ */
1
7
 
2
8
 
3
9
 
@@ -6,8 +12,9 @@ const chance = new Chance();
6
12
  import dayjs from "dayjs";
7
13
  import utc from "dayjs/plugin/utc.js";
8
14
  dayjs.extend(utc);
9
- import { uid, comma } from "ak-tools";
10
- import { pickAWinner, weighNumRange, date, integer, weighChoices, range } from "../utils/utils.js";
15
+ import "dotenv/config";
16
+ import { weighNumRange, range, date, initChance, exhaust, choose, integer, decimal, odds } from "../../lib/utils/utils.js";
17
+ const { NODE_ENV = "unknown" } = process.env;
11
18
 
12
19
 
13
20
  /** @type {import("../../types.js").Dungeon} */
@@ -119,8 +126,9 @@ const DUNGEON = {
119
126
  */
120
127
  userProps: {
121
128
  title: ["Mr.", "Ms.", "Mrs.", "Dr.", "Prof.", "Sir", "Madam", "Lord", "Lady", "Dame", "Baron", "Baroness", "Count", "Countess", "Viscount", "Viscountess", "Marquis", "Marchioness"],
129
+ role: ["basic", "basic", "basic", "premium", "admin"], // role property that can be used in funnel conditions
122
130
  luckyNumber: weighNumRange(42, 420, .3),
123
- spiritAnimal: ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceros beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"]
131
+ spiritAnimal: ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceus beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"]
124
132
  },
125
133
  /**
126
134
  * Funnels represent intentional user journeys (e.g., sign-up, checkout),
@@ -130,10 +138,14 @@ const DUNGEON = {
130
138
  * unless otherwise specified in the prompt.
131
139
  * Funnels are the primary mechanism used to generate the example data, and it's critical sequences match events in the events array.
132
140
  * there are many different options for the funnels like:
133
- * isFirstFunnel, conversionRate, isChurnFunnel, order, props, requireRepeats, timeToConvert, weight
141
+ * isFirstFunnel, conversionRate, isChurnFunnel, order, props, requireRepeats, timeToConvert, weight, conditions
134
142
  *
135
143
  * isFirstFunnel are funnels a user will only go through once (like a sign up)
136
144
  * non isFirstFunnel are funnels a user will go through multiple times (like a purchase)
145
+ *
146
+ * conditions are used to filter which users are eligible for a specific funnel based on their user properties
147
+ * this is useful when different user segments should have different behavioral patterns
148
+ * for example: premium users might have access to advanced features, students vs teachers have different workflows
137
149
  *
138
150
  */
139
151
  funnels: [
@@ -170,6 +182,21 @@ const DUNGEON = {
170
182
  props: {
171
183
  "browsing type": ["casual", "intentional", "exploratory"] // you can add properties to the funnel
172
184
  }
185
+ },
186
+ {
187
+ name: "premium user workflow",
188
+ description: "advanced features only available to premium users",
189
+ sequence: ["page view", "view item", "save item", "checkout"],
190
+ isFirstFunnel: false,
191
+ conversionRate: 85,
192
+ timeToConvert: 3,
193
+ conditions: {
194
+ role: "premium" // only users with role "premium" are eligible for this funnel
195
+ },
196
+ order: "sequential",
197
+ props: {
198
+ "feature_tier": "premium"
199
+ }
173
200
  }
174
201
  ],
175
202
  /**
@@ -11,6 +11,7 @@ import { existsSync } from 'fs';
11
11
  dayjs.extend(utc);
12
12
  import 'dotenv/config';
13
13
  import { domainSuffix, domainPrefix } from '../templates/defaults.js';
14
+ const {NODE_ENV = "unknown"} = process.env;
14
15
 
15
16
  /** @typedef {import('../../types').Dungeon} Config */
16
17
  /** @typedef {import('../../types').EventConfig} EventConfig */
@@ -545,13 +546,7 @@ WEIGHERS
545
546
  ----
546
547
  */
547
548
 
548
- function weighFunnels(acc, funnel) {
549
- const weight = funnel?.weight || 1;
550
- for (let i = 0; i < weight; i++) {
551
- acc.push(funnel);
552
- }
553
- return acc;
554
- }
549
+
555
550
 
556
551
  /**
557
552
  * a utility function to generate a range of numbers within a given skew
@@ -1020,6 +1015,46 @@ let soupHits = 0;
1020
1015
  * @param {number} [peaks=5]
1021
1016
  */
1022
1017
  function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0) {
1018
+ if (!earliestTime) earliestTime = global.FIXED_BEGIN ? global.FIXED_BEGIN : dayjs().subtract(30, 'd').unix(); // 30 days ago
1019
+ if (!latestTime) latestTime = global.FIXED_NOW ? global.FIXED_NOW : dayjs().unix();
1020
+ const chance = getChance();
1021
+ const totalRange = latestTime - earliestTime;
1022
+ const chunkSize = totalRange / peaks;
1023
+
1024
+ // Select a random chunk based on the number of peaks
1025
+ const peakIndex = integer(0, peaks - 1);
1026
+ const chunkStart = earliestTime + peakIndex * chunkSize;
1027
+ const chunkEnd = chunkStart + chunkSize;
1028
+ const chunkMid = (chunkStart + chunkEnd) / 2;
1029
+
1030
+ // Generate a single timestamp within this chunk using a normal distribution centered at chunkMid
1031
+ let offset;
1032
+ let iterations = 0;
1033
+ let isValidTime = false;
1034
+ do {
1035
+ iterations++;
1036
+ soupHits++;
1037
+ offset = chance.normal({ mean: mean, dev: chunkSize / deviation });
1038
+ isValidTime = validTime(chunkMid + offset, earliestTime, latestTime);
1039
+ if (iterations > 25000) {
1040
+ throw `${iterations} iterations... exceeded`;
1041
+ }
1042
+ } while (chunkMid + offset < chunkStart || chunkMid + offset > chunkEnd);
1043
+
1044
+ try {
1045
+ return dayjs.unix(chunkMid + offset).toISOString();
1046
+ }
1047
+
1048
+ catch (e) {
1049
+ //escape hatch
1050
+ // console.log('BAD TIME', e?.message);
1051
+ if (NODE_ENV === 'dev') debugger;
1052
+ return dayjs.unix(integer(earliestTime, latestTime)).toISOString();
1053
+ }
1054
+ }
1055
+
1056
+
1057
+ function NewTimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0) {
1023
1058
  if (!earliestTime) earliestTime = global.FIXED_BEGIN ? global.FIXED_BEGIN : dayjs().subtract(30, 'd').unix(); // 30 days ago
1024
1059
  if (!latestTime) latestTime = global.FIXED_NOW ? global.FIXED_NOW : dayjs().unix();
1025
1060
  const chance = getChance();
@@ -1199,7 +1234,10 @@ function wrapFunc(obj, func, recursion = 0, parentKey = null, grandParentKey = n
1199
1234
 
1200
1235
  // }
1201
1236
 
1202
-
1237
+ const chance = getChance();
1238
+ function odds(num) {
1239
+ return chance.bool({ likelihood: num });
1240
+ }
1203
1241
 
1204
1242
  /**
1205
1243
  * makes a random-sized array of emojis
@@ -1295,7 +1333,7 @@ export {
1295
1333
  initChance,
1296
1334
  getChance,
1297
1335
  decimal,
1298
-
1336
+ odds,
1299
1337
  validTime,
1300
1338
  validEvent,
1301
1339
 
@@ -1311,7 +1349,6 @@ export {
1311
1349
  pickAWinner,
1312
1350
  quickHash,
1313
1351
  weighArray,
1314
- weighFunnels,
1315
1352
  validateEventConfig,
1316
1353
  shuffleArray,
1317
1354
  shuffleExceptFirst,