@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.
- package/package.json +1 -1
- package/src/entries.ts +4 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/decision-wheel/client.ts +2 -0
- package/src/tool/decision-wheel/components/SegmentEditor.astro +1 -1
- package/src/tool/decision-wheel/decision-wheel.css +23 -7
- package/src/tool/hidden-role-dealer/bibliography.astro +16 -0
- package/src/tool/hidden-role-dealer/bibliography.ts +12 -0
- package/src/tool/hidden-role-dealer/client.ts +265 -0
- package/src/tool/hidden-role-dealer/component.astro +184 -0
- package/src/tool/hidden-role-dealer/entry.ts +145 -0
- package/src/tool/hidden-role-dealer/hidden-role-dealer.css +604 -0
- package/src/tool/hidden-role-dealer/i18n/de.ts +195 -0
- package/src/tool/hidden-role-dealer/i18n/en.ts +279 -0
- package/src/tool/hidden-role-dealer/i18n/es.ts +279 -0
- package/src/tool/hidden-role-dealer/i18n/fr.ts +215 -0
- package/src/tool/hidden-role-dealer/i18n/id.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/it.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/ja.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/ko.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/nl.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/pl.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/pt.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/ru.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/sv.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/tr.ts +191 -0
- package/src/tool/hidden-role-dealer/i18n/zh.ts +191 -0
- package/src/tool/hidden-role-dealer/index.ts +10 -0
- package/src/tool/hidden-role-dealer/logic.test.ts +99 -0
- package/src/tool/hidden-role-dealer/logic.ts +134 -0
- package/src/tool/hidden-role-dealer/modules/custom-role-manager.ts +35 -0
- package/src/tool/hidden-role-dealer/modules/dealer-logic.ts +73 -0
- package/src/tool/hidden-role-dealer/modules/hold-controller.ts +38 -0
- package/src/tool/hidden-role-dealer/modules/particle-effects.ts +40 -0
- package/src/tool/hidden-role-dealer/modules/player-manager.ts +44 -0
- package/src/tool/hidden-role-dealer/modules/presets.ts +46 -0
- package/src/tool/hidden-role-dealer/modules/setup-handlers.ts +34 -0
- package/src/tool/hidden-role-dealer/modules/storage.ts +23 -0
- package/src/tool/hidden-role-dealer/modules/utils.ts +18 -0
- package/src/tool/hidden-role-dealer/modules/wizard-html.ts +161 -0
- package/src/tool/hidden-role-dealer/modules/wizard-renderer.ts +161 -0
- package/src/tool/hidden-role-dealer/modules/writer-handler.ts +70 -0
- package/src/tool/hidden-role-dealer/seo.astro +16 -0
- package/src/tool/hidden-role-dealer/types.ts +31 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
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
|
|
|
@@ -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:
|
|
264
|
-
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-wrap: wrap;
|
|
265
265
|
gap: 0.5rem;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
.preset-btn {
|
|
269
|
-
|
|
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:
|
|
375
|
-
padding: 0.5rem 0.
|
|
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>
|