@jjlmoya/utils-tabletop 1.3.0 → 1.4.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 +4 -1
- 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/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/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/tools.ts +2 -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,12 @@ 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';
|
|
5
7
|
export { tabletopCategory } from './category';
|
|
6
8
|
|
|
7
9
|
import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
|
|
8
10
|
import { boardGameTimer } from './tool/board-game-timer/entry';
|
|
11
|
+
import { firstPlayerSelector } from './tool/first-player-selector/entry';
|
|
9
12
|
|
|
10
|
-
export const ALL_ENTRIES = [diceRollerSimulator, boardGameTimer];
|
|
13
|
+
export const ALL_ENTRIES = [diceRollerSimulator, boardGameTimer, firstPlayerSelector];
|
|
@@ -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(3);
|
|
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
|
|
|
@@ -185,14 +185,14 @@ function rollDiceTumble(selectedDice: { type: string; max: number }[], visualDic
|
|
|
185
185
|
return new Promise((resolve) => {
|
|
186
186
|
const intervalId = setInterval(() => {
|
|
187
187
|
visualDiceEls.forEach((el, index) => {
|
|
188
|
-
const d = selectedDice[index]
|
|
188
|
+
const d = selectedDice[index]!;
|
|
189
189
|
el.textContent = (Math.floor(Math.random() * d.max) + 1).toString();
|
|
190
190
|
});
|
|
191
191
|
}, 60);
|
|
192
192
|
setTimeout(() => {
|
|
193
193
|
clearInterval(intervalId);
|
|
194
194
|
const results = visualDiceEls.map((el, index) => {
|
|
195
|
-
const d = selectedDice[index]
|
|
195
|
+
const d = selectedDice[index]!;
|
|
196
196
|
el.classList.remove('rolling-pulse');
|
|
197
197
|
const val = Math.floor(Math.random() * d.max) + 1;
|
|
198
198
|
if (val === d.max) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bibliography } from '../bibliography';
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
3
3
|
|
|
4
4
|
export const content: ToolLocaleContent = {
|
|
@@ -20,7 +20,7 @@ export const content: ToolLocaleContent = {
|
|
|
20
20
|
},
|
|
21
21
|
seo: [
|
|
22
22
|
{ type: 'title', text: 'Управление вероятностями в настольных играх: Как математика кубиков влияет на игровой процесс', level: 2 },
|
|
23
|
-
{ type: 'paragraph', html: 'Многогранные кости
|
|
23
|
+
{ type: 'paragraph', html: 'Многогранные кости - это сердце настольных ролевых игр и современных настолок. Стремитесь ли вы выбросить высокий спасбросок в Dungeons & Dragons или бросаете кости для получения ресурсов, результат подчиняется математической вероятности. Понимание того, как сочетание различных типов кубиков влияет на ваши шансы, имеет решающее значение для принятия тактических решений во время игры.' },
|
|
24
24
|
{
|
|
25
25
|
type: 'stats',
|
|
26
26
|
items: [
|
|
@@ -33,7 +33,7 @@ export const content: ToolLocaleContent = {
|
|
|
33
33
|
{ type: 'title', text: 'Математика нескольких костей: Почему 3d6 надежнее, чем 1d20', level: 2 },
|
|
34
34
|
{ type: 'paragraph', html: 'Когда вы бросаете одну кость D20, каждый результат от 1 до 20 имеет равный шанс в 5%, что дает равномерное распределение вероятностей. Однако при броске нескольких костей, например 3d6, результаты группируются вокруг среднего значения 10.5. Эта куполообразная кривая (распределение Гаусса) означает, что вы гораздо чаще будете выбрасывать средние значения, чем крайние, что обеспечивает стабильность и сглаживает непредсказуемость одиночного кубика.' },
|
|
35
35
|
{ type: 'title', text: 'Разбор вероятностей D20: Критические попадания, критические провалы (pifia) и роль модификаторов', level: 2 },
|
|
36
|
-
{ type: 'paragraph', html: 'Простой модификатор «плюс два» может кардинально изменить вероятность успеха, превращая сложные задачи в легко выполнимые. Во многих игровых системах чистая 20 означает автоматический критический успех (шанс 5%), тогда как чистая 1
|
|
36
|
+
{ type: 'paragraph', html: 'Простой модификатор «плюс два» может кардинально изменить вероятность успеха, превращая сложные задачи в легко выполнимые. Во многих игровых системах чистая 20 означает автоматический критический успех (шанс 5%), тогда как чистая 1 - критический провал (pifia). Визуализация этих исходов помогает демистифицировать случайность и позволяет планировать тактические ходы на основе статистики.' },
|
|
37
37
|
{
|
|
38
38
|
type: 'title',
|
|
39
39
|
text: 'Таблица характеристик стандартных многогранных костей',
|
|
@@ -108,7 +108,7 @@ export const content: ToolLocaleContent = {
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
title: 'Процентная система (Бросок под значение)',
|
|
111
|
-
description: 'Использует две 10-гранные кости для получения числа от 1 до 100. Цель
|
|
111
|
+
description: 'Использует две 10-гранные кости для получения числа от 1 до 100. Цель - выбросить значение меньше показателя персонажа, что делает шансы наглядными.',
|
|
112
112
|
icon: 'mdi:percent-box-outline',
|
|
113
113
|
highlight: false,
|
|
114
114
|
points: [
|
|
@@ -126,7 +126,7 @@ export const content: ToolLocaleContent = {
|
|
|
126
126
|
title: 'Ошибка игрока: Почему у кубиков нет памяти',
|
|
127
127
|
icon: 'mdi:information-outline',
|
|
128
128
|
badge: 'PROBABILITY TIP',
|
|
129
|
-
html: 'Распространенное заблуждение в настольных играх
|
|
129
|
+
html: 'Распространенное заблуждение в настольных играх - вера в то, что предыдущие броски влияют на будущие. Если вы выбросили три «1» подряд на D20, вам может показаться, что высокий результат уже «на подходе». На самом деле каждый бросок - независимое событие. Вероятность выбросить 20 при следующем броске по-прежнему составляет ровно 5%. �-нание этого помогает игрокам делать тактический выбор объективно.',
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
type: 'title',
|
|
@@ -154,7 +154,7 @@ export const content: ToolLocaleContent = {
|
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
term: 'Математическое ожидание',
|
|
157
|
-
definition: 'Долгосрочный средний результат ваших бросков. Для одного D6 математическое ожидание составляет 3.5; для D20
|
|
157
|
+
definition: 'Долгосрочный средний результат ваших бросков. Для одного D6 математическое ожидание составляет 3.5; для D20 - 10.5.',
|
|
158
158
|
},
|
|
159
159
|
{
|
|
160
160
|
term: 'Процентные кости',
|
|
@@ -174,7 +174,7 @@ export const content: ToolLocaleContent = {
|
|
|
174
174
|
},
|
|
175
175
|
{
|
|
176
176
|
question: 'Что такое модификаторы и как они работают?',
|
|
177
|
-
answer: 'Модификаторы
|
|
177
|
+
answer: 'Модификаторы - это фиксированные значения (+1, -2 и т. д.), которые прибавляются к сумме броска или вычитаются из нее для отражения навыков персонажа, бонусов или штрафов.',
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
question: 'Что означает бросок с Преимуществом или Помехой?',
|
|
@@ -242,7 +242,7 @@ export const content: ToolLocaleContent = {
|
|
|
242
242
|
'name': 'Что такое модификаторы и как они работают?',
|
|
243
243
|
'acceptedAnswer': {
|
|
244
244
|
'@type': 'Answer',
|
|
245
|
-
'text': 'Модификаторы
|
|
245
|
+
'text': 'Модификаторы - это фиксированные значения (+1, -2 и т. д.), которые прибавляются к сумме броска или вычитаются из нее для отражения навыков персонажа, бонусов или штрафов.'
|
|
246
246
|
}
|
|
247
247
|
},
|
|
248
248
|
{
|
|
@@ -81,7 +81,7 @@ export function renderHistory(refs: HistoryUIRefs, onDrawChart: (totals: number[
|
|
|
81
81
|
setText(refs.totalVal, history.length.toString());
|
|
82
82
|
setText(refs.badge, history.length.toString());
|
|
83
83
|
setText(refs.averageVal, (sum / history.length).toFixed(1));
|
|
84
|
-
setText(refs.lastVal, history[history.length - 1]
|
|
84
|
+
setText(refs.lastVal, history[history.length - 1]!.total.toString());
|
|
85
85
|
|
|
86
86
|
onDrawChart(totals);
|
|
87
87
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { firstPlayerSelector } 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 = firstPlayerSelector.i18n[locale] || firstPlayerSelector.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
if (!content) return null;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Multi-Touch Finger Chooser Mechanics',
|
|
6
|
+
url: 'https://en.wikipedia.org/wiki/Multi-touch',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Randomness in Game Design',
|
|
10
|
+
url: 'https://en.wikipedia.org/wiki/Randomness_in_game_design',
|
|
11
|
+
},
|
|
12
|
+
];
|