Преглед на файлове

Add explanations to conditions. Make digestion happen passively in combat

master
Fen Dweller преди 5 години
родител
ревизия
1ff96a390c
променени са 11 файла, в които са добавени 236 реда и са изтрити 30 реда
  1. +2
    -2
      src/game/ai.ts
  2. +5
    -4
      src/game/ai/deciders.ts
  3. +22
    -4
      src/game/combat.ts
  4. +17
    -11
      src/game/combat/actions.ts
  5. +158
    -0
      src/game/combat/conditions.ts
  6. +14
    -0
      src/game/combat/consequences.ts
  7. +1
    -4
      src/game/combat/tests.ts
  8. +7
    -2
      src/game/creature.ts
  9. +2
    -0
      src/game/entity.ts
  10. +3
    -3
      src/game/vore.ts
  11. +5
    -0
      src/game/words.ts

+ 2
- 2
src/game/ai.ts Целия файл

@@ -2,7 +2,7 @@ import { Creature } from './creature'
import { Encounter, Action } from './combat'
import { LogEntry } from './interface'
import { PassAction } from './combat/actions'
import { NoPassDecider, NoReleaseDecider, ChanceDecider, NoSurrenderDecider, FavorDigestDecider } from './ai/deciders'
import { NoPassDecider, NoReleaseDecider, ChanceDecider, NoSurrenderDecider, FavorRubDecider } from './ai/deciders'

/**
* A Decider determines how favorable an action is to perform.
@@ -68,7 +68,7 @@ export class VoreAI extends AI {
new NoSurrenderDecider(),
new NoPassDecider(),
new ChanceDecider(),
new FavorDigestDecider()
new FavorRubDecider()
]
)
}


+ 5
- 4
src/game/ai/deciders.ts Целия файл

@@ -1,7 +1,7 @@
import { Decider } from '../ai'
import { Encounter, Action, Consequence, CompositionAction } from '../combat'
import { Creature } from '../creature'
import { PassAction, ReleaseAction, DigestAction } from '../combat/actions'
import { PassAction, ReleaseAction, RubAction } from '../combat/actions'
import { StatusConsequence } from '../combat/consequences'
import { SurrenderEffect } from '../combat/effects'

@@ -44,6 +44,7 @@ export class ChanceDecider implements Decider {
* Adjusts the weights for [[CompositionAction]]s that contain the specified consequence
*/
export class ConsequenceDecider<T extends Consequence> implements Decider {
/* eslint-disable-next-line */
constructor (private consequenceType: new (...args: any) => T, private weight: number) {

}
@@ -100,11 +101,11 @@ export class NoSurrenderDecider extends ConsequenceFunctionDecider {
}

/**
* Favors [[DigestAction]]s
* Favors [[RubAction]]s
*/
export class FavorDigestDecider implements Decider {
export class FavorRubDecider implements Decider {
decide (encounter: Encounter, user: Creature, target: Creature, action: Action) {
if (action instanceof DigestAction) {
if (action instanceof RubAction) {
return 5
} else {
return 1


+ 22
- 4
src/game/combat.ts Целия файл

@@ -152,8 +152,11 @@ export class Damage {
}))
}

// TODO is there a way to do this that will satisfy the typechecker?
renderShort (): LogEntry {
/* eslint-disable-next-line */
const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
/* eslint-disable-next-line */
const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {})
this.damages.forEach(instance => {
const factor = instance.type === DamageType.Heal ? -1 : 1
@@ -388,9 +391,12 @@ export abstract class Action {

describe (user: Creature, target: Creature): LogEntry {
return new LogLines(
...this.conditions.map(condition => condition.explain(user, target)),
new Newline(),
new LogLine(
`Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%`
),
new Newline(),
...this.tests.map(test => test.explain(user, target))
)
}
@@ -439,6 +445,7 @@ export class CompositionAction extends Action {
*/
export interface Condition {
allowed: (user: Creature, target: Creature) => boolean;
explain: (user: Creature, target: Creature) => LogEntry;
}

export interface Actionable {
@@ -596,7 +603,7 @@ export type EncounterDesc = {
export class Encounter {
initiatives: Map<Creature, number>
currentMove: Creature
turnTime = 100
turnTime = 500

constructor (public desc: EncounterDesc, public combatants: Creature[]) {
this.initiatives = new Map()
@@ -607,7 +614,7 @@ export class Encounter {
this.nextMove()
}

nextMove (): LogEntry {
nextMove (totalTime = 0): LogEntry {
this.initiatives.set(this.currentMove, 0)
const times = new Map<Creature, number>()

@@ -635,15 +642,26 @@ export class Encounter {
// TODO: still let the creature use drained-vigor moves

if (this.currentMove.disabled) {
return this.nextMove()
return this.nextMove(closestRemaining + totalTime)
} else {
// applies digestion every time combat advances
const tickResults = this.combatants.flatMap(
combatant => combatant.containers.map(
container => container.tick(closestRemaining + totalTime)
)
)
const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented)

if (effectResults.some(result => result.prevented)) {
const parts = effectResults.map(result => result.log).concat([this.nextMove()])

return new LogLines(
...parts
...parts,
...tickResults
)
} else {
return new LogLines(
...tickResults
)
}
}


+ 17
- 11
src/game/combat/actions.ts Целия файл

@@ -2,10 +2,11 @@ import { StatTest, StatVigorTest, StatVigorSizeTest } from './tests'
import { DynText, LiveText, TextLike, Verb, PairLine, PairLineArgs } from '../language'
import { Entity } from '../entity'
import { Creature } from "../creature"
import { Damage, DamageFormula, Stat, Vigor, Action, Condition, CombatTest } from '../combat'
import { Damage, DamageFormula, Stat, Vigor, Action, Condition, CombatTest, CompositionAction } from '../combat'
import { LogLine, LogLines, LogEntry, nilLog } from '../interface'
import { VoreContainer, Container } from '../vore'
import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from './conditions'
import { ConsumeConsequence } from './consequences'

/**
* The PassAction has no effect.
@@ -90,17 +91,22 @@ export class AttackAction extends DamageAction {
/**
* Devours the target.
*/
export class DevourAction extends Action {
export class DevourAction extends CompositionAction {
constructor (protected container: Container) {
super(
new DynText(new LiveText(container, x => x.consumeVerb.capital), ' (', new LiveText(container, x => x.name.all), ')'),
new LiveText(container, x => `Try to ${x.consumeVerb} your foe`),
[new CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)],
[new StatVigorSizeTest(
Stat.Power,
-5,
(user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${container.consumeVerb} ${target.name.objective}.`)
)]
{
conditions: [new CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)],
tests: [new StatVigorSizeTest(
Stat.Power,
-5,
(user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${container.consumeVerb} ${target.name.objective}.`)
)],
consequences: [
new ConsumeConsequence(container)
]
}
)
}

@@ -197,11 +203,11 @@ export class StruggleAction extends Action {
)
}

export class DigestAction extends Action {
export class RubAction extends Action {
constructor (protected container: VoreContainer) {
super(
new DynText('Digest (', new LiveText(container, container => container.name.all), ')'),
'Digest your prey',
new DynText('Rub (', new LiveText(container, container => container.name.all), ')'),
'Digest your prey more quickly',
[new CapableCondition(), new SoloCondition()]
)
}


+ 158
- 0
src/game/combat/conditions.ts Целия файл

@@ -1,6 +1,9 @@
import { Condition, Vigor } from "../combat"
import { Creature } from "../creature"
import { Container } from '../vore'
import { LogEntry, LogLine, PropElem } from '../interface'
import { ToBe, Verb } from '../language'
import * as Words from '../words'

export class InverseCondition implements Condition {
constructor (private condition: Condition) {
@@ -10,12 +13,31 @@ export class InverseCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return !this.condition.allowed(user, target)
}

explain (user: Creature, target: Creature): LogEntry {
return new LogLine(
`The following must NOT hold: `,
this.condition.explain(user, target)
)
}
}

export class CapableCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return !user.disabled
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} not incapacitated`
)
} else {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} incapacitated`
)
}
}
}

export class UserDrainedVigorCondition implements Condition {
@@ -26,6 +48,14 @@ export class UserDrainedVigorCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.vigors[this.vigor] <= 0
}

explain (user: Creature, target: Creature): LogEntry {
return new LogLine(
`${user.name.capital} must have ${user.pronouns.possessive} `,
new PropElem(this.vigor),
` drained.`
)
}
}

export class TargetDrainedVigorCondition implements Condition {
@@ -36,24 +66,68 @@ export class TargetDrainedVigorCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return target.vigors[this.vigor] <= 0
}

explain (user: Creature, target: Creature): LogEntry {
return new LogLine(
`${target.name.capital} must have ${target.pronouns.possessive} `,
new PropElem(this.vigor),
` drained.`
)
}
}

export class SoloCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user === target
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} can't use this action on others.`
)
} else {
return new LogLine(
`${user.name.capital} can use this action on ${user.pronouns.reflexive}.`
)
}
}
}

export class PairCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user !== target
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} can use this action on others.`
)
} else {
return new LogLine(
`${user.name.capital} can't use this action on ${user.pronouns.reflexive}.`
)
}
}
}

export class TogetherCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.containedIn === target.containedIn && user !== target
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} together with ${target.name.objective}`
)
} else {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} not together with ${target.name.objective}`
)
}
}
}

export class HasRoomCondition implements Condition {
@@ -64,6 +138,18 @@ export class HasRoomCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return this.container.capacity >= this.container.fullness + target.voreStats.Bulk
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(Words.Have)} enough room in ${user.pronouns.possessive} ${this.container.name} to hold ${target.name.objective}`
)
} else {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(Words.Have)} enough room in ${user.pronouns.possessive} ${this.container.name} to hold ${target.name.objective}; ${user.name} only ${user.name.conjugate(Words.Have)} ${this.container.capacity - this.container.fullness}, but ${target.name.objective} ${target.name.conjugate(Words.Have)} a bulk of ${target.voreStats.Bulk}`
)
}
}
}

export class ContainedByCondition implements Condition {
@@ -74,6 +160,18 @@ export class ContainedByCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.containedIn === this.container && this.container.owner === target
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} contained in ${target.name.possessive} ${this.container.name}`
)
} else {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} not contained in ${target.name.possessive} ${this.container.name}`
)
}
}
}

export class ContainsCondition implements Condition {
@@ -84,18 +182,54 @@ export class ContainsCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return target.containedIn === this.container
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${target.name} ${target.name.conjugate(new ToBe())} contained within ${user.name.possessive} ${this.container.name}`
)
} else {
return new LogLine(
`${target.name} ${target.name.conjugate(new ToBe())} not contained within ${user.name.possessive} ${this.container.name}`
)
}
}
}

export class AllyCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.side === target.side
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${target.name.capital} is an ally of ${user.name.objective}`
)
} else {
return new LogLine(
`${target.name.capital} ${target.name.conjugate(new Verb("need"))} to be an ally of ${user.name.objective}`
)
}
}
}

export class EnemyCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.side !== target.side
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${target.name.capital} is an enemy of ${user.name.objective}`
)
} else {
return new LogLine(
`${target.name.capital} ${target.name.conjugate(new Verb("need"))} to be an enemy of ${user.name.objective}`
)
}
}
}

export class ContainerFullCondition implements Condition {
@@ -106,6 +240,18 @@ export class ContainerFullCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return this.container.contents.length > 0
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.possessive.capital} ${this.container.name} contains prey`
)
} else {
return new LogLine(
`${user.name.possessive.capital} ${this.container.name} is empty`
)
}
}
}

export class MassRatioCondition implements Condition {
@@ -116,4 +262,16 @@ export class MassRatioCondition implements Condition {
allowed (user: Creature, target: Creature): boolean {
return user.voreStats.Mass / target.voreStats.Mass > this.ratio
}

explain (user: Creature, target: Creature): LogEntry {
if (this.allowed(user, target)) {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} at least ${this.ratio} times as large as ${target.name.objective}`
)
} else {
return new LogLine(
`${user.name.capital} ${user.name.conjugate(new ToBe())} not at least ${this.ratio} times as large as ${target.name.objective}; ${user.name} ${user.name.conjugate(new ToBe())} only ${(user.voreStats.Mass / target.voreStats.Mass).toFixed(2)} times larger`
)
}
}
}

+ 14
- 0
src/game/combat/consequences.ts Целия файл

@@ -2,6 +2,7 @@ import { Consequence, DamageFormula, Condition, StatusEffect } from '../combat'
import { Creature } from '../creature'
import { LogEntry, LogLines, LogLine, nilLog } from '../interface'
import { Verb, PairLine } from '../language'
import { Container } from '../vore'

/**
* Takes a function, and thus can do anything.
@@ -99,3 +100,16 @@ export class StatusConsequence extends Consequence {
)
}
}

/**
* Consumes the target
*/
export class ConsumeConsequence extends Consequence {
constructor (public container: Container, conditions: Condition[] = []) {
super(conditions)
}

apply (user: Creature, target: Creature): LogEntry {
return this.container.consume(target)
}
}

+ 1
- 4
src/game/combat/tests.ts Целия файл

@@ -88,10 +88,7 @@ export class OpposedStatTest extends RandomTest {
` from ${target.name.possessive} stats.`
),
new LogLine(
`${user.name.capital.possessive} total score is ${this.getScore(user, this.userStats)}`
),
new LogLine(
`${target.name.capital.possessive} total score is ${this.getScore(target, this.targetStats)}`
`${user.name.capital}: ${this.getScore(user, this.userStats)} // ${this.getScore(target, this.targetStats)} :${target.name.capital}`
),
new LogLine(
`${user.name.capital} ${user.name.conjugate(new Verb("have", "has"))} a ${(this.odds(user, target) * 100).toFixed(0)}% chance of winning this test.`


+ 7
- 2
src/game/creature.ts Целия файл

@@ -29,6 +29,7 @@ export class Creature extends Mortal {
statusEffects: Array<StatusEffect> = [];
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 }, {});
otherActions: Array<Action> = [];
side: Side;
@@ -164,7 +165,7 @@ export class Creature extends Mortal {
return results
}

validActions (target: Creature): Array<Action> {
allActions (target: Creature): Array<Action> {
let choices = ([] as Action[]).concat(
this.actions,
this.containers.flatMap(container => container.actions),
@@ -178,7 +179,11 @@ export class Creature extends Mortal {
choices = choices.concat(this.containedIn.actions)
}

return choices.filter(action => {
return choices
}

validActions (target: Creature): Array<Action> {
return this.allActions(target).filter(action => {
return action.allowed(this, target)
})
}


+ 2
- 0
src/game/entity.ts Целия файл

@@ -50,7 +50,9 @@ export abstract class Mortal extends Entity {

constructor (name: Noun, kind: Noun, pronouns: Pronoun, public baseStats: Stats) {
super(name, kind, pronouns)
/* eslint-disable-next-line */
this.stats = Object.keys(Stat).reduce((base: any, key) => { base[key] = baseStats[key as Stat]; return base }, {})
/* eslint-disable-next-line */
this.baseResistances = Object.keys(DamageType).reduce((resist: any, key) => { resist[key] = 1; return resist }, {})
Object.entries(this.maxVigors).forEach(([key, val]) => {
this.vigors[key as Vigor] = val


+ 3
- 3
src/game/vore.ts Целия файл

@@ -2,7 +2,7 @@ import { Mortal } from './entity'
import { Damage, DamageType, Stats, Actionable, Action, Vigor, VoreStats, VisibleStatus, VoreStat, DamageInstance, DamageFormula } from './combat'
import { LogLines, LogEntry, LogLine, nilLog } from './interface'
import { Noun, Pronoun, ImproperNoun, TextLike, Verb, SecondPersonPronouns, PronounAsNoun, FirstPersonPronouns, PairLineArgs, SoloLine, POV, RandomWord } from './language'
import { DigestAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from './combat/actions'
import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from './combat/actions'
import * as Words from './words'
import { Creature } from './creature'

@@ -177,7 +177,7 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor

this.name = name

this.actions.push(new DigestAction(this))
this.actions.push(new RubAction(this))
}

get fullness (): number {
@@ -280,7 +280,7 @@ export abstract class InnerVoreContainer extends NormalVoreContainer {

this.actions = []

this.actions.push(new DigestAction(this))
this.actions.push(new RubAction(this))
this.actions.push(new StruggleAction(this))
}



+ 5
- 0
src/game/words.ts Целия файл

@@ -1,5 +1,10 @@
import { RandomWord, ImproperNoun, Adjective, Verb } from './language'

export const Have = new Verb(
"have",
"has"
)

export const SwallowSound = new RandomWord([
new ImproperNoun('gulp'),
new ImproperNoun('glurk'),


Loading…
Отказ
Запис