@schedule1-tools/mixer 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Schedule1 Tools
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Schedule1 Mixer
2
+
3
+ A package for calculating substance mixes in the game Schedule 1.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @schedule1-tools/mixer
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { mixSubstances, encodeMixState, decodeMixState } from '@schedule1-tools/mixer';
15
+
16
+ // Calculate a mix
17
+ const result = mixSubstances('OG Kush', ['A', 'B', 'C']);
18
+ console.log(result);
19
+ /*
20
+ {
21
+ effects: ['En', 'Se', 'To'],
22
+ cost: 12,
23
+ sellPrice: 42,
24
+ profit: -5,
25
+ profitMargin: -0.119
26
+ }
27
+ */
28
+
29
+ // Encode a mix state for sharing
30
+ const encoded = encodeMixState({
31
+ product: 'OG Kush',
32
+ substances: ['A', 'B', 'C'],
33
+ });
34
+ console.log(encoded); // "T0cgS3VzaDpBQkM"
35
+
36
+ // Decode a mix state
37
+ const decoded = decodeMixState('T0cgS3VzaDpBQkM');
38
+ console.log(decoded);
39
+ /*
40
+ {
41
+ product: 'OG Kush',
42
+ substances: ['A', 'B', 'C']
43
+ }
44
+ */
45
+ ```
46
+
47
+ ## Exports
48
+
49
+ The package also exports the following data objects:
50
+
51
+ - `effects`: Information about all effects
52
+ - `products`: Information about all products
53
+ - `substances`: Information about all substances
54
+ - `effectRulesBySubstance`: Rules for how substances transform effects
@@ -0,0 +1,41 @@
1
+ import { EffectCode } from '../types';
2
+ /**
3
+ * A class for efficiently managing a set of effects using a bitset
4
+ */
5
+ export declare class EffectSet {
6
+ private static readonly effectToIndex;
7
+ private static readonly indexToEffect;
8
+ private static initialized;
9
+ private bits;
10
+ /**
11
+ * Initialize the static mapping between effect codes and bit positions
12
+ */
13
+ private static initialize;
14
+ constructor(initialEffects?: EffectCode[]);
15
+ /**
16
+ * Add an effect to the set
17
+ * @returns true if the effect was added, false if it was already present
18
+ */
19
+ add(effect: EffectCode): boolean;
20
+ /**
21
+ * Remove an effect from the set
22
+ * @returns true if the effect was removed, false if it wasn't present
23
+ */
24
+ remove(effect: EffectCode): boolean;
25
+ /**
26
+ * Check if an effect is in the set
27
+ */
28
+ has(effect: EffectCode): boolean;
29
+ /**
30
+ * Convert the set to an array of effect codes
31
+ */
32
+ toArray(): EffectCode[];
33
+ /**
34
+ * Get the number of effects in the set
35
+ */
36
+ size(): number;
37
+ /**
38
+ * Create a copy of this effect set
39
+ */
40
+ clone(): EffectSet;
41
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EffectSet = void 0;
4
+ /**
5
+ * A class for efficiently managing a set of effects using a bitset
6
+ */
7
+ class EffectSet {
8
+ /**
9
+ * Initialize the static mapping between effect codes and bit positions
10
+ */
11
+ static initialize() {
12
+ if (this.initialized)
13
+ return;
14
+ const effects = [
15
+ 'Ag',
16
+ 'At',
17
+ 'Ba',
18
+ 'Be',
19
+ 'Ca',
20
+ 'Cd',
21
+ 'Cy',
22
+ 'Di',
23
+ 'El',
24
+ 'En',
25
+ 'Eu',
26
+ 'Ex',
27
+ 'Fc',
28
+ 'Fo',
29
+ 'Gi',
30
+ 'Gl',
31
+ 'Je',
32
+ 'La',
33
+ 'Lf',
34
+ 'Mu',
35
+ 'Pa',
36
+ 'Re',
37
+ 'Sc',
38
+ 'Se',
39
+ 'Sh',
40
+ 'Si',
41
+ 'Sl',
42
+ 'Sm',
43
+ 'Sn',
44
+ 'Sp',
45
+ 'To',
46
+ 'Tp',
47
+ 'Tt',
48
+ 'Zo',
49
+ ];
50
+ effects.forEach((effect, index) => {
51
+ this.effectToIndex.set(effect, index);
52
+ this.indexToEffect.set(index, effect);
53
+ });
54
+ this.initialized = true;
55
+ }
56
+ constructor(initialEffects = []) {
57
+ this.bits = 0;
58
+ EffectSet.initialize();
59
+ for (const effect of initialEffects) {
60
+ this.add(effect);
61
+ }
62
+ }
63
+ /**
64
+ * Add an effect to the set
65
+ * @returns true if the effect was added, false if it was already present
66
+ */
67
+ add(effect) {
68
+ const index = EffectSet.effectToIndex.get(effect);
69
+ if (index === undefined)
70
+ return false;
71
+ const mask = 1 << index;
72
+ const alreadyExists = (this.bits & mask) !== 0;
73
+ this.bits |= mask;
74
+ return !alreadyExists;
75
+ }
76
+ /**
77
+ * Remove an effect from the set
78
+ * @returns true if the effect was removed, false if it wasn't present
79
+ */
80
+ remove(effect) {
81
+ const index = EffectSet.effectToIndex.get(effect);
82
+ if (index === undefined)
83
+ return false;
84
+ const mask = 1 << index;
85
+ const existed = (this.bits & mask) !== 0;
86
+ this.bits &= ~mask;
87
+ return existed;
88
+ }
89
+ /**
90
+ * Check if an effect is in the set
91
+ */
92
+ has(effect) {
93
+ const index = EffectSet.effectToIndex.get(effect);
94
+ if (index === undefined)
95
+ return false;
96
+ return (this.bits & (1 << index)) !== 0;
97
+ }
98
+ /**
99
+ * Convert the set to an array of effect codes
100
+ */
101
+ toArray() {
102
+ const result = [];
103
+ for (let i = 0; i < 34; i++) {
104
+ if ((this.bits & (1 << i)) !== 0) {
105
+ const effect = EffectSet.indexToEffect.get(i);
106
+ if (effect)
107
+ result.push(effect);
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ /**
113
+ * Get the number of effects in the set
114
+ */
115
+ size() {
116
+ // Count the number of set bits using Brian Kernighan's algorithm
117
+ let count = 0;
118
+ let n = this.bits;
119
+ while (n) {
120
+ n &= n - 1;
121
+ count++;
122
+ }
123
+ return count;
124
+ }
125
+ /**
126
+ * Create a copy of this effect set
127
+ */
128
+ clone() {
129
+ const clone = new EffectSet();
130
+ clone.bits = this.bits;
131
+ return clone;
132
+ }
133
+ }
134
+ exports.EffectSet = EffectSet;
135
+ EffectSet.effectToIndex = new Map();
136
+ EffectSet.indexToEffect = new Map();
137
+ EffectSet.initialized = false;
@@ -0,0 +1,5 @@
1
+ import { MixResult, ProductType, SubstanceCode } from '../types';
2
+ /**
3
+ * Calculate the result of mixing substances with a product
4
+ */
5
+ export declare function mixSubstances(product: ProductType, substanceCodes: SubstanceCode[]): MixResult;
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mixSubstances = mixSubstances;
4
+ const effects_1 = require("../data/effects");
5
+ const products_1 = require("../data/products");
6
+ const substances_1 = require("../data/substances");
7
+ const rules_1 = require("../data/rules");
8
+ const effectSet_1 = require("./effectSet");
9
+ const MAX_EFFECTS = 8;
10
+ /**
11
+ * Calculate the result of mixing substances with a product
12
+ */
13
+ function mixSubstances(product, substanceCodes) {
14
+ if (!products_1.products[product]) {
15
+ throw new Error(`Unknown product: ${product}`);
16
+ }
17
+ const productInfo = products_1.products[product];
18
+ const effectsSet = new effectSet_1.EffectSet(productInfo.effects);
19
+ const processedEffects = new effectSet_1.EffectSet();
20
+ const removedEffects = new effectSet_1.EffectSet();
21
+ let totalCost = 0;
22
+ for (const code of substanceCodes) {
23
+ const substance = substances_1.substances[code];
24
+ if (!substance)
25
+ continue;
26
+ totalCost += substance.price;
27
+ applySubstanceRules(code, effectsSet, processedEffects, removedEffects);
28
+ if (effectsSet.size() < MAX_EFFECTS) {
29
+ for (const effect of substance.effect) {
30
+ if (!effectsSet.has(effect)) {
31
+ effectsSet.add(effect);
32
+ if (effectsSet.size() >= MAX_EFFECTS)
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ }
38
+ const finalEffects = effectsSet.toArray().slice(0, MAX_EFFECTS);
39
+ const effectValue = calculateEffectValue(finalEffects);
40
+ const productPrice = productInfo.price;
41
+ const sellPrice = Math.round(productPrice * (1 + effectValue));
42
+ const profit = sellPrice - totalCost - productPrice;
43
+ const profitMargin = Math.round((profit / sellPrice) * 100) / 100;
44
+ return {
45
+ effects: finalEffects,
46
+ cost: totalCost,
47
+ sellPrice,
48
+ profit,
49
+ profitMargin,
50
+ };
51
+ }
52
+ /**
53
+ * Apply the rules for a substance to the current effect set
54
+ */
55
+ function applySubstanceRules(substanceCode, effectsSet, processedEffects, removedEffects) {
56
+ const rules = rules_1.effectRulesBySubstance[substanceCode];
57
+ if (!rules || rules.length === 0)
58
+ return;
59
+ const initialEffects = effectsSet.clone();
60
+ const appliedRules = new Set();
61
+ // Phase 1: Apply rules where conditions are met in the initial state
62
+ for (let i = 0; i < rules.length; i++) {
63
+ const rule = rules[i];
64
+ if (checkRulePreconditions(rule, initialEffects)) {
65
+ applyReplaceEffects(rule.replace, initialEffects, effectsSet, processedEffects, removedEffects);
66
+ appliedRules.add(i);
67
+ }
68
+ }
69
+ // Phase 2: Apply rules where conditions are met after phase 1
70
+ for (let i = 0; i < rules.length; i++) {
71
+ if (appliedRules.has(i))
72
+ continue;
73
+ const rule = rules[i];
74
+ if (meetsPhaseTwo(rule, initialEffects, effectsSet, removedEffects)) {
75
+ if (canApplyTransformation(rule.replace, effectsSet)) {
76
+ applyTransformations(rule.replace, effectsSet, processedEffects);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * Check if a rule's preconditions are met
83
+ */
84
+ function checkRulePreconditions(rule, initialEffects) {
85
+ // Check if all required effects are present
86
+ for (const effect of rule.ifPresent) {
87
+ if (!initialEffects.has(effect))
88
+ return false;
89
+ }
90
+ // Check if all forbidden effects are absent
91
+ for (const effect of rule.ifNotPresent) {
92
+ if (initialEffects.has(effect))
93
+ return false;
94
+ }
95
+ // Check if at least one replaceable effect is present
96
+ for (const oldEffect of Object.keys(rule.replace)) {
97
+ if (initialEffects.has(oldEffect))
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ /**
103
+ * Check if a rule meets phase two conditions
104
+ */
105
+ function meetsPhaseTwo(rule, initialEffects, currentEffects, removedEffects) {
106
+ // All required effects must have been initially present
107
+ for (const effect of rule.ifPresent) {
108
+ if (!initialEffects.has(effect))
109
+ return false;
110
+ }
111
+ // At least one forbidden effect must have been removed
112
+ let hasRemovedForbidden = false;
113
+ for (const effect of rule.ifNotPresent) {
114
+ if (removedEffects.has(effect)) {
115
+ hasRemovedForbidden = true;
116
+ break;
117
+ }
118
+ }
119
+ if (!hasRemovedForbidden)
120
+ return false;
121
+ // All forbidden effects must be absent from current set
122
+ for (const effect of rule.ifNotPresent) {
123
+ if (currentEffects.has(effect))
124
+ return false;
125
+ }
126
+ return true;
127
+ }
128
+ /**
129
+ * Check if a transformation can be applied
130
+ */
131
+ function canApplyTransformation(replace, effectsSet) {
132
+ for (const oldEffect of Object.keys(replace)) {
133
+ if (effectsSet.has(oldEffect))
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+ /**
139
+ * Apply effect replacements
140
+ */
141
+ function applyReplaceEffects(replace, initialEffects, effectsSet, processedEffects, removedEffects) {
142
+ for (const [oldEffect, newEffect] of Object.entries(replace)) {
143
+ if (initialEffects.has(oldEffect)) {
144
+ effectsSet.remove(oldEffect);
145
+ effectsSet.add(newEffect);
146
+ processedEffects.add(oldEffect);
147
+ removedEffects.add(oldEffect);
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Apply transformations to effects
153
+ */
154
+ function applyTransformations(replace, effectsSet, processedEffects) {
155
+ for (const [oldEffect, newEffect] of Object.entries(replace)) {
156
+ if (effectsSet.has(oldEffect)) {
157
+ effectsSet.remove(oldEffect);
158
+ effectsSet.add(newEffect);
159
+ processedEffects.add(oldEffect);
160
+ }
161
+ }
162
+ }
163
+ /**
164
+ * Calculate the total value multiplier from effects
165
+ */
166
+ function calculateEffectValue(effectCodes) {
167
+ var _a;
168
+ let value = 0;
169
+ for (const code of effectCodes) {
170
+ value += ((_a = effects_1.effects[code]) === null || _a === void 0 ? void 0 : _a.price) || 0;
171
+ }
172
+ return value;
173
+ }
@@ -0,0 +1,2 @@
1
+ import { EffectCode, EffectData } from '../types';
2
+ export declare const effects: Record<EffectCode, EffectData>;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.effects = void 0;
4
+ exports.effects = {
5
+ Ca: {
6
+ name: 'Calming',
7
+ price: 0.1,
8
+ color: '#fdba74',
9
+ },
10
+ Re: {
11
+ name: 'Refreshing',
12
+ price: 0.14,
13
+ color: '#bef264',
14
+ },
15
+ En: {
16
+ name: 'Energizing',
17
+ price: 0.22,
18
+ color: '#a3e635',
19
+ },
20
+ Se: {
21
+ name: 'Sedating',
22
+ price: 0.26,
23
+ color: '#818cf8',
24
+ },
25
+ Be: {
26
+ name: 'Bright-Eyed',
27
+ price: 0.4,
28
+ color: '#67e8f9',
29
+ },
30
+ Cd: {
31
+ name: 'Calorie-Dense',
32
+ price: 0.28,
33
+ color: '#e879f9',
34
+ },
35
+ Eu: {
36
+ name: 'Euphoric',
37
+ price: 0.18,
38
+ color: '#fde68a',
39
+ },
40
+ To: {
41
+ name: 'Toxic',
42
+ price: 0,
43
+ color: '#a3e635',
44
+ },
45
+ At: {
46
+ name: 'Athletic',
47
+ price: 0.32,
48
+ color: '#7dd3fc',
49
+ },
50
+ Ba: {
51
+ name: 'Balding',
52
+ price: 0.3,
53
+ color: '#c79232',
54
+ },
55
+ Ag: {
56
+ name: 'Anti-Gravity',
57
+ price: 0.54,
58
+ color: '#3b82f6',
59
+ },
60
+ Mu: {
61
+ name: 'Munchies',
62
+ price: 0.12,
63
+ color: '#C96E57',
64
+ },
65
+ Sl: {
66
+ name: 'Slippery',
67
+ price: 0.34,
68
+ color: '#7dd3fc',
69
+ },
70
+ Gi: {
71
+ name: 'Gingeritis',
72
+ price: 0.2,
73
+ color: '#fb923c',
74
+ },
75
+ Sn: {
76
+ name: 'Sneaky',
77
+ price: 0.24,
78
+ color: '#a8a29e',
79
+ },
80
+ Tp: {
81
+ name: 'Thought-Provoking',
82
+ price: 0.44,
83
+ color: '#f9a8d4',
84
+ },
85
+ Sp: {
86
+ name: 'Spicy',
87
+ price: 0.38,
88
+ color: '#f87171',
89
+ },
90
+ Pa: {
91
+ name: 'Paranoia',
92
+ price: 0,
93
+ color: '#f87171',
94
+ },
95
+ Tt: {
96
+ name: 'Tropic Thunder',
97
+ price: 0.46,
98
+ color: '#fdba74',
99
+ },
100
+ Gl: {
101
+ name: 'Glowing',
102
+ price: 0.48,
103
+ color: '#85E459',
104
+ },
105
+ Cy: {
106
+ name: 'Cyclopean',
107
+ price: 0.56,
108
+ color: '#FEC174',
109
+ },
110
+ Fo: {
111
+ name: 'Foggy',
112
+ price: 0.36,
113
+ color: '#94a3b8',
114
+ },
115
+ Ex: {
116
+ name: 'Explosive',
117
+ price: 0,
118
+ color: '#ef4444',
119
+ },
120
+ La: {
121
+ name: 'Laxative',
122
+ price: 0,
123
+ color: '#a16207',
124
+ },
125
+ Lf: {
126
+ name: 'Long Faced',
127
+ price: 0.52,
128
+ color: '#fde047',
129
+ },
130
+ Je: {
131
+ name: 'Jennerising',
132
+ price: 0.42,
133
+ color: '#e879f9',
134
+ },
135
+ El: {
136
+ name: 'Electrifying',
137
+ price: 0.5,
138
+ color: '#22d3ee',
139
+ },
140
+ Di: {
141
+ name: 'Disorienting',
142
+ price: 0,
143
+ color: '#FE7551',
144
+ },
145
+ Sc: {
146
+ name: 'Schizophrenia',
147
+ price: 0,
148
+ color: '#645AFD',
149
+ },
150
+ Si: {
151
+ name: 'Seizure-Inducing',
152
+ price: 0,
153
+ color: '#FEE900',
154
+ },
155
+ Zo: {
156
+ name: 'Zombifying',
157
+ price: 0.58,
158
+ color: '#71AB5D',
159
+ },
160
+ Fc: {
161
+ name: 'Focused',
162
+ price: 0.16,
163
+ color: '#75F1FD',
164
+ },
165
+ Sm: {
166
+ name: 'Smelly',
167
+ price: 0,
168
+ color: '#84cc16',
169
+ },
170
+ Sh: {
171
+ name: 'Shrinking',
172
+ price: 0.6,
173
+ color: '#B6FEDA',
174
+ },
175
+ };
@@ -0,0 +1,3 @@
1
+ import { ProductData, ProductType } from '../types';
2
+ export declare const products: Record<ProductType, ProductData>;
3
+ export declare const productAbbreviations: Record<string, ProductType>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.productAbbreviations = exports.products = void 0;
4
+ exports.products = {
5
+ 'OG Kush': {
6
+ price: 35,
7
+ effects: ['Ca'],
8
+ abbreviation: 'OH',
9
+ },
10
+ 'Sour Diesel': {
11
+ price: 35,
12
+ effects: ['Re'],
13
+ abbreviation: 'SL',
14
+ },
15
+ 'Green Crack': {
16
+ price: 35,
17
+ effects: ['En'],
18
+ abbreviation: 'GK',
19
+ },
20
+ 'Grandaddy Purple': {
21
+ price: 35,
22
+ effects: ['Se'],
23
+ abbreviation: 'GE',
24
+ },
25
+ Cocaine: {
26
+ price: 150,
27
+ effects: [],
28
+ abbreviation: 'CE',
29
+ },
30
+ Meth: {
31
+ price: 70,
32
+ effects: [],
33
+ abbreviation: 'MH',
34
+ },
35
+ };
36
+ exports.productAbbreviations = Object.entries(exports.products).reduce((acc, [product, data]) => {
37
+ acc[data.abbreviation] = product;
38
+ return acc;
39
+ }, {});
@@ -0,0 +1,2 @@
1
+ import { EffectRule, SubstanceCode } from '../types';
2
+ export declare const effectRulesBySubstance: Record<SubstanceCode, EffectRule[]>;
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.effectRulesBySubstance = void 0;
4
+ exports.effectRulesBySubstance = {
5
+ A: [
6
+ { ifPresent: ['Eu'], ifNotPresent: ['La'], replace: { Eu: 'La' } },
7
+ { ifPresent: ['Fo'], ifNotPresent: ['Cy'], replace: { Fo: 'Cy' } },
8
+ { ifPresent: ['Gi'], ifNotPresent: ['Tp'], replace: { Gi: 'Tp' } },
9
+ { ifPresent: ['Mu'], ifNotPresent: ['At'], replace: { Mu: 'At' } },
10
+ { ifPresent: ['Sl'], ifNotPresent: ['Mu'], replace: { Sl: 'Mu' } },
11
+ { ifPresent: ['Sn'], ifNotPresent: ['Pa'], replace: { Sn: 'Pa' } },
12
+ { ifPresent: ['To'], ifNotPresent: ['Eu'], replace: { To: 'Eu' } },
13
+ ],
14
+ B: [
15
+ { ifPresent: ['At'], ifNotPresent: ['Mu'], replace: { At: 'Mu' } },
16
+ { ifPresent: ['Ca'], ifNotPresent: ['Be'], replace: { Ca: 'Be' } },
17
+ { ifPresent: ['Cy'], ifNotPresent: ['Fo'], replace: { Cy: 'Fo' } },
18
+ { ifPresent: ['El'], ifNotPresent: ['Re'], replace: { El: 'Re' } },
19
+ { ifPresent: ['Eu'], ifNotPresent: ['To'], replace: { Eu: 'To' } },
20
+ { ifPresent: ['Fc'], ifNotPresent: ['Ca'], replace: { Fc: 'Ca' } },
21
+ { ifPresent: ['La'], ifNotPresent: ['Eu'], replace: { La: 'Eu' } },
22
+ { ifPresent: ['Mu'], ifNotPresent: ['Sl'], replace: { Mu: 'Sl' } },
23
+ { ifPresent: ['Sh'], ifNotPresent: ['Pa'], replace: { Sh: 'Pa' } },
24
+ { ifPresent: ['Tp'], ifNotPresent: ['Gi'], replace: { Tp: 'Gi' } },
25
+ ],
26
+ C: [
27
+ { ifPresent: ['Di'], ifNotPresent: ['Gl'], replace: { Di: 'Gl' } },
28
+ { ifPresent: ['El'], ifNotPresent: ['Di'], replace: { El: 'Di' } },
29
+ { ifPresent: ['En'], ifNotPresent: ['Eu'], replace: { En: 'Eu' } },
30
+ { ifPresent: ['Eu'], ifNotPresent: ['Sp'], replace: { Eu: 'Sp' } },
31
+ { ifPresent: ['Gi'], ifNotPresent: ['Sm'], replace: { Gi: 'Sm' } },
32
+ { ifPresent: ['Je'], ifNotPresent: ['Sn'], replace: { Je: 'Sn' } },
33
+ { ifPresent: ['La'], ifNotPresent: ['Fo'], replace: { La: 'Fo' } },
34
+ { ifPresent: ['Mu'], ifNotPresent: ['Se'], replace: { Mu: 'Se' } },
35
+ { ifPresent: ['Pa'], ifNotPresent: ['Ca'], replace: { Pa: 'Ca' } },
36
+ { ifPresent: ['Sh'], ifNotPresent: ['Fc'], replace: { Sh: 'Fc' } },
37
+ { ifPresent: ['Sn'], ifNotPresent: ['Tt'], replace: { Sn: 'Tt' } },
38
+ ],
39
+ D: [
40
+ { ifPresent: ['Ag'], ifNotPresent: ['Sl'], replace: { Ag: 'Sl' } },
41
+ { ifPresent: ['Ba'], ifNotPresent: ['Sn'], replace: { Ba: 'Sn' } },
42
+ { ifPresent: ['Cd'], ifNotPresent: ['Ex'], replace: { Cd: 'Ex' } },
43
+ { ifPresent: ['Fc'], ifNotPresent: ['Eu'], replace: { Fc: 'Eu' } },
44
+ { ifPresent: ['Je'], ifNotPresent: ['Gi'], replace: { Je: 'Gi' } },
45
+ { ifPresent: ['Mu'], ifNotPresent: ['Ca'], replace: { Mu: 'Ca' } },
46
+ { ifPresent: ['Sh'], ifNotPresent: ['En'], replace: { Sh: 'En' } },
47
+ ],
48
+ E: [
49
+ { ifPresent: ['Di'], ifNotPresent: ['El'], replace: { Di: 'El' } },
50
+ { ifPresent: ['Eu'], ifNotPresent: ['En'], replace: { Eu: 'En' } },
51
+ { ifPresent: ['Fc'], ifNotPresent: ['Sh'], replace: { Fc: 'Sh' } },
52
+ { ifPresent: ['Fo'], ifNotPresent: ['La'], replace: { Fo: 'La' } },
53
+ { ifPresent: ['Gl'], ifNotPresent: ['Di'], replace: { Gl: 'Di' } },
54
+ { ifPresent: ['Sc'], ifNotPresent: ['Ba'], replace: { Sc: 'Ba' } },
55
+ { ifPresent: ['Se'], ifNotPresent: ['Mu'], replace: { Se: 'Mu' } },
56
+ { ifPresent: ['Sp'], ifNotPresent: ['Eu'], replace: { Sp: 'Eu' } },
57
+ { ifPresent: ['Tt'], ifNotPresent: ['Sn'], replace: { Tt: 'Sn' } },
58
+ ],
59
+ F: [
60
+ { ifPresent: ['Ca'], ifNotPresent: ['Ag'], replace: { Ca: 'Ag' } },
61
+ { ifPresent: ['Cd'], ifNotPresent: ['Sn'], replace: { Cd: 'Sn' } },
62
+ { ifPresent: ['Ex'], ifNotPresent: ['Se'], replace: { Ex: 'Se' } },
63
+ { ifPresent: ['Fc'], ifNotPresent: ['Je'], replace: { Fc: 'Je' } },
64
+ ],
65
+ G: [
66
+ { ifPresent: ['En'], ifNotPresent: ['Mu'], replace: { En: 'Mu' } },
67
+ { ifPresent: ['Eu'], ifNotPresent: ['Se'], replace: { Eu: 'Se' } },
68
+ { ifPresent: ['Fo'], ifNotPresent: ['To'], replace: { Fo: 'To' } },
69
+ { ifPresent: ['Mu'], ifNotPresent: ['Sc'], replace: { Mu: 'Sc' } },
70
+ { ifPresent: ['Pa'], ifNotPresent: ['Ag'], replace: { Pa: 'Ag' } },
71
+ ],
72
+ H: [
73
+ { ifPresent: ['Ca'], ifNotPresent: ['Sn'], replace: { Ca: 'Sn' } },
74
+ { ifPresent: ['Cy'], ifNotPresent: ['En'], replace: { Cy: 'En' } },
75
+ { ifPresent: ['Di'], ifNotPresent: ['Fc'], replace: { Di: 'Fc' } },
76
+ { ifPresent: ['En'], ifNotPresent: ['Tp'], replace: { En: 'Tp' } },
77
+ { ifPresent: ['Fc'], ifNotPresent: ['Si'], replace: { Fc: 'Si' } },
78
+ { ifPresent: ['Lf'], ifNotPresent: ['Re'], replace: { Lf: 'Re' } },
79
+ { ifPresent: ['Pa'], ifNotPresent: ['Je'], replace: { Pa: 'Je' } },
80
+ { ifPresent: ['Sm'], ifNotPresent: ['Ag'], replace: { Sm: 'Ag' } },
81
+ { ifPresent: ['To'], ifNotPresent: ['Sm'], replace: { To: 'Sm' } },
82
+ ],
83
+ I: [
84
+ { ifPresent: ['Ag'], ifNotPresent: ['Tt'], replace: { Ag: 'Tt' } },
85
+ { ifPresent: ['At'], ifNotPresent: ['Eu'], replace: { At: 'Eu' } },
86
+ { ifPresent: ['La'], ifNotPresent: ['Lf'], replace: { La: 'Lf' } },
87
+ { ifPresent: ['Mu'], ifNotPresent: ['To'], replace: { Mu: 'To' } },
88
+ { ifPresent: ['Sh'], ifNotPresent: ['Re'], replace: { Sh: 'Re' } },
89
+ { ifPresent: ['Sn'], ifNotPresent: ['Be'], replace: { Sn: 'Be' } },
90
+ { ifPresent: ['Tp'], ifNotPresent: ['Fc'], replace: { Tp: 'Fc' } },
91
+ ],
92
+ J: [
93
+ { ifPresent: ['Ca'], ifNotPresent: ['Ba'], replace: { Ca: 'Ba' } },
94
+ { ifPresent: ['Cd'], ifNotPresent: ['Gi'], replace: { Cd: 'Gi' } },
95
+ { ifPresent: ['Eu'], ifNotPresent: ['Si'], replace: { Eu: 'Si' } },
96
+ { ifPresent: ['Fo'], ifNotPresent: ['Pa'], replace: { Fo: 'Pa' } },
97
+ { ifPresent: ['Re'], ifNotPresent: ['Tp'], replace: { Re: 'Tp' } },
98
+ { ifPresent: ['To'], ifNotPresent: ['Sn'], replace: { To: 'Sn' } },
99
+ ],
100
+ K: [
101
+ { ifPresent: ['Ca'], ifNotPresent: ['Sl'], replace: { Ca: 'Sl' } },
102
+ { ifPresent: ['El'], ifNotPresent: ['At'], replace: { El: 'At' } },
103
+ { ifPresent: ['En'], ifNotPresent: ['Pa'], replace: { En: 'Pa' } },
104
+ { ifPresent: ['Fc'], ifNotPresent: ['Gi'], replace: { Fc: 'Gi' } },
105
+ { ifPresent: ['Fo'], ifNotPresent: ['Ca'], replace: { Fo: 'Ca' } },
106
+ { ifPresent: ['Gl'], ifNotPresent: ['To'], replace: { Gl: 'To' } },
107
+ { ifPresent: ['Mu'], ifNotPresent: ['Ag'], replace: { Mu: 'Ag' } },
108
+ { ifPresent: ['Pa'], ifNotPresent: ['Ba'], replace: { Pa: 'Ba' } },
109
+ { ifPresent: ['Sp'], ifNotPresent: ['Be'], replace: { Sp: 'Be' } },
110
+ { ifPresent: ['To'], ifNotPresent: ['Tt'], replace: { To: 'Tt' } },
111
+ ],
112
+ L: [
113
+ { ifPresent: ['At'], ifNotPresent: ['Sn'], replace: { At: 'Sn' } },
114
+ { ifPresent: ['Di'], ifNotPresent: ['To'], replace: { Di: 'To' } },
115
+ { ifPresent: ['Eu'], ifNotPresent: ['Be'], replace: { Eu: 'Be' } },
116
+ { ifPresent: ['La'], ifNotPresent: ['Ca'], replace: { La: 'Ca' } },
117
+ { ifPresent: ['Sh'], ifNotPresent: ['Gi'], replace: { Sh: 'Gi' } },
118
+ ],
119
+ M: [
120
+ { ifPresent: ['Ag'], ifNotPresent: ['Ca'], replace: { Ag: 'Ca' } },
121
+ { ifPresent: ['Gi'], ifNotPresent: ['Re'], replace: { Gi: 'Re' } },
122
+ { ifPresent: ['Si'], ifNotPresent: ['En'], replace: { Si: 'En' } },
123
+ { ifPresent: ['Tp'], ifNotPresent: ['El'], replace: { Tp: 'El' } },
124
+ ],
125
+ N: [
126
+ { ifPresent: ['At'], ifNotPresent: ['La'], replace: { At: 'La' } },
127
+ { ifPresent: ['Ca'], ifNotPresent: ['Gl'], replace: { Ca: 'Gl' } },
128
+ { ifPresent: ['En'], ifNotPresent: ['Cy'], replace: { En: 'Cy' } },
129
+ { ifPresent: ['Fc'], ifNotPresent: ['Di'], replace: { Fc: 'Di' } },
130
+ { ifPresent: ['Je'], ifNotPresent: ['Pa'], replace: { Je: 'Pa' } },
131
+ { ifPresent: ['Si'], ifNotPresent: ['Fc'], replace: { Si: 'Fc' } },
132
+ { ifPresent: ['Sh'], ifNotPresent: ['El'], replace: { Sh: 'El' } },
133
+ { ifPresent: ['Sl'], ifNotPresent: ['To'], replace: { Sl: 'To' } },
134
+ { ifPresent: ['Sn'], ifNotPresent: ['Ca'], replace: { Sn: 'Ca' } },
135
+ { ifPresent: ['Tp'], ifNotPresent: ['En'], replace: { Tp: 'En' } },
136
+ ],
137
+ O: [
138
+ { ifPresent: ['Ex'], ifNotPresent: ['Eu'], replace: { Ex: 'Eu' } },
139
+ { ifPresent: ['Fo'], ifNotPresent: ['En'], replace: { Fo: 'En' } },
140
+ { ifPresent: ['Gl'], ifNotPresent: ['Re'], replace: { Gl: 'Re' } },
141
+ { ifPresent: ['Lf'], ifNotPresent: ['El'], replace: { Lf: 'El' } },
142
+ { ifPresent: ['Se'], ifNotPresent: ['Gi'], replace: { Se: 'Gi' } },
143
+ ],
144
+ P: [
145
+ { ifPresent: ['Cy'], ifNotPresent: ['Gl'], replace: { Cy: 'Gl' } },
146
+ { ifPresent: ['El'], ifNotPresent: ['Eu'], replace: { El: 'Eu' } },
147
+ { ifPresent: ['Eu'], ifNotPresent: ['Zo'], replace: { Eu: 'Zo' } },
148
+ { ifPresent: ['La'], ifNotPresent: ['Cd'], replace: { La: 'Cd' } },
149
+ { ifPresent: ['Mu'], ifNotPresent: ['Tt'], replace: { Mu: 'Tt' } },
150
+ { ifPresent: ['Sh'], ifNotPresent: ['Mu'], replace: { Sh: 'Mu' } },
151
+ ],
152
+ };
@@ -0,0 +1,2 @@
1
+ import { SubstanceCode, SubstanceData } from '../types';
2
+ export declare const substances: Record<SubstanceCode, SubstanceData>;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.substances = void 0;
4
+ exports.substances = {
5
+ A: {
6
+ name: 'Cuke',
7
+ rank: '1',
8
+ price: 2,
9
+ effect: ['En'],
10
+ },
11
+ B: {
12
+ name: 'Flu Medicine',
13
+ rank: '4',
14
+ price: 5,
15
+ effect: ['Se'],
16
+ },
17
+ C: {
18
+ name: 'Gasoline',
19
+ rank: '5',
20
+ price: 5,
21
+ effect: ['To'],
22
+ },
23
+ D: {
24
+ name: 'Donut',
25
+ rank: '1',
26
+ price: 3,
27
+ effect: ['Cd'],
28
+ },
29
+ E: {
30
+ name: 'Energy Drink',
31
+ rank: '6',
32
+ price: 6,
33
+ effect: ['At'],
34
+ },
35
+ F: {
36
+ name: 'Mouth Wash',
37
+ rank: '3',
38
+ price: 4,
39
+ effect: ['Ba'],
40
+ },
41
+ G: {
42
+ name: 'Motor Oil',
43
+ rank: '7',
44
+ price: 6,
45
+ effect: ['Sl'],
46
+ },
47
+ H: {
48
+ name: 'Banana',
49
+ rank: '1',
50
+ price: 2,
51
+ effect: ['Gi'],
52
+ },
53
+ I: {
54
+ name: 'Chili',
55
+ rank: '9',
56
+ price: 7,
57
+ effect: ['Sp'],
58
+ },
59
+ J: {
60
+ name: 'Iodine',
61
+ rank: '11',
62
+ price: 8,
63
+ effect: ['Je'],
64
+ },
65
+ K: {
66
+ name: 'Paracetamol',
67
+ rank: '1',
68
+ price: 3,
69
+ effect: ['Sn'],
70
+ },
71
+ L: {
72
+ name: 'Viagra',
73
+ rank: '2',
74
+ price: 4,
75
+ effect: ['Tt'],
76
+ },
77
+ M: {
78
+ name: 'Horse Semen',
79
+ rank: '13',
80
+ price: 9,
81
+ effect: ['Lf'],
82
+ },
83
+ N: {
84
+ name: 'Mega Bean',
85
+ rank: '8',
86
+ price: 7,
87
+ effect: ['Fo'],
88
+ },
89
+ O: {
90
+ name: 'Addy',
91
+ rank: '12',
92
+ price: 9,
93
+ effect: ['Tp'],
94
+ },
95
+ P: {
96
+ name: 'Battery',
97
+ rank: '10',
98
+ price: 8,
99
+ effect: ['Be'],
100
+ },
101
+ };
@@ -0,0 +1,7 @@
1
+ export { mixSubstances } from './core/mixer';
2
+ export { encodeMixState, decodeMixState } from './utils/encoding';
3
+ export { effects } from './data/effects';
4
+ export { products, productAbbreviations } from './data/products';
5
+ export { substances } from './data/substances';
6
+ export { effectRulesBySubstance } from './data/rules';
7
+ export type { EffectCode, SubstanceCode, ProductType, RankCode, EffectData, SubstanceData, ProductData, EffectRule, MixResult, MixState, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.effectRulesBySubstance = exports.substances = exports.productAbbreviations = exports.products = exports.effects = exports.decodeMixState = exports.encodeMixState = exports.mixSubstances = void 0;
4
+ // Main exports
5
+ var mixer_1 = require("./core/mixer");
6
+ Object.defineProperty(exports, "mixSubstances", { enumerable: true, get: function () { return mixer_1.mixSubstances; } });
7
+ var encoding_1 = require("./utils/encoding");
8
+ Object.defineProperty(exports, "encodeMixState", { enumerable: true, get: function () { return encoding_1.encodeMixState; } });
9
+ Object.defineProperty(exports, "decodeMixState", { enumerable: true, get: function () { return encoding_1.decodeMixState; } });
10
+ // Data exports
11
+ var effects_1 = require("./data/effects");
12
+ Object.defineProperty(exports, "effects", { enumerable: true, get: function () { return effects_1.effects; } });
13
+ var products_1 = require("./data/products");
14
+ Object.defineProperty(exports, "products", { enumerable: true, get: function () { return products_1.products; } });
15
+ Object.defineProperty(exports, "productAbbreviations", { enumerable: true, get: function () { return products_1.productAbbreviations; } });
16
+ var substances_1 = require("./data/substances");
17
+ Object.defineProperty(exports, "substances", { enumerable: true, get: function () { return substances_1.substances; } });
18
+ var rules_1 = require("./data/rules");
19
+ Object.defineProperty(exports, "effectRulesBySubstance", { enumerable: true, get: function () { return rules_1.effectRulesBySubstance; } });
@@ -0,0 +1,36 @@
1
+ export type EffectCode = 'Ag' | 'At' | 'Ba' | 'Be' | 'Ca' | 'Cd' | 'Cy' | 'Di' | 'El' | 'En' | 'Eu' | 'Ex' | 'Fc' | 'Fo' | 'Gi' | 'Gl' | 'Je' | 'La' | 'Lf' | 'Mu' | 'Pa' | 'Re' | 'Sc' | 'Se' | 'Sh' | 'Si' | 'Sl' | 'Sm' | 'Sn' | 'Sp' | 'To' | 'Tp' | 'Tt' | 'Zo';
2
+ export type SubstanceCode = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P';
3
+ export type ProductType = 'OG Kush' | 'Sour Diesel' | 'Green Crack' | 'Grandaddy Purple' | 'Meth' | 'Cocaine';
4
+ export type RankCode = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '13';
5
+ export interface EffectData {
6
+ name: string;
7
+ price: number;
8
+ color: string;
9
+ }
10
+ export interface SubstanceData {
11
+ name: string;
12
+ rank: RankCode;
13
+ price: number;
14
+ effect: EffectCode[];
15
+ }
16
+ export interface ProductData {
17
+ price: number;
18
+ effects: EffectCode[];
19
+ abbreviation: string;
20
+ }
21
+ export interface EffectRule {
22
+ ifPresent: EffectCode[];
23
+ ifNotPresent: EffectCode[];
24
+ replace: Partial<Record<EffectCode, EffectCode>>;
25
+ }
26
+ export interface MixResult {
27
+ effects: EffectCode[];
28
+ cost: number;
29
+ sellPrice: number;
30
+ profit: number;
31
+ profitMargin: number;
32
+ }
33
+ export interface MixState {
34
+ product: ProductType;
35
+ substances: SubstanceCode[];
36
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ import { MixState } from '../types';
2
+ /**
3
+ * Encode a mix state into a URL-safe string
4
+ */
5
+ export declare function encodeMixState(state: MixState): string;
6
+ /**
7
+ * Decode a mix state from a URL-safe string
8
+ */
9
+ export declare function decodeMixState(hash: string): MixState | null;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeMixState = encodeMixState;
4
+ exports.decodeMixState = decodeMixState;
5
+ const products_1 = require("../data/products");
6
+ const substances_1 = require("../data/substances");
7
+ /**
8
+ * Encode a mix state into a URL-safe string
9
+ */
10
+ function encodeMixState(state) {
11
+ // Validate inputs
12
+ if (!products_1.products[state.product]) {
13
+ throw new Error('Invalid product type');
14
+ }
15
+ for (const substance of state.substances) {
16
+ if (!substances_1.substances[substance]) {
17
+ throw new Error(`Invalid substance code: ${substance}`);
18
+ }
19
+ }
20
+ const encoded = `${state.product}:${state.substances.join('')}`;
21
+ return toBase64Url(encoded);
22
+ }
23
+ /**
24
+ * Decode a mix state from a URL-safe string
25
+ */
26
+ function decodeMixState(hash) {
27
+ try {
28
+ const decoded = fromBase64Url(hash);
29
+ const [product, substancesStr] = decoded.split(':');
30
+ if (!product || substancesStr === undefined || !products_1.products[product]) {
31
+ return null;
32
+ }
33
+ const substanceCodes = substancesStr.split('');
34
+ // Validate substances
35
+ for (const code of substanceCodes) {
36
+ if (!substances_1.substances[code]) {
37
+ return null;
38
+ }
39
+ }
40
+ return {
41
+ product: product,
42
+ substances: substanceCodes,
43
+ };
44
+ }
45
+ catch (_a) {
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Convert a string to a URL-safe base64 string
51
+ */
52
+ function toBase64Url(str) {
53
+ return Buffer.from(str, 'binary')
54
+ .toString('base64')
55
+ .replace(/\+/g, '-')
56
+ .replace(/\//g, '_')
57
+ .replace(/=/g, '');
58
+ }
59
+ /**
60
+ * Convert a URL-safe base64 string back to a regular string
61
+ */
62
+ function fromBase64Url(str) {
63
+ const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
64
+ const padding = 4 - (base64.length % 4);
65
+ const padded = padding < 4 ? base64 + '='.repeat(padding) : base64;
66
+ return Buffer.from(padded, 'base64').toString('binary');
67
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@schedule1-tools/mixer",
3
+ "version": "0.0.0",
4
+ "description": "A package for calculating substance mixes in Schedule 1",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "keywords": [
11
+ "schedule1"
12
+ ],
13
+ "author": "Schedule1 Tools",
14
+ "license": "MIT",
15
+ "devDependencies": {
16
+ "@types/jest": "^29.5.14",
17
+ "@types/node": "^22.14.1",
18
+ "prettier": "^3.5.3",
19
+ "jest": "^29.7.0",
20
+ "ts-jest": "^29.3.2",
21
+ "typescript": "^5.8.3"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/schedule1-tools/mixer.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/schedule1-tools/mixer/issues"
29
+ },
30
+ "homepage": "https://github.com/schedule1-tools/mixer#readme",
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "test": "jest",
34
+ "test:coverage": "jest --coverage",
35
+ "format": "prettier --write ."
36
+ }
37
+ }