You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
15 KiB
314 lines
15 KiB
import {assertion} from "./Assertions";
|
|
|
|
/**
|
|
* HitPointsWithoutConfidence contains the hit points data without the current confidence as is needed for calculating
|
|
* the confidence limit itself. It's impossible to check whether a hit points object is valid without checking its
|
|
* confidence limit, and when calculating said confidence limit, this would lead to endless recursion.
|
|
*/
|
|
export type HitPointsWithoutConfidence = Omit<HitPoints, "currentConfidence">
|
|
|
|
/** Formats the HitPointsWithoutConfidence object into a human-readable string for logging. Should never throw. */
|
|
export function hitPointsWithoutConfidenceToString(hp: HitPointsWithoutConfidence): string {
|
|
return `{Health: ${hp.currentHealth}/${hp.maxHealth}, Confidence: -/-/${hp.maxConfidence}}`
|
|
}
|
|
|
|
/**
|
|
* The full data for a character's hit points - i.e., their counters that limit their survival, and which are primarily
|
|
* impacted by the opponent's attacks.
|
|
*/
|
|
export interface HitPoints {
|
|
/** The maximum Health the character can have, which they recover to when resting. Valid values are integers between 1 and 99999. */
|
|
readonly maxHealth: number
|
|
/** The current Health the character has, after any damage taken since the last rest. Valid values are integers between -maxHealth and maxHealth. */
|
|
readonly currentHealth: number
|
|
/**
|
|
* The maximum Confidence the character can have when maxHealth = currentHealth.
|
|
* Below this, the confidence limit is scaled down proportionately.
|
|
* Valid values are integers between 1 and 99999.
|
|
*/
|
|
readonly maxConfidence: number
|
|
/** The current Confidence the character has, after any damage taken since the last victorious battle. Between 0 and confidenceLimit(this). */
|
|
readonly currentConfidence: number
|
|
}
|
|
|
|
/** Formats the HitPoints object into a human-readable string for logging. Should never throw. */
|
|
export function hitPointsToString(hp: HitPoints): string {
|
|
let confidenceLimitString: string
|
|
try {
|
|
confidenceLimitString = confidenceLimit(hp).toString()
|
|
} catch (e) {
|
|
confidenceLimitString = "<err>"
|
|
}
|
|
return `{Health: ${hp.currentHealth}/${hp.maxHealth}, Confidence: ${hp.currentConfidence}/${confidenceLimitString}/${hp.maxConfidence}}`
|
|
}
|
|
|
|
/** Returns true if the value given is valid for maxHealth or maxConfidence. See their documentation for what that means. */
|
|
export function isValidMaxHitPoints(maxHitPoints: number): boolean {
|
|
return maxHitPoints > 0 && maxHitPoints <= 99999 && Number.isSafeInteger(maxHitPoints)
|
|
}
|
|
|
|
/** Returns true if the value given is valid for currentHealth or currentConfidence. See their documentation for what that means. */
|
|
export function isValidCurrentHitPoints(currentHitPoints: number, maxHitPoints: number, minHitPoints: number) {
|
|
return currentHitPoints <= maxHitPoints && currentHitPoints >= minHitPoints && Number.isSafeInteger(currentHitPoints)
|
|
}
|
|
|
|
/** Returns true if the given HitPointsWithoutConfidence object lives up to all its requirements. See the HitPoints documentation for what that means. */
|
|
export function isValidHitPointsWithoutConfidence(hp: HitPointsWithoutConfidence): boolean {
|
|
return isValidMaxHitPoints(hp.maxHealth)
|
|
&& isValidMaxHitPoints(hp.maxConfidence)
|
|
&& isValidCurrentHitPoints(hp.currentHealth, hp.maxHealth, -hp.maxHealth)
|
|
}
|
|
|
|
/** Returns true if the given HitPoints object lives up to all its requirements. See the HitPoints documentation for what that means. */
|
|
export function isValidHitPoints(hp: HitPoints): boolean {
|
|
return isValidHitPointsWithoutConfidence(hp)
|
|
&& isValidCurrentHitPoints(hp.currentConfidence, confidenceLimit(hp), 0)
|
|
}
|
|
|
|
/** Returns true if the given confidenceLimit value is at least plausible. See the confidenceLimit documentation for what that means.*/
|
|
export function isValidConfidenceLimit(limit: number, hp: HitPointsWithoutConfidence): boolean {
|
|
return (hp.currentHealth > 0 ? limit >= 0 : limit === 0)
|
|
&& (hp.currentHealth === hp.maxHealth ? limit === hp.maxConfidence : limit < hp.maxConfidence)
|
|
&& Number.isSafeInteger(limit)
|
|
}
|
|
|
|
/** Asserts that the given value is valid for maxHealth or maxConfidence. */
|
|
export function checkValidMaxHitPoints(maxHitPoints: number): number {
|
|
return assertion.check(maxHitPoints, isValidMaxHitPoints, (maxHitPoints) => `Invalid max hit points: ${maxHitPoints}`)
|
|
}
|
|
|
|
/** Asserts that the given value is valid for currentHealth or currentConfidence. */
|
|
export function checkValidCurrentHitPoints(currentHitPoints: number, maxHitPoints: number, minHitPoints: number): number {
|
|
return assertion.check(currentHitPoints,
|
|
(currentHitPoints) => isValidCurrentHitPoints(currentHitPoints, maxHitPoints, minHitPoints),
|
|
(currentHitPoints) => `Invalid current hit points: ${minHitPoints} <= ${currentHitPoints} <= ${maxHitPoints}`)
|
|
}
|
|
|
|
/** Asserts that the given HitPointsWithoutConfidence object lives up to all its requirements. */
|
|
export function checkValidHitPointsWithoutConfidence(hp: HitPointsWithoutConfidence): HitPointsWithoutConfidence {
|
|
return assertion.check(hp, isValidHitPointsWithoutConfidence, (hp) => `Invalid hit points: ${hitPointsWithoutConfidenceToString(hp)}`)
|
|
}
|
|
|
|
/** Asserts that the given HitPoints object lives up to all its requirements. */
|
|
export function checkValidHitPoints(hp: HitPoints): HitPoints {
|
|
return assertion.check(hp, isValidHitPoints, (hp) => `Invalid hit points object: ${hitPointsToString(hp)}`)
|
|
}
|
|
|
|
/** Asserts that the given confidenceLimit value is at least plausible. */
|
|
export function checkValidConfidenceLimit(limit: number, hp: HitPointsWithoutConfidence): number {
|
|
return assertion.check(limit, (limit) => isValidConfidenceLimit(limit, hp), (limit) => {
|
|
return `Invalid confidence limit: ${limit} for ${hp.currentHealth}/${hp.maxHealth} Health, ${hp.maxConfidence} Max Confidence`
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Asserts that the given change in health has resulted in a congruent change in confidence - i.e., that the new
|
|
* confidence is the same distance below the new confidence limit as the old confidence was below the old one.
|
|
* A delta of 1 is permitted due to the fact that the confidence change will be rounded (up in the case of damage,
|
|
* down in the case of healing).
|
|
*
|
|
* Used by the damageHealth and recoverHealth functions.
|
|
*/
|
|
export function checkConsistentConfidenceDelta(newConfidenceUnclamped: number, action: string, oldHp: HitPoints, newHealth: number): number {
|
|
return assertion.check(newConfidenceUnclamped, (newConfidenceUnclamped) => {
|
|
return Math.abs((confidenceLimit({...oldHp, currentHealth: newHealth}) - newConfidenceUnclamped) - (confidenceLimit(oldHp) - oldHp.currentConfidence)) <= 1
|
|
}, (newConfidenceUnclamped) => {
|
|
const newLimit = confidenceLimit({...oldHp, currentHealth: newHealth})
|
|
const oldLimit = confidenceLimit(oldHp)
|
|
return (`${action} from ${hitPointsToString(oldHp)} to ${hitPointsToString({...oldHp, currentHealth: newHealth, currentConfidence: newConfidenceUnclamped})} `
|
|
+ `resulted in a change in the difference between the confidenceLimit and the currentConfidenceUnclamped `
|
|
+ `from ${oldHp.currentConfidence}/${oldLimit} = ${oldLimit - oldHp.currentConfidence} `
|
|
+ `to ${newConfidenceUnclamped}/${newLimit} = ${newLimit - newConfidenceUnclamped}`)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* The effective maximum confidence, based on scaling maxConfidence by the character's current health percentage.
|
|
* Always an integer, rounding down if the result is fractional.
|
|
* Is 0 if currentHealth <= 0.
|
|
* Is maxConfidence if and only if currentHealth === maxHealth, due to rounding down (floor).
|
|
* Otherwise, scaled to somewhere between them.
|
|
*/
|
|
export function confidenceLimit(hp: HitPointsWithoutConfidence): number {
|
|
checkValidHitPointsWithoutConfidence(hp)
|
|
|
|
const result = Math.max(0, Math.floor(hp.currentHealth * hp.maxConfidence / hp.maxHealth))
|
|
|
|
return checkValidConfidenceLimit(result, hp)
|
|
}
|
|
|
|
/**
|
|
* Inflicts the given amount of damage on the character's health.
|
|
* The character's health will never be reduced below -maxHealth this way.
|
|
* If damageConfidence is true, confidence will be reduced proportionately.
|
|
* e.g., if Confidence is 50/80/100 and Health is 40/50, damaging Health by 10 with damageConfidence off
|
|
* results in Health 30/50 and Confidence 50/60/100 - the currentConfidence has not changed.
|
|
* On the other hand, with damageConfidence on, that same scenario
|
|
* results in Health 30/50 and Confidence 30/60/100 - the currentConfidence has fallen by 20 due to scaling the
|
|
* 10 Health damage to 20 Confidence damage. The character's confidence will not be reduced below 0 this way.
|
|
* If damageConfidence is off, currentConfidence will still change if the health change causes the confidenceLimit to
|
|
* drop below the currentConfidence. In this case, currentConfidence will be clamped to the new confidenceLimit.
|
|
*/
|
|
export function damageHealth(hp: HitPoints, damage: number, options: {damageConfidence: boolean}): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
assertion.checkPositiveIntegerOrZero(damage, (damage) => `Tried to damage health by negative or non-integer value ${damage}`)
|
|
|
|
const {damageConfidence} = options
|
|
let currentHealth = Math.max(-hp.maxHealth,hp.currentHealth - damage)
|
|
let currentConfidence = damageConfidence
|
|
? Math.max(0,
|
|
checkConsistentConfidenceDelta(
|
|
hp.currentConfidence - Math.ceil((Math.max(0, hp.currentHealth) - Math.max(0, currentHealth)) * hp.maxConfidence / hp.maxHealth),
|
|
"damageHealth",
|
|
hp,
|
|
currentHealth))
|
|
: Math.min(hp.currentConfidence, confidenceLimit({
|
|
...hp,
|
|
currentHealth: currentHealth
|
|
}))
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
currentHealth,
|
|
currentConfidence,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Heals the character's health by the given amount.
|
|
* The character's health will never be increased above maxHealth this way.
|
|
* If recoverConfidence is true, confidence will be increased proportionately to the increase in health.
|
|
* e.g., if Confidence is 50/60/100 and Health is 30/50, recovering Health by 10 with recoverConfidence off
|
|
* results in Health 40/50 and Confidence 50/80/100 - the currentConfidence has not changed.
|
|
* On the other hand, with recoverConfidence on, that same scenario
|
|
* results in Health 40/50 and Confidence 70/80/100 - the currentConfidence has risen by 20 due to scaling the
|
|
* 10 Health recovery to 20 Confidence recovery.
|
|
*/
|
|
export function recoverHealth(hp: HitPoints, healing: number, options: {recoverConfidence: boolean}): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
assertion.checkPositiveIntegerOrZero(healing, (healing) => `Tried to heal health by negative or non-integer value ${healing}`)
|
|
|
|
const {recoverConfidence} = options
|
|
const currentHealth = Math.min(hp.maxHealth, hp.currentHealth + healing)
|
|
// We don't need to check against the confidenceLimit here. When recoverConfidence is on, we're raising confidence
|
|
// by the same amount the confidenceLimit went up. So if it was under the limit before, it should be the same amount
|
|
// under the limit now. And if recoverConfidence is off, we're not raising confidence at all, so we're even more
|
|
// under the limit than we were before.
|
|
// The assertion checks that the former case holds.
|
|
const currentConfidence = recoverConfidence
|
|
? checkConsistentConfidenceDelta(
|
|
hp.currentConfidence + Math.floor((Math.max(0, currentHealth) - Math.max(0, hp.currentHealth)) * hp.maxConfidence / hp.maxHealth),
|
|
"recoverHealth",
|
|
hp,
|
|
currentHealth)
|
|
: hp.currentConfidence
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
currentHealth,
|
|
currentConfidence,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Sets the character's new maximum health.
|
|
* If the new value is below the character's current absolute-value health, the character's current health will be clamped to that value.
|
|
* If the new value is above the character's previous maximum health, the character's confidence will be clamped to the new confidence limit, which will be lower.
|
|
*/
|
|
export function setMaxHealth(hp: HitPoints, maxHealth: number): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
checkValidMaxHitPoints(maxHealth)
|
|
|
|
let {currentHealth, currentConfidence} = hp
|
|
if (maxHealth <= Math.abs(currentHealth)) {
|
|
currentHealth = Math.sign(currentHealth) * maxHealth
|
|
}
|
|
if (maxHealth > hp.maxHealth) {
|
|
currentConfidence = Math.min(currentConfidence, confidenceLimit({...hp, maxHealth}))
|
|
}
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
maxHealth,
|
|
currentHealth,
|
|
currentConfidence
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Sets the character's new current health.
|
|
* The character's confidence will be clamped to the new confidence limit.
|
|
*/
|
|
export function setCurrentHealth(hp: HitPoints, currentHealth: number): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
checkValidCurrentHitPoints(currentHealth, hp.maxHealth, -hp.maxHealth)
|
|
|
|
const currentConfidence = Math.min(confidenceLimit({...hp, currentHealth}), hp.currentConfidence)
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
currentHealth,
|
|
currentConfidence
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Inflicts the given amount of damage on the character's confidence.
|
|
* The character's confidence will never be reduced below 0 this way.
|
|
*/
|
|
export function damageConfidence(hp: HitPoints, damage: number): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
assertion.checkPositiveIntegerOrZero(damage, (damage) => `Tried to damage confidence by negative or non-integer value ${damage}`)
|
|
|
|
let newConfidence = Math.max(0, hp.currentConfidence - damage)
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
currentConfidence: newConfidence,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Heals the given amount of damage to the character's confidence.
|
|
* The character's confidence will never be increased above the current confidenceLimit this way.
|
|
*/
|
|
export function recoverConfidence(hp: HitPoints, healing: number): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
assertion.checkPositiveIntegerOrZero(healing, (healing) => `Tried to heal confidence by negative or non-integer value ${healing}`)
|
|
|
|
let newConfidence = Math.min(confidenceLimit(hp), hp.currentConfidence + healing)
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
currentConfidence: newConfidence,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Sets the character's new maximum confidence.
|
|
* The current confidence will be clamped to the new confidenceLimit.
|
|
*/
|
|
export function setMaxConfidence(hp: HitPoints, maxConfidence: number): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
checkValidMaxHitPoints(maxConfidence)
|
|
|
|
let {currentConfidence} = hp
|
|
currentConfidence = Math.min(currentConfidence, confidenceLimit({...hp, maxConfidence}))
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
maxConfidence,
|
|
currentConfidence
|
|
})
|
|
}
|
|
|
|
/** Sets the character's new current confidence. */
|
|
export function setCurrentConfidence(hp: HitPoints, currentConfidence: number): HitPoints {
|
|
checkValidHitPoints(hp)
|
|
checkValidCurrentHitPoints(currentConfidence, confidenceLimit(hp), 0)
|
|
|
|
return checkValidHitPoints({
|
|
...hp,
|
|
currentConfidence
|
|
})
|
|
} |