diff --git a/src/components/Combat.vue b/src/components/Combat.vue index 4ca0576..8166ad4 100644 --- a/src/components/Combat.vue +++ b/src/components/Combat.vue @@ -145,6 +145,10 @@ a { font-weight: bold; } +.damage-instance { + white-space: nowrap; +} + #log > div { color: #888; padding-top: 8pt; diff --git a/src/game/combat.ts b/src/game/combat.ts index a30d307..d8c0bb9 100644 --- a/src/game/combat.ts +++ b/src/game/combat.ts @@ -1,8 +1,50 @@ import { Creature, POV, Entity } from './entity' import { POVPair, POVPairArgs } from './language' import { Container } from './vore' -import { LogEntry, LogLines, CompositeLog, FAElem, LogLine } from './interface' +import { LogEntry, LogLines, CompositeLog, FAElem, LogLine, FormatEntry, FormatOpt } from './interface' +export enum DamageType { + Pierce = "Pierce", + Slash = "Slash", + Crush = "Crush", + Acid = "Acid", + Seduction = "Seduction", + Dominance = "Dominance" +} + +export interface DamageInstance { + type: DamageType; + amount: number; + target: Vigor; +} + +export enum Vigor { + Health = "Health", + Stamina = "Stamina", + Willpower = "Willpower" +} + +export const VigorIcons: {[key in Vigor]: string} = { + [Vigor.Health]: "fas fa-heart", + [Vigor.Stamina]: "fas fa-bolt", + [Vigor.Willpower]: "fas fa-brain" +} + +export type Vigors = {[key in Vigor]: number} + +export enum Stat { + STR = 'Strength', + DEX = 'Dexterity', + CON = 'Constitution' +} + +export type Stats = {[key in Stat]: number} + +export const StatIcons: {[key in Stat]: string} = { + [Stat.STR]: 'fas fa-fist-raised', + [Stat.DEX]: 'fas fa-feather', + [Stat.CON]: 'fas fa-heartbeat' +} export interface CombatTest { test: (user: Creature, target: Creature) => boolean; odds: (user: Creature, target: Creature) => number; @@ -24,7 +66,7 @@ abstract class RandomTest implements CombatTest { abstract explain(user: Creature, target: Creature): LogEntry } -export class StatTest extends RandomTest { +export class StatVigorTest extends RandomTest { private f: (x: number) => number constructor (public readonly stat: Stat, k = 0.1) { @@ -33,7 +75,27 @@ export class StatTest extends RandomTest { } odds (user: Creature, target: Creature): number { - return this.f(user.stats[this.stat] - target.stats[this.stat]) + 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 + } + + console.log(userPercent, targetPercent, this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)) + return this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) } explain (user: Creature, target: Creature): LogEntry { @@ -54,61 +116,48 @@ export class StatTest extends RandomTest { } } -export class ChanceTest extends RandomTest { - constructor (public readonly chance: number) { +export class StatTest extends RandomTest { + private f: (x: number) => number + + constructor (public readonly stat: Stat, k = 0.1) { super() + this.f = logistic(0, 1, k) } odds (user: Creature, target: Creature): number { - return this.chance + return this.f(user.stats[this.stat] - target.stats[this.stat]) } explain (user: Creature, target: Creature): LogEntry { - return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.') - } -} - -export enum DamageType { - Pierce = "Pierce", - Slash = "Slash", - Crush = "Crush", - Acid = "Acid", - Seduction = "Seduction", - Dominance = "Dominance" -} + const delta: number = user.stats[this.stat] - target.stats[this.stat] + let result: string -export interface DamageInstance { - type: DamageType; - amount: number; - target: Vigor; -} + if (delta === 0) { + result = 'You and the target have the same ' + this.stat + '.' + } else if (delta < 0) { + result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.' + } else { + result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.' + } -export enum Vigor { - Health = "Health", - Stamina = "Stamina", - Willpower = "Willpower" -} + result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%' -export const VigorIcons: {[key in Vigor]: string} = { - [Vigor.Health]: "fas fa-heart", - [Vigor.Stamina]: "fas fa-bolt", - [Vigor.Willpower]: "fas fa-brain" + return new LogLines(result) + } } -export type Vigors = {[key in Vigor]: number} - -export enum Stat { - STR = 'Strength', - DEX = 'Dexterity', - CON = 'Constitution' -} +export class ChanceTest extends RandomTest { + constructor (public readonly chance: number) { + super() + } -export type Stats = {[key in Stat]: number} + odds (user: Creature, target: Creature): number { + return this.chance + } -export const StatIcons: {[key in Stat]: string} = { - [Stat.STR]: 'fas fa-fist-raised', - [Stat.DEX]: 'fas fa-feather', - [Stat.CON]: 'fas fa-heartbeat' + explain (user: Creature, target: Creature): LogEntry { + return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.') + } } export class Damage { @@ -149,7 +198,7 @@ export class Damage { totals[instance.target] += instance.amount }) - return new LogLine(...Object.keys(Vigor).flatMap(key => totals[key as Vigor] === 0 ? [] : [totals[key as Vigor].toString(), new FAElem(VigorIcons[key as Vigor])])) + return new FormatEntry(new LogLine(...Object.keys(Vigor).flatMap(key => totals[key as Vigor] === 0 ? [] : [totals[key as Vigor].toFixed(0).toString(), new FAElem(VigorIcons[key as Vigor])])), FormatOpt.DamageInst) } } @@ -276,7 +325,7 @@ export class AttackAction extends TogetherAction { } export class DevourAction extends TogetherAction { - private test: StatTest + private test: StatVigorTest protected failLines: POVPair = new POVPair([ [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to make a meal out of ${target.name}`)], @@ -299,7 +348,7 @@ export class DevourAction extends TogetherAction { constructor (protected container: Container) { super('Devour', 'Try to consume your foe', [new CapableCondition()]) this.name += ` (${container.name})` - this.test = new StatTest(Stat.STR) + this.test = new StatVigorTest(Stat.STR) } execute (user: Creature, target: Creature): LogEntry { @@ -348,7 +397,7 @@ export class FeedAction extends TogetherAction { } export class StruggleAction extends PairAction { - private test: StatTest + private test: StatVigorTest protected failLines: POVPair = new POVPair([ [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to escape from ${target.name}`)], @@ -366,7 +415,7 @@ export class StruggleAction extends PairAction { constructor (public container: Container) { super('Struggle', 'Try to escape your predator', [new CapableCondition()]) - this.test = new StatTest(Stat.STR) + this.test = new StatVigorTest(Stat.STR) } execute (user: Creature, target: Creature): LogEntry { @@ -406,7 +455,7 @@ export class DigestAction extends SelfAction { export class ReleaseAction extends PairAction { allowed (user: Creature, target: Creature) { - if (target.containedIn === this.container) { + if (target.containedIn === this.container && this.container.contents.indexOf(target) >= 0) { return super.allowed(user, target) } else { return false @@ -424,7 +473,11 @@ export class ReleaseAction extends PairAction { } export class TransferAction extends PairAction { - protected lines: POVPair = new POVPair([]) + protected lines: POVPairArgs = new POVPairArgs([ + [[POV.First, POV.Third], (user, target, args) => new LogLine(`You squeeze ${target.name} from your ${args.from.name} to your ${args.to.name}`)], + [[POV.Third, POV.First], (user, target, args) => new LogLine(`You're squeezed from ${user.name}'s ${args.from.name} to ${target.pronouns.possessive} ${args.to.name}`)], + [[POV.Third, POV.Third], (user, target, args) => new LogLine(`${user.name} squeezes ${target.name} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}`)] + ]) allowed (user: Creature, target: Creature) { if (target.containedIn === this.from) { @@ -441,6 +494,6 @@ export class TransferAction extends PairAction { execute (user: Creature, target: Creature): LogEntry { this.from.release(target) this.to.consume(target) - return this.lines.run(user, target) + return this.lines.run(user, target, { from: this.from, to: this.to }) } } diff --git a/src/game/creatures/player.ts b/src/game/creatures/player.ts index 04775a9..f70cc4b 100644 --- a/src/game/creatures/player.ts +++ b/src/game/creatures/player.ts @@ -5,11 +5,11 @@ import { Stomach, Bowels, VoreType } from '../vore' export class Player extends Creature { constructor () { - super(new ProperNoun('The Dude'), TheyPronouns, { [Stat.STR]: 20, [Stat.DEX]: 20, [Stat.CON]: 20 }, new Set([VoreType.Oral]), new Set([VoreType.Oral, VoreType.Anal]), 50) + super(new ProperNoun('The Dude'), TheyPronouns, { [Stat.STR]: 20, [Stat.DEX]: 20, [Stat.CON]: 20 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 50) this.actions.push(new AttackAction(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina }))) - const stomach = new Stomach(this, 100, new Damage({ amount: 100000000000, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health })) + const stomach = new Stomach(this, 100, 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, 100, new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health })) diff --git a/src/game/creatures/wolf.ts b/src/game/creatures/wolf.ts index b8ecd11..5f56292 100644 --- a/src/game/creatures/wolf.ts +++ b/src/game/creatures/wolf.ts @@ -42,14 +42,23 @@ class HypnoAction extends AttackAction { export class Wolf extends Creature { constructor () { - super(new ImproperNoun('wolf', 'wolves'), MalePronouns, { [Stat.STR]: 15, [Stat.DEX]: 15, [Stat.CON]: 25 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral]), 25) + super(new ImproperNoun('wolf', 'wolves'), MalePronouns, { [Stat.STR]: 15, [Stat.DEX]: 15, [Stat.CON]: 25 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 25) this.actions.push(new BiteAction()) this.actions.push(new HypnoAction()) - const stomach = new Stomach(this, 50, new Damage({ amount: 50, type: DamageType.Acid, target: Vigor.Health }, { amount: 500, type: DamageType.Crush, target: Vigor.Health })) + const stomach = new Stomach(this, 50, 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.Willpower } + )) this.containers.push(stomach) - const bowels = new Bowels(this, 50, new Damage({ amount: 50, type: DamageType.Acid, target: Vigor.Health }, { amount: 500, type: DamageType.Crush, target: Vigor.Health })) + + const bowels = new Bowels(this, 50, new Damage( + { amount: 10, type: DamageType.Crush, target: Vigor.Health }, + { amount: 25, type: DamageType.Crush, target: Vigor.Stamina }, + { amount: 25, type: DamageType.Dominance, target: Vigor.Willpower } + )) this.containers.push(bowels) diff --git a/src/game/entity.ts b/src/game/entity.ts index 96d10cc..a97535e 100644 --- a/src/game/entity.ts +++ b/src/game/entity.ts @@ -71,14 +71,23 @@ export class Creature implements Mortal, Pred, Prey, Combatant { } get status (): string { - if (this.vigors[Vigor.Health] < 0) { - return "DEAD" + if (this.vigors[Vigor.Health] <= -100) { + return "Dead" } - if (this.vigors[Vigor.Stamina] < 0) { + if (this.vigors[Vigor.Stamina] <= -100) { return "Unconscious" } - if (this.vigors[Vigor.Willpower] < 0) { - return "Too horny" + if (this.vigors[Vigor.Willpower] <= -100) { + return "Broken" + } + if (this.vigors[Vigor.Health] <= 0) { + return "Unconscious" + } + if (this.vigors[Vigor.Stamina] <= 0) { + return "Exhausted" + } + if (this.vigors[Vigor.Willpower] <= 0) { + return "Overpowered" } if (this.containedIn !== null) { return "Devoured" diff --git a/src/game/interface.ts b/src/game/interface.ts index 05f3501..7fcd349 100644 --- a/src/game/interface.ts +++ b/src/game/interface.ts @@ -19,11 +19,30 @@ export class LogLines implements LogEntry { } export enum FormatOpt { - Damage = "log-damage" + Damage = "log-damage", + DamageInst = "damage-instance" +} + +export class FormatEntry implements LogEntry { + constructor (private entry: LogEntry, private opt: FormatOpt) { + + } + + render (): HTMLElement[] { + const span = document.createElement("span") + + this.entry.render().forEach(elem => { + span.appendChild(elem) + }) + + span.classList.add(this.opt) + + return [span] + } } export class FormatText implements LogEntry { - constructor (private line: string, private opt: FormatOpt) { + constructor (private opt: FormatOpt, private line: string) { } diff --git a/src/game/vore.ts b/src/game/vore.ts index 70b326d..7f64a19 100644 --- a/src/game/vore.ts +++ b/src/game/vore.ts @@ -1,6 +1,6 @@ import { Entity, Mortal, POV } from './entity' import { Damage, Actionable, Action, DevourAction, FeedAction, DigestAction, ReleaseAction, StruggleAction, Vigor } from './combat' -import { LogLines, LogEntry, CompositeLog } from './interface' +import { LogLines, LogEntry, CompositeLog, LogLine } from './interface' import { POVSolo, POVPair, POVPairArgs } from './language' export enum VoreType { @@ -46,7 +46,7 @@ abstract class NormalContainer implements Container { abstract consumeLines: POVPair abstract releaseLines: POVPair abstract struggleLines: POVPair - abstract tickLines: POVSolo + abstract tickLines: POVPairArgs abstract digestLines: POVPair abstract absorbLines: POVPair abstract disposeLines: POVPair @@ -85,9 +85,11 @@ abstract class NormalContainer implements Container { const digested: Array = [] const absorbed: Array = [] + const scaled = this.damage.scale(dt / 60) + this.contents.forEach(prey => { const start = prey.vigors[Vigor.Health] - prey.takeDamage(this.damage.scale(dt / 3600)) + prey.takeDamage(scaled) const end = prey.vigors[Vigor.Health] if (start > 0 && end <= 0) { @@ -99,6 +101,7 @@ abstract class NormalContainer implements Container { } }) + const tickedEntries = new CompositeLog(...this.contents.map(prey => this.tickLines.run(this.owner, prey, { damage: scaled }))) const digestedEntries = new CompositeLog(...digested.map(prey => this.digest(prey))) const absorbedEntries = new CompositeLog(...absorbed.map(prey => this.absorb(prey))) @@ -106,7 +109,7 @@ abstract class NormalContainer implements Container { return prey.vigors[Vigor.Health] > -100 }) - return new CompositeLog(this.tickLines.run(this.owner), digestedEntries, absorbedEntries) + return new CompositeLog(tickedEntries, digestedEntries, absorbedEntries) } describe (): LogEntry { @@ -168,9 +171,10 @@ export class Stomach extends NormalContainer { [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} escapes from the gut of ${target.name}`)] ]) - tickLines = new POVSolo([ - [[POV.First], (user) => new LogLines(`Your stomach gurgles and churns`)], - [[POV.Third], (user) => new LogLines(`${user.name.capital}'s gut snarls and gurgles`)] + tickLines = new POVPairArgs([ + [[POV.First, POV.Third], (user, target, args) => new LogLine(`Your stomach gurgles ${target.name} for `, args.damage.renderShort())], + [[POV.Third, POV.First], (user, target, args) => new LogLine(`${user.name.capital}'s stomach churns you for `, args.damage.renderShort())], + [[POV.Third, POV.Third], (user, target, args) => new LogLine(`${target.name.capital} churns ${user.name} for `, args.damage.renderShort())] ]) digestLines = new POVPair([ @@ -198,32 +202,33 @@ export class Bowels extends NormalContainer { } consumeLines = new POVPair([ - [[POV.First, POV.Third], (user, target) => new LogLines(`You devour ${target.name}`)], - [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} munches you`)], - [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} munches ${target.name.capital}`)] + [[POV.First, POV.Third], (user, target) => new LogLines(`You force ${target.name} into your bowels`)], + [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} works you into ${user.pronouns.possessive} ass`)], + [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} anal-vores ${target.name.capital}`)] ]) releaseLines = new POVPair([ - [[POV.First, POV.Third], (user, target) => new LogLines(`You hork up ${target.name}`)], - [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} horks you up`)], - [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} horks up ${target.name.capital}`)] + [[POV.First, POV.Third], (user, target) => new LogLines(`You let out ${target.name}`)], + [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} lets you out `)], + [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} lets out ${target.name.capital}`)] ]) struggleLines = new POVPair([ [[POV.First, POV.Third], (user, target) => new LogLines(`You claw your way out of ${target.name}`)], - [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} forces ${user.pronouns.possessive} way up your throat!`)], - [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} escapes from the gut of ${target.name}`)] + [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} forces ${user.pronouns.possessive} way out your rump!`)], + [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} escapes from the bowels of ${target.name}`)] ]) - tickLines = new POVSolo([ - [[POV.First], (user) => new LogLines(`Your stomach gurgles and churns!`)], - [[POV.Third], (user) => new LogLines(`${user.name.capital}'s gut snarls and gurgles`)] + tickLines = new POVPairArgs([ + [[POV.First, POV.Third], (user, target, args) => new LogLine(`Your bowels gurgle ${target.name} for `, args.damage.renderShort())], + [[POV.Third, POV.First], (user, target, args) => new LogLine(`${user.name.capital}'s bowels churn you for `, args.damage.renderShort())], + [[POV.Third, POV.Third], (user, target, args) => new LogLine(`${target.name.capital} churns ${user.name} for `, args.damage.renderShort())] ]) digestLines = new POVPair([ - [[POV.First, POV.Third], (user, target) => new LogLines(`Your stomach overwhelms ${target.name}`)], - [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital}'s stomach finishes you off`)], - [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name.capital}'s squirms fade, overwhelmed by the stomach of ${user.name}`)] + [[POV.First, POV.Third], (user, target) => new LogLines(`Your bowels overwhelm ${target.name}`)], + [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital}'s bowels finish you off`)], + [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name.capital}'s squirms fade, overwhelmed by the bowels of ${user.name}`)] ]) absorbLines = new POVPair([