@crockery/fellowship-components 0.1.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/RELEASING.md +75 -0
  4. package/THIRD_PARTY_LICENSES.md +79 -0
  5. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  6. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  7. package/dist/assets/ibm-plex-sans-condensed-latin-400-normal-DKyD6XsY.woff2 +0 -0
  8. package/dist/assets/ibm-plex-sans-condensed-latin-500-normal-BqaLkUoX.woff2 +0 -0
  9. package/dist/assets/ibm-plex-sans-condensed-latin-600-normal-CRd5VyFf.woff2 +0 -0
  10. package/dist/assets/ibm-plex-sans-condensed-latin-700-normal-D8r4s4aS.woff2 +0 -0
  11. package/dist/chunks/data-source-CoIFvV72.js +10 -0
  12. package/dist/chunks/data-source-CoIFvV72.js.map +1 -0
  13. package/dist/chunks/decorate-B_ZTN9li.js +92 -0
  14. package/dist/chunks/decorate-B_ZTN9li.js.map +1 -0
  15. package/dist/chunks/dungeon-map-IPN6sEDj.js +1144 -0
  16. package/dist/chunks/dungeon-map-IPN6sEDj.js.map +1 -0
  17. package/dist/chunks/talent-calculator-Ch1Pbbba.js +1086 -0
  18. package/dist/chunks/talent-calculator-Ch1Pbbba.js.map +1 -0
  19. package/dist/chunks/tooltip-DoQVSnOn.js +412 -0
  20. package/dist/chunks/tooltip-DoQVSnOn.js.map +1 -0
  21. package/dist/data/data-source.d.ts +17 -0
  22. package/dist/data/data-source.d.ts.map +1 -0
  23. package/dist/data/package-data-source.d.ts +6 -0
  24. package/dist/data/package-data-source.d.ts.map +1 -0
  25. package/dist/data/package-talent-data-source.d.ts +6 -0
  26. package/dist/data/package-talent-data-source.d.ts.map +1 -0
  27. package/dist/data/talent-data-source.d.ts +11 -0
  28. package/dist/data/talent-data-source.d.ts.map +1 -0
  29. package/dist/dungeon-map/dungeon-map.d.ts +56 -0
  30. package/dist/dungeon-map/dungeon-map.d.ts.map +1 -0
  31. package/dist/dungeon-map/dungeon-map.styles.d.ts +2 -0
  32. package/dist/dungeon-map/dungeon-map.styles.d.ts.map +1 -0
  33. package/dist/dungeon-map/index.d.ts +5 -0
  34. package/dist/dungeon-map/index.d.ts.map +1 -0
  35. package/dist/dungeon-map/layout.d.ts +3 -0
  36. package/dist/dungeon-map/layout.d.ts.map +1 -0
  37. package/dist/dungeon-map/render-model.d.ts +7 -0
  38. package/dist/dungeon-map/render-model.d.ts.map +1 -0
  39. package/dist/dungeon-map/types.d.ts +107 -0
  40. package/dist/dungeon-map/types.d.ts.map +1 -0
  41. package/dist/dungeon-map/viewport-controller.d.ts +13 -0
  42. package/dist/dungeon-map/viewport-controller.d.ts.map +1 -0
  43. package/dist/dungeon-map.js +7 -0
  44. package/dist/dungeon-map.js.map +1 -0
  45. package/dist/index.d.ts +9 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +8 -0
  48. package/dist/react/dungeon-map.d.ts +22 -0
  49. package/dist/react/dungeon-map.d.ts.map +1 -0
  50. package/dist/react/dungeon-map.js +180 -0
  51. package/dist/react/dungeon-map.js.map +1 -0
  52. package/dist/react/index.d.ts +7 -0
  53. package/dist/react/index.d.ts.map +1 -0
  54. package/dist/react/talent-calculator.d.ts +22 -0
  55. package/dist/react/talent-calculator.d.ts.map +1 -0
  56. package/dist/react/talent-calculator.js +71 -0
  57. package/dist/react/talent-calculator.js.map +1 -0
  58. package/dist/react/tooltip.d.ts +15 -0
  59. package/dist/react/tooltip.d.ts.map +1 -0
  60. package/dist/react/tooltip.js +26 -0
  61. package/dist/react/tooltip.js.map +1 -0
  62. package/dist/react.js +4 -0
  63. package/dist/styles/field-manual-fonts.d.ts +3 -0
  64. package/dist/styles/field-manual-fonts.d.ts.map +1 -0
  65. package/dist/styles/field-manual.styles.d.ts +3 -0
  66. package/dist/styles/field-manual.styles.d.ts.map +1 -0
  67. package/dist/talent-calculator/index.d.ts +5 -0
  68. package/dist/talent-calculator/index.d.ts.map +1 -0
  69. package/dist/talent-calculator/render-model.d.ts +5 -0
  70. package/dist/talent-calculator/render-model.d.ts.map +1 -0
  71. package/dist/talent-calculator/selection-state.d.ts +10 -0
  72. package/dist/talent-calculator/selection-state.d.ts.map +1 -0
  73. package/dist/talent-calculator/talent-calculator.d.ts +51 -0
  74. package/dist/talent-calculator/talent-calculator.d.ts.map +1 -0
  75. package/dist/talent-calculator/talent-calculator.styles.d.ts +2 -0
  76. package/dist/talent-calculator/talent-calculator.styles.d.ts.map +1 -0
  77. package/dist/talent-calculator/types.d.ts +92 -0
  78. package/dist/talent-calculator/types.d.ts.map +1 -0
  79. package/dist/talent-calculator.js +7 -0
  80. package/dist/talent-calculator.js.map +1 -0
  81. package/dist/tooltip/index.d.ts +6 -0
  82. package/dist/tooltip/index.d.ts.map +1 -0
  83. package/dist/tooltip/positioning.d.ts +23 -0
  84. package/dist/tooltip/positioning.d.ts.map +1 -0
  85. package/dist/tooltip/rich-text.d.ts +4 -0
  86. package/dist/tooltip/rich-text.d.ts.map +1 -0
  87. package/dist/tooltip/tooltip.d.ts +33 -0
  88. package/dist/tooltip/tooltip.d.ts.map +1 -0
  89. package/dist/tooltip/tooltip.styles.d.ts +2 -0
  90. package/dist/tooltip/tooltip.styles.d.ts.map +1 -0
  91. package/dist/tooltip/types.d.ts +19 -0
  92. package/dist/tooltip/types.d.ts.map +1 -0
  93. package/dist/tooltip.js +7 -0
  94. package/dist/tooltip.js.map +1 -0
  95. package/package.json +149 -0
  96. package/src/dungeon-map/README.md +168 -0
  97. package/src/talent-calculator/README.md +190 -0
  98. package/src/tooltip/README.md +113 -0
@@ -0,0 +1,1144 @@
1
+ import { t as e } from "./data-source-CoIFvV72.js";
2
+ import { n as t, r as n, t as r } from "./decorate-B_ZTN9li.js";
3
+ import { findImage as i, getAssetUrl as a } from "@crockery/fellowship-data";
4
+ import { LitElement as o, css as s, html as c, nothing as l } from "lit";
5
+ import { customElement as u, property as d, query as f, state as p } from "lit/decorators.js";
6
+ import { repeat as m } from "lit/directives/repeat.js";
7
+ import { styleMap as h } from "lit/directives/style-map.js";
8
+ //#region src/data/package-data-source.ts
9
+ var g, _ = (t) => {
10
+ if (t?.aborted) throw new e("aborted", "The dungeon data request was aborted.", t.reason);
11
+ }, v = () => (g ??= Promise.all([
12
+ import("@crockery/fellowship-data/dungeons"),
13
+ import("@crockery/fellowship-data/enemies"),
14
+ import("@crockery/fellowship-data/manifest")
15
+ ]).then(([t, n, r]) => {
16
+ let i = r.default;
17
+ if (i.schemaVersion !== 1) throw new e("unsupported-schema", `Unsupported fellowship-data schema version: ${String(i.schemaVersion)}.`);
18
+ return {
19
+ manifest: i,
20
+ dungeons: new Map(t.default.map((e) => [e.id, e])),
21
+ enemies: new Map(n.default.map((e) => [e.id, e]))
22
+ };
23
+ }).catch((t) => {
24
+ throw g = void 0, t instanceof e ? t : new e("data-load-failed", "Unable to load @crockery/fellowship-data.", t);
25
+ }), g), y = (t = {}) => ({
26
+ async loadDungeon(t, n) {
27
+ _(n);
28
+ let r = await v();
29
+ _(n);
30
+ let i = r.dungeons.get(t);
31
+ if (!i) throw new e("dungeon-not-found", `No dungeon with ID “${t}” exists in @crockery/fellowship-data.`);
32
+ return {
33
+ manifest: r.manifest,
34
+ dungeon: i,
35
+ enemies: r.enemies
36
+ };
37
+ },
38
+ resolveAssetUrl(e) {
39
+ return t.assetBaseUrl ? a(e, { baseUrl: t.assetBaseUrl }) : a(e);
40
+ }
41
+ }), b = s`
42
+ :host {
43
+ --fellowship-map-soot: var(--_fellowship-field-canvas);
44
+ --fellowship-map-ink: var(--_fellowship-field-ink);
45
+ --fellowship-map-muted-ink: var(--_fellowship-field-muted);
46
+ --fellowship-map-border: var(--_fellowship-field-border);
47
+ --fellowship-map-focus: var(--_fellowship-field-focus);
48
+ --fellowship-map-rune: var(--_fellowship-field-accent);
49
+ --fellowship-map-ember: var(--_fellowship-field-ember);
50
+ --fellowship-map-control-surface: color-mix(
51
+ in srgb,
52
+ var(--_fellowship-field-surface-raised) 92%,
53
+ transparent
54
+ );
55
+ --fellowship-map-enemy-size: 32px;
56
+ --fellowship-map-radius: var(--_fellowship-field-radius);
57
+ --fellowship-map-font-family: var(--_fellowship-field-font-family);
58
+ --fellowship-map-tactical-font-family: var(--_fellowship-field-mono-font-family);
59
+
60
+ display: block;
61
+ inline-size: 100%;
62
+ min-block-size: 240px;
63
+ aspect-ratio: 2 / 1;
64
+ container-type: inline-size;
65
+ color: var(--fellowship-map-ink);
66
+ font-family: var(--fellowship-map-font-family);
67
+ -webkit-font-smoothing: antialiased;
68
+ }
69
+
70
+ :host([hidden]) {
71
+ display: none;
72
+ }
73
+
74
+ *,
75
+ *::before,
76
+ *::after {
77
+ box-sizing: border-box;
78
+ }
79
+
80
+ .viewer,
81
+ .viewport {
82
+ inline-size: 100%;
83
+ block-size: 100%;
84
+ min-block-size: inherit;
85
+ }
86
+
87
+ .viewer {
88
+ position: relative;
89
+ overflow: hidden;
90
+ border: 1px solid var(--fellowship-map-border);
91
+ border-radius: calc(var(--fellowship-map-radius) + 2px);
92
+ background: var(--fellowship-map-soot);
93
+ }
94
+
95
+ .viewport {
96
+ position: relative;
97
+ overflow: hidden;
98
+ outline: none;
99
+ cursor: grab;
100
+ touch-action: none;
101
+ }
102
+
103
+ .viewport[data-panning="true"] {
104
+ cursor: grabbing;
105
+ }
106
+
107
+ .viewport:focus-visible {
108
+ box-shadow: inset 0 0 0 2px var(--fellowship-map-focus);
109
+ }
110
+
111
+ .stage {
112
+ position: absolute;
113
+ inset: 0 auto auto 0;
114
+ transform-origin: 0 0;
115
+ will-change: transform;
116
+ }
117
+
118
+ .map-canvas,
119
+ .overlay {
120
+ position: absolute;
121
+ inset: 0;
122
+ inline-size: 100%;
123
+ block-size: 100%;
124
+ }
125
+
126
+ .map-canvas {
127
+ display: block;
128
+ background: #d5ceb1;
129
+ box-shadow: inset 0 0 0 1px rgb(0 0 0 / 10%);
130
+ }
131
+
132
+ .overlay {
133
+ pointer-events: none;
134
+ }
135
+
136
+ .group-anchor {
137
+ position: absolute;
138
+ pointer-events: none;
139
+ }
140
+
141
+ .default-group,
142
+ .custom-group-host {
143
+ position: absolute;
144
+ inset: 0;
145
+ }
146
+
147
+ .group-ring {
148
+ position: absolute;
149
+ inset: 0;
150
+ border: calc(1px * var(--fellowship-map-inverse-scale, 1)) solid transparent;
151
+ border-radius: 50%;
152
+ background: transparent;
153
+ transition:
154
+ border-color 140ms cubic-bezier(0.23, 1, 0.32, 1),
155
+ background-color 140ms cubic-bezier(0.23, 1, 0.32, 1);
156
+ }
157
+
158
+ .group-anchor:has(.enemy-anchor:hover) .group-ring,
159
+ .group-anchor:focus-within .group-ring {
160
+ border-color: var(--fellowship-map-rune);
161
+ background: color-mix(in srgb, var(--fellowship-map-rune) 8%, transparent);
162
+ }
163
+
164
+ .group-label {
165
+ position: absolute;
166
+ inset: 0 auto auto 50%;
167
+ z-index: 1;
168
+ display: inline-flex;
169
+ align-items: center;
170
+ min-block-size: 22px;
171
+ padding: 0 8px 0 12px;
172
+ border-radius: 4px;
173
+ background: var(--fellowship-map-control-surface);
174
+ color: var(--fellowship-map-ink);
175
+ box-shadow: inset 0 0 0 1px var(--fellowship-map-border);
176
+ font-family: var(--fellowship-map-tactical-font-family);
177
+ font-size: 10px;
178
+ font-weight: 600;
179
+ font-variant-numeric: tabular-nums;
180
+ letter-spacing: 0.04em;
181
+ line-height: 1;
182
+ white-space: nowrap;
183
+ transform: translate(-50%, -45%) scale(var(--fellowship-map-inverse-scale, 1));
184
+ transform-origin: center;
185
+ }
186
+
187
+ .group-label::before {
188
+ position: absolute;
189
+ inset: 5px auto 5px 0;
190
+ width: 2px;
191
+ background: var(--fellowship-map-rune);
192
+ content: "";
193
+ }
194
+
195
+ .enemy-anchor {
196
+ position: absolute;
197
+ z-index: 2;
198
+ inline-size: 0;
199
+ block-size: 0;
200
+ pointer-events: auto;
201
+ }
202
+
203
+ .enemy-visual {
204
+ position: absolute;
205
+ inset: 0 auto auto 0;
206
+ transform: translate(-50%, -50%) scale(var(--fellowship-map-inverse-scale, 1));
207
+ transform-origin: center;
208
+ }
209
+
210
+ .enemy-marker {
211
+ position: relative;
212
+ display: grid;
213
+ inline-size: var(--fellowship-map-enemy-size);
214
+ block-size: var(--fellowship-map-enemy-size);
215
+ place-items: center;
216
+ overflow: hidden;
217
+ border: 2px solid var(--fellowship-map-rune);
218
+ border-radius: 50%;
219
+ background: var(--_fellowship-field-surface-raised);
220
+ color: var(--fellowship-map-ink);
221
+ font-family: var(--fellowship-map-tactical-font-family);
222
+ font-size: 10px;
223
+ font-weight: 600;
224
+ box-shadow: 0 0 0 1px rgb(0 0 0 / 68%);
225
+ transition:
226
+ border-color 140ms cubic-bezier(0.23, 1, 0.32, 1),
227
+ transform 140ms cubic-bezier(0.23, 1, 0.32, 1);
228
+ }
229
+
230
+ .enemy-marker[data-boss="true"] {
231
+ inline-size: calc(var(--fellowship-map-enemy-size) + 4px);
232
+ block-size: calc(var(--fellowship-map-enemy-size) + 4px);
233
+ border-color: var(--fellowship-map-ember);
234
+ }
235
+
236
+ .enemy-anchor:hover .enemy-marker {
237
+ transform: scale(1.08);
238
+ }
239
+
240
+ .enemy-image {
241
+ inline-size: 100%;
242
+ block-size: 100%;
243
+ object-fit: cover;
244
+ }
245
+
246
+ .controls,
247
+ .map-selector {
248
+ position: absolute;
249
+ z-index: 20;
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 4px;
253
+ padding: 4px;
254
+ border-radius: calc(var(--fellowship-map-radius) + 2px);
255
+ background: var(--fellowship-map-control-surface);
256
+ box-shadow: inset 0 0 0 1px var(--fellowship-map-border);
257
+ backdrop-filter: blur(12px);
258
+ }
259
+
260
+ .map-selector {
261
+ inset: 12px auto auto 12px;
262
+ max-inline-size: calc(100% - 172px);
263
+ overflow-x: auto;
264
+ scrollbar-width: none;
265
+ }
266
+
267
+ .map-selector::-webkit-scrollbar {
268
+ display: none;
269
+ }
270
+
271
+ .controls {
272
+ inset: 12px 12px auto auto;
273
+ }
274
+
275
+ .control-button,
276
+ .map-selector-button {
277
+ position: relative;
278
+ min-inline-size: 44px;
279
+ min-block-size: 44px;
280
+ border: 0;
281
+ border-radius: 4px;
282
+ background: transparent;
283
+ color: var(--fellowship-map-muted-ink);
284
+ font: inherit;
285
+ font-size: 13px;
286
+ font-weight: 600;
287
+ line-height: 1;
288
+ cursor: pointer;
289
+ transition:
290
+ color 140ms cubic-bezier(0.23, 1, 0.32, 1),
291
+ background-color 140ms cubic-bezier(0.23, 1, 0.32, 1),
292
+ transform 120ms cubic-bezier(0.23, 1, 0.32, 1);
293
+ }
294
+
295
+ .control-button {
296
+ font-family: var(--fellowship-map-tactical-font-family);
297
+ font-size: 16px;
298
+ font-weight: 500;
299
+ }
300
+
301
+ .map-selector-button {
302
+ padding-inline: 12px;
303
+ letter-spacing: 0.02em;
304
+ white-space: nowrap;
305
+ }
306
+
307
+ .map-selector-button[aria-selected="true"] {
308
+ background: rgb(255 255 255 / 6%);
309
+ color: var(--fellowship-map-ink);
310
+ }
311
+
312
+ .map-selector-button[aria-selected="true"]::after {
313
+ position: absolute;
314
+ inset: auto 12px 0;
315
+ height: 2px;
316
+ background: var(--fellowship-map-rune);
317
+ content: "";
318
+ }
319
+
320
+ .control-button:focus-visible,
321
+ .map-selector-button:focus-visible,
322
+ .retry-button:focus-visible {
323
+ outline: 2px solid var(--fellowship-map-focus);
324
+ outline-offset: 2px;
325
+ }
326
+
327
+ .control-button:active,
328
+ .map-selector-button:active,
329
+ .retry-button:active {
330
+ transform: scale(0.97);
331
+ }
332
+
333
+ .status {
334
+ position: absolute;
335
+ z-index: 30;
336
+ inset: 0;
337
+ display: grid;
338
+ padding: 24px;
339
+ place-items: center;
340
+ background: color-mix(in srgb, var(--fellowship-map-soot) 84%, transparent);
341
+ text-align: center;
342
+ }
343
+
344
+ .status-card {
345
+ display: grid;
346
+ max-inline-size: 376px;
347
+ gap: 8px;
348
+ justify-items: center;
349
+ padding: 24px;
350
+ border-radius: calc(var(--fellowship-map-radius) + 2px);
351
+ background: var(--fellowship-map-control-surface);
352
+ box-shadow: inset 0 0 0 1px var(--fellowship-map-border);
353
+ }
354
+
355
+ .status-mark {
356
+ position: relative;
357
+ display: grid;
358
+ inline-size: 40px;
359
+ block-size: 24px;
360
+ place-items: center;
361
+ color: var(--fellowship-map-rune);
362
+ font-family: var(--fellowship-map-tactical-font-family);
363
+ font-size: 12px;
364
+ }
365
+
366
+ .status-mark::before,
367
+ .status-mark::after {
368
+ position: absolute;
369
+ width: 8px;
370
+ height: 1px;
371
+ background: var(--fellowship-map-border);
372
+ content: "";
373
+ }
374
+
375
+ .status-mark::before {
376
+ left: 0;
377
+ }
378
+
379
+ .status-mark::after {
380
+ right: 0;
381
+ }
382
+
383
+ .status-title {
384
+ margin: 4px 0 0;
385
+ color: var(--fellowship-map-ink);
386
+ font-size: 18px;
387
+ font-weight: 600;
388
+ line-height: 24px;
389
+ text-wrap: balance;
390
+ }
391
+
392
+ .status-copy {
393
+ margin: 0;
394
+ color: var(--fellowship-map-muted-ink);
395
+ font-size: 14px;
396
+ line-height: 1.5;
397
+ text-wrap: pretty;
398
+ }
399
+
400
+ .retry-button {
401
+ min-block-size: 44px;
402
+ margin-block-start: 8px;
403
+ padding-inline: 16px;
404
+ border: 1px solid var(--fellowship-map-border);
405
+ border-radius: 4px;
406
+ background: var(--_fellowship-field-inset);
407
+ color: var(--fellowship-map-ink);
408
+ font: inherit;
409
+ font-weight: 600;
410
+ cursor: pointer;
411
+ }
412
+
413
+ .empty-badge {
414
+ position: absolute;
415
+ z-index: 20;
416
+ inset: auto auto 12px 12px;
417
+ padding: 8px;
418
+ border-radius: 4px;
419
+ background: var(--fellowship-map-control-surface);
420
+ color: var(--fellowship-map-muted-ink);
421
+ box-shadow: inset 0 0 0 1px var(--fellowship-map-border);
422
+ font-size: 12px;
423
+ }
424
+
425
+ .visually-hidden {
426
+ position: absolute;
427
+ inline-size: 1px;
428
+ block-size: 1px;
429
+ overflow: hidden;
430
+ clip: rect(0 0 0 0);
431
+ clip-path: inset(50%);
432
+ white-space: nowrap;
433
+ }
434
+
435
+ @media (hover: hover) {
436
+ .control-button:hover,
437
+ .map-selector-button:hover {
438
+ background: rgb(255 255 255 / 6%);
439
+ color: var(--fellowship-map-ink);
440
+ }
441
+
442
+ .retry-button:hover {
443
+ border-color: color-mix(in srgb, var(--fellowship-map-rune) 48%, transparent);
444
+ color: var(--fellowship-map-focus);
445
+ }
446
+ }
447
+
448
+ @container (max-width: 560px) {
449
+ .map-selector,
450
+ .controls {
451
+ inset-block-start: 8px;
452
+ }
453
+
454
+ .map-selector {
455
+ inset-inline-start: 8px;
456
+ max-inline-size: calc(100% - 164px);
457
+ }
458
+
459
+ .controls {
460
+ inset-inline-end: 8px;
461
+ }
462
+
463
+ .map-selector-button {
464
+ padding-inline: 8px;
465
+ }
466
+
467
+ .map-selector-button[aria-selected="true"]::after {
468
+ inset-inline: 8px;
469
+ }
470
+ }
471
+
472
+ @media (prefers-reduced-motion: reduce) {
473
+ .group-ring,
474
+ .enemy-marker,
475
+ .control-button,
476
+ .map-selector-button,
477
+ .retry-button {
478
+ transition: none;
479
+ }
480
+
481
+ .control-button:active,
482
+ .map-selector-button:active,
483
+ .retry-button:active {
484
+ transform: none;
485
+ }
486
+ }
487
+ `, x = .018, S = .036, C = .06, w = .12, T = (e, t, n) => Math.min(n, Math.max(t, e)), E = (e) => {
488
+ let t = e.map(({ position: e }) => e.x), n = e.map(({ position: e }) => e.y), r = Math.min(...t), i = Math.max(...t), a = Math.min(...n), o = Math.max(...n);
489
+ return {
490
+ x: r,
491
+ y: a,
492
+ width: i - r,
493
+ height: o - a
494
+ };
495
+ }, D = (e) => {
496
+ let t = e.x + e.width / 2, n = e.y + e.height / 2, r = Math.max(C, e.width + x * 2), i = Math.max(w, e.height + S * 2);
497
+ return {
498
+ x: T(t - r / 2, 0, Math.max(0, 1 - r)),
499
+ y: T(n - i / 2, 0, Math.max(0, 1 - i)),
500
+ width: Math.min(r, 1),
501
+ height: Math.min(i, 1)
502
+ };
503
+ }, O = (e) => ({
504
+ x: e.reduce((e, t) => e + t.position.x, 0) / e.length,
505
+ y: e.reduce((e, t) => e + t.position.y, 0) / e.length
506
+ }), k = (e) => {
507
+ let t = e.map(({ enemy: e }) => e?.dungeonScore).filter((e) => typeof e == "number");
508
+ return t.length > 0 ? t.reduce((e, t) => e + t, 0) : null;
509
+ }, A = (e, t) => {
510
+ let n = E(t);
511
+ return {
512
+ id: e,
513
+ enemies: t,
514
+ enemyBounds: n,
515
+ visualBounds: D(n),
516
+ center: O(t),
517
+ dungeonScore: k(t)
518
+ };
519
+ }, j = (e) => e !== null && Number.isFinite(e.x) && Number.isFinite(e.y) && e.x >= 0 && e.x <= 1 && e.y >= 0 && e.y <= 1, M = (e, t, n) => {
520
+ let r = [], a = /* @__PURE__ */ new Map();
521
+ return e.dungeon.enemies.forEach((o, s) => {
522
+ if (o.mapImageId !== t.id) return;
523
+ if (!j(o.mapPosition)) {
524
+ r.push({
525
+ code: "invalid-map-position",
526
+ message: `Skipped ${o.id}: its map position is missing or outside the map.`,
527
+ placementIndex: s,
528
+ enemyId: o.id
529
+ });
530
+ return;
531
+ }
532
+ let c = e.enemies.get(o.id) ?? null;
533
+ c || r.push({
534
+ code: "missing-enemy",
535
+ message: `No enemy metadata exists for ${o.id}.`,
536
+ placementIndex: s,
537
+ enemyId: o.id
538
+ });
539
+ let l = c ? i(c.images, {
540
+ type: "enemy",
541
+ size: "200"
542
+ }) : null, u = null;
543
+ if (l) try {
544
+ u = n.resolveAssetUrl(l);
545
+ } catch (e) {
546
+ r.push({
547
+ code: "asset-url-error",
548
+ message: `Could not resolve the image URL for ${o.id}: ${String(e)}.`,
549
+ placementIndex: s,
550
+ enemyId: o.id
551
+ });
552
+ }
553
+ else c && r.push({
554
+ code: "missing-enemy-image",
555
+ message: `No 200px enemy image exists for ${o.id}.`,
556
+ placementIndex: s,
557
+ enemyId: o.id
558
+ });
559
+ let d = {
560
+ instanceId: `${e.dungeon.id}:${t.id}:${s}`,
561
+ enemy: c,
562
+ placement: o,
563
+ position: o.mapPosition,
564
+ imageUrl: u
565
+ }, f = o.groupId === null ? `singleton:${s}` : `group:${o.groupId}`, p = a.get(f) ?? {
566
+ id: o.groupId,
567
+ enemies: []
568
+ };
569
+ p.enemies.push(d), a.set(f, p);
570
+ }), {
571
+ dungeon: e.dungeon,
572
+ mapImage: t,
573
+ groups: [...a.values()].map(({ id: e, enemies: t }) => A(e, t)),
574
+ warnings: r
575
+ };
576
+ }, N = (e, t) => {
577
+ let n = (e.id.split(":").at(-1) ?? e.id).replace(/^T_(?:Map_)?/i, "").replace(/^map[-_]/i, "").replaceAll(/[_-]+/g, " ").replace(/([a-z\d])([A-Z])/g, "$1 $2").trim(), r = t.id.split(".").at(-1)?.toLowerCase();
578
+ return (n.split(/\s+/).filter((e) => e.toLowerCase() !== r).join(" ") || n || "Map").replace(/\b[a-z]/g, (e) => e.toUpperCase());
579
+ }, P = (e, t, n) => Math.min(n, Math.max(t, e)), F = class {
580
+ constructor() {
581
+ this.#e = 0, this.#t = 0, this.#n = 0, this.#r = 0, this.#i = 1, this.#a = 0, this.#o = 0, this.maxZoom = 8;
582
+ }
583
+ #e;
584
+ #t;
585
+ #n;
586
+ #r;
587
+ #i;
588
+ #a;
589
+ #o;
590
+ get state() {
591
+ return {
592
+ zoom: this.#i,
593
+ scale: this.scale,
594
+ x: this.#a,
595
+ y: this.#o
596
+ };
597
+ }
598
+ get scale() {
599
+ return this.fitScale * this.#i;
600
+ }
601
+ get fitScale() {
602
+ return !this.#e || !this.#t || !this.#n || !this.#r ? 1 : Math.min(this.#e / this.#n, this.#t / this.#r);
603
+ }
604
+ setDimensions(e, t, n, r) {
605
+ let i = this.scale, a = this.#n ? (this.#e / 2 - this.#a) / i / this.#n : .5, o = this.#r ? (this.#t / 2 - this.#o) / i / this.#r : .5;
606
+ return this.#e = Math.max(0, e), this.#t = Math.max(0, t), this.#n = Math.max(0, n), this.#r = Math.max(0, r), this.#a = this.#e / 2 - a * this.#n * this.scale, this.#o = this.#t / 2 - o * this.#r * this.scale, this.#s(), this.state;
607
+ }
608
+ fit() {
609
+ return this.#i = 1, this.#a = (this.#e - this.#n * this.scale) / 2, this.#o = (this.#t - this.#r * this.scale) / 2, this.state;
610
+ }
611
+ zoomAt(e, t = this.#e / 2, n = this.#t / 2) {
612
+ let r = this.scale, i = (t - this.#a) / r, a = (n - this.#o) / r;
613
+ return this.#i = P(this.#i * e, 1, Math.max(1, this.maxZoom)), this.#a = t - i * this.scale, this.#o = n - a * this.scale, this.#s(), this.state;
614
+ }
615
+ panBy(e, t) {
616
+ return this.#a += e, this.#o += t, this.#s(), this.state;
617
+ }
618
+ #s() {
619
+ let e = this.#n * this.scale, t = this.#r * this.scale;
620
+ this.#a = e <= this.#e ? (this.#e - e) / 2 : P(this.#a, this.#e - e, 0), this.#o = t <= this.#t ? (this.#t - t) / 2 : P(this.#o, this.#t - t, 0);
621
+ }
622
+ }, I = {
623
+ bubbles: !0,
624
+ composed: !0
625
+ }, L = class extends o {
626
+ constructor(...e) {
627
+ super(...e), this.dungeonId = "", this.mapImageId = null, this.assetBaseUrl = null, this.dataSource = null, this.renderEnemy = null, this.renderGroup = null, this.mapLabelFormatter = null, this.showControls = !0, this.showMapSelector = !0, this.maxZoom = 8, this.externalOverlay = !1, this._status = "empty", this._error = null, this._data = null, this._model = null, this._selectedMap = null, this._viewportState = {
628
+ zoom: 1,
629
+ scale: 1,
630
+ x: 0,
631
+ y: 0
632
+ }, this._isPanning = !1, this._enemyRenderContent = /* @__PURE__ */ new Map(), this._groupRenderContent = /* @__PURE__ */ new Map(), this._mapLabels = /* @__PURE__ */ new Map(), this.#e = null, this.#t = null, this.#n = 0, this.#r = 0, this.#i = null, this.#a = new F(), this.#o = /* @__PURE__ */ new Map(), this.#s = null, this.#c = 0, this.#l = null, this.#u = /* @__PURE__ */ new Set(), this.#d = [], this.#j = (e) => {
633
+ if (![
634
+ "ArrowLeft",
635
+ "ArrowRight",
636
+ "Home",
637
+ "End"
638
+ ].includes(e.key)) return;
639
+ let t = this._data?.dungeon.mapImages ?? [], n = t.findIndex(({ id: e }) => e === this._selectedMap?.id), r = e.key === "Home" ? 0 : e.key === "End" ? t.length - 1 : e.key === "ArrowRight" ? (n + 1) % t.length : (n - 1 + t.length) % t.length, i = t[r];
640
+ i && (e.preventDefault(), this.#A(i), this.updateComplete.then(() => {
641
+ this.renderRoot.querySelector(`[data-map-index="${r}"]`)?.focus();
642
+ }));
643
+ }, this.#M = (e) => {
644
+ if (!this._model || this._status !== "ready") return;
645
+ e.preventDefault();
646
+ let t = this._viewportElement?.getBoundingClientRect();
647
+ if (!t) return;
648
+ let n = Math.exp(-e.deltaY * .0015);
649
+ this.#a.zoomAt(n, e.clientX - t.left, e.clientY - t.top), this.#z("wheel");
650
+ }, this.#N = (e) => {
651
+ if (!(e.target !== this._viewportElement || !this._model)) {
652
+ if (e.key === "+" || e.key === "=") e.preventDefault(), this.#a.zoomAt(1.35);
653
+ else if (e.key === "-") e.preventDefault(), this.#a.zoomAt(1 / 1.35);
654
+ else if (e.key === "0") e.preventDefault(), this.#a.fit();
655
+ else if (e.key === "ArrowLeft") e.preventDefault(), this.#a.panBy(56, 0);
656
+ else if (e.key === "ArrowRight") e.preventDefault(), this.#a.panBy(-56, 0);
657
+ else if (e.key === "ArrowUp") e.preventDefault(), this.#a.panBy(0, 56);
658
+ else if (e.key === "ArrowDown") e.preventDefault(), this.#a.panBy(0, -56);
659
+ else return;
660
+ this.#z("keyboard");
661
+ }
662
+ }, this.#P = (e) => {
663
+ if (!this._model || this.#L(e)) return;
664
+ let t = this._viewportElement, n = t?.getBoundingClientRect();
665
+ if (!t || !n) return;
666
+ t.setPointerCapture(e.pointerId);
667
+ let r = {
668
+ x: e.clientX - n.left,
669
+ y: e.clientY - n.top
670
+ };
671
+ if (this.#o.set(e.pointerId, r), this._isPanning = !0, this.#o.size === 1) this.#s = r;
672
+ else if (this.#o.size === 2) {
673
+ let [e, t] = [...this.#o.values()];
674
+ e && t && (this.#c = Math.hypot(t.x - e.x, t.y - e.y), this.#l = {
675
+ x: (e.x + t.x) / 2,
676
+ y: (e.y + t.y) / 2
677
+ });
678
+ }
679
+ }, this.#F = (e) => {
680
+ if (!this.#o.has(e.pointerId)) return;
681
+ let t = this._viewportElement?.getBoundingClientRect();
682
+ if (!t) return;
683
+ let n = {
684
+ x: e.clientX - t.left,
685
+ y: e.clientY - t.top
686
+ };
687
+ if (this.#o.set(e.pointerId, n), this.#o.size === 1 && this.#s) {
688
+ this.#a.panBy(n.x - this.#s.x, n.y - this.#s.y), this.#s = n, this.#z("pan");
689
+ return;
690
+ }
691
+ if (this.#o.size === 2) {
692
+ let [e, t] = [...this.#o.values()];
693
+ if (!e || !t) return;
694
+ let n = Math.hypot(t.x - e.x, t.y - e.y), r = {
695
+ x: (e.x + t.x) / 2,
696
+ y: (e.y + t.y) / 2
697
+ };
698
+ this.#c > 0 && this.#a.zoomAt(n / this.#c, r.x, r.y), this.#l && this.#a.panBy(r.x - this.#l.x, r.y - this.#l.y), this.#c = n, this.#l = r, this.#z("pinch");
699
+ }
700
+ }, this.#I = (e) => {
701
+ this.#o.delete(e.pointerId), this.#o.size === 1 ? this.#s = [...this.#o.values()][0] ?? null : this.#o.size === 0 && (this.#s = null, this.#l = null, this.#c = 0, this._isPanning = !1);
702
+ };
703
+ }
704
+ static {
705
+ this.styles = [n, b];
706
+ }
707
+ #e;
708
+ #t;
709
+ #n;
710
+ #r;
711
+ #i;
712
+ #a;
713
+ #o;
714
+ #s;
715
+ #c;
716
+ #l;
717
+ #u;
718
+ #d;
719
+ connectedCallback() {
720
+ super.connectedCallback(), t(this.ownerDocument), this.#i ??= new ResizeObserver(() => this.#R());
721
+ }
722
+ disconnectedCallback() {
723
+ this.#t?.abort(), this.#i?.disconnect(), super.disconnectedCallback();
724
+ }
725
+ firstUpdated(e) {
726
+ super.firstUpdated(e);
727
+ let t = this._viewportElement;
728
+ t && this.#i?.observe(t);
729
+ }
730
+ updated(e) {
731
+ if (super.updated(e), this.#V(), e.has("dungeonId") || e.has("dataSource") || e.has("assetBaseUrl")) {
732
+ queueMicrotask(() => {
733
+ this.isConnected && this.#m();
734
+ });
735
+ return;
736
+ }
737
+ e.has("mapImageId") && this._data && queueMicrotask(() => {
738
+ this.isConnected && this.#h(!0);
739
+ }), e.has("maxZoom") && e.get("maxZoom") !== void 0 && (this.#a.maxZoom = Math.max(1, this.maxZoom), this.#a.zoomAt(1), queueMicrotask(() => this.#z("button")));
740
+ }
741
+ willUpdate(e) {
742
+ super.willUpdate(e), (e.has("renderEnemy") || e.has("renderGroup") || e.has("mapLabelFormatter")) && this.#u.clear(), (e.has("_model") || e.has("renderEnemy") || e.has("renderGroup")) && this.#v(), (e.has("_data") || e.has("mapLabelFormatter")) && this.#y();
743
+ }
744
+ fitToView() {
745
+ this.#a.fit(), this.#z("fit");
746
+ }
747
+ zoomIn() {
748
+ this.#a.zoomAt(1.35), this.#z("button");
749
+ }
750
+ zoomOut() {
751
+ this.#a.zoomAt(1 / 1.35), this.#z("button");
752
+ }
753
+ retry() {
754
+ this.#m();
755
+ }
756
+ get renderModel() {
757
+ return this._model;
758
+ }
759
+ render() {
760
+ let e = this._model, t = this._viewportState, n = this.#p, r = e ? {
761
+ width: `${e.mapImage.width}px`,
762
+ height: `${e.mapImage.height}px`,
763
+ transform: `translate3d(${t.x}px, ${t.y}px, 0) scale(${t.scale})`,
764
+ "--fellowship-map-inverse-scale": String(1 / t.scale)
765
+ } : {};
766
+ return c`
767
+ <section class="viewer" aria-label=${this.#f}>
768
+ <div
769
+ class="viewport"
770
+ part="viewport"
771
+ role="region"
772
+ aria-label="Interactive dungeon map"
773
+ tabindex="0"
774
+ aria-describedby="map-instructions"
775
+ data-panning=${String(this._isPanning)}
776
+ @wheel=${this.#M}
777
+ @keydown=${this.#N}
778
+ @pointerdown=${this.#P}
779
+ @pointermove=${this.#F}
780
+ @pointerup=${this.#I}
781
+ @pointercancel=${this.#I}
782
+ >
783
+ ${e ? c`
784
+ <div
785
+ class="stage"
786
+ id="map-stage"
787
+ role=${n === l ? l : "tabpanel"}
788
+ aria-labelledby=${n}
789
+ style=${h(r)}
790
+ >
791
+ <canvas
792
+ class="map-canvas"
793
+ part="map-canvas"
794
+ role="img"
795
+ aria-label=${`${e.dungeon.name ?? e.dungeon.id}, ${this.#k(e.mapImage)}`}
796
+ .width=${e.mapImage.width}
797
+ .height=${e.mapImage.height}
798
+ ></canvas>
799
+ <div class="overlay" part="overlay">
800
+ ${this.externalOverlay ? c`<slot name="overlay"></slot>` : this.#b(e)}
801
+ </div>
802
+ </div>
803
+ ${this.#E(e)} ${this.#D()}
804
+ ${e.groups.length === 0 && this._status === "ready" ? c`<div class="empty-badge">No enemy placements on this map</div>` : l}
805
+ ` : l}
806
+ ${this.#O()}
807
+ <p class="visually-hidden" id="map-instructions">
808
+ Drag to pan. Use the mouse wheel, plus and minus keys, or zoom controls to zoom. Press
809
+ zero to fit the full map.
810
+ </p>
811
+ </div>
812
+ </section>
813
+ `;
814
+ }
815
+ get #f() {
816
+ return this._data ? `${this._data.dungeon.name ?? this._data.dungeon.id} dungeon map` : "Fellowship dungeon map";
817
+ }
818
+ get #p() {
819
+ if (!this.showMapSelector || !this._model || this._model.dungeon.mapImages.length <= 1 || !this._selectedMap) return l;
820
+ let e = this._model.dungeon.mapImages.findIndex((e) => e.id === this._selectedMap?.id);
821
+ return e >= 0 ? `map-tab-${e}` : l;
822
+ }
823
+ async #m() {
824
+ let t = this.dungeonId.trim(), n = ++this.#n;
825
+ if (this.#r += 1, this.#t?.abort(), this.#t = new AbortController(), this._data = null, this._model = null, this._selectedMap = null, this._error = null, this.#u.clear(), !t) {
826
+ this._status = "empty";
827
+ return;
828
+ }
829
+ this._status = "loading";
830
+ let r = this.dataSource ?? y(this.assetBaseUrl ? { assetBaseUrl: this.assetBaseUrl } : {});
831
+ this.#e = r;
832
+ try {
833
+ let i = await r.loadDungeon(t, this.#t.signal);
834
+ if (n !== this.#n || this.#t.signal.aborted) return;
835
+ if (i.manifest.schemaVersion !== 1) throw new e("unsupported-schema", `Unsupported fellowship-data schema version: ${String(i.manifest.schemaVersion)}.`);
836
+ this._data = i, await this.#h(!0);
837
+ } catch (e) {
838
+ if (n !== this.#n || this.#t.signal.aborted) return;
839
+ this.#_(e);
840
+ }
841
+ }
842
+ async #h(e) {
843
+ let t = this._data, n = this.#e;
844
+ if (!t || !n) return;
845
+ let r = this.mapImageId ? t.dungeon.mapImages.find(({ id: e }) => e === this.mapImageId) : t.dungeon.mapImages[0];
846
+ if (!r) {
847
+ this.#_({
848
+ code: "map-not-found",
849
+ message: this.mapImageId ? `No map image with ID “${this.mapImageId}” exists for this dungeon.` : "This dungeon does not include a map image."
850
+ });
851
+ return;
852
+ }
853
+ let i = ++this.#r, a = M(t, r, n);
854
+ this._model = a, this._selectedMap = r, this._status = "loading", this._error = null, await this.updateComplete;
855
+ let o = this._viewportElement;
856
+ o && (this.#a.maxZoom = Math.max(1, this.maxZoom), this.#a.setDimensions(o.clientWidth, o.clientHeight, r.width, r.height), e && this.#a.fit(), this._viewportState = this.#a.state, await this.updateComplete);
857
+ try {
858
+ if (await this.#g(r, n, i), i !== this.#r) return;
859
+ this._status = "ready", this.dispatchEvent(new CustomEvent("fellowship-map-ready", {
860
+ ...I,
861
+ detail: {
862
+ model: a,
863
+ data: t
864
+ }
865
+ })), a.warnings.length > 0 && this.dispatchEvent(new CustomEvent("fellowship-map-warning", {
866
+ ...I,
867
+ detail: {
868
+ warnings: a.warnings,
869
+ model: a
870
+ }
871
+ }));
872
+ } catch (e) {
873
+ i === this.#r && this.#_({
874
+ code: "map-image-error",
875
+ message: `Unable to load the dungeon map image: ${String(e)}.`,
876
+ cause: e
877
+ });
878
+ }
879
+ }
880
+ async #g(e, t, n) {
881
+ let r = new Image();
882
+ if (r.crossOrigin = "anonymous", r.src = t.resolveAssetUrl(e), await r.decode(), n !== this.#r) return;
883
+ let i = this._canvasElement, a = i?.getContext("2d");
884
+ if (!i || !a) throw Error("A 2D canvas context is unavailable.");
885
+ a.clearRect(0, 0, i.width, i.height), a.drawImage(r, 0, 0, e.width, e.height);
886
+ }
887
+ #_(t) {
888
+ let n = typeof t == "object" && t && "code" in t && "message" in t ? {
889
+ code: String(t.code),
890
+ message: String(t.message),
891
+ ...t instanceof e && t.cause !== void 0 ? { cause: t.cause } : {}
892
+ } : {
893
+ code: "unknown",
894
+ message: t instanceof Error ? t.message : String(t)
895
+ };
896
+ this._error = n, this._status = "error", this.dispatchEvent(new CustomEvent("fellowship-map-error", {
897
+ ...I,
898
+ detail: n
899
+ }));
900
+ }
901
+ #v() {
902
+ let e = this._model, t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map();
903
+ if (!e) {
904
+ this._enemyRenderContent = t, this._groupRenderContent = n;
905
+ return;
906
+ }
907
+ if (this.renderEnemy) for (let n of e.groups) for (let r of n.enemies) try {
908
+ t.set(r.instanceId, this.renderEnemy({
909
+ model: r,
910
+ group: n,
911
+ dungeon: e.dungeon,
912
+ mapImage: e.mapImage
913
+ }));
914
+ } catch (e) {
915
+ this.#B(`enemy:${r.instanceId}`, "enemy-renderer-error", e);
916
+ }
917
+ if (this._enemyRenderContent = t, this.renderGroup) for (let t of e.groups) {
918
+ let r = t.enemies.map((e) => this.#w(t, e)), i = this.#x(e, t);
919
+ try {
920
+ n.set(i, this.renderGroup({
921
+ group: t,
922
+ children: r,
923
+ dungeon: e.dungeon,
924
+ mapImage: e.mapImage
925
+ }));
926
+ } catch (e) {
927
+ this.#B(`group:${i}`, "group-renderer-error", e);
928
+ }
929
+ }
930
+ this._groupRenderContent = n;
931
+ }
932
+ #y() {
933
+ let e = /* @__PURE__ */ new Map(), t = this._data?.dungeon;
934
+ if (t) for (let n of t.mapImages) try {
935
+ e.set(n.id, this.mapLabelFormatter?.(n, t) ?? N(n, t));
936
+ } catch (r) {
937
+ this.#B(`map-label:${n.id}`, "map-label-formatter-error", r), e.set(n.id, N(n, t));
938
+ }
939
+ this._mapLabels = e;
940
+ }
941
+ #b(e) {
942
+ return m(e.groups, (t) => this.#x(e, t), (t) => this.#S(e, t));
943
+ }
944
+ #x(e, t) {
945
+ return `${e.mapImage.id}:${String(t.id)}:${t.enemies[0]?.instanceId ?? "empty"}`;
946
+ }
947
+ #S(e, t) {
948
+ let n = t.visualBounds, r = {
949
+ left: `${n.x * 100}%`,
950
+ top: `${n.y * 100}%`,
951
+ width: `${n.width * 100}%`,
952
+ height: `${n.height * 100}%`
953
+ }, i = this.#x(e, t), a = this.renderGroup && this._groupRenderContent.has(i) ? this._groupRenderContent.get(i) ?? l : this.#C(t);
954
+ return c` <div class="group-anchor" part="group" style=${h(r)}>${a}</div> `;
955
+ }
956
+ #C(e) {
957
+ return c`
958
+ <div class="default-group">
959
+ <div class="group-ring" aria-hidden="true"></div>
960
+ <span class="group-label">${`G${e.id ?? "–"}${e.dungeonScore === null ? "" : ` · ${e.dungeonScore}`}`}</span>
961
+ ${m(e.enemies, (e) => e.instanceId, (t) => this.#w(e, t))}
962
+ </div>
963
+ `;
964
+ }
965
+ #w(e, t) {
966
+ let n = e.visualBounds, r = {
967
+ left: `${(t.position.x - n.x) / n.width * 100}%`,
968
+ top: `${(t.position.y - n.y) / n.height * 100}%`
969
+ }, i = this.renderEnemy && this._enemyRenderContent.has(t.instanceId) ? this._enemyRenderContent.get(t.instanceId) ?? l : this.#T(t), a = t.enemy?.name ?? t.placement.id;
970
+ return c`
971
+ <div
972
+ class="enemy-anchor"
973
+ part="enemy"
974
+ style=${h(r)}
975
+ role="img"
976
+ aria-label=${a}
977
+ title=${a}
978
+ >
979
+ <div class="enemy-visual">${i}</div>
980
+ </div>
981
+ `;
982
+ }
983
+ #T(e) {
984
+ let t = (e.enemy?.name ?? e.placement.id).split(/\s+/).map((e) => e[0]).join("").slice(0, 2).toUpperCase();
985
+ return c`
986
+ <span class="enemy-marker" data-boss=${String(e.enemy?.isBoss === !0)}>
987
+ ${e.imageUrl ? c`<img class="enemy-image" src=${e.imageUrl} alt="" draggable="false" />` : t}
988
+ </span>
989
+ `;
990
+ }
991
+ #E(e) {
992
+ let t = e.dungeon.mapImages;
993
+ return !this.showMapSelector || t.length <= 1 ? l : c`
994
+ <div class="map-selector" part="map-selector" role="tablist" aria-label="Dungeon maps">
995
+ ${m(t, (e) => e.id, (e, t) => c`
996
+ <button
997
+ class="map-selector-button"
998
+ id=${`map-tab-${t}`}
999
+ part="map-selector-button"
1000
+ type="button"
1001
+ role="tab"
1002
+ aria-controls="map-stage"
1003
+ aria-selected=${String(e.id === this._selectedMap?.id)}
1004
+ tabindex=${e.id === this._selectedMap?.id ? "0" : "-1"}
1005
+ data-map-index=${String(t)}
1006
+ @click=${() => this.#A(e)}
1007
+ @keydown=${this.#j}
1008
+ >
1009
+ ${this.#k(e)}
1010
+ </button>
1011
+ `)}
1012
+ </div>
1013
+ `;
1014
+ }
1015
+ #D() {
1016
+ return !this.showControls || !this._model ? l : c`
1017
+ <div class="controls" part="controls" role="group" aria-label="Map zoom controls">
1018
+ <button
1019
+ class="control-button"
1020
+ part="control-button"
1021
+ type="button"
1022
+ aria-label="Zoom out"
1023
+ title="Zoom out"
1024
+ @click=${this.zoomOut}
1025
+ >
1026
+
1027
+ </button>
1028
+ <button
1029
+ class="control-button"
1030
+ part="control-button"
1031
+ type="button"
1032
+ aria-label="Fit map"
1033
+ title="Fit map"
1034
+ @click=${this.fitToView}
1035
+ >
1036
+
1037
+ </button>
1038
+ <button
1039
+ class="control-button"
1040
+ part="control-button"
1041
+ type="button"
1042
+ aria-label="Zoom in"
1043
+ title="Zoom in"
1044
+ @click=${this.zoomIn}
1045
+ >
1046
+ +
1047
+ </button>
1048
+ </div>
1049
+ `;
1050
+ }
1051
+ #O() {
1052
+ if (this._status === "ready") return l;
1053
+ let e = this._status === "loading", t = this._status === "empty", n = e ? "Charting dungeon" : t ? "Choose a dungeon" : "Map unavailable", r = e ? "Loading Fellowship data and preparing the tactical layer." : t ? "Set the dungeon-id attribute or dungeonId property to render a map." : this._error?.message ?? "An unexpected map error occurred.";
1054
+ return c`
1055
+ <div class="status" part="status" role=${this._status === "error" ? "alert" : "status"}>
1056
+ <div class="status-card">
1057
+ <div class="status-mark" aria-hidden="true"><span>${e ? "…" : "✦"}</span></div>
1058
+ <p class="status-title">${n}</p>
1059
+ <p class="status-copy">${r}</p>
1060
+ ${this._status === "error" ? c`<button class="retry-button" type="button" @click=${this.retry}>Retry</button>` : l}
1061
+ </div>
1062
+ </div>
1063
+ `;
1064
+ }
1065
+ #k(e) {
1066
+ return this._mapLabels.get(e.id) ?? "Map";
1067
+ }
1068
+ #A(e) {
1069
+ e.id !== this._selectedMap?.id && (this.mapImageId = e.id, this.dispatchEvent(new CustomEvent("fellowship-map-image-change", {
1070
+ ...I,
1071
+ detail: {
1072
+ mapImageId: e.id,
1073
+ mapImage: e
1074
+ }
1075
+ })));
1076
+ }
1077
+ #j;
1078
+ #M;
1079
+ #N;
1080
+ #P;
1081
+ #F;
1082
+ #I;
1083
+ #L(e) {
1084
+ return e.composedPath().some((e) => e instanceof HTMLElement && e.matches("button, a, input, select, textarea, [contenteditable='true']"));
1085
+ }
1086
+ #R() {
1087
+ let e = this._viewportElement, t = this._selectedMap;
1088
+ !e || !t || (this.#a.setDimensions(e.clientWidth, e.clientHeight, t.width, t.height), this.#z("resize"));
1089
+ }
1090
+ #z(e) {
1091
+ this._viewportState = this.#a.state, this.dispatchEvent(new CustomEvent("fellowship-map-viewport-change", {
1092
+ ...I,
1093
+ detail: {
1094
+ viewport: this.#a.state,
1095
+ source: e
1096
+ }
1097
+ }));
1098
+ }
1099
+ #B(e, t, n) {
1100
+ this.#u.has(e) || (this.#u.add(e), this.#d.push({
1101
+ code: t,
1102
+ message: n instanceof Error ? n.message : String(n),
1103
+ cause: n
1104
+ }));
1105
+ }
1106
+ #V() {
1107
+ if (this.#d.length === 0) return;
1108
+ let e = this.#d;
1109
+ this.#d = [];
1110
+ for (let t of e) this.dispatchEvent(new CustomEvent("fellowship-map-error", {
1111
+ ...I,
1112
+ detail: t
1113
+ }));
1114
+ }
1115
+ };
1116
+ r([d({
1117
+ type: String,
1118
+ attribute: "dungeon-id"
1119
+ })], L.prototype, "dungeonId", void 0), r([d({
1120
+ type: String,
1121
+ attribute: "map-image-id"
1122
+ })], L.prototype, "mapImageId", void 0), r([d({
1123
+ type: String,
1124
+ attribute: "asset-base-url"
1125
+ })], L.prototype, "assetBaseUrl", void 0), r([d({ attribute: !1 })], L.prototype, "dataSource", void 0), r([d({ attribute: !1 })], L.prototype, "renderEnemy", void 0), r([d({ attribute: !1 })], L.prototype, "renderGroup", void 0), r([d({ attribute: !1 })], L.prototype, "mapLabelFormatter", void 0), r([d({
1126
+ type: Boolean,
1127
+ attribute: "show-controls"
1128
+ })], L.prototype, "showControls", void 0), r([d({
1129
+ type: Boolean,
1130
+ attribute: "show-map-selector"
1131
+ })], L.prototype, "showMapSelector", void 0), r([d({
1132
+ type: Number,
1133
+ attribute: "max-zoom"
1134
+ })], L.prototype, "maxZoom", void 0), r([d({
1135
+ type: Boolean,
1136
+ attribute: !1
1137
+ })], L.prototype, "externalOverlay", void 0), r([p()], L.prototype, "_status", void 0), r([p()], L.prototype, "_error", void 0), r([p()], L.prototype, "_data", void 0), r([p()], L.prototype, "_model", void 0), r([p()], L.prototype, "_selectedMap", void 0), r([p()], L.prototype, "_viewportState", void 0), r([p()], L.prototype, "_isPanning", void 0), r([p()], L.prototype, "_enemyRenderContent", void 0), r([p()], L.prototype, "_groupRenderContent", void 0), r([p()], L.prototype, "_mapLabels", void 0), r([f(".viewport")], L.prototype, "_viewportElement", void 0), r([f("canvas")], L.prototype, "_canvasElement", void 0), L = r([u("fellowship-dungeon-map")], L);
1138
+ var R = () => {
1139
+ globalThis.customElements !== void 0 && !globalThis.customElements.get("fellowship-dungeon-map") && globalThis.customElements.define("fellowship-dungeon-map", L);
1140
+ };
1141
+ //#endregion
1142
+ export { N as a, M as i, R as n, y as o, F as r, L as t };
1143
+
1144
+ //# sourceMappingURL=dungeon-map-IPN6sEDj.js.map