| name | implementation-guide |
| description | Step-by-step workflows for adding new item types, mechanics, compendium entries, and features from planning to code. |
Implementation Guide Skill
Using This Skill
Ask questions like:
- "How do I add a new item type?"
- "What steps to implement a new mechanic?"
- "How do I add data to an existing item type?"
- "What's the process for compendium entries?"
- "How do I implement a spell that needs special handling?"
This skill helps you:
- Follow systematic steps for adding features
- Understand what changes in each layer (schema, sheet, mechanics)
- Test thoroughly at each phase
- Organize workflow to avoid rework
Phase: Add a New Item Type
Step 1: Define the Schema
File: src/module/system/item/data-models.mjs or equivalent
import { ItemDataModel } from './_item-data-model.mjs';
import { FormulaFamiliar } from '...fields/index.mjs';
class SpecialWeapon extends ItemDataModel {
static defineSchema() {
return foundry.utils.mergeObject(super.defineSchema(), {
properties: new SchemaField({
specialAbility: new StringField({ initial: 'none' }),
saveDC: new NumberField({ min: 0, initial: 10 }),
specialEffect: new FormulaFamiliar({ label: "Effect Formula" }),
}),
});
}
get effectiveSpecialAbility() {
return this.properties.specialAbility;
}
}
Checklist:
Step 2: Register the Type
File: system.json or type registration code
{
"Item": {
"types": ["weapon", "specialWeapon"],
"specialized": {
"specialWeapon": {
"class": "SpecialWeapon",
"dataModel": "SpecialWeapon"
}
}
}
}
CONFIG.Item.documentClass.register('specialWeapon', SpecialWeapon);
Step 3: Create Sheet Component
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:
Step 4: Add Compendium Entry (If Core Content)
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"
}
}
});
await game.packs.get('dnd35e.items-special-weapons').importDocument(item);
Checklist:
Step 5: Test Schema & Type
const item = await Item.create({
name: "Test",
type: "specialWeapon"
});
console.assert(item.system.properties.specialAbility !== undefined);
await item.update({ 'system.properties.specialAbility': 'fire' });
item.sheet.render(true);
const scope = { enhancement: 1 };
const result = await item.system.formula('properties.specialEffect', scope);
console.assert(result > 0);
Phase: Add Mechanic (e.g., "Defensive Stance")
Step 1: Define Mechanic Rules
Document what "Defensive Stance" does:
- Grants +2 AC bonus
- Lasts 1 round
- Ends if you move
- Can be toggled each turn
- Stacks with shield bonus but not other AC bonuses
Step 2: Choose Implementation Method
Option A: Active Effect (Simple, reusable)
Option B: Character Sheet Toggle (Complex, custom logic)
class Character extends ActorDataModel {
static defineSchema() {
return {
defenses: new SchemaField({
defensiveStance: new BooleanField({ initial: false })
})
};
}
get defensiveStanceBonus() {
return this.defenses.defensiveStance ? 2 : 0;
}
}
Option C: Feat or Feature (Role-based)
Step 3: Implement in Schema Layer
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 BooleanField({ initial: false }),
}),
});
}
}
Step 4: Add UI (Sheet Component)
<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>
Step 5: Add Mechanics (Derived Values)
get totalAC() {
const base = this.ac.base || 10;
const defensiveBonus = this.defenses.defensiveStance ? 2 : 0;
const aoeBonus = this.getActiveEffectBonus('ac');
return base + defensiveBonus + aoeBonus;
}
get acBonuses() {
return {
base: this.ac.base,
defensive: this.defenses.defensiveStance ? 2 : 0,
effects: this.getActiveEffectBonus('ac'),
};
}
Step 6: Test Implementation
const actor = await Actor.create({ name: "Test", type: "character" });
await actor.update({ 'system.defenses.defensiveStance': true });
console.assert(actor.system.defenses.defensiveStance === true);
const baseAC = actor.totalAC;
await actor.update({ 'system.defenses.defensiveStance': true });
const stanceAC = actor.totalAC;
console.assert(stanceAC === baseAC + 2);
actor.sheet.render(true);
Phase: Add Compendium Entries
For Simple Items
Workflow:
- Create item via sheet
- Edit data as needed
- Right-click item → "To Compendium"
- Select pack
- Item added to
.db file
For Complex Entries (With Relationships)
const entries = [
{
name: "Longsword",
type: "weapon",
system: { damageFormula: "1d8+@str.mod" }
},
{
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);
}
Workflow Checklists
New Item Type (Complete)
New Mechanic
New Compendium Entries
Related Skills
- foundry-reference: API details for each step
- phase-reference: Which phase covers this feature
- system-comparison: How other systems handle similar