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.
 
 
 
 
 

454 line
12 KiB

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