diff --git a/module/chat.ts b/module/chat.ts index 4128ee86..a64a8906 100644 --- a/module/chat.ts +++ b/module/chat.ts @@ -1,7 +1,8 @@ /** * Chat message helpers */ -import { handleFateReroll } from "./rolls.js"; + +import { handleFateReroll } from "./rolls/rerollFate.js"; /** * Binds buttons in chat log to perform actions diff --git a/module/rolls.ts b/module/rolls.ts index f6185c30..33876ea9 100644 --- a/module/rolls.ts +++ b/module/rolls.ts @@ -1,7 +1,13 @@ import { Ability, BWActor, TracksTests } from "./actor.js"; import { BWActorSheet } from "./bwactor-sheet.js"; import * as helpers from "./helpers.js"; -import { Relationship, Skill, SkillData, SkillDataRoot } from "./items/item.js"; +import { Skill, SkillDataRoot } from "./items/item.js"; +import { handleAttrRoll } from "./rolls/rollAttribute.js"; +import { handleCirclesRoll } from "./rolls/rollCircles.js"; +import { handleLearningRoll } from "./rolls/rollLearning.js"; +import { handleGritRoll, handleShrugRoll } from "./rolls/rollPtgs.js"; +import { handleSkillRoll } from "./rolls/rollSkill.js"; +import { handleStatRoll } from "./rolls/rollStat.js"; export async function handleRollable( e: JQuery.ClickEvent, sheet: BWActorSheet): Promise { @@ -32,584 +38,10 @@ export async function handleRollable( } } -export async function handleFateReroll(target: HTMLButtonElement): Promise { - const actor = game.actors.get(target.dataset.actorId || "") as BWActor; - const accessor = target.dataset.accessor || ''; - const itemId = target.dataset.itemId || ""; - let rollStat: Ability | SkillData; - if (target.dataset.rerollType === "stat") { - rollStat = getProperty(actor, `data.${accessor}`); - } else { - rollStat = (actor.getOwnedItem(itemId) as Skill).data.data; - } - - const rollArray = target.dataset.dice?.split(',').map(s => parseInt(s, 10)) || []; - const successTarget = rollStat.shade === "B" ? 3 : (rollStat.shade === "G" ? 2 : 1); - let reroll: Roll | null; - if (rollStat.open) { - // only reroll dice if there were any traitors - const numDice = rollArray.filter(r => r <= successTarget).length ? 1 : 0; - reroll = rollDice(numDice, false, rollStat.shade); - } else { - const numDice = rollArray.filter(s => s === 6).length; - reroll = rollDice(numDice, true, rollStat.shade); - } - - if (!reroll) { return; } - - if (actor.data.data.fate !== "0") { - if (target.dataset.rerollType === "stat") { - const fateSpent = parseInt(getProperty(actor, `data.${accessor}.fate`) || "0", 10); - const updateData = {}; - updateData[`${accessor}.fate`] = fateSpent + 1; - actor.update(updateData); - } else if (target.dataset.rerollType === "skill") { - const skill = actor.getOwnedItem(itemId) as Skill; - const fateSpent = parseInt(skill.data.data.fate, 10) || 0; - skill.update({ 'data.fate': fateSpent + 1 }, {}); - } - - const actorFateCount = parseInt(actor.data.data.fate, 10); - actor.update({ 'data.fate': actorFateCount -1 }); - } - - const successes = parseInt(target.dataset.successes || "0", 10); - const obstacleTotal = parseInt(target.dataset.difficulty || "0", 10); - const newSuccesses = parseInt(reroll.result, 10); - const success = (newSuccesses + successes) >= obstacleTotal; - const data: FateRerollMessageData = { - rolls: rollArray.map(r => { return { roll: r, success: r > successTarget }; }), - rerolls: reroll.dice[0].rolls, - successes, - obstacleTotal, - newSuccesses, - success - }; - const html = await renderTemplate(templates.rerollChatMessage, data); - return ChatMessage.create({ - content: html, - speaker: ChatMessage.getSpeaker({actor}) - }); -} - - -/* ================================================= */ -/* Private Roll Handlers */ -/* ================================================= */ -async function handleShrugRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - return handlePtgsRoll(target, sheet, true); -} -async function handleGritRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - return handlePtgsRoll(target, sheet, false); -} - -async function handlePtgsRoll(target: HTMLButtonElement, sheet: BWActorSheet, shrugging: boolean): Promise { - const actor = sheet.actor as BWActor; - const stat = getProperty(actor.data, "data.health" || "") as Ability; - const data: AttributeDialogData = { - name: shrugging ? "Shrug It Off" : "Grit Your Teeth", - difficulty: shrugging ? 2 : 4, - bonusDice: 0, - arthaDice: 0, - stat - }; - - const buttons: Record = {}; - buttons.roll = { - label: "Roll", - callback: async (dialogHtml: JQuery) => - ptgsRollCallback(dialogHtml, stat, sheet, shrugging) - }; - const updateData = {}; - const accessor = shrugging ? "data.ptgs.shrugging" : "data.ptgs.gritting"; - updateData[accessor] = true; - buttons.doIt = { - label: "Just do It", - callback: async (_: JQuery) => actor.update(updateData) - }; - - if (!shrugging && parseInt(actor.data.data.persona, 10)) { - // we're gritting our teeth and have persona points. give option - // to spend persona. - buttons.withPersona = { - label: "Spend Persona", - callback: async (_: JQuery) => { - updateData["data.persona"] = parseInt(actor.data.data.persona, 10) - 1; - updateData["data.health.persona"] = (parseInt(actor.data.data.health.persona, 10) || 0) + 1; - return actor.update(updateData); - } - }; - } - if (shrugging && parseInt(actor.data.data.fate, 10)) { - // we're shrugging it off and have fate points. give option - // to spend fate. - buttons.withFate = { - label: "Spend Fate", - callback: async (_: JQuery) => { - updateData["data.fate"] = parseInt(actor.data.data.fate, 10) - 1; - updateData["data.health.fate"] = (parseInt(actor.data.data.health.fate, 10) || 0) + 1; - return actor.update(updateData); - } - }; - } - - const html = await renderTemplate(templates.attrDialog, data); - return new Promise(_resolve => - new Dialog({ - title: `${target.dataset.rollableName} Test`, - content: html, - buttons - }).render(true) - ); -} - -async function ptgsRollCallback( - dialogHtml: JQuery, - stat: Ability, - sheet: BWActorSheet, - shrugging: boolean) { - const baseData = extractBaseData(dialogHtml, sheet); - const exp = parseInt(stat.exp, 10); - const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, 0, 0); - const dg = helpers.difficultyGroup(exp + baseData.bDice, baseData.diff); - const numDice = exp + baseData.bDice + baseData.aDice - baseData.woundDice; - - const roll = rollDice(numDice, stat.open, stat.shade); - if (!roll) { return; } - - const isSuccessful = parseInt(roll.result, 10) >= (baseData.diff); - - const data: RollChatMessageData = { - name: shrugging ? "Shrug It Off Health Test" : "Grit Your Teeth Health Test", - successes: roll.result, - difficulty: baseData.diff, - nameClass: getRollNameClass(stat.open, stat.shade), - obstacleTotal: baseData.obstacleTotal -= baseData.obPenalty, - success: isSuccessful, - rolls: roll.dice[0].rolls, - difficultyGroup: dg, - dieSources - }; - if (isSuccessful) { - const accessor = shrugging ? "data.ptgs.shrugging" : "data.ptgs.gritting"; - const updateData = {}; - updateData[accessor] = true; - sheet.actor.update(updateData); - } - sheet.actor.addAttributeTest(stat, "Health", "data.health", dg, isSuccessful); - const messageHtml = await renderTemplate(templates.attrMessage, data); - return ChatMessage.create({ - content: messageHtml, - speaker: ChatMessage.getSpeaker({actor: sheet.actor}) - }); -} - - -async function handleAttrRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - const stat = getProperty(sheet.actor.data, target.dataset.accessor || "") as Ability; - const actor = sheet.actor as BWActor; - const attrName = target.dataset.rollableName || "Unknown Attribute"; - let tax = 0; - if (attrName === "Resources") { - tax = parseInt(actor.data.data.resourcesTax, 10); - } - const data: AttributeDialogData = { - name: `${attrName}`, - difficulty: 3, - bonusDice: 0, - arthaDice: 0, - woundDice: attrName === "Steel" ? actor.data.data.ptgs.woundDice : undefined, - obPenalty: actor.data.data.ptgs.obPenalty, - tax, - stat, - }; - - const html = await renderTemplate(templates.attrDialog, data); - return new Promise(_resolve => - new Dialog({ - title: `${target.dataset.rollableName} Test`, - content: html, - buttons: { - roll: { - label: "Roll", - callback: async (dialogHtml: JQuery) => - attrRollCallback(dialogHtml, stat, sheet, tax, attrName, target.dataset.accessor || "") - } - } - }).render(true) - ); -} - -async function attrRollCallback( - dialogHtml: JQuery, - stat: Ability, - sheet: BWActorSheet, - tax: number, - name: string, - accessor: string) { - const baseData = extractBaseData(dialogHtml, sheet); - const exp = parseInt(stat.exp, 10); - const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, baseData.woundDice, tax); - const dg = helpers.difficultyGroup(exp + baseData.bDice - tax - baseData.woundDice, baseData.diff); - - const numDice = exp + baseData.bDice + baseData.aDice - baseData.woundDice - tax; - const roll = rollDice(numDice, stat.open, stat.shade); - if (!roll) { return; } - - const isSuccessful = parseInt(roll.result, 10) >= (baseData.diff + baseData.obPenalty); - - const fateReroll = buildFateRerollData(sheet.actor, roll, accessor); - const data: RollChatMessageData = { - name: `${name} Test`, - successes: roll.result, - difficulty: baseData.diff, - obstacleTotal: baseData.obstacleTotal, - nameClass: getRollNameClass(stat.open, stat.shade), - success: isSuccessful, - rolls: roll.dice[0].rolls, - difficultyGroup: dg, - penaltySources: baseData.penaltySources, - dieSources, - fateReroll - }; - - sheet.actor.addAttributeTest(stat, name, accessor, dg, isSuccessful); - const messageHtml = await renderTemplate(templates.attrMessage, data); - return ChatMessage.create({ - content: messageHtml, - speaker: ChatMessage.getSpeaker({actor: sheet.actor}) - }); -} - -async function handleCirclesRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - const stat = getProperty(sheet.actor.data, "data.circles") as Ability; - let circlesContact: Relationship | undefined; - if (target.dataset.relationshipId) { - circlesContact = sheet.actor.getOwnedItem(target.dataset.relationshipId) as Relationship; - } - const actor = sheet.actor as BWActor; - const data: CirclesDialogData = { - name: target.dataset.rollableName || "Circles Test", - difficulty: 3, - bonusDice: 0, - arthaDice: 0, - obPenalty: actor.data.data.ptgs.obPenalty, - stat, - circlesBonus: actor.data.circlesBonus, - circlesMalus: actor.data.circlesMalus, - circlesContact - }; - - const html = await renderTemplate(templates.circlesDialog, data); - return new Promise(_resolve => - new Dialog({ - title: `Circles Test`, - content: html, - buttons: { - roll: { - label: "Roll", - callback: async (dialogHtml: JQuery) => - circlesRollCallback(dialogHtml, stat, sheet, circlesContact) - } - } - }).render(true) - ); -} - -async function circlesRollCallback( - dialogHtml: JQuery, - stat: Ability, - sheet: BWActorSheet, - contact?: Relationship) { - const baseData = extractBaseData(dialogHtml, sheet); - const bonusData = extractCirclesBonuses(dialogHtml, "circlesBonus"); - const penaltyData = extractCirclesPenalty(dialogHtml, "circlesMalus"); - const exp = parseInt(stat.exp, 10); - const dieSources = { - ...buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, 0, 0), - ...bonusData.bonuses - }; - const dg = helpers.difficultyGroup( - exp + baseData.bDice, - baseData.diff + baseData.obPenalty + penaltyData.sum); - - if (contact) { - dieSources["Named Contact"] = "+1"; - baseData.bDice ++; - } - - const roll = rollDice(exp + baseData.bDice + baseData.aDice + bonusData.sum, stat.open, stat.shade); - if (!roll) { return; } - - const fateReroll = buildFateRerollData(sheet.actor, roll, "data.circles"); - - baseData.obstacleTotal += penaltyData.sum; - const data: RollChatMessageData = { - name: `Circles Test`, - successes: roll.result, - difficulty: baseData.diff, - obstacleTotal: baseData.obstacleTotal, - nameClass: getRollNameClass(stat.open, stat.shade), - success: parseInt(roll.result, 10) >= baseData.obstacleTotal, - rolls: roll.dice[0].rolls, - difficultyGroup: dg, - dieSources, - penaltySources: { ...baseData.penaltySources, ...penaltyData.bonuses }, - fateReroll - }; - const messageHtml = await renderTemplate(templates.circlesMessage, data); - - // incremet relationship tracking values... - if (contact && contact.data.data.building) { - contact.update({"data.buildingProgress": parseInt(contact.data.data.buildingProgress, 10) + 1 }, null); - } - - sheet.actor.addAttributeTest(stat, "Circles", "data.circles", dg, true); - - return ChatMessage.create({ - content: messageHtml, - speaker: ChatMessage.getSpeaker({actor: sheet.actor}) - }); -} - -async function handleLearningRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - const skillId = target.dataset.skillId || ""; - const skill = (sheet.actor.getOwnedItem(skillId) as Skill); - const actor = sheet.actor as BWActor; - const data: LearningDialogData = { - name: `Beginner's Luck ${target.dataset.rollableName} Test`, - difficulty: 3, - bonusDice: 0, - arthaDice: 0, - woundDice: actor.data.data.ptgs.woundDice, - obPenalty: actor.data.data.ptgs.obPenalty, - skill: { exp: 10 - (skill.data.data.aptitude || 1) } as any - }; - - const html = await renderTemplate(templates.learnDialog, data); - return new Promise(_resolve => - new Dialog({ - title: `${target.dataset.rollableName} Test`, - content: html, - buttons: { - roll: { - label: "Roll", - callback: async (dialogHtml: JQuery) => - learningRollCallback(dialogHtml, skill, sheet) - } - } - }).render(true) - ); -} - -async function learningRollCallback( - dialogHtml: JQuery, skill: Skill, sheet: BWActorSheet): Promise { - - const baseData = extractBaseData(dialogHtml, sheet); - baseData.obstacleTotal += baseData.diff; - baseData.penaltySources["Beginner's Luck"] = `+${baseData.diff}`; - const exp = 10 - (skill.data.data.aptitude || 1); - const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, baseData.woundDice, 0); - const dg = helpers.difficultyGroup(exp + baseData.bDice- baseData.woundDice, baseData.diff); - const rollSettings = getRootStatInfo(skill, sheet.actor); - - const roll = rollDice( - exp + baseData.bDice + baseData.aDice - baseData.woundDice, - rollSettings.open, - rollSettings.shade - ); - if (!roll) { return; } - const isSuccessful = parseInt(roll.result, 10) >= baseData.obstacleTotal; - const fateReroll = buildFateRerollData(sheet.actor, roll, undefined, skill._id); - if (fateReroll) { fateReroll!.type = "learning"; } - - const data: RollChatMessageData = { - name: `Beginner's Luck ${skill.data.name} Test`, - successes: roll.result, - difficulty: baseData.diff, - obstacleTotal: baseData.obstacleTotal, - nameClass: getRollNameClass(rollSettings.open, rollSettings.shade), - success: isSuccessful, - rolls: roll.dice[0].rolls, - difficultyGroup: dg, - penaltySources: baseData.penaltySources, - dieSources, - fateReroll - }; - const messageHtml = await renderTemplate(templates.learnMessage, data); - advanceLearning(skill, sheet.actor, dg, isSuccessful); - return ChatMessage.create({ - content: messageHtml, - speaker: ChatMessage.getSpeaker({actor: sheet.actor}) - }); -} - - -async function handleStatRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - const stat = getProperty(sheet.actor.data, target.dataset.accessor || "") as Ability; - const actor = sheet.actor as BWActor; - const statName = target.dataset.rollableName || "Unknown Stat"; - let tax = 0; - if (target.dataset.rollableName!.toLowerCase() === "will") { - tax = parseInt(actor.data.data.willTax, 10); - } - const data: StatDialogData = { - name: `${statName} Test`, - difficulty: 3, - bonusDice: 0, - arthaDice: 0, - woundDice: actor.data.data.ptgs.woundDice, - obPenalty: actor.data.data.ptgs.obPenalty, - stat, - tax - }; - - const html = await renderTemplate(templates.statDialog, data); - return new Promise(_resolve => - new Dialog({ - title: `${statName} Test`, - content: html, - buttons: { - roll: { - label: "Roll", - callback: async (dialogHtml: JQuery) => - statRollCallback(dialogHtml, stat, sheet, tax, statName, target.dataset.accessor || "") - } - } - }).render(true) - ); -} - -async function statRollCallback( - dialogHtml: JQuery, - stat: Ability, - sheet: BWActorSheet, - tax: number, - name: string, - accessor: string) { - const baseData = extractBaseData(dialogHtml, sheet); - const exp = parseInt(stat.exp, 10); - - const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, baseData.woundDice, tax); - const dg = helpers.difficultyGroup(exp + baseData.bDice - tax - baseData.woundDice, baseData.diff); - - const roll = rollDice( - exp + baseData.bDice + baseData.aDice - baseData.woundDice - tax, - stat.open, - stat.shade); - if (!roll) { return; } - const isSuccessful = parseInt(roll.result, 10) >= baseData.obstacleTotal; - - const fateReroll = buildFateRerollData(sheet.actor, roll, accessor); - - const data: RollChatMessageData = { - name: `${name} Test`, - successes: roll.result, - difficulty: baseData.diff + baseData.obPenalty, - obstacleTotal: baseData.obstacleTotal, - nameClass: getRollNameClass(stat.open, stat.shade), - success: isSuccessful, - rolls: roll.dice[0].rolls, - difficultyGroup: dg, - penaltySources: baseData.penaltySources, - dieSources, - fateReroll - }; - - sheet.actor.addStatTest(stat, name, accessor, dg, isSuccessful); - - const messageHtml = await renderTemplate(templates.skillMessage, data); - return ChatMessage.create({ - content: messageHtml, - speaker: ChatMessage.getSpeaker({actor: sheet.actor}) - }); -} - -async function handleSkillRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { - const skillId = target.dataset.skillId || ""; - const skill = (sheet.actor.getOwnedItem(skillId) as Skill); - const actor = sheet.actor as BWActor; - const templateData: SkillDialogData = { - name: skill.data.name, - difficulty: 3, - bonusDice: 0, - arthaDice: 0, - woundDice: actor.data.data.ptgs.woundDice, - obPenalty: actor.data.data.ptgs.obPenalty, - skill: skill.data.data, - forkOptions: actor.getForkOptions(skill.data.name) - }; - const html = await renderTemplate(templates.skillDialog, templateData); - return new Promise(_resolve => - new Dialog({ - title: `${skill.data.name} Test`, - content: html, - buttons: { - roll: { - label: "Roll", - callback: async (dialogHtml: JQuery) => - skillRollCallback(dialogHtml, skill, sheet) - } - } - }).render(true) - ); -} - -async function skillRollCallback( - dialogHtml: JQuery, skill: Skill, sheet: BWActorSheet): Promise { - - const forks = extractForksValue(dialogHtml, "forkOptions"); - const baseData = extractBaseData(dialogHtml, sheet); - const exp = parseInt(skill.data.data.exp, 10); - const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, forks, baseData.woundDice, 0); - const dg = helpers.difficultyGroup(exp + baseData.bDice + forks - baseData.woundDice, baseData.diff); - - const roll = rollDice( - exp + baseData.bDice + baseData.aDice + forks - baseData.woundDice, - skill.data.data.open, - skill.data.data.shade); - if (!roll) { return; } - const fateReroll = buildFateRerollData(sheet.actor, roll, undefined, skill._id); - - const data: RollChatMessageData = { - name: `${skill.name} Test`, - successes: roll.result, - difficulty: baseData.diff, - obstacleTotal: baseData.obstacleTotal, - nameClass: getRollNameClass(skill.data.data.open, skill.data.data.shade), - success: parseInt(roll.result, 10) >= baseData.obstacleTotal, - rolls: roll.dice[0].rolls, - difficultyGroup: dg, - penaltySources: baseData.penaltySources, - dieSources, - fateReroll - }; - - await helpers.addTestToSkill(skill, dg); - skill = sheet.actor.getOwnedItem(skill._id) as Skill; // update skill with new data - if (helpers.canAdvance(skill.data.data)) { - Dialog.confirm({ - title: `Advance ${skill.name}?`, - content: `

${skill.name} is ready to advance. Go ahead?

`, - yes: () => helpers.advanceSkill(skill), - // tslint:disable-next-line: no-empty - no: () => {}, - defaultYes: true - }); - } - - const messageHtml = await renderTemplate(templates.skillMessage, data); - return ChatMessage.create({ - content: messageHtml, - speaker: ChatMessage.getSpeaker({actor: sheet.actor}) - }); -} - - /* ================================================= */ /* Helper functions */ /* ================================================= */ -function buildDiceSourceObject( +export function buildDiceSourceObject( exp: number, aDice: number, bDice: number, @@ -627,7 +59,7 @@ function buildDiceSourceObject( return dieSources; } -function buildFateRerollData(actor: BWActor, roll: Roll, accessor?: string, itemId?: string): +export function buildFateRerollData(actor: BWActor, roll: Roll, accessor?: string, itemId?: string): FateRerollData | undefined { if (!parseInt(actor.data.data.fate, 10)) { return; @@ -651,7 +83,7 @@ function buildFateRerollData(actor: BWActor, roll: Roll, accessor?: string, item } } -function extractBaseData(html: JQuery, sheet: BWActorSheet ) { +export function extractBaseData(html: JQuery, sheet: BWActorSheet ) { const actorData = sheet.actor.data; const woundDice = extractNumber(html, "woundDice") || 0; const obPenalty = actorData.data.ptgs.obPenalty || 0; @@ -664,15 +96,15 @@ function extractBaseData(html: JQuery, sheet: BWActorSheet ) { return { woundDice, obPenalty, diff, aDice, bDice, penaltySources, obstacleTotal }; } -function extractString(html: JQuery, name: string): string { +export function extractString(html: JQuery, name: string): string { return html.find(`input[name=\"${name}\"]`).val() as string; } -function extractNumber(html: JQuery, name: string): number { +export function extractNumber(html: JQuery, name: string): number { return parseInt(extractString(html, name), 10); } -function extractForksValue(html: JQuery, name: string): number { +export function extractForksValue(html: JQuery, name: string): number { let sum: number = 0; html.find(`input[name=\"${name}\"]:checked`).each((_i, v) => { sum += parseInt(v.getAttribute("value") || "", 10); @@ -680,121 +112,7 @@ function extractForksValue(html: JQuery, name: string): number { return sum; } -function extractCirclesBonuses(html: JQuery, name: string): - { bonuses: {[name: string]: string }, sum: number} { - const bonuses:{[name: string]: string } = {}; - let sum = 0; - html.find(`input[name=\"${name}\"]:checked`).each((_i, v) => { - sum += parseInt(v.getAttribute("value") || "", 10); - bonuses[v.dataset.name || ""] = "+" + v.getAttribute("value"); - }); - return { bonuses, sum }; -} - -function extractCirclesPenalty(html: JQuery, name: string): - { bonuses: {[name: string]: string }, sum: number} { - return extractCirclesBonuses(html, name); -} - -async function advanceLearning( - skill: Skill, - owner: BWActor, - difficultyGroup: helpers.TestString, - isSuccessful: boolean) { - switch (difficultyGroup) { - default: - return advanceBaseStat(skill, owner, difficultyGroup, isSuccessful); - case "Routine": - return advanceLearningProgress(skill); - case "Routine/Difficult": - // we can either apply this to the base stat or to the learning - const dialog = new Dialog({ - title: "Pick where to assing the test", - content: "

This test can count as routine of difficult for the purposes of advancement

Pick which option you'd prefer.

", - buttons: { - skill: { - label: "Apply as Routine", - callback: async () => advanceLearningProgress(skill) - }, - stat: { - label: "Apply as Difficult", - callback: async () => advanceBaseStat(skill, owner, "Difficult", isSuccessful) - } - } - }); - return dialog.render(true); - } -} - -async function advanceBaseStat( - skill: Skill, - owner: BWActor, - difficultyGroup: helpers.TestString, - isSuccessful: boolean) { - if (!skill.data.data.root2) { - // we can immediately apply the test to the one root stat. - const rootName = skill.data.data.root1; - const accessor = `data.${rootName.toLowerCase()}`; - const rootStat = getProperty(owner, `data.${accessor}`); - await owner.addStatTest(rootStat, rootName, accessor, difficultyGroup, isSuccessful); - return skill.update({}, {}); // force refresh in case the base stat changes. - } - - // otherwise we have 2 roots and we let the player pick one. - const choice = new Dialog({ - title: "Pick root stat to advance", - content: `

This test can count towards advancing ${skill.data.data.root1} or ${skill.data.data.root2}

Which one to advance?

`, - buttons: { - stat1: { - label: skill.data.data.root1, - callback: async () => { - const rootName = skill.data.data.root1.titleCase(); - const accessor = `data.${rootName.toLowerCase()}`; - const rootStat = getProperty(owner, `data.${accessor}`); - await owner.addStatTest( - rootStat, rootName, `${accessor}`, difficultyGroup, isSuccessful); - return skill.update({}, {}); // force refresh in case the base stat changes. - } - }, - stat2: { - label: skill.data.data.root2, - callback: async () => { - const rootName = skill.data.data.root2.titleCase(); - const accessor = `data.${rootName.toLowerCase()}`; - const rootStat = getProperty(owner, `data.${accessor}`); - await owner.addStatTest( - rootStat, rootName, `${accessor}`, difficultyGroup, isSuccessful); - return skill.update({}, {}); // force refresh in case the base stat changes. - } - } - } - }); - return choice.render(true); -} - -async function advanceLearningProgress(skill: Skill) { - const progress = parseInt(skill.data.data.learningProgress, 10); - const requiredTests = skill.data.data.aptitude || 10; - - skill.update({"data.learningProgress": progress + 1 }, {}); - if (progress + 1 >= requiredTests) { - Dialog.confirm({ - title: `Finish Training ${skill.name}?`, - content: `

${skill.name} is ready to become a full skill. Go ahead?

`, - yes: () => { - const updateData = {}; - updateData["data.learning"] = false; - updateData["data.exp"] = Math.floor((10 - requiredTests) / 2); - skill.update(updateData, {}); - }, - // tslint:disable-next-line: no-empty - no: () => {}, - defaultYes: true - }); - } -} - -function rollDice(numDice: number, open: boolean = false, shade: helpers.ShadeString = 'B'): Roll | null { +export function rollDice(numDice: number, open: boolean = false, shade: helpers.ShadeString = 'B'): Roll | null { if (numDice <= 0) { getNoDiceErrorDialog(numDice); return null; @@ -804,7 +122,7 @@ function rollDice(numDice: number, open: boolean = false, shade: helpers.ShadeSt } } -function getRootStatInfo(skill: Skill, actor: BWActor): { open: boolean, shade: helpers.ShadeString } { +export function getRootStatInfo(skill: Skill, actor: BWActor): { open: boolean, shade: helpers.ShadeString } { const root1 = getProperty(actor, `data.data.${skill.data.data.root1}`) as Ability; const root2 = skill.data.data.root2 ? getProperty(actor, `data.data.${skill.data.data.root2}`) as Ability : root1; @@ -823,7 +141,7 @@ function getRootStatInfo(skill: Skill, actor: BWActor): { open: boolean, shade: }; } -function getRollNameClass(open: boolean, shade: helpers.ShadeString): string { +export function getRollNameClass(open: boolean, shade: helpers.ShadeString): string { let css = "shade-black"; if (shade === "G") { css = "shade-grey"; @@ -837,7 +155,7 @@ function getRollNameClass(open: boolean, shade: helpers.ShadeString): string { return css; } -async function getNoDiceErrorDialog(numDice: number) { +export async function getNoDiceErrorDialog(numDice: number) { return new Dialog({ title: "Too Few Dice", content: `

Too few dice to be rolled. Must roll a minimum of one. Currently, bonuses and penalties add up to ${numDice}

`, @@ -850,7 +168,7 @@ async function getNoDiceErrorDialog(numDice: number) { } /* ============ Constants =============== */ -const templates = { +export const templates = { attrDialog: "systems/burningwheel/templates/chat/roll-dialog.html", attrMessage: "systems/burningwheel/templates/chat/roll-message.html", circlesDialog: "systems/burningwheel/templates/chat/circles-dialog.html", @@ -870,28 +188,12 @@ export interface LearningDialogData extends RollDialogData { skill: SkillDataRoot; } -export interface CirclesDialogData extends AttributeDialogData { - circlesBonus?: {name: string, amount: number}[]; - circlesMalus?: {name: string, amount: number}[]; - circlesContact?: Item; -} - export interface AttributeDialogData extends RollDialogData { stat: TracksTests; tax?: number; } -export interface StatDialogData extends RollDialogData { - tax?: number; - stat: TracksTests; -} - -export interface SkillDialogData extends RollDialogData { - skill: TracksTests; - forkOptions: { name: string, amount: number }[]; -} - -interface RollDialogData { +export interface RollDialogData { name: string; difficulty: number; arthaDice: number; @@ -916,19 +218,12 @@ export interface RollChatMessageData { fateReroll?: FateRerollData; } -export interface FateRerollMessageData { - rolls: { roll: number, success: boolean }[]; - rerolls: { roll: number, success: boolean }[]; - success: boolean; - successes: number; - newSuccesses: number; - obstacleTotal: number; -} - export interface FateRerollData { dice: string; actorId: string; type?: "stat" | "skill" | "learning"; + learningTarget?: string; // for reroll, which attribute to apply the fate to. + ptgsAction?: string; itemId?: string; accessor?: string; } \ No newline at end of file diff --git a/module/rolls/rerollFate.ts b/module/rolls/rerollFate.ts new file mode 100644 index 00000000..e3312e1b --- /dev/null +++ b/module/rolls/rerollFate.ts @@ -0,0 +1,123 @@ +import { TestString } from "module/helpers.js"; +import { Ability, BWActor, TracksTests } from "../actor.js"; +import { Skill, SkillData } from "../items/item.js"; +import { rollDice, templates } from "../rolls.js"; + +export async function handleFateReroll(target: HTMLButtonElement): Promise { + const actor = game.actors.get(target.dataset.actorId || "") as BWActor; + const accessor = target.dataset.accessor || ''; + const name = target.dataset.rollName || ''; + const itemId = target.dataset.itemId || ''; + const rollArray = target.dataset.dice?.split(',').map(s => parseInt(s, 10)) || []; + const successes = parseInt(target.dataset.successes || "0", 10); + const obstacleTotal = parseInt(target.dataset.difficulty || "0", 10); + + let rollStat: Ability | SkillData; + if (target.dataset.rerollType === "stat") { + rollStat = getProperty(actor, `data.${accessor}`); + } else { + rollStat = (actor.getOwnedItem(itemId) as Skill).data.data; + } + + const successTarget = rollStat.shade === "B" ? 3 : (rollStat.shade === "G" ? 2 : 1); + + let reroll: Roll | null; + if (rollStat.open) { + // only reroll dice if there were any traitors + const numDice = rollArray.filter(r => r <= successTarget).length ? 1 : 0; + reroll = rollDice(numDice, false, rollStat.shade); + } else { + const numDice = rollArray.filter(s => s === 6).length; + reroll = rollDice(numDice, true, rollStat.shade); + } + + if (!reroll) { return; } + const newSuccesses = parseInt(reroll.result, 10); + const success = (newSuccesses + successes) >= obstacleTotal; + + if (actor.data.data.fate !== "0") { + if (target.dataset.rerollType === "stat") { + const fateSpent = parseInt(getProperty(actor, `data.${accessor}.fate`) || "0", 10); + const updateData = {}; + updateData[`${accessor}.fate`] = fateSpent + 1; + if (successes <= obstacleTotal && success) { + // we turned a failure into a success. we might need to retroactively award xp. + if (target.dataset.ptgsAction) { // shrug/grit flags may need to be set. + updateData[`data.ptgs.${target.dataset.ptgsAction}`] = true; + } + if (name === "Faith" || name === "Resources") { + actor.addAttributeTest( + getProperty(actor, `data.${accessor}`) as TracksTests, + name, + accessor, + target.dataset.difficultyGroup as TestString, + true); + } + if (name === "Perception") { + actor.addStatTest( + getProperty(actor, `data.${accessor}`) as TracksTests, + name, + accessor, + target.dataset.difficultyGroup as TestString, + true); + } + } + actor.update(updateData); + } else if (target.dataset.rerollType === "skill") { + const skill = actor.getOwnedItem(itemId) as Skill; + const fateSpent = parseInt(skill.data.data.fate, 10) || 0; + skill.update({ 'data.fate': fateSpent + 1 }, {}); + } else if (target.dataset.rerollType === "learning") { + const learningTarget = target.dataset.learningTarget || 'skill'; + const skill = actor.getOwnedItem(itemId) as Skill; + if (learningTarget === 'skill') { + // learning roll went to the root skill + const fateSpent = parseInt(skill.data.data.fate, 10) || 0; + skill.update({'data.fate': fateSpent + 1 }, {}); + } else { + if (successes <= obstacleTotal && success) { + if (learningTarget === "perception") { + actor.addStatTest( + getProperty(actor, "data.data.perception") as TracksTests, + "Perception", + "data.perception", + target.dataset.difficultyGroup as TestString, + true); + } + } + const rootAccessor = `data.${learningTarget}.fate`; + const rootStatFate = parseInt(getProperty(actor, `data.${rootAccessor}`), 10) || 0; + const updateData = {}; + updateData[rootAccessor] = rootStatFate + 1; + actor.update(updateData); + } + } + + const actorFateCount = parseInt(actor.data.data.fate, 10); + actor.update({ 'data.fate': actorFateCount -1 }); + } + + const data: FateRerollMessageData = { + rolls: rollArray.map(r => { return { roll: r, success: r > successTarget }; }), + rerolls: reroll.dice[0].rolls, + successes, + obstacleTotal, + newSuccesses, + success + }; + const html = await renderTemplate(templates.rerollChatMessage, data); + return ChatMessage.create({ + content: html, + speaker: ChatMessage.getSpeaker({actor}) + }); +} + + +export interface FateRerollMessageData { + rolls: { roll: number, success: boolean }[]; + rerolls: { roll: number, success: boolean }[]; + success: boolean; + successes: number; + newSuccesses: number; + obstacleTotal: number; +} diff --git a/module/rolls/rollAttribute.ts b/module/rolls/rollAttribute.ts new file mode 100644 index 00000000..f253086f --- /dev/null +++ b/module/rolls/rollAttribute.ts @@ -0,0 +1,89 @@ +import { Ability, BWActor } from "module/actor.js"; +import { BWActorSheet } from "module/bwactor-sheet.js"; +import * as helpers from "../helpers.js"; +import { + AttributeDialogData, + buildDiceSourceObject, + buildFateRerollData, + extractBaseData, + getRollNameClass, + RollChatMessageData, + rollDice, + templates +} from "../rolls.js"; + +export async function handleAttrRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + const stat = getProperty(sheet.actor.data, target.dataset.accessor || "") as Ability; + const actor = sheet.actor as BWActor; + const attrName = target.dataset.rollableName || "Unknown Attribute"; + let tax = 0; + if (attrName === "Resources") { + tax = parseInt(actor.data.data.resourcesTax, 10); + } + const data: AttributeDialogData = { + name: `${attrName} Test`, + difficulty: 3, + bonusDice: 0, + arthaDice: 0, + woundDice: attrName === "Steel" ? actor.data.data.ptgs.woundDice : undefined, + obPenalty: actor.data.data.ptgs.obPenalty, + tax, + stat, + }; + + const html = await renderTemplate(templates.attrDialog, data); + return new Promise(_resolve => + new Dialog({ + title: `${target.dataset.rollableName} Test`, + content: html, + buttons: { + roll: { + label: "Roll", + callback: async (dialogHtml: JQuery) => + attrRollCallback(dialogHtml, stat, sheet, tax, attrName, target.dataset.accessor || "") + } + } + }).render(true) + ); +} + +async function attrRollCallback( + dialogHtml: JQuery, + stat: Ability, + sheet: BWActorSheet, + tax: number, + name: string, + accessor: string) { + const baseData = extractBaseData(dialogHtml, sheet); + const exp = parseInt(stat.exp, 10); + const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, baseData.woundDice, tax); + const dg = helpers.difficultyGroup(exp + baseData.bDice - tax - baseData.woundDice, baseData.diff); + + const numDice = exp + baseData.bDice + baseData.aDice - baseData.woundDice - tax; + const roll = rollDice(numDice, stat.open, stat.shade); + if (!roll) { return; } + + const isSuccessful = parseInt(roll.result, 10) >= (baseData.diff + baseData.obPenalty); + + const fateReroll = buildFateRerollData(sheet.actor, roll, accessor); + const data: RollChatMessageData = { + name: `${name}`, + successes: roll.result, + difficulty: baseData.diff, + obstacleTotal: baseData.obstacleTotal, + nameClass: getRollNameClass(stat.open, stat.shade), + success: isSuccessful, + rolls: roll.dice[0].rolls, + difficultyGroup: dg, + penaltySources: baseData.penaltySources, + dieSources, + fateReroll + }; + + sheet.actor.addAttributeTest(stat, name, accessor, dg, isSuccessful); + const messageHtml = await renderTemplate(templates.attrMessage, data); + return ChatMessage.create({ + content: messageHtml, + speaker: ChatMessage.getSpeaker({actor: sheet.actor}) + }); +} \ No newline at end of file diff --git a/module/rolls/rollCircles.ts b/module/rolls/rollCircles.ts new file mode 100644 index 00000000..c2e436a2 --- /dev/null +++ b/module/rolls/rollCircles.ts @@ -0,0 +1,127 @@ +import { Ability, BWActor } from "module/actor.js"; +import { BWActorSheet } from "module/bwactor-sheet.js"; +import { Relationship } from "module/items/item.js"; +import * as helpers from "../helpers.js"; +import { + AttributeDialogData, + buildDiceSourceObject, + buildFateRerollData, + extractBaseData, + getRollNameClass, + RollChatMessageData, + rollDice, + templates +} from "../rolls.js"; + +export async function handleCirclesRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + const stat = getProperty(sheet.actor.data, "data.circles") as Ability; + let circlesContact: Relationship | undefined; + if (target.dataset.relationshipId) { + circlesContact = sheet.actor.getOwnedItem(target.dataset.relationshipId) as Relationship; + } + const actor = sheet.actor as BWActor; + const data: CirclesDialogData = { + name: target.dataset.rollableName || "Circles Test", + difficulty: 3, + bonusDice: 0, + arthaDice: 0, + obPenalty: actor.data.data.ptgs.obPenalty, + stat, + circlesBonus: actor.data.circlesBonus, + circlesMalus: actor.data.circlesMalus, + circlesContact + }; + + const html = await renderTemplate(templates.circlesDialog, data); + return new Promise(_resolve => + new Dialog({ + title: `Circles Test`, + content: html, + buttons: { + roll: { + label: "Roll", + callback: async (dialogHtml: JQuery) => + circlesRollCallback(dialogHtml, stat, sheet, circlesContact) + } + } + }).render(true) + ); +} + +async function circlesRollCallback( + dialogHtml: JQuery, + stat: Ability, + sheet: BWActorSheet, + contact?: Relationship) { + const baseData = extractBaseData(dialogHtml, sheet); + const bonusData = extractCirclesBonuses(dialogHtml, "circlesBonus"); + const penaltyData = extractCirclesPenalty(dialogHtml, "circlesMalus"); + const exp = parseInt(stat.exp, 10); + const dieSources = { + ...buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, 0, 0), + ...bonusData.bonuses + }; + const dg = helpers.difficultyGroup( + exp + baseData.bDice, + baseData.diff + baseData.obPenalty + penaltyData.sum); + + if (contact) { + dieSources["Named Contact"] = "+1"; + baseData.bDice ++; + } + + const roll = rollDice(exp + baseData.bDice + baseData.aDice + bonusData.sum, stat.open, stat.shade); + if (!roll) { return; } + + const fateReroll = buildFateRerollData(sheet.actor, roll, "data.circles"); + + baseData.obstacleTotal += penaltyData.sum; + const data: RollChatMessageData = { + name: `Circles`, + successes: roll.result, + difficulty: baseData.diff, + obstacleTotal: baseData.obstacleTotal, + nameClass: getRollNameClass(stat.open, stat.shade), + success: parseInt(roll.result, 10) >= baseData.obstacleTotal, + rolls: roll.dice[0].rolls, + difficultyGroup: dg, + dieSources, + penaltySources: { ...baseData.penaltySources, ...penaltyData.bonuses }, + fateReroll + }; + const messageHtml = await renderTemplate(templates.circlesMessage, data); + + // incremet relationship tracking values... + if (contact && contact.data.data.building) { + contact.update({"data.buildingProgress": parseInt(contact.data.data.buildingProgress, 10) + 1 }, null); + } + + sheet.actor.addAttributeTest(stat, "Circles", "data.circles", dg, true); + + return ChatMessage.create({ + content: messageHtml, + speaker: ChatMessage.getSpeaker({actor: sheet.actor}) + }); +} + +function extractCirclesBonuses(html: JQuery, name: string): + { bonuses: {[name: string]: string }, sum: number} { + const bonuses:{[name: string]: string } = {}; + let sum = 0; + html.find(`input[name=\"${name}\"]:checked`).each((_i, v) => { + sum += parseInt(v.getAttribute("value") || "", 10); + bonuses[v.dataset.name || ""] = "+" + v.getAttribute("value"); + }); + return { bonuses, sum }; +} + +function extractCirclesPenalty(html: JQuery, name: string): + { bonuses: {[name: string]: string }, sum: number} { + return extractCirclesBonuses(html, name); +} + +export interface CirclesDialogData extends AttributeDialogData { + circlesBonus?: {name: string, amount: number}[]; + circlesMalus?: {name: string, amount: number}[]; + circlesContact?: Item; +} \ No newline at end of file diff --git a/module/rolls/rollLearning.ts b/module/rolls/rollLearning.ts new file mode 100644 index 00000000..6aee4c7a --- /dev/null +++ b/module/rolls/rollLearning.ts @@ -0,0 +1,203 @@ +import { BWActor } from "module/actor.js"; +import { BWActorSheet } from "module/bwactor-sheet.js"; +import { Skill } from "module/items/item.js"; +import * as helpers from "../helpers.js"; +import { + buildDiceSourceObject, + buildFateRerollData, + extractBaseData, + FateRerollData, + getRollNameClass, + getRootStatInfo, + LearningDialogData, + RollChatMessageData, + rollDice, + templates +} from "../rolls.js"; + +export async function handleLearningRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + const skillId = target.dataset.skillId || ""; + const skill = (sheet.actor.getOwnedItem(skillId) as Skill); + const actor = sheet.actor as BWActor; + const data: LearningDialogData = { + name: `Beginner's Luck ${target.dataset.rollableName} Test`, + difficulty: 3, + bonusDice: 0, + arthaDice: 0, + woundDice: actor.data.data.ptgs.woundDice, + obPenalty: actor.data.data.ptgs.obPenalty, + skill: { exp: 10 - (skill.data.data.aptitude || 1) } as any + }; + + const html = await renderTemplate(templates.learnDialog, data); + return new Promise(_resolve => + new Dialog({ + title: `${target.dataset.rollableName}`, + content: html, + buttons: { + roll: { + label: "Roll", + callback: async (dialogHtml: JQuery) => + learningRollCallback(dialogHtml, skill, sheet) + } + } + }).render(true) + ); +} + +async function learningRollCallback( + dialogHtml: JQuery, skill: Skill, sheet: BWActorSheet): Promise { + + const baseData = extractBaseData(dialogHtml, sheet); + baseData.obstacleTotal += baseData.diff; + baseData.penaltySources["Beginner's Luck"] = `+${baseData.diff}`; + const exp = 10 - (skill.data.data.aptitude || 1); + const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, baseData.woundDice, 0); + const dg = helpers.difficultyGroup(exp + baseData.bDice- baseData.woundDice, baseData.diff); + const rollSettings = getRootStatInfo(skill, sheet.actor); + + const roll = rollDice( + exp + baseData.bDice + baseData.aDice - baseData.woundDice, + rollSettings.open, + rollSettings.shade + ); + if (!roll) { return; } + const isSuccessful = parseInt(roll.result, 10) >= baseData.obstacleTotal; + const fateReroll = buildFateRerollData(sheet.actor, roll, undefined, skill._id); + if (fateReroll) { fateReroll!.type = "learning"; } + + const sendChatMessage = async (fr?: FateRerollData) => { + const data: RollChatMessageData = { + name: `Beginner's Luck ${skill.data.name}`, + successes: roll.result, + difficulty: baseData.diff, + obstacleTotal: baseData.obstacleTotal, + nameClass: getRollNameClass(rollSettings.open, rollSettings.shade), + success: isSuccessful, + rolls: roll.dice[0].rolls, + difficultyGroup: dg, + penaltySources: baseData.penaltySources, + dieSources, + fateReroll: fr + }; + const messageHtml = await renderTemplate(templates.learnMessage, data); + return ChatMessage.create({ + content: messageHtml, + speaker: ChatMessage.getSpeaker({actor: sheet.actor}) + }); + }; + + return advanceLearning(skill, sheet.actor, dg, isSuccessful, fateReroll, sendChatMessage); +} + +async function advanceLearning( + skill: Skill, + owner: BWActor, + difficultyGroup: helpers.TestString, + isSuccessful: boolean, + fr: FateRerollData | undefined, + cb: (fr?: FateRerollData) => Promise) { + switch (difficultyGroup) { + default: + return advanceBaseStat(skill, owner, difficultyGroup, isSuccessful, fr, cb); + case "Routine": + return advanceLearningProgress(skill, fr, cb); + case "Routine/Difficult": + // we can either apply this to the base stat or to the learning + const dialog = new Dialog({ + title: "Pick where to assing the test", + content: "

This test can count as routine of difficult for the purposes of advancement

Pick which option you'd prefer.

", + buttons: { + skill: { + label: "Apply as Routine", + callback: async () => advanceLearningProgress(skill, fr, cb) + }, + stat: { + label: "Apply as Difficult", + callback: async () => advanceBaseStat(skill, owner, "Difficult", isSuccessful, fr, cb) + } + } + }); + return dialog.render(true); + } +} + +async function advanceBaseStat( + skill: Skill, + owner: BWActor, + difficultyGroup: helpers.TestString, + isSuccessful: boolean, + fr: FateRerollData | undefined, + cb: (fr?: FateRerollData) => Promise) { + if (!skill.data.data.root2) { + // we can immediately apply the test to the one root stat. + const rootName = skill.data.data.root1; + const accessor = `data.${rootName.toLowerCase()}`; + const rootStat = getProperty(owner, `data.${accessor}`); + await owner.addStatTest(rootStat, rootName, accessor, difficultyGroup, isSuccessful); + if (fr) { fr.learningTarget = skill.data.data.root1; } + return cb(fr); + } + + // otherwise we have 2 roots and we let the player pick one. + const choice = new Dialog({ + title: "Pick root stat to advance", + content: `

This test can count towards advancing ${skill.data.data.root1} or ${skill.data.data.root2}

Which one to advance?

`, + buttons: { + stat1: { + label: skill.data.data.root1, + callback: async () => { + const rootName = skill.data.data.root1.titleCase(); + const accessor = `data.${rootName.toLowerCase()}`; + const rootStat = getProperty(owner, `data.${accessor}`); + await owner.addStatTest( + rootStat, rootName, `${accessor}`, difficultyGroup, isSuccessful); + if (fr) { fr.learningTarget = skill.data.data.root1; } + return cb(fr); + } + }, + stat2: { + label: skill.data.data.root2, + callback: async () => { + const rootName = skill.data.data.root2.titleCase(); + const accessor = `data.${rootName.toLowerCase()}`; + const rootStat = getProperty(owner, `data.${accessor}`); + await owner.addStatTest( + rootStat, rootName, `${accessor}`, difficultyGroup, isSuccessful); + if (fr) { fr.learningTarget = skill.data.data.root2; } + return cb(fr); + } + } + } + }); + return choice.render(true); +} + +async function advanceLearningProgress( + skill: Skill, + fr: FateRerollData | undefined, + cb: (fr?: FateRerollData) => Promise) { + const progress = parseInt(skill.data.data.learningProgress, 10); + const requiredTests = skill.data.data.aptitude || 10; + + skill.update({"data.learningProgress": progress + 1 }, {}); + if (progress + 1 >= requiredTests) { + Dialog.confirm({ + title: `Finish Training ${skill.name}?`, + content: `

${skill.name} is ready to become a full skill. Go ahead?

`, + yes: () => { + const updateData = {}; + updateData["data.learning"] = false; + updateData["data.exp"] = Math.floor((10 - requiredTests) / 2); + skill.update(updateData, {}); + }, + // tslint:disable-next-line: no-empty + no: () => {}, + defaultYes: true + }); + } + if (fr) { + fr.learningTarget = "skill"; + } + return cb(fr); +} \ No newline at end of file diff --git a/module/rolls/rollPtgs.ts b/module/rolls/rollPtgs.ts new file mode 100644 index 00000000..d2925bf6 --- /dev/null +++ b/module/rolls/rollPtgs.ts @@ -0,0 +1,124 @@ +import { Ability, BWActor } from "../actor.js"; +import { BWActorSheet } from "../bwactor-sheet.js"; +import * as helpers from "../helpers.js"; +import { + AttributeDialogData, + buildDiceSourceObject, + buildFateRerollData, + extractBaseData, + getRollNameClass, + RollChatMessageData, + rollDice, + templates +} from "../rolls.js"; + +export async function handleShrugRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + return handlePtgsRoll(target, sheet, true); +} +export async function handleGritRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + return handlePtgsRoll(target, sheet, false); +} + +async function handlePtgsRoll(target: HTMLButtonElement, sheet: BWActorSheet, shrugging: boolean): Promise { + const actor = sheet.actor as BWActor; + const stat = getProperty(actor.data, "data.health" || "") as Ability; + const data: AttributeDialogData = { + name: shrugging ? "Shrug It Off" : "Grit Your Teeth", + difficulty: shrugging ? 2 : 4, + bonusDice: 0, + arthaDice: 0, + stat + }; + + const buttons: Record = {}; + buttons.roll = { + label: "Roll", + callback: async (dialogHtml: JQuery) => + ptgsRollCallback(dialogHtml, stat, sheet, shrugging) + }; + const updateData = {}; + const accessor = shrugging ? "data.ptgs.shrugging" : "data.ptgs.gritting"; + updateData[accessor] = true; + buttons.doIt = { + label: "Just do It", + callback: async (_: JQuery) => actor.update(updateData) + }; + + if (!shrugging && parseInt(actor.data.data.persona, 10)) { + // we're gritting our teeth and have persona points. give option + // to spend persona. + buttons.withPersona = { + label: "Spend Persona", + callback: async (_: JQuery) => { + updateData["data.persona"] = parseInt(actor.data.data.persona, 10) - 1; + updateData["data.health.persona"] = (parseInt(actor.data.data.health.persona, 10) || 0) + 1; + return actor.update(updateData); + } + }; + } + if (shrugging && parseInt(actor.data.data.fate, 10)) { + // we're shrugging it off and have fate points. give option + // to spend fate. + buttons.withFate = { + label: "Spend Fate", + callback: async (_: JQuery) => { + updateData["data.fate"] = parseInt(actor.data.data.fate, 10) - 1; + updateData["data.health.fate"] = (parseInt(actor.data.data.health.fate, 10) || 0) + 1; + return actor.update(updateData); + } + }; + } + + const html = await renderTemplate(templates.attrDialog, data); + return new Promise(_resolve => + new Dialog({ + title: `${target.dataset.rollableName} Test`, + content: html, + buttons + }).render(true) + ); +} + +async function ptgsRollCallback( + dialogHtml: JQuery, + stat: Ability, + sheet: BWActorSheet, + shrugging: boolean) { + const baseData = extractBaseData(dialogHtml, sheet); + const exp = parseInt(stat.exp, 10); + const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, 0, 0); + const dg = helpers.difficultyGroup(exp + baseData.bDice, baseData.diff); + const numDice = exp + baseData.bDice + baseData.aDice - baseData.woundDice; + + const roll = rollDice(numDice, stat.open, stat.shade); + if (!roll) { return; } + + const isSuccessful = parseInt(roll.result, 10) >= (baseData.diff); + const fateReroll = buildFateRerollData(sheet.actor, roll, "data.health"); + if (fateReroll) { fateReroll.ptgsAction = shrugging? "shrugging" : "gritting"; } + + const data: RollChatMessageData = { + name: shrugging ? "Shrug It Off Health" : "Grit Your Teeth Health", + successes: roll.result, + difficulty: baseData.diff, + nameClass: getRollNameClass(stat.open, stat.shade), + obstacleTotal: baseData.obstacleTotal -= baseData.obPenalty, + success: isSuccessful, + rolls: roll.dice[0].rolls, + difficultyGroup: dg, + dieSources, + fateReroll + }; + if (isSuccessful) { + const accessor = shrugging ? "data.ptgs.shrugging" : "data.ptgs.gritting"; + const updateData = {}; + updateData[accessor] = true; + sheet.actor.update(updateData); + } + sheet.actor.addAttributeTest(stat, "Health", "data.health", dg, isSuccessful); + const messageHtml = await renderTemplate(templates.attrMessage, data); + return ChatMessage.create({ + content: messageHtml, + speaker: ChatMessage.getSpeaker({actor: sheet.actor}) + }); +} \ No newline at end of file diff --git a/module/rolls/rollSkill.ts b/module/rolls/rollSkill.ts new file mode 100644 index 00000000..a8fbd627 --- /dev/null +++ b/module/rolls/rollSkill.ts @@ -0,0 +1,100 @@ +import { BWActor, TracksTests } from "../actor.js"; +import { BWActorSheet } from "../bwactor-sheet.js"; +import * as helpers from "../helpers.js"; +import { Skill } from "../items/item.js"; +import { + buildDiceSourceObject, + buildFateRerollData, + extractBaseData, + extractForksValue, + getRollNameClass, + RollChatMessageData, + RollDialogData, + rollDice, + templates +} from "../rolls.js"; + +export async function handleSkillRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + const skillId = target.dataset.skillId || ""; + const skill = (sheet.actor.getOwnedItem(skillId) as Skill); + const actor = sheet.actor as BWActor; + const templateData: SkillDialogData = { + name: skill.data.name, + difficulty: 3, + bonusDice: 0, + arthaDice: 0, + woundDice: actor.data.data.ptgs.woundDice, + obPenalty: actor.data.data.ptgs.obPenalty, + skill: skill.data.data, + forkOptions: actor.getForkOptions(skill.data.name) + }; + const html = await renderTemplate(templates.skillDialog, templateData); + return new Promise(_resolve => + new Dialog({ + title: `${skill.data.name} Test`, + content: html, + buttons: { + roll: { + label: "Roll", + callback: async (dialogHtml: JQuery) => + skillRollCallback(dialogHtml, skill, sheet) + } + } + }).render(true) + ); +} + +async function skillRollCallback( + dialogHtml: JQuery, skill: Skill, sheet: BWActorSheet): Promise { + + const forks = extractForksValue(dialogHtml, "forkOptions"); + const baseData = extractBaseData(dialogHtml, sheet); + const exp = parseInt(skill.data.data.exp, 10); + const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, forks, baseData.woundDice, 0); + const dg = helpers.difficultyGroup(exp + baseData.bDice + forks - baseData.woundDice, baseData.diff); + + const roll = rollDice( + exp + baseData.bDice + baseData.aDice + forks - baseData.woundDice, + skill.data.data.open, + skill.data.data.shade); + if (!roll) { return; } + const fateReroll = buildFateRerollData(sheet.actor, roll, undefined, skill._id); + + const data: RollChatMessageData = { + name: `${skill.name}`, + successes: roll.result, + difficulty: baseData.diff, + obstacleTotal: baseData.obstacleTotal, + nameClass: getRollNameClass(skill.data.data.open, skill.data.data.shade), + success: parseInt(roll.result, 10) >= baseData.obstacleTotal, + rolls: roll.dice[0].rolls, + difficultyGroup: dg, + penaltySources: baseData.penaltySources, + dieSources, + fateReroll + }; + + await helpers.addTestToSkill(skill, dg); + skill = sheet.actor.getOwnedItem(skill._id) as Skill; // update skill with new data + if (helpers.canAdvance(skill.data.data)) { + Dialog.confirm({ + title: `Advance ${skill.name}?`, + content: `

${skill.name} is ready to advance. Go ahead?

`, + yes: () => helpers.advanceSkill(skill), + // tslint:disable-next-line: no-empty + no: () => {}, + defaultYes: true + }); + } + + const messageHtml = await renderTemplate(templates.skillMessage, data); + return ChatMessage.create({ + content: messageHtml, + speaker: ChatMessage.getSpeaker({actor: sheet.actor}) + }); +} + +interface SkillDialogData extends RollDialogData { + skill: TracksTests; + forkOptions: { name: string, amount: number }[]; +} \ No newline at end of file diff --git a/module/rolls/rollStat.ts b/module/rolls/rollStat.ts new file mode 100644 index 00000000..e201dc17 --- /dev/null +++ b/module/rolls/rollStat.ts @@ -0,0 +1,98 @@ +import { Ability, BWActor, TracksTests } from "../actor.js"; +import { BWActorSheet } from "../bwactor-sheet.js"; +import * as helpers from "../helpers.js"; +import { + buildDiceSourceObject, + buildFateRerollData, + extractBaseData, + getRollNameClass, + RollChatMessageData, + RollDialogData, + rollDice, + templates +} from "../rolls.js"; + +export async function handleStatRoll(target: HTMLButtonElement, sheet: BWActorSheet): Promise { + const stat = getProperty(sheet.actor.data, target.dataset.accessor || "") as Ability; + const actor = sheet.actor as BWActor; + const statName = target.dataset.rollableName || "Unknown Stat"; + let tax = 0; + if (target.dataset.rollableName!.toLowerCase() === "will") { + tax = parseInt(actor.data.data.willTax, 10); + } + const data: StatDialogData = { + name: `${statName} Test`, + difficulty: 3, + bonusDice: 0, + arthaDice: 0, + woundDice: actor.data.data.ptgs.woundDice, + obPenalty: actor.data.data.ptgs.obPenalty, + stat, + tax + }; + + const html = await renderTemplate(templates.statDialog, data); + return new Promise(_resolve => + new Dialog({ + title: `${statName} Test`, + content: html, + buttons: { + roll: { + label: "Roll", + callback: async (dialogHtml: JQuery) => + statRollCallback(dialogHtml, stat, sheet, tax, statName, target.dataset.accessor || "") + } + } + }).render(true) + ); +} + +async function statRollCallback( + dialogHtml: JQuery, + stat: Ability, + sheet: BWActorSheet, + tax: number, + name: string, + accessor: string) { + const baseData = extractBaseData(dialogHtml, sheet); + const exp = parseInt(stat.exp, 10); + + const dieSources = buildDiceSourceObject(exp, baseData.aDice, baseData.bDice, 0, baseData.woundDice, tax); + const dg = helpers.difficultyGroup(exp + baseData.bDice - tax - baseData.woundDice, baseData.diff); + + const roll = rollDice( + exp + baseData.bDice + baseData.aDice - baseData.woundDice - tax, + stat.open, + stat.shade); + if (!roll) { return; } + const isSuccessful = parseInt(roll.result, 10) >= baseData.obstacleTotal; + + const fateReroll = buildFateRerollData(sheet.actor, roll, accessor); + + const data: RollChatMessageData = { + name: `${name}`, + successes: roll.result, + difficulty: baseData.diff + baseData.obPenalty, + obstacleTotal: baseData.obstacleTotal, + nameClass: getRollNameClass(stat.open, stat.shade), + success: isSuccessful, + rolls: roll.dice[0].rolls, + difficultyGroup: dg, + penaltySources: baseData.penaltySources, + dieSources, + fateReroll + }; + + sheet.actor.addStatTest(stat, name, accessor, dg, isSuccessful); + + const messageHtml = await renderTemplate(templates.skillMessage, data); + return ChatMessage.create({ + content: messageHtml, + speaker: ChatMessage.getSpeaker({actor: sheet.actor}) + }); +} + +interface StatDialogData extends RollDialogData { + tax?: number; + stat: TracksTests; +} \ No newline at end of file diff --git a/templates/chat/roll-message.html b/templates/chat/roll-message.html index 394b4b61..a4a111ae 100644 --- a/templates/chat/roll-message.html +++ b/templates/chat/roll-message.html @@ -1,6 +1,6 @@
- {{name}} + {{name}} Test
{{difficultyGroup}} @@ -60,8 +60,11 @@ data-actor-id="{{fateReroll.actorId}}" data-reroll-type="{{fateReroll.type}}" data-dice="{{fateReroll.dice}}" + data-roll-name="{{name}}" {{#if fateReroll.itemId}} data-item-id="{{fateReroll.itemId}}" {{/if}} {{#if fateReroll.accessor}} data-accessor="{{fateReroll.accessor}}" {{/if}} + {{#if fateReroll.learningTarget}} data-learning-target="{{fateReroll.learningTarget}}" {{/if}} + {{#if fateReroll.ptgsAction}} data-ptgs-action="{{fateReroll.ptgsAction}}" {{/if}} data-difficulty="{{obstacleTotal}}" data-successes="{{successes}}" data-difficulty-group="{{difficultyGroup}}">Reroll with Fate