| @@ -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] | |||
| @@ -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 () { | |||
| @@ -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<Creature>; | |||
| } | |||
| export class CompositionAction extends Action { | |||
| private consequences: Array<Consequence>; | |||
| private tests: Array<TestBundle>; | |||
| constructor (name: TextLike, desc: TextLike, properties: { conditions?: Array<Condition>; consequences?: Array<Consequence>; tests?: Array<TestBundle> }) { | |||
| 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<Creature, number>() | |||
| 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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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<Creature>, 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()) | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 { | |||
| @@ -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 } | |||
| @@ -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<Creature, { container: Container }> = (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<Creature>): 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) | |||
| ) | |||
| ] | |||
| } | |||
| )) | |||
| } | |||
| } | |||
| @@ -195,7 +195,7 @@ export abstract class NormalContainer implements Container { | |||
| } | |||
| export abstract class InnerContainer extends NormalContainer { | |||
| constructor (name: Noun, owner: Vore, voreTypes: Set<VoreType>, capacity: number, private escape: VoreContainer) { | |||
| constructor (name: Noun, owner: Vore, voreTypes: Set<VoreType>, 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<VoreType>, capacity: number, damage: Damage, private escape: VoreContainer) { | |||
| export abstract class InnerVoreContainer extends NormalVoreContainer { | |||
| constructor (name: Noun, owner: Vore, voreTypes: Set<VoreType>, capacity: number, damage: Damage, private escape: Container) { | |||
| super(name, owner, voreTypes, capacity, damage) | |||
| this.actions = [] | |||