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

313 строки
8.5 KiB

  1. import { Creature, POV, Entity } from './entity'
  2. import { POVPair, POVPairArgs, TextLike, DynText, LiveText } from './language'
  3. import { VoreContainer } from './vore'
  4. import { LogEntry, LogLines, CompositeLog, FAElem, LogLine, FormatEntry, FormatOpt, PropElem } from './interface'
  5. import { StatTest, StatVigorTest } from './combat/tests'
  6. export enum DamageType {
  7. Pierce = "Pierce",
  8. Slash = "Slash",
  9. Crush = "Crush",
  10. Acid = "Acid",
  11. Seduction = "Seduction",
  12. Dominance = "Dominance"
  13. }
  14. export interface DamageInstance {
  15. type: DamageType;
  16. amount: number;
  17. target: Vigor | Stat;
  18. }
  19. export enum Vigor {
  20. Health = "Health",
  21. Stamina = "Stamina",
  22. Resolve = "Resolve"
  23. }
  24. export const VigorIcons: {[key in Vigor]: string} = {
  25. Health: "fas fa-heart",
  26. Stamina: "fas fa-bolt",
  27. Resolve: "fas fa-brain"
  28. }
  29. export const VigorDescs: {[key in Vigor]: string} = {
  30. Health: "How much damage you can take",
  31. Stamina: "How much energy you have",
  32. Resolve: "How much dominance you can resist"
  33. }
  34. export type Vigors = {[key in Vigor]: number}
  35. export enum Stat {
  36. Toughness = "Toughness",
  37. Power = "Power",
  38. Speed = "Speed",
  39. Willpower = "Willpower",
  40. Charm = "Charm"
  41. }
  42. export type Stats = {[key in Stat]: number}
  43. export const StatIcons: {[key in Stat]: string} = {
  44. Toughness: 'fas fa-heartbeat',
  45. Power: 'fas fa-fist-raised',
  46. Speed: 'fas fa-feather',
  47. Willpower: 'fas fa-book',
  48. Charm: 'fas fa-comments'
  49. }
  50. export const StatDescs: {[key in Stat]: string} = {
  51. Toughness: 'Your physical resistance',
  52. Power: 'Your physical power',
  53. Speed: 'How quickly you can act',
  54. Willpower: 'Your mental resistance',
  55. Charm: 'Your mental power'
  56. }
  57. export enum VoreStat {
  58. Mass = "Mass",
  59. Bulk = "Bulk",
  60. PreyCount = "Prey Count"
  61. }
  62. export type VoreStats = {[key in VoreStat]: number}
  63. export const VoreStatIcons: {[key in VoreStat]: string} = {
  64. [VoreStat.Mass]: "fas fa-weight",
  65. [VoreStat.Bulk]: "fas fa-weight-hanging",
  66. [VoreStat.PreyCount]: "fas fa-utensils"
  67. }
  68. export const VoreStatDescs: {[key in VoreStat]: string} = {
  69. [VoreStat.Mass]: "How much you weigh",
  70. [VoreStat.Bulk]: "Your weight, plus the weight of your prey",
  71. [VoreStat.PreyCount]: "How many creatures you've got inside of you"
  72. }
  73. export interface CombatTest {
  74. test: (user: Creature, target: Creature) => boolean;
  75. odds: (user: Creature, target: Creature) => number;
  76. explain: (user: Creature, target: Creature) => LogEntry;
  77. }
  78. export interface Effect {
  79. name: string;
  80. desc: string;
  81. apply: (target: Creature) => LogEntry;
  82. }
  83. /**
  84. * An instance of damage. Contains zero or more [[DamageInstance]] objects
  85. */
  86. export class Damage {
  87. readonly damages: DamageInstance[]
  88. constructor (...damages: DamageInstance[]) {
  89. this.damages = damages
  90. }
  91. scale (factor: number): Damage {
  92. const results: Array<DamageInstance> = []
  93. this.damages.forEach(damage => {
  94. results.push({
  95. type: damage.type,
  96. amount: damage.amount * factor,
  97. target: damage.target
  98. })
  99. })
  100. return new Damage(...results)
  101. }
  102. // TODO make this combine damage instances when appropriate
  103. combine (other: Damage): Damage {
  104. return new Damage(...this.damages.concat(other.damages))
  105. }
  106. toString (): string {
  107. return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
  108. }
  109. render (): LogEntry {
  110. return new LogLine(...this.damages.flatMap(instance => {
  111. if (instance.target in Vigor) {
  112. return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type]
  113. } else if (instance.target in Stat) {
  114. return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type]
  115. } else {
  116. // this should never happen!
  117. return []
  118. }
  119. }))
  120. }
  121. renderShort (): LogEntry {
  122. const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
  123. const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {})
  124. this.damages.forEach(instance => {
  125. if (instance.target in Vigor) {
  126. vigorTotals[instance.target as Vigor] += instance.amount
  127. } else if (instance.target in Stat) {
  128. statTotals[instance.target as Stat] += 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. {
  180. amount: factor.fraction * user.stats[factor.stat],
  181. target: factor.target,
  182. type: factor.type
  183. }
  184. ))
  185. return new Damage(...instances)
  186. }
  187. describe (user: Creature, target: Creature): LogEntry {
  188. return new LogLine(
  189. this.explain(user),
  190. `, for a total of `,
  191. this.calc(user, target).renderShort()
  192. )
  193. }
  194. explain (user: Creature): LogEntry {
  195. return new LogLine(
  196. `Deal `,
  197. ...this.factors.map(factor => new LogLine(
  198. `${factor.fraction * 100}% of your `,
  199. new PropElem(factor.stat),
  200. ` as `,
  201. new PropElem(factor.target)
  202. )).joinGeneral(new LogLine(`, `), new LogLine(` and `))
  203. )
  204. }
  205. constructor (private factors: Array<{ stat: Stat; fraction: number; type: DamageType; target: Vigor|Stat }>) {
  206. }
  207. }
  208. export enum Side {
  209. Heroes,
  210. Monsters
  211. }
  212. /**
  213. * A Combatant has a list of possible actions to take, as well as a side.
  214. */
  215. export interface Combatant {
  216. actions: Array<Action>;
  217. groupActions: Array<GroupAction>;
  218. side: Side;
  219. }
  220. /**
  221. * An Action is anything that can be done by a [[Creature]] to a [[Creature]].
  222. */
  223. export abstract class Action {
  224. allowed (user: Creature, target: Creature): boolean {
  225. return this.conditions.every(cond => cond.allowed(user, target))
  226. }
  227. abstract execute (user: Creature, target: Creature): LogEntry
  228. abstract describe (user: Creature, target: Creature): LogEntry
  229. constructor (public name: TextLike, public desc: TextLike, private conditions: Array<Condition> = []) {
  230. }
  231. toString (): string {
  232. return this.name.toString()
  233. }
  234. }
  235. /**
  236. * A Condition describes whether or not something is permissible between two [[Creature]]s
  237. */
  238. export interface Condition {
  239. allowed: (user: Creature, target: Creature) => boolean;
  240. }
  241. export interface Actionable {
  242. actions: Array<Action>;
  243. }
  244. export abstract class GroupAction extends Action {
  245. allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> {
  246. return targets.filter(target => this.allowed(user, target))
  247. }
  248. executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
  249. return new LogLines(...targets.map(target => this.execute(user, target)))
  250. }
  251. abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry
  252. constructor (name: TextLike, desc: TextLike, conditions: Array<Condition>) {
  253. super(name, desc, conditions)
  254. }
  255. }