From 1bd418883f1c81055fa7b52c4b98f282e2c1aa67 Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Thu, 29 Aug 2024 22:14:34 -0300 Subject: [PATCH 1/8] feat!: use foundry items for equipments. Changes: * Add option to import GCA/GCS inventory as Foundry Item. Closes #1966. * Add option to show debug information for Document types in dialog. Closes #1965. * Fix bug in Smart Importer when Actor is not mapped to his exported file. Closes #1964 --- lang/de.json | 17 +- lang/en.json | 20 +++ lang/fr.json | 19 ++- lang/pt_br.json | 20 +++ lang/ru.json | 13 +- lib/miscellaneous-settings.js | 24 +++ lib/utilities.js | 16 ++ module/actor/actor-components.js | 106 +++++++++++- module/actor/actor-importer.js | 281 ++++++++++++++++++++++++------- module/actor/actor-sheet.js | 78 +++++++-- module/actor/actor.js | 127 ++++++++++++-- module/damage/applydamage.js | 2 +- module/global-references.js | 2 + module/gurps-wiring.js | 2 +- module/gurps.js | 4 + module/item-sheet.js | 49 +++--- module/item.js | 22 ++- module/smart-importer.js | 24 ++- styles/apps.css | 13 ++ template.json | 4 +- templates/import-gcs-or-gca.hbs | 14 +- utils/debugger.js | 72 ++++++++ 22 files changed, 798 insertions(+), 131 deletions(-) create mode 100644 utils/debugger.js diff --git a/lang/de.json b/lang/de.json index d7e685525..0ee8045ad 100644 --- a/lang/de.json +++ b/lang/de.json @@ -616,6 +616,14 @@ "GURPS.importTooManyContainers": "Es gibt zu viele Ebenen von Containern. Der Foundry-Import unterstützt nur bis zu 3 Ebenen von Unterbehältern.", "GURPS.importSuccessful": "{name} erfolgreich importiert.", "GURPS.importSeeUsersGuide": "Im Benutzerhandbuch befinden sich Informationen darüber, wo die neueste Version zu erhalten ist.", + "GURPS.importSheetTitle": "Importiere {generator} Blatt", + "GURPS.importSheetHint": "Importiere Daten für {name} mit {generator}. Bitte warten...", + "GURPS.importSelectFileTitle": "Wähle eine Datei, die aus GCS oder GCA exportiert wurde.", + "GURPS.importSelectFileSource": "Quelldaten", + "GURPS.importSelectFileDescribeAction": "Dieser Import wird:", + "GURPS.importSelectFileOverwriteAction": "Die Daten für: {name} überschreiben.", + "GURPS.importSelectFileItemAction": "Ausrüstung importieren als: {equipType}.", + "GURPS.importSelectFileNote": "HINWEIS: Dies kann nicht rückgängig gemacht werden.", "__System Settings__": "=========", "GURPS.settingShowReadMe": "Bei Versionswechsel 'Liesmich' anzeigen", "GURPS.settingHintShowReadMe": "Wenn diese Option aktiviert ist, zeigt das System bei jeder Versionsänderung die Datei 'Liesmich' an.", @@ -710,7 +718,14 @@ "GURPS.settingHintRemoveUnequipped": "Wenn diese Option aktiviert ist, werden die Namen der Nahkampf- und Fernkampfangriffe mit der Liste der getragenen Ausrüstung verglichen, und wenn eine Namensübereinstimmung gefunden wird, wird der Angriff nur aufgeführt, wenn die Ausrüstung ausgerüstet ist", "GURPS.settingImportBrowserImporter": "Nicht lokal gehosteten Importdialog verwenden", "GURPS.settingImportHintBrowserImporter": "Diese Option aktivieren, wenn die Foundry-Instanz nicht lokal gehosted wird (Z.B. über The Forge). Dieser Importdialog kann sich den Speicherort der Importdatei während der Sitzung merken (d. h., wenn die Figur in derselben Sitzung erneut importiert wird, muss der Dateidialog nicht aufgerufen werden).", - + "GURPS.settingUseFoundryItems": "Ausrüstung: Verwende Foundry Items", + "GURPS.settingHintUseFoundryItems": "Wenn diese Option aktiviert ist, wird das System Foundry Items für Ausrüstung erstellen, wenn Charakterbögen importiert werden. Wenn du den Charakterbogen von GCA/GCS zum ersten Mal importierst, wird das System versuchen, jedes Element zu erstellen und dabei die aktuelle Ausrüstungsinformationen auf dem Charakterbogen zu erhalten, einschließlich Name, Bild, Anzahl, Verwendungen und Notizen. Dies kann eine sehr lange Operation sein, insbesondere für GCS-Blätter.", + "GURPS.settingNoEditAllowed": "Ausrüstung bearbeiten nicht erlaubt", + "GURPS.settingNoEquipAllowedHint": "Warnung: Du versuchst, eine Ausrüstung zu bearbeiten, die nicht mit einem Foundry-Item verknüpft ist, wenn die Einstellung 'Verwende Foundry-Items für Ausrüstung' aktiv ist. Bitte importiere den Charakterbogen erneut, um das Foundry-Item für diesen Akteur zu erstellen.", + "GURPS.settingNoItemAllowedHint": "Warnung: Du versuchst, ein importiertes Item zu bearbeiten, wenn die Einstellung 'Verwende Foundry-Items für Ausrüstung' deaktiviert ist. Bitte importiere den Charakterbogen erneut, um das Equipment für diesen Akteur zu erstellen.", + "GURPS.settingShowDebugInfo": "Dokument Debug Info anzeigen", + "GURPS.settingHintShowDebugInfo": "Für Dokumentdialogs (Akteure, Gegenstände, etc.) wird das Debug-Symbol im Fenstertitel angezeigt. Wenn es gedrückt wird, wird die Dokumentdaten in einem Dialog angezeigt.", + "GURPS.settingShowDebugTooltip": "Zeige Debug-Informationen", "GURPS.adDisad": "Vorteil/Nachteil", "GURPS.adDisadQuirkPerk": "Vorteil/Nachteil/Marotte/Minivorteil", "GURPS.conditionalModifier": "Situativer Modifikator", diff --git a/lang/en.json b/lang/en.json index 72cfaad85..49057de6c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -30,6 +30,8 @@ "GURPS.descriptionReligion": "Religion", "GURPS.descriptionSizeModifier": "Size Modifier (SM)", "GURPS.descriptionSkin": "Skin", + "GURPS.SM": "SM", + "GURPS.TL": "TL", "GURPS.descriptionTechLevel": "Tech Level (TL)", "GURPS.weight": "Weight", "GURPS.share": "Share", @@ -774,6 +776,16 @@ "GURPS.importSeeUsersGuide": "Check the Users Guide for details on where to get the latest version.", "GURPS.importOldGCSFile": "Your character was saved with an older version of GCS, which does not output some required attributes. Update GCS to at least version 4.36, open and save your character file, then try again.", "GURPS.importNoJSONDetected": "Cannot parse JSON. Your GCS file seems to be corrupted.", + "GURPS.importSheetTitle": "Importing {generator} Sheet", + "GURPS.importSheetHint": "Importing data for {name} using {generator}. Please wait...", + "GURPS.importTraitToFoundryItem": "Foundry Item", + "GURPS.importTraitToClassicData": "Classic Data", + "GURPS.importSelectFileTitle": "Select a file exported from GCS or GCA.", + "GURPS.importSelectFileSource": "Source Data", + "GURPS.importSelectFileDescribeAction": "This import will:", + "GURPS.importSelectFileOverwriteAction": "Overwrite the data for: {name}.", + "GURPS.importSelectFileItemAction": "Import Equipment as: {equipType}.", + "GURPS.importSelectFileNote": "NOTE: This cannot be un-done.", "__System Settings__": "=========", "GURPS.settingShowReadMe": "Show 'Read Me' on version change", "GURPS.settingHintShowReadMe": "If checked, the system will display the 'Read Me' file every time a version change is detected.", @@ -878,6 +890,14 @@ "GURPS.settingApplyBasedOnTarget": "'Target'", "GURPS.settingTokenOverrideRefresh": "Override Token scaling", "GURPS.settingHintTokenOverrideRefresh": "If \"on\", try to draw tokens to properly fit the hex grid. Overrides Foundry drawing functionality -- turn this off if there's any odd Foundry drawing behavior. Requires reloading the world.", + "GURPS.settingUseFoundryItems": "Use Foundry Items for Equipment", + "GURPS.settingHintUseFoundryItems": "If checked, the system will create Foundry Items for actor equipments when importing character sheets. When you import the character sheet from GCA/GCS for the first time, the system will create each item trying to preserve current equipment info on actor sheet, including name, image, count, uses and notes. This can be a very long operation, especially for GCS sheets.", + "GURPS.settingNoEditAllowed": "No Equipment Editing Allowed", + "GURPS.settingNoEquipAllowedHint": "Warning: You're trying to edit an equipment that is not linked to a Foundry item when settings 'Use Foundry Items for Equipment' is active. Please reimport the character sheet to recreate the Foundry item for this actor.", + "GURPS.settingNoItemAllowedHint": "Warning: You're trying to edit an imported Item when settings 'Use Foundry Items for Equipment' is disabled. Please reimport the character sheet to recreate the Equipment for this actor.", + "GURPS.settingShowDebugInfo": "Show Document Debug Info", + "GURPS.settingHintShowDebugInfo": "For Document dialogs (Actors, Items, etc.), show the debug icon on the window title. When pressed it will display the document data in a dialog.", + "GURPS.settingShowDebugTooltip": "Show debug information", "__Color Settings__": "=========", "GURPS.settingColorSheetMenuTitle": "Color Character Sheet", "GURPS.settingColorSheetMenuHint": "Color Character Sheet Settings", diff --git a/lang/fr.json b/lang/fr.json index 65107240e..8741f2672 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -839,7 +839,14 @@ "GURPS.settingHintRemoveUnequipped": "Si coché, les noms des attaques de Mêlée et à Distance seront comparés à la liste des équipements portés, et si une correspondance est trouvée, l'attaque sera seulement listée si l'équipement est équipé", "GURPS.settingImportBrowserImporter": "Utiliser la fenêtre d'import pour hôte distant", "GURPS.settingImportHintBrowserImporter": "Cocher ceci si vous n'hébergez pas votre instance de Foundry localement (vous hébergez à distance, par ex. Forge). Cette fenêtre d'import peut mémoriser l'emplacement de vos fichiers d'import pendant la session (cela signifie que si vous importez de nouveau le personnage dans la même session, la fenêtre ne se rouvrira pas).", - + "GURPS.settingUseFoundryItems": "Utiliser les Objets Foundry pour l'Equipement", + "GURPS.settingHintUseFoundryItems": "Si coché, le système créera des Objets Foundry pour l'équipement de l'acteur lors de l'importation des feuilles de personnage. Lorsque vous importez la feuille de personnage de GCA/GCS pour la première fois, le système créera chaque objet en essayant de préserver les informations d'équipement actuelles sur la feuille de personnage, y compris le nom, l'image, le compte, les utilisations et les notes. Cela peut être une opération très longue, surtout pour les feuilles GCS.", + "GURPS.settingNoEditAllowed": "Pas d'édition d'équipement autorisée", + "GURPS.settingNoEquipAllowedHint": "Attention: Vous essayez d'éditer un équipement qui n'est pas lié à un objet Foundry lorsque les paramètres 'Utiliser les Objets Foundry pour l'Equipement' sont actifs. Veuillez réimporter la feuille de personnage pour créer l'objet Foundry pour cet acteur.", + "GURPS.settingNoItemAllowedHint": "Attention: Vous essayez d'éditer un équipement importé lorsque les paramètres 'Utiliser les Objets Foundry pour l'Equipement' sont désactivés. Veuillez réimporter la feuille de personnage pour recréer l'équipement pour cet acteur.", + "GURPS.settingShowDebugInfo": "Montrer les infos de débogage", + "GURPS.settingHintShowDebugInfo": "Pour les dialogues de document (Acteurs, Objets, etc.), montre l'icône de débogage sur le titre de la fenêtre. Quand pressé, cela affichera les données du document dans un dialogue.", + "GURPS.settingShowDebugTooltip": "Montrer les infos de débogage", "__Color Settings__": "=========", "GURPS.settingColorSheetMenuTitle": "Coloration Feuille Personnage", "GURPS.settingColorSheetMenuHint": "Paramètres Coloration Feuille Personnage", @@ -961,6 +968,16 @@ "GURPS.itemEditor": "Editeurs Objets", "GURPS.itemFeatures": "Caractéristiques", "GURPS.itemImport": "Import Bibliothèque d'Equipement", + "GURPS.importSheetTitle": "Importation de la Feuille {generator}", + "GURPS.importSheetHint": "Importation des données pour {name} en utilisant {generator}. Veuillez patienter...", + "GURPS.importTraitToFoundryItem": "Objet Foundry", + "GURPS.importTraitToClassicData": "Données Classiques", + "GURPS.importSelectFileTitle": "Sélectionnez un fichier exporté depuis GCS ou GCA.", + "GURPS.importSelectFileSource": "Données Source", + "GURPS.importSelectFileDescribeAction": "Cet import va:", + "GURPS.importSelectFileOverwriteAction": "Ecraser les données pour: {name}.", + "GURPS.importSelectFileItemAction": "Importer l'Equipement comme: {equipType}.", + "GURPS.importSelectFileNote": "NOTE: Cela ne peut pas être annulé.", "GURPS.knockback": "Renversement, {amount} {unit}", "GURPS.knockbackCheck": "{name}: jet {dx}, {acrobatics}, ou {judo} ou chute!", "GURPS.level": "Niveau", diff --git a/lang/pt_br.json b/lang/pt_br.json index 7213486a7..096928bcd 100644 --- a/lang/pt_br.json +++ b/lang/pt_br.json @@ -31,6 +31,8 @@ "GURPS.descriptionReligion": "Religião", "GURPS.descriptionSizeModifier": "Mod. de Tamanho (MT)", "GURPS.descriptionSkin": "Pele", + "GURPS.SM": "MD", + "GURPS.TL": "NT", "GURPS.descriptionTechLevel": "Nível Tecnológico (NT)", "GURPS.weight": "Peso", "GURPS.share": "Compartilhar", @@ -744,6 +746,16 @@ "GURPS.importSeeUsersGuide": "Verifique o Manual do Usuário para detalhes de onde conseguir a última versão.", "GURPS.importOldGCSFile": "Seu personagem foi salvo com uma versão mais antiga do GCS, que não exporta alguns dos atributos necessários. Atualize o GCS para no mínimo a versão 4.36, abra e salve o arquivo de personagem e então tente novamente.", "GURPS.importNoJSONDetected": "Incapaz de analisar JSON. Seu arquivo do GCS aparenta estar corrompido.", + "GURPS.importSheetTitle": "Importando Planilha {generator}", + "GURPS.importSheetHint": "Importando dados para {name} usando {generator}. Por favor, aguarde...", + "GURPS.importTraitToFoundryItem": "Item do Foundry", + "GURPS.importTraitToClassicData": "Dados Clássicos", + "GURPS.importSelectFileTitle": "Selecione um arquivo exportado do GCS ou GCA.", + "GURPS.importSelectFileSource": "Dados de Origem", + "GURPS.importSelectFileDescribeAction": "Esta importação irá:", + "GURPS.importSelectFileOverwriteAction": "Sobrescrever os dados de: {name}.", + "GURPS.importSelectFileItemAction": "Importar Equipamento como: {equipType}.", + "GURPS.importSelectFileNote": "NOTA: Esta ação não pode ser desfeita.", "__System Settings__": "=========", "GURPS.settingShowReadMe": "Exibir 'Leia-Me' quando houver atualização", "GURPS.settingHintShowReadMe": "Se marcado, o sistema vai exibir o arquivo 'Leia-Me' sempre que uma mudança de versão for detectada.", @@ -848,6 +860,14 @@ "GURPS.settingApplyBasedOnTarget": "'Alvo'", "GURPS.settingTokenOverrideRefresh": "Desconsiderar dimensionamento de miniatura", "GURPS.settingHintTokenOverrideRefresh": "Se estiver \"ativado\", tentará desenhar as miniaturas para que se encaixem adequadamente na grade hexagonal. Ignora as funções de desenho do Foundry -- desative esta opção se houver qualquer comportamento inadequado nos desenhos do Foundry. Será necessário recarregar o mundo.", + "GURPS.settingUseFoundryItems": "Usar Itens do Foundry para Equipamentos", + "GURPS.settingHintUseFoundryItems": "Se marcado, o sistema criará Itens do Foundry para os equipamentos do ator durante a importação de planilhas de personagem. Quando você importar a planilha de personagem do GCA/GCS pela primeira vez, o sistema criará cada item tentando preservar as informações de equipamento atuais na planilha do ator, incluindo nome, imagem, contagem, usos e notas. Isto pode ser uma operação muito longa, especialmente para planilhas GCS.", + "GURPS.settingNoEditAllowed": "Edição de Equipamento Não Permitida", + "GURPS.settingNoEquipAllowedHint": "Aviso: Você está tentando editar um equipamento que não está vinculado a um item do Foundry quando a configuração 'Usar Itens do Foundry para Equipamentos' está ativa. Por favor, reimporte a planilha de personagem para criar o item do Foundry para este ator.", + "GURPS.settingNoItemAllowedHint": "Aviso: Você está tentando editar um Item importado quando a configuração 'Usar Itens do Foundry para Equipamentos' está desativada. Por favor, reimporte a planilha de personagem para recriar o Equipamento para este ator.", + "GURPS.settingShowDebugInfo": "Exibir Informações de Depuração", + "GURPS.settingHintShowDebugInfo": "Para janelas de Documento (Atores, Itens, etc.), exibir o ícone de depuração no título da janela. Quando pressionado, ele exibirá os dados do documento em uma janela de diálogo.", + "GURPS.settingShowDebugTooltip": "Exibir informações de depuração", "__Color Settings__": "=========", "GURPS.settingColorSheetMenuTitle": "Cor da Planilha de Personagem", "GURPS.settingColorSheetMenuHint": "Configuração de Cor da Planilha de Personagem", diff --git a/lang/ru.json b/lang/ru.json index 3a4907497..0bd990111 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -27,6 +27,7 @@ "GURPS.descriptionReligion": "Религия", "GURPS.descriptionHeight": "Рост", "GURPS.descriptionSizeModifier": "Модификатор размера (МР)", + "GURPS.TL": "ТУ", "GURPS.descriptionTechLevel": "Технологический уровень (ТУ)", "GURPS.descriptionBodyPlan": "Форма тела", "GURPS.descriptionHair": "Причёска", @@ -434,7 +435,17 @@ "GURPS.equipmentTab": "Снаряжение", "GURPS.itemImport": "Импортой Сбор Оборудования", + "GURPS.importSheetTitle": "Импорт листа {generator}", + "GURPS.importSheetHint": "Импорт данных для {name} с использованием {generator}. Пожалуйста, подождите...", "__System Settings__": "=========", - "GURPS.settingDamageLocationTorso": "Торс" + "GURPS.settingDamageLocationTorso": "Торс", + "GURPS.settingUseFoundryItems": "Использовать Foundry Items для снаряжения", + "GURPS.settingHintUseFoundryItems": "Если установлено, система будет создавать Foundry Items для снаряжения персонажа при импорте листов персонажей. При импорте листа персонажа из GCA/GCS в первый раз система будет создавать каждый предмет, пытаясь сохранить текущую информацию о снаряжении на листе персонажа, включая имя, изображение, количество, использование и заметки. Это может быть очень долгой операцией, особенно для листов GCS.", + "GURPS.settingNoEditAllowed": "Редактирование снаряжения запрещено", + "GURPS.settingNoEquipAllowedHint": "Предупреждение: Вы пытаетесь отредактировать снаряжение, которое не связано с Foundry item, когда активированы настройки 'Использовать Foundry Items для снаряжения'. Пожалуйста, перимпортируйте лист персонажа, чтобы создать Foundry item для этого актёра.", + "GURPS.settingNoItemAllowedHint": "Предупреждение: Вы пытаетесь отредактировать импортированный предмет, когда активированы настройки 'Использовать Foundry Items для снаряжения'. Пожалуйста, перимпортируйте лист персонажа, чтобы создать снаряжение для этого актёра.", + "GURPS.settingShowDebugInfo": "Показать отладочную информацию", + "GURPS.settingHintShowDebugInfo": "Для диалогов документов (Актёры, Предметы и т.д.) показывать значок отладки в заголовке окна. При нажатии он отобразит данные документа в диалоге.", + "GURPS.settingShowDebugTooltip": "Показать отладочную информацию" } diff --git a/lib/miscellaneous-settings.js b/lib/miscellaneous-settings.js index def644611..d86c500f0 100755 --- a/lib/miscellaneous-settings.js +++ b/lib/miscellaneous-settings.js @@ -65,6 +65,8 @@ export const SETTING_PORTRAIT_PATH = 'portrait-path' export const SETTING_OVERWRITE_PORTRAITS = 'overwrite-portraitsk' export const SETTING_CTRL_KEY = 'ctrl-key' export const SETTING_USE_ON_TARGET = 'use-on-target' +export const SETTING_USE_FOUNDRY_ITEMS = 'use-foundry-items' +export const SETTING_SHOW_DEBUG_INFO = 'show-debug-info' export const VERSION_096 = SemanticVersion.fromString('0.9.6') export const VERSION_097 = SemanticVersion.fromString('0.9.7') @@ -75,6 +77,18 @@ export function initializeSettings() { Hooks.once('init', async function () { // Game Aid Information Settings ---- + // Show Debug Information for Documents + game.settings.register(SYSTEM_NAME, SETTING_SHOW_DEBUG_INFO, { + name: i18n('GURPS.settingShowDebugInfo'), + hint: i18n('GURPS.settingHintShowDebugInfo'), + scope: 'client', + config: true, + type: Boolean, + default: false, + requiresReload: true, + onChange: value => console.log(`Show Debug Info for Documents: ${value}`), + }) + // Keep track of the last version number game.settings.register(SYSTEM_NAME, SETTING_CHANGELOG_VERSION, { name: 'Changelog Version', @@ -155,6 +169,16 @@ export function initializeSettings() { // GCS/GCA Import Configuration ---- + game.settings.register(SYSTEM_NAME, SETTING_USE_FOUNDRY_ITEMS, { + name: i18n('GURPS.settingUseFoundryItems'), + hint: i18n('GURPS.settingHintUseFoundryItems'), + scope: 'world', + config: true, + type: Boolean, + default: false, + onChange: value => console.log(`Using Foundry Items for Equipment : ${value}`), + }) + game.settings.register(SYSTEM_NAME, SETTING_IGNORE_IMPORT_NAME, { name: i18n('GURPS.settingImportIgnoreName'), hint: i18n('GURPS.settingHintImportIgnoreName'), diff --git a/lib/utilities.js b/lib/utilities.js index 28e3955c2..feb3f5600 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -272,6 +272,22 @@ export function recurselist(list, fn, parentkey = '', depth = 0) { } } +/** + * @param {Object} list + * @param {Promise} pm + * @param {string} parentkey + * @param {number} depth + */ +export async function aRecurselist(list, pm, parentkey = '', depth = 0) { + if (!!list) + for (const [key, value] of Object.entries(list)) { + if ((await pm(value, parentkey + key, depth)) !== false) { + await aRecurselist(value.contains, pm, parentkey + key + '.contains.', depth + 1) + await aRecurselist(value.collapsed, pm, parentkey + key + '.collapsed.', depth + 1) + } + } +} + export function generateUniqueId() { return foundry.utils.randomID() } diff --git a/module/actor/actor-components.js b/module/actor/actor-components.js index f151737a2..050500140 100644 --- a/module/actor/actor-components.js +++ b/module/actor/actor-components.js @@ -7,6 +7,7 @@ */ import { convertRollStringToArrayOfInt, extractP } from '../../lib/utilities.js' +import * as Settings from '../../lib/miscellaneous-settings.js' export class _Base { constructor() { @@ -302,13 +303,14 @@ export class Equipment extends Named { this.collapsed = {} /** @type {{ [key: string]: any }} */ this.contains = {} + this.itemInfo = {} } /** * @param {Equipment} eqt */ - static calc(eqt) { - Equipment.calcUpdate(null, eqt, '') + static async calc(eqt) { + await Equipment.calcUpdate(null, eqt, '') } // OMG, do NOT fuck around with this method. So many gotchas... @@ -360,6 +362,106 @@ export class Equipment extends Named { eqt.costsum = cs eqt.weightsum = ws } + + /** + * Create new GURPSItem payload using Equipment's data + */ + toItemData() { + const timestamp = new Date() + const system = this.itemInfo?.system || {} + const importId = !this.save ? this.uuid : '' + const importFrom = this.importFrom || !!importId ? (importId.startsWith('k') ? 'GCA' : 'GCS') : '' + return { + name: this.name, + img: this.itemInfo?.img || this.findDefaultImage(), + type: 'equipment', + system: { + eqt: { + name: this.name, + notes: this.notes, + pageref: this.pageref, + count: this.count, + cost: this.cost, + weight: this.weight, + carried: this.carried, + equipped: this.equipped, + techlevel: this.techlevel, + categories: this.categories, + legalityclass: this.legalityclass, + costsum: this.costsum, + uses: this.uses, + maxuses: this.maxuses, + last_import: timestamp, + uuid: this.uuid || this._getGGAId(), + location: this.location, + parentuuid: this.parentuuid, + }, + ads: system.ads || {}, + skills: system.skills || {}, + spells: system.spells || {}, + melee: system.melee || {}, + ranged: system.ranged || {}, + bonuses: system.bonuses || '', + equipped: this.equipped, + carried: this.carried, + globalid: this.globalid || '', + importid: importId, + importFrom: importFrom, + }, + } + } + + /** + * For now, just return the first image found + * In the future, we can implement a better way to find the best image + * + * @return string + * @private + * @param name + */ + findDefaultImage() { + return 'icons/svg/item-bag.svg' + } + + _getGGAId() { + return `GGA${foundry.utils.randomID(13)}` + } + + static fromObject(data, actor) { + let equip + if (data instanceof Equipment) { + equip = data + } else { + equip = new Equipment(data.name, data.save) + equip.count = data.count + equip.cost = data.cost + equip.weight = data.weight + equip.carried = data.carried + equip.equipped = data.equipped + equip.techlevel = data.techlevel + equip.categories = data.categories + equip.legalityclass = data.legalityclass + equip.costsum = data.costsum + equip.uses = data.uses + equip.maxuses = data.maxuses + equip.uuid = data.uuid + equip.parentuuid = data.parentuuid + equip.location = data.location + equip.notes = data.notes + equip.pageref = data.pageref + equip.ignoreImportQty = data.ignoreImportQty + } + // This equipment already exists in Actor? + const existingEquipmentKey = actor._findEqtkeyForId('uuid', equip.uuid) + if (!!existingEquipmentKey) { + const existingEquipment = foundry.utils.getProperty(actor, existingEquipmentKey) + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + equip.itemid = existingEquipment.itemid || '' + } + equip.itemInfo = existingEquipment.itemInfo || {} + } + return equip + } } export class Reaction { diff --git a/module/actor/actor-importer.js b/module/actor/actor-importer.js index 296ccb1e8..35c1656bf 100644 --- a/module/actor/actor-importer.js +++ b/module/actor/actor-importer.js @@ -1,10 +1,9 @@ -import { xmlTextToJson, recurselist, i18n, i18n_f, arrayBuffertoBase64 } from '../../lib/utilities.js' +import { xmlTextToJson, recurselist, i18n, i18n_f, arrayBuffertoBase64, aRecurselist } from '../../lib/utilities.js' import * as HitLocations from '../hitlocation/hitlocation.js' import * as settings from '../../lib/miscellaneous-settings.js' import { SmartImporter } from '../smart-importer.js' import { parseDecimalNumber } from '../../lib/parse-decimal-number/parse-decimal-number.js' import { - _Base, Skill, Spell, Advantage, @@ -17,6 +16,7 @@ import { Melee, Language, } from './actor-components.js' +import * as Settings from '../../lib/miscellaneous-settings.js' // const GCA5Version = 'GCA5-14' const GCAVersion = 'GCA-11' @@ -47,21 +47,21 @@ export class ActorImporter { } }) xhr.send(null) - } else this._openImportDialog() - } else this._openImportDialog() + } else await this._openImportDialog() + } else await this._openImportDialog() } async _openImportDialog() { if (game.settings.get(settings.SYSTEM_NAME, settings.SETTING_USE_BROWSER_IMPORTER)) - this._openNonLocallyHostedImportDialog() - else this._openLocallyHostedImportDialog() + await this._openNonLocallyHostedImportDialog() + else await this._openLocallyHostedImportDialog() } async _openNonLocallyHostedImportDialog() { try { - const file = await SmartImporter.getFileForActor(this) - const res = await this.importActorFromExternalProgram(await file.text(), file.name) - if (res) SmartImporter.setFileForActor(this, file) + const file = await SmartImporter.getFileForActor(this.actor) + const res = await this.importActorFromExternalProgram(await file.text(), file.name, file.path) + if (res) SmartImporter.setFileForActor(this.actor, file) } catch (e) { ui.notifications?.error(e) throw e @@ -80,7 +80,7 @@ export class ActorImporter { import: { icon: '', label: 'Import', - callback: html => { + callback: async html => { const form = html.find('form')[0] let files = form.data.files let file = null @@ -88,9 +88,8 @@ export class ActorImporter { return ui.notifications.error('You did not upload a data file!') } else { file = files[0] - GURPS.readTextFromFile(file).then(text => - this.importActorFromExternalProgram(text, file.name, file.path) - ) + const text = await GURPS.readTextFromFile(file) + await this.importActorFromExternalProgram(text, file.name, file.path) } }, }, @@ -122,6 +121,8 @@ export class ActorImporter { let r let msg = [] let exit = false + let loadingDialog + let importResult = false try { r = JSON.parse(json) } catch (err) { @@ -170,19 +171,16 @@ export class ActorImporter { ...commit, ...this.importSizeFromGCS(commit, r.profile, r.traits || r.advantages, r.skills, r.equipment), } - if (r.version === 4) { - commit = { ...commit, ...this.importAdsFromGCS(r.traits || r.advantages) } - commit = { ...commit, ...this.importSkillsFromGCS(r.skills) } - commit = { ...commit, ...this.importSpellsFromGCS(r.spells) } - commit = { ...commit, ...this.importEquipmentFromGCS(r.equipment, r.other_equipment) } - commit = { ...commit, ...this.importNotesFromGCS(r.notes) } - } else if (r.version === 5) { - commit = { ...commit, ...this.importAdsFromGCS(r.traits || r.advantages) } - commit = { ...commit, ...this.importSkillsFromGCS(r.skills) } - commit = { ...commit, ...this.importSpellsFromGCS(r.spells) } - commit = { ...commit, ...this.importEquipmentFromGCS(r.equipment, r.other_equipment) } - commit = { ...commit, ...this.importNotesFromGCS(r.notes) } - } + commit = { ...commit, ...this.importAdsFromGCS(r.traits || r.advantages) } + commit = { ...commit, ...this.importSkillsFromGCS(r.skills) } + commit = { ...commit, ...this.importSpellsFromGCS(r.spells) } + if ( + !!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS) || + this.actor.items.filter(i => !!i.system.importid).length > 10 + ) + loadingDialog = await this._showLoadingDialog({ name: nm, generator: 'GCS' }) + commit = { ...commit, ...(await this.importEquipmentFromGCS(r.equipment, r.other_equipment)) } + commit = { ...commit, ...this.importNotesFromGCS(r.notes) } commit = { ...commit, @@ -245,7 +243,7 @@ export class ActorImporter { 'ms.) You can inspect the character data below:' ) console.log(this) - return true + importResult = true } catch (err) { console.log(err.stack) let msg = [i18n_f('GURPS.importGenericError', { name: nm, error: err.name, message: err.message })] @@ -265,8 +263,30 @@ export class ActorImporter { whisper: [game.user.id], } ChatMessage.create(chatData, {}) - return false + } finally { + if (!!loadingDialog) await loadingDialog.close() } + return importResult + } + + async _showLoadingDialog(diagOps) { + const { name, generator } = diagOps + const dialog = new Dialog( + { + title: game.i18n.format('GURPS.importSheetTitle', { generator }), + content: `

${game.i18n.format('GURPS.importSheetHint', { name, generator })}

`, + buttons: {}, + close: () => {}, + }, + { + width: 400, + height: 200, + resizable: false, + closeOnSubmit: false, + } + ) + await dialog.render(true) + return dialog } async importActorFromGCA(source, importName, importPath, suppressMessage) { @@ -386,6 +406,8 @@ export class ActorImporter { ar.importversion = ra.version commit = { ...commit, ...{ 'system.additionalresources': ar } } + let loadingDialog + let importResult = false try { // This is going to get ugly, so break out various data into different methods commit = { ...commit, ...(await this.importAttributesFromGCA(c.attributes)) } @@ -400,7 +422,9 @@ export class ActorImporter { commit = { ...commit, ...this.importEncumbranceFromGCA(c.encumbrance) } commit = { ...commit, ...this.importPointTotalsFromGCA(c.pointtotals) } commit = { ...commit, ...this.importNotesFromGCA(c.description, c.notelist) } - commit = { ...commit, ...this.importEquipmentFromGCA(c.inventorylist) } + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) + loadingDialog = await this._showLoadingDialog({ name: nm, generator: 'GCA' }) + commit = { ...commit, ...(await this.importEquipmentFromGCA(c.inventorylist)) } commit = { ...commit, ...(await this.importProtectionFromGCA(c.combat?.protectionlist)) } } catch (err) { console.log(err.stack) @@ -447,7 +471,7 @@ export class ActorImporter { 'ms.) You can inspect the character data below:' ) console.log(this) - return true + importResult = true } catch (err) { console.log(err.stack) let msg = [i18n_f('GURPS.importGenericError', { name: nm, error: err.name, message: err.message })] @@ -467,8 +491,10 @@ export class ActorImporter { whisper: [game.user.id], } ChatMessage.create(chatData, {}) - return false + } finally { + if (!!loadingDialog) await loadingDialog.close() } + return importResult } // Import the section of the GCS FG XML file. @@ -888,14 +914,43 @@ export class ActorImporter { } } + async _preImport(generator) { + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // Before we import, we need to find all eligible items, + // and backup their exclusive info inside their equipments + const isEligibleItem = item => + (!!item.system.importid && item.system.importFrom === generator) || + !!foundry.utils.getProperty(this.actor, this.actor._findEqtkeyForId('itemid', item.id))?.save + + let eligibleItems = this.actor.items + .filter(i => !!isEligibleItem(i)) + .map(i => { + const itemInfo = i.getItemInfo() + // Update equipment + const eqtKey = this.actor._findEqtkeyForId('itemid', i.id) + if (!!eqtKey) { + let equip = foundry.utils.getProperty(this.actor, eqtKey) + equip.itemid = '' + equip.itemInfo = itemInfo + this.actor.internalUpdate({ [eqtKey]: equip }) + } + return i.id + }) + if (!!eligibleItems.length) await this.actor.deleteEmbeddedDocuments('Item', eligibleItems) + } + } + /** * @param {{ [key: string]: any }} json */ - importEquipmentFromGCA(json) { + async importEquipmentFromGCA(json) { if (!json) return let t = this.textFrom let i = this.intFrom + this.ignoreRender = true + await this._preImport('GCA') + /** * @type {Equipment[]} */ @@ -904,28 +959,31 @@ export class ActorImporter { if (key.startsWith('id-')) { // Allows us to skip over junk elements created by xml->json code, and only select the skills. let j = json[key] + let { name, techLevel } = this.parseEquipmentNameAndTL(t, j) + let parentuuid = t(j.parentuuid) let eqt = new Equipment() - eqt.name = t(j.name) + eqt.name = name eqt.count = t(j.count) - eqt.cost = t(j.cost) + eqt.cost = !!parentuuid ? t(j.cost) : 0 eqt.location = t(j.location) let cstatus = i(j.carried) eqt.carried = cstatus >= 1 eqt.equipped = cstatus == 2 - eqt.techlevel = t(j.tl) + eqt.techlevel = techLevel eqt.legalityclass = t(j.lc) eqt.categories = t(j.type) eqt.uses = t(j.uses) eqt.maxuses = t(j.maxuses) eqt.uuid = t(j.uuid) - eqt.parentuuid = t(j.parentuuid) + eqt.parentuuid = parentuuid eqt.setNotes(t(j.notes)) - eqt.weight = t(j.weightsum) // GCA sends calculated weight in 'weightsum' + eqt.weight = !!parentuuid ? t(j.weightsum) : 0 // GCA sends calculated weight in 'weightsum' eqt.pageRef(t(j.pageref)) let old = this._findElementIn('equipment.carried', eqt.uuid) if (!old) old = this._findElementIn('equipment.other', eqt.uuid) this._migrateOtfsAndNotes(old, eqt) if (!!old) { + eqt.name = old.name eqt.carried = old.carried eqt.equipped = old.equipped eqt.parentuuid = old.parentuuid @@ -936,20 +994,37 @@ export class ActorImporter { eqt.ignoreImportQty = true } } + // Process Item here + eqt = await this._processItemFrom(eqt) temp.push(eqt) } } // Save the old User Entered Notes. - recurselist(this.actor.system.equipment?.carried, t => { + await aRecurselist(this.actor.system.equipment?.carried, async t => { t.carried = true - if (!!t.save) temp.push(t) + if (!!t.save) { + t = await this._processItemFrom(t) + temp.push(t) + } }) // Ensure carried eqt stays in carried - recurselist(this.actor.system.equipment?.other, t => { + await aRecurselist(this.actor.system.equipment?.other, async t => { t.carried = false - if (!!t.save) temp.push(t) + if (!!t.save) { + t = await this._processItemFrom(t) + temp.push(t) + } }) + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // After retrieve all relevant data + // Lets remove equipments now + await this.actor.internalUpdate({ + 'system.equipment.-=carried': null, + 'system.equipment.-=other': null, + }) + } + temp.forEach(eqt => { // Remove all entries from inside items because if they still exist, they will be added back in eqt.contains = {} @@ -957,14 +1032,15 @@ export class ActorImporter { }) // Put everything in it container (if found), otherwise at the top level - temp.forEach(eqt => { + for (const eqt of temp) { + let parent = null if (!!eqt.parentuuid) { - let parent = null parent = temp.find(e => e.uuid === eqt.parentuuid) if (!!parent) GURPS.put(parent.contains, eqt) else eqt.parentuuid = '' // Can't find a parent, so put it in the top list } - }) + await this._updateItemContains(eqt, parent) + } let equipment = { carried: {}, @@ -973,19 +1049,43 @@ export class ActorImporter { let cindex = 0 let oindex = 0 - temp.forEach(eqt => { - Equipment.calc(eqt) + for (const eqt of temp) { + await Equipment.calc(eqt) if (!eqt.parentuuid) { if (eqt.carried) GURPS.put(equipment.carried, eqt, cindex++) else GURPS.put(equipment.other, eqt, oindex++) } - }) + } return { 'system.-=equipment': null, 'system.equipment': equipment, } } + /* + * Parse Name and TL for GCA data. + * + * Example: Backpack/TL8+3^ + */ + parseEquipmentNameAndTL(t, j) { + let name + let fullName = t(j.name) + let techLevel = t(j.tl) + const localizedTL = i18n('GURPS.TL') + let regex = new RegExp(`.+\/[TL|${localizedTL}].+`) + if (!!fullName.match(regex)) { + let i = fullName.lastIndexOf('/TL') || fullName.lastIndexOf(`/${localizedTL}`) + if (!!i) { + name = fullName.substring(0, i) + techLevel = fullName.substring(i + 3) + } + } + if (!name) { + name = fullName + } + return { name, techLevel } + } + /** * @param {{ [x: string]: any; bodyplan: Record; }} json */ @@ -1393,7 +1493,6 @@ export class ActorImporter { 'system.basicspeed': data.basicspeed, 'system.thrust': data.thrust, 'system.swing': data.swing, - 'system.currentmove': data.currentmove, 'system.frightcheck': data.frightcheck, 'system.hearing': data.hearing, 'system.tastesmell': data.tastesmell, @@ -1596,40 +1695,59 @@ export class ActorImporter { return [s].concat(ch) } - importEquipmentFromGCS(eq, oeq) { + async importEquipmentFromGCS(eq, oeq) { + this.ignoreRender = true + await this._preImport('GCS') + if (!eq && !oeq) return let temp = [] if (!!eq) for (let i of eq) { - temp = temp.concat(this.importEq(i, '', true)) + temp = temp.concat(await this.importEq(i, '', true)) } if (!!oeq) for (let i of oeq) { - temp = temp.concat(this.importEq(i, '', false)) + temp = temp.concat(await this.importEq(i, '', false)) } - recurselist(this.actor.system.equipment?.carried, t => { + await aRecurselist(this.actor.system.equipment?.carried, async t => { t.carried = true - if (!!t.save) temp.push(t) + if (!!t.save) { + t = await this._processItemFrom(t) + temp.push(t) + } }) - recurselist(this.actor.system.equipment?.other, t => { + await aRecurselist(this.actor.system.equipment?.other, async t => { t.carried = false - if (!!t.save) temp.push(t) + if (!!t.save) { + t = await this._processItemFrom(t) + temp.push(t) + } }) + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // After retrieve all relevant data + // Lets remove equipments now + await this.actor.internalUpdate({ + 'system.equipment.-=carried': null, + 'system.equipment.-=other': null, + }) + } + temp.forEach(e => { e.contains = {} e.collapsed = {} }) - temp.forEach(e => { + for (const e of temp) { if (!!e.parentuuid) { let parent = null parent = temp.find(f => f.uuid === e.parentuuid) if (!!parent) GURPS.put(parent.contains, e) else e.parentuuid = '' } - }) + await this._updateItemContains(e, parent) + } let equipment = { carried: {}, @@ -1638,20 +1756,20 @@ export class ActorImporter { let cindex = 0 let oindex = 0 - temp.forEach(eqt => { - Equipment.calc(eqt) + for (const eqt of temp) { + await Equipment.calc(eqt) if (!eqt.parentuuid) { if (eqt.carried) GURPS.put(equipment.carried, eqt, cindex++) else GURPS.put(equipment.other, eqt, oindex++) } - }) + } return { 'system.-=equipment': null, 'system.equipment': equipment, } } - importEq(i, p, carried) { + async importEq(i, p, carried) { let e = new Equipment() if (this.GCSVersion === 5) { i.type = i.id.startsWith('e') ? 'equipment' : 'equipment_container' @@ -1684,6 +1802,7 @@ export class ActorImporter { if (!old) old = this._findElementIn('equipment.other', e.uuid) this._migrateOtfsAndNotes(old, e, i.vtt_notes) if (!!old) { + e.name = old.name e.carried = old.carried e.equipped = old.equipped e.parentuuid = old.parentuuid @@ -1694,9 +1813,11 @@ export class ActorImporter { e.ignoreImportQty = true } } + // Process Item here + e = await this._processItemFrom(e) let ch = [] if (i.children?.length) { - for (let j of i.children) ch = ch.concat(this.importEq(j, i.id, carried)) + for (let j of i.children) ch = ch.concat(await this.importEq(j, i.id, carried)) for (let j of ch) { e.cost -= j.cost * j.count e.weight -= j.weight * j.count @@ -2335,4 +2456,40 @@ export class ActorImporter { } return item } + + async _processItemFrom(eqt) { + // Non Equipment instance objects need to be converted to Equipment first. + let equip = Equipment.fromObject(eqt, this.actor) + + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // Create or Update item + const existingItem = this.actor.items.find(i => i._id === equip.itemid) + const itemData = equip.toItemData() + const [item] = !!existingItem + ? await this.actor.updateEmbeddedDocuments('Item', [{ _id: existingItem._id, ...itemData }]) + : await this.actor.createEmbeddedDocuments('Item', [equip.toItemData()]) + // Update Equipment for new Items + if (!existingItem && !!item) { + equip.itemid = item._id + equip.itemInfo = item.getItemInfo() + } + } + return equip + } + async _updateItemContains(eqt, parent) { + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + const item = this.actor.items.get(eqt.itemid) + if (!!item) { + if (!eqt.parentuuid) { + await this.actor.updateEmbeddedDocuments('Item', [{ _id: item._id, 'system.eqt.contains': eqt.contains }]) + } else { + const parentItem = this.actor.items.get(parent.itemid) + if (!!parentItem?.id) + await this.actor.updateEmbeddedDocuments('Item', [ + { _id: item._id, 'system.eqt.parentuuid': parentItem.id }, + ]) + } + } + } + } } diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index 81e3bf34d..88839f3a7 100755 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -12,6 +12,7 @@ import MoveModeEditor from './move-mode-editor.js' import { Advantage, Equipment, Melee, Modifier, Note, Ranged, Reaction, Skill, Spell } from './actor-components.js' import SplitDREditor from './splitdr-editor.js' import { ActorImporter } from './actor-importer.js' +import * as Settings from '../../lib/miscellaneous-settings.js' /** * Extend the basic ActorSheet with some very simple modifications @@ -448,6 +449,7 @@ export class GurpsActorSheet extends ActorSheet { let actor = this.actor let obj = foundry.utils.duplicate(foundry.utils.getProperty(actor, path)) // must dup so difference can be detected when updated if (!!obj.itemid) { + if (!(await this.actor._sanityCheckItemSettings(obj))) return let item = this.actor.items.get(obj.itemid) item.editingActor = this.actor item.sheet.render(true) @@ -481,19 +483,33 @@ export class GurpsActorSheet extends ActorSheet { let parent = $(ev.currentTarget).closest('[data-key]') let path = parent.attr('data-key') let eqt = foundry.utils.getProperty(this.actor, path) + if (!(await this.actor._sanityCheckItemSettings(eqt))) return let value = parseInt(eqt.uses) + (ev.shiftKey ? 5 : 1) if (isNaN(value)) value = eqt.uses - await this.actor.internalUpdate({ [path + '.uses']: value }) + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await this.actor.internalUpdate({ [path + '.uses']: value }) + } else { + let item = this.actor.items.get(eqt.itemid) + item.system.eqt.uses = value + await this.actor._updateItemFromForm(item) + } }) html.find('button[data-operation="equipment-dec-uses"]').click(async ev => { ev.preventDefault() let parent = $(ev.currentTarget).closest('[data-key]') let path = parent.attr('data-key') let eqt = foundry.utils.getProperty(this.actor, path) + if (!(await this.actor._sanityCheckItemSettings(eqt))) return let value = parseInt(eqt.uses) - (ev.shiftKey ? 5 : 1) if (isNaN(value)) value = eqt.uses if (value < 0) value = 0 - await this.actor.internalUpdate({ [path + '.uses']: value }) + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await this.actor.internalUpdate({ [path + '.uses']: value }) + } else { + let item = this.actor.items.get(eqt.itemid) + item.system.eqt.uses = value + await this.actor._updateItemFromForm(item) + } }) // On clicking equipment quantity decrement, decrease the amount or remove from list. @@ -503,6 +519,7 @@ export class GurpsActorSheet extends ActorSheet { let path = parent.attr('data-key') let actor = this.actor let eqt = foundry.utils.getProperty(actor, path) + if (!(await this.actor._sanityCheckItemSettings(eqt))) return if (eqt.count == 0) { await Dialog.confirm({ title: i18n('GURPS.removeItem'), @@ -776,7 +793,16 @@ export class GurpsActorSheet extends ActorSheet { return { name: i18n_f('GURPS.editorAddItem', { name: name }, 'Add {name} at the end'), icon: '', - callback: e => { + callback: async e => { + if (path.includes('system.equipment')) { + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + obj.save = true + let payload = obj.toItemData() + const [item] = await this.actor.createEmbeddedDocuments('Item', [payload]) + obj.itemid = item._id + } + if (!obj.uuid) obj.uuid = obj._getGGAId() + } let o = GURPS.decode(this.actor, path) || {} GURPS.put(o, foundry.utils.duplicate(obj)) this.actor.internalUpdate({ [path]: o }) @@ -817,13 +843,14 @@ export class GurpsActorSheet extends ActorSheet { isLinked: !this.actor.isToken, type: type, key: eqtkey, + uuid: this.actor.items.get(eqt.itemid)?.uuid, itemid: eqt.itemid, itemData: itemData, } if (!!oldd) foundry.utils.mergeObject(newd, JSON.parse(oldd)) // May need to merge in OTF drag info let payload = JSON.stringify(newd) - //console.log(payload) + console.log('GGA DragDrop Payload: ', payload) return ev.dataTransfer.setData('text/plain', payload) }) }) @@ -1000,6 +1027,8 @@ export class GurpsActorSheet extends ActorSheet { obj.f_count = obj.count // Hack to get around The Furnace's "helpful" Handlebar helper {{count}} let dlgHtml = await renderTemplate('systems/gurps/templates/equipment-editor-popup.html', obj) + if (!(await this.actor._sanityCheckItemSettings(obj))) return + let d = new Dialog( { title: 'Equipment Editor', @@ -1008,16 +1037,30 @@ export class GurpsActorSheet extends ActorSheet { one: { label: 'Update', callback: async html => { - ;['name', 'uses', 'maxuses', 'techlevel', 'notes', 'pageref'].forEach( - a => (obj[a] = html.find(`.${a}`).val()) - ) - ;['count', 'cost', 'weight'].forEach(a => (obj[a] = parseFloat(html.find(`.${a}`).val()))) - let u = html.find('.save') // Should only find in Note (or equipment) - if (!!u && obj.save != null) obj.save = u.is(':checked') // only set 'saved' if it was already defined - let v = html.find('.ignoreImportQty') // Should only find in equipment - if (!!v) obj.ignoreImportQty = v.is(':checked') - await actor.internalUpdate({ [path]: obj }) - await actor.updateParentOf(path, false) + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + ;['name', 'uses', 'maxuses', 'techlevel', 'notes', 'pageref'].forEach( + a => (obj[a] = html.find(`.${a}`).val()) + ) + ;['count', 'cost', 'weight'].forEach(a => (obj[a] = parseFloat(html.find(`.${a}`).val()))) + let u = html.find('.save') // Should only find in Note (or equipment) + if (!!u && obj.save != null) obj.save = u.is(':checked') // only set 'saved' if it was already defined + let v = html.find('.ignoreImportQty') // Should only find in equipment + if (!!v) obj.ignoreImportQty = v.is(':checked') + await actor.internalUpdate({ [path]: obj }) + await actor.updateParentOf(path, false) + } else { + let item = actor.items.get(obj.itemid) + item.name = obj.name + item.system.eqt.count = obj.count + item.system.eqt.cost = obj.cost + item.system.eqt.uses = obj.uses + item.system.eqt.maxuses = obj.maxuses + item.system.eqt.techlevel = obj.techlevel + item.system.eqt.notes = obj.notes + item.system.eqt.pageref = obj.pageref + await actor._updateItemFromForm(item) + await actor.updateParentOf(path, false) + } }, }, }, @@ -1618,10 +1661,17 @@ export class GurpsActorSheet extends ActorSheet { ev.preventDefault() let element = ev.currentTarget let key = element.dataset.key + if (!(await this.actor._sanityCheckItemSettings(GURPS.decode(this.actor, key)))) return let eqt = foundry.utils.duplicate(GURPS.decode(this.actor, key)) eqt.equipped = !eqt.equipped await this.actor.updateItemAdditionsBasedOn(eqt, key) await this.actor.internalUpdate({ [key]: eqt }) + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + let item = this.actor.items.get(eqt.itemid) + item.system.equipped = eqt.equipped + item.system.eqt.equipped = eqt.equipped + await this.actor._updateItemFromForm(item) + } let p = this.actor.getEquippedParry() let b = this.actor.getEquippedBlock() await this.actor.internalUpdate({ diff --git a/module/actor/actor.js b/module/actor/actor.js index 051948a71..18d69ce4c 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -31,6 +31,7 @@ import { GurpsItem } from '../item.js' import GurpsToken from '../token.js' import { Advantage, Equipment, HitLocationEntry } from './actor-components.js' import { multiplyDice } from '../utilities/damage-utils.js' +import * as Settings from '../../lib/miscellaneous-settings.js' // Ensure that ALL actors has the current version loaded into them (for migration purposes) Hooks.on('createActor', async function (/** @type {Actor} */ actor) { @@ -510,6 +511,7 @@ export class GurpsActor extends Actor { for (let enckey in encs) { let enc = encs[enckey] + if (!enc.level) enc.level = parseInt(enckey) // FIXME: Why enc.level=NaN after GCA import? let threshold = 10 - 2 * parseInt(enc.level) // each encumbrance level reduces move by 20% threshold /= 10 // JS likes to calculate 0.2*3 = 3.99999, but handles 2*3/10 fine. enc.currentmove = this._getCurrentMove(effectiveMove, threshold) //Math.max(1, Math.floor(m * t)) @@ -1131,10 +1133,11 @@ export class GurpsActor extends Actor { ui.notifications?.warn(i18n('GURPS.youDoNotHavePermssion')) return } - const uuid = - typeof dragData.pack === 'string' - ? `Compendium.${dragData.pack}.${dragData.id}` - : `${dragData.type}.${dragData.id}` + // New item created in Foundry v12.331 dragData: + // { + // "type": "Item", + // "uuid": "Item.542YuRvzxVx83kL1" + // } let global = await fromUuid(dragData.uuid) let data = !!global ? global : dragData if (!data) { @@ -1142,7 +1145,7 @@ export class GurpsActor extends Actor { return } ui.notifications?.info(data.name + ' => ' + this.name) - if (!data.globalid) await data.update({ _id: data._id, 'system.globalid': uuid }) + if (!data.globalid) await data.update({ _id: data._id, 'system.globalid': dragData.uuid }) this.ignoreRender = true await this.addNewItemData(data) this._forceRender() @@ -1165,7 +1168,7 @@ export class GurpsActor extends Actor { return } if (!dragData.isLinked) { - ui.notifications?.warn("You cannot drag from an un-linked token. The source must have 'Linked Actor Data'") + ui.notifications?.warn("You cannot drag from an un-linked token. The source must have 'Linked Actor Data'") return } let srcActor = game.actors.get(dragData.actorid) @@ -1302,6 +1305,10 @@ export class GurpsActor extends Actor { let localItem = localItems[0] await this.updateEmbeddedDocuments('Item', [{ _id: localItem.id, 'system.eqt.uuid': generateUniqueId() }]) await this.addItemData(localItem, targetkey) // only created 1 item + if (game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + const item = await this.items.get(localItem._id) + return this._updateItemFromForm(item) + } } // Once the Items has been added to our items list, add the equipment and any features @@ -1365,6 +1372,7 @@ export class GurpsActor extends Actor { eqt.equipped = !!_data.equipped ?? true eqt.img = itemData.img eqt.carried = !!_data.carried ?? true + if (!!eqt.uuid.startsWith('GGA')) eqt.save = true await GURPS.insertBeforeKey(this, targetkey, eqt) await this.updateParentOf(targetkey, true) return [targetkey, eqt.carried && eqt.equipped] @@ -1906,19 +1914,30 @@ export class GurpsActor extends Actor { */ async updateEqtCount(eqtkey, count) { /** @type {{ [key: string]: any }} */ - let update = { [eqtkey + '.count']: count } - if (game.settings.get(settings.SYSTEM_NAME, settings.SETTING_AUTOMATICALLY_SET_IGNOREQTY)) - update[eqtkey + '.ignoreImportQty'] = true - await this.update(update) let eqt = foundry.utils.getProperty(this, eqtkey) - await this.updateParentOf(eqtkey, false) - if (!!eqt.itemid) { + if (!(await this._sanityCheckItemSettings(eqt))) return + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + let update = { [eqtkey + '.count']: count } + if (game.settings.get(settings.SYSTEM_NAME, settings.SETTING_AUTOMATICALLY_SET_IGNOREQTY)) + update[eqtkey + '.ignoreImportQty'] = true + await this.update(update) + eqt = foundry.utils.getProperty(this, eqtkey) + await this.updateParentOf(eqtkey, false) + if (!!eqt.itemid) { + let item = this.items.get(eqt.itemid) + if (!!item) await this.updateEmbeddedDocuments('Item', [{ _id: item.id, 'system.eqt.count': count }]) + else { + ui.notifications?.warn('Invalid Item in Actor... removing all features') + await this._removeItemAdditions(eqt.itemid) + } + } + } else { let item = this.items.get(eqt.itemid) - if (!!item) await this.updateEmbeddedDocuments('Item', [{ _id: item.id, 'system.eqt.count': count }]) - else { - ui.notifications?.warn('Invalid Item in Actor... removing all features') - this._removeItemAdditions(eqt.itemid) + if (!!item) { + item.system.eqt.count = count + await item.actor._updateItemFromForm(item) } + await this.updateParentOf(eqtkey, false) } } @@ -1971,4 +1990,80 @@ export class GurpsActor extends Actor { return true } + + async _updateEquipmentCalc(equipKey) { + const equip = foundry.utils.getProperty(this, equipKey) + await Equipment.calc(equip) + if (!!equip.parentuuid) { + const parentKey = this._findEqtkeyForId('itemid', equip.parentuuid) + if (parentKey) { + await this._updateEquipmentCalc(parentKey) + } + } + } + + async _updateItemFromForm(item) { + const equipKey = this._findEqtkeyForId('itemid', item.id) + const equip = foundry.utils.getProperty(this, equipKey) + if (!(await this._sanityCheckItemSettings(equip))) return + // Update Item + if (!!item.editingActor) delete item.editingActor + await this.updateEmbeddedDocuments('Item', [{ _id: item.id, system: item.system, name: item.name }]) + // Update Equipment + const itemInfo = item.getItemInfo() + await this.internalUpdate({ + [equipKey]: { + ...item.system.eqt, + uuid: equip.uuid, + parentuuid: equip.parentuuid, + itemInfo, + }, + }) + await this._updateEquipmentCalc(equipKey) + await this.updateParentOf(equipKey, true) + this.calculateDerivedValues() + } + + async _sanityCheckItemSettings(eqt) { + let canEdit = false + let message + + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + message = 'GURPS.settingNoEquipAllowedHint' + if (!!eqt.itemid) canEdit = true + } else { + message = 'GURPS.settingNoItemAllowedHint' + if (!eqt.itemid) { + canEdit = true + } else { + const item = this.items.get(eqt.itemid) + if (!!item && !item.system.importid) canEdit = true + } + } + + if (!canEdit) { + const phrases = game.i18n + .localize(message) + .split('.') + .filter(p => !!p) + .map(p => `${p.trim()}.`) + const body = phrases.join('

') + const dialog = new Dialog( + { + title: game.i18n.localize('GURPS.settingNoEditAllowed'), + content: `

${body}

`, + buttons: { + ok: { + label: 'OK', + }, + }, + }, + { + width: 400, + } + ) + await dialog.render(true) + } + return canEdit + } } diff --git a/module/damage/applydamage.js b/module/damage/applydamage.js index 1d23464ad..0d3490f15 100755 --- a/module/damage/applydamage.js +++ b/module/damage/applydamage.js @@ -699,7 +699,7 @@ export default class ApplyDamageDialog extends Application { messageData.whisper = ids } - ChatMessage.create(messageData).then((message) => { + ChatMessage.create(messageData).then(message => { GURPS.lastInjuryRoll = data GURPS.lastInjuryRolls[this.actor.id] = data GURPS.lastInjuryRolls[message.id] = data diff --git a/module/global-references.js b/module/global-references.js index 4ac16c163..b4afb6067 100644 --- a/module/global-references.js +++ b/module/global-references.js @@ -132,6 +132,8 @@ import GURPSConditionalInjury from './injury/foundry/conditional-injury.js' * parentuuid: string, * img: string | null, * globalid: string, + * importid: string, + * importFrom: string, * uuid: string}} eqt * === End GurpsItemData * diff --git a/module/gurps-wiring.js b/module/gurps-wiring.js index f4cbae53a..6310f661f 100644 --- a/module/gurps-wiring.js +++ b/module/gurps-wiring.js @@ -122,7 +122,7 @@ export default class GurpsWiring { * any text). If not, we will just re-parse the text looking for the action block. * * @param {JQuery.MouseEventBase} event - * @param {import("./actor/actor").GurpsActor | null} actor + * @param {import("./actor/actor.js").GurpsActor | null} actor * @param {undefined} [desc] * @param {undefined} [targets] */ diff --git a/module/gurps.js b/module/gurps.js index 348fb4ce6..0f9e7f0ad 100644 --- a/module/gurps.js +++ b/module/gurps.js @@ -78,6 +78,7 @@ import { gurpslink } from './utilities/gurpslink.js' import { PDFEditorSheet } from './pdf/edit.js' import { JournalEntryPageGURPS } from './pdf/index.js' import ApplyDamageDialog from './damage/applydamage.js' +import { GGADebugger } from '../utils/debugger.js' let GURPS = undefined @@ -1937,6 +1938,9 @@ if (!globalThis.GURPS) { GurpsActiveEffect.init() GURPSSpeedProvider.init() + // Add Debugger info + GGADebugger.init() + // Modifier Bucket must be defined after hit locations GURPS.ModifierBucket = new ModifierBucket() GURPS.ModifierBucket.render(true) diff --git a/module/item-sheet.js b/module/item-sheet.js index 0c5ef79e6..a3e4521c0 100755 --- a/module/item-sheet.js +++ b/module/item-sheet.js @@ -2,6 +2,7 @@ import { _Base, Melee, Skill, Spell, Advantage, Ranged } from './actor/actor-components.js' import { digitsAndDecimalOnly, digitsOnly } from '../lib/jquery-helper.js' import { recurselist } from '../lib/utilities.js' +import * as Settings from '../lib/miscellaneous-settings.js' export class GurpsItemSheet extends ItemSheet { /** @override */ @@ -40,7 +41,7 @@ export class GurpsItemSheet extends ItemSheet { html.find('.digits-only').inputFilter(value => digitsOnly.test(value)) html.find('.decimal-digits-only').inputFilter(value => digitsAndDecimalOnly.test(value)) - html.find('#itemname').change(ev => { + html.find('#itemname').change(async ev => { let nm = ev.currentTarget.value let commit = { 'system.eqt.name': nm, @@ -52,44 +53,44 @@ export class GurpsItemSheet extends ItemSheet { recurselist(this.item.system.ranged, (e, k, d) => { commit = { ...commit, ...{ ['system.ranged.' + k + '.name']: nm } } }) - this.item.update(commit) + await this.item.update(commit) }) // html.find('#quantity').change(ev => this.item.update({ 'system.eqt.count': parseInt(ev.currentTarget.value) })) - html.find('#add-melee').click(ev => { + html.find('#add-melee').click(async ev => { ev.preventDefault() let m = new Melee() m.name = this.item.name - this._addToList('melee', m) + await this._addToList('melee', m) }) html.find('.delete.button').click(this._deleteKey.bind(this)) - html.find('#add-ranged').click(ev => { + html.find('#add-ranged').click(async ev => { ev.preventDefault() let r = new Ranged() r.name = this.item.name r.legalityclass = 'lc' - this._addToList('ranged', r) + await this._addToList('ranged', r) }) - html.find('#add-skill').click(ev => { + html.find('#add-skill').click(async ev => { ev.preventDefault() let r = new Skill() r.rsl = '-' - this._addToList('skills', r) + await this._addToList('skills', r) }) - html.find('#add-spell').click(ev => { + html.find('#add-spell').click(async ev => { ev.preventDefault() let r = new Spell() - this._addToList('spells', r) + await this._addToList('spells', r) }) - html.find('#add-ads').click(ev => { + html.find('#add-ads').click(async ev => { ev.preventDefault() let r = new Advantage() - this._addToList('ads', r) + await this._addToList('ads', r) }) html.find('textarea').on('drop', this.dropFoundryLinks) @@ -109,8 +110,9 @@ export class GurpsItemSheet extends ItemSheet { JSON.stringify({ type: 'Item', id: this.item.id, + uuid: this.item.uuid, pack: this.item.pack, - data: this.item.data, + itemData: this.item.system, }) ) }) @@ -159,18 +161,25 @@ export class GurpsItemSheet extends ItemSheet { }) return } - this._addToList(dragData.type, srcData) + await this._addToList(dragData.type, srcData) } - _addToList(key, data) { + async _addToList(key, data) { let list = this.item.system[key] || {} GURPS.put(list, data) - this.item.update({ ['system.' + key]: list }) + await this.item.update({ ['system.' + key]: list }) } - close() { - super.close() - this.item.update({ 'system.eqt.name': this.item.name }) - if (!!this.object.editingActor) this.object.editingActor.updateItem(this.object) + async close() { + await super.close() + const equipKey = this.object.editingActor._findEqtkeyForId('itemid', this.item.id) + const equip = foundry.utils.getProperty(this.object.editingActor, equipKey) + if (!(await this.object.editingActor._sanityCheckItemSettings(equip))) return + await this.item.update({ 'system.eqt.name': this.item.name }) + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + if (!!this.object.editingActor) await this.object.editingActor.updateItem(this.object) + } else { + await this.object.editingActor._updateItemFromForm(this.item) + } } } diff --git a/module/item.js b/module/item.js index e92468c3b..0dd922433 100755 --- a/module/item.js +++ b/module/item.js @@ -7,12 +7,6 @@ export class GurpsItem extends Item { return /** @type {GurpsItem} */ (item) } - async internalUpdate(data, context) { - let ctx = { render: true } - if (!!context) ctx = { ...context, ...ctx } - await this.update(data, ctx) - } - prepareData() { super.prepareData() } @@ -22,4 +16,20 @@ export class GurpsItem extends Item { if (!!context) ctx = { ...context, ...ctx } await this.update(data, ctx) } + + /* + * Get Item's exclusive data not found in Equipment + * + * @returns {object} + */ + getItemInfo() { + let data = foundry.utils.duplicate(this) + let itemSystem = data.system + delete itemSystem.eqt + return { + id: this._id, + img: this.img, + system: itemSystem, + } + } } diff --git a/module/smart-importer.js b/module/smart-importer.js index d41d34024..3e3ff98a4 100644 --- a/module/smart-importer.js +++ b/module/smart-importer.js @@ -1,4 +1,5 @@ import { UniversalFileHandler } from './file-handlers/universal-file-handler.js' +import * as Settings from '../lib/miscellaneous-settings.js' export class SmartImporter { static async getFileForActor(actor) { @@ -9,7 +10,7 @@ export class SmartImporter { file ?? (await UniversalFileHandler.getFile({ template, - templateOptions: { name: actor.name }, + templateOptions: this.getTemplateOptions(actor), extensions: ['.xml', '.txt', '.gcs'], })) ) @@ -17,5 +18,26 @@ export class SmartImporter { static setFileForActor(actor, file) { this.actorToFileMap.set(actor, file) } + static getTemplateOptions(actor) { + const { name } = actor + const useFoundryItems = game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS) + const equipType = useFoundryItems + ? game.i18n.localize('GURPS.importTraitToFoundryItem') + : game.i18n.localize('GURPS.importTraitToClassicData') + const equipColor = useFoundryItems ? '#0b2e13' : '#2e0b0b' + return { + title: game.i18n.localize('GURPS.importSelectFileTitle'), + source: game.i18n.localize('GURPS.importSelectFileSource'), + describeAction: game.i18n.localize('GURPS.importSelectFileDescribeAction'), + overwriteAction: new Handlebars.SafeString(game.i18n.format('GURPS.importSelectFileOverwriteAction', { name })), + itemAction: new Handlebars.SafeString( + game.i18n.format('GURPS.importSelectFileItemAction', { + equipType: equipType, + equipColor: equipColor, + }) + ), + note: game.i18n.localize('GURPS.importSelectFileNote'), + } + } } SmartImporter.actorToFileMap = new Map() diff --git a/styles/apps.css b/styles/apps.css index 65ea320c8..42a7868a8 100644 --- a/styles/apps.css +++ b/styles/apps.css @@ -2325,4 +2325,17 @@ button.equipmentbutton:last-child { #spells .tooltip { min-width: 200px; +} + +.window-app .window-header .window-title .document-debug-link { + margin-left: 0.25rem; + opacity: 0.5; + cursor: pointer; +} + +.debug-content { + max-height: 400px; + overflow-y: auto; + white-space: pre-wrap; + word-wrap: break-word; } \ No newline at end of file diff --git a/template.json b/template.json index e99186615..8082d671a 100755 --- a/template.json +++ b/template.json @@ -233,7 +233,9 @@ "bonuses": "", "equipped": true, "carried": true, - "globalid": "" + "globalid": "", + "importid": "", + "importFrom": "" } } } diff --git a/templates/import-gcs-or-gca.hbs b/templates/import-gcs-or-gca.hbs index 73af871f7..991160729 100644 --- a/templates/import-gcs-or-gca.hbs +++ b/templates/import-gcs-or-gca.hbs @@ -1,7 +1,13 @@ -

Select a file exported from GCS or GCA.

+

{{title}}


- + {{{inputElement}}}
-


The import will overwrite the data for "{{name}}".

-
NOTE: This cannot be un-done. \ No newline at end of file +
+

{{describeAction}}

+
    +
  • {{overwriteAction}}
  • +
  • {{itemAction}}
  • +
+
+

 {{note}}

diff --git a/utils/debugger.js b/utils/debugger.js new file mode 100644 index 000000000..8e218f06c --- /dev/null +++ b/utils/debugger.js @@ -0,0 +1,72 @@ +import * as Settings from '../lib/miscellaneous-settings.js' + +// Copied from Monks Little Details module +export let patchFunc = (prop, func, type = 'WRAPPER') => { + let nonLibWrapper = () => { + const oldFunc = eval(prop) + eval(`${prop} = function (event) { + return func.call(this, ${type != 'OVERRIDE' ? 'oldFunc.bind(this),' : ''} ...arguments); + }`) + } + if (game.modules.get('lib-wrapper')?.active) { + try { + libWrapper.register('gurps', prop, func, type) + } catch (e) { + nonLibWrapper() + } + } else { + nonLibWrapper() + } +} + +export class GGADebugger { + static init() { + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_SHOW_DEBUG_INFO)) return + patchFunc('DocumentSheet.prototype._createDocumentIdLink', async function (wrapped, ...args) { + wrapped(...args) + let [html] = args + + if ( + !(this.object instanceof foundry.abstract.Document) || + !this.object.id || + !(this.object.src || this.object.img) + ) + return + const title = html.find('.window-title') + const label = game.i18n.localize(this.object.constructor.metadata.label) + const name = this.object.name + const srcLink = document.createElement('a') + srcLink.classList.add('document-debug-link') + srcLink.setAttribute('alt', game.i18n.localize('GURPS.settingShowDebugTooltip')) + srcLink.dataset.tooltip = game.i18n.localize('GURPS.settingShowDebugTooltip') + srcLink.dataset.tooltipDirection = 'UP' + srcLink.innerHTML = '' + srcLink.addEventListener('click', async event => { + event.preventDefault() + const dialog = new Dialog({ + title: `Debug: ${name} (${label})`, + content: `
${JSON.stringify(this.object, null, 2)}
`, + buttons: { + copy: { + icon: '', + label: 'Copy', + callback: () => { + let src = JSON.stringify(this.object, null, 2) + game.clipboard.copyPlainText(src) + ui.notifications.info(`Copied to clipboard`) + }, + }, + close: { + icon: '', + label: 'Close', + callback: () => {}, + }, + }, + default: 'close', + }) + await dialog.render(true) + }) + title.append(srcLink) + }) + } +} From 37fffced0c3b4368b0251255d1c5faadea12c40e Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Fri, 30 Aug 2024 15:05:25 -0300 Subject: [PATCH 2/8] chore: add missing itemAdditions logic --- module/actor/actor.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/module/actor/actor.js b/module/actor/actor.js index 18d69ce4c..00748a4c1 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -565,7 +565,7 @@ export class GurpsActor extends Actor { let inCombat = false try { inCombat = !!game.combat?.combatants.filter(c => c.actorId == this.id) - } catch (err) { } // During game startup, an exception is being thrown trying to access 'game.combat' + } catch (err) {} // During game startup, an exception is being thrown trying to access 'game.combat' let updateMove = game.settings.get(settings.SYSTEM_NAME, settings.SETTING_MANEUVER_UPDATES_MOVE) && inCombat let maneuver = this._getMoveAdjustedForManeuver(move, threshold) @@ -592,9 +592,9 @@ export class GurpsActor extends Actor { return !!adjustment ? adjustment : { - move: Math.max(1, Math.floor(move * threshold)), - text: i18n('GURPS.moveFull'), - } + move: Math.max(1, Math.floor(move * threshold)), + text: i18n('GURPS.moveFull'), + } } _adjustMove(move, threshold, value, reason) { @@ -648,9 +648,9 @@ export class GurpsActor extends Actor { return !!adjustment ? adjustment : { - move: Math.max(1, Math.floor(move * threshold)), - text: i18n('GURPS.moveFull'), - } + move: Math.max(1, Math.floor(move * threshold)), + text: i18n('GURPS.moveFull'), + } } _calculateRangedRanges() { @@ -843,7 +843,7 @@ export class GurpsActor extends Actor { let token = /** @type {GurpsToken} */ (this.token.object) return [token] } - return this.getActiveTokens().map(it => /** @type {GurpsToken} */(it)) + return this.getActiveTokens().map(it => /** @type {GurpsToken} */ (it)) } /** @@ -2006,8 +2006,9 @@ export class GurpsActor extends Actor { const equipKey = this._findEqtkeyForId('itemid', item.id) const equip = foundry.utils.getProperty(this, equipKey) if (!(await this._sanityCheckItemSettings(equip))) return - // Update Item if (!!item.editingActor) delete item.editingActor + await this._removeItemAdditions(item.id) + // Update Item await this.updateEmbeddedDocuments('Item', [{ _id: item.id, system: item.system, name: item.name }]) // Update Equipment const itemInfo = item.getItemInfo() @@ -2019,6 +2020,7 @@ export class GurpsActor extends Actor { itemInfo, }, }) + await this._addItemAdditions(item, equipKey) await this._updateEquipmentCalc(equipKey) await this.updateParentOf(equipKey, true) this.calculateDerivedValues() From 160420d111f0c09de253f7ae8f974fce16754b73 Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Sat, 31 Aug 2024 21:22:37 -0300 Subject: [PATCH 3/8] chore: fix actor update error when no data is available to commit when update Item Additions --- module/actor/actor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/module/actor/actor.js b/module/actor/actor.js index 00748a4c1..ed7422142 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -1390,7 +1390,7 @@ export class GurpsActor extends Actor { commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'ads')) } commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'skills')) } commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'spells')) } - await this.internalUpdate(commit, { diff: false }) + if (!!commit) await this.internalUpdate(commit, { diff: false }) this.calculateDerivedValues() // new skills and bonuses may affect other items... force a recalc } @@ -2023,7 +2023,6 @@ export class GurpsActor extends Actor { await this._addItemAdditions(item, equipKey) await this._updateEquipmentCalc(equipKey) await this.updateParentOf(equipKey, true) - this.calculateDerivedValues() } async _sanityCheckItemSettings(eqt) { From 9933996048749a333494e7fe636c46577203fee0 Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Sun, 1 Sep 2024 21:07:14 -0300 Subject: [PATCH 4/8] fix: bug when item imported from GCS/GCA does not have uuid --- module/actor/actor-components.js | 36 ++++++++++++++++++++++++++------ module/actor/actor-importer.js | 18 ++++++++-------- module/actor/actor-sheet.js | 6 +++--- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/module/actor/actor-components.js b/module/actor/actor-components.js index 050500140..be935a1fc 100644 --- a/module/actor/actor-components.js +++ b/module/actor/actor-components.js @@ -366,11 +366,12 @@ export class Equipment extends Named { /** * Create new GURPSItem payload using Equipment's data */ - toItemData() { + toItemData(fromProgram = '') { const timestamp = new Date() const system = this.itemInfo?.system || {} - const importId = !this.save ? this.uuid : '' - const importFrom = this.importFrom || !!importId ? (importId.startsWith('k') ? 'GCA' : 'GCS') : '' + const uniqueId = this._getGGAId({ name: this.name, type: 'equipment', generator: fromProgram }) + const importId = !this.save ? uniqueId : '' + const importFrom = this.importFrom || fromProgram return { name: this.name, img: this.itemInfo?.img || this.findDefaultImage(), @@ -392,7 +393,7 @@ export class Equipment extends Named { uses: this.uses, maxuses: this.maxuses, last_import: timestamp, - uuid: this.uuid || this._getGGAId(), + uuid: uniqueId, location: this.location, parentuuid: this.parentuuid, }, @@ -423,8 +424,31 @@ export class Equipment extends Named { return 'icons/svg/item-bag.svg' } - _getGGAId() { - return `GGA${foundry.utils.randomID(13)}` + /** + * Generates a unique GGA identifier based on the given system object properties. + * + * @param {Object} objProps - The properties of the object used for generating the unique ID. + * @param {string} objProps.name - The name of the System Object. + * @param {string} objProps.type - The system. of the System Object: equipment, ads, etc. + * @param {string} objProps.generator - The generator of the item: GCS or GCA. + * @return {string} The generated unique GGA identifier. + */ + _getGGAId(objProps) { + let uniqueId + if (!!this.uuid) { + // UUID from GCS/GCA + uniqueId = this.uuid + } else if (!!this.save) { + // User created System Object + uniqueId = `GGA${foundry.utils.randomID(13)}` + } else { + // System Object imported from GCS/GCA without a UUID + const { name, type, generator } = objProps + const hashKey = `${name}${type}${generator}` + const hash = crypto.createHash('md5').update(hashKey).digest('hex') + uniqueId = hash.substring(0, 16) + } + return uniqueId } static fromObject(data, actor) { diff --git a/module/actor/actor-importer.js b/module/actor/actor-importer.js index 35c1656bf..2bc0f2b76 100644 --- a/module/actor/actor-importer.js +++ b/module/actor/actor-importer.js @@ -995,7 +995,7 @@ export class ActorImporter { } } // Process Item here - eqt = await this._processItemFrom(eqt) + eqt = await this._processItemFrom(eqt, 'GCA') temp.push(eqt) } } @@ -1004,14 +1004,14 @@ export class ActorImporter { await aRecurselist(this.actor.system.equipment?.carried, async t => { t.carried = true if (!!t.save) { - t = await this._processItemFrom(t) + t = await this._processItemFrom(t, 'GCA') temp.push(t) } }) // Ensure carried eqt stays in carried await aRecurselist(this.actor.system.equipment?.other, async t => { t.carried = false if (!!t.save) { - t = await this._processItemFrom(t) + t = await this._processItemFrom(t, 'GCA') temp.push(t) } }) @@ -1713,14 +1713,14 @@ export class ActorImporter { await aRecurselist(this.actor.system.equipment?.carried, async t => { t.carried = true if (!!t.save) { - t = await this._processItemFrom(t) + t = await this._processItemFrom(t, 'GCS') temp.push(t) } }) await aRecurselist(this.actor.system.equipment?.other, async t => { t.carried = false if (!!t.save) { - t = await this._processItemFrom(t) + t = await this._processItemFrom(t, 'GCS') temp.push(t) } }) @@ -1814,7 +1814,7 @@ export class ActorImporter { } } // Process Item here - e = await this._processItemFrom(e) + e = await this._processItemFrom(e, 'GCS') let ch = [] if (i.children?.length) { for (let j of i.children) ch = ch.concat(await this.importEq(j, i.id, carried)) @@ -2457,17 +2457,17 @@ export class ActorImporter { return item } - async _processItemFrom(eqt) { + async _processItemFrom(eqt, fromProgram) { // Non Equipment instance objects need to be converted to Equipment first. let equip = Equipment.fromObject(eqt, this.actor) if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { // Create or Update item const existingItem = this.actor.items.find(i => i._id === equip.itemid) - const itemData = equip.toItemData() + const itemData = equip.toItemData(fromProgram) const [item] = !!existingItem ? await this.actor.updateEmbeddedDocuments('Item', [{ _id: existingItem._id, ...itemData }]) - : await this.actor.createEmbeddedDocuments('Item', [equip.toItemData()]) + : await this.actor.createEmbeddedDocuments('Item', [itemData]) // Update Equipment for new Items if (!existingItem && !!item) { equip.itemid = item._id diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index 88839f3a7..bdbdb9745 100755 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -797,15 +797,15 @@ export class GurpsActorSheet extends ActorSheet { if (path.includes('system.equipment')) { if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { obj.save = true - let payload = obj.toItemData() + let payload = obj.toItemData('') const [item] = await this.actor.createEmbeddedDocuments('Item', [payload]) obj.itemid = item._id } - if (!obj.uuid) obj.uuid = obj._getGGAId() + if (!obj.uuid) obj.uuid = obj._getGGAId({ name: obj.name, type: path.split('.')[1], generator: '' }) } let o = GURPS.decode(this.actor, path) || {} GURPS.put(o, foundry.utils.duplicate(obj)) - this.actor.internalUpdate({ [path]: o }) + await this.actor.internalUpdate({ [path]: o }) }, } } From 61b9a1923ec0595abd604afda7266db76944ae7a Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Mon, 2 Sep 2024 20:26:32 -0300 Subject: [PATCH 5/8] feat: Add Debug Info on Document Dialogs chore: fix editing items with no actor (compendium items) --- module/gurps.js | 1 - module/item-sheet.js | 23 ++++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/module/gurps.js b/module/gurps.js index 0f9c0d9db..0f9e7f0ad 100644 --- a/module/gurps.js +++ b/module/gurps.js @@ -43,7 +43,6 @@ import { ItemImporter } from '../module/item-import.js' import GURPSTokenHUD from './token-hud.js' import GurpsJournalEntry from './journal.js' import TriggerHappySupport from './effects/triggerhappy.js' -import { GGADebugger } from '../utils/debugger.js' /** * /dded to color the rollable parts of the character sheet. diff --git a/module/item-sheet.js b/module/item-sheet.js index a3e4521c0..b8852b313 100755 --- a/module/item-sheet.js +++ b/module/item-sheet.js @@ -172,14 +172,23 @@ export class GurpsItemSheet extends ItemSheet { async close() { await super.close() - const equipKey = this.object.editingActor._findEqtkeyForId('itemid', this.item.id) - const equip = foundry.utils.getProperty(this.object.editingActor, equipKey) - if (!(await this.object.editingActor._sanityCheckItemSettings(equip))) return - await this.item.update({ 'system.eqt.name': this.item.name }) - if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { - if (!!this.object.editingActor) await this.object.editingActor.updateItem(this.object) + // When editing a Compendium Item, Actor does not exist, so we need to update the Item directly + if (!!this.item.editingActor) { + const equipKey = this.item.editingActor._findEqtkeyForId('itemid', this.item.id) + const equip = foundry.utils.getProperty(this.item.editingActor, equipKey) + if (!(await this.item.editingActor._sanityCheckItemSettings(equip))) return + await this.item.update({ 'system.eqt.name': this.item.name }) + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await this.item.editingActor.updateItem(this.object) + } else { + await this.item.editingActor._updateItemFromForm(this.item) + } } else { - await this.object.editingActor._updateItemFromForm(this.item) + await this.item.update({ + name: this.item.name, + img: this.item.img, + system: this.item.system, + }) } } } From 452f442aa344c39be12a03840f7d4fed1c9269f2 Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Mon, 2 Sep 2024 22:03:14 -0300 Subject: [PATCH 6/8] chore: fix crypto import error --- lib/simple-hash.js | 15 +++++++ module/actor/actor-components.js | 70 ++++++++++++++------------------ 2 files changed, 46 insertions(+), 39 deletions(-) create mode 100644 lib/simple-hash.js diff --git a/lib/simple-hash.js b/lib/simple-hash.js new file mode 100644 index 000000000..d4c03a8de --- /dev/null +++ b/lib/simple-hash.js @@ -0,0 +1,15 @@ +export const simpleHash = input => { + let hash = '' + for (let i = 0; i < input.length; i++) { + const charCode = input.charCodeAt(i) + hash += (charCode * (i + 1)).toString(16) + } + if (hash.length > 16) { + hash = hash.substring(0, 16) + } else { + while (hash.length < 16) { + hash += '0' + } + } + return hash +} diff --git a/module/actor/actor-components.js b/module/actor/actor-components.js index be935a1fc..547b87128 100644 --- a/module/actor/actor-components.js +++ b/module/actor/actor-components.js @@ -8,6 +8,7 @@ import { convertRollStringToArrayOfInt, extractP } from '../../lib/utilities.js' import * as Settings from '../../lib/miscellaneous-settings.js' +import { simpleHash } from '../../lib/simple-hash.js' export class _Base { constructor() { @@ -73,6 +74,36 @@ export class Named extends _Base { } } } + + findDefaultImage() { + return 'icons/svg/item-bag.svg' + } + + /** + * Generates a unique GGA identifier based on the given system object properties. + * + * @param {Object} objProps - The properties of the object used for generating the unique ID. + * @param {string} objProps.name - The name of the System Object. + * @param {string} objProps.type - The system. of the System Object: equipment, ads, etc. + * @param {string} objProps.generator - The generator of the item: GCS or GCA. + * @return {string} The generated unique GGA identifier. + */ + _getGGAId(objProps) { + let uniqueId + if (!!this.uuid) { + // UUID from GCS/GCA + uniqueId = this.uuid + } else if (!!this.save) { + // User created System Object + uniqueId = `GGA${foundry.utils.randomID(13)}` + } else { + // System Object imported from GCS/GCA without a UUID + const { name, type, generator } = objProps + const hashKey = `${name}${type}${generator}` + uniqueId = simpleHash(hashKey) + } + return uniqueId + } } export class NamedCost extends Named { @@ -412,45 +443,6 @@ export class Equipment extends Named { } } - /** - * For now, just return the first image found - * In the future, we can implement a better way to find the best image - * - * @return string - * @private - * @param name - */ - findDefaultImage() { - return 'icons/svg/item-bag.svg' - } - - /** - * Generates a unique GGA identifier based on the given system object properties. - * - * @param {Object} objProps - The properties of the object used for generating the unique ID. - * @param {string} objProps.name - The name of the System Object. - * @param {string} objProps.type - The system. of the System Object: equipment, ads, etc. - * @param {string} objProps.generator - The generator of the item: GCS or GCA. - * @return {string} The generated unique GGA identifier. - */ - _getGGAId(objProps) { - let uniqueId - if (!!this.uuid) { - // UUID from GCS/GCA - uniqueId = this.uuid - } else if (!!this.save) { - // User created System Object - uniqueId = `GGA${foundry.utils.randomID(13)}` - } else { - // System Object imported from GCS/GCA without a UUID - const { name, type, generator } = objProps - const hashKey = `${name}${type}${generator}` - const hash = crypto.createHash('md5').update(hashKey).digest('hex') - uniqueId = hash.substring(0, 16) - } - return uniqueId - } - static fromObject(data, actor) { let equip if (data instanceof Equipment) { From 34eda9ad2e4d4887d77823948db5b98779f99878 Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Sat, 7 Sep 2024 18:05:07 -0300 Subject: [PATCH 7/8] feat: Import all Player Data as Foundry Items. Changes: * Add new settings: SETTING_USE_FOUNDRY_ITEMS, SETTING_SHOW_FOUNDRY_GLOBAL_ITEMS * Updated Actor Importer. Now GCA and GCS character sheets imported create Foundry Items on Actor Sheet from equipments, skills, spells and features (GURPS Traits). * Updated Item Sheet. Item sheet now shows an individual experience for items, spells, skills and features. * Updated Actor Sheet: Actor sheet now shows icons for dragged items and child items. Also, added context menu to remove dragged items. --- lang/de.json | 9 +- lang/en.json | 15 +- lang/fr.json | 11 +- lang/pt_br.json | 12 +- lang/ru.json | 5 +- lib/miscellaneous-settings.js | 13 +- lib/moustachewax.js | 60 ++- lib/utilities.js | 35 ++ module/actor/actor-components.js | 467 +++++++++++++++++- module/actor/actor-importer.js | 320 +++++++++--- module/actor/actor-sheet.js | 80 ++- module/actor/actor.js | 310 ++++++++++-- module/item-sheet.js | 51 +- module/item.js | 41 +- styles/apps.css | 39 +- styles/simple.css | 8 + template.json | 99 +++- templates/actor/sections/advantages.hbs | 18 +- templates/actor/sections/equipment.hbs | 7 +- templates/actor/sections/skills.hbs | 14 + templates/actor/sections/spells.hbs | 16 +- templates/import-gcs-v1-data.hbs | 14 + templates/import-gcs-v1-data.html | 9 - .../{item-sheet.html => item/item-sheet.hbs} | 100 +--- templates/item/sections/features.hbs | 30 ++ templates/item/sections/items.hbs | 73 +++ templates/item/sections/skill.hbs | 95 ++++ templates/item/sections/spell.hbs | 162 ++++++ 28 files changed, 1840 insertions(+), 273 deletions(-) create mode 100644 templates/import-gcs-v1-data.hbs delete mode 100644 templates/import-gcs-v1-data.html rename templates/{item-sheet.html => item/item-sheet.hbs} (87%) mode change 100755 => 100644 create mode 100644 templates/item/sections/features.hbs create mode 100644 templates/item/sections/items.hbs create mode 100644 templates/item/sections/skill.hbs create mode 100644 templates/item/sections/spell.hbs diff --git a/lang/de.json b/lang/de.json index 0ee8045ad..2343930e1 100644 --- a/lang/de.json +++ b/lang/de.json @@ -718,8 +718,8 @@ "GURPS.settingHintRemoveUnequipped": "Wenn diese Option aktiviert ist, werden die Namen der Nahkampf- und Fernkampfangriffe mit der Liste der getragenen Ausrüstung verglichen, und wenn eine Namensübereinstimmung gefunden wird, wird der Angriff nur aufgeführt, wenn die Ausrüstung ausgerüstet ist", "GURPS.settingImportBrowserImporter": "Nicht lokal gehosteten Importdialog verwenden", "GURPS.settingImportHintBrowserImporter": "Diese Option aktivieren, wenn die Foundry-Instanz nicht lokal gehosted wird (Z.B. über The Forge). Dieser Importdialog kann sich den Speicherort der Importdatei während der Sitzung merken (d. h., wenn die Figur in derselben Sitzung erneut importiert wird, muss der Dateidialog nicht aufgerufen werden).", - "GURPS.settingUseFoundryItems": "Ausrüstung: Verwende Foundry Items", - "GURPS.settingHintUseFoundryItems": "Wenn diese Option aktiviert ist, wird das System Foundry Items für Ausrüstung erstellen, wenn Charakterbögen importiert werden. Wenn du den Charakterbogen von GCA/GCS zum ersten Mal importierst, wird das System versuchen, jedes Element zu erstellen und dabei die aktuelle Ausrüstungsinformationen auf dem Charakterbogen zu erhalten, einschließlich Name, Bild, Anzahl, Verwendungen und Notizen. Dies kann eine sehr lange Operation sein, insbesondere für GCS-Blätter.", + "GURPS.settingUseFoundryItems": "Verwende Foundry-Items für Ausrüstung", + "GURPS.settingHintUseFoundryItems": "Wenn aktiviert, erstellt das System Foundry-Items für Spielerinventar, Funktionen, Fertigkeiten und Zaubersprüche beim Importieren von Charakterbögen. Wenn du den Charakterbogen von GCA/GCS zum ersten Mal importierst, erstellt das System jedes Item und versucht, die aktuellen Spielerdaten auf dem Akteursbogen zu erhalten, einschließlich Name, Bild, Anzahl, Verwendungen und Notizen. Dies kann eine sehr lange Operation sein, insbesondere für GCS-Blätter.", "GURPS.settingNoEditAllowed": "Ausrüstung bearbeiten nicht erlaubt", "GURPS.settingNoEquipAllowedHint": "Warnung: Du versuchst, eine Ausrüstung zu bearbeiten, die nicht mit einem Foundry-Item verknüpft ist, wenn die Einstellung 'Verwende Foundry-Items für Ausrüstung' aktiv ist. Bitte importiere den Charakterbogen erneut, um das Foundry-Item für diesen Akteur zu erstellen.", "GURPS.settingNoItemAllowedHint": "Warnung: Du versuchst, ein importiertes Item zu bearbeiten, wenn die Einstellung 'Verwende Foundry-Items für Ausrüstung' deaktiviert ist. Bitte importiere den Charakterbogen erneut, um das Equipment für diesen Akteur zu erstellen.", @@ -824,6 +824,9 @@ "GURPS.overridden": "überschrieben", "GURPS.pass": "Passieren", "GURPS.pdfPageReference": "Seiten Ref", + "GURPS.parentItemTooltip": "
Von {type}:
{name}
", + "GURPS.cannotDropItemAlreadyExists": "Du hast diesen Gegenstand bereits.", + "GURPS.droppingItemNotification": "{actorName} erhält {itemName}", "GURPS.pointDamage": "{damage} Punkte Schaden", "GURPS.pointsDamage": "{damage} Punkte Schaden.", @@ -846,6 +849,8 @@ "GURPS.showQuestion": "Zeigen?", "GURPS.skill": "Fertigkeit", "GURPS.skillLevel": "Fertigkeits Level", + "GURPS.skillType": "Fertigkeits Typ", + "GURPS.skillRelativeLevel": "Fertigkeits Level (relativ)", "GURPS.skillsTab": "Fertigkeiten", "GURPS.spell": "Zauberspruch", "GURPS.spellsTab": "Zaubersprüche", diff --git a/lang/en.json b/lang/en.json index 49057de6c..66a9f7601 100755 --- a/lang/en.json +++ b/lang/en.json @@ -116,7 +116,7 @@ "GURPS.spellDuration": "Duration", "GURPS.spellMaintain": "Maintain", "GURPS.spells": "Spells", - "GURPS.spellTime": "Time", + "GURPS.spellTime": "Casting Time", "GURPS.spellResist": "Resisted By", "GURPS.spellDifficulty": "Difficulty", "__Character Equipment__": "=========", @@ -824,6 +824,8 @@ "GURPS.settingHintFlagUserCreated": "If checked, a small icon will appear after user created (not imported) equipment and before user created notes.", "GURPS.settingFlagItems": "Actor: Display Foundry Item Flag", "GURPS.settingHintFlagItems": "If checked, a small icon will appear after equipment (and features) created from Foundry Items", + "GURPS.settingGlobalItems": "Actor: Display Foundry Global Item Flag", + "GURPS.settingHintGlobalItems": "If checked, a small icon will appear after equipment (and features) dropped from Foundry Compendiums", "GURPS.settingQtyItems": "Actor: Display QTY/Count saved Flag", "GURPS.settingHintQtyItems": "If checked, a small icon will appear after equipment where the QTY/Count will be saved during imports", "GURPS.settingConvertRanged": "Actor: Convert 'x2/x5' range to yards", @@ -890,8 +892,8 @@ "GURPS.settingApplyBasedOnTarget": "'Target'", "GURPS.settingTokenOverrideRefresh": "Override Token scaling", "GURPS.settingHintTokenOverrideRefresh": "If \"on\", try to draw tokens to properly fit the hex grid. Overrides Foundry drawing functionality -- turn this off if there's any odd Foundry drawing behavior. Requires reloading the world.", - "GURPS.settingUseFoundryItems": "Use Foundry Items for Equipment", - "GURPS.settingHintUseFoundryItems": "If checked, the system will create Foundry Items for actor equipments when importing character sheets. When you import the character sheet from GCA/GCS for the first time, the system will create each item trying to preserve current equipment info on actor sheet, including name, image, count, uses and notes. This can be a very long operation, especially for GCS sheets.", + "GURPS.settingUseFoundryItems": "Use Foundry Items for Player Data", + "GURPS.settingHintUseFoundryItems": "If checked, the system will create Foundry Items for player inventory, features, skills and spells when importing character sheets. When you import the character sheet from GCA/GCS for the first time, the system will create each item trying to preserve current player data info on actor sheet, including name, image, count, uses and notes. This can be a very long operation, especially for GCS sheets.", "GURPS.settingNoEditAllowed": "No Equipment Editing Allowed", "GURPS.settingNoEquipAllowedHint": "Warning: You're trying to edit an equipment that is not linked to a Foundry item when settings 'Use Foundry Items for Equipment' is active. Please reimport the character sheet to recreate the Foundry item for this actor.", "GURPS.settingNoItemAllowedHint": "Warning: You're trying to edit an imported Item when settings 'Use Foundry Items for Equipment' is disabled. Please reimport the character sheet to recreate the Equipment for this actor.", @@ -1045,6 +1047,7 @@ "GURPS.ok": "OK", "GURPS.overridden": "overridden", "GURPS.pass": "Pass", + "GURPS.parentItemTooltip": "
From {type}:
{name}
", "GURPS.pdfPageReference": "Page Ref", "GURPS.pdfRef": "Ref", "GURPS.pointDamage": "{damage} point of damage", @@ -1068,6 +1071,8 @@ "GURPS.showQuestion": "Show?", "GURPS.skill": "Skill", "GURPS.skillLevel": "Skill Level", + "GURPS.skillType": "Skill Type", + "GURPS.skillRelativeLevel": "Relative Level", "GURPS.skillsTab": "Skills", "GURPS.spell": "Spell", "GURPS.spellsTab": "Spells", @@ -1172,6 +1177,10 @@ "GURPS.pdfOffset": "Page Offset", "GURPS.pdfCode": "PDF Book Code", "GURPS.noViableSkill": "This character does not have a viable skill (effective level 3 or higher) that can be rolled", + "GURPS.droppedItem": "Dragged Item", + "GURPS.cannotDropItemAlreadyExists": "You already have this item.", + "GURPS.droppingItemNotification": "{actorName} gets {itemName}", + "GURPS.OTFormulasEvent": "OTF Formulas", "__Rolling__": "=========", "GURPS.rollVs": "Roll vs", "GURPS.rollNewTarget": "New Target: ({target})", diff --git a/lang/fr.json b/lang/fr.json index 8741f2672..42a3b833f 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -839,8 +839,8 @@ "GURPS.settingHintRemoveUnequipped": "Si coché, les noms des attaques de Mêlée et à Distance seront comparés à la liste des équipements portés, et si une correspondance est trouvée, l'attaque sera seulement listée si l'équipement est équipé", "GURPS.settingImportBrowserImporter": "Utiliser la fenêtre d'import pour hôte distant", "GURPS.settingImportHintBrowserImporter": "Cocher ceci si vous n'hébergez pas votre instance de Foundry localement (vous hébergez à distance, par ex. Forge). Cette fenêtre d'import peut mémoriser l'emplacement de vos fichiers d'import pendant la session (cela signifie que si vous importez de nouveau le personnage dans la même session, la fenêtre ne se rouvrira pas).", - "GURPS.settingUseFoundryItems": "Utiliser les Objets Foundry pour l'Equipement", - "GURPS.settingHintUseFoundryItems": "Si coché, le système créera des Objets Foundry pour l'équipement de l'acteur lors de l'importation des feuilles de personnage. Lorsque vous importez la feuille de personnage de GCA/GCS pour la première fois, le système créera chaque objet en essayant de préserver les informations d'équipement actuelles sur la feuille de personnage, y compris le nom, l'image, le compte, les utilisations et les notes. Cela peut être une opération très longue, surtout pour les feuilles GCS.", + "GURPS.settingUseFoundryItems": "Utiliser les Objets Foundry pour les Données Joueurs", + "GURPS.settingHintUseFoundryItems": "Si coché, le système créera des Objets Foundry pour l'inventaire, les traits, les compétences et les sorts des joueurs lors de l'importation des feuilles de personnage. Lorsque vous importez la feuille de personnage de GCA/GCS pour la première fois, le système créera chaque objet en essayant de préserver les informations actuelles des données des joueurs sur la feuille de personnage, y compris le nom, l'image, le compte, les utilisations et les notes. Cela peut être une opération très longue, surtout pour les feuilles GCS.", "GURPS.settingNoEditAllowed": "Pas d'édition d'équipement autorisée", "GURPS.settingNoEquipAllowedHint": "Attention: Vous essayez d'éditer un équipement qui n'est pas lié à un objet Foundry lorsque les paramètres 'Utiliser les Objets Foundry pour l'Equipement' sont actifs. Veuillez réimporter la feuille de personnage pour créer l'objet Foundry pour cet acteur.", "GURPS.settingNoItemAllowedHint": "Attention: Vous essayez d'éditer un équipement importé lorsque les paramètres 'Utiliser les Objets Foundry pour l'Equipement' sont désactivés. Veuillez réimporter la feuille de personnage pour recréer l'équipement pour cet acteur.", @@ -1025,6 +1025,8 @@ "GURPS.showQuestion": "Montrer?", "GURPS.skill": "Compétence", "GURPS.skillLevel": "Niveau Compétence", + "GURPS.skillType": "Type Compétence", + "GURPS.skillRelativeLevel": "Niveau Compétence Relatif", "GURPS.skillsTab": "Compétences", "GURPS.spell": "Sort", "GURPS.spellsTab": "Sorts", @@ -1110,5 +1112,8 @@ "GURPS.CR12": "PC: 12 (Résiste Très Souvent)", "GURPS.CR15": "PC: 15 (Résiste Quasi-Systématiquement)", "GURPS.modifiersBlindAttack": "-10 pour toucher (Aveugle)", - "GURPS.modifiersBlindDefend": "-4 aux Défenses Actives (Aveugle)" + "GURPS.modifiersBlindDefend": "-4 aux Défenses Actives (Aveugle)", + "GURPS.parentItemTooltip": "De {type}: {name}", + "GURPS.cannotDropItemAlreadyExists": "Vous avez déjà cet objet.", + "GURPS.droppingItemNotification": "{actorName} reçoit {itemName}" } diff --git a/lang/pt_br.json b/lang/pt_br.json index 096928bcd..2cafe12cb 100644 --- a/lang/pt_br.json +++ b/lang/pt_br.json @@ -117,7 +117,7 @@ "GURPS.spellDuration": "Duração", "GURPS.spellMaintain": "Manutenção", "GURPS.spells": "Mágicas", - "GURPS.spellTime": "Tempo", + "GURPS.spellTime": "Tempo de Lançamento", "GURPS.spellResist": "Resistido Por", "GURPS.spellDifficulty": "Dificuldade", "__Character Equipment__": "=========", @@ -860,8 +860,8 @@ "GURPS.settingApplyBasedOnTarget": "'Alvo'", "GURPS.settingTokenOverrideRefresh": "Desconsiderar dimensionamento de miniatura", "GURPS.settingHintTokenOverrideRefresh": "Se estiver \"ativado\", tentará desenhar as miniaturas para que se encaixem adequadamente na grade hexagonal. Ignora as funções de desenho do Foundry -- desative esta opção se houver qualquer comportamento inadequado nos desenhos do Foundry. Será necessário recarregar o mundo.", - "GURPS.settingUseFoundryItems": "Usar Itens do Foundry para Equipamentos", - "GURPS.settingHintUseFoundryItems": "Se marcado, o sistema criará Itens do Foundry para os equipamentos do ator durante a importação de planilhas de personagem. Quando você importar a planilha de personagem do GCA/GCS pela primeira vez, o sistema criará cada item tentando preservar as informações de equipamento atuais na planilha do ator, incluindo nome, imagem, contagem, usos e notas. Isto pode ser uma operação muito longa, especialmente para planilhas GCS.", + "GURPS.settingUseFoundryItems": "Usar Itens do Foundry para dados do Jogador", + "GURPS.settingHintUseFoundryItems": "Se marcado, o sistema criará Itens do Foundry para o inventário, características, perícias e mágicas dos jogadores ao importar planilhas de personagem. Quando você importar a planilha de personagem do GCA/GCS pela primeira vez, o sistema criará cada item tentando preservar as informações atuais dos dados do jogador na planilha de ator, incluindo nome, imagem, contagem, usos e notas. Isto pode ser uma operação muito longa, especialmente para planilhas do GCS.", "GURPS.settingNoEditAllowed": "Edição de Equipamento Não Permitida", "GURPS.settingNoEquipAllowedHint": "Aviso: Você está tentando editar um equipamento que não está vinculado a um item do Foundry quando a configuração 'Usar Itens do Foundry para Equipamentos' está ativa. Por favor, reimporte a planilha de personagem para criar o item do Foundry para este ator.", "GURPS.settingNoItemAllowedHint": "Aviso: Você está tentando editar um Item importado quando a configuração 'Usar Itens do Foundry para Equipamentos' está desativada. Por favor, reimporte a planilha de personagem para recriar o Equipamento para este ator.", @@ -1036,6 +1036,8 @@ "GURPS.showQuestion": "Exibir?", "GURPS.skill": "Perícia", "GURPS.skillLevel": "Nível de Habilidade", + "GURPS.skillType": "Tipo de Perícia", + "GURPS.skillRelativeLevel": "NH Relativo", "GURPS.skillsTab": "Perícias", "GURPS.spell": "Mágica", "GURPS.spellsTab": "Mágicas", @@ -1136,6 +1138,10 @@ "GURPS.written": "Escrito", "GURPS.pdfOffset": "Ajuste de página", "GURPS.pdfCode": "Código do PDF", + "GURPS.parentItemTooltip": "
De {type}:
{name}
", + "GURPS.droppedItem": "Item Obtido", + "GURPS.cannotDropItemAlreadyExists": "Você já possui este item.", + "GURPS.droppingItemNotification": "{actorName} obteve {itemName}", "__Rolling__": "=========", "GURPS.rollVs": "Rolagem contra", "GURPS.rollNewTarget": "Novo alvo: ({target})", diff --git a/lang/ru.json b/lang/ru.json index 0bd990111..203d7dd80 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -447,5 +447,8 @@ "GURPS.settingNoItemAllowedHint": "Предупреждение: Вы пытаетесь отредактировать импортированный предмет, когда активированы настройки 'Использовать Foundry Items для снаряжения'. Пожалуйста, перимпортируйте лист персонажа, чтобы создать снаряжение для этого актёра.", "GURPS.settingShowDebugInfo": "Показать отладочную информацию", "GURPS.settingHintShowDebugInfo": "Для диалогов документов (Актёры, Предметы и т.д.) показывать значок отладки в заголовке окна. При нажатии он отобразит данные документа в диалоге.", - "GURPS.settingShowDebugTooltip": "Показать отладочную информацию" + "GURPS.settingShowDebugTooltip": "Показать отладочную информацию", + "GURPS.parentItemTooltip": "
Из {type}:
{name}
", + "GURPS.cannotDropItemAlreadyExists": "У вас уже есть этот предмет.", + "GURPS.droppingItemNotification": "{actorName} получил {itemName}" } diff --git a/lib/miscellaneous-settings.js b/lib/miscellaneous-settings.js index d39804094..1d0da22e7 100644 --- a/lib/miscellaneous-settings.js +++ b/lib/miscellaneous-settings.js @@ -67,6 +67,7 @@ export const SETTING_CTRL_KEY = 'ctrl-key' export const SETTING_USE_ON_TARGET = 'use-on-target' export const SETTING_USE_FOUNDRY_ITEMS = 'use-foundry-items' export const SETTING_SHOW_DEBUG_INFO = 'show-debug-info' +export const SETTING_SHOW_FOUNDRY_GLOBAL_ITEMS = 'show-foundry-global-items' export const VERSION_096 = SemanticVersion.fromString('0.9.6') export const VERSION_097 = SemanticVersion.fromString('0.9.7') @@ -84,7 +85,7 @@ export function initializeSettings() { scope: 'client', config: true, type: Boolean, - default: false, + default: true, requiresReload: true, onChange: value => console.log(`Show Debug Info for Documents: ${value}`), }) @@ -342,6 +343,16 @@ export function initializeSettings() { onChange: value => console.log(`Show a 'star' icon for Foundry items : ${value}`), }) + game.settings.register(SYSTEM_NAME, SETTING_SHOW_FOUNDRY_GLOBAL_ITEMS, { + name: i18n('GURPS.settingGlobalItems'), + hint: i18n('GURPS.settingHintGlobalItems'), + scope: 'world', + config: true, + type: Boolean, + default: true, + onChange: value => console.log(`Show a 'globe' icon for Foundry items : ${value}`), + }) + game.settings.register(SYSTEM_NAME, SETTING_ignoreImportQty, { name: i18n('GURPS.settingQtyItems'), hint: i18n('GURPS.settingHintQtyItems'), diff --git a/lib/moustachewax.js b/lib/moustachewax.js index f03735b21..ffe5ff971 100755 --- a/lib/moustachewax.js +++ b/lib/moustachewax.js @@ -609,15 +609,17 @@ export default function () { Handlebars.registerHelper('threshold-of', function (thresholds, max, value) { // return the index of the threshold that the value falls into let result = null - thresholds.some(function ( - /** @type {{ operator: string; comparison: string; value: number; }} */ threshold, - /** @type {number} */ index - ) { - let op = getOperation(threshold.operator) - let comparison = getComparison(threshold.comparison) - let testValue = op(max, threshold.value) - return comparison(value, testValue) ? ((result = index), true) : false - }) + thresholds.some( + function ( + /** @type {{ operator: string; comparison: string; value: number; }} */ threshold, + /** @type {number} */ index + ) { + let op = getOperation(threshold.operator) + let comparison = getComparison(threshold.comparison) + let testValue = op(max, threshold.value) + return comparison(value, testValue) ? ((result = index), true) : false + } + ) return result }) @@ -723,6 +725,42 @@ export default function () { return game.settings.get(settings.SYSTEM_NAME, settings.SETTING_SHOW_FOUNDRY_CREATED) && !!obj.itemid }) + Handlebars.registerHelper('isFoundryGlobalItem', function (obj, doc) { + let item + const actor = doc.data?.root?.document + item = actor?.items?.get(obj.itemid) + return ( + game.settings.get(settings.SYSTEM_NAME, settings.SETTING_SHOW_FOUNDRY_GLOBAL_ITEMS) && !!item?.system.globalid + ) + }) + + Handlebars.registerHelper('isImportedItem', function (obj) { + return !!obj.fromItem + }) + + Handlebars.registerHelper('parentItemTooltip', function (obj, doc) { + // Find parent item using fromItem + const actor = doc.data?.root?.document + let parentItem = actor?.items?.get(obj.fromItem) + return !!parentItem + ? new Handlebars.SafeString( + game.i18n.format('GURPS.parentItemTooltip', { name: parentItem.name, ['type']: parentItem.type }) + ) + : '' + }) + + Handlebars.registerHelper('globalItemTooltip', function (obj, doc) { + // Find global item using globalid + const actor = doc.data?.root?.document + let item = actor?.items?.get(obj.itemid) + if (!!item?.system.globalid) item = game.items.find(it => it.id === item.system.globalid.split('.').pop()) + return !!item + ? new Handlebars.SafeString( + game.i18n.format('GURPS.parentItemTooltip', { name: item.name, ['type']: `game ${item.type}` }) + ) + : game.i18n.localize('GURPS.droppedItem') + }) + Handlebars.registerHelper('ignoreImportQty', function (obj) { return game.settings.get(settings.SYSTEM_NAME, settings.SETTING_ignoreImportQty) && !!obj.ignoreImportQty }) @@ -854,6 +892,10 @@ ${content} 'systems/gurps/templates/actor/sections/speed-range-table.hbs', 'systems/gurps/templates/actor/sections/spells.hbs', 'systems/gurps/templates/actor/sections/trackers.hbs', + 'systems/gurps/templates/item/sections/items.hbs', + 'systems/gurps/templates/item/sections/features.hbs', + 'systems/gurps/templates/item/sections/skill.hbs', + 'systems/gurps/templates/item/sections/spell.hbs', ] templates.forEach(filename => { diff --git a/lib/utilities.js b/lib/utilities.js index feb3f5600..3290c5dca 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -628,3 +628,38 @@ export function requestFpHp(resp) { } }) } + +/** + * Compares two arrays for equality. + * + * @param {Array} arr1 - The first array to compare. + * @param {Array} arr2 - The second array to compare. + * @return {boolean} - Returns true if both arrays are equal, otherwise false. + */ +export const arraysEqual = (arr1, arr2) => { + if (arr1.length !== arr2.length) return false + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false + } + return true +} + +/** + * Compares two college lists to determine if they are identical. + * + * GCA import Spell Colleges as string. Ex. Fire, Water + * GCS import Spell Colleges as arrays. Ex. ['Fire', 'Water'] + * + * @param {Array|string} a - The first college list to compare. + * @param {Array|string} b - The second college list to compare. + * @returns {boolean} Returns true if both inputs represent the same list of colleges; otherwise, returns false. + */ +export const compareColleges = (a, b) => { + if (!Array.isArray(a)) { + a = a.split(',') + } + if (!Array.isArray(b)) { + b = b.split(',') + } + return arraysEqual(a, b) +} diff --git a/module/actor/actor-components.js b/module/actor/actor-components.js index 547b87128..116ef0e4b 100644 --- a/module/actor/actor-components.js +++ b/module/actor/actor-components.js @@ -6,10 +6,41 @@ * to think really hard about potentially moving the class back to actor.js. */ -import { convertRollStringToArrayOfInt, extractP } from '../../lib/utilities.js' +import { arraysEqual, compareColleges, convertRollStringToArrayOfInt, extractP } from '../../lib/utilities.js' import * as Settings from '../../lib/miscellaneous-settings.js' import { simpleHash } from '../../lib/simple-hash.js' +/** + * ### Base Actor Component + * + * Originally, these entities are where Actor stores the GURPS data. + * + * But naming is hard, and let's try to make it easier to understand, + * especially now, when we will store these data not only on these components + * but also on new Foundry Items besides the original `Equipment` actor/item. + * + * The biggest trap at this point is the naming of GURPS Traits. In GURPS, **Traits** + * are the Advantages, Disadvantages, Quirks and Perks. And **Attributes** + * are the player attributes (ST, DX, IQ, HT, dodge, etc.). + * + * But on GGA, all Traits are stored inside actor in the _ads_ key. And the + * attributes are scattered in many keys inside the actor, especially + * _attributes_ and... _traits_ :) + * + * So, let's try to name things: + * + * | GURPS terms | actor.system. | Actor Component | Foundry Item Type | item.system. | + * |-------------|----------------------------|-----------------------------|-------------------|-------------------| + * | Inventory | equipment | Equipment | equipment | eqt | + * | Attributes | attributes, traits, etc. | Encumbrance, Reaction, etc. | -- | -- | + * | Traits | ads | Advantage | feature | fea | + * | Skills | skills | Skill | skill | ski | + * | Spells | spells | Spell | spell | spl | + * | Melee Att. | melee | Melee | -- | -- | + * | Ranged Att. | ranged | Ranged | -- | -- | + * + * + */ export class _Base { constructor() { this.notes = '' @@ -75,8 +106,20 @@ export class Named extends _Base { } } + /** + * Retrieves the default image path for item. + * + * @return {string} The path to the default image. + */ findDefaultImage() { - return 'icons/svg/item-bag.svg' + let item + if (!!this.itemid) { + item = game.items.get(this.itemid) + if (!!item?.system.globalid) { + item = game.items.get(item.system.globalid.split('.').pop()) + } + } + return item?.img } /** @@ -93,10 +136,12 @@ export class Named extends _Base { if (!!this.uuid) { // UUID from GCS/GCA uniqueId = this.uuid - } else if (!!this.save) { + } + if (!!this.save && !uniqueId) { // User created System Object uniqueId = `GGA${foundry.utils.randomID(13)}` - } else { + } + if (!uniqueId) { // System Object imported from GCS/GCA without a UUID const { name, type, generator } = objProps const hashKey = `${name}${type}${generator}` @@ -104,6 +149,87 @@ export class Named extends _Base { } return uniqueId } + + /** + * Get Actor Component System Key. + * + * One of the **trickiest** parts of this project. + * Remember, actor component has a different key than his item. + * + * Examples: + * actor.system.equipment <-> item.system.eqt + * actor.system.ads <-> item.system.fea + * + * @return {string} the actor.system. for the item class + */ + static get actorSystemKey() { + throw new Error('Not implemented') + } + + /** + * Converts Actor Component data into Foundry Item Data. + * + * @param {string} fromProgram - The Generator, CGA or GCS. + * @return {object} The converted item data. + */ + toItemData(fromProgram = '') { + throw new Error('Not implemented') + } + + /** + * Converts Received Data into Actor Component. + * + * The Object.assign is a very simple implementation + * To make sure we are not missing any property, we should + * implement a more robust method on each class. + * + * @param {object} data - The data to convert (from form, item data, drag info, etc.). + * @param {Actor} actor - The actor to use. + */ + static fromObject(data, actor) { + // Just an example. Make sure you make a more robust method in the subclass. + let actorComp = new this(data.name) + Object.assign(actorComp, data) + return this._checkComponentInActor(actor, actorComp) + } + + static _checkComponentInActor(actor, actorComp) { + // This actor component already exists in Actor? + const existingComponentKey = + actorComp instanceof Equipment + ? actor._findEqtkeyForId('uuid', actorComp.uuid) + : actor._findSysKeyForId('uuid', actorComp.uuid, this.actorSystemKey) + if (!!existingComponentKey) { + const existingComponentItem = actor.items.get(actorComp.itemid) + if (!!existingComponentItem) { + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + actorComp.itemid = existingComponentItem.itemid || '' + } + actorComp.itemInfo = actorComp.itemInfo || !!existingComponentItem ? existingComponentItem.getItemInfo() : {} + } else { + actorComp.itemid = '' + actorComp.itemInfo = {} + } + } + return actorComp + } + + /** + * Checks if the given item needs an update. + * + * Another trickier part, because payloads from GCA and GCS uses different + * formats for the same fields. + * + * Some examples: spell `college`, skill `import` and `level` etc. + * + * This must be implemented on each subclass. + * + * @param {Object} item - The item to check for an update. + * @return {boolean} - Returns true if the item needs an update, otherwise false. + */ + _itemNeedsUpdate(item) { + throw new Error('Not implemented') + } } export class NamedCost extends Named { @@ -113,15 +239,26 @@ export class NamedCost extends Named { constructor(n1) { super(n1) this.points = 0 + this.save = false + this.itemid = '' + this.itemInfo = {} + this.fromItem = '' } } const _AnimationMixin = { + _otf: '', _checkotf: '', _duringotf: '', _passotf: '', _failotf: '', + get otf() { + return this._otf + }, + set otf(value) { + this._otf = value + }, get checkotf() { return this._checkotf }, @@ -182,6 +319,93 @@ export class Skill extends Leveled { this.type = '' // "DX/E"; this.relativelevel = '' // "DX+1"; } + + static get actorSystemKey() { + return 'skills' + } + + toItemData(fromProgram = '') { + const system = this.itemInfo?.system || {} + const uniqueId = this._getGGAId({ name: this.name, type: 'skill', generator: fromProgram }) + const importId = !this.save ? uniqueId : '' + const importFrom = this.importFrom || fromProgram + return { + name: this.name, + img: this.itemInfo?.img || this.findDefaultImage(), + type: 'skill', + system: { + ski: { + notes: this.notes || '', + pageref: this.pageref || '', + contains: this.contains || {}, + uuid: uniqueId, + parentuuid: this.parentuuid || '', + points: this.points || 0, + ['import']: this['import'] || '', + level: this.level || 0, + relativelevel: this.relativelevel || '', + name: this.name, + ['type']: this['type'] || '', + otf: this.otf || '', + checkotf: this.checkotf || '', + duringotf: this.duringotf || '', + passotf: this.passotf || '', + failotf: this.failotf || '', + }, + ads: system.ads || {}, + skills: system.skills || {}, + spells: system.spells || {}, + melee: system.melee || {}, + ranged: system.ranged || {}, + bonuses: system.bonuses || '', + globalid: system.globalid || '', + importid: importId, + importFrom: importFrom, + fromItem: this.fromItem || '', + }, + } + } + static fromObject(data, actor) { + let skill + if (data instanceof Skill) { + skill = data + } else { + skill = new Skill(data.name) + skill.notes = data.notes + skill.contains = data.contains || {} + skill.uuid = data.uuid + skill.parentuuid = data.parentuuid + skill.points = data.points + skill['import'] = data['import'] + skill.level = data.level + skill.relativelevel = data.relativelevel + skill['type'] = data['type'] + } + return this._checkComponentInActor(actor, skill) + } + _itemNeedsUpdate(item) { + let result = false + if (!item) { + result = true + console.log(`Foundry Item: ${this.name} does not exist`) + } else { + const itemData = item.system[item.itemSysKey] + result = + itemData.notes !== this.notes || + itemData.pageref !== this.pageref || + !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) || + itemData.points !== this.points || + itemData.import !== this['import'] || + itemData.relativelevel !== this.relativelevel || + itemData.name !== this.name || + itemData['type'] !== this['type'] + if (!!result) console.log(`Foundry Item: ${this.name} needs update`) + } + return result + } + findDefaultImage() { + return super.findDefaultImage() || 'icons/svg/dice-target.svg' + } } export class Spell extends Leveled { @@ -201,6 +425,115 @@ export class Spell extends Leveled { this.difficulty = '' this.relativelevel = '' // "IQ+1" } + static get actorSystemKey() { + return 'spells' + } + toItemData(fromProgram = '') { + const system = this.itemInfo?.system || {} + const uniqueId = this._getGGAId({ name: this.name, type: 'spell', generator: fromProgram }) + const importId = !this.save ? uniqueId : '' + const importFrom = this.importFrom || fromProgram + return { + name: this.name, + img: this.itemInfo?.img || this.findDefaultImage(), + type: 'spell', + system: { + spl: { + notes: this.notes || '', + pageref: this.pageref || '', + contains: this.contains || {}, + uuid: uniqueId, + parentuuid: this.parentuuid || '', + points: this.points || 0, + ['import']: this['import'] || '', + level: this.level || 0, + relativelevel: this.relativelevel || '', + name: this.name, + ['class']: this['class'] || '', + college: this.college || '', + cost: this.cost || '', + maintain: this.maintain || '', + duration: this.duration || '', + resist: this.resist || '', + casttime: this.casttime || '', + difficulty: this.difficulty || '', + otf: this.otf || '', + checkotf: this.checkotf || '', + duringotf: this.duringotf || '', + passotf: this.passotf || '', + failotf: this.failotf || '', + }, + ads: system.ads || {}, + skills: system.skills || {}, + spells: system.spells || {}, + melee: system.melee || {}, + ranged: system.ranged || {}, + bonuses: system.bonuses || '', + globalid: system.globalid || '', + importid: importId, + importFrom: importFrom, + fromItem: this.fromItem || '', + }, + } + } + static fromObject(data, actor) { + let spell + if (data instanceof Spell) { + spell = data + } else { + spell = new Spell(data.name) + spell.notes = data.notes + spell.pageref = data.pageref + spell.contains = data.contains || {} + spell.uuid = data.uuid + spell.parentuuid = data.parentuuid + spell.points = data.points + spell['import'] = data['import'] || '' + spell.level = data.level + spell.relativelevel = data.relativelevel + spell['class'] = data['class'] + spell.college = data.college + spell.cost = data.cost + spell.maintain = data.maintain + spell.duration = data.duration + spell.resist = data.resist + spell.casttime = data.casttime + spell.difficulty = data.difficulty + } + return this._checkComponentInActor(actor, spell) + } + + _itemNeedsUpdate(item) { + let result = false + if (!item) { + result = true + console.log(`Foundry Item: ${this.name} does not exist`) + } else { + const itemData = item.system[item.itemSysKey] + result = + itemData.notes !== this.notes || + itemData.pageref !== this.pageref || + !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) || + itemData.points !== this.points || + parseInt(itemData['import'] || 0) !== parseInt(this['import'] || 0) || + itemData.level !== this.level || + itemData.relativelevel !== this.relativelevel || + itemData.name !== this.name || + itemData.class !== this.class || + !compareColleges(itemData['college'], this['college']) || + itemData.cost !== this.cost || + itemData.maintain !== this.maintain || + itemData.duration !== this.duration || + itemData.resist !== this.resist || + itemData.casttime !== this.casttime || + itemData.difficulty !== this.difficulty + if (!!result) console.log(`Foundry Item: ${this.name} needs update`) + } + return result + } + findDefaultImage() { + return super.findDefaultImage() || 'icons/svg/daze.svg' + } } export class Advantage extends NamedCost { @@ -212,6 +545,91 @@ export class Advantage extends NamedCost { this.userdesc = '' this.note = '' // GCS has notes (note) and userdesc for an advantage, so the import code combines note and userdesc into notes } + + static get actorSystemKey() { + return 'ads' + } + + /** + * Create new Feature payload using Advantage's data. + */ + toItemData(fromProgram = '') { + const system = this.itemInfo?.system || {} + const uniqueId = this._getGGAId({ name: this.name, type: 'feature', generator: fromProgram }) + const importId = !this.save ? uniqueId : '' + const importFrom = this.importFrom || fromProgram + return { + name: this.name, + img: this.itemInfo?.img || this.findDefaultImage(), + type: 'feature', + system: { + fea: { + notes: this.notes || '', + pageref: this.pageref || '', + contains: this.contains || {}, + uuid: uniqueId, + parentuuid: this.parentuuid || '', + points: this.points || 0, + userdesc: this.userdesc || '', + note: this.note || '', + name: this.name, + checkotf: this.checkotf || '', + duringotf: this.duringotf || '', + passotf: this.passotf || '', + failotf: this.failotf || '', + }, + ads: system.ads || {}, + skills: system.skills || {}, + spells: system.spells || {}, + melee: system.melee || {}, + ranged: system.ranged || {}, + bonuses: system.bonuses || '', + globalid: system.globalid || '', + importid: importId, + importFrom: importFrom, + fromItem: this.fromItem || '', + }, + } + } + static fromObject(data, actor) { + let adv + if (data instanceof Advantage) { + adv = data + } else { + adv = new Advantage(data.name) + adv.notes = data.notes + adv.pageref = data.pageref + adv.contains = data.contains || {} + adv.uuid = data.uuid + adv.parentuuid = data.parentuuid + adv.points = data.points + adv.userdesc = data.userdesc + adv.note = data.note + } + return this._checkComponentInActor(actor, adv) + } + _itemNeedsUpdate(item) { + let result = false + if (!item) { + result = true + console.log(`Foundry Item: ${this.name} does not exist`) + } else { + const itemData = item.system[item.itemSysKey] + result = + itemData.notes !== this.notes || + (itemData.pageref || '') !== (this.pageref || '') || + !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) || + itemData.points !== this.points || + (itemData.userdesc || '') !== (this.userdesc || '') || + itemData.note !== this.note || + itemData.name !== this.name + if (!!result) console.log(`Foundry Item: ${this.name} needs update`) + } + return result + } + findDefaultImage() { + return super.findDefaultImage() || 'icons/svg/book.svg' + } } export class Attack extends Named { @@ -436,9 +854,10 @@ export class Equipment extends Named { bonuses: system.bonuses || '', equipped: this.equipped, carried: this.carried, - globalid: this.globalid || '', + globalid: system.globalid || '', importid: importId, importFrom: importFrom, + fromItem: this.fromItem || '', }, } } @@ -466,17 +885,37 @@ export class Equipment extends Named { equip.notes = data.notes equip.pageref = data.pageref equip.ignoreImportQty = data.ignoreImportQty + equip.contains = data.contains || {} } - // This equipment already exists in Actor? - const existingEquipmentKey = actor._findEqtkeyForId('uuid', equip.uuid) - if (!!existingEquipmentKey) { - const existingEquipment = foundry.utils.getProperty(actor, existingEquipmentKey) - if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { - equip.itemid = existingEquipment.itemid || '' - } - equip.itemInfo = existingEquipment.itemInfo || {} + return this._checkComponentInActor(actor, equip) + } + _itemNeedsUpdate(item) { + let result = false + if (!item) { + result = true + console.log(`Foundry Item: ${this.name} does not exist`) + } else { + const itemData = item.system[item.itemSysKey] + result = + itemData.notes !== this.notes || + itemData.pageref !== this.pageref || + itemData.cost !== this.cost || + itemData.weight !== this.weight || + itemData.techlevel !== this.techlevel || + itemData.categories !== this.categories || + itemData.legalityclass !== this.legalityclass || + itemData.costsum !== this.costsum || + itemData.maxuses !== this.maxuses || + itemData.uuid !== this.uuid || + itemData.parentuuid !== this.parentuuid || + itemData.location !== this.location || + !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) + if (!!result) console.log(`Foundry Item: ${this.name} needs update`) } - return equip + return result + } + findDefaultImage() { + return super.findDefaultImage() || 'icons/svg/item-bag.svg' } } diff --git a/module/actor/actor-importer.js b/module/actor/actor-importer.js index 2bc0f2b76..33be8a062 100644 --- a/module/actor/actor-importer.js +++ b/module/actor/actor-importer.js @@ -73,9 +73,10 @@ export class ActorImporter { new Dialog( { title: `Import character data for: ${this.actor.name}`, - content: await renderTemplate('systems/gurps/templates/import-gcs-v1-data.html', { - name: '"' + this.actor.name + '"', - }), + content: await renderTemplate( + 'systems/gurps/templates/import-gcs-v1-data.hbs', + SmartImporter.getTemplateOptions(this.actor) + ), buttons: { import: { icon: '', @@ -159,6 +160,11 @@ export class ActorImporter { let starttime = performance.now() let commit = {} + if ( + !!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS) || + this.actor.items.filter(i => !!i.system.importid).length > 10 + ) + loadingDialog = await this._showLoadingDialog({ name: nm, generator: 'GCS' }) commit = { ...commit, ...{ 'system.lastImport': new Date().toString().split(' ').splice(1, 4).join(' ') } } let ar = this.actor.system.additionalresources || {} ar.importname = importname || ar.importname @@ -171,14 +177,9 @@ export class ActorImporter { ...commit, ...this.importSizeFromGCS(commit, r.profile, r.traits || r.advantages, r.skills, r.equipment), } - commit = { ...commit, ...this.importAdsFromGCS(r.traits || r.advantages) } - commit = { ...commit, ...this.importSkillsFromGCS(r.skills) } - commit = { ...commit, ...this.importSpellsFromGCS(r.spells) } - if ( - !!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS) || - this.actor.items.filter(i => !!i.system.importid).length > 10 - ) - loadingDialog = await this._showLoadingDialog({ name: nm, generator: 'GCS' }) + commit = { ...commit, ...(await this.importAdsFromGCS(r.traits || r.advantages)) } + commit = { ...commit, ...(await this.importSkillsFromGCS(r.skills)) } + commit = { ...commit, ...(await this.importSpellsFromGCS(r.spells)) } commit = { ...commit, ...(await this.importEquipmentFromGCS(r.equipment, r.other_equipment)) } commit = { ...commit, ...this.importNotesFromGCS(r.notes) } @@ -236,6 +237,20 @@ export class ActorImporter { await this.actor.update({ name: nm, 'token.name': nm }) } + // For each saved item with global id, lets run their additions + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + for (let key of ['ads', 'skills', 'spells']) { + await aRecurselist(this.actor.system[key], async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i.system.globalid) { + await this.actor._addItemAdditions(i, '') + } + } + }) + } + } + if (!suppressMessage) ui.notifications?.info(i18n_f('GURPS.importSuccessful', { name: nm })) console.log( 'Done importing (' + @@ -409,24 +424,28 @@ export class ActorImporter { let loadingDialog let importResult = false try { + if ( + !!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS) || + this.actor.items.filter(i => !!i.system.importid).length > 10 + ) + loadingDialog = await this._showLoadingDialog({ name: nm, generator: 'GCA' }) // This is going to get ugly, so break out various data into different methods commit = { ...commit, ...(await this.importAttributesFromGCA(c.attributes)) } - commit = { ...commit, ...this.importSkillsFromGCA(c.abilities?.skilllist) } + commit = { ...commit, ...(await this.importSkillsFromGCA(c.abilities?.skilllist)) } commit = { ...commit, ...this.importTraitsfromGCA(c.traits) } commit = { ...commit, ...this.importCombatMeleeFromGCA(c.combat?.meleecombatlist) } commit = { ...commit, ...this.importCombatRangedFromGCA(c.combat?.rangedcombatlist) } - commit = { ...commit, ...this.importSpellsFromGCA(c.abilities?.spelllist) } + commit = { ...commit, ...(await this.importSpellsFromGCA(c.abilities?.spelllist)) } commit = { ...commit, ...this.importLangFromGCA(c.traits?.languagelist) } - commit = { ...commit, ...this.importAdsFromGCA(c.traits?.adslist, c.traits?.disadslist) } + commit = { ...commit, ...(await this.importAdsFromGCA(c.traits?.adslist, c.traits?.disadslist)) } commit = { ...commit, ...this.importReactionsFromGCA(c.traits?.reactionmodifiers, vernum) } commit = { ...commit, ...this.importEncumbranceFromGCA(c.encumbrance) } commit = { ...commit, ...this.importPointTotalsFromGCA(c.pointtotals) } commit = { ...commit, ...this.importNotesFromGCA(c.description, c.notelist) } - if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) - loadingDialog = await this._showLoadingDialog({ name: nm, generator: 'GCA' }) commit = { ...commit, ...(await this.importEquipmentFromGCA(c.inventorylist)) } commit = { ...commit, ...(await this.importProtectionFromGCA(c.combat?.protectionlist)) } } catch (err) { + throw err console.log(err.stack) let msg = i18n_f('GURPS.importGenericError', { name: nm, error: err.name, message: err.message }) let content = await renderTemplate('systems/gurps/templates/chat-import-actor-errors.html', { @@ -464,6 +483,20 @@ export class ActorImporter { await this.actor.update({ name: nm, 'token.name': nm }) } + // For each saved item with global id, lets run their additions + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + for (let key of ['ads', 'skills', 'spells']) { + await aRecurselist(this.actor.system[key], async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i.system.globalid) { + await this.actor._addItemAdditions(i, '') + } + } + }) + } + } + if (!suppressMessage) ui.notifications?.info(i18n_f('GURPS.importSuccessful', { name: nm })) console.log( 'Done importing (' + @@ -476,7 +509,7 @@ export class ActorImporter { console.log(err.stack) let msg = [i18n_f('GURPS.importGenericError', { name: nm, error: err.name, message: err.message })] if (err.message == 'Maximum depth exceeded') msg.push(i18n('GURPS.importTooManyContainers')) - if (!supressMessage) ui.notifications?.warn(msg.join('
')) + ui.notifications?.warn(msg.join('
')) // FIXME: Why suppressMessage is not available here? let content = await renderTemplate('systems/gurps/templates/chat-import-actor-errors.html', { lines: msg, version: version, @@ -654,11 +687,27 @@ export class ActorImporter { * @param {{ [key: string]: any }} adsjson * @param {{ [key: string]: any }} disadsjson */ - importAdsFromGCA(adsjson, disadsjson) { + async importAdsFromGCA(adsjson, disadsjson) { /** @type {Advantage[]} */ + if (!!adsjson || !!disadsjson) await this._preImport('GCA', 'feature') let list = [] - this.importBaseAdvantagesFromGCA(list, adsjson) - this.importBaseAdvantagesFromGCA(list, disadsjson) + await this.importBaseAdvantagesFromGCA(list, adsjson) + await this.importBaseAdvantagesFromGCA(list, disadsjson) + + // Find all Features with globalId + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await aRecurselist(this.actor.system.ads, async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i?.system.globalid) { + if (!(t instanceof Advantage)) t = Advantage.fromObject(t, this.actor) + t = await this._processItemFrom(t, 'GCA') + list.push(t) + } + } + }) + } + return { 'system.-=ads': null, 'system.ads': this.foldList(list), @@ -669,7 +718,7 @@ export class ActorImporter { * @param {Advantage[]} datalist * @param {{ [key: string]: any }} json */ - importBaseAdvantagesFromGCA(datalist, json) { + async importBaseAdvantagesFromGCA(datalist, json) { if (!json) return let t = this.textFrom /// shortcut to make code smaller for (let key in json) { @@ -685,6 +734,7 @@ export class ActorImporter { a.parentuuid = t(j.parentuuid) let old = this._findElementIn('ads', a.uuid) this._migrateOtfsAndNotes(old, a, t(j.vtt_notes)) + a = await this._processItemFrom(a, 'GCA') datalist.push(a) } } @@ -693,8 +743,9 @@ export class ActorImporter { /** * @param {{ [key: string]: any }} json */ - importSkillsFromGCA(json) { + async importSkillsFromGCA(json) { if (!json) return + await this._preImport('GCA', 'skill') let temp = [] let t = this.textFrom /// shortcut to make code smaller for (let key in json) { @@ -714,10 +765,25 @@ export class ActorImporter { sk.parentuuid = t(j.parentuuid) let old = this._findElementIn('skills', sk.uuid) this._migrateOtfsAndNotes(old, sk, t(j.vtt_notes)) - + sk = await this._processItemFrom(sk, 'GCA') temp.push(sk) } } + + // Find all skills with globalId + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await aRecurselist(this.actor.system.skills, async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i?.system.globalid) { + if (!(t instanceof Skill)) t = Skill.fromObject(t, this.actor) + t = await this._processItemFrom(t, 'GCA') + temp.push(t) + } + } + }) + } + return { 'system.-=skills': null, 'system.skills': this.foldList(temp), @@ -730,8 +796,9 @@ export class ActorImporter { /** * @param {{ [key: string]: any }} json */ - importSpellsFromGCA(json) { + async importSpellsFromGCA(json) { if (!json) return + await this._preImport('GCA', 'spell') let temp = [] let t = this.textFrom /// shortcut to make code smaller for (let key in json) { @@ -760,9 +827,25 @@ export class ActorImporter { sp.parentuuid = t(j.parentuuid) let old = this._findElementIn('spells', sp.uuid) this._migrateOtfsAndNotes(old, sp, t(j.vtt_notes)) + sp = await this._processItemFrom(sp, 'GCA') temp.push(sp) } } + + // Find all spells with globalId + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await aRecurselist(this.actor.system.spells, async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i?.system.globalid) { + if (!(t instanceof Spell)) t = Spell.fromObject(t, this.actor) + t = await this._processItemFrom(t, 'GCA') + temp.push(t) + } + } + }) + } + return { 'system.-=spells': null, 'system.spells': this.foldList(temp), @@ -914,28 +997,39 @@ export class ActorImporter { } } - async _preImport(generator) { + async _preImport(generator, itemType) { if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { // Before we import, we need to find all eligible items, - // and backup their exclusive info inside their equipments - const isEligibleItem = item => - (!!item.system.importid && item.system.importFrom === generator) || - !!foundry.utils.getProperty(this.actor, this.actor._findEqtkeyForId('itemid', item.id))?.save + // and backup their exclusive info inside their actor components (fea, eqt, etc.) + const isEligibleItem = item => { + const sysKey = + itemType === 'equipment' + ? this.actor._findEqtkeyForId('itemid', item.id) + : this.actor._findSysKeyForId('itemid', item.id, item.actorComponentKey) + return ( + (!!item.system.importid && item.system.importFrom === generator && item.type === itemType) || + !!foundry.utils.getProperty(this.actor, sysKey)?.save + ) + } - let eligibleItems = this.actor.items + let eligibleItemsPromises = this.actor.items .filter(i => !!isEligibleItem(i)) - .map(i => { + .map(async i => { + // Update actor component with item exclusive info const itemInfo = i.getItemInfo() - // Update equipment - const eqtKey = this.actor._findEqtkeyForId('itemid', i.id) - if (!!eqtKey) { - let equip = foundry.utils.getProperty(this.actor, eqtKey) - equip.itemid = '' - equip.itemInfo = itemInfo - this.actor.internalUpdate({ [eqtKey]: equip }) + const sysKey = + itemType === 'equipment' + ? this.actor._findEqtkeyForId('itemid', i.id) + : this.actor._findSysKeyForId('itemid', i.id, i.actorComponentKey) + if (!!sysKey) { + let actorComp = foundry.utils.getProperty(this.actor, sysKey) + actorComp.itemid = '' + actorComp.itemInfo = itemInfo + await this.actor.internalUpdate({ [sysKey]: actorComp }) } return i.id }) + let eligibleItems = await Promise.all(eligibleItemsPromises) if (!!eligibleItems.length) await this.actor.deleteEmbeddedDocuments('Item', eligibleItems) } } @@ -949,7 +1043,7 @@ export class ActorImporter { let i = this.intFrom this.ignoreRender = true - await this._preImport('GCA') + await this._preImport('GCA', 'equipment') /** * @type {Equipment[]} @@ -987,6 +1081,7 @@ export class ActorImporter { eqt.carried = old.carried eqt.equipped = old.equipped eqt.parentuuid = old.parentuuid + eqt.itemid = old.itemid if (old.ignoreImportQty) { eqt.count = old.count eqt.uses = old.uses @@ -1004,6 +1099,7 @@ export class ActorImporter { await aRecurselist(this.actor.system.equipment?.carried, async t => { t.carried = true if (!!t.save) { + if (!(t instanceof Equipment)) t = Equipment.fromObject(t, this.actor) t = await this._processItemFrom(t, 'GCA') temp.push(t) } @@ -1011,6 +1107,7 @@ export class ActorImporter { await aRecurselist(this.actor.system.equipment?.other, async t => { t.carried = false if (!!t.save) { + if (!(t instanceof Equipment)) t = Equipment.fromObject(t, this.actor) t = await this._processItemFrom(t, 'GCA') temp.push(t) } @@ -1548,19 +1645,34 @@ export class ActorImporter { return r } - importAdsFromGCS(ads) { + async importAdsFromGCS(ads) { let temp = [] - if (!!ads) - for (let i of ads) { - temp = temp.concat(this.importAd(i, '')) - } + if (!!ads) await this._preImport('GCS', 'feature') + for (let i of ads) { + temp = temp.concat(await this.importAd(i, '')) + } + + // Find all adds with globalId + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await aRecurselist(this.actor.system.ads, async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i?.system.globalid) { + if (!(t instanceof Advantage)) t = Advantage.fromObject(t, this.actor) + t = await this._processItemFrom(t, 'GCS') + temp.push(t) + } + } + }) + } + return { 'system.-=ads': null, 'system.ads': this.foldList(temp), } } - importAd(i, p) { + async importAd(i, p) { let a = new Advantage() if (this.GCSVersion === 5) { i.type = i.id.startsWith('t') ? 'trait' : 'trait_container' @@ -1588,27 +1700,44 @@ export class ActorImporter { let old = this._findElementIn('ads', a.uuid) this._migrateOtfsAndNotes(old, a, i.vtt_notes) + a = await this._processItemFrom(a, 'GCS') let ch = [] if (i.children?.length) { - for (let j of i.children) ch = ch.concat(this.importAd(j, i.id)) + for (let j of i.children) ch = ch.concat(await this.importAd(j, i.id)) } return [a].concat(ch) } - importSkillsFromGCS(sks) { + async importSkillsFromGCS(sks) { + await this._preImport('GCS', 'skill') if (!sks) return let temp = [] for (let i of sks) { - temp = temp.concat(this.importSk(i, '')) + temp = temp.concat(await this.importSk(i, '')) } + + // Find all skills with globalId + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + await aRecurselist(this.actor.system.skills, async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i?.system.globalid) { + if (!(t instanceof Skill)) t = Skill.fromObject(t, this.actor) + t = await this._processItemFrom(t, 'GCS') + temp.push(t) + } + } + }) + } + return { 'system.-=skills': null, 'system.skills': this.foldList(temp), } } - importSk(i, p) { + async importSk(i, p) { if (this.GCSVersion === 5) { i.type = i.id.startsWith('q') ? 'technique' : i.id.startsWith('s') ? 'skill' : 'skill_container' } @@ -1641,27 +1770,42 @@ export class ActorImporter { s = this._substituteItemReplacements(s, i) let old = this._findElementIn('skills', s.uuid) this._migrateOtfsAndNotes(old, s, i.vtt_notes) + s = await this._processItemFrom(s, 'GCS') let ch = [] if (i.children?.length) { - for (let j of i.children) ch = ch.concat(this.importSk(j, i.id)) + for (let j of i.children) ch = ch.concat(await this.importSk(j, i.id)) } return [s].concat(ch) } - importSpellsFromGCS(sps) { + async importSpellsFromGCS(sps) { + await this._preImport('GCS', 'spell') if (!sps) return let temp = [] for (let i of sps) { - temp = temp.concat(this.importSp(i, '')) + temp = temp.concat(await this.importSp(i, '')) } + + // Find all spells with globalId + await aRecurselist(this.actor.system.spells, async t => { + if (!!t.itemid) { + const i = this.actor.items.get(t.itemid) + if (!!i?.system.globalid) { + if (!(t instanceof Spell)) t = Spell.fromObject(t, this.actor) + t = await this._processItemFrom(t, 'GCS') + temp.push(t) + } + } + }) + return { 'system.-=spells': null, 'system.spells': this.foldList(temp), } } - importSp(i, p) { + async importSp(i, p) { let s = new Spell() if (this.GCSVersion === 5) { i.type = i.id.startsWith('r') ? 'ritual_magic_spell' : i.id.startsWith('p') ? 'spell' : 'spell_container' @@ -1687,18 +1831,18 @@ export class ActorImporter { s = this._substituteItemReplacements(s, i) let old = this._findElementIn('spells', s.uuid) this._migrateOtfsAndNotes(old, s, i.vtt_notes) + s = await this._processItemFrom(s, 'GCS') let ch = [] if (i.children?.length) { - for (let j of i.children) ch = ch.concat(this.importSp(j, i.id)) + for (let j of i.children) ch = ch.concat(await this.importSp(j, i.id)) } return [s].concat(ch) } async importEquipmentFromGCS(eq, oeq) { this.ignoreRender = true - await this._preImport('GCS') - + await this._preImport('GCS', 'equipment') if (!eq && !oeq) return let temp = [] if (!!eq) @@ -1713,6 +1857,7 @@ export class ActorImporter { await aRecurselist(this.actor.system.equipment?.carried, async t => { t.carried = true if (!!t.save) { + if (!(t instanceof Equipment)) t = Equipment.fromObject(t, this.actor) t = await this._processItemFrom(t, 'GCS') temp.push(t) } @@ -1720,6 +1865,7 @@ export class ActorImporter { await aRecurselist(this.actor.system.equipment?.other, async t => { t.carried = false if (!!t.save) { + if (!(t instanceof Equipment)) t = Equipment.fromObject(t, this.actor) t = await this._processItemFrom(t, 'GCS') temp.push(t) } @@ -2323,6 +2469,8 @@ export class ActorImporter { // Must be done AFTER OTFs have been stripped out newobj.notes = oldobj.notes if (oldobj.name?.startsWith(newobj.name)) newobj.name = oldobj.name + // If notes have `\n ` fix it + newobj.notes = newobj.notes.replace(/\n\s\s+/g, ' ') } /** @@ -2457,37 +2605,53 @@ export class ActorImporter { return item } - async _processItemFrom(eqt, fromProgram) { - // Non Equipment instance objects need to be converted to Equipment first. - let equip = Equipment.fromObject(eqt, this.actor) - + async _processItemFrom(actorComp, fromProgram) { if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // Sanity check + if ( + !(actorComp instanceof Equipment) && + !(actorComp instanceof Advantage) && + !(actorComp instanceof Skill) && + !(actorComp instanceof Spell) + ) { + throw new Error('Invalid Actor Component. To process a Item it must be an Equipment, Skill, Spell or Advantage') + } + const existingItem = this.actor.items.find(i => i.system.importid === actorComp.uuid) + + // Check if we need to update the Item + if (!actorComp._itemNeedsUpdate(existingItem)) { + actorComp.itemid = existingItem._id + actorComp.itemInfo = existingItem.getItemInfo() + actorComp.uuid = existingItem.system[existingItem.itemSysKey].uuid + return actorComp + } + // Create or Update item - const existingItem = this.actor.items.find(i => i._id === equip.itemid) - const itemData = equip.toItemData(fromProgram) + const itemData = actorComp.toItemData(fromProgram) const [item] = !!existingItem - ? await this.actor.updateEmbeddedDocuments('Item', [{ _id: existingItem._id, ...itemData }]) + ? await this.actor.updateEmbeddedDocuments('Item', [{ _id: existingItem._id, system: itemData.system }]) : await this.actor.createEmbeddedDocuments('Item', [itemData]) - // Update Equipment for new Items - if (!existingItem && !!item) { - equip.itemid = item._id - equip.itemInfo = item.getItemInfo() + // Update Actor Component for new Items + if (!!item) { + actorComp.itemid = item._id + actorComp.itemInfo = item.getItemInfo() + actorComp.uuid = item.system[item.itemSysKey].uuid + } else if (!!existingItem) { + console.warn(`Item '${actorComp.name}' was not updated correctly. Using old version.`) + actorComp.itemid = existingItem._id + actorComp.itemInfo = existingItem.getItemInfo() + actorComp.uuid = existingItem.system[existingItem.itemSysKey].uuid } } - return equip + return actorComp } - async _updateItemContains(eqt, parent) { + async _updateItemContains(actorComp, parent) { if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { - const item = this.actor.items.get(eqt.itemid) + const item = this.actor.items.get(actorComp.itemid) if (!!item) { - if (!eqt.parentuuid) { - await this.actor.updateEmbeddedDocuments('Item', [{ _id: item._id, 'system.eqt.contains': eqt.contains }]) - } else { - const parentItem = this.actor.items.get(parent.itemid) - if (!!parentItem?.id) - await this.actor.updateEmbeddedDocuments('Item', [ - { _id: item._id, 'system.eqt.parentuuid': parentItem.id }, - ]) + if (!actorComp.parentuuid) { + const itemSysContain = `system.${item.itemSysKey}.contains` + await this.actor.updateEmbeddedDocuments('Item', [{ _id: item._id, [itemSysContain]: actorComp.contains }]) } } } diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index bdbdb9745..81bb54ad9 100755 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -138,6 +138,9 @@ export class GurpsActorSheet extends ActorSheet { this._createHeaderMenus(html) this._createEquipmentItemMenus(html) + if (!!game.settings.get(settings.SYSTEM_NAME, settings.SETTING_USE_FOUNDRY_ITEMS)) { + this._createGlobalItemMenus(html) + } // if not doing automatic encumbrance calculations, allow a click on the Encumbrance table to set the current value. if (!game.settings.get(settings.SYSTEM_NAME, settings.SETTING_AUTOMATIC_ENCUMBRANCE)) { @@ -456,13 +459,13 @@ export class GurpsActorSheet extends ActorSheet { return } - if (path.includes('equipment')) this.editEquipment(actor, path, obj) - if (path.includes('melee')) this.editMelee(actor, path, obj) - if (path.includes('ranged')) this.editRanged(actor, path, obj) - if (path.includes('ads')) this.editAds(actor, path, obj) - if (path.includes('skills')) this.editSkills(actor, path, obj) - if (path.includes('spells')) this.editSpells(actor, path, obj) - if (path.includes('notes')) this.editNotes(actor, path, obj) + if (path.includes('equipment')) await this.editEquipment(actor, path, obj) + if (path.includes('melee')) await this.editMelee(actor, path, obj) + if (path.includes('ranged')) await this.editRanged(actor, path, obj) + if (path.includes('ads')) await this.editAds(actor, path, obj) + if (path.includes('skills')) await this.editSkills(actor, path, obj) + if (path.includes('spells')) await this.editSpells(actor, path, obj) + if (path.includes('notes')) await this.editNotes(actor, path, obj) }) html.find('.dblclkedit').on('drop', this.handleDblclickeditDrop.bind(this)) @@ -646,7 +649,7 @@ export class GurpsActorSheet extends ActorSheet { name: i18n('GURPS.addTracker'), icon: '', callback: e => { - this._addTracker() + this._addTracker().then() }, }, ], @@ -655,6 +658,20 @@ export class GurpsActorSheet extends ActorSheet { } } + _createGlobalItemMenus(html) { + let opts = [ + this._createMenu( + i18n('GURPS.delete'), + '', + this._deleteItem.bind(this), + this._isRemovable.bind(this) + ), + ] + new ContextMenu(html, '.adsdraggable', opts, { eventName: 'contextmenu' }) + new ContextMenu(html, '.skldraggable', opts, { eventName: 'contextmenu' }) + new ContextMenu(html, '.spldraggable', opts, { eventName: 'contextmenu' }) + } + _createEquipmentItemMenus(html) { let includeCollapsed = this instanceof GurpsActorEditorSheet @@ -707,8 +724,19 @@ export class GurpsActorSheet extends ActorSheet { _deleteItem(target) { let key = target[0].dataset.key - if (key.includes('.equipment.')) this.actor.deleteEquipment(key) - else GURPS.removeKey(this.actor, key) + if (key.includes('.equipment.')) { + this.actor.deleteEquipment(key) + } else if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + GURPS.removeKey(this.actor, key) + } else { + let item = this.actor.items.get(GURPS.decode(this.actor, key).itemid) + if (!!item) { + this.actor._removeItemAdditions(item.id).then(() => { + item.delete() + GURPS.removeKey(this.actor, key) + }) + } + } } _sortContentAscending(target) { @@ -760,6 +788,16 @@ export class GurpsActorSheet extends ActorSheet { return false } + _isRemovable(target) { + let path = target[0].dataset.key + let ac = GURPS.decode(this.actor, path) + let item + if (ac.itemid) { + item = this.actor.items.get(ac.itemid) + } + return item?.system.globalid + } + getMenuItems(elementid) { const map = { '#ranged': [this.sortAscendingMenu('system.ranged'), this.sortDescendingMenu('system.ranged')], @@ -1292,19 +1330,19 @@ export class GurpsActorSheet extends ActorSheet { let dragData = JSON.parse(event.dataTransfer.getData('text/plain')) if (dragData.type === 'damageItem') this.actor.handleDamageDrop(dragData.payload) - if (dragData.type === 'Item') this.actor.handleItemDrop(dragData) + if (dragData.type === 'Item') await this.actor.handleItemDrop(dragData) - this.handleDragFor(event, dragData, 'ranged', 'rangeddraggable') - this.handleDragFor(event, dragData, 'melee', 'meleedraggable') - this.handleDragFor(event, dragData, 'ads', 'adsdraggable') - this.handleDragFor(event, dragData, 'skills', 'skldraggable') - this.handleDragFor(event, dragData, 'spells', 'spldraggable') - this.handleDragFor(event, dragData, 'note', 'notedraggable') - this.handleDragFor(event, dragData, 'reactions', 'reactdraggable') - this.handleDragFor(event, dragData, 'condmod', 'condmoddraggable') + await this.handleDragFor(event, dragData, 'ranged', 'rangeddraggable') + await this.handleDragFor(event, dragData, 'melee', 'meleedraggable') + await this.handleDragFor(event, dragData, 'ads', 'adsdraggable') + await this.handleDragFor(event, dragData, 'skills', 'skldraggable') + await this.handleDragFor(event, dragData, 'spells', 'spldraggable') + await this.handleDragFor(event, dragData, 'note', 'notedraggable') + await this.handleDragFor(event, dragData, 'reactions', 'reactdraggable') + await this.handleDragFor(event, dragData, 'condmod', 'condmoddraggable') if (dragData.type === 'equipment') { - if ((await this.actor.handleEquipmentDrop(dragData)) != false) return // handle external drag/drop + if ((await this.actor.handleEquipmentDrop(dragData)) !== false) return // handle external drag/drop // drag/drop in same character sheet // Validate that the target is valid for the drop. @@ -1317,7 +1355,7 @@ export class GurpsActorSheet extends ActorSheet { let targetkey = dropTarget.dataset.key if (!!targetkey) { let srckey = dragData.key - this.actor.moveEquipment(srckey, targetkey, event.shiftKey) + await this.actor.moveEquipment(srckey, targetkey, event.shiftKey) } } } diff --git a/module/actor/actor.js b/module/actor/actor.js index cc19c9402..f5a452184 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -28,10 +28,10 @@ import { PROPERTY_MOVEOVERRIDE_POSTURE, } from './maneuver.js' import { GurpsItem } from '../item.js' -import GurpsToken from '../token.js' -import { Advantage, Equipment, HitLocationEntry } from './actor-components.js' +import { Advantage, Equipment, HitLocationEntry, Skill, Spell } from './actor-components.js' import { multiplyDice } from '../utilities/damage-utils.js' import * as Settings from '../../lib/miscellaneous-settings.js' +import { ActorImporter } from './actor-importer.js' // Ensure that ALL actors has the current version loaded into them (for migration purposes) Hooks.on('createActor', async function (/** @type {Actor} */ actor) { @@ -152,7 +152,12 @@ export class GurpsActor extends Actor { // Convoluted code to add Items (and features) into the equipment list // @ts-ignore - let orig = /** @type {GurpsItem[]} */ (this.items.contents.slice().sort((a, b) => b.name.localeCompare(a.name))) // in case items are in the same list... add them alphabetically + let orig = /** @type {GurpsItem[]} */ ( + this.items.contents + .filter(i => i.type === 'equipment') + .slice() + .sort((a, b) => b.name.localeCompare(a.name)) + ) // in case items are in the same list... add them alphabetically /** * @type {any[]} */ @@ -445,6 +450,23 @@ export class GurpsActor extends Actor { return eqtkey } + /** + * Finds the actor component key corresponding to the given ID. + * + * @param {string} key - The key to search for in the trait objects. + * @param {string | number} id - The ID to match within the trait objects. + * @param {string} sysKey - The system. to use for the search. + * @return {string | undefined} The trait key if found, otherwise undefined. + */ + _findSysKeyForId(key, id, sysKey) { + let traitKey + let data = this.system + recurselist(data[sysKey], (e, k, _d) => { + if (e[key] === id) traitKey = `system.${sysKey}.` + k + }) + return traitKey + } + /** * @param {{ [key: string]: any }} dict * @param {string} type @@ -1125,6 +1147,23 @@ export class GurpsActor extends Actor { // Drag and drop from Item colletion /** + * Handle Drag and Drop on Actor + * + * ### Scenario 1: Use Foundry Items is disabled + * We use the classic behavior: only works for equipment items + * + * ### Scenario 2: Use Foundry Items is enabled + * Far more trick. We can handle equipments, spells, skills and features. + * Current logic is: + * 1. Check if the global item was already dragged. If yes, do not import again. + * 2. Check if the Actor Component for this Item is already create. Same behavior. + * 3. Create a new Actor Component, manually adding global image and OTFs. + * 4. Create the correspondent Foundry Item. + * 5. Process Item Additions (Child Items) + * + * The biggest trap here is to add something to Actor Component but not Foundry Item + * and vice versa + * * @param {{ type: any; x?: number; y?: number; payload?: any; pack?: any; id?: any; data?: any; }} dragData */ async handleItemDrop(dragData) { @@ -1144,10 +1183,103 @@ export class GurpsActor extends Actor { ui.notifications?.warn('NO ITEM DATA!') return } - ui.notifications?.info(data.name + ' => ' + this.name) if (!data.globalid) await data.update({ _id: data._id, 'system.globalid': dragData.uuid }) this.ignoreRender = true - await this.addNewItemData(data) + if (!game.settings.get(settings.SYSTEM_NAME, settings.SETTING_USE_FOUNDRY_ITEMS)) { + // Scenario 1: Work only for Equipment dropped + ui.notifications?.info(data.name + ' => ' + this.name) + await this.addNewItemData(data) + } else { + // Scenario 2: Process Actor Component, Parent (dropped) Item and Child Items + + // 1. This global item was already dropped? + const found = this.items.find(it => it.system.globalid === data.system.globalid) + if (!!found) { + ui.notifications?.warn(i18n('GURPS.cannotDropItemAlreadyExists')) + return + } + ui.notifications?.info( + game.i18n.format('GURPS.droppingItemNotification', { actorName: this.name, itemName: data.name }) + ) + + // 2. Check if Actor Component exists + const actorCompKey = + data.type === 'equipment' + ? this._findEqtkeyForId('globalid', data.system.globalid) + : this._findSysKeyForId('globalid', data.system.globalid, data.actorComponentKey) + const actorComp = foundry.utils.getProperty(this, actorCompKey) + if (!!actorComp) { + ui.notifications?.warn(i18n('GURPS.cannotDropItemAlreadyExists')) + } else { + // 3. Create Actor Component + let actorComp + let targetKey + switch (data.type) { + case 'equipment': + actorComp = Equipment.fromObject({ name: data.name, ...data.system.eqt }, this) + targetKey = 'system.equipment.carried' + break + case 'feature': + actorComp = Advantage.fromObject({ name: data.name, ...data.system.fea }, this) + targetKey = 'system.ads' + break + case 'skill': + actorComp = Skill.fromObject({ name: data.name, ...data.system.ski }, this) + targetKey = 'system.skills' + break + case 'spell': + actorComp = Spell.fromObject({ name: data.name, ...data.system.spl }, this) + + targetKey = 'system.spells' + break + } + actorComp.itemInfo.img = data.img + actorComp.otf = data.system[data.itemSysKey].otf + actorComp.checkotf = data.system[data.itemSysKey].checkotf + actorComp.duringotf = data.system[data.itemSysKey].duringotf + actorComp.passotf = data.system[data.itemSysKey].passotf + actorComp.failotf = data.system[data.itemSysKey].failotf + + // 4. Create Parent Item + const importer = new ActorImporter(this) + actorComp = await importer._processItemFrom(actorComp, '') + let parentItem = this.items.get(actorComp.itemid) + const keys = ['melee', 'ranged', 'ads', 'spells', 'skills'] + for (const key of keys) { + recurselist(data.system[key], e => { + if (!e.uuid) e.uuid = foundry.utils.randomID(16) + }) + } + + await this.updateEmbeddedDocuments('Item', [ + { + _id: parentItem.id, + 'system.globalid': dragData.uuid, + 'system.melee': data.system.melee, + 'system.ranged': data.system.ranged, + 'system.ads': data.system.ads, + 'system.spells': data.system.spells, + 'system.skills': data.system.skills, + 'system.bonuses': data.system.bonuses, + }, + ]) + + // 5. Update Actor System with new Component + const systemObject = foundry.utils.duplicate(foundry.utils.getProperty(this, targetKey)) + const removeKey = targetKey.replace(/(\w+)$/, '-=$1') + await this.internalUpdate({ [removeKey]: null }) + await GURPS.put(systemObject, actorComp) + await this.internalUpdate({ [targetKey]: systemObject }) + if (data.type === 'equipment') await Equipment.calc(actorComp) + + // 6. Process Child Items for created Item + const actorCompKey = + data.type === 'equipment' + ? this._findEqtkeyForId('uuid', parentItem.system.eqt.uuid) + : this._findSysKeyForId('uuid', parentItem.system[parentItem.itemSysKey].uuid, parentItem.actorComponentKey) + await this._addItemAdditions(parentItem, actorCompKey) + } + } this._forceRender() } @@ -1372,7 +1504,6 @@ export class GurpsActor extends Actor { eqt.equipped = !!_data.equipped ?? true eqt.img = itemData.img eqt.carried = !!_data.carried ?? true - if (!!eqt.uuid.startsWith('GGA')) eqt.save = true await GURPS.insertBeforeKey(this, targetkey, eqt) await this.updateParentOf(targetkey, true) return [targetkey, eqt.carried && eqt.equipped] @@ -1380,16 +1511,39 @@ export class GurpsActor extends Actor { } /** + * Two scenarios here: + * + * ### Use Foundry Items is disabled. + * In this scenario if Actor Component has a itemId it's because this + * component is already a child item from a parent Equipment item. + * + * ### Use Foundry Items is enabled. + * In this scenario, the ItemData received is the Parent Item. We need to check + * for child items created with the `fromItem` equal to Parent itemId. + * * @param {GurpsItemData} itemData * @param {string} eqtkey */ async _addItemAdditions(itemData, eqtkey) { let commit = {} - commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'melee')) } - commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'ranged')) } - commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'ads')) } - commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'skills')) } - commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, 'spells')) } + const subTypes = ['melee', 'ranged', 'ads', 'skills', 'spells'] + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + for (const subType of subTypes) { + commit = { ...commit, ...(await this._addItemElement(itemData, eqtkey, subType)) } + } + } else { + const parentItem = await this.items.get(itemData._id) + for (const subType of subTypes) { + if (!!parentItem.system[subType] && typeof parentItem.system[subType] === 'object') { + for (const key in parentItem.system[subType]) { + if (parentItem.system[subType].hasOwnProperty(key)) { + const childItemData = parentItem.system[subType][key] + commit = { ...commit, ...(await this._addChildItemElement(parentItem, childItemData, subType)) } + } + } + } + } + } if (!!commit) await this.internalUpdate(commit, { diff: false }) this.calculateDerivedValues() // new skills and bonuses may affect other items... force a recalc } @@ -1457,6 +1611,70 @@ export class GurpsActor extends Actor { return i == 0 ? {} : { ['system.' + key]: list } } + /** + * Calculate Skill Level per OTF. + * + * On Skills and Spells item sheets, if you define the `otf` field + * and leave the `import` field blank, system will try to calculate + * the skill level based on the OTF formula informed. + * + * BTW, `import` is the name for the base skill level. I know, naming is hard. + * + * @param otf + * @returns {Promise<*>} + * @private + */ + async _getSkillLevelFromOTF(otf) { + if (!otf) return + let skillAction = parselink(otf).action + if (!skillAction) return + skillAction.calcOnly = true + const results = await GURPS.performAction(skillAction, this) + return results?.target + } + + /** + * Process Child Items from Parent Item. + * + * Why I did not use the original code? Too complex to add new scenarios. + * + * @param parentItem + * @param childItemData + * @param key + * @returns {Promise<{[p: string]: *}|{}>} + * @private + */ + async _addChildItemElement(parentItem, childItemData, key) { + let found = false + let list = { ...this.system[key] } // shallow copy + if (found) { + // Use existing actor component uuid + let existingActorComponent = this.system[key].find(e => e.fromItem === parentItem._id) + childItemData.uuid = existingActorComponent?.uuid || '' + } + // Let's (re)create the child Item with updated Child Item information + let actorComp + switch (key) { + case 'ads': + actorComp = Advantage.fromObject(childItemData, this) + break + case 'skills': + actorComp = Skill.fromObject(childItemData, this) + actorComp['import'] = await this._getSkillLevelFromOTF(childItemData.otf) + break + case 'spells': + actorComp = Spell.fromObject(childItemData, this) + actorComp['import'] = await this._getSkillLevelFromOTF(childItemData.otf) + break + } + if (!actorComp) return {} + actorComp.fromItem = parentItem._id + const importer = new ActorImporter(this) + actorComp = await importer._processItemFrom(actorComp, '') + GURPS.put(list, actorComp) + return { ['system.' + key]: list } + } + // return the item data that was deleted (since it might be transferred) /** * @param {string} path @@ -1499,10 +1717,23 @@ export class GurpsActor extends Actor { this.ignoreRender = saved } - // We have to remove matching items after we searched through the list - // because we cannot safely modify the list why iterating over it - // and as such, we can only remove 1 key at a time and must use thw while loop to check again /** + * Remove Item Element + * + * This is the original comment (still valid): + * + * `// We have to remove matching items after we searched through the list` + * + * `// because we cannot safely modify the list why iterating over it` + * + * `// and as such, we can only remove 1 key at a time and must use thw while loop to check again` + * + * When Use Foundry Items is enabled, we just find the item using their `fromItem` + * instead their `itemId`. This is because now every child item has the Id for their + * parent item in that field. + * + * The trick here: always remove Item before Actor Component. + * * @param {string} itemid * @param {string} key */ @@ -1514,11 +1745,22 @@ export class GurpsActor extends Actor { found = false let list = foundry.utils.getProperty(this, key) recurselist(list, (e, k, _d) => { - if (e.itemid == itemid) found = k + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + if (e.itemid === itemid) found = k + } else { + if (e.fromItem === itemid) found = k + } }) if (!!found) { any = true - await GURPS.removeKey(this, key + '.' + found) + const actorKey = key + '.' + found + if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // We need to remove the child item from the actor + const childActorComponent = foundry.utils.getProperty(this, actorKey) + const existingChildItem = await this.items.get(childActorComponent.itemid) + if (!!existingChildItem) await existingChildItem.delete() + } + await GURPS.removeKey(this, actorKey) } } return any @@ -1991,6 +2233,7 @@ export class GurpsActor extends Actor { } async _updateEquipmentCalc(equipKey) { + if (!equipKey.includes('system.eqt.')) return const equip = foundry.utils.getProperty(this, equipKey) await Equipment.calc(equip) if (!!equip.parentuuid) { @@ -2002,41 +2245,46 @@ export class GurpsActor extends Actor { } async _updateItemFromForm(item) { - const equipKey = this._findEqtkeyForId('itemid', item.id) - const equip = foundry.utils.getProperty(this, equipKey) - if (!(await this._sanityCheckItemSettings(equip))) return + const sysKey = + item.type === 'equipment' + ? this._findEqtkeyForId('itemid', item.id) + : this._findSysKeyForId('itemid', item.id, item.actorComponentKey) + const actorComp = foundry.utils.getProperty(this, sysKey) + if (!(await this._sanityCheckItemSettings(actorComp))) return if (!!item.editingActor) delete item.editingActor await this._removeItemAdditions(item.id) // Update Item await this.updateEmbeddedDocuments('Item', [{ _id: item.id, system: item.system, name: item.name }]) - // Update Equipment + // Update Actor Component const itemInfo = item.getItemInfo() await this.internalUpdate({ - [equipKey]: { - ...item.system.eqt, - uuid: equip.uuid, - parentuuid: equip.parentuuid, + [sysKey]: { + ...item.system[item.itemSysKey], + uuid: actorComp.uuid, + parentuuid: actorComp.parentuuid, itemInfo, }, }) - await this._addItemAdditions(item, equipKey) - await this._updateEquipmentCalc(equipKey) - await this.updateParentOf(equipKey, true) + await this._addItemAdditions(item, sysKey) + if (item.type === 'equipment') { + await this.updateParentOf(sysKey, true) + await this._updateEquipmentCalc(sysKey) + } } - async _sanityCheckItemSettings(eqt) { + async _sanityCheckItemSettings(actorComp) { let canEdit = false let message if (!!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { message = 'GURPS.settingNoEquipAllowedHint' - if (!!eqt.itemid) canEdit = true + if (!!actorComp.itemid) canEdit = true } else { message = 'GURPS.settingNoItemAllowedHint' - if (!eqt.itemid) { + if (!actorComp.itemid) { canEdit = true } else { - const item = this.items.get(eqt.itemid) + const item = this.items.get(actorComp.itemid) if (!!item && !item.system.importid) canEdit = true } } diff --git a/module/item-sheet.js b/module/item-sheet.js index b8852b313..8e9136ed5 100755 --- a/module/item-sheet.js +++ b/module/item-sheet.js @@ -9,7 +9,7 @@ export class GurpsItemSheet extends ItemSheet { static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { classes: ['sheet', 'item'], - template: 'systems/gurps/templates/item-sheet.html', + template: 'systems/gurps/templates/item/item-sheet.hbs', width: 680, height: 'auto', resizable: false, @@ -23,9 +23,10 @@ export class GurpsItemSheet extends ItemSheet { /** @override */ getData() { const sheetData = super.getData() + sheetData.itemType = this.item.type sheetData.data = this.item.system sheetData.system = this.item.system - sheetData.data.eqt.f_count = this.item.system.eqt.count // hack for Furnace module + if (!!this.item.system.eqt) sheetData.data.eqt.f_count = this.item.system.eqt.count // hack for Furnace module sheetData.name = this.item.name if (!this.item.system.globalid && !this.item.parent) this.item.update({ 'system.globalid': this.item.id, _id: this.item.id }) @@ -154,14 +155,17 @@ export class GurpsItemSheet extends ItemSheet { let srcData = foundry.utils.getProperty(srcActor, dragData.key) srcData.contains = {} // don't include any contained/collapsed items from source srcData.collapsed = {} - if (dragData.type == 'equipment') { - this.item.update({ - name: srcData.name, - 'system.eqt': srcData, - }) - return + if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { + // Scenario 1: Only works for Equipment + if (dragData.type === 'equipment') { + await this.item.update({ + name: srcData.name, + 'system.eqt': srcData, + }) + return + } + await this._addToList(dragData.type, srcData) } - await this._addToList(dragData.type, srcData) } async _addToList(key, data) { @@ -170,16 +174,35 @@ export class GurpsItemSheet extends ItemSheet { await this.item.update({ ['system.' + key]: list }) } + /** + * A convenience reference to the Item document + * @return {GurpsItem} + */ + get item() { + return this.object + } + async close() { await super.close() // When editing a Compendium Item, Actor does not exist, so we need to update the Item directly + await this.item.update({ [`system.${this.item.itemSysKey}.name`]: this.item.name }) if (!!this.item.editingActor) { - const equipKey = this.item.editingActor._findEqtkeyForId('itemid', this.item.id) - const equip = foundry.utils.getProperty(this.item.editingActor, equipKey) - if (!(await this.item.editingActor._sanityCheckItemSettings(equip))) return - await this.item.update({ 'system.eqt.name': this.item.name }) + const actorCompKey = + this.item.type === 'equipment' + ? this.item.editingActor._findEqtkeyForId('itemid', this.item.id) + : this.item.editingActor._findSysKeyForId('itemid', this.item.id, this.item.actorComponentKey) + const actorComp = foundry.utils.getProperty(this.item.editingActor, actorCompKey) + if (!(await this.item.editingActor._sanityCheckItemSettings(actorComp))) return if (!game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_FOUNDRY_ITEMS)) { - await this.item.editingActor.updateItem(this.object) + if (this.item.type === 'equipment') { + await this.item.editingActor.updateItem(this.item) + } else { + await this.item.update({ + name: this.item.name, + img: this.item.img, + system: this.item.system, + }) + } } else { await this.item.editingActor._updateItemFromForm(this.item) } diff --git a/module/item.js b/module/item.js index 0dd922433..cf7bc4db3 100755 --- a/module/item.js +++ b/module/item.js @@ -17,15 +17,48 @@ export class GurpsItem extends Item { await this.update(data, ctx) } - /* - * Get Item's exclusive data not found in Equipment + /** + * Find Actor Component Key for this Item Type + * + * @returns {string} actor.system. + */ + get actorComponentKey() { + const keys = { + equipment: 'equipment', + feature: 'ads', + skill: 'skills', + spell: 'spells', + } + const sysKey = keys[this.type] + if (!sysKey) throw new Error(`No actor system key found for ${this.type}`) + return sysKey + } + + /** + * Find Item System Key for this Item Type + * + * @return {string} item.system. + */ + get itemSysKey() { + const keys = { + equipment: 'eqt', + feature: 'fea', + skill: 'ski', + spell: 'spl', + } + const sysKey = keys[this.type] + if (!sysKey) throw new Error(`No item system key found for ${this.type}`) + return sysKey + } + + /** + * Backup Item's data in Actor Component * - * @returns {object} + * @return {object} */ getItemInfo() { let data = foundry.utils.duplicate(this) let itemSystem = data.system - delete itemSystem.eqt return { id: this._id, img: this.img, diff --git a/styles/apps.css b/styles/apps.css index 42a7868a8..da23e7e4d 100644 --- a/styles/apps.css +++ b/styles/apps.css @@ -699,6 +699,7 @@ ul#result-effects li { font-weight: normal; text-shadow: none; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + font-family: "Roboto Condensed", Helvetica, sans-serif; /* Position the tooltip text */ position: absolute; @@ -2323,7 +2324,7 @@ button.equipmentbutton:last-child { white-space: normal; } -#spells .tooltip { +#spells .gga-manual { min-width: 200px; } @@ -2338,4 +2339,40 @@ button.equipmentbutton:last-child { overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; +} + +#itemname { + font-size: 1.5em; + text-align: center; + padding: 4px; +} + +.gga-item-sheet-title { + text-transform: uppercase; + display: flex; + align-items: center; + justify-content: center; + color: white; + padding: 4px; + font-size: 16px; + text-align: center; + margin-bottom: 6px; +} + +.first-cap::first-letter { + font-size: 19px; + text-transform: uppercase; +} + +.color-equipment { + background-color: #35713e; +} +.color-spell { + background-color: #91241e; +} +.color-skill { + background-color: #453c96; +} +.color-feature { + background-color: #3177b2; } \ No newline at end of file diff --git a/styles/simple.css b/styles/simple.css index b961636d1..b39c7018b 100755 --- a/styles/simple.css +++ b/styles/simple.css @@ -740,6 +740,14 @@ color: olivedrab; } +.gga-child-item { + color: darkgoldenrod; +} + +.gga-global-item { + color: mediumslateblue; +} + .gga-inactive { color: var(--lightgrey); } diff --git a/template.json b/template.json index 8082d671a..8c945f335 100755 --- a/template.json +++ b/template.json @@ -202,7 +202,7 @@ } }, "Item": { - "types": ["equipment"], + "types": ["equipment", "feature", "skill", "spell"], "equipment": { "eqt": { "name": "", @@ -235,7 +235,102 @@ "carried": true, "globalid": "", "importid": "", - "importFrom": "" + "importFrom": "", + "fromItem": "" + }, + "feature": { + "fea": { + "notes": "", + "pageref": "", + "contains": {}, + "uuid": "", + "parentuuid": "", + "points": 0, + "userdesc": "", + "note": "", + "name": "" + }, + "melee": {}, + "ranged": {}, + "ads": {}, + "skills": {}, + "spells": {}, + "bonuses": "", + "globalid": "", + "importid": "", + "importFrom": "", + "fromItem": "", + "checkotf": "", + "duringotf": "", + "passotf": "", + "failotf": "" + }, + "skill": { + "ski": { + "name": "", + "notes": "", + "pageref": "", + "contains": {}, + "uuid": "", + "parentuuid": "", + "points": 0, + "import": "", + "level": 0, + "type": "", + "relativelevel": 0, + "otf": "", + "checkotf": "", + "duringotf": "", + "passotf": "", + "failotf": "" + }, + "melee": {}, + "ranged": {}, + "ads": {}, + "skills": {}, + "spells": {}, + "bonuses": "", + "globalid": "", + "importid": "", + "importFrom": "", + "fromItem": "" + }, + "spell": { + "spl": { + "name": "", + "notes": "", + "pageref": "", + "contains": {}, + "uuid": "", + "parentuuid": "", + "points": 0, + "import": "", + "level": 0, + "class": "", + "college": "", + "cost": "", + "maintain": "", + "duration": "", + "resist": "", + "casttime": "", + "difficulty": "", + "relativelevel": 0, + "otf": "", + "checkotf": "", + "duringotf": "", + "passotf": "", + "failotf": "" + }, + "melee": {}, + "ranged": {}, + "ads": {}, + "skills": {}, + "spells": {}, + "bonuses": "", + "globalid": "", + "importid": "", + "importFrom": "", + "fromItem": "" } } } diff --git a/templates/actor/sections/advantages.hbs b/templates/actor/sections/advantages.hbs index 1114425fc..3023650df 100644 --- a/templates/actor/sections/advantages.hbs +++ b/templates/actor/sections/advantages.hbs @@ -31,7 +31,7 @@
-
+
{{#if hasContains}}{{/if}} @@ -43,7 +43,21 @@
{{/if}}
-
+
+ {{#if (isFoundryGlobalItem this)}} + + {{{globalItemTooltip this}}} + + {{/if}} +
+
+ {{#if (isImportedItem this)}} + + {{{parentItemTooltip this}}} + + {{/if}} +
+
{{#if (isFoundryItem this)}} {{i18n "GURPS.equipmentFoundryItem"}} diff --git a/templates/actor/sections/equipment.hbs b/templates/actor/sections/equipment.hbs index 2989ca107..2b1f45331 100644 --- a/templates/actor/sections/equipment.hbs +++ b/templates/actor/sections/equipment.hbs @@ -107,7 +107,12 @@ {{/if}}
- {{#if (isUserCreated this)}} + {{#if (isFoundryGlobalItem this)}} + + {{{globalItemTooltip this}}} + + {{/if}} + {{#if (isUserCreated this)}} {{i18n "GURPS.equipmentUserCreated"}} diff --git a/templates/actor/sections/skills.hbs b/templates/actor/sections/skills.hbs index 43c30cbbe..609b73bb8 100644 --- a/templates/actor/sections/skills.hbs +++ b/templates/actor/sections/skills.hbs @@ -54,6 +54,20 @@
{{/if}}
+
+ {{#if (isFoundryGlobalItem this)}} + + {{{globalItemTooltip this}}} + + {{/if}} +
+
+ {{#if (isImportedItem this)}} + + {{{parentItemTooltip this}}} + + {{/if}} +
{{#if (isFoundryItem this)}} diff --git a/templates/actor/sections/spells.hbs b/templates/actor/sections/spells.hbs index 1c2e4be99..2dd33fb05 100644 --- a/templates/actor/sections/spells.hbs +++ b/templates/actor/sections/spells.hbs @@ -66,7 +66,21 @@
{{/if}}
-
+
+ {{#if (isFoundryGlobalItem this)}} + + {{{globalItemTooltip this}}} + + {{/if}} +
+
+ {{#if (isImportedItem this)}} + + {{{parentItemTooltip this}}} + + {{/if}} +
+
{{#if (isFoundryItem this)}} {{i18n "GURPS.equipmentFoundryItem"}} diff --git a/templates/import-gcs-v1-data.hbs b/templates/import-gcs-v1-data.hbs new file mode 100644 index 000000000..8a259bf5d --- /dev/null +++ b/templates/import-gcs-v1-data.hbs @@ -0,0 +1,14 @@ +
+

{{title}}

+
+ + +
+


{{describeAction}}

+
    +
  • {{overwriteAction}}
  • +
  • {{itemAction}}
  • +
+
+

 {{note}}

+
diff --git a/templates/import-gcs-v1-data.html b/templates/import-gcs-v1-data.html deleted file mode 100644 index a086368e0..000000000 --- a/templates/import-gcs-v1-data.html +++ /dev/null @@ -1,9 +0,0 @@ -
-

Select a file exported from GCS or GCA.

-
- - -
-


The import will overwrite the data for {{name}}.

-
NOTE: This cannot be un-done. -
diff --git a/templates/item-sheet.html b/templates/item/item-sheet.hbs old mode 100755 new mode 100644 similarity index 87% rename from templates/item-sheet.html rename to templates/item/item-sheet.hbs index a162f7d19..510d3b9f1 --- a/templates/item-sheet.html +++ b/templates/item/item-sheet.hbs @@ -1,6 +1,18 @@
-
-

{{i18n "GURPS.itemEditor"}}

+
+ {{#if (eq itemType "equipment")}} +
{{i18n "GURPS.equipment"}}
+ {{/if}} + {{#if (eq itemType "feature")}} +
{{i18n "GURPS.advDisadvPerkQuirks"}}
+ {{/if}} + {{#if (eq itemType "skill")}} +
{{i18n "GURPS.skillsTab"}}
+ {{/if}} + {{#if (eq itemType "spell")}} +
{{i18n "GURPS.spellsTab"}}
+ {{/if}} +
@@ -24,82 +36,24 @@

{{i18n "GURPS.itemEditor"}}

-

{{i18n "GURPS.attributes"}}

-
-
- -
- - -
- -
- - $ -
- -
- - -
-
-
- -
- -
- -
- -
- -
- -
- -
-
-
-
-
-
- -
- -
-
+

{{i18n "GURPS.attributes"}}

+ {{#if (eq itemType "equipment")}} + {{>items}} + {{/if}} + {{#if (eq itemType "feature")}} + {{>features}} + {{/if}} + {{#if (eq itemType "skill")}} + {{>skill}} + {{/if}} + {{#if (eq itemType "spell")}} + {{>spell}} + {{/if}}
-
-

{{i18n "GURPS.itemFeatures"}}

- +
+
+
+ +
+ +
+
diff --git a/templates/item/sections/items.hbs b/templates/item/sections/items.hbs new file mode 100644 index 000000000..69cc2e899 --- /dev/null +++ b/templates/item/sections/items.hbs @@ -0,0 +1,73 @@ +
+
+ +
+ + +
+ +
+ + $ +
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + {{i18n "GURPS.TL"}} +
+ +
+ + +
+
+
+
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/templates/item/sections/skill.hbs b/templates/item/sections/skill.hbs new file mode 100644 index 000000000..d28d11137 --- /dev/null +++ b/templates/item/sections/skill.hbs @@ -0,0 +1,95 @@ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+ +
+ +
+
+
+
+

{{i18n "GURPS.OTFormulasEvent"}}

+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/templates/item/sections/spell.hbs b/templates/item/sections/spell.hbs new file mode 100644 index 000000000..bcc34e4e7 --- /dev/null +++ b/templates/item/sections/spell.hbs @@ -0,0 +1,162 @@ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+ +
+ +
+
+
+
+

{{i18n "GURPS.OTFormulasEvent"}}

+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
\ No newline at end of file From 36b5ad7cc60213c0568e0b8e67cd91a6a7b389dd Mon Sep 17 00:00:00 2001 From: Chris Maillefaud Date: Mon, 23 Sep 2024 18:00:12 -0300 Subject: [PATCH 8/8] chore: fix merge from 0.17.11 Also: * Add new settings: SETTING_PDF_OPEN_FIRST * Add new methods to Item and Actor to be used in external modules. * Add new field in Actor/Items: `originalName` --- lang/de.json | 2 ++ lang/en.json | 3 ++ lang/fr.json | 2 ++ lang/pt_br.json | 2 ++ lib/miscellaneous-settings.js | 11 +++++++ module/actor/actor-components.js | 13 ++++++++ module/actor/actor-importer.js | 12 ++++++++ module/actor/actor.js | 24 +++++++++++++++ module/item.js | 51 ++++++++++++++++++++++++++++++++ module/pdf-refs.js | 28 ++++++++++-------- template.json | 12 +++++--- 11 files changed, 144 insertions(+), 16 deletions(-) diff --git a/lang/de.json b/lang/de.json index 2343930e1..30292a6a6 100644 --- a/lang/de.json +++ b/lang/de.json @@ -635,6 +635,8 @@ "GURPS.settingHintBasicPDFs": "Wähle 'Kombiniert' oder 'Getrennt' und verwende die entsprechenden PDF-Codes bei der Konfiguration von PDFoundry. Hinweis: Wenn 'Getrennt' gewählt wird, sollte sich das Basic Set Campaigns PDF während des PDFoundry-Tests auf Seite 340 öffnen.", "GURPS.settingBasicPDFsCombined": "Kombiniertes Basic Set (code 'B')", "GURPS.settingBasicPDFsSeparate": "Getrennt (Characters, 'B'; Campaigns, 'BX')", + "GURPS.settingPDFOpenFirst": "Öffne das erste gefundene PDF", + "GURPS.settingHintPDFOpenFirst": "Wenn diese Option aktiviert ist, wird das System das erste gefundene PDF öffnen, wenn mehrere Seitenreferenzen angegeben sind. Wenn diese Option deaktiviert ist, wird das System alle gefundenen PDFs öffnen.", "GURPS.settingImportIgnoreName": "Import: Attribut 'Name' ignorieren", "GURPS.settingHintImportIgnoreName": "Wenn diese Option aktiviert ist, ignoriert das System das Attribut 'Name' des Akteurs während des Imports. Dies ist nützlich, wenn der Name, der in Foundry verwendet wird, von GCA/GCS abweicht und nicht gewollt ist, dass er bei jedem Import überschrieben wird.", "GURPS.settingBlockImport": "Nur SERIÖSE Spieler dürfen importieren", diff --git a/lang/en.json b/lang/en.json index 66a9f7601..b0e20f650 100755 --- a/lang/en.json +++ b/lang/en.json @@ -798,6 +798,8 @@ "GURPS.settingHintBasicPDFs": "Select 'Combined' or 'Separate' and use the associated PDF codes when configuring PDFoundry. Note: If you select 'Separate', the Basic Set Campaigns PDF should open up to page 340 during the PDFoundry test.", "GURPS.settingBasicPDFsCombined": "Combined Basic Set (code 'B')", "GURPS.settingBasicPDFsSeparate": "Separate (Characters, 'B'; Campaigns, 'BX')", + "GURPS.settingPDFOpenFirst": "Open first PDF found", + "GURPS.settingHintPDFOpenFirst": "If checked, the system will open the first PDF found when multiple Page Refs are informed. If unchecked, system will open all PDFs founded.", "GURPS.settingImportIgnoreName": "Import: Ignore 'name' attribute", "GURPS.settingHintImportIgnoreName": "If checked, the system will ignore the 'name' attribute of the Actor during imports. This is useful if the name that you use in Foundry differs from GCA/GCS and you don't want it overwritten on every import.", "GURPS.settingBlockImport": "Only TRUSTED players may Import", @@ -992,6 +994,7 @@ "GURPS.current": "Current", "GURPS.currentdodge": "Dodge", "GURPS.damageAbove": "Damage above ", + "GURPS.disadvantage": "Disadvantage", "GURPS.dead": "Dead", "GURPS.defense": "Defense", "GURPS.destroyed": "Destroyed", diff --git a/lang/fr.json b/lang/fr.json index 42a3b833f..105851ac0 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -756,6 +756,8 @@ "GURPS.settingHintBasicPDFs": "Sélectionnez 'Combiné' ou 'Separé' et utilisez les codes PDF associés lors de la configuration de PDFFoundry. Note: Si vous sélectionnez 'Separate', le PDF Basic Set Campaigns devrait s'ouvrir à la page 340 pendant le test de PDFoundry.", "GURPS.settingBasicPDFsCombined": "Basic Set Combiné (code 'B')", "GURPS.settingBasicPDFsSeparate": "Séparé (Personnages, 'B'; Campagnes, 'BX')", + "GURPS.settingPDFOpenFirst": "Ouvrir le premier PDF trouvé", + "GURPS.settingHintPDFOpenFirst": "Si coché, le système ouvrira le premier PDF trouvé lorsque plusieurs références de page sont indiquées. Si décoché, le système ouvrira tous les PDF trouvés.", "GURPS.settingImportIgnoreName": "Import: Ignorer l'attribut 'Nom'", "GURPS.settingHintImportIgnoreName": "Si coché, le système ignorera l'attribut 'Nom' de l'Acteur durant les imports. C'est utile si le nom sur Foundry est différent de GCA/GCS et que vous ne voulez pas écraser à chaque import.", "GURPS.settingBlockImport": "Seuls les joueurs de 'Trusted' peuvent Importer", diff --git a/lang/pt_br.json b/lang/pt_br.json index 2cafe12cb..ac65de751 100644 --- a/lang/pt_br.json +++ b/lang/pt_br.json @@ -768,6 +768,8 @@ "GURPS.settingHintBasicPDFs": "Selecione 'Combinado' ou 'Separado' e utilize os códigos de PDF associados quando for configurar o PDFoundry. Nota: Se você selecionar 'Separado', o PDF do Módulo Básico Campanhas deve abrir a partir da página 340 ao executar os testes do PDFoundry.", "GURPS.settingBasicPDFsCombined": "Módulo Básico Combinado (código 'B')", "GURPS.settingBasicPDFsSeparate": "Separado (Personagens, 'B'; Campanhas, 'BX')", + "GURPS.settingPDFOpenFirst": "Abrir o primeiro PDF encontrado", + "GURPS.settingHintPDFOpenFirst": "Se marcado, o sistema vai abrir o primeiro PDF encontrado quando múltiplas referências de página forem informadas. Se desmarcado, o sistema vai abrir todos os PDFs encontrados.", "GURPS.settingImportIgnoreName": "Importação: Ignorar atributo 'nome'", "GURPS.settingHintImportIgnoreName": "Se marcado, o sistema vai ignorar o atributo 'nome' do Ator durante a importação. Isso é útil caso o nome que você utiliza no Foundry for diferente daquele que consta no GCA/GCS e se você não quiser ter que modificá-lo cada vez que fizer uma importação.", "GURPS.settingBlockImport": "Apenas jogadores CONFIÁVEIS podem Importar", diff --git a/lib/miscellaneous-settings.js b/lib/miscellaneous-settings.js index 1d0da22e7..ca932ec8c 100644 --- a/lib/miscellaneous-settings.js +++ b/lib/miscellaneous-settings.js @@ -18,6 +18,7 @@ export const SETTING_WHISPER_STATUS_EFFECTS = 'whisper-status-effectss' export const SETTING_CHANGELOG_VERSION = 'changelogVersion' export const SETTING_SHOW_CHANGELOG = 'showChangelogv2' //change setting to 'reset' for everyone... now that change log only displays changes since last start export const SETTING_BASICSET_PDF = 'basicsetpdf' +export const SETTING_PDF_OPEN_FIRST = 'pdf-open-first' export const SETTING_RANGE_TO_BUCKET = 'range-to-bucket' export const SETTING_MODIFIER_TOOLTIP = 'modifier_tooltip' export const SETTING_IGNORE_IMPORT_NAME = 'ignore_import_name' @@ -168,6 +169,16 @@ export function initializeSettings() { onChange: value => console.log(`Basic Set PDFs : ${value}`), }) + game.settings.register(SYSTEM_NAME, 'pdf-open-first', { + name: i18n('GURPS.settingPDFOpenFirst'), + hint: i18n('GURPS.settingHintPDFOpenFirst'), + scope: 'world', + config: true, + type: Boolean, + default: false, + onChange: value => console.log(`On multiple Page Refs open first PDF found : ${value}`), + }) + // GCS/GCA Import Configuration ---- game.settings.register(SYSTEM_NAME, SETTING_USE_FOUNDRY_ITEMS, { diff --git a/module/actor/actor-components.js b/module/actor/actor-components.js index 116ef0e4b..c32c3b4e7 100644 --- a/module/actor/actor-components.js +++ b/module/actor/actor-components.js @@ -48,6 +48,7 @@ export class _Base { this.contains = {} this.uuid = '' this.parentuuid = '' + this.originalName = '' } /** @@ -345,6 +346,7 @@ export class Skill extends Leveled { level: this.level || 0, relativelevel: this.relativelevel || '', name: this.name, + originalName: this.originalName || '', ['type']: this['type'] || '', otf: this.otf || '', checkotf: this.checkotf || '', @@ -371,6 +373,7 @@ export class Skill extends Leveled { skill = data } else { skill = new Skill(data.name) + skill.originalName = data.originalName || '' skill.notes = data.notes skill.contains = data.contains || {} skill.uuid = data.uuid @@ -391,6 +394,7 @@ export class Skill extends Leveled { } else { const itemData = item.system[item.itemSysKey] result = + itemData.originalName !== this.originalName || itemData.notes !== this.notes || itemData.pageref !== this.pageref || !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) || @@ -449,6 +453,7 @@ export class Spell extends Leveled { level: this.level || 0, relativelevel: this.relativelevel || '', name: this.name, + originalName: this.originalName || '', ['class']: this['class'] || '', college: this.college || '', cost: this.cost || '', @@ -482,6 +487,7 @@ export class Spell extends Leveled { spell = data } else { spell = new Spell(data.name) + spell.originalName = data.originalName || '' spell.notes = data.notes spell.pageref = data.pageref spell.contains = data.contains || {} @@ -511,6 +517,7 @@ export class Spell extends Leveled { } else { const itemData = item.system[item.itemSysKey] result = + itemData.originalName !== this.originalName || itemData.notes !== this.notes || itemData.pageref !== this.pageref || !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) || @@ -573,6 +580,7 @@ export class Advantage extends NamedCost { userdesc: this.userdesc || '', note: this.note || '', name: this.name, + originalName: this.originalName || '', checkotf: this.checkotf || '', duringotf: this.duringotf || '', passotf: this.passotf || '', @@ -597,6 +605,7 @@ export class Advantage extends NamedCost { adv = data } else { adv = new Advantage(data.name) + adv.originalName = data.originalName || '' adv.notes = data.notes adv.pageref = data.pageref adv.contains = data.contains || {} @@ -616,6 +625,7 @@ export class Advantage extends NamedCost { } else { const itemData = item.system[item.itemSysKey] result = + itemData.originalName !== this.originalName || itemData.notes !== this.notes || (itemData.pageref || '') !== (this.pageref || '') || !arraysEqual(Object.keys(itemData.contains), Object.keys(this.contains)) || @@ -828,6 +838,7 @@ export class Equipment extends Named { system: { eqt: { name: this.name, + originalName: this.originalName || '', notes: this.notes, pageref: this.pageref, count: this.count, @@ -868,6 +879,7 @@ export class Equipment extends Named { equip = data } else { equip = new Equipment(data.name, data.save) + equip.originalName = data.originalName || '' equip.count = data.count equip.cost = data.cost equip.weight = data.weight @@ -897,6 +909,7 @@ export class Equipment extends Named { } else { const itemData = item.system[item.itemSysKey] result = + itemData.originalName !== this.originalName || itemData.notes !== this.notes || itemData.pageref !== this.pageref || itemData.cost !== this.cost || diff --git a/module/actor/actor-importer.js b/module/actor/actor-importer.js index 33be8a062..ea39f6fb1 100644 --- a/module/actor/actor-importer.js +++ b/module/actor/actor-importer.js @@ -727,6 +727,7 @@ export class ActorImporter { let j = json[key] let a = new Advantage() a.name = t(j.name) + a.originalName = t(j.name) a.points = this.intFrom(j.points) a.setNotes(t(j.text)) a.pageRef(t(j.pageref) || a.pageref) @@ -754,6 +755,7 @@ export class ActorImporter { let j = json[key] let sk = new Skill() sk.name = t(j.name) + sk.originalName = t(j.name) sk.type = t(j.type) sk.import = t(j.level) if (sk.level == 0) sk.level = '' @@ -807,6 +809,7 @@ export class ActorImporter { let j = json[key] let sp = new Spell() sp.name = t(j.name) + sp.originalName = t(j.name) sp.class = t(j.class) sp.college = t(j.college) let cm = t(j.costmaintain) @@ -869,6 +872,7 @@ export class ActorImporter { let j2 = j.meleemodelist[k2] let m = new Melee() m.name = t(j.name) + m.originalName = t(j.name) m.st = t(j.st) m.weight = t(j.weight) m.techlevel = t(j.tl) @@ -916,6 +920,7 @@ export class ActorImporter { let j2 = j.rangedmodelist[k2] let r = new Ranged() r.name = t(j.name) + r.originalName = t(j.name) r.st = t(j.st) r.bulk = t(j.bulk) r.legalityclass = t(j.lc) @@ -1057,6 +1062,7 @@ export class ActorImporter { let parentuuid = t(j.parentuuid) let eqt = new Equipment() eqt.name = name + eqt.originalName = t(j.name) eqt.count = t(j.count) eqt.cost = !!parentuuid ? t(j.cost) : 0 eqt.location = t(j.location) @@ -1678,6 +1684,7 @@ export class ActorImporter { i.type = i.id.startsWith('t') ? 'trait' : 'trait_container' } a.name = i.name + (i.levels ? ' ' + i.levels.toString() : '') || 'Trait' + a.originalName = i.name a.points = i.calc?.points a.notes = i.calc?.resolved_notes ?? i.notes ?? '' a.userdesc = i.userdesc @@ -1753,6 +1760,7 @@ export class ActorImporter { name += addition + ')' } let s = new Skill(name, '') + s.originalName = i.name s.pageRef(i.reference || '') s.uuid = i.id s.parentuuid = p @@ -1811,6 +1819,7 @@ export class ActorImporter { i.type = i.id.startsWith('r') ? 'ritual_magic_spell' : i.id.startsWith('p') ? 'spell' : 'spell_container' } s.name = i.name || 'Spell' + s.originalName = i.name s.uuid = i.id s.parentuuid = p s.pageRef(i.reference || '') @@ -1921,6 +1930,7 @@ export class ActorImporter { i.type = i.id.startsWith('e') ? 'equipment' : 'equipment_container' } e.name = i.description || 'Equipment' + e.originalName = i.description e.count = i.type == 'equipment_container' ? '1' : i.quantity || '0' e.cost = (parseFloat(i.calc?.extended_value) / (i.type == 'equipment_container' ? 1 : i.quantity || 1)).toString() || '' @@ -2291,6 +2301,7 @@ export class ActorImporter { if (w.type == 'melee_weapon') { let m = new Melee() m.name = i.name || i.description || '' + m.originalName = i.name m.st = w.strength || '' m.weight = i.weight || '' m.techlevel = i.tech_level || '' @@ -2311,6 +2322,7 @@ export class ActorImporter { } else if (w.type == 'ranged_weapon') { let r = new Ranged() r.name = i.name || i.description || '' + r.originalName = i.name r.st = w.strength || '' r.bulk = w.bulk || '' r.legalityclass = i.legality_class || '4' diff --git a/module/actor/actor.js b/module/actor/actor.js index 71164a68c..c9a27fe19 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -2314,4 +2314,28 @@ export class GurpsActor extends Actor { } return canEdit } + + /** + * Executes a GURPS action parsed from a given OTF string. + * + * This is intended for external libraries like Argon Combat HUD. + * + * @param {string} otf - The On-The-Fly (OTF) string representing the action to be performed. + * @return {Promise} A promise that resolves once the action has been performed. + */ + async runOTF(otf) { + const action = parselink(otf) + await GURPS.performAction(action.action, this) + } + + /** + * Retrieves the value of the 'usingQuintessence' setting from the game settings. + * + * This is intended for external libraries like Argon Combat HUD. + * + * @return {boolean} The value of the 'usingQuintessence' setting. + */ + get usingQuintessence() { + return game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_USE_QUINTESSENCE) + } } diff --git a/module/item.js b/module/item.js index cf7bc4db3..576022f82 100755 --- a/module/item.js +++ b/module/item.js @@ -1,3 +1,5 @@ +import { recurselist } from '../lib/utilities.js' + export class GurpsItem extends Item { /** * @param {Item} item @@ -11,6 +13,55 @@ export class GurpsItem extends Item { super.prepareData() } + /** + * Return Item Attacks from melee and ranged Actor Components + * + * This is intended for external libraries like Argon Combat HUD, + * but can be used anytime you have only the Item UUID and need + * to know if this Item has any Melee or Ranged attacks registered + * on Actor System. + * + * Because GCA import did not populate the `uuid` field on these Actor Components + * we need to compare the Item original name for both Item and Component. + * + * @param getAttOptions + * @returns {*[]|boolean} + */ + getItemAttacks(getAttOptions = {}) { + const { attackType = 'both', checkOnly = false } = getAttOptions + const originalName = this.system[this.itemSysKey].originalName + const currentName = this.system[this.itemSysKey].name + const actorComponentUUID = this.system[this.itemSysKey].uuid + // Look at Melee and Ranged attacks in actor.system + let attacks = [] + let attackTypes = ['melee', 'ranged'] + if (attackType !== 'both') attackTypes = [attackType] + for (let at of attackTypes) { + recurselist(this.actor.system[at], (e, _k, _d) => { + let key = undefined + if (!!actorComponentUUID && e.uuid === actorComponentUUID) { + key = this.actor._findSysKeyForId('uuid', e.uuid, at) + } else if (!!originalName && e.originalName === originalName) { + key = this.actor._findSysKeyForId('originalName', e.originalName, at) + } else if (!!currentName && e.name === currentName) { + key = this.actor._findSysKeyForId('name', e.name, at) + } + if (!!key) { + attacks.push({ + component: e, + key, + }) + } + }) + } + if (!!checkOnly) return !!attacks.length > 0 + return attacks + } + + get hasAttacks() { + return !!this.getItemAttacks({ checkOnly: true }) + } + async internalUpdate(data, context) { let ctx = { render: !this.ignoreRender } if (!!context) ctx = { ...context, ...ctx } diff --git a/module/pdf-refs.js b/module/pdf-refs.js index acfbfaf5c..874b0041a 100644 --- a/module/pdf-refs.js +++ b/module/pdf-refs.js @@ -86,7 +86,10 @@ export function handlePdf(links) { // } // Just in case we get sent multiple links separated by commas, we will open them all - links.split(',').forEach(link => { + // or just the first found, depending on SETTING_PDF_OPEN_FIRST + let success = false + for (let link of links.split(',')) { + if (!!success && game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_PDF_OPEN_FIRST)) continue let t = link.trim() let i = t.indexOf(':') let book = '' @@ -94,16 +97,16 @@ export function handlePdf(links) { if (i > 0) { // Special case for refs like "PU8:12" or "DFRPG:A12" // First we need to check if after the colon is only numbers or has a letter - let afterColon = t.substring(i + 1).trim() - if (afterColon.match(/^[0-9]+$/)) { - book = t.substring(0, i).trim() - page = parseInt(afterColon) - } else { - let codeBefore = t.substring(0, i).trim() // e.g. "DFRPG" - let codeAfter = afterColon.replace(/[0-9]*/g, '').trim() // e.g. "A" - book = `${codeBefore}:${codeAfter}` // e.g. "DFRPG:A" - page = parseInt(afterColon.replace(/[a-zA-Z]*/g, '')) // e.g. 12 - } + let afterColon = t.substring(i + 1).trim() + if (afterColon.match(/^[0-9]+$/)) { + book = t.substring(0, i).trim() + page = parseInt(afterColon) + } else { + let codeBefore = t.substring(0, i).trim() // e.g. "DFRPG" + let codeAfter = afterColon.replace(/[0-9]*/g, '').trim() // e.g. "A" + book = `${codeBefore}:${codeAfter}` // e.g. "DFRPG:A" + page = parseInt(afterColon.replace(/[a-zA-Z]*/g, '')) // e.g. 12 + } } else { book = t.replace(/(.*?)[0-9].*/g, '$1').trim() page = parseInt(t.replace(/[a-zA-Z]*/g, '')) @@ -133,6 +136,7 @@ export function handlePdf(links) { if (journalPage) { const viewer = new PDFViewerSheet(journalPage, { pageNumber: page }) viewer.render(true) + success = true } else { let url = GURPS.SJGProductMappings[book] if (url) @@ -140,5 +144,5 @@ export function handlePdf(links) { window.open(url, '_blank') else ui.notifications?.warn("Unable to match book code '" + book + "'.") } - }) + } } diff --git a/template.json b/template.json index 8c945f335..4f26ac7f1 100755 --- a/template.json +++ b/template.json @@ -223,7 +223,8 @@ "maxuses": "", "parentuuid": "", "uuid": "", - "contains": {} + "contains": {}, + "originalName": "" }, "melee": {}, "ranged": {}, @@ -248,7 +249,8 @@ "points": 0, "userdesc": "", "note": "", - "name": "" + "name": "", + "originalName": "" }, "melee": {}, "ranged": {}, @@ -282,7 +284,8 @@ "checkotf": "", "duringotf": "", "passotf": "", - "failotf": "" + "failotf": "", + "originalName": "" }, "melee": {}, "ranged": {}, @@ -319,7 +322,8 @@ "checkotf": "", "duringotf": "", "passotf": "", - "failotf": "" + "failotf": "", + "originalName": "" }, "melee": {}, "ranged": {},