@jjlmoya/utils-sports 1.63.0 → 1.65.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/tool/dartsScoreKeeper/component.astro +4 -0
- package/src/tool/dartsScoreKeeper/darts-scorekeeper.css +45 -1
- package/src/tool/dartsScoreKeeper/game-logic.test.ts +29 -0
- package/src/tool/dartsScoreKeeper/game-logic.ts +13 -2
- package/src/tool/dartsScoreKeeper/i18n/de.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/en.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/es.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/fr.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/id.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/it.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/ja.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/ko.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/nl.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/pl.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/pt.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/ru.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/sv.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/tr.ts +3 -0
- package/src/tool/dartsScoreKeeper/i18n/zh.ts +3 -0
- package/src/tool/dartsScoreKeeper/logic.ts +20 -23
- package/src/tool/dartsScoreKeeper/mode.ts +14 -0
- package/src/tool/dartsScoreKeeper/render.ts +13 -6
- package/src/tool/dartsScoreKeeper/score-storage.ts +24 -0
- package/src/tool/dartsScoreKeeper/ui.ts +3 -0
package/package.json
CHANGED
|
@@ -13,6 +13,10 @@ const t = (ui ?? {}) as DartsScoreKeeperUI;
|
|
|
13
13
|
|
|
14
14
|
<div class="tn-console" id="tn-card" data-tn-ui={JSON.stringify(t)}>
|
|
15
15
|
<div class="tn-header-panel">
|
|
16
|
+
<div class="tn-mode-group" role="group" aria-label={t.modeLabel ?? 'Game mode'}>
|
|
17
|
+
<button type="button" class="tn-mode-btn" data-dt-mode="solo">{t.soloMode ?? 'Solo'}</button>
|
|
18
|
+
<button type="button" class="tn-mode-btn tn-mode-active" data-dt-mode="versus">{t.versusMode ?? 'Two players'}</button>
|
|
19
|
+
</div>
|
|
16
20
|
<div class="tn-header-actions">
|
|
17
21
|
<button class="tn-action-btn" id="tn-toggle-view-btn" title="Toggle View">
|
|
18
22
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -70,13 +70,39 @@
|
|
|
70
70
|
|
|
71
71
|
.tn-header-panel {
|
|
72
72
|
display: flex;
|
|
73
|
-
justify-content:
|
|
73
|
+
justify-content: space-between;
|
|
74
74
|
align-items: center;
|
|
75
75
|
padding: 0.5rem 1.25rem;
|
|
76
76
|
background: var(--tn-bg-panel);
|
|
77
77
|
border-bottom: 1px solid var(--tn-border-base);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
.tn-mode-group {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
gap: 0.25rem;
|
|
83
|
+
padding: 0.2rem;
|
|
84
|
+
border: 1px solid var(--tn-border-base);
|
|
85
|
+
border-radius: 999px;
|
|
86
|
+
background: var(--tn-bg-console);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tn-mode-btn {
|
|
90
|
+
padding: 0.42rem 0.72rem;
|
|
91
|
+
border: 0;
|
|
92
|
+
border-radius: 999px;
|
|
93
|
+
color: var(--tn-text-muted);
|
|
94
|
+
background: transparent;
|
|
95
|
+
font: inherit;
|
|
96
|
+
font-size: 0.72rem;
|
|
97
|
+
font-weight: 800;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.tn-mode-btn.tn-mode-active {
|
|
102
|
+
color: #0d1118;
|
|
103
|
+
background: var(--tn-color-active-border);
|
|
104
|
+
}
|
|
105
|
+
|
|
80
106
|
.tn-header-actions {
|
|
81
107
|
display: flex;
|
|
82
108
|
gap: 0.5rem;
|
|
@@ -109,6 +135,24 @@
|
|
|
109
135
|
background: var(--tn-bg-panel);
|
|
110
136
|
}
|
|
111
137
|
|
|
138
|
+
.tn-mode-solo .tn-scoreboard {
|
|
139
|
+
grid-template-columns: minmax(0, 1fr);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.tn-mode-solo #tn-card-b {
|
|
143
|
+
display: none;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.tn-mode-solo .tn-history-grid {
|
|
147
|
+
max-width: 22rem;
|
|
148
|
+
margin-inline: auto;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.tn-history-solo .tn-history-header-row,
|
|
152
|
+
.tn-history-solo .tn-history-row {
|
|
153
|
+
grid-template-columns: minmax(0, 1fr) 3rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
112
156
|
.tn-player-card {
|
|
113
157
|
background: var(--tn-bg-card);
|
|
114
158
|
border: 2px solid var(--tn-border-base);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createInitialScore, processThrow } from './game-logic';
|
|
3
|
+
|
|
4
|
+
function completeTurn(mode: 'solo' | 'versus') {
|
|
5
|
+
let score = createInitialScore('501', true, mode);
|
|
6
|
+
score = processThrow(score, 1, 20);
|
|
7
|
+
score = processThrow(score, 1, 20);
|
|
8
|
+
return processThrow(score, 1, 20);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('darts game modes', () => {
|
|
12
|
+
it('keeps player A active after a solo turn', () => {
|
|
13
|
+
const score = completeTurn('solo');
|
|
14
|
+
expect(score.activePlayer).toBe('a');
|
|
15
|
+
expect(score.playerA.remainingScore).toBe(441);
|
|
16
|
+
expect(score.playerB.remainingScore).toBe(501);
|
|
17
|
+
expect(score.history).toHaveLength(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('alternates players in versus mode', () => {
|
|
21
|
+
const score = completeTurn('versus');
|
|
22
|
+
expect(score.activePlayer).toBe('b');
|
|
23
|
+
expect(score.playerA.remainingScore).toBe(441);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('defaults existing callers to versus mode', () => {
|
|
27
|
+
expect(createInitialScore().mode).toBe('versus');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type PlayerKey = 'a' | 'b';
|
|
2
2
|
export type DartsFormat = '501' | '301';
|
|
3
|
+
export type DartsMode = 'solo' | 'versus';
|
|
3
4
|
|
|
4
5
|
export interface PlayerState {
|
|
5
6
|
remainingScore: number;
|
|
@@ -31,6 +32,7 @@ export interface TurnRecord {
|
|
|
31
32
|
|
|
32
33
|
export interface DartsMatchScore {
|
|
33
34
|
format: DartsFormat;
|
|
35
|
+
mode: DartsMode;
|
|
34
36
|
doubleOut: boolean;
|
|
35
37
|
playerA: PlayerState;
|
|
36
38
|
playerB: PlayerState;
|
|
@@ -50,10 +52,15 @@ export function createInitialPlayer(score: number): PlayerState {
|
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
export function createInitialScore(
|
|
55
|
+
export function createInitialScore(
|
|
56
|
+
format: DartsFormat = '501',
|
|
57
|
+
doubleOut: boolean = true,
|
|
58
|
+
mode: DartsMode = 'versus',
|
|
59
|
+
): DartsMatchScore {
|
|
54
60
|
const start = format === '501' ? 501 : 301;
|
|
55
61
|
return {
|
|
56
62
|
format,
|
|
63
|
+
mode,
|
|
57
64
|
doubleOut,
|
|
58
65
|
playerA: createInitialPlayer(start),
|
|
59
66
|
playerB: createInitialPlayer(start),
|
|
@@ -184,7 +191,11 @@ function concludeTurn(score: DartsMatchScore): void {
|
|
|
184
191
|
isBusted: score.turn.isBusted,
|
|
185
192
|
};
|
|
186
193
|
score.history.push(record);
|
|
187
|
-
|
|
194
|
+
if (score.mode === 'solo') {
|
|
195
|
+
score.activePlayer = 'a';
|
|
196
|
+
} else {
|
|
197
|
+
score.activePlayer = score.activePlayer === 'a' ? 'b' : 'a';
|
|
198
|
+
}
|
|
188
199
|
score.turn = {
|
|
189
200
|
throws: [],
|
|
190
201
|
isBusted: false,
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Darts Durchgang',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Spielmodus',
|
|
196
|
+
soloMode: 'Allein',
|
|
197
|
+
versusMode: 'Zwei Spieler',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Kein Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Darts Turn',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Game mode',
|
|
196
|
+
soloMode: 'Solo',
|
|
197
|
+
versusMode: 'Two players',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'No Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Turno de Dardos',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Modo de juego',
|
|
196
|
+
soloMode: 'Individual',
|
|
197
|
+
versusMode: 'Dos jugadores',
|
|
195
198
|
doubleOut: 'Cierre con Doble',
|
|
196
199
|
noCheckout: 'Sin Cierre',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Tour de Fléchettes',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Mode de jeu',
|
|
196
|
+
soloMode: 'Solo',
|
|
197
|
+
versusMode: 'Deux joueurs',
|
|
195
198
|
doubleOut: 'Double Sortie',
|
|
196
199
|
noCheckout: 'Pas de Fermeture',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Giliran Dart',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Mode permainan',
|
|
196
|
+
soloMode: 'Solo',
|
|
197
|
+
versusMode: 'Dua pemain',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Tidak Ada Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Turno Freccette',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Modalità di gioco',
|
|
196
|
+
soloMode: 'Singolo',
|
|
197
|
+
versusMode: 'Due giocatori',
|
|
195
198
|
doubleOut: 'Doppia Uscita',
|
|
196
199
|
noCheckout: 'Nessuna Chiusura',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'ダーツターン',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'ゲームモード',
|
|
196
|
+
soloMode: '一人',
|
|
197
|
+
versusMode: '二人対戦',
|
|
195
198
|
doubleOut: 'ダブルアウト',
|
|
196
199
|
noCheckout: 'チェックアウトなし',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: '다트 턴',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: '게임 모드',
|
|
196
|
+
soloMode: '혼자 하기',
|
|
197
|
+
versusMode: '두 명',
|
|
195
198
|
doubleOut: '더블 아웃',
|
|
196
199
|
noCheckout: '체크아웃 없음',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Pijlen Beurt',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Spelmodus',
|
|
196
|
+
soloMode: 'Solo',
|
|
197
|
+
versusMode: 'Twee spelers',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Geen Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Rzut Darta',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Tryb gry',
|
|
196
|
+
soloMode: 'Solo',
|
|
197
|
+
versusMode: 'Dwóch graczy',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Brak Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Vez de Dardos',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Modo de jogo',
|
|
196
|
+
soloMode: 'Individual',
|
|
197
|
+
versusMode: 'Dois jogadores',
|
|
195
198
|
doubleOut: 'Fecho com Duplo',
|
|
196
199
|
noCheckout: 'Sem Fecho',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Бросок дротика',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Режим игры',
|
|
196
|
+
soloMode: 'Один игрок',
|
|
197
|
+
versusMode: 'Два игрока',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Нет Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Pil Kast',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Spelläge',
|
|
196
|
+
soloMode: 'Solo',
|
|
197
|
+
versusMode: 'Två spelare',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Ingen Checkout',
|
|
197
200
|
},
|
|
@@ -192,6 +192,9 @@ export const content: ToolLocaleContent<DartsScoreKeeperUI> = {
|
|
|
192
192
|
dart: 'Dart Atışı',
|
|
193
193
|
score301: '301',
|
|
194
194
|
score501: '501',
|
|
195
|
+
modeLabel: 'Oyun modu',
|
|
196
|
+
soloMode: 'Tek oyuncu',
|
|
197
|
+
versusMode: 'İki oyuncu',
|
|
195
198
|
doubleOut: 'Double Out',
|
|
196
199
|
noCheckout: 'Checkout Yok',
|
|
197
200
|
},
|
|
@@ -2,6 +2,7 @@ import type { DartsScoreKeeperUI } from './ui';
|
|
|
2
2
|
import {
|
|
3
3
|
type DartsMatchScore,
|
|
4
4
|
type DartsFormat,
|
|
5
|
+
type DartsMode,
|
|
5
6
|
createInitialScore,
|
|
6
7
|
processThrow,
|
|
7
8
|
} from './game-logic';
|
|
@@ -10,8 +11,9 @@ import {
|
|
|
10
11
|
render,
|
|
11
12
|
} from './render';
|
|
12
13
|
import { buildDartboardSVG } from './dartboard';
|
|
14
|
+
import { setupMode, syncMode } from './mode';
|
|
15
|
+
import { clearStoredScore, loadInitialScore, persistScore } from './score-storage';
|
|
13
16
|
|
|
14
|
-
const STORAGE_KEY = 'dt_match_state';
|
|
15
17
|
const NAMES_KEY = 'dt_names';
|
|
16
18
|
|
|
17
19
|
interface GameContext {
|
|
@@ -183,49 +185,42 @@ function setupViewToggles(card: HTMLElement): void {
|
|
|
183
185
|
});
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
function loadInitialScore(): DartsMatchScore {
|
|
187
|
-
try {
|
|
188
|
-
const s = localStorage.getItem(STORAGE_KEY);
|
|
189
|
-
if (s) return JSON.parse(s);
|
|
190
|
-
} catch {
|
|
191
|
-
return createInitialScore();
|
|
192
|
-
}
|
|
193
|
-
return createInitialScore();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
188
|
function createSaveFn(stateRef: { score: DartsMatchScore; history: DartsMatchScore[] }, t: DartsScoreKeeperUI): (s: DartsMatchScore, pushHistory?: boolean) => void {
|
|
197
189
|
return (s: DartsMatchScore, pushHistory = true) => {
|
|
198
190
|
if (pushHistory) stateRef.history.push(JSON.parse(JSON.stringify(stateRef.score)));
|
|
199
191
|
stateRef.score = s;
|
|
200
|
-
|
|
192
|
+
persistScore(stateRef.score);
|
|
201
193
|
render(stateRef.score, t);
|
|
202
194
|
};
|
|
203
195
|
}
|
|
204
196
|
|
|
205
|
-
function createApiObject(stateRef: { score: DartsMatchScore; history: DartsMatchScore[] }, save: (s: DartsMatchScore, pushHistory?: boolean) => void, t: DartsScoreKeeperUI) {
|
|
197
|
+
function createApiObject(stateRef: { score: DartsMatchScore; history: DartsMatchScore[] }, save: (s: DartsMatchScore, pushHistory?: boolean) => void, t: DartsScoreKeeperUI, card: HTMLElement) {
|
|
206
198
|
return {
|
|
207
199
|
resetAll() {
|
|
208
|
-
|
|
200
|
+
clearStoredScore();
|
|
209
201
|
stateRef.history = [];
|
|
210
|
-
save(createInitialScore(stateRef.score.format, stateRef.score.doubleOut), false);
|
|
202
|
+
save(createInitialScore(stateRef.score.format, stateRef.score.doubleOut, stateRef.score.mode), false);
|
|
211
203
|
},
|
|
212
204
|
setFormat(f: DartsFormat) {
|
|
213
205
|
document.querySelectorAll('.tn-format-btn').forEach((b) => b.classList.remove('tn-format-active'));
|
|
214
206
|
document.querySelector(`[data-dt-format="${f}"]`)?.classList.add('tn-format-active');
|
|
215
|
-
save(createInitialScore(f, stateRef.score.doubleOut));
|
|
207
|
+
save(createInitialScore(f, stateRef.score.doubleOut, stateRef.score.mode));
|
|
216
208
|
},
|
|
217
209
|
toggleDoubleOut() {
|
|
218
210
|
const nextD = !stateRef.score.doubleOut;
|
|
219
211
|
document.querySelector('[data-dt-doubleout]')?.classList.toggle('tn-doubleout-active', nextD);
|
|
220
|
-
save(createInitialScore(stateRef.score.format, nextD));
|
|
212
|
+
save(createInitialScore(stateRef.score.format, nextD, stateRef.score.mode));
|
|
213
|
+
},
|
|
214
|
+
setMode(mode: DartsMode) {
|
|
215
|
+
syncMode(card, mode);
|
|
216
|
+
save(createInitialScore(stateRef.score.format, stateRef.score.doubleOut, mode));
|
|
221
217
|
},
|
|
222
218
|
undo() {
|
|
223
219
|
const prev = stateRef.history.pop();
|
|
224
|
-
if (prev)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
220
|
+
if (!prev) return;
|
|
221
|
+
stateRef.score = prev;
|
|
222
|
+
persistScore(stateRef.score);
|
|
223
|
+
render(stateRef.score, t);
|
|
229
224
|
}
|
|
230
225
|
};
|
|
231
226
|
}
|
|
@@ -248,11 +243,13 @@ function runInit(): void {
|
|
|
248
243
|
document.querySelector(`[data-dt-mod="${m}"]`)?.classList.add('tn-mod-active');
|
|
249
244
|
}
|
|
250
245
|
};
|
|
251
|
-
|
|
246
|
+
syncMode(card, stateRef.score.mode);
|
|
247
|
+
const api = createApiObject(stateRef, save, t, card);
|
|
252
248
|
bindClickDelegation({ stateRef, t, saveFn: save }, multRef);
|
|
253
249
|
setupResetModal(card, api);
|
|
254
250
|
setupUndoAndFormat(card, api);
|
|
255
251
|
setupMultiplier(card, multRef);
|
|
252
|
+
setupMode(card, api.setMode);
|
|
256
253
|
setupViewToggles(card);
|
|
257
254
|
}
|
|
258
255
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DartsMode } from './game-logic';
|
|
2
|
+
|
|
3
|
+
export function syncMode(root: HTMLElement, mode: DartsMode): void {
|
|
4
|
+
root.classList.toggle('tn-mode-solo', mode === 'solo');
|
|
5
|
+
root.querySelectorAll<HTMLElement>('[data-dt-mode]').forEach((button) => {
|
|
6
|
+
button.classList.toggle('tn-mode-active', button.dataset.dtMode === mode);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setupMode(root: HTMLElement, setMode: (mode: DartsMode) => void): void {
|
|
11
|
+
root.querySelectorAll<HTMLElement>('[data-dt-mode]').forEach((button) => {
|
|
12
|
+
button.addEventListener('click', () => setMode(button.dataset.dtMode as DartsMode));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -63,14 +63,17 @@ function renderTurnDots(score: DartsMatchScore): void {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function renderHistoryHeader(t: DartsScoreKeeperUI, histGrid: HTMLElement): void {
|
|
66
|
+
function renderHistoryHeader(score: DartsMatchScore, t: DartsScoreKeeperUI, histGrid: HTMLElement): void {
|
|
67
67
|
const nameA = el('tn-name-a') as HTMLInputElement;
|
|
68
68
|
const nameB = el('tn-name-b') as HTMLInputElement;
|
|
69
69
|
const labelA = nameA ? nameA.value : t.playerA;
|
|
70
70
|
const labelB = nameB ? nameB.value : t.playerB;
|
|
71
71
|
const headerRow = document.createElement('div');
|
|
72
72
|
headerRow.className = 'tn-history-header-row';
|
|
73
|
-
headerRow.innerHTML = `
|
|
73
|
+
headerRow.innerHTML = score.mode === 'solo' ? `
|
|
74
|
+
<span class="tn-hist-name tn-hist-a">${labelA}</span>
|
|
75
|
+
<span class="tn-hist-round-lbl">Rnd</span>
|
|
76
|
+
` : `
|
|
74
77
|
<span class="tn-hist-name tn-hist-a">${labelA}</span>
|
|
75
78
|
<span class="tn-hist-round-lbl">Rnd</span>
|
|
76
79
|
<span class="tn-hist-name tn-hist-b">${labelB}</span>
|
|
@@ -78,7 +81,7 @@ function renderHistoryHeader(t: DartsScoreKeeperUI, histGrid: HTMLElement): void
|
|
|
78
81
|
histGrid.appendChild(headerRow);
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
function renderHistoryRow(round: { roundNum: number; a?: typeof score.history[0]; b?: typeof score.history[0] }, histGrid: HTMLElement): void {
|
|
84
|
+
function renderHistoryRow(round: { roundNum: number; a?: typeof score.history[0]; b?: typeof score.history[0] }, histGrid: HTMLElement, solo: boolean): void {
|
|
82
85
|
const row = document.createElement('div');
|
|
83
86
|
row.className = 'tn-history-row';
|
|
84
87
|
const renderTurn = (turn?: typeof round.a) => {
|
|
@@ -88,7 +91,10 @@ function renderHistoryRow(round: { roundNum: number; a?: typeof score.history[0]
|
|
|
88
91
|
}
|
|
89
92
|
return `<span class="tn-hist-val">${turn.points} <span class="tn-hist-sub">(${turn.scoreAfter})</span></span>`;
|
|
90
93
|
};
|
|
91
|
-
row.innerHTML = `
|
|
94
|
+
row.innerHTML = solo ? `
|
|
95
|
+
<div class="tn-hist-cell tn-hist-cell-a">${renderTurn(round.a)}</div>
|
|
96
|
+
<div class="tn-hist-cell tn-hist-cell-round">${round.roundNum}</div>
|
|
97
|
+
` : `
|
|
92
98
|
<div class="tn-hist-cell tn-hist-cell-a">${renderTurn(round.a)}</div>
|
|
93
99
|
<div class="tn-hist-cell tn-hist-cell-round">${round.roundNum}</div>
|
|
94
100
|
<div class="tn-hist-cell tn-hist-cell-b">${renderTurn(round.b)}</div>
|
|
@@ -100,7 +106,8 @@ function renderHistoryList(score: DartsMatchScore, t: DartsScoreKeeperUI): void
|
|
|
100
106
|
const histGrid = el('tn-history-list');
|
|
101
107
|
if (!histGrid) return;
|
|
102
108
|
histGrid.innerHTML = '';
|
|
103
|
-
renderHistoryHeader(t, histGrid);
|
|
109
|
+
renderHistoryHeader(score, t, histGrid);
|
|
110
|
+
histGrid.classList.toggle('tn-history-solo', score.mode === 'solo');
|
|
104
111
|
interface RoundRow {
|
|
105
112
|
roundNum: number;
|
|
106
113
|
a?: typeof score.history[0];
|
|
@@ -120,7 +127,7 @@ function renderHistoryList(score: DartsMatchScore, t: DartsScoreKeeperUI): void
|
|
|
120
127
|
}
|
|
121
128
|
});
|
|
122
129
|
rounds.slice(-6).reverse().forEach((round) => {
|
|
123
|
-
renderHistoryRow(round, histGrid);
|
|
130
|
+
renderHistoryRow(round, histGrid, score.mode === 'solo');
|
|
124
131
|
});
|
|
125
132
|
}
|
|
126
133
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createInitialScore, type DartsMatchScore } from './game-logic';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'dt_match_state';
|
|
4
|
+
|
|
5
|
+
export function loadInitialScore(): DartsMatchScore {
|
|
6
|
+
try {
|
|
7
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
8
|
+
if (stored) {
|
|
9
|
+
const saved = JSON.parse(stored) as DartsMatchScore;
|
|
10
|
+
return { ...saved, mode: saved.mode ?? 'versus' };
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
return createInitialScore();
|
|
14
|
+
}
|
|
15
|
+
return createInitialScore();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function persistScore(score: DartsMatchScore): void {
|
|
19
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(score));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clearStoredScore(): void {
|
|
23
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
24
|
+
}
|