zenno: stamina calculator first pass
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import * as course from '$lib/course';
|
||||
import * as race from '$lib/race';
|
||||
import * as recovery from '$lib/racelib/recovery';
|
||||
import HPChart from './HPChart.svelte';
|
||||
import InfoPanels from './InfoPanels.svelte';
|
||||
|
||||
let rawStats = $state({
|
||||
[race.Stat.Speed]: 1200,
|
||||
@@ -23,6 +26,19 @@
|
||||
[race.Stat.Wit]: 0,
|
||||
});
|
||||
|
||||
const recovRates = [35, 50, 150, 350, 550, 750, 950];
|
||||
const debuffRates = [-25, -50, -100, -200, -300];
|
||||
const selfDebuffRates = [-200, -400];
|
||||
let recovCounts = $state(recovRates.map((v) => ({ r: v, n: 0 })));
|
||||
let debuffCounts = $state(debuffRates.map((v) => ({ r: v, n: 0 })));
|
||||
let selfDebuffCounts = $state(selfDebuffRates.map((v) => ({ r: v, n: 0 })));
|
||||
let risky = $state(false);
|
||||
const skills = $derived([...recovCounts, ...selfDebuffCounts].filter(({ n }) => n > 0));
|
||||
|
||||
function rate(r: number): string {
|
||||
return `${r / 100}%`;
|
||||
}
|
||||
|
||||
let raceLen = $state(2000);
|
||||
let slopesRaw: course.Slope[] = $state([]);
|
||||
let surface = $state(race.Surface.Turf);
|
||||
@@ -99,6 +115,23 @@
|
||||
|
||||
const maxHP = $derived(race.maxHP(raceLen, style, stats[race.Stat.Stamina]));
|
||||
|
||||
const baseDistribution = $derived(recovery.distribution(baseStats[race.Stat.Wit], skills, debuffCounts));
|
||||
const distribution = $derived(risky ? recovery.withRisky(baseStats[race.Stat.Wit], baseDistribution) : baseDistribution);
|
||||
const statistics = $derived.by(() => {
|
||||
let mp = 0,
|
||||
mr = 0,
|
||||
mean = 0;
|
||||
for (const [r, p] of distribution) {
|
||||
mean += (r * p * maxHP) / 10000;
|
||||
if (p > mp) {
|
||||
mp = p;
|
||||
mr = r;
|
||||
}
|
||||
}
|
||||
return { mode: (mr * maxHP) / 10000, modePer: mp * 100, mean };
|
||||
});
|
||||
const effStam = $derived(race.effectiveStam(raceLen, style, maxHP + statistics.mode));
|
||||
|
||||
const sections = $derived.by(() => {
|
||||
const r = course.splitSectionSlopes(raceLen, slopesRaw).map((s) => {
|
||||
const uphillMod = s.slope > 0 ? race.uphillMod(stats[race.Stat.Power], s.slope) : 0;
|
||||
@@ -129,7 +162,6 @@
|
||||
),
|
||||
);
|
||||
const needHP = $derived(race.fullSpurtHP(raceLen, phaseSpeed[race.Phase.LateRace][0], spurtHPRate));
|
||||
const needHPPer = $derived((100 * needHP) / maxHP);
|
||||
const spurtStartHP = $derived(sections.findLast((s) => s.phase !== race.Phase.LateRace)?.remain);
|
||||
|
||||
let slopeAddParams: course.Slope = $state({ per: 1, span: [0, 0] });
|
||||
@@ -153,15 +185,15 @@
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Stamina Calculator</h1>
|
||||
<div class="mx-auto mt-8 grid max-w-5xl grid-cols-1 rounded-md text-center shadow-md ring md:grid-cols-6">
|
||||
<div class="mx-auto mt-8 grid max-w-5xl grid-cols-1 gap-2 rounded-md p-2 text-center shadow-md ring md:grid-cols-6">
|
||||
{#each race.StatList as stat (stat)}
|
||||
{@const statName = race.Stat[stat]}
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for={statName}>{statName}</label>
|
||||
<input type="number" class="w-full" id={statName} required bind:value={rawStats[stat]} />
|
||||
</div>
|
||||
{/each}
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for="mood">Mood</label>
|
||||
<select class="w-full" id="mood" required bind:value={mood}>
|
||||
{#each race.MOODS as mood (mood)}
|
||||
@@ -169,7 +201,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for="style">Style</label>
|
||||
<select class="w-full" id="style" required bind:value={style}>
|
||||
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||
@@ -177,7 +209,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for="distanceApt">{race.Distance[distType]} Aptitude</label>
|
||||
<select class="w-full" id="distanceApt" required bind:value={distanceApt}>
|
||||
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||
@@ -185,7 +217,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for="surfaceApt">{race.Surface[surface]} Aptitude</label>
|
||||
<select class="w-full" id="surfaceApt" required bind:value={surfaceApt}>
|
||||
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||
@@ -193,7 +225,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for="styleApt"
|
||||
>{race.RUNNING_STYLES[style != race.RunningStyle.Runaway ? style : race.RunningStyle.FrontRunner][0]}</label
|
||||
>
|
||||
@@ -203,7 +235,7 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4 self-center">
|
||||
<div class=" self-center">
|
||||
<label for="isCareer" class="mr-1 align-middle">In Career</label>
|
||||
<input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" />
|
||||
</div>
|
||||
@@ -214,13 +246,41 @@
|
||||
</div>
|
||||
{#each race.StatList as stat (stat)}
|
||||
{@const statName = race.Stat[stat]}
|
||||
<div class="m-4">
|
||||
<div class="">
|
||||
<label for={`${statName}Bonus`}>{statName}</label>
|
||||
<input type="number" class="w-full" id={`${statName}Bonus`} required bind:value={skillStatBonus[stat]} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mx-auto mt-8 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6">
|
||||
<div class="mx-auto my-2 grid max-w-4xl grid-cols-2 rounded-md text-center shadow-md ring md:grid-cols-7">
|
||||
<div class="col-span-full mt-1 text-lg">Recoveries</div>
|
||||
{#each recovCounts as s (s.r)}
|
||||
<div class="grid grid-cols-3 p-2 md:grid-cols-1 md:flex-col">
|
||||
<label for={`recov${s.r}`}>{rate(s.r)}</label>
|
||||
<input id={`recov${s.r}`} type="number" min="0" bind:value={s.n} class="col-span-2" />
|
||||
</div>
|
||||
{/each}
|
||||
<div class="col-span-full border-t pt-1 text-lg md:col-span-2">Self-Damage</div>
|
||||
<div class="col-span-full border-t border-l pt-1 text-lg md:col-span-5">Debuffs (No Wit Check)</div>
|
||||
{#each selfDebuffCounts as s (s.r)}
|
||||
<div class="grid grid-cols-3 p-2 md:grid-cols-1 md:flex-col">
|
||||
<label for={`selfdamage${s.r}`}>{rate(s.r)}</label>
|
||||
<input id={`selfdamage${s.r}`} type="number" min="0" bind:value={s.n} />
|
||||
</div>
|
||||
{/each}
|
||||
{#each debuffCounts as s, i (s.r)}
|
||||
<div class={{ 'grid grid-cols-3 p-2 md:grid-cols-1 md:flex-col': true, 'border-l': i === 0 }}>
|
||||
<label for={`debuff${s.r}`}>{rate(s.r)}</label>
|
||||
<input id={`debuff${s.r}`} type="number" min="0" bind:value={s.n} />
|
||||
</div>
|
||||
{/each}
|
||||
<div class="col-span-2 flex flex-row py-2">
|
||||
<label for="risky" class="mr-1 ml-auto">Risky Business</label>
|
||||
<input id="risky" type="checkbox" bind:checked={risky} class="mr-auto ml-1 h-6 w-6" />
|
||||
</div>
|
||||
<div class="border-l"><!-- only here for the border --></div>
|
||||
</div>
|
||||
<div class="mx-auto my-2 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6">
|
||||
<div class="m-4 flex md:col-span-2">
|
||||
<input
|
||||
class="col-span-2 my-auto w-full justify-self-center-safe"
|
||||
@@ -330,22 +390,16 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto mt-8 flex max-w-3xl">
|
||||
<div class="m-2 flex-1 rounded-md border text-center shadow-sm transition-shadow hover:shadow-md">
|
||||
<span class="block text-lg">Max HP</span>
|
||||
<span class="block text-2xl">{Math.floor(maxHP)}</span>
|
||||
</div>
|
||||
<div class="m-2 flex-1 rounded-md border text-center shadow-sm transition-shadow hover:shadow-md">
|
||||
<span class="block text-lg">Spurt Threshold</span>
|
||||
<span class="block text-2xl">{Math.ceil(needHP)} ({needHPPer.toFixed(1)}%)</span>
|
||||
</div>
|
||||
<div class="m-2 flex-1 rounded-md border text-center shadow-sm transition-shadow hover:shadow-md">
|
||||
<span class="block text-lg">Late Race Start</span>
|
||||
{#if spurtStartHP != null}
|
||||
<span class="block text-2xl">{Math.ceil(spurtStartHP[1])} – {Math.ceil(spurtStartHP[0])}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<InfoPanels
|
||||
{maxHP}
|
||||
{needHP}
|
||||
{spurtStartHP}
|
||||
{effStam}
|
||||
showStats={distribution.size > 1}
|
||||
mode={statistics.mode}
|
||||
modePer={statistics.modePer}
|
||||
mean={statistics.mean}
|
||||
/>
|
||||
<!--
|
||||
<table class="mt-8 w-full border text-center">
|
||||
<thead>
|
||||
@@ -393,3 +447,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
-->
|
||||
{#if skills.length != 0 || risky}
|
||||
<div class="mx-auto h-96 max-w-5xl">
|
||||
<HPChart {distribution} {maxHP} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import * as Plot from '@observablehq/plot';
|
||||
import type { Attachment } from 'svelte/attachments';
|
||||
|
||||
interface Props {
|
||||
distribution: Map<number, number>;
|
||||
maxHP: number;
|
||||
}
|
||||
|
||||
let { distribution, maxHP }: Props = $props();
|
||||
const vals = $derived([...distribution.entries()].map(([r, p]) => ({ r: (r * maxHP) / 1e4, p: p * 100 })));
|
||||
|
||||
let width = $state(1);
|
||||
let height = $state(1);
|
||||
const makeChart: Attachment = (el) => {
|
||||
$effect(() => {
|
||||
el?.firstChild?.remove();
|
||||
el?.append(
|
||||
Plot.plot({
|
||||
width,
|
||||
height,
|
||||
x: {
|
||||
label: 'HP Mod',
|
||||
line: true,
|
||||
},
|
||||
y: {
|
||||
label: 'Probability (%)',
|
||||
domain: [0, 100],
|
||||
line: true,
|
||||
},
|
||||
marks: [
|
||||
Plot.ruleX([0], { strokeOpacity: 0.5 }),
|
||||
Plot.link(vals, { x: 'r', y1: 0, y2: 'p', markerEnd: 'circle-stroke', strokeWidth: 3 }),
|
||||
Plot.tip(vals, Plot.pointerX({ x: 'r', y: 'p', className: 'plot-tip' })),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div bind:clientWidth={width} bind:clientHeight={height} class="h-full w-full">
|
||||
<div role="img" {@attach makeChart}>
|
||||
<span>the chart seems to have didn't</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.plot-tip) {
|
||||
--plot-background: light-dark(var(--color-mist-200), var(--color-mist-800));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import CalcInfo from '../CalcInfo.svelte';
|
||||
|
||||
interface Props {
|
||||
maxHP: number;
|
||||
needHP: number;
|
||||
effStam: number;
|
||||
spurtStartHP?: number[];
|
||||
showStats: boolean;
|
||||
mode: number;
|
||||
modePer: number;
|
||||
mean: number;
|
||||
}
|
||||
|
||||
let { maxHP, needHP, effStam, spurtStartHP = [0, 0], showStats, mode, modePer, mean }: Props = $props();
|
||||
|
||||
const needHPPer = $derived((100 * needHP) / maxHP);
|
||||
const white = $derived(Math.trunc(maxHP * 0.015));
|
||||
const gold = $derived(Math.trunc(maxHP * 0.055));
|
||||
const goldDebuff = $derived(Math.trunc(maxHP * -0.03));
|
||||
</script>
|
||||
|
||||
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||
<CalcInfo title="Max HP" class="max-w-72 flex-1">{Math.floor(maxHP)}</CalcInfo>
|
||||
<CalcInfo title="Spurt Threshold" class="max-w-72 flex-1">{Math.ceil(needHP)} ({needHPPer.toFixed(1)}%)</CalcInfo>
|
||||
<CalcInfo title="Spurt Start HP" class="hidden max-w-72 flex-1">
|
||||
{Math.ceil(spurtStartHP[1])} – {Math.ceil(spurtStartHP[0])}
|
||||
</CalcInfo>
|
||||
</div>
|
||||
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||
<CalcInfo title="White Recovery" class="max-w-72 flex-1">+{white} HP</CalcInfo>
|
||||
<CalcInfo title="Gold Recovery" class="max-w-72 flex-1">+{gold} HP</CalcInfo>
|
||||
<CalcInfo title="Gold Debuff" class="max-w-72 flex-1">{goldDebuff} HP</CalcInfo>
|
||||
</div>
|
||||
{#if showStats}
|
||||
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||
<CalcInfo title="Mode HP from Skills" class="max-w-72 flex-1">{Math.round(mode)} (P={modePer.toFixed(1)}%)</CalcInfo>
|
||||
<CalcInfo title="Mean HP from Skills" class="max-w-72 flex-1">{Math.round(mean)}</CalcInfo>
|
||||
<CalcInfo title="Effective Stamina" class="max-w-72 flex-1">{Math.ceil(effStam)}</CalcInfo>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user