Feast 2.0!
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 

469 行
12 KiB

  1. <template>
  2. <div class="combat-layout">
  3. <div @wheel="horizWheelLeft" class="statblock-row" 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 class="spacer"></div>
  6. </div>
  7. <div @wheel="horizWheelRight" class="statblock-row" id="right-stats">
  8. <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)" />
  9. <div class="spacer"></div>
  10. </div>
  11. <div class="statblock-separator" id="statblock-separator-left"></div>
  12. <div class="statblock-separator" id="statblock-separator-center"></div>
  13. <div class="statblock-separator" id="statblock-separator-right"></div>
  14. <div id="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 id="action-desc">
  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 = document.querySelector("#action-desc")
  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 = document.querySelector("#log")
  85. if (log !== null) {
  86. const holder = document.createElement("div")
  87. entry.render().forEach(element => {
  88. holder.appendChild(element)
  89. })
  90. holder.classList.add("left-move")
  91. log.appendChild(holder)
  92. log.scrollTo({ top: log.scrollHeight, left: 0 })
  93. }
  94. this.encounter.nextMove()
  95. this.pickNext()
  96. }
  97. @Emit("executedRight")
  98. executedRight (entry: LogEntry) {
  99. const log = document.querySelector("#log")
  100. if (log !== null) {
  101. const holder = document.createElement("div")
  102. entry.render().forEach(element => {
  103. holder.appendChild(element)
  104. })
  105. holder.classList.add("right-move")
  106. log.appendChild(holder)
  107. log.scrollTo({ top: log.scrollHeight, left: 0 })
  108. }
  109. this.encounter.nextMove()
  110. this.pickNext()
  111. }
  112. pickNext () {
  113. if (this.encounter.currentMove.side === Side.Heroes) {
  114. this.$data.left = this.encounter.currentMove
  115. } else if (this.encounter.currentMove.side === Side.Monsters) {
  116. this.$data.right = this.encounter.currentMove
  117. }
  118. // scroll to the newly selected creature
  119. this.$nextTick(() => {
  120. const creature: HTMLElement|null = this.$el.querySelector("[data-current-turn]")
  121. if (creature !== null) {
  122. creature.scrollIntoView()
  123. }
  124. })
  125. }
  126. selectable (creature: Creature): boolean {
  127. return !creature.destroyed
  128. }
  129. doScroll (target: HTMLElement, speed: number, t: number) {
  130. if (t <= 0.25) {
  131. target.scrollBy(speed / 20 - speed / 20 * Math.abs(0.125 - t) * 8, 0)
  132. setTimeout(() => this.doScroll(target, speed, t + 1 / 60), 1000 / 60)
  133. }
  134. }
  135. horizWheelLeft (event: MouseWheelEvent) {
  136. const target = this.$el.querySelector("#left-stats") as HTMLElement
  137. if (target !== null) {
  138. this.doScroll(target, event.deltaY * 2, 0)
  139. }
  140. }
  141. horizWheelRight (event: MouseWheelEvent) {
  142. const target = this.$el.querySelector("#right-stats") as HTMLElement
  143. if (target !== null) {
  144. this.doScroll(target, event.deltaY * 2, 0)
  145. }
  146. }
  147. doSelectLeft (combatant: Creature) {
  148. if (combatant.side !== this.$props.encounter.currentMove.side && this.selectable(combatant)) {
  149. this.$data.left = combatant
  150. }
  151. }
  152. doSelectRight (combatant: Creature) {
  153. if (combatant.side !== this.$props.encounter.currentMove.side && this.selectable(combatant)) {
  154. this.$data.right = combatant
  155. }
  156. }
  157. created () {
  158. this.$data.left = this.encounter.combatants.filter(x => x.side === Side.Heroes)[0]
  159. this.$data.right = this.encounter.combatants.filter(x => x.side === Side.Monsters)[0]
  160. this.$data.combatants = this.encounter.combatants
  161. }
  162. mounted () {
  163. const leftStats = this.$el.querySelector("#left-stats")
  164. if (leftStats !== null) {
  165. leftStats.scrollTo(leftStats.getBoundingClientRect().width * 2, 0)
  166. }
  167. this.pickNext()
  168. }
  169. }
  170. </script>
  171. <!-- Add "scoped" attribute to limit CSS to this component only -->
  172. <style scoped>
  173. .spacer {
  174. flex: 1 0;
  175. min-width: 2px;
  176. min-height: 100%;
  177. }
  178. .combat-layout {
  179. position: relative;
  180. display: grid;
  181. grid-template-rows: fit-content(50%) 10% [main-row-start] 1fr 20% [main-row-end] ;
  182. grid-template-columns: 20% [main-col-start] 1fr 1fr [main-col-end] 20%;
  183. width: 100%;
  184. height: 100%;
  185. flex: 10;
  186. overflow: hidden;
  187. }
  188. #log {
  189. grid-area: main-row-start / main-col-start / main-row-end / main-col-end;
  190. overflow-y: scroll;
  191. font-size: 12pt;
  192. width: 100%;
  193. max-height: 100%;
  194. align-self: flex-start;
  195. }
  196. #left-stats,
  197. #right-stats {
  198. display: flex;
  199. }
  200. #left-stats {
  201. flex-direction: row;
  202. }
  203. #right-stats {
  204. flex-direction: row;
  205. }
  206. #left-stats {
  207. grid-area: 1 / 1 / 2 / 3
  208. }
  209. #right-stats {
  210. grid-area: 1 / 3 / 2 / 5;
  211. }
  212. #statblock-separator-left {
  213. grid-area: 1 / 1 / 2 / 1;
  214. }
  215. #statblock-separator-center {
  216. grid-area: 1 / 3 / 2 / 3;
  217. }
  218. #statblock-separator-right {
  219. grid-area: 1 / 5 / 2 / 5;
  220. }
  221. .statblock-separator {
  222. position: absolute;
  223. width: 10px;
  224. height: 100%;
  225. transform: translate(-5px, 0);
  226. background: linear-gradient(90deg, transparent, #111 3px, #111 7px, transparent 10px);
  227. }
  228. .statblock-row {
  229. overflow-x: scroll;
  230. overflow-y: auto;
  231. }
  232. .left-fader {
  233. grid-area: 2 / 1 / 4 / 2;
  234. }
  235. .right-fader {
  236. grid-area: 2 / 4 / 4 / 5;
  237. }
  238. .left-fader,
  239. .right-fader {
  240. z-index: 1;
  241. pointer-events: none;
  242. background: linear-gradient(to bottom, #111, #00000000 10%, #00000000 90%, #111 100%);
  243. height: 100%;
  244. width: 100%;
  245. }
  246. .left-actions {
  247. grid-area: 2 / 1 / 4 / 2;
  248. }
  249. .right-actions {
  250. grid-area: 2 / 4 / 4 / 5;
  251. }
  252. .left-actions,
  253. .right-actions {
  254. overflow-y: hidden;
  255. display: flex;
  256. flex-direction: column;
  257. height: 100%;
  258. width: 100%;
  259. }
  260. #action-desc {
  261. grid-area: 2 / main-col-start / main-row-start / main-col-end;
  262. padding: 8pt;
  263. text-align: center;
  264. font-size: 16px;
  265. }
  266. h3 {
  267. margin: 40px 0 0;
  268. }
  269. ul {
  270. list-style-type: none;
  271. padding: 0;
  272. }
  273. li {
  274. display: inline-block;
  275. margin: 0 10px;
  276. }
  277. a {
  278. color: #42b983;
  279. }
  280. .horiz-display {
  281. display: flex;
  282. justify-content: center;
  283. }
  284. .vert-display {
  285. display: flex;
  286. flex-direction: column;
  287. align-items: center;
  288. flex-wrap: nowrap;
  289. justify-content: start;
  290. height: 100%;
  291. overflow-y: auto;
  292. padding: 64px 0 64px;
  293. }
  294. .action-label {
  295. font-size: 200%;
  296. }
  297. </style>
  298. <style>
  299. .log-damage {
  300. font-weight: bold;
  301. }
  302. .damage-instance {
  303. white-space: nowrap;
  304. }
  305. #log > div {
  306. color: #888;
  307. padding-top: 4pt;
  308. padding-bottom: 4pt;
  309. }
  310. div.left-move,
  311. div.right-move {
  312. color: #888;
  313. }
  314. div.left-move {
  315. text-align: start;
  316. margin-right: 25%;
  317. margin-left: 2%;
  318. }
  319. div.right-move {
  320. text-align: end;
  321. margin-left: 25%;
  322. margin-right: 2%;
  323. }
  324. #log img {
  325. width: 75%;
  326. }
  327. #log > div.left-move:nth-last-child(7) {
  328. padding-top: 8pt;
  329. color: #988;
  330. }
  331. #log > div.left-move:nth-last-child(6) {
  332. padding-top: 12pt;
  333. color: #a88;
  334. }
  335. #log > div.left-move:nth-last-child(5) {
  336. padding-top: 16pt;
  337. color: #b88;
  338. }
  339. #log > div.left-move:nth-last-child(4) {
  340. padding-top: 20pt;
  341. color: #c88;
  342. }
  343. #log > div.left-move:nth-last-child(3) {
  344. padding-top: 24pt;
  345. color: #d88;
  346. }
  347. #log > div.left-move:nth-last-child(2) {
  348. padding-top: 28pt;
  349. color: #e88;
  350. }
  351. #log > div.left-move:nth-last-child(1) {
  352. padding-top: 32pt;
  353. color: #f88;
  354. }
  355. #log > div.right-move:nth-last-child(7) {
  356. padding-top: 8pt;
  357. color: #988;
  358. }
  359. #log > div.right-move:nth-last-child(6) {
  360. padding-top: 12pt;
  361. color: #a88;
  362. }
  363. #log > div.right-move:nth-last-child(5) {
  364. padding-top: 16pt;
  365. color: #b88;
  366. }
  367. #log > div.right-move:nth-last-child(4) {
  368. padding-top: 20pt;
  369. color: #c88;
  370. }
  371. #log > div.right-move:nth-last-child(3) {
  372. padding-top: 24pt;
  373. color: #d88;
  374. }
  375. #log > div.right-move:nth-last-child(2) {
  376. padding-top: 28pt;
  377. color: #e88;
  378. }
  379. #log > div.right-move:nth-last-child(1) {
  380. padding-top: 32pt;
  381. color: #f88;
  382. }
  383. .left-selector,
  384. .right-selector {
  385. display: flex;
  386. flex-wrap: wrap;
  387. }
  388. .combatant-picker {
  389. flex: 1 1;
  390. }
  391. </style>