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