diff --git a/src/game/combat.ts b/src/game/combat.ts index 3dece68..8d739ae 100644 --- a/src/game/combat.ts +++ b/src/game/combat.ts @@ -577,6 +577,13 @@ export class Effective { modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage { return damage } + + /** + * Affects a stat + */ + modStat (creature: Creature, stat: Stat, current: number): number { + return current + } } /** * A displayable status effect diff --git a/src/game/combat/effects.ts b/src/game/combat/effects.ts index 25920ad..8a26913 100644 --- a/src/game/combat/effects.ts +++ b/src/game/combat/effects.ts @@ -1,4 +1,4 @@ -import { StatusEffect, Damage, DamageType, Action, Condition, Vigor } from '../combat' +import { StatusEffect, Damage, DamageType, Action, Condition, Vigor, Stat } from '../combat' import { DynText, LiveText, ToBe, Verb } from '../language' import { Creature } from "../creature" import { LogLine, LogEntry, LogLines, FAElem, nilLog } from '../interface' @@ -194,3 +194,18 @@ export class DigestionPowerEffect extends StatusEffect { return damage.scale(this.factor) } } + +export class StatEffect extends StatusEffect { + constructor (private stat: Stat, private amount: number, private factor: number) { + super('Stat boosted', 'This creature has modified stats', 'fas fa-user-plus') + } + + modStat (creature: Creature, stat: Stat, current: number): number { + console.log(stat, this.stat) + if (stat === this.stat) { + return current * this.factor + this.amount + } else { + return current + } + } +} diff --git a/src/game/creature.ts b/src/game/creature.ts index d53dcbe..10c45a7 100644 --- a/src/game/creature.ts +++ b/src/game/creature.ts @@ -1,14 +1,33 @@ -import { Damage, Combatant, Stats, Action, Vigor, Side, GroupAction, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats } from './combat' +import { Damage, Combatant, Stats, Action, Vigor, Side, GroupAction, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors } from './combat' import { Noun, Pronoun, SoloLine, Verb } from './language' import { LogEntry, LogLines, LogLine } from './interface' import { VoreContainer, VoreType, Container } from './vore' import { Item, EquipmentSlot, Equipment, ItemKind, Currency } from './items' import { PassAction } from './combat/actions' import { AI } from './ai' -import { Mortal } from './entity' +import { Entity, Resistances } from './entity' import { Perk } from './combat/perks' -export class Creature extends Mortal { +export class Creature extends Entity { + 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 = [] otherContainers: Array = [] @@ -41,8 +60,16 @@ export class Creature extends Mortal { equipment: {[key in EquipmentSlot]?: Equipment } = {} ai: AI|null = null - constructor (name: Noun, kind: Noun, pronouns: Pronoun, stats: Stats, public preyPrefs: Set, public predPrefs: Set, private baseMass: number) { - super(name, kind, pronouns, stats) + 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 + }) + + console.log(this) this.actions.push(new PassAction()) this.side = Side.Heroes @@ -96,26 +123,85 @@ export class Creature extends Mortal { } } - applyEffect (effect: StatusEffect): LogEntry { - this.statusEffects.push(effect) - return effect.onApply(this) + 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) } - /** - * Determines how much damage an attack would do - */ effectiveDamage (damage: Damage): Damage { - const preDamage = this.effects.reduce((modifiedDamage: Damage, effect: Effective) => { - return effect.preDamage(this, modifiedDamage) - }, damage) - return super.effectiveDamage(preDamage) + 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() + } } - resistanceTo (damageType: DamageType) { - const base = super.resistanceTo(damageType) + toString (): string { + return this.name.toString() + } - const modified = this.effects.reduce((resist, effect) => effect.modResistance(damageType, resist), base) - return modified + applyEffect (effect: StatusEffect): LogEntry { + this.statusEffects.push(effect) + return effect.onApply(this) } executeAction (action: Action, target: Creature): LogEntry { diff --git a/src/game/entity.ts b/src/game/entity.ts index 4f6f413..42f480a 100644 --- a/src/game/entity.ts +++ b/src/game/entity.ts @@ -35,105 +35,3 @@ export abstract class Entity { } export type Resistances = {[key in DamageType]: number} - -export abstract class Mortal extends Entity { - baseResistances: Resistances - - stats: Stats; - vigors: {[key in Vigor]: number} = { - [Vigor.Health]: 100, - [Vigor.Stamina]: 100, - [Vigor.Resolve]: 100 - } - - destroyed = false; - - constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats) { - super(name, kind, pronouns) - /* eslint-disable-next-line */ - this.stats = Object.keys(Stat).reduce((base: any, key) => { base[key] = baseStats[key as Stat]; return base }, {}) - /* 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 - }) - } - - 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() - } - - abstract destroy (): LogEntry; -} diff --git a/src/game/items.ts b/src/game/items.ts index 9c4ec43..e09ec64 100644 --- a/src/game/items.ts +++ b/src/game/items.ts @@ -1,9 +1,9 @@ import { TextLike, LiveText, DynText, Word, ImproperNoun, Verb, Noun } from './language' -import { Actionable, Action, DamageFormula, ConstantDamageFormula, Damage, DamageType, Vigor, StatDamageFormula, Stat, Effective, CompositionAction, Condition, CompositeDamageFormula } from './combat' +import { Actionable, Action, DamageFormula, ConstantDamageFormula, Damage, DamageType, Vigor, StatDamageFormula, Stat, Effective, CompositionAction, Condition, CompositeDamageFormula, Consequence, StatusEffect } from './combat' import { AttackAction } from './combat/actions' import { Resistances } from './entity' -import { DamageTypeResistanceEffect, DigestionPowerEffect } from './combat/effects' -import { DamageConsequence, LogConsequence, HealingConsequence, StatusConsequence } from './combat/consequences' +import { DamageTypeResistanceEffect, DigestionPowerEffect, SizeEffect, StatEffect } from './combat/effects' +import { DamageConsequence, LogConsequence, HealingConsequence, StatusConsequence, ArbitraryConsequence } from './combat/consequences' import { SoloCondition } from './combat/conditions' import { LogLine, LogEntry } from './interface' import { Creature } from './creature' @@ -192,67 +192,94 @@ export class Consumable extends Item { } } -export class HealthPotion extends Consumable { - constructor () { +export abstract class Potion extends Consumable { + constructor (name: ImproperNoun, desc: string, consequences: Array) { super( new ImproperNoun("health potion"), - "Restores all of your vigors", + desc, new CompositionAction( - "Drink Potion", - "Heals your vigors", + "Drink " + name, + desc, { conditions: [ new SoloCondition() ], - consequences: [ + consequences: ([ new LogConsequence( - (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('drink'))} a potion.`) - ), - new HealingConsequence( - new CompositeDamageFormula([ - new ConstantDamageFormula( - new Damage( - { amount: 100, target: Vigor.Health, type: DamageType.Heal }, - { amount: 100, target: Vigor.Stamina, type: DamageType.Heal }, - { amount: 100, target: Vigor.Resolve, type: DamageType.Heal } - ) - ), - new StatDamageFormula([ - { fraction: 2, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Heal }, - { fraction: 2, stat: Stat.Agility, target: Vigor.Stamina, type: DamageType.Heal }, - { fraction: 2, stat: Stat.Willpower, target: Vigor.Resolve, type: DamageType.Heal } - ]) - ]) + (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('drink'))} a ${name}.`) ) - ] + ] as Consequence[]).concat(consequences) } ) ) } } -export class AcidPotion extends Consumable { +export class HealthPotion extends Potion { constructor () { super( - new ImproperNoun("Acid Potion"), - "Boosts your digestive power and causes a burst of damage to your prey", - new CompositionAction( - "Drink Potion", - "Speed up your digestion", - { - conditions: [ - new SoloCondition() - ], - consequences: [ - new LogConsequence( - (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('drink'))} an acid potion.`) + new ImproperNoun("Health Potion"), + "Heals your vigors", + [ + new HealingConsequence( + new CompositeDamageFormula([ + new ConstantDamageFormula( + new Damage( + { amount: 100, target: Vigor.Health, type: DamageType.Heal }, + { amount: 100, target: Vigor.Stamina, type: DamageType.Heal }, + { amount: 100, target: Vigor.Resolve, type: DamageType.Heal } + ) ), - new StatusConsequence( - (user, target) => new DigestionPowerEffect(2) - ) - ] - } - ) + new StatDamageFormula([ + { fraction: 2, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Heal }, + { fraction: 2, stat: Stat.Agility, target: Vigor.Stamina, type: DamageType.Heal }, + { fraction: 2, stat: Stat.Willpower, target: Vigor.Resolve, type: DamageType.Heal } + ]) + ]) + ) + ] + ) + } +} + +export class StrengthPotion extends Potion { + constructor () { + super( + new ImproperNoun("Strength Potion"), + "Power up!", + [ + new StatusConsequence( + (user, target) => new StatEffect(Stat.Power, 0, 1.5) + ) + ] + ) + } +} + +export class AcidPotion extends Potion { + constructor () { + super( + new ImproperNoun("Acid Potion"), + "Boosts your digestive power", + [ + new StatusConsequence( + (user, target) => new DigestionPowerEffect(2) + ) + ] + ) + } +} + +export class ShrinkPotion extends Potion { + constructor () { + super( + new ImproperNoun("Shrink Potion"), + "Drink me!", + [ + new StatusConsequence( + (user, target) => new SizeEffect(0.5) + ) + ] ) } } diff --git a/src/game/maps/town.ts b/src/game/maps/town.ts index a91646d..331868b 100644 --- a/src/game/maps/town.ts +++ b/src/game/maps/town.ts @@ -251,6 +251,8 @@ export const Town = (): Place => { (world, executor) => { executor.items.push(new Items.HealthPotion()) executor.items.push(new Items.AcidPotion()) + executor.items.push(new Items.ShrinkPotion()) + executor.items.push(new Items.StrengthPotion()) return new LogLine("You grab some potions") } ) diff --git a/src/main.ts b/src/main.ts index 170e710..c7e7d76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,8 @@ import App from './App.vue' declare global { interface Array { joinGeneral (item: T, endItem: T|null): Array; + /* eslint-disable-next-line */ + unique (predicate: (elem: T) => any): Array; } } @@ -16,6 +18,20 @@ Array.prototype.joinGeneral = function (item, endItem = null) { } } +/* eslint-disable-next-line */ +Array.prototype.unique = function (predicate: (elem: T) => any): Array { + const set = new Set() + const result: Array = [] as T[] + this.forEach(elem => { + const predResult = predicate(elem) + if (!set.has(predResult)) { + set.add(predResult) + result.push(elem) + } + }) + return result +} + Vue.config.productionTip = false new Vue({