import { Creature } from "./creature" import { TextLike, DynText, ToBe, LiveText } from './language' import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog } from './interface' export enum DamageType { Pierce = "Pierce", Slash = "Slash", Crush = "Crush", Acid = "Acid", Seduction = "Seduction", Dominance = "Dominance", Heal = "Heal", Pure = "Pure" } export interface DamageInstance { type: DamageType; amount: number; target: Vigor | Stat; } export enum Vigor { Health = "Health", Stamina = "Stamina", Resolve = "Resolve" } export const VigorIcons: {[key in Vigor]: string} = { Health: "fas fa-heart", Stamina: "fas fa-bolt", Resolve: "fas fa-brain" } export const VigorDescs: {[key in Vigor]: string} = { Health: "How much damage you can take", Stamina: "How much energy you have", Resolve: "How much dominance you can resist" } export type Vigors = {[key in Vigor]: number} export enum Stat { Toughness = "Toughness", Power = "Power", Speed = "Speed", Willpower = "Willpower", Charm = "Charm" } export type Stats = {[key in Stat]: number} export const StatIcons: {[key in Stat]: string} = { Toughness: 'fas fa-heartbeat', Power: 'fas fa-fist-raised', Speed: 'fas fa-feather', Willpower: 'fas fa-book', Charm: 'fas fa-comments' } export const StatDescs: {[key in Stat]: string} = { Toughness: 'Your physical resistance', Power: 'Your physical power', Speed: 'How quickly you can act', Willpower: 'Your mental resistance', Charm: 'Your mental power' } export enum VoreStat { Mass = "Mass", Bulk = "Bulk", PreyCount = "Prey Count" } export type VoreStats = {[key in VoreStat]: number} export const VoreStatIcons: {[key in VoreStat]: string} = { [VoreStat.Mass]: "fas fa-weight", [VoreStat.Bulk]: "fas fa-weight-hanging", [VoreStat.PreyCount]: "fas fa-utensils" } export const VoreStatDescs: {[key in VoreStat]: string} = { [VoreStat.Mass]: "How much you weigh", [VoreStat.Bulk]: "Your weight, plus the weight of your prey", [VoreStat.PreyCount]: "How many creatures you've got inside of you" } export interface CombatTest { test: (user: Creature, target: Creature) => boolean; odds: (user: Creature, target: Creature) => number; explain: (user: Creature, target: Creature) => LogEntry; } /** * An instance of damage. Contains zero or more [[DamageInstance]] objects */ export class Damage { readonly damages: DamageInstance[] constructor (...damages: DamageInstance[]) { this.damages = damages } scale (factor: number): Damage { const results: Array = [] this.damages.forEach(damage => { results.push({ type: damage.type, amount: damage.amount * factor, target: damage.target }) }) return new Damage(...results) } // TODO make this combine damage instances when appropriate combine (other: Damage): Damage { return new Damage(...this.damages.concat(other.damages)) } toString (): string { return this.damages.map(damage => damage.amount + " " + damage.type).join("/") } render (): LogEntry { return new LogLine(...this.damages.flatMap(instance => { if (instance.target in Vigor) { return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type] } else if (instance.target in Stat) { return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type] } else { // this should never happen! return [] } })) } renderShort (): LogEntry { const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {}) const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {}) this.damages.forEach(instance => { const factor = instance.type === DamageType.Heal ? -1 : 1 if (instance.target in Vigor) { vigorTotals[instance.target as Vigor] += factor * instance.amount } else if (instance.target in Stat) { statTotals[instance.target as Stat] += factor * instance.amount } }) const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' ']) const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' ']) return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst) } } /** * Computes damage given the source and target of the damage. */ export interface DamageFormula { calc (user: Creature, target: Creature): Damage; describe (user: Creature, target: Creature): LogEntry; explain (user: Creature): LogEntry; } /** * Simply returns the damage it was given. */ export class ConstantDamageFormula implements DamageFormula { constructor (private damage: Damage) { } calc (user: Creature, target: Creature): Damage { return this.damage } describe (user: Creature, target: Creature): LogEntry { return this.explain(user) } explain (user: Creature): LogEntry { return new LogLine('Deal ', this.damage.renderShort(), ' damage') } } /** * Randomly scales the damage it was given with a factor of (1-x) to (1+x) */ export class UniformRandomDamageFormula implements DamageFormula { constructor (private damage: Damage, private variance: number) { } calc (user: Creature, target: Creature): Damage { return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1) } describe (user: Creature, target: Creature): LogEntry { return this.explain(user) } explain (user: Creature): LogEntry { return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), ' damage.') } } export class StatDamageFormula implements DamageFormula { constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) { } calc (user: Creature, target: Creature): Damage { const instances: Array = this.factors.map(factor => { if (factor.stat in Stat) { return { amount: factor.fraction * user.stats[factor.stat as Stat], target: factor.target, type: factor.type } } else if (factor.stat in VoreStat) { return { amount: factor.fraction * user.voreStats[factor.stat as VoreStat], target: factor.target, type: factor.type } } else { // should be impossible; .stat is Stat|VoreStat return { amount: 0, target: Vigor.Health, type: DamageType.Heal } } }) return new Damage(...instances) } describe (user: Creature, target: Creature): LogEntry { return new LogLine( this.explain(user), `, for a total of `, this.calc(user, target).renderShort() ) } explain (user: Creature): LogEntry { return new LogLine( `Deal `, ...this.factors.map(factor => new LogLine( `${factor.fraction * 100}% of your `, new PropElem(factor.stat), ` as `, new PropElem(factor.target) )).joinGeneral(new LogLine(`, `), new LogLine(` and `)) ) } } /** * Deals a percentage of the target's current vigors/stats */ export class FractionDamageFormula implements DamageFormula { constructor (private factors: Array<{ fraction: number; target: Vigor|Stat; type: DamageType }>) { } calc (user: Creature, target: Creature): Damage { const instances: Array = this.factors.map(factor => { if (factor.target in Stat) { return { amount: Math.max(0, factor.fraction * target.stats[factor.target as Stat]), target: factor.target, type: factor.type } } else if (factor.target in Vigor) { return { amount: Math.max(factor.fraction * user.vigors[factor.target as Vigor]), target: factor.target, type: factor.type } } else { // should be impossible; .target is Stat|Vigor return { amount: 0, target: Vigor.Health, type: DamageType.Heal } } }) return new Damage(...instances) } describe (user: Creature, target: Creature): LogEntry { return this.explain(user) } explain (user: Creature): LogEntry { return new LogLine( `Deal damage equal to `, ...this.factors.map(factor => new LogLine( `${factor.fraction * 100}% of your target's `, new PropElem(factor.target) )).joinGeneral(new LogLine(`, `), new LogLine(` and `)) ) } } export enum Side { Heroes, Monsters } /** * A Combatant has a list of possible actions to take, as well as a side. */ export interface Combatant { actions: Array; groupActions: Array; side: Side; } /** * An Action is anything that can be done by a [[Creature]] to a [[Creature]]. */ export abstract class Action { constructor (public name: TextLike, public desc: TextLike, private conditions: Array = []) { } allowed (user: Creature, target: Creature): boolean { return this.conditions.every(cond => cond.allowed(user, target)) } toString (): string { return this.name.toString() } abstract execute (user: Creature, target: Creature): LogEntry abstract describe (user: Creature, target: Creature): LogEntry } /** * A Condition describes whether or not something is permissible between two [[Creature]]s */ export interface Condition { allowed: (user: Creature, target: Creature) => boolean; } export interface Actionable { actions: Array; } export abstract class GroupAction extends Action { constructor (name: TextLike, desc: TextLike, conditions: Array) { super(name, desc, conditions) } allowedGroup (user: Creature, targets: Array): Array { return targets.filter(target => this.allowed(user, target)) } executeGroup (user: Creature, targets: Array): LogEntry { return new LogLines(...targets.map(target => this.execute(user, target))) } abstract describeGroup (user: Creature, targets: Array): LogEntry } /** * Individual status effects, items, etc. should override some of these hooks. * Some hooks just produce a log entry. * Some hooks return results along with a log entry. */ export class Effective { onApply (creature: Creature): LogEntry { return nilLog } onRemove (creature: Creature): LogEntry { return nilLog } preAction (creature: Creature): { prevented: boolean; log: LogEntry } { return { prevented: false, log: nilLog } } preDamage (creature: Creature, damage: Damage): Damage { return damage } preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { return { prevented: false, log: nilLog } } } /** * A displayable status effect */ export interface VisibleStatus { name: TextLike; desc: TextLike; icon: TextLike; topLeft: string; bottomRight: string; } /** * This kind of status is never explicitly applied to an entity -- e.g., a dead entity will show * a status indicating that it is dead, but entities cannot be "given" the dead effect */ export class ImplicitStatus implements VisibleStatus { topLeft = '' bottomRight = '' constructor (public name: TextLike, public desc: TextLike, public icon: string) { } } /** * This kind of status is explicitly given to a creature. */ export abstract class StatusEffect extends Effective implements VisibleStatus { constructor (public name: TextLike, public desc: TextLike, public icon: string) { super() } get topLeft () { return '' } get bottomRight () { return '' } } export type EncounterDesc = { name: TextLike; } /** * An Encounter describes a fight: who is in it and whose turn it is */ export class Encounter { initiatives: Map currentMove: Creature turnTime = 100 constructor (public desc: EncounterDesc, public combatants: Creature[]) { this.initiatives = new Map() combatants.forEach(combatant => this.initiatives.set(combatant, 0)) this.currentMove = combatants[0] this.nextMove() } nextMove (): void { this.initiatives.set(this.currentMove, 0) const times = new Map() this.combatants.forEach(combatant => { // this should never be undefined const currentProgress = this.initiatives.get(combatant) ?? 0 const remaining = (this.turnTime - currentProgress) / Math.max(combatant.stats.Speed, 1) times.set(combatant, remaining) }) this.currentMove = this.combatants.reduce((closest, next) => { const closestTime = times.get(closest) ?? 0 const nextTime = times.get(next) ?? 0 return closestTime <= nextTime ? closest : next }, this.combatants[0]) const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.max(this.currentMove.stats.Speed, 1) this.combatants.forEach(combatant => { // still not undefined... const currentProgress = this.initiatives.get(combatant) ?? 0 this.initiatives.set(combatant, currentProgress + closestRemaining * Math.max(combatant.stats.Speed, 1)) }) // TODO: still let the creature use drained-vigor moves if (this.currentMove.disabled) { this.nextMove() } } }