Implemented Active Effects.
Used AE to implement exhaustion, stimulants, and rest.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { packSkillGroupsIntoColumns } from "../utils/skill-layout.js";
|
||||
import { createAgentResourceChatMessage } from "../utils/resource-chat.js";
|
||||
import {
|
||||
createSkillImprovementChatMessage,
|
||||
evaluateSkillImprovementRolls,
|
||||
@@ -7,15 +8,25 @@ import {
|
||||
} from "../roll/skill-improvement-roll.js";
|
||||
import DGActorSheet from "./base-actor-sheet.js";
|
||||
import ActorEditStatForm from "../applications/edit-stats.js";
|
||||
import EffectsTabMixin from "./mixins/effects-tab-mixin.js";
|
||||
import {
|
||||
applyStimulantEffect,
|
||||
clearStimulantEffects,
|
||||
getEffectiveSuppressExhaustion,
|
||||
hasActiveStimulantEffect,
|
||||
} from "../utils/stimulant-effect.js";
|
||||
|
||||
const { DialogV2 } = foundry.applications.api;
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
/** @extends {DGActorSheet} */
|
||||
export default class DGAgentSheet extends DGActorSheet {
|
||||
const AgentSheetBase = EffectsTabMixin(DGActorSheet);
|
||||
|
||||
/** @extends {AgentSheetBase} */
|
||||
export default class DGAgentSheet extends AgentSheetBase {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = /** @type {const} */ ({
|
||||
classes: ["agent-sheet"],
|
||||
position: { width: 958, height: 700 },
|
||||
position: { width: 978, height: 720 },
|
||||
actions: {
|
||||
// Resets.
|
||||
clearBondDamage: DGAgentSheet._clearBondDamage,
|
||||
@@ -30,6 +41,15 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
this._syncCustomSkillsEditModeUi();
|
||||
return this.render();
|
||||
},
|
||||
createEffect: AgentSheetBase.createEffect,
|
||||
openEffect: AgentSheetBase.openEffect,
|
||||
deleteEffect: AgentSheetBase.deleteEffect,
|
||||
toggleEffect: AgentSheetBase.toggleEffect,
|
||||
openEffectOrigin: AgentSheetBase.openEffectOrigin,
|
||||
toggleAcuteEpisode: DGAgentSheet._toggleAcuteEpisode,
|
||||
exhaustAgent: DGAgentSheet._exhaustAgent,
|
||||
restAgent: DGAgentSheet._restAgent,
|
||||
takeStimulants: DGAgentSheet._takeStimulants,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,16 +67,27 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
{ id: "motivations" },
|
||||
{ id: "gear" },
|
||||
{ id: "personal" },
|
||||
{ id: "effects" },
|
||||
{ id: "about", icon: "fas fa-question-circle", label: "" },
|
||||
],
|
||||
},
|
||||
leftBarRestSanity: {
|
||||
initial: "rest",
|
||||
labelPrefix: "DG.Navigation.Agent.LeftBar",
|
||||
tabs: [{ id: "rest" }, { id: "sanity" }],
|
||||
},
|
||||
});
|
||||
|
||||
/** @override */
|
||||
static PARTS = /** @type {const} */ ({
|
||||
leftBar: {
|
||||
template: `${this.TEMPLATE_PATH}/parts/left-bar.html`,
|
||||
templates: [`${this.TEMPLATE_PATH}/partials/sanity-partial.html`],
|
||||
templates: [
|
||||
`${this.TEMPLATE_PATH}/partials/sanity-partial.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/left-bar-exhaustion-section.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/left-bar-rest-tab.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/left-bar-sanity-tab.html`,
|
||||
],
|
||||
},
|
||||
tabs: this.BASE_PARTS.tabs,
|
||||
rightBar: {
|
||||
@@ -67,6 +98,7 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
`${this.TEMPLATE_PATH}/parts/motivations-tab-agent.html`,
|
||||
`${this.TEMPLATE_PATH}/parts/gear-tab-agent.html`,
|
||||
`${this.TEMPLATE_PATH}/parts/personal-tab-agent.html`,
|
||||
`${this.TEMPLATE_PATH}/parts/effects-tab.html`,
|
||||
`${this.TEMPLATE_PATH}/parts/about-tab.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/custom-skills-partial-agent.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/bonds-section-partial.html`,
|
||||
@@ -92,7 +124,9 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
_prepareTabs(group) {
|
||||
const tabs = super._prepareTabs(group);
|
||||
|
||||
if (!this.actor.limited || this.actor.type !== "agent") return tabs;
|
||||
if (!this.actor.limited || this.actor.type !== "agent" || group !== "primary") {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
return {
|
||||
personal: { ...tabs.personal, active: true, cssClass: "active" },
|
||||
@@ -103,14 +137,33 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
async _prepareContext(options) {
|
||||
const context = await super._prepareContext(options);
|
||||
|
||||
context.tabs = this._prepareTabs("primary");
|
||||
context.leftBarRestSanityTabs = this._prepareTabs("leftBarRestSanity");
|
||||
|
||||
if (this.actor.type !== "agent") return context;
|
||||
|
||||
context.typedSkillColumns = this._prepareTypedSkillColumns();
|
||||
context.customSkillsEditMode = this._customSkillsEditMode;
|
||||
context.physicalUi = this._preparePhysicalUi();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/** @returns {object} */
|
||||
_preparePhysicalUi() {
|
||||
const { physical } = this.actor.system;
|
||||
const isExhausted = DGAgentSheet._isActorExhausted(this.actor);
|
||||
const stimulantActive = hasActiveStimulantEffect(this.actor);
|
||||
const suppressExhaustion = getEffectiveSuppressExhaustion(this.actor);
|
||||
return {
|
||||
isExhausted,
|
||||
displayPenalty: isExhausted ? physical.exhaustedPenalty : 0,
|
||||
suppressExhaustionInactive: !isExhausted,
|
||||
suppressExhaustionInputDisabled: !isExhausted || stimulantActive,
|
||||
suppressExhaustionChecked: isExhausted && suppressExhaustion,
|
||||
};
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_sortCustomSkills() {
|
||||
// Agent sheet uses typedSkillColumns instead of sortedCustomSkills.
|
||||
@@ -130,6 +183,21 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async _toggleAcuteEpisode(event, target) {
|
||||
event.preventDefault();
|
||||
const itemId =
|
||||
target.dataset.itemId ?? target.closest("[data-item-id]")?.dataset?.itemId;
|
||||
const item = this.actor.items.get(itemId);
|
||||
if (!item || item.type !== "motivation") return;
|
||||
await item.update({
|
||||
"system.acuteEpisode": !item.system.acuteEpisode,
|
||||
});
|
||||
}
|
||||
|
||||
static _clearBondDamage() {
|
||||
for (const bond of this.actor.itemTypes.bond) {
|
||||
if (!bond.system.hasBeenDamagedSinceLastHomeScene) continue;
|
||||
@@ -145,8 +213,11 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
* @returns {void}
|
||||
*/
|
||||
static _resetBreakingPoint() {
|
||||
const pow =
|
||||
this.actor.system.statistics.pow.effectiveValue ??
|
||||
this.actor.system.statistics.pow.value;
|
||||
const currentBreakingPoint = Math.max(
|
||||
this.actor.system.sanity.value - this.actor.system.statistics.pow.value,
|
||||
this.actor.system.sanity.value - pow,
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -247,6 +318,181 @@ export default class DGAgentSheet extends DGActorSheet {
|
||||
form.render(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} raw
|
||||
* @returns {number}
|
||||
*/
|
||||
static _normalizeExhaustionPenalty(raw) {
|
||||
const value = Number(raw);
|
||||
if (Number.isNaN(value) || value === 0) return -20;
|
||||
return value > 0 ? -1 * Math.abs(value) : value;
|
||||
}
|
||||
|
||||
static async _exhaustAgent() {
|
||||
const actor = this.actor;
|
||||
const content = await renderTemplate(
|
||||
`${BASE_TEMPLATE_PATH}/dialog/exhaust-agent.html`,
|
||||
{
|
||||
penalty: actor.system.physical.exhaustedPenalty ?? -20,
|
||||
},
|
||||
);
|
||||
|
||||
let penalty = null;
|
||||
const confirmed = await DialogV2.wait({
|
||||
content,
|
||||
window: {
|
||||
title: game.i18n.localize("DG.Physical.ExhaustDialogTitle"),
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
action: "rollWpLoss",
|
||||
label: game.i18n.localize("DG.Physical.RollWillpowerLoss"),
|
||||
default: true,
|
||||
icon: "fas fa-dice",
|
||||
callback: (_event, _button, dialog) => {
|
||||
const input = dialog.element.querySelector('[name="exhaustionPenalty"]');
|
||||
penalty = DGAgentSheet._normalizeExhaustionPenalty(input?.value);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!confirmed || penalty === null) return;
|
||||
|
||||
const wpRoll = await new Roll("1d6").evaluate();
|
||||
const loss = wpRoll.total;
|
||||
const currentWp = Number(actor.system.wp.value) || 0;
|
||||
const maxWp = Number(actor.system.wp.max) || 0;
|
||||
const newWp = Math.max(0, currentWp - loss);
|
||||
|
||||
await actor.update({
|
||||
"system.physical.exhausted": true,
|
||||
"system.physical.exhaustedPenalty": penalty,
|
||||
"system.wp.value": newWp,
|
||||
});
|
||||
|
||||
await createAgentResourceChatMessage({
|
||||
actor,
|
||||
token: this.token,
|
||||
contentKey: "DG.Physical.Chat.Exhausted",
|
||||
i18nData: {
|
||||
name: actor.name,
|
||||
loss,
|
||||
current: newWp,
|
||||
max: maxWp,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async _restAgent() {
|
||||
const actor = this.actor;
|
||||
|
||||
if (getEffectiveSuppressExhaustion(actor)) {
|
||||
ui.notifications.warn(
|
||||
game.i18n.format("DG.Physical.RestBlockedWhileSuppressed", {
|
||||
name: actor.name,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const wpRoll = await new Roll("1d6").evaluate();
|
||||
const gain = wpRoll.total;
|
||||
const currentWp = Number(actor.system.wp.value) || 0;
|
||||
const maxWp = Number(actor.system.wp.max) || 0;
|
||||
const newWp = Math.min(maxWp, currentWp + gain);
|
||||
|
||||
await clearStimulantEffects(actor);
|
||||
|
||||
await actor.update({
|
||||
"system.physical.exhausted": false,
|
||||
"system.physical.suppressExhaustion": false,
|
||||
"system.wp.value": newWp,
|
||||
});
|
||||
|
||||
await createAgentResourceChatMessage({
|
||||
actor,
|
||||
token: this.token,
|
||||
contentKey: "DG.Physical.Chat.Rested",
|
||||
i18nData: {
|
||||
name: actor.name,
|
||||
gain,
|
||||
current: newWp,
|
||||
max: maxWp,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static _isActorExhausted(actor) {
|
||||
return Boolean(actor._source?.system?.physical?.exhausted);
|
||||
}
|
||||
|
||||
static async _takeStimulants() {
|
||||
const actor = this.actor;
|
||||
|
||||
if (!DGAgentSheet._isActorExhausted(actor)) {
|
||||
ui.notifications.info(
|
||||
game.i18n.format("DG.Physical.StimulantsNotExhaustedWarning", {
|
||||
name: actor.name,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await renderTemplate(
|
||||
`${BASE_TEMPLATE_PATH}/dialog/take-stimulants.html`,
|
||||
{},
|
||||
);
|
||||
|
||||
const choice = await DialogV2.wait({
|
||||
content,
|
||||
window: {
|
||||
title: game.i18n.localize("DG.Physical.StimulantsDialogTitle"),
|
||||
},
|
||||
classes: ["stimulants-dialog-app"],
|
||||
buttons: [
|
||||
{
|
||||
action: "regular",
|
||||
label: game.i18n.localize("DG.Physical.StimulantsRegular"),
|
||||
default: true,
|
||||
callback: () => "regular",
|
||||
},
|
||||
{
|
||||
action: "hard",
|
||||
label: game.i18n.localize("DG.Physical.StimulantsHard"),
|
||||
callback: () => "hard",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!choice) return;
|
||||
|
||||
const formula = choice === "hard" ? "2d6" : "1d6";
|
||||
const hoursRoll = await new Roll(formula).evaluate();
|
||||
const hours = hoursRoll.total;
|
||||
const contentKey =
|
||||
choice === "hard"
|
||||
? "DG.Physical.Chat.StimulantsHard"
|
||||
: "DG.Physical.Chat.StimulantsRegular";
|
||||
|
||||
const appliedHours = await applyStimulantEffect(actor, hours);
|
||||
|
||||
await createAgentResourceChatMessage({
|
||||
actor,
|
||||
token: this.token,
|
||||
contentKey,
|
||||
i18nData: {
|
||||
name: actor.name,
|
||||
hours: appliedHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs through the whole process of improving skills,
|
||||
* i.e., prompting the user, rolling, and creating the chat card.
|
||||
|
||||
@@ -5,10 +5,12 @@ import RollSheetMixin from "./mixins/roll-sheet-mixin.js";
|
||||
import SkillPrepMixin from "./mixins/skill-prep-mixin.js";
|
||||
import SpecialTrainingMixin from "./mixins/special-training-mixin.js";
|
||||
import TypedSkillMixin from "./mixins/typed-skill-mixin.js";
|
||||
import AeInputMixin from "./mixins/ae-input-mixin.js";
|
||||
|
||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||
|
||||
const ComposedActorSheetBase = composeMixins(
|
||||
AeInputMixin,
|
||||
SpecialTrainingMixin,
|
||||
TypedSkillMixin,
|
||||
ItemListMixin,
|
||||
@@ -125,8 +127,6 @@ export default class DGActorSheet extends ComposedActorSheetBase {
|
||||
if (!this.actor.limited) return;
|
||||
|
||||
options.parts = ["header"];
|
||||
|
||||
if (this.actor.type === "agent") options.parts.push("tabs", "bio");
|
||||
}
|
||||
|
||||
/** @override */
|
||||
@@ -135,15 +135,6 @@ export default class DGActorSheet extends ComposedActorSheetBase {
|
||||
this._setRightClickListeners();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_prepareTabs(group) {
|
||||
const tabs = super._prepareTabs(group);
|
||||
|
||||
if (!this.actor.limited || this.actor.type !== "agent") return tabs;
|
||||
|
||||
return { bio: { ...tabs.bio, active: true, cssClass: "active" } };
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _onRender(context, options) {
|
||||
await super._onRender(context, options);
|
||||
@@ -156,6 +147,7 @@ export default class DGActorSheet extends ComposedActorSheetBase {
|
||||
|
||||
this._setupItemDragListeners();
|
||||
this._tooltipsSettings(element);
|
||||
this._bindAeBackedInputs(element);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
|
||||
@@ -1,60 +1,83 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import DGSheetMixin from "./base-sheet.js";
|
||||
import EffectsTabMixin from "./mixins/effects-tab-mixin.js";
|
||||
import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js";
|
||||
|
||||
const { ItemSheetV2 } = foundry.applications.sheets;
|
||||
const ITEM_PARTS_PATH = `${BASE_TEMPLATE_PATH}/item/parts`;
|
||||
const TAB_NAV_TEMPLATE = "templates/generic/tab-navigation.hbs";
|
||||
|
||||
/** Tab group id used by item sheet templates (`data-group="primary"`). */
|
||||
const ITEM_TAB_GROUP = "primary";
|
||||
|
||||
const EFFECTS_TAB = /** @type {const} */ ({
|
||||
id: "effects",
|
||||
label: "DG.Navigation.Item.effects",
|
||||
});
|
||||
|
||||
const EFFECTS_PART = /** @type {const} */ ({
|
||||
effects: {
|
||||
template: `${ITEM_PARTS_PATH}/effects-tab.html`,
|
||||
scrollable: [""],
|
||||
},
|
||||
});
|
||||
|
||||
const TABS_PART = /** @type {const} */ ({
|
||||
tabs: { template: TAB_NAV_TEMPLATE },
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} config
|
||||
* @param {string} config.header
|
||||
* @param {string} config.description
|
||||
* @param {string} config.descriptionLabel
|
||||
* @returns {object}
|
||||
*/
|
||||
function activeEffectItemLayout({ header, description, descriptionLabel }) {
|
||||
return {
|
||||
tabs: {
|
||||
initial: "description",
|
||||
tabs: [
|
||||
{ id: "description", label: descriptionLabel },
|
||||
EFFECTS_TAB,
|
||||
],
|
||||
},
|
||||
parts: {
|
||||
header: { template: header },
|
||||
...TABS_PART,
|
||||
description: { template: description, scrollable: [""] },
|
||||
...EFFECTS_PART,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Tab and part layout per item type. */
|
||||
const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
|
||||
weapon: {
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/weapon-header.html` },
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/weapon-description.html`,
|
||||
scrollable: [""],
|
||||
},
|
||||
},
|
||||
},
|
||||
gear: {
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/gear-header.html` },
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/gear-description.html`,
|
||||
scrollable: [""],
|
||||
},
|
||||
},
|
||||
},
|
||||
armor: {
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/armor-header.html` },
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/armor-description.html`,
|
||||
scrollable: [""],
|
||||
},
|
||||
},
|
||||
},
|
||||
bond: {
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/bond-header.html` },
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/bond-description.html`,
|
||||
scrollable: [""],
|
||||
},
|
||||
},
|
||||
},
|
||||
motivation: {
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/motivation-header.html` },
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/motivation-description.html`,
|
||||
scrollable: [""],
|
||||
},
|
||||
},
|
||||
},
|
||||
weapon: activeEffectItemLayout({
|
||||
header: `${ITEM_PARTS_PATH}/weapon-header.html`,
|
||||
description: `${ITEM_PARTS_PATH}/weapon-description.html`,
|
||||
descriptionLabel: "DG.ItemWindow.Weapons.Description",
|
||||
}),
|
||||
gear: activeEffectItemLayout({
|
||||
header: `${ITEM_PARTS_PATH}/gear-header.html`,
|
||||
description: `${ITEM_PARTS_PATH}/gear-description.html`,
|
||||
descriptionLabel: "DG.ItemWindow.Gear.Description",
|
||||
}),
|
||||
armor: activeEffectItemLayout({
|
||||
header: `${ITEM_PARTS_PATH}/armor-header.html`,
|
||||
description: `${ITEM_PARTS_PATH}/armor-description.html`,
|
||||
descriptionLabel: "DG.ItemWindow.Armor.Description",
|
||||
}),
|
||||
bond: activeEffectItemLayout({
|
||||
header: `${ITEM_PARTS_PATH}/bond-header.html`,
|
||||
description: `${ITEM_PARTS_PATH}/bond-description.html`,
|
||||
descriptionLabel: "DG.ItemWindow.Bonds.Description",
|
||||
}),
|
||||
motivation: activeEffectItemLayout({
|
||||
header: `${ITEM_PARTS_PATH}/motivation-header.html`,
|
||||
description: `${ITEM_PARTS_PATH}/motivation-description.html`,
|
||||
descriptionLabel: "DG.ItemWindow.Motivations.Description",
|
||||
}),
|
||||
tome: {
|
||||
tabs: {
|
||||
initial: "description",
|
||||
@@ -69,7 +92,7 @@ const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
|
||||
},
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/tome-header.html` },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
...TABS_PART,
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/tome-description.html`,
|
||||
scrollable: [""],
|
||||
@@ -94,7 +117,7 @@ const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
|
||||
},
|
||||
parts: {
|
||||
header: { template: `${ITEM_PARTS_PATH}/ritual-header.html` },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
...TABS_PART,
|
||||
description: {
|
||||
template: `${ITEM_PARTS_PATH}/ritual-description.html`,
|
||||
scrollable: [""],
|
||||
@@ -107,10 +130,12 @@ const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
|
||||
},
|
||||
});
|
||||
|
||||
const ItemSheetBase = EffectsTabMixin(DGSheetMixin(ItemSheetV2));
|
||||
|
||||
/**
|
||||
* @extends {DGSheetMixin(ItemSheetV2)}
|
||||
* @extends {ItemSheetBase}
|
||||
*/
|
||||
export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) {
|
||||
export default class DGItemSheet extends ItemSheetBase {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = /** @type {const} */ ({
|
||||
classes: ["item"],
|
||||
@@ -118,6 +143,10 @@ export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) {
|
||||
actions: {
|
||||
roll: DGItemSheet._onRoll,
|
||||
toggleTomeRevealed: DGItemSheet._toggleTomeRevealed,
|
||||
createEffect: ItemSheetBase.createEffect,
|
||||
openEffect: ItemSheetBase.openEffect,
|
||||
deleteEffect: ItemSheetBase.deleteEffect,
|
||||
toggleEffect: ItemSheetBase.toggleEffect,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/** Copy typographic layout from the real input so the overlay matches except color. */
|
||||
function syncAeDisplayTypography(input, display) {
|
||||
const cs = getComputedStyle(input);
|
||||
display.style.inset = "auto";
|
||||
display.style.top = cs.borderTopWidth;
|
||||
display.style.left = cs.borderLeftWidth;
|
||||
display.style.right = cs.borderRightWidth;
|
||||
display.style.bottom = cs.borderBottomWidth;
|
||||
display.style.font = cs.font;
|
||||
display.style.lineHeight = cs.lineHeight;
|
||||
display.style.padding = cs.padding;
|
||||
display.style.textAlign = cs.textAlign;
|
||||
display.style.letterSpacing = cs.letterSpacing;
|
||||
display.style.boxSizing = cs.boxSizing;
|
||||
}
|
||||
|
||||
/** @type {WeakMap<HTMLElement, ResizeObserver>} */
|
||||
const hostObservers = new WeakMap();
|
||||
|
||||
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
|
||||
export default function AeInputMixin(Base) {
|
||||
return class AeInputHost extends Base {
|
||||
/** @param {HTMLElement} root */
|
||||
_bindAeBackedInputs(root) {
|
||||
for (const host of root.querySelectorAll(".ae-input-host")) {
|
||||
hostObservers.get(host)?.disconnect();
|
||||
|
||||
const input = host.querySelector("input");
|
||||
const display = host.querySelector(".ae-input-display");
|
||||
if (!input || !display) continue;
|
||||
|
||||
const modifier = Number(display.dataset.aeModifier) || 0;
|
||||
|
||||
const syncTypography = () => syncAeDisplayTypography(input, display);
|
||||
|
||||
const syncDisplay = () => {
|
||||
if (host.matches(":focus-within")) return;
|
||||
const base = Number(input.value);
|
||||
if (Number.isFinite(base)) {
|
||||
const effective = Math.max(0, Math.round(base + modifier));
|
||||
display.textContent = String(effective);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener("focus", () => input.select());
|
||||
input.addEventListener("blur", () => {
|
||||
syncTypography();
|
||||
syncDisplay();
|
||||
});
|
||||
|
||||
syncTypography();
|
||||
requestAnimationFrame(syncTypography);
|
||||
syncDisplay();
|
||||
|
||||
const observer = new ResizeObserver(syncTypography);
|
||||
observer.observe(input);
|
||||
hostObservers.set(host, observer);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { SUPPORTED_ITEM_TYPES } from "../../active-effect/effect-fields.js";
|
||||
import { getTransferSuppressionReasonKey } from "../../documents/dg-active-effect.js";
|
||||
|
||||
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
|
||||
export default function EffectsTabMixin(Base) {
|
||||
return class EffectsTabHost extends Base {
|
||||
/** @returns {Actor|Item} */
|
||||
get _effectsParent() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} target
|
||||
* @returns {ActiveEffect|undefined}
|
||||
*/
|
||||
_getEffectDocument(target) {
|
||||
const effectId = target.closest("[data-effect-id]")?.dataset?.effectId;
|
||||
if (!effectId) return undefined;
|
||||
|
||||
const parent = this._effectsParent;
|
||||
if (parent.documentName === "Actor") {
|
||||
if (target.closest("[data-parent-id]")?.dataset?.parentId) {
|
||||
const itemId = target.closest("[data-parent-id]").dataset.parentId;
|
||||
return parent.items.get(itemId)?.effects.get(effectId);
|
||||
}
|
||||
return parent.effects.get(effectId);
|
||||
}
|
||||
|
||||
return parent.effects.get(effectId);
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ temporary: object[], permanent: object[] }>} */
|
||||
async _prepareSheetEffects() {
|
||||
const temporary = [];
|
||||
const permanent = [];
|
||||
const parent = this._effectsParent;
|
||||
|
||||
const effects =
|
||||
parent.documentName === "Actor"
|
||||
? parent.allApplicableEffects()
|
||||
: parent.effects;
|
||||
|
||||
for (const effect of effects) {
|
||||
const entry = {
|
||||
id: effect.id,
|
||||
name: effect.name,
|
||||
img: effect.img,
|
||||
disabled: effect.disabled,
|
||||
active: effect.active,
|
||||
transfer: effect.transfer,
|
||||
suppressionReasonKey: getTransferSuppressionReasonKey(effect),
|
||||
duration: null,
|
||||
source: null,
|
||||
};
|
||||
|
||||
if (effect.parent !== parent && parent.documentName === "Actor") {
|
||||
entry.source = {
|
||||
id: effect.parent.id,
|
||||
name: effect.parent.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (effect.isTemporary) {
|
||||
entry.duration = {
|
||||
label: effect.duration?.label ?? "",
|
||||
};
|
||||
temporary.push(entry);
|
||||
} else {
|
||||
permanent.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return { temporary, permanent };
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext(options) {
|
||||
const context = await super._prepareContext(options);
|
||||
if (this._shouldPrepareSheetEffects()) {
|
||||
context.sheetEffects = await this._prepareSheetEffects();
|
||||
context.canManageEffects = this.isEditable;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
_shouldPrepareSheetEffects() {
|
||||
const parent = this._effectsParent;
|
||||
if (parent.documentName === "Actor") {
|
||||
return parent.type === "agent";
|
||||
}
|
||||
return SUPPORTED_ITEM_TYPES.has(parent.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} _event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async createEffect(_event, target) {
|
||||
const sheet = /** @type {EffectsTabHost} */ (this);
|
||||
const parent = sheet._effectsParent;
|
||||
const documentClass = getDocumentClass("ActiveEffect");
|
||||
|
||||
const docData = {
|
||||
name: game.i18n.localize("DG.ActiveEffects.DefaultName"),
|
||||
img: "systems/deltagreen/assets/icons/magic-shield.svg",
|
||||
transfer: SUPPORTED_ITEM_TYPES.has(parent.type),
|
||||
};
|
||||
|
||||
await documentClass.create(docData, {
|
||||
parent,
|
||||
renderSheet: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} _event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static openEffect(_event, target) {
|
||||
const sheet = /** @type {EffectsTabHost} */ (this);
|
||||
sheet._getEffectDocument(target)?.sheet?.render({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} _event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async deleteEffect(_event, target) {
|
||||
const sheet = /** @type {EffectsTabHost} */ (this);
|
||||
await sheet._getEffectDocument(target)?.deleteDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} _event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static async toggleEffect(_event, target) {
|
||||
const sheet = /** @type {EffectsTabHost} */ (this);
|
||||
const effect = sheet._getEffectDocument(target);
|
||||
if (!effect) return;
|
||||
await effect.update({ disabled: !effect.disabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} _event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
static openEffectOrigin(_event, target) {
|
||||
const sheet = /** @type {EffectsTabHost} */ (this);
|
||||
const itemId = target.closest("[data-parent-id]")?.dataset?.parentId;
|
||||
if (!itemId || sheet._effectsParent.documentName !== "Actor") return;
|
||||
const item = sheet._effectsParent.items.get(itemId);
|
||||
item?.sheet?.render({ force: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -164,7 +164,9 @@ export default function ItemListMixin(Base) {
|
||||
};
|
||||
|
||||
if (type === "bond") {
|
||||
itemData.system.score = this.actor.system.statistics.cha.value;
|
||||
itemData.system.score =
|
||||
this.actor.system.statistics.cha.effectiveValue ??
|
||||
this.actor.system.statistics.cha.value;
|
||||
}
|
||||
|
||||
return this.actor.createEmbeddedDocuments("Item", [itemData]);
|
||||
|
||||
@@ -42,14 +42,6 @@ export default function SkillPrepMixin(Base) {
|
||||
skill.sortLabel = skill.key;
|
||||
}
|
||||
|
||||
if (this.actor.type === "agent") {
|
||||
skill.hasPendingImprovement =
|
||||
skill.proficiency !== skill.targetProficiency;
|
||||
skill.proficiencyInputClass = skill.hasPendingImprovement
|
||||
? "target-proficiency-mod"
|
||||
: "";
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
(this.actor.type === "npc" || this.actor.type === "unnatural") &&
|
||||
|
||||
Reference in New Issue
Block a user