| @@ -1,8 +1,12 @@ | |||||
| <template> | <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> | </div> | ||||
| </template> | </template> | ||||
| @@ -11,34 +15,81 @@ import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||||
| import { Creature } from '@/game/creature' | import { Creature } from '@/game/creature' | ||||
| import { POV } from '@/game/language' | import { POV } from '@/game/language' | ||||
| import { Stats, Stat } from '@/game/combat' | 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 | @Component | ||||
| export default class ContainerView extends Vue { | export default class ContainerView extends Vue { | ||||
| @Prop({ required: true }) | @Prop({ required: true }) | ||||
| container!: Container | 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> | </script> | ||||
| <!-- Add "scoped" attribute to limit CSS to this component only --> | <!-- Add "scoped" attribute to limit CSS to this component only --> | ||||
| <style scoped> | <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> | </style> | ||||
| @@ -8,6 +8,9 @@ | |||||
| <p class="worldinfo-date">{{ world.time.format("hh:mm:ss a") }}</p> | <p class="worldinfo-date">{{ world.time.format("hh:mm:ss a") }}</p> | ||||
| </div> | </div> | ||||
| <Statblock :subject="world.player" :initiative="0" /> | <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"> | <div class="explore-info"> | ||||
| <h2 class="location-name">{{ location.name.capital }}</h2> | <h2 class="location-name">{{ location.name.capital }}</h2> | ||||
| <p class="location-desc">{{ location.desc }}</p> | <p class="location-desc">{{ location.desc }}</p> | ||||
| @@ -28,11 +31,12 @@ import { Direction, World, Place } from '@/game/world' | |||||
| import NavButton from './NavButton.vue' | import NavButton from './NavButton.vue' | ||||
| import ChoiceButton from './ChoiceButton.vue' | import ChoiceButton from './ChoiceButton.vue' | ||||
| import Statblock from './Statblock.vue' | import Statblock from './Statblock.vue' | ||||
| import ContainerView from './ContainerView.vue' | |||||
| import { LogEntry } from '@/game/interface' | import { LogEntry } from '@/game/interface' | ||||
| @Component({ | @Component({ | ||||
| components: { | components: { | ||||
| NavButton, ChoiceButton, Statblock | |||||
| NavButton, ChoiceButton, Statblock, ContainerView | |||||
| }, | }, | ||||
| data () { | data () { | ||||
| return { | return { | ||||
| @@ -89,18 +93,26 @@ export default class Explore extends Vue { | |||||
| flex: 10; | flex: 10; | ||||
| position: relative; | position: relative; | ||||
| display: grid; | 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%; | width: 100%; | ||||
| height: 100%; | height: 100%; | ||||
| overflow: hidden; | overflow: hidden; | ||||
| } | } | ||||
| .explore-containers { | |||||
| grid-area: containers; | |||||
| display: flex; | |||||
| flex-direction: row; | |||||
| flex-wrap: nowrap; | |||||
| overflow-x: scroll; | |||||
| } | |||||
| .explore-log { | .explore-log { | ||||
| grid-area: log; | grid-area: log; | ||||
| background: #222; | background: #222; | ||||
| @@ -21,7 +21,7 @@ export class Human extends Creature { | |||||
| } else { | } else { | ||||
| stats = options.stats | 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.title = "Snack" | ||||
| this.desc = "Definitely going on an adventure" | this.desc = "Definitely going on an adventure" | ||||
| @@ -1,12 +1,13 @@ | |||||
| import { Creature } from "../creature" | import { Creature } from "../creature" | ||||
| import { Damage, DamageType, ConstantDamageFormula, Vigor, Side } from '../combat' | 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 { AttackAction, TransferAction, FeedAction } from '../combat/actions' | ||||
| import { Human } from '../creatures' | |||||
| export class Wolf extends Creature { | export class Wolf extends Creature { | ||||
| constructor () { | 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( | this.actions.push( | ||||
| new AttackAction( | new AttackAction( | ||||
| new ConstantDamageFormula( | new ConstantDamageFormula( | ||||
| @@ -24,7 +25,6 @@ export class Wolf extends Creature { | |||||
| { amount: 30, type: DamageType.Crush, target: Vigor.Stamina }, | { amount: 30, type: DamageType.Crush, target: Vigor.Stamina }, | ||||
| { amount: 30, type: DamageType.Dominance, target: Vigor.Resolve } | { amount: 30, type: DamageType.Dominance, target: Vigor.Resolve } | ||||
| )) | )) | ||||
| this.containers.push(stomach) | this.containers.push(stomach) | ||||
| const bowels = new Bowels(this, 50, new Damage( | 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.actions.push(new TransferAction(bowels, stomach)) | ||||
| this.otherActions.push(new FeedAction(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 { 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 { Encounter } from '../combat' | ||||
| import * as Creatures from '../creatures' | import * as Creatures from '../creatures' | ||||
| import * as Items from '../items' | import * as Items from '../items' | ||||
| import { LogLine, nilLog } from '../interface' | import { LogLine, nilLog } from '../interface' | ||||
| import { Creature } from '../creature' | import { Creature } from '../creature' | ||||
| import { DevourAction } from '../combat/actions' | |||||
| import { SurrenderEffect } from '../combat/effects' | |||||
| function makeParty (): Creature[] { | function makeParty (): Creature[] { | ||||
| const fighter = new Creatures.Human(new ProperNoun("Redgar"), MalePronouns, { | 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 => { | bossEncounters.forEach(encounter => { | ||||
| bosses.choices.push( | bosses.choices.push( | ||||
| new Choice( | new Choice( | ||||
| @@ -123,9 +123,9 @@ export abstract class NormalContainer implements Container { | |||||
| contents: Array<Vore> = [] | contents: Array<Vore> = [] | ||||
| actions: Array<Action> = [] | 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) { | constructor (name: Noun, public owner: Vore, public voreTypes: Set<VoreType>, public capacity: number) { | ||||
| this.name = name.all | this.name = name.all | ||||
| @@ -214,12 +214,14 @@ export interface VoreContainer extends Container { | |||||
| digested: Array<Vore>; | digested: Array<Vore>; | ||||
| tick: (dt: number) => LogEntry; | tick: (dt: number) => LogEntry; | ||||
| digest: (preys: Vore[]) => LogEntry; | digest: (preys: Vore[]) => LogEntry; | ||||
| fluidColor: string; | |||||
| } | } | ||||
| export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer { | export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer { | ||||
| consumeVerb = new Verb('devour') | consumeVerb = new Verb('devour') | ||||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | ||||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | ||||
| fluidColor = "#00ff0088" | |||||
| digested: Array<Vore> = [] | digested: Array<Vore> = [] | ||||
| @@ -231,6 +233,10 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||||
| this.actions.push(new DigestAction(this)) | 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) => { | 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}.`) | 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) => { | 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 { | tick (dt: number): LogEntry { | ||||
| @@ -328,6 +334,35 @@ export class InnerStomach extends InnerVoreContainer { | |||||
| export class Bowels extends NormalVoreContainer { | export class Bowels extends NormalVoreContainer { | ||||
| constructor (owner: Vore, capacity: number, damage: Damage) { | 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 { Entity } from './entity' | ||||
| import { Creature } from './creature' | import { Creature } from './creature' | ||||
| import moment, { Moment, Duration } from 'moment' | import moment, { Moment, Duration } from 'moment' | ||||
| import { LogEntry, LogLine } from './interface' | |||||
| import { LogEntry, LogLine, LogLines } from './interface' | |||||
| import { Encounter } from './combat' | import { Encounter } from './combat' | ||||
| export enum Direction { | export enum Direction { | ||||
| @@ -57,9 +57,10 @@ export class Connection { | |||||
| } | } | ||||
| travel (world: World, traveler: Creature): LogEntry { | travel (world: World, traveler: Creature): LogEntry { | ||||
| world.advance(moment.duration(5, "minutes")) | |||||
| const advanceLogs = world.advance(moment.duration(5, "minutes")) | |||||
| traveler.location = this.dst | traveler.location = this.dst | ||||
| return new LogLine( | |||||
| return new LogLines( | |||||
| advanceLogs, | |||||
| `${traveler.name.capital} ${traveler.name.conjugate(new Verb('travel'))} to ${this.dst.name}.` | `${traveler.name.capital} ${traveler.name.conjugate(new Verb('travel'))} to ${this.dst.name}.` | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -98,7 +99,12 @@ export class World { | |||||
| this.creatures.push(player) | this.creatures.push(player) | ||||
| } | } | ||||
| advance (dt: Duration) { | |||||
| advance (dt: Duration): LogEntry { | |||||
| this.time.add(dt) | this.time.add(dt) | ||||
| return new LogLines( | |||||
| ...this.player.containers.map( | |||||
| container => container.tick(dt.asSeconds()) | |||||
| ) | |||||
| ) | |||||
| } | } | ||||
| } | } | ||||