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 /** 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 = "" } 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 }) }