Compare commits
13 Commits
9ce665865e
...
1d879cbf9f
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d879cbf9f | |||
| 240a3d9a73 | |||
| 3d73460934 | |||
| 0c518a9ce2 | |||
| 5cbb6d7b1d | |||
| b0bd16fd04 | |||
| 55175c9c40 | |||
| 06e5a06680 | |||
| dbb0dcd2dc | |||
| 02f723b83a | |||
| 1f58dec491 | |||
| a6d264c79d | |||
| d62e729052 |
@@ -55,39 +55,39 @@
|
|||||||
let width = $state(0);
|
let width = $state(0);
|
||||||
let height = $state(0);
|
let height = $state(0);
|
||||||
|
|
||||||
let expand = $state(false);
|
const xLabel = $derived(`Raw ${Stat[stat]}`);
|
||||||
|
|
||||||
const xLabel = $derived(Stat[stat]);
|
|
||||||
const xLines = $derived([xRule].flat(1));
|
const xLines = $derived([xRule].flat(1));
|
||||||
const xMin = $derived(xRange?.[0] ?? 200);
|
const xMin = $derived(xRange?.[0] ?? 200);
|
||||||
const xMax = $derived.by(() => {
|
const xMax = $derived.by(() => {
|
||||||
if (xRange?.[1] != null) {
|
if (xRange?.[1] != null) {
|
||||||
return xRange[1];
|
return xRange[1];
|
||||||
}
|
}
|
||||||
if (expand) {
|
return 100 * Math.ceil(Math.max(2000, ...xLines) / 100);
|
||||||
return 2000;
|
|
||||||
}
|
|
||||||
return 100 * Math.ceil(Math.max(1200, ...xLines) / 100);
|
|
||||||
});
|
});
|
||||||
const xVal = $derived(d3.range(xMin, xMax + 1, 5)); // +1 because d3.range does not include the max
|
const xVal = $derived(d3.range(xMin, xMax + 1, 5)); // +1 because d3.range does not include the max
|
||||||
const thrX = 1200;
|
const thrX = 1200;
|
||||||
|
|
||||||
|
function adj(x: number): number {
|
||||||
|
return Math.min(x, 1200) + 0.5 * (Math.max(x, 1200) - 1200);
|
||||||
|
}
|
||||||
const series = $derived([y].flat(1).filter((s) => s != null));
|
const series = $derived([y].flat(1).filter((s) => s != null));
|
||||||
const areas = $derived([yArea].flat(1).filter((s) => s != null));
|
const areas = $derived([yArea].flat(1).filter((s) => s != null));
|
||||||
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(adj(x), x), label }))));
|
||||||
const areaVals = $derived(areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(x), y2: y2(x), label }))));
|
const areaVals = $derived(
|
||||||
|
areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(adj(x), x), y2: y2(adj(x), x), label }))),
|
||||||
|
);
|
||||||
const yRange: [number, number] = $derived.by(() => {
|
const yRange: [number, number] = $derived.by(() => {
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
if (range.length === 2) {
|
if (range.length === 2) {
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
return [range[0], expand ? range[2] : range[1]];
|
return [range[0], range[2]];
|
||||||
}
|
}
|
||||||
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];
|
||||||
});
|
});
|
||||||
const yLines = $derived(xLines.flatMap((x) => series.map(({ y, label }) => ({ y: y(x), label }))));
|
const yLines = $derived(xLines.flatMap((x) => series.map(({ y, label }) => ({ y: y(adj(x), x), label }))));
|
||||||
|
|
||||||
const makeChart: Attachment = (el) => {
|
const makeChart: Attachment = (el) => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export interface ComputedSeries {
|
export interface ComputedSeries {
|
||||||
y: (x: number) => number;
|
y: (base: number, raw: number) => number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputedAreas {
|
export interface ComputedAreas {
|
||||||
y1: (x: number) => number;
|
y1: (base: number, raw: number) => number;
|
||||||
y2: (x: number) => number;
|
y2: (base: number, raw: number) => number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+100
-6
@@ -28,22 +28,116 @@ export interface Spark {
|
|||||||
* Spark type.
|
* Spark type.
|
||||||
* Roughly the spark color, with extra subdivisions for white sparks.
|
* Roughly the spark color, with extra subdivisions for white sparks.
|
||||||
*/
|
*/
|
||||||
type: 1 | 2 | 5 | 4 | 6 | 7 | 10 | 8 | 11 | 9 | 3;
|
type: Type;
|
||||||
/**
|
/**
|
||||||
* Possible effects applied by the spark during inspiration.
|
* Possible effects applied by the spark during inspiration.
|
||||||
* A random element is selected from this list according to unknown
|
* A random element is selected from this list according to unknown
|
||||||
* distributions, then all effects in that selection are applied.
|
* distributions, then all effects in that selection are applied.
|
||||||
*/
|
*/
|
||||||
effects: SparkEffect[][];
|
effects: Effect[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spark types.
|
||||||
|
* Roughly the spark color, with extra subdivisions for white sparks.
|
||||||
|
*/
|
||||||
|
export enum Type {
|
||||||
|
Stat = 1,
|
||||||
|
Aptitude = 2,
|
||||||
|
Unique = 3,
|
||||||
|
Skill = 4,
|
||||||
|
Race = 5,
|
||||||
|
Scenario = 6,
|
||||||
|
CarnivalBonus = 7,
|
||||||
|
Distance = 8,
|
||||||
|
Hidden = 9,
|
||||||
|
Surface = 10,
|
||||||
|
Style = 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mapping of spark types to their names. */
|
||||||
|
export const TYPE_NAMES: Readonly<Record<Type, string>> = {
|
||||||
|
[Type.Stat]: 'Stat (Blue)',
|
||||||
|
[Type.Aptitude]: 'Aptitude (Pink)',
|
||||||
|
[Type.Unique]: 'Unique (Green)',
|
||||||
|
[Type.Skill]: 'Skill',
|
||||||
|
[Type.Race]: 'Race',
|
||||||
|
[Type.Scenario]: 'Scenario',
|
||||||
|
[Type.CarnivalBonus]: 'Carnival Bonus (Scarlet)',
|
||||||
|
[Type.Distance]: 'Distance (White)',
|
||||||
|
[Type.Hidden]: 'Hidden (White)',
|
||||||
|
[Type.Surface]: 'Surface (White)',
|
||||||
|
[Type.Style]: 'Style (White)',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spark effect types.
|
||||||
|
*/
|
||||||
|
export enum Target {
|
||||||
|
Speed = 1,
|
||||||
|
Stamina = 2,
|
||||||
|
Power = 3,
|
||||||
|
Guts = 4,
|
||||||
|
Wit = 5,
|
||||||
|
SkillPoints = 6,
|
||||||
|
RandomStat = 7,
|
||||||
|
Turf = 11,
|
||||||
|
Dirt = 12,
|
||||||
|
FrontRunner = 21,
|
||||||
|
PaceChaser = 22,
|
||||||
|
LateSurger = 23,
|
||||||
|
EndCloser = 24,
|
||||||
|
Sprint = 31,
|
||||||
|
Mile = 32,
|
||||||
|
Medium = 33,
|
||||||
|
Long = 34,
|
||||||
|
Skill = 41,
|
||||||
|
CarnivalBonus = 51,
|
||||||
|
SpeedCap = 61,
|
||||||
|
StaminaCap = 62,
|
||||||
|
PowerCap = 63,
|
||||||
|
GutsCap = 64,
|
||||||
|
WitCap = 65,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of spark target types to their names.
|
||||||
|
* Note that Skill does not include the skill name.
|
||||||
|
*/
|
||||||
|
export const TARGET_NAMES: Readonly<Record<Target, string>> = {
|
||||||
|
[Target.Speed]: 'Speed',
|
||||||
|
[Target.Stamina]: 'Stamina',
|
||||||
|
[Target.Power]: 'Power',
|
||||||
|
[Target.Guts]: 'Guts',
|
||||||
|
[Target.Wit]: 'Wit',
|
||||||
|
[Target.SkillPoints]: 'SP',
|
||||||
|
[Target.RandomStat]: 'Random Stat',
|
||||||
|
[Target.Turf]: 'Turf aptitude',
|
||||||
|
[Target.Dirt]: 'Dirt aptitude',
|
||||||
|
[Target.FrontRunner]: 'Front Runner aptitude',
|
||||||
|
[Target.PaceChaser]: 'Pace Chaser aptitude',
|
||||||
|
[Target.LateSurger]: 'Late Surger aptitude',
|
||||||
|
[Target.EndCloser]: 'End Closer aptitude',
|
||||||
|
[Target.Sprint]: 'Sprint aptitude',
|
||||||
|
[Target.Mile]: 'Mile aptitude',
|
||||||
|
[Target.Medium]: 'Medium aptitude',
|
||||||
|
[Target.Long]: 'Long aptitude',
|
||||||
|
[Target.Skill]: 'hint level',
|
||||||
|
[Target.CarnivalBonus]: 'Carnival Bonus level',
|
||||||
|
[Target.SpeedCap]: 'Speed cap',
|
||||||
|
[Target.StaminaCap]: 'Stamina cap',
|
||||||
|
[Target.PowerCap]: 'Power cap',
|
||||||
|
[Target.GutsCap]: 'Guts cap',
|
||||||
|
[Target.WitCap]: 'Wit cap',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Effects that a spark can apply.
|
* Effects that a spark can apply.
|
||||||
*/
|
*/
|
||||||
export interface SparkEffect {
|
export interface Effect {
|
||||||
target: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 41 | 51 | 61 | 62 | 63 | 64 | 65;
|
target: Target;
|
||||||
value1?: number;
|
value1: number;
|
||||||
value2: number;
|
value2?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sparks(): Promise<Spark[]> {
|
export async function sparks(): Promise<Spark[]> {
|
||||||
|
|||||||
+386
-24
@@ -31,6 +31,233 @@ export enum Mood {
|
|||||||
Great,
|
Great,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moods as a descending list for easy iteration.
|
||||||
|
*/
|
||||||
|
export const MOODS = [Mood.Great, Mood.Good, Mood.Normal, Mood.Bad, Mood.Awful] as const;
|
||||||
|
|
||||||
|
const MOOD_COEF = {
|
||||||
|
[Mood.Awful]: 0.96,
|
||||||
|
[Mood.Bad]: 0.98,
|
||||||
|
[Mood.Normal]: 1,
|
||||||
|
[Mood.Good]: 1.02,
|
||||||
|
[Mood.Great]: 1.04,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function statClamp(x: number): number {
|
||||||
|
return Math.round(math.max(1, math.min(2000, x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a horse's base stat value from her mood and raw stat value.
|
||||||
|
* @param mood Horse's mood
|
||||||
|
* @param rawStat Raw value of the stat, i.e. the displayed stat
|
||||||
|
* @returns Base stat value
|
||||||
|
*/
|
||||||
|
export function baseStat(mood: Mood, rawStat: number): number {
|
||||||
|
const r = (math.min(rawStat, 1200) + math.max(rawStat - 1200, 0) * 0.5) * MOOD_COEF[mood];
|
||||||
|
return statClamp(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race distance types.
|
||||||
|
*/
|
||||||
|
export enum Distance {
|
||||||
|
Sprint,
|
||||||
|
Mile,
|
||||||
|
Medium,
|
||||||
|
Long,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the race distance type for a race length.
|
||||||
|
* @param raceLen Length of the race in meters
|
||||||
|
* @returns Distance type corresponding to the race length
|
||||||
|
*/
|
||||||
|
export function distance(raceLen: number): Distance {
|
||||||
|
if (raceLen < 1500) {
|
||||||
|
return Distance.Sprint;
|
||||||
|
}
|
||||||
|
if (raceLen < 1900) {
|
||||||
|
return Distance.Mile;
|
||||||
|
}
|
||||||
|
if (raceLen < 2500) {
|
||||||
|
return Distance.Medium;
|
||||||
|
}
|
||||||
|
return Distance.Long;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the distances at which each phase of a race ends.
|
||||||
|
* @param raceLen Length of the race in meters
|
||||||
|
* @returns Ends of each phase in meters
|
||||||
|
*/
|
||||||
|
export function phases(raceLen: number) {
|
||||||
|
const sixth = raceLen / 6;
|
||||||
|
return {
|
||||||
|
[Phase.EarlyRace]: sixth,
|
||||||
|
[Phase.MidRace]: 4 * sixth,
|
||||||
|
[Phase.LateRace]: raceLen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race surfaces.
|
||||||
|
*/
|
||||||
|
export enum Surface {
|
||||||
|
Turf,
|
||||||
|
Dirt,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race ground condition types.
|
||||||
|
*/
|
||||||
|
export enum GroundConditions {
|
||||||
|
Firm,
|
||||||
|
Good,
|
||||||
|
Soft,
|
||||||
|
Heavy,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ground condition types as a list for easy iterating.
|
||||||
|
*/
|
||||||
|
export const GROUND_CONDITONS = [
|
||||||
|
GroundConditions.Firm,
|
||||||
|
GroundConditions.Good,
|
||||||
|
GroundConditions.Soft,
|
||||||
|
GroundConditions.Heavy,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const speedStatGroundMod = {
|
||||||
|
[Surface.Turf]: {
|
||||||
|
[GroundConditions.Firm]: 0,
|
||||||
|
[GroundConditions.Good]: 0,
|
||||||
|
[GroundConditions.Soft]: 0,
|
||||||
|
[GroundConditions.Heavy]: -50,
|
||||||
|
},
|
||||||
|
[Surface.Dirt]: {
|
||||||
|
[GroundConditions.Firm]: 0,
|
||||||
|
[GroundConditions.Good]: 0,
|
||||||
|
[GroundConditions.Soft]: 0,
|
||||||
|
[GroundConditions.Heavy]: -50,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const powerStatGroundMod = {
|
||||||
|
[Surface.Turf]: {
|
||||||
|
[GroundConditions.Firm]: 0,
|
||||||
|
[GroundConditions.Good]: -50,
|
||||||
|
[GroundConditions.Soft]: -50,
|
||||||
|
[GroundConditions.Heavy]: -50,
|
||||||
|
},
|
||||||
|
[Surface.Dirt]: {
|
||||||
|
[GroundConditions.Firm]: -100,
|
||||||
|
[GroundConditions.Good]: -50,
|
||||||
|
[GroundConditions.Soft]: -100,
|
||||||
|
[GroundConditions.Heavy]: -100,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute adjusted speed for a race.
|
||||||
|
* @param speed
|
||||||
|
* @param baseSpeed Base speed stat as computed from raw speed with baseStat
|
||||||
|
* @param career Whether the horse is currently training in career
|
||||||
|
* @param surface Race surface type
|
||||||
|
* @param conditions Race ground conditions
|
||||||
|
* @param thresholdMod Achieved stat threshold modifier for the race track
|
||||||
|
*/
|
||||||
|
export function adjustedStat(
|
||||||
|
speed: Stat.Speed,
|
||||||
|
baseSpeed: number,
|
||||||
|
career: boolean,
|
||||||
|
surface: Surface,
|
||||||
|
conditions: GroundConditions,
|
||||||
|
thresholdMod: number,
|
||||||
|
): number;
|
||||||
|
/**
|
||||||
|
* Compute adjusted stamina for a race.
|
||||||
|
* @param stam
|
||||||
|
* @param baseStam Base stamina stat as computed from raw stamina with baseStat
|
||||||
|
* @param career Whether the horse is currently training in career
|
||||||
|
*/
|
||||||
|
export function adjustedStat(stam: Stat.Stamina, baseStam: number, career: boolean): number;
|
||||||
|
/**
|
||||||
|
* Compute adjusted power for a race.
|
||||||
|
* @param power
|
||||||
|
* @param basePower Base power stat as computed from raw power with baseStat
|
||||||
|
* @param career Whether the horse is currently training in career
|
||||||
|
* @param surface Race surface type
|
||||||
|
* @param conditions Race ground conditions
|
||||||
|
*/
|
||||||
|
export function adjustedStat(
|
||||||
|
power: Stat.Power,
|
||||||
|
basePower: number,
|
||||||
|
career: boolean,
|
||||||
|
surface: Surface,
|
||||||
|
conditions: GroundConditions,
|
||||||
|
): number;
|
||||||
|
/**
|
||||||
|
* Compute adjusted guts for a race.
|
||||||
|
* @param guts
|
||||||
|
* @param baseGuts Base guts stat as computed from raw guts with baseStat
|
||||||
|
* @param career Whether the horse is currently training in career
|
||||||
|
*/
|
||||||
|
export function adjustedStat(guts: Stat.Guts, baseGuts: number, career: boolean): number;
|
||||||
|
/**
|
||||||
|
* Compute adjusted wit for a race.
|
||||||
|
* @param wit
|
||||||
|
* @param baseWit Base wit stat as computed from raw wit with baseStat
|
||||||
|
* @param career Whether the horse is currently training in career
|
||||||
|
* @param styleApt Aptitude for the horse's current running style
|
||||||
|
*/
|
||||||
|
export function adjustedStat(wit: Stat.Wit, baseWit: number, career: boolean, styleApt: AptitudeLevel): number;
|
||||||
|
export function adjustedStat(
|
||||||
|
stat: Stat,
|
||||||
|
baseStat: number,
|
||||||
|
career: boolean,
|
||||||
|
surfaceOrStyle?: Surface | AptitudeLevel,
|
||||||
|
conditions?: GroundConditions,
|
||||||
|
thresholdMod?: number,
|
||||||
|
): number {
|
||||||
|
const careerMod = career ? 400 : 0;
|
||||||
|
switch (stat) {
|
||||||
|
case Stat.Speed:
|
||||||
|
return statClamp(baseStat * thresholdMod! + speedStatGroundMod[surfaceOrStyle as Surface][conditions!] + careerMod);
|
||||||
|
case Stat.Stamina:
|
||||||
|
return statClamp(baseStat + careerMod);
|
||||||
|
case Stat.Power:
|
||||||
|
return statClamp(baseStat + powerStatGroundMod[surfaceOrStyle as Surface][conditions!] + careerMod);
|
||||||
|
case Stat.Guts:
|
||||||
|
return statClamp(baseStat + careerMod);
|
||||||
|
case Stat.Wit:
|
||||||
|
return statClamp(baseStat * strategyProficiencyMod[surfaceOrStyle as AptitudeLevel] + careerMod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function thresholdMod(stat1: number, stat2?: number): number {
|
||||||
|
if (stat2 != null) {
|
||||||
|
return 0.5 * (thresholdMod(stat1) + thresholdMod(stat2));
|
||||||
|
}
|
||||||
|
if (stat1 > 900) {
|
||||||
|
return 1.2;
|
||||||
|
}
|
||||||
|
if (stat1 > 600) {
|
||||||
|
return 1.15;
|
||||||
|
}
|
||||||
|
if (stat1 > 300) {
|
||||||
|
return 1.1;
|
||||||
|
}
|
||||||
|
return 1.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function finalStat(adjStat: number, skillPassive: number): number {
|
||||||
|
return statClamp(adjStat + skillPassive);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Running styles for strategy–phase coefficients.
|
* Running styles for strategy–phase coefficients.
|
||||||
* Great Escape is distinguished as a separate style even though it is
|
* Great Escape is distinguished as a separate style even though it is
|
||||||
@@ -93,7 +320,103 @@ export enum Phase {
|
|||||||
LateRace,
|
LateRace,
|
||||||
}
|
}
|
||||||
|
|
||||||
function baseSpeed(raceLen: number): number {
|
const hpStrategyCoeff = {
|
||||||
|
[RunningStyle.FrontRunner]: 0.95,
|
||||||
|
[RunningStyle.PaceChaser]: 0.89,
|
||||||
|
[RunningStyle.LateSurger]: 1.0,
|
||||||
|
[RunningStyle.EndCloser]: 0.995,
|
||||||
|
[RunningStyle.GreatEscape]: 0.86,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a horse's max (starting) HP for a race.
|
||||||
|
* @param raceLen Length of the race in meters
|
||||||
|
* @param style Horse's running style
|
||||||
|
* @param stamStat Horse's final stamina stat
|
||||||
|
* @returns Max HP
|
||||||
|
*/
|
||||||
|
export function maxHP(raceLen: number, style: RunningStyle, stamStat: number): number {
|
||||||
|
return 0.8 * hpStrategyCoeff[style] * stamStat + raceLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groundHPRateMod = {
|
||||||
|
[Surface.Turf]: {
|
||||||
|
[GroundConditions.Firm]: 1,
|
||||||
|
[GroundConditions.Good]: 1,
|
||||||
|
[GroundConditions.Soft]: 1.02,
|
||||||
|
[GroundConditions.Heavy]: 1.02,
|
||||||
|
},
|
||||||
|
[Surface.Dirt]: {
|
||||||
|
[GroundConditions.Firm]: 1,
|
||||||
|
[GroundConditions.Good]: 1,
|
||||||
|
[GroundConditions.Soft]: 1.01,
|
||||||
|
[GroundConditions.Heavy]: 1.02,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate HP consumption in HP/s.
|
||||||
|
* @param raceLen Length of the race in meters
|
||||||
|
* @param surface Surface type of the race
|
||||||
|
* @param conditions Race ground conditions
|
||||||
|
* @param gutsStat Final guts stat, ignored outside of late race
|
||||||
|
* @param phase Current race phase
|
||||||
|
* @param currentSpeed Current speed in m/s, not including the effects of current speed skills
|
||||||
|
* @param dam Whether downhill accel mode is active (default false)
|
||||||
|
* @param pdm Whether pace-down mode is active (default false)
|
||||||
|
* @param rushed Whether the horse is currently rushed (default false)
|
||||||
|
*/
|
||||||
|
export function hpPerSecond(
|
||||||
|
raceLen: number,
|
||||||
|
surface: Surface,
|
||||||
|
conditions: GroundConditions,
|
||||||
|
gutsStat: number,
|
||||||
|
phase: Phase,
|
||||||
|
currentSpeed: number,
|
||||||
|
dam?: boolean,
|
||||||
|
pdm?: boolean,
|
||||||
|
rushed?: boolean,
|
||||||
|
): number {
|
||||||
|
const rushedMod = rushed ? 1.6 : 1;
|
||||||
|
const pdmMod = pdm ? 0.6 : 1;
|
||||||
|
const damMod = dam ? 0.4 : 1;
|
||||||
|
const groundMod = groundHPRateMod[surface][conditions];
|
||||||
|
const gutsMod = phase === Phase.LateRace ? 1 + 200 / (math.sqrt(600 * gutsStat) as number) : 1;
|
||||||
|
const sp = currentSpeed - baseSpeed(raceLen) + 12;
|
||||||
|
return (20 / 144) * sp * sp * rushedMod * pdmMod * damMod * groundMod * gutsMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the HP required for a full spurt.
|
||||||
|
* @param raceLen Length of the race in meters
|
||||||
|
* @param spurtSpeed Target speed that a full spurt would produce
|
||||||
|
* @param hpRate HP usage per second during late race
|
||||||
|
* @returns HP threshold required for a full spurt
|
||||||
|
*/
|
||||||
|
export function fullSpurtHP(raceLen: number, spurtSpeed: number, hpRate: number): number {
|
||||||
|
// Per Aya:
|
||||||
|
/*
|
||||||
|
on hakuraku I do 62 here over 60 since we need the distance from last
|
||||||
|
spurt start to 60m before the end, not late race start to 60m to the
|
||||||
|
end, so since umas take an average of 1.5 frames to check their spurt
|
||||||
|
after late race and run at about 20 m/s mid race on most courses I
|
||||||
|
shifted it over by 1.5/15*20=2
|
||||||
|
forgot which CM it was but I checked what would be the best fit
|
||||||
|
empirically one time and got like 62.1
|
||||||
|
*/
|
||||||
|
// https://discord.com/channels/1493222996662419576/1493222998688137301/1517348759229436036
|
||||||
|
const spurtLen = raceLen / 3 - 62;
|
||||||
|
const spurtTime = spurtLen / spurtSpeed;
|
||||||
|
return spurtTime * hpRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the base speed for a race.
|
||||||
|
* Note that this is not the base target speed for a horse.
|
||||||
|
* @param raceLen Race length in meters
|
||||||
|
* @returns Base speed for the race
|
||||||
|
*/
|
||||||
|
export function baseSpeed(raceLen: number): number {
|
||||||
return 20 - (raceLen - 2000) / 1000;
|
return 20 - (raceLen - 2000) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,25 +583,6 @@ export function inverseSpurtSpeed(
|
|||||||
return Math.round(r);
|
return Math.round(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hpStrategyCoeff = {
|
|
||||||
[RunningStyle.FrontRunner]: 0.95,
|
|
||||||
[RunningStyle.PaceChaser]: 0.89,
|
|
||||||
[RunningStyle.LateSurger]: 1.0,
|
|
||||||
[RunningStyle.EndCloser]: 0.995,
|
|
||||||
[RunningStyle.GreatEscape]: 0.86,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate a horse's max HP (starting HP) for a race.
|
|
||||||
* @param raceLen Length of the race in meters
|
|
||||||
* @param style Horse's running style
|
|
||||||
* @param stamStat Final stamina stat
|
|
||||||
* @returns Max HP
|
|
||||||
*/
|
|
||||||
export function maxHP(raceLen: number, style: RunningStyle, stamStat: number): number {
|
|
||||||
return 0.8 * hpStrategyCoeff[style] * stamStat + raceLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Meters per horse length (馬身). */
|
/** Meters per horse length (馬身). */
|
||||||
export const HORSE_LENGTH = 2.5;
|
export const HORSE_LENGTH = 2.5;
|
||||||
/** Meters per course width (a constant unit of measure). */
|
/** Meters per course width (a constant unit of measure). */
|
||||||
@@ -382,6 +686,56 @@ export function laneChangeSpeed(powerStat: number, maxLaneOrOrder?: number, curr
|
|||||||
*/
|
*/
|
||||||
export const LANE_CHANGE_ACCEL = 0.03;
|
export const LANE_CHANGE_ACCEL = 0.03;
|
||||||
|
|
||||||
|
const conserveStratDistCoef = {
|
||||||
|
[RunningStyle.FrontRunner]: {
|
||||||
|
[Distance.Sprint]: 1,
|
||||||
|
[Distance.Mile]: 1,
|
||||||
|
[Distance.Medium]: 1,
|
||||||
|
[Distance.Long]: 1,
|
||||||
|
},
|
||||||
|
[RunningStyle.PaceChaser]: {
|
||||||
|
[Distance.Sprint]: 0.7,
|
||||||
|
[Distance.Mile]: 0.8,
|
||||||
|
[Distance.Medium]: 0.9,
|
||||||
|
[Distance.Long]: 0.9,
|
||||||
|
},
|
||||||
|
[RunningStyle.LateSurger]: {
|
||||||
|
[Distance.Sprint]: 0.75,
|
||||||
|
[Distance.Mile]: 0.7,
|
||||||
|
[Distance.Medium]: 0.875,
|
||||||
|
[Distance.Long]: 1,
|
||||||
|
},
|
||||||
|
[RunningStyle.EndCloser]: {
|
||||||
|
[Distance.Sprint]: 0.7,
|
||||||
|
[Distance.Mile]: 0.75,
|
||||||
|
[Distance.Medium]: 0.86,
|
||||||
|
[Distance.Long]: 0.9,
|
||||||
|
},
|
||||||
|
[RunningStyle.GreatEscape]: {
|
||||||
|
[Distance.Sprint]: 1,
|
||||||
|
[Distance.Mile]: 1,
|
||||||
|
[Distance.Medium]: 1,
|
||||||
|
[Distance.Long]: 1,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function conservePowerAccel(
|
||||||
|
distanceType: Distance,
|
||||||
|
style: RunningStyle,
|
||||||
|
rawPower: number,
|
||||||
|
powerSkillBonus?: number,
|
||||||
|
spotStruggled?: boolean,
|
||||||
|
rushed?: boolean,
|
||||||
|
): number {
|
||||||
|
const power = rawPower + (powerSkillBonus ?? 0);
|
||||||
|
if (power <= 1200) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const sdc = conserveStratDistCoef[style][distanceType];
|
||||||
|
const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1;
|
||||||
|
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate a horse's minimum speed for a race.
|
* Calculate a horse's minimum speed for a race.
|
||||||
* @param raceLen Length of the race in meters
|
* @param raceLen Length of the race in meters
|
||||||
@@ -410,8 +764,6 @@ export function spotStruggleSpeed(gutsStat: number): number {
|
|||||||
return Math.pow(500 * gutsStat, 0.6) * 0.0001;
|
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.
|
* Calculate the max duration of spot struggle.
|
||||||
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,
|
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,
|
||||||
@@ -447,13 +799,22 @@ export function duelAccelMod(gutsStat: number): number {
|
|||||||
* Calculate the speed modifier for running uphill.
|
* Calculate the speed modifier for running uphill.
|
||||||
* Contrary to the race mechanics document, this is expressed as a negative number.
|
* Contrary to the race mechanics document, this is expressed as a negative number.
|
||||||
* @param powerStat Final power stat
|
* @param powerStat Final power stat
|
||||||
* @param slopePer Slope percentage, generally one of 0.5, 1.0, 1.5, or 2.0
|
* @param slopePer Slope percentage, one of 1.0, 1.5, or 2.0
|
||||||
* @returns Speed modifier for running uphill, a negative value
|
* @returns Speed modifier for running uphill, a negative value
|
||||||
*/
|
*/
|
||||||
export function uphillMod(powerStat: number, slopePer: number): number {
|
export function uphillMod(powerStat: number, slopePer: number): number {
|
||||||
return (slopePer * -200) / powerStat;
|
return (slopePer * -200) / powerStat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the speed modifier while running in downhill accel mode.
|
||||||
|
* @param slopePer Slope percentage, one of -1.0, -1.5, or -2.0
|
||||||
|
* @returns Speed modifier for downhill accel mode
|
||||||
|
*/
|
||||||
|
export function downhillAccelMod(slopePer: number): number {
|
||||||
|
return 0.3 - slopePer / 10;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the forward speed boost given when moving lanewise while a skill
|
* Calculate the forward speed boost given when moving lanewise while a skill
|
||||||
* that grants a lane change speed boost is active.
|
* that grants a lane change speed boost is active.
|
||||||
@@ -519,7 +880,8 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null,
|
|||||||
* @returns Probability per candidate to accept
|
* @returns Probability per candidate to accept
|
||||||
*/
|
*/
|
||||||
export function reducedSpurtChance(witStat: number): number {
|
export function reducedSpurtChance(witStat: number): number {
|
||||||
return 0.15 + 0.0005 * witStat;
|
// 1900 wit style S exceeds 100% chance if we don't explicitly cap.
|
||||||
|
return Math.min(0.15 + 0.0005 * witStat, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Instantaneous characteristics of a horse in motion.
|
||||||
|
*/
|
||||||
|
export interface Instant {
|
||||||
|
/** Timestamp of the instant. */
|
||||||
|
t: number;
|
||||||
|
/** Forward position along the course. */
|
||||||
|
x: number;
|
||||||
|
/** Actual speed. */
|
||||||
|
v: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward travel characteristics under the effects of acceleration.
|
||||||
|
*/
|
||||||
|
export interface Travel {
|
||||||
|
start: Instant;
|
||||||
|
end: Instant;
|
||||||
|
/** Actual acceleration value applied between start and end. */
|
||||||
|
a: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance a horse according to its forward motion characteristics.
|
||||||
|
* @param start Motion at the beginning of the segment
|
||||||
|
* @param targetSpeed Target speed to accelerate to
|
||||||
|
* @param totalAccel Total (positive) acceleration including skill bonuses
|
||||||
|
* @param maxDur Max duration of the acceleration, or infinite if not given
|
||||||
|
* @param maxDist Max distance of the acceleration, or infinite if not given
|
||||||
|
* @param decel Negative acceleration value for cases where target speed is below start speed
|
||||||
|
* @returns Travel segment after reaching the first of the given limits
|
||||||
|
*/
|
||||||
|
export function travelSegment(
|
||||||
|
start: Instant,
|
||||||
|
targetSpeed: number,
|
||||||
|
totalAccel: number,
|
||||||
|
maxDur?: number,
|
||||||
|
maxDist?: number,
|
||||||
|
decel?: number,
|
||||||
|
): Travel {
|
||||||
|
const a = start.v < targetSpeed ? totalAccel : (decel ?? -1);
|
||||||
|
const dvt = (targetSpeed - start.v) / a;
|
||||||
|
const dxt = maxDist != null ? (-start.v + Math.sqrt(start.v * start.v + 4 * a * maxDist)) / (2 * a) : dvt;
|
||||||
|
const time = maxDur != null ? Math.min(dvt, dxt, maxDur) : Math.min(dvt, dxt);
|
||||||
|
// To address numerical imprecision, if accel time comes from velocity or distance,
|
||||||
|
// set the reached values directly.
|
||||||
|
const v = time === dvt ? targetSpeed : start.v + time * a;
|
||||||
|
const dist = maxDist != null && time === dxt ? maxDist : (start.v + 0.5 * a * time) * time;
|
||||||
|
const end = { t: start.t + time, x: start.x + dist, v };
|
||||||
|
return { start, end, a };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acceleration bonus from skills.
|
||||||
|
*/
|
||||||
|
export interface Skill {
|
||||||
|
/**
|
||||||
|
* Acceleration modifier in m/s².
|
||||||
|
*/
|
||||||
|
accel: number;
|
||||||
|
/**
|
||||||
|
* Remaining actual duration in seconds.
|
||||||
|
*/
|
||||||
|
dur: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accelerate up to a new target speed while under the effects of acceleration skills.
|
||||||
|
* @param startSpeed Speed at the start of the acceleration in m/s
|
||||||
|
* @param targetSpeed Speed at which the acceleration ends in m/s
|
||||||
|
* @param baseAccel Base acceleration value to which skills add, in m/s²
|
||||||
|
* @param skills Skills that are active for the acceleration period
|
||||||
|
* @returns Acceleration segments and remainders of skills upon reaching the target speed
|
||||||
|
*/
|
||||||
|
export function up(start: Instant, targetSpeed: number, baseAccel: number, skills: Skill[]) {
|
||||||
|
const segments: Travel[] = [];
|
||||||
|
const sk = skills.map((s) => ({ ...s })).filter((s) => s.dur > 0);
|
||||||
|
while (start.v < targetSpeed) {
|
||||||
|
const active = sk.filter((s) => s.dur > 0);
|
||||||
|
if (active.length === 0) {
|
||||||
|
segments.push(travelSegment(start, targetSpeed, baseAccel));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const boost = active.reduce((a, s) => a + s.accel, 0);
|
||||||
|
const time = Math.min(...active.map((s) => s.dur));
|
||||||
|
const seg = travelSegment(start, targetSpeed, baseAccel + boost, time);
|
||||||
|
segments.push(seg);
|
||||||
|
start = seg.end;
|
||||||
|
for (const s of active) {
|
||||||
|
s.dur -= seg.end.t - seg.start.t;
|
||||||
|
if (s.dur < 1e-5) {
|
||||||
|
s.dur = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { segments, skills: sk };
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Fundamental stats of umas.
|
||||||
|
*/
|
||||||
|
export enum Stat {
|
||||||
|
speed,
|
||||||
|
stamina,
|
||||||
|
power,
|
||||||
|
guts,
|
||||||
|
wit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stats as a list for easy iteration.
|
||||||
|
*/
|
||||||
|
export const LIST = [Stat.speed, Stat.stamina, Stat.power, Stat.guts, Stat.wit] as const;
|
||||||
|
|
||||||
|
declare const __stage: unique symbol;
|
||||||
|
|
||||||
|
export type Stage = 'raw' | 'base' | 'adjusted' | 'final' | 'skills';
|
||||||
|
|
||||||
|
export type StatValue<T extends Stage> = number & { [__stage]: T };
|
||||||
|
|
||||||
|
export interface Stats<T extends Stage> {
|
||||||
|
speed: StatValue<T>;
|
||||||
|
stamina: StatValue<T>;
|
||||||
|
power: StatValue<T>;
|
||||||
|
guts: StatValue<T>;
|
||||||
|
wit: StatValue<T>;
|
||||||
|
}
|
||||||
@@ -15,9 +15,12 @@
|
|||||||
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the
|
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the
|
||||||
effects of individual skills.
|
effects of individual skills.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/race/gate')}>Gate Calculator</a> — Calculate the importance of acceleration effects at the starting gate.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 class="mt-8 mb-4 text-4xl">Character Tools</h2>
|
<h2 class="mt-8 mb-4 text-4xl">Character Tools</h2>
|
||||||
<ul>
|
<ul class="mb-4 list-disc pl-4">
|
||||||
<li>
|
<li>
|
||||||
<a href={resolve('/chara/affinity')}>Affinity Details</a> — Details of why characters have the base compatibility numbers
|
<a href={resolve('/chara/affinity')}>Affinity Details</a> — Details of why characters have the base compatibility numbers
|
||||||
they have.
|
they have.
|
||||||
@@ -26,11 +29,6 @@
|
|||||||
<a href={resolve('/chara/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations
|
<a href={resolve('/chara/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations
|
||||||
on unlocking them quickly.
|
on unlocking them quickly.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="https://discord.com/oauth2/authorize?client_id=1461931240264568994" target="_blank" rel="noopener noreferrer"
|
|
||||||
>Discord Bot</a
|
|
||||||
> — Skill search by name or unique owner within Discord. Install to a server or user.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="my-4 text-4xl">Documents</h2>
|
<h2 class="my-4 text-4xl">Documents</h2>
|
||||||
@@ -51,6 +49,18 @@
|
|||||||
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
||||||
> for easier visualization of how mechanics scale.
|
> for easier visualization of how mechanics scale.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/doc/spark')}>Spark Explorer</a> — View all possible inheritance effects of any spark.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="my-4 text-4xl">&c.</h2>
|
||||||
|
<ul class="mb-4 list-disc pl-4">
|
||||||
|
<li>
|
||||||
|
<a href="https://discord.com/oauth2/authorize?client_id=1461931240264568994" target="_blank" rel="noopener noreferrer"
|
||||||
|
>Discord Bot</a
|
||||||
|
> — Skill search by name or unique owner within Discord. Install to a server or user.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="my-4 text-4xl">About</h2>
|
<h2 class="my-4 text-4xl">About</h2>
|
||||||
|
|||||||
@@ -22,4 +22,7 @@
|
|||||||
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
||||||
> for easier visualization of how mechanics scale.
|
> for easier visualization of how mechanics scale.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/doc/spark')}>Spark Explorer</a> — View all possible inheritance effects of any spark.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import type { ComputedAreas, ComputedSeries, HorizontalRule } from '$lib/chart';
|
import type { ComputedAreas, ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||||
import {
|
import {
|
||||||
AptitudeLevel,
|
AptitudeLevel,
|
||||||
|
conservePowerAccel,
|
||||||
|
distance,
|
||||||
downhillAccelEnterChance,
|
downhillAccelEnterChance,
|
||||||
frontModeEnterChance,
|
frontModeEnterChance,
|
||||||
HORSE_LENGTH,
|
HORSE_LENGTH,
|
||||||
@@ -89,8 +91,11 @@
|
|||||||
loneWolf: allSkills.get(201641),
|
loneWolf: allSkills.get(201641),
|
||||||
slipstream: allSkills.get(201651),
|
slipstream: allSkills.get(201651),
|
||||||
pto: allSkills.get(201661),
|
pto: allSkills.get(201661),
|
||||||
|
headon: allSkills.get(201902),
|
||||||
risky: allSkills.get(202032),
|
risky: allSkills.get(202032),
|
||||||
runaway: allSkills.get(202051),
|
runaway: allSkills.get(202051),
|
||||||
|
fr: allSkills.get(202462),
|
||||||
|
burningPOW: allSkills.get(210031),
|
||||||
burningWIT: allSkills.get(210051),
|
burningWIT: allSkills.get(210051),
|
||||||
ignitedWIT: allSkills.get(210052),
|
ignitedWIT: allSkills.get(210052),
|
||||||
radiant: allSkills.get(210061),
|
radiant: allSkills.get(210061),
|
||||||
@@ -209,6 +214,12 @@
|
|||||||
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
|
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
const fcSeries: ComputedSeries[] = $derived([
|
||||||
|
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) },
|
||||||
|
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) },
|
||||||
|
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) },
|
||||||
|
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Article>
|
<Article>
|
||||||
@@ -233,6 +244,28 @@
|
|||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
<Sec h={2} id="7-1">1 Jul Balance Update</Sec>
|
||||||
|
<p>A quick summary of how the 1 Jul balance update affects front runners:</p>
|
||||||
|
<ul class="mb-4 list-disc pl-4">
|
||||||
|
<li>
|
||||||
|
Pace chasers and late surgers gained access to much more consistent accel skills. Still not as good as Angling, but good
|
||||||
|
enough that front runners can be considered substantially nerfed.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#full-charge">Conserving Power/Fully Charged</a> is in, along with stat uncaps, which means that about 1270 power
|
||||||
|
(base + skills) is now worth a perfectly timed 0.1 accel skill on front runners. The exact mechanics of conserving power
|
||||||
|
aren't yet known, but front runners show a <i>negative correlation</i> between guts stat and probability of activating full charge.
|
||||||
|
As good as spot struggle may be in sprint, a perfect accel is always going to be better, so we are now actively minimizing guts.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
A solo front no longer loses access to speed-up mode. However, having watched a few dozen replays, it seems that while a
|
||||||
|
solo front is typically about 2 to 4 meters ahead of the best pace at position keep end, the best front in multi-front is
|
||||||
|
typically about 6 to 10 meters ahead. Solo front still overwhelmingly benefits other styles. The main impact of the position
|
||||||
|
keep changes in practice seems to be that pace promotion can happen <i>even if there are front runners in the lobby</i>,
|
||||||
|
which means you can't skimp on gate skills when running solo front as a pace promo blocker.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<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
|
||||||
@@ -259,14 +292,6 @@
|
|||||||
some of the information from the race mechanics doc in chart form, but I will generally leave out exact mechanic numbers and
|
some of the information from the race mechanics doc in chart form, but I will generally leave out exact mechanic numbers and
|
||||||
conditions; the doc is already the place for that information.
|
conditions; the doc is already the place for that information.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
Also worth mentioning is that I've been unemployed for over two years, with a computer science degree that is increasingly
|
|
||||||
difficult for an anti-AI person to apply. If this article helps you, I'd be tremendously grateful if you could <a
|
|
||||||
href="https://ko-fi.com/zephyrtronium"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">drop me some spare change</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Sec h={2} id="mechanics">Race Mechanics</Sec>
|
<Sec h={2} id="mechanics">Race Mechanics</Sec>
|
||||||
<p>
|
<p>
|
||||||
@@ -371,9 +396,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The running modes for front runners are speed-up (+4% target speed for first among that front type) and overtake (+5% for
|
The running modes for front runners are speed-up (+4% target speed for first among that front type) and overtake (+5% for
|
||||||
not-first). Entering these modes requires meeting certain conditions relating to positioning, which collectively can be read
|
not-first). Entering these modes requires meeting certain conditions relating to positioning. They also require passing a wit
|
||||||
as "solo fronts are heavily penalized." (With the 1.5 anni balance patch, we expect that solo fronts will be given more
|
check, with the same chance for both modes.
|
||||||
forgiving running mode conditions.) They also require passing a wit check, with the same chance for both modes.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="mx-auto h-60 w-full max-w-3xl md:h-96">
|
<div class="mx-auto h-60 w-full max-w-3xl md:h-96">
|
||||||
<StatChart
|
<StatChart
|
||||||
@@ -392,14 +416,14 @@
|
|||||||
whenever a horse gets what their style defines as too close to first place.
|
whenever a horse gets what their style defines as too close to first place.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Watch a MANT late surger with 1000+ power and wit in a daily legend race. As long as they don't get blocked, they should <a
|
Watch a UC late surger with 1200+ power and wit in a daily legend race. As long as they don't get blocked, they should <a
|
||||||
href="#section-speed">slide forward</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
|
> 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.
|
of the late surgers, often near the back of the group. That's PDM.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
On styles with PDM, early race and sometimes mid race speed skills are effectively converted from distance gain into HP
|
On styles with PDM, early race and sometimes mid race speed skills are effectively converted from distance gain into HP
|
||||||
conservation. The thing that really makes front runners good is that they don't have to worry about that – they aren't
|
conservation. The thing that really makes front runners strong is that they don't have to worry about that – they aren't
|
||||||
subject to PDM at all. Their mid race speed skills always gain distance.
|
subject to PDM at all. Their mid race speed skills always gain distance.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -411,17 +435,18 @@
|
|||||||
</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
|
Spot struggle provides a target speed bonus that scales with the guts stat. If it isn't cut short, which will approximately
|
||||||
never happen, its duration also scales with the guts stat. Unlike skills, its duration <i>does not</i> scale with race distance.
|
never happen outside of sprint, its duration also scales with the guts stat. Unlike skills, its duration <i>does not</i> scale with
|
||||||
|
race distance.
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-4 grid h-60 w-full grid-cols-2 md:h-96">
|
<div class="mb-4 grid h-60 w-full grid-cols-2 md:h-96">
|
||||||
<StatChart stat={Stat.Guts} y={ssBoostSeries} yLabel="Speed Bonus (m/s)" range={[0, 0.3]} />
|
<StatChart stat={Stat.Guts} y={ssBoostSeries} yLabel="Speed Bonus (m/s)" range={[0, 0.4]} />
|
||||||
<StatChart stat={Stat.Guts} y={ssDurSeries} yLabel="Duration (s)" range={[0, 12]} />
|
<StatChart stat={Stat.Guts} y={ssDurSeries} yLabel="Duration (s)" range={[0, 14]} />
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Spot struggle also greatly increases HP consumption. For normal front runners, the rate is slightly less than Rushed. For
|
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.)
|
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;
|
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.
|
red-light green-light pretty much guarantees that horse won't full spurt.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than
|
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than
|
||||||
@@ -429,6 +454,47 @@
|
|||||||
multiple gold speed skills in total distance gained. See the <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for
|
multiple gold speed skills in total distance gained. See the <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for
|
||||||
precise analysis.
|
precise analysis.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
However, while the details aren't yet known, it appears that spot struggle reduces <a href="#full-charge">conserved power</a>,
|
||||||
|
which means that front runners who spot struggle are less likely to full charge, and the probability falls as the spot
|
||||||
|
struggle duration rises.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec h={3} id="full-charge">Conserve Power & Fully Charged</Sec>
|
||||||
|
<p>
|
||||||
|
New in the 1 Jul balance patch is the conserve power mechanic. Horses with base power plus power passive skills greater than
|
||||||
|
1200 effectively gain access to a free perfectly-timed accel skill, Fully Charged. The effectiveness of full charge varies
|
||||||
|
with running style and distance, but it is greatest for front runners at all distances.
|
||||||
|
</p>
|
||||||
|
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
|
||||||
|
<StatChart
|
||||||
|
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
|
||||||
|
stat={Stat.Power}
|
||||||
|
y={fcSeries}
|
||||||
|
yLabel="Accel Bonus (m/s²)"
|
||||||
|
range={[0, 0.25]}
|
||||||
|
plotOptions={{ color: { legend: true } }}
|
||||||
|
/>
|
||||||
|
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||||
|
<label class="my-auto hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
|
||||||
|
<input
|
||||||
|
class="my-auto max-w-40 flex-1 md:flex-2"
|
||||||
|
type="range"
|
||||||
|
id="secSpeedRaceLen"
|
||||||
|
min="1000"
|
||||||
|
max="3600"
|
||||||
|
step="100"
|
||||||
|
bind:value={raceLen}
|
||||||
|
/>
|
||||||
|
<span class="my-auto flex-none pl-2 text-sm">{raceLen}m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a
|
||||||
|
href="#spot-struggle">spot struggle</a
|
||||||
|
> both reduce the chance for it to fire. Since spot struggle duration scales with guts, it appears that taking high guts to benefit
|
||||||
|
from the strength of spot struggle has become likely to deprive you of an even stronger mechanic.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="lane-combo">Lane Combo</Sec>
|
<Sec h={3} id="lane-combo">Lane Combo</Sec>
|
||||||
<p>
|
<p>
|
||||||
@@ -443,7 +509,7 @@
|
|||||||
y={laneComboSeries}
|
y={laneComboSeries}
|
||||||
yLabel="Speed Boost"
|
yLabel="Speed Boost"
|
||||||
yRule={lcYRule}
|
yRule={lcYRule}
|
||||||
range={[0.2, 0.5]}
|
range={[0.2, 0.6]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
@@ -530,7 +596,7 @@
|
|||||||
stat={Stat.Wit}
|
stat={Stat.Wit}
|
||||||
y={downhillSeries}
|
y={downhillSeries}
|
||||||
yLabel="Entry Chance (% each second)"
|
yLabel="Entry Chance (% each second)"
|
||||||
range={[0, 60]}
|
range={[0, 75]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
@@ -550,7 +616,7 @@
|
|||||||
stat={Stat.Wit}
|
stat={Stat.Wit}
|
||||||
yArea={secSpeedSeries}
|
yArea={secSpeedSeries}
|
||||||
yLabel="Section Speed (m/s)"
|
yLabel="Section Speed (m/s)"
|
||||||
range={[17.5, 22.5]}
|
range={[17, 23]}
|
||||||
plotOptions={{ color: { legend: true } }}
|
plotOptions={{ color: { legend: true } }}
|
||||||
/>
|
/>
|
||||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||||
@@ -674,7 +740,7 @@
|
|||||||
<Sec h={3} id="spurt-skills">Spurt Skills</Sec>
|
<Sec h={3} id="spurt-skills">Spurt Skills</Sec>
|
||||||
<p>
|
<p>
|
||||||
Because they are post-Angling, skills that activate in the final spurt are typically less interesting to front runners.
|
Because they are post-Angling, skills that activate in the final spurt are typically less interesting to front runners.
|
||||||
Notable exceptions are <Skill skill={skills.barcaroleInherit} /> and, on Tokyo turf, <Skill
|
Notable exceptions are <Skill skill={skills.fr} />, <Skill skill={skills.barcaroleInherit} /> and, on Tokyo turf, <Skill
|
||||||
skill={skills.inesInherit}
|
skill={skills.inesInherit}
|
||||||
/><!-- skill name ends with ! -->
|
/><!-- skill name ends with ! -->
|
||||||
These are good inherits for those who don't have easy access to <Skill skill={skills.cacaoInherit} mention /> and <Skill
|
These are good inherits for those who don't have easy access to <Skill skill={skills.cacaoInherit} mention /> and <Skill
|
||||||
@@ -708,8 +774,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Eyes especially has the potential to be threatening, since it's roughly unconditional. However, it hits others <i>in vision</i
|
Eyes especially has the potential to be threatening, since it's roughly unconditional. However, it hits others <i>in vision</i
|
||||||
>, which is essentially always a range of 20m. Debuffers are typically built with very little stamina and power, so Eyes is
|
>, which is essentially always a range of 20m, or 8 lengths. Debuffers are typically built with very little stamina and power,
|
||||||
almost never going to reach front runners, especially the front who got Angling.
|
so Eyes is almost never going to reach front runners, especially the front who got Angling.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The other category of skills to think about is other horses' uniques.
|
The other category of skills to think about is other horses' uniques.
|
||||||
@@ -753,6 +819,10 @@
|
|||||||
Obviously, CM is run in teams of three. While spamming the highest stats you can manage is a valid strategy that will get
|
Obviously, CM is run in teams of three. While spamming the highest stats you can manage is a valid strategy that will get
|
||||||
wins, building a team holistically will lead to higher win rates.
|
wins, building a team holistically will lead to higher win rates.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
N.B. This section has only partially been updated for the 1 Jul balance patch, which means that some of the advice especially
|
||||||
|
relating to runaways and spot struggle is out of date. A full update here probably won't come until after CM17 Virgo Cup.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="pace-promo">Zero Front</Sec>
|
<Sec h={3} id="pace-promo">Zero Front</Sec>
|
||||||
<p>
|
<p>
|
||||||
@@ -790,21 +860,20 @@
|
|||||||
<Sec h={3} id="solo-front">Solo Front</Sec>
|
<Sec h={3} id="solo-front">Solo Front</Sec>
|
||||||
<p>
|
<p>
|
||||||
Because solo fronts essentially don't have access to their <a href="#front-modes">running modes</a>, they are unlikely to
|
Because solo fronts essentially don't have access to their <a href="#front-modes">running modes</a>, they are unlikely to
|
||||||
build enough of a lead over paces to win, even with Angling. (This should change to some degree with the release of Grand
|
build enough of a lead over paces to win, even with Angling. (This changed a bit with the 1 Jul balance patch, as solo fronts
|
||||||
Live, as solo fronts will get an extra long speed-up zone.)
|
now get an extra-long speed-up range, but it doesn't appear that they're maintaining much of a gap over paces in practice.)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In particularly front-dominated metas, this is manageable, since you'll have opponent fronts to run against. When any other
|
In particularly front-dominated metas, this is manageable, since you'll have opponent fronts to run against. When any other
|
||||||
style is viable, which is probably always now that Nishino Flower exists, you'll have matches where your team's front runner
|
style is viable, which is always now that <Skill skill={skills.headon} mention /> is buffed, you'll have matches where your team's
|
||||||
is solo. The best choice for how to design a solo front depends on the rest of your team composition, because ultimately the
|
front runner is solo. The best choice for how to design a solo front depends on the rest of your team composition, because ultimately
|
||||||
choice you are faced with is whether to use a runaway blocker.
|
the choice you are faced with is whether to use a runaway blocker.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In the MANT era, building a runaway that can defeat normal front runners is difficult. For one thing, a runaway existing means
|
The ideal runaway blocker has capped power, capped wit, and surface S, along with full gate and <a href="#lane-combo"
|
||||||
that all fronts have access to overtake mode for position keep, which is better than speed-up mode. Moreover, beating their
|
>lane combos</a
|
||||||
mid race skill list requires some lucky hints in Unity Cup. The ideal runaway blocker has capped power, capped wit, and
|
> where applicable, to build as much of an early race lead as possible. Her speed stat doesn't particularly matter, because her
|
||||||
surface S, along with full gate and <a href="#lane-combo">lane combos</a> where applicable, to build as much of an early race lead
|
job is to block, not to win.
|
||||||
as possible.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Because runaways get so far ahead during the first half of the race, they essentially disable <a href="#pdm">pace-down mode</a
|
Because runaways get so far ahead during the first half of the race, they essentially disable <a href="#pdm">pace-down mode</a
|
||||||
@@ -826,12 +895,12 @@
|
|||||||
<p>
|
<p>
|
||||||
There is still a difference between dual ace double front and double front with an ace and a backup. The ideal stat lines and
|
There is still a difference between dual ace double front and double front with an ace and a backup. The ideal stat lines and
|
||||||
skill lists will be the same between the two styles, but character choice will be different. In particular, dual ace wants
|
skill lists will be the same between the two styles, but character choice will be different. In particular, dual ace wants
|
||||||
both to have strong speed uniques – VBourbon, SMaru, Ines Fujin on Tokyo turf.
|
both to have strong speed uniques – VBourbon or SMaru.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
With an ace and a backup, the ace is the one with the speed unique, and the other is a horse who gambles on winning mid race
|
With an ace and a backup, the ace is the one with the speed unique, and the other is a horse who gambles on winning mid race
|
||||||
and turns that into a first place finish. Seiun Sky is the strongest such option; as an own unique rather than inherited,
|
and turns that into a first place finish. Seiun Sky is the strongest such option; as an own unique rather than inherited,
|
||||||
Angling gives so much perfectly timed accel that speed inherits give her more lengths than other accel skills.
|
Angling gives so much perfectly timed acceleration that speed inherits give her more lengths than other accel skills.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Since your two front runners are both meant to be winners, making your third horse a support type is likely to give the best
|
Since your two front runners are both meant to be winners, making your third horse a support type is likely to give the best
|
||||||
@@ -853,12 +922,6 @@
|
|||||||
long distance; front runners are common generally here.) So, double front for long is ultimately about the same as double front
|
long distance; front runners are common generally here.) So, double front for long is ultimately about the same as double front
|
||||||
for other distances, just with different goals when choosing the legacy.
|
for other distances, just with different goals when choosing the legacy.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
Given that NN is good, it's also worth considering the gold version, <Skill skill={skills.nsm} /><!-- ends with ! -->
|
|
||||||
NSM is a better skill for the long double front build, but for now, the only way to get it while running MANT is from Yukino Bijin
|
|
||||||
Wit. That card's numbers are not great. It's still possible to get a good stat line using her, but it will take more careers to
|
|
||||||
get there than to just get NN from Fine Motion.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
If you're feeling adventurous, runaway is another consideration for double (and triple) front on long distances. Your ace
|
If you're feeling adventurous, runaway is another consideration for double (and triple) front on long distances. Your ace
|
||||||
(hopefully) still gets VC, but the runaway paces up the race. As mentioned in the <a href="#solo-front">solo front</a>
|
(hopefully) still gets VC, but the runaway paces up the race. As mentioned in the <a href="#solo-front">solo front</a>
|
||||||
@@ -888,10 +951,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The fact that triple front tries to occupy positions 3-4 lets you do some very interesting team comps where you run pace
|
The fact that triple front tries to occupy positions 3-4 lets you do some very interesting team comps where you run pace
|
||||||
chasers as front runners. E.g., I ran Taiki Shuttle as front in dirt CM and Curren Chan as front in sprint CM. This is not
|
chasers as front runners. E.g., I ran Taiki Shuttle as front in CM10 on dirt and Curren Chan as front in the CM9 sprint. This
|
||||||
terribly consistent because they're readily equipped to get into first just as easily as third, but then they just get Angling
|
is not terribly consistent because they're readily equipped to get into first just as easily as third, but then they just get
|
||||||
instead and follow the normal front runner game plan. If you see people talking about a pace chaser being especially strong
|
Angling instead and follow the normal front runner game plan. If you see people talking about a given pace chaser being
|
||||||
for a given CM, consider running them as a front runner in a triple front build instead.
|
especially strong for a CM, consider running them as a front runner in a triple front build instead.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
A more mechanical type of support is to build for <a href="#spot-struggle">spot struggle</a> and have her pull your less gutsy fronts
|
A more mechanical type of support is to build for <a href="#spot-struggle">spot struggle</a> and have her pull your less gutsy fronts
|
||||||
@@ -905,13 +968,13 @@
|
|||||||
showing. In addition to the normal front win cons, it's potentially strong to take unconditional gambles like <Skill
|
showing. In addition to the normal front win cons, it's potentially strong to take unconditional gambles like <Skill
|
||||||
skill={skills.cornerConn}
|
skill={skills.cornerConn}
|
||||||
mention
|
mention
|
||||||
/> and <Skill skill={skills.radiant} mention />.
|
/>, <Skill skill={skills.burningPOW} mention />, and <Skill skill={skills.radiant} mention />.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
SS supports do best on medium and long tracks, because you want to be building <i>everyone</i> for spot struggle on miles and
|
Now that <a href="#full-charge">conserving power</a> is a game mechanic, SS supports have the problem that spot struggle risks
|
||||||
sprints. However, spot struggle is the only mechanic in the game that scales superlinearly with stats, so even the difference
|
taking away full charge, which means losing a strong accel. This can be alleviated to some degree by managing
|
||||||
between 1200 and 1000 guts can matter enough to justify an extra guts focus on the SS support in miles. See the
|
<a href="#lane-combo">Dodging Danger</a> to keep your ace out of the spot struggle – either the ace has lane combo and the
|
||||||
<a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for details.
|
other two don't, or the ace doesn't have it and the other two do.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="hidden" id="chariot"></div>
|
<div class="hidden" id="chariot"></div>
|
||||||
@@ -934,15 +997,17 @@
|
|||||||
/>, so the normal fronts (typically UG1 or better) pass them during early race.
|
/>, so the normal fronts (typically UG1 or better) pass them during early race.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If this build ever had an era, it was during Unity Cup, when NSM was essentially free and there was no VC. I don't think it's
|
If this build ever had an era, it was during the first pass of Unity Cup, when NSM was essentially free and there was no VC.
|
||||||
viable now.
|
We have reworked UC for one CM before Our Grand Concert releases, but it's a sprint CM that overwhelmingly favors pace
|
||||||
|
chasers. I don't think it's viable now.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={2} id="career">Career</Sec>
|
<Sec h={2} id="career">Career</Sec>
|
||||||
<p>Most front runners enjoy easy careers thanks to strong kits and little chance to be blocked.</p>
|
<p>Most front runners enjoy easy careers thanks to strong kits and little chance to be blocked.</p>
|
||||||
<p>
|
<p>
|
||||||
Currently, this chapter is about MANT (a.k.a. Trackblazer). Unity Cup is still useful – obviously for runaways, but also
|
Currently, this chapter is about MANT (a.k.a. Trackblazer). Current data for CM16 following the URA and UC reworks suggests
|
||||||
for spinning up an <Skill skill={skills.ignitedWIT} mention /> legacy.
|
that MANT horses win approximately just as much as UC horses relative to their respective populations, despite the lower stat
|
||||||
|
caps. So, I am going to leave this section largely untouched until the next scenario releases.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="support-cards">Support Cards</Sec>
|
<Sec h={3} id="support-cards">Support Cards</Sec>
|
||||||
@@ -1170,7 +1235,8 @@
|
|||||||
<Sec h={3} id="taiki-shuttle">Taiki Shuttle & Curren Chan</Sec>
|
<Sec h={3} id="taiki-shuttle">Taiki Shuttle & Curren Chan</Sec>
|
||||||
<p>
|
<p>
|
||||||
When building <a href="#triple-front">triple fronts</a>, you will naturally have a horse in third. This presents an
|
When building <a href="#triple-front">triple fronts</a>, you will naturally have a horse in third. This presents an
|
||||||
opportunity to use a horse who is normally a pace chaser as a type of gambler.
|
opportunity to use a horse who is normally a pace chaser, with a strong unique that requires position 3 or behind, as a type
|
||||||
|
of gambler.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Taiki Shuttle and original Curren Chan are notable for having C and B front, respectively, which is quite reasonable to fix.
|
Taiki Shuttle and original Curren Chan are notable for having C and B front, respectively, which is quite reasonable to fix.
|
||||||
@@ -1196,14 +1262,15 @@
|
|||||||
|
|
||||||
<Sec h={3} id="sparks">Important Sparks</Sec>
|
<Sec h={3} id="sparks">Important Sparks</Sec>
|
||||||
<p>
|
<p>
|
||||||
Front runners are uniquely capable of making good use of all stat (blue) sparks, since guts is unusually important for them.
|
Front runners are uniquely capable of making good use of wit sparks, since wit governs early- and mid-race speed, which is
|
||||||
Power sparks are typically still best, and stamina sparks can help to forgo a stamina card or two for 2400m+ builds. Fronts
|
unusually important for them. Power sparks are typically still best, and stamina sparks can help to forgo a stamina card or
|
||||||
have a disproportionate representation of horses with high speed talent, which makes speed sparks a bit less valuable.
|
two for 2400m+ builds. Fronts have a disproportionate representation of horses with high speed talent, which makes speed
|
||||||
|
sparks a bit less valuable.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Pink sparks are more interesting, because – at least until other styles catch up on accels – they're less reliant
|
Pink sparks are more interesting, because – at least until other styles finish catching up on accels – they're
|
||||||
on distance S to win. Building a high affinity legacy tree requires keeping some mile and long sparks on hand. Front sparks
|
less reliant on distance S to win. Building a high affinity legacy tree requires keeping some mile and long sparks on hand.
|
||||||
are better than other style sparks, especially if you do <a href="#triple-front">triple front</a> CM builds using
|
Front sparks are better than other style sparks, especially if you do <a href="#triple-front">triple front</a> CM builds using
|
||||||
<a href="#taiki-shuttle">fixed pace chasers</a>.
|
<a href="#taiki-shuttle">fixed pace chasers</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>Front runners are more reliant than other styles on getting certain skills from inspiration:</p>
|
<p>Front runners are more reliant than other styles on getting certain skills from inspiration:</p>
|
||||||
@@ -1221,7 +1288,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<Skill skill={skills.ignitedWIT} /> is the stronger lane combo piece. The only sources of it are inheritance and Unity Cup; which
|
<Skill skill={skills.ignitedWIT} /> is the stronger lane combo piece. The only sources of it are inheritance and Unity Cup; which
|
||||||
is to say, the source of it is inheritance. When searching databases for borrows, looking for Ignited WIT is a shortcut for listing
|
is to say, the source of it is inheritance. When searching databases for borrows, looking for Ignited WIT is a shortcut for listing
|
||||||
good front runner legacies, since it's only valuable to fronts and somewhat hard to get even when you focus on it.
|
good front runner legacies, since it's only valuable to fronts.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Almost all green skills are valuable to inherit at one point or another as GW triggers.
|
Almost all green skills are valuable to inherit at one point or another as GW triggers.
|
||||||
@@ -1236,12 +1303,17 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
All generic speed skills that aren't strictly late race are good inherits.
|
All generic speed skills that aren't strictly late race are good inherits.
|
||||||
<Skill skill={skills.risky} /> is the best one by ratio of strength to difficulty to acquire.
|
<Skill skill={skills.risky} /> is the best one by ratio of strength to difficulty to acquire; if you aren't forced to borrow Smart
|
||||||
|
Falcon, you can get the gold version of this from Nakayama Festa Wit.
|
||||||
<Skill skill={skills.thh} />, <Skill skill={skills.pto} />, <Skill skill={skills.ramp} />, and <Skill
|
<Skill skill={skills.thh} />, <Skill skill={skills.pto} />, <Skill skill={skills.ramp} />, and <Skill
|
||||||
skill={skills.cornerAdept}
|
skill={skills.cornerAdept}
|
||||||
/> are also strong, but available as hints or chain skills from some usable-to-good cards.
|
/> are also strong, but available as hints or chain skills from some usable-to-good cards.
|
||||||
</li>
|
</li>
|
||||||
<li>Obviously, getting front-runner-specific skills from inheritance makes them cheaper and more consistent to get.</li>
|
<li>
|
||||||
|
Getting front-runner-specific skills from inheritance has become especially important now that reworked UC doesn't give
|
||||||
|
skills according to aptitudes. This is a significant reason for MANT to remain the parenting scenario, arguably even more so
|
||||||
|
than the higher affinity potential.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Sec h={3} id="parents">Parents</Sec>
|
<Sec h={3} id="parents">Parents</Sec>
|
||||||
@@ -1270,7 +1342,8 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Mayano Top Gun doesn't have an actual front runner variant (until Halloween Mayano in 2028 or so), but she acts like a front
|
Mayano Top Gun doesn't have an actual front runner variant (until Halloween Mayano in 2028 or so), but she acts like a front
|
||||||
runner for affinity. In particular, Maya and Kitasan is the highest base compatibility pair currently in the game.
|
runner for affinity. In particular, Maya and Kitasan is among the highest base compatibility pairs currently in the game.
|
||||||
|
(They were #1 prior to the 1 Jul balance patch, but now that spot goes to Tamamo Cross and Inari One.)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
TM Opera O is technically a pace chaser parent, but she still has good compatibility with most front runners, and her unique
|
TM Opera O is technically a pace chaser parent, but she still has good compatibility with most front runners, and her unique
|
||||||
@@ -1286,6 +1359,37 @@
|
|||||||
|
|
||||||
<Sec h={2} id="cm">My CM Teams</Sec>
|
<Sec h={2} id="cm">My CM Teams</Sec>
|
||||||
|
|
||||||
|
<Sec h={3} id="cm16">CM16 – Leo Cup (Sprinters Stakes)</Sec>
|
||||||
|
<p>
|
||||||
|
The <a href="#7-1">1 Jul balance patch</a> redefined everything for everyone, and front runners are suddenly actually quite bad.
|
||||||
|
I guess the stakes are lower for my triple front mandate.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
An additional interesting detail is that this track has no straights before late race. That means straight velos are much less
|
||||||
|
important.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I had some amazing luck with support card pulls and got both Maruzensky and Nakayama Festa to MLB in 400. Are these runs what
|
||||||
|
it feels like to not be F2P?
|
||||||
|
</p>
|
||||||
|
<ol class="mb-4 list-decimal pl-4">
|
||||||
|
<li>Suzuka gets to be a runaway again!</li>
|
||||||
|
<li>
|
||||||
|
Mejiro Palmer gets to have G sprint fixed and be a second runaway! Hopefully she will both stay in front of Taiki and not
|
||||||
|
block Suzuka!
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Taiki Shuttle has a very-nearly-perfect 3-4 unique, which will be great with her as a front runner. Given the importance of
|
||||||
|
skills, I'm building her in MANT.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
This CM will be interesting as the first opportunity to use multiple runaways and not have them be vaporized by spot struggle.
|
||||||
|
I'll actually want to click on power to make sure I break 1200, and I'll want to minimize guts to conserve more power, which
|
||||||
|
means I'll reasonably get the ca. 800 stamina required for them to survive. The only downside is that runaways, and front
|
||||||
|
runners broadly, are not actually good here, because <Skill skill={skills.headon} mention /> is so wildly good in sprint now.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="cm15">CM15 – Cancer Cup (Takarazuka Kinen)</Sec>
|
<Sec h={3} id="cm15">CM15 – Cancer Cup (Takarazuka Kinen)</Sec>
|
||||||
<p>
|
<p>
|
||||||
Kitasan's unique ends about a sixth of a second before late race starts, which means massive carryover potential (although
|
Kitasan's unique ends about a sixth of a second before late race starts, which means massive carryover potential (although
|
||||||
@@ -1309,6 +1413,14 @@
|
|||||||
ace, and that's what Chocobon does best.
|
ace, and that's what Chocobon does best.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<p>Win rates are a bit messy because I experimented with a few different comps and Throne reworks. After 80:</p>
|
||||||
|
<ul class="mb-4 list-disc pl-4">
|
||||||
|
<li>VBourbon: 28-15-17-20</li>
|
||||||
|
<li>Kitasan: 12-14-11-38 (of 75)</li>
|
||||||
|
<li>Suzuka: 8-5-7-60 = 8-4-6-37 + 0-1-1-23 (replaced after 25; I ended up not using a runaway either time)</li>
|
||||||
|
<li>Seiun Sky: 0-0-2-3 (of 5)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Lost finals to two COCs, one getting Pump and the other NSM, while Chocobon blew velos during accel.</p>
|
||||||
|
|
||||||
<Sec h={3} id="cm14">CM14 – Gemini Cup (Yasuda Kinen, NHK Mile Cup, Victoria Mile)</Sec>
|
<Sec h={3} id="cm14">CM14 – Gemini Cup (Yasuda Kinen, NHK Mile Cup, Victoria Mile)</Sec>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -14,18 +14,9 @@
|
|||||||
let surfaceApt = $state(race.AptitudeLevel.A);
|
let surfaceApt = $state(race.AptitudeLevel.A);
|
||||||
let distanceApt = $state(race.AptitudeLevel.A);
|
let distanceApt = $state(race.AptitudeLevel.A);
|
||||||
let raceLen = $state(2000);
|
let raceLen = $state(2000);
|
||||||
const raceLenType = $derived.by(() => {
|
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.GreatEscape);
|
||||||
if (raceLen < 1500) {
|
const distanceType = $derived(race.distance(raceLen));
|
||||||
return 'Sprint';
|
const raceLenType = $derived(race.Distance[distanceType]);
|
||||||
}
|
|
||||||
if (raceLen < 1900) {
|
|
||||||
return 'Mile';
|
|
||||||
}
|
|
||||||
if (raceLen < 2500) {
|
|
||||||
return 'Medium';
|
|
||||||
}
|
|
||||||
return 'Long';
|
|
||||||
});
|
|
||||||
|
|
||||||
const spurtSpeed: Array<ComputedSeries | null> = $derived([
|
const spurtSpeed: Array<ComputedSeries | null> = $derived([
|
||||||
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
|
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
|
||||||
@@ -66,11 +57,21 @@
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
|
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
|
||||||
|
const conservePower: Array<ComputedSeries | null> = $derived([
|
||||||
|
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) },
|
||||||
|
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) },
|
||||||
|
styleIsFront
|
||||||
|
? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) }
|
||||||
|
: null,
|
||||||
|
]);
|
||||||
const gutsSpurt: Array<ComputedSeries | null> = $derived([
|
const gutsSpurt: Array<ComputedSeries | null> = $derived([
|
||||||
{ label: `Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, distanceApt, raceLen) },
|
|
||||||
{ label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) },
|
{ label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) },
|
||||||
|
{ label: `Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, distanceApt, raceLen) },
|
||||||
|
// NOTE(zeph): The label is 1600 despite the input being 1400
|
||||||
|
// for raw vs final difference.
|
||||||
|
{ label: `Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, distanceApt, raceLen) },
|
||||||
distanceApt !== race.AptitudeLevel.A
|
distanceApt !== race.AptitudeLevel.A
|
||||||
? { label: `${raceLenType} A, Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, race.AptitudeLevel.A, raceLen) }
|
? { label: `${raceLenType} A, Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, race.AptitudeLevel.A, raceLen) }
|
||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
|
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
|
||||||
@@ -82,7 +83,6 @@
|
|||||||
];
|
];
|
||||||
const duelSpeed: ComputedSeries[] = [{ label: 'Dueling Speed Boost', y: (x) => race.duelSpeedMod(x) }];
|
const duelSpeed: ComputedSeries[] = [{ label: 'Dueling Speed Boost', y: (x) => race.duelSpeedMod(x) }];
|
||||||
const duelAccel: ComputedSeries[] = [{ label: 'Dueling Acceleration Boost', y: (x) => race.duelAccelMod(x) }];
|
const duelAccel: ComputedSeries[] = [{ label: 'Dueling Acceleration Boost', y: (x) => race.duelAccelMod(x) }];
|
||||||
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.GreatEscape);
|
|
||||||
const secSpeed: Array<ComputedAreas | null> = $derived([
|
const secSpeed: Array<ComputedAreas | null> = $derived([
|
||||||
{
|
{
|
||||||
label: 'Early Race',
|
label: 'Early Race',
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
|
|
||||||
<Sec h={3} id="spurt-speed">Spurt Speed</Sec>
|
<Sec h={3} id="spurt-speed">Spurt Speed</Sec>
|
||||||
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed-guts">the effect of Guts</a>.</p>
|
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed-guts">the effect of Guts</a>.</p>
|
||||||
{@render statChart(race.Stat.Speed, spurtSpeed, 'Spurt Speed (m/s)', [20, 26], { len: true, style: true, dist: true })}
|
{@render statChart(race.Stat.Speed, spurtSpeed, 'Spurt Speed (m/s)', [20, 26.5], { len: true, style: true, dist: true })}
|
||||||
|
|
||||||
<Sec h={3} id="late-race-speed">Late Race Speed</Sec>
|
<Sec h={3} id="late-race-speed">Late Race Speed</Sec>
|
||||||
<p>Base target speed during late race when the Uma is not spurting (but not dead yet).</p>
|
<p>Base target speed during late race when the Uma is not spurting (but not dead yet).</p>
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
href={resolve('/doc/frbm#lane-combo')}>lane combo</a
|
href={resolve('/doc/frbm#lane-combo')}>lane combo</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.5])}
|
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.6])}
|
||||||
|
|
||||||
<Sec h={3} id="uphill">Uphill Target Speed Loss</Sec>
|
<Sec h={3} id="uphill">Uphill Target Speed Loss</Sec>
|
||||||
<p>Target speed modifier while running uphill.</p>
|
<p>Target speed modifier while running uphill.</p>
|
||||||
@@ -269,7 +269,7 @@
|
|||||||
|
|
||||||
<Sec h={3} id="acceleration">Acceleration</Sec>
|
<Sec h={3} id="acceleration">Acceleration</Sec>
|
||||||
<p>Acceleration.</p>
|
<p>Acceleration.</p>
|
||||||
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.5], { style: true })}
|
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.55], { style: true })}
|
||||||
|
|
||||||
<Sec h={3} id="lane-change-target-speed">Lane Change Target Speed</Sec>
|
<Sec h={3} id="lane-change-target-speed">Lane Change Target Speed</Sec>
|
||||||
<p>Horizontal (rather than forward) target speed of changing lanes.</p>
|
<p>Horizontal (rather than forward) target speed of changing lanes.</p>
|
||||||
@@ -279,19 +279,27 @@
|
|||||||
stat={race.Stat.Power}
|
stat={race.Stat.Power}
|
||||||
y={laneChange}
|
y={laneChange}
|
||||||
yLabel="Lane Change Target Speed (CW/s)"
|
yLabel="Lane Change Target Speed (CW/s)"
|
||||||
range={[0.01, 0.03]}
|
range={[0.01, 0.04]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Sec h={3} id="conserve-power">Conserve Power Acceleration</Sec>
|
||||||
|
<p>Bonus acceleration at spurt start from the Conserve Power mechanic.</p>
|
||||||
|
<p>
|
||||||
|
This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed
|
||||||
|
and spot struggle multipliers apply.
|
||||||
|
</p>
|
||||||
|
{@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })}
|
||||||
|
|
||||||
<Sec h={2} id="guts">Guts</Sec>
|
<Sec h={2} id="guts">Guts</Sec>
|
||||||
|
|
||||||
<Sec h={3} id="spurt-speed-guts">Spurt Speed</Sec>
|
<Sec h={3} id="spurt-speed-guts">Spurt Speed</Sec>
|
||||||
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed">the effect of Speed</a>.</p>
|
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed">the effect of Speed</a>.</p>
|
||||||
{@render statChart(race.Stat.Guts, gutsSpurt, 'Spurt Speed (m/s)', [20, 26], { len: true, style: true, dist: true })}
|
{@render statChart(race.Stat.Guts, gutsSpurt, 'Spurt Speed (m/s)', [20, 26.5], { len: true, style: true, dist: true })}
|
||||||
|
|
||||||
<Sec h={3} id="min-speed">Minimum Speed</Sec>
|
<Sec h={3} id="min-speed">Minimum Speed</Sec>
|
||||||
<p>Forced minimum speed, as well as target speed when out of HP.</p>
|
<p>Forced minimum speed, as well as target speed when out of HP.</p>
|
||||||
{@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.4], { len: true })}
|
{@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.5], { len: true })}
|
||||||
|
|
||||||
<Sec h={3} id="hp-rate-mod">Late Race HP Consumption Rate Modifier</Sec>
|
<Sec h={3} id="hp-rate-mod">Late Race HP Consumption Rate Modifier</Sec>
|
||||||
<p>Multiplier on HP consumption rate during late race and last spurt phase.</p>
|
<p>Multiplier on HP consumption rate during late race and last spurt phase.</p>
|
||||||
@@ -300,15 +308,15 @@
|
|||||||
<Sec h={3} id="spot-struggle">Spot Struggle</Sec>
|
<Sec h={3} id="spot-struggle">Spot Struggle</Sec>
|
||||||
<p>Speed boost and duration of spot struggle.</p>
|
<p>Speed boost and duration of spot struggle.</p>
|
||||||
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
||||||
<StatChart stat={race.Stat.Guts} y={ssBoost} yLabel="Speed Bonus (m/s)" range={[0, 0.3]} />
|
<StatChart stat={race.Stat.Guts} y={ssBoost} yLabel="Speed Bonus (m/s)" range={[0, 0.4]} />
|
||||||
<StatChart stat={race.Stat.Guts} y={ssDur} yLabel="Duration (s)" range={[0, 12]} />
|
<StatChart stat={race.Stat.Guts} y={ssDur} yLabel="Duration (s)" range={[0, 14]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={3} id="dueling">Dueling</Sec>
|
<Sec h={3} id="dueling">Dueling</Sec>
|
||||||
<p>Speed boost and acceleration boost of dueling.</p>
|
<p>Speed boost and acceleration boost of dueling.</p>
|
||||||
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
||||||
<StatChart stat={race.Stat.Guts} y={duelSpeed} yLabel="Speed Bonus (m/s)" range={[0, 0.65]} />
|
<StatChart stat={race.Stat.Guts} y={duelSpeed} yLabel="Speed Bonus (m/s)" range={[0, 0.8]} />
|
||||||
<StatChart stat={race.Stat.Guts} y={duelAccel} yLabel="Acceleration Bonus (m/s²)" range={[0, 0.14]} />
|
<StatChart stat={race.Stat.Guts} y={duelAccel} yLabel="Acceleration Bonus (m/s²)" range={[0, 0.16]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={2} id="wit">Wit</Sec>
|
<Sec h={2} id="wit">Wit</Sec>
|
||||||
@@ -321,7 +329,7 @@
|
|||||||
stat={race.Stat.Wit}
|
stat={race.Stat.Wit}
|
||||||
yArea={secSpeed}
|
yArea={secSpeed}
|
||||||
yLabel="Section Speed (m/s)"
|
yLabel="Section Speed (m/s)"
|
||||||
range={[17.5, 22.5]}
|
range={[17, 23]}
|
||||||
plotOptions={{ color: { legend: true } }}
|
plotOptions={{ color: { legend: true } }}
|
||||||
/>
|
/>
|
||||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||||
@@ -347,7 +355,7 @@
|
|||||||
|
|
||||||
<Sec h={3} id="downhill">Downhill Accel Mode Chance</Sec>
|
<Sec h={3} id="downhill">Downhill Accel Mode Chance</Sec>
|
||||||
<p>Chance each second to enter downhill accel mode when eligible.</p>
|
<p>Chance each second to enter downhill accel mode when eligible.</p>
|
||||||
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 60])}
|
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 75])}
|
||||||
|
|
||||||
<Sec h={3} id="spurt-accept">Reduced Spurt Accept Chance</Sec>
|
<Sec h={3} id="spurt-accept">Reduced Spurt Accept Chance</Sec>
|
||||||
<p>Chance to accept each checked spurt delay and speed when not full spurting.</p>
|
<p>Chance to accept each checked spurt delay and speed when not full spurting.</p>
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as skill from '$lib/data/skill';
|
||||||
|
import * as spark from '$lib/data/spark';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
let sparks: spark.Spark[] = $state([]);
|
||||||
|
let skills: SvelteMap<number, skill.Skill> = new SvelteMap();
|
||||||
|
onMount(async () => {
|
||||||
|
const [sp, sk] = await Promise.all([spark.sparks(), skill.skills()]);
|
||||||
|
sparks = sp;
|
||||||
|
for (const s of sk) {
|
||||||
|
skills.set(s.skill_id, s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let group = $state(1);
|
||||||
|
|
||||||
|
const groups = $derived.by(() => {
|
||||||
|
const m = new SvelteMap<number, spark.Spark[]>();
|
||||||
|
for (const s of sparks) {
|
||||||
|
const l = m.get(s.spark_group) ?? [];
|
||||||
|
l.push(s);
|
||||||
|
m.set(s.spark_group, l);
|
||||||
|
}
|
||||||
|
return [...m.entries()];
|
||||||
|
});
|
||||||
|
function describe(eff: spark.Effect) {
|
||||||
|
if (eff.target === spark.Target.Skill) {
|
||||||
|
const sk = skills.get(eff.value1);
|
||||||
|
return `${sk?.name} ${spark.TARGET_NAMES[eff.target]} +${eff.value2}`;
|
||||||
|
}
|
||||||
|
return `${spark.TARGET_NAMES[eff.target]} +${eff.value1}`;
|
||||||
|
}
|
||||||
|
const cur = $derived(sparks.find((s) => s.spark_group === group));
|
||||||
|
const curEffectGroups = $derived(cur?.effects ?? []);
|
||||||
|
const curDescriptions = $derived(curEffectGroups.map((g) => g.map((e) => describe(e))));
|
||||||
|
const curClass = $derived.by(() => {
|
||||||
|
if (cur == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
switch (cur.type) {
|
||||||
|
case 1:
|
||||||
|
return ['stat'];
|
||||||
|
case 2:
|
||||||
|
return ['aptitude'];
|
||||||
|
case 3:
|
||||||
|
return ['unique'];
|
||||||
|
case 5:
|
||||||
|
case 4:
|
||||||
|
case 6:
|
||||||
|
case 10:
|
||||||
|
case 8:
|
||||||
|
case 11:
|
||||||
|
case 9:
|
||||||
|
return ['white-spark'];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-4xl">Spark Explorer</h1>
|
||||||
|
<form class="mx-auto mt-8 rounded-md p-4 text-center shadow-md ring md:max-w-2xl">
|
||||||
|
<select class="w-full" bind:value={group}>
|
||||||
|
{#each groups as [id, s] (id)}
|
||||||
|
<option value={id}>{s[0].name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<svelte:boundary>
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
'spark mx-auto mt-8 max-w-md flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md',
|
||||||
|
curClass,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<span class="block text-xl">{cur?.name}</span>
|
||||||
|
{#if cur != null}
|
||||||
|
<span class="block">
|
||||||
|
{spark.TYPE_NAMES[cur.type]} Spark
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="block">{cur?.description}</span>
|
||||||
|
</div>
|
||||||
|
<table class="mx-auto mt-8 w-full max-w-xl table-fixed">
|
||||||
|
<caption class="text-xl">Possible Effects</caption>
|
||||||
|
<tbody>
|
||||||
|
{#each curDescriptions as s (s)}
|
||||||
|
<tr class="text-center even:bg-mist-300 dark:even:bg-mist-900">
|
||||||
|
<td>
|
||||||
|
{#each s as n (n)}
|
||||||
|
<span class="block">{n}</span>
|
||||||
|
{/each}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{#snippet pending()}
|
||||||
|
<div>Loading data...</div>
|
||||||
|
{/snippet}
|
||||||
|
</svelte:boundary>
|
||||||
|
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
|
||||||
|
<p>
|
||||||
|
When a spark activates during inspiration, it has an additional random roll for one of the effects listed in the table here.
|
||||||
|
All star levels of each spark have the same possible effects. Higher star levels are more likely to give better rewards.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.white-spark {
|
||||||
|
--spark-color: light-dark(var(--color-mist-200), var(--color-mist-800));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
--spark-color: light-dark(var(--color-sky-200), var(--color-sky-800));
|
||||||
|
}
|
||||||
|
|
||||||
|
.aptitude {
|
||||||
|
--spark-color: light-dark(var(--color-rose-300), var(--color-rose-800));
|
||||||
|
}
|
||||||
|
|
||||||
|
.unique {
|
||||||
|
--spark-color: light-dark(var(--color-lime-400), var(--color-lime-800));
|
||||||
|
}
|
||||||
|
|
||||||
|
.spark {
|
||||||
|
background-color: var(--spark-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,4 +14,7 @@
|
|||||||
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the
|
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the
|
||||||
effects of individual skills.
|
effects of individual skills.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/race/gate')}>Gate Calculator</a> — Calculate the importance of acceleration effects at the starting gate.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Plot from '@observablehq/plot';
|
||||||
|
import * as race from '$lib/race';
|
||||||
|
import * as accel from '$lib/racelib/accel';
|
||||||
|
import type { Attachment } from 'svelte/attachments';
|
||||||
|
|
||||||
|
let rawPower = $state(1200);
|
||||||
|
let rawGuts = $state(1200);
|
||||||
|
let rawWit = $state(1200);
|
||||||
|
let powerBonus = $state(0);
|
||||||
|
let gutsBonus = $state(0);
|
||||||
|
let witBonus = $state(0);
|
||||||
|
let style = $state(race.RunningStyle.FrontRunner);
|
||||||
|
let surfApt = $state(race.AptitudeLevel.A);
|
||||||
|
let styleApt = $state(race.AptitudeLevel.A);
|
||||||
|
let mood = $state(race.Mood.Normal);
|
||||||
|
let isCareer = $state(false);
|
||||||
|
|
||||||
|
let raceLen = $state(2000);
|
||||||
|
let surface = $state(race.Surface.Turf);
|
||||||
|
let conditions = $state(race.GroundConditions.Firm);
|
||||||
|
let slope = $state(0);
|
||||||
|
|
||||||
|
let rawSkills: accel.Skill[] = $state([]);
|
||||||
|
let skillToAdd: accel.Skill = $state({ accel: 0.2, dur: 1.2 });
|
||||||
|
const quick = [
|
||||||
|
{
|
||||||
|
name: 'GW+TTL',
|
||||||
|
skills: [
|
||||||
|
{ accel: 0.2, dur: 3 },
|
||||||
|
{ accel: 0.4, dur: 1.2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GW+EL',
|
||||||
|
skills: [
|
||||||
|
{ accel: 0.2, dur: 3 },
|
||||||
|
{ accel: 0.2, dur: 1.2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'Clear', skills: [] },
|
||||||
|
];
|
||||||
|
function addSkill() {
|
||||||
|
rawSkills.push(skillToAdd);
|
||||||
|
skillToAdd = { accel: 0.2, dur: 1.2 };
|
||||||
|
}
|
||||||
|
function removeSkill(i: number) {
|
||||||
|
rawSkills.splice(i, 1);
|
||||||
|
}
|
||||||
|
function setSkillList(l: accel.Skill[]) {
|
||||||
|
rawSkills = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phases = $derived(race.phases(raceLen));
|
||||||
|
|
||||||
|
const basePower = $derived(race.baseStat(mood, rawPower));
|
||||||
|
const adjPower = $derived(race.adjustedStat(race.Stat.Power, basePower, isCareer, surface, conditions));
|
||||||
|
const power = $derived(race.finalStat(adjPower, powerBonus));
|
||||||
|
const baseGuts = $derived(race.baseStat(mood, rawGuts));
|
||||||
|
const adjGuts = $derived(race.adjustedStat(race.Stat.Guts, baseGuts, isCareer));
|
||||||
|
const guts = $derived(race.finalStat(adjGuts, gutsBonus));
|
||||||
|
const baseWit = $derived(race.baseStat(mood, rawWit));
|
||||||
|
const adjWit = $derived(race.adjustedStat(race.Stat.Wit, baseWit, isCareer, styleApt));
|
||||||
|
const wit = $derived(race.finalStat(adjWit, witBonus));
|
||||||
|
|
||||||
|
const dashAccel = $derived(
|
||||||
|
race.acceleration(power, style, surfApt, race.Phase.EarlyRace, race.AptitudeLevel.A, slope > 0, true),
|
||||||
|
);
|
||||||
|
const postAccel = $derived(
|
||||||
|
race.acceleration(power, style, surfApt, race.Phase.EarlyRace, race.AptitudeLevel.A, slope > 0, false),
|
||||||
|
);
|
||||||
|
const minSpeed = $derived(race.minSpeed(raceLen, guts));
|
||||||
|
const baseSpeed = $derived(race.baseSpeed(raceLen));
|
||||||
|
const dashTarget = $derived(baseSpeed * 0.85);
|
||||||
|
const hillMod: [number, number] = $derived.by(() => {
|
||||||
|
if (slope > 0) {
|
||||||
|
const m = race.uphillMod(power, slope);
|
||||||
|
return [m, m];
|
||||||
|
}
|
||||||
|
if (slope < 0) {
|
||||||
|
return [0, race.downhillAccelMod(slope)];
|
||||||
|
}
|
||||||
|
return [0, 0];
|
||||||
|
});
|
||||||
|
const earlyTarget = $derived(race.sectionSpeed(raceLen, baseWit, wit, style, race.Phase.EarlyRace));
|
||||||
|
const speeds: [number, number] = $derived([earlyTarget[0] + hillMod[0], earlyTarget[1] + hillMod[1]]);
|
||||||
|
|
||||||
|
const startInstant: accel.Instant = { t: 0, x: 0, v: 3 };
|
||||||
|
function doSeggsToIt(targetSpeed: number, skills: accel.Skill[]) {
|
||||||
|
const startDash = accel.up(startInstant, dashTarget, dashAccel, skills);
|
||||||
|
const postStart = { ...last(startDash.segments).end, v: minSpeed };
|
||||||
|
const post = accel.up(postStart, targetSpeed, postAccel, startDash.skills);
|
||||||
|
const r = [...startDash.segments, ...post.segments];
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
function last<T>(l: T[]): T {
|
||||||
|
return l[l.length - 1];
|
||||||
|
}
|
||||||
|
const skills = $derived(rawSkills.map((s) => ({ ...s, dur: race.skillDuration(s.dur, raceLen) })));
|
||||||
|
const seggs = $derived([doSeggsToIt(speeds[0], skills), doSeggsToIt(speeds[1], skills)]);
|
||||||
|
const unskilledSeggs = $derived([doSeggsToIt(speeds[0], []), doSeggsToIt(speeds[1], [])]);
|
||||||
|
|
||||||
|
const distance = $derived([last(seggs[0]).end.x, last(seggs[1]).end.x]);
|
||||||
|
const time = $derived([last(seggs[0]).end.t, last(seggs[1]).end.t]);
|
||||||
|
const midTime = $derived([
|
||||||
|
time[0] + (phases[race.Phase.EarlyRace] - distance[0]) / speeds[1],
|
||||||
|
time[1] + (phases[race.Phase.EarlyRace] - distance[1]) / speeds[0],
|
||||||
|
]);
|
||||||
|
function gainLengths(fast: accel.Instant, slow: accel.Instant): number {
|
||||||
|
const m = fast.v * (slow.t - fast.t) + fast.x - slow.x;
|
||||||
|
return m / race.HORSE_LENGTH;
|
||||||
|
}
|
||||||
|
const gain = $derived([
|
||||||
|
gainLengths(last(seggs[0]).end, last(unskilledSeggs[0]).end),
|
||||||
|
gainLengths(last(seggs[1]).end, last(unskilledSeggs[1]).end),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let width = $state(1);
|
||||||
|
let height = $state(1);
|
||||||
|
const plotLines = $derived([
|
||||||
|
{ t: 0, v: 3, x: 0, label: 'Slowest Speed' },
|
||||||
|
...seggs[0].map((t) => ({ t: t.end.t, v: t.end.v, x: t.end.x, label: 'Slowest Speed' })),
|
||||||
|
{ t: midTime[1], v: speeds[0], label: 'Slowest Speed' },
|
||||||
|
{ t: 0, v: 3, x: 0, label: 'Fastest Speed' },
|
||||||
|
...seggs[1].map((t) => ({ t: t.end.t, v: t.end.v, x: t.end.x, label: 'Fastest Speed' })),
|
||||||
|
{ t: midTime[1], v: speeds[1], label: 'Fastest Speed' },
|
||||||
|
{ t: 0, v: 3, x: 0, label: 'Without Skills' },
|
||||||
|
...unskilledSeggs[1].map((t) => ({ t: t.end.t, v: t.end.v, x: t.end.x, label: 'Without Skills' })),
|
||||||
|
{ t: midTime[1], v: speeds[1], label: 'Without Skills' },
|
||||||
|
]);
|
||||||
|
const makeChart: Attachment = (el) => {
|
||||||
|
$effect(() => {
|
||||||
|
el?.firstChild?.remove();
|
||||||
|
el?.append(
|
||||||
|
Plot.plot({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
clip: true,
|
||||||
|
x: {
|
||||||
|
domain: [0, midTime[1]],
|
||||||
|
label: 'Time (s)',
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
domain: [0, 23],
|
||||||
|
label: 'Speed (m/s)',
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
color: { legend: true },
|
||||||
|
marks: [Plot.frame(), Plot.line(plotLines, { x: 't', y: 'v', stroke: 'label', strokeWidth: 3 })],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-4xl">Gate Calculator</h1>
|
||||||
|
<form class="mx-auto my-4 grid max-w-4xl grid-cols-1 gap-4 rounded-md p-4 shadow-sm md:grid-cols-5">
|
||||||
|
<div class="grid rounded-md text-center shadow-md ring md:col-span-5 md:grid-cols-5">
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="powerStat">Power</label>
|
||||||
|
<input type="number" id="powerStat" bind:value={rawPower} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="gutsStat">Guts</label>
|
||||||
|
<input type="number" id="gutsStat" bind:value={rawGuts} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="witStat">Wit</label>
|
||||||
|
<input type="number" id="witStat" bind:value={rawWit} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="style">Style</label>
|
||||||
|
<select id="style" required bind:value={style} class="w-full">
|
||||||
|
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||||
|
<option value={style}>{name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="mood">Mood</label>
|
||||||
|
<select id="mood" required bind:value={mood} class="w-full">
|
||||||
|
{#each race.MOODS as mood (mood)}
|
||||||
|
<option value={mood}>{race.Mood[mood]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="powerBonus">Power Greens</label>
|
||||||
|
<input type="number" id="powerBonus" bind:value={powerBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="gutsBonus">Guts Greens</label>
|
||||||
|
<input type="number" id="gutsBonus" bind:value={gutsBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="witBonus">Wit Greens</label>
|
||||||
|
<input type="number" id="witBonus" bind:value={witBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="surfaceApt">{race.Surface[surface]}</label>
|
||||||
|
<select id="surfaceApt" required bind:value={surfApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="styleApt">Style</label>
|
||||||
|
<select id="styleApt" required bind:value={styleApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
|
||||||
|
<input
|
||||||
|
class="col-span-4 m-4 w-full justify-self-center-safe"
|
||||||
|
type="range"
|
||||||
|
id="raceLen"
|
||||||
|
min="1000"
|
||||||
|
max="3600"
|
||||||
|
step="100"
|
||||||
|
bind:value={raceLen}
|
||||||
|
/>
|
||||||
|
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
|
||||||
|
<select id="surface" bind:value={surface} class="col-span-2 m-4">
|
||||||
|
<option value={race.Surface.Turf}>Turf</option>
|
||||||
|
<option value={race.Surface.Dirt}>Dirt</option>
|
||||||
|
</select>
|
||||||
|
<select id="conditions" bind:value={conditions} class="col-span-2 m-4">
|
||||||
|
{#each race.GROUND_CONDITONS as cond (cond)}
|
||||||
|
<option value={cond}>{race.GroundConditions[cond]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select id="slope" bind:value={slope} class="col-span-2 m-4">
|
||||||
|
{#each [2, 1.5, 1, 0, -1, -1.5, -2] as per (per)}
|
||||||
|
<option value={per}>{per > 0 ? `Uphill +${per}` : per < 0 ? `Downhill ${per}` : 'Flat'}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-auto flex w-full max-w-4xl flex-col rounded-md text-center shadow-md ring md:col-span-2">
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<span class="w-24 flex-1">Duration</span>
|
||||||
|
<span class="w-24 flex-1">Accel</span>
|
||||||
|
<span class="w-8 flex-none"><!-- filler to align other columns --></span>
|
||||||
|
</div>
|
||||||
|
{#each rawSkills as skill, i (i)}
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.dur}s</span>
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.accel}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="my-auto h-8 w-8 flex-none rounded-full bg-mist-300 text-2xl hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={() => removeSkill(i)}>×</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0.9" step="0.3" bind:value={skillToAdd.dur} />
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0" step="0.05" bind:value={skillToAdd.accel} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="my-auto h-8 w-8 flex-none rounded-full bg-mist-300 text-2xl hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={addSkill}>+</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex flex-row">
|
||||||
|
<span class="my-auto h-0 flex-1 border"></span>
|
||||||
|
<span class="mx-2 flex-none">Quick Setup</span>
|
||||||
|
<span class="my-auto h-0 flex-1 border"></span>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 flex flex-row place-content-center-safe gap-2">
|
||||||
|
{#each quick as q (q.name)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={() => setSkillList(q.skills)}>{q.name}</button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#snippet info(name: string, unit: string, val: number, val2?: number)}
|
||||||
|
<div class="m-2 max-w-72 flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md">
|
||||||
|
<span class="block">{name}</span>
|
||||||
|
{#if val2 == null}
|
||||||
|
<span class="block text-xl">{val.toFixed(3)} {unit}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="block text-xl">{val.toFixed(3)} – {val2.toFixed(3)} {unit}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Base Acceleration', 'm/s²', postAccel)}
|
||||||
|
{@render info('Target Speed', 'm/s', speeds[0], speeds[1])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Accel Time', 's', time[0], time[1])}
|
||||||
|
{@render info('Accel Distance', 'm', distance[0], distance[1])}
|
||||||
|
{@render info('Time to Mid-Race', 's', midTime[0], midTime[1])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Skill Gain', 'L', gain[0], gain[1])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mb-12 h-72 max-w-4xl" bind:clientWidth={width} bind:clientHeight={height}>
|
||||||
|
<div role="img" {@attach makeChart}>the chart seems to have didn't</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#snippet seggsTable(name: string, s: accel.Travel[])}
|
||||||
|
<table class="table-fixed border">
|
||||||
|
<caption class="text-xl">{name}</caption>
|
||||||
|
<thead>
|
||||||
|
<tr class="py-2">
|
||||||
|
<th class="px-2" scope="col">Time</th>
|
||||||
|
<th class="px-2" scope="col">Accel</th>
|
||||||
|
<th class="px-2" scope="col">Distance</th>
|
||||||
|
<th class="px-2" scope="col">Speed</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each s as { start, end, a } (start.t)}
|
||||||
|
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
|
||||||
|
<td class="px-4 text-center">{end.t.toFixed(3)}</td>
|
||||||
|
<td class="px-4 text-center">{a.toFixed(3)}</td>
|
||||||
|
<td class="px-4 text-center">{end.x.toFixed(3)}</td>
|
||||||
|
<td class="px-4 text-center">{end.v.toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/snippet}
|
||||||
|
<div class="mx-auto mt-8 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
|
{@render seggsTable(slope < 0 ? 'Fastest Speed (Downhill)' : 'Fastest Speed', seggs[1])}
|
||||||
|
{@render seggsTable('Slowest Speed', seggs[0])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
|
||||||
|
<p>
|
||||||
|
This calculator integrates acceleration analytically for efficiency, but the game uses a finite step approximation. The
|
||||||
|
difference has some implications:
|
||||||
|
</p>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li>Late starts are dramatically more punishing in the game, as acceleration is not applied for the first frame.</li>
|
||||||
|
<li>
|
||||||
|
Start dash always completes a single frame at the dash target speed. This calculator finds the exact moment start dash
|
||||||
|
target speed would be reached and continues integrating from there, leading to a slightly higher speed.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -164,21 +164,13 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
|
<div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
|
||||||
<StatChart
|
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 2]} xRule={gutsStat} yRule={ssYRule} />
|
||||||
class="flex-1"
|
|
||||||
stat={Stat.Guts}
|
|
||||||
y={ssY}
|
|
||||||
yLabel="Lengths Gained"
|
|
||||||
range={[0, 1.5, 2.5]}
|
|
||||||
xRule={gutsStat}
|
|
||||||
yRule={ssYRule}
|
|
||||||
/>
|
|
||||||
<StatChart
|
<StatChart
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
stat={Stat.Power}
|
stat={Stat.Power}
|
||||||
y={lcY}
|
y={lcY}
|
||||||
yLabel="Lengths Gained"
|
yLabel="Lengths Gained"
|
||||||
range={[0, 1.5, 2.5]}
|
range={[0, 1.1]}
|
||||||
xRule={powerStat}
|
xRule={powerStat}
|
||||||
yRule={lcYRule}
|
yRule={lcYRule}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user