@jjlmoya/utils-games-development 1.63.0 → 1.65.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.
Files changed (181) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/seo_parity.test.ts +2 -2
  7. package/src/tests/seo_translation_completeness.test.ts +69 -0
  8. package/src/tests/tool_validation.test.ts +2 -2
  9. package/src/tests/translation_copy.test.ts +21 -18
  10. package/src/tool/audioLoopPointFinder/i18n/de.ts +13 -0
  11. package/src/tool/audioLoopPointFinder/i18n/fr.ts +26 -0
  12. package/src/tool/audioLoopPointFinder/i18n/id.ts +4 -0
  13. package/src/tool/audioLoopPointFinder/i18n/it.ts +3 -0
  14. package/src/tool/audioLoopPointFinder/i18n/nl.ts +4 -0
  15. package/src/tool/audioLoopPointFinder/i18n/pl.ts +4 -0
  16. package/src/tool/audioLoopPointFinder/i18n/pt.ts +3 -0
  17. package/src/tool/audioLoopPointFinder/i18n/ru.ts +4 -0
  18. package/src/tool/audioLoopPointFinder/i18n/sv.ts +4 -0
  19. package/src/tool/audioLoopPointFinder/i18n/tr.ts +4 -0
  20. package/src/tool/audioLoopPointFinder/i18n/zh.ts +4 -0
  21. package/src/tool/damageFormulaLab/bibliography.astro +24 -0
  22. package/src/tool/damageFormulaLab/bibliography.ts +10 -0
  23. package/src/tool/damageFormulaLab/component.astro +178 -0
  24. package/src/tool/damageFormulaLab/controller.ts +235 -0
  25. package/src/tool/damageFormulaLab/damage-formula-lab.css +644 -0
  26. package/src/tool/damageFormulaLab/dom-views.ts +193 -0
  27. package/src/tool/damageFormulaLab/entry.ts +28 -0
  28. package/src/tool/damageFormulaLab/evaluator.ts +84 -0
  29. package/src/tool/damageFormulaLab/expression.ts +191 -0
  30. package/src/tool/damageFormulaLab/i18n/de.ts +194 -0
  31. package/src/tool/damageFormulaLab/i18n/en.ts +231 -0
  32. package/src/tool/damageFormulaLab/i18n/es.ts +181 -0
  33. package/src/tool/damageFormulaLab/i18n/fr.ts +194 -0
  34. package/src/tool/damageFormulaLab/i18n/id.ts +194 -0
  35. package/src/tool/damageFormulaLab/i18n/it.ts +182 -0
  36. package/src/tool/damageFormulaLab/i18n/ja.ts +183 -0
  37. package/src/tool/damageFormulaLab/i18n/ko.ts +182 -0
  38. package/src/tool/damageFormulaLab/i18n/nl.ts +182 -0
  39. package/src/tool/damageFormulaLab/i18n/pl.ts +182 -0
  40. package/src/tool/damageFormulaLab/i18n/pt.ts +182 -0
  41. package/src/tool/damageFormulaLab/i18n/ru.ts +183 -0
  42. package/src/tool/damageFormulaLab/i18n/sv.ts +183 -0
  43. package/src/tool/damageFormulaLab/i18n/tr.ts +183 -0
  44. package/src/tool/damageFormulaLab/i18n/zh.ts +183 -0
  45. package/src/tool/damageFormulaLab/index.ts +11 -0
  46. package/src/tool/damageFormulaLab/logic.test.ts +138 -0
  47. package/src/tool/damageFormulaLab/logic.ts +138 -0
  48. package/src/tool/damageFormulaLab/model.ts +151 -0
  49. package/src/tool/damageFormulaLab/seo.astro +13 -0
  50. package/src/tool/damageFormulaLab/storage.ts +62 -0
  51. package/src/tool/damageFormulaLab/transfers.ts +89 -0
  52. package/src/tool/damageFormulaLab/ui.ts +62 -0
  53. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +22 -0
  54. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.ts +10 -0
  55. package/src/tool/gameDeltaTimeFixedTimestepLab/component.astro +51 -0
  56. package/src/tool/gameDeltaTimeFixedTimestepLab/controller.ts +79 -0
  57. package/src/tool/gameDeltaTimeFixedTimestepLab/dom-views.ts +88 -0
  58. package/src/tool/gameDeltaTimeFixedTimestepLab/entry.ts +27 -0
  59. package/src/tool/gameDeltaTimeFixedTimestepLab/evaluator.ts +21 -0
  60. package/src/tool/gameDeltaTimeFixedTimestepLab/game-delta-time-fixed-timestep-lab.css +491 -0
  61. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/de.ts +56 -0
  62. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/en.ts +180 -0
  63. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/es.ts +56 -0
  64. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/fr.ts +46 -0
  65. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/id.ts +46 -0
  66. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/it.ts +46 -0
  67. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ja.ts +46 -0
  68. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ko.ts +46 -0
  69. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/nl.ts +46 -0
  70. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pl.ts +46 -0
  71. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pt.ts +46 -0
  72. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ru.ts +46 -0
  73. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/sv.ts +46 -0
  74. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/tr.ts +46 -0
  75. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/zh.ts +46 -0
  76. package/src/tool/gameDeltaTimeFixedTimestepLab/index.ts +11 -0
  77. package/src/tool/gameDeltaTimeFixedTimestepLab/logic.test.ts +35 -0
  78. package/src/tool/gameDeltaTimeFixedTimestepLab/logic.ts +140 -0
  79. package/src/tool/gameDeltaTimeFixedTimestepLab/seo.astro +15 -0
  80. package/src/tool/gameDeltaTimeFixedTimestepLab/seo_contract.test.ts +25 -0
  81. package/src/tool/gameDeltaTimeFixedTimestepLab/storage.ts +23 -0
  82. package/src/tool/gameDeltaTimeFixedTimestepLab/ui.ts +54 -0
  83. package/src/tool/gameUIAccessibilityTester/i18n/de.ts +13 -0
  84. package/src/tool/gameUIAccessibilityTester/i18n/fr.ts +13 -0
  85. package/src/tool/gameUIAccessibilityTester/i18n/id.ts +13 -0
  86. package/src/tool/gameUIAccessibilityTester/i18n/it.ts +1 -0
  87. package/src/tool/gameUIAccessibilityTester/i18n/nl.ts +5 -0
  88. package/src/tool/gameUIAccessibilityTester/i18n/pl.ts +5 -0
  89. package/src/tool/gameUIAccessibilityTester/i18n/pt.ts +2 -0
  90. package/src/tool/gameUIAccessibilityTester/i18n/ru.ts +1 -0
  91. package/src/tool/gameUIAccessibilityTester/i18n/sv.ts +1 -0
  92. package/src/tool/gameUIAccessibilityTester/i18n/tr.ts +2 -0
  93. package/src/tool/gameUIAccessibilityTester/i18n/zh.ts +1 -0
  94. package/src/tool/hitboxHurtboxAnimator/i18n/de.ts +14 -0
  95. package/src/tool/hitboxHurtboxAnimator/i18n/fr.ts +13 -0
  96. package/src/tool/hitboxHurtboxAnimator/i18n/id.ts +13 -0
  97. package/src/tool/hitboxHurtboxAnimator/i18n/it.ts +1 -0
  98. package/src/tool/hitboxHurtboxAnimator/i18n/nl.ts +1 -0
  99. package/src/tool/hitboxHurtboxAnimator/i18n/pl.ts +2 -0
  100. package/src/tool/hitboxHurtboxAnimator/i18n/pt.ts +2 -0
  101. package/src/tool/hitboxHurtboxAnimator/i18n/ru.ts +2 -0
  102. package/src/tool/hitboxHurtboxAnimator/i18n/sv.ts +1 -0
  103. package/src/tool/hitboxHurtboxAnimator/i18n/tr.ts +2 -0
  104. package/src/tool/itchioGameTester/i18n/de.ts +13 -0
  105. package/src/tool/itchioGameTester/i18n/es.ts +2 -2
  106. package/src/tool/itchioGameTester/i18n/fr.ts +26 -0
  107. package/src/tool/itchioGameTester/i18n/id.ts +3 -0
  108. package/src/tool/itchioGameTester/i18n/it.ts +3 -0
  109. package/src/tool/itchioGameTester/i18n/ja.ts +3 -0
  110. package/src/tool/itchioGameTester/i18n/ko.ts +3 -0
  111. package/src/tool/itchioGameTester/i18n/nl.ts +3 -0
  112. package/src/tool/itchioGameTester/i18n/pl.ts +3 -0
  113. package/src/tool/itchioGameTester/i18n/pt.ts +3 -0
  114. package/src/tool/itchioGameTester/i18n/ru.ts +3 -0
  115. package/src/tool/itchioGameTester/i18n/sv.ts +3 -0
  116. package/src/tool/itchioGameTester/i18n/tr.ts +3 -0
  117. package/src/tool/itchioGameTester/i18n/zh.ts +3 -0
  118. package/src/tool/localizationSanitizer/i18n/de.ts +13 -0
  119. package/src/tool/localizationSanitizer/i18n/es.ts +26 -0
  120. package/src/tool/localizationSanitizer/i18n/fr.ts +4 -0
  121. package/src/tool/localizationSanitizer/i18n/id.ts +4 -0
  122. package/src/tool/localizationSanitizer/i18n/it.ts +4 -0
  123. package/src/tool/localizationSanitizer/i18n/ja.ts +4 -0
  124. package/src/tool/localizationSanitizer/i18n/ko.ts +4 -0
  125. package/src/tool/localizationSanitizer/i18n/nl.ts +5 -0
  126. package/src/tool/localizationSanitizer/i18n/pl.ts +5 -0
  127. package/src/tool/localizationSanitizer/i18n/pt.ts +5 -0
  128. package/src/tool/localizationSanitizer/i18n/ru.ts +5 -0
  129. package/src/tool/localizationSanitizer/i18n/sv.ts +5 -0
  130. package/src/tool/localizationSanitizer/i18n/tr.ts +5 -0
  131. package/src/tool/localizationSanitizer/i18n/zh.ts +6 -0
  132. package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +13 -0
  133. package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +13 -0
  134. package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +18 -0
  135. package/src/tool/retroSfxGenerator/i18n/de.ts +13 -0
  136. package/src/tool/retroSfxGenerator/i18n/fr.ts +13 -0
  137. package/src/tool/retroSfxGenerator/i18n/id.ts +13 -0
  138. package/src/tool/retroSfxGenerator/i18n/it.ts +1 -0
  139. package/src/tool/retroSfxGenerator/i18n/nl.ts +1 -0
  140. package/src/tool/retroSfxGenerator/i18n/pl.ts +1 -0
  141. package/src/tool/retroSfxGenerator/i18n/pt.ts +1 -0
  142. package/src/tool/retroSfxGenerator/i18n/ru.ts +1 -0
  143. package/src/tool/retroSfxGenerator/i18n/sv.ts +2 -0
  144. package/src/tool/retroSfxGenerator/i18n/tr.ts +1 -0
  145. package/src/tool/retroSfxGenerator/i18n/zh.ts +1 -0
  146. package/src/tool/spriteSheetPacker/i18n/de.ts +13 -0
  147. package/src/tool/spriteSheetPacker/i18n/es.ts +2 -2
  148. package/src/tool/spriteSheetPacker/i18n/fr.ts +26 -0
  149. package/src/tool/spriteSheetPacker/i18n/id.ts +4 -0
  150. package/src/tool/spriteSheetPacker/i18n/it.ts +4 -0
  151. package/src/tool/spriteSheetPacker/i18n/ja.ts +3 -0
  152. package/src/tool/spriteSheetPacker/i18n/ko.ts +3 -0
  153. package/src/tool/spriteSheetPacker/i18n/nl.ts +4 -0
  154. package/src/tool/spriteSheetPacker/i18n/pl.ts +4 -0
  155. package/src/tool/spriteSheetPacker/i18n/pt.ts +4 -0
  156. package/src/tool/spriteSheetPacker/i18n/ru.ts +4 -0
  157. package/src/tool/spriteSheetPacker/i18n/sv.ts +4 -0
  158. package/src/tool/spriteSheetPacker/i18n/tr.ts +4 -0
  159. package/src/tool/spriteSheetPacker/i18n/zh.ts +4 -0
  160. package/src/tool/steamBbcodeTranslator/i18n/de.ts +22 -0
  161. package/src/tool/steamBbcodeTranslator/i18n/es.ts +23 -1
  162. package/src/tool/steamBbcodeTranslator/i18n/fr.ts +6 -1
  163. package/src/tool/steamBbcodeTranslator/i18n/id.ts +7 -1
  164. package/src/tool/steamBbcodeTranslator/i18n/it.ts +6 -1
  165. package/src/tool/steamBbcodeTranslator/i18n/ja.ts +5 -1
  166. package/src/tool/steamBbcodeTranslator/i18n/ko.ts +5 -1
  167. package/src/tool/steamBbcodeTranslator/i18n/nl.ts +7 -1
  168. package/src/tool/steamBbcodeTranslator/i18n/pl.ts +7 -1
  169. package/src/tool/steamBbcodeTranslator/i18n/pt.ts +7 -1
  170. package/src/tool/steamBbcodeTranslator/i18n/ru.ts +7 -1
  171. package/src/tool/steamBbcodeTranslator/i18n/sv.ts +7 -1
  172. package/src/tool/steamBbcodeTranslator/i18n/tr.ts +7 -1
  173. package/src/tool/steamBbcodeTranslator/i18n/zh.ts +5 -1
  174. package/src/tool/steamCapsuleGenerator/i18n/de.ts +2 -2
  175. package/src/tool/steamCapsuleGenerator/i18n/es.ts +2 -2
  176. package/src/tool/steamCapsuleGenerator/i18n/fr.ts +2 -2
  177. package/src/tool/steamCapsuleGenerator/i18n/pl.ts +13 -0
  178. package/src/tool/steamCapsuleGenerator/i18n/ru.ts +26 -0
  179. package/src/tool/steamCapsuleGenerator/i18n/sv.ts +1 -0
  180. package/src/tool/steamCapsuleGenerator/i18n/tr.ts +1 -0
  181. package/src/tools.ts +4 -0
@@ -0,0 +1,193 @@
1
+ import { buildCurve, buildHeatmap } from "./logic";
2
+ import type { FormulaResult, SamplePoint } from "./logic";
3
+ import { inspectFormula, inspectSweep } from "./evaluator";
4
+ import type { LabConfig } from "./model";
5
+ import type { DamageFormulaLabUI } from "./ui";
6
+
7
+ function query(root: HTMLElement, selector: string): HTMLElement | null {
8
+ return root.querySelector<HTMLElement>(selector);
9
+ }
10
+
11
+ function format(value: number, digits = 1): string {
12
+ return Number.isFinite(value)
13
+ ? value.toLocaleString("en", { maximumFractionDigits: digits })
14
+ : "∞";
15
+ }
16
+
17
+ function setText(root: HTMLElement, selector: string, value: string): void {
18
+ const target = query(root, selector);
19
+ if (target) target.textContent = value;
20
+ }
21
+
22
+ function resultText(
23
+ result: FormulaResult | undefined,
24
+ key: keyof FormulaResult,
25
+ suffix = "",
26
+ ): string {
27
+ return result ? `${format(result[key])}${suffix}` : "Error";
28
+ }
29
+
30
+ function setResult(
31
+ root: HTMLElement,
32
+ prefix: string,
33
+ result: FormulaResult | undefined,
34
+ ): void {
35
+ setText(
36
+ root,
37
+ `[data-result="${prefix}-damage"]`,
38
+ resultText(result, "roundedDamage"),
39
+ );
40
+ setText(
41
+ root,
42
+ `[data-result="${prefix}-hits"]`,
43
+ resultText(result, "hitsToKill"),
44
+ );
45
+ setText(
46
+ root,
47
+ `[data-result="${prefix}-ttk"]`,
48
+ resultText(result, "timeToKill", " s"),
49
+ );
50
+ }
51
+
52
+ function pointPath(points: SamplePoint[], maximum: number): string {
53
+ return points
54
+ .map((point, index) => {
55
+ const x = points.length === 1 ? 0 : (index * 100) / (points.length - 1);
56
+ const y = 100 - Math.min(point.roundedDamage / maximum, 1) * 84 - 8;
57
+ return `${index === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`;
58
+ })
59
+ .join(" ");
60
+ }
61
+
62
+ function renderCurve(
63
+ root: HTMLElement,
64
+ a: SamplePoint[],
65
+ b: SamplePoint[],
66
+ ): void {
67
+ const maximum = Math.max(
68
+ 1,
69
+ ...a.map((point) => point.roundedDamage),
70
+ ...b.map((point) => point.roundedDamage),
71
+ );
72
+ const pathA = root.querySelector<SVGPathElement>('[data-curve="a"]');
73
+ const pathB = root.querySelector<SVGPathElement>('[data-curve="b"]');
74
+ if (pathA) pathA.setAttribute("d", pointPath(a, maximum));
75
+ if (pathB) pathB.setAttribute("d", pointPath(b, maximum));
76
+ setText(root, "[data-curve-max]", format(maximum));
77
+ }
78
+
79
+ function renderHeatmap(root: HTMLElement, rows: SamplePoint[][]): void {
80
+ const target = query(root, "[data-heatmap]");
81
+ if (!target) return;
82
+ const maximum = Math.max(
83
+ 1,
84
+ ...rows.flat().map((point) => point.roundedDamage),
85
+ );
86
+ target.replaceChildren(
87
+ ...rows.flat().map((point) => {
88
+ const cell = document.createElement("span");
89
+ cell.className = "damage-lab-heat-cell";
90
+ cell.style.setProperty(
91
+ "--damage-intensity",
92
+ String(Math.max(0.06, point.roundedDamage / maximum)),
93
+ );
94
+ cell.textContent = format(point.roundedDamage, 0);
95
+ cell.title = `Attack ${format(point.attack)} · Defense ${format(point.defense)}`;
96
+ return cell;
97
+ }),
98
+ );
99
+ }
100
+
101
+ function renderDiagnostics(
102
+ root: HTMLElement,
103
+ config: LabConfig,
104
+ ui: DamageFormulaLabUI,
105
+ ): void {
106
+ const target = query(root, "[data-diagnostics]");
107
+ if (!target) return;
108
+ const a = inspectFormula(config.formulaA, config.variables, config.settings);
109
+ const b = inspectFormula(config.formulaB, config.variables, config.settings);
110
+ const messages = [
111
+ ...a.diagnostics.map((item) => `A: ${item}`),
112
+ ...b.diagnostics.map((item) => `B: ${item}`),
113
+ ...inspectSweep(
114
+ config.formulaA,
115
+ config.variables,
116
+ config.settings,
117
+ config.attackRange,
118
+ ),
119
+ ];
120
+ const output = messages.length > 0 ? messages : [ui.statusBalanced];
121
+ target.replaceChildren(
122
+ ...output.map((message) => {
123
+ const item = document.createElement("li");
124
+ item.textContent = message;
125
+ return item;
126
+ }),
127
+ );
128
+ target.dataset.state = diagnosticState(a.kind, b.kind, messages.length);
129
+ }
130
+
131
+ function diagnosticState(a: string, b: string, count: number): string {
132
+ if (a === "error" || b === "error") return "error";
133
+ return count > 0 ? "warning" : "ok";
134
+ }
135
+
136
+ function renderGap(
137
+ root: HTMLElement,
138
+ a: FormulaResult | undefined,
139
+ b: FormulaResult | undefined,
140
+ ): void {
141
+ const gap = a && b ? b.roundedDamage - a.roundedDamage : Number.NaN;
142
+ const sign = gap > 0 ? "+" : "";
143
+ setText(
144
+ root,
145
+ '[data-result="gap"]',
146
+ Number.isFinite(gap) ? `${sign}${format(gap)}` : "Error",
147
+ );
148
+ }
149
+
150
+ export function renderLab(
151
+ root: HTMLElement,
152
+ config: LabConfig,
153
+ ui: DamageFormulaLabUI,
154
+ ): void {
155
+ const a = inspectFormula(config.formulaA, config.variables, config.settings);
156
+ const b = inspectFormula(config.formulaB, config.variables, config.settings);
157
+ setResult(root, "a", a.result);
158
+ setResult(root, "b", b.result);
159
+ renderGap(root, a.result, b.result);
160
+ renderDiagnostics(root, config, ui);
161
+ renderVisuals(root, config);
162
+ }
163
+
164
+ function renderVisuals(root: HTMLElement, config: LabConfig): void {
165
+ try {
166
+ const curveA = buildCurve(
167
+ config.formulaA,
168
+ config.variables,
169
+ config.settings,
170
+ config.attackRange,
171
+ );
172
+ const curveB = buildCurve(
173
+ config.formulaB,
174
+ config.variables,
175
+ config.settings,
176
+ config.attackRange,
177
+ );
178
+ renderCurve(root, curveA, curveB);
179
+ renderHeatmap(
180
+ root,
181
+ buildHeatmap({
182
+ expression: config.formulaA,
183
+ variables: config.variables,
184
+ settings: config.settings,
185
+ attackRange: { ...config.attackRange, steps: 9 },
186
+ defenseRange: config.defenseRange,
187
+ }),
188
+ );
189
+ } catch {
190
+ renderCurve(root, [], []);
191
+ renderHeatmap(root, []);
192
+ }
193
+ }
@@ -0,0 +1,28 @@
1
+ import type { GamesToolEntry, ToolLocaleContent } from "../../types";
2
+ import type { DamageFormulaLabUI } from "./ui";
3
+
4
+ export type { DamageFormulaLabUI };
5
+ export type DamageFormulaLabLocaleContent =
6
+ ToolLocaleContent<DamageFormulaLabUI>;
7
+
8
+ export const damageFormulaLab: GamesToolEntry<DamageFormulaLabUI> = {
9
+ id: "damage-formula-lab",
10
+ icons: { bg: "mdi:sword-cross", fg: "mdi:function-variant" },
11
+ i18n: {
12
+ de: () => import("./i18n/de").then((module) => module.content),
13
+ en: () => import("./i18n/en").then((module) => module.content),
14
+ es: () => import("./i18n/es").then((module) => module.content),
15
+ fr: () => import("./i18n/fr").then((module) => module.content),
16
+ id: () => import("./i18n/id").then((module) => module.content),
17
+ it: () => import("./i18n/it").then((module) => module.content),
18
+ ja: () => import("./i18n/ja").then((module) => module.content),
19
+ ko: () => import("./i18n/ko").then((module) => module.content),
20
+ nl: () => import("./i18n/nl").then((module) => module.content),
21
+ pl: () => import("./i18n/pl").then((module) => module.content),
22
+ pt: () => import("./i18n/pt").then((module) => module.content),
23
+ ru: () => import("./i18n/ru").then((module) => module.content),
24
+ sv: () => import("./i18n/sv").then((module) => module.content),
25
+ tr: () => import("./i18n/tr").then((module) => module.content),
26
+ zh: () => import("./i18n/zh").then((module) => module.content),
27
+ },
28
+ };
@@ -0,0 +1,84 @@
1
+ import { buildCurve, evaluateExpression, evaluateFormula } from "./logic";
2
+ import type {
3
+ CombatSettings,
4
+ FormulaResult,
5
+ FormulaVariables,
6
+ SamplePoint,
7
+ SweepRange,
8
+ } from "./logic";
9
+
10
+ export type DiagnosticKind = "ok" | "warning" | "error";
11
+
12
+ export interface FormulaEvaluation {
13
+ result?: FormulaResult;
14
+ diagnostics: string[];
15
+ kind: DiagnosticKind;
16
+ }
17
+
18
+ function curveDiagnostics(points: SamplePoint[]): string[] {
19
+ const messages: string[] = [];
20
+ if (points.some((point) => point.roundedDamage <= 0))
21
+ messages.push("The sweep contains a zero damage region.");
22
+ const cliffs = points.slice(1).filter((point, index) => {
23
+ const previous = points[index]?.roundedDamage ?? 0;
24
+ return (
25
+ previous > 0 && Math.abs(point.roundedDamage - previous) / previous > 0.5
26
+ );
27
+ });
28
+ if (cliffs.length > 0)
29
+ messages.push("The sweep contains a damage jump above 50%.");
30
+ const ttkSteps = new Set(points.map((point) => point.hitsToKill));
31
+ if (ttkSteps.size > Math.ceil(points.length * 0.65))
32
+ messages.push(
33
+ "Small attack changes create frequent hit count breakpoints.",
34
+ );
35
+ return messages;
36
+ }
37
+
38
+ export function inspectFormula(
39
+ expression: string,
40
+ variables: FormulaVariables,
41
+ settings: CombatSettings,
42
+ ): FormulaEvaluation {
43
+ try {
44
+ const raw = evaluateExpression(expression, variables);
45
+ const result = evaluateFormula(expression, variables, settings);
46
+ const diagnostics: string[] = [];
47
+ if (raw < 0)
48
+ diagnostics.push(
49
+ "The formula is negative before modifiers and clamping.",
50
+ );
51
+ if (result.roundedDamage <= 0)
52
+ diagnostics.push("This setup cannot damage the target.");
53
+ if (Math.abs(result.expectedDamage - result.roundedDamage) >= 0.5)
54
+ diagnostics.push("Rounding changes expected damage by at least 0.5.");
55
+ return {
56
+ result,
57
+ diagnostics,
58
+ kind: diagnostics.length > 0 ? "warning" : "ok",
59
+ };
60
+ } catch (error) {
61
+ const message =
62
+ error instanceof Error
63
+ ? error.message
64
+ : "The formula could not be evaluated";
65
+ return { diagnostics: [message], kind: "error" };
66
+ }
67
+ }
68
+
69
+ export function inspectSweep(
70
+ expression: string,
71
+ variables: FormulaVariables,
72
+ settings: CombatSettings,
73
+ range: SweepRange,
74
+ ): string[] {
75
+ try {
76
+ return curveDiagnostics(buildCurve(expression, variables, settings, range));
77
+ } catch (error) {
78
+ return [
79
+ error instanceof Error
80
+ ? error.message
81
+ : "The sweep could not be evaluated",
82
+ ];
83
+ }
84
+ }
@@ -0,0 +1,191 @@
1
+ import type { FormulaVariables } from "./logic";
2
+
3
+ type Token =
4
+ | { kind: "number"; value: number }
5
+ | { kind: "name"; value: string }
6
+ | { kind: "symbol"; value: string };
7
+
8
+ const FUNCTIONS: Record<string, (...values: number[]) => number> = {
9
+ abs: Math.abs,
10
+ ceil: Math.ceil,
11
+ clamp: (value, min, max) => Math.min(Math.max(value, min), max),
12
+ floor: Math.floor,
13
+ max: Math.max,
14
+ min: Math.min,
15
+ pow: Math.pow,
16
+ round: Math.round,
17
+ sqrt: Math.sqrt,
18
+ };
19
+
20
+ const PRECEDENCE: Record<string, number> = {
21
+ "+": 1,
22
+ "-": 1,
23
+ "*": 2,
24
+ "/": 2,
25
+ "^": 3,
26
+ neg: 3,
27
+ };
28
+
29
+ function readNumber(expression: string, start: number): [Token, number] {
30
+ const match = expression
31
+ .slice(start)
32
+ .match(/^(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i);
33
+ if (!match) throw new Error(`Invalid number at position ${start + 1}`);
34
+ return [{ kind: "number", value: Number(match[0]) }, start + match[0].length];
35
+ }
36
+
37
+ function readName(expression: string, start: number): [Token, number] {
38
+ const match = expression.slice(start).match(/^[a-z][a-z0-9_]*/i);
39
+ if (!match) throw new Error(`Invalid name at position ${start + 1}`);
40
+ return [{ kind: "name", value: match[0] }, start + match[0].length];
41
+ }
42
+
43
+ export function tokenize(expression: string): Token[] {
44
+ const tokens: Token[] = [];
45
+ let index = 0;
46
+ while (index < expression.length) {
47
+ const character = expression[index] ?? "";
48
+ if (/\s/.test(character)) index += 1;
49
+ else if (/[\d.]/.test(character)) {
50
+ const [token, next] = readNumber(expression, index);
51
+ tokens.push(token);
52
+ index = next;
53
+ } else if (/[a-z_]/i.test(character)) {
54
+ const [token, next] = readName(expression, index);
55
+ tokens.push(token);
56
+ index = next;
57
+ } else if ("+-*/^(),".includes(character)) {
58
+ tokens.push({ kind: "symbol", value: character });
59
+ index += 1;
60
+ } else throw new Error(`Unsupported character at position ${index + 1}`);
61
+ }
62
+ if (tokens.length === 0) throw new Error("Enter a damage formula");
63
+ return tokens;
64
+ }
65
+
66
+ class Parser {
67
+ private index = 0;
68
+ constructor(
69
+ private readonly tokens: Token[],
70
+ private readonly variables: FormulaVariables,
71
+ ) {}
72
+
73
+ parse(): number {
74
+ const value = this.expression(0);
75
+ if (this.index !== this.tokens.length) throw new Error("Unexpected token");
76
+ return value;
77
+ }
78
+
79
+ private expression(minimum: number): number {
80
+ let left = this.prefix();
81
+ while (this.isBinary() && this.precedence() >= minimum) {
82
+ const operator = this.takeSymbol();
83
+ const nextMinimum =
84
+ (PRECEDENCE[operator] ?? 0) + (operator === "^" ? 0 : 1);
85
+ left = calculate(operator, left, this.expression(nextMinimum));
86
+ }
87
+ return left;
88
+ }
89
+
90
+ private prefix(): number {
91
+ const token = this.tokens[this.index];
92
+ if (!token) throw new Error("Formula ended unexpectedly");
93
+ if (token.kind === "number") {
94
+ this.index += 1;
95
+ return token.value;
96
+ }
97
+ if (token.kind === "name") return this.nameValue(token.value);
98
+ if (token.kind === "symbol") return this.symbolValue(token.value);
99
+ throw new Error("Expected a number, variable, function, or group");
100
+ }
101
+
102
+ private symbolValue(symbol: string): number {
103
+ if (symbol === "-") {
104
+ this.index += 1;
105
+ return -this.expression(PRECEDENCE.neg ?? 3);
106
+ }
107
+ if (symbol === "(") return this.group();
108
+ throw new Error("Expected a number, variable, function, or group");
109
+ }
110
+
111
+ private nameValue(name: string): number {
112
+ this.index += 1;
113
+ const next = this.tokens[this.index];
114
+ if (next?.kind === "symbol" && next.value === "(")
115
+ return this.callFunction(name);
116
+ if (!(name in this.variables)) throw new Error(`Unknown variable: ${name}`);
117
+ return this.variables[name as keyof FormulaVariables];
118
+ }
119
+
120
+ private callFunction(name: string): number {
121
+ const callable = FUNCTIONS[name];
122
+ if (!callable) throw new Error(`Unknown function: ${name}`);
123
+ this.index += 1;
124
+ const values: number[] = [];
125
+ if (!this.isSymbol(")")) {
126
+ values.push(this.expression(0));
127
+ while (this.isSymbol(",")) {
128
+ this.index += 1;
129
+ values.push(this.expression(0));
130
+ }
131
+ }
132
+ this.expect(")");
133
+ return callable(...values);
134
+ }
135
+
136
+ private group(): number {
137
+ this.index += 1;
138
+ const value = this.expression(0);
139
+ this.expect(")");
140
+ return value;
141
+ }
142
+
143
+ private expect(symbol: string): void {
144
+ if (!this.isSymbol(symbol)) throw new Error(`Expected ${symbol}`);
145
+ this.index += 1;
146
+ }
147
+
148
+ private takeSymbol(): string {
149
+ const token = this.tokens[this.index];
150
+ if (token?.kind !== "symbol") throw new Error("Expected an operator");
151
+ this.index += 1;
152
+ return token.value;
153
+ }
154
+
155
+ private isSymbol(symbol: string): boolean {
156
+ const token = this.tokens[this.index];
157
+ return token?.kind === "symbol" && token.value === symbol;
158
+ }
159
+
160
+ private isBinary(): boolean {
161
+ const token = this.tokens[this.index];
162
+ return (
163
+ token?.kind === "symbol" &&
164
+ token.value in PRECEDENCE &&
165
+ token.value !== "neg"
166
+ );
167
+ }
168
+
169
+ private precedence(): number {
170
+ const token = this.tokens[this.index];
171
+ return token?.kind === "symbol" ? (PRECEDENCE[token.value] ?? -1) : -1;
172
+ }
173
+ }
174
+
175
+ function calculate(operator: string, left: number, right: number): number {
176
+ if (operator === "+") return left + right;
177
+ if (operator === "-") return left - right;
178
+ if (operator === "*") return left * right;
179
+ if (operator === "/") return left / right;
180
+ return left ** right;
181
+ }
182
+
183
+ export function evaluateExpression(
184
+ expression: string,
185
+ variables: FormulaVariables,
186
+ ): number {
187
+ const result = new Parser(tokenize(expression), variables).parse();
188
+ if (!Number.isFinite(result))
189
+ throw new Error("Formula produced a non-finite value");
190
+ return result;
191
+ }
@@ -0,0 +1,194 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { DamageFormulaLabUI } from '../ui';
4
+ import { bibliographyEntries } from '../bibliography';
5
+
6
+ const slug = 'spiel-schadensformel-rechner-ttk';
7
+ const title = 'Damage Formula Lab mit TTK Diagrammen';
8
+ const description = 'Vergleichen Sie sichere Schadensformeln für Spiele mit Echtzeit-Kurven, Heatmaps, Rundungs-Breakpoints, Trefferanzahl und Time-to-Kill (TTK).';
9
+
10
+ const faq = [
11
+ {
12
+ question: 'Was vergleicht der Schadensformel-Rechner?',
13
+ answer: 'Er führt zwei mathematische Formeln sicher gegen dieselben Kampfwerte aus. Sie können Schadenskurven, Trefferschwellen, Time to Kill (TTK), Rundungsregeln und Rüstungs-Heatmaps ohne JavaScript-Ausführung vergleichen.',
14
+ },
15
+ {
16
+ question: 'Welche Variablen und Funktionen stehen zur Verfügung?',
17
+ answer: 'Verfügbare Variablen sind attack, defense, level, power, resistance, flat, criticalChance und criticalMultiplier. Sichere Funktionen sind min, max, clamp, abs, sqrt, pow, floor, round und ceil.',
18
+ },
19
+ {
20
+ question: 'Wie wird die Time to Kill (TTK) berechnet?',
21
+ answer: 'Erforderliche Treffer sind die Ziel-Gesundheit geteilt durch den gerundeten Erwartungsschaden (aufgerundet). TTK misst das Intervall zwischen erstem und letztem Treffer: (Treffer - 1) / Angriffe pro Sekunde.',
22
+ },
23
+ {
24
+ question: 'Warum spielt die Reihenfolge der Widerstände eine Rolle?',
25
+ answer: 'Wird ein flacher Modifikator vor der prozentualen Resistenz angewendet, reduziert die Resistenz auch diesen Wert. Wird die Resistenz zuerst angewendet, bleibt der flache Modifikator unberührt.',
26
+ },
27
+ {
28
+ question: 'Bedeutet eine glatte Schadenskurve ein ausbalanciertes Spiel?',
29
+ answer: 'Nein. Eine Kurve zeigt Schwellenwerte und Nullschadenszonen, aber Balance hängt vom Spielkontext, Feedback und Spieltests ab.',
30
+ },
31
+ ];
32
+
33
+ const howTo = [
34
+ { name: 'Zwei Formeln wählen', text: 'Starten Sie mit einer Vorlage (linear, Verhältnis, Level-Skalierung) oder geben Sie zwei eigene Formeln ein.' },
35
+ { name: 'Kampfzustand einstellen', text: 'Geben Sie Werte für Angriff, Verteidigung, Level, Kraftkoeffizient, Resistenz, flachen Modifikator, Krit-Werte, Gesundheit und Kadenz ein.' },
36
+ { name: 'Engine-Regeln festlegen', text: 'Bestimmen Sie, wie der Schaden gerundet wird und ob Resistenzen vor oder nach dem flachen Modifikator wirken.' },
37
+ { name: 'Kurven und Schwellen analysieren', text: 'Vergleichen Sie Schadensverlauf, Angriffs-Sweep, Verteidigungs-Heatmap, Trefferanzahl und Warnhinweise.' },
38
+ { name: 'Experiment speichern', text: 'Kopieren Sie einen Freigabelink oder laden Sie die Konfiguration als JSON, CSV oder Diagramm-PNG herunter.' },
39
+ ];
40
+
41
+ const softwareApplication: WithContext<SoftwareApplication> = {
42
+ '@context': 'https://schema.org',
43
+ '@type': 'SoftwareApplication',
44
+ name: title,
45
+ applicationCategory: 'DeveloperApplication',
46
+ operatingSystem: 'Any',
47
+ };
48
+
49
+ const faqPage: WithContext<FAQPage> = {
50
+ '@context': 'https://schema.org',
51
+ '@type': 'FAQPage',
52
+ mainEntity: faq.map((item) => ({
53
+ '@type': 'Question',
54
+ name: item.question,
55
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
56
+ })),
57
+ };
58
+
59
+ const howToSchema: WithContext<HowTo> = {
60
+ '@context': 'https://schema.org',
61
+ '@type': 'HowTo',
62
+ name: title,
63
+ step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })),
64
+ };
65
+
66
+ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
67
+ slug,
68
+ title,
69
+ description,
70
+ ui: {
71
+ onboarding: 'Geben Sie Ihre aktuelle Schadensregel ein, stellen Sie eine Alternative gegenüber und passen Sie die Kampfwerte an.',
72
+ localNote: 'Privates Modell. Formeln und Daten verbleiben in diesem Browser.',
73
+ formulaDeck: 'Formelkammer',
74
+ formulaALabel: 'Formel A (Aktuelles Modell)',
75
+ formulaBLabel: 'Formel B (Herausforderer)',
76
+ formulaHint: 'Variablen: attack, defense, level, power, resistance, flat, criticalChance, criticalMultiplier',
77
+ formulaFunctions: 'Funktionen: min, max, clamp, abs, sqrt, pow, floor, round, ceil',
78
+ presetLinear: 'Linearer Schutz',
79
+ presetRatio: 'Verhältnis-Rüstung',
80
+ presetLevel: 'Level-Skalierung',
81
+ combatInputs: 'Kampfzustand',
82
+ attackLabel: 'Angriff',
83
+ defenseLabel: 'Verteidigung',
84
+ levelLabel: 'Level',
85
+ powerLabel: 'Kraftkoeffizient',
86
+ resistanceLabel: 'Resistenz (%)',
87
+ flatLabel: 'Flacher Modifikator',
88
+ criticalChanceLabel: 'Kritische Chance (%)',
89
+ criticalMultiplierLabel: 'Kritischer Multiplikator',
90
+ healthLabel: 'Ziel-Gesundheit',
91
+ cadenceLabel: 'Angriffe pro Sekunde',
92
+ roundingLabel: 'Schadensrundung',
93
+ roundingNone: 'Dezimalstellen behalten',
94
+ roundingFloor: 'Abrunden (Floor)',
95
+ roundingRound: 'Nächste Ganzzahl',
96
+ roundingCeil: 'Aufrunden (Ceil)',
97
+ orderLabel: 'Modifikator-Reihenfolge',
98
+ resistanceFirst: 'Resistenz dann Flach',
99
+ flatFirst: 'Flach dann Resistenz',
100
+ runLabel: 'Vergleich im Live-Modell',
101
+ resultDamage: 'Erwarteter Schaden',
102
+ resultHits: 'Treffer bis Besiegung',
103
+ resultTtk: 'Time to Kill (TTK)',
104
+ resultDifference: 'Schadensdifferenz',
105
+ formulaAName: 'Aktuell',
106
+ formulaBName: 'Herausforderer',
107
+ curveTitle: 'Angriffs-Verlaufskurve',
108
+ curveCaption: 'Der Angriffswert wird von der Hälfte bis zum Doppelten variiert, während die Verteidigung fest bleibt.',
109
+ heatmapTitle: 'Druckfeld-Heatmap',
110
+ heatmapCaption: 'Zeigt den Erwartungsschaden von Formel A über verschiedene Angriff- und Verteidigungskombinationen.',
111
+ attackAxis: 'Angriff steigt nach rechts',
112
+ defenseAxis: 'Verteidigung steigt nach unten',
113
+ scenariosTitle: 'Kampfszenarien',
114
+ scenarioSkirmisher: 'Plänkler',
115
+ scenarioGuardian: 'Wächter',
116
+ scenarioBoss: 'Boss',
117
+ scenarioCustom: 'Aktuelles Setup',
118
+ diagnosticsTitle: 'Schwellenwerte & Warnungen',
119
+ statusBalanced: 'Keine auffälligen mathematischen Sprünge im Testbereich erkannt.',
120
+ exportTitle: 'Experiment exportieren',
121
+ copyLink: 'Freigabelink kopieren',
122
+ exportCsv: 'CSV herunterladen',
123
+ exportJson: 'JSON herunterladen',
124
+ importJson: 'JSON importieren',
125
+ exportPng: 'Diagramm PNG herunterladen',
126
+ reset: 'Modell zurücksetzen',
127
+ privacyDisclosure: 'Der Freigabelink speichert die Konfiguration in der URL-Hash und sendet sie nicht an Server.',
128
+ limitationDisclosure: 'Der kritische Erwartungsschaden ist ein Durchschnitt und keine stochastische Simulation.',
129
+ importError: 'Die Datei ist keine gültige Konfiguration.',
130
+ copiedStatus: 'Freigabelink in die Zwischenablage kopiert.',
131
+ },
132
+ seo: [
133
+ {
134
+ type: 'title',
135
+ level: 2,
136
+ text: 'Schadensformeln vor der Implementierung in der Spiel-Engine testen',
137
+ },
138
+ {
139
+ type: 'paragraph',
140
+ html: 'Eine Schadensformel kann bei einzelnen Beispielwerten gut funktionieren, aber bei extremen Fortschrittswerten zusammenbrechen. Dieses Labor zeigt Schwellenwerte und Nullschadenszonen frühzeitig auf.',
141
+ },
142
+ {
143
+ type: 'title',
144
+ level: 2,
145
+ text: 'Sichere mathematische Auswertung ohne Code-Risiken',
146
+ },
147
+ {
148
+ type: 'paragraph',
149
+ html: 'Das Formelfeld akzeptiert vordefinierte Variablen und mathematische Funktionen ohne Ausführung von ungesichertem Code.',
150
+ },
151
+ {
152
+ type: 'table',
153
+ headers: ['Messwert', 'Berechnung', 'Designfrage'],
154
+ rows: [
155
+ ['Erwarteter Schaden', 'Grundformel inklusive Krit-Faktor und Widerstand', 'Verhält sich die Regel bei schwachen und starken Einheiten sinnvoll?'],
156
+ ['Treffer bis Besiegung', 'Ziel-Gesundheit geteilt durch gerundeten Schaden', 'Führt ein einziger Statuspunkt zum Wegfall eines kompletten Treffers?'],
157
+ ['Time to Kill (TTK)', 'Intervalle zwischen Treffern geteilt durch Kadenz', 'Erzeugt die Frequenz den gewünschten Kampfrhythmus?'],
158
+ ['Druckfeld', 'Rasteranalyse von Formel A über Angriff und Verteidigung', 'Gibt es abrupte Schwellen oder tote Zonen?'],
159
+ ],
160
+ },
161
+ {
162
+ type: 'title',
163
+ level: 2,
164
+ text: 'Mathematische Daten von Game-Design-Entscheidungen trennen',
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Ein mathematisch sauberer Graph garantiert noch kein unterhaltsames Spiel. Nutzen Sie das Labor, um Annahmen gezielt in Spieltests zu überprüfen.',
169
+ },
170
+ {
171
+ type: 'tip',
172
+ title: 'Schaden und Trefferanzahl stets gemeinsam betrachten',
173
+ html: 'Eine kleine Schadensänderung kann eine gesundheitsbezogene Schwelle überschreiten und einen Treffer einsparen. Vergleichen Sie Schadenswerte immer mit Treffern und TTK.',
174
+ },
175
+ {
176
+ type: 'title',
177
+ level: 2,
178
+ text: 'Rundung und Widerstandsreihenfolge sichtbar machen',
179
+ },
180
+ {
181
+ type: 'paragraph',
182
+ html: 'Die gleiche Formel kann je nach Rundung und Reihenfolge der Modifikatoren ein anderes Kampfergebnis liefern. Prüfen Sie deshalb nicht nur den mittleren Kurvenbereich, sondern auch kleine Trefferwerte, hohe Verteidigung und die Schwelle, an der ein weiterer Treffer nötig wird.',
183
+ },
184
+ {
185
+ type: 'paragraph',
186
+ html: 'Die TTK-Anzeige ist eine mathematische Projektion auf Basis der eingegebenen Angriffskadenz. Sie berücksichtigt keine Ausweichbewegung, Cooldowns, Zufallsfolgen oder Unterbrechungen; verwenden Sie die Kurven als Designhilfe und validieren Sie die Regel anschließend im Spieltest.',
187
+ },
188
+ ],
189
+ faq,
190
+ bibliographyTitle: 'Referenzen zur Schadensberechnung in Spielen',
191
+ bibliography: bibliographyEntries,
192
+ howTo,
193
+ schemas: [softwareApplication, faqPage, howToSchema],
194
+ };