# Stage 4: Full TypeDataModel Migration Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace `template.json` with TypeDataModel classes for all 23 actor/item types, eliminating the Foundry v14 deprecation warning.

**Architecture:** Field names exactly match `template.json` to avoid any changes to `actor.js`, sheets, or templates. Shared field structures (attributes, skills, monitors) are extracted to `schema-helpers.mjs`. Two base actor classes cover the Player/NPC and Critter/Spirit overlaps. All 18 item types extend `SR6BaseItemData`. Delivered in two commits — DataModels registered (template.json still present), then template.json deleted.

**Tech Stack:** Foundry VTT v14 TypeDataModel API (`foundry.abstract.TypeDataModel`, `foundry.data.fields`), ESModules

---

## File Map

**New files — actors:**
- `module/datamodels/schema-helpers.mjs` — shared field factory functions used by both humanoid and critter base DataModels
- `module/datamodels/actors/humanoid-actor-data.mjs` — `SR6HumanoidActorData` (Player + NPC shared fields)
- `module/datamodels/actors/player-actor-data.mjs` — `SR6PlayerActorData`
- `module/datamodels/actors/npc-actor-data.mjs` — `SR6NPCActorData`
- `module/datamodels/actors/critter-base-actor-data.mjs` — `SR6CritterBaseActorData` (Critter + Spirit shared fields)
- `module/datamodels/actors/critter-actor-data.mjs` — `SR6CritterActorData`
- `module/datamodels/actors/spirit-actor-data.mjs` — `SR6SpiritActorData`
- `module/datamodels/actors/vehicle-actor-data.mjs` — `SR6VehicleActorData`

**New files — items:**
- `module/datamodels/items/adeptpower-item-data.mjs`
- `module/datamodels/items/complexform-item-data.mjs`
- `module/datamodels/items/contact-item-data.mjs`
- `module/datamodels/items/critterpower-item-data.mjs`
- `module/datamodels/items/spritepower-item-data.mjs`
- `module/datamodels/items/echo-item-data.mjs`
- `module/datamodels/items/focus-item-data.mjs`
- `module/datamodels/items/gear-item-data.mjs`
- `module/datamodels/items/lifestyle-item-data.mjs`
- `module/datamodels/items/martialartstyle-item-data.mjs`
- `module/datamodels/items/martialarttech-item-data.mjs`
- `module/datamodels/items/metamagic-item-data.mjs`
- `module/datamodels/items/quality-item-data.mjs`
- `module/datamodels/items/ritual-item-data.mjs`
- `module/datamodels/items/sin-item-data.mjs`
- `module/datamodels/items/skill-item-data.mjs`
- `module/datamodels/items/software-item-data.mjs`
- `module/datamodels/items/spell-item-data.mjs`

**Modified:**
- `module/datamodels/_module.mjs` — add all new exports, remove legacy-gear-item-data export
- `module/Shadowrun6.js` — register all DataModels in CONFIG.Actor.dataModels and CONFIG.Item.dataModels

**Deleted:**
- `module/datamodels/legacy-gear-item-data.mjs` — empty stub superseded by SR6GearItemData
- `template.json` — deleted in Commit 2 only

---

## Task 1: schema-helpers.mjs

Shared field factory functions referenced by both actor base DataModels. Called at `defineSchema()` time so `foundry.data.fields` is available.

**Files:**
- Create: `module/datamodels/schema-helpers.mjs`

- [ ] Create the directories:
```bash
mkdir -p module/datamodels/actors module/datamodels/items
```

- [ ] Create `module/datamodels/schema-helpers.mjs`:

```js
/**
 * Shared field factory functions for actor DataModels.
 * All field names match template.json exactly.
 * TODO: migrate to SR6AttributeField / SR6SkillField / SR6ConditionMonitorField
 *       per the sprite actor pattern once field naming is modernised.
 */

/** Returns a standard attribute SchemaField (bod/agi/rea/str/wil/log/int/cha pattern). */
export function standardAttributeField(base = 2, pool = 2) {
    const fields = foundry.data.fields;
    return new fields.SchemaField({
        base:      new fields.NumberField({ initial: base }),
        mod:       new fields.NumberField({ initial: 0 }),
        modString: new fields.StringField({ initial: "" }),
        augment:   new fields.NumberField({ initial: 0 }),
        pool:      new fields.NumberField({ initial: pool })
    });
}

/** Returns a skill SchemaField with expandedSpecializations. Most skills use this. */
export function skillField() {
    const fields = foundry.data.fields;
    return new fields.SchemaField({
        points:                 new fields.NumberField({ initial: 0 }),
        specialization:         new fields.StringField({ initial: "" }),
        expertise:              new fields.StringField({ initial: "" }),
        modifier:               new fields.NumberField({ initial: 0 }),
        augment:                new fields.NumberField({ initial: 0 }),
        expandedSpecializations: new fields.ArrayField(new fields.StringField())
    });
}

/** Returns a skill SchemaField WITHOUT expandedSpecializations. Used for electronics only. */
export function skillFieldNoExpanded() {
    const fields = foundry.data.fields;
    return new fields.SchemaField({
        points:         new fields.NumberField({ initial: 0 }),
        specialization: new fields.StringField({ initial: "" }),
        expertise:      new fields.StringField({ initial: "" }),
        modifier:       new fields.NumberField({ initial: 0 }),
        augment:        new fields.NumberField({ initial: 0 })
    });
}

/**
 * Returns the full attributes SchemaField matching template.json attribute-template.
 * Includes bod/agi/rea/str/wil/log/int/cha, mag, res, edg, essence.
 */
export function attributesSchema() {
    const fields = foundry.data.fields;
    return new fields.SchemaField({
        // TODO: migrate each to SR6AttributeField
        bod: standardAttributeField(2, 2),
        agi: standardAttributeField(2, 2),
        rea: standardAttributeField(2, 2),
        str: standardAttributeField(2, 2),
        wil: standardAttributeField(2, 2),
        log: standardAttributeField(2, 2),
        int: standardAttributeField(2, 2),
        cha: standardAttributeField(2, 2),
        mag: new fields.SchemaField({
            base:       new fields.NumberField({ initial: 0 }),
            mod:        new fields.NumberField({ initial: 0 }),
            pool:       new fields.NumberField({ initial: 0 }),
            min:        new fields.NumberField({ initial: 0 }),
            initiation: new fields.NumberField({ initial: 0 })
        }),
        res: new fields.SchemaField({
            base:       new fields.NumberField({ initial: 0 }),
            mod:        new fields.NumberField({ initial: 0 }),
            pool:       new fields.NumberField({ initial: 0 }),
            submersion: new fields.NumberField({ initial: 0 })
        }),
        edg: new fields.SchemaField({
            current: new fields.NumberField({ initial: 0 }),
            max:     new fields.NumberField({ initial: 1 })
        }),
        essence: new fields.SchemaField({
            base: new fields.NumberField({ initial: 6 }),
            mod:  new fields.NumberField({ initial: 0 }),
            pool: new fields.NumberField({ initial: 6 })
        })
    });
}

/** Returns the full monitors SchemaField (template.json monitor-template). */
export function monitorsSchema() {
    const fields = foundry.data.fields;
    const monitor = (base = 9, value = 9) => new fields.SchemaField({
        base:      new fields.NumberField({ initial: base }),
        mod:       new fields.NumberField({ initial: 0 }),
        modString: new fields.StringField({ initial: "" }),
        value:     new fields.NumberField({ initial: value }),
        dmg:       new fields.NumberField({ initial: 0 }),
        max:       new fields.NumberField({ initial: 18 })
    });
    return {
        // TODO: migrate to SR6ConditionMonitorField
        physical: monitor(9, 9),
        stun:     monitor(9, 9),
        overflow: new fields.SchemaField({
            mod:       new fields.NumberField({ initial: 0 }),
            modString: new fields.StringField({ initial: "" }),
            value:     new fields.NumberField({ initial: 0 }),
            dmg:       new fields.NumberField({ initial: 0 }),
            max:       new fields.NumberField({ initial: 32 })
        }),
        edge: new fields.SchemaField({
            value: new fields.NumberField({ initial: 0 }),
            max:   new fields.NumberField({ initial: 1 })
        })
    };
}

/** Returns the initiative SchemaField (template.json initiative-template). */
export function initiativeSchema() {
    const fields = foundry.data.fields;
    const initiative = (dice = 1) => new fields.SchemaField({
        mod:     new fields.NumberField({ initial: 0 }),
        dice:    new fields.NumberField({ initial: dice }),
        diceMod: new fields.NumberField({ initial: 0 })
    });
    return new fields.SchemaField({
        // TODO: migrate to SR6InitiativeField
        astral:   initiative(2),
        matrix:   initiative(1),
        physical: initiative(1),
        actions:  new fields.NumberField({ initial: 0 })
    });
}

/** Returns the skills SchemaField (template.json skill-template, 19 skills). */
export function skillsSchema() {
    return new (foundry.data.fields.SchemaField)({
        // TODO: migrate to SR6SkillField
        astral:         skillField(),
        athletics:      skillField(),
        biotech:        skillField(),
        close_combat:   skillField(),
        con:            skillField(),
        conjuring:      skillField(),
        cracking:       skillField(),
        electronics:    skillFieldNoExpanded(), // template.json omits expandedSpecializations here
        enchanting:     skillField(),
        engineering:    skillField(),
        exotic_weapons: skillField(),
        firearms:       skillField(),
        influence:      skillField(),
        outdoors:       skillField(),
        perception:     skillField(),
        piloting:       skillField(),
        sorcery:        skillField(),
        stealth:        skillField(),
        tasking:        skillField()
    });
}
```

- [ ] Verify no syntax errors:
```bash
node --input-type=module --eval "import './module/datamodels/schema-helpers.mjs'" 2>&1 || echo "Note: Foundry globals not available outside browser — import check skipped"
```
Expected: either silent success or the "Foundry globals not available" note. Any SyntaxError means a typo in the file.

---

## Task 2: Actor Base DataModels

**Files:**
- Create: `module/datamodels/actors/humanoid-actor-data.mjs`
- Create: `module/datamodels/actors/critter-base-actor-data.mjs`

- [ ] Create `module/datamodels/actors/humanoid-actor-data.mjs`:

```js
import SR6BaseActorData from '../base-actor-data.mjs';
import { attributesSchema, monitorsSchema, initiativeSchema, skillsSchema } from '../schema-helpers.mjs';

/**
 * Base DataModel for Player and NPC actors.
 * Field names match template.json exactly.
 * TODO: migrate nested structures to SR6AttributeField / SR6SkillField per sprite actor pattern.
 */
export default class SR6HumanoidActorData extends SR6BaseActorData {
    static defineSchema() {
        const fields = foundry.data.fields;
        const modOnly = () => new fields.SchemaField({ mod: new fields.NumberField({ initial: 0 }) });

        return {
            ...super.defineSchema(),
            attributes: attributesSchema(),
            type:       new fields.StringField({ initial: "mundane" }),
            gender:     new fields.StringField({ initial: "" }),
            metatype:   new fields.StringField({ initial: "human" }),
            mortype:    new fields.StringField({ initial: "mundane" }),
            matrixIni:  new fields.StringField({ initial: "vrcold" }),
            ...monitorsSchema(),
            derived: new fields.SchemaField({
                composure:         modOnly(),
                judge_intentions:  modOnly(),
                memory:            modOnly(),
                lift_carry:        modOnly(),
                matrix_perception: modOnly()
            }),
            defenserating: new fields.SchemaField({
                physical: modOnly(),
                matrix:   modOnly(),
                vehicle:  modOnly(),
                astral:   modOnly(),
                social:   modOnly()
            }),
            attackrating: new fields.SchemaField({
                physical: modOnly(),
                astral:   modOnly(),
                matrix:   modOnly(),
                vehicle:  modOnly(),
                social:   modOnly()
            }),
            defensepool: new fields.SchemaField({
                physical:        modOnly(),
                astral:          modOnly(),
                spells_direct:   modOnly(),
                spells_indirect: modOnly(),
                spells_other:    modOnly(),
                vehicle:         modOnly(),
                toxin:           modOnly(),
                damage_physical: modOnly(),
                damage_astral:   modOnly()
            }),
            initiative: initiativeSchema(),
            walk:    new fields.NumberField({ initial: 5 }),
            sprint:  new fields.NumberField({ initial: 10 }),
            perHit:  new fields.NumberField({ initial: 1 }),
            skills:  skillsSchema(),
            tradition: new fields.SchemaField({
                genesisID: new fields.StringField({ initial: "" }),
                name:      new fields.StringField({ initial: "" }),
                attribute: new fields.StringField({ initial: "log" })
            }),
            persona: new fields.SchemaField({
                device: new fields.SchemaField({
                    mod: new fields.SchemaField({
                        a: new fields.NumberField({ initial: 0 }),
                        s: new fields.NumberField({ initial: 0 }),
                        d: new fields.NumberField({ initial: 0 }),
                        f: new fields.NumberField({ initial: 0 })
                    })
                }),
                living: new fields.SchemaField({
                    mod: new fields.SchemaField({
                        a: new fields.NumberField({ initial: 0 }),
                        s: new fields.NumberField({ initial: 0 }),
                        d: new fields.NumberField({ initial: 0 }),
                        f: new fields.NumberField({ initial: 0 })
                    })
                }),
                used: new fields.SchemaField({
                    a: new fields.NumberField({ initial: 0 }),
                    s: new fields.NumberField({ initial: 0 }),
                    d: new fields.NumberField({ initial: 0 }),
                    f: new fields.NumberField({ initial: 0 })
                })
            })
        };
    }
}
```

- [ ] Create `module/datamodels/actors/critter-base-actor-data.mjs`:

```js
import SR6BaseActorData from '../base-actor-data.mjs';
import { attributesSchema, monitorsSchema, initiativeSchema, skillsSchema } from '../schema-helpers.mjs';

/**
 * Base DataModel for Critter and Spirit actors.
 * Shares attributes, monitors, initiative, and skills with humanoid actors via helpers.
 */
export default class SR6CritterBaseActorData extends SR6BaseActorData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            attributes: attributesSchema(),
            metatype:   new fields.StringField({ initial: "Critter" }),
            editmode:   new fields.BooleanField({ initial: false }),
            ...monitorsSchema(),
            initiative: initiativeSchema(),
            skills:     skillsSchema()
        };
    }
}
```

---

## Task 3: Concrete Actor DataModels

**Files:**
- Create: `module/datamodels/actors/player-actor-data.mjs`
- Create: `module/datamodels/actors/npc-actor-data.mjs`
- Create: `module/datamodels/actors/critter-actor-data.mjs`
- Create: `module/datamodels/actors/spirit-actor-data.mjs`
- Create: `module/datamodels/actors/vehicle-actor-data.mjs`

- [ ] Create `module/datamodels/actors/player-actor-data.mjs`:

```js
import SR6HumanoidActorData from './humanoid-actor-data.mjs';

export default class SR6PlayerActorData extends SR6HumanoidActorData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            nuyen:       new fields.NumberField({ initial: 0 }),
            overwatch:   new fields.NumberField({ initial: 0 }),
            karma:       new fields.NumberField({ initial: 0 }),
            karma_total: new fields.NumberField({ initial: 0 }),
            heat:        new fields.NumberField({ initial: 0 }),
            reputation:  new fields.NumberField({ initial: 0 })
        };
    }
}
```

- [ ] Create `module/datamodels/actors/npc-actor-data.mjs`:

```js
import SR6HumanoidActorData from './humanoid-actor-data.mjs';

export default class SR6NPCActorData extends SR6HumanoidActorData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            // template.json NPC overrides these attribute-template defaults
            type:      new fields.StringField({ initial: "npc" }),
            metatype:  new fields.StringField({ initial: "NPC" }),
            rating:    new fields.NumberField({ initial: 1 }),
            gruntmeta: new fields.StringField({ initial: "" }),
            editmode:  new fields.BooleanField({ initial: false })
        };
    }
}
```

- [ ] Create `module/datamodels/actors/critter-actor-data.mjs`:

```js
import SR6CritterBaseActorData from './critter-base-actor-data.mjs';

export default class SR6CritterActorData extends SR6CritterBaseActorData {
    static defineSchema() {
        return { ...super.defineSchema() };
    }
}
```

- [ ] Create `module/datamodels/actors/spirit-actor-data.mjs`:

```js
import SR6CritterBaseActorData from './critter-base-actor-data.mjs';

export default class SR6SpiritActorData extends SR6CritterBaseActorData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            metatype:   new fields.StringField({ initial: "Spirit" }), // override critter base "Critter"
            rating:     new fields.NumberField({ initial: 1 }),
            spiritType: new fields.StringField({ initial: "air" })
        };
    }
}
```

- [ ] Create `module/datamodels/actors/vehicle-actor-data.mjs`:

```js
import SR6BaseActorData from '../base-actor-data.mjs';
import { monitorsSchema } from '../schema-helpers.mjs';

export default class SR6VehicleActorData extends SR6BaseActorData {
    static defineSchema() {
        const fields = foundry.data.fields;
        const vskill = () => new fields.SchemaField({
            points:   new fields.NumberField({ initial: 0 }),
            modifier: new fields.NumberField({ initial: 0 }),
            pool:     new fields.NumberField({ initial: 0 })
        });
        return {
            ...super.defineSchema(),
            handlOn:  new fields.NumberField({ initial: 1 }),
            handlOff: new fields.NumberField({ initial: 2 }),
            accOn:    new fields.NumberField({ initial: 3 }),
            accOff:   new fields.NumberField({ initial: 4 }),
            spdiOn:   new fields.NumberField({ initial: 5 }),
            spdiOff:  new fields.NumberField({ initial: 0 }),
            tspd:     new fields.NumberField({ initial: 0 }),
            bod:      new fields.NumberField({ initial: 0 }),
            arm:      new fields.NumberField({ initial: 0 }),
            pil:      new fields.NumberField({ initial: 0 }),
            sen:      new fields.NumberField({ initial: 0 }),
            sea:      new fields.NumberField({ initial: 0 }),
            vtype:    new fields.StringField({ initial: "ground_craft" }),
            vehicle: new fields.SchemaField({
                belongs: new fields.StringField({ initial: "" }),
                opMode:  new fields.StringField({ initial: "manual" }),
                offRoad: new fields.BooleanField({ initial: false }),
                speed:   new fields.NumberField({ initial: 0 })
            }),
            ...monitorsSchema(),
            skills: new fields.SchemaField({
                piloting:   vskill(),
                evasion:    vskill(),
                perception: vskill(),
                cracking:   vskill(),
                stealth:    vskill()
            })
        };
    }
}
```

---

## Task 4: Simple Item DataModels

All 11 files. Each extends `SR6BaseItemData` which already provides: `description` (HTMLField), `genesisID`, `availDef`, `product` (StringField), `price`, `page` (NumberField).

**Files:** Create all 11 in `module/datamodels/items/`

- [ ] Create `module/datamodels/items/echo-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

/** Echo items have no fields beyond the genesis template. */
export default class SR6EchoItemData extends SR6BaseItemData {}
```

- [ ] Create `module/datamodels/items/focus-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6FocusItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            rating: new fields.NumberField({ initial: 1 })
        };
    }
}
```

- [ ] Create `module/datamodels/items/skill-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6SkillItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            points:   new fields.NumberField({ initial: 0 }),
            modifier: new fields.NumberField({ initial: 0 })
        };
    }
}
```

- [ ] Create `module/datamodels/items/metamagic-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6MetamagicItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            level:  new fields.BooleanField({ initial: false }),
            adepts: new fields.BooleanField({ initial: false }),
            mages:  new fields.BooleanField({ initial: false })
        };
    }
}
```

- [ ] Create `module/datamodels/items/sin-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6SinItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            quality: new fields.StringField({ initial: "REAL_SIN" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/lifestyle-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6LifestyleItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            type: new fields.StringField({ initial: "low" }),
            paid: new fields.NumberField({ initial: 1 }),
            cost: new fields.NumberField({ initial: 2000 }),
            sin:  new fields.StringField({ initial: "" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/adeptpower-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6AdeptpowerItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            hasLevel:   new fields.BooleanField({ initial: false }),
            level:      new fields.NumberField({ initial: 0 }),
            choice:     new fields.StringField({ initial: "" }),
            cost:       new fields.NumberField({ initial: 0 }),
            activation: new fields.StringField({ initial: "" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/martialartstyle-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6MartialartstyleItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            category: new fields.SchemaField({
                grappling: new fields.BooleanField({ initial: false }),
                mobility:  new fields.BooleanField({ initial: false }),
                ranged:    new fields.BooleanField({ initial: false }),
                striking:  new fields.BooleanField({ initial: false }),
                weapon:    new fields.BooleanField({ initial: false })
            })
        };
    }
}
```

- [ ] Create `module/datamodels/items/martialarttech-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6MartialartechItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            style:  new fields.StringField({ initial: "" }),
            choice: new fields.StringField({ initial: "" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/contact-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6ContactItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            rating:   new fields.NumberField({ initial: 1 }),
            loyalty:  new fields.NumberField({ initial: 1 }),
            favors:   new fields.NumberField({ initial: 0 }),
            type:     new fields.StringField({ initial: "" }),
            pronouns: new fields.StringField({ initial: "" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/critterpower-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6CritterpowerItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            duration: new fields.StringField({ initial: "instantaneous" }),
            action:   new fields.StringField({ initial: "" }),
            type:     new fields.StringField({ initial: "physical" }),
            range:    new fields.StringField({ initial: "self" })
        };
    }
}
```

---

## Task 5: Complex Item DataModels

Six item types with more fields.

**Files:** Create all 6 in `module/datamodels/items/`

- [ ] Create `module/datamodels/items/spritepower-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6SpritepowerItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            duration:    new fields.StringField({ initial: "sustained" }),
            isSustained: new fields.BooleanField({ initial: false }),
            skill:       new fields.StringField({ initial: "" }),
            skillSpec:   new fields.StringField({ initial: "" }),
            oppAttr1:    new fields.StringField({ initial: "" }),
            oppAttr2:    new fields.StringField({ initial: "" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/complexform-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6ComplexformItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            duration:  new fields.StringField({ initial: "sustained" }),
            fading:    new fields.NumberField({ initial: 3 }),
            skill:     new fields.StringField({ initial: "" }),
            skillSpec: new fields.StringField({ initial: "" }),
            attrib:    new fields.StringField({ initial: "res" }),
            threshold: new fields.NumberField({ initial: 0 }),
            oppAttr1:  new fields.StringField({ initial: "" }),
            oppAttr2:  new fields.StringField({ initial: "" })
        };
    }
}
```

- [ ] Create `module/datamodels/items/software-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6SoftwareItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            type:     new fields.StringField({ initial: "SOFTWARE" }),
            subtype:  new fields.StringField({ initial: "AUTOSOFT" }),
            category: new fields.StringField({ initial: "" }),
            rating:   new fields.NumberField({ initial: 1 })
        };
    }
}
```

- [ ] Create `module/datamodels/items/quality-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6QualityItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            category: new fields.StringField({ initial: "" }),
            level:    new fields.BooleanField({ initial: false }),
            value:    new fields.NumberField({ initial: 0 }),
            explain:  new fields.StringField({ initial: "" }),
            modifier: new fields.ArrayField(new fields.StringField())
        };
    }
}
```

- [ ] Create `module/datamodels/items/ritual-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6RitualItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            threshold: new fields.NumberField({ initial: 5 }),
            features: new fields.SchemaField({
                anchored:      new fields.BooleanField({ initial: false }),
                material_link: new fields.BooleanField({ initial: false }),
                minion:        new fields.BooleanField({ initial: false }),
                spell:         new fields.BooleanField({ initial: false }),
                spotter:       new fields.BooleanField({ initial: false })
            })
        };
    }
}
```

- [ ] Create `module/datamodels/items/spell-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6SpellItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            category:        new fields.StringField({ initial: "health" }),
            duration:        new fields.StringField({ initial: "instantaneous" }),
            drain:           new fields.NumberField({ initial: 1 }),
            type:            new fields.StringField({ initial: "physical" }),
            range:           new fields.StringField({ initial: "self" }),
            damage:          new fields.StringField({ initial: "physical" }),
            combatSpellType: new fields.StringField({ initial: "spells_indirect" }),
            alchemic:        new fields.BooleanField({ initial: false }),
            multiSense:      new fields.BooleanField({ initial: false }),
            isOpposed:       new fields.BooleanField({ initial: true }),
            withEssence:     new fields.BooleanField({ initial: false }),
            wildDie:         new fields.BooleanField({ initial: false }),
            isSustained:     new fields.BooleanField({ initial: false })
        };
    }
}
```

---

## Task 6: Gear DataModel

Supersedes `legacy-gear-item-data.mjs` (which is an empty stub — all fields commented out).

**Files:**
- Create: `module/datamodels/items/gear-item-data.mjs`

- [ ] Create `module/datamodels/items/gear-item-data.mjs`:

```js
import SR6BaseItemData from '../base-item-data.mjs';

export default class SR6GearItemData extends SR6BaseItemData {
    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            type:        new fields.StringField({ initial: "WEAPON_FIREARMS" }),
            subtype:     new fields.StringField({ initial: "" }),
            count:       new fields.NumberField({ initial: 0 }),
            countable:   new fields.BooleanField({ initial: false }),
            availDef:    new fields.StringField({ initial: "" }),
            avail:       new fields.NumberField({ initial: 0 }),
            ammocap:     new fields.NumberField({ initial: 0 }),
            ammocount:   new fields.NumberField({ initial: 0 }),
            ammoLoaded:  new fields.StringField({ initial: "regular" }),
            priceDef:    new fields.StringField({ initial: "" }),
            customName:  new fields.StringField({ initial: "" }),
            usedForPool: new fields.BooleanField({ initial: false }),
            notes:       new fields.HTMLField(),
            accessories: new fields.StringField({ initial: "" }),
            needsRating: new fields.BooleanField({ initial: false }),
            rating:      new fields.NumberField({ initial: 0 }),
            skill:       new fields.StringField({ initial: "" }),
            skillSpec:   new fields.StringField({ initial: "" }),
            dmg:         new fields.NumberField({ initial: 0 }),
            stun:        new fields.BooleanField({ initial: false }),
            dmgDef:      new fields.StringField({ initial: "" }),
            attackRating: new fields.ArrayField(new fields.NumberField({ initial: 0 })),
            modes: new fields.SchemaField({
                BF: new fields.BooleanField({ initial: false }),
                FA: new fields.BooleanField({ initial: false }),
                SA: new fields.BooleanField({ initial: false }),
                SS: new fields.BooleanField({ initial: false })
            }),
            defense:    new fields.NumberField({ initial: 0 }),
            social:     new fields.NumberField({ initial: 0 }),
            essence:    new fields.NumberField({ initial: 0 }),
            capacity:   new fields.NumberField({ initial: 0 }),
            natural:    new fields.BooleanField({ initial: false }),
            devRating:  new fields.NumberField({ initial: 0 }),
            a:          new fields.NumberField({ initial: 0 }),
            s:          new fields.NumberField({ initial: 0 }),
            d:          new fields.NumberField({ initial: 0 }),
            f:          new fields.NumberField({ initial: 0 }),
            progSlots:  new fields.NumberField({ initial: 0 }),
            handlOn:    new fields.NumberField({ initial: 0 }),
            handlOff:   new fields.NumberField({ initial: 0 }),
            accOn:      new fields.NumberField({ initial: 0 }),
            accOff:     new fields.NumberField({ initial: 0 }),
            spdiOn:     new fields.NumberField({ initial: 0 }),
            spdiOff:    new fields.NumberField({ initial: 0 }),
            tspd:       new fields.NumberField({ initial: 0 }),
            bod:        new fields.NumberField({ initial: 0 }),
            arm:        new fields.NumberField({ initial: 0 }),
            pil:        new fields.NumberField({ initial: 0 }),
            sen:        new fields.NumberField({ initial: 0 }),
            sea:        new fields.NumberField({ initial: 0 }),
            vtype:      new fields.StringField({ initial: "" }),
            vehicle: new fields.SchemaField({
                opMode: new fields.StringField({ initial: "manual" })
            }),
            strWeapon: new fields.BooleanField({ initial: false }),
            dualHand:  new fields.BooleanField({ initial: false })
        };
    }
}
```

---

## Task 7: Wire Up Exports and Registration

Update `_module.mjs` to export all new classes, remove the legacy gear export. Update `Shadowrun6.js` to register all DataModels.

**Files:**
- Modify: `module/datamodels/_module.mjs`
- Modify: `module/Shadowrun6.js`
- Delete: `module/datamodels/legacy-gear-item-data.mjs`

- [ ] Replace the full contents of `module/datamodels/_module.mjs`:

```js
// Export Actors
export { default as SR6BaseActorData } from './base-actor-data.mjs';
export { default as SR6SpriteActorData } from './sprite-actor-data.mjs';
export { default as SR6HumanoidActorData } from './actors/humanoid-actor-data.mjs';
export { default as SR6PlayerActorData } from './actors/player-actor-data.mjs';
export { default as SR6NPCActorData } from './actors/npc-actor-data.mjs';
export { default as SR6CritterBaseActorData } from './actors/critter-base-actor-data.mjs';
export { default as SR6CritterActorData } from './actors/critter-actor-data.mjs';
export { default as SR6SpiritActorData } from './actors/spirit-actor-data.mjs';
export { default as SR6VehicleActorData } from './actors/vehicle-actor-data.mjs';

// Export Items
export { default as SR6BaseItemData } from './base-item-data.mjs';
export { default as SR6ModItemData } from './mod-item-data.mjs';
export { default as SR6AdeptpowerItemData } from './items/adeptpower-item-data.mjs';
export { default as SR6ComplexformItemData } from './items/complexform-item-data.mjs';
export { default as SR6ContactItemData } from './items/contact-item-data.mjs';
export { default as SR6CritterpowerItemData } from './items/critterpower-item-data.mjs';
export { default as SR6SpritepowerItemData } from './items/spritepower-item-data.mjs';
export { default as SR6EchoItemData } from './items/echo-item-data.mjs';
export { default as SR6FocusItemData } from './items/focus-item-data.mjs';
export { default as SR6GearItemData } from './items/gear-item-data.mjs';
export { default as SR6LifestyleItemData } from './items/lifestyle-item-data.mjs';
export { default as SR6MartialartstyleItemData } from './items/martialartstyle-item-data.mjs';
export { default as SR6MartialartechItemData } from './items/martialarttech-item-data.mjs';
export { default as SR6MetamagicItemData } from './items/metamagic-item-data.mjs';
export { default as SR6QualityItemData } from './items/quality-item-data.mjs';
export { default as SR6RitualItemData } from './items/ritual-item-data.mjs';
export { default as SR6SinItemData } from './items/sin-item-data.mjs';
export { default as SR6SkillItemData } from './items/skill-item-data.mjs';
export { default as SR6SoftwareItemData } from './items/software-item-data.mjs';
export { default as SR6SpellItemData } from './items/spell-item-data.mjs';

// Export Active Effect
export { default as SR6ActiveEffectData } from './active-effect-data.mjs';

// Export object of fields
export * as fields from './fields/fields.mjs';
```

- [ ] In `module/Shadowrun6.js`, find the two `Object.assign` blocks and replace them:

Find the existing actor block:
```js
Object.assign(CONFIG.Actor.dataModels, {
    sprite: datamodels.SR6SpriteActorData
});
```

Replace with:
```js
Object.assign(CONFIG.Actor.dataModels, {
    Player:  datamodels.SR6PlayerActorData,
    NPC:     datamodels.SR6NPCActorData,
    Critter: datamodels.SR6CritterActorData,
    Spirit:  datamodels.SR6SpiritActorData,
    Vehicle: datamodels.SR6VehicleActorData,
    sprite:  datamodels.SR6SpriteActorData
});
```

Find the existing item block:
```js
Object.assign(CONFIG.Item.dataModels, {
    mod: datamodels.SR6ModItemData
});
```

Replace with:
```js
Object.assign(CONFIG.Item.dataModels, {
    adeptpower:      datamodels.SR6AdeptpowerItemData,
    complexform:     datamodels.SR6ComplexformItemData,
    contact:         datamodels.SR6ContactItemData,
    critterpower:    datamodels.SR6CritterpowerItemData,
    spritepower:     datamodels.SR6SpritepowerItemData,
    echo:            datamodels.SR6EchoItemData,
    focus:           datamodels.SR6FocusItemData,
    gear:            datamodels.SR6GearItemData,
    lifestyle:       datamodels.SR6LifestyleItemData,
    martialartstyle: datamodels.SR6MartialartstyleItemData,
    martialarttech:  datamodels.SR6MartialartechItemData,
    metamagic:       datamodels.SR6MetamagicItemData,
    quality:         datamodels.SR6QualityItemData,
    ritual:          datamodels.SR6RitualItemData,
    sin:             datamodels.SR6SinItemData,
    skill:           datamodels.SR6SkillItemData,
    software:        datamodels.SR6SoftwareItemData,
    spell:           datamodels.SR6SpellItemData,
    mod:             datamodels.SR6ModItemData
});
```

- [ ] Delete `module/datamodels/legacy-gear-item-data.mjs` (it is an empty stub — all fields were commented out):
```bash
rm module/datamodels/legacy-gear-item-data.mjs
```

---

## Task 8: Commit 1 — Deploy and Verify, then Commit 2 — Remove template.json

### Commit 1: All DataModels registered, template.json still present

- [ ] Deploy to the v14 server:
```bash
rsync -av --delete \
  --exclude '.git' --exclude 'node_modules' --exclude 'scripts/screenshots' \
  ./ todd@encoder.local:/opt/share/foundry14/Data/systems/shadowrun6-eden/
```

- [ ] Restart the Foundry service:
```bash
ssh todd@encoder.local 'systemctl --user restart FoundryVTT14.service'
sleep 10
```

- [ ] Run the Playwright verification script:
```bash
node scripts/test-v14.mjs --label stage4-pre --headless
```

Expected output:
```
errors:   0
warnings: 24  (or fewer — template.json deprecation warning should still appear)
```

If `errors > 0`, check `scripts/screenshots/stage4-pre/console.json` for the error messages and fix before proceeding.

- [ ] Stage and commit everything (DataModels + _module.mjs + Shadowrun6.js + legacy deletion):
```bash
git add module/datamodels/ module/Shadowrun6.js
git status  # verify legacy-gear-item-data.mjs shows as deleted
git commit -m "Add TypeDataModel classes for all 23 actor/item types (Stage 4)

Player, NPC, Critter, Spirit, Vehicle actor DataModels and all 18 item type
DataModels. Shared attribute/skill/monitor fields extracted to schema-helpers.mjs.
Field names match template.json exactly; TODO comments mark migration targets.
legacy-gear-item-data.mjs removed (empty stub superseded by SR6GearItemData).
template.json still present as safety net pending final verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

### Commit 2: Delete template.json

- [ ] Delete `template.json`:
```bash
rm template.json
```

- [ ] Deploy again:
```bash
rsync -av --delete \
  --exclude '.git' --exclude 'node_modules' --exclude 'scripts/screenshots' \
  ./ todd@encoder.local:/opt/share/foundry14/Data/systems/shadowrun6-eden/
ssh todd@encoder.local 'systemctl --user restart FoundryVTT14.service'
sleep 10
```

- [ ] Run the final verification:
```bash
node scripts/test-v14.mjs --label stage4-final --headless
```

Expected output:
```
errors:   0
warnings: <24  (the template.json deprecation warning should now be absent)
```

Confirm by checking console.json:
```bash
python3 -c "
import json
data = json.load(open('scripts/screenshots/stage4-final/console.json'))
tpl = [m for m in data if 'template.json' in m['text']]
print('template.json warnings:', len(tpl))
print('Total messages:', len(data))
"
```

Expected: `template.json warnings: 0`

- [ ] Commit the template.json deletion:
```bash
git add template.json
git commit -m "Remove template.json — all types now defined via TypeDataModel (Stage 4)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```
