Feast 2.0!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

498 lines
16 KiB

  1. import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula, ConstantDamageFormula, StatusEffect } from '@/game/combat'
  2. import { LogLines, LogEntry, LogLine, nilLog, RandomEntry, FormatEntry, FormatOpt } from '@/game/interface'
  3. import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe, Adjective } from '@/game/language'
  4. import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction, StruggleMoveAction } from '@/game/combat/actions'
  5. import * as Words from '@/game/words'
  6. import * as Onomatopoeia from '@/game/onomatopoeia'
  7. import { Creature } from '@/game/creature'
  8. import { VoreRelay } from '@/game/events'
  9. export enum VoreType {
  10. Oral = "Oral Vore"
  11. }
  12. export const anyVore = new Set([
  13. VoreType.Oral
  14. ])
  15. export type Wall = {
  16. name: Word;
  17. texture: Adjective;
  18. material: Noun;
  19. color: Adjective;
  20. }
  21. export type Fluid = {
  22. name: Word;
  23. color: Adjective;
  24. sound: Word;
  25. sloshVerb: Verb;
  26. }
  27. export type Gas = {
  28. name: Word;
  29. color: Adjective;
  30. smell: Adjective;
  31. bubbleVerb: Verb;
  32. releaseVerb: Verb;
  33. exit: Noun;
  34. }
  35. export enum ContainerCapability {
  36. Consume,
  37. Release,
  38. Digest,
  39. Absorb
  40. }
  41. export enum ConnectionDirection {
  42. Deeper,
  43. Neutral,
  44. Shallower
  45. }
  46. export type Connection = {
  47. destination: Container;
  48. direction: ConnectionDirection;
  49. description: (to: Container, from: Container, prey: Creature) => LogEntry;
  50. }
  51. export interface Container extends Actionable {
  52. name: Noun;
  53. owner: Creature;
  54. voreTypes: Set<VoreType>;
  55. capabilities: Set<ContainerCapability>;
  56. connections: Array<Connection>;
  57. effects: Array<StatusEffect>;
  58. wall: Wall | null;
  59. fluid: Fluid | null;
  60. gas: Gas | null;
  61. voreRelay: VoreRelay;
  62. contents: Array<Creature>;
  63. digested: Array<Creature>;
  64. damage: DamageFormula;
  65. sound: Word;
  66. capacity: number;
  67. fullness: number;
  68. consumeVerb: Verb;
  69. consumePreposition: Preposition;
  70. releaseVerb: Verb;
  71. releasePreposition: Preposition;
  72. struggleVerb: Verb;
  73. strugglePreposition: Preposition;
  74. canTake (prey: Creature): boolean;
  75. consume (prey: Creature): LogEntry;
  76. release (prey: Creature): LogEntry;
  77. enter (prey: Creature): LogEntry;
  78. exit (prey: Creature): LogEntry;
  79. struggle (prey: Creature): LogEntry;
  80. tick (dt: number, victims?: Array<Creature>): LogEntry;
  81. digest (preys: Creature[]): LogEntry;
  82. absorb (preys: Creature[]): LogEntry;
  83. onDigest (prey: Creature): LogEntry;
  84. onAbsorb (prey: Creature): LogEntry;
  85. consumeLine (user: Creature, target: Creature): LogEntry;
  86. statusLine (user: Creature, target: Creature): LogEntry;
  87. describe (): LogEntry;
  88. describeDetail (prey: Creature): LogEntry;
  89. connect (dest: Connection): void;
  90. }
  91. export abstract class DefaultContainer implements Container {
  92. public name: Noun
  93. contents: Array<Creature> = []
  94. actions: Array<Action> = []
  95. wall: Wall | null = null
  96. fluid: Fluid | null = null
  97. gas: Gas | null = null
  98. connections: Array<Connection> = []
  99. voreRelay = new VoreRelay()
  100. effects: Array<StatusEffect> = []
  101. consumeVerb = new Verb('devour')
  102. consumePreposition = new Preposition("into")
  103. releaseVerb = new Verb('release', 'releases', 'releasing', 'released')
  104. releasePreposition = new Preposition("out from")
  105. struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled')
  106. strugglePreposition = new Preposition("within")
  107. fluidColor = "#00ff0088"
  108. digested: Array<Creature> = []
  109. absorbed: Array<Creature> = []
  110. damage: DamageFormula = new ConstantDamageFormula(new Damage());
  111. sound = new Verb("slosh")
  112. constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number, public capabilities: Set<ContainerCapability>) {
  113. this.name = name.all
  114. if (capabilities.has(ContainerCapability.Consume)) {
  115. this.actions.push(new DevourAction(this))
  116. }
  117. if (capabilities.has(ContainerCapability.Release)) {
  118. this.actions.push(new ReleaseAction(this))
  119. this.actions.push(new StruggleAction(this))
  120. }
  121. if (capabilities.has(ContainerCapability.Digest)) {
  122. this.actions.push(new RubAction(this))
  123. }
  124. }
  125. connect (connection: Connection): void {
  126. this.connections.push(connection)
  127. this.actions.push(new TransferAction(this, connection))
  128. this.actions.push(new StruggleMoveAction(this, connection.destination))
  129. }
  130. get capacity (): number {
  131. return this.capacityFactor * this.owner.voreStats.Mass
  132. }
  133. statusLine (user: Creature, target: Creature): LogEntry {
  134. return new LogLine(
  135. `${target.name.capital} ${target.name.conjugate(new ToBe())} ${Words.Stuck} inside ${user.name.possessive} ${this.name}.`
  136. )
  137. }
  138. releaseLine (user: Creature, target: Creature): LogEntry {
  139. return new LogLine(`${user.name.capital} ${user.name.conjugate(this.releaseVerb)} ${target.name.objective} ${this.releasePreposition} ${user.pronouns.possessive} ${this.name}.`)
  140. }
  141. struggleLine (user: Creature, target: Creature): LogEntry {
  142. return new LogLine(`${user.name.capital} ${user.name.conjugate(this.struggleVerb)} ${this.strugglePreposition} ${target.name.possessive} ${this.name}.`)
  143. }
  144. get fullness (): number {
  145. return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0)
  146. }
  147. canTake (prey: Creature): boolean {
  148. const fits = this.capacity - this.fullness >= prey.voreStats.Bulk
  149. const permitted = Array.from(this.voreTypes).every(voreType => {
  150. return prey.preyPrefs.has(voreType)
  151. })
  152. return fits && permitted
  153. }
  154. consume (prey: Creature): LogEntry {
  155. const results: Array<LogEntry> = [
  156. this.enter(prey),
  157. this.voreRelay.dispatch("onEaten", this, { prey: prey }),
  158. prey.voreRelay.dispatch("onEaten", this, { prey: prey }),
  159. this.consumeLine(this.owner, prey)
  160. ]
  161. this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this)))
  162. return new LogLines(...results)
  163. }
  164. release (prey: Creature): LogEntry {
  165. const results = [
  166. this.exit(prey),
  167. this.releaseLine(this.owner, prey),
  168. this.voreRelay.dispatch("onReleased", this, { prey: prey }),
  169. prey.voreRelay.dispatch("onReleased", this, { prey: prey })
  170. ]
  171. return new LogLines(...results)
  172. }
  173. enter (prey: Creature): LogEntry {
  174. if (prey.containedIn !== null) {
  175. prey.containedIn.contents = prey.containedIn.contents.filter(item => prey !== item)
  176. }
  177. this.contents.push(prey)
  178. prey.containedIn = this
  179. const effectResults = this.effects.map(effect => prey.applyEffect(effect))
  180. const results = [
  181. this.voreRelay.dispatch("onEntered", this, { prey: prey }),
  182. prey.voreRelay.dispatch("onEntered", this, { prey: prey })
  183. ]
  184. this.owner.effects.forEach(effect => results.push(effect.postEnter(this.owner, prey, this)))
  185. return new LogLines(...results, ...effectResults)
  186. }
  187. exit (prey: Creature): LogEntry {
  188. prey.containedIn = this.owner.containedIn
  189. this.contents = this.contents.filter(victim => victim !== prey)
  190. if (this.owner.containedIn !== null) {
  191. this.owner.containedIn.contents.push(prey)
  192. }
  193. const effectResults = this.effects.map(effect => prey.removeEffect(effect))
  194. const results = [
  195. this.voreRelay.dispatch("onExited", this, { prey: prey }),
  196. prey.voreRelay.dispatch("onExited", this, { prey: prey })
  197. ]
  198. return new LogLines(...results, ...effectResults)
  199. }
  200. struggle (prey: Creature): LogEntry {
  201. return this.struggleLine(prey, this.owner)
  202. }
  203. describe (): LogEntry {
  204. const lines: Array<string> = []
  205. this.contents.forEach(prey => {
  206. lines.push(prey.toString())
  207. })
  208. return new LogLine(...lines)
  209. }
  210. describeDetail (prey: Creature): LogEntry {
  211. const lines: Array<LogLine> = []
  212. if (this.gas) {
  213. lines.push(
  214. new LogLine(`${this.gas.color.capital} ${this.gas.name.plural} ${this.gas.bubbleVerb} in ${this.owner.name.possessive} ${this.name}.`)
  215. )
  216. }
  217. if (this.fluid) {
  218. lines.push(
  219. new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} around ${prey.name.objective}.`)
  220. )
  221. }
  222. if (this.wall) {
  223. lines.push(
  224. new LogLine(`The ${this.wall.color} walls ${prey.name.conjugate(Words.Clench)} over ${prey.name.objective} like a vice.`)
  225. )
  226. }
  227. return new LogLine(...lines)
  228. }
  229. consumeLine (user: Creature, target: Creature) {
  230. return new RandomEntry(
  231. 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}.`),
  232. 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}.`)
  233. )
  234. }
  235. tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry {
  236. const options = [
  237. new LogLine(`${user.name.capital} ${Words.Churns.singular} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`),
  238. new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`),
  239. 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(), `.`)
  240. ]
  241. if (this.fluid) {
  242. 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}.`))
  243. }
  244. const result: Array<LogEntry> = [
  245. new RandomEntry(...options)
  246. ]
  247. if (Math.random() < 0.3) {
  248. result.push(new FormatEntry(new LogLine(`${Onomatopoeia.Gurgle}`), FormatOpt.Onomatopoeia))
  249. }
  250. return new LogLines(...result)
  251. }
  252. digestLine (user: Creature, target: Creature): LogEntry {
  253. 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)}.`)
  254. }
  255. absorbLine (user: Creature, target: Creature): LogEntry {
  256. 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}.`)
  257. }
  258. tick (dt: number, victims?: Array<Creature>): LogEntry {
  259. const justDigested: Array<Creature> = []
  260. const justAbsorbed: Array<Creature> = []
  261. const damageResults: Array<LogEntry> = []
  262. const tickedEntryList: LogEntry[] = []
  263. if (this.capabilities.has(ContainerCapability.Digest)) {
  264. this.contents.forEach(prey => {
  265. if (victims === undefined || victims.indexOf(prey) >= 0) {
  266. const scaled = this.damage.calc(this.owner, prey).scale(dt / 60)
  267. const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled)
  268. if (modified.nonzero()) {
  269. tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified }))
  270. damageResults.push(prey.takeDamage(modified))
  271. }
  272. if (prey.vigors[Vigor.Health] <= 0) {
  273. prey.destroyed = true
  274. this.digested.push(prey)
  275. justDigested.push(prey)
  276. damageResults.push(this.onDigest(prey))
  277. }
  278. }
  279. })
  280. }
  281. const tickedEntries = new LogLines(...tickedEntryList)
  282. this.digested.forEach(prey => {
  283. if (victims === undefined || victims.indexOf(prey) >= 0) {
  284. const scaled = this.damage.calc(this.owner, prey).scale(dt / 60)
  285. const damageTotal: number = scaled.damages.filter(instance => instance.target === Vigor.Health).reduce(
  286. (total: number, instance: DamageInstance) => total + instance.amount,
  287. 0
  288. )
  289. const massStolen = Math.min(damageTotal / 100, prey.voreStats.Mass)
  290. prey.voreStats.Mass -= massStolen
  291. this.owner.voreStats.Mass += massStolen
  292. if (prey.voreStats.Mass === 0) {
  293. this.absorbed.push(prey)
  294. justAbsorbed.push(prey)
  295. damageResults.push(this.onAbsorb(prey))
  296. }
  297. }
  298. })
  299. const digestedEntries = this.digest(justDigested)
  300. const absorbedEntries = this.absorb(justAbsorbed)
  301. this.contents = this.contents.filter(prey => {
  302. return prey.vigors[Vigor.Health] > 0
  303. })
  304. this.digested = this.digested.filter(prey => {
  305. return prey.voreStats.Mass > 0
  306. })
  307. return new LogLines(tickedEntries, new LogLines(...damageResults), digestedEntries, absorbedEntries)
  308. }
  309. absorb (preys: Creature[]): LogEntry {
  310. return new LogLines(...preys.map(prey => this.absorbLine(this.owner, prey)))
  311. }
  312. digest (preys: Creature[]): LogEntry {
  313. const results = preys.map(prey => this.digestLine(this.owner, prey))
  314. if (preys.length > 0 && this.gas) {
  315. results.push(new LogLine(
  316. `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}.`
  317. ))
  318. }
  319. return new LogLines(...results)
  320. }
  321. onAbsorb (prey: Creature): LogEntry {
  322. return this.voreRelay.dispatch("onAbsorbed", this, { prey: prey })
  323. }
  324. onDigest (prey: Creature): LogEntry {
  325. return this.voreRelay.dispatch("onDigested", this, { prey: prey })
  326. }
  327. }
  328. export class Stomach extends DefaultContainer {
  329. fluid = {
  330. color: new Adjective("green"),
  331. name: new Noun("chyme"),
  332. sound: new Verb("gurgle"),
  333. sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed")
  334. }
  335. gas = {
  336. bubbleVerb: new Verb("bubble", "bubbles", "bubbling", "bubbled"),
  337. color: new Adjective("hazy"),
  338. name: new Noun("fume", "fumes"),
  339. releaseVerb: new Verb("belch", "belches", "belching", "belched"),
  340. smell: new Adjective("acrid"),
  341. exit: new Noun("jaws")
  342. }
  343. wall = {
  344. color: new Adjective("red"),
  345. material: new Noun("muscle"),
  346. name: new Noun("wall"),
  347. texture: new Adjective("slimy")
  348. }
  349. constructor (owner: Creature, capacityFactor: number, damage: DamageFormula) {
  350. super(new Noun("stomach"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([
  351. ContainerCapability.Digest,
  352. ContainerCapability.Absorb
  353. ]))
  354. this.voreRelay.subscribe("onEntered", (sender: Container, args: { prey: Creature }) => {
  355. return new FormatEntry(new LogLine(`${Onomatopoeia.Glunk}`), FormatOpt.Onomatopoeia)
  356. })
  357. this.damage = damage
  358. }
  359. }
  360. export class Throat extends DefaultContainer {
  361. fluid = {
  362. color: new Adjective("clear"),
  363. name: new RandomWord([
  364. new Noun("saliva"),
  365. new Noun("drool"),
  366. new Noun("slobber")
  367. ]),
  368. sound: new Verb("squish", "squishes"),
  369. sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed")
  370. }
  371. wall = {
  372. color: new Adjective("red"),
  373. material: new Noun("muscle"),
  374. name: new Noun("wall"),
  375. texture: new Adjective("slimy")
  376. }
  377. constructor (owner: Creature, capacityFactor: number) {
  378. super(new Noun("throat"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([
  379. ContainerCapability.Consume,
  380. ContainerCapability.Release
  381. ]))
  382. this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => {
  383. return new FormatEntry(new LogLine(`${Onomatopoeia.Swallow}`), FormatOpt.Onomatopoeia)
  384. })
  385. }
  386. }
  387. export function transferDescription (verb: Word, preposition: Preposition): ((from: Container, to: Container, prey: Creature) => LogEntry) {
  388. return (from: Container, to: Container, prey: Creature) => {
  389. 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}.`)
  390. }
  391. }