Feast 2.0!
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 

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