Переглянути джерело

Redesign the vore system

Everything is now a Container; Containers have a set
of capabilities that describes what they can do
master
Fen Dweller 3 роки тому
джерело
коміт
d96b836724
16 змінених файлів з 943 додано та 870 видалено
  1. +6
    -5
      src/components/ContainerView.vue
  2. +0
    -4
      src/components/ItemView.vue
  3. +0
    -3
      src/components/WalletView.vue
  4. +20
    -1
      src/game/ai.ts
  5. +303
    -152
      src/game/combat.ts
  6. +50
    -8
      src/game/combat/actions.ts
  7. +137
    -42
      src/game/combat/effects.ts
  8. +5
    -11
      src/game/creature.ts
  9. +0
    -97
      src/game/creatures/characters/inazuma.ts
  10. +0
    -39
      src/game/creatures/monsters/slime.ts
  11. +20
    -4
      src/game/creatures/monsters/werewolf.ts
  12. +11
    -3
      src/game/creatures/player.ts
  13. +2
    -2
      src/game/events.ts
  14. +187
    -89
      src/game/language.ts
  15. +0
    -41
      src/game/maps/town.ts
  16. +202
    -369
      src/game/vore.ts

+ 6
- 5
src/components/ContainerView.vue Переглянути файл

@@ -15,7 +15,7 @@ import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
import { Creature } from '@/game/creature'
import { POV } from '@/game/language'
import { Stats, Stat } from '@/game/combat'
import { Container, VoreContainer } from '@/game/vore'
import { Container } from '@/game/vore'

function wiggle (contents: HTMLElement) {
setTimeout(() => wiggle(contents), 3000)
@@ -36,12 +36,13 @@ function wiggle (contents: HTMLElement) {

// yoinked from https://jsfiddle.net/yckart/0adfw47y/

function draw (delta: number, dt: number, total: number, parent: HTMLElement, canvas: HTMLCanvasElement, container: VoreContainer, smoothedFraction: number, smoothedLiveliness: number) {
function draw (delta: number, dt: number, total: number, parent: HTMLElement, canvas: HTMLCanvasElement, container: Container, smoothedFraction: number, smoothedLiveliness: number) {
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D

canvas.width = parent.clientWidth
canvas.height = parent.clientHeight
ctx.fillStyle = container.fluidColor
// TODO: put this back on the container
ctx.fillStyle = "#00ff00"
const fraction = container.fullness / container.capacity
const livingFraction = container.contents.reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) / container.capacity
const deadFraction = container.digested.reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) / container.capacity
@@ -74,7 +75,7 @@ function draw (delta: number, dt: number, total: number, parent: HTMLElement, ca
@Component
export default class ContainerView extends Vue {
@Prop({ required: true })
container!: VoreContainer
container!: Container

mounted () {
const canvas = this.$el.querySelector('.container-waves') as HTMLCanvasElement
@@ -82,7 +83,7 @@ export default class ContainerView extends Vue {
canvas.width = (this.$el as HTMLElement).clientWidth
canvas.height = (this.$el as HTMLElement).clientHeight
canvas.width = canvas.width + 0
requestAnimationFrame((delta: number) => draw(delta, delta, Math.random() * 1000, this.$el as HTMLElement, canvas, (this.container as VoreContainer), 0, 0))
requestAnimationFrame((delta: number) => draw(delta, delta, Math.random() * 1000, this.$el as HTMLElement, canvas, (this.container as Container), 0, 0))

wiggle(this.$el.querySelector(".container-contents") as HTMLElement)
}


+ 0
- 4
src/components/ItemView.vue Переглянути файл

@@ -12,10 +12,6 @@

<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
import { Creature } from '@/game/creature'
import { POV } from '@/game/language'
import { Stats, Stat } from '@/game/combat'
import { Container, VoreContainer } from '@/game/vore'
import { Item, ItemKindIcons, ItemKind } from '@/game/items'

@Component({


+ 0
- 3
src/components/WalletView.vue Переглянути файл

@@ -7,9 +7,6 @@
<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator'
import { Creature } from '@/game/creature'
import { POV } from '@/game/language'
import { Stats, Stat } from '@/game/combat'
import { Container, VoreContainer } from '@/game/vore'
import { Item, ItemKindIcons, ItemKind, Currency, CurrencyData } from '@/game/items'

@Component({


+ 20
- 1
src/game/ai.ts Переглянути файл

@@ -1,11 +1,12 @@
import { Creature } from '@/game/creature'
import { Encounter, Action, CompositionAction, Consequence } from '@/game/combat'
import { LogEntry, LogLine, nilLog } from '@/game/interface'
import { PassAction, ReleaseAction, RubAction } from '@/game/combat/actions'
import { PassAction, ReleaseAction, RubAction, TransferAction } from '@/game/combat/actions'
import { VoreRelay } from '@/game/events'
import { StatusConsequence } from '@/game/combat/consequences'
import { SurrenderEffect } from '@/game/combat/effects'
import { ToBe, Verb } from '@/game/language'
import { ConnectionDirection } from './vore'

/**
* A Decider determines how favorable an action is to perform.
@@ -96,6 +97,23 @@ export class NoReleaseDecider extends Decider {
}
}

/**
* Only transfers prey deeper
*/
export class OnlyDeeperDecider extends Decider {
decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number {
if (action instanceof TransferAction) {
if ((action as TransferAction).to.direction === ConnectionDirection.Shallower) {
return 0
} else {
return 1
}
} else {
return 1
}
}
}

/**
* Weights actions based on how likely they are to succeed
*/
@@ -224,6 +242,7 @@ export class VoreAI extends AI {
super(
[
new NoReleaseDecider(),
new OnlyDeeperDecider(),
new NoSurrenderDecider(),
new NoPassDecider(),
new ChanceDecider(),


+ 303
- 152
src/game/combat.ts Переглянути файл

@@ -1,10 +1,20 @@
import { Creature } from "./creature"
import { TextLike } from '@/game/language'
import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog, Newline } from '@/game/interface'
import { World } from '@/game/world'
import { TestCategory } from '@/game/combat/tests'
import { VoreContainer } from '@/game/vore'
import { SoloTargeter } from '@/game/combat/targeters'
import { TextLike } from "@/game/language"
import {
LogEntry,
LogLines,
FAElem,
LogLine,
FormatEntry,
FormatOpt,
PropElem,
nilLog,
Newline
} from "@/game/interface"
import { World } from "@/game/world"
import { TestCategory } from "@/game/combat/tests"
import { Container } from "@/game/vore"
import { SoloTargeter } from "@/game/combat/targeters"

export enum DamageType {
Pierce = "Pierce",
@@ -29,19 +39,19 @@ export enum Vigor {
Resolve = "Resolve"
}

export const VigorIcons: {[key in Vigor]: string} = {
export const VigorIcons: { [key in Vigor]: string } = {
Health: "fas fa-heart",
Stamina: "fas fa-bolt",
Resolve: "fas fa-brain"
}

export const VigorDescs: {[key in Vigor]: string} = {
export const VigorDescs: { [key in Vigor]: string } = {
Health: "How much damage you can take",
Stamina: "How much energy you have",
Resolve: "How much dominance you can resist"
}

export type Vigors = {[key in Vigor]: number}
export type Vigors = { [key in Vigor]: number }

export enum Stat {
Toughness = "Toughness",
@@ -52,9 +62,9 @@ export enum Stat {
Charm = "Charm"
}

export type Stats = {[key in Stat]: number}
export type Stats = { [key in Stat]: number }

export const StatToVigor: {[key in Stat]: Vigor} = {
export const StatToVigor: { [key in Stat]: Vigor } = {
Toughness: Vigor.Health,
Power: Vigor.Health,
Reflexes: Vigor.Stamina,
@@ -63,22 +73,22 @@ export const StatToVigor: {[key in Stat]: Vigor} = {
Charm: Vigor.Resolve
}

export const StatIcons: {[key in Stat]: string} = {
Toughness: 'fas fa-heartbeat',
Power: 'fas fa-fist-raised',
Reflexes: 'fas fa-stopwatch',
Agility: 'fas fa-feather',
Willpower: 'fas fa-book',
Charm: 'fas fa-comments'
export const StatIcons: { [key in Stat]: string } = {
Toughness: "fas fa-heartbeat",
Power: "fas fa-fist-raised",
Reflexes: "fas fa-stopwatch",
Agility: "fas fa-feather",
Willpower: "fas fa-book",
Charm: "fas fa-comments"
}

export const StatDescs: {[key in Stat]: string} = {
Toughness: 'Your brute resistance',
Power: 'Your brute power',
Reflexes: 'Your ability to dodge',
Agility: 'Your ability to move quickly',
Willpower: 'Your mental resistance',
Charm: 'Your mental power'
export const StatDescs: { [key in Stat]: string } = {
Toughness: "Your brute resistance",
Power: "Your brute power",
Reflexes: "Your ability to dodge",
Agility: "Your ability to move quickly",
Willpower: "Your mental resistance",
Charm: "Your mental power"
}

export enum VoreStat {
@@ -87,15 +97,15 @@ export enum VoreStat {
Prey = "Prey"
}

export type VoreStats = {[key in VoreStat]: number}
export type VoreStats = { [key in VoreStat]: number }

export const VoreStatIcons: {[key in VoreStat]: string} = {
export const VoreStatIcons: { [key in VoreStat]: string } = {
[VoreStat.Mass]: "fas fa-weight",
[VoreStat.Bulk]: "fas fa-weight-hanging",
[VoreStat.Prey]: "fas fa-utensils"
}

export const VoreStatDescs: {[key in VoreStat]: string} = {
export const VoreStatDescs: { [key in VoreStat]: string } = {
[VoreStat.Mass]: "How much you weigh",
[VoreStat.Bulk]: "Your weight, plus the weight of your prey",
[VoreStat.Prey]: "How many creatures you've got inside of you"
@@ -109,7 +119,7 @@ export interface CombatTest {
}

export interface Targeter {
targets (primary: Creature, encounter: Encounter): Array<Creature>;
targets(primary: Creature, encounter: Encounter): Array<Creature>;
}

/**
@@ -142,28 +152,46 @@ export class Damage {
}

toString (): string {
return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
return this.damages
.map(damage => damage.amount + " " + damage.type)
.join("/")
}

render (): LogEntry {
return new LogLine(...this.damages.flatMap(instance => {
if (instance.target in Vigor) {
return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type]
} else if (instance.target in Stat) {
return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type]
} else {
// this should never happen!
return []
}
}))
return new LogLine(
...this.damages.flatMap(instance => {
if (instance.target in Vigor) {
return [
instance.amount.toString(),
new FAElem(VigorIcons[instance.target as Vigor]),
" " + instance.type
]
} else if (instance.target in Stat) {
return [
instance.amount.toString(),
new FAElem(StatIcons[instance.target as Stat]),
" " + instance.type
]
} else {
// this should never happen!
return []
}
})
)
}

// 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 }, {})
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 }, {})
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
if (instance.target in Vigor) {
@@ -173,16 +201,31 @@ export class Damage {
}
})

const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' '])
const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' '])
return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst)
const vigorEntries = Object.keys(Vigor).flatMap(key =>
vigorTotals[key as Vigor] === 0
? []
: [new PropElem(key as Vigor, vigorTotals[key as Vigor]), " "])
const statEntries = Object.keys(Stat).flatMap(key =>
statTotals[key as Stat] === 0
? []
: [new PropElem(key as Stat, statTotals[key as Stat]), " "])
return new FormatEntry(
new LogLine(...vigorEntries.concat(statEntries)),
FormatOpt.DamageInst
)
}

nonzero (): boolean {
/* eslint-disable-next-line */
const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
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 }, {})
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
if (instance.target in Vigor) {
@@ -191,7 +234,10 @@ export class Damage {
statTotals[instance.target as Stat] += factor * instance.amount
}
})
return Object.values(vigorTotals).some(v => v !== 0) || Object.values(statTotals).some(v => v !== 0)
return (
Object.values(vigorTotals).some(v => v !== 0) ||
Object.values(statTotals).some(v => v !== 0)
)
}
}

@@ -199,22 +245,26 @@ export class Damage {
* Computes damage given the source and target of the damage.
*/
export interface DamageFormula {
calc (user: Creature, target: Creature): Damage;
describe (user: Creature, target: Creature): LogEntry;
explain (user: Creature): LogEntry;
calc(user: Creature, target: Creature): Damage;
describe(user: Creature, target: Creature): LogEntry;
explain(user: Creature): LogEntry;
}

export class CompositeDamageFormula implements DamageFormula {
constructor (private formulas: DamageFormula[]) {

}
constructor (private formulas: DamageFormula[]) {}

calc (user: Creature, target: Creature): Damage {
return this.formulas.reduce((total: Damage, next: DamageFormula) => total.combine(next.calc(user, target)), new Damage())
return this.formulas.reduce(
(total: Damage, next: DamageFormula) =>
total.combine(next.calc(user, target)),
new Damage()
)
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLines(...this.formulas.map(formula => formula.describe(user, target)))
return new LogLines(
...this.formulas.map(formula => formula.describe(user, target))
)
}

explain (user: Creature): LogEntry {
@@ -226,9 +276,7 @@ export class CompositeDamageFormula implements DamageFormula {
* Simply returns the damage it was given.
*/
export class ConstantDamageFormula implements DamageFormula {
constructor (private damage: Damage) {

}
constructor (private damage: Damage) {}

calc (user: Creature, target: Creature): Damage {
return this.damage
@@ -239,7 +287,7 @@ export class ConstantDamageFormula implements DamageFormula {
}

explain (user: Creature): LogEntry {
return new LogLine('Deal ', this.damage.renderShort())
return new LogLine("Deal ", this.damage.renderShort())
}
}

@@ -247,12 +295,12 @@ export class ConstantDamageFormula implements DamageFormula {
* Randomly scales the damage it was given with a factor of (1-x) to (1+x)
*/
export class UniformRandomDamageFormula implements DamageFormula {
constructor (private damage: Damage, private variance: number) {

}
constructor (private damage: Damage, private variance: number) {}

calc (user: Creature, target: Creature): Damage {
return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1)
return this.damage.scale(
Math.random() * this.variance * 2 - this.variance + 1
)
}

describe (user: Creature, target: Creature): LogEntry {
@@ -260,7 +308,13 @@ export class UniformRandomDamageFormula implements DamageFormula {
}

explain (user: Creature): LogEntry {
return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), '.')
return new LogLine(
"Deal between ",
this.damage.scale(1 - this.variance).renderShort(),
" and ",
this.damage.scale(1 + this.variance).renderShort(),
"."
)
}
}

@@ -268,9 +322,14 @@ export class UniformRandomDamageFormula implements DamageFormula {
* A [[DamageFormula]] that uses the attacker's stats
*/
export class StatDamageFormula implements DamageFormula {
constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) {

}
constructor (
private factors: Array<{
stat: Stat | VoreStat;
fraction: number;
type: DamageType;
target: Vigor | Stat;
}>
) {}

calc (user: Creature, target: Creature): Damage {
const instances: Array<DamageInstance> = this.factors.map(factor => {
@@ -310,12 +369,17 @@ export class StatDamageFormula implements DamageFormula {
explain (user: Creature): LogEntry {
return new LogLine(
`Deal `,
...this.factors.map(factor => new LogLine(
`${factor.fraction * 100}% of your `,
new PropElem(factor.stat),
` as `,
new PropElem(factor.target)
)).joinGeneral(new LogLine(`, `), new LogLine(` and `))
...this.factors
.map(
factor =>
new LogLine(
`${factor.fraction * 100}% of your `,
new PropElem(factor.stat),
` as `,
new PropElem(factor.target)
)
)
.joinGeneral(new LogLine(`, `), new LogLine(` and `))
)
}
}
@@ -324,21 +388,30 @@ export class StatDamageFormula implements DamageFormula {
* Deals a percentage of the target's current vigors/stats
*/
export class FractionDamageFormula implements DamageFormula {
constructor (private factors: Array<{ fraction: number; target: Vigor|Stat; type: DamageType }>) {

}
constructor (
private factors: Array<{
fraction: number;
target: Vigor | Stat;
type: DamageType;
}>
) {}

calc (user: Creature, target: Creature): Damage {
const instances: Array<DamageInstance> = this.factors.map(factor => {
if (factor.target in Stat) {
return {
amount: Math.max(0, factor.fraction * target.stats[factor.target as Stat]),
amount: Math.max(
0,
factor.fraction * target.stats[factor.target as Stat]
),
target: factor.target,
type: factor.type
}
} else if (factor.target in Vigor) {
return {
amount: Math.max(factor.fraction * target.vigors[factor.target as Vigor]),
amount: Math.max(
factor.fraction * target.vigors[factor.target as Vigor]
),
target: factor.target,
type: factor.type
}
@@ -362,10 +435,15 @@ export class FractionDamageFormula implements DamageFormula {
explain (user: Creature): LogEntry {
return new LogLine(
`Deal damage equal to `,
...this.factors.map(factor => new LogLine(
`${factor.fraction * 100}% of your target's `,
new PropElem(factor.target)
)).joinGeneral(new LogLine(`, `), new LogLine(` and `))
...this.factors
.map(
factor =>
new LogLine(
`${factor.fraction * 100}% of your target's `,
new PropElem(factor.target)
)
)
.joinGeneral(new LogLine(`, `), new LogLine(` and `))
)
}
}
@@ -379,8 +457,8 @@ export enum Side {
* A Combatant has a list of possible actions to take, as well as a side.
*/
export interface Combatant {
actions: Array<Action>;
side: Side;
actions: Array<Action>;
side: Side;
}

/**
@@ -392,9 +470,7 @@ export abstract class Action {
public desc: TextLike,
public conditions: Array<Condition> = [],
public tests: Array<CombatTest> = []
) {

}
) {}

allowed (user: Creature, target: Creature): boolean {
return this.conditions.every(cond => cond.allowed(user, target))
@@ -408,21 +484,36 @@ export abstract class Action {
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) }
return {
failed: true,
target: target,
log: failReason.fail(user, target)
}
} else {
return { failed: false, target: target, log: this.execute(user, target) }
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))
this.executeAll(
user,
results.filter(result => !result.failed).map(result => result.target)
)
)
}

describe (user: Creature, target: Creature, verbose = true): LogEntry {
return new LogLines(
...(verbose ? this.conditions.map(condition => condition.explain(user, target)).concat([new Newline()]) : []),
...(verbose
? this.conditions
.map(condition => condition.explain(user, target))
.concat([new Newline()])
: []),
new LogLine(
`Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%`
),
@@ -432,7 +523,10 @@ export abstract class Action {
}

odds (user: Creature, target: Creature): number {
return this.tests.reduce((total, test) => total * test.odds(user, target), 1)
return this.tests.reduce(
(total, test) => total * test.odds(user, target),
1
)
}

targets (primary: Creature, encounter: Encounter): Array<Creature> {
@@ -443,13 +537,13 @@ export abstract class Action {
return nilLog
}

abstract execute (user: Creature, target: Creature): LogEntry
abstract execute(user: Creature, target: Creature): LogEntry
}

export class CompositionAction extends Action {
public consequences: Array<Consequence>;
public groupConsequences: Array<GroupConsequence>;
public targeters: Array<Targeter>;
public consequences: Array<Consequence>
public groupConsequences: Array<GroupConsequence>
public targeters: Array<Targeter>

constructor (
name: TextLike,
@@ -470,27 +564,34 @@ export class CompositionAction extends Action {

execute (user: Creature, target: Creature): LogEntry {
return new LogLines(
...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target))
...this.consequences
.filter(consequence => consequence.applicable(user, target))
.map(consequence => consequence.apply(user, target))
)
}

executeAll (user: Creature, targets: Array<Creature>): LogEntry {
return new LogLines(
...this.groupConsequences.map(consequence => consequence.apply(user, targets.filter(target => consequence.applicable(user, target))))
...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(
new Newline(),
super.describe(user, target)
)
...this.consequences
.map(consequence => consequence.describe(user, target))
.concat(new Newline(), super.describe(user, target))
)
}

targets (primary: Creature, encounter: Encounter) {
return this.targeters.flatMap(targeter => targeter.targets(primary, encounter)).unique()
return this.targeters
.flatMap(targeter => targeter.targets(primary, encounter))
.unique()
}
}

@@ -515,12 +616,16 @@ export class Effective {
/**
* Executes when the effect is initially applied
*/
onApply (creature: Creature): LogEntry { return nilLog }
onApply (creature: Creature): LogEntry {
return nilLog
}

/**
* Executes when the effect is removed
*/
onRemove (creature: Creature): LogEntry { return nilLog }
onRemove (creature: Creature): LogEntry {
return nilLog
}

/**
* Executes before the creature tries to perform an action
@@ -535,7 +640,10 @@ export class Effective {
/**
* Executes before another creature tries to perform an action that targets this creature
*/
preReceiveAction (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
preReceiveAction (
creature: Creature,
attacker: Creature
): { prevented: boolean; log: LogEntry } {
return {
prevented: false,
log: nilLog
@@ -552,7 +660,10 @@ export class Effective {
/**
* Executes before the creature is attacked
*/
preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } {
preAttack (
creature: Creature,
attacker: Creature
): { prevented: boolean; log: LogEntry } {
return {
prevented: false,
log: nilLog
@@ -580,7 +691,10 @@ export class Effective {
* Called when a test is about to resolve. Decides if the creature should automatically fail.
*/

failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } {
failTest (
creature: Creature,
opponent: Creature
): { failed: boolean; log: LogEntry } {
return {
failed: false,
log: nilLog
@@ -597,28 +711,45 @@ export class Effective {
/**
* Additively modifies a creature's score for an offensive test
*/
modTestOffense (attacker: Creature, defender: Creature, kind: TestCategory): number {
modTestOffense (
attacker: Creature,
defender: Creature,
kind: TestCategory
): number {
return 0
}

/**
* Additively modifies a creature's score for a defensive test
*/
modTestDefense (defender: Creature, attacker: Creature, kind: TestCategory): number {
modTestDefense (
defender: Creature,
attacker: Creature,
kind: TestCategory
): number {
return 0
}

/**
* Affects digestion damage
*/
modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage {
modDigestionDamage (
predator: Creature,
prey: Creature,
container: Container,
damage: Damage
): Damage {
return damage
}

/**
* Triggers after consumption
*/
postConsume (predator: Creature, prey: Creature, container: VoreContainer): LogEntry {
postConsume (
predator: Creature,
prey: Creature,
container: Container
): LogEntry {
return nilLog
}

@@ -653,24 +784,35 @@ export interface VisibleStatus {
* a status indicating that it is dead, but entities cannot be "given" the dead effect
*/
export class ImplicitStatus implements VisibleStatus {
topLeft = ''
bottomRight = ''
topLeft = ""
bottomRight = ""

constructor (public name: TextLike, public desc: TextLike, public icon: string) {

}
constructor (
public name: TextLike,
public desc: TextLike,
public icon: string
) {}
}

/**
* This kind of status is explicitly given to a creature.
*/
export abstract class StatusEffect extends Effective implements VisibleStatus {
constructor (public name: TextLike, public desc: TextLike, public icon: string) {
constructor (
public name: TextLike,
public desc: TextLike,
public icon: string
) {
super()
}

get topLeft () { return '' }
get bottomRight () { return '' }
get topLeft () {
return ""
}

get bottomRight () {
return ""
}
}

export type EncounterDesc = {
@@ -702,7 +844,9 @@ export class Encounter {
this.combatants.forEach(combatant => {
// this should never be undefined
const currentProgress = this.initiatives.get(combatant) ?? 0
const remaining = (this.turnTime - currentProgress) / Math.sqrt(Math.max(combatant.stats.Agility, 1))
const remaining =
(this.turnTime - currentProgress) /
Math.sqrt(Math.max(combatant.stats.Agility, 1))
times.set(combatant, remaining)
})

@@ -712,12 +856,18 @@ export class Encounter {

return closestTime <= nextTime ? closest : next
}, this.combatants[0])
const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.sqrt(Math.max(this.currentMove.stats.Agility, 1))
const closestRemaining =
(this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) /
Math.sqrt(Math.max(this.currentMove.stats.Agility, 1))

this.combatants.forEach(combatant => {
// still not undefined...
const currentProgress = this.initiatives.get(combatant) ?? 0
this.initiatives.set(combatant, currentProgress + closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1)))
this.initiatives.set(
combatant,
currentProgress +
closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1))
)
})

// TODO: still let the creature use drained-vigor moves
@@ -726,24 +876,21 @@ export class Encounter {
return this.nextMove(closestRemaining + totalTime)
} else {
// applies digestion every time combat advances
const tickResults = this.combatants.flatMap(
combatant => combatant.containers.map(
container => container.tick(5 * (closestRemaining + totalTime))
)
)
const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented)
const tickResults = this.combatants.flatMap(combatant =>
combatant.containers.map(container =>
container.tick(5 * (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()])
const parts = effectResults
.map(result => result.log)
.concat([this.nextMove()])

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

@@ -753,8 +900,12 @@ export class Encounter {
/**
* Combat is won once one side is completely disabled
*/
get winner (): null|Side {
const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.disabled).map(combatant => combatant.side))
get winner (): null | Side {
const remaining: Set<Side> = new Set(
this.combatants
.filter(combatant => !combatant.disabled)
.map(combatant => combatant.side)
)

if (remaining.size === 1) {
return Array.from(remaining)[0]
@@ -766,8 +917,12 @@ export class Encounter {
/**
* 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))
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]
@@ -778,27 +933,23 @@ export class Encounter {
}

export abstract class Consequence {
constructor (public conditions: Condition[]) {

}
constructor (public conditions: Condition[]) {}

applicable (user: Creature, target: Creature): boolean {
return this.conditions.every(cond => cond.allowed(user, target))
}

abstract describe (user: Creature, target: Creature): LogEntry
abstract apply (user: Creature, target: Creature): LogEntry
abstract describe(user: Creature, target: Creature): LogEntry
abstract apply(user: Creature, target: Creature): LogEntry
}

export abstract class GroupConsequence {
constructor (public conditions: Condition[]) {

}
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
abstract describe(user: Creature, targets: Array<Creature>): LogEntry
abstract apply(user: Creature, targets: Array<Creature>): LogEntry
}

+ 50
- 8
src/game/combat/actions.ts Переглянути файл

@@ -4,7 +4,7 @@ import { Entity } from '@/game/entity'
import { Creature } from "../creature"
import { Damage, DamageFormula, Vigor, Action, Condition, CombatTest, CompositionAction } from '@/game/combat'
import { LogLine, LogLines, LogEntry } from '@/game/interface'
import { VoreContainer, Container } from '@/game/vore'
import { Connection, Container } from '@/game/vore'
import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from '@/game/combat/conditions'
import { ConsumeConsequence } from '@/game/combat/consequences'

@@ -216,8 +216,50 @@ export class StruggleAction extends Action {
)
}

/**
* Tries to move between containers
*/
export class StruggleMoveAction extends Action {
constructor (public from: Container, public to: Container) {
super(
new DynText('Struggle (', new LiveText(from, x => x.name.all), ' to ', new LiveText(to, x => x.name.all), ')'),
'Try to escape from your foe',
[new CapableCondition(), new PairCondition(), new ContainedByCondition(from)],
[
new CompositionTest(
[
new OpposedStatScorer(
{ Power: 1, Agility: 1, Bulk: 0.05 },
{ Toughness: 1, Reflexes: 1, Mass: 0.05 }
)
],
(user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to escape from ${target.name.possessive} ${from.name}.`),
TestCategory.Vore,
-5
)
]
)
}

execute (user: Creature, target: Creature): LogEntry {
if (user.containedIn !== null) {
return new LogLines(this.successLine(user, target, { container: this.from }), 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.baseName.possessive} ${this.from.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'))}!`
)
}

export class RubAction extends Action {
constructor (protected container: VoreContainer) {
constructor (protected container: Container) {
super(
new DynText('Rub (', new LiveText(container, container => container.name.all), ')'),
'Digest your prey more quickly',
@@ -272,16 +314,16 @@ export class ReleaseAction extends Action {
export class TransferAction extends Action {
verb: Verb = new Verb('send')

constructor (protected from: Container, protected to: Container, name = 'Transfer') {
constructor (public from: Container, public to: Connection, name = 'Transfer') {
super(
name,
`${from.name.all.capital} to ${to.name.all}`,
`${from.name.all.capital} to ${to.destination.name.all}`,
[new CapableCondition(), new PairCondition()]
)
}

line: PairLineArgs<Creature, { from: Container; to: Container }> = (user, target, args) => new LogLine(
`${user.name.capital} ${user.name.conjugate(this.verb)} ${target.name.objective} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}`
line: PairLineArgs<Creature, { from: Container; to: Connection }> = (user, target, args) => new LogLine(
`${user.name.capital} ${user.name.conjugate(this.verb)} ${target.name.objective} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.destination.name}`
)

allowed (user: Creature, target: Creature) {
@@ -294,12 +336,12 @@ export class TransferAction extends Action {

execute (user: Creature, target: Creature): LogEntry {
this.from.release(target)
this.to.consume(target)
this.to.destination.consume(target)
return this.line(user, target, { from: this.from, to: this.to })
}

describe (user: Creature, target: Creature): LogEntry {
return new LogLine(`Push ${target.baseName} from your ${this.from.name} to your ${this.to.name}`)
return new LogLine(`Push ${target.baseName} from your ${this.from.name} to your ${this.to.destination.name}`)
}
}



+ 137
- 42
src/game/combat/effects.ts Переглянути файл

@@ -1,13 +1,21 @@
import { StatusEffect, Damage, DamageType, Action, Condition, Vigor, Stat } from '@/game/combat'
import { DynText, LiveText, ToBe, Verb } from '@/game/language'
import {
StatusEffect,
Damage,
DamageType,
Action,
Condition,
Vigor,
Stat
} from "@/game/combat"
import { DynText, LiveText, ToBe, Verb } from "@/game/language"
import { Creature } from "../creature"
import { LogLine, LogEntry, LogLines, FAElem, nilLog } from '@/game/interface'
import { VoreContainer } from '@/game/vore'
import * as Words from '@/game/words'
import { LogLine, LogEntry, LogLines, FAElem, nilLog } from "@/game/interface"
import { Container } from "@/game/vore"
import * as Words from "@/game/words"

export class InstantKillEffect extends StatusEffect {
constructor () {
super('Instant Kill', 'Instant kill!', 'fas fa-skull')
super("Instant Kill", "Instant kill!", "fas fa-skull")
}

onApply (creature: Creature) {
@@ -15,8 +23,10 @@ export class InstantKillEffect extends StatusEffect {
creature.removeEffect(this)
return new LogLines(
new LogLine(
`${creature.name.capital} ${creature.name.conjugate(new ToBe())} killed instantly! `,
new FAElem('fas fa-skull')
`${creature.name.capital} ${creature.name.conjugate(
new ToBe()
)} killed instantly! `,
new FAElem("fas fa-skull")
),
creature.takeDamage(new Damage())
)
@@ -25,8 +35,12 @@ export class InstantKillEffect extends StatusEffect {

export class StunEffect extends StatusEffect {
constructor (private duration: number) {
super('Stun', 'Cannot act!', 'fas fa-sun')
this.desc = new DynText('Stunned for your next ', new LiveText(this, x => x.duration), ' actions!')
super("Stun", "Cannot act!", "fas fa-sun")
this.desc = new DynText(
"Stunned for your next ",
new LiveText(this, x => x.duration),
" actions!"
)
}

get topLeft () {
@@ -34,11 +48,19 @@ export class StunEffect extends StatusEffect {
}

onApply (creature: Creature) {
return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} is stunned!`)
return new LogLine(
`${creature.name.capital} ${creature.name.conjugate(
new ToBe()
)} is stunned!`
)
}

onRemove (creature: Creature) {
return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} no longer stunned.`)
return new LogLine(
`${creature.name.capital} ${creature.name.conjugate(
new ToBe()
)} no longer stunned.`
)
}

preTurn (creature: Creature): { prevented: boolean; log: LogEntry } {
@@ -46,7 +68,9 @@ export class StunEffect extends StatusEffect {
return {
prevented: true,
log: new LogLines(
`${creature.name.capital} ${creature.name.conjugate(new ToBe())} stunned! ${creature.pronouns.capital.subjective} can't move.`,
`${creature.name.capital} ${creature.name.conjugate(
new ToBe()
)} stunned! ${creature.pronouns.capital.subjective} can't move.`,
creature.removeEffect(this)
)
}
@@ -54,7 +78,9 @@ export class StunEffect extends StatusEffect {
return {
prevented: true,
log: new LogLines(
`${creature.name.capital} ${creature.name.conjugate(new ToBe())} stunned! ${creature.pronouns.capital.subjective} can't move!`
`${creature.name.capital} ${creature.name.conjugate(
new ToBe()
)} stunned! ${creature.pronouns.capital.subjective} can't move!`
)
}
}
@@ -63,15 +89,30 @@ export class StunEffect extends StatusEffect {

export class DamageTypeResistanceEffect extends StatusEffect {
constructor (private damageTypes: DamageType[], private amount: number) {
super('Resistance', 'Block ' + ((1 - amount) * 100).toFixed() + '% of these damage types: ' + damageTypes.join(", "), 'fas fa-shield-alt')
super(
"Resistance",
"Block " +
((1 - amount) * 100).toFixed() +
"% of these damage types: " +
damageTypes.join(", "),
"fas fa-shield-alt"
)
}

onApply (creature: Creature) {
return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new Verb('gain'))} a shield!`)
return new LogLine(
`${creature.name.capital} ${creature.name.conjugate(
new Verb("gain")
)} a shield!`
)
}

onRemove (creature: Creature) {
return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new Verb('lose'))} ${creature.pronouns.possessive} shield!`)
return new LogLine(
`${creature.name.capital} ${creature.name.conjugate(new Verb("lose"))} ${
creature.pronouns.possessive
} shield!`
)
}

modResistance (type: DamageType, factor: number) {
@@ -85,17 +126,27 @@ export class DamageTypeResistanceEffect extends StatusEffect {

export class PredatorCounterEffect extends StatusEffect {
constructor (private devour: Action, private chance: number) {
super('Predatory Counter', 'Eat them back', 'fas fa-redo')
this.desc = new DynText(new LiveText(this, x => (x.chance * 100).toFixed(0)), '% chance to devour your attackers')
super("Predatory Counter", "Eat them back", "fas fa-redo")
this.desc = new DynText(
new LiveText(this, x => (x.chance * 100).toFixed(0)),
"% chance to devour your attackers"
)
}

preAttack (creature: Creature, attacker: Creature) {
if (this.devour.allowed(creature, attacker) && Math.random() < this.chance) {
if (
this.devour.allowed(creature, attacker) &&
Math.random() < this.chance
) {
return {
prevented: true,
log: new LogLines(
`${creature.name.capital} ${creature.name.conjugate(new Verb('surprise'))} ${attacker.name.objective} and ${creature.name.conjugate(new Verb('try', 'tries'))} to devour ${attacker.pronouns.objective}!`,
this.devour.execute(creature, attacker)
`${creature.name.capital} ${creature.name.conjugate(
new Verb("surprise")
)} ${attacker.name.objective} and ${creature.name.conjugate(
new Verb("try", "tries")
)} to devour ${attacker.pronouns.objective}!`,
this.devour.execute(creature, attacker)
)
}
} else {
@@ -106,7 +157,7 @@ export class PredatorCounterEffect extends StatusEffect {

export class UntouchableEffect extends StatusEffect {
constructor () {
super('Untouchable', 'Cannot be attacked', 'fas fa-times')
super("Untouchable", "Cannot be attacked", "fas fa-times")
}

preAttack (creature: Creature, attacker: Creature) {
@@ -119,7 +170,11 @@ export class UntouchableEffect extends StatusEffect {

export class DazzlingEffect extends StatusEffect {
constructor (private conditions: Condition[]) {
super('Dazzling', 'Stuns enemies who try to affect this creature', 'fas fa-spinner')
super(
"Dazzling",
"Stuns enemies who try to affect this creature",
"fas fa-spinner"
)
}

preReceiveAction (creature: Creature, attacker: Creature) {
@@ -127,7 +182,9 @@ export class DazzlingEffect extends StatusEffect {
attacker.applyEffect(new StunEffect(1))
return {
prevented: true,
log: new LogLine(`${attacker.name.capital} can't act against ${creature.name.objective}!`)
log: new LogLine(
`${attacker.name.capital} can't act against ${creature.name.objective}!`
)
}
} else {
return {
@@ -140,21 +197,32 @@ export class DazzlingEffect extends StatusEffect {

export class SurrenderEffect extends StatusEffect {
constructor () {
super('Surrendered', 'This creature has given up, and will fail most tests', 'fas fa-flag')
super(
"Surrendered",
"This creature has given up, and will fail most tests",
"fas fa-flag"
)
}

onApply (creature: Creature): LogEntry {
creature.takeDamage(
new Damage(
{ amount: creature.vigors.Resolve, target: Vigor.Resolve, type: DamageType.Pure }
)
new Damage({
amount: creature.vigors.Resolve,
target: Vigor.Resolve,
type: DamageType.Pure
})
)
return new LogLine(
`${creature.name.capital} ${creature.name.conjugate(new Verb('surrender'))}!`
`${creature.name.capital} ${creature.name.conjugate(
new Verb("surrender")
)}!`
)
}

failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } {
failTest (
creature: Creature,
opponent: Creature
): { failed: boolean; log: LogEntry } {
return {
failed: true,
log: nilLog
@@ -164,7 +232,7 @@ export class SurrenderEffect extends StatusEffect {

export class SizeEffect extends StatusEffect {
constructor (private change: number) {
super('Size-Shifted', 'This creature has changed in size', 'fas fa-ruler')
super("Size-Shifted", "This creature has changed in size", "fas fa-ruler")
}

onApply (creature: Creature): LogLine {
@@ -178,26 +246,47 @@ export class SizeEffect extends StatusEffect {

export class DigestionPowerEffect extends StatusEffect {
constructor (private factor: number) {
super('Acid-fueled', 'This creature is digesting faster than nomral', 'fas fa-flask')
super(
"Acid-fueled",
"This creature is digesting faster than nomral",
"fas fa-flask"
)
}

onApply (creature: Creature): LogLine {
const voreContainer: VoreContainer|undefined = creature.containers.find(c => c.digest !== null)
const voreContainer: Container | undefined = creature.containers.find(
c => c.digest !== null
)
if (voreContainer !== undefined) {
return new LogLine(`${creature.name.capital.possessive}'s ${voreContainer.name} ${Words.Churns} and ${voreContainer.sound}`)
return new LogLine(
`${creature.name.capital.possessive}'s ${voreContainer.name} ${Words.Churns} and ${voreContainer.sound}`
)
} else {
return new LogLine(`${creature.name.capital} can't digest people...`)
}
}

modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage {
modDigestionDamage (
predator: Creature,
prey: Creature,
container: Container,
damage: Damage
): Damage {
return damage.scale(this.factor)
}
}

export class StatEffect extends StatusEffect {
constructor (private stat: Stat, private amount: number, private factor: number) {
super('Stat boosted', 'This creature has modified stats', 'fas fa-user-plus')
constructor (
private stat: Stat,
private amount: number,
private factor: number
) {
super(
"Stat boosted",
"This creature has modified stats",
"fas fa-user-plus"
)
}

modStat (creature: Creature, stat: Stat, current: number): number {
@@ -211,16 +300,22 @@ export class StatEffect extends StatusEffect {

export class InstantDigestionEffect extends StatusEffect {
constructor () {
super("Instant digestion", "This creature will melt people instantly", "fas fa-skull")
super(
"Instant digestion",
"This creature will melt people instantly",
"fas fa-skull"
)
}

postConsume (predator: Creature, prey: Creature, container: VoreContainer) {
postConsume (predator: Creature, prey: Creature, container: Container) {
prey.applyEffect(new InstantKillEffect())
predator.voreStats.Mass += prey.voreStats.Mass
prey.voreStats.Mass = 0
return new LogLines(
`${prey.name.capital} ${prey.name.conjugate(new ToBe())} instantly digested! `,
new FAElem('fas fa-skull'),
`${prey.name.capital} ${prey.name.conjugate(
new ToBe()
)} instantly digested! `,
new FAElem("fas fa-skull"),
container.tick(0, [prey])
)
}


+ 5
- 11
src/game/creature.ts Переглянути файл

@@ -1,7 +1,7 @@
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'
import { VoreType, Container } from '@/game/vore'
import { Item, EquipmentSlot, Equipment, ItemKind, Currency } from '@/game/items'
import { PassAction } from '@/game/combat/actions'
import { AI, RandomAI } from '@/game/ai'
@@ -31,8 +31,7 @@ export class Creature extends Entity {

destroyed = false;

containers: Array<VoreContainer> = []
otherContainers: Array<Container> = []
containers: Array<Container> = []

containedIn: Container | null = null

@@ -81,7 +80,7 @@ export class Creature extends Entity {
this.voreStats = {
get [VoreStat.Bulk] () {
return self.containers.reduce(
(total: number, container: VoreContainer) => {
(total: number, container: Container) => {
return total + container.contents.reduce(
(total: number, prey: Creature) => {
return total + prey.voreStats.Bulk
@@ -111,7 +110,7 @@ export class Creature extends Entity {
},
get [VoreStat.Prey] () {
return self.containers.reduce(
(total: number, container: VoreContainer) => {
(total: number, container: Container) => {
return total + container.contents.concat(container.digested).reduce(
(total: number, prey: Creature) => {
return total + 1 + prey.voreStats[VoreStat.Prey]
@@ -206,15 +205,11 @@ export class Creature extends Entity {
return effect.onApply(this)
}

addVoreContainer (container: VoreContainer): void {
addContainer (container: Container): void {
this.containers.push(container)
this.voreRelay.connect(container.voreRelay)
}

addOtherContainer (container: Container): void {
this.otherContainers.push(container)
}

addPerk (perk: Perk): void {
this.perks.push(perk)
}
@@ -273,7 +268,6 @@ export class Creature extends Entity {
this.actions,
this.containers.flatMap(container => container.actions),
target.otherActions,
this.otherContainers.flatMap(container => container.actions),
Object.values(this.equipment).filter(item => item !== undefined).flatMap(item => (item as Equipment).actions),
this.items.filter(item => item.kind === ItemKind.Consumable && !item.consumed).flatMap(item => item.actions),
this.perks.flatMap(perk => perk.actions(this))


+ 0
- 97
src/game/creatures/characters/inazuma.ts Переглянути файл

@@ -1,97 +0,0 @@
import { FavorEscapedPrey, VoreAI } from '@/game/ai'
import { CompositionAction, ConstantDamageFormula, Damage, DamageType, FractionDamageFormula, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat'
import { PairCondition, TogetherCondition } from '@/game/combat/conditions'
import { ConsumeConsequence, DamageConsequence, DrainConsequence, StatusConsequence } from '@/game/combat/consequences'
import { LogGroupConsequence } from '@/game/combat/groupConsequences'
import { PreyTargeter, SideTargeter, SoloTargeter } from '@/game/combat/targeters'
import { CompositionTest, OpposedStatScorer, TestCategory } from '@/game/combat/tests'
import { Creature } from '@/game/creature'
import { LogLine, nilLog } from '@/game/interface'
import { ImproperNoun, MalePronouns, ProperNoun } from '@/game/language'
import { anyVore, Stomach } from '@/game/vore'

export default class Inazuma extends Creature {
constructor () {
super(
new ProperNoun("Inazuma"),
new ImproperNoun("zorgoia"),
MalePronouns,
{
Power: 30,
Toughness: 35,
Agility: 45,
Reflexes: 40,
Charm: 60,
Willpower: 50
},
new Set(),
anyVore,
200
)

this.side = Side.Monsters
this.ai = (new VoreAI(this))

this.ai.addDecider(new FavorEscapedPrey())

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)],
tests: [new CompositionTest(
[
new OpposedStatScorer(
{
Power: 0.5,
Agility: 0.75
},
{
Power: 0.5,
Reflexes: 0.75
}
)
],
() => nilLog,
TestCategory.Vore,
0
)],
groupConsequences: [new LogGroupConsequence(
(user, targets) => new LogLine(`With a mighty GULP!, all ${targets.length} of ${user.name.possessive} prey are swallowed down.`)
)]
}
))

this.actions.push(new CompositionAction(
"Level Drain",
"Steal stats from prey",
{
conditions: [new PairCondition()],
targeters: [new PreyTargeter(stomach)],
consequences: [new DrainConsequence(
new FractionDamageFormula([
{ fraction: 0.25, target: Stat.Power, type: DamageType.Pure },
{ fraction: 0.25, target: Stat.Toughness, type: DamageType.Pure },
{ fraction: 0.25, target: Stat.Agility, type: DamageType.Pure },
{ fraction: 0.25, target: Stat.Reflexes, type: DamageType.Pure },
{ fraction: 0.25, target: Stat.Charm, type: DamageType.Pure },
{ fraction: 0.25, target: Stat.Willpower, type: DamageType.Pure }
])
)]
}
))

this.addVoreContainer(stomach)
this.ai = new VoreAI(this)
}
}

+ 0
- 39
src/game/creatures/monsters/slime.ts Переглянути файл

@@ -1,39 +0,0 @@
import { VoreAI } from '@/game/ai'
import { DamageType, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat'
import { Creature } from '@/game/creature'
import { ImproperNoun, ObjectPronouns } from '@/game/language'
import { anyVore, Goo } from '@/game/vore'

export default class Slime extends Creature {
constructor () {
super(
new ImproperNoun("slime", "slimes"),
new ImproperNoun("slime", "slimes"),
ObjectPronouns,
{
Power: 20,
Toughness: 20,
Agility: 5,
Reflexes: 5,
Charm: 5,
Willpower: 5
},
anyVore,
anyVore,
50
)

const gooContainer = new Goo(
this,
3,
new StatDamageFormula([
{ fraction: 1, stat: Stat.Toughness, type: DamageType.Acid, target: Vigor.Health }
])
)

this.addVoreContainer(gooContainer)

this.side = Side.Monsters
this.ai = new VoreAI(this)
}
}

+ 20
- 4
src/game/creatures/monsters/werewolf.ts Переглянути файл

@@ -1,15 +1,15 @@
import { VoreAI } from '@/game/ai'
import { DamageType, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat'
import { Creature } from '@/game/creature'
import { ImproperNoun, ObjectPronouns } from '@/game/language'
import { anyVore, Goo, Stomach } from '@/game/vore'
import { ImproperNoun, MalePronouns, ObjectPronouns } from '@/game/language'
import { anyVore, ConnectionDirection, Stomach, Throat } from '@/game/vore'

export default class Werewolf extends Creature {
constructor () {
super(
new ImproperNoun("werewolf", "werewolves"),
new ImproperNoun("werewolf", "werewolves"),
ObjectPronouns,
MalePronouns,
{
Power: 45,
Toughness: 30,
@@ -23,6 +23,11 @@ export default class Werewolf extends Creature {
75
)

const throat = new Throat(
this,
25
)

const stomach = new Stomach(
this,
50,
@@ -31,7 +36,18 @@ export default class Werewolf extends Creature {
])
)

this.addVoreContainer(stomach)
this.addContainer(throat)
this.addContainer(stomach)

throat.connect({
destination: stomach,
direction: ConnectionDirection.Deeper
})

stomach.connect({
destination: throat,
direction: ConnectionDirection.Shallower
})

this.side = Side.Monsters
this.ai = new VoreAI(this)


+ 11
- 3
src/game/creatures/player.ts Переглянути файл

@@ -1,10 +1,10 @@
import { Creature } from "../creature"
import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '@/game/language'
import { Damage, DamageType, Vigor, ConstantDamageFormula } from '@/game/combat'
import { Stomach, Bowels, anyVore, Cock, Balls, Breasts, InnerBladder, Slit, Womb, biconnectContainers } from '@/game/vore'
import { Stomach, anyVore, Container } from '@/game/vore'
import { AttackAction } from '@/game/combat/actions'
import { RavenousPerk, BellyBulwakPerk, FlauntPerk } from '@/game/combat/perks'
import { VoreAI } from "../ai"
import { nilLog } from "../interface"

export default class Player extends Creature {
constructor () {
@@ -21,10 +21,18 @@ export default class Player extends Creature {
this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina }))))

const stomach = new Stomach(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health })))
this.addVoreContainer(stomach)
this.addContainer(stomach)

this.perspective = POV.First

this.ai = new VoreAI(this)

this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => {
if (this === args.prey) {
return sender.describeDetail(this)
} else {
return nilLog
}
})
}
}

+ 2
- 2
src/game/events.ts Переглянути файл

@@ -1,7 +1,7 @@
import { TargetDrainedVigorCondition } from '@/game/combat/conditions'
import { Creature } from '@/game/creature'
import { LogEntry, LogLines, nilLog } from "@/game/interface"
import { VoreContainer } from '@/game/vore'
import { Container } from '@/game/vore'
import { Action } from './combat'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -47,7 +47,7 @@ type VoreMap = {
"onAbsorbed": { prey: Creature };
}

export class VoreRelay extends Relay<VoreContainer, VoreMap> {
export class VoreRelay extends Relay<Container, VoreMap> {
constructor () {
super(["onEaten", "onReleased", "onDigested", "onAbsorbed"])
}


+ 187
- 89
src/game/language.ts Переглянути файл

@@ -1,6 +1,10 @@
import { LogEntry } from '@/game/interface'
import { LogEntry } from "@/game/interface"

export enum POV {First, Second, Third}
export enum POV {
First,
Second,
Third
}

export type SoloLine<T> = (user: T) => LogEntry
export type SoloLineArgs<T, V> = (user: T, args: V) => LogEntry
@@ -9,15 +13,15 @@ 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,
Nonspecific,
All
Specific,
Nonspecific,
All
}

enum VowelSound {
Default,
Vowel,
NonVowel
Default,
Vowel,
NonVowel
}

enum VerbKind {
@@ -64,9 +68,7 @@ export type TextLike = { toString: () => string }

// updates as needed
export class LiveText<T> {
constructor (private contents: T, private run: (thing: T) => TextLike) {

}
constructor (private contents: T, private run: (thing: T) => TextLike) {}

toString (): string {
return this.run(this.contents).toString()
@@ -80,17 +82,15 @@ export class DynText {
}

toString (): string {
return (this.parts.map(part => part.toString())).join('')
return this.parts.map(part => part.toString()).join("")
}
}

export abstract class Word {
constructor (public opt: WordOptions = emptyConfig) {
constructor (public opt: WordOptions = emptyConfig) {}

}

abstract configure (opts: WordOptions): Word;
abstract toString (): string;
abstract configure(opts: WordOptions): Word
abstract toString(): string

// These functions are pure; they don't mutate the original object.
// This is necessary to avoid causing chaos.
@@ -230,7 +230,11 @@ export class OptionalWord extends Word {

export class RandomWord extends Word {
private history: { last: number }
constructor (public choices: Array<Word>, opt: WordOptions = emptyConfig, history: { last: number } = { last: -1 }) {
constructor (
public choices: Array<Word>,
opt: WordOptions = emptyConfig,
history: { last: number } = { last: -1 }
) {
super(opt)
this.history = history
}
@@ -252,12 +256,22 @@ export class RandomWord extends Word {
}

export class Noun extends Word {
constructor (protected singularNoun: string, protected pluralNoun: string|null = null, protected possessiveNoun: string|null = null, protected options: WordOptions = emptyConfig) {
constructor (
protected singularNoun: string,
protected pluralNoun: string | null = null,
protected possessiveNoun: string | null = null,
protected options: WordOptions = emptyConfig
) {
super(options)
}

configure (opts: WordOptions): Word {
return new Noun(this.singularNoun, this.pluralNoun, this.possessiveNoun, opts)
return new Noun(
this.singularNoun,
this.pluralNoun,
this.possessiveNoun,
opts
)
}

toString (): string {
@@ -274,31 +288,34 @@ export class Noun extends Word {
if (this.pluralNoun === null) {
result = this.singularNoun
} else {
result = (this.pluralNoun as string)
result = this.pluralNoun as string
}
} else {
result = this.singularNoun
}

if (!this.options.proper) {
if (this.options.nounKind === NounKind.Nonspecific && this.options.count) {
if (
this.options.nounKind === NounKind.Nonspecific &&
this.options.count
) {
if (this.options.plural) {
result = 'some ' + result
result = "some " + result
} else {
if (this.options.vowel === VowelSound.Default) {
if ('aeiouAEIOU'.indexOf(result.slice(0, 1)) >= 0) {
result = 'an ' + result
if ("aeiouAEIOU".indexOf(result.slice(0, 1)) >= 0) {
result = "an " + result
} else {
result = 'a ' + result
result = "a " + result
}
} else if (this.options.vowel === VowelSound.Vowel) {
result = 'an ' + result
result = "an " + result
} else if (this.options.vowel === VowelSound.NonVowel) {
result = 'a ' + result
result = "a " + result
}
}
} else if (this.options.nounKind === NounKind.Specific) {
result = 'the ' + result
result = "the " + result
}
}

@@ -322,13 +339,37 @@ export class Noun extends Word {

export class ImproperNoun extends Noun {
constructor (singularNoun: string, pluralNoun: string = singularNoun) {
super(singularNoun, pluralNoun, null, { plural: false, allCaps: false, capital: false, proper: false, nounKind: NounKind.Specific, verbKind: VerbKind.Root, vowel: VowelSound.Default, count: true, possessive: false, objective: false, perspective: POV.Third })
super(singularNoun, pluralNoun, null, {
plural: false,
allCaps: false,
capital: false,
proper: false,
nounKind: NounKind.Specific,
verbKind: VerbKind.Root,
vowel: VowelSound.Default,
count: true,
possessive: false,
objective: false,
perspective: POV.Third
})
}
}

export class ProperNoun extends Noun {
constructor (singularNoun: string) {
super(singularNoun, null, null, { plural: false, allCaps: false, capital: false, proper: true, nounKind: NounKind.Specific, verbKind: VerbKind.Root, vowel: VowelSound.Default, count: true, possessive: false, objective: false, perspective: POV.Third })
super(singularNoun, null, null, {
plural: false,
allCaps: false,
capital: false,
proper: true,
nounKind: NounKind.Specific,
verbKind: VerbKind.Root,
vowel: VowelSound.Default,
count: true,
possessive: false,
objective: false,
perspective: POV.Third
})
}
}

@@ -341,9 +382,14 @@ export class Adjective extends Word {
return new Adjective(this.adjective, opts)
}

// TODO caps et al.
toString (): string {
return this.adjective
let word = this.adjective
if (this.opt.allCaps) {
word = word.toUpperCase()
} else if (this.opt.capital) {
word = word.slice(0, 1).toUpperCase() + word.slice(1)
}
return word
}
}

@@ -361,8 +407,21 @@ export class Adverb extends Word {
}
}

/**
* root: break
* singular: breaks
* present: breaking
* past: broken
*/
export class Verb extends Word {
constructor (private _root: string, private _singular: string = _root + "s", private _present: string = _root + "ing", private _past: string = _root + "ed", private _pastParticiple: string = _past, public opt: WordOptions = emptyConfig) {
constructor (
private _root: string,
private _singular: string = _root + "s",
private _present: string = _root + "ing",
private _past: string = _root + "ed",
private _pastParticiple: string = _past,
public opt: WordOptions = emptyConfig
) {
super(opt)
}

@@ -381,11 +440,21 @@ export class Verb extends Word {
let choice: string

switch (this.opt.verbKind) {
case VerbKind.Root: choice = this._root; break
case VerbKind.Singular: choice = this._singular; break
case VerbKind.Present: choice = this._present; break
case VerbKind.Past: choice = this._past; break
case VerbKind.PastParticiple: choice = this._pastParticiple; break
case VerbKind.Root:
choice = this._root
break
case VerbKind.Singular:
choice = this._singular
break
case VerbKind.Present:
choice = this._present
break
case VerbKind.Past:
choice = this._past
break
case VerbKind.PastParticiple:
choice = this._pastParticiple
break
}

if (this.opt.allCaps) {
@@ -430,12 +499,18 @@ export class ToBe extends Word {
let choice

if (this.opts.plural) {
choice = 'are'
choice = "are"
}
switch (this.opts.perspective) {
case POV.First: choice = 'am'; break
case POV.Second: choice = 'are'; break
case POV.Third: choice = 'is'; break
case POV.First:
choice = "am"
break
case POV.Second:
choice = "are"
break
case POV.Third:
choice = "is"
break
}

if (this.opt.allCaps) {
@@ -449,16 +524,18 @@ export class ToBe extends Word {
}

interface PronounDict {
subjective: string;
objective: string;
possessive: string;
reflexive: string;
subjective: string;
objective: string;
possessive: string;
reflexive: string;
}

export class Pronoun implements Pluralizable {
constructor (private pronouns: PronounDict, private capitalize: boolean = false, public isPlural: boolean = false) {

}
constructor (
private pronouns: PronounDict,
private capitalize: boolean = false,
public isPlural: boolean = false
) {}

get capital (): Pronoun {
return new Pronoun(this.pronouns, true)
@@ -498,53 +575,69 @@ export class Pronoun implements Pluralizable {
}

export const MalePronouns = new Pronoun({
subjective: 'he',
objective: 'him',
possessive: 'his',
reflexive: 'himself'
subjective: "he",
objective: "him",
possessive: "his",
reflexive: "himself"
})

export const FemalePronouns = new Pronoun({
subjective: 'she',
objective: 'her',
possessive: 'her',
reflexive: 'herself'
subjective: "she",
objective: "her",
possessive: "her",
reflexive: "herself"
})

export const TheyPronouns = new Pronoun({
subjective: 'they',
objective: 'them',
possessive: 'their',
reflexive: 'themself'
}, false, true)

export const TheyPluralPronouns = new Pronoun({
subjective: 'they',
objective: 'them',
possessive: 'their',
reflexive: 'themselves'
}, false, true)
export const TheyPronouns = new Pronoun(
{
subjective: "they",
objective: "them",
possessive: "their",
reflexive: "themself"
},
false,
true
)

export const TheyPluralPronouns = new Pronoun(
{
subjective: "they",
objective: "them",
possessive: "their",
reflexive: "themselves"
},
false,
true
)

export const ObjectPronouns = new Pronoun({
subjective: 'it',
objective: 'it',
possessive: 'its',
reflexive: 'itself'
subjective: "it",
objective: "it",
possessive: "its",
reflexive: "itself"
})

export const SecondPersonPronouns = new Pronoun({
subjective: 'you',
objective: 'you',
possessive: 'your',
reflexive: 'yourself'
}, false, true)

export const FirstPersonPronouns = new Pronoun({
subjective: 'I',
objective: 'me',
possessive: 'my',
reflexive: 'myself'
}, false, true)
export const SecondPersonPronouns = new Pronoun(
{
subjective: "you",
objective: "you",
possessive: "your",
reflexive: "yourself"
},
false,
true
)

export const FirstPersonPronouns = new Pronoun(
{
subjective: "I",
objective: "me",
possessive: "my",
reflexive: "myself"
},
false,
true
)

export class PronounAsNoun extends Noun {
constructor (private pronouns: Pronoun, opt: WordOptions = emptyConfig) {
@@ -559,7 +652,12 @@ export class PronounAsNoun extends Noun {

toString (): string {
if (this.options.objective) {
return new Noun(this.pronouns.objective, this.pronouns.objective, this.pronouns.possessive, this.options).toString()
return new Noun(
this.pronouns.objective,
this.pronouns.objective,
this.pronouns.possessive,
this.options
).toString()
} else {
return super.toString()
}


+ 0
- 41
src/game/maps/town.ts Переглянути файл

@@ -9,9 +9,7 @@ import { InstantDigestionEffect, SurrenderEffect } from '@/game/combat/effects'
import moment from 'moment'
import { VoreAI } from '@/game/ai'
import { DeliciousPerk } from '@/game/combat/perks'
import Inazuma from '../creatures/characters/inazuma'
import Human from '../creatures/human'
import Slime from '../creatures/monsters/slime'
import Werewolf from '../creatures/monsters/werewolf'

function makeParty (): Creature[] {
@@ -103,13 +101,6 @@ export const Town = (): Place => {
"The center of town"
)

const bossEncounters = [
new Encounter(
{ name: "Inazuma", intro: () => nilLog },
makeParty().concat([new Inazuma()])
)
]

home.choices.push(
new Choice(
"Nap",
@@ -228,19 +219,6 @@ export const Town = (): Place => {
)
)

bossEncounters.forEach(encounter => {
bosses.choices.push(
new Choice(
encounter.desc.name,
"Boss fight!",
(world) => {
world.encounter = encounter
return nilLog
}
)
)
})

debug.choices.push(
new Choice(
"Cut stats",
@@ -322,25 +300,6 @@ export const Town = (): Place => {
)
)

woods.choices.push(
new Choice(
"Fight a slime",
"Go fight a slime",
(world, executor) => {
const enemy = new Slime()
const encounter = new Encounter(
{
name: "Fight some tasty nerd",
intro: () => new LogLine(`A slime draws near!`)
},
[world.player, enemy].concat(world.party)
)
world.encounter = encounter
return nilLog
}
)
)

woods.choices.push(
new Choice(
"Fight a werewolf",


+ 202
- 369
src/game/vore.ts Переглянути файл

@@ -1,48 +1,82 @@
import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula } from '@/game/combat'
import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula, ConstantDamageFormula } from '@/game/combat'
import { LogLines, LogEntry, LogLine, nilLog, RandomEntry } from '@/game/interface'
import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe } from '@/game/language'
import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from '@/game/combat/actions'
import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe, Adjective } from '@/game/language'
import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction, StruggleMoveAction } from '@/game/combat/actions'
import * as Words from '@/game/words'
import { Creature } from '@/game/creature'
import { VoreRelay } from '@/game/events'

export enum VoreType {
Oral = "Oral Vore",
Anal = "Anal Vore",
Cock = "Cock Vore",
Unbirth = "Unbirthing",
Breast = "Breast Vore",
Bladder = "Bladder Vore",
Tail = "Tail Vore",
Goo = "Goo Vore"
Oral = "Oral Vore"
}

export const anyVore = new Set([
VoreType.Oral,
VoreType.Anal,
VoreType.Cock,
VoreType.Unbirth,
VoreType.Breast,
VoreType.Bladder,
VoreType.Tail,
VoreType.Goo
VoreType.Oral
])

export type Wall = {
name: Word;
texture: Adjective;
material: Noun;
color: Adjective;
}

export type Fluid = {
name: Word;
color: Adjective;
sound: Word;
sloshVerb: Verb;
}

export type Gas = {
name: Word;
color: Adjective;
smell: Adjective;
bubbleVerb: Verb;
releaseVerb: Verb;
}

export enum ContainerCapability {
Consume,
Release,
Digest,
Absorb
}

export enum ConnectionDirection {
Deeper,
Neutral,
Shallower
}

export type Connection = {
destination: Container;
direction: ConnectionDirection;
}

export interface Container extends Actionable {
name: Noun;
owner: Creature;
voreTypes: Set<VoreType>;
capabilities: Set<ContainerCapability>;

capacity: number;
fullness: number;
connections: Array<Connection>;

wall: Wall | null;
fluid: Fluid | null;
gas: Gas | null;

voreRelay: VoreRelay;

contents: Array<Creature>;
describe: () => LogEntry;
digested: Array<Creature>;

canTake: (prey: Creature) => boolean;
consume: (prey: Creature) => LogEntry;
release: (prey: Creature) => LogEntry;
struggle: (prey: Creature) => LogEntry;
damage: DamageFormula;

sound: Word;

capacity: number;
fullness: number;

consumeVerb: Verb;
consumePreposition: Preposition;
@@ -51,37 +85,75 @@ export interface Container extends Actionable {
struggleVerb: Verb;
strugglePreposition: Preposition;

consumeLine (user: Creature, target: Creature): LogEntry;
canTake (prey: Creature): boolean;
consume (prey: Creature): LogEntry;
release (prey: Creature): LogEntry;
struggle (prey: Creature): LogEntry;

tick (dt: number, victims?: Array<Creature>): LogEntry;
digest (preys: Creature[]): LogEntry;
absorb (preys: Creature[]): LogEntry;

onDigest (prey: Creature): LogEntry;
onAbsorb (prey: Creature): LogEntry;

consumeLine (user: Creature, target: Creature): LogEntry;
statusLine (user: Creature, target: Creature): LogEntry;

describe (): LogEntry;
describeDetail (prey: Creature): LogEntry;

connect (dest: Connection): void;
}

export abstract class NormalContainer implements Container {
export abstract class DefaultContainer implements Container {
public name: Noun
contents: Array<Creature> = []
actions: Array<Action> = []

consumeVerb = new Verb('trap')
consumePreposition = new Preposition("in")
wall: Wall | null = null
fluid: Fluid | null = null
gas: Gas | null = null

connections: Array<Connection> = []

voreRelay = new VoreRelay()

consumeVerb = new Verb('devour')
consumePreposition = new Preposition("into")
releaseVerb = new Verb('release', 'releases', 'releasing', 'released')
releasePreposition = new Preposition("from")
releasePreposition = new Preposition("out from")
struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled')
strugglePreposition = new Preposition("in")
strugglePreposition = new Preposition("within")
fluidColor = "#00ff0088"

constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number) {
digested: Array<Creature> = []
absorbed: Array<Creature> = []

damage: DamageFormula = new ConstantDamageFormula(new Damage());

sound = new Verb("slosh")

constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number, public capabilities: Set<ContainerCapability>) {
this.name = name.all
this.actions.push(new DevourAction(this))
this.actions.push(new ReleaseAction(this))
this.actions.push(new StruggleAction(this))
if (capabilities.has(ContainerCapability.Consume)) {
this.actions.push(new DevourAction(this))
}
if (capabilities.has(ContainerCapability.Release)) {
this.actions.push(new ReleaseAction(this))
this.actions.push(new StruggleAction(this))
}
}

get capacity (): number {
return this.capacityFactor * this.owner.voreStats.Mass
connect (connection: Connection): void {
this.connections.push(connection)

this.actions.push(new TransferAction(this, connection))
this.actions.push(new StruggleMoveAction(this, connection.destination))
}

consumeLine (user: Creature, target: Creature): LogEntry {
return new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective} ${this.consumePreposition} ${user.pronouns.possessive} ${this.name}.`)
get capacity (): number {
return this.capacityFactor * this.owner.voreStats.Mass
}

statusLine (user: Creature, target: Creature): LogEntry {
@@ -99,7 +171,7 @@ export abstract class NormalContainer implements Container {
}

get fullness (): number {
return Array.from(this.contents.values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0)
return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0)
}

canTake (prey: Creature): boolean {
@@ -118,7 +190,14 @@ export abstract class NormalContainer implements Container {
}
this.contents.push(prey)
prey.containedIn = this
return this.consumeLine(this.owner, prey)

const results: Array<LogEntry> = []
this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this)))
const relayResults = this.voreRelay.dispatch("onEaten", this, { prey: prey })
const preyRelayResults = prey.voreRelay.dispatch("onEaten", this, { prey: prey })
const consumeLineResult: LogEntry = this.consumeLine(this.owner, prey)

return new LogLines(...[consumeLineResult].concat(results).concat(relayResults).concat(preyRelayResults))
}

release (prey: Creature): LogEntry {
@@ -128,7 +207,7 @@ export abstract class NormalContainer implements Container {
if (this.owner.containedIn !== null) {
this.owner.containedIn.contents.push(prey)
}
return this.releaseLine(this.owner, prey)
return new LogLines(this.releaseLine(this.owner, prey), this.voreRelay.dispatch("onReleased", this, { prey: prey }))
}

struggle (prey: Creature): LogEntry {
@@ -144,79 +223,22 @@ export abstract class NormalContainer implements Container {

return new LogLine(...lines)
}
}

export class Hand extends NormalContainer {
consumeVerb = new Verb("grab")
constructor (owner: Creature, capacity: number) {
super(
new ImproperNoun('hand'),
owner,
new Set(),
capacity
)
}
}

export abstract class InnerContainer extends NormalContainer {
constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, private escape: Container) {
super(name, owner, voreTypes, capacity)

this.actions = []

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

release (prey: Creature): LogEntry {
prey.containedIn = this.escape
this.contents = this.contents.filter(victim => victim !== prey)
return this.releaseLine(this.owner, prey)
}
}

export interface VoreContainer extends Container {
voreRelay: VoreRelay;

digested: Array<Creature>;
tick: (dt: number, victims?: Array<Creature>) => LogEntry;
digest: (preys: Creature[]) => LogEntry;
absorb: (preys: Creature[]) => LogEntry;
sound: Word;
fluidName: Word;
fluidColor: string;

onDigest: (prey: Creature) => LogEntry;
onAbsorb: (prey: Creature) => LogEntry;
}
describeDetail (prey: Creature): LogEntry {
const lines: Array<LogLine> = []

export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer {
voreRelay = new VoreRelay()

consumeVerb = new Verb('devour')
consumePreposition = new Preposition("into")
releaseVerb = new Verb('release', 'releases', 'releasing', 'released')
releasePreposition = new Preposition("out from")
struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled')
strugglePreposition = new Preposition("within")
fluidColor = "#00ff0088"

digested: Array<Creature> = []
absorbed: Array<Creature> = []

sound = new Verb("slosh")

abstract fluidName: Word

constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, private damage: DamageFormula) {
super(name, owner, voreTypes, capacity)

this.name = name

this.actions.push(new RubAction(this))
}
if (this.gas) {
lines.push(
new LogLine(`${this.gas.color.capital} ${this.gas.name.plural} ${this.gas.bubbleVerb} in ${this.owner.name.possessive} ${this.name}.`)
)
}
if (this.fluid) {
lines.push(
new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} around ${prey.name.objective}.`)
)
}

get fullness (): number {
return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0)
return new LogLine(...lines)
}

consumeLine (user: Creature, target: Creature) {
@@ -227,15 +249,21 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor
}

tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry {
return new RandomEntry(
const options = [
new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Churns)} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`),
new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`),
new LogLine(`${user.name.capital.possessive} ${this.name} ${user.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`),
new LogLine(`${target.name.capital} ${target.name.conjugate(new Verb("thrash", "thrashes"))} ${this.strugglePreposition} ${user.name.possessive} ${Words.Slick} ${this.name} as it ${Words.Churns.singular} ${target.pronouns.objective} for `, args.damage.renderShort(), `.`)
)
]

if (this.fluid) {
options.push(new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} and ${this.fluid.sound.singular} as ${this.owner.name.possessive} ${this.name} steadily ${Words.Digest.singular} ${target.name.objective}.`))
}

return new RandomEntry(...options)
}

digestLine (user: Creature, target: Creature): LogEntry {
return new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)} and ${target.pronouns.conjugate(Words.Digest)} into ${this.fluidName}.`)
return new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)}.`)
}

absorbLine (user: Creature, target: Creature): LogEntry {
@@ -250,23 +278,25 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor

const tickedEntryList: LogEntry[] = []

this.contents.forEach(prey => {
if (victims === undefined || victims.indexOf(prey) >= 0) {
const scaled = this.damage.calc(this.owner, prey).scale(dt / 60)
const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled)
if (modified.nonzero()) {
tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified }))
damageResults.push(prey.takeDamage(modified))
if (this.capabilities.has(ContainerCapability.Digest)) {
this.contents.forEach(prey => {
if (victims === undefined || victims.indexOf(prey) >= 0) {
const scaled = this.damage.calc(this.owner, prey).scale(dt / 60)
const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled)
if (modified.nonzero()) {
tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified }))
damageResults.push(prey.takeDamage(modified))
}

if (prey.vigors[Vigor.Health] <= 0) {
prey.destroyed = true
this.digested.push(prey)
justDigested.push(prey)
damageResults.push(this.onDigest(prey))
}
}

if (prey.vigors[Vigor.Health] <= 0) {
prey.destroyed = true
this.digested.push(prey)
justDigested.push(prey)
damageResults.push(this.onDigest(prey))
}
}
})
})
}

const tickedEntries = new LogLines(...tickedEntryList)

@@ -313,18 +343,6 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor
return new LogLines(...preys.map(prey => this.digestLine(this.owner, prey)))
}

consume (prey: Creature): LogEntry {
const logLine = super.consume(prey)
const results: Array<LogEntry> = []
this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this)))
const relayResults = this.voreRelay.dispatch("onEaten", this, { prey: prey })
return new LogLines(...[logLine].concat(results).concat(relayResults))
}

release (prey: Creature): LogEntry {
return new LogLines(super.release(prey), this.voreRelay.dispatch("onReleased", this, { prey: prey }))
}

onAbsorb (prey: Creature): LogEntry {
return this.voreRelay.dispatch("onAbsorbed", this, { prey: prey })
}
@@ -334,243 +352,58 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor
}
}

export abstract class InnerVoreContainer extends NormalVoreContainer {
constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, damage: DamageFormula, private escape: Container) {
super(name, owner, voreTypes, capacity, damage)

this.actions = []

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

release (prey: Creature): LogEntry {
prey.containedIn = this.escape
this.contents = this.contents.filter(victim => victim !== prey)
this.escape.consume(prey)
return this.releaseLine(this.owner, prey)
}
}

export class Stomach extends NormalVoreContainer {
fluidName = new Noun("chyme")

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(new ImproperNoun('stomach', 'stomachs').all, owner, new Set([VoreType.Oral]), capacity, damage)
}

digest (preys: Creature[]): LogEntry {
if (preys.length === 0) {
return super.digest(preys)
}

const heal = new Damage(
{
amount: preys.reduce((total: number, next: Creature) => total + next.maxVigors.Health / 5, 0),
type: DamageType.Heal,
target: Vigor.Health
}
)
this.owner.takeDamage(heal)
return new LogLines(
super.digest(preys),
new LogLine(`${this.owner.name.capital} ${this.owner.name.conjugate(new Verb("heal"))} for `, this.owner.effectiveDamage(heal).renderShort())
)
}
}

export class InnerStomach extends InnerVoreContainer {
fluidName = new Noun("chyme")

consumeVerb = new Verb('swallow')
releaseVerb = new Verb('hork')
constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: VoreContainer) {
super(new ImproperNoun('inner stomach', 'inner stomachs').all, owner, new Set([VoreType.Oral]), capacity, damage, escape)
}
}

export class Bowels extends NormalVoreContainer {
fluidName = new Noun("chyme")

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(new ImproperNoun('bowel', 'bowels').plural.all, owner, new Set([VoreType.Anal]), capacity, damage)
}

tickLine (user: Creature, target: Creature, args: { damage: Damage }) {
return new RandomEntry(
new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`),
super.tickLine(user, target, args)
)
}
}

export class Tail extends NormalVoreContainer {
fluidName = new Noun("chyme")

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(new ImproperNoun('tail', 'tails').all, owner, new Set([VoreType.Tail]), capacity, damage)
export class Stomach extends DefaultContainer {
fluid = {
color: new Adjective("green"),
name: new Noun("chyme"),
sound: new Verb("gurgle"),
sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed")
}

tickLine (user: Creature, target: Creature, args: { damage: Damage }) {
return new RandomEntry(
new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`)
)
gas = {
bubbleVerb: new Verb("bubble", "bubbles", "bubbling", "bubbled"),
color: new Adjective("hazy"),
name: new Noun("fume", "fumes"),
releaseVerb: new Verb("belch", "belches", "belching", "belched"),
smell: new Adjective("acrid")
}
}

export class Goo extends NormalVoreContainer {
fluidName = new Noun("goo")
fluidColor = "#66ee66";

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(new ImproperNoun('goo', 'goo').all, owner, new Set([VoreType.Goo]), capacity, damage)
}

tickLine (user: Creature, target: Creature, args: { damage: Damage }) {
return new RandomEntry(
new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`)
)
wall = {
color: new Adjective("red"),
material: new Noun("muscle"),
name: new Noun("wall"),
texture: new Adjective("slimy")
}
}

export class Cock extends NormalVoreContainer {
fluidName = new Noun("cum")
constructor (owner: Creature, capacityFactor: number, damage: DamageFormula) {
super(new Noun("stomach"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([
ContainerCapability.Digest,
ContainerCapability.Absorb
]))

fluidColor = "#eeeeee66";

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(
new ImproperNoun('cock').all,
owner,
new Set([VoreType.Cock]),
capacity,
damage
)
}

tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry {
return new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} with ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`)
this.damage = damage
}
}

export class Balls extends InnerVoreContainer {
fluidName = new Noun("cum")

fluidColor = "#eeeeeecc";

constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) {
super(
new ImproperNoun('ball', 'balls').all.plural,
owner,
new Set([VoreType.Cock]),
capacity,
damage,
escape
)
export class Throat extends DefaultContainer {
fluid = {
color: new Adjective("clear"),
name: new Noun("saliva"),
sound: new Verb("squish", "squishes"),
sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed")
}
}

export class Slit extends NormalVoreContainer {
fluidName = new Noun("femcum")

fluidColor = "#cccccc99";

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(
new ImproperNoun('slit').all,
owner,
new Set([VoreType.Unbirth]),
capacity,
damage
)
wall = {
color: new Adjective("red"),
material: new Noun("muscle"),
name: new Noun("wall"),
texture: new Adjective("slimy")
}
}
export class Womb extends InnerVoreContainer {
fluidName = new Noun("femcum")

fluidColor = "#ddddddbb";

constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) {
super(
new ImproperNoun('womb').all,
owner,
new Set([VoreType.Unbirth]),
capacity,
damage,
escape
)
}
}

export class Breasts extends NormalVoreContainer {
fluidName = new Noun("milk")

fluidColor = "#eeeeeecc";

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(
new ImproperNoun('breast', 'breasts').all.plural,
owner,
new Set([VoreType.Breast]),
capacity,
damage
)
constructor (owner: Creature, capacityFactor: number) {
super(new Noun("throat"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([
ContainerCapability.Consume,
ContainerCapability.Release
]))
}
}

export class Bladder extends NormalVoreContainer {
fluidName = new Noun("piss")

fluidColor = "#eeee3399";

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(
new ImproperNoun('bladder').all,
owner,
new Set([VoreType.Bladder]),
capacity,
damage
)
}
}

export class InnerBladder extends InnerVoreContainer {
fluidName = new Noun("piss")

fluidColor = "#eeee3399";

constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) {
super(
new ImproperNoun('bladder').all,
owner,
new Set([VoreType.Bladder]),
capacity,
damage,
escape
)
}
}

export function biconnectContainers (outer: VoreContainer, inner: VoreContainer): void {
const old = outer.onDigest
outer.onDigest = (prey: Creature) => {
const oldResult = old(prey)
outer.digested = outer.digested.filter(victim => victim !== prey)
inner.digested.push(prey)
return new LogLines(oldResult, inner.consumeLine(inner.owner, prey), inner.tick(0, [prey]))
}

outer.actions.push(
new TransferAction(
outer,
inner
)
)

inner.actions.push(
new TransferAction(
inner,
outer
)
)
}

Завантаження…
Відмінити
Зберегти