| @@ -498,6 +498,13 @@ export class Effective { | |||
| log: nilLog | |||
| } | |||
| } | |||
| /** | |||
| * Changes a creature's size. This represents the change in *mass* | |||
| */ | |||
| scale (scale: number): number { | |||
| return scale | |||
| } | |||
| } | |||
| /** | |||
| * A displayable status effect | |||
| @@ -1,11 +1,11 @@ | |||
| import { StatTest, StatVigorTest } from './tests' | |||
| import { StatTest, StatVigorTest, StatVigorSizeTest } from './tests' | |||
| import { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language' | |||
| import { Entity } from '../entity' | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageFormula, Stat, Vigor, Action, Condition } from '../combat' | |||
| import { LogLine, LogLines, LogEntry, nilLog } from '../interface' | |||
| import { VoreContainer, Container } from '../vore' | |||
| import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition } from './conditions' | |||
| import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from './conditions' | |||
| /** | |||
| * The PassAction has no effect. | |||
| @@ -85,15 +85,15 @@ export class AttackAction extends DamageAction { | |||
| * Devours the target. | |||
| */ | |||
| export class DevourAction extends Action { | |||
| private test: StatVigorTest | |||
| private test: StatVigorSizeTest | |||
| constructor (protected container: Container) { | |||
| super( | |||
| new DynText(new LiveText(container, x => x.consumeVerb.capital), ' (', new LiveText(container, x => x.name.all), ')'), | |||
| new LiveText(container, x => `Try to ${x.consumeVerb} your foe`), | |||
| [new CapableCondition(), new TogetherCondition()] | |||
| [new CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)] | |||
| ) | |||
| this.test = new StatVigorTest(Stat.Power) | |||
| this.test = new StatVigorSizeTest(Stat.Power) | |||
| } | |||
| allowed (user: Creature, target: Creature): boolean { | |||
| @@ -171,7 +171,7 @@ export class FeedAction extends Action { | |||
| * Tries to escape from the target's container | |||
| */ | |||
| export class StruggleAction extends Action { | |||
| private test: StatVigorTest | |||
| private test: StatVigorSizeTest | |||
| constructor (public container: Container) { | |||
| super( | |||
| @@ -179,7 +179,7 @@ export class StruggleAction extends Action { | |||
| 'Try to escape from your foe', | |||
| [new CapableCondition(), new PairCondition(), new ContainedByCondition(container)] | |||
| ) | |||
| this.test = new StatVigorTest(Stat.Power) | |||
| this.test = new StatVigorSizeTest(Stat.Power) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| @@ -56,6 +56,16 @@ export class TogetherCondition implements Condition { | |||
| } | |||
| } | |||
| export class HasRoomCondition implements Condition { | |||
| constructor (private container: Container) { | |||
| } | |||
| allowed (user: Creature, target: Creature): boolean { | |||
| return this.container.capacity > this.container.fullness + target.voreStats.Bulk | |||
| } | |||
| } | |||
| export class ContainedByCondition implements Condition { | |||
| constructor (private container: Container) { | |||
| @@ -159,3 +159,17 @@ export class SurrenderEffect extends StatusEffect { | |||
| } | |||
| } | |||
| } | |||
| export class SizeEffect extends StatusEffect { | |||
| constructor (private change: number) { | |||
| super('Size-Shifted', 'This creature has changed in size', 'fas fa-ruler') | |||
| } | |||
| onApply (creature: Creature): LogLine { | |||
| return new LogLine(`Smol`) | |||
| } | |||
| scale (scale: number): number { | |||
| return scale * this.change | |||
| } | |||
| } | |||
| @@ -28,6 +28,82 @@ abstract class RandomTest implements CombatTest { | |||
| abstract explain(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class StatVigorSizeTest extends RandomTest { | |||
| private f: (x: number) => number | |||
| private k = 0.1 | |||
| constructor (public readonly stat: Stat, private bias = 0) { | |||
| super() | |||
| this.f = logistic(0, 1, this.k) | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| let userPercent = 1 | |||
| let targetPercent = 1 | |||
| Object.keys(Vigor).forEach(key => { | |||
| userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor]) | |||
| targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor]) | |||
| userPercent = Math.max(0, userPercent) | |||
| targetPercent = Math.max(0, targetPercent) | |||
| }) | |||
| if (userPercent === 0) { | |||
| targetPercent *= 4 | |||
| } | |||
| if (targetPercent === 0) { | |||
| userPercent *= 4 | |||
| } | |||
| userPercent *= Math.sqrt(user.voreStats.Mass) | |||
| targetPercent *= Math.sqrt(target.voreStats.Mass) | |||
| return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| let result: LogEntry | |||
| let userPercent = 1 | |||
| let targetPercent = 1 | |||
| Object.keys(Vigor).forEach(key => { | |||
| userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor] | |||
| targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor] | |||
| userPercent = Math.max(0, userPercent) | |||
| targetPercent = Math.max(0, targetPercent) | |||
| }) | |||
| if (userPercent === 0) { | |||
| targetPercent *= 4 | |||
| } | |||
| if (targetPercent === 0) { | |||
| userPercent *= 4 | |||
| } | |||
| userPercent *= Math.sqrt(user.voreStats.Mass) | |||
| targetPercent *= Math.sqrt(target.voreStats.Mass) | |||
| const userMod = user.stats[this.stat] * userPercent | |||
| const targetMod = target.stats[this.stat] * targetPercent | |||
| const delta = userMod - targetMod | |||
| if (delta === 0) { | |||
| result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.') | |||
| } else if (delta < 0) { | |||
| result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.') | |||
| } else { | |||
| result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.') | |||
| } | |||
| result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%') | |||
| return result | |||
| } | |||
| } | |||
| export class StatVigorTest extends RandomTest { | |||
| private f: (x: number) => number | |||
| private k = 0.1 | |||
| @@ -1,4 +1,4 @@ | |||
| import { Damage, Combatant, Stats, Action, Vigor, Side, GroupAction, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective } from './combat' | |||
| import { Damage, Combatant, Stats, Action, Vigor, Side, GroupAction, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat } from './combat' | |||
| import { Noun, Pronoun } from './language' | |||
| import { LogEntry, LogLines } from './interface' | |||
| import { Vore, VoreContainer, VoreType } from './vore' | |||
| @@ -33,10 +33,28 @@ export class Creature extends Vore implements Combatant { | |||
| this.actions.push(new PassAction()) | |||
| this.side = Side.Heroes | |||
| const baseVoreStats = this.voreStats | |||
| /* eslint-disable-next-line */ | |||
| const self = this | |||
| this.voreStats = { | |||
| get [VoreStat.Bulk] () { | |||
| return baseVoreStats.Bulk | |||
| }, | |||
| get [VoreStat.Mass] () { | |||
| const base = baseVoreStats.Mass | |||
| const adjusted = self.effects.reduce((scale: number, effect: Effective) => effect.scale(scale), base) | |||
| return adjusted | |||
| }, | |||
| get [VoreStat.PreyCount] () { | |||
| return baseVoreStats["Prey Count"] | |||
| } | |||
| } | |||
| } | |||
| applyEffect (effect: StatusEffect): LogEntry { | |||
| this.effects.push(effect) | |||
| this.statusEffects.push(effect) | |||
| return effect.onApply(this) | |||
| } | |||
| @@ -8,5 +8,6 @@ import { Dragon } from './creatures/dragon' | |||
| import { Shingo } from './creatures/shingo' | |||
| import { Goldeneye } from './creatures/goldeneye' | |||
| import { Kuro } from './creatures/kuro' | |||
| import { Geta } from './creatures/geta' | |||
| export { Wolf, Player, Cafat, Human, Withers, Kenzie, Dragon, Shingo, Goldeneye, Kuro } | |||
| export { Wolf, Player, Cafat, Human, Withers, Kenzie, Dragon, Shingo, Goldeneye, Kuro, Geta } | |||
| @@ -0,0 +1,77 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side, CompositionAction } from '../combat' | |||
| import { MalePronouns, ImproperNoun, ProperNoun, ObjectPronouns, FemalePronouns, TheyPronouns } from '../language' | |||
| import { VoreType, Stomach, Bowels, Cock, Balls, anyVore, Slit, Womb, biconnectContainers } from '../vore' | |||
| import { AttackAction, TransferAction, FeedAction } from '../combat/actions' | |||
| import { StatusConsequence, LogConsequence, DamageConsequence } from '../combat/consequences' | |||
| import { SizeEffect, DamageTypeResistanceEffect } from '../combat/effects' | |||
| import { LogLine } from '../interface' | |||
| export class Geta extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ProperNoun('Geta'), | |||
| new ImproperNoun('fox', 'foxes'), | |||
| MalePronouns, | |||
| { Toughness: 10, Power: 10, Speed: 30, Willpower: 15, Charm: 40 }, | |||
| new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), | |||
| new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), | |||
| 100 | |||
| ) | |||
| this.side = Side.Monsters | |||
| const stomach = new Stomach(this, 10, 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, 10, 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, 5, 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, 10, 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) | |||
| this.actions.push( | |||
| new CompositionAction( | |||
| "Shrink", | |||
| "Zap!", | |||
| { | |||
| consequences: [ | |||
| new LogConsequence( | |||
| (user, target) => new LogLine(`ZAP!`) | |||
| ), | |||
| new StatusConsequence( | |||
| () => new SizeEffect(0.25) | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| @@ -113,6 +113,24 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight Geta", | |||
| "yolo", | |||
| (world, executor) => { | |||
| world.encounter = new Encounter( | |||
| { | |||
| name: "You punched Geta", | |||
| intro: (world: World) => new LogLine(`You punched Geta. Geta is angry.`) | |||
| }, | |||
| [executor, new Creatures.Geta()] | |||
| ) | |||
| return new LogLine(`FIGHT TIME`) | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a dragon", | |||
| @@ -52,7 +52,7 @@ export abstract class Vore extends Mortal { | |||
| 0 | |||
| ) | |||
| }, | |||
| this.Mass | |||
| self.voreStats.Mass | |||
| ) | |||
| }, | |||
| get [VoreStat.Mass] () { | |||