| @@ -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 | |||
| @@ -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 | |||
| } | |||
| } | |||
| } | |||
| @@ -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<Stat>).reduce((result: Partial<Stats>, 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<VoreContainer> = [] | |||
| otherContainers: Array<Container> = [] | |||
| @@ -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<VoreType>, public predPrefs: Set<VoreType>, private baseMass: number) { | |||
| super(name, kind, pronouns, stats) | |||
| constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats, public preyPrefs: Set<VoreType>, public predPrefs: Set<VoreType>, 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<Vigors> { | |||
| 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 { | |||
| @@ -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<Vigors> { | |||
| 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; | |||
| } | |||
| @@ -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<Consequence>) { | |||
| 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) | |||
| ) | |||
| ] | |||
| ) | |||
| } | |||
| } | |||
| @@ -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") | |||
| } | |||
| ) | |||
| @@ -4,6 +4,8 @@ import App from './App.vue' | |||
| declare global { | |||
| interface Array<T> { | |||
| joinGeneral (item: T, endItem: T|null): Array<T>; | |||
| /* eslint-disable-next-line */ | |||
| unique (predicate: (elem: T) => any): Array<T>; | |||
| } | |||
| } | |||
| @@ -16,6 +18,20 @@ Array.prototype.joinGeneral = function (item, endItem = null) { | |||
| } | |||
| } | |||
| /* eslint-disable-next-line */ | |||
| Array.prototype.unique = function<T> (predicate: (elem: T) => any): Array<T> { | |||
| const set = new Set() | |||
| const result: Array<T> = [] 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({ | |||