Feast 2.0!
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 

489 líneas
13 KiB

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