|
|
|
@@ -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<Stats & VoreStats>, private targetStats: Partial<Stats & VoreStats>) { |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
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<Stats & VoreStats>): 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 |
|
|
|
|