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.
 
 
 
 
 

438 lines
12 KiB

  1. import { Creature } from './entity'
  2. import { TextLike, DynText, ToBe, LiveText } from './language'
  3. import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog } 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. /**
  78. * An instance of damage. Contains zero or more [[DamageInstance]] objects
  79. */
  80. export class Damage {
  81. readonly damages: DamageInstance[]
  82. constructor (...damages: DamageInstance[]) {
  83. this.damages = damages
  84. }
  85. scale (factor: number): Damage {
  86. const results: Array<DamageInstance> = []
  87. this.damages.forEach(damage => {
  88. results.push({
  89. type: damage.type,
  90. amount: damage.amount * factor,
  91. target: damage.target
  92. })
  93. })
  94. return new Damage(...results)
  95. }
  96. // TODO make this combine damage instances when appropriate
  97. combine (other: Damage): Damage {
  98. return new Damage(...this.damages.concat(other.damages))
  99. }
  100. toString (): string {
  101. return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
  102. }
  103. render (): LogEntry {
  104. return new LogLine(...this.damages.flatMap(instance => {
  105. if (instance.target in Vigor) {
  106. return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type]
  107. } else if (instance.target in Stat) {
  108. return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type]
  109. } else {
  110. // this should never happen!
  111. return []
  112. }
  113. }))
  114. }
  115. renderShort (): LogEntry {
  116. const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
  117. const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {})
  118. this.damages.forEach(instance => {
  119. const factor = instance.type === DamageType.Heal ? -1 : 1
  120. if (instance.target in Vigor) {
  121. vigorTotals[instance.target as Vigor] += factor * instance.amount
  122. } else if (instance.target in Stat) {
  123. statTotals[instance.target as Stat] += factor * instance.amount
  124. }
  125. })
  126. const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' '])
  127. const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' '])
  128. return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst)
  129. }
  130. }
  131. /**
  132. * Computes damage given the source and target of the damage.
  133. */
  134. export interface DamageFormula {
  135. calc (user: Creature, target: Creature): Damage;
  136. describe (user: Creature, target: Creature): LogEntry;
  137. explain (user: Creature): LogEntry;
  138. }
  139. /**
  140. * Simply returns the damage it was given.
  141. */
  142. export class ConstantDamageFormula implements DamageFormula {
  143. calc (user: Creature, target: Creature): Damage {
  144. return this.damage
  145. }
  146. constructor (private damage: Damage) {
  147. }
  148. describe (user: Creature, target: Creature): LogEntry {
  149. return this.explain(user)
  150. }
  151. explain (user: Creature): LogEntry {
  152. return new LogLine('Deal ', this.damage.renderShort(), ' damage')
  153. }
  154. }
  155. /**
  156. * Randomly scales the damage it was given with a factor of (1-x) to (1+x)
  157. */
  158. export class UniformRandomDamageFormula implements DamageFormula {
  159. calc (user: Creature, target: Creature): Damage {
  160. return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1)
  161. }
  162. constructor (private damage: Damage, private variance: number) {
  163. }
  164. describe (user: Creature, target: Creature): LogEntry {
  165. return this.explain(user)
  166. }
  167. explain (user: Creature): LogEntry {
  168. return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), ' damage.')
  169. }
  170. }
  171. export class StatDamageFormula implements DamageFormula {
  172. calc (user: Creature, target: Creature): Damage {
  173. const instances: Array<DamageInstance> = this.factors.map(factor => {
  174. if (factor.stat in Stat) {
  175. return {
  176. amount: factor.fraction * user.stats[factor.stat as Stat],
  177. target: factor.target,
  178. type: factor.type
  179. }
  180. } else if (factor.stat in VoreStat) {
  181. return {
  182. amount: factor.fraction * user.voreStats[factor.stat as VoreStat],
  183. target: factor.target,
  184. type: factor.type
  185. }
  186. } else {
  187. // should be impossible; .stat is Stat|VoreStat
  188. return {
  189. amount: 0,
  190. target: Vigor.Health,
  191. type: DamageType.Heal
  192. }
  193. }
  194. })
  195. return new Damage(...instances)
  196. }
  197. describe (user: Creature, target: Creature): LogEntry {
  198. return new LogLine(
  199. this.explain(user),
  200. `, for a total of `,
  201. this.calc(user, target).renderShort()
  202. )
  203. }
  204. explain (user: Creature): LogEntry {
  205. return new LogLine(
  206. `Deal `,
  207. ...this.factors.map(factor => new LogLine(
  208. `${factor.fraction * 100}% of your `,
  209. new PropElem(factor.stat),
  210. ` as `,
  211. new PropElem(factor.target)
  212. )).joinGeneral(new LogLine(`, `), new LogLine(` and `))
  213. )
  214. }
  215. constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) {
  216. }
  217. }
  218. export enum Side {
  219. Heroes,
  220. Monsters
  221. }
  222. /**
  223. * A Combatant has a list of possible actions to take, as well as a side.
  224. */
  225. export interface Combatant {
  226. actions: Array<Action>;
  227. groupActions: Array<GroupAction>;
  228. side: Side;
  229. }
  230. /**
  231. * An Action is anything that can be done by a [[Creature]] to a [[Creature]].
  232. */
  233. export abstract class Action {
  234. allowed (user: Creature, target: Creature): boolean {
  235. return this.conditions.every(cond => cond.allowed(user, target))
  236. }
  237. abstract execute (user: Creature, target: Creature): LogEntry
  238. abstract describe (user: Creature, target: Creature): LogEntry
  239. constructor (public name: TextLike, public desc: TextLike, private conditions: Array<Condition> = []) {
  240. }
  241. toString (): string {
  242. return this.name.toString()
  243. }
  244. }
  245. /**
  246. * A Condition describes whether or not something is permissible between two [[Creature]]s
  247. */
  248. export interface Condition {
  249. allowed: (user: Creature, target: Creature) => boolean;
  250. }
  251. export interface Actionable {
  252. actions: Array<Action>;
  253. }
  254. export abstract class GroupAction extends Action {
  255. allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> {
  256. return targets.filter(target => this.allowed(user, target))
  257. }
  258. executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
  259. return new LogLines(...targets.map(target => this.execute(user, target)))
  260. }
  261. abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry
  262. constructor (name: TextLike, desc: TextLike, conditions: Array<Condition>) {
  263. super(name, desc, conditions)
  264. }
  265. }
  266. /**
  267. * Individual status effects, items, etc. should override some of these hooks.
  268. * Some hooks just produce a log entry.
  269. * Some hooks return results along with a log entry.
  270. */
  271. export class Effective {
  272. onApply (creature: Creature): LogEntry { return nilLog }
  273. onRemove (creature: Creature): LogEntry { return nilLog }
  274. preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
  275. return {
  276. prevented: false,
  277. log: nilLog
  278. }
  279. }
  280. preDamage (creature: Creature, damage: Damage): Damage {
  281. return damage
  282. }
  283. preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
  284. return {
  285. prevented: false,
  286. log: nilLog
  287. }
  288. }
  289. }
  290. /**
  291. * A displayable status effect
  292. */
  293. export interface VisibleStatus {
  294. name: TextLike;
  295. desc: TextLike;
  296. icon: TextLike;
  297. topLeft: string;
  298. bottomRight: string;
  299. }
  300. /**
  301. * This kind of status is never explicitly applied to an entity -- e.g., a dead entity will show
  302. * a status indicating that it is dead, but entities cannot be "given" the dead effect
  303. */
  304. export class ImplicitStatus implements VisibleStatus {
  305. constructor (public name: TextLike, public desc: TextLike, public icon: string) {
  306. }
  307. topLeft = ''
  308. bottomRight = ''
  309. }
  310. /**
  311. * This kind of status is explicitly given to a creature.
  312. */
  313. export abstract class StatusEffect extends Effective implements VisibleStatus {
  314. constructor (public name: TextLike, public desc: TextLike, public icon: string) {
  315. super()
  316. }
  317. get topLeft () { return '' }
  318. get bottomRight () { return '' }
  319. }
  320. /**
  321. * An Encounter describes a fight: who is in it and whose turn it is
  322. */
  323. export class Encounter {
  324. private initiatives: Map<Creature, number>
  325. currentMove: Creature
  326. turnTime = 100
  327. constructor (public combatants: Creature[]) {
  328. this.initiatives = new Map()
  329. combatants.forEach(combatant => this.initiatives.set(combatant, 0))
  330. this.currentMove = combatants[0]
  331. this.nextMove()
  332. }
  333. nextMove (): void {
  334. this.initiatives.set(this.currentMove, 0)
  335. const times = new Map<Creature, number>()
  336. this.combatants.forEach(combatant => {
  337. // this should never be undefined
  338. const currentProgress = this.initiatives.get(combatant) ?? 0
  339. const remaining = (this.turnTime - currentProgress) / Math.max(combatant.stats.Speed, 1)
  340. times.set(combatant, remaining)
  341. })
  342. this.currentMove = this.combatants.reduce((closest, next) => {
  343. const closestTime = times.get(closest) ?? 0
  344. const nextTime = times.get(next) ?? 0
  345. return closestTime <= nextTime ? closest : next
  346. }, this.combatants[0])
  347. const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.max(this.currentMove.stats.Speed, 1)
  348. this.combatants.forEach(combatant => {
  349. // still not undefined...
  350. const currentProgress = this.initiatives.get(combatant) ?? 0
  351. this.initiatives.set(combatant, currentProgress + closestRemaining * Math.max(combatant.stats.Speed, 1))
  352. console.log(combatant.name.toString(), currentProgress, closestRemaining)
  353. })
  354. // TODO: still let the creature use drained-vigor moves
  355. console.log(this.currentMove.name.toString())
  356. if (this.currentMove.disabled) {
  357. this.nextMove()
  358. }
  359. }
  360. }