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.
 
 
 
 
 

334 lines
10 KiB

  1. import { Damage, Stats, Action, Vigor, Side, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors, Encounter } from '@/game/combat'
  2. import { Noun, Pronoun, SoloLine, Verb } from '@/game/language'
  3. import { LogEntry, LogLines, LogLine } from '@/game/interface'
  4. import { VoreType, Container } from '@/game/vore'
  5. import { Item, EquipmentSlot, Equipment, ItemKind, Currency } from '@/game/items'
  6. import { PassAction } from '@/game/combat/actions'
  7. import { AI, RandomAI } from '@/game/ai'
  8. import { Entity, Resistances } from '@/game/entity'
  9. import { Perk } from '@/game/combat/perks'
  10. import { VoreRelay } from '@/game/events'
  11. export class Creature extends Entity {
  12. voreRelay: VoreRelay = new VoreRelay()
  13. baseResistances: Resistances
  14. stats: Stats = (Object.keys(Stat) as Array<Stat>).reduce((result: Partial<Stats>, stat: Stat) => {
  15. Object.defineProperty(result, stat, {
  16. get: () => this.effects.reduce((total, effect) => effect.modStat(this, stat, total), this.baseStats[stat]),
  17. set: (value: number) => { this.baseStats[stat] = value },
  18. enumerable: true
  19. })
  20. return result
  21. }, {}) as Stats
  22. vigors: {[key in Vigor]: number} = {
  23. [Vigor.Health]: 100,
  24. [Vigor.Stamina]: 100,
  25. [Vigor.Resolve]: 100
  26. }
  27. destroyed = false;
  28. containers: Array<Container> = []
  29. containedIn: Container | null = null
  30. voreStats: VoreStats
  31. actions: Array<Action> = [];
  32. desc = "Some creature";
  33. get effects (): Array<Effective> {
  34. const effects: Array<Effective> = (this.statusEffects as Effective[]).concat(
  35. Object.values(this.equipment).filter(item => item !== undefined).flatMap(
  36. item => (item as Equipment).effects
  37. ),
  38. this.perks
  39. )
  40. return effects
  41. }
  42. statusEffects: Array<StatusEffect> = [];
  43. perks: Array<Perk> = [];
  44. items: Array<Item> = [];
  45. /* eslint-disable-next-line */
  46. wallet: { [key in Currency]: number } = Object.keys(Currency).reduce((total: any, key) => { total[key] = 0; return total }, {});
  47. otherActions: Array<Action> = [];
  48. side: Side;
  49. title = "Lv. 1 Creature";
  50. equipment: {[key in EquipmentSlot]?: Equipment } = {}
  51. ai: AI
  52. constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats, public preyPrefs: Set<VoreType>, public predPrefs: Set<VoreType>, private baseMass: number) {
  53. super(name, kind, pronouns)
  54. /* eslint-disable-next-line */
  55. this.baseResistances = Object.keys(DamageType).reduce((resist: any, key) => { resist[key] = 1; return resist }, {})
  56. Object.entries(this.maxVigors).forEach(([key, val]) => {
  57. this.vigors[key as Vigor] = val
  58. })
  59. this.actions.push(new PassAction())
  60. this.side = Side.Heroes
  61. /* eslint-disable-next-line */
  62. const self = this
  63. this.ai = new RandomAI(this)
  64. this.voreStats = {
  65. get [VoreStat.Bulk] () {
  66. return self.containers.reduce(
  67. (total: number, container: Container) => {
  68. return total + container.contents.reduce(
  69. (total: number, prey: Creature) => {
  70. return total + prey.voreStats.Bulk
  71. },
  72. 0
  73. ) + container.digested.reduce(
  74. (total: number, prey: Creature) => {
  75. return total + prey.voreStats.Bulk
  76. },
  77. 0
  78. )
  79. },
  80. self.voreStats.Mass
  81. )
  82. },
  83. get [VoreStat.Mass] () {
  84. const base = self.baseMass
  85. const adjusted = self.effects.reduce((scale: number, effect: Effective) => effect.scale(scale), base)
  86. return adjusted
  87. },
  88. // we want to account for anything changing our current size;
  89. // we will assume that the modifiers are all multiplicative
  90. set [VoreStat.Mass] (mass: number) {
  91. const modifier = self.effects.reduce((scale: number, effect: Effective) => effect.scale(scale), 1)
  92. const adjusted = mass / modifier
  93. self.baseMass = adjusted
  94. },
  95. get [VoreStat.Prey] () {
  96. return self.containers.reduce(
  97. (total: number, container: Container) => {
  98. return total + container.contents.concat(container.digested).reduce(
  99. (total: number, prey: Creature) => {
  100. return total + 1 + prey.voreStats[VoreStat.Prey]
  101. },
  102. 0
  103. )
  104. },
  105. 0
  106. )
  107. }
  108. }
  109. }
  110. resistanceTo (damageType: DamageType): number {
  111. return this.baseResistances[damageType]
  112. }
  113. get maxVigors (): Readonly<Vigors> {
  114. return {
  115. Health: this.stats.Toughness * 10 + this.stats.Power * 5,
  116. Resolve: this.stats.Willpower * 10 + this.stats.Charm * 5,
  117. Stamina: this.stats.Agility * 5 + this.stats.Reflexes * 5
  118. }
  119. }
  120. get disabled (): boolean {
  121. return Object.values(this.vigors).some(val => val <= 0)
  122. }
  123. effectiveDamage (damage: Damage): Damage {
  124. const newDamages: DamageInstance[] = []
  125. damage.damages.forEach(instance => {
  126. const factor = instance.type === DamageType.Heal ? -1 : 1
  127. const baseResistance: number = this.resistanceTo(instance.type)
  128. const resistance = baseResistance * factor
  129. newDamages.push({
  130. amount: instance.amount * resistance,
  131. target: instance.target,
  132. type: instance.type
  133. })
  134. })
  135. return new Damage(...newDamages)
  136. }
  137. takeDamage (damage: Damage): LogEntry {
  138. // first, we record health to decide if the entity just died
  139. const startHealth = this.vigors.Health
  140. damage = this.effectiveDamage(damage)
  141. damage.damages.forEach(instance => {
  142. if (instance.target in Vigor) {
  143. // just deal damage
  144. this.vigors[instance.target as Vigor] -= instance.amount
  145. } else if (instance.target in Stat) {
  146. // drain the stats, then deal damage to match
  147. const startVigors = this.maxVigors
  148. this.stats[instance.target as Stat] -= instance.amount
  149. const endVigors = this.maxVigors
  150. Object.keys(Vigor).map(vigor => {
  151. this.vigors[vigor as Vigor] -= startVigors[vigor as Vigor] - endVigors[vigor as Vigor]
  152. })
  153. }
  154. })
  155. Object.keys(Vigor).forEach(vigorStr => {
  156. const vigor = vigorStr as Vigor
  157. if (this.vigors[vigor] > this.maxVigors[vigor]) {
  158. this.vigors[vigor] = this.maxVigors[vigor]
  159. }
  160. })
  161. if (this.vigors.Health <= -this.maxVigors.Health) {
  162. this.destroyed = true
  163. }
  164. if (this.vigors.Health <= 0 && startHealth > 0) {
  165. return this.destroy()
  166. } else {
  167. return new LogLine()
  168. }
  169. }
  170. toString (): string {
  171. return this.name.toString()
  172. }
  173. applyEffect (effect: StatusEffect): LogEntry {
  174. this.statusEffects.push(effect)
  175. return effect.onApply(this)
  176. }
  177. addContainer (container: Container): void {
  178. this.containers.push(container)
  179. this.voreRelay.connect(container.voreRelay)
  180. }
  181. addPerk (perk: Perk): void {
  182. this.perks.push(perk)
  183. }
  184. // TODO replace the logic for getting blocked or prevented from acting
  185. executeAction (action: Action, targets: Array<Creature>): LogEntry {
  186. return action.try(this, targets)
  187. }
  188. removeEffect (effect: StatusEffect): LogEntry {
  189. this.statusEffects = this.statusEffects.filter(eff => eff !== effect)
  190. return effect.onRemove(this)
  191. }
  192. equip (item: Equipment, slot: EquipmentSlot) {
  193. const equipped = this.equipment[slot]
  194. if (equipped !== undefined) {
  195. this.unequip(slot)
  196. }
  197. this.equipment[slot] = item
  198. }
  199. unequip (slot: EquipmentSlot) {
  200. const item = this.equipment[slot]
  201. if (item !== undefined) {
  202. this.items.push(item)
  203. this.equipment[slot] = undefined
  204. }
  205. }
  206. get status (): Array<VisibleStatus> {
  207. const results: Array<VisibleStatus> = []
  208. if (this.vigors[Vigor.Health] <= 0) {
  209. results.push(new ImplicitStatus('Dead', 'Out of health', 'fas fa-heart'))
  210. }
  211. if (this.vigors[Vigor.Stamina] <= 0) {
  212. results.push(new ImplicitStatus('Unconscious', 'Out of stamina', 'fas fa-bolt'))
  213. }
  214. if (this.vigors[Vigor.Resolve] <= 0) {
  215. results.push(new ImplicitStatus('Broken', 'Out of resolve', 'fas fa-brain'))
  216. }
  217. if (this.containedIn !== null) {
  218. results.push(new ImplicitStatus('Eaten', 'Devoured by ' + this.containedIn.owner.name, 'fas fa-drumstick-bite'))
  219. }
  220. this.statusEffects.forEach(effect => {
  221. results.push(effect)
  222. })
  223. return results
  224. }
  225. allActions (target: Creature): Array<Action> {
  226. let choices = ([] as Action[]).concat(
  227. this.actions,
  228. this.containers.flatMap(container => container.actions),
  229. target.otherActions,
  230. Object.values(this.equipment).filter(item => item !== undefined).flatMap(item => (item as Equipment).actions),
  231. this.items.filter(item => item.kind === ItemKind.Consumable && !item.consumed).flatMap(item => item.actions),
  232. this.perks.flatMap(perk => perk.actions(this))
  233. )
  234. if (this.containedIn !== null) {
  235. choices = choices.concat(this.containedIn.actions)
  236. }
  237. return choices
  238. }
  239. validActions (target: Creature): Array<Action> {
  240. return this.allActions(target).filter(action => {
  241. return action.allowed(this, target)
  242. })
  243. }
  244. validSoloActions (target: Creature, encounter: Encounter): Array<Action> {
  245. return this.validActions(target).filter(action => action.targets(target, encounter).length === 1)
  246. }
  247. validGroupActions (target: Creature, encounter: Encounter): Array<Action> {
  248. return this.validActions(target).filter(action => action.targets(target, encounter).length > 1)
  249. }
  250. destroyLine: SoloLine<Creature> = (victim) => new LogLine(
  251. `${victim.name.capital} ${victim.name.conjugate(new Verb('die'))}`
  252. )
  253. destroy (): LogEntry {
  254. const released: Array<Creature> = this.containers.flatMap(container => {
  255. return container.contents.map(prey => {
  256. prey.containedIn = this.containedIn
  257. if (this.containedIn !== null) {
  258. this.containedIn.contents.push(prey)
  259. }
  260. return prey
  261. })
  262. })
  263. const names = released.reduce((list: Array<string>, prey: Creature) => list.concat([prey.name.toString()]), []).joinGeneral(", ", " and ").join("")
  264. if (released.length > 0) {
  265. if (this.containedIn === null) {
  266. return new LogLines(
  267. this.destroyLine(this),
  268. new LogLine(names + ` spill out!`)
  269. )
  270. } else {
  271. return new LogLines(
  272. this.destroyLine(this),
  273. new LogLine(names + ` spill out into ${this.containedIn.owner.name}'s ${this.containedIn.name}!`)
  274. )
  275. }
  276. } else {
  277. return this.destroyLine(this)
  278. }
  279. }
  280. }