Comments (6)
Awaiting more changes to the A5E character sheet, as there is a great deal of inspiring work being done there which can help us with Tidy. We'll come back and draw inspiration later ✅
from foundry-vtt-tidy-5e-sheets.
2.4.x legacy implementation reference:
The base sheet holds filter sets which can be reference during item preparation. Item preparation is generically called during getData()
. Implementing sheet classes then implement _prepareItems
.
_filterItems
on the base sheet class is housing all of the predicate logic for all filters when processing items.
base-sheet.mjs:
/**
* Track the set of item filters which are applied
* @type {Object<string, Set>}
* @protected
*/
_filters = {
inventory: new Set(),
spellbook: new Set(),
features: new Set(),
effects: new Set()
};
// ...
/**
* Initialize Item list filters by activating the set of filters which are currently applied
* @param {number} i Index of the filter in the list.
* @param {HTML} ul HTML object for the list item surrounding the filter.
* @private
*/
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for ( let li of filters ) {
if ( set.has(li.dataset.filter) ) li.classList.add("active");
}
}
// ...
/**
* Determine whether an Owned Item will be shown based on the current set of filters.
* @param {object[]} items Copies of item data to be filtered.
* @param {Set<string>} filters Filters applied to the item list.
* @returns {object[]} Subset of input items limited by the provided filters.
* @protected
*/
_filterItems(items, filters) {
return items.filter(item => {
// Action usage
for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) && (item.system.activation?.type !== f) ) return false;
}
// Spell-specific filters
if ( filters.has("ritual") && (item.system.components.ritual !== true) ) return false;
if ( filters.has("concentration") && (item.system.components.concentration !== true) ) return false;
if ( filters.has("prepared") ) {
if ( (item.system.level === 0) || ["innate", "always"].includes(item.system.preparation.mode) ) return true;
if ( this.actor.type === "npc" ) return true;
return item.system.preparation.prepared;
}
// Equipment-specific filters
if ( filters.has("equipped") && (item.system.equipped !== true) ) return false;
return true;
});
}
// ...
/**
* Handle toggling of filters to display a different set of owned items.
* @param {Event} event The click event which triggered the toggle.
* @returns {ActorSheet5e} This actor sheet with toggled filters.
* @private
*/
_onToggleFilter(event) {
event.preventDefault();
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
return this.render();
}
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// ...
}
from foundry-vtt-tidy-5e-sheets.
During _prepareItems
, the character sheet deals the items into their categories. Then, it feed them through the base sheet's _filterItems
function, passing the relevant filter set. The resulting items/spells/feats are further processed and distributed to their relevant context properties.
character-sheet.mjs:
_prepareItems(context) {
// Categorize items as inventory, spellbook, features, and classes
const inventory = {};
for ( const type of ["weapon", "equipment", "consumable", "tool", "backpack", "loot"] ) {
inventory[type] = {label: `${CONFIG.Item.typeLabels[type]}Pl`, items: [], dataset: {type}};
}
// Partition items by category
let {items, spells, feats, races, backgrounds, classes, subclasses} = context.items.reduce((obj, item) => {
const {quantity, uses, recharge} = item.system;
// Item details
const ctx = context.itemContext[item.id] ??= {};
ctx.isStack = Number.isNumeric(quantity) && (quantity !== 1);
ctx.attunement = {
[CONFIG.DND5E.attunementTypes.REQUIRED]: {
icon: "fa-sun",
cls: "not-attuned",
title: "DND5E.AttunementRequired"
},
[CONFIG.DND5E.attunementTypes.ATTUNED]: {
icon: "fa-sun",
cls: "attuned",
title: "DND5E.AttunementAttuned"
}
}[item.system.attunement];
// Prepare data needed to display expanded sections
ctx.isExpanded = this._expanded.has(item.id);
// Item usage
ctx.hasUses = item.hasLimitedUses;
ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
ctx.isDepleted = ctx.isOnCooldown && ctx.hasUses && (uses.value > 0);
ctx.hasTarget = item.hasAreaTarget || item.hasIndividualTarget;
// Item toggle state
this._prepareItemToggleState(item, ctx);
// Classify items into types
if ( item.type === "spell" ) obj.spells.push(item);
else if ( item.type === "feat" ) obj.feats.push(item);
else if ( item.type === "race" ) obj.races.push(item);
else if ( item.type === "background" ) obj.backgrounds.push(item);
else if ( item.type === "class" ) obj.classes.push(item);
else if ( item.type === "subclass" ) obj.subclasses.push(item);
else if ( Object.keys(inventory).includes(item.type) ) obj.items.push(item);
return obj;
}, { items: [], spells: [], feats: [], races: [], backgrounds: [], classes: [], subclasses: [] });
// Apply active item filters
items = this._filterItems(items, this._filters.inventory);
spells = this._filterItems(spells, this._filters.spellbook);
feats = this._filterItems(feats, this._filters.features);
// Organize items
for ( let i of items ) {
const ctx = context.itemContext[i.id] ??= {};
ctx.totalWeight = (i.system.quantity * i.system.weight).toNearest(0.1);
inventory[i.type].items.push(i);
}
// Organize Spellbook and count the number of prepared spells (excluding always, at will, etc...)
const spellbook = this._prepareSpellbook(context, spells);
const nPrepared = spells.filter(spell => {
const prep = spell.system.preparation;
return (spell.system.level > 0) && (prep.mode === "prepared") && prep.prepared;
}).length;
// Sort classes and interleave matching subclasses, put unmatched subclasses into features so they don't disappear
classes.sort((a, b) => b.system.levels - a.system.levels);
const maxLevelDelta = CONFIG.DND5E.maxLevel - this.actor.system.details.level;
classes = classes.reduce((arr, cls) => {
const ctx = context.itemContext[cls.id] ??= {};
ctx.availableLevels = Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(1).map(level => {
const delta = level - cls.system.levels;
return { level, delta, disabled: delta > maxLevelDelta };
});
arr.push(cls);
const identifier = cls.system.identifier || cls.name.slugify({strict: true});
const subclass = subclasses.findSplice(s => s.system.classIdentifier === identifier);
if ( subclass ) arr.push(subclass);
return arr;
}, []);
for ( const subclass of subclasses ) {
feats.push(subclass);
const message = game.i18n.format("DND5E.SubclassMismatchWarn", {
name: subclass.name, class: subclass.system.classIdentifier
});
context.warnings.push({ message, type: "warning" });
}
// Organize Features
const features = {
race: {
label: CONFIG.Item.typeLabels.race, items: races,
hasActions: false, dataset: {type: "race"} },
background: {
label: CONFIG.Item.typeLabels.background, items: backgrounds,
hasActions: false, dataset: {type: "background"} },
classes: {
label: `${CONFIG.Item.typeLabels.class}Pl`, items: classes,
hasActions: false, dataset: {type: "class"}, isClass: true },
active: {
label: "DND5E.FeatureActive", items: [],
hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: {
label: "DND5E.FeaturePassive", items: [],
hasActions: false, dataset: {type: "feat"} }
};
for ( const feat of feats ) {
if ( feat.system.activation?.type ) features.active.items.push(feat);
else features.passive.items.push(feat);
}
// Assign and return
context.inventoryFilters = true;
context.inventory = Object.values(inventory);
context.spellbook = spellbook;
context.preparedSpells = nPrepared;
context.features = Object.values(features);
}
from foundry-vtt-tidy-5e-sheets.
Much the same as character sheet logic, but tuned for NPCs. Notably, there is not an inventory context props and thus no inventory filtering.
npc-sheet.mjs:
/** @override */
_prepareItems(context) {
// Categorize Items as Features and Spells
const features = {
weapons: { label: game.i18n.localize("DND5E.AttackPl"), items: [], hasActions: true,
dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: game.i18n.localize("DND5E.ActionPl"), items: [], hasActions: true,
dataset: {type: "feat", "activation.type": "action"} },
passive: { label: game.i18n.localize("DND5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("DND5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Start by classifying items into groups for rendering
let [spells, other] = context.items.reduce((arr, item) => {
const {quantity, uses, recharge, target} = item.system;
const ctx = context.itemContext[item.id] ??= {};
ctx.isStack = Number.isNumeric(quantity) && (quantity !== 1);
ctx.isExpanded = this._expanded.has(item.id);
ctx.hasUses = uses && (uses.max > 0);
ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
ctx.isDepleted = item.isOnCooldown && (uses.per && (uses.value > 0));
ctx.hasTarget = !!target && !(["none", ""].includes(target.type));
ctx.canToggle = false;
if ( item.type === "spell" ) arr[0].push(item);
else arr[1].push(item);
return arr;
}, [[], []]);
// Apply item filters
spells = this._filterItems(spells, this._filters.spellbook);
other = this._filterItems(other, this._filters.features);
// Organize Spellbook
const spellbook = this._prepareSpellbook(context, spells);
// Organize Features
for ( let item of other ) {
if ( item.type === "weapon" ) features.weapons.items.push(item);
else if ( item.type === "feat" ) {
if ( item.system.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item);
}
else features.equipment.items.push(item);
}
// Assign and return
context.inventoryFilters = true;
context.features = Object.values(features);
context.spellbook = spellbook;
}
from foundry-vtt-tidy-5e-sheets.
Vehicles currently have no filters.
vehicle-sheet.mjs:
/** @override */
_prepareItems(context) {
const cargoColumns = [{
label: game.i18n.localize("DND5E.Quantity"),
css: "item-qty",
property: "quantity",
editable: "Number"
}];
const equipmentColumns = [{
label: game.i18n.localize("DND5E.Quantity"),
css: "item-qty",
property: "system.quantity",
editable: "Number"
}, {
label: game.i18n.localize("DND5E.AC"),
css: "item-ac",
property: "system.armor.value"
}, {
label: game.i18n.localize("DND5E.HP"),
css: "item-hp",
property: "system.hp.value",
editable: "Number"
}, {
label: game.i18n.localize("DND5E.Threshold"),
css: "item-threshold",
property: "threshold"
}];
const features = {
actions: {
label: game.i18n.localize("DND5E.ActionPl"),
items: [],
hasActions: true,
crewable: true,
dataset: {type: "feat", "activation.type": "crew"},
columns: [{
label: game.i18n.localize("DND5E.Cover"),
css: "item-cover",
property: "cover"
}]
},
equipment: {
label: game.i18n.localize(CONFIG.Item.typeLabels.equipment),
items: [],
crewable: true,
dataset: {type: "equipment", "armor.type": "vehicle"},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize("DND5E.Features"),
items: [],
dataset: {type: "feat"}
},
reactions: {
label: game.i18n.localize("DND5E.ReactionPl"),
items: [],
dataset: {type: "feat", "activation.type": "reaction"}
},
weapons: {
label: game.i18n.localize(`${CONFIG.Item.typeLabels.weapon}Pl`),
items: [],
crewable: true,
dataset: {type: "weapon", "weapon-type": "siege"},
columns: equipmentColumns
}
};
context.items.forEach(item => {
const {uses, recharge} = item.system;
const ctx = context.itemContext[item.id] ??= {};
ctx.canToggle = false;
ctx.isExpanded = this._expanded.has(item.id);
ctx.hasUses = uses && (uses.max > 0);
ctx.isOnCooldown = recharge && !!recharge.value && (recharge.charged === false);
ctx.isDepleted = item.isOnCooldown && (uses.per && (uses.value > 0));
});
const cargo = {
crew: {
label: game.i18n.localize("DND5E.VehicleCrew"),
items: context.actor.system.cargo.crew,
css: "cargo-row crew",
editableName: true,
dataset: {type: "crew"},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize("DND5E.VehiclePassengers"),
items: context.actor.system.cargo.passengers,
css: "cargo-row passengers",
editableName: true,
dataset: {type: "passengers"},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize("DND5E.VehicleCargo"),
items: [],
dataset: {type: "loot"},
columns: [{
label: game.i18n.localize("DND5E.Quantity"),
css: "item-qty",
property: "system.quantity",
editable: "Number"
}, {
label: game.i18n.localize("DND5E.Price"),
css: "item-price",
property: "system.price.value",
editable: "Number"
}, {
label: game.i18n.localize("DND5E.Weight"),
css: "item-weight",
property: "system.weight",
editable: "Number"
}]
}
};
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0;
for ( const item of context.items ) {
const ctx = context.itemContext[item.id] ??= {};
this._prepareCrewedItem(item, ctx);
// Handle cargo explicitly
const isCargo = item.flags.dnd5e?.vehicleCargo === true;
if ( isCargo ) {
totalWeight += (item.system.weight || 0) * item.system.quantity;
cargo.cargo.items.push(item);
continue;
}
// Handle non-cargo item types
switch ( item.type ) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
const act = item.system.activation;
if ( !act.type || (act.type === "none") ) features.passive.items.push(item);
else if (act.type === "reaction") features.reactions.items.push(item);
else features.actions.items.push(item);
break;
default:
totalWeight += (item.system.weight || 0) * item.system.quantity;
cargo.cargo.items.push(item);
}
}
// Update the rendering context data
context.inventoryFilters = false;
context.features = Object.values(features);
context.cargo = Object.values(cargo);
context.encumbrance = this._computeEncumbrance(totalWeight, context);
}
from foundry-vtt-tidy-5e-sheets.
Closing as completed.
from foundry-vtt-tidy-5e-sheets.
Related Issues (20)
- feat: Skill Bonus Hint / Summary on Actor sheets
- compat: Token Action HUD x Tidy Custom Sections
- NPC Initiative Sheet Button HOT 4
- Bug: Clicking on Spell Pips Does Nothing [4.0.0-beta-2] HOT 2
- feat: Add compendia migration option to CCSS migration HOT 1
- dnd5e: Compendium Consumption Target UI
- bug: Errors when changing equipment Action Type to Enchant [4.0.0-beta.3] HOT 3
- bug: Exhaustion SVG not compatible with 7+ levels of exhaustion HOT 1
- feat: Effects Descriptions - Item Card, Expandable Description on Effects table, ...
- Error with spell attack modifier on the spellbook page (4.0.1, DnD3.2.0, V11) HOT 1
- api: Add Tidy item pre creation hook HOT 2
- Bug: Cannot delete items from sidebar containers using right-click menu HOT 2
- Compatibility: Tidy 5e x Steinhardts Guide to Eldritch Hunt HOT 6
- bug: Receiving Effects from another source causing sheet errors
- compat: Beaver's Crafting, Round 2
- style: Pull Unidentified styles into global stylesheet space and out of compiled scoped svelte
- Show Players a Token image by Right Clicking on NPC portrait and "Show Token" broken when is the Token is using wildcards in path. HOT 6
- bug: Loot weight tag in expandable item description is showing [object Object] instead of weight HOT 1
- feat: Context Menu Options for Assigning To Section / Action Section, and Use Default Section / Action Section
- [Suggestion] sharable/linkable group containers. HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from foundry-vtt-tidy-5e-sheets.