From 5c6f9bc9a5ed73a782951497d21055a77a6df2c8 Mon Sep 17 00:00:00 2001 From: sladecraven Date: Wed, 23 Feb 2022 22:31:53 +0100 Subject: [PATCH] Init --- modules/mournblade-actor-sheet.js | 280 +++++++ modules/mournblade-actor.js | 189 +++++ modules/mournblade-combat.js | 24 + modules/mournblade-commands.js | 124 +++ modules/mournblade-item-sheet.js | 508 ++++++++++++ modules/mournblade-item.js | 19 + modules/mournblade-main.js | 111 +++ modules/mournblade-roll-dialog.js | 239 ++++++ modules/mournblade-utility.js | 395 ++++++++++ styles/simple.css | 1218 +++++++++++++++++++++++++++++ system.json | 48 ++ template.json | 138 ++++ 12 files changed, 3293 insertions(+) create mode 100644 modules/mournblade-actor-sheet.js create mode 100644 modules/mournblade-actor.js create mode 100644 modules/mournblade-combat.js create mode 100644 modules/mournblade-commands.js create mode 100644 modules/mournblade-item-sheet.js create mode 100644 modules/mournblade-item.js create mode 100644 modules/mournblade-main.js create mode 100644 modules/mournblade-roll-dialog.js create mode 100644 modules/mournblade-utility.js create mode 100644 styles/simple.css create mode 100644 system.json create mode 100644 template.json diff --git a/modules/mournblade-actor-sheet.js b/modules/mournblade-actor-sheet.js new file mode 100644 index 0000000..8d309d3 --- /dev/null +++ b/modules/mournblade-actor-sheet.js @@ -0,0 +1,280 @@ +/** + * Extend the basic ActorSheet with some very simple modifications + * @extends {ActorSheet} + */ + +import { MournbladeUtility } from "./mournblade-utility.js"; +import { MournbladeRollDialog } from "./Mournblade-roll-dialog.js"; + +/* -------------------------------------------- */ +export class MournbladeActorSheet extends ActorSheet { + + /** @override */ + static get defaultOptions() { + + return mergeObject(super.defaultOptions, { + classes: ["fvtt-mournblade", "sheet", "actor"], + template: "systems/fvtt-mournblade/templates/actor-sheet.html", + width: 640, + height: 720, + tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "stats" }], + dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }], + editScore: false + }); + } + + /* -------------------------------------------- */ + async getData() { + const objectData = MournbladeUtility.data(this.object); + + let actorData = duplicate(MournbladeUtility.templateData(this.object)); + + let formData = { + title: this.title, + id: objectData.id, + type: objectData.type, + img: objectData.img, + name: objectData.name, + editable: this.isEditable, + cssClass: this.isEditable ? "editable" : "locked", + data: actorData, + effects: this.object.effects.map(e => foundry.utils.deepClone(e.data)), + limited: this.object.limited, + weapons: this.actor.checkAndPrepareWeapons( duplicate(this.actor.getWeapons()) ), + armors: this.actor.checkAndPrepareArmors( duplicate(this.actor.getArmors())), + shields: duplicate(this.actor.getShields()), + equipments: duplicate(this.actor.getEquipments()), + options: this.options, + owner: this.document.isOwner, + editScore: this.options.editScore, + isGM: game.user.isGM + } + this.formData = formData; + + console.log("PC : ", formData, this.object); + return formData; + } + + + /* -------------------------------------------- */ + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Everything below here is only needed if the sheet is editable + if (!this.options.editable) return; + + html.bind("keydown", function(e) { // Ignore Enter in actores sheet + if (e.keyCode === 13) return false; + }); + + // Update Inventory Item + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + const item = this.actor.items.get( itemId ); + item.sheet.render(true); + }); + // Delete Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + MournbladeUtility.confirmDelete(this, li); + }); + + html.find('.spec-group-activate').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + this.actor.specPowerActivate( itemId) + }); + html.find('.spec-group-deactivate').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + this.actor.specPowerDeactivate( itemId) + }); + + html.find('.equip-activate').click(ev => { + const li = $(ev.currentTarget).parents(".item") + let itemId = li.data("item-id") + this.actor.equipActivate( itemId) + }); + html.find('.equip-deactivate').click(ev => { + const li = $(ev.currentTarget).parents(".item") + let itemId = li.data("item-id") + this.actor.equipDeactivate( itemId) + }); + + html.find('.effect-used').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + this.actor.perkEffectUsed( itemId) + }); + + html.find('.perk-status').change(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + this.actor.updatePerkStatus( itemId, ev.currentTarget.value) + }); + html.find('.power-cost-spent').change(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + this.actor.updatePowerSpentCost( itemId, ev.currentTarget.value) + }); + html.find('.perk-used').change(ev => { + const li = $(ev.currentTarget).parents(".item") + let itemId = li.data("item-id") + let index = Number($(ev.currentTarget).data("use-index") ) + this.actor.updatePerkUsed( itemId, index, ev.currentTarget.checked ) + }); + + html.find('.subactor-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let actorId = li.data("actor-id"); + let actor = game.actors.get( actorId ); + actor.sheet.render(true); + }); + + html.find('.subactor-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let actorId = li.data("actor-id"); + this.actor.delSubActor(actorId); + }); + + html.find('.quantity-minus').click(event => { + const li = $(event.currentTarget).parents(".item"); + this.actor.incDecQuantity( li.data("item-id"), -1 ); + } ); + html.find('.quantity-plus').click(event => { + const li = $(event.currentTarget).parents(".item"); + this.actor.incDecQuantity( li.data("item-id"), +1 ); + } ); + + html.find('.unarmed-attack').click((event) => { + this.actor.rollUnarmedAttack(); + }); + html.find('.generic-pool-roll').click((event) => { + this.openGenericRoll() + } ); + html.find('.attack-melee').click((event) => { + this.actor.rollPool( 'com'); + }); + html.find('.attack-ranged').click((event) => { + this.actor.rollPool( 'agi'); + }); + html.find('.defense-roll').click((event) => { + this.actor.rollPool( 'def', true); + }); + html.find('.damage-melee').click((event) => { + this.actor.rollPool( 'str'); + }); + html.find('.damage-ranged').click((event) => { + this.actor.rollPool( 'per'); + }); + html.find('.damage-resistance').click((event) => { + this.actor.rollPool( 'phy'); + }); + + html.find('.roll-stat').click((event) => { + const statId = $(event.currentTarget).data("stat-key"); + this.actor.rollStat(statId); + }); + html.find('.roll-mr').click((event) => { + this.actor.rollMR(); + }); + + html.find('.roll-spec').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const specId = li.data("item-id"); + this.actor.rollSpec(specId); + }); + html.find('.power-roll').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const powerId = li.data("item-id"); + this.actor.rollPower(powerId); + }); + html.find('.weapon-roll').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const weaponId = li.data("item-id"); + this.actor.rollWeapon(weaponId); + }); + html.find('.armor-roll').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const armorId = li.data("item-id"); + this.actor.rollArmor(armorId); + }); + + html.find('.weapon-damage-roll').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const weaponId = li.data("item-id"); + this.actor.rollWeapon(weaponId, true); + }); + + html.find('.weapon-damage').click((event) => { + const li = $(event.currentTarget).parents(".item"); + const weapon = this.actor.getOwnedItem(li.data("item-id")); + this.actor.rollDamage(weapon, 'damage'); + }); + + html.find('.lock-unlock-sheet').click((event) => { + this.options.editScore = !this.options.editScore; + this.render(true); + }); + html.find('.item-link a').click((event) => { + const itemId = $(event.currentTarget).data("item-id"); + const item = this.actor.getOwnedItem(itemId); + item.sheet.render(true); + }); + html.find('.item-equip').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + this.actor.equipItem( li.data("item-id") ); + this.render(true); + }); + html.find('.power-activate').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + this.actor.activatePower( li.data("item-id") ); + this.render(true); + }); + + html.find('.change-worstfear').change(ev => { + this.actor.manageWorstFear( ev.currentTarget.checked ) + }); + html.find('.change-desires').change(ev => { + this.actor.manageDesires( ev.currentTarget.checked ) + }); + + html.find('.update-field').change(ev => { + const fieldName = $(ev.currentTarget).data("field-name"); + let value = Number(ev.currentTarget.value); + this.actor.update( { [`${fieldName}`]: value } ); + }); + html.find('.perk-active').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + this.actor.activatePerk( li.data("item-id") ); + this.render(true); + }); + + } + + /* -------------------------------------------- */ + /** @override */ + setPosition(options = {}) { + const position = super.setPosition(options); + const sheetBody = this.element.find(".sheet-body"); + const bodyHeight = position.height - 192; + sheetBody.css("height", bodyHeight); + return position; + } + + /* -------------------------------------------- */ + async _onDropItem(event, dragData) { + let item = await MournbladeUtility.searchItem( dragData) + this.actor.preprocessItem( event, item, true ) + super._onDropItem(event, dragData) + } + + /* -------------------------------------------- */ + /** @override */ + _updateObject(event, formData) { + // Update the Actor + return this.object.update(formData); + } +} diff --git a/modules/mournblade-actor.js b/modules/mournblade-actor.js new file mode 100644 index 0000000..9d3921f --- /dev/null +++ b/modules/mournblade-actor.js @@ -0,0 +1,189 @@ +/* -------------------------------------------- */ +import { MournbladeUtility } from "./mournblade-utility.js"; +import { MournbladeRollDialog } from "./mournblade-roll-dialog.js"; + +/* -------------------------------------------- */ +/* -------------------------------------------- */ +/** + * Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system. + * @extends {Actor} + */ +export class MournbladeActor extends Actor { + + /* -------------------------------------------- */ + /** + * Override the create() function to provide additional SoS functionality. + * + * This overrided create() function adds initial items + * Namely: Basic skills, money, + * + * @param {Object} data Barebones actor data which this function adds onto. + * @param {Object} options (Unused) Additional options which customize the creation workflow. + * + */ + + static async create(data, options) { + + // Case of compendium global import + if (data instanceof Array) { + return super.create(data, options); + } + // If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic + if (data.items) { + let actor = super.create(data, options); + return actor; + } + + if (data.type == 'character') { + const skills = await MournbladeUtility.loadCompendium("fvtt-mournblade.skills") + data.items = skills.map(i => i.toObject()); + } + if (data.type == 'npc') { + } + + return super.create(data, options); + } + + /* -------------------------------------------- */ + prepareBaseData() { + } + + /* -------------------------------------------- */ + async prepareData() { + super.prepareData(); + } + + /* -------------------------------------------- */ + prepareDerivedData() { + + if (this.type == 'character') { + } + + super.prepareDerivedData(); + } + + /* -------------------------------------------- */ + _preUpdate(changed, options, user) { + + super._preUpdate(changed, options, user); + } + + /* -------------------------------------------- */ + getActivePerks() { + let perks = this.data.items.filter(item => item.type == 'perk' && item.data.data.active); + return perks; + } + /* -------------------------------------------- */ + getItemById(id) { + let item = this.data.items.find(item => item.id == id); + if (item) { + item = duplicate(item) + } + return item; + } + + /* -------------------------------------------- */ + async equipItem(itemId) { + let item = this.data.items.find(item => item.id == itemId); + if (item && item.data.data) { + let update = { _id: item.id, "data.equipped": !item.data.data.equipped }; + await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity + } + } + + /* -------------------------------------------- */ + compareName(a, b) { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + } + + /* -------------------------------------------- */ + getAttribute(attrKey) { + return this.data.data.attributes[attrKey]; + } + + /* -------------------------------------------- */ + async equipGear(equipmentId) { + let item = this.data.items.find(item => item.id == equipmentId); + if (item && item.data.data) { + let update = { _id: item.id, "data.equipped": !item.data.data.equipped }; + await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity + } + } + + /* -------------------------------------------- */ + getSubActors() { + let subActors = []; + for (let id of this.data.data.subactors) { + subActors.push(duplicate(game.actors.get(id))); + } + return subActors; + } + /* -------------------------------------------- */ + async addSubActor(subActorId) { + let subActors = duplicate(this.data.data.subactors); + subActors.push(subActorId); + await this.update({ 'data.subactors': subActors }); + } + /* -------------------------------------------- */ + async delSubActor(subActorId) { + let newArray = []; + for (let id of this.data.data.subactors) { + if (id != subActorId) { + newArray.push(id); + } + } + await this.update({ 'data.subactors': newArray }); + } + + /* -------------------------------------------- */ + async incDecQuantity(objetId, incDec = 0) { + let objetQ = this.data.items.get(objetId) + if (objetQ) { + let newQ = objetQ.data.data.quantity + incDec; + const updated = await this.updateEmbeddedDocuments('Item', [{ _id: objetQ.id, 'data.quantity': newQ }]); // pdates one EmbeddedEntity + } + } + + /* -------------------------------------------- */ + getCommonRollData(statKey = undefined, useShield = false) { + let rollData = MournbladeUtility.getBasicRollData() + rollData.alias = this.name + rollData.actorImg = this.img + rollData.actorId = this.id + rollData.img = this.img + rollData.activePerks = duplicate(this.getActivePerks()) + + if (statKey) { + rollData.statKey = statKey + rollData.stat = this.getStat(statKey) + rollData.statDicesLevel = rollData.stat.value + rollData.statMod = rollData.stat.mod + rollData.specList = this.getRelevantSpec(statKey) + rollData.selectedSpec = "0" + } + + this.addEffects(rollData) + this.addArmorsShields(rollData, statKey, useShield) + this.addWeapons(rollData, statKey, useShield) + this.addEquipments(rollData, statKey) + + return rollData + } + + /* -------------------------------------------- */ + async startRoll(rollData) { + this.syncRoll(rollData); + //console.log("ROLL DATA", rollData) + let rollDialog = await MournbladeRollDialog.create(this, rollData); + console.log(rollDialog); + rollDialog.render(true); + + } + +} diff --git a/modules/mournblade-combat.js b/modules/mournblade-combat.js new file mode 100644 index 0000000..14e21a8 --- /dev/null +++ b/modules/mournblade-combat.js @@ -0,0 +1,24 @@ +import { MournbladeUtility } from "./mournblade-utility.js"; + +/* -------------------------------------------- */ +export class MournbladeCombat extends Combat { + + /* -------------------------------------------- */ + async rollInitiative(ids, formula = undefined, messageOptions = {} ) { + ids = typeof ids === "string" ? [ids] : ids; + for (let cId = 0; cId < ids.length; cId++) { + const c = this.combatants.get(ids[cId]); + let id = c._id || c.id; + let initBonus = c.actor ? c.actor.getInitiativeScore( this.id, id ) : -1; + await this.updateEmbeddedDocuments("Combatant", [ { _id: id, initiative: initBonus } ]); + } + + return this; + } + + /* -------------------------------------------- */ + _onUpdate(changed, options, userId) { + } + + +} diff --git a/modules/mournblade-commands.js b/modules/mournblade-commands.js new file mode 100644 index 0000000..f97d9bc --- /dev/null +++ b/modules/mournblade-commands.js @@ -0,0 +1,124 @@ +/* -------------------------------------------- */ + +import { MournbladeActorCreate } from "./mournblade-create-char.js"; +import { MournbladeUtility } from "./mournblade-utility.js"; +import { MournbladeRollDialog } from "./mournblade-roll-dialog.js"; + +/* -------------------------------------------- */ +export class MournbladeCommands { + + static init() { + if (!game.system.Mournblade.commands) { + const MournbladeCommands = new MournbladeCommands(); + MournbladeCommands.registerCommand({ path: ["/char"], func: (content, msg, params) => MournbladeCommands.createChar(msg), descr: "Create a new character" }); + MournbladeCommands.registerCommand({ path: ["/pool"], func: (content, msg, params) => MournbladeCommands.poolRoll(msg), descr: "Generic Roll Window" }); + game.system.Mournblade.commands = MournbladeCommands; + } + } + constructor() { + this.commandsTable = {}; + } + + /* -------------------------------------------- */ + registerCommand(command) { + this._addCommand(this.commandsTable, command.path, '', command); + } + + /* -------------------------------------------- */ + _addCommand(targetTable, path, fullPath, command) { + if (!this._validateCommand(targetTable, path, command)) { + return; + } + const term = path[0]; + fullPath = fullPath + term + ' ' + if (path.length == 1) { + command.descr = `${fullPath}: ${command.descr}`; + targetTable[term] = command; + } + else { + if (!targetTable[term]) { + targetTable[term] = { subTable: {} }; + } + this._addCommand(targetTable[term].subTable, path.slice(1), fullPath, command) + } + } + + /* -------------------------------------------- */ + _validateCommand(targetTable, path, command) { + if (path.length > 0 && path[0] && command.descr && (path.length != 1 || targetTable[path[0]] == undefined)) { + return true; + } + console.warn("MournbladeCommands._validateCommand failed ", targetTable, path, command); + return false; + } + + + /* -------------------------------------------- */ + /* Manage chat commands */ + processChatCommand(commandLine, content = '', msg = {}) { + // Setup new message's visibility + let rollMode = game.settings.get("core", "rollMode"); + if (["gmroll", "blindroll"].includes(rollMode)) msg["whisper"] = ChatMessage.getWhisperRecipients("GM"); + if (rollMode === "blindroll") msg["blind"] = true; + msg["type"] = 0; + + let command = commandLine[0].toLowerCase(); + let params = commandLine.slice(1); + + return this.process(command, params, content, msg); + } + + /* -------------------------------------------- */ + process(command, params, content, msg) { + return this._processCommand(this.commandsTable, command, params, content, msg); + } + + /* -------------------------------------------- */ + _processCommand(commandsTable, name, params, content = '', msg = {}, path = "") { + console.log("===> Processing command") + let command = commandsTable[name]; + path = path + name + " "; + if (command && command.subTable) { + if (params[0]) { + return this._processCommand(command.subTable, params[0], params.slice(1), content, msg, path) + } + else { + this.help(msg, command.subTable); + return true; + } + } + if (command && command.func) { + const result = command.func(content, msg, params); + if (result == false) { + RdDCommands._chatAnswer(msg, command.descr); + } + return true; + } + return false; + } + + /* -------------------------------------------- */ + async createChar(msg) { + game.system.Mournblade.creator = new MournbladeActorCreate(); + game.system.Mournblade.creator.start(); + } + + /* -------------------------------------------- */ + static _chatAnswer(msg, content) { + msg.whisper = [game.user.id]; + msg.content = content; + ChatMessage.create(msg); + } + + /* -------------------------------------------- */ + async poolRoll( msg) { + let rollData = MournbladeUtility.getBasicRollData() + rollData.alias = "Dice Pool Roll", + rollData.mode = "generic" + rollData.title = `Dice Pool Roll`; + + let rollDialog = await MournbladeRollDialog.create( this, rollData); + rollDialog.render( true ); + } + +} \ No newline at end of file diff --git a/modules/mournblade-item-sheet.js b/modules/mournblade-item-sheet.js new file mode 100644 index 0000000..a3ec36e --- /dev/null +++ b/modules/mournblade-item-sheet.js @@ -0,0 +1,508 @@ +import { MournbladeUtility } from "./Mournblade-utility.js"; + +/** + * Extend the basic ItemSheet with some very simple modifications + * @extends {ItemSheet} + */ +export class MournbladeItemSheet extends ItemSheet { + + /** @override */ + static get defaultOptions() { + + return mergeObject(super.defaultOptions, { + classes: ["fvtt-mournblade", "sheet", "item"], + template: "systems/fvtt-mournblade/templates/item-sheet.html", + dragDrop: [{ dragSelector: null, dropSelector: null }], + width: 620, + height: 550 + //tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + // Add "Post to chat" button + // We previously restricted this to GM and editable items only. If you ever find this comment because it broke something: eh, sorry! + buttons.unshift( + { + class: "post", + icon: "fas fa-comment", + onclick: ev => { } + }) + return buttons + } + + /* -------------------------------------------- */ + /** @override */ + setPosition(options = {}) { + const position = super.setPosition(options); + const sheetBody = this.element.find(".sheet-body"); + const bodyHeight = position.height - 192; + sheetBody.css("height", bodyHeight); + if (this.item.type.includes('weapon')) { + position.width = 640; + } + return position; + } + + /* -------------------------------------------- */ + async getData() { + const objectData = MournbladeUtility.data(this.object); + + let itemData = foundry.utils.deepClone(MournbladeUtility.templateData(this.object)); + let formData = { + title: this.title, + id: this.id, + type: objectData.type, + img: objectData.img, + name: objectData.name, + editable: this.isEditable, + cssClass: this.isEditable ? "editable" : "locked", + optionsDiceList: MournbladeUtility.getOptionsDiceList(), + optionsStatusList: MournbladeUtility.getOptionsStatusList(), + data: itemData, + limited: this.object.limited, + options: this.options, + owner: this.document.isOwner, + mr: (this.object.type == 'specialisation'), + isGM: game.user.isGM + } + + this.options.editable = !(this.object.data.origin == "embeddedItem"); + console.log("ITEM DATA", formData, this); + return formData; + } + + + /* -------------------------------------------- */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + buttons.unshift({ + class: "post", + icon: "fas fa-comment", + onclick: ev => this.postItem() + }); + return buttons + } + + /* -------------------------------------------- */ + postItem() { + let chatData = duplicate(MournbladeUtility.data(this.item)); + if (this.actor) { + chatData.actor = { id: this.actor.id }; + } + // Don't post any image for the item (which would leave a large gap) if the default image is used + if (chatData.img.includes("/blank.png")) { + chatData.img = null; + } + // JSON object for easy creation + chatData.jsondata = JSON.stringify( + { + compendium: "postedItem", + payload: chatData, + }); + + renderTemplate('systems/fvtt-Mournblade-rpg/templates/post-item.html', chatData).then(html => { + let chatOptions = MournbladeUtility.chatDataSetup(html); + ChatMessage.create(chatOptions) + }); + } + + /* -------------------------------------------- */ + async viewSubitem(ev) { + let field = $(ev.currentTarget).data('type'); + let idx = Number($(ev.currentTarget).data('index')); + let itemData = this.object.data.data[field][idx]; + if (itemData.name != 'None') { + let spec = await Item.create(itemData, { temporary: true }); + spec.data.origin = "embeddedItem"; + new MournbladeItemSheet(spec).render(true); + } + } + + /* -------------------------------------------- */ + async deleteSubitem(ev) { + let field = $(ev.currentTarget).data('type'); + let idx = Number($(ev.currentTarget).data('index')); + let oldArray = this.object.data.data[field]; + let itemData = this.object.data.data[field][idx]; + if (itemData.name != 'None') { + let newArray = []; + for (var i = 0; i < oldArray.length; i++) { + if (i != idx) { + newArray.push(oldArray[i]); + } + } + this.object.update({ [`data.${field}`]: newArray }); + } + } + + /* -------------------------------------------- */ + async manageSpec() { + let itemData = this.object.data.data.specialisation[0]; + if (itemData.name != 'None') { + let spec = await Item.create(itemData, { temporary: true }); + spec.data.origin = "embeddedItem"; + new MournbladeItemSheet(spec).render(true); + } + } + + /* -------------------------------------------- */ + /** @override */ + activateListeners(html) { + super.activateListeners(html); + + // Everything below here is only needed if the sheet is editable + if (!this.options.editable) return; + + + // Update Inventory Item + html.find('.item-edit').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + const item = this.object.options.actor.getOwnedItem(li.data("item-id")); + item.sheet.render(true); + }); + + html.find('.delete-spec').click(ev => { + this.object.update({ "data.specialisation": [{ name: 'None' }] }); + }); + + html.find('.delete-subitem').click(ev => { + this.deleteSubitem(ev); + }); + + html.find('.stat-choice-flag').click(ev => { + let idx = $(ev.currentTarget).data("stat-idx"); + let array = duplicate(this.object.data.data.statincreasechoice); + array[Number(idx)].flag = !array[Number(idx)].flag; + this.object.update({ "data.statincreasechoice": array }); + }); + + // Update Inventory Item + html.find('.item-delete').click(ev => { + const li = $(ev.currentTarget).parents(".item"); + let itemId = li.data("item-id"); + let itemType = li.data("item-type"); + }); + + html.find('.view-subitem').click(ev => { + this.viewSubitem(ev); + }); + + html.find('.view-spec').click(ev => { + this.manageSpec(); + }); + + } + + /* -------------------------------------------- */ + async addAbility(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + console.log("ABB", event, item, dataItem) + if (event.toElement.className == 'drop-abilities') { + let abilityArray = duplicate(this.object.data.data.abilities); + abilityArray.push(newItem); + await this.object.update({ 'data.abilities': abilityArray }); + } + if (event.toElement.className == 'drop-optionnal-abilities') { + let abilityArray = duplicate(this.object.data.data.optionnalabilities); + abilityArray.push(newItem); + await this.object.update({ 'data.optionnalabilities': abilityArray }); + } + } + + /* -------------------------------------------- */ + async addRacePerk(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + if (event.toElement.className == 'drop-race-perk') { + let perkArray = duplicate(this.object.data.data.perks); + perkArray.push(newItem); + await this.object.update({ 'data.perks': perkArray }); + } + } + + /* -------------------------------------------- */ + async addSpecialisation(item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + let specArray = [newItem]; + await this.object.update({ 'data.specialisation': specArray }); + } + + /* -------------------------------------------- */ + async addRoleSpecialisation(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + console.log("Add spec", event, newItem); + if (event.toElement.className == 'drop-spec1') { + let specArray = duplicate(this.object.data.data.specialisationsplus1); + specArray.push(newItem); + await this.object.update({ 'data.specialisationsplus1': specArray }); + } + if (event.toElement.className == 'drop-spec2') { + let specArray = duplicate(this.object.data.data.specincrease); + specArray.push(newItem); + await this.object.update({ 'data.specincrease': specArray }); + } + } + + /* -------------------------------------------- */ + async addRolePerk(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + console.log("Add spec", event, newItem); + if (event.toElement.className == 'drop-perk2') { + let perkArray = duplicate(this.object.data.data.perks); + perkArray.push(newItem); + await this.object.update({ 'data.perks': perkArray }); + } + if (event.toElement.className == 'drop-specialperk1') { + let perkArray = duplicate(this.object.data.data.specialperk); + perkArray.push(newItem); + await this.object.update({ 'data.specialperk': perkArray }); + } + } + + /* -------------------------------------------- */ + async addPower(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + if (event.toElement.className == 'drop-spec-power') { + let powArray = duplicate(this.object.data.data.powers); + powArray.push(newItem); + await this.object.update({ 'data.powers': powArray }); + } + } + + /* -------------------------------------------- */ + async addAbilityPower(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + if (event.toElement.className == 'drop-ability-power') { + let powArray = duplicate(this.object.data.data.powersgained); + powArray.push(newItem); + await this.object.update({ 'data.powersgained': powArray }); + } + } + /* -------------------------------------------- */ + async addAbilityEffect(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + if (event.toElement.className == 'drop-ability-effect') { + let powArray = duplicate(this.object.data.data.effectsgained); + powArray.push(newItem); + await this.object.update({ 'data.effectsgained': powArray }); + } + } + + /* -------------------------------------------- */ + async addAbilitySpec(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + if (event.toElement.className == 'drop-ability-spec') { + let powArray = duplicate(this.object.data.data.specialisations); + powArray.push(newItem); + await this.object.update({ 'data.specialisations': powArray }); + } + } + /* -------------------------------------------- */ + async addAbilityWeaponArmor(event, item, dataItem) { + let newItem = duplicate(item.data); + newItem._id = randomID(dataItem.id.length); + if (event.toElement.className == 'drop-ability-weapon') { + let weaponArray = duplicate(this.object.data.data.attackgained); + weaponArray.push(newItem); + await this.object.update({ 'data.attackgained': weaponArray }); + } + if (event.toElement.className == 'drop-ability-armor') { + let armorArray = duplicate(this.object.data.data.armorgained); + armorArray.push(newItem); + await this.object.update({ 'data.armorgained': armorArray }); + } + } + + /* -------------------------------------------- */ + async addPerkSpecialisation(event, item, dataItem) { + let newItem = duplicate(item.data); + if (event.toElement.className == 'drop-spec-perk') { + //console.log("PER SPEC", event) + let key = event.toElement.dataset["key"]; + if (key == 'affectedspec') { + await this.object.update({ 'data.features.affectedspec.value': newItem.name }); + } else { + await this.object.update({ 'data.features.gainspecdice.value': newItem.name }); + } + } + } + + /* -------------------------------------------- */ + async addPerkEffect(event, item, dataItem) { + let newItem = duplicate(item.data) + if (event.toElement.className == 'drop-perk-effect') { + let effectArray = duplicate(this.object.data.data.effectsgained) + effectArray.push(newItem) + await this.object.update({ 'data.effectsgained': effectArray }) + } + } + + /* -------------------------------------------- */ + async addEffectPower(event, item, dataItem) { + let newItem = duplicate(item.data) + if (event.toElement.className == 'drop-power-effect') { + let effectArray = duplicate(this.object.data.data.effectsgained) + effectArray.push(newItem); + await this.object.update({ 'data.effectsgained': effectArray }) + } + } + + /* -------------------------------------------- */ + async addEffectSpec(event, item, dataItem) { + let newItem = duplicate(item.data); + if (event.toElement.className == 'drop-effect-spec') { + let specArray = duplicate(this.object.data.data.recoveryrollspec); + specArray.push(newItem); + await this.object.update({ 'data.recoveryrollspec': specArray }); + } + if (event.toElement.className == 'drop-effect-specaffected') { + let specArray = duplicate(this.object.data.data.specaffected); + specArray.push(newItem); + await this.object.update({ 'data.specaffected': specArray }); + } + } + + /* -------------------------------------------- */ + async addEffectItem(event, item, dataItem) { + let newItem = duplicate(item.data); + if (event.toElement.className == 'drop-equipment-effect') { + let effectArray = duplicate(this.object.data.data.effects); + effectArray.push(newItem); + await this.object.update({ 'data.effects': effectArray }); + } + } + + /* -------------------------------------------- */ + async _onDrop(event) { + + if (this.object.type == 'weapon' || this.object.type == 'shield' || this.object.type == 'armor' || this.object.type == 'shield') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'effect') { + return this.addEffectItem(event, item, dataItem); + } + } + } + + if (this.object.type == 'power') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'effect') { + return this.addEffectPower(event, item, dataItem); + } + } + } + + if (this.object.type == 'effect') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'specialisation') { + return this.addEffectSpec(event, item, dataItem); + } + } + } + + if (this.object.type == 'race') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'ability') { + return this.addAbility(event, item, dataItem); + } + if (item.data.type == 'perk') { + return this.addRacePerk(event, item, dataItem); + } + } + } + + if (this.object.type == 'perk') { + let data = event.dataTransfer.getData('text/plain') + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem) + if (item.data.type == 'specialisation') { + return this.addPerkSpecialisation(event, item, dataItem) + } + if (item.data.type == 'effect') { + return this.addPerkEffect(event, item, dataItem); + } + } + } + + if (this.object.type == 'specialisation') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'power') { + return this.addPower(event, item, dataItem); + } + } + } + if (this.object.type == 'ability') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'effect') { + return this.addAbilityEffect(event, item, dataItem); + } + if (item.data.type == 'power') { + return this.addAbilityPower(event, item, dataItem); + } + if (item.data.type == 'specialisation') { + return this.addAbilitySpec(event, item, dataItem); + } + if (item.data.type == 'weapon' || item.data.type == 'armor') { + return this.addAbilityWeaponArmor(event, item, dataItem); + } + } + } + + if (this.object.type == 'role') { + let data = event.dataTransfer.getData('text/plain'); + if (data) { + let dataItem = JSON.parse(data); + let item = await MournbladeUtility.searchItem(dataItem); + if (item.data.type == 'specialisation') { + return this.addRoleSpecialisation(event, item, dataItem); + } + if (item.data.type == 'perk') { + return this.addRolePerk(event, item, dataItem); + } + } + } + + ui.notifications.warn("This item can not be dropped over another item"); + } + + /* -------------------------------------------- */ + get template() { + let type = this.item.type; + return `systems/fvtt-Mournblade-rpg/templates/item-${type}-sheet.html`; + } + + /* -------------------------------------------- */ + /** @override */ + _updateObject(event, formData) { + return this.object.update(formData); + } +} diff --git a/modules/mournblade-item.js b/modules/mournblade-item.js new file mode 100644 index 0000000..8ad754e --- /dev/null +++ b/modules/mournblade-item.js @@ -0,0 +1,19 @@ +import { MournbladeUtility } from "./mournblade-utility.js"; + +export const defaultItemImg = { +} + +/** + * Extend the basic ItemSheet with some very simple modifications + * @extends {ItemSheet} + */ +export class MournbladeItem extends Item { + + constructor(data, context) { + if (!data.img) { + data.img = defaultItemImg[data.type]; + } + super(data, context); + } + +} diff --git a/modules/mournblade-main.js b/modules/mournblade-main.js new file mode 100644 index 0000000..f68986d --- /dev/null +++ b/modules/mournblade-main.js @@ -0,0 +1,111 @@ +/** + * Mournblade system + * Author: Uberwald + * Software License: Prop + */ + +/* -------------------------------------------- */ + +/* -------------------------------------------- */ +// Import Modules +import { MournbladeActor } from "./mournblade-actor.js"; +import { MournbladeItemSheet } from "./mournblade-item-sheet.js"; +import { MournbladeActorSheet } from "./mournblade-actor-sheet.js"; +import { MournbladeNPCSheet } from "./mournblade-npc-sheet.js"; +import { MournbladeUtility } from "./mournblade-utility.js"; +import { MournbladeCombat } from "./mournblade-combat.js"; +import { MournbladeItem } from "./mournblade-item.js"; + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ + +/************************************************************************************/ +Hooks.once("init", async function () { + console.log(`Initializing Mournblade RPG`); + + /* -------------------------------------------- */ + // preload handlebars templates + MournbladeUtility.preloadHandlebarsTemplates(); + + /* -------------------------------------------- */ + // Set an initiative formula for the system + CONFIG.Combat.initiative = { + formula: "1d6", + decimals: 1 + }; + + /* -------------------------------------------- */ + game.socket.on("system.fvtt-mournblade-rpg", data => { + MournbladeUtility.onSocketMesssage(data); + }); + + /* -------------------------------------------- */ + // Define custom Entity classes + CONFIG.Combat.documentClass = MournbladeCombat + CONFIG.Actor.documentClass = MournbladeActor + CONFIG.Item.documentClass = MournbladeItem + //CONFIG.Token.objectClass = MournbladeToken + game.system.Mournblade = { }; + + /* -------------------------------------------- */ + // Register sheet application classes + Actors.unregisterSheet("core", ActorSheet); + Actors.registerSheet("fvtt-mournblade", MournbladeActorSheet, { types: ["character"], makeDefault: true }); + Actors.registerSheet("fvtt-mournblade", MournbladeNPCSheet, { types: ["npc"], makeDefault: false }); + + Items.unregisterSheet("core", ItemSheet); + Items.registerSheet("fvtt-mournblade", MournbladeItemSheet, { makeDefault: true }); + + MournbladeUtility.init(); + +}); + +/* -------------------------------------------- */ +function welcomeMessage() { + ChatMessage.create({ + user: game.user.id, + whisper: [game.user.id], + content: `
+ Welcome to Mournblade RPG. + ` }); +} + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ +Hooks.once("ready", function () { + + MournbladeUtility.ready(); + // User warning + if (!game.user.isGM && game.user.character == undefined) { + ui.notifications.info("Warning ! No character linked to your user !"); + ChatMessage.create({ + content: "WARNING The player " + game.user.name + " is not linked to a character !", + user: game.user._id + }); + } + + // CSS patch for v9 + if (game.version) { + let sidebar = document.getElementById("sidebar"); + sidebar.style.width = "min-content"; + } + + welcomeMessage(); +}); + +/* -------------------------------------------- */ +/* Foundry VTT Initialization */ +/* -------------------------------------------- */ +Hooks.on("chatMessage", (html, content, msg) => { + if (content[0] == '/') { + let regExp = /(\S+)/g; + let commands = content.match(regExp); + if (game.system.Mournblade.commands.processChatCommand(commands, content, msg)) { + return false; + } + } + return true; +}); + diff --git a/modules/mournblade-roll-dialog.js b/modules/mournblade-roll-dialog.js new file mode 100644 index 0000000..5c3dd3c --- /dev/null +++ b/modules/mournblade-roll-dialog.js @@ -0,0 +1,239 @@ +import { MournbladeUtility } from "./mournblade-utility.js"; + +export class MournbladeRollDialog extends Dialog { + + /* -------------------------------------------- */ + static async create(actor, rollData ) { + + let options = { classes: ["MournbladeDialog"], width: 620, height: 380, 'z-index': 99999 }; + let html = await renderTemplate('systems/fvtt-mournblade/templates/roll-dialog-generic.html', rollData); + + return new MournbladeRollDialog(actor, rollData, html, options ); + } + + /* -------------------------------------------- */ + constructor(actor, rollData, html, options, close = undefined) { + let conf = { + title: (rollData.mode == "skill") ? "Skill" : "Roll", + content: html, + buttons: { + roll: { + icon: '', + label: "Roll !", + callback: () => { this.roll() } + }, + cancel: { + icon: '', + label: "Cancel", + callback: () => { this.close() } + } }, + close: close + } + + super(conf, options); + + this.actor = actor; + this.rollData = rollData; + } + + /* -------------------------------------------- */ + roll () { + MournbladeUtility.rollMournblade( this.rollData ) + } + + + /* -------------------------------------------- */ + manageEffects( effectIdx, toggled) { + let effect = this.rollData.effectsList[effectIdx] + if (effect) { + effect.applied = toggled + + let level, genre, idVal + if (effect.type == 'hindrance' ) { + level = effect.value + genre = 'positive' + idVal = "#hindranceDicesLevel" + } + if (effect.type == 'effect' ) { + let effectData = effect.effect + level = effectData.data.effectlevel + genre = effectData.data.genre + effectData.data.isUsed = toggled + if (effectData.data.bonusdice) { + idVal = "#bonusDicesLevel" + } + if (effectData.data.reducedicevalue || effectData.data.statdice) { + idVal = "#statDicesLevel" + } + if (effectData.data.otherdice) { + idVal = "#otherDicesLevel" + } + if (effectData.data.hindrance) { + idVal = "#hindranceDicesLevel" + genre = 'positive' // Dynamic fix + } + } + // Now process the dice level update + let newLevel = Number($(idVal).val()) + console.log("Ongoing", newLevel, toggled, idVal ) + if (toggled) { + if ( genre == 'positive') { + newLevel += Number(level) + }else { + newLevel -= Number(level) + } + }else { + if ( genre == 'positive') { + newLevel -= Number(level) + }else { + newLevel += Number(level) + } + } + newLevel = (newLevel<0) ? 0 : newLevel + $(idVal).val(newLevel) + } + //console.log("Effect", effect, toggled) + this.rollData.statDicesLevel = Number($('#statDicesLevel').val()) + this.rollData.specDicesLevel = Number($('#specDicesLevel').val()) + this.rollData.bonusDicesLevel = Number($('#bonusDicesLevel').val()) + this.rollData.hindranceDicesLevel = Number($('#hindranceDicesLevel').val()) + this.rollData.otherDicesLevel = Number($('#otherDicesLevel').val()) + } + + /* -------------------------------------------- */ + manageArmors( armorIdx, toggled) { + let armor = this.rollData.armorsList[armorIdx] + if (armor) { + armor.applied = toggled + if (armor.type == 'other' ) { + if (toggled) { + this.rollData.otherDicesLevel += Number(armor.value) + } else { + this.rollData.otherDicesLevel -= Number(armor.value) + this.rollData.otherDicesLevel = (this.rollData.otherDicesLevel<0) ? 0 : this.rollData.otherDicesLevel + } + $("#otherDicesLevel").val(this.rollData.otherDicesLevel) + } + } + console.log("Armor", armorIdx, toggled) + } + + /* -------------------------------------------- */ + manageWeapons( weaponIdx, toggled) { + let weapon = this.rollData.weaponsList[weaponIdx] + if (weapon) { + if (toggled) { + this.rollData.weaponName = weapon.weapon.name + } else { + this.rollData.weaponName = undefined + } + weapon.applied = toggled + if (weapon.type == 'damage' || weapon.type == 'enhanced' ) { + if (toggled) { + this.rollData.otherDicesLevel += Number(weapon.value) + } else { + this.rollData.weaponName = undefined + this.rollData.otherDicesLevel -= Number(weapon.value) + this.rollData.otherDicesLevel = (this.rollData.otherDicesLevel<0) ? 0 : this.rollData.otherDicesLevel + } + $("#otherDicesLevel").val(this.rollData.otherDicesLevel) + } + } + console.log("Weapon", weaponIdx, toggled, this.rollData.otherDicesLevel, weapon) + } + + /* -------------------------------------------- */ + manageEquip( equipIdx, toggled) { + let equip = this.rollData.equipmentsList[equipIdx] + if (equip) { + equip.applied = toggled + let idVal = "#otherDicesLevel" // Default + if (equip.equip.data.bonusdice) { + idVal = "#bonusDicesLevel" + } + if (equip.equip.data.statdice) { + idVal = "#statDicesLevel" + } + if (equip.equip.data.otherdice) { + idVal = "#otherDicesLevel" + } + let newLevel = Number($(idVal).val()) + if (toggled) { + newLevel += Number(equip.value) + } else { + newLevel -= Number(equip.value) + } + newLevel = (newLevel <0) ? 0 : newLevel + $(idVal).val(newLevel) + // Then refresh + this.rollData.statDicesLevel = Number($('#statDicesLevel').val()) + this.rollData.specDicesLevel = Number($('#specDicesLevel').val()) + this.rollData.bonusDicesLevel = Number($('#bonusDicesLevel').val()) + this.rollData.hindranceDicesLevel = Number($('#hindranceDicesLevel').val()) + this.rollData.otherDicesLevel = Number($('#otherDicesLevel').val()) + } + } + + /* -------------------------------------------- */ + activateListeners(html) { + super.activateListeners(html); + + var dialog = this; + function onLoad() { + } + $(function () { onLoad(); }); + + html.find('#specList').change(async (event) => { + this.rollData.selectedSpec = event.currentTarget.value + let spec = this.rollData.specList.find(item => item._id == this.rollData.selectedSpec) + if ( spec) { + this.rollData.specDiceLevel = spec.data.level + this.rollData.specName = spec.name + $('#specDicesLevel').val(this.rollData.specDiceLevel) + } else { + this.rollData.specName = undefined + $('#specDicesLevel').val(0) + } + const content = await renderTemplate("systems/fvtt-Mournblade-rpg/templates/roll-dialog-generic.html", this.rollData) + this.data.content = content + this.render(true) + }); + html.find('#statDicesLevel').change((event) => { + this.rollData.statDicesLevel = Number(event.currentTarget.value) + }); + html.find('#specDicesLevel').change((event) => { + this.rollData.specDicesLevel = Number(event.currentTarget.value) + }); + html.find('#bonusDicesLevel').change((event) => { + this.rollData.bonusDicesLevel = Number(event.currentTarget.value) + }); + html.find('#hindranceDicesLevel').change((event) => { + this.rollData.hindranceDicesLevel = Number(event.currentTarget.value) + }); + html.find('#otherDicesLevel').change((event) => { + this.rollData.otherDicesLevel = Number(event.currentTarget.value) + }); + html.find('.effect-clicked').change((event) => { + let toggled = event.currentTarget.checked + let effectIdx = $(event.currentTarget).data("effect-idx") + this.manageEffects( effectIdx, toggled) + }); + html.find('.armor-clicked').change((event) => { + let toggled = event.currentTarget.checked + let armorIdx = $(event.currentTarget).data("armor-idx") + this.manageArmors( armorIdx, toggled) + }); + html.find('.weapon-clicked').change((event) => { + let toggled = event.currentTarget.checked + let weaponIdx = $(event.currentTarget).data("weapon-idx") + this.manageWeapons( weaponIdx, toggled) + }); + html.find('.equip-clicked').change((event) => { + let toggled = event.currentTarget.checked + let equipIdx = $(event.currentTarget).data("equip-idx") + this.manageEquip( equipIdx, toggled) + }); + + + } +} \ No newline at end of file diff --git a/modules/mournblade-utility.js b/modules/mournblade-utility.js new file mode 100644 index 0000000..7e2e9da --- /dev/null +++ b/modules/mournblade-utility.js @@ -0,0 +1,395 @@ +/* -------------------------------------------- */ +import { MournbladeCombat } from "./mournblade-combat.js"; +import { MournbladeCommands } from "./mournblade-commands.js"; +import { MournbladeActorCreate } from "./mournblade-create-char.js"; + +/* -------------------------------------------- */ +export class MournbladeUtility { + + + /* -------------------------------------------- */ + static async init() { + Hooks.on('renderChatLog', (log, html, data) => MournbladeUtility.chatListeners(html)); + Hooks.on("getCombatTrackerEntryContext", (html, options) => { + MournbladeUtility.pushInitiativeOptions(html, options); + }); + Hooks.on("dropCanvasData", (canvas, data) => { + MournbladeUtility.dropItemOnToken(canvas, data) + }); + + this.rollDataStore = {} + this.defenderStore = {} + MournbladeCommands.init(); + + Handlebars.registerHelper('count', function (list) { + return list.length; + }); + Handlebars.registerHelper('includes', function (array, val) { + return array.includes(val); + }); + Handlebars.registerHelper('upper', function (text) { + return text.toUpperCase(); + }); + Handlebars.registerHelper('upperFirst', function (text) { + if (typeof text !== 'string') return text + return text.charAt(0).toUpperCase() + text.slice(1) + }); + Handlebars.registerHelper('notEmpty', function (list) { + return list.length > 0; + }); + + } + + /* -------------------------------------------- */ + static pushInitiativeOptions(html, options) { + } + + /* -------------------------------------------- */ + static getSkills() { + return this.skills + } + + /* -------------------------------------------- */ + static async ready() { + const skills = await MournbladeUtility.loadCompendium("fvtt-mournblade.skills") + this.skills = specs.map(i => i.toObject()) + } + + /* -------------------------------------------- */ + static async loadCompendiumData(compendium) { + const pack = game.packs.get(compendium); + return await pack?.getDocuments() ?? []; + } + + /* -------------------------------------------- */ + static async loadCompendium(compendium, filter = item => true) { + let compendiumData = await MournbladeUtility.loadCompendiumData(compendium); + return compendiumData.filter(filter); + } + + /* -------------------------------------------- */ + static getOptionsStatusList() { + return this.optionsStatusList; + } + /* -------------------------------------------- */ + static async chatListeners(html) { + + html.on("click", '.view-item-from-chat', event => { + game.system.Mournblade.creator.openItemView(event) + }); + } + + /* -------------------------------------------- */ + static async preloadHandlebarsTemplates() { + + const templatePaths = [ + 'systems/fvtt-mournblade/templates/editor-notes-gm.html', + 'systems/fvtt-mournblade/templates/partial-roll-select-effects.html', + 'systems/fvtt-mournblade/templates/partial-options-statistics.html', + 'systems/fvtt-mournblade/templates/partial-options-level.html', + 'systems/fvtt-mournblade/templates/partial-options-range.html', + 'systems/fvtt-mournblade/templates/partial-options-equipment-types.html', + 'systems/fvtt-mournblade/templates/partial-equipment-effects.html' + ] + return loadTemplates(templatePaths); + } + + /* -------------------------------------------- */ + static removeChatMessageId(messageId) { + if (messageId) { + game.messages.get(messageId)?.delete(); + } + } + + static findChatMessageId(current) { + return MournbladeUtility.getChatMessageId(MournbladeUtility.findChatMessage(current)); + } + + static getChatMessageId(node) { + return node?.attributes.getNamedItem('data-message-id')?.value; + } + + static findChatMessage(current) { + return MournbladeUtility.findNodeMatching(current, it => it.classList.contains('chat-message') && it.attributes.getNamedItem('data-message-id')); + } + + static findNodeMatching(current, predicate) { + if (current) { + if (predicate(current)) { + return current; + } + return MournbladeUtility.findNodeMatching(current.parentElement, predicate); + } + return undefined; + } + + /* -------------------------------------------- */ + static templateData(it) { + return MournbladeUtility.data(it)?.data ?? {} + } + + /* -------------------------------------------- */ + static data(it) { + if (it instanceof Actor || it instanceof Item || it instanceof Combatant) { + return it.data; + } + return it; + } + + /* -------------------------------------------- */ + static createDirectOptionList(min, max) { + let options = {}; + for (let i = min; i <= max; i++) { + options[`${i}`] = `${i}`; + } + return options; + } + + /* -------------------------------------------- */ + static buildListOptions(min, max) { + let options = "" + for (let i = min; i <= max; i++) { + options += `` + } + return options; + } + + /* -------------------------------------------- */ + static getTarget() { + if (game.user.targets && game.user.targets.size == 1) { + for (let target of game.user.targets) { + return target; + } + } + return undefined; + } + + /* -------------------------------------------- */ + static getDefenseState(actorId) { + return this.defenderStore[actorId]; + } + + /* -------------------------------------------- */ + static updateRollData(rollData) { + + let id = rollData.rollId; + let oldRollData = this.rollDataStore[id] || {}; + let newRollData = mergeObject(oldRollData, rollData); + this.rollDataStore[id] = newRollData; + } + /* -------------------------------------------- */ + static saveRollData(rollData) { + game.socket.emit("system.Mournblade-rpg", { + name: "msg_update_roll", data: rollData + }); // Notify all other clients of the roll + this.updateRollData(rollData); + } + + /* -------------------------------------------- */ + static getRollData(id) { + return this.rollDataStore[id]; + } + + /* -------------------------------------------- */ + static onSocketMesssage(msg) { + //console.log("SOCKET MESSAGE", msg.name, game.user.character.id, msg.data.defenderId); + if (msg.name == "msg_update_defense_state") { + this.updateDefenseState(msg.data.defenderId, msg.data.rollId); + } + if (msg.name == "msg_update_roll") { + this.updateRollData(msg.data); + } + } + + /* -------------------------------------------- */ + static chatDataSetup(content, modeOverride, isRoll = false, forceWhisper) { + let chatData = { + user: game.user.id, + rollMode: modeOverride || game.settings.get("core", "rollMode"), + content: content + }; + + if (["gmroll", "blindroll"].includes(chatData.rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM").map(u => u.id); + if (chatData.rollMode === "blindroll") chatData["blind"] = true; + else if (chatData.rollMode === "selfroll") chatData["whisper"] = [game.user]; + + if (forceWhisper) { // Final force ! + chatData["speaker"] = ChatMessage.getSpeaker(); + chatData["whisper"] = ChatMessage.getWhisperRecipients(forceWhisper); + } + + return chatData; + } + + /* -------------------------------------------- */ + static async showDiceSoNice(roll, rollMode) { + if (game.modules.get("dice-so-nice")?.active) { + if (game.dice3d) { + let whisper = null; + let blind = false; + rollMode = rollMode ?? game.settings.get("core", "rollMode"); + switch (rollMode) { + case "blindroll": //GM only + blind = true; + case "gmroll": //GM + rolling player + whisper = this.getUsers(user => user.isGM); + break; + case "roll": //everybody + whisper = this.getUsers(user => user.active); + break; + case "selfroll": + whisper = [game.user.id]; + break; + } + await game.dice3d.showForRoll(roll, game.user, true, whisper, blind); + } + } + } + + /* -------------------------------------------- */ + static async rollMournblade(rollData) { + + this.createChatWithRollMode(rollData.alias, { + content: await renderTemplate(`systems/fvtt-Mournblade-rpg/templates/chat-generic-result.html`, rollData) + }); + + // And save the roll + this.saveRollData(rollData); + } + + /* -------------------------------------------- */ + static getUsers(filter) { + return game.users.filter(filter).map(user => user.data._id); + } + /* -------------------------------------------- */ + static getWhisperRecipients(rollMode, name) { + switch (rollMode) { + case "blindroll": return this.getUsers(user => user.isGM); + case "gmroll": return this.getWhisperRecipientsAndGMs(name); + case "selfroll": return [game.user.id]; + } + return undefined; + } + /* -------------------------------------------- */ + static getWhisperRecipientsAndGMs(name) { + let recep1 = ChatMessage.getWhisperRecipients(name) || []; + return recep1.concat(ChatMessage.getWhisperRecipients('GM')); + } + + /* -------------------------------------------- */ + static blindMessageToGM(chatOptions) { + let chatGM = duplicate(chatOptions); + chatGM.whisper = this.getUsers(user => user.isGM); + chatGM.content = "Blinde message of " + game.user.name + "
" + chatOptions.content; + console.log("blindMessageToGM", chatGM); + game.socket.emit("system.fvtt-weapons-of-the-gods", { msg: "msg_gm_chat_message", data: chatGM }); + } + + /* -------------------------------------------- */ + static async searchItem(dataItem) { + let item; + if (dataItem.pack) { + item = await fromUuid("Compendium." + dataItem.pack + "." + dataItem.id); + } else { + item = game.items.get(dataItem.id) + } + return item; + } + + /* -------------------------------------------- */ + static split3Columns(data) { + + let array = [[], [], []]; + if (data == undefined) return array; + + let col = 0; + for (let key in data) { + let keyword = data[key]; + keyword.key = key; // Self-reference + array[col].push(keyword); + col++; + if (col == 3) col = 0; + } + return array; + } + + /* -------------------------------------------- */ + static createChatMessage(name, rollMode, chatOptions) { + switch (rollMode) { + case "blindroll": // GM only + if (!game.user.isGM) { + this.blindMessageToGM(chatOptions); + + chatOptions.whisper = [game.user.id]; + chatOptions.content = "Message only to the GM"; + } + else { + chatOptions.whisper = this.getUsers(user => user.isGM); + } + break; + default: + chatOptions.whisper = this.getWhisperRecipients(rollMode, name); + break; + } + chatOptions.alias = chatOptions.alias || name; + ChatMessage.create(chatOptions); + } + + /* -------------------------------------------- */ + static getBasicRollData() { + let rollData = { + rollId: randomID(16), + rollMode: game.settings.get("core", "rollMode"), + } + MournbladeUtility.updateWithTarget(rollData) + return rollData + } + + /* -------------------------------------------- */ + static updateWithTarget(rollData) { + let objectDefender + let target = MournbladeUtility.getTarget(); + if (target) { + let defenderActor = game.actors.get(target.data.actorId) + objectDefender = MournbladeUtility.data(defenderActor) + objectDefender = mergeObject(objectDefender, target.data.actorData) + rollData.defender = objectDefender + rollData.attackerId = this.id + rollData.defenderId = objectDefender._id + } + } + + /* -------------------------------------------- */ + static createChatWithRollMode(name, chatOptions) { + this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions); + } + + /* -------------------------------------------- */ + static async confirmDelete(actorSheet, li) { + let itemId = li.data("item-id"); + let msgTxt = "

Are you sure to remove this Item ?"; + let buttons = { + delete: { + icon: '', + label: "Yes, remove it", + callback: () => { + actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]); + li.slideUp(200, () => actorSheet.render(false)); + } + }, + cancel: { + icon: '', + label: "Cancel" + } + } + msgTxt += "

"; + let d = new Dialog({ + title: "Confirm removal", + content: msgTxt, + buttons: buttons, + default: "cancel" + }); + d.render(true); + } + +} \ No newline at end of file diff --git a/styles/simple.css b/styles/simple.css new file mode 100644 index 0000000..421b406 --- /dev/null +++ b/styles/simple.css @@ -0,0 +1,1218 @@ + /* ==================== (A) Fonts ==================== */ + + :root { + /* =================== 1. ACTOR SHEET FONT STYLES =========== */ + --window-header-title-font-size: 1.3rem; + --window-header-title-font-weight: normal; + --window-header-title-color: #f5f5f5; + + --major-button-font-size: 1.05rem; + --major-button-font-weight: normal; + --major-button-color: #dadada; + + --tab-header-font-size: 1.0rem; + --tab-header-font-weight: 700; + --tab-header-color: #403f3e; + --tab-header-color-active: #4a0404; + + --actor-input-font-size: 0.8rem; + --actor-input-font-weight: 500; + --actor-input-color: black; + + --actor-label-font-size: 0.8rem; + --actor-label-font-weight: 700; + --actor-label-color: #464331c4; + + /* =================== 2. DEBUGGING HIGHLIGHTERS ============ */ + --debug-background-color-red: #ff000054; + --debug-background-color-blue: #1d00ff54; + --debug-background-color-green: #54ff0054; + + --debug-box-shadow-red: inset 0 0 2px red; + --debug-box-shadow-blue: inset 0 0 2px blue; + --debug-box-shadow-green: inset 0 0 2px green; + } + +/*@import url("https://fonts.googleapis.com/css2?family=Martel:wght@400;800&family=Roboto:wght@300;400;500&display=swap");*/ +/* Global styles & Font */ +.window-app { + text-align: justify; + font-size: 16px; + letter-spacing: 1px; +} + +/* Fonts */ +.sheet header.sheet-header h1 input, .window-app .window-header, #actors .directory-list, #navigation #scene-list .scene.nav-item { + font-size: 1.0rem; +} /* For title, sidebar character and scene */ +.sheet nav.sheet-tabs { + font-size: 0.8rem; +} /* For nav and title */ +.window-app input, .foundryvtt-vadentis .item-form, .sheet header.sheet-header .flex-group-center.flex-compteurs, .sheet header.sheet-header .flex-group-center.flex-fatigue, select, button, .item-checkbox, #sidebar, #players, #navigation #nav-toggle { + font-size: 0.8rem; +} + +.window-header{ + background: rgba(0,0,0,0.75); +} + +.window-app.sheet .window-content { + margin: 0; + padding: 0; +} +.strong-text{ + font-weight: bold; +} + +.tabs .item.active, .blessures-list li ul li:first-child:hover, a:hover { + text-shadow: 1px 0px 0px #ff6600; +} + +.rollable:hover, .rollable:focus { + color: #000; + text-shadow: 0 0 10px red; + cursor: pointer; +} +input:disabled { + color:#1c2058; +} +select:disabled { + color:#1c2058; +} +table {border: 1px solid #7a7971;} + +.grid, .grid-2col { + display: grid; + grid-column: span 2 / span 2; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: 10px 0; + padding: 0; +} + +.grid-3col { + grid-column: span 3 / span 3; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-4col { + grid-column: span 4 / span 4; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.grid-5col { + grid-column: span 5 / span 5; + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-6col { + grid-column: span 5 / span 5; + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-7col { + grid-column: span 7 / span 7; + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + +.grid-8col { + grid-column: span 8 / span 8; + grid-template-columns: repeat(8, minmax(0, 1fr)); +} + +.grid-9col { + grid-column: span 9 / span 9; + grid-template-columns: repeat(9, minmax(0, 1fr)); +} + +.grid-10col { + grid-column: span 10 / span 10; + grid-template-columns: repeat(10, minmax(0, 1fr)); +} + +.grid-11col { + grid-column: span 11 / span 11; + grid-template-columns: repeat(11, minmax(0, 1fr)); +} + +.grid-12col { + grid-column: span 12 / span 12; + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +.flex-group-center, +.flex-group-left, +.flex-group-right { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + text-align: center; + padding: 5px; +} + +.flex-group-left { + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + text-align: left; +} + +.flex-group-right { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + text-align: right; +} + +.flex-center { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + text-align: center; +} + +.table-create-actor { + font-size: 0.8rem; +} + +.flex-between { + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.flex-shrink { + flex: 'flex-shrink' ; +} + +/* Styles limited to foundryvtt-vadentis sheets */ + +.fvtt-pegasus-rpg .sheet-header { + -webkit-box-flex: 0; + -ms-flex: 0 0 210px; + flex: 0 0 210px; + overflow: hidden; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + margin-bottom: 10px; +} + +.fvtt-pegasus-rpg .sheet-header .profile-img { + -webkit-box-flex: 0; + -ms-flex: 0 0 128px; + flex: 0 0 128px; + height: 128px; + width: 128px; + margin-right: 10px; + object-fit: cover; + object-position: 50% 0; +} + +.button-img { + vertical-align: baseline; + width: 8%; + height: 8%; + max-height: 48px; + border-width: 0; + border: 1px solid rgba(0, 0, 0, 0); +} +.button-img:hover { + color: rgba(255, 255, 128, 0.7); + border: 1px solid rgba(255, 128, 0, 0.8); + cursor: pointer; +} + +.button-effect-img { + vertical-align: baseline; + width: 16px; + max-height: 16px; + height: 16; + border-width: 0; +} + +.small-button-container { + height: 16px; + width: 16px; + border: 0; + vertical-align: bottom; +} + +.fvtt-pegasus-rpg .sheet-header .header-fields { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.fvtt-pegasus-rpg .sheet-header h1.charname { + height: 50px; + padding: 0px; + margin: 5px 0; + border-bottom: 0; +} + +.fvtt-pegasus-rpg .sheet-header h1.charname input { + width: 100%; + height: 100%; + margin: 0; +} + +.fvtt-pegasus-rpg .sheet-tabs { + -webkit-box-flex: 0; + -ms-flex: 0; + flex: 0; +} + +.fvtt-pegasus-rpg .sheet-body, +.fvtt-pegasus-rpg .sheet-body .tab, +.fvtt-pegasus-rpg .sheet-body .tab .editor { + height: 100%; + font-size: 0.8rem; +} + +.editor { + border: 2; + height: 300px; + padding: 0 3px; +} + +.medium-editor { + border: 2; + height: 240px; + padding: 0 3px; +} + +.small-editor { + border: 2; + height: 120px; + padding: 0 3px; +} + +.fvtt-pegasus-rpg .tox .tox-editor-container { + background: #fff; +} + +.fvtt-pegasus-rpg .tox .tox-edit-area { + padding: 0 8px; +} + +.fvtt-pegasus-rpg .resource-label { + font-weight: bold; + text-transform: uppercase; +} + +.fvtt-pegasus-rpg .tabs { + height: 40px; + border-top: 1px solid #AAA; + border-bottom: 1px solid #AAA; + color: #000000; +} + +.fvtt-pegasus-rpg .tabs .item { + line-height: 40px; + font-weight: bold; +} + +.fvtt-pegasus-rpg .tabs .item.active { + text-decoration: underline; + text-shadow: none; +} + +.fvtt-pegasus-rpg .items-list { + list-style: none; + margin: 1px 0; + padding: 0; + overflow-y: auto; +} + +.fvtt-pegasus-rpg .items-list .item-header { + font-weight: bold; +} + +.fvtt-pegasus-rpg .items-list .item { + height: 30px; + line-height: 24px; + padding: 1px 0; + border-bottom: 1px solid #BBB; +} + +.fvtt-pegasus-rpg .items-list .item .item-image { + -webkit-box-flex: 0; + -ms-flex: 0 0 24px; + flex: 0 0 24px; + margin-right: 5px; +} + +.fvtt-pegasus-rpg .items-list .item img { + display: block; +} + +.fvtt-pegasus-rpg .items-list .item-name { + margin: 0; +} + +.fvtt-pegasus-rpg .items-list .item-controls { + -webkit-box-flex: 0; + -ms-flex: 0 0 86px; + flex: 0 0 86px; + text-align: right; +} + + +/* ======================================== */ +/* Sheet */ +.window-app.sheet .window-content .sheet-header{ + background: url("../images/ui/pc_sheet_bg.webp") +} +/* background: #011d33 url("../images/ui/fond1.webp") repeat left top;*/ +/*color: rgba(168, 139, 139, 0.5);*/ + +.window-app.sheet .window-content .sheet-header input[type="text"], .window-app.sheet .window-content .sheet-header input[type="number"], .window-app.sheet .window-content .sheet-header input[type="password"], .window-app.sheet .window-content .sheet-header input[type="date"], .window-app.sheet .window-content .sheet-header input[type="time"] { + color: rgba(36, 37, 37, 0.75); + background: rgba(255, 255, 255, 0.05); + border: 0 none; + margin-bottom: 0.25rem; +} + +.window-app .window-content, .window-app.sheet .window-content .sheet-body{ + font-size: 0.8rem; + background: url("../images/ui/pc_sheet_bg.webp") repeat left top; +} + +/* background: rgba(245,245,240,0.6) url("../images/ui/sheet_background.webp") left top;*/ + +section.sheet-body{padding: 0.25rem 0.5rem;} + +.sheet header.sheet-header .profile-img { + object-fit: cover; + object-position: 50% 0; + margin: 0.5rem 0 0.5rem 0.5rem; + padding: 0; +} + +.sheet nav.sheet-tabs { + font-size: 0.70rem; + font-weight: bold; + height: 3rem; + flex: 0 0 3rem; + margin: 0; + padding: 0 0 0 0.25rem; + text-align: center; + text-transform: uppercase; + line-height: 1.5rem; + border-top: 0 none; + border-bottom: 0 none; + background-color:black; + color:beige; +} + +/* background: rgb(245,245,240) url("../images/ui/fond4.webp") repeat left top;*/ + +nav.sheet-tabs .item { + position: relative; + padding: 0 0.25rem; +} + +nav.sheet-tabs .item:after { + content: ""; + position: absolute; + top: 0; + right: 0; + height: 2rem; + width: 1px; + border-right: 1px dashed rgba(52, 52, 52, 0.25); +} + +.sheet .tab[data-tab] { + padding: 0; +} + +section.sheet-body:after { + content: ""; + display: block; + clear: both; +} + +.sheet header.sheet-header .flex-compteurs {text-align: right;} +.sheet header.sheet-header .resource-content {width: 2rem;} + +.select-diff { + display: inline-block; + text-align: left; + width: 50px; +} + +.window-app.sheet .window-content .tooltip:hover .tooltiptext { + top: 2rem; + left: 2rem; + margin: 0; + padding: 0.25rem; +} + +.window-app.sheet .window-content .carac-value, .window-app.sheet .window-content .competence-xp { + margin: 0.05rem; + flex-basis: 3rem; + text-align: center; +} + +/* ======================================== */ +/* Global UI elements */ + +/* ======================================== */ + +h1, h2, h3, h4 { + font-weight: bold; +} + +ul, ol { + margin: 0; + padding: 0; +} +ul, li { + list-style-type: none; +} + +.sheet li { + margin: 0.010rem; + padding: 0.25rem; +} +.header-fields li { + margin: 0; + padding: 0; +} + +.alterne-list > .list-item:hover { + background: rgba(100, 100, 50, 0.25); +} +.alterne-list > .list-item:nth-child(even) { + background: rgba(80, 60, 0, 0.10); +} +.alterne-list > .list-item:nth-child(odd) { + background: rgb(160, 130, 100, 0.05); +} + +.specialisation-label { + font-size: 0.8rem; +} + +.carac-label, +.attr-label { + font-weight: bold; +} + +.list-item { + margin: 0.125rem; + box-shadow: inset 0px 0px 1px #00000096; + border-radius: 0.25rem; + padding: 0.125rem; + flex: 1 1 5rem; +} +.item-display-show { + display: block; +} +.item-display-hide { + display: none; +} +.conteneur-type { + background: rgb(200, 10, 100, 0.25); +} + +.item-quantite { + margin-left: 0.5rem; +} + +.list-item-margin1 { + margin-left: 1rem; +} +.list-item-margin2 { + margin-left: 2rem; +} +.list-item-margin3 { + margin-left: 3rem; +} +.list-item-margin4 { + margin-left: 4rem; +} + +.sheet-competence-img { + width: 24px; + height: 24px; + flex-grow: 0; + margin-right: 0.25rem; +} +.competence-column { + flex-direction: column; + align-content: flex-start; + justify-content: flex-start; + flex-grow: 0; + flex-basis: 1; +} +.competence-header { + align-content: flex-start; + justify-content: flex-start; + font-weight: bold; + flex-grow: 0; +} +.secondaire-label, +.arme-label, +.generic-label, +.competence-label, +.devotion-label, +.sort-label, +.technique-label, +.stat-label, +.arme-label, +.armure-label, +.equipement-label, +.description-label { + flex-grow: 2; + margin-left: 4px; +} +.roll-dialog-label { + margin: 4px 0; +} + +.short-label { + flex-grow: 1; +} +.keyword-label { + font-size: 0.85rem; +} + +.item-sheet-label { + flex-grow: 1; +} + +.item-text-long-line { + flex-grow: 3; +} + +.score-label { + flex-grow: 2; + align-content: center; +} + +.attribut-value, +.carac-value { + flex-grow: 0; + flex-basis: 64px; + margin-right: 4px; + margin-left: 4px; +} +.sante-value, +.competence-value { + flex-grow: 0; + flex-basis: 2rem; + margin-right: 0.25rem; + margin-left: 0.25rem; +} +.description-value { + flex-grow: 0; + flex-basis: 4rem; + margin-right: 0.25rem; + margin-left: 0.25rem; +} +.competence-xp { + flex-grow: 0; + flex-basis: 2rem; + margin-right: 0.25rem; + margin-left: 0.25rem; +} +.blessures-title { + font-weight: bold; +} +.alchimie-title { + font-weight: bold; +} +.blessure-data { + flex-direction: row; + align-content: flex-start; + justify-content: flex-start; +} +.blessures-soins { + flex-grow: 0; + flex-basis: 32px; + margin-right: 4px; + margin-left: 4px; +} +.blessures-loc { + flex-grow: 0; + flex-basis: 96px; + margin-right: 4px; + margin-left: 4px; +} +.pointsreve-value { + flex-grow: 0; + flex-basis: 64px; + margin-right: 4px; + margin-left: 4px; +} + +.input-sante-header, +.stress-style { + flex-grow: 0; + flex-basis: 64px; + margin-right: 4px; + margin-left: 4px; +} + +.small-label { + margin-top: 5px; +} + +.padd-right { + margin-right: 8px; +} +.padd-left { + margin-left: 8px; +} + +.stack-left { + align-items:center; + flex-shrink: 1; + flex-grow: 0; +} +.npc-stat-label { + flex-grow: 2; +} + +.packed-left { + white-space: nowrap; + flex-grow: 0; +} + +.input-numeric-short { + width: 40px; + max-width: 40px; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 40px; + margin-right: 0.25rem; + margin-left: 0.25rem; +} + +.stats-table { + align-content: flex-start; +} + +/* ======================================== */ +.tokenhudext { + display: flex; + flex: 0 !important; + font-weight: 600; +} +.tokenhudext.left { + justify-content: flex-start; + flex-direction: column; + position: absolute; + top: 2.75rem; + right: 4rem; +} +.tokenhudext.right { + justify-content: flex-start; + flex-direction: column; + position: absolute; + top: 2.75rem; + left: 4rem; +} +.control-icon.tokenhudicon { + width: fit-content; + height: fit-content; + min-width: 6rem; + flex-basis: auto; + padding: 0; + line-height: 1rem; + margin: 0.25rem; +} +.control-icon.tokenhudicon.right { + margin-left: 8px; +} +#token-hud .status-effects.active{ + z-index: 2; +} +/* ======================================== */ +.item-checkbox { + height: 25px; + border: 1px solid #736953a6; + border-left: none; + font-weight: 500; + font-size: 1rem; + color: black; + padding-top: 5px; + margin-right: 0px; + width: 45px; + position: relative; + left: 0px; + text-align: center; +} + + +.flex-actions-bar { + flex-grow: 2; +} + +/* ======================================== */ +/* Sidebar CSS */ +#sidebar { + font-size: 1rem; + background-position: 100%; + color: rgba(220,220,220,0.75); +} + +/* background: rgb(105,85,65) url("../images/ui/texture_feuille_perso_onglets.webp") no-repeat right bottom;*/ + +#sidebar.collapsed { + height: 470px !important; +} + +#sidebar-tabs > .collapsed, #chat-controls .chat-control-icon { + color: rgba(220,220,220,0.75); + text-shadow: 1px 1px 0 rgba(0,0,0,0.75); +} + +.sidebar-tab .directory-list .entity { + border-top: 1px dashed rgba(0,0,0,0.25); + border-bottom: 0 none; + padding: 0.25rem 0; +} + +.sidebar-tab .directory-list .entity:hover { + background: rgba(0,0,0,0.05); + cursor: pointer; +} +.chat-message-header { + background: rgba(220,220,210,0.5); + font-size: 1.1rem; + height: 48px; + text-align: center; + vertical-align: middle; + display: flex; + align-items: center; +} + +.chat-message .message-header .flavor-text, .chat-message .message-header .whisper-to { + font-size: 0.9rem; +} +.chat-actor-name { + padding: 4px; +} + +.chat-img { + width: 64px; + height: 64px; +} + +.roll-dialog-header { + height: 52px; +} + +.actor-icon { + float: left; + width: 48px; + height: 48px; + padding: 2px 6px 2px 2px; +} + +.padding-dice { + padding-top: .2rem; + padding-bottom: .2rem; +} + +.dice-image { + box-sizing: border-box; + border: none; + border-radius: 0; + max-width: 100%; +} + +.dice-image-reroll { + background-color:rgba(115, 224, 115, 0.25); + border-color: #011d33; + box-sizing: border-box; + border: 1px; + border-radius: 0%; + max-width: 100%; +} + +.chat-dice { + width: 15%; + height: 15%; + font-size: 15px; + padding: 10px; + padding-bottom: 20px; + padding-top: .2rem; + padding-bottom: .2rem; +} + +.div-river-full { + height: 5rem; + align-items: flex-start; +} + +.div-river { + align-content: center; + margin-left: 8px; + align-content:space-around; + justify-content: space-around; +} + +.div-center { + align-self: center; +} + +.chat-message { + background: rgba(220,220,210,0.5); + font-size: 0.9rem; +} + +.chat-message.whisper { + background: rgba(220,220,210,0.75); + border: 2px solid #545469; +} +.chat-message .chat-icon { + border: 0; + padding: 2px 6px 2px 2px; + float: left; + width: 64px; + height: 64px; +} + +#sidebar-tabs { + flex: 0 0 32px; + box-sizing: border-box; + margin: 0 0 5px; + border-bottom: 1px solid rgba(0,0,0,0); + box-shadow: inset 0 0 2rem rgba(0,0,0,0.5); +} + +#sidebar-tabs > .item.active { + border: 1px solid rgba(114,98,72,1); + background: rgba(30, 25, 20, 0.75); + box-shadow: 0 0 6px inset rgba(114,98,72,1); +} + +#sidebar #sidebar-tabs i{ + width: 25px; + height: 25px; + display: inline-block; + background-position:center; + background-size:cover; + text-shadow: 1px 1px 0 rgba(0,0,0,0.75); + +} + +/*#sidebar #sidebar-tabs i.fa-comments:before, #sidebar #sidebar-tabs i.fa-fist-raised:before, #sidebar #sidebar-tabs i.fa-users:before, #sidebar #sidebar-tabs i.fa-map:before, #sidebar #sidebar-tabs i.fa-suitcase:before, #sidebar #sidebar-tabs i.fa-book-open:before, #sidebar #sidebar-tabs i.fa-th-list:before, #sidebar #sidebar-tabs i.fa-music:before, #sidebar #sidebar-tabs i.fa-atlas:before, #sidebar #sidebar-tabs i.fa-cogs:before {content: "";} +#sidebar #sidebar-tabs i.fa-comments {background: url("img/ui/icon_sidebar_chat.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-fist-raised {background: url("img/ui/icon_sidebar_fight.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-users {background: url("img/ui/icon_sidebar_actor.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-map {background: url("img/ui/icon_sidebar_scene.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-suitcase {background: url("img/ui/icon_sidebar_item.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-book-open {background: url("img/ui/icon_sidebar_journal.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-th-list {background: url("img/ui/icon_sidebar_rolltable.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-music {background: url("img/ui/icon_sidebar_music.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-atlas {background: url("img/ui/icon_sidebar_compendium.svg") no-repeat;} +#sidebar #sidebar-tabs i.fa-cogs {background: url("img/ui/icon_sidebar_settings.svg") no-repeat;} + +#combat #combat-controls { + box-shadow: inset 0 0 2rem rgba(0,0,0,0.5); +} +*/ + +/*--------------------------------------------------------------------------*/ +/* Control, Tool, hotbar & navigation */ + +#controls .scene-control, #controls .control-tool { + box-shadow: 0 0 3px #000; + margin: 0 0 8px; + border-radius: 0; + background: rgba(30, 25, 20, 1); + background-origin: padding-box; + border-image: url(img/ui/footer-button.png) 10 repeat; + border-image-width: 4px; + border-image-outset: 0px; +} + +#controls .scene-control.active, #controls .control-tool.active, #controls .scene-control:hover, #controls .control-tool:hover { + background: rgba(72, 46, 28, 1); + background-origin: padding-box; + border-image: url(img/ui/footer-button.png) 10 repeat; + border-image-width: 4px; + border-image-outset: 0px; + box-shadow: 0 0 3px #ff6400; +} + +#hotbar #action-bar #macro-list { + border: 1px solid rgba(72, 46, 28, 1); + box-shadow: 2px 2px 5px #000000; +} + +#hotbar #action-bar .macro { + border-image: url(img/ui/bg_control.jpg) 21 repeat; + border-image-slice: 6 6 6 6 fill; + border-image-width: 6px 6px 6px 6px; + border-image-outset: 0px 0px 0px 0px; + border-radius: 0px; +} + +#hotbar .bar-controls { + background: rgba(30, 25, 20, 1); + border: 1px solid rgba(72, 46, 28, 1); +} + +#players { + border-image: url(img/ui/footer-button.png) 10 repeat; + border-image-width: 4px; + border-image-outset: 0px; + background: rgba(30, 25, 20, 1); +} + +#navigation #scene-list .scene.nav-item.active { + background: rgba(72, 46, 28, 1); +} + +#navigation #scene-list .scene.nav-item { + background: rgba(30, 25, 20, 1); + background-origin: padding-box; + border-image: url(img/ui/footer-button.png) 10 repeat; + border-image-width: 4px; + border-image-outset: 0px; +} + +#navigation #scene-list .scene.view, #navigation #scene-list .scene.context { + background: rgba(72, 46, 28, 1); + background-origin: padding-box; + border-image: url(img/ui/footer-button.png) 10 repeat; + border-image-width: 4px; + border-image-outset: 0px; + box-shadow: 0 0 3px #ff6400; +} + +#navigation #nav-toggle { + background: rgba(30, 25, 20, 1); + background-origin: padding-box; + border-image: url(img/ui/footer-button.png) 10 repeat; + border-image-width: 4px; + border-image-outset: 0px; +} + +/* Tooltip container */ +.tooltip { + position: relative; + display: inline-block; + /*border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ +} + +/* Tooltip text */ +.tooltip .tooltiptext { + text-align: left; + background: rgba(231, 229, 226, 0.9); + width: 150px; + padding: 3px 0; + font-size: 0.9rem; + + /* Position the tooltip text */ + top: 1px; + position: absolute; + z-index: 1; + + /* Fade in tooltip */ + visibility: hidden; + opacity: 0; + transition: opacity 0.3s; +} + +.tooltip .ttt-fatigue{ + width: 360px; + + background: rgba(30, 25, 20, 0.9); + border-image: url(img/ui/bg_control.jpg) 21 repeat; + border-image-slice: 6 6 6 6 fill; + border-image-width: 6px 6px 6px 6px; + border-image-outset: 0px 0px 0px 0px; + border-radius: 0px; + + font-size: 0.8rem; + padding: 3px 0; +} + +.tooltip .ttt-ajustements { + width: 150px; + background: rgba(220,220,210,0.95); + border-radius: 6px; + font-size: 0.9rem; + padding: 3px 0; +} + +.tooltip-nobottom { + border-bottom: unset; /* If you want dots under the hoverable text */ +} +.tooltip .ttt-xp { + width: 250px; + background: rgba(220,220,210,0.95); + border-radius: 6px; + font-size: 0.9rem; + padding: 3px 0; +} + +/* Show the tooltip text when you mouse over the tooltip container */ +.tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; +} + +.river-button { + box-shadow: inset 0px 1px 0px 0px #a6827e; + background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%); + background-color: #7d5d3b00; + border-radius: 3px; + border: 2px ridge #846109; + display: inline-block; + cursor: pointer; + color: #ffffff; + font-size: 0.8rem; + padding: 2px 4px 0px 4px; + text-decoration: none; + text-shadow: 0px 1px 0px #4d3534; + position: relative; + margin:4px; +} + +.chat-card-button { + box-shadow: inset 0px 1px 0px 0px #a6827e; + background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%); + background-color: #7d5d3b00; + border-radius: 3px; + border: 2px ridge #846109; + display: inline-block; + cursor: pointer; + color: #ffffff; + font-size: 0.8rem; + padding: 4px 12px 0px 12px; + text-decoration: none; + text-shadow: 0px 1px 0px #4d3534; + position: relative; + margin:2px; +} + +.chat-card-button:hover { + background: linear-gradient(to bottom, #800000 5%, #3e0101 100%); + background-color: red; +} +.chat-card-button:active { + position:relative; + top:1px; +} + +.plus-minus-button { + box-shadow: inset 0px 1px 0px 0px #a6827e; + background: linear-gradient(to bottom, #21374afc 5%, #152833ab 100%); + background-color: #7d5d3b00; + border-radius: 3px; + border: 2px ridge #846109; + display: inline-block; + cursor: pointer; + color: #ffffff; + padding: 2px 6px 0px 6px; + text-decoration: none; + text-shadow: 0px 1px 0px #4d3534; + position: relative; + margin:3px; +} + +.river-button:hover, +.plus-minus-button:hover, +.chat-card-button:hover { + background: linear-gradient(to bottom, #800000 5%, #3e0101 100%); + background-color: red; +} + +.plus-minus-button:active, +.chat-card-button:active { + position:relative; + top:1px; +} + +.plus-minus { + font-size: 0.9rem; + font-weight: bold; +} + +.ul-level1 { + padding-left: 2rem; +} + +.drop-equipment-effect, +.drop-power-effect, +.drop-perk-effect, +.drop-ability-effect, +.drop-effect-specaffected, +.drop-effect-spec, +.drop-ability-weapon, +.drop-ability-armor, +.drop-race-perk, +.drop-spec-perk, +.drop-ability-power, +.drop-ability-spec, +.drop-spec-power, +.drop-abilities, +.drop-optionnal-abilities, +.drop-specialperk1, +.drop-perk2, +.drop-spec1 , +.drop-spec2 { + background: linear-gradient(to bottom, #6c95b9fc 5%, #105177ab 100%); + background-color: #7d5d3b00; + border-radius: 3px; + border: 2px ridge #846109; +} + +/*************************************************************/ +#pause +{ + font-size: 2rem; +} +#pause > h3 +{ + color: #CCC +} +#pause > img { + content: url(../images/ui/pegasus_logo_v1.webp); + height: 160px; + width: 256px; + top: -80px; + left: calc(50% - 132px); +} + +#logo { + content : url(../images/ui/pegasus_logo_v1.webp); + width: 100px; + height: 60px; +} + +.dice-cell { + padding-left: 12px; + padding-right: 12px; + width: 60px; + text-align: center; +} + +.dice-formula, +.dice-total { + height: 54px; + position:relative; +} diff --git a/system.json b/system.json new file mode 100644 index 0000000..a7691e4 --- /dev/null +++ b/system.json @@ -0,0 +1,48 @@ +{ + "author": "Uberwald/LeRatierBretonnien", + "compatibleCoreVersion": "9", + "description": "Mournblade RPG for FoundryVTT", + "download": "", + "esmodules": [ + "modules/mournblade-main.js" + ], + "gridDistance": 5, + "gridUnits": "m", + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "library": false, + "license": "LICENSE.txt", + "manifest": "", + "manifestPlusVersion": "1.0.0", + "media": [], + "minimumCoreVersion": "0.8.0", + "name": "fvtt-mournblade", + "packs": [ + { + "entity": "Item", + "label": "Skills", + "name": "skills", + "path": "./packs/skills.db", + "system": "fvtt-mournblade", + "tags": [ + "skill", "competence" + ] + } + ], + "primaryTokenAttribute": "secondary.health", + "secondaryTokenAttribute": "secondary.delirium", + "socket": true, + "styles": [ + "styles/simple.css" + ], + "templateVersion": 1, + "title": "Mournblade", + "url": "", + "version": "0.0.1", + "background": "./images/ui/mournblade_welcome.webp" +} \ No newline at end of file diff --git a/template.json b/template.json new file mode 100644 index 0000000..2719b4a --- /dev/null +++ b/template.json @@ -0,0 +1,138 @@ +{ + "Actor": { + "types": ["character", "npc"], + "templates": { + "biodata": { + "biodata": { + "name": "", + "age": 0, + "alignement": "", + "poids": "", + "taille": "", + "cheveux": "", + "sexe": "", + "yeux": "", + "description": "", + "notes": "", + "gmnotes": "" + } + }, + "core": { + "subactors": [], + "attributs": { + "adr":{ + "label": "Adresse", + "abbrev": "adr", + "value": 1 + }, + "pui":{ + "label": "Puissance", + "abbrev": "pui", + "value": 1 + }, + "cla":{ + "label": "Clairvoyance", + "abbrev": "cla", + "value": 1 + }, + "pre":{ + "label": "Présence", + "abbrev": "pre", + "value": 0 + }, + "tre":{ + "label": "Trempe", + "abbrev": "tre", + "value": 0 + } + }, + "bonneaventure": { + "base": 0, + "actuelle": 0 + }, + "experience": { + "value": 0 + }, + "eclat": { + "value": 0 + }, + "sante": { + "base": 0, + "nonletaux": 0, + "letaux": 0, + "sequelles": "" + }, + "ame": { + "base": 0, + "value": 0, + "traumatismes": "" + }, + "combat": { + "initiative": 0, + "vitesse": 0, + "bonusdegats": 0, + "defensebase": 0 + }, + "balance": { + "loi": 0, + "chaos": 0, + "aspect": 0, + "marge": 0, + "pointschaos": 0, + "pointsloi": 0 + } + }, + "npccore": { + "npctype": "", + "description": "" + } + }, + "character": { + "templates": [ "biodata", "core" ] + }, + "npc": { + "templates": [ "npccore" ] + } + }, + "Item": { + "templates": { + "base": { + "description": "" + } + }, + "types": [ "arme", "competence", "protection", "pacte", "traitchaotique", "monnaie", "don", "tendance", "rune"], + "arme": { + "templates": [ "base" ] + }, + "predilection": { + "competenceId": "", + "templates": [ "base" ] + }, + "competence": { + "niveau": 0, + "attribut": "", + "templates": [ "base" ] + }, + "protection": { + "templates": [ "base" ] + }, + "pacte": { + "templates": [ "base" ] + }, + "traitchaotique": { + "templates": [ "base" ] + }, + "monnaie": { + "templates": [ "base" ] + }, + "don": { + "templates": [ "base" ] + }, + "tendance": { + "templates": [ "base" ] + }, + "rune": { + "templates": [ "base" ] + } + } +}