Everything is now a Container; Containers have a set of capabilities that describes what they can domaster
| @@ -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) | |||
| } | |||
| @@ -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({ | |||
| @@ -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({ | |||
| @@ -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(), | |||
| @@ -1,10 +1,20 @@ | |||
| import { Creature } from "./creature" | |||
| import { TextLike } from '@/game/language' | |||
| import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog, Newline } from '@/game/interface' | |||
| import { World } from '@/game/world' | |||
| import { TestCategory } from '@/game/combat/tests' | |||
| import { VoreContainer } from '@/game/vore' | |||
| import { SoloTargeter } from '@/game/combat/targeters' | |||
| import { TextLike } from "@/game/language" | |||
| import { | |||
| LogEntry, | |||
| LogLines, | |||
| FAElem, | |||
| LogLine, | |||
| FormatEntry, | |||
| FormatOpt, | |||
| PropElem, | |||
| nilLog, | |||
| Newline | |||
| } from "@/game/interface" | |||
| import { World } from "@/game/world" | |||
| import { TestCategory } from "@/game/combat/tests" | |||
| import { Container } from "@/game/vore" | |||
| import { SoloTargeter } from "@/game/combat/targeters" | |||
| export enum DamageType { | |||
| Pierce = "Pierce", | |||
| @@ -29,19 +39,19 @@ export enum Vigor { | |||
| Resolve = "Resolve" | |||
| } | |||
| export const VigorIcons: {[key in Vigor]: string} = { | |||
| export const VigorIcons: { [key in Vigor]: string } = { | |||
| Health: "fas fa-heart", | |||
| Stamina: "fas fa-bolt", | |||
| Resolve: "fas fa-brain" | |||
| } | |||
| export const VigorDescs: {[key in Vigor]: string} = { | |||
| export const VigorDescs: { [key in Vigor]: string } = { | |||
| Health: "How much damage you can take", | |||
| Stamina: "How much energy you have", | |||
| Resolve: "How much dominance you can resist" | |||
| } | |||
| export type Vigors = {[key in Vigor]: number} | |||
| export type Vigors = { [key in Vigor]: number } | |||
| export enum Stat { | |||
| Toughness = "Toughness", | |||
| @@ -52,9 +62,9 @@ export enum Stat { | |||
| Charm = "Charm" | |||
| } | |||
| export type Stats = {[key in Stat]: number} | |||
| export type Stats = { [key in Stat]: number } | |||
| export const StatToVigor: {[key in Stat]: Vigor} = { | |||
| export const StatToVigor: { [key in Stat]: Vigor } = { | |||
| Toughness: Vigor.Health, | |||
| Power: Vigor.Health, | |||
| Reflexes: Vigor.Stamina, | |||
| @@ -63,22 +73,22 @@ export const StatToVigor: {[key in Stat]: Vigor} = { | |||
| Charm: Vigor.Resolve | |||
| } | |||
| export const StatIcons: {[key in Stat]: string} = { | |||
| Toughness: 'fas fa-heartbeat', | |||
| Power: 'fas fa-fist-raised', | |||
| Reflexes: 'fas fa-stopwatch', | |||
| Agility: 'fas fa-feather', | |||
| Willpower: 'fas fa-book', | |||
| Charm: 'fas fa-comments' | |||
| export const StatIcons: { [key in Stat]: string } = { | |||
| Toughness: "fas fa-heartbeat", | |||
| Power: "fas fa-fist-raised", | |||
| Reflexes: "fas fa-stopwatch", | |||
| Agility: "fas fa-feather", | |||
| Willpower: "fas fa-book", | |||
| Charm: "fas fa-comments" | |||
| } | |||
| export const StatDescs: {[key in Stat]: string} = { | |||
| Toughness: 'Your brute resistance', | |||
| Power: 'Your brute power', | |||
| Reflexes: 'Your ability to dodge', | |||
| Agility: 'Your ability to move quickly', | |||
| Willpower: 'Your mental resistance', | |||
| Charm: 'Your mental power' | |||
| export const StatDescs: { [key in Stat]: string } = { | |||
| Toughness: "Your brute resistance", | |||
| Power: "Your brute power", | |||
| Reflexes: "Your ability to dodge", | |||
| Agility: "Your ability to move quickly", | |||
| Willpower: "Your mental resistance", | |||
| Charm: "Your mental power" | |||
| } | |||
| export enum VoreStat { | |||
| @@ -87,15 +97,15 @@ export enum VoreStat { | |||
| Prey = "Prey" | |||
| } | |||
| export type VoreStats = {[key in VoreStat]: number} | |||
| export type VoreStats = { [key in VoreStat]: number } | |||
| export const VoreStatIcons: {[key in VoreStat]: string} = { | |||
| export const VoreStatIcons: { [key in VoreStat]: string } = { | |||
| [VoreStat.Mass]: "fas fa-weight", | |||
| [VoreStat.Bulk]: "fas fa-weight-hanging", | |||
| [VoreStat.Prey]: "fas fa-utensils" | |||
| } | |||
| export const VoreStatDescs: {[key in VoreStat]: string} = { | |||
| export const VoreStatDescs: { [key in VoreStat]: string } = { | |||
| [VoreStat.Mass]: "How much you weigh", | |||
| [VoreStat.Bulk]: "Your weight, plus the weight of your prey", | |||
| [VoreStat.Prey]: "How many creatures you've got inside of you" | |||
| @@ -109,7 +119,7 @@ export interface CombatTest { | |||
| } | |||
| export interface Targeter { | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature>; | |||
| targets(primary: Creature, encounter: Encounter): Array<Creature>; | |||
| } | |||
| /** | |||
| @@ -142,28 +152,46 @@ export class Damage { | |||
| } | |||
| toString (): string { | |||
| return this.damages.map(damage => damage.amount + " " + damage.type).join("/") | |||
| return this.damages | |||
| .map(damage => damage.amount + " " + damage.type) | |||
| .join("/") | |||
| } | |||
| render (): LogEntry { | |||
| return new LogLine(...this.damages.flatMap(instance => { | |||
| if (instance.target in Vigor) { | |||
| return [instance.amount.toString(), new FAElem(VigorIcons[instance.target as Vigor]), " " + instance.type] | |||
| } else if (instance.target in Stat) { | |||
| return [instance.amount.toString(), new FAElem(StatIcons[instance.target as Stat]), " " + instance.type] | |||
| } else { | |||
| // this should never happen! | |||
| return [] | |||
| } | |||
| })) | |||
| return new LogLine( | |||
| ...this.damages.flatMap(instance => { | |||
| if (instance.target in Vigor) { | |||
| return [ | |||
| instance.amount.toString(), | |||
| new FAElem(VigorIcons[instance.target as Vigor]), | |||
| " " + instance.type | |||
| ] | |||
| } else if (instance.target in Stat) { | |||
| return [ | |||
| instance.amount.toString(), | |||
| new FAElem(StatIcons[instance.target as Stat]), | |||
| " " + instance.type | |||
| ] | |||
| } else { | |||
| // this should never happen! | |||
| return [] | |||
| } | |||
| }) | |||
| ) | |||
| } | |||
| // TODO is there a way to do this that will satisfy the typechecker? | |||
| renderShort (): LogEntry { | |||
| /* eslint-disable-next-line */ | |||
| const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {}) | |||
| const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { | |||
| total[key] = 0 | |||
| return total | |||
| }, {}) | |||
| /* eslint-disable-next-line */ | |||
| const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {}) | |||
| const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { | |||
| total[key] = 0 | |||
| return total | |||
| }, {}) | |||
| this.damages.forEach(instance => { | |||
| const factor = instance.type === DamageType.Heal ? -1 : 1 | |||
| if (instance.target in Vigor) { | |||
| @@ -173,16 +201,31 @@ export class Damage { | |||
| } | |||
| }) | |||
| const vigorEntries = Object.keys(Vigor).flatMap(key => vigorTotals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), ' ']) | |||
| const statEntries = Object.keys(Stat).flatMap(key => statTotals[key as Stat] === 0 ? [] : [new PropElem(key as Stat, statTotals[key as Stat]), ' ']) | |||
| return new FormatEntry(new LogLine(...vigorEntries.concat(statEntries)), FormatOpt.DamageInst) | |||
| const vigorEntries = Object.keys(Vigor).flatMap(key => | |||
| vigorTotals[key as Vigor] === 0 | |||
| ? [] | |||
| : [new PropElem(key as Vigor, vigorTotals[key as Vigor]), " "]) | |||
| const statEntries = Object.keys(Stat).flatMap(key => | |||
| statTotals[key as Stat] === 0 | |||
| ? [] | |||
| : [new PropElem(key as Stat, statTotals[key as Stat]), " "]) | |||
| return new FormatEntry( | |||
| new LogLine(...vigorEntries.concat(statEntries)), | |||
| FormatOpt.DamageInst | |||
| ) | |||
| } | |||
| nonzero (): boolean { | |||
| /* eslint-disable-next-line */ | |||
| const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {}) | |||
| const vigorTotals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { | |||
| total[key] = 0 | |||
| return total | |||
| }, {}) | |||
| /* eslint-disable-next-line */ | |||
| const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { total[key] = 0; return total }, {}) | |||
| const statTotals: Stats = Object.keys(Stat).reduce((total: any, key) => { | |||
| total[key] = 0 | |||
| return total | |||
| }, {}) | |||
| this.damages.forEach(instance => { | |||
| const factor = instance.type === DamageType.Heal ? -1 : 1 | |||
| if (instance.target in Vigor) { | |||
| @@ -191,7 +234,10 @@ export class Damage { | |||
| statTotals[instance.target as Stat] += factor * instance.amount | |||
| } | |||
| }) | |||
| return Object.values(vigorTotals).some(v => v !== 0) || Object.values(statTotals).some(v => v !== 0) | |||
| return ( | |||
| Object.values(vigorTotals).some(v => v !== 0) || | |||
| Object.values(statTotals).some(v => v !== 0) | |||
| ) | |||
| } | |||
| } | |||
| @@ -199,22 +245,26 @@ export class Damage { | |||
| * Computes damage given the source and target of the damage. | |||
| */ | |||
| export interface DamageFormula { | |||
| calc (user: Creature, target: Creature): Damage; | |||
| describe (user: Creature, target: Creature): LogEntry; | |||
| explain (user: Creature): LogEntry; | |||
| calc(user: Creature, target: Creature): Damage; | |||
| describe(user: Creature, target: Creature): LogEntry; | |||
| explain(user: Creature): LogEntry; | |||
| } | |||
| export class CompositeDamageFormula implements DamageFormula { | |||
| constructor (private formulas: DamageFormula[]) { | |||
| } | |||
| constructor (private formulas: DamageFormula[]) {} | |||
| calc (user: Creature, target: Creature): Damage { | |||
| return this.formulas.reduce((total: Damage, next: DamageFormula) => total.combine(next.calc(user, target)), new Damage()) | |||
| return this.formulas.reduce( | |||
| (total: Damage, next: DamageFormula) => | |||
| total.combine(next.calc(user, target)), | |||
| new Damage() | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines(...this.formulas.map(formula => formula.describe(user, target))) | |||
| return new LogLines( | |||
| ...this.formulas.map(formula => formula.describe(user, target)) | |||
| ) | |||
| } | |||
| explain (user: Creature): LogEntry { | |||
| @@ -226,9 +276,7 @@ export class CompositeDamageFormula implements DamageFormula { | |||
| * Simply returns the damage it was given. | |||
| */ | |||
| export class ConstantDamageFormula implements DamageFormula { | |||
| constructor (private damage: Damage) { | |||
| } | |||
| constructor (private damage: Damage) {} | |||
| calc (user: Creature, target: Creature): Damage { | |||
| return this.damage | |||
| @@ -239,7 +287,7 @@ export class ConstantDamageFormula implements DamageFormula { | |||
| } | |||
| explain (user: Creature): LogEntry { | |||
| return new LogLine('Deal ', this.damage.renderShort()) | |||
| return new LogLine("Deal ", this.damage.renderShort()) | |||
| } | |||
| } | |||
| @@ -247,12 +295,12 @@ export class ConstantDamageFormula implements DamageFormula { | |||
| * Randomly scales the damage it was given with a factor of (1-x) to (1+x) | |||
| */ | |||
| export class UniformRandomDamageFormula implements DamageFormula { | |||
| constructor (private damage: Damage, private variance: number) { | |||
| } | |||
| constructor (private damage: Damage, private variance: number) {} | |||
| calc (user: Creature, target: Creature): Damage { | |||
| return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1) | |||
| return this.damage.scale( | |||
| Math.random() * this.variance * 2 - this.variance + 1 | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| @@ -260,7 +308,13 @@ export class UniformRandomDamageFormula implements DamageFormula { | |||
| } | |||
| explain (user: Creature): LogEntry { | |||
| return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), '.') | |||
| return new LogLine( | |||
| "Deal between ", | |||
| this.damage.scale(1 - this.variance).renderShort(), | |||
| " and ", | |||
| this.damage.scale(1 + this.variance).renderShort(), | |||
| "." | |||
| ) | |||
| } | |||
| } | |||
| @@ -268,9 +322,14 @@ export class UniformRandomDamageFormula implements DamageFormula { | |||
| * A [[DamageFormula]] that uses the attacker's stats | |||
| */ | |||
| export class StatDamageFormula implements DamageFormula { | |||
| constructor (private factors: Array<{ stat: Stat|VoreStat; fraction: number; type: DamageType; target: Vigor|Stat }>) { | |||
| } | |||
| constructor ( | |||
| private factors: Array<{ | |||
| stat: Stat | VoreStat; | |||
| fraction: number; | |||
| type: DamageType; | |||
| target: Vigor | Stat; | |||
| }> | |||
| ) {} | |||
| calc (user: Creature, target: Creature): Damage { | |||
| const instances: Array<DamageInstance> = this.factors.map(factor => { | |||
| @@ -310,12 +369,17 @@ export class StatDamageFormula implements DamageFormula { | |||
| explain (user: Creature): LogEntry { | |||
| return new LogLine( | |||
| `Deal `, | |||
| ...this.factors.map(factor => new LogLine( | |||
| `${factor.fraction * 100}% of your `, | |||
| new PropElem(factor.stat), | |||
| ` as `, | |||
| new PropElem(factor.target) | |||
| )).joinGeneral(new LogLine(`, `), new LogLine(` and `)) | |||
| ...this.factors | |||
| .map( | |||
| factor => | |||
| new LogLine( | |||
| `${factor.fraction * 100}% of your `, | |||
| new PropElem(factor.stat), | |||
| ` as `, | |||
| new PropElem(factor.target) | |||
| ) | |||
| ) | |||
| .joinGeneral(new LogLine(`, `), new LogLine(` and `)) | |||
| ) | |||
| } | |||
| } | |||
| @@ -324,21 +388,30 @@ export class StatDamageFormula implements DamageFormula { | |||
| * Deals a percentage of the target's current vigors/stats | |||
| */ | |||
| export class FractionDamageFormula implements DamageFormula { | |||
| constructor (private factors: Array<{ fraction: number; target: Vigor|Stat; type: DamageType }>) { | |||
| } | |||
| constructor ( | |||
| private factors: Array<{ | |||
| fraction: number; | |||
| target: Vigor | Stat; | |||
| type: DamageType; | |||
| }> | |||
| ) {} | |||
| calc (user: Creature, target: Creature): Damage { | |||
| const instances: Array<DamageInstance> = this.factors.map(factor => { | |||
| if (factor.target in Stat) { | |||
| return { | |||
| amount: Math.max(0, factor.fraction * target.stats[factor.target as Stat]), | |||
| amount: Math.max( | |||
| 0, | |||
| factor.fraction * target.stats[factor.target as Stat] | |||
| ), | |||
| target: factor.target, | |||
| type: factor.type | |||
| } | |||
| } else if (factor.target in Vigor) { | |||
| return { | |||
| amount: Math.max(factor.fraction * target.vigors[factor.target as Vigor]), | |||
| amount: Math.max( | |||
| factor.fraction * target.vigors[factor.target as Vigor] | |||
| ), | |||
| target: factor.target, | |||
| type: factor.type | |||
| } | |||
| @@ -362,10 +435,15 @@ export class FractionDamageFormula implements DamageFormula { | |||
| explain (user: Creature): LogEntry { | |||
| return new LogLine( | |||
| `Deal damage equal to `, | |||
| ...this.factors.map(factor => new LogLine( | |||
| `${factor.fraction * 100}% of your target's `, | |||
| new PropElem(factor.target) | |||
| )).joinGeneral(new LogLine(`, `), new LogLine(` and `)) | |||
| ...this.factors | |||
| .map( | |||
| factor => | |||
| new LogLine( | |||
| `${factor.fraction * 100}% of your target's `, | |||
| new PropElem(factor.target) | |||
| ) | |||
| ) | |||
| .joinGeneral(new LogLine(`, `), new LogLine(` and `)) | |||
| ) | |||
| } | |||
| } | |||
| @@ -379,8 +457,8 @@ export enum Side { | |||
| * A Combatant has a list of possible actions to take, as well as a side. | |||
| */ | |||
| export interface Combatant { | |||
| actions: Array<Action>; | |||
| side: Side; | |||
| actions: Array<Action>; | |||
| side: Side; | |||
| } | |||
| /** | |||
| @@ -392,9 +470,7 @@ export abstract class Action { | |||
| public desc: TextLike, | |||
| public conditions: Array<Condition> = [], | |||
| public tests: Array<CombatTest> = [] | |||
| ) { | |||
| } | |||
| ) {} | |||
| allowed (user: Creature, target: Creature): boolean { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| @@ -408,21 +484,36 @@ export abstract class Action { | |||
| const results = targets.map(target => { | |||
| const failReason = this.tests.find(test => !test.test(user, target)) | |||
| if (failReason !== undefined) { | |||
| return { failed: true, target: target, log: failReason.fail(user, target) } | |||
| return { | |||
| failed: true, | |||
| target: target, | |||
| log: failReason.fail(user, target) | |||
| } | |||
| } else { | |||
| return { failed: false, target: target, log: this.execute(user, target) } | |||
| return { | |||
| failed: false, | |||
| target: target, | |||
| log: this.execute(user, target) | |||
| } | |||
| } | |||
| }) | |||
| return new LogLines( | |||
| ...results.map(result => result.log), | |||
| this.executeAll(user, results.filter(result => !result.failed).map(result => result.target)) | |||
| this.executeAll( | |||
| user, | |||
| results.filter(result => !result.failed).map(result => result.target) | |||
| ) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature, verbose = true): LogEntry { | |||
| return new LogLines( | |||
| ...(verbose ? this.conditions.map(condition => condition.explain(user, target)).concat([new Newline()]) : []), | |||
| ...(verbose | |||
| ? this.conditions | |||
| .map(condition => condition.explain(user, target)) | |||
| .concat([new Newline()]) | |||
| : []), | |||
| new LogLine( | |||
| `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` | |||
| ), | |||
| @@ -432,7 +523,10 @@ export abstract class Action { | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.tests.reduce((total, test) => total * test.odds(user, target), 1) | |||
| return this.tests.reduce( | |||
| (total, test) => total * test.odds(user, target), | |||
| 1 | |||
| ) | |||
| } | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature> { | |||
| @@ -443,13 +537,13 @@ export abstract class Action { | |||
| return nilLog | |||
| } | |||
| abstract execute (user: Creature, target: Creature): LogEntry | |||
| abstract execute(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class CompositionAction extends Action { | |||
| public consequences: Array<Consequence>; | |||
| public groupConsequences: Array<GroupConsequence>; | |||
| public targeters: Array<Targeter>; | |||
| public consequences: Array<Consequence> | |||
| public groupConsequences: Array<GroupConsequence> | |||
| public targeters: Array<Targeter> | |||
| constructor ( | |||
| name: TextLike, | |||
| @@ -470,27 +564,34 @@ export class CompositionAction extends Action { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target)) | |||
| ...this.consequences | |||
| .filter(consequence => consequence.applicable(user, target)) | |||
| .map(consequence => consequence.apply(user, target)) | |||
| ) | |||
| } | |||
| executeAll (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLines( | |||
| ...this.groupConsequences.map(consequence => consequence.apply(user, targets.filter(target => consequence.applicable(user, target)))) | |||
| ...this.groupConsequences.map(consequence => | |||
| consequence.apply( | |||
| user, | |||
| targets.filter(target => consequence.applicable(user, target)) | |||
| )) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| ...this.consequences.map(consequence => consequence.describe(user, target)).concat( | |||
| new Newline(), | |||
| super.describe(user, target) | |||
| ) | |||
| ...this.consequences | |||
| .map(consequence => consequence.describe(user, target)) | |||
| .concat(new Newline(), super.describe(user, target)) | |||
| ) | |||
| } | |||
| targets (primary: Creature, encounter: Encounter) { | |||
| return this.targeters.flatMap(targeter => targeter.targets(primary, encounter)).unique() | |||
| return this.targeters | |||
| .flatMap(targeter => targeter.targets(primary, encounter)) | |||
| .unique() | |||
| } | |||
| } | |||
| @@ -515,12 +616,16 @@ export class Effective { | |||
| /** | |||
| * Executes when the effect is initially applied | |||
| */ | |||
| onApply (creature: Creature): LogEntry { return nilLog } | |||
| onApply (creature: Creature): LogEntry { | |||
| return nilLog | |||
| } | |||
| /** | |||
| * Executes when the effect is removed | |||
| */ | |||
| onRemove (creature: Creature): LogEntry { return nilLog } | |||
| onRemove (creature: Creature): LogEntry { | |||
| return nilLog | |||
| } | |||
| /** | |||
| * Executes before the creature tries to perform an action | |||
| @@ -535,7 +640,10 @@ export class Effective { | |||
| /** | |||
| * Executes before another creature tries to perform an action that targets this creature | |||
| */ | |||
| preReceiveAction (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { | |||
| preReceiveAction ( | |||
| creature: Creature, | |||
| attacker: Creature | |||
| ): { prevented: boolean; log: LogEntry } { | |||
| return { | |||
| prevented: false, | |||
| log: nilLog | |||
| @@ -552,7 +660,10 @@ export class Effective { | |||
| /** | |||
| * Executes before the creature is attacked | |||
| */ | |||
| preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { | |||
| preAttack ( | |||
| creature: Creature, | |||
| attacker: Creature | |||
| ): { prevented: boolean; log: LogEntry } { | |||
| return { | |||
| prevented: false, | |||
| log: nilLog | |||
| @@ -580,7 +691,10 @@ export class Effective { | |||
| * Called when a test is about to resolve. Decides if the creature should automatically fail. | |||
| */ | |||
| failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } { | |||
| failTest ( | |||
| creature: Creature, | |||
| opponent: Creature | |||
| ): { failed: boolean; log: LogEntry } { | |||
| return { | |||
| failed: false, | |||
| log: nilLog | |||
| @@ -597,28 +711,45 @@ export class Effective { | |||
| /** | |||
| * Additively modifies a creature's score for an offensive test | |||
| */ | |||
| modTestOffense (attacker: Creature, defender: Creature, kind: TestCategory): number { | |||
| modTestOffense ( | |||
| attacker: Creature, | |||
| defender: Creature, | |||
| kind: TestCategory | |||
| ): number { | |||
| return 0 | |||
| } | |||
| /** | |||
| * Additively modifies a creature's score for a defensive test | |||
| */ | |||
| modTestDefense (defender: Creature, attacker: Creature, kind: TestCategory): number { | |||
| modTestDefense ( | |||
| defender: Creature, | |||
| attacker: Creature, | |||
| kind: TestCategory | |||
| ): number { | |||
| return 0 | |||
| } | |||
| /** | |||
| * Affects digestion damage | |||
| */ | |||
| modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage { | |||
| modDigestionDamage ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container, | |||
| damage: Damage | |||
| ): Damage { | |||
| return damage | |||
| } | |||
| /** | |||
| * Triggers after consumption | |||
| */ | |||
| postConsume (predator: Creature, prey: Creature, container: VoreContainer): LogEntry { | |||
| postConsume ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container | |||
| ): LogEntry { | |||
| return nilLog | |||
| } | |||
| @@ -653,24 +784,35 @@ export interface VisibleStatus { | |||
| * a status indicating that it is dead, but entities cannot be "given" the dead effect | |||
| */ | |||
| export class ImplicitStatus implements VisibleStatus { | |||
| topLeft = '' | |||
| bottomRight = '' | |||
| topLeft = "" | |||
| bottomRight = "" | |||
| constructor (public name: TextLike, public desc: TextLike, public icon: string) { | |||
| } | |||
| constructor ( | |||
| public name: TextLike, | |||
| public desc: TextLike, | |||
| public icon: string | |||
| ) {} | |||
| } | |||
| /** | |||
| * This kind of status is explicitly given to a creature. | |||
| */ | |||
| export abstract class StatusEffect extends Effective implements VisibleStatus { | |||
| constructor (public name: TextLike, public desc: TextLike, public icon: string) { | |||
| constructor ( | |||
| public name: TextLike, | |||
| public desc: TextLike, | |||
| public icon: string | |||
| ) { | |||
| super() | |||
| } | |||
| get topLeft () { return '' } | |||
| get bottomRight () { return '' } | |||
| get topLeft () { | |||
| return "" | |||
| } | |||
| get bottomRight () { | |||
| return "" | |||
| } | |||
| } | |||
| export type EncounterDesc = { | |||
| @@ -702,7 +844,9 @@ export class Encounter { | |||
| this.combatants.forEach(combatant => { | |||
| // this should never be undefined | |||
| const currentProgress = this.initiatives.get(combatant) ?? 0 | |||
| const remaining = (this.turnTime - currentProgress) / Math.sqrt(Math.max(combatant.stats.Agility, 1)) | |||
| const remaining = | |||
| (this.turnTime - currentProgress) / | |||
| Math.sqrt(Math.max(combatant.stats.Agility, 1)) | |||
| times.set(combatant, remaining) | |||
| }) | |||
| @@ -712,12 +856,18 @@ export class Encounter { | |||
| return closestTime <= nextTime ? closest : next | |||
| }, this.combatants[0]) | |||
| const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.sqrt(Math.max(this.currentMove.stats.Agility, 1)) | |||
| const closestRemaining = | |||
| (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / | |||
| Math.sqrt(Math.max(this.currentMove.stats.Agility, 1)) | |||
| this.combatants.forEach(combatant => { | |||
| // still not undefined... | |||
| const currentProgress = this.initiatives.get(combatant) ?? 0 | |||
| this.initiatives.set(combatant, currentProgress + closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1))) | |||
| this.initiatives.set( | |||
| combatant, | |||
| currentProgress + | |||
| closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1)) | |||
| ) | |||
| }) | |||
| // TODO: still let the creature use drained-vigor moves | |||
| @@ -726,24 +876,21 @@ export class Encounter { | |||
| return this.nextMove(closestRemaining + totalTime) | |||
| } else { | |||
| // applies digestion every time combat advances | |||
| const tickResults = this.combatants.flatMap( | |||
| combatant => combatant.containers.map( | |||
| container => container.tick(5 * (closestRemaining + totalTime)) | |||
| ) | |||
| ) | |||
| const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented) | |||
| const tickResults = this.combatants.flatMap(combatant => | |||
| combatant.containers.map(container => | |||
| container.tick(5 * (closestRemaining + totalTime)))) | |||
| const effectResults = this.currentMove.effects | |||
| .map(effect => effect.preTurn(this.currentMove)) | |||
| .filter(effect => effect.prevented) | |||
| if (effectResults.some(result => result.prevented)) { | |||
| const parts = effectResults.map(result => result.log).concat([this.nextMove()]) | |||
| const parts = effectResults | |||
| .map(result => result.log) | |||
| .concat([this.nextMove()]) | |||
| return new LogLines( | |||
| ...parts, | |||
| ...tickResults | |||
| ) | |||
| return new LogLines(...parts, ...tickResults) | |||
| } else { | |||
| return new LogLines( | |||
| ...tickResults | |||
| ) | |||
| return new LogLines(...tickResults) | |||
| } | |||
| } | |||
| @@ -753,8 +900,12 @@ export class Encounter { | |||
| /** | |||
| * Combat is won once one side is completely disabled | |||
| */ | |||
| get winner (): null|Side { | |||
| const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.disabled).map(combatant => combatant.side)) | |||
| get winner (): null | Side { | |||
| const remaining: Set<Side> = new Set( | |||
| this.combatants | |||
| .filter(combatant => !combatant.disabled) | |||
| .map(combatant => combatant.side) | |||
| ) | |||
| if (remaining.size === 1) { | |||
| return Array.from(remaining)[0] | |||
| @@ -766,8 +917,12 @@ export class Encounter { | |||
| /** | |||
| * Combat is completely won once one side is completely destroyed | |||
| */ | |||
| get totalWinner (): null|Side { | |||
| const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.destroyed).map(combatant => combatant.side)) | |||
| get totalWinner (): null | Side { | |||
| const remaining: Set<Side> = new Set( | |||
| this.combatants | |||
| .filter(combatant => !combatant.destroyed) | |||
| .map(combatant => combatant.side) | |||
| ) | |||
| if (remaining.size === 1) { | |||
| return Array.from(remaining)[0] | |||
| @@ -778,27 +933,23 @@ export class Encounter { | |||
| } | |||
| export abstract class Consequence { | |||
| constructor (public conditions: Condition[]) { | |||
| } | |||
| constructor (public conditions: Condition[]) {} | |||
| applicable (user: Creature, target: Creature): boolean { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| } | |||
| abstract describe (user: Creature, target: Creature): LogEntry | |||
| abstract apply (user: Creature, target: Creature): LogEntry | |||
| abstract describe(user: Creature, target: Creature): LogEntry | |||
| abstract apply(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export abstract class GroupConsequence { | |||
| constructor (public conditions: Condition[]) { | |||
| } | |||
| constructor (public conditions: Condition[]) {} | |||
| applicable (user: Creature, target: Creature): boolean { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| } | |||
| abstract describe (user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract apply (user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract describe(user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract apply(user: Creature, targets: Array<Creature>): LogEntry | |||
| } | |||
| @@ -4,7 +4,7 @@ import { Entity } from '@/game/entity' | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageFormula, Vigor, Action, Condition, CombatTest, CompositionAction } from '@/game/combat' | |||
| import { LogLine, LogLines, LogEntry } from '@/game/interface' | |||
| import { VoreContainer, Container } from '@/game/vore' | |||
| import { Connection, Container } from '@/game/vore' | |||
| import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from '@/game/combat/conditions' | |||
| import { ConsumeConsequence } from '@/game/combat/consequences' | |||
| @@ -216,8 +216,50 @@ export class StruggleAction extends Action { | |||
| ) | |||
| } | |||
| /** | |||
| * Tries to move between containers | |||
| */ | |||
| export class StruggleMoveAction extends Action { | |||
| constructor (public from: Container, public to: Container) { | |||
| super( | |||
| new DynText('Struggle (', new LiveText(from, x => x.name.all), ' to ', new LiveText(to, x => x.name.all), ')'), | |||
| 'Try to escape from your foe', | |||
| [new CapableCondition(), new PairCondition(), new ContainedByCondition(from)], | |||
| [ | |||
| new CompositionTest( | |||
| [ | |||
| new OpposedStatScorer( | |||
| { Power: 1, Agility: 1, Bulk: 0.05 }, | |||
| { Toughness: 1, Reflexes: 1, Mass: 0.05 } | |||
| ) | |||
| ], | |||
| (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to escape from ${target.name.possessive} ${from.name}.`), | |||
| TestCategory.Vore, | |||
| -5 | |||
| ) | |||
| ] | |||
| ) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (user.containedIn !== null) { | |||
| return new LogLines(this.successLine(user, target, { container: this.from }), user.containedIn.release(user)) | |||
| } else { | |||
| return new LogLine("Vore's bugged!") | |||
| } | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Try to escape from ${target.baseName.possessive} ${this.from.name}. `, super.describe(user, target)) | |||
| } | |||
| protected successLine: PairLineArgs<Entity, { container: Container }> = (prey, pred, args) => new LogLine( | |||
| `${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))}!` | |||
| ) | |||
| } | |||
| export class RubAction extends Action { | |||
| constructor (protected container: VoreContainer) { | |||
| constructor (protected container: Container) { | |||
| super( | |||
| new DynText('Rub (', new LiveText(container, container => container.name.all), ')'), | |||
| 'Digest your prey more quickly', | |||
| @@ -272,16 +314,16 @@ export class ReleaseAction extends Action { | |||
| export class TransferAction extends Action { | |||
| verb: Verb = new Verb('send') | |||
| constructor (protected from: Container, protected to: Container, name = 'Transfer') { | |||
| constructor (public from: Container, public to: Connection, name = 'Transfer') { | |||
| super( | |||
| name, | |||
| `${from.name.all.capital} to ${to.name.all}`, | |||
| `${from.name.all.capital} to ${to.destination.name.all}`, | |||
| [new CapableCondition(), new PairCondition()] | |||
| ) | |||
| } | |||
| line: PairLineArgs<Creature, { from: Container; to: Container }> = (user, target, args) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(this.verb)} ${target.name.objective} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}` | |||
| line: PairLineArgs<Creature, { from: Container; to: Connection }> = (user, target, args) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(this.verb)} ${target.name.objective} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.destination.name}` | |||
| ) | |||
| allowed (user: Creature, target: Creature) { | |||
| @@ -294,12 +336,12 @@ export class TransferAction extends Action { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| this.from.release(target) | |||
| this.to.consume(target) | |||
| this.to.destination.consume(target) | |||
| return this.line(user, target, { from: this.from, to: this.to }) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Push ${target.baseName} from your ${this.from.name} to your ${this.to.name}`) | |||
| return new LogLine(`Push ${target.baseName} from your ${this.from.name} to your ${this.to.destination.name}`) | |||
| } | |||
| } | |||
| @@ -1,13 +1,21 @@ | |||
| import { StatusEffect, Damage, DamageType, Action, Condition, Vigor, Stat } from '@/game/combat' | |||
| import { DynText, LiveText, ToBe, Verb } from '@/game/language' | |||
| import { | |||
| StatusEffect, | |||
| Damage, | |||
| DamageType, | |||
| Action, | |||
| Condition, | |||
| Vigor, | |||
| Stat | |||
| } from "@/game/combat" | |||
| import { DynText, LiveText, ToBe, Verb } from "@/game/language" | |||
| import { Creature } from "../creature" | |||
| import { LogLine, LogEntry, LogLines, FAElem, nilLog } from '@/game/interface' | |||
| import { VoreContainer } from '@/game/vore' | |||
| import * as Words from '@/game/words' | |||
| import { LogLine, LogEntry, LogLines, FAElem, nilLog } from "@/game/interface" | |||
| import { Container } from "@/game/vore" | |||
| import * as Words from "@/game/words" | |||
| export class InstantKillEffect extends StatusEffect { | |||
| constructor () { | |||
| super('Instant Kill', 'Instant kill!', 'fas fa-skull') | |||
| super("Instant Kill", "Instant kill!", "fas fa-skull") | |||
| } | |||
| onApply (creature: Creature) { | |||
| @@ -15,8 +23,10 @@ export class InstantKillEffect extends StatusEffect { | |||
| creature.removeEffect(this) | |||
| return new LogLines( | |||
| new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate(new ToBe())} killed instantly! `, | |||
| new FAElem('fas fa-skull') | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} killed instantly! `, | |||
| new FAElem("fas fa-skull") | |||
| ), | |||
| creature.takeDamage(new Damage()) | |||
| ) | |||
| @@ -25,8 +35,12 @@ export class InstantKillEffect extends StatusEffect { | |||
| export class StunEffect extends StatusEffect { | |||
| constructor (private duration: number) { | |||
| super('Stun', 'Cannot act!', 'fas fa-sun') | |||
| this.desc = new DynText('Stunned for your next ', new LiveText(this, x => x.duration), ' actions!') | |||
| super("Stun", "Cannot act!", "fas fa-sun") | |||
| this.desc = new DynText( | |||
| "Stunned for your next ", | |||
| new LiveText(this, x => x.duration), | |||
| " actions!" | |||
| ) | |||
| } | |||
| get topLeft () { | |||
| @@ -34,11 +48,19 @@ export class StunEffect extends StatusEffect { | |||
| } | |||
| onApply (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} is stunned!`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} is stunned!` | |||
| ) | |||
| } | |||
| onRemove (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} no longer stunned.`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} no longer stunned.` | |||
| ) | |||
| } | |||
| preTurn (creature: Creature): { prevented: boolean; log: LogEntry } { | |||
| @@ -46,7 +68,9 @@ export class StunEffect extends StatusEffect { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLines( | |||
| `${creature.name.capital} ${creature.name.conjugate(new ToBe())} stunned! ${creature.pronouns.capital.subjective} can't move.`, | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} stunned! ${creature.pronouns.capital.subjective} can't move.`, | |||
| creature.removeEffect(this) | |||
| ) | |||
| } | |||
| @@ -54,7 +78,9 @@ export class StunEffect extends StatusEffect { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLines( | |||
| `${creature.name.capital} ${creature.name.conjugate(new ToBe())} stunned! ${creature.pronouns.capital.subjective} can't move!` | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} stunned! ${creature.pronouns.capital.subjective} can't move!` | |||
| ) | |||
| } | |||
| } | |||
| @@ -63,15 +89,30 @@ export class StunEffect extends StatusEffect { | |||
| export class DamageTypeResistanceEffect extends StatusEffect { | |||
| constructor (private damageTypes: DamageType[], private amount: number) { | |||
| super('Resistance', 'Block ' + ((1 - amount) * 100).toFixed() + '% of these damage types: ' + damageTypes.join(", "), 'fas fa-shield-alt') | |||
| super( | |||
| "Resistance", | |||
| "Block " + | |||
| ((1 - amount) * 100).toFixed() + | |||
| "% of these damage types: " + | |||
| damageTypes.join(", "), | |||
| "fas fa-shield-alt" | |||
| ) | |||
| } | |||
| onApply (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new Verb('gain'))} a shield!`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new Verb("gain") | |||
| )} a shield!` | |||
| ) | |||
| } | |||
| onRemove (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new Verb('lose'))} ${creature.pronouns.possessive} shield!`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate(new Verb("lose"))} ${ | |||
| creature.pronouns.possessive | |||
| } shield!` | |||
| ) | |||
| } | |||
| modResistance (type: DamageType, factor: number) { | |||
| @@ -85,17 +126,27 @@ export class DamageTypeResistanceEffect extends StatusEffect { | |||
| export class PredatorCounterEffect extends StatusEffect { | |||
| constructor (private devour: Action, private chance: number) { | |||
| super('Predatory Counter', 'Eat them back', 'fas fa-redo') | |||
| this.desc = new DynText(new LiveText(this, x => (x.chance * 100).toFixed(0)), '% chance to devour your attackers') | |||
| super("Predatory Counter", "Eat them back", "fas fa-redo") | |||
| this.desc = new DynText( | |||
| new LiveText(this, x => (x.chance * 100).toFixed(0)), | |||
| "% chance to devour your attackers" | |||
| ) | |||
| } | |||
| preAttack (creature: Creature, attacker: Creature) { | |||
| if (this.devour.allowed(creature, attacker) && Math.random() < this.chance) { | |||
| if ( | |||
| this.devour.allowed(creature, attacker) && | |||
| Math.random() < this.chance | |||
| ) { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLines( | |||
| `${creature.name.capital} ${creature.name.conjugate(new Verb('surprise'))} ${attacker.name.objective} and ${creature.name.conjugate(new Verb('try', 'tries'))} to devour ${attacker.pronouns.objective}!`, | |||
| this.devour.execute(creature, attacker) | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new Verb("surprise") | |||
| )} ${attacker.name.objective} and ${creature.name.conjugate( | |||
| new Verb("try", "tries") | |||
| )} to devour ${attacker.pronouns.objective}!`, | |||
| this.devour.execute(creature, attacker) | |||
| ) | |||
| } | |||
| } else { | |||
| @@ -106,7 +157,7 @@ export class PredatorCounterEffect extends StatusEffect { | |||
| export class UntouchableEffect extends StatusEffect { | |||
| constructor () { | |||
| super('Untouchable', 'Cannot be attacked', 'fas fa-times') | |||
| super("Untouchable", "Cannot be attacked", "fas fa-times") | |||
| } | |||
| preAttack (creature: Creature, attacker: Creature) { | |||
| @@ -119,7 +170,11 @@ export class UntouchableEffect extends StatusEffect { | |||
| export class DazzlingEffect extends StatusEffect { | |||
| constructor (private conditions: Condition[]) { | |||
| super('Dazzling', 'Stuns enemies who try to affect this creature', 'fas fa-spinner') | |||
| super( | |||
| "Dazzling", | |||
| "Stuns enemies who try to affect this creature", | |||
| "fas fa-spinner" | |||
| ) | |||
| } | |||
| preReceiveAction (creature: Creature, attacker: Creature) { | |||
| @@ -127,7 +182,9 @@ export class DazzlingEffect extends StatusEffect { | |||
| attacker.applyEffect(new StunEffect(1)) | |||
| return { | |||
| prevented: true, | |||
| log: new LogLine(`${attacker.name.capital} can't act against ${creature.name.objective}!`) | |||
| log: new LogLine( | |||
| `${attacker.name.capital} can't act against ${creature.name.objective}!` | |||
| ) | |||
| } | |||
| } else { | |||
| return { | |||
| @@ -140,21 +197,32 @@ export class DazzlingEffect extends StatusEffect { | |||
| export class SurrenderEffect extends StatusEffect { | |||
| constructor () { | |||
| super('Surrendered', 'This creature has given up, and will fail most tests', 'fas fa-flag') | |||
| super( | |||
| "Surrendered", | |||
| "This creature has given up, and will fail most tests", | |||
| "fas fa-flag" | |||
| ) | |||
| } | |||
| onApply (creature: Creature): LogEntry { | |||
| creature.takeDamage( | |||
| new Damage( | |||
| { amount: creature.vigors.Resolve, target: Vigor.Resolve, type: DamageType.Pure } | |||
| ) | |||
| new Damage({ | |||
| amount: creature.vigors.Resolve, | |||
| target: Vigor.Resolve, | |||
| type: DamageType.Pure | |||
| }) | |||
| ) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate(new Verb('surrender'))}!` | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new Verb("surrender") | |||
| )}!` | |||
| ) | |||
| } | |||
| failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } { | |||
| failTest ( | |||
| creature: Creature, | |||
| opponent: Creature | |||
| ): { failed: boolean; log: LogEntry } { | |||
| return { | |||
| failed: true, | |||
| log: nilLog | |||
| @@ -164,7 +232,7 @@ export class SurrenderEffect extends StatusEffect { | |||
| export class SizeEffect extends StatusEffect { | |||
| constructor (private change: number) { | |||
| super('Size-Shifted', 'This creature has changed in size', 'fas fa-ruler') | |||
| super("Size-Shifted", "This creature has changed in size", "fas fa-ruler") | |||
| } | |||
| onApply (creature: Creature): LogLine { | |||
| @@ -178,26 +246,47 @@ export class SizeEffect extends StatusEffect { | |||
| export class DigestionPowerEffect extends StatusEffect { | |||
| constructor (private factor: number) { | |||
| super('Acid-fueled', 'This creature is digesting faster than nomral', 'fas fa-flask') | |||
| super( | |||
| "Acid-fueled", | |||
| "This creature is digesting faster than nomral", | |||
| "fas fa-flask" | |||
| ) | |||
| } | |||
| onApply (creature: Creature): LogLine { | |||
| const voreContainer: VoreContainer|undefined = creature.containers.find(c => c.digest !== null) | |||
| const voreContainer: Container | undefined = creature.containers.find( | |||
| c => c.digest !== null | |||
| ) | |||
| if (voreContainer !== undefined) { | |||
| return new LogLine(`${creature.name.capital.possessive}'s ${voreContainer.name} ${Words.Churns} and ${voreContainer.sound}`) | |||
| return new LogLine( | |||
| `${creature.name.capital.possessive}'s ${voreContainer.name} ${Words.Churns} and ${voreContainer.sound}` | |||
| ) | |||
| } else { | |||
| return new LogLine(`${creature.name.capital} can't digest people...`) | |||
| } | |||
| } | |||
| modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage { | |||
| modDigestionDamage ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container, | |||
| damage: Damage | |||
| ): Damage { | |||
| return damage.scale(this.factor) | |||
| } | |||
| } | |||
| export class StatEffect extends StatusEffect { | |||
| constructor (private stat: Stat, private amount: number, private factor: number) { | |||
| super('Stat boosted', 'This creature has modified stats', 'fas fa-user-plus') | |||
| constructor ( | |||
| private stat: Stat, | |||
| private amount: number, | |||
| private factor: number | |||
| ) { | |||
| super( | |||
| "Stat boosted", | |||
| "This creature has modified stats", | |||
| "fas fa-user-plus" | |||
| ) | |||
| } | |||
| modStat (creature: Creature, stat: Stat, current: number): number { | |||
| @@ -211,16 +300,22 @@ export class StatEffect extends StatusEffect { | |||
| export class InstantDigestionEffect extends StatusEffect { | |||
| constructor () { | |||
| super("Instant digestion", "This creature will melt people instantly", "fas fa-skull") | |||
| super( | |||
| "Instant digestion", | |||
| "This creature will melt people instantly", | |||
| "fas fa-skull" | |||
| ) | |||
| } | |||
| postConsume (predator: Creature, prey: Creature, container: VoreContainer) { | |||
| postConsume (predator: Creature, prey: Creature, container: Container) { | |||
| prey.applyEffect(new InstantKillEffect()) | |||
| predator.voreStats.Mass += prey.voreStats.Mass | |||
| prey.voreStats.Mass = 0 | |||
| return new LogLines( | |||
| `${prey.name.capital} ${prey.name.conjugate(new ToBe())} instantly digested! `, | |||
| new FAElem('fas fa-skull'), | |||
| `${prey.name.capital} ${prey.name.conjugate( | |||
| new ToBe() | |||
| )} instantly digested! `, | |||
| new FAElem("fas fa-skull"), | |||
| container.tick(0, [prey]) | |||
| ) | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import { Damage, Stats, Action, Vigor, Side, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors, Encounter } from '@/game/combat' | |||
| import { Noun, Pronoun, SoloLine, Verb } from '@/game/language' | |||
| import { LogEntry, LogLines, LogLine } from '@/game/interface' | |||
| import { VoreContainer, VoreType, Container } from '@/game/vore' | |||
| import { VoreType, Container } from '@/game/vore' | |||
| import { Item, EquipmentSlot, Equipment, ItemKind, Currency } from '@/game/items' | |||
| import { PassAction } from '@/game/combat/actions' | |||
| import { AI, RandomAI } from '@/game/ai' | |||
| @@ -31,8 +31,7 @@ export class Creature extends Entity { | |||
| destroyed = false; | |||
| containers: Array<VoreContainer> = [] | |||
| otherContainers: Array<Container> = [] | |||
| containers: Array<Container> = [] | |||
| containedIn: Container | null = null | |||
| @@ -81,7 +80,7 @@ export class Creature extends Entity { | |||
| this.voreStats = { | |||
| get [VoreStat.Bulk] () { | |||
| return self.containers.reduce( | |||
| (total: number, container: VoreContainer) => { | |||
| (total: number, container: Container) => { | |||
| return total + container.contents.reduce( | |||
| (total: number, prey: Creature) => { | |||
| return total + prey.voreStats.Bulk | |||
| @@ -111,7 +110,7 @@ export class Creature extends Entity { | |||
| }, | |||
| get [VoreStat.Prey] () { | |||
| return self.containers.reduce( | |||
| (total: number, container: VoreContainer) => { | |||
| (total: number, container: Container) => { | |||
| return total + container.contents.concat(container.digested).reduce( | |||
| (total: number, prey: Creature) => { | |||
| return total + 1 + prey.voreStats[VoreStat.Prey] | |||
| @@ -206,15 +205,11 @@ export class Creature extends Entity { | |||
| return effect.onApply(this) | |||
| } | |||
| addVoreContainer (container: VoreContainer): void { | |||
| addContainer (container: Container): void { | |||
| this.containers.push(container) | |||
| this.voreRelay.connect(container.voreRelay) | |||
| } | |||
| addOtherContainer (container: Container): void { | |||
| this.otherContainers.push(container) | |||
| } | |||
| addPerk (perk: Perk): void { | |||
| this.perks.push(perk) | |||
| } | |||
| @@ -273,7 +268,6 @@ export class Creature extends Entity { | |||
| this.actions, | |||
| this.containers.flatMap(container => container.actions), | |||
| target.otherActions, | |||
| this.otherContainers.flatMap(container => container.actions), | |||
| Object.values(this.equipment).filter(item => item !== undefined).flatMap(item => (item as Equipment).actions), | |||
| this.items.filter(item => item.kind === ItemKind.Consumable && !item.consumed).flatMap(item => item.actions), | |||
| this.perks.flatMap(perk => perk.actions(this)) | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -1,15 +1,15 @@ | |||
| import { VoreAI } from '@/game/ai' | |||
| import { DamageType, Side, Stat, StatDamageFormula, Vigor } from '@/game/combat' | |||
| import { Creature } from '@/game/creature' | |||
| import { ImproperNoun, ObjectPronouns } from '@/game/language' | |||
| import { anyVore, Goo, Stomach } from '@/game/vore' | |||
| import { ImproperNoun, MalePronouns, ObjectPronouns } from '@/game/language' | |||
| import { anyVore, ConnectionDirection, Stomach, Throat } from '@/game/vore' | |||
| export default class Werewolf extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ImproperNoun("werewolf", "werewolves"), | |||
| new ImproperNoun("werewolf", "werewolves"), | |||
| ObjectPronouns, | |||
| MalePronouns, | |||
| { | |||
| Power: 45, | |||
| Toughness: 30, | |||
| @@ -23,6 +23,11 @@ export default class Werewolf extends Creature { | |||
| 75 | |||
| ) | |||
| const throat = new Throat( | |||
| this, | |||
| 25 | |||
| ) | |||
| const stomach = new Stomach( | |||
| this, | |||
| 50, | |||
| @@ -31,7 +36,18 @@ export default class Werewolf extends Creature { | |||
| ]) | |||
| ) | |||
| this.addVoreContainer(stomach) | |||
| this.addContainer(throat) | |||
| this.addContainer(stomach) | |||
| throat.connect({ | |||
| destination: stomach, | |||
| direction: ConnectionDirection.Deeper | |||
| }) | |||
| stomach.connect({ | |||
| destination: throat, | |||
| direction: ConnectionDirection.Shallower | |||
| }) | |||
| this.side = Side.Monsters | |||
| this.ai = new VoreAI(this) | |||
| @@ -1,10 +1,10 @@ | |||
| import { Creature } from "../creature" | |||
| import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '@/game/language' | |||
| import { Damage, DamageType, Vigor, ConstantDamageFormula } from '@/game/combat' | |||
| import { Stomach, Bowels, anyVore, Cock, Balls, Breasts, InnerBladder, Slit, Womb, biconnectContainers } from '@/game/vore' | |||
| import { Stomach, anyVore, Container } from '@/game/vore' | |||
| import { AttackAction } from '@/game/combat/actions' | |||
| import { RavenousPerk, BellyBulwakPerk, FlauntPerk } from '@/game/combat/perks' | |||
| import { VoreAI } from "../ai" | |||
| import { nilLog } from "../interface" | |||
| export default class Player extends Creature { | |||
| constructor () { | |||
| @@ -21,10 +21,18 @@ export default class Player extends Creature { | |||
| this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina })))) | |||
| const stomach = new Stomach(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.addVoreContainer(stomach) | |||
| this.addContainer(stomach) | |||
| this.perspective = POV.First | |||
| this.ai = new VoreAI(this) | |||
| this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => { | |||
| if (this === args.prey) { | |||
| return sender.describeDetail(this) | |||
| } else { | |||
| return nilLog | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import { TargetDrainedVigorCondition } from '@/game/combat/conditions' | |||
| import { Creature } from '@/game/creature' | |||
| import { LogEntry, LogLines, nilLog } from "@/game/interface" | |||
| import { VoreContainer } from '@/game/vore' | |||
| import { Container } from '@/game/vore' | |||
| import { Action } from './combat' | |||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
| @@ -47,7 +47,7 @@ type VoreMap = { | |||
| "onAbsorbed": { prey: Creature }; | |||
| } | |||
| export class VoreRelay extends Relay<VoreContainer, VoreMap> { | |||
| export class VoreRelay extends Relay<Container, VoreMap> { | |||
| constructor () { | |||
| super(["onEaten", "onReleased", "onDigested", "onAbsorbed"]) | |||
| } | |||
| @@ -1,6 +1,10 @@ | |||
| import { LogEntry } from '@/game/interface' | |||
| import { LogEntry } from "@/game/interface" | |||
| export enum POV {First, Second, Third} | |||
| export enum POV { | |||
| First, | |||
| Second, | |||
| Third | |||
| } | |||
| export type SoloLine<T> = (user: T) => LogEntry | |||
| export type SoloLineArgs<T, V> = (user: T, args: V) => LogEntry | |||
| @@ -9,15 +13,15 @@ export type PairLineArgs<T, V> = (user: T, target: T, args: V) => LogEntry | |||
| export type GroupLine<T> = (user: T, targets: Array<T>) => LogEntry | |||
| enum NounKind { | |||
| Specific, | |||
| Nonspecific, | |||
| All | |||
| Specific, | |||
| Nonspecific, | |||
| All | |||
| } | |||
| enum VowelSound { | |||
| Default, | |||
| Vowel, | |||
| NonVowel | |||
| Default, | |||
| Vowel, | |||
| NonVowel | |||
| } | |||
| enum VerbKind { | |||
| @@ -64,9 +68,7 @@ export type TextLike = { toString: () => string } | |||
| // updates as needed | |||
| export class LiveText<T> { | |||
| constructor (private contents: T, private run: (thing: T) => TextLike) { | |||
| } | |||
| constructor (private contents: T, private run: (thing: T) => TextLike) {} | |||
| toString (): string { | |||
| return this.run(this.contents).toString() | |||
| @@ -80,17 +82,15 @@ export class DynText { | |||
| } | |||
| toString (): string { | |||
| return (this.parts.map(part => part.toString())).join('') | |||
| return this.parts.map(part => part.toString()).join("") | |||
| } | |||
| } | |||
| export abstract class Word { | |||
| constructor (public opt: WordOptions = emptyConfig) { | |||
| constructor (public opt: WordOptions = emptyConfig) {} | |||
| } | |||
| abstract configure (opts: WordOptions): Word; | |||
| abstract toString (): string; | |||
| abstract configure(opts: WordOptions): Word | |||
| abstract toString(): string | |||
| // These functions are pure; they don't mutate the original object. | |||
| // This is necessary to avoid causing chaos. | |||
| @@ -230,7 +230,11 @@ export class OptionalWord extends Word { | |||
| export class RandomWord extends Word { | |||
| private history: { last: number } | |||
| constructor (public choices: Array<Word>, opt: WordOptions = emptyConfig, history: { last: number } = { last: -1 }) { | |||
| constructor ( | |||
| public choices: Array<Word>, | |||
| opt: WordOptions = emptyConfig, | |||
| history: { last: number } = { last: -1 } | |||
| ) { | |||
| super(opt) | |||
| this.history = history | |||
| } | |||
| @@ -252,12 +256,22 @@ export class RandomWord extends Word { | |||
| } | |||
| export class Noun extends Word { | |||
| constructor (protected singularNoun: string, protected pluralNoun: string|null = null, protected possessiveNoun: string|null = null, protected options: WordOptions = emptyConfig) { | |||
| constructor ( | |||
| protected singularNoun: string, | |||
| protected pluralNoun: string | null = null, | |||
| protected possessiveNoun: string | null = null, | |||
| protected options: WordOptions = emptyConfig | |||
| ) { | |||
| super(options) | |||
| } | |||
| configure (opts: WordOptions): Word { | |||
| return new Noun(this.singularNoun, this.pluralNoun, this.possessiveNoun, opts) | |||
| return new Noun( | |||
| this.singularNoun, | |||
| this.pluralNoun, | |||
| this.possessiveNoun, | |||
| opts | |||
| ) | |||
| } | |||
| toString (): string { | |||
| @@ -274,31 +288,34 @@ export class Noun extends Word { | |||
| if (this.pluralNoun === null) { | |||
| result = this.singularNoun | |||
| } else { | |||
| result = (this.pluralNoun as string) | |||
| result = this.pluralNoun as string | |||
| } | |||
| } else { | |||
| result = this.singularNoun | |||
| } | |||
| if (!this.options.proper) { | |||
| if (this.options.nounKind === NounKind.Nonspecific && this.options.count) { | |||
| if ( | |||
| this.options.nounKind === NounKind.Nonspecific && | |||
| this.options.count | |||
| ) { | |||
| if (this.options.plural) { | |||
| result = 'some ' + result | |||
| result = "some " + result | |||
| } else { | |||
| if (this.options.vowel === VowelSound.Default) { | |||
| if ('aeiouAEIOU'.indexOf(result.slice(0, 1)) >= 0) { | |||
| result = 'an ' + result | |||
| if ("aeiouAEIOU".indexOf(result.slice(0, 1)) >= 0) { | |||
| result = "an " + result | |||
| } else { | |||
| result = 'a ' + result | |||
| result = "a " + result | |||
| } | |||
| } else if (this.options.vowel === VowelSound.Vowel) { | |||
| result = 'an ' + result | |||
| result = "an " + result | |||
| } else if (this.options.vowel === VowelSound.NonVowel) { | |||
| result = 'a ' + result | |||
| result = "a " + result | |||
| } | |||
| } | |||
| } else if (this.options.nounKind === NounKind.Specific) { | |||
| result = 'the ' + result | |||
| result = "the " + result | |||
| } | |||
| } | |||
| @@ -322,13 +339,37 @@ export class Noun extends Word { | |||
| export class ImproperNoun extends Noun { | |||
| constructor (singularNoun: string, pluralNoun: string = singularNoun) { | |||
| super(singularNoun, pluralNoun, null, { plural: false, allCaps: false, capital: false, proper: false, nounKind: NounKind.Specific, verbKind: VerbKind.Root, vowel: VowelSound.Default, count: true, possessive: false, objective: false, perspective: POV.Third }) | |||
| super(singularNoun, pluralNoun, null, { | |||
| plural: false, | |||
| allCaps: false, | |||
| capital: false, | |||
| proper: false, | |||
| nounKind: NounKind.Specific, | |||
| verbKind: VerbKind.Root, | |||
| vowel: VowelSound.Default, | |||
| count: true, | |||
| possessive: false, | |||
| objective: false, | |||
| perspective: POV.Third | |||
| }) | |||
| } | |||
| } | |||
| export class ProperNoun extends Noun { | |||
| constructor (singularNoun: string) { | |||
| super(singularNoun, null, null, { plural: false, allCaps: false, capital: false, proper: true, nounKind: NounKind.Specific, verbKind: VerbKind.Root, vowel: VowelSound.Default, count: true, possessive: false, objective: false, perspective: POV.Third }) | |||
| super(singularNoun, null, null, { | |||
| plural: false, | |||
| allCaps: false, | |||
| capital: false, | |||
| proper: true, | |||
| nounKind: NounKind.Specific, | |||
| verbKind: VerbKind.Root, | |||
| vowel: VowelSound.Default, | |||
| count: true, | |||
| possessive: false, | |||
| objective: false, | |||
| perspective: POV.Third | |||
| }) | |||
| } | |||
| } | |||
| @@ -341,9 +382,14 @@ export class Adjective extends Word { | |||
| return new Adjective(this.adjective, opts) | |||
| } | |||
| // TODO caps et al. | |||
| toString (): string { | |||
| return this.adjective | |||
| let word = this.adjective | |||
| if (this.opt.allCaps) { | |||
| word = word.toUpperCase() | |||
| } else if (this.opt.capital) { | |||
| word = word.slice(0, 1).toUpperCase() + word.slice(1) | |||
| } | |||
| return word | |||
| } | |||
| } | |||
| @@ -361,8 +407,21 @@ export class Adverb extends Word { | |||
| } | |||
| } | |||
| /** | |||
| * root: break | |||
| * singular: breaks | |||
| * present: breaking | |||
| * past: broken | |||
| */ | |||
| export class Verb extends Word { | |||
| constructor (private _root: string, private _singular: string = _root + "s", private _present: string = _root + "ing", private _past: string = _root + "ed", private _pastParticiple: string = _past, public opt: WordOptions = emptyConfig) { | |||
| constructor ( | |||
| private _root: string, | |||
| private _singular: string = _root + "s", | |||
| private _present: string = _root + "ing", | |||
| private _past: string = _root + "ed", | |||
| private _pastParticiple: string = _past, | |||
| public opt: WordOptions = emptyConfig | |||
| ) { | |||
| super(opt) | |||
| } | |||
| @@ -381,11 +440,21 @@ export class Verb extends Word { | |||
| let choice: string | |||
| switch (this.opt.verbKind) { | |||
| case VerbKind.Root: choice = this._root; break | |||
| case VerbKind.Singular: choice = this._singular; break | |||
| case VerbKind.Present: choice = this._present; break | |||
| case VerbKind.Past: choice = this._past; break | |||
| case VerbKind.PastParticiple: choice = this._pastParticiple; break | |||
| case VerbKind.Root: | |||
| choice = this._root | |||
| break | |||
| case VerbKind.Singular: | |||
| choice = this._singular | |||
| break | |||
| case VerbKind.Present: | |||
| choice = this._present | |||
| break | |||
| case VerbKind.Past: | |||
| choice = this._past | |||
| break | |||
| case VerbKind.PastParticiple: | |||
| choice = this._pastParticiple | |||
| break | |||
| } | |||
| if (this.opt.allCaps) { | |||
| @@ -430,12 +499,18 @@ export class ToBe extends Word { | |||
| let choice | |||
| if (this.opts.plural) { | |||
| choice = 'are' | |||
| choice = "are" | |||
| } | |||
| switch (this.opts.perspective) { | |||
| case POV.First: choice = 'am'; break | |||
| case POV.Second: choice = 'are'; break | |||
| case POV.Third: choice = 'is'; break | |||
| case POV.First: | |||
| choice = "am" | |||
| break | |||
| case POV.Second: | |||
| choice = "are" | |||
| break | |||
| case POV.Third: | |||
| choice = "is" | |||
| break | |||
| } | |||
| if (this.opt.allCaps) { | |||
| @@ -449,16 +524,18 @@ export class ToBe extends Word { | |||
| } | |||
| interface PronounDict { | |||
| subjective: string; | |||
| objective: string; | |||
| possessive: string; | |||
| reflexive: string; | |||
| subjective: string; | |||
| objective: string; | |||
| possessive: string; | |||
| reflexive: string; | |||
| } | |||
| export class Pronoun implements Pluralizable { | |||
| constructor (private pronouns: PronounDict, private capitalize: boolean = false, public isPlural: boolean = false) { | |||
| } | |||
| constructor ( | |||
| private pronouns: PronounDict, | |||
| private capitalize: boolean = false, | |||
| public isPlural: boolean = false | |||
| ) {} | |||
| get capital (): Pronoun { | |||
| return new Pronoun(this.pronouns, true) | |||
| @@ -498,53 +575,69 @@ export class Pronoun implements Pluralizable { | |||
| } | |||
| export const MalePronouns = new Pronoun({ | |||
| subjective: 'he', | |||
| objective: 'him', | |||
| possessive: 'his', | |||
| reflexive: 'himself' | |||
| subjective: "he", | |||
| objective: "him", | |||
| possessive: "his", | |||
| reflexive: "himself" | |||
| }) | |||
| export const FemalePronouns = new Pronoun({ | |||
| subjective: 'she', | |||
| objective: 'her', | |||
| possessive: 'her', | |||
| reflexive: 'herself' | |||
| subjective: "she", | |||
| objective: "her", | |||
| possessive: "her", | |||
| reflexive: "herself" | |||
| }) | |||
| export const TheyPronouns = new Pronoun({ | |||
| subjective: 'they', | |||
| objective: 'them', | |||
| possessive: 'their', | |||
| reflexive: 'themself' | |||
| }, false, true) | |||
| export const TheyPluralPronouns = new Pronoun({ | |||
| subjective: 'they', | |||
| objective: 'them', | |||
| possessive: 'their', | |||
| reflexive: 'themselves' | |||
| }, false, true) | |||
| export const TheyPronouns = new Pronoun( | |||
| { | |||
| subjective: "they", | |||
| objective: "them", | |||
| possessive: "their", | |||
| reflexive: "themself" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export const TheyPluralPronouns = new Pronoun( | |||
| { | |||
| subjective: "they", | |||
| objective: "them", | |||
| possessive: "their", | |||
| reflexive: "themselves" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export const ObjectPronouns = new Pronoun({ | |||
| subjective: 'it', | |||
| objective: 'it', | |||
| possessive: 'its', | |||
| reflexive: 'itself' | |||
| subjective: "it", | |||
| objective: "it", | |||
| possessive: "its", | |||
| reflexive: "itself" | |||
| }) | |||
| export const SecondPersonPronouns = new Pronoun({ | |||
| subjective: 'you', | |||
| objective: 'you', | |||
| possessive: 'your', | |||
| reflexive: 'yourself' | |||
| }, false, true) | |||
| export const FirstPersonPronouns = new Pronoun({ | |||
| subjective: 'I', | |||
| objective: 'me', | |||
| possessive: 'my', | |||
| reflexive: 'myself' | |||
| }, false, true) | |||
| export const SecondPersonPronouns = new Pronoun( | |||
| { | |||
| subjective: "you", | |||
| objective: "you", | |||
| possessive: "your", | |||
| reflexive: "yourself" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export const FirstPersonPronouns = new Pronoun( | |||
| { | |||
| subjective: "I", | |||
| objective: "me", | |||
| possessive: "my", | |||
| reflexive: "myself" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export class PronounAsNoun extends Noun { | |||
| constructor (private pronouns: Pronoun, opt: WordOptions = emptyConfig) { | |||
| @@ -559,7 +652,12 @@ export class PronounAsNoun extends Noun { | |||
| toString (): string { | |||
| if (this.options.objective) { | |||
| return new Noun(this.pronouns.objective, this.pronouns.objective, this.pronouns.possessive, this.options).toString() | |||
| return new Noun( | |||
| this.pronouns.objective, | |||
| this.pronouns.objective, | |||
| this.pronouns.possessive, | |||
| this.options | |||
| ).toString() | |||
| } else { | |||
| return super.toString() | |||
| } | |||
| @@ -9,9 +9,7 @@ import { InstantDigestionEffect, SurrenderEffect } from '@/game/combat/effects' | |||
| import moment from 'moment' | |||
| import { VoreAI } from '@/game/ai' | |||
| import { DeliciousPerk } from '@/game/combat/perks' | |||
| import Inazuma from '../creatures/characters/inazuma' | |||
| import Human from '../creatures/human' | |||
| import Slime from '../creatures/monsters/slime' | |||
| import Werewolf from '../creatures/monsters/werewolf' | |||
| function makeParty (): Creature[] { | |||
| @@ -103,13 +101,6 @@ export const Town = (): Place => { | |||
| "The center of town" | |||
| ) | |||
| const bossEncounters = [ | |||
| new Encounter( | |||
| { name: "Inazuma", intro: () => nilLog }, | |||
| makeParty().concat([new Inazuma()]) | |||
| ) | |||
| ] | |||
| home.choices.push( | |||
| new Choice( | |||
| "Nap", | |||
| @@ -228,19 +219,6 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| bossEncounters.forEach(encounter => { | |||
| bosses.choices.push( | |||
| new Choice( | |||
| encounter.desc.name, | |||
| "Boss fight!", | |||
| (world) => { | |||
| world.encounter = encounter | |||
| return nilLog | |||
| } | |||
| ) | |||
| ) | |||
| }) | |||
| debug.choices.push( | |||
| new Choice( | |||
| "Cut stats", | |||
| @@ -322,25 +300,6 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a slime", | |||
| "Go fight a slime", | |||
| (world, executor) => { | |||
| const enemy = new Slime() | |||
| const encounter = new Encounter( | |||
| { | |||
| name: "Fight some tasty nerd", | |||
| intro: () => new LogLine(`A slime draws near!`) | |||
| }, | |||
| [world.player, enemy].concat(world.party) | |||
| ) | |||
| world.encounter = encounter | |||
| return nilLog | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a werewolf", | |||
| @@ -1,48 +1,82 @@ | |||
| import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula } from '@/game/combat' | |||
| import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula, ConstantDamageFormula } from '@/game/combat' | |||
| import { LogLines, LogEntry, LogLine, nilLog, RandomEntry } from '@/game/interface' | |||
| import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe } from '@/game/language' | |||
| import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from '@/game/combat/actions' | |||
| import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe, Adjective } from '@/game/language' | |||
| import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction, StruggleMoveAction } from '@/game/combat/actions' | |||
| import * as Words from '@/game/words' | |||
| import { Creature } from '@/game/creature' | |||
| import { VoreRelay } from '@/game/events' | |||
| export enum VoreType { | |||
| Oral = "Oral Vore", | |||
| Anal = "Anal Vore", | |||
| Cock = "Cock Vore", | |||
| Unbirth = "Unbirthing", | |||
| Breast = "Breast Vore", | |||
| Bladder = "Bladder Vore", | |||
| Tail = "Tail Vore", | |||
| Goo = "Goo Vore" | |||
| Oral = "Oral Vore" | |||
| } | |||
| export const anyVore = new Set([ | |||
| VoreType.Oral, | |||
| VoreType.Anal, | |||
| VoreType.Cock, | |||
| VoreType.Unbirth, | |||
| VoreType.Breast, | |||
| VoreType.Bladder, | |||
| VoreType.Tail, | |||
| VoreType.Goo | |||
| VoreType.Oral | |||
| ]) | |||
| export type Wall = { | |||
| name: Word; | |||
| texture: Adjective; | |||
| material: Noun; | |||
| color: Adjective; | |||
| } | |||
| export type Fluid = { | |||
| name: Word; | |||
| color: Adjective; | |||
| sound: Word; | |||
| sloshVerb: Verb; | |||
| } | |||
| export type Gas = { | |||
| name: Word; | |||
| color: Adjective; | |||
| smell: Adjective; | |||
| bubbleVerb: Verb; | |||
| releaseVerb: Verb; | |||
| } | |||
| export enum ContainerCapability { | |||
| Consume, | |||
| Release, | |||
| Digest, | |||
| Absorb | |||
| } | |||
| export enum ConnectionDirection { | |||
| Deeper, | |||
| Neutral, | |||
| Shallower | |||
| } | |||
| export type Connection = { | |||
| destination: Container; | |||
| direction: ConnectionDirection; | |||
| } | |||
| export interface Container extends Actionable { | |||
| name: Noun; | |||
| owner: Creature; | |||
| voreTypes: Set<VoreType>; | |||
| capabilities: Set<ContainerCapability>; | |||
| capacity: number; | |||
| fullness: number; | |||
| connections: Array<Connection>; | |||
| wall: Wall | null; | |||
| fluid: Fluid | null; | |||
| gas: Gas | null; | |||
| voreRelay: VoreRelay; | |||
| contents: Array<Creature>; | |||
| describe: () => LogEntry; | |||
| digested: Array<Creature>; | |||
| canTake: (prey: Creature) => boolean; | |||
| consume: (prey: Creature) => LogEntry; | |||
| release: (prey: Creature) => LogEntry; | |||
| struggle: (prey: Creature) => LogEntry; | |||
| damage: DamageFormula; | |||
| sound: Word; | |||
| capacity: number; | |||
| fullness: number; | |||
| consumeVerb: Verb; | |||
| consumePreposition: Preposition; | |||
| @@ -51,37 +85,75 @@ export interface Container extends Actionable { | |||
| struggleVerb: Verb; | |||
| strugglePreposition: Preposition; | |||
| consumeLine (user: Creature, target: Creature): LogEntry; | |||
| canTake (prey: Creature): boolean; | |||
| consume (prey: Creature): LogEntry; | |||
| release (prey: Creature): LogEntry; | |||
| struggle (prey: Creature): LogEntry; | |||
| tick (dt: number, victims?: Array<Creature>): LogEntry; | |||
| digest (preys: Creature[]): LogEntry; | |||
| absorb (preys: Creature[]): LogEntry; | |||
| onDigest (prey: Creature): LogEntry; | |||
| onAbsorb (prey: Creature): LogEntry; | |||
| consumeLine (user: Creature, target: Creature): LogEntry; | |||
| statusLine (user: Creature, target: Creature): LogEntry; | |||
| describe (): LogEntry; | |||
| describeDetail (prey: Creature): LogEntry; | |||
| connect (dest: Connection): void; | |||
| } | |||
| export abstract class NormalContainer implements Container { | |||
| export abstract class DefaultContainer implements Container { | |||
| public name: Noun | |||
| contents: Array<Creature> = [] | |||
| actions: Array<Action> = [] | |||
| consumeVerb = new Verb('trap') | |||
| consumePreposition = new Preposition("in") | |||
| wall: Wall | null = null | |||
| fluid: Fluid | null = null | |||
| gas: Gas | null = null | |||
| connections: Array<Connection> = [] | |||
| voreRelay = new VoreRelay() | |||
| consumeVerb = new Verb('devour') | |||
| consumePreposition = new Preposition("into") | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| releasePreposition = new Preposition("from") | |||
| releasePreposition = new Preposition("out from") | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| strugglePreposition = new Preposition("in") | |||
| strugglePreposition = new Preposition("within") | |||
| fluidColor = "#00ff0088" | |||
| constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number) { | |||
| digested: Array<Creature> = [] | |||
| absorbed: Array<Creature> = [] | |||
| damage: DamageFormula = new ConstantDamageFormula(new Damage()); | |||
| sound = new Verb("slosh") | |||
| constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number, public capabilities: Set<ContainerCapability>) { | |||
| this.name = name.all | |||
| this.actions.push(new DevourAction(this)) | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| if (capabilities.has(ContainerCapability.Consume)) { | |||
| this.actions.push(new DevourAction(this)) | |||
| } | |||
| if (capabilities.has(ContainerCapability.Release)) { | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| } | |||
| get capacity (): number { | |||
| return this.capacityFactor * this.owner.voreStats.Mass | |||
| connect (connection: Connection): void { | |||
| this.connections.push(connection) | |||
| this.actions.push(new TransferAction(this, connection)) | |||
| this.actions.push(new StruggleMoveAction(this, connection.destination)) | |||
| } | |||
| consumeLine (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective} ${this.consumePreposition} ${user.pronouns.possessive} ${this.name}.`) | |||
| get capacity (): number { | |||
| return this.capacityFactor * this.owner.voreStats.Mass | |||
| } | |||
| statusLine (user: Creature, target: Creature): LogEntry { | |||
| @@ -99,7 +171,7 @@ export abstract class NormalContainer implements Container { | |||
| } | |||
| get fullness (): number { | |||
| return Array.from(this.contents.values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) | |||
| return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) | |||
| } | |||
| canTake (prey: Creature): boolean { | |||
| @@ -118,7 +190,14 @@ export abstract class NormalContainer implements Container { | |||
| } | |||
| this.contents.push(prey) | |||
| prey.containedIn = this | |||
| return this.consumeLine(this.owner, prey) | |||
| const results: Array<LogEntry> = [] | |||
| this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this))) | |||
| const relayResults = this.voreRelay.dispatch("onEaten", this, { prey: prey }) | |||
| const preyRelayResults = prey.voreRelay.dispatch("onEaten", this, { prey: prey }) | |||
| const consumeLineResult: LogEntry = this.consumeLine(this.owner, prey) | |||
| return new LogLines(...[consumeLineResult].concat(results).concat(relayResults).concat(preyRelayResults)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| @@ -128,7 +207,7 @@ export abstract class NormalContainer implements Container { | |||
| if (this.owner.containedIn !== null) { | |||
| this.owner.containedIn.contents.push(prey) | |||
| } | |||
| return this.releaseLine(this.owner, prey) | |||
| return new LogLines(this.releaseLine(this.owner, prey), this.voreRelay.dispatch("onReleased", this, { prey: prey })) | |||
| } | |||
| struggle (prey: Creature): LogEntry { | |||
| @@ -144,79 +223,22 @@ export abstract class NormalContainer implements Container { | |||
| return new LogLine(...lines) | |||
| } | |||
| } | |||
| export class Hand extends NormalContainer { | |||
| consumeVerb = new Verb("grab") | |||
| constructor (owner: Creature, capacity: number) { | |||
| super( | |||
| new ImproperNoun('hand'), | |||
| owner, | |||
| new Set(), | |||
| capacity | |||
| ) | |||
| } | |||
| } | |||
| export abstract class InnerContainer extends NormalContainer { | |||
| constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, private escape: Container) { | |||
| super(name, owner, voreTypes, capacity) | |||
| this.actions = [] | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| prey.containedIn = this.escape | |||
| this.contents = this.contents.filter(victim => victim !== prey) | |||
| return this.releaseLine(this.owner, prey) | |||
| } | |||
| } | |||
| export interface VoreContainer extends Container { | |||
| voreRelay: VoreRelay; | |||
| digested: Array<Creature>; | |||
| tick: (dt: number, victims?: Array<Creature>) => LogEntry; | |||
| digest: (preys: Creature[]) => LogEntry; | |||
| absorb: (preys: Creature[]) => LogEntry; | |||
| sound: Word; | |||
| fluidName: Word; | |||
| fluidColor: string; | |||
| onDigest: (prey: Creature) => LogEntry; | |||
| onAbsorb: (prey: Creature) => LogEntry; | |||
| } | |||
| describeDetail (prey: Creature): LogEntry { | |||
| const lines: Array<LogLine> = [] | |||
| export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer { | |||
| voreRelay = new VoreRelay() | |||
| consumeVerb = new Verb('devour') | |||
| consumePreposition = new Preposition("into") | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| releasePreposition = new Preposition("out from") | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| strugglePreposition = new Preposition("within") | |||
| fluidColor = "#00ff0088" | |||
| digested: Array<Creature> = [] | |||
| absorbed: Array<Creature> = [] | |||
| sound = new Verb("slosh") | |||
| abstract fluidName: Word | |||
| constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, private damage: DamageFormula) { | |||
| super(name, owner, voreTypes, capacity) | |||
| this.name = name | |||
| this.actions.push(new RubAction(this)) | |||
| } | |||
| if (this.gas) { | |||
| lines.push( | |||
| new LogLine(`${this.gas.color.capital} ${this.gas.name.plural} ${this.gas.bubbleVerb} in ${this.owner.name.possessive} ${this.name}.`) | |||
| ) | |||
| } | |||
| if (this.fluid) { | |||
| lines.push( | |||
| new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} around ${prey.name.objective}.`) | |||
| ) | |||
| } | |||
| get fullness (): number { | |||
| return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) | |||
| return new LogLine(...lines) | |||
| } | |||
| consumeLine (user: Creature, target: Creature) { | |||
| @@ -227,15 +249,21 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry { | |||
| return new RandomEntry( | |||
| const options = [ | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Churns)} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`), | |||
| new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`), | |||
| new LogLine(`${user.name.capital.possessive} ${this.name} ${user.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`), | |||
| new LogLine(`${target.name.capital} ${target.name.conjugate(new Verb("thrash", "thrashes"))} ${this.strugglePreposition} ${user.name.possessive} ${Words.Slick} ${this.name} as it ${Words.Churns.singular} ${target.pronouns.objective} for `, args.damage.renderShort(), `.`) | |||
| ) | |||
| ] | |||
| if (this.fluid) { | |||
| options.push(new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} and ${this.fluid.sound.singular} as ${this.owner.name.possessive} ${this.name} steadily ${Words.Digest.singular} ${target.name.objective}.`)) | |||
| } | |||
| return new RandomEntry(...options) | |||
| } | |||
| digestLine (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)} and ${target.pronouns.conjugate(Words.Digest)} into ${this.fluidName}.`) | |||
| return new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)}.`) | |||
| } | |||
| absorbLine (user: Creature, target: Creature): LogEntry { | |||
| @@ -250,23 +278,25 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| const tickedEntryList: LogEntry[] = [] | |||
| this.contents.forEach(prey => { | |||
| if (victims === undefined || victims.indexOf(prey) >= 0) { | |||
| const scaled = this.damage.calc(this.owner, prey).scale(dt / 60) | |||
| const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled) | |||
| if (modified.nonzero()) { | |||
| tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified })) | |||
| damageResults.push(prey.takeDamage(modified)) | |||
| if (this.capabilities.has(ContainerCapability.Digest)) { | |||
| this.contents.forEach(prey => { | |||
| if (victims === undefined || victims.indexOf(prey) >= 0) { | |||
| const scaled = this.damage.calc(this.owner, prey).scale(dt / 60) | |||
| const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled) | |||
| if (modified.nonzero()) { | |||
| tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified })) | |||
| damageResults.push(prey.takeDamage(modified)) | |||
| } | |||
| if (prey.vigors[Vigor.Health] <= 0) { | |||
| prey.destroyed = true | |||
| this.digested.push(prey) | |||
| justDigested.push(prey) | |||
| damageResults.push(this.onDigest(prey)) | |||
| } | |||
| } | |||
| if (prey.vigors[Vigor.Health] <= 0) { | |||
| prey.destroyed = true | |||
| this.digested.push(prey) | |||
| justDigested.push(prey) | |||
| damageResults.push(this.onDigest(prey)) | |||
| } | |||
| } | |||
| }) | |||
| }) | |||
| } | |||
| const tickedEntries = new LogLines(...tickedEntryList) | |||
| @@ -313,18 +343,6 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| return new LogLines(...preys.map(prey => this.digestLine(this.owner, prey))) | |||
| } | |||
| consume (prey: Creature): LogEntry { | |||
| const logLine = super.consume(prey) | |||
| const results: Array<LogEntry> = [] | |||
| this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this))) | |||
| const relayResults = this.voreRelay.dispatch("onEaten", this, { prey: prey }) | |||
| return new LogLines(...[logLine].concat(results).concat(relayResults)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| return new LogLines(super.release(prey), this.voreRelay.dispatch("onReleased", this, { prey: prey })) | |||
| } | |||
| onAbsorb (prey: Creature): LogEntry { | |||
| return this.voreRelay.dispatch("onAbsorbed", this, { prey: prey }) | |||
| } | |||
| @@ -334,243 +352,58 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| } | |||
| } | |||
| export abstract class InnerVoreContainer extends NormalVoreContainer { | |||
| constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, damage: DamageFormula, private escape: Container) { | |||
| super(name, owner, voreTypes, capacity, damage) | |||
| this.actions = [] | |||
| this.actions.push(new RubAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| prey.containedIn = this.escape | |||
| this.contents = this.contents.filter(victim => victim !== prey) | |||
| this.escape.consume(prey) | |||
| return this.releaseLine(this.owner, prey) | |||
| } | |||
| } | |||
| export class Stomach extends NormalVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('stomach', 'stomachs').all, owner, new Set([VoreType.Oral]), capacity, damage) | |||
| } | |||
| digest (preys: Creature[]): LogEntry { | |||
| if (preys.length === 0) { | |||
| return super.digest(preys) | |||
| } | |||
| const heal = new Damage( | |||
| { | |||
| amount: preys.reduce((total: number, next: Creature) => total + next.maxVigors.Health / 5, 0), | |||
| type: DamageType.Heal, | |||
| target: Vigor.Health | |||
| } | |||
| ) | |||
| this.owner.takeDamage(heal) | |||
| return new LogLines( | |||
| super.digest(preys), | |||
| new LogLine(`${this.owner.name.capital} ${this.owner.name.conjugate(new Verb("heal"))} for `, this.owner.effectiveDamage(heal).renderShort()) | |||
| ) | |||
| } | |||
| } | |||
| export class InnerStomach extends InnerVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| consumeVerb = new Verb('swallow') | |||
| releaseVerb = new Verb('hork') | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: VoreContainer) { | |||
| super(new ImproperNoun('inner stomach', 'inner stomachs').all, owner, new Set([VoreType.Oral]), capacity, damage, escape) | |||
| } | |||
| } | |||
| export class Bowels extends NormalVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('bowel', 'bowels').plural.all, owner, new Set([VoreType.Anal]), capacity, damage) | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }) { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`), | |||
| super.tickLine(user, target, args) | |||
| ) | |||
| } | |||
| } | |||
| export class Tail extends NormalVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('tail', 'tails').all, owner, new Set([VoreType.Tail]), capacity, damage) | |||
| export class Stomach extends DefaultContainer { | |||
| fluid = { | |||
| color: new Adjective("green"), | |||
| name: new Noun("chyme"), | |||
| sound: new Verb("gurgle"), | |||
| sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed") | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }) { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`) | |||
| ) | |||
| gas = { | |||
| bubbleVerb: new Verb("bubble", "bubbles", "bubbling", "bubbled"), | |||
| color: new Adjective("hazy"), | |||
| name: new Noun("fume", "fumes"), | |||
| releaseVerb: new Verb("belch", "belches", "belching", "belched"), | |||
| smell: new Adjective("acrid") | |||
| } | |||
| } | |||
| export class Goo extends NormalVoreContainer { | |||
| fluidName = new Noun("goo") | |||
| fluidColor = "#66ee66"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('goo', 'goo').all, owner, new Set([VoreType.Goo]), capacity, damage) | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }) { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`) | |||
| ) | |||
| wall = { | |||
| color: new Adjective("red"), | |||
| material: new Noun("muscle"), | |||
| name: new Noun("wall"), | |||
| texture: new Adjective("slimy") | |||
| } | |||
| } | |||
| export class Cock extends NormalVoreContainer { | |||
| fluidName = new Noun("cum") | |||
| constructor (owner: Creature, capacityFactor: number, damage: DamageFormula) { | |||
| super(new Noun("stomach"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([ | |||
| ContainerCapability.Digest, | |||
| ContainerCapability.Absorb | |||
| ])) | |||
| fluidColor = "#eeeeee66"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('cock').all, | |||
| owner, | |||
| new Set([VoreType.Cock]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} with ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`) | |||
| this.damage = damage | |||
| } | |||
| } | |||
| export class Balls extends InnerVoreContainer { | |||
| fluidName = new Noun("cum") | |||
| fluidColor = "#eeeeeecc"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) { | |||
| super( | |||
| new ImproperNoun('ball', 'balls').all.plural, | |||
| owner, | |||
| new Set([VoreType.Cock]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| export class Throat extends DefaultContainer { | |||
| fluid = { | |||
| color: new Adjective("clear"), | |||
| name: new Noun("saliva"), | |||
| sound: new Verb("squish", "squishes"), | |||
| sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed") | |||
| } | |||
| } | |||
| export class Slit extends NormalVoreContainer { | |||
| fluidName = new Noun("femcum") | |||
| fluidColor = "#cccccc99"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('slit').all, | |||
| owner, | |||
| new Set([VoreType.Unbirth]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| wall = { | |||
| color: new Adjective("red"), | |||
| material: new Noun("muscle"), | |||
| name: new Noun("wall"), | |||
| texture: new Adjective("slimy") | |||
| } | |||
| } | |||
| export class Womb extends InnerVoreContainer { | |||
| fluidName = new Noun("femcum") | |||
| fluidColor = "#ddddddbb"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) { | |||
| super( | |||
| new ImproperNoun('womb').all, | |||
| owner, | |||
| new Set([VoreType.Unbirth]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| } | |||
| } | |||
| export class Breasts extends NormalVoreContainer { | |||
| fluidName = new Noun("milk") | |||
| fluidColor = "#eeeeeecc"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('breast', 'breasts').all.plural, | |||
| owner, | |||
| new Set([VoreType.Breast]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| constructor (owner: Creature, capacityFactor: number) { | |||
| super(new Noun("throat"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([ | |||
| ContainerCapability.Consume, | |||
| ContainerCapability.Release | |||
| ])) | |||
| } | |||
| } | |||
| export class Bladder extends NormalVoreContainer { | |||
| fluidName = new Noun("piss") | |||
| fluidColor = "#eeee3399"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('bladder').all, | |||
| owner, | |||
| new Set([VoreType.Bladder]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| } | |||
| } | |||
| export class InnerBladder extends InnerVoreContainer { | |||
| fluidName = new Noun("piss") | |||
| fluidColor = "#eeee3399"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) { | |||
| super( | |||
| new ImproperNoun('bladder').all, | |||
| owner, | |||
| new Set([VoreType.Bladder]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| } | |||
| } | |||
| export function biconnectContainers (outer: VoreContainer, inner: VoreContainer): void { | |||
| const old = outer.onDigest | |||
| outer.onDigest = (prey: Creature) => { | |||
| const oldResult = old(prey) | |||
| outer.digested = outer.digested.filter(victim => victim !== prey) | |||
| inner.digested.push(prey) | |||
| return new LogLines(oldResult, inner.consumeLine(inner.owner, prey), inner.tick(0, [prey])) | |||
| } | |||
| outer.actions.push( | |||
| new TransferAction( | |||
| outer, | |||
| inner | |||
| ) | |||
| ) | |||
| inner.actions.push( | |||
| new TransferAction( | |||
| inner, | |||
| outer | |||
| ) | |||
| ) | |||
| } | |||