Feast 2.0!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

624 lines
15 KiB

  1. <template>
  2. <div class="combat-layout">
  3. <div @wheel="horizWheelLeft" class="statblock-row left-stats">
  4. <Statblock @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)" />
  5. <div class="spacer"></div>
  6. </div>
  7. <div @wheel="horizWheelRight" class="statblock-row right-stats">
  8. <Statblock @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)" />
  9. <div class="spacer"></div>
  10. </div>
  11. <div class="statblock-separator statblock-separator-left"></div>
  12. <div class="statblock-separator statblock-separator-center"></div>
  13. <div class="statblock-separator statblock-separator-right"></div>
  14. <div class="log">
  15. </div>
  16. <div class="left-fader">
  17. </div>
  18. <div class="left-actions">
  19. <div v-if="encounter.currentMove === left" class="vert-display">
  20. <i class="action-label fas fa-users" v-if="left.validGroupActions(combatants).length > 0"></i>
  21. <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" />
  22. <i class="action-label fas fa-user-friends" v-if="left.validActions(right).length > 0"></i>
  23. <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left.validActions(right)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="right" :combatants="combatants" />
  24. <i class="action-label fas fa-user" v-if="left.validActions(left).length > 0"></i>
  25. <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left.validActions(left)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="left" :combatants="combatants" />
  26. </div>
  27. <div>{{actionDescription}}</div>
  28. </div>
  29. <div class="right-fader">
  30. </div>
  31. <div class="right-actions">
  32. <div v-if="encounter.currentMove === right" class="vert-display">
  33. <i class="action-label fas fa-users" v-if="right.validGroupActions(combatants).length > 0"></i>
  34. <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" />
  35. <i class="action-label fas fa-user-friends" v-if="right.validActions(left).length > 0"></i>
  36. <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right.validActions(left)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="left" :combatants="combatants" />
  37. <i class="action-label fas fa-user" v-if="right.validActions(right).length > 0"></i>
  38. <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right.validActions(right)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="right" :combatants="combatants" />
  39. </div>
  40. </div>
  41. <div v-if="encounter.winner === null" class="action-description">
  42. </div>
  43. <button @click="$emit('leaveCombat')" v-if="encounter.winner !== null" class="exit-combat">
  44. Exit Combat
  45. </button>
  46. </div>
  47. </template>
  48. <script lang="ts">
  49. import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
  50. import { Creature } from '@/game/creature'
  51. import { POV } from '@/game/language'
  52. import { LogEntry, LogLine } from '@/game/interface'
  53. import Statblock from './Statblock.vue'
  54. import ActionButton from './ActionButton.vue'
  55. import { Side, Encounter } from '@/game/combat'
  56. @Component(
  57. {
  58. components: { Statblock, ActionButton },
  59. data () {
  60. return {
  61. left: null,
  62. right: null,
  63. combatants: null
  64. }
  65. }
  66. }
  67. )
  68. export default class Combat extends Vue {
  69. @Prop()
  70. encounter!: Encounter
  71. Side = Side
  72. actionDescription = ''
  73. @Emit("described")
  74. described (entry: LogEntry) {
  75. const actionDesc = this.$el.querySelector(".action-description")
  76. if (actionDesc !== null) {
  77. const holder = document.createElement("div")
  78. entry.render().forEach(element => {
  79. holder.appendChild(element)
  80. })
  81. actionDesc.innerHTML = ''
  82. actionDesc.appendChild(holder)
  83. }
  84. }
  85. @Emit("executedLeft")
  86. executedLeft (entry: LogEntry) {
  87. this.writeLog(entry, "left")
  88. this.writeLog(this.encounter.nextMove(), "center")
  89. this.pickNext()
  90. }
  91. // TODO these need to render on the correct side
  92. @Emit("executedRight")
  93. executedRight (entry: LogEntry) {
  94. this.writeLog(entry, "right")
  95. this.writeLog(this.encounter.nextMove(), "center")
  96. this.pickNext()
  97. }
  98. writeLog (entry: LogEntry, side = "") {
  99. const log = this.$el.querySelector(".log")
  100. if (log !== null) {
  101. const elements = entry.render()
  102. if (elements.length > 0) {
  103. const before = log.querySelector("div.log-entry")
  104. const holder = document.createElement("div")
  105. holder.classList.add("log-entry")
  106. entry.render().forEach(element => {
  107. holder.appendChild(element)
  108. })
  109. if (side !== "") {
  110. holder.classList.add(side + "-move")
  111. }
  112. const hline = document.createElement("div")
  113. hline.classList.add("log-separator")
  114. if (side !== "") {
  115. hline.classList.add("log-separator-" + side)
  116. }
  117. log.insertBefore(hline, before)
  118. log.insertBefore(holder, hline)
  119. log.scrollTo({ top: 0, left: 0 })
  120. }
  121. }
  122. }
  123. pickNext () {
  124. // Did one side win?
  125. if (this.encounter.winner !== null) {
  126. this.writeLog(
  127. new LogLine(
  128. `game o-vore lmaoooooooo`
  129. ),
  130. "center"
  131. )
  132. } else {
  133. if (this.encounter.currentMove.side === Side.Heroes) {
  134. this.$data.left = this.encounter.currentMove
  135. if (this.encounter.currentMove.containedIn !== null) {
  136. this.$data.right = this.encounter.currentMove.containedIn.owner
  137. }
  138. } else if (this.encounter.currentMove.side === Side.Monsters) {
  139. this.$data.right = this.encounter.currentMove
  140. if (this.encounter.currentMove.containedIn !== null) {
  141. this.$data.left = this.encounter.currentMove.containedIn.owner
  142. }
  143. }
  144. // scroll to the newly selected creature
  145. this.$nextTick(() => {
  146. const creature: HTMLElement|null = this.$el.querySelector("[data-current-turn]")
  147. if (creature !== null) {
  148. this.scrollParentTo(creature)
  149. }
  150. const target: HTMLElement|null = this.$el.querySelector("[data-active]")
  151. if (target !== null) {
  152. this.scrollParentTo(target)
  153. }
  154. })
  155. }
  156. }
  157. selectable (creature: Creature): boolean {
  158. return !creature.destroyed && this.encounter.currentMove !== creature
  159. }
  160. doScroll (target: HTMLElement, speed: number, t: number) {
  161. if (t <= 0.25) {
  162. target.scrollBy(speed / 20 - speed / 20 * Math.abs(0.125 - t) * 8, 0)
  163. setTimeout(() => this.doScroll(target, speed, t + 1 / 60), 1000 / 60)
  164. }
  165. }
  166. horizWheelLeft (event: MouseWheelEvent) {
  167. const target = this.$el.querySelector(".left-stats") as HTMLElement
  168. if (target !== null) {
  169. this.doScroll(target, event.deltaY > 0 ? 200 : -200, 0)
  170. }
  171. }
  172. horizWheelRight (event: MouseWheelEvent) {
  173. const target = this.$el.querySelector(".right-stats") as HTMLElement
  174. if (target !== null) {
  175. this.doScroll(target, event.deltaY > 0 ? 200 : -200, 0)
  176. }
  177. }
  178. scrollParentTo (element: HTMLElement): void {
  179. if (element.parentElement !== null) {
  180. const pos = (element.offsetLeft - element.parentElement.offsetLeft)
  181. const width = element.getBoundingClientRect().width / 2
  182. const offset = element.parentElement.getBoundingClientRect().width / 2
  183. element.parentElement.scrollTo({ left: pos + width - offset, behavior: "smooth" })
  184. }
  185. }
  186. doSelectLeft (combatant: Creature, element: HTMLElement) {
  187. if (this.selectable(combatant)) {
  188. if (combatant.side !== this.$props.encounter.currentMove.side) {
  189. this.$data.left = combatant
  190. } else {
  191. this.$data.right = combatant
  192. }
  193. }
  194. this.scrollParentTo(element)
  195. }
  196. doSelectRight (combatant: Creature, element: HTMLElement) {
  197. if (this.selectable(combatant)) {
  198. if (combatant.side !== this.$props.encounter.currentMove.side) {
  199. this.$data.right = combatant
  200. } else {
  201. this.$data.left = combatant
  202. }
  203. }
  204. this.scrollParentTo(element)
  205. }
  206. created () {
  207. this.$data.left = this.encounter.combatants.filter(x => x.side === Side.Heroes)[0]
  208. this.$data.right = this.encounter.combatants.filter(x => x.side === Side.Monsters)[0]
  209. this.$data.combatants = this.encounter.combatants
  210. }
  211. mounted () {
  212. const leftStats = this.$el.querySelector(".left-stats")
  213. if (leftStats !== null) {
  214. leftStats.scrollTo(leftStats.getBoundingClientRect().width * 2, 0)
  215. }
  216. this.pickNext()
  217. }
  218. }
  219. </script>
  220. <!-- Add "scoped" attribute to limit CSS to this component only -->
  221. <style scoped>
  222. .spacer {
  223. flex: 1 0;
  224. min-width: 2px;
  225. min-height: 100%;
  226. }
  227. .exit-combat {
  228. grid-area: 2 / main-col-start / main-row-start / main-col-end;
  229. width: 100%;
  230. padding: 4pt;
  231. flex: 0 1;
  232. background: #333;
  233. border-color: #666;
  234. border-style: outset;
  235. user-select: none;
  236. color: #eee;
  237. font-size: 36px;
  238. }
  239. .combat-layout {
  240. position: relative;
  241. display: grid;
  242. grid-template-rows: fit-content(50%) 10% [main-row-start] 1fr 20% [main-row-end] ;
  243. grid-template-columns: 1fr [main-col-start] fit-content(25%) fit-content(25%) [main-col-end] 1fr;
  244. width: 100%;
  245. height: 100%;
  246. overflow-x: hidden;
  247. overflow-y: hidden;
  248. margin: auto;
  249. }
  250. .log {
  251. grid-area: main-row-start / main-col-start / main-row-end / main-col-end;
  252. overflow-y: scroll;
  253. overflow-x: hidden;
  254. font-size: 12pt;
  255. width: 100%;
  256. max-height: 100%;
  257. width: 70vw;
  258. max-width: 1000px;
  259. align-self: flex-start;
  260. }
  261. .left-stats,
  262. .right-stats {
  263. display: flex;
  264. }
  265. .left-stats {
  266. flex-direction: row;
  267. }
  268. .right-stats {
  269. flex-direction: row;
  270. }
  271. .left-stats {
  272. grid-area: 1 / 1 / 2 / 3
  273. }
  274. .right-stats {
  275. grid-area: 1 / 3 / 2 / 5;
  276. }
  277. .statblock-separator-left {
  278. grid-area: 1 / 1 / 2 / 1;
  279. }
  280. .statblock-separator-center {
  281. grid-area: 1 / 3 / 2 / 3;
  282. }
  283. .statblock-separator-right {
  284. grid-area: 1 / 5 / 2 / 5;
  285. }
  286. .statblock-separator {
  287. position: absolute;
  288. width: 10px;
  289. height: 100%;
  290. transform: translate(-5px, 0);
  291. background: linear-gradient(90deg, transparent, #111 3px, #111 7px, transparent 10px);
  292. }
  293. .statblock-row {
  294. overflow-x: scroll;
  295. overflow-y: auto;
  296. }
  297. .left-fader {
  298. grid-area: 2 / 1 / 4 / 2;
  299. }
  300. .right-fader {
  301. grid-area: 2 / 4 / 4 / 5;
  302. }
  303. .left-fader,
  304. .right-fader {
  305. position: absolute;
  306. z-index: 1;
  307. pointer-events: none;
  308. background: linear-gradient(to bottom, #111, #00000000 10%, #00000000 90%, #111 100%);
  309. height: 100%;
  310. width: 100%;
  311. }
  312. .left-actions {
  313. grid-area: 2 / 1 / 4 / 2;
  314. }
  315. .right-actions {
  316. grid-area: 2 / 4 / 4 / 5;
  317. }
  318. .left-actions > .vert-display {
  319. align-items: flex-end;
  320. }
  321. .right-actions > .vert-display {
  322. align-items: flex-start;
  323. }
  324. .left-actions,
  325. .right-actions {
  326. overflow-y: hidden;
  327. display: flex;
  328. flex-direction: column;
  329. height: 100%;
  330. width: 100%;
  331. }
  332. .action-description {
  333. grid-area: 2 / main-col-start / main-row-start / main-col-end;
  334. padding: 8pt;
  335. text-align: center;
  336. font-size: 16px;
  337. max-width: 1000px;
  338. }
  339. h3 {
  340. margin: 40px 0 0;
  341. }
  342. ul {
  343. list-style-type: none;
  344. padding: 0;
  345. }
  346. li {
  347. display: inline-block;
  348. margin: 0 10px;
  349. }
  350. a {
  351. color: #42b983;
  352. }
  353. .horiz-display {
  354. display: flex;
  355. justify-content: center;
  356. }
  357. .vert-display {
  358. display: flex;
  359. flex-direction: column;
  360. align-items: center;
  361. flex-wrap: nowrap;
  362. justify-content: start;
  363. height: 100%;
  364. width: 100%;
  365. overflow-y: auto;
  366. padding: 64px 0 64px;
  367. }
  368. .action-label {
  369. font-size: 200%;
  370. max-width: 300px;
  371. width: 100%;
  372. }
  373. </style>
  374. <style>
  375. .log-damage {
  376. font-weight: bold;
  377. }
  378. .damage-instance {
  379. white-space: nowrap;
  380. }
  381. .log > div.log-entry {
  382. color: #888;
  383. padding-top: 4pt;
  384. padding-bottom: 4pt;
  385. }
  386. div.left-move,
  387. div.right-move {
  388. color: #888;
  389. }
  390. div.left-move {
  391. text-align: start;
  392. margin-right: 25%;
  393. margin-left: 2%;
  394. }
  395. div.right-move {
  396. text-align: end;
  397. margin-left: 25%;
  398. margin-right: 2%;
  399. }
  400. .log img {
  401. width: 75%;
  402. }
  403. .log > div.left-move:nth-child(7) {
  404. color: #898;
  405. }
  406. .log > div.left-move:nth-child(6) {
  407. color: #8a8;
  408. }
  409. .log > div.left-move:nth-child(5) {
  410. color: #8b8;
  411. }
  412. .log > div.left-move:nth-child(4) {
  413. color: #8c8;
  414. }
  415. .log > div.left-move:nth-child(3) {
  416. color: #8d8;
  417. }
  418. .log > div.left-move:nth-child(2) {
  419. color: #8e8;
  420. }
  421. .log > div.left-move:nth-child(1) {
  422. color: #8f8;
  423. }
  424. .log > div.right-move:nth-child(7) {
  425. color: #988;
  426. }
  427. .log > div.right-move:nth-child(6) {
  428. color: #a88;
  429. }
  430. .log > div.right-move:nth-child(5) {
  431. color: #b88;
  432. }
  433. .log > div.right-move:nth-child(4) {
  434. color: #c88;
  435. }
  436. .log > div.right-move:nth-child(3) {
  437. color: #d88;
  438. }
  439. .log > div.right-move:nth-child(2) {
  440. color: #e88;
  441. }
  442. .log > div.right-move:nth-child(1) {
  443. color: #f88;
  444. }
  445. .left-selector,
  446. .right-selector {
  447. display: flex;
  448. flex-wrap: wrap;
  449. }
  450. .combatant-picker {
  451. flex: 1 1;
  452. }
  453. .log-separator {
  454. animation: log-keyframes 0.5s;
  455. height: 4px;
  456. background: linear-gradient(90deg, transparent, #444 10%, #444 90%, transparent 100%);
  457. }
  458. .log-separator-left {
  459. margin: 4pt auto 4pt 0;
  460. }
  461. .log-separator-center {
  462. margin: 4pt auto 4pt;
  463. }
  464. .log-separator-right {
  465. margin: 4pt 0 4pt auto;
  466. }
  467. @keyframes log-keyframes {
  468. from {
  469. width: 0%;
  470. }
  471. to {
  472. width: 100%;
  473. }
  474. }
  475. .left-move {
  476. animation: left-fly-in 1s;
  477. }
  478. .right-move {
  479. animation: right-fly-in 1s;
  480. }
  481. .center-move {
  482. animation: center-fly-in 1s;
  483. }
  484. @keyframes left-fly-in {
  485. 0% {
  486. opacity: 0;
  487. transform: translate(-50px, 0);
  488. }
  489. 50% {
  490. transform: translate(0, 0);
  491. }
  492. 100% {
  493. opacity: 1;
  494. }
  495. }
  496. @keyframes right-fly-in {
  497. 0% {
  498. opacity: 0;
  499. transform: translate(50px, 0);
  500. }
  501. 50% {
  502. transform: translate(0, 0);
  503. }
  504. 100% {
  505. opacity: 1;
  506. }
  507. }
  508. @keyframes center-fly-in {
  509. 0% {
  510. opacity: 0;
  511. }
  512. 100% {
  513. opacity: 1;
  514. }
  515. }
  516. </style>