Skip to content

Commit

Permalink
Use tagified traits input for compendium browser feat search (#3577)
Browse files Browse the repository at this point in the history
Bonus: Fix compendium browser feat search launched from PC sheet
  • Loading branch information
stwlam committed Aug 20, 2022
1 parent 52df450 commit 850f6ca
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 232 deletions.
2 changes: 1 addition & 1 deletion src/module/actor/character/data/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ interface FeatCategorySheetData {
label: string;
feats: (SlottedFeat | BonusFeat)[];
/** Will move to sheet data later */
featFilter?: string;
featFilter?: string | null;
}

export { CharacterSheetData, CharacterSheetTabVisibility, FeatCategorySheetData };
23 changes: 18 additions & 5 deletions src/module/actor/character/feats.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FeatPF2e, ItemPF2e } from "@item";
import { ItemGrantData } from "@item/data/base";
import { FeatType } from "@item/feat/data";
import { sluggify } from "@util";
import { CharacterPF2e } from ".";
import { BonusFeat, GrantedFeat, SlottedFeat } from "./data";

Expand All @@ -9,7 +10,7 @@ type FeatSlotLevel = number | { id: string; label: string };
interface FeatCategoryOptions {
id: string;
label: string;
featFilter?: string;
featFilter?: string | null;
supported?: FeatType[];
slots?: FeatSlotLevel[];
level?: number;
Expand Down Expand Up @@ -43,14 +44,26 @@ class CharacterFeats extends Collection<FeatCategory> {
this.createCategory({
id: "ancestry",
label: "PF2E.FeatAncestryHeader",
featFilter: "ancestry-" + actor.ancestry?.slug,
featFilter: actor.system.details.ancestry?.trait
? `ancestry-${actor.system.details.ancestry?.trait}`
: null,
supported: ["ancestry"],
slots: classFeatSlots?.ancestry ?? [],
});

// Attempt to acquire the trait corresponding with actor's class, falling back to homebrew variations
const classSlug = actor.class ? actor.class.slug ?? sluggify(actor.class.name) : null;
const classTrait =
(classSlug ?? "") in CONFIG.PF2E.featTraits
? classSlug
: `hb_${classSlug}` in CONFIG.PF2E.featTraits
? `hb_${classSlug}`
: null;

this.createCategory({
id: "class",
label: "PF2E.FeatClassHeader",
featFilter: "classes-" + actor.class?.slug,
featFilter: classTrait ? `traits-${classTrait},traits-archetype` : null,
supported: ["class"],
slots: classFeatSlots?.class ?? [],
});
Expand Down Expand Up @@ -262,7 +275,7 @@ class FeatCategory {
/** Whether the feats are slotted by level or free-form */
slotted = false;
/** Will move to sheet data later */
featFilter?: string;
featFilter: string | null;

/** Feat Types that are supported */
supported: FeatType[] = [];
Expand All @@ -275,7 +288,7 @@ class FeatCategory {
this.id = options.id;
this.label = options.label;
this.supported = options.supported ?? [];
this.featFilter = options.featFilter;
this.featFilter = options.featFilter ?? null;
if (options.slots) {
this.slotted = true;
for (const level of options.slots) {
Expand Down
40 changes: 21 additions & 19 deletions src/module/actor/character/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PCSheetTabManager } from "./tab-manager";
import { AbilityBuilderPopup } from "../sheet/popups/ability-builder";
import { CharacterConfig } from "./config";
import { DropCanvasItemDataPF2e } from "@module/canvas/drop-canvas-data";
import { InitialFeatFilters } from "@module/apps/compendium-browser/tabs/data";

class CharacterSheetPF2e extends CreatureSheetPF2e<CharacterPF2e> {
protected readonly actorConfigClass = CharacterConfig;
Expand Down Expand Up @@ -342,7 +343,7 @@ class CharacterSheetPF2e extends CreatureSheetPF2e<CharacterPF2e> {
label: "PF2E.FeatBonusHeader",
feats: this.actor.feats.unorganized,
};
return [...this.actor.feats.contents, unorganized];
return [...this.actor.feats, unorganized];
}

/** Disable the initiative button located on the sidebar */
Expand All @@ -364,6 +365,7 @@ class CharacterSheetPF2e extends CreatureSheetPF2e<CharacterPF2e> {

override activateListeners($html: JQuery): void {
super.activateListeners($html);
const html = $html[0];

// Initiative button
if (game.combat) {
Expand Down Expand Up @@ -746,37 +748,37 @@ class CharacterSheetPF2e extends CreatureSheetPF2e<CharacterPF2e> {

$formulas.find(".daily-crafting").on("click", async () => await this.actor.performDailyCrafting());

PCSheetTabManager.initialize(this.actor, $html.find<HTMLAnchorElement>('a[data-action="manage-tabs"]')[0]);
PCSheetTabManager.initialize(this.actor, $html.find<HTMLAnchorElement>("a[data-action=manage-tabs]")[0]);

// Feat Browser shortcut links
$html.find(".feat-browse").on("click", (event) => this.onClickBrowseFeatCompendia(event));
for (const link of html.querySelectorAll<HTMLElement>(".feat-browse").values()) {
link.addEventListener("click", this.onClickBrowseFeatCompendia);
}
}

/** Contextually search the feats tab of the Compendium Browser */
private async onClickBrowseFeatCompendia(event: JQuery.ClickEvent): Promise<void> {
const maxLevel = Number($(event.currentTarget).attr("data-level")) || this.actor.level;
private async onClickBrowseFeatCompendia(event: MouseEvent): Promise<void> {
if (!(event.currentTarget instanceof HTMLElement)) return;

const maxLevel = Number(event.currentTarget.dataset.level) || this.actor.level;
const button: HTMLElement = event.currentTarget;
const checkboxesFilterCodes = button.dataset.filter?.split(",").filter((f) => !!f) ?? [];
if (checkboxesFilterCodes.includes("feattype-general")) checkboxesFilterCodes.push("feattype-skill");
if (checkboxesFilterCodes.includes("feattype-class")) checkboxesFilterCodes.push("feattype-archetype");

const filter: Record<string, string[]> = {};
const filter: InitialFeatFilters = { level: { max: maxLevel }, traits: [] };
for (const filterCode of checkboxesFilterCodes) {
const splitValues = filterCode.split("-");
if (splitValues.length !== 2) {
console.error(
`Invalid filter value for opening the compendium browser:\n'${JSON.stringify(
checkboxesFilterCodes
)}'`
);
return;
const [filterType, value] = filterCode.split("-");
if (!(filterType && value)) {
const codesData = JSON.stringify(checkboxesFilterCodes);
throw ErrorPF2e(`Invalid filter value for opening the compendium browser:\n${codesData}`);
}
if (filterType === "traits") {
filter.traits?.push(value);
}

const [filterType, value] = splitValues;
const filterCategory = filter[filterType] ?? (filter[filterType] = []);
filterCategory.push(value);
}

await game.pf2e.compendiumBrowser.openTab("feat", Object.assign(filter, { levelRange: { max: maxLevel } }));
await game.pf2e.compendiumBrowser.openTab("feat", filter);
}

/** Handle changing of proficiency-rank via dropdown */
Expand Down
4 changes: 2 additions & 2 deletions src/module/actor/sheet/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,7 @@ export abstract class ActorSheetPF2e<TActor extends ActorPF2e> extends ActorShee
const data = super._getSubmitData(updateData);

// Use delta values for inputs that have `data-allow-delta` if input value starts with + or -
for (const el of Array.from(this.form.elements)) {
for (const el of this.form.elements) {
if (el instanceof HTMLInputElement && el.dataset.allowDelta !== undefined) {
const strValue = el.value.trim();
const value = Number(strValue);
Expand All @@ -1223,7 +1223,7 @@ export abstract class ActorSheetPF2e<TActor extends ActorPF2e> extends ActorShee

// Process tagify. Tagify has a convention (used in their codebase as well) where it prepends the input element
const tagifyInputElements = this.form.querySelectorAll<HTMLInputElement>("tags.tagify ~ input");
for (const inputEl of Array.from(tagifyInputElements)) {
for (const inputEl of tagifyInputElements.values()) {
const path = inputEl.name;
const selections = data[path];
if (Array.isArray(selections)) {
Expand Down
103 changes: 37 additions & 66 deletions src/module/apps/compendium-browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,6 @@ class CompendiumBrowser extends Application {
openTab(tab: "hazard", filter?: InitialHazardFilters): Promise<void>;
openTab(tab: "spell", filter?: InitialSpellFilters): Promise<void>;
openTab(tab: "settings"): Promise<void>;

async openTab(tab: TabName, filter: InitialFilters = {}): Promise<void> {
this.initialFilter = filter;
await this._render(true);
Expand Down Expand Up @@ -339,12 +338,12 @@ class CompendiumBrowser extends Application {
return filterType;
})();

// Checkboxes
if (
currentTab.filterData.checkboxes !== undefined &&
currentTab.filterData.checkboxes &&
objectHasKey(currentTab.filterData.checkboxes, mappedFilterType) &&
Array.isArray(filterValue)
) {
// Checkboxes
const checkbox = currentTab.filterData.checkboxes[mappedFilterType];
for (const value of filterValue) {
const option = checkbox.options[value];
Expand All @@ -358,13 +357,12 @@ class CompendiumBrowser extends Application {
);
}
}
}
// Selects
else if (
currentTab.filterData.selects !== undefined &&
} else if (
currentTab.filterData.selects &&
objectHasKey(currentTab.filterData.selects, mappedFilterType) &&
typeof filterValue === "string"
) {
// Selects
const select = currentTab.filterData.selects[mappedFilterType];
const option = select.options[filterValue];
if (option) {
Expand All @@ -374,13 +372,12 @@ class CompendiumBrowser extends Application {
`Tab '${currentTab.tabName}' select filter '${mappedFilterType}' has no option: '${filterValue}'`
);
}
}
// Multiselects
else if (
currentTab.filterData.multiselects !== undefined &&
} else if (
currentTab.filterData.multiselects &&
objectHasKey(currentTab.filterData.multiselects, mappedFilterType) &&
Array.isArray(filterValue)
) {
// Multiselects
// A convoluted cast is necessary here to not get an infered type of MultiSelectData<PhysicalItem> since MultiSelectData is not exported
const multiselects = (currentTab.filterData.multiselects as BaseFilterData["multiselects"])!;
const multiselect = multiselects[mappedFilterType];
Expand All @@ -394,13 +391,12 @@ class CompendiumBrowser extends Application {
);
}
}
}
// Ranges (e.g. price)
else if (
currentTab.filterData.ranges !== undefined &&
} else if (
currentTab.filterData.ranges &&
objectHasKey(currentTab.filterData.ranges, mappedFilterType) &&
this.isRange(filterValue)
this.#isRange(filterValue)
) {
// Ranges (e.g. price)
if (
(filterValue.min !== undefined && filterValue.min !== null) ||
(filterValue.max !== undefined && filterValue.max !== null)
Expand Down Expand Up @@ -441,44 +437,29 @@ class CompendiumBrowser extends Application {

range.isExpanded = true;
}
}
// Sliders (e.g. level)
else if (
currentTab.filterData.sliders !== undefined &&
} else if (
currentTab.filterData.sliders &&
objectHasKey(currentTab.filterData.sliders, mappedFilterType) &&
this.isRange(filterValue)
this.#isRange(filterValue) &&
(typeof filterValue.min === "number" || typeof filterValue.max === "number")
) {
if (
(filterValue.min !== undefined && filterValue.min !== null) ||
(filterValue.max !== undefined && filterValue.max !== null)
) {
const slider = currentTab.filterData.sliders[mappedFilterType];

let minValue = typeof filterValue.min === "string" ? parseInt(filterValue.min) : filterValue.min;
let maxValue = typeof filterValue.max === "string" ? parseInt(filterValue.max) : filterValue.max;
if ((minValue && isNaN(minValue)) || (maxValue && isNaN(maxValue))) {
console.error(`Invalid filter value for '${filterType}', it needs to be a valid number.`);
continue;
}

minValue =
minValue !== undefined && minValue !== null
? Math.clamped(minValue, slider.values.lowerLimit, slider.values.upperLimit)
: slider.values.lowerLimit;
maxValue =
maxValue !== undefined && maxValue !== null
? Math.clamped(maxValue, slider.values.lowerLimit, slider.values.upperLimit)
: slider.values.upperLimit;

// Set max value to min value if min value is higher
if (minValue > maxValue) {
maxValue = minValue;
}
// Sliders (e.g. level)
const slider = currentTab.filterData.sliders[mappedFilterType];

const minValue =
typeof filterValue.min === "number"
? Math.clamped(filterValue.min, slider.values.lowerLimit, slider.values.upperLimit) || 0
: slider.values.lowerLimit;
const maxValue = Math.max(
minValue,
typeof filterValue.max === "number"
? Math.clamped(filterValue.max, slider.values.lowerLimit, slider.values.upperLimit) || 0
: slider.values.upperLimit
);

slider.values.min = minValue;
slider.values.max = maxValue;
slider.isExpanded = true;
}
slider.values.min = minValue;
slider.values.max = maxValue;
slider.isExpanded = true;
}
// Filter name did not match a filter on the tab
else {
Expand All @@ -489,21 +470,11 @@ class CompendiumBrowser extends Application {
this.initialFilter = {};
}

private isRange(value: unknown): value is { min: number | string | undefined; max: number | string | undefined } {
if (typeof value === "object" && value !== null && "min" in value && "max" in value) {
const range = value as { min: unknown; max: unknown };
return (
(range.min === undefined ||
range.min === null ||
typeof range.min === "number" ||
typeof range.min === "string") &&
(range.max === undefined ||
range.max === null ||
typeof range.max === "number" ||
typeof range.max === "string")
);
}
return false;
#isRange(value: unknown): value is { min?: number | string; max?: number | string } {
return (
isObject<{ min: unknown; max: unknown }>(value) &&
(["number", "string"].includes(typeof value.min) || ["number", "string"].includes(typeof value.max))
);
}

loadedPacks(tab: TabName): string[] {
Expand Down
6 changes: 4 additions & 2 deletions src/module/apps/compendium-browser/tabs/data.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FeatTrait } from "@item/feat/data";
import { PhysicalItemTrait } from "@item/physical/data";
import { CommonSortByOption, SortByOption, SortDirection } from "../data";

Expand Down Expand Up @@ -81,7 +82,8 @@ interface EquipmentFilters extends BaseFilterData {
}

interface FeatFilters extends BaseFilterData {
checkboxes: Record<"ancestry" | "classes" | "feattype" | "skills" | "rarity" | "source" | "traits", CheckboxData>;
checkboxes: Record<"feattype" | "skills" | "rarity" | "source", CheckboxData>;
multiselects: Record<"traits", MultiselectData<FeatTrait>>;
sliders: Record<"level", SliderData>;
}

Expand Down Expand Up @@ -139,7 +141,7 @@ interface InitialFeatFilters extends BaseInitialFilters {
rarity?: string[];
source?: string[];
traits?: string[];
levelRange?: { min?: number; max?: number };
level?: { min?: number; max?: number };
orderBy?: CommonSortByOption;
}

Expand Down
14 changes: 6 additions & 8 deletions src/module/apps/compendium-browser/tabs/equipment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,21 @@ export class CompendiumBrowserEquipmentTab extends CompendiumBrowserTab {
) {
return false;
}
// Weapons

// Weapon categories
if (
checkboxes.weaponTypes.selected.length > 0 &&
!this.arrayIncludes(checkboxes.weaponTypes.selected, [entry.category, entry.group])
) {
return false;
}

// Traits
if (
multiselects.traits.selected.length > 0 &&
!this.arrayIncludes(
multiselects.traits.selected.map((s) => s.value),
entry.traits
)
) {
const selectedTraits = multiselects.traits.selected.map((s) => s.value);
if (selectedTraits.length > 0 && !selectedTraits.some((t) => entry.traits.includes(t))) {
return false;
}

// Source
if (checkboxes.source.selected.length > 0 && !checkboxes.source.selected.includes(entry.source)) {
return false;
Expand Down
Loading

0 comments on commit 850f6ca

Please sign in to comment.