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.
 
vore-rpg/src/battlers/HitPoints.ts

314 lines
15 KiB

import {assertion} from "../testing/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
})
}