dndkit 0.4.0__py3-none-any.whl
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.
- dndkit/__init__.py +10 -0
- dndkit/__main__.py +6 -0
- dndkit/activity.py +68 -0
- dndkit/app.py +105 -0
- dndkit/config.py +104 -0
- dndkit/errors.py +48 -0
- dndkit/game/__init__.py +1 -0
- dndkit/game/areas.py +181 -0
- dndkit/game/battlefield.py +154 -0
- dndkit/game/bestiary.py +155 -0
- dndkit/game/characters.py +227 -0
- dndkit/game/combat.py +209 -0
- dndkit/game/combatants.py +188 -0
- dndkit/game/concentration.py +43 -0
- dndkit/game/dice.py +186 -0
- dndkit/game/effects.py +183 -0
- dndkit/game/grid.py +88 -0
- dndkit/game/journal.py +47 -0
- dndkit/game/movement.py +264 -0
- dndkit/game/pathing.py +153 -0
- dndkit/game/proficiencies.py +104 -0
- dndkit/game/progression.py +77 -0
- dndkit/game/readiness.py +214 -0
- dndkit/game/reference.py +123 -0
- dndkit/game/registry.py +172 -0
- dndkit/game/resources.py +96 -0
- dndkit/game/rests.py +98 -0
- dndkit/game/rules.py +196 -0
- dndkit/game/seating.py +71 -0
- dndkit/game/sight.py +142 -0
- dndkit/game/spellbook.py +97 -0
- dndkit/game/tables.py +216 -0
- dndkit/game/world.py +125 -0
- dndkit/models/__init__.py +69 -0
- dndkit/models/base.py +26 -0
- dndkit/models/battle.py +40 -0
- dndkit/models/bestiary.py +56 -0
- dndkit/models/campaign.py +209 -0
- dndkit/models/character.py +149 -0
- dndkit/models/world.py +62 -0
- dndkit/skills/__init__.py +14 -0
- dndkit/skills/catalog.py +113 -0
- dndkit/skills/dnd-battle-map/SKILL.md +78 -0
- dndkit/skills/dnd-game-master/SKILL.md +125 -0
- dndkit/skills/dnd-monsters/SKILL.md +85 -0
- dndkit/skills/extension.py +93 -0
- dndkit/storage/__init__.py +6 -0
- dndkit/storage/catalog.py +72 -0
- dndkit/storage/confirmations.py +54 -0
- dndkit/storage/files.py +55 -0
- dndkit/storage/sharing.py +67 -0
- dndkit/storage/store.py +278 -0
- dndkit/text.py +13 -0
- dndkit/tools/__init__.py +47 -0
- dndkit/tools/apps.py +47 -0
- dndkit/tools/battle.py +117 -0
- dndkit/tools/bestiary.py +121 -0
- dndkit/tools/campaign.py +208 -0
- dndkit/tools/characters.py +189 -0
- dndkit/tools/combat.py +169 -0
- dndkit/tools/deps.py +42 -0
- dndkit/tools/journal.py +102 -0
- dndkit/tools/locations.py +98 -0
- dndkit/tools/npcs.py +101 -0
- dndkit/tools/play.py +118 -0
- dndkit/tools/players.py +44 -0
- dndkit/tools/progression.py +41 -0
- dndkit/tools/quests.py +95 -0
- dndkit/tools/recorder.py +63 -0
- dndkit/tools/render.py +281 -0
- dndkit/tools/render_combat.py +121 -0
- dndkit/tools/render_map.py +141 -0
- dndkit/tools/resources.py +104 -0
- dndkit/tools/schema.py +47 -0
- dndkit/tools/screen.py +47 -0
- dndkit/tools/spells.py +67 -0
- dndkit/tools/utility.py +42 -0
- dndkit/ui/__init__.py +52 -0
- dndkit/ui/backstage.js +41 -0
- dndkit/ui/battlemap.js +70 -0
- dndkit/ui/character.html +23 -0
- dndkit/ui/combat.html +18 -0
- dndkit/ui/components.css +283 -0
- dndkit/ui/components.js +261 -0
- dndkit/ui/desk.js +91 -0
- dndkit/ui/dice.html +42 -0
- dndkit/ui/dicetray.js +99 -0
- dndkit/ui/gm.html +59 -0
- dndkit/ui/party.html +18 -0
- dndkit/ui/player.html +61 -0
- dndkit/ui/seating.html +18 -0
- dndkit/ui/shared.css +195 -0
- dndkit/ui/shared.js +242 -0
- dndkit/ui/sheet.js +75 -0
- dndkit/ui/table.html +41 -0
- dndkit/ui/words.js +120 -0
- dndkit/ui/world.js +56 -0
- dndkit/views/__init__.py +7 -0
- dndkit/views/backstage.py +49 -0
- dndkit/views/battle.py +92 -0
- dndkit/views/desk.py +76 -0
- dndkit/views/everyone.py +194 -0
- dndkit/views/public.py +89 -0
- dndkit/views/widgets.py +58 -0
- dndkit/views/world.py +70 -0
- dndkit/web/__init__.py +10 -0
- dndkit/web/access.py +136 -0
- dndkit/web/audiences.py +111 -0
- dndkit/web/changes.py +84 -0
- dndkit/web/feed.py +112 -0
- dndkit/web/routes.py +134 -0
- dndkit/web/server.py +131 -0
- dndkit-0.4.0.dist-info/METADATA +567 -0
- dndkit-0.4.0.dist-info/RECORD +118 -0
- dndkit-0.4.0.dist-info/WHEEL +5 -0
- dndkit-0.4.0.dist-info/entry_points.txt +2 -0
- dndkit-0.4.0.dist-info/licenses/LICENSE +22 -0
- dndkit-0.4.0.dist-info/top_level.txt +1 -0
dndkit/ui/components.css
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/* Component looks, each scoped by its root class (combat, party, sheet,
|
|
2
|
+
dicebox, journal, join, backstage): a web page shows several at once.
|
|
3
|
+
Then the web pages' own layout, under body.web. */
|
|
4
|
+
|
|
5
|
+
[hidden] { display: none !important; }
|
|
6
|
+
.head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
|
7
|
+
.section { margin-top: 12px; }
|
|
8
|
+
.section h4 { margin: 0 0 6px; font: 600 11px var(--sans); color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
|
|
9
|
+
.list { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
10
|
+
.slot { margin-right: 10px; white-space: nowrap; }
|
|
11
|
+
|
|
12
|
+
/* Combat tracker */
|
|
13
|
+
.combat .head { margin-bottom: 8px; }
|
|
14
|
+
.combat .order { display: flex; flex-direction: column; gap: 6px; }
|
|
15
|
+
.combat .fighter {
|
|
16
|
+
display: grid; grid-template-columns: 34px 1fr minmax(110px, 38%); gap: 4px 10px; align-items: center;
|
|
17
|
+
padding: 7px 10px; border-radius: 10px; background: var(--card-2); border: 1px solid transparent;
|
|
18
|
+
transition: background .4s, border-color .4s, transform .3s;
|
|
19
|
+
}
|
|
20
|
+
.combat .fighter.pc { box-shadow: inset 3px 0 0 var(--gold); }
|
|
21
|
+
.combat .fighter.current { border-color: var(--gold); background: color-mix(in srgb, var(--gold) 14%, var(--card-2)); transform: scale(1.01); }
|
|
22
|
+
.combat .fighter.hurt { animation: hurt .7s; }
|
|
23
|
+
.combat .fighter.healed { animation: healed .7s; }
|
|
24
|
+
.combat .init {
|
|
25
|
+
grid-row: span 2; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
|
|
26
|
+
font: 700 13px var(--serif); background: var(--card); border: 1.5px solid var(--line); font-variant-numeric: tabular-nums;
|
|
27
|
+
}
|
|
28
|
+
.combat .current .init { border-color: var(--gold); color: var(--gold-hi); }
|
|
29
|
+
.combat .name { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
30
|
+
.combat .name .arrow { color: var(--gold-hi); margin-right: 4px; }
|
|
31
|
+
.combat .ac { font-size: 11px; color: var(--muted); margin-left: 6px; font-weight: 400; }
|
|
32
|
+
.combat .fighter .chips { grid-column: 2 / 4; }
|
|
33
|
+
.combat .down .name { color: var(--red); }
|
|
34
|
+
.combat .defeated { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
|
|
35
|
+
.combat .defeated:empty { display: none; }
|
|
36
|
+
.combat .defeated .gone { text-decoration: line-through; }
|
|
37
|
+
.combat .pos { font-size: 11px; color: var(--gold); margin-left: 6px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
38
|
+
.combat .map { margin: 2px 0 8px; }
|
|
39
|
+
|
|
40
|
+
/* Battle map */
|
|
41
|
+
.battlemap { display: block; width: 100%; height: auto; max-height: 70vh; }
|
|
42
|
+
.battlemap .floor { fill: var(--card-2); }
|
|
43
|
+
.battlemap .grid { stroke: var(--line); stroke-width: 1; fill: none; }
|
|
44
|
+
.battlemap .wall { fill: color-mix(in srgb, var(--text) 62%, var(--card)); }
|
|
45
|
+
.battlemap .rough { fill: url(#gm-rough); }
|
|
46
|
+
.battlemap .hatch { stroke: color-mix(in srgb, var(--amber) 60%, transparent); stroke-width: 1.4; }
|
|
47
|
+
.battlemap .reach { fill: color-mix(in srgb, var(--gold) 13%, transparent); }
|
|
48
|
+
.battlemap .label { fill: var(--muted); font: 600 11px var(--sans); text-anchor: middle; }
|
|
49
|
+
.battlemap .label.row { text-anchor: end; }
|
|
50
|
+
.battlemap .token circle { stroke: var(--card); stroke-width: 2.5; }
|
|
51
|
+
.battlemap .token text { font: 700 13px var(--sans); text-anchor: middle; pointer-events: none; }
|
|
52
|
+
.battlemap .token.party circle { fill: var(--gold); }
|
|
53
|
+
.battlemap .token.party text { fill: #1d1a22; }
|
|
54
|
+
.battlemap .token.foe circle { fill: var(--red); }
|
|
55
|
+
.battlemap .token.foe text { fill: #fff; }
|
|
56
|
+
.battlemap .token.foe.hurt circle { fill: color-mix(in srgb, var(--red) 80%, #000); }
|
|
57
|
+
.battlemap .token.foe.bloodied circle { fill: color-mix(in srgb, var(--red) 60%, #000); }
|
|
58
|
+
.battlemap .token.current circle { stroke: var(--gold-hi); stroke-width: 4; animation: turn 1.4s ease-in-out infinite; }
|
|
59
|
+
.battlemap .token.me circle { stroke: var(--text); stroke-width: 3; stroke-dasharray: 5 3; }
|
|
60
|
+
.battlemap .token.down { opacity: .35; }
|
|
61
|
+
.battlemap .token.hidden { opacity: .5; }
|
|
62
|
+
.battlemap .token.boss circle { stroke: var(--gold-hi); stroke-width: 3.5; }
|
|
63
|
+
.battlemap .token text.crown { fill: var(--gold-hi); font-size: 16px; paint-order: stroke; stroke: #1d1a22; stroke-width: 2px; }
|
|
64
|
+
.combat .crown { color: var(--gold-hi); margin-right: 4px; }
|
|
65
|
+
.combat .legendary { grid-column: 2 / 4; display: flex; flex-wrap: wrap; gap: 4px 14px; }
|
|
66
|
+
.combat .legendary:empty { display: none; }
|
|
67
|
+
.battlemap .token.hidden circle { stroke-dasharray: 3 3; stroke: var(--text); }
|
|
68
|
+
.battlemap .area { fill: color-mix(in srgb, #ff7a2f 32%, transparent); stroke: #ff7a2f; stroke-width: .5; }
|
|
69
|
+
.battlemap .life { fill: rgba(0,0,0,.35); }
|
|
70
|
+
.battlemap .life-left { fill: var(--green); }
|
|
71
|
+
.battlemap .life-left.mid { fill: var(--amber); }
|
|
72
|
+
.battlemap .life-left.low { fill: #fff; }
|
|
73
|
+
@keyframes turn { 50% { stroke-opacity: .35; } }
|
|
74
|
+
@media (prefers-reduced-motion: reduce) { .battlemap .token.current circle { animation: none; } }
|
|
75
|
+
|
|
76
|
+
.health { padding: 1px 8px; border-radius: 999px; font-size: 11px; }
|
|
77
|
+
.health.unhurt { background: color-mix(in srgb, var(--green) 18%, transparent); color: var(--green); }
|
|
78
|
+
.health.hurt { background: color-mix(in srgb, var(--amber) 20%, transparent); color: var(--amber); }
|
|
79
|
+
.health.bloodied { background: color-mix(in srgb, var(--red) 22%, transparent); color: var(--red); }
|
|
80
|
+
.health.down { background: var(--card); color: var(--muted); }
|
|
81
|
+
@keyframes hurt { 30% { background: color-mix(in srgb, var(--red) 35%, var(--card-2)); transform: translateX(-3px); } }
|
|
82
|
+
@keyframes healed { 30% { background: color-mix(in srgb, var(--green) 30%, var(--card-2)); } }
|
|
83
|
+
|
|
84
|
+
/* Party board */
|
|
85
|
+
.party .facts { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 6px 0 10px; }
|
|
86
|
+
.party .facts span b { color: var(--text); font-weight: 600; }
|
|
87
|
+
.party .banner { margin-bottom: 10px; padding: 6px 10px; border-radius: 8px; color: var(--gold-hi);
|
|
88
|
+
background: color-mix(in srgb, var(--red) 18%, transparent); border: 1px solid color-mix(in srgb, var(--red) 45%, transparent); }
|
|
89
|
+
.party .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; }
|
|
90
|
+
.party .pc { background: var(--card-2); border-radius: 10px; padding: 8px 10px; display: flex; flex-direction: column; gap: 5px; }
|
|
91
|
+
.party .pc.down { box-shadow: inset 0 0 0 1px var(--red); }
|
|
92
|
+
.party .pc .top { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }
|
|
93
|
+
.party .pc .top b { font-family: var(--serif); font-size: 15px; }
|
|
94
|
+
.party .quest { display: grid; grid-template-columns: 1fr 90px; gap: 8px; align-items: center; padding: 3px 0; }
|
|
95
|
+
.party .quest .bar { height: 6px; background: var(--card-2); border-radius: 3px; overflow: hidden; }
|
|
96
|
+
.party .quest .bar i { display: block; height: 100%; background: var(--gold); }
|
|
97
|
+
|
|
98
|
+
.party .pc.away { opacity: .55; }
|
|
99
|
+
.party .chip.away { margin-left: 6px; }
|
|
100
|
+
|
|
101
|
+
/* The table: players seated around it */
|
|
102
|
+
.seating .round { position: relative; aspect-ratio: 16 / 9; min-height: 230px; max-height: 320px; margin: 6px 0 4px; }
|
|
103
|
+
.seating .round > .top {
|
|
104
|
+
position: absolute; inset: 22% 24%; border-radius: 50%; display: grid; place-items: center; text-align: center;
|
|
105
|
+
background: radial-gradient(ellipse at 40% 35%, color-mix(in srgb, var(--gold) 26%, var(--card-2)), var(--card-2) 70%);
|
|
106
|
+
border: 2px solid color-mix(in srgb, var(--gold) 45%, var(--line)); box-shadow: inset 0 0 18px rgba(0,0,0,.18);
|
|
107
|
+
font: italic 13px var(--serif); color: var(--muted); padding: 0 10%;
|
|
108
|
+
}
|
|
109
|
+
.seating .seat {
|
|
110
|
+
position: absolute; transform: translate(-50%, -50%); max-width: 120px; min-width: 76px; box-sizing: border-box;
|
|
111
|
+
display: flex; flex-direction: column; align-items: center; gap: 1px; padding: 5px 10px; text-align: center;
|
|
112
|
+
background: var(--card); border: 1.5px solid var(--gold); border-radius: 12px; box-shadow: 0 2px 6px rgba(0,0,0,.18);
|
|
113
|
+
}
|
|
114
|
+
.seating .seat b { font-size: 13px; overflow: hidden; text-overflow: ellipsis; max-width: 100%; white-space: nowrap; }
|
|
115
|
+
.seating .seat .plays { font: 600 13px var(--serif); color: var(--gold-hi); }
|
|
116
|
+
.seating .seat.new { border-style: dashed; border-color: var(--muted); }
|
|
117
|
+
.seating .round.list { aspect-ratio: auto; min-height: 0; display: flex; flex-wrap: wrap; gap: 8px; }
|
|
118
|
+
.seating .round.list > .top { display: none; }
|
|
119
|
+
.seating .round.list .seat { position: static; transform: none; max-width: none; }
|
|
120
|
+
.seating .others { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
|
|
121
|
+
.seating .others:empty { display: none; }
|
|
122
|
+
.seating .row { flex-wrap: wrap; gap: 4px; }
|
|
123
|
+
|
|
124
|
+
/* Character sheet */
|
|
125
|
+
.sheet .vitals { margin-top: 8px; }
|
|
126
|
+
.sheet .vitals + .small { margin-top: 6px; }
|
|
127
|
+
.sheet .stats { display: flex; gap: 8px; margin: 10px 0; }
|
|
128
|
+
.sheet .stat { flex: 1; background: var(--card-2); border-radius: 10px; padding: 6px 8px; text-align: center; }
|
|
129
|
+
.sheet .stat b { display: block; font: 700 18px var(--serif); }
|
|
130
|
+
.sheet .stat span { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
|
|
131
|
+
.sheet .abilities { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin: 10px 0; }
|
|
132
|
+
.sheet .ability { background: var(--card-2); border-radius: 10px; padding: 6px 2px; text-align: center; }
|
|
133
|
+
.sheet .ability .key { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
|
|
134
|
+
.sheet .ability .mod { font: 700 18px var(--serif); color: var(--gold-hi); }
|
|
135
|
+
.sheet .ability .score { font-size: 11px; color: var(--muted); }
|
|
136
|
+
.sheet .identity { margin-top: 2px; }
|
|
137
|
+
.sheet .stats { flex-wrap: wrap; }
|
|
138
|
+
.sheet .stat { min-width: 64px; }
|
|
139
|
+
.sheet .stats.casting .stat { background: color-mix(in srgb, var(--violet) 14%, var(--card-2)); }
|
|
140
|
+
.sheet .spells { display: flex; flex-direction: column; gap: 4px; }
|
|
141
|
+
.sheet .spell { background: var(--card-2); border-radius: 8px; padding: 4px 8px; }
|
|
142
|
+
.sheet .spell summary { cursor: pointer; display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
|
|
143
|
+
.sheet .spell p { margin: 4px 0 2px; }
|
|
144
|
+
.sheet .spell.prepared { box-shadow: inset 3px 0 0 var(--amber); }
|
|
145
|
+
.sheet .item { padding: 2px 0; }
|
|
146
|
+
.sheet .appearance { font-style: italic; }
|
|
147
|
+
.sheet .section p { margin: 0 0 6px; }
|
|
148
|
+
.sheet .xp { height: 5px; background: var(--card-2); border-radius: 3px; overflow: hidden; margin-top: 4px; }
|
|
149
|
+
.sheet .xp i { display: block; height: 100%; background: var(--gold); }
|
|
150
|
+
|
|
151
|
+
/* Dice tray */
|
|
152
|
+
.dicebox .tray {
|
|
153
|
+
position: relative; overflow: hidden;
|
|
154
|
+
background: radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--gold) 10%, transparent), transparent 60%), var(--card);
|
|
155
|
+
}
|
|
156
|
+
.dicebox .dice { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; padding: 14px 0 6px; min-height: 64px; }
|
|
157
|
+
.dicebox .die { position: relative; width: 58px; height: 58px; animation: tumble 0.9s cubic-bezier(.2,.9,.3,1.2) both; }
|
|
158
|
+
.dicebox .die svg { width: 100%; height: 100%; filter: drop-shadow(0 3px 4px rgba(0,0,0,.45)); }
|
|
159
|
+
.dicebox .die .face {
|
|
160
|
+
position: absolute; inset: 0; display: grid; place-items: center;
|
|
161
|
+
font: 700 20px/1 var(--serif); color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.6); font-variant-numeric: tabular-nums;
|
|
162
|
+
}
|
|
163
|
+
.dicebox .die.d4 .face { padding-top: 12px; }
|
|
164
|
+
.dicebox .die .kind { position: absolute; left: 0; bottom: -14px; width: 100%; text-align: center; font-size: 10px; color: var(--muted); }
|
|
165
|
+
.dicebox .die.dropped { opacity: .35; transition: opacity .4s; }
|
|
166
|
+
.dicebox .die.dropped .face { text-decoration: line-through; }
|
|
167
|
+
.dicebox .die.crit svg { animation: glow 1.2s ease-in-out infinite alternate; }
|
|
168
|
+
.dicebox .die.fumble { animation: tumble .9s cubic-bezier(.2,.9,.3,1.2) both, shake .5s .95s; }
|
|
169
|
+
.dicebox .die.fumble svg { filter: drop-shadow(0 0 8px rgba(227,88,79,.9)); }
|
|
170
|
+
.dicebox .const { align-self: center; font: 600 18px var(--serif); color: var(--muted); }
|
|
171
|
+
.dicebox .total { text-align: center; padding-top: 14px; }
|
|
172
|
+
.dicebox .total b { display: block; font: 700 40px/1 var(--serif); color: var(--gold-hi); font-variant-numeric: tabular-nums; }
|
|
173
|
+
.dicebox .total.crit b { color: var(--gold-hi); text-shadow: 0 0 18px rgba(245,216,137,.8); }
|
|
174
|
+
.dicebox .total.fumble b { color: var(--red); }
|
|
175
|
+
.dicebox .history { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 8px; }
|
|
176
|
+
.dicebox .history:empty { display: none; }
|
|
177
|
+
@keyframes tumble {
|
|
178
|
+
0% { transform: translate(-60px, -40px) rotate(-280deg) scale(.4); opacity: 0; }
|
|
179
|
+
55% { transform: translate(0, 6px) rotate(18deg) scale(1.08); opacity: 1; }
|
|
180
|
+
75% { transform: translate(0, -5px) rotate(-8deg) scale(.97); }
|
|
181
|
+
100% { transform: none; }
|
|
182
|
+
}
|
|
183
|
+
@keyframes glow { from { filter: drop-shadow(0 0 4px rgba(245,216,137,.5)); } to { filter: drop-shadow(0 0 16px rgba(245,216,137,1)); } }
|
|
184
|
+
@keyframes shake { 20%, 60% { transform: translateX(-5px); } 40%, 80% { transform: translateX(5px); } }
|
|
185
|
+
@media (prefers-reduced-motion: reduce) { .dicebox .die, .dicebox .die.fumble, .dicebox .die.crit svg { animation: none; } }
|
|
186
|
+
|
|
187
|
+
/* Journal */
|
|
188
|
+
.journal .section:first-child { margin-top: 0; }
|
|
189
|
+
.journal .questlog { padding: 4px 0 6px; }
|
|
190
|
+
.journal .questlog ul { margin: 4px 0 0; padding-left: 20px; }
|
|
191
|
+
.journal .questlog li.done { color: var(--muted); text-decoration: line-through; }
|
|
192
|
+
.journal .event { padding: 4px 0 6px; border-top: 1px solid var(--line); }
|
|
193
|
+
.journal .event:first-child { border-top: 0; }
|
|
194
|
+
|
|
195
|
+
/* Join codes */
|
|
196
|
+
.join .codes { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; margin-top: 10px; }
|
|
197
|
+
.join .code { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px; border-radius: 10px;
|
|
198
|
+
background: var(--card-2); color: var(--text); text-decoration: none; text-align: center; }
|
|
199
|
+
.join .code img { width: 100%; max-width: 150px; border-radius: 6px; image-rendering: pixelated; }
|
|
200
|
+
|
|
201
|
+
/* Backstage */
|
|
202
|
+
.backstage .entry { padding: 5px 0 7px; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 2px; }
|
|
203
|
+
.backstage .section > div > .entry:first-child { border-top: 0; }
|
|
204
|
+
.backstage .row { flex-wrap: wrap; gap: 6px; }
|
|
205
|
+
.backstage .note { color: var(--muted); }
|
|
206
|
+
.backstage .note b { color: var(--text); font-weight: 600; }
|
|
207
|
+
.backstage .chip.friendly, .backstage .chip.easy { color: var(--green); }
|
|
208
|
+
.backstage .chip.hostile, .backstage .chip.deadly { color: var(--red); }
|
|
209
|
+
.backstage .chip.hard { color: var(--amber); }
|
|
210
|
+
|
|
211
|
+
/* Campaign heading (web pages) */
|
|
212
|
+
.campaign { margin: 2px 2px 14px; }
|
|
213
|
+
.campaign h1 { margin: 0; font: 600 30px/1.15 var(--serif); color: var(--gold); letter-spacing: .01em; }
|
|
214
|
+
.campaign .pitch {
|
|
215
|
+
margin: 6px 0 8px; max-width: 75ch; font: italic 15px/1.45 var(--serif); color: var(--text); opacity: .85;
|
|
216
|
+
white-space: pre-line; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
.campaign .pitch.open { -webkit-line-clamp: unset; }
|
|
219
|
+
.campaign .facts { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 13px; }
|
|
220
|
+
.campaign .facts b { color: var(--text); font-weight: 600; }
|
|
221
|
+
|
|
222
|
+
.backstage .objectives { margin: 2px 0; padding-left: 20px; }
|
|
223
|
+
.backstage .objectives li.done { color: var(--muted); text-decoration: line-through; }
|
|
224
|
+
|
|
225
|
+
/* The world, on the backstage */
|
|
226
|
+
.worldgrid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); margin-top: 12px; align-items: start; }
|
|
227
|
+
.world .entry { border-top: 1px solid var(--line); padding: 5px 0; }
|
|
228
|
+
.world .entry:first-of-type { border-top: 0; }
|
|
229
|
+
.world summary { cursor: pointer; display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
|
|
230
|
+
.world details[open] summary { margin-bottom: 4px; }
|
|
231
|
+
.world p { margin: 4px 0; }
|
|
232
|
+
.world .actions { white-space: pre-line; }
|
|
233
|
+
.world .note { color: var(--muted); }
|
|
234
|
+
.world .note b, .world .small b { color: var(--text); font-weight: 600; }
|
|
235
|
+
.world .chip.here { color: var(--gold-hi); }
|
|
236
|
+
.world .chip.friendly { color: var(--green); }
|
|
237
|
+
.world .chip.hostile { color: var(--red); }
|
|
238
|
+
.world .crown { color: var(--gold-hi); }
|
|
239
|
+
|
|
240
|
+
.combat .lastfight { margin-top: 8px; padding: 6px 8px; border-radius: 8px; background: var(--card-2); }
|
|
241
|
+
.journal .stars { color: var(--gold); font-size: 11px; }
|
|
242
|
+
.backstage .entry.done { opacity: .6; }
|
|
243
|
+
|
|
244
|
+
/* The desk, on the backstage */
|
|
245
|
+
.activity .calls { display: flex; flex-direction: column; max-height: 62vh; overflow-y: auto; }
|
|
246
|
+
.activity .call { border-top: 1px solid var(--line); padding: 3px 0; }
|
|
247
|
+
.activity .call:first-child { border-top: 0; }
|
|
248
|
+
.activity summary { cursor: pointer; display: grid; grid-template-columns: auto auto 1fr auto; gap: 8px; align-items: baseline; font-size: 12px; }
|
|
249
|
+
.activity .when, .activity .ms { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 11px; }
|
|
250
|
+
.activity .tool { font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
|
|
251
|
+
.activity .gist { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
252
|
+
.activity .call.failed .tool, .activity .call.failed .gist { color: var(--red); }
|
|
253
|
+
.activity .call.skill .tool { color: var(--violet); }
|
|
254
|
+
.activity pre { margin: 4px 0 2px; padding: 6px 8px; border-radius: 6px; background: var(--card-2); font: 11px/1.4 ui-monospace, Menlo, monospace;
|
|
255
|
+
white-space: pre-wrap; word-break: break-word; }
|
|
256
|
+
.activity pre.args { color: var(--muted); }
|
|
257
|
+
.notebook .prose { white-space: pre-line; margin: 0 0 4px; }
|
|
258
|
+
.notebook .session summary { cursor: pointer; font-size: 13px; }
|
|
259
|
+
.notebook .server { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 6px; word-break: break-all; }
|
|
260
|
+
.sheets { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); margin-top: 12px; align-items: start; }
|
|
261
|
+
.sheetbox .note { color: var(--muted); padding: 6px 4px 0; }
|
|
262
|
+
.sheetbox .note b { color: var(--text); }
|
|
263
|
+
|
|
264
|
+
/* Web pages */
|
|
265
|
+
body.web { background: var(--page); min-height: 100vh; box-sizing: border-box; padding: 16px; font-size: 14px; }
|
|
266
|
+
.toggle {
|
|
267
|
+
font: inherit; font-size: 12px; color: var(--muted); background: var(--card-2); border: 1px solid var(--line);
|
|
268
|
+
border-radius: 999px; padding: 2px 10px; cursor: pointer;
|
|
269
|
+
}
|
|
270
|
+
.board { display: grid; gap: 12px; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); align-items: start; }
|
|
271
|
+
.board.three { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.2fr) minmax(0, 1fr); }
|
|
272
|
+
.board .col { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
|
273
|
+
.yourturn {
|
|
274
|
+
padding: 12px; border-radius: var(--radius); text-align: center; font: 700 22px var(--serif); color: #1d1a22;
|
|
275
|
+
background: linear-gradient(90deg, var(--gold), var(--gold-hi)); animation: pulse 1.6s ease-in-out infinite;
|
|
276
|
+
}
|
|
277
|
+
@keyframes pulse { 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--gold) 30%, transparent); } }
|
|
278
|
+
@media (max-width: 1100px) { .board.three { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
|
|
279
|
+
@media (max-width: 760px) {
|
|
280
|
+
body.web { padding: 10px 16px; }
|
|
281
|
+
.campaign h1 { font-size: 24px; }
|
|
282
|
+
.board, .board.three { grid-template-columns: minmax(0, 1fr); }
|
|
283
|
+
}
|
dndkit/ui/components.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/* dndkit views: one renderer per kind of game data, shared by the
|
|
2
|
+
* MCP widgets (one view each) and the web pages (several at once).
|
|
3
|
+
* Each factory returns { el, render(data) } and keeps what it needs between
|
|
4
|
+
* updates: the last hit points (to flash a hit or a heal), the last roll. */
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { GM } from "./shared.js";
|
|
8
|
+
import { DiceTray } from "./dicetray.js";
|
|
9
|
+
import { BattleMap } from "./battlemap.js";
|
|
10
|
+
import { Backstage } from "./backstage.js";
|
|
11
|
+
import { World } from "./world.js";
|
|
12
|
+
import { Activity, Notebook, Sheets } from "./desk.js";
|
|
13
|
+
import { CharacterSheet } from "./sheet.js";
|
|
14
|
+
|
|
15
|
+
export const Views = (() => {
|
|
16
|
+
const { h, fill, t } = GM;
|
|
17
|
+
const signed = (n) => (n >= 0 ? `+${n}` : `${n}`);
|
|
18
|
+
const section = (title, ...content) => h("div", { class: "section" }, h("h4", {}, title), ...content);
|
|
19
|
+
const empty = (key) => h("div", { class: "empty small" }, t(key));
|
|
20
|
+
const head = (...children) => h("div", { class: "head" }, ...children);
|
|
21
|
+
const HEALTH = { down: 0, bloodied: 1, hurt: 2, unhurt: 3 };
|
|
22
|
+
|
|
23
|
+
const deathSaves = (c) => h("div", { class: "small" }, `${t("deathSaves")} `,
|
|
24
|
+
GM.pips(c.death_saves[0], 3, "good"), " ", GM.pips(c.death_saves[1], 3, "bad"));
|
|
25
|
+
const slots = (c) => c.slots.map(([level, left, max]) =>
|
|
26
|
+
h("span", { class: "slot" }, `${t("lvl")} ${level} `, GM.pips(left, max)));
|
|
27
|
+
|
|
28
|
+
/** The battle map, if any, and the initiative order with hit points; for the players,
|
|
29
|
+
* how hurt a monster looks. *me*: the name of the player's own token, to ring it. */
|
|
30
|
+
function combat({ me = () => null } = {}) {
|
|
31
|
+
const title = h("span", { class: "title" }, "⚔");
|
|
32
|
+
const turn = h("span", { class: "muted small" });
|
|
33
|
+
const map = h("div", { class: "map" });
|
|
34
|
+
const order = h("div", { class: "order" });
|
|
35
|
+
const defeated = h("div", { class: "defeated" });
|
|
36
|
+
const el = h("div", { class: "card combat" }, head(title, turn), map, order, defeated);
|
|
37
|
+
const last = new Map();
|
|
38
|
+
|
|
39
|
+
function vitals(f) {
|
|
40
|
+
if (f.health === undefined) return GM.hpBar(f.hp, f.hp_max, f.temp);
|
|
41
|
+
return h("div", {}, f.health ? h("span", { class: `health ${f.health}` }, t(f.health)) : null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fighter(f) {
|
|
45
|
+
const value = f.health === undefined ? f.hp : HEALTH[f.health];
|
|
46
|
+
const before = last.get(f.name);
|
|
47
|
+
last.set(f.name, value);
|
|
48
|
+
const known = before !== undefined && before !== null && value !== undefined && value !== null;
|
|
49
|
+
const change = !known ? "" : value < before ? " hurt" : value > before ? " healed" : "";
|
|
50
|
+
const down = f.hp === 0 || f.health === "down";
|
|
51
|
+
const classes = "fighter" + (f.pc ? " pc" : "") + (f.current ? " current" : "") + (down ? " down" : "") + change;
|
|
52
|
+
return h("div", { class: classes },
|
|
53
|
+
h("div", { class: "init" }, f.initiative),
|
|
54
|
+
h("div", { class: "name" }, f.current ? h("span", { class: "arrow" }, "▶") : null,
|
|
55
|
+
f.boss ? h("span", { class: "crown", title: t("boss") }, "♛") : null, f.name,
|
|
56
|
+
f.ac !== null && f.ac !== undefined ? h("span", { class: "ac" }, `${t("ac")} ${f.ac}`) : null,
|
|
57
|
+
f.position ? h("span", { class: "pos" }, f.position) : null,
|
|
58
|
+
f.hidden ? h("span", { class: "chip away" }, t("hidden")) : null,
|
|
59
|
+
f.reaction_used ? h("span", { class: "chip", title: t("reactionUsed") }, "↺") : null),
|
|
60
|
+
vitals(f),
|
|
61
|
+
GM.conditions(f),
|
|
62
|
+
legendary(f));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The fight that ended last, when none is running (the game master's views send it). */
|
|
66
|
+
const lastFight = (last) => h("div", { class: "lastfight small" },
|
|
67
|
+
h("b", {}, t("lastFight", { rounds: last.rounds })), " ", last.fighters.join(", "),
|
|
68
|
+
last.defeated.length ? h("div", { class: "muted" }, `${t("defeated")}: ${last.defeated.join(", ")} · ${last.xp} ${t("xp")}`) : null);
|
|
69
|
+
|
|
70
|
+
/** The game master's count of legendary actions and resistances. */
|
|
71
|
+
function legendary(f) {
|
|
72
|
+
if (!f.legendary || !(f.legendary[1] || f.resistances)) return null;
|
|
73
|
+
return h("div", { class: "legendary small muted" },
|
|
74
|
+
f.legendary[1] ? h("span", {}, `${t("legendaryActions")} `, GM.pips(f.legendary[0], f.legendary[1])) : null,
|
|
75
|
+
f.resistances ? h("span", {}, `${t("legendaryResistances")} `, GM.pips(f.resistances, f.resistances)) : null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function render(data) {
|
|
79
|
+
const current = data.order.find((f) => f.current);
|
|
80
|
+
if (!data.in_combat) last.clear(); // the next fight's "Goblin 1" is another goblin
|
|
81
|
+
title.textContent = data.in_combat ? `⚔ ${t("round")} ${data.round}` : "⚔";
|
|
82
|
+
turn.textContent = current ? t("turnOf", { name: current.name }) : "";
|
|
83
|
+
fill(map, data.map ? BattleMap(data.map, me()) : null);
|
|
84
|
+
map.hidden = !data.map;
|
|
85
|
+
fill(order, data.order.length ? data.order.map(fighter) : h("div", { class: "empty" }, t("noCombat")),
|
|
86
|
+
data.last ? lastFight(data.last) : null);
|
|
87
|
+
fill(defeated, data.defeated.length ? [
|
|
88
|
+
h("span", { class: "muted small" }, `${t("defeated")}:`),
|
|
89
|
+
data.defeated.map((name) => h("span", { class: "chip gone" }, name)),
|
|
90
|
+
data.xp ? h("span", { class: "chip warn" }, `${data.xp} ${t("xp")}`) : null,
|
|
91
|
+
] : null);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { el, render };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A web page's heading: the campaign's name and pitch, the session, where and when. */
|
|
98
|
+
function campaign() {
|
|
99
|
+
const name = h("h1", {});
|
|
100
|
+
const pitch = h("p", { class: "pitch" });
|
|
101
|
+
const facts = h("div", { class: "facts muted" });
|
|
102
|
+
const el = h("header", { class: "campaign" }, name, pitch, facts);
|
|
103
|
+
pitch.addEventListener("click", () => pitch.classList.toggle("open"));
|
|
104
|
+
|
|
105
|
+
function render(data) {
|
|
106
|
+
name.textContent = data.campaign;
|
|
107
|
+
pitch.textContent = data.description || "";
|
|
108
|
+
pitch.hidden = !data.description;
|
|
109
|
+
fill(facts, [[t("session"), data.session], ["📍", data.location], ["📅", data.date]]
|
|
110
|
+
.filter(([, value]) => value)
|
|
111
|
+
.map(([label, value]) => h("span", {}, `${label} `, h("b", {}, value))));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { el, render };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Each character's vitals. *campaign*: under the campaign's name, with where and
|
|
118
|
+
* when (a widget), else under "Party" (a web page, whose heading says the rest).
|
|
119
|
+
* *quests* adds their progress. */
|
|
120
|
+
function party({ quests = true, campaign = true } = {}) {
|
|
121
|
+
const name = h("span", { class: "title" }, "🛡");
|
|
122
|
+
const session = h("span", { class: "muted small" });
|
|
123
|
+
const facts = h("div", { class: "facts muted small" });
|
|
124
|
+
const banner = h("div");
|
|
125
|
+
const grid = h("div", { class: "grid" });
|
|
126
|
+
const questList = h("div");
|
|
127
|
+
const el = h("div", { class: "card party" }, head(name, session), facts, banner, grid,
|
|
128
|
+
quests ? section(t("quests"), questList) : null);
|
|
129
|
+
|
|
130
|
+
const member = (c) => h("div", { class: "pc" + (c.hp === 0 ? " down" : "") + (c.present === false ? " away" : "") },
|
|
131
|
+
h("div", { class: "top" }, h("b", {}, c.name), h("span", { class: "muted small" }, `${t("ac")} ${c.ac}`)),
|
|
132
|
+
h("div", { class: "muted small" }, c.summary + (c.player ? ` — ${c.player}` : ""),
|
|
133
|
+
c.present === false ? h("span", { class: "chip away" }, t(c.player ? "away" : "unplayed")) : null),
|
|
134
|
+
GM.hpBar(c.hp, c.hp_max, c.temp),
|
|
135
|
+
c.hp === 0 ? deathSaves(c) : null,
|
|
136
|
+
c.slots.length ? h("div", { class: "small muted" }, slots(c)) : null,
|
|
137
|
+
GM.conditions(c));
|
|
138
|
+
|
|
139
|
+
const progress = (q) => h("div", { class: "quest" }, h("span", {}, q.title),
|
|
140
|
+
h("div", { class: "bar", title: `${q.done}/${q.total}` },
|
|
141
|
+
h("i", { style: `width:${(q.total ? q.done / q.total : 0) * 100}%` })));
|
|
142
|
+
|
|
143
|
+
function render(data) {
|
|
144
|
+
name.textContent = campaign ? data.campaign : `🛡 ${t("party")}`;
|
|
145
|
+
session.textContent = campaign ? `${t("session")} ${data.session}` : "";
|
|
146
|
+
const place = campaign ? [["📍", data.location], ["📅", data.date]] : [];
|
|
147
|
+
fill(facts, [...place, [t("funds"), data.funds]]
|
|
148
|
+
.filter(([, value]) => value)
|
|
149
|
+
.map(([label, value]) => h("span", {}, `${label} `, h("b", {}, value))));
|
|
150
|
+
fill(banner, data.in_combat
|
|
151
|
+
? h("div", { class: "banner" }, `⚔ ${t("inCombat")} — ${t("round")} ${data.round}` +
|
|
152
|
+
(data.current ? ` — ${t("turnOf", { name: data.current })}` : ""))
|
|
153
|
+
: null);
|
|
154
|
+
fill(grid, data.characters.length ? data.characters.map(member) : h("div", { class: "empty" }, t("noParty")));
|
|
155
|
+
fill(questList, data.quests.length ? data.quests.map(progress) : empty("noQuest"));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { el, render };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Who sits at the table: the players around it with their characters, then who is
|
|
162
|
+
* missing. A narrow screen or a big table lists them instead. */
|
|
163
|
+
function seating() {
|
|
164
|
+
const session = h("span", { class: "muted small" });
|
|
165
|
+
const round = h("div", { class: "round" });
|
|
166
|
+
const others = h("div", { class: "others" });
|
|
167
|
+
const el = h("div", { class: "card seating" },
|
|
168
|
+
head(h("span", { class: "title" }, t("theTable")), session), round, others);
|
|
169
|
+
let seats = 0;
|
|
170
|
+
const layout = () => round.classList.toggle("list", seats > 8 || el.clientWidth < 440);
|
|
171
|
+
// Deferred: toggling the layout inside the observer would resize it again in the
|
|
172
|
+
// same frame, and roomkit shows the browser's loop warning under the widget.
|
|
173
|
+
new ResizeObserver(() => requestAnimationFrame(layout)).observe(el);
|
|
174
|
+
|
|
175
|
+
function seat(s, index) {
|
|
176
|
+
const angle = (index / seats) * 2 * Math.PI - Math.PI / 2; // clockwise from the top
|
|
177
|
+
const style = `left:${50 + 37 * Math.cos(angle)}%;top:${50 + 38 * Math.sin(angle)}%`;
|
|
178
|
+
return h("div", { class: "seat" + (s.characters.length ? "" : " new"), style },
|
|
179
|
+
h("b", {}, s.player),
|
|
180
|
+
s.characters.length
|
|
181
|
+
? s.characters.map((c) => h("span", { class: "plays", title: c.summary }, c.name))
|
|
182
|
+
: h("span", { class: "muted small" }, t("noCharacter")));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const chips = (label, items) => (items.length
|
|
186
|
+
? h("div", { class: "row small" }, h("span", { class: "muted" }, `${t(label)}:`), items.map((text) => h("span", { class: "chip" }, text)))
|
|
187
|
+
: null);
|
|
188
|
+
|
|
189
|
+
function render(data) {
|
|
190
|
+
seats = data.seats.length;
|
|
191
|
+
session.textContent = `${t("session")} ${data.session}`;
|
|
192
|
+
fill(round, seats ? [h("div", { class: "top" }, h("span", {}, data.campaign)), data.seats.map(seat)] : null);
|
|
193
|
+
round.hidden = !seats;
|
|
194
|
+
layout();
|
|
195
|
+
fill(others,
|
|
196
|
+
data.called ? null : h("div", { class: "empty small" }, t("nobodySeated")),
|
|
197
|
+
chips(data.called ? "absentees" : "knownPlayers", data.absent.map((a) => `${a.player} (${a.name})`)),
|
|
198
|
+
chips("noPlayer", data.unplayed));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { el, render };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Active quests and their objectives, and the latest moments of the adventure log. */
|
|
205
|
+
function journal() {
|
|
206
|
+
const quests = h("div", { class: "quests" });
|
|
207
|
+
const events = h("div", { class: "events" });
|
|
208
|
+
const el = h("div", { class: "card journal" }, section(t("quests"), quests), section(t("journal"), events));
|
|
209
|
+
|
|
210
|
+
const quest = (q) => h("div", { class: "questlog" },
|
|
211
|
+
h("div", {}, h("b", {}, q.title), q.giver ? h("span", { class: "muted small" }, ` — ${t("from", { name: q.giver })}`) : null),
|
|
212
|
+
q.objectives.length ? h("ul", {}, q.objectives.map((o) => h("li", { class: o.done ? "done" : "" }, o.text))) : null);
|
|
213
|
+
|
|
214
|
+
// The game master's log adds the session, importance (1-5), who and tags.
|
|
215
|
+
const event = (e) => h("div", { class: "event" },
|
|
216
|
+
h("div", {}, h("span", { class: "chip" }, t(e.type)), " ", h("b", {}, e.title),
|
|
217
|
+
e.importance ? h("span", { class: "stars", title: `${e.importance}/5` }, " " + "★".repeat(e.importance)) : null,
|
|
218
|
+
e.location ? h("span", { class: "muted small" }, ` — ${e.location}`) : null),
|
|
219
|
+
e.description ? h("div", { class: "muted small" }, e.description) : null,
|
|
220
|
+
e.characters && (e.characters.length || e.tags.length || e.session) ? h("div", { class: "muted small" },
|
|
221
|
+
[e.session ? `${t("session")} ${e.session}` : "", e.characters.join(", "), e.tags.map((tag) => `#${tag}`).join(" ")]
|
|
222
|
+
.filter(Boolean).join(" · ")) : null);
|
|
223
|
+
|
|
224
|
+
function render(data) {
|
|
225
|
+
fill(quests, data.quests.length ? data.quests.map(quest) : empty("noQuest"));
|
|
226
|
+
fill(events, data.events.length ? data.events.map(event) : empty("noEvent"));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { el, render };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The players' links as QR codes, for the table screen; they fold away once scanned. */
|
|
233
|
+
function join() {
|
|
234
|
+
const hint = h("div", { class: "muted small" });
|
|
235
|
+
const codes = h("div", { class: "codes" });
|
|
236
|
+
const toggle = h("button", { type: "button", class: "toggle" });
|
|
237
|
+
const el = h("div", { class: "card join" }, head(h("span", { class: "title" }, t("players")), toggle), hint, codes);
|
|
238
|
+
const remembered = () => { try { return localStorage.getItem("gm-codes"); } catch (_) { return null; } };
|
|
239
|
+
|
|
240
|
+
function unfold(visible) {
|
|
241
|
+
hint.hidden = codes.hidden = !visible;
|
|
242
|
+
toggle.textContent = t(visible ? "hideCodes" : "showCodes");
|
|
243
|
+
try { localStorage.setItem("gm-codes", visible ? "shown" : "hidden"); } catch (_) { /* storage off */ }
|
|
244
|
+
}
|
|
245
|
+
toggle.addEventListener("click", () => unfold(codes.hidden));
|
|
246
|
+
unfold(remembered() !== "hidden");
|
|
247
|
+
|
|
248
|
+
function render(data) {
|
|
249
|
+
hint.textContent = t(data.reachable ? "scanToJoin" : "unreachable");
|
|
250
|
+
fill(codes, data.players.length ? data.players.map((p) =>
|
|
251
|
+
h("a", { class: "code", href: p.url, target: "_blank", rel: "noopener" },
|
|
252
|
+
h("img", { src: p.qr, alt: p.name }), h("b", {}, p.name),
|
|
253
|
+
p.player ? h("span", { class: "muted small" }, p.player) : null)) : empty("noParty"));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { el, render };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { campaign, combat, party, seating, character: CharacterSheet, journal, join, backstage: Backstage,
|
|
260
|
+
world: World, activity: Activity, notebook: Notebook, sheets: Sheets, dice: DiceTray };
|
|
261
|
+
})();
|
dndkit/ui/desk.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* The game master's desk, on the backstage: what the model called (to follow
|
|
2
|
+
* and debug it), the campaign's notebook, and every full sheet with its notes. */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
import { GM } from "./shared.js";
|
|
6
|
+
import { CharacterSheet } from "./sheet.js";
|
|
7
|
+
|
|
8
|
+
/** The model's calls, newest first: tool, arguments, answer or error, time taken. */
|
|
9
|
+
export function Activity() {
|
|
10
|
+
const { h, fill, t } = GM;
|
|
11
|
+
const count = h("span", { class: "muted small" });
|
|
12
|
+
const calls = h("div", { class: "calls" });
|
|
13
|
+
const el = h("div", { class: "card activity" },
|
|
14
|
+
h("div", { class: "head" }, h("span", { class: "title" }, t("activity")), count), calls);
|
|
15
|
+
const firstLine = (text) => (text || "").split("\n")[0];
|
|
16
|
+
|
|
17
|
+
const call = (c) => h("details", { class: "call" + (c.failed ? " failed" : "") + (c.kind === "skill" ? " skill" : "") },
|
|
18
|
+
h("summary", {},
|
|
19
|
+
h("span", { class: "when" }, c.time),
|
|
20
|
+
h("b", { class: "tool" }, c.name),
|
|
21
|
+
h("span", { class: "gist" }, firstLine(c.failed ? `⚠ ${c.outcome}` : c.outcome)),
|
|
22
|
+
h("span", { class: "ms" }, `${c.ms} ms`)),
|
|
23
|
+
c.arguments ? h("pre", { class: "args" }, c.arguments) : null,
|
|
24
|
+
c.outcome ? h("pre", { class: "outcome" }, c.outcome) : null);
|
|
25
|
+
|
|
26
|
+
function render(data) {
|
|
27
|
+
count.textContent = data.failures ? t("failures", { n: data.failures }) : "";
|
|
28
|
+
fill(calls, data.calls.length ? data.calls.map(call) : h("div", { class: "empty small" }, t("noCall")));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { el, render };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Situation, world notes, setting, past sessions, and where the campaign lives. */
|
|
35
|
+
export function Notebook() {
|
|
36
|
+
const { h, fill, t } = GM;
|
|
37
|
+
const section = (title, ...content) => h("div", { class: "section" }, h("h4", {}, title), ...content);
|
|
38
|
+
const body = h("div");
|
|
39
|
+
const el = h("div", { class: "card notebook" },
|
|
40
|
+
h("div", { class: "head" }, h("span", { class: "title" }, t("notebook"))), body);
|
|
41
|
+
const text = (value) => h("p", { class: "small prose" }, value);
|
|
42
|
+
|
|
43
|
+
function render(n) {
|
|
44
|
+
const server = n.server;
|
|
45
|
+
fill(body,
|
|
46
|
+
n.situation ? section(t("situation"), text(n.situation)) : null,
|
|
47
|
+
n.world_notes ? section(t("worldNotes"), text(n.world_notes)) : null,
|
|
48
|
+
n.setting ? section(t("setting"), h("details", {}, h("summary", { class: "small" }, t("show")), text(n.setting))) : null,
|
|
49
|
+
n.sessions.length ? section(t("sessions"), n.sessions.map((s) => h("details", { class: "session" },
|
|
50
|
+
h("summary", {}, h("b", {}, `${t("session")} ${s.number}`), s.title ? ` — ${s.title}` : ""),
|
|
51
|
+
text(s.summary),
|
|
52
|
+
s.moments.length ? h("ul", { class: "small" }, s.moments.map((m) => h("li", {}, m))) : null,
|
|
53
|
+
h("div", { class: "muted small" }, [s.present.join(", "), s.xp ? `${s.xp} ${t("xp")}` : "",
|
|
54
|
+
s.treasure.length ? `${t("treasure")}: ${s.treasure.join(", ")}` : ""].filter(Boolean).join(" · ")),
|
|
55
|
+
s.notes ? text(s.notes) : null))) : null,
|
|
56
|
+
!n.situation && !n.world_notes && !n.setting && !n.sessions.length
|
|
57
|
+
? h("div", { class: "empty small" }, t("noNotes")) : null,
|
|
58
|
+
h("div", { class: "server muted small" },
|
|
59
|
+
n.dm ? [`${t("dm")} ${n.dm}`, h("br")] : null,
|
|
60
|
+
`dndkit ${server.version} · ${server.file || "—"} · ${t("saved")} ${server.saved}`, h("br"),
|
|
61
|
+
`${server.data_dir} · ${t("skills")}: ${server.skills.join(", ") || "—"}`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { el, render };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Every character's full sheet, with the game master's notes under it. */
|
|
68
|
+
export function Sheets() {
|
|
69
|
+
const { h, fill, t } = GM;
|
|
70
|
+
const el = h("div", { class: "sheets" });
|
|
71
|
+
const shown = new Map();
|
|
72
|
+
|
|
73
|
+
function render(data) {
|
|
74
|
+
const cards = data.sheets.map((sheet) => {
|
|
75
|
+
if (!shown.has(sheet.name)) {
|
|
76
|
+
const view = CharacterSheet();
|
|
77
|
+
const notes = h("div", { class: "note small" });
|
|
78
|
+
shown.set(sheet.name, { view, notes, box: h("div", { class: "sheetbox" }, view.el, notes) });
|
|
79
|
+
}
|
|
80
|
+
const { view, notes, box } = shown.get(sheet.name);
|
|
81
|
+
view.render(sheet);
|
|
82
|
+
fill(notes, sheet.notes ? [h("b", {}, `${t("notes")} `), sheet.notes] : null);
|
|
83
|
+
return box;
|
|
84
|
+
});
|
|
85
|
+
const names = new Set(data.sheets.map((sheet) => sheet.name));
|
|
86
|
+
[...shown.keys()].filter((name) => !names.has(name)).forEach((name) => shown.delete(name));
|
|
87
|
+
fill(el, cards);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { el, render };
|
|
91
|
+
}
|