| @@ -1,8 +1,6 @@ | |||||
| <template> | <template> | ||||
| <div id="app"> | <div id="app"> | ||||
| <img alt="Vue logo" src="./assets/logo.png"> | <img alt="Vue logo" src="./assets/logo.png"> | ||||
| <Statblock :subject="player" /> | |||||
| <Statblock :subject="enemy" /> | |||||
| <Combat :player="player" :enemy="enemy" /> | <Combat :player="player" :enemy="enemy" /> | ||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| @@ -10,14 +8,12 @@ | |||||
| <script lang="ts"> | <script lang="ts"> | ||||
| import { Component, Vue } from 'vue-property-decorator' | import { Component, Vue } from 'vue-property-decorator' | ||||
| import Combat from './components/Combat.vue' | import Combat from './components/Combat.vue' | ||||
| import Statblock from './components/Statblock.vue' | |||||
| import * as Creatures from '@/game/creatures' | import * as Creatures from '@/game/creatures' | ||||
| import { Creature, POV } from '@/game/entity' | import { Creature, POV } from '@/game/entity' | ||||
| @Component({ | @Component({ | ||||
| components: { | components: { | ||||
| Combat, | |||||
| Statblock | |||||
| Combat | |||||
| } | } | ||||
| }) | }) | ||||
| export default class App extends Vue { | export default class App extends Vue { | ||||
| @@ -28,6 +24,7 @@ export default class App extends Vue { | |||||
| this.player = new Creatures.Wolf() | this.player = new Creatures.Wolf() | ||||
| this.player.perspective = POV.First | this.player.perspective = POV.First | ||||
| this.enemy = new Creatures.Wolf() | this.enemy = new Creatures.Wolf() | ||||
| console.log(this.player) | |||||
| } | } | ||||
| } | } | ||||
| </script> | </script> | ||||
| @@ -1,9 +1,12 @@ | |||||
| <template> | <template> | ||||
| <div class="hello"> | <div class="hello"> | ||||
| <h1>VORE TIME</h1> | <h1>VORE TIME</h1> | ||||
| <Statblock :subject="player" /> | |||||
| <Statblock :subject="enemy" /> | |||||
| <h2>Your moves</h2> | <h2>Your moves</h2> | ||||
| <button v-for="action in player.validActions(enemy)" :key="'player-' + action.name" v-on:click="log(action.execute(player, enemy))">{{action.name}}</button> | |||||
| <button v-for="action in player.validActions(player)" :key="'palyer-' + action.name" v-on:click="log(action.execute(player, player))">{{action.name}}</button> | |||||
| <button @mouseleave="actionDescription= ''" @mouseover="actionDescription = action.desc" v-for="action in player.validActions(enemy)" :key="'player-' + action.name" v-on:click="log(action.execute(player, enemy))">{{action.name}}</button> | |||||
| <button @mouseleave="actionDescription= ''" @mouseover="actionDescription = action.desc" v-for="action in player.validActions(player)" :key="'player-' + action.name" v-on:click="log(action.execute(player, player))">{{action.name}}</button> | |||||
| <div>{{actionDescription}}</div> | |||||
| <div id="log"></div> | <div id="log"></div> | ||||
| <h2>Enemy moves</h2> | <h2>Enemy moves</h2> | ||||
| <button v-for="action in enemy.validActions(player)" :key="'enemy-' + action.name" v-on:click="log(action.execute(enemy, player))">{{action.name}}</button> | <button v-for="action in enemy.validActions(player)" :key="'enemy-' + action.name" v-on:click="log(action.execute(enemy, player))">{{action.name}}</button> | ||||
| @@ -15,8 +18,13 @@ | |||||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | ||||
| import { Creature, POV } from '@/game/entity' | import { Creature, POV } from '@/game/entity' | ||||
| import { log, LogEntry } from '@/game/interface' | import { log, LogEntry } from '@/game/interface' | ||||
| import Statblock from './Statblock.vue' | |||||
| @Component | |||||
| @Component( | |||||
| { | |||||
| components: { Statblock } | |||||
| } | |||||
| ) | |||||
| export default class Combat extends Vue { | export default class Combat extends Vue { | ||||
| @Prop({ type: Creature, required: true }) | @Prop({ type: Creature, required: true }) | ||||
| player!: Creature | player!: Creature | ||||
| @@ -24,6 +32,8 @@ export default class Combat extends Vue { | |||||
| @Prop({ type: Creature, required: true }) | @Prop({ type: Creature, required: true }) | ||||
| enemy!: Creature | enemy!: Creature | ||||
| actionDescription = '' | |||||
| private log: (entry: LogEntry) => void; | private log: (entry: LogEntry) => void; | ||||
| constructor () { | constructor () { | ||||
| @@ -0,0 +1,42 @@ | |||||
| <template> | |||||
| <div v-if="container.fullness > 0" class="statblock"> | |||||
| <h3>{{container.name}}</h3> | |||||
| <div>Fullness: {{container.fullness}} / {{container.capacity}}</div> | |||||
| <div v-for="(prey, index) in container.contents" :key="'prey-' + index">{{prey.name}}</div> | |||||
| </div> | |||||
| </template> | |||||
| <script lang="ts"> | |||||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||||
| import { Creature, POV } from '@/game/entity' | |||||
| import { Stats, Stat } from '@/game/combat' | |||||
| import { Container } from '@/game/vore' | |||||
| @Component | |||||
| export default class ContainerView extends Vue { | |||||
| @Prop({ required: true }) | |||||
| container!: Container | |||||
| constructor () { | |||||
| super() | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <!-- Add "scoped" attribute to limit CSS to this component only --> | |||||
| <style scoped> | |||||
| h3 { | |||||
| margin: 40px 0 0; | |||||
| } | |||||
| ul { | |||||
| list-style-type: none; | |||||
| padding: 0; | |||||
| } | |||||
| li { | |||||
| display: inline-block; | |||||
| margin: 0 10px; | |||||
| } | |||||
| a { | |||||
| color: #42b983; | |||||
| } | |||||
| </style> | |||||
| @@ -3,9 +3,10 @@ | |||||
| <h2>Stats</h2> | <h2>Stats</h2> | ||||
| <div v-if="subject.perspective === firstperson">Player</div> | <div v-if="subject.perspective === firstperson">Player</div> | ||||
| <div v-if="subject.perspective !== firstperson">Name: {{subject.name.all.capital}}</div> | <div v-if="subject.perspective !== firstperson">Name: {{subject.name.all.capital}}</div> | ||||
| <div>Health: {{subject.health.toFixed(0)}}</div> | |||||
| <div v-for="stat in Object.keys(subject.stats)" :key="stat">{{stat}}: {{subject.stats[stat]}}</div> | |||||
| <div>Health: {{subject.health.toFixed(0)}} / {{subject.maxHealth.toFixed(0)}}</div> | |||||
| <div v-for="stat in Object.keys(subject.stats)" v-bind:key="stat">{{stat}}: {{subject.stats[stat]}}</div> | |||||
| <div>Status: {{subject.state}}</div> | <div>Status: {{subject.state}}</div> | ||||
| <ContainerView v-for="container in subject.containers" :key="container.name" :container="container" /> | |||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| @@ -13,8 +14,13 @@ | |||||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | ||||
| import { Creature, POV } from '@/game/entity' | import { Creature, POV } from '@/game/entity' | ||||
| import { Stats, Stat } from '@/game/combat' | import { Stats, Stat } from '@/game/combat' | ||||
| import ContainerView from './ContainerView.vue' | |||||
| @Component | |||||
| @Component({ | |||||
| components: { | |||||
| ContainerView | |||||
| } | |||||
| }) | |||||
| export default class Statblock extends Vue { | export default class Statblock extends Vue { | ||||
| @Prop({ type: Creature, required: true }) | @Prop({ type: Creature, required: true }) | ||||
| subject!: Creature | subject!: Creature | ||||
| @@ -51,15 +51,14 @@ export interface Combatant { | |||||
| } | } | ||||
| export abstract class Action { | export abstract class Action { | ||||
| public name: string; | |||||
| allowed (user: Creature, target: Creature) { | allowed (user: Creature, target: Creature) { | ||||
| return this.userStates.has(user.state) && this.targetStates.has(target.state) | return this.userStates.has(user.state) && this.targetStates.has(target.state) | ||||
| } | } | ||||
| abstract execute(user: Creature, target: Creature): LogEntry | abstract execute(user: Creature, target: Creature): LogEntry | ||||
| constructor (name: string, public userStates: Set<State>, public targetStates: Set<State>) { | |||||
| this.name = name | |||||
| constructor (public name: string, public desc: string, public userStates: Set<State>, public targetStates: Set<State>) { | |||||
| } | } | ||||
| toString (): string { | toString (): string { | ||||
| @@ -100,7 +99,7 @@ export class AttackAction extends PairAction { | |||||
| } | } | ||||
| constructor (protected damage: Damage) { | constructor (protected damage: Damage) { | ||||
| super('Attack', new Set([State.Normal]), new Set([State.Normal])) | |||||
| super('Attack', 'Attack the enemy', new Set([State.Normal]), new Set([State.Normal])) | |||||
| } | } | ||||
| execute (user: Creature, target: Creature): LogEntry { | execute (user: Creature, target: Creature): LogEntry { | ||||
| @@ -122,7 +121,7 @@ export class DevourAction extends PairAction { | |||||
| } | } | ||||
| constructor (protected container: Container) { | constructor (protected container: Container) { | ||||
| super('Devour', new Set([State.Normal]), new Set([State.Normal])) | |||||
| super('Devour', 'Try to consume your foe', new Set([State.Normal]), new Set([State.Normal])) | |||||
| } | } | ||||
| execute (user: Creature, target: Creature): LogEntry { | execute (user: Creature, target: Creature): LogEntry { | ||||
| @@ -144,7 +143,7 @@ export class StruggleAction extends PairAction { | |||||
| } | } | ||||
| constructor () { | constructor () { | ||||
| super('Struggle', new Set([State.Eaten]), new Set([State.Normal])) | |||||
| super('Struggle', 'Try to escape your predator', new Set([State.Eaten]), new Set([State.Normal])) | |||||
| } | } | ||||
| execute (user: Creature, target: Creature): LogEntry { | execute (user: Creature, target: Creature): LogEntry { | ||||
| @@ -168,8 +167,8 @@ export class DigestAction extends SelfAction { | |||||
| } | } | ||||
| allowed (user: Creature, target: Creature) { | allowed (user: Creature, target: Creature) { | ||||
| if (Array.from(user.containers).some(container => { | |||||
| return container.contents.size > 0 | |||||
| if (user.containers.some(container => { | |||||
| return container.contents.length > 0 | |||||
| })) { | })) { | ||||
| return super.allowed(user, target) | return super.allowed(user, target) | ||||
| } else { | } else { | ||||
| @@ -178,11 +177,11 @@ export class DigestAction extends SelfAction { | |||||
| } | } | ||||
| constructor () { | constructor () { | ||||
| super('Digest', new Set([State.Normal]), new Set([State.Normal])) | |||||
| super('Digest', 'Digest all of your current prey', new Set([State.Normal]), new Set([State.Normal])) | |||||
| } | } | ||||
| execute (user: Creature, target: Creature): LogEntry { | execute (user: Creature, target: Creature): LogEntry { | ||||
| const results = new CompositeLog(...Array.from(user.containers).map(container => container.tick(60))) | |||||
| const results = new CompositeLog(...user.containers.map(container => container.tick(60))) | |||||
| return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), results) | return new CompositeLog(this.lines[user.perspective][target.perspective](user, target), results) | ||||
| } | } | ||||
| } | } | ||||
| @@ -10,7 +10,7 @@ export class Player extends Creature { | |||||
| const stomach = new Stomach(this, 100, new Damage({ amount: 10, type: DamageType.Acid }, { amount: 10, type: DamageType.Crush })) | const stomach = new Stomach(this, 100, new Damage({ amount: 10, type: DamageType.Acid }, { amount: 10, type: DamageType.Crush })) | ||||
| this.containers.add(stomach) | |||||
| this.containers.push(stomach) | |||||
| this.actions.push(new DevourAction(stomach)) | this.actions.push(new DevourAction(stomach)) | ||||
| this.perspective = POV.First | this.perspective = POV.First | ||||
| @@ -20,7 +20,7 @@ export class Wolf extends Creature { | |||||
| const stomach = new Stomach(this, 50, new Damage({ amount: 50, type: DamageType.Acid }, { amount: 500, type: DamageType.Crush })) | const stomach = new Stomach(this, 50, new Damage({ amount: 50, type: DamageType.Acid }, { amount: 500, type: DamageType.Crush })) | ||||
| this.containers.add(stomach) | |||||
| this.containers.push(stomach) | |||||
| this.actions.push(new DevourAction(stomach)) | this.actions.push(new DevourAction(stomach)) | ||||
| this.actions.push(new StruggleAction()) | this.actions.push(new StruggleAction()) | ||||
| this.actions.push(new DigestAction()) | this.actions.push(new DigestAction()) | ||||
| @@ -13,6 +13,7 @@ export interface Entity { | |||||
| export interface Mortal extends Entity { | export interface Mortal extends Entity { | ||||
| health: number; | health: number; | ||||
| maxHealth: number; | |||||
| resistances: Map<DamageType, number>; | resistances: Map<DamageType, number>; | ||||
| takeDamage: (damage: Damage) => void; | takeDamage: (damage: Damage) => void; | ||||
| stats: Stats; | stats: Stats; | ||||
| @@ -20,15 +21,22 @@ export interface Mortal extends Entity { | |||||
| export class Creature implements Mortal, Pred, Prey, Combatant { | export class Creature implements Mortal, Pred, Prey, Combatant { | ||||
| health = 100 | health = 100 | ||||
| maxHealth = 100 | |||||
| resistances: Map<DamageType, number> = new Map() | resistances: Map<DamageType, number> = new Map() | ||||
| perspective: POV = POV.Third | perspective: POV = POV.Third | ||||
| state: State = State.Normal | state: State = State.Normal | ||||
| containers: Set<Container> = new Set(); | |||||
| containers: Array<Container> = [] | |||||
| actions: Array<Action> = []; | actions: Array<Action> = []; | ||||
| containedIn: Container|null = null; | |||||
| private baseBulk: number; | |||||
| get bulk (): number { | |||||
| return this.baseBulk | |||||
| } | |||||
| constructor (public name: Noun, public pronouns: Pronoun, public stats: Stats, public preyPrefs: Set<VoreType>, public predPrefs: Set<VoreType>, public bulk: number) { | |||||
| containedIn: Container|null = null; | |||||
| constructor (public name: Noun, public pronouns: Pronoun, public stats: Stats, public preyPrefs: Set<VoreType>, public predPrefs: Set<VoreType>, bulk: number) { | |||||
| this.baseBulk = bulk | |||||
| } | } | ||||
| toString (): string { | toString (): string { | ||||
| @@ -37,8 +45,9 @@ export class Creature implements Mortal, Pred, Prey, Combatant { | |||||
| takeDamage (damage: Damage): void { | takeDamage (damage: Damage): void { | ||||
| damage.damages.forEach(instance => { | damage.damages.forEach(instance => { | ||||
| if (this.resistances.has(instance.type)) { | |||||
| this.health -= instance.amount * this.resistances.get(instance.type)! | |||||
| const resistance: number|undefined = this.resistances.get(instance.type) | |||||
| if (resistance !== undefined) { | |||||
| this.health -= instance.amount * resistance | |||||
| } else { | } else { | ||||
| this.health -= instance.amount | this.health -= instance.amount | ||||
| } | } | ||||
| @@ -46,7 +55,6 @@ export class Creature implements Mortal, Pred, Prey, Combatant { | |||||
| } | } | ||||
| validActions (target: Creature): Array<Action> { | validActions (target: Creature): Array<Action> { | ||||
| console.log(this) | |||||
| return this.actions.filter(action => { | return this.actions.filter(action => { | ||||
| return action.allowed(this, target) | return action.allowed(this, target) | ||||
| }) | }) | ||||
| @@ -34,5 +34,10 @@ export class CompositeLog implements LogEntry { | |||||
| } | } | ||||
| export function log (entry: LogEntry): void { | export function log (entry: LogEntry): void { | ||||
| entry.render().forEach(elem => document.querySelector('#log')?.appendChild(elem)) | |||||
| entry.render().forEach(elem => { | |||||
| document.querySelector('#log')!.insertBefore(elem, document?.querySelector('#log > *')) | |||||
| }) | |||||
| const entries: Array<HTMLElement> = Array.from(document.querySelectorAll('#log > *')) | |||||
| entries.slice(5).forEach(element => element.remove()) | |||||
| } | } | ||||
| @@ -1,6 +1,7 @@ | |||||
| import { Entity, Mortal, POV } from './entity' | import { Entity, Mortal, POV } from './entity' | ||||
| import { Damage } from './combat' | import { Damage } from './combat' | ||||
| import { LogLines, LogEntry, CompositeLog } from './interface' | import { LogLines, LogEntry, CompositeLog } from './interface' | ||||
| import { ProperNoun } from './language' | |||||
| export enum VoreType {Oral} | export enum VoreType {Oral} | ||||
| export interface Prey extends Mortal { | export interface Prey extends Mortal { | ||||
| @@ -11,13 +12,13 @@ export interface Prey extends Mortal { | |||||
| export interface Pred extends Entity { | export interface Pred extends Entity { | ||||
| predPrefs: Set<VoreType>; | predPrefs: Set<VoreType>; | ||||
| containers: Set<Container>; | |||||
| containers: Array<Container>; | |||||
| } | } | ||||
| export interface Container { | export interface Container { | ||||
| name: string; | name: string; | ||||
| voreTypes: Set<VoreType>; | voreTypes: Set<VoreType>; | ||||
| contents: Set<Prey>; | |||||
| contents: Array<Prey>; | |||||
| capacity: number; | capacity: number; | ||||
| fullness: number; | fullness: number; | ||||
| canTake: (prey: Prey) => boolean; | canTake: (prey: Prey) => boolean; | ||||
| @@ -32,7 +33,7 @@ export interface Container { | |||||
| } | } | ||||
| abstract class NormalContainer implements Container { | abstract class NormalContainer implements Container { | ||||
| contents: Set<Prey> | |||||
| contents: Array<Prey> | |||||
| get fullness (): number { | get fullness (): number { | ||||
| return Array.from(this.contents.values()).reduce((total: number, prey: Prey) => total + prey.bulk, 0) | return Array.from(this.contents.values()).reduce((total: number, prey: Prey) => total + prey.bulk, 0) | ||||
| @@ -49,14 +50,14 @@ abstract class NormalContainer implements Container { | |||||
| } | } | ||||
| consume (prey: Prey): LogEntry { | consume (prey: Prey): LogEntry { | ||||
| this.contents.add(prey) | |||||
| this.contents.push(prey) | |||||
| prey.containedIn = this | prey.containedIn = this | ||||
| return new LogLines('MUNCH') | return new LogLines('MUNCH') | ||||
| } | } | ||||
| release (prey: Prey): LogEntry { | release (prey: Prey): LogEntry { | ||||
| prey.containedIn = null | prey.containedIn = null | ||||
| this.contents.delete(prey) | |||||
| this.contents = this.contents.filter(victim => victim !== prey) | |||||
| return new LogLines('ANTI-MUNCH') | return new LogLines('ANTI-MUNCH') | ||||
| } | } | ||||
| @@ -82,9 +83,9 @@ abstract class NormalContainer implements Container { | |||||
| const digestedEntries = new CompositeLog(...digested.map(prey => this.digest(prey))) | const digestedEntries = new CompositeLog(...digested.map(prey => this.digest(prey))) | ||||
| const absorbedEntries = new CompositeLog(...digested.map(prey => this.absorb(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 | |||||
| })) | |||||
| this.contents = this.contents.filter(prey => { | |||||
| return prey.health > -100 | |||||
| }) | |||||
| return new CompositeLog(digestedEntries, absorbedEntries) | return new CompositeLog(digestedEntries, absorbedEntries) | ||||
| } | } | ||||
| @@ -112,7 +113,7 @@ abstract class NormalContainer implements Container { | |||||
| } | } | ||||
| constructor (public name: string, protected owner: Pred, public voreTypes: Set<VoreType>, public capacity: number, private damage: Damage) { | constructor (public name: string, protected owner: Pred, public voreTypes: Set<VoreType>, public capacity: number, private damage: Damage) { | ||||
| this.contents = new Set() | |||||
| this.contents = [] | |||||
| } | } | ||||
| } | } | ||||