@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,49 @@
|
|
|
1
|
+
import type { TimerState, TimerEventCallback, TimeControlMode } from '../types';
|
|
2
|
+
|
|
3
|
+
export class TimerEngineBase {
|
|
4
|
+
protected timers = new Map<string, TimerState>();
|
|
5
|
+
protected expiredCallbacks: TimerEventCallback[] = [];
|
|
6
|
+
protected tickCallbacks: TimerEventCallback[] = [];
|
|
7
|
+
protected warningCallbacks: TimerEventCallback[] = [];
|
|
8
|
+
protected activeId: string | undefined;
|
|
9
|
+
|
|
10
|
+
protected currentMode: TimeControlMode = 'normal';
|
|
11
|
+
protected currentIncrement = 0;
|
|
12
|
+
protected currentDelay = 0;
|
|
13
|
+
|
|
14
|
+
public getTimer(id: string) {
|
|
15
|
+
return this.timers.get(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public getAllTimers() {
|
|
19
|
+
return Array.from(this.timers.values());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public onExpired(cb: TimerEventCallback) {
|
|
23
|
+
this.expiredCallbacks.push(cb);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public onTick(cb: TimerEventCallback) {
|
|
27
|
+
this.tickCallbacks.push(cb);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public onWarning(cb: TimerEventCallback) {
|
|
31
|
+
this.warningCallbacks.push(cb);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public setMode(mode: TimeControlMode) {
|
|
35
|
+
this.currentMode = mode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public setIncrement(seconds: number) {
|
|
39
|
+
this.currentIncrement = seconds;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public setDelay(seconds: number) {
|
|
43
|
+
this.currentDelay = seconds;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public get activeTimerId() {
|
|
47
|
+
return this.activeId;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { TimerState, TimerConfig } from '../types';
|
|
2
|
+
import { TimerEngineBase } from './TimerEngineBase';
|
|
3
|
+
|
|
4
|
+
export class TimerEngineClock extends TimerEngineBase {
|
|
5
|
+
protected intervalId: ReturnType<typeof setInterval> | undefined;
|
|
6
|
+
protected lastWarningId: string | undefined;
|
|
7
|
+
protected delayRemaining = 0;
|
|
8
|
+
protected config: TimerConfig | undefined;
|
|
9
|
+
|
|
10
|
+
constructor(config?: TimerConfig) {
|
|
11
|
+
super();
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.currentMode = config?.mode ?? 'normal';
|
|
14
|
+
this.currentIncrement = config?.increment ?? 0;
|
|
15
|
+
this.currentDelay = config?.delay ?? 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected getNow(): number {
|
|
19
|
+
return Date.now();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected notifyTick(id: string) {
|
|
23
|
+
const t = this.timers.get(id);
|
|
24
|
+
if (!t) return;
|
|
25
|
+
this.tickCallbacks.forEach(cb => cb(id, t.remaining));
|
|
26
|
+
this.checkWarning(t);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private checkWarning(t: TimerState) {
|
|
30
|
+
const limit = this.config?.warningTime ?? 30;
|
|
31
|
+
if (t.remaining > limit) {
|
|
32
|
+
this.resetWarning(t.id);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (t.status === 'running' && t.remaining > 0 && this.lastWarningId !== t.id) {
|
|
36
|
+
this.triggerWarning(t);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private resetWarning(id: string) {
|
|
41
|
+
if (this.lastWarningId === id) {
|
|
42
|
+
this.lastWarningId = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private triggerWarning(t: TimerState) {
|
|
47
|
+
this.lastWarningId = t.id;
|
|
48
|
+
this.warningCallbacks.forEach(cb => cb(t.id, t.remaining));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected notifyExpired(id: string) {
|
|
52
|
+
const t = this.timers.get(id);
|
|
53
|
+
if (t) {
|
|
54
|
+
this.expiredCallbacks.forEach(cb => cb(id, t.remaining));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected processTick() {
|
|
59
|
+
if (!this.activeId) return;
|
|
60
|
+
const t = this.timers.get(this.activeId);
|
|
61
|
+
if (!t) return;
|
|
62
|
+
const now = this.getNow();
|
|
63
|
+
const elapsed = (now - (t._lastTickAt ?? now)) / 1000;
|
|
64
|
+
t._lastTickAt = now;
|
|
65
|
+
if (t.status === 'running') {
|
|
66
|
+
this.tickRunning(t, elapsed);
|
|
67
|
+
} else if (t.status === 'overtime') {
|
|
68
|
+
this.tickOvertime(t, elapsed);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private tickRunning(t: TimerState, elapsed: number) {
|
|
73
|
+
if (this.delayRemaining > 0) {
|
|
74
|
+
this.delayRemaining = Math.max(0, this.delayRemaining - elapsed);
|
|
75
|
+
t._lastTickAt = this.getNow();
|
|
76
|
+
this.notifyTick(t.id);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (elapsed >= t.remaining) {
|
|
80
|
+
this.handleExpiration(t, elapsed);
|
|
81
|
+
} else {
|
|
82
|
+
this.handleCountdown(t, elapsed);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private handleExpiration(t: TimerState, elapsed: number) {
|
|
87
|
+
t.overtime += elapsed - t.remaining;
|
|
88
|
+
t.remaining = 0;
|
|
89
|
+
t.status = 'overtime';
|
|
90
|
+
this.notifyTick(t.id);
|
|
91
|
+
this.notifyExpired(t.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private handleCountdown(t: TimerState, elapsed: number) {
|
|
95
|
+
t.remaining = Math.max(0, +(t.remaining - elapsed).toFixed(3));
|
|
96
|
+
this.notifyTick(t.id);
|
|
97
|
+
if (this.currentMode === 'hourglass') {
|
|
98
|
+
this.applyHourglass(elapsed);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private applyHourglass(elapsed: number) {
|
|
103
|
+
this.timers.forEach((other, id) => {
|
|
104
|
+
if (id !== this.activeId && other.status !== 'expired') {
|
|
105
|
+
other.remaining = +(other.remaining + elapsed).toFixed(3);
|
|
106
|
+
this.notifyTick(id);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private tickOvertime(t: TimerState, elapsed: number) {
|
|
112
|
+
t.overtime = +(t.overtime + elapsed).toFixed(3);
|
|
113
|
+
this.notifyTick(t.id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected startClock() {
|
|
117
|
+
this.stopClock();
|
|
118
|
+
this.intervalId = setInterval(() => this.processTick(), this.config?.tickInterval ?? 100);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected stopClock() {
|
|
122
|
+
if (this.intervalId !== undefined) {
|
|
123
|
+
clearInterval(this.intervalId);
|
|
124
|
+
this.intervalId = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { TimerState } from '../types';
|
|
2
|
+
|
|
3
|
+
export function createTimerState(id: string, label: string, duration: number): TimerState {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
label,
|
|
7
|
+
initialDuration: duration,
|
|
8
|
+
remaining: duration,
|
|
9
|
+
status: 'idle',
|
|
10
|
+
_lastTickAt: 0,
|
|
11
|
+
roundsPlayed: 0,
|
|
12
|
+
overtime: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function addTimerTime(t: TimerState, seconds: number) {
|
|
17
|
+
if (t.status === 'overtime') {
|
|
18
|
+
const remainingOvertime = t.overtime - seconds;
|
|
19
|
+
if (remainingOvertime <= 0) {
|
|
20
|
+
t.remaining = Math.abs(remainingOvertime);
|
|
21
|
+
t.overtime = 0;
|
|
22
|
+
t.status = 'paused';
|
|
23
|
+
} else {
|
|
24
|
+
t.overtime = +remainingOvertime.toFixed(3);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
t.remaining = Math.max(0, +(t.remaining + seconds).toFixed(3));
|
|
28
|
+
if (t.status === 'expired' && t.remaining > 0) {
|
|
29
|
+
t.status = 'paused';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function removeTimerTime(t: TimerState, seconds: number, notifyExpired: () => void) {
|
|
35
|
+
if (t.status === 'overtime') {
|
|
36
|
+
t.overtime = +(t.overtime + seconds).toFixed(3);
|
|
37
|
+
} else if (t.status === 'running') {
|
|
38
|
+
if (seconds >= t.remaining) {
|
|
39
|
+
t.overtime = +(seconds - t.remaining + (t.overtime || 0)).toFixed(3);
|
|
40
|
+
t.remaining = 0;
|
|
41
|
+
t.status = 'overtime';
|
|
42
|
+
notifyExpired();
|
|
43
|
+
} else {
|
|
44
|
+
t.remaining = Math.max(0, +(t.remaining - seconds).toFixed(3));
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
if (seconds >= t.remaining) {
|
|
48
|
+
t.remaining = 0;
|
|
49
|
+
t.status = 'expired';
|
|
50
|
+
notifyExpired();
|
|
51
|
+
} else {
|
|
52
|
+
t.remaining = Math.max(0, +(t.remaining - seconds).toFixed(3));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function spawnParticle(options: { parent: HTMLElement; text: string; x: number; y: number; variant?: 'accent' | 'warning' | 'danger' | 'success' }) {
|
|
2
|
+
const el = document.createElement('div');
|
|
3
|
+
el.className = `timer-particle timer-particle-${options.variant ?? 'accent'}`;
|
|
4
|
+
el.textContent = options.text;
|
|
5
|
+
el.style.left = `${options.x}px`;
|
|
6
|
+
el.style.top = `${options.y}px`;
|
|
7
|
+
const container = options.parent.closest('.timer-player-card') || options.parent.closest('.timer-single-display') || options.parent.closest('.timer-main-card') || options.parent;
|
|
8
|
+
container.appendChild(el);
|
|
9
|
+
setTimeout(() => el.remove(), 900);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function spawnTimerPop(parent: HTMLElement, text: string, variant: 'accent' | 'warning' | 'danger' | 'success' = 'accent') {
|
|
13
|
+
const el = document.createElement('div');
|
|
14
|
+
el.className = `timer-pop-burst`;
|
|
15
|
+
el.textContent = text;
|
|
16
|
+
el.style.color = `var(--color-timer-${variant})`;
|
|
17
|
+
parent.appendChild(el);
|
|
18
|
+
setTimeout(() => el.remove(), 1200);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function spawnExpiredBurst(parent: HTMLElement) {
|
|
22
|
+
const symbols = ['!', '!!', '!!!', '!!'];
|
|
23
|
+
const colors = ['#ef4444', '#dc2626', '#b91c1c', '#ef4444'];
|
|
24
|
+
for (let i = 0; i < 8; i++) {
|
|
25
|
+
const el = document.createElement('div');
|
|
26
|
+
el.className = 'timer-expired-particle';
|
|
27
|
+
el.textContent = symbols[i % symbols.length]!;
|
|
28
|
+
const angle = (i / 8) * 360;
|
|
29
|
+
const dist = 60 + Math.random() * 60;
|
|
30
|
+
const rad = (angle * Math.PI) / 180;
|
|
31
|
+
el.style.setProperty('--dx', `${Math.cos(rad) * dist}px`);
|
|
32
|
+
el.style.setProperty('--dy', `${Math.sin(rad) * dist}px`);
|
|
33
|
+
el.style.setProperty('--color', colors[i % colors.length]!);
|
|
34
|
+
parent.appendChild(el);
|
|
35
|
+
setTimeout(() => el.remove(), 1000);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function spawnTurnBurst(parent: HTMLElement, color?: string) {
|
|
40
|
+
const symbols = ['>', '>>', '>>>'];
|
|
41
|
+
for (let i = 0; i < 6; i++) {
|
|
42
|
+
const el = document.createElement('div');
|
|
43
|
+
el.className = 'timer-turn-particle';
|
|
44
|
+
el.textContent = symbols[i % symbols.length]!;
|
|
45
|
+
const angle = (i / 6) * 360 + 15;
|
|
46
|
+
const dist = 40 + Math.random() * 40;
|
|
47
|
+
const rad = (angle * Math.PI) / 180;
|
|
48
|
+
el.style.setProperty('--dx', `${Math.cos(rad) * dist}px`);
|
|
49
|
+
el.style.setProperty('--dy', `${Math.sin(rad) * dist}px`);
|
|
50
|
+
if (color) el.style.setProperty('--accent', color);
|
|
51
|
+
parent.appendChild(el);
|
|
52
|
+
setTimeout(() => el.remove(), 800);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function spawnConfetti(container: HTMLElement, count = 50) {
|
|
57
|
+
const colors = ['#ef4444', '#3b82f6', '#22c55e', '#eab308', '#a855f7', '#f97316', '#14b8a6', '#ec4899'];
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
const el = document.createElement('div');
|
|
60
|
+
el.className = `timer-confetti-piece${Math.random() > 0.5 ? ' timer-confetti-round' : ''}`;
|
|
61
|
+
el.style.left = `${10 + Math.random() * 80}%`;
|
|
62
|
+
el.style.top = `-${5 + Math.random() * 15}px`;
|
|
63
|
+
el.style.background = colors[Math.floor(Math.random() * colors.length)]!;
|
|
64
|
+
el.style.width = `${5 + Math.random() * 7}px`;
|
|
65
|
+
el.style.height = `${5 + Math.random() * 7}px`;
|
|
66
|
+
el.style.animationDelay = `${Math.random() * 1.2}s`;
|
|
67
|
+
el.style.animationDuration = `${2 + Math.random() * 1.5}s`;
|
|
68
|
+
container.appendChild(el);
|
|
69
|
+
setTimeout(() => el.remove(), 5000);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function spawnRippleEffect(container: HTMLElement) {
|
|
74
|
+
const el = document.createElement('div');
|
|
75
|
+
el.className = 'ripple';
|
|
76
|
+
const size = 200 + Math.random() * 200;
|
|
77
|
+
const x = 20 + Math.random() * 60;
|
|
78
|
+
const y = 20 + Math.random() * 60;
|
|
79
|
+
el.style.width = `${size}px`;
|
|
80
|
+
el.style.height = `${size}px`;
|
|
81
|
+
el.style.left = `${x}%`;
|
|
82
|
+
el.style.top = `${y}%`;
|
|
83
|
+
el.style.marginLeft = `-${size / 2}px`;
|
|
84
|
+
el.style.marginTop = `-${size / 2}px`;
|
|
85
|
+
container.appendChild(el);
|
|
86
|
+
setTimeout(() => el.remove(), 800);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function spawnDigitSparkle(element: HTMLElement) {
|
|
90
|
+
element.classList.remove('timer-digit-update');
|
|
91
|
+
void element.offsetWidth;
|
|
92
|
+
element.classList.add('timer-digit-update');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function spawnWarningFlash(element: HTMLElement) {
|
|
96
|
+
element.classList.remove('warning-glow');
|
|
97
|
+
void element.offsetWidth;
|
|
98
|
+
element.classList.add('warning-glow');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function spawnDangerFlash(element: HTMLElement) {
|
|
102
|
+
element.classList.remove('danger-glow');
|
|
103
|
+
void element.offsetWidth;
|
|
104
|
+
element.classList.add('danger-glow');
|
|
105
|
+
element.classList.remove('danger-shake');
|
|
106
|
+
void element.offsetWidth;
|
|
107
|
+
element.classList.add('danger-shake');
|
|
108
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { boardGameTimer } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const loader = boardGameTimer.i18n[locale] || boardGameTimer.i18n.en;
|
|
12
|
+
const content = await loader?.();
|
|
13
|
+
if (!content) return null;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type TimerStatus = 'idle' | 'running' | 'paused' | 'expired' | 'overtime';
|
|
2
|
+
|
|
3
|
+
export interface TimerState {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
initialDuration: number;
|
|
7
|
+
remaining: number;
|
|
8
|
+
status: TimerStatus;
|
|
9
|
+
_lastTickAt: number;
|
|
10
|
+
roundsPlayed: number;
|
|
11
|
+
overtime: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TimerEventCallback = (id: string, remaining: number) => void;
|
|
15
|
+
|
|
16
|
+
export type TimeControlMode = 'normal' | 'fischer' | 'bronstein' | 'hourglass' | 'turn';
|
|
17
|
+
|
|
18
|
+
export interface TimerConfig {
|
|
19
|
+
tickInterval?: number;
|
|
20
|
+
mode?: TimeControlMode;
|
|
21
|
+
increment?: number;
|
|
22
|
+
delay?: number;
|
|
23
|
+
warningTime?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PlayerConfig {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
color: string;
|
|
30
|
+
avatar?: string;
|
|
31
|
+
time: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PlayerColor = 'blue' | 'red' | 'green' | 'yellow' | 'purple' | 'orange' | 'teal' | 'pink';
|
|
35
|
+
|
|
36
|
+
export const PLAYER_COLORS: Record<PlayerColor, string> = {
|
|
37
|
+
blue: '#3b82f6',
|
|
38
|
+
red: '#ef4444',
|
|
39
|
+
green: '#22c55e',
|
|
40
|
+
yellow: '#eab308',
|
|
41
|
+
purple: '#a855f7',
|
|
42
|
+
orange: '#f97316',
|
|
43
|
+
teal: '#14b8a6',
|
|
44
|
+
pink: '#ec4899',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const PLAYER_COLOR_NAMES: Record<PlayerColor, string> = {
|
|
48
|
+
blue: 'Blue',
|
|
49
|
+
red: 'Red',
|
|
50
|
+
green: 'Green',
|
|
51
|
+
yellow: 'Yellow',
|
|
52
|
+
purple: 'Purple',
|
|
53
|
+
orange: 'Orange',
|
|
54
|
+
teal: 'Teal',
|
|
55
|
+
pink: 'Pink',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface GameConfig {
|
|
59
|
+
players: PlayerConfig[];
|
|
60
|
+
turnOrder: 'clockwise' | 'sequential';
|
|
61
|
+
increment?: number;
|
|
62
|
+
delay?: number;
|
|
63
|
+
warningTime: number;
|
|
64
|
+
mode: TimeControlMode;
|
|
65
|
+
soundEnabled: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface GameState {
|
|
69
|
+
activePlayerIndex: number;
|
|
70
|
+
phase: 'setup' | 'playing' | 'paused' | 'finished';
|
|
71
|
+
round: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TimerEngine {
|
|
75
|
+
create(id: string, label: string, duration: number): void;
|
|
76
|
+
start(id: string): void;
|
|
77
|
+
pause(id: string): void;
|
|
78
|
+
resume(id: string): void;
|
|
79
|
+
reset(id: string): void;
|
|
80
|
+
addTime(id: string, seconds: number): void;
|
|
81
|
+
removeTime(id: string, seconds: number): void;
|
|
82
|
+
getTimer(id: string): TimerState | undefined;
|
|
83
|
+
getAllTimers(): TimerState[];
|
|
84
|
+
switchTurn(nextId: string): void;
|
|
85
|
+
switchToNextPlayer(playerIds: string[], currentIndex: number): { nextId: string; nextIndex: number };
|
|
86
|
+
pauseAll(): void;
|
|
87
|
+
resumeAll(playerIds: string[], activeIndex: number): void;
|
|
88
|
+
readonly activeTimerId: string | undefined;
|
|
89
|
+
onExpired(cb: TimerEventCallback): void;
|
|
90
|
+
onTick(cb: TimerEventCallback): void;
|
|
91
|
+
onWarning(cb: TimerEventCallback): void;
|
|
92
|
+
destroyTimer(id: string): void;
|
|
93
|
+
destroy(): void;
|
|
94
|
+
setMode(mode: TimeControlMode): void;
|
|
95
|
+
setIncrement(seconds: number): void;
|
|
96
|
+
setDelay(seconds: number): void;
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { diceRollerSimulator } 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 = diceRollerSimulator.i18n[locale] || diceRollerSimulator.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: 'Dice Probability Mathematics',
|
|
6
|
+
url: 'https://en.wikipedia.org/wiki/Dice_probability_formula',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Board Game Geek Dice Guide',
|
|
10
|
+
url: 'https://boardgamegeek.com/thread/3319696/dice-games-properly-explained',
|
|
11
|
+
},
|
|
12
|
+
];
|