|  | import { Creature, POV, Entity } from './entity'
import { POVPair, POVPairArgs, TextLike, DynText, LiveText } from './language'
import { Container } from './vore'
import { LogEntry, LogLines, CompositeLog, FAElem, LogLine, FormatEntry, FormatOpt } from './interface'
export enum DamageType {
  Pierce = "Pierce",
  Slash = "Slash",
  Crush = "Crush",
  Acid = "Acid",
  Seduction = "Seduction",
  Dominance = "Dominance"
}
export interface DamageInstance {
  type: DamageType;
  amount: number;
  target: Vigor;
}
export enum Vigor {
  Health = "Health",
  Stamina = "Stamina",
  Willpower = "Willpower"
}
export const VigorIcons: {[key in Vigor]: string} = {
  [Vigor.Health]: "fas fa-heart",
  [Vigor.Stamina]: "fas fa-bolt",
  [Vigor.Willpower]: "fas fa-brain"
}
export type Vigors = {[key in Vigor]: number}
export enum Stat {
    STR = 'Strength',
    DEX = 'Dexterity',
    CON = 'Constitution'
}
export type Stats = {[key in Stat]: number}
export const StatIcons: {[key in Stat]: string} = {
  [Stat.STR]: 'fas fa-fist-raised',
  [Stat.DEX]: 'fas fa-feather',
  [Stat.CON]: 'fas fa-heartbeat'
}
export interface CombatTest {
  test: (user: Creature, target: Creature) => boolean;
  odds: (user: Creature, target: Creature) => number;
  explain: (user: Creature, target: Creature) => LogEntry;
}
function logistic (x0: number, L: number, k: number): (x: number) => number {
  return (x: number) => {
    return L / (1 + Math.exp(-k * (x - x0)))
  }
}
abstract class RandomTest implements CombatTest {
  test (user: Creature, target: Creature): boolean {
    return Math.random() < this.odds(user, target)
  }
  abstract odds(user: Creature, target: Creature): number
  abstract explain(user: Creature, target: Creature): LogEntry
}
export class StatVigorTest extends RandomTest {
  private f: (x: number) => number
  constructor (public readonly stat: Stat, k = 0.1) {
    super()
    this.f = logistic(0, 1, k)
  }
  odds (user: Creature, target: Creature): number {
    let userPercent = 1
    let targetPercent = 1
    Object.keys(Vigor).forEach(key => {
      userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
      targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
      userPercent = Math.max(0, userPercent)
      targetPercent = Math.max(0, targetPercent)
    })
    if (userPercent === 0) {
      targetPercent *= 4
    }
    if (targetPercent === 0) {
      userPercent *= 4
    }
    console.log(userPercent, targetPercent, this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent))
    return this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
  }
  explain (user: Creature, target: Creature): LogEntry {
    const delta: number = user.stats[this.stat] - target.stats[this.stat]
    let result: string
    if (delta === 0) {
      result = 'You and the target have the same ' + this.stat + '.'
    } else if (delta < 0) {
      result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.'
    } else {
      result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.'
    }
    result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%'
    return new LogLines(result)
  }
}
export class StatTest extends RandomTest {
  private f: (x: number) => number
  constructor (public readonly stat: Stat, k = 0.1) {
    super()
    this.f = logistic(0, 1, k)
  }
  odds (user: Creature, target: Creature): number {
    return this.f(user.stats[this.stat] - target.stats[this.stat])
  }
  explain (user: Creature, target: Creature): LogEntry {
    const delta: number = user.stats[this.stat] - target.stats[this.stat]
    let result: string
    if (delta === 0) {
      result = 'You and the target have the same ' + this.stat + '.'
    } else if (delta < 0) {
      result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.'
    } else {
      result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.'
    }
    result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%'
    return new LogLines(result)
  }
}
export class ChanceTest extends RandomTest {
  constructor (public readonly chance: number) {
    super()
  }
  odds (user: Creature, target: Creature): number {
    return this.chance
  }
  explain (user: Creature, target: Creature): LogEntry {
    return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.')
  }
}
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)
  }
  toString (): string {
    return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
  }
  render (): LogEntry {
    return new LogLine(...this.damages.flatMap(instance => {
      return [instance.amount.toString(), new FAElem(VigorIcons[instance.target]), " " + instance.type]
    }))
  }
  renderShort (): LogEntry {
    const totals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
    this.damages.forEach(instance => {
      totals[instance.target] += instance.amount
    })
    return new FormatEntry(new LogLine(...Object.keys(Vigor).flatMap(key => totals[key as Vigor] === 0 ? [] : [totals[key as Vigor].toFixed(0).toString(), new FAElem(VigorIcons[key as Vigor])])), FormatOpt.DamageInst)
  }
}
export interface Combatant {
    actions: Array<Action>;
}
export abstract class Action {
  allowed (user: Creature, target: Creature): boolean {
    return this.conditions.every(cond => cond.allowed(user, target))
  }
  abstract execute(user: Creature, target: Creature): LogEntry
  constructor (public name: TextLike, public desc: string, private conditions: Array<Condition> = []) {
  }
  toString (): string {
    return this.name.toString()
  }
}
export interface Condition {
  allowed: (user: Creature, target: Creature) => boolean;
}
class InverseCondition implements Condition {
  allowed (user: Creature, target: Creature): boolean {
    return !this.condition.allowed(user, target)
  }
  constructor (private condition: Condition) {
  }
}
class CapableCondition implements Condition {
  allowed (user: Creature, target: Creature): boolean {
    return !user.disabled
  }
}
class DrainedVigorCondition implements Condition {
  allowed (user: Creature, target: Creature): boolean {
    return user.vigors[this.vigor] <= 0
  }
  constructor (private vigor: Vigor) {
  }
}
export interface Actionable {
  actions: Array<Action>;
}
abstract class SelfAction extends Action {
  allowed (user: Creature, target: Creature) {
    if (user === target) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
}
abstract class PairAction extends Action {
  allowed (user: Creature, target: Creature) {
    if (user !== target) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
}
abstract class TogetherAction extends PairAction {
  allowed (user: Creature, target: Creature) {
    if (user.containedIn === target.containedIn) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
}
export class AttackAction extends TogetherAction {
  protected test: StatTest
  protected successLines: POVPairArgs<Entity, Entity, { damage: Damage }> = new POVPairArgs([
    [[POV.First, POV.Third], (user, target, args) => new LogLine(
      `You smack ${target.name} for `,
      args.damage.renderShort()
    )],
    [[POV.Third, POV.First], (user, target, args) => new LogLine(
      `${user.name.capital} smacks you for `,
      args.damage.renderShort()
    )],
    [[POV.Third, POV.Third], (user, target, args) => new LogLine(
      `${user.name.capital} smacks ${target.name} for `,
      args.damage.renderShort()
    )]
  ])
  protected failLines: POVPair<Entity, Entity> = new POVPair([
    [[POV.First, POV.Third], (user, target) => new LogLines(`You try to smack ${target.name}, but you miss`)],
    [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} misses you`)],
    [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} misses ${target.name}`)]
  ])
  constructor (protected damage: Damage) {
    super('Attack', 'Attack the enemy', [new CapableCondition()])
    this.test = new StatTest(Stat.STR)
  }
  execute (user: Creature, target: Creature): LogEntry {
    if (this.test.test(user, target)) {
      target.takeDamage(this.damage)
      return this.successLines.run(user, target, { damage: this.damage })
    } else {
      return this.failLines.run(user, target)
    }
  }
}
export class DevourAction extends TogetherAction {
  private test: StatVigorTest
  protected failLines: POVPair<Entity, Entity> = new POVPair([
    [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to make a meal out of ${target.name}`)],
    [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to devour you, but fails`)],
    [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} unsuccessfully tries to swallow ${target.name}`)]
  ])
  allowed (user: Creature, target: Creature): boolean {
    const owner = this.container.owner === user
    const predOk = Array.from(this.container.voreTypes).every(pref => user.predPrefs.has(pref))
    const preyOk = Array.from(this.container.voreTypes).every(pref => target.preyPrefs.has(pref))
    if (owner && predOk && preyOk) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (protected container: Container) {
    super(new DynText('Devour (', new LiveText(container, x => x.name.all), ')'), 'Try to consume your foe', [new CapableCondition()])
    this.test = new StatVigorTest(Stat.STR)
  }
  execute (user: Creature, target: Creature): LogEntry {
    if (this.test.test(user, target)) {
      return this.container.consume(target)
    } else {
      return this.failLines.run(user, target)
    }
  }
}
export class FeedAction extends TogetherAction {
  private test: StatTest
  protected failLines: POVPair<Entity, Entity> = new POVPair([
    [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to feed yourself to ${target.name}`)],
    [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to feed ${user.pronouns.possessive} to you, but fails`)],
    [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} unsuccessfully tries to feed ${user.pronouns.possessive} to ${target.name}`)]
  ])
  allowed (user: Creature, target: Creature): boolean {
    const owner = this.container.owner === target
    const predOk = Array.from(this.container.voreTypes).every(pref => user.predPrefs.has(pref))
    const preyOk = Array.from(this.container.voreTypes).every(pref => target.preyPrefs.has(pref))
    if (owner && predOk && preyOk) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (protected container: Container) {
    super('Feed', 'Feed yourself to your opponent', [new DrainedVigorCondition(Vigor.Willpower)])
    this.name += ` (${container.name})`
    this.test = new StatTest(Stat.STR)
  }
  execute (user: Creature, target: Creature): LogEntry {
    return this.container.consume(user)
  }
}
export class StruggleAction extends PairAction {
  private test: StatVigorTest
  protected failLines: POVPair<Entity, Entity> = new POVPair([
    [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to escape from ${target.name}`)],
    [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to escape from you, but fails`)],
    [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} unsuccessfully struggles within ${target.name}`)]
  ])
  allowed (user: Creature, target: Creature) {
    if (user.containedIn === this.container && this.container.owner === target) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (public container: Container) {
    super(new DynText('Struggle (', new LiveText(container, x => x.name.all), ')'), 'Try to escape from your foe', [new CapableCondition()])
    this.test = new StatVigorTest(Stat.STR)
  }
  execute (user: Creature, target: Creature): LogEntry {
    if (user.containedIn !== null) {
      if (this.test.test(user, target)) {
        return user.containedIn.release(user)
      } else {
        return this.failLines.run(user, target)
      }
    } else {
      return new LogLines("Vore's bugged!")
    }
  }
}
export abstract class EatenAction extends PairAction {
  protected lines: POVPair<Entity, Entity> = new POVPair([])
  allowed (user: Creature, target: Creature) {
    if (target.containedIn === this.container) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (public container: Container, name: TextLike, desc: string) {
    super(new DynText(name, ' (', new LiveText(container, x => x.name.all), ')'), 'Do something to your prey!', [new CapableCondition()])
  }
}
export class DigestAction extends SelfAction {
  protected lines: POVPair<Entity, Entity> = new POVPair([])
  allowed (user: Creature, target: Creature) {
    if (this.container.owner === user && this.container.contents.length > 0) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (protected container: Container) {
    super(new DynText('Digest (', new LiveText(container, container => container.name.all), ')'), 'Digest your prey', [new CapableCondition()])
  }
  execute (user: Creature, target: Creature): LogEntry {
    const results = this.container.tick(60)
    return new CompositeLog(results)
  }
}
export class ReleaseAction extends PairAction {
  allowed (user: Creature, target: Creature) {
    if (target.containedIn === this.container && this.container.contents.indexOf(target) >= 0) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (protected container: Container) {
    super(new DynText('Release (', new LiveText(container, x => x.name.all), ')'), 'Release one of your prey', [new CapableCondition()])
  }
  execute (user: Creature, target: Creature): LogEntry {
    return this.container.release(target)
  }
}
export class TransferAction extends PairAction {
  lines: POVPairArgs<Entity, Entity, { from: Container; to: Container }> = new POVPairArgs([
    [[POV.First, POV.Third], (user, target, args) => new LogLine(`You squeeze ${target.name} from your ${args.from.name} to your ${args.to.name}`)],
    [[POV.Third, POV.First], (user, target, args) => new LogLine(`You're squeezed from ${user.name}'s ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}`)],
    [[POV.Third, POV.Third], (user, target, args) => new LogLine(`${user.name} squeezes ${target.name} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}`)]
  ])
  allowed (user: Creature, target: Creature) {
    if (target.containedIn === this.from) {
      return super.allowed(user, target)
    } else {
      return false
    }
  }
  constructor (protected from: Container, protected to: Container) {
    super('Transfer', `Shove your prey from your ${from.name} to your ${to.name}`, [new CapableCondition()])
  }
  execute (user: Creature, target: Creature): LogEntry {
    this.from.release(target)
    this.to.consume(target)
    return this.lines.run(user, target, { from: this.from, to: this.to })
  }
}
 |