@jjlmoya/utils-games-development 1.63.0 → 1.64.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/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/damageFormulaLab/bibliography.astro +24 -0
- package/src/tool/damageFormulaLab/bibliography.ts +10 -0
- package/src/tool/damageFormulaLab/component.astro +178 -0
- package/src/tool/damageFormulaLab/controller.ts +235 -0
- package/src/tool/damageFormulaLab/damage-formula-lab.css +644 -0
- package/src/tool/damageFormulaLab/dom-views.ts +193 -0
- package/src/tool/damageFormulaLab/entry.ts +28 -0
- package/src/tool/damageFormulaLab/evaluator.ts +84 -0
- package/src/tool/damageFormulaLab/expression.ts +191 -0
- package/src/tool/damageFormulaLab/i18n/de.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/en.ts +231 -0
- package/src/tool/damageFormulaLab/i18n/es.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/fr.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/id.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/it.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/ja.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/ko.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/nl.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/pl.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/pt.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/ru.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/sv.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/tr.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/zh.ts +181 -0
- package/src/tool/damageFormulaLab/index.ts +11 -0
- package/src/tool/damageFormulaLab/logic.test.ts +138 -0
- package/src/tool/damageFormulaLab/logic.ts +138 -0
- package/src/tool/damageFormulaLab/model.ts +151 -0
- package/src/tool/damageFormulaLab/seo.astro +13 -0
- package/src/tool/damageFormulaLab/storage.ts +62 -0
- package/src/tool/damageFormulaLab/transfers.ts +89 -0
- package/src/tool/damageFormulaLab/ui.ts +62 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -6,10 +6,11 @@ import { saveFileEditor } from '../tool/saveFileEditor/entry';
|
|
|
6
6
|
import { steamBbcodeTranslator } from '../tool/steamBbcodeTranslator/entry';
|
|
7
7
|
import { gameUiAccessibilityTester } from '../tool/gameUIAccessibilityTester/entry';
|
|
8
8
|
import { hitboxHurtboxAnimator } from '../tool/hitboxHurtboxAnimator/entry';
|
|
9
|
+
import { damageFormulaLab } from '../tool/damageFormulaLab/entry';
|
|
9
10
|
|
|
10
11
|
export const gamesCategory: GamesCategoryEntry = {
|
|
11
12
|
icon: 'mdi:gamepad-variant',
|
|
12
|
-
tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator, gameUiAccessibilityTester, hitboxHurtboxAnimator],
|
|
13
|
+
tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab],
|
|
13
14
|
i18n: {
|
|
14
15
|
de: () => import('./i18n/de').then((m) => m.content),
|
|
15
16
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
|
18
18
|
export type { PixelArtPaletteSwapperUI, PixelArtPaletteSwapperLocaleContent } from './tool/pixelArtPaletteSwapper/entry';
|
|
19
19
|
export { hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator/entry';
|
|
20
20
|
export type { HitboxHurtboxAnimatorUI, HitboxHurtboxAnimatorLocaleContent } from './tool/hitboxHurtboxAnimator/entry';
|
|
21
|
+
export { damageFormulaLab } from './tool/damageFormulaLab/entry';
|
|
22
|
+
export type { DamageFormulaLabUI, DamageFormulaLabLocaleContent } from './tool/damageFormulaLab/entry';
|
|
21
23
|
export { gamesCategory } from './category';
|
|
22
24
|
import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
23
25
|
import { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
@@ -30,5 +32,6 @@ import { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
|
|
|
30
32
|
import { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
31
33
|
import { gameUiAccessibilityTester } from './tool/gameUIAccessibilityTester/entry';
|
|
32
34
|
import { hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator/entry';
|
|
35
|
+
import { damageFormulaLab } from './tool/damageFormulaLab/entry';
|
|
33
36
|
|
|
34
|
-
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, steamBbcodeTranslator, retroSfxGenerator, pixelArtPaletteSwapper, gameUiAccessibilityTester, hitboxHurtboxAnimator];
|
|
37
|
+
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, steamBbcodeTranslator, retroSfxGenerator, pixelArtPaletteSwapper, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab];
|
package/src/index.ts
CHANGED
|
@@ -22,5 +22,6 @@ export { RETRO_SFX_GENERATOR_TOOL, retroSfxGenerator } from './tool/retroSfxGene
|
|
|
22
22
|
export { PIXEL_ART_PALETTE_SWAPPER_TOOL, pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper';
|
|
23
23
|
export { GAME_UI_ACCESSIBILITY_TESTER_TOOL, gameUiAccessibilityTester } from './tool/gameUIAccessibilityTester';
|
|
24
24
|
export { HITBOX_HURTBOX_ANIMATOR_TOOL, hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator';
|
|
25
|
+
export { DAMAGE_FORMULA_LAB_TOOL, damageFormulaLab } from './tool/damageFormulaLab';
|
|
25
26
|
|
|
26
27
|
export type { ToolLocaleContent as GamesToolLocaleContent } from './types';
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
3
|
|
|
4
4
|
describe('Locale Completeness Validation', () => {
|
|
5
|
-
it('
|
|
6
|
-
expect(ALL_TOOLS.length).toBe(
|
|
5
|
+
it('twelve tools are registered', () => {
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(12);
|
|
7
7
|
});
|
|
8
8
|
});
|
|
@@ -4,8 +4,8 @@ import { gamesCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 12 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(12);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('gamesCategory should be defined', () => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { BibliographyEntry } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
title?: string;
|
|
6
|
+
entries?: BibliographyEntry[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { title = 'References', entries = [] } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
{entries.length > 0 && (
|
|
13
|
+
<section class="damage-lab-references" aria-labelledby="damage-lab-reference-title">
|
|
14
|
+
<h2 id="damage-lab-reference-title">{title}</h2>
|
|
15
|
+
<ul>
|
|
16
|
+
{entries.map((entry) => <li><a href={entry.url} rel="noopener noreferrer" target="_blank">{entry.name}</a></li>)}
|
|
17
|
+
</ul>
|
|
18
|
+
</section>
|
|
19
|
+
)}
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
.damage-lab-references { margin-block: 2rem; }
|
|
23
|
+
.damage-lab-references a { color: var(--n-primary); }
|
|
24
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const bibliographyEntries = [
|
|
2
|
+
{
|
|
3
|
+
name: "Game Balance Concepts: Numeric Relationships",
|
|
4
|
+
url: "https://gamebalanceconcepts.wordpress.com/2010/07/14/level-2-numeric-relationships/",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: "Oracle Numerical Computation Guide: What Is IEEE Arithmetic?",
|
|
8
|
+
url: "https://docs.oracle.com/cd/E37069_01/html/E39019/z4000ac019127.html",
|
|
9
|
+
},
|
|
10
|
+
];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DamageFormulaLabUI } from './ui';
|
|
3
|
+
import './damage-formula-lab.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: DamageFormulaLabUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
|
|
11
|
+
const coreInputs = [
|
|
12
|
+
['attack', ui.attackLabel, 0, 100000, 1],
|
|
13
|
+
['defense', ui.defenseLabel, 0, 100000, 1],
|
|
14
|
+
['health', ui.healthLabel, 1, 10000000, 1],
|
|
15
|
+
['attacksPerSecond', ui.cadenceLabel, 0.01, 100, 0.05],
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
const advancedInputs = [
|
|
19
|
+
['level', ui.levelLabel, 1, 999, 1],
|
|
20
|
+
['power', ui.powerLabel, 0, 100, 0.05],
|
|
21
|
+
['resistance', ui.resistanceLabel, 0, 100, 1],
|
|
22
|
+
['flat', ui.flatLabel, -100000, 100000, 1],
|
|
23
|
+
['criticalChance', ui.criticalChanceLabel, 0, 100, 1],
|
|
24
|
+
['criticalMultiplier', ui.criticalMultiplierLabel, 1, 20, 0.05],
|
|
25
|
+
] as const;
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<section class="damage-lab" data-damage-lab>
|
|
29
|
+
<script is:inline type="application/json" data-damage-ui set:html={JSON.stringify(ui)} />
|
|
30
|
+
|
|
31
|
+
<header class="damage-lab-header">
|
|
32
|
+
<p>{ui.onboarding}</p>
|
|
33
|
+
<span>{ui.localNote}</span>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<section class="damage-lab-composer" aria-labelledby="damage-lab-formulas-title">
|
|
37
|
+
<div class="damage-lab-heading">
|
|
38
|
+
<h2 id="damage-lab-formulas-title">{ui.formulaDeck}</h2>
|
|
39
|
+
<div class="damage-lab-presets" aria-label="Formula presets">
|
|
40
|
+
<button type="button" data-preset="linear">{ui.presetLinear}</button>
|
|
41
|
+
<button type="button" data-preset="ratio">{ui.presetRatio}</button>
|
|
42
|
+
<button type="button" data-preset="level">{ui.presetLevel}</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="damage-lab-formulas">
|
|
46
|
+
<label class="damage-lab-formula damage-lab-formula-a">
|
|
47
|
+
<span><i aria-hidden="true"></i>{ui.formulaALabel}</span>
|
|
48
|
+
<input name="formulaA" type="text" spellcheck="false" autocomplete="off" />
|
|
49
|
+
</label>
|
|
50
|
+
<label class="damage-lab-formula damage-lab-formula-b">
|
|
51
|
+
<span><i aria-hidden="true"></i>{ui.formulaBLabel}</span>
|
|
52
|
+
<input name="formulaB" type="text" spellcheck="false" autocomplete="off" />
|
|
53
|
+
</label>
|
|
54
|
+
</div>
|
|
55
|
+
<details class="damage-lab-language">
|
|
56
|
+
<summary>Formula language</summary>
|
|
57
|
+
<p>{ui.formulaHint}<br />{ui.formulaFunctions}</p>
|
|
58
|
+
</details>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<section class="damage-lab-tuning" aria-labelledby="damage-lab-tuning-title">
|
|
62
|
+
<div class="damage-lab-heading">
|
|
63
|
+
<h2 id="damage-lab-tuning-title">{ui.combatInputs}</h2>
|
|
64
|
+
<div class="damage-lab-scenarios" aria-label={ui.scenariosTitle}>
|
|
65
|
+
<button type="button" data-scenario="skirmisher">{ui.scenarioSkirmisher}</button>
|
|
66
|
+
<button type="button" data-scenario="guardian">{ui.scenarioGuardian}</button>
|
|
67
|
+
<button type="button" data-scenario="boss">{ui.scenarioBoss}</button>
|
|
68
|
+
<button type="button" data-scenario="custom">{ui.scenarioCustom}</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="damage-lab-core-inputs">
|
|
72
|
+
{coreInputs.map(([name, label, min, max, step]) => (
|
|
73
|
+
<label>
|
|
74
|
+
<span>{label}</span>
|
|
75
|
+
<input name={name} type="number" min={min} max={max} step={step} inputmode="decimal" />
|
|
76
|
+
</label>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
<details class="damage-lab-advanced">
|
|
80
|
+
<summary>Advanced combat and engine rules</summary>
|
|
81
|
+
<div class="damage-lab-advanced-grid">
|
|
82
|
+
{advancedInputs.map(([name, label, min, max, step]) => (
|
|
83
|
+
<label>
|
|
84
|
+
<span>{label}</span>
|
|
85
|
+
<input name={name} type="number" min={min} max={max} step={step} inputmode="decimal" />
|
|
86
|
+
</label>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
<fieldset class="damage-lab-choice">
|
|
90
|
+
<legend>{ui.roundingLabel}</legend>
|
|
91
|
+
<button type="button" data-rounding="none">{ui.roundingNone}</button>
|
|
92
|
+
<button type="button" data-rounding="floor">{ui.roundingFloor}</button>
|
|
93
|
+
<button type="button" data-rounding="round">{ui.roundingRound}</button>
|
|
94
|
+
<button type="button" data-rounding="ceil">{ui.roundingCeil}</button>
|
|
95
|
+
</fieldset>
|
|
96
|
+
<fieldset class="damage-lab-choice">
|
|
97
|
+
<legend>{ui.orderLabel}</legend>
|
|
98
|
+
<button type="button" data-order="resistance-first">{ui.resistanceFirst}</button>
|
|
99
|
+
<button type="button" data-order="flat-first">{ui.flatFirst}</button>
|
|
100
|
+
</fieldset>
|
|
101
|
+
</details>
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<section class="damage-lab-result" aria-labelledby="damage-lab-result-title">
|
|
105
|
+
<div class="damage-lab-result-heading">
|
|
106
|
+
<div><span>Live model</span><h2 id="damage-lab-result-title">{ui.runLabel}</h2></div>
|
|
107
|
+
<div class="damage-lab-gap"><small>{ui.resultDifference}</small><strong data-result="gap">0</strong></div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="damage-lab-totals">
|
|
110
|
+
<article>
|
|
111
|
+
<span>{ui.formulaAName}</span>
|
|
112
|
+
<strong data-result="a-damage">0</strong>
|
|
113
|
+
<small>{ui.resultDamage}</small>
|
|
114
|
+
<dl><div><dt>{ui.resultHits}</dt><dd data-result="a-hits">0</dd></div><div><dt>{ui.resultTtk}</dt><dd data-result="a-ttk">0 s</dd></div></dl>
|
|
115
|
+
</article>
|
|
116
|
+
<article>
|
|
117
|
+
<span>{ui.formulaBName}</span>
|
|
118
|
+
<strong data-result="b-damage">0</strong>
|
|
119
|
+
<small>{ui.resultDamage}</small>
|
|
120
|
+
<dl><div><dt>{ui.resultHits}</dt><dd data-result="b-hits">0</dd></div><div><dt>{ui.resultTtk}</dt><dd data-result="b-ttk">0 s</dd></div></dl>
|
|
121
|
+
</article>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="damage-lab-curve-heading">
|
|
124
|
+
<div><h3>{ui.curveTitle}</h3><p>{ui.curveCaption}</p></div>
|
|
125
|
+
<span><b data-curve-max>0</b> max</span>
|
|
126
|
+
</div>
|
|
127
|
+
<svg class="damage-lab-chart" data-chart viewBox="0 0 100 100" preserveAspectRatio="none" role="img" aria-label={ui.curveTitle}>
|
|
128
|
+
<path class="damage-lab-guide" d="M0 25 H100 M0 50 H100 M0 75 H100" />
|
|
129
|
+
<path class="damage-lab-curve damage-lab-curve-a" data-curve="a" d="" />
|
|
130
|
+
<path class="damage-lab-curve damage-lab-curve-b" data-curve="b" d="" />
|
|
131
|
+
</svg>
|
|
132
|
+
<div class="damage-lab-legend"><span>{ui.formulaAName}</span><span>{ui.formulaBName}</span></div>
|
|
133
|
+
</section>
|
|
134
|
+
|
|
135
|
+
<details class="damage-lab-secondary">
|
|
136
|
+
<summary>{ui.heatmapTitle} + {ui.diagnosticsTitle}</summary>
|
|
137
|
+
<div class="damage-lab-notes">
|
|
138
|
+
<section class="damage-lab-pressure" aria-labelledby="damage-lab-heatmap-title">
|
|
139
|
+
<h2 id="damage-lab-heatmap-title">{ui.heatmapTitle}</h2>
|
|
140
|
+
<p>{ui.heatmapCaption}</p>
|
|
141
|
+
<div class="damage-lab-axis">{ui.attackAxis}</div>
|
|
142
|
+
<div class="damage-lab-heatmap" data-heatmap></div>
|
|
143
|
+
<small>{ui.defenseAxis}</small>
|
|
144
|
+
</section>
|
|
145
|
+
<section class="damage-lab-diagnostics" aria-labelledby="damage-lab-diagnostics-title">
|
|
146
|
+
<h2 id="damage-lab-diagnostics-title">{ui.diagnosticsTitle}</h2>
|
|
147
|
+
<ul data-diagnostics></ul>
|
|
148
|
+
<p>{ui.limitationDisclosure}</p>
|
|
149
|
+
</section>
|
|
150
|
+
</div>
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details class="damage-lab-export-panel">
|
|
154
|
+
<summary>{ui.exportTitle}</summary>
|
|
155
|
+
<footer class="damage-lab-exports">
|
|
156
|
+
<p>{ui.privacyDisclosure}</p>
|
|
157
|
+
<div class="damage-lab-actions">
|
|
158
|
+
<button type="button" data-share>{ui.copyLink}</button>
|
|
159
|
+
<button type="button" data-csv>{ui.exportCsv}</button>
|
|
160
|
+
<button type="button" data-json>{ui.exportJson}</button>
|
|
161
|
+
<label>{ui.importJson}<input type="file" accept="application/json,.json" data-import /></label>
|
|
162
|
+
<button type="button" data-png>{ui.exportPng}</button>
|
|
163
|
+
<button type="button" data-reset>{ui.reset}</button>
|
|
164
|
+
</div>
|
|
165
|
+
<p class="damage-lab-status" data-status aria-live="polite"></p>
|
|
166
|
+
</footer>
|
|
167
|
+
</details>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<script>
|
|
171
|
+
import { mountDamageFormulaLab } from './controller';
|
|
172
|
+
import type { DamageFormulaLabUI } from './ui';
|
|
173
|
+
|
|
174
|
+
document.querySelectorAll<HTMLElement>('[data-damage-lab]').forEach((root) => {
|
|
175
|
+
const source = root.querySelector<HTMLScriptElement>('[data-damage-ui]');
|
|
176
|
+
if (source?.textContent) mountDamageFormulaLab(root, JSON.parse(source.textContent) as DamageFormulaLabUI);
|
|
177
|
+
});
|
|
178
|
+
</script>
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { renderLab } from "./dom-views";
|
|
2
|
+
import {
|
|
3
|
+
configFromHash,
|
|
4
|
+
defaultConfig,
|
|
5
|
+
formulaPresets,
|
|
6
|
+
parseConfig,
|
|
7
|
+
scenarios,
|
|
8
|
+
} from "./model";
|
|
9
|
+
import type { LabConfig } from "./model";
|
|
10
|
+
import { loadPreferences, savePreferences } from "./storage";
|
|
11
|
+
import {
|
|
12
|
+
copyShareLink,
|
|
13
|
+
exportChartPng,
|
|
14
|
+
exportCsv,
|
|
15
|
+
exportJson,
|
|
16
|
+
} from "./transfers";
|
|
17
|
+
import type { DamageFormulaLabUI } from "./ui";
|
|
18
|
+
|
|
19
|
+
type Update = (config: LabConfig) => void;
|
|
20
|
+
type GetConfig = () => LabConfig;
|
|
21
|
+
|
|
22
|
+
function input(root: HTMLElement, name: string): HTMLInputElement | null {
|
|
23
|
+
return root.querySelector<HTMLInputElement>(`[name="${name}"]`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function numberValue(
|
|
27
|
+
root: HTMLElement,
|
|
28
|
+
name: string,
|
|
29
|
+
fallback: number,
|
|
30
|
+
): number {
|
|
31
|
+
const value = Number(input(root, name)?.value);
|
|
32
|
+
return Number.isFinite(value) ? value : fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setActive(root: HTMLElement, selector: string, value: string): void {
|
|
36
|
+
root.querySelectorAll<HTMLElement>(selector).forEach((item) => {
|
|
37
|
+
const candidate = item.dataset.rounding ?? item.dataset.order;
|
|
38
|
+
item.dataset.active = String(candidate === value);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeInputs(root: HTMLElement, config: LabConfig): void {
|
|
43
|
+
const values: Record<string, number | string> = {
|
|
44
|
+
formulaA: config.formulaA,
|
|
45
|
+
formulaB: config.formulaB,
|
|
46
|
+
...config.variables,
|
|
47
|
+
health: config.settings.health,
|
|
48
|
+
attacksPerSecond: config.settings.attacksPerSecond,
|
|
49
|
+
};
|
|
50
|
+
Object.entries(values).forEach(([name, value]) => {
|
|
51
|
+
const field = input(root, name);
|
|
52
|
+
if (field) field.value = String(value);
|
|
53
|
+
});
|
|
54
|
+
setActive(root, "[data-rounding]", config.settings.rounding);
|
|
55
|
+
setActive(root, "[data-order]", config.settings.modifierOrder);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readVariables(
|
|
59
|
+
root: HTMLElement,
|
|
60
|
+
current: LabConfig,
|
|
61
|
+
): Record<string, number> {
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.keys(current.variables).map((name) => [
|
|
64
|
+
name,
|
|
65
|
+
numberValue(
|
|
66
|
+
root,
|
|
67
|
+
name,
|
|
68
|
+
current.variables[name as keyof typeof current.variables],
|
|
69
|
+
),
|
|
70
|
+
]),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readConfig(root: HTMLElement, current: LabConfig): LabConfig {
|
|
75
|
+
return parseConfig({
|
|
76
|
+
...current,
|
|
77
|
+
formulaA: input(root, "formulaA")?.value,
|
|
78
|
+
formulaB: input(root, "formulaB")?.value,
|
|
79
|
+
variables: readVariables(root, current),
|
|
80
|
+
settings: {
|
|
81
|
+
...current.settings,
|
|
82
|
+
health: numberValue(root, "health", current.settings.health),
|
|
83
|
+
attacksPerSecond: numberValue(
|
|
84
|
+
root,
|
|
85
|
+
"attacksPerSecond",
|
|
86
|
+
current.settings.attacksPerSecond,
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function save(config: LabConfig): void {
|
|
93
|
+
savePreferences({
|
|
94
|
+
formulaA: config.formulaA,
|
|
95
|
+
formulaB: config.formulaB,
|
|
96
|
+
rounding: config.settings.rounding,
|
|
97
|
+
modifierOrder: config.settings.modifierOrder,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function initialConfig(): LabConfig {
|
|
102
|
+
const preferences = loadPreferences();
|
|
103
|
+
return (
|
|
104
|
+
configFromHash(location.hash) ?? {
|
|
105
|
+
...structuredClone(defaultConfig),
|
|
106
|
+
formulaA: preferences.formulaA,
|
|
107
|
+
formulaB: preferences.formulaB,
|
|
108
|
+
settings: {
|
|
109
|
+
...defaultConfig.settings,
|
|
110
|
+
rounding: preferences.rounding,
|
|
111
|
+
modifierOrder: preferences.modifierOrder,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function applyScenario(config: LabConfig, name: string): LabConfig {
|
|
118
|
+
const scenario = scenarios[name];
|
|
119
|
+
if (!scenario) return config;
|
|
120
|
+
const { health, ...variables } = scenario;
|
|
121
|
+
return {
|
|
122
|
+
...config,
|
|
123
|
+
variables: { ...config.variables, ...variables },
|
|
124
|
+
settings: { ...config.settings, health },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function bindModes(root: HTMLElement, get: GetConfig, update: Update): void {
|
|
129
|
+
root
|
|
130
|
+
.querySelectorAll<HTMLButtonElement>("[data-rounding]")
|
|
131
|
+
.forEach((button) =>
|
|
132
|
+
button.addEventListener("click", () => {
|
|
133
|
+
const config = get();
|
|
134
|
+
update({
|
|
135
|
+
...config,
|
|
136
|
+
settings: {
|
|
137
|
+
...config.settings,
|
|
138
|
+
rounding: button.dataset
|
|
139
|
+
.rounding as LabConfig["settings"]["rounding"],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
root.querySelectorAll<HTMLButtonElement>("[data-order]").forEach((button) =>
|
|
145
|
+
button.addEventListener("click", () => {
|
|
146
|
+
const config = get();
|
|
147
|
+
update({
|
|
148
|
+
...config,
|
|
149
|
+
settings: {
|
|
150
|
+
...config.settings,
|
|
151
|
+
modifierOrder: button.dataset
|
|
152
|
+
.order as LabConfig["settings"]["modifierOrder"],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function bindPresets(root: HTMLElement, get: GetConfig, update: Update): void {
|
|
160
|
+
root.querySelectorAll<HTMLButtonElement>("[data-preset]").forEach((button) =>
|
|
161
|
+
button.addEventListener("click", () => {
|
|
162
|
+
const formula =
|
|
163
|
+
formulaPresets[button.dataset.preset as keyof typeof formulaPresets];
|
|
164
|
+
update({ ...get(), formulaB: formula });
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
root
|
|
168
|
+
.querySelectorAll<HTMLButtonElement>("[data-scenario]")
|
|
169
|
+
.forEach((button) =>
|
|
170
|
+
button.addEventListener("click", () => {
|
|
171
|
+
update(applyScenario(get(), button.dataset.scenario ?? ""));
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function bindImport(
|
|
177
|
+
root: HTMLElement,
|
|
178
|
+
update: Update,
|
|
179
|
+
ui: DamageFormulaLabUI,
|
|
180
|
+
): void {
|
|
181
|
+
const picker = root.querySelector<HTMLInputElement>("[data-import]");
|
|
182
|
+
picker?.addEventListener("change", async () => {
|
|
183
|
+
try {
|
|
184
|
+
const file = picker.files?.[0];
|
|
185
|
+
if (file) update(parseConfig(JSON.parse(await file.text())));
|
|
186
|
+
} catch {
|
|
187
|
+
const status = root.querySelector<HTMLElement>("[data-status]");
|
|
188
|
+
if (status) status.textContent = ui.importError;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function bindExports(
|
|
194
|
+
root: HTMLElement,
|
|
195
|
+
get: GetConfig,
|
|
196
|
+
update: Update,
|
|
197
|
+
ui: DamageFormulaLabUI,
|
|
198
|
+
): void {
|
|
199
|
+
root
|
|
200
|
+
.querySelector("[data-reset]")
|
|
201
|
+
?.addEventListener("click", () => update(structuredClone(defaultConfig)));
|
|
202
|
+
root
|
|
203
|
+
.querySelector("[data-json]")
|
|
204
|
+
?.addEventListener("click", () => exportJson(get()));
|
|
205
|
+
root
|
|
206
|
+
.querySelector("[data-csv]")
|
|
207
|
+
?.addEventListener("click", () => exportCsv(get()));
|
|
208
|
+
root
|
|
209
|
+
.querySelector("[data-png]")
|
|
210
|
+
?.addEventListener("click", () => exportChartPng(root));
|
|
211
|
+
root.querySelector("[data-share]")?.addEventListener("click", async () => {
|
|
212
|
+
await copyShareLink(get());
|
|
213
|
+
const status = root.querySelector<HTMLElement>("[data-status]");
|
|
214
|
+
if (status) status.textContent = ui.copiedStatus;
|
|
215
|
+
});
|
|
216
|
+
bindImport(root, update, ui);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function mountDamageFormulaLab(
|
|
220
|
+
root: HTMLElement,
|
|
221
|
+
ui: DamageFormulaLabUI,
|
|
222
|
+
): void {
|
|
223
|
+
let config = initialConfig();
|
|
224
|
+
const update: Update = (next) => {
|
|
225
|
+
config = next;
|
|
226
|
+
writeInputs(root, config);
|
|
227
|
+
save(config);
|
|
228
|
+
renderLab(root, config, ui);
|
|
229
|
+
};
|
|
230
|
+
root.addEventListener("input", () => update(readConfig(root, config)));
|
|
231
|
+
bindModes(root, () => config, update);
|
|
232
|
+
bindPresets(root, () => config, update);
|
|
233
|
+
bindExports(root, () => config, update, ui);
|
|
234
|
+
update(config);
|
|
235
|
+
}
|