| @@ -8,7 +8,7 @@ | |||
| <script lang="ts"> | |||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||
| import { Action, GroupAction } from '@/game/combat' | |||
| import { Action, Encounter } from '@/game/combat' | |||
| import { Creature } from '@/game/creature' | |||
| import { nilLog } from '@/game/interface' | |||
| @@ -23,28 +23,21 @@ export default class ActionButton extends Vue { | |||
| @Prop() | |||
| target!: Creature | |||
| @Prop() | |||
| encounter!: Encounter | |||
| @Prop() | |||
| combatants!: Array<Creature> | |||
| @Emit("execute") | |||
| execute () { | |||
| if ((this.action as GroupAction).executeGroup !== undefined) { | |||
| const action = (this.action as GroupAction) | |||
| this.$emit('executed', (this.action as GroupAction).executeGroup(this.user, action.allowedGroup(this.user, this.combatants))) | |||
| } else { | |||
| this.$emit('executed', this.user.executeAction(this.action, this.target)) | |||
| } | |||
| this.$emit('executed', this.user.executeAction(this.action, this.action.targets(this.target, this.encounter))) | |||
| this.undescribe() | |||
| } | |||
| @Emit("describe") | |||
| describe () { | |||
| if ((this.action as GroupAction).describeGroup !== undefined) { | |||
| const action = (this.action as GroupAction) | |||
| this.$emit('described', action.describeGroup(this.user, action.allowedGroup(this.user, this.combatants))) | |||
| } else { | |||
| this.$emit('described', this.action.describe(this.user, this.target)) | |||
| } | |||
| this.$emit('described', this.action.describe(this.user, this.target)) | |||
| } | |||
| @Emit("undescribe") | |||
| @@ -11,7 +11,7 @@ | |||
| <script lang="ts"> | |||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||
| import { Action, GroupAction } from '@/game/combat' | |||
| import { Action } from '@/game/combat' | |||
| import { Creature } from '@/game/creature' | |||
| import { Place, Direction, Choice, World } from '@/game/world' | |||
| import tippy from 'tippy.js' | |||
| @@ -19,12 +19,12 @@ | |||
| </div> | |||
| <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" /> | |||
| <i class="action-label fas fa-user-friends" v-if="left.validActions(right).length > 0 && left !== right"></i> | |||
| <ActionButton @described="described" @executed="executedLeft" v-for="(action, index) in left === right ? [] : left.validActions(right)" :key="'left-' + action.name + '-' + index" :action="action" :user="left" :target="right" :combatants="combatants" /> | |||
| <i class="action-label fas fa-users" v-if="left.validGroupActions(right, encounter).length > 0 && left !== right"></i> | |||
| <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" /> | |||
| <i class="action-label fas fa-user-friends" v-if="left.validSoloActions(right, encounter).length > 0 && left !== right"></i> | |||
| <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" /> | |||
| <i class="action-label fas fa-user" v-if="left.validActions(left).length > 0"></i> | |||
| <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" /> | |||
| <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" /> | |||
| </div> | |||
| </div> | |||
| <div class="right-fader"> | |||
| @@ -32,12 +32,12 @@ | |||
| </div> | |||
| <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" /> | |||
| <i class="action-label fas fa-user-friends" v-if="right.validActions(left).length > 0 && right !== left"></i> | |||
| <ActionButton @described="described" @executed="executedRight" v-for="(action, index) in right === left ? [] : right.validActions(left)" :key="'right-' + action.name + '-' + index" :action="action" :user="right" :target="left" :combatants="combatants" /> | |||
| <i class="action-label fas fa-users" v-if="right.validGroupActions(left, encounter).length > 0 && right !== left"></i> | |||
| <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" /> | |||
| <i class="action-label fas fa-user-friends" v-if="right.validSoloActions(left, encounter).length > 0 && right !== left"></i> | |||
| <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" /> | |||
| <i class="action-label fas fa-user" v-if="right.validActions(right).length > 0"></i> | |||
| <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" /> | |||
| <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" /> | |||
| </div> | |||
| </div> | |||
| <div v-show="actionDescVisible && encounter.winner === null" class="action-description"> | |||
| @@ -11,7 +11,7 @@ | |||
| <script lang="ts"> | |||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||
| import { Action, GroupAction } from '@/game/combat' | |||
| import { Action } from '@/game/combat' | |||
| import { Creature } from '@/game/creature' | |||
| import { Place, Direction } from '@/game/world' | |||
| import tippy from 'tippy.js' | |||
| @@ -61,12 +61,12 @@ export class AI { | |||
| ) | |||
| if (chosen !== undefined) { | |||
| return chosen.action.try(actor, chosen.target) | |||
| return chosen.action.try(actor, chosen.action.targets(chosen.target, encounter)) | |||
| } | |||
| // if we filtered out EVERY action, we should just give up and pass | |||
| return new PassAction().try(actor, actor) | |||
| return new PassAction().try(actor, [actor]) | |||
| } | |||
| } | |||
| @@ -107,6 +107,10 @@ export interface CombatTest { | |||
| fail: (user: Creature, target: Creature) => LogEntry; | |||
| } | |||
| export interface Targeter { | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature>; | |||
| } | |||
| /** | |||
| * An instance of damage. Contains zero or more [[DamageInstance]] objects | |||
| */ | |||
| @@ -375,7 +379,6 @@ export enum Side { | |||
| */ | |||
| export interface Combatant { | |||
| actions: Array<Action>; | |||
| groupActions: Array<GroupAction>; | |||
| side: Side; | |||
| } | |||
| @@ -400,13 +403,20 @@ export abstract class Action { | |||
| return this.name.toString() | |||
| } | |||
| try (user: Creature, target: Creature): LogEntry { | |||
| const failReason = this.tests.find(test => !test.test(user, target)) | |||
| if (failReason !== undefined) { | |||
| return failReason.fail(user, target) | |||
| } else { | |||
| return this.execute(user, target) | |||
| } | |||
| try (user: Creature, targets: Array<Creature>): LogEntry { | |||
| const results = targets.map(target => { | |||
| const failReason = this.tests.find(test => !test.test(user, target)) | |||
| if (failReason !== undefined) { | |||
| return { failed: true, target: target, log: failReason.fail(user, target) } | |||
| } else { | |||
| return { failed: false, target: target, log: this.execute(user, target) } | |||
| } | |||
| }) | |||
| return new LogLines( | |||
| ...results.map(result => result.log), | |||
| this.executeAll(user, results.filter(result => !result.failed).map(result => result.target)) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature, verbose = true): LogEntry { | |||
| @@ -424,11 +434,21 @@ export abstract class Action { | |||
| return this.tests.reduce((total, test) => total * test.odds(user, target), 1) | |||
| } | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature> { | |||
| return [primary] | |||
| } | |||
| executeAll (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return nilLog | |||
| } | |||
| abstract execute (user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class CompositionAction extends Action { | |||
| public consequences: Array<Consequence>; | |||
| public groupConsequences: Array<GroupConsequence>; | |||
| public targeters: Array<Targeter>; | |||
| constructor ( | |||
| name: TextLike, | |||
| @@ -436,11 +456,15 @@ export class CompositionAction extends Action { | |||
| properties: { | |||
| conditions?: Array<Condition>; | |||
| consequences?: Array<Consequence>; | |||
| groupConsequences?: Array<GroupConsequence>; | |||
| tests?: Array<CombatTest>; | |||
| targeters?: Array<Targeter>; | |||
| } | |||
| ) { | |||
| super(name, desc, properties.conditions ?? [], properties.tests ?? []) | |||
| this.consequences = properties.consequences ?? [] | |||
| this.groupConsequences = properties.groupConsequences ?? [] | |||
| this.targeters = properties.targeters ?? [] | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| @@ -449,6 +473,12 @@ export class CompositionAction extends Action { | |||
| ) | |||
| } | |||
| executeAll (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLines( | |||
| ...this.groupConsequences.map(consequence => consequence.apply(user, targets.filter(target => consequence.applicable(user, target)))) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| ...this.consequences.map(consequence => consequence.describe(user, target)).concat( | |||
| @@ -457,6 +487,10 @@ export class CompositionAction extends Action { | |||
| ) | |||
| ) | |||
| } | |||
| targets (primary: Creature, encounter: Encounter) { | |||
| return this.targeters.flatMap(targeter => targeter.targets(primary, encounter)).unique() | |||
| } | |||
| } | |||
| /** | |||
| @@ -471,22 +505,6 @@ export interface Actionable { | |||
| actions: Array<Action>; | |||
| } | |||
| export abstract class GroupAction extends Action { | |||
| constructor (name: TextLike, desc: TextLike, conditions: Array<Condition>) { | |||
| super(name, desc, conditions) | |||
| } | |||
| allowedGroup (user: Creature, targets: Array<Creature>): Array<Creature> { | |||
| return targets.filter(target => this.allowed(user, target)) | |||
| } | |||
| executeGroup (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLines(...targets.map(target => this.execute(user, target))) | |||
| } | |||
| abstract describeGroup (user: Creature, targets: Array<Creature>): LogEntry | |||
| } | |||
| /** | |||
| * Individual status effects, items, etc. should override some of these hooks. | |||
| * Some hooks just produce a log entry. | |||
| @@ -770,3 +788,16 @@ export abstract class Consequence { | |||
| abstract describe (user: Creature, target: Creature): LogEntry | |||
| abstract apply (user: Creature, target: Creature): LogEntry | |||
| } | |||
| export abstract class GroupConsequence { | |||
| constructor (public conditions: Condition[]) { | |||
| } | |||
| applicable (user: Creature, target: Creature): boolean { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| } | |||
| abstract describe (user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract apply (user: Creature, targets: Array<Creature>): LogEntry | |||
| } | |||
| @@ -40,15 +40,7 @@ export abstract class DamageAction extends Action { | |||
| ) | |||
| } | |||
| try (user: Creature, target: Creature): LogEntry { | |||
| const effectResults = target.effects.map(effect => effect.preAttack(target, user)) | |||
| if (effectResults.some(result => result.prevented)) { | |||
| return new LogLines(...effectResults.map(result => result.log)) | |||
| } else { | |||
| return super.try(user, target) | |||
| } | |||
| } | |||
| // TODO: remove me or replace the logic for damage prevention | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const damage = this.damage.calc(user, target) | |||
| @@ -117,7 +117,7 @@ export class ConsumeConsequence extends Consequence { | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine( | |||
| `Devours the target.` | |||
| `${this.container.consumeVerb.singular.capital} ${target.name.objective}, sending ${target.pronouns.objective} ${this.container.consumePreposition} ${user.name.possessive} ${this.container.name}.` | |||
| ) | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| import { GroupConsequence, Condition } from '../combat' | |||
| import { Creature } from '../creature' | |||
| import { LogEntry, nilLog } from '../interface' | |||
| import { GroupLine } from '../language' | |||
| /** | |||
| * Renders some text. | |||
| */ | |||
| export class LogGroupConsequence extends GroupConsequence { | |||
| constructor (private line: GroupLine<Creature>, conditions: Condition[] = []) { | |||
| super(conditions) | |||
| } | |||
| apply (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return this.line(user, targets) | |||
| } | |||
| describe (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return nilLog | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| import { Encounter, Targeter } from '../combat' | |||
| import { Creature } from '../creature' | |||
| export class SoloTargeter implements Targeter { | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature> { | |||
| return [primary] | |||
| } | |||
| } | |||
| export class SideTargeter implements Targeter { | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature> { | |||
| return encounter.combatants.filter(combatant => primary.side === combatant.side) | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import { Damage, Stats, Action, Vigor, Side, GroupAction, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors } from '@/game/combat' | |||
| import { Damage, Stats, Action, Vigor, Side, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors, Encounter } from '@/game/combat' | |||
| import { Noun, Pronoun, SoloLine, Verb } from '@/game/language' | |||
| import { LogEntry, LogLines, LogLine } from '@/game/interface' | |||
| import { VoreContainer, VoreType, Container } from '@/game/vore' | |||
| @@ -53,7 +53,6 @@ export class Creature extends Entity { | |||
| statusEffects: Array<StatusEffect> = []; | |||
| perks: Array<Perk> = []; | |||
| groupActions: Array<GroupAction> = []; | |||
| items: Array<Item> = []; | |||
| /* eslint-disable-next-line */ | |||
| wallet: { [key in Currency]: number } = Object.keys(Currency).reduce((total: any, key) => { total[key] = 0; return total }, {}); | |||
| @@ -218,16 +217,10 @@ export class Creature extends Entity { | |||
| this.perks.push(perk) | |||
| } | |||
| executeAction (action: Action, target: Creature): LogEntry { | |||
| const preActionResults = this.effects.map(effect => effect.preAction(this)) | |||
| const preReceiveActionResults = target.effects.map(effect => effect.preReceiveAction(target, this)) | |||
| // TODO replace the logic for getting blocked or prevented from acting | |||
| const blocking = preActionResults.concat(preReceiveActionResults).filter(result => result.prevented) | |||
| if (blocking.length > 0) { | |||
| return new LogLines(...blocking.map(result => result.log)) | |||
| } else { | |||
| return action.try(this, target) | |||
| } | |||
| executeAction (action: Action, targets: Array<Creature>): LogEntry { | |||
| return action.try(this, targets) | |||
| } | |||
| removeEffect (effect: StatusEffect): LogEntry { | |||
| @@ -297,12 +290,12 @@ export class Creature extends Entity { | |||
| }) | |||
| } | |||
| validGroupActions (targets: Array<Creature>): Array<GroupAction> { | |||
| const choices = this.groupActions | |||
| validSoloActions (target: Creature, encounter: Encounter): Array<Action> { | |||
| return this.validActions(target).filter(action => action.targets(target, encounter).length === 1) | |||
| } | |||
| return choices.filter(action => { | |||
| return targets.some(target => action.allowed(this, target)) | |||
| }) | |||
| validGroupActions (target: Creature, encounter: Encounter): Array<Action> { | |||
| return this.validActions(target).filter(action => action.targets(target, encounter).length > 1) | |||
| } | |||
| destroyLine: SoloLine<Creature> = (victim) => new LogLine( | |||
| @@ -1,6 +1,11 @@ | |||
| import { FavorEscapedPrey, VoreAI } from '@/game/ai' | |||
| import { DamageType, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat' | |||
| import { CompositionAction, DamageType, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat' | |||
| import { PairCondition, TogetherCondition } from '@/game/combat/conditions' | |||
| import { ConsumeConsequence } from '@/game/combat/consequences' | |||
| import { LogGroupConsequence } from '@/game/combat/groupConsequences' | |||
| import { SideTargeter } from '@/game/combat/targeters' | |||
| import { Creature } from '@/game/creature' | |||
| import { LogLine } from '@/game/interface' | |||
| import { ImproperNoun, MalePronouns, ProperNoun } from '@/game/language' | |||
| import { anyVore, Stomach } from '@/game/vore' | |||
| @@ -28,12 +33,29 @@ export default class Inazuma extends Creature { | |||
| this.ai.addDecider(new FavorEscapedPrey()) | |||
| this.addVoreContainer(new Stomach( | |||
| const stomach = new Stomach( | |||
| this, | |||
| 2, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Power, target: Vigor.Health, type: DamageType.Acid } | |||
| ]) | |||
| ) | |||
| this.actions.push(new CompositionAction( | |||
| "Mass Devour", | |||
| "Eat everyone!", | |||
| { | |||
| conditions: [new PairCondition(), new TogetherCondition()], | |||
| targeters: [new SideTargeter()], | |||
| consequences: [new ConsumeConsequence(stomach)], | |||
| groupConsequences: [new LogGroupConsequence( | |||
| (user, targets) => new LogLine(`With a mighty GULP!, all ${targets.length} of ${user.name.possessive} prey are swallowed down.`) | |||
| )] | |||
| } | |||
| )) | |||
| this.addVoreContainer(stomach) | |||
| this.ai = null | |||
| } | |||
| } | |||
| @@ -6,6 +6,7 @@ export type SoloLine<T> = (user: T) => LogEntry | |||
| export type SoloLineArgs<T, V> = (user: T, args: V) => LogEntry | |||
| export type PairLine<T> = (user: T, target: T) => LogEntry | |||
| export type PairLineArgs<T, V> = (user: T, target: T, args: V) => LogEntry | |||
| export type GroupLine<T> = (user: T, targets: Array<T>) => LogEntry | |||
| enum NounKind { | |||
| Specific, | |||
| @@ -5,7 +5,7 @@ declare global { | |||
| interface Array<T> { | |||
| joinGeneral (item: T, endItem: T|null): Array<T>; | |||
| /* eslint-disable-next-line */ | |||
| unique (predicate: (elem: T) => any): Array<T>; | |||
| unique (predicate?: (elem: T) => any): Array<T>; | |||
| } | |||
| } | |||
| @@ -19,11 +19,12 @@ Array.prototype.joinGeneral = function (item, endItem = null) { | |||
| } | |||
| /* eslint-disable-next-line */ | |||
| Array.prototype.unique = function<T> (predicate: (elem: T) => any): Array<T> { | |||
| Array.prototype.unique = function<T> (predicate?: (elem: T) => any): Array<T> { | |||
| const set = new Set() | |||
| const result: Array<T> = [] as T[] | |||
| this.forEach(elem => { | |||
| const predResult = predicate(elem) | |||
| // if there is no predicate, just use the identity function | |||
| const predResult = (predicate ?? (x => x))(elem) | |||
| if (!set.has(predResult)) { | |||
| set.add(predResult) | |||
| result.push(elem) | |||