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 + `
+
+
` +
+ `
` + 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
+
+
` +
+ `
` + 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": {