@jjlmoya/utils-tabletop 1.3.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 +65 -0
- package/scripts/postinstall.mjs +27 -0
- package/src/category/TabletopCategorySEO.astro +14 -0
- package/src/category/i18n/de.ts +27 -0
- package/src/category/i18n/en.ts +27 -0
- package/src/category/i18n/es.ts +27 -0
- package/src/category/i18n/fr.ts +27 -0
- package/src/category/i18n/id.ts +27 -0
- package/src/category/i18n/it.ts +27 -0
- package/src/category/i18n/ja.ts +27 -0
- package/src/category/i18n/ko.ts +27 -0
- package/src/category/i18n/nl.ts +27 -0
- package/src/category/i18n/pl.ts +27 -0
- package/src/category/i18n/pt.ts +27 -0
- package/src/category/i18n/ru.ts +27 -0
- package/src/category/i18n/sv.ts +27 -0
- package/src/category/i18n/tr.ts +27 -0
- package/src/category/i18n/zh.ts +27 -0
- package/src/category/index.ts +27 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +10 -0
- package/src/entries.ts +10 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +21 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +161 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +18 -0
- package/src/tests/i18n_coverage.test.ts +36 -0
- package/src/tests/locale_completeness.test.ts +29 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_en_dash.test.ts +41 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/slug_language_code_format.test.ts +23 -0
- package/src/tests/slug_uniqueness.test.ts +81 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +16 -0
- package/src/tool/board-game-timer/bibliography.astro +16 -0
- package/src/tool/board-game-timer/bibliography.ts +12 -0
- package/src/tool/board-game-timer/board-game-timer.css +1615 -0
- package/src/tool/board-game-timer/client.ts +17 -0
- package/src/tool/board-game-timer/component.astro +22 -0
- package/src/tool/board-game-timer/components/DuelTimer.astro +75 -0
- package/src/tool/board-game-timer/components/MultiplayerTimer.astro +58 -0
- package/src/tool/board-game-timer/components/SetupPanel.astro +155 -0
- package/src/tool/board-game-timer/components/StatsModal.astro +52 -0
- package/src/tool/board-game-timer/entry.ts +76 -0
- package/src/tool/board-game-timer/i18n/de.ts +303 -0
- package/src/tool/board-game-timer/i18n/en.ts +304 -0
- package/src/tool/board-game-timer/i18n/es.ts +303 -0
- package/src/tool/board-game-timer/i18n/fr.ts +303 -0
- package/src/tool/board-game-timer/i18n/id.ts +303 -0
- package/src/tool/board-game-timer/i18n/it.ts +303 -0
- package/src/tool/board-game-timer/i18n/ja.ts +306 -0
- package/src/tool/board-game-timer/i18n/ko.ts +303 -0
- package/src/tool/board-game-timer/i18n/nl.ts +303 -0
- package/src/tool/board-game-timer/i18n/pl.ts +303 -0
- package/src/tool/board-game-timer/i18n/pt.ts +303 -0
- package/src/tool/board-game-timer/i18n/ru.ts +303 -0
- package/src/tool/board-game-timer/i18n/sv.ts +303 -0
- package/src/tool/board-game-timer/i18n/tr.ts +303 -0
- package/src/tool/board-game-timer/i18n/zh.ts +303 -0
- package/src/tool/board-game-timer/index.ts +9 -0
- package/src/tool/board-game-timer/logic.test.ts +282 -0
- package/src/tool/board-game-timer/logic.ts +6 -0
- package/src/tool/board-game-timer/modules/ColorHelper.ts +26 -0
- package/src/tool/board-game-timer/modules/DuelViewManager.ts +120 -0
- package/src/tool/board-game-timer/modules/FormatHelper.ts +12 -0
- package/src/tool/board-game-timer/modules/GameRunner.ts +136 -0
- package/src/tool/board-game-timer/modules/GameState.ts +48 -0
- package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +163 -0
- package/src/tool/board-game-timer/modules/SetupPanelManager.ts +209 -0
- package/src/tool/board-game-timer/modules/SoundManager.ts +109 -0
- package/src/tool/board-game-timer/modules/SpeechManager.ts +1 -0
- package/src/tool/board-game-timer/modules/StatsManager.ts +87 -0
- package/src/tool/board-game-timer/modules/StatsModalManager.ts +74 -0
- package/src/tool/board-game-timer/modules/TimerEngine.ts +175 -0
- package/src/tool/board-game-timer/modules/TimerEngineBase.ts +49 -0
- package/src/tool/board-game-timer/modules/TimerEngineClock.ts +127 -0
- package/src/tool/board-game-timer/modules/TimerStateHelper.ts +55 -0
- package/src/tool/board-game-timer/modules/particles.ts +108 -0
- package/src/tool/board-game-timer/seo.astro +16 -0
- package/src/tool/board-game-timer/types.ts +97 -0
- package/src/tool/dice-roller-simulator/bibliography.astro +16 -0
- package/src/tool/dice-roller-simulator/bibliography.ts +12 -0
- package/src/tool/dice-roller-simulator/client.ts +273 -0
- package/src/tool/dice-roller-simulator/component.astro +75 -0
- package/src/tool/dice-roller-simulator/components/DiceGrid.astro +55 -0
- package/src/tool/dice-roller-simulator/components/RollHistorySection.astro +26 -0
- package/src/tool/dice-roller-simulator/components/RollResultBanner.astro +10 -0
- package/src/tool/dice-roller-simulator/components/StatsPanel.astro +36 -0
- package/src/tool/dice-roller-simulator/dice-roller-simulator.css +1319 -0
- package/src/tool/dice-roller-simulator/entry.ts +39 -0
- package/src/tool/dice-roller-simulator/i18n/de.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/en.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/es.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/fr.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/id.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/it.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/ja.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/ko.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/nl.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/pl.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/pt.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/ru.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/sv.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/tr.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/zh.ts +305 -0
- package/src/tool/dice-roller-simulator/index.ts +9 -0
- package/src/tool/dice-roller-simulator/modules/chart.ts +32 -0
- package/src/tool/dice-roller-simulator/modules/history.ts +87 -0
- package/src/tool/dice-roller-simulator/modules/particles.ts +22 -0
- package/src/tool/dice-roller-simulator/seo.astro +16 -0
- package/src/tools.ts +9 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { BoardGameTimerUI } from '../entry';
|
|
4
|
+
|
|
5
|
+
const ui: BoardGameTimerUI = {
|
|
6
|
+
title: '桌游计时器',
|
|
7
|
+
setupTitle: '游戏设置',
|
|
8
|
+
playerNamePlaceholder: '玩家名称',
|
|
9
|
+
addPlayer: '添加玩家',
|
|
10
|
+
removePlayer: '删除',
|
|
11
|
+
startGame: '开始游戏',
|
|
12
|
+
pauseGame: '暂停',
|
|
13
|
+
resumeGame: '继续',
|
|
14
|
+
resetGame: '重置',
|
|
15
|
+
nextTurn: '结束回合',
|
|
16
|
+
addTime: '+30秒',
|
|
17
|
+
removeTime: '-30秒',
|
|
18
|
+
playerLabel: '玩家',
|
|
19
|
+
minutesLabel: '分',
|
|
20
|
+
secondsLabel: '秒',
|
|
21
|
+
expiredLabel: '时间到',
|
|
22
|
+
overtimeLabel: '超时',
|
|
23
|
+
turnIndicator: '轮到你了',
|
|
24
|
+
warningLowTime: '时间紧张',
|
|
25
|
+
pausedOverlay: '游戏已暂停',
|
|
26
|
+
roundLabel: '轮次',
|
|
27
|
+
settingsTitle: '设置',
|
|
28
|
+
baseTimeLabel: '基础时间',
|
|
29
|
+
incrementLabel: '增时/延时',
|
|
30
|
+
warningTimeLabel: '警告阈值',
|
|
31
|
+
confirmLabel: '确认',
|
|
32
|
+
cancelLabel: '取消',
|
|
33
|
+
noPlayersHint: '请至少添加 2 名玩家以开始游戏',
|
|
34
|
+
addPlayerHint: '输入玩家名称',
|
|
35
|
+
timeUpLabel: '时间到!',
|
|
36
|
+
presetsTitle: '预设',
|
|
37
|
+
timeControlTitle: '时间控制',
|
|
38
|
+
modeLabel: '模式',
|
|
39
|
+
modeNormal: '普通倒计时',
|
|
40
|
+
modeFischer: '菲舍尔 (增时)',
|
|
41
|
+
modeBronstein: '布龙斯坦 (延时)',
|
|
42
|
+
modeHourglass: '沙漏',
|
|
43
|
+
modeTurn: '回合重置',
|
|
44
|
+
audioTitle: '声音提示',
|
|
45
|
+
soundAlerts: '音效',
|
|
46
|
+
playersTitle: '玩家',
|
|
47
|
+
playersAdded: '已添加玩家',
|
|
48
|
+
statsTitle: '游戏统计数据',
|
|
49
|
+
totalGameTime: '总时间',
|
|
50
|
+
totalRounds: '总轮次',
|
|
51
|
+
slowestPlayer: '最慢玩家',
|
|
52
|
+
playerPerformance: '玩家用时分析',
|
|
53
|
+
playAgain: '新游戏',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const content: ToolLocaleContent<BoardGameTimerUI> = {
|
|
57
|
+
slug: 'board-game-timer',
|
|
58
|
+
title: '桌游计时器:双人对局棋钟与多人回合管理器',
|
|
59
|
+
description: '使用专业的桌游计时器记录回合和管理游戏时间。支持双人棋钟对局模式、多人网格布局、菲舍尔增时、布龙斯坦延时、沙漏模式以及详细的游戏数据统计。',
|
|
60
|
+
ui,
|
|
61
|
+
seo: [
|
|
62
|
+
{ type: 'title', text: '掌控桌面节奏:专业的时间控制如何提升桌面游戏体验', level: 2 },
|
|
63
|
+
{ type: 'paragraph', html: '时间管理是现代桌面游戏中的一个重要元素。无论你是在竞争激烈的派对游戏还是在深度的策略战争游戏中,限制思考时间都可以确保所有参与者获得公平的游戏机会。使用菲舍尔增时、布龙斯坦延时或沙漏模式等专业计时方法可以杜绝长考分析瘫痪,保证游戏节奏的一致性,并为游戏版图增添额外的战略决策维度。' },
|
|
64
|
+
{
|
|
65
|
+
type: 'stats',
|
|
66
|
+
items: [
|
|
67
|
+
{ value: '2-8人', label: '支持玩家数量' },
|
|
68
|
+
{ value: '5种模式', label: '时间配置' },
|
|
69
|
+
{ value: '100ms', label: '引擎精度' },
|
|
70
|
+
],
|
|
71
|
+
columns: 3,
|
|
72
|
+
},
|
|
73
|
+
{ type: 'title', text: '菲舍尔、布龙斯坦与沙漏:高级时间控制模式规则解析', level: 2 },
|
|
74
|
+
{ type: 'paragraph', html: '我们的专业桌游计时器支持多种灵感源自竞技锦标赛的计时选项,让你的游戏过程更顺畅:' },
|
|
75
|
+
{
|
|
76
|
+
type: 'table',
|
|
77
|
+
headers: ['模式', '工作原理', '最适合'],
|
|
78
|
+
rows: [
|
|
79
|
+
['普通倒计时', '标准的全局倒计时时钟,在回合结束时暂停。', '拥有共享思考时间池的休闲桌游'],
|
|
80
|
+
['菲舍尔 (增时)', '在完成回合后,向你的时钟添加设定的秒数。', '快速行动可以积累时间缓冲的策略德式桌游和兵棋'],
|
|
81
|
+
['布龙斯坦 (延时)', '在回合开始时提供一个延时窗口,期间主计时器不进行倒计时。', '复杂的战术游戏,玩家需要简短的实体操作整理时间'],
|
|
82
|
+
['沙漏', '你的时间流逝同时,对手的时间实时增加。', '紧张刺激的双人对抗赛和国际象棋对局'],
|
|
83
|
+
['回合重置', '每个回合拥有一段全新的固定倒计时时间,在下个回合开始时完全复原。', '节奏飞快的派对游戏、问答挑战和快速轮次'],
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'tip',
|
|
88
|
+
title: '寻找理想的时间配置',
|
|
89
|
+
html: '推荐的初始设置是:轻度派对游戏分配 30-45 秒,中度策略桌游使用 1-2 分钟并伴随 5 秒布龙斯坦延时,而重度战争游戏或策略战役则使用 30-45 分钟并伴随 10 秒菲舍尔增时。',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'title',
|
|
93
|
+
text: '视觉优化:双人分屏对局棋钟对比多人网格中央结束按钮',
|
|
94
|
+
level: 3,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'paragraph',
|
|
98
|
+
html: '对于双人对决,应用会自动展示分屏棋钟布局。此视图允许玩家将各自的屏幕区域旋转 180 度,方便桌子对面的玩家看清时间。对于 3 名或更多参与者的游戏,界面将切换为多人网格,包含当前玩家发光提示、个人状态卡调整以及一个巨大的中央结束回合按钮,该按钮可用作放置在游戏区中央的共用物理按键。',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'proscons',
|
|
102
|
+
title: '菲舍尔增时与布龙斯坦延时的对比分析',
|
|
103
|
+
items: [
|
|
104
|
+
{
|
|
105
|
+
pro: '菲舍尔增时奖励快速思考,通过为未来的回合积累时间储备来获得优势。',
|
|
106
|
+
con: '如果玩家持续进行极快的行动,总游戏时间可能会变得很长。',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
pro: '布龙斯坦延时防止在简单的物理管理回合中流失时间,且不会积攒过多的无用储备。',
|
|
110
|
+
con: '需要配置与游戏物理管理用时相匹配的合理延时阈值。',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
pro: '沙漏模式在双人决斗中产生直接的紧张感,通过不断吸取你的时间来喂养对手。',
|
|
114
|
+
con: '在多于两人的游戏中,数学计算变得非常复杂且缺乏策略性。',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'title',
|
|
120
|
+
text: '得益于回合计时器的常见桌游类型',
|
|
121
|
+
level: 3,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'comparative',
|
|
125
|
+
items: [
|
|
126
|
+
{
|
|
127
|
+
title: '德式策略桌游',
|
|
128
|
+
description: '工人放置和资源管理类游戏,其回合复杂度往往有所起伏。菲舍尔增时可以帮助玩家处理关键的残局决策,而不会拖慢开局的速度。',
|
|
129
|
+
icon: 'mdi:chess-knight',
|
|
130
|
+
highlight: true,
|
|
131
|
+
points: [
|
|
132
|
+
'缓解长考分析瘫痪',
|
|
133
|
+
'支持动态复杂回合',
|
|
134
|
+
'时间成为一种主动资源',
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
title: '派对与问答游戏',
|
|
139
|
+
description: '注重快速反应和乐趣的高能量游戏。回合重置计时器可以保持团队热度,防止单个玩家卡死整局游戏。',
|
|
140
|
+
icon: 'mdi:party-popper',
|
|
141
|
+
highlight: false,
|
|
142
|
+
points: [
|
|
143
|
+
'简短的固定倒计时',
|
|
144
|
+
'维持派对的愉快氛围',
|
|
145
|
+
'强制要求立即做出行动',
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
title: '锦标赛双人决斗',
|
|
150
|
+
description: '严格的棋钟决斗需要精确、支持旋转的屏幕布局,以保证在整场锦标赛中的竞争公平性与合理调度。',
|
|
151
|
+
icon: 'mdi:trophy',
|
|
152
|
+
highlight: false,
|
|
153
|
+
points: [
|
|
154
|
+
'屏幕支持 180 度旋转',
|
|
155
|
+
'高精度的 100 毫秒时间追踪',
|
|
156
|
+
'支持突然死亡与超时状态判定',
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
columns: 3,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'glossary',
|
|
164
|
+
items: [
|
|
165
|
+
{
|
|
166
|
+
term: '菲舍尔增时 (Fischer Increment)',
|
|
167
|
+
definition: '一种时间控制方法,在每个玩家完成其回合后,向其剩余时间中添加设定数量的的秒数。',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
term: '布龙斯坦延时 (Bronstein Delay)',
|
|
171
|
+
definition: '一种时间控制方法,在回合开始时延迟倒计时一段设定的秒数,如果在该秒数内完成回合则不扣除时间。',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
term: '沙漏模式 (Hourglass Mode)',
|
|
175
|
+
definition: '一种动态计时系统,其中一个玩家被扣除的时间会实时直接转移给对手。',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
term: '分析瘫痪 / 长考 (Analysis Paralysis)',
|
|
179
|
+
definition: '玩家因选项过多而陷入过度思考的状态,从而导致游戏进程停滞并影响其他玩家的体验。',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
term: '游戏统计分析',
|
|
183
|
+
definition: '游戏后性能分析指标,总结总经历轮次、平均思考速度、最大回合耗时以及最慢回合排名。',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
faq: [
|
|
189
|
+
{
|
|
190
|
+
question: '双人模式的布局是如何工作的?',
|
|
191
|
+
answer: '当开始 2 人玩家游戏时,界面将显示双人分屏棋钟。每一边代表一个玩家,点击他们各自的半屏即可结束其回合。屏幕可以旋转 180 度,方便面对面坐着的玩家观看。',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
question: '菲舍尔模式和布龙斯坦模式有什么区别?',
|
|
195
|
+
answer: '菲舍尔模式在结束回合后向玩家的时间池中添加额外的秒数,允许他们积累时间。布龙斯坦模式在每个回合开始时给予一定的延迟时间;如果回合在延迟时间内结束,则不扣除时间。',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
question: '应用会记录回合统计数据吗?',
|
|
199
|
+
answer: '是的!游戏结束或计时器用尽后,统计弹窗会显示游戏总耗时、总轮次、平均回合时间、最长回合时间以及玩家思考速度排名。',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
question: '刷新页面后,我的玩家信息会保存吗?',
|
|
203
|
+
answer: '是的,玩家配置和颜色选择会自动保存在浏览器的本地存储中,当你返回页面时,它会恢复你的玩家列表。',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
question: '我可以在实时游戏期间增加或减少时间吗?',
|
|
207
|
+
answer: '可以!在多人模式下,玩家卡片提供了专门的 +30秒 和 -30秒 按钮,以方便在游戏过程中快速调节剩余时间。',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
bibliography,
|
|
211
|
+
howTo: [
|
|
212
|
+
{
|
|
213
|
+
name: '配置游戏设置',
|
|
214
|
+
text: '选择预设(休闲、闪电战、策略、沙漏)或设定自定义参数:基础用时、增时/延时以及警告阈值。',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: '添加与保存玩家',
|
|
218
|
+
text: '输入玩家名称并选择识别颜色。使用箭头控件重新排序回合顺序;详细信息会自动保存。',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: '启动并交替回合',
|
|
222
|
+
text: '开始游戏。在双人模式中,点击你的屏幕半区以结束回合。在多人模式中,点击中央巨大的“结束回合”按钮。',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
schemas: [
|
|
226
|
+
{
|
|
227
|
+
'@context': 'https://schema.org',
|
|
228
|
+
'@type': 'SoftwareApplication',
|
|
229
|
+
'name': '桌游计时器与回合管理器',
|
|
230
|
+
'operatingSystem': 'All',
|
|
231
|
+
'applicationCategory': 'UtilitiesApplication',
|
|
232
|
+
'browserRequirements': 'Requires HTML5. Requires JavaScript.'
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
'@context': 'https://schema.org',
|
|
236
|
+
'@type': 'FAQPage',
|
|
237
|
+
'mainEntity': [
|
|
238
|
+
{
|
|
239
|
+
'@type': 'Question',
|
|
240
|
+
'name': '双人模式的布局是如何工作的?',
|
|
241
|
+
'acceptedAnswer': {
|
|
242
|
+
'@type': 'Answer',
|
|
243
|
+
'text': '当开始 2 人玩家游戏时,界面将显示双人分屏棋钟。每一边代表一个玩家,点击他们各自的半屏即可结束其回合。屏幕可以旋转 180 度,方便面对面坐着的玩家观看。'
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
'@type': 'Question',
|
|
248
|
+
'name': '菲舍尔模式和布龙斯坦模式有什么区别?',
|
|
249
|
+
'acceptedAnswer': {
|
|
250
|
+
'@type': 'Answer',
|
|
251
|
+
'text': '菲舍尔模式在结束回合后向玩家的时间池中添加额外的秒数,允许他们积累时间。布龙斯坦模式在每个回合开始时给予一定的延迟时间;如果回合在延迟时间内结束,则不扣除时间。'
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
'@type': 'Question',
|
|
256
|
+
'name': '应用会记录回合统计数据吗?',
|
|
257
|
+
'acceptedAnswer': {
|
|
258
|
+
'@type': 'Answer',
|
|
259
|
+
'text': '是的!游戏结束或计时器用尽后,统计弹窗会显示游戏总耗时、总轮次、平均回合时间、最长回合时间以及玩家思考速度排名。'
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
'@type': 'Question',
|
|
264
|
+
'name': '刷新页面后,我的玩家信息会保存吗?',
|
|
265
|
+
'acceptedAnswer': {
|
|
266
|
+
'@type': 'Answer',
|
|
267
|
+
'text': '是的,玩家配置和颜色选择会自动保存在浏览器的本地存储中,当你返回页面时,它会恢复你的玩家列表。'
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
'@type': 'Question',
|
|
272
|
+
'name': '我可以在实时游戏期间增加或减少时间吗?',
|
|
273
|
+
'acceptedAnswer': {
|
|
274
|
+
'@type': 'Answer',
|
|
275
|
+
'text': '可以!在多人模式下,玩家卡片提供了专门的 +30秒 和 -30秒 按钮,以方便在游戏过程中快速调节剩余时间。'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
'@context': 'https://schema.org',
|
|
282
|
+
'@type': 'HowTo',
|
|
283
|
+
'name': '如何使用桌游计时器',
|
|
284
|
+
'step': [
|
|
285
|
+
{
|
|
286
|
+
'@type': 'HowToStep',
|
|
287
|
+
'name': '配置游戏设置',
|
|
288
|
+
'text': '选择预设(休闲、闪电战、策略、沙漏)或设定自定义参数:基础用时、增时/延时以及警告阈值。'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
'@type': 'HowToStep',
|
|
292
|
+
'name': '添加与保存玩家',
|
|
293
|
+
'text': '输入玩家名称并选择识别颜色。使用箭头控件重新排序回合顺序;详细信息会自动保存。'
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
'@type': 'HowToStep',
|
|
297
|
+
'name': '启动并交替回合',
|
|
298
|
+
'text': '开始游戏。在双人模式中,点击你的屏幕半区以结束回合。在多人模式中,点击中央巨大的“结束回合”按钮。'
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { boardGameTimer } from './entry';
|
|
3
|
+
export * from './entry';
|
|
4
|
+
export const BOARD_GAME_TIMER_TOOL: ToolDefinition = {
|
|
5
|
+
entry: boardGameTimer,
|
|
6
|
+
Component: () => import('./component.astro'),
|
|
7
|
+
SEOComponent: () => import('./seo.astro'),
|
|
8
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
9
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createTimerEngine } from './logic';
|
|
3
|
+
|
|
4
|
+
describe('BoardGameTimerEngine', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('single timer', () => {
|
|
14
|
+
it('creates a timer with correct initial state', () => {
|
|
15
|
+
const engine = createTimerEngine();
|
|
16
|
+
engine.create('player-1', 'Player 1', 60);
|
|
17
|
+
|
|
18
|
+
const timer = engine.getTimer('player-1');
|
|
19
|
+
expect(timer).toBeDefined();
|
|
20
|
+
expect(timer!.id).toBe('player-1');
|
|
21
|
+
expect(timer!.label).toBe('Player 1');
|
|
22
|
+
expect(timer!.initialDuration).toBe(60);
|
|
23
|
+
expect(timer!.remaining).toBe(60);
|
|
24
|
+
expect(timer!.status).toBe('idle');
|
|
25
|
+
expect(timer!.roundsPlayed).toBe(0);
|
|
26
|
+
expect(timer!.overtime).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('starts a timer and sets status to running', () => {
|
|
30
|
+
const engine = createTimerEngine();
|
|
31
|
+
engine.create('p1', 'P1', 60);
|
|
32
|
+
engine.start('p1');
|
|
33
|
+
|
|
34
|
+
const timer = engine.getTimer('p1');
|
|
35
|
+
expect(timer!.status).toBe('running');
|
|
36
|
+
expect(engine.activeTimerId).toBe('p1');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('pauses a running timer without losing remaining time', () => {
|
|
40
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
41
|
+
engine.create('p1', 'P1', 60);
|
|
42
|
+
engine.start('p1');
|
|
43
|
+
|
|
44
|
+
vi.advanceTimersByTime(1000);
|
|
45
|
+
|
|
46
|
+
engine.pause('p1');
|
|
47
|
+
|
|
48
|
+
const timer = engine.getTimer('p1');
|
|
49
|
+
expect(timer!.status).toBe('paused');
|
|
50
|
+
expect(timer!.remaining).toBeLessThan(60);
|
|
51
|
+
expect(timer!.remaining).toBeGreaterThan(58);
|
|
52
|
+
expect(engine.activeTimerId).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('resumes a paused timer', () => {
|
|
56
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
57
|
+
engine.create('p1', 'P1', 60);
|
|
58
|
+
engine.start('p1');
|
|
59
|
+
|
|
60
|
+
vi.advanceTimersByTime(1000);
|
|
61
|
+
|
|
62
|
+
engine.pause('p1');
|
|
63
|
+
engine.resume('p1');
|
|
64
|
+
expect(engine.getTimer('p1')!.status).toBe('running');
|
|
65
|
+
expect(engine.activeTimerId).toBe('p1');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('resets a timer to initial duration', () => {
|
|
69
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
70
|
+
engine.create('p1', 'P1', 60);
|
|
71
|
+
engine.start('p1');
|
|
72
|
+
|
|
73
|
+
vi.advanceTimersByTime(2000);
|
|
74
|
+
|
|
75
|
+
engine.reset('p1');
|
|
76
|
+
const timer = engine.getTimer('p1');
|
|
77
|
+
expect(timer!.remaining).toBe(60);
|
|
78
|
+
expect(timer!.status).toBe('idle');
|
|
79
|
+
expect(engine.activeTimerId).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('calls expired callback when timer reaches zero', () => {
|
|
83
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
84
|
+
const onExpired = vi.fn();
|
|
85
|
+
engine.onExpired(onExpired);
|
|
86
|
+
|
|
87
|
+
engine.create('p1', 'P1', 0.5);
|
|
88
|
+
engine.start('p1');
|
|
89
|
+
|
|
90
|
+
vi.advanceTimersByTime(1000);
|
|
91
|
+
|
|
92
|
+
expect(onExpired).toHaveBeenCalledWith('p1', 0);
|
|
93
|
+
const timer = engine.getTimer('p1');
|
|
94
|
+
expect(timer!.remaining).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('adds time to a timer', () => {
|
|
98
|
+
const engine = createTimerEngine();
|
|
99
|
+
engine.create('p1', 'P1', 60);
|
|
100
|
+
engine.addTime('p1', 30);
|
|
101
|
+
|
|
102
|
+
expect(engine.getTimer('p1')!.remaining).toBe(90);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('removes time from a timer', () => {
|
|
106
|
+
const engine = createTimerEngine();
|
|
107
|
+
engine.create('p1', 'P1', 60);
|
|
108
|
+
engine.removeTime('p1', 10);
|
|
109
|
+
|
|
110
|
+
expect(engine.getTimer('p1')!.remaining).toBe(50);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('overtime', () => {
|
|
115
|
+
it('transitions to overtime when time runs out while running', () => {
|
|
116
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
117
|
+
engine.create('p1', 'P1', 0.5);
|
|
118
|
+
engine.start('p1');
|
|
119
|
+
|
|
120
|
+
vi.advanceTimersByTime(1000);
|
|
121
|
+
|
|
122
|
+
const timer = engine.getTimer('p1');
|
|
123
|
+
expect(timer!.status).toBe('overtime');
|
|
124
|
+
expect(timer!.remaining).toBe(0);
|
|
125
|
+
expect(timer!.overtime).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('addTime can recover from overtime back to positive', () => {
|
|
129
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
130
|
+
engine.create('p1', 'P1', 0.3);
|
|
131
|
+
engine.start('p1');
|
|
132
|
+
|
|
133
|
+
vi.advanceTimersByTime(1000);
|
|
134
|
+
|
|
135
|
+
let timer = engine.getTimer('p1');
|
|
136
|
+
expect(timer!.status).toBe('overtime');
|
|
137
|
+
|
|
138
|
+
const overtimeUsed = timer!.overtime;
|
|
139
|
+
engine.addTime('p1', Math.ceil(overtimeUsed) + 10);
|
|
140
|
+
|
|
141
|
+
timer = engine.getTimer('p1');
|
|
142
|
+
expect(timer!.status).toBe('paused');
|
|
143
|
+
expect(timer!.remaining).toBeGreaterThan(0);
|
|
144
|
+
expect(timer!.overtime).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('removeTime from running can push into overtime', () => {
|
|
148
|
+
const engine = createTimerEngine();
|
|
149
|
+
engine.create('p1', 'P1', 10);
|
|
150
|
+
engine.start('p1');
|
|
151
|
+
engine.removeTime('p1', 15);
|
|
152
|
+
|
|
153
|
+
const timer = engine.getTimer('p1');
|
|
154
|
+
expect(timer!.status).toBe('overtime');
|
|
155
|
+
expect(timer!.remaining).toBe(0);
|
|
156
|
+
expect(timer!.overtime).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('multi-player turn management', () => {
|
|
161
|
+
it('starts a timer and pauses the previous active timer on switchTurn', () => {
|
|
162
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
163
|
+
engine.create('p1', 'P1', 60);
|
|
164
|
+
engine.create('p2', 'P2', 60);
|
|
165
|
+
|
|
166
|
+
engine.start('p1');
|
|
167
|
+
vi.advanceTimersByTime(500);
|
|
168
|
+
|
|
169
|
+
engine.switchTurn('p2');
|
|
170
|
+
|
|
171
|
+
expect(engine.getTimer('p1')!.status).toBe('paused');
|
|
172
|
+
expect(engine.getTimer('p2')!.status).toBe('running');
|
|
173
|
+
expect(engine.activeTimerId).toBe('p2');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('increments roundsPlayed on switchTurn', () => {
|
|
177
|
+
const engine = createTimerEngine();
|
|
178
|
+
engine.create('p1', 'P1', 60);
|
|
179
|
+
engine.create('p2', 'P2', 60);
|
|
180
|
+
|
|
181
|
+
engine.start('p1');
|
|
182
|
+
engine.switchTurn('p2');
|
|
183
|
+
engine.switchTurn('p1');
|
|
184
|
+
engine.switchTurn('p2');
|
|
185
|
+
|
|
186
|
+
expect(engine.getTimer('p1')!.roundsPlayed).toBe(1);
|
|
187
|
+
expect(engine.getTimer('p2')!.roundsPlayed).toBe(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('does not switch to an expired player', () => {
|
|
191
|
+
const engine = createTimerEngine();
|
|
192
|
+
engine.create('p1', 'P1', 60);
|
|
193
|
+
engine.create('p2', 'P2', 10);
|
|
194
|
+
|
|
195
|
+
engine.removeTime('p2', 15);
|
|
196
|
+
expect(engine.getTimer('p2')!.status).toBe('expired');
|
|
197
|
+
|
|
198
|
+
engine.switchTurn('p2');
|
|
199
|
+
expect(engine.getTimer('p2')!.status).toBe('expired');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('lists all timers', () => {
|
|
203
|
+
const engine = createTimerEngine();
|
|
204
|
+
engine.create('p1', 'P1', 60);
|
|
205
|
+
engine.create('p2', 'P2', 120);
|
|
206
|
+
engine.create('p3', 'P3', 90);
|
|
207
|
+
|
|
208
|
+
const all = engine.getAllTimers();
|
|
209
|
+
expect(all).toHaveLength(3);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('switchToNextPlayer cycles through player IDs', () => {
|
|
213
|
+
const engine = createTimerEngine();
|
|
214
|
+
const ids = ['p1', 'p2', 'p3'];
|
|
215
|
+
|
|
216
|
+
const result1 = engine.switchToNextPlayer(ids, 0);
|
|
217
|
+
expect(result1.nextId).toBe('p2');
|
|
218
|
+
expect(result1.nextIndex).toBe(1);
|
|
219
|
+
|
|
220
|
+
const result2 = engine.switchToNextPlayer(ids, 1);
|
|
221
|
+
expect(result2.nextId).toBe('p3');
|
|
222
|
+
expect(result2.nextIndex).toBe(2);
|
|
223
|
+
|
|
224
|
+
const result3 = engine.switchToNextPlayer(ids, 2);
|
|
225
|
+
expect(result3.nextId).toBe('p1');
|
|
226
|
+
expect(result3.nextIndex).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('pauseAll / resumeAll', () => {
|
|
231
|
+
it('pauseAll stops the active timer', () => {
|
|
232
|
+
const engine = createTimerEngine();
|
|
233
|
+
engine.create('p1', 'P1', 60);
|
|
234
|
+
engine.start('p1');
|
|
235
|
+
|
|
236
|
+
engine.pauseAll();
|
|
237
|
+
|
|
238
|
+
expect(engine.getTimer('p1')!.status).toBe('paused');
|
|
239
|
+
expect(engine.activeTimerId).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('resumeAll restarts the active timer', () => {
|
|
243
|
+
const engine = createTimerEngine();
|
|
244
|
+
engine.create('p1', 'P1', 60);
|
|
245
|
+
engine.start('p1');
|
|
246
|
+
|
|
247
|
+
engine.pauseAll();
|
|
248
|
+
engine.resumeAll(['p1', 'p2'], 0);
|
|
249
|
+
|
|
250
|
+
expect(engine.getTimer('p1')!.status).toBe('running');
|
|
251
|
+
expect(engine.activeTimerId).toBe('p1');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('warning callback', () => {
|
|
256
|
+
it('triggers onWarning when remaining is <= 30s', () => {
|
|
257
|
+
const engine = createTimerEngine({ tickInterval: 50 });
|
|
258
|
+
const onWarning = vi.fn();
|
|
259
|
+
engine.onWarning(onWarning);
|
|
260
|
+
|
|
261
|
+
engine.create('p1', 'P1', 31);
|
|
262
|
+
engine.start('p1');
|
|
263
|
+
|
|
264
|
+
vi.advanceTimersByTime(2000);
|
|
265
|
+
|
|
266
|
+
expect(onWarning).toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('engine lifecycle', () => {
|
|
271
|
+
it('destroy clears all timers and stops clock', () => {
|
|
272
|
+
const engine = createTimerEngine();
|
|
273
|
+
engine.create('p1', 'P1', 60);
|
|
274
|
+
engine.start('p1');
|
|
275
|
+
|
|
276
|
+
engine.destroy();
|
|
277
|
+
|
|
278
|
+
expect(engine.getTimer('p1')).toBeUndefined();
|
|
279
|
+
expect(engine.getAllTimers()).toHaveLength(0);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlayerColor } from '../types';
|
|
2
|
+
|
|
3
|
+
export function getLuminance(hex: string): number {
|
|
4
|
+
const cleanHex = hex.replace('#', '');
|
|
5
|
+
const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
|
|
6
|
+
const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
|
|
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;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getContrastColorForBackground(colorName: PlayerColor): string {
|
|
13
|
+
const colorHexMap: Record<PlayerColor, string> = {
|
|
14
|
+
blue: '#3b82f6',
|
|
15
|
+
red: '#ef4444',
|
|
16
|
+
green: '#22c55e',
|
|
17
|
+
yellow: '#eab308',
|
|
18
|
+
purple: '#a855f7',
|
|
19
|
+
orange: '#f97316',
|
|
20
|
+
teal: '#14b8a6',
|
|
21
|
+
pink: '#ec4899'
|
|
22
|
+
};
|
|
23
|
+
const hex = colorHexMap[colorName] || '#3b82f6';
|
|
24
|
+
const lum = getLuminance(hex);
|
|
25
|
+
return lum > 0.3 ? '#0f172a' : '#ffffff';
|
|
26
|
+
}
|