Selaa lähdekoodia

Update actions to properly support group actions

master
Fen Dweller 5 vuotta sitten
vanhempi
commit
5232027540
14 muutettua tiedostoa jossa 151 lisäystä ja 82 poistoa
  1. +6
    -13
      src/components/ActionButton.vue
  2. +1
    -1
      src/components/ChoiceButton.vue
  3. +10
    -10
      src/components/Combat.vue
  4. +1
    -1
      src/components/NavButton.vue
  5. +2
    -2
      src/game/ai.ts
  6. +55
    -24
      src/game/combat.ts
  7. +1
    -9
      src/game/combat/actions.ts
  8. +1
    -1
      src/game/combat/consequences.ts
  9. +22
    -0
      src/game/combat/groupConsequences.ts
  10. +14
    -0
      src/game/combat/targeters.ts
  11. +9
    -16
      src/game/creature.ts
  12. +24
    -2
      src/game/creatures/characters/inazuma.ts
  13. +1
    -0
      src/game/language.ts
  14. +4
    -3
      src/main.ts

+ 6
- 13
src/components/ActionButton.vue Näytä tiedosto

@@ -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")


+ 1
- 1
src/components/ChoiceButton.vue Näytä tiedosto

@@ -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'


+ 10
- 10
src/components/Combat.vue Näytä tiedosto

@@ -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">


+ 1
- 1
src/components/NavButton.vue Näytä tiedosto

@@ -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'


+ 2
- 2
src/game/ai.ts Näytä tiedosto

@@ -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])
}
}



+ 55
- 24
src/game/combat.ts Näytä tiedosto

@@ -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
}

+ 1
- 9
src/game/combat/actions.ts Näytä tiedosto

@@ -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)


+ 1
- 1
src/game/combat/consequences.ts Näytä tiedosto

@@ -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}.`
)
}
}

+ 22
- 0
src/game/combat/groupConsequences.ts Näytä tiedosto

@@ -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
}
}

+ 14
- 0
src/game/combat/targeters.ts Näytä tiedosto

@@ -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)
}
}

+ 9
- 16
src/game/creature.ts Näytä tiedosto

@@ -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(


+ 24
- 2
src/game/creatures/characters/inazuma.ts Näytä tiedosto

@@ -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
}
}

+ 1
- 0
src/game/language.ts Näytä tiedosto

@@ -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,


+ 4
- 3
src/main.ts Näytä tiedosto

@@ -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)


Loading…
Peruuta
Tallenna