diff --git a/README.md b/README.md index 8a8f0e9..8f7445e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ It provides support for **character sheets only**, game content should be drawn * clean-up layout * Add relationship Item Type -* Vaesen, NPC and HQ sheets +* Vaesen and NPC sheets * UX improvements * Dialogs and Chat @@ -36,6 +36,8 @@ It provides support for **character sheets only**, game content should be drawn * Localize Dialog and tooltip texts +* Link conditions on sheet and status icons on the character sheet (adding a status from the token toggels it on the sheet and visa versa) + ## Related Website - https://foundryvtt.com/ @@ -45,6 +47,13 @@ It provides support for **character sheets only**, game content should be drawn [GNU General Public License v3.0](https://choosealicense.com/licenses/gpl-3.0/) ## Release Notes +v2.1.4 +- Added roll dialog to the Recovery Rolls to allow for bonus dice from headquarters etc. +- Removed Damage from results in chat on things which have no damage. +- Added custom condition icons in foundry status icon menu (https://github.com/fvtt-fria-ligan/vaesen-foundry-vtt/blob/master/asset/status_icons.png?raw=true) +- Tightned styled and clarified the roll dialog (https://github.com/fvtt-fria-ligan/vaesen-foundry-vtt/blob/master/asset/roll_dialog.png?raw=true) + + v2.1.3 - (bugfix) Agility modifier for Armor was not calculating in the roll dialog. Now highest negative modifier for armor will apply to agility tests. - (Change) Updated look and feel of Headquarters sheet. Now all upgrades are on first page and "history" is relegated to a tab. diff --git a/asset/roll_dialog.png b/asset/roll_dialog.png new file mode 100644 index 0000000..3e67e69 Binary files /dev/null and b/asset/roll_dialog.png differ diff --git a/asset/status/arm-sling.svg b/asset/status/arm-sling.svg new file mode 100644 index 0000000..cba4c3f --- /dev/null +++ b/asset/status/arm-sling.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/broken-bone.svg b/asset/status/broken-bone.svg new file mode 100644 index 0000000..34c8ea7 --- /dev/null +++ b/asset/status/broken-bone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/despair.svg b/asset/status/despair.svg new file mode 100644 index 0000000..54f9e6b --- /dev/null +++ b/asset/status/despair.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/oppression.svg b/asset/status/oppression.svg new file mode 100644 index 0000000..f72c12c --- /dev/null +++ b/asset/status/oppression.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/pummeled.svg b/asset/status/pummeled.svg new file mode 100644 index 0000000..d945c2b --- /dev/null +++ b/asset/status/pummeled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/revolt.svg b/asset/status/revolt.svg new file mode 100644 index 0000000..4786a98 --- /dev/null +++ b/asset/status/revolt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/shattered-heart.svg b/asset/status/shattered-heart.svg new file mode 100644 index 0000000..ff42353 --- /dev/null +++ b/asset/status/shattered-heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status/terror.svg b/asset/status/terror.svg new file mode 100644 index 0000000..221e496 --- /dev/null +++ b/asset/status/terror.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/asset/status_icons.png b/asset/status_icons.png new file mode 100644 index 0000000..c7e1aa1 Binary files /dev/null and b/asset/status_icons.png differ diff --git a/script/actor/vaesen.js b/script/actor/vaesen.js index 39b2c71..3fccdb2 100644 --- a/script/actor/vaesen.js +++ b/script/actor/vaesen.js @@ -9,6 +9,77 @@ export class VaesenActor extends Actor { } + /* -------------------------------------------- */ + findStatusEffectById(id) { + return Array.from(this.effects?.values()) + .find(it => it.data.flags.core?.statusId === id); +} + +/* -------------------------------------------- */ +async deleteStatusEffectById(id, options = {renderSheet: true}) { + console.log("delete by id: " + id); + const effects = Array.from(this.effects?.values()) + .filter(it => it.data.flags.core?.statusId === id); + await this._deleteStatusEffects(effects, options); +} + +/* -------------------------------------------- */ +async _deleteStatusEffects(effects, options) { + console.log(effects); + console.log(effects.map(it => it.id)); + console.log(options); + const ids = Array.from(effects.map(it => it.id)); + await this.deleteEmbeddedDocuments('ActiveEffect', ids, options); + //await this._deleteStatusEffectsByIds(effects.map(it => it.id), options); + +} + +/* -------------------------------------------- */ +async _deleteStatusEffectsByIds(effectIds, options) { + await this.deleteEmbeddedDocuments('ActiveEffect', effectIds, options); +} + +/* -------------------------------------------- */ +async addStatusEffectById(id, options = {renderSheet: false, unique: true}) { + if (this.hasStatusEffectById(id) && options.unique === true) { + return; + } + const statusEffect = CONFIG.statusEffects.find(it => it.id === id); + await this.addStatusEffect(statusEffect, options); +} + +/* -------------------------------------------- */ +async addStatusEffect(statusEffect, options = {renderSheet: false, overlay: false}) { + //await this.deleteStatusEffectById(statusEffect.id, options); + const effect = duplicate(statusEffect); + console.log(effect.label); + console.log(effect.id); + await this.createEmbeddedDocuments("ActiveEffect", [{ + "flags.core.statusId": effect.id, + "flags.core.overlay": options.overlay, + label: effect.label, + icon: effect.icon, + origin: this.uuid, + }], options); +} + + /* -------------------------------------------- */ + hasStatusEffectById(id) { + const effects = this.findStatusEffectById(id); + return (effects !== undefined); +} + +async toggleStatusEffectById(id, options = {renderSheet: true}) { + console.log("over to the character for toggeling"); + + const effect = this.findStatusEffectById(id); + + if (effect) { + await this.deleteStatusEffectById(id); + } else { + await this.addStatusEffectById(id, options) + } +} static async create(data, options={}) { data.token = data.token || {}; diff --git a/script/config.js b/script/config.js new file mode 100644 index 0000000..f3e67f8 --- /dev/null +++ b/script/config.js @@ -0,0 +1,3 @@ +export const tftloop = {}; + +export const vaesen = {}; \ No newline at end of file diff --git a/script/hooks.js b/script/hooks.js index 0de90ce..72ebac7 100644 --- a/script/hooks.js +++ b/script/hooks.js @@ -14,8 +14,16 @@ import { AttackCharacterSheet } from "./sheet/attack.js"; import { UpgradeCharacterSheet } from "./sheet/upgrade.js"; import { prepareRollDialog, push } from "./util/roll.js"; import { registerSystemSettings } from "./util/settings.js"; +import {vaesen} from "./config.js" +import {conditions} from "./util/conditions.js"; + +Hooks.once("ready", function(){ + conditions.onReady(); +}); + Hooks.once("init", () => { + CONFIG.vaesen = vaesen; CONFIG.Combat.initiative = { formula: "1d10", decimals: 0 }; CONFIG.Actor.documentClass = VaesenActor; CONFIG.anonymousSheet = {}; @@ -39,6 +47,18 @@ Hooks.once("init", () => { preloadHandlebarsTemplates(); // Register System Settings registerSystemSettings(); + Token.prototype._drawEffect = async function(src, i, bg, w, tint) { + const multiplier = 3; + const divisor = 3 * this.data.height; + w = (w / 2) * multiplier; + let tex = await loadTexture(src); + let icon = this.effects.addChild(new PIXI.Sprite(tex)); + icon.width = icon.height = w; + icon.y = Math.floor(i / divisor) * w; + icon.x = (i % divisor) * w; + if ( tint ) icon.tint = tint; + this.effects.addChild(icon); + }; }); Hooks.once('diceSoNiceReady', (dice3d) => { @@ -114,3 +134,5 @@ function preloadHandlebarsTemplates() { ]; return loadTemplates(templatePaths); } + + diff --git a/script/sheet/player.js b/script/sheet/player.js index 12650a9..abfcdbf 100644 --- a/script/sheet/player.js +++ b/script/sheet/player.js @@ -1,4 +1,5 @@ import { prepareRollDialog, push, roll } from "../util/roll.js"; +import {conditions} from "../util/conditions.js"; export class PlayerCharacterSheet extends ActorSheet { @@ -43,9 +44,67 @@ export class PlayerCharacterSheet extends ActorSheet { this.setSwag(superData.data); this.computeSkills(superData.data); this.computeItems(superData.data); + //console.log("these are the effects we have: ") + //console.log(this.actor.effects); + //this.affirmConditions(this.actor); return superData; } + + async affirmConditions(actor){ + + + let currentConditions = []; + actor.effects.forEach(function(value, key) { + currentConditions.push(value.data.flags.core?.statusId); + }); + + console.log(currentConditions); + // set state of sheet checks for set conditions + if (currentConditions.indexOf('physical') === -1){ + actor.update({"data.condition.physical.isBroken": false}); + } else { + actor.update({"data.condition.physical.isBroken": true}); + } + if (currentConditions.indexOf('exhausted') === -1){ + actor.update({"data.condition.physical.states.exhausted.isChecked": false}); + } else { + actor.update({"data.condition.physical.states.exhausted.isChecked": true}); + } + if (currentConditions.indexOf('battered') === -1){ + actor.update({"data.condition.physical.states.battered.isChecked": false}); + } else { + actor.update({"data.condition.physical.states.battered.isChecked": true}); + } + if (currentConditions.indexOf('wounded') === -1){ + actor.update({"data.condition.physical.states.wounded.isChecked": false}); + } else { + actor.update({"data.condition.physical.states.wounded.isChecked": true}); + } + if (currentConditions.indexOf('angry') === -1){ + actor.update({"data.condition.mental.states.angry.isChecked": false}); + } else { + actor.update({"data.condition.mental.states.angry.isChecked": true}); + } + if (currentConditions.indexOf('frightened') === -1){ + actor.update({"data.condition.mental.states.frightened.isChecked": false}); + } else { + actor.update({"data.condition.mental.states.frightened.isChecked": true}); + } + if (currentConditions.indexOf('hopeless') === -1){ + actor.update({"data.condition.mental.states.hopeless.isChecked": false}); + } else { + actor.update({"data.condition.mental.states.hopeless.isChecked": true}); + } + if (currentConditions.indexOf('mental') === -1){ + actor.update({"data.condition.mental.isBroken": false}); + } else { + actor.update({"data.condition.mental.isBroken": true}); + } + } + + + activateListeners(html) { super.activateListeners(html); html.find('.item-create').click(ev => { this.onItemCreate(ev); }); @@ -58,9 +117,14 @@ export class PlayerCharacterSheet extends ActorSheet { html.find('.resources b').click(ev => { prepareRollDialog(this, "Resources", 0, 0, this.actor.data.data.resources, 0) }); - + //html.find(".physical .condition").click(conditions.eventsProcessing.onToggleEffect.bind(this)); html.find('.physical .condition').click(ev => { + ev.preventDefault(); const conditionName = $(ev.currentTarget).data("key"); + //let actor = this.actor; + //await actor.toggleStatusEffectByID(conditionName); + + //console.log(conditionName); let conditionValue; if (conditionName === "physical") { conditionValue = this.actor.data.data.condition.physical.isBroken; @@ -77,6 +141,8 @@ export class PlayerCharacterSheet extends ActorSheet { } this._render(); }); + + //html.find(".mental .condition").click(conditions.eventsProcessing.onToggleEffect.bind(this)); html.find('.mental .condition').click(ev => { const conditionName = $(ev.currentTarget).data("key"); let conditionValue; @@ -85,7 +151,7 @@ export class PlayerCharacterSheet extends ActorSheet { this.actor.update({"data.condition.mental.isBroken": !conditionValue}); } else { conditionValue = this.actor.data.data.condition.mental.states[conditionName].isChecked; - if (conditionName === "angry") { + if (conditionName === "angry") { this.actor.update({"data.condition.mental.states.angry.isChecked": !conditionValue}); } else if (conditionName === "frightened") { this.actor.update({"data.condition.mental.states.frightened.isChecked": !conditionValue}); @@ -102,8 +168,10 @@ export class PlayerCharacterSheet extends ActorSheet { const attribute = this.actor.data.data.attribute[attributeName]; const testName = game.i18n.localize(attribute.label + "_ROLL"); let bonus = this.computeBonusFromConditions(attributeName); - prepareRollDialog(this, testName, attribute.value, 0, bonus, 0) + prepareRollDialog(this, testName, attribute.value, 0, bonus, 0, testName, '') }); + + html.find('.skill b').click(ev => { const div = $(ev.currentTarget).parents(".skill"); const skillName = div.data("key"); @@ -112,7 +180,8 @@ export class PlayerCharacterSheet extends ActorSheet { let bonusConditions = this.computeBonusFromConditions(skill.attribute); let bonusArmor = this.computeBonusFromArmor(skillName); const testName = game.i18n.localize(skill.label); - prepareRollDialog(this, testName, attribute.value, skill.value, bonusConditions + bonusArmor, 0) + prepareRollDialog(this, testName, attribute.value, skill.value, bonusConditions + bonusArmor, 0, skill.attribute, testName ) + //prepareRollDialog(this, testName, attribute.value, skill.value, bonusConditions + bonusArmor, 0) }); html.find('.armor .icon').click(ev => { this.onArmorRoll(ev); }); @@ -220,11 +289,11 @@ export class PlayerCharacterSheet extends ActorSheet { if(type==="physical"){ let pool = physique+precision; - roll(this, "Physical Recovery", 0, 0, pool, 0); + prepareRollDialog(this, "Physical Recovery", pool, 0, 0, 0, "Physique + Precision"); } else { let pool = logic+empathy; - roll(this, "Mental Recovery", 0, 0, pool, 0); + prepareRollDialog(this, "Mental Recovery", pool, 0, 0, 0, "Logic + Empathy"); } diff --git a/script/util/conditions.js b/script/util/conditions.js new file mode 100644 index 0000000..3b6247c --- /dev/null +++ b/script/util/conditions.js @@ -0,0 +1,89 @@ + +export class conditions{ + + static EXHAUSTED = "exhausted"; + static BATTERED = "battered"; + static WOUNDED = "wounded"; + static P_BROKEN = "physical"; + static ANGRY = "angry"; + static FRIGHTENED = "frightened"; + static HOPELESS = "hopeless"; + static M_BROKEN = "mental"; + + static conditionPath = "systems/vaesen/asset/status/"; + + static vasenConditions = [ + { + id: "exhausted", + label: "Exhausted", + icon: `${this.conditionPath}oppression.svg` + }, + { + id: "battered", + label: "Battered", + icon: `${this.conditionPath}pummeled.svg` + }, + { + id: "wounded", + label: "Wounded", + icon: `${this.conditionPath}arm-sling.svg` + }, + { + id: "physical", + label: "Physically Broken", + icon: `${this.conditionPath}broken-bone.svg` + }, + { + id: "angry", + label: "Angry", + icon: `${this.conditionPath}revolt.svg` + }, + { + id: "frightened", + label: "Frightened", + icon: `${this.conditionPath}terror.svg` + }, + { + id: "hopeless", + label: "Hopeless", + icon: `${this.conditionPath}despair.svg` + }, + { + id: "mental", + label: "Mentally Broken", + icon: `${this.conditionPath}shattered-heart.svg` + } + ]; + + + static allConditions = conditions.vasenConditions; + + static onReady() { + + CONFIG.vaesen.allConditions = this.allConditions; + CONFIG.statusEffects = conditions.vasenConditions; + } + + + static getStatusEffectBy(id){ + return this.allStatusEffects.find(effect => effect.id === id); + } + +} + +conditions.eventsProcessing = { + "onToggleEffect": async function (event) { + + + event.preventDefault(); + //let element = event.currentTarget; + let actor = this.actor; + let conditionName = $(event.currentTarget).data("key"); + //let effectId = element.dataset.effectId; + console.log(conditionName); + console.log(actor); + + await actor.toggleStatusEffectById(conditionName) + + } +} \ No newline at end of file diff --git a/script/util/roll.js b/script/util/roll.js index dc8d76f..3753ea9 100644 --- a/script/util/roll.js +++ b/script/util/roll.js @@ -1,11 +1,21 @@ -export function prepareRollDialog(sheet, testName, attributeDefault, skillDefault, bonusDefault, damageDefault) { - let attributeHtml = buildInputHtmlDialog("Attribute", attributeDefault); - let skillHtml = buildInputHtmlDialog("Skill", skillDefault); - let bonusHtml = buildInputHtmlDialog("Bonus", bonusDefault); - let damageHtml = buildInputHtmlDialog("Damage", damageDefault); +export function prepareRollDialog(sheet, testName, attributeDefault, skillDefault, bonusDefault, damageDefault, attName = "Attribute", skName = "Skill"){ + let attributeHtml = buildHtmlDialog(attName, attributeDefault, "attribute"); + let skillHtml = buildHtmlDialog(skName, skillDefault, "skill"); + let bonusHtml = buildInputHtmlDialog("Bonus Dice", bonusDefault, "bonus"); + let damageHtml = buildInputHtmlDialog("Damage", damageDefault, "damage"); + + let d = new Dialog({ title: "Test : " + testName, - content: buildDivHtmlDialog(attributeHtml + skillHtml + bonusHtml + damageHtml), + content: buildDivHtmlDialog(` +
+

Test: ` + testName + ` +

+
+ `+ attributeHtml + skillHtml + ` +
Base Dice Pool: `+ (attributeDefault+skillDefault) + `
+ +
` + bonusHtml + damageHtml + `
`), buttons: { roll: { icon: '', @@ -33,10 +43,14 @@ export function prepareRollDialog(sheet, testName, attributeDefault, skillDefaul }, default: "roll", close: () => {} - }); + }, + { width: "230" } + ); d.render(true); + } + export function roll(sheet, testName, attribute, skill, bonus, damage) { sheet.dices = []; sheet.lastTestName = testName; @@ -78,6 +92,7 @@ function sendRollToChat(sheet, isPushed) { sheet.dices.sort(function(a, b){return b - a}); let numberOfSuccess = countSuccess(sheet); let resultMessage; + let damageMessage; if (isPushed) { if (numberOfSuccess > 0) { resultMessage = "" + sheet.lastTestName + " (PUSHED)
"; @@ -92,13 +107,25 @@ function sendRollToChat(sheet, isPushed) { } } let successMessage = " Success: " + numberOfSuccess + "
"; - let damageMessage = " Damage: " + sheet.lastDamage + "
"; let diceMessage = printDices(sheet) + "
"; - let chatData = { + let chatData; + if(sheet.lastDamage > 0){ + damageMessage = " Damage: " + sheet.lastDamage + "
"; + chatData = { user: game.user.id, rollMode: game.settings.get("core", "rollMode"), content: resultMessage + successMessage + damageMessage + diceMessage }; + } else { + chatData = { + user: game.user.id, + rollMode: game.settings.get("core", "rollMode"), + content: resultMessage + successMessage + diceMessage + }; + } + + + if (["gmroll", "blindroll"].includes(chatData.rollMode)) { chatData.whisper = ChatMessage.getWhisperRecipients("GM"); } else if (chatData.rollMode === "selfroll") { @@ -136,10 +163,24 @@ function countSuccess(sheet) { return sheet.dices.filter(dice => dice === 6).length; } -function buildInputHtmlDialog(diceName, diceValue) { - return "" + diceName + ""; +function buildInputHtmlDialog(diceName, diceValue, type) { + return ` +
+

` + diceName + + `:

+
`; + +} + +function buildHtmlDialog(diceName, diceValue, type) { + return ` +
+

` + diceName + + `:

+
`; + } function buildDivHtmlDialog(divContent) { - return "
" + divContent + "
"; + return "
" + divContent + "
"; } \ No newline at end of file diff --git a/style/common.css b/style/common.css index e0ac0f4..2436964 100644 --- a/style/common.css +++ b/style/common.css @@ -1,3 +1,14 @@ +#token-hud .status-effects .effect-control { + height: 40px; + width: 40px; +} + +#token-hud .status-effects { + top: 37%; + width: 160px; + grid-template-columns: 40px 40px 40px 40px; +} + .vaesen .heavy-border { border: 2px solid #412017; margin: 4px; @@ -134,6 +145,10 @@ border-color: #826657; } +.dialog{ + width: fit-content; +} + .dialog .dialog-buttons button, .dialog .dialog-buttons button.default { border: 1px solid #826657; diff --git a/style/dialog/roll.css b/style/dialog/roll.css index 1185072..fb9e9cf 100644 --- a/style/dialog/roll.css +++ b/style/dialog/roll.css @@ -10,9 +10,20 @@ align-items: center; } -.roll-dialog input { - background-color: #d6c9c2; +.roll-fields { + display: flex; + flex-direction: column; + flex-wrap: wrap; + width: 100%; + text-align: center; + align-items: center; +} + + + +.roll-fields input { + background-color: #e7e7e7; border-color: #826657; - flex-basis: 75%; + flex-basis: 35px; margin-bottom: 5px; -} \ No newline at end of file +} diff --git a/system.json b/system.json index fc8e01b..4872050 100644 --- a/system.json +++ b/system.json @@ -3,7 +3,7 @@ "name": "vaesen", "title": "Vaesen", "description": "Nordic horror role-playing", - "version": "2.1.3", + "version": "2.1.4", "minimumCoreVersion": "0.8.6", "compatibleCoreVersion": "0.8.8", "templateVersion": 2,