Feast 2.0!
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 

337 строки
9.1 KiB

  1. import { Creature } from './entity'
  2. import { TextLike } from './language'
  3. import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem } from './interface'
  4. export enum DamageType {
  5. Pierce = "Pierce",
  6. Slash = "Slash",
  7. Crush = "Crush",
  8. Acid = "Acid",
  9. Seduction = "Seduction",
  10. Dominance = "Dominance",
  11. Heal = "Heal"
  12. }
  13. export interface DamageInstance {
  14. type: DamageType;
  15. amount: number;
  16. target: Vigor | Stat;
  17. }
  18. export enum Vigor {
  19. Health = "Health",
  20. Stamina = "Stamina",
  21. Resolve = "Resolve"
  22. }
  23. export const VigorIcons: {[key in Vigor]: string} = {
  24. Health: "fas fa-heart",
  25. Stamina: "fas fa-bolt",
  26. Resolve: "fas fa-brain"
  27. }
  28. export const VigorDescs: {[key in Vigor]: string} = {
  29. Health: "How much damage you can take",
  30. Stamina: "How much energy you have",
  31. Resolve: "How much dominance you can resist"
  32. }
  33. export type Vigors = {[key in Vigor]: number}
  34. export enum Stat {
  35. Toughness = "Toughness",
  36. Power = "Power",
  37. Speed = "Speed",
  38. Willpower = "Willpower",
  39. Charm = "Charm"
  40. }
  41. export type Stats = {[key in Stat]: number}
  42. export const StatIcons: {[key in Stat]: string} = {
  43. Toughness: 'fas fa-heartbeat',
  44. Power: 'fas fa-fist-raised',
  45. Speed: 'fas fa-feather',
  46. Willpower: 'fas fa-book',
  47. Charm: 'fas fa-comments'
  48. }
  49. export const StatDescs: {[key in Stat]: string} = {
  50. Toughness: 'Your physical resistance',
  51. Power: 'Your physical power',
  52. Speed: 'How quickly you can act',
  53. Willpower: 'Your mental resistance',
  54. Charm: 'Your mental power'
  55. }
  56. export enum VoreStat {
  57. Mass = "Mass",
  58. Bulk = "Bulk",
  59. PreyCount = "Prey Count"
  60. }
  61. export type VoreStats = {[key in VoreStat]: number}
  62. export const VoreStatIcons: {[key in VoreStat]: string} = {
  63. [VoreStat.Mass]: "fas fa-weight",
  64. [VoreStat.Bulk]: "fas fa-weight-hanging",
  65. [VoreStat.PreyCount]: "fas fa-utensils"
  66. }
  67. export const VoreStatDescs: {[key in VoreStat]: string} = {
  68. [VoreStat.Mass]: "How much you weigh",
  69. [VoreStat.Bulk]: "Your weight, plus the weight of your prey",
  70. [VoreStat.PreyCount]: "How many creatures you've got inside of you"
  71. }
  72. export interface CombatTest {
  73. test: (user: Creature, target: Creature) => boolean;
  74. odds: (user: Creature, target: Creature) => number;
  75. explain: (user: Creature, target: Creature) => LogEntry;
  76. }
  77. export interface Effect {
  78. name: string;
  79. desc: string;
  80. apply: (target: Creature) => LogEntry;
  81. }
  82. /**
  83. * An instance of damage. Contains zero or more [[DamageInstance]] objects
  84. */
  85. export class Damage {
  86. readonly damages: DamageInstance[]
  87. constructor (...damages: DamageInstance[]) {
  88. this.damages = damages
  89. }
  90. scale (factor: number): Damage {
  91. const results: Array<DamageInstance> = []
  92. this.damages.forEach(damage => {
  93. results.push({
  94. type: damage.type,
  95. amount: damage.amount * factor,
  96. target: damage.target
  97. })
  98. })
  99. return new Damage(...results)
  100. }
  101. // TODO make this combine damage instances when appropriate
  102. combine (other: Damage): Damage {
  103. return new Damage(...this.damages.concat(other.damages))
  104. }
  105. toString (): string {
  106. return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
  107. }
  108. render (): LogEntry {
  109. return new LogLine(...this.damages.flatMap(instance => {
  110. if (instance.target in Vigor) {
  111. return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type]
  112. } else if (instance.target in Stat) {
  113. return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type]
  114. } else {
  115. // this should never happen!
  116. return []
  117. }
  118. }))
  119. }
  120. renderShort (): LogEntry {
  121. const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
  122. const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {})
  123. this.damages.forEach(instance => {
  124. const factor = instance.type === DamageType.Heal ? -1 : 1
  125. if (instance.target in Vigor) {
  126. vigorTotals[instance.target as Vigor] += factor * instance.amount
  127. } else if (instance.target in Stat) {
  128. statTotals[instance.target as Stat] += factor * instance.amount
  129. }
  130. })
  131. const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' '])
  132. const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' '])
  133. return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst)
  134. }
  135. }
  136. /**
  137. * Computes damage given the source and target of the damage.
  138. */
  139. export interface DamageFormula {
  140. calc (user: Creature, target: Creature): Damage;
  141. describe (user: Creature, target: Creature): LogEntry;
  142. explain (user: Creature): LogEntry;
  143. }
  144. /**
  145. * Simply returns the damage it was given.
  146. */
  147. export class ConstantDamageFormula implements DamageFormula {
  148. calc (user: Creature, target: Creature): Damage {
  149. return this.damage
  150. }
  151. constructor (private damage: Damage) {
  152. }
  153. describe (user: Creature, target: Creature): LogEntry {
  154. return this.explain(user)
  155. }
  156. explain (user: Creature): LogEntry {
  157. return new LogLine('Deal ', this.damage.renderShort(), ' damage')
  158. }
  159. }
  160. /**
  161. * Randomly scales the damage it was given with a factor of (1-x) to (1+x)
  162. */
  163. export class UniformRandomDamageFormula implements DamageFormula {
  164. calc (user: Creature, target: Creature): Damage {
  165. return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1)
  166. }
  167. constructor (private damage: Damage, private variance: number) {
  168. }
  169. describe (user: Creature, target: Creature): LogEntry {
  170. return this.explain(user)
  171. }
  172. explain (user: Creature): LogEntry {
  173. return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), ' damage.')
  174. }
  175. }
  176. export class StatDamageFormula implements DamageFormula {
  177. calc (user: Creature, target: Creature): Damage {
  178. const instances: Array<DamageInstance> = this.factors.map(factor => {
  179. if (factor.stat in Stat) {
  180. return {
  181. amount: factor.fraction * user.stats[factor.stat as Stat],
  182. target: factor.target,
  183. type: factor.type
  184. }
  185. } else if (factor.stat in VoreStat) {
  186. return {
  187. amount: factor.fraction * user.voreStats[factor.stat as VoreStat],
  188. target: factor.target,
  189. type: factor.type
  190. }
  191. } else {
  192. // should be impossible; .stat is Stat|VoreStat
  193. return {
  194. amount: 0,
  195. target: Vigor.Health,
  196. type: DamageType.Heal
  197. }
  198. }
  199. })
  200. return new Damage(...instances)
  201. }
  202. describe (user: Creature, target: Creature): LogEntry {
  203. return new LogLine(
  204. this.explain(user),
  205. `, for a total of `,
  206. this.calc(user, target).renderShort()
  207. )
  208. }
  209. explain (user: Creature): LogEntry {
  210. return new LogLine(
  211. `Deal `,
  212. ...this.factors.map(factor => new LogLine(
  213. `${factor.fraction * 100}% of your `,
  214. new PropElem(factor.stat),
  215. ` as `,
  216. new PropElem(factor.target)
  217. )).joinGeneral(new LogLine(`, `), new LogLine(` and `))
  218. )
  219. }
  220. constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) {
  221. }
  222. }
  223. export enum Side {
  224. Heroes,
  225. Monsters
  226. }
  227. /**
  228. * A Combatant has a list of possible actions to take, as well as a side.
  229. */
  230. export interface Combatant {
  231. actions: Array<Action>;
  232. groupActions: Array<GroupAction>;
  233. side: Side;
  234. }
  235. /**
  236. * An Action is anything that can be done by a [[Creature]] to a [[Creature]].
  237. */
  238. export abstract class Action {
  239. allowed (user: Creature, target: Creature): boolean {
  240. return this.conditions.every(cond => cond.allowed(user, target))
  241. }
  242. abstract execute (user: Creature, target: Creature): LogEntry
  243. abstract describe (user: Creature, target: Creature): LogEntry
  244. constructor (public name: TextLike, public desc: TextLike, private conditions: Array<Condition> = []) {
  245. }
  246. toString (): string {
  247. return this.name.toString()
  248. }
  249. }
  250. /**
  251. * A Condition describes whether or not something is permissible between two [[Creature]]s
  252. */
  253. export interface Condition {
  254. allowed: (user: Creature, target: Creature) => boolean;
  255. }
  256. export interface Actionable {
  257. actions: Array<Action>;
  258. }
  259. export abstract class GroupAction extends Action {
  260. allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> {
  261. return targets.filter(target => this.allowed(user, target))
  262. }
  263. executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
  264. return new LogLines(...targets.map(target => this.execute(user, target)))
  265. }
  266. abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry
  267. constructor (name: TextLike, desc: TextLike, conditions: Array<Condition>) {
  268. super(name, desc, conditions)
  269. }
  270. }
  271. /**
  272. * A displayable status effect (whether implicit, like being dead, or explicit, like having a stun effect on you)
  273. */
  274. export class VisibleStatus {
  275. constructor (public name: TextLike, public desc: TextLike, public icon: string) {
  276. }
  277. }