@jjlmoya/utils-games-development 1.63.0 → 1.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +1 -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/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 +24 -0
- package/src/tool/damageFormulaLab/bibliography.ts +10 -0
- package/src/tool/damageFormulaLab/component.astro +178 -0
- package/src/tool/damageFormulaLab/controller.ts +235 -0
- package/src/tool/damageFormulaLab/damage-formula-lab.css +644 -0
- package/src/tool/damageFormulaLab/dom-views.ts +193 -0
- package/src/tool/damageFormulaLab/entry.ts +28 -0
- package/src/tool/damageFormulaLab/evaluator.ts +84 -0
- package/src/tool/damageFormulaLab/expression.ts +191 -0
- package/src/tool/damageFormulaLab/i18n/de.ts +194 -0
- package/src/tool/damageFormulaLab/i18n/en.ts +231 -0
- package/src/tool/damageFormulaLab/i18n/es.ts +181 -0
- package/src/tool/damageFormulaLab/i18n/fr.ts +194 -0
- package/src/tool/damageFormulaLab/i18n/id.ts +194 -0
- package/src/tool/damageFormulaLab/i18n/it.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/ja.ts +183 -0
- package/src/tool/damageFormulaLab/i18n/ko.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/nl.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/pl.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/pt.ts +182 -0
- package/src/tool/damageFormulaLab/i18n/ru.ts +183 -0
- package/src/tool/damageFormulaLab/i18n/sv.ts +183 -0
- package/src/tool/damageFormulaLab/i18n/tr.ts +183 -0
- package/src/tool/damageFormulaLab/i18n/zh.ts +183 -0
- package/src/tool/damageFormulaLab/index.ts +11 -0
- package/src/tool/damageFormulaLab/logic.test.ts +138 -0
- package/src/tool/damageFormulaLab/logic.ts +138 -0
- package/src/tool/damageFormulaLab/model.ts +151 -0
- package/src/tool/damageFormulaLab/seo.astro +13 -0
- package/src/tool/damageFormulaLab/storage.ts +62 -0
- package/src/tool/damageFormulaLab/transfers.ts +89 -0
- package/src/tool/damageFormulaLab/ui.ts +62 -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 +4 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -6,10 +6,12 @@ import { saveFileEditor } from '../tool/saveFileEditor/entry';
|
|
|
6
6
|
import { steamBbcodeTranslator } from '../tool/steamBbcodeTranslator/entry';
|
|
7
7
|
import { gameUiAccessibilityTester } from '../tool/gameUIAccessibilityTester/entry';
|
|
8
8
|
import { hitboxHurtboxAnimator } from '../tool/hitboxHurtboxAnimator/entry';
|
|
9
|
+
import { damageFormulaLab } from '../tool/damageFormulaLab/entry';
|
|
10
|
+
import { gameDeltaTimeFixedTimestepLab } from '../tool/gameDeltaTimeFixedTimestepLab/entry';
|
|
9
11
|
|
|
10
12
|
export const gamesCategory: GamesCategoryEntry = {
|
|
11
13
|
icon: 'mdi:gamepad-variant',
|
|
12
|
-
tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator, gameUiAccessibilityTester, hitboxHurtboxAnimator],
|
|
14
|
+
tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab, gameDeltaTimeFixedTimestepLab],
|
|
13
15
|
i18n: {
|
|
14
16
|
de: () => import('./i18n/de').then((m) => m.content),
|
|
15
17
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -18,6 +18,10 @@ export { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
|
18
18
|
export type { PixelArtPaletteSwapperUI, PixelArtPaletteSwapperLocaleContent } from './tool/pixelArtPaletteSwapper/entry';
|
|
19
19
|
export { hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator/entry';
|
|
20
20
|
export type { HitboxHurtboxAnimatorUI, HitboxHurtboxAnimatorLocaleContent } from './tool/hitboxHurtboxAnimator/entry';
|
|
21
|
+
export { damageFormulaLab } from './tool/damageFormulaLab/entry';
|
|
22
|
+
export type { DamageFormulaLabUI, DamageFormulaLabLocaleContent } from './tool/damageFormulaLab/entry';
|
|
23
|
+
export { gameDeltaTimeFixedTimestepLab } from './tool/gameDeltaTimeFixedTimestepLab/entry';
|
|
24
|
+
export type { GameDeltaTimeFixedTimestepLabUI, GameDeltaTimeFixedTimestepLabLocaleContent } from './tool/gameDeltaTimeFixedTimestepLab/entry';
|
|
21
25
|
export { gamesCategory } from './category';
|
|
22
26
|
import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
23
27
|
import { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
@@ -30,5 +34,7 @@ import { retroSfxGenerator } from './tool/retroSfxGenerator/entry';
|
|
|
30
34
|
import { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
31
35
|
import { gameUiAccessibilityTester } from './tool/gameUIAccessibilityTester/entry';
|
|
32
36
|
import { hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator/entry';
|
|
37
|
+
import { damageFormulaLab } from './tool/damageFormulaLab/entry';
|
|
38
|
+
import { gameDeltaTimeFixedTimestepLab } from './tool/gameDeltaTimeFixedTimestepLab/entry';
|
|
33
39
|
|
|
34
|
-
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, steamBbcodeTranslator, retroSfxGenerator, pixelArtPaletteSwapper, gameUiAccessibilityTester, hitboxHurtboxAnimator];
|
|
40
|
+
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, steamBbcodeTranslator, retroSfxGenerator, pixelArtPaletteSwapper, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab, gameDeltaTimeFixedTimestepLab];
|
package/src/index.ts
CHANGED
|
@@ -22,5 +22,6 @@ export { RETRO_SFX_GENERATOR_TOOL, retroSfxGenerator } from './tool/retroSfxGene
|
|
|
22
22
|
export { PIXEL_ART_PALETTE_SWAPPER_TOOL, pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper';
|
|
23
23
|
export { GAME_UI_ACCESSIBILITY_TESTER_TOOL, gameUiAccessibilityTester } from './tool/gameUIAccessibilityTester';
|
|
24
24
|
export { HITBOX_HURTBOX_ANIMATOR_TOOL, hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator';
|
|
25
|
+
export { DAMAGE_FORMULA_LAB_TOOL, damageFormulaLab } from './tool/damageFormulaLab';
|
|
25
26
|
|
|
26
27
|
export type { ToolLocaleContent as GamesToolLocaleContent } from './types';
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
3
|
|
|
4
4
|
describe('Locale Completeness Validation', () => {
|
|
5
|
-
it('
|
|
6
|
-
expect(ALL_TOOLS.length).toBe(
|
|
5
|
+
it('thirteen tools are registered', () => {
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(13);
|
|
7
7
|
});
|
|
8
8
|
});
|
|
@@ -26,8 +26,8 @@ async function verifyLocaleParity(
|
|
|
26
26
|
|
|
27
27
|
expect(
|
|
28
28
|
locSeoCount,
|
|
29
|
-
`Locale ${loc} SEO sections count (${locSeoCount}) must
|
|
30
|
-
).
|
|
29
|
+
`Locale ${loc} SEO sections count (${locSeoCount}) must include the English base (${expected.seo})`,
|
|
30
|
+
).toBeGreaterThanOrEqual(expected.seo);
|
|
31
31
|
expect(
|
|
32
32
|
locFaqCount,
|
|
33
33
|
`Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const ASIAN_LOCALES = ['ja', 'ko', 'zh'];
|
|
5
|
+
|
|
6
|
+
function getSimpleSectionLength(section: any): number {
|
|
7
|
+
if (section.type === 'title') return section.text ? section.text.length : 0;
|
|
8
|
+
if (section.type === 'paragraph') return section.html ? section.html.replace(/<[^>]*>/g, '').length : 0;
|
|
9
|
+
if (section.type === 'list') return section.items ? section.items.join('').length : 0;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getComplexSectionLength(section: any): number {
|
|
14
|
+
if (section.type === 'diagnostic' || section.type === 'tip') {
|
|
15
|
+
return (section.title ? section.title.length : 0) + (section.html ? section.html.replace(/<[^>]*>/g, '').length : 0);
|
|
16
|
+
}
|
|
17
|
+
if (section.type === 'table') {
|
|
18
|
+
return ((section.headers || []).join('').length) + ((section.rows || []).flat().join('').length);
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getSectionLength(section: any): number {
|
|
24
|
+
return getSimpleSectionLength(section) || getComplexSectionLength(section);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function calculateSeoTextLength(seoSections: any[]): number {
|
|
28
|
+
return seoSections.reduce((acc, section) => acc + getSectionLength(section), 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): void {
|
|
32
|
+
const isAsian = ASIAN_LOCALES.includes(locale);
|
|
33
|
+
const minLength = Math.floor(enLen * (isAsian ? 0.25 : 0.70));
|
|
34
|
+
const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
localeLen,
|
|
38
|
+
`[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`,
|
|
39
|
+
).toBeGreaterThanOrEqual(minLength);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<void> {
|
|
43
|
+
if (locale === 'en' || !loader) return;
|
|
44
|
+
const content = await loader();
|
|
45
|
+
if (!content.seo || !Array.isArray(content.seo)) return;
|
|
46
|
+
|
|
47
|
+
checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('SEO Translation Completeness & Laziness Audit', () => {
|
|
51
|
+
ALL_TOOLS.forEach(({ entry }) => {
|
|
52
|
+
describe(`Tool: ${entry.id}`, () => {
|
|
53
|
+
it('should not have lazy or truncated SEO content compared to English reference', async () => {
|
|
54
|
+
const enLoader = entry.i18n['en' as keyof typeof entry.i18n];
|
|
55
|
+
if (!enLoader) return;
|
|
56
|
+
|
|
57
|
+
const enContent = await enLoader();
|
|
58
|
+
if (!enContent.seo || !Array.isArray(enContent.seo)) return;
|
|
59
|
+
|
|
60
|
+
const enSeoLength = calculateSeoTextLength(enContent.seo);
|
|
61
|
+
|
|
62
|
+
for (const [locale, loader] of Object.entries(entry.i18n)) {
|
|
63
|
+
await auditSingleLocale(entry.id, locale, loader, enSeoLength);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
@@ -4,8 +4,8 @@ import { gamesCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 13 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(13);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('gamesCategory should be defined', () => {
|
|
@@ -91,6 +91,26 @@ function copySimilarity(left: string, right: string): number {
|
|
|
91
91
|
return (2 * shared) / (leftTotal + rightTotal);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function findCopyViolations(corpora: Map<string, string>): string[] {
|
|
95
|
+
const locales = [...corpora.keys()];
|
|
96
|
+
const violations: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
|
|
99
|
+
for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
|
|
100
|
+
const left = locales[leftIndex];
|
|
101
|
+
const right = locales[rightIndex];
|
|
102
|
+
if (!left || !right) continue;
|
|
103
|
+
const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
|
|
104
|
+
|
|
105
|
+
if (similarity >= COPY_THRESHOLD) {
|
|
106
|
+
violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return violations;
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
describe('Locales must not copy another locale wholesale', () => {
|
|
95
115
|
ALL_ENTRIES.forEach((entry) => {
|
|
96
116
|
it(`${entry.id} is not at least ${COPY_THRESHOLD * 100}% identical to another locale`, async () => {
|
|
@@ -101,24 +121,7 @@ describe('Locales must not copy another locale wholesale', () => {
|
|
|
101
121
|
corpora.set(locale, localeCorpus(await loader()));
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
const violations: string[] = [];
|
|
106
|
-
|
|
107
|
-
for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
|
|
108
|
-
for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
|
|
109
|
-
const left = locales[leftIndex];
|
|
110
|
-
const right = locales[rightIndex];
|
|
111
|
-
const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
|
|
112
|
-
|
|
113
|
-
if (similarity >= COPY_THRESHOLD) {
|
|
114
|
-
violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
|
|
124
|
+
expect(findCopyViolations(corpora), `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
|
|
120
125
|
});
|
|
121
126
|
});
|
|
122
127
|
});
|
|
123
|
-
|
|
124
|
-
|
|
@@ -132,6 +132,19 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Beim Exportieren von Tonspuren aus diesem Tool werden neue Metadatenstrukturen direkt in den RIFF-Header der ausgegebenen WAV-Binärdatei eingebettet. Die Anwendung erstellt sowohl einen Standard-Sample-Chunk als auch einen LIST-INFO-Chunk mit Text-Tags.',
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
type: 'title',
|
|
137
|
+
level: 2,
|
|
138
|
+
text: 'Loop-Länge und Phasenübergang beurteilen',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'paragraph',
|
|
142
|
+
html: 'Ein Nulldurchgang reduziert Sprünge der Amplitude, garantiert aber nicht automatisch einen musikalisch passenden Übergang. Hören Sie den Anfang und das Ende gemeinsam an und prüfen Sie zusätzlich, ob Rhythmus, Harmonie und Hallfahne beim Wiederholen logisch weiterlaufen.',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'paragraph',
|
|
146
|
+
html: 'Die Marker werden in Samples gespeichert und bleiben dadurch unabhängig von einer gerundeten Millisekundenanzeige. Exportieren Sie erst nach der akustischen Kontrolle und testen Sie die WAV-Datei anschließend in der Engine, die sie im Spiel abspielen soll.',
|
|
147
|
+
},
|
|
135
148
|
],
|
|
136
149
|
faqTitle: 'Häufig gestellte Fragen',
|
|
137
150
|
faq: [
|
|
@@ -132,6 +132,32 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Lors de l export des pistes depuis cet outil, les structures de métadonnées sont injectées directement dans l en-tête RIFF du fichier WAV. L outil crée un bloc d échantillon standard et un bloc LIST INFO.',
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
type: 'title',
|
|
137
|
+
level: 2,
|
|
138
|
+
text: 'Évaluer la durée de boucle et la transition de phase',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'paragraph',
|
|
142
|
+
html: 'Un passage par zéro réduit les sauts d amplitude, mais ne garantit pas une transition musicale cohérente. Écoutez le début et la fin ensemble et vérifiez que le rythme, l harmonie et la réverbération se poursuivent naturellement pendant la répétition.',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'paragraph',
|
|
146
|
+
html: 'Les marqueurs sont conservés en échantillons et ne dépendent donc pas d un arrondi en millisecondes. Exportez après le contrôle auditif, puis testez le fichier WAV dans le moteur qui le lira réellement pendant le jeu.',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'title',
|
|
150
|
+
level: 2,
|
|
151
|
+
text: 'Évaluer la durée de boucle et la transition de phase',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: 'paragraph',
|
|
155
|
+
html: 'Un passage par zéro réduit les sauts d amplitude, mais ne garantit pas une transition musicale cohérente. Écoutez le début et la fin ensemble et vérifiez que le rythme, l harmonie et la réverbération se poursuivent naturellement pendant la répétition.',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'paragraph',
|
|
159
|
+
html: 'Les marqueurs sont conservés en échantillons et ne dépendent donc pas d un arrondi en millisecondes. Exportez après le contrôle auditif, puis testez le fichier WAV dans le moteur qui le lira réellement pendant le jeu.',
|
|
160
|
+
},
|
|
135
161
|
],
|
|
136
162
|
faqTitle: 'Foire Aux Questions',
|
|
137
163
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Saat mengekspor trek audio dari alat ini, struktur metadata baru disuntikkan langsung ke dalam header RIFF dari file WAV.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Menilai durasi loop dan transisi fase' },
|
|
136
|
+
{ type: 'paragraph', html: 'Zero crossing mengurangi lonjakan amplitudo, tetapi tidak otomatis membuat transisi musik terdengar alami. Dengarkan bagian awal dan akhir secara berurutan, lalu periksa ritme, harmoni, serta ekor reverb saat loop diulang.' },
|
|
137
|
+
{ type: 'paragraph', html: 'Marker disimpan dalam satuan sample sehingga tidak bergantung pada pembulatan milidetik. Setelah pemeriksaan audio, ekspor WAV dan uji berkas tersebut di engine yang akan memutarnya dalam game.' },
|
|
138
|
+
{ type: 'paragraph', html: 'Periksa juga bahwa panjang loop sesuai dengan frasa musik dan tidak memotong nada atau ekor efek. Nilai marker dalam sample memberikan ketelitian yang tetap saat file berpindah antara perangkat, tetapi hasil akhirnya tetap harus didengarkan pada tempo dan volume permainan yang sebenarnya.' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: 'Pertanyaan yang Sering Diajukan',
|
|
137
141
|
faq: [
|
|
@@ -132,6 +132,9 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Durante l esportazione delle tracce, le nuove strutture di metadati vengono iniettate direttamente nell intestazione RIFF del file binario WAV.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Valutare la durata del loop e la transizione di fase' },
|
|
136
|
+
{ type: 'paragraph', html: 'Un passaggio per lo zero riduce i salti di ampiezza, ma non garantisce da solo una transizione musicale naturale. Ascolta in sequenza inizio e fine e controlla ritmo, armonia e coda del riverbero durante la ripetizione.' },
|
|
137
|
+
{ type: 'paragraph', html: 'I marker sono conservati in campioni e non dipendono dall arrotondamento dei millisecondi. Dopo il controllo d ascolto, esporta il WAV e provalo nel motore che lo riprodurrà nel gioco.' },
|
|
135
138
|
],
|
|
136
139
|
faqTitle: 'Domande Frequenti',
|
|
137
140
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Bij het exporteren van audiotracks vanuit deze tool worden nieuwe metadatastructuren direct in de RIFF-header van het WAV-bestand geplaatst.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Loopduur en faseovergang beoordelen' },
|
|
136
|
+
{ type: 'paragraph', html: 'Een nuldoorgang beperkt sprongen in amplitude, maar garandeert niet automatisch een muzikale overgang. Luister naar begin en einde achter elkaar en controleer ritme, harmonie en de galmstaart tijdens het herhalen.' },
|
|
137
|
+
{ type: 'paragraph', html: 'Markers worden in samples bewaard en zijn daardoor niet afhankelijk van afronding naar milliseconden. Exporteer pas na de luistercontrole en test het WAV-bestand in de engine die het in de game afspeelt.' },
|
|
138
|
+
{ type: 'paragraph', html: 'Controleer ook of de looplengte bij de muzikale frase past en geen noot of effectstaart afsnijdt. Markers in samples behouden hun nauwkeurigheid wanneer het bestand tussen apparaten verhuist, maar luister altijd op het echte tempo en volume van de game.' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: 'Veelgestelde Vragen',
|
|
137
141
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Podczas eksportowania ścieżek z tego narzędzia nowe struktury metadanych są wstrzykiwane bezpośrednio do nagłówka RIFF pliku WAV.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Ocena długości pętli i przejścia fazowego' },
|
|
136
|
+
{ type: 'paragraph', html: 'Przejście przez zero ogranicza skoki amplitudy, ale samo nie gwarantuje naturalnego przejścia muzycznego. Posłuchaj początku i końca jeden po drugim oraz sprawdź rytm, harmonię i wybrzmienie pogłosu podczas powtarzania.' },
|
|
137
|
+
{ type: 'paragraph', html: 'Znaczniki są zapisywane w próbkach, więc nie zależą od zaokrąglenia do milisekund. Po kontroli odsłuchowej wyeksportuj WAV i przetestuj go w silniku, który odtworzy go w grze.' },
|
|
138
|
+
{ type: 'paragraph', html: 'Sprawdź także, czy długość pętli pasuje do frazy muzycznej i nie ucina nuty ani wybrzmienia efektu. Znaczniki w próbkach zachowują precyzję między urządzeniami, ale wynik trzeba odsłuchać w rzeczywistym tempie i przy docelowej głośności gry.' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: 'Często Zadawane Pytania',
|
|
137
141
|
faq: [
|
|
@@ -132,6 +132,9 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Ao exportar faixas de áudio desta ferramenta, novas estruturas de metadatos são injetadas diretamente no cabeçalho RIFF do arquivo WAV.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Avaliar a duração do loop e a transição de fase' },
|
|
136
|
+
{ type: 'paragraph', html: 'Um cruzamento por zero reduz saltos de amplitude, mas não garante sozinho uma transição musical natural. Ouça o início e o fim em sequência e confira ritmo, harmonia e cauda de reverberação durante a repetição.' },
|
|
137
|
+
{ type: 'paragraph', html: 'Os marcadores são armazenados em samples e não dependem do arredondamento em milissegundos. Depois da conferência auditiva, exporte o WAV e teste-o na engine que fará a reprodução no jogo.' },
|
|
135
138
|
],
|
|
136
139
|
faqTitle: 'Perguntas Frequentes',
|
|
137
140
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'При экспорте аудиодорожек из этой утилиты новые структуры метаданных внедряются непосредственно в заголовок RIFF выходного файла WAV.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Оценка длины петли и перехода фазы' },
|
|
136
|
+
{ type: 'paragraph', html: 'Переход через ноль уменьшает скачки амплитуды, но сам по себе не гарантирует естественное музыкальное соединение. Прослушайте начало и конец подряд и проверьте ритм, гармонию и хвост реверберации при повторении.' },
|
|
137
|
+
{ type: 'paragraph', html: 'Маркеры сохраняются в сэмплах и не зависят от округления миллисекунд. После проверки на слух экспортируйте WAV и протестируйте его в движке, который будет воспроизводить файл в игре.' },
|
|
138
|
+
{ type: 'paragraph', html: 'Также проверьте, соответствует ли длина петли музыкальной фразе и не обрезает ли ноту или хвост эффекта. Маркеры в сэмплах сохраняют точность при переносе файла между устройствами, однако результат нужно прослушать в реальном темпе и с целевой громкостью игры.' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: 'Часто Задаваемые Вопросы',
|
|
137
141
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'När du exporterar ljudspår från detta verktyg bäddas nya metadatastrukturer in direkt i RIFF-headern på den exporterade WAV-filen.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Bedöm loopens längd och fasövergång' },
|
|
136
|
+
{ type: 'paragraph', html: 'En nollgenomgång minskar amplitudhopp, men garanterar inte automatiskt en naturlig musikalisk övergång. Lyssna på början och slutet efter varandra och kontrollera rytm, harmoni och efterklang när loopen upprepas.' },
|
|
137
|
+
{ type: 'paragraph', html: 'Markörerna sparas i samplingar och påverkas därför inte av avrundning till millisekunder. Exportera efter lyssningskontrollen och testa WAV-filen i motorn som ska spela den i spelet.' },
|
|
138
|
+
{ type: 'paragraph', html: 'Kontrollera också att loopens längd passar den musikaliska frasen och inte klipper en ton eller en effektklang. Markörer i samplingar behåller precision mellan enheter, men resultatet ska alltid lyssnas på i spelets verkliga tempo och volym.' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: 'Vanliga Frågor',
|
|
137
141
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: 'Bu araçtan ses parçalarını dışa aktarırken yeni üstveri yapıları doğrudan WAV dosyasının RIFF başlığına enjekte edilir.',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: 'Döngü süresini ve faz geçişini değerlendirme' },
|
|
136
|
+
{ type: 'paragraph', html: 'Sıfır geçişi genlik sıçramalarını azaltır, ancak tek başına doğal bir müzik geçişini garanti etmez. Başlangıç ve bitişi art arda dinleyin; tekrar sırasında ritmi, armoniyi ve yankı kuyruğunu kontrol edin.' },
|
|
137
|
+
{ type: 'paragraph', html: 'İşaretçiler sample cinsinden saklandığı için milisaniye yuvarlamasına bağlı değildir. Dinleme kontrolünden sonra WAV dosyasını dışa aktarın ve oyunda çalacak motorda test edin.' },
|
|
138
|
+
{ type: 'paragraph', html: 'Döngü uzunluğunun müzik cümlesine uyduğunu ve bir notayı ya da efekt kuyruğunu kesmediğini de kontrol edin. Sample cinsindeki işaretçiler dosya cihazlar arasında taşınsa bile hassasiyeti korur; yine de sonucu oyunun gerçek temposunda ve ses düzeyinde dinleyin.' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: 'Sıkça Sorulan Sorular',
|
|
137
141
|
faq: [
|
|
@@ -132,6 +132,10 @@ export const content: ToolLocaleContent<AudioLoopPointFinderUI> = {
|
|
|
132
132
|
type: 'paragraph',
|
|
133
133
|
html: '从此工具导出音频轨道时,新的元数据结构将直接注入到输出WAV二进制文件的RIFF头中。',
|
|
134
134
|
},
|
|
135
|
+
{ type: 'title', level: 2, text: '评估循环长度与相位衔接' },
|
|
136
|
+
{ type: 'paragraph', html: '过零点可以减少振幅跳变,但不能单独保证自然的音乐衔接。请连续聆听开头和结尾,并检查循环重复时的节奏、和声以及混响尾音。' },
|
|
137
|
+
{ type: 'paragraph', html: '标记点以采样数保存,不依赖毫秒四舍五入。完成听感检查后再导出 WAV,并在游戏实际使用的引擎中测试播放结果。' },
|
|
138
|
+
{ type: 'paragraph', html: '还要确认循环长度符合音乐乐句,不会截断音符或效果尾音。采样数标记在设备之间传递时仍能保持精度,但最终结果应在游戏的实际速度和音量下试听。' },
|
|
135
139
|
],
|
|
136
140
|
faqTitle: '常见问题解答',
|
|
137
141
|
faq: [
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { BibliographyEntry } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
title?: string;
|
|
6
|
+
entries?: BibliographyEntry[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { title = 'References', entries = [] } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
{entries.length > 0 && (
|
|
13
|
+
<section class="damage-lab-references" aria-labelledby="damage-lab-reference-title">
|
|
14
|
+
<h2 id="damage-lab-reference-title">{title}</h2>
|
|
15
|
+
<ul>
|
|
16
|
+
{entries.map((entry) => <li><a href={entry.url} rel="noopener noreferrer" target="_blank">{entry.name}</a></li>)}
|
|
17
|
+
</ul>
|
|
18
|
+
</section>
|
|
19
|
+
)}
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
.damage-lab-references { margin-block: 2rem; }
|
|
23
|
+
.damage-lab-references a { color: var(--n-primary); }
|
|
24
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const bibliographyEntries = [
|
|
2
|
+
{
|
|
3
|
+
name: "Game Balance Concepts: Numeric Relationships",
|
|
4
|
+
url: "https://gamebalanceconcepts.wordpress.com/2010/07/14/level-2-numeric-relationships/",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: "Oracle Numerical Computation Guide: What Is IEEE Arithmetic?",
|
|
8
|
+
url: "https://docs.oracle.com/cd/E37069_01/html/E39019/z4000ac019127.html",
|
|
9
|
+
},
|
|
10
|
+
];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DamageFormulaLabUI } from './ui';
|
|
3
|
+
import './damage-formula-lab.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: DamageFormulaLabUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
|
|
11
|
+
const coreInputs = [
|
|
12
|
+
['attack', ui.attackLabel, 0, 100000, 1],
|
|
13
|
+
['defense', ui.defenseLabel, 0, 100000, 1],
|
|
14
|
+
['health', ui.healthLabel, 1, 10000000, 1],
|
|
15
|
+
['attacksPerSecond', ui.cadenceLabel, 0.01, 100, 0.05],
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
const advancedInputs = [
|
|
19
|
+
['level', ui.levelLabel, 1, 999, 1],
|
|
20
|
+
['power', ui.powerLabel, 0, 100, 0.05],
|
|
21
|
+
['resistance', ui.resistanceLabel, 0, 100, 1],
|
|
22
|
+
['flat', ui.flatLabel, -100000, 100000, 1],
|
|
23
|
+
['criticalChance', ui.criticalChanceLabel, 0, 100, 1],
|
|
24
|
+
['criticalMultiplier', ui.criticalMultiplierLabel, 1, 20, 0.05],
|
|
25
|
+
] as const;
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<section class="damage-lab" data-damage-lab>
|
|
29
|
+
<script is:inline type="application/json" data-damage-ui set:html={JSON.stringify(ui)} />
|
|
30
|
+
|
|
31
|
+
<header class="damage-lab-header">
|
|
32
|
+
<p>{ui.onboarding}</p>
|
|
33
|
+
<span>{ui.localNote}</span>
|
|
34
|
+
</header>
|
|
35
|
+
|
|
36
|
+
<section class="damage-lab-composer" aria-labelledby="damage-lab-formulas-title">
|
|
37
|
+
<div class="damage-lab-heading">
|
|
38
|
+
<h2 id="damage-lab-formulas-title">{ui.formulaDeck}</h2>
|
|
39
|
+
<div class="damage-lab-presets" aria-label="Formula presets">
|
|
40
|
+
<button type="button" data-preset="linear">{ui.presetLinear}</button>
|
|
41
|
+
<button type="button" data-preset="ratio">{ui.presetRatio}</button>
|
|
42
|
+
<button type="button" data-preset="level">{ui.presetLevel}</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="damage-lab-formulas">
|
|
46
|
+
<label class="damage-lab-formula damage-lab-formula-a">
|
|
47
|
+
<span><i aria-hidden="true"></i>{ui.formulaALabel}</span>
|
|
48
|
+
<input name="formulaA" type="text" spellcheck="false" autocomplete="off" />
|
|
49
|
+
</label>
|
|
50
|
+
<label class="damage-lab-formula damage-lab-formula-b">
|
|
51
|
+
<span><i aria-hidden="true"></i>{ui.formulaBLabel}</span>
|
|
52
|
+
<input name="formulaB" type="text" spellcheck="false" autocomplete="off" />
|
|
53
|
+
</label>
|
|
54
|
+
</div>
|
|
55
|
+
<details class="damage-lab-language">
|
|
56
|
+
<summary>Formula language</summary>
|
|
57
|
+
<p>{ui.formulaHint}<br />{ui.formulaFunctions}</p>
|
|
58
|
+
</details>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<section class="damage-lab-tuning" aria-labelledby="damage-lab-tuning-title">
|
|
62
|
+
<div class="damage-lab-heading">
|
|
63
|
+
<h2 id="damage-lab-tuning-title">{ui.combatInputs}</h2>
|
|
64
|
+
<div class="damage-lab-scenarios" aria-label={ui.scenariosTitle}>
|
|
65
|
+
<button type="button" data-scenario="skirmisher">{ui.scenarioSkirmisher}</button>
|
|
66
|
+
<button type="button" data-scenario="guardian">{ui.scenarioGuardian}</button>
|
|
67
|
+
<button type="button" data-scenario="boss">{ui.scenarioBoss}</button>
|
|
68
|
+
<button type="button" data-scenario="custom">{ui.scenarioCustom}</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="damage-lab-core-inputs">
|
|
72
|
+
{coreInputs.map(([name, label, min, max, step]) => (
|
|
73
|
+
<label>
|
|
74
|
+
<span>{label}</span>
|
|
75
|
+
<input name={name} type="number" min={min} max={max} step={step} inputmode="decimal" />
|
|
76
|
+
</label>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
<details class="damage-lab-advanced">
|
|
80
|
+
<summary>Advanced combat and engine rules</summary>
|
|
81
|
+
<div class="damage-lab-advanced-grid">
|
|
82
|
+
{advancedInputs.map(([name, label, min, max, step]) => (
|
|
83
|
+
<label>
|
|
84
|
+
<span>{label}</span>
|
|
85
|
+
<input name={name} type="number" min={min} max={max} step={step} inputmode="decimal" />
|
|
86
|
+
</label>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
<fieldset class="damage-lab-choice">
|
|
90
|
+
<legend>{ui.roundingLabel}</legend>
|
|
91
|
+
<button type="button" data-rounding="none">{ui.roundingNone}</button>
|
|
92
|
+
<button type="button" data-rounding="floor">{ui.roundingFloor}</button>
|
|
93
|
+
<button type="button" data-rounding="round">{ui.roundingRound}</button>
|
|
94
|
+
<button type="button" data-rounding="ceil">{ui.roundingCeil}</button>
|
|
95
|
+
</fieldset>
|
|
96
|
+
<fieldset class="damage-lab-choice">
|
|
97
|
+
<legend>{ui.orderLabel}</legend>
|
|
98
|
+
<button type="button" data-order="resistance-first">{ui.resistanceFirst}</button>
|
|
99
|
+
<button type="button" data-order="flat-first">{ui.flatFirst}</button>
|
|
100
|
+
</fieldset>
|
|
101
|
+
</details>
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<section class="damage-lab-result" aria-labelledby="damage-lab-result-title">
|
|
105
|
+
<div class="damage-lab-result-heading">
|
|
106
|
+
<div><span>Live model</span><h2 id="damage-lab-result-title">{ui.runLabel}</h2></div>
|
|
107
|
+
<div class="damage-lab-gap"><small>{ui.resultDifference}</small><strong data-result="gap">0</strong></div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="damage-lab-totals">
|
|
110
|
+
<article>
|
|
111
|
+
<span>{ui.formulaAName}</span>
|
|
112
|
+
<strong data-result="a-damage">0</strong>
|
|
113
|
+
<small>{ui.resultDamage}</small>
|
|
114
|
+
<dl><div><dt>{ui.resultHits}</dt><dd data-result="a-hits">0</dd></div><div><dt>{ui.resultTtk}</dt><dd data-result="a-ttk">0 s</dd></div></dl>
|
|
115
|
+
</article>
|
|
116
|
+
<article>
|
|
117
|
+
<span>{ui.formulaBName}</span>
|
|
118
|
+
<strong data-result="b-damage">0</strong>
|
|
119
|
+
<small>{ui.resultDamage}</small>
|
|
120
|
+
<dl><div><dt>{ui.resultHits}</dt><dd data-result="b-hits">0</dd></div><div><dt>{ui.resultTtk}</dt><dd data-result="b-ttk">0 s</dd></div></dl>
|
|
121
|
+
</article>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="damage-lab-curve-heading">
|
|
124
|
+
<div><h3>{ui.curveTitle}</h3><p>{ui.curveCaption}</p></div>
|
|
125
|
+
<span><b data-curve-max>0</b> max</span>
|
|
126
|
+
</div>
|
|
127
|
+
<svg class="damage-lab-chart" data-chart viewBox="0 0 100 100" preserveAspectRatio="none" role="img" aria-label={ui.curveTitle}>
|
|
128
|
+
<path class="damage-lab-guide" d="M0 25 H100 M0 50 H100 M0 75 H100" />
|
|
129
|
+
<path class="damage-lab-curve damage-lab-curve-a" data-curve="a" d="" />
|
|
130
|
+
<path class="damage-lab-curve damage-lab-curve-b" data-curve="b" d="" />
|
|
131
|
+
</svg>
|
|
132
|
+
<div class="damage-lab-legend"><span>{ui.formulaAName}</span><span>{ui.formulaBName}</span></div>
|
|
133
|
+
</section>
|
|
134
|
+
|
|
135
|
+
<details class="damage-lab-secondary">
|
|
136
|
+
<summary>{ui.heatmapTitle} + {ui.diagnosticsTitle}</summary>
|
|
137
|
+
<div class="damage-lab-notes">
|
|
138
|
+
<section class="damage-lab-pressure" aria-labelledby="damage-lab-heatmap-title">
|
|
139
|
+
<h2 id="damage-lab-heatmap-title">{ui.heatmapTitle}</h2>
|
|
140
|
+
<p>{ui.heatmapCaption}</p>
|
|
141
|
+
<div class="damage-lab-axis">{ui.attackAxis}</div>
|
|
142
|
+
<div class="damage-lab-heatmap" data-heatmap></div>
|
|
143
|
+
<small>{ui.defenseAxis}</small>
|
|
144
|
+
</section>
|
|
145
|
+
<section class="damage-lab-diagnostics" aria-labelledby="damage-lab-diagnostics-title">
|
|
146
|
+
<h2 id="damage-lab-diagnostics-title">{ui.diagnosticsTitle}</h2>
|
|
147
|
+
<ul data-diagnostics></ul>
|
|
148
|
+
<p>{ui.limitationDisclosure}</p>
|
|
149
|
+
</section>
|
|
150
|
+
</div>
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details class="damage-lab-export-panel">
|
|
154
|
+
<summary>{ui.exportTitle}</summary>
|
|
155
|
+
<footer class="damage-lab-exports">
|
|
156
|
+
<p>{ui.privacyDisclosure}</p>
|
|
157
|
+
<div class="damage-lab-actions">
|
|
158
|
+
<button type="button" data-share>{ui.copyLink}</button>
|
|
159
|
+
<button type="button" data-csv>{ui.exportCsv}</button>
|
|
160
|
+
<button type="button" data-json>{ui.exportJson}</button>
|
|
161
|
+
<label>{ui.importJson}<input type="file" accept="application/json,.json" data-import /></label>
|
|
162
|
+
<button type="button" data-png>{ui.exportPng}</button>
|
|
163
|
+
<button type="button" data-reset>{ui.reset}</button>
|
|
164
|
+
</div>
|
|
165
|
+
<p class="damage-lab-status" data-status aria-live="polite"></p>
|
|
166
|
+
</footer>
|
|
167
|
+
</details>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<script>
|
|
171
|
+
import { mountDamageFormulaLab } from './controller';
|
|
172
|
+
import type { DamageFormulaLabUI } from './ui';
|
|
173
|
+
|
|
174
|
+
document.querySelectorAll<HTMLElement>('[data-damage-lab]').forEach((root) => {
|
|
175
|
+
const source = root.querySelector<HTMLScriptElement>('[data-damage-ui]');
|
|
176
|
+
if (source?.textContent) mountDamageFormulaLab(root, JSON.parse(source.textContent) as DamageFormulaLabUI);
|
|
177
|
+
});
|
|
178
|
+
</script>
|