@jjlmoya/utils-games-development 1.64.0 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/seo_parity.test.ts +2 -2
  6. package/src/tests/seo_translation_completeness.test.ts +69 -0
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tests/translation_copy.test.ts +21 -18
  9. package/src/tool/audioLoopPointFinder/i18n/de.ts +13 -0
  10. package/src/tool/audioLoopPointFinder/i18n/fr.ts +26 -0
  11. package/src/tool/audioLoopPointFinder/i18n/id.ts +4 -0
  12. package/src/tool/audioLoopPointFinder/i18n/it.ts +3 -0
  13. package/src/tool/audioLoopPointFinder/i18n/nl.ts +4 -0
  14. package/src/tool/audioLoopPointFinder/i18n/pl.ts +4 -0
  15. package/src/tool/audioLoopPointFinder/i18n/pt.ts +3 -0
  16. package/src/tool/audioLoopPointFinder/i18n/ru.ts +4 -0
  17. package/src/tool/audioLoopPointFinder/i18n/sv.ts +4 -0
  18. package/src/tool/audioLoopPointFinder/i18n/tr.ts +4 -0
  19. package/src/tool/audioLoopPointFinder/i18n/zh.ts +4 -0
  20. package/src/tool/damageFormulaLab/i18n/de.ts +13 -0
  21. package/src/tool/damageFormulaLab/i18n/fr.ts +13 -0
  22. package/src/tool/damageFormulaLab/i18n/id.ts +13 -0
  23. package/src/tool/damageFormulaLab/i18n/it.ts +1 -0
  24. package/src/tool/damageFormulaLab/i18n/ja.ts +1 -0
  25. package/src/tool/damageFormulaLab/i18n/ko.ts +1 -0
  26. package/src/tool/damageFormulaLab/i18n/nl.ts +1 -0
  27. package/src/tool/damageFormulaLab/i18n/pl.ts +1 -0
  28. package/src/tool/damageFormulaLab/i18n/pt.ts +1 -0
  29. package/src/tool/damageFormulaLab/i18n/ru.ts +2 -0
  30. package/src/tool/damageFormulaLab/i18n/sv.ts +2 -0
  31. package/src/tool/damageFormulaLab/i18n/tr.ts +2 -0
  32. package/src/tool/damageFormulaLab/i18n/zh.ts +2 -0
  33. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.astro +22 -0
  34. package/src/tool/gameDeltaTimeFixedTimestepLab/bibliography.ts +10 -0
  35. package/src/tool/gameDeltaTimeFixedTimestepLab/component.astro +51 -0
  36. package/src/tool/gameDeltaTimeFixedTimestepLab/controller.ts +79 -0
  37. package/src/tool/gameDeltaTimeFixedTimestepLab/dom-views.ts +88 -0
  38. package/src/tool/gameDeltaTimeFixedTimestepLab/entry.ts +27 -0
  39. package/src/tool/gameDeltaTimeFixedTimestepLab/evaluator.ts +21 -0
  40. package/src/tool/gameDeltaTimeFixedTimestepLab/game-delta-time-fixed-timestep-lab.css +491 -0
  41. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/de.ts +56 -0
  42. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/en.ts +180 -0
  43. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/es.ts +56 -0
  44. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/fr.ts +46 -0
  45. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/id.ts +46 -0
  46. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/it.ts +46 -0
  47. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ja.ts +46 -0
  48. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ko.ts +46 -0
  49. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/nl.ts +46 -0
  50. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pl.ts +46 -0
  51. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/pt.ts +46 -0
  52. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/ru.ts +46 -0
  53. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/sv.ts +46 -0
  54. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/tr.ts +46 -0
  55. package/src/tool/gameDeltaTimeFixedTimestepLab/i18n/zh.ts +46 -0
  56. package/src/tool/gameDeltaTimeFixedTimestepLab/index.ts +11 -0
  57. package/src/tool/gameDeltaTimeFixedTimestepLab/logic.test.ts +35 -0
  58. package/src/tool/gameDeltaTimeFixedTimestepLab/logic.ts +140 -0
  59. package/src/tool/gameDeltaTimeFixedTimestepLab/seo.astro +15 -0
  60. package/src/tool/gameDeltaTimeFixedTimestepLab/seo_contract.test.ts +25 -0
  61. package/src/tool/gameDeltaTimeFixedTimestepLab/storage.ts +23 -0
  62. package/src/tool/gameDeltaTimeFixedTimestepLab/ui.ts +54 -0
  63. package/src/tool/gameUIAccessibilityTester/i18n/de.ts +13 -0
  64. package/src/tool/gameUIAccessibilityTester/i18n/fr.ts +13 -0
  65. package/src/tool/gameUIAccessibilityTester/i18n/id.ts +13 -0
  66. package/src/tool/gameUIAccessibilityTester/i18n/it.ts +1 -0
  67. package/src/tool/gameUIAccessibilityTester/i18n/nl.ts +5 -0
  68. package/src/tool/gameUIAccessibilityTester/i18n/pl.ts +5 -0
  69. package/src/tool/gameUIAccessibilityTester/i18n/pt.ts +2 -0
  70. package/src/tool/gameUIAccessibilityTester/i18n/ru.ts +1 -0
  71. package/src/tool/gameUIAccessibilityTester/i18n/sv.ts +1 -0
  72. package/src/tool/gameUIAccessibilityTester/i18n/tr.ts +2 -0
  73. package/src/tool/gameUIAccessibilityTester/i18n/zh.ts +1 -0
  74. package/src/tool/hitboxHurtboxAnimator/i18n/de.ts +14 -0
  75. package/src/tool/hitboxHurtboxAnimator/i18n/fr.ts +13 -0
  76. package/src/tool/hitboxHurtboxAnimator/i18n/id.ts +13 -0
  77. package/src/tool/hitboxHurtboxAnimator/i18n/it.ts +1 -0
  78. package/src/tool/hitboxHurtboxAnimator/i18n/nl.ts +1 -0
  79. package/src/tool/hitboxHurtboxAnimator/i18n/pl.ts +2 -0
  80. package/src/tool/hitboxHurtboxAnimator/i18n/pt.ts +2 -0
  81. package/src/tool/hitboxHurtboxAnimator/i18n/ru.ts +2 -0
  82. package/src/tool/hitboxHurtboxAnimator/i18n/sv.ts +1 -0
  83. package/src/tool/hitboxHurtboxAnimator/i18n/tr.ts +2 -0
  84. package/src/tool/itchioGameTester/i18n/de.ts +13 -0
  85. package/src/tool/itchioGameTester/i18n/es.ts +2 -2
  86. package/src/tool/itchioGameTester/i18n/fr.ts +26 -0
  87. package/src/tool/itchioGameTester/i18n/id.ts +3 -0
  88. package/src/tool/itchioGameTester/i18n/it.ts +3 -0
  89. package/src/tool/itchioGameTester/i18n/ja.ts +3 -0
  90. package/src/tool/itchioGameTester/i18n/ko.ts +3 -0
  91. package/src/tool/itchioGameTester/i18n/nl.ts +3 -0
  92. package/src/tool/itchioGameTester/i18n/pl.ts +3 -0
  93. package/src/tool/itchioGameTester/i18n/pt.ts +3 -0
  94. package/src/tool/itchioGameTester/i18n/ru.ts +3 -0
  95. package/src/tool/itchioGameTester/i18n/sv.ts +3 -0
  96. package/src/tool/itchioGameTester/i18n/tr.ts +3 -0
  97. package/src/tool/itchioGameTester/i18n/zh.ts +3 -0
  98. package/src/tool/localizationSanitizer/i18n/de.ts +13 -0
  99. package/src/tool/localizationSanitizer/i18n/es.ts +26 -0
  100. package/src/tool/localizationSanitizer/i18n/fr.ts +4 -0
  101. package/src/tool/localizationSanitizer/i18n/id.ts +4 -0
  102. package/src/tool/localizationSanitizer/i18n/it.ts +4 -0
  103. package/src/tool/localizationSanitizer/i18n/ja.ts +4 -0
  104. package/src/tool/localizationSanitizer/i18n/ko.ts +4 -0
  105. package/src/tool/localizationSanitizer/i18n/nl.ts +5 -0
  106. package/src/tool/localizationSanitizer/i18n/pl.ts +5 -0
  107. package/src/tool/localizationSanitizer/i18n/pt.ts +5 -0
  108. package/src/tool/localizationSanitizer/i18n/ru.ts +5 -0
  109. package/src/tool/localizationSanitizer/i18n/sv.ts +5 -0
  110. package/src/tool/localizationSanitizer/i18n/tr.ts +5 -0
  111. package/src/tool/localizationSanitizer/i18n/zh.ts +6 -0
  112. package/src/tool/pixelArtPaletteSwapper/i18n/ja.ts +13 -0
  113. package/src/tool/pixelArtPaletteSwapper/i18n/ko.ts +13 -0
  114. package/src/tool/pixelArtPaletteSwapper/i18n/zh.ts +18 -0
  115. package/src/tool/retroSfxGenerator/i18n/de.ts +13 -0
  116. package/src/tool/retroSfxGenerator/i18n/fr.ts +13 -0
  117. package/src/tool/retroSfxGenerator/i18n/id.ts +13 -0
  118. package/src/tool/retroSfxGenerator/i18n/it.ts +1 -0
  119. package/src/tool/retroSfxGenerator/i18n/nl.ts +1 -0
  120. package/src/tool/retroSfxGenerator/i18n/pl.ts +1 -0
  121. package/src/tool/retroSfxGenerator/i18n/pt.ts +1 -0
  122. package/src/tool/retroSfxGenerator/i18n/ru.ts +1 -0
  123. package/src/tool/retroSfxGenerator/i18n/sv.ts +2 -0
  124. package/src/tool/retroSfxGenerator/i18n/tr.ts +1 -0
  125. package/src/tool/retroSfxGenerator/i18n/zh.ts +1 -0
  126. package/src/tool/spriteSheetPacker/i18n/de.ts +13 -0
  127. package/src/tool/spriteSheetPacker/i18n/es.ts +2 -2
  128. package/src/tool/spriteSheetPacker/i18n/fr.ts +26 -0
  129. package/src/tool/spriteSheetPacker/i18n/id.ts +4 -0
  130. package/src/tool/spriteSheetPacker/i18n/it.ts +4 -0
  131. package/src/tool/spriteSheetPacker/i18n/ja.ts +3 -0
  132. package/src/tool/spriteSheetPacker/i18n/ko.ts +3 -0
  133. package/src/tool/spriteSheetPacker/i18n/nl.ts +4 -0
  134. package/src/tool/spriteSheetPacker/i18n/pl.ts +4 -0
  135. package/src/tool/spriteSheetPacker/i18n/pt.ts +4 -0
  136. package/src/tool/spriteSheetPacker/i18n/ru.ts +4 -0
  137. package/src/tool/spriteSheetPacker/i18n/sv.ts +4 -0
  138. package/src/tool/spriteSheetPacker/i18n/tr.ts +4 -0
  139. package/src/tool/spriteSheetPacker/i18n/zh.ts +4 -0
  140. package/src/tool/steamBbcodeTranslator/i18n/de.ts +22 -0
  141. package/src/tool/steamBbcodeTranslator/i18n/es.ts +23 -1
  142. package/src/tool/steamBbcodeTranslator/i18n/fr.ts +6 -1
  143. package/src/tool/steamBbcodeTranslator/i18n/id.ts +7 -1
  144. package/src/tool/steamBbcodeTranslator/i18n/it.ts +6 -1
  145. package/src/tool/steamBbcodeTranslator/i18n/ja.ts +5 -1
  146. package/src/tool/steamBbcodeTranslator/i18n/ko.ts +5 -1
  147. package/src/tool/steamBbcodeTranslator/i18n/nl.ts +7 -1
  148. package/src/tool/steamBbcodeTranslator/i18n/pl.ts +7 -1
  149. package/src/tool/steamBbcodeTranslator/i18n/pt.ts +7 -1
  150. package/src/tool/steamBbcodeTranslator/i18n/ru.ts +7 -1
  151. package/src/tool/steamBbcodeTranslator/i18n/sv.ts +7 -1
  152. package/src/tool/steamBbcodeTranslator/i18n/tr.ts +7 -1
  153. package/src/tool/steamBbcodeTranslator/i18n/zh.ts +5 -1
  154. package/src/tool/steamCapsuleGenerator/i18n/de.ts +2 -2
  155. package/src/tool/steamCapsuleGenerator/i18n/es.ts +2 -2
  156. package/src/tool/steamCapsuleGenerator/i18n/fr.ts +2 -2
  157. package/src/tool/steamCapsuleGenerator/i18n/pl.ts +13 -0
  158. package/src/tool/steamCapsuleGenerator/i18n/ru.ts +26 -0
  159. package/src/tool/steamCapsuleGenerator/i18n/sv.ts +1 -0
  160. package/src/tool/steamCapsuleGenerator/i18n/tr.ts +1 -0
  161. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-games-development",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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];
@@ -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('twelve tools are registered', () => {
6
- expect(ALL_TOOLS.length).toBe(12);
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 match EN (${expected.seo})`,
30
- ).toBe(expected.seo);
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 12 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(12);
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
- const locales = [...corpora.keys()];
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: [
@@ -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',
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
172
172
  title: 'Анализируйте урон и количество ударов одновременно',
173
173
  html: 'Небольшое изменение урона может пересечь пороговое значение здоровья и сократить число ударов. Всегда сравнивайте урон с TTK.',
174
174
  },
175
+ { type: 'paragraph', html: 'Одна и та же формула может дать другой бой из-за округления и порядка модификаторов. Проверьте также малый урон, высокую защиту и порог дополнительного удара; TTK не моделирует уклонения, перезарядки и прерывания.' },
176
+ { type: 'paragraph', html: 'После выбора формулы сохраните несколько контрольных сценариев: слабая атака против высокой защиты, критический удар и граница, на которой меняется число попаданий. Такие примеры помогают сравнить версии баланса и заметить регрессию после изменения коэффициентов или правил округления.' },
175
177
  ],
176
178
  faq,
177
179
  bibliographyTitle: 'Ссылки по расчету урона в играх',
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
172
172
  title: 'Granska skada och antal träffar tillsammans',
173
173
  html: 'En liten förändring i skada kan passera en hälsotröskel och minska antal träffar med en hel attack. Jämför alltid skada med TTK.',
174
174
  },
175
+ { type: 'paragraph', html: 'Samma formel kan ge ett annat stridsresultat beroende på avrundning och modifierarnas ordning. Kontrollera även låg skada, högt försvar och tröskeln för en extra träff; TTK modellerar inte undvikanden, cooldowns eller avbrott.' },
176
+ { type: 'paragraph', html: 'Spara flera kontrollscenarier efter varje ändring: svag attack mot högt försvar, kritisk träff och gränsen där antalet träffar ändras. Dessa exempel gör det lättare att jämföra balansversioner och upptäcka regressioner efter att koefficienter eller avrundningsregler har ändrats.' },
175
177
  ],
176
178
  faq,
177
179
  bibliographyTitle: 'Referenser för skadeberäkning i spel',
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
172
172
  title: 'Hasar ve vuruş sayısını daima birlikte inceleyin',
173
173
  html: 'Hasardaki küçük bir değişim bir can eşiğini geçerek vuruş sayısını 1 azaltabilir. Hasarı daima TTK ile karşılaştırın.',
174
174
  },
175
+ { type: 'paragraph', html: 'Aynı formül yuvarlama ve modifier sırasına göre farklı bir savaş sonucu üretebilir. Küçük hasarı, yüksek savunmayı ve ek vuruş gerektiren eşiği de kontrol edin; TTK kaçınma, bekleme süresi veya kesintileri modellemez.' },
176
+ { type: 'paragraph', html: 'Her değişiklikten sonra birkaç kontrol senaryosu kaydedin: yüksek savunmaya karşı zayıf saldırı, kritik vuruş ve vuruş sayısının değiştiği eşik. Bu örnekler denge sürümlerini karşılaştırmayı ve katsayılar ya da yuvarlama kuralları değiştiğinde oluşan gerilemeleri fark etmeyi kolaylaştırır.' },
175
177
  ],
176
178
  faq,
177
179
  bibliographyTitle: 'Oyunlarda hasar matematiği referansları',
@@ -172,6 +172,8 @@ export const content: ToolLocaleContent<DamageFormulaLabUI> = {
172
172
  title: '同时观察伤害数值与击杀次数',
173
173
  html: '伤害数值的微小变化可能会跨越血量阈值,从而使所需攻击次数减少一次。请务必结合 TTK 进行综合评估。',
174
174
  },
175
+ { type: 'paragraph', html: '同一公式会因舍入方式和修正顺序不同而产生不同的战斗结果。还要检查低伤害、高防御以及需要额外攻击的临界点;TTK 不模拟闪避、冷却或中断。' },
176
+ { type: 'paragraph', html: '每次修改后请保存几组基准场景,包括低攻击对高防御、暴击命中,以及攻击次数发生变化的临界值。这些案例有助于比较不同平衡版本,也能在系数或舍入规则调整后及时发现回归问题。' },
175
177
  ],
176
178
  faq,
177
179
  bibliographyTitle: '游戏伤害数值设计参考资料',
@@ -0,0 +1,22 @@
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 && <section class="gll-references" aria-labelledby="gll-reference-title"><h2 id="gll-reference-title">{title}</h2><ul>{entries.map((entry) => <li><a href={entry.url} rel="noopener noreferrer" target="_blank">{entry.name}</a></li>)}</ul></section>}
13
+
14
+ <style>
15
+ .gll-references {
16
+ margin-block: 2rem;
17
+ }
18
+
19
+ .gll-references a {
20
+ color: var(--n-primary);
21
+ }
22
+ </style>
@@ -0,0 +1,10 @@
1
+ export const bibliographyEntries = [
2
+ {
3
+ name: 'Fix Your Timestep! by Glenn Fiedler',
4
+ url: 'https://gafferongames.com/post/fix_your_timestep/',
5
+ },
6
+ {
7
+ name: 'Game Loop from Game Programming Patterns',
8
+ url: 'https://gameprogrammingpatterns.com/game-loop.html',
9
+ },
10
+ ];