| @@ -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() | |||
| ] | |||
| ) | |||
| } | |||
| @@ -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<T extends Consequence> 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 | |||
| @@ -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<Creature, number> | |||
| 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<Creature, number>() | |||
| @@ -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 | |||
| ) | |||
| } | |||
| } | |||
| @@ -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()] | |||
| ) | |||
| } | |||
| @@ -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` | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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.` | |||
| @@ -29,6 +29,7 @@ export class Creature extends Mortal { | |||
| statusEffects: Array<StatusEffect> = []; | |||
| groupActions: Array<GroupAction> = []; | |||
| items: Array<Item> = []; | |||
| /* eslint-disable-next-line */ | |||
| wallet: { [key in Currency]: number } = Object.keys(Currency).reduce((total: any, key) => { total[key] = 0; return total }, {}); | |||
| otherActions: Array<Action> = []; | |||
| side: Side; | |||
| @@ -164,7 +165,7 @@ export class Creature extends Mortal { | |||
| return results | |||
| } | |||
| validActions (target: Creature): Array<Action> { | |||
| allActions (target: Creature): Array<Action> { | |||
| 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<Action> { | |||
| return this.allActions(target).filter(action => { | |||
| return action.allowed(this, target) | |||
| }) | |||
| } | |||
| @@ -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 | |||
| @@ -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)) | |||
| } | |||
| @@ -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'), | |||