| @@ -13,6 +13,8 @@ module.exports = { | |||
| }, | |||
| rules: { | |||
| 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', | |||
| 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' | |||
| 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', | |||
| 'no-useless-constructor': 'off', | |||
| '@typescript-eslint/no-unused-vars': 'off' | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| <template> | |||
| <div id="app"> | |||
| <img alt="Vue logo" src="./assets/logo.png"> | |||
| <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/> | |||
| <HelloWorld msg="what's a blockchain" seen="yes"/> | |||
| </div> | |||
| </template> | |||
| @@ -1,42 +1,49 @@ | |||
| <template> | |||
| <div class="hello"> | |||
| <h1>{{ msg }}</h1> | |||
| <h1>VORE TIME</h1> | |||
| <p> | |||
| For a guide and recipes on how to configure / customize this project,<br> | |||
| check out the | |||
| <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>. | |||
| {{ meme }} | |||
| </p> | |||
| <h3>Installed CLI Plugins</h3> | |||
| <ul> | |||
| <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li> | |||
| <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li> | |||
| <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li> | |||
| </ul> | |||
| <h3>Essential Links</h3> | |||
| <ul> | |||
| <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li> | |||
| <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li> | |||
| <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li> | |||
| <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li> | |||
| <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li> | |||
| </ul> | |||
| <h3>Ecosystem</h3> | |||
| <ul> | |||
| <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li> | |||
| <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li> | |||
| <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li> | |||
| <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li> | |||
| <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li> | |||
| </ul> | |||
| <button v-on:click="dab">VORE</button> | |||
| </div> | |||
| </template> | |||
| <script lang="ts"> | |||
| import { Component, Prop, Vue } from 'vue-property-decorator' | |||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||
| import { Creature } from '@/game/entity' | |||
| import { Wolf } from '@/game/creatures/wolf' | |||
| /// <reference path="game/feast.ts" /> | |||
| /// <reference path="game/creatures.ts" /> | |||
| @Component | |||
| export default class HelloWorld extends Vue { | |||
| @Prop() private msg!: string; | |||
| @Prop() public seen!: boolean; | |||
| private player: Creature; | |||
| private enemy: Creature; | |||
| private meme = 'hm'; | |||
| mounted () { | |||
| this.meme = 'Yeet' | |||
| } | |||
| @Emit() | |||
| dab () { | |||
| this.meme = 'Yeeeeet' | |||
| } | |||
| @Watch('meme') | |||
| onPropertyChanged (value: string, oldValue: string) { | |||
| this.msg = value + oldValue | |||
| } | |||
| constructor () { | |||
| super() | |||
| this.player = new Wolf() | |||
| this.enemy = new Wolf() | |||
| } | |||
| } | |||
| </script> | |||
| @@ -0,0 +1,250 @@ | |||
| import { Creature, POV } from './entity' | |||
| import { POVActionPicker } from './language' | |||
| import { Container } from './vore' | |||
| import { LogEntry, LogLines, CompositeLog } from './interface' | |||
| export enum DamageType {Pierce, Slash, Crush, Acid} | |||
| export interface DamageInstance { | |||
| type: DamageType; | |||
| amount: number; | |||
| } | |||
| export class Damage { | |||
| readonly damages: DamageInstance[] | |||
| constructor (...damages: DamageInstance[]) { | |||
| this.damages = damages | |||
| } | |||
| scale (factor: number): Damage { | |||
| const results: Array<DamageInstance> = [] | |||
| this.damages.forEach(damage => { | |||
| results.push({ | |||
| type: damage.type, | |||
| amount: damage.amount * factor | |||
| }) | |||
| }) | |||
| return new Damage(...results) | |||
| } | |||
| } | |||
| export enum Stat { | |||
| STR = 'Strength', | |||
| DEX = 'Dexterity', | |||
| CON = 'Constitution' | |||
| } | |||
| export type Stats = {[key in Stat]: number} | |||
| export enum State { | |||
| Normal, | |||
| Grappled, | |||
| Grappling, | |||
| Eaten, | |||
| } | |||
| export interface Combatant { | |||
| actions: Array<Action>; | |||
| } | |||
| export abstract class Action { | |||
| public name: string; | |||
| allowed (user: Creature, target: Creature) { | |||
| return this.userStates.has(user.state) && this.targetStates.has(target.state) | |||
| } | |||
| abstract execute(user: Creature, target: Creature): LogEntry | |||
| constructor (name: string, public userStates: Set<State>, public targetStates: Set<State>) { | |||
| this.name = name | |||
| } | |||
| toString (): string { | |||
| return this.name | |||
| } | |||
| } | |||
| abstract class SelfAction extends Action { | |||
| allowed (user: Creature, target: Creature) { | |||
| if (user === target) { | |||
| return super.allowed(user, target) | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| } | |||
| abstract class PairAction extends Action { | |||
| allowed (user: Creature, target: Creature) { | |||
| if (user !== target) { | |||
| return super.allowed(user, target) | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| } | |||
| export class AttackAction extends PairAction { | |||
| 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', new Set([State.Normal]), new Set([State.Normal])) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| target.takeDamage(this.damage) | |||
| return this.lines[user.perspective][target.perspective](user, target) | |||
| } | |||
| } | |||
| export class DevourAction extends PairAction { | |||
| 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) | |||
| } | |||
| } | |||
| constructor (protected container: Container) { | |||
| super('Devour', new Set([State.Normal]), new Set([State.Normal])) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| target.state = State.Eaten | |||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), this.container.consume(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) | |||
| } | |||
| } | |||
| constructor () { | |||
| super('Struggle', new Set([State.Eaten]), new Set([State.Normal])) | |||
| } | |||
| 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...") } | |||
| } | |||
| } | |||
| 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.') | |||
| } | |||
| } | |||
| allowed (user: Creature, target: Creature) { | |||
| if (Array.from(user.containers).some(container => { | |||
| return container.contents.size > 0 | |||
| })) { | |||
| return super.allowed(user, target) | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| constructor () { | |||
| super('Digest', new Set([State.Normal]), new Set([State.Normal])) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const results = new CompositeLog(...Array.from(user.containers).map(container => container.tick(60))) | |||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), results) | |||
| } | |||
| } | |||
| 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.') | |||
| } | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| import { Wolf } from './creatures/wolf' | |||
| import { Player } from './creatures/player' | |||
| export { Wolf, Player } | |||
| @@ -0,0 +1,18 @@ | |||
| import { Creature, POV } from '../entity' | |||
| import { ProperNoun, TheyPronouns } from '../language' | |||
| import { Stat, Damage, AttackAction, DevourAction, DamageType } from '../combat' | |||
| import { Stomach, 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) | |||
| 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 })) | |||
| this.containers.add(stomach) | |||
| this.actions.push(new DevourAction(stomach)) | |||
| this.perspective = POV.First | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| import { Creature, POV } from '../entity' | |||
| import { Stat, Damage, DamageType, AttackAction, DevourAction, StruggleAction, DigestAction } from '../combat' | |||
| import { MalePronouns, ImproperNoun } from '../language' | |||
| import { VoreType, Stomach } 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) | |||
| this.actions.push(new BiteAction()) | |||
| const stomach = new Stomach(this, 50, new Damage({ amount: 5, type: DamageType.Acid }, { amount: 5, type: DamageType.Crush })) | |||
| this.containers.add(stomach) | |||
| this.actions.push(new DevourAction(stomach)) | |||
| this.actions.push(new StruggleAction()) | |||
| this.actions.push(new DigestAction()) | |||
| } | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| import { DamageType, Damage, Combatant, Stats, State, Action } from './combat' | |||
| import { Noun, Pronoun } from './language' | |||
| import { Pred, Prey, Container, VoreType } from './vore' | |||
| export enum POV {First, Third} | |||
| export interface Entity { | |||
| name: Noun; | |||
| pronouns: Pronoun; | |||
| perspective: POV; | |||
| } | |||
| export interface Mortal extends Entity { | |||
| health: number; | |||
| resistances: Map<DamageType, number>; | |||
| takeDamage: (damage: Damage) => void; | |||
| stats: Stats; | |||
| } | |||
| export class Creature implements Mortal, Pred, Prey, Combatant { | |||
| health = 100 | |||
| resistances: Map<DamageType, number> = new Map() | |||
| perspective: POV = POV.Third | |||
| state: State = State.Normal | |||
| containers: Set<Container> = new Set(); | |||
| actions: Array<Action> = []; | |||
| containedIn: Container|null = null; | |||
| constructor (public name: Noun, public pronouns: Pronoun, public stats: Stats, public preyPrefs: Set<VoreType>, public predPrefs: Set<VoreType>, public bulk: number) { | |||
| } | |||
| toString (): string { | |||
| return this.name + ': ' + this.health + ' HP' | |||
| } | |||
| takeDamage (damage: Damage): void { | |||
| damage.damages.forEach(instance => { | |||
| if (this.resistances.has(instance.type)) { | |||
| this.health -= instance.amount * this.resistances.get(instance.type)! | |||
| } else { | |||
| this.health -= instance.amount | |||
| } | |||
| }) | |||
| } | |||
| validActions (target: Creature): Array<Action> { | |||
| console.log(this) | |||
| return this.actions.filter(action => { | |||
| return action.allowed(this, target) | |||
| }) | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| import { Wolf } from './creatures/wolf' | |||
| import { POV } from './entity' | |||
| import { CompositeLog, LogLines, log } from './interface' | |||
| const wolf = new Wolf() | |||
| const player = new Wolf() | |||
| player.perspective = POV.First | |||
| log(new CompositeLog(...player.validActions(wolf).map(action => new LogLines(action.toString())))) | |||
| log(new CompositeLog(...player.validActions(player).map(action => new LogLines(action.toString())))) | |||
| log(player.actions[1].execute(player, wolf)) | |||
| log(new CompositeLog(...player.validActions(wolf).map(action => new LogLines(action.toString())))) | |||
| log(new CompositeLog(...player.validActions(player).map(action => new LogLines(action.toString())))) | |||
| @@ -0,0 +1,38 @@ | |||
| export interface LogEntry { | |||
| render: () => HTMLElement[]; | |||
| } | |||
| export class LogLines implements LogEntry { | |||
| lines: string[] | |||
| constructor (...lines: string[]) { | |||
| this.lines = lines | |||
| } | |||
| render (): HTMLElement[] { | |||
| const p = document.createElement('p') | |||
| this.lines.forEach(line => { | |||
| const div = document.createElement('div') | |||
| div.innerText = line | |||
| p.appendChild(div) | |||
| }) | |||
| return [p] | |||
| } | |||
| } | |||
| export class CompositeLog implements LogEntry { | |||
| entries: LogEntry[] | |||
| constructor (...entries: LogEntry[]) { | |||
| this.entries = entries | |||
| } | |||
| render (): HTMLElement[] { | |||
| return this.entries.map(entry => entry.render()).reduce((results: HTMLElement[], next: HTMLElement[]) => results.concat(next), []) | |||
| } | |||
| } | |||
| export function log (entry: LogEntry): void { | |||
| entry.render().forEach(elem => document.body.appendChild(elem)) | |||
| } | |||
| @@ -0,0 +1,209 @@ | |||
| import { Creature, POV } from './entity' | |||
| import { LogEntry } from './interface' | |||
| export type POVActionPicker = { [key in POV]: { [key in POV]: (user: Creature, target: Creature) => LogEntry } } | |||
| enum NounKind { | |||
| Specific, | |||
| Nonspecific, | |||
| All | |||
| } | |||
| enum VowelSound { | |||
| Default, | |||
| Vowel, | |||
| NonVowel | |||
| } | |||
| interface WordOptions { | |||
| plural: boolean; | |||
| capital: boolean; | |||
| proper: boolean; | |||
| kind: NounKind; | |||
| vowel: VowelSound; | |||
| count: boolean; | |||
| } | |||
| export class Noun { | |||
| constructor (private singularNoun: string, private pluralNoun: string|null = null, private options: WordOptions = { plural: false, capital: false, proper: false, kind: NounKind.Specific, vowel: VowelSound.Default, count: true }) { | |||
| } | |||
| get capital (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.capital = true | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get plural (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.plural = true | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get proper (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.proper = true | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get improper (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.proper = false | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get specific (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.kind = NounKind.Specific | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get nonspecific (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.kind = NounKind.Nonspecific | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get all (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.kind = NounKind.All | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| get uncountable (): Noun { | |||
| const opts: WordOptions = Object.assign({}, this.options) | |||
| opts.count = false | |||
| return new Noun(this.singularNoun, this.pluralNoun, opts) | |||
| } | |||
| toString (): string { | |||
| let result: string | |||
| if (this.options.plural) { | |||
| if (this.pluralNoun === null) { | |||
| result = this.singularNoun | |||
| } else { | |||
| result = (this.pluralNoun as string) | |||
| } | |||
| } else { | |||
| result = this.singularNoun | |||
| } | |||
| if (!this.options.proper) { | |||
| if (this.options.kind === NounKind.Nonspecific && this.options.count) { | |||
| if (this.options.plural) { | |||
| result = 'some ' + result | |||
| } else { | |||
| if (this.options.vowel === VowelSound.Default) { | |||
| if ('aeiouAEIOU'.indexOf(result.slice(0, 1)) >= 0) { | |||
| result = 'an ' + result | |||
| } else { | |||
| result = 'a ' + result | |||
| } | |||
| } else if (this.options.vowel === VowelSound.Vowel) { | |||
| result = 'an ' + result | |||
| } else if (this.options.vowel === VowelSound.NonVowel) { | |||
| result = 'a ' + result | |||
| } | |||
| } | |||
| } else if (this.options.kind === NounKind.Specific) { | |||
| result = 'the ' + result | |||
| } | |||
| } | |||
| if (this.options.capital) { | |||
| result = result.slice(0, 1).toUpperCase() + result.slice(1) | |||
| } | |||
| return result | |||
| } | |||
| } | |||
| export class ImproperNoun extends Noun { | |||
| constructor (singularNoun: string, pluralNoun: string) { | |||
| super(singularNoun, pluralNoun, { plural: false, capital: false, proper: false, kind: NounKind.Specific, vowel: VowelSound.Default, count: true }) | |||
| } | |||
| } | |||
| export class ProperNoun extends Noun { | |||
| constructor (singularNoun: string) { | |||
| super(singularNoun, null, { plural: false, capital: false, proper: true, kind: NounKind.Specific, vowel: VowelSound.Default, count: true }) | |||
| } | |||
| } | |||
| interface PronounDict { | |||
| subjective: string; | |||
| objective: string; | |||
| possessive: string; | |||
| reflexive: string; | |||
| } | |||
| export class Pronoun { | |||
| constructor (private pronouns: PronounDict, private capitalize: boolean = false) { | |||
| } | |||
| get capital (): Pronoun { | |||
| return new Pronoun(this.pronouns, true) | |||
| } | |||
| get subjective (): string { | |||
| return this.caps(this.pronouns.subjective) | |||
| } | |||
| get objective (): string { | |||
| return this.caps(this.pronouns.objective) | |||
| } | |||
| get possessive (): string { | |||
| return this.caps(this.pronouns.possessive) | |||
| } | |||
| get reflexive (): string { | |||
| return this.caps(this.pronouns.reflexive) | |||
| } | |||
| private caps (input: string): string { | |||
| if (this.capitalize) { | |||
| return input.slice(0, 1).toUpperCase() + input.slice(1) | |||
| } else { | |||
| return input | |||
| } | |||
| } | |||
| } | |||
| export const MalePronouns = new Pronoun({ | |||
| subjective: 'he', | |||
| objective: 'him', | |||
| possessive: 'his', | |||
| reflexive: 'himself' | |||
| }) | |||
| export const FemalePronouns = new Pronoun({ | |||
| subjective: 'she', | |||
| objective: 'her', | |||
| possessive: 'her', | |||
| reflexive: 'herself' | |||
| }) | |||
| export const TheyPronouns = new Pronoun({ | |||
| subjective: 'they', | |||
| objective: 'them', | |||
| possessive: 'their', | |||
| reflexive: 'themself' | |||
| }) | |||
| export const TheyPluralPronouns = new Pronoun({ | |||
| subjective: 'they', | |||
| objective: 'them', | |||
| possessive: 'their', | |||
| reflexive: 'themselves' | |||
| }) | |||
| export const ObjectPronouns = new Pronoun({ | |||
| subjective: 'it', | |||
| objective: 'it', | |||
| possessive: 'its', | |||
| reflexive: 'itself' | |||
| }) | |||
| @@ -0,0 +1,172 @@ | |||
| import { Entity, Mortal, POV } from './entity' | |||
| import { Damage } from './combat' | |||
| import { LogLines, LogEntry, CompositeLog } from './interface' | |||
| export enum VoreType {Oral} | |||
| export interface Prey extends Mortal { | |||
| preyPrefs: Set<VoreType>; | |||
| bulk: number; | |||
| containedIn: Container | null; | |||
| } | |||
| export interface Pred extends Entity { | |||
| predPrefs: Set<VoreType>; | |||
| containers: Set<Container>; | |||
| } | |||
| export interface Container { | |||
| name: string; | |||
| voreTypes: Set<VoreType>; | |||
| contents: Set<Prey>; | |||
| capacity: number; | |||
| fullness: number; | |||
| canTake: (prey: Prey) => boolean; | |||
| consume: (prey: Prey) => LogEntry; | |||
| release: (prey: Prey) => LogEntry; | |||
| struggle: (prey: Prey) => LogEntry; | |||
| tick: (dt: number) => LogEntry; | |||
| describe: () => LogEntry; | |||
| digest: (prey: Prey) => LogEntry; | |||
| absorb: (prey: Prey) => LogEntry; | |||
| dispose: (preys: Prey[]) => LogEntry; | |||
| } | |||
| abstract class NormalContainer implements Container { | |||
| contents: Set<Prey> | |||
| get fullness (): number { | |||
| return Array.from(this.contents.values()).reduce((total: number, prey: Prey) => total + prey.bulk, 0) | |||
| } | |||
| canTake (prey: Prey): boolean { | |||
| const fits = this.capacity - this.fullness >= prey.bulk | |||
| const permitted = Array.from(this.voreTypes).every(voreType => { | |||
| return prey.preyPrefs.has(voreType) | |||
| }) | |||
| return fits && permitted | |||
| } | |||
| consume (prey: Prey): LogEntry { | |||
| this.contents.add(prey) | |||
| prey.containedIn = this | |||
| return new LogLines('MUNCH') | |||
| } | |||
| release (prey: Prey): LogEntry { | |||
| prey.containedIn = null | |||
| return new LogLines('ANTI-MUNCH') | |||
| } | |||
| struggle (prey: Prey): LogEntry { | |||
| return new LogLines('Slosh!') | |||
| } | |||
| tick (dt: number): LogEntry { | |||
| const digested: Array<Prey> = [] | |||
| const absorbed: Array<Prey> = [] | |||
| this.contents.forEach(prey => { | |||
| const start = prey.health | |||
| prey.takeDamage(this.damage.scale(dt / 3600)) | |||
| const end = prey.health | |||
| if (start > 0 && end <= 0) { | |||
| digested.push(prey) | |||
| } else if (start > -100 && end <= -100) { | |||
| absorbed.push(prey) | |||
| } | |||
| }) | |||
| const digestedEntries = new CompositeLog(...digested.map(prey => this.digest(prey))) | |||
| const absorbedEntries = new CompositeLog(...digested.map(prey => this.absorb(prey))) | |||
| this.contents = new Set(Array.from(this.contents.values()).filter(prey => { | |||
| return prey.health > 0 | |||
| })) | |||
| return new CompositeLog(digestedEntries, absorbedEntries) | |||
| } | |||
| describe (): LogEntry { | |||
| const lines: Array<string> = [] | |||
| this.contents.forEach(prey => { | |||
| lines.push(prey.toString()) | |||
| }) | |||
| return new LogLines(...lines) | |||
| } | |||
| digest (prey: Prey): LogEntry { | |||
| return new LogLines('Glorp!') | |||
| } | |||
| absorb (prey: Prey): LogEntry { | |||
| return new LogLines('Glorp...') | |||
| } | |||
| dispose (preys: Prey[]): LogEntry { | |||
| return new LogLines('GLORP') | |||
| } | |||
| constructor (public name: string, protected owner: Pred, public voreTypes: Set<VoreType>, public capacity: number, private damage: Damage) { | |||
| this.contents = new Set() | |||
| } | |||
| } | |||
| export class Stomach extends NormalContainer { | |||
| constructor (owner: Pred, capacity: number, damage: Damage) { | |||
| super('Stomach', owner, new Set([VoreType.Oral]), capacity, damage) | |||
| } | |||
| 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!') | |||
| } | |||
| } | |||
| } | |||