@jjlmoya/utils-games-development 1.64.0 → 1.66.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/bibliography_wellformed_export.test.ts +46 -0
- 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/seo_wellformed_export.test.ts +65 -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/bibliography.astro +3 -21
- 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/damageFormulaLab/seo.astro +7 -5
- package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +6 -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/hitboxHurtboxAnimator/seo.astro +7 -5
- 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
|
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Анализируйте урон и количество ударов одновременно',
|
|
173
173
|
html: 'Небольшое изменение урона может пересечь пороговое значение здоровья и сократить число ударов. Всегда сравнивайте урон с TTK.',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'Одна и та же формула может дать другой бой из-за округления и порядка модификаторов. Проверьте также малый урон, высокую защиту и порог дополнительного удара; TTK не моделирует уклонения, перезарядки и прерывания.' },
|
|
176
|
+
{ type: 'paragraph', html: 'После выбора формулы сохраните несколько контрольных сценариев: слабая атака против высокой защиты, критический удар и граница, на которой меняется число попаданий. Такие примеры помогают сравнить версии баланса и заметить регрессию после изменения коэффициентов или правил округления.' },
|
|
175
177
|
],
|
|
176
178
|
faq,
|
|
177
179
|
bibliographyTitle: 'Ссылки по расчету урона в играх',
|
|
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Granska skada och antal träffar tillsammans',
|
|
173
173
|
html: 'En liten förändring i skada kan passera en hälsotröskel och minska antal träffar med en hel attack. Jämför alltid skada med TTK.',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'Samma formel kan ge ett annat stridsresultat beroende på avrundning och modifierarnas ordning. Kontrollera även låg skada, högt försvar och tröskeln för en extra träff; TTK modellerar inte undvikanden, cooldowns eller avbrott.' },
|
|
176
|
+
{ type: 'paragraph', html: 'Spara flera kontrollscenarier efter varje ändring: svag attack mot högt försvar, kritisk träff och gränsen där antalet träffar ändras. Dessa exempel gör det lättare att jämföra balansversioner och upptäcka regressioner efter att koefficienter eller avrundningsregler har ändrats.' },
|
|
175
177
|
],
|
|
176
178
|
faq,
|
|
177
179
|
bibliographyTitle: 'Referenser för skadeberäkning i spel',
|
|
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Hasar ve vuruş sayısını daima birlikte inceleyin',
|
|
173
173
|
html: 'Hasardaki küçük bir değişim bir can eşiğini geçerek vuruş sayısını 1 azaltabilir. Hasarı daima TTK ile karşılaştırın.',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'Aynı formül yuvarlama ve modifier sırasına göre farklı bir savaş sonucu üretebilir. Küçük hasarı, yüksek savunmayı ve ek vuruş gerektiren eşiği de kontrol edin; TTK kaçınma, bekleme süresi veya kesintileri modellemez.' },
|
|
176
|
+
{ type: 'paragraph', html: 'Her değişiklikten sonra birkaç kontrol senaryosu kaydedin: yüksek savunmaya karşı zayıf saldırı, kritik vuruş ve vuruş sayısının değiştiği eşik. Bu örnekler denge sürümlerini karşılaştırmayı ve katsayılar ya da yuvarlama kuralları değiştiğinde oluşan gerilemeleri fark etmeyi kolaylaştırır.' },
|
|
175
177
|
],
|
|
176
178
|
faq,
|
|
177
179
|
bibliographyTitle: 'Oyunlarda hasar matematiği referansları',
|
|
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: '同时观察伤害数值与击杀次数',
|
|
173
173
|
html: '伤害数值的微小变化可能会跨越血量阈值,从而使所需攻击次数减少一次。请务必结合 TTK 进行综合评估。',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: '同一公式会因舍入方式和修正顺序不同而产生不同的战斗结果。还要检查低伤害、高防御以及需要额外攻击的临界点;TTK 不模拟闪避、冷却或中断。' },
|
|
176
|
+
{ type: 'paragraph', html: '每次修改后请保存几组基准场景,包括低攻击对高防御、暴击命中,以及攻击次数发生变化的临界值。这些案例有助于比较不同平衡版本,也能在系数或舍入规则调整后及时发现回归问题。' },
|
|
175
177
|
],
|
|
176
178
|
faq,
|
|
177
179
|
bibliographyTitle: '游戏伤害数值设计参考资料',
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
-
import
|
|
3
|
+
import { damageFormulaLab } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
|
-
locale?:
|
|
7
|
-
sections?: SEOSection[];
|
|
7
|
+
locale?: KnownLocale;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const { locale = '
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const loader = damageFormulaLab.i18n[locale as KnownLocale] ?? damageFormulaLab.i18n.es ?? damageFormulaLab.i18n.en;
|
|
12
|
+
const content = loader ? await loader() : null;
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
<SEORenderer content={{ locale, sections }} />
|
|
15
|
+
{content?.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
+
}
|