From 15a12f0c69b86826a18b75c9d3a18de0300db157 Mon Sep 17 00:00:00 2001 From: Fen Dweller Date: Thu, 23 Jul 2020 10:04:17 -0400 Subject: [PATCH] Add healing; adjust digestion text; allow VoreStats to be used for damage calculations --- src/game/combat.ts | 37 +++-- src/game/combat/actions.ts | 14 +- src/game/creatures/human.ts | 5 - src/game/creatures/kenzie.ts | 16 +- src/game/creatures/withers.ts | 5 +- src/game/entity.ts | 287 +++++++++++++++++----------------- src/game/interface.ts | 6 +- src/game/items.ts | 18 ++- src/game/vore.ts | 35 ++++- 9 files changed, 239 insertions(+), 184 deletions(-) diff --git a/src/game/combat.ts b/src/game/combat.ts index 688458d..905f15c 100644 --- a/src/game/combat.ts +++ b/src/game/combat.ts @@ -10,7 +10,8 @@ export enum DamageType { Crush = "Crush", Acid = "Acid", Seduction = "Seduction", - Dominance = "Dominance" + Dominance = "Dominance", + Heal = "Heal" } export interface DamageInstance { @@ -147,10 +148,11 @@ export class Damage { 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] += instance.amount + vigorTotals[instance.target as Vigor] += factor * instance.amount } else if (instance.target in Stat) { - statTotals[instance.target as Stat] += instance.amount + statTotals[instance.target as Stat] += factor * instance.amount } }) @@ -213,13 +215,28 @@ export class UniformRandomDamageFormula implements DamageFormula { export class StatDamageFormula implements DamageFormula { calc (user: Creature, target: Creature): Damage { - const instances: Array = this.factors.map(factor => ( - { - amount: factor.fraction * user.stats[factor.stat], - target: factor.target, - type: factor.type + 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) } @@ -244,7 +261,7 @@ export class StatDamageFormula implements DamageFormula { ) } - constructor (private factors: Array<{ stat: Stat; fraction: number; type: DamageType; target: Vigor|Stat }>) { + constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) { } } diff --git a/src/game/combat/actions.ts b/src/game/combat/actions.ts index a8336e1..38f37ce 100644 --- a/src/game/combat/actions.ts +++ b/src/game/combat/actions.ts @@ -1,5 +1,5 @@ import { StatTest, StatVigorTest } from './tests' -import { POVPairArgs, POVPair, DynText, LiveText, TextLike } from '../language' +import { POVPairArgs, POVPair, DynText, LiveText, TextLike, Verb } from '../language' import { Entity, POV, Creature } from '../entity' import { Damage, DamageFormula, Stat, Vigor, Action } from '../combat' import { LogLine, LogLines, LogEntry, CompositeLog } from '../interface' @@ -11,28 +11,28 @@ export class AttackAction extends Action { protected successLines: POVPairArgs = new POVPairArgs([ [[POV.First, POV.Third], (user, target, args) => new LogLine( - `You smack ${target.name} for `, + `You ${this.verb} ${target.name} for `, args.damage.renderShort() )], [[POV.Third, POV.First], (user, target, args) => new LogLine( - `${user.name.capital} smacks you for `, + `${user.name.capital} ${this.verb.singular} you for `, args.damage.renderShort() )], [[POV.Third, POV.Third], (user, target, args) => new LogLine( - `${user.name.capital} smacks ${target.name} for `, + `${user.name.capital} ${this.verb.singular} ${target.name} for `, args.damage.renderShort() )] ]) protected failLines: POVPair = new POVPair([ - [[POV.First, POV.Third], (user, target) => new LogLine(`You try to smack ${target.name}, but you miss`)], + [[POV.First, POV.Third], (user, target) => new LogLine(`You try to ${this.verb.present} ${target.name}, but you miss`)], [[POV.Third, POV.First], (user, target) => new LogLine(`${user.name.capital} misses you`)], [[POV.Third, POV.Third], (user, target) => new LogLine(`${user.name.capital} misses ${target.name}`)] ]) - constructor (protected damage: DamageFormula) { + constructor (protected damage: DamageFormula, protected verb: Verb = new Verb('smack')) { super( - 'Attack', + verb.root.capital, 'Attack the enemy', [new CapableCondition(), new TogetherCondition(), new EnemyCondition()] ) diff --git a/src/game/creatures/human.ts b/src/game/creatures/human.ts index 50b8ee3..ba3cf65 100644 --- a/src/game/creatures/human.ts +++ b/src/game/creatures/human.ts @@ -23,10 +23,5 @@ export class Human extends Creature { this.title = "Snack" this.desc = "Definitely going on an adventure" - this.actions.push(new AttackAction(new ConstantDamageFormula( - new Damage( - { amount: 20, target: Vigor.Health, type: DamageType.Slash } - ) - ))) } } diff --git a/src/game/creatures/kenzie.ts b/src/game/creatures/kenzie.ts index cbc40b9..dba26a7 100644 --- a/src/game/creatures/kenzie.ts +++ b/src/game/creatures/kenzie.ts @@ -1,9 +1,9 @@ import { Creature, POV } from '../entity' -import { ProperNoun, ImproperNoun, FemalePronouns, POVPairArgs, POVPair } from '../language' +import { ProperNoun, ImproperNoun, FemalePronouns, POVPairArgs, POVPair, Verb } from '../language' import { VoreType, Stomach, Vore } from '../vore' -import { Side, Damage, DamageType, Vigor, UniformRandomDamageFormula } from '../combat' +import { Side, Damage, DamageType, Vigor, UniformRandomDamageFormula, ConstantDamageFormula, StatDamageFormula, Stat, VoreStat } from '../combat' import { LogLine } from '../interface' -import { FeedAction, TransferAction } from '../combat/actions' +import { FeedAction, TransferAction, AttackAction } from '../combat/actions' import * as Words from '../words' export class Kenzie extends Creature { @@ -30,5 +30,15 @@ export class Kenzie extends Creature { )) this.containers.push(stomach) + + this.actions.push( + new AttackAction( + new StatDamageFormula([ + { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Crush }, + { fraction: 0.05, stat: VoreStat.Bulk, target: Vigor.Health, type: DamageType.Crush } + ]), + new Verb('crush', 'crushes', 'crushing', 'crushed') + ) + ) } } diff --git a/src/game/creatures/withers.ts b/src/game/creatures/withers.ts index d2dc5c2..8c0f5db 100644 --- a/src/game/creatures/withers.ts +++ b/src/game/creatures/withers.ts @@ -116,7 +116,10 @@ const huge = new RandomWord([ class BiteAction extends AttackAction { constructor () { - super(new ConstantDamageFormula(new Damage({ amount: 50, type: DamageType.Slash, target: Vigor.Health }))) + super( + new ConstantDamageFormula(new Damage({ amount: 50, type: DamageType.Slash, target: Vigor.Health })), + new Verb('bite', 'bites', 'biting', 'bit') + ) this.name = "Bite" } } diff --git a/src/game/entity.ts b/src/game/entity.ts index cc4db55..a1bc214 100644 --- a/src/game/entity.ts +++ b/src/game/entity.ts @@ -30,158 +30,165 @@ export interface Mortal extends Entity { export class Creature extends Vore implements Combatant { title = "Lv. 1 Creature" desc = "Some creature" - vigors = { - [Vigor.Health]: 100, - [Vigor.Stamina]: 100, - [Vigor.Resolve]: 100 + vigors = { + [Vigor.Health]: 100, + [Vigor.Stamina]: 100, + [Vigor.Resolve]: 100 + } + + maxVigors = { + [Vigor.Health]: 100, + [Vigor.Stamina]: 100, + [Vigor.Resolve]: 100 + } + + baseStats: Stats + voreStats: VoreStats + side: Side + + get disabled (): boolean { + return Object.values(this.vigors).some(val => val <= 0) + } + + resistances: Map = new Map() + perspective: POV = POV.Third + containers: Array = [] + otherContainers: Array = [] + actions: Array = [] + groupActions: Array = [] + otherActions: Array = [] + + items: Array = [] + + get bulk (): number { + return this.voreStats.Mass + this.containers.reduce((total, conatiner) => { return total + conatiner.contents.reduce((total, prey) => total + prey.voreStats.Bulk, 0) }, 0) + } + + containedIn: VoreContainer|null = null; + + constructor (public name: Noun, public kind: Noun, public pronouns: Pronoun, public stats: Stats, public preyPrefs: Set, public predPrefs: Set, mass: number) { + super() + const containers = this.containers + + this.vigors.Health = this.maxVigors.Health = stats.Toughness * 10 + stats.Power * 5 + this.vigors.Stamina = this.maxVigors.Stamina = stats.Toughness * 3 + stats.Speed * 10 + stats.Willpower * 3 + this.vigors.Resolve = this.maxVigors.Resolve = stats.Willpower * 10 + stats.Charm * 5 + + this.baseStats = Object.keys(Stat).reduce((base: any, key) => { base[key] = stats[key as Stat]; return base }, {}) + this.side = Side.Heroes + this.voreStats = { + get [VoreStat.Bulk] () { + console.log(containers) + return containers.reduce( + (total: number, container: VoreContainer) => { + return total + container.contents.reduce( + (total: number, prey: Vore) => { + return total + prey.voreStats.Bulk + }, + 0 + ) + container.digested.reduce( + (total: number, prey: Vore) => { + return total + prey.voreStats.Bulk + }, + 0 + ) + }, + this.Mass + ) + }, + [VoreStat.Mass]: mass, + get [VoreStat.PreyCount] () { + return containers.reduce( + (total: number, container: VoreContainer) => { + return total + container.contents.reduce( + (total: number, prey: Vore) => { + return total + 1 + prey.voreStats[VoreStat.PreyCount] + }, + 0 + ) + }, + 0 + ) + } } + } + + toString (): string { + return this.name.toString() + } + + takeDamage (damage: Damage): LogEntry { + const startHealth = this.vigors.Health + damage.damages.forEach(instance => { + const factor = instance.type === DamageType.Heal ? -1 : 1 + const resistance: number|undefined = this.resistances.get(instance.type) + if (resistance !== undefined) { + if (instance.target in Vigor) { + this.vigors[instance.target as Vigor] -= instance.amount * factor * resistance + } else if (instance.target in Stat) { + this.stats[instance.target as Stat] -= instance.amount * factor * resistance + } + } else { + if (instance.target in Vigor) { + this.vigors[instance.target as Vigor] -= instance.amount * factor + } else if (instance.target in Stat) { + this.stats[instance.target as Stat] -= instance.amount * factor + } + } + }) - maxVigors = { - [Vigor.Health]: 100, - [Vigor.Stamina]: 100, - [Vigor.Resolve]: 100 + if (this.vigors.Health <= 0 && startHealth > 0) { + return this.destroy() + } else { + return new LogLine() } + } - baseStats: Stats - voreStats: VoreStats - side: Side - - get disabled (): boolean { - return Object.values(this.vigors).some(val => val <= 0) + get status (): string { + if (this.vigors[Vigor.Health] <= 0) { + return "Dead" } - - resistances: Map = new Map() - perspective: POV = POV.Third - containers: Array = [] - otherContainers: Array = [] - actions: Array = [] - groupActions: Array = [] - otherActions: Array = [] - - items: Array = [] - - get bulk (): number { - return this.voreStats.Mass + this.containers.reduce((total, conatiner) => { return total + conatiner.contents.reduce((total, prey) => total + prey.voreStats.Bulk, 0) }, 0) + if (this.vigors[Vigor.Stamina] <= 0) { + return "Unconscious" } - - containedIn: VoreContainer|null = null; - - constructor (public name: Noun, public kind: Noun, public pronouns: Pronoun, public stats: Stats, public preyPrefs: Set, public predPrefs: Set, mass: number) { - super() - const containers = this.containers - this.baseStats = Object.keys(Stat).reduce((base: any, key) => { base[key] = stats[key as Stat]; return base }, {}) - this.side = Side.Heroes - this.voreStats = { - get [VoreStat.Bulk] () { - console.log(containers) - return containers.reduce( - (total: number, container: VoreContainer) => { - return total + container.contents.reduce( - (total: number, prey: Vore) => { - return total + prey.voreStats.Bulk - }, - 0 - ) + container.digested.reduce( - (total: number, prey: Vore) => { - return total + prey.voreStats.Bulk - }, - 0 - ) - }, - this.Mass - ) - }, - [VoreStat.Mass]: mass, - get [VoreStat.PreyCount] () { - return containers.reduce( - (total: number, container: VoreContainer) => { - return total + container.contents.reduce( - (total: number, prey: Vore) => { - return total + 1 + prey.voreStats[VoreStat.PreyCount] - }, - 0 - ) - }, - 0 - ) - } - } + if (this.vigors[Vigor.Resolve] <= 0) { + return "Broken" } - - toString (): string { - return this.name.toString() + if (this.containedIn !== null) { + return `${this.containedIn.consumeVerb.past.capital} by ${this.containedIn.owner.name}` } - takeDamage (damage: Damage): LogEntry { - const startHealth = this.vigors.Health - damage.damages.forEach(instance => { - const resistance: number|undefined = this.resistances.get(instance.type) - if (resistance !== undefined) { - if (instance.target in Vigor) { - this.vigors[instance.target as Vigor] -= instance.amount * resistance - } else if (instance.target in Stat) { - this.stats[instance.target as Stat] -= instance.amount * resistance - } - } else { - if (instance.target in Vigor) { - this.vigors[instance.target as Vigor] -= instance.amount - } else if (instance.target in Stat) { - this.stats[instance.target as Stat] -= instance.amount - } - } - }) + return "Normal" + } - if (this.vigors.Health <= 0 && startHealth > 0) { - return this.destroy() - } else { - return new LogLine() - } - } - - get status (): string { - if (this.vigors[Vigor.Health] <= 0) { - return "Dead" - } - if (this.vigors[Vigor.Stamina] <= 0) { - return "Unconscious" - } - if (this.vigors[Vigor.Resolve] <= 0) { - return "Broken" - } - if (this.containedIn !== null) { - return `${this.containedIn.consumeVerb.past.capital} by ${this.containedIn.owner.name}` - } - - return "Normal" - } - - validActions (target: Creature): Array { - let choices = this.actions.concat( - this.containers.flatMap(container => container.actions)).concat( - target.otherActions.concat( - this.otherContainers.flatMap(container => container.actions).concat( - this.items.flatMap(item => item.actions) - ) + validActions (target: Creature): Array { + let choices = this.actions.concat( + this.containers.flatMap(container => container.actions) + ).concat( + target.otherActions.concat( + this.otherContainers.flatMap(container => container.actions).concat( + this.items.flatMap(item => item.actions) ) ) + ) - if (this.containedIn !== null) { - choices = choices.concat(this.containedIn.actions) - } - return choices.filter(action => { - return action.allowed(this, target) - }) - } - - validGroupActions (targets: Array): Array { - const choices = this.groupActions - - return choices.filter(action => { - return targets.some(target => action.allowed(this, target)) - }) - } - - destroy (): LogEntry { - return super.destroy() + if (this.containedIn !== null) { + choices = choices.concat(this.containedIn.actions) } + return choices.filter(action => { + return action.allowed(this, target) + }) + } + + validGroupActions (targets: Array): Array { + const choices = this.groupActions + + return choices.filter(action => { + return targets.some(target => action.allowed(this, target)) + }) + } + + destroy (): LogEntry { + return super.destroy() + } } diff --git a/src/game/interface.ts b/src/game/interface.ts index 02b0985..b073df6 100644 --- a/src/game/interface.ts +++ b/src/game/interface.ts @@ -154,7 +154,7 @@ export class PropElem implements LogEntry { cls = StatIcons[this.prop as Stat] } else if (this.prop in Vigor) { cls = VigorIcons[this.prop as Vigor] - } else if (this.prop in Vigor) { + } else if (this.prop in VoreStat) { cls = VoreStatIcons[this.prop as VoreStat] } else { // this shouldn't be possible, given the typing... @@ -184,8 +184,8 @@ export class PropElem implements LogEntry { } if (this.value !== null) { - const numText = Math.round(this.value).toFixed(0) === this.value.toFixed(0) ? this.value.toFixed(0) : this.value.toFixed(1) - span.textContent = numText + ' ' + const numText = Math.round(this.value).toFixed(0) === this.value.toFixed(0) ? Math.abs(this.value).toFixed(0) : Math.abs(this.value).toFixed(1) + span.textContent = (this.value < 0 ? '+' : '') + numText + ' ' } const icon = new FAElem(cls).render()[0] diff --git a/src/game/items.ts b/src/game/items.ts index a3ac3c3..891e4c2 100644 --- a/src/game/items.ts +++ b/src/game/items.ts @@ -1,4 +1,4 @@ -import { TextLike, LiveText, DynText, Word, ImproperNoun } from './language' +import { TextLike, LiveText, DynText, Word, ImproperNoun, Verb } from './language' import { Actionable, Action, DamageFormula, ConstantDamageFormula, Damage, DamageType, Vigor, StatDamageFormula, Stat } from './combat' import { AttackAction } from './combat/actions' @@ -10,8 +10,8 @@ export interface Item extends Actionable { export class Weapon implements Actionable { actions: Array = [] - constructor (public name: Word, public desc: TextLike, damageFormula: DamageFormula) { - const attack = new AttackAction(damageFormula) + constructor (public name: Word, public desc: TextLike, damageFormula: DamageFormula, verb: Verb) { + const attack = new AttackAction(damageFormula, verb) attack.desc = new DynText(`Attack with your `, this.name.all) this.actions.push(attack) } @@ -23,7 +23,8 @@ export const Sword = new Weapon( new StatDamageFormula([ { fraction: 0.35, stat: Stat.Power, target: Vigor.Health, type: DamageType.Slash }, { fraction: 0.25, stat: Stat.Power, target: Vigor.Health, type: DamageType.Pierce } - ]) + ]), + new Verb('slash', 'slashes') ) export const Dagger = new Weapon( @@ -32,7 +33,8 @@ export const Dagger = new Weapon( new StatDamageFormula([ { fraction: 0.50, stat: Stat.Speed, target: Vigor.Health, type: DamageType.Pierce }, { fraction: 0.05, stat: Stat.Speed, target: Vigor.Health, type: DamageType.Slash } - ]) + ]), + new Verb('stab', 'stabs', 'stabbing', 'stabbed') ) export const Wand = new Weapon( @@ -41,7 +43,8 @@ export const Wand = new Weapon( new StatDamageFormula([ { fraction: 0.25, stat: Stat.Charm, target: Vigor.Health, type: DamageType.Crush }, { fraction: 0.25, stat: Stat.Willpower, target: Vigor.Health, type: DamageType.Crush } - ]) + ]), + new Verb('zap', 'zaps', 'zapping', 'zapped') ) export const Mace = new Weapon( @@ -50,5 +53,6 @@ export const Mace = new Weapon( new StatDamageFormula([ { fraction: 0.4, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, { fraction: 0.2, stat: Stat.Power, target: Vigor.Health, type: DamageType.Pierce } - ]) + ]), + new Verb('bash', 'bashes', 'bashing', 'bashed') ) diff --git a/src/game/vore.ts b/src/game/vore.ts index e4df573..5c5aed7 100644 --- a/src/game/vore.ts +++ b/src/game/vore.ts @@ -138,8 +138,8 @@ export abstract class NormalContainer implements Container { export interface VoreContainer extends Container { digested: Array; tick: (dt: number) => LogEntry; - digest: (prey: Vore) => LogEntry; - absorb: (prey: Vore) => LogEntry; + digest: (preys: Vore[]) => LogEntry; + absorb: (preys: Vore[]) => LogEntry; dispose: (preys: Vore[]) => LogEntry; } @@ -172,7 +172,7 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor }) const tickedEntries = new LogLines(...this.contents.map(prey => this.tickLines.run(this.owner, prey, { damage: scaled }))) - const digestedEntries = new LogLines(...justDigested.map(prey => this.digest(prey))) + const digestedEntries = this.digest(justDigested) this.contents = this.contents.filter(prey => { return prey.vigors[Vigor.Health] > 0 @@ -181,16 +181,16 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor return new LogLines(tickedEntries, new LogLines(...damageResults), digestedEntries) } - digest (prey: Vore): LogEntry { - return this.digestLines.run(this.owner, prey) + digest (preys: Vore[]): LogEntry { + return new LogLines(...preys.map(prey => this.digestLines.run(this.owner, prey))) } - absorb (prey: Vore): LogEntry { - return this.absorbLines.run(this.owner, prey) + absorb (preys: Vore[]): LogEntry { + return new LogLines(...preys.map(prey => this.absorbLines.run(this.owner, prey))) } dispose (preys: Vore[]): LogEntry { - return new CompositeLog(...preys.map(prey => this.disposeLines.run(this.owner, prey))) + return new LogLines(...preys.map(prey => this.disposeLines.run(this.owner, prey))) } constructor (name: Noun, owner: Vore, voreTypes: Set, capacity: number, private damage: Damage) { @@ -265,6 +265,25 @@ export class Stomach extends NormalVoreContainer { [[POV.Third, POV.First], (user, target) => new LogLine(`${user.name.capital}'s guts soak you up like water in a sponge`)], [[POV.Third, POV.Third], (user, target) => new LogLine(`${user.name.capital} finishes absorbing the remains of ${target.name}`)] ]) + + digest (preys: Vore[]): LogEntry { + if (preys.length === 0) { + return super.digest(preys) + } + + const heal = new Damage( + { + amount: preys.reduce((total: number, next: Vore) => total + next.maxVigors.Health / 5, 0), + type: DamageType.Heal, + target: Vigor.Health + } + ) + this.owner.takeDamage(heal) + return new LogLines( + super.digest(preys), + new LogLine(`${this.owner.name.capital} heals for `, heal.renderShort()) + ) + } } export class InnerStomach extends InnerContainer {