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: `
`,
+ 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 @@