| @@ -1,19 +1,32 @@ | |||
| import { Creature } from '@/game/creature' | |||
| import { Encounter, Action } from '@/game/combat' | |||
| import { LogEntry } from '@/game/interface' | |||
| import { PassAction } from '@/game/combat/actions' | |||
| import { NoPassDecider, NoReleaseDecider, ChanceDecider, NoSurrenderDecider, FavorRubDecider } from '@/game/ai/deciders' | |||
| import { Encounter, Action, CompositionAction, Consequence } from '@/game/combat' | |||
| import { LogEntry, LogLine, nilLog } from '@/game/interface' | |||
| import { PassAction, ReleaseAction, RubAction } from '@/game/combat/actions' | |||
| import { VoreRelay } from '@/game/events' | |||
| import { StatusConsequence } from '@/game/combat/consequences' | |||
| import { SurrenderEffect } from '@/game/combat/effects' | |||
| import { ToBe, Verb } from '@/game/language' | |||
| /** | |||
| * A Decider determines how favorable an action is to perform. | |||
| */ | |||
| export interface Decider { | |||
| decide: (encounter: Encounter, user: Creature, target: Creature, action: Action) => number; | |||
| export abstract class Decider { | |||
| // eslint-disable-next-line @typescript-eslint/no-empty-function | |||
| attach (ai: AI): void { } ; | |||
| abstract decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number; | |||
| } | |||
| export class AI { | |||
| constructor (public deciders: Decider[]) { | |||
| voreRelay = new VoreRelay() | |||
| constructor (public deciders: Decider[], owner: Creature) { | |||
| this.voreRelay.connect(owner.voreRelay) | |||
| deciders.forEach(decider => decider.attach(this)) | |||
| } | |||
| addDecider (decider: Decider): void { | |||
| this.deciders.push(decider) | |||
| decider.attach(this) | |||
| } | |||
| decide (actor: Creature, encounter: Encounter): LogEntry { | |||
| @@ -57,11 +70,143 @@ export class AI { | |||
| } | |||
| } | |||
| /** | |||
| * Specifically avoids using a [[PassAction]] | |||
| */ | |||
| export class NoPassDecider extends Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof PassAction) { | |||
| return 0 | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Specifically avoids using a [[ReleaseAction]] | |||
| */ | |||
| export class NoReleaseDecider extends Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof ReleaseAction) { | |||
| return 0 | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Weights actions based on how likely they are to succeed | |||
| */ | |||
| export class ChanceDecider extends Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| return action.odds(user, target) | |||
| } | |||
| } | |||
| /** | |||
| * Adjusts the weights for [[CompositionAction]]s that contain the specified consequence | |||
| */ | |||
| export class ConsequenceDecider<T extends Consequence> extends Decider { | |||
| /* eslint-disable-next-line */ | |||
| constructor (private consequenceType: new (...args: any) => T, private weight: number) { | |||
| super() | |||
| } | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof CompositionAction) { | |||
| if (action.consequences.some( | |||
| consequence => consequence instanceof this.consequenceType | |||
| )) { | |||
| return this.weight | |||
| } else { | |||
| return 1 | |||
| } | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Adjusts the weights for [[CompositionAction]]s, using the provided function to make the choice. | |||
| */ | |||
| export class ConsequenceFunctionDecider extends Decider { | |||
| constructor (private func: (encounter: Encounter, user: Creature, target: Creature, action: CompositionAction) => number) { | |||
| super() | |||
| } | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof CompositionAction) { | |||
| return this.func(encounter, user, target, action) | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| export class NoSurrenderDecider extends ConsequenceFunctionDecider { | |||
| constructor () { | |||
| super( | |||
| (encounter, user, target, action) => { | |||
| return action.consequences.some( | |||
| consequence => { | |||
| if (consequence instanceof StatusConsequence) { | |||
| return (consequence.statusMaker(user, target) instanceof SurrenderEffect) | |||
| } | |||
| } | |||
| ) ? 0 : 1 | |||
| } | |||
| ) | |||
| } | |||
| } | |||
| /** | |||
| * Favors [[RubAction]]s | |||
| */ | |||
| export class FavorRubDecider extends Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action) { | |||
| if (action instanceof RubAction) { | |||
| return 5 | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Favors creatures that have escaped this creature | |||
| */ | |||
| export class FavorEscapedPrey extends Decider { | |||
| private memory: Set<Creature> = new Set() | |||
| attach (ai: AI) { | |||
| ai.voreRelay.subscribe( | |||
| "onReleased", | |||
| (sender, args) => { | |||
| if (this.memory.has(args.prey)) { | |||
| return nilLog | |||
| } else { | |||
| this.memory.add(args.prey) | |||
| return new LogLine( | |||
| `${sender.owner.name} ${sender.owner.name.conjugate(new Verb("crave"))} ${args.prey.name.possessive} flavor, and won't be giving ${args.prey.pronouns.objective} up so easily...` | |||
| ) | |||
| } | |||
| } | |||
| ) | |||
| } | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action) { | |||
| return this.memory.has(target) ? 5 : 1 | |||
| } | |||
| } | |||
| /** | |||
| * The VoreAI tries to eat opponents, but only if the odds are good enough | |||
| */ | |||
| export class VoreAI extends AI { | |||
| constructor () { | |||
| constructor (owner: Creature) { | |||
| super( | |||
| [ | |||
| new NoReleaseDecider(), | |||
| @@ -69,7 +214,8 @@ export class VoreAI extends AI { | |||
| new NoPassDecider(), | |||
| new ChanceDecider(), | |||
| new FavorRubDecider() | |||
| ] | |||
| ], | |||
| owner | |||
| ) | |||
| } | |||
| } | |||
| @@ -1,111 +0,0 @@ | |||
| import { Decider } from '@/game/ai' | |||
| import { Encounter, Action, Consequence, CompositionAction } from '@/game/combat' | |||
| import { Creature } from '@/game/creature' | |||
| import { PassAction, ReleaseAction, RubAction } from '@/game/combat/actions' | |||
| import { StatusConsequence } from '@/game/combat/consequences' | |||
| import { SurrenderEffect } from '@/game/combat/effects' | |||
| /** | |||
| * Specifically avoids using a [[PassAction]] | |||
| */ | |||
| export class NoPassDecider implements Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof PassAction) { | |||
| return 0 | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Specifically avoids using a [[ReleaseAction]] | |||
| */ | |||
| export class NoReleaseDecider implements Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof ReleaseAction) { | |||
| return 0 | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Weights actions based on how likely they are to succeed | |||
| */ | |||
| export class ChanceDecider implements Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| return action.odds(user, target) | |||
| } | |||
| } | |||
| /** | |||
| * Adjusts the weights for [[CompositionAction]]s that contain the specified consequence | |||
| */ | |||
| export class ConsequenceDecider<T extends Consequence> implements Decider { | |||
| /* eslint-disable-next-line */ | |||
| constructor (private consequenceType: new (...args: any) => T, private weight: number) { | |||
| } | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof CompositionAction) { | |||
| if (action.consequences.some( | |||
| consequence => consequence instanceof this.consequenceType | |||
| )) { | |||
| return this.weight | |||
| } else { | |||
| return 1 | |||
| } | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Adjusts the weights for [[CompositionAction]]s, using the provided function to make the choice. | |||
| */ | |||
| export class ConsequenceFunctionDecider implements Decider { | |||
| constructor (private func: (encounter: Encounter, user: Creature, target: Creature, action: CompositionAction) => number) { | |||
| } | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { | |||
| if (action instanceof CompositionAction) { | |||
| return this.func(encounter, user, target, action) | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| export class NoSurrenderDecider extends ConsequenceFunctionDecider { | |||
| constructor () { | |||
| super( | |||
| (encounter, user, target, action) => { | |||
| return action.consequences.some( | |||
| consequence => { | |||
| if (consequence instanceof StatusConsequence) { | |||
| return (consequence.statusMaker(user, target) instanceof SurrenderEffect) | |||
| } | |||
| } | |||
| ) ? 0 : 1 | |||
| } | |||
| ) | |||
| } | |||
| } | |||
| /** | |||
| * Favors [[RubAction]]s | |||
| */ | |||
| export class FavorRubDecider implements Decider { | |||
| decide (encounter: Encounter, user: Creature, target: Creature, action: Action) { | |||
| if (action instanceof RubAction) { | |||
| return 5 | |||
| } else { | |||
| return 1 | |||
| } | |||
| } | |||
| } | |||
| @@ -195,7 +195,7 @@ export class StruggleAction extends Action { | |||
| new CompositionTest( | |||
| [ | |||
| new OpposedStatScorer( | |||
| { Power: 1, Agility: 1, Bulk: 0.05 }, | |||
| { Power: 100, Agility: 1, Bulk: 0.05 }, | |||
| { Toughness: 1, Reflexes: 1, Mass: 0.05 } | |||
| ) | |||
| ], | |||
| @@ -220,7 +220,7 @@ export class StruggleAction extends Action { | |||
| } | |||
| protected successLine: PairLineArgs<Entity, { container: Container }> = (prey, pred, args) => new LogLine( | |||
| `${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))} from ${pred.name.possessive} ${args.container.name}.` | |||
| `${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))}!` | |||
| ) | |||
| } | |||
| @@ -7,8 +7,11 @@ import { PassAction } from '@/game/combat/actions' | |||
| import { AI } from '@/game/ai' | |||
| import { Entity, Resistances } from '@/game/entity' | |||
| import { Perk } from '@/game/combat/perks' | |||
| import { VoreRelay } from '@/game/events' | |||
| export class Creature extends Entity { | |||
| voreRelay: VoreRelay = new VoreRelay() | |||
| baseResistances: Resistances | |||
| stats: Stats = (Object.keys(Stat) as Array<Stat>).reduce((result: Partial<Stats>, stat: Stat) => { | |||
| @@ -202,6 +205,19 @@ export class Creature extends Entity { | |||
| return effect.onApply(this) | |||
| } | |||
| addVoreContainer (container: VoreContainer): void { | |||
| this.containers.push(container) | |||
| this.voreRelay.connect(container.voreRelay) | |||
| } | |||
| addOtherContainer (container: Container): void { | |||
| this.otherContainers.push(container) | |||
| } | |||
| addPerk (perk: Perk): void { | |||
| this.perks.push(perk) | |||
| } | |||
| executeAction (action: Action, target: Creature): LogEntry { | |||
| const preActionResults = this.effects.map(effect => effect.preAction(this)) | |||
| const preReceiveActionResults = target.effects.map(effect => effect.preReceiveAction(target, this)) | |||
| @@ -1,15 +1,4 @@ | |||
| import { Wolf, DireWolf } from '@/game/creatures/wolves' | |||
| import { Player } from '@/game/creatures/player' | |||
| import { Cafat } from '@/game/creatures/cafat' | |||
| import { Human } from '@/game/creatures/human' | |||
| import { Withers } from '@/game/creatures/withers' | |||
| import { Kenzie } from '@/game/creatures/kenzie' | |||
| import { Dragon } from '@/game/creatures/dragon' | |||
| import { Shingo } from '@/game/creatures/shingo' | |||
| import { Goldeneye } from '@/game/creatures/goldeneye' | |||
| import { Kuro } from '@/game/creatures/kuro' | |||
| import { Geta } from '@/game/creatures/geta' | |||
| import { Werewolf } from '@/game/creatures/werewolf' | |||
| import { Taluthus } from '@/game/creatures/taluthus' | |||
| export { Wolf, DireWolf, Player, Cafat, Human, Withers, Kenzie, Dragon, Shingo, Goldeneye, Kuro, Geta, Werewolf, Taluthus } | |||
| import Human from '@/game/creatures/human' | |||
| import Player from '@/game/creatures/player' | |||
| import Inazuma from '@/game/creatures/characters/inazuma' | |||
| export { Human, Player, Inazuma } | |||
| @@ -1,128 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Stat, Damage, DamageType, Vigor, ConstantDamageFormula, Side, Action } from '@/game/combat' | |||
| import { ProperNoun, TheyPronouns, ImproperNoun, FemalePronouns, Verb, PairLineArgs } from '@/game/language' | |||
| import { VoreType, Stomach, InnerStomach, VoreContainer } from '@/game/vore' | |||
| import { LogLine, LogLines, LogEntry, FAElem, ImgElem } from '@/game/interface' | |||
| import { AttackAction, TransferAction, FeedAction } from '@/game/combat/actions' | |||
| import { InstantKillEffect } from '@/game/combat/effects' | |||
| import * as Words from '@/game/words' | |||
| import { ContainsCondition } from '@/game/combat/conditions' | |||
| class BellyCrushAction extends AttackAction { | |||
| constructor (_damage: Damage) { | |||
| super({ | |||
| calc (user) { return _damage.scale(user.voreStats.Bulk / 25) }, | |||
| describe (user) { return new LogLine('Deal ', _damage.scale(user.voreStats.Bulk / 25).renderShort(), ` with your ${user.voreStats.Bulk} `, new FAElem('fas fa-weight-hanging')) }, | |||
| explain (user) { return new LogLine('Deal ', _damage.scale(user.voreStats.Bulk / 25).renderShort(), ` with your ${user.voreStats.Bulk} `, new FAElem('fas fa-weight-hanging')) } | |||
| }) | |||
| this.name = 'Belly Crush' | |||
| this.desc = 'Use your weight!' | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Crush ${target.name} under your gut. `, this.damage.describe(user, target)) | |||
| } | |||
| successLine: PairLineArgs<Creature, { damage: Damage }> = (user, target, args) => new LogLines(new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb('crush', 'crushes'))} on ${target.name.objective} with ${user.pronouns.possessive} belly for `, | |||
| target.effectiveDamage(args.damage).renderShort() | |||
| ), new ImgElem('./media/cafat/images/belly-crush.webp')) | |||
| } | |||
| class BelchAction extends AttackAction { | |||
| constructor (damage: Damage) { | |||
| super(new ConstantDamageFormula(damage)) | |||
| this.name = 'Belch' | |||
| this.desc = 'Drain your foe\'s stats with a solid BELCH' | |||
| } | |||
| successLine: PairLineArgs<Creature, { damage: Damage }> = (user, target, args) => new LogLines( | |||
| new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb('belch', 'belches'))} on ${target.name.objective} for `, | |||
| target.effectiveDamage(args.damage).renderShort() | |||
| ), | |||
| new ImgElem('./media/cafat/images/belch.webp') | |||
| ) | |||
| } | |||
| class CrushAction extends Action { | |||
| constructor (private container: VoreContainer) { | |||
| super( | |||
| "Crush", | |||
| "Crush 'em!", | |||
| [ | |||
| new ContainsCondition(container) | |||
| ] | |||
| ) | |||
| this.desc = "Crush somebody in your gut" | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Crush ${target.name} in your ${this.container.name} for massive, unavoidable damage.`) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines(this.line(user, target, { container: this.container }), target.applyEffect(new InstantKillEffect())) | |||
| } | |||
| line: PairLineArgs<Creature, { container: VoreContainer }> = (user, target, args) => new LogLine( | |||
| `${user.name.capital.possessive} ${args.container.name} ${Words.Brutally} ${user.name.conjugate(new Verb('crush', 'crushes'))} ${target.name.objective}; ${user.pronouns.subjective} ${user.pronouns.conjugate(new Verb('belch', 'belches'))} as ${user.pronouns.possessive} gut lets out a fatal CRUNCH `, | |||
| new ImgElem('./media/cafat/images/crunch.webp') | |||
| ) | |||
| } | |||
| export class Cafat extends Creature { | |||
| constructor () { | |||
| super(new ProperNoun('Cafat'), new ImproperNoun('taur', 'taurs'), [TheyPronouns, FemalePronouns][Math.floor(Math.random() * 2)], { | |||
| [Stat.Toughness]: 30, | |||
| [Stat.Power]: 30, | |||
| [Stat.Agility]: 15, | |||
| [Stat.Reflexes]: 15, | |||
| [Stat.Willpower]: 25, | |||
| [Stat.Charm]: 20 | |||
| }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 150) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 1, new ConstantDamageFormula(new Damage( | |||
| { amount: 20, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 10, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 10, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| stomach.name = new ImproperNoun("upper stomach", "upper stomachs").all | |||
| this.containers.push(stomach) | |||
| const lowerStomach = new InnerStomach(this, 1.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 40, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 20, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 20, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| )), stomach) | |||
| lowerStomach.name = new ImproperNoun("lower stomach", "lower stomachs").all | |||
| const crush = new CrushAction(lowerStomach) | |||
| lowerStomach.actions.push(crush) | |||
| this.containers.push(lowerStomach) | |||
| const transfer = new TransferAction(stomach, lowerStomach) | |||
| transfer.verb = new Verb('gulp') | |||
| this.actions.push(transfer) | |||
| this.actions.push(new TransferAction(lowerStomach, stomach)) | |||
| this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ amount: 40, type: DamageType.Crush, target: Vigor.Health })))) | |||
| this.actions.push(new BellyCrushAction(new Damage({ amount: 10, type: DamageType.Crush, target: Vigor.Health }, { amount: 10, type: DamageType.Dominance, target: Vigor.Resolve }))) | |||
| this.actions.push(new BelchAction(new Damage( | |||
| { amount: 10, target: Stat.Toughness, type: DamageType.Acid }, | |||
| { amount: 10, target: Stat.Power, type: DamageType.Acid }, | |||
| { amount: 10, target: Stat.Agility, type: DamageType.Acid }, | |||
| { amount: 10, target: Stat.Willpower, type: DamageType.Acid }, | |||
| { amount: 10, target: Stat.Charm, type: DamageType.Acid } | |||
| ))) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| } | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| import { FavorEscapedPrey, VoreAI } from '@/game/ai' | |||
| import { DamageType, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat' | |||
| import { Creature } from '@/game/creature' | |||
| import { ImproperNoun, MalePronouns, ProperNoun } from '@/game/language' | |||
| import { anyVore, Stomach } from '@/game/vore' | |||
| export default class Inazuma extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun("Inazuma"), | |||
| new ImproperNoun("zorgoia"), | |||
| MalePronouns, | |||
| { | |||
| Power: 30, | |||
| Toughness: 35, | |||
| Agility: 45, | |||
| Reflexes: 40, | |||
| Charm: 60, | |||
| Willpower: 50 | |||
| }, | |||
| new Set(), | |||
| anyVore, | |||
| 200 | |||
| ) | |||
| this.side = Side.Monsters | |||
| this.ai = (new VoreAI(this)) | |||
| this.ai.addDecider(new FavorEscapedPrey()) | |||
| this.addVoreContainer(new Stomach( | |||
| this, | |||
| 2, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Acid } | |||
| ]) | |||
| )) | |||
| } | |||
| } | |||
| @@ -1,44 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side } from '@/game/combat' | |||
| import { ImproperNoun, FemalePronouns, Verb } from '@/game/language' | |||
| import { VoreType, Stomach, Bowels } from '@/game/vore' | |||
| import { AttackAction, TransferAction, FeedAction } from '@/game/combat/actions' | |||
| export class Dragon extends Creature { | |||
| constructor () { | |||
| super(new ImproperNoun('dragon', 'dragons'), new ImproperNoun('wolf', 'wolves'), FemalePronouns, { Toughness: 35, Power: 35, Reflexes: 15, Agility: 15, Willpower: 30, Charm: 20 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 300) | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new ConstantDamageFormula( | |||
| new Damage( | |||
| { amount: 20, type: DamageType.Pierce, target: Vigor.Health } | |||
| ) | |||
| ), | |||
| new Verb("bite", "bites", "biting", "bit") | |||
| ) | |||
| ) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 0.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 40, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 20, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 20, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(stomach) | |||
| const bowels = new Bowels(this, 0.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 10, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 25, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 50, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(bowels) | |||
| this.actions.push(new TransferAction(bowels, stomach)) | |||
| this.actions.push(new TransferAction(stomach, bowels)) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| } | |||
| } | |||
| @@ -1,202 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side, CompositionAction } from '@/game/combat' | |||
| import { MalePronouns, ImproperNoun, ProperNoun, Verb } from '@/game/language' | |||
| import { VoreType, Stomach, Bowels, Cock, Balls, biconnectContainers, Hand } from '@/game/vore' | |||
| import { TransferAction, FeedAction } from '@/game/combat/actions' | |||
| import { StatusConsequence, LogConsequence, ArbitraryConsequence } from '@/game/combat/consequences' | |||
| import { SizeEffect, InstantKillEffect } from '@/game/combat/effects' | |||
| import { LogLine, nilLog } from '@/game/interface' | |||
| import { TogetherCondition, MassRatioCondition, ContainsCondition } from '@/game/combat/conditions' | |||
| import { ChanceTest } from '@/game/combat/tests' | |||
| export class Geta extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Geta'), | |||
| new ImproperNoun('fox', 'foxes'), | |||
| MalePronouns, | |||
| { Toughness: 10, Power: 10, Reflexes: 30, Agility: 30, Willpower: 15, Charm: 40 }, | |||
| new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), | |||
| new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), | |||
| 40 | |||
| ) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 0.25, new ConstantDamageFormula(new Damage( | |||
| { amount: 100, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 40, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 80, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(stomach) | |||
| const bowels = new Bowels(this, 0.25, new ConstantDamageFormula(new Damage( | |||
| { amount: 30, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 90, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 120, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(bowels) | |||
| this.actions.push(new TransferAction(bowels, stomach)) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| const cock = new class extends Cock { | |||
| digestLine (user: Creature, target: Creature) { | |||
| return new LogLine(`${user.name.capital.possessive} ${this.name} throbs as it abruptly absorbs ${target.name.objective}, transforming ${target.name.objective} into more of ${user.pronouns.possessive} meaty shaft.`) | |||
| } | |||
| }(this, 0.25, new ConstantDamageFormula(new Damage( | |||
| { amount: 10, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 50, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 150, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| cock.actions.push( | |||
| new CompositionAction( | |||
| "Clench", | |||
| "Try to crush the life from your cock-snack", | |||
| { | |||
| conditions: [ | |||
| new ContainsCondition(cock) | |||
| ], | |||
| tests: [ | |||
| new ChanceTest( | |||
| 0.5, | |||
| (user, target) => new LogLine( | |||
| `${user.name.capital.possessive} cock clenches hard around ${target.name.objective}, but ${target.pronouns.subjective} ${target.name.conjugate(new Verb("avoid"))} being crushed.` | |||
| ) | |||
| ) | |||
| ], | |||
| consequences: [ | |||
| new LogConsequence( | |||
| (user, target) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb('let'))} out a lustful moan as ${user.pronouns.subjective} crushes the life from ${target.name.possessive} body with a flex of ${user.pronouns.possessive} shaft.` | |||
| ) | |||
| ), | |||
| new StatusConsequence( | |||
| () => new InstantKillEffect() | |||
| ), | |||
| new ArbitraryConsequence( | |||
| () => cock.tick(0), | |||
| () => nilLog | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| ) | |||
| const balls = new Balls(this, 0.25, new ConstantDamageFormula(new Damage( | |||
| { amount: 50, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 25, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 50, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| )), cock) | |||
| this.containers.push(balls) | |||
| this.containers.push(cock) | |||
| biconnectContainers(cock, balls) | |||
| cock.onDigest = () => nilLog | |||
| const shrinkAction = new CompositionAction( | |||
| "Shrink", | |||
| "Zap!", | |||
| { | |||
| conditions: [ | |||
| new TogetherCondition() | |||
| ], | |||
| consequences: [ | |||
| new LogConsequence( | |||
| () => new LogLine(`ZAP!`) | |||
| ), | |||
| new StatusConsequence( | |||
| () => new SizeEffect(0.1) | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| this.actions.push( | |||
| shrinkAction | |||
| ) | |||
| this.otherActions.push( | |||
| shrinkAction | |||
| ) | |||
| const crushAction = new CompositionAction( | |||
| "Crush", | |||
| "Crush them like a bug underfoot", | |||
| { | |||
| conditions: [ | |||
| new TogetherCondition(), | |||
| new MassRatioCondition(10) | |||
| ], | |||
| consequences: [ | |||
| new LogConsequence( | |||
| (user, target) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb("raise"))} ${user.pronouns.possessive} paw over ${target.name.objective}, stomping down hard and crushing the life from ${user.pronouns.possessive} prey with a sickening CRUNCH.` | |||
| ) | |||
| ), | |||
| new StatusConsequence( | |||
| () => new InstantKillEffect() | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| this.actions.push( | |||
| crushAction | |||
| ) | |||
| this.otherActions.push( | |||
| crushAction | |||
| ) | |||
| const hand = new Hand(this, 0.25) | |||
| this.otherContainers.push( | |||
| hand | |||
| ) | |||
| this.actions.push( | |||
| new CompositionAction( | |||
| "Devour", | |||
| "Pop your prey into your mouth", | |||
| { | |||
| conditions: [ | |||
| new ContainsCondition(hand) | |||
| ], | |||
| consequences: [ | |||
| new ArbitraryConsequence( | |||
| (user, target) => stomach.consume(target), | |||
| () => new LogLine(`Devours the target.`) | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| ) | |||
| this.actions.push( | |||
| new CompositionAction( | |||
| "Grip", | |||
| "Squeeze your prey like a grape", | |||
| { | |||
| conditions: [ | |||
| new ContainsCondition(hand) | |||
| ], | |||
| consequences: [ | |||
| new LogConsequence( | |||
| (user, target) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb("crush", "crushes"))} ${target.name.objective} in ${user.pronouns.possessive} merciless grip, breaking bones and pulping ${user.pronouns.possessive} victim with ease.` | |||
| ) | |||
| ), | |||
| new StatusConsequence( | |||
| () => new InstantKillEffect() | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| @@ -1,195 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side, GroupAction, FractionDamageFormula, DamageFormula, UniformRandomDamageFormula, CompositionAction, CompositeDamageFormula } from '@/game/combat' | |||
| import { MalePronouns, ImproperNoun, Verb, ProperNoun, ToBe, SoloLineArgs, Noun } from '@/game/language' | |||
| import { VoreType, NormalContainer, InnerVoreContainer, Container } from '@/game/vore' | |||
| import { TransferAction } from '@/game/combat/actions' | |||
| import { LogEntry, LogLine, LogLines } from '@/game/interface' | |||
| import { ContainerFullCondition, CapableCondition, EnemyCondition, TogetherCondition } from '@/game/combat/conditions' | |||
| import { DazzlingEffect, StunEffect } from '@/game/combat/effects' | |||
| import { DamageConsequence, StatusConsequence, LogConsequence } from '@/game/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: Creature) { | |||
| 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 { | |||
| fluidName = new Noun("chyme") | |||
| consumeVerb: Verb = new Verb('swallow') | |||
| releaseVerb: Verb = new Verb('free') | |||
| struggleVerb: Verb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| constructor (owner: Creature, crop: GoldeneyeCrop) { | |||
| super( | |||
| new ImproperNoun('stomach').all, | |||
| owner, | |||
| new Set([VoreType.Oral]), | |||
| 900, | |||
| new ConstantDamageFormula( | |||
| 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, Reflexes: 200, Agility: 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(crop)) | |||
| 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 CompositeDamageFormula([ | |||
| new FractionDamageFormula([ | |||
| { fraction: 0.75, target: Vigor.Health, type: DamageType.Pure } | |||
| ]), | |||
| new ConstantDamageFormula( | |||
| new Damage( | |||
| { amount: 50, target: Vigor.Health, type: DamageType.Crush } | |||
| ) | |||
| ) | |||
| ]) | |||
| ), | |||
| new StatusConsequence( | |||
| () => new StunEffect(3) | |||
| ) | |||
| ] | |||
| } | |||
| )) | |||
| } | |||
| } | |||
| @@ -6,7 +6,7 @@ import { StatusConsequence } from '@/game/combat/consequences' | |||
| import { SurrenderEffect } from '@/game/combat/effects' | |||
| import { SoloCondition } from '@/game/combat/conditions' | |||
| export class Human extends Creature { | |||
| export default class Human extends Creature { | |||
| constructor (name: Noun, pronouns: Pronoun, options: { | |||
| vigors?: Vigor; | |||
| stats?: Stats; | |||
| @@ -1,63 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { ProperNoun, ImproperNoun, FemalePronouns, Verb } from '@/game/language' | |||
| import { VoreType, Stomach } from '@/game/vore' | |||
| import { Side, Damage, DamageType, Vigor, StatDamageFormula, Stat, VoreStat, DamageFormula, ConstantDamageFormula } from '@/game/combat' | |||
| import { AttackAction, DevourAction } from '@/game/combat/actions' | |||
| import { LogEntry, LogLines } from '@/game/interface' | |||
| import { StunEffect, PredatorCounterEffect } from '@/game/combat/effects' | |||
| class StompAttack extends AttackAction { | |||
| constructor (protected damage: DamageFormula, protected verb: Verb = new Verb('smack')) { | |||
| super( | |||
| damage, | |||
| verb | |||
| ) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const damage = this.damage.calc(user, target) | |||
| const targetResult = target.takeDamage(damage) | |||
| const ownResult = this.successLine(user, target, { damage: damage }) | |||
| const effResult = target.applyEffect(new StunEffect(3)) | |||
| return new LogLines(ownResult, targetResult, effResult) | |||
| } | |||
| } | |||
| export class Kenzie extends Creature { | |||
| title = "Large Lycanroc" | |||
| desc = "Will eat your party" | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Kenzie'), | |||
| new ImproperNoun('lycanroc', 'lycanrocs'), | |||
| FemalePronouns, | |||
| { Toughness: 25, Power: 35, Reflexes: 20, Agility: 20, Willpower: 20, Charm: 30 }, | |||
| new Set([VoreType.Oral]), | |||
| new Set([VoreType.Oral]), | |||
| 1000 | |||
| ) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 0.1, new ConstantDamageFormula(new Damage( | |||
| { amount: 100, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 100, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 100, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.applyEffect(new PredatorCounterEffect(new DevourAction(stomach), 0.4)) | |||
| this.containers.push(stomach) | |||
| this.actions.push( | |||
| new StompAttack( | |||
| 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 }, | |||
| { fraction: 0.01, stat: VoreStat.Bulk, target: Stat.Toughness, type: DamageType.Crush } | |||
| ]), | |||
| new Verb('stomp') | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| @@ -1,57 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side } from '@/game/combat' | |||
| import { MalePronouns, ProperNoun } from '@/game/language' | |||
| import { VoreType, Stomach, Bowels, Cock, Balls, biconnectContainers } from '@/game/vore' | |||
| import { TransferAction, FeedAction } from '@/game/combat/actions' | |||
| export class Kuro extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Kuro'), | |||
| new ProperNoun('Luxray'), | |||
| MalePronouns, | |||
| { Toughness: 20, Power: 30, Reflexes: 50, Agility: 50, Willpower: 30, Charm: 50 }, | |||
| new Set(), | |||
| new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), | |||
| 100 | |||
| ) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 0.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 100, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 40, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 80, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(stomach) | |||
| const bowels = new Bowels(this, 0.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 30, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 90, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 120, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(bowels) | |||
| this.actions.push(new TransferAction(bowels, stomach)) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| const cock = new Cock(this, 0.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 10, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 30, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 30, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| const balls = new Balls(this, 0.5, new ConstantDamageFormula(new Damage( | |||
| { amount: 50, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 25, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 150, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| )), cock) | |||
| this.containers.push(balls) | |||
| this.containers.push(cock) | |||
| biconnectContainers(cock, balls) | |||
| } | |||
| } | |||
| @@ -5,7 +5,7 @@ import { Stomach, Bowels, anyVore, Cock, Balls, Breasts, InnerBladder, Slit, Wom | |||
| import { AttackAction } from '@/game/combat/actions' | |||
| import { RavenousPerk, BellyBulwakPerk, FlauntPerk } from '@/game/combat/perks' | |||
| export class Player extends Creature { | |||
| export default class Player extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Player'), | |||
| @@ -20,37 +20,8 @@ export class Player extends Creature { | |||
| this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina })))) | |||
| const stomach = new Stomach(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.containers.push(stomach) | |||
| const bowels = new Bowels(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.containers.push(bowels) | |||
| const cock = new Cock(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.containers.push(cock) | |||
| const balls = new Balls(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health })), cock) | |||
| this.containers.push(balls) | |||
| const slit = new Slit(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.containers.push(slit) | |||
| const womb = new Womb(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health })), slit) | |||
| this.containers.push(womb) | |||
| const bladder = new InnerBladder(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health })), cock) | |||
| this.containers.push(bladder) | |||
| const breasts = new Breasts(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.containers.push(breasts) | |||
| biconnectContainers(cock, balls) | |||
| biconnectContainers(cock, bladder) | |||
| biconnectContainers(slit, womb) | |||
| this.addVoreContainer(stomach) | |||
| this.perspective = POV.Second | |||
| this.perks.push(new RavenousPerk()) | |||
| this.perks.push(new BellyBulwakPerk()) | |||
| this.perks.push(new FlauntPerk()) | |||
| } | |||
| } | |||
| @@ -1,191 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side, StatDamageFormula, Stat, DamageFormula, Condition, FractionDamageFormula } from '@/game/combat' | |||
| import { ImproperNoun, ProperNoun, MalePronouns, Verb, PairLineArgs, TextLike } from '@/game/language' | |||
| import { VoreType, Stomach, NormalContainer, Container } from '@/game/vore' | |||
| import { AttackAction, TransferAction, FeedAction, StruggleAction, DamageAction } from '@/game/combat/actions' | |||
| import { LogLine, LogEntry } from '@/game/interface' | |||
| import { ContainsCondition, CapableCondition, EnemyCondition, TargetDrainedVigorCondition } from '@/game/combat/conditions' | |||
| export class TrappedAction extends DamageAction { | |||
| constructor (name: TextLike, desc: TextLike, protected verb: Verb, protected damage: DamageFormula, container: Container, conditions: Condition[] = []) { | |||
| super( | |||
| name, | |||
| desc, | |||
| damage, | |||
| [], | |||
| [new CapableCondition(), new ContainsCondition(container), new EnemyCondition()].concat(conditions) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Chew on ${target.name}. `, this.damage.describe(user, target), '. ') | |||
| } | |||
| successLine: PairLineArgs<Creature, { damage: Damage }> = (user, target, args) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(this.verb)} ${target.name.objective} for `, | |||
| target.effectiveDamage(args.damage).renderShort() | |||
| ) | |||
| } | |||
| class Hand extends NormalContainer { | |||
| consumeVerb: Verb = new Verb('grab', 'grabs', 'grabbing', 'grabbed') | |||
| releaseVerb: Verb = new Verb('release', 'releases', 'releasing', 'released') | |||
| struggleVerb: Verb = new Verb('squirm', 'squirms', 'squirming', 'squirmed') | |||
| constructor (owner: Creature) { | |||
| super(new ImproperNoun('hand', 'hands'), owner, new Set(), 0.1) | |||
| this.actions.push(new TrappedAction( | |||
| "Grip", | |||
| "Give your victim the squeeze", | |||
| new Verb( | |||
| 'squeeze' | |||
| ), | |||
| new ConstantDamageFormula( | |||
| new Damage( | |||
| { amount: 200, target: Vigor.Health, type: DamageType.Crush } | |||
| ) | |||
| ), | |||
| this | |||
| )) | |||
| } | |||
| consumeLine (user: Creature, target: Creature) { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective} up in ${user.pronouns.possessive} ${this.name}, giving ${target.pronouns.objective} a firm squeeze in ${user.pronouns.possessive} fingers.`) | |||
| } | |||
| } | |||
| class Paw extends NormalContainer { | |||
| consumeVerb: Verb = new Verb('pin', 'pins', 'pinning', 'pinbed') | |||
| releaseVerb: Verb = new Verb('release', 'releases', 'releasing', 'released') | |||
| struggleVerb: Verb = new Verb('squirm', 'squirms', 'squirming', 'squirmed') | |||
| constructor (owner: Creature) { | |||
| super(new ImproperNoun('paw', 'paws'), owner, new Set([VoreType.Oral]), 0.1) | |||
| this.actions.push(new TrappedAction( | |||
| "Smother", | |||
| "Bury your victim under your toes", | |||
| new Verb( | |||
| 'smother' | |||
| ), | |||
| new ConstantDamageFormula( | |||
| new Damage( | |||
| { amount: 100, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { amount: 3, target: Stat.Toughness, type: DamageType.Crush }, | |||
| { amount: 5, target: Stat.Power, type: DamageType.Crush }, | |||
| { amount: 10, target: Stat.Agility, type: DamageType.Crush } | |||
| ) | |||
| ), | |||
| this | |||
| )) | |||
| this.actions.push(new TrappedAction( | |||
| "Crush", | |||
| "Finish them off", | |||
| new Verb( | |||
| 'crush', | |||
| 'crushes' | |||
| ), | |||
| new FractionDamageFormula( | |||
| [ | |||
| { fraction: 1, target: Stat.Toughness, type: DamageType.Pure }, | |||
| { fraction: 1, target: Stat.Power, type: DamageType.Pure }, | |||
| { fraction: 1, target: Stat.Agility, type: DamageType.Pure }, | |||
| { fraction: 1, target: Stat.Willpower, type: DamageType.Pure }, | |||
| { fraction: 1, target: Stat.Charm, type: DamageType.Pure } | |||
| ] | |||
| ), | |||
| this, | |||
| [new TargetDrainedVigorCondition(Vigor.Stamina)] | |||
| )) | |||
| } | |||
| consumeLine (user: Creature, target: Creature) { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective} beneath ${user.pronouns.possessive} ${this.name}.`) | |||
| } | |||
| } | |||
| class Maw extends NormalContainer { | |||
| consumeVerb: Verb = new Verb('grab', 'grabs', 'grabbing', 'grabbed') | |||
| releaseVerb: Verb = new Verb('release', 'releases', 'releasing', 'released') | |||
| struggleVerb: Verb = new Verb('squirm', 'squirms', 'squirming', 'squirmed') | |||
| constructor (owner: Creature) { | |||
| super(new ImproperNoun('maw', 'maws'), owner, new Set([VoreType.Oral]), 100) | |||
| this.actions = [] | |||
| this.actions.push(new StruggleAction(this)) | |||
| this.actions.push(new TrappedAction( | |||
| "Chew", | |||
| "Chew on your victim", | |||
| new Verb( | |||
| 'bite down on', | |||
| 'bites down on', | |||
| 'biting down on', | |||
| 'bit down on' | |||
| ), | |||
| new ConstantDamageFormula( | |||
| new Damage( | |||
| { amount: 200, target: Vigor.Health, type: DamageType.Crush } | |||
| ) | |||
| ), | |||
| this | |||
| )) | |||
| } | |||
| } | |||
| export class Shingo extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Shingo'), | |||
| new ImproperNoun('red panda', 'red pandas'), | |||
| MalePronouns, | |||
| { Toughness: 40, Power: 50, Reflexes: 30, Agility: 30, Willpower: 30, Charm: 60 }, | |||
| new Set(), | |||
| new Set([VoreType.Oral]), | |||
| 3000 | |||
| ) | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new StatDamageFormula( | |||
| [ | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush } | |||
| ] | |||
| ) | |||
| ) | |||
| ) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 0.1, new ConstantDamageFormula(new Damage( | |||
| { amount: 200, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 100, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 100, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(stomach) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| const hand = new Hand(this) | |||
| const maw = new Maw(this) | |||
| this.otherContainers.push(hand) | |||
| this.otherContainers.push(maw) | |||
| const stuff = new TransferAction(hand, maw) | |||
| stuff.verb = new Verb('stuff') | |||
| stuff.line = (user, target, args) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(stuff.verb)} ${target.name.objective} into ${user.pronouns.possessive} ${args.to.name}!` | |||
| ) | |||
| this.actions.push(stuff) | |||
| const gulp = new TransferAction(maw, stomach) | |||
| gulp.verb = new Verb('gulp') | |||
| gulp.line = (user, target, args) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(gulp.verb)} ${target.name.objective} down ${user.pronouns.possessive} tight, inescapable gullet, sending ${target.pronouns.objective} down to ${user.pronouns.possessive} ${args.to.name}` | |||
| ) | |||
| this.actions.push(gulp) | |||
| this.otherContainers.push(new Paw(this)) | |||
| } | |||
| } | |||
| @@ -1,103 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { DamageType, Vigor, Side, StatDamageFormula, Stat } from '@/game/combat' | |||
| import { MalePronouns, ProperNoun, Verb } from '@/game/language' | |||
| import { Stomach, Bowels, Cock, Balls, anyVore, biconnectContainers, Tail } from '@/game/vore' | |||
| import { AttackAction, TransferAction, FeedAction, WillingTransferAction } from '@/game/combat/actions' | |||
| import { VoreAI } from '@/game/ai' | |||
| import { LogLine } from '@/game/interface' | |||
| export class Taluthus extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Taluthus'), | |||
| new ProperNoun('Taluthus'), | |||
| MalePronouns, | |||
| { Toughness: 40, Power: 50, Reflexes: 40, Agility: 30, Willpower: 50, Charm: 60 }, | |||
| new Set(), | |||
| anyVore, | |||
| 100 | |||
| ) | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Pierce }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush } | |||
| ]), | |||
| new Verb("bite") | |||
| ) | |||
| ) | |||
| this.ai = new VoreAI() | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach( | |||
| this, | |||
| 1, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]) | |||
| ) | |||
| this.containers.push(stomach) | |||
| const tail = new Tail( | |||
| this, | |||
| 1.5, | |||
| new StatDamageFormula([ | |||
| { fraction: 0.05, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 0.1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]) | |||
| ) | |||
| this.containers.push(tail) | |||
| this.actions.push(new TransferAction(tail, stomach)) | |||
| this.otherActions.push(new WillingTransferAction(tail, stomach)) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| const cock = new Cock( | |||
| this, | |||
| 1.5, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Charm, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Charm, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Charm, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]) | |||
| ) | |||
| const balls = new Balls( | |||
| this, | |||
| 1.5, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1.5, stat: Stat.Toughness, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 1.5, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]), | |||
| cock | |||
| ) | |||
| this.containers.push(balls) | |||
| this.containers.push(cock) | |||
| biconnectContainers(cock, balls) | |||
| this.destroyLine = victim => new LogLine(`${victim.name.capital} ${victim.name.conjugate(new Verb("yeet"))}`) | |||
| } | |||
| } | |||
| @@ -1,99 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { DamageType, Vigor, Side, StatDamageFormula, Stat } from '@/game/combat' | |||
| import { MalePronouns, ImproperNoun, Verb } from '@/game/language' | |||
| import { Stomach, Bowels, Cock, Balls, anyVore, biconnectContainers } from '@/game/vore' | |||
| import { AttackAction, TransferAction, FeedAction } from '@/game/combat/actions' | |||
| import { VoreAI } from '@/game/ai' | |||
| export class Werewolf extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ImproperNoun('werewolf', 'werewolves'), | |||
| new ImproperNoun('werewolf', 'werewolves'), | |||
| MalePronouns, | |||
| { Toughness: 40, Power: 50, Reflexes: 40, Agility: 30, Willpower: 20, Charm: 50 }, | |||
| anyVore, | |||
| anyVore, | |||
| 100 | |||
| ) | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Pierce }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush } | |||
| ]), | |||
| new Verb("bite") | |||
| ) | |||
| ) | |||
| this.ai = new VoreAI() | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach( | |||
| this, | |||
| 1, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]) | |||
| ) | |||
| this.containers.push(stomach) | |||
| const bowels = new Bowels( | |||
| this, | |||
| 1.5, | |||
| new StatDamageFormula([ | |||
| { fraction: 0.5, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]) | |||
| ) | |||
| this.containers.push(bowels) | |||
| this.actions.push(new TransferAction(bowels, stomach)) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| const cock = new Cock( | |||
| this, | |||
| 1.5, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Charm, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 0.5, stat: Stat.Charm, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 0.5, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Charm, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]) | |||
| ) | |||
| const balls = new Balls( | |||
| this, | |||
| 1.5, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Toughness, target: Vigor.Stamina, type: DamageType.Acid }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1.5, stat: Stat.Toughness, target: Vigor.Resolve, type: DamageType.Acid }, | |||
| { fraction: 1.5, stat: Stat.Power, target: Vigor.Resolve, type: DamageType.Crush } | |||
| ]), | |||
| cock | |||
| ) | |||
| this.containers.push(balls) | |||
| this.containers.push(cock) | |||
| biconnectContainers(cock, balls) | |||
| } | |||
| } | |||
| @@ -1,362 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side, GroupAction, Stat, DamageFormula, UniformRandomDamageFormula, Action, DamageInstance, StatDamageFormula, VoreStat } from '@/game/combat' | |||
| import { ImproperNoun, ProperNoun, FemalePronouns, RandomWord, Adjective, Verb, PairLine } from '@/game/language' | |||
| import { LogLine, LogLines, LogEntry, Newline } from '@/game/interface' | |||
| import { VoreType, Stomach, VoreContainer, NormalContainer, Container } from '@/game/vore' | |||
| import { AttackAction, FeedAction, TransferAction } from '@/game/combat/actions' | |||
| import { TogetherCondition, ContainsCondition, EnemyCondition, AllyCondition, PairCondition, CapableCondition } from '@/game/combat/conditions' | |||
| import { InstantKillEffect, DamageTypeResistanceEffect } from '@/game/combat/effects' | |||
| import * as Words from '@/game/words' | |||
| class LevelDrain extends Action { | |||
| constructor (private container: Container) { | |||
| super( | |||
| 'Level Drain', | |||
| 'Drain energy from your prey', | |||
| [ | |||
| new ContainsCondition(container), | |||
| new CapableCondition() | |||
| ] | |||
| ) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const damage: Damage = new Damage(...Object.keys(Stat).map(stat => { | |||
| return { | |||
| type: DamageType.Acid, | |||
| target: stat as Stat, | |||
| amount: target.baseStats[stat as Stat] / 5 | |||
| } | |||
| })) | |||
| const heal: Damage = new Damage(...Object.keys(Stat).map(stat => { | |||
| return { | |||
| type: DamageType.Heal, | |||
| target: stat as Stat, | |||
| amount: target.baseStats[stat as Stat] / 5 | |||
| } | |||
| })) | |||
| // TODO make this respect resistances | |||
| user.takeDamage(heal) | |||
| const targetResult = target.takeDamage(damage) | |||
| return new LogLines( | |||
| new LogLine(`${user.name.capital.possessive} ${this.container.name} drains power from ${target.name.objective}, siphoning `, damage.renderShort(), ` from ${target.pronouns.possessive} body!`), | |||
| targetResult | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Drain energy from ${target.name}`) | |||
| } | |||
| } | |||
| class HypnotizeAction extends Action { | |||
| constructor () { | |||
| super( | |||
| `Hypnotize`, | |||
| `Change their mind!`, | |||
| [ | |||
| new TogetherCondition(), | |||
| new EnemyCondition(), | |||
| new CapableCondition() | |||
| ] | |||
| ) | |||
| } | |||
| line: PairLine<Creature> = (user, target) => new LogLine( | |||
| `${user.name.capital.possessive} hypnotic gaze enthralls ${target.name}, putting ${target.pronouns.objective} under ${user.pronouns.possessive} control!` | |||
| ) | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| target.side = user.side | |||
| return this.line(user, target) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Force your target to fight by your side`) | |||
| } | |||
| } | |||
| class MawContainer extends NormalContainer { | |||
| consumeVerb = new Verb('grab', 'grabs', 'grabbing', 'grabbed') | |||
| releaseVerb = new Verb('release') | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| constructor (owner: Creature, stomach: VoreContainer) { | |||
| super(new ImproperNoun('maw'), owner, new Set([VoreType.Oral]), 0.05) | |||
| const transfer = new TransferAction(this, stomach) | |||
| transfer.verb = new Verb('gulp') | |||
| this.actions.push(transfer) | |||
| } | |||
| } | |||
| class FlexToesAction extends GroupAction { | |||
| constructor (private damage: DamageFormula, container: Container) { | |||
| super('Flex Toes', 'Flex your toes!', [ | |||
| new ContainsCondition(container), | |||
| new PairCondition() | |||
| ]) | |||
| } | |||
| line = (user: Creature, target: Creature, args: { damage: Damage }) => new LogLine(`${user.name.capital.possessive} toes crush ${target.name.objective} for `, args.damage.renderShort(), ` damage!`) | |||
| describeGroup (user: Creature, targets: Creature[]): LogEntry { | |||
| return new LogLine(`Flex your toes. `, this.damage.explain(user)) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const damage = this.damage.calc(user, target) | |||
| return new LogLines(target.takeDamage(damage), this.line(user, target, { damage: damage })) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Flex your toes! `, this.damage.describe(user, target)) | |||
| } | |||
| } | |||
| class BootContainer extends NormalContainer { | |||
| consumeVerb = new Verb('trap', 'traps', 'trapped', 'trapping') | |||
| releaseVerb = new Verb('dump') | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| constructor (owner: Creature) { | |||
| super(new ImproperNoun('boot'), owner, new Set(), 0.05) | |||
| const flex = new FlexToesAction( | |||
| new UniformRandomDamageFormula(new Damage( | |||
| { target: Stat.Toughness, type: DamageType.Crush, amount: 10 }, | |||
| { target: Stat.Power, type: DamageType.Crush, amount: 10 }, | |||
| { target: Stat.Agility, type: DamageType.Crush, amount: 10 }, | |||
| { target: Stat.Willpower, type: DamageType.Crush, amount: 30 }, | |||
| { target: Stat.Charm, type: DamageType.Crush, amount: 10 } | |||
| ), 0.5), | |||
| this | |||
| ) | |||
| this.actions.push(flex) | |||
| } | |||
| } | |||
| const huge = new RandomWord([ | |||
| new Adjective('massive'), | |||
| new Adjective('colossal'), | |||
| new Adjective('big ol\''), | |||
| new Adjective('heavy'), | |||
| new Adjective('crushing'), | |||
| new Adjective('huge') | |||
| ]) | |||
| class BiteAction extends AttackAction { | |||
| constructor () { | |||
| super( | |||
| new ConstantDamageFormula(new Damage({ amount: 50, type: DamageType.Slash, target: Vigor.Health })), | |||
| new Verb('bite', 'bites', 'biting', 'bit') | |||
| ) | |||
| this.name = "Bite" | |||
| } | |||
| } | |||
| class ChewAction extends GroupAction { | |||
| constructor (private damage: DamageFormula, container: Container, private killAction: Action) { | |||
| super('Chew', 'Give them the big chew', [ | |||
| new ContainsCondition(container) | |||
| ]) | |||
| } | |||
| line = (user: Creature, target: Creature, args: { damage: Damage }) => new LogLine(`${user.name.capital} chews on ${target.name.objective} for `, args.damage.renderShort(), `!`) | |||
| describeGroup (user: Creature, targets: Creature[]): LogEntry { | |||
| return new LogLine('Crunch \'em all. ', this.damage.explain(user)) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const damage = this.damage.calc(user, target) | |||
| const results: Array<LogEntry> = [] | |||
| results.push(this.line(user, target, { damage: damage })) | |||
| results.push(new LogLine(' ')) | |||
| results.push(target.takeDamage(damage)) | |||
| if (target.vigors.Health <= 0) { | |||
| if (this.killAction.allowed(user, target)) { | |||
| results.push(new Newline()) | |||
| results.push(this.killAction.execute(user, target)) | |||
| } | |||
| } | |||
| return new LogLine(...results) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine('Do the crunch') | |||
| } | |||
| } | |||
| class StompAction extends GroupAction { | |||
| constructor () { | |||
| super('Stomp', 'STOMP!', [ | |||
| new TogetherCondition(), | |||
| new EnemyCondition(), | |||
| new CapableCondition() | |||
| ]) | |||
| } | |||
| line: PairLine<Creature> = (user, target) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb('flatten'))} ${target.name.objective} under ${user.pronouns.possessive} ${huge} foot!` | |||
| ) | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines(this.line(user, target), target.applyEffect(new InstantKillEffect())) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine('Stomp one sucker') | |||
| } | |||
| describeGroup (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLine('Stomp all ', targets.length.toString(), ' of \'em!') | |||
| } | |||
| } | |||
| class StompAllyAction extends Action { | |||
| constructor () { | |||
| super('Stomp Ally', '-1 ally, +1 buff', [ | |||
| new TogetherCondition(), | |||
| new AllyCondition(), | |||
| new CapableCondition() | |||
| ]) | |||
| } | |||
| line: PairLine<Creature> = (user, target) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(new Verb('flatten'))} ${target.name.objective} under ${user.pronouns.possessive} ${huge} boot!` | |||
| ) | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const damages: Array<DamageInstance> = Object.keys(Stat).map(stat => ({ | |||
| target: stat as Stat, | |||
| amount: target.stats[stat as Stat] / 3, | |||
| type: DamageType.Heal | |||
| })) | |||
| const heal = new Damage( | |||
| ...damages | |||
| ) | |||
| user.takeDamage(heal) | |||
| target.destroyed = true | |||
| return new LogLines( | |||
| this.line(user, target), | |||
| target.applyEffect(new InstantKillEffect()), | |||
| user.applyEffect(new DamageTypeResistanceEffect( | |||
| [DamageType.Crush, DamageType.Slash, DamageType.Pierce], | |||
| 0.5 | |||
| )), | |||
| new LogLine(`${user.name.capital} absorbs ${target.pronouns.possessive} power, gaining `, heal.renderShort()) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine('Crush an ally to absorb their power') | |||
| } | |||
| } | |||
| class DevourAllAction extends GroupAction { | |||
| constructor (private container: VoreContainer) { | |||
| super('Devour All', 'GULP!', [ | |||
| new TogetherCondition(), | |||
| new EnemyCondition(), | |||
| new CapableCondition() | |||
| ]) | |||
| } | |||
| line = (user: Creature, target: Creature) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('scoop'))} ${target.name} up!`) | |||
| groupLine = (user: Creature, args: { count: number }) => new LogLine(`${Words.SwallowSound.allCaps}! All ${args.count} of ${user.pronouns.possessive} prey pour down ${user.name.possessive} ${Words.Slick} gullet as ${user.pronouns.subjective} ${user.name.conjugate(Words.Swallow)}; they're just ${user.kind.all} chow now`) | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| this.container.consume(target) | |||
| return new LogLines(this.line(user, target)) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine('Stomp one sucker') | |||
| } | |||
| executeGroup (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLines(...targets.map(target => this.execute(user, target)).concat( | |||
| [ | |||
| new Newline(), | |||
| this.groupLine(user, { count: targets.length }) | |||
| ] | |||
| )) | |||
| } | |||
| describeGroup (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLine('Eat all ', targets.length.toString(), ' of \'em!') | |||
| } | |||
| } | |||
| export class Withers extends Creature { | |||
| title = "Huge Hellhound" | |||
| desc = "Will eat your party" | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Withers'), | |||
| new ImproperNoun('hellhound', 'hellhounds'), | |||
| FemalePronouns, | |||
| { Toughness: 40, Power: 50, Reflexes: 30, Agility: 30, Willpower: 40, Charm: 70 }, | |||
| new Set(), | |||
| new Set([VoreType.Oral]), | |||
| 5000 | |||
| ) | |||
| this.actions.push(new BiteAction()) | |||
| this.groupActions.push(new StompAction()) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 0.1, new ConstantDamageFormula(new Damage( | |||
| { amount: 300, type: DamageType.Acid, target: Vigor.Health }, | |||
| { amount: 200, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 200, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ))) | |||
| this.containers.push(stomach) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| this.groupActions.push(new DevourAllAction(stomach)) | |||
| const maw = new MawContainer(this, stomach) | |||
| this.otherContainers.push(maw) | |||
| const transfer = new TransferAction(maw, stomach) | |||
| transfer.verb = new Verb('gulp') | |||
| this.actions.push(new ChewAction( | |||
| new UniformRandomDamageFormula(new Damage( | |||
| { target: Vigor.Health, type: DamageType.Crush, amount: 10000 } | |||
| ), 0.5), | |||
| maw, | |||
| transfer | |||
| )) | |||
| const boot = new BootContainer(this) | |||
| this.otherContainers.push(boot) | |||
| this.actions.push(new StompAllyAction()) | |||
| 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('stomp') | |||
| ) | |||
| ) | |||
| this.actions.push(new HypnotizeAction()) | |||
| this.actions.push(new LevelDrain(stomach)) | |||
| } | |||
| } | |||
| @@ -1,77 +0,0 @@ | |||
| import { Creature } from "../creature" | |||
| import { DamageType, Vigor, Side, StatDamageFormula, Stat } from '@/game/combat' | |||
| import { MalePronouns, ImproperNoun, FemalePronouns, TheyPronouns, Verb } from '@/game/language' | |||
| import { Stomach, anyVore } from '@/game/vore' | |||
| import { AttackAction } from '@/game/combat/actions' | |||
| import { VoreAI } from '@/game/ai' | |||
| import { RavenousPerk } from '@/game/combat/perks' | |||
| export class Wolf extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ImproperNoun('wolf', 'wolves'), | |||
| new ImproperNoun('wolf', 'wolves'), | |||
| [MalePronouns, FemalePronouns, TheyPronouns][Math.floor(Math.random() * 3)], | |||
| { Toughness: 15, Power: 20, Reflexes: 25, Agility: 25, Willpower: 5, Charm: 10 }, | |||
| anyVore, | |||
| anyVore, | |||
| 25 | |||
| ) | |||
| this.side = Side.Monsters | |||
| this.ai = new VoreAI() | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Pierce }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush } | |||
| ]), | |||
| new Verb("bite") | |||
| ) | |||
| ) | |||
| const stomach = new Stomach(this, 2, new StatDamageFormula([ | |||
| { fraction: 3, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 2, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Charm, target: Vigor.Resolve, type: DamageType.Dominance } | |||
| ])) | |||
| this.containers.push(stomach) | |||
| } | |||
| } | |||
| export class DireWolf extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ImproperNoun('dire wolf', 'dire wolves'), | |||
| new ImproperNoun('wolf', 'wolves'), | |||
| [MalePronouns, FemalePronouns, TheyPronouns][Math.floor(Math.random() * 3)], | |||
| { Toughness: 25, Power: 40, Reflexes: 35, Agility: 45, Willpower: 15, Charm: 20 }, | |||
| anyVore, | |||
| anyVore, | |||
| 75 | |||
| ) | |||
| this.side = Side.Monsters | |||
| this.ai = new VoreAI() | |||
| this.perks.push(new RavenousPerk()) | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Pierce }, | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush } | |||
| ]), | |||
| new Verb("bite") | |||
| ) | |||
| ) | |||
| const stomach = new Stomach(this, 2, new StatDamageFormula([ | |||
| { fraction: 3, stat: Stat.Toughness, target: Vigor.Health, type: DamageType.Acid }, | |||
| { fraction: 2, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush }, | |||
| { fraction: 1, stat: Stat.Charm, target: Vigor.Resolve, type: DamageType.Dominance } | |||
| ])) | |||
| this.containers.push(stomach) | |||
| } | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| import { TargetDrainedVigorCondition } from '@/game/combat/conditions' | |||
| import { Creature } from '@/game/creature' | |||
| import { LogEntry, LogLines, nilLog } from "@/game/interface" | |||
| import { VoreContainer } from '@/game/vore' | |||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
| abstract class Relay<Sender, EventMap extends { [name: string]: any }> { | |||
| private subscriptions: { [K in keyof EventMap]: Array<(sender: Sender, args: EventMap[K]) => LogEntry> } | |||
| constructor (private eventNames: Array<keyof EventMap>) { | |||
| const partialSubscriptions: Partial<{ [K in keyof EventMap]?: Array<(sender: Sender, args: EventMap[K]) => LogEntry>}> = {} | |||
| this.eventNames.forEach(name => { | |||
| partialSubscriptions[name] = [] | |||
| }) | |||
| this.subscriptions = partialSubscriptions as Required<{ [K in keyof EventMap]: Array<(sender: Sender, args: EventMap[K]) => LogEntry> }> | |||
| } | |||
| subscribe<K extends keyof EventMap> (name: K, callback: (sender: Sender, args: EventMap[K]) => LogEntry): void { | |||
| if (this.subscriptions[name] === undefined) { | |||
| this.subscriptions[name] = [] | |||
| } | |||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |||
| this.subscriptions[name]!.push(callback) | |||
| } | |||
| dispatch<K extends keyof EventMap> (name: K, sender: Sender, args: EventMap[K]): LogEntry { | |||
| const subscriptionList = this.subscriptions[name] | |||
| if (subscriptionList !== undefined) { | |||
| return new LogLines(...subscriptionList.map(sub => sub(sender, args))) | |||
| } else { | |||
| return nilLog | |||
| } | |||
| } | |||
| connect<ParentEventMap extends EventMap> (parent: Relay<Sender, ParentEventMap>): void { | |||
| this.eventNames.forEach(name => { | |||
| parent.subscribe(name, (sender, args) => this.dispatch(name, sender, args)) | |||
| }) | |||
| } | |||
| } | |||
| type VoreMap = { | |||
| "onEaten": { prey: Creature }; | |||
| "onReleased": { prey: Creature }; | |||
| "onDigested": { prey: Creature }; | |||
| "onAbsorbed": { prey: Creature }; | |||
| } | |||
| export class VoreRelay extends Relay<VoreContainer, VoreMap> { | |||
| constructor () { | |||
| super(["onEaten", "onReleased", "onDigested", "onAbsorbed"]) | |||
| } | |||
| } | |||
| @@ -120,112 +120,10 @@ export const Town = (): Place => { | |||
| "The center of town" | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a wolf", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched a wolf", | |||
| intro: () => new LogLine(`You punched a wolf. The wolf is angry.`) | |||
| }, | |||
| [executor, new Creatures.Wolf()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a dire wolf", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched a dire wolf", | |||
| intro: () => new LogLine(`You punched a dire wolf. The wolf is angry.`) | |||
| }, | |||
| [executor, new Creatures.DireWolf()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a werewolf", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched a werewolf", | |||
| intro: () => new LogLine(`You punched a werewolf. The werewolf is angry.`) | |||
| }, | |||
| [executor, new Creatures.Werewolf()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a dragon", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched a dragon", | |||
| intro: () => new LogLine(`You punched a dragon. The dragon is angry.`) | |||
| }, | |||
| [executor, new Creatures.Dragon()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight Taluthus", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched Taluthus", | |||
| intro: () => new LogLine(`You punched Tal.`) | |||
| }, | |||
| [executor, new Creatures.Taluthus()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| const bossEncounters = [ | |||
| new Encounter( | |||
| { name: "Withers & Kenzie", intro: () => nilLog }, | |||
| makeParty().concat([new Creatures.Withers(), new Creatures.Kenzie()]) | |||
| ), | |||
| new Encounter( | |||
| { name: "Goldeneye", intro: () => nilLog }, | |||
| makeParty().concat([new Creatures.Goldeneye()]) | |||
| ), | |||
| new Encounter( | |||
| { name: "Large Wah", intro: () => nilLog }, | |||
| makeParty().concat([new Creatures.Shingo()]) | |||
| ), | |||
| new Encounter( | |||
| { name: "Cafat", intro: () => nilLog }, | |||
| makeParty().concat([new Creatures.Cafat()]) | |||
| { name: "Inazuma", intro: () => nilLog }, | |||
| makeParty().concat([new Creatures.Inazuma()]) | |||
| ) | |||
| ] | |||
| @@ -296,9 +194,9 @@ export const Town = (): Place => { | |||
| (world) => { | |||
| const enemy = new Creatures.Human(new ProperNoun("Nerd"), TheyPronouns) | |||
| enemy.side = Side.Monsters | |||
| enemy.ai = new VoreAI() | |||
| enemy.ai = new VoreAI(enemy) | |||
| enemy.equip(new Items.Sword(), Items.EquipmentSlot.MainHand) | |||
| enemy.perks.push(new DeliciousPerk()) | |||
| enemy.addPerk(new DeliciousPerk()) | |||
| const encounter = new Encounter( | |||
| { | |||
| name: "Fight some tasty nerd", | |||
| @@ -319,7 +217,7 @@ export const Town = (): Place => { | |||
| (world) => { | |||
| const ally = new Creatures.Human(new ProperNoun("Ally"), TheyPronouns) | |||
| ally.side = Side.Heroes | |||
| ally.ai = new VoreAI() | |||
| ally.ai = new VoreAI(ally) | |||
| ally.equip(new Items.Sword(), Items.EquipmentSlot.MainHand) | |||
| world.party.push(ally) | |||
| @@ -328,24 +226,6 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| square.choices.push( | |||
| new Choice( | |||
| "Fight Geta", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched Geta", | |||
| intro: () => new LogLine(`You punched Geta. Geta is angry.`) | |||
| }, | |||
| [executor, new Creatures.Geta()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| square.choices.push( | |||
| new Choice( | |||
| "Buy a shiny rock", | |||
| @@ -365,26 +245,6 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| alley.choices.push( | |||
| new Choice( | |||
| "Kuro", | |||
| "Get eaten by a Luxray", | |||
| (world) => { | |||
| const enemy = new Creatures.Kuro() | |||
| enemy.ai = new VoreAI() | |||
| const encounter = new Encounter( | |||
| { | |||
| name: "Luxray time", | |||
| intro: () => new LogLine(`Luxray time!`) | |||
| }, | |||
| [world.player, enemy] | |||
| ) | |||
| world.encounter = encounter | |||
| return nilLog | |||
| } | |||
| ) | |||
| ) | |||
| bossEncounters.forEach(encounter => { | |||
| bosses.choices.push( | |||
| new Choice( | |||
| @@ -4,6 +4,7 @@ import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition } from '@/game/ | |||
| import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from '@/game/combat/actions' | |||
| import * as Words from '@/game/words' | |||
| import { Creature } from '@/game/creature' | |||
| import { VoreRelay } from '@/game/events' | |||
| export enum VoreType { | |||
| Oral = "Oral Vore", | |||
| @@ -164,6 +165,8 @@ export abstract class InnerContainer extends NormalContainer { | |||
| } | |||
| export interface VoreContainer extends Container { | |||
| voreRelay: VoreRelay; | |||
| digested: Array<Creature>; | |||
| tick: (dt: number, victims?: Array<Creature>) => LogEntry; | |||
| digest: (preys: Creature[]) => LogEntry; | |||
| @@ -177,6 +180,8 @@ export interface VoreContainer extends Container { | |||
| } | |||
| export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer { | |||
| voreRelay = new VoreRelay() | |||
| consumeVerb = new Verb('devour') | |||
| consumePreposition = new Preposition("into") | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| @@ -302,15 +307,20 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| const logLine = super.consume(prey) | |||
| const results: Array<LogEntry> = [] | |||
| this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this))) | |||
| return new LogLines(...[logLine].concat(results)) | |||
| const relayResults = this.voreRelay.dispatch("onEaten", this, { prey: prey }) | |||
| return new LogLines(...[logLine].concat(results).concat(relayResults)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| return new LogLines(super.release(prey), this.voreRelay.dispatch("onReleased", this, { prey: prey })) | |||
| } | |||
| onAbsorb (prey: Creature): LogEntry { | |||
| return nilLog | |||
| return this.voreRelay.dispatch("onAbsorbed", this, { prey: prey }) | |||
| } | |||
| onDigest (prey: Creature): LogEntry { | |||
| return nilLog | |||
| return this.voreRelay.dispatch("onDigested", this, { prey: prey }) | |||
| } | |||
| } | |||