Feast 2.0!
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 
 

726 Zeilen
19 KiB

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