| @@ -145,6 +145,10 @@ a { | |||
| font-weight: bold; | |||
| } | |||
| .damage-instance { | |||
| white-space: nowrap; | |||
| } | |||
| #log > div { | |||
| color: #888; | |||
| padding-top: 8pt; | |||
| @@ -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<Entity, Entity> = 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<Entity, Entity> = 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<Entity, Entity> = new POVPair([]) | |||
| protected lines: POVPairArgs<Entity, Entity, { from: Container; to: Container }> = 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 }) | |||
| } | |||
| } | |||
| @@ -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 })) | |||
| @@ -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) | |||
| @@ -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" | |||
| @@ -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) { | |||
| } | |||
| @@ -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<Pred, Prey> | |||
| abstract releaseLines: POVPair<Pred, Prey> | |||
| abstract struggleLines: POVPair<Prey, Pred> | |||
| abstract tickLines: POVSolo<Pred> | |||
| abstract tickLines: POVPairArgs<Pred, Prey, { damage: Damage }> | |||
| abstract digestLines: POVPair<Pred, Prey> | |||
| abstract absorbLines: POVPair<Pred, Prey> | |||
| abstract disposeLines: POVPair<Pred, Prey> | |||
| @@ -85,9 +85,11 @@ abstract class NormalContainer implements Container { | |||
| const digested: Array<Prey> = [] | |||
| const absorbed: Array<Prey> = [] | |||
| 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<Pred, Prey, { damage: Damage }>([ | |||
| [[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<Pred, Prey, { damage: Damage }>([ | |||
| [[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([ | |||