@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
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -7,10 +7,11 @@ import { steamBbcodeTranslator } from '../tool/steamBbcodeTranslator/entry';
|
|
|
7
7
|
import { gameUiAccessibilityTester } from '../tool/gameUIAccessibilityTester/entry';
|
|
8
8
|
import { hitboxHurtboxAnimator } from '../tool/hitboxHurtboxAnimator/entry';
|
|
9
9
|
import { damageFormulaLab } from '../tool/damageFormulaLab/entry';
|
|
10
|
+
import { gameDeltaTimeFixedTimestepLab } from '../tool/gameDeltaTimeFixedTimestepLab/entry';
|
|
10
11
|
|
|
11
12
|
export const gamesCategory: GamesCategoryEntry = {
|
|
12
13
|
icon: 'mdi:gamepad-variant',
|
|
13
|
-
tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab],
|
|
14
|
+
tools: [steamCapsuleGenerator, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, steamBbcodeTranslator, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab, gameDeltaTimeFixedTimestepLab],
|
|
14
15
|
i18n: {
|
|
15
16
|
de: () => import('./i18n/de').then((m) => m.content),
|
|
16
17
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -20,6 +20,8 @@ export { hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator/entry';
|
|
|
20
20
|
export type { HitboxHurtboxAnimatorUI, HitboxHurtboxAnimatorLocaleContent } from './tool/hitboxHurtboxAnimator/entry';
|
|
21
21
|
export { damageFormulaLab } from './tool/damageFormulaLab/entry';
|
|
22
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';
|
|
23
25
|
export { gamesCategory } from './category';
|
|
24
26
|
import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
25
27
|
import { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
@@ -33,5 +35,6 @@ import { pixelArtPaletteSwapper } from './tool/pixelArtPaletteSwapper/entry';
|
|
|
33
35
|
import { gameUiAccessibilityTester } from './tool/gameUIAccessibilityTester/entry';
|
|
34
36
|
import { hitboxHurtboxAnimator } from './tool/hitboxHurtboxAnimator/entry';
|
|
35
37
|
import { damageFormulaLab } from './tool/damageFormulaLab/entry';
|
|
38
|
+
import { gameDeltaTimeFixedTimestepLab } from './tool/gameDeltaTimeFixedTimestepLab/entry';
|
|
36
39
|
|
|
37
|
-
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, steamBbcodeTranslator, retroSfxGenerator, pixelArtPaletteSwapper, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab];
|
|
40
|
+
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker, audioLoopPointFinder, saveFileEditor, localizationSanitizer, steamBbcodeTranslator, retroSfxGenerator, pixelArtPaletteSwapper, gameUiAccessibilityTester, hitboxHurtboxAnimator, damageFormulaLab, gameDeltaTimeFixedTimestepLab];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
|
|
5
|
+
function findBibliographyAstroFiles(dir: string): string[] {
|
|
6
|
+
const files: string[] = [];
|
|
7
|
+
if (!existsSync(dir)) return files;
|
|
8
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
9
|
+
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
files.push(...findBibliographyAstroFiles(fullPath));
|
|
14
|
+
} else if (entry.isFile() && entry.name === 'bibliography.astro') {
|
|
15
|
+
files.push(fullPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toolDir = join(process.cwd(), 'src', 'tool');
|
|
23
|
+
const bibliographyFiles = findBibliographyAstroFiles(toolDir);
|
|
24
|
+
|
|
25
|
+
describe('Bibliography Component Wellformed Export', () => {
|
|
26
|
+
it('found tool bibliography.astro components', () => {
|
|
27
|
+
expect(bibliographyFiles.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
bibliographyFiles.forEach((file) => {
|
|
31
|
+
const relativePath = relative(process.cwd(), file);
|
|
32
|
+
|
|
33
|
+
it(`${relativePath} should use SharedBibliography from @jjlmoya/utils-shared`, () => {
|
|
34
|
+
const content = readFileSync(file, 'utf-8');
|
|
35
|
+
|
|
36
|
+
const usesSharedComponent =
|
|
37
|
+
content.includes('@jjlmoya/utils-shared') &&
|
|
38
|
+
(content.includes('Bibliography') || content.includes('SharedBibliography'));
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
usesSharedComponent,
|
|
42
|
+
`File "${relativePath}" does not use the standard Bibliography component from @jjlmoya/utils-shared, resulting in unstyled or raw bibliography output.`,
|
|
43
|
+
).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
|
|
5
|
+
function findSeoAstroFiles(dir: string): string[] {
|
|
6
|
+
const files: string[] = [];
|
|
7
|
+
if (!existsSync(dir)) return files;
|
|
8
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
9
|
+
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
files.push(...findSeoAstroFiles(fullPath));
|
|
14
|
+
} else if (entry.isFile() && entry.name === 'seo.astro') {
|
|
15
|
+
files.push(fullPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toolDir = join(process.cwd(), 'src', 'tool');
|
|
23
|
+
const seoFiles = findSeoAstroFiles(toolDir);
|
|
24
|
+
|
|
25
|
+
describe('SEO Component Wellformed Export', () => {
|
|
26
|
+
it('found tool seo.astro components', () => {
|
|
27
|
+
expect(seoFiles.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
seoFiles.forEach((file) => {
|
|
31
|
+
const relativePath = relative(process.cwd(), file);
|
|
32
|
+
|
|
33
|
+
it(`${relativePath} should dynamically load SEO sections and use SEORenderer`, () => {
|
|
34
|
+
const content = readFileSync(file, 'utf-8');
|
|
35
|
+
|
|
36
|
+
const usesSeoRenderer =
|
|
37
|
+
content.includes('@jjlmoya/utils-shared') &&
|
|
38
|
+
(content.includes('SEORenderer') || content.includes('SEOArticle'));
|
|
39
|
+
|
|
40
|
+
const acquiresDynamicContent =
|
|
41
|
+
content.includes('.i18n') ||
|
|
42
|
+
content.includes('loader') ||
|
|
43
|
+
content.includes('await loader') ||
|
|
44
|
+
content.includes('.seo');
|
|
45
|
+
|
|
46
|
+
const isBrokenPattern =
|
|
47
|
+
/sections\s*=\s*\[\s*\]/.test(content) && !acquiresDynamicContent;
|
|
48
|
+
|
|
49
|
+
expect(
|
|
50
|
+
usesSeoRenderer,
|
|
51
|
+
`File "${relativePath}" does not import or use SEORenderer from @jjlmoya/utils-shared.`,
|
|
52
|
+
).toBe(true);
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
isBrokenPattern,
|
|
56
|
+
`File "${relativePath}" relies on a static sections=[] prop default without fetching content from entry.i18n, resulting in empty SEO text on consumers.`,
|
|
57
|
+
).toBe(false);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
acquiresDynamicContent,
|
|
61
|
+
`File "${relativePath}" does not fetch dynamic i18n content for SEO rendering.`,
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -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: [
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
title?: string;
|
|
6
|
-
entries?: BibliographyEntry[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { title = 'References', entries = [] } = Astro.props;
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { bibliographyEntries } from './bibliography';
|
|
10
4
|
---
|
|
11
5
|
|
|
12
|
-
{
|
|
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>
|
|
6
|
+
<SharedBibliography links={bibliographyEntries} />
|
|
@@ -172,6 +172,19 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Schaden und Trefferanzahl stets gemeinsam betrachten',
|
|
173
173
|
html: 'Eine kleine Schadensänderung kann eine gesundheitsbezogene Schwelle überschreiten und einen Treffer einsparen. Vergleichen Sie Schadenswerte immer mit Treffern und TTK.',
|
|
174
174
|
},
|
|
175
|
+
{
|
|
176
|
+
type: 'title',
|
|
177
|
+
level: 2,
|
|
178
|
+
text: 'Rundung und Widerstandsreihenfolge sichtbar machen',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'paragraph',
|
|
182
|
+
html: 'Die gleiche Formel kann je nach Rundung und Reihenfolge der Modifikatoren ein anderes Kampfergebnis liefern. Prüfen Sie deshalb nicht nur den mittleren Kurvenbereich, sondern auch kleine Trefferwerte, hohe Verteidigung und die Schwelle, an der ein weiterer Treffer nötig wird.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'paragraph',
|
|
186
|
+
html: 'Die TTK-Anzeige ist eine mathematische Projektion auf Basis der eingegebenen Angriffskadenz. Sie berücksichtigt keine Ausweichbewegung, Cooldowns, Zufallsfolgen oder Unterbrechungen; verwenden Sie die Kurven als Designhilfe und validieren Sie die Regel anschließend im Spieltest.',
|
|
187
|
+
},
|
|
175
188
|
],
|
|
176
189
|
faq,
|
|
177
190
|
bibliographyTitle: 'Referenzen zur Schadensberechnung in Spielen',
|
|
@@ -172,6 +172,19 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Observer conjointement les dégâts et le nombre de coups',
|
|
173
173
|
html: 'Une légère variation de dégâts peut franchir un seuil de santé et supprimer un coup complet. Comparez toujours les dégâts avec le nombre de coups et le TTK.',
|
|
174
174
|
},
|
|
175
|
+
{
|
|
176
|
+
type: 'title',
|
|
177
|
+
level: 2,
|
|
178
|
+
text: 'Rendre visibles l arrondi et l ordre des résistances',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'paragraph',
|
|
182
|
+
html: 'Une même formule peut produire un combat différent selon l arrondi et l ordre des modificateurs. Examinez donc la zone centrale de la courbe, mais aussi les petits dégâts, la défense élevée et le seuil qui ajoute une attaque nécessaire.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'paragraph',
|
|
186
|
+
html: 'L affichage du TTK est une projection mathématique fondée sur la cadence saisie. Il ne modélise ni esquive, ni temps de recharge, ni suite aléatoire, ni interruption: utilisez les courbes pour guider le design puis vérifiez la règle en jeu.',
|
|
187
|
+
},
|
|
175
188
|
],
|
|
176
189
|
faq,
|
|
177
190
|
bibliographyTitle: 'Références sur la modélisation des dégâts',
|
|
@@ -172,6 +172,19 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Periksa damage dan jumlah pukulan secara bersamaan',
|
|
173
173
|
html: 'Perubahan kecil pada damage bisa saja melewati ambang batas darah target dan mengurangi 1 pukulan penuh. Selalu bandingkan nilai damage dengan jumlah pukulan dan TTK.',
|
|
174
174
|
},
|
|
175
|
+
{
|
|
176
|
+
type: 'title',
|
|
177
|
+
level: 2,
|
|
178
|
+
text: 'Membuat pembulatan dan urutan resistance terlihat',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'paragraph',
|
|
182
|
+
html: 'Formula yang sama dapat menghasilkan pertarungan berbeda bergantung pada pembulatan dan urutan modifier. Jangan hanya memeriksa bagian tengah kurva; lihat juga damage kecil, defense tinggi, dan batas yang menambah satu serangan lagi.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'paragraph',
|
|
186
|
+
html: 'Nilai TTK adalah proyeksi matematika berdasarkan cadence yang dimasukkan. Nilai ini tidak memodelkan dodge, cooldown, urutan acak, atau interupsi, sehingga hasil kurva perlu diuji kembali dalam game.',
|
|
187
|
+
},
|
|
175
188
|
],
|
|
176
189
|
faq,
|
|
177
190
|
bibliographyTitle: 'Referensi kalkulasi damage game',
|
|
@@ -172,6 +172,7 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Esamina sempre sia il danno che i colpi necessari',
|
|
173
173
|
html: 'Una piccola variazione di danno può superare una soglia di salute ed eliminare un intero colpo. Confronta sempre il danno con il TTK.',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'La stessa formula può produrre un combattimento diverso in base all arrotondamento e all ordine dei modificatori. Controlla anche danni piccoli, difesa alta e la soglia che richiede un colpo aggiuntivo; il TTK non include schivate, cooldown o interruzioni.' },
|
|
175
176
|
],
|
|
176
177
|
faq,
|
|
177
178
|
bibliographyTitle: 'Riferimenti per il calcolo del danno nei videogiochi',
|
|
@@ -70,6 +70,7 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
70
70
|
ui: {
|
|
71
71
|
onboarding: '現在のダメージ計算式と代替案を入力し、戦闘ステータスを調整して極端なステータス帯での挙動を検証します。',
|
|
72
72
|
privacyNote: 'ローカル専用設計。計算式や設定はブラウザ内のみで処理されます。',
|
|
73
|
+
localNote: '戦闘モデルは非公開です。計算式とファイルはこのブラウザ内に留まります。',
|
|
73
74
|
loadSprite: '計算式入力エリア',
|
|
74
75
|
formulaDeck: '計算式デッキ',
|
|
75
76
|
formulaALabel: '計算式A (現行モデル)',
|
|
@@ -70,6 +70,7 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
70
70
|
ui: {
|
|
71
71
|
onboarding: '현재 사용 중인 데미지 공식과 대안 공식을 입력하고 전투 능력치를 조정하여 수치적 경계를 검증하세요.',
|
|
72
72
|
privacyNote: '로컬 전용 모델. 공식과 설정은 브라우저 내부에서만 처리됩니다.',
|
|
73
|
+
localNote: '비공개 전투 모델입니다. 공식과 파일은 이 브라우저 안에만 보관됩니다.',
|
|
73
74
|
formulaDeck: '공식 챔버',
|
|
74
75
|
formulaALabel: '공식 A (현재 모델)',
|
|
75
76
|
formulaBLabel: '공식 B (대안 모델)',
|
|
@@ -172,6 +172,7 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Bekijk schade en treffers altijd samen',
|
|
173
173
|
html: 'Een kleine verandering in schade kan een gezondheidsdrempel overschrijden en een complete treffer schelen. Vergelijk schade altijd met TTK.',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'Dezelfde formule kan door afronding en de volgorde van modifiers een ander gevecht opleveren. Controleer ook kleine schade, hoge verdediging en de drempel voor een extra treffer; TTK modelleert geen ontwijken, cooldowns of onderbrekingen.' },
|
|
175
176
|
],
|
|
176
177
|
faq,
|
|
177
178
|
bibliographyTitle: 'Referenties voor schadeberekening in games',
|
|
@@ -172,6 +172,7 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Analizuj obrażenia i liczbę ciosów jednocześnie',
|
|
173
173
|
html: 'Niewielka zmiana obrażeń może przekroczyć próg zdrowia i zmniejszyć liczbę ciosów o jeden. Zawsze porównuj obrażenia z czasem zabicia (TTK).',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'Ta sama formuła może dać inny przebieg walki zależnie od zaokrągleń i kolejności modyfikatorów. Sprawdź także małe obrażenia, wysoką obronę i próg dodatkowego trafienia; TTK nie obejmuje uników, cooldownów ani przerw.' },
|
|
175
176
|
],
|
|
176
177
|
faq,
|
|
177
178
|
bibliographyTitle: 'Referencje do projektowania matematyki w grach',
|
|
@@ -172,6 +172,7 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
|
|
|
172
172
|
title: 'Examine o dano e o número de golpes juntos',
|
|
173
173
|
html: 'Uma pequena alteração no dano pode cruzar um limiar de vida e eliminar um golpe completo. Compare sempre o dano com o TTK.',
|
|
174
174
|
},
|
|
175
|
+
{ type: 'paragraph', html: 'A mesma fórmula pode produzir um combate diferente conforme o arredondamento e a ordem dos modificadores. Confira também danos pequenos, defesa alta e o limite que exige um golpe extra; o TTK não modela esquivas, recargas ou interrupções.' },
|
|
175
176
|
],
|
|
176
177
|
faq,
|
|
177
178
|
bibliographyTitle: 'Referências de cálculo de dano em jogos',
|