Add Assertions and HitPoints. We're making this project happen!

main
Mari 3 years ago
parent 9d02647cdd
commit 84c6744080
  1. 6
      .idea/inspectionProfiles/Project_Default.xml
  2. 11
      .idea/runConfigurations/All_Tests.xml
  3. 6
      .idea/vcs.xml
  4. 174
      src/battlers/Assertions.test.ts
  5. 58
      src/battlers/Assertions.ts
  6. 558
      src/battlers/HitPoints.test.ts
  7. 314
      src/battlers/HitPoints.ts

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="JavaScriptTestRunnerJest" nameIsGenerated="true">
<node-interpreter value="project" />
<node-options value="" />
<jest-package value="$PROJECT_DIR$/node_modules/react-scripts" />
<working-dir value="$PROJECT_DIR$" />
<envs />
<scope-kind value="ALL" />
<method v="2" />
</configuration>
</component>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,174 @@
import {AssertionConfig, AssertionMode, Assertions} from "./Assertions";
describe("Assertion", () => {
describe("check", () => {
const defaults = {
value: Symbol("test value"),
config: AssertionConfig.Skip,
conditionImpl: () => true,
messageImpl: () => "Mockery",
} as const
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: typeof AssertionConfig.Skip, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval: T}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: typeof AssertionConfig.Throw, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: ReturnType<typeof AssertionConfig.Log>, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval: T}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: AssertionConfig.Type, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log?: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: AssertionConfig.Type, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log?: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error} {
const assertion = new Assertions()
assertion.config = config
let log: jest.Mock<void, any[]>|undefined = undefined
if (config.mode === AssertionMode.LOG) {
log = jest.fn(config.logger)
assertion.config = AssertionConfig.Log(log)
}
const condition = jest.fn(conditionImpl)
const message = jest.fn(messageImpl)
let error: Error|undefined = undefined
let retval: T|undefined = undefined
try {
retval = assertion.check(value, condition, message)
} catch (e) {
if (e instanceof Error && config.mode === AssertionMode.THROW) {
error = e
} else {
throw e
}
}
return {
value, log, condition, message, error, retval
}
}
it(`returns the input and does not throw in SKIP`, () => {
const {value, retval} = run({
...defaults,
config: AssertionConfig.Skip
})
expect(retval).toBe(value)
});
it(`does not call the condition or message in SKIP`, () => {
const {condition, message} = run({
...defaults,
config: AssertionConfig.Skip
})
expect(condition).not.toHaveBeenCalled()
expect(message).not.toHaveBeenCalled()
});
it(`calls the log function with an error, returns the value and does not throw when the condition fails in LOG`, () => {
const {value, log, retval} = run({
...defaults,
config: AssertionConfig.Log(() => {}),
conditionImpl: () => false,
messageImpl: () => "Mockery"
})
expect(log).toHaveBeenCalledTimes(1)
expect(log).toHaveBeenCalledWith(expect.objectContaining({message: "Mockery"}))
expect(log).toHaveBeenCalledWith(expect.any(Error))
expect(retval).toBe(value)
});
it(`throws an error when the condition fails in THROW`, () => {
const {error} = run({
...defaults,
config: AssertionConfig.Throw,
conditionImpl: () => false,
messageImpl: () => "Mockery"
})
expect(error).toEqual(expect.objectContaining({message: "Mockery"}))
expect(error).toBeInstanceOf(Error)
});
it(`does not log and returns the input when the condition succeeds in LOG`, () => {
const {value, retval, log} = run({
...defaults,
config: AssertionConfig.Log(() => {}),
conditionImpl: () => true
})
expect(log).not.toHaveBeenCalled()
expect(retval).toBe(value)
});
it(`does not throw and returns the input when the condition succeeds in THROW`, () => {
const {value, retval, error} = run({
...defaults,
config: AssertionConfig.Throw,
conditionImpl: () => true
})
expect(error).toBeUndefined()
expect(retval).toBe(value)
});
[AssertionConfig.Log(() => {}), AssertionConfig.Throw].forEach((config) => {
it(`calls the condition, but not the message, once with the value in ${config.mode} when the condition succeeds`, () => {
const {value, condition, message} = run({
...defaults,
config,
conditionImpl: () => true
})
expect(condition).toHaveBeenCalledWith(value)
expect(condition).toHaveBeenCalledTimes(1)
expect(message).not.toHaveBeenCalled()
})
it(`calls the condition and message once each with the value in ${config.mode} when the condition fails`, () => {
const {value, condition, message} = run({
...defaults,
config,
conditionImpl: () => false
})
expect(condition).toHaveBeenCalledWith(value)
expect(condition).toHaveBeenCalledTimes(1)
expect(message).toHaveBeenCalledWith(value)
expect(message).toHaveBeenCalledTimes(1)
})
})
})
function testCheck<T>(run: (input: T) => void, valid: T[], invalid: T[]) {
valid.forEach((input) => {
test(`passes ${input}`, () => {
expect(() => run(input)).not.toThrow()
})
})
invalid.forEach((input) => {
test(`fails ${input}`, () => {
expect(() => run(input)).toThrow()
})
})
}
describe("checkInteger", () => {
function run(input: number) {
const assertion = new Assertions()
assertion.config = AssertionConfig.Throw
assertion.checkInteger(input, () => "Assertion failed")
}
const valid: number[] = [0, 1, -1, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]
const invalid: number[] = [Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 999, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 999, Number.EPSILON]
testCheck(run, valid, invalid)
})
describe("checkPositiveIntegerOrZero", () => {
function run(input: number) {
const assertion = new Assertions()
assertion.config = AssertionConfig.Throw
assertion.checkPositiveIntegerOrZero(input, () => "Assertion failed")
}
const valid: number[] = [0, 1, Number.MAX_SAFE_INTEGER]
const invalid: number[] = [-1, Number.MIN_SAFE_INTEGER, Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 999, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 999, Number.EPSILON]
testCheck(run, valid, invalid)
})
})

@ -0,0 +1,58 @@
export enum AssertionMode {
SKIP = "SKIP",
LOG = "LOG",
THROW = "THROW",
}
/** Configurations for the different assertion modes. */
export namespace AssertionConfig {
/** The types of the assertion configurations. */
export type Type = typeof Skip|typeof Throw|ReturnType<typeof Log>
/** An assertion configuration suitable for production, that skips assertions without evaluating them. */
export const Skip = {mode: AssertionMode.SKIP} as const
/** An assertion configuration suitable for testing, that throws an exception when an assertion fails. */
export const Throw = {mode: AssertionMode.THROW} as const
/** An assertion configuration suitable for debugging, that logs an exception when an assertion fails but allows the program to continue. */
export function Log(logger: (message?: any, ...params: any[]) => void) {
return {mode: AssertionMode.LOG, logger} as const
}
}
/** Assertion class for test-only checks on preconditions and postconditions. */
export class Assertions {
config: AssertionConfig.Type = AssertionConfig.Skip
/** General assert method. Checks that the condition is true. */
check<T>(value: T, condition: (value: T) => boolean, message: (value: T) => string): T {
if (this.config.mode === AssertionMode.SKIP) {
return value
}
const result = condition(value)
if (result) {
return value
}
const err = Error(message(value))
if (this.config.mode === AssertionMode.THROW) {
throw err
} else {
// All we can do is stand by and watch in horror...
this.config.logger(err)
return value
}
}
/** Checks that the value is an integer. */
checkInteger(value: number, message: (value: number) => string): number {
return this.check(value, Number.isSafeInteger, message)
}
/** Checks that the value is an integer and either positive or zero. */
checkPositiveIntegerOrZero(value: number, message: (value: number) => string): number {
return this.check(value, (value) => {
return Number.isSafeInteger(value) && value >= 0
}, message)
}
}
/** Global assertion object for use by other modules. */
export const assertion = new Assertions();

@ -0,0 +1,558 @@
import {
checkValidConfidenceLimit,
checkValidCurrentHitPoints,
checkValidHitPoints,
checkValidHitPointsWithoutConfidence,
checkValidMaxHitPoints,
confidenceLimit,
damageConfidence,
damageHealth,
HitPoints,
HitPointsWithoutConfidence,
hitPointsWithoutConfidenceToString,
hitPointsToString, isValidConfidenceLimit,
isValidCurrentHitPoints,
isValidHitPoints,
isValidHitPointsWithoutConfidence,
isValidMaxHitPoints,
recoverConfidence,
recoverHealth, setCurrentConfidence,
setCurrentHealth,
setMaxConfidence,
setMaxHealth, checkConsistentConfidenceDelta
} from "./HitPoints";
import {assertion, AssertionConfig} from "./Assertions";
const InvalidHitPoints = {currentHealth: 999, maxHealth: 99, maxConfidence: 100, currentConfidence: 20};
let originalConfig: AssertionConfig.Type
beforeEach(() => {
originalConfig = assertion.config
assertion.config = AssertionConfig.Throw
});
afterEach(() => {
assertion.config = originalConfig
});
describe("hitPointsWithoutConfidenceToString", () => {
const pairs: [HitPointsWithoutConfidence, string][] = [
[{currentHealth: 99, maxHealth: 100, maxConfidence: 200}, "{Health: 99/100, Confidence: -/-/200}"],
[{currentHealth: -25, maxHealth: 0, maxConfidence: -200}, "{Health: -25/0, Confidence: -/-/-200}"],
]
pairs.forEach(([hp, expected]) => {
test(`for ${hp} = "${expected}"`, () => {
expect(hitPointsWithoutConfidenceToString(hp)).toStrictEqual(expected)
})
})
});
describe("hitPointsToString", () => {
const pairs: [HitPoints, string][] = [
[{currentHealth: 99, maxHealth: 100, maxConfidence: 200, currentConfidence: 10}, "{Health: 99/100, Confidence: 10/198/200}"],
[{currentHealth: -25, maxHealth: 0, maxConfidence: -200, currentConfidence: -15}, "{Health: -25/0, Confidence: -15/<err>/-200}"],
]
pairs.forEach(([hp, expected]) => {
test(`for ${hp} = "${expected}"`, () => {
expect(hitPointsToString(hp)).toStrictEqual(expected)
})
})
});
describe("isValidMaxHitPoints", () => {
const validValues = [1, 2, 99999]
const invalidValues = [0, -1, 1.5, NaN, Infinity, -Infinity, 100000, 999999]
validValues.forEach((value) => {
test(`returns true for ${value}`, () => {
expect(isValidMaxHitPoints(value)).toBe(true)
})
})
invalidValues.forEach((value) => {
test(`returns false for ${value}`, () => {
expect(isValidMaxHitPoints(value)).toBe(false)
})
})
});
describe("isValidCurrentHitPoints", () => {
const validValues: [number, number, number][] = [[1, 1, 0], [-1, 1, -1], [0, 1, 0], [0, 1, -1], [50, 99999, 0]]
const invalidValues: [number, number, number][] = [[2, 1, 0], [-1, 1, 0], [0.5, 1, 0], [-2, 1, -1]]
validValues.forEach(([current, max, min]) => {
test(`returns true for ${current} between ${min} and ${max}`, () => {
expect(isValidCurrentHitPoints(current, max, min)).toBe(true)
})
})
invalidValues.forEach(([current, max, min]) => {
test(`returns false for ${current} between ${min} and ${max}`, () => {
expect(isValidCurrentHitPoints(current, max, min)).toBe(false)
})
})
});
describe("isValidHitPointsWithoutConfidence", () => {
const validValues: HitPointsWithoutConfidence[] = [{
currentHealth: 100,
maxHealth: 100,
maxConfidence: 100,
}, {
currentHealth: 50,
maxHealth: 100,
maxConfidence: 25
}, {
currentHealth: 0,
maxHealth: 1,
maxConfidence: 1,
}]
const invalidValues: HitPointsWithoutConfidence[] = [{
currentHealth: 0,
maxHealth: 0,
maxConfidence: 99,
}, {
currentHealth: 0,
maxHealth: 20,
maxConfidence: 0,
}, {
currentHealth: -300,
maxHealth: 100,
maxConfidence: 99,
}]
validValues.forEach((value) => {
test(`returns true for ${hitPointsWithoutConfidenceToString(value)}`, () => {
expect(isValidHitPointsWithoutConfidence(value)).toBe(true)
})
})
invalidValues.forEach((value) => {
test(`returns false for ${hitPointsWithoutConfidenceToString(value)}`, () => {
expect(isValidHitPointsWithoutConfidence(value)).toBe(false)
})
})
})
describe("isValidHitPoints", () => {
const validValues: HitPoints[] = [{
currentHealth: 100,
maxHealth: 100,
currentConfidence: 100,
maxConfidence: 100,
}, {
currentHealth: 50,
maxHealth: 100,
currentConfidence: 30,
maxConfidence: 60,
}, {
currentHealth: 0,
maxHealth: 100,
currentConfidence: 0,
maxConfidence: 30,
}]
const invalidValues: HitPoints[] = [{
currentHealth: 11,
maxHealth: 10,
currentConfidence: 99,
maxConfidence: 99,
}, {
currentHealth: 10,
maxHealth: 20,
currentConfidence: 20,
maxConfidence: 20,
}, {
currentHealth: 20,
maxHealth: 20,
currentConfidence: 25,
maxConfidence: 20,
}]
validValues.forEach((value) => {
test(`returns true for ${hitPointsToString(value)}`, () => {
expect(isValidHitPoints(value)).toBe(true)
})
})
invalidValues.forEach((value) => {
test(`returns false for ${hitPointsToString(value)}`, () => {
expect(isValidHitPoints(value)).toBe(false)
})
})
})
describe("isValidConfidenceLimit", () => {
const validValues: [number, HitPointsWithoutConfidence][] = [
[200, {
currentHealth: 100,
maxHealth: 100,
maxConfidence: 200,
}], [0, {
currentHealth: 50,
maxHealth: 100,
maxConfidence: 60
}], [10, {
currentHealth: 50,
maxHealth: 100,
maxConfidence: 60
}], [0, {
currentHealth: 0,
maxHealth: 100,
maxConfidence: 100,
}]]
const invalidValues: [number, HitPointsWithoutConfidence][] = [
[195, {
currentHealth: 100,
maxHealth: 100,
maxConfidence: 200,
}], [25, {
currentHealth: 0,
maxHealth: 100,
maxConfidence: 100,
}]]
validValues.forEach(([value, hp]) => {
test(`returns true for ${value} and ${hitPointsWithoutConfidenceToString(hp)}`, () => {
expect(isValidConfidenceLimit(value, hp)).toBe(true)
})
})
invalidValues.forEach(([value, hp]) => {
test(`returns false for ${value} and ${hitPointsWithoutConfidenceToString(hp)}`, () => {
expect(isValidConfidenceLimit(value, hp)).toBe(false)
})
})
})
describe("checkValidMaxHitPoints", () => {
test("returns on valid max HP", () => {
expect(checkValidMaxHitPoints(30)).toEqual(30)
})
test("throws on invalid max HP", () => {
expect(() => checkValidMaxHitPoints(-1)).toThrow()
})
})
describe("checkValidCurrentHitPoints", () => {
test("returns on valid current HP", () => {
expect(checkValidCurrentHitPoints(30, 30, 0)).toEqual(30)
})
test("throws on invalid current HP", () => {
expect(() => checkValidCurrentHitPoints(-10, 30, 0)).toThrow()
})
})
describe("checkValidHitPointsWithoutConfidence", () => {
test("returns on valid hit points base", () => {
expect(checkValidHitPointsWithoutConfidence({currentHealth: 30, maxHealth: 40, maxConfidence: 99})).toStrictEqual({currentHealth: 30, maxHealth: 40, maxConfidence: 99})
})
test("throws on invalid hit points base", () => {
expect(() => checkValidHitPointsWithoutConfidence({currentHealth: -20, maxHealth: 10, maxConfidence: 30})).toThrow()
})
})
describe("checkValidHitPoints", () => {
test("returns on valid hit points", () => {
expect(checkValidHitPoints({currentHealth: 30, maxHealth: 40, maxConfidence: 99, currentConfidence: 30})).toStrictEqual({currentHealth: 30, maxHealth: 40, maxConfidence: 99, currentConfidence: 30})
})
test("throws on invalid hit points", () => {
expect(() => checkValidHitPoints({currentHealth: -20, maxHealth: 10, maxConfidence: 30, currentConfidence: 50})).toThrow()
})
})
describe("checkValidConfidenceLimit", () => {
test("returns on valid confidence limit", () => {
expect(checkValidConfidenceLimit(30, {currentHealth: 15, maxHealth: 20, maxConfidence: 40})).toEqual(30)
})
test("throws on invalid confidence limit", () => {
expect(() => checkValidConfidenceLimit(40, {currentHealth: 15, maxHealth: 20, maxConfidence: 40})).toThrow()
})
})
describe("checkConsistentConfidenceDelta", () => {
const consistentValues: [HitPoints, number, number][] = [
[{currentHealth: 30, maxHealth: 30, currentConfidence: 10, maxConfidence: 20}, -10, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 0, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 10, 15],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 10, 4],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 9, 4],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 25, 10],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 24, 10],
]
consistentValues.forEach(([hp, confidence, health]) => {
const newHealth = {...hp, currentConfidence: confidence, currentHealth: health}
test(`returns for ${hitPointsToString(hp)} -> ${hitPointsToString(newHealth)}`, () => {
expect(checkConsistentConfidenceDelta(confidence, "testing", hp, health)).toEqual(confidence)
})
})
const inconsistentValues: [HitPoints, number, number][] = [
[{currentHealth: 30, maxHealth: 30, currentConfidence: 10, maxConfidence: 20}, 0, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 20, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 5, 15],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 10, maxConfidence: 25}, 8, 4],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 23, 10],
]
inconsistentValues.forEach(([hp, confidence, health]) => {
const newHealth = {...hp, currentConfidence: confidence, currentHealth: health}
test(`throws for ${hitPointsToString(hp)} -> ${hitPointsToString(newHealth)}`, () => {
expect(() => checkConsistentConfidenceDelta(confidence, "testing", hp, health)).toThrow()
})
})
test("throws an error starting with the evaluation of the action function", () => {
expect(() => checkConsistentConfidenceDelta(
30, "frozzing the snagler",
{maxHealth: 10, currentHealth: 0, maxConfidence: 30, currentConfidence: 0},
9
)).toThrow(/^frozzing the snagler /)
})
})
describe("confidenceLimit", () => {
test("throws on invalid input", () => {
expect(() => confidenceLimit({ currentHealth: -99, maxHealth: 30, maxConfidence: 20})).toThrow()
})
const values: [HitPointsWithoutConfidence, number][] = [
[{maxConfidence: 99, maxHealth: 30, currentHealth: 30}, 99],
// Even 1 missing health results in a loss of at least 1 confidence.
[{maxConfidence: 10, maxHealth: 99999, currentHealth: 99998}, 9],
// Confidence is 0 when each point of confidence represents multiple points of health and there aren't enough to make 1 point of confidence
[{maxConfidence: 10, maxHealth: 50, currentHealth: 4}, 0],
// Confidence is 0 when health is 0
[{maxConfidence: 99999, maxHealth: 99, currentHealth: 0}, 0],
// or when it's below 0
[{maxConfidence: 99999, maxHealth: 99, currentHealth: -50}, 0],
]
values.forEach(([hp, expected]) => {
test(`returns ${expected} for ${hitPointsWithoutConfidenceToString(hp)}`, () => {
expect(confidenceLimit(hp)).toBe(expected)
})
})
})
describe("damageHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, -1],
]
invalidValues.forEach(([hp, damage]) => {
test(`for ${damage} on ${hitPointsToString(hp)} throws`, () => {
expect(() => damageHealth(hp, damage, {damageConfidence: false})).toThrow()
})
})
const values: [[HitPoints, number, {damageConfidence: boolean}?], Partial<HitPoints>][] = [
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5], {currentHealth: -10}],
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: -10}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: -10}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 15], {currentHealth: -10}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 15], {currentHealth: -5}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 5, {damageConfidence: true}], {currentHealth: 5, currentConfidence: 8}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 5, {damageConfidence: false}], {currentHealth: 5, currentConfidence: 10}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 15, {damageConfidence: true}], {currentHealth: -5, currentConfidence: 0}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 15, {damageConfidence: false}], {currentHealth: -5, currentConfidence: 0}],
[[{currentHealth: 100, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 7, {damageConfidence: true}], {currentHealth: 93, currentConfidence: 16}],
[[{currentHealth: 100, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 7, {damageConfidence: false}], {currentHealth: 93, currentConfidence: 18}],
[[{currentHealth: 93, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 2, {damageConfidence: true}], {currentHealth: 91, currentConfidence: 17}],
[[{currentHealth: 93, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 2, {damageConfidence: false}], {currentHealth: 91, currentConfidence: 18}],
]
values.forEach(([[hp, damage, options = {damageConfidence: true}], expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${damage} on ${hitPointsToString(hp)} with damageConfidence ${options.damageConfidence ? "on" : "off"} results in ${hitPointsToString(expected)}`, () => {
expect(damageHealth(hp, damage, options)).toStrictEqual(expected)
})
})
})
describe("recoverHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, -1],
]
invalidValues.forEach(([hp, healing]) => {
test(`for ${healing} on ${hitPointsToString(hp)} throws`, () => {
expect(() => recoverHealth(hp, healing, {recoverConfidence: false})).toThrow()
})
})
const values: [[HitPoints, number, {recoverConfidence: boolean}?], Partial<HitPoints>][] = [
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5], {currentHealth: 0}],
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: 0}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 2, {recoverConfidence: true}], {currentHealth: 9, currentConfidence: 12}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 2, {recoverConfidence: false}], {currentHealth: 9, currentConfidence: 8}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 5, {recoverConfidence: true}], {currentHealth: 10, currentConfidence: 14}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 5, {recoverConfidence: false}], {currentHealth: 10, currentConfidence: 8}],
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 20, currentConfidence: 0}, 15, {recoverConfidence: true}], {currentHealth: 5, currentConfidence: 10}],
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 20, currentConfidence: 0}, 15, {recoverConfidence: false}], {currentHealth: 5, currentConfidence: 0}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 25, currentConfidence: 0}, 3, {recoverConfidence: true}], {currentHealth: 3, currentConfidence: 7}],
[[{currentHealth: 3, maxHealth: 10, maxConfidence: 25, currentConfidence: 7}, 3, {recoverConfidence: true}], {currentHealth: 6, currentConfidence: 14}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 25, currentConfidence: 0}, 3, {recoverConfidence: false}], {currentHealth: 3, currentConfidence: 0}],
]
values.forEach(([[hp, damage, options = {recoverConfidence: true}], expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${damage} on ${hitPointsToString(hp)} with recoverConfidence ${options.recoverConfidence ? "on" : "off"} results in ${hitPointsToString(expected)}`, () => {
expect(recoverHealth(hp, damage, options)).toStrictEqual(expected)
})
})
})
describe("setMaxHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0],
]
invalidValues.forEach(([hp, newMax]) => {
test(`to ${newMax} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setMaxHealth(hp, newMax)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 10, {}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {maxHealth: 5, currentHealth: 5}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 20, {maxHealth: 20}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 19, {maxHealth: 19}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 21, {maxHealth: 21, currentConfidence: 3}],
]
values.forEach(([hp, newMax, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newMax} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setMaxHealth(hp, newMax)).toStrictEqual(expected)
})
})
})
describe("setCurrentHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -11],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 11],
]
invalidValues.forEach(([hp, newValue]) => {
test(`to ${newValue} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setCurrentHealth(hp, newValue)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0, {currentHealth: 0}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5, {currentHealth: 5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {currentHealth: 10}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, -5, {currentHealth: -5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, -10, {currentHealth: -10}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 0, {currentHealth: 0, currentConfidence: 0}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 8, {currentHealth: 8}],
]
values.forEach(([hp, newValue, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newValue} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setCurrentHealth(hp, newValue)).toStrictEqual(expected)
})
})
})
describe("damageConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1],
]
invalidValues.forEach(([hp, damage]) => {
test(`for ${damage} on ${hitPointsToString(hp)} throws`, () => {
expect(() => damageConfidence(hp, damage)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {currentConfidence: 0}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {currentConfidence: 0}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {currentConfidence: 1}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 9, {currentConfidence: 1}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 11, {currentConfidence: 0}],
]
values.forEach(([hp, damage, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${damage} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(damageConfidence(hp, damage)).toStrictEqual(expected)
})
})
})
describe("recoverConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1],
]
invalidValues.forEach(([hp, healing]) => {
test(`for ${healing} on ${hitPointsToString(hp)} throws`, () => {
expect(() => recoverConfidence(hp, healing)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {currentConfidence: 10}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {currentConfidence: 10}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {currentConfidence: 9}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 9, {currentConfidence: 9}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 11, {currentConfidence: 10}],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 6, {currentConfidence: 10}],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 5, {currentConfidence: 10}],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 4, {currentConfidence: 9}],
]
values.forEach(([hp, healing, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${healing} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(recoverConfidence(hp, healing)).toStrictEqual(expected)
})
})
})
describe("setMaxConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0],
]
invalidValues.forEach(([hp, newMax]) => {
test(`to ${newMax} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setMaxConfidence(hp, newMax)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {maxConfidence: 6}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {maxConfidence: 5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {maxConfidence: 4, currentConfidence: 4}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 30, {maxConfidence: 30}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 5, {maxConfidence: 5, currentConfidence: 2}],
]
values.forEach(([hp, newMax, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newMax} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setMaxConfidence(hp, newMax)).toStrictEqual(expected)
})
})
})
describe("setCurrentConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 11],
]
invalidValues.forEach(([hp, newValue]) => {
test(`to ${newValue} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setCurrentConfidence(hp, newValue)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5, {currentConfidence: 5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {currentConfidence: 10}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 0, {currentConfidence: 0}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 8, {currentConfidence: 8}],
]
values.forEach(([hp, newValue, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newValue} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setCurrentConfidence(hp, newValue)).toStrictEqual(expected)
})
})
})

@ -0,0 +1,314 @@
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
})
}
Loading…
Cancel
Save