diff --git a/css/mosh.css b/css/mosh.css index e827cb3..56a0dbb 100644 --- a/css/mosh.css +++ b/css/mosh.css @@ -1702,4 +1702,36 @@ p { .mosh .items-tab-grid{ grid-template-columns: 50% 50%; +} +.mosh form.actor-generator div.fulllabel { + border-radius: 2em; +} +.mosh form.actor-generator div.ulinputfield { + min-height: 150px; +} +.mosh form.actor-generator .smalltext { + font-size: 0.7rem; +} +.mosh form.actor-generator div#generator-header-grid-1col { + grid-column: span 1 / span 1; + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.mosh form.actor-generator .element-group { + margin-top: 10px; +} + +.mosh form.actor-generator .credit-img{ + height: 30px; + margin-top: 0%; +} + +.mosh form.actor-generator .checkbox-round { + appearance: none; +} +.mosh form.actor-generator .checkbox-round:checked { + background-color: gray; +} +.mosh form.actor-generator .clicable-item{ + cursor: pointer; } \ No newline at end of file diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index 8286c35..c3a6679 100644 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -1,3 +1,4 @@ +import { DLActorGenerator } from "../windows/actor-generator.js"; /** * Extend the basic ActorSheet with some very simple modifications * @extends {ActorSheet} @@ -633,6 +634,28 @@ export class MothershipActorSheet extends ActorSheet { }); } - - + /** + * Extend and override the sheet header buttons + * @override + */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + const canConfigure = game.user.isGM || this.actor.isOwner; + if (this.options.editable && canConfigure) { + buttons = [{ + label: 'Character Generator', + class: 'configure-actor', + icon: 'fas fa-cogs', + onclick: (ev) => this._onConfigureCreature(ev), + },].concat(buttons); + } + return buttons; + } + _onConfigureCreature(event) { + event.preventDefault(); + new DLActorGenerator(this.actor, { + top: this.position.top + 40, + left: this.position.left + (this.position.width - 400) / 2 + }).render(true); + } } diff --git a/module/actor/actor.js b/module/actor/actor.js index 969f368..5740ada 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -3192,6 +3192,11 @@ export class MothershipActor extends Actor { await this.createEmbeddedDocuments('Item', [itemData]); //create message text flavorText = `You add this to your inventory.`; + } else if (itemData.type === 'skill' ) { + //add item to the players inventory + await this.createEmbeddedDocuments('Item', [itemData]); + //create message text + flavorText = `You learn this skill.`; } } //generate chat message diff --git a/module/windows/actor-generator.js b/module/windows/actor-generator.js new file mode 100644 index 0000000..7c37418 --- /dev/null +++ b/module/windows/actor-generator.js @@ -0,0 +1,431 @@ +export class DLActorGenerator extends FormApplication { + static get defaultOptions() { + const options = super.defaultOptions; + options.id = 'sheet-modifiers'; + options.classes = ["mosh", "sheet", "actor", "character"]; + options.template = 'systems/mosh/templates/dialogs/actor-generator-dialog.html'; + options.width = 800; + options.dragDrop = [{dragSelector: null, dropSelector: ".dropitem"}]; + return options; + } + + /* -------------------------------------------- */ + /** + * Add the Entity name into the window title + * @type {String} + */ + get title() { + return `${this.object.name}: Character Generator`; + } + /* -------------------------------------------- */ + + /** + * Construct and return the data object used to render the HTML template for this form application. + * @return {Object} + */ + getData() { + //if (this.object.system.class.uuid != null){ + // this.updateClass(this.object.system.class.uuid); + //} + let data=this.object; + data.class_options = game.items.filter(i => i.type=="class"); + data.system.class.value = ""; + return data; + } + /* -------------------------------------------- */ + + async rollDices(dices, html, id,chatmsg="") { + + if (html.find(`img[id="` + id + `"]`).prop('hidden') == false) { + let roll = await new Roll(dices).roll(); + if (chatmsg != ""){ + await roll.toMessage({flavor: chatmsg}); + } + //console.log(id + "->" + roll.total) + html.find(`img[id="` + id + `"]`).prop('hidden', true); + html.find(`input[id="` + id + `"]`).prop('hidden', false); + html.find(`input[id="` + id + `"]`).prop('value', roll.total); + } + } + + + /* todo: remove this functions and set the values as configuration */ + async rollStrength(html) { + this.rollDices("2d10+25", html, `system.stats.strength.value`,`Rolling for Strength stat`) + } + + async rollSpeed(html) { + this.rollDices("2d10+25", html, `system.stats.speed.value`,`Rolling for Speed stat`) + } + async rollIntellect(html) { + this.rollDices("2d10+25", html, `system.stats.intellect.value`,`Rolling for Intellect stat`) + } + async rollCombat(html) { + this.rollDices("2d10+25", html, `system.stats.combat.value`,`Rolling for Combat stat`) + } + async rollHealth(html) { + this.rollDices("1d10+10", html, `system.health.max`,`Rolling for Health`) + } + async rollSanity(html) { + this.rollDices("2d10+10", html, `system.stats.sanity.value`,`Rolling for Sanity save`) + } + async rollFear(html) { + this.rollDices("2d10+10", html, `system.stats.fear.value`,`Rolling for Fear save`) + } + async rollBody(html) { + this.rollDices("2d10+10", html, `system.stats.body.value`,`Rolling for Body save`) + } + async rollCredits(html) { + this.rollDices("2d10*10", html, `system.credits.value`,`Rolling for Credits`) + } + + async rollTable(html,id,tableId,type = "input"){ + + if (html.find(`img[id="` + id + `.value"]`).prop('hidden') == false && html.find(`img[id="system.class.value"]`).prop('value')!="" ) { + + let table = await fromUuid(tableId); + let tableResult = await table.draw({displayChat: true}); + + let tableRoll = tableResult.results[0].range[0]; + + let resultText = ""; + let resultUuid = []; + for (var i = 0; i < tableResult.results.length; i++) { + if (tableResult.results[i].type == "pack"){ + if(type=="ul"){ + this._element.find(`ul[id="${id}.text"]`).append(`
  • ${tableResult.results[i].text}
  • `); + } else{ + resultText += tableResult.results[i].text+"; "; + } + resultUuid.push( tableResult.results[i].documentId); + }else if (tableResult.results[i].type == "text"){ + let tableTextmatch = tableResult.results[i].text.match(/(.*)(@UUID.*)/i); + if(type=="ul"){ + this._element.find(`ul[id="${id}.text"]`).append(`
  • ${await TextEditor.enrichHTML(tableTextmatch[2].replace(/(\)+/i,""), {async: true})}
  • `); + } + else{ + resultText +=tableTextmatch[1].replace(/(\)+/i,""); + } + /**we need to keep only the id of the item, not the complete uuid string (for now) */ + resultUuid.push(tableTextmatch[2].replace(/(])+(.*)/i,"").replace(/(.*)(\.)/i,"")); + } + } + + html.find(`img[id="` + id + `.value"]`).prop('hidden', true); + html.find(`input[id="` + id + `.value"]`).prop('hidden', false); + html.find(`input[id="` + id + `.value"]`).prop('value', tableRoll); + html.find(`input[id="` + id + `.text"]`).prop('hidden', false); + html.find(`input[id="` + id + `.text"]`).prop('value', resultText); + html.find(`input[id="` + id + `.uuid"]`).prop('value', resultUuid); + + } + + } + + + async rollPatch(html){ + if (this.patchTable == null){ + this.patchTable = "Compendium.fvtt_mosh_1e_psg.rolltables_1e.RollTable.Vry1q39PNXR3X4oY"; + } + let id = "system.class.patch"; + await this.rollTable(html,id,this.patchTable); + + } + async rollTrinket(html){ + if (this.trinketTable == null){ + this.trinketTable = "Compendium.fvtt_mosh_1e_psg.rolltables_1e.RollTable.3iJxbTylcgZ0BRaQ"; + } + let id = "system.class.trinket"; + await this.rollTable(html,id,this.trinketTable); + + } + async rollLoadout(html){ + if (this.loadoutTable == null){ + ui.notifications.error("You need a class with defined loadout table."); + return; + } + let id = "system.class.loadout"; + await this.rollTable(html,id,this.loadoutTable,"ul"); + + } + + async rollEverything(html) { + await this.rollStrength(html); + await this.rollSpeed(html); + await this.rollIntellect(html); + await this.rollCombat(html); + await this.rollHealth(html); + await this.rollFear(html); + await this.rollSanity(html); + await this.rollBody(html); + await this.rollCredits(html); + await this.rollPatch(html); + await this.rollTrinket(html); + await this.rollLoadout(html); + } + + + async updateClass(classUuid){ + + const droppedObject = await fromUuid(classUuid); + if (droppedObject.type != "class"){ + return; + } + this._element.find(`input[id="system.class.value"]`).prop("value", droppedObject.name); + + const prev_uuid = this._element.find(`input[id="system.class.uuid"]`).prop("value"); + if(prev_uuid != null){ + //we already had a class, so we remove previously applied bonuses and skill + + //remove previous bonuses + this._element.find(`input[name$="bonus"]`).prop("value",null) + + //posible-todo: un-do the rolls of patch, trinket and loadout? + } + + this._element.find(`input[id="system.class.uuid"]`).prop("value", classUuid); + this._element.find(`input[id="system.class.traumaresponse"]`).prop("value", droppedObject.system.trauma); + this.trinketTable = droppedObject.system.TrinketTable; + this.patchTable = droppedObject.system.PatchTable; + this.loadoutTable = droppedObject.system.loadoutTable; + + try{ + let skills = JSON.parse(droppedObject.system.skills.replaceAll("

    ","").replaceAll("

    ","").replaceAll("
    ","").replaceAll("
    ","").replaceAll(" ","")); + let skillsUuid = []; + + this._element.find(`ul[id="system.class.skils.text"]`).empty(); + if(skills.hasOwnProperty("fixed")){ + for (let i = 0;i${await TextEditor.enrichHTML(skillUuid, {async: true})}`; + this._element.find(`ul[id="system.class.skils.text"]`).append(li_html); + } + } + if(skills.hasOwnProperty("options")){ + for (let k in skills["options"]){ + //todo: instead of a reminder, render a selection pop-up loaded with all skils available found (compendium and items) + let li_html = skills["options"][k].join(` ${k} `); + this._element.find(`ul[id="system.class.skils.text"]`).append(`
  • ${li_html}
  • `); + } + } + this._element.find(`input[id="system.class.skills.uuid"]`).prop("value", skillsUuid); + }catch{ + ui.notifications.error("Class has invalid skills configuration."); + } + + try{ + let statsandsaves = JSON.parse(droppedObject.system.statsandsaves.replaceAll("

    ","").replaceAll("

    ","").replaceAll("
    ","").replaceAll("
    ","").replaceAll(" ","")); + + for(let i = 0; i < statsandsaves.length; i++) { + let obj = statsandsaves[i]; + + if (obj.options.length == 1){ + //only 1 option, then its forced + this._element.find(`input[name="${obj.options[0]}"]`).prop("value",obj.value); + }else{ + + let buttons_options = {}; + for(let j = 0; j < obj.options.length; j++) { + buttons_options[j] = { + icon: '', + label: obj.options[j].replace(/\.bonus/i,"").replace(/(.*)\.+/i,""), + callback: () => this._element.find(`input[name="${obj.options[j]}"]`).prop("value",obj.value) + }; + } + let d = new Dialog({ + title: "Class Attribute and Saves option", + content: `

    You must choose where to apply the bonus of (${obj.value})

    `, + buttons: buttons_options, + default: "1", + //render: html => console.log("Register interactivity in the rendered dialog"), + //close: html => console.log("This always is logged no matter which option is chosen") + }); + d.render(true); + } + } + }catch{ + ui.notifications.error("Class has invalid stats and saves configuration."); + } + return; + } + + async _onDrop(event){ + await super._onDrop(event); + const droppedUuid = TextEditor.getDragEventData(event); + if (droppedUuid.type != "Item"){ + return; + } + await this.updateClass(droppedUuid.uuid); + //this._render(); + } + + activateListeners(html) { + super.activateListeners(html); + + /** Stats */ + html.find(`img[id="system.stats.strength.value"]`).click(ev => { + this.rollStrength(html) + }); + html.find(`img[id="system.stats.speed.value"]`).click(ev => { + this.rollSpeed(html) + }); + html.find(`img[id="system.stats.intellect.value"]`).click(ev => { + this.rollIntellect(html) + }); + html.find(`img[id="system.stats.combat.value"]`).click(ev => { + this.rollCombat(html) + }); + + /** health */ + html.find(`img[id="system.health.max"]`).click(ev => { + this.rollHealth(html) + }); + + /**Saves */ + html.find(`img[id="system.stats.sanity.value"]`).click(ev => { + this.rollSanity(html) + }); + html.find(`img[id="system.stats.fear.value"]`).click(ev => { + this.rollFear(html) + }); + html.find(`img[id="system.stats.body.value"]`).click(ev => { + this.rollBody(html) + }); + + /** credits */ + + html.find(`img[id="system.credits.value"]`).click(ev => { + this.rollCredits(html) + }); + /** Roll everything button */ + html.find(`div[id="roll.everything"]`).click(ev => { + this.rollEverything(html) + }); + + /** Class input + * when changed will lockup if its a defined class item and apply the apropiate modifiers + * + */ + html.find(`input[id="system.class.value"]`).change(ev => { + let class_name = html.find(`input[id="system.class.value"]`).prop('value'); + if(class_name == ""){ + //class name is empty, no class selected. + return; + } + let class_option = html.find(`option[class="class_option"][value="${class_name}"]`).prop('dataset'); + if(class_option == null){ + //class name is not part of the option, leave the string, and dont process uuid. + return; + } + //we have a valid class item to process. + let class_uuid = `Item.${class_option["uuid"]}`; + this.updateClass(class_uuid); + + }); + + + /** Roll patch */ + html.find(`img[id="system.class.patch.value"]`).click(ev => { + this.rollPatch(html) + }); + /** Roll trinket */ + html.find(`img[id="system.class.trinket.value"]`).click(ev => { + this.rollTrinket(html) + }); + /** Roll loadout */ + html.find(`img[id="system.class.loadout.value"]`).click(ev => { + this.rollLoadout(html) + }); + + + /** Save and submit */ + html.find(`div[id="submit"]`).click(ev => { + this.submit() + }); + } + + /** + * This method is called upon form submission after form data is validated + * @param event {Event} The initial triggering submission event + * @param formData {Object} The object of validated form data with which to update the object + * @private + */ + async _updateObject(event, formData) { + + let data = { + "system.credits.value": formData["system.credits.value"], + "system.stats.strength.value": formData["system.stats.strength.value"] + (formData["system.stats.strength.bonus"] || 0), + "system.stats.speed.value": formData["system.stats.speed.value"] + (formData["system.stats.speed.bonus"] || 0), + "system.stats.intellect.value": formData["system.stats.intellect.value"] + (formData["system.stats.intellect.bonus"] || 0), + "system.stats.combat.value": formData["system.stats.combat.value"] + (formData["system.stats.combat.bonus"] || 0), + "system.health.value": formData["system.health.max"] + (formData["system.health.mod"] || 0), + "system.health.max": formData["system.health.max"] + (formData["system.health.mod"] || 0), + "system.stats.sanity.value": formData["system.stats.sanity.value"] + (formData["system.stats.sanity.bonus"] || 0), + "system.stats.fear.value": formData["system.stats.fear.value"] + (formData["system.stats.fear.bonus"] || 0), + "system.stats.body.value": formData["system.stats.body.value"] + (formData["system.stats.body.bonus"] || 0), + } + if (formData["system.hits.bonus"]){ + data["system.hits.max"] = 2+formData["system.hits.bonus"]; + } + else{ + data["system.hits.max"] = 2; + } + + if (formData["name"]) { + data["name"] = formData["name"]; + } + if (formData["system.class.value"]) { + data["system.class.value"] = formData["system.class.value"]; + data["system.class.uuid"] = formData["system.class.uuid"]; + data["system.other.stressdesc.value"] = formData["system.class.traumaresponse"]; + } + ChatMessage.create({content:`Character Rolls
    + STATS
    + Strength: ${data["system.stats.strength.value"]} = ${formData["system.stats.strength.value"]}+${formData["system.stats.strength.bonus"]}
    + Speed: ${data["system.stats.speed.value"]} = ${formData["system.stats.speed.value"]}+${formData["system.stats.speed.bonus"]}
    + Intellect: ${data["system.stats.intellect.value"]} = ${formData["system.stats.intellect.value"]}+${formData["system.stats.intellect.bonus"]}
    + Combat: ${data["system.stats.combat.value"]} = ${formData["system.stats.combat.value"]}+${formData["system.stats.combat.bonus"]}
    + SAVES
    + Sanity: ${data["system.stats.sanity.value"]} = ${formData["system.stats.sanity.value"]}+${formData["system.stats.sanity.bonus"]}
    + Fear: ${data["system.stats.fear.value"]} = ${formData["system.stats.fear.value"]}+${formData["system.stats.fear.bonus"]}
    + Body: ${data["system.stats.body.value"]} = ${formData["system.stats.body.value"]}+${formData["system.stats.body.bonus"]}
    +
    + Health: ${data["system.health.max"]}
    + Extra wounds: ${formData["system.hits.bonus"]}
    + Credits: ${data["system.credits.value"]}
    +
    + Trinket roll:
    + Patch roll:
    + Loadout roll
    `}); + + + if(formData["system.removepreviousitems"]){ + + let itemTypesToDelete = ["item", "armor", "weapon","skill","condition"]; + let itemsToDelete = this.object.items.filter(item => itemTypesToDelete.includes(item.type)); + await this.object.deleteEmbeddedDocuments("Item", itemsToDelete.map(item => item.id)); + } + if(formData["system.class.loadout.uuid"]){ + let loadoutItems = formData["system.class.loadout.uuid"].split(","); + for (var i = 0; i < loadoutItems.length; i++) { + await this.object.modifyItem(loadoutItems[i],1); + } + } + if(formData["system.class.patch.uuid"]){ + await this.object.modifyItem(formData["system.class.patch.uuid"],1); + } + if(formData["system.class.trinket.uuid"]){ + await this.object.modifyItem(formData["system.class.trinket.uuid"],1); + } + if(formData["system.class.skills.uuid"]){ + let skillsItems = formData["system.class.skills.uuid"].split(","); + for (var i = 0; i < skillsItems.length; i++) { + await this.object.modifyItem(skillsItems[i],1); + } + } + this.object.update(data); + } + + } \ No newline at end of file diff --git a/scss/components/_actor_generator.scss b/scss/components/_actor_generator.scss new file mode 100644 index 0000000..120f26f --- /dev/null +++ b/scss/components/_actor_generator.scss @@ -0,0 +1,31 @@ +form.actor-generator{ + div.fulllabel{ + border-radius: 2em; + } + div.ulinputfield{ + min-height: 150px; + } + .smalltext{ + font-size: 0.7rem; + } + div#generator-header-grid-1col { + grid-column: span 1 / span 1; + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + .element-group { + margin-top: 10px; + } + .credit-img{ + height: 30px; + margin-top: 0%; + } + .checkbox-round { + appearance: none; + } + .checkbox-round:checked { + background-color: gray; + } + .clicable-item{ + cursor: pointer; + } +} \ No newline at end of file diff --git a/scss/mothership.scss b/scss/mothership.scss index 5b381c3..e43bc19 100644 --- a/scss/mothership.scss +++ b/scss/mothership.scss @@ -15,4 +15,5 @@ @import 'components/resource'; @import 'components/tabs'; @import 'components/items'; + @import 'components/actor_generator'; } diff --git a/template.json b/template.json index f042f01..1500762 100644 --- a/template.json +++ b/template.json @@ -352,7 +352,8 @@ "module", "condition", "crew", - "repair" + "repair", + "class" ], "templates": { "base": { @@ -447,6 +448,18 @@ "quantity": 1, "major": false }, + "class": { + "templates": [ + "base" + ], + + "TrinketTable": "Compendium.fvtt_mosh_1e_psg.rolltables_1e.RollTable.3iJxbTylcgZ0BRaQ", + "PatchTable": "Compendium.fvtt_mosh_1e_psg.rolltables_1e.RollTable.Vry1q39PNXR3X4oY", + "loadoutTable": "", + "statsandsaves":"", + "skills":"", + "trauma":"" + }, "module": { "templates": [ "base" diff --git a/templates/dialogs/actor-generator-dialog.html b/templates/dialogs/actor-generator-dialog.html new file mode 100644 index 0000000..6968b85 --- /dev/null +++ b/templates/dialogs/actor-generator-dialog.html @@ -0,0 +1,278 @@ +
    + + {{!-- Sheet Header --}} +
    + +
    + +
    +
    NAME
    +
    + +
    +
    + +
    +
    CLASS
    +
    + + + + + {{#each class_options as |class_option id|}} + + {{/each}} + + + +
    + + +
    + + +
    +
    Credits
    +
    + + roll + +
    +
    + + +
    + +
    + {{!-- The grid classes are defined in scss/global/_grid.scss. To use, + use both the "grid" and "grid-Ncol" class where "N" can be any number + from 1 to 12 and will create that number of columns. --}} + +
    + +
    +
    +
    + STRENGTH +
    + roll + +
    + +
    +
    +
    + + +
    +
    +
    + SPEED +
    + roll + +
    + +
    +
    +
    + + +
    +
    +
    + INTELLECT +
    + roll + +
    + +
    +
    +
    + + +
    +
    +
    + COMBAT +
    + roll + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + HEALTH +
    + roll + +
    + +
    +
    + +
    + + +
    +
    +
    + SANITY +
    + roll + +
    + +
    +
    +
    + + +
    +
    +
    + FEAR +
    + roll + +
    + +
    +
    +
    + + +
    +
    +
    + BODY +
    + roll + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + PATCH +
    + roll + +
    +
    + + +
    + + +
    + +
    +
    +
    + TRINKET +
    + roll + +
    +
    + + +
    + + +
    +
    +
    + + + +
    +
    +
    + Skills +
    +
    +
    + +
      +
    +
    +
    + + +
    +
    +
    + loadout +
    + roll + +
    +
    + +
      +
    +
    + +
    +
    + +
    +
    +
    +
    + + Roll everything +
    + roll +
    +
    + +
    +
    + Remove previous items +
    + +
    + +
    +
    + + Save character +
    + save +
    +
    +
    \ No newline at end of file diff --git a/templates/item/item-class-sheet.html b/templates/item/item-class-sheet.html new file mode 100644 index 0000000..e2318e6 --- /dev/null +++ b/templates/item/item-class-sheet.html @@ -0,0 +1,85 @@ +
    + +
    +
    + + + + +
    +
    Name
    +
    +
    + +
    +
    +
    + +
    + +
    + + +
    + +
    +
    + +
    + + +
    + +
    +
    + +
    + + +
    + +
    +
    +
    + +
    + + + {{!-- Sheet Tab Navigation --}} + + + {{!-- Sheet Body --}} +
    + + {{!-- Description Tab --}} +
    + {{editor enriched.description target="system.description" button=true owner=owner editable=true}} +
    + {{!-- Trauma Tab --}} +
    + + + {{!--editor system.trauma target="system.trauma" button=true owner=owner editable=true--}} +
    + {{!-- stats&saves Tab --}} +
    + + {{editor system.statsandsaves target="system.statsandsaves" button=true owner=owner editable=true}} + +
    + {{!-- skills Tab --}} +
    + {{editor enriched.skills target="system.skills" button=true owner=owner editable=true}} +
    +
    +
    \ No newline at end of file