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.
 
 
 
 
 

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