diff --git a/icons/LICENSE b/icons/LICENSE index 5891970d45..794de2a1a8 100644 --- a/icons/LICENSE +++ b/icons/LICENSE @@ -12,6 +12,7 @@ The dnd5e system for Foundry Virtual Tabletop includes icon artwork licensed fro /svg/item-grant.svg - "White book" by Willdabeast under CC BY 3.0 /svg/scale-value.svg - "Dice target" by Delapouite under CC BY 3.0 /svg/size.svg - "Body height" by Delapouite under CC BY 3.0 +/svg/starting-equipment.svg - "Battle gear" by Lorc under CC BY 3.0 /svg/trait.svg - "Scroll unfurled" by Lorc under CC BY 3.0 /svg/trait-armor-proficiencies.svg - "Leather armor" by Delapouite under CC BY 3.0 /svg/trait-damage-immunities.svg - "Aura" by Lorc under CC BY 3.0 diff --git a/icons/svg/starting-equipment.svg b/icons/svg/starting-equipment.svg new file mode 100644 index 0000000000..b561152bbf --- /dev/null +++ b/icons/svg/starting-equipment.svg @@ -0,0 +1 @@ + diff --git a/lang/en.json b/lang/en.json index 927ac3f5ef..2c20451b58 100644 --- a/lang/en.json +++ b/lang/en.json @@ -175,6 +175,7 @@ "DND5E.AdvancementConfiguredComplete": "Fully Configured", "DND5E.AdvancementConfiguredIncomplete": "Not Configured", "DND5E.AdvancementControlCreate": "Create Advancement", +"DND5E.AdvancementControlConfigure": "Configure {name}", "DND5E.AdvancementControlDelete": "Delete Advancement", "DND5E.AdvancementControlDuplicate": "Duplicate Advancement", "DND5E.AdvancementControlEdit": "Edit Advancement", @@ -1373,6 +1374,25 @@ "DND5E.SpellUnprepared": "Unprepared", "DND5E.SpellUsage": "Spell Usage", "DND5E.Spellbook": "Spellbook", +"DND5E.StartingEquipment": { + "Title": "Starting Equipment", + "Choice": { + "Armor": "Choose Armor", + "Focus": "Choose Spellcasting Focus", + "Tool": "Choose Tool", + "Weapon": "Choose Weapon" + }, + "IfProficient": "If Proficient", + "Operator": { + "AND": "All", + "OR": "One" + }, + "SpecificItem": "Specific Item", + "Wealth": { + "Label": "Starting Wealth", + "Hint": "Formula in GP that can be used in place of starting equipment." + } +}, "DND5E.SubclassIdentifierHint": "This identifier should match the identifier on the parent class to ensure they are properly linked.", "DND5E.SubclassAssignmentError": "{class} already has a subclass. Remove the existing '{subclass}' subclass before adding a new one.", "DND5E.SubclassDuplicateError": "A subclass with the identifier {identifier} already exists on this actor.", @@ -1589,6 +1609,7 @@ "DND5E.WarnCantAddMultipleAdvancements": "It is not currently possible to add multiple items with advancements to an actor at the same time. Please add them individually.", "DND5E.WarnMultipleArmor": "More than one suit of armor equipped, AC calculation may be incorrect.", "DND5E.WarnMultipleShields": "More than one shield equipped, AC calculation may be incorrect.", +"DND5E.WeaponCategory": "{category} Weapon", "DND5E.WeaponImprov": "Improvised", "DND5E.WeaponMartialM": "Martial Melee", "DND5E.WeaponMartialProficiency": "Martial", diff --git a/module/applications/advancement/_module.mjs b/module/applications/advancement/_module.mjs index 9ef0fce0e5..332fd47f75 100644 --- a/module/applications/advancement/_module.mjs +++ b/module/applications/advancement/_module.mjs @@ -17,5 +17,6 @@ export {default as ScaleValueConfig} from "./scale-value-config.mjs"; export {default as ScaleValueFlow} from "./scale-value-flow.mjs"; export {default as SizeConfig} from "./size-config.mjs"; export {default as SizeFlow} from "./size-flow.mjs"; +export {default as StartingEquipmentConfig} from "./starting-equipment-config.mjs"; export {default as TraitConfig} from "./trait-config.mjs"; export {default as TraitFlow} from "./trait-flow.mjs"; diff --git a/module/applications/advancement/starting-equipment-config.mjs b/module/applications/advancement/starting-equipment-config.mjs new file mode 100644 index 0000000000..80c6c83440 --- /dev/null +++ b/module/applications/advancement/starting-equipment-config.mjs @@ -0,0 +1,23 @@ +import AdvancementConfig from "./advancement-config.mjs"; + +/** + * Configuration application for Starting Equipment. + */ +export default class StartingEquipmentConfig extends AdvancementConfig { + + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["dnd5e", "advancement", "starting-equipment"], + template: "systems/dnd5e/templates/advancement/starting-equipment-config.hbs" + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + getData(options={}) { + const context = super.getData(options); + return context; + } +} diff --git a/module/applications/item/_module.mjs b/module/applications/item/_module.mjs index 522a3394ca..bacc98c9a1 100644 --- a/module/applications/item/_module.mjs +++ b/module/applications/item/_module.mjs @@ -4,4 +4,5 @@ export {default as ItemDirectory5e} from "./item-directory.mjs"; export {default as ItemSheet5e} from "./item-sheet.mjs"; export {default as AbilityUseDialog} from "./ability-use-dialog.mjs"; +export {default as StartingEquipmentConfig} from "./starting-equipment-config.mjs"; export {default as SummoningConfig} from "./summoning-config.mjs"; diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index e6c3c29550..c866cf0bb5 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -8,6 +8,7 @@ import AdvancementMigrationDialog from "../advancement/advancement-migration-dia import Accordion from "../accordion.mjs"; import EffectsElement from "../components/effects.mjs"; import SourceConfig from "../source-config.mjs"; +import StartingEquipmentConfig from "./starting-equipment-config.mjs"; import SummoningConfig from "./summoning-config.mjs"; /** @@ -203,6 +204,8 @@ export default class ItemSheet5e extends ItemSheet { }; } + const supplementalAdvancement = item.system.supplementalAdvancement ?? {}; + // All other advancements by level for ( let [level, advancements] of Object.entries(item.advancement.byLevel) ) { if ( !configMode ) advancements = advancements.filter(a => a.appliesToClass); @@ -215,6 +218,7 @@ export default class ItemSheet5e extends ItemSheet { summary: advancement.summaryForLevel(level, { configMode }), configured: advancement.configuredForLevel(level) })); + items.push(...(supplementalAdvancement[level] ?? [])); if ( !items.length ) continue; advancement[level] = { items: items.sort((a, b) => a.order.localeCompare(b.order, game.i18n.lang)), @@ -562,6 +566,9 @@ export default class ItemSheet5e extends ItemSheet { case "source": app = new SourceConfig(this.item, { keyPath: "system.source" }); break; + case "starting-equipment": + app = new StartingEquipmentConfig(this.item); + break; case "summoning": app = new SummoningConfig(this.item); break; diff --git a/module/applications/item/starting-equipment-config.mjs b/module/applications/item/starting-equipment-config.mjs new file mode 100644 index 0000000000..51166989ae --- /dev/null +++ b/module/applications/item/starting-equipment-config.mjs @@ -0,0 +1,40 @@ +/** + * Configuration application for Starting Equipment. + */ +export default class StartingEquipmentConfig extends DocumentSheet { + + /** @inheritDoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["dnd5e", "starting-equipment"], + dragDrop: [{ dropSelector: "form" }], + template: "systems/dnd5e/templates/apps/starting-equipment-config.hbs", + width: 400, + height: "auto", + sheetConfig: false, + closeOnSubmit: false, + submitOnChange: true, + submitOnClose: true + }); + } + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + async getData(options={}) { + const context = await super.getData(options); + context.startingEquipment = this.document.system.startingEquipment; + return context; + } + + /* -------------------------------------------- */ + /* Event Handling */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _updateObject(event, formData) { + console.log(event, formData); + } +} diff --git a/module/data/item/_module.mjs b/module/data/item/_module.mjs index ee19a05908..e0cf74eeea 100644 --- a/module/data/item/_module.mjs +++ b/module/data/item/_module.mjs @@ -34,6 +34,7 @@ export {default as ItemDescriptionTemplate} from "./templates/item-description.m export {default as ItemTypeTemplate} from "./templates/item-type.mjs"; export {default as MountableTemplate} from "./templates/mountable.mjs"; export {default as PhysicalItemTemplate} from "./templates/physical-item.mjs"; +export * as startingEquipment from "./templates/starting-equipment.mjs"; export const config = { background: BackgroundData, diff --git a/module/data/item/background.mjs b/module/data/item/background.mjs index 0eef4a8299..f7cdabd4ae 100644 --- a/module/data/item/background.mjs +++ b/module/data/item/background.mjs @@ -1,14 +1,16 @@ import { ItemDataModel } from "../abstract.mjs"; import { AdvancementField } from "../fields.mjs"; import ItemDescriptionTemplate from "./templates/item-description.mjs"; +import StartingEquipmentTemplate from "./templates/starting-equipment.mjs"; /** * Data definition for Background items. * @mixes ItemDescriptionTemplate + * @mixes StartingEquipmentTemplate * * @property {object[]} advancement Advancement objects for this background. */ -export default class BackgroundData extends ItemDataModel.mixin(ItemDescriptionTemplate) { +export default class BackgroundData extends ItemDataModel.mixin(ItemDescriptionTemplate, StartingEquipmentTemplate) { /** @inheritdoc */ static defineSchema() { return this.mergeSchema(super.defineSchema(), { @@ -23,6 +25,18 @@ export default class BackgroundData extends ItemDataModel.mixin(ItemDescriptionT singleton: true }, {inplace: false})); + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * Fetch additional advancement entries. + * @type {object[]} + */ + get supplementalAdvancement() { + return { 1: [this.startingEquipmentAdvancement] }; + } + /* -------------------------------------------- */ /* Socket Event Handlers */ /* -------------------------------------------- */ diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index da4f97f367..50fb104159 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -1,11 +1,15 @@ import TraitAdvancement from "../../documents/advancement/trait.mjs"; import { ItemDataModel } from "../abstract.mjs"; -import { AdvancementField, IdentifierField } from "../fields.mjs"; +import { AdvancementField, FormulaField, IdentifierField } from "../fields.mjs"; import ItemDescriptionTemplate from "./templates/item-description.mjs"; +import StartingEquipmentTemplate from "./templates/starting-equipment.mjs"; + +const { ArrayField, NumberField, SchemaField, StringField } = foundry.data.fields; /** * Data definition for Class items. * @mixes ItemDescriptionTemplate + * @mixes StartingEquipmentTemplate * * @property {string} identifier Identifier slug for this class. * @property {number} levels Current number of levels in this class. @@ -15,32 +19,46 @@ import ItemDescriptionTemplate from "./templates/item-description.mjs"; * @property {object} spellcasting Details on class's spellcasting ability. * @property {string} spellcasting.progression Spell progression granted by class as from `DND5E.spellProgression`. * @property {string} spellcasting.ability Ability score to use for spellcasting. + * @property {string} wealth Formula used to determine starting wealth. */ -export default class ClassData extends ItemDataModel.mixin(ItemDescriptionTemplate) { +export default class ClassData extends ItemDataModel.mixin(ItemDescriptionTemplate, StartingEquipmentTemplate) { /** @inheritdoc */ static defineSchema() { return this.mergeSchema(super.defineSchema(), { identifier: new IdentifierField({required: true, label: "DND5E.Identifier"}), - levels: new foundry.data.fields.NumberField({ + levels: new NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1, label: "DND5E.ClassLevels" }), - hitDice: new foundry.data.fields.StringField({ + hitDice: new StringField({ required: true, initial: "d6", blank: false, label: "DND5E.HitDice", validate: v => /d\d+/.test(v), validationError: "must be a dice value in the format d#" }), - hitDiceUsed: new foundry.data.fields.NumberField({ + hitDiceUsed: new NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, label: "DND5E.HitDiceUsed" }), - advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}), - spellcasting: new foundry.data.fields.SchemaField({ - progression: new foundry.data.fields.StringField({ + advancement: new ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}), + spellcasting: new SchemaField({ + progression: new StringField({ required: true, initial: "none", blank: false, label: "DND5E.SpellProgression" }), - ability: new foundry.data.fields.StringField({required: true, label: "DND5E.SpellAbility"}) - }, {label: "DND5E.Spellcasting"}) + ability: new StringField({required: true, label: "DND5E.SpellAbility"}) + }, {label: "DND5E.Spellcasting"}), + wealth: new FormulaField({label: "DND5E.StartingEquipment.Wealth.Label"}) }); } + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * Fetch additional advancement entries. + * @type {object[]} + */ + get supplementalAdvancement() { + return { 1: [this.startingEquipmentAdvancement] }; + } + /* -------------------------------------------- */ /* Data Preparation */ /* -------------------------------------------- */ diff --git a/module/data/item/templates/starting-equipment.mjs b/module/data/item/templates/starting-equipment.mjs new file mode 100644 index 0000000000..ea67efc1f2 --- /dev/null +++ b/module/data/item/templates/starting-equipment.mjs @@ -0,0 +1,197 @@ +import { formatNumber } from "../../../utils.mjs"; +import SystemDataModel from "../../abstract.mjs"; + +const { + ArrayField, BooleanField, DocumentIdField, EmbeddedDataField, IntegerSortField, NumberField, StringField +} = foundry.data.fields; + +/** + * Data model template representing a background & class's starting equipment. + * + * @property {EquipmentEntryData[]} startingEquipment Different equipment entries that will be granted. + */ +export default class StartingEquipmentTemplate extends SystemDataModel { + static defineSchema() { + return { + startingEquipment: new ArrayField(new EmbeddedDataField(EquipmentEntryData), {required: true}) + }; + } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * Fake advancement for displaying starting equipment within the advancement list. + * @type {object} + */ + get startingEquipmentAdvancement() { + return { + id: "starting-equipment", + order: "0150", + title: game.i18n.localize("DND5E.StartingEquipment.Title"), + icon: "systems/dnd5e/icons/svg/starting-equipment.svg", + summary: this.startingEquipmentDescription, + supplemental: true, + configured: true + }; + } + + /* -------------------------------------------- */ + + /** + * HTML formatted description of the starting equipment on this item. + * @type {string} + */ + get startingEquipmentDescription() { + const topLevel = this.startingEquipment.filter(e => !e.group); + if ( !topLevel.length ) return ""; + + // If more than one entry, display as an unordered list (like for classes) + if ( topLevel.length > 1 ) return `