| @@ -17,7 +17,7 @@ | |||
| <div class="left-fader"> | |||
| </div> | |||
| <div class="left-actions"> | |||
| <div v-if="running" class="left-actions"> | |||
| <div v-if="encounter.currentMove === left" class="vert-display"> | |||
| <i class="action-label fas fa-users" v-if="left.validGroupActions(combatants).length > 0"></i> | |||
| <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" /> | |||
| @@ -31,7 +31,7 @@ | |||
| <div class="right-fader"> | |||
| </div> | |||
| <div class="right-actions"> | |||
| <div v-if="running" class="right-actions"> | |||
| <div v-if="encounter.currentMove === right" class="vert-display"> | |||
| <i class="action-label fas fa-users" v-if="right.validGroupActions(combatants).length > 0"></i> | |||
| <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" /> | |||
| @@ -46,6 +46,9 @@ | |||
| <button @click="$emit('leaveCombat')" v-if="encounter.winner !== null" class="exit-combat"> | |||
| Exit Combat | |||
| </button> | |||
| <button @click="continuing = true; pickNext()" v-if="encounter.winner !== null && !continuing" class="continue-combat"> | |||
| Continue | |||
| </button> | |||
| </div> | |||
| </template> | |||
| @@ -66,7 +69,10 @@ import { NoAI } from '../game/ai' | |||
| return { | |||
| left: null, | |||
| right: null, | |||
| combatants: null | |||
| combatants: null, | |||
| won: false, | |||
| continuing: false, | |||
| totalWon: false | |||
| } | |||
| } | |||
| } | |||
| @@ -80,6 +86,14 @@ export default class Combat extends Vue { | |||
| actionDescription = '' | |||
| get running () { | |||
| if (this.encounter.winner === null || (this.$data.continuing === true && this.encounter.totalWinner === null)) { | |||
| return true | |||
| } else { | |||
| return false | |||
| } | |||
| } | |||
| @Emit("described") | |||
| described (entry: LogEntry) { | |||
| const actionDesc = this.$el.querySelector(".action-description") | |||
| @@ -150,11 +164,22 @@ export default class Combat extends Vue { | |||
| pickNext () { | |||
| // Did one side win? | |||
| console.log(this.encounter.winner, this.encounter.totalWinner) | |||
| if (this.encounter.winner !== null) { | |||
| if (this.encounter.totalWinner !== null && !this.$data.totalWon) { | |||
| this.$data.totalWon = true | |||
| this.$data.won = true | |||
| this.writeLog( | |||
| new LogLine( | |||
| `game o-vore lmaoooooooo` | |||
| `game o-vore for good` | |||
| ), | |||
| "center" | |||
| ) | |||
| } else if (this.encounter.winner !== null && !this.$data.won && !this.$data.continuing) { | |||
| this.$data.won = true | |||
| this.writeLog( | |||
| new LogLine( | |||
| `game o-vore` | |||
| ), | |||
| "center" | |||
| ) | |||
| @@ -282,8 +307,8 @@ export default class Combat extends Vue { | |||
| min-height: 100%; | |||
| } | |||
| .exit-combat { | |||
| grid-area: 2 / main-col-start / main-row-start / main-col-end; | |||
| .exit-combat, | |||
| .continue-combat { | |||
| width: 100%; | |||
| padding: 4pt; | |||
| flex: 0 1; | |||
| @@ -295,6 +320,14 @@ export default class Combat extends Vue { | |||
| font-size: 36px; | |||
| } | |||
| .exit-combat { | |||
| grid-area: 2 / main-col-start / main-row-start / 3; | |||
| } | |||
| .continue-combat { | |||
| grid-area: 2 / 3 / main-row-start / main-col-end; | |||
| } | |||
| .combat-layout { | |||
| position: relative; | |||
| display: grid; | |||
| @@ -71,7 +71,7 @@ | |||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||
| import { Creature } from '@/game/creature' | |||
| import { POV } from '@/game/language' | |||
| import { NoAI, RandomAI } from '@/game/ai' | |||
| import { NoAI, RandomAI, VoreAI } from '@/game/ai' | |||
| import { Stats, Stat, StatIcons, StatDescs, Vigor, VigorIcons, VigorDescs, VoreStatDescs, VoreStatIcons, VisibleStatus } from '@/game/combat' | |||
| import ContainerView from './ContainerView.vue' | |||
| import tippy, { delegate, createSingleton } from 'tippy.js' | |||
| @@ -84,7 +84,7 @@ import 'tippy.js/dist/tippy.css' | |||
| data () { | |||
| return { | |||
| POV: POV, | |||
| ais: [new NoAI(), new RandomAI()] | |||
| ais: [new NoAI(), new RandomAI(), new VoreAI()] | |||
| } | |||
| }, | |||
| methods: { | |||
| @@ -1,6 +1,7 @@ | |||
| import { Creature } from './creature' | |||
| import { Encounter } from './combat' | |||
| import { LogEntry } from './interface' | |||
| import { ReleaseAction, TransferAction } from './combat/actions' | |||
| export interface AI { | |||
| name: string; | |||
| @@ -28,3 +29,23 @@ export class RandomAI implements AI { | |||
| return chosen.action.execute(actor, chosen.target) | |||
| } | |||
| } | |||
| /** | |||
| * The VoreAI tries to perform moves from its containers | |||
| */ | |||
| export class VoreAI extends RandomAI { | |||
| name = "Vore AI" | |||
| decide (actor: Creature, encounter: Encounter): LogEntry { | |||
| const actions = encounter.combatants.flatMap(enemy => actor.validActions(enemy).map(action => ({ | |||
| target: enemy, | |||
| action: action | |||
| }))) | |||
| const voreActions = actions.filter(action => actor.otherContainers.concat(actor.containers).some(container => container.actions.includes(action.action) || action instanceof TransferAction)) | |||
| const aggressiveActions = voreActions.filter(action => !(action.action instanceof ReleaseAction)) | |||
| const chosen = aggressiveActions[Math.floor(Math.random() * aggressiveActions.length)] | |||
| if (chosen === undefined) { | |||
| return super.decide(actor, encounter) | |||
| } | |||
| return chosen.action.execute(actor, chosen.target) | |||
| } | |||
| } | |||
| @@ -579,6 +579,9 @@ export class Encounter { | |||
| return nilLog | |||
| } | |||
| /** | |||
| * Combat is won once one side is completely disabled | |||
| */ | |||
| get winner (): null|Side { | |||
| const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.disabled).map(combatant => combatant.side)) | |||
| @@ -588,6 +591,19 @@ export class Encounter { | |||
| return null | |||
| } | |||
| } | |||
| /** | |||
| * Combat is completely won once one side is completely destroyed | |||
| */ | |||
| get totalWinner (): null|Side { | |||
| const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.destroyed).map(combatant => combatant.side)) | |||
| if (remaining.size === 1) { | |||
| return Array.from(remaining)[0] | |||
| } else { | |||
| return null | |||
| } | |||
| } | |||
| } | |||
| export abstract class Consequence { | |||
| @@ -46,6 +46,8 @@ export abstract class Mortal extends Entity { | |||
| [Vigor.Resolve]: 100 | |||
| } | |||
| destroyed = false; | |||
| constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats) { | |||
| super(name, kind, pronouns) | |||
| this.stats = Object.keys(Stat).reduce((base: any, key) => { base[key] = baseStats[key as Stat]; return base }, {}) | |||
| @@ -109,6 +111,10 @@ export abstract class Mortal extends Entity { | |||
| } | |||
| }) | |||
| if (this.vigors.Health <= -this.maxVigors.Health) { | |||
| this.destroyed = true | |||
| } | |||
| if (this.vigors.Health <= 0 && startHealth > 0) { | |||
| return this.destroy() | |||
| } else { | |||
| @@ -24,7 +24,6 @@ export abstract class Vore extends Mortal { | |||
| otherContainers: Array<Container> = [] | |||
| containedIn: Container | null = null | |||
| destroyed = false; | |||
| voreStats: VoreStats | |||