Skip to content

Commit da1ac70

Browse files
authored
Merge pull request #143 from keyxmakerx/claude/foundry-module-setup-ZjHlL
Claude/foundry module setup zj hl l
2 parents 93f5388 + cc528c7 commit da1ac70

29 files changed

Lines changed: 4942 additions & 1 deletion

.ai/status.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
<!-- ====================================================================== -->
99

1010
## Last Updated
11-
2026-03-17 -- **Sprint: Draw Steel System Module + Foundry Infrastructure Fixes.**
11+
2026-03-17 -- **Sprint: Package Manager Plugin + Repo Separation Docs.**
12+
13+
45. **Sprint: Package Manager Plugin + Repo Separation.**
14+
- **Repo Separation Documentation (COMPLETE)** — Created comprehensive documentation templates in `docs/repo-templates/` for `chronicle-foundry-module` and `chronicle-systems` repos. Includes README, CLAUDE.md, .ai/ architecture docs, API contracts, adapter docs, JSON schemas, and step-by-step system creation guides.
15+
- **Package Manager Plugin (COMPLETE)** — New `internal/plugins/packages/` plugin with full admin UI. Features: GitHub release fetching, version management (install/pin/unpin), auto-update policies (off/nightly/weekly/on_release), background update worker, package CRUD, HTMX version picker and usage tracking UI. Database: `packages` and `package_versions` tables. Routes under `/admin/packages`. Sidebar link added.
16+
- **Next Steps**: Wire usage tracking to campaign addon tables. Replace bundled Foundry module serving with package manager's installed path. Remove bundled system data and load from package manager instead.
1217

1318
44. **Sprint: Draw Steel System Module + Infrastructure.**
1419
- **Draw Steel System (COMPLETE)** — Full manifest expansion: 24-field character preset with Foundry path annotations, creature preset with EV/role/role_type, kit preset. Reference data: 8 abilities, 6 creatures, 10 ancestries, 6 kits (CC-BY-4.0). Status changed from `coming_soon` to `available`.

cmd/server/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/keyxmakerx/chronicle/internal/database"
1818
"github.com/keyxmakerx/chronicle/internal/plugins/calendar"
1919
"github.com/keyxmakerx/chronicle/internal/plugins/maps"
20+
"github.com/keyxmakerx/chronicle/internal/plugins/packages"
2021
"github.com/keyxmakerx/chronicle/internal/plugins/sessions"
2122
"github.com/keyxmakerx/chronicle/internal/plugins/syncapi"
2223
"github.com/keyxmakerx/chronicle/internal/plugins/timeline"
@@ -166,5 +167,6 @@ func registeredPlugins() []database.PluginSchema {
166167
{Slug: "sessions", MigrationsFS: mustSub(sessions.MigrationsFS, database.PluginMigrationsSubdir)},
167168
{Slug: "timeline", MigrationsFS: mustSub(timeline.MigrationsFS, database.PluginMigrationsSubdir)},
168169
{Slug: "syncapi", MigrationsFS: mustSub(syncapi.MigrationsFS, database.PluginMigrationsSubdir)},
170+
{Slug: "packages", MigrationsFS: mustSub(packages.MigrationsFS, database.PluginMigrationsSubdir)},
169171
}
170172
}
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)