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.vintage
| @@ -1,5 +1,5 @@ | |||
| <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-desc">{{ action.desc }}</div> | |||
| </button> | |||
| @@ -1,6 +1,6 @@ | |||
| import { Creature } from "./creature" | |||
| 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 { World } from './world' | |||
| @@ -52,6 +52,15 @@ export enum Stat { | |||
| 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} = { | |||
| Toughness: 'fas fa-heartbeat', | |||
| 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 { | |||
| return this.conditions.every(cond => cond.allowed(user, target)) | |||
| @@ -380,6 +388,9 @@ export abstract class Action { | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| new LogLine( | |||
| `Success chance: ${(this.odds(user, target) * 100).toFixed(0)}%` | |||
| ), | |||
| ...this.tests.map(test => test.explain(user, target)) | |||
| ) | |||
| } | |||
| @@ -416,6 +427,7 @@ export class CompositionAction extends Action { | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines( | |||
| ...this.consequences.map(consequence => consequence.describePair(user, target)).concat( | |||
| new Newline(), | |||
| 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 { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface' | |||
| import { Verb } from '../language' | |||
| function logistic (x0: number, L: number, k: number): (x: number) => number { | |||
| return (x: number) => { | |||
| @@ -32,6 +33,94 @@ abstract class RandomTest implements CombatTest { | |||
| 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 { | |||
| private f: (x: number) => number | |||
| private k = 0.1 | |||
| @@ -1,8 +1,12 @@ | |||
| import { Creature } from "../creature" | |||
| 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 { 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 { | |||
| constructor () { | |||
| @@ -25,5 +29,38 @@ export class Player extends Creature { | |||
| this.containers.push(bowels) | |||
| 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 | |||
| ) | |||
| ] | |||
| } | |||
| ) | |||
| ) | |||
| } | |||
| } | |||