Skip to content

Commit f82ada0

Browse files
committed
Sprint B-1: System-dependent item presets + manifest changes
Manifest structure: - Added Category field to EntityPresetDef for feature classification ("character", "item", "creature") — enables dynamic item identification - Added RelationPresetDef for system-specific relation types with typed metadata schemas (quantity, equipped, attuned/invested, notes) - Added ItemPreset() and ItemFieldsForAPI() helpers mirroring character equivalents - Extended ValidationReport with item preset analysis System manifests: - D&D 5e: item preset (weapons, armor, potions) with rarity/weight/cost fields and Foundry path annotations for system.type/price/weight - PF2e: equipment preset with level/price/bulk/traits, PF2e-specific Foundry paths - Draw Steel: kit preset with tier/armor_bonus/speed_bonus/melee_damage — reflects Draw Steel's kit-based system instead of traditional items - All systems: has-item relation preset with system-appropriate metadata (D&D: attuned, PF2e: invested, Draw Steel: minimal) - All existing presets tagged with category field API: - New GET /api/v1/campaigns/:id/systems/:systemId/item-fields endpoint - ListSystems response includes has_item_fields flag https://claude.ai/code/session_01WJEjfBqjZaGatHiXXXDupo
1 parent 1b17534 commit f82ada0

6 files changed

Lines changed: 209 additions & 0 deletions

File tree

internal/plugins/syncapi/api_handler.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@ type systemInfoResponse struct {
714714
Name string `json:"name"`
715715
Status string `json:"status"`
716716
HasCharacterFields bool `json:"has_character_fields"`
717+
HasItemFields bool `json:"has_item_fields"`
717718
FoundrySystemID string `json:"foundry_system_id,omitempty"`
718719
Enabled bool `json:"enabled"`
719720
}
@@ -749,6 +750,7 @@ func (h *APIHandler) ListSystems(c echo.Context) error {
749750
Name: manifest.Name,
750751
Status: string(manifest.Status),
751752
HasCharacterFields: manifest.CharacterPreset() != nil,
753+
HasItemFields: manifest.ItemPreset() != nil,
752754
FoundrySystemID: manifest.FoundrySystemID,
753755
Enabled: enabled,
754756
})
@@ -808,3 +810,31 @@ func (h *APIHandler) GetCharacterFields(c echo.Context) error {
808810

809811
return c.JSON(http.StatusOK, resp)
810812
}
813+
814+
// GetItemFields returns the item preset field definitions for a specific
815+
// system, including Foundry path annotations. Used by the Foundry module
816+
// for item sync field mappings.
817+
// GET /api/v1/campaigns/:id/systems/:systemId/item-fields
818+
func (h *APIHandler) GetItemFields(c echo.Context) error {
819+
campaignID := c.Param("id")
820+
systemID := c.Param("systemId")
821+
822+
// Look up the system manifest: first in global registry, then custom.
823+
manifest := systems.Find(systemID)
824+
if manifest == nil && h.campaignSystemLister != nil {
825+
if custom := h.campaignSystemLister.GetManifest(campaignID); custom != nil && custom.ID == systemID {
826+
manifest = custom
827+
}
828+
}
829+
830+
if manifest == nil {
831+
return apperror.NewNotFound("system not found: " + systemID)
832+
}
833+
834+
resp := manifest.ItemFieldsForAPI()
835+
if resp == nil {
836+
return apperror.NewNotFound("item fields not found for system: " + systemID)
837+
}
838+
839+
return c.JSON(http.StatusOK, resp)
840+
}

internal/plugins/syncapi/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func RegisterAPIRoutes(e *echo.Echo, api *APIHandler, calAPI *CalendarAPIHandler
7676
cg.GET("", api.GetCampaign, RequirePermission(PermRead))
7777
cg.GET("/systems", api.ListSystems, RequirePermission(PermRead))
7878
cg.GET("/systems/:systemId/character-fields", api.GetCharacterFields, RequirePermission(PermRead))
79+
cg.GET("/systems/:systemId/item-fields", api.GetItemFields, RequirePermission(PermRead))
7980
cg.GET("/entity-types", api.ListEntityTypes, RequirePermission(PermRead))
8081
cg.GET("/entity-types/:typeID", api.GetEntityType, RequirePermission(PermRead))
8182
cg.GET("/entities", api.ListEntities, RequirePermission(PermRead))

internal/systems/dnd5e/manifest.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,27 @@
7575
]
7676
}
7777
],
78+
"relation_presets": [
79+
{
80+
"slug": "has-item",
81+
"name": "Has Item",
82+
"reverse_name": "In Inventory Of",
83+
"metadata_schema": {
84+
"quantity": { "type": "number", "default": 1 },
85+
"equipped": { "type": "boolean", "default": false },
86+
"attuned": { "type": "boolean", "default": false },
87+
"notes": { "type": "string", "default": "" }
88+
}
89+
}
90+
],
7891
"entity_presets": [
7992
{
8093
"slug": "dnd5e-character",
8194
"name": "D&D Character",
8295
"name_plural": "D&D Characters",
8396
"icon": "fa-hat-wizard",
8497
"color": "#7C3AED",
98+
"category": "character",
8599
"fields": [
86100
{ "key": "class", "label": "Class", "type": "string", "foundry_path": "system.details.class" },
87101
{ "key": "level", "label": "Level", "type": "number", "foundry_path": "system.details.level" },
@@ -106,11 +120,30 @@
106120
"name_plural": "D&D Creatures",
107121
"icon": "fa-dragon",
108122
"color": "#DC2626",
123+
"category": "creature",
109124
"fields": [
110125
{ "key": "cr", "label": "Challenge Rating", "type": "string" },
111126
{ "key": "type", "label": "Type", "type": "string" },
112127
{ "key": "hp", "label": "Hit Points", "type": "string" }
113128
]
129+
},
130+
{
131+
"slug": "dnd5e-item",
132+
"name": "D&D Item",
133+
"name_plural": "D&D Items",
134+
"icon": "fa-gem",
135+
"color": "#A855F7",
136+
"category": "item",
137+
"fields": [
138+
{ "key": "item_type", "label": "Type", "type": "string", "foundry_path": "system.type.value" },
139+
{ "key": "rarity", "label": "Rarity", "type": "string", "foundry_path": "system.rarity" },
140+
{ "key": "weight", "label": "Weight (lb)", "type": "number", "foundry_path": "system.weight.value" },
141+
{ "key": "cost", "label": "Cost (gp)", "type": "string", "foundry_path": "system.price.value" },
142+
{ "key": "damage", "label": "Damage", "type": "string" },
143+
{ "key": "properties", "label": "Properties", "type": "string" },
144+
{ "key": "attunement", "label": "Requires Attunement", "type": "string", "foundry_path": "system.attunement" },
145+
{ "key": "description", "label": "Description", "type": "markdown" }
146+
]
114147
}
115148
]
116149
}

internal/systems/drawsteel/manifest.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,49 @@
4141
]
4242
}
4343
],
44+
"relation_presets": [
45+
{
46+
"slug": "has-item",
47+
"name": "Has Item",
48+
"reverse_name": "In Inventory Of",
49+
"metadata_schema": {
50+
"quantity": { "type": "number", "default": 1 },
51+
"equipped": { "type": "boolean", "default": false },
52+
"notes": { "type": "string", "default": "" }
53+
}
54+
}
55+
],
4456
"entity_presets": [
4557
{
4658
"slug": "drawsteel-character",
4759
"name": "Draw Steel Character",
4860
"name_plural": "Draw Steel Characters",
4961
"icon": "fa-bolt",
5062
"color": "#F59E0B",
63+
"category": "character",
5164
"fields": [
5265
{ "key": "class", "label": "Class", "type": "string" },
5366
{ "key": "level", "label": "Level", "type": "number" },
5467
{ "key": "ancestry", "label": "Ancestry", "type": "string" }
5568
]
69+
},
70+
{
71+
"slug": "drawsteel-kit",
72+
"name": "Draw Steel Kit",
73+
"name_plural": "Draw Steel Kits",
74+
"icon": "fa-toolbox",
75+
"color": "#EF4444",
76+
"category": "item",
77+
"fields": [
78+
{ "key": "kit_type", "label": "Kit Type", "type": "string" },
79+
{ "key": "tier", "label": "Tier", "type": "string" },
80+
{ "key": "armor_bonus", "label": "Armor Bonus", "type": "number" },
81+
{ "key": "speed_bonus", "label": "Speed Bonus", "type": "number" },
82+
{ "key": "stability_bonus", "label": "Stability Bonus", "type": "number" },
83+
{ "key": "melee_damage", "label": "Melee Damage", "type": "string" },
84+
{ "key": "ranged_damage", "label": "Ranged Damage", "type": "string" },
85+
{ "key": "signature_ability", "label": "Signature Ability", "type": "markdown" }
86+
]
5687
}
5788
]
5889
}

internal/systems/manifest.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ type SystemManifest struct {
4747
// enabling this module (e.g., "D&D Character" with predefined fields).
4848
EntityPresets []EntityPresetDef `json:"entity_presets,omitempty"`
4949

50+
// RelationPresets are relation type templates that campaigns can adopt when
51+
// enabling this module (e.g., "has-item" for inventory tracking).
52+
RelationPresets []RelationPresetDef `json:"relation_presets,omitempty"`
53+
5054
// FoundrySystemID is the Foundry VTT game.system.id that this system
5155
// corresponds to (e.g., "dnd5e", "pf2e"). When set, the Foundry module
5256
// can automatically match this Chronicle system to the running Foundry
@@ -129,10 +133,38 @@ type EntityPresetDef struct {
129133
// Color is the hex color for the entity type badge.
130134
Color string `json:"color"`
131135

136+
// Category classifies the preset for feature gating (e.g., "character",
137+
// "item", "creature"). Used to identify which entity types belong to
138+
// specific features like the Armory (items) or NPC gallery (characters).
139+
Category string `json:"category,omitempty"`
140+
132141
// Fields are the default field definitions for entities of this type.
133142
Fields []FieldDef `json:"fields,omitempty"`
134143
}
135144

145+
// RelationPresetDef describes a relation type template that a module provides.
146+
// Used to create system-specific relation types (e.g., "has-item" for inventory).
147+
type RelationPresetDef struct {
148+
// Slug is the URL-safe identifier (e.g., "has-item").
149+
Slug string `json:"slug"`
150+
151+
// Name is the display name (e.g., "Has Item").
152+
Name string `json:"name"`
153+
154+
// ReverseName is the reverse direction label (e.g., "In Inventory Of").
155+
ReverseName string `json:"reverse_name"`
156+
157+
// MetadataSchema defines the JSON metadata fields for this relation.
158+
// Keys are field names, values describe type and default value.
159+
MetadataSchema map[string]RelationFieldSchema `json:"metadata_schema,omitempty"`
160+
}
161+
162+
// RelationFieldSchema defines a single metadata field on a relation preset.
163+
type RelationFieldSchema struct {
164+
Type string `json:"type"` // "number", "boolean", "string"
165+
Default any `json:"default"` // Default value for new relations.
166+
}
167+
136168
// CharacterPreset returns the first entity preset whose slug ends with
137169
// "-character", or nil if no character preset is defined. Used by the
138170
// sync API to expose character field templates for actor sync.
@@ -145,6 +177,45 @@ func (m *SystemManifest) CharacterPreset() *EntityPresetDef {
145177
return nil
146178
}
147179

180+
// ItemPreset returns the first entity preset with category "item", or nil
181+
// if no item preset is defined. Used by the Armory plugin and item sync.
182+
func (m *SystemManifest) ItemPreset() *EntityPresetDef {
183+
for i := range m.EntityPresets {
184+
if m.EntityPresets[i].Category == "item" {
185+
return &m.EntityPresets[i]
186+
}
187+
}
188+
return nil
189+
}
190+
191+
// ItemFieldsForAPI builds the API response for item preset fields.
192+
// Returns nil if no item preset exists. Mirrors CharacterFieldsForAPI.
193+
func (m *SystemManifest) ItemFieldsForAPI() *CharacterFieldsResponse {
194+
preset := m.ItemPreset()
195+
if preset == nil {
196+
return nil
197+
}
198+
199+
fields := make([]CharacterFieldExport, len(preset.Fields))
200+
for i, f := range preset.Fields {
201+
fields[i] = CharacterFieldExport{
202+
Key: f.Key,
203+
Label: f.Label,
204+
Type: f.Type,
205+
FoundryPath: f.FoundryPath,
206+
FoundryWritable: f.FoundryPath != "" && f.IsFoundryWritable(),
207+
}
208+
}
209+
210+
return &CharacterFieldsResponse{
211+
SystemID: m.ID,
212+
PresetSlug: preset.Slug,
213+
PresetName: preset.Name,
214+
FoundrySystemID: m.FoundrySystemID,
215+
Fields: fields,
216+
}
217+
}
218+
148219
// CharacterFieldsResponse is the API response shape for the character
149220
// fields endpoint, containing field definitions with Foundry annotations.
150221
type CharacterFieldsResponse struct {
@@ -211,6 +282,12 @@ type ValidationReport struct {
211282
// CharacterFieldCount is the number of fields on the character preset.
212283
CharacterFieldCount int `json:"character_field_count"`
213284

285+
// HasItemPreset indicates an item-category preset was found.
286+
HasItemPreset bool `json:"has_item_preset"`
287+
288+
// ItemFieldCount is the number of fields on the item preset.
289+
ItemFieldCount int `json:"item_field_count"`
290+
214291
// FoundryCompatible indicates foundry_system_id is set.
215292
FoundryCompatible bool `json:"foundry_compatible"`
216293

@@ -257,6 +334,12 @@ func (m *SystemManifest) BuildValidationReport() *ValidationReport {
257334
}
258335
}
259336

337+
// Analyze item preset.
338+
if itemPreset := m.ItemPreset(); itemPreset != nil {
339+
r.HasItemPreset = true
340+
r.ItemFieldCount = len(itemPreset.Fields)
341+
}
342+
260343
// Generate warnings.
261344
if r.CategoryCount == 0 {
262345
r.Warnings = append(r.Warnings, "No reference data categories defined")

internal/systems/pathfinder2e/manifest.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,27 @@
7676
]
7777
}
7878
],
79+
"relation_presets": [
80+
{
81+
"slug": "has-item",
82+
"name": "Has Item",
83+
"reverse_name": "In Inventory Of",
84+
"metadata_schema": {
85+
"quantity": { "type": "number", "default": 1 },
86+
"equipped": { "type": "boolean", "default": false },
87+
"invested": { "type": "boolean", "default": false },
88+
"notes": { "type": "string", "default": "" }
89+
}
90+
}
91+
],
7992
"entity_presets": [
8093
{
8194
"slug": "pf2e-character",
8295
"name": "PF2e Character",
8396
"name_plural": "PF2e Characters",
8497
"icon": "fa-shield-halved",
8598
"color": "#2563EB",
99+
"category": "character",
86100
"fields": [
87101
{ "key": "class", "label": "Class", "type": "string", "foundry_path": "system.details.class.name", "foundry_writable": false },
88102
{ "key": "level", "label": "Level", "type": "number", "foundry_path": "system.details.level.value", "foundry_writable": false },
@@ -107,11 +121,28 @@
107121
"name_plural": "PF2e Creatures",
108122
"icon": "fa-skull",
109123
"color": "#DC2626",
124+
"category": "creature",
110125
"fields": [
111126
{ "key": "level", "label": "Level", "type": "number" },
112127
{ "key": "type", "label": "Type", "type": "string" },
113128
{ "key": "hp", "label": "Hit Points", "type": "number" }
114129
]
130+
},
131+
{
132+
"slug": "pf2e-equipment",
133+
"name": "PF2e Equipment",
134+
"name_plural": "PF2e Equipment",
135+
"icon": "fa-shield-halved",
136+
"color": "#3B82F6",
137+
"category": "item",
138+
"fields": [
139+
{ "key": "level", "label": "Level", "type": "number", "foundry_path": "system.level.value" },
140+
{ "key": "price", "label": "Price", "type": "string", "foundry_path": "system.price.value" },
141+
{ "key": "bulk", "label": "Bulk", "type": "string", "foundry_path": "system.bulk.value" },
142+
{ "key": "traits", "label": "Traits", "type": "string" },
143+
{ "key": "category", "label": "Category", "type": "string" },
144+
{ "key": "description", "label": "Description", "type": "markdown" }
145+
]
115146
}
116147
]
117148
}

0 commit comments

Comments
 (0)