Преглед на файлове

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>
<div id="app">
<Header version="pre-alpha" />
<Combat :combatants="combatants" />
<Combat :encounter="encounter" />
</div>
</template>

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

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

constructor () {
super()

@@ -73,7 +75,9 @@ export default class App extends Vue {
const kenzie = new Creatures.Kenzie()
const cafat = new Creatures.Cafat()
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>


+ 55
- 9
src/components/Combat.vue Целия файл

@@ -1,10 +1,11 @@
<template>
<div class="combat-layout">
<div>{{ encounter.currentMove.name }}</div>
<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 @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 id="log">
</div>
@@ -12,7 +13,7 @@

</div>
<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>
<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>
@@ -26,7 +27,7 @@

</div>
<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>
<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>
@@ -47,7 +48,7 @@ import { POV } from '@/game/language'
import { LogEntry } from '@/game/interface'
import Statblock from './Statblock.vue'
import ActionButton from './ActionButton.vue'
import { Side } from '@/game/combat'
import { Side, Encounter } from '@/game/combat'

@Component(
{
@@ -55,7 +56,8 @@ import { Side } from '@/game/combat'
data () {
return {
left: null,
right: null
right: null,
combatants: null
}
},
methods: {
@@ -72,6 +74,16 @@ import { Side } from '@/game/combat'
if (target !== null) {
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 {
@Prop()
combatants!: Array<Creature>
encounter!: Encounter

Side = Side

actionDescription = ''

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 () {
@@ -112,6 +125,23 @@ export default class Combat extends Vue {

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")
@@ -128,6 +158,22 @@ export default class Combat extends Vue {

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")


+ 14
- 0
src/components/Statblock.vue Целия файл

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

@Prop()
initiative!: number

private vigorIcons = VigorIcons
private statIcons = StatIcons
private voreStatIcons = VoreStatIcons
@@ -283,6 +288,15 @@ a {
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 {
background: red;
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
*/
@@ -346,38 +374,64 @@ export class ImplicitStatus implements VisibleStatus {

/**
* 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) {
super()
}

get topLeft () { 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 { Entity, Creature } from '../entity'
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 { 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 {
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 { Vore, VoreContainer, VoreType, Container } from './vore'
import { Item } from './items'
import { PassAction } from './combat/actions'

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

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


Loading…
Отказ
Запис