diff --git a/src/game/combat.ts b/src/game/combat.ts index 63e1237..f67e99e 100644 --- a/src/game/combat.ts +++ b/src/game/combat.ts @@ -91,6 +91,7 @@ export interface CombatTest { test: (user: Creature, target: Creature) => boolean; odds: (user: Creature, target: Creature) => number; explain: (user: Creature, target: Creature) => LogEntry; + fail: (user: Creature, target: Creature) => LogEntry; } /** @@ -347,9 +348,15 @@ export interface Combatant { * An Action is anything that can be done by a [[Creature]] to a [[Creature]]. */ export abstract class Action { - constructor (public name: TextLike, public desc: TextLike, public conditions: Array = []) { + constructor ( + public name: TextLike, + public desc: TextLike, + public conditions: Array = [], + public tests: Array = [] + ) { } + // TODO explain the tests in here allowed (user: Creature, target: Creature): boolean { return this.conditions.every(cond => cond.allowed(user, target)) @@ -359,34 +366,44 @@ export abstract class Action { return this.name.toString() } - abstract execute (user: Creature, target: Creature): LogEntry - abstract describe (user: Creature, target: Creature): LogEntry -} + try (user: Creature, target: Creature): LogEntry { + const failReason = this.tests.find(test => !test.test(user, target)) + if (failReason !== undefined) { + return failReason.fail(user, target) + } else { + return this.execute(user, target) + } + } + + describe (user: Creature, target: Creature): LogEntry { + return new LogLines( + ...this.tests.map(test => test.explain(user, target)) + ) + } -export type TestBundle = { - test: CombatTest; - fail: PairLine; + abstract execute (user: Creature, target: Creature): LogEntry } export class CompositionAction extends Action { private consequences: Array; - private tests: Array; - constructor (name: TextLike, desc: TextLike, properties: { conditions?: Array; consequences?: Array; tests?: Array }) { - super(name, desc, properties.conditions ?? []) + constructor ( + name: TextLike, + desc: TextLike, + properties: { + conditions?: Array; + consequences?: Array; + tests?: Array; + } + ) { + super(name, desc, properties.conditions ?? [], properties.tests ?? []) this.consequences = properties.consequences ?? [] - this.tests = properties.tests ?? [] } execute (user: Creature, target: Creature): LogEntry { - const failReason = this.tests.find(test => !test.test.test(user, target)) - if (failReason !== undefined) { - return failReason.fail(user, target) - } else { - return new LogLines( - ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target)) - ) - } + return new LogLines( + ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target)) + ) } describe (user: Creature, target: Creature): LogEntry { diff --git a/src/game/combat/actions.ts b/src/game/combat/actions.ts index 7e94083..45155f3 100644 --- a/src/game/combat/actions.ts +++ b/src/game/combat/actions.ts @@ -2,7 +2,7 @@ import { StatTest, StatVigorTest, StatVigorSizeTest } from './tests' import { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language' import { Entity } from '../entity' import { Creature } from "../creature" -import { Damage, DamageFormula, Stat, Vigor, Action, Condition } from '../combat' +import { Damage, DamageFormula, Stat, Vigor, Action, Condition, CombatTest } from '../combat' import { LogLine, LogLines, LogEntry, nilLog } from '../interface' import { VoreContainer, Container } from '../vore' import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from './conditions' @@ -28,35 +28,33 @@ export class PassAction extends Action { * A generic action that causes damage. */ export abstract class DamageAction extends Action { - protected test: StatTest - abstract successLine: PairLineArgs - abstract failLine: PairLine - constructor (name: TextLike, desc: TextLike, protected damage: DamageFormula, conditions: Condition[] = []) { + constructor (name: TextLike, desc: TextLike, protected damage: DamageFormula, tests: CombatTest[], conditions: Condition[] = []) { super( name, desc, - [new CapableCondition(), new EnemyCondition()].concat(conditions) + [new CapableCondition(), new EnemyCondition()].concat(conditions), + tests ) - this.test = new StatTest(Stat.Power, 10) } - execute (user: Creature, target: Creature): LogEntry { + try (user: Creature, target: Creature): LogEntry { const effectResults = target.effects.map(effect => effect.preAttack(target, user)) if (effectResults.some(result => result.prevented)) { return new LogLines(...effectResults.map(result => result.log)) - } - if (this.test.test(user, target)) { - const damage = this.damage.calc(user, target) - const targetResult = target.takeDamage(damage) - const ownResult = this.successLine(user, target, { damage: damage }) - return new LogLines(ownResult, targetResult) } else { - return this.failLine(user, target) + return super.try(user, target) } } + + execute (user: Creature, target: Creature): LogEntry { + const damage = this.damage.calc(user, target) + const targetResult = target.takeDamage(damage) + const ownResult = this.successLine(user, target, { damage: damage }) + return new LogLines(ownResult, targetResult) + } } /** @@ -64,11 +62,23 @@ export abstract class DamageAction extends Action { */ export class AttackAction extends DamageAction { constructor (damage: DamageFormula, protected verb: Verb = new Verb('smack')) { - super(verb.root.capital, 'Attack the enemy', damage, [new TogetherCondition()]) + super( + verb.root.capital, + 'Attack the enemy', + damage, + [new StatTest( + Stat.Power, + 10, + (user, target) => new LogLine( + `${user.name.capital} ${user.name.conjugate(new Verb('miss', 'misses'))} ${target.name.objective}!` + ) + )], + [new TogetherCondition()] + ) } describe (user: Creature, target: Creature): LogEntry { - return new LogLine(`Attack ${target.name}. `, this.damage.describe(user, target), '. ', this.test.explain(user, target)) + return new LogLine(`Attack ${target.name}. `, this.damage.describe(user, target), '. ', super.describe(user, target)) } successLine: PairLineArgs = (user, target, args) => new LogLine( @@ -85,15 +95,17 @@ export class AttackAction extends DamageAction { * Devours the target. */ export class DevourAction extends Action { - private test: StatVigorSizeTest - constructor (protected container: Container) { super( new DynText(new LiveText(container, x => x.consumeVerb.capital), ' (', new LiveText(container, x => x.name.all), ')'), new LiveText(container, x => `Try to ${x.consumeVerb} your foe`), - [new CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)] + [new CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)], + [new StatVigorSizeTest( + Stat.Power, + 10, + (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${container.consumeVerb} ${target.name.objective}.`) + )] ) - this.test = new StatVigorSizeTest(Stat.Power) } allowed (user: Creature, target: Creature): boolean { @@ -109,20 +121,12 @@ export class DevourAction extends Action { } describe (user: Creature, target: Creature): LogEntry { - return new LogLine(`Try to ${this.container.consumeVerb} your opponent, sending them to your ${this.container.name}. `, this.test.explain(user, target)) + return new LogLine(`Try to ${this.container.consumeVerb} your opponent, sending them to your ${this.container.name}. `, super.describe(user, target)) } execute (user: Creature, target: Creature): LogEntry { - if (this.test.test(user, target)) { - return this.container.consume(target) - } else { - return this.failLine(user, target, { container: this.container }) - } + return this.container.consume(target) } - - protected failLine: PairLineArgs = (user, target, args) => new LogLine( - `${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${args.container.consumeVerb} ${target.name.objective}.` - ) } /** @@ -171,40 +175,34 @@ export class FeedAction extends Action { * Tries to escape from the target's container */ export class StruggleAction extends Action { - private test: StatVigorSizeTest - constructor (public container: Container) { super( new DynText('Struggle (', new LiveText(container, x => x.name.all), ')'), 'Try to escape from your foe', - [new CapableCondition(), new PairCondition(), new ContainedByCondition(container)] + [new CapableCondition(), new PairCondition(), new ContainedByCondition(container)], + [new StatVigorSizeTest( + Stat.Power, + 0, + (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to escape from ${target.name.possessive} ${container.name}.`) + )] ) - this.test = new StatVigorSizeTest(Stat.Power) } execute (user: Creature, target: Creature): LogEntry { if (user.containedIn !== null) { - if (this.test.test(user, target)) { - return new LogLines(this.successLine(user, target, { container: this.container }), user.containedIn.release(user)) - } else { - return this.failLine(user, target, { container: this.container }) - } + return new LogLines(this.successLine(user, target, { container: this.container }), user.containedIn.release(user)) } else { return new LogLine("Vore's bugged!") } } describe (user: Creature, target: Creature): LogEntry { - return new LogLine(`Try to escape from ${target.name}'s ${this.container.name}. `, this.test.explain(user, target)) + return new LogLine(`Try to escape from ${target.name}'s ${this.container.name}. `, super.describe(user, target)) } protected successLine: PairLineArgs = (prey, pred, args) => new LogLine( `${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))} from ${pred.name.possessive} ${args.container.name}.` ) - - protected failLine: PairLineArgs = (prey, pred, args) => new LogLine( - `${prey.name.capital} ${prey.name.conjugate(new Verb('fail'))} to escape from ${pred.name.possessive} ${args.container.name}.` - ) } export class DigestAction extends Action { diff --git a/src/game/combat/tests.ts b/src/game/combat/tests.ts index 44e6fa7..900ef1b 100644 --- a/src/game/combat/tests.ts +++ b/src/game/combat/tests.ts @@ -1,6 +1,6 @@ import { CombatTest, Stat, Vigor } from '../combat' import { Creature } from "../creature" -import { LogEntry, LogLines, PropElem, LogLine } from '../interface' +import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface' function logistic (x0: number, L: number, k: number): (x: number) => number { return (x: number) => { @@ -11,6 +11,10 @@ function logistic (x0: number, L: number, k: number): (x: number) => number { // TODO this will need to be able to return a LogEntry at some point abstract class RandomTest implements CombatTest { + constructor (public fail: (user: Creature, target: Creature) => LogEntry) { + + } + test (user: Creature, target: Creature): boolean { const userFail = user.effects.map(effect => effect.failTest(user, target)) if (userFail.some(result => result.failed)) { @@ -32,8 +36,8 @@ export class StatVigorSizeTest extends RandomTest { private f: (x: number) => number private k = 0.1 - constructor (public readonly stat: Stat, private bias = 0) { - super() + constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) { + super(fail) this.f = logistic(0, 1, this.k) } @@ -57,10 +61,9 @@ export class StatVigorSizeTest extends RandomTest { userPercent *= 4 } - userPercent *= Math.sqrt(user.voreStats.Mass) - targetPercent *= Math.sqrt(target.voreStats.Mass) + const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass) - return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) + return this.f(this.bias + sizeOffset * 5 + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) } explain (user: Creature, target: Creature): LogEntry { @@ -84,11 +87,12 @@ export class StatVigorSizeTest extends RandomTest { if (targetPercent === 0) { userPercent *= 4 } - userPercent *= Math.sqrt(user.voreStats.Mass) - targetPercent *= Math.sqrt(target.voreStats.Mass) + + 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 + const delta = userMod - targetMod + sizeOffset * 5 if (delta === 0) { result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.') @@ -108,8 +112,8 @@ export class StatVigorTest extends RandomTest { private f: (x: number) => number private k = 0.1 - constructor (public readonly stat: Stat, private bias = 0) { - super() + constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) { + super(fail) this.f = logistic(0, 1, this.k) } @@ -179,8 +183,8 @@ export class StatTest extends RandomTest { private f: (x: number) => number private k = 0.1 - constructor (public readonly stat: Stat, private bias = 0) { - super() + constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) { + super(fail) this.f = logistic(0, 1, this.k) } @@ -207,8 +211,8 @@ export class StatTest extends RandomTest { } export class ChanceTest extends RandomTest { - constructor (public readonly chance: number) { - super() + constructor (public readonly chance: number, fail: (user: Creature, target: Creature) => LogEntry) { + super(fail) } odds (user: Creature, target: Creature): number { diff --git a/src/game/creature.ts b/src/game/creature.ts index 5c62873..d6fef26 100644 --- a/src/game/creature.ts +++ b/src/game/creature.ts @@ -86,7 +86,7 @@ export class Creature extends Vore implements Combatant { if (blocking.length > 0) { return new LogLines(...blocking.map(result => result.log)) } else { - return action.execute(this, target) + return action.try(this, target) } } diff --git a/src/game/creatures/geta.ts b/src/game/creatures/geta.ts index c45e6b7..5dea6ec 100644 --- a/src/game/creatures/geta.ts +++ b/src/game/creatures/geta.ts @@ -61,12 +61,12 @@ export class Geta extends Creature { new ContainsCondition(cock) ], tests: [ - { - test: new ChanceTest(0.5), - fail: (user, target) => new LogLine( - `${user.name.capital.possessive} cock clenches hard around ${target.name.objective}, but ${target.pronouns.subjective} ${target.name.conjugate(new Verb("avoid"))} being crushed.` + new ChanceTest( + 0.5, + (user, target) => new LogLine( + `${user.name.capital.possessive} cock clenches hard around ${target.name.objective}, but ${target.pronouns.subjective} ${target.name.conjugate(new Verb("avoid"))} being crushed.` ) - } + ) ], consequences: [ new LogConsequence( diff --git a/src/game/creatures/kenzie.ts b/src/game/creatures/kenzie.ts index 813a2db..838d447 100644 --- a/src/game/creatures/kenzie.ts +++ b/src/game/creatures/kenzie.ts @@ -4,7 +4,6 @@ import { VoreType, Stomach } from '../vore' import { Side, Damage, DamageType, Vigor, StatDamageFormula, Stat, VoreStat, DamageFormula } from '../combat' import { AttackAction, DevourAction } from '../combat/actions' import { LogEntry, LogLines } from '../interface' -import { StatTest } from '../combat/tests' import { StunEffect, PredatorCounterEffect } from '../combat/effects' class StompAttack extends AttackAction { @@ -13,19 +12,14 @@ class StompAttack extends AttackAction { damage, verb ) - this.test = new StatTest(Stat.Power) } execute (user: Creature, target: Creature): LogEntry { - if (this.test.test(user, target)) { - const damage = this.damage.calc(user, target) - const targetResult = target.takeDamage(damage) - const ownResult = this.successLine(user, target, { damage: damage }) - const effResult = target.applyEffect(new StunEffect(3)) - return new LogLines(ownResult, targetResult, effResult) - } else { - return this.failLine(user, target) - } + const damage = this.damage.calc(user, target) + const targetResult = target.takeDamage(damage) + const ownResult = this.successLine(user, target, { damage: damage }) + const effResult = target.applyEffect(new StunEffect(3)) + return new LogLines(ownResult, targetResult, effResult) } } export class Kenzie extends Creature { diff --git a/src/game/creatures/shingo.ts b/src/game/creatures/shingo.ts index 52c0538..b4ec908 100644 --- a/src/game/creatures/shingo.ts +++ b/src/game/creatures/shingo.ts @@ -8,20 +8,18 @@ import { ContainsCondition, CapableCondition, EnemyCondition, TargetDrainedVigor import { StatTest } from '../combat/tests' export class TrappedAction extends DamageAction { - protected test: StatTest - constructor (name: TextLike, desc: TextLike, protected verb: Verb, protected damage: DamageFormula, container: Container, conditions: Condition[] = []) { super( name, desc, damage, + [], [new CapableCondition(), new ContainsCondition(container), new EnemyCondition()].concat(conditions) ) - this.test = new StatTest(Stat.Power) } describe (user: Creature, target: Creature): LogEntry { - return new LogLine(`Chew on ${target.name}. `, this.damage.describe(user, target), '. ', this.test.explain(user, target)) + return new LogLine(`Chew on ${target.name}. `, this.damage.describe(user, target), '. ') } successLine: PairLineArgs = (user, target, args) => new LogLine( diff --git a/src/game/creatures/withers.ts b/src/game/creatures/withers.ts index d343165..bd2fe7d 100644 --- a/src/game/creatures/withers.ts +++ b/src/game/creatures/withers.ts @@ -262,15 +262,12 @@ class StompAllyAction extends Action { } class DevourAllAction extends GroupAction { - private test: CombatTest - constructor (private container: VoreContainer) { super('Devour All', 'GULP!', [ new TogetherCondition(), new EnemyCondition(), new CapableCondition() ]) - this.test = new StatVigorTest(Stat.Power) } line = (user: Creature, target: Creature) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('scoop'))} ${target.name} up!`) @@ -287,7 +284,7 @@ class DevourAllAction extends GroupAction { } executeGroup (user: Creature, targets: Array): LogEntry { - return new LogLines(...targets.filter(target => this.test.test(user, target)).map(target => this.execute(user, target)).concat( + return new LogLines(...targets.map(target => this.execute(user, target)).concat( [ new Newline(), this.groupLine(user, { count: targets.length })