|  | import { Creature } from "./creature"
import { TextLike } from '@/game/language'
import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog, Newline } from '@/game/interface'
import { World } from '@/game/world'
import { TestCategory } from '@/game/combat/tests'
import { VoreContainer } from '@/game/vore'
import { SoloTargeter } from '@/game/combat/targeters'
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",
  Reflexes = "Reflexes",
  Agility = "Agility",
  Willpower = "Willpower",
  Charm = "Charm"
}
export type Stats = {[key in Stat]: number}
export const StatToVigor: {[key in Stat]: Vigor} = {
  Toughness: Vigor.Health,
  Power: Vigor.Health,
  Reflexes: Vigor.Stamina,
  Agility: Vigor.Stamina,
  Willpower: Vigor.Resolve,
  Charm: Vigor.Resolve
}
export const StatIcons: {[key in Stat]: string} = {
  Toughness: 'fas fa-heartbeat',
  Power: 'fas fa-fist-raised',
  Reflexes: 'fas fa-stopwatch',
  Agility: 'fas fa-feather',
  Willpower: 'fas fa-book',
  Charm: 'fas fa-comments'
}
export const StatDescs: {[key in Stat]: string} = {
  Toughness: 'Your brute resistance',
  Power: 'Your brute power',
  Reflexes: 'Your ability to dodge',
  Agility: 'Your ability to move quickly',
  Willpower: 'Your mental resistance',
  Charm: 'Your mental power'
}
export enum VoreStat {
  Mass = "Mass",
  Bulk = "Bulk",
  Prey = "Prey"
}
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.Prey]: "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.Prey]: "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;
  fail: (user: Creature, target: Creature) => LogEntry;
}
export interface Targeter {
  targets (primary: Creature, encounter: Encounter): Array<Creature>;
}
/**
 * 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<DamageInstance> = []
    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 []
      }
    }))
  }
  // TODO is there a way to do this that will satisfy the typechecker?
  renderShort (): LogEntry {
    /* eslint-disable-next-line */
    const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
    /* eslint-disable-next-line */
    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)
  }
  nonzero (): boolean {
    /* eslint-disable-next-line */
    const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
    /* eslint-disable-next-line */
    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
      }
    })
    return Object.values(vigorTotals).some(v => v !== 0) || Object.values(statTotals).some(v => v !== 0)
  }
}
/**
 * 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;
}
export class CompositeDamageFormula implements DamageFormula {
  constructor (private formulas: DamageFormula[]) {
  }
  calc (user: Creature, target: Creature): Damage {
    return this.formulas.reduce((total: Damage, next: DamageFormula) => total.combine(next.calc(user, target)), new Damage())
  }
  describe (user: Creature, target: Creature): LogEntry {
    return new LogLines(...this.formulas.map(formula => formula.describe(user, target)))
  }
  explain (user: Creature): LogEntry {
    return new LogLines(...this.formulas.map(formula => formula.explain(user)))
  }
}
/**
 * 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())
  }
}
/**
 * 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(), '.')
  }
}
/**
 * A [[DamageFormula]] that uses the attacker's stats
 */
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<DamageInstance> = 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<DamageInstance> = 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 * target.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<Action>;
    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,
    public conditions: Array<Condition> = [],
    public tests: Array<CombatTest> = []
  ) {
  }
  allowed (user: Creature, target: Creature): boolean {
    return this.conditions.every(cond => cond.allowed(user, target))
  }
  toString (): string {
    return this.name.toString()
  }
  try (user: Creature, targets: Array<Creature>): LogEntry {
    const results = targets.map(target => {
      const failReason = this.tests.find(test => !test.test(user, target))
      if (failReason !== undefined) {
        return { failed: true, target: target, log: failReason.fail(user, target) }
      } else {
        return { failed: false, target: target, log: this.execute(user, target) }
      }
    })
    return new LogLines(
      ...results.map(result => result.log),
      this.executeAll(user, results.filter(result => !result.failed).map(result => result.target))
    )
  }
  describe (user: Creature, target: Creature, verbose = true): LogEntry {
    return new LogLines(
      ...(verbose ? this.conditions.map(condition => condition.explain(user, target)).concat([new Newline()]) : []),
      new LogLine(
        `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%`
      ),
      new Newline(),
      ...this.tests.map(test => test.explain(user, target))
    )
  }
  odds (user: Creature, target: Creature): number {
    return this.tests.reduce((total, test) => total * test.odds(user, target), 1)
  }
  targets (primary: Creature, encounter: Encounter): Array<Creature> {
    return [primary]
  }
  executeAll (user: Creature, targets: Array<Creature>): LogEntry {
    return nilLog
  }
  abstract execute (user: Creature, target: Creature): LogEntry
}
export class CompositionAction extends Action {
  public consequences: Array<Consequence>;
  public groupConsequences: Array<GroupConsequence>;
  public targeters: Array<Targeter>;
  constructor (
    name: TextLike,
    desc: TextLike,
    properties: {
      conditions?: Array<Condition>;
      consequences?: Array<Consequence>;
      groupConsequences?: Array<GroupConsequence>;
      tests?: Array<CombatTest>;
      targeters?: Array<Targeter>;
    }
  ) {
    super(name, desc, properties.conditions ?? [], properties.tests ?? [])
    this.consequences = properties.consequences ?? []
    this.groupConsequences = properties.groupConsequences ?? []
    this.targeters = properties.targeters ?? [new SoloTargeter()]
  }
  execute (user: Creature, target: Creature): LogEntry {
    return new LogLines(
      ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target))
    )
  }
  executeAll (user: Creature, targets: Array<Creature>): LogEntry {
    return new LogLines(
      ...this.groupConsequences.map(consequence => consequence.apply(user, targets.filter(target => consequence.applicable(user, target))))
    )
  }
  describe (user: Creature, target: Creature): LogEntry {
    return new LogLines(
      ...this.consequences.map(consequence => consequence.describe(user, target)).concat(
        new Newline(),
        super.describe(user, target)
      )
    )
  }
  targets (primary: Creature, encounter: Encounter) {
    return this.targeters.flatMap(targeter => targeter.targets(primary, encounter)).unique()
  }
}
/**
 * A Condition describes whether or not something is permissible between two [[Creature]]s
 */
export interface Condition {
  allowed: (user: Creature, target: Creature) => boolean;
  explain: (user: Creature, target: Creature) => LogEntry;
}
export interface Actionable {
  actions: Array<Action>;
}
/**
 * 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 {
  /**
   * Executes when the effect is initially applied
   */
  onApply (creature: Creature): LogEntry { return nilLog }
  /**
   * Executes when the effect is removed
   */
  onRemove (creature: Creature): LogEntry { return nilLog }
  /**
   * Executes before the creature tries to perform an action
   */
  preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
    return {
      prevented: false,
      log: nilLog
    }
  }
  /**
   * Executes before another creature tries to perform an action that targets this creature
   */
  preReceiveAction (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
    return {
      prevented: false,
      log: nilLog
    }
  }
  /**
   * Executes before the creature receives damage (or healing)
   */
  preDamage (creature: Creature, damage: Damage): Damage {
    return damage
  }
  /**
   * Executes before the creature is attacked
   */
  preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
    return {
      prevented: false,
      log: nilLog
    }
  }
  /**
   * Executes when a creature's turn starts
   */
  preTurn (creature: Creature): { prevented: boolean; log: LogEntry } {
    return {
      prevented: false,
      log: nilLog
    }
  }
  /**
   * Modifies the effective resistance to a certain damage type
   */
  modResistance (type: DamageType, factor: number): number {
    return factor
  }
  /**
   * Called when a test is about to resolve. Decides if the creature should automatically fail.
   */
  failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } {
    return {
      failed: false,
      log: nilLog
    }
  }
  /**
   * Changes a creature's size. This represents the change in *mass*
   */
  scale (scale: number): number {
    return scale
  }
  /**
   * Additively modifies a creature's score for an offensive test
   */
  modTestOffense (attacker: Creature, defender: Creature, kind: TestCategory): number {
    return 0
  }
  /**
   * Additively modifies a creature's score for a defensive test
   */
  modTestDefense (defender: Creature, attacker: Creature, kind: TestCategory): number {
    return 0
  }
  /**
   * Affects digestion damage
   */
  modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage {
    return damage
  }
  /**
   * Triggers after consumption
   */
  postConsume (predator: Creature, prey: Creature, container: VoreContainer): LogEntry {
    return nilLog
  }
  /**
   * Affects a stat
   */
  modStat (creature: Creature, stat: Stat, current: number): number {
    return current
  }
  /**
   * Provides actions
   */
  actions (user: Creature): Array<Action> {
    return []
  }
}
/**
 * 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;
  intro: (world: World) => LogEntry;
}
/**
 * An Encounter describes a fight: who is in it and whose turn it is
 */
export class Encounter {
  initiatives: Map<Creature, number>
  currentMove: Creature
  turnTime = 100
  reward = 50 // Gold
  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 (totalTime = 0): LogEntry {
    this.initiatives.set(this.currentMove, 0)
    const times = new Map<Creature, number>()
    this.combatants.forEach(combatant => {
      // this should never be undefined
      const currentProgress = this.initiatives.get(combatant) ?? 0
      const remaining = (this.turnTime - currentProgress) / Math.sqrt(Math.max(combatant.stats.Agility, 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.sqrt(Math.max(this.currentMove.stats.Agility, 1))
    this.combatants.forEach(combatant => {
      // still not undefined...
      const currentProgress = this.initiatives.get(combatant) ?? 0
      this.initiatives.set(combatant, currentProgress + closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1)))
    })
    // TODO: still let the creature use drained-vigor moves
    if (this.currentMove.disabled) {
      return this.nextMove(closestRemaining + totalTime)
    } else {
      // applies digestion every time combat advances
      const tickResults = this.combatants.flatMap(
        combatant => combatant.containers.map(
          container => container.tick(5 * (closestRemaining + totalTime))
        )
      )
      const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented)
      if (effectResults.some(result => result.prevented)) {
        const parts = effectResults.map(result => result.log).concat([this.nextMove()])
        return new LogLines(
          ...parts,
          ...tickResults
        )
      } else {
        return new LogLines(
          ...tickResults
        )
      }
    }
    return nilLog
  }
  /**
   * Combat is won once one side is completely disabled
   */
  get winner (): null|Side {
    const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.disabled).map(combatant => combatant.side))
    if (remaining.size === 1) {
      return Array.from(remaining)[0]
    } else {
      return null
    }
  }
  /**
   * Combat is completely won once one side is completely destroyed
   */
  get totalWinner (): null|Side {
    const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.destroyed).map(combatant => combatant.side))
    if (remaining.size === 1) {
      return Array.from(remaining)[0]
    } else {
      return null
    }
  }
}
export abstract class Consequence {
  constructor (public conditions: Condition[]) {
  }
  applicable (user: Creature, target: Creature): boolean {
    return this.conditions.every(cond => cond.allowed(user, target))
  }
  abstract describe (user: Creature, target: Creature): LogEntry
  abstract apply (user: Creature, target: Creature): LogEntry
}
export abstract class GroupConsequence {
  constructor (public conditions: Condition[]) {
  }
  applicable (user: Creature, target: Creature): boolean {
    return this.conditions.every(cond => cond.allowed(user, target))
  }
  abstract describe (user: Creature, targets: Array<Creature>): LogEntry
  abstract apply (user: Creature, targets: Array<Creature>): LogEntry
}
 |