import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula, ConstantDamageFormula, StatusEffect } from '@/game/combat' import { LogLines, LogEntry, LogLine, nilLog, RandomEntry, FormatEntry, FormatOpt } from '@/game/interface' import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe, Adjective } from '@/game/language' import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction, StruggleMoveAction } from '@/game/combat/actions' import * as Words from '@/game/words' import * as Onomatopoeia from '@/game/onomatopoeia' import { Creature } from '@/game/creature' import { VoreRelay } from '@/game/events' export enum VoreType { Oral = "Oral Vore" } export const anyVore = new Set([ VoreType.Oral ]) export type Wall = { name: Word; texture: Adjective; material: Noun; color: Adjective; } export type Fluid = { name: Word; color: Adjective; sound: Word; sloshVerb: Verb; } export type Gas = { name: Word; color: Adjective; smell: Adjective; bubbleVerb: Verb; releaseVerb: Verb; exit: Noun; } export enum ContainerCapability { Consume, Release, Digest, Absorb } export enum ConnectionDirection { Deeper, Neutral, Shallower } export type Connection = { destination: Container; direction: ConnectionDirection; description: (to: Container, from: Container, prey: Creature) => LogEntry; } export interface Container extends Actionable { name: Noun; owner: Creature; voreTypes: Set; capabilities: Set; connections: Array; effects: Array; wall: Wall | null; fluid: Fluid | null; gas: Gas | null; voreRelay: VoreRelay; contents: Array; digested: Array; damage: DamageFormula; sound: Word; capacity: number; fullness: number; consumeVerb: Verb; consumePreposition: Preposition; releaseVerb: Verb; releasePreposition: Preposition; struggleVerb: Verb; strugglePreposition: Preposition; canTake (prey: Creature): boolean; consume (prey: Creature): LogEntry; release (prey: Creature): LogEntry; enter (prey: Creature): LogEntry; exit (prey: Creature): LogEntry; struggle (prey: Creature): LogEntry; tick (dt: number, victims?: Array): LogEntry; digest (preys: Creature[]): LogEntry; absorb (preys: Creature[]): LogEntry; onDigest (prey: Creature): LogEntry; onAbsorb (prey: Creature): LogEntry; consumeLine (user: Creature, target: Creature): LogEntry; statusLine (user: Creature, target: Creature): LogEntry; describe (): LogEntry; describeDetail (prey: Creature): LogEntry; connect (dest: Connection): void; } export abstract class DefaultContainer implements Container { public name: Noun contents: Array = [] actions: Array = [] wall: Wall | null = null fluid: Fluid | null = null gas: Gas | null = null connections: Array = [] voreRelay = new VoreRelay() effects: Array = [] consumeVerb = new Verb('devour') consumePreposition = new Preposition("into") releaseVerb = new Verb('release', 'releases', 'releasing', 'released') releasePreposition = new Preposition("out from") struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') strugglePreposition = new Preposition("within") fluidColor = "#00ff0088" digested: Array = [] absorbed: Array = [] damage: DamageFormula = new ConstantDamageFormula(new Damage()); sound = new Verb("slosh") constructor (name: Noun, public owner: Creature, public voreTypes: Set, public capacityFactor: number, public capabilities: Set) { this.name = name.all if (capabilities.has(ContainerCapability.Consume)) { this.actions.push(new DevourAction(this)) } if (capabilities.has(ContainerCapability.Release)) { this.actions.push(new ReleaseAction(this)) this.actions.push(new StruggleAction(this)) } if (capabilities.has(ContainerCapability.Digest)) { this.actions.push(new RubAction(this)) } } connect (connection: Connection): void { this.connections.push(connection) this.actions.push(new TransferAction(this, connection)) this.actions.push(new StruggleMoveAction(this, connection.destination)) } get capacity (): number { return this.capacityFactor * this.owner.voreStats.Mass } statusLine (user: Creature, target: Creature): LogEntry { return new LogLine( `${target.name.capital} ${target.name.conjugate(new ToBe())} ${Words.Stuck} inside ${user.name.possessive} ${this.name}.` ) } releaseLine (user: Creature, target: Creature): LogEntry { return new LogLine(`${user.name.capital} ${user.name.conjugate(this.releaseVerb)} ${target.name.objective} ${this.releasePreposition} ${user.pronouns.possessive} ${this.name}.`) } struggleLine (user: Creature, target: Creature): LogEntry { return new LogLine(`${user.name.capital} ${user.name.conjugate(this.struggleVerb)} ${this.strugglePreposition} ${target.name.possessive} ${this.name}.`) } get fullness (): number { return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) } canTake (prey: Creature): boolean { const fits = this.capacity - this.fullness >= prey.voreStats.Bulk const permitted = Array.from(this.voreTypes).every(voreType => { return prey.preyPrefs.has(voreType) }) return fits && permitted } consume (prey: Creature): LogEntry { const results: Array = [ this.enter(prey), this.voreRelay.dispatch("onEaten", this, { prey: prey }), prey.voreRelay.dispatch("onEaten", this, { prey: prey }), this.consumeLine(this.owner, prey) ] this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this))) return new LogLines(...results) } release (prey: Creature): LogEntry { const results = [ this.exit(prey), this.releaseLine(this.owner, prey), this.voreRelay.dispatch("onReleased", this, { prey: prey }), prey.voreRelay.dispatch("onReleased", this, { prey: prey }) ] return new LogLines(...results) } enter (prey: Creature): LogEntry { if (prey.containedIn !== null) { prey.containedIn.contents = prey.containedIn.contents.filter(item => prey !== item) } this.contents.push(prey) prey.containedIn = this const effectResults = this.effects.map(effect => prey.applyEffect(effect)) const results = [ this.voreRelay.dispatch("onEntered", this, { prey: prey }), prey.voreRelay.dispatch("onEntered", this, { prey: prey }) ] this.owner.effects.forEach(effect => results.push(effect.postEnter(this.owner, prey, this))) return new LogLines(...results, ...effectResults) } exit (prey: Creature): LogEntry { prey.containedIn = this.owner.containedIn this.contents = this.contents.filter(victim => victim !== prey) if (this.owner.containedIn !== null) { this.owner.containedIn.contents.push(prey) } const effectResults = this.effects.map(effect => prey.removeEffect(effect)) const results = [ this.voreRelay.dispatch("onExited", this, { prey: prey }), prey.voreRelay.dispatch("onExited", this, { prey: prey }) ] return new LogLines(...results, ...effectResults) } struggle (prey: Creature): LogEntry { return this.struggleLine(prey, this.owner) } describe (): LogEntry { const lines: Array = [] this.contents.forEach(prey => { lines.push(prey.toString()) }) return new LogLine(...lines) } describeDetail (prey: Creature): LogEntry { const lines: Array = [] if (this.gas) { lines.push( new LogLine(`${this.gas.color.capital} ${this.gas.name.plural} ${this.gas.bubbleVerb} in ${this.owner.name.possessive} ${this.name}.`) ) } if (this.fluid) { lines.push( new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} around ${prey.name.objective}.`) ) } if (this.wall) { lines.push( new LogLine(`The ${this.wall.color} walls ${prey.name.conjugate(Words.Clench)} over ${prey.name.objective} like a vice.`) ) } return new LogLine(...lines) } consumeLine (user: Creature, target: Creature) { return new RandomEntry( new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective}, ${Words.Force.present} ${target.pronouns.objective} ${this.consumePreposition} ${user.pronouns.possessive} ${this.name}.`), new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb("pounce"))} on ${target.name.objective} and ${user.name.conjugate(this.consumeVerb)} ${target.pronouns.objective}, ${Words.Force.present} ${target.pronouns.objective} ${this.consumePreposition} ${user.pronouns.possessive} ${this.name}.`) ) } tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry { const options = [ new LogLine(`${user.name.capital} ${Words.Churns.singular} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`), new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`), new LogLine(`${target.name.capital} ${target.name.conjugate(Words.Struggle)} ${this.strugglePreposition} ${user.name.possessive} ${Words.Slick} ${this.name} as it ${Words.Churns.singular} ${target.pronouns.objective} for `, args.damage.renderShort(), `.`) ] if (this.fluid) { options.push(new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} and ${this.fluid.sound.singular} as ${this.owner.name.possessive} ${this.name} steadily ${Words.Digest.singular} ${target.name.objective}.`)) } const result: Array = [ new RandomEntry(...options) ] if (Math.random() < 0.3) { result.push(new FormatEntry(new LogLine(`${Onomatopoeia.Gurgle}`), FormatOpt.Onomatopoeia)) } return new LogLines(...result) } digestLine (user: Creature, target: Creature): LogEntry { return new LogLine(`${user.name.capital.possessive} ${this.name} finishes ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)}.`) } absorbLine (user: Creature, target: Creature): LogEntry { return new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Absorb.present} ${target.name.objective}, fully claiming ${target.pronouns.objective}.`) } tick (dt: number, victims?: Array): LogEntry { const justDigested: Array = [] const justAbsorbed: Array = [] const damageResults: Array = [] const tickedEntryList: LogEntry[] = [] if (this.capabilities.has(ContainerCapability.Digest)) { this.contents.forEach(prey => { if (victims === undefined || victims.indexOf(prey) >= 0) { const scaled = this.damage.calc(this.owner, prey).scale(dt / 60) const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled) if (modified.nonzero()) { tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified })) damageResults.push(prey.takeDamage(modified)) } if (prey.vigors[Vigor.Health] <= 0) { prey.destroyed = true this.digested.push(prey) justDigested.push(prey) damageResults.push(this.onDigest(prey)) } } }) } const tickedEntries = new LogLines(...tickedEntryList) this.digested.forEach(prey => { if (victims === undefined || victims.indexOf(prey) >= 0) { const scaled = this.damage.calc(this.owner, prey).scale(dt / 60) const damageTotal: number = scaled.damages.filter(instance => instance.target === Vigor.Health).reduce( (total: number, instance: DamageInstance) => total + instance.amount, 0 ) const massStolen = Math.min(damageTotal / 100, prey.voreStats.Mass) prey.voreStats.Mass -= massStolen this.owner.voreStats.Mass += massStolen if (prey.voreStats.Mass === 0) { this.absorbed.push(prey) justAbsorbed.push(prey) damageResults.push(this.onAbsorb(prey)) } } }) const digestedEntries = this.digest(justDigested) const absorbedEntries = this.absorb(justAbsorbed) this.contents = this.contents.filter(prey => { return prey.vigors[Vigor.Health] > 0 }) this.digested = this.digested.filter(prey => { return prey.voreStats.Mass > 0 }) return new LogLines(tickedEntries, new LogLines(...damageResults), digestedEntries, absorbedEntries) } absorb (preys: Creature[]): LogEntry { return new LogLines(...preys.map(prey => this.absorbLine(this.owner, prey))) } digest (preys: Creature[]): LogEntry { const results = preys.map(prey => this.digestLine(this.owner, prey)) if (preys.length > 0 && this.gas) { results.push(new LogLine( `A crass ${this.gas.releaseVerb} escapes ${this.owner.name.possessive} ${this.gas.exit} as ${this.owner.name.possessive} prey is digested, spewing ${this.gas.color} ${this.gas.name}.` )) } return new LogLines(...results) } onAbsorb (prey: Creature): LogEntry { return this.voreRelay.dispatch("onAbsorbed", this, { prey: prey }) } onDigest (prey: Creature): LogEntry { return this.voreRelay.dispatch("onDigested", this, { prey: prey }) } } export class Stomach extends DefaultContainer { fluid = { color: new Adjective("green"), name: new Noun("chyme"), sound: new Verb("gurgle"), sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed") } gas = { bubbleVerb: new Verb("bubble", "bubbles", "bubbling", "bubbled"), color: new Adjective("hazy"), name: new Noun("fume", "fumes"), releaseVerb: new Verb("belch", "belches", "belching", "belched"), smell: new Adjective("acrid"), exit: new Noun("jaws") } wall = { color: new Adjective("red"), material: new Noun("muscle"), name: new Noun("wall"), texture: new Adjective("slimy") } constructor (owner: Creature, capacityFactor: number, damage: DamageFormula) { super(new Noun("stomach"), owner, new Set([VoreType.Oral]), capacityFactor, new Set([ ContainerCapability.Digest, ContainerCapability.Absorb ])) this.voreRelay.subscribe("onEntered", (sender: Container, args: { prey: Creature }) => { return new FormatEntry(new LogLine(`${Onomatopoeia.Glunk}`), FormatOpt.Onomatopoeia) }) this.damage = damage } } export class Throat extends DefaultContainer { fluid = { color: new Adjective("clear"), name: new RandomWord([ new Noun("saliva"), new Noun("drool"), new Noun("slobber") ]), sound: new Verb("squish", "squishes"), sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed") } wall = { color: new Adjective("red"), material: new Noun("muscle"), name: new Noun("wall"), texture: new Adjective("slimy") } constructor (owner: Creature, capacityFactor: number) { super(new Noun("throat"), owner, new Set([VoreType.Oral]), capacityFactor, new Set([ ContainerCapability.Consume, ContainerCapability.Release ])) this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => { return new FormatEntry(new LogLine(`${Onomatopoeia.Swallow}`), FormatOpt.Onomatopoeia) }) } } export function transferDescription (verb: Word, preposition: Preposition): ((from: Container, to: Container, prey: Creature) => LogEntry) { return (from: Container, to: Container, prey: Creature) => { return new LogLine(`${from.owner.name.capital} ${from.owner.name.conjugate(verb.singular)} ${prey.name.objective} ${preposition} ${to.consumePreposition} ${from.owner.pronouns.possessive} ${to.name}.`) } }