| @@ -498,6 +498,13 @@ export class Effective { | |||||
| log: nilLog | log: nilLog | ||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Changes a creature's size. This represents the change in *mass* | |||||
| */ | |||||
| scale (scale: number): number { | |||||
| return scale | |||||
| } | |||||
| } | } | ||||
| /** | /** | ||||
| * A displayable status effect | * 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 { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language' | ||||
| import { Entity } from '../entity' | import { Entity } from '../entity' | ||||
| import { Creature } from "../creature" | import { Creature } from "../creature" | ||||
| import { Damage, DamageFormula, Stat, Vigor, Action, Condition } from '../combat' | import { Damage, DamageFormula, Stat, Vigor, Action, Condition } from '../combat' | ||||
| import { LogLine, LogLines, LogEntry, nilLog } from '../interface' | import { LogLine, LogLines, LogEntry, nilLog } from '../interface' | ||||
| import { VoreContainer, Container } from '../vore' | 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. | * The PassAction has no effect. | ||||
| @@ -85,15 +85,15 @@ export class AttackAction extends DamageAction { | |||||
| * Devours the target. | * Devours the target. | ||||
| */ | */ | ||||
| export class DevourAction extends Action { | export class DevourAction extends Action { | ||||
| private test: StatVigorTest | |||||
| private test: StatVigorSizeTest | |||||
| constructor (protected container: Container) { | constructor (protected container: Container) { | ||||
| super( | super( | ||||
| new DynText(new LiveText(container, x => x.consumeVerb.capital), ' (', new LiveText(container, x => x.name.all), ')'), | 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 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 { | allowed (user: Creature, target: Creature): boolean { | ||||
| @@ -171,7 +171,7 @@ export class FeedAction extends Action { | |||||
| * Tries to escape from the target's container | * Tries to escape from the target's container | ||||
| */ | */ | ||||
| export class StruggleAction extends Action { | export class StruggleAction extends Action { | ||||
| private test: StatVigorTest | |||||
| private test: StatVigorSizeTest | |||||
| constructor (public container: Container) { | constructor (public container: Container) { | ||||
| super( | super( | ||||
| @@ -179,7 +179,7 @@ export class StruggleAction extends Action { | |||||
| 'Try to escape from your foe', | 'Try to escape from your foe', | ||||
| [new CapableCondition(), new PairCondition(), new ContainedByCondition(container)] | [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 { | 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 { | export class ContainedByCondition implements Condition { | ||||
| constructor (private container: Container) { | 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 | 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 { | export class StatVigorTest extends RandomTest { | ||||
| private f: (x: number) => number | private f: (x: number) => number | ||||
| private k = 0.1 | 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 { Noun, Pronoun } from './language' | ||||
| import { LogEntry, LogLines } from './interface' | import { LogEntry, LogLines } from './interface' | ||||
| import { Vore, VoreContainer, VoreType } from './vore' | import { Vore, VoreContainer, VoreType } from './vore' | ||||
| @@ -33,10 +33,28 @@ export class Creature extends Vore implements Combatant { | |||||
| this.actions.push(new PassAction()) | this.actions.push(new PassAction()) | ||||
| this.side = Side.Heroes | 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 { | applyEffect (effect: StatusEffect): LogEntry { | ||||
| this.effects.push(effect) | |||||
| this.statusEffects.push(effect) | |||||
| return effect.onApply(this) | return effect.onApply(this) | ||||
| } | } | ||||
| @@ -8,5 +8,6 @@ import { Dragon } from './creatures/dragon' | |||||
| import { Shingo } from './creatures/shingo' | import { Shingo } from './creatures/shingo' | ||||
| import { Goldeneye } from './creatures/goldeneye' | import { Goldeneye } from './creatures/goldeneye' | ||||
| import { Kuro } from './creatures/kuro' | 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( | woods.choices.push( | ||||
| new Choice( | new Choice( | ||||
| "Fight a dragon", | "Fight a dragon", | ||||
| @@ -52,7 +52,7 @@ export abstract class Vore extends Mortal { | |||||
| 0 | 0 | ||||
| ) | ) | ||||
| }, | }, | ||||
| this.Mass | |||||
| self.voreStats.Mass | |||||
| ) | ) | ||||
| }, | }, | ||||
| get [VoreStat.Mass] () { | get [VoreStat.Mass] () { | ||||