| @@ -22,8 +22,7 @@ export default class App extends Vue { | |||
| enemy: Creature | |||
| constructor () { | |||
| super() | |||
| this.player = new Creatures.Wolf() | |||
| this.player.perspective = POV.First | |||
| this.player = new Creatures.Player() | |||
| this.enemy = new Creatures.Wolf() | |||
| console.log(this.player) | |||
| console.log(this.enemy) | |||
| @@ -5,20 +5,24 @@ | |||
| <Statblock :subject="player" /> | |||
| <Statblock :subject="enemy" /> | |||
| </div> | |||
| <div id="log"></div> | |||
| <div class="horiz-display"> | |||
| <div> | |||
| <h2>Your moves</h2> | |||
| <button class="combat-button" @mouseleave="actionDescription= ''" @mouseover="actionDescription = action.desc" v-for="action in player.validActions(enemy)" :key="'player-' + action.name" v-on:click="log(action.execute(player, enemy))">{{action.name}}</button> | |||
| <button class="combat-button" @mouseleave="actionDescription= ''" @mouseover="actionDescription = action.desc" v-for="action in player.validActions(player)" :key="'player-' + action.name" v-on:click="log(action.execute(player, player))">{{action.name}}</button> | |||
| <div class="vert-display"> | |||
| <button class="combat-button" @mouseleave="actionDescription= ''" @mouseover="actionDescription = action.desc" v-for="action in player.validActions(enemy)" :key="'player-' + action.name" v-on:click="log(action.execute(player, enemy))">{{action.name}}</button> | |||
| <button class="combat-button" @mouseleave="actionDescription= ''" @mouseover="actionDescription = action.desc" v-for="action in player.validActions(player)" :key="'player-' + action.name" v-on:click="log(action.execute(player, player))">{{action.name}}</button> | |||
| </div> | |||
| <div>{{actionDescription}}</div> | |||
| </div> | |||
| <div> | |||
| <h2>Enemy moves</h2> | |||
| <button class="combat-button" v-for="action in enemy.validActions(player)" :key="'enemy-' + action.name" v-on:click="log(action.execute(enemy, player))">{{action.name}}</button> | |||
| <button class="combat-button" v-for="action in enemy.validActions(enemy)" :key="'enemy-' + action.name" v-on:click="log(action.execute(enemy, enemy))">{{action.name}}</button> | |||
| <div class="vert-display"> | |||
| <button class="combat-button" v-for="action in enemy.validActions(player)" :key="'enemy-' + action.name" v-on:click="log(action.execute(enemy, player))">{{action.name}}</button> | |||
| <button class="combat-button" v-for="action in enemy.validActions(enemy)" :key="'enemy-' + action.name" v-on:click="log(action.execute(enemy, enemy))">{{action.name}}</button> | |||
| </div> | |||
| </div> | |||
| <div id="log"></div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| @@ -71,8 +75,20 @@ a { | |||
| display: flex; | |||
| justify-content: center; | |||
| } | |||
| .vert-display { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .combat-button { | |||
| width: 100px; | |||
| height: 100px; | |||
| } | |||
| </style> | |||
| <style> | |||
| #log p:nth-last-child(1) { | |||
| padding-top: 16pt; | |||
| } | |||
| </style> | |||
| @@ -1,9 +1,78 @@ | |||
| import { Creature, POV } from './entity' | |||
| import { POVActionPicker } from './language' | |||
| import { Creature, POV, Entity } from './entity' | |||
| import { POVPair, POVPairArgs } from './language' | |||
| import { Container } from './vore' | |||
| import { LogEntry, LogLines, CompositeLog } from './interface' | |||
| export enum DamageType {Pierce, Slash, Crush, Acid} | |||
| export interface CombatTest { | |||
| test: (user: Creature, target: Creature) => boolean; | |||
| odds: (user: Creature, target: Creature) => number; | |||
| explain: (user: Creature, target: Creature) => LogEntry; | |||
| } | |||
| function logistic (x0: number, L: number, k: number): (x: number) => number { | |||
| return (x: number) => { | |||
| return L / (1 + Math.exp(-k * (x - x0))) | |||
| } | |||
| } | |||
| abstract class RandomTest implements CombatTest { | |||
| test (user: Creature, target: Creature): boolean { | |||
| return Math.random() < this.odds(user, target) | |||
| } | |||
| abstract odds(user: Creature, target: Creature): number | |||
| abstract explain(user: Creature, target: Creature): LogEntry | |||
| } | |||
| 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.f(user.stats[this.stat] - target.stats[this.stat]) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| const delta: number = user.stats[this.stat] - target.stats[this.stat] | |||
| let result: string | |||
| 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.' | |||
| } | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%' | |||
| return new LogLines(result) | |||
| } | |||
| } | |||
| export class ChanceTest extends RandomTest { | |||
| constructor (public readonly chance: number) { | |||
| super() | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.chance | |||
| } | |||
| 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"} | |||
| export interface DamageInstance { | |||
| type: DamageType; | |||
| @@ -11,24 +80,28 @@ export interface DamageInstance { | |||
| } | |||
| export class Damage { | |||
| readonly damages: DamageInstance[] | |||
| readonly damages: DamageInstance[] | |||
| constructor (...damages: DamageInstance[]) { | |||
| this.damages = damages | |||
| } | |||
| constructor (...damages: DamageInstance[]) { | |||
| this.damages = damages | |||
| } | |||
| scale (factor: number): Damage { | |||
| const results: Array<DamageInstance> = [] | |||
| scale (factor: number): Damage { | |||
| const results: Array<DamageInstance> = [] | |||
| this.damages.forEach(damage => { | |||
| results.push({ | |||
| type: damage.type, | |||
| amount: damage.amount * factor | |||
| }) | |||
| this.damages.forEach(damage => { | |||
| results.push({ | |||
| type: damage.type, | |||
| amount: damage.amount * factor | |||
| }) | |||
| }) | |||
| return new Damage(...results) | |||
| } | |||
| return new Damage(...results) | |||
| } | |||
| toString (): string { | |||
| return this.damages.map(damage => damage.amount + " " + damage.type).join("/") | |||
| } | |||
| } | |||
| export enum Stat { | |||
| @@ -83,91 +156,108 @@ abstract class TogetherAction extends PairAction { | |||
| } | |||
| export class AttackAction extends TogetherAction { | |||
| protected lines: POVActionPicker = { | |||
| [POV.First]: { | |||
| [POV.First]: (user, target) => new LogLines('You bite...yourself?'), | |||
| [POV.Third]: (user, target) => new LogLines('You bite ' + target.name) | |||
| }, | |||
| [POV.Third]: { | |||
| [POV.First]: (user, target) => new LogLines(user.name.capital + ' bites you'), | |||
| [POV.Third]: (user, target) => new LogLines(user.name.capital + ' bites ' + target.name) | |||
| } | |||
| } | |||
| constructor (protected damage: Damage) { | |||
| super('Attack', 'Attack the enemy') | |||
| } | |||
| private test: StatTest | |||
| protected successLines: POVPairArgs<Entity, Entity, { damage: Damage }> = new POVPairArgs([ | |||
| [[POV.First, POV.Third], (user, target, args) => new LogLines(`You smack ${target.name} for ${args.damage} damage`)], | |||
| [[POV.Third, POV.First], (user, target, args) => new LogLines(`${user.name.capital} hits you for ${args.damage} damage`)], | |||
| [[POV.Third, POV.Third], (user, target, args) => new LogLines(`${user.name.capital} hits ${target.name.capital} for ${args.damage} damage`)] | |||
| ]) | |||
| protected failLines: POVPair<Entity, Entity> = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLines(`You try to smack ${target.name}, but you miss`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} misses you`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} misses ${target.name}`)] | |||
| ]) | |||
| constructor (protected damage: Damage) { | |||
| super('Attack', 'Attack the enemy') | |||
| this.test = new StatTest(Stat.STR) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (this.test.test(user, target)) { | |||
| target.takeDamage(this.damage) | |||
| return this.lines[user.perspective][target.perspective](user, target) | |||
| return this.successLines.run(user, target, { damage: this.damage }) | |||
| } else { | |||
| return this.failLines.run(user, target) | |||
| } | |||
| } | |||
| } | |||
| export class DevourAction extends TogetherAction { | |||
| protected lines: POVActionPicker = { | |||
| [POV.First]: { | |||
| [POV.First]: (user, target) => new LogLines('You devour...yourself?'), | |||
| [POV.Third]: (user, target) => new LogLines('You devour ' + target.name) | |||
| }, | |||
| [POV.Third]: { | |||
| [POV.First]: (user, target) => new LogLines(user.name.capital + ' devours you'), | |||
| [POV.Third]: (user, target) => new LogLines(user.name.capital + ' devours ' + target.name) | |||
| } | |||
| } | |||
| private test: StatTest | |||
| constructor (protected container: Container) { | |||
| super('Devour', 'Try to consume your foe') | |||
| this.name += ` (${container.name})` | |||
| 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}`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to devour you, but fails`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} unsuccessfully tries to swallow ${target.name}`)] | |||
| ]) | |||
| allowed (user: Creature, target: Creature): boolean { | |||
| const owner = this.container.owner === user | |||
| const predOk = Array.from(this.container.voreTypes).every(pref => user.predPrefs.has(pref)) | |||
| const preyOk = Array.from(this.container.voreTypes).every(pref => target.preyPrefs.has(pref)) | |||
| if (owner && predOk && preyOk) { | |||
| return super.allowed(user, target) | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), this.container.consume(target)) | |||
| constructor (protected container: Container) { | |||
| super('Devour', 'Try to consume your foe') | |||
| this.name += ` (${container.name})` | |||
| this.test = new StatTest(Stat.STR) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (this.test.test(user, target)) { | |||
| return this.container.consume(target) | |||
| } else { | |||
| return this.failLines.run(user, target) | |||
| } | |||
| } | |||
| } | |||
| export class StruggleAction extends PairAction { | |||
| protected lines: POVActionPicker = { | |||
| [POV.First]: { | |||
| [POV.First]: (user, target) => new LogLines('You escape from...yourself?'), | |||
| [POV.Third]: (user, target) => new LogLines('You escape from ' + target.name) | |||
| }, | |||
| [POV.Third]: { | |||
| [POV.First]: (user, target) => new LogLines(user.name.capital + ' escapes from you'), | |||
| [POV.Third]: (user, target) => new LogLines(user.name.capital + ' escapes from ' + target.name) | |||
| } | |||
| } | |||
| private test: StatTest | |||
| allowed (user: Creature, target: Creature) { | |||
| if (user.containedIn === this.container) { | |||
| return super.allowed(user, target) | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| 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}`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to devour you, but fails`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} unsuccessfully tries to swallow ${target.name}`)] | |||
| ]) | |||
| constructor (public container: Container) { | |||
| super('Struggle', 'Try to escape your predator') | |||
| allowed (user: Creature, target: Creature) { | |||
| if (user.containedIn === this.container) { | |||
| return super.allowed(user, target) | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (user.containedIn) { | |||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), user.containedIn.release(user)) | |||
| } else { return new LogLines("The prey wasn't actually eaten...") } | |||
| constructor (public container: Container) { | |||
| super('Struggle', 'Try to escape your predator') | |||
| this.test = new StatTest(Stat.STR) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (user.containedIn !== null) { | |||
| if (this.test.test(user, target)) { | |||
| return user.containedIn.release(user) | |||
| } else { | |||
| return this.failLines.run(user, target) | |||
| } | |||
| } else { | |||
| return new LogLines("Vore's bugged!") | |||
| } | |||
| } | |||
| } | |||
| export class DigestAction extends SelfAction { | |||
| protected lines: POVActionPicker = { | |||
| [POV.First]: { | |||
| [POV.First]: (user, target) => new LogLines('You rub your stomach'), | |||
| [POV.Third]: (user, target) => new LogLines("You can't digest for other people...") | |||
| }, | |||
| [POV.Third]: { | |||
| [POV.First]: (user, target) => new LogLines("Other people can't digest for you..."), | |||
| [POV.Third]: (user, target) => new LogLines(user.name.capital + ' rubs ' + user.pronouns.possessive + ' gut.') | |||
| } | |||
| } | |||
| protected lines: POVPair<Entity, Entity> = new POVPair([]) | |||
| allowed (user: Creature, target: Creature) { | |||
| if (this.container.owner === user && this.container.contents.length > 0) { | |||
| @@ -184,22 +274,11 @@ export class DigestAction extends SelfAction { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const results = new CompositeLog(...user.containers.map(container => container.tick(60))) | |||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), results) | |||
| return new CompositeLog(this.lines.run(user, target), results) | |||
| } | |||
| } | |||
| export class ReleaseAction extends PairAction { | |||
| protected lines: POVActionPicker = { | |||
| [POV.First]: { | |||
| [POV.First]: (user, target) => new LogLines(`You can't release yourself`), | |||
| [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]: (user, target) => new LogLines(`${user.name.capital} horks up ${target.name.capital}`) | |||
| } | |||
| } | |||
| allowed (user: Creature, target: Creature) { | |||
| if (target.containedIn === this.container) { | |||
| return super.allowed(user, target) | |||
| @@ -214,22 +293,12 @@ export class ReleaseAction extends PairAction { | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const results = this.container.release(target) | |||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), results) | |||
| return this.container.release(target) | |||
| } | |||
| } | |||
| export class TransferAction extends PairAction { | |||
| protected lines: POVActionPicker = { | |||
| [POV.First]: { | |||
| [POV.First]: (user, target) => new LogLines(), | |||
| [POV.Third]: (user, target) => new LogLines(`You push your prey from your ${this.from.name} to your ${this.to.name}`) | |||
| }, | |||
| [POV.Third]: { | |||
| [POV.First]: (user, target) => new LogLines(`You're shoved from ${user.name}'s ${this.from.name} to their ${this.to.name}`), | |||
| [POV.Third]: (user, target) => new LogLines(`${user.name.capital} pushes ${target.name} from their ${this.from.name} to their ${this.to.name}`) | |||
| } | |||
| } | |||
| protected lines: POVPair<Entity, Entity> = new POVPair([]) | |||
| allowed (user: Creature, target: Creature) { | |||
| if (target.containedIn === this.from) { | |||
| @@ -246,71 +315,6 @@ export class TransferAction extends PairAction { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| this.from.release(target) | |||
| this.to.consume(target) | |||
| return this.lines[user.perspective][target.perspective](user, target) | |||
| } | |||
| } | |||
| export interface CombatTest { | |||
| test: (user: Creature, target: Creature) => boolean; | |||
| odds: (user: Creature, target: Creature) => number; | |||
| explain: (user: Creature, target: Creature) => LogEntry; | |||
| } | |||
| function logistic (x0: number, L: number, k: number): (x: number) => number { | |||
| return (x: number) => { | |||
| return L / (1 + Math.exp(-k * (x - x0))) | |||
| } | |||
| } | |||
| abstract class RandomTest implements CombatTest { | |||
| test (user: Creature, target: Creature): boolean { | |||
| return Math.random() < this.odds(user, target) | |||
| } | |||
| abstract odds(user: Creature, target: Creature): number | |||
| abstract explain(user: Creature, target: Creature): LogEntry | |||
| } | |||
| 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.f(user.stats[this.stat] - target.stats[this.stat]) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| const delta: number = user.stats[this.stat] - target.stats[this.stat] | |||
| let result: string | |||
| 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.' | |||
| } | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%' | |||
| return new LogLines(result) | |||
| } | |||
| } | |||
| export class ChanceTest extends RandomTest { | |||
| constructor (public readonly chance: number) { | |||
| super() | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.chance | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.') | |||
| return this.lines.run(user, target) | |||
| } | |||
| } | |||
| @@ -1,18 +1,20 @@ | |||
| import { Creature, POV } from '../entity' | |||
| import { ProperNoun, TheyPronouns } from '../language' | |||
| import { Stat, Damage, AttackAction, DevourAction, DamageType } from '../combat' | |||
| import { Stomach, VoreType } from '../vore' | |||
| import { Stat, Damage, AttackAction, DamageType } from '../combat' | |||
| 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]), 50) | |||
| 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) | |||
| this.actions.push(new AttackAction(new Damage({ type: DamageType.Pierce, amount: 20 }))) | |||
| const stomach = new Stomach(this, 100, new Damage({ amount: 10, type: DamageType.Acid }, { amount: 10, type: DamageType.Crush })) | |||
| const stomach = new Stomach(this, 100, new Damage({ amount: 100000000000, type: DamageType.Acid }, { amount: 10, type: DamageType.Crush })) | |||
| this.containers.push(stomach) | |||
| this.actions.push(new DevourAction(stomach)) | |||
| const bowels = new Bowels(this, 100, new Damage({ amount: 20, type: DamageType.Crush })) | |||
| this.containers.push(bowels) | |||
| this.perspective = POV.First | |||
| } | |||
| } | |||
| @@ -2,20 +2,16 @@ import { Creature, POV } from '../entity' | |||
| import { Stat, Damage, DamageType, AttackAction, StruggleAction, TransferAction } from '../combat' | |||
| import { MalePronouns, ImproperNoun } from '../language' | |||
| import { VoreType, Stomach, Bowels } from '../vore' | |||
| import { LogLines } from '../interface' | |||
| class BiteAction extends AttackAction { | |||
| constructor () { | |||
| super(new Damage({ amount: 10, type: DamageType.Pierce })) | |||
| this.lines[POV.First][POV.Third] = (user, target) => new LogLines('You sink your fangs into ' + target.name) | |||
| this.lines[POV.Third][POV.First] = (user, target) => new LogLines(user.name.capital + ' bites you. This bite is easily the top 1% of bites produced by ' + user.name.plural.all + '.') | |||
| this.lines[POV.Third][POV.Third] = (user, target) => new LogLines(user.name.capital + ' chomps ' + target.name) | |||
| } | |||
| } | |||
| export class Wolf extends Creature { | |||
| constructor () { | |||
| super(new ImproperNoun('wolf', 'wolves'), MalePronouns, { [Stat.STR]: 10, [Stat.DEX]: 10, [Stat.CON]: 10 }, new Set([VoreType.Oral]), new Set([VoreType.Oral]), 25) | |||
| super(new ImproperNoun('wolf', 'wolves'), MalePronouns, { [Stat.STR]: 10, [Stat.DEX]: 10, [Stat.CON]: 10 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral]), 25) | |||
| this.actions.push(new BiteAction()) | |||
| const stomach = new Stomach(this, 50, new Damage({ amount: 50, type: DamageType.Acid }, { amount: 500, type: DamageType.Crush })) | |||
| @@ -35,9 +35,6 @@ export class CompositeLog implements LogEntry { | |||
| export function log (entry: LogEntry): void { | |||
| entry.render().forEach(elem => { | |||
| document.querySelector('#log')!.insertBefore(elem, document?.querySelector('#log > *')) | |||
| document.querySelector('#log')!.appendChild(elem) | |||
| }) | |||
| const entries: Array<HTMLElement> = Array.from(document.querySelectorAll('#log > *')) | |||
| entries.slice(5).forEach(element => element.remove()) | |||
| } | |||
| @@ -1,7 +1,52 @@ | |||
| import { Creature, POV } from './entity' | |||
| import { LogEntry } from './interface' | |||
| import { Entity, POV } from './entity' | |||
| import { LogEntry, LogLines } from './interface' | |||
| export type POVActionPicker = { [key in POV]: { [key in POV]: (user: Creature, target: Creature) => LogEntry } } | |||
| export class POVPair<K extends Entity, V extends Entity> { | |||
| run (user: K, target: V): LogEntry { | |||
| const choice = this.options.find(element => element[0][0] === user.perspective && element[0][1] === target.perspective) | |||
| if (choice === undefined) { | |||
| return new LogLines("Fen didn't write any text for this...") | |||
| } else { | |||
| return choice[1](user, target) | |||
| } | |||
| } | |||
| constructor (private options: Array<[[POV, POV], (user: K, target: V) => LogEntry]>) { | |||
| } | |||
| } | |||
| export class POVPairArgs<K extends Entity, V extends Entity, U> { | |||
| run (user: K, target: V, args: U): LogEntry { | |||
| const choice = this.options.find(element => element[0][0] === user.perspective && element[0][1] === target.perspective) | |||
| if (choice === undefined) { | |||
| return new LogLines("Fen didn't write any text for this...") | |||
| } else { | |||
| return choice[1](user, target, args) | |||
| } | |||
| } | |||
| constructor (private options: Array<[[POV, POV], (user: K, target: V, args: U) => LogEntry]>) { | |||
| } | |||
| } | |||
| export class POVSolo<K extends Entity> { | |||
| run (user: K, args: any = {}): LogEntry { | |||
| const choice = this.options.find(element => element[0][0] === user.perspective) | |||
| if (choice === undefined) { | |||
| return new LogLines("Fen didn't write any text for this...") | |||
| } else { | |||
| return choice[1](user, args) | |||
| } | |||
| } | |||
| constructor (private options: Array<[[POV], (user: K, args: any) => LogEntry]>) { | |||
| } | |||
| } | |||
| enum NounKind { | |||
| Specific, | |||
| @@ -1,7 +1,14 @@ | |||
| import { Entity, Mortal, POV } from './entity' | |||
| import { Damage, Actionable, Action, DevourAction, DigestAction, ReleaseAction, StruggleAction } from './combat' | |||
| import { LogLines, LogEntry, CompositeLog } from './interface' | |||
| export enum VoreType {Oral} | |||
| import { POVSolo, POVPair, POVPairArgs } from './language' | |||
| export enum VoreType { | |||
| Oral = "Oral Vore", | |||
| Anal = "Anal Vore", | |||
| Cock = "Cock Vore", | |||
| Unbirth = "Unbirthing" | |||
| } | |||
| export interface Prey extends Mortal { | |||
| preyPrefs: Set<VoreType>; | |||
| @@ -36,6 +43,14 @@ export interface Container extends Actionable { | |||
| abstract class NormalContainer implements Container { | |||
| contents: Array<Prey> | |||
| abstract consumeLines: POVPair<Pred, Prey> | |||
| abstract releaseLines: POVPair<Pred, Prey> | |||
| abstract struggleLines: POVPair<Prey, Pred> | |||
| abstract tickLines: POVSolo<Pred> | |||
| abstract digestLines: POVPair<Pred, Prey> | |||
| abstract absorbLines: POVPair<Pred, Prey> | |||
| abstract disposeLines: POVPair<Pred, Prey> | |||
| get fullness (): number { | |||
| return Array.from(this.contents.values()).reduce((total: number, prey: Prey) => total + prey.bulk, 0) | |||
| } | |||
| @@ -53,17 +68,17 @@ abstract class NormalContainer implements Container { | |||
| consume (prey: Prey): LogEntry { | |||
| this.contents.push(prey) | |||
| prey.containedIn = this | |||
| return new LogLines('MUNCH') | |||
| return this.consumeLines.run(this.owner, prey) | |||
| } | |||
| release (prey: Prey): LogEntry { | |||
| prey.containedIn = null | |||
| this.contents = this.contents.filter(victim => victim !== prey) | |||
| return new LogLines('ANTI-MUNCH') | |||
| return this.releaseLines.run(this.owner, prey) | |||
| } | |||
| struggle (prey: Prey): LogEntry { | |||
| return new LogLines('Slosh!') | |||
| return this.struggleLines.run(prey, this.owner) | |||
| } | |||
| tick (dt: number): LogEntry { | |||
| @@ -88,7 +103,7 @@ abstract class NormalContainer implements Container { | |||
| return prey.health > -100 | |||
| }) | |||
| return new CompositeLog(digestedEntries, absorbedEntries) | |||
| return new CompositeLog(this.tickLines.run(this.owner), digestedEntries, absorbedEntries) | |||
| } | |||
| describe (): LogEntry { | |||
| @@ -102,15 +117,15 @@ abstract class NormalContainer implements Container { | |||
| } | |||
| digest (prey: Prey): LogEntry { | |||
| return new LogLines('Glorp!') | |||
| return this.digestLines.run(this.owner, prey) | |||
| } | |||
| absorb (prey: Prey): LogEntry { | |||
| return new LogLines('Glorp...') | |||
| return this.absorbLines.run(this.owner, prey) | |||
| } | |||
| dispose (preys: Prey[]): LogEntry { | |||
| return new LogLines('GLORP') | |||
| return new CompositeLog(...preys.map(prey => this.disposeLines.run(this.owner, prey))) | |||
| } | |||
| actions: Array<Action> | |||
| @@ -119,127 +134,104 @@ abstract class NormalContainer implements Container { | |||
| this.contents = [] | |||
| this.actions = [] | |||
| this.actions.push(new DevourAction(this)) | |||
| this.actions.push(new DigestAction(this)) | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| } | |||
| export class Stomach extends NormalContainer { | |||
| constructor (owner: Pred, capacity: number, damage: Damage) { | |||
| super('Stomach', owner, new Set([VoreType.Oral]), capacity, damage) | |||
| this.actions.push(new DevourAction(this)) | |||
| this.actions.push(new DigestAction(this)) | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| consume (prey: Prey): LogEntry { | |||
| super.consume(prey) | |||
| const predPOV = this.owner.perspective | |||
| const preyPOV = prey.perspective | |||
| if (predPOV === POV.First && preyPOV === POV.Third) { | |||
| return new LogLines(prey.name.capital + ' slides down into your stomach') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.First) { | |||
| return new LogLines(this.owner.name.capital + "'s guts swell as you slush down into " + this.owner.pronouns.possessive + ' stomach') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.Third) { | |||
| return new LogLines(this.owner.name.capital + "'s belly fills with the struggling form of " + prey.name) | |||
| } else { | |||
| return new LogLines('FIX ME!') | |||
| } | |||
| } | |||
| digest (prey: Prey): LogEntry { | |||
| super.digest(prey) | |||
| const predPOV = this.owner.perspective | |||
| const preyPOV = prey.perspective | |||
| if (predPOV === POV.First && preyPOV === POV.Third) { | |||
| return new LogLines('Your stomach finishes off ' + prey.name) | |||
| } else if (predPOV === POV.Third && preyPOV === POV.First) { | |||
| return new LogLines(this.owner.name.capital + ' digests you') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.Third) { | |||
| return new LogLines(this.owner.name.capital + ' finishes digesting ' + prey.name) | |||
| } else { | |||
| return new LogLines('FIX ME!') | |||
| } | |||
| } | |||
| absorb (prey: Prey): LogEntry { | |||
| super.absorb(prey) | |||
| const predPOV = this.owner.perspective | |||
| const preyPOV = prey.perspective | |||
| if (predPOV === POV.First && preyPOV === POV.Third) { | |||
| return new LogLines("Your stomach melts down what's left of " + prey.name) | |||
| } else if (predPOV === POV.Third && preyPOV === POV.First) { | |||
| return new LogLines(this.owner.name.capital + ' finishes absorbing you') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.Third) { | |||
| return new LogLines(this.owner.name.capital + ' fully absorbs ' + prey.name) | |||
| } else { | |||
| return new LogLines('FIX ME!') | |||
| } | |||
| } | |||
| 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}`)] | |||
| ]) | |||
| 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}`)] | |||
| ]) | |||
| 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}`)] | |||
| ]) | |||
| 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`)] | |||
| ]) | |||
| 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}`)] | |||
| ]) | |||
| absorbLines = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLines(`Your guts completely absorb ${target.name}`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital}'s guts soak you up like water in a sponge`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} finishes absorbing the remains of ${target.name}`)] | |||
| ]) | |||
| disposeLines = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLines(`Your guts completely absorb ${target.name}`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital}'s guts soak you up like water in a sponge`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} finishes absorbing the remains of ${target.name}`)] | |||
| ]) | |||
| } | |||
| export class Bowels extends NormalContainer { | |||
| constructor (owner: Pred, capacity: number, damage: Damage) { | |||
| super('Bowels', owner, new Set([VoreType.Oral]), capacity, damage) | |||
| this.actions.push(new DevourAction(this)) | |||
| this.actions.push(new DigestAction(this)) | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| consume (prey: Prey): LogEntry { | |||
| super.consume(prey) | |||
| const predPOV = this.owner.perspective | |||
| const preyPOV = prey.perspective | |||
| if (predPOV === POV.First && preyPOV === POV.Third) { | |||
| return new LogLines(prey.name.capital + ' slides down into your bowels') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.First) { | |||
| return new LogLines(this.owner.name.capital + "'s guts swell as you slush down into " + this.owner.pronouns.possessive + ' bowels') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.Third) { | |||
| return new LogLines(this.owner.name.capital + "'s belly fills with the struggling form of " + prey.name) | |||
| } else { | |||
| return new LogLines('FIX ME!') | |||
| } | |||
| super('Bowels', owner, new Set([VoreType.Anal]), capacity, damage) | |||
| } | |||
| digest (prey: Prey): LogEntry { | |||
| super.digest(prey) | |||
| const predPOV = this.owner.perspective | |||
| const preyPOV = prey.perspective | |||
| if (predPOV === POV.First && preyPOV === POV.Third) { | |||
| return new LogLines('Your bowels finishes off ' + prey.name) | |||
| } else if (predPOV === POV.Third && preyPOV === POV.First) { | |||
| return new LogLines(this.owner.name.capital + ' digests you') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.Third) { | |||
| return new LogLines(this.owner.name.capital + ' finishes digesting ' + prey.name) | |||
| } else { | |||
| return new LogLines('FIX ME!') | |||
| } | |||
| } | |||
| absorb (prey: Prey): LogEntry { | |||
| super.absorb(prey) | |||
| const predPOV = this.owner.perspective | |||
| const preyPOV = prey.perspective | |||
| if (predPOV === POV.First && preyPOV === POV.Third) { | |||
| return new LogLines("Your bowels melts down what's left of " + prey.name) | |||
| } else if (predPOV === POV.Third && preyPOV === POV.First) { | |||
| return new LogLines(this.owner.name.capital + ' finishes absorbing you') | |||
| } else if (predPOV === POV.Third && preyPOV === POV.Third) { | |||
| return new LogLines(this.owner.name.capital + ' fully absorbs ' + prey.name) | |||
| } else { | |||
| return new LogLines('FIX ME!') | |||
| } | |||
| } | |||
| 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}`)] | |||
| ]) | |||
| 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}`)] | |||
| ]) | |||
| 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}`)] | |||
| ]) | |||
| 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`)] | |||
| ]) | |||
| 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}`)] | |||
| ]) | |||
| absorbLines = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLines(`Your guts completely absorb ${target.name}`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital}'s guts soak you up like water in a sponge`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} finishes absorbing the remains of ${target.name}`)] | |||
| ]) | |||
| disposeLines = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLines(`Your guts completely absorb ${target.name}`)], | |||
| [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital}'s guts soak you up like water in a sponge`)], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} finishes absorbing the remains of ${target.name}`)] | |||
| ]) | |||
| } | |||