Feast 2.0!
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 

511 satır
14 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 class="action-description">
  42. </div>
  43. </div>
  44. </template>
  45. <script lang="ts">
  46. import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
  47. import { Creature } from '@/game/creature'
  48. import { POV } from '@/game/language'
  49. import { LogEntry } from '@/game/interface'
  50. import Statblock from './Statblock.vue'
  51. import ActionButton from './ActionButton.vue'
  52. import { Side, Encounter } from '@/game/combat'
  53. @Component(
  54. {
  55. components: { Statblock, ActionButton },
  56. data () {
  57. return {
  58. left: null,
  59. right: null,
  60. combatants: null
  61. }
  62. }
  63. }
  64. )
  65. export default class Combat extends Vue {
  66. @Prop()
  67. encounter!: Encounter
  68. Side = Side
  69. actionDescription = ''
  70. @Emit("described")
  71. described (entry: LogEntry) {
  72. const actionDesc = this.$el.querySelector(".action-description")
  73. if (actionDesc !== null) {
  74. const holder = document.createElement("div")
  75. entry.render().forEach(element => {
  76. holder.appendChild(element)
  77. })
  78. actionDesc.innerHTML = ''
  79. actionDesc.appendChild(holder)
  80. }
  81. }
  82. @Emit("executedLeft")
  83. executedLeft (entry: LogEntry) {
  84. const log = this.$el.querySelector(".log")
  85. if (log !== null) {
  86. const before = log.querySelector("div.log-entry")
  87. const holder = document.createElement("div")
  88. holder.classList.add("log-entry")
  89. entry.render().forEach(element => {
  90. holder.appendChild(element)
  91. })
  92. holder.classList.add("left-move")
  93. const hline = document.createElement("div")
  94. hline.classList.add("log-separator")
  95. log.insertBefore(hline, before)
  96. log.insertBefore(holder, hline)
  97. log.scrollTo({ top: 0, left: 0 })
  98. }
  99. this.encounter.nextMove()
  100. this.pickNext()
  101. }
  102. @Emit("executedRight")
  103. executedRight (entry: LogEntry) {
  104. const log = this.$el.querySelector(".log")
  105. if (log !== null) {
  106. const before = log.querySelector("div.log-entry")
  107. const holder = document.createElement("div")
  108. holder.classList.add("log-entry")
  109. entry.render().forEach(element => {
  110. holder.appendChild(element)
  111. })
  112. holder.classList.add("right-move")
  113. const hline = document.createElement("div")
  114. hline.classList.add("log-separator")
  115. log.insertBefore(hline, before)
  116. log.insertBefore(holder, hline)
  117. log.scrollTo({ top: 0, left: 0 })
  118. }
  119. this.encounter.nextMove()
  120. this.pickNext()
  121. }
  122. pickNext () {
  123. if (this.encounter.currentMove.side === Side.Heroes) {
  124. this.$data.left = this.encounter.currentMove
  125. if (this.encounter.currentMove.containedIn !== null) {
  126. this.$data.right = this.encounter.currentMove.containedIn.owner
  127. }
  128. } else if (this.encounter.currentMove.side === Side.Monsters) {
  129. this.$data.right = this.encounter.currentMove
  130. if (this.encounter.currentMove.containedIn !== null) {
  131. this.$data.left = this.encounter.currentMove.containedIn.owner
  132. }
  133. }
  134. // scroll to the newly selected creature
  135. this.$nextTick(() => {
  136. const creature: HTMLElement|null = this.$el.querySelector("[data-current-turn]")
  137. if (creature !== null) {
  138. this.scrollParentTo(creature)
  139. }
  140. const target: HTMLElement|null = this.$el.querySelector("[data-active]")
  141. if (creature !== null) {
  142. this.scrollParentTo(target)
  143. }
  144. })
  145. }
  146. selectable (creature: Creature): boolean {
  147. return !creature.destroyed && this.encounter.currentMove !== creature
  148. }
  149. doScroll (target: HTMLElement, speed: number, t: number) {
  150. if (t <= 0.25) {
  151. target.scrollBy(speed / 20 - speed / 20 * Math.abs(0.125 - t) * 8, 0)
  152. setTimeout(() => this.doScroll(target, speed, t + 1 / 60), 1000 / 60)
  153. }
  154. }
  155. horizWheelLeft (event: MouseWheelEvent) {
  156. const target = this.$el.querySelector(".left-stats") as HTMLElement
  157. if (target !== null) {
  158. this.doScroll(target, event.deltaY > 0 ? 200 : -200, 0)
  159. }
  160. }
  161. horizWheelRight (event: MouseWheelEvent) {
  162. const target = this.$el.querySelector(".right-stats") as HTMLElement
  163. if (target !== null) {
  164. this.doScroll(target, event.deltaY > 0 ? 200 : -200, 0)
  165. }
  166. }
  167. scrollParentTo (element: HTMLElement): void {
  168. if (element.parentElement !== null) {
  169. const pos = (element.offsetLeft - element.parentElement.offsetLeft)
  170. const width = element.getBoundingClientRect().width / 2
  171. const offset = element.parentElement.getBoundingClientRect().width / 2
  172. console.log(pos, offset)
  173. element.parentElement.scrollTo({ left: pos + width - offset, behavior: "smooth" })
  174. }
  175. }
  176. doSelectLeft (combatant: Creature, element: HTMLElement) {
  177. if (this.selectable(combatant)) {
  178. if (combatant.side !== this.$props.encounter.currentMove.side) {
  179. this.$data.left = combatant
  180. } else {
  181. this.$data.right = combatant
  182. }
  183. }
  184. this.scrollParentTo(element)
  185. }
  186. doSelectRight (combatant: Creature, element: HTMLElement) {
  187. if (this.selectable(combatant)) {
  188. if (combatant.side !== this.$props.encounter.currentMove.side) {
  189. this.$data.right = combatant
  190. } else {
  191. this.$data.left = combatant
  192. }
  193. }
  194. this.scrollParentTo(element)
  195. }
  196. created () {
  197. this.$data.left = this.encounter.combatants.filter(x => x.side === Side.Heroes)[0]
  198. this.$data.right = this.encounter.combatants.filter(x => x.side === Side.Monsters)[0]
  199. this.$data.combatants = this.encounter.combatants
  200. }
  201. mounted () {
  202. const leftStats = this.$el.querySelector(".left-stats")
  203. if (leftStats !== null) {
  204. leftStats.scrollTo(leftStats.getBoundingClientRect().width * 2, 0)
  205. }
  206. this.pickNext()
  207. }
  208. }
  209. </script>
  210. <!-- Add "scoped" attribute to limit CSS to this component only -->
  211. <style scoped>
  212. .spacer {
  213. flex: 1 0;
  214. min-width: 2px;
  215. min-height: 100%;
  216. }
  217. .combat-layout {
  218. position: relative;
  219. display: grid;
  220. grid-template-rows: fit-content(50%) 10% [main-row-start] 1fr 20% [main-row-end] ;
  221. grid-template-columns: 20% [main-col-start] 1fr 1fr [main-col-end] 20%;
  222. width: 100%;
  223. height: 100%;
  224. flex: 10;
  225. overflow-x: hidden;
  226. overflow-y: hidden;
  227. }
  228. .log {
  229. grid-area: main-row-start / main-col-start / main-row-end / main-col-end;
  230. overflow-y: scroll;
  231. font-size: 12pt;
  232. width: 100%;
  233. max-height: 100%;
  234. align-self: flex-start;
  235. }
  236. .left-stats,
  237. .right-stats {
  238. display: flex;
  239. }
  240. .left-stats {
  241. flex-direction: row;
  242. }
  243. .right-stats {
  244. flex-direction: row;
  245. }
  246. .left-stats {
  247. grid-area: 1 / 1 / 2 / 3
  248. }
  249. .right-stats {
  250. grid-area: 1 / 3 / 2 / 5;
  251. }
  252. .statblock-separator-left {
  253. grid-area: 1 / 1 / 2 / 1;
  254. }
  255. .statblock-separator-center {
  256. grid-area: 1 / 3 / 2 / 3;
  257. }
  258. .statblock-separator-right {
  259. grid-area: 1 / 5 / 2 / 5;
  260. }
  261. .statblock-separator {
  262. position: absolute;
  263. width: 10px;
  264. height: 100%;
  265. transform: translate(-5px, 0);
  266. background: linear-gradient(90deg, transparent, #111 3px, #111 7px, transparent 10px);
  267. }
  268. .statblock-row {
  269. overflow-x: scroll;
  270. overflow-y: auto;
  271. }
  272. .left-fader {
  273. grid-area: 2 / 1 / 4 / 2;
  274. }
  275. .right-fader {
  276. grid-area: 2 / 4 / 4 / 5;
  277. }
  278. .left-fader,
  279. .right-fader {
  280. z-index: 1;
  281. pointer-events: none;
  282. background: linear-gradient(to bottom, #111, #00000000 10%, #00000000 90%, #111 100%);
  283. height: 100%;
  284. width: 100%;
  285. }
  286. .left-actions {
  287. grid-area: 2 / 1 / 4 / 2;
  288. }
  289. .right-actions {
  290. grid-area: 2 / 4 / 4 / 5;
  291. }
  292. .left-actions,
  293. .right-actions {
  294. overflow-y: hidden;
  295. display: flex;
  296. flex-direction: column;
  297. height: 100%;
  298. width: 100%;
  299. }
  300. .action-description {
  301. grid-area: 2 / main-col-start / main-row-start / main-col-end;
  302. padding: 8pt;
  303. text-align: center;
  304. font-size: 16px;
  305. }
  306. h3 {
  307. margin: 40px 0 0;
  308. }
  309. ul {
  310. list-style-type: none;
  311. padding: 0;
  312. }
  313. li {
  314. display: inline-block;
  315. margin: 0 10px;
  316. }
  317. a {
  318. color: #42b983;
  319. }
  320. .horiz-display {
  321. display: flex;
  322. justify-content: center;
  323. }
  324. .vert-display {
  325. display: flex;
  326. flex-direction: column;
  327. align-items: center;
  328. flex-wrap: nowrap;
  329. justify-content: start;
  330. height: 100%;
  331. overflow-y: auto;
  332. padding: 64px 0 64px;
  333. }
  334. .action-label {
  335. font-size: 200%;
  336. }
  337. </style>
  338. <style>
  339. .log-damage {
  340. font-weight: bold;
  341. }
  342. .damage-instance {
  343. white-space: nowrap;
  344. }
  345. .log > div.log-entry {
  346. color: #888;
  347. padding-top: 4pt;
  348. padding-bottom: 4pt;
  349. }
  350. div.left-move,
  351. div.right-move {
  352. color: #888;
  353. }
  354. div.left-move {
  355. text-align: start;
  356. margin-right: 25%;
  357. margin-left: 2%;
  358. }
  359. div.right-move {
  360. text-align: end;
  361. margin-left: 25%;
  362. margin-right: 2%;
  363. }
  364. .log img {
  365. width: 75%;
  366. }
  367. .log > div.left-move:nth-child(7) {
  368. color: #898;
  369. }
  370. .log > div.left-move:nth-child(6) {
  371. color: #8a8;
  372. }
  373. .log > div.left-move:nth-child(5) {
  374. color: #8b8;
  375. }
  376. .log > div.left-move:nth-child(4) {
  377. color: #8c8;
  378. }
  379. .log > div.left-move:nth-child(3) {
  380. color: #8d8;
  381. }
  382. .log > div.left-move:nth-child(2) {
  383. color: #8e8;
  384. }
  385. .log > div.left-move:nth-child(1) {
  386. color: #8f8;
  387. }
  388. .log > div.right-move:nth-child(7) {
  389. color: #988;
  390. }
  391. .log > div.right-move:nth-child(6) {
  392. color: #a88;
  393. }
  394. .log > div.right-move:nth-child(5) {
  395. color: #b88;
  396. }
  397. .log > div.right-move:nth-child(4) {
  398. color: #c88;
  399. }
  400. .log > div.right-move:nth-child(3) {
  401. color: #d88;
  402. }
  403. .log > div.right-move:nth-child(2) {
  404. color: #e88;
  405. }
  406. .log > div.right-move:nth-child(1) {
  407. color: #f88;
  408. }
  409. .left-selector,
  410. .right-selector {
  411. display: flex;
  412. flex-wrap: wrap;
  413. }
  414. .combatant-picker {
  415. flex: 1 1;
  416. }
  417. .log-separator {
  418. width: 100%;
  419. height: 4px;
  420. margin: 4pt 0pt 4pt;
  421. background: linear-gradient(90deg, transparent, #444 10%, #444 90%, transparent 100%);
  422. }
  423. </style>