Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/module/actor/loot/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { PhysicalItemPF2e } from "@item";
import { TextEditorPF2e } from "@system/text-editor.ts";
import { htmlClosest, htmlQuery } from "@util";
import { ActorSheetPF2e } from "../sheet/base.ts";
import { DistributeCoinsPopup } from "../sheet/popups/distribute-coins-popup.ts";
import { DistributeCoinsDialog } from "../sheet/popups/distribute-coins-dialog.ts";
import { LootNPCsPopup } from "../sheet/popups/loot-npcs-popup.ts";
import type { LootSystemSchema } from "./data.ts";

Expand Down Expand Up @@ -59,7 +59,7 @@ export class LootSheetPF2e<TActor extends LootPF2e> extends ActorSheetPF2e<TActo
htmlQuery(html, "[data-sidebar-buttons]")?.addEventListener("click", (event) => {
const button = htmlClosest(event.target, "button[data-action]");
if (button?.dataset.action === "split-coins") {
new DistributeCoinsPopup(this.actor).render(true);
new DistributeCoinsDialog({ actor: this.actor }).render(true);
} else if (button?.dataset.action === "loot-npcs") {
if (canvas.tokens.controlled.some((token) => token.actor?.id !== this.actor.id)) {
new LootNPCsPopup(this.actor).render(true);
Expand Down
4 changes: 2 additions & 2 deletions src/module/actor/party/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isReallyPC } from "@actor/helpers.ts";
import { ActorSheetPF2e } from "@actor/sheet/base.ts";
import type { ActorSheetDataPF2e, ActorSheetRenderOptionsPF2e } from "@actor/sheet/data-types.ts";
import { condenseSenses } from "@actor/sheet/helpers.ts";
import { DistributeCoinsPopup } from "@actor/sheet/popups/distribute-coins-popup.ts";
import { DistributeCoinsDialog } from "@actor/sheet/popups/distribute-coins-dialog.ts";
import type { ActorSheetOptions } from "@client/appv1/sheets/actor-sheet.d.mts";
import type { DropCanvasData } from "@client/helpers/hooks.d.mts";
import type { ActorUUID } from "@common/documents/_module.d.mts";
Expand Down Expand Up @@ -408,7 +408,7 @@ class PartySheetPF2e extends ActorSheetPF2e<PartyPF2e> {
}

htmlQuery(html, "button[data-action=distribute-coins]")?.addEventListener("click", () => {
new DistributeCoinsPopup(this.actor, { recipients: this.actor.members }).render(true);
new DistributeCoinsDialog({ actor: this.actor, recipients: this.actor.members }).render(true);
});

htmlQuery(html, "[data-action=clear-exploration]")?.addEventListener("click", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,86 +1,101 @@
import type { ActorPF2e, CharacterPF2e } from "@actor";
import { Coins } from "@item/physical/helpers.ts";
import { ChatMessagePF2e } from "@module/chat-message/document.ts";
import appv1 = foundry.appv1;

interface PopupData extends appv1.api.FormApplicationData<ActorPF2e> {
selection?: string[];
actorInfo?: {
id: string;
name: string;
checked: boolean;
}[];
}
/** Allows the distribution and split of coins to multiple players */
export class DistributeCoinsDialog extends fa.api.HandlebarsApplicationMixin(fa.api.ApplicationV2) {
constructor(
options: Partial<DistributeCoinsConfiguration> & Required<Pick<DistributeCoinsConfiguration, "actor">>,
) {
super(options);
this.actor = options.actor;
}

interface PopupFormData extends FormData {
actorIds: string[];
breakCoins: boolean;
}
static override DEFAULT_OPTIONS: DeepPartial<DistributeCoinsConfiguration> = {
id: "distribute-coins",
tag: "form",
window: {
icon: "fa-solid fa-coins",
title: "Distribute Coins",
contentClasses: ["standard-form"],
},
position: {
width: 400,
},
form: {
closeOnSubmit: true,
handler: DistributeCoinsDialog.#onSubmit,
},
};

/**
* @category Other
*/
export class DistributeCoinsPopup extends appv1.api.FormApplication<ActorPF2e, DistributeCoinsOptions> {
constructor(actor: ActorPF2e, options: Partial<DistributeCoinsOptions> = {}) {
super(actor, options);
}
static override PARTS = {
base: { template: "systems/pf2e/templates/actors/distribute-coins.hbs", root: true },
};

static override get defaultOptions(): appv1.api.FormApplicationOptions {
const options = super.defaultOptions;
options.id = "distribute-coins";
options.classes = [];
options.title = "Distribute Coins";
options.template = "systems/pf2e/templates/actors/distribute-coins.hbs";
options.width = "auto";
return options;
}
actor: ActorPF2e;
declare options: DistributeCoinsConfiguration;

override async getData(options?: Partial<DistributeCoinsOptions>): Promise<PopupData> {
const sheetData: PopupData = await super.getData(options);
const playerActors = (options?.recipients ?? game.actors.contents).filter(
override async _prepareContext(options: fa.ApplicationRenderOptions): Promise<DistributeCoinsContext> {
const data = await super._prepareContext(options);
const playerActors = (this.options.recipients ?? game.actors.contents).filter(
(a) =>
a.hasPlayerOwner &&
a.isOfType("character") &&
!a.isToken &&
!a.system.traits.value.some((t) => ["minion", "eidolon"].includes(t)),
);
sheetData.actorInfo = playerActors.map((a) => ({
id: a.id,
name: a.name,
checked: game.users.players.some((u) => u.active && u.character?.id === a.id),
}));
if (playerActors.length === 0) {
ui.notifications.warn("PF2E.loot.NoPlayerCharacters", { localize: true });
await this.close();
}

return sheetData;
return {
...data,
rootId: this.id,
actorInfo: playerActors.map((a) => ({
id: a.id,
name: a.name,
checked: game.users.players.some((u) => u.active && u.character?.id === a.id),
})),
};
}

override async _updateObject(_event: Event, formData: Record<string, unknown> & PopupFormData): Promise<void> {
const thisActor = this.object;
const selectedActors: CharacterPF2e[] = formData.actorIds.flatMap((actorId) => {
static async #onSubmit(
this: DistributeCoinsDialog,
_event: Event,
form: HTMLFormElement,
formData: fa.ux.FormDataExtended,
) {
// Get actor ids, we need to get it directly since the DOM may not give back an array if only one
const actor = this.actor;
const actorIds: string[] = Array.from(form.elements).flatMap((element) =>
element instanceof HTMLInputElement && element.name === "actorIds" && element.checked ? element.value : [],
);

const selectedActors: CharacterPF2e[] = actorIds.flatMap((actorId) => {
const maybeActor = game.actors.get(actorId);
return maybeActor?.isOfType("character") ? maybeActor : [];
});

const playerCount = selectedActors.length;
if (playerCount === 0) {
return;
}
if (playerCount === 0) return;

const coinShare = new Coins();
if (formData.breakCoins) {
const thisActorCopperValue = thisActor.inventory.coins.copperValue;
if (formData.object.breakCoins) {
const thisActorCopperValue = actor.inventory.coins.copperValue;
const copperToDistribute = Math.trunc(thisActorCopperValue / playerCount);
// return if there is nothing to distribute
if (copperToDistribute === 0) {
ui.notifications.warn("Nothing to distribute");
return;
}
thisActor.inventory.removeCoins({ cp: copperToDistribute * playerCount });
actor.inventory.removeCoins({ cp: copperToDistribute * playerCount });
coinShare.cp = copperToDistribute % 10;
coinShare.sp = Math.trunc(copperToDistribute / 10) % 10;
coinShare.gp = Math.trunc(copperToDistribute / 100) % 10;
coinShare.pp = Math.trunc(copperToDistribute / 1000);
} else {
const thisActorCurrency = thisActor.inventory.coins;
const thisActorCurrency = actor.inventory.coins;
coinShare.pp = Math.trunc(thisActorCurrency.pp / playerCount);
coinShare.cp = Math.trunc(thisActorCurrency.cp / playerCount);
coinShare.gp = Math.trunc(thisActorCurrency.gp / playerCount);
Expand All @@ -92,15 +107,15 @@ export class DistributeCoinsPopup extends appv1.api.FormApplication<ActorPF2e, D
}

const coinsToRemove = coinShare.scale(playerCount);
thisActor.inventory.removeCoins(coinsToRemove, { byValue: false });
actor.inventory.removeCoins(coinsToRemove, { byValue: false });
}
let message = `Distributed `;
if (coinShare.pp !== 0) message += `${coinShare.pp} pp `;
if (coinShare.gp !== 0) message += `${coinShare.gp} gp `;
if (coinShare.sp !== 0) message += `${coinShare.sp} sp `;
if (coinShare.cp !== 0) message += `${coinShare.cp} cp `;
const each = playerCount > 1 ? "each " : "";
message += `${each}from ${thisActor.name} to `;
message += `${each}from ${actor.name} to `;
for (const actor of selectedActors) {
await actor.inventory.addCoins(coinShare);
const index = selectedActors.indexOf(actor);
Expand All @@ -114,21 +129,19 @@ export class DistributeCoinsPopup extends appv1.api.FormApplication<ActorPF2e, D
content: message,
});
}

/** Prevent Foundry from converting the actor IDs to boolean values */
protected override async _onSubmit(
event: Event,
options: appv1.api.OnSubmitFormOptions = {},
): Promise<Record<string, unknown> | false> {
const actorIds: string[] = Array.from(this.form.elements).flatMap((element) =>
element instanceof HTMLInputElement && element.name === "actorIds" && element.checked ? element.value : [],
);
options.updateData = fu.mergeObject(options.updateData ?? {}, { actorIds: actorIds });
return super._onSubmit(event, options);
}
}

interface DistributeCoinsOptions extends appv1.api.FormApplicationOptions {
interface DistributeCoinsConfiguration extends fa.api.DialogV2Configuration {
actor: ActorPF2e;
/** An optional initial list of recipients to receive coins */
recipients?: ActorPF2e[];
}

interface DistributeCoinsContext extends fa.ApplicationRenderContext {
rootId: string;
actorInfo: {
id: string;
name: string;
checked: boolean;
}[];
}
2 changes: 2 additions & 0 deletions static/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -6685,6 +6685,7 @@
}
},
"loot": {
"AllowConversion": "Allow coin type conversion",
"BuySubtitle": "Buy item",
"DepositMessage": "{depositor} deposits {quantity} × {item} in {container}.",
"DepositSubtitle": "Deposit item",
Expand Down Expand Up @@ -6721,6 +6722,7 @@
"SheetType": "Sheet Type",
"SplitCoinsLabel": "Distribute Coins",
"SplitCoinsPopupHeader": "Choose actors to receive a share",
"NoPlayerCharacters": "There are no player characters to distribute to",
"Stack": "Create new stack?",
"TakeMessage": "{taker} takes {quantity} × {item} from {container}.",
"TakeSubtitle": "Take item",
Expand Down
45 changes: 20 additions & 25 deletions static/templates/actors/distribute-coins.hbs
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
<form class="loot-actor-popup" autocomplete="off" onsubmit="event.preventDefault();">
<div class="checkboxes">
<h1>{{localize "PF2E.loot.SplitCoinsPopupHeader"}}</h1>
{{#each actorInfo as |info|}}
<p>
<label>
<input type="checkbox" name="actorIds" value="{{info.id}}" {{checked info.checked}} />
<span>{{info.name}}</span>
</label>
</p>
{{/each}}
<hr />
<p>
<label class="break-coins" for="breakCoins">
<input type="checkbox" name="breakCoins" />
<span>Allow coin type conversion</span>
</label>
</p>
<fieldset>
<legend>{{localize "PF2E.loot.SplitCoinsPopupHeader"}}</legend>
{{#each actorInfo as |info|}}
<label>
<input type="checkbox" name="actorIds" value="{{info.id}}" {{checked info.checked}} />
<span>{{info.name}}</span>
</label>
{{/each}}
</fieldset>
<div class="form-group slim">
<label for="{{rootId}}-breakCoins">{{localize "PF2E.loot.AllowConversion"}}</label>
<div class="form-fields">
<input type="checkbox" name="breakCoins" id="{{rootId}}-breakCoins"/>
</div>
<p>
<button class="confirm-button" type="submit">
<i class="fa-regular fa-floppy-disk"></i>
{{localize "PF2E.loot.SplitCoinsLabel"}}
</button>
</p>
</form>
</div>
<div class="form-footer">
<button type="submit">
<i class="fa-regular fa-floppy-disk"></i>
{{localize "PF2E.loot.SplitCoinsLabel"}}
</button>
</div>