@jjlmoya/utils-tabletop 1.3.0 → 1.5.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 (171) hide show
  1. package/package.json +1 -1
  2. package/src/category/TabletopCategorySEO.astro +2 -7
  3. package/src/category/i18n/en.ts +4 -4
  4. package/src/category/i18n/ru.ts +2 -2
  5. package/src/category/i18n/zh.ts +8 -8
  6. package/src/category/index.ts +2 -1
  7. package/src/entries.ts +14 -1
  8. package/src/index.ts +1 -0
  9. package/src/pages/[locale]/[slug].astro +7 -6
  10. package/src/tests/locale_completeness.test.ts +1 -1
  11. package/src/tests/no_em_dash.test.ts +47 -0
  12. package/src/tests/seo_length.test.ts +1 -0
  13. package/src/tests/title_quality.test.ts +1 -1
  14. package/src/tests/tool_validation.test.ts +1 -1
  15. package/src/tool/board-game-timer/modules/ColorHelper.ts +2 -2
  16. package/src/tool/board-game-timer/modules/DuelViewManager.ts +4 -4
  17. package/src/tool/board-game-timer/modules/GameRunner.ts +1 -1
  18. package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +2 -2
  19. package/src/tool/board-game-timer/modules/SetupPanelManager.ts +1 -1
  20. package/src/tool/board-game-timer/modules/TimerEngine.ts +1 -1
  21. package/src/tool/board-game-timer/types.ts +1 -1
  22. package/src/tool/decision-wheel/audio.ts +78 -0
  23. package/src/tool/decision-wheel/bibliography.astro +16 -0
  24. package/src/tool/decision-wheel/bibliography.ts +8 -0
  25. package/src/tool/decision-wheel/client.ts +226 -0
  26. package/src/tool/decision-wheel/component.astro +48 -0
  27. package/src/tool/decision-wheel/components/SegmentEditor.astro +22 -0
  28. package/src/tool/decision-wheel/components/SpinHistory.astro +17 -0
  29. package/src/tool/decision-wheel/components/SpinResult.astro +13 -0
  30. package/src/tool/decision-wheel/components/WheelCanvas.astro +23 -0
  31. package/src/tool/decision-wheel/decision-wheel.css +632 -0
  32. package/src/tool/decision-wheel/drawing.ts +144 -0
  33. package/src/tool/decision-wheel/entry.ts +48 -0
  34. package/src/tool/decision-wheel/i18n/de.ts +97 -0
  35. package/src/tool/decision-wheel/i18n/en.ts +251 -0
  36. package/src/tool/decision-wheel/i18n/es.ts +93 -0
  37. package/src/tool/decision-wheel/i18n/fr.ts +93 -0
  38. package/src/tool/decision-wheel/i18n/id.ts +93 -0
  39. package/src/tool/decision-wheel/i18n/it.ts +93 -0
  40. package/src/tool/decision-wheel/i18n/ja.ts +93 -0
  41. package/src/tool/decision-wheel/i18n/ko.ts +93 -0
  42. package/src/tool/decision-wheel/i18n/nl.ts +93 -0
  43. package/src/tool/decision-wheel/i18n/pl.ts +93 -0
  44. package/src/tool/decision-wheel/i18n/pt.ts +93 -0
  45. package/src/tool/decision-wheel/i18n/ru.ts +75 -0
  46. package/src/tool/decision-wheel/i18n/sv.ts +93 -0
  47. package/src/tool/decision-wheel/i18n/tr.ts +93 -0
  48. package/src/tool/decision-wheel/i18n/zh.ts +93 -0
  49. package/src/tool/decision-wheel/index.ts +9 -0
  50. package/src/tool/decision-wheel/logic.test.ts +163 -0
  51. package/src/tool/decision-wheel/logic.ts +145 -0
  52. package/src/tool/decision-wheel/seo.astro +16 -0
  53. package/src/tool/decision-wheel/types.ts +14 -0
  54. package/src/tool/decision-wheel/ui.ts +121 -0
  55. package/src/tool/dice-roller-simulator/client.ts +2 -2
  56. package/src/tool/dice-roller-simulator/i18n/ru.ts +8 -8
  57. package/src/tool/dice-roller-simulator/modules/history.ts +1 -1
  58. package/src/tool/fantasy-runes-translator/bibliography.astro +16 -0
  59. package/src/tool/fantasy-runes-translator/bibliography.ts +16 -0
  60. package/src/tool/fantasy-runes-translator/client.ts +205 -0
  61. package/src/tool/fantasy-runes-translator/component.astro +95 -0
  62. package/src/tool/fantasy-runes-translator/entry.ts +56 -0
  63. package/src/tool/fantasy-runes-translator/fantasy-runes-translator.css +712 -0
  64. package/src/tool/fantasy-runes-translator/i18n/de.ts +139 -0
  65. package/src/tool/fantasy-runes-translator/i18n/en.ts +139 -0
  66. package/src/tool/fantasy-runes-translator/i18n/es.ts +139 -0
  67. package/src/tool/fantasy-runes-translator/i18n/fr.ts +139 -0
  68. package/src/tool/fantasy-runes-translator/i18n/id.ts +139 -0
  69. package/src/tool/fantasy-runes-translator/i18n/it.ts +139 -0
  70. package/src/tool/fantasy-runes-translator/i18n/ja.ts +139 -0
  71. package/src/tool/fantasy-runes-translator/i18n/ko.ts +139 -0
  72. package/src/tool/fantasy-runes-translator/i18n/nl.ts +139 -0
  73. package/src/tool/fantasy-runes-translator/i18n/pl.ts +139 -0
  74. package/src/tool/fantasy-runes-translator/i18n/pt.ts +139 -0
  75. package/src/tool/fantasy-runes-translator/i18n/ru.ts +139 -0
  76. package/src/tool/fantasy-runes-translator/i18n/sv.ts +139 -0
  77. package/src/tool/fantasy-runes-translator/i18n/tr.ts +139 -0
  78. package/src/tool/fantasy-runes-translator/i18n/zh.ts +139 -0
  79. package/src/tool/fantasy-runes-translator/index.ts +9 -0
  80. package/src/tool/fantasy-runes-translator/logic.ts +174 -0
  81. package/src/tool/fantasy-runes-translator/seo.astro +16 -0
  82. package/src/tool/fantasy-runes-translator/types.ts +26 -0
  83. package/src/tool/first-player-selector/bibliography.astro +16 -0
  84. package/src/tool/first-player-selector/bibliography.ts +12 -0
  85. package/src/tool/first-player-selector/client.ts +197 -0
  86. package/src/tool/first-player-selector/component.astro +77 -0
  87. package/src/tool/first-player-selector/entry.ts +42 -0
  88. package/src/tool/first-player-selector/first-player-selector.css +303 -0
  89. package/src/tool/first-player-selector/i18n/de.ts +226 -0
  90. package/src/tool/first-player-selector/i18n/en.ts +226 -0
  91. package/src/tool/first-player-selector/i18n/es.ts +226 -0
  92. package/src/tool/first-player-selector/i18n/fr.ts +227 -0
  93. package/src/tool/first-player-selector/i18n/id.ts +226 -0
  94. package/src/tool/first-player-selector/i18n/it.ts +227 -0
  95. package/src/tool/first-player-selector/i18n/ja.ts +227 -0
  96. package/src/tool/first-player-selector/i18n/ko.ts +227 -0
  97. package/src/tool/first-player-selector/i18n/nl.ts +226 -0
  98. package/src/tool/first-player-selector/i18n/pl.ts +227 -0
  99. package/src/tool/first-player-selector/i18n/pt.ts +226 -0
  100. package/src/tool/first-player-selector/i18n/ru.ts +227 -0
  101. package/src/tool/first-player-selector/i18n/sv.ts +226 -0
  102. package/src/tool/first-player-selector/i18n/tr.ts +226 -0
  103. package/src/tool/first-player-selector/i18n/zh.ts +227 -0
  104. package/src/tool/first-player-selector/index.ts +9 -0
  105. package/src/tool/first-player-selector/modules/AnimationEffects.ts +256 -0
  106. package/src/tool/first-player-selector/modules/SelectionEngine.ts +249 -0
  107. package/src/tool/first-player-selector/modules/SoundEffects.ts +87 -0
  108. package/src/tool/first-player-selector/modules/TouchTracker.ts +148 -0
  109. package/src/tool/first-player-selector/seo.astro +16 -0
  110. package/src/tool/first-player-selector/types.ts +21 -0
  111. package/src/tool/rpg-initiative-tracker/bibliography.astro +16 -0
  112. package/src/tool/rpg-initiative-tracker/bibliography.ts +12 -0
  113. package/src/tool/rpg-initiative-tracker/client.ts +201 -0
  114. package/src/tool/rpg-initiative-tracker/component.astro +23 -0
  115. package/src/tool/rpg-initiative-tracker/components/AddForm.astro +49 -0
  116. package/src/tool/rpg-initiative-tracker/components/CombatantList.astro +16 -0
  117. package/src/tool/rpg-initiative-tracker/components/TurnControls.astro +28 -0
  118. package/src/tool/rpg-initiative-tracker/entry.ts +50 -0
  119. package/src/tool/rpg-initiative-tracker/i18n/de.ts +266 -0
  120. package/src/tool/rpg-initiative-tracker/i18n/en.ts +263 -0
  121. package/src/tool/rpg-initiative-tracker/i18n/es.ts +264 -0
  122. package/src/tool/rpg-initiative-tracker/i18n/fr.ts +263 -0
  123. package/src/tool/rpg-initiative-tracker/i18n/id.ts +263 -0
  124. package/src/tool/rpg-initiative-tracker/i18n/it.ts +263 -0
  125. package/src/tool/rpg-initiative-tracker/i18n/ja.ts +263 -0
  126. package/src/tool/rpg-initiative-tracker/i18n/ko.ts +263 -0
  127. package/src/tool/rpg-initiative-tracker/i18n/nl.ts +263 -0
  128. package/src/tool/rpg-initiative-tracker/i18n/pl.ts +264 -0
  129. package/src/tool/rpg-initiative-tracker/i18n/pt.ts +263 -0
  130. package/src/tool/rpg-initiative-tracker/i18n/ru.ts +264 -0
  131. package/src/tool/rpg-initiative-tracker/i18n/sv.ts +263 -0
  132. package/src/tool/rpg-initiative-tracker/i18n/tr.ts +263 -0
  133. package/src/tool/rpg-initiative-tracker/i18n/zh.ts +263 -0
  134. package/src/tool/rpg-initiative-tracker/index.ts +9 -0
  135. package/src/tool/rpg-initiative-tracker/logic.test.ts +121 -0
  136. package/src/tool/rpg-initiative-tracker/logic.ts +122 -0
  137. package/src/tool/rpg-initiative-tracker/rpg-initiative-tracker.css +617 -0
  138. package/src/tool/rpg-initiative-tracker/seo.astro +16 -0
  139. package/src/tool/rpg-initiative-tracker/types.ts +15 -0
  140. package/src/tool/score-tracker/bibliography.astro +16 -0
  141. package/src/tool/score-tracker/bibliography.ts +12 -0
  142. package/src/tool/score-tracker/client.ts +248 -0
  143. package/src/tool/score-tracker/component.astro +34 -0
  144. package/src/tool/score-tracker/components/Leaderboard.astro +19 -0
  145. package/src/tool/score-tracker/components/PlayerSetup.astro +32 -0
  146. package/src/tool/score-tracker/components/RoundScoring.astro +22 -0
  147. package/src/tool/score-tracker/components/ScoreHistory.astro +25 -0
  148. package/src/tool/score-tracker/entry.ts +57 -0
  149. package/src/tool/score-tracker/i18n/de.ts +278 -0
  150. package/src/tool/score-tracker/i18n/en.ts +278 -0
  151. package/src/tool/score-tracker/i18n/es.ts +251 -0
  152. package/src/tool/score-tracker/i18n/fr.ts +278 -0
  153. package/src/tool/score-tracker/i18n/id.ts +278 -0
  154. package/src/tool/score-tracker/i18n/it.ts +278 -0
  155. package/src/tool/score-tracker/i18n/ja.ts +278 -0
  156. package/src/tool/score-tracker/i18n/ko.ts +278 -0
  157. package/src/tool/score-tracker/i18n/nl.ts +278 -0
  158. package/src/tool/score-tracker/i18n/pl.ts +278 -0
  159. package/src/tool/score-tracker/i18n/pt.ts +278 -0
  160. package/src/tool/score-tracker/i18n/ru.ts +278 -0
  161. package/src/tool/score-tracker/i18n/sv.ts +278 -0
  162. package/src/tool/score-tracker/i18n/tr.ts +278 -0
  163. package/src/tool/score-tracker/i18n/zh.ts +278 -0
  164. package/src/tool/score-tracker/index.ts +9 -0
  165. package/src/tool/score-tracker/logic.test.ts +83 -0
  166. package/src/tool/score-tracker/logic.ts +122 -0
  167. package/src/tool/score-tracker/score-tracker.css +1029 -0
  168. package/src/tool/score-tracker/seo.astro +16 -0
  169. package/src/tool/score-tracker/types.ts +17 -0
  170. package/src/tools.ts +10 -0
  171. package/src/types.ts +1 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-tabletop",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,14 +1,9 @@
1
1
  ---
2
2
  import { SEORenderer } from '@jjlmoya/utils-shared';
3
3
  import { tabletopCategory } from './index';
4
- import type { KnownLocale } from '../types';
5
4
 
6
- interface Props {
7
- locale?: string;
8
- }
9
-
10
- const { locale = 'es' } = Astro.props;
11
- const categoryContent = await tabletopCategory.i18n[locale as KnownLocale]?.();
5
+ const locale: string = Astro.props.locale ?? 'es';
6
+ const categoryContent = await tabletopCategory.i18n[locale as keyof typeof tabletopCategory.i18n]?.();
12
7
  ---
13
8
 
14
9
  {categoryContent && <SEORenderer content={{ locale, sections: categoryContent.seo }} />}
@@ -1,4 +1,4 @@
1
- import type { CategoryLocaleContent } from '../../types';
1
+ import type { CategoryLocaleContent } from '../../types';
2
2
 
3
3
  const slug = 'tabletop';
4
4
  const title = 'Tabletop Game Utilities & Tools';
@@ -10,13 +10,13 @@ export const content: CategoryLocaleContent = {
10
10
  description,
11
11
  seo: [
12
12
  { type: 'title', text: 'Tabletop Game Tools | Roll Dice Online & Track Turn Times', level: 2 },
13
- { type: 'paragraph', html: 'Game nights should be about fun, not hunting for lost dice or waiting for someone to add up their roll. This utility library gives you and your group fast, well-designed tools that work instantlyno strings attached. Whether you are a dungeon master juggling a hundred things or a player who just wants to roll without the fuss, these tools are built for you.' },
13
+ { type: 'paragraph', html: 'Game nights should be about fun, not hunting for lost dice or waiting for someone to add up their roll. This utility library gives you and your group fast, well-designed tools that work instantly-no strings attached. Whether you are a dungeon master juggling a hundred things or a player who just wants to roll without the fuss, these tools are built for you.' },
14
14
  { type: 'title', text: 'Roll Virtual Dice with Real-Time Statistics', level: 2 },
15
15
  { type: 'paragraph', html: 'Tired of scrambling under the table for missing dice or waiting for someone to tally results? The virtual dice roller lets you throw any combination of polyhedral dice instantly, with modifiers, advantage, disadvantage, and a full roll history. The built-in probability analyzer shows your real odds before you roll, helping you make smarter decisions at the table. Perfect for Dungeons & Dragons, Pathfinder, Call of Cthulhu, Shadowrun, or any game that relies on chance.' },
16
16
  { type: 'title', text: 'Keep Your Game on Pace with a Smart Turn Timer', level: 2 },
17
- { type: 'paragraph', html: 'We all know that one player who takes forever on every turn. The game timer lets you set fair and fun time limits, with modes that adapt to every play stylefrom a simple shared countdown to chess-style Fischer increment or Bronstein delay. Works for two-player duels with split-screen rotation or groups of up to eight with central turn control. When the game ends, review detailed stats on timing, rounds, and play pace.' },
17
+ { type: 'paragraph', html: 'We all know that one player who takes forever on every turn. The game timer lets you set fair and fun time limits, with modes that adapt to every play style-from a simple shared countdown to chess-style Fischer increment or Bronstein delay. Works for two-player duels with split-screen rotation or groups of up to eight with central turn control. When the game ends, review detailed stats on timing, rounds, and play pace.' },
18
18
  { type: 'title', text: 'Privacy First: Everything Runs on Your Device', level: 2 },
19
- { type: 'paragraph', html: 'One of the best things about this library is that everything processes in your browser. No data is sent to any server, no tracking cookies, no email or registration required. Use the tools offline, share your screen with the table, or project to a big screen TVnothing to worry about except the game itself. And more tools are coming, always designed to make life easier for tabletop players.' },
19
+ { type: 'paragraph', html: 'One of the best things about this library is that everything processes in your browser. No data is sent to any server, no tracking cookies, no email or registration required. Use the tools offline, share your screen with the table, or project to a big screen TV-nothing to worry about except the game itself. And more tools are coming, always designed to make life easier for tabletop players.' },
20
20
  { type: 'stats', items: [
21
21
  { label: 'Tools', value: '2+', icon: 'mdi:tools' },
22
22
  { label: 'Players', value: '2-8', icon: 'mdi:account-group' },
@@ -1,4 +1,4 @@
1
- import type { CategoryLocaleContent } from '../../types';
1
+ import type { CategoryLocaleContent } from '../../types';
2
2
 
3
3
  const slug = 'nastolnye-igry';
4
4
  const title = 'Инструменты и утилиты для настольных игр';
@@ -16,7 +16,7 @@ export const content: CategoryLocaleContent = {
16
16
  { type: 'title', text: 'Контролируйте темп игры с умным таймером ходов', level: 2 },
17
17
  { type: 'paragraph', html: 'Все мы знаем того игрока, который тратит вечность на каждый ход. Игровой таймер позволяет устанавливать справедливые и увлекательные лимиты времени с режимами на любой стиль: от простого общего отсчёта до шахматных систем с инкрементом Фишера или задержкой Бронштейна. Работает для дуэлей с разделённым экраном или групп до восьми игроков с централизованным управлением. После игры доступна подробная статистика времени и темпа.' },
18
18
  { type: 'title', text: 'Конфиденциальность превыше всего: все данные остаются на вашем устройстве', level: 2 },
19
- { type: 'paragraph', html: 'Одно из главных преимуществ этой библиотеки вся обработка происходит в вашем браузере. Никакие данные не отправляются на сервер, нет отслеживающих cookie, не требуется email или регистрация. Используйте инструменты офлайн, делитесь экраном с игроками или выводите на телевизор ни о чём, кроме игры, беспокоиться не придётся. А новые инструменты уже в разработке, всегда нацеленные на то, чтобы облегчить жизнь любителям настольных игр.' },
19
+ { type: 'paragraph', html: 'Одно из главных преимуществ этой библиотеки - вся обработка происходит в вашем браузере. Никакие данные не отправляются на сервер, нет отслеживающих cookie, не требуется email или регистрация. Используйте инструменты офлайн, делитесь экраном с игроками или выводите на телевизор - ни о чём, кроме игры, беспокоиться не придётся. А новые инструменты уже в разработке, всегда нацеленные на то, чтобы облегчить жизнь любителям настольных игр.' },
20
20
  { type: 'stats', items: [
21
21
  { label: 'Инструментов', value: '2+', icon: 'mdi:tools' },
22
22
  { label: 'Игроков', value: '2-8', icon: 'mdi:account-group' },
@@ -1,22 +1,22 @@
1
- import type { CategoryLocaleContent } from '../../types';
1
+ import type { CategoryLocaleContent } from '../../types';
2
2
 
3
3
  const slug = 'tabletop';
4
4
  const title = '桌游工具与实用程序';
5
- const description = '免费的桌游和TRPG工具:虚拟骰子带实时统计,多人回合计时器等。在浏览器中运行,无需注册,100%隐私保护。';
5
+ const description = '免费的桌游和TRPG工具:虚拟骰子带实�-�统计,多人回合计�-�器等。在浏览器中运行,�-�需注册,100%隐私保护。';
6
6
 
7
7
  export const content: CategoryLocaleContent = {
8
8
  slug,
9
9
  title,
10
10
  description,
11
11
  seo: [
12
- { type: 'title', text: '桌游工具 | 在线掷骰子和回合计时器', level: 2 },
13
- { type: 'paragraph', html: '游戏之夜本该充满乐趣,而不是到处找丢失的骰子或等待别人计算结果。这款实用工具库为您和您的团队提供快速、精心设计的工具,即刻可用,无需任何回报。无论您是手忙脚乱的 dungeon master,还是只想轻松掷骰的玩家,这些工具都是为您打造的。' },
14
- { type: 'title', text: '使用虚拟骰子进行实时统计分析', level: 2 },
12
+ { type: 'title', text: '桌游工具 | 在线掷骰子和回合计�-�器', level: 2 },
13
+ { type: 'paragraph', html: '游戏之夜本该充满乐趣,而不是到处找丢失的骰子或等待别人计�-结果。这款实用工具库为您和您的团队提供快速、精心设计的工具,即刻可用,�-�需任何回报。�-�论您是手忙脚乱的 dungeon master,还是只想轻松掷骰的玩家,这些工具都是为您打造的。' },
14
+ { type: 'title', text: '使用虚拟骰子进行实�-�统计分析', level: 2 },
15
15
  { type: 'paragraph', html: '厌倦了在桌子下找骰子或等待别人加总结果?虚拟骰子掷投器让您立即投掷任何组合的多面骰子,支持调整值、优势、劣势和完整历史记录。内置概率分析器在投掷前显示您的真实成功率。非常适合 Dungeons & Dragons、Pathfinder、Call of Cthulhu、Shadowrun 或任何依赖运气的游戏。' },
16
- { type: 'title', text: '使用智能回合计时器掌控游戏节奏', level: 2 },
17
- { type: 'paragraph', html: '我们都知道那个每回合都要花上大量时间的玩家。游戏计时器让您设定公平又有趣的时间限制,提供适应各种风格的模式:从简单的共享倒计时到 Fischer 增量或 Bronstein 延迟等国际象棋系统。支持双人分屏对决和最多八人的中央控制模式。游戏结束后,可查看时间和游戏节奏的详细统计数据。' },
16
+ { type: 'title', text: '使用智能回合计�-�器掌控游戏节奏', level: 2 },
17
+ { type: 'paragraph', html: '我们都知道那个每回合都要花上大量�-��-�的玩家。游戏计�-�器让您设定公平又有趣的�-��-�限制,提供适应各种风格的模式:从简单的共享倒计�-�到 Fischer 增量或 Bronstein 延迟等国际象棋系统。支持双人分屏对决和最多八人的中央控制模式。游戏结束后,可查看�-��-�和游戏节奏的详细统计数据。' },
18
18
  { type: 'title', text: '隐私至上:所有数据保留在您的设备上', level: 2 },
19
- { type: 'paragraph', html: '该库的最大优势之一是所有处理都在您的浏览器内完成。不会有数据发送到服务器,没有跟踪 Cookie,无需电子邮件或注册。可离线使用工具,与桌上玩家共享屏幕,或投影到电视上——除了游戏本身,无需担心任何事。更多的工具正在开发中,始终以方便桌游玩家为设计目标。' },
19
+ { type: 'paragraph', html: '该库的最大优势之一是所有处理都在您的浏览器内完成。不会有数据发送到服务器,没有跟踪 Cookie,�-�需电子邮件或注册。可离线使用工具,与桌上玩家共享屏幕,或投影到电视上--除了游戏本身,�-�需担心任何事。更多的工具正在开发中,始终以方便桌游玩家为设计目标。' },
20
20
  { type: 'stats', items: [
21
21
  { label: '工具', value: '2+', icon: 'mdi:tools' },
22
22
  { label: '玩家', value: '2-8', icon: 'mdi:account-group' },
@@ -1,5 +1,6 @@
1
1
  import { diceRollerSimulator } from '../tool/dice-roller-simulator/entry';
2
2
  import { boardGameTimer } from '../tool/board-game-timer/entry';
3
+ import type { CategoryLocaleContent, KnownLocale } from '../types';
3
4
 
4
5
  export const tabletopCategory = {
5
6
  icon: 'mdi:dice-multiple-outline',
@@ -23,5 +24,5 @@ export const tabletopCategory = {
23
24
  sv: () => import('./i18n/sv').then((m) => m.content),
24
25
  tr: () => import('./i18n/tr').then((m) => m.content),
25
26
  zh: () => import('./i18n/zh').then((m) => m.content),
26
- },
27
+ } as Partial<Record<KnownLocale, () => Promise<CategoryLocaleContent>>>,
27
28
  };
package/src/entries.ts CHANGED
@@ -2,9 +2,22 @@ export { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
2
2
  export type { DiceRollerUI, DiceRollerLocaleContent } from './tool/dice-roller-simulator/entry';
3
3
  export { boardGameTimer } from './tool/board-game-timer/entry';
4
4
  export type { BoardGameTimerUI, BoardGameTimerLocaleContent } from './tool/board-game-timer/entry';
5
+ export { firstPlayerSelector } from './tool/first-player-selector/entry';
6
+ export type { FirstPlayerSelectorUI, FirstPlayerSelectorLocaleContent } from './tool/first-player-selector/entry';
7
+ export { scoreTracker } from './tool/score-tracker/entry';
8
+ export type { ScoreTrackerUI, ScoreTrackerLocaleContent } from './tool/score-tracker/entry';
9
+ export { initiativeTracker } from './tool/rpg-initiative-tracker/entry';
10
+ export type { InitiativeTrackerUI, InitiativeTrackerLocaleContent } from './tool/rpg-initiative-tracker/entry';
11
+ export { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
12
+ export type { FantasyRunesTranslatorUI, FantasyRunesTranslatorLocaleContent } from './tool/fantasy-runes-translator/entry';
5
13
  export { tabletopCategory } from './category';
6
14
 
7
15
  import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
8
16
  import { boardGameTimer } from './tool/board-game-timer/entry';
17
+ import { firstPlayerSelector } from './tool/first-player-selector/entry';
18
+ import { scoreTracker } from './tool/score-tracker/entry';
19
+ import { initiativeTracker } from './tool/rpg-initiative-tracker/entry';
20
+ import { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
21
+ import { decisionWheel } from './tool/decision-wheel/entry';
9
22
 
10
- export const ALL_ENTRIES = [diceRollerSimulator, boardGameTimer];
23
+ export const ALL_ENTRIES = [diceRollerSimulator, boardGameTimer, firstPlayerSelector, scoreTracker, initiativeTracker, fantasyRunesTranslator, decisionWheel];
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export const tabletopCategorySEO = () => import('./category/TabletopCategorySEO.
3
3
 
4
4
  export { diceRollerSimulator, DICE_ROLLER_SIMULATOR_TOOL } from './tool/dice-roller-simulator';
5
5
  export { boardGameTimer, BOARD_GAME_TIMER_TOOL } from './tool/board-game-timer';
6
+ export { fantasyRunesTranslator, FANTASY_RUNES_TRANSLATOR_TOOL } from './tool/fantasy-runes-translator';
6
7
 
7
8
  export type {
8
9
  KnownLocale,
@@ -15,7 +15,7 @@ export async function getStaticPaths() {
15
15
  const paths = [];
16
16
 
17
17
  for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
- const { default: Component } = await lazyComp();
18
+ const { default: Component } = await (lazyComp as () => Promise<{ default: unknown }>)();
19
19
  const localeEntries = Object.entries(entry.i18n) as [
20
20
  KnownLocale,
21
21
  () => Promise<ToolLocaleContent>,
@@ -70,20 +70,21 @@ interface NavItem {
70
70
  isActive?: boolean;
71
71
  }
72
72
 
73
- interface Props {
73
+ type PageProps = {
74
74
  Component: unknown;
75
75
  locale: KnownLocale;
76
76
  content: ToolLocaleContent;
77
77
  localeUrls: Partial<Record<KnownLocale, string>>;
78
78
  allToolsNav: NavItem[];
79
79
  englishSlug: string;
80
- }
80
+ };
81
81
 
82
- const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as PageProps;
83
83
 
84
- const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
84
+ const cssFiles = import.meta.glob<{ default: string }>("../../tool/*/*.css", { query: "?raw", import: "default" });
85
85
  const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
- const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
86
+ const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
87
+ const toolCss = cssLoader ? await cssLoader() as string : "";
87
88
 
88
89
  const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
89
90
 
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(2);
25
+ expect(ALL_TOOLS.length).toBe(7);
26
26
  });
27
27
 
28
28
  });
@@ -0,0 +1,47 @@
1
+ import { describe, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ function getFiles(dir: string, ext: string[]): string[] {
6
+ const results: string[] = [];
7
+ if (!fs.existsSync(dir)) return results;
8
+ const list = fs.readdirSync(dir);
9
+ for (const file of list) {
10
+ const fullPath = path.join(dir, file);
11
+ const stat = fs.statSync(fullPath);
12
+ if (stat && stat.isDirectory()) {
13
+ results.push(...getFiles(fullPath, ext));
14
+ } else if (ext.some((e) => file.endsWith(e))) {
15
+ results.push(fullPath);
16
+ }
17
+ }
18
+ return results;
19
+ }
20
+
21
+ const EM_DASH = '\u2014';
22
+
23
+ describe('Em Dash Prohibition', () => {
24
+ const files = [
25
+ ...getFiles(path.join(process.cwd(), 'src', 'tool'), ['.ts']),
26
+ ...getFiles(path.join(process.cwd(), 'src', 'category'), ['.ts']),
27
+ ].filter(f => f.includes('i18n'));
28
+
29
+ it.each(files)('should not contain em dash (—) in %s', (filePath) => {
30
+ const content = fs.readFileSync(filePath, 'utf-8');
31
+ const relativePath = path.relative(process.cwd(), filePath);
32
+
33
+ const lines = content.split('\n');
34
+ const findings: { line: number; text: string }[] = [];
35
+
36
+ lines.forEach((line, index) => {
37
+ if (line.includes(EM_DASH)) {
38
+ findings.push({ line: index + 1, text: line.trim() });
39
+ }
40
+ });
41
+
42
+ if (findings.length > 0) {
43
+ const list = findings.map((f) => ` Line ${f.line}: ${f.text}`).join('\n');
44
+ throw new Error(`Em dash (—) found in ${relativePath}:\n${list}`);
45
+ }
46
+ });
47
+ });
@@ -11,6 +11,7 @@ describe('SEO Content Length Validation', () => {
11
11
  Object.keys(entry.i18n).forEach((locale) => {
12
12
  it(`${locale}: SEO section should exist`, async () => {
13
13
  const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
14
+ if (!loader) return;
14
15
  const content = await loader();
15
16
  if (!content.seo) return;
16
17
  expect(Array.isArray(content.seo)).toBe(true);
@@ -40,7 +40,7 @@ describe('Project Titles - Separator Validation', () => {
40
40
  for (const pattern of titlePatterns) {
41
41
  let match;
42
42
  while ((match = pattern.exec(content)) !== null) {
43
- const title = match[1];
43
+ const title = match[1]!;
44
44
  if (title.includes('|') || title.includes('-')) {
45
45
  findings.push(title);
46
46
  }
@@ -5,7 +5,7 @@ import { tabletopCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(2);
8
+ expect(ALL_TOOLS.length).toBe(7);
9
9
  });
10
10
 
11
11
  it('tabletopCategory should be defined', () => {
@@ -5,8 +5,8 @@ export function getLuminance(hex: string): number {
5
5
  const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
6
6
  const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
7
7
  const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
8
- const a = [r, g, b].map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
9
- return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
8
+ const [lr, lg, lb] = [r, g, b].map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
9
+ return lr * 0.2126 + lg * 0.7152 + lb * 0.0722;
10
10
  }
11
11
 
12
12
  export function getContrastColorForBackground(colorName: PlayerColor): string {
@@ -48,8 +48,8 @@ export class DuelViewManager {
48
48
 
49
49
  public initView() {
50
50
  this.duelPanel.style.display = 'grid';
51
- const p1 = this.state.players[0];
52
- const p2 = this.state.players[1];
51
+ const p1 = this.state.players[0]!;
52
+ const p2 = this.state.players[1]!;
53
53
  const d1 = document.getElementById('duel-p1') as HTMLElement;
54
54
  const d2 = document.getElementById('duel-p2') as HTMLElement;
55
55
  d1.setAttribute('data-player-id', p1.id);
@@ -86,8 +86,8 @@ export class DuelViewManager {
86
86
  }
87
87
 
88
88
  public update() {
89
- const p1 = this.state.players[0];
90
- const p2 = this.state.players[1];
89
+ const p1 = this.state.players[0]!;
90
+ const p2 = this.state.players[1]!;
91
91
  const t1 = this.state.engine.getTimer(p1.id);
92
92
  const t2 = this.state.engine.getTimer(p2.id);
93
93
  if (!t1 || !t2) return;
@@ -45,7 +45,7 @@ export class GameRunner {
45
45
  this.state.activeMode = 'multi';
46
46
  this.multiView.initView();
47
47
  }
48
- const firstPlayer = this.state.players[0];
48
+ const firstPlayer = this.state.players[0]!;
49
49
  this.state.engine.start(firstPlayer.id);
50
50
  this.state.stats.startTurn();
51
51
  }
@@ -21,7 +21,7 @@ export class MultiplayerViewManager {
21
21
  private handleGiantButtonClick() {
22
22
  if (this.state.isPaused) return;
23
23
  this.state.sound.playTurn();
24
- const currPlayer = this.state.players[this.state.activePlayerIndex];
24
+ const currPlayer = this.state.players[this.state.activePlayerIndex]!;
25
25
  this.state.stats.endTurn(currPlayer.id, this.state.roundNumber);
26
26
  this.state.activePlayerIndex = (this.state.activePlayerIndex + 1) % this.state.players.length;
27
27
  if (this.state.activePlayerIndex === 0) {
@@ -29,7 +29,7 @@ export class MultiplayerViewManager {
29
29
  const roundEl = document.getElementById('multi-round-number');
30
30
  if (roundEl) roundEl.textContent = this.state.roundNumber.toString();
31
31
  }
32
- const nextPlayer = this.state.players[this.state.activePlayerIndex];
32
+ const nextPlayer = this.state.players[this.state.activePlayerIndex]!;
33
33
  this.state.engine.switchTurn(nextPlayer.id);
34
34
  this.state.stats.startTurn();
35
35
  this.update();
@@ -138,7 +138,7 @@ export class SetupPanelManager {
138
138
  const name = this.nameInput.value.trim();
139
139
  if (!name) return;
140
140
  this.state.players.push({
141
- id: `p-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
141
+ id: `p-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
142
142
  name,
143
143
  color: this.state.selectedColor,
144
144
  time: this.getBaseDuration()
@@ -136,7 +136,7 @@ export class TimerEngineImpl extends TimerEngineClock implements TimerEngine {
136
136
 
137
137
  public switchToNextPlayer(playerIds: string[], currentIndex: number) {
138
138
  const nextIndex = (currentIndex + 1) % playerIds.length;
139
- const nextId = playerIds[nextIndex];
139
+ const nextId = playerIds[nextIndex]!;
140
140
  return { nextId, nextIndex };
141
141
  }
142
142
 
@@ -26,7 +26,7 @@ export interface TimerConfig {
26
26
  export interface PlayerConfig {
27
27
  id: string;
28
28
  name: string;
29
- color: string;
29
+ color: PlayerColor;
30
30
  avatar?: string;
31
31
  time: number;
32
32
  }
@@ -0,0 +1,78 @@
1
+ export class SoundEngine {
2
+ private ctx: AudioContext | null = null;
3
+ private muted = false;
4
+
5
+ private initCtx(): void {
6
+ if (this.ctx) return;
7
+ const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
8
+ if (AudioContextClass) {
9
+ this.ctx = new AudioContextClass();
10
+ }
11
+ }
12
+
13
+ public setMuted(muted: boolean): void {
14
+ this.muted = muted;
15
+ }
16
+
17
+ public isMuted(): boolean {
18
+ return this.muted;
19
+ }
20
+
21
+ public playTick(): void {
22
+ if (this.muted) return;
23
+ this.initCtx();
24
+ if (!this.ctx) return;
25
+
26
+ if (this.ctx.state === 'suspended') {
27
+ this.ctx.resume();
28
+ }
29
+
30
+ const osc = this.ctx.createOscillator();
31
+ const gain = this.ctx.createGain();
32
+
33
+ osc.connect(gain);
34
+ gain.connect(this.ctx.destination);
35
+
36
+ osc.type = 'sine';
37
+ osc.frequency.setValueAtTime(800, this.ctx.currentTime);
38
+ osc.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.05);
39
+
40
+ gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
41
+ gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.05);
42
+
43
+ osc.start();
44
+ osc.stop(this.ctx.currentTime + 0.05);
45
+ }
46
+
47
+ public playChime(): void {
48
+ if (this.muted) return;
49
+ this.initCtx();
50
+ if (!this.ctx) return;
51
+
52
+ if (this.ctx.state === 'suspended') {
53
+ this.ctx.resume();
54
+ }
55
+
56
+ const notes = [523.25, 659.25, 783.99, 1046.50];
57
+ const now = this.ctx.currentTime;
58
+
59
+ notes.forEach((freq, idx) => {
60
+ if (!this.ctx) return;
61
+ const osc = this.ctx.createOscillator();
62
+ const gain = this.ctx.createGain();
63
+
64
+ osc.connect(gain);
65
+ gain.connect(this.ctx.destination);
66
+
67
+ osc.type = 'triangle';
68
+ osc.frequency.setValueAtTime(freq, now + idx * 0.08);
69
+
70
+ gain.gain.setValueAtTime(0, now + idx * 0.08);
71
+ gain.gain.linearRampToValueAtTime(0.15, now + idx * 0.08 + 0.02);
72
+ gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.08 + 0.3);
73
+
74
+ osc.start(now + idx * 0.08);
75
+ osc.stop(now + idx * 0.08 + 0.35);
76
+ });
77
+ }
78
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { decisionWheel } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props as Props;
11
+ const loader = decisionWheel.i18n[locale] || decisionWheel.i18n.en;
12
+ const content = await loader?.();
13
+ if (!content) return null;
14
+ ---
15
+
16
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,8 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Decision Wheel Spinner Guide',
6
+ url: 'https://en.wikipedia.org/wiki/Wheel_of_fortune_(game_show)',
7
+ },
8
+ ];