@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,151 @@
1
+ import type {
2
+ CombatSettings,
3
+ FormulaVariables,
4
+ ModifierOrder,
5
+ RoundingMode,
6
+ SweepRange,
7
+ } from "./logic";
8
+
9
+ export interface LabConfig {
10
+ version: 1;
11
+ formulaA: string;
12
+ formulaB: string;
13
+ variables: FormulaVariables;
14
+ settings: CombatSettings;
15
+ attackRange: SweepRange;
16
+ defenseRange: SweepRange;
17
+ }
18
+
19
+ export const defaultConfig: LabConfig = {
20
+ version: 1,
21
+ formulaA: "max(1, attack * power - defense)",
22
+ formulaB: "max(1, attack * power * 100 / (100 + defense))",
23
+ variables: {
24
+ attack: 80,
25
+ defense: 35,
26
+ level: 20,
27
+ power: 1.4,
28
+ resistance: 15,
29
+ flat: 3,
30
+ criticalChance: 20,
31
+ criticalMultiplier: 1.75,
32
+ },
33
+ settings: {
34
+ health: 450,
35
+ attacksPerSecond: 1.5,
36
+ rounding: "round",
37
+ modifierOrder: "resistance-first",
38
+ },
39
+ attackRange: { min: 40, max: 160, steps: 17 },
40
+ defenseRange: { min: 10, max: 90, steps: 9 },
41
+ };
42
+
43
+ export const formulaPresets = {
44
+ linear: "max(1, attack * power - defense)",
45
+ ratio: "max(1, attack * power * 100 / (100 + defense))",
46
+ level:
47
+ "max(1, ((2 * level + 10) / 250) * (attack / max(1, defense)) * power + 2)",
48
+ };
49
+
50
+ export const scenarios: Record<
51
+ string,
52
+ Partial<FormulaVariables> & { health: number }
53
+ > = {
54
+ skirmisher: { defense: 20, resistance: 5, health: 220 },
55
+ guardian: { defense: 70, resistance: 25, health: 700 },
56
+ boss: { defense: 110, resistance: 35, health: 2200 },
57
+ };
58
+
59
+ function finite(value: unknown, fallback: number): number {
60
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
61
+ }
62
+
63
+ function rounding(value: unknown): RoundingMode {
64
+ if (value === "none" || value === "floor" || value === "ceil") return value;
65
+ return "round";
66
+ }
67
+
68
+ function order(value: unknown): ModifierOrder {
69
+ return value === "flat-first" ? "flat-first" : "resistance-first";
70
+ }
71
+
72
+ function variables(value: unknown): FormulaVariables {
73
+ const source =
74
+ typeof value === "object" && value
75
+ ? (value as Partial<FormulaVariables>)
76
+ : {};
77
+ const base = defaultConfig.variables;
78
+ return {
79
+ attack: finite(source.attack, base.attack),
80
+ defense: finite(source.defense, base.defense),
81
+ level: finite(source.level, base.level),
82
+ power: finite(source.power, base.power),
83
+ resistance: finite(source.resistance, base.resistance),
84
+ flat: finite(source.flat, base.flat),
85
+ criticalChance: finite(source.criticalChance, base.criticalChance),
86
+ criticalMultiplier: finite(
87
+ source.criticalMultiplier,
88
+ base.criticalMultiplier,
89
+ ),
90
+ };
91
+ }
92
+
93
+ function range(value: unknown, fallback: SweepRange): SweepRange {
94
+ const source =
95
+ typeof value === "object" && value ? (value as Partial<SweepRange>) : {};
96
+ return {
97
+ min: finite(source.min, fallback.min),
98
+ max: finite(source.max, fallback.max),
99
+ steps: finite(source.steps, fallback.steps),
100
+ };
101
+ }
102
+
103
+ function combatSettings(value: unknown): CombatSettings {
104
+ const source =
105
+ typeof value === "object" && value
106
+ ? (value as Partial<CombatSettings>)
107
+ : {};
108
+ return {
109
+ health: finite(source.health, defaultConfig.settings.health),
110
+ attacksPerSecond: finite(
111
+ source.attacksPerSecond,
112
+ defaultConfig.settings.attacksPerSecond,
113
+ ),
114
+ rounding: rounding(source.rounding),
115
+ modifierOrder: order(source.modifierOrder),
116
+ };
117
+ }
118
+
119
+ export function parseConfig(value: unknown): LabConfig {
120
+ if (typeof value !== "object" || !value)
121
+ throw new Error("Invalid configuration");
122
+ const source = value as Partial<LabConfig>;
123
+ return {
124
+ version: 1,
125
+ formulaA:
126
+ typeof source.formulaA === "string"
127
+ ? source.formulaA
128
+ : defaultConfig.formulaA,
129
+ formulaB:
130
+ typeof source.formulaB === "string"
131
+ ? source.formulaB
132
+ : defaultConfig.formulaB,
133
+ variables: variables(source.variables),
134
+ settings: combatSettings(source.settings),
135
+ attackRange: range(source.attackRange, defaultConfig.attackRange),
136
+ defenseRange: range(source.defenseRange, defaultConfig.defenseRange),
137
+ };
138
+ }
139
+
140
+ export function configFromHash(hash: string): LabConfig | undefined {
141
+ try {
142
+ if (!hash.startsWith("#damage=")) return undefined;
143
+ return parseConfig(JSON.parse(decodeURIComponent(hash.slice(8))));
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ export function configToHash(config: LabConfig): string {
150
+ return `#damage=${encodeURIComponent(JSON.stringify(config))}`;
151
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import type { SEOSection } from '../../types';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ sections?: SEOSection[];
8
+ }
9
+
10
+ const { locale = 'en', sections = [] } = Astro.props;
11
+ ---
12
+
13
+ <SEORenderer content={{ locale, sections }} />
@@ -0,0 +1,62 @@
1
+ import type { ModifierOrder, RoundingMode } from "./logic";
2
+
3
+ export interface LabPreferences {
4
+ formulaA: string;
5
+ formulaB: string;
6
+ rounding: RoundingMode;
7
+ modifierOrder: ModifierOrder;
8
+ }
9
+
10
+ const STORAGE_KEY = "jjlmoya-damage-formula-lab";
11
+
12
+ export const defaultPreferences: LabPreferences = {
13
+ formulaA: "max(1, attack * power - defense)",
14
+ formulaB: "max(1, attack * power * 100 / (100 + defense))",
15
+ rounding: "round",
16
+ modifierOrder: "resistance-first",
17
+ };
18
+
19
+ function isRounding(value: unknown): value is RoundingMode {
20
+ return (
21
+ value === "none" ||
22
+ value === "floor" ||
23
+ value === "round" ||
24
+ value === "ceil"
25
+ );
26
+ }
27
+
28
+ function isOrder(value: unknown): value is ModifierOrder {
29
+ return value === "resistance-first" || value === "flat-first";
30
+ }
31
+
32
+ export function loadPreferences(): LabPreferences {
33
+ try {
34
+ const parsed = JSON.parse(
35
+ localStorage.getItem(STORAGE_KEY) ?? "",
36
+ ) as Partial<LabPreferences>;
37
+ return {
38
+ formulaA:
39
+ typeof parsed.formulaA === "string"
40
+ ? parsed.formulaA
41
+ : defaultPreferences.formulaA,
42
+ formulaB:
43
+ typeof parsed.formulaB === "string"
44
+ ? parsed.formulaB
45
+ : defaultPreferences.formulaB,
46
+ rounding: isRounding(parsed.rounding)
47
+ ? parsed.rounding
48
+ : defaultPreferences.rounding,
49
+ modifierOrder: isOrder(parsed.modifierOrder)
50
+ ? parsed.modifierOrder
51
+ : defaultPreferences.modifierOrder,
52
+ };
53
+ } catch {
54
+ return { ...defaultPreferences };
55
+ }
56
+ }
57
+
58
+ export function savePreferences(preferences: LabPreferences): void {
59
+ try {
60
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
61
+ } catch {}
62
+ }
@@ -0,0 +1,89 @@
1
+ import { buildCurve } from "./logic";
2
+ import { configToHash } from "./model";
3
+ import type { LabConfig } from "./model";
4
+
5
+ export function download(name: string, type: string, body: BlobPart): void {
6
+ const link = document.createElement("a");
7
+ link.href = URL.createObjectURL(new Blob([body], { type }));
8
+ link.download = name;
9
+ link.click();
10
+ URL.revokeObjectURL(link.href);
11
+ }
12
+
13
+ function curveRows(config: LabConfig): Array<Array<number | undefined>> {
14
+ const a = buildCurve(
15
+ config.formulaA,
16
+ config.variables,
17
+ config.settings,
18
+ config.attackRange,
19
+ );
20
+ const b = buildCurve(
21
+ config.formulaB,
22
+ config.variables,
23
+ config.settings,
24
+ config.attackRange,
25
+ );
26
+ return a.map((point, index) => [
27
+ point.attack,
28
+ point.defense,
29
+ point.roundedDamage,
30
+ point.hitsToKill,
31
+ point.timeToKill,
32
+ b[index]?.roundedDamage,
33
+ b[index]?.hitsToKill,
34
+ b[index]?.timeToKill,
35
+ ]);
36
+ }
37
+
38
+ export function exportCsv(config: LabConfig): void {
39
+ const header = [
40
+ "attack",
41
+ "defense",
42
+ "formula_a_damage",
43
+ "formula_a_hits",
44
+ "formula_a_ttk",
45
+ "formula_b_damage",
46
+ "formula_b_hits",
47
+ "formula_b_ttk",
48
+ ];
49
+ const body = [header, ...curveRows(config)]
50
+ .map((row) => row.join(","))
51
+ .join("\n");
52
+ download("damage-formula-sweep.csv", "text/csv", body);
53
+ }
54
+
55
+ export function exportJson(config: LabConfig): void {
56
+ download(
57
+ "damage-formula-lab.json",
58
+ "application/json",
59
+ JSON.stringify(config, null, 2),
60
+ );
61
+ }
62
+
63
+ function drawPng(image: HTMLImageElement): void {
64
+ const canvas = document.createElement("canvas");
65
+ canvas.width = 1200;
66
+ canvas.height = 700;
67
+ const context = canvas.getContext("2d");
68
+ if (!context) return;
69
+ context.fillStyle = "#f7f4ee";
70
+ context.fillRect(0, 0, canvas.width, canvas.height);
71
+ context.drawImage(image, 70, 70, 1060, 560);
72
+ canvas.toBlob((blob) => {
73
+ if (blob) download("damage-formula-curve.png", "image/png", blob);
74
+ }, "image/png");
75
+ }
76
+
77
+ export function exportChartPng(root: HTMLElement): void {
78
+ const svg = root.querySelector<SVGSVGElement>("[data-chart]");
79
+ if (!svg) return;
80
+ const source = new XMLSerializer().serializeToString(svg);
81
+ const image = new Image();
82
+ image.onload = () => drawPng(image);
83
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
84
+ }
85
+
86
+ export async function copyShareLink(config: LabConfig): Promise<void> {
87
+ const url = `${location.origin}${location.pathname}${configToHash(config)}`;
88
+ await navigator.clipboard.writeText(url);
89
+ }
@@ -0,0 +1,62 @@
1
+ export interface DamageFormulaLabUI extends Record<string, string> {
2
+ onboarding: string;
3
+ localNote: string;
4
+ formulaDeck: string;
5
+ formulaALabel: string;
6
+ formulaBLabel: string;
7
+ formulaHint: string;
8
+ formulaFunctions: string;
9
+ presetLinear: string;
10
+ presetRatio: string;
11
+ presetLevel: string;
12
+ combatInputs: string;
13
+ attackLabel: string;
14
+ defenseLabel: string;
15
+ levelLabel: string;
16
+ powerLabel: string;
17
+ resistanceLabel: string;
18
+ flatLabel: string;
19
+ criticalChanceLabel: string;
20
+ criticalMultiplierLabel: string;
21
+ healthLabel: string;
22
+ cadenceLabel: string;
23
+ roundingLabel: string;
24
+ roundingNone: string;
25
+ roundingFloor: string;
26
+ roundingRound: string;
27
+ roundingCeil: string;
28
+ orderLabel: string;
29
+ resistanceFirst: string;
30
+ flatFirst: string;
31
+ runLabel: string;
32
+ resultDamage: string;
33
+ resultHits: string;
34
+ resultTtk: string;
35
+ resultDifference: string;
36
+ formulaAName: string;
37
+ formulaBName: string;
38
+ curveTitle: string;
39
+ curveCaption: string;
40
+ heatmapTitle: string;
41
+ heatmapCaption: string;
42
+ attackAxis: string;
43
+ defenseAxis: string;
44
+ scenariosTitle: string;
45
+ scenarioSkirmisher: string;
46
+ scenarioGuardian: string;
47
+ scenarioBoss: string;
48
+ scenarioCustom: string;
49
+ diagnosticsTitle: string;
50
+ statusBalanced: string;
51
+ exportTitle: string;
52
+ copyLink: string;
53
+ exportCsv: string;
54
+ exportJson: string;
55
+ importJson: string;
56
+ exportPng: string;
57
+ reset: string;
58
+ privacyDisclosure: string;
59
+ limitationDisclosure: string;
60
+ importError: string;
61
+ copiedStatus: string;
62
+ }
@@ -0,0 +1,22 @@
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 && <section class="gll-references" aria-labelledby="gll-reference-title"><h2 id="gll-reference-title">{title}</h2><ul>{entries.map((entry) => <li><a href={entry.url} rel="noopener noreferrer" target="_blank">{entry.name}</a></li>)}</ul></section>}
13
+
14
+ <style>
15
+ .gll-references {
16
+ margin-block: 2rem;
17
+ }
18
+
19
+ .gll-references a {
20
+ color: var(--n-primary);
21
+ }
22
+ </style>
@@ -0,0 +1,10 @@
1
+ export const bibliographyEntries = [
2
+ {
3
+ name: 'Fix Your Timestep! by Glenn Fiedler',
4
+ url: 'https://gafferongames.com/post/fix_your_timestep/',
5
+ },
6
+ {
7
+ name: 'Game Loop from Game Programming Patterns',
8
+ url: 'https://gameprogrammingpatterns.com/game-loop.html',
9
+ },
10
+ ];
@@ -0,0 +1,51 @@
1
+ ---
2
+ import type { GameDeltaTimeFixedTimestepLabUI } from './ui';
3
+
4
+ interface Props {
5
+ ui: GameDeltaTimeFixedTimestepLabUI;
6
+ }
7
+
8
+ const { ui } = Astro.props as Props;
9
+ ---
10
+
11
+ <div class="game-loop-lab" data-game-loop-lab>
12
+ <div class="gll-shell">
13
+ <form class="gll-controls" data-controls>
14
+ <div class="gll-control-heading"><p class="gll-kicker">{ui.controlsTitle}</p></div>
15
+ <label class="gll-range-field"><span>{ui.fpsLabel}</span><output data-output="fps">60</output><input data-field="fps" type="range" min="1" max="240" step="1" value="60" /></label>
16
+ <p class="gll-field-hint">{ui.fpsHint}</p>
17
+ <label class="gll-range-field"><span>{ui.spikeLabel} ({ui.unitMs})</span><output data-output="spikeMs">80</output><input data-field="spikeMs" type="range" min="0" max="250" step="1" value="80" /></label>
18
+ <p class="gll-field-hint">{ui.spikeHint}</p>
19
+ <label class="gll-number-field"><span>{ui.spikeEveryLabel}</span><input data-field="spikeEveryFrames" type="number" min="1" max="600" step="1" value="30" /></label>
20
+ <label class="gll-range-field"><span>{ui.fixedDtLabel} ({ui.unitMs})</span><output data-output="fixedDtMs">16.666</output><input data-field="fixedDtMs" type="range" min="1" max="50" step="0.001" value="16.666" /></label>
21
+ <p class="gll-field-hint">{ui.fixedDtHint}</p>
22
+ <label class="gll-range-field"><span>{ui.velocityLabel}</span><output data-output="velocity">20</output><input data-field="velocity" type="range" min="0" max="100" step="1" value="20" /></label>
23
+ <label class="gll-range-field"><span>{ui.durationLabel}</span><output data-output="durationSeconds">6</output><input data-field="durationSeconds" type="range" min="1" max="20" step="1" value="6" /></label>
24
+ <div class="gll-clamp" data-clamp-detail>
25
+ <label class="gll-check-field"><input data-field="clampEnabled" type="checkbox" /><span>{ui.clampToggle}</span></label>
26
+ <label class="gll-range-field"><span>{ui.clampLabel} ({ui.unitMs})</span><output data-output="clampMs">100</output><input data-field="clampMs" type="range" min="1" max="250" step="1" value="100" /></label>
27
+ </div>
28
+ <div class="gll-actions"><button type="button" class="gll-primary" data-run>{ui.runLabel}</button><button type="button" class="gll-secondary" data-reset>{ui.resetLabel}</button></div>
29
+ </form>
30
+ <section class="gll-stage" aria-labelledby="gll-stage-title">
31
+ <div class="gll-stage-heading"><div><p class="gll-kicker">{ui.stageKicker}</p><h2 id="gll-stage-title">{ui.stageTitle}</h2><p>{ui.stageCaption}</p></div><span class="gll-live-badge" data-live aria-live="polite">{ui.statusReady}</span></div>
32
+ <div class="gll-stage-visual" data-stage></div>
33
+ <div data-summary></div>
34
+ <div class="gll-timeline-heading"><h3>{ui.timelineTitle}</h3><p>{ui.timelineCaption}</p></div>
35
+ <div data-chart></div>
36
+ <div class="gll-legend"><span><i class="gll-legend-dot gll-variable-dot"></i>{ui.legendVariable}</span><span><i class="gll-legend-dot gll-fixed-dot"></i>{ui.legendFixed}</span><span><i class="gll-legend-dot gll-spike-dot"></i>{ui.legendSpike}</span></div>
37
+ <div class="gll-table" data-table></div>
38
+ <p class="gll-model-note">{ui.modelNote}</p>
39
+ <p class="gll-privacy">{ui.privacyDisclosure}</p>
40
+ </section>
41
+ </div>
42
+ </div>
43
+
44
+ <script is:inline type="application/json" data-game-loop-ui set:html={JSON.stringify(ui)}></script>
45
+
46
+ <script>
47
+ import { mountGameDeltaTimeFixedTimestepLab } from './controller';
48
+ const root = document.querySelector<HTMLElement>('[data-game-loop-lab]');
49
+ const uiScript = document.querySelector<HTMLScriptElement>('[data-game-loop-ui]');
50
+ if (root && uiScript) mountGameDeltaTimeFixedTimestepLab(root, JSON.parse(uiScript.textContent ?? '{}'));
51
+ </script>
@@ -0,0 +1,79 @@
1
+ import { evaluateLoop } from './evaluator';
2
+ import { DEFAULT_LOOP_CONFIG, normalizeLoopConfig, simulateLoop, type LoopConfig } from './logic';
3
+ import { renderAccessibleTable, renderChart, renderStage, renderSummary } from './dom-views';
4
+ import { loadLoopConfig, saveLoopConfig } from './storage';
5
+ import type { GameDeltaTimeFixedTimestepLabUI } from './ui';
6
+
7
+ const fieldNames: (keyof LoopConfig)[] = ['fps', 'spikeMs', 'spikeEveryFrames', 'fixedDtMs', 'velocity', 'durationSeconds', 'clampMs'];
8
+
9
+ function input(root: HTMLElement, name: string): HTMLInputElement {
10
+ return root.querySelector<HTMLInputElement>(`[data-field="${name}"]`) as HTMLInputElement;
11
+ }
12
+
13
+ function readConfig(root: HTMLElement): LoopConfig {
14
+ const values = Object.fromEntries(fieldNames.map((name) => [name, Number(input(root, name).value)]));
15
+ const clampEnabled = root.querySelector<HTMLInputElement>('[data-field="clampEnabled"]')?.checked ?? false;
16
+ return normalizeLoopConfig({ ...values, clampEnabled });
17
+ }
18
+
19
+ function syncOutputs(root: HTMLElement): void {
20
+ fieldNames.forEach((name) => {
21
+ const field = input(root, name);
22
+ const output = root.querySelector<HTMLOutputElement>(`[data-output="${name}"]`);
23
+ if (output) output.value = field.value;
24
+ });
25
+ }
26
+
27
+ function syncClamp(root: HTMLElement): void {
28
+ const enabled = root.querySelector<HTMLInputElement>('[data-field="clampEnabled"]')?.checked ?? false;
29
+ input(root, 'clampMs').disabled = !enabled;
30
+ root.querySelector<HTMLElement>('[data-clamp-detail]')?.toggleAttribute('data-disabled', !enabled);
31
+ }
32
+
33
+ function render(root: HTMLElement, ui: GameDeltaTimeFixedTimestepLabUI, config: LoopConfig): void {
34
+ const result = simulateLoop(config);
35
+ const diagnostic = evaluateLoop(result);
36
+ const stage = root.querySelector<HTMLElement>('[data-stage]');
37
+ const summary = root.querySelector<HTMLElement>('[data-summary]');
38
+ const chart = root.querySelector<HTMLElement>('[data-chart]');
39
+ const table = root.querySelector<HTMLElement>('[data-table]');
40
+ if (stage) renderStage(stage, result, ui);
41
+ if (summary) renderSummary(summary, result, diagnostic, ui);
42
+ if (chart) renderChart(chart, result, ui);
43
+ if (table) renderAccessibleTable(table, result, ui);
44
+ root.querySelector<HTMLElement>('[data-live]')!.textContent = ui.statusReady;
45
+ saveLoopConfig(config);
46
+ }
47
+
48
+ function bindInputs(root: HTMLElement, ui: GameDeltaTimeFixedTimestepLabUI): void {
49
+ fieldNames.forEach((name) => input(root, name).addEventListener('input', () => {
50
+ syncOutputs(root);
51
+ render(root, ui, readConfig(root));
52
+ }));
53
+ root.querySelector<HTMLInputElement>('[data-field="clampEnabled"]')?.addEventListener('change', () => {
54
+ syncClamp(root);
55
+ render(root, ui, readConfig(root));
56
+ });
57
+ root.querySelector<HTMLButtonElement>('[data-run]')?.addEventListener('click', () => {
58
+ render(root, ui, readConfig(root));
59
+ });
60
+ root.querySelector<HTMLButtonElement>('[data-reset]')?.addEventListener('click', () => {
61
+ fieldNames.forEach((name) => { input(root, name).value = String(DEFAULT_LOOP_CONFIG[name]); });
62
+ const checkbox = root.querySelector<HTMLInputElement>('[data-field="clampEnabled"]');
63
+ if (checkbox) checkbox.checked = DEFAULT_LOOP_CONFIG.clampEnabled;
64
+ syncOutputs(root);
65
+ syncClamp(root);
66
+ render(root, ui, readConfig(root));
67
+ });
68
+ }
69
+
70
+ export function mountGameDeltaTimeFixedTimestepLab(root: HTMLElement, ui: GameDeltaTimeFixedTimestepLabUI): void {
71
+ const stored = normalizeLoopConfig(loadLoopConfig());
72
+ fieldNames.forEach((name) => { input(root, name).value = String(stored[name]); });
73
+ const checkbox = root.querySelector<HTMLInputElement>('[data-field="clampEnabled"]');
74
+ if (checkbox) checkbox.checked = stored.clampEnabled;
75
+ syncOutputs(root);
76
+ syncClamp(root);
77
+ bindInputs(root, ui);
78
+ render(root, ui, stored);
79
+ }
@@ -0,0 +1,88 @@
1
+ import type { GameDeltaTimeFixedTimestepLabUI } from './ui';
2
+ import type { LoopResult, LoopSample } from './logic';
3
+ import type { Diagnostic } from './evaluator';
4
+
5
+ function escapeHtml(value: string): string {
6
+ return value.replace(/[&<>"']/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] ?? character);
7
+ }
8
+
9
+ function number(value: number, digits = 2): string {
10
+ return value.toLocaleString('en-US', { maximumFractionDigits: digits, minimumFractionDigits: digits });
11
+ }
12
+
13
+ function samplePoints(samples: LoopSample[]): LoopSample[] {
14
+ const stride = Math.max(1, Math.ceil(samples.length / 80));
15
+ return samples.filter((_, index) => index % stride === 0 || index === samples.length - 1);
16
+ }
17
+
18
+ interface PathOptions {
19
+ samples: LoopSample[];
20
+ key: 'variablePosition' | 'fixedPosition';
21
+ max: number;
22
+ top?: number;
23
+ height?: number;
24
+ }
25
+
26
+ function pathFor(options: PathOptions): string {
27
+ const { samples, key, max, top = 190, height = 140 } = options;
28
+ return samples.map((sample, index) => {
29
+ const x = 54 + (index / Math.max(1, samples.length - 1)) * 672;
30
+ const y = top - (sample[key] / max) * height;
31
+ return `${index === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
32
+ }).join(' ');
33
+ }
34
+
35
+ function differencePath(samples: LoopSample[], max: number): string {
36
+ return samples.map((sample, index) => {
37
+ const x = 54 + (index / Math.max(1, samples.length - 1)) * 672;
38
+ const difference = sample.variablePosition - sample.fixedPosition;
39
+ const y = 247 - ((difference + max) / (max * 2)) * 54;
40
+ return `${index === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
41
+ }).join(' ');
42
+ }
43
+
44
+ function frameBars(samples: LoopSample[], baseMs: number, maxMs: number): string {
45
+ return samples.map((sample, index) => {
46
+ const x = 54 + (index / Math.max(1, samples.length - 1)) * 672;
47
+ const height = (sample.frameMs / maxMs) * 30;
48
+ const isSpike = sample.frameMs > baseMs * 1.25;
49
+ return `<rect class="${isSpike ? 'gll-spike-bar' : 'gll-frame-bar'}" x="${(x - 3).toFixed(1)}" y="${(76 - height).toFixed(1)}" width="6" height="${height.toFixed(1)}" rx="2" />`;
50
+ }).join('');
51
+ }
52
+
53
+ export function renderStage(target: HTMLElement, result: LoopResult, ui: GameDeltaTimeFixedTimestepLabUI): void {
54
+ const points = samplePoints(result.samples);
55
+ const maxPosition = Math.max(1, result.variablePosition, result.fixedPosition);
56
+ const maxDifference = Math.max(1, ...points.map((sample) => Math.abs(sample.variablePosition - sample.fixedPosition)));
57
+ const baseMs = 1000 / result.config.fps;
58
+ const maxMs = Math.max(baseMs, ...points.map((sample) => sample.frameMs));
59
+ const bars = frameBars(points, baseMs, maxMs);
60
+ const variablePath = pathFor({ samples: points, key: 'variablePosition', max: maxPosition, top: 170, height: 60 });
61
+ const fixedPath = pathFor({ samples: points, key: 'fixedPosition', max: maxPosition, top: 170, height: 60 });
62
+ const deltaPath = differencePath(points, maxDifference);
63
+ const variableY = 170 - (result.variablePosition / maxPosition) * 60;
64
+ const fixedY = 170 - (result.fixedPosition / maxPosition) * 60;
65
+ target.innerHTML = `<svg class="gll-stage-svg" viewBox="0 0 760 330" role="img" aria-label="${escapeHtml(ui.stageTitle)}"><rect class="gll-paper" x="0" y="0" width="760" height="330" rx="18" /><text class="gll-plot-title" x="24" y="28">${escapeHtml(ui.frameTraceLabel)}</text><line class="gll-base-line" x1="54" y1="76" x2="726" y2="76" />${bars}<text class="gll-plot-axis" x="726" y="94" text-anchor="end">${number(maxMs, 1)} ${escapeHtml(ui.unitMs)}</text><text class="gll-plot-title" x="24" y="112">${escapeHtml(ui.positionPlotLabel)}</text><line class="gll-grid-line" x1="54" y1="170" x2="726" y2="170" /><text class="gll-plot-label" x="24" y="140">${escapeHtml(ui.variableLane)}</text><text class="gll-plot-label" x="24" y="165">${escapeHtml(ui.fixedLane)}</text><path class="gll-variable-line gll-stage-position-line" d="${variablePath}" /><path class="gll-fixed-line gll-stage-position-line" d="${fixedPath}" /><circle class="gll-variable-token" cx="726" cy="${variableY.toFixed(1)}" r="7" /><circle class="gll-fixed-token" cx="726" cy="${fixedY.toFixed(1)}" r="7" /><text class="gll-plot-axis" x="726" y="184" text-anchor="end">${number(maxPosition)} ${escapeHtml(ui.unitPixels)}</text><text class="gll-plot-title" x="24" y="210">${escapeHtml(ui.differencePlotLabel)}</text><line class="gll-zero-line" x1="54" y1="247" x2="726" y2="247" />${deltaPath ? `<path class="gll-difference-line" d="${deltaPath}" />` : ''}<text class="gll-plot-axis" x="726" y="268" text-anchor="end">+/- ${number(maxDifference)} ${escapeHtml(ui.unitPixels)}</text><text class="gll-stage-axis" x="54" y="302">0 ${escapeHtml(ui.unitSeconds)}</text><text class="gll-stage-axis" x="726" y="302" text-anchor="end">${number(result.totalWallTime)} ${escapeHtml(ui.unitSeconds)}</text></svg>`;
66
+ }
67
+
68
+ export function renderChart(target: HTMLElement, result: LoopResult, ui: GameDeltaTimeFixedTimestepLabUI): void {
69
+ const points = samplePoints(result.samples);
70
+ const max = Math.max(1, ...points.map((sample) => Math.max(sample.variablePosition, sample.fixedPosition)));
71
+ const variablePath = pathFor({ samples: points, key: 'variablePosition', max });
72
+ const fixedPath = pathFor({ samples: points, key: 'fixedPosition', max });
73
+ target.innerHTML = `<svg class="gll-chart-svg" viewBox="0 0 760 250" role="img" aria-label="${escapeHtml(ui.timelineTitle)}"><line class="gll-chart-axis" x1="54" y1="190" x2="726" y2="190" /><line class="gll-chart-axis" x1="54" y1="32" x2="54" y2="190" /><path class="gll-variable-line" d="${variablePath}" /><path class="gll-fixed-line" d="${fixedPath}" /><text class="gll-chart-label" x="54" y="220">0 ${escapeHtml(ui.unitSeconds)}</text><text class="gll-chart-label" x="726" y="220" text-anchor="end">${number(result.totalWallTime)} ${escapeHtml(ui.unitSeconds)}</text><text class="gll-chart-label" x="42" y="38" text-anchor="end">${number(max)} ${escapeHtml(ui.unitPixels)}</text><text class="gll-chart-label" x="390" y="244" text-anchor="middle">${escapeHtml(ui.frameAxis)}</text><text class="gll-chart-label" x="14" y="110" transform="rotate(-90 14 110)" text-anchor="middle">${escapeHtml(ui.positionAxis)}</text></svg>`;
74
+ }
75
+
76
+ function diagnosticText(diagnostic: Diagnostic, ui: GameDeltaTimeFixedTimestepLabUI): string {
77
+ const labels = { stable: ui.stableStatus, variable: ui.variableStatus, fixed: ui.fixedStatus, clamped: ui.clampStatus };
78
+ return labels[diagnostic.key];
79
+ }
80
+
81
+ export function renderSummary(target: HTMLElement, result: LoopResult, diagnostic: Diagnostic, ui: GameDeltaTimeFixedTimestepLabUI): void {
82
+ target.innerHTML = `<div class="gll-summary-grid"><div><span>${escapeHtml(ui.frameCountLabel)}</span><strong>${result.frameCount.toLocaleString('en-US')}</strong></div><div><span>${escapeHtml(ui.wallTimeLabel)}</span><strong>${number(result.totalWallTime)} ${escapeHtml(ui.unitSeconds)}</strong></div><div><span>${escapeHtml(ui.variableTimeLabel)}</span><strong>${number(result.variableTime)} ${escapeHtml(ui.unitSeconds)}</strong></div><div><span>${escapeHtml(ui.fixedTimeLabel)}</span><strong>${number(result.fixedTime)} ${escapeHtml(ui.unitSeconds)}</strong></div><div><span>${escapeHtml(ui.divergenceLabel)}</span><strong>${number(result.divergence)} ${escapeHtml(ui.unitPixels)}</strong></div><div><span>${escapeHtml(ui.stepsLabel)}</span><strong>${result.fixedSteps.toLocaleString('en-US')}</strong></div></div><p class="gll-diagnostic gll-diagnostic-${diagnostic.severity}">${escapeHtml(ui.diagnosticsTitle)}: ${escapeHtml(diagnosticText(diagnostic, ui))}</p>`;
83
+ }
84
+
85
+ export function renderAccessibleTable(target: HTMLElement, result: LoopResult, ui: GameDeltaTimeFixedTimestepLabUI): void {
86
+ const rows = samplePoints(result.samples).slice(-8).map((sample) => `<tr><td>${sample.frame}</td><td>${number(sample.wallTime)} ${escapeHtml(ui.unitSeconds)}</td><td>${number(sample.variablePosition)} ${escapeHtml(ui.unitPixels)}</td><td>${number(sample.fixedPosition)} ${escapeHtml(ui.unitPixels)}</td><td>${number(sample.variablePosition - sample.fixedPosition)} ${escapeHtml(ui.unitPixels)}</td></tr>`).join('');
87
+ target.innerHTML = `<h3>${escapeHtml(ui.tableTitle)}</h3><table><thead><tr><th>${escapeHtml(ui.tableFrame)}</th><th>${escapeHtml(ui.tableWall)}</th><th>${escapeHtml(ui.tableVariable)}</th><th>${escapeHtml(ui.tableFixed)}</th><th>${escapeHtml(ui.tableDelta)}</th></tr></thead><tbody>${rows}</tbody></table>`;
88
+ }
@@ -0,0 +1,27 @@
1
+ import type { GamesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { GameDeltaTimeFixedTimestepLabUI } from './ui';
3
+
4
+ export type { GameDeltaTimeFixedTimestepLabUI };
5
+ export type GameDeltaTimeFixedTimestepLabLocaleContent = ToolLocaleContent<GameDeltaTimeFixedTimestepLabUI>;
6
+
7
+ export const gameDeltaTimeFixedTimestepLab: GamesToolEntry<GameDeltaTimeFixedTimestepLabUI> = {
8
+ id: 'game-delta-time-fixed-timestep-lab',
9
+ icons: { bg: 'mdi:timer-sand', fg: 'mdi:gamepad-variant' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((module) => module.content),
12
+ en: () => import('./i18n/en').then((module) => module.content),
13
+ es: () => import('./i18n/es').then((module) => module.content),
14
+ fr: () => import('./i18n/fr').then((module) => module.content),
15
+ id: () => import('./i18n/id').then((module) => module.content),
16
+ it: () => import('./i18n/it').then((module) => module.content),
17
+ ja: () => import('./i18n/ja').then((module) => module.content),
18
+ ko: () => import('./i18n/ko').then((module) => module.content),
19
+ nl: () => import('./i18n/nl').then((module) => module.content),
20
+ pl: () => import('./i18n/pl').then((module) => module.content),
21
+ pt: () => import('./i18n/pt').then((module) => module.content),
22
+ ru: () => import('./i18n/ru').then((module) => module.content),
23
+ sv: () => import('./i18n/sv').then((module) => module.content),
24
+ tr: () => import('./i18n/tr').then((module) => module.content),
25
+ zh: () => import('./i18n/zh').then((module) => module.content),
26
+ },
27
+ };