@dicelette/core 1.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/core/dice.ts ADDED
@@ -0,0 +1,104 @@
1
+ /* eslint-disable no-useless-escape */
2
+ import { DiceRoller } from "@dice-roller/rpg-dice-roller";
3
+ import { evaluate } from "mathjs";
4
+
5
+ import { Compare, Modifier, Resultat, Sign } from ".";
6
+
7
+ export const COMMENT_REGEX = /\s+(#|\/{2}|\[|\/\*)(.*)/;
8
+ const SIGN_REGEX =/[><=!]+/;
9
+ const SIGN_REGEX_SPACE = /[><=!]+(\S+)/;
10
+
11
+ /**
12
+ * Parse the string provided and turn it as a readable dice for dice parser
13
+ * @param dice {string}
14
+ */
15
+ export function roll(dice: string): Resultat | undefined{
16
+ //parse dice string
17
+ if (!dice.includes("d")) return undefined;
18
+ const compareRegex = dice.match(SIGN_REGEX_SPACE);
19
+ let compare : Compare | undefined;
20
+ if (compareRegex) {
21
+ dice = dice.replace(SIGN_REGEX_SPACE, "");
22
+ const calc = compareRegex[1];
23
+ const sign = calc.match(/[+-\/\*\^]/)?.[0];
24
+ const compareSign = compareRegex[0].match(SIGN_REGEX)?.[0];
25
+ if (sign) {
26
+ const toCalc = calc.replace(SIGN_REGEX, "").replace(/\s/g, "");
27
+ const total = evaluate(toCalc);
28
+ dice = dice.replace(SIGN_REGEX_SPACE, `${compareSign}${total}`);
29
+ compare = {
30
+ sign: compareSign as "<" | ">" | ">=" | "<=" | "=" | "!=" | "==",
31
+ value: total,
32
+ };
33
+ } else compare = {
34
+ sign: compareSign as "<" | ">" | ">=" | "<=" | "=" | "!=" | "==",
35
+ value: parseInt(calc, 10),
36
+ };
37
+ }
38
+ const modifier = dice.matchAll(/(\+|\-|%|\/|\^|\*|\*{2})(\d+)/gi);
39
+ let modificator : Modifier | undefined;
40
+ for (const mod of modifier) {
41
+ //calculate the modifier if multiple
42
+ if (modificator) {
43
+ const sign = modificator.sign;
44
+ let value = modificator.value;
45
+ if (sign)
46
+ value = calculator(sign, value, parseInt(mod[2], 10));
47
+ modificator = {
48
+ sign: mod[1] as Sign,
49
+ value,
50
+ };
51
+ } else {
52
+ modificator = {
53
+ sign: mod[1] as Sign,
54
+ value: parseInt(mod[2], 10),
55
+ };
56
+ }
57
+ }
58
+
59
+ if (dice.match(/\d+?#(.*)/)) {
60
+ const diceArray = dice.split("#");
61
+ const numberOfDice = parseInt(diceArray[0], 10);
62
+ const diceToRoll = diceArray[1].replace(COMMENT_REGEX, "");
63
+ const commentsMatch = diceArray[1].match(COMMENT_REGEX);
64
+ const comments = commentsMatch ? commentsMatch[2] : undefined;
65
+ const roller = new DiceRoller();
66
+ //remove comments if any
67
+ for (let i = 0; i < numberOfDice; i++) {
68
+ roller.roll(diceToRoll);
69
+ }
70
+ return {
71
+ dice: diceToRoll,
72
+ result: roller.output,
73
+ comment: comments,
74
+ compare: compare ? compare : undefined,
75
+ modifier: modificator,
76
+ };
77
+ }
78
+ const roller = new DiceRoller();
79
+ const diceWithoutComment = dice.replace(COMMENT_REGEX, "");
80
+ roller.roll(diceWithoutComment);
81
+ const commentMatch = dice.match(COMMENT_REGEX);
82
+ const comment = commentMatch ? commentMatch[2] : undefined;
83
+ return {
84
+ dice,
85
+ result: roller.output,
86
+ comment,
87
+ compare: compare ? compare : undefined,
88
+ modifier: modificator,
89
+ };
90
+ }
91
+ /**
92
+ * Evaluate a formula and replace "^" by "**" if any
93
+ * @param {Sign} sign
94
+ * @param {number} value
95
+ * @param {number} total
96
+ * @returns
97
+ */
98
+ export function calculator(sign: Sign, value: number, total: number): number {
99
+ if (sign === "^") sign = "**";
100
+ return evaluate(`${total} ${sign} ${value}`);
101
+ }
102
+
103
+
104
+
@@ -0,0 +1,71 @@
1
+ export interface Resultat {
2
+ dice: string;
3
+ result: string;
4
+ comment?: string;
5
+ compare?: Compare | undefined;
6
+ modifier?: Modifier;
7
+ }
8
+
9
+
10
+ export interface Compare {
11
+ sign: "<" | ">" | ">=" | "<=" | "=" | "!=" | "==";
12
+ value: number;
13
+ }
14
+
15
+ export type Sign = "+" | "-" | "*" | "/" | "%" | "^" | "**";
16
+
17
+ export interface Modifier {
18
+ sign?: Sign;
19
+ value: number;
20
+ }
21
+
22
+ export type Statistic = {
23
+ [name: string] : {
24
+ max?: number;
25
+ min?: number;
26
+ combinaison?: string;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @example
32
+ * diceType: 1d20
33
+ * comparator: {
34
+ * sign: ">="
35
+ * value: 20
36
+ * formula: +$
37
+ * }
38
+ * The dice throw will be 1d20 + statistique that must be >= 20
39
+ * @example
40
+ * diceType: 1d20
41
+ * comparator: {
42
+ * sign: "<="
43
+ * }
44
+ * The dice throw will be 1d20 that must be <= statistique
45
+ */
46
+ export interface StatisticalTemplate {
47
+ /** Allow to force the user to choose a name for them characters */
48
+ charName?: boolean
49
+ statistics?: Statistic
50
+ /**
51
+ * A total can be set, it allows to calculate the total value of a future register member
52
+ * If the sum of the value > total, the bot will send a message to the user to inform him that the total is exceeded and an error will be thrown
53
+ * @note statistique that have a formula will be ignored from the total
54
+ */
55
+ total?: number
56
+ /** A dice type in the notation supported by the bot */
57
+ diceType?: string;
58
+ /**
59
+ * How the success/echec will be done
60
+ */
61
+ critical?: Critical;
62
+ /** Special dice for damage */
63
+ damage?: {
64
+ [name: string]: string;
65
+ }
66
+ }
67
+
68
+ export interface Critical {
69
+ success?: number,
70
+ failure?: number,
71
+ }
package/core/utils.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { evaluate } from "mathjs";
2
+ import removeAccents from "remove-accents";
3
+
4
+ /**
5
+ * Escape regex string
6
+ * @param string {string}
7
+ */
8
+ export function escapeRegex(string: string) {
9
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
+ }
11
+
12
+
13
+ /**
14
+ * Replace the stat name by their value using stat and after evaluate any formula using `replaceFormulaInDice`
15
+ * @param originalDice {dice}
16
+ * @param stats {[name: string]: number}
17
+ */
18
+ export function generateStatsDice(originalDice: string, stats?: {[name: string]: number}) {
19
+ let dice = originalDice;
20
+ if (stats && Object.keys(stats).length > 0) {
21
+ //damage field support adding statistic, like : 1d6 + strength
22
+ //check if the value contains a statistic & calculate if it's okay
23
+ //the dice will be converted before roll
24
+ const allStats = Object.keys(stats);
25
+ for (const stat of allStats) {
26
+ const regex = new RegExp(escapeRegex(removeAccents(stat)), "gi");
27
+ if (dice.match(regex)) {
28
+ const statValue = stats[stat];
29
+ dice = dice.replace(regex, statValue.toString());
30
+ }
31
+ }
32
+ }
33
+ return replaceFormulaInDice(dice);
34
+
35
+ }
36
+
37
+ /**
38
+ * Replace the {{}} in the dice string and evaluate the interior if any
39
+ * @param dice {string}
40
+ */
41
+ export function replaceFormulaInDice(dice: string) {
42
+ const formula = /(?<formula>\{{2}(.+?)\}{2})/gmi;
43
+ const formulaMatch = formula.exec(dice);
44
+ if (formulaMatch?.groups?.formula) {
45
+ const formula = formulaMatch.groups.formula.replaceAll("{{", "").replaceAll("}}", "");
46
+ try {
47
+ const result = evaluate(formula);
48
+ return cleanedDice(dice.replace(formulaMatch.groups.formula, result.toString()));
49
+ } catch (error) {
50
+ throw new Error(`[error.invalidFormula, common.space]: ${formulaMatch.groups.formula}`);
51
+ }
52
+ }
53
+ return cleanedDice(dice);
54
+ }
55
+
56
+ /**
57
+ * Replace the ++ +- -- by their proper value:
58
+ * - `++` = `+`
59
+ * - `+-` = `-`
60
+ * - `--` = `+`
61
+ * @param dice {string}
62
+ */
63
+ export function cleanedDice(dice: string) {
64
+ return dice.replaceAll("+-", "-").replaceAll("--", "+").replaceAll("++", "+");
65
+ }
@@ -0,0 +1,263 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { evaluate } from "mathjs";
3
+ import {Random } from "random-js";
4
+ import removeAccents from "remove-accents";
5
+
6
+ import { Statistic, StatisticalTemplate } from ".";
7
+ import { roll } from "./dice";
8
+ import { escapeRegex, replaceFormulaInDice } from "./utils";
9
+
10
+ /**
11
+ * Verify if the provided dice work with random value
12
+ * @param testDice {string}
13
+ * @param stats {[name: string]: number}
14
+ */
15
+ export function evalStatsDice(testDice: string, stats?: {[name: string]: number}) {
16
+ let dice = testDice;
17
+ if (stats && Object.keys(stats).length > 0) {
18
+ const allStats = Object.keys(stats);
19
+ for (const stat of allStats) {
20
+ const regex = new RegExp(escapeRegex(removeAccents(stat)), "gi");
21
+ if (testDice.match(regex)) {
22
+ const statValue = stats[stat];
23
+ dice = testDice.replace(regex, statValue.toString());
24
+ }
25
+ }
26
+ }
27
+ try {
28
+ if (!roll(replaceFormulaInDice(dice))) throw new Error(`[error.invalidDice.withoutDice, common.space] ${dice}`);
29
+ return testDice;
30
+ } catch (error) {
31
+ throw new Error(`[error.invalidDice.withoutDice, common.space]: ${testDice}\n${(error as Error).message}`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Generate a random dice and remove the formula (+ evaluate it)
37
+ * Used for diceDamage only
38
+ * @param value {string}
39
+ * @param template {StatisticalTemplate}
40
+ * @returns
41
+ */
42
+ export function diceRandomParse(value: string, template: StatisticalTemplate) {
43
+ if (!template.statistics) return value;
44
+ value = removeAccents(value);
45
+ const allStats = Object.keys(template.statistics).map(stat => removeAccents(stat).toLowerCase());
46
+ let newDice = value;
47
+ for (const stat of allStats) {
48
+ const regex = new RegExp(escapeRegex(stat), "gi");
49
+ if (value.match(regex)) {
50
+ let max: undefined | number = undefined;
51
+ let min: undefined | number = undefined;
52
+ const stats = template.statistics?.[stat];
53
+ if (stats) {
54
+ max = template.statistics[removeAccents(stat).toLowerCase()].max;
55
+ min = template.statistics[removeAccents(stat).toLowerCase()].min;
56
+ }
57
+ const total = template.total || 100;
58
+ const randomStatValue = generateRandomStat(total, max, min);
59
+ newDice = value.replace(regex, randomStatValue.toString());
60
+ }
61
+ }
62
+ return replaceFormulaInDice(newDice);
63
+ }
64
+
65
+ /**
66
+ * Same as damageDice but for DiceType
67
+ * @param dice {string}
68
+ * @param template {StatisticalTemplate}
69
+ */
70
+ export function diceTypeRandomParse(dice: string, template: StatisticalTemplate) {
71
+ if (!template.statistics) return dice;
72
+ const firstStatNotCombinaison = Object.keys(template.statistics).find(stat => !template.statistics?.[stat].combinaison);
73
+ if (!firstStatNotCombinaison) return dice;
74
+ const stats = template.statistics[firstStatNotCombinaison];
75
+ const {min, max} = stats;
76
+ const total = template.total || 100;
77
+ const randomStatValue = generateRandomStat(total, max, min);
78
+ return replaceFormulaInDice(dice.replace("$", randomStatValue.toString()));
79
+ }
80
+
81
+ /**
82
+ * Random the combinaison and evaluate it to check if everything is valid
83
+ * @param combinaison {[name: string]: string}
84
+ * @param stats {[name: string]: string|number}
85
+ */
86
+ export function evalCombinaison(combinaison: {[name: string]: string}, stats: {[name: string]: string | number}) {
87
+ const newStats: {[name: string]: number} = {};
88
+ for (const [stat, combin] of Object.entries(combinaison)) {
89
+ //replace the stats in formula
90
+ let formula = removeAccents(combin);
91
+ for (const [statName, value] of Object.entries(stats)) {
92
+ const regex = new RegExp(removeAccents(statName), "gi");
93
+ formula = formula.replace(regex, value.toString());
94
+ }
95
+ try {
96
+ const result = evaluate(formula);
97
+ newStats[stat] = result;
98
+ } catch (error) {
99
+ throw new Error(`[error.invalidFormula, common.space]: ${stat}`);
100
+ }
101
+ }
102
+ return newStats;
103
+ }
104
+
105
+ /**
106
+ * Evaluate one selected combinaison
107
+ * @param combinaison {string}
108
+ * @param stats {[name: string]: string|number}
109
+ */
110
+ export function evalOneCombinaison(combinaison: string, stats: {[name: string]: string | number}) {
111
+ let formula = removeAccents(combinaison);
112
+ for (const [statName, value] of Object.entries(stats)) {
113
+ const regex = new RegExp(removeAccents(statName), "gi");
114
+ formula = formula.replace(regex, value.toString());
115
+ }
116
+ try {
117
+ return evaluate(formula);
118
+ } catch (error) {
119
+ throw new Error(`[error.invalidFormula, common.space]: ${combinaison}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Parse the provided JSON and verify each field to check if everything could work when rolling
125
+ * @param {any} template
126
+ * @returns {StatisticalTemplate}
127
+ */
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ export function verifyTemplateValue(template: any): StatisticalTemplate {
130
+ const statistiqueTemplate: StatisticalTemplate = {
131
+ diceType: "",
132
+ statistics: {} as Statistic
133
+ };
134
+ if (!template.statistics) statistiqueTemplate.statistics = undefined;
135
+ else if (template.statistics && Object.keys(template.statistics).length > 0) {
136
+ for (const [key, value] of Object.entries(template.statistics)) {
137
+ const dataValue = value as { max?: number, min?: number, combinaison?: string };
138
+ if (dataValue.max && dataValue.min && dataValue.max <= dataValue.min)
139
+ throw new Error("[error.maxGreater]");
140
+ if (dataValue.max && dataValue.max <= 0 ) dataValue.max = undefined;
141
+ if (dataValue.min && dataValue.min <= 0 ) dataValue.min = undefined;
142
+ let formula = dataValue.combinaison ? removeAccents(dataValue.combinaison).toLowerCase() : undefined;
143
+ formula = formula && formula.trim().length > 0 ? formula : undefined;
144
+ if (!statistiqueTemplate.statistics) {
145
+ statistiqueTemplate.statistics = {} as Statistic;
146
+ }
147
+ statistiqueTemplate.statistics[key] = {
148
+ max: dataValue.max,
149
+ min: dataValue.min,
150
+ combinaison: formula || undefined,
151
+ };
152
+ }
153
+ }
154
+ if (template.diceType) {
155
+ try {
156
+ statistiqueTemplate.diceType = template.diceType;
157
+ diceTypeRandomParse(template.diceType, statistiqueTemplate);
158
+ } catch (e) {
159
+ throw new Error((e as Error).message);
160
+ }
161
+ }
162
+
163
+
164
+ if (template.critical && Object.keys(template.critical).length > 0){
165
+ statistiqueTemplate.critical = {
166
+ failure: template.critical.failure ?? undefined,
167
+ success: template.critical.success ?? undefined
168
+ };
169
+
170
+ }
171
+ if (template.total) {
172
+ if (template.total <= 0)
173
+ template.total = undefined;
174
+ statistiqueTemplate.total = template.total;
175
+ }
176
+ if (template.charName) statistiqueTemplate.charName = template.charName;
177
+ if (template.damage) statistiqueTemplate.damage = template.damage;
178
+ try {
179
+ testDamageRoll(statistiqueTemplate);
180
+ testCombinaison(statistiqueTemplate);
181
+ } catch (error) {
182
+ throw new Error((error as Error).message);
183
+ }
184
+ return statistiqueTemplate;
185
+ }
186
+
187
+ /**
188
+ * Test each damage roll from the template.damage
189
+ * @param {StatisticalTemplate} template
190
+ */
191
+ export function testDamageRoll(template: StatisticalTemplate) {
192
+ if (!template.damage) return;
193
+ if (Object.keys(template.damage).length === 0) throw new Error("[error.emptyObject]");
194
+ if (Object.keys(template.damage).length > 25) throw new Error("[error.tooManyDice]");
195
+ for (const [name, dice] of Object.entries(template.damage)) {
196
+ if (!dice) continue;
197
+ const randomDiceParsed = diceRandomParse(dice, template);
198
+ try {
199
+ roll(randomDiceParsed);
200
+ } catch (error) {
201
+ throw new Error(`[error.invalidDice, common.space] ${name}`);
202
+ }
203
+ }
204
+ }
205
+
206
+
207
+
208
+ /**
209
+ * Test all combinaison with generated random value
210
+ * @param {StatisticalTemplate} template
211
+ */
212
+ export function testCombinaison(template: StatisticalTemplate) {
213
+ if (!template.statistics) return;
214
+ const onlyCombinaisonStats = Object.fromEntries(Object.entries(template.statistics).filter(([_, value]) => value.combinaison !== undefined));
215
+ const allOtherStats = Object.fromEntries(Object.entries(template.statistics).filter(([_, value]) => !value.combinaison));
216
+ if (Object.keys(onlyCombinaisonStats).length===0) return;
217
+ const allStats = Object.keys(template.statistics).filter(stat => !template.statistics![stat].combinaison);
218
+ if (allStats.length === 0)
219
+ throw new Error("[error.noStat]");
220
+ const error= [];
221
+ for (const [stat, value] of Object.entries(onlyCombinaisonStats)) {
222
+ let formula = value.combinaison as string;
223
+ for (const [other, data] of Object.entries(allOtherStats)) {
224
+ const {max, min} = data;
225
+ const total = template.total || 100;
226
+ const randomStatValue = generateRandomStat(total, max, min);
227
+ const regex = new RegExp(other, "gi");
228
+ formula = formula.replace(regex, randomStatValue.toString());
229
+ }
230
+ try {
231
+ evaluate(formula);
232
+ } catch (e) {
233
+ error.push(stat);
234
+ }
235
+ }
236
+ if (error.length > 0)
237
+ throw new Error(`[error.invalidFormula, common.space] ${error.join(", ")}`);
238
+ return;
239
+ }
240
+
241
+ /**
242
+ * Generate a random stat based on the template and the statistical min and max
243
+ * @param {number|undefined} total
244
+ * @param {number | undefined} max
245
+ * @param {number | undefined} min
246
+ * @returns
247
+ */
248
+ export function generateRandomStat(total: number | undefined = 100, max?: number, min?: number) {
249
+ let randomStatValue = total + 1;
250
+ while (randomStatValue >= total) {
251
+ const random = new Random();
252
+ if (max && min)
253
+ randomStatValue = random.integer(min, max);
254
+ else if (max)
255
+ randomStatValue = random.integer(0, max);
256
+ else if (min)
257
+ randomStatValue = random.integer(min, total);
258
+ else
259
+ randomStatValue = random.integer(0, total);
260
+ }
261
+ return randomStatValue;
262
+ }
263
+
package/jest.config.js ADDED
@@ -0,0 +1,6 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: "ts-jest",
4
+ testEnvironment: "node",
5
+ verbose: true,
6
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@dicelette/core",
3
+ "version": "1.0.0",
4
+ "description": "Core library for the Dicelette Discord bot",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Dicelette/core.git"
8
+ },
9
+ "types": "index.d.ts",
10
+ "keywords": [
11
+ "discord",
12
+ "roll",
13
+ "library",
14
+ "bot",
15
+ "typescript"
16
+ ],
17
+ "author": "Mara-Li",
18
+ "license": "GPL-3.0-only",
19
+ "dependencies": {
20
+ "@dice-roller/rpg-dice-roller": "^5.5.0",
21
+ "@lisandra-dev/eslint-config": "^1.1.4",
22
+ "@types/jest": "^29.5.12",
23
+ "eslint": "^8.57.0",
24
+ "mathjs": "^12.4.1",
25
+ "moment": "^2.30.1",
26
+ "random-js": "^2.1.0",
27
+ "remove-accents": "^0.5.0",
28
+ "ts-dedent": "^2.2.0"
29
+ },
30
+ "scripts": {
31
+ "test": "jest"
32
+ }
33
+ }