| @@ -6,7 +6,7 @@ module.exports = { | |||||
| extends: [ | extends: [ | ||||
| 'plugin:vue/essential', | 'plugin:vue/essential', | ||||
| '@vue/standard', | '@vue/standard', | ||||
| '@vue/typescript/recommended' | |||||
| '@vue/typescript/recommended', | |||||
| ], | ], | ||||
| parserOptions: { | parserOptions: { | ||||
| ecmaVersion: 2020 | ecmaVersion: 2020 | ||||
| @@ -18,6 +18,11 @@ module.exports = { | |||||
| '@typescript-eslint/no-unused-vars': 'off', | '@typescript-eslint/no-unused-vars': 'off', | ||||
| quotes: 'off', | quotes: 'off', | ||||
| 'function-paren-newline': ['error', 'multiline-arguments'], | '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 */ { | /* .component-fade-leave-active below version 2.1.8 */ { | ||||
| opacity: 0; | 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> | </style> | ||||
| @@ -242,6 +242,15 @@ export default class Combat extends Vue { | |||||
| } else { | } else { | ||||
| this.executedRight(this.encounter.currentMove.ai.decide(this.encounter.currentMove, this.encounter)) | 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 { | .left-stats { | ||||
| flex-direction: row; | |||||
| flex-direction: row-reverse; | |||||
| } | } | ||||
| .right-stats { | .right-stats { | ||||
| @@ -15,7 +15,7 @@ import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||||
| import { Creature } from '@/game/creature' | import { Creature } from '@/game/creature' | ||||
| import { POV } from '@/game/language' | import { POV } from '@/game/language' | ||||
| import { Stats, Stat } from '@/game/combat' | import { Stats, Stat } from '@/game/combat' | ||||
| import { Container, VoreContainer } from '@/game/vore' | |||||
| import { Container } from '@/game/vore' | |||||
| function wiggle (contents: HTMLElement) { | function wiggle (contents: HTMLElement) { | ||||
| setTimeout(() => wiggle(contents), 3000) | setTimeout(() => wiggle(contents), 3000) | ||||
| @@ -36,12 +36,13 @@ function wiggle (contents: HTMLElement) { | |||||
| // yoinked from https://jsfiddle.net/yckart/0adfw47y/ | // 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 | const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | ||||
| canvas.width = parent.clientWidth | canvas.width = parent.clientWidth | ||||
| canvas.height = parent.clientHeight | 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 fraction = container.fullness / container.capacity | ||||
| const livingFraction = container.contents.reduce((total: number, prey: Creature) => total + prey.voreStats.Bulk, 0) / 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 | 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 | @Component | ||||
| export default class ContainerView extends Vue { | export default class ContainerView extends Vue { | ||||
| @Prop({ required: true }) | @Prop({ required: true }) | ||||
| container!: VoreContainer | |||||
| container!: Container | |||||
| mounted () { | mounted () { | ||||
| const canvas = this.$el.querySelector('.container-waves') as HTMLCanvasElement | 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.width = (this.$el as HTMLElement).clientWidth | ||||
| canvas.height = (this.$el as HTMLElement).clientHeight | canvas.height = (this.$el as HTMLElement).clientHeight | ||||
| canvas.width = canvas.width + 0 | 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) | wiggle(this.$el.querySelector(".container-contents") as HTMLElement) | ||||
| } | } | ||||
| @@ -12,10 +12,6 @@ | |||||
| <script lang="ts"> | <script lang="ts"> | ||||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | 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' | import { Item, ItemKindIcons, ItemKind } from '@/game/items' | ||||
| @Component({ | @Component({ | ||||
| @@ -7,9 +7,6 @@ | |||||
| <script lang="ts"> | <script lang="ts"> | ||||
| import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | ||||
| import { Creature } from '@/game/creature' | 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' | import { Item, ItemKindIcons, ItemKind, Currency, CurrencyData } from '@/game/items' | ||||
| @Component({ | @Component({ | ||||
| @@ -1,11 +1,12 @@ | |||||
| import { Creature } from '@/game/creature' | import { Creature } from '@/game/creature' | ||||
| import { Encounter, Action, CompositionAction, Consequence } from '@/game/combat' | import { Encounter, Action, CompositionAction, Consequence } from '@/game/combat' | ||||
| import { LogEntry, LogLine, nilLog } from '@/game/interface' | 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 { VoreRelay } from '@/game/events' | ||||
| import { StatusConsequence } from '@/game/combat/consequences' | import { StatusConsequence } from '@/game/combat/consequences' | ||||
| import { SurrenderEffect } from '@/game/combat/effects' | import { SurrenderEffect } from '@/game/combat/effects' | ||||
| import { ToBe, Verb } from '@/game/language' | import { ToBe, Verb } from '@/game/language' | ||||
| import { ConnectionDirection } from './vore' | |||||
| /** | /** | ||||
| * A Decider determines how favorable an action is to perform. | * 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 | * Weights actions based on how likely they are to succeed | ||||
| */ | */ | ||||
| @@ -224,6 +242,7 @@ export class VoreAI extends AI { | |||||
| super( | super( | ||||
| [ | [ | ||||
| new NoReleaseDecider(), | new NoReleaseDecider(), | ||||
| new OnlyDeeperDecider(), | |||||
| new NoSurrenderDecider(), | new NoSurrenderDecider(), | ||||
| new NoPassDecider(), | new NoPassDecider(), | ||||
| new ChanceDecider(), | new ChanceDecider(), | ||||
| @@ -1,10 +1,20 @@ | |||||
| import { Creature } from "./creature" | 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 { | export enum DamageType { | ||||
| Pierce = "Pierce", | Pierce = "Pierce", | ||||
| @@ -29,19 +39,19 @@ export enum Vigor { | |||||
| Resolve = "Resolve" | Resolve = "Resolve" | ||||
| } | } | ||||
| export const VigorIcons: {[key in Vigor]: string} = { | |||||
| export const VigorIcons: { [key in Vigor]: string } = { | |||||
| Health: "fas fa-heart", | Health: "fas fa-heart", | ||||
| Stamina: "fas fa-bolt", | Stamina: "fas fa-bolt", | ||||
| Resolve: "fas fa-brain" | 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", | Health: "How much damage you can take", | ||||
| Stamina: "How much energy you have", | Stamina: "How much energy you have", | ||||
| Resolve: "How much dominance you can resist" | Resolve: "How much dominance you can resist" | ||||
| } | } | ||||
| export type Vigors = {[key in Vigor]: number} | |||||
| export type Vigors = { [key in Vigor]: number } | |||||
| export enum Stat { | export enum Stat { | ||||
| Toughness = "Toughness", | Toughness = "Toughness", | ||||
| @@ -52,9 +62,9 @@ export enum Stat { | |||||
| Charm = "Charm" | 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, | Toughness: Vigor.Health, | ||||
| Power: Vigor.Health, | Power: Vigor.Health, | ||||
| Reflexes: Vigor.Stamina, | Reflexes: Vigor.Stamina, | ||||
| @@ -63,22 +73,22 @@ export const StatToVigor: {[key in Stat]: Vigor} = { | |||||
| Charm: Vigor.Resolve | 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 { | export enum VoreStat { | ||||
| @@ -87,15 +97,15 @@ export enum VoreStat { | |||||
| Prey = "Prey" | 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.Mass]: "fas fa-weight", | ||||
| [VoreStat.Bulk]: "fas fa-weight-hanging", | [VoreStat.Bulk]: "fas fa-weight-hanging", | ||||
| [VoreStat.Prey]: "fas fa-utensils" | [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.Mass]: "How much you weigh", | ||||
| [VoreStat.Bulk]: "Your weight, plus the weight of your prey", | [VoreStat.Bulk]: "Your weight, plus the weight of your prey", | ||||
| [VoreStat.Prey]: "How many creatures you've got inside of you" | [VoreStat.Prey]: "How many creatures you've got inside of you" | ||||
| @@ -109,7 +119,7 @@ export interface CombatTest { | |||||
| } | } | ||||
| export interface Targeter { | 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 { | toString (): string { | ||||
| return this.damages.map(damage => damage.amount + " " + damage.type).join("/") | |||||
| return this.damages | |||||
| .map(damage => damage.amount + " " + damage.type) | |||||
| .join("/") | |||||
| } | } | ||||
| render (): LogEntry { | 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? | // TODO is there a way to do this that will satisfy the typechecker? | ||||
| renderShort (): LogEntry { | renderShort (): LogEntry { | ||||
| /* eslint-disable-next-line */ | /* 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 */ | /* 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 => { | this.damages.forEach(instance => { | ||||
| const factor = instance.type === DamageType.Heal ? -1 : 1 | const factor = instance.type === DamageType.Heal ? -1 : 1 | ||||
| if (instance.target in Vigor) { | 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 { | nonzero (): boolean { | ||||
| /* eslint-disable-next-line */ | /* 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 */ | /* 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 => { | this.damages.forEach(instance => { | ||||
| const factor = instance.type === DamageType.Heal ? -1 : 1 | const factor = instance.type === DamageType.Heal ? -1 : 1 | ||||
| if (instance.target in Vigor) { | if (instance.target in Vigor) { | ||||
| @@ -191,7 +234,10 @@ export class Damage { | |||||
| statTotals[instance.target as Stat] += factor * instance.amount | 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. | * Computes damage given the source and target of the damage. | ||||
| */ | */ | ||||
| export interface DamageFormula { | 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 { | export class CompositeDamageFormula implements DamageFormula { | ||||
| constructor (private formulas: DamageFormula[]) { | |||||
| } | |||||
| constructor (private formulas: DamageFormula[]) {} | |||||
| calc (user: Creature, target: Creature): Damage { | 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 { | 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 { | explain (user: Creature): LogEntry { | ||||
| @@ -226,9 +276,7 @@ export class CompositeDamageFormula implements DamageFormula { | |||||
| * Simply returns the damage it was given. | * Simply returns the damage it was given. | ||||
| */ | */ | ||||
| export class ConstantDamageFormula implements DamageFormula { | export class ConstantDamageFormula implements DamageFormula { | ||||
| constructor (private damage: Damage) { | |||||
| } | |||||
| constructor (private damage: Damage) {} | |||||
| calc (user: Creature, target: Creature): Damage { | calc (user: Creature, target: Creature): Damage { | ||||
| return this.damage | return this.damage | ||||
| @@ -239,7 +287,7 @@ export class ConstantDamageFormula implements DamageFormula { | |||||
| } | } | ||||
| explain (user: Creature): LogEntry { | 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) | * Randomly scales the damage it was given with a factor of (1-x) to (1+x) | ||||
| */ | */ | ||||
| export class UniformRandomDamageFormula implements DamageFormula { | 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 { | 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 { | describe (user: Creature, target: Creature): LogEntry { | ||||
| @@ -260,7 +308,13 @@ export class UniformRandomDamageFormula implements DamageFormula { | |||||
| } | } | ||||
| explain (user: Creature): LogEntry { | 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 | * A [[DamageFormula]] that uses the attacker's stats | ||||
| */ | */ | ||||
| export class StatDamageFormula implements DamageFormula { | 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 { | calc (user: Creature, target: Creature): Damage { | ||||
| const instances: Array<DamageInstance> = this.factors.map(factor => { | const instances: Array<DamageInstance> = this.factors.map(factor => { | ||||
| @@ -310,12 +369,17 @@ export class StatDamageFormula implements DamageFormula { | |||||
| explain (user: Creature): LogEntry { | explain (user: Creature): LogEntry { | ||||
| return new LogLine( | return new LogLine( | ||||
| `Deal `, | `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 | * Deals a percentage of the target's current vigors/stats | ||||
| */ | */ | ||||
| export class FractionDamageFormula implements DamageFormula { | 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 { | calc (user: Creature, target: Creature): Damage { | ||||
| const instances: Array<DamageInstance> = this.factors.map(factor => { | const instances: Array<DamageInstance> = this.factors.map(factor => { | ||||
| if (factor.target in Stat) { | if (factor.target in Stat) { | ||||
| return { | 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, | target: factor.target, | ||||
| type: factor.type | type: factor.type | ||||
| } | } | ||||
| } else if (factor.target in Vigor) { | } else if (factor.target in Vigor) { | ||||
| return { | 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, | target: factor.target, | ||||
| type: factor.type | type: factor.type | ||||
| } | } | ||||
| @@ -362,10 +435,15 @@ export class FractionDamageFormula implements DamageFormula { | |||||
| explain (user: Creature): LogEntry { | explain (user: Creature): LogEntry { | ||||
| return new LogLine( | return new LogLine( | ||||
| `Deal damage equal to `, | `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. | * A Combatant has a list of possible actions to take, as well as a side. | ||||
| */ | */ | ||||
| export interface Combatant { | 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 desc: TextLike, | ||||
| public conditions: Array<Condition> = [], | public conditions: Array<Condition> = [], | ||||
| public tests: Array<CombatTest> = [] | public tests: Array<CombatTest> = [] | ||||
| ) { | |||||
| } | |||||
| ) {} | |||||
| allowed (user: Creature, target: Creature): boolean { | allowed (user: Creature, target: Creature): boolean { | ||||
| return this.conditions.every(cond => cond.allowed(user, target)) | return this.conditions.every(cond => cond.allowed(user, target)) | ||||
| @@ -405,24 +481,69 @@ export abstract class Action { | |||||
| } | } | ||||
| try (user: Creature, targets: Array<Creature>): LogEntry { | 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 results = targets.map(target => { | ||||
| const failReason = this.tests.find(test => !test.test(user, target)) | const failReason = this.tests.find(test => !test.test(user, target)) | ||||
| if (failReason !== undefined) { | if (failReason !== undefined) { | ||||
| return { failed: true, target: target, log: failReason.fail(user, target) } | |||||
| return { | |||||
| failed: true, | |||||
| target: target, | |||||
| log: failReason.fail(user, target) | |||||
| } | |||||
| } else { | } else { | ||||
| return { failed: false, target: target, log: this.execute(user, target) } | |||||
| return { | |||||
| failed: false, | |||||
| target: target, | |||||
| log: this.execute(user, target) | |||||
| } | |||||
| } | } | ||||
| }) | }) | ||||
| return new LogLines( | return new LogLines( | ||||
| preActionLogs, | |||||
| preReceiveActionLogs, | |||||
| ...results.map(result => result.log), | ...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 { | describe (user: Creature, target: Creature, verbose = true): LogEntry { | ||||
| return new LogLines( | 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( | new LogLine( | ||||
| `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` | `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` | ||||
| ), | ), | ||||
| @@ -432,7 +553,10 @@ export abstract class Action { | |||||
| } | } | ||||
| odds (user: Creature, target: Creature): number { | 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> { | targets (primary: Creature, encounter: Encounter): Array<Creature> { | ||||
| @@ -443,13 +567,13 @@ export abstract class Action { | |||||
| return nilLog | return nilLog | ||||
| } | } | ||||
| abstract execute (user: Creature, target: Creature): LogEntry | |||||
| abstract execute(user: Creature, target: Creature): LogEntry | |||||
| } | } | ||||
| export class CompositionAction extends Action { | 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 ( | constructor ( | ||||
| name: TextLike, | name: TextLike, | ||||
| @@ -470,27 +594,34 @@ export class CompositionAction extends Action { | |||||
| execute (user: Creature, target: Creature): LogEntry { | execute (user: Creature, target: Creature): LogEntry { | ||||
| return new LogLines( | 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 { | executeAll (user: Creature, targets: Array<Creature>): LogEntry { | ||||
| return new LogLines( | 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 { | describe (user: Creature, target: Creature): LogEntry { | ||||
| return new LogLines( | 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) { | 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 | * 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 | * 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 | * 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 | * 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 { | return { | ||||
| prevented: false, | prevented: false, | ||||
| log: nilLog | log: nilLog | ||||
| @@ -552,7 +690,10 @@ export class Effective { | |||||
| /** | /** | ||||
| * Executes before the creature is attacked | * 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 { | return { | ||||
| prevented: false, | prevented: false, | ||||
| log: nilLog | log: nilLog | ||||
| @@ -580,7 +721,10 @@ export class Effective { | |||||
| * Called when a test is about to resolve. Decides if the creature should automatically fail. | * 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 { | return { | ||||
| failed: false, | failed: false, | ||||
| log: nilLog | log: nilLog | ||||
| @@ -597,28 +741,56 @@ export class Effective { | |||||
| /** | /** | ||||
| * Additively modifies a creature's score for an offensive test | * 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 | return 0 | ||||
| } | } | ||||
| /** | /** | ||||
| * Additively modifies a creature's score for a defensive test | * 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 | return 0 | ||||
| } | } | ||||
| /** | /** | ||||
| * Affects digestion damage | * 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 | return damage | ||||
| } | } | ||||
| /** | /** | ||||
| * Triggers after consumption | * 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 | return nilLog | ||||
| } | } | ||||
| @@ -653,24 +825,35 @@ export interface VisibleStatus { | |||||
| * a status indicating that it is dead, but entities cannot be "given" the dead effect | * a status indicating that it is dead, but entities cannot be "given" the dead effect | ||||
| */ | */ | ||||
| export class ImplicitStatus implements VisibleStatus { | 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. | * This kind of status is explicitly given to a creature. | ||||
| */ | */ | ||||
| export abstract class StatusEffect extends Effective implements VisibleStatus { | 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() | super() | ||||
| } | } | ||||
| get topLeft () { return '' } | |||||
| get bottomRight () { return '' } | |||||
| get topLeft () { | |||||
| return "" | |||||
| } | |||||
| get bottomRight () { | |||||
| return "" | |||||
| } | |||||
| } | } | ||||
| export type EncounterDesc = { | export type EncounterDesc = { | ||||
| @@ -704,7 +887,9 @@ export class Encounter { | |||||
| this.combatants.forEach(combatant => { | this.combatants.forEach(combatant => { | ||||
| // this should never be undefined | // this should never be undefined | ||||
| const currentProgress = this.initiatives.get(combatant) ?? 0 | 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) | times.set(combatant, remaining) | ||||
| }) | }) | ||||
| @@ -714,12 +899,18 @@ export class Encounter { | |||||
| return closestTime <= nextTime ? closest : next | return closestTime <= nextTime ? closest : next | ||||
| }, this.combatants[0]) | }, 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 => { | this.combatants.forEach(combatant => { | ||||
| // still not undefined... | // still not undefined... | ||||
| const currentProgress = this.initiatives.get(combatant) ?? 0 | 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 | // TODO: still let the creature use drained-vigor moves | ||||
| @@ -728,24 +919,21 @@ export class Encounter { | |||||
| return this.nextMove(closestRemaining + totalTime) | return this.nextMove(closestRemaining + totalTime) | ||||
| } else { | } else { | ||||
| // applies digestion every time combat advances | // 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)) { | 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 { | } 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 | * 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) { | if (remaining.size === 1) { | ||||
| return Array.from(remaining)[0] | return Array.from(remaining)[0] | ||||
| @@ -768,8 +960,12 @@ export class Encounter { | |||||
| /** | /** | ||||
| * Combat is completely won once one side is completely destroyed | * 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) { | if (remaining.size === 1) { | ||||
| return Array.from(remaining)[0] | return Array.from(remaining)[0] | ||||
| @@ -780,27 +976,23 @@ export class Encounter { | |||||
| } | } | ||||
| export abstract class Consequence { | export abstract class Consequence { | ||||
| constructor (public conditions: Condition[]) { | |||||
| } | |||||
| constructor (public conditions: Condition[]) {} | |||||
| applicable (user: Creature, target: Creature): boolean { | applicable (user: Creature, target: Creature): boolean { | ||||
| return this.conditions.every(cond => cond.allowed(user, target)) | 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 { | export abstract class GroupConsequence { | ||||
| constructor (public conditions: Condition[]) { | |||||
| } | |||||
| constructor (public conditions: Condition[]) {} | |||||
| applicable (user: Creature, target: Creature): boolean { | applicable (user: Creature, target: Creature): boolean { | ||||
| return this.conditions.every(cond => cond.allowed(user, target)) | 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 { Creature } from "../creature" | ||||
| import { Damage, DamageFormula, Vigor, Action, Condition, CombatTest, CompositionAction } from '@/game/combat' | import { Damage, DamageFormula, Vigor, Action, Condition, CombatTest, CompositionAction } from '@/game/combat' | ||||
| import { LogLine, LogLines, LogEntry } from '@/game/interface' | 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 { CapableCondition, UserDrainedVigorCondition, TogetherCondition, EnemyCondition, SoloCondition, PairCondition, ContainsCondition, ContainedByCondition, HasRoomCondition } from '@/game/combat/conditions' | ||||
| import { ConsumeConsequence } from '@/game/combat/consequences' | import { ConsumeConsequence } from '@/game/combat/consequences' | ||||
| import * as Words from '@/game/words' | |||||
| /** | /** | ||||
| * The PassAction has no effect. | * 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 { | export class RubAction extends Action { | ||||
| constructor (protected container: VoreContainer) { | |||||
| constructor (protected container: Container) { | |||||
| super( | super( | ||||
| new DynText('Rub (', new LiveText(container, container => container.name.all), ')'), | new DynText('Rub (', new LiveText(container, container => container.name.all), ')'), | ||||
| 'Digest your prey more quickly', | 'Digest your prey more quickly', | ||||
| @@ -234,8 +277,12 @@ export class RubAction extends Action { | |||||
| } | } | ||||
| execute (user: Creature, target: Creature): LogEntry { | 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 { | describe (user: Creature, target: Creature): LogEntry { | ||||
| @@ -272,17 +319,15 @@ export class ReleaseAction extends Action { | |||||
| export class TransferAction extends Action { | export class TransferAction extends Action { | ||||
| verb: Verb = new Verb('send') | verb: Verb = new Verb('send') | ||||
| constructor (protected from: Container, protected to: Container, name = 'Transfer') { | |||||
| constructor (public from: Container, public to: Connection, name = 'Transfer') { | |||||
| super( | super( | ||||
| name, | name, | ||||
| `${from.name.all.capital} to ${to.name.all}`, | |||||
| `${from.name.all.capital} to ${to.destination.name.all}`, | |||||
| [new CapableCondition(), new PairCondition()] | [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) { | allowed (user: Creature, target: Creature) { | ||||
| if (target.containedIn === this.from && this.from.contents.includes(target)) { | 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 { | 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 { | 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 { 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 { | export class InstantKillEffect extends StatusEffect { | ||||
| constructor () { | constructor () { | ||||
| super('Instant Kill', 'Instant kill!', 'fas fa-skull') | |||||
| super("Instant Kill", "Instant kill!", "fas fa-skull") | |||||
| } | } | ||||
| onApply (creature: Creature) { | onApply (creature: Creature) { | ||||
| @@ -15,8 +24,10 @@ export class InstantKillEffect extends StatusEffect { | |||||
| creature.removeEffect(this) | creature.removeEffect(this) | ||||
| return new LogLines( | return new LogLines( | ||||
| new LogLine( | 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()) | creature.takeDamage(new Damage()) | ||||
| ) | ) | ||||
| @@ -25,8 +36,12 @@ export class InstantKillEffect extends StatusEffect { | |||||
| export class StunEffect extends StatusEffect { | export class StunEffect extends StatusEffect { | ||||
| constructor (private duration: number) { | 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 () { | get topLeft () { | ||||
| @@ -34,11 +49,19 @@ export class StunEffect extends StatusEffect { | |||||
| } | } | ||||
| onApply (creature: Creature) { | 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) { | 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 } { | preTurn (creature: Creature): { prevented: boolean; log: LogEntry } { | ||||
| @@ -46,7 +69,9 @@ export class StunEffect extends StatusEffect { | |||||
| return { | return { | ||||
| prevented: true, | prevented: true, | ||||
| log: new LogLines( | 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) | creature.removeEffect(this) | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -54,7 +79,9 @@ export class StunEffect extends StatusEffect { | |||||
| return { | return { | ||||
| prevented: true, | prevented: true, | ||||
| log: new LogLines( | 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 { | export class DamageTypeResistanceEffect extends StatusEffect { | ||||
| constructor (private damageTypes: DamageType[], private amount: number) { | 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) { | 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) { | 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) { | modResistance (type: DamageType, factor: number) { | ||||
| @@ -85,17 +127,27 @@ export class DamageTypeResistanceEffect extends StatusEffect { | |||||
| export class PredatorCounterEffect extends StatusEffect { | export class PredatorCounterEffect extends StatusEffect { | ||||
| constructor (private devour: Action, private chance: number) { | 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) { | 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 { | return { | ||||
| prevented: true, | prevented: true, | ||||
| log: new LogLines( | 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 { | } else { | ||||
| @@ -106,20 +158,31 @@ export class PredatorCounterEffect extends StatusEffect { | |||||
| export class UntouchableEffect extends StatusEffect { | export class UntouchableEffect extends StatusEffect { | ||||
| constructor () { | 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 { | export class DazzlingEffect extends StatusEffect { | ||||
| constructor (private conditions: Condition[]) { | 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) { | preReceiveAction (creature: Creature, attacker: Creature) { | ||||
| @@ -127,7 +190,9 @@ export class DazzlingEffect extends StatusEffect { | |||||
| attacker.applyEffect(new StunEffect(1)) | attacker.applyEffect(new StunEffect(1)) | ||||
| return { | return { | ||||
| prevented: true, | 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 { | } else { | ||||
| return { | return { | ||||
| @@ -140,21 +205,32 @@ export class DazzlingEffect extends StatusEffect { | |||||
| export class SurrenderEffect extends StatusEffect { | export class SurrenderEffect extends StatusEffect { | ||||
| constructor () { | 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 { | onApply (creature: Creature): LogEntry { | ||||
| creature.takeDamage( | 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( | 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 { | return { | ||||
| failed: true, | failed: true, | ||||
| log: nilLog | log: nilLog | ||||
| @@ -164,7 +240,7 @@ export class SurrenderEffect extends StatusEffect { | |||||
| export class SizeEffect extends StatusEffect { | export class SizeEffect extends StatusEffect { | ||||
| constructor (private change: number) { | 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 { | onApply (creature: Creature): LogLine { | ||||
| @@ -178,26 +254,47 @@ export class SizeEffect extends StatusEffect { | |||||
| export class DigestionPowerEffect extends StatusEffect { | export class DigestionPowerEffect extends StatusEffect { | ||||
| constructor (private factor: number) { | 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 { | 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) { | 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 { | } else { | ||||
| return new LogLine(`${creature.name.capital} can't digest people...`) | 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) | return damage.scale(this.factor) | ||||
| } | } | ||||
| } | } | ||||
| export class StatEffect extends StatusEffect { | 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 { | modStat (creature: Creature, stat: Stat, current: number): number { | ||||
| @@ -211,16 +308,26 @@ export class StatEffect extends StatusEffect { | |||||
| export class InstantDigestionEffect extends StatusEffect { | export class InstantDigestionEffect extends StatusEffect { | ||||
| constructor () { | 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()) | prey.applyEffect(new InstantKillEffect()) | ||||
| predator.voreStats.Mass += prey.voreStats.Mass | predator.voreStats.Mass += prey.voreStats.Mass | ||||
| prey.voreStats.Mass = 0 | prey.voreStats.Mass = 0 | ||||
| return new LogLines( | 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]) | 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 { 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 { Noun, Pronoun, SoloLine, Verb } from '@/game/language' | ||||
| import { LogEntry, LogLines, LogLine } from '@/game/interface' | 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 { Item, EquipmentSlot, Equipment, ItemKind, Currency } from '@/game/items' | ||||
| import { PassAction } from '@/game/combat/actions' | import { PassAction } from '@/game/combat/actions' | ||||
| import { AI, RandomAI } from '@/game/ai' | import { AI, RandomAI } from '@/game/ai' | ||||
| @@ -31,8 +31,7 @@ export class Creature extends Entity { | |||||
| destroyed = false; | destroyed = false; | ||||
| containers: Array<VoreContainer> = [] | |||||
| otherContainers: Array<Container> = [] | |||||
| containers: Array<Container> = [] | |||||
| containedIn: Container | null = null | containedIn: Container | null = null | ||||
| @@ -42,12 +41,14 @@ export class Creature extends Entity { | |||||
| desc = "Some creature"; | desc = "Some creature"; | ||||
| get effects (): Array<Effective> { | 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( | Object.values(this.equipment).filter(item => item !== undefined).flatMap( | ||||
| item => (item as Equipment).effects | item => (item as Equipment).effects | ||||
| ), | ), | ||||
| this.perks | this.perks | ||||
| ) | ) | ||||
| return effects | |||||
| } | } | ||||
| statusEffects: Array<StatusEffect> = []; | statusEffects: Array<StatusEffect> = []; | ||||
| @@ -81,7 +82,7 @@ export class Creature extends Entity { | |||||
| this.voreStats = { | this.voreStats = { | ||||
| get [VoreStat.Bulk] () { | get [VoreStat.Bulk] () { | ||||
| return self.containers.reduce( | return self.containers.reduce( | ||||
| (total: number, container: VoreContainer) => { | |||||
| (total: number, container: Container) => { | |||||
| return total + container.contents.reduce( | return total + container.contents.reduce( | ||||
| (total: number, prey: Creature) => { | (total: number, prey: Creature) => { | ||||
| return total + prey.voreStats.Bulk | return total + prey.voreStats.Bulk | ||||
| @@ -111,7 +112,7 @@ export class Creature extends Entity { | |||||
| }, | }, | ||||
| get [VoreStat.Prey] () { | get [VoreStat.Prey] () { | ||||
| return self.containers.reduce( | return self.containers.reduce( | ||||
| (total: number, container: VoreContainer) => { | |||||
| (total: number, container: Container) => { | |||||
| return total + container.contents.concat(container.digested).reduce( | return total + container.contents.concat(container.digested).reduce( | ||||
| (total: number, prey: Creature) => { | (total: number, prey: Creature) => { | ||||
| return total + 1 + prey.voreStats[VoreStat.Prey] | return total + 1 + prey.voreStats[VoreStat.Prey] | ||||
| @@ -206,15 +207,11 @@ export class Creature extends Entity { | |||||
| return effect.onApply(this) | return effect.onApply(this) | ||||
| } | } | ||||
| addVoreContainer (container: VoreContainer): void { | |||||
| addContainer (container: Container): void { | |||||
| this.containers.push(container) | this.containers.push(container) | ||||
| this.voreRelay.connect(container.voreRelay) | this.voreRelay.connect(container.voreRelay) | ||||
| } | } | ||||
| addOtherContainer (container: Container): void { | |||||
| this.otherContainers.push(container) | |||||
| } | |||||
| addPerk (perk: Perk): void { | addPerk (perk: Perk): void { | ||||
| this.perks.push(perk) | this.perks.push(perk) | ||||
| } | } | ||||
| @@ -265,6 +262,7 @@ export class Creature extends Entity { | |||||
| this.statusEffects.forEach(effect => { | this.statusEffects.forEach(effect => { | ||||
| results.push(effect) | results.push(effect) | ||||
| }) | }) | ||||
| return results | return results | ||||
| } | } | ||||
| @@ -273,7 +271,6 @@ export class Creature extends Entity { | |||||
| this.actions, | this.actions, | ||||
| this.containers.flatMap(container => container.actions), | this.containers.flatMap(container => container.actions), | ||||
| target.otherActions, | target.otherActions, | ||||
| this.otherContainers.flatMap(container => container.actions), | |||||
| Object.values(this.equipment).filter(item => item !== undefined).flatMap(item => (item as Equipment).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.items.filter(item => item.kind === ItemKind.Consumable && !item.consumed).flatMap(item => item.actions), | ||||
| this.perks.flatMap(perk => perk.actions(this)) | 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 { Creature } from "../creature" | ||||
| import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '@/game/language' | import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '@/game/language' | ||||
| import { Damage, DamageType, Vigor, ConstantDamageFormula } from '@/game/combat' | 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 { AttackAction } from '@/game/combat/actions' | ||||
| import { RavenousPerk, BellyBulwakPerk, FlauntPerk } from '@/game/combat/perks' | |||||
| import { VoreAI } from "../ai" | import { VoreAI } from "../ai" | ||||
| import { nilLog } from "../interface" | |||||
| export default class Player extends Creature { | export default class Player extends Creature { | ||||
| constructor () { | 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 })))) | 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 }))) | 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.perspective = POV.Second | ||||
| this.ai = new VoreAI(this) | 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 { TargetDrainedVigorCondition } from '@/game/combat/conditions' | ||||
| import { Creature } from '@/game/creature' | import { Creature } from '@/game/creature' | ||||
| import { LogEntry, LogLines, nilLog } from "@/game/interface" | import { LogEntry, LogLines, nilLog } from "@/game/interface" | ||||
| import { VoreContainer } from '@/game/vore' | |||||
| import { Container } from '@/game/vore' | |||||
| import { Action } from './combat' | import { Action } from './combat' | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| @@ -43,13 +43,15 @@ abstract class Relay<Sender, EventMap extends { [name: string]: any }> { | |||||
| type VoreMap = { | type VoreMap = { | ||||
| "onEaten": { prey: Creature }; | "onEaten": { prey: Creature }; | ||||
| "onReleased": { prey: Creature }; | "onReleased": { prey: Creature }; | ||||
| "onEntered": { prey: Creature }; | |||||
| "onExited": { prey: Creature }; | |||||
| "onDigested": { prey: Creature }; | "onDigested": { prey: Creature }; | ||||
| "onAbsorbed": { prey: Creature }; | "onAbsorbed": { prey: Creature }; | ||||
| } | } | ||||
| export class VoreRelay extends Relay<VoreContainer, VoreMap> { | |||||
| export class VoreRelay extends Relay<Container, VoreMap> { | |||||
| constructor () { | 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 { | export enum FormatOpt { | ||||
| Damage = "log-damage", | 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 SoloLine<T> = (user: T) => LogEntry | ||||
| export type SoloLineArgs<T, V> = (user: T, args: V) => 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 | export type GroupLine<T> = (user: T, targets: Array<T>) => LogEntry | ||||
| enum NounKind { | enum NounKind { | ||||
| Specific, | |||||
| Nonspecific, | |||||
| All | |||||
| Specific, | |||||
| Nonspecific, | |||||
| All | |||||
| } | } | ||||
| enum VowelSound { | enum VowelSound { | ||||
| Default, | |||||
| Vowel, | |||||
| NonVowel | |||||
| Default, | |||||
| Vowel, | |||||
| NonVowel | |||||
| } | } | ||||
| enum VerbKind { | enum VerbKind { | ||||
| @@ -64,9 +69,7 @@ export type TextLike = { toString: () => string } | |||||
| // updates as needed | // updates as needed | ||||
| export class LiveText<T> { | export class LiveText<T> { | ||||
| constructor (private contents: T, private run: (thing: T) => TextLike) { | |||||
| } | |||||
| constructor (private contents: T, private run: (thing: T) => TextLike) {} | |||||
| toString (): string { | toString (): string { | ||||
| return this.run(this.contents).toString() | return this.run(this.contents).toString() | ||||
| @@ -80,17 +83,15 @@ export class DynText { | |||||
| } | } | ||||
| toString (): string { | toString (): string { | ||||
| return (this.parts.map(part => part.toString())).join('') | |||||
| return this.parts.map(part => part.toString()).join("") | |||||
| } | } | ||||
| } | } | ||||
| export abstract class Word { | 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. | // These functions are pure; they don't mutate the original object. | ||||
| // This is necessary to avoid causing chaos. | // This is necessary to avoid causing chaos. | ||||
| @@ -230,7 +231,11 @@ export class OptionalWord extends Word { | |||||
| export class RandomWord extends Word { | export class RandomWord extends Word { | ||||
| private history: { last: number } | 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) | super(opt) | ||||
| this.history = history | this.history = history | ||||
| } | } | ||||
| @@ -252,12 +257,22 @@ export class RandomWord extends Word { | |||||
| } | } | ||||
| export class Noun 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) | super(options) | ||||
| } | } | ||||
| configure (opts: WordOptions): Word { | 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 { | toString (): string { | ||||
| @@ -274,31 +289,34 @@ export class Noun extends Word { | |||||
| if (this.pluralNoun === null) { | if (this.pluralNoun === null) { | ||||
| result = this.singularNoun | result = this.singularNoun | ||||
| } else { | } else { | ||||
| result = (this.pluralNoun as string) | |||||
| result = this.pluralNoun as string | |||||
| } | } | ||||
| } else { | } else { | ||||
| result = this.singularNoun | result = this.singularNoun | ||||
| } | } | ||||
| if (!this.options.proper) { | 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) { | if (this.options.plural) { | ||||
| result = 'some ' + result | |||||
| result = "some " + result | |||||
| } else { | } else { | ||||
| if (this.options.vowel === VowelSound.Default) { | 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 { | } else { | ||||
| result = 'a ' + result | |||||
| result = "a " + result | |||||
| } | } | ||||
| } else if (this.options.vowel === VowelSound.Vowel) { | } else if (this.options.vowel === VowelSound.Vowel) { | ||||
| result = 'an ' + result | |||||
| result = "an " + result | |||||
| } else if (this.options.vowel === VowelSound.NonVowel) { | } else if (this.options.vowel === VowelSound.NonVowel) { | ||||
| result = 'a ' + result | |||||
| result = "a " + result | |||||
| } | } | ||||
| } | } | ||||
| } else if (this.options.nounKind === NounKind.Specific) { | } 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 { | export class ImproperNoun extends Noun { | ||||
| constructor (singularNoun: string, pluralNoun: string = singularNoun) { | 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 { | export class ProperNoun extends Noun { | ||||
| constructor (singularNoun: string) { | 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) | return new Adjective(this.adjective, opts) | ||||
| } | } | ||||
| // TODO caps et al. | |||||
| toString (): string { | 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 { | 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) | super(opt) | ||||
| } | } | ||||
| @@ -381,11 +441,21 @@ export class Verb extends Word { | |||||
| let choice: string | let choice: string | ||||
| switch (this.opt.verbKind) { | 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) { | if (this.opt.allCaps) { | ||||
| @@ -430,12 +500,18 @@ export class ToBe extends Word { | |||||
| let choice | let choice | ||||
| if (this.opts.plural) { | if (this.opts.plural) { | ||||
| choice = 'are' | |||||
| choice = "are" | |||||
| } | } | ||||
| switch (this.opts.perspective) { | 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) { | if (this.opt.allCaps) { | ||||
| @@ -449,16 +525,18 @@ export class ToBe extends Word { | |||||
| } | } | ||||
| interface PronounDict { | interface PronounDict { | ||||
| subjective: string; | |||||
| objective: string; | |||||
| possessive: string; | |||||
| reflexive: string; | |||||
| subjective: string; | |||||
| objective: string; | |||||
| possessive: string; | |||||
| reflexive: string; | |||||
| } | } | ||||
| export class Pronoun implements Pluralizable { | 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 { | get capital (): Pronoun { | ||||
| return new Pronoun(this.pronouns, true) | 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({ | 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({ | 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({ | 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 { | export class PronounAsNoun extends Noun { | ||||
| constructor (private pronouns: Pronoun, opt: WordOptions = emptyConfig) { | constructor (private pronouns: Pronoun, opt: WordOptions = emptyConfig) { | ||||
| @@ -559,7 +678,12 @@ export class PronounAsNoun extends Noun { | |||||
| toString (): string { | toString (): string { | ||||
| if (this.options.objective) { | 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 { | } else { | ||||
| return super.toString() | return super.toString() | ||||
| } | } | ||||
| @@ -9,10 +9,9 @@ import { InstantDigestionEffect, SurrenderEffect } from '@/game/combat/effects' | |||||
| import moment from 'moment' | import moment from 'moment' | ||||
| import { VoreAI } from '@/game/ai' | import { VoreAI } from '@/game/ai' | ||||
| import { DeliciousPerk } from '@/game/combat/perks' | import { DeliciousPerk } from '@/game/combat/perks' | ||||
| import Inazuma from '../creatures/characters/inazuma' | |||||
| import Samuel from '../creatures/characters/Samuel' | import Samuel from '../creatures/characters/Samuel' | ||||
| import Human from '../creatures/human' | import Human from '../creatures/human' | ||||
| import Slime from '../creatures/monsters/slime' | |||||
| import Werewolf from '../creatures/monsters/werewolf' | |||||
| function makeParty (): Creature[] { | function makeParty (): Creature[] { | ||||
| const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, { | const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, { | ||||
| @@ -122,14 +121,14 @@ export const Newtown = (): Place => { | |||||
| ) | ) | ||||
| deepwoods.choices.push( | deepwoods.choices.push( | ||||
| new Choice( | new Choice( | ||||
| "Fight Inazuma", | |||||
| "Go fight Inazuma!", | |||||
| "Fight Werewolf", | |||||
| "Go fight Werewolf!", | |||||
| (world, executor) => { | (world, executor) => { | ||||
| const enemy = new Inazuma() | |||||
| const enemy = new Werewolf() | |||||
| const encounter = new Encounter( | const encounter = new Encounter( | ||||
| { | { | ||||
| name: "Fight some tough nerd", | name: "Fight some tough nerd", | ||||
| intro: () => new LogLine(`Inazuma Approaches!`) | |||||
| intro: () => new LogLine(`Werewolf Approaches!`) | |||||
| }, | }, | ||||
| [world.player, enemy].concat(world.party) | [world.player, enemy].concat(world.party) | ||||
| ) | ) | ||||
| @@ -141,7 +140,7 @@ export const Newtown = (): Place => { | |||||
| const bossEncounters = [ | const bossEncounters = [ | ||||
| new Encounter( | new Encounter( | ||||
| { name: "Inazuma", intro: () => nilLog }, | { 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( | woods.choices.push( | ||||
| new Choice( | new Choice( | ||||
| @@ -1,5 +1,5 @@ | |||||
| import { Place, Choice, Direction, World } from '@/game/world' | 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 { Encounter, Stat, Damage, DamageType, Vigor, Side } from '@/game/combat' | ||||
| import * as Items from '@/game/items' | import * as Items from '@/game/items' | ||||
| import { LogLine, nilLog, LogLines } from '@/game/interface' | import { LogLine, nilLog, LogLines } from '@/game/interface' | ||||
| @@ -9,10 +9,9 @@ import { InstantDigestionEffect, SurrenderEffect } from '@/game/combat/effects' | |||||
| import moment from 'moment' | import moment from 'moment' | ||||
| import { VoreAI } from '@/game/ai' | import { VoreAI } from '@/game/ai' | ||||
| import { DeliciousPerk } from '@/game/combat/perks' | import { DeliciousPerk } from '@/game/combat/perks' | ||||
| import Inazuma from '../creatures/characters/inazuma' | |||||
| import Human from '../creatures/human' | import Human from '../creatures/human' | ||||
| import Slime from '../creatures/monsters/slime' | |||||
| import Samuel from '../creatures/characters/Samuel' | import Samuel from '../creatures/characters/Samuel' | ||||
| import Werewolf from '../creatures/monsters/werewolf' | |||||
| function makeParty (): Creature[] { | function makeParty (): Creature[] { | ||||
| const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, { | const fighter = new Human(new ProperNoun("Redgar"), MalePronouns, { | ||||
| @@ -103,13 +102,6 @@ export const Town = (): Place => { | |||||
| "The center of town" | "The center of town" | ||||
| ) | ) | ||||
| const bossEncounters = [ | |||||
| new Encounter( | |||||
| { name: "Inazuma", intro: () => nilLog }, | |||||
| makeParty().concat([new Inazuma()]) | |||||
| ) | |||||
| ] | |||||
| home.choices.push( | home.choices.push( | ||||
| new Choice( | new Choice( | ||||
| "Nap", | "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( | square.choices.push( | ||||
| new Choice( | new Choice( | ||||
| "Eat someone", | "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( | debug.choices.push( | ||||
| new Choice( | new Choice( | ||||
| "Cut stats", | "Cut stats", | ||||
| @@ -324,14 +317,15 @@ export const Town = (): Place => { | |||||
| woods.choices.push( | woods.choices.push( | ||||
| new Choice( | new Choice( | ||||
| "Fight a slime", | |||||
| "Go fight a slime", | |||||
| "Fight a werewolf", | |||||
| "Go fight a werewolf", | |||||
| (world, executor) => { | (world, executor) => { | ||||
| const enemy = new Slime() | |||||
| const enemy = new Werewolf() | |||||
| enemy.location = world.player.location | |||||
| const encounter = new Encounter( | const encounter = new Encounter( | ||||
| { | { | ||||
| name: "Fight some tasty nerd", | 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) | [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 Words from '@/game/words' | ||||
| import * as Onomatopoeia from '@/game/onomatopoeia' | |||||
| import { Creature } from '@/game/creature' | import { Creature } from '@/game/creature' | ||||
| import { VoreRelay } from '@/game/events' | import { VoreRelay } from '@/game/events' | ||||
| export enum VoreType { | 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([ | 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 { | export interface Container extends Actionable { | ||||
| name: Noun; | name: Noun; | ||||
| owner: Creature; | owner: Creature; | ||||
| voreTypes: Set<VoreType>; | 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>; | 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; | consumeVerb: Verb; | ||||
| consumePreposition: Preposition; | consumePreposition: Preposition; | ||||
| @@ -51,35 +90,91 @@ export interface Container extends Actionable { | |||||
| struggleVerb: Verb; | struggleVerb: Verb; | ||||
| strugglePreposition: Preposition; | 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; | 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 | public name: Noun | ||||
| contents: Array<Creature> = [] | contents: Array<Creature> = [] | ||||
| actions: Array<Action> = [] | 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') | releaseVerb = new Verb('release', 'releases', 'releasing', 'released') | ||||
| releasePreposition = new Preposition("from") | |||||
| releasePreposition = new Preposition("out from") | |||||
| struggleVerb = new Verb('struggle', 'struggles', 'struggling', 'struggled') | 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.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 { | get capacity (): number { | ||||
| return this.capacityFactor * this.owner.voreStats.Mass | 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 { | releaseLine (user: Creature, target: Creature): LogEntry { | ||||
| @@ -91,7 +186,7 @@ export abstract class NormalContainer implements Container { | |||||
| } | } | ||||
| get fullness (): number { | 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 { | canTake (prey: Creature): boolean { | ||||
| @@ -105,22 +200,64 @@ export abstract class NormalContainer implements Container { | |||||
| } | } | ||||
| consume (prey: Creature): LogEntry { | 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) { | if (prey.containedIn !== null) { | ||||
| prey.containedIn.contents = prey.containedIn.contents.filter(item => prey !== item) | prey.containedIn.contents = prey.containedIn.contents.filter(item => prey !== item) | ||||
| } | } | ||||
| this.contents.push(prey) | this.contents.push(prey) | ||||
| prey.containedIn = this | 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 | prey.containedIn = this.owner.containedIn | ||||
| this.contents = this.contents.filter(victim => victim !== prey) | this.contents = this.contents.filter(victim => victim !== prey) | ||||
| if (this.owner.containedIn !== null) { | if (this.owner.containedIn !== null) { | ||||
| this.owner.containedIn.contents.push(prey) | 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 { | struggle (prey: Creature): LogEntry { | ||||
| @@ -136,79 +273,27 @@ export abstract class NormalContainer implements Container { | |||||
| return new LogLine(...lines) | 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) { | 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 { | 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(`${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 { | 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 { | absorbLine (user: Creature, target: Creature): LogEntry { | ||||
| @@ -242,23 +341,25 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||||
| const tickedEntryList: LogEntry[] = [] | 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) | const tickedEntries = new LogLines(...tickedEntryList) | ||||
| @@ -302,19 +403,13 @@ export abstract class NormalVoreContainer extends NormalContainer implements Vor | |||||
| } | } | ||||
| digest (preys: Creature[]): LogEntry { | 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 { | 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([ | export const Swallow = new RandomWord([ | ||||
| new Verb("swallow"), | new Verb("swallow"), | ||||
| new Verb("gulp"), | |||||
| new Verb("consume", "consumes", "consuming", "consumed") | |||||
| new Verb("gulp") | |||||
| ]) | ]) | ||||
| export const Churns = new RandomWord([ | export const Churns = new RandomWord([ | ||||
| @@ -45,10 +44,8 @@ export const Dark = new RandomWord([ | |||||
| export const Digest = new RandomWord([ | export const Digest = new RandomWord([ | ||||
| new Verb("digest"), | new Verb("digest"), | ||||
| new Verb("melt down", "melts down", "melting down", "melted down"), | |||||
| new Verb("dissolve", "dissolves", "dissolving", "dissolved"), | 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([ | export const Absorb = new RandomWord([ | ||||
| @@ -120,3 +117,22 @@ export const Sloppily = new RandomWord([ | |||||
| new Adverb("sloppily"), | new Adverb("sloppily"), | ||||
| new Adverb("messily") | 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>; | joinGeneral (item: T, endItem: T|null): Array<T>; | ||||
| /* eslint-disable-next-line */ | /* eslint-disable-next-line */ | ||||
| unique (predicate?: (elem: T) => any): Array<T>; | 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 | 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 | Vue.config.productionTip = false | ||||
| new Vue({ | new Vue({ | ||||