zenno/doc/fans: new data coverage and fix off-by-ones
This commit is contained in:
@@ -1,17 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { asset, resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import CalcInfo from '../../race/CalcInfo.svelte';
|
import CalcInfo from '../../race/CalcInfo.svelte';
|
||||||
import Article from '../Article.svelte';
|
import Article from '../Article.svelte';
|
||||||
import Sec from '../Sec.svelte';
|
import Sec from '../Sec.svelte';
|
||||||
import * as fans from './fans';
|
import * as fans from './fans';
|
||||||
|
|
||||||
let baseFans = $state(700);
|
let winFans = $state(700);
|
||||||
let fanBonus = $state(0);
|
let fanBonus = $state(0);
|
||||||
let finishOrder = $state(1);
|
let finishOrder = $state(1);
|
||||||
let margin = $state(0);
|
let margin = $state(0);
|
||||||
let skills = $state(0);
|
let skills = $state(0);
|
||||||
let glowsticks = $state(false);
|
let glowsticks = $state(false);
|
||||||
|
|
||||||
|
const baseFans = $derived(fans.baseFans(winFans, finishOrder));
|
||||||
const marginFans = $derived(fans.marginFans(baseFans, finishOrder, margin));
|
const marginFans = $derived(fans.marginFans(baseFans, finishOrder, margin));
|
||||||
const skillFans = $derived(fans.skillFans(baseFans, skills));
|
const skillFans = $derived(fans.skillFans(baseFans, skills));
|
||||||
const glowstickBonus = $derived(glowsticks ? 50 : 0);
|
const glowstickBonus = $derived(glowsticks ? 50 : 0);
|
||||||
@@ -76,18 +77,21 @@
|
|||||||
<li>In Trackblazer/MANT, use of glowsticks.</li>
|
<li>In Trackblazer/MANT, use of glowsticks.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
The identified formula perfectly fits hundreds of recorded career race gains, including losses. Some questions remain, but
|
The identified formula perfectly fits hundreds of recorded career races, including losses. These results have important
|
||||||
the results so far have important implications both for fan runs and for low fan bonus decks for which debut can fall short
|
implications both for fan runs and for low fan bonus decks for which debut can fall short of fan requirements for early
|
||||||
of fan requirements for early career goals.
|
career goals.
|
||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<Sec h={2} id="calc-fans">Calculate Fans</Sec>
|
<Sec h={2} id="calc-fans">Calculate Fans</Sec>
|
||||||
<p>This tool will calculate your exact fan gain from a race.</p>
|
<p>
|
||||||
|
This tool will calculate your exact fan gain from a race. Use the fan value displayed in the race selection menu for the Race
|
||||||
|
Fans field.
|
||||||
|
</p>
|
||||||
<form class="mx-auto mb-4 grid grid-cols-1 gap-2 rounded-md p-4 text-center shadow-md ring md:max-w-2xl md:grid-cols-3">
|
<form class="mx-auto mb-4 grid grid-cols-1 gap-2 rounded-md p-4 text-center shadow-md ring md:max-w-2xl md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-base-fans">Base Fans</label>
|
<label for="calc-base-fans">Race Fans</label>
|
||||||
<input id="calc-base-fans" class="max-w-32" type="number" min="0" max="30000" step="100" bind:value={baseFans} />
|
<input id="calc-base-fans" class="max-w-32" type="number" min="0" max="30000" step="100" bind:value={winFans} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-fan-bonus">Fan Bonus</label>
|
<label for="calc-fan-bonus">Fan Bonus</label>
|
||||||
@@ -95,7 +99,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-finish-order">Finish Order</label>
|
<label for="calc-finish-order">Finish Order</label>
|
||||||
<input id="calc-finish-order" class="max-w-32" type="number" min="1" max="18" step="1" bind:value={finishOrder} />
|
<select id="calc-finish-order" class="max-w-32" bind:value={finishOrder}>
|
||||||
|
{#each orderPct as [v] (v)}
|
||||||
|
<option value={v}>{v}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-margin">{marginType} (L)</label>
|
<label for="calc-margin">{marginType} (L)</label>
|
||||||
@@ -110,17 +118,18 @@
|
|||||||
<label for="calc-glowsticks">Glowsticks</label>
|
<label for="calc-glowsticks">Glowsticks</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-12">
|
<div class="mx-auto grid max-w-2xl grid-cols-1 md:grid-cols-3">
|
||||||
<CalcInfo title="Fans from Margin" class="md:col-span-4 md:col-start-3">{marginFans}</CalcInfo>
|
<CalcInfo title="Base Fans">{baseFans}</CalcInfo>
|
||||||
<CalcInfo title="Fans from Skills" class="md:col-span-4">{skillFans}</CalcInfo>
|
<CalcInfo title="Fans from Margin">{marginFans}</CalcInfo>
|
||||||
<CalcInfo title="Fans Gained" class="md:col-span-4 md:col-start-5">{gainedFans}</CalcInfo>
|
<CalcInfo title="Fans from Skills">{skillFans}</CalcInfo>
|
||||||
|
<CalcInfo title="Fans Gained" class="md:col-start-2">{gainedFans}</CalcInfo>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={2} id="base-fans">Base Fans</Sec>
|
<Sec h={2} id="base-fans">Base Fans</Sec>
|
||||||
<p>
|
<p>
|
||||||
Base fans is a parameter per race and finish placement, defined in <a href={resolve('/doc/mdb')}>master data</a>. The number
|
Base fans is a parameter per race and finish placement, defined in <a href={resolve('/doc/mdb')}>master data</a>. The number
|
||||||
shown in the career race selection menu is the base fans for first place. While it is defined separately for each race and
|
shown in the career race selection menu is the base fans for first place. While it is defined separately for each race and
|
||||||
finish order, every race currently follows the same curve for all placements.
|
finish order, every race currently follows the same curve for all placements, up to rounding.
|
||||||
</p>
|
</p>
|
||||||
<table class="mx-auto mb-4 border text-center">
|
<table class="mx-auto mb-4 border text-center">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -140,14 +149,10 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<Sec h={2} id="margin">Fans from Margin</Sec>
|
<Sec h={2} id="margin">Fans from Margin</Sec>
|
||||||
<p>
|
<p>When the trainee places in the top five, she receives a bonus percentage of base fans according to the finish margin.</p>
|
||||||
When the trainee places in the top three (top five?), she receives a bonus percentage of base fans according to the finish
|
|
||||||
margin.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
The bonus depends on whether the trainee won the race. If so, it is calculated by her margin versus second place. Otherwise,
|
The bonus depends on whether the trainee won the race. If so, it is calculated by her margin versus second place. Otherwise,
|
||||||
her total margin behind the winner is used. If she did not place in the top three (top five?), then she gains no fans from
|
her total margin behind the winner is used. If she did not place in the top five, then she gains no fans from margin.
|
||||||
margin.
|
|
||||||
</p>
|
</p>
|
||||||
<table class="mx-auto mb-4 border text-center">
|
<table class="mx-auto mb-4 border text-center">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -190,34 +195,44 @@
|
|||||||
<Sec h={2} id="rounding">Calculation Order and Rounding</Sec>
|
<Sec h={2} id="rounding">Calculation Order and Rounding</Sec>
|
||||||
<p>The calculation is perfomed as if in the following order.</p>
|
<p>The calculation is perfomed as if in the following order.</p>
|
||||||
<ol class="mb-4 list-decimal pl-4">
|
<ol class="mb-4 list-decimal pl-4">
|
||||||
<li>Calculate fans from win margin and round down.</li>
|
<li>Look up base fans for the race and placement. This value is always an integer.</li>
|
||||||
<li>Calculate fans from skill activation count and round down.</li>
|
<li>Calculate fans from win margin as a proportion of base fans and round down.</li>
|
||||||
|
<li>Calculate fans from skill activation count as a proportion of base fans and round down.</li>
|
||||||
<li>Calculate the sum of base fans, win margin fans, and skill activation fans.</li>
|
<li>Calculate the sum of base fans, win margin fans, and skill activation fans.</li>
|
||||||
<li>Apply fan bonus to the sum and round to the nearest integer with halfway cases away from zero.</li>
|
<li>Apply fan bonus to the sum in single-precision floating point, without otherwise rounding.</li>
|
||||||
|
<li>
|
||||||
|
Round to five significant figures with halfway cases rounded up, then round down. Examples are useful here.
|
||||||
|
<ul class="list-[revert] pl-8">
|
||||||
|
<li>
|
||||||
|
700 base plus 14 from margin with 88% fan bonus = <span class="inline-block">714×1.88</span> = 1342.32, rounded to five
|
||||||
|
significant figures becomes 1342.3, rounded down to 1342.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
10500 base plus 210 from margin with 105% fan bonus =
|
||||||
|
<span class="inline-block">10710×2.05</span>
|
||||||
|
= 21955.5, rounded to five significant figures becomes 21956, rounded down to 21956.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
100 base fans plus 2 fans from skills with 88% fan bonus = <span class="inline-block">102×1.88</span> = 191.76, rounded
|
||||||
|
to five significant figures becomes 191.76, rounded down to 191.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>(Trackblazer/MANT) If glowsticks were used for the race, apply their multiplier and round down.</li>
|
<li>(Trackblazer/MANT) If glowsticks were used for the race, apply their multiplier and round down.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<Sec h={2} id="data">Data and Limitations</Sec>
|
<Sec h={2} id="data">Data</Sec>
|
||||||
<p>
|
<p>
|
||||||
The data for this research comes from 193 races and fan results dumped using <a
|
The data for this research comes from 325 races and fan results dumped using <a
|
||||||
href="https://werseter.github.io/umadump/"
|
href="https://werseter.github.io/umadump/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">Umadump</a
|
rel="noopener noreferrer">Umadump</a
|
||||||
>. The races come from sixteen careers run between 19 Aug and 11 Oct 2026. These careers include five different values of race
|
>. The races come from careers run between 19 Aug and 18 Oct 2026. These careers include fan bonus values of 50, 55, 65, 70,
|
||||||
bonus ranging from 55 to 105. The identified formula predicts the actual fan gain perfectly for all of the races. You can
|
80, 88, and 105. The identified formula predicts the actual fan gain perfectly for all of the races. You can
|
||||||
<a
|
<a
|
||||||
href="https://docs.google.com/spreadsheets/d/1_jtOv6qk9dP4jJTagJqG53Pvj48IVd6gl2zKpyT6Kvs/edit?gid=463816108#gid=463816108"
|
href="https://docs.google.com/spreadsheets/d/1_jtOv6qk9dP4jJTagJqG53Pvj48IVd6gl2zKpyT6Kvs/edit?gid=463816108#gid=463816108"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">view the data</a
|
rel="noopener noreferrer">view the data</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
There are some gaps in the data. No collected race had a finish placement of 4th or 5th, nor lower than 6th, so the exact
|
|
||||||
details of the win margin fan bonus remain open. Similarly, no race featured a trainee who activated more than ten skills, so
|
|
||||||
it is unknown whether there are further tiers of skill activation count fan bonus.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Once these edge cases are covered, this article will be updated. If you'd like to contribute, please contact me
|
|
||||||
(@zephyrtronium) via Discord on the Hakuraku, uma.moe, GameTora, or EoG servers.
|
|
||||||
</p>
|
|
||||||
</Article>
|
</Article>
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
const placementPms = [1000, 400, 250, 150, 100, 90, 80, 70, 60, 50, 40, 30, 25, 20, 15, 10, 10, 10] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate base fans for a given placement in a race.
|
||||||
|
* @param winBase Base fans for a win, as displayed in the race menu
|
||||||
|
* @param finishOrder Placement of the trainee, with winner being 1
|
||||||
|
* @returns Base fans for the race and placement together
|
||||||
|
*/
|
||||||
|
export function baseFans(winBase: number, finishOrder: number): number {
|
||||||
|
return Math.round(winBase * placementPms[finishOrder - 1] / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
const margins = [
|
const margins = [
|
||||||
[0.75, 0.0, 0.09],
|
[0.75, 0.0, 0.09],
|
||||||
[2.5, 0.02, 0.06],
|
[2.5, 0.02, 0.06],
|
||||||
@@ -9,15 +21,15 @@ const margins = [
|
|||||||
/**
|
/**
|
||||||
* Calculate fans gained from a career race due to the margin of the finish.
|
* Calculate fans gained from a career race due to the margin of the finish.
|
||||||
* @param baseFans Base fans from the race and finish order
|
* @param baseFans Base fans from the race and finish order
|
||||||
* @param finishOrder Placement of the trainee
|
* @param finishOrder Placement of the trainee, with winner being 1
|
||||||
* @param margin Margin in lengths between the trainee and second if a win, or the winner and trainee if a loss
|
* @param margin Margin in lengths between the trainee and second if a win, or the winner and trainee if a loss
|
||||||
* @returns Fans gained due to finish margin
|
* @returns Fans gained due to finish margin
|
||||||
*/
|
*/
|
||||||
export function marginFans(baseFans: number, finishOrder: number, margin: number): number {
|
export function marginFans(baseFans: number, finishOrder: number, margin: number): number {
|
||||||
if (finishOrder > 3) {
|
if (finishOrder > 5) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const [, wb, lb] = margins.find(([m]) => margin < m)!;
|
const [, wb, lb] = margins.find(([m]) => margin <= m)!;
|
||||||
const b = finishOrder === 1 ? wb : lb;
|
const b = finishOrder === 1 ? wb : lb;
|
||||||
return Math.floor(baseFans * b);
|
return Math.floor(baseFans * b);
|
||||||
}
|
}
|
||||||
@@ -52,7 +64,10 @@ export function skillFans(baseFans: number, skillsActivated: number): number {
|
|||||||
*/
|
*/
|
||||||
export function gained(baseFans: number, marginFans: number, skillFans: number, fanBonus: number, itemBonus?: number): number {
|
export function gained(baseFans: number, marginFans: number, skillFans: number, fanBonus: number, itemBonus?: number): number {
|
||||||
const f = baseFans + marginFans + skillFans;
|
const f = baseFans + marginFans + skillFans;
|
||||||
const d = Math.round(f * (1 + fanBonus / 100));
|
const s = Math.fround(f * Math.fround(1 + fanBonus / 100));
|
||||||
|
const p = Math.exp((4 - Math.floor(Math.log10(s))) * Math.LN10);
|
||||||
|
// Add half a significant figure to account for precision.
|
||||||
|
const d = Math.floor(Math.round(s * p + 5e-6) / p + 5e-6);
|
||||||
const b = itemBonus ?? 0;
|
const b = itemBonus ?? 0;
|
||||||
return Math.floor(d * (1 + b / 100));
|
return Math.floor(d * (1 + b / 100));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user