diff --git a/assets/images/totem-logo.jpg b/assets/images/totem-logo.jpg new file mode 100644 index 0000000..a570275 Binary files /dev/null and b/assets/images/totem-logo.jpg differ diff --git a/module/system/config.mjs b/module/system/config.mjs new file mode 100644 index 0000000..5aebee7 --- /dev/null +++ b/module/system/config.mjs @@ -0,0 +1,66 @@ +export const TOTEM = {}; + +/** + * The set of Ability Scores used within the sytem. + * @type {Object} + */ +/* + + + +TOTEM.nations = { + "istanie1":{ + "label": "Istanie (îles du couchant)", + "cities": ["tanger", "argan", "ar'kobah", "ishandra"] + }, + "istanie2":{ + "label": "Istanie (monts dinariques)", + "cities": ["montenegro", "ishandra"] + }, + "istanie3":{ + "label": "Istanie (anatolie)", + "cities": ["ismyr", "istanbul", "ankara"] + }, + "pentapolie":{ + "label": "Pentapolie", + "cities": ["serone", "éole", "relais de l'affrevie", "relais de bragee", "géode", "théorie", "démos", "négoce", "lucé"] + }, + "venice":{ + "label": "Venice", + "cities": ["venice"] + }, + "rhodesiennes":{ + "label": "Provinces rhodesiennes", + "cities": ["alsyde", "spicule", "urbs", "les syénites"] + }, + "methalune":{ + "label": "Méthalune", + "cities": ["méthalune", "ferraille"] + }, + "gloriana":{ + "label": "Gloriana", + "cities": ["enclosure", "londres", "camelot", "hivernee"] + }, + "antipolie":{ + "label": "Antipolie", + "cities": ["paris", "ithar","candbury","abaya", "relais d'elphiel", "entrelace", "prague", "vienne"] + }, + "olmune":{ + "label": "Principautés d'Olmune", + "cities": ["entrepont", "olmune","arssens","braysine"] + }, + "lansk":{ + "label": "Lansk", + "cities": ["saint-petersbourg", "hypogée","sancre","moscou", "kiev","kryo"] + }, + "nordanie":{ + "label": "Nordanie", + "cities": ["souspente", "gottenborg","solth", "nacre", "dorvik", "mystille"] + }, + "terraincognita":{ + "label": "Terra Incognita", + "cities": ["chantier de transécryme"] + } + +} +*/ \ No newline at end of file diff --git a/module/system/dialogs.mjs b/module/system/dialogs.mjs new file mode 100644 index 0000000..39ab57c --- /dev/null +++ b/module/system/dialogs.mjs @@ -0,0 +1,71 @@ +export class WarningDialog extends Dialog { + + constructor(dialogData) { + let options = { classes: ["warning"] }; + let conf = { + title: "Avertissement", + content: dialogData.content + }; + super(conf, options); + this.dialogData = dialogData; + } + + /* -------------------------------------------- */ + activateListeners(html) { + /*super.activateListeners(html); + this.html = html; + this.setEphemere(this.dialogData.signe.system.ephemere); + html.find(".signe-aleatoire").click(event => this.setSigneAleatoire()); + html.find("[name='signe.system.ephemere']").change((event) => this.setEphemere(event.currentTarget.checked)); + html.find(".signe-xp-sort").change((event) => this.onValeurXpSort(event)); + html.find("input.select-actor").change((event) => this.onSelectActor(event)); + html.find("input.select-tmr").change((event) => this.onSelectTmr(event));*/ + } + + + async onSelectActor(event) { + /*const actorId = this.html.find(event.currentTarget)?.data("actor-id"); + const actor = this.dialogData.actors.find(it => it.id == actorId); + if (actor) { + actor.selected = event.currentTarget.checked; + }*/ + } + + + } + + export class CombatResultDialog extends Dialog { + + constructor(dialogData, options) { + let options = { classes: ["combat", "result"], ...options }; + let conf = { + title: "Résultat de la confrontation", + content: dialogData.content + }; + super(conf, options); + this.dialogData = dialogData; + } + + /* -------------------------------------------- */ + activateListeners(html) { + /*super.activateListeners(html); + this.html = html; + this.setEphemere(this.dialogData.signe.system.ephemere); + html.find(".signe-aleatoire").click(event => this.setSigneAleatoire()); + html.find("[name='signe.system.ephemere']").change((event) => this.setEphemere(event.currentTarget.checked)); + html.find(".signe-xp-sort").change((event) => this.onValeurXpSort(event)); + html.find("input.select-actor").change((event) => this.onSelectActor(event)); + html.find("input.select-tmr").change((event) => this.onSelectTmr(event));*/ + } + + + async onSelectActor(event) { + /*const actorId = this.html.find(event.currentTarget)?.data("actor-id"); + const actor = this.dialogData.actors.find(it => it.id == actorId); + if (actor) { + actor.selected = event.currentTarget.checked; + }*/ + } + + + } \ No newline at end of file diff --git a/module/system/effects.mjs b/module/system/effects.mjs new file mode 100644 index 0000000..8052b39 --- /dev/null +++ b/module/system/effects.mjs @@ -0,0 +1,63 @@ +/** + * Manage Active Effect instances through the Actor Sheet via effect control buttons. + * @param {MouseEvent} event The left-click event on the effect control + * @param {Actor|Item} owner The owning document which manages this effect + */ + export function onManageActiveEffect(event, owner) { + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest("li"); + const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; + switch ( a.dataset.action ) { + case "create": + return owner.createEmbeddedDocuments("ActiveEffect", [{ + label: "New Effect", + icon: "icons/svg/aura.svg", + origin: owner.uuid, + "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, + disabled: li.dataset.effectType === "inactive" + }]); + case "edit": + return effect.sheet.render(true); + case "delete": + return effect.delete(); + case "toggle": + return effect.update({disabled: !effect.disabled}); + } +} + +/** + * Prepare the data structure for Active Effects which are currently applied to an Actor or Item. + * @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for + * @return {object} Data for rendering + */ +export function prepareActiveEffectCategories(effects) { + + // Define effect header categories + const categories = { + temporary: { + type: "temporary", + label: "Temporary Effects", + effects: [] + }, + passive: { + type: "passive", + label: "Passive Effects", + effects: [] + }, + inactive: { + type: "inactive", + label: "Inactive Effects", + effects: [] + } + }; + + // Iterate over active effects, classifying them into categories + for ( let e of effects ) { + e._getSourceName(); // Trigger a lookup for the source name + if ( e.disabled ) categories.inactive.effects.push(e); + else if ( e.isTemporary ) categories.temporary.effects.push(e); + else categories.passive.effects.push(e); + } + return categories; +} \ No newline at end of file diff --git a/module/system/fight.mjs b/module/system/fight.mjs new file mode 100644 index 0000000..6d5b2ef --- /dev/null +++ b/module/system/fight.mjs @@ -0,0 +1,424 @@ +import { TOTEM } from "./config.mjs"; +import { getActorSkillScore,updateActorSkillScore } from "./functions.mjs"; +import { CombatResultDialog } from "./dialogs.mjs"; + +export class TotemFight { + + async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) { + const dicePool = (params.spleen != undefined || params.purpose != undefined) ? '5' : '4'; + const r = new Roll(dicePool +`d6`); + let diceString = ''; + let dicePoolHint = ''; + let discardedRoll = false; + let bonus = 0; + let bonusText = '/+'; + let currentSkillScore = skill; + + + r.roll(); // dice are rolled + if (params.usure != undefined){ + currentSkillScore += params.usure; + bonus += params.usure; + } + + if (params.specialization != undefined){ + currentSkillScore += 2; + } + + if (params.trait != undefined){ + currentSkillScore += params.trait; + bonus += params.trait; + } + + bonusText += bonus; + + let targetText = game.i18n.format('TOTEM.Selected') + ' : ' + game.i18n.format(skillKey) + " " + skill + bonusText; + if (params.specialization != undefined){ + targetText += " (S)"; + } + // tri par ordre croissant + r.terms[0].results.sort((a,b) => a.result - b.result ); + + if (params.purpose != undefined){ + discardedRoll = r.terms[0].results.shift(); + dicePoolHint = ' - ' + game.i18n.format('TOTEM.PurposeTrait'); + } else if (params.spleen != undefined){ + discardedRoll = r.terms[0].results.pop(); + dicePoolHint = ' - ' + game.i18n.format('TOTEM.SpleenTrait'); + } + const discardedRollText = (discardedRoll.result != undefined) ? '
' + discardedRoll.result + '
' : ""; + + for (let i = 0; i < r.terms[0].results.length; i++) { + let result = r.terms[0].results[i].result; + diceString += '
  • ' + result + '
  • '; + } + + let hintText = game.i18n.format('TOTEM.ConfrontationHint'); + + // Build a dynamic html using the variables from above. + const html = ` +
    +
    +
    +
    + ` + dicePool + `d6 ` + dicePoolHint + ` +
    +
    +
    +
    + ` + targetText + ` +
    +
    +
      ` + diceString + `
    +
    ` + discardedRollText + `
    +
    +
    +
    ` + + `

    ` + hintText + `

    + +
    +
    +
    + `; + + // Check if the dice3d module exists (Dice So Nice). If it does, post a roll in that and then + // send to chat after the roll has finished. If not just send to chat. + if (game.dice3d) { + game.dice3d.showForRoll(r).then((displayed) => { + this.sendToChat(html, r, actor); + }); + } else { + this.sendToChat(html, r, actor); + }; + + // on fait les comptes + + } + + async sendToChat(content, roll, actor) { + let conf = { + user: game.user._id, + content: content, + roll: roll, + // sound: 'sounds/dice.wav' + }; + + if (actor) + conf.speaker = ChatMessage.getSpeaker({ actor: actor }); + // Send's Chat Message to foundry, if items are missing they will appear as false or undefined and this not be rendered. + ChatMessage.create(conf).then((msg) => { + return msg; + }); + } + + static instance = null; + + static get() { + if (!TotemFight.instance) + TotemFight.instance = new TotemFight(); + return TotemFight.instance; + } + + + // data injected to char data + static previousValues = { + dicePool: 4, + skills: TOTEM.skillsList, + cskills: TOTEM.cskills, + cephalic: false, + achievementReroll: TOTEM.achievementReroll, + conservationReroll: TOTEM.conservationReroll + }; + + static rollerTemplate = 'systems/totem/templates/fight.html'; + static CombatResultTemplate = 'systems/totem/templates/fight-result.html'; + + static async chatMessageHandler(message, html, data) { + // console.log("accès au fin du fin", message._id); + + // sélection du dé actif + html.on("click", '.confrontation .die.d6', event => { + const diceResult = parseInt($(event.target).html(),10); + html.find('.confrontation .die.d6').removeClass('active'); + $(event.target).addClass('active'); + }); + + // sélection des dés d'accomplissement + html.on("click", '.confrontation .add-to-achievement', event => { + const diceResult = parseInt(html.find('.confrontation .die.d6.active').html(),10); + html.find('.confrontation .die.d6.active').removeClass('min').addClass('max'); + }); + + // sélection des dés de conservation + html.on("click", '.confrontation .add-to-conservation', event => { + const diceResult = parseInt(html.find('.confrontation .die.d6.active').html(),10); + html.find('.confrontation .die.d6.active').removeClass('max').addClass('min'); + }); + + // reset de la sélection des pools + html.on("click", '.confrontation .reset', event => { + html.find('.confrontation .die.d6') + .removeClass('max') + .removeClass('min'); + }); + + // résolution de la confrontation + html.on("click", '.confrontation .resolve', async event => { + let achievementDice = 0; + let conservationDice = 0; + let achievementBasis = 0; + let conservationBasis = 0; + html.find('.confrontation .die.d6.max').each(function(index){ + achievementDice += parseInt($(this).html(), 10); + }); + html.find('.confrontation .die.d6.min').each(function(index){ + conservationDice += parseInt($(this).html(), 10); + }); + + // saisie des résultats + achievementBasis = html.find('td.achievement-result').data('achievement-basis'); + html.find('td.achievement-result').data('achievement-value', achievementDice); + html.find('td.achievement-result').html(achievementBasis + achievementDice); + + conservationBasis = html.find('td.conservation-result').data('conservation-basis'); + html.find('td.conservation-result').data('conservation-value', conservationDice); + html.find('td.conservation-result').html(conservationBasis + conservationDice); + + // calcul des marges + const achievementMargin = achievementBasis + achievementDice - parseInt(html.find('td.adv-achievement-result').html(),10); + const conservationMargin = conservationBasis + conservationDice - parseInt(html.find('td.adv-conservation-result').html(),10); + html.find('td.achievement-margin').html(achievementMargin); + html.find('td.conservation-margin').html(conservationMargin); + + }); + + + // fin de la résolution de la confrontation + + + } + + static async chatListeners(html) { + // supprime le masquage des résultats du dé + html.off("click", ".dice-roll"); + } + + /** + * main class function + * @returns + */ + static async ui(externalData = {}) { + let actor = {}; + + // get the actor + try { + actor = game.user.character; + } catch(e){ + throw("Aucun personnage défini !"); + } + + if (actor == null && externalData.speakerId != undefined && externalData.speakerId != null){ + // on récupère le speakerId, et de là l'objet actor + actor = game.actors.get(externalData.speakerId); + TotemFight.previousValues['speakerName'] = actor.name; + TotemFight.previousValues['speakerImg'] = actor.img; + } else { + TotemFight.previousValues['speakerName'] = "Anonyme"; + } + + // get the data + let charData = (externalData) => { + let o = Object.assign({ _template: TotemFight.rollerTemplate }, {...TotemFight.previousValues, ...externalData}); + return o; + }; + let data = charData(externalData); + console.log(data); + + // render template + let html = await renderTemplate(data._template, data); + + let ui = new Dialog({ + title: game.i18n.localize("TOTEM.FightTool"), + content: html, + buttons: { + roll: { + label: game.i18n.localize('TOTEM.Roll4Fight'), + callback: (html) => { + let form = html.find('#dice-pool-form'); + if (!form[0].checkValidity()) { + throw "Invalid Data"; + } + let enemyAchievement, enemyConservation, skillKey, skill = 5, enemySkill, params = {}; + form.serializeArray().forEach(e => { + switch (e.name) { + case "skill": + case "cephalic": + if (e.value !== ''){ + skillKey = e.value; + } + break; + case "skill-score": + skill = +e.value; + break; + case "specialization": + params.specialization = true; + break; + case "usure": + params.usure = +e.value; + break; + case "trait": + params.trait = +e.value; + break; + case "purpose": + params.purpose = true; + break; + case "spleen": + params.spleen = true; + break; + case "adv-skill": + enemySkill = +e.value; + break; + case "achievement": + enemyAchievement = +e.value; + break; + case "conservation": + enemyConservation = +e.value; + break; + } + }); + // prise en compte de l'usure sur la feuille de perso + if (params.usure != undefined){ + const newSpentScore = getActorSkillScore(actor, skillKey, 'spent') + params.usure; + console.log(newSpentScore); + updateActorSkillScore(actor, skillKey, 'spent', newSpentScore); + } + + return TotemFight.get().performTest(enemyAchievement + enemySkill, enemyConservation + enemySkill, skillKey, skill, params, actor); + } + }, + cancel: { + label: game.i18n.localize('Close'), + callback: () => { } + } + }, + render: function (h) { + h.on("change", 'select[name="skill"]', event => { + const skillLabel = $(event.target).val(); + const currentSkillScore = getActorSkillScore(actor, skillLabel) - getActorSkillScore(actor, skillLabel, 'spent'); + if (parseInt(currentSkillScore,10) >= 0){ + h.find('input#skillScore').val(currentSkillScore); + } + }); + } + }, { width: 601, height: 'fit-content' }); + ui.render(true); + return ui; + } +} + +export class TotemCombat extends Combat { + _encounterCheck(){ + console.log('encounter combat object', this); + } + + async rollInitiative(ids, formula = undefined, messageOptions = {}) { + // console.log(`${game.system.title} | Combat.rollInitiative()`, ids, formula, messageOptions); + // Structure input data + ids = typeof ids === "string" ? [ids] : ids; + + // étape 1 : on vérifie que le combattant est un pj + /*if (ids.length == 1){ + console.log("il n'y a qu'un actor en lice"); + } else { + console.log("il faut prendre le premier pj pour lancer la confrontation"); + }*/ + const combatant = this.combatants.get(ids[0]); + let token = canvas.scene.tokens.get(combatant.tokenId); + combatant.type = game.actors.get( combatant.actorId)?.type; + combatant.disposition = token.disposition; + let enemies = []; + + let adversaries = this.combatants.filter((cbt) => { + let token = canvas.scene.tokens.get(cbt.tokenId); + let enemy = token.actor; + const isEnemy = (token.disposition == -1) ? true : false; + if (isEnemy){ + enemies.push({ + id: enemy.id, + name: enemy.name, + img: enemy.img, + achievement: parseInt(enemy.system.reroll.achievement.value) + 7, + conservation: 7 - parseInt(enemy.system.reroll.conservation.value) + }) + } + return isEnemy; + }); + + let allies = this.combatants.filter((cbt) => { + let token = canvas.scene.tokens.get(cbt.tokenId); + return (token.disposition == 1 && cbt.id != combatant.id) ? true : false; + }); + + if (combatant.type != 'character'){ + let warningDialogHTML = await renderTemplate('systems/totem/templates/dialogs/warning.html', { + warningText: "Seuls les PJs peuvent initier des confrontations. Relancer l'opération au tour du PJ actif." + }); + Dialog.prompt({ + title: "Avertissement", + content: warningDialogHTML, + label: 'Okay !', + callback: () => { + // console.log('Il a compris'); + }, + }); + } else { + // étape 2 : on envoie les infos + let fightingActor = game.actors.get(combatant.actorId); + TotemFight.ui({ + speakerId: combatant.actorId, + speakerWeapons: fightingActor.items.filter(item => item.type == 'weapon'), + speakerExperience:fightingActor.system.attributes.experience.value, + speakerEffects: token.actor.effects, + adversaries: enemies, + allies: allies + }); + } + + } + + nextRound() { + /*let combatants = this.combatants.contents + for (let c of combatants) { + let actor = game.actors.get( c.actorId ) + actor.clearRoundModifiers() + }*/ + super.nextRound(); + } + + /************************************************************************************/ + startCombat() { + /*let combatants = this.combatants.contents + for (let c of combatants) { + let actor = game.actors.get( c.actorId ) + actor.storeVitaliteCombat() + }*/ + + return super.startCombat(); + } + + /************************************************************************************/ + _onDelete() { + /*let combatants = this.combatants.contents + for (let c of combatants) { + let actor = game.actors.get(c.actorId) + actor.clearInitiative() + actor.displayRecuperation() + } + super._onDelete()*/ + } +} \ No newline at end of file diff --git a/module/system/functions.mjs b/module/system/functions.mjs new file mode 100644 index 0000000..6a7c51c --- /dev/null +++ b/module/system/functions.mjs @@ -0,0 +1,109 @@ +import { TOTEM } from './config.mjs' +/** + * renvoie le score d'une compétence d'un actor existant + * @param {TotemActor} + * @return {number||null} Data for rendering or null + */ +export function getActorSkillScore(actor, skillLabel, property = "value") { + let returnedValue = null; + + for(let i in actor.system.skills){ + for(let j in actor.system.skills[i].data){ + if (actor.system.skills[i].data[j].label == skillLabel){ + returnedValue = actor.system.skills[i].data[j][property]; + } + } + } + if (returnedValue == null){ + for(let i in actor.system.cskills.data){ + if (actor.system.cskills.data[i].label == skillLabel){ + returnedValue = actor.system.cskills.data[i][property]; + } + } + } + + return returnedValue; +} + +/** + * renvoie le type d'une compétence + * @param {TotemActor} + * @return {string||null} Data for rendering or null + */ +export function getSkillTypeFromLabel(skillLabel) { + let returnedValue = null; + + for(let i in TOTEM.skills){ + for(let j in TOTEM.skills[i].data){ + if (TOTEM.skills[i].data[j].label == skillLabel){ + returnedValue = j; + } + } + } + + return returnedValue; +} + +/** + * met à jour le score d'une compétence d'un actor existant + * @param {TotemActor} + * @return {boolean} bool + */ +export function updateActorSkillScore(selectedActor, skillLabel, property = "value", updatedValue) { + try { + let updated = false; + // on recherche le label parmi les compétences + for (let st in selectedActor.system.skills){ + for (let s in selectedActor.system.skills[st].data){ + if (selectedActor.system.skills[st].data[s].label == skillLabel){ + selectedActor.system.skills[st].data[s][property] = updatedValue; // printing the new value + const systemSkillKey = `system.skills.${st}.data.${s}.${property}`; + selectedActor.update({[systemSkillKey]:updatedValue }); // updating actor's data + updated = true; + } + } + } + + if (updated == false){ + for (let s in selectedActor.system.cskills.data){ + if (selectedActor.system.cskills.data[s].label == skillLabel){ + selectedActor.system.cskills.data[s][property] = updatedValue; // printing the new value + const systemSkillKey = `system.cskills.data.${s}.${property}`; + selectedActor.update({[systemSkillKey]:updatedValue }); // updating actor's data + updated = true; + } + } + } + + return updated; + } catch(e){ + return false; + } +} + + +/** + * réinitialise toutes les dépenses d'usure + * @param {TotemActor} + * @return {boolean} bool + */ +export function resetActorSkillUsure(selectedActor) { + try { + // on recherche les usures des compétences + for (let st in selectedActor.system.skills){ + for (let s in selectedActor.system.skills[st].data){ + const systemSkillKey = `system.skills.${st}.data.${s}.spent`; + selectedActor.update({[systemSkillKey]:0 }); // updating actor's data + } + } + + // on recherche les usures des compétences céphaliques + for (let s in selectedActor.system.cskills.data){ + const systemSkillKey = `system.cskills.data.${s}.spent`; + selectedActor.update({[systemSkillKey]:0 }); // updating actor's data + } + return true; + } catch(e){ + return false; + } +} diff --git a/module/system/helpers.mjs b/module/system/helpers.mjs new file mode 100644 index 0000000..f3fa509 --- /dev/null +++ b/module/system/helpers.mjs @@ -0,0 +1,25 @@ +export const registerHandlebarsHelpers = function () { + Handlebars.registerHelper('concat', (...args) => args.slice(0, -1).join('')); + Handlebars.registerHelper('lower', e => e.toLocaleLowerCase()); + + Handlebars.registerHelper('toLowerCase', function(str) { + return str.toLowerCase(); + }); + + // Ifis not equal + Handlebars.registerHelper('ifne', function (v1, v2, options) { + if (v1 !== v2) return options.fn(this); + else return options.inverse(this); + }); + + // if equal + Handlebars.registerHelper('ife', function (v1, v2, options) { + if (v1 === v2) return options.fn(this); + else return options.inverse(this); + }); + // if equal + Handlebars.registerHelper('ifgt', function (v1, v2, options) { + if (v1 > v2) return options.fn(this); + else return options.inverse(this); + }); +} \ No newline at end of file diff --git a/module/system/hooks.mjs b/module/system/hooks.mjs new file mode 100644 index 0000000..28292f4 --- /dev/null +++ b/module/system/hooks.mjs @@ -0,0 +1,105 @@ +import { TotemFight } from './fight.mjs'; + +export const registerHooks = function () { + /** + * Ready hook loads tables, and override's foundry's entity link functions to provide extension to pseudo entities + */ + + Hooks.once("ready", async () => { + console.info("Totem | System Initialized."); + }); + + // changement de la pause + Hooks.on("renderPause", async function () { + if ($("#pause").attr("class") !== "paused") return; + $(".paused img").attr("src", 'systems/totem/images/pause.webp'); + $(".paused img").css({ "opacity": 1}); + $("#pause.paused").css({ "display": "flex", "justify-content": "center" }); + $("#pause.paused figcaption").css({ "width": `256px`, "height": `256px` }); + $("#pause.paused figcaption").text(game.i18n.localize("TOTEM.PausedText")); + }); + + /*Hooks.on("renderPause", ((_app, html) => { + html.find("img").attr("src", "systems/bol/ui/pause2.webp") + })) + + Hooks.on('renderChatLog', (log, html, data) => BoLUtility.chatListeners(html)) + Hooks.on('renderChatMessage', (message, html, data) => BoLUtility.chatMessageHandler(message, html, data)) + */ + console.log("rendering hooks"); + Hooks.on('renderChatLog', (log, html, data) => TotemFight.chatListeners(html)); + Hooks.on('renderChatMessage', (message, html, data) => TotemFight.chatMessageHandler(message, html, data)); + + /** + * Create a macro when dropping an entity on the hotbar + * Item - open roll dialog for item + * Actor - open actor sheet + * Journal - open journal sheet + */ + Hooks.on("hotbarDrop", async (bar, data, slot) => { + // console.log(data.type); + // Create item macro if rollable item - weapon, spell, prayer, trait, or skill + + return false; + }); + + Hooks.on('getSceneControlButtons', (controls) => { + controls.find((c) => c.name === 'token').tools.push({ + name: 'Dice Roller', + title: game.i18n.localize("TOTEM.RollTool"), + icon: 'fas fa-dice-d6', + button: true, + onClick() { + TotemRoll.ui(); + } + }); + }); + + /* -------------------------------------------- */ + /* PreCreate Hooks */ + /* -------------------------------------------- */ + + Hooks.on("preCreateActor", function (actor) { + console.log('pre create actor', actor); + if (actor.img == "icons/svg/mystery-man.svg") { + // actor.updateSource({"img": `systems/totem/icons/actors/${actor.type}.webp`}); + // item.updateSource({"img": `systems/totem/icons/competence.webp`}); + } + }); + + Hooks.on("preCreateItem", function (item) { + if (item.img == "icons/svg/item-bag.svg") { + item.updateSource({"img": `systems/totem/icons/items/${item.type}.webp`}); + // item.updateSource({"img": `systems/totem/icons/competence.webp`}); + } + }); + + /* -------------------------------------------- */ + /* Combat Hooks */ + /* -------------------------------------------- */ + + /* + Hooks.on("createCombatant", function (combatant) { + if (game.user.isGM) { + let actor = combatant.actor; + + console.log('create combatant', actor); + } + });*/ + + Hooks.on("updateCombat", function () { + if (game.user.isGM) { + let combatant = (game.combat.combatant) ? game.combat.combatant.actor : ""; + + console.log('update combat', game.combat); + + /*if (combatant.type == "marker" && combatant.system.settings.general.isCounter == true) { + let step = (!combatant.system.settings.general.counting) ? -1 : combatant.system.settings.general.counting; + let newQuantity = combatant.system.pools.quantity.value + step; + combatant.update({"system.pools.quantity.value": newQuantity}); + }*/ + } + }); + + +} diff --git a/module/system/roll.js b/module/system/roll.js new file mode 100644 index 0000000..cd8e706 --- /dev/null +++ b/module/system/roll.js @@ -0,0 +1,284 @@ +import { getActorSkillScore, updateActorSkillScore } from "./functions.mjs"; + +export class TotemRoll { + async performTest(dicePool, target, trait, usingSpecialization, difficulty, skill, params, actor) { + const r = new Roll(dicePool + 'd6'); + r.roll(); + let _trait = trait || 0; + let _usingSpecialization = usingSpecialization || 0; + let _skillLabel = (params.skill != undefined) ? game.i18n.format(params.skill) : ""; + let _used = (params.usure != undefined) ? params.usure : 0; + let diceString = ''; + let total = 0; + + // affichage des valeurs + let targetText = _skillLabel + ' : ' + skill + ' (+'+ _used +')'; + if (trait) + targetText += ', '+ game.i18n.format('TOTEM.Traits') + ' : ' + _trait; + if (_usingSpecialization != 0) + targetText += ', '+ game.i18n.format('TOTEM.UsingSpecialization'); + if (difficulty) + targetText += '
    '+ game.i18n.format('TOTEM.Against') +': ' + Math.abs(difficulty); + + // affichage des jets + for (let i = 0; i < dicePool; i++) { + let result = r.terms[0].results[i].result; + if (result == 6) { + diceString += '
  • ' + result + '
  • '; + } + else if (result <= 5) { + diceString += '
  • ' + result + '
  • '; + } + else if (result >= 1) { + diceString += '
  • ' + result + '
  • '; + } + total += result; + } + + // Here we want to check if the success was exactly one (as "1 Successes" doesn't make grammatical sense). + // We create a string for the Successes. + let successText = ''; + let successMargin = 0; + + successMargin = total + skill + _trait + _usingSpecialization + _used + difficulty; + if (params.usure != undefined){ + successMargin += parseInt(params.usure,10); + } + + // console.log(total, skill, _trait, _usingSpecialization, difficulty, successMargin); + // règle de la MR qui ne peut pas dépasser la compétence + if (successMargin > skill){ + successMargin = skill; + } + + if (successMargin > target + 1) { + successText = game.i18n.localize('TOTEM.RollSuccess') + ' (' + game.i18n.localize('TOTEM.SM') + ' ' + successMargin.toString() +')'; + } else if (successMargin == target + 1) { + successText = game.i18n.localize('TOTEM.RollSuccess'); + } else if (successMargin == target) { + successText = game.i18n.localize('TOTEM.PartialSuccess'); + } else { + successText = game.i18n.localize('TOTEM.Failure'); + } + + + // Build a dynamic html using the variables from above. + const html = ` +
    +
    +
    +
    + ` + dicePool + `d6 +
    +
    +
    +
    +
    + ` + targetText + ` +
    +
    +
      ` + diceString + `
    +
    +
    +
    + Résultat +

    ` + (total + skill + _trait + _usingSpecialization + _used).toString() + `

    +
    +
    +
    ` + + `

    ` + successText + `

    +
    +
    +
    + `; + + // Check if the dice3d module exists (Dice So Nice). If it does, post a roll in that and then + // send to chat after the roll has finished. If not just send to chat. + if (game.dice3d) { + game.dice3d.showForRoll(r).then((displayed) => { + this.sendToChat(html, r, actor); + }); + } else { + this.sendToChat(html, r, actor); + }; + } + + async sendToChat(content, roll, actor) { + let conf = { + user: game.user._id, + content: content, + roll: roll, + sound: 'sounds/dice.wav' + }; + if (actor) + conf.speaker = ChatMessage.getSpeaker({ actor: actor }); + // Send's Chat Message to foundry, if items are missing they will appear as false or undefined and this not be rendered. + ChatMessage.create(conf).then((msg) => { + return msg; + }); + } + + static instance = null; + + static get() { + if (!TotemRoll.instance) + TotemRoll.instance = new TotemRoll(); + return TotemRoll.instance; + } + + // Parse XdYtZfAc || XdYsZfAc + // {size of dice pool}d{target number}(t|s)[{skill level - for trait}f][{complication range}c][D] + async parse(cmd, usingSpecialization) { + let actor = game.user.character; + if (canvas.tokens.controlled.length > 0) + actor = canvas.tokens.controlled[0].actor; + let r = cmd.match(/([2-5])d([01]?[0-9])[ts](([4-8])f)?((20|[1][5-9])c)?(D)?/); + if (r) { + //console.log(r); + let dicePool = +r[1]; + let target = +r[2]; + let trait = +r[4]; + if (!!r[7]) usingSpecialization = true; + let difficulty = +r[6]; + this.performTest(dicePool, target, trait, usingSpecialization, difficulty, actor); + } else + ui.notifications.error("Unparsable command: " + cmd); + } + + // data injected to char data + static previousValues = { + dicePool: 2 + }; + + static rollerTemplate = 'systems/totem/templates/roll.html'; + + /** + * main class function + * @returns + */ + static async ui(externalData = {}) { + let charData = (externalData) => { + return Object.assign({ _template: TotemRoll.rollerTemplate }, {...TotemRoll.previousValues, ...externalData}); + }; + + // get the actor + let actor = null; + + try { + let actor = game.user.character; + + if (canvas.tokens.controlled.length > 0) + actor = canvas.tokens.controlled[0].actor; + } catch (e) { + console.log(e); + } + + if (actor == null && externalData.speakerId != undefined && externalData.speakerId != null){ + // on récupère le speakerId, et de là l'objet actor + actor = game.actors.get(externalData.speakerId); + } + + // get the data + let data = charData(externalData); + console.log('npc2', data); + + if (actor.type != undefined){ + data.actor_type = actor.type; + if (actor.type == 'character'){ + data.skillMaxScore = getActorSkillScore(actor, data.skill); + data.skillScore = data.skillMaxScore - getActorSkillScore(actor, data.skill, 'spent'); + data.skillSpent = getActorSkillScore(actor, data.skill, 'spent'); + } else if(actor.type == 'npc'){ + + if (data.specialization == 1){ + //data.skillMaxScore = getActorSkillScore(actor, data.skill); + // data.skillScore = data.skillMaxScore; + } else { + // compétence, il faut récupérer le score du skill type + data.skillScore = data.value; + } + } + } + + // render template + let html = await renderTemplate(data._template, data); + + let ui = new Dialog({ + title: game.i18n.localize("TOTEM.RollTool"), + content: html, + buttons: { + roll: { + label: game.i18n.localize('TOTEM.RollDice'), + callback: (html) => { + let form = html.find('#dice-pool-form'); + if (!form[0].checkValidity()) { + throw "Invalid Data"; + } + let target = 0, trait, usingSpecialization, difficulty, skill = 0, params = {}; + form.serializeArray().forEach(e => { + switch (e.name) { + case "difficulty": + if (e.value != "") + difficulty = -e.value; + break; + case "skillLabel": + params.skill = e.value; + break; + case "usure": + params.usure = +e.value; + break; + case "skill": + skill = +e.value; + break; + case "trait": + trait = +e.value; + break; + case "usingSpecialization": + if (e.value && +e.value > 1) + usingSpecialization = +e.value; + break; + } + + // prise en compte de l'usure sur la feuille de perso + if (params.usure != undefined){ + updateActorSkillScore(actor, data.skill, 'spent', data.skillSpent + parseInt(params.usure,10)); + } + }); + return TotemRoll.get().performTest(data.dicePool, target, trait, usingSpecialization, difficulty, skill, params, actor); + } + }, + close: { + label: game.i18n.localize('Close'), + callback: () => { } + } + }, + render: function (h) { + h.find("#skills-radio input").change(function () { + let s = $(this).attr("data-skill"); + h.find(".trait-list .hidden").removeClass("show"); + let f = h.find(".trait-list ." + s); + f.addClass("show"); + if (f.length == 0) { + h.find(".use-trait input").attr("disabled", "disabled").prop("checked", false); + } else + h.find(".use-trait input").attr("disabled", null); + }); + } + }); + ui.render(true); + return ui; + } +} + + +Hooks.on("chatCommandsReady", function (chatCommands) { + chatCommands.registerCommand(chatCommands.createCommandFromData({ + commandKey: "/dr", + invokeOnCommand: (chatlog, messageText, chatdata) => { + TotemRoll.get().parse(messageText); + }, + shouldDisplayToChat: false, + iconClass: "fa-dice-d6", + description: "Roll Totem check" + })); +}); diff --git a/module/system/settings.mjs b/module/system/settings.mjs new file mode 100644 index 0000000..ca9dc36 --- /dev/null +++ b/module/system/settings.mjs @@ -0,0 +1,29 @@ +export const registerSettings = function () { + game.settings.register("totem", "game-level", { + name: game.i18n.localize("TOTEM.WorldSettings.GameLevel.Name"), + hint: game.i18n.localize("TOTEM.WorldSettings.GameLevel.Hint"), + scope: "system", + config: true, + type: String, + choices: { + "e": "Totem", + "c": "Céphale", + "b": "Bohème", + "a": "Amertume" + }, + default: 'e', + onChange: value => { + console.log(value); + } + }); + + game.settings.register("totem", "granting_cephalie", { + name: game.i18n.localize("TOTEM.WorldSettings.GrantingCephales.Label"), + hint: game.i18n.localize("TOTEM.WorldSettings.GrantingCephales.Description"), + scope: "system", + config: true, + type: Boolean, + default: !1 + }) + +} \ No newline at end of file diff --git a/module/system/templates.mjs b/module/system/templates.mjs new file mode 100644 index 0000000..9cf5964 --- /dev/null +++ b/module/system/templates.mjs @@ -0,0 +1,17 @@ +/** + * Define a set of template paths to pre-load + * Pre-loaded templates are compiled and cached for fast access when rendering + * @return {Promise} + */ + export const preloadHandlebarsTemplates = async function() { + return loadTemplates([ + + // Actor partials. + "systems/totem/templates/actor/parts/actor-traits.html", + "systems/totem/templates/actor/parts/actor-background.html", + "systems/totem/templates/actor/parts/actor-skills.html", + "systems/totem/templates/actor/parts/actor-items.html", + "systems/totem/templates/actor/parts/actor-cephalie.html", + "systems/totem/templates/actor/parts/actor-effects.html", + ]); +}; diff --git a/template.json b/template.json index d4d3b7e..ee318dd 100644 --- a/template.json +++ b/template.json @@ -1,19 +1,20 @@ { "Actor": { - "types": ["character", "npc"], + "types": ["character", "npc", "creature"], "templates": { "base": { - "health": { - "value": 10, + "minorWound": { + "value": 0, "min": 0, - "max": 10 + "max": 4 }, - "power": { - "value": 5, + "majorWound": { + "value": 0, "min": 0, - "max": 5 + "max": 2 }, - "biography": "" + "totem":0, + "activity": "" } }, "character": { @@ -23,30 +24,378 @@ "value": 1 } }, - "abilities": { - "str": { - "value": 10 + "AbilityCategories": { + "physical": { + "label":"TOTEM.abilityCategory.physical" }, - "dex": { - "value": 10 + "manual": { + "label":"TOTEM.abilityCategory.manual" }, - "con": { - "value": 10 + "mental": { + "label":"TOTEM.abilityCategory.mental" }, - "int": { - "value": 10 - }, - "wis": { - "value": 10 - }, - "cha": { - "value": 10 + "social": { + "label":"TOTEM.abilityCategory.social" } + }, + "abilities": { + "vig": { + "label":"TOTEM.abilities.vigor", + "value": 0, + "min": 0, + "max": 5, + "category": "physical" + }, + "vie": { + "label":"TOTEM.abilities.health", + "value": 0, + "min": 0, + "max": 5, + "category": "physical" + }, + "pre": { + "label":"TOTEM.abilities.precision", + "value": 0, + "min": 0, + "max": 5, + "category": "manual" + }, + "ref": { + "label":"TOTEM.abilities.reflexes", + "value": 0, + "min": 0, + "max": 5, + "category": "manual" + }, + "sav": { + "label":"TOTEM.abilities.knowledge", + "value": 0, + "min": 0, + "max": 5, + "category": "mental" + }, + "per": { + "label":"TOTEM.abilities.perception", + "value": 0, + "min": 0, + "max": 5, + "category": "mental" + }, + "vol": { + "label":"TOTEM.abilities.will", + "value": 0, + "min": 0, + "max": 5, + "category": "social" + }, + "emp": { + "label":"TOTEM.abilities.empathy", + "value": 0, + "min": 0, + "max": 5, + "category": "social" + } + }, + "skillCategories": { + "man": { + "label":"TOTEM.skillCategory.man" + }, + "animal": { + "label":"TOTEM.skillCategory.animal" + }, + "machine": { + "label":"TOTEM.skillCategory.machine" + }, + "weapon": { + "label":"TOTEM.skillCategory.weapon" + }, + "survival": { + "label":"TOTEM.skillCategory.survival" + }, + "earth": { + "label":"TOTEM.skillCategory.earth" + } + }, + "skills": { + "arts": { + "label":"TOTEM.skills.arts", + "value": 0, + "min": 0, + "max": 5, + "category": "man", + "rarity":1 + }, + "civilization": { + "label":"TOTEM.skills.civilization", + "value": 0, + "min": 0, + "max": 5, + "category": "man", + "rarity":2 + }, + "psychology": { + "label":"TOTEM.skills.psychology", + "value": 0, + "min": 0, + "max": 5, + "category": "man", + "rarity":1 + }, + "rumors": { + "label":"TOTEM.skills.rumors", + "value": 0, + "min": 0, + "max": 5, + "category": "man", + "rarity":0 + }, + "healing": { + "label":"TOTEM.skills.healing", + "value": 0, + "min": 0, + "max": 5, + "category": "man", + "rarity":1 + }, + "animalism": { + "label":"TOTEM.skills.animalism", + "value": 0, + "min": 0, + "max": 5, + "category": "animal", + "rarity":1 + }, + "dissection": { + "label":"TOTEM.skills.dissection", + "value": 0, + "min": 0, + "max": 5, + "category": "animal", + "rarity":2 + }, + "wildlife": { + "label":"TOTEM.skills.wildlife", + "value": 0, + "min": 0, + "max": 5, + "category": "animal", + "rarity":1 + }, + "repulsion": { + "label":"TOTEM.skills.repulsion", + "value": 0, + "min": 0, + "max": 5, + "category": "animal", + "rarity":0 + }, + "tracks": { + "label":"TOTEM.skills.tracks", + "value": 0, + "min": 0, + "max": 5, + "category": "animal", + "rarity":0 + }, + "crafting": { + "label":"TOTEM.skills.crafting", + "value": 0, + "min": 0, + "max": 5, + "category": "machine", + "rarity":2 + }, + "diy": { + "label":"TOTEM.skills.diy", + "value": 0, + "min": 0, + "max": 5, + "category": "machine", + "rarity":0 + }, + "mecanical": { + "label":"TOTEM.skills.mecanical", + "value": 0, + "min": 0, + "max": 5, + "category": "machine", + "rarity":2 + }, + "driving": { + "label":"TOTEM.skills.driving", + "value": 0, + "min": 0, + "max": 5, + "category": "machine", + "rarity":1 + }, + "technology": { + "label":"TOTEM.skills.technology", + "value": 0, + "min": 0, + "max": 5, + "category": "machine", + "rarity":2 + }, + "firearms": { + "label":"TOTEM.skills.firearms", + "value": 0, + "min": 0, + "max": 5, + "category": "weapon", + "rarity":2 + }, + "archery": { + "label":"TOTEM.skills.archery", + "value": 0, + "min": 0, + "max": 5, + "category": "weapon", + "rarity":0 + }, + "armory": { + "label":"TOTEM.skills.armory", + "value": 0, + "min": 0, + "max": 5, + "category": "weapon", + "rarity":2 + }, + "throwing": { + "label":"TOTEM.skills.throwing", + "value": 0, + "min": 0, + "max": 5, + "category": "weapon", + "rarity":0 + }, + "melee": { + "label":"TOTEM.skills.melee", + "value": 0, + "min": 0, + "max": 5, + "category": "weapon", + "rarity":0 + }, + "feed": { + "label":"TOTEM.skills.feed", + "value": 0, + "min": 0, + "max": 5, + "category": "survival", + "rarity":0 + }, + "atletics": { + "label":"TOTEM.skills.atletics", + "value": 0, + "min": 0, + "max": 5, + "category": "survival", + "rarity":0 + }, + "brawling": { + "label":"TOTEM.skills.brawling", + "value": 0, + "min": 0, + "max": 5, + "category": "survival", + "rarity":0 + }, + "stealth": { + "label":"TOTEM.skills.stealth", + "value": 0, + "min": 0, + "max": 5, + "category": "survival", + "rarity":0 + }, + "alertness": { + "label":"TOTEM.skills.alertness", + "value": 0, + "min": 0, + "max": 5, + "category": "survival", + "rarity":0 + }, + "environment": { + "label":"TOTEM.skills.environment", + "value": 0, + "min": 0, + "max": 5, + "category": "earth", + "rarity":0 + }, + "flora": { + "label":"TOTEM.skills.flora", + "value": 0, + "min": 0, + "max": 5, + "category": "earth", + "rarity":0 + }, + "road": { + "label":"TOTEM.skills.road", + "value": 0, + "min": 0, + "max": 5, + "category": "earth", + "rarity":0 + }, + "toxics": { + "label":"TOTEM.skills.toxics", + "value": 0, + "min": 0, + "max": 5, + "category": "earth", + "rarity":0 + }, + "remains": { + "label":"TOTEM.skills.remains", + "value": 0, + "min": 0, + "max": 5, + "category": "earth", + "rarity":0 + } } }, "npc": { - "templates": ["base"], - "cr": 0 + "templates": ["base"], + "age": 15, + "threat": { + "value": 1, + "min": 1, + "max": 4 + }, + "experience": { + "value": 1, + "min": 1, + "max": 4 + }, + "role": { + "value": 1, + "min": 1, + "max": 4 + } + }, + "creature": { + "templates": ["base"], + "age": 15, + "template": { + "value": 1, + "min": 1, + "max": 4 + }, + "size": { + "value": 1, + "min": 1, + "max": 4 + }, + "role": { + "value": 1, + "min": 1, + "max": 4 + } } }, "Item": {