@jjlmoya/utils-tabletop 1.10.0 → 1.11.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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -0
  3. package/src/tests/locale_completeness.test.ts +1 -1
  4. package/src/tests/tool_validation.test.ts +1 -1
  5. package/src/tool/decision-wheel/client.ts +2 -0
  6. package/src/tool/decision-wheel/components/SegmentEditor.astro +1 -1
  7. package/src/tool/decision-wheel/decision-wheel.css +23 -7
  8. package/src/tool/hidden-role-dealer/bibliography.astro +16 -0
  9. package/src/tool/hidden-role-dealer/bibliography.ts +12 -0
  10. package/src/tool/hidden-role-dealer/client.ts +265 -0
  11. package/src/tool/hidden-role-dealer/component.astro +184 -0
  12. package/src/tool/hidden-role-dealer/entry.ts +145 -0
  13. package/src/tool/hidden-role-dealer/hidden-role-dealer.css +604 -0
  14. package/src/tool/hidden-role-dealer/i18n/de.ts +195 -0
  15. package/src/tool/hidden-role-dealer/i18n/en.ts +279 -0
  16. package/src/tool/hidden-role-dealer/i18n/es.ts +279 -0
  17. package/src/tool/hidden-role-dealer/i18n/fr.ts +215 -0
  18. package/src/tool/hidden-role-dealer/i18n/id.ts +191 -0
  19. package/src/tool/hidden-role-dealer/i18n/it.ts +191 -0
  20. package/src/tool/hidden-role-dealer/i18n/ja.ts +191 -0
  21. package/src/tool/hidden-role-dealer/i18n/ko.ts +191 -0
  22. package/src/tool/hidden-role-dealer/i18n/nl.ts +191 -0
  23. package/src/tool/hidden-role-dealer/i18n/pl.ts +191 -0
  24. package/src/tool/hidden-role-dealer/i18n/pt.ts +191 -0
  25. package/src/tool/hidden-role-dealer/i18n/ru.ts +191 -0
  26. package/src/tool/hidden-role-dealer/i18n/sv.ts +191 -0
  27. package/src/tool/hidden-role-dealer/i18n/tr.ts +191 -0
  28. package/src/tool/hidden-role-dealer/i18n/zh.ts +191 -0
  29. package/src/tool/hidden-role-dealer/index.ts +10 -0
  30. package/src/tool/hidden-role-dealer/logic.test.ts +99 -0
  31. package/src/tool/hidden-role-dealer/logic.ts +134 -0
  32. package/src/tool/hidden-role-dealer/modules/custom-role-manager.ts +35 -0
  33. package/src/tool/hidden-role-dealer/modules/dealer-logic.ts +73 -0
  34. package/src/tool/hidden-role-dealer/modules/hold-controller.ts +38 -0
  35. package/src/tool/hidden-role-dealer/modules/particle-effects.ts +40 -0
  36. package/src/tool/hidden-role-dealer/modules/player-manager.ts +44 -0
  37. package/src/tool/hidden-role-dealer/modules/presets.ts +46 -0
  38. package/src/tool/hidden-role-dealer/modules/setup-handlers.ts +34 -0
  39. package/src/tool/hidden-role-dealer/modules/storage.ts +23 -0
  40. package/src/tool/hidden-role-dealer/modules/utils.ts +18 -0
  41. package/src/tool/hidden-role-dealer/modules/wizard-html.ts +161 -0
  42. package/src/tool/hidden-role-dealer/modules/wizard-renderer.ts +161 -0
  43. package/src/tool/hidden-role-dealer/modules/writer-handler.ts +70 -0
  44. package/src/tool/hidden-role-dealer/seo.astro +16 -0
  45. package/src/tool/hidden-role-dealer/types.ts +31 -0
  46. package/src/tools.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-tabletop",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -14,6 +14,8 @@ export { investigationBoard } from './tool/investigation-board/entry';
14
14
  export type { InvestigationBoardUI, InvestigationBoardLocaleContent } from './tool/investigation-board/entry';
15
15
  export { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
16
16
  export type { LunarTideUI, LunarTideLocaleContent } from './tool/lunar-tide-tracker/entry';
17
+ export { hiddenRoleDealer } from './tool/hidden-role-dealer/entry';
18
+ export type { HiddenRoleDealerUI, HiddenRoleDealerLocaleContent } from './tool/hidden-role-dealer/entry';
17
19
  export { tabletopCategory } from './category';
18
20
 
19
21
  import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
@@ -25,6 +27,7 @@ import { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
25
27
  import { decisionWheel } from './tool/decision-wheel/entry';
26
28
  import { investigationBoard } from './tool/investigation-board/entry';
27
29
  import { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
30
+ import { hiddenRoleDealer } from './tool/hidden-role-dealer/entry';
28
31
 
29
32
  export const ALL_ENTRIES = [
30
33
  diceRollerSimulator,
@@ -36,5 +39,6 @@ export const ALL_ENTRIES = [
36
39
  decisionWheel,
37
40
  investigationBoard,
38
41
  lunarTideTracker,
42
+ hiddenRoleDealer,
39
43
  ];
40
44
 
@@ -22,7 +22,7 @@ describe('Locale Completeness Validation', () => {
22
22
  });
23
23
 
24
24
  it('all tools registered', () => {
25
- expect(ALL_TOOLS.length).toBe(9);
25
+ expect(ALL_TOOLS.length).toBe(10);
26
26
  });
27
27
 
28
28
 
@@ -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(9);
8
+ expect(ALL_TOOLS.length).toBe(10);
9
9
  });
10
10
 
11
11
 
@@ -197,6 +197,8 @@ addBtn.addEventListener('click', () => {
197
197
  document.querySelectorAll('.preset-btn').forEach((btn) => {
198
198
  btn.addEventListener('click', (e) => {
199
199
  const el = e.currentTarget as HTMLButtonElement;
200
+ document.querySelectorAll('.preset-btn').forEach((b) => b.classList.remove('active'));
201
+ el.classList.add('active');
200
202
  const key = el.dataset.preset as PresetKey;
201
203
  loadPreset(key);
202
204
  });
@@ -9,7 +9,7 @@ const { labels } = Astro.props;
9
9
  <div class="editor-card">
10
10
  <span class="editor-label">{labels.presetLabel || 'Presets'}</span>
11
11
  <div class="preset-group">
12
- <button type="button" class="preset-btn" data-preset="yesno">{labels.presetYesNo || 'Yes / No'}</button>
12
+ <button type="button" class="preset-btn active" data-preset="yesno">{labels.presetYesNo || 'Yes / No'}</button>
13
13
  <button type="button" class="preset-btn" data-preset="numbers">{labels.presetNumbers || 'Numbers 1-6'}</button>
14
14
  <button type="button" class="preset-btn" data-preset="actions">{labels.presetActions || 'Actions'}</button>
15
15
  <button type="button" class="preset-btn" data-preset="d20">{labels.presetD20 || 'D20'}</button>
@@ -260,13 +260,15 @@
260
260
  }
261
261
 
262
262
  .preset-group {
263
- display: grid;
264
- grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
263
+ display: flex;
264
+ flex-wrap: wrap;
265
265
  gap: 0.5rem;
266
266
  }
267
267
 
268
268
  .preset-btn {
269
- padding: 0.55rem 0.75rem;
269
+ flex: 1 1 auto;
270
+ min-width: fit-content;
271
+ padding: 0.55rem 0.85rem;
270
272
  border: 1px solid var(--border-color);
271
273
  border-radius: 0.6rem;
272
274
  background: var(--bg-surface);
@@ -276,6 +278,7 @@
276
278
  cursor: pointer;
277
279
  transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
278
280
  text-align: center;
281
+ white-space: nowrap;
279
282
  }
280
283
 
281
284
  .preset-btn:hover {
@@ -286,6 +289,13 @@
286
289
  box-shadow: 0 4px 12px var(--wheel-glow);
287
290
  }
288
291
 
292
+ .preset-btn.active {
293
+ background: var(--wheel-accent);
294
+ color: var(--color-white);
295
+ border-color: var(--wheel-accent);
296
+ box-shadow: 0 4px 12px var(--wheel-glow);
297
+ }
298
+
289
299
  .segment-list {
290
300
  display: flex;
291
301
  flex-direction: column;
@@ -371,18 +381,24 @@
371
381
  }
372
382
 
373
383
  .segment-weight {
374
- width: 3.85rem;
375
- padding: 0.5rem 0.25rem;
384
+ width: 4.85rem;
385
+ padding: 0.5rem 1.5rem 0.5rem 0.65rem;
376
386
  border: 1px solid var(--border-color);
377
387
  border-radius: 0.6rem;
378
- background: var(--bg-page);
388
+ background-color: var(--bg-page);
389
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'></polyline></svg>");
390
+ background-repeat: no-repeat;
391
+ background-position: right 0.45rem center;
392
+ background-size: 0.85rem;
379
393
  color: var(--text-base);
380
394
  font-weight: 800;
381
395
  font-size: 0.85rem;
382
- text-align: center;
383
396
  cursor: pointer;
384
397
  flex-shrink: 0;
385
398
  transition: all 0.2s ease;
399
+ appearance: none;
400
+ -webkit-appearance: none;
401
+ -moz-appearance: none;
386
402
  }
387
403
 
388
404
  .segment-weight:focus {
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { hiddenRoleDealer } 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 = hiddenRoleDealer.i18n[locale] || hiddenRoleDealer.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: 'Social Deduction Games Design',
6
+ url: 'https://en.wikipedia.org/wiki/Social_deduction_game',
7
+ },
8
+ {
9
+ name: 'Fisher-Yates Shuffle Algorithm',
10
+ url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
11
+ },
12
+ ];
@@ -0,0 +1,265 @@
1
+ import { renderWizardDOM } from './modules/wizard-html';
2
+ import { updateSetupDOM } from './modules/wizard-renderer';
3
+ import { parseInputValue } from './modules/utils';
4
+ import { performRoleAssignment } from './modules/dealer-logic';
5
+ import { PlayerManager } from './modules/player-manager';
6
+ import { CustomRoleManager } from './modules/custom-role-manager';
7
+ import { HoldController } from './modules/hold-controller';
8
+ import { savePreroundWord, saveWriterWord } from './modules/writer-handler';
9
+ import type { Player, ImpostorValues } from './types';
10
+
11
+ class HiddenRoleDealerApp {
12
+ private container: HTMLElement;
13
+ private playerMgr: PlayerManager;
14
+ private roleMgr: CustomRoleManager;
15
+ private holdCtrl: HoldController;
16
+ private activePreset: string = 'werewolf';
17
+ private currentStep: number = 0;
18
+ private ui: Record<string, string> = {};
19
+ private impostorSecret: string = '';
20
+ private preroundActive: boolean = false;
21
+ private preroundContributors: Player[] = [];
22
+ private preroundStep: number = 0;
23
+ private preroundWords: string[] = [];
24
+
25
+ constructor(container: HTMLElement) {
26
+ this.container = container;
27
+ this.ui = JSON.parse(container.getAttribute('data-ui') || '{}');
28
+ this.playerMgr = new PlayerManager();
29
+ this.roleMgr = new CustomRoleManager();
30
+ this.holdCtrl = new HoldController(container, () => this.showScreen('revealed'));
31
+ this.activePreset = localStorage.getItem('hidden-role-dealer-preset') || 'werewolf';
32
+ this.bindEvents();
33
+ this.render();
34
+ }
35
+
36
+ private bindEvents(): void {
37
+ const cancel = () => this.holdCtrl.end();
38
+ this.container.addEventListener('click', (e) => this.handleClick(e));
39
+ this.container.addEventListener('keydown', (e) => this.handleKeydown(e));
40
+ this.container.addEventListener('pointerdown', (e) => this.handlePointerdown(e));
41
+ this.container.addEventListener('change', () => this.render());
42
+ ['pointerup', 'pointerleave', 'pointercancel'].forEach(ev => {
43
+ this.container.addEventListener(ev, cancel);
44
+ });
45
+ }
46
+
47
+ private handleKeydown(e: KeyboardEvent): void {
48
+ if (e.key !== 'Enter') return;
49
+ const active = document.activeElement;
50
+ if (active === this.container.querySelector('#input-player-name')) this.playerMgr.addFromInput(this.container);
51
+ if (active === this.container.querySelector('#input-role-name')) this.roleMgr.addFromInput(this.container);
52
+ this.render();
53
+ }
54
+
55
+ private handlePointerdown(e: PointerEvent): void {
56
+ if ((e.target as HTMLElement).closest('#btn-hold-reveal')) this.holdCtrl.start(e.clientX, e.clientY);
57
+ }
58
+
59
+ private handleSetupClick(target: HTMLElement): boolean {
60
+ if (target.closest('#btn-add-player')) {
61
+ this.playerMgr.addFromInput(this.container);
62
+ this.render();
63
+ return true;
64
+ }
65
+ const btnDel = target.closest('.hrd-btn-delete') as HTMLElement;
66
+ if (btnDel) {
67
+ const id = btnDel.getAttribute('data-id');
68
+ if (id) { this.playerMgr.delete(id); this.render(); }
69
+ return true;
70
+ }
71
+ const card = target.closest('.hrd-preset-card') as HTMLElement;
72
+ if (card) {
73
+ const key = card.getAttribute('data-key');
74
+ if (key) {
75
+ this.activePreset = key;
76
+ localStorage.setItem('hidden-role-dealer-preset', key);
77
+ this.render();
78
+ }
79
+ return true;
80
+ }
81
+ return this.handleOtherSetupClick(target);
82
+ }
83
+
84
+ private handleOtherSetupClick(target: HTMLElement): boolean {
85
+ if (target.closest('#btn-clear-players')) {
86
+ this.playerMgr.clear();
87
+ this.render();
88
+ return true;
89
+ }
90
+ if (target.closest('#btn-deal-roles')) {
91
+ this.startDealing();
92
+ return true;
93
+ }
94
+ if (target.closest('#btn-add-role')) {
95
+ this.roleMgr.addFromInput(this.container);
96
+ this.render();
97
+ return true;
98
+ }
99
+ const btnDelRole = target.closest('.hrd-btn-delete-role') as HTMLElement;
100
+ if (btnDelRole) {
101
+ const id = btnDelRole.getAttribute('data-id');
102
+ if (id) { this.roleMgr.delete(id); this.render(); }
103
+ return true;
104
+ }
105
+ return false;
106
+ }
107
+
108
+ private handleClick(e: Event): void {
109
+ const target = e.target as HTMLElement;
110
+ if (this.handleSetupClick(target)) return;
111
+ if (target.closest('#btn-hide-role')) {
112
+ this.nextStep();
113
+ } else if (target.closest('#btn-restart')) {
114
+ this.resetGame();
115
+ } else if (target.closest('#btn-save-writer-word')) {
116
+ this.handleSaveWriterWord();
117
+ }
118
+ }
119
+
120
+ private getImpostorValues(): ImpostorValues {
121
+ const fixedEl = this.container.querySelector('#select-impostor-fixed-count') as HTMLSelectElement;
122
+ const percentEl = this.container.querySelector('#input-impostor-percent') as HTMLInputElement;
123
+ const minEl = this.container.querySelector('#input-impostor-min') as HTMLInputElement;
124
+ const maxEl = this.container.querySelector('#input-impostor-max') as HTMLInputElement;
125
+ return {
126
+ fixedVal: parseInputValue(fixedEl, 1),
127
+ percentVal: parseInputValue(percentEl, 25),
128
+ minVal: parseInputValue(minEl, 1),
129
+ maxVal: parseInputValue(maxEl, 2)
130
+ };
131
+ }
132
+
133
+ private getSelectorValue(selector: string, fallback: string): string {
134
+ const el = this.container.querySelector(selector) as HTMLInputElement | HTMLSelectElement;
135
+ return el ? el.value : fallback;
136
+ }
137
+
138
+ private startDealing(): void {
139
+ const players = this.playerMgr.getPlayers();
140
+ if (players.length < 3) return;
141
+ this.impostorSecret = '';
142
+ const mode = this.getSelectorValue('#select-impostor-mode', 'fixed');
143
+ const result = performRoleAssignment({
144
+ players,
145
+ activePreset: this.activePreset,
146
+ customRoles: this.roleMgr.getRoles(),
147
+ ui: this.ui,
148
+ vals: this.getImpostorValues(),
149
+ mode,
150
+ writerId: this.getSelectorValue('#select-impostor-writer', 'random'),
151
+ secretInput: this.getSelectorValue('#input-impostor-secret', '')
152
+ });
153
+ this.playerMgr.setPlayers(result.players);
154
+ this.preroundActive = result.preroundActive;
155
+ this.impostorSecret = result.impostorSecret;
156
+ if (this.preroundActive) {
157
+ this.preroundContributors = [...result.players].sort(() => Math.random() - 0.5);
158
+ this.preroundStep = 0;
159
+ this.preroundWords = [];
160
+ }
161
+ this.currentStep = 0;
162
+ this.showScreen('distributing');
163
+ }
164
+
165
+ private handleSavePreroundWord(secret: string): void {
166
+ const mode = this.getSelectorValue('#select-impostor-mode', 'fixed');
167
+ const result = savePreroundWord({
168
+ secret,
169
+ preroundWords: this.preroundWords,
170
+ preroundStep: this.preroundStep,
171
+ playerMgr: this.playerMgr,
172
+ vals: this.getImpostorValues(),
173
+ mode,
174
+ ui: this.ui
175
+ });
176
+ this.preroundWords = result.preroundWords;
177
+ this.preroundStep = result.preroundStep;
178
+ this.preroundActive = result.preroundActive;
179
+ this.impostorSecret = result.impostorSecret;
180
+ this.currentStep = 0;
181
+ this.showScreen('distributing');
182
+ }
183
+
184
+ private handleSaveWriterWord(): void {
185
+ const input = this.container.querySelector('#input-writer-secret') as HTMLInputElement;
186
+ const secret = input ? input.value : '';
187
+ if (this.preroundActive) {
188
+ this.handleSavePreroundWord(secret);
189
+ return;
190
+ }
191
+ this.impostorSecret = saveWriterWord(secret, this.playerMgr, this.ui);
192
+ this.nextStep();
193
+ }
194
+
195
+ private nextStep(): void {
196
+ this.currentStep++;
197
+ const players = this.playerMgr.getPlayers();
198
+ if (this.currentStep >= players.length) {
199
+ this.showScreen('complete');
200
+ } else {
201
+ this.showScreen('distributing');
202
+ }
203
+ }
204
+
205
+ private resetGame(): void {
206
+ const players = this.playerMgr.getPlayers();
207
+ players.forEach(p => {
208
+ p.role = null;
209
+ p.revealed = false;
210
+ });
211
+ this.playerMgr.setPlayers(players);
212
+ this.preroundActive = false;
213
+ this.preroundContributors = [];
214
+ this.preroundStep = 0;
215
+ this.preroundWords = [];
216
+ this.showScreen('setup');
217
+ }
218
+
219
+ private showScreen(state: 'setup' | 'distributing' | 'revealed' | 'complete'): void {
220
+ const isSetup = state === 'setup';
221
+ (this.container.querySelector('#view-setup') as HTMLElement).style.display = isSetup ? 'block' : 'none';
222
+ (this.container.querySelector('#view-wizard') as HTMLElement).style.display = isSetup ? 'none' : 'block';
223
+ if (isSetup) {
224
+ this.render();
225
+ } else {
226
+ const wizardView = this.container.querySelector('#view-wizard') as HTMLElement;
227
+ if (wizardView) {
228
+ renderWizardDOM({
229
+ state,
230
+ wizardView,
231
+ preroundActive: this.preroundActive,
232
+ preroundContributors: this.preroundContributors,
233
+ preroundStep: this.preroundStep,
234
+ players: this.playerMgr.getPlayers(),
235
+ currentStep: this.currentStep,
236
+ impostorSecret: this.impostorSecret,
237
+ ui: this.ui
238
+ });
239
+ }
240
+ }
241
+ }
242
+
243
+ private render(): void {
244
+ updateSetupDOM({
245
+ container: this.container,
246
+ players: this.playerMgr.getPlayers(),
247
+ customRoles: this.roleMgr.getRoles(),
248
+ activePreset: this.activePreset,
249
+ ui: this.ui
250
+ });
251
+ }
252
+ }
253
+
254
+ function init(): void {
255
+ const container = document.querySelector('.hrd-card[data-tool="hidden-role-dealer"]') as HTMLElement;
256
+ if (container) {
257
+ new HiddenRoleDealerApp(container);
258
+ }
259
+ }
260
+
261
+ if (document.readyState === 'loading') {
262
+ document.addEventListener('DOMContentLoaded', init);
263
+ } else {
264
+ init();
265
+ }
@@ -0,0 +1,184 @@
1
+ ---
2
+ import './hidden-role-dealer.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="hrd-card" data-tool="hidden-role-dealer" data-ui={JSON.stringify(ui)}>
12
+ <div id="view-setup">
13
+ <div class="hrd-setup-grid">
14
+ <div class="hrd-setup-section">
15
+ <div class="hrd-section-title">
16
+ <span>{ui.playerList}</span>
17
+ <button id="btn-clear-players" class="hrd-btn-secondary" type="button">
18
+ {ui.clearPlayers}
19
+ </button>
20
+ </div>
21
+ <div class="hrd-input-row">
22
+ <input
23
+ id="input-player-name"
24
+ class="hrd-input"
25
+ type="text"
26
+ placeholder={ui.addPlayerPlaceholder}
27
+ maxlength="20"
28
+ />
29
+ <button id="btn-add-player" class="hrd-btn-primary" type="button">
30
+ {ui.addPlayer}
31
+ </button>
32
+ </div>
33
+ <div id="players-list" class="hrd-players-container"></div>
34
+ </div>
35
+
36
+ <div class="hrd-setup-section">
37
+ <div class="hrd-section-title">
38
+ <span>{ui.presetsTitle}</span>
39
+ </div>
40
+ <div class="hrd-presets-list">
41
+ <button class="hrd-preset-card active" data-key="werewolf" type="button">
42
+ <div class="hrd-preset-name">{ui.werewolfName}</div>
43
+ <div class="hrd-preset-desc">{ui.werewolfDesc}</div>
44
+ </button>
45
+ <button class="hrd-preset-card" data-key="avalon" type="button">
46
+ <div class="hrd-preset-name">{ui.avalonName}</div>
47
+ <div class="hrd-preset-desc">{ui.avalonDesc}</div>
48
+ </button>
49
+ <button class="hrd-preset-card" data-key="hitler" type="button">
50
+ <div class="hrd-preset-name">{ui.hitlerName}</div>
51
+ <div class="hrd-preset-desc">{ui.hitlerDesc}</div>
52
+ </button>
53
+ <button class="hrd-preset-card" data-key="impostor" type="button">
54
+ <div class="hrd-preset-name">{ui.impostorPresetName}</div>
55
+ <div class="hrd-preset-desc">{ui.impostorPresetDesc}</div>
56
+ </button>
57
+ <button class="hrd-preset-card" data-key="custom" type="button">
58
+ <div class="hrd-preset-name">{ui.presetCustom}</div>
59
+ <div class="hrd-preset-desc">{ui.customDesc}</div>
60
+ </button>
61
+ </div>
62
+ <div id="impostor-settings" class="hrd-custom-section" style="display: none; margin-top: 1.5rem; border-top: 1px solid var(--dealer-border); padding-top: 1.5rem;">
63
+ <div class="hrd-section-title">
64
+ <span>{ui.impostorTitle}</span>
65
+ </div>
66
+ <div class="hrd-input-row" style="margin-bottom: 1rem;">
67
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
68
+ <span>{ui.impostorWriterLabel}</span>
69
+ <select id="select-impostor-writer" class="hrd-select" style="width: 100%;">
70
+ <option value="random">{ui.impostorWriterRandom}</option>
71
+ <option value="preround">{ui.impostorWriterPreround}</option>
72
+ </select>
73
+ <div id="impostor-writer-desc" style="font-size: 0.75rem; color: var(--dealer-text-muted); margin-top: 0.25rem; line-height: 1.4;"></div>
74
+ </label>
75
+ </div>
76
+ <div id="impostor-secret-wrapper" class="hrd-input-row" style="margin-bottom: 1rem;">
77
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
78
+ <span>{ui.impostorSharedSecretLabel}</span>
79
+ <input
80
+ id="input-impostor-secret"
81
+ class="hrd-input"
82
+ type="text"
83
+ placeholder={ui.impostorSharedSecretPlaceholder}
84
+ maxlength="50"
85
+ />
86
+ </label>
87
+ </div>
88
+ <div class="hrd-input-row" style="margin-bottom: 1rem;">
89
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
90
+ <span>{ui.impostorModeLabel}</span>
91
+ <select id="select-impostor-mode" class="hrd-select" style="width: 100%;">
92
+ <option value="fixed">{ui.impostorModeFixed}</option>
93
+ <option value="percentage">{ui.impostorModePercentage}</option>
94
+ <option value="range">{ui.impostorModeRange}</option>
95
+ </select>
96
+ </label>
97
+ </div>
98
+ <div id="impostor-fixed-wrapper" class="hrd-input-row" style="margin-bottom: 1rem;">
99
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
100
+ <span>{ui.impostorFixedLabel}</span>
101
+ <select id="select-impostor-fixed-count" class="hrd-select" style="width: 100%;">
102
+ <option value="1">1</option>
103
+ <option value="2">2</option>
104
+ <option value="3">3</option>
105
+ </select>
106
+ </label>
107
+ </div>
108
+ <div id="impostor-percent-wrapper" class="hrd-input-row" style="display: none; margin-bottom: 1rem;">
109
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
110
+ <span>{ui.impostorPercentageLabel}</span>
111
+ <input
112
+ id="input-impostor-percent"
113
+ class="hrd-input"
114
+ type="number"
115
+ value="25"
116
+ min="10"
117
+ max="90"
118
+ />
119
+ </label>
120
+ </div>
121
+ <div id="impostor-range-wrapper" class="hrd-input-row" style="display: none; margin-bottom: 1rem; gap: 1rem;">
122
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
123
+ <span>{ui.impostorRangeMinLabel}</span>
124
+ <input
125
+ id="input-impostor-min"
126
+ class="hrd-input"
127
+ type="number"
128
+ value="1"
129
+ min="1"
130
+ max="10"
131
+ />
132
+ </label>
133
+ <label style="flex: 1; display: flex; flex-direction: column; gap: 0.5rem; font-size: 0.875rem;">
134
+ <span>{ui.impostorRangeMaxLabel}</span>
135
+ <input
136
+ id="input-impostor-max"
137
+ class="hrd-input"
138
+ type="number"
139
+ value="2"
140
+ min="1"
141
+ max="10"
142
+ />
143
+ </label>
144
+ </div>
145
+ </div>
146
+ <div id="custom-roles-editor" class="hrd-custom-section" style="display: none; margin-top: 1.5rem; border-top: 1px solid var(--dealer-border); padding-top: 1.5rem;">
147
+ <div class="hrd-section-title">
148
+ <span>{ui.customRolesTitle}</span>
149
+ </div>
150
+ <div class="hrd-input-row" style="margin-bottom: 0.5rem;">
151
+ <input
152
+ id="input-role-name"
153
+ class="hrd-input"
154
+ type="text"
155
+ placeholder={ui.customRolePlaceholder}
156
+ maxlength="20"
157
+ style="flex: 2;"
158
+ />
159
+ <select id="select-role-alignment" class="hrd-select">
160
+ <option value="neutral">{ui.neutralLabel}</option>
161
+ <option value="good">{ui.goodLabel}</option>
162
+ <option value="evil">{ui.evilLabel}</option>
163
+ </select>
164
+ <button id="btn-add-role" class="hrd-btn-primary" type="button">
165
+ {ui.addPlayer}
166
+ </button>
167
+ </div>
168
+ <div id="custom-roles-list" class="hrd-players-container"></div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="hrd-deal-action-panel">
174
+ <div id="deal-warning" class="hrd-warning-box" style="display: none;"></div>
175
+ <button id="btn-deal-roles" class="hrd-btn-primary" type="button">
176
+ {ui.dealRoles}
177
+ </button>
178
+ </div>
179
+ </div>
180
+
181
+ <div id="view-wizard" class="hrd-wizard-container" style="display: none;"></div>
182
+ </div>
183
+
184
+ <script src="./client.ts"></script>