@jjlmoya/utils-tabletop 1.3.0 → 1.5.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 (171) hide show
  1. package/package.json +1 -1
  2. package/src/category/TabletopCategorySEO.astro +2 -7
  3. package/src/category/i18n/en.ts +4 -4
  4. package/src/category/i18n/ru.ts +2 -2
  5. package/src/category/i18n/zh.ts +8 -8
  6. package/src/category/index.ts +2 -1
  7. package/src/entries.ts +14 -1
  8. package/src/index.ts +1 -0
  9. package/src/pages/[locale]/[slug].astro +7 -6
  10. package/src/tests/locale_completeness.test.ts +1 -1
  11. package/src/tests/no_em_dash.test.ts +47 -0
  12. package/src/tests/seo_length.test.ts +1 -0
  13. package/src/tests/title_quality.test.ts +1 -1
  14. package/src/tests/tool_validation.test.ts +1 -1
  15. package/src/tool/board-game-timer/modules/ColorHelper.ts +2 -2
  16. package/src/tool/board-game-timer/modules/DuelViewManager.ts +4 -4
  17. package/src/tool/board-game-timer/modules/GameRunner.ts +1 -1
  18. package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +2 -2
  19. package/src/tool/board-game-timer/modules/SetupPanelManager.ts +1 -1
  20. package/src/tool/board-game-timer/modules/TimerEngine.ts +1 -1
  21. package/src/tool/board-game-timer/types.ts +1 -1
  22. package/src/tool/decision-wheel/audio.ts +78 -0
  23. package/src/tool/decision-wheel/bibliography.astro +16 -0
  24. package/src/tool/decision-wheel/bibliography.ts +8 -0
  25. package/src/tool/decision-wheel/client.ts +226 -0
  26. package/src/tool/decision-wheel/component.astro +48 -0
  27. package/src/tool/decision-wheel/components/SegmentEditor.astro +22 -0
  28. package/src/tool/decision-wheel/components/SpinHistory.astro +17 -0
  29. package/src/tool/decision-wheel/components/SpinResult.astro +13 -0
  30. package/src/tool/decision-wheel/components/WheelCanvas.astro +23 -0
  31. package/src/tool/decision-wheel/decision-wheel.css +632 -0
  32. package/src/tool/decision-wheel/drawing.ts +144 -0
  33. package/src/tool/decision-wheel/entry.ts +48 -0
  34. package/src/tool/decision-wheel/i18n/de.ts +97 -0
  35. package/src/tool/decision-wheel/i18n/en.ts +251 -0
  36. package/src/tool/decision-wheel/i18n/es.ts +93 -0
  37. package/src/tool/decision-wheel/i18n/fr.ts +93 -0
  38. package/src/tool/decision-wheel/i18n/id.ts +93 -0
  39. package/src/tool/decision-wheel/i18n/it.ts +93 -0
  40. package/src/tool/decision-wheel/i18n/ja.ts +93 -0
  41. package/src/tool/decision-wheel/i18n/ko.ts +93 -0
  42. package/src/tool/decision-wheel/i18n/nl.ts +93 -0
  43. package/src/tool/decision-wheel/i18n/pl.ts +93 -0
  44. package/src/tool/decision-wheel/i18n/pt.ts +93 -0
  45. package/src/tool/decision-wheel/i18n/ru.ts +75 -0
  46. package/src/tool/decision-wheel/i18n/sv.ts +93 -0
  47. package/src/tool/decision-wheel/i18n/tr.ts +93 -0
  48. package/src/tool/decision-wheel/i18n/zh.ts +93 -0
  49. package/src/tool/decision-wheel/index.ts +9 -0
  50. package/src/tool/decision-wheel/logic.test.ts +163 -0
  51. package/src/tool/decision-wheel/logic.ts +145 -0
  52. package/src/tool/decision-wheel/seo.astro +16 -0
  53. package/src/tool/decision-wheel/types.ts +14 -0
  54. package/src/tool/decision-wheel/ui.ts +121 -0
  55. package/src/tool/dice-roller-simulator/client.ts +2 -2
  56. package/src/tool/dice-roller-simulator/i18n/ru.ts +8 -8
  57. package/src/tool/dice-roller-simulator/modules/history.ts +1 -1
  58. package/src/tool/fantasy-runes-translator/bibliography.astro +16 -0
  59. package/src/tool/fantasy-runes-translator/bibliography.ts +16 -0
  60. package/src/tool/fantasy-runes-translator/client.ts +205 -0
  61. package/src/tool/fantasy-runes-translator/component.astro +95 -0
  62. package/src/tool/fantasy-runes-translator/entry.ts +56 -0
  63. package/src/tool/fantasy-runes-translator/fantasy-runes-translator.css +712 -0
  64. package/src/tool/fantasy-runes-translator/i18n/de.ts +139 -0
  65. package/src/tool/fantasy-runes-translator/i18n/en.ts +139 -0
  66. package/src/tool/fantasy-runes-translator/i18n/es.ts +139 -0
  67. package/src/tool/fantasy-runes-translator/i18n/fr.ts +139 -0
  68. package/src/tool/fantasy-runes-translator/i18n/id.ts +139 -0
  69. package/src/tool/fantasy-runes-translator/i18n/it.ts +139 -0
  70. package/src/tool/fantasy-runes-translator/i18n/ja.ts +139 -0
  71. package/src/tool/fantasy-runes-translator/i18n/ko.ts +139 -0
  72. package/src/tool/fantasy-runes-translator/i18n/nl.ts +139 -0
  73. package/src/tool/fantasy-runes-translator/i18n/pl.ts +139 -0
  74. package/src/tool/fantasy-runes-translator/i18n/pt.ts +139 -0
  75. package/src/tool/fantasy-runes-translator/i18n/ru.ts +139 -0
  76. package/src/tool/fantasy-runes-translator/i18n/sv.ts +139 -0
  77. package/src/tool/fantasy-runes-translator/i18n/tr.ts +139 -0
  78. package/src/tool/fantasy-runes-translator/i18n/zh.ts +139 -0
  79. package/src/tool/fantasy-runes-translator/index.ts +9 -0
  80. package/src/tool/fantasy-runes-translator/logic.ts +174 -0
  81. package/src/tool/fantasy-runes-translator/seo.astro +16 -0
  82. package/src/tool/fantasy-runes-translator/types.ts +26 -0
  83. package/src/tool/first-player-selector/bibliography.astro +16 -0
  84. package/src/tool/first-player-selector/bibliography.ts +12 -0
  85. package/src/tool/first-player-selector/client.ts +197 -0
  86. package/src/tool/first-player-selector/component.astro +77 -0
  87. package/src/tool/first-player-selector/entry.ts +42 -0
  88. package/src/tool/first-player-selector/first-player-selector.css +303 -0
  89. package/src/tool/first-player-selector/i18n/de.ts +226 -0
  90. package/src/tool/first-player-selector/i18n/en.ts +226 -0
  91. package/src/tool/first-player-selector/i18n/es.ts +226 -0
  92. package/src/tool/first-player-selector/i18n/fr.ts +227 -0
  93. package/src/tool/first-player-selector/i18n/id.ts +226 -0
  94. package/src/tool/first-player-selector/i18n/it.ts +227 -0
  95. package/src/tool/first-player-selector/i18n/ja.ts +227 -0
  96. package/src/tool/first-player-selector/i18n/ko.ts +227 -0
  97. package/src/tool/first-player-selector/i18n/nl.ts +226 -0
  98. package/src/tool/first-player-selector/i18n/pl.ts +227 -0
  99. package/src/tool/first-player-selector/i18n/pt.ts +226 -0
  100. package/src/tool/first-player-selector/i18n/ru.ts +227 -0
  101. package/src/tool/first-player-selector/i18n/sv.ts +226 -0
  102. package/src/tool/first-player-selector/i18n/tr.ts +226 -0
  103. package/src/tool/first-player-selector/i18n/zh.ts +227 -0
  104. package/src/tool/first-player-selector/index.ts +9 -0
  105. package/src/tool/first-player-selector/modules/AnimationEffects.ts +256 -0
  106. package/src/tool/first-player-selector/modules/SelectionEngine.ts +249 -0
  107. package/src/tool/first-player-selector/modules/SoundEffects.ts +87 -0
  108. package/src/tool/first-player-selector/modules/TouchTracker.ts +148 -0
  109. package/src/tool/first-player-selector/seo.astro +16 -0
  110. package/src/tool/first-player-selector/types.ts +21 -0
  111. package/src/tool/rpg-initiative-tracker/bibliography.astro +16 -0
  112. package/src/tool/rpg-initiative-tracker/bibliography.ts +12 -0
  113. package/src/tool/rpg-initiative-tracker/client.ts +201 -0
  114. package/src/tool/rpg-initiative-tracker/component.astro +23 -0
  115. package/src/tool/rpg-initiative-tracker/components/AddForm.astro +49 -0
  116. package/src/tool/rpg-initiative-tracker/components/CombatantList.astro +16 -0
  117. package/src/tool/rpg-initiative-tracker/components/TurnControls.astro +28 -0
  118. package/src/tool/rpg-initiative-tracker/entry.ts +50 -0
  119. package/src/tool/rpg-initiative-tracker/i18n/de.ts +266 -0
  120. package/src/tool/rpg-initiative-tracker/i18n/en.ts +263 -0
  121. package/src/tool/rpg-initiative-tracker/i18n/es.ts +264 -0
  122. package/src/tool/rpg-initiative-tracker/i18n/fr.ts +263 -0
  123. package/src/tool/rpg-initiative-tracker/i18n/id.ts +263 -0
  124. package/src/tool/rpg-initiative-tracker/i18n/it.ts +263 -0
  125. package/src/tool/rpg-initiative-tracker/i18n/ja.ts +263 -0
  126. package/src/tool/rpg-initiative-tracker/i18n/ko.ts +263 -0
  127. package/src/tool/rpg-initiative-tracker/i18n/nl.ts +263 -0
  128. package/src/tool/rpg-initiative-tracker/i18n/pl.ts +264 -0
  129. package/src/tool/rpg-initiative-tracker/i18n/pt.ts +263 -0
  130. package/src/tool/rpg-initiative-tracker/i18n/ru.ts +264 -0
  131. package/src/tool/rpg-initiative-tracker/i18n/sv.ts +263 -0
  132. package/src/tool/rpg-initiative-tracker/i18n/tr.ts +263 -0
  133. package/src/tool/rpg-initiative-tracker/i18n/zh.ts +263 -0
  134. package/src/tool/rpg-initiative-tracker/index.ts +9 -0
  135. package/src/tool/rpg-initiative-tracker/logic.test.ts +121 -0
  136. package/src/tool/rpg-initiative-tracker/logic.ts +122 -0
  137. package/src/tool/rpg-initiative-tracker/rpg-initiative-tracker.css +617 -0
  138. package/src/tool/rpg-initiative-tracker/seo.astro +16 -0
  139. package/src/tool/rpg-initiative-tracker/types.ts +15 -0
  140. package/src/tool/score-tracker/bibliography.astro +16 -0
  141. package/src/tool/score-tracker/bibliography.ts +12 -0
  142. package/src/tool/score-tracker/client.ts +248 -0
  143. package/src/tool/score-tracker/component.astro +34 -0
  144. package/src/tool/score-tracker/components/Leaderboard.astro +19 -0
  145. package/src/tool/score-tracker/components/PlayerSetup.astro +32 -0
  146. package/src/tool/score-tracker/components/RoundScoring.astro +22 -0
  147. package/src/tool/score-tracker/components/ScoreHistory.astro +25 -0
  148. package/src/tool/score-tracker/entry.ts +57 -0
  149. package/src/tool/score-tracker/i18n/de.ts +278 -0
  150. package/src/tool/score-tracker/i18n/en.ts +278 -0
  151. package/src/tool/score-tracker/i18n/es.ts +251 -0
  152. package/src/tool/score-tracker/i18n/fr.ts +278 -0
  153. package/src/tool/score-tracker/i18n/id.ts +278 -0
  154. package/src/tool/score-tracker/i18n/it.ts +278 -0
  155. package/src/tool/score-tracker/i18n/ja.ts +278 -0
  156. package/src/tool/score-tracker/i18n/ko.ts +278 -0
  157. package/src/tool/score-tracker/i18n/nl.ts +278 -0
  158. package/src/tool/score-tracker/i18n/pl.ts +278 -0
  159. package/src/tool/score-tracker/i18n/pt.ts +278 -0
  160. package/src/tool/score-tracker/i18n/ru.ts +278 -0
  161. package/src/tool/score-tracker/i18n/sv.ts +278 -0
  162. package/src/tool/score-tracker/i18n/tr.ts +278 -0
  163. package/src/tool/score-tracker/i18n/zh.ts +278 -0
  164. package/src/tool/score-tracker/index.ts +9 -0
  165. package/src/tool/score-tracker/logic.test.ts +83 -0
  166. package/src/tool/score-tracker/logic.ts +122 -0
  167. package/src/tool/score-tracker/score-tracker.css +1029 -0
  168. package/src/tool/score-tracker/seo.astro +16 -0
  169. package/src/tool/score-tracker/types.ts +17 -0
  170. package/src/tools.ts +10 -0
  171. package/src/types.ts +1 -2
@@ -0,0 +1,201 @@
1
+ import { addCombatant, removeCombatant, toggleStatus, startCombat, nextTurn, prevTurn, endCombat, createBlankState } from './logic';
2
+ import type { InitiativeState } from './types';
3
+
4
+ const STORAGE_KEY = 'initiative-tracker-state';
5
+
6
+ const wrapper = document.querySelector('.init-main-wrapper') as HTMLElement;
7
+ if (!wrapper) throw new Error('Initiative tracker wrapper not found');
8
+
9
+ const ui: Record<string, string> = JSON.parse(wrapper.dataset.ui || '{}');
10
+
11
+ function saveState(): void {
12
+ try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch { }
13
+ }
14
+
15
+ function loadState(): InitiativeState {
16
+ try {
17
+ const s = localStorage.getItem(STORAGE_KEY);
18
+ if (s) return JSON.parse(s) as InitiativeState;
19
+ } catch { }
20
+ return createBlankState();
21
+ }
22
+
23
+ let state: InitiativeState = loadState();
24
+
25
+ const $ = (id: string) => document.getElementById(id) as HTMLElement;
26
+ const combatantList = $('init-combatant-list');
27
+ const emptyState = $('init-empty-state');
28
+ const nameInput = $('init-name-input') as HTMLInputElement;
29
+ const initInput = $('init-value-input') as HTMLInputElement;
30
+ const modInput = $('init-mod-input') as HTMLInputElement;
31
+ const addBtn = $('init-add-btn');
32
+ const startBtn = $('init-start-btn');
33
+ const endBtn = $('init-end-btn');
34
+ const nextBtn = $('init-next-btn');
35
+ const prevBtn = $('init-prev-btn');
36
+ const roundLabel = $('init-round-label');
37
+
38
+ let currentType: 'player' | 'npc' = 'player';
39
+
40
+ function renderCombatantList(): void {
41
+ if (state.combatants.length === 0) {
42
+ combatantList.innerHTML = '';
43
+ combatantList.appendChild(emptyState);
44
+ return;
45
+ }
46
+ const items = state.combatants.map((c, i) => {
47
+ const isActive = state.isCombatActive && i === state.currentIndex;
48
+ const typeClass = c.isPlayer ? 'player-order' : 'npc-order';
49
+ const activeClass = isActive ? ' active' : '';
50
+ const npcClass = c.isPlayer ? '' : ' npc';
51
+ const orderClass = isActive ? ' active-order' : ' ' + typeClass;
52
+ const statusHtml = c.statuses.map(s => {
53
+ return `<span class="init-status-tag active" data-status="${s}">${s}</span>`;
54
+ }).join('');
55
+ return `<div class="init-combatant-item${activeClass}${npcClass}" data-cbt-id="${c.id}"><span class="init-cbt-order${orderClass}">${i + 1}</span><span class="init-cbt-name">${c.name}</span><span class="init-cbt-init">${c.initiative}</span><div class="init-cbt-statuses">${statusHtml}</div><div class="init-cbt-actions"><button type="button" class="init-btn-icon remove" data-action="remove" title="${ui.removeCombatant}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div></div>`;
56
+ }).join('');
57
+ combatantList.innerHTML = items;
58
+ combatantList.querySelectorAll('[data-action="remove"]').forEach(b => b.addEventListener('click', onRemove));
59
+ combatantList.querySelectorAll('.init-status-tag').forEach(t => {
60
+ t.addEventListener('click', onToggleStatus);
61
+ });
62
+ }
63
+
64
+ function onRemove(e: Event): void {
65
+ const item = (e.currentTarget as HTMLElement).closest('.init-combatant-item') as HTMLElement;
66
+ const id = item?.dataset.cbtId;
67
+ if (id) {
68
+ state = removeCombatant(state, id);
69
+ renderAll();
70
+ }
71
+ }
72
+
73
+ function onToggleStatus(e: Event): void {
74
+ const tag = e.currentTarget as HTMLElement;
75
+ const item = tag.closest('.init-combatant-item') as HTMLElement;
76
+ const id = item?.dataset.cbtId;
77
+ const status = tag.dataset.status;
78
+ if (id && status) {
79
+ state = toggleStatus(state, id, status);
80
+ renderAll();
81
+ }
82
+ }
83
+
84
+ function spawnTurnParticle(text: string): void {
85
+ const el = document.createElement('div');
86
+ el.className = 'init-turn-particle';
87
+ el.textContent = text;
88
+ el.style.cssText = 'left:50%;top:50%;position:fixed';
89
+ document.body.appendChild(el);
90
+ setTimeout(() => el.remove(), 1000);
91
+ }
92
+
93
+ function longPress(el: HTMLElement, duration: number, cb: () => void): void {
94
+ let timer: number | null = null;
95
+ const start = (): void => {
96
+ el.style.setProperty('--lp-dur', duration + 'ms');
97
+ el.classList.add('lp-active');
98
+ timer = window.setTimeout(() => {
99
+ el.classList.remove('lp-active');
100
+ cb();
101
+ }, duration);
102
+ };
103
+ const cancel = (): void => {
104
+ if (timer !== null) {
105
+ clearTimeout(timer);
106
+ timer = null;
107
+ }
108
+ el.classList.remove('lp-active');
109
+ };
110
+ el.addEventListener('mousedown', start);
111
+ el.addEventListener('mouseup', cancel);
112
+ el.addEventListener('mouseleave', cancel);
113
+ el.addEventListener('touchstart', start);
114
+ el.addEventListener('touchend', cancel);
115
+ el.addEventListener('touchcancel', cancel);
116
+ }
117
+
118
+ function getActiveCombatantName(): string {
119
+ const c = state.combatants[state.currentIndex];
120
+ return c ? c.name : '';
121
+ }
122
+
123
+ function renderAll(): void {
124
+ renderCombatantList();
125
+ const hasCombatants = state.combatants.length > 0;
126
+ const inCombat = state.isCombatActive;
127
+
128
+ if (inCombat && hasCombatants) {
129
+ roundLabel.textContent = ui.roundLabel + ' ' + state.round + ' - ' + ui.turnLabel + ' ' + (state.currentIndex + 1);
130
+ } else {
131
+ roundLabel.textContent = ui.roundLabel + ': --';
132
+ }
133
+
134
+ startBtn.style.display = inCombat ? 'none' : 'inline-block';
135
+ startBtn.disabled = !hasCombatants;
136
+ endBtn.style.display = inCombat ? 'inline-block' : 'none';
137
+ endBtn.disabled = !inCombat;
138
+ nextBtn.disabled = !inCombat;
139
+ prevBtn.disabled = !inCombat;
140
+
141
+ saveState();
142
+ }
143
+
144
+ addBtn.addEventListener('click', () => {
145
+ const name = nameInput.value.trim();
146
+ const initiative = parseInt(initInput.value, 10);
147
+ const modifier = parseInt(modInput.value, 10) || 0;
148
+ if (!name || isNaN(initiative)) return;
149
+ state = addCombatant(state, name, initiative, { modifier, isPlayer: currentType === 'player' });
150
+ nameInput.value = '';
151
+ initInput.value = '';
152
+ modInput.value = '';
153
+ nameInput.focus();
154
+ renderAll();
155
+ });
156
+
157
+ initInput.addEventListener('keydown', (e) => {
158
+ if (e.key === 'Enter') addBtn.click();
159
+ });
160
+
161
+ nameInput.addEventListener('keydown', (e) => {
162
+ if (e.key === 'Enter') initInput.focus();
163
+ });
164
+
165
+ const toggleBtns = document.querySelectorAll('.init-toggle-btn');
166
+ toggleBtns.forEach(btn => {
167
+ btn.addEventListener('click', () => {
168
+ toggleBtns.forEach(b => b.classList.remove('active'));
169
+ btn.classList.add('active');
170
+ currentType = btn.dataset.type as 'player' | 'npc';
171
+ });
172
+ });
173
+
174
+ startBtn.addEventListener('click', () => {
175
+ state = startCombat(state);
176
+ const name = getActiveCombatantName();
177
+ if (name) spawnTurnParticle(name + ' ' + ui.yourTurn);
178
+ renderAll();
179
+ });
180
+
181
+ nextBtn.addEventListener('click', () => {
182
+ state = nextTurn(state);
183
+ const name = getActiveCombatantName();
184
+ if (name) spawnTurnParticle(name + ' ' + ui.yourTurn);
185
+ renderAll();
186
+ });
187
+
188
+ prevBtn.addEventListener('click', () => {
189
+ state = prevTurn(state);
190
+ const name = getActiveCombatantName();
191
+ if (name) spawnTurnParticle(name + ' ' + ui.yourTurn);
192
+ renderAll();
193
+ });
194
+
195
+ longPress(endBtn, 1000, () => {
196
+ state = endCombat(state);
197
+ spawnTurnParticle(ui.combatEnded);
198
+ renderAll();
199
+ });
200
+
201
+ renderAll();
@@ -0,0 +1,23 @@
1
+ ---
2
+ import './rpg-initiative-tracker.css';
3
+ import AddForm from './components/AddForm.astro';
4
+ import CombatantList from './components/CombatantList.astro';
5
+ import TurnControls from './components/TurnControls.astro';
6
+
7
+ interface Props {
8
+ ui: Record<string, string>;
9
+ }
10
+
11
+ const { ui } = Astro.props;
12
+ ---
13
+
14
+ <div class="init-main-wrapper" data-ui={JSON.stringify(ui)}>
15
+ <div class="init-header">
16
+ <h2>{ui.title}</h2>
17
+ </div>
18
+ <AddForm labels={ui} />
19
+ <CombatantList labels={ui} />
20
+ <TurnControls labels={ui} />
21
+ </div>
22
+
23
+ <script src="./client.ts"></script>
@@ -0,0 +1,49 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="init-add-form" id="init-add-form">
10
+ <div class="init-form-group">
11
+ <label for="init-name-input">{labels.namePlaceholder}</label>
12
+ <input
13
+ type="text"
14
+ class="init-input"
15
+ id="init-name-input"
16
+ placeholder={labels.namePlaceholder}
17
+ autocomplete="off"
18
+ />
19
+ </div>
20
+ <div class="init-form-group">
21
+ <label for="init-value-input">{labels.initiativeLabel}</label>
22
+ <input
23
+ type="number"
24
+ class="init-input"
25
+ id="init-value-input"
26
+ placeholder={labels.initiativePlaceholder}
27
+ />
28
+ </div>
29
+ <div class="init-form-group">
30
+ <label for="init-mod-input">{labels.modifierLabel}</label>
31
+ <input
32
+ type="number"
33
+ class="init-input"
34
+ id="init-mod-input"
35
+ placeholder={labels.modifierPlaceholder}
36
+ />
37
+ </div>
38
+ <div class="init-toggle-group" id="init-type-toggle">
39
+ <button type="button" class="init-toggle-btn player active" data-type="player">
40
+ {labels.playerLabel}
41
+ </button>
42
+ <button type="button" class="init-toggle-btn npc" data-type="npc">
43
+ {labels.npcLabel}
44
+ </button>
45
+ </div>
46
+ <button type="button" class="init-btn-add" id="init-add-btn">
47
+ {labels.addButton}
48
+ </button>
49
+ </div>
@@ -0,0 +1,16 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div id="init-combatant-list" class="init-combatant-list">
10
+ <div id="init-empty-state" class="init-empty-state">
11
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
12
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
13
+ </svg>
14
+ <span>{labels.noCombatants}</span>
15
+ </div>
16
+ </div>
@@ -0,0 +1,28 @@
1
+ ---
2
+ interface Props {
3
+ labels: Record<string, string>;
4
+ }
5
+
6
+ const { labels } = Astro.props;
7
+ ---
8
+
9
+ <div class="init-round-display" id="init-round-display">
10
+ <span class="init-round-number" id="init-round-label">
11
+ {labels.roundLabel}: --
12
+ </span>
13
+ </div>
14
+
15
+ <div class="init-turn-controls" id="init-turn-controls">
16
+ <button type="button" class="init-turn-btn primary" id="init-start-btn">
17
+ {labels.startCombat}
18
+ </button>
19
+ <button type="button" class="init-turn-btn" id="init-prev-btn" disabled>
20
+ {labels.prevTurn}
21
+ </button>
22
+ <button type="button" class="init-turn-btn" id="init-next-btn" disabled>
23
+ {labels.nextTurn}
24
+ </button>
25
+ <button type="button" class="init-turn-btn danger" id="init-end-btn" disabled>
26
+ {labels.endCombat}
27
+ </button>
28
+ </div>
@@ -0,0 +1,50 @@
1
+ import type { TabletopToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export type InitiativeTrackerUI = {
4
+ title: string;
5
+ addCombatant: string;
6
+ removeCombatant: string;
7
+ playerLabel: string;
8
+ npcLabel: string;
9
+ initiativeLabel: string;
10
+ modifierLabel: string;
11
+ namePlaceholder: string;
12
+ initiativePlaceholder: string;
13
+ modifierPlaceholder: string;
14
+ addButton: string;
15
+ startCombat: string;
16
+ endCombat: string;
17
+ nextTurn: string;
18
+ prevTurn: string;
19
+ roundLabel: string;
20
+ turnLabel: string;
21
+ noCombatants: string;
22
+ statusInput: string;
23
+ combatStarted: string;
24
+ combatEnded: string;
25
+ yourTurn: string;
26
+ };
27
+
28
+ export type InitiativeTrackerLocaleContent = ToolLocaleContent<InitiativeTrackerUI>;
29
+
30
+ export const initiativeTracker: TabletopToolEntry<InitiativeTrackerUI> = {
31
+ id: 'rpg-initiative-tracker',
32
+ icons: { bg: 'mdi:sword-cross', fg: 'mdi:sword-cross' },
33
+ i18n: {
34
+ de: () => import('./i18n/de').then((m) => m.content),
35
+ en: () => import('./i18n/en').then((m) => m.content),
36
+ es: () => import('./i18n/es').then((m) => m.content),
37
+ fr: () => import('./i18n/fr').then((m) => m.content),
38
+ id: () => import('./i18n/id').then((m) => m.content),
39
+ it: () => import('./i18n/it').then((m) => m.content),
40
+ ja: () => import('./i18n/ja').then((m) => m.content),
41
+ ko: () => import('./i18n/ko').then((m) => m.content),
42
+ nl: () => import('./i18n/nl').then((m) => m.content),
43
+ pl: () => import('./i18n/pl').then((m) => m.content),
44
+ pt: () => import('./i18n/pt').then((m) => m.content),
45
+ ru: () => import('./i18n/ru').then((m) => m.content),
46
+ sv: () => import('./i18n/sv').then((m) => m.content),
47
+ tr: () => import('./i18n/tr').then((m) => m.content),
48
+ zh: () => import('./i18n/zh').then((m) => m.content),
49
+ },
50
+ };
@@ -0,0 +1,266 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { InitiativeTrackerUI } from '../entry';
4
+
5
+ const ui: InitiativeTrackerUI = {
6
+ title: 'InitiativenVerfolger',
7
+ addCombatant: 'Kämpfer hinzufügen',
8
+ removeCombatant: 'Entfernen',
9
+ playerLabel: 'Spieler',
10
+ npcLabel: 'NSC',
11
+ initiativeLabel: 'Initiative',
12
+ modifierLabel: 'Modifikator',
13
+ namePlaceholder: 'Charaktername',
14
+ initiativePlaceholder: 'Initiative',
15
+ modifierPlaceholder: 'Modifikator',
16
+ addButton: 'Hinzufügen',
17
+ startCombat: 'Kampf beginnen',
18
+ endCombat: 'Kampf beenden',
19
+ nextTurn: 'Nächster Zug',
20
+ prevTurn: 'Vorheriger Zug',
21
+ roundLabel: 'Runde',
22
+ turnLabel: 'Zug',
23
+ noCombatants: 'Noch keine Kämpfer. Füge Charaktere hinzu, um zu beginnen.',
24
+ statusInput: 'Status',
25
+ combatStarted: 'Kampf begonnen!',
26
+ combatEnded: 'Kampf beendet',
27
+ yourTurn: 'Zug',
28
+ };
29
+
30
+ export const content: ToolLocaleContent<InitiativeTrackerUI> = {
31
+ slug: 'initiativen-verfolger',
32
+ title: 'RPG Initiativen Verfolger: Kampfreihenfolge & InitiativenManager',
33
+ description: 'Verfolge die Initiative-Reihenfolge für jedes Tabletop-RPG. Füge Kämpfer hinzu, würfle Initiative, verwalte Statuseffekte und navigiere durch die Kampfzüge.',
34
+ ui,
35
+ seo: [
36
+ { type: 'title', text: 'Wie Initiative in D&D und anderen Tabletop-RPGs funktioniert', level: 2 },
37
+ { type: 'paragraph', html: 'Die Initiative bestimmt die Reihenfolge der Aktionen im Kampf. Jeder Teilnehmer würfelt zu Beginn eines Kampfes einen Geschicklichkeitswurf, und die Zugreihenfolge verläuft vom höchsten zum niedrigsten Wert. Ein digitaler Initiativen Verfolger automatisiert die Sortierung, hebt den aktiven Kämpfer hervor, verfolgt laufende Statuseffekte und informiert den gesamten Tisch darüber, wer als Nächstes an der Reihe ist. Dies ist besonders nützlich für Spielleiter, die komplexe Begegnungen mit mehreren Gegnertypen und Spielercharakteren leiten.' },
38
+ {
39
+ type: 'stats',
40
+ items: [
41
+ { value: 'Unbegrenzt', label: 'Kämpfer unterstützt' },
42
+ { value: 'Auto-Sortierung', label: 'Nach Initiative-Wurf' },
43
+ { value: 'Echtzeit', label: 'Zugverfolgung' },
44
+ ],
45
+ columns: 3,
46
+ },
47
+ { type: 'title', text: 'Standard Initiative-Regeln für D&D 5e', level: 2 },
48
+ { type: 'paragraph', html: 'In D&D 5. Edition würfelt jeder Teilnehmer zu Beginn des Kampfes einen Geschicklichkeitswurf. Spieler würfeln einzeln. Der Spielleiter würfelt einmal für jede Gruppe identischer Monster. Die Zugreihenfolge verläuft vom höchsten zum niedrigsten Wert und springt dann für die nächste Runde zurück zum Anfang. Gleichstände werden durch den Geschicklichkeitswert entschieden oder der SL entscheidet. Ein digitaler Verfolger erledigt dies alles automatisch und zeigt die vollständige Zugreihenfolge auf einen Blick.' },
49
+ {
50
+ type: 'table',
51
+ headers: ['Funktion', 'Manuelle Verfolgung', 'Digitaler Verfolger'],
52
+ rows: [
53
+ ['Sortierung', 'Liste neu schreiben, wenn sich die Initiative ändert', 'Sofortige automatische Neusortierung'],
54
+ ['Aktueller Zug', 'Muss mündlich oder mit einem Marker verfolgt werden', 'Mit grünem Leuchten hervorgehoben'],
55
+ ['Statuseffekte', 'Separater Notizblock oder Würfel neben der Miniatur', 'Klicken zum Umschalten bei jedem Kämpfer'],
56
+ ['Rundenzählung', 'Im Kopf oder auf Schmierpapier zählen', 'Wird automatisch angezeigt'],
57
+ ['Hinzufügen während des Kampfes', 'Reihenfolge ausradieren und neu schreiben', 'An der richtigen Position eingefügt'],
58
+ ],
59
+ },
60
+ {
61
+ type: 'tip',
62
+ title: 'Tipps zur Initiative am Spieltisch',
63
+ html: 'Würfle die Initiative für alle Monster vor der Sitzung aus, um während des Kampfes Zeit zu sparen. Füge einem Kämpfer häufige Statuseffekte wie Betäubt oder Vergiftet hinzu, sobald sie angewendet werden. So verhinderst du, dass eine Bedingung vergessen wird, wenn der betroffene Charakter an der Reihe ist. Verwende den Button „Vorheriger Zug", um nachzusehen, was passiert ist, falls du eine Regelentscheidung aus einer früheren Runde überprüfen musst.',
64
+ },
65
+ {
66
+ type: 'title',
67
+ text: 'Häufig verwendete Statuseffekte im Kampf',
68
+ level: 3,
69
+ },
70
+ { type: 'paragraph', html: 'Das Verfolgen von Zuständen während des Kampfes ist eine der am häufigsten übersehenen Aufgaben. Ein Statuseffekt kann die Effektivität eines Charakters völlig verändern, und das Vergessen, ihn zu entfernen, kann das Gleichgewicht einer Begegnung kippen. Der Initiativen Verfolger ermöglicht es dir, Zustände mit einem einzigen Klick anzuwenden und zu entfernen, sodass sie neben dem Namen des Kämpfers sichtbar bleiben.' },
71
+ {
72
+ type: 'proscons',
73
+ title: 'Statuseffekte digital vs. manuell verwalten',
74
+ items: [
75
+ {
76
+ pro: 'Zustände sind direkt neben dem Charakternamen sichtbar, sodass niemand einen aktiven Effekt vergisst.',
77
+ con: 'Erfordert, dass man daran denkt, Status ein- und auszuschalten, wenn sie angewendet und entfernt werden.',
78
+ },
79
+ {
80
+ pro: 'Mehrere Status können gleichzeitig auf demselben Kämpfer aktiv sein und werden gemeinsam angezeigt.',
81
+ con: 'Der Verfolger setzt die Regeln nicht automatisch durch; der SL muss sie weiterhin anwenden.',
82
+ },
83
+ {
84
+ pro: 'Status können während des Kampfes hinzugefügt werden, ohne anzuhalten, um auf einem Charakterblatt zu schreiben.',
85
+ con: 'Standardmäßig sind nur vordefinierte Status-Tags verfügbar.',
86
+ },
87
+ ],
88
+ },
89
+ {
90
+ type: 'title',
91
+ text: 'Alternative Initiative-Systeme',
92
+ level: 3,
93
+ },
94
+ {
95
+ type: 'comparative',
96
+ items: [
97
+ {
98
+ title: 'Standard Initiative',
99
+ description: 'Jeder Kämpfer würfelt einmal zu Beginn. Dieselbe Reihenfolge wiederholt sich jede Runde. Einfach und vorhersehbar, aber die Reihenfolge ist nach dem ersten Wurf festgelegt. Funktioniert gut für die meisten Gruppen und Begegnungsgrößen.',
100
+ icon: 'mdi:swap-vertical',
101
+ highlight: true,
102
+ points: ['Einfach zu lernen und zu lehren', 'Feste Zugreihenfolge jede Runde', 'Funktioniert für jede Gruppengröße'],
103
+ },
104
+ {
105
+ title: 'Seiten Initiative',
106
+ description: 'Alle Spielercharaktere handeln gemeinsam, dann alle Gegner gemeinsam. Üblich in älteren Editionen und OSR-Spielen. Beschleunigt den Kampf durch weniger individuelle Verfolgung, reduziert aber die taktische Vielfalt.',
107
+ icon: 'mdi:account-group',
108
+ highlight: false,
109
+ points: ['Schnellste Kampfauflösung', 'Fördert Gruppenkoordination', 'Weniger individuelle Zugvielfalt'],
110
+ },
111
+ {
112
+ title: 'Geschwindigkeitsfaktor Variante',
113
+ description: 'Jede Runde erklären die Teilnehmer zuerst ihre Aktionen, dann würfeln sie Initiative, modifiziert durch die gewählte Aktion. Wird in optionalen Regelsätzen verwendet. Erzeugt eine unvorhersehbare Zugreihenfolge, verleiht der Entscheidungsfindung aber mehr Tiefe.',
114
+ icon: 'mdi:run-fast',
115
+ highlight: false,
116
+ points: ['Andere Reihenfolge jede Runde', 'Aktionswahl beeinflusst Timing', 'Mehr taktische Entscheidungen pro Zug'],
117
+ },
118
+ ],
119
+ columns: 3,
120
+ },
121
+ {
122
+ type: 'title',
123
+ text: 'Kampfverfolger-Glossar',
124
+ level: 3,
125
+ },
126
+ {
127
+ type: 'glossary',
128
+ items: [
129
+ {
130
+ term: 'Initiative',
131
+ definition: 'Ein Geschicklichkeitswurf zu Beginn des Kampfes, der die Reihenfolge der Züge bestimmt. Jeder Teilnehmer würfelt und handelt in absteigender Reihenfolge.',
132
+ },
133
+ {
134
+ term: 'Runde',
135
+ definition: 'Ein vollständiger Zyklus, in dem jeder Kämpfer einen Zug macht. Nachdem der letzte Kämpfer gehandelt hat, endet die Runde und eine neue Runde beginnt von oben in der Reihenfolge.',
136
+ },
137
+ {
138
+ term: 'Statuseffekt',
139
+ definition: 'Ein vorübergehender Zustand, der die Fähigkeiten eines Charakters verändert. Beispiele sind Betäubt (kann nicht handeln), Vergiftet (Nachteil auf Würfe) und Geblendet (Angriffe haben Nachteil).',
140
+ },
141
+ {
142
+ term: 'NSC',
143
+ definition: 'Nicht-Spieler-Charakter, der vom Spielleiter gesteuert wird. Bei der Initiative-Verfolgung sind NSC typischerweise Gegner oder neutrale Kreaturen in der Begegnung.',
144
+ },
145
+ {
146
+ term: 'SC',
147
+ definition: 'Spieler-Charakter, der von einem der Spieler am Tisch gesteuert wird. SC werden im Verfolger von NSC unterschieden, damit der SL freundliche Ziele erkennen kann.',
148
+ },
149
+ ],
150
+ },
151
+ {
152
+ type: 'diagnostic',
153
+ variant: 'info',
154
+ title: 'Wann die Initiative jede Runde neu würfeln?',
155
+ icon: 'mdi:information-outline',
156
+ badge: 'SL-TIPP',
157
+ html: 'Manche SL würfeln die Initiative lieber jede Runde neu für Unvorhersehbarkeit, während die meisten aus Einfachheit dieselbe Reihenfolge beibehalten. Wenn ein neuer Kämpfer mitten im Kampf dazustößt, würfle seine Initiative und füge ihn an der aktuellen Position ein. Der Verfolger erledigt dies automatisch. Für Boss-Begegnungen solltest du dem Schurken eine feste Initiative zu einem bestimmten Wert geben, damit der Kampf strukturierter wirkt.',
158
+ },
159
+ ],
160
+ faq: [
161
+ {
162
+ question: 'Wie füge ich Kämpfer zur Initiative-Reihenfolge hinzu?',
163
+ answer: 'Gib den Charakternamen, den Initiative-Wurf und den Initiative-Modifikator ein und klicke dann auf Hinzufügen. Wechsle zwischen Spieler- und NSC-Typen, um Gruppenmitglieder optisch von Gegnern in der Liste zu unterscheiden.',
164
+ },
165
+ {
166
+ question: 'Was passiert, wenn ich auf Kampf beginnen klicke?',
167
+ answer: 'Alle Kämpfer werden nach Initiative vom höchsten zum niedrigsten Wert sortiert. Die Zugreihenfolge beginnt von oben, der Runden-Zähler startet bei 1 und der aktive Kämpfer wird mit einem grünen Leuchten hervorgehoben.',
168
+ },
169
+ {
170
+ question: 'Wie verfolge ich Statuseffekte während des Kampfes?',
171
+ answer: 'Klicke auf ein Status-Tag (Betäubt, Vergiftet, Geblendet usw.), um es für den ausgewählten Kämpfer ein- oder auszuschalten. Aktive Status erscheinen neben dem Charakternamen. So werden alle an laufende Effekte erinnert, wenn dieser Kämpfer an der Reihe ist.',
172
+ },
173
+ {
174
+ question: 'Bleiben meine Kampfdaten erhalten, wenn ich die Seite aktualisiere?',
175
+ answer: 'Ja. Der Initiativen Verfolger speichert den Kampfzustand im lokalen Speicher deines Browsers. Deine Kämpfer, Zugreihenfolge und Statuseffekte bleiben nach einem Seitenneuladen erhalten. Die Daten werden gelöscht, wenn du auf Kampf beenden klickst.',
176
+ },
177
+ {
178
+ question: 'Kann ich zum vorherigen Zug zurückgehen?',
179
+ answer: 'Ja. Der Button Vorheriger Zug ermöglicht es dir, rückwärts durch die Zugreihenfolge zu navigieren. Dies ist nützlich, um nachzusehen, was früher in der Runde passiert ist, oder um einen Fehler zu korrigieren.',
180
+ },
181
+ {
182
+ question: 'Kann ich während des Kampfes Kämpfer hinzufügen oder entfernen?',
183
+ answer: 'Ja. Neue Kämpfer können jederzeit hinzugefügt werden und werden automatisch an der richtigen Position basierend auf ihrem Initiative-Wurf eingefügt. Vorhandene Kämpfer können entfernt werden, wenn sie die Begegnung verlassen.',
184
+ },
185
+ {
186
+ question: 'Funktioniert dies mit jedem RPG-System?',
187
+ answer: 'Ja. Der Verfolger funktioniert mit jedem Tabletop-RPG, das eine initiativebasierte Zugreihenfolge verwendet. Gib einfach die Initiative-Werte ein, die dein System verwendet, sei es ein d20-Wurf plus Dex für D&D oder eine andere Mechanik für andere Spiele.',
188
+ },
189
+ ],
190
+ bibliography,
191
+ howTo: [
192
+ {
193
+ name: 'Kämpfer vor dem Kampf hinzufügen',
194
+ text: 'Gib für jeden Charakter den Namen, den Initiative-Wurf und den Modifikator ein. Markiere sie als Spieler oder NSC. Füge so viele Kämpfer hinzu, wie deine Begegnung benötigt.',
195
+ },
196
+ {
197
+ name: 'Kampf beginnen, um die Reihenfolge festzulegen',
198
+ text: 'Klicke auf Kampf beginnen, um alle nach Initiative zu sortieren und die Zugverfolgung zu starten. Der erste Kämpfer in der Reihenfolge wird hervorgehoben.',
199
+ },
200
+ {
201
+ name: 'Züge navigieren und Zustände verfolgen',
202
+ text: 'Verwende Nächster Zug und Vorheriger Zug, um durch die Reihenfolge zu gehen. Wende Statuseffekte durch Klicken auf die Status-Tags an, um Zustände während des Kampfes zu verfolgen.',
203
+ },
204
+ {
205
+ name: 'Kampf beenden, wenn fertig',
206
+ text: 'Klicke auf Kampf beenden, um die Zugverfolgung zu stoppen. Dies löscht den aktiven Status und setzt den Runden-Zähler zurück, während die Kämpferliste für die nächste Begegnung erhalten bleibt.',
207
+ },
208
+ ],
209
+ schemas: [
210
+ {
211
+ '@context': 'https://schema.org',
212
+ '@type': 'SoftwareApplication',
213
+ 'name': 'RPG Initiativen Verfolger',
214
+ 'operatingSystem': 'Alle',
215
+ 'applicationCategory': 'UtilitiesApplication',
216
+ 'browserRequirements': 'Erfordert HTML5. Erfordert JavaScript.',
217
+ },
218
+ {
219
+ '@context': 'https://schema.org',
220
+ '@type': 'FAQPage',
221
+ 'mainEntity': [
222
+ {
223
+ '@type': 'Question',
224
+ 'name': 'Wie füge ich Kämpfer hinzu?',
225
+ 'acceptedAnswer': {
226
+ '@type': 'Answer',
227
+ 'text': 'Gib den Charakternamen, den Initiative-Wurf und den Initiative-Modifikator ein und klicke dann auf Hinzufügen. Spieler und NSC können umgeschaltet werden, um sie in der Liste zu unterscheiden.',
228
+ },
229
+ },
230
+ {
231
+ '@type': 'Question',
232
+ 'name': 'Was passiert, wenn ich auf Kampf beginnen klicke?',
233
+ 'acceptedAnswer': {
234
+ '@type': 'Answer',
235
+ 'text': 'Alle Kämpfer werden nach Initiative sortiert (höchster zuerst). Die Zugreihenfolge beginnt mit dem ersten Kämpfer und ein Runden-Zähler startet bei 1.',
236
+ },
237
+ },
238
+ ],
239
+ },
240
+ {
241
+ '@context': 'https://schema.org',
242
+ '@type': 'HowTo',
243
+ 'name': 'So verwendest du den Initiativen Verfolger',
244
+ 'step': [
245
+ {
246
+ '@type': 'HowToStep',
247
+ 'name': 'Kämpfer hinzufügen',
248
+ 'text': 'Gib die Charakterdetails einschließlich Name, Initiative-Wurf und Modifikator ein. Wechsle zwischen Spieler- und NSC-Typen vor dem Hinzufügen.',
249
+ },
250
+ {
251
+ '@type': 'HowToStep',
252
+ 'name': 'Kampf beginnen',
253
+ 'text': 'Klicke auf Kampf beginnen, um alle Kämpfer nach Initiative zu sortieren und die Zugreihenfolge-Verfolgung zu starten.',
254
+ },
255
+ {
256
+ '@type': 'HowToStep',
257
+ 'name': 'Züge verwalten',
258
+ 'text': 'Verwende Nächster Zug und Vorheriger Zug, um durch die Zugreihenfolge zu navigieren. Der aktive Kämpfer wird mit einem grünen Leuchten hervorgehoben.',
259
+ },
260
+ ],
261
+ },
262
+ ],
263
+ };
264
+
265
+
266
+