diff --git a/src/game/ai.ts b/src/game/ai.ts index 7161926..2142d83 100644 --- a/src/game/ai.ts +++ b/src/game/ai.ts @@ -2,7 +2,7 @@ import { Creature } from './creature' import { Encounter, Action } from './combat' import { LogEntry } from './interface' import { PassAction } from './combat/actions' -import { NoPassDecider, NoReleaseDecider, ChanceDecider, NoSurrenderDecider, FavorDigestDecider } from './ai/deciders' +import { NoPassDecider, NoReleaseDecider, ChanceDecider, NoSurrenderDecider, FavorRubDecider } from './ai/deciders' /** * A Decider determines how favorable an action is to perform. @@ -68,7 +68,7 @@ export class VoreAI extends AI { new NoSurrenderDecider(), new NoPassDecider(), new ChanceDecider(), - new FavorDigestDecider() + new FavorRubDecider() ] ) } diff --git a/src/game/ai/deciders.ts b/src/game/ai/deciders.ts index eaed6cf..2925160 100644 --- a/src/game/ai/deciders.ts +++ b/src/game/ai/deciders.ts @@ -1,7 +1,7 @@ import { Decider } from '../ai' import { Encounter, Action, Consequence, CompositionAction } from '../combat' import { Creature } from '../creature' -import { PassAction, ReleaseAction, DigestAction } from '../combat/actions' +import { PassAction, ReleaseAction, RubAction } from '../combat/actions' import { StatusConsequence } from '../combat/consequences' import { SurrenderEffect } from '../combat/effects' @@ -44,6 +44,7 @@ export class ChanceDecider implements Decider { * Adjusts the weights for [[CompositionAction]]s that contain the specified consequence */ export class ConsequenceDecider implements Decider { + /* eslint-disable-next-line */ constructor (private consequenceType: new (...args: any) => T, private weight: number) { } @@ -100,11 +101,11 @@ export class NoSurrenderDecider extends ConsequenceFunctionDecider { } /** - * Favors [[DigestAction]]s + * Favors [[RubAction]]s */ -export class FavorDigestDecider implements Decider { +export class FavorRubDecider implements Decider { decide (encounter: Encounter, user: Creature, target: Creature, action: Action) { - if (action instanceof DigestAction) { + if (action instanceof RubAction) { return 5 } else { return 1 diff --git a/src/game/combat.ts b/src/game/combat.ts index 96c90a9..3779998 100644 --- a/src/game/combat.ts +++ b/src/game/combat.ts @@ -152,8 +152,11 @@ export class Damage { })) } + // TODO is there a way to do this that will satisfy the typechecker? renderShort (): LogEntry { + /* eslint-disable-next-line */ const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {}) + /* eslint-disable-next-line */ const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {}) this.damages.forEach(instance => { const factor = instance.type === DamageType.Heal ? -1 : 1 @@ -388,9 +391,12 @@ export abstract class Action { describe (user: Creature, target: Creature): LogEntry { return new LogLines( + ...this.conditions.map(condition => condition.explain(user, target)), + new Newline(), new LogLine( `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` ), + new Newline(), ...this.tests.map(test => test.explain(user, target)) ) } @@ -439,6 +445,7 @@ export class CompositionAction extends Action { */ export interface Condition { allowed: (user: Creature, target: Creature) => boolean; + explain: (user: Creature, target: Creature) => LogEntry; } export interface Actionable { @@ -596,7 +603,7 @@ export type EncounterDesc = { export class Encounter { initiatives: Map currentMove: Creature - turnTime = 100 + turnTime = 500 constructor (public desc: EncounterDesc, public combatants: Creature[]) { this.initiatives = new Map() @@ -607,7 +614,7 @@ export class Encounter { this.nextMove() } - nextMove (): LogEntry { + nextMove (totalTime = 0): LogEntry { this.initiatives.set(this.currentMove, 0) const times = new Map() @@ -635,15 +642,26 @@ export class Encounter { // TODO: still let the creature use drained-vigor moves if (this.currentMove.disabled) { - return this.nextMove() + return this.nextMove(closestRemaining + totalTime) } else { + // applies digestion every time combat advances + const tickResults = this.combatants.flatMap( + combatant => combatant.containers.map( + container => container.tick(closestRemaining + totalTime) + ) + ) const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented) if (effectResults.some(result => result.prevented)) { const parts = effectResults.map(result => result.log).concat([this.nextMove()]) return new LogLines( - ...parts + ...parts, + ...tickResults + ) + } else { + return new LogLines( + ...tickResults ) } } diff --git a/src/game/combat/actions.ts b/src/game/combat/actions.ts index 4a788c2..1978e6a 100644 --- a/src/game/combat/actions.ts +++ b/src/game/combat/actions.ts @@ -2,10 +2,11 @@ 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, CombatTest } from '../combat' +import { Damage, DamageFormula, Stat, Vigor, Action, Condition, CombatTest, CompositionAction } 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' +import { ConsumeConsequence } from './consequences' /** * The PassAction has no effect. @@ -90,17 +91,22 @@ export class AttackAction extends DamageAction { /** * Devours the target. */ -export class DevourAction extends Action { +export class DevourAction extends CompositionAction { 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 StatVigorSizeTest( - Stat.Power, - -5, - (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${container.consumeVerb} ${target.name.objective}.`) - )] + { + conditions: [new CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)], + tests: [new StatVigorSizeTest( + Stat.Power, + -5, + (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${container.consumeVerb} ${target.name.objective}.`) + )], + consequences: [ + new ConsumeConsequence(container) + ] + } ) } @@ -197,11 +203,11 @@ export class StruggleAction extends Action { ) } -export class DigestAction extends Action { +export class RubAction extends Action { constructor (protected container: VoreContainer) { super( - new DynText('Digest (', new LiveText(container, container => container.name.all), ')'), - 'Digest your prey', + new DynText('Rub (', new LiveText(container, container => container.name.all), ')'), + 'Digest your prey more quickly', [new CapableCondition(), new SoloCondition()] ) } diff --git a/src/game/combat/conditions.ts b/src/game/combat/conditions.ts index 1f40c74..d99ad92 100644 --- a/src/game/combat/conditions.ts +++ b/src/game/combat/conditions.ts @@ -1,6 +1,9 @@ import { Condition, Vigor } from "../combat" import { Creature } from "../creature" import { Container } from '../vore' +import { LogEntry, LogLine, PropElem } from '../interface' +import { ToBe, Verb } from '../language' +import * as Words from '../words' export class InverseCondition implements Condition { constructor (private condition: Condition) { @@ -10,12 +13,31 @@ export class InverseCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return !this.condition.allowed(user, target) } + + explain (user: Creature, target: Creature): LogEntry { + return new LogLine( + `The following must NOT hold: `, + this.condition.explain(user, target) + ) + } } export class CapableCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return !user.disabled } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} not incapacitated` + ) + } else { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} incapacitated` + ) + } + } } export class UserDrainedVigorCondition implements Condition { @@ -26,6 +48,14 @@ export class UserDrainedVigorCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user.vigors[this.vigor] <= 0 } + + explain (user: Creature, target: Creature): LogEntry { + return new LogLine( + `${user.name.capital} must have ${user.pronouns.possessive} `, + new PropElem(this.vigor), + ` drained.` + ) + } } export class TargetDrainedVigorCondition implements Condition { @@ -36,24 +66,68 @@ export class TargetDrainedVigorCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return target.vigors[this.vigor] <= 0 } + + explain (user: Creature, target: Creature): LogEntry { + return new LogLine( + `${target.name.capital} must have ${target.pronouns.possessive} `, + new PropElem(this.vigor), + ` drained.` + ) + } } export class SoloCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user === target } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} can't use this action on others.` + ) + } else { + return new LogLine( + `${user.name.capital} can use this action on ${user.pronouns.reflexive}.` + ) + } + } } export class PairCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user !== target } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} can use this action on others.` + ) + } else { + return new LogLine( + `${user.name.capital} can't use this action on ${user.pronouns.reflexive}.` + ) + } + } } export class TogetherCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user.containedIn === target.containedIn && user !== target } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} together with ${target.name.objective}` + ) + } else { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} not together with ${target.name.objective}` + ) + } + } } export class HasRoomCondition implements Condition { @@ -64,6 +138,18 @@ export class HasRoomCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return this.container.capacity >= this.container.fullness + target.voreStats.Bulk } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(Words.Have)} enough room in ${user.pronouns.possessive} ${this.container.name} to hold ${target.name.objective}` + ) + } else { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(Words.Have)} enough room in ${user.pronouns.possessive} ${this.container.name} to hold ${target.name.objective}; ${user.name} only ${user.name.conjugate(Words.Have)} ${this.container.capacity - this.container.fullness}, but ${target.name.objective} ${target.name.conjugate(Words.Have)} a bulk of ${target.voreStats.Bulk}` + ) + } + } } export class ContainedByCondition implements Condition { @@ -74,6 +160,18 @@ export class ContainedByCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user.containedIn === this.container && this.container.owner === target } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} contained in ${target.name.possessive} ${this.container.name}` + ) + } else { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} not contained in ${target.name.possessive} ${this.container.name}` + ) + } + } } export class ContainsCondition implements Condition { @@ -84,18 +182,54 @@ export class ContainsCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return target.containedIn === this.container } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${target.name} ${target.name.conjugate(new ToBe())} contained within ${user.name.possessive} ${this.container.name}` + ) + } else { + return new LogLine( + `${target.name} ${target.name.conjugate(new ToBe())} not contained within ${user.name.possessive} ${this.container.name}` + ) + } + } } export class AllyCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user.side === target.side } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${target.name.capital} is an ally of ${user.name.objective}` + ) + } else { + return new LogLine( + `${target.name.capital} ${target.name.conjugate(new Verb("need"))} to be an ally of ${user.name.objective}` + ) + } + } } export class EnemyCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user.side !== target.side } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${target.name.capital} is an enemy of ${user.name.objective}` + ) + } else { + return new LogLine( + `${target.name.capital} ${target.name.conjugate(new Verb("need"))} to be an enemy of ${user.name.objective}` + ) + } + } } export class ContainerFullCondition implements Condition { @@ -106,6 +240,18 @@ export class ContainerFullCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return this.container.contents.length > 0 } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.possessive.capital} ${this.container.name} contains prey` + ) + } else { + return new LogLine( + `${user.name.possessive.capital} ${this.container.name} is empty` + ) + } + } } export class MassRatioCondition implements Condition { @@ -116,4 +262,16 @@ export class MassRatioCondition implements Condition { allowed (user: Creature, target: Creature): boolean { return user.voreStats.Mass / target.voreStats.Mass > this.ratio } + + explain (user: Creature, target: Creature): LogEntry { + if (this.allowed(user, target)) { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} at least ${this.ratio} times as large as ${target.name.objective}` + ) + } else { + return new LogLine( + `${user.name.capital} ${user.name.conjugate(new ToBe())} not at least ${this.ratio} times as large as ${target.name.objective}; ${user.name} ${user.name.conjugate(new ToBe())} only ${(user.voreStats.Mass / target.voreStats.Mass).toFixed(2)} times larger` + ) + } + } } diff --git a/src/game/combat/consequences.ts b/src/game/combat/consequences.ts index b50cac9..aa78748 100644 --- a/src/game/combat/consequences.ts +++ b/src/game/combat/consequences.ts @@ -2,6 +2,7 @@ import { Consequence, DamageFormula, Condition, StatusEffect } from '../combat' import { Creature } from '../creature' import { LogEntry, LogLines, LogLine, nilLog } from '../interface' import { Verb, PairLine } from '../language' +import { Container } from '../vore' /** * Takes a function, and thus can do anything. @@ -99,3 +100,16 @@ export class StatusConsequence extends Consequence { ) } } + +/** + * Consumes the target + */ +export class ConsumeConsequence extends Consequence { + constructor (public container: Container, conditions: Condition[] = []) { + super(conditions) + } + + apply (user: Creature, target: Creature): LogEntry { + return this.container.consume(target) + } +} diff --git a/src/game/combat/tests.ts b/src/game/combat/tests.ts index d555b6c..b5a7a68 100644 --- a/src/game/combat/tests.ts +++ b/src/game/combat/tests.ts @@ -88,10 +88,7 @@ export class OpposedStatTest extends RandomTest { ` from ${target.name.possessive} stats.` ), new LogLine( - `${user.name.capital.possessive} total score is ${this.getScore(user, this.userStats)}` - ), - new LogLine( - `${target.name.capital.possessive} total score is ${this.getScore(target, this.targetStats)}` + `${user.name.capital}: ${this.getScore(user, this.userStats)} // ${this.getScore(target, 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.` diff --git a/src/game/creature.ts b/src/game/creature.ts index 381f98d..75e38ae 100644 --- a/src/game/creature.ts +++ b/src/game/creature.ts @@ -29,6 +29,7 @@ export class Creature extends Mortal { statusEffects: Array = []; groupActions: Array = []; items: Array = []; + /* eslint-disable-next-line */ wallet: { [key in Currency]: number } = Object.keys(Currency).reduce((total: any, key) => { total[key] = 0; return total }, {}); otherActions: Array = []; side: Side; @@ -164,7 +165,7 @@ export class Creature extends Mortal { return results } - validActions (target: Creature): Array { + allActions (target: Creature): Array { let choices = ([] as Action[]).concat( this.actions, this.containers.flatMap(container => container.actions), @@ -178,7 +179,11 @@ export class Creature extends Mortal { choices = choices.concat(this.containedIn.actions) } - return choices.filter(action => { + return choices + } + + validActions (target: Creature): Array { + return this.allActions(target).filter(action => { return action.allowed(this, target) }) } diff --git a/src/game/entity.ts b/src/game/entity.ts index 79aff6e..4f6f413 100644 --- a/src/game/entity.ts +++ b/src/game/entity.ts @@ -50,7 +50,9 @@ export abstract class Mortal extends Entity { constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats) { super(name, kind, pronouns) + /* eslint-disable-next-line */ this.stats = Object.keys(Stat).reduce((base: any, key) => { base[key] = baseStats[key as Stat]; return base }, {}) + /* eslint-disable-next-line */ this.baseResistances = Object.keys(DamageType).reduce((resist: any, key) => { resist[key] = 1; return resist }, {}) Object.entries(this.maxVigors).forEach(([key, val]) => { this.vigors[key as Vigor] = val diff --git a/src/game/vore.ts b/src/game/vore.ts index 9239b3a..53c7afc 100644 --- a/src/game/vore.ts +++ b/src/game/vore.ts @@ -2,7 +2,7 @@ import { Mortal } from './entity' import { Damage, DamageType, Stats, Actionable, Action, Vigor, VoreStats, VisibleStatus, VoreStat, DamageInstance, DamageFormula } from './combat' import { LogLines, LogEntry, LogLine, nilLog } from './interface' import { Noun, Pronoun, ImproperNoun, TextLike, Verb, SecondPersonPronouns, PronounAsNoun, FirstPersonPronouns, PairLineArgs, SoloLine, POV, RandomWord } from './language' -import { DigestAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from './combat/actions' +import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from './combat/actions' import * as Words from './words' import { Creature } from './creature' @@ -177,7 +177,7 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor this.name = name - this.actions.push(new DigestAction(this)) + this.actions.push(new RubAction(this)) } get fullness (): number { @@ -280,7 +280,7 @@ export abstract class InnerVoreContainer extends NormalVoreContainer { this.actions = [] - this.actions.push(new DigestAction(this)) + this.actions.push(new RubAction(this)) this.actions.push(new StruggleAction(this)) } diff --git a/src/game/words.ts b/src/game/words.ts index b1df047..ec0d7ee 100644 --- a/src/game/words.ts +++ b/src/game/words.ts @@ -1,5 +1,10 @@ import { RandomWord, ImproperNoun, Adjective, Verb } from './language' +export const Have = new Verb( + "have", + "has" +) + export const SwallowSound = new RandomWord([ new ImproperNoun('gulp'), new ImproperNoun('glurk'),