@carmaclouds/core 2.3.1
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/dist/cache/CacheManager.d.ts.map +1 -0
- package/dist/cache/CacheManager.js +131 -0
- package/dist/cache/CacheManager.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/index.d.ts +11 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +9 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/normalize.d.ts +10 -0
- package/dist/ir/normalize.d.ts.map +1 -0
- package/dist/ir/normalize.js +207 -0
- package/dist/ir/normalize.js.map +1 -0
- package/dist/ir/persistence.d.ts +26 -0
- package/dist/ir/persistence.d.ts.map +1 -0
- package/dist/ir/persistence.js +21 -0
- package/dist/ir/persistence.js.map +1 -0
- package/dist/ir/sync.d.ts +12 -0
- package/dist/ir/sync.d.ts.map +1 -0
- package/dist/ir/sync.js +36 -0
- package/dist/ir/sync.js.map +1 -0
- package/dist/ir/types.d.ts +143 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/ir/types.js +13 -0
- package/dist/ir/types.js.map +1 -0
- package/dist/ir/views/dnd5e.d.ts +40 -0
- package/dist/ir/views/dnd5e.d.ts.map +1 -0
- package/dist/ir/views/dnd5e.js +50 -0
- package/dist/ir/views/dnd5e.js.map +1 -0
- package/dist/render/character.d.ts +19 -0
- package/dist/render/character.d.ts.map +1 -0
- package/dist/render/character.js +156 -0
- package/dist/render/character.js.map +1 -0
- package/dist/render/h.d.ts +27 -0
- package/dist/render/h.d.ts.map +1 -0
- package/dist/render/h.js +64 -0
- package/dist/render/h.js.map +1 -0
- package/dist/render/index.d.ts +11 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/mount.d.ts +31 -0
- package/dist/render/mount.d.ts.map +1 -0
- package/dist/render/mount.js +63 -0
- package/dist/render/mount.js.map +1 -0
- package/dist/supabase/fields.d.ts.map +1 -0
- package/dist/supabase/fields.js +120 -0
- package/dist/supabase/fields.js.map +1 -0
- package/dist/types/character.d.ts.map +1 -0
- package/dist/types/character.js +5 -0
- package/dist/types/character.js.map +1 -0
- package/package.json +73 -0
- package/src/browser.js +51 -0
- package/src/cache/CacheManager.ts +174 -0
- package/src/common/browser-polyfill.js +319 -0
- package/src/common/debug.js +123 -0
- package/src/common/html-utils.js +134 -0
- package/src/common/theme-manager.js +265 -0
- package/src/index.ts +25 -0
- package/src/ir/__fixtures__/dnd5e-character.json +75962 -0
- package/src/ir/__fixtures__/non-dnd-character.json +14218 -0
- package/src/ir/index.ts +10 -0
- package/src/ir/normalize.ts +245 -0
- package/src/ir/persistence.ts +37 -0
- package/src/ir/sync.ts +49 -0
- package/src/ir/types.ts +161 -0
- package/src/ir/views/dnd5e.ts +94 -0
- package/src/lib/indexeddb-cache.js +320 -0
- package/src/modules/action-announcements.js +102 -0
- package/src/modules/action-display.js +1557 -0
- package/src/modules/action-executor.js +860 -0
- package/src/modules/action-filters.js +167 -0
- package/src/modules/action-options.js +117 -0
- package/src/modules/card-creator.js +142 -0
- package/src/modules/character-portrait.js +169 -0
- package/src/modules/character-trait-popups.js +959 -0
- package/src/modules/character-traits.js +814 -0
- package/src/modules/class-feature-edge-cases.js +1320 -0
- package/src/modules/color-utils.js +69 -0
- package/src/modules/combat-maneuver-edge-cases.js +660 -0
- package/src/modules/companions-manager.js +178 -0
- package/src/modules/concentration-tracker.js +178 -0
- package/src/modules/data-manager.js +514 -0
- package/src/modules/dice-roller.js +719 -0
- package/src/modules/effects-manager.js +743 -0
- package/src/modules/feature-modals.js +1264 -0
- package/src/modules/formula-resolver.js +444 -0
- package/src/modules/gm-mode.js +184 -0
- package/src/modules/health-modals.js +399 -0
- package/src/modules/hp-management.js +752 -0
- package/src/modules/inventory-manager.js +242 -0
- package/src/modules/macro-system.js +825 -0
- package/src/modules/notification-system.js +92 -0
- package/src/modules/racial-feature-edge-cases.js +746 -0
- package/src/modules/resource-manager.js +775 -0
- package/src/modules/sheet-builder.js +654 -0
- package/src/modules/spell-action-modals.js +583 -0
- package/src/modules/spell-cards.js +602 -0
- package/src/modules/spell-casting.js +723 -0
- package/src/modules/spell-display.js +314 -0
- package/src/modules/spell-edge-cases.js +509 -0
- package/src/modules/spell-macros.js +201 -0
- package/src/modules/spell-modals.js +1221 -0
- package/src/modules/spell-slots.js +224 -0
- package/src/modules/status-bar-bridge.js +101 -0
- package/src/modules/ui-utilities.js +284 -0
- package/src/modules/warlock-invocations.js +219 -0
- package/src/modules/window-management.js +211 -0
- package/src/render/character.ts +234 -0
- package/src/render/h.ts +74 -0
- package/src/render/index.ts +10 -0
- package/src/render/mount.ts +94 -0
- package/src/supabase/client.js +1383 -0
- package/src/supabase/config.js +60 -0
- package/src/supabase/fields.ts +129 -0
- package/src/types/character.ts +85 -0
package/src/ir/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-agnostic IR for DiceCloud characters. See REBUILD.md.
|
|
3
|
+
*/
|
|
4
|
+
export * from './types';
|
|
5
|
+
export { normalize } from './normalize';
|
|
6
|
+
export { deriveDnd, DND_ABILITIES } from './views/dnd5e';
|
|
7
|
+
export type { Dnd5eView, AbilityView, PoolView, HitDicePool } from './views/dnd5e';
|
|
8
|
+
export { IR_VERSION, toIRRow } from './persistence';
|
|
9
|
+
export type { IRRow } from './persistence';
|
|
10
|
+
export { upsertCharacterIR } from './sync';
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* normalize(raw) -> IRCharacter
|
|
3
|
+
*
|
|
4
|
+
* Pure transform from a raw DiceCloud API response into the system-agnostic IR.
|
|
5
|
+
* No D&D assumptions: every attribute is carried with its type/reset, every
|
|
6
|
+
* action/spell keeps its real uses + consumed resources.
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
IRAction,
|
|
10
|
+
IRAttribute,
|
|
11
|
+
IRCharacter,
|
|
12
|
+
IRConsumes,
|
|
13
|
+
IRDamage,
|
|
14
|
+
IRItem,
|
|
15
|
+
IRSkill,
|
|
16
|
+
RawDiceCloud,
|
|
17
|
+
ResetPeriod,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
const DND_ABILITIES = [
|
|
21
|
+
'strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Best-effort system hint (never load-bearing). PF2e also uses the six abilities,
|
|
26
|
+
* so we additionally require D&D-5e-specific signals: a single proficiencyBonus
|
|
27
|
+
* variable and a class-based hit-dice attribute.
|
|
28
|
+
*/
|
|
29
|
+
function detectSystem(byVar: Record<string, IRAttribute>): 'dnd5e' | 'generic' {
|
|
30
|
+
const hasAbilities = DND_ABILITIES.every((ab) => byVar[ab]);
|
|
31
|
+
const hasProfBonus = !!byVar['proficiencyBonus'];
|
|
32
|
+
const hasHitDice = Object.values(byVar).some((a) => a.type === 'hitDice');
|
|
33
|
+
return hasAbilities && hasProfBonus && hasHitDice ? 'dnd5e' : 'generic';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Coerce a DiceCloud value (number, calculation object, or string) to a number. */
|
|
37
|
+
function numOf(v: any): number {
|
|
38
|
+
if (v == null) return 0;
|
|
39
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
|
|
40
|
+
if (typeof v === 'object') return numOf(v.value ?? v.total ?? 0);
|
|
41
|
+
const n = Number(v);
|
|
42
|
+
return Number.isFinite(n) ? n : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True when a DiceCloud calculation/value is actually present (not just absent). */
|
|
46
|
+
function has(v: any): boolean {
|
|
47
|
+
return v != null && !(typeof v === 'object' && v.value == null && v.total == null);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Pull display text out of a DiceCloud description ({ text } | { value } | string). */
|
|
51
|
+
function textOf(d: any): string | undefined {
|
|
52
|
+
if (!d) return undefined;
|
|
53
|
+
if (typeof d === 'string') return d || undefined;
|
|
54
|
+
return d.text ?? d.value ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resetOf(p: any): ResetPeriod {
|
|
58
|
+
return p.reset ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether a property has been soft-deleted (truly gone). */
|
|
62
|
+
function isRemoved(p: any): boolean {
|
|
63
|
+
return !p || p.removed === true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a property is currently active. Deactivated properties (e.g. an
|
|
68
|
+
* unprepared spell, a toggled-off feature) are still imported - we carry this
|
|
69
|
+
* flag so adapters can grey them out rather than dropping data.
|
|
70
|
+
*/
|
|
71
|
+
function activeOf(p: any): boolean {
|
|
72
|
+
return !p.inactive && !p.deactivatedBySelf && !p.deactivatedByAncestor;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeAttribute(p: any): IRAttribute {
|
|
76
|
+
const value = numOf(p.value);
|
|
77
|
+
const total = numOf(p.total);
|
|
78
|
+
const damage = numOf(p.damage);
|
|
79
|
+
|
|
80
|
+
const attr: IRAttribute = {
|
|
81
|
+
id: p._id,
|
|
82
|
+
name: p.name ?? p.variableName ?? '',
|
|
83
|
+
variableName: p.variableName ?? '',
|
|
84
|
+
type: p.attributeType ?? 'stat',
|
|
85
|
+
value,
|
|
86
|
+
total,
|
|
87
|
+
damage,
|
|
88
|
+
reset: resetOf(p),
|
|
89
|
+
active: activeOf(p),
|
|
90
|
+
tags: Array.isArray(p.tags) ? p.tags : [],
|
|
91
|
+
description: textOf(p.description),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (p.attributeType === 'ability') {
|
|
95
|
+
attr.modifier = has(p.modifier) ? numOf(p.modifier) : Math.floor((value - 10) / 2);
|
|
96
|
+
}
|
|
97
|
+
if (p.attributeType === 'hitDice' && p.hitDiceSize) {
|
|
98
|
+
attr.hitDiceSize = String(p.hitDiceSize);
|
|
99
|
+
}
|
|
100
|
+
if (p.attributeType === 'spellSlot' && has(p.spellSlotLevel)) {
|
|
101
|
+
attr.spellSlotLevel = numOf(p.spellSlotLevel);
|
|
102
|
+
}
|
|
103
|
+
return attr;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeSkill(p: any): IRSkill {
|
|
107
|
+
return {
|
|
108
|
+
id: p._id,
|
|
109
|
+
name: p.name ?? p.variableName ?? '',
|
|
110
|
+
variableName: p.variableName ?? '',
|
|
111
|
+
skillType: p.skillType ?? 'skill',
|
|
112
|
+
value: numOf(p.value),
|
|
113
|
+
ability: p.ability || undefined,
|
|
114
|
+
proficiency: numOf(p.proficiency),
|
|
115
|
+
active: activeOf(p),
|
|
116
|
+
tags: Array.isArray(p.tags) ? p.tags : [],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeItem(p: any): IRItem {
|
|
121
|
+
return {
|
|
122
|
+
id: p._id,
|
|
123
|
+
name: p.name ?? '',
|
|
124
|
+
plural: p.plural || undefined,
|
|
125
|
+
quantity: p.quantity != null ? numOf(p.quantity) : 1,
|
|
126
|
+
equipped: !!p.equipped,
|
|
127
|
+
weight: has(p.weight) ? numOf(p.weight) : undefined,
|
|
128
|
+
value: has(p.value) ? numOf(p.value) : undefined,
|
|
129
|
+
description: textOf(p.description),
|
|
130
|
+
tags: Array.isArray(p.tags) ? p.tags : [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function consumesOf(p: any): IRConsumes[] {
|
|
135
|
+
const consumed = p.resources?.attributesConsumed;
|
|
136
|
+
if (!Array.isArray(consumed)) return [];
|
|
137
|
+
return consumed.map((c: any): IRConsumes => ({
|
|
138
|
+
variableName: c.variableName || undefined,
|
|
139
|
+
propertyId: c._id || c.variableId || undefined,
|
|
140
|
+
amount: numOf(c.quantity ?? c.amount ?? 1),
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeAction(p: any, damageByParent: Record<string, any[]>): IRAction {
|
|
145
|
+
const kind: IRAction['kind'] =
|
|
146
|
+
p.type === 'spell' ? 'spell' : p.type === 'feature' ? 'feature' : 'action';
|
|
147
|
+
|
|
148
|
+
const damage = (damageByParent[p._id] ?? [])
|
|
149
|
+
.map((d): IRDamage => ({
|
|
150
|
+
formula: d.amount?.calculation ?? String(d.amount?.value ?? ''),
|
|
151
|
+
type: d.damageType || undefined,
|
|
152
|
+
}))
|
|
153
|
+
.filter((d) => d.formula);
|
|
154
|
+
|
|
155
|
+
const action: IRAction = {
|
|
156
|
+
id: p._id,
|
|
157
|
+
name: p.name ?? '',
|
|
158
|
+
kind,
|
|
159
|
+
active: activeOf(p),
|
|
160
|
+
consumes: consumesOf(p),
|
|
161
|
+
damage,
|
|
162
|
+
tags: Array.isArray(p.tags) ? p.tags : [],
|
|
163
|
+
description: textOf(p.description),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const max = numOf(p.uses);
|
|
167
|
+
if (has(p.uses) && max > 0) {
|
|
168
|
+
const current = has(p.usesLeft) ? numOf(p.usesLeft) : Math.max(0, max - numOf(p.usesUsed));
|
|
169
|
+
action.uses = { current, max, reset: resetOf(p) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (has(p.attackRoll)) {
|
|
173
|
+
action.attack = { bonus: numOf(p.attackRoll) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (kind === 'spell') {
|
|
177
|
+
action.spell = {
|
|
178
|
+
level: numOf(p.level),
|
|
179
|
+
school: p.school || undefined,
|
|
180
|
+
castingTime: p.castingTime || undefined,
|
|
181
|
+
range: p.range || undefined,
|
|
182
|
+
duration: p.duration || undefined,
|
|
183
|
+
components: p.components || undefined,
|
|
184
|
+
concentration: p.components?.concentration ?? undefined,
|
|
185
|
+
ritual: p.components?.ritual ?? undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return action;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Whether a property should appear in `actions` (things that do something). */
|
|
193
|
+
function isActionLike(p: any): boolean {
|
|
194
|
+
if (p.type === 'action' || p.type === 'spell') return true;
|
|
195
|
+
// Include only features that have a limited-use pool (e.g. Channel Divinity).
|
|
196
|
+
if (p.type === 'feature') return has(p.uses) && numOf(p.uses) > 0;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function normalize(raw: RawDiceCloud): IRCharacter {
|
|
201
|
+
// Accept both the REST API shape ({ creatures[], creatureProperties[] }) and the
|
|
202
|
+
// extension's internal shape ({ creature, properties[] }).
|
|
203
|
+
const creature = raw?.creatures?.[0] ?? raw?.creature ?? {};
|
|
204
|
+
const allProps = raw?.creatureProperties ?? raw?.properties ?? [];
|
|
205
|
+
const props = allProps.filter((p) => !isRemoved(p));
|
|
206
|
+
|
|
207
|
+
const attributes = props
|
|
208
|
+
.filter((p) => p.type === 'attribute')
|
|
209
|
+
.map(normalizeAttribute);
|
|
210
|
+
|
|
211
|
+
const skills = props
|
|
212
|
+
.filter((p) => p.type === 'skill')
|
|
213
|
+
.map(normalizeSkill);
|
|
214
|
+
|
|
215
|
+
// Damage is stored as child `damage` properties pointing at their action/spell.
|
|
216
|
+
const damageByParent: Record<string, any[]> = {};
|
|
217
|
+
for (const p of props) {
|
|
218
|
+
if (p.type === 'damage' && p.parent?.id) {
|
|
219
|
+
(damageByParent[p.parent.id] ??= []).push(p);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const actions = props.filter(isActionLike).map((p) => normalizeAction(p, damageByParent));
|
|
224
|
+
|
|
225
|
+
const inventory = props
|
|
226
|
+
.filter((p) => p.type === 'item')
|
|
227
|
+
.map(normalizeItem);
|
|
228
|
+
|
|
229
|
+
const byVar: Record<string, IRAttribute> = {};
|
|
230
|
+
for (const a of attributes) {
|
|
231
|
+
if (a.variableName) byVar[a.variableName] = a;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
id: creature._id ?? '',
|
|
236
|
+
name: creature.name ?? '',
|
|
237
|
+
portrait: creature.picture || creature.avatarPicture || undefined,
|
|
238
|
+
systemHint: detectSystem(byVar),
|
|
239
|
+
attributes,
|
|
240
|
+
skills,
|
|
241
|
+
actions,
|
|
242
|
+
inventory,
|
|
243
|
+
byVar,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence shape for the clouds_character_ir table (see the matching SQL
|
|
3
|
+
* migration). The IR is already plain JSON, so this just frames the upsert row
|
|
4
|
+
* and stamps the schema version.
|
|
5
|
+
*/
|
|
6
|
+
import type { IRCharacter, RawDiceCloud } from './types';
|
|
7
|
+
|
|
8
|
+
/** Bump when the IR shape changes in a way consumers must migrate for. */
|
|
9
|
+
export const IR_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
export interface IRRow {
|
|
12
|
+
dicecloud_character_id: string;
|
|
13
|
+
character_name: string;
|
|
14
|
+
system_hint: string;
|
|
15
|
+
ir: IRCharacter;
|
|
16
|
+
ir_version: number;
|
|
17
|
+
raw?: RawDiceCloud;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the row to upsert into clouds_character_ir.
|
|
22
|
+
*
|
|
23
|
+
* supabase.from('clouds_character_ir')
|
|
24
|
+
* .upsert(toIRRow(normalize(raw), raw), { onConflict: 'dicecloud_character_id' })
|
|
25
|
+
*
|
|
26
|
+
* Pass `raw` to keep a re-normalizable snapshot; omit it to store IR only.
|
|
27
|
+
*/
|
|
28
|
+
export function toIRRow(ir: IRCharacter, raw?: RawDiceCloud): IRRow {
|
|
29
|
+
return {
|
|
30
|
+
dicecloud_character_id: ir.id,
|
|
31
|
+
character_name: ir.name,
|
|
32
|
+
system_hint: ir.systemHint,
|
|
33
|
+
ir,
|
|
34
|
+
ir_version: IR_VERSION,
|
|
35
|
+
...(raw ? { raw } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/ir/sync.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write the system-agnostic IR to Supabase. Called from each sync path alongside
|
|
3
|
+
* the existing clouds_characters write, so the rebuild's data flows into its own
|
|
4
|
+
* table without disturbing the legacy path. See REBUILD.md.
|
|
5
|
+
*
|
|
6
|
+
* Uses the Supabase REST API via fetch so it works everywhere the extension runs
|
|
7
|
+
* (background service worker, content scripts, popups) regardless of whether a
|
|
8
|
+
* supabase-js client is in scope.
|
|
9
|
+
*/
|
|
10
|
+
import { normalize } from './normalize';
|
|
11
|
+
import { toIRRow } from './persistence';
|
|
12
|
+
import type { IRCharacter, RawDiceCloud } from './types';
|
|
13
|
+
|
|
14
|
+
export interface SupabaseRestTarget {
|
|
15
|
+
url: string;
|
|
16
|
+
anonKey: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize raw DiceCloud data and upsert it into clouds_character_ir.
|
|
21
|
+
* Returns the IR. Throws on a real upsert error; callers should treat IR sync as
|
|
22
|
+
* non-fatal (wrap in try/catch) so it never blocks the legacy sync.
|
|
23
|
+
*/
|
|
24
|
+
export async function upsertCharacterIR(
|
|
25
|
+
raw: RawDiceCloud,
|
|
26
|
+
target: SupabaseRestTarget,
|
|
27
|
+
): Promise<IRCharacter> {
|
|
28
|
+
const ir = normalize(raw);
|
|
29
|
+
if (!ir.id) throw new Error('upsertCharacterIR: normalized IR has no character id');
|
|
30
|
+
|
|
31
|
+
const res = await fetch(
|
|
32
|
+
`${target.url}/rest/v1/clouds_character_ir?on_conflict=dicecloud_character_id`,
|
|
33
|
+
{
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
apikey: target.anonKey,
|
|
37
|
+
Authorization: `Bearer ${target.anonKey}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
Prefer: 'resolution=merge-duplicates,return=minimal',
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(toIRRow(ir)),
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(`clouds_character_ir upsert failed: ${res.status} ${await res.text()}`);
|
|
47
|
+
}
|
|
48
|
+
return ir;
|
|
49
|
+
}
|
package/src/ir/types.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-agnostic intermediate representation (IR) for a DiceCloud character.
|
|
3
|
+
*
|
|
4
|
+
* The IR mirrors DiceCloud's own generic stat engine instead of a fixed D&D 5e
|
|
5
|
+
* shape: every attribute is carried with its type and reset period, every action/
|
|
6
|
+
* spell references the resources it actually consumes. D&D conveniences (the six
|
|
7
|
+
* abilities, skills, spell-slot levels) are derived from this by `views/dnd5e`,
|
|
8
|
+
* never baked in here.
|
|
9
|
+
*
|
|
10
|
+
* See REBUILD.md for the design rationale.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** DiceCloud attributeType, kept open so unknown/custom systems still round-trip. */
|
|
14
|
+
export type AttributeType =
|
|
15
|
+
| 'ability'
|
|
16
|
+
| 'stat'
|
|
17
|
+
| 'modifier'
|
|
18
|
+
| 'hitDice'
|
|
19
|
+
| 'healthBar'
|
|
20
|
+
| 'resource'
|
|
21
|
+
| 'spellSlot'
|
|
22
|
+
| 'utility'
|
|
23
|
+
| (string & {});
|
|
24
|
+
|
|
25
|
+
/** How a use/charge pool refreshes. Open set; DiceCloud commonly uses the rest kinds. */
|
|
26
|
+
export type ResetPeriod = 'shortRest' | 'longRest' | (string & {}) | null;
|
|
27
|
+
|
|
28
|
+
/** A single attribute of any type. HP, hit dice, spell slots, ki, sanity, glory... */
|
|
29
|
+
export interface IRAttribute {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
variableName: string;
|
|
33
|
+
type: AttributeType;
|
|
34
|
+
/** Current effective value (for damageable attrs this is total - damage). */
|
|
35
|
+
value: number;
|
|
36
|
+
/** Max / total. */
|
|
37
|
+
total: number;
|
|
38
|
+
/** Amount consumed/reduced (healthBar, resource). */
|
|
39
|
+
damage: number;
|
|
40
|
+
/** Derived ability modifier, when the attribute is an ability score. */
|
|
41
|
+
modifier?: number;
|
|
42
|
+
reset: ResetPeriod;
|
|
43
|
+
/** False when the property is toggled/deactivated (e.g. an unprepared spell). Still imported. */
|
|
44
|
+
active: boolean;
|
|
45
|
+
/** e.g. 'd6' for hitDice attributes. */
|
|
46
|
+
hitDiceSize?: string;
|
|
47
|
+
/** Slot level for spellSlot attributes. */
|
|
48
|
+
spellSlotLevel?: number;
|
|
49
|
+
tags: string[];
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A trained/rollable proficiency. DiceCloud models D&D skills, saves, tool/language/
|
|
55
|
+
* armor/weapon proficiencies - and custom things like 13th Age backgrounds - all as
|
|
56
|
+
* `skill` properties distinguished by skillType.
|
|
57
|
+
*/
|
|
58
|
+
export interface IRSkill {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
variableName: string;
|
|
62
|
+
/** 'skill' | 'save' | 'check' | 'language' | 'armor' | 'weapon' | custom. */
|
|
63
|
+
skillType: string;
|
|
64
|
+
/** Computed roll bonus. */
|
|
65
|
+
value: number;
|
|
66
|
+
/** Linked ability variableName, when any. */
|
|
67
|
+
ability?: string;
|
|
68
|
+
/** Proficiency multiplier (0, 0.5, 1, 2). */
|
|
69
|
+
proficiency: number;
|
|
70
|
+
active: boolean;
|
|
71
|
+
tags: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface IRItem {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
plural?: string;
|
|
78
|
+
quantity: number;
|
|
79
|
+
equipped: boolean;
|
|
80
|
+
weight?: number;
|
|
81
|
+
value?: number;
|
|
82
|
+
description?: string;
|
|
83
|
+
tags: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface IRUses {
|
|
87
|
+
current: number;
|
|
88
|
+
max: number;
|
|
89
|
+
reset: ResetPeriod;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A resource an action/spell spends when used. */
|
|
93
|
+
export interface IRConsumes {
|
|
94
|
+
variableName?: string;
|
|
95
|
+
propertyId?: string;
|
|
96
|
+
amount: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface IRDamage {
|
|
100
|
+
formula: string;
|
|
101
|
+
type?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface IRSpellMeta {
|
|
105
|
+
level: number;
|
|
106
|
+
school?: string;
|
|
107
|
+
castingTime?: string;
|
|
108
|
+
range?: string;
|
|
109
|
+
duration?: string;
|
|
110
|
+
components?: Record<string, boolean>;
|
|
111
|
+
concentration?: boolean;
|
|
112
|
+
ritual?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Anything that "does something": an action, a spell, or an activatable feature. */
|
|
116
|
+
export interface IRAction {
|
|
117
|
+
id: string;
|
|
118
|
+
name: string;
|
|
119
|
+
kind: 'action' | 'spell' | 'feature';
|
|
120
|
+
/** False when toggled/deactivated (e.g. an unprepared spell). Still imported. */
|
|
121
|
+
active: boolean;
|
|
122
|
+
/** Limited-use pool, with reset period (the "2 charges, recharge on long rest" case). */
|
|
123
|
+
uses?: IRUses;
|
|
124
|
+
/** Resources spent on use, by DiceCloud variableName / property id. */
|
|
125
|
+
consumes: IRConsumes[];
|
|
126
|
+
attack?: { bonus: number };
|
|
127
|
+
damage: IRDamage[];
|
|
128
|
+
spell?: IRSpellMeta;
|
|
129
|
+
description?: string;
|
|
130
|
+
tags: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The normalized character. Generic first; D&D is a derived view on top. */
|
|
134
|
+
export interface IRCharacter {
|
|
135
|
+
id: string;
|
|
136
|
+
name: string;
|
|
137
|
+
portrait?: string;
|
|
138
|
+
/** Best-effort hint, never load-bearing. */
|
|
139
|
+
systemHint: 'dnd5e' | 'generic' | (string & {});
|
|
140
|
+
attributes: IRAttribute[];
|
|
141
|
+
skills: IRSkill[];
|
|
142
|
+
actions: IRAction[];
|
|
143
|
+
inventory: IRItem[];
|
|
144
|
+
/** variableName -> attribute, for fast lookup by adapters and the D&D view. */
|
|
145
|
+
byVar: Record<string, IRAttribute>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Raw DiceCloud data we normalize from. Two shapes occur in the wild:
|
|
150
|
+
* - REST `/api/creature/{id}`: { creatures[], creatureProperties[], creatureVariables[] }
|
|
151
|
+
* - the extension's internal store: { creature, properties[], variables }
|
|
152
|
+
* normalize() accepts either.
|
|
153
|
+
*/
|
|
154
|
+
export interface RawDiceCloud {
|
|
155
|
+
creatures?: any[];
|
|
156
|
+
creatureProperties?: any[];
|
|
157
|
+
creatureVariables?: any[];
|
|
158
|
+
creature?: any;
|
|
159
|
+
properties?: any[];
|
|
160
|
+
variables?: any;
|
|
161
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D&D 5e view derived from the system-agnostic IR.
|
|
3
|
+
*
|
|
4
|
+
* This is a *projection*, not the source of truth: it picks the six abilities,
|
|
5
|
+
* skills, saves, HP, hit dice and spell-slot levels out of the generic IR so
|
|
6
|
+
* existing D&D adapters keep their familiar shape. Non-D&D characters simply
|
|
7
|
+
* produce a sparse view and render from the generic IR instead.
|
|
8
|
+
*/
|
|
9
|
+
import type { IRAttribute, IRCharacter } from '../types';
|
|
10
|
+
|
|
11
|
+
export const DND_ABILITIES = [
|
|
12
|
+
'strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export interface AbilityView {
|
|
16
|
+
score: number;
|
|
17
|
+
modifier: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PoolView {
|
|
21
|
+
current: number;
|
|
22
|
+
max: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface HitDicePool extends PoolView {
|
|
26
|
+
size?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Dnd5eView {
|
|
30
|
+
abilities: Record<string, AbilityView>;
|
|
31
|
+
/** Save bonus keyed by ability variableName. */
|
|
32
|
+
saves: Record<string, number>;
|
|
33
|
+
/** Skill bonus keyed by skill variableName. */
|
|
34
|
+
skills: Record<string, number>;
|
|
35
|
+
hitPoints: PoolView & { temp: number };
|
|
36
|
+
hitDice: HitDicePool[];
|
|
37
|
+
/** Spell slots keyed by level (1-9). */
|
|
38
|
+
spellSlots: Record<number, PoolView>;
|
|
39
|
+
proficiencyBonus: number;
|
|
40
|
+
armorClass: number;
|
|
41
|
+
speed: number;
|
|
42
|
+
initiative: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const valOf = (a?: IRAttribute): number => a?.value ?? 0;
|
|
46
|
+
|
|
47
|
+
/** Current/max for a damageable attribute (HP, slots): current = total - damage. */
|
|
48
|
+
function pool(a?: IRAttribute): PoolView {
|
|
49
|
+
if (!a) return { current: 0, max: 0 };
|
|
50
|
+
return { current: a.total - a.damage, max: a.total };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function deriveDnd(ir: IRCharacter): Dnd5eView {
|
|
54
|
+
const { byVar } = ir;
|
|
55
|
+
|
|
56
|
+
const abilities: Record<string, AbilityView> = {};
|
|
57
|
+
for (const ab of DND_ABILITIES) {
|
|
58
|
+
const a = byVar[ab];
|
|
59
|
+
if (a) abilities[ab] = { score: a.value, modifier: a.modifier ?? Math.floor((a.value - 10) / 2) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const saves: Record<string, number> = {};
|
|
63
|
+
const skills: Record<string, number> = {};
|
|
64
|
+
for (const s of ir.skills) {
|
|
65
|
+
if (s.skillType === 'save') saves[s.ability || s.variableName] = s.value;
|
|
66
|
+
else if (s.skillType === 'skill') skills[s.variableName] = s.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const hitDice = ir.attributes
|
|
70
|
+
.filter((a) => a.type === 'hitDice')
|
|
71
|
+
.map((a): HitDicePool => ({ current: a.value, max: a.total, size: a.hitDiceSize }));
|
|
72
|
+
|
|
73
|
+
const spellSlots: Record<number, PoolView> = {};
|
|
74
|
+
for (const a of ir.attributes) {
|
|
75
|
+
if (a.type === 'spellSlot' && a.spellSlotLevel) {
|
|
76
|
+
spellSlots[a.spellSlotLevel] = pool(a);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hp = pool(byVar['hitPoints']);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
abilities,
|
|
84
|
+
saves,
|
|
85
|
+
skills,
|
|
86
|
+
hitPoints: { ...hp, temp: valOf(byVar['tempHP'] || byVar['temporaryHitPoints']) },
|
|
87
|
+
hitDice,
|
|
88
|
+
spellSlots,
|
|
89
|
+
proficiencyBonus: valOf(byVar['proficiencyBonus']),
|
|
90
|
+
armorClass: valOf(byVar['armorClass']),
|
|
91
|
+
speed: valOf(byVar['speed']),
|
|
92
|
+
initiative: valOf(byVar['initiative']),
|
|
93
|
+
};
|
|
94
|
+
}
|