diff --git a/src/module/actors/actor.js b/src/module/actors/actor.js new file mode 100644 index 0000000..76e1be9 --- /dev/null +++ b/src/module/actors/actor.js @@ -0,0 +1,87 @@ +import { ActorCharacter } from "./character.js"; + +export class MGT2Combatant extends Combatant { + +} + +export class TravellerActor extends Actor { + + + prepareDerivedData() { + if (this.type === "character") { + this.system.initiative = ActorCharacter.getInitiative(this); + } + } + + async _preCreate(data, options, user) { + if ( (await super._preCreate(data, options, user)) === false ) return false; + + if (this.type === "character") { + ActorCharacter.preCreate(this, data, options, user); + } + } + + async _onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId) { + await super._onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId); + + if (this.type === "character") { + await ActorCharacter.onDeleteDescendantDocuments(this, parent, collection, documents, ids, options, userId); + } + } + + async _onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) { + super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId); + //console.log("_onUpdateDescendantDocuments"); + + if (this.type === "character") { + await ActorCharacter.onUpdateDescendantDocuments(this, parent, collection, documents, changes, options, userId); + } + } + + async _preUpdate(changed, options, user) { + if ((await super._preUpdate(changed, options, user)) === false) return false; + + if (this.type === "character") { + await ActorCharacter.preUpdate(this, changed, options, user); + } + } + + getInitiative($this) { + if (this.type === "character") { + return ActorCharacter.getInitiative(this); + } + } + + applyDamage(amount) { + if (this.type === "character") { + ActorCharacter.applyDamage(this, amount); + } + } + + getContainers() { + if (this.type === "character") { + return ActorCharacter.getContainers(this); + } + + return []; + } + + getComputers() { + if (this.type === "character") { + return ActorCharacter.getComputers(this); + } + + return []; + } + + getSkills() { + if (this.type === "character") { + return ActorCharacter.getSkills(this); + } + + return []; + } + + + +} diff --git a/src/module/actors/character-prompts.js b/src/module/actors/character-prompts.js new file mode 100644 index 0000000..931e244 --- /dev/null +++ b/src/module/actors/character-prompts.js @@ -0,0 +1,153 @@ +class EditorFullViewDialog extends Dialog { + constructor(dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"]; + this.options.resizable = true; + } + + static async create(title, html) { + const htmlContent = await renderTemplate("systems/mgt2/templates/editor-fullview.html", { + config: CONFIG.MGT2, + html: html + }); + + const results = new Promise(resolve => { + new this({ + title: title, + content: htmlContent, + buttons: { + //close: { label: game.i18n.localize("MGT2.Close") } + } + }).render(true); + }); + + return results; + } +} + +class ActorConfigDialog extends Dialog { + constructor(dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"]; + } + + static async create(system) { + const htmlContent = await renderTemplate("systems/mgt2/templates/actors/actor-config-sheet.html", { + config: CONFIG.MGT2, + system: system + }); + + const results = new Promise(resolve => { + new this({ + title: "Configuration", + content: htmlContent, + buttons: { + submit: { + label: game.i18n.localize("MGT2.Save"), + icon: '', + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')).object; + resolve(formData); + }, + } + } + }).render(true); + }); + + return results; + } +} + +class ActorCharacteristicDialog extends Dialog { + // https://foundryvtt.wiki/en/development/api/dialog + constructor(dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"]; + } + + static async create(name, show, showMax, showAll = false) { + const htmlContent = await renderTemplate("systems/mgt2/templates/actors/actor-config-characteristic-sheet.html", { + name: name, + show: show, + showMax: showMax, + showAll: showAll + }); + + const results = new Promise(resolve => { + new this({ + title: "Configuration: " + name, + content: htmlContent, + buttons: { + submit: { + label: game.i18n.localize("MGT2.Save"), + icon: '', + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')).object; + resolve(formData); + }, + } + } + }).render(true); + }); + + return results; + } +} + +class TraitEditDialog extends Dialog { + constructor(dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet"]; + } + + static async create(data) { + const htmlContent = await renderTemplate("systems/mgt2/templates/actors/trait-sheet.html", { + config: CONFIG.MGT2, + data: data + }); + const title = data.hasOwnProperty("name") && data.name !== undefined ? data.name : game.i18n.localize("MGT2.Actor.EditTrait"); + const results = new Promise(resolve => { + new this({ + title: title, + content: htmlContent, + buttons: { + submit: { + label: game.i18n.localize("MGT2.Save"), + icon: '', + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')).object; + resolve(formData); + }, + } + //cancel: { label: "Cancel" } + } + // close: (html) => { + // console.log("This always is logged no matter which option is chosen"); + // const formData = new FormDataExtended(html[0].querySelector('form')).object; + // resolve(formData); + // } + }).render(true); + }); + + return results; + } +} + +export class CharacterPrompts { + + static async openConfig(system) { + return await ActorConfigDialog.create(system); + } + + static async openCharacteristic(name, hide, showMax, showAll = false) { + return await ActorCharacteristicDialog.create(name, hide, showMax, showAll); + } + + static async openTraitEdit(data) { + return await TraitEditDialog.create(data); + } + + static async openEditorFullView(title, html) { + return await EditorFullViewDialog.create(title, html); + } +} \ No newline at end of file diff --git a/src/module/actors/character-sheet.js b/src/module/actors/character-sheet.js new file mode 100644 index 0000000..f025ce1 --- /dev/null +++ b/src/module/actors/character-sheet.js @@ -0,0 +1,1078 @@ +import { MGT2 } from "../config.js"; +import { MGT2Helper } from "../helper.js"; +import { RollPromptHelper } from "../roll-prompt.js"; +import { CharacterPrompts } from "./character-prompts.js"; + +export class TravellerActorSheet extends ActorSheet { + + /** @inheritdoc */ + static get defaultOptions() { + const options = super.defaultOptions; + + if (game.user.isGM || options.editable) + options.dragDrop.push({ dragSelector: ".drag-item-list", dropSelector: ".drop-item-list" }); + + return foundry.utils.mergeObject(options, { + classes: ["mgt2", game.settings.get("mgt2", "theme"), "sheet", "actor", "character", "nopad"], + template: "systems/mgt2/templates/actors/actor-sheet.html", + width: 780, + //height: 600, + tabs: [ + { navSelector: ".sheet-sidebar", contentSelector: "form" }, + { navSelector: "nav[data-group='characteristics']", contentSelector: "section.characteristics-panel", initial: "core" }, + { navSelector: "nav[data-group='inventory']", contentSelector: "div.tab[data-tab='inventory']", initial: "onhand" } + ] + }); + } + + async getData(options) { + const context = super.getData(options); + //console.log(context); + /*const context = { + actor: this.actor, + source: source.system + + }*/ + + this._prepareCharacterItems(context); + + /*context.biographyHTML = await TextEditor.enrichHTML(context.data.system.biography, { + secrets: this.actor.isOwner, + rollData: context.rollData, + async: true, + relativeTo: this.actor + });*/ + + return context.data; + } + + _prepareCharacterItems(sheetData) { + const actorData = sheetData.data; + actorData.isGM = game.user.isGM; + actorData.showTrash = false;//game.user.isGM || game.settings.get("mgt2", "showTrash"); + actorData.initiative = this.actor.getInitiative(); + + const weapons = []; + const armors = []; + const augments = []; + const computers = []; + const softwares = []; + const items = []; + const equipments = []; + + const containerItems = []; + const careers = []; + const skills = []; + const psionics = []; + const diseases = []; + const wounds = []; + const contacts = []; + + const settings = { + weightUnit: "kg", + //weightUnit: game.settings.get("mgt2", "useWeightMetric") ? "kg" : "lb", + usePronouns: game.settings.get("mgt2", "usePronouns"), + useGender: game.settings.get("mgt2", "useGender"), + showLife: game.settings.get("mgt2", "showLife") + }; + actorData.settings = settings; + + const actorContainers = [];//sheetData.actor.getContainers(); + + for (let item of sheetData.items) { + if (item.type === "container") { + actorContainers.push(item); + } else if (item.type === "computer") { + computers.push(item); + item.subItems = []; + if (item.system.overload === true) + item.overloadClass = "computer-overload"; + } + } + + actorContainers.sort(MGT2Helper.compareByName); + + const containers = [{ "name": "(tous)", "_id": "" }].concat(actorContainers); + const containerIndex = new Map(); + + for (let c of actorContainers) { + containerIndex.set(c._id, c); + + if (c.system.weight > 0) { + c.weight = MGT2Helper.convertWeightForDisplay(c.system.weight) + " " + settings.weightUnit; + c.display = c.name.length > 12 ? `${c.name.substring(0, 12)}... (${c.weight})` : `${c.name} (${c.weight})`; + } else { + c.display = c.name.length > 12 ? c.name.substring(0, 12) + "..." : c.name; + } + + if (c.system.onHand === true/* && c.system.count > 0*/) + c.subItems = []; + } + + let currentContainerView; + if (actorData.system.containerView !== "") { + currentContainerView = containerIndex.get(actorData.system.containerView); + if (currentContainerView !== undefined) { + actorData.containerView = currentContainerView; + actorData.containerWeight = MGT2Helper.convertWeightForDisplay(currentContainerView.system.weight); + } else { + currentContainerView = null; + actorData.containerWeight = MGT2Helper.convertWeightForDisplay(0); + } + } else { + currentContainerView = null; + actorData.containerWeight = MGT2Helper.convertWeightForDisplay(0); + } + + actorData.containerShowAll = actorData.system.containerView === ""; + + for (let i of sheetData.items) { + let item = i.system; + + if (i.system.hasOwnProperty("weight") && i.system.weight > 0) { + if (isNaN(i.system.quantity)) + i.weight = MGT2Helper.convertWeightForDisplay(i.system.weight) + " " + settings.weightUnit; + else + i.weight = MGT2Helper.convertWeightForDisplay(i.system.weight * i.system.quantity) + " " + settings.weightUnit; + } + + // Item in Storage + if (item.hasOwnProperty("container") && item.container.id !== "" && item.container.id !== undefined) { + let container = containerIndex.get(item.container.id); + if (container === undefined) { // container deleted + if (actorData.containerShowAll) { + i.containerName = "#deleted#"; + containerItems.push(i); + } + continue; + } + + if (container.system.locked && !game.user.isGM) continue; + + if (container.system.onHand === true) { + container.subItems.push(i); + } + + if (actorData.containerShowAll || (!actorData.containerShowAll && actorData.system.containerView == item.container.id)) { + if (container === undefined) + i.containerName = "#deleted#"; + else + i.containerName = container.name; + + containerItems.push(i); + } + + continue; + } + + if (i.system.hasOwnProperty("equipped")) { + i.canEquip = true; + if (i.system.equipped === true) + i.toggleClass = "active"; + } else { + i.canEquip = false; + } + + switch (i.type) { + case "equipment": + switch (i.system.subType) { + case "augment": + augments.push(i); + break; + + default: + equipments.push(i); + break; + } + break; + + case "armor": + armors.push(i); + if (i.system.options && i.system.options.length > 0) { + i.subInfo = i.system.options.map(x => x.name).join(", "); + } + break; + + case "computer": + //computers.push(i); + if (i.system.options && i.system.options.length > 0) { + i.subInfo = i.system.options.map(x => x.name).join(", "); + } + break; + + case "item": + if (i.system.subType === "software") { + if (i.system.software.computerId && i.system.software.computerId !== "") { + const computer = computers.find(x => x._id === i.system.software.computerId); + if (computer !== undefined) + computer.subItems.push(i); + else + softwares.push(i); + } else { + if (i.system.software.bandwidth > 0) + i.display = `${i.name} (${i.system.software.bandwidth})`; + else + i.display = i.name; + softwares.push(i); + } + } else { + items.push(i); + } + break; + + case "weapon": + if (i.system.range.isMelee) + i.range = game.i18n.localize("MGT2.Melee") + else { + i.range = MGT2Helper.getRangeDisplay(i.system.range); + } + + // Traits + // if (i.system.traits == undefined) + // i.system.traits = { + // parts: [] + // }; + + // let traits = i.system.traits.parts.map(x => x[0]); + + // traits.sort(); + // i.traits = traits.join(", "); + // if (i.system.options && i.system.options.length > 0) { + // i.subInfo = i.system.options.map(x => x.name).join(", "); + // } + if (i.system.traits && i.system.traits.length > 0) { + i.subInfo = i.system.traits.map(x => x.name).join(", "); + } + + weapons.push(i); + break; + + case "career": + careers.push(i); + break; + + case "contact": + contacts.push(i); + break; + + case "disease": + switch (i.system.subType) { + case "wound": + wounds.push(i); + break; + + default: + diseases.push(i); + break; + } + break; + + case "talent": + if (i.system.subType === "skill") { + skills.push(i); + } else { + if (MGT2Helper.hasValue(i.system.psionic, "reach")) { + i.reach = game.i18n.localize(`MGT2.PsionicReach.${i.system.psionic.reach}`); + } + + if (MGT2Helper.hasValue(i.system.roll, "difficulty")) { + i.difficulty = game.i18n.localize(`MGT2.Difficulty.${i.system.roll.difficulty}`); + //i.difficulty = MGT2Helper.getDifficultyDisplay(i.system.roll.difficulty); + } + psionics.push(i); + } + break; + + case "container": + if (i.system.onHand === true) { + items.push(i); + // sous item + } + break; + } + } + + // let weight = MGT2Helper.getItemsWeight(weapons) + + // MGT2Helper.getItemsWeight(augments) + + // MGT2Helper.getItemsWeight(armors) + + // MGT2Helper.getItemsWeight(computers) + + // MGT2Helper.getItemsWeight(items); + + //let containerWeight = MGT2Helper.getItemsWeight(containerItems); + + actorData.encumbranceNormal = MGT2Helper.convertWeightForDisplay(actorData.system.inventory.encumbrance.normal); + actorData.encumbranceHeavy = MGT2Helper.convertWeightForDisplay(actorData.system.inventory.encumbrance.heavy); + //actorData.weight = MGT2Helper.convertWeightForDisplay(weight); // actorData.system.inventory.weight + //actorData.containerWeight = MGT2Helper.convertWeightForDisplay(containerWeight); + //actorData.dropInContainer = + + if (actorData.system.inventory.weight > actorData.system.inventory.encumbrance.heavy) { + actorData.encumbranceClasses = "encumbrance-heavy" + actorData.encumbrance = 2; + } else if (actorData.system.inventory.weight > actorData.system.inventory.encumbrance.normal) { + actorData.encumbranceClasses = "encumbrance-normal" + actorData.encumbrance = 1; + } else { + actorData.encumbrance = 0; + } + + if (softwares.length > 0) { + softwares.sort(MGT2Helper.compareByName); + actorData.softwares = softwares; + } + + augments.sort(this.compareEquippedByName); + actorData.augments = augments; + + armors.sort(this.compareEquippedByName); + actorData.armors = armors; + + computers.sort(this.compareEquippedByName); + actorData.computers = computers; + + //careers.sort(this.compareByName); + actorData.careers = careers; // First In First Out + + contacts.sort(MGT2Helper.compareByName); + actorData.contacts = contacts; + + containers.sort(MGT2Helper.compareByName); + actorData.containers = containers; + + diseases.sort(MGT2Helper.compareByName); + actorData.diseases = diseases; + + actorData.wounds = wounds; + + equipments.sort(this.compareEquippedByName); + actorData.equipments = equipments; + + items.sort(this.compareEquippedByName); + actorData.items = items; + + actorContainers.sort(MGT2Helper.compareByName); + actorData.actorContainers = actorContainers; + + skills.sort(MGT2Helper.compareByName); + actorData.skills = skills; + + psionics.sort(MGT2Helper.compareByName); + actorData.psionics = psionics; + + weapons.sort(this.compareEquippedByName); + actorData.weapons = weapons; + + if (containerItems.length > 0) { + containerItems.sort((a, b) => { + const containerResult = a.containerName.localeCompare(b.containerName); + if (containerResult !== 0) return containerResult; + + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + } + + actorData.containerItems = containerItems; + } + + compareEquippedByName(a, b) { + //if (a.system.hasOwnProperty("equipped") && !b.system.hasOwnProperty("equipped")) return -1; + //if (!a.system.hasOwnProperty("equipped") && b.system.hasOwnProperty("equipped")) return 1; + + //if (a.system.equipped === true && b.system.equipped === false) return -1; + //if (a.system.equipped === false && b.system.equipped === true) return 1; + + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + } + + activateListeners(html) { + super.activateListeners(html); + + // Everything below here is only needed if the sheet is editable + if (!this.options.editable) return; + + // if (this.actor.isOwner) { + // let handler = ev => this._onDragStart(ev); + + // html.find('div.dropitem').each((i, e) => { + // //if (li.classList.contains("inventory-header")) return; + // e.setAttribute("draggable", true); + // e.addEventListener("dragstart", handler, false); + // }); + // } + + html.find('.container-create').click(this._onContainerCreate.bind(this)); + html.find('.container-edit').click(ev => { + const container = this.actor.getEmbeddedDocument("Item", this.actor.system.containerView); + container.sheet.render(true); + }); + + html.find('.container-delete').click(ev => { + ev.preventDefault(); + const containers = this.actor.getContainers(); + const container = containers.find(x => x._id == this.actor.system.containerView); + const containerItems = this.actor.items.filter(x => x.system.hasOwnProperty("container") && x.system.container.id === container._id); + + if (containerItems.length > 0) { + for (let item of containerItems) { + let clone = duplicate(item); + clone.system.container.id = ""; + this.actor.updateEmbeddedDocuments('Item', [clone]); + } + } + + const cloneActor = duplicate(this.actor); + cloneActor.system.containerView = ""; + if (cloneActor.system.containerDropIn === container._id) { + cloneActor.system.containerDropIn = ""; + const remainingContainers = containers.filter(x => x._id !== container._id); + if (remainingContainers.length > 0) + cloneActor.system.containerDropIn = remainingContainers[0]._id; + } + this.actor.deleteEmbeddedDocuments("Item", [container._id]); + this.actor.update(cloneActor); + }); + + html.find('.item-create').click(this._onItemCreate.bind(this)); + html.find('.item-edit').click(ev => { + ev.preventDefault(); + const html = $(ev.currentTarget).parents("[data-item-id]"); + const item = this.actor.getEmbeddedDocument("Item", html.data("itemId")); + item.sheet.render(true); + }); + + html.find('.item-delete').click(ev => { + ev.preventDefault(); + const html = $(ev.currentTarget).parents("[data-item-id]"); + // ev.ctrlKey === true + + this.actor.deleteEmbeddedDocuments("Item", [html.data("itemId")]); + html.slideUp(200, () => this.render(false)); + }); + html.find('a.item-equip').click(this._onItemEquip.bind(this)); + html.find('a.item-storage-out').click(this._onItemStorageOut.bind(this)); + html.find('a.item-storage-in').click(this._onItemStorageIn.bind(this)); + html.find('a.software-eject').click(this._onSoftwareEject.bind(this)); + + html.find('a[data-roll]').click(this._onRoll.bind(this)); + html.find('a[name="config"]').click(this._onOpenConfig.bind(this)); + html.find('a[data-cfg-characteristic]').click(this._onOpenCharacteristic.bind(this)); + html.find('.traits-create').click(this._onTraitCreate.bind(this)); + html.find('.traits-edit').click(this._onTraitEdit.bind(this)); + html.find('.traits-delete').click(this._onTraitDelete.bind(this)); + html.find('a[data-editor="open"]').click(this._onOpenEditor.bind(this)); + }; + + async _onOpenEditor(event) { + event.preventDefault(); + await CharacterPrompts.openEditorFullView(this.actor.system.personal.species, this.actor.system.personal.speciesText.descriptionLong); + } + + async _onTraitCreate(event) { + event.preventDefault(); + await this._onSubmit(event); + let traits = this.actor.system.personal.traits; + let index; + if (traits.length === 0) { + traits = {}; + traits["0"] = { name: "", description: "" }; + } else { + index = Math.max(...Object.keys(traits)); + index++; + traits[index] = { name: "", description: "" }; + } + + return this.actor.update({ system: { personal: { traits: traits } } }); + } + + async _onTraitEdit(event) { + event.preventDefault(); + const index = $(event.currentTarget).parents("[data-traits-part]").data("traits-part"); + const trait = this.actor.system.personal.traits[index]; + let result = await CharacterPrompts.openTraitEdit(trait); + const traits = this.actor.system.personal.traits; + traits[index].name = result.name; + traits[index].description = result.description; + return this.actor.update({ system: { personal: { traits: traits } } }); + } + + async _onTraitDelete(event) { + event.preventDefault(); + await this._onSubmit(event); + const element = event.currentTarget.closest(".traits-part"); + const traits = foundry.utils.deepClone(this.actor.system.personal.traits); + let index = Number(element.dataset.traitsPart); + + const newTraits = []; + let entries = Object.entries(traits); + if (entries.length > 1) { + for (const [key, value] of entries) { + if (key != index) + newTraits.push(value); + } + } + + return this.actor.update({ system: { personal: { traits: newTraits } } }); + } + + async _onOpenConfig(ev) { + ev.preventDefault(); + //console.log(this.actor.system); + const userConfig = await CharacterPrompts.openConfig(this.actor.system); + //console.log(userData); + // {initiative: 'dexterity', damage.rank1: 'strength', damage.rank2: 'dexterity', damage.rank3: 'endurance'} + if (userConfig) { + this.actor.update({ "system.config": userConfig }); + } + } + + async _onOpenCharacteristic(ev) { + ev.preventDefault(); + const name = ev.currentTarget.dataset.cfgCharacteristic; + const c = this.actor.system.characteristics[name]; + + let showAll = false; + for (const [key, value] of Object.entries(this.actor.system.characteristics)) { + if (!value.show) { + showAll = true; + break; + } + } + + const userConfig = await CharacterPrompts.openCharacteristic(game.i18n.localize(`MGT2.Characteristics.${name}.name`), c.show, c.showMax, showAll); + + // {hide: false, showMax: true, showAll: false} + if (userConfig) { + const data = { + system: { + characteristics: {} + } + }; + + data.system.characteristics[name] = { + show: userConfig.show, + showMax: userConfig.showMax + }; + + if (userConfig.showAll === true) { + for (const [key, value] of Object.entries(this.actor.system.characteristics)) { + if (key !== name && !value.show) { + data.system.characteristics[key] = { show: true }; + } + } + } + + this.actor.update(data); + } + } + + async _onRoll(event) { + event.preventDefault(); + + const rollOptions = { + rollTypeName: game.i18n.localize("MGT2.RollPrompt.Roll"), + rollObjectName: "", + characteristics: [{ _id: "", name: "" }], + characteristic: "", + skills: [], + skill: "", + fatigue: this.actor.system.states.fatigue, + encumbrance: this.actor.system.states.encumbrance, + difficulty: null, + damageFormula: null + }; + + // TODO Convertir le bouton des dégâts + const cardButtons = []; + + for (const [key, label] of Object.entries(MGT2.Characteristics)) { + const c = this.actor.system.characteristics[key]; + if (c.show) { + rollOptions.characteristics.push({ _id: key, name: game.i18n.localize(label) + MGT2Helper.getDisplayDM(c.dm) }); + } + } + + for (let item of this.actor.items) { + if (item.type === "talent") { + if (item.system.subType === "skill") + rollOptions.skills.push({ _id: item._id, name: item.getRollDisplay() }); + } + } + + rollOptions.skills.sort(MGT2Helper.compareByName); + rollOptions.skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(rollOptions.skills); + + let itemObj = null; + let isInitiative = false; + const button = event.currentTarget; + if (button.dataset.roll === "initiative") { + rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.InitiativeRoll"); + rollOptions.characteristic = this.actor.system.config.initiative; + isInitiative = true; + } else if (button.dataset.roll === "characteristic") { + rollOptions.characteristic = button.dataset.rollCharacteristic; + rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.CharacteristicRoll"); + rollOptions.rollObjectName = game.i18n.localize(`MGT2.Characteristics.${rollOptions.characteristic}.name`); + } else { + + if (button.dataset.roll === "skill") { + rollOptions.skill = button.dataset.rollSkill; + itemObj = this.actor.getEmbeddedDocument("Item", rollOptions.skill); + rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.SkillRoll"); + rollOptions.rollObjectName = itemObj.name; + } else { + if (button.dataset.roll === "psionic") { + rollOptions.rollTypeName = game.i18n.localize("MGT2.RollPrompt.PsionicRoll"); + } + } + + if (itemObj === null && button.dataset.itemId) { + itemObj = this.actor.getEmbeddedDocument("Item", button.dataset.itemId); + rollOptions.rollObjectName = itemObj.name; + if (itemObj.type === "weapon") + rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.weapon"); + else if (itemObj.type === "armor") + rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.armor"); + else if (itemObj.type === "computer") + rollOptions.rollTypeName = game.i18n.localize("TYPES.Item.computer"); + } + + if (button.dataset.roll === "psionic") { + rollOptions.rollObjectName = itemObj.name; + if (MGT2Helper.hasValue(itemObj.system.psionic, "duration")) { + cardButtons.push({ + label: game.i18n.localize("MGT2.Items.Duration"), + formula: itemObj.system.psionic.duration, + message: { + objectName: itemObj.name, + flavor: "{0} ".concat(game.i18n.localize(`MGT2.Durations.${itemObj.system.psionic.durationUnit}`)) + } + }); + } + } + + if (itemObj.system.hasOwnProperty("damage")) { + rollOptions.damageFormula = itemObj.system.damage; + if (itemObj.type === "disease") { + if (itemObj.system.subTypetype === "disease") { + rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.disease"); + } else if (itemObj.system.subTypetype === "poison") { + rollOptions.rollTypeName = game.i18n.localize("MGT2.DiseaseSubType.poison"); + } + } + } + + if (itemObj.system.hasOwnProperty("roll")) { + if (MGT2Helper.hasValue(itemObj.system.roll, "characteristic")) { + rollOptions.characteristic = itemObj.system.roll.characteristic; + } + + if (MGT2Helper.hasValue(itemObj.system.roll, "skill")) { + rollOptions.skill = itemObj.system.roll.skill; + } + + if (MGT2Helper.hasValue(itemObj.system.roll, "difficulty")) { + rollOptions.difficulty = itemObj.system.roll.difficulty; + } + } + } + + const userRollData = await RollPromptHelper.roll(rollOptions); + + const rollModifiers = []; + const rollFormulaParts = []; + if (userRollData.diceModifier) { + rollFormulaParts.push("3d6"); + rollFormulaParts.push(userRollData.diceModifier); + } else { + rollFormulaParts.push("2d6"); + } + + if (userRollData.hasOwnProperty("characteristic") && userRollData.characteristic !== "") { + let c = this.actor.system.characteristics[userRollData.characteristic]; + let dm = c.dm; + rollFormulaParts.push(MGT2Helper.getFormulaDM(dm)); + rollModifiers.push(game.i18n.localize(`MGT2.Characteristics.${userRollData.characteristic}.name`) + MGT2Helper.getDisplayDM(dm)); + } + + if (userRollData.hasOwnProperty("skill") && userRollData.skill !== "") { + if (userRollData.skill === "NP") { + rollFormulaParts.push("-3"); + rollModifiers.push(game.i18n.localize("MGT2.Items.NotProficient")); + } else { + const skillObj = this.actor.getEmbeddedDocument("Item", userRollData.skill); + rollFormulaParts.push(MGT2Helper.getFormulaDM(skillObj.system.level)); + rollModifiers.push(skillObj.getRollDisplay()); + } + } + + if (userRollData.hasOwnProperty("psionic") && userRollData.psionic !== "") { + let psionicObj = this.actor.getEmbeddedDocument("Item", userRollData.psionic); + rollFormulaParts.push(MGT2Helper.getFormulaDM(psionicObj.system.level)); + rollModifiers.push(psionicObj.getRollDisplay()); + } + + if (userRollData.hasOwnProperty("timeframes") && + userRollData.timeframes !== "" && + userRollData.timeframes !== "Normal") { + rollModifiers.push(game.i18n.localize(`MGT2.Timeframes.${userRollData.timeframes}`)); + rollFormulaParts.push(userRollData.timeframes === "Slower" ? "+2" : "-2"); + } + + if (userRollData.hasOwnProperty("encumbrance") && userRollData.encumbrance === true) { + rollFormulaParts.push("-2"); + + rollModifiers.push(game.i18n.localize("MGT2.Actor.Encumbrance") + " -2"); + } + + if (userRollData.hasOwnProperty("fatigue") && userRollData.fatigue === true) { + rollFormulaParts.push("-2"); + rollModifiers.push(game.i18n.localize("MGT2.Actor.Fatigue") + " -2"); + } + + if (userRollData.hasOwnProperty("customDM") && userRollData.customDM !== "") { + let s = userRollData.customDM.trim(); + if (/^[0-9]/.test(s)) + rollFormulaParts.push("+"); + rollFormulaParts.push(s); + } + + if (MGT2Helper.hasValue(userRollData, "difficulty")) { + rollOptions.difficulty = userRollData.difficulty; + } + + const rollData = this.actor.getRollData(); + + const rollFormula = rollFormulaParts.join(""); + + if (!Roll.validate(rollFormula)) { + ui.notifications.error(game.i18n.localize("MGT2.Errors.InvalidRollFormula")); + return; + } + + let roll = await new Roll(rollFormula, rollData).roll({ async: true, rollMode: userRollData.rollMode }); + + if (isInitiative && this.token && this.token.combatant) { + await this.token.combatant.update({ initiative: roll.total }); + } + + let isPrivate = false; + //let flavor = "Roule!"; + const chatData = { + user: game.user.id, + speaker: this.actor ? ChatMessage.getSpeaker({ actor: this.actor }) : null, + formula: isPrivate ? "???" : roll._formula, + //flavor: isPrivate ? null : flavor, + tooltip: isPrivate ? "" : await roll.getTooltip(), + total: isPrivate ? "?" : Math.round(roll.total * 100) / 100, + type: CONST.CHAT_MESSAGE_TYPES.ROLL, + showButtons: true, + showLifeButtons: false, + showRollRequest: false, + rollTypeName: rollOptions.rollTypeName, + rollObjectName: rollOptions.rollObjectName, + rollModifiers: rollModifiers, + showRollDamage: rollOptions.damageFormula !== null && rollOptions.damageFormula !== "", + cardButtons: cardButtons + }; + + if (MGT2Helper.hasValue(rollOptions, "difficulty")) { + chatData.rollDifficulty = rollOptions.difficulty; + chatData.rollDifficultyLabel = MGT2Helper.getDifficultyDisplay(rollOptions.difficulty); + + if (roll.total >= MGT2Helper.getDifficultyValue(rollOptions.difficulty)) { + chatData.rollSuccess = true; + } else { + chatData.rollFailure = true; + } + } + + const html = await renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); + chatData.content = html; + + let flags = null; + + if (rollOptions.damageFormula !== null && rollOptions.damageFormula !== "") { + flags = { mgt2: { damage: { formula: rollOptions.damageFormula, rollObjectName: rollOptions.rollObjectName, rollTypeName: rollOptions.rollTypeName } } }; + } + + if (cardButtons.length > 0) { + if (flags === null) flags = { mgt2: {} }; + flags.mgt2.buttons = cardButtons; + } + + if (flags !== null) + chatData.flags = flags; + + return roll.toMessage(chatData); + } + + _onItemCreate(ev) { + ev.preventDefault(); + const html = $(ev.currentTarget); + + const data = { + name: html.data("create-name"), + type: html.data("type-item") + }; + + if (html.data("subtype")) { + data.system = { + subType: html.data("subtype") + }; + } + + const cls = getDocumentClass("Item"); + + return cls.create(data, { parent: this.actor }); + } + + _onItemEquip(ev) { + ev.preventDefault(); + const html = $(ev.currentTarget).parents("[data-item-id]"); + const item = duplicate(this.actor.getEmbeddedDocument("Item", html.data("itemId"))); + item.system.equipped = !item.system.equipped; + this.actor.updateEmbeddedDocuments('Item', [item]); + } + + _onItemStorageIn(ev) { + ev.preventDefault(); + const html = $(ev.currentTarget).parents("[data-item-id]"); + const item = duplicate(this.actor.getEmbeddedDocument("Item", html.data("itemId"))); + if (item.type === "container") { + item.system.onHand = false; + } else { + let container; + const containers = this.actor.getContainers(); + if (this.actor.system.containerDropIn == "" || this.actor.system.containerDropIn === null) { + // Placer dans le premier container + if (containers.length === 0) { + // Create default container + const cls = getDocumentClass("Item"); + container = cls.create({ name: "New container", type: "container" }, { parent: this.actor }); + } else { + container = containers[0]; + } + } else { + container = containers.find(x => x._id == this.actor.system.containerDropIn); + } + + if (container.system.locked) { + if (game.user.isGM) { + item.system.container.id = container._id; + } else { + ui.notifications.error("Objet verrouillé"); + } + } else { + item.system.container.id = container._id; + } + + } + this.actor.updateEmbeddedDocuments('Item', [item]); + } + + _onItemStorageOut(ev) { + ev.preventDefault(); + const html = $(ev.currentTarget).parents("[data-item-id]"); + const item = duplicate(this.actor.getEmbeddedDocument("Item", html.data("itemId"))); + item.system.container.id = ""; + //item.system.container.inContainer = false; + this.actor.updateEmbeddedDocuments('Item', [item]); + } + + _onSoftwareEject(ev) { + ev.preventDefault(); + const html = $(ev.currentTarget).parents("[data-item-id]"); + const item = duplicate(this.actor.getEmbeddedDocument("Item", html.data("itemId"))); + item.system.software.computerId = ""; + this.actor.updateEmbeddedDocuments('Item', [item]); + } + + _onContainerCreate(ev) { + ev.preventDefault(); + const cls = getDocumentClass("Item"); + return cls.create({ name: "New container", type: "container" }, { parent: this.actor }); + } + + _canDragDrop(selector) { + return this.isEditable; + } + + async _onDrop(event) { + //console.log("_onDrop"); + //console.log(event); + event.preventDefault(); + event.stopImmediatePropagation(); + const dropData = MGT2Helper.getDataFromDropEvent(event); + if (!dropData) return false; + + const sourceItemData = await MGT2Helper.getItemDataFromDropData(dropData); + + if (sourceItemData.type === "species") { + const update = { + system: { + personal: { + species: sourceItemData.name, + speciesText: { + description: sourceItemData.system.description, + descriptionLong: sourceItemData.system.descriptionLong + }, + } + } + }; + + update.system.personal.traits = this.actor.system.personal.traits.concat(sourceItemData.system.traits); + + // characteristics + if (sourceItemData.system.modifiers && sourceItemData.system.modifiers.length > 0) { + update.system.characteristics = {}; + for (let modifier of sourceItemData.system.modifiers) { + if (MGT2Helper.hasValue(modifier, "characteristic") && MGT2Helper.hasValue(modifier, "value")) { + const c = this.actor.system.characteristics[modifier.characteristic]; + const updateValue = { value: c.value }; + updateValue.value += modifier.value; + if (c.showMax) { + updateValue.max = c.max + modifier.value; + } + + update.system.characteristics[modifier.characteristic] = updateValue; + } + } + + // recalcul modifier? + } + + this.actor.update(update); + + return true; + } + + // Simple drop + if (sourceItemData.type === "contact" || sourceItemData.type === "disease" || + sourceItemData.type === "career" || sourceItemData.type === "talent") { + let transferData = {}; + try { + transferData = sourceItemData.toJSON(); + } catch (err) { + transferData = sourceItemData; + } + + delete transferData._id; + delete transferData.id; + await this.actor.createEmbeddedDocuments("Item", [transferData]); + return true; + } + + // Supported drop (don't drop vehicule stuff) + if (sourceItemData.type !== "armor" && sourceItemData.type !== "weapon" && + sourceItemData.type !== "computer" && sourceItemData.type !== "container" && + sourceItemData.type !== "item" && sourceItemData.type !== "equipment") return false; + + const target = event.target.closest(".table-row"); + let targetId = null; + let targetItem = null; + + if (target !== null && target !== undefined) { + targetId = target.dataset.itemId; + targetItem = this.actor.getEmbeddedDocument("Item", targetId); + } + + let sourceItem = this.actor.getEmbeddedDocument("Item", sourceItemData.id); + if (sourceItem) { // same actor item move + if (targetItem === null || targetItem === undefined) return false; + + sourceItem = duplicate(sourceItem); + if (sourceItem._id === targetId) return false; // Same item + + if (targetItem.type === "item" || targetItem.type === "equipment") { + // SOFTWARE --> COMPUTER + if (targetItem.system.subType === "software") { + sourceItem.system.software.computerId = targetItem.system.software.computerId; + } else { + sourceItem.system.container.id = targetItem.system.container.id; + } + this.actor.updateEmbeddedDocuments('Item', [sourceItem]); + return true; + } else if (targetItem.type === "computer") { + sourceItem.system.software.computerId = targetId; + this.actor.updateEmbeddedDocuments('Item', [sourceItem]); + return true; + } else if (targetItem.type === "container") { + // locked refuse + if (targetItem.system.locked && !game.user.isGM) + ui.notifications.error("Verrouillé"); + else { + sourceItem.system.container.id = targetId; + this.actor.updateEmbeddedDocuments('Item', [sourceItem]); + return true; + } + } + } else { + // Copy item from other source + let transferData = {}; + try { + transferData = sourceItemData.toJSON(); + } catch (err) { + transferData = sourceItemData; + } + + //const sourceItemId = transferData._id; + + delete transferData._id; + delete transferData.id; + + const recalcWeight = transferData.system.hasOwnProperty("weight"); + + // Normalize data + if (transferData.system.hasOwnProperty("container")) + transferData.system.container.id = ""; + if (transferData.type === "item" && transferData.system.subType === "software") + transferData.system.software.computerId = ""; + + if (transferData.type === "container") + transferData.onHand = true; + + if (transferData.system.hasOwnProperty("equipment")) + transferData.system.equipped = false; + + + if (targetItem !== null) { + // Handle computer & container + if (transferData.type === "item" && transferData.system.subType === "software") { + if (targetItem.type === "item" && targetItem.system.subType === "software") { + transferData.system.software.computerId = targetItem.system.software.computerId; + + } else if (targetItem.type === "computer") { + transferData.system.software.computerId = targetItem._id; + } + } else if (transferData.type === "armor" || transferData.type === "computer" || transferData.type === "equipment" || transferData.type === "item" || transferData.type === "weapon") { + if (targetItem.type === "container") { + if (!targetItem.system.locked || game.user.isGM) { + transferData.system.container.id = targetId; + } + } else { + transferData.system.container.id = targetItem.system.container.id; + } + } + } + + const addedItem = (await this.actor.createEmbeddedDocuments("Item", [transferData]))[0]; + + if (transferData.actor) { + // delete item + // if container, tranferts content onHand true + } + + + if (recalcWeight) { + await this.actor.recalculateWeight(); + } + } + return true; + } + + _getSubmitData(updateData = {}) { + const formData = foundry.utils.expandObject(super._getSubmitData(updateData)); + // this.actor.computeCharacteristics(formData); + return foundry.utils.flattenObject(formData); + } +} diff --git a/src/module/actors/character.js b/src/module/actors/character.js new file mode 100644 index 0000000..b96595d --- /dev/null +++ b/src/module/actors/character.js @@ -0,0 +1,455 @@ +export class ActorCharacter { + static preCreate($this, data, options, user) { + $this.updateSource({ prototypeToken: { actorLink: true } }) // QoL + } + + static prepareData(actorData) { + actorData.initiative = this.getInitiative(actorData); + } + + static getInitiative($this) { + let c = $this.system.config.initiative; + return $this.system.characteristics[c].dm; + } + + static async onDeleteDescendantDocuments($this, parent, collection, documents, ids, options, userId) { + const toDeleteIds = []; + const itemToUpdates = []; + + for (let d of documents) { + if (d.type === "container") { + // Delete content + for (let item of $this.items) { + if (item.system.hasOwnProperty("container") && item.system.container.id === d._id) + toDeleteIds.push(item._id); + } + } else if (d.type === "computer") { + // Eject software + for (let item of $this.items) { + if (item.system.hasOwnProperty("software") && item.system.computerId === d._id) { + let clone = duplicate(item); + clone.system.software.computerId = ""; + itemToUpdates.push(clone); + } + } + } + } + + if (toDeleteIds.length > 0) + await $this.deleteEmbeddedDocuments("Item", toDeleteIds); + + if (itemToUpdates.length > 0) + await $this.updateEmbeddedDocuments('Item', itemToUpdates); + + await this.recalculateWeight(); + } + + static async onUpdateDescendantDocuments($this, parent, collection, documents, changes, options, userId) { + await this.calculEncumbranceAndWeight($this, parent, collection, documents, changes, options, userId); + await this.calculComputers($this, parent, collection, documents, changes, options, userId); + } + + static async calculComputers($this, parent, collection, documents, changes, options, userId) { + let change; + let i = 0; + + let recalculProcessing = false; + for (let d of documents) { + if (changes[i].hasOwnProperty("system")) { + change = changes[i]; + if (d.type === "item" && d.system.subType === "software") { + if (change.system.software.hasOwnProperty("bandwidth") || change.system.software.hasOwnProperty("computerId")) { + recalculProcessing = true; + break; + } + } + } + } + + if (recalculProcessing) { + let updatedComputers = []; + let computerChanges = {}; + let computers = []; + + for (let item of $this.items) { + if (item.system.trash === true) continue; + if (item.type === "computer") { + computers.push(item); + computerChanges[item._id] = { processingUsed: 0 }; + } + } + + for (let item of $this.items) { + if (item.type !== "item" && item.system.subType !== "software") continue; + + if (item.system.software.hasOwnProperty("computerId") && item.system.software.computerId !== "") { + computerChanges[item.system.software.computerId].processingUsed += item.system.software.bandwidth; + } + } + + for (let computer of computers) { + let newProcessingUsed = computerChanges[computer._id].processingUsed; + if (computer.system.processingUsed !== newProcessingUsed) { + const cloneComputer = duplicate($this.getEmbeddedDocument("Item", computer._id)); + cloneComputer.system.processingUsed = newProcessingUsed; + cloneComputer.system.overload = cloneComputer.system.processingUsed > cloneComputer.system.processing; + updatedComputers.push(cloneComputer); + } + } + + if (updatedComputers.length > 0) { + await $this.updateEmbeddedDocuments('Item', updatedComputers); + } + } + } + + static async calculEncumbranceAndWeight($this, parent, collection, documents, changes, options, userId) { + let recalculEncumbrance = false; + let recalculWeight = false; + + let change; + let i = 0; + for (let d of documents) { + if (changes[i].hasOwnProperty("system")) { + change = changes[i]; + + if (d.type === "armor" || + d.type === "computer" || + d.type === "gear" || + d.type === "item" || + d.type === "weapon") { + if (change.system.hasOwnProperty("quantity") || + change.system.hasOwnProperty("weight") || + change.system.hasOwnProperty("weightless") || + change.system.hasOwnProperty("container") || + change.system.hasOwnProperty("equipped") || + d.type === "armor") { + recalculWeight = true; + } + } else if (d.type === "talent" && d.system.subType === "skill") { + if (change.system.level || (change.system?.hasOwnProperty("skill") && change.system?.skill.hasOwnProperty("reduceEncumbrance"))) { + recalculEncumbrance = true; + } + } else if (d.type === "container" && (change.system.hasOwnProperty("onHand") || change.system.hasOwnProperty("weightless"))) { + recalculWeight = true; + } + } + i++; + } + + if (recalculEncumbrance || recalculWeight) { + const cloneActor = duplicate($this); + + await this.recalculateArmor($this, cloneActor); + + if (recalculEncumbrance) { + //console.log("recalculEncumbrance"); + const str = $this.system.characteristics.strength.value; + const end = $this.system.characteristics.endurance.value; + let sumSkill = 0; + $this.items.filter(x => x.type === "talent" && x.system.subType === "skill" && x.system.skill.reduceEncumbrance === true).forEach(x => sumSkill += x.system.level); + let normal = str + end + sumSkill; + let heavy = normal * 2; + + cloneActor.system.states.encumbrance = $this.system.inventory.weight > normal; + cloneActor.system.encumbrance.normal = normal; + cloneActor.system.encumbrance.heavy = heavy; + } + + if (recalculWeight) + await this.recalculateWeight($this, cloneActor); + } + } + + static async recalculateArmor($this, cloneActor) { + if (cloneActor === null || cloneActor === undefined) + cloneActor = duplicate($this); + + let armor = 0; + for (let item of $this.items) { + if (item.type === "armor") { + if (item.system.equipped === true && !isNaN(item.system.protection)) { + armor += (+item.system.protection || 0); + } + } + } + + cloneActor.system.inventory.armor = armor; + } + + static async recalculateWeight($this, cloneActor) { + + if (cloneActor === null || cloneActor === undefined) + cloneActor = duplicate($this); + + let updatedContainers = []; + let containerChanges = {}; + + //console.log("recalculWeight"); + let containers = []; + + // List all containers + for (let item of $this.items) { + if (item.system.trash === true) continue; + + if (item.type === "container") { + containers.push(item); + containerChanges[item._id] = { count: 0, weight: 0 }; + } + } + + let onHandWeight = 0; + for (let item of $this.items) { + if (item.type === "container") continue; + if (item.system.hasOwnProperty("weightless") && item.system.weightless === true) continue; + + let itemWeight = 0; + if (item.system.hasOwnProperty("weight")) { + let itemQty = item.system.quantity + if (!isNaN(itemQty) && itemQty > 0) { + itemWeight = item.system.weight; + if (itemWeight > 0) { + itemWeight *= itemQty; + } + } + + if (item.type === "armor") { + if (item.system.equipped === true) { + if (item.system.powered === true) + itemWeight = 0; + else + itemWeight *= 0.25; // mass of armor that is being worn by 75% OPTIONAL + } + } + + if (item.system.container && item.system.container.id && item.system.container.id !== "") { + // bad deleted container id + if (containerChanges.hasOwnProperty(item.system.container.id)) { + containerChanges[item.system.container.id].weight += Math.round(itemWeight * 10) / 10; + containerChanges[item.system.container.id].count += item.system.quantity; + } + } else { + onHandWeight += Math.round(itemWeight * 10) / 10; + } + } + } + + //cloneActor.system.inventory.weight = onHandWeight.toFixed(1); + + // Check containers new weight + for (let container of containers) { + let newWeight = containerChanges[container._id].weight; + let newCount = containerChanges[container._id].count; + if (container.system.weight !== newWeight || container.system.count !== newCount) { + //const cloneContainer = duplicate(); + const cloneContainer = duplicate($this.getEmbeddedDocument("Item", container._id)); + //foundry.utils.setProperty(cloneContainer, "system.weight", newWeight); + cloneContainer.system.weight = newWeight; + cloneContainer.system.count = newCount; + updatedContainers.push(cloneContainer); + + if (container.system.onHand === true && + (container.system.weight > 0 || container.system.weightless !== true)) { + onHandWeight += container.system.weight; + } + } + } + + cloneActor.system.inventory.weight = onHandWeight; + cloneActor.system.states.encumbrance = onHandWeight > $this.system.inventory.encumbrance.normal; + + + await $this.update(cloneActor); + + if (updatedContainers.length > 0) { + await $this.updateEmbeddedDocuments('Item', updatedContainers); + } + } + + static async preUpdate($this, changed, options, user) { + // Calc encumbrance + + const newStr = foundry.utils.getProperty(changed, "system.characteristics.strength.value") ?? $this.system.characteristics.strength.value; + const newEnd = foundry.utils.getProperty(changed, "system.characteristics.endurance.value") ?? $this.system.characteristics.endurance.value; + if ((newStr !== $this.system.characteristics.strength.value) || (newEnd !== $this.system.characteristics.endurance.value)) { + let sumSkill = 0; + $this.items.filter(x => x.type === "talent" && x.system.subType === "skill" && x.system.skill.reduceEncumbrance === true).forEach(x => sumSkill += x.system.level); + let normal = newStr + newEnd + sumSkill; + let heavy = normal * 2; + foundry.utils.setProperty(changed, "system.inventory.encumbrance.normal", normal); + foundry.utils.setProperty(changed, "system.inventory.encumbrance.heavy", heavy); + } + + //console.log(foundry.utils.getProperty(changed, "system.characteristics.strength.value")); + const characteristicModified = this.computeCharacteristics(changed); + const strengthValue = foundry.utils.getProperty(changed, "system.characteristics.strength.value") ?? $this.system.characteristics.strength.value; + const strengthMax = foundry.utils.getProperty(changed, "system.characteristics.strength.max") ?? $this.system.characteristics.strength.max; + const dexterityValue = foundry.utils.getProperty(changed, "system.characteristics.dexterity.value") ?? $this.system.characteristics.dexterity.value; + const dexterityMax = foundry.utils.getProperty(changed, "system.characteristics.dexterity.max") ?? $this.system.characteristics.dexterity.max; + const enduranceValue = foundry.utils.getProperty(changed, "system.characteristics.endurance.value") ?? $this.system.characteristics.endurance.value; + const enduranceMax = foundry.utils.getProperty(changed, "system.characteristics.endurance.max") ?? $this.system.characteristics.endurance.max; + const lifeValue = strengthValue + dexterityValue + enduranceValue; + const lifeMax = strengthMax + dexterityMax + enduranceMax; + + if ($this.system.life.value !== lifeValue) + foundry.utils.setProperty(changed, "system.life.value", lifeValue); + if ($this.system.life.max !== lifeMax) + foundry.utils.setProperty(changed, "system.life.max", lifeMax); + + if (characteristicModified && $this.system.personal.ucp === undefined || $this.system.personal.ucp === "") { + // calc + + } + //} + + // Apply changes in Actor size to Token width/height + // if ( "size" in (this.system.traits || {}) ) { + // const newSize = foundry.utils.getProperty(changed, "system.traits.size"); + // if ( newSize && (newSize !== this.system.traits?.size) ) { + // let size = CONFIG.DND5E.tokenSizes[newSize]; + // if ( !foundry.utils.hasProperty(changed, "prototypeToken.width") ) { + // changed.prototypeToken ||= {}; + // changed.prototypeToken.height = size; + // changed.prototypeToken.width = size; + // } + // } + // } + } + + // static applyHealing($this, amount) { + // if (isNaN(amount) || amount === 0) return; + + // const strength = $this.system.characteristics.strength; + // const dexterity = $this.system.characteristics.dexterity; + // const endurance = $this.system.characteristics.endurance; + + // const data = { + // strength: { value: strength.value }, + // dexterity: { value: dexterity.value }, + // endurance: { value: endurance.value } + // }; + + + + // $this.update({ system: { characteristics: data } }); + // } + + static applyDamage($this, amount) { + if (isNaN(amount) || amount === 0) return; + const rank1 = $this.system.config.damages.rank1; + const rank2 = $this.system.config.damages.rank2; + const rank3 = $this.system.config.damages.rank3; + + const data = {}; + data[rank1] = { value: $this.system.characteristics[rank1].value }; + data[rank2] = { value: $this.system.characteristics[rank2].value }; + data[rank3] = { value: $this.system.characteristics[rank3].value }; + + if (amount < 0) amount = Math.abs(amount); + + for (const [key, rank] of Object.entries(data)) { + if (rank.value > 0) { + if (rank.value >= amount) { + rank.value -= amount; + amount = 0; + } else { + amount -= rank.value; + rank.value = 0; + } + rank.dm = this.getModifier(rank.value); + if (amount <= 0) break; + } + } + + $this.update({ system: { characteristics: data } }); + } + + static getContainers($this) { + const containers = []; + for (let item of $this.items) { + if (item.type == "container") { + containers.push(item); + } + } + + containers.sort(this.compareByName); + + return containers; + } + + static getComputers($this) { + const containers = []; + for (let item of $this.items) { + if (item.type == "computer") { + containers.push(item); + } + } + + containers.sort(this.compareByName); + + return containers; + } + + static getSkills($this) { + const skills = []; + for (let item of $this.items) { + if (item.type === "talent" && item.system.subType === "skill") { + skills.push(item); + } + } + + skills.sort(this.compareByName); + + return skills; + } + + static computeCharacteristics(changed) { + let modified = this.computeCharacteristic(changed, "strength"); + + if (this.computeCharacteristic(changed, "dexterity") && !modified) modified = true; + if (this.computeCharacteristic(changed, "endurance") && !modified) modified = true; + if (this.computeCharacteristic(changed, "intellect") && !modified) modified = true; + if (this.computeCharacteristic(changed, "education") && !modified) modified = true; + if (this.computeCharacteristic(changed, "social") && !modified) modified = true; + if (this.computeCharacteristic(changed, "morale") && !modified) modified = true; + if (this.computeCharacteristic(changed, "luck") && !modified) modified = true; + if (this.computeCharacteristic(changed, "sanity") && !modified) modified = true; + if (this.computeCharacteristic(changed, "charm") && !modified) modified = true; + if (this.computeCharacteristic(changed, "psionic") && !modified) modified = true; + if (this.computeCharacteristic(changed, "other") && !modified) modified = true; + + return modified; + } + + static computeCharacteristic(changed, name) { + //if (isNaN(c.value) || c.value <= 0) c.value = 0; + //c.dm = this._getModifier(c.value) + const path = `system.characteristics.${name}`; + const newValue = foundry.utils.getProperty(changed, path + ".value");// || this.system.characteristics[name].value; + if (newValue) { + const dm = this.getModifier(newValue); + foundry.utils.setProperty(changed, path + ".dm", dm); + return true; + } + + return false; + } + + static getModifier(value) { + if (isNaN(value) || value <= 0) return -3; + if (value >= 1 && value <= 2) return -2; + if (value >= 3 && value <= 5) return -1; + if (value >= 6 && value <= 8) return 0; + if (value >= 9 && value <= 11) return 1; + if (value >= 12 && value <= 14) return 2; + + return 3; + } + + static compareByName(a, b) { + if (!a.hasOwnProperty("name") || !b.hasOwnProperty("name")) { + return 0; + } + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + } +} \ No newline at end of file diff --git a/src/module/actors/vehicule-sheet.js b/src/module/actors/vehicule-sheet.js new file mode 100644 index 0000000..46da955 --- /dev/null +++ b/src/module/actors/vehicule-sheet.js @@ -0,0 +1,18 @@ +export class VehiculeActorSheet extends ActorSheet { + static get defaultOptions() { + const options = super.defaultOptions; + + //if (game.user.isGM || options.editable) + // options.dragDrop.push({ dragSelector: ".drag-item-list", dropSelector: ".drop-item-list" }); + + return foundry.utils.mergeObject(options, { + classes: ["mgt2", game.settings.get("mgt2", "theme"), "actor", "vehicule", "nopad"], + template: "systems/mgt2/templates/actors/vehicule-sheet.html", + width: 780, + //height: 600, + tabs: [ + { navSelector: ".sheet-sidebar", contentSelector: "form" } + ] + }); + } +} \ No newline at end of file diff --git a/src/module/chatHelper.js b/src/module/chatHelper.js new file mode 100644 index 0000000..dfed04d --- /dev/null +++ b/src/module/chatHelper.js @@ -0,0 +1,126 @@ +import { MGT2Helper } from "./helper.js"; + +export class ChatHelper { + + + // _injectContent(message, type, html) { + + // _setupCardListeners(message, html); + + // } + + + static setupCardListeners(message, html, messageData) { + if (!message || !html) { + return; + } + // if (SettingsUtility.getSettingValue(SETTING_NAMES.MANUAL_DAMAGE_MODE) > 0) { + // html.find('.card-buttons').find(`[data-action='rsr-${ROLL_TYPE.DAMAGE}']`).click(async event => { + // await _processDamageButtonEvent(message, event); + // }); + // } + html.find('button[data-action="rollDamage"]').click(async event => { + //ui.notifications.warn("rollDamage"); + await this._processRollDamageButtonEvent(message, event); + }); + + html.find('button[data-action="damage"]').click(async event => { + //ui.notifications.warn("damage"); + await this._applyChatCardDamage(message, event); + //await _processApplyButtonEvent(message, event); + }); + + html.find('button[data-action="healing"]').click(async event => { + ui.notifications.warn("healing"); + //await _processApplyTotalButtonEvent(message, event); + }); + + html.find('button[data-index]').click(async event => { + + await this._processRollButtonEvent(message, event); + }); + } + + static async _processRollButtonEvent(message, event) { + event.preventDefault(); + event.stopPropagation(); + let buttons = message.flags.mgt2.buttons; + const index = event.target.dataset.index; + const button = buttons[index]; + let roll = await new Roll(button.formula, {}).roll({ async: true }); + //console.log(message); + + const chatData = { + user: game.user.id, + speaker: message.speaker, + formula: roll._formula, + tooltip: await roll.getTooltip(), + total: Math.round(roll.total * 100) / 100, + //formula: isPrivate ? "???" : roll._formula, + //tooltip: isPrivate ? "" : await roll.getTooltip(), + //total: isPrivate ? "?" : Math.round(roll.total * 100) / 100, + type: CONST.CHAT_MESSAGE_TYPES.ROLL, + rollObjectName: button.message.objectName, + rollMessage: MGT2Helper.format(button.message.flavor, Math.round(roll.total * 100) / 100), + }; + + const html = await renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); + chatData.content = html; + return roll.toMessage(chatData); + } + + static async _processRollDamageButtonEvent(message, event) { + event.preventDefault(); + event.stopPropagation(); + let rollFormula = message.flags.mgt2.damage.formula; + + let roll = await new Roll(rollFormula, {}).roll({ async: true }); + + let speaker; + let selectTokens = canvas.tokens.controlled; + if (selectTokens.length > 0) { + speaker = selectTokens[0].actor; + } else { + speaker = game.user.character; + } + + let rollTypeName = message.flags.mgt2.damage.rollTypeName ? message.flags.mgt2.damage.rollTypeName + " DAMAGE" : null; + + const chatData = { + user: game.user.id, + speaker: ChatMessage.getSpeaker({ actor: speaker }), + formula: roll._formula, + tooltip: await roll.getTooltip(), + total: Math.round(roll.total * 100) / 100, + type: CONST.CHAT_MESSAGE_TYPES.ROLL, + showButtons: true, + hasDamage: true, + rollTypeName: rollTypeName, + rollObjectName: message.flags.mgt2.damage.rollObjectName + }; + + const html = await renderTemplate("systems/mgt2/templates/chat/roll.html", chatData); + chatData.content = html; + + return roll.toMessage(chatData); + } + + async _processDamageButtonEvent(message, event) { + event.preventDefault(); + event.stopPropagation(); + + //message.flags[MODULE_SHORT].manualDamage = false + //message.flags[MODULE_SHORT].renderDamage = true; + // current user/actor + + await ItemUtility.runItemAction(null, message, ROLL_TYPE.DAMAGE); + } + + static _applyChatCardDamage(message, event) { + const roll = message.rolls[0]; + return Promise.all(canvas.tokens.controlled.map(t => { + const a = t.actor; + return a.applyDamage(roll.total); + })); + } +} \ No newline at end of file diff --git a/src/module/config.js b/src/module/config.js new file mode 100644 index 0000000..33a55df --- /dev/null +++ b/src/module/config.js @@ -0,0 +1,157 @@ +export const MGT2 = {}; + +MGT2.MetricRange = Object.freeze({ + meter: "MGT2.MetricRange.meter", + kilometer: "MGT2.MetricRange.kilometer" +}); + +MGT2.MetricWeight = Object.freeze({ + kilogram: "MGT2.MetricWeight.kilogram", + ton: "MGT2.MetricWeight.ton" +}); + +MGT2.Difficulty = Object.freeze({ + NA: "MGT2.Difficulty.NA", + Simple: "MGT2.Difficulty.Simple", + Easy: "MGT2.Difficulty.Easy", + Routine: "MGT2.Difficulty.Routine", + Average: "MGT2.Difficulty.Average", + Difficult: "MGT2.Difficulty.Difficult", + VeryDifficult: "MGT2.Difficulty.VeryDifficult", + Formidable: "MGT2.Difficulty.Formidable", + Impossible: "MGT2.Difficulty.Impossible" +}); + +MGT2.ItemSubType = Object.freeze({ + loot: "MGT2.ItemSubType.loot", + software: "MGT2.ItemSubType.software" +}); + +MGT2.EquipmentSubType = Object.freeze({ + augment: "MGT2.EquipmentSubType.augment", + clothing: "MGT2.EquipmentSubType.clothing", + equipment: "MGT2.EquipmentSubType.equipment", + trinket: "MGT2.EquipmentSubType.trinket", + toolkit: "MGT2.EquipmentSubType.toolkit" +}); + +MGT2.TalentSubType = Object.freeze({ + skill: "MGT2.TalentSubType.skill", + psionic: "MGT2.TalentSubType.psionic" +}); + +MGT2.DiseaseSubType = Object.freeze({ + disease: "MGT2.DiseaseSubType.disease", + poison: "MGT2.DiseaseSubType.poison", + wound: "MGT2.DiseaseSubType.wound" +}); + +MGT2.PsionicReach = Object.freeze({ + NA: "MGT2.PsionicReach.NA", + Personal: "MGT2.PsionicReach.Personal", + Close: "MGT2.PsionicReach.Close", + Short: "MGT2.PsionicReach.Short", + Medium: "MGT2.PsionicReach.Medium", + Long: "MGT2.PsionicReach.Long", + VeryLong: "MGT2.PsionicReach.VeryLong", + Distant: "MGT2.PsionicReach.Distant", + VeryDistant: "MGT2.PsionicReach.VeryDistant", + Continental: "MGT2.PsionicReach.Continental", + Planetary: "MGT2.PsionicReach.Planetary" +}); + +MGT2.ContactRelations = Object.freeze({ + Allie: "MGT2.Contact.Relation.Allie", + Contact: "MGT2.Contact.Relation.Contact", + Rival: "MGT2.Contact.Relation.Rival", + Enemy: "MGT2.Contact.Relation.Enemy" +}); + +MGT2.ContactStatus = Object.freeze({ + Alive: "MGT2.Contact.Status.Alive", + Unknow: "MGT2.Contact.Status.Unknow", + Dead: "MGT2.Contact.Status.Dead" +}); + +MGT2.Attitudes = Object.freeze({ + Unknow: "MGT2.Contact.Attitude.Unknow", + Hostile: "MGT2.Contact.Attitude.Hostile", + Unfriendly: "MGT2.Contact.Attitude.Unfriendly", + Indifferent: "MGT2.Contact.Attitude.Indifferent", + Friendly: "MGT2.Contact.Attitude.Friendly", + Helpful: "MGT2.Contact.Attitude.Helpful", + Complicated: "MGT2.Contact.Attitude.Complicated" +}); + +MGT2.Characteristics = Object.freeze({ + strength: "MGT2.Characteristics.strength.name", + dexterity: "MGT2.Characteristics.dexterity.name", + endurance: "MGT2.Characteristics.endurance.name", + intellect: "MGT2.Characteristics.intellect.name", + education: "MGT2.Characteristics.education.name", + social: "MGT2.Characteristics.social.name", + morale: "MGT2.Characteristics.morale.name", + luck: "MGT2.Characteristics.luck.name", + sanity: "MGT2.Characteristics.sanity.name", + charm: "MGT2.Characteristics.charm.name", + psionic: "MGT2.Characteristics.psionic.name", + other: "MGT2.Characteristics.other.name" +}); + +MGT2.InitiativeCharacteristics = Object.freeze({ + dexterity: "MGT2.Characteristics.dexterity.name", + intellect: "MGT2.Characteristics.intellect.name" +}); + +MGT2.DamageCharacteristics = Object.freeze({ + strength: "MGT2.Characteristics.strength.name", + dexterity: "MGT2.Characteristics.dexterity.name", + endurance: "MGT2.Characteristics.endurance.name" +}); + +MGT2.TL = Object.freeze({ + NA: "MGT2.TL.NA", + Unknow: "MGT2.TL.Unknow", + NotIdentified: "MGT2.TL.NotIdentified", + TL00: "MGT2.TL.L00", + TL01: "MGT2.TL.L01", + TL02: "MGT2.TL.L02", + TL03: "MGT2.TL.L03", + TL04: "MGT2.TL.L04", + TL05: "MGT2.TL.L05", + TL06: "MGT2.TL.L06", + TL07: "MGT2.TL.L07", + TL08: "MGT2.TL.L08", + TL09: "MGT2.TL.L09", + TL10: "MGT2.TL.L10", + TL11: "MGT2.TL.L11", + TL12: "MGT2.TL.L12", + TL13: "MGT2.TL.L13", + TL14: "MGT2.TL.L14", + TL15: "MGT2.TL.L15" +}); + +MGT2.Timeframes = Object.freeze({ + Normal: "MGT2.Timeframes.Normal", + Slower: "MGT2.Timeframes.Slower", + Faster: "MGT2.Timeframes.Faster" +}); + +MGT2.SpeedBands = Object.freeze({ + Stoppped: "MGT2.SpeedBands.Stoppped", + Idle: "MGT2.SpeedBands.Idle", + VerySlow: "MGT2.SpeedBands.VerySlow", + Slow: "MGT2.SpeedBands.Slow", + Medium: "MGT2.SpeedBands.Medium", + High: "MGT2.SpeedBands.High.", + Fast: "MGT2.SpeedBands.Fast", + VeryFast: "MGT2.SpeedBands.VeryFast", + Subsonic: "MGT2.SpeedBands.Subsonic", + Hypersonic: "MGT2.SpeedBands.Hypersonic" +}); + +MGT2.Durations = Object.freeze({ + Seconds: "MGT2.Durations.Seconds", + Minutes: "MGT2.Durations.Minutes", + Heures: "MGT2.Durations.Heures" +}); \ No newline at end of file diff --git a/src/module/constants.js b/src/module/constants.js new file mode 100644 index 0000000..f2f79f6 --- /dev/null +++ b/src/module/constants.js @@ -0,0 +1 @@ +export const ATTRIBUTE_TYPES = ["String", "Number", "Boolean", "Formula", "Resource"]; \ No newline at end of file diff --git a/src/module/core.js b/src/module/core.js new file mode 100644 index 0000000..a3bda7b --- /dev/null +++ b/src/module/core.js @@ -0,0 +1,124 @@ +import { + CharacterData, + ItemData, + EquipmentData, + DiseaseData, + CareerData, + TalentData, + ContactData, + ArmorData, + ComputerData, + WeaponData, + ItemContainerData, + SpeciesData +} from "./datamodels.js"; + +import { MGT2 } from "./config.js"; +import { TravellerActor, MGT2Combatant } from "./actors/actor.js"; +import { TravellerItem } from "./item.js"; +import { TravellerItemSheet } from "./item-sheet.js"; +import { TravellerActorSheet } from "./actors/character-sheet.js"; +import { preloadHandlebarsTemplates } from "./templates.js"; +//import { MGT2Helper } from "./helper.js"; +import {ChatHelper} from "./chatHelper.js"; + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ +import { registerSettings } from "./settings.js"; + +function registerHandlebarsHelpers() { + Handlebars.registerHelper('showDM', function (dm) { + if (dm === 0) return "0"; + if (dm > 0) return `+${dm}`; + if (dm < 0) return `${dm}`; + return ""; + }); +} + +Hooks.once("init", async function () { + CONFIG.MGT2 = MGT2; + CONFIG.Combat.initiative = { + formula: "2d6 + @initiative", + decimals: 2 + }; + + CONFIG.Actor.trackableAttributes = { + character: { + bar: ["life", + "characteristics.strength", + "characteristics.dexterity", + "characteristics.endurance", + "characteristics.intellect", + "characteristics.education", + "characteristics.social", + "characteristics.morale", + "characteristics.luck", + "characteristics.sanity", + "characteristics.charm", + "characteristics.psionic", + "characteristics.other" + ], + value: ["life.value", + "health.radiations", + "characteristics.strength.value", + "characteristics.dexterity.value", + "characteristics.endurance.value", + "characteristics.intellect.value", + "characteristics.education.value", + "characteristics.social.value", + "characteristics.morale.value", + "characteristics.luck.value", + "characteristics.sanity.value", + "characteristics.charm.value", + "characteristics.psionic.value", + "characteristics.other.value"] + } + }; + + game.mgt2 = { + TravellerActor, + TravellerItem + }; + + registerHandlebarsHelpers(); + registerSettings(); + + CONFIG.Combatant.documentClass = MGT2Combatant; + CONFIG.Actor.documentClass = TravellerActor; + CONFIG.Item.documentClass = TravellerItem; + + Actors.unregisterSheet("core", ActorSheet); + Actors.registerSheet("mgt2", TravellerActorSheet, { types: ["character"], makeDefault: true, label: "Traveller Sheet" }); + + Items.unregisterSheet("core", ItemSheet); + Items.registerSheet("mgt2", TravellerItemSheet, { makeDefault: true }); + + Object.assign(CONFIG.Actor.dataModels, { + "character": CharacterData + }); + + Object.assign(CONFIG.Item.dataModels, { + "item": ItemData, + "equipment": EquipmentData, + "disease": DiseaseData, + "career": CareerData, + "talent": TalentData, + "contact": ContactData, + "weapon": WeaponData, + "computer": ComputerData, + "armor": ArmorData, + "container": ItemContainerData, + "species": SpeciesData + }); + + + Hooks.on("renderChatMessage", (message, html, messageData) => { + ChatHelper.setupCardListeners(message, html, messageData); + }); + + // Preload template partials + await preloadHandlebarsTemplates(); +}); + +export { MGT2 }; \ No newline at end of file diff --git a/src/module/datamodels.js b/src/module/datamodels.js new file mode 100644 index 0000000..8098027 --- /dev/null +++ b/src/module/datamodels.js @@ -0,0 +1,485 @@ +// https://foundryvtt.com/article/system-data-models/ +// https://foundryvtt.com/api/classes/foundry.data.fields.NumberField.html +// https://foundryvtt.com/api/v10/classes/foundry.data.fields.DataField.html +const fields = foundry.data.fields; + +export class CharacterData extends foundry.abstract.TypeDataModel { + + static defineSchema() { + // XP + return { + name: new fields.StringField({ required: false, blank: false, trim: true }), + life: new fields.SchemaField({ + value: new fields.NumberField({ required: false, initial: 0, integer: true }), + max: new fields.NumberField({ required: true, initial: 0, integer: true }) + }), + personal: new fields.SchemaField({ + title: new fields.StringField({ required: false, blank: true, trim: true }), + species: new fields.StringField({ required: false, blank: true, trim: true }), + speciesText: new fields.SchemaField({ + description: new fields.StringField({ required: false, blank: true, trim: true, nullable: true }), + descriptionLong: new fields.HTMLField({ required: false, blank: true, trim: true }) + }), + age: new fields.StringField({ required: false, blank: true, trim: true }), + gender: new fields.StringField({ required: false, blank: true, trim: true }), + pronouns: new fields.StringField({ required: false, blank: true, trim: true }), + homeworld: new fields.StringField({ required: false, blank: true, trim: true }), + ucp: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }), + traits: new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField({ required: true, blank: true, trim: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ) + }), + biography: new fields.HTMLField({ required: false, blank: true, trim: true }), + + characteristics: new fields.SchemaField({ + strength: createCharacteristicField(true, true), + dexterity: createCharacteristicField(true, true), + endurance: createCharacteristicField(true, true), + intellect: createCharacteristicField(true, false), + education: createCharacteristicField(true, false), + social: createCharacteristicField(true, false), + morale: createCharacteristicField(true, false), + luck: createCharacteristicField(true, false), + sanity: createCharacteristicField(true, false), + charm: createCharacteristicField(true, false), + psionic: createCharacteristicField(true, false), + other: createCharacteristicField(true, false) + }), + + health: new fields.SchemaField({ + radiations: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }) + }), + study: new fields.SchemaField({ + skill: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }), + total: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }), + completed: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }) + }), + finance: new fields.SchemaField({ + pension: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + credits: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + cashOnHand: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + debt: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + livingCost: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + monthlyShipPayments: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + notes: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }) + }), + containerView: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }), + containerDropIn: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }), + notes: new fields.HTMLField({ required: false, blank: true, trim: true }), + + inventory: new fields.SchemaField({ + armor: new fields.NumberField({ required: true, initial: 0, integer: true }), + weight: new fields.NumberField({ required: true, initial: 0, min: 0, integer: false }), + encumbrance: new fields.SchemaField({ + normal: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + heavy: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }) + }) + }), + states: new fields.SchemaField({ + encumbrance: new fields.BooleanField({ required: false, initial: false }), + fatigue: new fields.BooleanField({ required: false, initial: false }), + unconscious: new fields.BooleanField({ required: false, initial: false }), + surgeryRequired: new fields.BooleanField({ required: false, initial: false }) + }), + + config: new fields.SchemaField({ + psionic: new fields.BooleanField({ required: false, initial: true }), + initiative: new fields.StringField({ required: false, blank: true, initial: "dexterity" }), + damages: new fields.SchemaField({ + rank1: new fields.StringField({ required: false, blank: true, initial: "strength" }), + rank2: new fields.StringField({ required: false, blank: true, initial: "dexterity" }), + rank3: new fields.StringField({ required: false, blank: true, initial: "endurance" }) + }) + }) + }; + } +} + +// export class CreatureData extends foundry.abstract.TypeDataModel { +// static defineSchema() { +// return { +// name: new fields.StringField({ required: false, blank: false, trim: true }), +// TL: new fields.StringField({ required: true, blank: false, initial: "NA" }), +// species: new fields.StringField({ required: false, blank: true, trim: true }), +// //cost: new fields.NumberField({ required: true, integer: true }), +// armor: new fields.NumberField({ required: false, initial: 0, integer: true }), +// life: new fields.SchemaField({ +// value: new fields.NumberField({ required: false, initial: 0, integer: true }), +// max: new fields.NumberField({ required: true, initial: 0, integer: true }) +// }), + +// speed: new fields.StringField({ required: false, initial: "4m", blank: true, trim: true }), + +// traits: new fields.ArrayField( +// new fields.SchemaField({ +// name: new fields.StringField({ required: true, blank: true, trim: true }), +// description: new fields.StringField({ required: false, blank: true, trim: true }) +// }) +// ), + +// description: new fields.HTMLField({ required: false, blank: true, trim: true }), +// behaviour: new fields.StringField({ required: false, blank: true, trim: true }) +// } +// }; +// } + +// export class NPCData extends CreatureData { +// static defineSchema() { +// const schema = super.defineSchema(); +// // Species, Gender, Age +// // STR, DEX, END, INT,. EDU, SOC, PSI, SKILL/Psy, equipment +// // Status +// schema.secret = new fields.HTMLField({ required: false, blank: true, trim: true }); + +// return schema; +// } +// } + +export class VehiculeData extends foundry.abstract.TypeDataModel { + + static defineSchema() { + return { + name: new fields.StringField({ required: false, blank: false, trim: true }), + + skillId: new fields.StringField({ required: false, initial: "", blank: true, trim: true }), + speed: new fields.SchemaField({ + cruise: new fields.StringField({ required: false, initial: "Slow", blank: true }), + maximum: new fields.StringField({ required: false, initial: "Medium", blank: true }) + }), + agility: new fields.NumberField({ required: false, min: 0, integer: true }), + crew: new fields.NumberField({ required: false, min: 0, integer: true }), + passengers: new fields.NumberField({ required: false, min: 0, integer: true }), + cargo: new fields.NumberField({ required: false, min: 0, integer: false }), + //hull + life: new fields.SchemaField({ + value: new fields.NumberField({ required: true, initial: 0, integer: true }), + max: new fields.NumberField({ required: true, initial: 0, integer: true }) + }), + shipping: new fields.NumberField({ required: false, min: 0, integer: true }), + cost: new fields.NumberField({ required: false, min: 0, integer: true }), + armor: new fields.SchemaField({ + front: new fields.NumberField({ required: true, initial: 0, integer: true }), + rear: new fields.NumberField({ required: true, initial: 0, integer: true }), + sides: new fields.NumberField({ required: true, initial: 0, integer: true }) + }), + + skills: new fields.SchemaField({ + // Skill Level + autopilot: new fields.NumberField({ required: true, initial: 0, integer: true }) + // Communication Range + // Navigation + // Sensors + // Camouflage / Recon + // Stealth + }) + // config: new fields.SchemaField({ + // }) + }; + } +} + +class ItemBaseData extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + const schema = { + //name: new fields.StringField({ required: true, blank: true, trim: true, nullable: true }), + description: new fields.StringField({ required: false, blank: true, trim: true, nullable: true }), + //type: new fields.StringField({ required: false, blank: false }), + subType: new fields.StringField({ required: false, blank: false, nullable: true }) + }; + + return schema; + } +} + +class PhysicalItemData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + schema.quantity = new fields.NumberField({ required: true, initial: 1, min: 0, integer: true }); + schema.weight = new fields.NumberField({ required: true, initial: 0, min: 0, integer: false }); + schema.weightless = new fields.BooleanField({ required: false, initial: false }); + schema.cost = new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }); + schema.tl = new fields.StringField({ required: true, blank: false, initial: "TL12" }); + schema.container = new fields.SchemaField({ + //inContainer: new fields.BooleanField({ required: false, initial: false }), + id: new fields.StringField({ required: false, blank: true }) + }); + + schema.roll = new fields.SchemaField({ + characteristic: new fields.StringField({ required: false, blank: true, trim: true }), + skill: new fields.StringField({ required: false, blank: true, trim: true }), + difficulty: new fields.StringField({ required: false, blank: true, trim: true }) + }); + + schema.trash = new fields.BooleanField({ required: false, initial: false }); + + return schema; + } +} + +export class ItemData extends PhysicalItemData { + static defineSchema() { + const schema = super.defineSchema(); + schema.subType.initial = "loot"; + schema.software = new fields.SchemaField({ + bandwidth: new fields.NumberField({ required: false, initial: 0, min: 0, max: 10, integer: true }), + effect: new fields.StringField({ required: false, blank: true, trim: true, initial: "" }), + computerId: new fields.StringField({ required: false, blank: true, initial: "" }) + }); + return schema; + } +} + +export class EquipmentData extends PhysicalItemData { + static defineSchema() { + const schema = super.defineSchema(); + // augment, clothes + schema.equipped = new fields.BooleanField({ required: false, initial: false }); + //schema.skillModifier = new fields.StringField({ required: false, blank: true }); + //schema.characteristicModifier = new fields.StringField({ required: false, blank: true }); + + schema.augment = new fields.SchemaField({ + improvement: new fields.StringField({ required: false, blank: true, trim: true }) + }); + + schema.subType.initial = "equipment"; // augment, clothing, trinket, toolkit, equipment + + return schema; + } +} + +export class DiseaseData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + schema.subType.initial = "disease"; // disease;poison + schema.difficulty = new fields.StringField({ required: true, initial: "Average" }); + schema.damage = new fields.StringField({ required: false, blank: true }); + schema.interval = new fields.StringField({ required: false, blank: true }); + return schema; + } +} + +export class CareerData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.difficulty = new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }); + schema.damage = new fields.StringField({ required: false, blank: true }); + schema.interval = new fields.StringField({ required: false, blank: true }); + + schema.assignment = new fields.StringField({ required: false, blank: true }); + schema.terms = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.rank = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.events = new fields.ArrayField( + new fields.SchemaField({ + age: new fields.NumberField({ required: false, integer: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ); + + return schema; + } +} + +export class TalentData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.subType.initial = "skill"; + schema.cost = new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }) + schema.level = new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }) + schema.skill = new fields.SchemaField({ + speciality: new fields.StringField({ required: false, blank: true, trim: true }), + reduceEncumbrance: new fields.BooleanField({ required: false, initial: false }) + }); + + schema.psionic = new fields.SchemaField({ + reach: new fields.StringField({ required: false, blank: true, trim: true }), + cost: new fields.NumberField({ required: false, initial: 1, min: 0, integer: true }), + duration: new fields.StringField({ required: false, blank: true, trim: true }), + durationUnit: new fields.StringField({ required: false }) + }); + + schema.roll = new fields.SchemaField({ + characteristic: new fields.StringField({ required: false, blank: true, trim: true }), + skill: new fields.StringField({ required: false, blank: true, trim: true }), + difficulty: new fields.StringField({ required: false, blank: true, trim: true }) + }); + + return schema; + } +} + +export class ContactData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.subType.initial = "skill"; + schema.cost = new fields.NumberField({ required: true, initial: 1, min: 0, integer: true }) + + schema.skill = new fields.SchemaField({ + speciality: new fields.StringField({ required: false, blank: true, trim: true }), + characteristic: new fields.StringField({ required: false, blank: true, trim: true }) + }); + + schema.status = new fields.StringField({ required: false, blank: true, trim: true, initial: "Alive" }); + schema.attitude = new fields.StringField({ required: false, blank: true, trim: true, initial: "Unknow" }); + schema.relation = new fields.StringField({ required: false, blank: true, trim: true, initial: "Contact" }); + schema.title = new fields.StringField({ required: false, blank: true, trim: true }); + schema.nickname = new fields.StringField({ required: false, blank: true, trim: true }); + schema.species = new fields.StringField({ required: false, blank: true, trim: true }); + schema.gender = new fields.StringField({ required: false, blank: true, trim: true }); + schema.pronouns = new fields.StringField({ required: false, blank: true, trim: true }); + schema.homeworld = new fields.StringField({ required: false, blank: true, trim: true }); + schema.location = new fields.StringField({ required: false, blank: true, trim: true }); + schema.occupation = new fields.StringField({ required: false, blank: true, trim: true }); + schema.notes = new fields.HTMLField({ required: false, blank: true, trim: true }); + + return schema; + } +} + +export class WeaponData extends PhysicalItemData { + static defineSchema() { + const schema = super.defineSchema(); + schema.equipped = new fields.BooleanField({ required: false, initial: false }); + schema.range = new fields.SchemaField({ + isMelee: new fields.BooleanField({ required: false, initial: false }), + value: new fields.NumberField({ required: false, integer: true, nullable: true }), + unit: new fields.StringField({ required: false, blank: true, nullable: true }) + }), + //schema.tons = new fields.NumberField({ required: false, initial: 0, min: 0, integer: false }); + schema.damage = new fields.StringField({ required: false, blank: true, trim: true }); + schema.magazine = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.magazineCost = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.traits = new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField({ required: true, blank: true, trim: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ); + schema.options = new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField({ required: true, blank: true, trim: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ); + + return schema; + } +} + +export class ArmorData extends PhysicalItemData { + static defineSchema() { + const schema = super.defineSchema(); + schema.equipped = new fields.BooleanField({ required: false, initial: false }); + schema.radiations = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.protection = new fields.StringField({ required: false, blank: false, trim: true }); + + // Some armours have a required skill. A Traveller suffers DM-1 to all checks taken in the armour per missing + // skill level. For example, a Traveller with Vacc Suit skill 0 who is in a suit that requires Vacc Suit 2 would have + // DM-2 to all their checks. Not having the skill at all inflicts the usual DM-3 unskilled penalty instead. + schema.requireSkill = new fields.StringField({ required: false, blank: false }); + schema.requireSkillLevel = new fields.NumberField({ required: false, min: 0, integer: true }); + + //requirements: new fields.StringField({ required: false, blank: false, trim: true }), + + // As powered armour, battle dress supports its own weight. While powered and active, the mass of battle dress + // does not count against the encumbrance of the wearer and is effectively weightless. + schema.powered = new fields.BooleanField({ required: false, initial: false }); + schema.options = new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField({ required: true, blank: true, trim: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ); + + // Characteristics Modifiers (Pirate of Drinax - ASLAN BATTLE DRESS STR/DEX, Slot) + + return schema; + } +} + +export class ComputerData extends PhysicalItemData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.processing = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.processingUsed = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.overload = new fields.BooleanField({ required: false, initial: false }); + //schema.softwares = new fields.ArrayField(new fields.StringField({ required: false, blank: true, trim: true })); + schema.options = new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField({ required: true, blank: true, trim: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ); + + return schema; + } +} + +export class SoftwareData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.bandwidth = new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }); + schema.inUse = new fields.BooleanField({ required: false, initial: false }); + schema.computer = new fields.StringField({ required: false, blank: true, nullable: true }); + + return schema; + } +} + +export class SpeciesData extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + const schema = { + description: new fields.StringField({ required: false, blank: true, trim: true, nullable: true }), + descriptionLong: new fields.HTMLField({ required: false, blank: true, trim: true }), + traits: new fields.ArrayField( + new fields.SchemaField({ + name: new fields.StringField({ required: true, blank: true, trim: true }), + description: new fields.StringField({ required: false, blank: true, trim: true }) + }) + ), + modifiers: new fields.ArrayField( + new fields.SchemaField({ + characteristic: new fields.StringField({ required: false, blank: true, trim: true }), + value: new fields.NumberField({ required: false, integer: true, nullable: true }) + }) + ) + }; + + return schema; + } +} + +export class ItemContainerData extends ItemBaseData { + static defineSchema() { + const schema = super.defineSchema(); + + schema.onHand = new fields.BooleanField({ required: false, initial: false }); + schema.location = new fields.StringField({ required: false, blank: true, trim: true }); + schema.count = new fields.NumberField({ required: false, initial: 0, integer: true }); + schema.weight = new fields.NumberField({ required: false, initial: 0, integer: false }); + schema.weightless = new fields.BooleanField({ required: false, initial: false }); + + schema.locked = new fields.BooleanField({ required: false, initial: false }); // GM only + schema.lockedDescription = new fields.StringField({ required: false, blank: true, trim: true, nullable: true }); + return schema; + } +} + +function createCharacteristicField(show = true, showMax = false) { + return new fields.SchemaField({ + value: new fields.NumberField({ required: true, initial: 0, min: 0, integer: true }), + max: new fields.NumberField({ required: false, initial: 0, min: 0, integer: true }), + dm: new fields.NumberField({ required: false, initial: 0, integer: true }), + show: new fields.BooleanField({ required: false, initial: show }), + showMax: new fields.BooleanField({ required: false, initial: showMax }) + }); +} \ No newline at end of file diff --git a/src/module/helper.js b/src/module/helper.js new file mode 100644 index 0000000..b7f8048 --- /dev/null +++ b/src/module/helper.js @@ -0,0 +1,230 @@ +export class MGT2Helper { + static POUNDS_CONVERT = 2.20462262185; + + static decimalSeparator; + static badDecimalSeparator; + + static { + this.decimalSeparator = Number(1.1).toLocaleString().charAt(1); + this.badDecimalSeparator = (this.decimalSeparator === "." ? "," : "."); + } + + static format = function() { + var s = arguments[0]; + for (var i = 0; i < arguments.length - 1; i++) { + var reg = new RegExp("\\{" + i + "\\}", "gm"); + s = s.replace(reg, arguments[i + 1]); + } + return s; + } + + static hasValue(object, property) { + return object !== undefined && object.hasOwnProperty(property) && object[property] !== null && object[property] !== undefined && object[property] !== ""; + } + + static getItemsWeight(items) { + let weight = 0; + for (let i of items) { + let item = i.hasOwnProperty("system") ? i.system : i; + if (item.hasOwnProperty("weightless") && item.weightless === true) { + continue; + } + + if (item.hasOwnProperty("weight")) { + let itemQty = item.quantity + if (!isNaN(itemQty) && itemQty > 0) { + let itemWeight = item.weight; + if (itemWeight > 0) { + weight += itemWeight * itemQty; + } + } + } + } + return weight; + } + + static generateUID() { + let result = ''; + const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < 36; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters.charAt(randomIndex); + if (i === 8 || i === 12 || i === 16 || i === 20) + result += "-"; + } + + return result; + } + + static compareByName(a, b) { + if (!a.hasOwnProperty("name") || !b.hasOwnProperty("name")) { + return 0; + } + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + } + + static getDisplayDM(dm) { + if (dm === 0) return " (0)"; + if (dm > 0) return ` (+${dm})`; + if (dm < 0) return ` (${dm})`; + return ""; + } + static getFormulaDM(dm) { + if (dm === 0) return "+0"; + if (dm > 0) return `+${dm}`; + if (dm < 0) return `${dm}`; + return ""; + } + + static getDiceResults(roll) { + const results = []; + for (const die of roll.dice) { + results.push(die.results); + } + return results.flat(2); + } + + static getDiceTotal(roll) { + let total = 0; + for (const die of roll.dice) { + total += die.total; + } + return total; + } + + static getDifficultyValue(difficulty) { + switch(difficulty) { + case "Simple": return 2; + case "Easy": return 4; + case "Routine": return 6; + case "Average": return 8; + case "Difficult": return 10; + case "VeryDifficult": return 12; + case "Formidable": return 14; + case "Impossible": return 16; + default: + return 0; + } + } + + static getDifficultyDisplay(difficulty) { + switch(difficulty) { + case "Simple": return game.i18n.localize("MGT2.Difficulty.Simple") + " (2+)"; + case "Easy": return game.i18n.localize("MGT2.Difficulty.Easy") + " (4+)"; + case "Routine": return game.i18n.localize("MGT2.Difficulty.Routine") + " (6+)"; + case "Average": return game.i18n.localize("MGT2.Difficulty.Average") + " (8+)"; + case "Difficult": return game.i18n.localize("MGT2.Difficulty.Difficult") + " (10+)"; + case "VeryDifficult": return game.i18n.localize("MGT2.Difficulty.VeryDifficult") + " (12+)"; + case "Formidable": return game.i18n.localize("MGT2.Difficulty.Formidable") + " (14+)"; + case "Impossible": return game.i18n.localize("MGT2.Difficulty.Impossible") + " (16+)"; + default: + return null; + } + } + + static getRangeDisplay(range) { + let value = Number(range.value); + + if (isNaN(value)) return null; + + let label; + //if (game.settings.get("mgt2", "useDistanceMetric") === true) { + if (range.unit !== null && range.unit !== undefined && range.unit !== "") + label = game.i18n.localize(`MGT2.MetricRange.${range.unit}`).toLowerCase(); + else + label = ""; + //} else { + // TODO + //} + + return `${value}${label}`; + } + + static getWeightLabel() { + //const label = game.settings.get("mgt2", "useWeightMetric") === true ? "MGT2.MetricSystem.Weight.kg" : "MGT2.ImperialSystem.Weight.lb"; + //return game.i18n.localize(label); + return game.i18n.localize("MGT2.MetricSystem.Weight.kg"); + } + + static getDistanceLabel() { + //const label = game.settings.get("mgt2", "useDistanceMetric") === true ? "MGT2.MetricSystem.Distance.km" : "MGT2.ImperialSystem.Distance.mi"; + //return game.i18n.localize(label); + return game.i18n.localize("MGT2.MetricSystem.Distance.km"); + } + + static getIntegerFromInput(data) { + return Math.trunc(this.getNumberFromInput(data)); + } + + static getNumberFromInput(data) { + if (data === undefined || data === null) return 0; + + if (typeof data === "string") { + let converted = Number(data.replace(/\s+/g, '').replace(this.badDecimalSeparator, this.decimalSeparator).trim()); + if (isNaN(converted)) + return 0; + + return converted; + } + + let converted = Number(data); + + if (isNaN(converted)) + return 0; + + return converted; + } + + static convertWeightForDisplay(weight) { + //if (game.settings.get("mgt2", "useWeightMetric") === true || weight === 0) + return weight; + + // Metric to Imperial + //const pounds = weight * this.POUNDS_CONVERT; + //return Math.round(pounds * 10) / 10; + } + + static convertWeightFromInput(weight) { + //if (game.settings.get("mgt2", "useWeightMetric") === true || weight === 0) + return Math.round(weight * 10) / 10; + + // Imperial to Metric + //const kg = this.POUNDS_CONVERT / weight; + //return Math.round(kg * 10) / 10; + } + + static getDataFromDropEvent(event) { + let data; + try { + return JSON.parse(event.dataTransfer?.getData("text/plain")); + } catch (err) { + return false; + } + + //if ( data.type !== "Item" ) return false; + //const item = await Item.implementation.fromDropData(data); + } + + static async getItemDataFromDropData(dropData) { + //console.log("getItemDataFromDropData"); + let item; + if (game.modules.get("monks-enhanced-journal")?.active && dropData.itemId && dropData.uuid.includes("JournalEntry")) { + const journalEntry = await fromUuid(dropData.uuid); + } else if (dropData.hasOwnProperty("uuid")) { + item = await fromUuid(dropData.uuid); + } else { + let uuid = `${dropData.type}.${dropData.data._id}`; + item = await fromUuid(uuid); + } + + if (!item) { + throw new Error(game.i18n.localize("Errors.CouldNotFindItem").replace("_ITEM_ID_", dropData.uuid)); + } + if (item.pack) { + const pack = game.packs.get(item.pack); + item = await pack?.getDocument(item._id); + } + return deepClone(item); + } +} \ No newline at end of file diff --git a/src/module/item-sheet.js b/src/module/item-sheet.js new file mode 100644 index 0000000..0de9415 --- /dev/null +++ b/src/module/item-sheet.js @@ -0,0 +1,402 @@ +import { MGT2Helper } from "./helper.js"; + +/** + * Extend the basic ItemSheet with some very simple modifications + * @extends {ItemSheet} + */ +export class TravellerItemSheet extends ItemSheet { + + /** @inheritdoc */ + static get defaultOptions() { + const options = super.defaultOptions; + return foundry.utils.mergeObject(options, { + classes: ["mgt2", game.settings.get("mgt2", "theme"), "sheet"], + width: 630, + tabs: [{ navSelector: ".horizontal-tabs", contentSelector: ".itemsheet-panel", initial: "tab1" }] + }); + } + + /* -------------------------------------------- */ + + get template() { + const path = "systems/mgt2/templates/items"; + return `${path}/${this.item.type}-sheet.html`; + } + + /** @inheritdoc */ + async getData(options) { + const context = await super.getData(options); + //console.log('-=getData=-'); + //console.log(context); + const item = context.item; + + const source = item.toObject(); + context.config = CONFIG.MGT2; + + const settings = {}; + settings.usePronouns = game.settings.get("mgt2", "usePronouns"); + + let containers = null; + let computers = null;; + let hadContainer; + if (context.item.actor != null) { + hadContainer = true; + containers = [{ "name": "", "_id": "" }].concat(context.item.actor.getContainers()); + computers = [{ "name": "", "_id": "" }].concat(context.item.actor.getComputers()); + } else { + hadContainer = false; + } + + let weight = null; + if (item.system.hasOwnProperty("weight")) { + weight = MGT2Helper.convertWeightForDisplay(item.system.weight); + } + let unitlabels = { + weight: MGT2Helper.getWeightLabel() + }; + let skills = []; + + if (this.actor !== null) { + for (let item of this.actor.items) { + if (item.type === "talent") { + if (item.system.subType === "skill") + skills.push({ _id: item._id, name: item.getRollDisplay() }); + } + } + } + + skills.sort(MGT2Helper.compareByName); + skills = [{ _id: "NP", name: game.i18n.localize("MGT2.Items.NotProficient") }].concat(skills); + + foundry.utils.mergeObject(context, { + source: source.system, + system: item.system, + settings: settings, + containers: containers, + computers: computers, + hadContainer: hadContainer, + weight: weight, + unitlabels: unitlabels, + editable: this.isEditable, + isGM: game.user.isGM, + skills: skills, + config: CONFIG + //rollData: this.item.getRollData(), + }); + + return context; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + activateListeners(html) { + super.activateListeners(html); + + // Everything below here is only needed if the sheet is editable + if (!this.isEditable) return; + + //let handler = ev => this._onDropCustom(ev); + + //console.log(html); + // itemsheet-panel + //html.addEventListener("dragstart", this._onDropCustom, false); + html.find('div.itemsheet-panel').each((i, li) => { + // //if (li.classList.contains("inventory-header")) return; + //li.setAttribute("draggable", true); + //li.addEventListener("drop", handler, false); + }); + + + //html.find('div.dropitem').each((i, li) => { + // //if (li.classList.contains("inventory-header")) return; + // li.setAttribute("draggable", true); + // li.addEventListener("dragstart", handler, false); + //}); + + // if (this.item.type == "weapon") { + // html.find('.trait-create').click(this._onTraitCreate.bind(this)); + // html.find('.trait-delete').click(this._onTraitDelete.bind(this)); + // } + + if (this.item.type == "career") { + html.find('.event-create').click(this._onCareerEventCreate.bind(this)); + html.find('.event-delete').click(this._onCareerEventDelete.bind(this)); + } + + else if (this.item.type == "armor" || + this.item.type == "computer" || + this.item.type == "species" || + this.item.type == "weapon") { + html.find('.options-create').click(this._onOptionCreate.bind(this)); + html.find('.options-delete').click(this._onOptionDelete.bind(this)); + } + + if (this.item.type == "species") { + html.find('.modifiers-create').click(this._onModifierEventCreate.bind(this)); + html.find('.modifiers-delete').click(this._onModifierEventDelete.bind(this)); + } + } + + async _onModifierEventCreate(event) { + event.preventDefault(); + await this._onSubmit(event); + + let modifiers = this.item.system.modifiers; + let index; + if (modifiers.length === 0) { + modifiers = {}; + modifiers["0"] = { characteristic: "Endurance", value: null }; + } else { + index = Math.max(...Object.keys(modifiers)); + index++; + modifiers[index] = { characteristic: "Endurance", value: null }; + } + + let update = { + system: { + modifiers: modifiers + } + }; + + return this.item.update(update); + } + + async _onModifierEventDelete(event) { + event.preventDefault(); + await this._onSubmit(event); + const element = event.currentTarget.closest(".modifiers-part"); + const modifiers = foundry.utils.deepClone(this.item.system.modifiers); + let index = Number(element.dataset.modifiersPart); + + const newModifiers = []; + let entries = Object.entries(modifiers); + if (entries.length > 1) { + for (const [key, value] of entries) { + if (key != index) + newModifiers.push(value); + } + } + + let update = { + system: { + modifiers: newModifiers + } + }; + + return this.item.update(update); + } + + async _onCareerEventCreate(event) { + event.preventDefault(); + await this._onSubmit(event); + + let events = this.item.system.events; + let index; + if (events.length === 0) { + events = {}; + events["0"] = { age: "", description: "" }; + } else { + index = Math.max(...Object.keys(events)); + index++; + events[index] = { age: "", description: "" }; + } + + let update = { + system: { + events: events + } + }; + + return this.item.update(update); + } + + async _onCareerEventDelete(event) { + event.preventDefault(); + await this._onSubmit(event); + const element = event.currentTarget.closest(".events-part"); + const events = foundry.utils.deepClone(this.item.system.events); + let index = Number(element.dataset.eventsPart); + + const newEvents = []; + let entries = Object.entries(events); + if (entries.length > 1) { + for (const [key, value] of entries) { + if (key != index) + newEvents.push(value); + } + } + + let update = { + system: { + events: newEvents + } + }; + + return this.item.update(update); + } + + async _onOptionCreate(event) { + event.preventDefault(); + await this._onSubmit(event); + + //const subType = event.currentTarget.dataset.subType; + const property = event.currentTarget.dataset.property; + + //let options = this.item.system[subType][property]; + let options = this.item.system[property]; + let index; + if (options.length === 0) { + options = {}; + options["0"] = { name: "", description: "" }; + } else { + index = Math.max(...Object.keys(options)); + index++; + options[index] = { name: "", description: "" }; + } + + let update = {}; + //update[`system.${subType}.${property}`] = options; + update[`system.${property}`] = options; + return this.item.update(update); + } + + async _onOptionDelete(event) { + event.preventDefault(); + await this._onSubmit(event); + const element = event.currentTarget.closest(".options-part"); + //const subType = element.dataset.subType; + const property = element.dataset.property; + //const options = foundry.utils.deepClone(this.item.system[subType][property]); + const options = foundry.utils.deepClone(this.item.system[property]); + let index = Number(element.dataset.optionsPart); + + const newOptions = []; + let entries = Object.entries(options); + if (entries.length > 1) { + for (const [key, value] of entries) { + if (key != index) + newOptions.push(value); + } + } + + let update = {}; + //update[`system.${subType}.${property}`] = newOptions; + update[`system.${property}`] = newOptions; + return this.item.update(update); + } + + // async _onTraitCreate(event) { + // event.preventDefault(); + // await this._onSubmit(event); + // const traits = this.item.system.traits; + // return this.item.update({ "system.traits.parts": traits.parts.concat([["", ""]]) }); + // } + + // async _onTraitDelete(event) { + // event.preventDefault(); + // await this._onSubmit(event); + // const element = event.currentTarget.closest(".traits-part"); + // const traits = foundry.utils.deepClone(this.item.system.traits); + // traits.parts.splice(Number(element.dataset.traitsPart), 1); + // return this.item.update({ "system.traits.parts": traits.parts }); + // } + + _getSubmitData(updateData = {}) { + const formData = foundry.utils.expandObject(super._getSubmitData(updateData)); + + // Gestion des containers + if (formData.hasOwnProperty("system") && formData.system.hasOwnProperty("container") && + (this.item.system.hasOwnProperty("equipped"))) { + //*console.log('-=_getSubmitData=-'); + //console.log(this.item.system.onHand); + //console.log(formData.system.onHand); + //const onHandChange = this.item.system.onHand !== formData.system.onHand; + const equippedChange = this.item.system.equipped !== formData.system.equipped; + const containerChange = this.item.system.container.id !== formData.system.container.id; + // Maintenant équipé + if (equippedChange) { + if (formData.system.equipped === true) { + //formData.system.onHand = true; + //console.log("clear container"); + formData.system.container = { + //inContainer: false, + id: "" + }; + } + } + + /*else if (onHandChange) { + // Maintenant à portée + if (formData.system.onHand === true) { + //console.log("clear container"); + formData.system.container = { + inContainer: false, + id: "" + }; + } else { + formData.system.equipped = false; + } + }*/ + + else if (containerChange) { + // Mise en storage + if (formData.system.container.id !== "" && (this.item.system.container.id === "" || this.item.system.container.id === null)) { + //console.log("put in container"); + //formData.system.onHand = false; + formData.system.equipped = false; + //formData.system.container.inContainer = true; + } + } + } + + // if (this.item.type == "weapon") { + // const traits = formData.system?.traits; + // if (traits) + // traits.parts = Object.values(traits?.parts || {}).map(d => [d[0] || "", d[1] || ""]); + // } + + // else if (this.item.type == "career") { + // const events = formData.system?.events; + // if (events) + // events.parts = Object.values(events?.parts || {}).map(d => [d[0] || "", d[1] || ""]); + // } + + // else if (this.item.type == "equipment") { + // if (this.item.system.subType == "armor") { + // // const armor = formData.system?.armor; + // // if (armor) + // // //options.parts = Object.values(options?.parts || {}).map(d => [d[0] || "", d[1] || ""]); + // // console.log(armor.options); + // // armor.options = Object.values(armor?.options || {}) + // // .map(d => [d.name || "", d.description || ""]); + // // console.log(armor.options); + // } else if (this.item.system.subType == "computer") { + // const computer = formData.system?.computer; + // if (computer) + // //options.parts = Object.values(options?.parts || {}).map(d => [d[0] || "", d[1] || ""]); + // computer.options = Object.values(computer?.options || {}).map(d => [d[0] || "", d[1] || ""]); + // } + // } + + if (formData.hasOwnProperty("weight")) { + formData.system.weight = MGT2Helper.convertWeightFromInput(formData.weight); + delete formData.weight; + } + + if (formData.system.hasOwnProperty("quantity")) { + formData.system.quantity = MGT2Helper.getIntegerFromInput(formData.system.quantity); + } + + if (formData.system.hasOwnProperty("cost")) { + formData.system.cost = MGT2Helper.getIntegerFromInput(formData.system.cost); + } + //console.log("before flatten"); + //console.log(formData); + //console.log("after flatten"); + // let x = foundry.utils.flattenObject(formData);; + // console.log(x); + // return x; + return foundry.utils.flattenObject(formData); + } +} diff --git a/src/module/item.js b/src/module/item.js new file mode 100644 index 0000000..e51243e --- /dev/null +++ b/src/module/item.js @@ -0,0 +1,61 @@ +export class TravellerItem extends Item { + + /** @inheritdoc */ + prepareDerivedData() { + super.prepareDerivedData(); + + } + + async _preUpdate(changed, options, user) { + if ((await super._preUpdate(changed, options, user)) === false) return false; + + if (this.type === "computer") { + // Overload + const newProcessing = foundry.utils.getProperty(changed, "system.processing") ?? this.system.processing; + if (newProcessing !== this.system.processing) { + let overload = this.system.processingUsed > newProcessing; + foundry.utils.setProperty(changed, "system.overload", overload); + } + } + + // Qty max 1 + if (this.type === "computer" || this.type === "container" || (this.type === "item" && this.system.subType === "software")) { + const newQty = foundry.utils.getProperty(changed, "system.quantity") ?? this.system.quantity; + if (newQty !== this.system.quantity && newQty > 1) { + foundry.utils.setProperty(changed, "system.quantity", 1); + } + } + + // No Weight + if (this.type === "item" && this.system.subType === "software") { + const newWeight = foundry.utils.getProperty(changed, "system.weight") ?? this.system.weight; + if (newWeight !== this.system.weight && newWeight > 0) { + foundry.utils.setProperty(changed, "system.weight", 0); + } + } + } + + getRollDisplay() { + if (this.type === "talent") { + if (this.system.subType === "skill") { + let label; + if (this.system.skill.speciality !== "" && this.system.skill.speciality !== undefined) { + label = `${this.name} (${this.system.skill.speciality})`; + } else { + label = this.name; + } + + if (this.system.level > 0) + label += ` (+${this.system.level})`; + else if (this.system.level < 0) + label += ` (${this.system.level})`; + + return label; + } else if (this.system.subType === "psionic") { + + } + } + + return name; + } +} diff --git a/src/module/roll-prompt.js b/src/module/roll-prompt.js new file mode 100644 index 0000000..ef9e143 --- /dev/null +++ b/src/module/roll-prompt.js @@ -0,0 +1,103 @@ +class RollPromptDialog extends Dialog { + constructor(dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["mgt2", game.settings.get("mgt2", "theme"), "sheet", "dialog"]; + } + + static async create(options) { + + const htmlContent = await renderTemplate('systems/mgt2/templates/roll-prompt.html', { + config: CONFIG.MGT2, + //formula: formula, + characteristics: options.characteristics, + characteristic: options.characteristic, + skills: options.skills, + skill: options.skill, + fatigue: options.fatigue, + encumbrance: options.encumbrance, + difficulty: options.difficulty + }); + + const results = new Promise(resolve => { + new this({ + title: options.title, + content: htmlContent, + buttons: { + boon: { + label: game.i18n.localize("MGT2.RollPrompt.Boon"), + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')).object; + formData.diceModifier = "dl"; + resolve(formData); + } + }, + submit: { + label: game.i18n.localize("MGT2.RollPrompt.Roll"), + icon: '', + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')).object; + resolve(formData); + }, + }, + bane: { + label: game.i18n.localize("MGT2.RollPrompt.Bane"), + //icon: '', + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')).object; + formData.diceModifier = "dh"; + resolve(formData); + } + } + } + //close: () => { resolve(false) } + }).render(true); + }); + + //console.log(Promise.resolve(results)); + return results; + } +} + +export class RollPromptHelper { + + static async roll(options) { + return await RollPromptDialog.create(options); + } + + static async promptForFruitTraits() { + const htmlContent = await renderTemplate('systems/mgt2/templateschat/chat/roll-prompt.html'); + + return new Promise((resolve, reject) => { + const dialog = new Dialog({ + title: "Fruit Traits", + content: htmlContent, + buttons: { + submit: { + label: "Roll", + icon: '', + callback: (html) => { + const formData = new FormDataExtended(html[0].querySelector('form')) + .toObject(); + + //verifyFruitInputs(formData); + + resolve(formData); + }, + }, + skip: { + label: "Cancel", + callback: () => resolve(null), + } + }, + render: (html) => { + //html.on('click', 'button[data-preset]', handleFruitPreset); + }, + close: () => { + reject('User closed dialog without making a selection.'); + }, + }); + + dialog.render(true); + }); + } +} \ No newline at end of file diff --git a/src/module/settings.js b/src/module/settings.js new file mode 100644 index 0000000..17b70a5 --- /dev/null +++ b/src/module/settings.js @@ -0,0 +1,86 @@ +export const registerSettings = function () { + + game.settings.register("mgt2", "theme", { + name: "MGT2.Settings.theme.name", + hint: "MGT2.Settings.theme.hint", + scope: "client", + config: true, + default: "black-and-red", + type: String, + choices: { + "black-and-red": "MGT2.Themes.BlackAndRed", + "mwamba": "MGT2.Themes.Mwamba", + "blue": "MGT2.Themes.Blue" + }, + requiresReload: true + }); + + game.settings.register('mgt2', 'usePronouns', { + name: "MGT2.Settings.usePronouns.name", + hint: "MGT2.Settings.usePronouns.hint", + default: false, + scope: 'world', + type: Boolean, + config: true, + requiresReload: false + }); + + game.settings.register('mgt2', 'useGender', { + name: "MGT2.Settings.useGender.name", + hint: "MGT2.Settings.useGender.hint", + default: false, + scope: 'world', + type: Boolean, + config: true, + requiresReload: false + }); + + game.settings.register('mgt2', 'showLife', { + name: "MGT2.Settings.showLife.name", + hint: "MGT2.Settings.showLife.hint", + default: false, + scope: 'world', + type: Boolean, + config: true, + requiresReload: false + }); + + // game.settings.register('mgt2', 'useWeightMetric', { + // name: "MGT2.Settings.useWeightMetric.name", + // hint: "MGT2.Settings.useWeightMetric.hint", + // default: true, + // scope: 'world', + // type: Boolean, + // config: true, + // requiresReload: true + // }); + + // game.settings.register('mgt2', 'useDistanceMetric', { + // name: "MGT2.Settings.useDistanceMetric.name", + // hint: "MGT2.Settings.useDistanceMetric.hint", + // default: true, + // scope: 'world', + // type: Boolean, + // config: true, + // requiresReload: true + // }); + + // game.settings.register('mgt2', 'showTrash', { + // name: "Show Trash tab to Player", + // hint: "Player can see the Trash tab and recover item", + // default: false, + // scope: 'world', + // type: Boolean, + // config: true, + // requiresReload: false + // }); + + /*game.settings.register('mgt2', 'containerDropIn', { + name: "Test", + hint: "Mon hint", + default: true, + scope: 'client', + type: Boolean, + config: true + });*/ +}; diff --git a/src/module/templates.js b/src/module/templates.js new file mode 100644 index 0000000..a3dbf22 --- /dev/null +++ b/src/module/templates.js @@ -0,0 +1,22 @@ +/** + * Define a set of template paths to pre-load + * Pre-loaded templates are compiled and cached for fast access when rendering + * @return {Promise} + */ +export const preloadHandlebarsTemplates = async function() { + + const templatePaths = [ + "systems/mgt2/templates/items/parts/sheet-configuration.html", + "systems/mgt2/templates/items/parts/sheet-physical-item.html", + "systems/mgt2/templates/roll-prompt.html", + "systems/mgt2/templates/chat/roll.html", + //"systems/mgt2/templates/chat/roll-characteristic.html", + "systems/mgt2/templates/actors/actor-config-sheet.html", + "systems/mgt2/templates/actors/actor-config-characteristic-sheet.html", + "systems/mgt2/templates/actors/trait-sheet.html", + "systems/mgt2/templates/editor-fullview.html" + //"systems/mgt2/templates/actors/parts/actor-characteristic.html" + ]; + + return loadTemplates(templatePaths); + }; \ No newline at end of file diff --git a/src/sass/components/_character.sass b/src/sass/components/_character.sass new file mode 100644 index 0000000..04720c7 --- /dev/null +++ b/src/sass/components/_character.sass @@ -0,0 +1,276 @@ +.characteristics-panel + .tab + padding: 4px + +.species + font-size: 13px + margin: 4px 1rem 0 1rem + text-align: justify + a + margin-right: 1rem + +.mgt2 + .characteristics + flex-basis: 138px + flex-grow: 0 + flex-shrink: 0 + position: relative + + .characteristics-header + color: var(--mgt2-color-primary) + background: var(--mgt2-bgcolor-primary) + font-family: "Rubik", monospace + font-style: normal + font-size: 1rem + line-height: 2rem + text-transform: uppercase + + .characteristic-row + display: flex + flex-direction: row + align-items: center + justify-content: center + position: relative + + .characteristic-minmax + display: flex + flex-direction: row + align-items: center + justify-content: center + flex-wrap: nowrap + + .characteristic-dm + span + &.label + font-size: 0.8rem + font-weight: 600 + + .characteristic-label + font-family: "Barlow Condensed", sans-serif + font-size: 1.2rem + font-weight: 600 + font-style: italic + text-align: center + position: relative + & > a + &.roll + color: black + position: absolute + left: 0 + a + &.cfg-characteristic + display: none + font-size: 12px + position: absolute + right: 0 + top: 0 + &:hover + a + &.cfg-characteristic + display: block + + .characteristic-input + color: var(--mgt2-bgcolor-primary) + text-align: center + font-size: 1.5rem + font-weight: 500 + width: 2.4rem + height: 2rem + box-sizing: border-box + border: none + outline: none + background: linear-gradient(45deg, #0000 5.66px, #000 0 calc(5.66px + 2px), #0000 0 calc(100% - 5.66px - 2px), #000 0 calc(100% - 5.66px), #0000 0), linear-gradient(-45deg, #0000 5.66px, #000 0 calc(5.66px + 2px), #0000 0 calc(100% - 5.66px - 2px), #000 0 calc(100% - 5.66px), #0000 0), linear-gradient(90deg, #000 4px, #0000 0) -2px 50%/100% calc(100% - 16px) repeat-x, linear-gradient(#000 4px, #0000 0) 50% -2px/calc(100% - 16px) 100% repeat-y + + .characteristic-dm + color: var(--mgt2-color-dm) + background-color: transparent + text-align: center + font-size: 1rem + width: 1.5rem + height: 1.4rem + padding: 0 + outline: none + border: none + margin: 0 + position: absolute + right: 0 + background-color: var(--mgt2-bgcolor-dm) + border-radius: 9px + margin-right: 2px + &:focus + outline: none + box-shadow: none + + .characteristic-dm-minmax + &:focus + outline: none + box-shadow: none + + .minmaxwrapper + display: flex + flex-direction: row + flex-wrap: nowrap + justify-content: center + justify-content: center + align-items: center + margin: 0 0.5rem + box-sizing: border-box + background: linear-gradient(45deg, #0000 7.07px, #000 0 calc(7.07px + 2px), #0000 0 calc(100% - 7.07px - 2px), #000 0 calc(100% - 7.07px), #0000 0), linear-gradient(-45deg, #0000 7.07px, #000 0 calc(7.07px + 2px), #0000 0 calc(100% - 7.07px - 2px), #000 0 calc(100% - 7.07px), #0000 0), linear-gradient(90deg, #000 4px, #0000 0) -2px 50%/100% calc(100% - 20px) repeat-x, linear-gradient(#000 4px, #0000 0) 50% -2px/calc(100% - 20px) 100% repeat-y + input + display: inline-block + color: black + background-color: transparent + text-align: center + font-size: 1.5rem + width: 2rem + height: 2rem + border: none + outline: none + &:focus + outline: none + box-shadow: none + &:first-child + margin-left: 5px + &:last-child + margin-right: 5px + span + font-size: 1.5rem + font-weight: 500 +.computer-overload, +.computer-overload i + color: var(--mgt2-color-warning) !important +ul + &.softwares + list-style: none + margin: 0 + padding: 0 + li + display: inline-block + color: var(--mgt2-color-software) + background-color: var(--mgt2-bgcolor-software) + padding: 3px 7px + border-radius: 8px + a + display: none + &:first-child + margin: 0 0.5rem + &:hover + a + display: inline-block + +.character-header + display: flex + margin-top: 8px + margin-right: 8px + flex-direction: row + flex-wrap: nowrap + flex-grow: 0 + flex-shrink: 0 + justify-content: flex-start + align-items: flex-start +.character-header-img + flex-basis: 138px + flex-grow: 0 + flex-shrink: 0 + text-align: center +.character-summary + flex: 0 0 100% + margin: 0 + padding: 0 + list-style: none + border-top: 5px double var(--mgt2-bgcolor-primary) + li + float: left + margin: 0 + padding: 0 + color: var(--mgt2-color-primary-light) + input + display: block + border: none + font-weight: bold + font-family: "Roboto Condensed", sans-serif + background-color: #fff + font-size: 0.8rem + border: 1px solid #fff + &:hover + border: 1px solid #111 +.character-header-body + display: flex + flex-direction: column + flex-wrap: nowrap + justify-content: flex-start + legend + font-size: 0.7rem + text-transform: uppercase + text-wrap: nowrap + i + margin-right: 0.25rem +.character-body + display: flex + flex-direction: row + align-content: flex-start + flex-wrap: nowrap + .tab + width: 100% + + +.lifes + height: 100% + display: flex + flex-direction: row + justify-content: center + align-items: center + div + font-size: 2rem +.character-states + margin: 0 + padding: 0 + list-style: none + display: flex + flex-direction: column + justify-content: flex-start + align-items: flex-start + width: 100% + float: right + li + display: flex + margin: 0 + padding: 0 + color: #4b4a44 + justify-content: space-between + align-items: center + width: 100% + font-size: 0.7rem + line-height: 1.1rem + +.encumbrance-normal + color: var(--mgt2-encumbrance-normal)!important + +.encumbrance-heavy + color: var(--mgt2-encumbrance-heavy)!important + font-weight: bold + +.character-body + height: 100% + overflow: hidden + display: flex + flex-direction: row + width: 100% + justify-content: flex-start + align-items: flex-start + border-top: 3px solid black + +.actor-footer + bottom: 0 + color: var(--mgt2-color-primary) + background-color: var(--mgt2-bgcolor-primary) + width: 100% + margin: 0 -8px + height: 1.5rem + justify-content: space-between + align-items: center + padding: 0 1rem + flex-grow: 0 + flex-shrink: 0 + display: flex + flex-direction: row diff --git a/src/sass/components/_chat-sidebar.sass b/src/sass/components/_chat-sidebar.sass new file mode 100644 index 0000000..7e371aa --- /dev/null +++ b/src/sass/components/_chat-sidebar.sass @@ -0,0 +1,47 @@ +.chat-sidebar, +.mgt2-buttons button + background: rgba(0, 0, 0, 0.1) + border: 1px solid var(--color-border-light-2) + border-radius: 3px + box-shadow: 0 0 2px #FFF inset +//.chat-message + // &.message + // color: #0A0405 + // background-color: #fff + // background-image: none +.dice-formula, +.dice-total + background-color: #fff +.mgt2-buttons + display: flex + justify-content: center + align-items: center + flex-wrap: nowrap + color: #0A0405 + margin-top: 5px + button + i + font-size: 1.1rem + padding: 0 + margin: 0 +.roll-info + display: flex + flex-direction: column +.roll-type-group + flex-direction: row + flex-wrap: wrap + justify-content: space-between + display: flex +.roll-type-name + font-size: 11px + text-transform: uppercase + color: #515151 +.roll-object-name + font-weight: 400 + font-size: 1.4rem +.roll-success + font-size: 1.2rem + font-weight: bold + text-transform: uppercase + margin-top: 1rem + text-align: center \ No newline at end of file diff --git a/src/sass/components/_dialog.sass b/src/sass/components/_dialog.sass new file mode 100644 index 0000000..e36ad17 --- /dev/null +++ b/src/sass/components/_dialog.sass @@ -0,0 +1,4 @@ +.mgt2 + .dialog-button + color: var(--mgt2-color-primary) + background-color: var(--mgt2-bgcolor-primary) !important diff --git a/src/sass/components/_forms.sass b/src/sass/components/_forms.sass new file mode 100644 index 0000000..fc466eb --- /dev/null +++ b/src/sass/components/_forms.sass @@ -0,0 +1,84 @@ +.mgt2 + &.sheet + textarea + color: var(--mgt2-input-color) + background-color: var(--mgt2-input-bgcolor) + font-family: "Roboto", sans-serif + font-size: 13px + font-stretch: 100% + + input:focus, + textarea:focus, + select:focus + outline: none + box-shadow: none + + .checkbox-small + flex: none!important + width: auto!important + height: auto!important + margin: 0!important + + .header + color: var(--mgt2-color-primary) + background: var(--mgt2-bgcolor-primary) + font-size: 14px + font-family: "Roboto Condensed", sans-serif + font-weight: bold + padding-left: 5px + margin-bottom: 4px + line-height: 30px + text-transform: uppercase + + .field-groups + display: flex + flex-direction: row + flex-wrap: nowrap + align-items: center + justify-content: space-between + + .field-group + label + text-transform: uppercase + font-weight: 700 + font-size: 14px + font-family: "Roboto Condensed", sans-serif + font-optical-sizing: auto + input + &.field + background-color: var(--mgt2-input-bgcolor) + font-size: 13px + &.field-name + background-color: var(--mgt2-input-bgcolor) + font-size: 2rem + border: none + font-weight: 700 + font-family: "Roboto Condensed", sans-serif + margin-bottom: 0.5rem + padding: 0 + &.field-item-name + background-color: var(--mgt2-input-bgcolor) + height: auto + font-size: 2rem + font-weight: 700 + font-family: "Roboto Condensed", sans-serif + + .fields + display: flex + + .editor + min-height: 3rem + border: 1px solid var(--mgt2-editor-border) + height: 100% + + .sheet-body + margin-left: 140px + padding-bottom: 1.5rem + + label + &.mgt2-checkbox + display: flex + flex-direction: row + align-items: center + input + margin: 0 0.3rem 0 0 diff --git a/src/sass/components/_item.sass b/src/sass/components/_item.sass new file mode 100644 index 0000000..71b8b22 --- /dev/null +++ b/src/sass/components/_item.sass @@ -0,0 +1,38 @@ +.itemsheet + display: flex + flex-wrap: nowrap + flex-direction: row +.itemsheet-header + display: flex + background-color: var(--mgt2-bgcolor-primary) + color: red + padding: 0.5rem + align-items: center + flex: 0 0 2rem + label + writing-mode: tb-rl + transform: rotate(-180deg) + font-weight: 700 + font-size: 20px + letter-spacing: 5px + font-family: "Rubik Mono One", monospace + font-style: normal + text-transform: uppercase +.itemsheet-maincol + flex: 0 0 130px + padding: 0 1rem 0 0 +.itemsheet-panel + display: flex + flex: inherit + padding: 1rem + img + &.profile-img + width: 100px + height: 100px +.itemsheet input, +.itemsheet select + color: var(--mgt2-input-color) + background-color: var(--mgt2-input-bgcolor) + display: block + width: 100% + font-size: 13px diff --git a/src/sass/components/_tab-sidebar.sass b/src/sass/components/_tab-sidebar.sass new file mode 100644 index 0000000..0d1751a --- /dev/null +++ b/src/sass/components/_tab-sidebar.sass @@ -0,0 +1,40 @@ +.mgt2 + .sheet-sidebar + .item + margin: 0 1rem + + nav[data-group="sidebar"].tabs + position: absolute + left: 100% + top: 172px + display: flex + flex-direction: column + z-index: -1 + & > .item + height: 40px + position: relative + display: flex + justify-content: end + align-items: center + padding-right: 0.75rem + background: var(--mgt2-bgcolor-primary) + color: var(--mgt2-color-primary) + border: 1px solid transparent + font-size: 1rem + transition: all 250ms ease + margin-left: 0 + &.active + text-shadow: none + margin: 0 + border-color: var(--mgt2-color-primary) + &::after + border-left: none + inset: 0.25rem 0.25rem 0.25rem 0 + &::after + content: "" + position: absolute + inset: 0.25rem + border: 1px solid var(--mgt2-color-primary) + pointer-events: none + i + margin-left: 0.8rem diff --git a/src/sass/components/_tables.sass b/src/sass/components/_tables.sass new file mode 100644 index 0000000..b2d80fd --- /dev/null +++ b/src/sass/components/_tables.sass @@ -0,0 +1,172 @@ +.container-controls + display: inline-block + margin-left: 1rem + a + &:not(:last-child) + margin-right: 0.5rem + +.table-container + display: flex + flex-flow: column nowrap + width: 100% + margin: 0 auto +.table-row + display: flex + flex-flow: row nowrap + width: 100% + position: relative + align-items: flex-start + &.heading + background-color: var(--mgt2-bgcolor-primary) + align-items: center + .row-item + text-transform: uppercase + font-size: 12px + &:first-child + font-weight: bold + font-size: 13px + letter-spacing: 3px + i + margin-right: 0.5rem + &.color-1 + .row-item + background-color: var(--mgt2-bgcolor-primary) + color: var(--mgt2-color-primary) + &.color-2 + .row-item + background-color: var(--mgt2-bgcolor-form) + color: var(--mgt2-bgcolor-primary) + div + &.row-item + padding-left: 5px + &:last-child + padding-right: 5px + &:hover + &:not(.heading) + background-color: var(--mgt2-row-hover) +.table-row-mb-4 + margin-bottom: 4px +.row-item + display: flex + flex-grow: 1 + font-size: 14px + line-height: 25px + align-items: center + transition: all 0.15s ease-in-out + overflow: hidden !important + text-overflow: ellipsis + text-wrap: nowrap + &.item-controls + justify-content: right + padding-right: 4px + a + &:not(:last-child) + margin-right: 0.4rem + i + color: black + a[data-roll] + margin-right: 0.5rem +.heading + &.color-1 + .row-item + i + color: var(--mgt2-color-primary) !important + &.color-2 + .row-item + i + color: var(--mgt2-bgcolor-primary) !important +.table-subrow + border-left: 2px var(--mgt2-subrow-color) dashed + color: var(--mgt2-subrow-color) + .row-item + font-size: 0.8em + line-height: 20px + &:first-child + padding-left: 1rem + & > i + margin-right: 4px +.row-item-center + justify-content: center + text-align: center +.row-item-left + justify-content: left +.row-item-right + justify-content: right +.row-item-space-between + justify-content: space-between +.row-item-2 + flex-basis: 4rem +.row-item-5 + flex-basis: 5% +.row-item-10 + flex-basis: 10% +.row-item-12 + flex-basis: 4rem +.row-item-15 + flex-basis: 5rem +.row-item-20 + flex-basis: 20% +.row-item-25 + flex-basis: 25% +.row-item-30 + flex-basis: 30% +.row-item-35 + flex-basis: 35% +.row-item-40 + flex-basis: 40% +.row-item-45 + flex-basis: 45% +.row-item-50 + flex-basis: 50% +.row-item-65 + flex-basis: 50% +.row-item-85 + flex-basis: 50% +.row-item-storage + flex-wrap: wrap + flex-grow: 0 + flex-basis: 20% + font-size: 0.7rem + line-height: 0.8rem +.item-control + &.item-equip + i + color: var(--mgt2-row-inactive-icon) + &.active + i + color: var(--mgt2-color-form) +.row-description + flex-basis: 100% + font-size: 14px + padding: 4px 0 + justify-content: left + transition: all 0.15s ease-in-out +.row-sub-container + display: flex + flex-flow: column nowrap + flex: 1 + .row-item + padding: 8px 0 + border-bottom: 1px solid var(--mgt2-bgcolor-primary) +.table-row:last-child, +.row-sub-container .row-item:last-child + border-bottom: 0 +.table-container + &.editable + .table-row + margin-top: 4px +.table-container + &.editable + .table-row:last-child + margin-bottom: 4px +.item-options + position: absolute + top: 0.7rem + font-size: 0.7em + left: 1.6rem + text-transform: uppercase + font-family: "DM Sans", sans-serif + font-optical-sizing: auto + font-weight: 600 + font-style: normal + color: var(--mgt2-subrow-color) diff --git a/src/sass/components/_tabs.sass b/src/sass/components/_tabs.sass new file mode 100644 index 0000000..1da273a --- /dev/null +++ b/src/sass/components/_tabs.sass @@ -0,0 +1,58 @@ +.mgt2 + nav + &.horizontal-tabs + color: var(--mgt2-color-primary) + background: var(--mgt2-bgcolor-primary) + font-style: normal + font-weight: 700 + font-size: 14px + line-height: 30px + text-transform: uppercase + justify-content: space-around + align-items: center + font-family: "Roboto Condensed", sans-serif + a + &.item + position: relative + flex: 1 1 auto + i + margin-right: 0.5rem + & > a + &.item + &::after + content: "" + position: absolute + inset: 0.25rem 0.25rem 0.25rem 0.25rem + border: 1px solid var( --mgt2-color-primary-active) + pointer-events: none + &.active + &::after + border-bottom: none + border-top: 2px solid var( --mgt2-color-primary-active) + border-left: 2px solid var( --mgt2-color-primary-active) + border-right: 2px solid var( --mgt2-color-primary-active) + inset: 0.25rem 0.25rem 0 0.25rem + .active + color: var(--mgt2-color-primary) + text-decoration: none + text-shadow: none + border-bottom: none + + +.tab[data-tab].fullsize + height: calc(100% - 3rem) + +.subTab + flex-flow: column + height: 100% + display: flex + justify-content: flex-start + align-items: stretch +.tab-scroll + overflow-y: auto + height: 100% +.subTabs + height: 100% + flex-direction: column + &.active + display: flex !important diff --git a/src/sass/mgt2.sass b/src/sass/mgt2.sass new file mode 100644 index 0000000..fef61d1 --- /dev/null +++ b/src/sass/mgt2.sass @@ -0,0 +1,15 @@ +@import 'utils/typography' +@import 'utils/colors' +@import 'utils/global' +@import 'utils/window' +@import 'utils/flex' + +@import 'components/_forms' +@import 'components/_dialog' +@import 'components/_character' +@import 'components/_item' +@import 'components/_chat-sidebar' + +@import 'components/_tabs' +@import 'components/_tab-sidebar' +@import 'components/_tables' \ No newline at end of file diff --git a/src/sass/utils/_colors.sass b/src/sass/utils/_colors.sass new file mode 100644 index 0000000..453a4f4 --- /dev/null +++ b/src/sass/utils/_colors.sass @@ -0,0 +1,63 @@ +$primary-color: #3498db +$secondary-color: #2ecc71 +$background-color: #ecf0f1 +$text-color: #34495e + +.black-and-red + --mgt2-color-form: #0A0405 + --mgt2-bgcolor-form: #fff + --mgt2-color-primary: #EE4050 + --mgt2-color-primary-active: #AF2F3C + --mgt2-bgcolor-primary: #0A0405 + --mgt2-color-primary-light: #4b4a44 + --mgt2-color-warning: #EE4050 + --mgt2-color-dm: #fff + --mgt2-bgcolor-dm: #0A0405 + --mgt2-color-software: #fff + --mgt2-bgcolor-software: #0A0405 + --mgt2-input-color: #0A0405 + --mgt2-input-bgcolor: #fff + --mgt2-editor-border: #C6C6C6 + --mgt2-row-hover: #F2F2F2 + --mgt2-subrow-color: #727272 + --mgt2-row-inactive-icon: #b5b3a4 + --mgt2-encumbrance-normal: #D94826 + --mgt2-encumbrance-heavy: #D82727 + +.mwamba + --mgt2-color-form: #0A0405 + --mgt2-bgcolor-form: #fff + --mgt2-color-primary: #2A9932 + --mgt2-color-primary-active: #40ED4E + --mgt2-bgcolor-primary: #0A0405 + --mgt2-color-primary-light: #4b4a44 + --mgt2-color-warning: #EE4050 + --mgt2-color-dm: #fff + --mgt2-bgcolor-dm: #0A0405 + --mgt2-color-software: #fff + --mgt2-bgcolor-software: #0A0405 + --mgt2-input-color: #0A0405 + --mgt2-input-bgcolor: #fff + --mgt2-editor-border: #C6C6C6 + --mgt2-row-hover: #F2F2F2 + --mgt2-subrow-color: #727272 + --mgt2-row-inactive-icon: #b5b3a4 + +.blue + --mgt2-color-form: #0A0405 + --mgt2-bgcolor-form: #fff + --mgt2-color-primary: #91AAC8 + --mgt2-color-primary-active: #BCDCFF + --mgt2-bgcolor-primary: #0A0405 + --mgt2-color-primary-light: #4b4a44 + --mgt2-color-warning: #EE4050 + --mgt2-color-dm: #fff + --mgt2-bgcolor-dm: #0A0405 + --mgt2-color-software: #fff + --mgt2-bgcolor-software: #0A0405 + --mgt2-input-color: #0A0405 + --mgt2-input-bgcolor: #fff + --mgt2-editor-border: #C6C6C6 + --mgt2-row-hover: #F2F2F2 + --mgt2-subrow-color: #727272 + --mgt2-row-inactive-icon: #b5b3a4 \ No newline at end of file diff --git a/src/sass/utils/_flex.sass b/src/sass/utils/_flex.sass new file mode 100644 index 0000000..a25c3e0 --- /dev/null +++ b/src/sass/utils/_flex.sass @@ -0,0 +1,18 @@ +.mgt2 + .flex-fix + flex-grow: 0 !important + flex-shrink: 0 !important + .flex-basis-10 + flex-basis: 10% + .flex-basis-20 + flex-basis: 20% + .flex-basis-30 + flex-basis: 30% + .flex-basis-40 + flex-basis: 40% + .flex-basis-50 + flex-basis: 50% + .flex-basis-60 + flex-basis: 60% + .flex-basis-70 + flex-basis: 70% \ No newline at end of file diff --git a/src/sass/utils/_global.sass b/src/sass/utils/_global.sass new file mode 100644 index 0000000..64bc876 --- /dev/null +++ b/src/sass/utils/_global.sass @@ -0,0 +1,39 @@ +.upcase + text-transform: uppercase + +.w1-10 + width: calc(100% / 10) + +.w2-10 + width: calc(100% / 10 * 2) + +.w3-10 + width: calc(100% / 10 * 3) + +.w4-10 + width: calc(100% / 10 * 4) + +.w5-10 + width: calc(100% / 10 * 5) + +.h100 + height: 100% + +.w100 + width: 100% + +.mgt2 + a:hover + text-shadow: none + + .w-100 + width: 100% + + .mb-1 + margin-bottom: 8px + + .mt-1, .mt-05 + margin-top: 8px + + .mt-2 + margin-top: 14px \ No newline at end of file diff --git a/src/sass/utils/_typography.sass b/src/sass/utils/_typography.sass new file mode 100644 index 0000000..d96338c --- /dev/null +++ b/src/sass/utils/_typography.sass @@ -0,0 +1,3 @@ +@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap') +@import url('https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap') +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap') \ No newline at end of file diff --git a/src/sass/utils/_variables.sass b/src/sass/utils/_variables.sass new file mode 100644 index 0000000..b97050a --- /dev/null +++ b/src/sass/utils/_variables.sass @@ -0,0 +1,4 @@ +$myFont: Helvetica, sans-serif +$myColor: red +$myFontSize: 18px +$myWidth: 680px \ No newline at end of file diff --git a/src/sass/utils/_window.sass b/src/sass/utils/_window.sass new file mode 100644 index 0000000..3cd62de --- /dev/null +++ b/src/sass/utils/_window.sass @@ -0,0 +1,18 @@ +.mgt2 + &.sheet + header + &.window-header + color: var(--mgt2-color-primary) + background-color: var(--mgt2-bgcolor-primary) + h4 + &.window-title + font-weight: bold + text-transform: uppercase + &.window-app + .window-content + background: var(--mgt2-bgcolor-form) + padding: 0 + +.nopad + .window-content + padding: 0 \ No newline at end of file diff --git a/src/todo.md b/src/todo.md new file mode 100644 index 0000000..cdc568f --- /dev/null +++ b/src/todo.md @@ -0,0 +1,11 @@ +# BUGS + + +# Chose à faire + +- Enlever les styles inlines + +Actors +- NPC +- Creature +- Container \ No newline at end of file