Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#3629, #4142] Add units to container capacity, add volume capacity #4912

Open
wants to merge 1 commit into
base: 4.2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,40 @@

"DND5E.Casting": "Casting",
"DND5E.Conditions": "Conditions",

"DND5E.CONTAINER": {
"Details": "Container Details",
"FIELDS": {
"capacity": {
"label": "Capacity",
"count": {
"label": "Item Count"
},
"volume": {
"label": "Volume Capacity",
"units": {
"label": "Volume Units"
},
"value": {
"label": "Maximum Volume"
}
},
"weight": {
"label": "Weight Capacity",
"units": {
"label": "Weight Units"
},
"value": {
"label": "Maximum Weight"
}
}
},
"properties": {
"label": "Container Properties"
}
}
},

"DND5E.Controls": {
"Hint": "Enable various hints throughout the UI for certain mouse and keyboard controls.",
"LeftClick": "Left Click",
Expand Down Expand Up @@ -2229,14 +2263,6 @@
"DND5E.ItemConsumableStatus": "Consumable Status",
"DND5E.ItemConsumableSubtype": "{category} Type",
"DND5E.ItemConsumableType": "Consumable Type",
"DND5E.ItemContainerCapacity": "Capacity",
"DND5E.ItemContainerCapacityItems": "Items",
"DND5E.ItemContainerCapacityMax": "Max Capacity",
"DND5E.ItemContainerCapacityType": "Capacity Type",
"DND5E.ItemContainerCapacityWeight": "Weight",
"DND5E.ItemContainerDetails": "Container Details",
"DND5E.ItemContainerProperties": "Container Properties",
"DND5E.ItemContainerStatus": "Container Status",
"DND5E.ItemCreate": "Create Item",
"DND5E.ItemCritThreshold": "Critical Hit Threshold",
"DND5E.ItemCritExtraDamage": "Extra Critical Hit Damage",
Expand Down Expand Up @@ -2304,6 +2330,7 @@
"DND5E.ItemRecoveryRollMax": "{name} recovers all charges",
"DND5E.ItemRecoveryFormulaWarning": "Unable to recover uses for {name}. Invalid recovery formula '{formula}' ({uuid}).",
"DND5E.ItemRequiredStr": "Required Strength",
"DND5E.Items": "Items",
"DND5E.ItemSiegeProperties": "Siege Properties",
"DND5E.ItemSpeciesDetails": "Species Details",
"DND5E.ItemSubclassDetails": "Subclass Details",
Expand Down Expand Up @@ -3516,6 +3543,7 @@
"DND5E.Unidentified.Notice": "You must identify this item to learn its details.",
"DND5E.Unidentified.Title": "Unidentified",
"DND5E.Unidentified.Value": "???",
"DND5E.Unit": "Unit",

"DND5E.UNITS": {
"DISTANCE": {
Expand All @@ -3537,6 +3565,31 @@
"Abbreviation": "mi"
}
},
"VOLUME": {
"Label": "Volume Units",
"CubicFoot": {
"Label": "Cubic Feet",
"Abbreviation": "ft^3",
"Counted": {
"narrow": {
"one": "{number}ft^3",
"other": "{number}ft^3"
},
"short": {
"one": "{number} cu ft",
"other": "{number} cu ft"
},
"long": {
"one": "{number} cubic foot",
"other": "{number} cubic feet"
}
}
},
"Liter": {
"Label": "Liters",
"Abbreviation": "L"
}
},
"WEIGHT": {
"Label": "Weight Units",
"Kilogram": {
Expand Down Expand Up @@ -4073,6 +4126,10 @@
"Name": "Use Metric Length Units",
"Hint": "Defaults to using meters instead of feet for movement and senses."
},
"VolumeUnits": {
"Name": "Use Metric Volume Units",
"Hint": "Defaults to using liters instead of cubic feet for container capacity."
},
"WeightUnits": {
"Name": "Use Metric Weight Units",
"Hint": "Replaces all reference to lbs with kgs and updates the encumbrance calculations to use metric weight units."
Expand Down
27 changes: 27 additions & 0 deletions module/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2358,6 +2358,10 @@ DND5E.defaultUnits = {
imperial: "ft",
metric: "m"
},
volume: {
imperial: "cubicFoot",
metric: "liter"
},
weight: {
imperial: "lb",
metric: "kg"
Expand Down Expand Up @@ -2447,6 +2451,29 @@ preLocalize("distanceUnits");

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

/**
* The valid units for measurement of volume.
* @enum {UnitConfiguration}
*/
DND5E.volumeUnits = {
cubicFoot: {
label: "DND5E.UNITS.VOLUME.CubicFoot.Label",
abbreviation: "DND5E.UNITS.Volume.CubicFoot.Abbreviation",
counted: "DND5E.UNITS.Volume.CubicFoot.Counted",
conversion: 1,
type: "imperial"
},
liter: {
label: "DND5E.UNITS.VOLUME.Liter.Label",
abbreviation: "DND5E.UNITS.Volume.Liter.Abbreviation",
conversion: 1 / 28.317, // Should we do an approximate conversion here? Does it matter?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure localized 5e books have some simplified conversion of cubic feet to liters. Got an example?

type: "metric"
}
};
preLocalize("volumeUnits", { keys: ["label", "abbreviation"] });

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

/**
* The valid units for measurement of weight.
* @enum {UnitConfiguration}
Expand Down
67 changes: 49 additions & 18 deletions module/data/item/container.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defaultUnits } from "../../utils.mjs";
import { ItemDataModel } from "../abstract.mjs";
import CurrencyTemplate from "../shared/currency.mjs";
import EquippableItemTemplate from "./templates/equippable-item.mjs";
import IdentifiableTemplate from "./templates/identifiable.mjs";
import ItemDescriptionTemplate from "./templates/item-description.mjs";
import PhysicalItemTemplate from "./templates/physical-item.mjs";
import CurrencyTemplate from "../shared/currency.mjs";

const { NumberField, SchemaField, SetField, StringField } = foundry.data.fields;

Expand All @@ -15,10 +16,15 @@ const { NumberField, SchemaField, SetField, StringField } = foundry.data.fields;
* @mixes EquippableItemTemplate
* @mixes CurrencyTemplate
*
* @property {Set<string>} properties Container properties.
* @property {object} capacity Information on container's carrying capacity.
* @property {string} capacity.type Method for tracking max capacity as defined in `DND5E.itemCapacityTypes`.
* @property {number} capacity.value Total amount of the type this container can carry.
* @property {number} capacity.count Number of items that can be stored within the container.
* @property {object} capacity.volume
* @property {string} capacity.volume.units Units used to measure volume capacity.
* @property {number} capacity.volume.value Amount of volume that can be stored.
* @property {object} capacity.weight
* @property {string} capacity.weight.units Units used to measure weight capacity.
* @property {number} capacity.weight.value Amount of weight that can be stored.
* @property {Set<string>} properties Container properties.
*/
export default class ContainerData extends ItemDataModel.mixin(
ItemDescriptionTemplate, IdentifiableTemplate, PhysicalItemTemplate, EquippableItemTemplate, CurrencyTemplate
Expand All @@ -29,21 +35,26 @@ export default class ContainerData extends ItemDataModel.mixin(
/* -------------------------------------------- */

/** @override */
static LOCALIZATION_PREFIXES = ["DND5E.SOURCE"];
static LOCALIZATION_PREFIXES = ["DND5E.CONTAINER", "DND5E.SOURCE"];

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

/** @inheritDoc */
static defineSchema() {
return this.mergeSchema(super.defineSchema(), {
quantity: new NumberField({ min: 1, max: 1 }),
properties: new SetField(new StringField(), { label: "DND5E.ItemContainerProperties" }),
capacity: new SchemaField({
type: new StringField({
required: true, initial: "weight", blank: false, label: "DND5E.ItemContainerCapacityType"
count: new NumberField({ min: 0, integer: true }),
volume: new SchemaField({
value: new NumberField({ min: 0 }),
units: new StringField({ initial: () => defaultUnits("volume") })
}),
value: new NumberField({ required: true, min: 0, label: "DND5E.ItemContainerCapacityMax" })
}, { label: "DND5E.ItemContainerCapacity" })
weight: new SchemaField({
value: new NumberField({ min: 0 }),
units: new StringField({ initial: () => defaultUnits("weight") })
})
}),
properties: new SetField(new StringField()),
quantity: new NumberField({ min: 1, max: 1 })
});
}

Expand Down Expand Up @@ -74,6 +85,7 @@ export default class ContainerData extends ItemDataModel.mixin(
/** @inheritDoc */
static _migrateData(source) {
super._migrateData(source);
ContainerData.#migrateCapacity(source);
ContainerData.#migrateQuantity(source);
}

Expand All @@ -91,6 +103,24 @@ export default class ContainerData extends ItemDataModel.mixin(

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

/**
* Migrate capacity to support multiple fields and units.
* @param {object} source The candidate source data from which the model will be constructed.
*/
static #migrateCapacity(source) {
if ( !source?.capacity || !source.capacity?.type || !source.capacity?.value ) return;
if ( source.capacity.type === "weight" ) {
source.capacity.weight ??= {};
source.capacity.weight.value = source.capacity.value;
} else if ( source.capacity.type === "item" ) {
source.capacity.count = source.capacity.value;
}
delete source.capacity.type;
delete source.capacity.value;
}

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

/**
* Force quantity to always be 1.
* @param {object} source The candidate source data from which the model will be constructed.
Expand Down Expand Up @@ -275,14 +305,15 @@ export default class ContainerData extends ItemDataModel.mixin(
* @returns {Promise<Item5eCapacityDescriptor>}
*/
async computeCapacity() {
const { value, type } = this.capacity;
const context = { max: value ?? Infinity };
if ( type === "weight" ) {
context.value = await this.contentsWeight;
context.units = game.i18n.localize("DND5E.AbbreviationLbs");
} else {
const context = { max: Infinity, value: 0 };
if ( this.capacity.count ) {
context.value = await this.contentsCount;
context.units = game.i18n.localize("DND5E.ItemContainerCapacityItems");
context.max = this.capacity.count;
context.units = game.i18n.localize("DND5E.Items");
} else if ( this.capacity.weight.value ) {
context.value = await this.contentsWeight;
context.max = this.capacity.weight.value;
context.units = CONFIG.DND5E.weightUnits[this.capacity.weight.units]?.label ?? "";
}
context.value = context.value.toNearest(0.1);
context.pct = Math.clamp(context.max ? (context.value / context.max) * 100 : 0, 0, 100);
Expand Down
10 changes: 10 additions & 0 deletions module/settings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,16 @@ export function registerSystemSettings() {
default: false
});

// Metric Volume Weights
game.settings.register("dnd5e", "metricVolumeUnits", {
name: "SETTINGS.DND5E.METRIC.VolumeUnits.Name",
hint: "SETTINGS.DND5E.METRIC.VolumeUnits.Hint",
scope: "world",
config: true,
type: Boolean,
default: false
});

// Metric Unit Weights
game.settings.register("dnd5e", "metricWeightUnits", {
name: "SETTINGS.DND5E.METRIC.WeightUnits.Name",
Expand Down
13 changes: 13 additions & 0 deletions module/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ export function formatText(value) {

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

/**
* Form a number using the provided volume unit.
* @param {number} value The volume to format.
* @param {string} unit Volume unit as defined in `CONFIG.DND5E.volumeUnits`.
* @param {object} [options={}] Formatting options passed to `formatNumber`.
* @returns {string}
*/
export function formatVolume(value, unit, options={}) {
return _formatSystemUnits(value, unit, CONFIG.DND5E.volumeUnits[unit], options);
}

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

/**
* Form a number using the provided weight unit.
* @param {number} value The weight to format.
Expand Down
63 changes: 44 additions & 19 deletions templates/items/details/details-container.hbs
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
<fieldset>
<legend>{{ localize "DND5E.ItemContainerDetails" }}</legend>
<legend>{{ localize "DND5E.CONTAINER.Details" }}</legend>

{{!-- Container Properties --}}
{{ formField fields.properties options=properties.options label="DND5E.ItemContainerProperties" localize=true
input=inputs.createMultiCheckboxInput stacked=true classes="checkbox-grid checkbox-grid-3" }}

{{!-- Capacity --}}
<div class="form-group split-group">
<label>{{ localize "DND5E.ItemContainerCapacity" }}</label>
<div class="form-fields">

{{!-- Amount --}}
{{ formField fields.capacity.fields.value value=source.capacity.value label="DND5E.Amount" localize=true
classes="label-top" placeholder="—" }}

{{!-- Type --}}
{{ formField fields.capacity.fields.type value=source.capacity.type label="DND5E.Type" localize=true
classes="label-top" choices=config.itemCapacityTypes }}

</div>
</div>
{{ formField fields.properties options=properties.options input=inputs.createMultiCheckboxInput stacked=true
classes="checkbox-grid checkbox-grid-3" }}

{{!-- Attunement --}}
{{#if properties.object.mgc}}
Expand All @@ -42,3 +26,44 @@
{{/if}}

</fieldset>

<fieldset>
<legend>{{ localize "DND5E.CONTAINER.FIELDS.capacity.label" }}</legend>

{{!-- Item Capacity --}}
{{ formField fields.capacity.fields.count value=source.capacity.count placeholder="—" }}

{{!-- Volume Capacity --}}
<div class="form-group split-group">
<label>{{ localize "DND5E.CONTAINER.FIELDS.capacity.volume.label" }}</label>
<div class="form-fields">

{{!-- Amount --}}
{{ formField fields.capacity.fields.volume.fields.value value=source.capacity.volume.value
label="DND5E.Amount" localize=true classes="label-top" placeholder="—" }}

{{!-- Units --}}
{{ formField fields.capacity.fields.volume.fields.units value=source.capacity.volume.units
label="DND5E.Unit" localize=true classes="label-top" choices=config.volumeUnits
labelAttr="label" }}

</div>
</div>

{{!-- Weight Capacity --}}
<div class="form-group split-group">
<label>{{ localize "DND5E.CONTAINER.FIELDS.capacity.weight.label" }}</label>
<div class="form-fields">

{{!-- Amount --}}
{{ formField fields.capacity.fields.weight.fields.value value=source.capacity.weight.value
label="DND5E.Amount" localize=true classes="label-top" placeholder="—" }}

{{!-- Units --}}
{{ formField fields.capacity.fields.weight.fields.units value=source.capacity.weight.units
label="DND5E.Unit" localize=true classes="label-top" choices=config.weightUnits
labelAttr="label" }}

</div>
</div>
</fieldset>
2 changes: 1 addition & 1 deletion templates/shared/inventory.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
</ol>

{{#with capacity}}
<div class="capacity" role="meter" aria-label="{{localize 'DND5E.ItemContainerCapacity'}}"
<div class="capacity" role="meter" aria-label="{{ localize 'DND5E.CONTAINER.FIELDS.capacity.label' }}"
aria-value="{{pct}}" aria-valuetext="{{value}} {{units}}"
aria-valuemin="0" aria-valuemax="{{max}}" style="--percentage: {{pct}}%">
</div>
Expand Down