Feast 2.0!
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

240 lines
7.2 KiB

  1. import { DamageType, Damage, Combatant, Stats, Action, Vigor, VoreStats, VoreStat, Stat, Side, GroupAction, Vigors, VisibleStatus, ImplicitStatus, StatusEffect } from './combat'
  2. import { Noun, Pronoun, TextLike, POV } from './language'
  3. import { LogEntry, LogLine, LogLines } from './interface'
  4. import { Vore, VoreContainer, VoreType, Container } from './vore'
  5. import { Item } from './items'
  6. import { PassAction } from './combat/actions'
  7. export interface Entity {
  8. name: Noun;
  9. pronouns: Pronoun;
  10. baseName: Noun;
  11. basePronouns: Pronoun;
  12. title: TextLike;
  13. desc: TextLike;
  14. perspective: POV;
  15. }
  16. export interface Mortal extends Entity {
  17. kind: Noun;
  18. vigors: {[key in Vigor]: number};
  19. maxVigors: Readonly<{[key in Vigor]: number}>;
  20. disabled: boolean;
  21. resistances: Map<DamageType, number>;
  22. takeDamage: (damage: Damage) => void;
  23. stats: Stats;
  24. baseStats: Stats;
  25. status: VisibleStatus[];
  26. destroy: () => LogEntry;
  27. }
  28. export class Creature extends Vore implements Combatant {
  29. title = "Lv. 1 Creature"
  30. desc = "Some creature"
  31. vigors = {
  32. [Vigor.Health]: 100,
  33. [Vigor.Stamina]: 100,
  34. [Vigor.Resolve]: 100
  35. }
  36. get maxVigors (): Readonly<Vigors> {
  37. return {
  38. Health: this.stats.Toughness * 10 + this.stats.Power * 5,
  39. Resolve: this.stats.Willpower * 10 + this.stats.Charm * 5,
  40. Stamina: this.stats.Speed * 10 + this.stats.Power * 2.5 + this.stats.Charm * 2.5
  41. }
  42. }
  43. baseStats: Stats
  44. voreStats: VoreStats
  45. side: Side
  46. effects: Array<StatusEffect> = []
  47. applyEffect (effect: StatusEffect): LogEntry {
  48. this.effects.push(effect)
  49. return effect.onApply(this)
  50. }
  51. removeEffect (effect: StatusEffect): LogEntry {
  52. this.effects = this.effects.filter(eff => eff !== effect)
  53. return effect.onRemove(this)
  54. }
  55. executeAction (action: Action, target: Creature): LogEntry {
  56. const effectResults = this.effects.map(effect => effect.preAction(this))
  57. const blocking = effectResults.filter(result => result.prevented)
  58. if (blocking.length > 0) {
  59. return new LogLines(...blocking.map(result => result.log))
  60. } else {
  61. return action.execute(this, target)
  62. }
  63. }
  64. get disabled (): boolean {
  65. return Object.values(this.vigors).some(val => val <= 0)
  66. }
  67. resistances: Map<DamageType, number> = new Map()
  68. perspective: POV = POV.Third
  69. containers: Array<VoreContainer> = []
  70. otherContainers: Array<Container> = []
  71. actions: Array<Action> = []
  72. groupActions: Array<GroupAction> = []
  73. otherActions: Array<Action> = []
  74. items: Array<Item> = []
  75. get bulk (): number {
  76. return this.voreStats.Mass + this.containers.reduce((total, conatiner) => { return total + conatiner.contents.reduce((total, prey) => total + prey.voreStats.Bulk, 0) }, 0)
  77. }
  78. containedIn: VoreContainer|null = null;
  79. constructor (public baseName: Noun, public kind: Noun, public basePronouns: Pronoun, public stats: Stats, public preyPrefs: Set<VoreType>, public predPrefs: Set<VoreType>, mass: number) {
  80. super()
  81. const containers = this.containers
  82. this.actions.push(new PassAction())
  83. Object.entries(this.maxVigors).forEach(([key, val]) => {
  84. this.vigors[key as Vigor] = val
  85. })
  86. this.baseStats = Object.keys(Stat).reduce((base: any, key) => { base[key] = stats[key as Stat]; return base }, {})
  87. this.side = Side.Heroes
  88. this.voreStats = {
  89. get [VoreStat.Bulk] () {
  90. return containers.reduce(
  91. (total: number, container: VoreContainer) => {
  92. return total + container.contents.reduce(
  93. (total: number, prey: Vore) => {
  94. return total + prey.voreStats.Bulk
  95. },
  96. 0
  97. ) + container.digested.reduce(
  98. (total: number, prey: Vore) => {
  99. return total + prey.voreStats.Bulk
  100. },
  101. 0
  102. )
  103. },
  104. this.Mass
  105. )
  106. },
  107. [VoreStat.Mass]: mass,
  108. get [VoreStat.PreyCount] () {
  109. return containers.reduce(
  110. (total: number, container: VoreContainer) => {
  111. return total + container.contents.reduce(
  112. (total: number, prey: Vore) => {
  113. return total + 1 + prey.voreStats[VoreStat.PreyCount]
  114. },
  115. 0
  116. )
  117. },
  118. 0
  119. )
  120. }
  121. }
  122. }
  123. toString (): string {
  124. return this.name.toString()
  125. }
  126. /**
  127. * Determines how much damage an attack would do
  128. */
  129. effectiveDamage (damage: Damage): Damage {
  130. return this.effects.reduce((modifiedDamage: Damage, effect: StatusEffect) => {
  131. return effect.preDamage(this, modifiedDamage)
  132. }, damage)
  133. }
  134. takeDamage (damage: Damage): LogEntry {
  135. // first, we record health to decide if the entity just died
  136. const startHealth = this.vigors.Health
  137. damage = this.effectiveDamage(damage)
  138. damage.damages.forEach(instance => {
  139. const factor = instance.type === DamageType.Heal ? -1 : 1
  140. const effectiveResistance: number|undefined = this.resistances.get(instance.type)
  141. const resistance = effectiveResistance === undefined ? factor : effectiveResistance * factor
  142. if (instance.target in Vigor) {
  143. // just deal damage
  144. this.vigors[instance.target as Vigor] -= instance.amount * resistance
  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 * resistance
  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. if (this.vigors.Health <= 0 && startHealth > 0) {
  156. return this.destroy()
  157. } else {
  158. return new LogLine()
  159. }
  160. }
  161. get status (): Array<VisibleStatus> {
  162. const results: Array<VisibleStatus> = []
  163. if (this.vigors[Vigor.Health] <= 0) {
  164. results.push(new ImplicitStatus('Dead', 'Out of health', 'fas fa-heart'))
  165. }
  166. if (this.vigors[Vigor.Stamina] <= 0) {
  167. results.push(new ImplicitStatus('Unconscious', 'Out of stamina', 'fas fa-bolt'))
  168. }
  169. if (this.vigors[Vigor.Resolve] <= 0) {
  170. results.push(new ImplicitStatus('Broken', 'Out of resolve', 'fas fa-brain'))
  171. }
  172. if (this.containedIn !== null) {
  173. results.push(new ImplicitStatus('Eaten', 'Devoured by ' + this.containedIn.owner.name, 'fas fa-drumstick-bite'))
  174. }
  175. this.effects.forEach(effect => {
  176. results.push(effect)
  177. })
  178. return results
  179. }
  180. validActions (target: Creature): Array<Action> {
  181. let choices = this.actions.concat(
  182. this.containers.flatMap(container => container.actions)
  183. ).concat(
  184. target.otherActions.concat(
  185. this.otherContainers.flatMap(container => container.actions).concat(
  186. this.items.flatMap(item => item.actions)
  187. )
  188. )
  189. )
  190. if (this.containedIn !== null) {
  191. choices = choices.concat(this.containedIn.actions)
  192. }
  193. return choices.filter(action => {
  194. return action.allowed(this, target)
  195. })
  196. }
  197. validGroupActions (targets: Array<Creature>): Array<GroupAction> {
  198. const choices = this.groupActions
  199. return choices.filter(action => {
  200. return targets.some(target => action.allowed(this, target))
  201. })
  202. }
  203. destroy (): LogEntry {
  204. return super.destroy()
  205. }
  206. }