From ea0a874e38f466e1f172f2e34cebe1f183958aea Mon Sep 17 00:00:00 2001 From: supervj <64861570+supervj@users.noreply.github.com> Date: Thu, 20 May 2021 03:45:18 -0400 Subject: [PATCH 01/15] Flesh out New Dice structure - Not complete add structure for Hull, Shield, and Power Dice to allow for recharge, refitting, and regeneration. Not complete. --- lang/en.json | 48 ++++-- module/actor/entity.js | 203 ++++++++++++++++++++++-- module/apps/recharge-rest.js | 117 ++++++++++++++ module/apps/refitting-rest.js | 69 ++++++++ template.json | 29 +++- templates/actors/newActor/starship.html | 132 +++++++-------- templates/apps/recharge-rest.html | 38 +++++ templates/apps/refitting-rest.html | 20 +++ 8 files changed, 549 insertions(+), 107 deletions(-) create mode 100644 module/apps/recharge-rest.js create mode 100644 module/apps/refitting-rest.js create mode 100644 templates/apps/recharge-rest.html create mode 100644 templates/apps/refitting-rest.html diff --git a/lang/en.json b/lang/en.json index 1decdafc..8d4493fb 100644 --- a/lang/en.json +++ b/lang/en.json @@ -542,9 +542,12 @@ "SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!", "SW5E.HP": "Health", "SW5E.HPFormula": "Health Formula", - "SW5E.HullDice": "Hull Dice", - "SW5E.HullPoints": "Hull Points", - "SW5E.HullPointsFormula": "Hull Points Formula", + "SW5E.HullDice": "Hull Dice", + "SW5E.HullDiceRoll": "Roll Hull Dice", + "SW5E.HullDiceUsed": "Hull Dice Used", + "SW5E.HullDiceWarn": "{name} has no available {formula} Hull Dice remaining!", + "SW5E.HullPoints": "Hull Points", + "SW5E.HullPointsFormula": "Hull Points Formula", "SW5E.HyperdriveClass": "Hyperdrive Class", "SW5E.Ideals": "Ideals", "SW5E.Identified": "Identified", @@ -786,8 +789,8 @@ "SW5E.MovementCrawl": "Crawl", "SW5E.MovementFly": "Fly", "SW5E.MovementHover": "Hover", - "SW5E.MovementRoll": "Roll", - "SW5E.MovementSpace": "Space Flight", + "SW5E.MovementRoll": "Roll", + "SW5E.MovementSpace": "Space Flight", "SW5E.MovementSwim": "Swim", "SW5E.MovementTurn": "Turning", "SW5E.MovementUnits": "Units", @@ -847,10 +850,10 @@ "SW5E.PowerCreate": "Create Power", "SW5E.PowerDC": "Power DC", "SW5E.PowerDetails": "Power Details", + "SW5E.PowerDie": "Power Die", + "SW5E.PowerDiePl": "Power Dice", + "SW5E.PowerDieAlloc": "Power Die Allocation", "SW5E.PowerDiceRecovery": "Power Dice Recovery", - "SW5E.PowerDie": "Power Die", - "SW5E.PowerDieAlloc": "Power Die Allocation", - "SW5E.PowerDiePl": "Power Dice", "SW5E.PowerEffects": "Power Effects", "SW5E.PowerfulCritical": "Powerful Critical", "SW5E.PowerLevel": "Power Level", @@ -900,7 +903,23 @@ "SW5E.Reaction": "Reaction", "SW5E.ReactionPl": "Reactions", "SW5E.Recharge": "Recharge", - "SW5E.Refitting": "Refitting", + "SW5E.RechargeRestHint": "Take a recharge rest? On a recharge rest you may spend remaining Hull Dice and recover Shields.", + "SW5E.RechargetRestNoHD": "No Hull Dice remaining", + "SW5E.RechargeRestNormal": "Recharge Rest (1 hour)", + "SW5E.RechargeRestOvernight": "Recharge Rest (New Day)", + "SW5E.RechargeRestResult": "{name} takes a recharge rest spending {dice} Hull Dice to recover {health} Hull Points.", + "SW5E.RechargeRestResultShort": "{name} takes a recharge rest.", + "SW5E.RechargeRestSelect": "Select Dice to Roll", + "SW5E.Refitting": "Refitting", + "SW5E.RefittingRest": "Refitting Rest", + "SW5E.RefittingRestEpic": "Refitting Rest (1 hour)", + "SW5E.RefittingRestGritty": "Refitting Rest (7 days)", + "SW5E.RefittingRestNormal": "Refitting Rest (8 hours)", + "SW5E.RefittingRestOvernight": "Refitting Rest (New Day)", + "SW5E.RefittingRestResult": "{name} takes a refitting rest.", + "SW5E.RefittingRestResultHD": "{name} takes a refitting rest and recovers {dice} Hull Dice.", + "SW5E.RefittingRestResultHP": "{name} takes a refitting rest and recovers {health} Hull Points.", + "SW5E.RefittingRestResultHPHD": "{name} takes a refitting rest and recovers {health} Hull Points and {dice} Hull Dice.", "SW5E.Refuel": "Refuel", "SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient", "SW5E.RequiredMaterials": "Required Materials", @@ -948,10 +967,13 @@ "SW5E.SheetClassNPC": "Default NPC Sheet", "SW5E.SheetClassNPCOld": "Old NPC Sheet", "SW5E.SheetClassVehicle": "Default Vehicle Sheet", - "SW5E.ShieldDice": "Shield Dice", - "SW5E.ShieldPoints": "Shield Points", - "SW5E.ShieldPointsFormula": "Shield Points Formula", - "SW5E.ShieldRegen": "Regen", + "SW5E.ShieldDice": "Shield Dice", + "SW5E.ShieldDiceRoll": "Roll Shield Dice", + "SW5E.ShieldDiceUsed": "Shield Dice Used", + "SW5E.ShieldDiceWarn": "{name} has no available {formula} Shield Dice remaining!", + "SW5E.ShieldPoints": "Shield Points", + "SW5E.ShieldPointsFormula": "Shield Points Formula", + "SW5E.ShieldRegen": "Regen", "SW5E.ShortRest": "Short Rest", "SW5E.ShortRestEpic": "Short Rest (5 minutes)", "SW5E.ShortRestGritty": "Short Rest (8 hours)", diff --git a/module/actor/entity.js b/module/actor/entity.js index ad37ab56..9c86d49a 100644 --- a/module/actor/entity.js +++ b/module/actor/entity.js @@ -356,13 +356,13 @@ export default class Actor5e extends Actor { const data = actorData.data; // Proficiency - data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4); + data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4); // Link hull to hp and shields to temp hp - data.attributes.hull.value = data.attributes.hp.value; - data.attributes.hull.max = data.attributes.hp.max; - data.attributes.shld.value = data.attributes.hp.temp; - data.attributes.shld.max = data.attributes.hp.tempmax; + //data.attributes.hull.value = data.attributes.hp.value; + //data.attributes.hull.max = data.attributes.hp.max; + //data.attributes.shld.value = data.attributes.hp.temp; + //data.attributes.shld.max = data.attributes.hp.tempmax; } /* -------------------------------------------- */ @@ -1171,15 +1171,192 @@ export default class Actor5e extends Actor { /* -------------------------------------------- */ /** - * Results from a rest operation. - * - * @typedef {object} RestResult - * @property {number} dhp Hit points recovered during the rest. - * @property {number} dhd Hit dice recovered or spent during the rest. - * @property {object} updateData Updates applied to the actor. - * @property {Array.} updateItems Updates applied to actor's items. - * @property {boolean} newDay Whether a new day occurred during the rest. + * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier + * @param {string} [denomination] The hit denomination of hull die to roll. Example "d8". + * If no denomination is provided, the first available HD will be used + * @param {string} [numDice] How many damage dice to roll? + * @param {string} [keep] Which dice to keep? Example "kh1". + * @param {boolean} [dialog] Show a dialog prompt for configuring the hull die roll? + * @return {Promise} The created Roll instance, or null if no hull die was rolled */ + async rollHullDie(denomination, numDice="1", keep="",{dialog=true}={}) { + + // If no denomination was provided, choose the first available + let sship = null; + if ( !denomination ) { + sship = this.itemTypes.class.find(s => s.data.data.hullDiceUsed < (s.data.data.tier + s.data.data.hullDiceStart)); + if ( !sship ) return null; + denomination = sship.data.data.hullDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find(i => { + const d = i.data.data; + return (d.hullDice === denomination) && ((d.hitDiceUsed || 0) < ((d.tier || 0) + d.hullDiceStart)); + }); + } + + // If no class is available, display an error notification + if ( !sship ) { + ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HullDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hullDie"}} + }); + if ( !roll ) return null; + + // Adjust actor data + await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier + * @return {Promise} The created Roll instance, or null if no hull die was rolled + */ + async rollHullDieCheck() { + + // If no denomination was provided, choose the first available + let sship = null; + if ( !denomination ) { + sship = this.itemTypes.class.find(s => s.data.data.hullDiceUsed < (s.data.data.tier + s.data.data.hullDiceStart)); + if ( !sship ) return null; + denomination = sship.data.data.hullDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find(i => { + const d = i.data.data; + return (d.hullDice === denomination) && ((d.hitDiceUsed || 0) < ((d.tier || 0) + d.hullDiceStart)); + }); + } + + // If no class is available, display an error notification + if ( !sship ) { + ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HullDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hullDie"}} + }); + if ( !roll ) return null; + + // Adjust actor data + await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a shield die of the appropriate type, gaining shield points equal to the die roll + * multiplied by the shield regeneration coefficient + * @param {string} [denomination] The denomination of shield die to roll. Example "d8". + * If no denomination is provided, the first available SD will be used + * @param {boolean} [natural] Natural ship shield regeneration (true) or user action (false)? + * @param {string} [numDice] How many damage dice to roll? + * @param {string} [keep] Which dice to keep? Example "kh1". + * @param {boolean} [dialog] Show a dialog prompt for configuring the shield die roll? + * @return {Promise} The created Roll instance, or null if no shield die was rolled + */ + async rollShieldDie(denomination, natural=false, numDice="1", keep="", {dialog=true}={}) { + + // If no denomination was provided, choose the first available + let sship = null; + if ( !denomination ) { + sship = this.itemTypes.class.find(s => s.data.data.shldDiceUsed < (s.data.data.tier + s.data.data.shldDiceStart)); + if ( !sship ) return null; + denomination = sship.data.data.shldDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find(i => { + const d = i.data.data; + return (d.shldDice === denomination) && ((d.shldDiceUsed || 0) < ((d.tier || 0) + d.shldDiceStart)); + }); + } + + // If no starship is available, display an error notification + if ( !sship ) { + ui.notifications.error(game.i18n.format("SW5E.ShldDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // if natural regeneration roll max + if (natural) { + numdice = denomination.substring(1); + denomination = ""; + keep = ""; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep} * @attributes.regenRate`]; + const title = game.i18n.localize("SW5E.ShieldDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + roll = await damageRoll({ + event: new Event("shldDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "shldDie"}} + }); + if ( !roll ) return null; + + // Adjust actor data + await sship.update({"data.shldDiceUsed": sship.data.data.shldDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.tempmax - hp.temp, roll.total); + await this.update({"data.attributes.hp.temp": hp.temp + dhp}); + return roll; + } /* -------------------------------------------- */ diff --git a/module/apps/recharge-rest.js b/module/apps/recharge-rest.js new file mode 100644 index 00000000..1d61c7a4 --- /dev/null +++ b/module/apps/recharge-rest.js @@ -0,0 +1,117 @@ +/** + * A helper Dialog subclass for rolling Hit Dice on a recharge rest + * @extends {Dialog} + */ +export default class RechargeRestDialog extends Dialog { + constructor(actor, dialogData={}, options={}) { + super(dialogData, options); + + /** + * Store a reference to the Actor entity which is resting + * @type {Actor} + */ + this.actor = actor; + + /** + * Track the most recently used HD denomination for re-rendering the form + * @type {string} + */ + this._denom = null; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/recharge-rest.html", + classes: ["sw5e", "dialog"] + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + const data = super.getData(); + + // Determine Hull Dice + data.availableHD = this.actor.data.items.reduce((hd, item) => { + if ( item.type === "starship" ) { + const d = item.data; + const denom = d.hullDice || "d6"; + const available = parseInt(d.hullDiceStart || 1) + parseInt(d.tier || 0) - parseInt(d.hullDiceUsed || 0); + hd[denom] = denom in hd ? hd[denom] + available : available; + } + return hd; + }, {}); + data.canRoll = this.actor.data.data.attributes.hull.dice > 0; + data.denomination = this._denom; + + // Determine rest type + const variant = game.settings.get("sw5e", "restVariant"); + data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute + data.newDay = false; // It may be a new day, but not by default + return data; + } + + /* -------------------------------------------- */ + + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + let btn = html.find("#roll-hulld"); + btn.click(this._onRollHullDie.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Hull Die as part of a Recharge Rest action + * @param {Event} event The triggering click event + * @private + */ + async _onRollHullDie(event) { + event.preventDefault(); + const btn = event.currentTarget; + this._denom = btn.form.hulld.value; + await this.actor.rollHullDie(this._denom); + this.render(); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has + * been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async rechargeRestDialog({actor}={}) { + return new Promise((resolve, reject) => { + const dlg = new this(actor, { + title: "Recharge Rest", + buttons: { + rest: { + icon: '', + label: "Rest", + callback: html => { + let newDay = false; + if (game.settings.get("sw5e", "restVariant") === "gritty") + newDay = html.find('input[name="newDay"]')[0].checked; + resolve(newDay); + } + }, + cancel: { + icon: '', + label: "Cancel", + callback: reject + } + }, + close: reject + }); + dlg.render(true); + }); + } +} diff --git a/module/apps/refitting-rest.js b/module/apps/refitting-rest.js new file mode 100644 index 00000000..090fa693 --- /dev/null +++ b/module/apps/refitting-rest.js @@ -0,0 +1,69 @@ +/** + * A helper Dialog subclass for completing a refitting rest + * @extends {Dialog} + */ +export default class RefittingRestDialog extends Dialog { + constructor(actor, dialogData = {}, options = {}) { + super(dialogData, options); + this.actor = actor; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/refitting-rest.html", + classes: ["sw5e", "dialog"] + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + const data = super.getData(); + const variant = game.settings.get("sw5e", "restVariant"); + data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week + data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) + return data; + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Refitting Rest confirmation dialog and returns a Promise once it's + * workflow has been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async refittingRestDialog({ actor } = {}) { + return new Promise((resolve, reject) => { + const dlg = new this(actor, { + title: "Refitting Rest", + buttons: { + rest: { + icon: '', + label: "Rest", + callback: html => { + let newDay = false; + if (game.settings.get("sw5e", "restVariant") === "normal") + newDay = html.find('input[name="newDay"]')[0].checked; + else if(game.settings.get("sw5e", "restVariant") === "gritty") + newDay = true; + resolve(newDay); + } + }, + cancel: { + icon: '', + label: "Cancel", + callback: reject + } + }, + default: 'rest', + close: reject + }); + dlg.render(true); + }); + } +} \ No newline at end of file diff --git a/template.json b/template.json index 1681d52e..4e026efc 100644 --- a/template.json +++ b/template.json @@ -423,6 +423,26 @@ "failure": 0, "success": 0 }, + "deployment": { + "coord": { + "uuid": null + }, + "gunner": { + "uuid": null + }, + "mechanic": { + "uuid": null + }, + "operator": { + "uuid": null + }, + "pilot": { + "uuid": null + }, + "technician": { + "uuid": null + } + }, "dr": 0, "engpow": 1, "exhaustion": 0, @@ -430,6 +450,7 @@ "hull": { "die": "", "dice": 0, + "dicemax": 0, "formula":"", "value": null, "max": null @@ -439,7 +460,7 @@ "max": 10 }, "pwrdice": { - "pwrdie": "", + "die": "", "recovery": 1, "central": { "value": 0, @@ -469,7 +490,11 @@ "shld": { "die": "", "dice": 0, + "dicemax": 0, + "depleted": false, "formula":"", + "regenRate": 1, + "capMod": 1, "value": null, "max": null }, @@ -507,7 +532,7 @@ "value": 0, "ability": "cha" }, - "int": { + "inf": { "value": 0, "ability": "cha" }, diff --git a/templates/actors/newActor/starship.html b/templates/actors/newActor/starship.html index 5738742c..fa905c46 100644 --- a/templates/actors/newActor/starship.html +++ b/templates/actors/newActor/starship.html @@ -14,14 +14,8 @@
- {{lookup config.actorSizes data.traits.size}} - - {{lookup config.starshipRolessm data.details.role}} - +
{{!-- ARMOR CLASS --}} @@ -31,10 +25,11 @@
-
- {{ localize "SW5E.Proficiency" }} - {{numberFormat data.attributes.prof decimals=0 sign=true}} -
+
+ + + +
{{!-- HULL POINTS --}} @@ -42,14 +37,15 @@

{{ localize "SW5E.HullPoints" }}

+ data-dtype="Number" class="value-number" /> / + data-dtype="Number" class="value-number" />
-