zenno/mspeed: calculator for front runner mechanical speed bonuses
This commit is contained in:
@@ -2,23 +2,42 @@
|
|||||||
import * as Plot from '@observablehq/plot';
|
import * as Plot from '@observablehq/plot';
|
||||||
import * as d3 from 'd3';
|
import * as d3 from 'd3';
|
||||||
import { Stat } from './race';
|
import { Stat } from './race';
|
||||||
import type { ComputedSeries } from './chart';
|
import type { ComputedSeries, HorizontalRule } from './chart';
|
||||||
import type { ClassValue } from 'svelte/elements';
|
import type { ClassValue } from 'svelte/elements';
|
||||||
import type { Attachment } from 'svelte/attachments';
|
import type { Attachment } from 'svelte/attachments';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/** The stat the chart shows. */
|
||||||
stat: Stat;
|
stat: Stat;
|
||||||
|
/** Series to show in the chart. */
|
||||||
y: ComputedSeries | Array<ComputedSeries | null>;
|
y: ComputedSeries | Array<ComputedSeries | null>;
|
||||||
|
/** Label for the dependent variable. */
|
||||||
yLabel: string;
|
yLabel: string;
|
||||||
range?: [number, number];
|
/**
|
||||||
|
* Range of the dependent variable to show.
|
||||||
|
* If not given, the limits are the minimum and maximum values plotted.
|
||||||
|
* If given as a triple, the second value is the default maximum and
|
||||||
|
* the third is the maximum when the chart is expanded to 2000.
|
||||||
|
*/
|
||||||
|
range?: [number, number] | [number, number, number];
|
||||||
|
/** Range of the stat to plot. */
|
||||||
xRange?: [number, number] | null;
|
xRange?: [number, number] | null;
|
||||||
|
/**
|
||||||
|
* Vertical rules to place on the graph.
|
||||||
|
* Each rule gets a corresponding horizontal rule at the intersection
|
||||||
|
* with each series.
|
||||||
|
*/
|
||||||
xRule?: number | number[];
|
xRule?: number | number[];
|
||||||
|
/**
|
||||||
|
* Horizontal rules to place on the graph.
|
||||||
|
*/
|
||||||
|
yRule?: HorizontalRule[];
|
||||||
|
|
||||||
class?: ClassValue | null;
|
class?: ClassValue | null;
|
||||||
plotOptions?: Omit<Plot.PlotOptions, 'marks' | 'x' | 'y'>;
|
plotOptions?: Omit<Plot.PlotOptions, 'marks' | 'x' | 'y'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { stat, y, yLabel, range, xRange, xRule = [], class: className, plotOptions = {} }: Props = $props();
|
let { stat, y, yLabel, range, xRange, xRule = [], yRule = [], class: className, plotOptions = {} }: Props = $props();
|
||||||
|
|
||||||
let width = $state(0);
|
let width = $state(0);
|
||||||
let height = $state(0);
|
let height = $state(0);
|
||||||
@@ -48,8 +67,11 @@
|
|||||||
const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(x), label }))));
|
const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(x), label }))));
|
||||||
const yRange: [number, number] = $derived.by(() => {
|
const yRange: [number, number] = $derived.by(() => {
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
|
if (range.length === 2) {
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
return [range[0], expand ? range[2] : range[1]];
|
||||||
|
}
|
||||||
const l = d3.min(vals, ({ y }) => y) ?? 0;
|
const l = d3.min(vals, ({ y }) => y) ?? 0;
|
||||||
const r = d3.max(vals, ({ y }) => y) ?? 1;
|
const r = d3.max(vals, ({ y }) => y) ?? 1;
|
||||||
return [l, r];
|
return [l, r];
|
||||||
@@ -81,6 +103,8 @@
|
|||||||
Plot.ruleX([thrX], { strokeOpacity: 0.25 }),
|
Plot.ruleX([thrX], { strokeOpacity: 0.25 }),
|
||||||
Plot.ruleX(xLines, { strokeOpacity: 0.5 }),
|
Plot.ruleX(xLines, { strokeOpacity: 0.5 }),
|
||||||
Plot.ruleY(yLines, { y: 'y', stroke: 'label', strokeOpacity: 0.5 }),
|
Plot.ruleY(yLines, { y: 'y', stroke: 'label', strokeOpacity: 0.5 }),
|
||||||
|
Plot.ruleY(yRule, { y: 'y', strokeOpacity: 0.75 }),
|
||||||
|
Plot.tip(yRule, { x: xMax, y: 'y', title: 'label', anchor: 'top-right', className: 'plot-tip' }),
|
||||||
Plot.frame(),
|
Plot.frame(),
|
||||||
Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
|
Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
|
||||||
Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })),
|
Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })),
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ export interface ComputedSeries {
|
|||||||
y: (x: number) => number;
|
y: (x: number) => number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HorizontalRule {
|
||||||
|
y: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
import type { Uma } from './data/uma';
|
import type { Uma } from './data/uma';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fundamental stats of umas.
|
||||||
|
*/
|
||||||
export enum Stat {
|
export enum Stat {
|
||||||
Speed,
|
Speed,
|
||||||
Stamina,
|
Stamina,
|
||||||
@@ -11,8 +14,14 @@ export enum Stat {
|
|||||||
Wit,
|
Wit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats as a list for easy iteration.
|
||||||
|
*/
|
||||||
export const StatList = [Stat.Speed, Stat.Stamina, Stat.Power, Stat.Guts, Stat.Wit] as const;
|
export const StatList = [Stat.Speed, Stat.Stamina, Stat.Power, Stat.Guts, Stat.Wit] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race runner, i.e. a trained horse.
|
||||||
|
*/
|
||||||
export interface Runner {
|
export interface Runner {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@@ -41,6 +50,12 @@ export interface Runner {
|
|||||||
unique_level: number;
|
unique_level: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new runner with baseline stats.
|
||||||
|
* @param name Name to apply to the runner
|
||||||
|
* @param base_uma Character card (trainee or uma) to use for aptitudes; otherwise all aptitudes are A
|
||||||
|
* @returns Baseline runner
|
||||||
|
*/
|
||||||
export function new_runner(name?: string, base_uma?: Uma): Runner {
|
export function new_runner(name?: string, base_uma?: Uma): Runner {
|
||||||
return {
|
return {
|
||||||
name: name ?? '',
|
name: name ?? '',
|
||||||
@@ -68,6 +83,9 @@ export function new_runner(name?: string, base_uma?: Uma): Runner {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mood levels.
|
||||||
|
*/
|
||||||
export enum Mood {
|
export enum Mood {
|
||||||
Awful = -2,
|
Awful = -2,
|
||||||
Bad,
|
Bad,
|
||||||
@@ -76,6 +94,11 @@ export enum Mood {
|
|||||||
Great,
|
Great,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Running styles for strategy–phase coefficients.
|
||||||
|
* Great Escape is distinguished as a separate style even though it is
|
||||||
|
* mechanically identical to Front Runner.
|
||||||
|
*/
|
||||||
export enum RunningStyle {
|
export enum RunningStyle {
|
||||||
FrontRunner,
|
FrontRunner,
|
||||||
PaceChaser,
|
PaceChaser,
|
||||||
@@ -84,6 +107,9 @@ export enum RunningStyle {
|
|||||||
GreatEscape,
|
GreatEscape,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aptitude or proficiency levels.
|
||||||
|
*/
|
||||||
export enum AptitudeLevel {
|
export enum AptitudeLevel {
|
||||||
G,
|
G,
|
||||||
F,
|
F,
|
||||||
@@ -95,6 +121,24 @@ export enum AptitudeLevel {
|
|||||||
S,
|
S,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aptitude levels as a descending list for easy iterating.
|
||||||
|
*/
|
||||||
|
export const APTITUDE_LEVELS = [
|
||||||
|
AptitudeLevel.S,
|
||||||
|
AptitudeLevel.A,
|
||||||
|
AptitudeLevel.B,
|
||||||
|
AptitudeLevel.C,
|
||||||
|
AptitudeLevel.D,
|
||||||
|
AptitudeLevel.E,
|
||||||
|
AptitudeLevel.F,
|
||||||
|
AptitudeLevel.G,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race phases.
|
||||||
|
* While last spurt phase is also a phase, it is not distinguished here.
|
||||||
|
*/
|
||||||
export enum Phase {
|
export enum Phase {
|
||||||
EarlyRace,
|
EarlyRace,
|
||||||
MidRace,
|
MidRace,
|
||||||
@@ -179,6 +223,12 @@ namespace Alpha123Umalator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a runner from an external source.
|
||||||
|
* @param obj Decoded object to import
|
||||||
|
* @param name Name or memo to apply to the runner
|
||||||
|
* @returns Imported runner, or null if import was not possible
|
||||||
|
*/
|
||||||
export function import_runner(obj: any, name?: string): Runner | null {
|
export function import_runner(obj: any, name?: string): Runner | null {
|
||||||
// TODO(zeph): check for keys that identify the uma source
|
// TODO(zeph): check for keys that identify the uma source
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
@@ -199,7 +249,7 @@ function baseSpeed(raceLen: number): number {
|
|||||||
return 20 - (raceLen - 2000) / 1000;
|
return 20 - (raceLen - 2000) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strategyPhaseCoeff = [
|
const speedStrategyPhaseCoeff = [
|
||||||
[1.0, 0.98, 0.962],
|
[1.0, 0.98, 0.962],
|
||||||
[0.978, 0.991, 0.975],
|
[0.978, 0.991, 0.975],
|
||||||
[0.938, 0.998, 0.994],
|
[0.938, 0.998, 0.994],
|
||||||
@@ -226,7 +276,7 @@ export function spurtSpeed(
|
|||||||
raceLen: number,
|
raceLen: number,
|
||||||
): number {
|
): number {
|
||||||
const speedStat = rawSpeed <= 1200 ? rawSpeed : 1200 + (rawSpeed - 1200) * 0.5;
|
const speedStat = rawSpeed <= 1200 ? rawSpeed : 1200 + (rawSpeed - 1200) * 0.5;
|
||||||
const spc = strategyPhaseCoeff[style][Phase.LateRace];
|
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
|
||||||
const dpm = distanceProficiencyMod[distance];
|
const dpm = distanceProficiencyMod[distance];
|
||||||
const base = baseSpeed(raceLen);
|
const base = baseSpeed(raceLen);
|
||||||
// Expand and rearrange terms from the doccy to make solving for the inverse easier.
|
// Expand and rearrange terms from the doccy to make solving for the inverse easier.
|
||||||
@@ -260,7 +310,7 @@ export function inverseSpurtSpeed(
|
|||||||
// spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001 = 0.0041*sqrt(500*speedStat)*dpm
|
// spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001 = 0.0041*sqrt(500*speedStat)*dpm
|
||||||
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.0041²*dpm²*500) = speedStat
|
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.0041²*dpm²*500) = speedStat
|
||||||
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.008405*dpm²) = speedStat
|
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.008405*dpm²) = speedStat
|
||||||
const spc = strategyPhaseCoeff[style][Phase.LateRace];
|
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
|
||||||
const dpm = distanceProficiencyMod[distance];
|
const dpm = distanceProficiencyMod[distance];
|
||||||
const base = baseSpeed(raceLen);
|
const base = baseSpeed(raceLen);
|
||||||
const nr = spurtSpeed - base * (1.05 * spc + 0.0105) - Math.pow(450 * gutsStat, 0.597) * 0.0001;
|
const nr = spurtSpeed - base * (1.05 * spc + 0.0105) - Math.pow(450 * gutsStat, 0.597) * 0.0001;
|
||||||
@@ -270,3 +320,139 @@ export function inverseSpurtSpeed(
|
|||||||
}
|
}
|
||||||
return Math.round(r);
|
return Math.round(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Meters per horse length (馬身). */
|
||||||
|
export const HORSE_LENGTH = 2.5;
|
||||||
|
/** Meters per course width (a constant unit of measure). */
|
||||||
|
export const COURSE_WIDTH = 11.25;
|
||||||
|
/** Meters per lane width. */
|
||||||
|
export const LANE_WIDTH = COURSE_WIDTH / 18;
|
||||||
|
|
||||||
|
const accelStrategyPhaseCoeff = {
|
||||||
|
[RunningStyle.FrontRunner]: [1.0, 1.0, 0.996],
|
||||||
|
[RunningStyle.PaceChaser]: [0.985, 1.0, 0.996],
|
||||||
|
[RunningStyle.LateSurger]: [0.975, 1.0, 1.0],
|
||||||
|
[RunningStyle.EndCloser]: [0.945, 1.0, 0.997],
|
||||||
|
[RunningStyle.GreatEscape]: [1.17, 0.94, 0.956],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
|
||||||
|
const accelDistanceProficiencyMod = [0.4, 0.5, 0.6, 1, 1, 1, 1, 1] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a horse's instantaneous acceleration value.
|
||||||
|
* @param powerStat Final power stat
|
||||||
|
* @param style Running style
|
||||||
|
* @param phase Current race phase for this frame
|
||||||
|
* @param surfaceAptitude Surface aptitude
|
||||||
|
* @param distanceAptitude Distance aptitude; no effect if not given
|
||||||
|
* @param uphill Whether this frame has a positive SlopePer value
|
||||||
|
* @param startDash Whether this frame is in the start dash period, i.e. current speed has not yet reached 85% of the race's base speed
|
||||||
|
* @returns Acceleration in m/s²
|
||||||
|
*/
|
||||||
|
export function acceleration(
|
||||||
|
powerStat: number,
|
||||||
|
style: RunningStyle,
|
||||||
|
surfaceAptitude: AptitudeLevel,
|
||||||
|
phase: Phase,
|
||||||
|
distanceAptitude?: AptitudeLevel,
|
||||||
|
uphill?: boolean,
|
||||||
|
startDash?: boolean,
|
||||||
|
): number {
|
||||||
|
const baseAccel = uphill ? 0.0004 : 0.0006;
|
||||||
|
const startDashMod = startDash ? 24.0 : 0;
|
||||||
|
const spc = accelStrategyPhaseCoeff[style][phase];
|
||||||
|
const spm = surfaceProficiencyMod[surfaceAptitude];
|
||||||
|
const dpm = accelDistanceProficiencyMod[distanceAptitude ?? AptitudeLevel.A];
|
||||||
|
return baseAccel * Math.sqrt(500 * powerStat) * spc * spm * dpm + startDashMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseDecel = {
|
||||||
|
[Phase.EarlyRace]: -1.2,
|
||||||
|
[Phase.MidRace]: -0.8,
|
||||||
|
[Phase.LateRace]: -1.0,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the phase-based deceleration value.
|
||||||
|
* @param phase Race phase that the horse is running this frame
|
||||||
|
* @param pdm Whether the horse is currently running in pace-down mode
|
||||||
|
* @param dead Whether the horse has zero or less HP
|
||||||
|
* @returns Current deceleration value in m/s², a negative value
|
||||||
|
*/
|
||||||
|
export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): number {
|
||||||
|
if (dead) {
|
||||||
|
return -1.2;
|
||||||
|
}
|
||||||
|
if (pdm) {
|
||||||
|
// This isn't until 1.5anni.
|
||||||
|
// return -0.5;
|
||||||
|
return phaseDecel[phase];
|
||||||
|
}
|
||||||
|
return phaseDecel[phase];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the speed boost gained from spot struggle.
|
||||||
|
* @param gutsStat Final guts stat
|
||||||
|
* @returns Spot struggle speed boost in m/s
|
||||||
|
*/
|
||||||
|
export function spotStruggleSpeed(gutsStat: number): number {
|
||||||
|
return Math.pow(500 * gutsStat, 0.6) * 0.0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the max duration of spot struggle.
|
||||||
|
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,
|
||||||
|
* or at the start of section 9.
|
||||||
|
* @param gutsStat Final guts stat
|
||||||
|
* @param frontAptitude Front runner aptitude level
|
||||||
|
* @returns Spot struggle duration in s
|
||||||
|
*/
|
||||||
|
export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLevel): number {
|
||||||
|
// https://hakuraku.moe/notes/spot-struggle
|
||||||
|
return Math.sqrt(700 * gutsStat) * 0.012 * strategyProficiencyMod[frontAptitude];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the forward speed boost given when moving lanewise while a skill
|
||||||
|
* that grants a lane change speed boost is active.
|
||||||
|
* @param powerStat Final power stat
|
||||||
|
* @returns Move-lane speed modifier in m/s
|
||||||
|
*/
|
||||||
|
export function moveLaneModifier(powerStat: number): number {
|
||||||
|
return Math.sqrt(0.0002 * powerStat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a skill's actual duration scaled to race length.
|
||||||
|
* @param baseDur Skill's listed duration in s
|
||||||
|
* @param raceLen Length of the race in m
|
||||||
|
* @returns Actual skill duration in s
|
||||||
|
*/
|
||||||
|
export function skillDuration(baseDur: number, raceLen: number): number {
|
||||||
|
return baseDur * raceLen * 0.001;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the distance gained from a target speed boost, including
|
||||||
|
* acceleration to the boosted target speed and deceleration back to baseline.
|
||||||
|
* @param speedBonus Difference between baseline and boosted speed in m/s
|
||||||
|
* @param accel Current acceleration value in m/s²
|
||||||
|
* @param decel Current phase-based deceleration value in m/s², a negative value.
|
||||||
|
* @param dur Duration of the boosted speed
|
||||||
|
* @returns Distance gained from the speed boost in m
|
||||||
|
*/
|
||||||
|
export function speedGain(speedBonus: number, accel: number, decel: number, dur: number): number {
|
||||||
|
// Actual effect of a target speed bonus looks like
|
||||||
|
// speed: __/-----\__
|
||||||
|
// bonus: ======
|
||||||
|
// I.e., the speed bonus duration includes acceleration to the new speed
|
||||||
|
// and does not include the acceleration back to baseline after it ends.
|
||||||
|
const accelTime = speedBonus / accel;
|
||||||
|
const decelTime = -speedBonus / decel;
|
||||||
|
// speedBonus*(dur-accelTime) + speedBonus*accelTime/2 + speedBonus*decelTime/2
|
||||||
|
return speedBonus * (dur + 0.5 * (decelTime - accelTime));
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<span class="flex-1 text-center">
|
<span class="flex-1 text-center">
|
||||||
<a href={resolve('/')} class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
|
<a href={resolve('/')} class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
|
||||||
<a href={resolve('/spurt')} class="mx-8 my-1 inline-block">Spurt Speed</a>
|
<a href={resolve('/spurt')} class="mx-8 my-1 inline-block">Spurt Speed</a>
|
||||||
|
<a href={resolve('/mspeed')} class="mx-8 my-1 inline-block">Mechanical Speed</a>
|
||||||
<a href={resolve('/convo')} class="mx-8 my-1 inline-block">Lobby Conversations</a>
|
<a href={resolve('/convo')} class="mx-8 my-1 inline-block">Lobby Conversations</a>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
<a href={resolve('/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other distance aptitudes
|
<a href={resolve('/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other distance aptitudes
|
||||||
and running styles.
|
and running styles.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the effects
|
||||||
|
of individual skills.
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={resolve('/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking
|
<a href={resolve('/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking
|
||||||
them quickly.
|
them quickly.
|
||||||
|
|||||||
173
zenno/src/routes/mspeed/+page.svelte
Normal file
173
zenno/src/routes/mspeed/+page.svelte
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||||
|
import {
|
||||||
|
acceleration,
|
||||||
|
APTITUDE_LEVELS,
|
||||||
|
AptitudeLevel,
|
||||||
|
deceleration,
|
||||||
|
HORSE_LENGTH,
|
||||||
|
moveLaneModifier,
|
||||||
|
Phase,
|
||||||
|
RunningStyle,
|
||||||
|
skillDuration,
|
||||||
|
speedGain,
|
||||||
|
spotStruggleDuration,
|
||||||
|
spotStruggleSpeed,
|
||||||
|
Stat,
|
||||||
|
} from '$lib/race';
|
||||||
|
import StatChart from '$lib/StatChart.svelte';
|
||||||
|
import SpeedDur from './SpeedDur.svelte';
|
||||||
|
|
||||||
|
let rawPower = $state(1200);
|
||||||
|
let rawGuts = $state(1200);
|
||||||
|
let raceLen = $state(1600);
|
||||||
|
let surfApt = $state(AptitudeLevel.A);
|
||||||
|
let frontApt = $state(AptitudeLevel.A);
|
||||||
|
let isRunaway = $state(false);
|
||||||
|
let isCareer = $state(false);
|
||||||
|
|
||||||
|
const careerMod = $derived(isCareer ? 400 : 0);
|
||||||
|
const powerStat = $derived(rawPower + careerMod);
|
||||||
|
const gutsStat = $derived(rawGuts + careerMod);
|
||||||
|
const style = $derived(isRunaway ? RunningStyle.GreatEscape : RunningStyle.FrontRunner);
|
||||||
|
|
||||||
|
const phases = [Phase.EarlyRace, Phase.MidRace, Phase.LateRace] as const;
|
||||||
|
const accel = $derived(phases.map((p) => acceleration(powerStat, style, surfApt, p)));
|
||||||
|
const decel = phases.map((p) => deceleration(p));
|
||||||
|
|
||||||
|
const ssBoost = $derived(spotStruggleSpeed(gutsStat));
|
||||||
|
const ssDur = $derived(spotStruggleDuration(gutsStat, frontApt));
|
||||||
|
|
||||||
|
const lcBoost = $derived(moveLaneModifier(powerStat));
|
||||||
|
const lcDur = $derived(Math.min(skillDuration(3, raceLen), 6));
|
||||||
|
|
||||||
|
const uniques = [
|
||||||
|
['Operation Cacao', 0.35, 5, Phase.MidRace],
|
||||||
|
["All Charged! It's Go Time! (Tokyo turf)", 0.45, 5, Phase.LateRace],
|
||||||
|
] as const;
|
||||||
|
const skills = [
|
||||||
|
['Fast-Paced', 0.15, 3, Phase.MidRace],
|
||||||
|
['Professor of Curvature (mid race)', 0.35, 2.4, Phase.MidRace],
|
||||||
|
["All Charged! It's Go Time! (inherited)", 0.25, 3, Phase.LateRace],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ssY: Array<ComputedSeries | null> = $derived([
|
||||||
|
{
|
||||||
|
label: 'Aptitude S',
|
||||||
|
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, AptitudeLevel.S)) / HORSE_LENGTH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Aptitude A',
|
||||||
|
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, AptitudeLevel.A)) / HORSE_LENGTH,
|
||||||
|
},
|
||||||
|
frontApt < AptitudeLevel.A
|
||||||
|
? {
|
||||||
|
label: `Aptitude ${AptitudeLevel[frontApt]}`,
|
||||||
|
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, frontApt)) / HORSE_LENGTH,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
]);
|
||||||
|
const lcY: Array<ComputedSeries | null> = $derived([
|
||||||
|
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), accel[0], decel[0], lcDur) / HORSE_LENGTH },
|
||||||
|
]);
|
||||||
|
const yRule: HorizontalRule[] = $derived([
|
||||||
|
{ y: speedGain(0.35, accel[1], decel[1], skillDuration(2.4, raceLen)) / HORSE_LENGTH, label: 'Professor of Curvature' },
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-4xl">Front Runner Mechanical Speed Comparator</h1>
|
||||||
|
<div class="mx-auto mt-8 grid max-w-4xl grid-cols-1 rounded-md text-center shadow-md ring md:grid-cols-8">
|
||||||
|
<div class="m-4 md:col-span-2">
|
||||||
|
<label for="powerStat">Power Stat</label>
|
||||||
|
<input type="number" id="powerStat" bind:value={rawPower} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4 md:col-span-2">
|
||||||
|
<label for="gutsStat">Guts Stat</label>
|
||||||
|
<input type="number" id="gutsStat" bind:value={rawGuts} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4 md:col-span-2">
|
||||||
|
<label for="surfaceApt">Surface Aptitude</label>
|
||||||
|
<select id="surfaceApt" required bind:value={surfApt} class="w-full">
|
||||||
|
{#each APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4 md:col-span-2">
|
||||||
|
<label for="frontApt">Front Runner Aptitude</label>
|
||||||
|
<select id="frontApt" required bind:value={frontApt} class="w-full">
|
||||||
|
{#each APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4 md:col-span-2 md:col-start-2">
|
||||||
|
<label for="raceLen">Race Distance</label>
|
||||||
|
<input type="number" id="raceLen" required bind:value={raceLen} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4 self-center md:col-span-2">
|
||||||
|
<label for="isRunaway" class="mr-1 align-middle">Runaway</label>
|
||||||
|
<input type="checkbox" id="isRunaway" role="switch" bind:checked={isRunaway} class="min-h-6 min-w-6 align-middle" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4 self-center md:col-span-2">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<span class="mt-8 block w-full text-center text-lg">Mechanics</span>
|
||||||
|
<div class="mx-auto flex w-full flex-col md:flex-row md:justify-center">
|
||||||
|
<SpeedDur speed={ssBoost} dur={ssDur} accel={accel[0]} decel={[decel[0], decel[1]]}>Spot Struggle</SpeedDur>
|
||||||
|
<SpeedDur speed={lcBoost} dur={lcDur} accel={accel[0]} decel={decel[0]}>Idealized Lane Combo</SpeedDur>
|
||||||
|
</div>
|
||||||
|
<span class="mt-8 block w-full text-center text-lg">Unique Skills</span>
|
||||||
|
<div class="mx-auto flex flex-col md:flex-row md:justify-center">
|
||||||
|
{#each uniques as [name, boost, dur, phase] (name)}
|
||||||
|
<SpeedDur speed={boost} dur={skillDuration(dur, raceLen)} accel={accel[phase]} decel={decel[phase]}>{name}</SpeedDur>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<span class="mt-8 block w-full text-center text-lg">Inherited Uniques & Other Skills</span>
|
||||||
|
<div class="mx-auto flex flex-col md:flex-row md:justify-center">
|
||||||
|
{#each skills as [name, boost, dur, phase] (name)}
|
||||||
|
<SpeedDur speed={boost} dur={skillDuration(dur, raceLen)} accel={accel[phase]} decel={decel[phase]}>{name}</SpeedDur>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex h-60 flex-col place-content-center py-4 md:h-96 md:flex-row">
|
||||||
|
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={gutsStat} {yRule} />
|
||||||
|
<StatChart class="flex-1" stat={Stat.Power} y={lcY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={powerStat} {yRule} />
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mt-8 w-full max-w-4xl">
|
||||||
|
<ul class="list-disc">
|
||||||
|
<li>All lengths gained include acceleration at the beginning of each speed boost and deceleration after its end.</li>
|
||||||
|
<li>Each effect is assumed to be isolated and executed on level ground.</li>
|
||||||
|
<li>
|
||||||
|
Spot struggle has two numbers to distinguish ending in early race versus ending in mid race, which gives different
|
||||||
|
deceleration values. Since spot struggle duration does not scale with race length, it is more likely to end in mid race on
|
||||||
|
shorter races.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Lane combo is idealized in the sense of assuming second lane change speed skill executes immediately after Dodging Danger
|
||||||
|
completes and the horse is never blocked.
|
||||||
|
<ul class="ml-4 list-[revert]">
|
||||||
|
<li>
|
||||||
|
The move lane modifier is capped to 6 seconds, which is the approximate observed time to move from the Dodging Danger
|
||||||
|
fixed lane back to the rail under the effect of Prudent Positioning.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
On medium+ tracks, with a proper gate acceleration build, Dodging Danger should realize some lane movement speed
|
||||||
|
modifier, so the actual benefit will be more than the idealized number.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Ignited Spirit WIT has a longer duration and lower lane change speed boost than Prudent Positioning, so it is likely to
|
||||||
|
give more benefit than the idealized number.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
For full simulated analysis of lane combo, see <a
|
||||||
|
href="https://lanecalc.hf-uma.net/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">危険回避シミュ</a
|
||||||
|
>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
33
zenno/src/routes/mspeed/SpeedDur.svelte
Normal file
33
zenno/src/routes/mspeed/SpeedDur.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { HORSE_LENGTH, speedGain } from '$lib/race';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Snippet;
|
||||||
|
speed: number;
|
||||||
|
dur: number;
|
||||||
|
accel: number;
|
||||||
|
decel: number | number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtp(x: number): string {
|
||||||
|
return x >= 0 ? '+' + x.toFixed(3) : x.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children, speed, dur, accel, decel }: Props = $props();
|
||||||
|
const decels = $derived([decel].flat(1));
|
||||||
|
|
||||||
|
const gain = $derived(decels.map((d) => speedGain(speed, accel, d, dur) / HORSE_LENGTH));
|
||||||
|
const text = $derived(gain.map(fmtp).join(' – '));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="m-2 flex h-full w-full max-w-80 flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md"
|
||||||
|
>
|
||||||
|
<div class="block">{@render children()}</div>
|
||||||
|
<span class="block text-xl">{@html text} L</span>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<span class="flex-1 text-xs">{fmtp(speed)} m/s</span>
|
||||||
|
<span class="flex-1 text-xs">{dur.toFixed(3)} s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user