Samuel Dweller 3 년 전
부모
커밋
04ddd57033
24개의 변경된 파일1455개의 추가작업 그리고 898개의 파일을 삭제
  1. +7
    -2
      .eslintrc.js
  2. +14
    -0
      src/App.vue
  3. +10
    -1
      src/components/Combat.vue
  4. +6
    -5
      src/components/ContainerView.vue
  5. +0
    -4
      src/components/ItemView.vue
  6. +0
    -3
      src/components/WalletView.vue
  7. +20
    -1
      src/game/ai.ts
  8. +344
    -152
      src/game/combat.ts
  9. +63
    -13
      src/game/combat/actions.ts
  10. +153
    -46
      src/game/combat/effects.ts
  11. +9
    -12
      src/game/creature.ts
  12. +0
    -97
      src/game/creatures/characters/inazuma.ts
  13. +0
    -39
      src/game/creatures/monsters/slime.ts
  14. +95
    -0
      src/game/creatures/monsters/werewolf.ts
  15. +11
    -3
      src/game/creatures/player.ts
  16. +5
    -3
      src/game/events.ts
  17. +2
    -1
      src/game/interface.ts
  18. +213
    -89
      src/game/language.ts
  19. +6
    -25
      src/game/maps/Newtown.ts
  20. +21
    -27
      src/game/maps/town.ts
  21. +119
    -0
      src/game/onomatopoeia.ts
  22. +299
    -370
      src/game/vore.ts
  23. +21
    -5
      src/game/words.ts
  24. +37
    -0
      src/main.ts

+ 7
- 2
.eslintrc.js 파일 보기

@@ -6,7 +6,7 @@ module.exports = {
extends: [
'plugin:vue/essential',
'@vue/standard',
'@vue/typescript/recommended'
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 2020
@@ -18,6 +18,11 @@ module.exports = {
'@typescript-eslint/no-unused-vars': 'off',
quotes: 'off',
'function-paren-newline': ['error', 'multiline-arguments'],
'@typescript-eslint/member-ordering': ['warn']
'@typescript-eslint/member-ordering': ['warn'],
'indent': 'off',
'@typescript-eslint/indent': [
'error',
2
],
}
}

+ 14
- 0
src/App.vue 파일 보기

@@ -193,4 +193,18 @@ html {
/* .component-fade-leave-active below version 2.1.8 */ {
opacity: 0;
}

.onomatopoeia {
font-weight: bold;
font-style: italic;
font-size: 200%;
animation: fly-in 0.4s;
animation-timing-function: ease-out;
display: inline-block;
}

@keyframes fly-in {
0% { transform: scale(0, 0); opacity: 0 };
100% { transform: scale(1, 1); opacity: 1 };
}
</style>

+ 10
- 1
src/components/Combat.vue 파일 보기

@@ -242,6 +242,15 @@ export default class Combat extends Vue {
} else {
this.executedRight(this.encounter.currentMove.ai.decide(this.encounter.currentMove, this.encounter))
}
} else {
if (this.encounter.currentMove.containedIn) {
this.writeLog(
this.encounter.currentMove.containedIn.statusLine(
this.encounter.currentMove.containedIn.owner,
this.encounter.currentMove
)
)
}
}
}
}
@@ -397,7 +406,7 @@ export default class Combat extends Vue {
}

.left-stats {
flex-direction: row;
flex-direction: row-reverse;
}

.right-stats {


+ 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(),


+ 344
- 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))
@@ -405,24 +481,69 @@ export abstract class Action {
}

try (user: Creature, targets: Array<Creature>): LogEntry {
// Check if any pre-action effect will cancel this action.
const preActionResults = user.effects.mapUntil(effect => effect.preAction(user), result => result.prevented)
const preActionLogs = new LogLines(...preActionResults.map(result => result.log))

if (preActionResults.some(result => result.prevented)) {
return preActionLogs
}

// Check if any pre-receive-action effect will cancel this action.
const preReceiveActionResults = targets.mapUntil(target => {
const outcome = target.effects.mapUntil(effect => effect.preReceiveAction(user, target), result => result.prevented)

return outcome
}, results => results.some(result => result.prevented))

console.log(preReceiveActionResults)

const preReceiveActionLogs = new LogLines(...preReceiveActionResults.flatMap(
target => target.map(result => result.log)
))

if (preReceiveActionResults.some(results => results.some(result => result.prevented))) {
return new LogLines(
preActionLogs,
preReceiveActionLogs
)
}

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(
preActionLogs,
preReceiveActionLogs,
...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 +553,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 +567,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 +594,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 +646,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 +670,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 +690,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 +721,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 +741,56 @@ 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
}

/**
* Triggers after prey enters a container
*/
postEnter (
predator: Creature,
prey: Creature,
container: Container
): LogEntry {
return nilLog
}

@@ -653,24 +825,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 = ''

constructor (public name: TextLike, public desc: TextLike, public icon: string) {
topLeft = ""
bottomRight = ""

}
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 = {
@@ -704,7 +887,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)
})

@@ -714,12 +899,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
@@ -728,24 +919,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)
}
}

@@ -755,8 +943,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]
@@ -768,8 +960,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]
@@ -780,27 +976,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
}

+ 63
- 13
src/game/combat/actions.ts 파일 보기

@@ -4,9 +4,10 @@ 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'
import * as Words from '@/game/words'

/**
* The PassAction has no effect.
@@ -216,8 +217,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.exit(user), this.to.enter(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',
@@ -234,8 +277,12 @@ export class RubAction extends Action {
}

execute (user: Creature, target: Creature): LogEntry {
const results = this.container.tick(60)
return new LogLines(results)
const results: Array<LogEntry> = []
results.push(new LogLine(
`${user.name.capital} ${user.name.conjugate(Words.Rub)} ${user.pronouns.possessive} ${Words.Full} ${this.container.name}.`
))
results.push(this.container.tick(60))
return new LogLines(...results)
}

describe (user: Creature, target: Creature): LogEntry {
@@ -272,17 +319,15 @@ 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) => this.to.description(this.from, this.to.destination, target)

allowed (user: Creature, target: Creature) {
if (target.containedIn === this.from && this.from.contents.includes(target)) {
@@ -293,13 +338,18 @@ export class TransferAction extends Action {
}

execute (user: Creature, target: Creature): LogEntry {
this.from.release(target)
this.to.consume(target)
return this.line(user, target, { from: this.from, to: this.to })
const results = [
this.from.exit(target),
this.line(user, target, { from: this.from, to: this.to }),
this.to.destination.enter(target)

]

return new LogLines(...results)
}

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



+ 153
- 46
src/game/combat/effects.ts 파일 보기

@@ -1,13 +1,22 @@
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, ContainerCapability } from "@/game/vore"
import * as Words from "@/game/words"
import * as Onomatopoeia from "@/game/onomatopoeia"

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 +24,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 +36,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 +49,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 +69,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 +79,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 +90,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 +127,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,20 +158,31 @@ 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) {
return {
prevented: true,
log: new LogLine(`${creature.name.capital} cannot be attacked.`)
preReceiveAction (user: Creature, target: Creature) {
if (user !== target) {
return {
prevented: true,
log: new LogLine(`${target.name.capital} cannot be attacked.`)
}
} else {
return {
prevented: false,
log: nilLog
}
}
}
}

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 +190,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 +205,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 +240,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 +254,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 +308,26 @@ 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) {
postEnter (predator: Creature, prey: Creature, container: Container) {
if (!container.capabilities.has(ContainerCapability.Digest)) {
return nilLog
}
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"),
Onomatopoeia.makeOnomatopoeia(Onomatopoeia.Crunch),
container.tick(0, [prey])
)
}


+ 9
- 12
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

@@ -42,12 +41,14 @@ export class Creature extends Entity {
desc = "Some creature";

get effects (): Array<Effective> {
return (this.statusEffects as Effective[]).concat(
const effects: Array<Effective> = (this.statusEffects as Effective[]).concat(
Object.values(this.equipment).filter(item => item !== undefined).flatMap(
item => (item as Equipment).effects
),
this.perks
)

return effects
}

statusEffects: Array<StatusEffect> = [];
@@ -81,7 +82,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 +112,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 +207,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)
}
@@ -265,6 +262,7 @@ export class Creature extends Entity {
this.statusEffects.forEach(effect => {
results.push(effect)
})

return results
}

@@ -273,7 +271,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)
}
}

+ 95
- 0
src/game/creatures/monsters/werewolf.ts 파일 보기

@@ -0,0 +1,95 @@
import { VoreAI } from '@/game/ai'
import { DamageType, Side, Stat, StatDamageFormula, StatusEffect, Vigor } from '@/game/combat'
import { DigestionPowerEffect } from '@/game/combat/effects'
import { Creature } from '@/game/creature'
import { LogEntry, LogLine, nilLog } from '@/game/interface'
import { ImproperNoun, MalePronouns, ObjectPronouns, Preposition, Verb } from '@/game/language'
import { anyVore, ConnectionDirection, Container, Stomach, Throat, transferDescription } from '@/game/vore'
import * as Words from '@/game/words'
import * as Onomatopoeia from '@/game/onomatopoeia'

export default class Werewolf extends Creature {
constructor () {
super(
new ImproperNoun("werewolf", "werewolves"),
new ImproperNoun("werewolf", "werewolves"),
MalePronouns,
{
Power: 45,
Toughness: 30,
Agility: 25,
Reflexes: 25,
Charm: 10,
Willpower: 15
},
anyVore,
anyVore,
75
)

const throat = new Throat(
this,
25
)

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

stomach.effects.push(new class extends StatusEffect {
constructor () {
super(
"Pinned",
"Prey sometimes can't move.",
"fas fa-sun"
)
}

onApply (creature: Creature): LogEntry {
return new LogLine(
`${stomach.owner.name.capital.possessive} ${stomach.name} is incredibly tight, gripping ${creature.name.objective} like a vice!`
)
}

preAction (creature: Creature): { prevented: boolean; log: LogEntry } {
if (Math.random() < 0.5) {
return {
prevented: true,
log: new LogLine(`${creature.name.capital} can't move!`)
}
} else {
return {
prevented: false,
log: nilLog
}
}
}
}())

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

throat.connect({
destination: stomach,
direction: ConnectionDirection.Deeper,
description: transferDescription(Words.Swallow, new Preposition("down"))
})

stomach.connect({
destination: throat,
direction: ConnectionDirection.Shallower,
description: transferDescription(new Verb("hork"), new Preposition("up"))
})

stomach.voreRelay.subscribe("onDigested", (sender, args) => {
return Onomatopoeia.makeOnomatopoeia(Onomatopoeia.Burp)
})

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.Second

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

+ 5
- 3
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
@@ -43,13 +43,15 @@ abstract class Relay<Sender, EventMap extends { [name: string]: any }> {
type VoreMap = {
"onEaten": { prey: Creature };
"onReleased": { prey: Creature };
"onEntered": { prey: Creature };
"onExited": { prey: Creature };
"onDigested": { prey: Creature };
"onAbsorbed": { prey: Creature };
}

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



+ 2
- 1
src/game/interface.ts 파일 보기

@@ -55,7 +55,8 @@ export class LogLines implements LogEntry {

export enum FormatOpt {
Damage = "log-damage",
DamageInst = "damage-instance"
DamageInst = "damage-instance",
Onomatopoeia = "onomatopoeia"
}

/**


+ 213
- 89
src/game/language.ts 파일 보기

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

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 +14,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 +69,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 +83,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 +231,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 +257,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 +289,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 +340,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 +383,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 +408,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 +441,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 +500,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 +525,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)
@@ -497,54 +575,95 @@ export class Pronoun implements Pluralizable {
}
}

export type OnomatopoeiaPart = [string, number, number]

export class Onomatopoeia extends Word {
constructor (protected parts: Array<OnomatopoeiaPart>, public opts: WordOptions = emptyConfig) {
super(opts)
}

configure (opts: WordOptions): Word {
return new Onomatopoeia(this.parts, opts)
}

toString (): string {
const built = this.parts.reduce((result, next) => {
const [piece, min, max] = next
const count = Math.floor(Math.random() * (max - min)) + min
for (let i = 0; i < count; i++) {
result += piece
}
return result
}, "")

return built
}
}

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

export const FirstPersonPronouns = new Pronoun({
subjective: 'I',
objective: 'me',
possessive: 'my',
reflexive: 'myself'
})
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 +678,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()
}


+ 6
- 25
src/game/maps/Newtown.ts 파일 보기

@@ -9,10 +9,9 @@ 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 Samuel from '../creatures/characters/Samuel'
import Human from '../creatures/human'
import Slime from '../creatures/monsters/slime'
import Werewolf from '../creatures/monsters/werewolf'

function makeParty (): Creature[] {
const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, {
@@ -122,14 +121,14 @@ export const Newtown = (): Place => {
)
deepwoods.choices.push(
new Choice(
"Fight Inazuma",
"Go fight Inazuma!",
"Fight Werewolf",
"Go fight Werewolf!",
(world, executor) => {
const enemy = new Inazuma()
const enemy = new Werewolf()
const encounter = new Encounter(
{
name: "Fight some tough nerd",
intro: () => new LogLine(`Inazuma Approaches!`)
intro: () => new LogLine(`Werewolf Approaches!`)
},
[world.player, enemy].concat(world.party)
)
@@ -141,7 +140,7 @@ export const Newtown = (): Place => {
const bossEncounters = [
new Encounter(
{ name: "Inazuma", intro: () => nilLog },
makeParty().concat([new Inazuma()])
makeParty().concat([new Werewolf()])
)
]

@@ -461,24 +460,6 @@ export const Newtown = (): 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(


+ 21
- 27
src/game/maps/town.ts 파일 보기

@@ -1,5 +1,5 @@
import { Place, Choice, Direction, World } from '@/game/world'
import { ProperNoun, ImproperNoun, MalePronouns, FemalePronouns, TheyPronouns } from '@/game/language'
import { ProperNoun, ImproperNoun, MalePronouns, FemalePronouns, TheyPronouns, POV } from '@/game/language'
import { Encounter, Stat, Damage, DamageType, Vigor, Side } from '@/game/combat'
import * as Items from '@/game/items'
import { LogLine, nilLog, LogLines } from '@/game/interface'
@@ -9,10 +9,9 @@ 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 Samuel from '../creatures/characters/Samuel'
import Werewolf from '../creatures/monsters/werewolf'

function makeParty (): Creature[] {
const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, {
@@ -103,13 +102,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",
@@ -157,6 +149,20 @@ export const Town = (): Place => {
)
)

home.choices.push(
new Choice(
"Become a werewolf",
"Yum",
(world, executor) => {
world.player = new Werewolf()
world.player.location = home
world.player.perspective = POV.Second
world.player.side = Side.Heroes
return new LogLine("Nice")
}
)
)

square.choices.push(
new Choice(
"Eat someone",
@@ -228,19 +234,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",
@@ -324,14 +317,15 @@ export const Town = (): Place => {

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


+ 119
- 0
src/game/onomatopoeia.ts 파일 보기

@@ -0,0 +1,119 @@
import { FormatEntry, FormatOpt, LogEntry, LogLine } from "./interface"
import { Onomatopoeia, RandomWord, Word } from "./language"

export function makeOnomatopoeia (word: Word): LogEntry {
return new FormatEntry(new LogLine(`${word}`), FormatOpt.Onomatopoeia)
}
export const Swallow = new RandomWord([
new Onomatopoeia([
["GL", 1, 1],
["U", 1, 10],
["R", 1, 3],
["K", 1, 1],
["!", 1, 3]
]),
new Onomatopoeia([
["GL", 1, 1],
["U", 1, 3],
["NK", 1, 1],
["!", 1, 3]
]),
new Onomatopoeia([
["G", 1, 1],
["L", 1, 3],
["U", 1, 3],
["RSH", 1, 1],
["!", 1, 3]
])
])

export const Glunk = new RandomWord([
new Onomatopoeia([
["G", 1, 1],
["L", 2, 4],
["U", 1, 3],
["NK", 1, 1],
["!", 1, 1]
]),
new Onomatopoeia([
["GL", 1, 1],
["O", 2, 5],
["RSH", 1, 1],
["!", 1, 1]
])
])

export const Gurgle = new RandomWord([
new Onomatopoeia([
["g", 1, 1],
["w", 1, 3],
["o", 1, 3],
["r", 1, 2],
["b", 1, 3],
["le", 1, 1]
]),
new Onomatopoeia([
["g", 2, 5],
["r", 3, 7],
["rg", 1, 1],
["l", 1, 3],
["e", 1, 1]
])
])

export const MuffledScream = new RandomWord([
new Onomatopoeia([
["m", 5, 9],
["p", 2, 4],
["h", 4, 8]
]),
new Onomatopoeia([
["h", 4, 9],
["m", 3, 8],
["p", 1, 3],
["h", 3, 5]
]),
new Onomatopoeia([
["n", 4, 9],
["g", 3, 5],
["h", 5, 10]
])
])

export const Crunch = new RandomWord([
new Onomatopoeia([
["C", 1, 1],
["R", 4, 8],
["U", 1, 3],
["N", 2, 4],
["CH", 1, 1],
["!", 1, 1]
])
])

export const Burp = new RandomWord([
new Onomatopoeia([
["B", 1, 1],
["U", 5, 12],
["R", 3, 6],
["P", 1, 1],
["!", 1, 3]
]),
new Onomatopoeia([
["H", 1, 1],
["U", 1, 4],
["O", 4, 6],
["R", 2, 3],
["P", 4, 6],
["H", 2, 3],
["!", 1, 3]
]),
new Onomatopoeia([
["B", 1, 1],
["W", 1, 2],
["A", 6, 11],
["R", 2, 4],
["P", 2, 3],
["!", 1, 3]
])
])

+ 299
- 370
src/game/vore.ts 파일 보기

@@ -1,48 +1,87 @@
import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula } from '@/game/combat'
import { LogLines, LogEntry, LogLine, nilLog, RandomEntry } from '@/game/interface'
import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition } from '@/game/language'
import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from '@/game/combat/actions'
import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula, ConstantDamageFormula, StatusEffect } from '@/game/combat'
import { LogLines, LogEntry, LogLine, nilLog, RandomEntry, FormatEntry, FormatOpt } from '@/game/interface'
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 * as Onomatopoeia from '@/game/onomatopoeia'
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;
exit: Noun;
}

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

export enum ConnectionDirection {
Deeper,
Neutral,
Shallower
}

export type Connection = {
destination: Container;
direction: ConnectionDirection;
description: (to: Container, from: Container, prey: Creature) => LogEntry;
}

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

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

effects: Array<StatusEffect>;

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

voreRelay: VoreRelay;

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

damage: DamageFormula;

canTake: (prey: Creature) => boolean;
consume: (prey: Creature) => LogEntry;
release: (prey: Creature) => LogEntry;
struggle: (prey: Creature) => LogEntry;
sound: Word;
capacity: number;
fullness: number;

consumeVerb: Verb;
consumePreposition: Preposition;
@@ -51,35 +90,91 @@ export interface Container extends Actionable {
struggleVerb: Verb;
strugglePreposition: Preposition;

canTake (prey: Creature): boolean;

consume (prey: Creature): LogEntry;
release (prey: Creature): LogEntry;

enter (prey: Creature): LogEntry;
exit (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()

effects: Array<StatusEffect> = []

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))
}
if (capabilities.has(ContainerCapability.Digest)) {
this.actions.push(new RubAction(this))
}
}

connect (connection: Connection): void {
this.connections.push(connection)

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

get capacity (): number {
return this.capacityFactor * this.owner.voreStats.Mass
}

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}.`)
statusLine (user: Creature, target: Creature): LogEntry {
return new LogLine(
`${target.name.capital} ${target.name.conjugate(new ToBe())} ${Words.Stuck} inside ${user.name.possessive} ${this.name}.`
)
}

releaseLine (user: Creature, target: Creature): LogEntry {
@@ -91,7 +186,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 {
@@ -105,22 +200,64 @@ export abstract class NormalContainer implements Container {
}

consume (prey: Creature): LogEntry {
const results: Array<LogEntry> = [
this.enter(prey),
this.voreRelay.dispatch("onEaten", this, { prey: prey }),
prey.voreRelay.dispatch("onEaten", this, { prey: prey }),
this.consumeLine(this.owner, prey)
]

this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this)))

return new LogLines(...results)
}

release (prey: Creature): LogEntry {
const results = [
this.exit(prey),
this.releaseLine(this.owner, prey),
this.voreRelay.dispatch("onReleased", this, { prey: prey }),
prey.voreRelay.dispatch("onReleased", this, { prey: prey })
]

return new LogLines(...results)
}

enter (prey: Creature): LogEntry {
if (prey.containedIn !== null) {
prey.containedIn.contents = prey.containedIn.contents.filter(item => prey !== item)
}
this.contents.push(prey)
prey.containedIn = this
return this.consumeLine(this.owner, prey)

const effectResults = this.effects.map(effect => prey.applyEffect(effect))

const results = [
this.voreRelay.dispatch("onEntered", this, { prey: prey }),
prey.voreRelay.dispatch("onEntered", this, { prey: prey })
]

this.owner.effects.forEach(effect => results.push(effect.postEnter(this.owner, prey, this)))

return new LogLines(...results, ...effectResults)
}

release (prey: Creature): LogEntry {
exit (prey: Creature): LogEntry {
prey.containedIn = this.owner.containedIn
this.contents = this.contents.filter(victim => victim !== prey)

if (this.owner.containedIn !== null) {
this.owner.containedIn.contents.push(prey)
}
return this.releaseLine(this.owner, prey)

const effectResults = this.effects.map(effect => prey.removeEffect(effect))

const results = [
this.voreRelay.dispatch("onExited", this, { prey: prey }),
prey.voreRelay.dispatch("onExited", this, { prey: prey })
]

return new LogLines(...results, ...effectResults)
}

struggle (prey: Creature): LogEntry {
@@ -136,79 +273,27 @@ 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;
}

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
describeDetail (prey: Creature): LogEntry {
const lines: Array<LogLine> = []

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}.`)
)
}
if (this.wall) {
lines.push(
new LogLine(`The ${this.wall.color} walls ${prey.name.conjugate(Words.Clench)} over ${prey.name.objective} like a vice.`)
)
}

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) {
@@ -219,15 +304,29 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor
}

tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry {
return new RandomEntry(
new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Churns)} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`),
const options = [
new LogLine(`${user.name.capital} ${Words.Churns.singular} ${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(`${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(), `.`)
)
new LogLine(`${target.name.capital} ${target.name.conjugate(Words.Struggle)} ${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}.`))
}

const result: Array<LogEntry> = [
new RandomEntry(...options)
]

if (Math.random() < 0.3) {
result.push(new FormatEntry(new LogLine(`${Onomatopoeia.Gurgle}`), FormatOpt.Onomatopoeia))
}

return new LogLines(...result)
}

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} 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 {
@@ -242,23 +341,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 (prey.vigors[Vigor.Health] <= 0) {
prey.destroyed = true
this.digested.push(prey)
justDigested.push(prey)
damageResults.push(this.onDigest(prey))
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))
}
}
}
})
})
}

const tickedEntries = new LogLines(...tickedEntryList)

@@ -302,19 +403,13 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor
}

digest (preys: Creature[]): LogEntry {
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 }))
const results = preys.map(prey => this.digestLine(this.owner, prey))
if (preys.length > 0 && this.gas) {
results.push(new LogLine(
`A crass ${this.gas.releaseVerb} escapes ${this.owner.name.possessive} ${this.gas.exit} as ${this.owner.name.possessive} prey is digested, spewing ${this.gas.color} ${this.gas.name}.`
))
}
return new LogLines(...results)
}

onAbsorb (prey: Creature): LogEntry {
@@ -326,243 +421,77 @@ 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 Stomach extends DefaultContainer {
fluid = {
color: new Adjective("green"),
name: new Noun("chyme"),
sound: new Verb("gurgle"),
sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed")
}
}

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)
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"),
exit: new Noun("jaws")
}

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)
)
wall = {
color: new Adjective("red"),
material: new Noun("muscle"),
name: new Noun("wall"),
texture: new Adjective("slimy")
}
}

export class Tail extends NormalVoreContainer {
fluidName = new Noun("chyme")
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
]))

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(new ImproperNoun('tail', 'tails').all, owner, new Set([VoreType.Tail]), 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(), `.`)
)
}
}

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

export class Cock extends NormalVoreContainer {
fluidName = new Noun("cum")

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

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

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

fluidColor = "#eeee3399";
this.voreRelay.subscribe("onEntered", (sender: Container, args: { prey: Creature }) => {
return new FormatEntry(new LogLine(`${Onomatopoeia.Glunk}`), FormatOpt.Onomatopoeia)
})

constructor (owner: Creature, capacity: number, damage: DamageFormula) {
super(
new ImproperNoun('bladder').all,
owner,
new Set([VoreType.Bladder]),
capacity,
damage
)
this.damage = 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 class Throat extends DefaultContainer {
fluid = {
color: new Adjective("clear"),
name: new RandomWord([
new Noun("saliva"),
new Noun("drool"),
new Noun("slobber")
]),
sound: new Verb("squish", "squishes"),
sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed")
}

wall = {
color: new Adjective("red"),
material: new Noun("muscle"),
name: new Noun("wall"),
texture: new Adjective("slimy")
}

constructor (owner: Creature, capacityFactor: number) {
super(new Noun("throat"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([
ContainerCapability.Consume,
ContainerCapability.Release
]))

this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => {
return new FormatEntry(new LogLine(`${Onomatopoeia.Swallow}`), FormatOpt.Onomatopoeia)
})
}
}

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]))
export function transferDescription (verb: Word, preposition: Preposition): ((from: Container, to: Container, prey: Creature) => LogEntry) {
return (from: Container, to: Container, prey: Creature) => {
return new LogLine(`${from.owner.name.capital} ${from.owner.name.conjugate(verb.singular)} ${prey.name.objective} ${preposition} ${to.consumePreposition} ${from.owner.pronouns.possessive} ${to.name}.`)
}

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

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

+ 21
- 5
src/game/words.ts 파일 보기

@@ -21,8 +21,7 @@ export const Slick = new RandomWord([

export const Swallow = new RandomWord([
new Verb("swallow"),
new Verb("gulp"),
new Verb("consume", "consumes", "consuming", "consumed")
new Verb("gulp")
])

export const Churns = new RandomWord([
@@ -45,10 +44,8 @@ export const Dark = new RandomWord([

export const Digest = new RandomWord([
new Verb("digest"),
new Verb("melt down", "melts down", "melting down", "melted down"),
new Verb("dissolve", "dissolves", "dissolving", "dissolved"),
new Verb("mulch", "mulches", "mulching", "mulched"),
new Verb("break down", "breaks down", "breaking down", "broken down")
new Verb("mulch", "mulches", "mulching", "mulched")
])

export const Absorb = new RandomWord([
@@ -120,3 +117,22 @@ export const Sloppily = new RandomWord([
new Adverb("sloppily"),
new Adverb("messily")
])

export const Stuck = new RandomWord([
new Adjective("stuck"),
new Adjective("trapped"),
new Adjective("imprisoned")
])

export const Rub = new RandomWord([
new Verb("rub", "rubs", "rubbing", "rubbed"),
new Verb("knead", "kneads", "kneading", "kneaded"),
new Verb("press over", "presses over", "pressing over", "pressed over")
])

export const Full = new RandomWord([
new Adjective("full"),
new Adjective("stuffed"),
new Adjective("packed"),
new Adjective("bulging")
])

+ 37
- 0
src/main.ts 파일 보기

@@ -6,6 +6,8 @@ declare global {
joinGeneral (item: T, endItem: T|null): Array<T>;
/* eslint-disable-next-line */
unique (predicate?: (elem: T) => any): Array<T>;
findResult<U> (func: (elem: T) => U, predicate: (result: U) => boolean): U | undefined;
mapUntil<U> (func: (elem: T) => U, predicate: (result: U) => boolean): Array<U>;
}
}

@@ -33,6 +35,41 @@ Array.prototype.unique = function<T> (predicate?: (elem: T) => any): Array<T> {
return result
}

/* eslint-disable-next-line */
Array.prototype.findResult = function<T,U> (func: (elem: T) => U, predicate: (result: U) => boolean): U | undefined {
for (let i = 0; i < this.length; i++) {
const elem: T = this[i]
const result: U = func(elem)
const decision = predicate(result)
if (decision) {
return result
}
}
return undefined
}

/**
* Maps over the array until the predicate is satisfied.
*/
/* eslint-disable-next-line */
Array.prototype.mapUntil = function<T,U> (func: (elem: T) => U, predicate: (result: U) => boolean): Array<U> {
const results: Array<U> = []

for (let i = 0; i < this.length; i++) {
const elem: T = this[i]
const result: U = func(elem)
const decision = predicate(result)

results.push(result)

if (decision) {
break
}
}

return results
}

Vue.config.productionTip = false

new Vue({


불러오는 중...
취소
저장