From 13dce9ef145bec501f62c1d49328e6a957e4ef0a Mon Sep 17 00:00:00 2001 From: Fen Dweller Date: Sat, 1 Aug 2020 13:01:54 -0400 Subject: [PATCH] Add Goldeneye; add composition-based actions --- src/App.vue | 1 + src/components/Combat.vue | 34 ++---- src/game/combat.ts | 97 ++++++++++++++-- src/game/combat/conditions.ts | 10 ++ src/game/combat/consequences.ts | 57 ++++++++++ src/game/combat/effects.ts | 38 ++++++- src/game/creature.ts | 6 +- src/game/creatures.ts | 4 +- src/game/creatures/goldeneye.ts | 190 ++++++++++++++++++++++++++++++++ src/game/vore.ts | 6 +- 10 files changed, 405 insertions(+), 38 deletions(-) create mode 100644 src/game/combat/consequences.ts create mode 100644 src/game/creatures/goldeneye.ts diff --git a/src/App.vue b/src/App.vue index 60f2255..aedc56c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -53,6 +53,7 @@ export default class App extends Vue { this.$data.encounters.push(new Encounter({ name: 'Dragon' }, this.makeParty().concat([new Creatures.Dragon()]))) this.$data.encounters.push(new Encounter({ name: 'Wolves' }, this.makeParty().concat([new Creatures.Wolf(), new Creatures.Wolf(), new Creatures.Wolf(), new Creatures.Wolf()]))) this.$data.encounters.push(new Encounter({ name: 'Large Wah' }, this.makeParty().concat([new Creatures.Shingo()]))) + this.$data.encounters.push(new Encounter({ name: 'Goldeneye' }, this.makeParty().concat([new Creatures.Goldeneye()]))) this.$data.encounter = this.$data.encounters[0] diff --git a/src/components/Combat.vue b/src/components/Combat.vue index 6b8d678..7758c0c 100644 --- a/src/components/Combat.vue +++ b/src/components/Combat.vue @@ -91,33 +91,24 @@ export default class Combat extends Vue { @Emit("executedLeft") executedLeft (entry: LogEntry) { - const log = this.$el.querySelector(".log") - if (log !== null) { - const before = log.querySelector("div.log-entry") - const holder = document.createElement("div") - holder.classList.add("log-entry") + this.writeLog(entry, "left-move") - entry.render().forEach(element => { - holder.appendChild(element) - }) + this.writeLog(this.encounter.nextMove(), "left-move") + this.pickNext() + } - holder.classList.add("left-move") - const hline = document.createElement("div") - hline.classList.add("log-separator") - log.insertBefore(hline, before) - log.insertBefore(holder, hline) + // TODO these need to render on the correct side - log.scrollTo({ top: 0, left: 0 }) - } + @Emit("executedRight") + executedRight (entry: LogEntry) { + this.writeLog(entry, "right-move") - this.encounter.nextMove() + this.writeLog(this.encounter.nextMove(), "right-move") this.pickNext() } - @Emit("executedRight") - executedRight (entry: LogEntry) { + writeLog (entry: LogEntry, cls: string) { const log = this.$el.querySelector(".log") - if (log !== null) { const before = log.querySelector("div.log-entry") const holder = document.createElement("div") @@ -127,7 +118,7 @@ export default class Combat extends Vue { holder.appendChild(element) }) - holder.classList.add("right-move") + holder.classList.add(cls) const hline = document.createElement("div") hline.classList.add("log-separator") log.insertBefore(hline, before) @@ -135,9 +126,6 @@ export default class Combat extends Vue { log.scrollTo({ top: 0, left: 0 }) } - - this.encounter.nextMove() - this.pickNext() } pickNext () { diff --git a/src/game/combat.ts b/src/game/combat.ts index 41e46fd..9b1a908 100644 --- a/src/game/combat.ts +++ b/src/game/combat.ts @@ -1,5 +1,5 @@ import { Creature } from "./creature" -import { TextLike, DynText, ToBe, LiveText } from './language' +import { TextLike, DynText, ToBe, LiveText, PairLineArgs, PairLine } from './language' import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog } from './interface' export enum DamageType { @@ -277,7 +277,7 @@ export class FractionDamageFormula implements DamageFormula { } } else if (factor.target in Vigor) { return { - amount: Math.max(factor.fraction * user.vigors[factor.target as Vigor]), + amount: Math.max(factor.fraction * target.vigors[factor.target as Vigor]), target: factor.target, type: factor.type } @@ -343,6 +343,32 @@ export abstract class Action { abstract describe (user: Creature, target: Creature): LogEntry } +export type TestBundle = { + test: CombatTest; + fail: PairLine; +} + +export class CompositionAction extends Action { + private consequences: Array; + private tests: Array; + + constructor (name: TextLike, desc: TextLike, properties: { conditions?: Array; consequences?: Array; tests?: Array }) { + super(name, desc, properties.conditions ?? []) + this.consequences = properties.consequences ?? [] + this.tests = properties.tests ?? [] + } + + execute (user: Creature, target: Creature): LogEntry { + return new LogLines( + ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target)) + ) + } + + describe (user: Creature, target: Creature): LogEntry { + return new LogLine(`No descriptions yet...`) + } +} + /** * A Condition describes whether or not something is permissible between two [[Creature]]s */ @@ -376,10 +402,19 @@ export abstract class GroupAction extends Action { * Some hooks return results along with a log entry. */ export class Effective { + /** + * Executes when the effect is initially applied + */ onApply (creature: Creature): LogEntry { return nilLog } + /** + * Executes when the effect is removed + */ onRemove (creature: Creature): LogEntry { return nilLog } + /** + * Executes before the creature tries to perform an action + */ preAction (creature: Creature): { prevented: boolean; log: LogEntry } { return { prevented: false, @@ -387,16 +422,42 @@ export class Effective { } } + /** + * Executes before another creature tries to perform an action that targets this creature + */ + preReceiveAction (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { + return { + prevented: false, + log: nilLog + } + } + + /** + * Executes before the creature receives damage (or healing) + */ preDamage (creature: Creature, damage: Damage): Damage { return damage } + /** + * Executes before the creature is attacked + */ preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { return { prevented: false, log: nilLog } } + + /** + * Executes when a creature's turn starts + */ + preTurn (creature: Creature): { prevented: boolean; log: LogEntry } { + return { + prevented: false, + log: nilLog + } + } } /** * A displayable status effect @@ -454,14 +515,14 @@ export class Encounter { this.nextMove() } - nextMove (): void { + nextMove (): LogEntry { this.initiatives.set(this.currentMove, 0) const times = new Map() this.combatants.forEach(combatant => { // this should never be undefined const currentProgress = this.initiatives.get(combatant) ?? 0 - const remaining = (this.turnTime - currentProgress) / Math.max(combatant.stats.Speed, 1) + const remaining = (this.turnTime - currentProgress) / Math.sqrt(Math.max(combatant.stats.Speed, 1)) times.set(combatant, remaining) }) @@ -471,18 +532,40 @@ export class Encounter { return closestTime <= nextTime ? closest : next }, this.combatants[0]) - const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.max(this.currentMove.stats.Speed, 1) + const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.sqrt(Math.max(this.currentMove.stats.Speed, 1)) this.combatants.forEach(combatant => { // still not undefined... const currentProgress = this.initiatives.get(combatant) ?? 0 - this.initiatives.set(combatant, currentProgress + closestRemaining * Math.max(combatant.stats.Speed, 1)) + this.initiatives.set(combatant, currentProgress + closestRemaining * Math.sqrt(Math.max(combatant.stats.Speed, 1))) }) // TODO: still let the creature use drained-vigor moves if (this.currentMove.disabled) { - this.nextMove() + return this.nextMove() + } else { + const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented) + + if (effectResults.some(result => result.prevented)) { + return new LogLines( + ...effectResults.map(result => result.log).concat([this.nextMove()]) + ) + } } + + return nilLog } } + +export abstract class Consequence { + constructor (public conditions: Condition[]) { + + } + + applicable (user: Creature, target: Creature): boolean { + return this.conditions.every(cond => cond.allowed(user, target)) + } + + abstract apply (user: Creature, target: Creature): LogEntry +} diff --git a/src/game/combat/conditions.ts b/src/game/combat/conditions.ts index cf92b9c..fcfd1eb 100644 --- a/src/game/combat/conditions.ts +++ b/src/game/combat/conditions.ts @@ -87,3 +87,13 @@ export class EnemyCondition implements Condition { return user.side !== target.side } } + +export class ContainerFullCondition implements Condition { + constructor (private container: Container) { + + } + + allowed (user: Creature, target: Creature): boolean { + return this.container.contents.length > 0 + } +} diff --git a/src/game/combat/consequences.ts b/src/game/combat/consequences.ts new file mode 100644 index 0000000..168e333 --- /dev/null +++ b/src/game/combat/consequences.ts @@ -0,0 +1,57 @@ +import { Consequence, DamageFormula, Condition, StatusEffect } from '../combat' +import { Creature } from '../creature' +import { LogEntry, LogLines, LogLine } from '../interface' +import { Verb, PairLine } from '../language' + +/** + * Takes a function, and thus can do anything. + */ +export class ArbitraryConsequence extends Consequence { + constructor (public apply: (user: Creature, target: Creature) => LogEntry, conditions: Condition[] = []) { + super(conditions) + } +} + +/** + * Renders some text. + */ + +export class LogConsequence extends Consequence { + constructor (private line: PairLine, conditions: Condition[] = []) { + super(conditions) + } + + apply (user: Creature, target: Creature): LogEntry { + return this.line(user, target) + } +} +/** + * Deals damage. + */ +export class DamageConsequence extends Consequence { + constructor (private damageFormula: DamageFormula, conditions: Condition[] = []) { + super(conditions) + } + + apply (user: Creature, target: Creature): LogEntry { + const damage = this.damageFormula.calc(user, target) + return new LogLines( + new LogLine(`${target.name.capital} ${target.name.conjugate(new Verb('take'))} `, damage.renderShort(), ` damage!`), + target.takeDamage(damage) + ) + } +} + +/** + * Applies a status effect + */ + +export class StatusConsequence extends Consequence { + constructor (private statusMaker: () => StatusEffect, conditions: Condition[] = []) { + super(conditions) + } + + apply (user: Creature, target: Creature): LogEntry { + return target.applyEffect(this.statusMaker()) + } +} diff --git a/src/game/combat/effects.ts b/src/game/combat/effects.ts index 9c52c6a..6d7094c 100644 --- a/src/game/combat/effects.ts +++ b/src/game/combat/effects.ts @@ -1,4 +1,4 @@ -import { StatusEffect, Damage, DamageType, Action } from '../combat' +import { StatusEffect, Damage, DamageType, Action, Condition } from '../combat' import { DynText, LiveText, ToBe, Verb } from '../language' import { Creature } from "../creature" import { LogLine, LogEntry, LogLines, FAElem, nilLog } from '../interface' @@ -38,7 +38,7 @@ export class StunEffect extends StatusEffect { return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} no longer stunned.`) } - preAction (creature: Creature): { prevented: boolean; log: LogEntry } { + preTurn (creature: Creature): { prevented: boolean; log: LogEntry } { if (--this.duration <= 0) { return { prevented: true, @@ -96,3 +96,37 @@ export class PredatorCounterEffect extends StatusEffect { } } } + +export class UntouchableEffect extends StatusEffect { + constructor () { + super('Untouchable', 'Cannot be attacked', 'fas fa-times') + } + + preAttack (creature: Creature, attacker: Creature) { + return { + prevented: true, + log: new LogLine(`${creature.name.capital} cannot be attacked.`) + } + } +} + +export class DazzlingEffect extends StatusEffect { + constructor (private conditions: Condition[]) { + super('Dazzling', 'Stuns enemies who try to affect this creature', 'fas fa-spinner') + } + + preReceiveAction (creature: Creature, attacker: Creature) { + if (this.conditions.every(cond => cond.allowed(creature, attacker))) { + attacker.applyEffect(new StunEffect(1)) + return { + prevented: true, + log: new LogLine(`${attacker.name.capital} can't act against ${creature.name.objective}!`) + } + } else { + return { + prevented: false, + log: nilLog + } + } + } +} diff --git a/src/game/creature.ts b/src/game/creature.ts index 6df8b2e..ce71c3f 100644 --- a/src/game/creature.ts +++ b/src/game/creature.ts @@ -38,8 +38,10 @@ export class Creature extends Vore implements Combatant { } executeAction (action: Action, target: Creature): LogEntry { - const effectResults = this.effects.map(effect => effect.preAction(this)) - const blocking = effectResults.filter(result => result.prevented) + const preActionResults = this.effects.map(effect => effect.preAction(this)) + const preReceiveActionResults = target.effects.map(effect => effect.preReceiveAction(target, this)) + + const blocking = preActionResults.concat(preReceiveActionResults).filter(result => result.prevented) if (blocking.length > 0) { return new LogLines(...blocking.map(result => result.log)) } else { diff --git a/src/game/creatures.ts b/src/game/creatures.ts index e744393..91939dd 100644 --- a/src/game/creatures.ts +++ b/src/game/creatures.ts @@ -6,4 +6,6 @@ import { Withers } from './creatures/withers' import { Kenzie } from './creatures/kenzie' import { Dragon } from './creatures/dragon' import { Shingo } from './creatures/shingo' -export { Wolf, Player, Cafat, Human, Withers, Kenzie, Dragon, Shingo } +import { Goldeneye } from './creatures/goldeneye' + +export { Wolf, Player, Cafat, Human, Withers, Kenzie, Dragon, Shingo, Goldeneye } diff --git a/src/game/creatures/goldeneye.ts b/src/game/creatures/goldeneye.ts new file mode 100644 index 0000000..6ba56d6 --- /dev/null +++ b/src/game/creatures/goldeneye.ts @@ -0,0 +1,190 @@ +import { Creature } from "../creature" +import { Damage, DamageType, ConstantDamageFormula, Vigor, Side, GroupAction, FractionDamageFormula, DamageFormula, UniformRandomDamageFormula, CompositionAction, StatusEffect } from '../combat' +import { MalePronouns, ImproperNoun, Verb, ProperNoun, ToBe, SoloLineArgs } from '../language' +import { VoreType, NormalContainer, Vore, InnerVoreContainer, Container } from '../vore' +import { TransferAction } from '../combat/actions' +import { LogEntry, LogLine, LogLines } from '../interface' +import { ContainerFullCondition, CapableCondition, EnemyCondition, TogetherCondition } from '../combat/conditions' +import { UntouchableEffect, DazzlingEffect, StunEffect } from '../combat/effects' +import { DamageConsequence, StatusConsequence, LogConsequence } from '../combat/consequences' + +class GoldeneyeCrop extends NormalContainer { + consumeVerb: Verb = new Verb('swallow') + releaseVerb: Verb = new Verb('free') + struggleVerb: Verb = new Verb('struggle', 'struggles', 'struggling', 'struggled') + + constructor (owner: Vore) { + super( + new ImproperNoun('crop').all, + owner, + new Set([VoreType.Oral]), + 300 + ) + } +} + +class Taunt extends GroupAction { + damage: DamageFormula = new UniformRandomDamageFormula( + new Damage( + { amount: 50, target: Vigor.Resolve, type: DamageType.Dominance } + ), + 0.5 + ) + + constructor () { + super( + "Taunt", + "Demoralize your enemies", + [ + new EnemyCondition(), + new CapableCondition() + ] + ) + } + + describeGroup (user: Creature, targets: Creature[]): LogEntry { + return new LogLine(`Demoralize your foes`) + } + + execute (user: Creature, target: Creature): LogEntry { + return target.takeDamage(this.damage.calc(user, target)) + } + + describe (user: Creature, target: Creature): LogEntry { + return new LogLine(`Demoralize your foes`) + } +} +class Flaunt extends GroupAction { + constructor (public container: Container) { + super( + "Flaunt " + container.name, + "Show off your " + container.name, + [new ContainerFullCondition(container), new CapableCondition(), new EnemyCondition(), new TogetherCondition()] + ) + } + + groupLine: SoloLineArgs = (user, args) => new LogLine( + `${user.name.capital} ${user.name.conjugate(new Verb('show'))} off ${user.pronouns.possessive} squirming ${args.container.name}, ${user.pronouns.possessive} doomed prey writhing beneath ${user.pronouns.possessive} pelt.` + ) + + describeGroup (user: Creature, targets: Creature[]): LogEntry { + return new LogLine(`Flaunt your bulging ${this.container.name} for all your foes to see`) + } + + execute (user: Creature, target: Creature): LogEntry { + const fracDamage = new FractionDamageFormula([ + { fraction: 0.25, target: Vigor.Resolve, type: DamageType.Dominance } + ]) + const flatDamage = new ConstantDamageFormula( + new Damage( + { amount: 50, target: Vigor.Resolve, type: DamageType.Dominance } + ) + ) + + const damage = fracDamage.calc(user, target).combine(flatDamage.calc(user, target)) + + return new LogLines( + new LogLine(`${target.name.capital} ${target.name.conjugate(new ToBe())} shaken for `, damage.renderShort(), '.'), + target.takeDamage(damage) + ) + } + + executeGroup (user: Creature, targets: Array): LogEntry { + return new LogLines(...[this.groupLine(user, { container: this.container })].concat(targets.map(target => this.execute(user, target)))) + } + + describe (user: Creature, target: Creature): LogEntry { + return new LogLine(`Flaunt your bulging gut for all your foes to see`) + } +} + +class GoldeneyeStomach extends InnerVoreContainer { + consumeVerb: Verb = new Verb('swallow') + releaseVerb: Verb = new Verb('free') + struggleVerb: Verb = new Verb('struggle', 'struggles', 'struggling', 'struggled') + + constructor (owner: Vore, crop: GoldeneyeCrop) { + super( + new ImproperNoun('stomach').all, + owner, + new Set([VoreType.Oral]), + 900, + new Damage( + { amount: 1000, target: Vigor.Health, type: DamageType.Acid } + ), + crop + ) + } +} + +export class Goldeneye extends Creature { + constructor () { + super( + new ProperNoun("Goldeneye"), + new ImproperNoun('gryphon', 'gryphons'), + MalePronouns, + { Toughness: 200, Power: 200, Speed: 200, Willpower: 200, Charm: 200 }, + new Set(), + new Set([VoreType.Oral]), + 2000 + ) + + this.title = "Not really a gryphon" + this.desc = "Not really survivable, either." + + this.side = Side.Monsters + this.applyEffect(new DazzlingEffect([ + new EnemyCondition(), + new TogetherCondition() + ])) + + const crop = new GoldeneyeCrop(this) + const stomach = new GoldeneyeStomach(this, crop) + + this.containers.push(stomach) + this.otherContainers.push(crop) + + this.actions.push( + new TransferAction( + crop, + stomach + ) + ) + + this.groupActions.push(new Flaunt(stomach)) + this.groupActions.push(new Taunt()) + + this.actions.push(new CompositionAction( + "Stomp", + "Big step", + { + conditions: [ + new TogetherCondition(), + new EnemyCondition() + ], + consequences: [ + new LogConsequence( + (user, target) => new LogLine( + `${user.name.capital} ${user.name.conjugate(new Verb('stomp'))} on ${target.name.objective} with crushing force!` + ) + ), + new DamageConsequence( + new FractionDamageFormula([ + { fraction: 0.75, target: Vigor.Health, type: DamageType.Pure } + ]) + ), + new DamageConsequence( + new ConstantDamageFormula( + new Damage( + { amount: 50, target: Vigor.Health, type: DamageType.Crush } + ) + ) + ), + new StatusConsequence( + () => new StunEffect(3) + ) + ] + } + )) + } +} diff --git a/src/game/vore.ts b/src/game/vore.ts index 391565a..3c19310 100644 --- a/src/game/vore.ts +++ b/src/game/vore.ts @@ -195,7 +195,7 @@ export abstract class NormalContainer implements Container { } export abstract class InnerContainer extends NormalContainer { - constructor (name: Noun, owner: Vore, voreTypes: Set, capacity: number, private escape: VoreContainer) { + constructor (name: Noun, owner: Vore, voreTypes: Set, capacity: number, private escape: Container) { super(name, owner, voreTypes, capacity) this.actions = [] @@ -276,8 +276,8 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor } } -abstract class InnerVoreContainer extends NormalVoreContainer { - constructor (name: Noun, owner: Vore, voreTypes: Set, capacity: number, damage: Damage, private escape: VoreContainer) { +export abstract class InnerVoreContainer extends NormalVoreContainer { + constructor (name: Noun, owner: Vore, voreTypes: Set, capacity: number, damage: Damage, private escape: Container) { super(name, owner, voreTypes, capacity, damage) this.actions = []