| @@ -22,8 +22,7 @@ export default class App extends Vue { | |||||
| enemy: Creature | enemy: Creature | ||||
| constructor () { | constructor () { | ||||
| super() | super() | ||||
| this.player = new Creatures.Wolf() | |||||
| this.player.perspective = POV.First | |||||
| this.player = new Creatures.Player() | |||||
| this.enemy = new Creatures.Wolf() | this.enemy = new Creatures.Wolf() | ||||
| console.log(this.player) | console.log(this.player) | ||||
| console.log(this.enemy) | console.log(this.enemy) | ||||
| @@ -5,20 +5,24 @@ | |||||
| <Statblock :subject="player" /> | <Statblock :subject="player" /> | ||||
| <Statblock :subject="enemy" /> | <Statblock :subject="enemy" /> | ||||
| </div> | </div> | ||||
| <div id="log"></div> | |||||
| <div class="horiz-display"> | <div class="horiz-display"> | ||||
| <div> | <div> | ||||
| <h2>Your moves</h2> | <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>{{actionDescription}}</div> | ||||
| </div> | </div> | ||||
| <div> | <div> | ||||
| <h2>Enemy moves</h2> | <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> | </div> | ||||
| <div id="log"></div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| @@ -71,8 +75,20 @@ a { | |||||
| display: flex; | display: flex; | ||||
| justify-content: center; | justify-content: center; | ||||
| } | } | ||||
| .vert-display { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| } | |||||
| .combat-button { | .combat-button { | ||||
| width: 100px; | width: 100px; | ||||
| height: 100px; | height: 100px; | ||||
| } | } | ||||
| </style> | </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 { Container } from './vore' | ||||
| import { LogEntry, LogLines, CompositeLog } from './interface' | 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 { | export interface DamageInstance { | ||||
| type: DamageType; | type: DamageType; | ||||
| @@ -11,24 +80,28 @@ export interface DamageInstance { | |||||
| } | } | ||||
| export class Damage { | 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 { | export enum Stat { | ||||
| @@ -83,91 +156,108 @@ abstract class TogetherAction extends PairAction { | |||||
| } | } | ||||
| export class AttackAction extends TogetherAction { | 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) | 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 { | 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 { | 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 { | 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) { | allowed (user: Creature, target: Creature) { | ||||
| if (this.container.owner === user && this.container.contents.length > 0) { | 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 { | execute (user: Creature, target: Creature): LogEntry { | ||||
| const results = new CompositeLog(...user.containers.map(container => container.tick(60))) | 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 { | 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) { | allowed (user: Creature, target: Creature) { | ||||
| if (target.containedIn === this.container) { | if (target.containedIn === this.container) { | ||||
| return super.allowed(user, target) | return super.allowed(user, target) | ||||
| @@ -214,22 +293,12 @@ export class ReleaseAction extends PairAction { | |||||
| } | } | ||||
| execute (user: Creature, target: Creature): LogEntry { | 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 { | 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) { | allowed (user: Creature, target: Creature) { | ||||
| if (target.containedIn === this.from) { | if (target.containedIn === this.from) { | ||||
| @@ -246,71 +315,6 @@ export class TransferAction extends PairAction { | |||||
| execute (user: Creature, target: Creature): LogEntry { | execute (user: Creature, target: Creature): LogEntry { | ||||
| this.from.release(target) | this.from.release(target) | ||||
| this.to.consume(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 { Creature, POV } from '../entity' | ||||
| import { ProperNoun, TheyPronouns } from '../language' | 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 { | export class Player extends Creature { | ||||
| constructor () { | 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 }))) | 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.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 | this.perspective = POV.First | ||||
| } | } | ||||
| } | } | ||||
| @@ -2,20 +2,16 @@ import { Creature, POV } from '../entity' | |||||
| import { Stat, Damage, DamageType, AttackAction, StruggleAction, TransferAction } from '../combat' | import { Stat, Damage, DamageType, AttackAction, StruggleAction, TransferAction } from '../combat' | ||||
| import { MalePronouns, ImproperNoun } from '../language' | import { MalePronouns, ImproperNoun } from '../language' | ||||
| import { VoreType, Stomach, Bowels } from '../vore' | import { VoreType, Stomach, Bowels } from '../vore' | ||||
| import { LogLines } from '../interface' | |||||
| class BiteAction extends AttackAction { | class BiteAction extends AttackAction { | ||||
| constructor () { | constructor () { | ||||
| super(new Damage({ amount: 10, type: DamageType.Pierce })) | 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 { | export class Wolf extends Creature { | ||||
| constructor () { | 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()) | this.actions.push(new BiteAction()) | ||||
| const stomach = new Stomach(this, 50, new Damage({ amount: 50, type: DamageType.Acid }, { amount: 500, type: DamageType.Crush })) | 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 { | export function log (entry: LogEntry): void { | ||||
| entry.render().forEach(elem => { | 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 { | enum NounKind { | ||||
| Specific, | Specific, | ||||
| @@ -1,7 +1,14 @@ | |||||
| import { Entity, Mortal, POV } from './entity' | import { Entity, Mortal, POV } from './entity' | ||||
| import { Damage, Actionable, Action, DevourAction, DigestAction, ReleaseAction, StruggleAction } from './combat' | import { Damage, Actionable, Action, DevourAction, DigestAction, ReleaseAction, StruggleAction } from './combat' | ||||
| import { LogLines, LogEntry, CompositeLog } from './interface' | 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 { | export interface Prey extends Mortal { | ||||
| preyPrefs: Set<VoreType>; | preyPrefs: Set<VoreType>; | ||||
| @@ -36,6 +43,14 @@ export interface Container extends Actionable { | |||||
| abstract class NormalContainer implements Container { | abstract class NormalContainer implements Container { | ||||
| contents: Array<Prey> | 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 { | get fullness (): number { | ||||
| return Array.from(this.contents.values()).reduce((total: number, prey: Prey) => total + prey.bulk, 0) | 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 { | consume (prey: Prey): LogEntry { | ||||
| this.contents.push(prey) | this.contents.push(prey) | ||||
| prey.containedIn = this | prey.containedIn = this | ||||
| return new LogLines('MUNCH') | |||||
| return this.consumeLines.run(this.owner, prey) | |||||
| } | } | ||||
| release (prey: Prey): LogEntry { | release (prey: Prey): LogEntry { | ||||
| prey.containedIn = null | prey.containedIn = null | ||||
| this.contents = this.contents.filter(victim => victim !== prey) | this.contents = this.contents.filter(victim => victim !== prey) | ||||
| return new LogLines('ANTI-MUNCH') | |||||
| return this.releaseLines.run(this.owner, prey) | |||||
| } | } | ||||
| struggle (prey: Prey): LogEntry { | struggle (prey: Prey): LogEntry { | ||||
| return new LogLines('Slosh!') | |||||
| return this.struggleLines.run(prey, this.owner) | |||||
| } | } | ||||
| tick (dt: number): LogEntry { | tick (dt: number): LogEntry { | ||||
| @@ -88,7 +103,7 @@ abstract class NormalContainer implements Container { | |||||
| return prey.health > -100 | return prey.health > -100 | ||||
| }) | }) | ||||
| return new CompositeLog(digestedEntries, absorbedEntries) | |||||
| return new CompositeLog(this.tickLines.run(this.owner), digestedEntries, absorbedEntries) | |||||
| } | } | ||||
| describe (): LogEntry { | describe (): LogEntry { | ||||
| @@ -102,15 +117,15 @@ abstract class NormalContainer implements Container { | |||||
| } | } | ||||
| digest (prey: Prey): LogEntry { | digest (prey: Prey): LogEntry { | ||||
| return new LogLines('Glorp!') | |||||
| return this.digestLines.run(this.owner, prey) | |||||
| } | } | ||||
| absorb (prey: Prey): LogEntry { | absorb (prey: Prey): LogEntry { | ||||
| return new LogLines('Glorp...') | |||||
| return this.absorbLines.run(this.owner, prey) | |||||
| } | } | ||||
| dispose (preys: Prey[]): LogEntry { | dispose (preys: Prey[]): LogEntry { | ||||
| return new LogLines('GLORP') | |||||
| return new CompositeLog(...preys.map(prey => this.disposeLines.run(this.owner, prey))) | |||||
| } | } | ||||
| actions: Array<Action> | actions: Array<Action> | ||||
| @@ -119,127 +134,104 @@ abstract class NormalContainer implements Container { | |||||
| this.contents = [] | this.contents = [] | ||||
| this.actions = [] | 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 { | export class Stomach extends NormalContainer { | ||||
| constructor (owner: Pred, capacity: number, damage: Damage) { | constructor (owner: Pred, capacity: number, damage: Damage) { | ||||
| super('Stomach', owner, new Set([VoreType.Oral]), capacity, 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 { | export class Bowels extends NormalContainer { | ||||
| constructor (owner: Pred, capacity: number, damage: Damage) { | 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}`)] | |||||
| ]) | |||||
| } | } | ||||