| @@ -13,6 +13,8 @@ module.exports = { | |||||
| }, | }, | ||||
| rules: { | rules: { | ||||
| 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', | '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> | <template> | ||||
| <div id="app"> | <div id="app"> | ||||
| <img alt="Vue logo" src="./assets/logo.png"> | <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> | </div> | ||||
| </template> | </template> | ||||
| @@ -1,42 +1,49 @@ | |||||
| <template> | <template> | ||||
| <div class="hello"> | <div class="hello"> | ||||
| <h1>{{ msg }}</h1> | |||||
| <h1>VORE TIME</h1> | |||||
| <p> | <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> | </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> | </div> | ||||
| </template> | </template> | ||||
| <script lang="ts"> | <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 | @Component | ||||
| export default class HelloWorld extends Vue { | export default class HelloWorld extends Vue { | ||||
| @Prop() private msg!: string; | @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> | </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!') | |||||
| } | |||||
| } | |||||
| } | |||||