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
} )
}