ソースを参照

Redo CombatTests to include a fail line

master
Fen Dweller 5年前
コミット
de40bf4a65
8個のファイルの変更112行の追加104行の削除
  1. +36
    -19
      src/game/combat.ts
  2. +43
    -45
      src/game/combat/actions.ts
  3. +19
    -15
      src/game/combat/tests.ts
  4. +1
    -1
      src/game/creature.ts
  5. +5
    -5
      src/game/creatures/geta.ts
  6. +5
    -11
      src/game/creatures/kenzie.ts
  7. +2
    -4
      src/game/creatures/shingo.ts
  8. +1
    -4
      src/game/creatures/withers.ts

+ 36
- 19
src/game/combat.ts ファイルの表示

@@ -91,6 +91,7 @@ export interface CombatTest {
test: (user: Creature, target: Creature) => boolean;
odds: (user: Creature, target: Creature) => number;
explain: (user: Creature, target: Creature) => LogEntry;
fail: (user: Creature, target: Creature) => LogEntry;
}

/**
@@ -347,9 +348,15 @@ export interface Combatant {
* An Action is anything that can be done by a [[Creature]] to a [[Creature]].
*/
export abstract class Action {
constructor (public name: TextLike, public desc: TextLike, public conditions: Array<Condition> = []) {
constructor (
public name: TextLike,
public desc: TextLike,
public conditions: Array<Condition> = [],
public tests: Array<CombatTest> = []
) {

}
// TODO explain the tests in here

allowed (user: Creature, target: Creature): boolean {
return this.conditions.every(cond => cond.allowed(user, target))
@@ -359,34 +366,44 @@ export abstract class Action {
return this.name.toString()
}

abstract execute (user: Creature, target: Creature): LogEntry
abstract describe (user: Creature, target: Creature): LogEntry
}
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)
}
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLines(
...this.tests.map(test => test.explain(user, target))
)
}

export type TestBundle = {
test: CombatTest;
fail: PairLine<Creature>;
abstract execute (user: Creature, target: Creature): LogEntry
}

export class CompositionAction extends Action {
private consequences: Array<Consequence>;
private tests: Array<TestBundle>;

constructor (name: TextLike, desc: TextLike, properties: { conditions?: Array<Condition>; consequences?: Array<Consequence>; tests?: Array<TestBundle> }) {
super(name, desc, properties.conditions ?? [])
constructor (
name: TextLike,
desc: TextLike,
properties: {
conditions?: Array<Condition>;
consequences?: Array<Consequence>;
tests?: Array<CombatTest>;
}
) {
super(name, desc, properties.conditions ?? [], properties.tests ?? [])
this.consequences = properties.consequences ?? []
this.tests = properties.tests ?? []
}

execute (user: Creature, target: Creature): LogEntry {
const failReason = this.tests.find(test => !test.test.test(user, target))
if (failReason !== undefined) {
return failReason.fail(user, target)
} else {
return new LogLines(
...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target))
)
}
return new LogLines(
...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target))
)
}

describe (user: Creature, target: Creature): LogEntry {


+ 43
- 45
src/game/combat/actions.ts ファイルの表示

@@ -2,7 +2,7 @@ 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 } from '../combat'
import { Damage, DamageFormula, Stat, Vigor, Action, Condition, CombatTest } 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'
@@ -28,35 +28,33 @@ export class PassAction extends Action {
* A generic action that causes damage.
*/
export abstract class DamageAction extends Action {
protected test: StatTest

abstract successLine: PairLineArgs<Creature, { damage: Damage }>
abstract failLine: PairLine<Creature>

constructor (name: TextLike, desc: TextLike, protected damage: DamageFormula, conditions: Condition[] = []) {
constructor (name: TextLike, desc: TextLike, protected damage: DamageFormula, tests: CombatTest[], conditions: Condition[] = []) {
super(
name,
desc,
[new CapableCondition(), new EnemyCondition()].concat(conditions)
[new CapableCondition(), new EnemyCondition()].concat(conditions),
tests
)
this.test = new StatTest(Stat.Power, 10)
}

execute (user: Creature, target: Creature): LogEntry {
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))
}
if (this.test.test(user, target)) {
const damage = this.damage.calc(user, target)
const targetResult = target.takeDamage(damage)
const ownResult = this.successLine(user, target, { damage: damage })
return new LogLines(ownResult, targetResult)
} else {
return this.failLine(user, target)
return super.try(user, target)
}
}

execute (user: Creature, target: Creature): LogEntry {
const damage = this.damage.calc(user, target)
const targetResult = target.takeDamage(damage)
const ownResult = this.successLine(user, target, { damage: damage })
return new LogLines(ownResult, targetResult)
}
}

/**
@@ -64,11 +62,23 @@ export abstract class DamageAction extends Action {
*/
export class AttackAction extends DamageAction {
constructor (damage: DamageFormula, protected verb: Verb = new Verb('smack')) {
super(verb.root.capital, 'Attack the enemy', damage, [new TogetherCondition()])
super(
verb.root.capital,
'Attack the enemy',
damage,
[new StatTest(
Stat.Power,
10,
(user, target) => new LogLine(
`${user.name.capital} ${user.name.conjugate(new Verb('miss', 'misses'))} ${target.name.objective}!`
)
)],
[new TogetherCondition()]
)
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine(`Attack ${target.name}. `, this.damage.describe(user, target), '. ', this.test.explain(user, target))
return new LogLine(`Attack ${target.name}. `, this.damage.describe(user, target), '. ', super.describe(user, target))
}

successLine: PairLineArgs<Creature, { damage: Damage }> = (user, target, args) => new LogLine(
@@ -85,15 +95,17 @@ export class AttackAction extends DamageAction {
* Devours the target.
*/
export class DevourAction extends Action {
private test: StatVigorSizeTest

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 CapableCondition(), new TogetherCondition(), new HasRoomCondition(container)],
[new StatVigorSizeTest(
Stat.Power,
10,
(user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${container.consumeVerb} ${target.name.objective}.`)
)]
)
this.test = new StatVigorSizeTest(Stat.Power)
}

allowed (user: Creature, target: Creature): boolean {
@@ -109,20 +121,12 @@ export class DevourAction extends Action {
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine(`Try to ${this.container.consumeVerb} your opponent, sending them to your ${this.container.name}. `, this.test.explain(user, target))
return new LogLine(`Try to ${this.container.consumeVerb} your opponent, sending them to your ${this.container.name}. `, super.describe(user, target))
}

execute (user: Creature, target: Creature): LogEntry {
if (this.test.test(user, target)) {
return this.container.consume(target)
} else {
return this.failLine(user, target, { container: this.container })
}
return this.container.consume(target)
}

protected failLine: PairLineArgs<Entity, { container: Container }> = (user, target, args) => new LogLine(
`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to ${args.container.consumeVerb} ${target.name.objective}.`
)
}

/**
@@ -171,40 +175,34 @@ export class FeedAction extends Action {
* Tries to escape from the target's container
*/
export class StruggleAction extends Action {
private test: StatVigorSizeTest

constructor (public container: Container) {
super(
new DynText('Struggle (', new LiveText(container, x => x.name.all), ')'),
'Try to escape from your foe',
[new CapableCondition(), new PairCondition(), new ContainedByCondition(container)]
[new CapableCondition(), new PairCondition(), new ContainedByCondition(container)],
[new StatVigorSizeTest(
Stat.Power,
0,
(user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to escape from ${target.name.possessive} ${container.name}.`)
)]
)
this.test = new StatVigorSizeTest(Stat.Power)
}

execute (user: Creature, target: Creature): LogEntry {
if (user.containedIn !== null) {
if (this.test.test(user, target)) {
return new LogLines(this.successLine(user, target, { container: this.container }), user.containedIn.release(user))
} else {
return this.failLine(user, target, { container: this.container })
}
return new LogLines(this.successLine(user, target, { container: this.container }), user.containedIn.release(user))
} else {
return new LogLine("Vore's bugged!")
}
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine(`Try to escape from ${target.name}'s ${this.container.name}. `, this.test.explain(user, target))
return new LogLine(`Try to escape from ${target.name}'s ${this.container.name}. `, super.describe(user, target))
}

protected successLine: PairLineArgs<Entity, { container: Container }> = (prey, pred, args) => new LogLine(
`${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))} from ${pred.name.possessive} ${args.container.name}.`
)

protected failLine: PairLineArgs<Entity, { container: Container }> = (prey, pred, args) => new LogLine(
`${prey.name.capital} ${prey.name.conjugate(new Verb('fail'))} to escape from ${pred.name.possessive} ${args.container.name}.`
)
}

export class DigestAction extends Action {


+ 19
- 15
src/game/combat/tests.ts ファイルの表示

@@ -1,6 +1,6 @@
import { CombatTest, Stat, Vigor } from '../combat'
import { Creature } from "../creature"
import { LogEntry, LogLines, PropElem, LogLine } from '../interface'
import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface'

function logistic (x0: number, L: number, k: number): (x: number) => number {
return (x: number) => {
@@ -11,6 +11,10 @@ function logistic (x0: number, L: number, k: number): (x: number) => number {
// TODO this will need to be able to return a LogEntry at some point

abstract class RandomTest implements CombatTest {
constructor (public fail: (user: Creature, target: Creature) => LogEntry) {

}

test (user: Creature, target: Creature): boolean {
const userFail = user.effects.map(effect => effect.failTest(user, target))
if (userFail.some(result => result.failed)) {
@@ -32,8 +36,8 @@ export class StatVigorSizeTest extends RandomTest {
private f: (x: number) => number
private k = 0.1

constructor (public readonly stat: Stat, private bias = 0) {
super()
constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
super(fail)
this.f = logistic(0, 1, this.k)
}

@@ -57,10 +61,9 @@ export class StatVigorSizeTest extends RandomTest {
userPercent *= 4
}

userPercent *= Math.sqrt(user.voreStats.Mass)
targetPercent *= Math.sqrt(target.voreStats.Mass)
const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)

return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
return this.f(this.bias + sizeOffset * 5 + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
}

explain (user: Creature, target: Creature): LogEntry {
@@ -84,11 +87,12 @@ export class StatVigorSizeTest extends RandomTest {
if (targetPercent === 0) {
userPercent *= 4
}
userPercent *= Math.sqrt(user.voreStats.Mass)
targetPercent *= Math.sqrt(target.voreStats.Mass)

const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)

const userMod = user.stats[this.stat] * userPercent
const targetMod = target.stats[this.stat] * targetPercent
const delta = userMod - targetMod
const delta = userMod - targetMod + sizeOffset * 5

if (delta === 0) {
result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
@@ -108,8 +112,8 @@ export class StatVigorTest extends RandomTest {
private f: (x: number) => number
private k = 0.1

constructor (public readonly stat: Stat, private bias = 0) {
super()
constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
super(fail)
this.f = logistic(0, 1, this.k)
}

@@ -179,8 +183,8 @@ export class StatTest extends RandomTest {
private f: (x: number) => number
private k = 0.1

constructor (public readonly stat: Stat, private bias = 0) {
super()
constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
super(fail)
this.f = logistic(0, 1, this.k)
}

@@ -207,8 +211,8 @@ export class StatTest extends RandomTest {
}

export class ChanceTest extends RandomTest {
constructor (public readonly chance: number) {
super()
constructor (public readonly chance: number, fail: (user: Creature, target: Creature) => LogEntry) {
super(fail)
}

odds (user: Creature, target: Creature): number {


+ 1
- 1
src/game/creature.ts ファイルの表示

@@ -86,7 +86,7 @@ export class Creature extends Vore implements Combatant {
if (blocking.length > 0) {
return new LogLines(...blocking.map(result => result.log))
} else {
return action.execute(this, target)
return action.try(this, target)
}
}



+ 5
- 5
src/game/creatures/geta.ts ファイルの表示

@@ -61,12 +61,12 @@ export class Geta extends Creature {
new ContainsCondition(cock)
],
tests: [
{
test: new ChanceTest(0.5),
fail: (user, target) => new LogLine(
`${user.name.capital.possessive} cock clenches hard around ${target.name.objective}, but ${target.pronouns.subjective} ${target.name.conjugate(new Verb("avoid"))} being crushed.`
new ChanceTest(
0.5,
(user, target) => new LogLine(
`${user.name.capital.possessive} cock clenches hard around ${target.name.objective}, but ${target.pronouns.subjective} ${target.name.conjugate(new Verb("avoid"))} being crushed.`
)
}
)
],
consequences: [
new LogConsequence(


+ 5
- 11
src/game/creatures/kenzie.ts ファイルの表示

@@ -4,7 +4,6 @@ import { VoreType, Stomach } from '../vore'
import { Side, Damage, DamageType, Vigor, StatDamageFormula, Stat, VoreStat, DamageFormula } from '../combat'
import { AttackAction, DevourAction } from '../combat/actions'
import { LogEntry, LogLines } from '../interface'
import { StatTest } from '../combat/tests'
import { StunEffect, PredatorCounterEffect } from '../combat/effects'

class StompAttack extends AttackAction {
@@ -13,19 +12,14 @@ class StompAttack extends AttackAction {
damage,
verb
)
this.test = new StatTest(Stat.Power)
}

execute (user: Creature, target: Creature): LogEntry {
if (this.test.test(user, target)) {
const damage = this.damage.calc(user, target)
const targetResult = target.takeDamage(damage)
const ownResult = this.successLine(user, target, { damage: damage })
const effResult = target.applyEffect(new StunEffect(3))
return new LogLines(ownResult, targetResult, effResult)
} else {
return this.failLine(user, target)
}
const damage = this.damage.calc(user, target)
const targetResult = target.takeDamage(damage)
const ownResult = this.successLine(user, target, { damage: damage })
const effResult = target.applyEffect(new StunEffect(3))
return new LogLines(ownResult, targetResult, effResult)
}
}
export class Kenzie extends Creature {


+ 2
- 4
src/game/creatures/shingo.ts ファイルの表示

@@ -8,20 +8,18 @@ import { ContainsCondition, CapableCondition, EnemyCondition, TargetDrainedVigor
import { StatTest } from '../combat/tests'

export class TrappedAction extends DamageAction {
protected test: StatTest

constructor (name: TextLike, desc: TextLike, protected verb: Verb, protected damage: DamageFormula, container: Container, conditions: Condition[] = []) {
super(
name,
desc,
damage,
[],
[new CapableCondition(), new ContainsCondition(container), new EnemyCondition()].concat(conditions)
)
this.test = new StatTest(Stat.Power)
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine(`Chew on ${target.name}. `, this.damage.describe(user, target), '. ', this.test.explain(user, target))
return new LogLine(`Chew on ${target.name}. `, this.damage.describe(user, target), '. ')
}

successLine: PairLineArgs<Creature, { damage: Damage }> = (user, target, args) => new LogLine(


+ 1
- 4
src/game/creatures/withers.ts ファイルの表示

@@ -262,15 +262,12 @@ class StompAllyAction extends Action {
}

class DevourAllAction extends GroupAction {
private test: CombatTest

constructor (private container: VoreContainer) {
super('Devour All', 'GULP!', [
new TogetherCondition(),
new EnemyCondition(),
new CapableCondition()
])
this.test = new StatVigorTest(Stat.Power)
}

line = (user: Creature, target: Creature) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('scoop'))} ${target.name} up!`)
@@ -287,7 +284,7 @@ class DevourAllAction extends GroupAction {
}

executeGroup (user: Creature, targets: Array<Creature>): LogEntry {
return new LogLines(...targets.filter(target => this.test.test(user, target)).map(target => this.execute(user, target)).concat(
return new LogLines(...targets.map(target => this.execute(user, target)).concat(
[
new Newline(),
this.groupLine(user, { count: targets.length })


読み込み中…
キャンセル
保存