Feast 2.0!
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

997 рядки
25 KiB

  1. import { Creature } from "./creature"
  2. import { TextLike } from "@/game/language"
  3. import {
  4. LogEntry,
  5. LogLines,
  6. FAElem,
  7. LogLine,
  8. FormatEntry,
  9. FormatOpt,
  10. PropElem,
  11. nilLog,
  12. Newline
  13. } from "@/game/interface"
  14. import { World } from "@/game/world"
  15. import { TestCategory } from "@/game/combat/tests"
  16. import { Container } from "@/game/vore"
  17. import { SoloTargeter } from "@/game/combat/targeters"
  18. export enum DamageType {
  19. Pierce = "Pierce",
  20. Slash = "Slash",
  21. Crush = "Crush",
  22. Acid = "Acid",
  23. Seduction = "Seduction",
  24. Dominance = "Dominance",
  25. Heal = "Heal",
  26. Pure = "Pure"
  27. }
  28. export interface DamageInstance {
  29. type: DamageType;
  30. amount: number;
  31. target: Vigor | Stat;
  32. }
  33. export enum Vigor {
  34. Health = "Health",
  35. Stamina = "Stamina",
  36. Resolve = "Resolve"
  37. }
  38. export const VigorIcons: { [key in Vigor]: string } = {
  39. Health: "fas fa-heart",
  40. Stamina: "fas fa-bolt",
  41. Resolve: "fas fa-brain"
  42. }
  43. export const VigorDescs: { [key in Vigor]: string } = {
  44. Health: "How much damage you can take",
  45. Stamina: "How much energy you have",
  46. Resolve: "How much dominance you can resist"
  47. }
  48. export type Vigors = { [key in Vigor]: number }
  49. export enum Stat {
  50. Toughness = "Toughness",
  51. Power = "Power",
  52. Reflexes = "Reflexes",
  53. Agility = "Agility",
  54. Willpower = "Willpower",
  55. Charm = "Charm"
  56. }
  57. export type Stats = { [key in Stat]: number }
  58. export const StatToVigor: { [key in Stat]: Vigor } = {
  59. Toughness: Vigor.Health,
  60. Power: Vigor.Health,
  61. Reflexes: Vigor.Stamina,
  62. Agility: Vigor.Stamina,
  63. Willpower: Vigor.Resolve,
  64. Charm: Vigor.Resolve
  65. }
  66. export const StatIcons: { [key in Stat]: string } = {
  67. Toughness: "fas fa-heartbeat",
  68. Power: "fas fa-fist-raised",
  69. Reflexes: "fas fa-stopwatch",
  70. Agility: "fas fa-feather",
  71. Willpower: "fas fa-book",
  72. Charm: "fas fa-comments"
  73. }
  74. export const StatDescs: { [key in Stat]: string } = {
  75. Toughness: "Your brute resistance",
  76. Power: "Your brute power",
  77. Reflexes: "Your ability to dodge",
  78. Agility: "Your ability to move quickly",
  79. Willpower: "Your mental resistance",
  80. Charm: "Your mental power"
  81. }
  82. export enum VoreStat {
  83. Mass = "Mass",
  84. Bulk = "Bulk",
  85. Prey = "Prey"
  86. }
  87. export type VoreStats = { [key in VoreStat]: number }
  88. export const VoreStatIcons: { [key in VoreStat]: string } = {
  89. [VoreStat.Mass]: "fas fa-weight",
  90. [VoreStat.Bulk]: "fas fa-weight-hanging",
  91. [VoreStat.Prey]: "fas fa-utensils"
  92. }
  93. export const VoreStatDescs: { [key in VoreStat]: string } = {
  94. [VoreStat.Mass]: "How much you weigh",
  95. [VoreStat.Bulk]: "Your weight, plus the weight of your prey",
  96. [VoreStat.Prey]: "How many creatures you've got inside of you"
  97. }
  98. export interface CombatTest {
  99. test: (user: Creature, target: Creature) => boolean;
  100. odds: (user: Creature, target: Creature) => number;
  101. explain: (user: Creature, target: Creature) => LogEntry;
  102. fail: (user: Creature, target: Creature) => LogEntry;
  103. }
  104. export interface Targeter {
  105. targets(primary: Creature, encounter: Encounter): Array<Creature>;
  106. }
  107. /**
  108. * An instance of damage. Contains zero or more [[DamageInstance]] objects
  109. */
  110. export class Damage {
  111. readonly damages: DamageInstance[]
  112. constructor (...damages: DamageInstance[]) {
  113. this.damages = damages
  114. }
  115. scale (factor: number): Damage {
  116. const results: Array<DamageInstance> = []
  117. this.damages.forEach(damage => {
  118. results.push({
  119. type: damage.type,
  120. amount: damage.amount * factor,
  121. target: damage.target
  122. })
  123. })
  124. return new Damage(...results)
  125. }
  126. // TODO make this combine damage instances when appropriate
  127. combine (other: Damage): Damage {
  128. return new Damage(...this.damages.concat(other.damages))
  129. }
  130. toString (): string {
  131. return this.damages
  132. .map(damage => damage.amount + " " + damage.type)
  133. .join("/")
  134. }
  135. render (): LogEntry {
  136. return new LogLine(
  137. ...this.damages.flatMap(instance => {
  138. if (instance.target in Vigor) {
  139. return [
  140. instance.amount.toString(),
  141. new FAElem(VigorIcons[instance.target as Vigor]),
  142. " " + instance.type
  143. ]
  144. } else if (instance.target in Stat) {
  145. return [
  146. instance.amount.toString(),
  147. new FAElem(StatIcons[instance.target as Stat]),
  148. " " + instance.type
  149. ]
  150. } else {
  151. // this should never happen!
  152. return []
  153. }
  154. })
  155. )
  156. }
  157. // TODO is there a way to do this that will satisfy the typechecker?
  158. renderShort (): LogEntry {
  159. /* eslint-disable-next-line */
  160. const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => {
  161. total[key] = 0
  162. return total
  163. }, {})
  164. /* eslint-disable-next-line */
  165. const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => {
  166. total[key] = 0
  167. return total
  168. }, {})
  169. this.damages.forEach(instance => {
  170. const factor = instance.type === DamageType.Heal ? -1 : 1
  171. if (instance.target in Vigor) {
  172. vigorTotals[instance.target as Vigor] += factor * instance.amount
  173. } else if (instance.target in Stat) {
  174. statTotals[instance.target as Stat] += factor * instance.amount
  175. }
  176. })
  177. const vigorEntries = Object.keys(Vigor).flatMap(key =>
  178. vigorTotals[key as Vigor] === 0
  179. ? []
  180. : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), " "])
  181. const statEntries = Object.keys(Stat).flatMap(key =>
  182. statTotals[key as Stat] === 0
  183. ? []
  184. : [new PropElem(key as Stat, statTotals[key as Stat]), " "])
  185. return new FormatEntry(
  186. new LogLine(...vigorEntries.concat(statEntries)),
  187. FormatOpt.DamageInst
  188. )
  189. }
  190. nonzero (): boolean {
  191. /* eslint-disable-next-line */
  192. const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => {
  193. total[key] = 0
  194. return total
  195. }, {})
  196. /* eslint-disable-next-line */
  197. const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => {
  198. total[key] = 0
  199. return total
  200. }, {})
  201. this.damages.forEach(instance => {
  202. const factor = instance.type === DamageType.Heal ? -1 : 1
  203. if (instance.target in Vigor) {
  204. vigorTotals[instance.target as Vigor] += factor * instance.amount
  205. } else if (instance.target in Stat) {
  206. statTotals[instance.target as Stat] += factor * instance.amount
  207. }
  208. })
  209. return (
  210. Object.values(vigorTotals).some(v => v !== 0) ||
  211. Object.values(statTotals).some(v => v !== 0)
  212. )
  213. }
  214. }
  215. /**
  216. * Computes damage given the source and target of the damage.
  217. */
  218. export interface DamageFormula {
  219. calc(user: Creature, target: Creature): Damage;
  220. describe(user: Creature, target: Creature): LogEntry;
  221. explain(user: Creature): LogEntry;
  222. }
  223. export class CompositeDamageFormula implements DamageFormula {
  224. constructor (private formulas: DamageFormula[]) {}
  225. calc (user: Creature, target: Creature): Damage {
  226. return this.formulas.reduce(
  227. (total: Damage, next: DamageFormula) =>
  228. total.combine(next.calc(user, target)),
  229. new Damage()
  230. )
  231. }
  232. describe (user: Creature, target: Creature): LogEntry {
  233. return new LogLines(
  234. ...this.formulas.map(formula => formula.describe(user, target))
  235. )
  236. }
  237. explain (user: Creature): LogEntry {
  238. return new LogLines(...this.formulas.map(formula => formula.explain(user)))
  239. }
  240. }
  241. /**
  242. * Simply returns the damage it was given.
  243. */
  244. export class ConstantDamageFormula implements DamageFormula {
  245. constructor (private damage: Damage) {}
  246. calc (user: Creature, target: Creature): Damage {
  247. return this.damage
  248. }
  249. describe (user: Creature, target: Creature): LogEntry {
  250. return this.explain(user)
  251. }
  252. explain (user: Creature): LogEntry {
  253. return new LogLine("Deal ", this.damage.renderShort())
  254. }
  255. }
  256. /**
  257. * Randomly scales the damage it was given with a factor of (1-x) to (1+x)
  258. */
  259. export class UniformRandomDamageFormula implements DamageFormula {
  260. constructor (private damage: Damage, private variance: number) {}
  261. calc (user: Creature, target: Creature): Damage {
  262. return this.damage.scale(
  263. Math.random() * this.variance * 2 - this.variance + 1
  264. )
  265. }
  266. describe (user: Creature, target: Creature): LogEntry {
  267. return this.explain(user)
  268. }
  269. explain (user: Creature): LogEntry {
  270. return new LogLine(
  271. "Deal between ",
  272. this.damage.scale(1 - this.variance).renderShort(),
  273. " and ",
  274. this.damage.scale(1 + this.variance).renderShort(),
  275. "."
  276. )
  277. }
  278. }
  279. /**
  280. * A [[DamageFormula]] that uses the attacker's stats
  281. */
  282. export class StatDamageFormula implements DamageFormula {
  283. constructor (
  284. private factors: Array<{
  285. stat: Stat | VoreStat;
  286. fraction: number;
  287. type: DamageType;
  288. target: Vigor | Stat;
  289. }>
  290. ) {}
  291. calc (user: Creature, target: Creature): Damage {
  292. const instances: Array<DamageInstance> = this.factors.map(factor => {
  293. if (factor.stat in Stat) {
  294. return {
  295. amount: factor.fraction * user.stats[factor.stat as Stat],
  296. target: factor.target,
  297. type: factor.type
  298. }
  299. } else if (factor.stat in VoreStat) {
  300. return {
  301. amount: factor.fraction * user.voreStats[factor.stat as VoreStat],
  302. target: factor.target,
  303. type: factor.type
  304. }
  305. } else {
  306. // should be impossible; .stat is Stat|VoreStat
  307. return {
  308. amount: 0,
  309. target: Vigor.Health,
  310. type: DamageType.Heal
  311. }
  312. }
  313. })
  314. return new Damage(...instances)
  315. }
  316. describe (user: Creature, target: Creature): LogEntry {
  317. return new LogLine(
  318. this.explain(user),
  319. `, for a total of `,
  320. this.calc(user, target).renderShort()
  321. )
  322. }
  323. explain (user: Creature): LogEntry {
  324. return new LogLine(
  325. `Deal `,
  326. ...this.factors
  327. .map(
  328. factor =>
  329. new LogLine(
  330. `${factor.fraction * 100}% of your `,
  331. new PropElem(factor.stat),
  332. ` as `,
  333. new PropElem(factor.target)
  334. )
  335. )
  336. .joinGeneral(new LogLine(`, `), new LogLine(` and `))
  337. )
  338. }
  339. }
  340. /**
  341. * Deals a percentage of the target's current vigors/stats
  342. */
  343. export class FractionDamageFormula implements DamageFormula {
  344. constructor (
  345. private factors: Array<{
  346. fraction: number;
  347. target: Vigor | Stat;
  348. type: DamageType;
  349. }>
  350. ) {}
  351. calc (user: Creature, target: Creature): Damage {
  352. const instances: Array<DamageInstance> = this.factors.map(factor => {
  353. if (factor.target in Stat) {
  354. return {
  355. amount: Math.max(
  356. 0,
  357. factor.fraction * target.stats[factor.target as Stat]
  358. ),
  359. target: factor.target,
  360. type: factor.type
  361. }
  362. } else if (factor.target in Vigor) {
  363. return {
  364. amount: Math.max(
  365. factor.fraction * target.vigors[factor.target as Vigor]
  366. ),
  367. target: factor.target,
  368. type: factor.type
  369. }
  370. } else {
  371. // should be impossible; .target is Stat|Vigor
  372. return {
  373. amount: 0,
  374. target: Vigor.Health,
  375. type: DamageType.Heal
  376. }
  377. }
  378. })
  379. return new Damage(...instances)
  380. }
  381. describe (user: Creature, target: Creature): LogEntry {
  382. return this.explain(user)
  383. }
  384. explain (user: Creature): LogEntry {
  385. return new LogLine(
  386. `Deal damage equal to `,
  387. ...this.factors
  388. .map(
  389. factor =>
  390. new LogLine(
  391. `${factor.fraction * 100}% of your target's `,
  392. new PropElem(factor.target)
  393. )
  394. )
  395. .joinGeneral(new LogLine(`, `), new LogLine(` and `))
  396. )
  397. }
  398. }
  399. export enum Side {
  400. Heroes,
  401. Monsters
  402. }
  403. /**
  404. * A Combatant has a list of possible actions to take, as well as a side.
  405. */
  406. export interface Combatant {
  407. actions: Array<Action>;
  408. side: Side;
  409. }
  410. /**
  411. * An Action is anything that can be done by a [[Creature]] to a [[Creature]].
  412. */
  413. export abstract class Action {
  414. constructor (
  415. public name: TextLike,
  416. public desc: TextLike,
  417. public conditions: Array<Condition> = [],
  418. public tests: Array<CombatTest> = []
  419. ) {}
  420. allowed (user: Creature, target: Creature): boolean {
  421. return this.conditions.every(cond => cond.allowed(user, target))
  422. }
  423. toString (): string {
  424. return this.name.toString()
  425. }
  426. try (user: Creature, targets: Array<Creature>): LogEntry {
  427. // Check if any pre-action effect will cancel this action.
  428. const preActionResults = user.effects.mapUntil(effect => effect.preAction(user), result => result.prevented)
  429. const preActionLogs = new LogLines(...preActionResults.map(result => result.log))
  430. if (preActionResults.some(result => result.prevented)) {
  431. return preActionLogs
  432. }
  433. // Check if any pre-receive-action effect will cancel this action.
  434. const preReceiveActionResults = targets.mapUntil(target => {
  435. const outcome = target.effects.mapUntil(effect => effect.preReceiveAction(user, target), result => result.prevented)
  436. return outcome
  437. }, results => results.some(result => result.prevented))
  438. console.log(preReceiveActionResults)
  439. const preReceiveActionLogs = new LogLines(...preReceiveActionResults.flatMap(
  440. target => target.map(result => result.log)
  441. ))
  442. if (preReceiveActionResults.some(results => results.some(result => result.prevented))) {
  443. return new LogLines(
  444. preActionLogs,
  445. preReceiveActionLogs
  446. )
  447. }
  448. const results = targets.map(target => {
  449. const failReason = this.tests.find(test => !test.test(user, target))
  450. if (failReason !== undefined) {
  451. return {
  452. failed: true,
  453. target: target,
  454. log: failReason.fail(user, target)
  455. }
  456. } else {
  457. return {
  458. failed: false,
  459. target: target,
  460. log: this.execute(user, target)
  461. }
  462. }
  463. })
  464. return new LogLines(
  465. preActionLogs,
  466. preReceiveActionLogs,
  467. ...results.map(result => result.log),
  468. this.executeAll(
  469. user,
  470. results.filter(result => !result.failed).map(result => result.target)
  471. )
  472. )
  473. }
  474. describe (user: Creature, target: Creature, verbose = true): LogEntry {
  475. return new LogLines(
  476. ...(verbose
  477. ? this.conditions
  478. .map(condition => condition.explain(user, target))
  479. .concat([new Newline()])
  480. : []),
  481. new LogLine(
  482. `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%`
  483. ),
  484. new Newline(),
  485. ...this.tests.map(test => test.explain(user, target))
  486. )
  487. }
  488. odds (user: Creature, target: Creature): number {
  489. return this.tests.reduce(
  490. (total, test) => total * test.odds(user, target),
  491. 1
  492. )
  493. }
  494. targets (primary: Creature, encounter: Encounter): Array<Creature> {
  495. return [primary]
  496. }
  497. executeAll (user: Creature, targets: Array<Creature>): LogEntry {
  498. return nilLog
  499. }
  500. abstract execute(user: Creature, target: Creature): LogEntry
  501. }
  502. export class CompositionAction extends Action {
  503. public consequences: Array<Consequence>
  504. public groupConsequences: Array<GroupConsequence>
  505. public targeters: Array<Targeter>
  506. constructor (
  507. name: TextLike,
  508. desc: TextLike,
  509. properties: {
  510. conditions?: Array<Condition>;
  511. consequences?: Array<Consequence>;
  512. groupConsequences?: Array<GroupConsequence>;
  513. tests?: Array<CombatTest>;
  514. targeters?: Array<Targeter>;
  515. }
  516. ) {
  517. super(name, desc, properties.conditions ?? [], properties.tests ?? [])
  518. this.consequences = properties.consequences ?? []
  519. this.groupConsequences = properties.groupConsequences ?? []
  520. this.targeters = properties.targeters ?? [new SoloTargeter()]
  521. }
  522. execute (user: Creature, target: Creature): LogEntry {
  523. return new LogLines(
  524. ...this.consequences
  525. .filter(consequence => consequence.applicable(user, target))
  526. .map(consequence => consequence.apply(user, target))
  527. )
  528. }
  529. executeAll (user: Creature, targets: Array<Creature>): LogEntry {
  530. return new LogLines(
  531. ...this.groupConsequences.map(consequence =>
  532. consequence.apply(
  533. user,
  534. targets.filter(target => consequence.applicable(user, target))
  535. ))
  536. )
  537. }
  538. describe (user: Creature, target: Creature): LogEntry {
  539. return new LogLines(
  540. ...this.consequences
  541. .map(consequence => consequence.describe(user, target))
  542. .concat(new Newline(), super.describe(user, target))
  543. )
  544. }
  545. targets (primary: Creature, encounter: Encounter) {
  546. return this.targeters
  547. .flatMap(targeter => targeter.targets(primary, encounter))
  548. .unique()
  549. }
  550. }
  551. /**
  552. * A Condition describes whether or not something is permissible between two [[Creature]]s
  553. */
  554. export interface Condition {
  555. allowed: (user: Creature, target: Creature) => boolean;
  556. explain: (user: Creature, target: Creature) => LogEntry;
  557. }
  558. export interface Actionable {
  559. actions: Array<Action>;
  560. }
  561. /**
  562. * Individual status effects, items, etc. should override some of these hooks.
  563. * Some hooks just produce a log entry.
  564. * Some hooks return results along with a log entry.
  565. */
  566. export class Effective {
  567. /**
  568. * Executes when the effect is initially applied
  569. */
  570. onApply (creature: Creature): LogEntry {
  571. return nilLog
  572. }
  573. /**
  574. * Executes when the effect is removed
  575. */
  576. onRemove (creature: Creature): LogEntry {
  577. return nilLog
  578. }
  579. /**
  580. * Executes before the creature tries to perform an action
  581. */
  582. preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
  583. return {
  584. prevented: false,
  585. log: nilLog
  586. }
  587. }
  588. /**
  589. * Executes before another creature tries to perform an action that targets this creature
  590. */
  591. preReceiveAction (
  592. creature: Creature,
  593. attacker: Creature
  594. ): { prevented: boolean; log: LogEntry } {
  595. return {
  596. prevented: false,
  597. log: nilLog
  598. }
  599. }
  600. /**
  601. * Executes before the creature receives damage (or healing)
  602. */
  603. preDamage (creature: Creature, damage: Damage): Damage {
  604. return damage
  605. }
  606. /**
  607. * Executes before the creature is attacked
  608. */
  609. preAttack (
  610. creature: Creature,
  611. attacker: Creature
  612. ): { prevented: boolean; log: LogEntry } {
  613. return {
  614. prevented: false,
  615. log: nilLog
  616. }
  617. }
  618. /**
  619. * Executes when a creature's turn starts
  620. */
  621. preTurn (creature: Creature): { prevented: boolean; log: LogEntry } {
  622. return {
  623. prevented: false,
  624. log: nilLog
  625. }
  626. }
  627. /**
  628. * Modifies the effective resistance to a certain damage type
  629. */
  630. modResistance (type: DamageType, factor: number): number {
  631. return factor
  632. }
  633. /**
  634. * Called when a test is about to resolve. Decides if the creature should automatically fail.
  635. */
  636. failTest (
  637. creature: Creature,
  638. opponent: Creature
  639. ): { failed: boolean; log: LogEntry } {
  640. return {
  641. failed: false,
  642. log: nilLog
  643. }
  644. }
  645. /**
  646. * Changes a creature's size. This represents the change in *mass*
  647. */
  648. scale (scale: number): number {
  649. return scale
  650. }
  651. /**
  652. * Additively modifies a creature's score for an offensive test
  653. */
  654. modTestOffense (
  655. attacker: Creature,
  656. defender: Creature,
  657. kind: TestCategory
  658. ): number {
  659. return 0
  660. }
  661. /**
  662. * Additively modifies a creature's score for a defensive test
  663. */
  664. modTestDefense (
  665. defender: Creature,
  666. attacker: Creature,
  667. kind: TestCategory
  668. ): number {
  669. return 0
  670. }
  671. /**
  672. * Affects digestion damage
  673. */
  674. modDigestionDamage (
  675. predator: Creature,
  676. prey: Creature,
  677. container: Container,
  678. damage: Damage
  679. ): Damage {
  680. return damage
  681. }
  682. /**
  683. * Triggers after consumption
  684. */
  685. postConsume (
  686. predator: Creature,
  687. prey: Creature,
  688. container: Container
  689. ): LogEntry {
  690. return nilLog
  691. }
  692. /**
  693. * Triggers after prey enters a container
  694. */
  695. postEnter (
  696. predator: Creature,
  697. prey: Creature,
  698. container: Container
  699. ): LogEntry {
  700. return nilLog
  701. }
  702. /**
  703. * Affects a stat
  704. */
  705. modStat (creature: Creature, stat: Stat, current: number): number {
  706. return current
  707. }
  708. /**
  709. * Provides actions
  710. */
  711. actions (user: Creature): Array<Action> {
  712. return []
  713. }
  714. }
  715. /**
  716. * A displayable status effect
  717. */
  718. export interface VisibleStatus {
  719. name: TextLike;
  720. desc: TextLike;
  721. icon: TextLike;
  722. topLeft: string;
  723. bottomRight: string;
  724. }
  725. /**
  726. * This kind of status is never explicitly applied to an entity -- e.g., a dead entity will show
  727. * a status indicating that it is dead, but entities cannot be "given" the dead effect
  728. */
  729. export class ImplicitStatus implements VisibleStatus {
  730. topLeft = ""
  731. bottomRight = ""
  732. constructor (
  733. public name: TextLike,
  734. public desc: TextLike,
  735. public icon: string
  736. ) {}
  737. }
  738. /**
  739. * This kind of status is explicitly given to a creature.
  740. */
  741. export abstract class StatusEffect extends Effective implements VisibleStatus {
  742. constructor (
  743. public name: TextLike,
  744. public desc: TextLike,
  745. public icon: string
  746. ) {
  747. super()
  748. }
  749. get topLeft () {
  750. return ""
  751. }
  752. get bottomRight () {
  753. return ""
  754. }
  755. }
  756. export type EncounterDesc = {
  757. name: TextLike;
  758. intro: (world: World) => LogEntry;
  759. }
  760. /**
  761. * An Encounter describes a fight: who is in it and whose turn it is
  762. */
  763. export class Encounter {
  764. initiatives: Map<Creature, number>
  765. currentMove: Creature
  766. turnTime = 100
  767. constructor (public desc: EncounterDesc, public combatants: Creature[]) {
  768. this.initiatives = new Map()
  769. combatants.forEach(combatant => this.initiatives.set(combatant, 0))
  770. this.currentMove = combatants[0]
  771. this.nextMove()
  772. }
  773. nextMove (totalTime = 0): LogEntry {
  774. this.initiatives.set(this.currentMove, 0)
  775. const times = new Map<Creature, number>()
  776. this.combatants.forEach(combatant => {
  777. // this should never be undefined
  778. const currentProgress = this.initiatives.get(combatant) ?? 0
  779. const remaining =
  780. (this.turnTime - currentProgress) /
  781. Math.sqrt(Math.max(combatant.stats.Agility, 1))
  782. times.set(combatant, remaining)
  783. })
  784. this.currentMove = this.combatants.reduce((closest, next) => {
  785. const closestTime = times.get(closest) ?? 0
  786. const nextTime = times.get(next) ?? 0
  787. return closestTime <= nextTime ? closest : next
  788. }, this.combatants[0])
  789. const closestRemaining =
  790. (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) /
  791. Math.sqrt(Math.max(this.currentMove.stats.Agility, 1))
  792. this.combatants.forEach(combatant => {
  793. // still not undefined...
  794. const currentProgress = this.initiatives.get(combatant) ?? 0
  795. this.initiatives.set(
  796. combatant,
  797. currentProgress +
  798. closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1))
  799. )
  800. })
  801. // TODO: still let the creature use drained-vigor moves
  802. if (this.currentMove.disabled) {
  803. return this.nextMove(closestRemaining + totalTime)
  804. } else {
  805. // applies digestion every time combat advances
  806. const tickResults = this.combatants.flatMap(combatant =>
  807. combatant.containers.map(container =>
  808. container.tick(5 * (closestRemaining + totalTime))))
  809. const effectResults = this.currentMove.effects
  810. .map(effect => effect.preTurn(this.currentMove))
  811. .filter(effect => effect.prevented)
  812. if (effectResults.some(result => result.prevented)) {
  813. const parts = effectResults
  814. .map(result => result.log)
  815. .concat([this.nextMove()])
  816. return new LogLines(...parts, ...tickResults)
  817. } else {
  818. return new LogLines(...tickResults)
  819. }
  820. }
  821. return nilLog
  822. }
  823. /**
  824. * Combat is won once one side is completely disabled
  825. */
  826. get winner (): null | Side {
  827. const remaining: Set<Side> = new Set(
  828. this.combatants
  829. .filter(combatant => !combatant.disabled)
  830. .map(combatant => combatant.side)
  831. )
  832. if (remaining.size === 1) {
  833. return Array.from(remaining)[0]
  834. } else {
  835. return null
  836. }
  837. }
  838. /**
  839. * Combat is completely won once one side is completely destroyed
  840. */
  841. get totalWinner (): null | Side {
  842. const remaining: Set<Side> = new Set(
  843. this.combatants
  844. .filter(combatant => !combatant.destroyed)
  845. .map(combatant => combatant.side)
  846. )
  847. if (remaining.size === 1) {
  848. return Array.from(remaining)[0]
  849. } else {
  850. return null
  851. }
  852. }
  853. }
  854. export abstract class Consequence {
  855. constructor (public conditions: Condition[]) {}
  856. applicable (user: Creature, target: Creature): boolean {
  857. return this.conditions.every(cond => cond.allowed(user, target))
  858. }
  859. abstract describe(user: Creature, target: Creature): LogEntry
  860. abstract apply(user: Creature, target: Creature): LogEntry
  861. }
  862. export abstract class GroupConsequence {
  863. constructor (public conditions: Condition[]) {}
  864. applicable (user: Creature, target: Creature): boolean {
  865. return this.conditions.every(cond => cond.allowed(user, target))
  866. }
  867. abstract describe(user: Creature, targets: Array<Creature>): LogEntry
  868. abstract apply(user: Creature, targets: Array<Creature>): LogEntry
  869. }