| @@ -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<Condition> = []) { | |||
| constructor ( | |||
| public name: TextLike, | |||
| public desc: TextLike, | |||
| public conditions: Array<Condition> = [], | |||
| public tests: Array<CombatTest> = [] | |||
| ) { | |||
| } | |||
| // 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<Creature>; | |||
| abstract execute (user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class CompositionAction extends Action { | |||
| private consequences: Array<Consequence>; | |||
| private tests: Array<TestBundle>; | |||
| constructor (name: TextLike, desc: TextLike, properties: { conditions?: Array<Condition>; consequences?: Array<Consequence>; tests?: Array<TestBundle> }) { | |||
| super(name, desc, properties.conditions ?? []) | |||
| constructor ( | |||
| name: TextLike, | |||
| desc: TextLike, | |||
| properties: { | |||
| conditions?: Array<Condition>; | |||
| consequences?: Array<Consequence>; | |||
| tests?: Array<CombatTest>; | |||
| } | |||
| ) { | |||
| 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 { | |||
| @@ -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<Creature, { damage: Damage }> | |||
| abstract failLine: PairLine<Creature> | |||
| 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<Creature, { damage: Damage }> = (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<Entity, { container: Container }> = (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<Entity, { container: Container }> = (prey, pred, args) => new LogLine( | |||
| `${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))} from ${pred.name.possessive} ${args.container.name}.` | |||
| ) | |||
| protected failLine: PairLineArgs<Entity, { container: Container }> = (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 { | |||
| @@ -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 { | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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( | |||
| @@ -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 { | |||
| @@ -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<Creature, { damage: Damage }> = (user, target, args) => new LogLine( | |||
| @@ -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<Creature>): 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 }) | |||