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.
 
 
 
 
 

325 line
10 KiB

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