| @@ -61,4 +61,55 @@ body, html { | |||||
| display: flex; | display: flex; | ||||
| flex-direction: column; | 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> | </style> | ||||
| @@ -85,52 +85,6 @@ a { | |||||
| justify-content: space-evenly; | justify-content: space-evenly; | ||||
| user-select: none; | 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> | ||||
| <style> | <style> | ||||
| @@ -1,7 +1,8 @@ | |||||
| import { Creature, POV, Entity } from './entity' | import { Creature, POV, Entity } from './entity' | ||||
| import { POVPair, POVPairArgs, TextLike, DynText, LiveText } from './language' | import { POVPair, POVPairArgs, TextLike, DynText, LiveText } from './language' | ||||
| import { Container } from './vore' | 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 { | export enum DamageType { | ||||
| Pierce = "Pierce", | Pierce = "Pierce", | ||||
| @@ -70,115 +71,6 @@ export interface CombatTest { | |||||
| explain: (user: Creature, target: Creature) => LogEntry; | 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 { | export class Damage { | ||||
| readonly damages: DamageInstance[] | readonly damages: DamageInstance[] | ||||
| @@ -217,7 +109,7 @@ export class Damage { | |||||
| totals[instance.target] += instance.amount | 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 { 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 { 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 { LogLine, LogLines, LogEntry, FAElem, CompositeLog, ImgElem } from '../interface' | ||||
| import { Wolf } from '../creatures' | |||||
| class BellyCrushAction extends AttackAction { | class BellyCrushAction extends AttackAction { | ||||
| successLines = new POVPairArgs<Entity, Entity, { damage: Damage }>([ | successLines = new POVPairArgs<Entity, Entity, { damage: Damage }>([ | ||||
| @@ -23,8 +22,8 @@ class BellyCrushAction extends AttackAction { | |||||
| constructor (_damage: Damage) { | constructor (_damage: Damage) { | ||||
| super({ | 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.name = 'Belly Crush' | ||||
| this.desc = 'Use your weight!' | this.desc = 'Use your weight!' | ||||
| @@ -60,7 +59,7 @@ class BelchAction extends AttackAction { | |||||
| class CrushAction extends EatenAction { | class CrushAction extends EatenAction { | ||||
| lines: POVPair<Entity, Entity> = new POVPair([ | lines: POVPair<Entity, Entity> = new POVPair([ | ||||
| [[POV.First, POV.Third], (user, target) => new LogLine(`You crush ${target.name} `, new FAElem('fas fa-skull'))], | [[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'))] | [[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([ | stomach.consumeLines = new POVPair([ | ||||
| [[POV.First, POV.Third], (user, target) => new LogLines(`You devour ${target.name}`)], | [[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}`)] | [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} munches ${target.name.capital}`)] | ||||
| ]) | ]) | ||||
| const crush = new CrushAction(lowerStomach) | const crush = new CrushAction(lowerStomach) | ||||
| @@ -1,8 +1,9 @@ | |||||
| import { Creature, POV, Entity } from '../entity' | 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 { MalePronouns, ImproperNoun, POVPair, POVPairArgs } from '../language' | ||||
| import { LogLine, LogLines } from '../interface' | import { LogLine, LogLines } from '../interface' | ||||
| import { VoreType, Stomach, Bowels } from '../vore' | import { VoreType, Stomach, Bowels } from '../vore' | ||||
| import { StatTest } from '../combat/tests' | |||||
| class BiteAction extends AttackAction { | class BiteAction extends AttackAction { | ||||
| constructor () { | constructor () { | ||||
| @@ -95,7 +95,7 @@ export class FAElem implements LogEntry { | |||||
| } | } | ||||
| export class PropElem 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") | const span = document.createElement("span") | ||||
| span.classList.add("stat-entry") | 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 => { | new FAElem(cls).render().forEach(elem => { | ||||
| span.appendChild(elem) | span.appendChild(elem) | ||||
| }) | }) | ||||
| span.dataset.tooltip = this.prop | span.dataset.tooltip = this.prop | ||||
| span.dataset["tooltip-full"] = this.prop | |||||
| span.dataset.tooltipFull = this.prop | |||||
| return [span] | return [span] | ||||
| } | } | ||||