浏览代码

Add an initiative system

master
Fen Dweller 5 年前
父节点
当前提交
25df00a9ec
共有 6 个文件被更改,包括 167 次插入33 次删除
  1. +7
    -3
      src/App.vue
  2. +55
    -9
      src/components/Combat.vue
  3. +14
    -0
      src/components/Statblock.vue
  4. +74
    -20
      src/game/combat.ts
  5. +15
    -1
      src/game/combat/actions.ts
  6. +2
    -0
      src/game/entity.ts

+ 7
- 3
src/App.vue 查看文件

@@ -1,7 +1,7 @@
<template> <template>
<div id="app"> <div id="app">
<Header version="pre-alpha" /> <Header version="pre-alpha" />
<Combat :combatants="combatants" />
<Combat :encounter="encounter" />
</div> </div>
</template> </template>


@@ -13,6 +13,7 @@ import * as Creatures from '@/game/creatures'
import * as Items from '@/game/items' import * as Items from '@/game/items'
import { Creature } from '@/game/entity' import { Creature } from '@/game/entity'
import { ProperNoun, TheyPronouns, FemalePronouns, MalePronouns, ImproperNoun } from '@/game/language' import { ProperNoun, TheyPronouns, FemalePronouns, MalePronouns, ImproperNoun } from '@/game/language'
import { Encounter } from './game/combat'


@Component({ @Component({
components: { components: {
@@ -20,7 +21,8 @@ import { ProperNoun, TheyPronouns, FemalePronouns, MalePronouns, ImproperNoun }
} }
}) })
export default class App extends Vue { export default class App extends Vue {
combatants: Array<Creature>
encounter: Encounter

constructor () { constructor () {
super() super()


@@ -73,7 +75,9 @@ export default class App extends Vue {
const kenzie = new Creatures.Kenzie() const kenzie = new Creatures.Kenzie()
const cafat = new Creatures.Cafat() const cafat = new Creatures.Cafat()
const wolf = new Creatures.Wolf() const wolf = new Creatures.Wolf()
this.combatants = [fighter, withers, wizard, rogue, cleric, kenzie, cafat, wolf]
const combatants = [fighter, withers, wizard, rogue, cleric, kenzie, cafat, wolf]
this.encounter = new Encounter(combatants)
console.log(this.encounter)
} }
} }
</script> </script>


+ 55
- 9
src/components/Combat.vue 查看文件

@@ -1,10 +1,11 @@
<template> <template>
<div class="combat-layout"> <div class="combat-layout">
<div>{{ encounter.currentMove.name }}</div>
<div @wheel="horizWheelLeft" class="stat-column" id="left-stats"> <div @wheel="horizWheelLeft" class="stat-column" id="left-stats">
<Statblock @selectPredator="right = combatant.containedIn.owner" @selectAlly="right = combatant" @select="left = combatant" class="left-stats" :data-active="combatant === left" :data-active-ally="combatant === right" :data-eaten="combatant.containedIn !== null" :data-dead="combatant.vigors.Health <= 0" v-for="(combatant, index) in combatants.filter(c => c.side == Side.Heroes && !c.destroyed).slice().reverse()" v-bind:key="'left-stat-' + index" :subject="combatant" />
<Statblock @selectPredator="right = combatant.containedIn.owner" @selectAlly="right = combatant" @select="doSelectLeft(combatant)" class="left-stats" :data-disabled="encounter.currentMove.side === combatant.side && encounter.currentMove !== combatant" :data-current-turn="encounter.currentMove === combatant" :data-active="combatant === left" :data-active-ally="combatant === right" :data-eaten="combatant.containedIn !== null" :data-dead="combatant.vigors.Health <= 0" v-for="(combatant, index) in combatants.filter(c => c.side == Side.Heroes && !c.destroyed).slice().reverse()" v-bind:key="'left-stat-' + index" :subject="combatant" :initiative="encounter.initiatives.get(combatant)" />
</div> </div>
<div @wheel="horizWheelRight" class="stat-column" id="right-stats"> <div @wheel="horizWheelRight" class="stat-column" id="right-stats">
<Statblock @selectPredator="left = combatant.containedIn.owner" @selectAlly="left = combatant" @select="right = combatant" class="right-stats" :data-active="combatant === right" :data-active-ally="combatant === left" :data-eaten="combatant.containedIn !== null" :data-dead="combatant.vigors.Health <= 0" v-for="(combatant, index) in combatants.filter(c => c.side == Side.Monsters && !c.destroyed)" v-bind:key="'right-stat-' + index" :subject="combatant" />
<Statblock @selectPredator="left = combatant.containedIn.owner" @selectAlly="left = combatant" @select="doSelectRight(combatant)" class="right-stats" :data-disabled="encounter.currentMove.side === combatant.side && encounter.currentMove !== combatant" :data-current-turn="encounter.currentMove === combatant" :data-active="combatant === right" :data-active-ally="combatant === left" :data-eaten="combatant.containedIn !== null" :data-dead="combatant.vigors.Health <= 0" v-for="(combatant, index) in combatants.filter(c => c.side == Side.Monsters && !c.destroyed)" v-bind:key="'right-stat-' + index" :subject="combatant" :initiative="encounter.initiatives.get(combatant)" />
</div> </div>
<div id="log"> <div id="log">
</div> </div>
@@ -12,7 +13,7 @@


</div> </div>
<div class="left-actions"> <div class="left-actions">
<div class="vert-display">
<div v-if="encounter.currentMove === left" class="vert-display">
<i class="action-label fas fa-users" v-if="left.validGroupActions(combatants).length > 0"></i> <i class="action-label fas fa-users" v-if="left.validGroupActions(combatants).length > 0"></i>
<ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left.validGroupActions(combatants)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="right" :combatants="combatants" /> <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left.validGroupActions(combatants)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="right" :combatants="combatants" />
<i class="action-label fas fa-user-friends" v-if="left.validActions(right).length > 0"></i> <i class="action-label fas fa-user-friends" v-if="left.validActions(right).length > 0"></i>
@@ -26,7 +27,7 @@


</div> </div>
<div class="right-actions"> <div class="right-actions">
<div class="vert-display">
<div v-if="encounter.currentMove === right" class="vert-display">
<i class="action-label fas fa-users" v-if="right.validGroupActions(combatants).length > 0"></i> <i class="action-label fas fa-users" v-if="right.validGroupActions(combatants).length > 0"></i>
<ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right.validGroupActions(combatants)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="left" :combatants="combatants" /> <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right.validGroupActions(combatants)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="left" :combatants="combatants" />
<i class="action-label fas fa-user-friends" v-if="right.validActions(left).length > 0"></i> <i class="action-label fas fa-user-friends" v-if="right.validActions(left).length > 0"></i>
@@ -47,7 +48,7 @@ import { POV } from '@/game/language'
import { LogEntry } from '@/game/interface' import { LogEntry } from '@/game/interface'
import Statblock from './Statblock.vue' import Statblock from './Statblock.vue'
import ActionButton from './ActionButton.vue' import ActionButton from './ActionButton.vue'
import { Side } from '@/game/combat'
import { Side, Encounter } from '@/game/combat'


@Component( @Component(
{ {
@@ -55,7 +56,8 @@ import { Side } from '@/game/combat'
data () { data () {
return { return {
left: null, left: null,
right: null
right: null,
combatants: null
} }
}, },
methods: { methods: {
@@ -72,6 +74,16 @@ import { Side } from '@/game/combat'
if (target !== null) { if (target !== null) {
target.scrollBy({ top: 0, left: event.deltaY, behavior: 'smooth' }) target.scrollBy({ top: 0, left: event.deltaY, behavior: 'smooth' })
} }
},
doSelectLeft (combatant: Creature) {
if (combatant.side !== this.$props.encounter.currentMove.side) {
this.$data.left = combatant
}
},
doSelectRight (combatant: Creature) {
if (combatant.side !== this.$props.encounter.currentMove.side) {
this.$data.right = combatant
}
} }
} }
} }
@@ -79,15 +91,16 @@ import { Side } from '@/game/combat'


export default class Combat extends Vue { export default class Combat extends Vue {
@Prop() @Prop()
combatants!: Array<Creature>
encounter!: Encounter


Side = Side Side = Side


actionDescription = '' actionDescription = ''


created () { created () {
this.$data.left = this.combatants.filter(x => x.side === Side.Heroes)[0]
this.$data.right = this.combatants.filter(x => x.side === Side.Monsters)[0]
this.$data.left = this.encounter.combatants.filter(x => x.side === Side.Heroes)[0]
this.$data.right = this.encounter.combatants.filter(x => x.side === Side.Monsters)[0]
this.$data.combatants = this.encounter.combatants
} }


mounted () { mounted () {
@@ -112,6 +125,23 @@ export default class Combat extends Vue {


log.scrollTo({ top: log.scrollHeight, left: 0 }) log.scrollTo({ top: log.scrollHeight, left: 0 })
} }

this.encounter.nextMove()

if (this.encounter.currentMove.side === Side.Heroes) {
this.$data.left = this.encounter.currentMove
this.$el.querySelector("#left-stats ")
} else if (this.encounter.currentMove.side === Side.Monsters) {
this.$data.right = this.encounter.currentMove
}

// scroll to the newly selected creature
this.$nextTick(() => {
const creature: HTMLElement|null = this.$el.querySelector("[data-current-turn]")
if (creature !== null) {
creature.scrollIntoView()
}
})
} }


@Emit("executedRight") @Emit("executedRight")
@@ -128,6 +158,22 @@ export default class Combat extends Vue {


log.scrollTo({ top: log.scrollHeight, left: 0 }) log.scrollTo({ top: log.scrollHeight, left: 0 })
} }

this.encounter.nextMove()

if (this.encounter.currentMove.side === Side.Heroes) {
this.$data.left = this.encounter.currentMove
} else if (this.encounter.currentMove.side === Side.Monsters) {
this.$data.right = this.encounter.currentMove
}

// scroll to the newly selected creature
this.$nextTick(() => {
const creature: HTMLElement|null = this.$el.querySelector("[data-current-turn]")
if (creature !== null) {
creature.scrollIntoView()
}
})
} }


@Emit("described") @Emit("described")


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

@@ -3,6 +3,7 @@
<div class="statblock-shader statblock-shader-hover"></div> <div class="statblock-shader statblock-shader-hover"></div>
<div class="statblock-shader statblock-shader-selected"></div> <div class="statblock-shader statblock-shader-selected"></div>
<div class="statblock-shader statblock-shader-selected-ally"></div> <div class="statblock-shader statblock-shader-selected-ally"></div>
<div class="statblock-shader statblock-shader-current-turn"></div>
<div class="statblock-shader statblock-shader-dead"></div> <div class="statblock-shader statblock-shader-dead"></div>
<div class="statblock-shader statblock-shader-eaten"></div> <div class="statblock-shader statblock-shader-eaten"></div>
<div class="statblock-content"> <div class="statblock-content">
@@ -13,6 +14,7 @@
<div class="tooltip-body">{{ subject.desc }}</div> <div class="tooltip-body">{{ subject.desc }}</div>
</div> </div>
</h2> </h2>
<div> Initiative: {{ (initiative).toFixed(0) }}%</div>
<div class="statblock-status-icons"> <div class="statblock-status-icons">
<i :class="status.icon" v-for="(status, index) in subject.status" :key="'status' + index"> <i :class="status.icon" v-for="(status, index) in subject.status" :key="'status' + index">
<div class="statblock-status-icon-topleft">{{ status.topLeft }}</div> <div class="statblock-status-icon-topleft">{{ status.topLeft }}</div>
@@ -105,6 +107,9 @@ export default class Statblock extends Vue {
@Prop({ type: Creature, required: true }) @Prop({ type: Creature, required: true })
subject!: Creature subject!: Creature


@Prop()
initiative!: number

private vigorIcons = VigorIcons private vigorIcons = VigorIcons
private statIcons = StatIcons private statIcons = StatIcons
private voreStatIcons = VoreStatIcons private voreStatIcons = VoreStatIcons
@@ -283,6 +288,15 @@ a {
opacity: 0.20; opacity: 0.20;
} }


.statblock[data-current-turn] .statblock-shader-current-turn {
background: #0f0;
opacity: 0.3;
}

.statblock[data-disabled] {
color: #888;
}

.statblock[data-dead] .statblock-shader-dead { .statblock[data-dead] .statblock-shader-dead {
background: red; background: red;
opacity: 0.50; opacity: 0.50;


+ 74
- 20
src/game/combat.ts 查看文件

@@ -320,6 +320,34 @@ export abstract class GroupAction extends Action {
} }
} }


/**
* Individual status effects, items, etc. should override some of these hooks.
* Some hooks just produce a log entry.
* Some hooks return results along with a log entry.
*/
export class Effective {
onApply (creature: Creature): LogEntry { return nilLog }

onRemove (creature: Creature): LogEntry { return nilLog }

preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
return {
prevented: false,
log: nilLog
}
}

preDamage (creature: Creature, damage: Damage): Damage {
return damage
}

preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
return {
prevented: false,
log: nilLog
}
}
}
/** /**
* A displayable status effect * A displayable status effect
*/ */
@@ -346,38 +374,64 @@ export class ImplicitStatus implements VisibleStatus {


/** /**
* This kind of status is explicitly given to a creature. * This kind of status is explicitly given to a creature.
*
* Individual status effects should override some of its hooks.
* Some hooks just produce a log entry.
* Some hooks return results along with a log entry.
*/ */
export abstract class StatusEffect implements VisibleStatus {
export abstract class StatusEffect extends Effective implements VisibleStatus {
constructor (public name: TextLike, public desc: TextLike, public icon: string) { constructor (public name: TextLike, public desc: TextLike, public icon: string) {
super()
} }


get topLeft () { return '' } get topLeft () { return '' }
get bottomRight () { return '' } get bottomRight () { return '' }
}


onApply (creature: Creature): LogEntry { return nilLog }
/**
* An Encounter describes a fight: who is in it and whose turn it is
*/
export class Encounter {
private initiatives: Map<Creature, number>
currentMove: Creature
turnTime = 100


onRemove (creature: Creature): LogEntry { return nilLog }
constructor (public combatants: Creature[]) {
this.initiatives = new Map()


preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
return {
prevented: false,
log: nilLog
}
}
combatants.forEach(combatant => this.initiatives.set(combatant, 0))
this.currentMove = combatants[0]


preDamage (creature: Creature, damage: Damage): Damage {
return damage
this.nextMove()
} }


preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
return {
prevented: false,
log: nilLog
nextMove (): void {
this.initiatives.set(this.currentMove, 0)
const times = new Map<Creature, number>()

this.combatants.forEach(combatant => {
// this should never be undefined
const currentProgress = this.initiatives.get(combatant) ?? 0
const remaining = (this.turnTime - currentProgress) / Math.max(combatant.stats.Speed, 1)
times.set(combatant, remaining)
})

this.currentMove = this.combatants.reduce((closest, next) => {
const closestTime = times.get(closest) ?? 0
const nextTime = times.get(next) ?? 0

return closestTime <= nextTime ? closest : next
}, this.combatants[0])
const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.max(this.currentMove.stats.Speed, 1)

this.combatants.forEach(combatant => {
// still not undefined...
const currentProgress = this.initiatives.get(combatant) ?? 0
this.initiatives.set(combatant, currentProgress + closestRemaining * Math.max(combatant.stats.Speed, 1))
console.log(combatant.name.toString(), currentProgress, closestRemaining)
})

// TODO: still let the creature use drained-vigor moves

console.log(this.currentMove.name.toString())
if (this.currentMove.disabled) {
this.nextMove()
} }
} }
} }

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

@@ -2,10 +2,24 @@ import { StatTest, StatVigorTest } from './tests'
import { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language' import { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language'
import { Entity, Creature } from '../entity' import { Entity, Creature } from '../entity'
import { Damage, DamageFormula, Stat, Vigor, Action } from '../combat' import { Damage, DamageFormula, Stat, Vigor, Action } from '../combat'
import { LogLine, LogLines, LogEntry, CompositeLog } from '../interface'
import { LogLine, LogLines, LogEntry, CompositeLog, nilLog } from '../interface'
import { VoreContainer, Container } from '../vore' import { VoreContainer, Container } from '../vore'
import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition } from './conditions' import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition } from './conditions'


export class PassAction extends Action {
execute (user: Creature, target: Creature): LogEntry {
return nilLog
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine("Do nothing.")
}

constructor () {
super("Pass", "Do nothing", [new SoloCondition()])
}
}

export class AttackAction extends Action { export class AttackAction extends Action {
protected test: StatTest protected test: StatTest




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

@@ -3,6 +3,7 @@ import { Noun, Pronoun, TextLike, POV } from './language'
import { LogEntry, LogLine, LogLines } from './interface' import { LogEntry, LogLine, LogLines } from './interface'
import { Vore, VoreContainer, VoreType, Container } from './vore' import { Vore, VoreContainer, VoreType, Container } from './vore'
import { Item } from './items' import { Item } from './items'
import { PassAction } from './combat/actions'


export interface Entity { export interface Entity {
name: Noun; name: Noun;
@@ -94,6 +95,7 @@ export class Creature extends Vore implements Combatant {
super() super()
const containers = this.containers const containers = this.containers


this.actions.push(new PassAction())
Object.entries(this.maxVigors).forEach(([key, val]) => { Object.entries(this.maxVigors).forEach(([key, val]) => {
this.vigors[key as Vigor] = val this.vigors[key as Vigor] = val
}) })


正在加载...
取消
保存