Skip to content

Commit effaba7

Browse files
committed
docs: repo templates for chronicle-foundry-module and chronicle-systems
Complete documentation packages for separating the Foundry VTT module and game system data packs into independent repositories. Includes: chronicle-foundry-module template: - README with installation, configuration, and development docs - CLAUDE.md with AI context, file structure, and conventions - .ai/architecture.md: component diagram, data flow, sync guards - .ai/api-contract.md: every Chronicle API endpoint with request/response schemas - .ai/adapters.md: system adapter architecture and creation guide - .ai/conventions.md: Foundry VTT coding patterns chronicle-systems template: - README with system list, ZIP format, and contribution guide - CLAUDE.md with AI context and validation rules - .ai/architecture.md: complete manifest.json schema, ReferenceItem format, API endpoints - .ai/conventions.md: JSON authoring guide, Foundry path annotations - .ai/creating-a-system.md: step-by-step guide for new systems - JSON Schema files for manifest and reference-item validation Package manager design doc: - Database schema (packages + package_versions tables) - Service interface with version management and auto-updates - GitHub Releases API client design - Admin UI wireframes - Chronicle code changes required for the transition https://claude.ai/code/session_01NnKM8NqJzGz8756CZ4PEGg
1 parent 38cdc07 commit effaba7

14 files changed

Lines changed: 3183 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# System Adapter Architecture
2+
3+
System adapters map between Foundry VTT Actor data and Chronicle entity `fields_data`.
4+
They enable character sync by translating game-system-specific data paths.
5+
6+
## Adapter Interface
7+
8+
Every adapter (built-in or generic) exposes:
9+
10+
```javascript
11+
{
12+
systemId: string, // Chronicle system ID (e.g., "dnd5e", "drawsteel")
13+
characterTypeSlug: string, // Entity type slug (e.g., "dnd5e-character")
14+
actorType: string, // Foundry Actor type (e.g., "character", "hero")
15+
16+
toChronicleFields(actor: Actor): object,
17+
// Reads Foundry Actor system data, returns Chronicle fields_data object.
18+
// Example: { str: 18, hp_current: 45, level: 5 }
19+
20+
fromChronicleFields(entity: object): object,
21+
// Takes Chronicle entity, returns Foundry actor.update() data.
22+
// Uses dot-notation keys: { "system.abilities.str.value": 18, name: "Aragorn" }
23+
}
24+
```
25+
26+
## Adapter Loading Priority
27+
28+
When `ActorSync.init()` is called with a matched system ID, it loads an adapter:
29+
30+
```
31+
1. Check built-in adapters:
32+
- "dnd5e" → import('./adapters/dnd5e-adapter.mjs')
33+
- "pf2e" → import('./adapters/pf2e-adapter.mjs')
34+
(Hardcoded switch in actor-sync.mjs)
35+
36+
2. Fall back to generic adapter:
37+
→ createGenericAdapter(api, systemId)
38+
→ Fetches GET /systems/:id/character-fields
39+
→ Auto-generates toChronicleFields/fromChronicleFields from foundry_path annotations
40+
41+
3. If generic adapter returns null (no fields with foundry_path):
42+
→ Character sync is disabled for this system
43+
```
44+
45+
## How actorType Works
46+
47+
Different game systems define different Actor types in Foundry VTT:
48+
49+
| Game System | Foundry system.id | Actor Type | Chronicle Preset Slug |
50+
|-------------|-------------------|-----------|----------------------|
51+
| D&D 5e | `dnd5e` | `character` | `dnd5e-character` |
52+
| Pathfinder 2e | `pf2e` | `character` | `pf2e-character` |
53+
| Draw Steel | `draw-steel` | `hero` | `drawsteel-character` |
54+
55+
The adapter's `actorType` is used to:
56+
1. **Filter actors** — Only sync actors of the correct type
57+
2. **Create actors** — Set `type` when creating new Actor documents
58+
3. **List actors** — Filter the sync dashboard's character list
59+
60+
The generic adapter reads `actorType` from the API response's `foundry_actor_type` field.
61+
If not specified, it defaults to `"character"`.
62+
63+
## Writing a Built-In Adapter
64+
65+
Create a new file at `scripts/adapters/<system>-adapter.mjs`:
66+
67+
```javascript
68+
/**
69+
* Chronicle Sync - <System Name> Adapter
70+
*
71+
* Maps fields between Foundry VTT <system> Actor data and Chronicle entity
72+
* fields_data. Used by ActorSync when the matched system is "<systemId>".
73+
*/
74+
75+
/** Chronicle system ID. */
76+
export const systemId = '<chronicle-system-id>';
77+
78+
/** Chronicle entity type slug for characters. */
79+
export const characterTypeSlug = '<system>-character';
80+
81+
/** Foundry VTT actor type. */
82+
export const actorType = 'character'; // or 'hero', 'npc', etc.
83+
84+
/**
85+
* Extract Chronicle fields from a Foundry Actor.
86+
* @param {Actor} actor - Foundry Actor document.
87+
* @returns {object} Chronicle fields_data.
88+
*/
89+
export function toChronicleFields(actor) {
90+
const sys = actor.system || {};
91+
return {
92+
// Map each Chronicle field key to its Foundry system data path:
93+
field_key: sys.path?.to?.value ?? null,
94+
};
95+
}
96+
97+
/**
98+
* Convert Chronicle fields to a Foundry Actor update.
99+
* @param {object} entity - Chronicle entity with fields_data.
100+
* @returns {object} Foundry update data (dot-notation keys).
101+
*/
102+
export function fromChronicleFields(entity) {
103+
const f = entity.fields_data || {};
104+
const update = {};
105+
106+
// Only write non-null values to avoid overwriting Foundry calculations:
107+
if (f.field_key != null) update['system.path.to.value'] = Number(f.field_key);
108+
109+
// Name syncs at document level:
110+
if (entity.name) update.name = entity.name;
111+
112+
return update;
113+
}
114+
```
115+
116+
Then add the import to the switch in `actor-sync.mjs`:
117+
118+
```javascript
119+
async _loadAdapter(systemId) {
120+
switch (systemId) {
121+
case 'dnd5e':
122+
return import('./adapters/dnd5e-adapter.mjs');
123+
case 'pathfinder2e':
124+
return import('./adapters/pf2e-adapter.mjs');
125+
case 'newsystem': // ADD THIS
126+
return import('./adapters/newsystem-adapter.mjs'); // ADD THIS
127+
default:
128+
return createGenericAdapter(this.api, systemId);
129+
}
130+
}
131+
```
132+
133+
## Generic Adapter Internals
134+
135+
The generic adapter requires NO code changes. It works for any system whose
136+
manifest includes `foundry_path` annotations on character fields.
137+
138+
### How It Works
139+
140+
1. **Fetch field definitions:**
141+
```
142+
GET /systems/<systemId>/character-fields
143+
```
144+
145+
2. **Filter fields with `foundry_path`:**
146+
Only fields that have a non-empty `foundry_path` are mapped.
147+
148+
3. **Generate `toChronicleFields(actor)`:**
149+
For each mapped field, reads `actor.<foundry_path>` using dot-notation traversal.
150+
151+
4. **Generate `fromChronicleFields(entity)`:**
152+
For each writable field (`foundry_writable !== false`), writes `entity.fields_data[key]`
153+
to the `foundry_path` using dot-notation keys for `actor.update()`.
154+
155+
5. **Type casting:**
156+
Fields with `type: "number"` are cast via `Number()` before writing to Foundry.
157+
158+
### Example: Draw Steel
159+
160+
The Draw Steel manifest defines:
161+
```json
162+
{
163+
"key": "might",
164+
"label": "Might",
165+
"type": "number",
166+
"foundry_path": "system.characteristics.might.value",
167+
"foundry_writable": true
168+
}
169+
```
170+
171+
Generic adapter generates:
172+
```javascript
173+
// toChronicleFields:
174+
result.might = actor.system.characteristics.might.value;
175+
176+
// fromChronicleFields:
177+
update['system.characteristics.might.value'] = Number(entity.fields_data.might);
178+
```
179+
180+
### When to Use Generic vs Built-In
181+
182+
**Use the generic adapter** (recommended for new systems):
183+
- System manifest has `foundry_path` annotations on all character fields
184+
- Field mappings are straightforward (one Chronicle field = one Foundry path)
185+
- No special transformation logic needed
186+
187+
**Write a built-in adapter** when:
188+
- Fields require complex transformations (e.g., combining multiple Foundry paths)
189+
- You need to read from paths that can't be expressed as simple dot-notation
190+
- Performance matters (avoids an extra API call to fetch field definitions)
191+
- The system is widely used and worth maintaining a hand-optimized adapter
192+
193+
## D&D 5e Adapter Reference
194+
195+
The `dnd5e-adapter.mjs` maps 15 fields:
196+
197+
| Chronicle Key | Foundry Path | Writable | Notes |
198+
|---------------|-------------|----------|-------|
199+
| `str` | `system.abilities.str.value` | Yes | Ability score |
200+
| `dex` | `system.abilities.dex.value` | Yes | |
201+
| `con` | `system.abilities.con.value` | Yes | |
202+
| `int` | `system.abilities.int.value` | Yes | |
203+
| `wis` | `system.abilities.wis.value` | Yes | |
204+
| `cha` | `system.abilities.cha.value` | Yes | |
205+
| `hp_current` | `system.attributes.hp.value` | Yes | Current HP |
206+
| `hp_max` | `system.attributes.hp.max` | Yes | Max HP |
207+
| `ac` | `system.attributes.ac.value` | No | Calculated |
208+
| `speed` | `system.attributes.movement.walk` | No | |
209+
| `level` | `system.details.level` | No | |
210+
| `class` | `system.details.class` | No | |
211+
| `race` | `system.details.race` | No | |
212+
| `alignment` | `system.details.alignment` | No | |
213+
| `proficiency_bonus` | `system.attributes.prof` | No | Calculated |
214+
215+
Note: The built-in adapter only writes ability scores and HP back to Foundry.
216+
All other fields are read-only (pulled from Foundry to Chronicle). This prevents
217+
overwriting Foundry's calculated values.

0 commit comments

Comments
 (0)