@jjlmoya/utils-games-development 1.64.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 (161) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/seo_parity.test.ts +2 -2
  6. package/src/tests/seo_translation_completeness.test.ts +69 -0
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tests/translation_copy.test.ts +21 -18
  9. package/src/tool/audioLoopPointFinder/i18n/de.ts +13 -0
  10. package/src/tool/audioLoopPointFinder/i18n/fr.ts +26 -0
  11. package/src/tool/audioLoopPointFinder/i18n/id.ts +4 -0
  12. package/src/tool/audioLoopPointFinder/i18n/it.ts +3 -0
  13. package/src/tool/audioLoopPointFinder/i18n/nl.ts +4 -0
  14. package/src/tool/audioLoopPointFinder/i18n/pl.ts +4 -0
  15. package/src/tool/audioLoopPointFinder/i18n/pt.ts +3 -0
  16. package/src/tool/audioLoopPointFinder/i18n/ru.ts +4 -0
  17. package/src/tool/audioLoopPointFinder/i18n/sv.ts +4 -0
  18. package/src/tool/audioLoopPointFinder/i18n/tr.ts +4 -0
  19. package/src/tool/audioLoopPointFinder/i18n/zh.ts +4 -0
  20. package/src/tool/damageFormulaLab/i18n/de.ts +13 -0
  21. package/src/tool/damageFormulaLab/i18n/fr.ts +13 -0
  22. package/src/tool/damageFormulaLab/i18n/id.ts +13 -0
  23. package/src/tool/damageFormulaLab/i18n/it.ts +1 -0
  24. package/src/tool/damageFormulaLab/i18n/ja.ts +1 -0
  25. package/src/tool/damageFormulaLab/i18n/ko.ts +1 -0
  26. package/src/tool/damageFormulaLab/i18n/nl.ts +1 -0
  27. package/src/tool/damageFormulaLab/i18n/pl.ts +1 -0
  28. package/src/tool/damageFormulaLab/i18n/pt.ts +1 -0
  29. package/src/tool/damageFormulaLab/i18n/ru.ts +2 -0
  30. package/src/tool/damageFormulaLab/i18n/sv.ts +2 -0
  31. package/src/tool/damageFormulaLab/i18n/tr.ts +2 -0
  32. package/src/tool/damageFormulaLab/i18n/zh.ts +2 -0
  33. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +22 -0
  34. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.ts +10 -0
  35. package/src/tool/gameDeltaTimeFixedTimestepLab/component.astro +51 -0
  36. package/src/tool/gameDeltaTimeFixedTimestepLab/controller.ts +79 -0
  37. package/src/tool/gameDeltaTimeFixedTimestepLab/dom-views.ts +88 -0
  38. package/src/tool/gameDeltaTimeFixedTimestepLab/entry.ts +27 -0
  39. package/src/tool/gameDeltaTimeFixedTimestepLab/evaluator.ts +21 -0
  40. package/src/tool/gameDeltaTimeFixedTimestepLab/game-delta-time-fixed-timestep-lab.css +491 -0
  41. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/de.ts +56 -0
  42. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/en.ts +180 -0
  43. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/es.ts +56 -0
  44. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/fr.ts +46 -0
  45. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/id.ts +46 -0
  46. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/it.ts +46 -0
  47. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ja.ts +46 -0
  48. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ko.ts +46 -0
  49. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/nl.ts +46 -0
  50. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pl.ts +46 -0
  51. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pt.ts +46 -0
  52. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ru.ts +46 -0
  53. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/sv.ts +46 -0
  54. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/tr.ts +46 -0
  55. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/zh.ts +46 -0
  56. package/src/tool/gameDeltaTimeFixedTimestepLab/index.ts +11 -0
  57. package/src/tool/gameDeltaTimeFixedTimestepLab/logic.test.ts +35 -0
  58. package/src/tool/gameDeltaTimeFixedTimestepLab/logic.ts +140 -0
  59. package/src/tool/gameDeltaTimeFixedTimestepLab/seo.astro +15 -0
  60. package/src/tool/gameDeltaTimeFixedTimestepLab/seo_contract.test.ts +25 -0
  61. package/src/tool/gameDeltaTimeFixedTimestepLab/storage.ts +23 -0
  62. package/src/tool/gameDeltaTimeFixedTimestepLab/ui.ts +54 -0
  63. package/src/tool/gameUIAccessibilityTester/i18n/de.ts +13 -0
  64. package/src/tool/gameUIAccessibilityTester/i18n/fr.ts +13 -0
  65. package/src/tool/gameUIAccessibilityTester/i18n/id.ts +13 -0
  66. package/src/tool/gameUIAccessibilityTester/i18n/it.ts +1 -0
  67. package/src/tool/gameUIAccessibilityTester/i18n/nl.ts +5 -0
  68. package/src/tool/gameUIAccessibilityTester/i18n/pl.ts +5 -0
  69. package/src/tool/gameUIAccessibilityTester/i18n/pt.ts +2 -0
  70. package/src/tool/gameUIAccessibilityTester/i18n/ru.ts +1 -0
  71. package/src/tool/gameUIAccessibilityTester/i18n/sv.ts +1 -0
  72. package/src/tool/gameUIAccessibilityTester/i18n/tr.ts +2 -0
  73. package/src/tool/gameUIAccessibilityTester/i18n/zh.ts +1 -0
  74. package/src/tool/hitboxHurtboxAnimator/i18n/de.ts +14 -0
  75. package/src/tool/hitboxHurtboxAnimator/i18n/fr.ts +13 -0
  76. package/src/tool/hitboxHurtboxAnimator/i18n/id.ts +13 -0
  77. package/src/tool/hitboxHurtboxAnimator/i18n/it.ts +1 -0
  78. package/src/tool/hitboxHurtboxAnimator/i18n/nl.ts +1 -0
  79. package/src/tool/hitboxHurtboxAnimator/i18n/pl.ts +2 -0
  80. package/src/tool/hitboxHurtboxAnimator/i18n/pt.ts +2 -0
  81. package/src/tool/hitboxHurtboxAnimator/i18n/ru.ts +2 -0
  82. package/src/tool/hitboxHurtboxAnimator/i18n/sv.ts +1 -0
  83. package/src/tool/hitboxHurtboxAnimator/i18n/tr.ts +2 -0
  84. package/src/tool/itchioGameTester/i18n/de.ts +13 -0
  85. package/src/tool/itchioGameTester/i18n/es.ts +2 -2
  86. package/src/tool/itchioGameTester/i18n/fr.ts +26 -0
  87. package/src/tool/itchioGameTester/i18n/id.ts +3 -0
  88. package/src/tool/itchioGameTester/i18n/it.ts +3 -0
  89. package/src/tool/itchioGameTester/i18n/ja.ts +3 -0
  90. package/src/tool/itchioGameTester/i18n/ko.ts +3 -0
  91. package/src/tool/itchioGameTester/i18n/nl.ts +3 -0
  92. package/src/tool/itchioGameTester/i18n/pl.ts +3 -0
  93. package/src/tool/itchioGameTester/i18n/pt.ts +3 -0
  94. package/src/tool/itchioGameTester/i18n/ru.ts +3 -0
  95. package/src/tool/itchioGameTester/i18n/sv.ts +3 -0
  96. package/src/tool/itchioGameTester/i18n/tr.ts +3 -0
  97. package/src/tool/itchioGameTester/i18n/zh.ts +3 -0
  98. package/src/tool/localizationSanitizer/i18n/de.ts +13 -0
  99. package/src/tool/localizationSanitizer/i18n/es.ts +26 -0
  100. package/src/tool/localizationSanitizer/i18n/fr.ts +4 -0
  101. package/src/tool/localizationSanitizer/i18n/id.ts +4 -0
  102. package/src/tool/localizationSanitizer/i18n/it.ts +4 -0
  103. package/src/tool/localizationSanitizer/i18n/ja.ts +4 -0
  104. package/src/tool/localizationSanitizer/i18n/ko.ts +4 -0
  105. package/src/tool/localizationSanitizer/i18n/nl.ts +5 -0
  106. package/src/tool/localizationSanitizer/i18n/pl.ts +5 -0
  107. package/src/tool/localizationSanitizer/i18n/pt.ts +5 -0
  108. package/src/tool/localizationSanitizer/i18n/ru.ts +5 -0
  109. package/src/tool/localizationSanitizer/i18n/sv.ts +5 -0
  110. package/src/tool/localizationSanitizer/i18n/tr.ts +5 -0
  111. package/src/tool/localizationSanitizer/i18n/zh.ts +6 -0
  112. package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +13 -0
  113. package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +13 -0
  114. package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +18 -0
  115. package/src/tool/retroSfxGenerator/i18n/de.ts +13 -0
  116. package/src/tool/retroSfxGenerator/i18n/fr.ts +13 -0
  117. package/src/tool/retroSfxGenerator/i18n/id.ts +13 -0
  118. package/src/tool/retroSfxGenerator/i18n/it.ts +1 -0
  119. package/src/tool/retroSfxGenerator/i18n/nl.ts +1 -0
  120. package/src/tool/retroSfxGenerator/i18n/pl.ts +1 -0
  121. package/src/tool/retroSfxGenerator/i18n/pt.ts +1 -0
  122. package/src/tool/retroSfxGenerator/i18n/ru.ts +1 -0
  123. package/src/tool/retroSfxGenerator/i18n/sv.ts +2 -0
  124. package/src/tool/retroSfxGenerator/i18n/tr.ts +1 -0
  125. package/src/tool/retroSfxGenerator/i18n/zh.ts +1 -0
  126. package/src/tool/spriteSheetPacker/i18n/de.ts +13 -0
  127. package/src/tool/spriteSheetPacker/i18n/es.ts +2 -2
  128. package/src/tool/spriteSheetPacker/i18n/fr.ts +26 -0
  129. package/src/tool/spriteSheetPacker/i18n/id.ts +4 -0
  130. package/src/tool/spriteSheetPacker/i18n/it.ts +4 -0
  131. package/src/tool/spriteSheetPacker/i18n/ja.ts +3 -0
  132. package/src/tool/spriteSheetPacker/i18n/ko.ts +3 -0
  133. package/src/tool/spriteSheetPacker/i18n/nl.ts +4 -0
  134. package/src/tool/spriteSheetPacker/i18n/pl.ts +4 -0
  135. package/src/tool/spriteSheetPacker/i18n/pt.ts +4 -0
  136. package/src/tool/spriteSheetPacker/i18n/ru.ts +4 -0
  137. package/src/tool/spriteSheetPacker/i18n/sv.ts +4 -0
  138. package/src/tool/spriteSheetPacker/i18n/tr.ts +4 -0
  139. package/src/tool/spriteSheetPacker/i18n/zh.ts +4 -0
  140. package/src/tool/steamBbcodeTranslator/i18n/de.ts +22 -0
  141. package/src/tool/steamBbcodeTranslator/i18n/es.ts +23 -1
  142. package/src/tool/steamBbcodeTranslator/i18n/fr.ts +6 -1
  143. package/src/tool/steamBbcodeTranslator/i18n/id.ts +7 -1
  144. package/src/tool/steamBbcodeTranslator/i18n/it.ts +6 -1
  145. package/src/tool/steamBbcodeTranslator/i18n/ja.ts +5 -1
  146. package/src/tool/steamBbcodeTranslator/i18n/ko.ts +5 -1
  147. package/src/tool/steamBbcodeTranslator/i18n/nl.ts +7 -1
  148. package/src/tool/steamBbcodeTranslator/i18n/pl.ts +7 -1
  149. package/src/tool/steamBbcodeTranslator/i18n/pt.ts +7 -1
  150. package/src/tool/steamBbcodeTranslator/i18n/ru.ts +7 -1
  151. package/src/tool/steamBbcodeTranslator/i18n/sv.ts +7 -1
  152. package/src/tool/steamBbcodeTranslator/i18n/tr.ts +7 -1
  153. package/src/tool/steamBbcodeTranslator/i18n/zh.ts +5 -1
  154. package/src/tool/steamCapsuleGenerator/i18n/de.ts +2 -2
  155. package/src/tool/steamCapsuleGenerator/i18n/es.ts +2 -2
  156. package/src/tool/steamCapsuleGenerator/i18n/fr.ts +2 -2
  157. package/src/tool/steamCapsuleGenerator/i18n/pl.ts +13 -0
  158. package/src/tool/steamCapsuleGenerator/i18n/ru.ts +26 -0
  159. package/src/tool/steamCapsuleGenerator/i18n/sv.ts +1 -0
  160. package/src/tool/steamCapsuleGenerator/i18n/tr.ts +1 -0
  161. package/src/tools.ts +2 -0
@@ -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
+ };
@@ -0,0 +1,21 @@
1
+ import type { LoopResult } from './logic';
2
+
3
+ export type DiagnosticKey = 'stable' | 'variable' | 'fixed' | 'clamped';
4
+
5
+ export interface Diagnostic {
6
+ key: DiagnosticKey;
7
+ severity: 'calm' | 'notice' | 'warning';
8
+ }
9
+
10
+ export function evaluateLoop(result: LoopResult): Diagnostic {
11
+ if (result.config.clampEnabled && Math.abs(result.divergence) > 0.01) {
12
+ return { key: 'clamped', severity: 'warning' };
13
+ }
14
+ if (Math.abs(result.divergence) > 0.01 && result.variablePosition > result.fixedPosition) {
15
+ return { key: 'variable', severity: 'warning' };
16
+ }
17
+ if (result.maxCatchUp > 1) {
18
+ return { key: 'fixed', severity: 'notice' };
19
+ }
20
+ return { key: 'stable', severity: 'calm' };
21
+ }