import { Creature, POV, Entity } from './entity' import { POVPair, POVPairArgs } 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 = [] 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; } 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: string, public desc: string, private conditions: Array = []) { } toString (): string { return this.name } } 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; } 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 = 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 = 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(`${target.name} 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 = 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(`${target.name} 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('Devour', 'Try to consume your foe', [new CapableCondition()]) this.name += ` (${container.name})` 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 = 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(`${target.name} 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 { if (this.test.test(user, target)) { return this.container.consume(user) } else { return this.failLines.run(user, target) } } } export class StruggleAction extends PairAction { private test: StatVigorTest protected failLines: POVPair = 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(`${target.name} unsuccessfully struggles within ${target.name}`)] ]) allowed (user: Creature, target: Creature) { if (user.containedIn === this.container) { return super.allowed(user, target) } else { return false } } constructor (public container: Container) { super('Struggle', 'Try to escape your predator', [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 class DigestAction extends SelfAction { protected lines: POVPair = 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('Digest', 'Digest all of your current prey', [new CapableCondition()]) this.name += ` (${container.name})` } 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('Release', 'Release one of your prey') this.name += ` (${container.name})` } execute (user: Creature, target: Creature): LogEntry { return this.container.release(target) } } export class TransferAction extends PairAction { protected lines: POVPairArgs = 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 ${target.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 }) } }