Feast 2.0!
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 

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