Step-by-step workflows for adding new item types, mechanics, compendium entries, and features from planning to code.
Ask questions like:
This skill helps you:
File: src/module/system/item/data-models.mjs or equivalent
import { ItemDataModel } from './_item-data-model.mjs';
import { FormulaFamiliar, Dnd35eField } from '...fields/index.mjs';
class SpecialWeapon extends ItemDataModel {
static defineSchema() {
return foundry.utils.mergeObject(super.defineSchema(), {
// Add custom properties
properties: new SchemaField({
specialAbility: new Dnd35eField(new StringField({ initial: 'none' })),
saveDC: new Dnd35eField(new NumberField({ min: 0, initial: 10 })),
specialEffect: new FormulaFamiliar({ label: "Effect Formula" }),
}),
});
}
// Optional: computed properties
get effectiveSpecialAbility() {
return this.properties.specialAbility;
}
}
Checklist:
defineSchema() call super firstDnd35eFieldFormulaFamiliarinitial or required: falseFile: system.json or type registration code
// If using system.json (Phase 4)
{
"Item": {
"types": ["weapon", "specialWeapon"], // Add new type
"specialized": {
"specialWeapon": {
"class": "SpecialWeapon",
"dataModel": "SpecialWeapon"
}
}
}
}
// Or dynamic registration
CONFIG.Item.documentClass.register('specialWeapon', SpecialWeapon);
File: src/vue/sheets/item/special-weapon-sheet.vue
<script setup>
import ItemSheetBase from './item-sheet-base.vue';
import FormGroupSection from '../../form/form-group-section.vue';
import StringFormGroup from '../../form/string-form-group.vue';
import NumberFormGroup from '../../form/number-form-group.vue';
import FormulaFormGroup from '../../form/formula-form-group.vue';
defineProps({ documentSheet: Object });
const emit = defineEmits(['update']);
const handleUpdate = (path, value) => {
emit('update', { [path]: value });
};
</script>
<template>
<ItemSheetBase :document-sheet="documentSheet">
<FormGroupSection label="Special Abilities" field-path="system.properties">
<StringFormGroup
label="Ability"
:value="documentSheet.system.properties.specialAbility"
field-path="system.properties.specialAbility"
@update="(val) => handleUpdate('system.properties.specialAbility', val)"
/>
<NumberFormGroup
label="Save DC"
:value="documentSheet.system.properties.saveDC"
field-path="system.properties.saveDC"
:min="0"
@update="(val) => handleUpdate('system.properties.saveDC', val)"
/>
<FormulaFormGroup
label="Effect"
:value="documentSheet.system.properties.specialEffect"
field-path="system.properties.specialEffect"
@update="(val) => handleUpdate('system.properties.specialEffect', val)"
/>
</FormGroupSection>
</ItemSheetBase>
</template>
Checklist:
File: packs/items-special-weapons.json or database pack
[
{
"_id": "special-weapon-01",
"name": "Frost Blade",
"type": "specialWeapon",
"data": {
"properties": {
"specialAbility": "frost",
"saveDC": 15,
"specialEffect": "2d6 + #context.enhancement"
}
}
}
]
Or import via script:
const item = await Item.create({
name: "Frost Blade",
type: "specialWeapon",
system: {
properties: {
specialAbility: "frost",
saveDC: 15,
specialEffect: "2d6 + #context.enhancement" // #context.X syntax resolves from evaluation scope
}
}
});
// Add to compendium pack
await game.packs.get('dnd35e.items-special-weapons').importDocument(item);
Checklist:
// Test: Can create item of new type
const item = await Item.create({
name: "Test",
type: "specialWeapon"
});
// Test: Can access schema fields
console.assert(item.system.properties.specialAbility !== undefined);
// Test: Can update fields
await item.update({ 'system.properties.specialAbility': 'fire' });
// Test: Sheet renders
item.sheet.render(true);
// Test: Formulas evaluate
const scope = { enhancement: 1 };
const result = await item.system.formula('properties.specialEffect', scope);
console.assert(result > 0);
Document what "Defensive Stance" does:
Option A: Active Effect (Simple, reusable)
// Already built-in, GM just creates AE with changes
// changes: [{ key: "system.ac.base", mode: "ADD", value: 2 }]
Option B: Character Sheet Toggle (Complex, custom logic)
// Add to actor schema
class Character extends ActorDataModel {
static defineSchema() {
return {
defenses: new SchemaField({
defensiveStance: new BooleanField({ initial: false })
})
};
}
// Get AC bonus from stance
get defensiveStanceBonus() {
return this.defenses.defensiveStance ? 2 : 0;
}
}
Option C: Feat or Feature (Role-based)
// Add to item type "Feature"
// "Defensive Stance" replaces the feature name
// Feature grants active effect via component chain
Add to actor data model if not in Option A:
class Character extends ActorDataModel {
static defineSchema() {
return foundry.utils.mergeObject(super.defineSchema(), {
defenses: new SchemaField({
defensiveStance: new Dnd35eField(
new BooleanField({ initial: false })
),
}),
});
}
}
<script setup>
import FormGroupSection from '../../form/form-group-section.vue';
import ToggleFormGroup from '../../form/toggle-form-group.vue';
const emit = defineEmits(['update']);
</script>
<template>
<FormGroupSection label="Defenses" field-path="system.defenses">
<ToggleFormGroup
label="Defensive Stance"
:value="character.system.defenses.defensiveStance"
field-path="system.defenses.defensiveStance"
@update="(val) => emit('update', { 'system.defenses.defensiveStance': val })"
/>
</FormGroupSection>
</template>
// In ActorDataModel
get totalAC() {
const base = this.ac.base || 10;
const defensiveBonus = this.defenses.defensiveStance ? 2 : 0;
const aoeBonus = this.getActiveEffectBonus('ac');
return base + defensiveBonus + aoeBonus;
}
// Or in getter if it's computed during render
get acBonuses() {
return {
base: this.ac.base,
defensive: this.defenses.defensiveStance ? 2 : 0,
effects: this.getActiveEffectBonus('ac'),
};
}
// Create test actor
const actor = await Actor.create({ name: "Test", type: "character" });
// Test: Can toggle stance
await actor.update({ 'system.defenses.defensiveStance': true });
console.assert(actor.system.defenses.defensiveStance === true);
// Test: AC changes
const baseAC = actor.totalAC;
await actor.update({ 'system.defenses.defensiveStance': true });
const stanceAC = actor.totalAC;
console.assert(stanceAC === baseAC + 2);
// Test: UI renders
actor.sheet.render(true);
Workflow:
.db file// Use script to batch import
const entries = [
{
name: "Longsword",
type: "weapon",
system: { damageFormula: "[email protected]" }
},
{
name: "Armor Proficiency (Light)",
type: "feat",
system: { level: 1 }
}
];
for (const entry of entries) {
const doc = await Item.create(entry);
await game.packs.get('dnd35e.weapons').importDocument(doc);
}