The test can compare one or more of the user's stats against one or more of the target's stats. This is explained by the test.master
| @@ -1,5 +1,5 @@ | |||||
| <template> | <template> | ||||
| <button @focus="describe" @mouseover="describe" @mouseleave="undescribe" class="action-button" @click="execute"> | |||||
| <button @focus="describe" @mouseover="describe" class="action-button" @click="execute"> | |||||
| <div class="action-title">{{ action.name }}</div> | <div class="action-title">{{ action.name }}</div> | ||||
| <div class="action-desc">{{ action.desc }}</div> | <div class="action-desc">{{ action.desc }}</div> | ||||
| </button> | </button> | ||||
| @@ -1,6 +1,6 @@ | |||||
| import { Creature } from "./creature" | import { Creature } from "./creature" | ||||
| import { TextLike, DynText, ToBe, LiveText, PairLineArgs, PairLine } from './language' | import { TextLike, DynText, ToBe, LiveText, PairLineArgs, PairLine } from './language' | ||||
| import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog } from './interface' | |||||
| import { LogEntry, LogLines, FAElem, LogLine, FormatEntry, FormatOpt, PropElem, nilLog, Newline } from './interface' | |||||
| import { Resistances } from './entity' | import { Resistances } from './entity' | ||||
| import { World } from './world' | import { World } from './world' | ||||
| @@ -52,6 +52,15 @@ export enum Stat { | |||||
| export type Stats = {[key in Stat]: number} | export type Stats = {[key in Stat]: number} | ||||
| export const StatToVigor: {[key in Stat]: Vigor} = { | |||||
| Toughness: Vigor.Health, | |||||
| Power: Vigor.Health, | |||||
| Reflexes: Vigor.Stamina, | |||||
| Agility: Vigor.Stamina, | |||||
| Willpower: Vigor.Resolve, | |||||
| Charm: Vigor.Resolve | |||||
| } | |||||
| export const StatIcons: {[key in Stat]: string} = { | export const StatIcons: {[key in Stat]: string} = { | ||||
| Toughness: 'fas fa-heartbeat', | Toughness: 'fas fa-heartbeat', | ||||
| Power: 'fas fa-fist-raised', | Power: 'fas fa-fist-raised', | ||||
| @@ -359,7 +368,6 @@ export abstract class Action { | |||||
| ) { | ) { | ||||
| } | } | ||||
| // TODO explain the tests in here | |||||
| 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)) | ||||
| @@ -380,6 +388,9 @@ export abstract class Action { | |||||
| describe (user: Creature, target: Creature): LogEntry { | describe (user: Creature, target: Creature): LogEntry { | ||||
| return new LogLines( | return new LogLines( | ||||
| new LogLine( | |||||
| `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` | |||||
| ), | |||||
| ...this.tests.map(test => test.explain(user, target)) | ...this.tests.map(test => test.explain(user, target)) | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -416,6 +427,7 @@ export class CompositionAction extends Action { | |||||
| describe (user: Creature, target: Creature): LogEntry { | describe (user: Creature, target: Creature): LogEntry { | ||||
| return new LogLines( | return new LogLines( | ||||
| ...this.consequences.map(consequence => consequence.describePair(user, target)).concat( | ...this.consequences.map(consequence => consequence.describePair(user, target)).concat( | ||||
| new Newline(), | |||||
| super.describe(user, target) | super.describe(user, target) | ||||
| ) | ) | ||||
| ) | ) | ||||
| @@ -1,6 +1,7 @@ | |||||
| import { CombatTest, Stat, Vigor } from '../combat' | |||||
| import { CombatTest, Stat, Vigor, Stats, StatToVigor } from '../combat' | |||||
| import { Creature } from "../creature" | import { Creature } from "../creature" | ||||
| import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface' | import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface' | ||||
| import { Verb } from '../language' | |||||
| function logistic (x0: number, L: number, k: number): (x: number) => number { | function logistic (x0: number, L: number, k: number): (x: number) => number { | ||||
| return (x: number) => { | return (x: number) => { | ||||
| @@ -32,6 +33,94 @@ abstract class RandomTest implements CombatTest { | |||||
| abstract explain(user: Creature, target: Creature): LogEntry | abstract explain(user: Creature, target: Creature): LogEntry | ||||
| } | } | ||||
| export enum TestCategory { | |||||
| Attack = "Attack", | |||||
| Vore = "Vore" | |||||
| } | |||||
| export class OpposedStatTest extends RandomTest { | |||||
| private f: (x: number) => number | |||||
| private k = 0.1 | |||||
| // how much a stat can be reduced by its corresponding vigor being low | |||||
| private maxStatVigorPenalty = 0.5 | |||||
| // how much the total score can be reduced by each vigor being low | |||||
| private maxTotalVigorPenalty = 0.1 | |||||
| constructor ( | |||||
| public readonly userStats: Partial<Stats>, | |||||
| public readonly targetStats: Partial<Stats>, | |||||
| fail: (user: Creature, target: Creature) => LogEntry, | |||||
| public category: TestCategory, | |||||
| private bias = 0 | |||||
| ) { | |||||
| super(fail) | |||||
| this.f = logistic(0, 1, this.k) | |||||
| } | |||||
| odds (user: Creature, target: Creature): number { | |||||
| const userScore = this.getScore(user, this.userStats) | |||||
| const targetScore = this.getScore(target, this.targetStats) | |||||
| console.log(userScore, targetScore) | |||||
| return this.f(userScore - targetScore + this.bias) | |||||
| } | |||||
| explain (user: Creature, target: Creature): LogEntry { | |||||
| return new LogLines( | |||||
| new LogLine( | |||||
| `Pits `, | |||||
| ...Object.entries(this.userStats).map(([stat, frac]) => { | |||||
| if (frac !== undefined) { | |||||
| return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat)) | |||||
| } else { | |||||
| return nilLog | |||||
| } | |||||
| }), | |||||
| ` from ${user.name.possessive} stats against `, | |||||
| ...Object.entries(this.targetStats).map(([stat, frac]) => { | |||||
| if (frac !== undefined) { | |||||
| return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat)) | |||||
| } else { | |||||
| return nilLog | |||||
| } | |||||
| }), | |||||
| ` from ${target.name.possessive} stats.` | |||||
| ), | |||||
| new LogLine( | |||||
| `${user.name.capital.possessive} total score is ${this.getScore(user, this.userStats)}` | |||||
| ), | |||||
| new LogLine( | |||||
| `${target.name.capital.possessive} total score is ${this.getScore(target, this.targetStats)}` | |||||
| ), | |||||
| new LogLine( | |||||
| `${user.name.capital} ${user.name.conjugate(new Verb("have", "has"))} a ${(this.odds(user, target) * 100).toFixed(0)}% chance of winning this test.` | |||||
| ) | |||||
| ) | |||||
| } | |||||
| private getScore (actor: Creature, parts: Partial<Stats>): number { | |||||
| const total = Object.entries(parts).reduce((total: number, [stat, frac]) => { | |||||
| let value = actor.stats[stat as Stat] * (frac === undefined ? 0 : frac) | |||||
| const vigor = StatToVigor[stat as Stat] | |||||
| value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * actor.vigors[vigor] / actor.maxVigors[vigor] | |||||
| console.log(value) | |||||
| return total + value | |||||
| }, 0) | |||||
| const modifiedTotal = Object.keys(Vigor).reduce( | |||||
| (total, vigor) => { | |||||
| return total * (1 - this.maxStatVigorPenalty) + total * actor.vigors[vigor as Vigor] / actor.maxVigors[vigor as Vigor] | |||||
| }, | |||||
| total | |||||
| ) | |||||
| return modifiedTotal | |||||
| } | |||||
| } | |||||
| export class StatVigorSizeTest extends RandomTest { | export class StatVigorSizeTest extends RandomTest { | ||||
| private f: (x: number) => number | private f: (x: number) => number | ||||
| private k = 0.1 | private k = 0.1 | ||||
| @@ -1,8 +1,12 @@ | |||||
| import { Creature } from "../creature" | import { Creature } from "../creature" | ||||
| import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '../language' | import { ProperNoun, TheyPronouns, ImproperNoun, POV } from '../language' | ||||
| import { Damage, DamageType, Vigor, ConstantDamageFormula } from '../combat' | |||||
| import { Damage, DamageType, Vigor, ConstantDamageFormula, CompositionAction, UniformRandomDamageFormula, StatDamageFormula, Stat } from '../combat' | |||||
| import { Stomach, Bowels, VoreType, anyVore } from '../vore' | import { Stomach, Bowels, VoreType, anyVore } from '../vore' | ||||
| import { AttackAction } from '../combat/actions' | import { AttackAction } from '../combat/actions' | ||||
| import { TogetherCondition } from '../combat/conditions' | |||||
| import { DamageConsequence } from '../combat/consequences' | |||||
| import { OpposedStatTest, TestCategory } from '../combat/tests' | |||||
| import { LogLine } from '../interface' | |||||
| export class Player extends Creature { | export class Player extends Creature { | ||||
| constructor () { | constructor () { | ||||
| @@ -25,5 +29,38 @@ export class Player extends Creature { | |||||
| this.containers.push(bowels) | this.containers.push(bowels) | ||||
| this.perspective = POV.Second | this.perspective = POV.Second | ||||
| this.actions.push( | |||||
| new CompositionAction( | |||||
| "Bite", | |||||
| "Munch", | |||||
| { | |||||
| conditions: [ | |||||
| new TogetherCondition() | |||||
| ], | |||||
| consequences: [ | |||||
| new DamageConsequence( | |||||
| new StatDamageFormula([ | |||||
| { fraction: 2, stat: Stat.Power, target: Vigor.Health, type: DamageType.Crush }, | |||||
| { fraction: 2, stat: Stat.Power, target: Vigor.Stamina, type: DamageType.Crush } | |||||
| ]) | |||||
| ) | |||||
| ], | |||||
| tests: [ | |||||
| new OpposedStatTest( | |||||
| { | |||||
| Power: 1 | |||||
| }, | |||||
| { | |||||
| Agility: 1 | |||||
| }, | |||||
| (user, target) => new LogLine(`No munch.`), | |||||
| TestCategory.Attack, | |||||
| 0 | |||||
| ) | |||||
| ] | |||||
| } | |||||
| ) | |||||
| ) | |||||
| } | } | ||||
| } | } | ||||