Skip to content

Commit

Permalink
Add ability for features to suppress/disable other features (foundryv…
Browse files Browse the repository at this point in the history
  • Loading branch information
CarlosFdez authored Nov 3, 2024
1 parent e933c9a commit 28458c5
Show file tree
Hide file tree
Showing 16 changed files with 159 additions and 40 deletions.
3 changes: 3 additions & 0 deletions src/module/actor/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,9 @@ class ActorPF2e<TParent extends TokenDocumentPF2e | null = TokenDocumentPF2e | n

for (const item of this.items) {
item.prepareSiblingData?.();
}

for (const item of this.items) {
item.prepareActorData?.();
}

Expand Down
5 changes: 5 additions & 0 deletions src/module/actor/character/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ class CharacterPF2e<TParent extends TokenDocumentPF2e | null = TokenDocumentPF2e
* modifiers according to them.
*/
override prepareDataFromItems(): void {
// Set up feat hierarchies first, so that we know who is a parent of whom later
for (const feat of this.itemTypes.feat) {
feat.establishHierarchy();
}

super.prepareDataFromItems();
this.prepareBuildData();
}
Expand Down
2 changes: 1 addition & 1 deletion src/module/actor/character/feats/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class FeatGroup<TActor extends ActorPF2e = ActorPF2e, TItem extends FeatLike = F
? (feat.system.level.taken?.toString() ?? "")
: (feat.system.location ?? "");
const slot: FeatSlot<TItem> | undefined = this.slots[slotId];
if (!slot && this.slotted) return false;
if ((!slot && this.slotted) || feat.suppressed) return false;

if (slot?.feat) {
console.debug(`PF2e System | Multiple feats with same index: ${feat.name}, ${slot.feat.name}`);
Expand Down
1 change: 1 addition & 0 deletions src/module/actor/character/feats/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface FeatLike<TParent extends ActorPF2e | null = ActorPF2e | null> extends
system: ItemSystemData & {
location: string | null;
};
suppressed?: boolean;
}

/** Data that defines how a feat group is structured */
Expand Down
21 changes: 6 additions & 15 deletions src/module/item/ability/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ItemPF2e } from "@item";
import { ActionCost, FrequencySource } from "@item/base/data/system.ts";
import type { FeatSheetPF2e } from "@item/feat/sheet.ts";
import { RangeData } from "@item/types.ts";
import { ErrorPF2e, htmlQuery, isImageFilePath } from "@util";
import { htmlQuery, isImageFilePath } from "@util";
import * as R from "remeda";
import type { AbilitySystemData, SelfEffectReference } from "./data.ts";
import type { AbilitySheetPF2e } from "./sheet.ts";
Expand Down Expand Up @@ -87,23 +87,14 @@ interface SelfEffectSheetReference extends SelfEffectReference {
pack: string | null;
}

/** Save data from an effect item dropped on an ability or feat sheet. */
async function handleSelfEffectDrop(sheet: AbilitySheetPF2e | FeatSheetPF2e, event: DragEvent): Promise<void> {
/** Save data from an effect item dropped on an ability or feat sheet. Returns true if handled */
async function handleSelfEffectDrop(sheet: AbilitySheetPF2e | FeatSheetPF2e, item: ItemPF2e): Promise<boolean> {
if (!sheet.isEditable || sheet.item.system.actionType.value === "passive") {
return;
return false;
}
const item = await (async (): Promise<ItemPF2e | null> => {
try {
const dataString = event.dataTransfer?.getData("text/plain");
const dropData = JSON.parse(dataString ?? "");
return (await ItemPF2e.fromDropData(dropData)) ?? null;
} catch {
return null;
}
})();
if (!item?.isOfType("effect")) throw ErrorPF2e("Invalid item drop");

if (!item?.isOfType("effect")) return false;
await sheet.item.update({ "system.selfEffect": { uuid: item.uuid, name: item.name } });
return true;
}

function createActionRangeLabel(range: Maybe<RangeData>): string | null {
Expand Down
9 changes: 8 additions & 1 deletion src/module/item/ability/sheet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { AbilityItemPF2e } from "@item/ability/document.ts";
import { ItemSheetDataPF2e, ItemSheetOptions, ItemSheetPF2e } from "@item/base/sheet/sheet.ts";
import { getItemFromDragEvent } from "@module/sheet/helpers.ts";
import { ancestryTraits } from "@scripts/config/traits.ts";
import { ErrorPF2e } from "@util";
import * as R from "remeda";
import type { AbilitySystemSchema, SelfEffectReference } from "./data.ts";
import { activateActionSheetListeners, createSelfEffectSheetData, handleSelfEffectDrop } from "./helpers.ts";
Expand Down Expand Up @@ -60,7 +62,12 @@ class AbilitySheetPF2e extends ItemSheetPF2e<AbilityItemPF2e> {
}

override async _onDrop(event: DragEvent): Promise<void> {
return handleSelfEffectDrop(this, event);
const item = await getItemFromDragEvent(event);
if (!item) return;

if (!(await handleSelfEffectDrop(this, item))) {
throw ErrorPF2e("Invalid item drop");
}
}
}

Expand Down
17 changes: 16 additions & 1 deletion src/module/item/base/sheet/sheet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ItemPF2e } from "@item";
import { ItemPF2e } from "@item";
import { ItemSourcePF2e } from "@item/base/data/index.ts";
import { Rarity } from "@module/data.ts";
import { RuleElements, RuleElementSource } from "@module/rules/index.ts";
Expand Down Expand Up @@ -566,6 +566,21 @@ class ItemSheetPF2e<TItem extends ItemPF2e> extends ItemSheet<TItem, ItemSheetOp
refreshAnchor.dataset.tooltip = "PF2E.Item.RefreshFromCompendium.Tooltip.Enabled";
}
}

// View a referenced item
for (const link of htmlQueryAll(html, "a[data-action=view-item]")) {
link.addEventListener("click", async (): Promise<void> => {
const uuid = htmlClosest(link, "li")?.dataset.uuid ?? "";
const item = await fromUuid(uuid);
if (!(item instanceof ItemPF2e)) {
this.render(false);
ui.notifications.error(`An item with the UUID "${uuid}" no longer exists`);
return;
}

item.sheet.render(true);
});
}
}

/** Add button to refresh from compendium if setting is enabled. */
Expand Down
15 changes: 0 additions & 15 deletions src/module/item/deity/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,6 @@ export class DeitySheetPF2e extends ItemSheetPF2e<DeityPF2e> {
const clericSpells = htmlQuery(html, ".cleric-spells");
if (!clericSpells) return;

// View one of the spells
for (const link of htmlQueryAll(clericSpells, "a[data-action=view-spell]")) {
link.addEventListener("click", async (): Promise<void> => {
const uuid = htmlClosest(link, "li")?.dataset.uuid ?? "";
const spell = await fromUuid(uuid);
if (!(spell instanceof SpellPF2e)) {
this.render(false);
ui.notifications.error(`A spell with the UUID "${uuid}" no longer exists`);
return;
}

spell.sheet.render(true);
});
}

// Remove a stored spell reference
for (const link of htmlQueryAll(clericSpells, "a[data-action=remove-spell]")) {
link.addEventListener("click", async (): Promise<void> => {
Expand Down
1 change: 1 addition & 0 deletions src/module/item/feat/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ interface FeatSubfeatures {
languages: LanguagesSubfeature;
proficiencies: { [K in IncreasableProficiency]?: { rank: OneToFour; attribute?: AttributeString | null } };
senses: { [K in SenseType]?: SenseSubfeature };
suppressedFeatures: ItemUUID[];
}

interface LanguagesSubfeature {
Expand Down
38 changes: 34 additions & 4 deletions src/module/item/feat/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import { getActionCostRollOptions, normalizeActionChangeData, processSanctificat
import { AbilityTraitToggles } from "@item/ability/trait-toggles.ts";
import { ActionCost, Frequency, RawItemChatData } from "@item/base/data/index.ts";
import { Rarity } from "@module/data.ts";
import { RuleElementSource } from "@module/rules/index.ts";
import { RuleElementOptions, RuleElementPF2e, RuleElementSource } from "@module/rules/index.ts";
import type { UserPF2e } from "@module/user/index.ts";
import { ErrorPF2e, objectHasKey, setHasElement, sluggify } from "@util";
import * as R from "remeda";
import { FeatSource, FeatSubfeatures, FeatSystemData } from "./data.ts";
import { featCanHaveKeyOptions } from "./helpers.ts";
import { featCanHaveKeyOptions, suppressFeats } from "./helpers.ts";
import { FeatOrFeatureCategory, FeatTrait } from "./types.ts";
import { FEATURE_CATEGORIES, FEAT_CATEGORIES } from "./values.ts";

class FeatPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends ItemPF2e<TParent> {
declare group: FeatGroup | null;
declare grants: (FeatPF2e<ActorPF2e> | HeritagePF2e<ActorPF2e>)[];

/** If suppressed, this feature should not be assigned to any feat category nor create rule elements */
declare suppressed: boolean;

static override get validTraits(): Record<FeatTrait, string> {
return CONFIG.PF2E.featTraits;
}
Expand Down Expand Up @@ -77,6 +80,7 @@ class FeatPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Item

this.group = null;
this.system.level.taken ??= null;
this.suppressed = false;

// Handle legacy data with empty-string locations
this.system.location ||= null;
Expand Down Expand Up @@ -130,6 +134,7 @@ class FeatPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Item
languages: { granted: [], slots: 0 },
proficiencies: {},
senses: {},
suppressedFeatures: [],
} satisfies FeatSubfeatures,
this.system.subfeatures ?? {},
);
Expand All @@ -147,14 +152,19 @@ class FeatPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Item
throw ErrorPF2e("Feats much be embedded in PC-type actors");
}

// Exit early if the feat is being suppressed
if (this.suppressed) return;

// Set a self roll option for this feat(ure)
const prefix = this.isFeature ? "feature" : "feat";
const slug = this.slug ?? sluggify(this.name);
actor.rollOptions.all[`${prefix}:${slug}`] = true;

// Process subfeatures
const subfeatures = this.system.subfeatures;
if (!featCanHaveKeyOptions(this)) subfeatures.keyOptions = [];
if (!featCanHaveKeyOptions(this)) {
subfeatures.keyOptions = [];
}

// Key attribute options
if (subfeatures.keyOptions.length > 0) {
Expand Down Expand Up @@ -267,7 +277,8 @@ class FeatPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Item
}
}

override prepareSiblingData(): void {
/** Assigns the grants of this item based on the given item. */
establishHierarchy(): void {
this.grants = Object.values(this.flags.pf2e.itemGrants).flatMap((grant) => {
const item = this.actor?.items.get(grant.id);
return (item?.isOfType("feat") && !item.system.location) || item?.isOfType("heritage") ? [item] : [];
Expand All @@ -277,10 +288,29 @@ class FeatPF2e<TParent extends ActorPF2e | null = ActorPF2e | null> extends Item
}
}

override prepareSiblingData(): void {
if (!this.actor) return;

const subfeatures = this.system.subfeatures;
if (!featCanHaveKeyOptions(this)) {
subfeatures.suppressedFeatures = [];
} else if (subfeatures.suppressedFeatures.length) {
const uuids: string[] = subfeatures.suppressedFeatures;
const feats = this.actor.itemTypes.feat.filter((f) => uuids.includes(f.sourceId ?? ""));
suppressFeats(feats);
}
}

override onPrepareSynthetics(this: FeatPF2e<ActorPF2e>): void {
processSanctification(this);
}

/** Overriden to not create rule elements when suppressed */
override prepareRuleElements(options?: Omit<RuleElementOptions, "parent">): RuleElementPF2e[] {
if (this.suppressed) return [];
return super.prepareRuleElements(options);
}

override async getChatData(
this: FeatPF2e<ActorPF2e>,
htmlOptions: EnrichmentOptions = {},
Expand Down
11 changes: 10 additions & 1 deletion src/module/item/feat/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ActorPF2e } from "@actor";
import type { FeatPF2e } from "./document.ts";

/**
Expand All @@ -14,4 +15,12 @@ function featCanHaveKeyOptions(feat: FeatPF2e): boolean {
return !grantedBy || (grantedBy.isOfType("feat") && grantedBy.category === "classfeature");
}

export { featCanHaveKeyOptions };
/** Recursively suppresses a feat and its granted feats */
function suppressFeats(feats: FeatPF2e[]): void {
for (const feat of feats) {
feat.suppressed = true;
suppressFeats(feat.grants.filter((i): i is FeatPF2e<ActorPF2e> => i.isOfType("feat")));
}
}

export { featCanHaveKeyOptions, suppressFeats };
42 changes: 41 additions & 1 deletion src/module/item/feat/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ItemSheetDataPF2e, ItemSheetOptions, ItemSheetPF2e } from "@item/base/s
import type { FeatPF2e } from "@item/feat/document.ts";
import { WEAPON_CATEGORIES } from "@item/weapon/values.ts";
import { OneToFour } from "@module/data.ts";
import { getItemFromDragEvent } from "@module/sheet/helpers.ts";
import type { HTMLTagifyTagsElement } from "@system/html-elements/tagify-tags.ts";
import {
ErrorPF2e,
Expand All @@ -23,6 +24,7 @@ import {
tagify,
tupleHasValue,
} from "@util";
import { UUIDUtils } from "@util/uuid.ts";
import * as R from "remeda";
import { featCanHaveKeyOptions } from "./helpers.ts";

Expand Down Expand Up @@ -79,6 +81,9 @@ class FeatSheetPF2e extends ItemSheetPF2e<FeatPF2e> {
selfEffect: createSelfEffectSheetData(sheetData.data.selfEffect),
senses: this.#getSenseOptions(),
showPrerequisites,
suppressedFeatures: (await UUIDUtils.fromUUIDs(this.item.system.subfeatures.suppressedFeatures)).map((f) =>
R.pick(f, ["uuid", "name", "img"]),
),
};
}

Expand Down Expand Up @@ -223,6 +228,7 @@ class FeatSheetPF2e extends ItemSheetPF2e<FeatPF2e> {
this.#activateLanguagesListeners(html);
this.#activateProficienciesListeners(html);
this.#activateSensesListeners(html);
this.#activateSuppressedFeaturesListeners(html);
}

#activateLanguagesListeners(html: HTMLElement): void {
Expand Down Expand Up @@ -340,8 +346,41 @@ class FeatSheetPF2e extends ItemSheetPF2e<FeatPF2e> {
});
}

#activateSuppressedFeaturesListeners(html: HTMLElement) {
// Remove a stored spell reference
for (const link of htmlQueryAll(html, "a[data-action=remove-suppressed-feature]")) {
link.addEventListener("click", async (): Promise<void> => {
const uuidToRemove = htmlClosest(link, "li")?.dataset.uuid;
const newFeatures = this.item._source.system.subfeatures?.suppressedFeatures?.filter(
(uuid) => uuid !== uuidToRemove,
);
await this.item.update({ "system.subfeatures.suppressedFeatures": newFeatures ?? [] });
});
}
}

override async _onDrop(event: DragEvent): Promise<void> {
return handleSelfEffectDrop(this, event);
if (!this.isEditable) return;

const item = await getItemFromDragEvent(event);
if (!item) return;

if (await handleSelfEffectDrop(this, item)) return;
if (
item.isOfType("feat") &&
item.category === "classfeature" &&
item.sourceId &&
item.sourceId !== this.item.sourceId
) {
const feats = this.item._source.system.subfeatures?.suppressedFeatures ?? [];
if (!feats.includes(item.sourceId)) {
const newFeatures = [...feats, item.sourceId];
await this.item.update({ "system.subfeatures.suppressedFeatures": newFeatures });
}
return;
}

throw ErrorPF2e("Invalid item drop");
}

protected override _updateObject(event: Event, formData: Record<string, unknown>): Promise<void> {
Expand Down Expand Up @@ -401,6 +440,7 @@ interface FeatSheetData extends ItemSheetDataPF2e<FeatPF2e> {
selfEffect: SelfEffectReference | null;
senses: SenseOption[];
showPrerequisites: boolean;
suppressedFeatures: { uuid: string; name: string; img: string }[];
}

interface LanguageOptions {
Expand Down
12 changes: 12 additions & 0 deletions src/module/sheet/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ItemPF2e } from "@item";
import { htmlClosest, htmlQuery, sortLabeledRecord } from "@util";
import * as R from "remeda";

Expand Down Expand Up @@ -104,6 +105,16 @@ async function maintainFocusInRender(sheet: Application, renderLogic: () => Prom
}
}

async function getItemFromDragEvent(event: DragEvent): Promise<ItemPF2e | null> {
try {
const dataString = event.dataTransfer?.getData("text/plain");
const dropData = JSON.parse(dataString ?? "");
return (await ItemPF2e.fromDropData(dropData)) ?? null;
} catch {
return null;
}
}

interface SheetOption {
value: string;
label: string;
Expand Down Expand Up @@ -137,6 +148,7 @@ export {
createTagifyTraits,
getAdjustedValue,
getAdjustment,
getItemFromDragEvent,
maintainFocusInRender,
};
export type { AdjustedValue, SheetOption, SheetOptions, TagifyEntry };
Loading

0 comments on commit 28458c5

Please sign in to comment.