|  |  | @@ -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 | 
		
	
	
		
			
				|  |  | 
 |