|  |  | @@ -1,7 +1,6 @@ | 
		
	
		
			
			|  |  |  | import { CombatTest, Stat, Vigor, Stats, StatToVigor, VoreStats, VoreStat } from '../combat' | 
		
	
		
			
			|  |  |  | import { Creature } from "../creature" | 
		
	
		
			
			|  |  |  | import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface' | 
		
	
		
			
			|  |  |  | import { Verb } from '../language' | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | function logistic (x0: number, L: number, k: number): (x: number) => number { | 
		
	
		
			
			|  |  |  | return (x: number) => { | 
		
	
	
		
			
				|  |  | @@ -146,283 +145,8 @@ export class CompositionTest extends RandomTest { | 
		
	
		
			
			|  |  |  | const userScore = this.scorers.reduce((score, scorer) => scorer.userScore(user, score), 0) | 
		
	
		
			
			|  |  |  | const targetScore = this.scorers.reduce((score, scorer) => scorer.targetScore(target, score), 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 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | // how much a stat can be reduced by its corresponding vigor being low | 
		
	
		
			
			|  |  |  | private maxStatVigorPenalty = 0.5 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | // how much the total score can be reduced by each vigor being low | 
		
	
		
			
			|  |  |  | private maxTotalVigorPenalty = 0.1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | constructor ( | 
		
	
		
			
			|  |  |  | public readonly userStats: Partial<Stats & VoreStats>, | 
		
	
		
			
			|  |  |  | public readonly targetStats: Partial<Stats & VoreStats>, | 
		
	
		
			
			|  |  |  | fail: (user: Creature, target: Creature) => LogEntry, | 
		
	
		
			
			|  |  |  | public category: TestCategory, | 
		
	
		
			
			|  |  |  | private bias = 0 | 
		
	
		
			
			|  |  |  | ) { | 
		
	
		
			
			|  |  |  | super(fail) | 
		
	
		
			
			|  |  |  | this.f = logistic(0, 1, this.k) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | odds (user: Creature, target: Creature): number { | 
		
	
		
			
			|  |  |  | const userScore = this.getScoreOffense(user, target, this.userStats) | 
		
	
		
			
			|  |  |  | const targetScore = this.getScoreDefense(target, user, this.targetStats) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return this.f(userScore - targetScore + this.bias) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | 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)}% of `, 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)}% of `, new PropElem(stat as Stat), `, `) | 
		
	
		
			
			|  |  |  | } else { | 
		
	
		
			
			|  |  |  | return nilLog | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | }) | 
		
	
		
			
			|  |  |  | ), | 
		
	
		
			
			|  |  |  | new LogLine( | 
		
	
		
			
			|  |  |  | `${user.name.capital}: ${this.getScoreOffense(user, target, this.userStats)} // ${this.getScoreDefense(target, user, this.targetStats)} :${target.name.capital}` | 
		
	
		
			
			|  |  |  | ), | 
		
	
		
			
			|  |  |  | new LogLine( | 
		
	
		
			
			|  |  |  | `${user.name.capital} ${user.name.conjugate(new Verb("have", "has"))} a ${(this.odds(user, target) * 100).toFixed(0)}% chance of winning this test.` | 
		
	
		
			
			|  |  |  | ) | 
		
	
		
			
			|  |  |  | ) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | private getScoreDefense (defender: Creature, attacker: Creature, parts: Partial<Stats>): number { | 
		
	
		
			
			|  |  |  | return this.getScore(defender, parts) + defender.effects.reduce((total, effect) => total + effect.modTestDefense(defender, attacker, this.category), 0) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | private getScoreOffense (attacker: Creature, defender: Creature, parts: Partial<Stats>): number { | 
		
	
		
			
			|  |  |  | return this.getScore(attacker, parts) + attacker.effects.reduce((total, effect) => total + effect.modTestOffense(attacker, defender, this.category), 0) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | private getScore (actor: Creature, parts: Partial<Stats>): number { | 
		
	
		
			
			|  |  |  | const total = Object.entries(parts).reduce((total: number, [stat, frac]) => { | 
		
	
		
			
			|  |  |  | if (stat in Stat) { | 
		
	
		
			
			|  |  |  | let value = actor.stats[stat as Stat] * (frac === undefined ? 0 : frac) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | const vigor = StatToVigor[stat as Stat] | 
		
	
		
			
			|  |  |  | value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * actor.vigors[vigor] / actor.maxVigors[vigor] | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return total + value | 
		
	
		
			
			|  |  |  | } else if (stat in VoreStat) { | 
		
	
		
			
			|  |  |  | const value = actor.voreStats[stat as VoreStat] * (frac === undefined ? 0 : frac) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return total + value | 
		
	
		
			
			|  |  |  | } else { | 
		
	
		
			
			|  |  |  | return total | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | }, 0) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | const modifiedTotal = Object.keys(Vigor).reduce( | 
		
	
		
			
			|  |  |  | (total, vigor) => { | 
		
	
		
			
			|  |  |  | return total * (1 - this.maxStatVigorPenalty) + total * this.maxStatVigorPenalty * actor.vigors[vigor as Vigor] / actor.maxVigors[vigor as Vigor] | 
		
	
		
			
			|  |  |  | }, | 
		
	
		
			
			|  |  |  | total | 
		
	
		
			
			|  |  |  | ) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return modifiedTotal | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | export class StatVigorSizeTest extends RandomTest { | 
		
	
		
			
			|  |  |  | private f: (x: number) => number | 
		
	
		
			
			|  |  |  | private k = 0.1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) { | 
		
	
		
			
			|  |  |  | super(fail) | 
		
	
		
			
			|  |  |  | this.f = logistic(0, 1, this.k) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | odds (user: Creature, target: Creature): number { | 
		
	
		
			
			|  |  |  | let userPercent = 1 | 
		
	
		
			
			|  |  |  | let targetPercent = 1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | Object.keys(Vigor).forEach(key => { | 
		
	
		
			
			|  |  |  | userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor]) | 
		
	
		
			
			|  |  |  | targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor]) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | userPercent = Math.max(0, userPercent) | 
		
	
		
			
			|  |  |  | targetPercent = Math.max(0, targetPercent) | 
		
	
		
			
			|  |  |  | }) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (userPercent === 0) { | 
		
	
		
			
			|  |  |  | targetPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (targetPercent === 0) { | 
		
	
		
			
			|  |  |  | userPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return this.f(this.bias + sizeOffset * 5 + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | explain (user: Creature, target: Creature): LogEntry { | 
		
	
		
			
			|  |  |  | let result: LogEntry | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | let userPercent = 1 | 
		
	
		
			
			|  |  |  | let targetPercent = 1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | Object.keys(Vigor).forEach(key => { | 
		
	
		
			
			|  |  |  | userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor] | 
		
	
		
			
			|  |  |  | targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor] | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | userPercent = Math.max(0, userPercent) | 
		
	
		
			
			|  |  |  | targetPercent = Math.max(0, targetPercent) | 
		
	
		
			
			|  |  |  | }) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (userPercent === 0) { | 
		
	
		
			
			|  |  |  | targetPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (targetPercent === 0) { | 
		
	
		
			
			|  |  |  | userPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | const userMod = user.stats[this.stat] * userPercent | 
		
	
		
			
			|  |  |  | const targetMod = target.stats[this.stat] * targetPercent | 
		
	
		
			
			|  |  |  | const delta = userMod - targetMod + sizeOffset * 5 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (delta === 0) { | 
		
	
		
			
			|  |  |  | result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.') | 
		
	
		
			
			|  |  |  | } else if (delta < 0) { | 
		
	
		
			
			|  |  |  | result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.') | 
		
	
		
			
			|  |  |  | } else { | 
		
	
		
			
			|  |  |  | result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.') | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%') | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return result | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | export class StatVigorTest extends RandomTest { | 
		
	
		
			
			|  |  |  | private f: (x: number) => number | 
		
	
		
			
			|  |  |  | private k = 0.1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) { | 
		
	
		
			
			|  |  |  | super(fail) | 
		
	
		
			
			|  |  |  | this.f = logistic(0, 1, this.k) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | odds (user: Creature, target: Creature): number { | 
		
	
		
			
			|  |  |  | let userPercent = 1 | 
		
	
		
			
			|  |  |  | let targetPercent = 1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | Object.keys(Vigor).forEach(key => { | 
		
	
		
			
			|  |  |  | userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor]) | 
		
	
		
			
			|  |  |  | targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor]) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | userPercent = Math.max(0, userPercent) | 
		
	
		
			
			|  |  |  | targetPercent = Math.max(0, targetPercent) | 
		
	
		
			
			|  |  |  | }) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (userPercent === 0) { | 
		
	
		
			
			|  |  |  | targetPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (targetPercent === 0) { | 
		
	
		
			
			|  |  |  | userPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | explain (user: Creature, target: Creature): LogEntry { | 
		
	
		
			
			|  |  |  | let result: LogEntry | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | let userPercent = 1 | 
		
	
		
			
			|  |  |  | let targetPercent = 1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | Object.keys(Vigor).forEach(key => { | 
		
	
		
			
			|  |  |  | userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor] | 
		
	
		
			
			|  |  |  | targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor] | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | userPercent = Math.max(0, userPercent) | 
		
	
		
			
			|  |  |  | targetPercent = Math.max(0, targetPercent) | 
		
	
		
			
			|  |  |  | }) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (userPercent === 0) { | 
		
	
		
			
			|  |  |  | targetPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (targetPercent === 0) { | 
		
	
		
			
			|  |  |  | userPercent *= 4 | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | const userMod = user.stats[this.stat] * userPercent | 
		
	
		
			
			|  |  |  | const targetMod = target.stats[this.stat] * targetPercent | 
		
	
		
			
			|  |  |  | const delta = userMod - targetMod | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (delta === 0) { | 
		
	
		
			
			|  |  |  | result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.') | 
		
	
		
			
			|  |  |  | } else if (delta < 0) { | 
		
	
		
			
			|  |  |  | result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.') | 
		
	
		
			
			|  |  |  | } else { | 
		
	
		
			
			|  |  |  | result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.') | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%') | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return result | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | export class StatTest extends RandomTest { | 
		
	
		
			
			|  |  |  | private f: (x: number) => number | 
		
	
		
			
			|  |  |  | private k = 0.1 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) { | 
		
	
		
			
			|  |  |  | super(fail) | 
		
	
		
			
			|  |  |  | this.f = logistic(0, 1, this.k) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | odds (user: Creature, target: Creature): number { | 
		
	
		
			
			|  |  |  | return this.f(this.bias + user.stats[this.stat] - target.stats[this.stat]) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | explain (user: Creature, target: Creature): LogEntry { | 
		
	
		
			
			|  |  |  | const delta: number = user.stats[this.stat] - target.stats[this.stat] | 
		
	
		
			
			|  |  |  | let result: LogEntry | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | if (delta === 0) { | 
		
	
		
			
			|  |  |  | result = new LogLine('You and the target have the same ', new PropElem(this.stat), '.') | 
		
	
		
			
			|  |  |  | } else if (delta < 0) { | 
		
	
		
			
			|  |  |  | result = new LogLine('You have ', new PropElem(this.stat, -delta), ' less than your foe.') | 
		
	
		
			
			|  |  |  | } else { | 
		
	
		
			
			|  |  |  | result = new LogLine('You have ', new PropElem(this.stat, delta), ' more than you foe.') | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%') | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | return result | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | export class ChanceTest extends RandomTest { | 
		
	
	
		
			
				|  |  | 
 |