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.
 
 
 
 
 

482 lines
16 KiB

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