@jjlmoya/utils-tabletop 1.24.0 → 1.26.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/category/index.ts +2 -0
- package/src/entries.ts +2 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/dungeon-map-generator/component.astro +1 -0
- package/src/tool/dungeon-map-generator/controller.ts +1 -0
- package/src/tool/dungeon-map-generator/dom-views.ts +19 -14
- package/src/tool/dungeon-map-generator/i18n/en.ts +3 -2
- package/src/tool/dungeon-map-generator/i18n/es.ts +1 -0
- package/src/tool/dungeon-map-generator/logic.ts +42 -16
- package/src/tool/dungeon-map-generator/ui.ts +1 -0
- package/src/tool/encounter-difficulty-calculator/bibliography.astro +6 -0
- package/src/tool/encounter-difficulty-calculator/bibliography.ts +12 -0
- package/src/tool/encounter-difficulty-calculator/component.astro +53 -0
- package/src/tool/encounter-difficulty-calculator/controller.ts +133 -0
- package/src/tool/encounter-difficulty-calculator/dnd-5e-encounter-difficulty-calculator.css +389 -0
- package/src/tool/encounter-difficulty-calculator/dom-views.ts +54 -0
- package/src/tool/encounter-difficulty-calculator/entry.ts +27 -0
- package/src/tool/encounter-difficulty-calculator/evaluator.ts +12 -0
- package/src/tool/encounter-difficulty-calculator/i18n/de.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/en.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/es.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/fr.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/id.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/it.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/ja.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/ko.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/nl.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/pl.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/pt.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/ru.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/sv.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/tr.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/i18n/zh.ts +155 -0
- package/src/tool/encounter-difficulty-calculator/index.ts +11 -0
- package/src/tool/encounter-difficulty-calculator/logic.test.ts +37 -0
- package/src/tool/encounter-difficulty-calculator/logic.ts +174 -0
- package/src/tool/encounter-difficulty-calculator/seo.astro +16 -0
- package/src/tool/encounter-difficulty-calculator/storage.ts +26 -0
- package/src/tool/encounter-difficulty-calculator/ui.ts +45 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
.encounter-calculator {
|
|
2
|
+
--n-ink: #18212b;
|
|
3
|
+
--n-muted: #5b6875;
|
|
4
|
+
--n-paper: #f7f4ed;
|
|
5
|
+
--n-card: #fff;
|
|
6
|
+
--n-line: #d9d5cb;
|
|
7
|
+
--n-accent: #bf542f;
|
|
8
|
+
--n-accent-soft: #f2d1c5;
|
|
9
|
+
--n-safe: #5e8b68;
|
|
10
|
+
--n-medium: #c18b3c;
|
|
11
|
+
--n-hard: #b8643d;
|
|
12
|
+
--n-deadly: #853e42;
|
|
13
|
+
--n-shadow: #d5cec2;
|
|
14
|
+
|
|
15
|
+
color: var(--n-ink);
|
|
16
|
+
background: var(--n-paper);
|
|
17
|
+
border: 1px solid var(--n-line);
|
|
18
|
+
border-radius: 1.75rem;
|
|
19
|
+
padding: clamp(1rem, 3vw, 2rem);
|
|
20
|
+
box-shadow: 0 1rem 0 var(--n-shadow);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.theme-dark .encounter-calculator {
|
|
24
|
+
--n-ink: #f7f4ed;
|
|
25
|
+
--n-muted: #b5bdc4;
|
|
26
|
+
--n-paper: #1d252d;
|
|
27
|
+
--n-card: #26313b;
|
|
28
|
+
--n-line: #44515b;
|
|
29
|
+
--n-accent: #ee8661;
|
|
30
|
+
--n-accent-soft: #633d36;
|
|
31
|
+
--n-safe: #8cb596;
|
|
32
|
+
--n-medium: #dfb265;
|
|
33
|
+
--n-hard: #e18b63;
|
|
34
|
+
--n-deadly: #dc7a7d;
|
|
35
|
+
--n-shadow: #11171c;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.encounter-intro,
|
|
39
|
+
.rules-note {
|
|
40
|
+
color: var(--n-muted);
|
|
41
|
+
line-height: 1.6;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.encounter-presets {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: .5rem;
|
|
49
|
+
margin: 1.25rem 0;
|
|
50
|
+
font-size: .8rem;
|
|
51
|
+
font-weight: 700;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.encounter-presets span {
|
|
55
|
+
color: var(--n-muted);
|
|
56
|
+
margin-right: .25rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.encounter-presets button,
|
|
60
|
+
.cr-chip,
|
|
61
|
+
.reset-button {
|
|
62
|
+
color: var(--n-ink);
|
|
63
|
+
background: var(--n-card);
|
|
64
|
+
border: 1px solid var(--n-line);
|
|
65
|
+
border-radius: 999px;
|
|
66
|
+
padding: .55rem .75rem;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
font: inherit;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.encounter-presets button:hover,
|
|
72
|
+
.cr-chip:hover,
|
|
73
|
+
.reset-button:hover,
|
|
74
|
+
.cr-chip.is-active {
|
|
75
|
+
color: var(--n-card);
|
|
76
|
+
background: var(--n-accent);
|
|
77
|
+
border-color: var(--n-accent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.encounter-layout {
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: minmax(16rem, .75fr) minmax(20rem, 1.25fr);
|
|
83
|
+
gap: 1rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.encounter-controls,
|
|
87
|
+
.encounter-result {
|
|
88
|
+
background: var(--n-card);
|
|
89
|
+
border: 1px solid var(--n-line);
|
|
90
|
+
border-radius: 1.25rem;
|
|
91
|
+
padding: 1.25rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.encounter-result {
|
|
95
|
+
align-self: start;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.control-group + .control-group {
|
|
99
|
+
border-top: 1px solid var(--n-line);
|
|
100
|
+
margin-top: 1.25rem;
|
|
101
|
+
padding-top: 1.25rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.group-heading,
|
|
105
|
+
.orbit-labels,
|
|
106
|
+
.result-head,
|
|
107
|
+
.result-stats,
|
|
108
|
+
.encounter-controls label {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
gap: .75rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.group-heading {
|
|
116
|
+
font-size: .75rem;
|
|
117
|
+
letter-spacing: .12em;
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
font-weight: 800;
|
|
120
|
+
margin-bottom: 1rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.eyebrow,
|
|
124
|
+
.result-kicker {
|
|
125
|
+
color: var(--n-accent);
|
|
126
|
+
font-size: .65rem;
|
|
127
|
+
letter-spacing: .1em;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.encounter-controls label {
|
|
132
|
+
margin-top: .95rem;
|
|
133
|
+
font-size: .9rem;
|
|
134
|
+
font-weight: 750;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.encounter-controls output {
|
|
138
|
+
min-width: 2.5rem;
|
|
139
|
+
color: var(--n-accent);
|
|
140
|
+
font-size: 1.15rem;
|
|
141
|
+
text-align: right;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.encounter-controls input[type='range'] {
|
|
145
|
+
accent-color: var(--n-accent);
|
|
146
|
+
width: 100%;
|
|
147
|
+
margin: .7rem 0 .25rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.encounter-controls small {
|
|
151
|
+
display: block;
|
|
152
|
+
color: var(--n-muted);
|
|
153
|
+
font-size: .75rem;
|
|
154
|
+
line-height: 1.45;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.cr-list {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
gap: .4rem;
|
|
161
|
+
margin: .75rem 0 .35rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.cr-chip {
|
|
165
|
+
padding: .4rem .55rem;
|
|
166
|
+
font-size: .78rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.cr-chip[data-advanced='true'] {
|
|
170
|
+
display: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cr-list.is-expanded .cr-chip[data-advanced='true'] {
|
|
174
|
+
display: inline-block;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.cr-more-button {
|
|
178
|
+
border: 0;
|
|
179
|
+
padding: .25rem 0;
|
|
180
|
+
color: var(--n-accent);
|
|
181
|
+
background: transparent;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
font: inherit;
|
|
184
|
+
font-size: .72rem;
|
|
185
|
+
font-weight: 800;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.reset-button {
|
|
189
|
+
margin-top: 1.25rem;
|
|
190
|
+
color: var(--n-muted);
|
|
191
|
+
font-size: .8rem;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.result-orbit {
|
|
195
|
+
display: flex;
|
|
196
|
+
flex-direction: column;
|
|
197
|
+
gap: 1rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.orbit-labels {
|
|
201
|
+
color: var(--n-muted);
|
|
202
|
+
font-size: .72rem;
|
|
203
|
+
font-weight: 800;
|
|
204
|
+
letter-spacing: .1em;
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.orbit-scene {
|
|
209
|
+
position: relative;
|
|
210
|
+
height: 11rem;
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
border: 1px solid var(--n-line);
|
|
213
|
+
border-radius: 1rem;
|
|
214
|
+
background: var(--n-paper);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.orbit-line {
|
|
218
|
+
position: absolute;
|
|
219
|
+
top: 50%;
|
|
220
|
+
left: 12%;
|
|
221
|
+
right: 12%;
|
|
222
|
+
border-top: 2px solid var(--n-line);
|
|
223
|
+
transform: scaleX(var(--threat-scale));
|
|
224
|
+
transform-origin: center;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.party-orbit,
|
|
228
|
+
.threat-orbit {
|
|
229
|
+
position: absolute;
|
|
230
|
+
top: 50%;
|
|
231
|
+
display: flex;
|
|
232
|
+
gap: .4rem;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.party-orbit {
|
|
236
|
+
left: 10%;
|
|
237
|
+
transform: translateY(-50%);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.threat-orbit {
|
|
241
|
+
right: 10%;
|
|
242
|
+
opacity: var(--threat-opacity);
|
|
243
|
+
transform: translateY(-50%) scale(var(--threat-scale));
|
|
244
|
+
transform-origin: right center;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.party-orbit i,
|
|
248
|
+
.threat-orbit b {
|
|
249
|
+
display: block;
|
|
250
|
+
width: 1rem;
|
|
251
|
+
height: 1rem;
|
|
252
|
+
border-radius: 50%;
|
|
253
|
+
background: var(--n-safe);
|
|
254
|
+
box-shadow: 0 0 0 .35rem var(--n-card), 0 0 0 .45rem var(--n-safe);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.party-marker,
|
|
258
|
+
.threat-marker {
|
|
259
|
+
position: relative;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.threat-orbit b {
|
|
263
|
+
background: var(--n-deadly);
|
|
264
|
+
box-shadow: 0 0 0 .35rem var(--n-card), 0 0 0 .45rem var(--n-deadly);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.result-head {
|
|
268
|
+
align-items: flex-end;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.result-head strong {
|
|
272
|
+
color: var(--n-accent);
|
|
273
|
+
font-size: clamp(1.5rem, 4vw, 2.4rem);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.result-head small {
|
|
277
|
+
color: var(--n-muted);
|
|
278
|
+
font-size: .8rem;
|
|
279
|
+
font-weight: 700;
|
|
280
|
+
white-space: nowrap;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.result-hint {
|
|
284
|
+
margin: -.5rem 0 0;
|
|
285
|
+
color: var(--n-muted);
|
|
286
|
+
font-size: .78rem;
|
|
287
|
+
line-height: 1.45;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.threshold-track {
|
|
291
|
+
display: grid;
|
|
292
|
+
grid-template-columns: repeat(5, 1fr);
|
|
293
|
+
gap: .35rem;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.threshold-band {
|
|
297
|
+
min-height: 4.1rem;
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-direction: column;
|
|
300
|
+
justify-content: space-between;
|
|
301
|
+
padding: .55rem;
|
|
302
|
+
border-top: .25rem solid var(--n-line);
|
|
303
|
+
color: var(--n-muted);
|
|
304
|
+
font-size: .68rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.threshold-band strong {
|
|
308
|
+
color: var(--n-ink);
|
|
309
|
+
font-size: .72rem;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.threshold-band.easy {
|
|
313
|
+
border-color: var(--n-safe);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.threshold-band.medium {
|
|
317
|
+
border-color: var(--n-medium);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.threshold-band.hard {
|
|
321
|
+
border-color: var(--n-hard);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.threshold-band.deadly {
|
|
325
|
+
border-color: var(--n-deadly);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.threshold-band.is-active {
|
|
329
|
+
background: var(--n-accent-soft);
|
|
330
|
+
color: var(--n-ink);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.result-stats {
|
|
334
|
+
border-top: 1px solid var(--n-line);
|
|
335
|
+
padding-top: 1rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.result-stats span {
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-direction: column;
|
|
341
|
+
gap: .25rem;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.result-stats small {
|
|
345
|
+
color: var(--n-muted);
|
|
346
|
+
font-size: .7rem;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.result-stats strong {
|
|
350
|
+
font-size: .9rem;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.encounter-warning {
|
|
354
|
+
display: grid;
|
|
355
|
+
grid-template-columns: auto 1fr;
|
|
356
|
+
gap: .7rem;
|
|
357
|
+
border-left: .25rem solid var(--n-medium);
|
|
358
|
+
padding: .7rem .8rem;
|
|
359
|
+
color: var(--n-ink);
|
|
360
|
+
background: var(--n-paper);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.encounter-warning span {
|
|
364
|
+
color: var(--n-medium);
|
|
365
|
+
font-weight: 800;
|
|
366
|
+
font-size: .75rem;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.encounter-warning p {
|
|
370
|
+
margin: 0;
|
|
371
|
+
color: var(--n-muted);
|
|
372
|
+
font-size: .78rem;
|
|
373
|
+
line-height: 1.45;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.rules-note {
|
|
377
|
+
margin: 1.25rem 0 0;
|
|
378
|
+
font-size: .75rem;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.rules-note a {
|
|
382
|
+
color: var(--n-accent);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@media (max-width: 760px) {
|
|
386
|
+
.encounter-layout { grid-template-columns: 1fr; }
|
|
387
|
+
.threshold-track { grid-template-columns: repeat(2, 1fr); }
|
|
388
|
+
.threshold-band:last-child { grid-column: span 2; }
|
|
389
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EncounterDifficulty, EncounterResult } from './logic';
|
|
2
|
+
import { evaluateEncounter } from './evaluator';
|
|
3
|
+
|
|
4
|
+
type Labels = Record<string, string>;
|
|
5
|
+
|
|
6
|
+
function formatNumber(value: number): string {
|
|
7
|
+
return new Intl.NumberFormat('en-US').format(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function labelForDifficulty(key: EncounterDifficulty, labels: Labels): string {
|
|
11
|
+
return labels[key] ?? key;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderMarkers(count: number, className: string, element: string): string {
|
|
15
|
+
return Array.from({ length: count }, () => '<' + element + ' class="' + className + '"></' + element + '>').join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderHint(result: EncounterResult, labels: Labels): string {
|
|
19
|
+
return labels[result.difficulty + 'Hint'] ?? '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderWarnings(result: EncounterResult, labels: Labels): string {
|
|
23
|
+
const warningText = result.warnings.map((key) => labels[key] ?? '').filter(Boolean);
|
|
24
|
+
if (warningText.length === 0) return '';
|
|
25
|
+
return '<div class="encounter-warning"><span>' + labels.warning + '</span><p>' + warningText.join(' ') + '</p></div>';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderThresholds(result: EncounterResult, labels: Labels): string {
|
|
29
|
+
const bands: EncounterDifficulty[] = ['belowEasy', 'easy', 'medium', 'hard', 'deadly'];
|
|
30
|
+
return bands.map((key) => {
|
|
31
|
+
const value = key === 'belowEasy' ? 0 : result.thresholds[key];
|
|
32
|
+
const active = key === result.difficulty ? ' is-active' : '';
|
|
33
|
+
const amount = value ? formatNumber(value) + ' XP' : '<' + formatNumber(result.thresholds.easy) + ' XP';
|
|
34
|
+
return '<div class="threshold-band ' + key + active + '"><span>' + labelForDifficulty(key, labels) + '</span><strong>' + amount + '</strong></div>';
|
|
35
|
+
}).join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderResult(result: EncounterResult, labels: Labels): string {
|
|
39
|
+
const evaluation = evaluateEncounter(result);
|
|
40
|
+
const title = labelForDifficulty(evaluation.titleKey, labels);
|
|
41
|
+
const scale = Math.min(1.2, Math.max(0.7, 0.7 + evaluation.ratio * 0.5));
|
|
42
|
+
const opacity = Math.min(1, 0.8 + evaluation.ratio * 0.2);
|
|
43
|
+
const style = '--threat-scale:' + scale.toFixed(2) + ';--threat-opacity:' + opacity.toFixed(2);
|
|
44
|
+
return '<div class="result-orbit" data-difficulty="' + result.difficulty + '">'
|
|
45
|
+
+ '<div class="orbit-labels"><span>' + labels.partyMarker + '</span><span>' + labels.threatMarker + '</span></div>'
|
|
46
|
+
+ '<div class="orbit-scene" style="' + style + '">'
|
|
47
|
+
+ '<div class="party-orbit">' + renderMarkers(result.settings.partySize, 'party-marker', 'i') + '</div>'
|
|
48
|
+
+ '<div class="threat-orbit">' + renderMarkers(result.settings.monsterCount, 'threat-marker', 'b') + '</div><div class="orbit-line"></div></div>'
|
|
49
|
+
+ '<div class="result-head"><span class="result-kicker">' + labels.resultSection + '</span><strong>' + title + '</strong><small>' + formatNumber(result.adjustedXp) + ' ' + labels.xpUnit + '</small></div>'
|
|
50
|
+
+ '<p class="result-hint">' + renderHint(result, labels) + '</p>'
|
|
51
|
+
+ '<div class="threshold-track">' + renderThresholds(result, labels) + '</div>'
|
|
52
|
+
+ '<div class="result-stats"><span><small>' + labels.baseXp + '</small><strong>' + formatNumber(result.baseXp) + ' ' + labels.xpUnit + '</strong></span><span><small>' + labels.multiplier + '</small><strong>×' + result.multiplier + '</strong></span><span><small>' + labels.partyThreshold + '</small><strong>' + formatNumber(result.thresholds.medium) + ' ' + labels.xpUnit + '</strong></span></div>'
|
|
53
|
+
+ renderWarnings(result, labels) + '</div>';
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TabletopToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { EncounterDifficultyUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { EncounterDifficultyUI } from './ui';
|
|
5
|
+
export type EncounterDifficultyLocaleContent = ToolLocaleContent<EncounterDifficultyUI>;
|
|
6
|
+
|
|
7
|
+
export const encounterDifficultyCalculator: TabletopToolEntry<EncounterDifficultyUI> = {
|
|
8
|
+
id: 'encounter-difficulty-calculator',
|
|
9
|
+
icons: { bg: 'mdi:sword-cross', fg: 'mdi:scale-balance' },
|
|
10
|
+
i18n: {
|
|
11
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
12
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
13
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
14
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
15
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
16
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
17
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
18
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
19
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
21
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
22
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
23
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
24
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
25
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EncounterDifficulty, EncounterResult } from './logic';
|
|
2
|
+
|
|
3
|
+
export interface EncounterEvaluation {
|
|
4
|
+
titleKey: EncounterDifficulty;
|
|
5
|
+
ratio: number;
|
|
6
|
+
warningKeys: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function evaluateEncounter(result: EncounterResult): EncounterEvaluation {
|
|
10
|
+
const ratio = result.thresholds.deadly === 0 ? 1 : result.adjustedXp / result.thresholds.deadly;
|
|
11
|
+
return { titleKey: result.difficulty, ratio: Math.min(ratio, 1.35), warningKeys: result.warnings };
|
|
12
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
|
|
2
|
+
import { bibliography } from '../bibliography';
|
|
3
|
+
import type { EncounterDifficultyLocaleContent, EncounterDifficultyUI } from '../entry';
|
|
4
|
+
|
|
5
|
+
const ui: EncounterDifficultyUI = {
|
|
6
|
+
intro: 'Stellen Sie Gruppe und Bedrohung ein. Der Rechner gleicht die Begegnung mit den D&D 5e 2014 Schwellenwerten ab.',
|
|
7
|
+
partySection: 'Die Gruppe',
|
|
8
|
+
partyLevel: 'Charakterstufe',
|
|
9
|
+
partyLevelHint: 'Verwenden Sie eine Stufe für Gruppen mit ähnlicher Charakterstufe.',
|
|
10
|
+
partySize: 'Charaktere',
|
|
11
|
+
partySizeHint: 'Die Regeln passen den Monster-Multiplikator für sehr kleine oder große Gruppen an.',
|
|
12
|
+
threatSection: 'Die Bedrohung',
|
|
13
|
+
monsterCr: 'Herausforderungsgrad (CR)',
|
|
14
|
+
monsterCrHint: 'Wählen Sie den CR eines Monsters. Gemischte Gruppen erfordern eine eigene Berechnung.',
|
|
15
|
+
moreCr: 'Höhere CR anzeigen',
|
|
16
|
+
lessCr: 'Weniger CR anzeigen',
|
|
17
|
+
monsterCount: 'Anzahl der Monster',
|
|
18
|
+
monsterCountHint: 'Mehr Kreaturen erhöhen den Aktionsdruck selbst bei moderater Basis-XP.',
|
|
19
|
+
presets: 'Mit einer Szene starten',
|
|
20
|
+
presetClassic: 'Klassische Patrouille',
|
|
21
|
+
presetBoss: 'Boss alleine',
|
|
22
|
+
presetSwarm: 'Horde von Dienern',
|
|
23
|
+
resultSection: 'Begegnungsdruck',
|
|
24
|
+
belowEasyHint: 'Eine leichte Szene zur Ressourcenschonung für spätere Herausforderungen.',
|
|
25
|
+
easyHint: 'Eine überschaubare Szene mit geringem Druck auf die Gruppe.',
|
|
26
|
+
mediumHint: 'Eine spürbare Prüfung, die Trefferpunkte oder Ressourcen kosten kann.',
|
|
27
|
+
hardHint: 'Eine gefährliche Szene, in der Taktik und Ressourcenwahl entscheiden.',
|
|
28
|
+
deadlyHint: 'Signal für tödlichen Druck. Prüfen Sie Fluchtwege, Gelände und Fehlerkosten.',
|
|
29
|
+
adjustedXp: 'Angepasste XP',
|
|
30
|
+
baseXp: 'Basis-XP',
|
|
31
|
+
multiplier: 'Gruppen-Multiplikator',
|
|
32
|
+
partyThreshold: 'Mittlere Schwelle',
|
|
33
|
+
belowEasy: 'Unter Leicht',
|
|
34
|
+
easy: 'Leicht',
|
|
35
|
+
medium: 'Mittel',
|
|
36
|
+
hard: 'Schwer',
|
|
37
|
+
deadly: 'Tödlich',
|
|
38
|
+
warning: 'Hinweis',
|
|
39
|
+
partyAdjustment: 'Der Multiplikator wurde angepasst, da die Gruppe weniger als drei oder mehr als fünf Charaktere umfasst.',
|
|
40
|
+
highCr: 'Ein Monster mit höherem CR als die Gruppenstufe kann Charaktere schnell zu Boden werfen.',
|
|
41
|
+
manyMonsters: 'Elf oder mehr Monster erschweren die Kampfverwaltung und erzeugen starke Aktionsschwankungen.',
|
|
42
|
+
rulesNote: 'Schätzung nach D&D 5e 2014 Regeln. Gelände, Taktik, Zauber und magische Gegenstände werden nicht berücksichtigt.',
|
|
43
|
+
rulesLinkLabel: 'Quellregeln lesen',
|
|
44
|
+
reset: 'Auf Beispiel zurücksetzen',
|
|
45
|
+
xpUnit: 'XP',
|
|
46
|
+
sceneLabel: 'Grafische Begegnungsdruck-Anzeige',
|
|
47
|
+
partyMarker: 'Gruppe',
|
|
48
|
+
threatMarker: 'Bedrohung',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const faq = [
|
|
52
|
+
{
|
|
53
|
+
question: 'Welche Regeln nutzt dieser Begegnungsrechner?',
|
|
54
|
+
answer: 'Er nutzt die D&D 5e 2014 Regeln aus den Grundregeln. Er addiert die Gruppenschwellen für leicht, mittel, schwer und tödlich und vergleicht sie mit den angepassten XP.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
question: 'Warum unterscheidet sich die angepasste XP von der Belohnungs-XP?',
|
|
58
|
+
answer: 'Die Regeln multiplizieren die Gesamt-XP der Monster, um die Gefahr mehrerer agierender Kreaturen abzubilden. Angepasste XP dienen nur dem Schwierigkeitsvergleich.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
question: 'Kann ich gemischte Monstergruppen berechnen?',
|
|
62
|
+
answer: 'Nutzen Sie das Tool als schnelle Schätzung für gleiche Monster. Für gemischte Gruppen addieren Sie die XP aller Kreaturen und wenden den Multiplikator an.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
question: 'Bedeutet ein tödliches Ergebnis den sicheren Tod?',
|
|
66
|
+
answer: 'Nein. Tödlich bedeutet, dass die angepasste XP die tödliche Schwelle erreicht. Taktik, Gelände, Zauber und Entscheidungen verändern das reale Ergebnis.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
question: 'Warum ändert die Gruppengröße den Multiplikator?',
|
|
70
|
+
answer: 'Die Grundregeln empfehlen eine Erhöhung des Multiplikators für Gruppen unter drei Charakteren und eine Verringerung für Gruppen ab sechs Charakteren.',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const howTo = [
|
|
75
|
+
{
|
|
76
|
+
name: 'Gruppenstufe festlegen',
|
|
77
|
+
text: 'Wählen Sie die durchschnittliche Stufe der Charaktere.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Gruppengröße wählen',
|
|
81
|
+
text: 'Geben Sie die Anzahl der teilnehmenden Charaktere ein.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'Bedrohung beschreiben',
|
|
85
|
+
text: 'Wählen Sie den Herausforderungsgrad (CR) und die Anzahl der Monster.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'Druck ablesen',
|
|
89
|
+
text: 'Vergleichen Sie die angepassten XP mit den Schwellenwerten.',
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
94
|
+
'@context': 'https://schema.org',
|
|
95
|
+
'@type': 'FAQPage',
|
|
96
|
+
mainEntity: faq.map((item) => ({
|
|
97
|
+
'@type': 'Question',
|
|
98
|
+
name: item.question,
|
|
99
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
100
|
+
})),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
104
|
+
'@context': 'https://schema.org',
|
|
105
|
+
'@type': 'SoftwareApplication',
|
|
106
|
+
name: 'D&D 5e Begegnungs-Schwierigkeitsrechner',
|
|
107
|
+
operatingSystem: 'All',
|
|
108
|
+
applicationCategory: 'GameApplication',
|
|
109
|
+
description: 'Berechnen Sie die Begegnungsschwierigkeit für D&D 5e 2014 basierend auf Stufe, Gruppengröße, Monster-CR, Anzahl und offiziellen Schwellenwerten.',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const howToSchema: WithContext<HowTo> = {
|
|
113
|
+
'@context': 'https://schema.org',
|
|
114
|
+
'@type': 'HowTo',
|
|
115
|
+
name: 'Wie man die D&D 5e Begegnungsschwierigkeit berechnet',
|
|
116
|
+
step: howTo.map((item) => ({
|
|
117
|
+
'@type': 'HowToStep',
|
|
118
|
+
name: item.name,
|
|
119
|
+
text: item.text,
|
|
120
|
+
})),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const content: EncounterDifficultyLocaleContent = {
|
|
124
|
+
slug: 'dnd-5e-begegnungs-schwierigkeitsrechner',
|
|
125
|
+
title: 'D&D 5e Begegnungs Schwierigkeitsrechner',
|
|
126
|
+
description: 'Schätzen Sie den Begegnungsdruck für D&D 5e 2014 mit Stufe, Gruppengröße, Monster-CR, Anzahl, angepassten XP und Schwellenwerten.',
|
|
127
|
+
ui,
|
|
128
|
+
seo: [
|
|
129
|
+
{ type: 'title', text: 'Begegnungsschwierigkeit vor der Initiative einschätzen', level: 2 },
|
|
130
|
+
{ type: 'paragraph', html: 'Ein D&D-Kampf ist mehr als nur die gedruckte Zahl eines Monsters. Dieser Rechner wandelt Gruppengröße, Stufe, CR und Monsteranzahl in die angepassten XP nach den D&D 5e 2014 Regeln um. Das visuelle Ergebnis zeigt übersichtlich, wo die geplante Szene im Vergleich zu den Schwellenwerten für leicht, mittel, schwer und tödlich liegt.' },
|
|
131
|
+
{ type: 'title', text: 'Wie die D&D 5e Begegnungsformel funktioniert', level: 2 },
|
|
132
|
+
{ type: 'paragraph', html: 'Die Methode addiert die XP-Schwellenwerte jedes Charakters für jede Schwierigkeitsstufe, summiert die Monster-XP und wendet den Gruppenmultiplikator an. Gruppen mit weniger als drei Charakteren nutzen den nächsthöheren Multiplikator, während Gruppen ab sechs Charakteren den nächstniedrigeren Multiplikator verwenden.' },
|
|
133
|
+
{
|
|
134
|
+
type: 'table',
|
|
135
|
+
headers: ['Signal', 'Bedeutung am Spieltisch'],
|
|
136
|
+
rows: [
|
|
137
|
+
['Unter Leicht', 'Die Szene dient als Aufwärmung oder zum leichten Ressourcenverbrauch.'],
|
|
138
|
+
['Leicht', 'Die Gruppe gewinnt in der Regel ohne großen Ressourceneinsatz.'],
|
|
139
|
+
['Mittel', 'Erwarten Sie spürbaren Druck und Entscheidungen über Ressourceneinsatz.'],
|
|
140
|
+
['Schwer', 'Planen Sie den Verlust von Trefferpunkten und Zauberslots ein.'],
|
|
141
|
+
['Tödlich', 'Prüfen Sie Taktik, Gelände und die Folgen schlechter Würfe.'],
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{ type: 'title', text: 'Warum die Monsteranzahl entscheidend ist', level: 2 },
|
|
145
|
+
{ type: 'paragraph', html: 'Mehrere Monster erzeugen durch mehr Aktionen und Reaktionen höhere Gefahr als ein einzelnes Monster mit gleichen Gesamt-XP. Deshalb nutzt ein Paar von Kreaturen einen höheren Multiplikator als eine einzelne Kreatur mit derselben kombinierten XP-Zahl.' },
|
|
146
|
+
{ type: 'tip', title: 'Hohen CR als spezifisches Warnsignal betrachten', html: 'Ein Monster mit höherem CR als die Gruppenstufe kann Charaktere mit einem Angriff ausschalten. Prüfen Sie Schaden und Effekte genau.' },
|
|
147
|
+
{ type: 'title', text: 'Das Ergebnis als Vorbereitungshilfe nutzen', level: 2 },
|
|
148
|
+
{ type: 'paragraph', html: 'Prüfen Sie vor schweren oder tödlichen Begegnungen Raumgröße, Deckung und Zauberoptionen der Charaktere. Enge Räume, Überraschung, Umgebungsschaden und Erschöpfung können die tatsächliche Gefahr am Spieltisch erheblich verändern.' },
|
|
149
|
+
{ type: 'tip', title: 'Gemischte Gruppen manuell anpassen', html: 'Addieren Sie für gemischte Gruppen die Basis-XP aller Kreaturen und wenden Sie den Multiplikator auf die Gesamtzahl an.' },
|
|
150
|
+
],
|
|
151
|
+
faq,
|
|
152
|
+
bibliography,
|
|
153
|
+
howTo,
|
|
154
|
+
schemas: [faqSchema, appSchema, howToSchema] as unknown as Record<string, unknown>[],
|
|
155
|
+
};
|