| @@ -6,7 +6,7 @@ module.exports = { | |||
| extends: [ | |||
| 'plugin:vue/essential', | |||
| '@vue/standard', | |||
| '@vue/typescript/recommended' | |||
| '@vue/typescript/recommended', | |||
| ], | |||
| parserOptions: { | |||
| ecmaVersion: 2020 | |||
| @@ -18,6 +18,11 @@ module.exports = { | |||
| '@typescript-eslint/no-unused-vars': 'off', | |||
| quotes: 'off', | |||
| 'function-paren-newline': ['error', 'multiline-arguments'], | |||
| '@typescript-eslint/member-ordering': ['warn'] | |||
| '@typescript-eslint/member-ordering': ['warn'], | |||
| 'indent': 'off', | |||
| '@typescript-eslint/indent': [ | |||
| 'error', | |||
| 2 | |||
| ], | |||
| } | |||
| } | |||
| @@ -193,4 +193,18 @@ html { | |||
| /* .component-fade-leave-active below version 2.1.8 */ { | |||
| opacity: 0; | |||
| } | |||
| .onomatopoeia { | |||
| font-weight: bold; | |||
| font-style: italic; | |||
| font-size: 200%; | |||
| animation: fly-in 0.4s; | |||
| animation-timing-function: ease-out; | |||
| display: inline-block; | |||
| } | |||
| @keyframes fly-in { | |||
| 0% { transform: scale(0, 0); opacity: 0 }; | |||
| 100% { transform: scale(1, 1); opacity: 1 }; | |||
| } | |||
| </style> | |||
| @@ -242,6 +242,15 @@ export default class Combat extends Vue { | |||
| } else { | |||
| this.executedRight(this.encounter.currentMove.ai.decide(this.encounter.currentMove, this.encounter)) | |||
| } | |||
| } else { | |||
| if (this.encounter.currentMove.containedIn) { | |||
| this.writeLog( | |||
| this.encounter.currentMove.containedIn.statusLine( | |||
| this.encounter.currentMove.containedIn.owner, | |||
| this.encounter.currentMove | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -397,7 +406,7 @@ export default class Combat extends Vue { | |||
| } | |||
| .left-stats { | |||
| flex-direction: row; | |||
| flex-direction: row-reverse; | |||
| } | |||
| .right-stats { | |||
| @@ -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)) | |||
| @@ -405,24 +481,69 @@ export abstract class Action { | |||
| } | |||
| try (user: Creature, targets: Array<Creature>): LogEntry { | |||
| // Check if any pre-action effect will cancel this action. | |||
| const preActionResults = user.effects.mapUntil(effect => effect.preAction(user), result => result.prevented) | |||
| const preActionLogs = new LogLines(...preActionResults.map(result => result.log)) | |||
| if (preActionResults.some(result => result.prevented)) { | |||
| return preActionLogs | |||
| } | |||
| // Check if any pre-receive-action effect will cancel this action. | |||
| const preReceiveActionResults = targets.mapUntil(target => { | |||
| const outcome = target.effects.mapUntil(effect => effect.preReceiveAction(user, target), result => result.prevented) | |||
| return outcome | |||
| }, results => results.some(result => result.prevented)) | |||
| console.log(preReceiveActionResults) | |||
| const preReceiveActionLogs = new LogLines(...preReceiveActionResults.flatMap( | |||
| target => target.map(result => result.log) | |||
| )) | |||
| if (preReceiveActionResults.some(results => results.some(result => result.prevented))) { | |||
| return new LogLines( | |||
| preActionLogs, | |||
| preReceiveActionLogs | |||
| ) | |||
| } | |||
| const results = targets.map(target => { | |||
| const failReason = this.tests.find(test => !test.test(user, target)) | |||
| if (failReason !== undefined) { | |||
| return { failed: true, target: target, log: failReason.fail(user, target) } | |||
| return { | |||
| failed: true, | |||
| target: target, | |||
| log: failReason.fail(user, target) | |||
| } | |||
| } else { | |||
| return { failed: false, target: target, log: this.execute(user, target) } | |||
| return { | |||
| failed: false, | |||
| target: target, | |||
| log: this.execute(user, target) | |||
| } | |||
| } | |||
| }) | |||
| return new LogLines( | |||
| preActionLogs, | |||
| preReceiveActionLogs, | |||
| ...results.map(result => result.log), | |||
| this.executeAll(user, results.filter(result => !result.failed).map(result => result.target)) | |||
| this.executeAll( | |||
| user, | |||
| results.filter(result => !result.failed).map(result => result.target) | |||
| ) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature, verbose = true): LogEntry { | |||
| return new LogLines( | |||
| ...(verbose ? this.conditions.map(condition => condition.explain(user, target)).concat([new Newline()]) : []), | |||
| ...(verbose | |||
| ? this.conditions | |||
| .map(condition => condition.explain(user, target)) | |||
| .concat([new Newline()]) | |||
| : []), | |||
| new LogLine( | |||
| `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` | |||
| ), | |||
| @@ -432,7 +553,10 @@ export abstract class Action { | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.tests.reduce((total, test) => total * test.odds(user, target), 1) | |||
| return this.tests.reduce( | |||
| (total, test) => total * test.odds(user, target), | |||
| 1 | |||
| ) | |||
| } | |||
| targets (primary: Creature, encounter: Encounter): Array<Creature> { | |||
| @@ -443,13 +567,13 @@ export abstract class Action { | |||
| return nilLog | |||
| } | |||
| abstract execute (user: Creature, target: Creature): LogEntry | |||
| abstract execute(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class CompositionAction extends Action { | |||
| public consequences: Array<Consequence>; | |||
| public groupConsequences: Array<GroupConsequence>; | |||
| public targeters: Array<Targeter>; | |||
| public consequences: Array<Consequence> | |||
| public groupConsequences: Array<GroupConsequence> | |||
| public targeters: Array<Targeter> | |||
| constructor ( | |||
| name: TextLike, | |||
| @@ -470,27 +594,34 @@ export class CompositionAction extends Action { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| ...this.consequences.filter(consequence => consequence.applicable(user, target)).map(consequence => consequence.apply(user, target)) | |||
| ...this.consequences | |||
| .filter(consequence => consequence.applicable(user, target)) | |||
| .map(consequence => consequence.apply(user, target)) | |||
| ) | |||
| } | |||
| executeAll (user: Creature, targets: Array<Creature>): LogEntry { | |||
| return new LogLines( | |||
| ...this.groupConsequences.map(consequence => consequence.apply(user, targets.filter(target => consequence.applicable(user, target)))) | |||
| ...this.groupConsequences.map(consequence => | |||
| consequence.apply( | |||
| user, | |||
| targets.filter(target => consequence.applicable(user, target)) | |||
| )) | |||
| ) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| ...this.consequences.map(consequence => consequence.describe(user, target)).concat( | |||
| new Newline(), | |||
| super.describe(user, target) | |||
| ) | |||
| ...this.consequences | |||
| .map(consequence => consequence.describe(user, target)) | |||
| .concat(new Newline(), super.describe(user, target)) | |||
| ) | |||
| } | |||
| targets (primary: Creature, encounter: Encounter) { | |||
| return this.targeters.flatMap(targeter => targeter.targets(primary, encounter)).unique() | |||
| return this.targeters | |||
| .flatMap(targeter => targeter.targets(primary, encounter)) | |||
| .unique() | |||
| } | |||
| } | |||
| @@ -515,12 +646,16 @@ export class Effective { | |||
| /** | |||
| * Executes when the effect is initially applied | |||
| */ | |||
| onApply (creature: Creature): LogEntry { return nilLog } | |||
| onApply (creature: Creature): LogEntry { | |||
| return nilLog | |||
| } | |||
| /** | |||
| * Executes when the effect is removed | |||
| */ | |||
| onRemove (creature: Creature): LogEntry { return nilLog } | |||
| onRemove (creature: Creature): LogEntry { | |||
| return nilLog | |||
| } | |||
| /** | |||
| * Executes before the creature tries to perform an action | |||
| @@ -535,7 +670,10 @@ export class Effective { | |||
| /** | |||
| * Executes before another creature tries to perform an action that targets this creature | |||
| */ | |||
| preReceiveAction (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { | |||
| preReceiveAction ( | |||
| creature: Creature, | |||
| attacker: Creature | |||
| ): { prevented: boolean; log: LogEntry } { | |||
| return { | |||
| prevented: false, | |||
| log: nilLog | |||
| @@ -552,7 +690,10 @@ export class Effective { | |||
| /** | |||
| * Executes before the creature is attacked | |||
| */ | |||
| preAttack (creature: Creature, attacker: Creature): { prevented: boolean; log: LogEntry } { | |||
| preAttack ( | |||
| creature: Creature, | |||
| attacker: Creature | |||
| ): { prevented: boolean; log: LogEntry } { | |||
| return { | |||
| prevented: false, | |||
| log: nilLog | |||
| @@ -580,7 +721,10 @@ export class Effective { | |||
| * Called when a test is about to resolve. Decides if the creature should automatically fail. | |||
| */ | |||
| failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } { | |||
| failTest ( | |||
| creature: Creature, | |||
| opponent: Creature | |||
| ): { failed: boolean; log: LogEntry } { | |||
| return { | |||
| failed: false, | |||
| log: nilLog | |||
| @@ -597,28 +741,56 @@ export class Effective { | |||
| /** | |||
| * Additively modifies a creature's score for an offensive test | |||
| */ | |||
| modTestOffense (attacker: Creature, defender: Creature, kind: TestCategory): number { | |||
| modTestOffense ( | |||
| attacker: Creature, | |||
| defender: Creature, | |||
| kind: TestCategory | |||
| ): number { | |||
| return 0 | |||
| } | |||
| /** | |||
| * Additively modifies a creature's score for a defensive test | |||
| */ | |||
| modTestDefense (defender: Creature, attacker: Creature, kind: TestCategory): number { | |||
| modTestDefense ( | |||
| defender: Creature, | |||
| attacker: Creature, | |||
| kind: TestCategory | |||
| ): number { | |||
| return 0 | |||
| } | |||
| /** | |||
| * Affects digestion damage | |||
| */ | |||
| modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage { | |||
| modDigestionDamage ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container, | |||
| damage: Damage | |||
| ): Damage { | |||
| return damage | |||
| } | |||
| /** | |||
| * Triggers after consumption | |||
| */ | |||
| postConsume (predator: Creature, prey: Creature, container: VoreContainer): LogEntry { | |||
| postConsume ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container | |||
| ): LogEntry { | |||
| return nilLog | |||
| } | |||
| /** | |||
| * Triggers after prey enters a container | |||
| */ | |||
| postEnter ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container | |||
| ): LogEntry { | |||
| return nilLog | |||
| } | |||
| @@ -653,24 +825,35 @@ export interface VisibleStatus { | |||
| * a status indicating that it is dead, but entities cannot be "given" the dead effect | |||
| */ | |||
| export class ImplicitStatus implements VisibleStatus { | |||
| topLeft = '' | |||
| bottomRight = '' | |||
| constructor (public name: TextLike, public desc: TextLike, public icon: string) { | |||
| topLeft = "" | |||
| bottomRight = "" | |||
| } | |||
| constructor ( | |||
| public name: TextLike, | |||
| public desc: TextLike, | |||
| public icon: string | |||
| ) {} | |||
| } | |||
| /** | |||
| * This kind of status is explicitly given to a creature. | |||
| */ | |||
| export abstract class StatusEffect extends Effective implements VisibleStatus { | |||
| constructor (public name: TextLike, public desc: TextLike, public icon: string) { | |||
| constructor ( | |||
| public name: TextLike, | |||
| public desc: TextLike, | |||
| public icon: string | |||
| ) { | |||
| super() | |||
| } | |||
| get topLeft () { return '' } | |||
| get bottomRight () { return '' } | |||
| get topLeft () { | |||
| return "" | |||
| } | |||
| get bottomRight () { | |||
| return "" | |||
| } | |||
| } | |||
| export type EncounterDesc = { | |||
| @@ -704,7 +887,9 @@ export class Encounter { | |||
| this.combatants.forEach(combatant => { | |||
| // this should never be undefined | |||
| const currentProgress = this.initiatives.get(combatant) ?? 0 | |||
| const remaining = (this.turnTime - currentProgress) / Math.sqrt(Math.max(combatant.stats.Agility, 1)) | |||
| const remaining = | |||
| (this.turnTime - currentProgress) / | |||
| Math.sqrt(Math.max(combatant.stats.Agility, 1)) | |||
| times.set(combatant, remaining) | |||
| }) | |||
| @@ -714,12 +899,18 @@ export class Encounter { | |||
| return closestTime <= nextTime ? closest : next | |||
| }, this.combatants[0]) | |||
| const closestRemaining = (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / Math.sqrt(Math.max(this.currentMove.stats.Agility, 1)) | |||
| const closestRemaining = | |||
| (this.turnTime - (this.initiatives.get(this.currentMove) ?? 0)) / | |||
| Math.sqrt(Math.max(this.currentMove.stats.Agility, 1)) | |||
| this.combatants.forEach(combatant => { | |||
| // still not undefined... | |||
| const currentProgress = this.initiatives.get(combatant) ?? 0 | |||
| this.initiatives.set(combatant, currentProgress + closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1))) | |||
| this.initiatives.set( | |||
| combatant, | |||
| currentProgress + | |||
| closestRemaining * Math.sqrt(Math.max(combatant.stats.Agility, 1)) | |||
| ) | |||
| }) | |||
| // TODO: still let the creature use drained-vigor moves | |||
| @@ -728,24 +919,21 @@ export class Encounter { | |||
| return this.nextMove(closestRemaining + totalTime) | |||
| } else { | |||
| // applies digestion every time combat advances | |||
| const tickResults = this.combatants.flatMap( | |||
| combatant => combatant.containers.map( | |||
| container => container.tick(5 * (closestRemaining + totalTime)) | |||
| ) | |||
| ) | |||
| const effectResults = this.currentMove.effects.map(effect => effect.preTurn(this.currentMove)).filter(effect => effect.prevented) | |||
| const tickResults = this.combatants.flatMap(combatant => | |||
| combatant.containers.map(container => | |||
| container.tick(5 * (closestRemaining + totalTime)))) | |||
| const effectResults = this.currentMove.effects | |||
| .map(effect => effect.preTurn(this.currentMove)) | |||
| .filter(effect => effect.prevented) | |||
| if (effectResults.some(result => result.prevented)) { | |||
| const parts = effectResults.map(result => result.log).concat([this.nextMove()]) | |||
| const parts = effectResults | |||
| .map(result => result.log) | |||
| .concat([this.nextMove()]) | |||
| return new LogLines( | |||
| ...parts, | |||
| ...tickResults | |||
| ) | |||
| return new LogLines(...parts, ...tickResults) | |||
| } else { | |||
| return new LogLines( | |||
| ...tickResults | |||
| ) | |||
| return new LogLines(...tickResults) | |||
| } | |||
| } | |||
| @@ -755,8 +943,12 @@ export class Encounter { | |||
| /** | |||
| * Combat is won once one side is completely disabled | |||
| */ | |||
| get winner (): null|Side { | |||
| const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.disabled).map(combatant => combatant.side)) | |||
| get winner (): null | Side { | |||
| const remaining: Set<Side> = new Set( | |||
| this.combatants | |||
| .filter(combatant => !combatant.disabled) | |||
| .map(combatant => combatant.side) | |||
| ) | |||
| if (remaining.size === 1) { | |||
| return Array.from(remaining)[0] | |||
| @@ -768,8 +960,12 @@ export class Encounter { | |||
| /** | |||
| * Combat is completely won once one side is completely destroyed | |||
| */ | |||
| get totalWinner (): null|Side { | |||
| const remaining: Set<Side> = new Set(this.combatants.filter(combatant => !combatant.destroyed).map(combatant => combatant.side)) | |||
| get totalWinner (): null | Side { | |||
| const remaining: Set<Side> = new Set( | |||
| this.combatants | |||
| .filter(combatant => !combatant.destroyed) | |||
| .map(combatant => combatant.side) | |||
| ) | |||
| if (remaining.size === 1) { | |||
| return Array.from(remaining)[0] | |||
| @@ -780,27 +976,23 @@ export class Encounter { | |||
| } | |||
| export abstract class Consequence { | |||
| constructor (public conditions: Condition[]) { | |||
| } | |||
| constructor (public conditions: Condition[]) {} | |||
| applicable (user: Creature, target: Creature): boolean { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| } | |||
| abstract describe (user: Creature, target: Creature): LogEntry | |||
| abstract apply (user: Creature, target: Creature): LogEntry | |||
| abstract describe(user: Creature, target: Creature): LogEntry | |||
| abstract apply(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export abstract class GroupConsequence { | |||
| constructor (public conditions: Condition[]) { | |||
| } | |||
| constructor (public conditions: Condition[]) {} | |||
| applicable (user: Creature, target: Creature): boolean { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| } | |||
| abstract describe (user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract apply (user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract describe(user: Creature, targets: Array<Creature>): LogEntry | |||
| abstract apply(user: Creature, targets: Array<Creature>): LogEntry | |||
| } | |||
| @@ -4,9 +4,10 @@ import { Entity } from '@/game/entity' | |||
| import { Creature } from "../creature" | |||
| import { Damage, DamageFormula, Vigor, Action, Condition, CombatTest, CompositionAction } from '@/game/combat' | |||
| import { LogLine, LogLines, LogEntry } from '@/game/interface' | |||
| import { VoreContainer, Container } from '@/game/vore' | |||
| import { Connection, Container } from '@/game/vore' | |||
| import { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from '@/game/combat/conditions' | |||
| import { ConsumeConsequence } from '@/game/combat/consequences' | |||
| import * as Words from '@/game/words' | |||
| /** | |||
| * The PassAction has no effect. | |||
| @@ -216,8 +217,50 @@ export class StruggleAction extends Action { | |||
| ) | |||
| } | |||
| /** | |||
| * Tries to move between containers | |||
| */ | |||
| export class StruggleMoveAction extends Action { | |||
| constructor (public from: Container, public to: Container) { | |||
| super( | |||
| new DynText('Struggle (', new LiveText(from, x => x.name.all), ' to ', new LiveText(to, x => x.name.all), ')'), | |||
| 'Try to escape from your foe', | |||
| [new CapableCondition(), new PairCondition(), new ContainedByCondition(from)], | |||
| [ | |||
| new CompositionTest( | |||
| [ | |||
| new OpposedStatScorer( | |||
| { Power: 1, Agility: 1, Bulk: 0.05 }, | |||
| { Toughness: 1, Reflexes: 1, Mass: 0.05 } | |||
| ) | |||
| ], | |||
| (user, target) => new LogLine(`${user.name.capital} ${user.name.conjugate(new Verb('fail'))} to escape from ${target.name.possessive} ${from.name}.`), | |||
| TestCategory.Vore, | |||
| -5 | |||
| ) | |||
| ] | |||
| ) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (user.containedIn !== null) { | |||
| return new LogLines(this.successLine(user, target, { container: this.from }), user.containedIn.exit(user), this.to.enter(user)) | |||
| } else { | |||
| return new LogLine("Vore's bugged!") | |||
| } | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Try to escape from ${target.baseName.possessive} ${this.from.name}. `, super.describe(user, target)) | |||
| } | |||
| protected successLine: PairLineArgs<Entity, { container: Container }> = (prey, pred, args) => new LogLine( | |||
| `${prey.name.capital} ${prey.name.conjugate(new Verb('escape'))}!` | |||
| ) | |||
| } | |||
| export class RubAction extends Action { | |||
| constructor (protected container: VoreContainer) { | |||
| constructor (protected container: Container) { | |||
| super( | |||
| new DynText('Rub (', new LiveText(container, container => container.name.all), ')'), | |||
| 'Digest your prey more quickly', | |||
| @@ -234,8 +277,12 @@ export class RubAction extends Action { | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| const results = this.container.tick(60) | |||
| return new LogLines(results) | |||
| const results: Array<LogEntry> = [] | |||
| results.push(new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(Words.Rub)} ${user.pronouns.possessive} ${Words.Full} ${this.container.name}.` | |||
| )) | |||
| results.push(this.container.tick(60)) | |||
| return new LogLines(...results) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| @@ -272,17 +319,15 @@ export class ReleaseAction extends Action { | |||
| export class TransferAction extends Action { | |||
| verb: Verb = new Verb('send') | |||
| constructor (protected from: Container, protected to: Container, name = 'Transfer') { | |||
| constructor (public from: Container, public to: Connection, name = 'Transfer') { | |||
| super( | |||
| name, | |||
| `${from.name.all.capital} to ${to.name.all}`, | |||
| `${from.name.all.capital} to ${to.destination.name.all}`, | |||
| [new CapableCondition(), new PairCondition()] | |||
| ) | |||
| } | |||
| line: PairLineArgs<Creature, { from: Container; to: Container }> = (user, target, args) => new LogLine( | |||
| `${user.name.capital} ${user.name.conjugate(this.verb)} ${target.name.objective} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}` | |||
| ) | |||
| line: PairLineArgs<Creature, { from: Container; to: Connection }> = (user, target, args) => this.to.description(this.from, this.to.destination, target) | |||
| allowed (user: Creature, target: Creature) { | |||
| if (target.containedIn === this.from && this.from.contents.includes(target)) { | |||
| @@ -293,13 +338,18 @@ export class TransferAction extends Action { | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| this.from.release(target) | |||
| this.to.consume(target) | |||
| return this.line(user, target, { from: this.from, to: this.to }) | |||
| const results = [ | |||
| this.from.exit(target), | |||
| this.line(user, target, { from: this.from, to: this.to }), | |||
| this.to.destination.enter(target) | |||
| ] | |||
| return new LogLines(...results) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Push ${target.baseName} from your ${this.from.name} to your ${this.to.name}`) | |||
| return new LogLine(`Push ${target.baseName} from your ${this.from.name} to your ${this.to.destination.name}`) | |||
| } | |||
| } | |||
| @@ -1,13 +1,22 @@ | |||
| import { StatusEffect, Damage, DamageType, Action, Condition, Vigor, Stat } from '@/game/combat' | |||
| import { DynText, LiveText, ToBe, Verb } from '@/game/language' | |||
| import { | |||
| StatusEffect, | |||
| Damage, | |||
| DamageType, | |||
| Action, | |||
| Condition, | |||
| Vigor, | |||
| Stat | |||
| } from "@/game/combat" | |||
| import { DynText, LiveText, ToBe, Verb } from "@/game/language" | |||
| import { Creature } from "../creature" | |||
| import { LogLine, LogEntry, LogLines, FAElem, nilLog } from '@/game/interface' | |||
| import { VoreContainer } from '@/game/vore' | |||
| import * as Words from '@/game/words' | |||
| import { LogLine, LogEntry, LogLines, FAElem, nilLog } from "@/game/interface" | |||
| import { Container, ContainerCapability } from "@/game/vore" | |||
| import * as Words from "@/game/words" | |||
| import * as Onomatopoeia from "@/game/onomatopoeia" | |||
| export class InstantKillEffect extends StatusEffect { | |||
| constructor () { | |||
| super('Instant Kill', 'Instant kill!', 'fas fa-skull') | |||
| super("Instant Kill", "Instant kill!", "fas fa-skull") | |||
| } | |||
| onApply (creature: Creature) { | |||
| @@ -15,8 +24,10 @@ export class InstantKillEffect extends StatusEffect { | |||
| creature.removeEffect(this) | |||
| return new LogLines( | |||
| new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate(new ToBe())} killed instantly! `, | |||
| new FAElem('fas fa-skull') | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} killed instantly! `, | |||
| new FAElem("fas fa-skull") | |||
| ), | |||
| creature.takeDamage(new Damage()) | |||
| ) | |||
| @@ -25,8 +36,12 @@ export class InstantKillEffect extends StatusEffect { | |||
| export class StunEffect extends StatusEffect { | |||
| constructor (private duration: number) { | |||
| super('Stun', 'Cannot act!', 'fas fa-sun') | |||
| this.desc = new DynText('Stunned for your next ', new LiveText(this, x => x.duration), ' actions!') | |||
| super("Stun", "Cannot act!", "fas fa-sun") | |||
| this.desc = new DynText( | |||
| "Stunned for your next ", | |||
| new LiveText(this, x => x.duration), | |||
| " actions!" | |||
| ) | |||
| } | |||
| get topLeft () { | |||
| @@ -34,11 +49,19 @@ export class StunEffect extends StatusEffect { | |||
| } | |||
| onApply (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} is stunned!`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} is stunned!` | |||
| ) | |||
| } | |||
| onRemove (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new ToBe())} no longer stunned.`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} no longer stunned.` | |||
| ) | |||
| } | |||
| preTurn (creature: Creature): { prevented: boolean; log: LogEntry } { | |||
| @@ -46,7 +69,9 @@ export class StunEffect extends StatusEffect { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLines( | |||
| `${creature.name.capital} ${creature.name.conjugate(new ToBe())} stunned! ${creature.pronouns.capital.subjective} can't move.`, | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} stunned! ${creature.pronouns.capital.subjective} can't move.`, | |||
| creature.removeEffect(this) | |||
| ) | |||
| } | |||
| @@ -54,7 +79,9 @@ export class StunEffect extends StatusEffect { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLines( | |||
| `${creature.name.capital} ${creature.name.conjugate(new ToBe())} stunned! ${creature.pronouns.capital.subjective} can't move!` | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new ToBe() | |||
| )} stunned! ${creature.pronouns.capital.subjective} can't move!` | |||
| ) | |||
| } | |||
| } | |||
| @@ -63,15 +90,30 @@ export class StunEffect extends StatusEffect { | |||
| export class DamageTypeResistanceEffect extends StatusEffect { | |||
| constructor (private damageTypes: DamageType[], private amount: number) { | |||
| super('Resistance', 'Block ' + ((1 - amount) * 100).toFixed() + '% of these damage types: ' + damageTypes.join(", "), 'fas fa-shield-alt') | |||
| super( | |||
| "Resistance", | |||
| "Block " + | |||
| ((1 - amount) * 100).toFixed() + | |||
| "% of these damage types: " + | |||
| damageTypes.join(", "), | |||
| "fas fa-shield-alt" | |||
| ) | |||
| } | |||
| onApply (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new Verb('gain'))} a shield!`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new Verb("gain") | |||
| )} a shield!` | |||
| ) | |||
| } | |||
| onRemove (creature: Creature) { | |||
| return new LogLine(`${creature.name.capital} ${creature.name.conjugate(new Verb('lose'))} ${creature.pronouns.possessive} shield!`) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate(new Verb("lose"))} ${ | |||
| creature.pronouns.possessive | |||
| } shield!` | |||
| ) | |||
| } | |||
| modResistance (type: DamageType, factor: number) { | |||
| @@ -85,17 +127,27 @@ export class DamageTypeResistanceEffect extends StatusEffect { | |||
| export class PredatorCounterEffect extends StatusEffect { | |||
| constructor (private devour: Action, private chance: number) { | |||
| super('Predatory Counter', 'Eat them back', 'fas fa-redo') | |||
| this.desc = new DynText(new LiveText(this, x => (x.chance * 100).toFixed(0)), '% chance to devour your attackers') | |||
| super("Predatory Counter", "Eat them back", "fas fa-redo") | |||
| this.desc = new DynText( | |||
| new LiveText(this, x => (x.chance * 100).toFixed(0)), | |||
| "% chance to devour your attackers" | |||
| ) | |||
| } | |||
| preAttack (creature: Creature, attacker: Creature) { | |||
| if (this.devour.allowed(creature, attacker) && Math.random() < this.chance) { | |||
| if ( | |||
| this.devour.allowed(creature, attacker) && | |||
| Math.random() < this.chance | |||
| ) { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLines( | |||
| `${creature.name.capital} ${creature.name.conjugate(new Verb('surprise'))} ${attacker.name.objective} and ${creature.name.conjugate(new Verb('try', 'tries'))} to devour ${attacker.pronouns.objective}!`, | |||
| this.devour.execute(creature, attacker) | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new Verb("surprise") | |||
| )} ${attacker.name.objective} and ${creature.name.conjugate( | |||
| new Verb("try", "tries") | |||
| )} to devour ${attacker.pronouns.objective}!`, | |||
| this.devour.execute(creature, attacker) | |||
| ) | |||
| } | |||
| } else { | |||
| @@ -106,20 +158,31 @@ export class PredatorCounterEffect extends StatusEffect { | |||
| export class UntouchableEffect extends StatusEffect { | |||
| constructor () { | |||
| super('Untouchable', 'Cannot be attacked', 'fas fa-times') | |||
| super("Untouchable", "Cannot be attacked", "fas fa-times") | |||
| } | |||
| preAttack (creature: Creature, attacker: Creature) { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLine(`${creature.name.capital} cannot be attacked.`) | |||
| preReceiveAction (user: Creature, target: Creature) { | |||
| if (user !== target) { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLine(`${target.name.capital} cannot be attacked.`) | |||
| } | |||
| } else { | |||
| return { | |||
| prevented: false, | |||
| log: nilLog | |||
| } | |||
| } | |||
| } | |||
| } | |||
| export class DazzlingEffect extends StatusEffect { | |||
| constructor (private conditions: Condition[]) { | |||
| super('Dazzling', 'Stuns enemies who try to affect this creature', 'fas fa-spinner') | |||
| super( | |||
| "Dazzling", | |||
| "Stuns enemies who try to affect this creature", | |||
| "fas fa-spinner" | |||
| ) | |||
| } | |||
| preReceiveAction (creature: Creature, attacker: Creature) { | |||
| @@ -127,7 +190,9 @@ export class DazzlingEffect extends StatusEffect { | |||
| attacker.applyEffect(new StunEffect(1)) | |||
| return { | |||
| prevented: true, | |||
| log: new LogLine(`${attacker.name.capital} can't act against ${creature.name.objective}!`) | |||
| log: new LogLine( | |||
| `${attacker.name.capital} can't act against ${creature.name.objective}!` | |||
| ) | |||
| } | |||
| } else { | |||
| return { | |||
| @@ -140,21 +205,32 @@ export class DazzlingEffect extends StatusEffect { | |||
| export class SurrenderEffect extends StatusEffect { | |||
| constructor () { | |||
| super('Surrendered', 'This creature has given up, and will fail most tests', 'fas fa-flag') | |||
| super( | |||
| "Surrendered", | |||
| "This creature has given up, and will fail most tests", | |||
| "fas fa-flag" | |||
| ) | |||
| } | |||
| onApply (creature: Creature): LogEntry { | |||
| creature.takeDamage( | |||
| new Damage( | |||
| { amount: creature.vigors.Resolve, target: Vigor.Resolve, type: DamageType.Pure } | |||
| ) | |||
| new Damage({ | |||
| amount: creature.vigors.Resolve, | |||
| target: Vigor.Resolve, | |||
| type: DamageType.Pure | |||
| }) | |||
| ) | |||
| return new LogLine( | |||
| `${creature.name.capital} ${creature.name.conjugate(new Verb('surrender'))}!` | |||
| `${creature.name.capital} ${creature.name.conjugate( | |||
| new Verb("surrender") | |||
| )}!` | |||
| ) | |||
| } | |||
| failTest (creature: Creature, opponent: Creature): { failed: boolean; log: LogEntry } { | |||
| failTest ( | |||
| creature: Creature, | |||
| opponent: Creature | |||
| ): { failed: boolean; log: LogEntry } { | |||
| return { | |||
| failed: true, | |||
| log: nilLog | |||
| @@ -164,7 +240,7 @@ export class SurrenderEffect extends StatusEffect { | |||
| export class SizeEffect extends StatusEffect { | |||
| constructor (private change: number) { | |||
| super('Size-Shifted', 'This creature has changed in size', 'fas fa-ruler') | |||
| super("Size-Shifted", "This creature has changed in size", "fas fa-ruler") | |||
| } | |||
| onApply (creature: Creature): LogLine { | |||
| @@ -178,26 +254,47 @@ export class SizeEffect extends StatusEffect { | |||
| export class DigestionPowerEffect extends StatusEffect { | |||
| constructor (private factor: number) { | |||
| super('Acid-fueled', 'This creature is digesting faster than nomral', 'fas fa-flask') | |||
| super( | |||
| "Acid-fueled", | |||
| "This creature is digesting faster than nomral", | |||
| "fas fa-flask" | |||
| ) | |||
| } | |||
| onApply (creature: Creature): LogLine { | |||
| const voreContainer: VoreContainer|undefined = creature.containers.find(c => c.digest !== null) | |||
| const voreContainer: Container | undefined = creature.containers.find( | |||
| c => c.digest !== null | |||
| ) | |||
| if (voreContainer !== undefined) { | |||
| return new LogLine(`${creature.name.capital.possessive}'s ${voreContainer.name} ${Words.Churns} and ${voreContainer.sound}`) | |||
| return new LogLine( | |||
| `${creature.name.capital.possessive}'s ${voreContainer.name} ${Words.Churns} and ${voreContainer.sound}` | |||
| ) | |||
| } else { | |||
| return new LogLine(`${creature.name.capital} can't digest people...`) | |||
| } | |||
| } | |||
| modDigestionDamage (predator: Creature, prey: Creature, container: VoreContainer, damage: Damage): Damage { | |||
| modDigestionDamage ( | |||
| predator: Creature, | |||
| prey: Creature, | |||
| container: Container, | |||
| damage: Damage | |||
| ): Damage { | |||
| return damage.scale(this.factor) | |||
| } | |||
| } | |||
| export class StatEffect extends StatusEffect { | |||
| constructor (private stat: Stat, private amount: number, private factor: number) { | |||
| super('Stat boosted', 'This creature has modified stats', 'fas fa-user-plus') | |||
| constructor ( | |||
| private stat: Stat, | |||
| private amount: number, | |||
| private factor: number | |||
| ) { | |||
| super( | |||
| "Stat boosted", | |||
| "This creature has modified stats", | |||
| "fas fa-user-plus" | |||
| ) | |||
| } | |||
| modStat (creature: Creature, stat: Stat, current: number): number { | |||
| @@ -211,16 +308,26 @@ export class StatEffect extends StatusEffect { | |||
| export class InstantDigestionEffect extends StatusEffect { | |||
| constructor () { | |||
| super("Instant digestion", "This creature will melt people instantly", "fas fa-skull") | |||
| super( | |||
| "Instant digestion", | |||
| "This creature will melt people instantly", | |||
| "fas fa-skull" | |||
| ) | |||
| } | |||
| postConsume (predator: Creature, prey: Creature, container: VoreContainer) { | |||
| postEnter (predator: Creature, prey: Creature, container: Container) { | |||
| if (!container.capabilities.has(ContainerCapability.Digest)) { | |||
| return nilLog | |||
| } | |||
| prey.applyEffect(new InstantKillEffect()) | |||
| predator.voreStats.Mass += prey.voreStats.Mass | |||
| prey.voreStats.Mass = 0 | |||
| return new LogLines( | |||
| `${prey.name.capital} ${prey.name.conjugate(new ToBe())} instantly digested! `, | |||
| new FAElem('fas fa-skull'), | |||
| `${prey.name.capital} ${prey.name.conjugate( | |||
| new ToBe() | |||
| )} instantly digested! `, | |||
| new FAElem("fas fa-skull"), | |||
| Onomatopoeia.makeOnomatopoeia(Onomatopoeia.Crunch), | |||
| container.tick(0, [prey]) | |||
| ) | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import { Damage, Stats, Action, Vigor, Side, VisibleStatus, ImplicitStatus, StatusEffect, DamageType, Effective, VoreStat, VoreStats, DamageInstance, Stat, Vigors, Encounter } from '@/game/combat' | |||
| import { Noun, Pronoun, SoloLine, Verb } from '@/game/language' | |||
| import { LogEntry, LogLines, LogLine } from '@/game/interface' | |||
| import { VoreContainer, VoreType, Container } from '@/game/vore' | |||
| import { VoreType, Container } from '@/game/vore' | |||
| import { Item, EquipmentSlot, Equipment, ItemKind, Currency } from '@/game/items' | |||
| import { PassAction } from '@/game/combat/actions' | |||
| import { AI, RandomAI } from '@/game/ai' | |||
| @@ -31,8 +31,7 @@ export class Creature extends Entity { | |||
| destroyed = false; | |||
| containers: Array<VoreContainer> = [] | |||
| otherContainers: Array<Container> = [] | |||
| containers: Array<Container> = [] | |||
| containedIn: Container | null = null | |||
| @@ -42,12 +41,14 @@ export class Creature extends Entity { | |||
| desc = "Some creature"; | |||
| get effects (): Array<Effective> { | |||
| return (this.statusEffects as Effective[]).concat( | |||
| const effects: Array<Effective> = (this.statusEffects as Effective[]).concat( | |||
| Object.values(this.equipment).filter(item => item !== undefined).flatMap( | |||
| item => (item as Equipment).effects | |||
| ), | |||
| this.perks | |||
| ) | |||
| return effects | |||
| } | |||
| statusEffects: Array<StatusEffect> = []; | |||
| @@ -81,7 +82,7 @@ export class Creature extends Entity { | |||
| this.voreStats = { | |||
| get [VoreStat.Bulk] () { | |||
| return self.containers.reduce( | |||
| (total: number, container: VoreContainer) => { | |||
| (total: number, container: Container) => { | |||
| return total + container.contents.reduce( | |||
| (total: number, prey: Creature) => { | |||
| return total + prey.voreStats.Bulk | |||
| @@ -111,7 +112,7 @@ export class Creature extends Entity { | |||
| }, | |||
| get [VoreStat.Prey] () { | |||
| return self.containers.reduce( | |||
| (total: number, container: VoreContainer) => { | |||
| (total: number, container: Container) => { | |||
| return total + container.contents.concat(container.digested).reduce( | |||
| (total: number, prey: Creature) => { | |||
| return total + 1 + prey.voreStats[VoreStat.Prey] | |||
| @@ -206,15 +207,11 @@ export class Creature extends Entity { | |||
| return effect.onApply(this) | |||
| } | |||
| addVoreContainer (container: VoreContainer): void { | |||
| addContainer (container: Container): void { | |||
| this.containers.push(container) | |||
| this.voreRelay.connect(container.voreRelay) | |||
| } | |||
| addOtherContainer (container: Container): void { | |||
| this.otherContainers.push(container) | |||
| } | |||
| addPerk (perk: Perk): void { | |||
| this.perks.push(perk) | |||
| } | |||
| @@ -265,6 +262,7 @@ export class Creature extends Entity { | |||
| this.statusEffects.forEach(effect => { | |||
| results.push(effect) | |||
| }) | |||
| return results | |||
| } | |||
| @@ -273,7 +271,6 @@ export class Creature extends Entity { | |||
| this.actions, | |||
| this.containers.flatMap(container => container.actions), | |||
| target.otherActions, | |||
| this.otherContainers.flatMap(container => container.actions), | |||
| Object.values(this.equipment).filter(item => item !== undefined).flatMap(item => (item as Equipment).actions), | |||
| this.items.filter(item => item.kind === ItemKind.Consumable && !item.consumed).flatMap(item => item.actions), | |||
| this.perks.flatMap(perk => perk.actions(this)) | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -0,0 +1,95 @@ | |||
| import { VoreAI } from '@/game/ai' | |||
| import { DamageType, Side, Stat, StatDamageFormula, StatusEffect, Vigor } from '@/game/combat' | |||
| import { DigestionPowerEffect } from '@/game/combat/effects' | |||
| import { Creature } from '@/game/creature' | |||
| import { LogEntry, LogLine, nilLog } from '@/game/interface' | |||
| import { ImproperNoun, MalePronouns, ObjectPronouns, Preposition, Verb } from '@/game/language' | |||
| import { anyVore, ConnectionDirection, Container, Stomach, Throat, transferDescription } from '@/game/vore' | |||
| import * as Words from '@/game/words' | |||
| import * as Onomatopoeia from '@/game/onomatopoeia' | |||
| export default class Werewolf extends Creature { | |||
| constructor () { | |||
| super( | |||
| new ImproperNoun("werewolf", "werewolves"), | |||
| new ImproperNoun("werewolf", "werewolves"), | |||
| MalePronouns, | |||
| { | |||
| Power: 45, | |||
| Toughness: 30, | |||
| Agility: 25, | |||
| Reflexes: 25, | |||
| Charm: 10, | |||
| Willpower: 15 | |||
| }, | |||
| anyVore, | |||
| anyVore, | |||
| 75 | |||
| ) | |||
| const throat = new Throat( | |||
| this, | |||
| 25 | |||
| ) | |||
| const stomach = new Stomach( | |||
| this, | |||
| 50, | |||
| new StatDamageFormula([ | |||
| { fraction: 1, stat: Stat.Toughness, type: DamageType.Acid, target: Vigor.Health } | |||
| ]) | |||
| ) | |||
| stomach.effects.push(new class extends StatusEffect { | |||
| constructor () { | |||
| super( | |||
| "Pinned", | |||
| "Prey sometimes can't move.", | |||
| "fas fa-sun" | |||
| ) | |||
| } | |||
| onApply (creature: Creature): LogEntry { | |||
| return new LogLine( | |||
| `${stomach.owner.name.capital.possessive} ${stomach.name} is incredibly tight, gripping ${creature.name.objective} like a vice!` | |||
| ) | |||
| } | |||
| preAction (creature: Creature): { prevented: boolean; log: LogEntry } { | |||
| if (Math.random() < 0.5) { | |||
| return { | |||
| prevented: true, | |||
| log: new LogLine(`${creature.name.capital} can't move!`) | |||
| } | |||
| } else { | |||
| return { | |||
| prevented: false, | |||
| log: nilLog | |||
| } | |||
| } | |||
| } | |||
| }()) | |||
| this.addContainer(throat) | |||
| this.addContainer(stomach) | |||
| throat.connect({ | |||
| destination: stomach, | |||
| direction: ConnectionDirection.Deeper, | |||
| description: transferDescription(Words.Swallow, new Preposition("down")) | |||
| }) | |||
| stomach.connect({ | |||
| destination: throat, | |||
| direction: ConnectionDirection.Shallower, | |||
| description: transferDescription(new Verb("hork"), new Preposition("up")) | |||
| }) | |||
| stomach.voreRelay.subscribe("onDigested", (sender, args) => { | |||
| return Onomatopoeia.makeOnomatopoeia(Onomatopoeia.Burp) | |||
| }) | |||
| this.side = Side.Monsters | |||
| this.ai = new VoreAI(this) | |||
| } | |||
| } | |||
| @@ -1,10 +1,10 @@ | |||
| import { Creature } from "../creature" | |||
| import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '@/game/language' | |||
| import { Damage, DamageType, Vigor, ConstantDamageFormula } from '@/game/combat' | |||
| import { Stomach, Bowels, anyVore, Cock, Balls, Breasts, InnerBladder, Slit, Womb, biconnectContainers } from '@/game/vore' | |||
| import { Stomach, anyVore, Container } from '@/game/vore' | |||
| import { AttackAction } from '@/game/combat/actions' | |||
| import { RavenousPerk, BellyBulwakPerk, FlauntPerk } from '@/game/combat/perks' | |||
| import { VoreAI } from "../ai" | |||
| import { nilLog } from "../interface" | |||
| export default class Player extends Creature { | |||
| constructor () { | |||
| @@ -21,10 +21,18 @@ export default class Player extends Creature { | |||
| this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina })))) | |||
| const stomach = new Stomach(this, 2, new ConstantDamageFormula(new Damage({ amount: 20, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.addVoreContainer(stomach) | |||
| this.addContainer(stomach) | |||
| this.perspective = POV.Second | |||
| this.ai = new VoreAI(this) | |||
| this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => { | |||
| if (this === args.prey) { | |||
| return sender.describeDetail(this) | |||
| } else { | |||
| return nilLog | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import { TargetDrainedVigorCondition } from '@/game/combat/conditions' | |||
| import { Creature } from '@/game/creature' | |||
| import { LogEntry, LogLines, nilLog } from "@/game/interface" | |||
| import { VoreContainer } from '@/game/vore' | |||
| import { Container } from '@/game/vore' | |||
| import { Action } from './combat' | |||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
| @@ -43,13 +43,15 @@ abstract class Relay<Sender, EventMap extends { [name: string]: any }> { | |||
| type VoreMap = { | |||
| "onEaten": { prey: Creature }; | |||
| "onReleased": { prey: Creature }; | |||
| "onEntered": { prey: Creature }; | |||
| "onExited": { prey: Creature }; | |||
| "onDigested": { prey: Creature }; | |||
| "onAbsorbed": { prey: Creature }; | |||
| } | |||
| export class VoreRelay extends Relay<VoreContainer, VoreMap> { | |||
| export class VoreRelay extends Relay<Container, VoreMap> { | |||
| constructor () { | |||
| super(["onEaten", "onReleased", "onDigested", "onAbsorbed"]) | |||
| super(["onEaten", "onReleased", "onEntered", "onExited", "onDigested", "onAbsorbed"]) | |||
| } | |||
| } | |||
| @@ -55,7 +55,8 @@ export class LogLines implements LogEntry { | |||
| export enum FormatOpt { | |||
| Damage = "log-damage", | |||
| DamageInst = "damage-instance" | |||
| DamageInst = "damage-instance", | |||
| Onomatopoeia = "onomatopoeia" | |||
| } | |||
| /** | |||
| @@ -1,6 +1,11 @@ | |||
| import { LogEntry } from '@/game/interface' | |||
| import { LogEntry } from "@/game/interface" | |||
| import { parseTwoDigitYear } from "moment" | |||
| export enum POV {First, Second, Third} | |||
| export enum POV { | |||
| First, | |||
| Second, | |||
| Third | |||
| } | |||
| export type SoloLine<T> = (user: T) => LogEntry | |||
| export type SoloLineArgs<T, V> = (user: T, args: V) => LogEntry | |||
| @@ -9,15 +14,15 @@ export type PairLineArgs<T, V> = (user: T, target: T, args: V) => LogEntry | |||
| export type GroupLine<T> = (user: T, targets: Array<T>) => LogEntry | |||
| enum NounKind { | |||
| Specific, | |||
| Nonspecific, | |||
| All | |||
| Specific, | |||
| Nonspecific, | |||
| All | |||
| } | |||
| enum VowelSound { | |||
| Default, | |||
| Vowel, | |||
| NonVowel | |||
| Default, | |||
| Vowel, | |||
| NonVowel | |||
| } | |||
| enum VerbKind { | |||
| @@ -64,9 +69,7 @@ export type TextLike = { toString: () => string } | |||
| // updates as needed | |||
| export class LiveText<T> { | |||
| constructor (private contents: T, private run: (thing: T) => TextLike) { | |||
| } | |||
| constructor (private contents: T, private run: (thing: T) => TextLike) {} | |||
| toString (): string { | |||
| return this.run(this.contents).toString() | |||
| @@ -80,17 +83,15 @@ export class DynText { | |||
| } | |||
| toString (): string { | |||
| return (this.parts.map(part => part.toString())).join('') | |||
| return this.parts.map(part => part.toString()).join("") | |||
| } | |||
| } | |||
| export abstract class Word { | |||
| constructor (public opt: WordOptions = emptyConfig) { | |||
| } | |||
| constructor (public opt: WordOptions = emptyConfig) {} | |||
| abstract configure (opts: WordOptions): Word; | |||
| abstract toString (): string; | |||
| abstract configure(opts: WordOptions): Word | |||
| abstract toString(): string | |||
| // These functions are pure; they don't mutate the original object. | |||
| // This is necessary to avoid causing chaos. | |||
| @@ -230,7 +231,11 @@ export class OptionalWord extends Word { | |||
| export class RandomWord extends Word { | |||
| private history: { last: number } | |||
| constructor (public choices: Array<Word>, opt: WordOptions = emptyConfig, history: { last: number } = { last: -1 }) { | |||
| constructor ( | |||
| public choices: Array<Word>, | |||
| opt: WordOptions = emptyConfig, | |||
| history: { last: number } = { last: -1 } | |||
| ) { | |||
| super(opt) | |||
| this.history = history | |||
| } | |||
| @@ -252,12 +257,22 @@ export class RandomWord extends Word { | |||
| } | |||
| export class Noun extends Word { | |||
| constructor (protected singularNoun: string, protected pluralNoun: string|null = null, protected possessiveNoun: string|null = null, protected options: WordOptions = emptyConfig) { | |||
| constructor ( | |||
| protected singularNoun: string, | |||
| protected pluralNoun: string | null = null, | |||
| protected possessiveNoun: string | null = null, | |||
| protected options: WordOptions = emptyConfig | |||
| ) { | |||
| super(options) | |||
| } | |||
| configure (opts: WordOptions): Word { | |||
| return new Noun(this.singularNoun, this.pluralNoun, this.possessiveNoun, opts) | |||
| return new Noun( | |||
| this.singularNoun, | |||
| this.pluralNoun, | |||
| this.possessiveNoun, | |||
| opts | |||
| ) | |||
| } | |||
| toString (): string { | |||
| @@ -274,31 +289,34 @@ export class Noun extends Word { | |||
| if (this.pluralNoun === null) { | |||
| result = this.singularNoun | |||
| } else { | |||
| result = (this.pluralNoun as string) | |||
| result = this.pluralNoun as string | |||
| } | |||
| } else { | |||
| result = this.singularNoun | |||
| } | |||
| if (!this.options.proper) { | |||
| if (this.options.nounKind === NounKind.Nonspecific && this.options.count) { | |||
| if ( | |||
| this.options.nounKind === NounKind.Nonspecific && | |||
| this.options.count | |||
| ) { | |||
| if (this.options.plural) { | |||
| result = 'some ' + result | |||
| result = "some " + result | |||
| } else { | |||
| if (this.options.vowel === VowelSound.Default) { | |||
| if ('aeiouAEIOU'.indexOf(result.slice(0, 1)) >= 0) { | |||
| result = 'an ' + result | |||
| if ("aeiouAEIOU".indexOf(result.slice(0, 1)) >= 0) { | |||
| result = "an " + result | |||
| } else { | |||
| result = 'a ' + result | |||
| result = "a " + result | |||
| } | |||
| } else if (this.options.vowel === VowelSound.Vowel) { | |||
| result = 'an ' + result | |||
| result = "an " + result | |||
| } else if (this.options.vowel === VowelSound.NonVowel) { | |||
| result = 'a ' + result | |||
| result = "a " + result | |||
| } | |||
| } | |||
| } else if (this.options.nounKind === NounKind.Specific) { | |||
| result = 'the ' + result | |||
| result = "the " + result | |||
| } | |||
| } | |||
| @@ -322,13 +340,37 @@ export class Noun extends Word { | |||
| export class ImproperNoun extends Noun { | |||
| constructor (singularNoun: string, pluralNoun: string = singularNoun) { | |||
| super(singularNoun, pluralNoun, null, { plural: false, allCaps: false, capital: false, proper: false, nounKind: NounKind.Specific, verbKind: VerbKind.Root, vowel: VowelSound.Default, count: true, possessive: false, objective: false, perspective: POV.Third }) | |||
| super(singularNoun, pluralNoun, null, { | |||
| plural: false, | |||
| allCaps: false, | |||
| capital: false, | |||
| proper: false, | |||
| nounKind: NounKind.Specific, | |||
| verbKind: VerbKind.Root, | |||
| vowel: VowelSound.Default, | |||
| count: true, | |||
| possessive: false, | |||
| objective: false, | |||
| perspective: POV.Third | |||
| }) | |||
| } | |||
| } | |||
| export class ProperNoun extends Noun { | |||
| constructor (singularNoun: string) { | |||
| super(singularNoun, null, null, { plural: false, allCaps: false, capital: false, proper: true, nounKind: NounKind.Specific, verbKind: VerbKind.Root, vowel: VowelSound.Default, count: true, possessive: false, objective: false, perspective: POV.Third }) | |||
| super(singularNoun, null, null, { | |||
| plural: false, | |||
| allCaps: false, | |||
| capital: false, | |||
| proper: true, | |||
| nounKind: NounKind.Specific, | |||
| verbKind: VerbKind.Root, | |||
| vowel: VowelSound.Default, | |||
| count: true, | |||
| possessive: false, | |||
| objective: false, | |||
| perspective: POV.Third | |||
| }) | |||
| } | |||
| } | |||
| @@ -341,9 +383,14 @@ export class Adjective extends Word { | |||
| return new Adjective(this.adjective, opts) | |||
| } | |||
| // TODO caps et al. | |||
| toString (): string { | |||
| return this.adjective | |||
| let word = this.adjective | |||
| if (this.opt.allCaps) { | |||
| word = word.toUpperCase() | |||
| } else if (this.opt.capital) { | |||
| word = word.slice(0, 1).toUpperCase() + word.slice(1) | |||
| } | |||
| return word | |||
| } | |||
| } | |||
| @@ -361,8 +408,21 @@ export class Adverb extends Word { | |||
| } | |||
| } | |||
| /** | |||
| * root: break | |||
| * singular: breaks | |||
| * present: breaking | |||
| * past: broken | |||
| */ | |||
| export class Verb extends Word { | |||
| constructor (private _root: string, private _singular: string = _root + "s", private _present: string = _root + "ing", private _past: string = _root + "ed", private _pastParticiple: string = _past, public opt: WordOptions = emptyConfig) { | |||
| constructor ( | |||
| private _root: string, | |||
| private _singular: string = _root + "s", | |||
| private _present: string = _root + "ing", | |||
| private _past: string = _root + "ed", | |||
| private _pastParticiple: string = _past, | |||
| public opt: WordOptions = emptyConfig | |||
| ) { | |||
| super(opt) | |||
| } | |||
| @@ -381,11 +441,21 @@ export class Verb extends Word { | |||
| let choice: string | |||
| switch (this.opt.verbKind) { | |||
| case VerbKind.Root: choice = this._root; break | |||
| case VerbKind.Singular: choice = this._singular; break | |||
| case VerbKind.Present: choice = this._present; break | |||
| case VerbKind.Past: choice = this._past; break | |||
| case VerbKind.PastParticiple: choice = this._pastParticiple; break | |||
| case VerbKind.Root: | |||
| choice = this._root | |||
| break | |||
| case VerbKind.Singular: | |||
| choice = this._singular | |||
| break | |||
| case VerbKind.Present: | |||
| choice = this._present | |||
| break | |||
| case VerbKind.Past: | |||
| choice = this._past | |||
| break | |||
| case VerbKind.PastParticiple: | |||
| choice = this._pastParticiple | |||
| break | |||
| } | |||
| if (this.opt.allCaps) { | |||
| @@ -430,12 +500,18 @@ export class ToBe extends Word { | |||
| let choice | |||
| if (this.opts.plural) { | |||
| choice = 'are' | |||
| choice = "are" | |||
| } | |||
| switch (this.opts.perspective) { | |||
| case POV.First: choice = 'am'; break | |||
| case POV.Second: choice = 'are'; break | |||
| case POV.Third: choice = 'is'; break | |||
| case POV.First: | |||
| choice = "am" | |||
| break | |||
| case POV.Second: | |||
| choice = "are" | |||
| break | |||
| case POV.Third: | |||
| choice = "is" | |||
| break | |||
| } | |||
| if (this.opt.allCaps) { | |||
| @@ -449,16 +525,18 @@ export class ToBe extends Word { | |||
| } | |||
| interface PronounDict { | |||
| subjective: string; | |||
| objective: string; | |||
| possessive: string; | |||
| reflexive: string; | |||
| subjective: string; | |||
| objective: string; | |||
| possessive: string; | |||
| reflexive: string; | |||
| } | |||
| export class Pronoun implements Pluralizable { | |||
| constructor (private pronouns: PronounDict, private capitalize: boolean = false, public isPlural: boolean = false) { | |||
| } | |||
| constructor ( | |||
| private pronouns: PronounDict, | |||
| private capitalize: boolean = false, | |||
| public isPlural: boolean = false | |||
| ) {} | |||
| get capital (): Pronoun { | |||
| return new Pronoun(this.pronouns, true) | |||
| @@ -497,54 +575,95 @@ export class Pronoun implements Pluralizable { | |||
| } | |||
| } | |||
| export type OnomatopoeiaPart = [string, number, number] | |||
| export class Onomatopoeia extends Word { | |||
| constructor (protected parts: Array<OnomatopoeiaPart>, public opts: WordOptions = emptyConfig) { | |||
| super(opts) | |||
| } | |||
| configure (opts: WordOptions): Word { | |||
| return new Onomatopoeia(this.parts, opts) | |||
| } | |||
| toString (): string { | |||
| const built = this.parts.reduce((result, next) => { | |||
| const [piece, min, max] = next | |||
| const count = Math.floor(Math.random() * (max - min)) + min | |||
| for (let i = 0; i < count; i++) { | |||
| result += piece | |||
| } | |||
| return result | |||
| }, "") | |||
| return built | |||
| } | |||
| } | |||
| export const MalePronouns = new Pronoun({ | |||
| subjective: 'he', | |||
| objective: 'him', | |||
| possessive: 'his', | |||
| reflexive: 'himself' | |||
| subjective: "he", | |||
| objective: "him", | |||
| possessive: "his", | |||
| reflexive: "himself" | |||
| }) | |||
| export const FemalePronouns = new Pronoun({ | |||
| subjective: 'she', | |||
| objective: 'her', | |||
| possessive: 'her', | |||
| reflexive: 'herself' | |||
| subjective: "she", | |||
| objective: "her", | |||
| possessive: "her", | |||
| reflexive: "herself" | |||
| }) | |||
| export const TheyPronouns = new Pronoun({ | |||
| subjective: 'they', | |||
| objective: 'them', | |||
| possessive: 'their', | |||
| reflexive: 'themself' | |||
| }, false, true) | |||
| export const TheyPluralPronouns = new Pronoun({ | |||
| subjective: 'they', | |||
| objective: 'them', | |||
| possessive: 'their', | |||
| reflexive: 'themselves' | |||
| }, false, true) | |||
| export const TheyPronouns = new Pronoun( | |||
| { | |||
| subjective: "they", | |||
| objective: "them", | |||
| possessive: "their", | |||
| reflexive: "themself" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export const TheyPluralPronouns = new Pronoun( | |||
| { | |||
| subjective: "they", | |||
| objective: "them", | |||
| possessive: "their", | |||
| reflexive: "themselves" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export const ObjectPronouns = new Pronoun({ | |||
| subjective: 'it', | |||
| objective: 'it', | |||
| possessive: 'its', | |||
| reflexive: 'itself' | |||
| subjective: "it", | |||
| objective: "it", | |||
| possessive: "its", | |||
| reflexive: "itself" | |||
| }) | |||
| export const SecondPersonPronouns = new Pronoun({ | |||
| subjective: 'you', | |||
| objective: 'you', | |||
| possessive: 'your', | |||
| reflexive: 'yourself' | |||
| }) | |||
| export const FirstPersonPronouns = new Pronoun({ | |||
| subjective: 'I', | |||
| objective: 'me', | |||
| possessive: 'my', | |||
| reflexive: 'myself' | |||
| }) | |||
| export const SecondPersonPronouns = new Pronoun( | |||
| { | |||
| subjective: "you", | |||
| objective: "you", | |||
| possessive: "your", | |||
| reflexive: "yourself" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export const FirstPersonPronouns = new Pronoun( | |||
| { | |||
| subjective: "I", | |||
| objective: "me", | |||
| possessive: "my", | |||
| reflexive: "myself" | |||
| }, | |||
| false, | |||
| true | |||
| ) | |||
| export class PronounAsNoun extends Noun { | |||
| constructor (private pronouns: Pronoun, opt: WordOptions = emptyConfig) { | |||
| @@ -559,7 +678,12 @@ export class PronounAsNoun extends Noun { | |||
| toString (): string { | |||
| if (this.options.objective) { | |||
| return new Noun(this.pronouns.objective, this.pronouns.objective, this.pronouns.possessive, this.options).toString() | |||
| return new Noun( | |||
| this.pronouns.objective, | |||
| this.pronouns.objective, | |||
| this.pronouns.possessive, | |||
| this.options | |||
| ).toString() | |||
| } else { | |||
| return super.toString() | |||
| } | |||
| @@ -9,10 +9,9 @@ import { InstantDigestionEffect, SurrenderEffect } from '@/game/combat/effects' | |||
| import moment from 'moment' | |||
| import { VoreAI } from '@/game/ai' | |||
| import { DeliciousPerk } from '@/game/combat/perks' | |||
| import Inazuma from '../creatures/characters/inazuma' | |||
| import Samuel from '../creatures/characters/Samuel' | |||
| import Human from '../creatures/human' | |||
| import Slime from '../creatures/monsters/slime' | |||
| import Werewolf from '../creatures/monsters/werewolf' | |||
| function makeParty (): Creature[] { | |||
| const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, { | |||
| @@ -122,14 +121,14 @@ export const Newtown = (): Place => { | |||
| ) | |||
| deepwoods.choices.push( | |||
| new Choice( | |||
| "Fight Inazuma", | |||
| "Go fight Inazuma!", | |||
| "Fight Werewolf", | |||
| "Go fight Werewolf!", | |||
| (world, executor) => { | |||
| const enemy = new Inazuma() | |||
| const enemy = new Werewolf() | |||
| const encounter = new Encounter( | |||
| { | |||
| name: "Fight some tough nerd", | |||
| intro: () => new LogLine(`Inazuma Approaches!`) | |||
| intro: () => new LogLine(`Werewolf Approaches!`) | |||
| }, | |||
| [world.player, enemy].concat(world.party) | |||
| ) | |||
| @@ -141,7 +140,7 @@ export const Newtown = (): Place => { | |||
| const bossEncounters = [ | |||
| new Encounter( | |||
| { name: "Inazuma", intro: () => nilLog }, | |||
| makeParty().concat([new Inazuma()]) | |||
| makeParty().concat([new Werewolf()]) | |||
| ) | |||
| ] | |||
| @@ -461,24 +460,6 @@ export const Newtown = (): Place => { | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a slime", | |||
| "Go fight a slime", | |||
| (world, executor) => { | |||
| const enemy = new Slime() | |||
| const encounter = new Encounter( | |||
| { | |||
| name: "Fight some tasty nerd", | |||
| intro: () => new LogLine(`A slime draws near!`) | |||
| }, | |||
| [world.player, enemy].concat(world.party) | |||
| ) | |||
| world.encounter = encounter | |||
| return nilLog | |||
| } | |||
| ) | |||
| ) | |||
| woods.choices.push( | |||
| new Choice( | |||
| @@ -1,5 +1,5 @@ | |||
| import { Place, Choice, Direction, World } from '@/game/world' | |||
| import { ProperNoun, ImproperNoun, MalePronouns, FemalePronouns, TheyPronouns } from '@/game/language' | |||
| import { ProperNoun, ImproperNoun, MalePronouns, FemalePronouns, TheyPronouns, POV } from '@/game/language' | |||
| import { Encounter, Stat, Damage, DamageType, Vigor, Side } from '@/game/combat' | |||
| import * as Items from '@/game/items' | |||
| import { LogLine, nilLog, LogLines } from '@/game/interface' | |||
| @@ -9,10 +9,9 @@ import { InstantDigestionEffect, SurrenderEffect } from '@/game/combat/effects' | |||
| import moment from 'moment' | |||
| import { VoreAI } from '@/game/ai' | |||
| import { DeliciousPerk } from '@/game/combat/perks' | |||
| import Inazuma from '../creatures/characters/inazuma' | |||
| import Human from '../creatures/human' | |||
| import Slime from '../creatures/monsters/slime' | |||
| import Samuel from '../creatures/characters/Samuel' | |||
| import Werewolf from '../creatures/monsters/werewolf' | |||
| function makeParty (): Creature[] { | |||
| const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, { | |||
| @@ -103,13 +102,6 @@ export const Town = (): Place => { | |||
| "The center of town" | |||
| ) | |||
| const bossEncounters = [ | |||
| new Encounter( | |||
| { name: "Inazuma", intro: () => nilLog }, | |||
| makeParty().concat([new Inazuma()]) | |||
| ) | |||
| ] | |||
| home.choices.push( | |||
| new Choice( | |||
| "Nap", | |||
| @@ -157,6 +149,20 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| home.choices.push( | |||
| new Choice( | |||
| "Become a werewolf", | |||
| "Yum", | |||
| (world, executor) => { | |||
| world.player = new Werewolf() | |||
| world.player.location = home | |||
| world.player.perspective = POV.Second | |||
| world.player.side = Side.Heroes | |||
| return new LogLine("Nice") | |||
| } | |||
| ) | |||
| ) | |||
| square.choices.push( | |||
| new Choice( | |||
| "Eat someone", | |||
| @@ -228,19 +234,6 @@ export const Town = (): Place => { | |||
| ) | |||
| ) | |||
| bossEncounters.forEach(encounter => { | |||
| bosses.choices.push( | |||
| new Choice( | |||
| encounter.desc.name, | |||
| "Boss fight!", | |||
| (world) => { | |||
| world.encounter = encounter | |||
| return nilLog | |||
| } | |||
| ) | |||
| ) | |||
| }) | |||
| debug.choices.push( | |||
| new Choice( | |||
| "Cut stats", | |||
| @@ -324,14 +317,15 @@ export const Town = (): Place => { | |||
| woods.choices.push( | |||
| new Choice( | |||
| "Fight a slime", | |||
| "Go fight a slime", | |||
| "Fight a werewolf", | |||
| "Go fight a werewolf", | |||
| (world, executor) => { | |||
| const enemy = new Slime() | |||
| const enemy = new Werewolf() | |||
| enemy.location = world.player.location | |||
| const encounter = new Encounter( | |||
| { | |||
| name: "Fight some tasty nerd", | |||
| intro: () => new LogLine(`A slime draws near!`) | |||
| intro: () => new LogLine(`A werewolf draws near!`) | |||
| }, | |||
| [world.player, enemy].concat(world.party) | |||
| ) | |||
| @@ -0,0 +1,119 @@ | |||
| import { FormatEntry, FormatOpt, LogEntry, LogLine } from "./interface" | |||
| import { Onomatopoeia, RandomWord, Word } from "./language" | |||
| export function makeOnomatopoeia (word: Word): LogEntry { | |||
| return new FormatEntry(new LogLine(`${word}`), FormatOpt.Onomatopoeia) | |||
| } | |||
| export const Swallow = new RandomWord([ | |||
| new Onomatopoeia([ | |||
| ["GL", 1, 1], | |||
| ["U", 1, 10], | |||
| ["R", 1, 3], | |||
| ["K", 1, 1], | |||
| ["!", 1, 3] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["GL", 1, 1], | |||
| ["U", 1, 3], | |||
| ["NK", 1, 1], | |||
| ["!", 1, 3] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["G", 1, 1], | |||
| ["L", 1, 3], | |||
| ["U", 1, 3], | |||
| ["RSH", 1, 1], | |||
| ["!", 1, 3] | |||
| ]) | |||
| ]) | |||
| export const Glunk = new RandomWord([ | |||
| new Onomatopoeia([ | |||
| ["G", 1, 1], | |||
| ["L", 2, 4], | |||
| ["U", 1, 3], | |||
| ["NK", 1, 1], | |||
| ["!", 1, 1] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["GL", 1, 1], | |||
| ["O", 2, 5], | |||
| ["RSH", 1, 1], | |||
| ["!", 1, 1] | |||
| ]) | |||
| ]) | |||
| export const Gurgle = new RandomWord([ | |||
| new Onomatopoeia([ | |||
| ["g", 1, 1], | |||
| ["w", 1, 3], | |||
| ["o", 1, 3], | |||
| ["r", 1, 2], | |||
| ["b", 1, 3], | |||
| ["le", 1, 1] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["g", 2, 5], | |||
| ["r", 3, 7], | |||
| ["rg", 1, 1], | |||
| ["l", 1, 3], | |||
| ["e", 1, 1] | |||
| ]) | |||
| ]) | |||
| export const MuffledScream = new RandomWord([ | |||
| new Onomatopoeia([ | |||
| ["m", 5, 9], | |||
| ["p", 2, 4], | |||
| ["h", 4, 8] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["h", 4, 9], | |||
| ["m", 3, 8], | |||
| ["p", 1, 3], | |||
| ["h", 3, 5] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["n", 4, 9], | |||
| ["g", 3, 5], | |||
| ["h", 5, 10] | |||
| ]) | |||
| ]) | |||
| export const Crunch = new RandomWord([ | |||
| new Onomatopoeia([ | |||
| ["C", 1, 1], | |||
| ["R", 4, 8], | |||
| ["U", 1, 3], | |||
| ["N", 2, 4], | |||
| ["CH", 1, 1], | |||
| ["!", 1, 1] | |||
| ]) | |||
| ]) | |||
| export const Burp = new RandomWord([ | |||
| new Onomatopoeia([ | |||
| ["B", 1, 1], | |||
| ["U", 5, 12], | |||
| ["R", 3, 6], | |||
| ["P", 1, 1], | |||
| ["!", 1, 3] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["H", 1, 1], | |||
| ["U", 1, 4], | |||
| ["O", 4, 6], | |||
| ["R", 2, 3], | |||
| ["P", 4, 6], | |||
| ["H", 2, 3], | |||
| ["!", 1, 3] | |||
| ]), | |||
| new Onomatopoeia([ | |||
| ["B", 1, 1], | |||
| ["W", 1, 2], | |||
| ["A", 6, 11], | |||
| ["R", 2, 4], | |||
| ["P", 2, 3], | |||
| ["!", 1, 3] | |||
| ]) | |||
| ]) | |||
| @@ -1,48 +1,87 @@ | |||
| import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula } from '@/game/combat' | |||
| import { LogLines, LogEntry, LogLine, nilLog, RandomEntry } from '@/game/interface' | |||
| import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition } from '@/game/language' | |||
| import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction } from '@/game/combat/actions' | |||
| import { Damage, DamageType, Actionable, Action, Vigor, DamageInstance, DamageFormula, ConstantDamageFormula, StatusEffect } from '@/game/combat' | |||
| import { LogLines, LogEntry, LogLine, nilLog, RandomEntry, FormatEntry, FormatOpt } from '@/game/interface' | |||
| import { Noun, ImproperNoun, Verb, RandomWord, Word, Preposition, ToBe, Adjective } from '@/game/language' | |||
| import { RubAction, DevourAction, ReleaseAction, StruggleAction, TransferAction, StruggleMoveAction } from '@/game/combat/actions' | |||
| import * as Words from '@/game/words' | |||
| import * as Onomatopoeia from '@/game/onomatopoeia' | |||
| import { Creature } from '@/game/creature' | |||
| import { VoreRelay } from '@/game/events' | |||
| export enum VoreType { | |||
| Oral = "Oral Vore", | |||
| Anal = "Anal Vore", | |||
| Cock = "Cock Vore", | |||
| Unbirth = "Unbirthing", | |||
| Breast = "Breast Vore", | |||
| Bladder = "Bladder Vore", | |||
| Tail = "Tail Vore", | |||
| Goo = "Goo Vore" | |||
| Oral = "Oral Vore" | |||
| } | |||
| export const anyVore = new Set([ | |||
| VoreType.Oral, | |||
| VoreType.Anal, | |||
| VoreType.Cock, | |||
| VoreType.Unbirth, | |||
| VoreType.Breast, | |||
| VoreType.Bladder, | |||
| VoreType.Tail, | |||
| VoreType.Goo | |||
| VoreType.Oral | |||
| ]) | |||
| export type Wall = { | |||
| name: Word; | |||
| texture: Adjective; | |||
| material: Noun; | |||
| color: Adjective; | |||
| } | |||
| export type Fluid = { | |||
| name: Word; | |||
| color: Adjective; | |||
| sound: Word; | |||
| sloshVerb: Verb; | |||
| } | |||
| export type Gas = { | |||
| name: Word; | |||
| color: Adjective; | |||
| smell: Adjective; | |||
| bubbleVerb: Verb; | |||
| releaseVerb: Verb; | |||
| exit: Noun; | |||
| } | |||
| export enum ContainerCapability { | |||
| Consume, | |||
| Release, | |||
| Digest, | |||
| Absorb | |||
| } | |||
| export enum ConnectionDirection { | |||
| Deeper, | |||
| Neutral, | |||
| Shallower | |||
| } | |||
| export type Connection = { | |||
| destination: Container; | |||
| direction: ConnectionDirection; | |||
| description: (to: Container, from: Container, prey: Creature) => LogEntry; | |||
| } | |||
| export interface Container extends Actionable { | |||
| name: Noun; | |||
| owner: Creature; | |||
| voreTypes: Set<VoreType>; | |||
| capabilities: Set<ContainerCapability>; | |||
| capacity: number; | |||
| fullness: number; | |||
| connections: Array<Connection>; | |||
| effects: Array<StatusEffect>; | |||
| wall: Wall | null; | |||
| fluid: Fluid | null; | |||
| gas: Gas | null; | |||
| voreRelay: VoreRelay; | |||
| contents: Array<Creature>; | |||
| describe: () => LogEntry; | |||
| digested: Array<Creature>; | |||
| damage: DamageFormula; | |||
| canTake: (prey: Creature) => boolean; | |||
| consume: (prey: Creature) => LogEntry; | |||
| release: (prey: Creature) => LogEntry; | |||
| struggle: (prey: Creature) => LogEntry; | |||
| sound: Word; | |||
| capacity: number; | |||
| fullness: number; | |||
| consumeVerb: Verb; | |||
| consumePreposition: Preposition; | |||
| @@ -51,35 +90,91 @@ export interface Container extends Actionable { | |||
| struggleVerb: Verb; | |||
| strugglePreposition: Preposition; | |||
| canTake (prey: Creature): boolean; | |||
| consume (prey: Creature): LogEntry; | |||
| release (prey: Creature): LogEntry; | |||
| enter (prey: Creature): LogEntry; | |||
| exit (prey: Creature): LogEntry; | |||
| struggle (prey: Creature): LogEntry; | |||
| tick (dt: number, victims?: Array<Creature>): LogEntry; | |||
| digest (preys: Creature[]): LogEntry; | |||
| absorb (preys: Creature[]): LogEntry; | |||
| onDigest (prey: Creature): LogEntry; | |||
| onAbsorb (prey: Creature): LogEntry; | |||
| consumeLine (user: Creature, target: Creature): LogEntry; | |||
| statusLine (user: Creature, target: Creature): LogEntry; | |||
| describe (): LogEntry; | |||
| describeDetail (prey: Creature): LogEntry; | |||
| connect (dest: Connection): void; | |||
| } | |||
| export abstract class NormalContainer implements Container { | |||
| export abstract class DefaultContainer implements Container { | |||
| public name: Noun | |||
| contents: Array<Creature> = [] | |||
| actions: Array<Action> = [] | |||
| consumeVerb = new Verb('trap') | |||
| consumePreposition = new Preposition("in") | |||
| wall: Wall | null = null | |||
| fluid: Fluid | null = null | |||
| gas: Gas | null = null | |||
| connections: Array<Connection> = [] | |||
| voreRelay = new VoreRelay() | |||
| effects: Array<StatusEffect> = [] | |||
| consumeVerb = new Verb('devour') | |||
| consumePreposition = new Preposition("into") | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| releasePreposition = new Preposition("from") | |||
| releasePreposition = new Preposition("out from") | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| strugglePreposition = new Preposition("in") | |||
| strugglePreposition = new Preposition("within") | |||
| fluidColor = "#00ff0088" | |||
| constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number) { | |||
| digested: Array<Creature> = [] | |||
| absorbed: Array<Creature> = [] | |||
| damage: DamageFormula = new ConstantDamageFormula(new Damage()); | |||
| sound = new Verb("slosh") | |||
| constructor (name: Noun, public owner: Creature, public voreTypes: Set<VoreType>, public capacityFactor: number, public capabilities: Set<ContainerCapability>) { | |||
| this.name = name.all | |||
| this.actions.push(new DevourAction(this)) | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| if (capabilities.has(ContainerCapability.Consume)) { | |||
| this.actions.push(new DevourAction(this)) | |||
| } | |||
| if (capabilities.has(ContainerCapability.Release)) { | |||
| this.actions.push(new ReleaseAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| if (capabilities.has(ContainerCapability.Digest)) { | |||
| this.actions.push(new RubAction(this)) | |||
| } | |||
| } | |||
| connect (connection: Connection): void { | |||
| this.connections.push(connection) | |||
| this.actions.push(new TransferAction(this, connection)) | |||
| this.actions.push(new StruggleMoveAction(this, connection.destination)) | |||
| } | |||
| get capacity (): number { | |||
| return this.capacityFactor * this.owner.voreStats.Mass | |||
| } | |||
| consumeLine (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(this.consumeVerb)} ${target.name.objective} ${this.consumePreposition} ${user.pronouns.possessive} ${this.name}.`) | |||
| statusLine (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine( | |||
| `${target.name.capital} ${target.name.conjugate(new ToBe())} ${Words.Stuck} inside ${user.name.possessive} ${this.name}.` | |||
| ) | |||
| } | |||
| releaseLine (user: Creature, target: Creature): LogEntry { | |||
| @@ -91,7 +186,7 @@ export abstract class NormalContainer implements Container { | |||
| } | |||
| get fullness (): number { | |||
| return Array.from(this.contents.values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) | |||
| return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) | |||
| } | |||
| canTake (prey: Creature): boolean { | |||
| @@ -105,22 +200,64 @@ export abstract class NormalContainer implements Container { | |||
| } | |||
| consume (prey: Creature): LogEntry { | |||
| const results: Array<LogEntry> = [ | |||
| this.enter(prey), | |||
| this.voreRelay.dispatch("onEaten", this, { prey: prey }), | |||
| prey.voreRelay.dispatch("onEaten", this, { prey: prey }), | |||
| this.consumeLine(this.owner, prey) | |||
| ] | |||
| this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this))) | |||
| return new LogLines(...results) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| const results = [ | |||
| this.exit(prey), | |||
| this.releaseLine(this.owner, prey), | |||
| this.voreRelay.dispatch("onReleased", this, { prey: prey }), | |||
| prey.voreRelay.dispatch("onReleased", this, { prey: prey }) | |||
| ] | |||
| return new LogLines(...results) | |||
| } | |||
| enter (prey: Creature): LogEntry { | |||
| if (prey.containedIn !== null) { | |||
| prey.containedIn.contents = prey.containedIn.contents.filter(item => prey !== item) | |||
| } | |||
| this.contents.push(prey) | |||
| prey.containedIn = this | |||
| return this.consumeLine(this.owner, prey) | |||
| const effectResults = this.effects.map(effect => prey.applyEffect(effect)) | |||
| const results = [ | |||
| this.voreRelay.dispatch("onEntered", this, { prey: prey }), | |||
| prey.voreRelay.dispatch("onEntered", this, { prey: prey }) | |||
| ] | |||
| this.owner.effects.forEach(effect => results.push(effect.postEnter(this.owner, prey, this))) | |||
| return new LogLines(...results, ...effectResults) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| exit (prey: Creature): LogEntry { | |||
| prey.containedIn = this.owner.containedIn | |||
| this.contents = this.contents.filter(victim => victim !== prey) | |||
| if (this.owner.containedIn !== null) { | |||
| this.owner.containedIn.contents.push(prey) | |||
| } | |||
| return this.releaseLine(this.owner, prey) | |||
| const effectResults = this.effects.map(effect => prey.removeEffect(effect)) | |||
| const results = [ | |||
| this.voreRelay.dispatch("onExited", this, { prey: prey }), | |||
| prey.voreRelay.dispatch("onExited", this, { prey: prey }) | |||
| ] | |||
| return new LogLines(...results, ...effectResults) | |||
| } | |||
| struggle (prey: Creature): LogEntry { | |||
| @@ -136,79 +273,27 @@ export abstract class NormalContainer implements Container { | |||
| return new LogLine(...lines) | |||
| } | |||
| } | |||
| export class Hand extends NormalContainer { | |||
| consumeVerb = new Verb("grab") | |||
| constructor (owner: Creature, capacity: number) { | |||
| super( | |||
| new ImproperNoun('hand'), | |||
| owner, | |||
| new Set(), | |||
| capacity | |||
| ) | |||
| } | |||
| } | |||
| export abstract class InnerContainer extends NormalContainer { | |||
| constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, private escape: Container) { | |||
| super(name, owner, voreTypes, capacity) | |||
| this.actions = [] | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| prey.containedIn = this.escape | |||
| this.contents = this.contents.filter(victim => victim !== prey) | |||
| return this.releaseLine(this.owner, prey) | |||
| } | |||
| } | |||
| export interface VoreContainer extends Container { | |||
| voreRelay: VoreRelay; | |||
| digested: Array<Creature>; | |||
| tick: (dt: number, victims?: Array<Creature>) => LogEntry; | |||
| digest: (preys: Creature[]) => LogEntry; | |||
| absorb: (preys: Creature[]) => LogEntry; | |||
| sound: Word; | |||
| fluidName: Word; | |||
| fluidColor: string; | |||
| onDigest: (prey: Creature) => LogEntry; | |||
| onAbsorb: (prey: Creature) => LogEntry; | |||
| } | |||
| export abstract class NormalVoreContainer extends NormalContainer implements VoreContainer { | |||
| voreRelay = new VoreRelay() | |||
| consumeVerb = new Verb('devour') | |||
| consumePreposition = new Preposition("into") | |||
| releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | |||
| releasePreposition = new Preposition("out from") | |||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | |||
| strugglePreposition = new Preposition("within") | |||
| fluidColor = "#00ff0088" | |||
| digested: Array<Creature> = [] | |||
| absorbed: Array<Creature> = [] | |||
| sound = new Verb("slosh") | |||
| abstract fluidName: Word | |||
| describeDetail (prey: Creature): LogEntry { | |||
| const lines: Array<LogLine> = [] | |||
| constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, private damage: DamageFormula) { | |||
| super(name, owner, voreTypes, capacity) | |||
| this.name = name | |||
| this.actions.push(new RubAction(this)) | |||
| } | |||
| if (this.gas) { | |||
| lines.push( | |||
| new LogLine(`${this.gas.color.capital} ${this.gas.name.plural} ${this.gas.bubbleVerb} in ${this.owner.name.possessive} ${this.name}.`) | |||
| ) | |||
| } | |||
| if (this.fluid) { | |||
| lines.push( | |||
| new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} around ${prey.name.objective}.`) | |||
| ) | |||
| } | |||
| if (this.wall) { | |||
| lines.push( | |||
| new LogLine(`The ${this.wall.color} walls ${prey.name.conjugate(Words.Clench)} over ${prey.name.objective} like a vice.`) | |||
| ) | |||
| } | |||
| get fullness (): number { | |||
| return Array.from(this.contents.concat(this.digested, this.absorbed).values()).reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) | |||
| return new LogLine(...lines) | |||
| } | |||
| consumeLine (user: Creature, target: Creature) { | |||
| @@ -219,15 +304,29 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Churns)} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`), | |||
| const options = [ | |||
| new LogLine(`${user.name.capital} ${Words.Churns.singular} ${target.name.objective} ${this.strugglePreposition} ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`), | |||
| new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(Words.Churns)}, ${Words.Churns.present} ${target.name.objective} for `, args.damage.renderShort(), `.`), | |||
| new LogLine(`${target.name.capital} ${target.name.conjugate(new Verb("thrash", "thrashes"))} ${this.strugglePreposition} ${user.name.possessive} ${Words.Slick} ${this.name} as it ${Words.Churns.singular} ${target.pronouns.objective} for `, args.damage.renderShort(), `.`) | |||
| ) | |||
| new LogLine(`${target.name.capital} ${target.name.conjugate(Words.Struggle)} ${this.strugglePreposition} ${user.name.possessive} ${Words.Slick} ${this.name} as it ${Words.Churns.singular} ${target.pronouns.objective} for `, args.damage.renderShort(), `.`) | |||
| ] | |||
| if (this.fluid) { | |||
| options.push(new LogLine(`${this.fluid.name.capital} ${this.fluid.sloshVerb.singular} and ${this.fluid.sound.singular} as ${this.owner.name.possessive} ${this.name} steadily ${Words.Digest.singular} ${target.name.objective}.`)) | |||
| } | |||
| const result: Array<LogEntry> = [ | |||
| new RandomEntry(...options) | |||
| ] | |||
| if (Math.random() < 0.3) { | |||
| result.push(new FormatEntry(new LogLine(`${Onomatopoeia.Gurgle}`), FormatOpt.Onomatopoeia)) | |||
| } | |||
| return new LogLines(...result) | |||
| } | |||
| digestLine (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`${user.name.capital.possessive} ${this.name} ${this.name.conjugate(new Verb('finish', 'finishes'))} ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)} and ${target.pronouns.conjugate(Words.Digest)} into ${this.fluidName}.`) | |||
| return new LogLine(`${user.name.capital.possessive} ${this.name} finishes ${Words.Digest.present} ${target.name.objective} down, ${target.pronouns.possessive} ${Words.Struggle.singular} fading away as ${target.pronouns.subjective} ${target.pronouns.conjugate(Words.Succumb)}.`) | |||
| } | |||
| absorbLine (user: Creature, target: Creature): LogEntry { | |||
| @@ -242,23 +341,25 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| const tickedEntryList: LogEntry[] = [] | |||
| this.contents.forEach(prey => { | |||
| if (victims === undefined || victims.indexOf(prey) >= 0) { | |||
| const scaled = this.damage.calc(this.owner, prey).scale(dt / 60) | |||
| const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled) | |||
| if (modified.nonzero()) { | |||
| tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified })) | |||
| damageResults.push(prey.takeDamage(modified)) | |||
| } | |||
| if (prey.vigors[Vigor.Health] <= 0) { | |||
| prey.destroyed = true | |||
| this.digested.push(prey) | |||
| justDigested.push(prey) | |||
| damageResults.push(this.onDigest(prey)) | |||
| if (this.capabilities.has(ContainerCapability.Digest)) { | |||
| this.contents.forEach(prey => { | |||
| if (victims === undefined || victims.indexOf(prey) >= 0) { | |||
| const scaled = this.damage.calc(this.owner, prey).scale(dt / 60) | |||
| const modified = this.owner.effects.reduce((damage, effect) => effect.modDigestionDamage(this.owner, prey, this, damage), scaled) | |||
| if (modified.nonzero()) { | |||
| tickedEntryList.push(this.tickLine(this.owner, prey, { damage: modified })) | |||
| damageResults.push(prey.takeDamage(modified)) | |||
| } | |||
| if (prey.vigors[Vigor.Health] <= 0) { | |||
| prey.destroyed = true | |||
| this.digested.push(prey) | |||
| justDigested.push(prey) | |||
| damageResults.push(this.onDigest(prey)) | |||
| } | |||
| } | |||
| } | |||
| }) | |||
| }) | |||
| } | |||
| const tickedEntries = new LogLines(...tickedEntryList) | |||
| @@ -302,19 +403,13 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| } | |||
| digest (preys: Creature[]): LogEntry { | |||
| return new LogLines(...preys.map(prey => this.digestLine(this.owner, prey))) | |||
| } | |||
| consume (prey: Creature): LogEntry { | |||
| const logLine = super.consume(prey) | |||
| const results: Array<LogEntry> = [] | |||
| this.owner.effects.forEach(effect => results.push(effect.postConsume(this.owner, prey, this))) | |||
| const relayResults = this.voreRelay.dispatch("onEaten", this, { prey: prey }) | |||
| return new LogLines(...[logLine].concat(results).concat(relayResults)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| return new LogLines(super.release(prey), this.voreRelay.dispatch("onReleased", this, { prey: prey })) | |||
| const results = preys.map(prey => this.digestLine(this.owner, prey)) | |||
| if (preys.length > 0 && this.gas) { | |||
| results.push(new LogLine( | |||
| `A crass ${this.gas.releaseVerb} escapes ${this.owner.name.possessive} ${this.gas.exit} as ${this.owner.name.possessive} prey is digested, spewing ${this.gas.color} ${this.gas.name}.` | |||
| )) | |||
| } | |||
| return new LogLines(...results) | |||
| } | |||
| onAbsorb (prey: Creature): LogEntry { | |||
| @@ -326,243 +421,77 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||
| } | |||
| } | |||
| export abstract class InnerVoreContainer extends NormalVoreContainer { | |||
| constructor (name: Noun, owner: Creature, voreTypes: Set<VoreType>, capacity: number, damage: DamageFormula, private escape: Container) { | |||
| super(name, owner, voreTypes, capacity, damage) | |||
| this.actions = [] | |||
| this.actions.push(new RubAction(this)) | |||
| this.actions.push(new StruggleAction(this)) | |||
| } | |||
| release (prey: Creature): LogEntry { | |||
| prey.containedIn = this.escape | |||
| this.contents = this.contents.filter(victim => victim !== prey) | |||
| this.escape.consume(prey) | |||
| return this.releaseLine(this.owner, prey) | |||
| } | |||
| } | |||
| export class Stomach extends NormalVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('stomach', 'stomachs').all, owner, new Set([VoreType.Oral]), capacity, damage) | |||
| } | |||
| digest (preys: Creature[]): LogEntry { | |||
| if (preys.length === 0) { | |||
| return super.digest(preys) | |||
| } | |||
| const heal = new Damage( | |||
| { | |||
| amount: preys.reduce((total: number, next: Creature) => total + next.maxVigors.Health / 5, 0), | |||
| type: DamageType.Heal, | |||
| target: Vigor.Health | |||
| } | |||
| ) | |||
| this.owner.takeDamage(heal) | |||
| return new LogLines( | |||
| super.digest(preys), | |||
| new LogLine(`${this.owner.name.capital} ${this.owner.name.conjugate(new Verb("heal"))} for `, this.owner.effectiveDamage(heal).renderShort()) | |||
| ) | |||
| } | |||
| } | |||
| export class InnerStomach extends InnerVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| consumeVerb = new Verb('swallow') | |||
| releaseVerb = new Verb('hork') | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: VoreContainer) { | |||
| super(new ImproperNoun('inner stomach', 'inner stomachs').all, owner, new Set([VoreType.Oral]), capacity, damage, escape) | |||
| export class Stomach extends DefaultContainer { | |||
| fluid = { | |||
| color: new Adjective("green"), | |||
| name: new Noun("chyme"), | |||
| sound: new Verb("gurgle"), | |||
| sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed") | |||
| } | |||
| } | |||
| export class Bowels extends NormalVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('bowel', 'bowels').plural.all, owner, new Set([VoreType.Anal]), capacity, damage) | |||
| gas = { | |||
| bubbleVerb: new Verb("bubble", "bubbles", "bubbling", "bubbled"), | |||
| color: new Adjective("hazy"), | |||
| name: new Noun("fume", "fumes"), | |||
| releaseVerb: new Verb("belch", "belches", "belching", "belched"), | |||
| smell: new Adjective("acrid"), | |||
| exit: new Noun("jaws") | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }) { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`), | |||
| super.tickLine(user, target, args) | |||
| ) | |||
| wall = { | |||
| color: new Adjective("red"), | |||
| material: new Noun("muscle"), | |||
| name: new Noun("wall"), | |||
| texture: new Adjective("slimy") | |||
| } | |||
| } | |||
| export class Tail extends NormalVoreContainer { | |||
| fluidName = new Noun("chyme") | |||
| constructor (owner: Creature, capacityFactor: number, damage: DamageFormula) { | |||
| super(new Noun("stomach"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([ | |||
| ContainerCapability.Digest, | |||
| ContainerCapability.Absorb | |||
| ])) | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('tail', 'tails').all, owner, new Set([VoreType.Tail]), capacity, damage) | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }) { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`) | |||
| ) | |||
| } | |||
| } | |||
| export class Goo extends NormalVoreContainer { | |||
| fluidName = new Noun("goo") | |||
| fluidColor = "#66ee66"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super(new ImproperNoun('goo', 'goo').all, owner, new Set([VoreType.Goo]), capacity, damage) | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }) { | |||
| return new RandomEntry( | |||
| new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} in ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`) | |||
| ) | |||
| } | |||
| } | |||
| export class Cock extends NormalVoreContainer { | |||
| fluidName = new Noun("cum") | |||
| fluidColor = "#eeeeee66"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('cock').all, | |||
| owner, | |||
| new Set([VoreType.Cock]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| } | |||
| tickLine (user: Creature, target: Creature, args: { damage: Damage }): LogEntry { | |||
| return new LogLine(`${user.name.capital} ${user.name.conjugate(Words.Clench)} ${target.name.objective} with ${user.pronouns.possessive} ${this.name} for `, args.damage.renderShort(), `.`) | |||
| } | |||
| } | |||
| export class Balls extends InnerVoreContainer { | |||
| fluidName = new Noun("cum") | |||
| fluidColor = "#eeeeeecc"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) { | |||
| super( | |||
| new ImproperNoun('ball', 'balls').all.plural, | |||
| owner, | |||
| new Set([VoreType.Cock]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| } | |||
| } | |||
| export class Slit extends NormalVoreContainer { | |||
| fluidName = new Noun("femcum") | |||
| fluidColor = "#cccccc99"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('slit').all, | |||
| owner, | |||
| new Set([VoreType.Unbirth]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| } | |||
| } | |||
| export class Womb extends InnerVoreContainer { | |||
| fluidName = new Noun("femcum") | |||
| fluidColor = "#ddddddbb"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) { | |||
| super( | |||
| new ImproperNoun('womb').all, | |||
| owner, | |||
| new Set([VoreType.Unbirth]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| } | |||
| } | |||
| export class Breasts extends NormalVoreContainer { | |||
| fluidName = new Noun("milk") | |||
| fluidColor = "#eeeeeecc"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('breast', 'breasts').all.plural, | |||
| owner, | |||
| new Set([VoreType.Breast]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| } | |||
| } | |||
| export class Bladder extends NormalVoreContainer { | |||
| fluidName = new Noun("piss") | |||
| fluidColor = "#eeee3399"; | |||
| this.voreRelay.subscribe("onEntered", (sender: Container, args: { prey: Creature }) => { | |||
| return new FormatEntry(new LogLine(`${Onomatopoeia.Glunk}`), FormatOpt.Onomatopoeia) | |||
| }) | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula) { | |||
| super( | |||
| new ImproperNoun('bladder').all, | |||
| owner, | |||
| new Set([VoreType.Bladder]), | |||
| capacity, | |||
| damage | |||
| ) | |||
| this.damage = damage | |||
| } | |||
| } | |||
| export class InnerBladder extends InnerVoreContainer { | |||
| fluidName = new Noun("piss") | |||
| fluidColor = "#eeee3399"; | |||
| constructor (owner: Creature, capacity: number, damage: DamageFormula, escape: Container) { | |||
| super( | |||
| new ImproperNoun('bladder').all, | |||
| owner, | |||
| new Set([VoreType.Bladder]), | |||
| capacity, | |||
| damage, | |||
| escape | |||
| ) | |||
| export class Throat extends DefaultContainer { | |||
| fluid = { | |||
| color: new Adjective("clear"), | |||
| name: new RandomWord([ | |||
| new Noun("saliva"), | |||
| new Noun("drool"), | |||
| new Noun("slobber") | |||
| ]), | |||
| sound: new Verb("squish", "squishes"), | |||
| sloshVerb: new Verb("slosh", "sloshes", "sloshing", "sloshed") | |||
| } | |||
| wall = { | |||
| color: new Adjective("red"), | |||
| material: new Noun("muscle"), | |||
| name: new Noun("wall"), | |||
| texture: new Adjective("slimy") | |||
| } | |||
| constructor (owner: Creature, capacityFactor: number) { | |||
| super(new Noun("throat"), owner, new Set<VoreType>([VoreType.Oral]), capacityFactor, new Set<ContainerCapability>([ | |||
| ContainerCapability.Consume, | |||
| ContainerCapability.Release | |||
| ])) | |||
| this.voreRelay.subscribe("onEaten", (sender: Container, args: { prey: Creature }) => { | |||
| return new FormatEntry(new LogLine(`${Onomatopoeia.Swallow}`), FormatOpt.Onomatopoeia) | |||
| }) | |||
| } | |||
| } | |||
| export function biconnectContainers (outer: VoreContainer, inner: VoreContainer): void { | |||
| const old = outer.onDigest | |||
| outer.onDigest = (prey: Creature) => { | |||
| const oldResult = old(prey) | |||
| outer.digested = outer.digested.filter(victim => victim !== prey) | |||
| inner.digested.push(prey) | |||
| return new LogLines(oldResult, inner.consumeLine(inner.owner, prey), inner.tick(0, [prey])) | |||
| export function transferDescription (verb: Word, preposition: Preposition): ((from: Container, to: Container, prey: Creature) => LogEntry) { | |||
| return (from: Container, to: Container, prey: Creature) => { | |||
| return new LogLine(`${from.owner.name.capital} ${from.owner.name.conjugate(verb.singular)} ${prey.name.objective} ${preposition} ${to.consumePreposition} ${from.owner.pronouns.possessive} ${to.name}.`) | |||
| } | |||
| outer.actions.push( | |||
| new TransferAction( | |||
| outer, | |||
| inner | |||
| ) | |||
| ) | |||
| inner.actions.push( | |||
| new TransferAction( | |||
| inner, | |||
| outer | |||
| ) | |||
| ) | |||
| } | |||
| @@ -21,8 +21,7 @@ export const Slick = new RandomWord([ | |||
| export const Swallow = new RandomWord([ | |||
| new Verb("swallow"), | |||
| new Verb("gulp"), | |||
| new Verb("consume", "consumes", "consuming", "consumed") | |||
| new Verb("gulp") | |||
| ]) | |||
| export const Churns = new RandomWord([ | |||
| @@ -45,10 +44,8 @@ export const Dark = new RandomWord([ | |||
| export const Digest = new RandomWord([ | |||
| new Verb("digest"), | |||
| new Verb("melt down", "melts down", "melting down", "melted down"), | |||
| new Verb("dissolve", "dissolves", "dissolving", "dissolved"), | |||
| new Verb("mulch", "mulches", "mulching", "mulched"), | |||
| new Verb("break down", "breaks down", "breaking down", "broken down") | |||
| new Verb("mulch", "mulches", "mulching", "mulched") | |||
| ]) | |||
| export const Absorb = new RandomWord([ | |||
| @@ -120,3 +117,22 @@ export const Sloppily = new RandomWord([ | |||
| new Adverb("sloppily"), | |||
| new Adverb("messily") | |||
| ]) | |||
| export const Stuck = new RandomWord([ | |||
| new Adjective("stuck"), | |||
| new Adjective("trapped"), | |||
| new Adjective("imprisoned") | |||
| ]) | |||
| export const Rub = new RandomWord([ | |||
| new Verb("rub", "rubs", "rubbing", "rubbed"), | |||
| new Verb("knead", "kneads", "kneading", "kneaded"), | |||
| new Verb("press over", "presses over", "pressing over", "pressed over") | |||
| ]) | |||
| export const Full = new RandomWord([ | |||
| new Adjective("full"), | |||
| new Adjective("stuffed"), | |||
| new Adjective("packed"), | |||
| new Adjective("bulging") | |||
| ]) | |||
| @@ -6,6 +6,8 @@ declare global { | |||
| joinGeneral (item: T, endItem: T|null): Array<T>; | |||
| /* eslint-disable-next-line */ | |||
| unique (predicate?: (elem: T) => any): Array<T>; | |||
| findResult<U> (func: (elem: T) => U, predicate: (result: U) => boolean): U | undefined; | |||
| mapUntil<U> (func: (elem: T) => U, predicate: (result: U) => boolean): Array<U>; | |||
| } | |||
| } | |||
| @@ -33,6 +35,41 @@ Array.prototype.unique = function<T> (predicate?: (elem: T) => any): Array<T> { | |||
| return result | |||
| } | |||
| /* eslint-disable-next-line */ | |||
| Array.prototype.findResult = function<T,U> (func: (elem: T) => U, predicate: (result: U) => boolean): U | undefined { | |||
| for (let i = 0; i < this.length; i++) { | |||
| const elem: T = this[i] | |||
| const result: U = func(elem) | |||
| const decision = predicate(result) | |||
| if (decision) { | |||
| return result | |||
| } | |||
| } | |||
| return undefined | |||
| } | |||
| /** | |||
| * Maps over the array until the predicate is satisfied. | |||
| */ | |||
| /* eslint-disable-next-line */ | |||
| Array.prototype.mapUntil = function<T,U> (func: (elem: T) => U, predicate: (result: U) => boolean): Array<U> { | |||
| const results: Array<U> = [] | |||
| for (let i = 0; i < this.length; i++) { | |||
| const elem: T = this[i] | |||
| const result: U = func(elem) | |||
| const decision = predicate(result) | |||
| results.push(result) | |||
| if (decision) { | |||
| break | |||
| } | |||
| } | |||
| return results | |||
| } | |||
| Vue.config.productionTip = false | |||
| new Vue({ | |||