@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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/seo_parity.test.ts +2 -2
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tests/translation_copy.test.ts +21 -18
- package/src/tool/audioLoopPointFinder/i18n/de.ts +13 -0
- package/src/tool/audioLoopPointFinder/i18n/fr.ts +26 -0
- package/src/tool/audioLoopPointFinder/i18n/id.ts +4 -0
- package/src/tool/audioLoopPointFinder/i18n/it.ts +3 -0
- package/src/tool/audioLoopPointFinder/i18n/nl.ts +4 -0
- package/src/tool/audioLoopPointFinder/i18n/pl.ts +4 -0
- package/src/tool/audioLoopPointFinder/i18n/pt.ts +3 -0
- package/src/tool/audioLoopPointFinder/i18n/ru.ts +4 -0
- package/src/tool/audioLoopPointFinder/i18n/sv.ts +4 -0
- package/src/tool/audioLoopPointFinder/i18n/tr.ts +4 -0
- package/src/tool/audioLoopPointFinder/i18n/zh.ts +4 -0
- package/src/tool/damageFormulaLab/i18n/de.ts +13 -0
- package/src/tool/damageFormulaLab/i18n/fr.ts +13 -0
- package/src/tool/damageFormulaLab/i18n/id.ts +13 -0
- package/src/tool/damageFormulaLab/i18n/it.ts +1 -0
- package/src/tool/damageFormulaLab/i18n/ja.ts +1 -0
- package/src/tool/damageFormulaLab/i18n/ko.ts +1 -0
- package/src/tool/damageFormulaLab/i18n/nl.ts +1 -0
- package/src/tool/damageFormulaLab/i18n/pl.ts +1 -0
- package/src/tool/damageFormulaLab/i18n/pt.ts +1 -0
- package/src/tool/damageFormulaLab/i18n/ru.ts +2 -0
- package/src/tool/damageFormulaLab/i18n/sv.ts +2 -0
- package/src/tool/damageFormulaLab/i18n/tr.ts +2 -0
- package/src/tool/damageFormulaLab/i18n/zh.ts +2 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +22 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.ts +10 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/component.astro +51 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/controller.ts +79 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/dom-views.ts +88 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/entry.ts +27 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/evaluator.ts +21 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/game-delta-time-fixed-timestep-lab.css +491 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/de.ts +56 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/en.ts +180 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/es.ts +56 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/fr.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/id.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/it.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ja.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ko.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/nl.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pl.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pt.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ru.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/sv.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/tr.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/zh.ts +46 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/index.ts +11 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/logic.test.ts +35 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/logic.ts +140 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/seo.astro +15 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/seo_contract.test.ts +25 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/storage.ts +23 -0
- package/src/tool/gameDeltaTimeFixedTimestepLab/ui.ts +54 -0
- package/src/tool/gameUIAccessibilityTester/i18n/de.ts +13 -0
- package/src/tool/gameUIAccessibilityTester/i18n/fr.ts +13 -0
- package/src/tool/gameUIAccessibilityTester/i18n/id.ts +13 -0
- package/src/tool/gameUIAccessibilityTester/i18n/it.ts +1 -0
- package/src/tool/gameUIAccessibilityTester/i18n/nl.ts +5 -0
- package/src/tool/gameUIAccessibilityTester/i18n/pl.ts +5 -0
- package/src/tool/gameUIAccessibilityTester/i18n/pt.ts +2 -0
- package/src/tool/gameUIAccessibilityTester/i18n/ru.ts +1 -0
- package/src/tool/gameUIAccessibilityTester/i18n/sv.ts +1 -0
- package/src/tool/gameUIAccessibilityTester/i18n/tr.ts +2 -0
- package/src/tool/gameUIAccessibilityTester/i18n/zh.ts +1 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/de.ts +14 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/fr.ts +13 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/id.ts +13 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/it.ts +1 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/nl.ts +1 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/pl.ts +2 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/pt.ts +2 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/ru.ts +2 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/sv.ts +1 -0
- package/src/tool/hitboxHurtboxAnimator/i18n/tr.ts +2 -0
- package/src/tool/itchioGameTester/i18n/de.ts +13 -0
- package/src/tool/itchioGameTester/i18n/es.ts +2 -2
- package/src/tool/itchioGameTester/i18n/fr.ts +26 -0
- package/src/tool/itchioGameTester/i18n/id.ts +3 -0
- package/src/tool/itchioGameTester/i18n/it.ts +3 -0
- package/src/tool/itchioGameTester/i18n/ja.ts +3 -0
- package/src/tool/itchioGameTester/i18n/ko.ts +3 -0
- package/src/tool/itchioGameTester/i18n/nl.ts +3 -0
- package/src/tool/itchioGameTester/i18n/pl.ts +3 -0
- package/src/tool/itchioGameTester/i18n/pt.ts +3 -0
- package/src/tool/itchioGameTester/i18n/ru.ts +3 -0
- package/src/tool/itchioGameTester/i18n/sv.ts +3 -0
- package/src/tool/itchioGameTester/i18n/tr.ts +3 -0
- package/src/tool/itchioGameTester/i18n/zh.ts +3 -0
- package/src/tool/localizationSanitizer/i18n/de.ts +13 -0
- package/src/tool/localizationSanitizer/i18n/es.ts +26 -0
- package/src/tool/localizationSanitizer/i18n/fr.ts +4 -0
- package/src/tool/localizationSanitizer/i18n/id.ts +4 -0
- package/src/tool/localizationSanitizer/i18n/it.ts +4 -0
- package/src/tool/localizationSanitizer/i18n/ja.ts +4 -0
- package/src/tool/localizationSanitizer/i18n/ko.ts +4 -0
- package/src/tool/localizationSanitizer/i18n/nl.ts +5 -0
- package/src/tool/localizationSanitizer/i18n/pl.ts +5 -0
- package/src/tool/localizationSanitizer/i18n/pt.ts +5 -0
- package/src/tool/localizationSanitizer/i18n/ru.ts +5 -0
- package/src/tool/localizationSanitizer/i18n/sv.ts +5 -0
- package/src/tool/localizationSanitizer/i18n/tr.ts +5 -0
- package/src/tool/localizationSanitizer/i18n/zh.ts +6 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +13 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +13 -0
- package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +18 -0
- package/src/tool/retroSfxGenerator/i18n/de.ts +13 -0
- package/src/tool/retroSfxGenerator/i18n/fr.ts +13 -0
- package/src/tool/retroSfxGenerator/i18n/id.ts +13 -0
- package/src/tool/retroSfxGenerator/i18n/it.ts +1 -0
- package/src/tool/retroSfxGenerator/i18n/nl.ts +1 -0
- package/src/tool/retroSfxGenerator/i18n/pl.ts +1 -0
- package/src/tool/retroSfxGenerator/i18n/pt.ts +1 -0
- package/src/tool/retroSfxGenerator/i18n/ru.ts +1 -0
- package/src/tool/retroSfxGenerator/i18n/sv.ts +2 -0
- package/src/tool/retroSfxGenerator/i18n/tr.ts +1 -0
- package/src/tool/retroSfxGenerator/i18n/zh.ts +1 -0
- package/src/tool/spriteSheetPacker/i18n/de.ts +13 -0
- package/src/tool/spriteSheetPacker/i18n/es.ts +2 -2
- package/src/tool/spriteSheetPacker/i18n/fr.ts +26 -0
- package/src/tool/spriteSheetPacker/i18n/id.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/it.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/ja.ts +3 -0
- package/src/tool/spriteSheetPacker/i18n/ko.ts +3 -0
- package/src/tool/spriteSheetPacker/i18n/nl.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/pl.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/pt.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/ru.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/sv.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/tr.ts +4 -0
- package/src/tool/spriteSheetPacker/i18n/zh.ts +4 -0
- package/src/tool/steamBbcodeTranslator/i18n/de.ts +22 -0
- package/src/tool/steamBbcodeTranslator/i18n/es.ts +23 -1
- package/src/tool/steamBbcodeTranslator/i18n/fr.ts +6 -1
- package/src/tool/steamBbcodeTranslator/i18n/id.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/it.ts +6 -1
- package/src/tool/steamBbcodeTranslator/i18n/ja.ts +5 -1
- package/src/tool/steamBbcodeTranslator/i18n/ko.ts +5 -1
- package/src/tool/steamBbcodeTranslator/i18n/nl.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/pl.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/pt.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/ru.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/sv.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/tr.ts +7 -1
- package/src/tool/steamBbcodeTranslator/i18n/zh.ts +5 -1
- package/src/tool/steamCapsuleGenerator/i18n/de.ts +2 -2
- package/src/tool/steamCapsuleGenerator/i18n/es.ts +2 -2
- package/src/tool/steamCapsuleGenerator/i18n/fr.ts +2 -2
- package/src/tool/steamCapsuleGenerator/i18n/pl.ts +13 -0
- package/src/tool/steamCapsuleGenerator/i18n/ru.ts +26 -0
- package/src/tool/steamCapsuleGenerator/i18n/sv.ts +1 -0
- package/src/tool/steamCapsuleGenerator/i18n/tr.ts +1 -0
- 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
+
}
|