Feast 2.0!
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

604 lignes
16 KiB

  1. import { Creature } from "./creature"
  2. import { TextLike, DynText, ToBe, LiveText, PairLineArgs, PairLine } from './language'
  3. import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog } from './interface'
  4. import { Resistances } from './entity'
  5. export enum DamageType {
  6. Pierce = "Pierce",
  7. Slash = "Slash",
  8. Crush = "Crush",
  9. Acid = "Acid",
  10. Seduction = "Seduction",
  11. Dominance = "Dominance",
  12. Heal = "Heal",
  13. Pure = "Pure"
  14. }
  15. export interface DamageInstance {
  16. type: DamageType;
  17. amount: number;
  18. target: Vigor | Stat;
  19. }
  20. export enum Vigor {
  21. Health = "Health",
  22. Stamina = "Stamina",
  23. Resolve = "Resolve"
  24. }
  25. export const VigorIcons: {[key in Vigor]: string} = {
  26. Health: "fas fa-heart",
  27. Stamina: "fas fa-bolt",
  28. Resolve: "fas fa-brain"
  29. }
  30. export const VigorDescs: {[key in Vigor]: string} = {
  31. Health: "How much damage you can take",
  32. Stamina: "How much energy you have",
  33. Resolve: "How much dominance you can resist"
  34. }
  35. export type Vigors = {[key in Vigor]: number}
  36. export enum Stat {
  37. Toughness = "Toughness",
  38. Power = "Power",
  39. Speed = "Speed",
  40. Willpower = "Willpower",
  41. Charm = "Charm"
  42. }
  43. export type Stats = {[key in Stat]: number}
  44. export const StatIcons: {[key in Stat]: string} = {
  45. Toughness: 'fas fa-heartbeat',
  46. Power: 'fas fa-fist-raised',
  47. Speed: 'fas fa-feather',
  48. Willpower: 'fas fa-book',
  49. Charm: 'fas fa-comments'
  50. }
  51. export const StatDescs: {[key in Stat]: string} = {
  52. Toughness: 'Your physical resistance',
  53. Power: 'Your physical power',
  54. Speed: 'How quickly you can act',
  55. Willpower: 'Your mental resistance',
  56. Charm: 'Your mental power'
  57. }
  58. export enum VoreStat {
  59. Mass = "Mass",
  60. Bulk = "Bulk",
  61. PreyCount = "Prey Count"
  62. }
  63. export type VoreStats = {[key in VoreStat]: number}
  64. export const VoreStatIcons: {[key in VoreStat]: string} = {
  65. [VoreStat.Mass]: "fas fa-weight",
  66. [VoreStat.Bulk]: "fas fa-weight-hanging",
  67. [VoreStat.PreyCount]: "fas fa-utensils"
  68. }
  69. export const VoreStatDescs: {[key in VoreStat]: string} = {
  70. [VoreStat.Mass]: "How much you weigh",
  71. [VoreStat.Bulk]: "Your weight, plus the weight of your prey",
  72. [VoreStat.PreyCount]: "How many creatures you've got inside of you"
  73. }
  74. export interface CombatTest {
  75. test: (user: Creature, target: Creature) => boolean;
  76. odds: (user: Creature, target: Creature) => number;
  77. explain: (user: Creature, target: Creature) => LogEntry;
  78. }
  79. /**
  80. * An instance of damage. Contains zero or more [[DamageInstance]] objects
  81. */
  82. export class Damage {
  83. readonly damages: DamageInstance[]
  84. constructor (...damages: DamageInstance[]) {
  85. this.damages = damages
  86. }
  87. scale (factor: number): Damage {
  88. const results: Array<DamageInstance> = []
  89. this.damages.forEach(damage => {
  90. results.push({
  91. type: damage.type,
  92. amount: damage.amount * factor,
  93. target: damage.target
  94. })
  95. })
  96. return new Damage(...results)
  97. }
  98. // TODO make this combine damage instances when appropriate
  99. combine (other: Damage): Damage {
  100. return new Damage(...this.damages.concat(other.damages))
  101. }
  102. toString (): string {
  103. return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
  104. }
  105. render (): LogEntry {
  106. return new LogLine(...this.damages.flatMap(instance => {
  107. if (instance.target in Vigor) {
  108. return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type]
  109. } else if (instance.target in Stat) {
  110. return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type]
  111. } else {
  112. // this should never happen!
  113. return []
  114. }
  115. }))
  116. }
  117. renderShort (): LogEntry {
  118. const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
  119. const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {})
  120. this.damages.forEach(instance => {
  121. const factor = instance.type === DamageType.Heal ? -1 : 1
  122. if (instance.target in Vigor) {
  123. vigorTotals[instance.target as Vigor] += factor * instance.amount
  124. } else if (instance.target in Stat) {
  125. statTotals[instance.target as Stat] += factor * instance.amount
  126. }
  127. })
  128. const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' '])
  129. const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' '])
  130. return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst)
  131. }
  132. }
  133. /**
  134. * Computes damage given the source and target of the damage.
  135. */
  136. export interface DamageFormula {
  137. calc (user: Creature, target: Creature): Damage;
  138. describe (user: Creature, target: Creature): LogEntry;
  139. explain (user: Creature): LogEntry;
  140. }
  141. /**
  142. * Simply returns the damage it was given.
  143. */
  144. export class ConstantDamageFormula implements DamageFormula {
  145. constructor (private damage: Damage) {
  146. }
  147. calc (user: Creature, target: Creature): Damage {
  148. return this.damage
  149. }
  150. describe (user: Creature, target: Creature): LogEntry {
  151. return this.explain(user)
  152. }
  153. explain (user: Creature): LogEntry {
  154. return new LogLine('Deal ', this.damage.renderShort(), ' damage')
  155. }
  156. }
  157. /**
  158. * Randomly scales the damage it was given with a factor of (1-x) to (1+x)
  159. */
  160. export class UniformRandomDamageFormula implements DamageFormula {
  161. constructor (private damage: Damage, private variance: number) {
  162. }
  163. calc (user: Creature, target: Creature): Damage {
  164. return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1)
  165. }
  166. describe (user: Creature, target: Creature): LogEntry {
  167. return this.explain(user)
  168. }
  169. explain (user: Creature): LogEntry {
  170. return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), ' damage.')
  171. }
  172. }
  173. export class StatDamageFormula implements DamageFormula {
  174. constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) {
  175. }
  176. calc (user: Creature, target: Creature): Damage {
  177. const instances: Array<DamageInstance> = this.factors.map(factor => {
  178. if (factor.stat in Stat) {
  179. return {
  180. amount: factor.fraction * user.stats[factor.stat as Stat],
  181. target: factor.target,
  182. type: factor.type
  183. }
  184. } else if (factor.stat in VoreStat) {
  185. return {
  186. amount: factor.fraction * user.voreStats[factor.stat as VoreStat],
  187. target: factor.target,
  188. type: factor.type
  189. }
  190. } else {
  191. // should be impossible; .stat is Stat|VoreStat
  192. return {
  193. amount: 0,
  194. target: Vigor.Health,
  195. type: DamageType.Heal
  196. }
  197. }
  198. })
  199. return new Damage(...instances)
  200. }
  201. describe (user: Creature, target: Creature): LogEntry {
  202. return new LogLine(
  203. this.explain(user),
  204. `, for a total of `,
  205. this.calc(user, target).renderShort()
  206. )
  207. }
  208. explain (user: Creature): LogEntry {
  209. return new LogLine(
  210. `Deal `,
  211. ...this.factors.map(factor => new LogLine(
  212. `${factor.fraction * 100}% of your `,
  213. new PropElem(factor.stat),
  214. ` as `,
  215. new PropElem(factor.target)
  216. )).joinGeneral(new LogLine(`, `), new LogLine(` and `))
  217. )
  218. }
  219. }
  220. /**
  221. * Deals a percentage of the target's current vigors/stats
  222. */
  223. export class FractionDamageFormula implements DamageFormula {
  224. constructor (private factors: Array<{ fraction: number; target: Vigor|Stat; type: DamageType }>) {
  225. }
  226. calc (user: Creature, target: Creature): Damage {
  227. const instances: Array<DamageInstance> = this.factors.map(factor => {
  228. if (factor.target in Stat) {
  229. return {
  230. amount: Math.max(0, factor.fraction * target.stats[factor.target as Stat]),
  231. target: factor.target,
  232. type: factor.type
  233. }
  234. } else if (factor.target in Vigor) {
  235. return {
  236. amount: Math.max(factor.fraction * target.vigors[factor.target as Vigor]),
  237. target: factor.target,
  238. type: factor.type
  239. }
  240. } else {
  241. // should be impossible; .target is Stat|Vigor
  242. return {
  243. amount: 0,
  244. target: Vigor.Health,
  245. type: DamageType.Heal
  246. }
  247. }
  248. })
  249. return new Damage(...instances)
  250. }
  251. describe (user: Creature, target: Creature): LogEntry {
  252. return this.explain(user)
  253. }
  254. explain (user: Creature): LogEntry {
  255. return new LogLine(
  256. `Deal damage equal to `,
  257. ...this.factors.map(factor => new LogLine(
  258. `${factor.fraction * 100}% of your target's `,
  259. new PropElem(factor.target)
  260. )).joinGeneral(new LogLine(`, `), new LogLine(` and `))
  261. )
  262. }
  263. }
  264. export enum Side {
  265. Heroes,
  266. Monsters
  267. }
  268. /**
  269. * A Combatant has a list of possible actions to take, as well as a side.
  270. */
  271. export interface Combatant {
  272. actions: Array<Action>;
  273. groupActions: Array<GroupAction>;
  274. side: Side;
  275. }
  276. /**
  277. * An Action is anything that can be done by a [[Creature]] to a [[Creature]].
  278. */
  279. export abstract class Action {
  280. constructor (public name: TextLike, public desc: TextLike, private conditions: Array<Condition> = []) {
  281. }
  282. allowed (user: Creature, target: Creature): boolean {
  283. return this.conditions.every(cond => cond.allowed(user, target))
  284. }
  285. toString (): string {
  286. return this.name.toString()
  287. }
  288. abstract execute (user: Creature, target: Creature): LogEntry
  289. abstract describe (user: Creature, target: Creature): LogEntry
  290. }
  291. export type TestBundle = {
  292. test: CombatTest;
  293. fail: PairLine<Creature>;
  294. }
  295. export class CompositionAction extends Action {
  296. private consequences: Array<Consequence>;
  297. private tests: Array<TestBundle>;
  298. constructor (name: TextLike, desc: TextLike, properties: { conditions?: Array<Condition>; consequences?: Array<Consequence>; tests?: Array<TestBundle> }) {
  299. super(name, desc, properties.conditions ?? [])
  300. this.consequences = properties.consequences ?? []
  301. this.tests = properties.tests ?? []
  302. }
  303. execute (user: Creature, target: Creature): LogEntry {
  304. return new LogLines(
  305. ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target))
  306. )
  307. }
  308. describe (user: Creature, target: Creature): LogEntry {
  309. return new LogLine(`No descriptions yet...`)
  310. }
  311. }
  312. /**
  313. * A Condition describes whether or not something is permissible between two [[Creature]]s
  314. */
  315. export interface Condition {
  316. allowed: (user: Creature, target: Creature) => boolean;
  317. }
  318. export interface Actionable {
  319. actions: Array<Action>;
  320. }
  321. export abstract class GroupAction extends Action {
  322. constructor (name: TextLike, desc: TextLike, conditions: Array<Condition>) {
  323. super(name, desc, conditions)
  324. }
  325. allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> {
  326. return targets.filter(target => this.allowed(user, target))
  327. }
  328. executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
  329. return new LogLines(...targets.map(target => this.execute(user, target)))
  330. }
  331. abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry
  332. }
  333. /**
  334. * Individual status effects, items, etc. should override some of these hooks.
  335. * Some hooks just produce a log entry.
  336. * Some hooks return results along with a log entry.
  337. */
  338. export class Effective {
  339. /**
  340. * Executes when the effect is initially applied
  341. */
  342. onApply (creature: Creature): LogEntry { return nilLog }
  343. /**
  344. * Executes when the effect is removed
  345. */
  346. onRemove (creature: Creature): LogEntry { return nilLog }
  347. /**
  348. * Executes before the creature tries to perform an action
  349. */
  350. preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
  351. return {
  352. prevented: false,
  353. log: nilLog
  354. }
  355. }
  356. /**
  357. * Executes before another creature tries to perform an action that targets this creature
  358. */
  359. preReceiveAction (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
  360. return {
  361. prevented: false,
  362. log: nilLog
  363. }
  364. }
  365. /**
  366. * Executes before the creature receives damage (or healing)
  367. */
  368. preDamage (creature: Creature, damage: Damage): Damage {
  369. return damage
  370. }
  371. /**
  372. * Executes before the creature is attacked
  373. */
  374. preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
  375. return {
  376. prevented: false,
  377. log: nilLog
  378. }
  379. }
  380. /**
  381. * Executes when a creature's turn starts
  382. */
  383. preTurn (creature: Creature): { prevented: boolean; log: LogEntry } {
  384. return {
  385. prevented: false,
  386. log: nilLog
  387. }
  388. }
  389. /**
  390. * Modifies the effective resistance to a certain damage type
  391. */
  392. modResistance (type: DamageType, factor: number): number {
  393. return factor
  394. }
  395. /**
  396. * Called when a test is about to resolve. Decides if the creature should automatically fail.
  397. */
  398. failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } {
  399. return {
  400. failed: false,
  401. log: nilLog
  402. }
  403. }
  404. }
  405. /**
  406. * A displayable status effect
  407. */
  408. export interface VisibleStatus {
  409. name: TextLike;
  410. desc: TextLike;
  411. icon: TextLike;
  412. topLeft: string;
  413. bottomRight: string;
  414. }
  415. /**
  416. * This kind of status is never explicitly applied to an entity -- e.g., a dead entity will show
  417. * a status indicating that it is dead, but entities cannot be "given" the dead effect
  418. */
  419. export class ImplicitStatus implements VisibleStatus {
  420. topLeft = ''
  421. bottomRight = ''
  422. constructor (public name: TextLike, public desc: TextLike, public icon: string) {
  423. }
  424. }
  425. /**
  426. * This kind of status is explicitly given to a creature.
  427. */
  428. export abstract class StatusEffect extends Effective implements VisibleStatus {
  429. constructor (public name: TextLike, public desc: TextLike, public icon: string) {
  430. super()
  431. }
  432. get topLeft () { return '' }
  433. get bottomRight () { return '' }
  434. }
  435. export type EncounterDesc = {
  436. name: TextLike;
  437. }
  438. /**
  439. * An Encounter describes a fight: who is in it and whose turn it is
  440. */
  441. export class Encounter {
  442. initiatives: Map<Creature, number>
  443. currentMove: Creature
  444. turnTime = 100
  445. constructor (public desc: EncounterDesc, public combatants: Creature[]) {
  446. this.initiatives = new Map()
  447. combatants.forEach(combatant => this.initiatives.set(combatant, 0))
  448. this.currentMove = combatants[0]
  449. this.nextMove()
  450. }
  451. nextMove (): LogEntry {
  452. this.initiatives.set(this.currentMove, 0)
  453. const times = new Map<Creature, number>()
  454. this.combatants.forEach(combatant => {
  455. // this should never be undefined
  456. const currentProgress = this.initiatives.get(combatant) ?? 0
  457. const remaining = (this.turnTime - currentProgress) / Math.sqrt(Math.max(combatant.stats.Speed, 1))
  458. times.set(combatant, remaining)
  459. })
  460. this.currentMove = this.combatants.reduce((closest, next) => {
  461. const closestTime = times.get(closest) ?? 0
  462. const nextTime = times.get(next) ?? 0
  463. return closestTime <= nextTime ? closest : next
  464. }, this.combatants[0])
  465. const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.sqrt(Math.max(this.currentMove.stats.Speed, 1))
  466. this.combatants.forEach(combatant => {
  467. // still not undefined...
  468. const currentProgress = this.initiatives.get(combatant) ?? 0
  469. this.initiatives.set(combatant, currentProgress + closestRemaining * Math.sqrt(Math.max(combatant.stats.Speed, 1)))
  470. })
  471. // TODO: still let the creature use drained-vigor moves
  472. if (this.currentMove.disabled) {
  473. return this.nextMove()
  474. } else {
  475. const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented)
  476. if (effectResults.some(result => result.prevented)) {
  477. const parts = effectResults.map(result => result.log).concat([this.nextMove()])
  478. console.log(parts)
  479. return new LogLines(
  480. ...parts
  481. )
  482. }
  483. }
  484. return nilLog
  485. }
  486. get winner (): null|Side {
  487. const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.disabled).map(combatant => combatant.side))
  488. if (remaining.size === 1) {
  489. return Array.from(remaining)[0]
  490. } else {
  491. return null
  492. }
  493. }
  494. }
  495. export abstract class Consequence {
  496. constructor (public conditions: Condition[]) {
  497. }
  498. applicable (user: Creature, target: Creature): boolean {
  499. return this.conditions.every(cond => cond.allowed(user, target))
  500. }
  501. abstract apply (user: Creature, target: Creature): LogEntry
  502. }