| @@ -1,8 +1,12 @@ | |||
| <template> | |||
| <div v-if="container.fullness > 0" class="statblock"> | |||
| <h3>{{container.name.capital}}</h3> | |||
| <div>{{container.fullness}} / {{container.capacity}}</div> | |||
| <div v-for="(prey, index) in container.contents" :key="'prey-' + index">{{prey.name}}</div> | |||
| <div v-show="container.fullness > 0" class="vore-container"> | |||
| <div class="container-name">{{container.name.capital}}</div> | |||
| <div class="container-fullness">{{container.fullness}} / {{container.capacity}}</div> | |||
| <p v-if="container.contents.length > 0">Live prey:</p> | |||
| <div class="container-prey" v-for="(prey, index) in container.contents" :key="'live-prey-' + index">{{prey.name}}</div> | |||
| <p v-if="container.digested.length > 0">Digested:</p> | |||
| <div class="container-prey" v-for="(prey, index) in container.digested" :key="'dead-prey-' + index">{{prey.name}}</div> | |||
| <canvas class="container-waves"></canvas> | |||
| </div> | |||
| </template> | |||
| @@ -11,34 +15,81 @@ import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||
| import { Creature } from '@/game/creature' | |||
| import { POV } from '@/game/language' | |||
| import { Stats, Stat } from '@/game/combat' | |||
| import { Container } from '@/game/vore' | |||
| import { Container, VoreContainer, Vore } from '@/game/vore' | |||
| // yoinked from https://jsfiddle.net/yckart/0adfw47y/ | |||
| function draw (delta: number, dt: number, total: number, parent: HTMLElement, canvas: HTMLCanvasElement, container: VoreContainer) { | |||
| const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | |||
| canvas.width = parent.clientWidth | |||
| canvas.height = parent.clientHeight | |||
| ctx.fillStyle = container.fluidColor | |||
| const fraction = container.fullness / container.capacity | |||
| const livingFraction = container.contents.reduce((total: number, prey: Vore) => total + prey.voreStats.Bulk, 0) / container.capacity | |||
| const deadFraction = container.digested.reduce((total: number, prey: Vore) => total + prey.voreStats.Bulk, 0) / container.capacity | |||
| const liveliness = livingFraction + deadFraction * 0.5 | |||
| total += dt * liveliness | |||
| requestAnimationFrame((newDelta: number) => draw(newDelta, newDelta - delta, total, parent, canvas, container)) | |||
| const randomLeft = Math.abs(Math.pow(Math.sin(total / 1000), 2)) * fraction * 100 + (1 - fraction) * canvas.height | |||
| const randomRight = Math.abs(Math.pow(Math.sin((total / 1000) + 10), 2)) * fraction * 100 + (1 - fraction) * canvas.height | |||
| const randomLeftConstraint = Math.abs(Math.pow(Math.sin((total / 1000) + 2), 2)) * fraction * 100 + (1 - fraction) * canvas.height | |||
| const randomRightConstraint = Math.abs(Math.pow(Math.sin((total / 1000) + 1), 2)) * fraction * 100 + (1 - fraction) * canvas.height | |||
| ctx.beginPath() | |||
| ctx.moveTo(0, randomLeft) | |||
| ctx.bezierCurveTo(canvas.width / 3, randomLeftConstraint, canvas.width / 3 * 2, randomRightConstraint, canvas.width, randomRight) | |||
| ctx.lineTo(canvas.width, canvas.height) | |||
| ctx.lineTo(0, canvas.height) | |||
| ctx.lineTo(0, randomLeft) | |||
| ctx.closePath() | |||
| ctx.fill() | |||
| } | |||
| @Component | |||
| export default class ContainerView extends Vue { | |||
| @Prop({ required: true }) | |||
| container!: Container | |||
| constructor () { | |||
| super() | |||
| mounted () { | |||
| if ((this.container as VoreContainer).fluidColor !== undefined) { | |||
| const canvas = this.$el.querySelector('.container-waves') as HTMLCanvasElement | |||
| canvas.width = (this.$el as HTMLElement).clientWidth | |||
| canvas.height = (this.$el as HTMLElement).clientHeight | |||
| canvas.width = canvas.width + 0 | |||
| requestAnimationFrame((delta: number) => draw(delta, delta, Math.random() * 1000, this.$el as HTMLElement, canvas, (this.container as VoreContainer))) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <!-- Add "scoped" attribute to limit CSS to this component only --> | |||
| <style scoped> | |||
| h3 { | |||
| margin: 40px 0 0; | |||
| font-size: 125%; | |||
| } | |||
| ul { | |||
| list-style-type: none; | |||
| padding: 0; | |||
| } | |||
| li { | |||
| display: inline-block; | |||
| margin: 0 10px; | |||
| } | |||
| a { | |||
| color: #42b983; | |||
| } | |||
| .vore-container { | |||
| position: relative; | |||
| min-width: 100pt; | |||
| } | |||
| .container-name { | |||
| margin: 8pt; | |||
| font-size: 150%; | |||
| } | |||
| .container-fullness { | |||
| margin: 6pt; | |||
| font-size: 125%; | |||
| } | |||
| .container-waves { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| } | |||
| </style> | |||
| @@ -8,6 +8,9 @@ | |||
| <p class="worldinfo-date">{{ world.time.format("hh:mm:ss a") }}</p> | |||
| </div> | |||
| <Statblock :subject="world.player" :initiative="0" /> | |||
| <div class="explore-containers"> | |||
| <ContainerView :container="container" v-for="(container, index) in world.player.containers" :key="'explore-container-' + index" /> | |||
| </div> | |||
| <div class="explore-info"> | |||
| <h2 class="location-name">{{ location.name.capital }}</h2> | |||
| <p class="location-desc">{{ location.desc }}</p> | |||
| @@ -28,11 +31,12 @@ import { Direction, World, Place } from '@/game/world' | |||
| import NavButton from './NavButton.vue' | |||
| import ChoiceButton from './ChoiceButton.vue' | |||
| import Statblock from './Statblock.vue' | |||
| import ContainerView from './ContainerView.vue' | |||
| import { LogEntry } from '@/game/interface' | |||
| @Component({ | |||
| components: { | |||
| NavButton, ChoiceButton, Statblock | |||
| NavButton, ChoiceButton, Statblock, ContainerView | |||
| }, | |||
| data () { | |||
| return { | |||
| @@ -89,18 +93,26 @@ export default class Explore extends Vue { | |||
| flex: 10; | |||
| position: relative; | |||
| display: grid; | |||
| grid-template-areas: "log worldinfo" | |||
| "log statblock" | |||
| "log info " | |||
| "log choices " | |||
| "nav choices "; | |||
| grid-template-rows: 0.5fr fit-content(250pt) 2fr 1fr 1fr; | |||
| grid-template-columns: 2fr 1fr; | |||
| grid-template-areas: "statblock containers containers" | |||
| "log log worldinfo" | |||
| "log log info " | |||
| "log log choices " | |||
| "nav nav choices "; | |||
| grid-template-rows: fit-content(30%) fit-content(250pt) 2fr 1fr 1fr; | |||
| grid-template-columns: 1fr 1fr 1fr; | |||
| width: 100%; | |||
| height: 100%; | |||
| overflow: hidden; | |||
| } | |||
| .explore-containers { | |||
| grid-area: containers; | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: nowrap; | |||
| overflow-x: scroll; | |||
| } | |||
| .explore-log { | |||
| grid-area: log; | |||
| background: #222; | |||
| @@ -21,7 +21,7 @@ export class Human extends Creature { | |||
| } else { | |||
| stats = options.stats | |||
| } | |||
| super(name, new ImproperNoun('human', 'humans'), pronouns, stats, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 25) | |||
| super(name, new ImproperNoun('human', 'humans'), pronouns, stats, new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock, VoreType.Unbirth]), new Set([VoreType.Oral, VoreType.Anal]), 25) | |||
| this.title = "Snack" | |||
| this.desc = "Definitely going on an adventure" | |||
| @@ -1,12 +1,13 @@ | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side } from '../combat' | |||
| import { MalePronouns, ImproperNoun } from '../language' | |||
| import { VoreType, Stomach, Bowels } from '../vore' | |||
| import { MalePronouns, ImproperNoun, ProperNoun, ObjectPronouns } from '../language' | |||
| import { VoreType, Stomach, Bowels, Cock, Balls } from '../vore' | |||
| import { AttackAction, TransferAction, FeedAction } from '../combat/actions' | |||
| import { Human } from '../creatures' | |||
| export class Wolf extends Creature { | |||
| constructor () { | |||
| super(new ImproperNoun('wolf', 'wolves'), new ImproperNoun('wolf', 'wolves'), MalePronouns, { Toughness: 20, Power: 20, Speed: 20, Willpower: 20, Charm: 20 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 25) | |||
| super(new ImproperNoun('wolf', 'wolves'), new ImproperNoun('wolf', 'wolves'), MalePronouns, { Toughness: 20, Power: 20, Speed: 20, Willpower: 20, Charm: 20 }, new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), new Set([VoreType.Oral, VoreType.Anal, VoreType.Cock]), 25) | |||
| this.actions.push( | |||
| new AttackAction( | |||
| new ConstantDamageFormula( | |||
| @@ -24,7 +25,6 @@ export class Wolf extends Creature { | |||
| { amount: 30, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 30, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| )) | |||
| this.containers.push(stomach) | |||
| const bowels = new Bowels(this, 50, new Damage( | |||
| @@ -38,5 +38,20 @@ export class Wolf extends Creature { | |||
| this.actions.push(new TransferAction(bowels, stomach)) | |||
| this.otherActions.push(new FeedAction(stomach)) | |||
| const cock = new Cock(this, 50, new Damage( | |||
| { amount: 30, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 60, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 60, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| )) | |||
| const balls = new Balls(this, 50, new Damage( | |||
| { amount: 30, type: DamageType.Crush, target: Vigor.Health }, | |||
| { amount: 60, type: DamageType.Crush, target: Vigor.Stamina }, | |||
| { amount: 60, type: DamageType.Dominance, target: Vigor.Resolve } | |||
| ), cock) | |||
| this.containers.push(balls) | |||
| this.containers.push(cock) | |||
| } | |||
| } | |||
| @@ -1,10 +1,12 @@ | |||
| import { Place, Choice, Direction } from '../world' | |||
| import { ProperNoun, ImproperNoun, MalePronouns, FemalePronouns } from '../language' | |||
| import { ProperNoun, ImproperNoun, MalePronouns, FemalePronouns, TheyPronouns } from '../language' | |||
| import { Encounter } from '../combat' | |||
| import * as Creatures from '../creatures' | |||
| import * as Items from '../items' | |||
| import { LogLine, nilLog } from '../interface' | |||
| import { Creature } from '../creature' | |||
| import { DevourAction } from '../combat/actions' | |||
| import { SurrenderEffect } from '../combat/effects' | |||
| function makeParty (): Creature[] { | |||
| const fighter = new Creatures.Human(new ProperNoun("Redgar"), MalePronouns, { | |||
| @@ -126,6 +128,19 @@ export const Town = (): Place => { | |||
| ) | |||
| ] | |||
| home.choices.push( | |||
| new Choice( | |||
| "Eat someone", | |||
| "Slurp", | |||
| (world, executor) => { | |||
| const snack = new Creatures.Human(new ProperNoun("Snack"), TheyPronouns) | |||
| snack.applyEffect(new SurrenderEffect()) | |||
| const options = executor.validActions(snack).filter(action => action instanceof DevourAction) | |||
| return options[Math.floor(options.length * Math.random())].execute(executor, snack) | |||
| } | |||
| ) | |||
| ) | |||
| bossEncounters.forEach(encounter => { | |||
| bosses.choices.push( | |||
| new Choice( | |||
| @@ -123,9 +123,9 @@ export abstract class NormalContainer implements Container { | |||
| contents: Array<Vore> = [] | |||
| actions: Array<Action> = [] | |||
| abstract consumeVerb: Verb | |||
| abstract releaseVerb: Verb | |||
| abstract struggleVerb: Verb | |||
| consumeVerb = new Verb('trap') | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| constructor (name: Noun, public owner: Vore, public voreTypes: Set<VoreType>, public capacity: number) { | |||
| this.name = name.all | |||
| @@ -214,12 +214,14 @@ export interface VoreContainer extends Container { | |||
| digested: Array<Vore>; | |||
| tick: (dt: number) => LogEntry; | |||
| digest: (preys: Vore[]) => LogEntry; | |||
| fluidColor: string; | |||
| } | |||
| export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer { | |||
| consumeVerb = new Verb('devour') | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| fluidColor = "#00ff0088" | |||
| digested: Array<Vore> = [] | |||
| @@ -231,6 +233,10 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| this.actions.push(new DigestAction(this)) | |||
| } | |||
| get fullness (): number { | |||
| return Array.from(this.contents.concat(this.digested).values()).reduce((total: number, prey: Vore) => total + prey.voreStats.Bulk, 0) | |||
| } | |||
| consumeLine: PairLineArgs<Vore, { container: Container }> = (user, target, args) => { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective}, forcing ${target.pronouns.objective} into ${user.pronouns.possessive} ${args.container.name}.`) | |||
| } | |||
| @@ -240,7 +246,7 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| } | |||
| digestLine: PairLineArgs<Vore, { container: VoreContainer }> = (user, target, args) => { | |||
| return new LogLine(`${user.name.capital.possessive} ${args.container.name} finishes ${Words.Digests.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggles.singular} fading away.`) | |||
| return new LogLine(`${user.name.capital.possessive} ${args.container.name} ${args.container.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Digests.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggles.singular} fading away.`) | |||
| } | |||
| tick (dt: number): LogEntry { | |||
| @@ -328,6 +334,35 @@ export class InnerStomach extends InnerVoreContainer { | |||
| export class Bowels extends NormalVoreContainer { | |||
| constructor (owner: Vore, capacity: number, damage: Damage) { | |||
| super(new ImproperNoun('bowel', 'bowels').plural, owner, new Set([VoreType.Anal]), capacity, damage) | |||
| super(new ImproperNoun('bowel', 'bowels').plural.all, owner, new Set([VoreType.Anal]), capacity, damage) | |||
| } | |||
| } | |||
| export class Cock extends NormalVoreContainer { | |||
| fluidColor = "#eeeeee66"; | |||
| constructor (owner: Vore, capacity: number, damage: Damage) { | |||
| super( | |||
| new ImproperNoun('cock').all, | |||
| owner, | |||
| new Set([VoreType.Cock]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| } | |||
| } | |||
| export class Balls extends InnerVoreContainer { | |||
| fluidColor = "#eeeeeecc"; | |||
| constructor (owner: Vore, capacity: number, damage: Damage, escape: Container) { | |||
| super( | |||
| new ImproperNoun('ball', 'balls').all.plural, | |||
| owner, | |||
| new Set([VoreType.Cock]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ import { TextLike, Verb, Noun, ProperNoun } from './language' | |||
| import { Entity } from './entity' | |||
| import { Creature } from './creature' | |||
| import moment, { Moment, Duration } from 'moment' | |||
| import { LogEntry, LogLine } from './interface' | |||
| import { LogEntry, LogLine, LogLines } from './interface' | |||
| import { Encounter } from './combat' | |||
| export enum Direction { | |||
| @@ -57,9 +57,10 @@ export class Connection { | |||
| } | |||
| travel (world: World, traveler: Creature): LogEntry { | |||
| world.advance(moment.duration(5, "minutes")) | |||
| const advanceLogs = world.advance(moment.duration(5, "minutes")) | |||
| traveler.location = this.dst | |||
| return new LogLine( | |||
| return new LogLines( | |||
| advanceLogs, | |||
| `${traveler.name.capital} ${traveler.name.conjugate(new Verb('travel'))} to ${this.dst.name}.` | |||
| ) | |||
| } | |||
| @@ -98,7 +99,12 @@ export class World { | |||
| this.creatures.push(player) | |||
| } | |||
| advance (dt: Duration) { | |||
| advance (dt: Duration): LogEntry { | |||
| this.time.add(dt) | |||
| return new LogLines( | |||
| ...this.player.containers.map( | |||
| container => container.tick(dt.asSeconds()) | |||
| ) | |||
| ) | |||
| } | |||
| } | |||