| @@ -61,4 +61,55 @@ body, html { | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .stat-entry { | |||
| position: relative; | |||
| } | |||
| .stat-entry::after { | |||
| opacity: 0; | |||
| position: absolute; | |||
| color: #eee; | |||
| font-size: 0pt; | |||
| content: attr(data-tooltip); | |||
| transition: 0.1s; | |||
| pointer-events: none; | |||
| left: 0pt; | |||
| top: 0pt; | |||
| transform: translate(calc(-50% + 16pt), -100%); | |||
| background: #555; | |||
| padding: 8pt; | |||
| border-radius: 8pt; | |||
| z-index: 1; | |||
| } | |||
| .stat-entry:hover::after { | |||
| font-size: 18pt; | |||
| opacity: 1; | |||
| } | |||
| .stat-entry::before { | |||
| opacity: 0; | |||
| position: absolute; | |||
| color: #eee; | |||
| font-size: 0pt; | |||
| content: attr(data-tooltip-full); | |||
| pointer-events: none; | |||
| left: 0pt; | |||
| top: 0pt; | |||
| transform: translate(calc(-50% + 16pt), calc(-100% - 18pt - 16pt)); | |||
| white-space: nowrap; | |||
| transition: 0.1s; | |||
| background: #555; | |||
| padding: 8pt; | |||
| border-radius: 8pt; | |||
| z-index: 1; | |||
| } | |||
| .stat-entry:hover::before { | |||
| font-size: 12pt; | |||
| transition: all 1s cubic-bezier(1, 0, 0.75, 0); | |||
| opacity: 1; | |||
| } | |||
| </style> | |||
| @@ -85,52 +85,6 @@ a { | |||
| justify-content: space-evenly; | |||
| user-select: none; | |||
| } | |||
| .stat-entry::after { | |||
| opacity: 0; | |||
| position: absolute; | |||
| color: #eee; | |||
| font-size: 0pt; | |||
| content: attr(data-tooltip); | |||
| transition: 0.1s; | |||
| pointer-events: none; | |||
| left: 0pt; | |||
| top: 0pt; | |||
| transform: translate(calc(-50% + 16pt), -100%); | |||
| background: #555; | |||
| padding: 8pt; | |||
| border-radius: 8pt; | |||
| z-index: 1; | |||
| } | |||
| .stat-entry:hover::after { | |||
| font-size: 18pt; | |||
| opacity: 1; | |||
| } | |||
| .stat-entry::before { | |||
| opacity: 0; | |||
| position: absolute; | |||
| color: #eee; | |||
| font-size: 0pt; | |||
| content: attr(data-tooltip-full); | |||
| pointer-events: none; | |||
| left: 0pt; | |||
| top: 0pt; | |||
| transform: translate(calc(-50% + 16pt), calc(-100% - 18pt - 16pt)); | |||
| white-space: nowrap; | |||
| transition: 0.1s; | |||
| background: #555; | |||
| padding: 8pt; | |||
| border-radius: 8pt; | |||
| z-index: 1; | |||
| } | |||
| .stat-entry:hover::before { | |||
| font-size: 12pt; | |||
| transition: all 1s cubic-bezier(1, 0, 0.75, 0); | |||
| opacity: 1; | |||
| } | |||
| </style> | |||
| <style> | |||
| @@ -1,7 +1,8 @@ | |||
| import { Creature, POV, Entity } from './entity' | |||
| import { POVPair, POVPairArgs, TextLike, DynText, LiveText } from './language' | |||
| import { Container } from './vore' | |||
| import { LogEntry, LogLines, CompositeLog, FAElem, LogLine, FormatEntry, FormatOpt } from './interface' | |||
| import { LogEntry, LogLines, CompositeLog, FAElem, LogLine, FormatEntry, FormatOpt, PropElem } from './interface' | |||
| import { StatTest, StatVigorTest } from './combat/tests' | |||
| export enum DamageType { | |||
| Pierce = "Pierce", | |||
| @@ -70,115 +71,6 @@ export interface CombatTest { | |||
| explain: (user: Creature, target: Creature) => LogEntry; | |||
| } | |||
| function logistic (x0: number, L: number, k: number): (x: number) => number { | |||
| return (x: number) => { | |||
| return L / (1 + Math.exp(-k * (x - x0))) | |||
| } | |||
| } | |||
| abstract class RandomTest implements CombatTest { | |||
| test (user: Creature, target: Creature): boolean { | |||
| return Math.random() < this.odds(user, target) | |||
| } | |||
| abstract odds(user: Creature, target: Creature): number | |||
| abstract explain(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class StatVigorTest extends RandomTest { | |||
| private f: (x: number) => number | |||
| constructor (public readonly stat: Stat, k = 0.1) { | |||
| super() | |||
| this.f = logistic(0, 1, k) | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| let userPercent = 1 | |||
| let targetPercent = 1 | |||
| Object.keys(Vigor).forEach(key => { | |||
| userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor] | |||
| targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor] | |||
| userPercent = Math.max(0, userPercent) | |||
| targetPercent = Math.max(0, targetPercent) | |||
| }) | |||
| if (userPercent === 0) { | |||
| targetPercent *= 4 | |||
| } | |||
| if (targetPercent === 0) { | |||
| userPercent *= 4 | |||
| } | |||
| console.log(userPercent, targetPercent, this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)) | |||
| return this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| const delta: number = user.stats[this.stat] - target.stats[this.stat] | |||
| let result: string | |||
| if (delta === 0) { | |||
| result = 'You and the target have the same ' + this.stat + '.' | |||
| } else if (delta < 0) { | |||
| result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.' | |||
| } else { | |||
| result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.' | |||
| } | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%' | |||
| return new LogLines(result) | |||
| } | |||
| } | |||
| export class StatTest extends RandomTest { | |||
| private f: (x: number) => number | |||
| constructor (public readonly stat: Stat, k = 0.1) { | |||
| super() | |||
| this.f = logistic(0, 1, k) | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.f(user.stats[this.stat] - target.stats[this.stat]) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| const delta: number = user.stats[this.stat] - target.stats[this.stat] | |||
| let result: string | |||
| if (delta === 0) { | |||
| result = 'You and the target have the same ' + this.stat + '.' | |||
| } else if (delta < 0) { | |||
| result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.' | |||
| } else { | |||
| result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.' | |||
| } | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%' | |||
| return new LogLines(result) | |||
| } | |||
| } | |||
| export class ChanceTest extends RandomTest { | |||
| constructor (public readonly chance: number) { | |||
| super() | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.chance | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.') | |||
| } | |||
| } | |||
| export class Damage { | |||
| readonly damages: DamageInstance[] | |||
| @@ -217,7 +109,7 @@ export class Damage { | |||
| totals[instance.target] += instance.amount | |||
| }) | |||
| return new FormatEntry(new LogLine(...Object.keys(Vigor).flatMap(key => totals[key as Vigor] === 0 ? [] : [totals[key as Vigor].toFixed(0).toString(), new FAElem(VigorIcons[key as Vigor])])), FormatOpt.DamageInst) | |||
| return new FormatEntry(new LogLine(...Object.keys(Vigor).flatMap(key => totals[key as Vigor] === 0 ? [] : [new PropElem(key as Vigor, totals[key as Vigor]), ' '])), FormatOpt.DamageInst) | |||
| } | |||
| } | |||
| @@ -0,0 +1,133 @@ | |||
| import { CombatTest, Stat, Vigor } from '../combat' | |||
| import { Creature } from '../entity' | |||
| import { LogEntry, LogLines, PropElem, LogLine } from '../interface' | |||
| function logistic (x0: number, L: number, k: number): (x: number) => number { | |||
| return (x: number) => { | |||
| return L / (1 + Math.exp(-k * (x - x0))) | |||
| } | |||
| } | |||
| abstract class RandomTest implements CombatTest { | |||
| test (user: Creature, target: Creature): boolean { | |||
| return Math.random() < this.odds(user, target) | |||
| } | |||
| abstract odds(user: Creature, target: Creature): number | |||
| abstract explain(user: Creature, target: Creature): LogEntry | |||
| } | |||
| export class StatVigorTest extends RandomTest { | |||
| private f: (x: number) => number | |||
| constructor (public readonly stat: Stat, k = 0.1) { | |||
| super() | |||
| this.f = logistic(0, 1, k) | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| let userPercent = 1 | |||
| let targetPercent = 1 | |||
| Object.keys(Vigor).forEach(key => { | |||
| userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor] | |||
| targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor] | |||
| userPercent = Math.max(0, userPercent) | |||
| targetPercent = Math.max(0, targetPercent) | |||
| }) | |||
| if (userPercent === 0) { | |||
| targetPercent *= 4 | |||
| } | |||
| if (targetPercent === 0) { | |||
| userPercent *= 4 | |||
| } | |||
| console.log(userPercent, targetPercent, this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)) | |||
| return this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| let result: LogEntry | |||
| let userPercent = 1 | |||
| let targetPercent = 1 | |||
| Object.keys(Vigor).forEach(key => { | |||
| userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor] | |||
| targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor] | |||
| userPercent = Math.max(0, userPercent) | |||
| targetPercent = Math.max(0, targetPercent) | |||
| }) | |||
| if (userPercent === 0) { | |||
| targetPercent *= 4 | |||
| } | |||
| if (targetPercent === 0) { | |||
| userPercent *= 4 | |||
| } | |||
| const userMod = user.stats[this.stat] * userPercent | |||
| const targetMod = target.stats[this.stat] * targetPercent | |||
| const delta = (userMod - targetMod) | |||
| if (delta === 0) { | |||
| result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.') | |||
| } else if (delta < 0) { | |||
| result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.') | |||
| } else { | |||
| result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.') | |||
| } | |||
| result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%') | |||
| return result | |||
| } | |||
| } | |||
| export class StatTest extends RandomTest { | |||
| private f: (x: number) => number | |||
| constructor (public readonly stat: Stat, k = 0.1) { | |||
| super() | |||
| this.f = logistic(0, 1, k) | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.f(user.stats[this.stat] - target.stats[this.stat]) | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| const delta: number = user.stats[this.stat] - target.stats[this.stat] | |||
| let result: LogEntry | |||
| if (delta === 0) { | |||
| result = new LogLine('You and the target have the same ', new PropElem(this.stat), '.') | |||
| } else if (delta < 0) { | |||
| result = new LogLine('You have ', new PropElem(this.stat, -delta), ' less than your foe.') | |||
| } else { | |||
| result = new LogLine('You have ', new PropElem(this.stat, delta), ' more than you foe.') | |||
| } | |||
| result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%') | |||
| return result | |||
| } | |||
| } | |||
| export class ChanceTest extends RandomTest { | |||
| constructor (public readonly chance: number) { | |||
| super() | |||
| } | |||
| odds (user: Creature, target: Creature): number { | |||
| return this.chance | |||
| } | |||
| explain (user: Creature, target: Creature): LogEntry { | |||
| return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.') | |||
| } | |||
| } | |||
| @@ -1,9 +1,8 @@ | |||
| import { Creature, POV, Entity } from '../entity' | |||
| import { Stat, Damage, DamageType, TransferAction, Vigor, StatTest, FeedAction, DigestAction, EatenAction, AttackAction, DamageFormula, ConstantDamageFormula } from '../combat' | |||
| import { Stat, Damage, DamageType, TransferAction, Vigor, FeedAction, EatenAction, AttackAction, ConstantDamageFormula } from '../combat' | |||
| import { ProperNoun, TheyPronouns, ImproperNoun, POVPair, FemalePronouns, POVPairArgs } from '../language' | |||
| import { VoreType, Stomach, InnerStomach, Container, Bowels } from '../vore' | |||
| import { VoreType, Stomach, InnerStomach, Container } from '../vore' | |||
| import { LogLine, LogLines, LogEntry, FAElem, CompositeLog, ImgElem } from '../interface' | |||
| import { Wolf } from '../creatures' | |||
| class BellyCrushAction extends AttackAction { | |||
| successLines = new POVPairArgs<Entity, Entity, { damage: Damage }>([ | |||
| @@ -23,8 +22,8 @@ class BellyCrushAction extends AttackAction { | |||
| constructor (_damage: Damage) { | |||
| super({ | |||
| calc (user, target) { return _damage.scale(user.bulk / 25) }, | |||
| describe (user, target) { return new LogLine('Deal ', _damage.scale(user.bulk / 25).renderShort(), ` with your ${user.bulk} `, new FAElem('fas fa-weight-hanging')) } | |||
| calc (user) { return _damage.scale(user.bulk / 25) }, | |||
| describe (user) { return new LogLine('Deal ', _damage.scale(user.bulk / 25).renderShort(), ` with your ${user.bulk} `, new FAElem('fas fa-weight-hanging')) } | |||
| }) | |||
| this.name = 'Belly Crush' | |||
| this.desc = 'Use your weight!' | |||
| @@ -60,7 +59,7 @@ class BelchAction extends AttackAction { | |||
| class CrushAction extends EatenAction { | |||
| lines: POVPair<Entity, Entity> = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLine(`You crush ${target.name} `, new FAElem('fas fa-skull'))], | |||
| [[POV.Third, POV.First], (user, target) => new CompositeLog(new LogLine(`${user.name.capital} crushes you; ${user.pronouns.subjective} ${user.pronouns.isPlural ? 'belch' : 'belches'} as ${user.pronouns.possessive} gut lets out a fatal CRUNCH `, new FAElem('fas fa-skull')), new ImgElem('./media/cafat/images/crunch.webp'))], | |||
| [[POV.Third, POV.First], (user) => new CompositeLog(new LogLine(`${user.name.capital} crushes you; ${user.pronouns.subjective} ${user.pronouns.isPlural ? 'belch' : 'belches'} as ${user.pronouns.possessive} gut lets out a fatal CRUNCH `, new FAElem('fas fa-skull')), new ImgElem('./media/cafat/images/crunch.webp'))], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLine(`${user.name.capital} crushes ${target.name}; ${user.pronouns.subjective} ${user.pronouns.isPlural ? 'belch' : 'belches'} as ${user.pronouns.possessive} gut lets out a fatal CRUNCH `, new FAElem('fas fa-skull'))] | |||
| ]) | |||
| @@ -120,7 +119,7 @@ export class Cafat extends Creature { | |||
| stomach.consumeLines = new POVPair([ | |||
| [[POV.First, POV.Third], (user, target) => new LogLines(`You devour ${target.name}`)], | |||
| [[POV.Third, POV.First], (user, target) => new CompositeLog(new LogLines(`${user.name.capital} devours you`), new ImgElem('./media/cafat/images/stomach.webp'))], | |||
| [[POV.Third, POV.First], (user) => new CompositeLog(new LogLines(`${user.name.capital} devours you`), new ImgElem('./media/cafat/images/stomach.webp'))], | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} munches ${target.name.capital}`)] | |||
| ]) | |||
| const crush = new CrushAction(lowerStomach) | |||
| @@ -1,8 +1,9 @@ | |||
| import { Creature, POV, Entity } from '../entity' | |||
| import { Stat, Damage, DamageType, AttackAction, TransferAction, Vigor, StatTest, FeedAction, ConstantDamageFormula } from '../combat' | |||
| import { Stat, Damage, DamageType, AttackAction, TransferAction, Vigor, FeedAction, ConstantDamageFormula } from '../combat' | |||
| import { MalePronouns, ImproperNoun, POVPair, POVPairArgs } from '../language' | |||
| import { LogLine, LogLines } from '../interface' | |||
| import { VoreType, Stomach, Bowels } from '../vore' | |||
| import { StatTest } from '../combat/tests' | |||
| class BiteAction extends AttackAction { | |||
| constructor () { | |||
| @@ -95,7 +95,7 @@ export class FAElem implements LogEntry { | |||
| } | |||
| export class PropElem implements LogEntry { | |||
| constructor (private value: number, private prop: Stat | Vigor) { | |||
| constructor (private prop: Stat | Vigor, private value: number|null = null) { | |||
| } | |||
| @@ -114,12 +114,18 @@ export class PropElem implements LogEntry { | |||
| const span = document.createElement("span") | |||
| span.classList.add("stat-entry") | |||
| span.textContent = this.value.toString() | |||
| if (this.value !== null) { | |||
| const numText = Math.round(this.value).toFixed(0) === this.value.toFixed(0) ? this.value.toFixed(0) : this.value.toFixed(1) | |||
| span.textContent = numText + ' ' | |||
| } | |||
| new FAElem(cls).render().forEach(elem => { | |||
| span.appendChild(elem) | |||
| }) | |||
| span.dataset.tooltip = this.prop | |||
| span.dataset["tooltip-full"] = this.prop | |||
| span.dataset.tooltipFull = this.prop | |||
| return [span] | |||
| } | |||