Compare commits
15 Commits
bc94d66002
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bd99cfaa6d | |||
| 3ab17cf9b0 | |||
| 4bd7962182 | |||
| 09f099171b | |||
| 4fff7069a8 | |||
| 3e2153b39c | |||
| d0fa6ab15c | |||
| 9f8024d488 | |||
| 1df3bc1db9 | |||
| a8c1b9c754 | |||
| 7600c48cc7 | |||
| 2e31560d6c | |||
| 2a07f193ec | |||
| b720b325b3 | |||
| 80573a84ea |
1808
zenno/package-lock.json
generated
1808
zenno/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,30 +16,36 @@
|
|||||||
"test": "npm run test:unit -- --run"
|
"test": "npm run test:unit -- --run"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^2.0.4",
|
"@eslint/compat": "^2.1.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@sveltejs/adapter-auto": "^7.0.1",
|
"@sveltejs/adapter-auto": "^7.0.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.56.1",
|
"@sveltejs/kit": "^2.60.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@types/node": "^22.19.17",
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/node": "^22.19.19",
|
||||||
"@vitest/browser-playwright": "^4.1.0",
|
"@vitest/browser-playwright": "^4.1.0",
|
||||||
"eslint": "^10.2.0",
|
"d3": "^7.9.0",
|
||||||
|
"eslint": "^10.4.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.17.0",
|
"eslint-plugin-svelte": "^3.17.1",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.6.0",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.60.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.3",
|
||||||
"prettier-plugin-svelte": "^3.5.1",
|
"prettier-plugin-svelte": "^3.5.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.4",
|
||||||
"svelte": "^5.55.1",
|
"svelte": "^5.55.7",
|
||||||
"svelte-check": "^4.4.6",
|
"svelte-check": "^4.4.8",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.59.3",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.3.3",
|
||||||
"vitest": "^4.1.0",
|
"vitest": "^4.1.0",
|
||||||
"vitest-browser-svelte": "^2.1.0"
|
"vitest-browser-svelte": "^2.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@observablehq/plot": "^0.6.17",
|
||||||
|
"mathjs": "^15.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
zenno/src/lib/Skill.svelte
Normal file
27
zenno/src/lib/Skill.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { skills, ZERO_SKILL, type Skill } from "./data/skill";
|
||||||
|
|
||||||
|
interface CommonProps {
|
||||||
|
hint?: string;
|
||||||
|
mention?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = CommonProps & ({skill: number, name?: never} | {name: string, skill?: never});
|
||||||
|
|
||||||
|
let {hint, mention, skill, name}: Props = $props();
|
||||||
|
|
||||||
|
const s: Readonly<Skill> = $derived.by(() => {
|
||||||
|
const l = skill != null ? skills.global.filter((s) => s.skill_id === skill) : skills.global.filter((s) => s.name.includes(name!));
|
||||||
|
if (name != null) {
|
||||||
|
console.warn(`skills specified as ${name} (${hint}):`, l);
|
||||||
|
}
|
||||||
|
if (l.length === 0) {
|
||||||
|
return ZERO_SKILL;
|
||||||
|
}
|
||||||
|
return l[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const spanClass = $derived(mention ? 'italic' : 'font-bold')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={spanClass}>{s.name}</span>
|
||||||
125
zenno/src/lib/StatChart.svelte
Normal file
125
zenno/src/lib/StatChart.svelte
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Plot from '@observablehq/plot';
|
||||||
|
import * as d3 from 'd3';
|
||||||
|
import { Stat } from './race';
|
||||||
|
import type { ComputedSeries, HorizontalRule } from './chart';
|
||||||
|
import type { ClassValue } from 'svelte/elements';
|
||||||
|
import type { Attachment } from 'svelte/attachments';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** The stat the chart shows. */
|
||||||
|
stat: Stat;
|
||||||
|
/** Series to show in the chart. */
|
||||||
|
y: ComputedSeries | Array<ComputedSeries | null>;
|
||||||
|
/** Label for the dependent variable. */
|
||||||
|
yLabel: string;
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
/**
|
||||||
|
* Vertical rules to place on the graph.
|
||||||
|
* Each rule gets a corresponding horizontal rule at the intersection
|
||||||
|
* with each series.
|
||||||
|
*/
|
||||||
|
xRule?: number | number[];
|
||||||
|
/**
|
||||||
|
* Horizontal rules to place on the graph.
|
||||||
|
*/
|
||||||
|
yRule?: HorizontalRule[];
|
||||||
|
|
||||||
|
class?: ClassValue | null;
|
||||||
|
plotOptions?: Omit<Plot.PlotOptions, 'marks' | 'x' | 'y'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { stat, y, yLabel, range, xRange, xRule = [], yRule = [], class: className, plotOptions = {} }: Props = $props();
|
||||||
|
|
||||||
|
let width = $state(0);
|
||||||
|
let height = $state(0);
|
||||||
|
|
||||||
|
let expand = $state(false);
|
||||||
|
|
||||||
|
const xLabel = $derived(Stat[stat]);
|
||||||
|
const xLines = $derived([xRule].flat(1));
|
||||||
|
const xMin = $derived(xRange?.[0] ?? 200);
|
||||||
|
const xMax = $derived.by(() => {
|
||||||
|
if (xRange?.[1] != null) {
|
||||||
|
return xRange[1];
|
||||||
|
}
|
||||||
|
if (expand) {
|
||||||
|
return 2000;
|
||||||
|
}
|
||||||
|
return 100 * Math.ceil(Math.max(1200, ...xLines) / 100);
|
||||||
|
});
|
||||||
|
const xVal = $derived(d3.range(xMin, xMax, 5));
|
||||||
|
const thrX = 1200;
|
||||||
|
|
||||||
|
const series = $derived([y].flat(1).filter((s) => s != null));
|
||||||
|
const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(x), label }))));
|
||||||
|
const yRange: [number, number] = $derived.by(() => {
|
||||||
|
if (range != null) {
|
||||||
|
if (range.length === 2) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
return [range[0], expand ? range[2] : range[1]];
|
||||||
|
}
|
||||||
|
const l = d3.min(vals, ({ y }) => y) ?? 0;
|
||||||
|
const r = d3.max(vals, ({ y }) => y) ?? 1;
|
||||||
|
return [l, r];
|
||||||
|
});
|
||||||
|
const yLines = $derived(xLines.flatMap((x) => series.map(({ y, label }) => ({ y: y(x), label }))));
|
||||||
|
|
||||||
|
const makeChart: Attachment = (el) => {
|
||||||
|
$effect(() => {
|
||||||
|
el?.firstChild?.remove();
|
||||||
|
el?.append(
|
||||||
|
Plot.plot({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
clip: true,
|
||||||
|
...plotOptions,
|
||||||
|
x: {
|
||||||
|
domain: [xMin, xMax],
|
||||||
|
interval: 5,
|
||||||
|
ticks: d3.range(2000, 0, -200).filter((x) => xMin <= x && x <= xMax),
|
||||||
|
label: xLabel,
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
domain: yRange,
|
||||||
|
grid: true,
|
||||||
|
label: yLabel,
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
marks: [
|
||||||
|
Plot.ruleX([thrX], { strokeOpacity: 0.25 }),
|
||||||
|
Plot.ruleX(xLines, { 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.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
|
||||||
|
Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:clientWidth={width} bind:clientHeight={height} class={['flex h-full w-full flex-col md:flex-row', className]}>
|
||||||
|
<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>
|
||||||
78
zenno/src/lib/alpha123Umalator.ts
Normal file
78
zenno/src/lib/alpha123Umalator.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { AptitudeLevel, Mood, RunningStyle } from './race';
|
||||||
|
import type { Runner } from './runner';
|
||||||
|
|
||||||
|
const aptMap = {
|
||||||
|
G: AptitudeLevel.G,
|
||||||
|
F: AptitudeLevel.F,
|
||||||
|
E: AptitudeLevel.E,
|
||||||
|
D: AptitudeLevel.D,
|
||||||
|
C: AptitudeLevel.C,
|
||||||
|
B: AptitudeLevel.B,
|
||||||
|
A: AptitudeLevel.A,
|
||||||
|
S: AptitudeLevel.S,
|
||||||
|
} as const;
|
||||||
|
type AptitudeString = keyof typeof aptMap;
|
||||||
|
|
||||||
|
const styleMap = {
|
||||||
|
Nige: RunningStyle.FrontRunner,
|
||||||
|
Sentou: RunningStyle.PaceChaser,
|
||||||
|
Sasi: RunningStyle.LateSurger,
|
||||||
|
Oikomi: RunningStyle.EndCloser,
|
||||||
|
Oonige: RunningStyle.GreatEscape,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface ImportUma {
|
||||||
|
outfitId: string;
|
||||||
|
starCount: number;
|
||||||
|
speed: number;
|
||||||
|
stamina: number;
|
||||||
|
power: number;
|
||||||
|
guts: number;
|
||||||
|
wisdom: number;
|
||||||
|
strategy: keyof typeof styleMap;
|
||||||
|
distanceAptitude: AptitudeString;
|
||||||
|
surfaceAptitude: AptitudeString;
|
||||||
|
strategyAptitude: AptitudeString;
|
||||||
|
aptitudes: [
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
AptitudeString,
|
||||||
|
];
|
||||||
|
skills: string[];
|
||||||
|
uniqueLv: number;
|
||||||
|
mood: Mood;
|
||||||
|
popularity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load(obj: ImportUma, name?: string): Runner {
|
||||||
|
return {
|
||||||
|
name: name ?? '',
|
||||||
|
chara_card_id: obj.outfitId !== '' ? parseInt(obj.outfitId) : 0,
|
||||||
|
style: styleMap[obj.strategy],
|
||||||
|
mood: obj.mood,
|
||||||
|
speed: obj.speed,
|
||||||
|
stamina: obj.stamina,
|
||||||
|
power: obj.power,
|
||||||
|
guts: obj.guts,
|
||||||
|
wit: obj.wisdom,
|
||||||
|
sprint: aptMap[obj.aptitudes[0]],
|
||||||
|
mile: aptMap[obj.aptitudes[1]],
|
||||||
|
medium: aptMap[obj.aptitudes[2]],
|
||||||
|
long: aptMap[obj.aptitudes[3]],
|
||||||
|
front: aptMap[obj.aptitudes[4]],
|
||||||
|
pace: aptMap[obj.aptitudes[5]],
|
||||||
|
late: aptMap[obj.aptitudes[6]],
|
||||||
|
end: aptMap[obj.aptitudes[7]],
|
||||||
|
turf: aptMap[obj.aptitudes[8]],
|
||||||
|
dirt: aptMap[obj.aptitudes[9]],
|
||||||
|
skills: obj.skills.map((s) => parseInt(s)),
|
||||||
|
unique_level: obj.uniqueLv,
|
||||||
|
};
|
||||||
|
}
|
||||||
9
zenno/src/lib/chart.ts
Normal file
9
zenno/src/lib/chart.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface ComputedSeries {
|
||||||
|
y: (x: number) => number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HorizontalRule {
|
||||||
|
y: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
173
zenno/src/lib/data/skill.ts
Normal file
173
zenno/src/lib/data/skill.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import skillGlobal from '../../../../global/skill.json'
|
||||||
|
import groupGlobal from '../../../../global/skill-group.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill data.
|
||||||
|
*/
|
||||||
|
export interface Skill {
|
||||||
|
/**
|
||||||
|
* Skill ID.
|
||||||
|
*/
|
||||||
|
skill_id: number;
|
||||||
|
/**
|
||||||
|
* Regional skill name.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Regional skil description.
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* Skill group ID.
|
||||||
|
*/
|
||||||
|
group: number;
|
||||||
|
/**
|
||||||
|
* Skill rarity. 3-5 are uniques for various star levels.
|
||||||
|
*/
|
||||||
|
rarity: 1 | 2 | 3 | 4 | 5;
|
||||||
|
/**
|
||||||
|
* Upgrade position within the skill's group.
|
||||||
|
* -1 is for negative (purple) skills.
|
||||||
|
*/
|
||||||
|
group_rate: 1 | 2 | 3 | -1;
|
||||||
|
/**
|
||||||
|
* Grade value, or the amount of rating gained for having the skill with
|
||||||
|
* appropriate aptitude.
|
||||||
|
*/
|
||||||
|
grade_value?: number;
|
||||||
|
/**
|
||||||
|
* Whether the skill requires a wit check.
|
||||||
|
*/
|
||||||
|
wit_check: boolean;
|
||||||
|
/**
|
||||||
|
* Conditions and results of skill activation.
|
||||||
|
*/
|
||||||
|
activations: Activation[];
|
||||||
|
/**
|
||||||
|
* Name of the Uma which owns this skill as a unique, if applicable.
|
||||||
|
*/
|
||||||
|
unique_owner?: string;
|
||||||
|
/**
|
||||||
|
* SP cost to purchase the skill, if applicable.
|
||||||
|
*/
|
||||||
|
sp_cost?: number;
|
||||||
|
/**
|
||||||
|
* Skill icon ID.
|
||||||
|
*/
|
||||||
|
icon_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditions and results of skill activation.
|
||||||
|
*/
|
||||||
|
export interface Activation {
|
||||||
|
/**
|
||||||
|
* Precondition which must be satisfied before the condition is checked.
|
||||||
|
*/
|
||||||
|
precondition?: string;
|
||||||
|
/**
|
||||||
|
* Activation conditions.
|
||||||
|
*/
|
||||||
|
condition: string;
|
||||||
|
/**
|
||||||
|
* Skill duration in ten thousandths of a second.
|
||||||
|
* Generally undefined for activations which only affect HP.
|
||||||
|
*/
|
||||||
|
duration?: number;
|
||||||
|
/**
|
||||||
|
* Special skill duration scaling mode.
|
||||||
|
*/
|
||||||
|
dur_scale: 1 | 2 | 3 | 4 | 5 | 7;
|
||||||
|
/**
|
||||||
|
* Skill cooldown in ten thousandths of a second.
|
||||||
|
* A value of 5000000 indicates that the cooldown is forever.
|
||||||
|
* Generally undefined for passive skills.
|
||||||
|
*/
|
||||||
|
cooldown?: number;
|
||||||
|
/**
|
||||||
|
* Results applied when the skill's conditions are met.
|
||||||
|
*/
|
||||||
|
abilities: Ability[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effects applied when a skill activates.
|
||||||
|
*/
|
||||||
|
export interface Ability {
|
||||||
|
/**
|
||||||
|
* Race mechanic affected by the ability.
|
||||||
|
*/
|
||||||
|
type: 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 13 | 21 | 27 | 28 | 31 | 35;
|
||||||
|
/**
|
||||||
|
* Special scaling type of the skill value.
|
||||||
|
*/
|
||||||
|
value_usage: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 19 | 20 | 22 | 23 | 24 | 25;
|
||||||
|
/**
|
||||||
|
* Amount that the skill modifies the race mechanic in ten thousandths of
|
||||||
|
* whatever is the appropriate unit.
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
/**
|
||||||
|
* Selector for horses targeted by the ability.
|
||||||
|
*/
|
||||||
|
target: 1 | 2 | 4 | 7 | 9 | 10 | 11 | 18 | 19 | 20 | 21 | 22 | 23;
|
||||||
|
/**
|
||||||
|
* Argument value for the ability target, when appropriate.
|
||||||
|
*/
|
||||||
|
target_value?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill groups.
|
||||||
|
* Skills in a skill group replace each other when purchased.
|
||||||
|
*
|
||||||
|
* As a special case, horsegen lists both unique skills and their inherited
|
||||||
|
* versions in the skill groups for both.
|
||||||
|
*/
|
||||||
|
export interface SkillGroup {
|
||||||
|
/**
|
||||||
|
* Skill group ID.
|
||||||
|
*/
|
||||||
|
skill_group: number;
|
||||||
|
/**
|
||||||
|
* Base skill in the skill group, if any.
|
||||||
|
* Either a common (white) skill or an Uma's own unique.
|
||||||
|
*
|
||||||
|
* Some skill groups, e.g. for G1 Averseness, have no base skill.
|
||||||
|
*/
|
||||||
|
skill1?: number;
|
||||||
|
/**
|
||||||
|
* First upgraded version of a skill, if any.
|
||||||
|
* A rare (gold) skill, double circle skill, or an inherited unique skill.
|
||||||
|
*/
|
||||||
|
skill2?: number;
|
||||||
|
/**
|
||||||
|
* Highest upgraded version of a skill, if any.
|
||||||
|
* Gold version of a skill with a double circle version.
|
||||||
|
*/
|
||||||
|
skill3?: number;
|
||||||
|
/**
|
||||||
|
* Negative (purple) version of a skill, if any.
|
||||||
|
*/
|
||||||
|
skill_bad?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const skills = {
|
||||||
|
global: skillGlobal as Skill[],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const skillGroups = {
|
||||||
|
global: groupGlobal as SkillGroup[],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ZERO_SKILL: Readonly<Skill> = {
|
||||||
|
skill_id: 0,
|
||||||
|
name: "invalid skill",
|
||||||
|
description: "an invalid skill was specified",
|
||||||
|
group: 0,
|
||||||
|
rarity: 1,
|
||||||
|
group_rate: 1,
|
||||||
|
wit_check: false,
|
||||||
|
activations: [],
|
||||||
|
icon_id: 0,
|
||||||
|
} as const;
|
||||||
9
zenno/src/lib/prob.ts
Normal file
9
zenno/src/lib/prob.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as math from "mathjs";
|
||||||
|
|
||||||
|
export function binomPMF(p: number, n: number, k: number): number {
|
||||||
|
// Operate in log domain for precision.
|
||||||
|
const lc = math.lgamma(n+1) - math.lgamma(k+1) - math.lgamma(n-k+1);
|
||||||
|
const lpk = k * math.log(p);
|
||||||
|
const lr = (n - k) * math.log(1 - p);
|
||||||
|
return math.exp(lc + lpk + lr);
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
// Umamusume race mechanics adapted from KuromiAK's doc:
|
// Umamusume race mechanics adapted from KuromiAK's doc:
|
||||||
// https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?usp=sharing
|
// https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?usp=sharing
|
||||||
|
|
||||||
import type { Uma } from './data/uma';
|
import { binomPMF } from "./prob";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fundamental stats of umas.
|
||||||
|
*/
|
||||||
export enum Stat {
|
export enum Stat {
|
||||||
Speed,
|
Speed,
|
||||||
Stamina,
|
Stamina,
|
||||||
@@ -11,63 +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;
|
||||||
|
|
||||||
export interface Runner {
|
/**
|
||||||
name: string;
|
* Mood levels.
|
||||||
|
*/
|
||||||
chara_card_id: number;
|
|
||||||
style: RunningStyle;
|
|
||||||
mood: Mood;
|
|
||||||
|
|
||||||
speed: number;
|
|
||||||
stamina: number;
|
|
||||||
power: number;
|
|
||||||
guts: number;
|
|
||||||
wit: number;
|
|
||||||
|
|
||||||
sprint: AptitudeLevel;
|
|
||||||
mile: AptitudeLevel;
|
|
||||||
medium: AptitudeLevel;
|
|
||||||
long: AptitudeLevel;
|
|
||||||
front: AptitudeLevel;
|
|
||||||
pace: AptitudeLevel;
|
|
||||||
late: AptitudeLevel;
|
|
||||||
end: AptitudeLevel;
|
|
||||||
turf: AptitudeLevel;
|
|
||||||
dirt: AptitudeLevel;
|
|
||||||
|
|
||||||
skills: number[];
|
|
||||||
unique_level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function new_runner(name?: string, base_uma?: Uma): Runner {
|
|
||||||
return {
|
|
||||||
name: name ?? '',
|
|
||||||
chara_card_id: base_uma?.chara_card_id ?? 0,
|
|
||||||
// TODO(zeph): default running style
|
|
||||||
style: RunningStyle.FrontRunner,
|
|
||||||
mood: Mood.Normal,
|
|
||||||
speed: 1200,
|
|
||||||
stamina: 1200,
|
|
||||||
power: 1200,
|
|
||||||
guts: 1200,
|
|
||||||
wit: 1200,
|
|
||||||
sprint: base_uma?.sprint ?? AptitudeLevel.A,
|
|
||||||
mile: base_uma?.mile ?? AptitudeLevel.A,
|
|
||||||
medium: base_uma?.medium ?? AptitudeLevel.A,
|
|
||||||
long: base_uma?.long ?? AptitudeLevel.A,
|
|
||||||
front: base_uma?.front ?? AptitudeLevel.A,
|
|
||||||
pace: base_uma?.pace ?? AptitudeLevel.A,
|
|
||||||
late: base_uma?.late ?? AptitudeLevel.A,
|
|
||||||
end: base_uma?.end ?? AptitudeLevel.A,
|
|
||||||
turf: base_uma?.turf ?? AptitudeLevel.A,
|
|
||||||
dirt: base_uma?.dirt ?? AptitudeLevel.A,
|
|
||||||
skills: base_uma != null ? [base_uma.unique] : [],
|
|
||||||
unique_level: 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Mood {
|
export enum Mood {
|
||||||
Awful = -2,
|
Awful = -2,
|
||||||
Bad,
|
Bad,
|
||||||
@@ -76,6 +30,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 +43,9 @@ export enum RunningStyle {
|
|||||||
GreatEscape,
|
GreatEscape,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aptitude or proficiency levels.
|
||||||
|
*/
|
||||||
export enum AptitudeLevel {
|
export enum AptitudeLevel {
|
||||||
G,
|
G,
|
||||||
F,
|
F,
|
||||||
@@ -95,111 +57,35 @@ 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,
|
||||||
LateRace,
|
LateRace,
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Alpha123Umalator {
|
|
||||||
const aptitude_map = {
|
|
||||||
G: AptitudeLevel.G,
|
|
||||||
F: AptitudeLevel.F,
|
|
||||||
E: AptitudeLevel.E,
|
|
||||||
D: AptitudeLevel.D,
|
|
||||||
C: AptitudeLevel.C,
|
|
||||||
B: AptitudeLevel.B,
|
|
||||||
A: AptitudeLevel.A,
|
|
||||||
S: AptitudeLevel.S,
|
|
||||||
} as const;
|
|
||||||
type AptitudeString = keyof typeof aptitude_map;
|
|
||||||
|
|
||||||
const style_map = {
|
|
||||||
Nige: RunningStyle.FrontRunner,
|
|
||||||
Sentou: RunningStyle.PaceChaser,
|
|
||||||
Sasi: RunningStyle.LateSurger,
|
|
||||||
Oikomi: RunningStyle.EndCloser,
|
|
||||||
Oonige: RunningStyle.GreatEscape,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export interface ImportUma {
|
|
||||||
outfitId: string;
|
|
||||||
starCount: number;
|
|
||||||
speed: number;
|
|
||||||
stamina: number;
|
|
||||||
power: number;
|
|
||||||
guts: number;
|
|
||||||
wisdom: number;
|
|
||||||
strategy: keyof typeof style_map;
|
|
||||||
distanceAptitude: AptitudeString;
|
|
||||||
surfaceAptitude: AptitudeString;
|
|
||||||
strategyAptitude: AptitudeString;
|
|
||||||
aptitudes: [
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
AptitudeString,
|
|
||||||
];
|
|
||||||
skills: string[];
|
|
||||||
uniqueLv: number;
|
|
||||||
mood: Mood;
|
|
||||||
popularity: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function load(obj: ImportUma, name?: string): Runner {
|
|
||||||
return {
|
|
||||||
name: name ?? '',
|
|
||||||
chara_card_id: obj.outfitId !== '' ? parseInt(obj.outfitId) : 0,
|
|
||||||
style: style_map[obj.strategy],
|
|
||||||
mood: obj.mood,
|
|
||||||
speed: obj.speed,
|
|
||||||
stamina: obj.stamina,
|
|
||||||
power: obj.power,
|
|
||||||
guts: obj.guts,
|
|
||||||
wit: obj.wisdom,
|
|
||||||
sprint: aptitude_map[obj.aptitudes[0]],
|
|
||||||
mile: aptitude_map[obj.aptitudes[1]],
|
|
||||||
medium: aptitude_map[obj.aptitudes[2]],
|
|
||||||
long: aptitude_map[obj.aptitudes[3]],
|
|
||||||
front: aptitude_map[obj.aptitudes[4]],
|
|
||||||
pace: aptitude_map[obj.aptitudes[5]],
|
|
||||||
late: aptitude_map[obj.aptitudes[6]],
|
|
||||||
end: aptitude_map[obj.aptitudes[7]],
|
|
||||||
turf: aptitude_map[obj.aptitudes[8]],
|
|
||||||
dirt: aptitude_map[obj.aptitudes[9]],
|
|
||||||
skills: obj.skills.map((s) => parseInt(s)),
|
|
||||||
unique_level: obj.uniqueLv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function import_runner(obj: any, name?: string): Runner | null {
|
|
||||||
// TODO(zeph): check for keys that identify the uma source
|
|
||||||
if (typeof obj === 'object') {
|
|
||||||
try {
|
|
||||||
const r = Alpha123Umalator.load(obj as Alpha123Umalator.ImportUma, name);
|
|
||||||
// TODO(zeph): validate?
|
|
||||||
return r;
|
|
||||||
} catch (exc) {
|
|
||||||
console.warn('failed to import', obj, 'as alpha123 umalator:', exc);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.warn('no guess on how to import', obj);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function baseSpeed(raceLen: number): number {
|
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],
|
||||||
@@ -211,22 +97,21 @@ const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as cons
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate an Uma's last spurt target speed.
|
* Calculate an Uma's last spurt target speed.
|
||||||
* @param rawSpeed Uma's speed stat. No accounting for mood lol.
|
* @param speedStat Adjusted speed stat
|
||||||
* @param gutsStat Uma's guts stat.
|
* @param gutsStat Adjusted guts stat
|
||||||
* @param style Running style
|
* @param style Running style
|
||||||
* @param distance Distance aptitude
|
* @param distance Distance aptitude
|
||||||
* @param raceLen Length of the race
|
* @param raceLen Length of the race
|
||||||
* @returns Target speed in the last spurt in m/s
|
* @returns Target speed in the last spurt in m/s
|
||||||
*/
|
*/
|
||||||
export function spurtSpeed(
|
export function spurtSpeed(
|
||||||
rawSpeed: number,
|
speedStat: number,
|
||||||
gutsStat: number,
|
gutsStat: number,
|
||||||
style: RunningStyle,
|
style: RunningStyle,
|
||||||
distance: AptitudeLevel,
|
distance: AptitudeLevel,
|
||||||
raceLen: number,
|
raceLen: number,
|
||||||
): number {
|
): number {
|
||||||
const speedStat = rawSpeed <= 1200 ? rawSpeed : 1200 + (rawSpeed - 1200) * 0.5;
|
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
|
||||||
const spc = strategyPhaseCoeff[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,13 +145,184 @@ 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;
|
||||||
const r = (nr * nr) / (0.008405 * dpm * dpm);
|
const r = (nr * nr) / (0.008405 * dpm * dpm);
|
||||||
if (r > 1200) {
|
|
||||||
return Math.round(2 * r - 1200);
|
|
||||||
}
|
|
||||||
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 speed modifier for running uphill.
|
||||||
|
* Contrary to the race mechanics document, this is expressed as a negative number.
|
||||||
|
* @param powerStat Final power stat
|
||||||
|
* @param slopePer Slope percentage, generally one of 0.5, 1.0, 1.5, or 2.0
|
||||||
|
* @returns Speed modifier for running uphill, a negative value
|
||||||
|
*/
|
||||||
|
export function uphillMod(powerStat: number, slopePer: number): number {
|
||||||
|
return slopePer * -200/powerStat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 the probability of n of N skills activating.
|
||||||
|
* @param baseWit Base wit stat
|
||||||
|
* @param N Number of skills available, default 1
|
||||||
|
* @param n Number of skills activating, default 1
|
||||||
|
* @returns Probability of exactly n skills out of N passing wit checks
|
||||||
|
*/
|
||||||
|
export function skillWitCheck(baseWit: number, N?: number, n?: number): number {
|
||||||
|
const p = Math.max(0.2, 1 - 90/baseWit);
|
||||||
|
return binomPMF(p, N ?? 1, n ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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², or null for instant acceleration
|
||||||
|
* @param decel Current phase-based deceleration value in m/s², a negative value; or null for instant deceleration
|
||||||
|
* @param dur Duration of the boosted speed
|
||||||
|
* @returns Distance gained from the speed boost in m
|
||||||
|
*/
|
||||||
|
export function speedGain(speedBonus: number, dur: number, accel: number | null, decel: number | null): 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 = accel !== null ? speedBonus / accel : 0;
|
||||||
|
const decelTime = decel !== null ? -speedBonus / decel : 0;
|
||||||
|
if (accelTime >= dur) {
|
||||||
|
// Acceleration is so low that the horse won't reach the boosted target
|
||||||
|
// speed before the effect ends. E.g., G surface aptitude.
|
||||||
|
const peakSpeed = (accel ?? 0) * dur;
|
||||||
|
return 0.5 * (peakSpeed * dur - peakSpeed / (decel ?? 0));
|
||||||
|
}
|
||||||
|
// speedBonus*(dur-accelTime) + speedBonus*accelTime/2 + speedBonus*decelTime/2
|
||||||
|
return speedBonus * (dur + 0.5 * (decelTime - accelTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the chance to enter downhill accel mode each second while running downhill.
|
||||||
|
* @param witStat Final wit stat, including style aptitude modifier
|
||||||
|
* @returns Probability each eligible tick to enter downhill accel mode
|
||||||
|
*/
|
||||||
|
export function downhillAccelEnterChance(witStat: number): number {
|
||||||
|
return witStat * 0.0004;
|
||||||
|
}
|
||||||
|
|||||||
89
zenno/src/lib/runner.ts
Normal file
89
zenno/src/lib/runner.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Uma } from './data/uma';
|
||||||
|
import { AptitudeLevel, Mood, RunningStyle } from './race';
|
||||||
|
import * as alpha123Umalator from './alpha123Umalator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race runner, i.e. a trained horse.
|
||||||
|
*/
|
||||||
|
export interface Runner {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
chara_card_id: number;
|
||||||
|
style: RunningStyle;
|
||||||
|
mood: Mood;
|
||||||
|
|
||||||
|
speed: number;
|
||||||
|
stamina: number;
|
||||||
|
power: number;
|
||||||
|
guts: number;
|
||||||
|
wit: number;
|
||||||
|
|
||||||
|
sprint: AptitudeLevel;
|
||||||
|
mile: AptitudeLevel;
|
||||||
|
medium: AptitudeLevel;
|
||||||
|
long: AptitudeLevel;
|
||||||
|
front: AptitudeLevel;
|
||||||
|
pace: AptitudeLevel;
|
||||||
|
late: AptitudeLevel;
|
||||||
|
end: AptitudeLevel;
|
||||||
|
turf: AptitudeLevel;
|
||||||
|
dirt: AptitudeLevel;
|
||||||
|
|
||||||
|
skills: 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 newRunner(name?: string, base_uma?: Uma): Runner {
|
||||||
|
return {
|
||||||
|
name: name ?? '',
|
||||||
|
chara_card_id: base_uma?.chara_card_id ?? 0,
|
||||||
|
// TODO(zeph): default running style
|
||||||
|
style: RunningStyle.FrontRunner,
|
||||||
|
mood: Mood.Normal,
|
||||||
|
speed: 1200,
|
||||||
|
stamina: 1200,
|
||||||
|
power: 1200,
|
||||||
|
guts: 1200,
|
||||||
|
wit: 1200,
|
||||||
|
sprint: base_uma?.sprint ?? AptitudeLevel.A,
|
||||||
|
mile: base_uma?.mile ?? AptitudeLevel.A,
|
||||||
|
medium: base_uma?.medium ?? AptitudeLevel.A,
|
||||||
|
long: base_uma?.long ?? AptitudeLevel.A,
|
||||||
|
front: base_uma?.front ?? AptitudeLevel.A,
|
||||||
|
pace: base_uma?.pace ?? AptitudeLevel.A,
|
||||||
|
late: base_uma?.late ?? AptitudeLevel.A,
|
||||||
|
end: base_uma?.end ?? AptitudeLevel.A,
|
||||||
|
turf: base_uma?.turf ?? AptitudeLevel.A,
|
||||||
|
dirt: base_uma?.dirt ?? AptitudeLevel.A,
|
||||||
|
skills: base_uma != null ? [base_uma.unique] : [],
|
||||||
|
unique_level: 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 importRunner(obj: unknown, name?: string): Runner | null {
|
||||||
|
// TODO(zeph): check for keys that identify the uma source
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
try {
|
||||||
|
const r = alpha123Umalator.load(obj as alpha123Umalator.ImportUma, name);
|
||||||
|
// TODO(zeph): validate?
|
||||||
|
return r;
|
||||||
|
} catch (exc) {
|
||||||
|
console.warn('failed to import', obj, 'as alpha123 umalator:', exc);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn('no guess on how to import', obj);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -19,13 +19,16 @@
|
|||||||
<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>
|
||||||
<div class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
|
<div class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
<footer class="inset-x-0 bottom-0 mt-8 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900">
|
<footer
|
||||||
|
class="inset-x-0 bottom-0 mt-12 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900"
|
||||||
|
>
|
||||||
Umamusume: Pretty Derby tools by <a href="https://zephyrtronium.date/" target="_blank" rel="noopener noreferrer"
|
Umamusume: Pretty Derby tools by <a href="https://zephyrtronium.date/" target="_blank" rel="noopener noreferrer"
|
||||||
>zephyrtronium</a
|
>zephyrtronium</a
|
||||||
>.<br />
|
>.<br />
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
import type { ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||||
|
import { AptitudeLevel, downhillAccelEnterChance, moveLaneModifier, skillWitCheck, spotStruggleDuration, spotStruggleSpeed, Stat, uphillMod } from '$lib/race';
|
||||||
|
import Skill from '$lib/Skill.svelte';
|
||||||
|
import StatChart from '$lib/StatChart.svelte';
|
||||||
import Sec from '../Sec.svelte';
|
import Sec from '../Sec.svelte';
|
||||||
|
|
||||||
|
const witCheckSeries: ComputedSeries[] = [
|
||||||
|
{ label: "1/1", y: (x) => 100*skillWitCheck(x, 1, 1) },
|
||||||
|
{ label: "1/2 or 2/2", y: (x) => 100*(skillWitCheck(x, 2, 1) + skillWitCheck(x, 2, 2)) },
|
||||||
|
{ label: "2/2", y: (x) => 100*skillWitCheck(x, 2, 2) },
|
||||||
|
];
|
||||||
|
const ssBoostSeries: ComputedSeries = { label: "Target Speed Boost", y: (x) => spotStruggleSpeed(x) };
|
||||||
|
const ssDurSeries: ComputedSeries[] = [
|
||||||
|
{ label: "Front Runner S", y: (x) => spotStruggleDuration(x, AptitudeLevel.S) },
|
||||||
|
{ label: "Front Runner A", y: (x) => spotStruggleDuration(x, AptitudeLevel.A) },
|
||||||
|
];
|
||||||
|
const laneComboSeries: ComputedSeries = {label: "Target Speed Boost", y: (x) => moveLaneModifier(x) };
|
||||||
|
const lcYRule: HorizontalRule[] = [
|
||||||
|
{ label: "+0.35", y: 0.35 },
|
||||||
|
{ label: "+0.45", y: 0.45 },
|
||||||
|
];
|
||||||
|
const uphillSeries: ComputedSeries[] = [
|
||||||
|
{ label: "+2 Hill", y: (x) => uphillMod(x, 2.0) },
|
||||||
|
{ label: "+1.5 Hill", y: (x) => uphillMod(x, 1.5) },
|
||||||
|
{ label: "+1 Hill", y: (x) => uphillMod(x, 1.0) },
|
||||||
|
];
|
||||||
|
const uphillYRule: HorizontalRule[] = [
|
||||||
|
{ label: "Dominator", y: -0.25 },
|
||||||
|
];
|
||||||
|
const downhillSeries: ComputedSeries[] = [
|
||||||
|
{ label: "Style S", y: (x) => downhillAccelEnterChance(x * 1.1) * 100 },
|
||||||
|
{ label: "Style A", y: (x) => downhillAccelEnterChance(x) * 100 },
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article class="mx-auto max-w-4xl text-justify">
|
<article class="mx-auto max-w-4xl text-justify">
|
||||||
@@ -12,9 +44,10 @@
|
|||||||
<p>
|
<p>
|
||||||
This document is advanced material. The target audience intends to win Champions Meet Group A Finals and either wants to use
|
This document is advanced material. The target audience intends to win Champions Meet Group A Finals and either wants to use
|
||||||
front runners to do it or wants to understand what front runners they need to beat. This is meant for players who are already
|
front runners to do it or wants to understand what front runners they need to beat. This is meant for players who are already
|
||||||
strong at training: players who can take a target stat line and skill set and turn it into a horse. This is about what those
|
strong at training: players who can take a target stat line and skill set and turn it into a horse. This document is about the
|
||||||
stat lines and skill sets should be, along with <i>why</i>.
|
mechanics that determine what those stat lines and skill sets should be.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h="2" id="me">About Me</Sec>
|
<Sec h="2" id="me">About Me</Sec>
|
||||||
<p>
|
<p>
|
||||||
About three weeks after Global launched, my friend told me to get a job, so I sent him a screenshot of me clicking the install
|
About three weeks after Global launched, my friend told me to get a job, so I sent him a screenshot of me clicking the install
|
||||||
@@ -39,6 +72,7 @@
|
|||||||
rationalize running triple fronts for every CM even though it's not actually very good and most of my favorite horses are late
|
rationalize running triple fronts for every CM even though it's not actually very good and most of my favorite horses are late
|
||||||
surgers.
|
surgers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h="2" id="mechanics">Race Mechanics</Sec>
|
<Sec h="2" id="mechanics">Race Mechanics</Sec>
|
||||||
<p>
|
<p>
|
||||||
Very quick gloss of race fundamentals. Races are divided into four phases: early race, mid race, late race, and last spurt
|
Very quick gloss of race fundamentals. Races are divided into four phases: early race, mid race, late race, and last spurt
|
||||||
@@ -49,68 +83,78 @@
|
|||||||
<p>
|
<p>
|
||||||
The numeric value of acceleration depends on the Power stat, dueling, surface aptitude, uphills, race phase, running style. At
|
The numeric value of acceleration depends on the Power stat, dueling, surface aptitude, uphills, race phase, running style. At
|
||||||
the start of early race, horses accelerate from 3 m/s to the early race <i>base target speed</i>, which varies by race
|
the start of early race, horses accelerate from 3 m/s to the early race <i>base target speed</i>, which varies by race
|
||||||
distance and running style. At the start of late race, if they have enough HP remaining for their last spurt, horses
|
distance and running style but is generally on the order of 20 m/s. At the start of late race, if they have enough HP remaining for their last spurt, horses
|
||||||
accelerate from the mid race base target speed to their spurt speed, which varies by speed stat, distance aptitude, race
|
accelerate from the mid race base target speed to their spurt speed, which varies by speed stat, distance aptitude, running style, race
|
||||||
distance, running style, and guts stat, in decreasing order of effect. "Last spurt" and "last spurt phase" are different and
|
distance, and guts stat, in decreasing order of effect. "Last spurt" and "last spurt phase" are different and
|
||||||
unrelated things; the latter is only used in the condition for Homestretch Haste.
|
unrelated things; the latter is only used in the condition for <Skill skill={200512} hint="homestretch haste" mention />.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Speed skills add a flat amount of target speed, generally +0.15 m/s for white skills, +0.25 m/s for double circle skills and
|
Speed skills add a flat amount of target speed, generally +0.15 m/s for white skills, +0.25 m/s for double circle skills and
|
||||||
some inherited uniques, +0.35 m/s for gold skills and most speed uniques, and +0.45 m/s for a handful of speed uniques. Accel
|
some inherited uniques, +0.35 m/s for gold skills and most speed uniques, and +0.45 m/s for a handful of speed uniques. Accel
|
||||||
skills similarly add a flat amount of acceleration, either +0.1 or +0.2 m/s² for white skills and inherited uniques, or +0.3
|
skills similarly add a flat amount of acceleration, typically +0.1 or +0.2 m/s² for white skills and inherited uniques, or +0.3
|
||||||
or +0.4 m/s² for gold skills and uniques.
|
or +0.4 m/s² for gold skills and uniques.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
Generally speaking, competitive races are won by the horse who gets the most acceleration at the start of late race. The
|
|
||||||
consistency of front runner accel skills is what makes it a viable running style.
|
|
||||||
</p>
|
|
||||||
<Sec h="3" id="runaway">Runaway</Sec>
|
<Sec h="3" id="runaway">Runaway</Sec>
|
||||||
<p>
|
<p>
|
||||||
The skill <b>Runaway</b> converts front runners into the <i>Great Escape</i> running style. However, no player has ever uttered
|
The skill <Skill skill={202051} hint="runaway" /> converts front runners into the <i>Great Escape</i> running style. However, no player has ever uttered
|
||||||
the words "Great Escape" when talking about Umamusume, presumably because Runaway is a much cooler name.
|
the words "Great Escape" when talking about Umamusume, presumably because Runaway is a much cooler name.
|
||||||
|
("Great Escape" is a direct translation of Japanese 大逃げ <i>oonige</i>, whereas "Front Runner" is a more liberal localization of 逃げ <i>nige</i> that technically just means "escape.")
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Runaways are still front runners for most purposes, the main difference just being different base target speeds per phase.
|
Runaways are still front runners for all purposes.
|
||||||
|
The main difference is just different numbers for things like base speed and acceleration, stamina to HP conversion, and distance thresholds for running modes.
|
||||||
Other mechanics that are specific to front runners also apply to runaways.
|
Other mechanics that are specific to front runners also apply to runaways.
|
||||||
</p>
|
</p>
|
||||||
<Sec h="3" id="spot-struggle">Spot Struggle</Sec>
|
|
||||||
|
<Sec h="2" id="win-cons">Win Conditions</Sec>
|
||||||
<p>
|
<p>
|
||||||
For each of runaways and non-runaways, there is at most one spot struggle per race. Runaways will not spot struggle with
|
On Global today, competitive horses usually have stat lines that are pretty similar to each other.
|
||||||
non-runaways, nor vice-versa. When a spot struggle triggers, all front runnners of that type within range participate; I've
|
Races, therefore, are more often won by skills – typically acceleration skills that activate at the start of late race.
|
||||||
had a horse join while in 6th.
|
Front runners have strong options.
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-4 mb-4">
|
||||||
|
<li>
|
||||||
|
<Skill skill={900201} hint="angling" />, sometimes called Rod, is the second best skill in the game.
|
||||||
|
Because only the horse in first place gets it, everything about training front runners becomes a matter of being in front at the start of late race, true to name.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
On long distance tracks, <Skill skill={900681} hint="vc" /> takes that role instead.
|
||||||
|
The front two horses get it, which opens the opportunity for multi-front builds using <Skill skill={200492} hint="nn" mention />/<Skill skill={200491} hint="nsm" mention /> –
|
||||||
|
especially because VC tracks aren't subject to the final corner spread that makes those skills worse on sprints and miles –
|
||||||
|
but otherwise the function is the same.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
On those sprints where Angling is dead, the front-specific options include <Skill skill={900141} hint="pasta" /> (VPP, or Pasta) and <Skill skill={910451} hint="mummy creek" /> (HCreek),
|
||||||
|
It takes both of them to equal Angling, so such sprints may be better served gambling on <Skill skill={200651} hint="turbo sprint" mention />, <Skill skill={200371} hint="rushing gale" mention />, and possibly <Skill skill={200551} hint="unrestrained" mention /> instead.
|
||||||
|
Front runners are especially strong on sprints for <a href="#spot-struggle">other reasons</a> anyway.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<Skill skill={200491} hint="nsm" /> is the best skill in the game.
|
||||||
|
Unfortunately, for the most part, it's bad on front runners; generally not a win condition.
|
||||||
|
Activating NSM requires not being in first, which means whoever <i>was</i> used Angling and is pulling away from you before you accumulate the blocked time to activate it.
|
||||||
|
Again, VC tracks may be an exception if you specifically build for it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="2" id="pdm">Pace Down Mode</Sec>
|
||||||
|
<p>
|
||||||
|
During the first 41.67% of the race, <i>position keep</i> is busy arranging each running style into their respective packs.
|
||||||
|
The primary mechanism for this is pace down mode (PDM), which activates whenever a horse gets what their style defines as too close to first place.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Spot struggle provides a target speed bonus that scales with the guts stat. If it isn't cut short, which will approximately
|
Watch a MANT late surger with 1000+ power and wit in a daily legend race.
|
||||||
never happen, its duration scales with the guts stat. Unlike skills, its duration <i>does not</i> scale with race distance.
|
As long as they don't get blocked, they should <a href="#section-speed">slide forward</a> throughout the early race.
|
||||||
|
Then, around when they reach the pace chaser pack, they'll suddenly start moonwalking back to the rest of the late surgers, often near the back of the group.
|
||||||
|
That's PDM.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Spot struggle also greatly increases HP consumption.
|
On lesser running styles, early race and sometimes mid race speed skills are effectively converted from distance gain into HP conservation via PDM.
|
||||||
For normal front runners, the rate is slightly less than Rushed.
|
The thing that really makes front runners good is that they don't have to worry about that – they aren't subject to PDM at all.
|
||||||
For runaways, it's more than double Rushed. (This is the reason people say you can't get enough stamina for runaways on Global.)
|
Their mid race speed skills always gain distance.
|
||||||
Actually getting Rushed during spot struggle dramatically increases HP consumption, much more than just adding them together; red-light green-light pretty much guarantees that horse won't spurt.
|
|
||||||
</p>
|
</p>
|
||||||
<Sec h="3" id="position-keep">Position Keep</Sec>
|
|
||||||
<p>Position Keep is the process by which pace chasers don't pass front runners in the mid race.</p>
|
<Sec h="2" id="skill-timing">Skill Timing</Sec>
|
||||||
<p>
|
|
||||||
During Position Keep, the frontmost front runner of each type uses <i>speed up mode</i> to try to stay at least 4.5m (a length
|
|
||||||
is 2.5m) ahead of second place, and other front runners use <i>overtake mode</i> to try to become the frontmost. Both of these modes
|
|
||||||
take wit checks to enter and apply a target speed bonus for their duration; overtake mode is slightly better than speed up mode.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Lesser running styles instead have <i>pace up mode</i> and <i>pace down mode</i>. Whether horses enter these is based on their
|
|
||||||
distance from the frontmost horse. Pace up also requires a wit check, but PDM doesn't – if a non-front horse gets too close to
|
|
||||||
the frontmost runner in the first 41.67% of the race, they are guaranteed to switch into PDM, which converts distance into HP.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Watch a MANT late surger with 1000+ power and wit in a solo practice match. When you see her advance forward early but then
|
|
||||||
moonwalk for two seconds to the back of the pack, that's PDM.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Converting distance into HP is quite a lot worse than just having enough HP. The fact that front runners don't have to worry
|
|
||||||
about PDM is one of their major strengths. In particular, it means that early race speed skills always gain distance for front
|
|
||||||
runners, which is not the case for the inferior styles.
|
|
||||||
</p>
|
|
||||||
<Sec h="3" id="skill-timing">Skill Timing</Sec>
|
|
||||||
<p>Thought experiment.</p>
|
<p>Thought experiment.</p>
|
||||||
<p>
|
<p>
|
||||||
Picture two cars driving on a straight freeway, both at exactly 59 mph because I am American, adjacent lanes, keeping exactly
|
Picture two cars driving on a straight freeway, both at exactly 59 mph because I am American, adjacent lanes, keeping exactly
|
||||||
@@ -143,37 +187,163 @@
|
|||||||
This thought experiment shows that speed skills are actually more valuable before late race than during it. Thus, front
|
This thought experiment shows that speed skills are actually more valuable before late race than during it. Thus, front
|
||||||
runners not having to worry about PDM is even more of an advantage.
|
runners not having to worry about PDM is even more of an advantage.
|
||||||
</p>
|
</p>
|
||||||
<Sec h="3" id="skill-stacking">Skill Stacking</Sec>
|
|
||||||
|
<Sec h="2" id="gate-skills">Gate Skills</Sec>
|
||||||
<p>
|
<p>
|
||||||
In a void, the fact that skill effects stack doesn't change the total distance you get from them.
|
Gate skills are <Skill skill={201601} hint="gw" /> (GW), <Skill skill={200531} hint="ttl" /> (TTL), and <Skill skill={200431} hint="conc" /> (Conc), as well as all green skills including <Skill skill={202051} hint="runaway" mention />.
|
||||||
However, getting multiple to activate at the same time drastically improves a front runner's ability to overtake.
|
These skills activate the moment the race starts.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The practical consequence of this is that <b>Tail Held High</b> and, for multi-front builds, <b>Ramp Up</b> are extremely good skills for front runners.
|
GW is an absolutely mandatory skill for all front runners.
|
||||||
THH has a long duration and will pretty much always trigger off another speed skill, pushing your horse far forward.
|
Even runaway blockers should have it, otherwise they will be passed by the normal fronts they're trying to block.
|
||||||
Ramp activates upon overtake mid-race, which can turn a lucky order change into a full pass.
|
It requires three other gate skills, which should be active greens to avoid overreliance on wit checks.
|
||||||
|
For reference, the chart below shows proc chances of one of one, one of two, or two of two skills with wit checks.
|
||||||
</p>
|
</p>
|
||||||
<Sec h="3" id="duels">Duels</Sec>
|
<div class="w-full h-60 md:h-96 mb-4">
|
||||||
|
<StatChart class="h-full w-full max-w-3xl mx-auto" stat={Stat.Wit} y={witCheckSeries} yLabel="% Chance" range={[50, 100]} xRule={1000} />
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Aside from spot struggle, the other mechanic that tries to make guts a stat that matters is dueling.
|
TTL must be combined with GW if they want any chance of being first out of early race.
|
||||||
This requires two horses on the final straight to sustain a certain distance both along and across the track, and to be close in actual speed.
|
Since the main source of it is the Mihono Bourbon Wit SSR from the first Halloween event, VBourbon can suffice with its white version <Skill skill={200532} hint="early lead" mention /> and get to the front with her unique instead.
|
||||||
There is some evidence that three horses might be able to enter a single duel, although I am not certain this is confirmed.
|
(Her other option is the Twin Turbo SSR that does generate a lot of stats but requires winning three 50/50s to get the gold skill.)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Duels give a substantial bonus to target speed, a gold speed skill's worth at only 500 guts, for the entire duration of the final straight.
|
Conc is less critical.
|
||||||
(Also a bit of acceleration, but this rarely matters.)
|
It's worth taking on horses who have it, but it isn't worth using support card slots just to get it.
|
||||||
|
On the other hand, its white version <Skill skill={200432} hint="focus" /> is bad; its only real use is as a backup gate skill for GW when you don't have enough greens available.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="2" id="spot-struggle">Spot Struggle</Sec>
|
||||||
|
<p>
|
||||||
|
For each of runaways and non-runaways, there is at most one spot struggle per race. Runaways will not spot struggle with
|
||||||
|
non-runaways, nor vice-versa. When a spot struggle triggers, all front runnners of that type within range participate; I've
|
||||||
|
had a horse join while in 6th a couple times.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Unfortunately, winning front runners almost never get duels.
|
Spot struggle provides a target speed bonus that scales with the guts stat. If it isn't cut short, which will approximately
|
||||||
After Angling, no one else will be nearly close enough to trigger; other fronts are left behind, and other styles are still catching up.
|
never happen, its duration also scales with the guts stat. Unlike skills, its duration <i>does not</i> scale with race distance.
|
||||||
On tracks like Tokyo 1600 where Angling is right before the final straight, the problem instead becomes width, since horses spread out on the final corner.
|
</p>
|
||||||
|
<div class="grid w-full h-60 md:h-96 grid-cols-2 mb-4">
|
||||||
|
<StatChart stat={Stat.Guts} y={ssBoostSeries} yLabel="Speed Bonus (m/s)" range={[0, 0.3]} />
|
||||||
|
<StatChart stat={Stat.Guts} y={ssDurSeries} yLabel="Duration (s)" range={[0, 12]} />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Spot struggle also greatly increases HP consumption.
|
||||||
|
For normal front runners, the rate is slightly less than Rushed.
|
||||||
|
For runaways, it's more than double Rushed. (This is the reason people say you can't get enough stamina for runaways on Global.)
|
||||||
|
Actually getting Rushed during spot struggle dramatically increases HP consumption, much more than just adding them together; red-light green-light pretty much guarantees that horse won't spurt.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For CM12 Aries Cup (Satsuki Sho), I used a guts/wit NSM Suzuka whose goal was to be in third place at late race start, trigger NSM to stick with the Angling user, and duel with high guts to finish out the race.
|
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than other styles.
|
||||||
This plan never materialized.
|
At 1600m and shorter, the fact that Spot Struggle doesn't scale with race distance means that it can be worth multiple gold speed skills in total distance gained.
|
||||||
I did get one win off the duel itself when it enabled Suzuka to pass back a pace chaser in the closest win I had in the whole event;
|
See the <a href={resolve('/mspeed')}>mechanical speed calculator</a> for precise analysis.
|
||||||
but one win in eighty races is not a great record.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<Sec h="2" id="lane-combo">Lane Combo</Sec>
|
||||||
|
<p>
|
||||||
|
While under the influence of a skill that increases lane movement speed (shoe icon skills), and while actively changing lanes (i.e. moving sideways), horses gain a (forward) target speed boost that scales with power.
|
||||||
|
This was a change Global received with the Unity Cup scenario.
|
||||||
|
</p>
|
||||||
|
<div class="w-full h-60 md:h-96 mb-4">
|
||||||
|
<StatChart class="h-full w-full max-w-3xl mx-auto" stat={Stat.Power} y={laneComboSeries} yLabel="Speed Boost" yRule={lcYRule} range={[0.2, 0.5]} />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Front runners have access to the skill <Skill skill={201262} hint="dd" />, which forces a horse who uses it to move outward to a specific distance from the rail.
|
||||||
|
DD almost always ends shortly before the horse has finished accelerating to early race speed, so it does not convert the move lane speed modifier into distance.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We get advantage from move lane speed modifier by following DD with <Skill skill={200452} hint="pp" /> or <Skill skill={210052} hint="ignited wit" />.
|
||||||
|
DD created an opportunity for those return skills to convert into huge forward speed.
|
||||||
|
This setup is called <i>lane combo</i>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Lane combo is only viable on tracks where early race ends before or at most very early into the first corner.
|
||||||
|
Since PP and Ignited WIT are <span class="font-mono">phase_random==0</span> skills, they can activate at the very end of late race.
|
||||||
|
If there's a corner there, and your horse is still on the outside from DD, you are now physically running a longer distance than those on the inside.
|
||||||
|
That can more than undo the gain from the lane combo itself.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The <a href={resolve('/mspeed')}>mechanical speed calculator</a> has an approximation of lane combo's benefit.
|
||||||
|
A more precise lane combo simulator <a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">exists</a>,
|
||||||
|
but I am not sufficiently confident in my Japanese to try to guide readers through it.
|
||||||
|
<!-- TODO(zeph): i could totally annotate a picture though -->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="2" id="slopes">Slopes</Sec>
|
||||||
|
<p>
|
||||||
|
Different slopes can be of different angles; the <i>SlopePer</i> parameter is positive for uphills and negative for downhills.
|
||||||
|
SlopePer values that currently exist on tracks include 1, 1.5, and 2, positive or negative.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="3" id="uphills">Uphills</Sec>
|
||||||
|
<p>
|
||||||
|
Running uphill carries a penalty to target speed.
|
||||||
|
This penalty scales negatively with the power stat; that is, higher power means faster uphill running.
|
||||||
|
It scales positively with slope angle.
|
||||||
|
</p>
|
||||||
|
<div class="w-full h-60 md:h-96 mb-4">
|
||||||
|
<StatChart class="w-full max-w-3xl h-full mx-auto" stat={Stat.Power} y={uphillSeries} yLabel="Speed Modifier (m/s)" yRule={uphillYRule} range={[-2, 0]} />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Note that surface aptitude <i>does not</i> affect uphill speed, nor power generally.
|
||||||
|
It only affects acceleration.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The practical impact is that steep early- and mid-race hills filter out front runners with low power.
|
||||||
|
Even with an otherwise perfect build, an 800 power VBourbon is likely to be passed by a 1280 power (<Skill skill={200152} hint="firm" mention /> + <Skill skill={200282} hint="comp spirit" mention />) Seiun Sky.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="3" id="downhills">Downhills</Sec>
|
||||||
|
<p>
|
||||||
|
Running downhill allows horses to enter <i>downhill accel mode</i>.
|
||||||
|
Contrary to its name, downhill accel mode does not affect acceleration at all;
|
||||||
|
it gives horses a target speed boost that scales with the slope angle, plus lowered HP consumption via a flat multiplier.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Entering downhill accel mode requires passing a wit check.
|
||||||
|
The success rate scales linearly with wit.
|
||||||
|
Style aptitude <i>does</i> affect the chance to pass the check.
|
||||||
|
Its duration is random with a geometric distribution; it does not scale with stats.
|
||||||
|
</p>
|
||||||
|
<div class="w-full h-60 md:h-96 mb-4">
|
||||||
|
<StatChart class="w-full max-w-3xl h-full mx-auto" stat={Stat.Wit} y={downhillSeries} yLabel="Entry Chance (% each second)" range={[0, 60]} />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Similar to uphills disproportionately rewarding front runners with higher power, downhills tend to reward high wit.
|
||||||
|
However, the random elements of downhill accel mode mean that lower wit horses may still keep up on downhills, depending on luck.
|
||||||
|
Conversely, the HP savings on long downhills can be enough to drop a recovery skill or two on some tracks.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="2" id="section-speed">Section Speed</Sec>
|
||||||
|
<p>
|
||||||
|
Each section, each horse gets a random modifier to target speed.
|
||||||
|
The modifier's range is determined by the wit stat.
|
||||||
|
(Curiously, the calculation uses both wit as modified by style proficiency and green skills as well as base wit.)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Section speed is generally very small; at 1200 wit with style S, it has a range of about -0.15% to 0.5% of race base speed.
|
||||||
|
At 2000m, that translates to an actual speed range of 19.97 to 20.10 m/s.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Unlike anything affected by the speed stat, though, it applies during the early and mid race, where front runners are trying to become frontest runners.
|
||||||
|
Wit difference alone can
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h="2" id="phase-speed">Phase Speed</Sec>
|
||||||
|
<p>
|
||||||
|
Race base speed is multiplied by the strategy–phase coefficient for each horse.
|
||||||
|
As the name suggests, SPC is different per running style and per race phase.
|
||||||
|
It's the thing that makes runaways take off in early race, and the thing that makes pace chaser promotion scary in late race (for those not using any of the correct running style).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Front runners, and even moreso runaways, have particularly punishing SPC for late race.
|
||||||
|
This makes sense; if they weren't forced to be substantially slower than the late surgers they're thirty meters ahead of at late race start, then they would be guaranteed to win every time.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Late race, or more precisely the last spurt, is also the only place where the speed stat and distance aptitude apply.
|
||||||
|
In terms of lengths gained, distance S actually does more for front runners than any other style due to SPC.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h="2" id="stats">Stats</Sec>
|
<Sec h="2" id="stats">Stats</Sec>
|
||||||
<Sec h="3" id="speed">Speed</Sec>
|
<Sec h="3" id="speed">Speed</Sec>
|
||||||
<!-- speed matters less than for other styles; corollary: distance S matters less -->
|
<!-- speed matters less than for other styles; corollary: distance S matters less -->
|
||||||
@@ -184,39 +354,6 @@
|
|||||||
<Sec h="3" id="wit">Wit</Sec>
|
<Sec h="3" id="wit">Wit</Sec>
|
||||||
<!-- position keep; front S -->
|
<!-- position keep; front S -->
|
||||||
<Sec h="2" id="skills">Skills</Sec>
|
<Sec h="2" id="skills">Skills</Sec>
|
||||||
<Sec h="3" id="win-cons">Win Conditions</Sec>
|
|
||||||
<p>
|
|
||||||
On all medium races, almost every mile, many sprints, and some longs, <b>Angling and Scheming</b> is the second strongest single
|
|
||||||
skill currently in the game. Any time late race starts on (or, in the case of Nakayama 2500, very shortly before) a corner, Angling
|
|
||||||
is how front runners win. This means that Seiun Sky is the most important horse to front runner trainers. Everything about training
|
|
||||||
a front runner is for the sake of improving the odds of being the horse to trigger Angling – because only the horse in first gets
|
|
||||||
it.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Long races get a different primary win condition: <b>Victory Cheer!</b> from Kitasan Black. VC is more forgiving than Angling, activating
|
|
||||||
for both first and second place, but it's also weaker.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The sprints where Angling is dead can try to work with <b>Victoria por plancha ☆</b> (Pasta) and <b>Give Mummy a Hug ♡</b>
|
|
||||||
(Mummy Creek) instead. These skills are also on the weaker side, so front running becomes more of a matter of gambling with skills
|
|
||||||
like Turbo Sprint, Rushing Gale, and Unrestrained. The closest anyone has to not-gambling on such sprints is Nishino Flower's unique,
|
|
||||||
though, and front runners are especially strong in sprints for <a href="#spot-struggle">other reasons</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If Angling is the second strongest skill, then the strongest is <b>No Stopping Me!</b>
|
|
||||||
Unfortunately, in most races, it is generally not a front runner skill. Because only one horse gets Angling, they will pull away
|
|
||||||
from whoever is in second, not giving a chance to activate NSM. For NSM to win on a front, the horse who got Angling has to not
|
|
||||||
be Seiun Sky (since Sei's own Angling is equal to NSM in strength), and there has to be no gap in the front pack so that it can
|
|
||||||
trigger. As I write this, I am 80 races into CM12 using a Silence Suzuka who has NSM, and it has been involved in exactly one win
|
|
||||||
where she leapfrogged off (my) Sei to pass (my) VBourbon (who placed second). It was cool, but she would have been better off in
|
|
||||||
the event if she hadn't had Yukino Wit in her deck.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The above paragraph notwithstanding, NSM and even its white version, <b>Nimble Navigator</b>, are a decent choice for
|
|
||||||
multi-front builds on VC races. Since the top two horses both get VC, and the accel from it is weaker, having an extra push
|
|
||||||
for your second front is strong. VC maps also don't suffer from the final corner spread that makes NN/NSM hard to trigger on
|
|
||||||
sprints and miles.
|
|
||||||
</p>
|
|
||||||
<Sec h="3" id="gate-skills">Gate Skills</Sec>
|
<Sec h="3" id="gate-skills">Gate Skills</Sec>
|
||||||
<!-- gw, ttl, conc -->
|
<!-- gw, ttl, conc -->
|
||||||
<Sec h="3" id="lane-combo">Lane Combo</Sec>
|
<Sec h="3" id="lane-combo">Lane Combo</Sec>
|
||||||
|
|||||||
181
zenno/src/routes/mspeed/+page.svelte
Normal file
181
zenno/src/routes/mspeed/+page.svelte
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<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), spotStruggleDuration(x, AptitudeLevel.S), accel[0], decel[0]) / HORSE_LENGTH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Aptitude A',
|
||||||
|
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, AptitudeLevel.A), accel[0], decel[0]) / HORSE_LENGTH,
|
||||||
|
},
|
||||||
|
frontApt < AptitudeLevel.A
|
||||||
|
? {
|
||||||
|
label: `Aptitude ${AptitudeLevel[frontApt]}`,
|
||||||
|
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, frontApt), accel[0], decel[0]) / HORSE_LENGTH,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
]);
|
||||||
|
const lcY: Array<ComputedSeries | null> = $derived([
|
||||||
|
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), lcDur, accel[0], decel[0]) / HORSE_LENGTH },
|
||||||
|
]);
|
||||||
|
const pcRuler = $derived({ y: speedGain(0.35, skillDuration(2.4, raceLen), accel[1], decel[1]) / HORSE_LENGTH, label: 'Professor of Curvature' });
|
||||||
|
const ssYRule = $derived([
|
||||||
|
pcRuler,
|
||||||
|
{ y: speedGain(lcBoost, lcDur, accel[0], decel[0]) / HORSE_LENGTH, label: 'Lane Combo' },
|
||||||
|
]);
|
||||||
|
const lcYRule = $derived([
|
||||||
|
pcRuler,
|
||||||
|
{ y: speedGain(ssBoost, ssDur, accel[0], decel[1]) / HORSE_LENGTH, label: 'Spot Struggle' },
|
||||||
|
]);
|
||||||
|
</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 min="1000" max="3600" step="100" 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 (DDPP)</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 h-60 py-4 md:h-96 md:w-3xl">
|
||||||
|
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={gutsStat} yRule={ssYRule} />
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mt-4 h-60 py-4 md:mt-0 md:h-96 md:w-3xl">
|
||||||
|
<StatChart class="flex-1" stat={Stat.Power} y={lcY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={powerStat} yRule={lcYRule} />
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mt-12 w-full max-w-4xl border-t md:mt-8">
|
||||||
|
<ul class="ml-4 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, the horse is never blocked, and the horse returns to the rail in early race before the first corner.
|
||||||
|
<ul class="ml-8 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, dur, accel, d) / 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">{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>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AptitudeLevel, inverseSpurtSpeed, RunningStyle, spurtSpeed } from '$lib/race';
|
import type { ComputedSeries } from '$lib/chart';
|
||||||
|
import { AptitudeLevel, inverseSpurtSpeed, RunningStyle, spurtSpeed, Stat } from '$lib/race';
|
||||||
|
import StatChart from '$lib/StatChart.svelte';
|
||||||
|
|
||||||
const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number');
|
const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number');
|
||||||
const stylesList = [
|
const stylesList = [
|
||||||
@@ -41,6 +43,19 @@
|
|||||||
const skillProf = $derived(
|
const skillProf = $derived(
|
||||||
skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, AptitudeLevel.S, raceLen) - careerMod]),
|
skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, AptitudeLevel.S, raceLen) - careerMod]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const y: Array<ComputedSeries | null> = $derived([
|
||||||
|
{ label: 'Aptitude S', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.S, raceLen) },
|
||||||
|
{ label: 'Aptitude A', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.A, raceLen) },
|
||||||
|
distanceApt < AptitudeLevel.A
|
||||||
|
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) }
|
||||||
|
: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const range: [number, number] = $derived([
|
||||||
|
spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen),
|
||||||
|
spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen),
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-4xl">Spurt Speed Calculator</h1>
|
<h1 class="text-4xl">Spurt Speed Calculator</h1>
|
||||||
@@ -71,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="m-4 md:col-start-2">
|
<div class="m-4 md:col-start-2">
|
||||||
<label for="raceLen">Race Distance</label>
|
<label for="raceLen">Race Distance</label>
|
||||||
<input type="number" id="raceLen" required bind:value={raceLen} class="w-full" />
|
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="m-4 self-center">
|
<div class="m-4 self-center">
|
||||||
<label for="isCareer" class="mr-1 align-middle">In Career</label>
|
<label for="isCareer" class="mr-1 align-middle">In Career</label>
|
||||||
@@ -121,3 +136,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mx-auto h-60 max-w-3xl place-content-center py-4 md:h-96">
|
||||||
|
<StatChart stat={Stat.Speed} {y} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} />
|
||||||
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user