Skip to content

Commit

Permalink
[#342] Add Race type, update Movement & Type config to work with items
Browse files Browse the repository at this point in the history
  • Loading branch information
arbron committed Oct 20, 2023
1 parent 64a4270 commit 8a76cd7
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 26 deletions.
12 changes: 12 additions & 0 deletions dnd5e.css
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,18 @@ h5 {
.dnd5e.sheet.item .sheet-body .item-properties [name="system.price.denomination"] {
border: none;
}
.dnd5e.sheet.item .form-group .config-button,
.dnd5e.sheet.item h4 .config-button {
color: #4b4a44;
font-size: 10px;
font-weight: normal;
opacity: 0;
}
.dnd5e.sheet.item .form-group:hover .config-button,
.dnd5e.sheet.item h4:hover .config-button {
display: inline;
opacity: 1;
}
.dnd5e.sheet.item .details input[type="text"],
.dnd5e.sheet.item .details input[type="number"],
.dnd5e.sheet.item .details select {
Expand Down
5 changes: 5 additions & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"ITEM.TypeFeaturePl": "Features",
"ITEM.TypeLoot": "Loot",
"ITEM.TypeLootPl": "Loot",
"ITEM.TypeRace": "Race",
"ITEM.TypeRacePl": "Races",
"ITEM.TypeSpell": "Spell",
"ITEM.TypeSpellPl": "Spells",
"ITEM.TypeSubclass": "Subclass",
Expand All @@ -57,6 +59,8 @@
"TYPES.Item.featurePl": "Features",
"TYPES.Item.loot": "Loot",
"TYPES.Item.lootPl": "Loot",
"TYPES.ITEM.race": "Race",
"TYPES.ITEM.racePl": "Races",
"TYPES.Item.spell": "Spell",
"TYPES.Item.spellPl": "Spells",
"TYPES.Item.subclass": "Subclass",
Expand Down Expand Up @@ -866,6 +870,7 @@
"DND5E.PropertyTotal": "Total",
"DND5E.Quantity": "Quantity",
"DND5E.Race": "Race",
"DND5E.RaceName": "Race Name",
"DND5E.RacialTraits": "Racial Traits",
"DND5E.Range": "Range",
"DND5E.RangeDistance": "Distance",
Expand Down
14 changes: 14 additions & 0 deletions less/items.less
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@
}
}

.form-group, h4 {
.config-button {
color: @colorOlive;
font-size: 10px;
font-weight: normal;
opacity: 0;
}

&:hover .config-button {
display: inline;
opacity: 1;
}
}

/* ----------------------------------------- */
/* Item Details Form */
/* ----------------------------------------- */
Expand Down
20 changes: 12 additions & 8 deletions module/applications/actor/movement-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,37 @@ import BaseConfigSheet from "./base-config.mjs";
*/
export default class ActorMovementConfig extends BaseConfigSheet {

/** @override */
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["dnd5e"],
template: "systems/dnd5e/templates/apps/movement-config.hbs",
width: 300,
height: "auto"
height: "auto",
sheetConfig: false
});
}

/* -------------------------------------------- */

/** @override */
/** @inheritdoc */
get title() {
return `${game.i18n.localize("DND5E.MovementConfig")}: ${this.document.name}`;
}

/* -------------------------------------------- */

/** @override */
/** @inheritdoc */
getData(options={}) {
const source = this.document.toObject();
const isActor = this.document instanceof Actor;
const keyPath = isActor ? "system.attributes.movement" : "system.movement";

// Current movement values
const movement = source.system.attributes?.movement || {};
const movement = foundry.utils.getProperty(source, keyPath) ?? {};
for ( let [k, v] of Object.entries(movement) ) {
if ( ["units", "hover"].includes(k) ) continue;
movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : null;
}

// Allowed speeds
Expand All @@ -53,8 +56,9 @@ export default class ActorMovementConfig extends BaseConfigSheet {
speeds,
movement,
selectUnits: source.type !== "group",
canHover: source.type !== "group",
units: CONFIG.DND5E.movementUnits
canHover: isActor && (source.type !== "group"),
units: CONFIG.DND5E.movementUnits,
keyPath
};
}
}
30 changes: 21 additions & 9 deletions module/applications/actor/type-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Actor5e from "../../documents/actor/actor.mjs";
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
*/
export default class ActorTypeConfig extends FormApplication {
export default class ActorTypeConfig extends DocumentSheet {

/** @inheritDoc */
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["dnd5e", "actor-type", "trait-selector"],
Expand All @@ -15,31 +15,41 @@ export default class ActorTypeConfig extends FormApplication {
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
maximum: null,
sheetConfig: false
});
}

/* -------------------------------------------- */

/** @inheritDoc */
/** @inheritdoc */
get title() {
return `${game.i18n.localize("DND5E.CreatureTypeTitle")}: ${this.object.name}`;
}

/* -------------------------------------------- */

/** @override */
/** @inheritdoc */
get id() {
return `actor-type-${this.object.id}`;
}

/* -------------------------------------------- */

/** @override */
getData(options={}) {
/**
* Key path to where the type is stored on the document.
* @type {string}
*/
get keyPath() {
return this.document instanceof Actor ? "system.details.type" : "system.type";
}

/* -------------------------------------------- */

/** @inheritdoc */
getData(options={}) {
// Get current value or new default
let attr = foundry.utils.getProperty(this.object.system, "details.type");
let attr = foundry.utils.getProperty(this.object, this.keyPath);
if ( foundry.utils.getType(attr) !== "Object" ) attr = {
value: (attr in CONFIG.DND5E.creatureTypes) ? attr : "humanoid",
subtype: "",
Expand All @@ -64,6 +74,8 @@ export default class ActorTypeConfig extends FormApplication {
label: game.i18n.localize("DND5E.CreatureTypeSelectorCustom"),
chosen: attr.value === "custom"
},
showCustom: Object.hasOwn(attr, "custom"),
showSwarm: Object.hasOwn(attr, "swarm"),
subtype: attr.subtype,
swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.DND5E.actorSizes)).reverse().reduce((obj, e) => {
Expand All @@ -79,7 +91,7 @@ export default class ActorTypeConfig extends FormApplication {
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({"system.details.type": typeObject});
return this.object.update({[this.keyPath]: typeObject});
}

/* -------------------------------------------- */
Expand Down
27 changes: 27 additions & 0 deletions module/applications/item/item-sheet.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import ActorMovementConfig from "../actor/movement-config.mjs";
import ActorTypeConfig from "../actor/type-config.mjs";
import AdvancementManager from "../advancement/advancement-manager.mjs";
import AdvancementMigrationDialog from "../advancement/advancement-migration-dialog.mjs";
import TraitSelector from "../trait-selector.mjs";
import ActiveEffect5e from "../../documents/active-effect.mjs";
import * as Trait from "../../documents/actor/trait.mjs";


/**
* Override and extend the core ItemSheet implementation to handle specific item types.
*/
Expand Down Expand Up @@ -425,6 +428,7 @@ export default class ItemSheet5e extends ItemSheet {
activateListeners(html) {
super.activateListeners(html);
if ( this.isEditable ) {
html.find(".config-button").click(this._onConfigMenu.bind(this));
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector").click(this._onConfigureTraits.bind(this));
html.find(".effect-control").click(ev => {
Expand Down Expand Up @@ -486,6 +490,28 @@ export default class ItemSheet5e extends ItemSheet {
];
}

/* -------------------------------------------- */
/**
* Handle spawning the configuration applications.
* @param {Event} event The click event which originated the selection.
* @private
*/
_onConfigMenu(event) {
event.preventDefault();
event.stopPropagation();
const button = event.currentTarget;
let app;
switch ( button.dataset.action ) {
case "movement":
app = new ActorMovementConfig(this.item);
break;
case "type":
app = new ActorTypeConfig(this.item);
break;
}
app?.render(true);
}

/* -------------------------------------------- */

/**
Expand Down Expand Up @@ -514,6 +540,7 @@ export default class ItemSheet5e extends ItemSheet {
return this.item.update({"system.damage.parts": damage.parts});
}
}

/* -------------------------------------------- */

/** @inheritdoc */
Expand Down
3 changes: 3 additions & 0 deletions module/data/item/_module.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ContainerData from "./container.mjs";
import EquipmentData from "./equipment.mjs";
import FeatData from "./feat.mjs";
import LootData from "./loot.mjs";
import RaceData from "./race.mjs";
import SpellData from "./spell.mjs";
import SubclassData from "./subclass.mjs";
import ToolData from "./tool.mjs";
Expand All @@ -18,6 +19,7 @@ export {
EquipmentData,
FeatData,
LootData,
RaceData,
SpellData,
SubclassData,
ToolData,
Expand All @@ -38,6 +40,7 @@ export const config = {
equipment: EquipmentData,
feat: FeatData,
loot: LootData,
race: RaceData,
spell: SpellData,
subclass: SubclassData,
tool: ToolData,
Expand Down
73 changes: 73 additions & 0 deletions module/data/item/race.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import SystemDataModel from "../abstract.mjs";
import { AdvancementField, IdentifierField } from "../fields.mjs";
import ItemDescriptionTemplate from "./templates/item-description.mjs";

/**
* Data definition for Race items.
* @mixes ItemDescriptionTemplate
*
* @property {string} identifier Identifier slug for this race.
* @property {object[]} advancement Advancement objects for this race.
* @property {number} darkvision Darkvision distance.
* @property {object} movement
* @property {number} movement.burrow
* @property {number} movement.climb
* @property {number} movement.fly
* @property {number} movement.swim
* @property {number} movement.walk
* @property {string} movement.units Units used to measure movement.
* @property {object} type
* @property {string} type.value Creature type (e.g. "humanoid", "construct").
* @property {string} type.subtype Additional type data (e.g. "elf" for Eladrin)
*/
export default class RaceData extends SystemDataModel.mixin(ItemDescriptionTemplate) {
/** @inheritdoc */
static defineSchema() {
return this.mergeSchema(super.defineSchema(), {
identifier: new IdentifierField({label: "DND5E.Identifier"}),
advancement: new foundry.data.fields.ArrayField(new AdvancementField(), {label: "DND5E.AdvancementTitle"}),
darkvision: new foundry.data.fields.NumberField({min: 0, integer: true, label: "DND5E.SenseDarkvision"}),
movement: new foundry.data.fields.SchemaField({
burrow: new foundry.data.fields.NumberField({
initial: null, min: 0, integer: true, label: "DND5E.MovementBurrow"
}),
climb: new foundry.data.fields.NumberField({
initial: null, min: 0, integer: true, label: "DND5E.MovementClimb"
}),
fly: new foundry.data.fields.NumberField({
initial: null, min: 0, integer: true, label: "DND5E.MovementFly"
}),
swim: new foundry.data.fields.NumberField({
initial: null, min: 0, integer: true, label: "DND5E.MovementSwim"
}),
walk: new foundry.data.fields.NumberField({
initial: 30, min: 0, integer: true, label: "DND5E.MovementWalk"
}),
units: new foundry.data.fields.StringField({initial: "ft", label: "DND5E.MovementUnits"})
}),
type: new foundry.data.fields.SchemaField({
value: new foundry.data.fields.StringField({initial: "humanoid", label: "DND5E.CreatureType"}),
subtype: new foundry.data.fields.StringField({label: "DND5E.CreatureTypeSelectorSubtype"})
})
});
}

/* -------------------------------------------- */

prepareDerivedData() {
const labels = this.parent.labels ??= {};

// Prepare type label
labels.type = game.i18n.localize(CONFIG.DND5E.creatureTypes[this.type.value]);
if ( this.type.subtype ) labels.type += ` (${this.type.subtype})`;

// Prepare movement labels
labels.movement ??= {};
const units = CONFIG.DND5E.movementUnits[this.movement.units];
for ( const [key, label] of Object.entries(CONFIG.DND5E.movementTypes) ) {
const value = this.movement[key];
if ( !value ) continue;
labels.movement[key] = `${label} ${value} ${units}`;
}
}
}
2 changes: 1 addition & 1 deletion module/documents/advancement/advancement.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default class Advancement extends BaseAdvancement {
title: game.i18n.localize("DND5E.AdvancementTitle"),
hint: "",
multiLevel: false,
validItemTypes: new Set(["background", "class", "subclass"]),
validItemTypes: new Set(["background", "class", "race", "subclass"]),
apps: {
config: AdvancementConfig,
flow: AdvancementFlow
Expand Down
2 changes: 1 addition & 1 deletion module/documents/item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export default class Item5e extends Item {
/** @inheritDoc */
prepareDerivedData() {
super.prepareDerivedData();
this.labels = {};
this.labels ??= {};

// Clear out linked item cache
this._classLink = undefined;
Expand Down
Loading

0 comments on commit 8a76cd7

Please sign in to comment.