diff --git a/src/game/combat/actions.ts b/src/game/combat/actions.ts index 9ca22d4..1ef6931 100644 --- a/src/game/combat/actions.ts +++ b/src/game/combat/actions.ts @@ -1,4 +1,4 @@ -import { StatTest, StatVigorTest, StatVigorSizeTest, OpposedStatTest, TestCategory } from './tests' +import { StatTest, StatVigorTest, StatVigorSizeTest, OpposedStatTest, TestCategory, CompositionTest, OpposedStatScorer } from './tests' import { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language' import { Entity } from '../entity' import { Creature } from "../creature" @@ -67,13 +67,23 @@ export class AttackAction extends DamageAction { verb.root.capital, 'Attack the enemy', damage, - [new StatTest( - Stat.Power, - 15, - (user, target) => new LogLine( - `${user.name.capital} ${user.name.conjugate(new Verb('miss', 'misses'))} ${target.name.objective}!` + [ + new CompositionTest( + [ + new OpposedStatScorer( + { + Power: 1 + }, + { + Reflexes: 1 + } + ) + ], + (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb("swing"))} and ${user.name.conjugate(new Verb("miss", "misses"))} ${target.name.objective}.`), + TestCategory.Attack, + 0 ) - )], + ], [new TogetherCondition()] ) } diff --git a/src/game/combat/tests.ts b/src/game/combat/tests.ts index d76e470..995c0de 100644 --- a/src/game/combat/tests.ts +++ b/src/game/combat/tests.ts @@ -9,6 +9,88 @@ function logistic (x0: number, L: number, k: number): (x: number) => number { } } +/** + * A [[Scorer]] produces a score for a creature in a certain situation + */ +export interface Scorer { + userScore (attacker: Creature): number; + targetScore (defender: Creature): number; + explain(user: Creature, target: Creature): LogEntry; +} + +export class OpposedStatScorer implements Scorer { + private maxStatVigorPenalty = 0.5 + private maxTotalVigorPenalty = 0.1 + + constructor (private userStats: Partial, private targetStats: Partial) { + + } + + explain (user: Creature, target: Creature): LogEntry { + return new LogLines( + new LogLine( + `Pits `, + ...Object.entries(this.userStats).map(([stat, frac]) => { + if (frac !== undefined) { + return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat)) + } else { + return nilLog + } + }), + ` against `, + ...Object.entries(this.targetStats).map(([stat, frac]) => { + if (frac !== undefined) { + return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat)) + } else { + return nilLog + } + }) + ), + new LogLine( + `Score delta: ${this.computeScore(user, this.userStats) - this.computeScore(target, this.targetStats)}` + ) + ) + } + + userScore (attacker: Creature): number { + return this.computeScore(attacker, this.userStats) + } + + targetScore (defender: Creature): number { + return this.computeScore(defender, this.targetStats) + } + + private computeScore (subject: Creature, parts: Partial): number { + const total = Object.entries(parts).reduce((total: number, [stat, frac]) => { + if (stat in Stat) { + let value = subject.stats[stat as Stat] * (frac === undefined ? 0 : frac) + + const vigor = StatToVigor[stat as Stat] + value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * subject.vigors[vigor] / subject.maxVigors[vigor] + + return total + value + } else if (stat in VoreStat) { + const value = subject.voreStats[stat as VoreStat] * (frac === undefined ? 0 : frac) + + return total + value + } else { + return total + } + }, 0) + + const modifiedTotal = Object.keys(Vigor).reduce( + (total, vigor) => { + const base = total * (1 - this.maxTotalVigorPenalty) + const modified = total * this.maxTotalVigorPenalty * subject.vigors[vigor as Vigor] / subject.maxVigors[vigor as Vigor] + return base + modified + }, + total + ) + + return modifiedTotal + } +} + // TODO this will need to be able to return a LogEntry at some point abstract class RandomTest implements CombatTest { @@ -38,6 +120,37 @@ export enum TestCategory { Vore = "Vore" } +export class CompositionTest extends RandomTest { + private f: (x: number) => number + private k = 0.1 + + constructor ( + private scorers: Scorer[], + fail: (user: Creature, target: Creature) => LogEntry, + public category: TestCategory, + private bias = 0 + ) { + super(fail) + this.f = logistic(0, 1, this.k) + } + + explain (user: Creature, target: Creature): LogEntry { + return new LogLines( + ...this.scorers.map(scorer => scorer.explain(user, target)) + ) + } + + odds (user: Creature, target: Creature): number { + const userScore = this.scorers.reduce((score, scorer) => score + scorer.userScore(user), 0) + const targetScore = this.scorers.reduce((score, scorer) => score + scorer.targetScore(target), 0) + + const userMod = user.effects.reduce((score, effect) => score + effect.modTestOffense(user, target, this.category), 0) + const targetMod = target.effects.reduce((score, effect) => score + effect.modTestDefense(target, user, this.category), 0) + + return this.f(userScore - targetScore + this.bias) + } +} + export class OpposedStatTest extends RandomTest { private f: (x: number) => number private k = 0.1