diff --git a/CHANGELOG.md b/CHANGELOG.md index 444ebb1..f7a453e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog -## 1.3.5 - ? +## 1.4.0 - Armies +- Added Army, Cohort and Fortification sheets. - Added real icon for Inversion and Mantra. - Standardization of "great-clans-presentation" pack name to "core-journal-great-clans-presentation". +- SendToChat now check links validity before adding them. +- Compendiums : + - PoW: 'Urbane and Worldly' advantage was misspelled 'Wroldly' (thx to Cernunnos). ## 1.3.4 - Compendiums Update - Fixed a bug with sheet item drop introduced in previous version. diff --git a/system/assets/icons/actors/army.svg b/system/assets/icons/actors/army.svg new file mode 100644 index 0000000..d9454c4 --- /dev/null +++ b/system/assets/icons/actors/army.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/system/assets/icons/items/army_cohort.svg b/system/assets/icons/items/army_cohort.svg new file mode 100644 index 0000000..d9454c4 --- /dev/null +++ b/system/assets/icons/items/army_cohort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/system/assets/icons/items/army_fortification.svg b/system/assets/icons/items/army_fortification.svg new file mode 100644 index 0000000..87d7694 --- /dev/null +++ b/system/assets/icons/items/army_fortification.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/system/lang/en-en.json b/system/lang/en-en.json index 84e050b..771f1bb 100644 --- a/system/lang/en-en.json +++ b/system/lang/en-en.json @@ -21,7 +21,8 @@ }, "ACTOR": { "TypeCharacter": "Player Character", - "TypeNpc": "Non-Player Character" + "TypeNpc": "Non-Player Character", + "TypeArmy": "Army" }, "ITEM": { "TypeItem": "Item", @@ -34,7 +35,9 @@ "TypeTitle": "Title", "TypeBond": "Bond", "TypeSignature_scroll": "Signature Scroll", - "TypeItem_pattern": "Item Pattern" + "TypeItem_pattern": "Item Pattern", + "TypeArmy_fortification": "Fortification", + "TypeArmy_cohort": "Cohort" }, "l5r5e": { "global": { @@ -364,6 +367,36 @@ "adversary": "Adversary", "minion": "Minion" }, + "army": { + "warlord": "Warlord", + "allies_backers": "Allies and Backers", + "purpose_mustering": "Purpose for Mustering", + "battle_readiness": { + "title": "Battle Readiness", + "strength": "Strength", + "casualties": "Casualties", + "discipline": "Discipline", + "panic": "Panic" + }, + "commander": "Commander", + "commander_abilities": "Commander's relevant abilities", + "army_abilities": "Army Abilities", + "commander_standing": "Commander's Standing", + "supplies_logistics": "Supplies and Logistics", + "past_battles": "Past Battles", + "cohort": { + "tab": "Cohorts", + "title": "Cohorts", + "leader": "Battle Readiness", + "abilities": "Abilities" + }, + "fortification": { + "tab": "Fortifications", + "title": "Fortification Held", + "difficulty": "Difficulty Value", + "attrition_reduction": "Attrition Reduction" + } + }, "twenty_questions": { "title": "Twenty questions", "bt_abrev": "20Q", diff --git a/system/lang/es-es.json b/system/lang/es-es.json index b1d4fe7..313075f 100644 --- a/system/lang/es-es.json +++ b/system/lang/es-es.json @@ -21,7 +21,8 @@ }, "ACTOR": { "TypeCharacter": "Personaje jugador", - "TypeNpc": "Personaje no jugador" + "TypeNpc": "Personaje no jugador", + "TypeArmy": "Army" }, "ITEM": { "TypeItem": "Objeto", @@ -34,7 +35,9 @@ "TypeTitle": "Title", "TypeBond": "Bond", "TypeSignatureScroll": "Signature Scroll", - "TypeItemPattern": "Item Pattern" + "TypeItemPattern": "Item Pattern", + "TypeArmy_fortification": "Fortification", + "TypeArmy_cohort": "Cohort" }, "l5r5e": { "global": { @@ -364,6 +367,36 @@ "adversary": "Adversario", "minion": "Esbirro" }, + "army": { + "warlord": "Warlord", + "allies_backers": "Allies and Backers", + "purpose_mustering": "Purpose for Mustering", + "battle_readiness": { + "title": "Battle Readiness", + "strength": "Strength", + "casualties": "Casualties", + "discipline": "Discipline", + "panic": "Panic" + }, + "commander": "Commander", + "commander_abilities": "Commander's relevant abilities", + "army_abilities": "Army Abilities", + "commander_standing": "Commander's Standing", + "supplies_logistics": "Supplies and Logistics", + "past_battles": "Past Battles", + "cohort": { + "tab": "Cohorts", + "title": "Cohorts", + "leader": "Battle Readiness", + "abilities": "Abilities" + }, + "fortification": { + "tab": "Fortifications", + "title": "Fortification Held", + "difficulty": "Difficulty Value", + "attrition_reduction": "Attrition Reduction" + } + }, "twenty_questions": { "title": "Veinte preguntas", "bt_abrev": "20P", diff --git a/system/lang/fr-fr.json b/system/lang/fr-fr.json index 5e7dc36..594094c 100644 --- a/system/lang/fr-fr.json +++ b/system/lang/fr-fr.json @@ -21,7 +21,8 @@ }, "ACTOR": { "TypeCharacter": "Personnage Joueur", - "TypeNpc": "Personnage non Joueur" + "TypeNpc": "Personnage non Joueur", + "TypeArmy": "Armée" }, "ITEM": { "TypeItem": "Objet", @@ -34,7 +35,9 @@ "TypeTitle": "Titre", "TypeBond": "Lien", "TypeSignature_scroll": "Rouleau de marque", - "TypeItem_pattern": "Procédé de fabrication" + "TypeItem_pattern": "Procédé de fabrication", + "TypeArmy_fortification": "Fortification", + "TypeArmy_cohort": "Régiment" }, "l5r5e": { "global": { @@ -364,6 +367,36 @@ "adversary": "Antagoniste", "minion": "Sous-fifre" }, + "army": { + "warlord": "Seigneur de guerre", + "allies_backers": "Alliés et Soutiens", + "purpose_mustering": "Objectif du rassemblement", + "battle_readiness": { + "title": "Aptitudes au combat", + "strength": "Force", + "casualties": "Usure et pertes", + "discipline": "Discipline", + "panic": "Panique" + }, + "commander": "Commandant", + "commander_abilities": "Capacités du commandant", + "army_abilities": "Capacités de l'armée", + "commander_standing": "Position du commandant", + "supplies_logistics": "Fournitures et logistique", + "past_battles": "Batailles passées", + "cohort": { + "tab": "Régiments", + "title": "Régiment", + "leader": "Aptitude au combat", + "abilities": "Capacités" + }, + "fortification": { + "tab": "Fortifications", + "title": "Fortifications", + "difficulty": "Difficulté", + "attrition_reduction": "Attrition Reduction" + } + }, "twenty_questions": { "title": "Le jeu des Vingt questions", "bt_abrev": "20Q", diff --git a/system/scripts/actor.js b/system/scripts/actor.js index 663a0ed..b1031e6 100644 --- a/system/scripts/actor.js +++ b/system/scripts/actor.js @@ -55,6 +55,23 @@ export class ActorL5r5e extends Actor { { overwrite: false } ); break; + + case "army": + foundry.utils.mergeObject( + data.token, + { + actorLink: true, + disposition: 0, // neutral + bar1: { + attribute: "battle_readiness.casualties_strength", + }, + bar2: { + attribute: "battle_readiness.panic_discipline", + }, + }, + { overwrite: false } + ); + break; } await super.create(data, options); } diff --git a/system/scripts/actors/army-sheet.js b/system/scripts/actors/army-sheet.js new file mode 100644 index 0000000..dab9fce --- /dev/null +++ b/system/scripts/actors/army-sheet.js @@ -0,0 +1,78 @@ +import { BaseSheetL5r5e } from "./base-sheet.js"; + +/** + * Sheet for Army "actor" + */ +export class ArmySheetL5r5e extends BaseSheetL5r5e { + /** + * Commons options + */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["l5r5e", "sheet", "actor", "army"], + template: CONFIG.l5r5e.paths.templates + "actors/army-sheet.html", + width: 600, + height: 800, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "cohort" }], + dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }], + }); + } + + /** @inheritdoc */ + getData(options = {}) { + const sheetData = super.getData(options); + + // Split Items by types + sheetData.data.splitItemsList = this._splitItems(sheetData); + + return sheetData; + } + + /** + * Split Items by types for better readability + * @private + */ + _splitItems(sheetData) { + const out = { + army_cohort: [], + army_fortification: [], + }; + + sheetData.items.forEach((item) => { + if (["army_cohort", "army_fortification"].includes(item.type)) { + out[item.type].push(item); + } + }); + + return out; + } + + /** + * Handle dropped data on the Actor sheet + * @param {DragEvent} event + */ + async _onDrop(event) { + // *** Everything below here is only needed if the sheet is editable *** + if (!this.isEditable) { + return; + } + + // Check item type and subtype + const item = await game.l5r5e.HelpersL5r5e.getDragnDropTargetObject(event); + if (!item || item.documentName !== "Item" || !["army_cohort", "army_fortification"].includes(item.data.type)) { + console.warn("L5R5E | Wrong type", item.data.type); + return; + } + + // Can add the item - Foundry override cause props + const allowed = Hooks.call("dropActorSheetData", this.actor, this, item); + if (allowed === false) { + return; + } + + let itemData = item.data.toObject(true); + + // Finally create the embed + return this.actor.createEmbeddedDocuments("Item", [itemData]); + } +} diff --git a/system/scripts/actors/base-character-sheet.js b/system/scripts/actors/base-character-sheet.js new file mode 100644 index 0000000..4d16569 --- /dev/null +++ b/system/scripts/actors/base-character-sheet.js @@ -0,0 +1,547 @@ +import { BaseSheetL5r5e } from "./base-sheet.js"; + +/** + * Base Sheet for Character types (Character and Npc) + */ +export class BaseCharacterSheetL5r5e extends BaseSheetL5r5e { + /** + * Commons options + */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["l5r5e", "sheet", "actor"], + // template: CONFIG.l5r5e.paths.templates + "actors/character-sheet.html", + width: 600, + height: 800, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "skills" }], + dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }], + }); + } + + /** @inheritdoc */ + getData(options = {}) { + const sheetData = super.getData(options); + + sheetData.data.stances = CONFIG.l5r5e.stances; + sheetData.data.techniquesList = game.l5r5e.HelpersL5r5e.getTechniquesList({ displayInTypes: true }); + + // Split Techniques by types + sheetData.data.splitTechniquesList = this._splitTechniques(sheetData); + + // Split Items by types + sheetData.data.splitItemsList = this._splitItems(sheetData); + + return sheetData; + } + + /** + * Split Techniques by types for better readability + * @private + */ + _splitTechniques(sheetData) { + const out = {}; + const schoolTechniques = Array.from(CONFIG.l5r5e.techniques) + .filter(([id, cfg]) => cfg.type === "school") + .map(([id, cfg]) => id); + + // Build the list order + Array.from(CONFIG.l5r5e.techniques) + .filter(([id, cfg]) => cfg.type !== "custom" || game.settings.get("l5r5e", "techniques-customs")) + .forEach(([id, cfg]) => { + out[id] = []; + }); + + // Add tech the character knows + sheetData.items.forEach((item) => { + switch (item.type) { + case "technique": + if (!out[item.data.technique_type]) { + console.warn( + `L5R5E | Empty or unknown technique type[${item.data.technique_type}] forced to "kata" in item id[${item._id}], name[${item.name}]` + ); + item.data.technique_type = "kata"; + } + out[item.data.technique_type].push(item); + break; + + case "title": + // Embed technique in titles + Array.from(item.data.items).forEach(([id, embedItem]) => { + if (embedItem.data.type === "technique") { + if (!out[embedItem.data.data.technique_type]) { + console.warn( + `L5R5E | Empty or unknown technique type[${embedItem.data.data.technique_type}] forced to "kata" in item id[${id}], name[${embedItem.data.name}], parent: id[${item._id}], name[${item.name}]` + ); + embedItem.data.data.technique_type = "kata"; + } + out[embedItem.data.data.technique_type].push(embedItem.data); + } + }); + + // If unlocked, add the "title_ability" as technique (or always displayed for npc) + if (item.data.xp_used >= item.data.xp_cost || this.document.type === "npc") { + out["title_ability"].push(item); + } + break; + } //swi + }); + + // Remove unused techs + Object.keys(out).forEach((tech) => { + if (out[tech].length < 1 && !sheetData.data.data.techniques[tech] && !schoolTechniques.includes(tech)) { + delete out[tech]; + } + }); + + // Manage school add button + sheetData.data.data.techniques["school_ability"] = out["school_ability"].length === 0; + sheetData.data.data.techniques["mastery_ability"] = out["mastery_ability"].length === 0; + + // Always display "school_ability", but display a empty "mastery_ability" field only if rank >= 5 + if (sheetData.data.data.identity?.school_rank < 5 && out["mastery_ability"].length === 0) { + delete out["mastery_ability"]; + } + + return out; + } + + /** + * Split Items by types for better readability + * @private + */ + _splitItems(sheetData) { + const out = { + weapon: [], + armor: [], + item: [], + }; + + sheetData.items.forEach((item) => { + if (["item", "armor", "weapon"].includes(item.type)) { + out[item.type].push(item); + } + }); + + return out; + } + + /** + * Handle dropped data on the Actor sheet + * @param {DragEvent} event + */ + async _onDrop(event) { + // *** Everything below here is only needed if the sheet is editable *** + if (!this.isEditable) { + return; + } + + // Check item type and subtype + const item = await game.l5r5e.HelpersL5r5e.getDragnDropTargetObject(event); + if (!item || !["Item", "JournalEntry"].includes(item.documentName) || item.data.type === "property") { + return; + } + + // Specific curriculum journal drop + if (item.documentName === "JournalEntry") { + // npc does not have this + if (!this.actor.data.data.identity?.school_curriculum_journal) { + return; + } + this.actor.data.data.identity.school_curriculum_journal = { + id: item.data._id, + name: item.data.name, + pack: item.pack || null, + }; + await this.actor.update({ + data: { + identity: { + school_curriculum_journal: this.actor.data.data.identity.school_curriculum_journal, + }, + }, + }); + return; + } + + // Dropped a item with same "id" as one owned + if (this.actor.data.items) { + // Exit if we already owned exactly this id (drag a personal item on our own sheet) + if ( + this.actor.data.items.some((embedItem) => { + // Search in children + if (embedItem.items instanceof Map && embedItem.items.has(item.data._id)) { + return true; + } + return embedItem.data._id === item.data._id; + }) + ) { + return; + } + + // Add quantity instead if they have (id is different so use type and name) + if (item.data.data.quantity) { + const tmpItem = this.actor.data.items.find( + (embedItem) => embedItem.name === item.data.name && embedItem.type === item.data.type + ); + if (tmpItem && this._modifyQuantity(tmpItem.id, 1)) { + return; + } + } + } + + // Can add the item - Foundry override cause props + const allowed = Hooks.call("dropActorSheetData", this.actor, this, item); + if (allowed === false) { + return; + } + + let itemData = item.data.toObject(true); + + // Item subtype specific + switch (itemData.type) { + case "advancement": + // Specific advancements, remove 1 to selected ring/skill + await this.actor.addBonus(item); + break; + + case "title": + // Generate new Ids for the embed items + await item.generateNewIdsForAllEmbedItems(); + + // Add embed advancements bonus + for (let [embedId, embedItem] of item.data.data.items) { + if (embedItem.data.type === "advancement") { + await this.actor.addBonus(embedItem); + } + } + + // refresh data + itemData = item.data.toObject(true); + break; + + case "technique": + // School_ability and mastery_ability, allow only 1 per type + if (CONFIG.l5r5e.techniques.get(itemData.data.technique_type)?.type === "school") { + if ( + Array.from(this.actor.items).some((e) => { + return ( + e.type === "technique" && e.data.data.technique_type === itemData.data.technique_type + ); + }) + ) { + ui.notifications.info(game.i18n.localize("l5r5e.techniques.only_one")); + return; + } + + // No cost for schools + itemData.data.xp_cost = 0; + itemData.data.xp_used = 0; + itemData.data.in_curriculum = true; + } else { + // Check if technique is allowed for this character + if (!game.user.isGM && !this.actor.data.data.techniques[itemData.data.technique_type]) { + ui.notifications.info(game.i18n.localize("l5r5e.techniques.not_allowed")); + return; + } + + // Verify cost + itemData.data.xp_cost = + itemData.data.xp_cost > 0 ? itemData.data.xp_cost : CONFIG.l5r5e.xp.techniqueCost; + itemData.data.xp_used = itemData.data.xp_cost; + } + break; + } + + // Modify the bought at rank to the current actor rank + if (itemData.data.bought_at_rank !== undefined && this.actor.data.data.identity?.school_rank) { + itemData.data.bought_at_rank = this.actor.data.data.identity.school_rank; + } + + // Finally create the embed + return this.actor.createEmbeddedDocuments("Item", [itemData]); + } + + /** + * Subscribe to events from the sheet. + * @param {jQuery} html HTML content of the sheet. + */ + activateListeners(html) { + super.activateListeners(html); + + // *** Everything below here is only needed if the sheet is editable *** + if (!this.isEditable) { + return; + } + + // *** Dice event on Skills clic *** + html.find(".dice-picker").on("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + const li = $(event.currentTarget); + let skillId = li.data("skill") || null; + + const weaponId = li.data("weapon-id") || null; + if (weaponId) { + skillId = this._getWeaponSkillId(weaponId); + } + + new game.l5r5e.DicePickerDialog({ + ringId: li.data("ring") || null, + skillId: skillId, + skillCatId: li.data("skillcat") || null, + isInitiativeRoll: li.data("initiative") || false, + actor: this.actor, + }).render(true); + }); + + // Prepared (Initiative) + html.find(".prepared-control").on("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + this._switchPrepared(); + }); + + // Equipped / Readied + html.find(".equip-readied-control").on("click", this._switchEquipReadied.bind(this)); + + // Others Advancements + html.find(".item-advancement-choose").on("click", this._showDialogAddSubItem.bind(this)); + } + + /** + * Switch the state "prepared" (initiative) + * @private + */ + _switchPrepared() { + this.actor.data.data.prepared = !this.actor.data.data.prepared; + this.actor.update({ + data: { + prepared: this.actor.data.data.prepared, + }, + }); + this.render(false); + } + + /** + * Add a generic item with sub type + * @param {string} type Item sub type (armor, weapon, bond...) + * @param {boolean} isEquipped For item with prop "Equipped" set the value + * @param {string|null} techniqueType Technique subtype (kata, shuji...) + * @return {Promise} + * @private + */ + async _createSubItem({ type, isEquipped = false, techniqueType = null }) { + if (!type) { + return; + } + + const created = await this.actor.createEmbeddedDocuments("Item", [ + { + name: game.i18n.localize(`ITEM.Type${type.capitalize()}`), + type: type, + img: `${CONFIG.l5r5e.paths.assets}icons/items/${type}.svg`, + }, + ]); + if (created?.length < 1) { + return; + } + const item = this.actor.items.get(created[0].id); + + // Assign current school rank to the new adv/tech + if (this.actor.data.data.identity?.school_rank) { + item.data.data.bought_at_rank = this.actor.data.data.identity.school_rank; + if (["advancement", "technique"].includes(item.data.type)) { + item.data.data.rank = this.actor.data.data.identity.school_rank; + } + } + + switch (item.data.type) { + case "item": // no break + case "armor": // no break + case "weapon": + item.data.data.equipped = isEquipped; + break; + + case "technique": { + // If technique, select the current sub-type + if (CONFIG.l5r5e.techniques.get(techniqueType)) { + item.data.name = game.i18n.localize(`l5r5e.techniques.${techniqueType}`); + item.data.img = `${CONFIG.l5r5e.paths.assets}icons/techs/${techniqueType}.svg`; + item.data.data.technique_type = techniqueType; + } + break; + } + } + + item.sheet.render(true); + } + + /** + * Display a dialog to choose what Item to add + * @param {Event} event + * @return {Promise} + * @private + */ + async _showDialogAddSubItem(event) { + game.l5r5e.HelpersL5r5e.showSubItemDialog(["bond", "title", "signature_scroll", "item_pattern"]).then( + (selectedType) => { + this._createSubItem({ type: selectedType }); + } + ); + } + + /** + * Add a generic item with sub type + * @param {Event} event + * @private + */ + async _addSubItem(event) { + event.preventDefault(); + event.stopPropagation(); + + const type = $(event.currentTarget).data("item-type"); + if (!type) { + return; + } + + const isEquipped = $(event.currentTarget).data("item-equipped") || false; + const techniqueType = $(event.currentTarget).data("tech-type") || null; + + return this._createSubItem({ type, isEquipped, techniqueType }); + } + + /** + * Delete a generic item with sub type + * @param {Event} event + * @private + */ + _deleteSubItem(event) { + event.preventDefault(); + event.stopPropagation(); + + const itemId = $(event.currentTarget).data("item-id"); + if (!itemId) { + return; + } + + const tmpItem = this.actor.items.get(itemId); + if (!tmpItem) { + return; + } + + // Remove 1 qty if possible + if (tmpItem.data.data.quantity > 1 && this._modifyQuantity(tmpItem.id, -1)) { + return; + } + + const callback = async () => { + switch (tmpItem.type) { + case "advancement": + // Remove advancements bonus (1 to selected ring/skill) + await this.actor.removeBonus(tmpItem); + break; + + case "title": + // Remove embed advancements bonus + for (let [embedId, embedItem] of tmpItem.data.data.items) { + if (embedItem.data.type === "advancement") { + await this.actor.removeBonus(embedItem); + } + } + break; + } + return this.actor.deleteEmbeddedDocuments("Item", [itemId]); + }; + + // Holing Ctrl = without confirm + if (event.ctrlKey) { + return callback(); + } + + game.l5r5e.HelpersL5r5e.confirmDeleteDialog( + game.i18n.format("l5r5e.global.delete_confirm", { name: tmpItem.name }), + callback + ); + } + + /** + * Switch "in_curriculum" + * @param {Event} event + * @private + */ + _switchSubItemCurriculum(event) { + event.preventDefault(); + event.stopPropagation(); + + const itemId = $(event.currentTarget).data("item-id"); + const item = this.actor.items.get(itemId); + if (item.type !== "item") { + item.update({ + data: { + in_curriculum: !item.data.data.in_curriculum, + }, + }); + } + } + + /** + * Add or subtract a quantity to a owned item + * @private + */ + _modifyQuantity(itemId, add) { + const tmpItem = this.actor.items.get(itemId); + if (tmpItem) { + tmpItem.data.data.quantity = Math.max(1, tmpItem.data.data.quantity + add); + tmpItem.update({ + data: { + quantity: tmpItem.data.data.quantity, + }, + }); + return true; + } + return false; + } + + /** + * Switch Readied state on a weapon + * @param {Event} event + * @private + */ + _switchEquipReadied(event) { + event.preventDefault(); + event.stopPropagation(); + + const type = $(event.currentTarget).data("type"); + if (!["equipped", "readied"].includes(type)) { + return; + } + + const itemId = $(event.currentTarget).data("item-id"); + const tmpItem = this.actor.items.get(itemId); + if (!tmpItem || tmpItem.data.data[type] === undefined) { + return; + } + + tmpItem.data.data[type] = !tmpItem.data.data[type]; + const data = { + equipped: tmpItem.data.data.equipped, + }; + // Only weapons + if (tmpItem.data.data.readied !== undefined) { + data.readied = tmpItem.data.data.readied; + } + + tmpItem.update({ data }); + } + + /** + * Get the skillId for this weaponId + * @private + */ + _getWeaponSkillId(weaponId) { + const item = this.actor.items.get(weaponId); + if (!!item && item.type === "weapon") { + return item.data.data.skill; + } + return null; + } +} diff --git a/system/scripts/actors/base-sheet.js b/system/scripts/actors/base-sheet.js index 22b0851..0969efe 100644 --- a/system/scripts/actors/base-sheet.js +++ b/system/scripts/actors/base-sheet.js @@ -6,7 +6,7 @@ export class BaseSheetL5r5e extends ActorSheet { * Commons options */ static get defaultOptions() { - return mergeObject(super.defaultOptions, { + return foundry.utils.mergeObject(super.defaultOptions, { classes: ["l5r5e", "sheet", "actor"], // template: CONFIG.l5r5e.paths.templates + "actors/character-sheet.html", width: 600, @@ -45,114 +45,15 @@ export class BaseSheetL5r5e extends ActorSheet { const sheetData = super.getData(options); sheetData.data.dtypes = ["String", "Number", "Boolean"]; - sheetData.data.stances = CONFIG.l5r5e.stances; - sheetData.data.techniquesList = game.l5r5e.HelpersL5r5e.getTechniquesList({ displayInTypes: true }); // Sort Items by name sheetData.items.sort((a, b) => { return a.name.localeCompare(b.name); }); - // Split Techniques by types - sheetData.data.splitTechniquesList = this._splitTechniques(sheetData); - - // Split Items by types - sheetData.data.splitItemsList = this._splitItems(sheetData); - return sheetData; } - /** - * Split Techniques by types for better readability - * @private - */ - _splitTechniques(sheetData) { - const out = {}; - const schoolTechniques = Array.from(CONFIG.l5r5e.techniques) - .filter(([id, cfg]) => cfg.type === "school") - .map(([id, cfg]) => id); - - // Build the list order - Array.from(CONFIG.l5r5e.techniques) - .filter(([id, cfg]) => cfg.type !== "custom" || game.settings.get("l5r5e", "techniques-customs")) - .forEach(([id, cfg]) => { - out[id] = []; - }); - - // Add tech the character knows - sheetData.items.forEach((item) => { - switch (item.type) { - case "technique": - if (!out[item.data.technique_type]) { - console.warn( - `L5R5E | Empty or unknown technique type[${item.data.technique_type}] forced to "kata" in item id[${item._id}], name[${item.name}]` - ); - item.data.technique_type = "kata"; - } - out[item.data.technique_type].push(item); - break; - - case "title": - // Embed technique in titles - Array.from(item.data.items).forEach(([id, embedItem]) => { - if (embedItem.data.type === "technique") { - if (!out[embedItem.data.data.technique_type]) { - console.warn( - `L5R5E | Empty or unknown technique type[${embedItem.data.data.technique_type}] forced to "kata" in item id[${id}], name[${embedItem.data.name}], parent: id[${item._id}], name[${item.name}]` - ); - embedItem.data.data.technique_type = "kata"; - } - out[embedItem.data.data.technique_type].push(embedItem.data); - } - }); - - // If unlocked, add the "title_ability" as technique (or always displayed for npc) - if (item.data.xp_used >= item.data.xp_cost || this.document.type === "npc") { - out["title_ability"].push(item); - } - break; - } //swi - }); - - // Remove unused techs - Object.keys(out).forEach((tech) => { - if (out[tech].length < 1 && !sheetData.data.data.techniques[tech] && !schoolTechniques.includes(tech)) { - delete out[tech]; - } - }); - - // Manage school add button - sheetData.data.data.techniques["school_ability"] = out["school_ability"].length === 0; - sheetData.data.data.techniques["mastery_ability"] = out["mastery_ability"].length === 0; - - // Always display "school_ability", but display a empty "mastery_ability" field only if rank >= 5 - if (sheetData.data.data.identity?.school_rank < 5 && out["mastery_ability"].length === 0) { - delete out["mastery_ability"]; - } - - return out; - } - - /** - * Split Items by types for better readability - * @private - */ - _splitItems(sheetData) { - const out = { - weapon: [], - armor: [], - item: [], - }; - - sheetData.items.forEach((item) => { - if (["item", "armor", "weapon"].includes(item.type)) { - out[item.type].push(item); - } - }); - - return out; - } - /** * Return a light sheet if in "limited" state * @override @@ -196,141 +97,6 @@ export class BaseSheetL5r5e extends ActorSheet { return super._updateObject(event, formData); } - /** - * Handle dropped data on the Actor sheet - * @param {DragEvent} event - */ - async _onDrop(event) { - // *** Everything below here is only needed if the sheet is editable *** - if (!this.isEditable) { - return; - } - - // Check item type and subtype - const item = await game.l5r5e.HelpersL5r5e.getDragnDropTargetObject(event); - if (!item || !["Item", "JournalEntry"].includes(item.documentName) || item.data.type === "property") { - return; - } - - // Specific curriculum journal drop - if (item.documentName === "JournalEntry") { - // npc does not have this - if (!this.actor.data.data.identity?.school_curriculum_journal) { - return; - } - this.actor.data.data.identity.school_curriculum_journal = { - id: item.data._id, - name: item.data.name, - pack: item.pack || null, - }; - await this.actor.update({ - data: { - identity: { - school_curriculum_journal: this.actor.data.data.identity.school_curriculum_journal, - }, - }, - }); - return; - } - - // Dropped a item with same "id" as one owned - if (this.actor.data.items) { - // Exit if we already owned exactly this id (drag a personal item on our own sheet) - if ( - this.actor.data.items.some((embedItem) => { - // Search in children - if (embedItem.items instanceof Map && embedItem.items.has(item.data._id)) { - return true; - } - return embedItem.data._id === item.data._id; - }) - ) { - return; - } - - // Add quantity instead if they have (id is different so use type and name) - if (item.data.data.quantity) { - const tmpItem = this.actor.data.items.find( - (embedItem) => embedItem.name === item.data.name && embedItem.type === item.data.type - ); - if (tmpItem && this._modifyQuantity(tmpItem.id, 1)) { - return; - } - } - } - - // Can add the item - Foundry override cause props - const allowed = Hooks.call("dropActorSheetData", this.actor, this, item); - if (allowed === false) { - return; - } - - let itemData = item.data.toObject(true); - - // Item subtype specific - switch (itemData.type) { - case "advancement": - // Specific advancements, remove 1 to selected ring/skill - await this.actor.addBonus(item); - break; - - case "title": - // Generate new Ids for the embed items - await item.generateNewIdsForAllEmbedItems(); - - // Add embed advancements bonus - for (let [embedId, embedItem] of item.data.data.items) { - if (embedItem.data.type === "advancement") { - await this.actor.addBonus(embedItem); - } - } - - // refresh data - itemData = item.data.toObject(true); - break; - - case "technique": - // School_ability and mastery_ability, allow only 1 per type - if (CONFIG.l5r5e.techniques.get(itemData.data.technique_type)?.type === "school") { - if ( - Array.from(this.actor.items).some((e) => { - return ( - e.type === "technique" && e.data.data.technique_type === itemData.data.technique_type - ); - }) - ) { - ui.notifications.info(game.i18n.localize("l5r5e.techniques.only_one")); - return; - } - - // No cost for schools - itemData.data.xp_cost = 0; - itemData.data.xp_used = 0; - itemData.data.in_curriculum = true; - } else { - // Check if technique is allowed for this character - if (!game.user.isGM && !this.actor.data.data.techniques[itemData.data.technique_type]) { - ui.notifications.info(game.i18n.localize("l5r5e.techniques.not_allowed")); - return; - } - - // Verify cost - itemData.data.xp_cost = - itemData.data.xp_cost > 0 ? itemData.data.xp_cost : CONFIG.l5r5e.xp.techniqueCost; - itemData.data.xp_used = itemData.data.xp_cost; - } - break; - } - - // Modify the bought at rank to the current actor rank - if (itemData.data.bought_at_rank !== undefined && this.actor.data.data.identity?.school_rank) { - itemData.data.bought_at_rank = this.actor.data.data.identity.school_rank; - } - - // Finally create the embed - return this.actor.createEmbeddedDocuments("Item", [itemData]); - } - /** * Subscribe to events from the sheet. * @param {jQuery} html HTML content of the sheet. @@ -346,27 +112,6 @@ export class BaseSheetL5r5e extends ActorSheet { return; } - // *** Dice event on Skills clic *** - html.find(".dice-picker").on("click", (event) => { - event.preventDefault(); - event.stopPropagation(); - const li = $(event.currentTarget); - let skillId = li.data("skill") || null; - - const weaponId = li.data("weapon-id") || null; - if (weaponId) { - skillId = this._getWeaponSkillId(weaponId); - } - - new game.l5r5e.DicePickerDialog({ - ringId: li.data("ring") || null, - skillId: skillId, - skillCatId: li.data("skillcat") || null, - isInitiativeRoll: li.data("initiative") || false, - actor: this.actor, - }).render(true); - }); - // On focus on one numeric element, select all text for better experience html.find(".select-on-focus").on("focus", (event) => { event.preventDefault(); @@ -374,48 +119,19 @@ export class BaseSheetL5r5e extends ActorSheet { event.target.select(); }); - // Prepared (Initiative) - html.find(".prepared-control").on("click", (event) => { - event.preventDefault(); - event.stopPropagation(); - this._switchPrepared(); - }); - - // Equipped / Readied - html.find(".equip-readied-control").on("click", this._switchEquipReadied.bind(this)); - // *** Items : add, edit, delete *** html.find(".item-add").on("click", this._addSubItem.bind(this)); html.find(`.item-edit`).on("click", this._editSubItem.bind(this)); html.find(`.item-delete`).on("click", this._deleteSubItem.bind(this)); - - // Others Advancements - html.find(".item-advancement-choose").on("click", this._showDialogAddSubItem.bind(this)); - } - - /** - * Switch the state "prepared" (initiative) - * @private - */ - _switchPrepared() { - this.actor.data.data.prepared = !this.actor.data.data.prepared; - this.actor.update({ - data: { - prepared: this.actor.data.data.prepared, - }, - }); - this.render(false); } /** * Add a generic item with sub type * @param {string} type Item sub type (armor, weapon, bond...) - * @param {boolean} isEquipped For item with prop "Equipped" set the value - * @param {string|null} techniqueType Technique subtype (kata, shuji...) * @return {Promise} * @private */ - async _createSubItem({ type, isEquipped = false, techniqueType = null }) { + async _createSubItem({ type }) { if (!type) { return; } @@ -432,49 +148,9 @@ export class BaseSheetL5r5e extends ActorSheet { } const item = this.actor.items.get(created[0].id); - // Assign current school rank to the new adv/tech - if (this.actor.data.data.identity?.school_rank) { - item.data.data.bought_at_rank = this.actor.data.data.identity.school_rank; - if (["advancement", "technique"].includes(item.data.type)) { - item.data.data.rank = this.actor.data.data.identity.school_rank; - } - } - - switch (item.data.type) { - case "item": // no break - case "armor": // no break - case "weapon": - item.data.data.equipped = isEquipped; - break; - - case "technique": { - // If technique, select the current sub-type - if (CONFIG.l5r5e.techniques.get(techniqueType)) { - item.data.name = game.i18n.localize(`l5r5e.techniques.${techniqueType}`); - item.data.img = `${CONFIG.l5r5e.paths.assets}icons/techs/${techniqueType}.svg`; - item.data.data.technique_type = techniqueType; - } - break; - } - } - item.sheet.render(true); } - /** - * Display a dialog to choose what Item to add - * @param {Event} event - * @return {Promise} - * @private - */ - async _showDialogAddSubItem(event) { - game.l5r5e.HelpersL5r5e.showSubItemDialog(["bond", "title", "signature_scroll", "item_pattern"]).then( - (selectedType) => { - this._createSubItem({ type: selectedType }); - } - ); - } - /** * Add a generic item with sub type * @param {Event} event @@ -489,10 +165,7 @@ export class BaseSheetL5r5e extends ActorSheet { return; } - const isEquipped = $(event.currentTarget).data("item-equipped") || false; - const techniqueType = $(event.currentTarget).data("tech-type") || null; - - return this._createSubItem({ type, isEquipped, techniqueType }); + return this._createSubItem({ type }); } /** @@ -530,27 +203,7 @@ export class BaseSheetL5r5e extends ActorSheet { return; } - // Remove 1 qty if possible - if (tmpItem.data.data.quantity > 1 && this._modifyQuantity(tmpItem.id, -1)) { - return; - } - const callback = async () => { - switch (tmpItem.type) { - case "advancement": - // Remove advancements bonus (1 to selected ring/skill) - await this.actor.removeBonus(tmpItem); - break; - - case "title": - // Remove embed advancements bonus - for (let [embedId, embedItem] of tmpItem.data.data.items) { - if (embedItem.data.type === "advancement") { - await this.actor.removeBonus(embedItem); - } - } - break; - } return this.actor.deleteEmbeddedDocuments("Item", [itemId]); }; @@ -564,86 +217,4 @@ export class BaseSheetL5r5e extends ActorSheet { callback ); } - - /** - * Switch "in_curriculum" - * @param {Event} event - * @private - */ - _switchSubItemCurriculum(event) { - event.preventDefault(); - event.stopPropagation(); - - const itemId = $(event.currentTarget).data("item-id"); - const item = this.actor.items.get(itemId); - if (item.type !== "item") { - item.update({ - data: { - in_curriculum: !item.data.data.in_curriculum, - }, - }); - } - } - - /** - * Add or subtract a quantity to a owned item - * @private - */ - _modifyQuantity(itemId, add) { - const tmpItem = this.actor.items.get(itemId); - if (tmpItem) { - tmpItem.data.data.quantity = Math.max(1, tmpItem.data.data.quantity + add); - tmpItem.update({ - data: { - quantity: tmpItem.data.data.quantity, - }, - }); - return true; - } - return false; - } - - /** - * Switch Readied state on a weapon - * @param {Event} event - * @private - */ - _switchEquipReadied(event) { - event.preventDefault(); - event.stopPropagation(); - - const type = $(event.currentTarget).data("type"); - if (!["equipped", "readied"].includes(type)) { - return; - } - - const itemId = $(event.currentTarget).data("item-id"); - const tmpItem = this.actor.items.get(itemId); - if (!tmpItem || tmpItem.data.data[type] === undefined) { - return; - } - - tmpItem.data.data[type] = !tmpItem.data.data[type]; - const data = { - equipped: tmpItem.data.data.equipped, - }; - // Only weapons - if (tmpItem.data.data.readied !== undefined) { - data.readied = tmpItem.data.data.readied; - } - - tmpItem.update({ data }); - } - - /** - * Get the skillId for this weaponId - * @private - */ - _getWeaponSkillId(weaponId) { - const item = this.actor.items.get(weaponId); - if (!!item && item.type === "weapon") { - return item.data.data.skill; - } - return null; - } } diff --git a/system/scripts/actors/character-sheet.js b/system/scripts/actors/character-sheet.js index e2b25d5..5f3ede2 100644 --- a/system/scripts/actors/character-sheet.js +++ b/system/scripts/actors/character-sheet.js @@ -1,10 +1,10 @@ -import { BaseSheetL5r5e } from "./base-sheet.js"; +import { BaseCharacterSheetL5r5e } from "./base-character-sheet.js"; import { TwentyQuestionsDialog } from "./twenty-questions-dialog.js"; /** * Actor / Character Sheet */ -export class CharacterSheetL5r5e extends BaseSheetL5r5e { +export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e { static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { classes: ["l5r5e", "sheet", "actor"], diff --git a/system/scripts/actors/npc-sheet.js b/system/scripts/actors/npc-sheet.js index 7325068..f64c482 100644 --- a/system/scripts/actors/npc-sheet.js +++ b/system/scripts/actors/npc-sheet.js @@ -1,9 +1,9 @@ -import { BaseSheetL5r5e } from "./base-sheet.js"; +import { BaseCharacterSheetL5r5e } from "./base-character-sheet.js"; /** * NPC Sheet */ -export class NpcSheetL5r5e extends BaseSheetL5r5e { +export class NpcSheetL5r5e extends BaseCharacterSheetL5r5e { /** * Sub Types */ diff --git a/system/scripts/helpers.js b/system/scripts/helpers.js index 164c0e1..792012d 100644 --- a/system/scripts/helpers.js +++ b/system/scripts/helpers.js @@ -478,7 +478,7 @@ export class HelpersL5r5e { * Get a Item from a Actor Sheet * @param {Event} event HTML Event * @param {ActorL5r5e} actor - * @return {ItemL5r5e} + * @return {Promise} */ static async getEmbedItemByEvent(event, actor) { const current = $(event.currentTarget); diff --git a/system/scripts/items/army-cohort-sheet.js b/system/scripts/items/army-cohort-sheet.js new file mode 100644 index 0000000..1f8aac5 --- /dev/null +++ b/system/scripts/items/army-cohort-sheet.js @@ -0,0 +1,17 @@ +import { ItemSheetL5r5e } from "./item-sheet.js"; + +/** + * @extends {ItemSheetL5r5e} + */ +export class ArmyCohortSheetL5r5e extends ItemSheetL5r5e { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["l5r5e", "sheet", "army-cohort"], + template: CONFIG.l5r5e.paths.templates + "items/army-cohort/army-cohort-sheet.html", + width: 520, + height: 480, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }], + }); + } +} diff --git a/system/scripts/items/army-fortification-sheet.js b/system/scripts/items/army-fortification-sheet.js new file mode 100644 index 0000000..24b09de --- /dev/null +++ b/system/scripts/items/army-fortification-sheet.js @@ -0,0 +1,17 @@ +import { ItemSheetL5r5e } from "./item-sheet.js"; + +/** + * @extends {ItemSheetL5r5e} + */ +export class ArmyFortificationSheetL5r5e extends ItemSheetL5r5e { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["l5r5e", "sheet", "army-fortification"], + template: CONFIG.l5r5e.paths.templates + "items/army-fortification/army-fortification-sheet.html", + width: 520, + height: 480, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }], + }); + } +} diff --git a/system/scripts/items/base-item-sheet.js b/system/scripts/items/base-item-sheet.js new file mode 100644 index 0000000..9243fed --- /dev/null +++ b/system/scripts/items/base-item-sheet.js @@ -0,0 +1,166 @@ +/** + * Extend the basic ItemSheet with some very simple modifications + * @extends {ItemSheet} + */ +export class BaseItemSheetL5r5e extends ItemSheet { + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["l5r5e", "sheet", "item"], + //template: CONFIG.l5r5e.paths.templates + "items/item/item-sheet.html", + width: 520, + height: 480, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }], + }); + } + + /** + * Add the SendToChat button on top of sheet + * @override + */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + + // Send To Chat + buttons.unshift({ + label: game.i18n.localize("l5r5e.global.send_to_chat"), + class: "send-to-chat", + icon: "fas fa-comment-dots", + onclick: () => + game.l5r5e.HelpersL5r5e.debounce( + "send2chat-" + this.object.id, + () => game.l5r5e.HelpersL5r5e.sendToChat(this.object), + 2000, + true + )(), + }); + + return buttons; + } + + /** + * @return {Object|Promise} + */ + async getData(options = {}) { + const sheetData = await super.getData(options); + + sheetData.data.dtypes = ["String", "Number", "Boolean"]; + + // Fix editable + sheetData.editable = this.isEditable; + sheetData.options.editable = sheetData.editable; + + return sheetData; + } + + /** + * Activate a named TinyMCE text editor + * @param {string} name The named data field which the editor modifies. + * @param {object} options TinyMCE initialization options passed to TextEditor.create + * @param {string} initialContent Initial text content for the editor area. + * @override + */ + activateEditor(name, options = {}, initialContent = "") { + if (name === "data.description" && initialContent) { + initialContent = game.l5r5e.HelpersL5r5e.convertSymbols(initialContent, false); + } + super.activateEditor(name, options, initialContent); + } + + /** + * This method is called upon form submission after form data is validated + * @param event {Event} The initial triggering submission event + * @param formData {Object} The object of validated form data with which to update the object + * @returns {Promise} A Promise which resolves once the update operation has completed + * @override + */ + async _updateObject(event, formData) { + if (formData["data.description"]) { + // Base links (Journal, compendiums...) + formData["data.description"] = TextEditor.enrichHTML(formData["data.description"]); + // L5R Symbols + formData["data.description"] = game.l5r5e.HelpersL5r5e.convertSymbols(formData["data.description"], true); + } + return super._updateObject(event, formData); + } + + /** + * Subscribe to events from the sheet. + * @param {jQuery} html HTML content of the sheet. + * @override + */ + activateListeners(html) { + super.activateListeners(html); + + // Commons + game.l5r5e.HelpersL5r5e.commonListeners(html, this.actor); + + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) { + return; + } + + // On focus on one numeric element, select all text for better experience + html.find(".select-on-focus").on("focus", (event) => { + event.preventDefault(); + event.stopPropagation(); + event.target.select(); + }); + } + + /** + * Add a embed item + * @param {Event} event + * @private + */ + _addSubItem(event) { + event.preventDefault(); + event.stopPropagation(); + const itemId = $(event.currentTarget).data("item-id"); + console.warn("L5R5E | TODO ItemSheetL5r5e._addSubItem()", itemId); // TODO _addSubItem Currently not used, title override it + } + + /** + * Add a embed item + * @param {Event} event + * @private + */ + _editSubItem(event) { + event.preventDefault(); + event.stopPropagation(); + const itemId = $(event.currentTarget).data("item-id"); + const item = this.document.items.get(itemId); + if (item) { + item.sheet.render(true); + } + } + + /** + * Delete a embed item + * @param {Event} event + * @private + */ + _deleteSubItem(event) { + event.preventDefault(); + event.stopPropagation(); + const itemId = $(event.currentTarget).data("item-id"); + const item = this.document.getEmbedItem(itemId); + if (!item) { + return; + } + + const callback = async () => { + this.document.deleteEmbedItem(itemId); + }; + + // Holing Ctrl = without confirm + if (event.ctrlKey) { + return callback(); + } + + game.l5r5e.HelpersL5r5e.confirmDeleteDialog( + game.i18n.format("l5r5e.global.delete_confirm", { name: item.name }), + callback + ); + } +} diff --git a/system/scripts/items/item-sheet.js b/system/scripts/items/item-sheet.js index 9edb579..ab1024e 100644 --- a/system/scripts/items/item-sheet.js +++ b/system/scripts/items/item-sheet.js @@ -1,8 +1,10 @@ +import { BaseItemSheetL5r5e } from "./base-item-sheet.js"; + /** - * Extend the basic ItemSheet with some very simple modifications + * Extend BaseItemSheetL5r5e with modifications for objects * @extends {ItemSheet} */ -export class ItemSheetL5r5e extends ItemSheet { +export class ItemSheetL5r5e extends BaseItemSheetL5r5e { /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -14,46 +16,17 @@ export class ItemSheetL5r5e extends ItemSheet { }); } - /** - * Add the SendToChat button on top of sheet - * @override - */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - - // Send To Chat - buttons.unshift({ - label: game.i18n.localize("l5r5e.global.send_to_chat"), - class: "send-to-chat", - icon: "fas fa-comment-dots", - onclick: () => - game.l5r5e.HelpersL5r5e.debounce( - "send2chat-" + this.object.id, - () => game.l5r5e.HelpersL5r5e.sendToChat(this.object), - 2000, - true - )(), - }); - - return buttons; - } - /** * @return {Object|Promise} */ async getData(options = {}) { const sheetData = await super.getData(options); - sheetData.data.dtypes = ["String", "Number", "Boolean"]; sheetData.data.ringsList = game.l5r5e.HelpersL5r5e.getRingsList(); // Prepare Properties (id/name => object) await this._prepareProperties(sheetData); - // Fix editable - sheetData.editable = this.isEditable; - sheetData.options.editable = sheetData.editable; - return sheetData; } @@ -79,37 +52,6 @@ export class ItemSheetL5r5e extends ItemSheet { } } - /** - * Activate a named TinyMCE text editor - * @param {string} name The named data field which the editor modifies. - * @param {object} options TinyMCE initialization options passed to TextEditor.create - * @param {string} initialContent Initial text content for the editor area. - * @override - */ - activateEditor(name, options = {}, initialContent = "") { - if (name === "data.description" && initialContent) { - initialContent = game.l5r5e.HelpersL5r5e.convertSymbols(initialContent, false); - } - super.activateEditor(name, options, initialContent); - } - - /** - * This method is called upon form submission after form data is validated - * @param event {Event} The initial triggering submission event - * @param formData {Object} The object of validated form data with which to update the object - * @returns {Promise} A Promise which resolves once the update operation has completed - * @override - */ - async _updateObject(event, formData) { - if (formData["data.description"]) { - // Base links (Journal, compendiums...) - formData["data.description"] = TextEditor.enrichHTML(formData["data.description"]); - // L5R Symbols - formData["data.description"] = game.l5r5e.HelpersL5r5e.convertSymbols(formData["data.description"], true); - } - return super._updateObject(event, formData); - } - /** * Subscribe to events from the sheet. * @param {jQuery} html HTML content of the sheet. @@ -118,21 +60,11 @@ export class ItemSheetL5r5e extends ItemSheet { activateListeners(html) { super.activateListeners(html); - // Commons - game.l5r5e.HelpersL5r5e.commonListeners(html, this.actor); - // Everything below here is only needed if the sheet is editable if (!this.isEditable) { return; } - // On focus on one numeric element, select all text for better experience - html.find(".select-on-focus").on("focus", (event) => { - event.preventDefault(); - event.stopPropagation(); - event.target.select(); - }); - // Delete a property html.find(`.property-delete`).on("click", this._deleteProperty.bind(this)); } @@ -260,60 +192,4 @@ export class ItemSheetL5r5e extends ItemSheet { callback ); } - - /** - * Add a embed item - * @param {Event} event - * @private - */ - _addSubItem(event) { - event.preventDefault(); - event.stopPropagation(); - const itemId = $(event.currentTarget).data("item-id"); - console.warn("L5R5E | TODO ItemSheetL5r5e._addSubItem()", itemId); // TODO _addSubItem Currently not used, title override it - } - - /** - * Add a embed item - * @param {Event} event - * @private - */ - _editSubItem(event) { - event.preventDefault(); - event.stopPropagation(); - const itemId = $(event.currentTarget).data("item-id"); - const item = this.document.items.get(itemId); - if (item) { - item.sheet.render(true); - } - } - - /** - * Delete a embed item - * @param {Event} event - * @private - */ - _deleteSubItem(event) { - event.preventDefault(); - event.stopPropagation(); - const itemId = $(event.currentTarget).data("item-id"); - const item = this.document.getEmbedItem(itemId); - if (!item) { - return; - } - - const callback = async () => { - this.document.deleteEmbedItem(itemId); - }; - - // Holing Ctrl = without confirm - if (event.ctrlKey) { - return callback(); - } - - game.l5r5e.HelpersL5r5e.confirmDeleteDialog( - game.i18n.format("l5r5e.global.delete_confirm", { name: item.name }), - callback - ); - } } diff --git a/system/scripts/main-l5r5e.js b/system/scripts/main-l5r5e.js index 0b9cdf6..a1e50f5 100644 --- a/system/scripts/main-l5r5e.js +++ b/system/scripts/main-l5r5e.js @@ -11,6 +11,7 @@ import HooksL5r5e from "./hooks.js"; import { ActorL5r5e } from "./actor.js"; import { CharacterSheetL5r5e } from "./actors/character-sheet.js"; import { NpcSheetL5r5e } from "./actors/npc-sheet.js"; +import { ArmySheetL5r5e } from "./actors/army-sheet.js"; // Dice and rolls import { L5rBaseDie } from "./dice/dietype/l5r-base-die.js"; import { AbilityDie } from "./dice/dietype/ability-die.js"; @@ -32,6 +33,8 @@ import { TitleSheetL5r5e } from "./items/title-sheet.js"; import { BondSheetL5r5e } from "./items/bond-sheet.js"; import { SignatureScrollSheetL5r5e } from "./items/signature-scroll-sheet.js"; import { ItemPatternSheetL5r5e } from "./items/item-pattern-sheet.js"; +import { ArmyCohortSheetL5r5e } from "./items/army-cohort-sheet.js"; +import { ArmyFortificationSheetL5r5e } from "./items/army-fortification-sheet.js"; // JournalEntry import { JournalL5r5e } from "./journal.js"; import { BaseJournalSheetL5r5e } from "./journals/base-journal-sheet.js"; @@ -106,6 +109,7 @@ Hooks.once("init", async () => { Actors.unregisterSheet("core", ActorSheet); Actors.registerSheet("l5r5e", CharacterSheetL5r5e, { types: ["character"], makeDefault: true }); Actors.registerSheet("l5r5e", NpcSheetL5r5e, { types: ["npc"], makeDefault: true }); + Actors.registerSheet("l5r5e", ArmySheetL5r5e, { types: ["army"], makeDefault: true }); // Items Items.unregisterSheet("core", ItemSheet); @@ -120,6 +124,8 @@ Hooks.once("init", async () => { Items.registerSheet("l5r5e", BondSheetL5r5e, { types: ["bond"], makeDefault: true }); Items.registerSheet("l5r5e", SignatureScrollSheetL5r5e, { types: ["signature_scroll"], makeDefault: true }); Items.registerSheet("l5r5e", ItemPatternSheetL5r5e, { types: ["item_pattern"], makeDefault: true }); + Items.registerSheet("l5r5e", ArmyCohortSheetL5r5e, { types: ["army_cohort"], makeDefault: true }); + Items.registerSheet("l5r5e", ArmyFortificationSheetL5r5e, { types: ["army_fortification"], makeDefault: true }); // Journal Items.unregisterSheet("core", JournalSheet); diff --git a/system/scripts/preloadTemplates.js b/system/scripts/preloadTemplates.js index fb02bce..7d9a4b3 100644 --- a/system/scripts/preloadTemplates.js +++ b/system/scripts/preloadTemplates.js @@ -28,6 +28,10 @@ export const PreloadTemplates = async function () { `${tpl}actors/npc/social.html`, `${tpl}actors/npc/skill.html`, `${tpl}actors/npc/techniques.html`, + // *** Actors : Army *** + `${tpl}actors/army/cohort.html`, + `${tpl}actors/army/fortification.html`, + `${tpl}actors/army/others.html`, // *** Items *** `${tpl}items/advancement/advancement-entry.html`, `${tpl}items/advancement/advancement-sheet.html`, @@ -57,5 +61,7 @@ export const PreloadTemplates = async function () { `${tpl}items/weapon/weapons.html`, `${tpl}items/weapon/weapon-entry.html`, `${tpl}items/weapon/weapon-sheet.html`, + `${tpl}items/army-cohort/army-cohort-entry.html`, + `${tpl}items/army-fortification/army-fortification-entry.html`, ]); }; diff --git a/system/system.json b/system/system.json index 6f8e205..dd074a6 100644 --- a/system/system.json +++ b/system/system.json @@ -4,8 +4,8 @@ "description": "This is an authorised multilingual game system En|Fr|Es, for Legend of the Five Rings (5th Edition) by Edge Studio

- Join the official Discord server: Official Discord

- Rejoignez la communauté Francophone: Francophone Discord

", "url": "https://gitlab.com/teaml5r/l5r5e", "manifest": "https://gitlab.com/teaml5r/l5r5e/-/raw/master/system/system.json", - "download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.3.5/raw/l5r5e.zip?job=build", - "version": "1.3.5", + "download": "https://gitlab.com/teaml5r/l5r5e/-/jobs/artifacts/v1.4.0/raw/l5r5e.zip?job=build", + "version": "1.4.0", "minimumCoreVersion": "0.8.5", "compatibleCoreVersion": "0.8.9", "manifestPlusVersion": "1.0.0", diff --git a/system/template.json b/system/template.json index bbcfcdc..5ba1041 100644 --- a/system/template.json +++ b/system/template.json @@ -1,6 +1,6 @@ { "Actor": { - "types": ["character", "npc"], + "types": ["character", "npc", "army"], "templates": { "identity": { "identity": { @@ -141,6 +141,33 @@ "social": 0, "trade": 0 } + }, + "army": { + "warlord": "", + "allies_backers": "", + "purpose_mustering": "", + "battle_readiness": { + "casualties_strength": { + "max": 0, + "value": 0 + }, + "panic_discipline": { + "max": 0, + "value": 0 + } + }, + "commander": "", + "commander_abilities": "", + "army_abilities": "", + "commander_standing": { + "honor": 0, + "glory": 0, + "status": 0 + }, + "supplies_logistics": "", + "notes": "", + "description": "", + "past_battles": "" } }, "Item": { @@ -155,7 +182,9 @@ "title", "bond", "signature_scroll", - "item_pattern" + "item_pattern", + "army_cohort", + "army_fortification" ], "templates": { "basics": { @@ -236,6 +265,28 @@ }, "signature_scroll": { "templates": ["basics", "advancement"] + }, + "army_cohort": { + "templates": ["basics"], + "leader": "", + "equipment": "", + "abilities": "", + "battle_readiness": { + "casualties_strength": { + "max": 0, + "value": 0 + }, + "panic_discipline": { + "max": 0, + "value": 0 + } + } + }, + "army_fortification": { + "templates": ["basics"], + "difficulty": "", + "attrition_reduction": "", + "notes": "" } } } diff --git a/system/templates/actors/army-sheet.html b/system/templates/actors/army-sheet.html new file mode 100644 index 0000000..5d59b54 --- /dev/null +++ b/system/templates/actors/army-sheet.html @@ -0,0 +1,99 @@ +
+ {{!-- Sheet Header --}} +
+ +
+

+
+
+
+

+ {{localize 'l5r5e.army.warlord'}} + +

+

+ {{localize 'l5r5e.army.allies_backers'}} + +

+

+ {{localize 'l5r5e.army.purpose_mustering'}} + +

+ +
+ {{localize 'l5r5e.army.battle_readiness.title'}} +

+ {{localize 'l5r5e.army.battle_readiness.strength'}} + +

+

+ {{localize 'l5r5e.army.battle_readiness.casualties'}} + +

+

+ {{localize 'l5r5e.army.battle_readiness.discipline'}} + +

+

+ {{localize 'l5r5e.army.battle_readiness.panic'}} + +

+
+
+
+

+ {{localize 'l5r5e.army.commander'}} + +

+

+ {{localize 'l5r5e.army.commander_abilities'}} + +

+

+ {{localize 'l5r5e.army.army_abilities'}} + +

+ +
+ {{localize 'l5r5e.army.commander_standing'}} +

+ {{localize 'l5r5e.social.honor'}} + +

+

+ {{localize 'l5r5e.social.glory'}} + +

+

+ {{localize 'l5r5e.social.status'}} + +

+
+
+
+
+ {{!-- Sheet Body --}} +
+ {{!-- Sheet Tab Navigation --}} + + + {{!-- Cohort Tab --}} +
+ {{> 'systems/l5r5e/templates/actors/army/cohort.html'}} +
+ + {{!-- Fortification Tab --}} +
+ {{> 'systems/l5r5e/templates/actors/army/fortification.html'}} +
+ + {{!-- Others Tab --}} +
+ {{> 'systems/l5r5e/templates/actors/army/others.html'}} +
+
+
\ No newline at end of file diff --git a/system/templates/actors/army/cohort.html b/system/templates/actors/army/cohort.html new file mode 100644 index 0000000..92501db --- /dev/null +++ b/system/templates/actors/army/cohort.html @@ -0,0 +1,13 @@ +
+ + {{localize 'l5r5e.army.cohort.title'}} + {{#if options.editable}} + + {{/if}} + +
    + {{#each data.splitItemsList.army_cohort as |item|}} + {{> 'systems/l5r5e/templates/items/army-cohort/army-cohort-entry.html' cohort=item editable=../options.editable}} + {{/each}} +
+
\ No newline at end of file diff --git a/system/templates/actors/army/fortification.html b/system/templates/actors/army/fortification.html new file mode 100644 index 0000000..9a3e033 --- /dev/null +++ b/system/templates/actors/army/fortification.html @@ -0,0 +1,13 @@ +
+ + {{localize 'l5r5e.army.fortification.title'}} + {{#if options.editable}} + + {{/if}} + +
    + {{#each data.splitItemsList.army_fortification as |item|}} + {{> 'systems/l5r5e/templates/items/army-fortification/army-fortification-entry.html' fortification=item editable=../options.editable}} + {{/each}} +
+
\ No newline at end of file diff --git a/system/templates/actors/army/others.html b/system/templates/actors/army/others.html new file mode 100644 index 0000000..5c24d56 --- /dev/null +++ b/system/templates/actors/army/others.html @@ -0,0 +1,25 @@ +
+ {{!-- Supplies and Logistics --}} +
+ {{localize 'l5r5e.army.supplies_logistics'}} + {{editor content=data.data.supplies_logistics target="data.supplies_logistics" button=true editable=options.editable}} +
+ + {{!-- Past Battles --}} +
+ {{localize 'l5r5e.army.past_battles'}} + {{editor content=data.data.past_battles target="data.past_battles" button=true editable=options.editable}} +
+ + {{!-- Description (public) --}} +
+ {{localize 'l5r5e.description'}} + {{editor content=data.data.description target="data.description" button=true editable=options.editable}} +
+ + {{!-- Notes (private) --}} +
+ {{localize 'l5r5e.notes'}} + {{editor content=data.data.notes target="data.notes" button=true editable=options.editable}} +
+
\ No newline at end of file diff --git a/system/templates/items/army-cohort/army-cohort-entry.html b/system/templates/items/army-cohort/army-cohort-entry.html new file mode 100644 index 0000000..f81f452 --- /dev/null +++ b/system/templates/items/army-cohort/army-cohort-entry.html @@ -0,0 +1,22 @@ +
  • +
      +
    • +
    • {{cohort.name}}
    • +
    • + {{cohort.data.battle_readiness.casualties_strength.value}} + {{cohort.data.battle_readiness.casualties_strength.max}} + {{cohort.data.battle_readiness.panic_discipline.value}} + {{cohort.data.battle_readiness.panic_discipline.max}} +
    • + {{#if editable}} +
    • +
    • + {{/if}} +
    +
      +
    • {{localize 'l5r5e.army.cohort.leader'}} : {{cohort.data.leader}}
    • +
    • {{localize 'l5r5e.equipment'}} : {{cohort.data.equipment}}
    • +
    • {{localize 'l5r5e.army.cohort.abilities'}} : {{cohort.data.abilities}}
    • +
    • {{localize 'l5r5e.description'}} : {{cohort.data.description}}
    • +
    +
  • \ No newline at end of file diff --git a/system/templates/items/army-cohort/army-cohort-sheet.html b/system/templates/items/army-cohort/army-cohort-sheet.html new file mode 100644 index 0000000..c1ef502 --- /dev/null +++ b/system/templates/items/army-cohort/army-cohort-sheet.html @@ -0,0 +1,43 @@ +
    +
    + +

    +
    + {{!-- Sheet Body --}} +
    + {{!-- properties Tab --}} +
    + + + + + {{!-- battle readiness --}} + + + + +
    + {{> 'systems/l5r5e/templates/items/item/item-infos.html'}} +
    +
    diff --git a/system/templates/items/army-fortification/army-fortification-entry.html b/system/templates/items/army-fortification/army-fortification-entry.html new file mode 100644 index 0000000..6a849bc --- /dev/null +++ b/system/templates/items/army-fortification/army-fortification-entry.html @@ -0,0 +1,17 @@ +
  • +
      +
    • +
    • {{fortification.name}}
    • +
    • + {{fortification.data.difficulty}} + {{fortification.data.attrition_reduction}} +
    • + {{#if editable}} +
    • +
    • + {{/if}} +
    +
      +
    • {{localize 'l5r5e.description'}} : {{fortification.data.description}}
    • +
    +
  • \ No newline at end of file diff --git a/system/templates/items/army-fortification/army-fortification-sheet.html b/system/templates/items/army-fortification/army-fortification-sheet.html new file mode 100644 index 0000000..4fb00f9 --- /dev/null +++ b/system/templates/items/army-fortification/army-fortification-sheet.html @@ -0,0 +1,22 @@ +
    +
    + +

    +
    + {{!-- Sheet Body --}} +
    + {{!-- properties Tab --}} +
    + + + +
    + {{> 'systems/l5r5e/templates/items/item/item-infos.html'}} +
    +