浏览代码

Add group actions. Add more actions for Withers

Also cleans up some old duplicate code that I made, somehow.
master
Fen Dweller 5 年前
父节点
当前提交
26f5d298a3
共有 14 个文件被更改,包括 136 次插入421 次删除
  1. +0
    -39
      src/assets/components/ActionButton.vue
  2. +0
    -237
      src/assets/components/Combat.vue
  3. +0
    -42
      src/assets/components/ContainerView.vue
  4. +0
    -29
      src/assets/components/Header.vue
  5. +0
    -54
      src/assets/components/Statblock.vue
  6. +16
    -3
      src/components/ActionButton.vue
  7. +16
    -10
      src/components/Combat.vue
  8. +1
    -0
      src/components/Statblock.vue
  9. +14
    -1
      src/game/combat.ts
  10. +6
    -0
      src/game/combat/conditions.ts
  11. +68
    -4
      src/game/creatures/withers.ts
  12. +10
    -1
      src/game/entity.ts
  13. +3
    -1
      src/game/interface.ts
  14. +2
    -0
      src/game/vore.ts

+ 0
- 39
src/assets/components/ActionButton.vue 查看文件

@@ -1,39 +0,0 @@
<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>

+ 0
- 237
src/assets/components/Combat.vue 查看文件

@@ -1,237 +0,0 @@
<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>

+ 0
- 42
src/assets/components/ContainerView.vue 查看文件

@@ -1,42 +0,0 @@
<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>

+ 0
- 29
src/assets/components/Header.vue 查看文件

@@ -1,29 +0,0 @@
<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>

+ 0
- 54
src/assets/components/Statblock.vue 查看文件

@@ -1,54 +0,0 @@
<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>

+ 16
- 3
src/components/ActionButton.vue 查看文件

@@ -8,7 +8,7 @@
<script lang="ts">

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

@Component({})
@@ -22,14 +22,27 @@ export default class ActionButton extends Vue {
@Prop()
target!: Creature

@Prop()
combatants!: Array<Creature>

@Emit("execute")
execute () {
this.$emit('executed', this.action.execute(this.user, this.target))
if ((this.action as GroupAction).executeGroup !== undefined) {
const action = (this.action as GroupAction)
this.$emit('executed', (this.action as GroupAction).executeGroup(this.user, action.allowedGroup(this.user, this.combatants)))
} else {
this.$emit('executed', this.action.execute(this.user, this.target))
}
}

@Emit("describe")
describe () {
this.$emit('described', this.action.describe(this.user, this.target))
if ((this.action as GroupAction).describeGroup !== undefined) {
const action = (this.action as GroupAction)
this.$emit('described', action.describeGroup(this.user, action.allowedGroup(this.user, this.combatants)))
} else {
this.$emit('described', this.action.describe(this.user, this.target))
}
}
}



+ 16
- 10
src/components/Combat.vue 查看文件

@@ -1,28 +1,30 @@
<template>
<div class="combat-layout">
<div id="left-stats">
<Statblock v-on:click.native="left = combatant" class="left-stats" :data-active="combatant === left" v-for="combatant in combatants.filter(c => c.side == Side.Heroes && c.vigors.Health > 0)" v-bind:key="combatant.name" :subject="combatant" />
<div class="stat-column" id="left-stats">
<Statblock v-on:click.native="left = combatant" class="left-stats" :data-active="combatant === left" v-for="combatant in combatants.filter(c => c.side == Side.Heroes && !c.digested)" v-bind:key="combatant.name" :subject="combatant" />
</div>
<div id="right-stats">
<Statblock v-on:click.native="right = combatant" class="right-stats" :data-active="combatant === right" v-for="combatant in combatants.filter(c => c.side == Side.Monsters && c.vigors.Health > 0)" v-bind:key="combatant.name" :subject="combatant" />
<div class="stat-column" id="right-stats">
<Statblock v-on:click.native="right = combatant" class="right-stats" :data-active="combatant === right" v-for="combatant in combatants.filter(c => c.side == Side.Monsters && !c.digested)" v-bind:key="combatant.name" :subject="combatant" />
</div>
<div id="log">
</div>
<div class="left-actions">
<div class="vert-display">
<h2>Moves</h2>
<ActionButton @described="described" @executed="executedLeft" v-for="action in left.validActions(right)" :key="'left' + action.name" :action="action" :user="left" :target="right" />
<ActionButton @described="described" @executed="executedLeft" v-for="action in left.validActions(right)" :key="'left' + action.name" :action="action" :user="left" :target="right" :combatants="combatants" />
<h2>Self-moves</h2>
<ActionButton @described="described" @executed="executedLeft" v-for="action in left.validActions(left)" :key="'left' + action.name" :action="action" :user="left" :target="right" />
<ActionButton @described="described" @executed="executedLeft" v-for="action in left.validActions(left)" :key="'left' + action.name" :action="action" :user="left" :target="right" :combatants="combatants" />
</div>
<div>{{actionDescription}}</div>
</div>
<div class="right-actions">
<div class="vert-display">
<h2>Group-moves</h2>
<ActionButton @described="described" @executed="executedRight" v-for="action in right.validGroupActions(combatants)" :key="'right' + action.name" :action="action" :user="right" :target="left" :combatants="combatants" />
<h2>Moves</h2>
<ActionButton @described="described" @executed="executedRight" v-for="action in right.validActions(left)" :key="'right' + action.name" :action="action" :user="right" :target="left" />
<ActionButton @described="described" @executed="executedRight" v-for="action in right.validActions(left)" :key="'right' + action.name" :action="action" :user="right" :target="left" :combatants="combatants" />
<h2>Self-moves</h2>
<ActionButton @described="described" @executed="executedRight" v-for="action in right.validActions(right)" :key="'right' + action.name" :action="action" :user="right" :target="left" />
<ActionButton @described="described" @executed="executedRight" v-for="action in right.validActions(right)" :key="'right' + action.name" :action="action" :user="right" :target="left" :combatants="combatants" />
</div>
</div>
<div id="action-desc">
@@ -143,12 +145,16 @@ export default class Combat extends Vue {
grid-area: 2 / 3 / 4 / 4;
}

.stat-column {
overflow-y: auto;
}

.left-actions {
grid-area: 5 / 1 / 6 / 2;
grid-area: 4 / 1 / 6 / 2;
}

.right-actions {
grid-area: 5 / 3 / 6 / 4;
grid-area: 4 / 3 / 6 / 4;
}

#action-desc {


+ 1
- 0
src/components/Statblock.vue 查看文件

@@ -33,6 +33,7 @@
</div>
</div>
<div>Status: {{subject.status}}</div>
<button @click="subject.perspective = firstperson">First-person</button>
</div>
</template>



+ 14
- 1
src/game/combat.ts 查看文件

@@ -207,6 +207,7 @@ export enum Side {
*/
export interface Combatant {
actions: Array<Action>;
groupActions: Array<GroupAction>;
side: Side;
}

@@ -218,7 +219,7 @@ export abstract class Action {
return this.conditions.every(cond => cond.allowed(user, target))
}

abstract execute(user: Creature, target: Creature): LogEntry
abstract execute (user: Creature, target: Creature): LogEntry

abstract describe (user: Creature, target: Creature): LogEntry

@@ -271,3 +272,15 @@ export abstract class TogetherAction extends PairAction {
}
}
}

export abstract class GroupAction extends PairAction {
allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> {
return targets.filter(target => this.allowed(user, target))
}

executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
return new LogLines(...targets.map(target => this.execute(user, target)))
}

abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry
}

+ 6
- 0
src/game/combat/conditions.ts 查看文件

@@ -25,3 +25,9 @@ export class DrainedVigorCondition implements Condition {

}
}

export class TogetherCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.containedIn === target.containedIn
}
}

+ 68
- 4
src/game/creatures/withers.ts 查看文件

@@ -1,10 +1,12 @@
import { Creature, POV, Entity } from '../entity'
import { Stat, Damage, DamageType, ConstantDamageFormula, Vigor, Side, PairAction, CombatTest } from '../combat'
import { MalePronouns, ImproperNoun, POVPair, POVPairArgs, ProperNoun, TheyPronouns } from '../language'
import { Stat, Damage, DamageType, ConstantDamageFormula, Vigor, Side, PairAction, CombatTest, GroupAction } from '../combat'
import { MalePronouns, ImproperNoun, POVPair, POVPairArgs, ProperNoun, TheyPronouns, FemalePronouns } from '../language'
import { LogLine, LogLines, LogEntry } from '../interface'
import { VoreType, Stomach, Bowels } from '../vore'
import { VoreType, Stomach, Bowels, Container } from '../vore'
import { StatTest } from '../combat/tests'
import { AttackAction, TransferAction, FeedAction } from '../combat/actions'
import { TogetherCondition } from '../combat/conditions'
import { InstantKill } from '../combat/effects'

class BiteAction extends AttackAction {
constructor () {
@@ -13,17 +15,77 @@ class BiteAction extends AttackAction {
}
}

class StompAction extends GroupAction {
lines: POVPair<Creature, Creature> = new POVPair([
[[POV.First, POV.Third], (user: Creature, target: Creature) => new LogLine(`You flatten ${target.name} under your foot!`)],
[[POV.Third, POV.First], (user: Creature, target: Creature) => new LogLine(`${user.name.capital} flattens you under ${user.pronouns.possessive} foot!`)],
[[POV.Third, POV.Third], (user: Creature, target: Creature) => new LogLine(`${user.name.capital} flattens ${target.name} under ${user.pronouns.possessive} foot!`)]
])

execute (user: Creature, target: Creature): LogEntry {
return new LogLines(this.lines.run(user, target), new InstantKill().apply(target))
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine('Stomp one sucker')
}

describeGroup (user: Creature, targets: Array<Creature>): LogEntry {
return new LogLine('Stomp all ', targets.length.toString(), ' of \'em!')
}

constructor () {
super('Stomp', 'STOMP!', [
new TogetherCondition()
])
}
}

class DevourAllAction extends GroupAction {
lines: POVPair<Creature, Creature> = new POVPair([
[[POV.First, POV.Third], (user: Creature, target: Creature) => new LogLine(`You scoop up ${target.name}!`)],
[[POV.Third, POV.First], (user: Creature, target: Creature) => new LogLine(`${user.name.capital} scoops you up!`)],
[[POV.Third, POV.Third], (user: Creature, target: Creature) => new LogLine(`${user.name.capital} scoops ${target.name} up!`)]
])

execute (user: Creature, target: Creature): LogEntry {
this.container.consume(target)
return new LogLines(this.lines.run(user, target))
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine('Stomp one sucker')
}

executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
return new LogLines(...targets.map(target => this.execute(user, target)).concat(
[new LogLine('GULP!')]
))
}

describeGroup (user: Creature, targets: Array<Creature>): LogEntry {
return new LogLine('Eat all ', targets.length.toString(), ' of \'em!')
}

constructor (private container: Container) {
super('Devour All', 'GULP!', [
new TogetherCondition()
])
}
}

export class Withers extends Creature {
constructor () {
super(
new ProperNoun('Withers'),
TheyPronouns,
FemalePronouns,
{ Toughness: 60, Power: 100, Speed: 40, Willpower: 60, Charm: 120 },
new Set(),
new Set([VoreType.Oral]),
5000)

this.actions.push(new BiteAction())
this.groupActions.push(new StompAction())

this.side = Side.Monsters

@@ -35,5 +97,7 @@ export class Withers extends Creature {

this.containers.push(stomach)
this.otherActions.push(new FeedAction(stomach))

this.groupActions.push(new DevourAllAction(stomach))
}
}

+ 10
- 1
src/game/entity.ts 查看文件

@@ -1,4 +1,4 @@
import { DamageType, Damage, Combatant, Stats, Action, Vigor, VoreStats, VoreStat, Stat, Side } from './combat'
import { DamageType, Damage, Combatant, Stats, Action, Vigor, VoreStats, VoreStat, Stat, Side, GroupAction } from './combat'
import { Noun, Pronoun } from './language'
import { LogEntry, LogLine } from './interface'
import { Vore, Container, VoreType } from './vore'
@@ -48,6 +48,7 @@ export class Creature extends Vore implements Combatant {
perspective: POV = POV.Third
containers: Array<Container> = []
actions: Array<Action> = [];
groupActions: Array<GroupAction> = [];
otherActions: Array<Action> = [];

get bulk (): number {
@@ -155,6 +156,14 @@ export class Creature extends Vore implements Combatant {
})
}

validGroupActions (targets: Array<Creature>): Array<GroupAction> {
const choices = this.groupActions

return choices.filter(action => {
return targets.some(target => action.allowed(this, target))
})
}

destroy (): LogEntry {
return super.destroy()
}


+ 3
- 1
src/game/interface.ts 查看文件

@@ -30,7 +30,9 @@ export class LogLines implements LogEntry {
div.appendChild(partDiv)
} else {
(part as LogEntry).render().forEach(logPart => {
div.appendChild(logPart)
const partDiv = document.createElement("div")
partDiv.appendChild(logPart)
div.appendChild(partDiv)
})
}
})


+ 2
- 0
src/game/vore.ts 查看文件

@@ -23,6 +23,7 @@ export abstract class Vore implements Mortal {
abstract stats: Stats;
abstract baseStats: Stats;
abstract status: string;
digested = false;
abstract preyPrefs: Set<VoreType>;
abstract voreStats: VoreStats;
abstract containedIn: Container | null;
@@ -122,6 +123,7 @@ abstract class NormalContainer implements Container {
damageResults.push(prey.takeDamage(scaled))

if (prey.vigors[Vigor.Health] <= 0) {
prey.digested = true
this.digested.push(prey)
justDigested.push(prey)
}


正在加载...
取消
保存