|
- <template>
- <div class="combat-layout">
- <div @wheel="horizWheelLeft" class="statblock-row left-stats">
- <Statblock v-on="$listeners" @selected="scrollParentTo($event)" @select="doSelectLeft(combatant, $event)" class="left-stats" :data-ally="combatant.side === encounter.currentMove.side" :data-destroyed="combatant.destroyed" :data-current-turn="encounter.currentMove === combatant" :data-active="combatant === left && combatant !== encounter.currentMove" :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).slice().reverse()" v-bind:key="'left-stat-' + index" :subject="combatant" :initiative="encounter.initiatives.get(combatant)" />
- <div class="spacer"></div>
- </div>
- <div @wheel="horizWheelRight" class="statblock-row right-stats">
- <Statblock v-on="$listeners" @selected="scrollParentTo($event)" @select="doSelectRight(combatant, $event)" class="right-stats" :data-ally="combatant.side === encounter.currentMove.side" :data-destroyed="combatant.destroyed" :data-current-turn="encounter.currentMove === combatant" :data-active="combatant === right && combatant !== encounter.currentMove" :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)" v-bind:key="'right-stat-' + index" :subject="combatant" :initiative="encounter.initiatives.get(combatant)" />
- <div class="spacer"></div>
- </div>
- <div class="statblock-separator statblock-separator-left"></div>
- <div class="statblock-separator statblock-separator-center"></div>
- <div class="statblock-separator statblock-separator-right"></div>
- <div class="log">
- <div class="log-entry log-filler"></div>
- </div>
- <div class="left-fader">
-
- </div>
- <div v-if="running" class="left-actions">
- <div v-if="encounter.currentMove === left" class="vert-display">
- <i class="action-label fas fa-users" v-if="left.validGroupActions(right, encounter).length > 0 && left !== right"></i>
- <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left === right ? [] : left.validGroupActions(right, encounter)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="right" :encounter="encounter" :combatants="combatants" />
- <i class="action-label fas fa-user-friends" v-if="left.validSoloActions(right, encounter).length > 0 && left !== right"></i>
- <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left === right ? [] : left.validSoloActions(right, encounter)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="right" :encounter="encounter" :combatants="combatants" />
- <i class="action-label fas fa-user" v-if="left.validActions(left).length > 0"></i>
- <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left.validActions(left)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="left" :encounter="encounter" :combatants="combatants" />
- </div>
- </div>
- <div class="right-fader">
-
- </div>
- <div v-if="running" class="right-actions">
- <div v-if="encounter.currentMove === right" class="vert-display">
- <i class="action-label fas fa-users" v-if="right.validGroupActions(left, encounter).length > 0 && right !== left"></i>
- <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right === left ? [] : right.validGroupActions(left, encounter)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="left" :encounter="encounter" :combatants="combatants" />
- <i class="action-label fas fa-user-friends" v-if="right.validSoloActions(left, encounter).length > 0 && right !== left"></i>
- <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right === left ? [] : right.validSoloActions(left, encounter)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="left" :encounter="encounter" :combatants="combatants" />
- <i class="action-label fas fa-user" v-if="right.validActions(right).length > 0"></i>
- <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right.validActions(right)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="right" :encounter="encounter" :combatants="combatants" />
- </div>
- </div>
- <div v-show="actionDescVisible && encounter.winner === null" class="action-description">
- </div>
- <button @click="$emit('give-in')" v-if="playerDigested" class="give-in">
- Give In
- </button>
- <button @click="$emit('leave-combat')" v-if="!playerDigested && encounter.winner !== null" class="exit-combat">
- Exit Combat
- </button>
- <button @click="continuing = true; pickNext()" v-if="!playerDigested && encounter.winner !== null && !continuing" class="continue-combat">
- Continue
- </button>
- </div>
- </template>
-
- <script lang="ts">
- import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
- import { Creature } from '@/game/creature'
- import { POV } from '@/game/language'
- import { LogEntry, LogLine, nilLog } from '@/game/interface'
- import Statblock from '@/components/Statblock.vue'
- import ActionButton from '@/components/ActionButton.vue'
- import { Side, Encounter } from '@/game/combat'
- import { World } from '@/game/world'
-
- @Component(
- {
- components: { Statblock, ActionButton },
- data () {
- return {
- left: null,
- right: null,
- combatants: null,
- won: false,
- continuing: false,
- totalWon: false,
- actionDescVisible: false
- }
- }
- }
- )
-
- export default class Combat extends Vue {
- @Prop()
- encounter!: Encounter
-
- @Prop()
- world!: World
-
- Side = Side
-
- get playerDigested () {
- return this.world.player.destroyed && this.world.player.containedIn !== null
- }
-
- get running () {
- if (this.encounter.winner === null || (this.$data.continuing === true && this.encounter.totalWinner === null)) {
- return true
- } else {
- return false
- }
- }
-
- @Emit("described")
- described (entry: LogEntry) {
- const actionDesc = this.$el.querySelector(".action-description")
- this.$data.actionDescVisible = entry !== nilLog
- if (actionDesc !== null) {
- const holder = document.createElement("div")
- entry.render().forEach(element => {
- holder.appendChild(element)
- })
- actionDesc.innerHTML = ''
- actionDesc.appendChild(holder)
- }
- }
-
- @Emit("executedLeft")
- executedLeft (entry: LogEntry) {
- this.writeLog(entry, "left")
-
- this.writeLog(this.encounter.nextMove(), "center")
- this.pickNext()
- }
-
- // TODO these need to render on the correct side
-
- @Emit("executedRight")
- executedRight (entry: LogEntry) {
- this.writeLog(entry, "right")
-
- this.writeLog(this.encounter.nextMove(), "center")
- this.pickNext()
- }
-
- writeLog (entry: LogEntry, side = "") {
- const log = this.$el.querySelector(".log")
- if (log !== null) {
- const elements = entry.render()
-
- if (elements.length > 0) {
- const before = log.querySelector("div.log-entry") as HTMLElement|null
-
- const holder = document.createElement("div")
- holder.classList.add("log-entry")
- entry.render().forEach(element => {
- holder.appendChild(element)
- })
-
- if (side !== "") {
- holder.classList.add(side + "-move")
- }
-
- const hline = document.createElement("div")
- hline.classList.add("log-separator")
- if (side !== "") {
- hline.classList.add("log-separator-" + side)
- }
- log.insertBefore(hline, before)
- log.insertBefore(holder, hline)
-
- // TODO this behaves a bit inconsistent -- sometimes it jerks and doesn't scroll to the top
- if (log.scrollTop === 0 && before !== null) {
- log.scrollTo({ top: before.offsetTop, left: 0 })
- }
-
- setTimeout(() => log.scrollTo({ top: 0, left: 0, behavior: "smooth" }), 20)
- }
- }
- }
-
- pickNext () {
- // Did one side win?
-
- if (this.encounter.totalWinner !== null && !this.$data.totalWon) {
- this.$data.totalWon = true
- this.$data.won = true
- this.writeLog(
- new LogLine(
- `game o-vore for good`
- ),
- "center"
- )
- if (this.encounter.winner === this.world.player.side && this.encounter.rewardGifted === false) {
- this.world.player.wallet.Gold += this.encounter.reward
- this.writeLog(
- new LogLine(
- `You found ` + this.encounter.reward + ` gold.`
- ),
- "center"
- )
- this.encounter.rewardGifted = true
- }
- } else if (this.encounter.winner !== null && !this.$data.won && !this.$data.continuing) {
- this.$data.won = true
- this.writeLog(
- new LogLine(
- `game o-vore`
- ),
- "center"
- )
- if (this.encounter.winner === this.world.player.side && this.encounter.rewardGifted === false) {
- this.world.player.wallet.Gold += this.encounter.reward
- this.writeLog(
- new LogLine(
- `You found ` + this.encounter.reward + ` gold.`
- ),
- "center"
- )
- this.encounter.rewardGifted = true
- }
- } else {
- if (this.encounter.currentMove.side === Side.Heroes) {
- this.$data.left = this.encounter.currentMove
-
- if (this.encounter.currentMove.containedIn !== null) {
- this.$data.right = this.encounter.currentMove.containedIn.owner
- }
- } else if (this.encounter.currentMove.side === Side.Monsters) {
- this.$data.right = this.encounter.currentMove
-
- if (this.encounter.currentMove.containedIn !== null) {
- this.$data.left = this.encounter.currentMove.containedIn.owner
- }
- }
- // scroll to the newly selected creature
- this.$nextTick(() => {
- const creature: HTMLElement|null = this.$el.querySelector("[data-current-turn]")
- if (creature !== null) {
- this.scrollParentTo(creature)
- }
- const target: HTMLElement|null = this.$el.querySelector("[data-active]")
- if (target !== null) {
- this.scrollParentTo(target)
- }
- })
-
- if (this.encounter.currentMove.perspective === POV.Third) {
- if (this.encounter.currentMove.side === Side.Heroes) {
- this.executedLeft(this.encounter.currentMove.ai.decide(this.encounter.currentMove, this.encounter))
- } else {
- this.executedRight(this.encounter.currentMove.ai.decide(this.encounter.currentMove, this.encounter))
- }
- } else {
- if (this.encounter.currentMove.containedIn) {
- this.writeLog(
- this.encounter.currentMove.containedIn.statusLine(
- this.encounter.currentMove.containedIn.owner,
- this.encounter.currentMove
- )
- )
- }
- }
- }
- }
-
- selectable (creature: Creature): boolean {
- return !creature.destroyed && this.encounter.currentMove !== creature
- }
-
- doScroll (target: HTMLElement, speed: number, t: number) {
- if (t <= 0.25) {
- target.scrollBy(speed / 20 - speed / 20 * Math.abs(0.125 - t) * 8, 0)
- setTimeout(() => this.doScroll(target, speed, t + 1 / 60), 1000 / 60)
- }
- }
-
- horizWheelLeft (event: MouseWheelEvent) {
- const target = this.$el.querySelector(".left-stats") as HTMLElement
-
- if (target !== null) {
- this.doScroll(target, event.deltaY > 0 ? 200 : -200, 0)
- }
- }
-
- horizWheelRight (event: MouseWheelEvent) {
- const target = this.$el.querySelector(".right-stats") as HTMLElement
-
- if (target !== null) {
- this.doScroll(target, event.deltaY > 0 ? 200 : -200, 0)
- }
- }
-
- scrollParentTo (element: HTMLElement): void {
- if (element.parentElement !== null) {
- const pos = (element.offsetLeft - element.parentElement.offsetLeft)
- const width = element.getBoundingClientRect().width / 2
- const offset = element.parentElement.getBoundingClientRect().width / 2
-
- element.parentElement.scrollTo({ left: pos + width - offset, behavior: "smooth" })
- }
- }
-
- doSelectLeft (combatant: Creature, element: HTMLElement) {
- if (this.selectable(combatant)) {
- if (combatant.side !== this.$props.encounter.currentMove.side) {
- this.$data.left = combatant
- } else {
- this.$data.right = combatant
- }
- }
-
- this.scrollParentTo(element)
- }
-
- doSelectRight (combatant: Creature, element: HTMLElement) {
- if (this.selectable(combatant)) {
- if (combatant.side !== this.$props.encounter.currentMove.side) {
- this.$data.right = combatant
- } else {
- this.$data.left = combatant
- }
- }
-
- this.scrollParentTo(element)
- }
-
- created () {
- 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 () {
- const leftStats = this.$el.querySelector(".left-stats")
-
- if (leftStats !== null) {
- leftStats.scrollTo(leftStats.getBoundingClientRect().width * 2, 0)
- }
-
- this.writeLog(this.encounter.desc.intro(this.world))
-
- this.pickNext()
- }
- }
- </script>
-
- <!-- Add "scoped" attribute to limit CSS to this component only -->
- <style scoped>
-
- .spacer {
- flex: 1 0;
- min-width: 2px;
- min-height: 100%;
- }
-
- .exit-combat,
- .continue-combat,
- .give-in {
- width: 100%;
- padding: 4pt;
- flex: 0 1;
- background: #333;
- border-color: #666;
- border-style: outset;
- user-select: none;
- color: #eee;
- font-size: 36px;
- }
-
- .give-in {
- grid-area: 2 / main-col-start / main-row-start / main-col-end;
- }
- .exit-combat {
- grid-area: 2 / main-col-start / main-row-start / 3;
- }
-
- .continue-combat {
- grid-area: 2 / 3 / main-row-start / main-col-end;
- }
-
- .combat-layout {
- position: relative;
- display: grid;
- grid-template-rows: fit-content(50%) fit-content(20%) [main-row-start] 1fr 20% [main-row-end] ;
- grid-template-columns: 1fr [main-col-start] fit-content(25%) fit-content(25%) [main-col-end] 1fr;
- width: 100%;
- height: 100%;
- overflow-x: hidden;
- overflow-y: hidden;
- margin: auto;
- }
-
- .log {
- position: relative;
- grid-area: main-row-start / main-col-start / main-row-end / main-col-end;
- overflow-y: scroll;
- overflow-x: hidden;
- font-size: 1rem;
- width: 100%;
- max-height: 100%;
- width: 70vw;
- max-width: 1000px;
- align-self: flex-start;
- height: 100%;
- }
-
- .log-filler {
- height: 100%;
- }
-
- .left-stats,
- .right-stats {
- display: flex;
- }
-
- .left-stats {
- flex-direction: row-reverse;
- }
-
- .right-stats {
- flex-direction: row;
- }
-
- .left-stats {
- grid-area: 1 / 1 / 2 / 3
- }
-
- .right-stats {
- grid-area: 1 / 3 / 2 / 5;
- }
-
- .statblock-separator-left {
- grid-area: 1 / 1 / 2 / 1;
- }
-
- .statblock-separator-center {
- grid-area: 1 / 3 / 2 / 3;
- }
-
- .statblock-separator-right {
- grid-area: 1 / 5 / 2 / 5;
- }
-
- .statblock-separator {
- position: absolute;
- width: 10px;
- height: 100%;
- transform: translate(-5px, 0);
- background: linear-gradient(90deg, transparent, #111 3px, #111 7px, transparent 10px);
- }
-
- .statblock-row {
- overflow-x: scroll;
- overflow-y: auto;
- }
-
- .left-fader {
- grid-area: 2 / 1 / 5 / 2;
- }
-
- .right-fader {
- grid-area: 2 / 4 / 5 / 5;
- }
-
- .left-fader,
- .right-fader {
- position: absolute;
- z-index: 1;
- pointer-events: none;
- background: linear-gradient(to bottom, #111, #00000000 10%, #00000000 90%, #111 100%);
- height: 100%;
- width: 100%;
- }
-
- .left-actions {
- grid-area: 2 / 1 / 5 / 2;
- }
-
- .right-actions {
- grid-area: 2 / 4 / 5 / 5;
- }
-
- .left-actions > .vert-display {
- align-items: flex-end;
- }
-
- .right-actions > .vert-display {
- align-items: flex-start;
- }
-
- .left-actions,
- .right-actions {
- overflow-y: hidden;
- display: flex;
- flex-direction: column;
- height: 100%;
- width: 100%;
- }
-
- .action-description {
- position: absolute;
- grid-area: 2 / main-col-start / main-row-end / main-col-end;
- text-align: center;
- font-size: 16px;
- padding-bottom: 48px;
- max-width: 1000px;
- text-align: center;
- width: 100%;
- background: linear-gradient(0deg, transparent, black 48px, black)
- }
-
- 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;
- flex-wrap: nowrap;
- justify-content: start;
- height: 100%;
- width: 100%;
- overflow-y: auto;
- padding: 64px 0 64px;
- }
-
- .action-label {
- font-size: 200%;
- max-width: 300px;
- width: 100%;
- }
- </style>
-
- <style>
-
- .log-damage {
- font-weight: bold;
- }
-
- .damage-instance {
- white-space: nowrap;
- }
-
- .log > div.log-entry {
- position: relative;
- color: #888;
- padding-top: 4pt;
- padding-bottom: 4pt;
- }
-
- div.left-move,
- div.right-move {
- color: #888;
- }
-
- div.left-move {
- text-align: start;
- margin-right: 25%;
- margin-left: 2%;
- }
-
- div.right-move {
- text-align: end;
- margin-left: 25%;
- margin-right: 2%;
- }
-
- .log img {
- width: 75%;
- }
-
- .log > div.left-move:nth-child(7) {
- color: #898;
- }
-
- .log > div.left-move:nth-child(6) {
- color: #8a8;
- }
-
- .log > div.left-move:nth-child(5) {
- color: #8b8;
- }
-
- .log > div.left-move:nth-child(4) {
- color: #8c8;
- }
-
- .log > div.left-move:nth-child(3) {
- color: #8d8;
- }
-
- .log > div.left-move:nth-child(2) {
- color: #8e8;
- }
-
- .log > div.left-move:nth-child(1) {
- color: #8f8;
- }
-
- .log > div.right-move:nth-child(7) {
- color: #988;
- }
-
- .log > div.right-move:nth-child(6) {
- color: #a88;
- }
-
- .log > div.right-move:nth-child(5) {
- color: #b88;
- }
-
- .log > div.right-move:nth-child(4) {
- color: #c88;
- }
-
- .log > div.right-move:nth-child(3) {
- color: #d88;
- }
-
- .log > div.right-move:nth-child(2) {
- color: #e88;
- }
-
- .log > div.right-move:nth-child(1) {
- color: #f88;
- }
-
- .left-selector,
- .right-selector {
- display: flex;
- flex-wrap: wrap;
- }
-
- .combatant-picker {
- flex: 1 1;
- }
-
- .log-separator {
- animation: log-keyframes 0.5s;
- height: 4px;
- background: linear-gradient(90deg, transparent, #444 10%, #444 90%, transparent 100%);
- }
-
- .log-separator-left {
- margin: 4pt auto 4pt 0;
- }
-
- .log-separator-center {
- margin: 4pt auto 4pt;
- }
-
- .log-separator-right {
- margin: 4pt 0 4pt auto;
- }
-
- @keyframes log-keyframes {
- from {
- width: 0%;
- }
-
- to {
- width: 100%;
- }
- }
-
- .left-move {
- animation: left-fly-in 1s;
- }
-
- .right-move {
- animation: right-fly-in 1s;
- }
-
- .center-move {
- animation: center-fly-in 1s;
- }
-
- @keyframes left-fly-in {
- 0% {
- opacity: 0;
- transform: translate(-50px, 0);
- }
-
- 50% {
- transform: translate(0, 0);
- }
-
- 100% {
- opacity: 1;
- }
- }
-
- @keyframes right-fly-in {
- 0% {
- opacity: 0;
- transform: translate(50px, 0);
- }
-
- 50% {
- transform: translate(0, 0);
- }
-
- 100% {
- opacity: 1;
- }
- }
-
- @keyframes center-fly-in {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
- }
- </style>
|