@jjlmoya/utils-tabletop 1.10.0 → 1.12.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 (76) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +8 -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/dice-roller-simulator/component.astro +116 -26
  9. package/src/tool/hidden-role-dealer/bibliography.astro +16 -0
  10. package/src/tool/hidden-role-dealer/bibliography.ts +12 -0
  11. package/src/tool/hidden-role-dealer/client.ts +265 -0
  12. package/src/tool/hidden-role-dealer/component.astro +184 -0
  13. package/src/tool/hidden-role-dealer/entry.ts +145 -0
  14. package/src/tool/hidden-role-dealer/hidden-role-dealer.css +604 -0
  15. package/src/tool/hidden-role-dealer/i18n/de.ts +195 -0
  16. package/src/tool/hidden-role-dealer/i18n/en.ts +279 -0
  17. package/src/tool/hidden-role-dealer/i18n/es.ts +279 -0
  18. package/src/tool/hidden-role-dealer/i18n/fr.ts +215 -0
  19. package/src/tool/hidden-role-dealer/i18n/id.ts +191 -0
  20. package/src/tool/hidden-role-dealer/i18n/it.ts +191 -0
  21. package/src/tool/hidden-role-dealer/i18n/ja.ts +191 -0
  22. package/src/tool/hidden-role-dealer/i18n/ko.ts +191 -0
  23. package/src/tool/hidden-role-dealer/i18n/nl.ts +191 -0
  24. package/src/tool/hidden-role-dealer/i18n/pl.ts +191 -0
  25. package/src/tool/hidden-role-dealer/i18n/pt.ts +191 -0
  26. package/src/tool/hidden-role-dealer/i18n/ru.ts +191 -0
  27. package/src/tool/hidden-role-dealer/i18n/sv.ts +191 -0
  28. package/src/tool/hidden-role-dealer/i18n/tr.ts +191 -0
  29. package/src/tool/hidden-role-dealer/i18n/zh.ts +191 -0
  30. package/src/tool/hidden-role-dealer/index.ts +10 -0
  31. package/src/tool/hidden-role-dealer/logic.test.ts +99 -0
  32. package/src/tool/hidden-role-dealer/logic.ts +134 -0
  33. package/src/tool/hidden-role-dealer/modules/custom-role-manager.ts +35 -0
  34. package/src/tool/hidden-role-dealer/modules/dealer-logic.ts +73 -0
  35. package/src/tool/hidden-role-dealer/modules/hold-controller.ts +38 -0
  36. package/src/tool/hidden-role-dealer/modules/particle-effects.ts +40 -0
  37. package/src/tool/hidden-role-dealer/modules/player-manager.ts +44 -0
  38. package/src/tool/hidden-role-dealer/modules/presets.ts +46 -0
  39. package/src/tool/hidden-role-dealer/modules/setup-handlers.ts +34 -0
  40. package/src/tool/hidden-role-dealer/modules/storage.ts +23 -0
  41. package/src/tool/hidden-role-dealer/modules/utils.ts +18 -0
  42. package/src/tool/hidden-role-dealer/modules/wizard-html.ts +161 -0
  43. package/src/tool/hidden-role-dealer/modules/wizard-renderer.ts +161 -0
  44. package/src/tool/hidden-role-dealer/modules/writer-handler.ts +70 -0
  45. package/src/tool/hidden-role-dealer/seo.astro +16 -0
  46. package/src/tool/hidden-role-dealer/types.ts +31 -0
  47. package/src/tool/scatter-direction-selector/animation.ts +278 -0
  48. package/src/tool/scatter-direction-selector/bibliography.astro +16 -0
  49. package/src/tool/scatter-direction-selector/bibliography.ts +12 -0
  50. package/src/tool/scatter-direction-selector/client.ts +296 -0
  51. package/src/tool/scatter-direction-selector/component.astro +184 -0
  52. package/src/tool/scatter-direction-selector/entry.ts +59 -0
  53. package/src/tool/scatter-direction-selector/history.ts +71 -0
  54. package/src/tool/scatter-direction-selector/i18n/de.ts +143 -0
  55. package/src/tool/scatter-direction-selector/i18n/en.ts +231 -0
  56. package/src/tool/scatter-direction-selector/i18n/es.ts +143 -0
  57. package/src/tool/scatter-direction-selector/i18n/fr.ts +143 -0
  58. package/src/tool/scatter-direction-selector/i18n/id.ts +143 -0
  59. package/src/tool/scatter-direction-selector/i18n/it.ts +143 -0
  60. package/src/tool/scatter-direction-selector/i18n/ja.ts +143 -0
  61. package/src/tool/scatter-direction-selector/i18n/ko.ts +143 -0
  62. package/src/tool/scatter-direction-selector/i18n/nl.ts +143 -0
  63. package/src/tool/scatter-direction-selector/i18n/pl.ts +143 -0
  64. package/src/tool/scatter-direction-selector/i18n/pt.ts +143 -0
  65. package/src/tool/scatter-direction-selector/i18n/ru.ts +143 -0
  66. package/src/tool/scatter-direction-selector/i18n/sv.ts +143 -0
  67. package/src/tool/scatter-direction-selector/i18n/tr.ts +143 -0
  68. package/src/tool/scatter-direction-selector/i18n/zh.ts +143 -0
  69. package/src/tool/scatter-direction-selector/index.ts +10 -0
  70. package/src/tool/scatter-direction-selector/logic.test.ts +62 -0
  71. package/src/tool/scatter-direction-selector/logic.ts +52 -0
  72. package/src/tool/scatter-direction-selector/scatter-direction-selector.css +794 -0
  73. package/src/tool/scatter-direction-selector/seo.astro +16 -0
  74. package/src/tool/scatter-direction-selector/settings.ts +81 -0
  75. package/src/tool/scatter-direction-selector/sound.ts +22 -0
  76. package/src/tools.ts +4 -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.12.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,10 @@ 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';
19
+ export { scatterDirectionSelector } from './tool/scatter-direction-selector/entry';
20
+ export type { ScatterSelectorUI, ScatterSelectorLocaleContent } from './tool/scatter-direction-selector/entry';
17
21
  export { tabletopCategory } from './category';
18
22
 
19
23
  import { diceRollerSimulator } from './tool/dice-roller-simulator/entry';
@@ -25,6 +29,8 @@ import { fantasyRunesTranslator } from './tool/fantasy-runes-translator/entry';
25
29
  import { decisionWheel } from './tool/decision-wheel/entry';
26
30
  import { investigationBoard } from './tool/investigation-board/entry';
27
31
  import { lunarTideTracker } from './tool/lunar-tide-tracker/entry';
32
+ import { hiddenRoleDealer } from './tool/hidden-role-dealer/entry';
33
+ import { scatterDirectionSelector } from './tool/scatter-direction-selector/entry';
28
34
 
29
35
  export const ALL_ENTRIES = [
30
36
  diceRollerSimulator,
@@ -36,5 +42,7 @@ export const ALL_ENTRIES = [
36
42
  decisionWheel,
37
43
  investigationBoard,
38
44
  lunarTideTracker,
45
+ hiddenRoleDealer,
46
+ scatterDirectionSelector,
39
47
  ];
40
48
 
@@ -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(11);
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(11);
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 {
@@ -1,8 +1,8 @@
1
1
  ---
2
- import DiceGrid from './components/DiceGrid.astro';
3
- import RollHistorySection from './components/RollHistorySection.astro';
4
- import StatsPanel from './components/StatsPanel.astro';
5
- import RollResultBanner from './components/RollResultBanner.astro';
2
+ import DiceGrid from "./components/DiceGrid.astro";
3
+ import RollHistorySection from "./components/RollHistorySection.astro";
4
+ import StatsPanel from "./components/StatsPanel.astro";
5
+ import RollResultBanner from "./components/RollResultBanner.astro";
6
6
 
7
7
  interface Props {
8
8
  ui: Record<string, string>;
@@ -15,33 +15,117 @@ const { ui } = Astro.props;
15
15
  <div class="tabletop-gaming-dashboard">
16
16
  <div class="forge-and-tray-grid">
17
17
  <DiceGrid labels={ui} />
18
-
18
+
19
19
  <div class="dashboard-center">
20
20
  <div class="rolling-tray-container">
21
21
  <div class="tray-rim">
22
22
  <div class="tray-felt" id="roll-arena">
23
23
  <div class="felt-placeholder" id="arena-placeholder">
24
- <svg viewBox="0 0 100 100" class="felt-crest-svg" aria-hidden="true">
25
- <circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" />
26
- <circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" stroke-opacity="0.08" stroke-width="1" stroke-dasharray="3,3" />
27
-
28
- <polygon points="50,15 80,32 80,68 50,85 20,68 20,32" fill="currentColor" fill-opacity="0.03" stroke="currentColor" stroke-width="2" />
29
-
30
- <polygon points="50,38 72,50 50,75 28,50" fill="none" stroke="currentColor" stroke-width="1.5" />
31
- <line x1="50" y1="15" x2="50" y2="38" stroke="currentColor" stroke-width="1.5" />
32
- <line x1="20" y1="32" x2="28" y2="50" stroke="currentColor" stroke-width="1.5" />
33
- <line x1="80" y1="32" x2="72" y2="50" stroke="currentColor" stroke-width="1.5" />
34
- <line x1="20" y1="68" x2="28" y2="50" stroke="currentColor" stroke-width="1.5" />
35
- <line x1="80" y1="68" x2="72" y2="50" stroke="currentColor" stroke-width="1.5" />
36
- <line x1="50" y1="85" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
37
- <line x1="28" y1="50" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
38
- <line x1="72" y1="50" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
39
-
40
- <text x="50" y="58" font-size="12" font-weight="900" text-anchor="middle" fill="currentColor" font-family="'Outfit', sans-serif">20</text>
24
+ <svg
25
+ viewBox="0 0 100 100"
26
+ class="felt-crest-svg"
27
+ aria-hidden="true"
28
+ >
29
+ <circle
30
+ cx="50"
31
+ cy="50"
32
+ r="45"
33
+ fill="none"
34
+ stroke="currentColor"
35
+ stroke-opacity="0.15"
36
+ stroke-width="1"></circle>
37
+ <circle
38
+ cx="50"
39
+ cy="50"
40
+ r="40"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-opacity="0.08"
44
+ stroke-width="1"
45
+ stroke-dasharray="3,3"></circle>
46
+
47
+ <polygon
48
+ points="50,15 80,32 80,68 50,85 20,68 20,32"
49
+ fill="currentColor"
50
+ fill-opacity="0.03"
51
+ stroke="currentColor"
52
+ stroke-width="2"></polygon>
53
+
54
+ <polygon
55
+ points="50,38 72,50 50,75 28,50"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ stroke-width="1.5"></polygon>
59
+ <line
60
+ x1="50"
61
+ y1="15"
62
+ x2="50"
63
+ y2="38"
64
+ stroke="currentColor"
65
+ stroke-width="1.5"></line>
66
+ <line
67
+ x1="20"
68
+ y1="32"
69
+ x2="28"
70
+ y2="50"
71
+ stroke="currentColor"
72
+ stroke-width="1.5"></line>
73
+ <line
74
+ x1="80"
75
+ y1="32"
76
+ x2="72"
77
+ y2="50"
78
+ stroke="currentColor"
79
+ stroke-width="1.5"></line>
80
+ <line
81
+ x1="20"
82
+ y1="68"
83
+ x2="28"
84
+ y2="50"
85
+ stroke="currentColor"
86
+ stroke-width="1.5"></line>
87
+ <line
88
+ x1="80"
89
+ y1="68"
90
+ x2="72"
91
+ y2="50"
92
+ stroke="currentColor"
93
+ stroke-width="1.5"></line>
94
+ <line
95
+ x1="50"
96
+ y1="85"
97
+ x2="50"
98
+ y2="75"
99
+ stroke="currentColor"
100
+ stroke-width="1.5"></line>
101
+ <line
102
+ x1="28"
103
+ y1="50"
104
+ x2="50"
105
+ y2="75"
106
+ stroke="currentColor"
107
+ stroke-width="1.5"></line>
108
+ <line
109
+ x1="72"
110
+ y1="50"
111
+ x2="50"
112
+ y2="75"
113
+ stroke="currentColor"
114
+ stroke-width="1.5"></line>
115
+
116
+ <text
117
+ x="50"
118
+ y="58"
119
+ font-size="12"
120
+ font-weight="900"
121
+ text-anchor="middle"
122
+ fill="currentColor">20</text
123
+ >
41
124
  </svg>
42
125
  <p class="felt-hint-text">Add dice to roll</p>
43
126
  </div>
44
- <div class="felt-dice-container" id="dice-results-container"></div>
127
+ <div class="felt-dice-container" id="dice-results-container">
128
+ </div>
45
129
  </div>
46
130
  </div>
47
131
  </div>
@@ -56,10 +140,16 @@ const { ui } = Astro.props;
56
140
 
57
141
  <div class="tabletop-extra-tabs">
58
142
  <div class="tabs-nav">
59
- <button type="button" class="extra-tab-btn active" data-extra-tab="logs-panel">Roll Logs</button>
60
- <button type="button" class="extra-tab-btn" data-extra-tab="stats-panel">Statistics</button>
143
+ <button
144
+ type="button"
145
+ class="extra-tab-btn active"
146
+ data-extra-tab="logs-panel">Roll Logs</button
147
+ >
148
+ <button type="button" class="extra-tab-btn" data-extra-tab="stats-panel"
149
+ >Statistics</button
150
+ >
61
151
  </div>
62
-
152
+
63
153
  <div class="extra-tabs-content">
64
154
  <div class="extra-tab-pane active" id="logs-panel">
65
155
  <RollHistorySection labels={ui} />
@@ -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
+ }