Feast 2.0!
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

806 linhas
22 KiB

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