Skip to content

Commit

Permalink
DFRPG Slam calculation is incorrect in /slam #2053
Browse files Browse the repository at this point in the history
  • Loading branch information
mjeffw committed Dec 15, 2024
1 parent 7049e94 commit e4ff128
Show file tree
Hide file tree
Showing 11 changed files with 3,601 additions and 4,637 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

- Bugfix: The "roll for me" option does not work #2010
- Bugfix: Fix bl unit for high strength characters #2041
- Bugfix: DFRPG Slam calculation is incorrect in /slam #2053

Release 0.17.16 11/29/2024

Expand Down
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
transform: {
'^.+\\.jsx?$': 'babel-jest',
},
transformIgnorePatterns: ['/node_modules/'],
}
27 changes: 3 additions & 24 deletions lib/ranges.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

import * as Settings from '../lib/miscellaneous-settings.js'
import RepeatingSequenceConverter from '../module/utilities/repeating-sequence.js'
import { SizeAndSpeedRangeTable } from './size-speed-range-table.js'
import { i18n, i18n_f } from './utilities.js'

/*
Expand Down Expand Up @@ -38,8 +38,8 @@ import { i18n, i18n_f } from './utilities.js'
*/

export class RulerGURPS extends Ruler {
_getSegmentLabel(segment, totalDistance) {
totalDistance ??= this.totalDistance;
_getSegmentLabel(segment, totalDistance) {
totalDistance ??= this.totalDistance
const units = canvas.scene.grid.units
let dist = (d, u) => {
return `${Math.round(d * 100) / 100} ${u}`
Expand Down Expand Up @@ -247,27 +247,6 @@ export class GURPSRange {
}
}

class SizeAndSpeedRangeTable {
constructor() {
this._table = new RepeatingSequenceConverter([2, 3, 5, 7, 10, 15])
}

// pass in distance in yards, get back modifier
getModifier(yards) {
return -this._table.valueToIndex(yards)
}

// pass in modifier, get distance in yards
getDistance(modifier) {
return this._table.indexToValue(modifier)
}

// pass in distance in yards, get back the furthest distance that has the same modifier
ceil(yards) {
return this._table.ceil(measure)
}
}

// Must be kept in order... checking range vs Max. If >Max, go to next entry.
/* Example code:
for (let range of GURPS.ranges) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
'use strict'

export class SizeAndSpeedRangeTable {
constructor() {
this._table = new RepeatingSequenceConverter([2, 3, 5, 7, 10, 15])
}

// pass in distance in yards, get back modifier
getModifier(yards) {
return -this._table.valueToIndex(yards)
}

// pass in modifier, get distance in yards
getDistance(modifier) {
return this._table.indexToValue(modifier)
}

// pass in distance in yards, get back the furthest distance that has the same modifier
ceil(yards) {
return this._table.ceil(measure)
}
}

/// This class handles a common pattern in GURPS -- a point cost that follows a
/// pattern, that repeats perhaps infinitely, which the individual values in
/// the pattern being multiplied by 10 over the previous occurrence of the
Expand Down
160 changes: 69 additions & 91 deletions module/chat/slam-calc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {
generateUniqueId,
isNiceDiceEnabled,
i18n,
i18n_f,
diceToFormula as diceToFormula,
} from '../../lib/utilities.js'
import { diceToFormula } from '../../lib/utilities.js'
import { addDice, getDiceData, getDicePlusAdds } from '../utilities/damage-utils.js'

const effects = {
unaffected: {
Expand All @@ -31,11 +26,12 @@ const effects = {

export class SlamCalculator {
constructor(dependencies) {
if (!!dependencies) {
this._generateUniqueId = dependencies.generateUniqueId
} else {
this._generateUniqueId = generateUniqueId
}
this._generateUniqueId = dependencies?.generateUniqueId
this._ssrt = dependencies?.sizeAndSpeedRangeTable
this._isDiceSoNiceEnabled = dependencies?.isNiceDiceEnabled
this._roll = dependencies?.roll
this._i18n = dependencies?.i18n
this._i18n_f = dependencies?.i18n_f
}

/*
Expand All @@ -52,54 +48,22 @@ export class SlamCalculator {
shieldDB: {Number},
*/
async process(data) {
// If you hit, you and your foe each inflict dice of
// crushing damage on the other equal to (HP x velocity)/100.
let rawDamageAttacker = (data.attackerHp * data.relativeSpeed) / 100
let attackerDice = this._getDicePlusAdds(rawDamageAttacker)

let rawDamageTarget = (data.targetHp * data.relativeSpeed) / 100
let targetDice = this._getDicePlusAdds(rawDamageTarget)

let attackerAdds = (data.isAoAStrong ? 2 : 0) + (data.shieldDB || 0)
let targetAdds = -(data.shieldDB || 0)

let velocityAdd = 0

if (data.useDFRPGRules) {
let thr = data.attackerThr
let diceMatch = thr.match(/(\d+)d(.*)/i)
if (!diceMatch) {
ui.notifications.warn('Attacker Thrust damage (' + thr + ") does not include 'd'")
return
}
attackerDice = { dice: +diceMatch[1], adds: +diceMatch[2] || 0 }
thr = data.targetThr
diceMatch = thr.match(/(\d+)d(.*)/i)
if (!diceMatch) {
ui.notifications.warn('Target Thrust damage (' + thr + ") does not include 'd'")
return
}
targetDice = { dice: +diceMatch[1], adds: +diceMatch[2] || 0 }
velocityAdd = -2 // combined speed 1
if (data.relativeSpeed >= 2) velocityAdd = -GURPS.SSRT.getModifier(data.relativeSpeed) // convert range mod to size mod
attackerAdds += velocityAdd * attackerDice.dice
targetAdds += velocityAdd * targetDice.dice
}
const slam = this._getSlamData(data)

let attackerRoll = Roll.create(diceToFormula(attackerDice, `[Slam Attacker's roll]`, true))
let attackerRoll = this._roll.create(diceToFormula(slam.attackerDice, `[Slam Attacker's roll]`, true))
await attackerRoll.evaluate()

let attackerMin = false
let attackerResult = attackerRoll.total + attackerAdds
let attackerResult = attackerRoll.total + slam.attackerAdds
if (attackerResult < 1) {
attackerResult = 1
attackerMin = true
}

let targetRoll = Roll.create(diceToFormula(targetDice, `[Slam Defender's roll]`, true))
let targetRoll = this._roll.create(diceToFormula(slam.targetDice, `[Slam Defender's roll]`, true))
await targetRoll.evaluate()
let targetMin = false
let targetResult = targetRoll.total + targetAdds
let targetResult = targetRoll.total + slam.targetAdds
if (targetResult < 1) {
targetResult = 1
targetMin = true
Expand Down Expand Up @@ -127,30 +91,36 @@ export class SlamCalculator {
resultData.isRealTarget = true
}

let result = i18n_f(resultData.effect.i18n, resultData)
let result = this._i18n_f(resultData.effect.i18n, resultData)
result = resultData.effect.createButton(result, resultData)

let html = await renderTemplate('systems/gurps/templates/slam-results.html', {
id: this._generateUniqueId(),
attacker: data.attackerToken?.name,
attackerHp: data.attackerHp,
attackerRaw: rawDamageAttacker,
attackerDice: attackerDice,
attackerRaw: slam.rawDamageAttacker,
attackerDice: slam.attackerDice,
attackerResult: attackerResult,
attackerExplain: this.explainDieRoll(
attackerRoll,
data.isAoAStrong,
data.shieldDB,
velocityAdd * attackerDice.dice,
slam.velocityAdd * slam.attackerDice.dice,
attackerMin
),
// ---
target: data.targetToken.name,
targetHp: data.targetHp,
targetRaw: rawDamageTarget,
targetDice: targetDice,
targetRaw: slam.rawDamageTarget,
targetDice: slam.targetDice,
targetResult: targetResult,
targetExplain: this.explainDieRoll(targetRoll, false, -data.shieldDB, velocityAdd * targetDice.dice, targetMin),
targetExplain: this.explainDieRoll(
targetRoll,
false,
-data.shieldDB,
slam.velocityAdd * slam.targetDice.dice,
targetMin
),
// ---
effect: resultData.effect,
isAoAStrong: data.isAoAStrong,
Expand All @@ -163,9 +133,7 @@ export class SlamCalculator {
// const speaker = { alias: attacker.name, _id: attacker._id, actor: attacker }
let messageData = {
user: game.user.id,
// speaker: speaker,
content: html,
type: CONST.CHAT_MESSAGE_STYLES.ROLL,
roll: JSON.stringify(attackerRoll),
sound: this.rollThemBones([targetRoll]),
}
Expand All @@ -182,6 +150,42 @@ export class SlamCalculator {
})
}

_getSlamData(data) {
let attackerAdds = (data.isAoAStrong ? 2 : 0) + (data.shieldDB || 0)
let attackerDice, targetDice
let targetAdds = 0
let rawDamageAttacker, rawDamageTarget
let velocityAdd = 0

if (data.useDFRPGRules) {
targetAdds = -data.shieldDB || 0
attackerDice = getDiceData(addDice(data.attackerThr, -2))
targetDice = getDiceData(addDice(data.targetThr, -2))
velocityAdd = 0 // combined speed 1
if (data.relativeSpeed >= 2) velocityAdd = -this._ssrt.getModifier(data.relativeSpeed) // convert range mod to size mod
attackerAdds += velocityAdd * attackerDice.dice
targetAdds += velocityAdd * targetDice.dice
} else {
// If you hit, you and your foe each inflict dice of
// crushing damage on the other equal to (HP x velocity)/100.
rawDamageAttacker = (data.attackerHp * data.relativeSpeed) / 100
attackerDice = getDicePlusAdds(rawDamageAttacker)

rawDamageTarget = (data.targetHp * data.relativeSpeed) / 100
targetDice = getDicePlusAdds(rawDamageTarget)
}

return {
attackerDice: attackerDice,
attackerAdds: attackerAdds,
rawDamageAttacker: rawDamageAttacker,
targetDice: targetDice,
targetAdds: targetAdds,
rawDamageTarget: rawDamageTarget,
velocityAdd: velocityAdd,
}
}

targetFallsDown(attackerResult, targetResult) {
return attackerResult >= targetResult * 2
}
Expand All @@ -194,38 +198,12 @@ export class SlamCalculator {
return attackerResult > targetResult && !this.targetFallsDown(attackerResult, targetResult)
}

/**
* Calculate the dice roll from the rawDamage value.
*
* @param {Number} rawDamage
* @returns an Object literal with two attributes: dice (integer) and adds (integer)
*/
_getDicePlusAdds(rawDamage) {
// If damage is less than 1d, ...
if (rawDamage < 1) {
// treat fractions up to 0.25 as 1d-3, ...
if (rawDamage <= 0.25) return { dice: 1, adds: -3 }

// fractions up to 0.5 as 1d-2, ...
if (rawDamage <= 0.5) return { dice: 1, adds: -2 }

// and any larger fraction as 1d-1.
return { dice: 1, adds: -1 }
}

// Otherwise, round fractions of 0.5 or more up to a full die.
let dice = Math.floor(rawDamage)
if (rawDamage - dice >= 0.5) dice++

return { dice: dice, adds: 0 }
}

/**
*
* @param {Aray<Roll>} rollArray
*/
rollThemBones(rollArray) {
if (!isNiceDiceEnabled()) return CONFIG.sounds.dice
if (!this._isDiceSoNiceEnabled()) return CONFIG?.sounds.dice

if (!Array.isArray(rollArray)) {
rollArray = [rollArray]
Expand All @@ -248,7 +226,7 @@ export class SlamCalculator {
})

if (dice.length > 0) {
game.dice3d.show({ throws: [{ dice: dice }] })
game?.dice3d.show({ throws: [{ dice: dice }] })
}
}

Expand All @@ -258,14 +236,14 @@ export class SlamCalculator {
let results = resultsArray.map(it => it.result)

let explanation =
roll.terms.length > 1 ? `${i18n('GURPS.rolled')} (${results})` : `${i18n('GURPS.rolled')} ${results}`
roll.terms.length > 1 ? `${this._i18n('GURPS.rolled')} (${results})` : `${this._i18n('GURPS.rolled')} ${results}`
if (roll.terms[2]?.number !== '0') explanation += `${roll.terms[1].formula}${roll.terms[2].formula}`

if (!!isAoAStrong) explanation += ` + 2 (${i18n('GURPS.slamAOAStrong')})`
if (!!isAoAStrong) explanation += ` + 2 (${this._i18n('GURPS.slamAOAStrong')})`
let sign = shieldDB >= 0 ? '+' : '-'
if (!!shieldDB) explanation += ` ${sign} ${Math.abs(shieldDB)} (${i18n('GURPS.slamShieldDB')})`
if (!!velocity) explanation += ` + ${velocity} ${i18n('GURPS.slamRelativeVelocity')}`
if (min) explanation += ` (${i18n('GURPS.minimum')} 1)`
if (!!shieldDB) explanation += ` ${sign} ${Math.abs(shieldDB)} (${this._i18n('GURPS.slamShieldDB')})`
if (!!velocity) explanation += ` + ${velocity} ${this._i18n('GURPS.slamRelativeVelocity')}`
if (min) explanation += ` (${this._i18n('GURPS.minimum')} 1)`
return explanation
}
}
Loading

0 comments on commit e4ff128

Please sign in to comment.