@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.
- package/package.json +1 -1
- package/src/category/TabletopCategorySEO.astro +2 -7
- package/src/category/i18n/en.ts +4 -4
- package/src/category/i18n/ru.ts +2 -2
- package/src/category/i18n/zh.ts +8 -8
- package/src/category/index.ts +2 -1
- package/src/entries.ts +14 -1
- package/src/index.ts +1 -0
- package/src/pages/[locale]/[slug].astro +7 -6
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/no_em_dash.test.ts +47 -0
- package/src/tests/seo_length.test.ts +1 -0
- package/src/tests/title_quality.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/board-game-timer/modules/ColorHelper.ts +2 -2
- package/src/tool/board-game-timer/modules/DuelViewManager.ts +4 -4
- package/src/tool/board-game-timer/modules/GameRunner.ts +1 -1
- package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +2 -2
- package/src/tool/board-game-timer/modules/SetupPanelManager.ts +1 -1
- package/src/tool/board-game-timer/modules/TimerEngine.ts +1 -1
- package/src/tool/board-game-timer/types.ts +1 -1
- package/src/tool/decision-wheel/audio.ts +78 -0
- package/src/tool/decision-wheel/bibliography.astro +16 -0
- package/src/tool/decision-wheel/bibliography.ts +8 -0
- package/src/tool/decision-wheel/client.ts +226 -0
- package/src/tool/decision-wheel/component.astro +48 -0
- package/src/tool/decision-wheel/components/SegmentEditor.astro +22 -0
- package/src/tool/decision-wheel/components/SpinHistory.astro +17 -0
- package/src/tool/decision-wheel/components/SpinResult.astro +13 -0
- package/src/tool/decision-wheel/components/WheelCanvas.astro +23 -0
- package/src/tool/decision-wheel/decision-wheel.css +632 -0
- package/src/tool/decision-wheel/drawing.ts +144 -0
- package/src/tool/decision-wheel/entry.ts +48 -0
- package/src/tool/decision-wheel/i18n/de.ts +97 -0
- package/src/tool/decision-wheel/i18n/en.ts +251 -0
- package/src/tool/decision-wheel/i18n/es.ts +93 -0
- package/src/tool/decision-wheel/i18n/fr.ts +93 -0
- package/src/tool/decision-wheel/i18n/id.ts +93 -0
- package/src/tool/decision-wheel/i18n/it.ts +93 -0
- package/src/tool/decision-wheel/i18n/ja.ts +93 -0
- package/src/tool/decision-wheel/i18n/ko.ts +93 -0
- package/src/tool/decision-wheel/i18n/nl.ts +93 -0
- package/src/tool/decision-wheel/i18n/pl.ts +93 -0
- package/src/tool/decision-wheel/i18n/pt.ts +93 -0
- package/src/tool/decision-wheel/i18n/ru.ts +75 -0
- package/src/tool/decision-wheel/i18n/sv.ts +93 -0
- package/src/tool/decision-wheel/i18n/tr.ts +93 -0
- package/src/tool/decision-wheel/i18n/zh.ts +93 -0
- package/src/tool/decision-wheel/index.ts +9 -0
- package/src/tool/decision-wheel/logic.test.ts +163 -0
- package/src/tool/decision-wheel/logic.ts +145 -0
- package/src/tool/decision-wheel/seo.astro +16 -0
- package/src/tool/decision-wheel/types.ts +14 -0
- package/src/tool/decision-wheel/ui.ts +121 -0
- package/src/tool/dice-roller-simulator/client.ts +2 -2
- package/src/tool/dice-roller-simulator/i18n/ru.ts +8 -8
- package/src/tool/dice-roller-simulator/modules/history.ts +1 -1
- package/src/tool/fantasy-runes-translator/bibliography.astro +16 -0
- package/src/tool/fantasy-runes-translator/bibliography.ts +16 -0
- package/src/tool/fantasy-runes-translator/client.ts +205 -0
- package/src/tool/fantasy-runes-translator/component.astro +95 -0
- package/src/tool/fantasy-runes-translator/entry.ts +56 -0
- package/src/tool/fantasy-runes-translator/fantasy-runes-translator.css +712 -0
- package/src/tool/fantasy-runes-translator/i18n/de.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/en.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/es.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/fr.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/id.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/it.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/ja.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/ko.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/nl.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/pl.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/pt.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/ru.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/sv.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/tr.ts +139 -0
- package/src/tool/fantasy-runes-translator/i18n/zh.ts +139 -0
- package/src/tool/fantasy-runes-translator/index.ts +9 -0
- package/src/tool/fantasy-runes-translator/logic.ts +174 -0
- package/src/tool/fantasy-runes-translator/seo.astro +16 -0
- package/src/tool/fantasy-runes-translator/types.ts +26 -0
- package/src/tool/first-player-selector/bibliography.astro +16 -0
- package/src/tool/first-player-selector/bibliography.ts +12 -0
- package/src/tool/first-player-selector/client.ts +197 -0
- package/src/tool/first-player-selector/component.astro +77 -0
- package/src/tool/first-player-selector/entry.ts +42 -0
- package/src/tool/first-player-selector/first-player-selector.css +303 -0
- package/src/tool/first-player-selector/i18n/de.ts +226 -0
- package/src/tool/first-player-selector/i18n/en.ts +226 -0
- package/src/tool/first-player-selector/i18n/es.ts +226 -0
- package/src/tool/first-player-selector/i18n/fr.ts +227 -0
- package/src/tool/first-player-selector/i18n/id.ts +226 -0
- package/src/tool/first-player-selector/i18n/it.ts +227 -0
- package/src/tool/first-player-selector/i18n/ja.ts +227 -0
- package/src/tool/first-player-selector/i18n/ko.ts +227 -0
- package/src/tool/first-player-selector/i18n/nl.ts +226 -0
- package/src/tool/first-player-selector/i18n/pl.ts +227 -0
- package/src/tool/first-player-selector/i18n/pt.ts +226 -0
- package/src/tool/first-player-selector/i18n/ru.ts +227 -0
- package/src/tool/first-player-selector/i18n/sv.ts +226 -0
- package/src/tool/first-player-selector/i18n/tr.ts +226 -0
- package/src/tool/first-player-selector/i18n/zh.ts +227 -0
- package/src/tool/first-player-selector/index.ts +9 -0
- package/src/tool/first-player-selector/modules/AnimationEffects.ts +256 -0
- package/src/tool/first-player-selector/modules/SelectionEngine.ts +249 -0
- package/src/tool/first-player-selector/modules/SoundEffects.ts +87 -0
- package/src/tool/first-player-selector/modules/TouchTracker.ts +148 -0
- package/src/tool/first-player-selector/seo.astro +16 -0
- package/src/tool/first-player-selector/types.ts +21 -0
- package/src/tool/rpg-initiative-tracker/bibliography.astro +16 -0
- package/src/tool/rpg-initiative-tracker/bibliography.ts +12 -0
- package/src/tool/rpg-initiative-tracker/client.ts +201 -0
- package/src/tool/rpg-initiative-tracker/component.astro +23 -0
- package/src/tool/rpg-initiative-tracker/components/AddForm.astro +49 -0
- package/src/tool/rpg-initiative-tracker/components/CombatantList.astro +16 -0
- package/src/tool/rpg-initiative-tracker/components/TurnControls.astro +28 -0
- package/src/tool/rpg-initiative-tracker/entry.ts +50 -0
- package/src/tool/rpg-initiative-tracker/i18n/de.ts +266 -0
- package/src/tool/rpg-initiative-tracker/i18n/en.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/es.ts +264 -0
- package/src/tool/rpg-initiative-tracker/i18n/fr.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/id.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/it.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/ja.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/ko.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/nl.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/pl.ts +264 -0
- package/src/tool/rpg-initiative-tracker/i18n/pt.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/ru.ts +264 -0
- package/src/tool/rpg-initiative-tracker/i18n/sv.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/tr.ts +263 -0
- package/src/tool/rpg-initiative-tracker/i18n/zh.ts +263 -0
- package/src/tool/rpg-initiative-tracker/index.ts +9 -0
- package/src/tool/rpg-initiative-tracker/logic.test.ts +121 -0
- package/src/tool/rpg-initiative-tracker/logic.ts +122 -0
- package/src/tool/rpg-initiative-tracker/rpg-initiative-tracker.css +617 -0
- package/src/tool/rpg-initiative-tracker/seo.astro +16 -0
- package/src/tool/rpg-initiative-tracker/types.ts +15 -0
- package/src/tool/score-tracker/bibliography.astro +16 -0
- package/src/tool/score-tracker/bibliography.ts +12 -0
- package/src/tool/score-tracker/client.ts +248 -0
- package/src/tool/score-tracker/component.astro +34 -0
- package/src/tool/score-tracker/components/Leaderboard.astro +19 -0
- package/src/tool/score-tracker/components/PlayerSetup.astro +32 -0
- package/src/tool/score-tracker/components/RoundScoring.astro +22 -0
- package/src/tool/score-tracker/components/ScoreHistory.astro +25 -0
- package/src/tool/score-tracker/entry.ts +57 -0
- package/src/tool/score-tracker/i18n/de.ts +278 -0
- package/src/tool/score-tracker/i18n/en.ts +278 -0
- package/src/tool/score-tracker/i18n/es.ts +251 -0
- package/src/tool/score-tracker/i18n/fr.ts +278 -0
- package/src/tool/score-tracker/i18n/id.ts +278 -0
- package/src/tool/score-tracker/i18n/it.ts +278 -0
- package/src/tool/score-tracker/i18n/ja.ts +278 -0
- package/src/tool/score-tracker/i18n/ko.ts +278 -0
- package/src/tool/score-tracker/i18n/nl.ts +278 -0
- package/src/tool/score-tracker/i18n/pl.ts +278 -0
- package/src/tool/score-tracker/i18n/pt.ts +278 -0
- package/src/tool/score-tracker/i18n/ru.ts +278 -0
- package/src/tool/score-tracker/i18n/sv.ts +278 -0
- package/src/tool/score-tracker/i18n/tr.ts +278 -0
- package/src/tool/score-tracker/i18n/zh.ts +278 -0
- package/src/tool/score-tracker/index.ts +9 -0
- package/src/tool/score-tracker/logic.test.ts +83 -0
- package/src/tool/score-tracker/logic.ts +122 -0
- package/src/tool/score-tracker/score-tracker.css +1029 -0
- package/src/tool/score-tracker/seo.astro +16 -0
- package/src/tool/score-tracker/types.ts +17 -0
- package/src/tools.ts +10 -0
- package/src/types.ts +1 -2
package/package.json
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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 }} />}
|
package/src/category/i18n/en.ts
CHANGED
|
@@ -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 instantly
|
|
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 style
|
|
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 TV
|
|
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' },
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -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: 'Одно из главных преимуществ этой библиотеки
|
|
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' },
|
package/src/category/i18n/zh.ts
CHANGED
|
@@ -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
|
|
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: '桌游工具 |
|
|
13
|
-
{ type: 'paragraph', html: '
|
|
14
|
-
{ type: 'title', text: '
|
|
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: '
|
|
17
|
-
{ type: 'paragraph', html: '
|
|
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' },
|
package/src/category/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
|
@@ -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(
|
|
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
|
|
9
|
-
return
|
|
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).
|
|
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
|
|
|
@@ -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} />}
|