import { Damage, Stats, Action, Vigor, Side, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors, Encounter } from '@/game/combat' import { Noun, Pronoun, SoloLine, Verb } from '@/game/language' import { LogEntry, LogLines, LogLine } from '@/game/interface' import { VoreType, Container } from '@/game/vore' import { Item, EquipmentSlot, Equipment, ItemKind, Currency } from '@/game/items' import { PassAction } from '@/game/combat/actions' import { AI, RandomAI } from '@/game/ai' import { Entity, Resistances } from '@/game/entity' import { Perk } from '@/game/combat/perks' import { VoreRelay } from '@/game/events' export class Creature extends Entity { voreRelay: VoreRelay = new VoreRelay() baseResistances: Resistances stats: Stats = (Object.keys(Stat) as Array).reduce((result: Partial, stat: Stat) => { Object.defineProperty(result, stat, { get: () => this.effects.reduce((total, effect) => effect.modStat(this, stat, total), this.baseStats[stat]), set: (value: number) => { this.baseStats[stat] = value }, enumerable: true }) return result }, {}) as Stats vigors: {[key in Vigor]: number} = { [Vigor.Health]: 100, [Vigor.Stamina]: 100, [Vigor.Resolve]: 100 } destroyed = false; containers: Array = [] containedIn: Container | null = null voreStats: VoreStats actions: Array = []; desc = "Some creature"; get effects (): Array { const effects: Array = (this.statusEffects as Effective[]).concat( Object.values(this.equipment).filter(item => item !== undefined).flatMap( item => (item as Equipment).effects ), this.perks ) return effects } statusEffects: Array = []; perks: Array = []; items: Array = []; /* eslint-disable-next-line */ wallet: { [key in Currency]: number } = Object.keys(Currency).reduce((total: any, key) => { total[key] = 0; return total }, {}); otherActions: Array = []; side: Side; title = "Lv. 1 Creature"; equipment: {[key in EquipmentSlot]?: Equipment } = {} ai: AI constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats, public preyPrefs: Set, public predPrefs: Set, private baseMass: number) { super(name, kind, pronouns) /* eslint-disable-next-line */ this.baseResistances = Object.keys(DamageType).reduce((resist: any, key) => { resist[key] = 1; return resist }, {}) Object.entries(this.maxVigors).forEach(([key, val]) => { this.vigors[key as Vigor] = val }) this.actions.push(new PassAction()) this.side = Side.Heroes /* eslint-disable-next-line */ const self = this this.ai = new RandomAI(this) this.voreStats = { get [VoreStat.Bulk] () { return self.containers.reduce( (total: number, container: Container) => { return total + container.contents.reduce( (total: number, prey: Creature) => { return total + prey.voreStats.Bulk }, 0 ) + container.digested.reduce( (total: number, prey: Creature) => { return total + prey.voreStats.Bulk }, 0 ) }, self.voreStats.Mass ) }, get [VoreStat.Mass] () { const base = self.baseMass const adjusted = self.effects.reduce((scale: number, effect: Effective) => effect.scale(scale), base) return adjusted }, // we want to account for anything changing our current size; // we will assume that the modifiers are all multiplicative set [VoreStat.Mass] (mass: number) { const modifier = self.effects.reduce((scale: number, effect: Effective) => effect.scale(scale), 1) const adjusted = mass / modifier self.baseMass = adjusted }, get [VoreStat.Prey] () { return self.containers.reduce( (total: number, container: Container) => { return total + container.contents.concat(container.digested).reduce( (total: number, prey: Creature) => { return total + 1 + prey.voreStats[VoreStat.Prey] }, 0 ) }, 0 ) } } } resistanceTo (damageType: DamageType): number { return this.baseResistances[damageType] } get maxVigors (): Readonly { return { Health: this.stats.Toughness * 10 + this.stats.Power * 5, Resolve: this.stats.Willpower * 10 + this.stats.Charm * 5, Stamina: this.stats.Agility * 5 + this.stats.Reflexes * 5 } } get disabled (): boolean { return Object.values(this.vigors).some(val => val <= 0) } effectiveDamage (damage: Damage): Damage { const newDamages: DamageInstance[] = [] damage.damages.forEach(instance => { const factor = instance.type === DamageType.Heal ? -1 : 1 const baseResistance: number = this.resistanceTo(instance.type) const resistance = baseResistance * factor newDamages.push({ amount: instance.amount * resistance, target: instance.target, type: instance.type }) }) return new Damage(...newDamages) } takeDamage (damage: Damage): LogEntry { // first, we record health to decide if the entity just died const startHealth = this.vigors.Health damage = this.effectiveDamage(damage) damage.damages.forEach(instance => { if (instance.target in Vigor) { // just deal damage this.vigors[instance.target as Vigor] -= instance.amount } else if (instance.target in Stat) { // drain the stats, then deal damage to match const startVigors = this.maxVigors this.stats[instance.target as Stat] -= instance.amount const endVigors = this.maxVigors Object.keys(Vigor).map(vigor => { this.vigors[vigor as Vigor] -= startVigors[vigor as Vigor] - endVigors[vigor as Vigor] }) } }) Object.keys(Vigor).forEach(vigorStr => { const vigor = vigorStr as Vigor if (this.vigors[vigor] > this.maxVigors[vigor]) { this.vigors[vigor] = this.maxVigors[vigor] } }) if (this.vigors.Health <= -this.maxVigors.Health) { this.destroyed = true } if (this.vigors.Health <= 0 && startHealth > 0) { return this.destroy() } else { return new LogLine() } } toString (): string { return this.name.toString() } applyEffect (effect: StatusEffect): LogEntry { this.statusEffects.push(effect) return effect.onApply(this) } addContainer (container: Container): void { this.containers.push(container) this.voreRelay.connect(container.voreRelay) } addPerk (perk: Perk): void { this.perks.push(perk) } // TODO replace the logic for getting blocked or prevented from acting executeAction (action: Action, targets: Array): LogEntry { return action.try(this, targets) } removeEffect (effect: StatusEffect): LogEntry { this.statusEffects = this.statusEffects.filter(eff => eff !== effect) return effect.onRemove(this) } equip (item: Equipment, slot: EquipmentSlot) { const equipped = this.equipment[slot] if (equipped !== undefined) { this.unequip(slot) } this.equipment[slot] = item } unequip (slot: EquipmentSlot) { const item = this.equipment[slot] if (item !== undefined) { this.items.push(item) this.equipment[slot] = undefined } } get status (): Array { const results: Array = [] if (this.vigors[Vigor.Health] <= 0) { results.push(new ImplicitStatus('Dead', 'Out of health', 'fas fa-heart')) } if (this.vigors[Vigor.Stamina] <= 0) { results.push(new ImplicitStatus('Unconscious', 'Out of stamina', 'fas fa-bolt')) } if (this.vigors[Vigor.Resolve] <= 0) { results.push(new ImplicitStatus('Broken', 'Out of resolve', 'fas fa-brain')) } if (this.containedIn !== null) { results.push(new ImplicitStatus('Eaten', 'Devoured by ' + this.containedIn.owner.name, 'fas fa-drumstick-bite')) } this.statusEffects.forEach(effect => { results.push(effect) }) return results } allActions (target: Creature): Array { let choices = ([] as Action[]).concat( this.actions, this.containers.flatMap(container => container.actions), target.otherActions, Object.values(this.equipment).filter(item => item !== undefined).flatMap(item => (item as Equipment).actions), this.items.filter(item => item.kind === ItemKind.Consumable && !item.consumed).flatMap(item => item.actions), this.perks.flatMap(perk => perk.actions(this)) ) if (this.containedIn !== null) { choices = choices.concat(this.containedIn.actions) } return choices } validActions (target: Creature): Array { return this.allActions(target).filter(action => { return action.allowed(this, target) }) } validSoloActions (target: Creature, encounter: Encounter): Array { return this.validActions(target).filter(action => action.targets(target, encounter).length === 1) } validGroupActions (target: Creature, encounter: Encounter): Array { return this.validActions(target).filter(action => action.targets(target, encounter).length > 1) } destroyLine: SoloLine = (victim) => new LogLine( `${victim.name.capital} ${victim.name.conjugate(new Verb('die'))}` ) destroy (): LogEntry { const released: Array = this.containers.flatMap(container => { return container.contents.map(prey => { prey.containedIn = this.containedIn if (this.containedIn !== null) { this.containedIn.contents.push(prey) } return prey }) }) const names = released.reduce((list: Array, prey: Creature) => list.concat([prey.name.toString()]), []).joinGeneral(", ", " and ").join("") if (released.length > 0) { if (this.containedIn === null) { return new LogLines( this.destroyLine(this), new LogLine(names + ` spill out!`) ) } else { return new LogLines( this.destroyLine(this), new LogLine(names + ` spill out into ${this.containedIn.owner.name}'s ${this.containedIn.name}!`) ) } } else { return this.destroyLine(this) } } }