Ver código fonte

Improve layout; add vigors

master
Fen Dweller 5 anos atrás
pai
commit
c869023334
13 arquivos alterados com 478 adições e 32 exclusões
  1. +1
    -0
      public/index.html
  2. +3
    -1
      src/App.vue
  3. +39
    -0
      src/assets/components/ActionButton.vue
  4. +237
    -0
      src/assets/components/Combat.vue
  5. +42
    -0
      src/assets/components/ContainerView.vue
  6. +29
    -0
      src/assets/components/Header.vue
  7. +54
    -0
      src/assets/components/Statblock.vue
  8. +14
    -3
      src/components/Statblock.vue
  9. +17
    -3
      src/game/combat.ts
  10. +4
    -4
      src/game/creatures/player.ts
  11. +4
    -4
      src/game/creatures/wolf.ts
  12. +30
    -13
      src/game/entity.ts
  13. +4
    -4
      src/game/vore.ts

+ 1
- 0
public/index.html Ver arquivo

@@ -7,6 +7,7 @@
<link rel="icon" href="https://crux.sexy/images/feast.ico">
<link rel="stylesheet" href="./reset.css"></link>
<title><%= htmlWebpackPlugin.options.title %></title>
<script src="https://kit.fontawesome.com/10a16c6083.js" crossorigin="anonymous"></script>
</head>
<body>
<noscript>


+ 3
- 1
src/App.vue Ver arquivo

@@ -32,7 +32,7 @@ export default class App extends Vue {

<style>
body, html {
background: #111;
background: #181818;
width: 100%;
height: 100%;
}
@@ -45,6 +45,8 @@ body, html {
color: #ddd;
background: #111;
width: 100%;
max-width: 1000pt;
margin: auto;
height: 100%;
}
</style>

+ 39
- 0
src/assets/components/ActionButton.vue Ver arquivo

@@ -0,0 +1,39 @@
<template>
<button class="action-button" @click="execute">
{{ action.name }}
</button>
</template>

<script lang="ts">

import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
import { Action } from '@/game/combat'
import { Creature } from '@/game/entity'

@Component({})
export default class ActionButton extends Vue {
@Prop()
action!: Action

@Prop()
user!: Creature

@Prop()
target!: Creature

@Emit("execute")
execute () {
this.$emit('executed', this.action.execute(this.user, this.target))
}
}

</script>

<style scoped>

.action-button {
width: 100px;
height: 100px;
}

</style>

+ 237
- 0
src/assets/components/Combat.vue Ver arquivo

@@ -0,0 +1,237 @@
<template>
<div class="combat-layout">
<Statblock class="player-stats" :subject="player" />
<Statblock class="enemy-stats" :subject="enemy" />
<div id="log">
</div>
<div class="player-actions">
<h2>Your moves</h2>
<div class="vert-display">
<ActionButton @executed="executed" v-for="action in player.validActions(enemy)" :key="'player' + action.name" :action="action" :user="player" :target="enemy" />
<ActionButton @executed="executed" v-for="action in player.validActions(player)" :key="'player' + action.name" :action="action" :user="player" :target="enemy" />
</div>
<div>{{actionDescription}}</div>
</div>
<div class="enemy-actions">
<h2>Enemy moves</h2>
<div class="vert-display">
<ActionButton @executed="executedEnemy" v-for="action in enemy.validActions(player)" :key="'player' + action.name" :action="action" :user="enemy" :target="player" />
<ActionButton @executed="executedEnemy" v-for="action in enemy.validActions(enemy)" :key="'player' + action.name" :action="action" :user="enemy" :target="player" />
</div>
</div>
</div>
</template>

<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
import { Creature, POV } from '@/game/entity'
import { LogEntry } from '@/game/interface'
import Statblock from './Statblock.vue'
import ActionButton from './ActionButton.vue'

@Component(
{
components: { Statblock, ActionButton }
}
)
export default class Combat extends Vue {
@Prop({ type: Creature, required: true })
player!: Creature

@Prop({ type: Creature, required: true })
enemy!: Creature

actionDescription = ''

constructor () {
super()
}

@Emit("executed")
executed (entry: LogEntry) {
const log = document.querySelector("#log")

if (log !== null) {
const holder = document.createElement("div")
entry.render().forEach(element => {
holder.appendChild(element)
})
holder.classList.add("player-move")
log.appendChild(holder)
log.scrollTo({ top: 10000000000, left: 0 })
}
}

@Emit("executedEnemy")
executedEnemy (entry: LogEntry) {
const log = document.querySelector("#log")

if (log !== null) {
const holder = document.createElement("div")
entry.render().forEach(element => {
holder.appendChild(element)
})
holder.classList.add("enemy-move")
log.appendChild(holder)
log.scrollTo({ top: 10000000000, left: 0 })
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.combat-layout {
display: grid;
grid-template-rows: 20% [main-row-start] 30% 30% [main-row-end] 20%;
grid-template-columns: 20% [main-col-start] 60% [main-col-end] 20%;
width: 100%;
height: 100%;
}

.player-stats {
grid-area: 1 / 1 / 2 / 2
}

.enemy-stats {
grid-area: 1 / 3 / 2 / 4;
}

#log {
grid-area: main-row-start / main-col-start / main-row-end / main-col-end;
overflow-y: auto;
font-size: 16pt;
width: 100%;
max-height: 100%;
align-self: end;
}

.player-actions {
grid-area: 3 / 1 / 4 / 2;
}

.enemy-actions {
grid-area: 3 / 3 / 4 / 4;
}

h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
.horiz-display {
display: flex;
justify-content: center;
}
.vert-display {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

<style>

.log-damage {
font-weight: bold;
}

#log > div {
color: #888;
padding-top: 8pt;
padding-bottom: 8pt;
}

div.player-move,
div.enemy-move {
color: #888;
}

div.player-move {
text-align: start;
margin-left: 48pt;
}

div.enemy-move {
text-align: end;
margin-right: 48pt;
}

#log > div.enemy-move:nth-last-child(7) {
color: #988;
}

#log > div.enemy-move:nth-last-child(6) {
padding-top: 8pt;
color: #a88;
}

#log > div.enemy-move:nth-last-child(5) {
padding-top: 16pt;
color: #b88;
}

#log > div.enemy-move:nth-last-child(4) {
padding-top: 24pt;
color: #c88;
}

#log > div.enemy-move:nth-last-child(3) {
padding-top: 32pt;
color: #d88;
}

#log > div.enemy-move:nth-last-child(2) {
padding-top: 40pt;
color: #e88;
}

#log > div.enemy-move:nth-last-child(1) {
padding-top: 48pt;
color: #f88;
}

#log > div.player-move:nth-last-child(7) {
color: #898;
}

#log > div.player-move:nth-last-child(6) {
padding-top: 8pt;
color: #8a8;
}

#log > div.player-move:nth-last-child(5) {
padding-top: 16pt;
color: #8b8;
}

#log > div.player-move:nth-last-child(4) {
padding-top: 24pt;
color: #8c8;
}

#log > div.player-move:nth-last-child(3) {
padding-top: 32pt;
color: #8d8;
}

#log > div.player-move:nth-last-child(2) {
padding-top: 40pt;
color: #8e8;
}

#log > div.player-move:nth-last-child(1) {
padding-top: 48pt;
color: #8f8;
}

</style>

+ 42
- 0
src/assets/components/ContainerView.vue Ver arquivo

@@ -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>

+ 29
- 0
src/assets/components/Header.vue Ver arquivo

@@ -0,0 +1,29 @@
<template>
<div id="header">
<div>
This is the (extremely early alpha of the) new Feast. If you're looking for the old version, <a href="https://classic.feast.crux.sexy">go here!</a>
</div>
<div>Version: {{version}}</div>
</div>
</template>

<script lang="ts">

import { Component, Vue, Prop } from 'vue-property-decorator'

@Component({})
export default class Header extends Vue {
@Prop() version!: string
}

</script>

<style scoped>
#header {
width: 100%;
background: #222;
top: 0%;
padding-top: 32pt;
padding-bottom: 32pt;
}
</style>

+ 54
- 0
src/assets/components/Statblock.vue Ver arquivo

@@ -0,0 +1,54 @@
<template>
<div class="statblock">
<h2 v-if="subject.perspective === firstperson">You</h2>
<h2 v-if="subject.perspective !== firstperson">{{subject.name.all.capital}}</h2>
<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.status}}</div>
<ContainerView v-for="container in subject.containers" :key="container.name" :container="container" />
</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 ContainerView from './ContainerView.vue'

@Component({
components: {
ContainerView
}
})
export default class Statblock extends Vue {
@Prop({ type: Creature, required: true })
subject!: Creature

firstperson: POV = POV.First
constructor () {
super()
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h2 {
margin-bottom: 16pt;
font-size: 200%;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
.statblock {
margin: 16px;
}
</style>

+ 14
- 3
src/components/Statblock.vue Ver arquivo

@@ -2,8 +2,11 @@
<div class="statblock">
<h2 v-if="subject.perspective === firstperson">You</h2>
<h2 v-if="subject.perspective !== firstperson">{{subject.name.all.capital}}</h2>
<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 class="stat-line"><i class="fas fa-heart" /> {{ subject.vigors[vigor.Health].toFixed(0) }}</div>
<div class="stat-line"><i class="fas fa-bolt" /> {{ subject.vigors[vigor.Stamina].toFixed(0) }}</div>
<div class="stat-line"><i class="fas fa-brain" /> {{ subject.vigors[vigor.Willpower].toFixed(0) }}</div>
<br>
<div class="stat-line" v-for="stat in Object.keys(subject.stats)" v-bind:key="stat"><i :class="statIcons[stat]" />: {{subject.stats[stat]}}</div>
<div>Status: {{subject.status}}</div>
<ContainerView v-for="container in subject.containers" :key="container.name" :container="container" />
</div>
@@ -12,7 +15,7 @@
<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 { Stats, Stat, StatIcons, Vigor } from '@/game/combat'
import ContainerView from './ContainerView.vue'

@Component({
@@ -24,6 +27,9 @@ export default class Statblock extends Vue {
@Prop({ type: Creature, required: true })
subject!: Creature

private statIcons = StatIcons
private vigor = Vigor

firstperson: POV = POV.First
constructor () {
super()
@@ -51,4 +57,9 @@ a {
.statblock {
margin: 16px;
}
.stat-line {
font-size: 24pt;
padding-top: 4pt;
padding-bottom: 4pt;
}
</style>

+ 17
- 3
src/game/combat.ts Ver arquivo

@@ -75,8 +75,9 @@ export enum DamageType {
Acid = "Acid"}

export interface DamageInstance {
type: DamageType;
amount: number;
type: DamageType;
amount: number;
target: Vigor;
}

export class Damage {
@@ -92,7 +93,8 @@ export class Damage {
this.damages.forEach(damage => {
results.push({
type: damage.type,
amount: damage.amount * factor
amount: damage.amount * factor,
target: damage.target
})
})

@@ -104,6 +106,12 @@ export class Damage {
}
}

export enum Vigor {
Health,
Stamina,
Willpower
}

export enum Stat {
STR = 'Strength',
DEX = 'Dexterity',
@@ -112,6 +120,12 @@ export enum Stat {

export type Stats = {[key in Stat]: number}

export const StatIcons: {[key in Stat]: string} = {
[Stat.STR]: 'fas fa-fist-raised',
[Stat.DEX]: 'fas fa-feather',
[Stat.CON]: 'fas fa-heartbeat'
}

export interface Combatant {
actions: Array<Action>;
}


+ 4
- 4
src/game/creatures/player.ts Ver arquivo

@@ -1,18 +1,18 @@
import { Creature, POV } from '../entity'
import { ProperNoun, TheyPronouns } from '../language'
import { Stat, Damage, AttackAction, DamageType } from '../combat'
import { Stat, Damage, AttackAction, DamageType, Vigor } from '../combat'
import { Stomach, Bowels, 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, VoreType.Anal]), 50)

this.actions.push(new AttackAction(new Damage({ type: DamageType.Pierce, amount: 20 })))
this.actions.push(new AttackAction(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health })))

const stomach = new Stomach(this, 100, new Damage({ amount: 100000000000, type: DamageType.Acid }, { amount: 10, type: DamageType.Crush }))
const stomach = new Stomach(this, 100, new Damage({ amount: 100000000000, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health }))

this.containers.push(stomach)
const bowels = new Bowels(this, 100, new Damage({ amount: 20, type: DamageType.Crush }))
const bowels = new Bowels(this, 100, new Damage({ amount: 20, type: DamageType.Crush, target: Vigor.Health }))

this.containers.push(bowels)
this.perspective = POV.First


+ 4
- 4
src/game/creatures/wolf.ts Ver arquivo

@@ -1,11 +1,11 @@
import { Creature, POV } from '../entity'
import { Stat, Damage, DamageType, AttackAction, StruggleAction, TransferAction } from '../combat'
import { Stat, Damage, DamageType, AttackAction, TransferAction, Vigor } from '../combat'
import { MalePronouns, ImproperNoun } from '../language'
import { VoreType, Stomach, Bowels } from '../vore'

class BiteAction extends AttackAction {
constructor () {
super(new Damage({ amount: 10, type: DamageType.Slash }))
super(new Damage({ amount: 10, type: DamageType.Slash, target: Vigor.Health }))
}
}

@@ -14,10 +14,10 @@ export class Wolf extends Creature {
super(new ImproperNoun('wolf', 'wolves'), MalePronouns, { [Stat.STR]: 10, [Stat.DEX]: 10, [Stat.CON]: 10 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral]), 25)
this.actions.push(new BiteAction())

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, target: Vigor.Health }, { amount: 500, type: DamageType.Crush, target: Vigor.Health }))

this.containers.push(stomach)
const bowels = new Bowels(this, 50, new Damage({ amount: 50, type: DamageType.Acid }, { amount: 500, type: DamageType.Crush }))
const bowels = new Bowels(this, 50, new Damage({ amount: 50, type: DamageType.Acid, target: Vigor.Health }, { amount: 500, type: DamageType.Crush, target: Vigor.Health }))

this.containers.push(bowels)



+ 30
- 13
src/game/entity.ts Ver arquivo

@@ -1,4 +1,4 @@
import { DamageType, Damage, Combatant, Stats, Action } from './combat'
import { DamageType, Damage, Combatant, Stats, Action, Vigor } from './combat'
import { Noun, Pronoun } from './language'

import { Pred, Prey, Container, VoreType } from './vore'
@@ -12,17 +12,28 @@ export interface Entity {
}

export interface Mortal extends Entity {
health: number;
maxHealth: number;
resistances: Map<DamageType, number>;
takeDamage: (damage: Damage) => void;
stats: Stats;
status: string;
vigors: {[key in Vigor]: number};
maxVigors: {[key in Vigor]: number};

resistances: Map<DamageType, number>;
takeDamage: (damage: Damage) => void;
stats: Stats;
status: string;
}

export class Creature implements Mortal, Pred, Prey, Combatant {
health = 100
maxHealth = 100
vigors = {
[Vigor.Health]: 100,
[Vigor.Stamina]: 100,
[Vigor.Willpower]: 100
}

maxVigors = {
[Vigor.Health]: 100,
[Vigor.Stamina]: 100,
[Vigor.Willpower]: 100
}

resistances: Map<DamageType, number> = new Map()
perspective: POV = POV.Third
containers: Array<Container> = []
@@ -40,24 +51,30 @@ export class Creature implements Mortal, Pred, Prey, Combatant {
}

toString (): string {
return this.name + ': ' + this.health + ' HP'
return this.name.toString()
}

takeDamage (damage: Damage): void {
damage.damages.forEach(instance => {
const resistance: number|undefined = this.resistances.get(instance.type)
if (resistance !== undefined) {
this.health -= instance.amount * resistance
this.vigors[instance.target] -= instance.amount * resistance
} else {
this.health -= instance.amount
this.vigors[instance.target] -= instance.amount
}
})
}

get status (): string {
if (this.health < 0) {
if (this.vigors[Vigor.Health] < 0) {
return "DEAD"
}
if (this.vigors[Vigor.Stamina] < 0) {
return "Unconscious"
}
if (this.vigors[Vigor.Willpower] < 0) {
return "Too horny"
}
if (this.containedIn !== null) {
return "Devoured"
}


+ 4
- 4
src/game/vore.ts Ver arquivo

@@ -1,5 +1,5 @@
import { Entity, Mortal, POV } from './entity'
import { Damage, Actionable, Action, DevourAction, DigestAction, ReleaseAction, StruggleAction } from './combat'
import { Damage, Actionable, Action, DevourAction, DigestAction, ReleaseAction, StruggleAction, Vigor } from './combat'
import { LogLines, LogEntry, CompositeLog } from './interface'
import { POVSolo, POVPair, POVPairArgs } from './language'

@@ -86,9 +86,9 @@ abstract class NormalContainer implements Container {
const absorbed: Array<Prey> = []

this.contents.forEach(prey => {
const start = prey.health
const start = prey.vigors[Vigor.Health]
prey.takeDamage(this.damage.scale(dt / 3600))
const end = prey.health
const end = prey.vigors[Vigor.Health]

if (start > 0 && end <= 0) {
digested.push(prey)
@@ -103,7 +103,7 @@ abstract class NormalContainer implements Container {
const absorbedEntries = new CompositeLog(...absorbed.map(prey => this.absorb(prey)))

this.contents = this.contents.filter(prey => {
return prey.health > -100
return prey.vigors[Vigor.Health] > -100
})

return new CompositeLog(this.tickLines.run(this.owner), digestedEntries, absorbedEntries)


Carregando…
Cancelar
Salvar