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