Skip to content

Commit

Permalink
Merge pull request #1398 from StarWarsFoundryVTT/xp_spending
Browse files Browse the repository at this point in the history
Xp spending
  • Loading branch information
wrycu authored Apr 6, 2024
2 parents 09eb95b + 4e5ea13 commit 6cdcf81
Show file tree
Hide file tree
Showing 21 changed files with 774 additions and 33 deletions.
34 changes: 33 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"SWFFG.TabBiography": "Biography",
"SWFFG.TabDescription": "Description",
"SWFFG.TabModifiers": "Modifiers",
"SWFFG.TabSpecializations": "Specializations",
"SWFFG.TabCareer": "Career Data",
"SWFFG.CharacteristicBrawn": "Brawn",
"SWFFG.CharacteristicBrawnAbbreviation": "Br",
"SWFFG.CharacteristicAgility": "Agility",
Expand Down Expand Up @@ -643,5 +645,35 @@
"SWFFG.Settings.showAdversaryCount.Name": "Show Adversary Count",
"SWFFG.Settings.showAdversaryCount.Hint": "Shows the ranks of the Adversary talent on tokens",
"SWFFG.Settings.AdversaryItemName.Name": "Adversary Item Name",
"SWFFG.Settings.AdversaryItemName.Hint": "The name of the talent to consider 'adversary' for the purposes of showing ranks on tokens (for translations)"
"SWFFG.Settings.AdversaryItemName.Hint": "The name of the talent to consider 'adversary' for the purposes of showing ranks on tokens (for translations)",
"SWFFG.Actors.Sheets.Purchase.LogTitle": "XP Spend Log",
"SWFFG.Actors.Sheets.Purchase.DialogTitle": "Purchase {itemType}",
"SWFFG.Actors.Sheets.Purchase.ConfirmPurchase": "Purchase",
"SWFFG.Actors.Sheets.Purchase.CancelPurchase": "Cancel",
"SWFFG.Actors.Sheets.Purchase.NotEnoughXP": "You do not have enough XP to buy a rank in this skill",
"SWFFG.Actors.Sheets.Purchase.SkillRank.ContextMenuText": "Purchase Skill Rank",
"SWFFG.Actors.Sheets.Purchase.SkillRank.ConfirmTitle": "Purchase Skill Rank",
"SWFFG.Actors.Sheets.Purchase.SkillRank.Text": "Spend {cost} XP to purchase a rank in {skill}?",
"SWFFG.Actors.Sheets.Purchase.Talent.ContextMenuText": "Purchase Talent",
"SWFFG.Actors.Sheets.Purchase.Talent.ConfirmTitle": "Purchase Talent",
"SWFFG.Actors.Sheets.Purchase.Talent.ConfirmText": "Spend {cost} XP to purchase {talent}?",
"SWFFG.Actors.Sheets.Purchase.FP.ContextMenuText": "Purchase Upgrade",
"SWFFG.Actors.Sheets.Purchase.FP.ConfirmTitle": "Purchase Upgrade",
"SWFFG.Actors.Sheets.Purchase.FP.ConfirmText": "Spend {cost} XP to purchase {upgrade}?",
"SWFFG.Actors.Sheets.Purchase.SA.ContextMenuText": "Purchase Upgrade",
"SWFFG.Actors.Sheets.Purchase.SA.ConfirmTitle": "Purchase Upgrade",
"SWFFG.Actors.Sheets.Purchase.SA.ConfirmText": "Spend {cost} XP to purchase {upgrade}?",
"SWFFG.Actors.Sheets.Purchase.SA.NotSet": "Please set career signature abilities in the career data tab on your career",
"SWFFG.Actors.Sheets.Purchase.Talent.New.ContextMenuText": "Purchase Specialization",
"SWFFG.Actors.Sheets.Purchase.Career.Specialization.DropHint": "(drop specializations here)",
"SWFFG.Actors.Sheets.Purchase.Career.Specializations.Header": "Specializations in this Career",
"SWFFG.Actors.Sheets.Purchase.Career.SignatureAbility.DropHint": "(drop signature abilities here)",
"SWFFG.Actors.Sheets.Purchase.Career.SignatureAbility.Header": "Signature Abilities in this Career",
"SWFFG.Actors.Sheets.Purchase.Dropdown": "Select a {itemType}...",
"SWFFG.Settings.Purchase.Specialization.Name": "Specialization Compendiums",
"SWFFG.Settings.Purchase.Specialization.Hint": "Comma seperated compendiums to search for buying Specializations (no spaces)",
"SWFFG.Settings.Purchase.ForcePower.Name": "Force Power Compendiums",
"SWFFG.Settings.Purchase.ForcePower.Hint": "Comma seperated compendiums to search for buying Force Powers (no spaces)",
"SWFFG.Settings.Purchase.SignatureAbility.Name": "Signature Abilities Compendiums",
"SWFFG.Settings.Purchase.SignatureAbility.Hint": "Comma seperated compendiums to search for buying Signature Abilities (no spaces)"
}
238 changes: 236 additions & 2 deletions modules/actors/actor-sheet-ffg.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import DiceHelpers from "../helpers/dice-helpers.js";
import ActorOptions from "./actor-ffg-options.js";
import ImportHelpers from "../importer/import-helpers.js";
import ModifierHelpers from "../helpers/modifiers.js";
import ActorHelpers from "../helpers/actor-helpers.js";
import ActorHelpers, {xpLogSpend} from "../helpers/actor-helpers.js";
import ItemHelpers from "../helpers/item-helpers.js";
import EmbeddedItemHelpers from "../helpers/embeddeditem-helpers.js";
import {change_role, deregister_crew, build_crew_roll} from "../helpers/crew.js";
Expand Down Expand Up @@ -147,6 +147,9 @@ export class ActorSheetFFG extends ActorSheet {
if (this.actor.flags?.config?.enableObligation === false && this.actor.flags?.config?.enableDuty === false && this.actor.flags?.config?.enableMorality === false && this.actor.flags?.config?.enableConflict === false) {
data.hideObligationDutyMoralityConflictTab = true;
}
if (this.actor.flags?.starwarsffg?.xpLog) {
data.xpLog = this.actor.flags.starwarsffg.xpLog.join("<br>");
}

return data;
}
Expand Down Expand Up @@ -264,6 +267,13 @@ export class ActorSheetFFG extends ActorSheet {
this._onRemoveSkill(li);
},
},
{
name: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.SkillRank.ContextMenuText"),
icon: '<i class="fas fa-dollar"></i>',
callback: (li) => {
this._buySkillRank(li);
},
},
]);

new ContextMenu(html, "div.skillsHeader", [
Expand All @@ -276,6 +286,10 @@ export class ActorSheetFFG extends ActorSheet {
},
]);

html.find("td.ffg-purchase").click(async (ev) => {
await this._buyCore(ev)
});

// Send Item Details to chat.

const sendToChatContextItem = {
Expand Down Expand Up @@ -959,7 +973,6 @@ export class ActorSheetFFG extends ActorSheet {
html.find(".force-conflict .enable-dice-pool").on("click", async (event) => {
event.preventDefault();
await this.actor.setFlag('starwarsffg', 'config', {enableForcePool: true});
console.log({this: this, event: event})
});

html.find(".force-conflict .remove-force-powers").on("click", async (event) => {
Expand Down Expand Up @@ -1194,6 +1207,57 @@ export class ActorSheetFFG extends ActorSheet {
).render(true);
}

/**
* Handle the right click -> buy skill rank event
* @param a - Event object
* @returns {Promise<void>}
* @private
*/
async _buySkillRank(a) {
const skill = $(a).data("ability");
const curRank = this.object.system.skills[skill].rank;
const availableXP = this.object.system.experience.available;
const careerSkill = this.object.system.skills[skill].careerskill;
const cost = careerSkill ? (curRank + 1) * 5 : (curRank + 1) * 5 + 5;

if (cost > availableXP) {
ui.notifications.warn(game.i18n.localize("SWFFG.Actors.Sheets.Purchase.NotEnoughXP"));
return;
}
const dialog = new Dialog(
{
title: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.SkillRank.ConfirmTitle"),
content: game.i18n.format("SWFFG.Actors.Sheets.Purchase.SkillRank.Text", {cost: cost, skill: skill, old: curRank, new: curRank + 1}),
buttons: {
done: {
icon: '<i class="fas fa-dollar"></i>',
label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.ConfirmPurchase"),
callback: async (that) => {
// update the form because the fields are read when an update is performed
this.object.sheet.element.find(`[name="data.skills.${skill}.rank"]`).val(curRank + 1);
await this.object.sheet.submit({preventClose: true});
await this.object.update({
system: {
experience: {
available: availableXP - cost,
}
}
});
await xpLogSpend(game.actors.get(this.object.id), `skill rank ${skill} ${curRank} --> ${curRank + 1}`, cost);
},
},
cancel: {
icon: '<i class="fas fa-cancel"></i>',
label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.CancelPurchase"),
},
},
},
{
classes: ["dialog", "starwarsffg"],
}
).render(true);
}

/**
* Remove skill from skill list
* @param {object} a - Event object
Expand Down Expand Up @@ -1525,4 +1589,174 @@ export class ActorSheetFFG extends ActorSheet {

return cols;
}

async _buyCore(event) {
const action = $(event.target).data("buy-action");
const template = "systems/starwarsffg/templates/dialogs/ffg-confirm-purchase.html";
let content;
const availableXP = this.object.system.experience.available;
let itemType;
if (action === "specialization") {
const inCareer = this.object.items.find(i => i.type === "career").system.specializations;
const inCareerNames = Object.values(inCareer).map(i => i.name);
const sources = game.settings.get("starwarsffg", "specializationCompendiums").split(",");
let outCareer = [];
for (const source of sources) {
const pack = game.packs.get(source);
if (!pack) {
continue;
}
const items = await pack.getDocuments();
for (const item of items) {
if (!inCareerNames.includes(item.name)) {
outCareer.push({
name: item.name,
id: item.id,
source: item.uuid,
});
}
}
}
outCareer = sortDataBy(outCareer, "name");
const baseCost = (this.object.items.filter(i => i.type === "specialization").length + 1) * 10;
const increasedCost = baseCost + 10;
if (baseCost > availableXP) {
ui.notifications.warn(game.i18n.localize("SWFFG.Actors.Sheets.Purchase.NotEnoughXP"));
return;
} else if (increasedCost > availableXP) {
outCareer = [];
}
itemType = game.i18n.localize("TYPES.Item.specialization");
content = await renderTemplate(template, { inCareer, outCareer, baseCost, increasedCost, itemType: itemType });
} else if (action === "signatureability") {
const sources = game.settings.get("starwarsffg", "signatureAbilityCompendiums").split(",");
const rawSelectableItems = this.object.items.find(i => i.type === "career").system.signatureabilities;
const sigAbilityNames = Object.values(rawSelectableItems).map(i => i.name);
let selectableItems = [];
// pull items out of the world
for (const itemId of Object.keys(rawSelectableItems)) {
const item = rawSelectableItems[itemId];
let retrievedItem = game.items.get(item.id);
if (retrievedItem) {
selectableItems.push({
name: retrievedItem.name,
id: retrievedItem.id,
source: retrievedItem.uuid,
cost: parseInt(retrievedItem.system.base_cost),
});
}
}
// pull items out of compendiums
for (const source of sources) {
const pack = game.packs.get(source);
if (!pack) {
continue;
}
const items = await pack.getDocuments();
for (const item of items) {
if (sigAbilityNames.includes(item.name)) {
selectableItems.push({
name: item.name,
id: item.id,
source: item.uuid,
cost: parseInt(item.system.base_cost),
});
}
}
}
if (selectableItems.length === 0) {
ui.notifications.warn(game.i18n.localize("SWFFG.Actors.Sheets.Purchase.SA.NotSet"));
return;
}
selectableItems = sortDataBy(selectableItems, "name");
itemType = game.i18n.localize("TYPES.Item.signatureability");
content = await renderTemplate(template, { selectableItems, itemType: itemType });
} else if (action === "forcepower") {
const sources = game.settings.get("starwarsffg", "forcePowerCompendiums").split(",");
let selectableItems = [];
const worldItems = game.items.filter(i => i.type === "forcepower");
for (const worldItem of worldItems) {
selectableItems.push({
name: worldItem.name,
id: worldItem.id,
source: worldItem.uuid,
cost: worldItem.system.base_cost,
});
}
for (const source of sources) {
const pack = game.packs.get(source);
if (!pack) {
continue;
}
const items = await pack.getDocuments();
for (const item of items) {
selectableItems.push({
name: item.name,
id: item.id,
source: item.uuid,
cost: item.system.base_cost,
});
}
selectableItems = sortDataBy(selectableItems, "name");
itemType = game.i18n.localize("TYPES.Item.forcepower");
content = await renderTemplate(template, { selectableItems, itemType: itemType });
}
}

const dialog = new Dialog(
{
title: game.i18n.format("SWFFG.Actors.Sheets.Purchase.DialogTitle", {itemType: itemType}),
content: content,
buttons: {
done: {
icon: '<i class="fas fa-dollar"></i>',
label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.ConfirmPurchase"),
callback: async (that) => {
const cost = $("#ffgPurchase option:selected", that).data("cost");
const selected_id = $("#ffgPurchase option:selected", that).data("id");
const selected_source = $("#ffgPurchase option:selected", that).data("source");
let purchasedItem = game.items.get(selected_id);
if (!purchasedItem) {
purchasedItem = await fromUuid(selected_source);
}
await this.object.createEmbeddedDocuments("Item", [purchasedItem]);
await this.object.update({
system: {
experience: {
available: availableXP - cost,
},
},
});
await xpLogSpend(game.actors.get(this.object.id), `new ${action} ${purchasedItem.name}`, cost);
},
},
cancel: {
icon: '<i class="fas fa-cancel"></i>',
label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.CancelPurchase"),
},
},
},
{
classes: ["dialog", "starwarsffg"],
}
).render(true);
}
}

/**
* Sort an array of dicts by a key. Totally not AI generated. But it works :)
* @param data
* @param byKey
* @returns {*}
*/
function sortDataBy(data, byKey) {
return data.sort((a, b) => {
if (a[byKey] < b[byKey]) {
return -1;
}
if (a[byKey] > b[byKey]) {
return 1;
}
return 0;
});
}
5 changes: 4 additions & 1 deletion modules/groupmanager-ffg.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {xpLogEarn} from "./helpers/actor-helpers.js";

export class GroupManagerLayer extends CanvasLayer {
constructor() {
super();
Expand Down Expand Up @@ -342,11 +344,12 @@ export class GroupManager extends FormApplication {
one: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("SWFFG.GrantXP"),
callback: () => {
callback: async () => {
const container = document.getElementById(id);
const amount = container.querySelector('input[name="amount"]');
character.update({ ["data.experience.total"]: +character.system.experience.total + +amount.value });
character.update({ ["data.experience.available"]: +character.system.experience.available + +amount.value });
await xpLogEarn(character, amount.value);
ui.notifications.info(`Granted ${amount.value} XP to ${character.name}.`);
},
},
Expand Down
27 changes: 27 additions & 0 deletions modules/helpers/actor-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,30 @@ export default class ActorHelpers {
return this.object.update(formData);
}
}

/**
* Adds a SPEND log entry to the actor's XP log (accessed via the notebook under specializations)
* @param actor - ffgActor object
* @param action - action taken (e.g. "skill rank Astrogation 1 --> 2")
* @param cost - XP spent
* @returns {Promise<void>}
*/
export async function xpLogSpend(actor, action, cost) {
const xpLog = actor.getFlag("starwarsffg", "xpLog") || [];
const date = new Date().toISOString().slice(0, 10);
let newEntry = `<font color="red"><b>${date}</b>: spent <b>${cost}</b> XP for <b>${action}</b></font>`;
await actor.setFlag("starwarsffg", "xpLog", [newEntry, ...xpLog]);
}

/**
* Adds a GRANT log entry to the actor's XP log (accessed via the notebook under specializations)
* @param actor - ffgActor object
* @param grant - XP granted
* @returns {Promise<void>}
*/
export async function xpLogEarn(actor, grant) {
const xpLog = actor.getFlag("starwarsffg", "xpLog") || [];
const date = new Date().toISOString().slice(0, 10);
let newEntry = `<font color="green"><b>${date}</b>: GM granted <b>${grant}</b> XP</font>`;
await actor.setFlag("starwarsffg", "xpLog", [newEntry, ...xpLog]);
}
Loading

0 comments on commit 6cdcf81

Please sign in to comment.