diff --git a/lang/en.json b/lang/en.json index 621875d5..bba847b5 100644 --- a/lang/en.json +++ b/lang/en.json @@ -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", @@ -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)" } diff --git a/modules/actors/actor-sheet-ffg.js b/modules/actors/actor-sheet-ffg.js index d3980339..d6258d0f 100644 --- a/modules/actors/actor-sheet-ffg.js +++ b/modules/actors/actor-sheet-ffg.js @@ -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"; @@ -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("
"); + } return data; } @@ -264,6 +267,13 @@ export class ActorSheetFFG extends ActorSheet { this._onRemoveSkill(li); }, }, + { + name: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.SkillRank.ContextMenuText"), + icon: '', + callback: (li) => { + this._buySkillRank(li); + }, + }, ]); new ContextMenu(html, "div.skillsHeader", [ @@ -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 = { @@ -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) => { @@ -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} + * @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: '', + 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: '', + label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.CancelPurchase"), + }, + }, + }, + { + classes: ["dialog", "starwarsffg"], + } + ).render(true); + } + /** * Remove skill from skill list * @param {object} a - Event object @@ -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: '', + 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: '', + 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; + }); } diff --git a/modules/groupmanager-ffg.js b/modules/groupmanager-ffg.js index 45365b5c..2ef26844 100644 --- a/modules/groupmanager-ffg.js +++ b/modules/groupmanager-ffg.js @@ -1,3 +1,5 @@ +import {xpLogEarn} from "./helpers/actor-helpers.js"; + export class GroupManagerLayer extends CanvasLayer { constructor() { super(); @@ -342,11 +344,12 @@ export class GroupManager extends FormApplication { one: { icon: '', 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}.`); }, }, diff --git a/modules/helpers/actor-helpers.js b/modules/helpers/actor-helpers.js index 8dafb029..7fa7a251 100644 --- a/modules/helpers/actor-helpers.js +++ b/modules/helpers/actor-helpers.js @@ -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} + */ +export async function xpLogSpend(actor, action, cost) { + const xpLog = actor.getFlag("starwarsffg", "xpLog") || []; + const date = new Date().toISOString().slice(0, 10); + let newEntry = `${date}: spent ${cost} XP for ${action}`; + 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} + */ +export async function xpLogEarn(actor, grant) { + const xpLog = actor.getFlag("starwarsffg", "xpLog") || []; + const date = new Date().toISOString().slice(0, 10); + let newEntry = `${date}: GM granted ${grant} XP`; + await actor.setFlag("starwarsffg", "xpLog", [newEntry, ...xpLog]); +} diff --git a/modules/importer/data-importer.js b/modules/importer/data-importer.js index 8da1e4e9..bd76d9d8 100644 --- a/modules/importer/data-importer.js +++ b/modules/importer/data-importer.js @@ -149,6 +149,7 @@ export default class DataImporter extends FormApplication { const promises = []; let isSpecialization = false; let isVehicle = false; + let isCareer = false; let skillsFileName; try { @@ -189,14 +190,14 @@ export default class DataImporter extends FormApplication { promises.push(OggDude.Import.Armor(xmlDoc, zip)); promises.push(OggDude.Import.Talents(xmlDoc, zip)); promises.push(OggDude.Import.ForcePowers(xmlDoc, zip)); - promises.push(OggDude.Import.SignatureAbilities(xmlDoc, zip)); + //promises.push(OggDude.Import.SignatureAbilities(xmlDoc, zip)); promises.push(OggDude.Import.ItemAttachments(xmlDoc)); } else { if (file.file.includes("/Specializations/")) { isSpecialization = true; } if (file.file.includes("/Careers/")) { - promises.push(OggDude.Import.Career(zip)); + isCareer = true; } if (file.file.includes("/Species/")) { promises.push(OggDude.Import.Species(zip)); @@ -211,10 +212,27 @@ export default class DataImporter extends FormApplication { if (isSpecialization) { await OggDude.Import.Specializations(zip); } + if (isCareer) { + await OggDude.Import.Career(zip); + } if (isVehicle) { await OggDude.Import.Vehicles(zip); } + // import signature abilities + ImportHelpers.clearCache(); + const promisesPhase2 = []; + await this.asyncForEach(importFiles, async (file) => { + if (zip.files[file.file] && !zip.files[file.file].dir) { + const data = await zip.file(file.file).async("text"); + const xmlDoc = ImportHelpers.stringToXml(data); + + promisesPhase2.push(OggDude.Import.SignatureAbilities(xmlDoc, zip)); + + } + }); + await Promise.all(promisesPhase2); + if ($(".debug input:checked").length > 0) { saveDataToFile(this._importLog.join("\n"), "text/plain", "import-log.txt"); } diff --git a/modules/importer/import-helpers.js b/modules/importer/import-helpers.js index 338a0e9b..96623b3a 100644 --- a/modules/importer/import-helpers.js +++ b/modules/importer/import-helpers.js @@ -157,6 +157,10 @@ export default class ImportHelpers { } } + static clearCache() { + CONFIG.temporary = {}; + } + /** * Find an entity by the import key. * @param {string} type - Entity type to search for @@ -2216,7 +2220,8 @@ export default class ImportHelpers { } CONFIG.logger.debug(`New ${type} ${dataType} ${data.name} : ${JSON.stringify(compendiumItem)}`); const crt = await pack.importDocument(compendiumItem); - CONFIG.temporary[pack.collection][data.flags.starwarsffg.ffgimportid] = duplicate(crt); + CONFIG.temporary[pack.collection][data.flags.starwarsffg.ffgimportid] = deepClone(crt); + return crt; } else { CONFIG.logger.debug(`Found existing ${type} ${dataType} ${data.name} : ${JSON.stringify(entry)}`); let upd; @@ -2266,6 +2271,7 @@ export default class ImportHelpers { } } CONFIG.temporary[pack.collection][data.flags.starwarsffg.ffgimportid] = upd; + return upd; } } diff --git a/modules/importer/oggdude/importers/careers.js b/modules/importer/oggdude/importers/careers.js index 482ff3bc..ee8bce90 100644 --- a/modules/importer/oggdude/importers/careers.js +++ b/modules/importer/oggdude/importers/careers.js @@ -26,6 +26,8 @@ export default class Career { data.data = { attributes: {}, description: item.Description, + specializations: {}, + signatureabilities: {}, }; data.data.description += ImportHelpers.getSources(item.Sources ?? item.Source); @@ -63,6 +65,21 @@ export default class Career { }); } + // process specializations + if (item?.Specializations) { + for (const specializationKey of Object.values(item.Specializations.Key)) { + let specializationItem = await ImportHelpers.findCompendiumEntityByImportId("Item", specializationKey, "world.oggdudespecializations", "specialization"); + if (!specializationItem) { + continue; + } + data.data.specializations[specializationItem._id] = { + name: specializationItem.name, + source: specializationItem.uuid, + id: specializationItem._id, + } + } + } + let imgPath = await ImportHelpers.getImageFilename(zip, "Career", "", data.flags.starwarsffg.ffgimportid); if (imgPath) { data.img = await ImportHelpers.importImage(imgPath.name, zip, pack); diff --git a/modules/importer/oggdude/importers/forcepowers.js b/modules/importer/oggdude/importers/forcepowers.js index a3392fb1..d9af8765 100644 --- a/modules/importer/oggdude/importers/forcepowers.js +++ b/modules/importer/oggdude/importers/forcepowers.js @@ -36,8 +36,15 @@ export default class ForcePowers { attributes: {}, description: basepower.Description, upgrades: {}, + required_force_rating: item?.MinForceRating ? item.MinForceRating : 1, }; + try { + data.data.base_cost = item.AbilityRows.AbilityRow[0].Costs.Cost[0]; + } catch (err) { + data.data.base_cost = 0; + } + data.data.description += ImportHelpers.getSources(item?.Sources ?? item?.Source); if (item?.DieModifiers) { const dieModifiers = await ImportHelpers.processDieMod(item.DieModifiers); diff --git a/modules/importer/oggdude/importers/signature-abilities.js b/modules/importer/oggdude/importers/signature-abilities.js index 4d143c06..d16960cb 100644 --- a/modules/importer/oggdude/importers/signature-abilities.js +++ b/modules/importer/oggdude/importers/signature-abilities.js @@ -30,13 +30,21 @@ export default class SignatureAbilities { description: item.Description, attributes: {}, upgrades: {}, + base_cost: 0, }; data.data.description += ImportHelpers.getSources(item.Sources ?? item.Source); item.AbilityRows.AbilityRow.forEach((row, i) => { try { - // skip the first row because it is the large primary ability box - if (i > 0) { + if (i === 0) { + try { + data.data.base_cost = row.Costs.Cost[0]; + } catch (err) { + data.data.base_cost = 0; + } + } + else { + // skip the first row because it is the large primary ability box row.Abilities.Key.forEach((keyName, index) => { let rowAbility = {}; @@ -108,8 +116,29 @@ export default class SignatureAbilities { data.img = `icons/svg/aura.svg`; } - await ImportHelpers.addImportItemToCompendium("Item", data, pack); + const sigAbility = await ImportHelpers.addImportItemToCompendium("Item", data, pack); currentCount += 1; + // process careers + if (item?.Careers) { + for (const careerKey of Object.values(item.Careers)) { + let careerItem = await ImportHelpers.findCompendiumEntityByImportId("Item", careerKey, "world.oggdudecareers", "career"); + if (!careerItem) { + continue; + } + const updateData = { + system: { + signatureabilities: { + [sigAbility._id]: { + name: sigAbility.name, + source: sigAbility.uuid, + id: sigAbility._id, + }, + }, + }, + } + await careerItem.update(updateData); + } + } $(".signatureabilities .import-progress-bar") .width(`${Math.trunc((currentCount / totalCount) * 100)}%`) diff --git a/modules/items/item-sheet-ffg-v2.js b/modules/items/item-sheet-ffg-v2.js index 857d90f0..e133bbcc 100644 --- a/modules/items/item-sheet-ffg-v2.js +++ b/modules/items/item-sheet-ffg-v2.js @@ -14,9 +14,4 @@ export class ItemSheetFFGV2 extends ItemSheetFFG { const data = super.getData(); return data; } - - /** @override */ - activateListeners(html) { - super.activateListeners(html); - } } diff --git a/modules/items/item-sheet-ffg.js b/modules/items/item-sheet-ffg.js index 0db728ba..6029c1c3 100644 --- a/modules/items/item-sheet-ffg.js +++ b/modules/items/item-sheet-ffg.js @@ -6,6 +6,7 @@ import ImportHelpers from "../importer/import-helpers.js"; import DiceHelpers from "../helpers/dice-helpers.js"; import item from "../helpers/embeddeditem-helpers.js"; import EmbeddedItemHelpers from "../helpers/embeddeditem-helpers.js"; +import {xpLogSpend} from "../helpers/actor-helpers.js"; import ItemOptions from "./item-ffg-options.js"; /** @@ -245,6 +246,14 @@ export class ItemSheetFFG extends ItemSheet { case "career": this.position.width = 500; this.position.height = 600; + if (Object.keys(this.object.system.specializations).length === 0) { + // handlebars sucks + data.data.specializations = false; + } + if (Object.keys(this.object.system.signatureabilities).length === 0) { + // handlebars sucks + data.data.signatureabilities = false; + } break; case "signatureability": { this.position.width = 720; @@ -273,6 +282,33 @@ export class ItemSheetFFG extends ItemSheet { /** @override */ activateListeners(html) { super.activateListeners(html); + new ContextMenu(this.element, ".talent-upgrade.specialization-talent", [ + { + name: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.Talent.ContextMenuText"), + icon: '', + callback: (li) => { + this._buyTalent(li); + }, + }, + ]); + new ContextMenu(this.element, ".talent-upgrade.force-power", [ + { + name: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.FP.ContextMenuText"), + icon: '', + callback: (li) => { + this._buyForcePower(li); + }, + }, + ]); + new ContextMenu(this.element, ".talent-upgrade.signature-ability", [ + { + name: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.SA.ContextMenuText"), + icon: '', + callback: (li) => { + this._buySignatureAbility(li); + }, + }, + ]); // register sheet options if (["gear", "weapon", "armour"].includes(this.object.type)) { @@ -374,6 +410,52 @@ export class ItemSheetFFG extends ItemSheet { } catch (err) { CONFIG.logger.debug(err); } + } else if (this.object.type === "career") { + try { + const dragDrop = new DragDrop({ + dragSelector: ".item", + dropSelector: ".tab.career", + permissions: { dragstart: this._canDragStart.bind(this), drop: this._canDragDrop.bind(this) }, + callbacks: { drop: this._onDragItemCareer.bind(this) }, + }); + + dragDrop.bind($(`form.editable.item-sheet-${this.object.type}`)[0]); + } catch (err) { + CONFIG.logger.debug(err); + } + // handle click events for specialization and signature ability on careers + html.find(".item-delete").on("click", async (event) => { + event.stopPropagation(); + const itemId = $(event.target).data("specialization-id"); + const itemType = $(event.target).data("item-type"); + if (itemType === "specialization") { + const updateData = this.object.system.specializations; + delete updateData[itemId]; + updateData[`-=${itemId}`] = null; + this.object.update({system: {specializations: updateData}}) + } else if (itemType === "signatureability") { + const updateData = this.object.system.signatureabilities; + delete updateData[itemId]; + updateData[`-=${itemId}`] = null; + this.object.update({system: {signatureabilities: updateData}}) + } + }); + // handle click events for specialization and signature ability on careers + html.find(".item-pill2").on("click", async (event) => { + event.stopPropagation(); + const itemId = $(event.target).data("specialization-id"); + const itemType = $(event.target).data("item-type"); + let item = game.items.get(itemId); + if (!item) { + // it was removed or came from a compendium, try that instead + if (itemType === "specialization") { + item = await fromUuid(this.object.system.specializations[itemId].source); + } else if (itemType === "signatureability") { + item = await fromUuid(this.object.system.signatureabilities[itemId].source); + } + } + new Item(item).sheet.render(true); + }); } // hidden here instead of css to prevent non-editable display of edit button @@ -659,6 +741,176 @@ export class ItemSheetFFG extends ItemSheet { }); } + async _buyHandleClick(li, desired_item_type) { + const owned = this.object.flags?.starwarsffg?.ffgIsOwned; + const type = this.object.type; + if (type !== desired_item_type || !owned) { + // you can't buy talents for any old item! + // you can only buy talents for owned items! + CONFIG.logger.warn(`Refused to buy talent for non-${desired_item_type} or unowned item`); + throw new Error(`Refused to buy talent for non-${desired_item_type} or unowned item`); + } + const ownerFlag = this.object.flags?.starwarsffg?.ffgUuid; + if (!ownerFlag) { + // bad flag data, move along, citizen + CONFIG.logger.warn("Refused to buy for item with no owner flag set"); + throw new Error("Refused to buy for item with no owner flag set"); + } + const ownerId = ownerFlag.split('.')[1]; + if (!ownerId) { + CONFIG.logger.warn("Refused to buy for item with no owner ID"); + throw new Error("Refused to buy for item with no owner ID"); + } + const owner = game.actors.get(ownerId); + if (!owner) { + CONFIG.logger.warn("Refused to buy for item with no found owner actor"); + throw new Error("Refused to buy for item with no found owner actor"); + } + const availableXP = owner.system.experience.available; + const cost = $(li).data("cost"); + if (cost > availableXP) { + ui.notifications.warn(game.i18n.localize("SWFFG.Actors.Sheets.Purchase.NotEnoughXP")); + throw new Error("Not enough XP"); + } + return { + owner: owner, + cost: cost, + availableXP: availableXP, + } + } + + async _buyTalent(li) { + let owner; + let cost; + let availableXP; + try { + const basic_data = await this._buyHandleClick(li, "specialization"); + owner = basic_data.owner; + cost = basic_data.cost; + availableXP = basic_data.availableXP; + } catch (e) { + return; + } + const baseName = $(li).data("base-item-name"); + const talent = $(".talent-name", li).data("name"); + const dialog = new Dialog( + { + title: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.Talent.ConfirmTitle"), + content: game.i18n.format("SWFFG.Actors.Sheets.Purchase.Talent.ConfirmText", {cost: cost, talent: talent}), + buttons: { + done: { + icon: '', + 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 + const talentId = $(li).attr("id"); + const input = $(`[name="data.talents.${talentId}.islearned"]`, this.element)[0]; + input.checked = true; + await this.object.sheet.submit({preventClose: true}); + owner.update({system: {experience: {available: availableXP - cost}}}); + await xpLogSpend(owner, `specialization ${baseName} talent ${talent}`, cost); + }, + }, + cancel: { + icon: '', + label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.CancelPurchase"), + }, + }, + }, + { + classes: ["dialog", "starwarsffg"], + } + ).render(true); + } + + async _buyForcePower(li) { + let owner; + let cost; + let availableXP; + try { + const basic_data = await this._buyHandleClick(li, "forcepower"); + owner = basic_data.owner; + cost = basic_data.cost; + availableXP = basic_data.availableXP; + } catch (e) { + return; + } + const baseName = $(li).data("base-item-name"); + const upgrade = $(".talent-name", li).data("name"); + const dialog = new Dialog( + { + title: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.FP.ConfirmTitle"), + content: game.i18n.format("SWFFG.Actors.Sheets.Purchase.FP.ConfirmText", {cost: cost, upgrade: upgrade}), + buttons: { + done: { + icon: '', + 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 + const talentId = $(li).attr("id"); + const input = $(`[name="data.upgrades.${talentId}.islearned"]`, this.element)[0]; + input.checked = true; + await this.object.sheet.submit({preventClose: true}); + owner.update({system: {experience: {available: availableXP - cost}}}); + await xpLogSpend(owner, `force power ${baseName} upgrade ${upgrade}`, cost); + }, + }, + cancel: { + icon: '', + label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.CancelPurchase"), + }, + }, + }, + { + classes: ["dialog", "starwarsffg"], + } + ).render(true); + } + + async _buySignatureAbility(li) { + let owner; + let cost; + let availableXP; + try { + const basic_data = await this._buyHandleClick(li, "signatureability"); + owner = basic_data.owner; + cost = basic_data.cost; + availableXP = basic_data.availableXP; + } catch (e) { + return; + } + const baseName = $(li).data("base-item-name"); + const upgrade = $(".talent-name", li).data("name"); + const dialog = new Dialog( + { + title: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.SA.ConfirmTitle"), + content: game.i18n.format("SWFFG.Actors.Sheets.Purchase.SA.ConfirmText", {cost: cost, upgrade: upgrade}), + buttons: { + done: { + icon: '', + 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 + const talentId = $(li).attr("id"); + const input = $(`[name="data.upgrades.${talentId}.islearned"]`, this.element)[0]; + input.checked = true; + await this.object.sheet.submit({preventClose: true}); + owner.update({system: {experience: {available: availableXP - cost}}}); + await xpLogSpend(owner, `signature ability ${baseName} upgrade ${upgrade}`, cost); + }, + }, + cancel: { + icon: '', + label: game.i18n.localize("SWFFG.Actors.Sheets.Purchase.CancelPurchase"), + }, + }, + }, + { + classes: ["dialog", "starwarsffg"], + } + ).render(true); + } + /* -------------------------------------------- */ /** @override */ @@ -981,5 +1233,34 @@ export class ItemSheetFFG extends ItemSheet { } } + /** + * Handles dragging specializations and signature abilities to the career sheet. + * This is used for purchasing (in order to determine which specializations are career specializations) + * @param event + * @returns {Promise} + * @private + */ + async _onDragItemCareer(event) { + let data; + + try { + data = JSON.parse(event.dataTransfer.getData("text/plain")); + if (data.type !== "Item") return; + } catch (err) { + return false; + } + + // as of v10, "id" is not passed in - instead, "uuid" is. Let's use the Foundry API to get the item Document from the uuid. + const itemObject = await fromUuid(data.uuid); + + if (!itemObject) return; + + if (itemObject.type === "specialization") { + await this.object.update({system: {specializations: {[itemObject.id]: {name: itemObject.name, source: itemObject.uuid, id: itemObject.id}}}}); + } else if (itemObject.type === "signatureability") { + await this.object.update({system: {signatureabilities: {[itemObject.id]: {name: itemObject.name, source: itemObject.uuid, id: itemObject.id}}}}); + } + } + async _onDragItemStart(event) {} } diff --git a/modules/swffg-main.js b/modules/swffg-main.js index f17f6b02..5e7d0113 100644 --- a/modules/swffg-main.js +++ b/modules/swffg-main.js @@ -173,6 +173,34 @@ Hooks.once("init", async function () { CONFIG.ui.combat = CombatTrackerFFG; } + /** + * Register compendiums for sources for purchasing + */ + game.settings.register("starwarsffg", "specializationCompendiums", { + name: game.i18n.localize("SWFFG.Settings.Purchase.Specialization.Name"), + hint: game.i18n.localize("SWFFG.Settings.Purchase.Specialization.Hint"), + scope: "world", + config: true, + default: "world.oggdudespecializations", + type: String, + }); + game.settings.register("starwarsffg", "signatureAbilityCompendiums", { + name: game.i18n.localize("SWFFG.Settings.Purchase.SignatureAbility.Name"), + hint: game.i18n.localize("SWFFG.Settings.Purchase.SignatureAbility.Hint"), + scope: "world", + config: true, + default: "world.oggdudesignatureabilities", + type: String, + }); + game.settings.register("starwarsffg", "forcePowerCompendiums", { + name: game.i18n.localize("SWFFG.Settings.Purchase.ForcePower.Name"), + hint: game.i18n.localize("SWFFG.Settings.Purchase.ForcePower.Hint"), + scope: "world", + config: true, + default: "world.oggdudeforcepowers", + type: String, + }); + /** * Set an initiative formula for the system * @type {String} diff --git a/styles/mandar.css b/styles/mandar.css index c0ec600a..42951eaf 100644 --- a/styles/mandar.css +++ b/styles/mandar.css @@ -3577,10 +3577,6 @@ img { height: calc(100% - 25.25rem); } -.starwarsffg .item-sheet-career .sheet-body { - height: calc(100% - 11.75rem); -} - .starwarsffg .attributes { margin: 0; border: 0; @@ -4037,6 +4033,22 @@ img { max-width: 24px; max-height: 24px; } +.starwarsffg.sheet.item .item-sheet-career .signatureability-pill.item-pill2, .starwarsffg.sheet.item .item-sheet-career .specialization-pill.item-pill2 { + padding: 0.1rem 0.5rem; + background-color: rgba(255, 255, 255, 0.25); + border: 1px solid rgba(255, 255, 255, 0.5); + border-radius: 1rem; + cursor: pointer; + width: fit-content; +} + +.starwarsffg.sheet.item .item-sheet-career .signatureability-pill.item-pill2 .item-delete, .starwarsffg.sheet.item .item-sheet-career .specialization-pill.item-pill2 .item-delete { + cursor: pointer; +} + +.starwarsffg.sheet.actor .character .header-fields .ffg-purchase { + cursor: pointer; +} .starwarsffg .dice-tooltip .dice-rolls .roll { width: 24px; diff --git a/styles/starwarsffg.css b/styles/starwarsffg.css index 0b694337..b5ddd040 100644 --- a/styles/starwarsffg.css +++ b/styles/starwarsffg.css @@ -2948,4 +2948,16 @@ img { .destiny-flip.dPoolLight { background-color: #b8383b; color: white; -} \ No newline at end of file +} +.starwarsffg.sheet.item .item-sheet-career .signatureability-pill.item-pill2, .starwarsffg.sheet.item .item-sheet-career .specialization-pill.item-pill2 { + padding: 0.1rem 0.5rem; + background-color: rgba(255, 255, 255, 0.25); + border: 1px solid rgba(255, 255, 255, 0.5); + border-radius: 1rem; + cursor: pointer; + width: fit-content; +} + +.starwarsffg.sheet.item .item-sheet-career .signatureability-pill.item-pill2 .item-delete, .starwarsffg.sheet.item .item-sheet-career .specialization-pill.item-pill2 .item-delete { + cursor: pointer; +} diff --git a/template.json b/template.json index 54025908..e582f8eb 100644 --- a/template.json +++ b/template.json @@ -785,7 +785,9 @@ } }, "career" : { - "templates": ["core"] + "templates": ["core"], + "specializations": {}, + "signatureabilities": {} }, "signatureability": { "templates": ["core"], @@ -798,7 +800,8 @@ "upgrade5": {}, "upgrade6": {}, "upgrade7": {} - } + }, + "base_cost": 0 }, "itemattachment": { "templates": ["core", "basic", "hardpoints", "qualities", "itemattachments"], diff --git a/templates/actors/ffg-character-sheet.html b/templates/actors/ffg-character-sheet.html index 0edf4329..a9e206a7 100644 --- a/templates/actors/ffg-character-sheet.html +++ b/templates/actors/ffg-character-sheet.html @@ -29,7 +29,7 @@

{{localize "SWFFG.DragNotes"}} {{/each_when}} - + {{localize "SWFFG.SignatureAbilities"}}: @@ -53,7 +53,7 @@

{{localize "SWFFG.DragNotes"}} {{/each_when}} - + {{#if (or (eq actor.flags.starwarsffg.config.enableForcePool undefined) (eq actor.flags.starwarsffg.config.enableForcePool true) )}} {{localize "SWFFG.ForcePowers"}}: {{/if}} @@ -71,7 +71,7 @@

- + {{localize "SWFFG.Specializations"}}: @@ -91,6 +91,7 @@

+ @@ -118,7 +119,7 @@

"systems/starwarsffg/templates/parts/shared/ffg-tabs.html" displayLimited=true limited=limited items=(array (object tab="characteristics" label="SWFFG.TabCharacteristics" icon="fas fa-user-circle" cls=classType) (object tab="items" label="SWFFG.TabGear" icon="fas fa-toolbox" cls=classType) (object tab="talents" label="SWFFG.TabTalents" icon="fab fa-superpowers" cls=classType) (object tab="general" label="SWFFG.TabGeneral" icon="fas fa-address-card" cls=classType isHidden=true) (object tab="description" label="SWFFG.TabBiography" icon="fas fa-sticky-note" cls=classType) (object tab="obligation" label="SWFFG.TabObligationDutyMorality" icon="fas fa-balance-scale" cls=classType isHidden=true isHiddenV2=hideObligationDutyMoralityConflictTab) (object tab="xp" label="xp" icon="fas fa-balance-scale" cls=classType isHidden=true) )}} {{!-- Sheet Body --}}
{{!-- Characteristics Tab --}}
@@ -239,6 +240,10 @@

+ {{> "systems/starwarsffg/templates/parts/shared/ffg-block.html" (object blocktype="single" title="SWFFG.Actors.Sheets.Purchase.LogTitle" type="PopoutEditor" value=xpLog)}} +

+
{{#if (or (eq actor.flags.starwarsffg.config.enableObligation undefined) (eq actor.flags.starwarsffg.config.enableObligation true) )}}
diff --git a/templates/dialogs/ffg-confirm-purchase.html b/templates/dialogs/ffg-confirm-purchase.html new file mode 100644 index 00000000..d3308f9d --- /dev/null +++ b/templates/dialogs/ffg-confirm-purchase.html @@ -0,0 +1,12 @@ + diff --git a/templates/items/ffg-career-sheet.html b/templates/items/ffg-career-sheet.html index d0020b99..69d0814f 100644 --- a/templates/items/ffg-career-sheet.html +++ b/templates/items/ffg-career-sheet.html @@ -12,7 +12,7 @@

- {{!-- Sheet Tab Navigation --}} {{> "systems/starwarsffg/templates/parts/shared/ffg-tabs.html" displayLimited=true limited=limited items=(array (object tab="description" label="SWFFG.TabDescription" icon="far fa-file-alt" cls=classType) (object tab="attributes" label="SWFFG.TabModifiers" icon="fas fa-cog" cls=classType) )}} {{!-- Sheet Body --}} + {{!-- Sheet Tab Navigation --}} {{> "systems/starwarsffg/templates/parts/shared/ffg-tabs.html" displayLimited=true limited=limited items=(array (object tab="description" label="SWFFG.TabDescription" icon="far fa-file-alt" cls=classType) (object tab="attributes" label="SWFFG.TabModifiers" icon="fas fa-cog" cls=classType) (object tab="career" label="SWFFG.TabCareer" icon="fas fa-cog" cls=classType))}} {{!-- Sheet Body --}}
{{!-- Description Tab --}}
@@ -23,5 +23,24 @@

{{> "systems/starwarsffg/templates/parts/shared/ffg-modifiers.html"}}

+ {{!-- Specializations Tab --}} +
+

{{localize "SWFFG.Actors.Sheets.Purchase.Career.Specializations.Header"}}

+ {{#if data.specializations}} + {{#each data.specializations as |s_data id|}} +
{{s_data.name}}
+ {{/each}} + {{else}} + {{ localize "SWFFG.Actors.Sheets.Purchase.Career.Specialization.DropHint" }} + {{/if}} +

{{localize "SWFFG.Actors.Sheets.Purchase.Career.SignatureAbility.Header"}}

+ {{#if data.signatureabilities}} + {{#each data.signatureabilities as |s_data id|}} +
{{s_data.name}}
+ {{/each}} + {{else}} + {{ localize "SWFFG.Actors.Sheets.Purchase.Career.SignatureAbility.DropHint" }} + {{/if}} +
diff --git a/templates/items/ffg-forcepower-sheet.html b/templates/items/ffg-forcepower-sheet.html index 95fe5e39..849f9629 100644 --- a/templates/items/ffg-forcepower-sheet.html +++ b/templates/items/ffg-forcepower-sheet.html @@ -33,11 +33,11 @@
{{#each data.upgrades as |upgrade key|}} -
+
-
+
diff --git a/templates/items/ffg-signatureability-sheet.html b/templates/items/ffg-signatureability-sheet.html index 10fafa2d..62345203 100644 --- a/templates/items/ffg-signatureability-sheet.html +++ b/templates/items/ffg-signatureability-sheet.html @@ -21,6 +21,7 @@
+
{{ localize "SWFFG.Cost"}}
@@ -29,11 +30,11 @@
{{#each data.upgrades as |upgrade key|}} -
+
-
+
diff --git a/templates/items/ffg-specialization-sheet.html b/templates/items/ffg-specialization-sheet.html index 3209d097..f28ca28d 100644 --- a/templates/items/ffg-specialization-sheet.html +++ b/templates/items/ffg-specialization-sheet.html @@ -30,7 +30,7 @@
{{#each data.talents as |talent key|}} -
+
@@ -40,7 +40,7 @@
{{/if}}
-
{{{talent.name}}} COST: {{calculateSpecializationTalentCost key}}
+
{{{talent.name}}} COST: {{calculateSpecializationTalentCost key}}