ENH: Attack rolls !
							parent
							
								
									e1319b6215
								
							
						
					
					
						commit
						7106f138d9
					
				|  | @ -167,5 +167,8 @@ | |||
|     "OSE.exploration.ft.long": "Find Room Trap", | ||||
|     "OSE.exploration.ft.short": "Find Trap", | ||||
| 
 | ||||
|     "OSE.messages.getExperience": "{name} gained {value} experience points!" | ||||
|     "OSE.messages.GetExperience": "{name} gained {value} experience points!", | ||||
|     "OSE.messages.AttackSuccess": "<b>Hits AC {result}!</b> ({bonus})", | ||||
|     "OSE.messages.AttackFailure": "<b>Attack fails</b> ({bonus})", | ||||
|     "OSE.messages.InflictsDamage": "Inflicts damage!" | ||||
| } | ||||
|  | @ -11,6 +11,7 @@ export class OseActor extends Actor { | |||
| 
 | ||||
|     // Compute modifiers from actor scores
 | ||||
|     this.computeModifiers(); | ||||
|     this.computeAttack(); | ||||
| 
 | ||||
|     // Determine Initiative
 | ||||
|     if (game.settings.get("ose", "individualInit")) { | ||||
|  | @ -37,7 +38,7 @@ export class OseActor extends Actor { | |||
|     }).then(() => { | ||||
|       const speaker = ChatMessage.getSpeaker({ actor: this }); | ||||
|       ChatMessage.create({ | ||||
|         content: game.i18n.format("OSE.messages.getExperience", { | ||||
|         content: game.i18n.format("OSE.messages.GetExperience", { | ||||
|           name: this.name, | ||||
|           value: modified, | ||||
|         }), | ||||
|  | @ -51,7 +52,6 @@ export class OseActor extends Actor { | |||
| 
 | ||||
|   rollHP(options = {}) { | ||||
|     let roll = new Roll(this.data.data.hp.hd).roll(); | ||||
|     console.log(roll); | ||||
|     return this.update({ | ||||
|       data: { | ||||
|         hp: { | ||||
|  | @ -177,8 +177,8 @@ export class OseActor extends Actor { | |||
|       ...this.data, | ||||
|       ...{ | ||||
|         rollData: { | ||||
|           type: "Exploration", | ||||
|           stat: expl, | ||||
|           type: "Below", | ||||
|           target: this.data.data.exploration[expl], | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|  | @ -236,9 +236,21 @@ export class OseActor extends Actor { | |||
|   } | ||||
| 
 | ||||
|   rollAttack(attData, options = {}) { | ||||
|     const rollParts = ["1d20"]; | ||||
|     const data = this.data.data; | ||||
| 
 | ||||
|     const rollParts = ["1d20"]; | ||||
|     const dmgParts = []; | ||||
| 
 | ||||
|     if (!attData.dmg || !game.settings.get("ose", "variableWeaponDamage")) { | ||||
|       dmgParts.push("1d6"); | ||||
|     } else { | ||||
|       dmgParts.push(attData.dmg); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     let ascending = game.settings.get("ose", "ascendingAC"); | ||||
|     if (ascending) { | ||||
|       rollParts.push(data.thac0.bba.toString()); | ||||
|       if (attData.type == "missile") { | ||||
|         rollParts.push( | ||||
|           data.scores.dex.mod.toString(), | ||||
|  | @ -250,8 +262,14 @@ export class OseActor extends Actor { | |||
|           data.thac0.mod.melee.toString() | ||||
|         ); | ||||
|       } | ||||
|     if (game.settings.get("ose", "ascendingAC")) { | ||||
|       rollParts.push(this.data.data.thac0.bba.toString()); | ||||
|     } | ||||
| 
 | ||||
|     let thac0 = 0; | ||||
|     if (attData.type == "melee") { | ||||
|       dmgParts.push(data.scores.str.mod); | ||||
|       thac0 = data.thac0.melee; | ||||
|     } else if (attData.type == "missile") { | ||||
|       thac0 = data.thac0.missile; | ||||
|     } | ||||
| 
 | ||||
|     const rollData = { | ||||
|  | @ -259,12 +277,15 @@ export class OseActor extends Actor { | |||
|       ...{ | ||||
|         rollData: { | ||||
|           type: "Attack", | ||||
|           stat: attData.type, | ||||
|           scores: data.scores, | ||||
|           thac0: thac0, | ||||
|           weapon: { | ||||
|             parts: dmgParts, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|     let skip = options.event && options.event.ctrlKey; | ||||
| 
 | ||||
|     // Roll and return
 | ||||
|     return OseDice.Roll({ | ||||
|       event: options.event, | ||||
|  | @ -274,8 +295,6 @@ export class OseActor extends Actor { | |||
|       speaker: ChatMessage.getSpeaker({ actor: this }), | ||||
|       flavor: `${attData.label} - ${game.i18n.localize("OSE.Attack")}`, | ||||
|       title: `${attData.label} - ${game.i18n.localize("OSE.Attack")}`, | ||||
|     }).then(() => { | ||||
|       this.rollDamage(attData, {}); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | @ -334,4 +353,21 @@ export class OseActor extends Actor { | |||
|     data.scores.dex.init = OseActor._cappedMod(this.data.data.scores.dex.value); | ||||
|     data.scores.cha.npc = OseActor._cappedMod(this.data.data.scores.cha.value); | ||||
|   } | ||||
| 
 | ||||
|   computeAttack() { | ||||
|     const data = this.data.data; | ||||
|     let ascending = game.settings.get("ose", "ascendingAC"); | ||||
|     data.thac0.missile = ascending ? data.thac0.bba : data.thac0.value; | ||||
|     data.thac0.melee = ascending ? data.thac0.bba : data.thac0.value; | ||||
|     if (this.data.type != "character") { | ||||
|       return; | ||||
|     } | ||||
|     if (ascending) { | ||||
|       data.thac0.missile += data.scores.dex.mod + data.thac0.mod.missile; | ||||
|       data.thac0.melee += data.scores.str.mod + data.thac0.mod.melee; | ||||
|     } else { | ||||
|       data.thac0.missile -= data.scores.dex.mod - data.thac0.mod.missile; | ||||
|       data.thac0.melee -= data.scores.str.mod - data.thac0.mod.melee; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -3,75 +3,27 @@ export class OseDice { | |||
|     let result = { | ||||
|       isSuccess: false, | ||||
|       isFailure: false, | ||||
|       target: "", | ||||
|       target: data.rollData.target, | ||||
|     }; | ||||
|     // ATTACKS
 | ||||
| 
 | ||||
|     let die = roll.parts[0].total; | ||||
|     if (data.rollData.type == "Attack") { | ||||
|       if (game.settings.get("ose", "ascendingAC")) { | ||||
|         let bba = data.data.thac0.bba; | ||||
|         if (data.rollData.stat == "melee") { | ||||
|           bba += data.data.thac0.mod.melee + data.rollData.scores.str.mod; | ||||
|         } else if (data.rollData.stat == "missile") { | ||||
|           bba += data.data.thac0.mod.missile + data.rollData.scores.dex.mod; | ||||
|         } | ||||
|         result.target = bba; | ||||
|         if (die == 1) { | ||||
|           result.isFailure = true; | ||||
|           return result; | ||||
|         } | ||||
|         result.isSuccess = true; | ||||
|       } else { | ||||
|         // B/X Historic THAC0 Calculation
 | ||||
|         let thac = data.data.thac0.value; | ||||
|         if (data.rollData.stat == "melee") { | ||||
|           thac -= data.data.thac0.mod.melee + data.rollData.scores.str.mod; | ||||
|         } else if (data.rollData.stat == "missile") { | ||||
|           thac -= data.data.thac0.mod.missile + data.rollData.scores.dex.mod; | ||||
|         } | ||||
|         result.target = thac; | ||||
|         if (thac - roll.total > 9) { | ||||
|           result.isFailure = true; | ||||
|           return result; | ||||
|         } | ||||
|         result.details = `<div class='roll-result'><b>Hits AC ${Math.clamped( | ||||
|           thac - roll.total, | ||||
|           -3, | ||||
|           9 | ||||
|         )}</b> (${thac})</div>`; | ||||
|       } | ||||
|     } else if (data.rollData.type == "Above") { | ||||
|     if (data.rollData.type == "Above") { | ||||
|       // SAVING THROWS
 | ||||
|       let sv = data.rollData.target; | ||||
|       result.target = sv; | ||||
|       if (roll.total >= sv) { | ||||
|       if (roll.total >= result.target) { | ||||
|         result.isSuccess = true; | ||||
|       } else { | ||||
|         result.isFailure = true; | ||||
|       } | ||||
|     } else if (data.rollData.type == "Below") { | ||||
|       // Morale
 | ||||
|       let m = data.rollData.target; | ||||
|       result.target = m; | ||||
|       if (roll.total <= m) { | ||||
|       // MORALE, EXPLORATION
 | ||||
|       if (roll.total <= result.target) { | ||||
|         result.isSuccess = true; | ||||
|       } else { | ||||
|         result.isFailure = true; | ||||
|       } | ||||
|     } else if (data.rollData.type == "Check") { | ||||
|       // SCORE CHECKS
 | ||||
|       let sc = data.rollData.target; | ||||
|       result.target = sc; | ||||
|       if (die == 1 || (roll.total <= sc && die < 20)) { | ||||
|         result.isSuccess = true; | ||||
|       } else { | ||||
|         result.isFailure = true; | ||||
|       } | ||||
|     } else if (data.rollData.type == "Exploration") { | ||||
|       // EXPLORATION CHECKS
 | ||||
|       let sc = data.data.exploration[data.rollData.stat]; | ||||
|       result.target = sc; | ||||
|       if (roll.total <= sc) { | ||||
|       // SCORE CHECKS (1s and 20s)
 | ||||
|       if (die == 1 || (roll.total <= result.target && die < 20)) { | ||||
|         result.isSuccess = true; | ||||
|       } else { | ||||
|         result.isFailure = true; | ||||
|  | @ -88,7 +40,7 @@ export class OseDice { | |||
|     speaker = null, | ||||
|     form = null, | ||||
|   } = {}) { | ||||
|     const template = "systems/ose/templates/chat/roll-attack.html"; | ||||
|     const template = "systems/ose/templates/chat/roll-result.html"; | ||||
| 
 | ||||
|     let chatData = { | ||||
|       user: game.user._id, | ||||
|  | @ -142,6 +94,110 @@ export class OseDice { | |||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   static digestAttackResult(data, roll) { | ||||
|     let result = { | ||||
|       isSuccess: false, | ||||
|       isFailure: false, | ||||
|       target: "", | ||||
|     }; | ||||
|     result.target = data.rollData.thac0; | ||||
|     if (game.settings.get("ose", "ascendingAC")) { | ||||
|       result.details = game.i18n.format('OSE.messages.AttackSuccess', {result: roll.total, bonus: result.target}); | ||||
|       result.isSuccess = true; | ||||
|     } else { | ||||
|       // B/X Historic THAC0 Calculation
 | ||||
|       if (result.target - roll.total > 9) { | ||||
|         result.details = game.i18n.format('OSE.messages.AttackFailure', {bonus: result.target}); | ||||
|         return result; | ||||
|       } | ||||
|       result.isSuccess = true; | ||||
|       let value = Math.clamped(result.target - roll.total, -3, 9); | ||||
|       result.details = game.i18n.format('OSE.messages.AttackSuccess', {result: value, bonus: result.target}); | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   static async sendAttackRoll({ | ||||
|     parts = [], | ||||
|     data = {}, | ||||
|     title = null, | ||||
|     flavor = null, | ||||
|     speaker = null, | ||||
|     form = null, | ||||
|   } = {}) { | ||||
|     const template = "systems/ose/templates/chat/roll-attack.html"; | ||||
| 
 | ||||
|     let chatData = { | ||||
|       user: game.user._id, | ||||
|       speaker: speaker, | ||||
|     }; | ||||
| 
 | ||||
|     let templateData = { | ||||
|       title: title, | ||||
|       flavor: flavor, | ||||
|       data: data, | ||||
|     }; | ||||
| 
 | ||||
|     // Optionally include a situational bonus
 | ||||
|     if (form !== null) data["bonus"] = form.bonus.value; | ||||
|     if (data["bonus"]) parts.push(data["bonus"]); | ||||
| 
 | ||||
|     const roll = new Roll(parts.join("+"), data).roll(); | ||||
|     const dmgRoll = new Roll(data.rollData.weapon.parts.join("+"), data).roll(); | ||||
| 
 | ||||
|     // Convert the roll to a chat message and return the roll
 | ||||
|     let rollMode = game.settings.get("core", "rollMode"); | ||||
|     rollMode = form ? form.rollMode.value : rollMode; | ||||
| 
 | ||||
|     templateData.result = OseDice.digestAttackResult(data, roll); | ||||
| 
 | ||||
|     return new Promise((resolve) => { | ||||
|       roll.render().then((r) => { | ||||
|         templateData.rollOSE = r; | ||||
|         dmgRoll.render().then((dr) => { | ||||
|           templateData.rollDamage = dr; | ||||
|           renderTemplate(template, templateData).then((content) => { | ||||
|             chatData.content = content; | ||||
|             // 2 Step Dice So Nice
 | ||||
|             if (game.dice3d) { | ||||
|               game.dice3d | ||||
|                 .showForRoll( | ||||
|                   roll, | ||||
|                   game.user, | ||||
|                   true, | ||||
|                   chatData.whisper, | ||||
|                   chatData.blind | ||||
|                 ) | ||||
|                 .then(() => { | ||||
|                   if (templateData.result.isSuccess) { | ||||
|                     game.dice3d | ||||
|                     .showForRoll( | ||||
|                       dmgRoll, | ||||
|                       game.user, | ||||
|                       true, | ||||
|                       chatData.whisper, | ||||
|                       chatData.blind | ||||
|                     ) | ||||
|                     .then(() => { | ||||
|                       ChatMessage.create(chatData); | ||||
|                       resolve(); | ||||
|                     }); | ||||
|                   } else { | ||||
|                     ChatMessage.create(chatData); | ||||
|                     resolve(); | ||||
|                   } | ||||
|                 }); | ||||
|             } else { | ||||
|               chatData.sound = CONFIG.sounds.dice; | ||||
|               ChatMessage.create(chatData); | ||||
|               resolve(); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   static async Roll({ | ||||
|     parts = [], | ||||
|     data = {}, | ||||
|  | @ -164,37 +220,40 @@ export class OseDice { | |||
|       rollModes: CONFIG.Dice.rollModes, | ||||
|     }; | ||||
| 
 | ||||
|     let buttons = { | ||||
|       ok: { | ||||
|         label: game.i18n.localize("OSE.Roll"), | ||||
|         icon: '<i class="fas fa-dice-d20"></i>', | ||||
|         callback: (html) => { | ||||
|           roll = OseDice.sendRoll({ | ||||
|     let rollData = { | ||||
|       parts: parts, | ||||
|       data: data, | ||||
|       title: title, | ||||
|       flavor: flavor, | ||||
|       speaker: speaker, | ||||
|             form: html[0].children[0], | ||||
|           }); | ||||
|     }; | ||||
| 
 | ||||
|     if (skipDialog) { | ||||
|       return data.rollData.type === "Attack" | ||||
|         ? OseDice.sendAttackRoll(rollData) | ||||
|         : OseDice.sendRoll(rollData); | ||||
|     } | ||||
| 
 | ||||
|     let buttons = { | ||||
|       ok: { | ||||
|         label: game.i18n.localize("OSE.Roll"), | ||||
|         icon: '<i class="fas fa-dice-d20"></i>', | ||||
|         callback: (html) => { | ||||
|           rolled = true; | ||||
|           rollData.form = html[0].children[0]; | ||||
|           roll = | ||||
|             data.rollData.type === "Attack" | ||||
|               ? OseDice.sendAttackRoll(rollData) | ||||
|               : OseDice.sendRoll(rollData); | ||||
|         }, | ||||
|       }, | ||||
|       cancel: { | ||||
|         icon: '<i class="fas fa-times"></i>', | ||||
|         label: game.i18n.localize("OSE.Cancel"), | ||||
|         callback: (html) => {}, | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     if (skipDialog) { | ||||
|       return OseDice.sendRoll({ | ||||
|         parts, | ||||
|         data, | ||||
|         title, | ||||
|         flavor, | ||||
|         speaker, | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     const html = await renderTemplate(template, dialogData); | ||||
|     let roll; | ||||
| 
 | ||||
|  |  | |||
|  | @ -156,12 +156,12 @@ | |||
|                         {{#if config.ascendingAC}} | ||||
|                         <div class="attribute-value" | ||||
|                             title="{{localize 'OSE.AB'}}({{data.thac0.bba}}) + {{localize 'OSE.scores.str.long'}}({{data.scores.str.mod}}) + {{localize 'OSE.Modifier'}}({{data.thac0.mod.melee}})"> | ||||
|                             {{add data.thac0.mod.melee (add data.scores.str.mod data.thac0.bba)}} | ||||
|                             {{data.thac0.melee}} | ||||
|                         </div> | ||||
|                         {{else}} | ||||
|                         <div class="attribute-value" | ||||
|                             title="{{localize 'OSE.Thac0'}}({{data.thac0.value}}) - {{localize 'OSE.scores.str.long'}}({{data.scores.str.mod}}) - {{localize 'OSE.Modifier'}}({{data.thac0.mod.melee}})"> | ||||
|                             {{subtract data.thac0.mod.melee (subtract data.scores.str.mod data.thac0.value)}} | ||||
|                             {{data.thac0.melee}} | ||||
|                         </div> | ||||
|                         {{/if}} | ||||
|                     </div> | ||||
|  | @ -196,12 +196,12 @@ | |||
|                         {{#if config.ascendingAC}} | ||||
|                         <div class="attribute-value" | ||||
|                             title="{{localize 'OSE.AB'}}({{data.thac0.bba}}) + {{localize 'OSE.scores.dex.long'}}({{data.scores.dex.mod}}) + {{localize 'OSE.Modifier'}}({{data.thac0.mod.missile}})"> | ||||
|                             {{add data.thac0.mod.missile (add data.scores.dex.mod data.thac0.bba)}} | ||||
|                             {{data.thac0.missile}} | ||||
|                         </div> | ||||
|                         {{else}} | ||||
|                         <div class="attribute-value" | ||||
|                             title="{{localize 'OSE.Thac0'}}({{data.thac0.value}}) - {{localize 'OSE.scores.dex.long'}}({{data.scores.dex.mod}}) - {{localize 'OSE.Modifier'}}({{data.thac0.mod.missile}})"> | ||||
|                             {{subtract data.thac0.mod.missile (subtract data.scores.dex.mod data.thac0.value)}} | ||||
|                             {{data.thac0.missile}} | ||||
|                         </div> | ||||
|                         {{/if}} | ||||
|                     </div> | ||||
|  |  | |||
|  | @ -1,9 +1,15 @@ | |||
| <section class="ose chat-message"> | ||||
|     <div class="ose chat-block"> | ||||
|         <h2 class="chat-title">{{title}}</h2> | ||||
|         {{#if result.details}}<div class="chat-details">{{{result.details}}}</div>{{/if}} | ||||
|         {{#if result.isFailure}}<div class='roll-result roll-fail'><b>{{localize 'OSE.Failure'}}</b> ({{result.target}})</div>{{/if}} | ||||
|         {{#if result.isSuccess}}<div class='roll-result roll-success'><b>{{localize 'OSE.Success'}}</b> ({{result.target}})</div>{{/if}} | ||||
|         <div class="chat-details"> | ||||
|             <div class="roll-result">{{{result.details}}}</div> | ||||
|         </div> | ||||
|         {{#if rollOSE}}<div>{{{rollOSE}}}</div>{{/if}} | ||||
|         {{#if result.isSuccess}} | ||||
|         <div class="chat-details"> | ||||
|             <div class="roll-result"><b>{{localize 'OSE.messages.InflictsDamage'}}</b></div> | ||||
|         </div> | ||||
|         <div>{{{rollDamage}}}</div> | ||||
|         {{/if}} | ||||
|     </div> | ||||
| </section> | ||||
|  | @ -0,0 +1,11 @@ | |||
| <section class="ose chat-message"> | ||||
|     <div class="ose chat-block"> | ||||
|         <h2 class="chat-title">{{title}}</h2> | ||||
|         {{#if result.details}}<div class="chat-details">{{{result.details}}}</div>{{/if}} | ||||
|         {{#if result.isFailure}}<div class='roll-result roll-fail'><b>{{localize 'OSE.Failure'}}</b> ({{result.target}}) | ||||
|         </div>{{/if}} | ||||
|         {{#if result.isSuccess}}<div class='roll-result roll-success'><b>{{localize 'OSE.Success'}}</b> | ||||
|             ({{result.target}})</div>{{/if}} | ||||
|         {{#if rollOSE}}<div>{{{rollOSE}}}</div>{{/if}} | ||||
|     </div> | ||||
| </section> | ||||
		Loading…
	
		Reference in New Issue