diff --git a/src/module/actor/base.ts b/src/module/actor/base.ts index bc7a8b24ff4..724c0967a6a 100644 --- a/src/module/actor/base.ts +++ b/src/module/actor/base.ts @@ -602,6 +602,14 @@ class ActorPF2e { + return; // no-op + } + /** Don't allow the user to create in-development actor types. */ static override createDialog( this: ConstructorOf, diff --git a/src/module/actor/character/apps/abc-picker/app.ts b/src/module/actor/character/apps/abc-picker/app.ts index c0ab36986a1..d3b6622c6cd 100644 --- a/src/module/actor/character/apps/abc-picker/app.ts +++ b/src/module/actor/character/apps/abc-picker/app.ts @@ -2,7 +2,11 @@ import type { CharacterPF2e } from "@actor"; import type { ABCItemPF2e, DeityPF2e, ItemPF2e } from "@item"; import type { ItemType } from "@item/base/data/index.ts"; import { Rarity } from "@module/data.ts"; -import { SvelteApplicationMixin, type SvelteApplicationRenderContext } from "@module/sheet/mixin.svelte.ts"; +import { + BaseSvelteState, + SvelteApplicationMixin, + type SvelteApplicationRenderContext, +} from "@module/sheet/mixin.svelte.ts"; import { sluggify } from "@util"; import { UUIDUtils } from "@util/uuid.ts"; import * as R from "remeda"; @@ -32,8 +36,7 @@ interface ABCItemRef { interface ABCPickerContext extends SvelteApplicationRenderContext { actor: CharacterPF2e; - foundryApp: ABCPicker; - state: { prompt: string; itemType: AhBCDType; items: ABCItemRef[] }; + state: BaseSvelteState & { prompt: string; itemType: AhBCDType; items: ABCItemRef[] }; } /** A `Compendium`-like application for presenting A(H)BCD options for a character */ @@ -116,11 +119,13 @@ class ABCPicker extends SvelteApplicationMixin< } protected override async _prepareContext(): Promise { + const base = await super._prepareContext(); const itemType = this.options.itemType; return { + ...base, actor: this.options.actor, - foundryApp: this, state: { + ...base.state, prompt: game.i18n.localize(`PF2E.Actor.Character.ABCPicker.Prompt.${itemType}`), itemType, items: await this.#gatherItems(), diff --git a/src/module/actor/character/apps/formula-picker/app.ts b/src/module/actor/character/apps/formula-picker/app.ts index a1a59de3e99..3a8e5cbb18e 100644 --- a/src/module/actor/character/apps/formula-picker/app.ts +++ b/src/module/actor/character/apps/formula-picker/app.ts @@ -5,7 +5,11 @@ import { ResourceData } from "@actor/creature/index.ts"; import { AbilityItemPF2e, FeatPF2e, PhysicalItemPF2e } from "@item"; import { ItemType, TraitChatData } from "@item/base/data/index.ts"; import { Rarity } from "@module/data.ts"; -import { SvelteApplicationMixin, type SvelteApplicationRenderContext } from "@module/sheet/mixin.svelte.ts"; +import { + BaseSvelteState, + SvelteApplicationMixin, + type SvelteApplicationRenderContext, +} from "@module/sheet/mixin.svelte.ts"; import MiniSearch from "minisearch"; import * as R from "remeda"; import { ApplicationConfiguration, ApplicationRenderOptions } from "types/foundry/client-esm/applications/_types.js"; @@ -81,6 +85,8 @@ class FormulaPicker extends SvelteApplicationMixin< } protected override async _prepareContext(): Promise { + const base = await super._prepareContext(); + const { actor, ability, mode } = this.options; const formulas = await ability.getValidFormulas(); const sheetData = await ability.getSheetData(); @@ -102,7 +108,7 @@ class FormulaPicker extends SvelteApplicationMixin< : game.i18n.localize("PF2E.Actor.Character.Crafting.Action.HintResourceless"); return { - foundryApp: this, + ...base, actor, ability, mode: this.options.mode, @@ -122,6 +128,7 @@ class FormulaPicker extends SvelteApplicationMixin< }, searchEngine: this.#searchEngine, state: { + ...base.state, name: this.options.item?.name ?? ability.label, resource, prompt, @@ -163,7 +170,7 @@ interface FormulaPickerContext extends SvelteApplicationRenderContext { onSelect: (uuid: ItemUUID) => void; onDeselect: (uuid: ItemUUID) => void; searchEngine: MiniSearch>; - state: { + state: BaseSvelteState & { name: string; resource: ResourceData | null; prompt: string; diff --git a/src/module/actor/creature/document.ts b/src/module/actor/creature/document.ts index eeac03cc36b..f373bbda9da 100644 --- a/src/module/actor/creature/document.ts +++ b/src/module/actor/creature/document.ts @@ -653,11 +653,11 @@ abstract class CreaturePF2e< return { ...data, slug, label }; } - /** - * Updates a resource. Redirects to special resources if needed. - * Accepts resource slugs in both kebab and dromedary, to handle token updates and direct ones. - */ - async updateResource(resource: string, value: number, { render }: { render?: boolean } = {}): Promise { + override async updateResource( + resource: string, + value: number, + { render }: { render?: boolean } = {}, + ): Promise { const slug = sluggify(resource); const key = sluggify(resource, { camel: "dromedary" }); if (key === "investiture") return; diff --git a/src/module/actor/party/components/overview-tab.svelte b/src/module/actor/party/components/overview-tab.svelte new file mode 100644 index 00000000000..a51cdb15419 --- /dev/null +++ b/src/module/actor/party/components/overview-tab.svelte @@ -0,0 +1,532 @@ + + +
+ {#if !members} +
+ {game.i18n.localize("PF2E.Actor.Party.BlankSlate")} +
+ {/if} + + {#if summary} +
+ +
+
+ {#if !summary.languages} + {game.i18n.localize("PF2E.NoneOption")} + {/if} + {#each summary.languages as language} + + {language.label}{#if language.actors.length > 1} + ({language.actors.length}){/if} + + {/each} +
+
+ {#each summary.skills as skill} + {@render skillTag(skill)} + {/each} +
+
+
+ {#each summary.knowledge.regular as skill} + {@render skillTag(skill)} + {/each} +
+
+ {#each summary.knowledge.lore as skill} + {@render skillTag(skill)} + {/each} +
+
+
+
+ {/if} + + {#each members as member (member.actor.uuid)} +
+
+ + {#if member.hp} +
+ {#if member.hp.temp} +
+ {/if} +
+ {member.hp.value} / {member.hp.max} +
+ {/if} +
+
+
+
+ {member.actor.name} + {#if user.isGM} + + {/if} +
+ {#if member.blurb} +
+ {member.blurb} + {#if member.genderPronouns} +
+ {member.genderPronouns} + {/if} +
+ {/if} + {#if member.resource} + {@const resource = member.resource} + + {/if} +
+ +
+
+ + {member.ac} +
+ {#if member.saves} +
+ + + {signedInteger(member.saves.fortitude)} + + + + {signedInteger(member.saves.reflex)} + + + + {signedInteger(member.saves.will)} + +
+ {/if} + +
+ +
+ {#each member.senses as sense} + {game.i18n.localize(sense.label)} + {/each} + {#if !member.senses} + {game.i18n.localize("PF2E.Actor.Party.NoSpecialSenses")} + {/if} +
+
+
+ +
+ {#if member.perception} + {@const perception = member.perception} + + {/if} + {#each member.bestSkills as skill} + {@render skillTag(skill)} + {/each} +
+
+
+ {/each} +
+ +{#snippet skillTag(skill: SkillSheetData)} + + {game.i18n.localize(skill.label)} + {signedInteger(skill.mod)} + +{/snippet} + + diff --git a/src/module/actor/party/components/sub-navigation.svelte b/src/module/actor/party/components/sub-navigation.svelte new file mode 100644 index 00000000000..32309fb2a78 --- /dev/null +++ b/src/module/actor/party/components/sub-navigation.svelte @@ -0,0 +1,75 @@ + + + + + diff --git a/src/module/actor/party/document.ts b/src/module/actor/party/document.ts index 2e6d250f53d..43ff912bb41 100644 --- a/src/module/actor/party/document.ts +++ b/src/module/actor/party/document.ts @@ -11,7 +11,6 @@ import * as R from "remeda"; import type { DataModelValidationOptions } from "types/foundry/common/abstract/data.d.ts"; import { PartySource, PartySystemData } from "./data.ts"; import { Kingdom } from "./kingdom/model.ts"; -import { PartySheetRenderOptions } from "./sheet.ts"; import { PartyCampaign, PartyUpdateOperation } from "./types.ts"; class PartyPF2e extends ActorPF2e { @@ -179,7 +178,7 @@ class PartyPF2e { - super.reset(); - this.sheet.render(false, { actor: true } as PartySheetRenderOptions); - }, 50); - /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ diff --git a/src/module/actor/party/sheet-new.ts b/src/module/actor/party/sheet-new.ts new file mode 100644 index 00000000000..eff5129041f --- /dev/null +++ b/src/module/actor/party/sheet-new.ts @@ -0,0 +1,436 @@ +import { HitPointsSummary } from "@actor/base.ts"; +import { CreaturePF2e, Language, ResourceData } from "@actor/creature/index.ts"; +import { ActorSizePF2e } from "@actor/data/size.ts"; +import { isReallyPC } from "@actor/helpers.ts"; +import { CoinageSummary, InventoryItem, SheetInventory } from "@actor/sheet/data-types.ts"; +import { condenseSenses, createBulkPerLabel } from "@actor/sheet/helpers.ts"; +import { SaveType } from "@actor/types.ts"; +import { SAVE_TYPES } from "@actor/values.ts"; +import { PhysicalItemPF2e } from "@item"; +import { ZeroToFour } from "@module/data.ts"; +import { SheetOptions, coinsToSheetData, createSheetTags } from "@module/sheet/helpers.ts"; +import { BaseSvelteState, SvelteApplicationMixin, SvelteApplicationRenderContext } from "@module/sheet/mixin.svelte.ts"; +import { Statistic } from "@system/statistic/statistic.ts"; +import * as R from "remeda"; +import { ApplicationRenderOptions } from "types/foundry/client-esm/applications/_types.js"; +import type { DocumentSheetConfiguration } from "types/foundry/client-esm/applications/api/document-sheet.js"; +import { PartyPF2e } from "./document.ts"; +import Root from "./sheet.svelte"; + +interface PartySheetConfiguration extends DocumentSheetConfiguration {} + +class PartySheetV2 extends SvelteApplicationMixin(foundry.applications.api.DocumentSheetV2) { + static override DEFAULT_OPTIONS = { + position: { + width: 720, + height: "auto", + }, + window: { + contentClasses: ["compact"], + resizable: true, + }, + form: { + submitOnChange: true, + }, + }; + + override root = Root; + + declare options: PartySheetConfiguration; + + override tabGroups = { + main: "overview", + summary: "languages", + }; + + get actor(): PartyPF2e { + return this.document; + } + + override _canRender(options: ApplicationRenderOptions): false | void { + if (super._canRender(options) === false) { + return false; + } + + if (!options.force && !this.rendered) { + return false; + } + } + + protected override async _prepareContext(): Promise { + const base = await super._prepareContext(); + const members = this.actor.members; + const canDistributeCoins = + game.user.isGM && this.isEditable + ? this.actor.inventory.coins.copperValue > 0 && members.some(isReallyPC) + : false; + + const travelSpeed = this.actor.system.attributes.speed.total; + const restricted = !(game.user.isGM || game.pf2e.settings.metagame.partyStats); + + return { + ...base, + state: { + ...base.state, + actor: R.pick(this.document, ["id", "name", "img", "uuid", "type"]), + playerRestricted: !game.pf2e.settings.metagame.partyStats, + restricted, + members: this.#prepareMembers(), + overviewSummary: this.#prepareOverviewSummary(), + inventorySummary: { + totalCoins: + R.sumBy(members, (actor) => actor.inventory.coins.goldValue ?? 0) + + this.actor.inventory.coins.goldValue, + totalWealth: + R.sumBy(members, (actor) => actor.inventory.totalWealth.goldValue ?? 0) + + this.actor.inventory.totalWealth.goldValue, + totalBulk: members + .map((actor) => actor.inventory.bulk.value) + .reduce((a, b) => a.plus(b), this.actor.inventory.bulk.value), + }, + inventory: { + coinage: this.prepareCoinage(), + canDistributeCoins, + }, + explorationSummary: { + speed: travelSpeed, + feetPerMinute: travelSpeed * 10, + milesPerHour: travelSpeed / 10, + milesPerDay: travelSpeed * 0.8, + activities: + Object.entries(CONFIG.PF2E.hexplorationActivities).find( + ([max]) => Number(max) >= this.actor.system.attributes.speed.total, + )?.[1] ?? 0, + }, + + // todo: + orphaned: [], + }, + }; + } + + #prepareMembers(): MemberBreakdown[] { + return this.actor.members.map((actor): MemberBreakdown => { + const observer = actor.testUserPermission(game.user, "OBSERVER"); + const restricted = !(game.pf2e.settings.metagame.partyStats || observer); + const genderPronouns = actor.isOfType("character") + ? actor.system.details.gender.value.trim() || null + : null; + console.log("MAPPING MEMBER"); + const blurb = + actor.isOfType("character") && actor.ancestry && actor.class + ? game.i18n.format("PF2E.Actor.Character.Blurb", { + level: actor.level, + ancestry: actor.ancestry.name, + class: actor.class.name, + }) + : actor.isOfType("familiar") && actor.master + ? game.i18n.format("PF2E.Actor.Familiar.Blurb", { master: actor.master.name }) + : actor.isOfType("npc") + ? actor.system.details.blurb.trim() || null + : null; + const activities = actor.isOfType("character") + ? actor.system.exploration.map((id) => actor.items.get(id)).filter(R.isTruthy) + : []; + + const mythicPoints = actor.getResource("mythic-points"); + const heroPoints = isReallyPC(actor) ? actor.getResource("hero-points") : null; + + return { + // Base actor data and permissions + actor: R.pick(actor, ["id", "name", "img", "uuid", "type"]), + owner: actor.isOwner, + observer, + limited: observer || actor.limited, + restricted, + + // View data + hasBulk: actor.inventory.bulk.encumberedAfter !== Infinity, + bestSkills: Object.values(actor.skills ?? {}) + .filter((s) => s.proficient && !s.lore) + .sort((a, b) => b.mod - a.mod) + .slice(0, 4) + .map((s) => ({ slug: s.slug, mod: s.mod, label: s.label, rank: s.rank })), + genderPronouns, + blurb, + resource: mythicPoints?.max ? mythicPoints : heroPoints, + speed: actor.attributes.speed.total, + speeds: [ + { label: "PF2E.Actor.Speed.Label", value: actor.attributes.speed.value }, + ...actor.attributes.speed.otherSpeeds.map((s) => R.pick(s, ["label", "value"])), + ], + senses: (() => { + return condenseSenses(actor.perception.senses.contents).map((r) => ({ + acuity: r.acuity, + labelFull: r.label ?? "", + label: CONFIG.PF2E.senses[r.type] ?? r.type, + })); + })(), + hp: actor.hitPoints, + ac: actor.attributes.ac.value, + saves: R.mapToObj(SAVE_TYPES, (s) => [s, actor.saves[s].mod]), + perception: { + ...R.pick(actor.perception, ["label", "rank", "mod"]), + dc: actor.perception.dc.value, + }, + activities: activities.map((action) => ({ + uuid: action.uuid, + name: action.name, + img: action.img, + traits: createSheetTags(CONFIG.PF2E.actionTraits, action.system.traits?.value ?? []), + })), + totalWealth: actor.inventory.totalWealth.goldValue, + bulk: actor.inventory.bulk.value.toString(), + }; + }); + } + + #prepareOverviewSummary(): PartySheetState["overviewSummary"] | null { + const members = this.actor.members; + if (members.length === 0) return null; + + // Get all member languages. If the common language is taken, replace with "common" explicitly + const commonLanguage = game.pf2e.settings.campaign.languages.commonLanguage; + const allLanguages = new Set(members.flatMap((m) => m.system.details.languages?.value ?? [])); + if (commonLanguage && allLanguages.delete(commonLanguage)) { + allLanguages.add("common"); + } + + const baseKnowledgeSkills = [ + "arcana", + "nature", + "occultism", + "religion", + "crafting", + "society", + "medicine", + ] as const; + + const loreSkills = new Set( + members + .flatMap((m) => Object.values(m.skills)) + .filter((s) => s.lore) + .map((s) => s.slug), + ); + + function getBestSkill(slug: string): SkillSheetData | null { + const bestMember = R.firstBy(members, [(m) => m.skills[slug]?.mod ?? -Infinity, "desc"]); + const statistic = bestMember?.skills[slug]; + return statistic ? R.pick(statistic, ["slug", "mod", "label", "rank"]) : null; + } + + return { + languages: R.sortBy( + [...allLanguages].map( + (language): LanguageSheetData => ({ + slug: language, + label: + language === "common" && commonLanguage + ? game.i18n.format("PF2E.Actor.Creature.Language.CommonLanguage", { + language: game.i18n.localize(CONFIG.PF2E.languages[commonLanguage]), + }) + : game.i18n.localize(CONFIG.PF2E.languages[language]), + actors: this.#getActorsThatUnderstand(language), + }), + ), + (l) => (l.slug === "common" ? "" : l.label), + ), + skills: R.sortBy( + Object.entries(CONFIG.PF2E.skills).map(([slug, { label }]): SkillSheetData => { + const best = getBestSkill(slug); + return best ?? { mod: 0, label, slug, rank: 0 }; + }), + (s) => s.label, + ), + knowledge: { + regular: baseKnowledgeSkills.map(getBestSkill).filter(R.isTruthy), + lore: R.sortBy([...loreSkills].map(getBestSkill).filter(R.isTruthy), (s) => s.label), + }, + }; + } + + protected prepareCoinage(): CoinageSheetData { + const coins = this.actor.inventory.coins; + return { + coins: coinsToSheetData(coins), + totalCoins: coins.goldValue, + totalWealth: this.actor.inventory.totalWealth.goldValue, + }; + } + + protected prepareInventory(): SheetInventory { + const sections: SheetInventory["sections"] = [ + { + label: game.i18n.localize("PF2E.Actor.Inventory.Section.WeaponsAndShields"), + types: ["weapon", "shield"], + items: [], + }, + { label: game.i18n.localize("TYPES.Item.armor"), types: ["armor"], items: [] }, + { label: game.i18n.localize("TYPES.Item.equipment"), types: ["equipment"], items: [] }, + { + label: game.i18n.localize("PF2E.Item.Consumable.Plural"), + types: ["consumable"], + items: [], + }, + { label: game.i18n.localize("TYPES.Item.treasure"), types: ["treasure"], items: [] }, + { label: game.i18n.localize("PF2E.Item.Container.Plural"), types: ["backpack"], items: [] }, + ]; + + const actor = this.actor.clone({}, { keepId: true }); + for (const item of actor.inventory.contents.sort((a, b) => (a.sort || 0) - (b.sort || 0))) { + if (item.isInContainer) continue; + const section = sections.find((s) => s.types.includes(item.type)); + section?.items.push(this.prepareInventoryItem(item)); + } + + return { + sections, + bulk: actor.inventory.bulk, + showValueAlways: actor.isOfType("npc", "loot", "party"), + showUnitBulkPrice: false, + hasStowedWeapons: + actor.itemTypes.weapon.some((i) => i.isStowed) || actor.itemTypes.shield.some((i) => i.isStowed), + hasStowingContainers: actor.itemTypes.backpack.some((c) => c.system.stowing && !c.isInContainer), + invested: actor.inventory.invested, + }; + } + + protected prepareInventoryItem(item: PhysicalItemPF2e): InventoryItem { + const editable = game.user.isGM || item.isIdentified; + const heldItems = item.isOfType("backpack") ? item.contents.map((i) => this.prepareInventoryItem(i)) : null; + heldItems?.sort((a, b) => (a.item.sort || 0) - (b.item.sort || 0)); + + const actor = this.actor; + const actorSize = new ActorSizePF2e({ value: actor.size }); + const itemSize = new ActorSizePF2e({ value: item.size }); + const sizeDifference = itemSize.difference(actorSize, { smallIsMedium: true }); + + return { + item, + canBeEquipped: !item.isStowed, + hasCharges: item.isOfType("consumable") && item.system.uses.max > 0, + heldItems, + isContainer: item.isOfType("backpack"), + isInvestable: false, + isSellable: editable && item.isOfType("treasure") && !item.isCoinage, + itemSize: sizeDifference !== 0 ? itemSize : null, + unitBulk: actor.isOfType("loot") ? createBulkPerLabel(item) : null, + hidden: false, + }; + } + + #getActorsThatUnderstand(slug: Language) { + return this.actor.members.filter((m): m is CreaturePF2e => !!m?.system.details.languages?.value.includes(slug)); + } +} + +interface PartySheetContext extends SvelteApplicationRenderContext { + state: PartySheetState; +} + +interface PartySheetState extends BaseSvelteState { + actor: ActorViewData; + + /** Is the sheet restricted to players? */ + playerRestricted: boolean; + /** Is the sheet restricted to the current user? */ + restricted: boolean; + + members: MemberBreakdown[]; + + overviewSummary: OverviewSummary | null; + inventorySummary: { + totalCoins: number; + totalWealth: number; + totalBulk: unknown; + }; + inventory: { + coinage: CoinageSheetData; + canDistributeCoins: boolean; + }; + explorationSummary: { + speed: number; + feetPerMinute: number; + milesPerHour: number; + milesPerDay: number; + activities: number; + }; + + /** Unsupported items on the sheet, may occur due to disabled campaign data */ + orphaned: never[]; +} + +interface SkillSheetData { + slug: string; + label: string; + mod: number; + rank?: ZeroToFour | null; +} + +interface MemberBreakdown { + actor: ActorViewData; + /** If the actor is owned by the current user */ + owner: boolean; + /** If the actor has observer or greater permission */ + observer: boolean; + /** If the actor has limited or greater permission */ + limited: boolean; + /** If true, the current user is restricted from seeing meta details */ + restricted: boolean; + + genderPronouns: string | null; + blurb: string | null; + resource: ResourceData | null; + hasBulk: boolean; + bestSkills: SkillSheetData[]; + + speed: number; + speeds: { label: string; value: number }[]; + senses: { label: string | null; labelFull: string; acuity?: string }[]; + hp: HitPointsSummary; + ac: number; + saves: Record; + perception: Pick & { dc: number }; + + activities: { + uuid: string; + name: string; + img: string; + traits: SheetOptions; + }[]; + + totalWealth: number; + bulk: string; +} + +interface OverviewSummary { + languages: LanguageSheetData[]; + skills: SkillSheetData[]; + knowledge: { + regular: SkillSheetData[]; + lore: SkillSheetData[]; + }; +} + +interface ActorViewData { + id: string; + name: string; + img: string; + uuid: string; + type: string; +} + +interface LanguageSheetData { + slug: string; + label: string; + actors: unknown[]; +} + +interface CoinageSheetData { + coins: CoinageSummary; + totalCoins: number; + totalWealth: number; +} + +export { PartySheetV2 }; +export type { CoinageSheetData, MemberBreakdown, OverviewSummary, PartySheetContext, SkillSheetData }; diff --git a/src/module/actor/party/sheet.svelte b/src/module/actor/party/sheet.svelte new file mode 100644 index 00000000000..b0f7c366003 --- /dev/null +++ b/src/module/actor/party/sheet.svelte @@ -0,0 +1,867 @@ + + +
+
+
+
+ {actor.name} +
+
+ +
+ + + {#if !restricted} + + {game.i18n.localize("PF2E.Actor.Party.Tabs.Overview")} + + {/if} + + {game.i18n.localize("PF2E.Actor.Party.Tabs.Exploration")} + + + {game.i18n.localize("PF2E.Actor.Party.Tabs.Stash")} + + {#if data.orphaned.length > 0} + {game.i18n.localize("PF2E.Actor.Party.Tabs.Orphaned")} + {/if} + + +
+
+ +
+ +
+ +
+ {#if data.editable} +
+ {game.i18n.localize("PF2E.Actor.Party.ClearActivities.Label")} +
+ + +
+
+ {/if} +
+ {#each data.members as member} + {#if member.actor.type === "character"} +
+
+ +
+ + {#if member.activities.length > 0} +
+ {#each member.activities as activity} +
+ {activity.name} + + {#each Object.values(activity.traits) as trait} + {trait.label} + {/each} + +
+ {/each} +
+ {:else} +
+
+
+
+ {game.i18n.localize("PF2E.Item.Ability.Type.Activity")} +
+
+ {game.i18n.localize("PF2E.Actor.Party.SlotAvailable")} +
+
+
+ {/if} +
+ {/if} + {/each} +
+
+
+ +
+ + +
+ + +
+
+ + {#if data.orphaned.length} +
+
    + {#each data.orphaned as item} + {#if item.isIdentified || data.user.isGM} +
  1. +
    + + + +

    {item.name}

    +
    +
    + {#if data.editable && !item.temporary} + + {/if} +
    +
  2. + {/if} + {/each} +
+
+ {/if} +
+
+ + diff --git a/src/module/actor/sheet/base.ts b/src/module/actor/sheet/base.ts index bd3ba2b11b9..52a6d66abff 100644 --- a/src/module/actor/sheet/base.ts +++ b/src/module/actor/sheet/base.ts @@ -10,12 +10,17 @@ import type { ActionType, ItemSourcePF2e } from "@item/base/data/index.ts"; import { createConsumableFromSpell } from "@item/consumable/spell-consumables.ts"; import { isContainerCycle } from "@item/container/helpers.ts"; import { itemIsOfType } from "@item/helpers.ts"; -import type { Coins } from "@item/physical/data.ts"; import { detachSubitem, sizeItemForActor } from "@item/physical/helpers.ts"; -import { DENOMINATIONS, PHYSICAL_ITEM_TYPES } from "@item/physical/values.ts"; +import { PHYSICAL_ITEM_TYPES } from "@item/physical/values.ts"; import { DropCanvasItemDataPF2e } from "@module/canvas/drop-canvas-data.ts"; import { createUseActionMessage } from "@module/chat-message/helpers.ts"; -import { createSheetTags, eventToRollMode, eventToRollParams, maintainFocusInRender } from "@module/sheet/helpers.ts"; +import { + coinsToSheetData, + createSheetTags, + eventToRollMode, + eventToRollParams, + maintainFocusInRender, +} from "@module/sheet/helpers.ts"; import { DamageRoll } from "@system/damage/roll.ts"; import type { StatisticRollParameters } from "@system/statistic/statistic.ts"; import { @@ -48,13 +53,7 @@ import MiniSearch from "minisearch"; import * as R from "remeda"; import Sortable from "sortablejs"; import { ActorSizePF2e } from "../data/size.ts"; -import type { - ActorSheetDataPF2e, - ActorSheetRenderOptionsPF2e, - CoinageSummary, - InventoryItem, - SheetInventory, -} from "./data-types.ts"; +import type { ActorSheetDataPF2e, ActorSheetRenderOptionsPF2e, InventoryItem, SheetInventory } from "./data-types.ts"; import { createBulkPerLabel, onClickCreateSpell } from "./helpers.ts"; import { ItemSummaryRenderer } from "./item-summary-renderer.ts"; import { AddCoinsPopup } from "./popups/add-coins-popup.ts"; @@ -136,8 +135,8 @@ abstract class ActorSheetPF2e extends ActorSheet extends ActorSheet ({ - ...accumulated, - [d]: { value: coins[d], label: CONFIG.PF2E.currencies[d] }, - }), - {} as CoinageSummary, - ); - } - protected getStrikeFromDOM(button: HTMLElement, readyOnly = false): StrikeData | null { const actionIndex = Number(htmlClosest(button, "[data-action-index]")?.dataset.actionIndex ?? "NaN"); const rootAction = this.actor.system.actions?.at(actionIndex) ?? null; diff --git a/src/module/apps/compendium-browser/browser.ts b/src/module/apps/compendium-browser/browser.ts index ced451d2a60..90555b55e89 100644 --- a/src/module/apps/compendium-browser/browser.ts +++ b/src/module/apps/compendium-browser/browser.ts @@ -2,7 +2,7 @@ import { AbilityTrait, ActionCategory } from "@item/ability/index.ts"; import { ActionType, ItemType } from "@item/base/data/index.ts"; import { PHYSICAL_ITEM_TYPES } from "@item/physical/values.ts"; import { BaseSpellcastingEntry } from "@item/spellcasting-entry/index.ts"; -import { SvelteApplicationMixin } from "@module/sheet/mixin.svelte.ts"; +import { BaseSvelteState, SvelteApplicationMixin, SvelteApplicationRenderContext } from "@module/sheet/mixin.svelte.ts"; import { ErrorPF2e, setHasElement } from "@util"; import * as R from "remeda"; import { untrack } from "svelte"; @@ -124,9 +124,12 @@ class CompendiumBrowser extends SvelteApplicationMixin(foundry.applications.api. return controls; } - protected override async _prepareContext(_options: ApplicationRenderOptions): Promise { + protected override async _prepareContext(): Promise { + const base = await super._prepareContext(); return { + ...base, state: { + ...base.state, activeTabName: "", resultList: document.createElement("ul"), // This is required to make the value bindable }, @@ -343,11 +346,11 @@ class CompendiumBrowser extends SvelteApplicationMixin(foundry.applications.api. } } -interface CompendiumBrowserContext { +interface CompendiumBrowserContext extends SvelteApplicationRenderContext { state: CompendiumBrowserState; } -interface CompendiumBrowserState { +interface CompendiumBrowserState extends BaseSvelteState { /** Changing this will trigger a tab rerender. An empty string will show the landing page */ activeTabName: ContentTabName | ""; /** The result list HTML element */ diff --git a/src/module/sheet/components/coinage.svelte b/src/module/sheet/components/coinage.svelte new file mode 100644 index 00000000000..fa1b1544c95 --- /dev/null +++ b/src/module/sheet/components/coinage.svelte @@ -0,0 +1,231 @@ + + +
+
+
  • {game.i18n.localize("PF2E.StackGroupCoins")}
  • + {#each Object.entries(coinage.coins) as [denomination, value]} +
  • +
    + {value.value} +
  • + {/each} + {#if editable} +
  • + +
  • +
  • + +
  • + {#if canDistribute} +
  • + +
  • + {/if} + {/if} +
    +
    +

    + + {game.i18n.localize("PF2E.TotalCoinage")} + {coinage.totalCoins} {game.i18n.localize("PF2E.CurrencyAbbreviations.gp")} +

    +

    + + {game.i18n.localize("PF2E.TotalWealth")} + {coinage.totalWealth} {game.i18n.localize("PF2E.CurrencyAbbreviations.gp")} +

    +
    +
    + + diff --git a/src/module/sheet/components/inventory.svelte b/src/module/sheet/components/inventory.svelte new file mode 100644 index 00000000000..fc19ed5373e --- /dev/null +++ b/src/module/sheet/components/inventory.svelte @@ -0,0 +1,26 @@ + diff --git a/src/module/sheet/helpers.ts b/src/module/sheet/helpers.ts index 0df9728362d..b7311e6933d 100644 --- a/src/module/sheet/helpers.ts +++ b/src/module/sheet/helpers.ts @@ -1,5 +1,8 @@ import { ActorPF2e } from "@actor"; +import { CoinageSummary } from "@actor/sheet/data-types.ts"; import { ItemPF2e, ItemProxyPF2e } from "@item"; +import { Coins } from "@item/physical/data.ts"; +import { DENOMINATIONS } from "@item/physical/values.ts"; import { htmlClosest, htmlQuery, sortLabeledRecord } from "@util"; import * as R from "remeda"; @@ -53,6 +56,16 @@ function createTagifyTraits(traits: Iterable, { sourceTraits, record }: .sort((t1, t2) => t1.value.localeCompare(t2.value)); } +function coinsToSheetData(coins: Coins): CoinageSummary { + return DENOMINATIONS.reduce( + (accumulated, d) => ({ + ...accumulated, + [d]: { value: coins[d], label: CONFIG.PF2E.currencies[d] }, + }), + {} as CoinageSummary, + ); +} + /** * Get a CSS class for an adjusted value * @param value A value from prepared/derived data @@ -232,6 +245,7 @@ interface TagifyEntry { } export { + coinsToSheetData, createSheetOptions, createSheetTags, createTagifyTraits, diff --git a/src/module/sheet/mixin.svelte.ts b/src/module/sheet/mixin.svelte.ts index 330c4f7afcb..4208c9eab37 100644 --- a/src/module/sheet/mixin.svelte.ts +++ b/src/module/sheet/mixin.svelte.ts @@ -6,9 +6,15 @@ import type { } from "types/foundry/client-esm/applications/_types.d.ts"; import type ApplicationV2 from "types/foundry/client-esm/applications/api/application.d.ts"; +interface BaseSvelteState { + tabGroups: Record; + user: { isGM: boolean }; + editable: boolean | null; +} + interface SvelteApplicationRenderContext { /** State data tracked by the root component: objects herein must be plain object. */ - state: object; + state: BaseSvelteState; /** This application instance */ foundryApp: SvelteApplication; } @@ -31,6 +37,15 @@ function SvelteApplicationMixin< /** The mounted root component, saved to be unmounted on application close */ #mount: object = {}; + override changeTab(tab: string, group: string, { force = false } = {}) { + if (!tab || !group) throw new Error("You must pass both the tab and tab group identifier"); + if (this.tabGroups[group] === tab && !force) return; // No change necessary + + // Update the tab group and trigger a re-render + this.tabGroups[group] = tab; + this.render(false); + } + protected override async _renderHTML( context: SvelteApplicationRenderContext, ): Promise { @@ -44,7 +59,14 @@ function SvelteApplicationMixin< ): void { Object.assign(this.$state, result.state); if (options.isFirstRender) { - this.#mount = svelte.mount(this.root, { target: content, props: { ...result, state: this.$state } }); + this.#mount = svelte.mount(this.root, { + target: content, + context: new Map([ + ["foundryApp", this], + ["state", this.$state], + ]), + props: { ...result, state: this.$state }, + }); } } @@ -52,6 +74,17 @@ function SvelteApplicationMixin< super._onClose(options); svelte.unmount(this.#mount); } + + protected override async _prepareContext(): Promise { + return { + foundryApp: this, + state: { + tabGroups: this.tabGroups, + user: { isGM: game.user.isGM }, + editable: this instanceof DocumentSheet ? this.isEditable : null, + }, + }; + } } return SvelteApplication; @@ -59,4 +92,4 @@ function SvelteApplicationMixin< type SvelteApplication = InstanceType>; -export { SvelteApplicationMixin, type SvelteApplicationRenderContext }; +export { SvelteApplicationMixin, type BaseSvelteState, type SvelteApplicationRenderContext }; diff --git a/src/scripts/register-sheets.ts b/src/scripts/register-sheets.ts index 1ce07b8fc77..867364c6548 100644 --- a/src/scripts/register-sheets.ts +++ b/src/scripts/register-sheets.ts @@ -4,6 +4,7 @@ import { FamiliarSheetPF2e } from "@actor/familiar/sheet.ts"; import { HazardSheetPF2e } from "@actor/hazard/sheet.ts"; import { LootSheetPF2e } from "@actor/loot/sheet.ts"; import { NPCSheetPF2e, SimpleNPCSheet } from "@actor/npc/sheet.ts"; +import { PartySheetV2 } from "@actor/party/sheet-new.ts"; import { PartySheetPF2e } from "@actor/party/sheet.ts"; import { VehicleSheetPF2e } from "@actor/vehicle/sheet.ts"; import { AbilitySheetPF2e } from "@item/ability/sheet.ts"; @@ -105,6 +106,12 @@ export function registerSheets(): void { makeDefault: true, }); + // Party + Actors.registerSheet("pf2e", PartySheetV2, { + types: ["party"], + label: game.i18n.format(sheetLabel, { type: localizeType("party") }), + }); + // Army Actors.registerSheet("pf2e", ArmySheetPF2e, { types: ["army"], diff --git a/types/foundry/client-esm/applications/_types.d.ts b/types/foundry/client-esm/applications/_types.d.ts index 5c6c60af753..ec4947c2fa0 100644 --- a/types/foundry/client-esm/applications/_types.d.ts +++ b/types/foundry/client-esm/applications/_types.d.ts @@ -14,7 +14,7 @@ export interface ApplicationConfiguration { /** Click actions supported by the Application and their event handler functions */ actions: Record; /** Configuration used if the application top-level element is a form */ - form?: ApplicationFormConfiguration; + form?: DeepPartial; /** Default positioning data for the application */ position: Partial; } diff --git a/types/foundry/client/apps/app.d.ts b/types/foundry/client/apps/app.d.ts index 55fc7003856..6ea64a16ca2 100644 --- a/types/foundry/client/apps/app.d.ts +++ b/types/foundry/client/apps/app.d.ts @@ -393,8 +393,8 @@ declare global { } interface ApplicationPosition { - width?: Maybe; - height?: Maybe; + width?: number | string; + height?: number | string; left?: Maybe; top?: Maybe; scale?: Maybe;