Skip to content

Commit 5d68222

Browse files
authored
Actor sheet actions menu V2 (#972)
1 parent 5efbd9a commit 5d68222

27 files changed

+804
-178
lines changed

lang/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@
714714
"DialogRemoveMessage": "Remove <strong>{label}</strong>?",
715715
"DialogClearMessage": "Clear all elements?",
716716
"DialogSelectionMissingError": "No items were selected!",
717+
"DialogEntriesMissing": "No matching entries were found.",
717718
"Success": "Success",
718719
"Critical": "Critical",
719720
"Failure": "Failure",
@@ -1954,6 +1955,8 @@
19541955
"Event": "Event",
19551956
"Lore": "Lore",
19561957
"Glossary": "Glossary"
1957-
}
1958+
},
1959+
"SwitchEquipment": "Switch Equipment",
1960+
"SwitchEquipmentChatMessage": "<strong>{actor}</strong> is now wearing the following equipment."
19581961
}
19591962
}

module/documents/actors/actor.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ export class FUActor extends Actor {
366366
* Returns an array of items that match a given FUID and optionally an item type
367367
* @param {string} fuid - The FUID of the item(s) which you want to retrieve
368368
* @param {string} [type] - Optionally, a type name to restrict the search
369-
* @returns {Array} - An array containing the found items
369+
* @returns {FUItem[]} - An array containing the found items
370370
*/
371371
getItemsByFuid(fuid, type) {
372372
const fuidFilter = (i) => i.system.fuid === fuid;

module/documents/actors/common/equip-data-model.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
/**
2+
* @typedef CharacterEquipment
3+
* @property mainHand
4+
* @property offHand
5+
* @property armor
6+
* @property accessory
7+
*/
8+
9+
import { FU } from '../../../helpers/config.mjs';
10+
111
/**
212
* @property {string} armor
313
* @property {string} mainHand
@@ -47,6 +57,22 @@ export class EquipDataModel extends foundry.abstract.DataModel {
4757
return equipment;
4858
}
4959

60+
/**
61+
* @param {FUActor} actor
62+
* @param {'mainHand', 'offHand', 'phantom', 'armor'} slot
63+
* @return {FUItem|null}
64+
*/
65+
static getEquipment(actor, slot) {
66+
return (
67+
[actor.system.equipped[slot]]
68+
.filter((value) => value)
69+
.map((value) => actor.items.get(value))
70+
.filter((value) => value)
71+
.filter((value) => value.type in FU.weaponItemTypes)
72+
.at(0) ?? null
73+
);
74+
}
75+
5076
/**
5177
* @param {FUItem} item
5278
* @return {null,"mainHand","offHand","phantom","armor","accessory","arcanum"}

module/documents/effects/active-effect-behaviour-mixin.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,18 @@ export function ActiveEffectBehaviourMixin(BaseDocument) {
316316
}
317317
}
318318

319+
/**
320+
* @param {function(RuleElementDataModel): boolean} predicate
321+
*/
322+
findRuleElement(predicate) {
323+
for (const rule of this.system.rules.elements) {
324+
if (predicate(rule)) {
325+
return rule;
326+
}
327+
}
328+
return undefined;
329+
}
330+
319331
/**
320332
* @description Emits a chat message with this effect
321333
* @returns {Promise<void>}

module/documents/effects/predicates/weapon-rule-predicate.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ export class WeaponRulePredicate extends RulePredicateDataModel {
9191
// If not using the event attack item...
9292
if (!attackItem) {
9393
if (character.actor.type === 'character') {
94-
const mainHand = WeaponResolver.getWeapon(context.character.actor, 'mainHand');
95-
const offHand = WeaponResolver.getWeapon(context.character.actor, 'offHand');
94+
const mainHand = WeaponResolver.getEquipment(context.character.actor, 'mainHand');
95+
const offHand = WeaponResolver.getEquipment(context.character.actor, 'offHand');
9696
attackItem = mainHand ?? offHand;
9797
} else if (character.actor.type === 'npc') {
9898
/** @type {BasicItemDataModel[]} **/

module/documents/items/skill/base-skill-data-model.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Hooks.on(CheckHooks.renderCheck, onRenderDisplay);
110110
* @property {ActionCostDataModel} cost
111111
* @property {TargetingDataModel} targeting
112112
* @property {Set<String>} traits
113+
* @property {Boolean} hasRoll.value
113114
*/
114115
export class BaseSkillDataModel extends FUStandardItemDataModel {
115116
static defineSchema() {
@@ -157,6 +158,25 @@ export class BaseSkillDataModel extends FUStandardItemDataModel {
157158
return [];
158159
}
159160

161+
/**
162+
* @returns {boolean} Whether this is a passive skill.
163+
* @remarks This is used to determine whether to show this skill in some UIs.
164+
*/
165+
get passive() {
166+
let active = this.hasRoll.value || this.useWeapon.accuracy || this.damage.hasDamage || this.effects.entries.size > 0;
167+
/** @type {FUItem} **/
168+
const item = this.parent;
169+
for (const effect of item.effects) {
170+
const rollRule = effect.findRuleElement((rule) => {
171+
return rule.trigger.type === 'itemRollRuleTrigger';
172+
});
173+
if (rollRule !== undefined) {
174+
active = true;
175+
}
176+
}
177+
return !active;
178+
}
179+
160180
/**
161181
* @param {KeyboardModifiers} modifiers
162182
* @return {Promise<void>}

module/documents/items/skill/weapon-resolver.mjs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import FoundryUtils from '../../../helpers/foundry-utils.mjs';
1414
* @param {'mainHand', 'offHand', 'phantom', 'armor'} slot
1515
* @return {FUItem|null}
1616
*/
17-
function getWeapon(actor, slot) {
17+
function getEquipment(actor, slot) {
1818
return (
1919
[actor.system.equipped[slot]]
2020
.filter((value) => value)
@@ -34,24 +34,24 @@ function getEquippedWeapons(actor, includeWeaponModules) {
3434
let equippedWeapons = [];
3535

3636
if (actor.system instanceof CharacterDataModel) {
37-
const phantomHand = getWeapon(actor, 'phantom');
37+
const phantomHand = getEquipment(actor, 'phantom');
3838
if (includeWeaponModules && actor.system.vehicle.embarked && actor.system.vehicle.weapons.length > 0) {
3939
let modules = actor.system.vehicle.weapons;
4040
modules = modules.filter((w) => w.system.data.type !== 'shield' || w === phantomHand);
4141
equippedWeapons.push(...modules, phantomHand);
4242
} else {
43-
const mainHand = getWeapon(actor, 'mainHand');
44-
const offHand = getWeapon(actor, 'offHand');
45-
const armor = getWeapon(actor, 'armor');
43+
const mainHand = getEquipment(actor, 'mainHand');
44+
const offHand = getEquipment(actor, 'offHand');
45+
const armor = getEquipment(actor, 'armor');
4646

4747
equippedWeapons.push(...new Set([mainHand, offHand, phantomHand, armor]));
4848
}
4949
}
5050
if (actor.system instanceof NpcDataModel) {
5151
if (actor.system.useEquipment.value) {
52-
const mainHand = getWeapon(actor, 'mainHand');
53-
const offHand = getWeapon(actor, 'offHand');
54-
const armor = getWeapon(actor, 'armor');
52+
const mainHand = getEquipment(actor, 'mainHand');
53+
const offHand = getEquipment(actor, 'offHand');
54+
const armor = getEquipment(actor, 'armor');
5555

5656
equippedWeapons.push(...new Set([mainHand, offHand, armor]));
5757
} else {
@@ -221,7 +221,7 @@ function getAccuracy(weapon) {
221221

222222
export const WeaponResolver = Object.freeze({
223223
prompt,
224-
getWeapon,
224+
getEquipment,
225225
getEquippedWeapons,
226226
getAccuracy,
227227
});

module/helpers/action-handler.mjs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { CheckHooks } from '../checks/check-hooks.mjs';
99
import { CHECK_FLAVOR } from '../checks/default-section-order.mjs';
1010
import { CheckConfiguration } from '../checks/check-configuration.mjs';
1111
import { StringUtils } from './string-utils.mjs';
12+
import { WeaponResolver } from '../documents/items/skill/weapon-resolver.mjs';
13+
import FoundryUtils from './foundry-utils.mjs';
14+
import { EquipmentHandlerDialog } from './equipment-handler.mjs';
1215

1316
const actionKey = 'ruleDefinedAction';
1417

@@ -48,7 +51,8 @@ const onRenderCheck = (data, check, actor) => {
4851
Hooks.on(CheckHooks.renderCheck, onRenderCheck);
4952

5053
/**
51-
* @desc Encapsulates basic character actions.\
54+
* @desc Encapsulates basic character actions.
55+
* @property {FUActor} actor
5256
* @property {Number} bonus
5357
*/
5458
export class ActionHandler {
@@ -65,19 +69,27 @@ export class ActionHandler {
6569
return this;
6670
}
6771

72+
/**
73+
*
74+
* @param {FUActionType} actionType
75+
* @param isShift
76+
* @returns {Promise<void>}
77+
*/
6878
async handleAction(actionType, isShift = false) {
6979
if (!isShift) {
7080
switch (actionType) {
81+
case 'attack':
82+
return this.attack();
7183
case 'equipment':
72-
return this.createActionMessage(actionType);
84+
return this.equipment();
7385
case 'guard':
7486
return this.toggleGuardEffect(this.actor);
7587
case 'hinder':
7688
return this.promptHinderCheck();
7789
case 'inventory':
7890
return this.createActionMessage(actionType);
7991
case 'objective':
80-
return this.createActionMessage(actionType);
92+
return this.objective();
8193
case 'spell':
8294
return this.createActionMessage(actionType);
8395
case 'study':
@@ -123,6 +135,35 @@ export class ActionHandler {
123135
});
124136
}
125137

138+
/**
139+
* @desc Performs an attack with one of the equipped weapons or attacks.
140+
* @returns {Promise<void>}
141+
*/
142+
async attack() {
143+
const resolution = await WeaponResolver.prompt(this.actor, true);
144+
if (resolution?.item) {
145+
resolution.item.roll();
146+
}
147+
}
148+
149+
/**
150+
* @desc Attempts to roll a check for one of the existing clocks.
151+
* @returns {Promise<void>}
152+
*/
153+
async objective() {
154+
// TODO: Look at active clocks in party/scene and roll a check to advance them
155+
return CheckPrompt.attributeCheck(this.actor);
156+
}
157+
158+
/**
159+
* @desc Opens a dialog to swap the current equipment.
160+
* @returns {Promise<void>}
161+
*/
162+
async equipment() {
163+
const dialog = new EquipmentHandlerDialog(this.actor);
164+
dialog.render(true);
165+
}
166+
126167
/**
127168
* Create a chat message for a given action.
128169
* @param {string} action - The type of action to create a message for.
@@ -155,4 +196,37 @@ export class ActionHandler {
155196
await this.createActionMessage('guard');
156197
}
157198
}
199+
200+
static skillsWithApps = ['invocations', 'verse'];
201+
202+
/**
203+
* @param {FUActor} actor
204+
* @param element
205+
*/
206+
static setupMenu(actor, element) {
207+
// ATTACKS
208+
const attacks = WeaponResolver.getEquippedWeapons(actor, true);
209+
FoundryUtils.itemContextMenu(element, '[data-context-menu="attack"]', attacks);
210+
// SPELLS
211+
const spells = ['spell'].map((t) => actor.getItemsByType(t)).flat();
212+
FoundryUtils.itemContextMenu(element, '[data-context-menu="spell"]', spells);
213+
// INVENTORY
214+
const consumables = ['consumable'].map((t) => actor.getItemsByType(t)).flat();
215+
FoundryUtils.itemContextMenu(element, '[data-context-menu="inventory"]', consumables);
216+
// SKILLS
217+
/** @type {FUItem[]} **/
218+
let skills = ['skill', 'miscAbility']
219+
.map((t) => actor.getItemsByType(t))
220+
.flat()
221+
.filter((s) => {
222+
return !s.system.passive;
223+
});
224+
for (const fuid of ActionHandler.skillsWithApps) {
225+
const skill = actor.getItemsByFuid(fuid);
226+
if (skill) {
227+
skills.push(...skill);
228+
}
229+
}
230+
FoundryUtils.itemContextMenu(element, '[data-context-menu="skill"]', skills);
231+
}
158232
}

module/helpers/config.mjs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,15 +291,15 @@ FU.actionTypes = {
291291
};
292292

293293
FU.actionIcons = {
294-
attack: 'ra ra-crossed-swords', // attack / melee
295-
equipment: 'ra ra-armor', // equipment / gear
296-
guard: 'ra ra-shield', // guard / defense
297-
hinder: 'ra ra-interdiction', // hinder / block
298-
inventory: 'ra ra-ammo-bag', // inventory / bag
299-
objective: 'ra ra-targeted', // objective / goal
300-
spell: 'ra ra-crystal-wand', // spell / magic
301-
study: 'ra ra-book', // study / learn
302-
skill: 'ra ra-muscle-up', // skill / ability improvement
294+
attack: 'ra ra-crossed-swords',
295+
equipment: 'ra ra-vest',
296+
guard: 'ra ra-shield',
297+
hinder: 'ra ra-interdiction',
298+
inventory: 'ra ra-ammo-bag',
299+
objective: 'ra ra-targeted',
300+
spell: 'ra ra-crystal-wand',
301+
study: 'ra ra-book',
302+
skill: 'ra ra-muscle-up',
303303
};
304304

305305
FU.actionRule = {
@@ -1141,6 +1141,7 @@ FU.allIcon = {
11411141

11421142
...FU.bondIcons,
11431143

1144+
...FU.actionIcons,
11441145
roll: FU.checkIcons.open,
11451146
...FU.checkIcons,
11461147
...FU.affinityIcons,
@@ -1178,3 +1179,13 @@ FU.codexTags = Object.freeze({
11781179
lore: 'FU.CODEX.Lore',
11791180
glossary: 'FU.CODEX.Glossary',
11801181
});
1182+
1183+
/**
1184+
* @typedef {'mainHand', 'offHand', 'armor', 'accessory', 'phantom', 'arcanum'} FUEquipmentSlot
1185+
*/
1186+
1187+
FU.equipmentSlots = {
1188+
mainHand: 'FU.MainHand',
1189+
offHand: 'FU.OffHand',
1190+
phantom: 'FU.PhantomHand',
1191+
};

0 commit comments

Comments
 (0)