33 Commits
Author SHA1 Message Date
zephyr 56bd5b460d zenno/doc/race: better section speed charts
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #49.
2026-11-05 14:45:59 -05:00
zephyr ddaf100069 meta: update go version in ci container
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-11-03 09:09:36 -05:00
zephyr f013aa4765 umadump/ism: remove old field 2026-11-03 09:09:36 -05:00
zephyr 2f676f0a62 umadump/ism: resparking info is populated now 2026-11-03 09:09:36 -05:00
zephyr 04cb6388c3 zenno/race/skillzone: indicate inherits
ci/woodpecker/push/horsebot Pipeline failed
ci/woodpecker/push/zenno Pipeline was successful
2026-10-31 21:12:17 -04:00
zephyr de2b871862 zenno: link up race/skillzone
ci/woodpecker/push/horsebot Pipeline failed
ci/woodpecker/push/zenno Pipeline was successful
2026-10-31 13:46:50 -04:00
zephyr c95bffbcb8 zenno/doc/race: disclaimer and bug reporting info 2026-10-31 13:45:13 -04:00
zephyr 04be0b75a9 zenno: lint fixes
ci/woodpecker/push/horsebot Pipeline failed
ci/woodpecker/push/zenno Pipeline was successful
2026-10-31 13:23:15 -04:00
zephyr 5a2b9b7bf4 zenno/race/skillzone: new tool for viewing skill activation locations
Fixes #48.
2026-10-31 13:21:37 -04:00
zephyr 2d935b87ec zenno/doc/fans: update attribution 2026-10-31 13:21:37 -04:00
zephyr f623388291 umadump/ism: add missing fields
ci/woodpecker/push/horsebot Pipeline failed
ci/woodpecker/push/zenno Pipeline failed
2026-10-24 23:00:43 -04:00
zephyr 74185d92d4 cmd/ismdb: tool to load auto mode umadumps into sqlite 2026-10-24 23:00:30 -04:00
zephyr 7f1aa00be6 horse: update dependencies and go version
ci/woodpecker/push/horsebot Pipeline failed
ci/woodpecker/push/zenno Pipeline failed
2026-10-21 10:18:55 -04:00
zephyr 16a5433594 zenno/doc/fans: new data coverage and fix off-by-ones
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-10-18 19:31:05 -04:00
zephyr 75558f35ba cmd/simdata: simple tool to dump race simdata 2026-10-18 13:09:10 -04:00
zephyr cf00bb54c5 zenno/doc/fans: margin is displayed margin
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-10-17 21:17:42 -04:00
zephyr cdda88181f zenno/doc/fans: missed placement base fan permilles
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-10-17 14:40:55 -04:00
zephyr 57a967978b zenno: link up doc/fans
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-10-17 14:32:05 -04:00
zephyr 939012b511 doc/fans: new article for career fan gain formula
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-17 14:25:57 -04:00
zephyr 888a6dbf3c zenno: format 2026-10-16 23:17:29 -04:00
zephyr fb27817038 zenno: fix course tests 2026-10-16 19:06:08 -04:00
zephyr f27b9dcaa5 mdb: fix duplicated carnival bonus skill 2026-10-16 17:53:09 -04:00
zephyr 645abb2f66 mdb: exclude all unused race courses 2026-10-12 16:49:55 -04:00
zephyr cf301745f1 zenno: use a real last spurt phase 2026-10-10 13:37:12 -04:00
zephyr 3a39794312 horse: use straight front type in racecourse api 2026-10-10 13:37:12 -04:00
zephyr 381df5bd35 umadump/ism: new end info fields
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-07 16:35:31 -04:00
zephyr e5ebd1894c umadump/ism: new reward summary fields
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-07 13:13:30 -04:00
zephyr c43f6b8292 cmd/racedb: program to load races into an sqlite db
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-06 21:00:37 -04:00
zephyr cccf0cc838 umadump/replay: allow fan count in race horse data 2026-10-06 21:00:18 -04:00
zephyr 64f82136cb umadump/replay: trainer name is null for npcs 2026-10-04 09:08:25 -04:00
zephyr 53c7079f46 umadump/replay: decode race replays
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-03 22:34:02 -04:00
zephyr 1cfabb18f5 zenno: fix type error on fake data
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-22 22:31:12 -04:00
zephyr bf4e750825 zenno/chara/convo: hyperlink to umadump 2026-09-22 22:30:00 -04:00
65 changed files with 22712 additions and 597 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ when:
steps:
- name: test
image: golang:1.26-alpine
image: golang:1.27-alpine
commands:
- go vet ./...
- go test ./...
+455
View File
@@ -0,0 +1,455 @@
package main
import (
"context"
_ "embed"
"errors"
"fmt"
"log/slog"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitemigration"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump/ism"
)
const appID int32 = 0x157db
//go:embed schema-00.sql
var schema0 string
//go:embed schema-final.sql
var schemaFinal string
func opendb(uri, journalMode string) *sqlitemigration.Pool {
schema := sqlitemigration.Schema{
Migrations: []string{
schema0,
},
AppID: appID,
RepeatableMigration: schemaFinal,
}
j := "PRAGMA journal_mode=" + journalMode
opts := sqlitemigration.Options{
Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI,
PrepareConn: func(conn *sqlite.Conn) error {
if journalMode == "" {
return nil
}
stmt, _, err := conn.PrepareTransient(j)
if err != nil {
return err
}
_, err = stmt.Step()
return errors.Join(err, stmt.Finalize())
},
}
db := sqlitemigration.NewPool(uri, schema, opts)
return db
}
func insert(ctx context.Context, db *sqlitemigration.Pool, log *slog.Logger, career *ism.WorkIdleSingleMode, trainerID int64) (err error) {
log = log.With(slog.Int64("trainer", trainerID), slog.Int("id", int(career.End.Chara.SingleModeCharaID)))
conn, err := db.Take(ctx)
if err != nil {
return err
}
defer db.Put(conn)
defer sqlitex.Transaction(conn)(&err)
// Do the first insert separately so we can check whether the career is
// already in, and so that the join doesn't report the same error repeatedly
// if there's something wrong with the db.
if err := insertCareer(conn, log, career, trainerID); err != nil {
code := sqlite.ErrCode(err)
if code == sqlite.ResultConstraintUnique {
log.InfoContext(ctx, "already loaded")
return nil
}
return err
}
trainedChara := career.End.Chara.SingleModeCharaID
err = errors.Join(
insertConditions(conn, log, career.ProgressLog.CharaEffectLog, trainerID, trainedChara),
insertGain(conn, log, &career.ProgressLog.EventGain, trainerID, trainedChara, 0),
insertGainSkillHint(conn, log, career.ProgressLog.EventGain.SkillTips, trainerID, trainedChara, 0),
insertGain(conn, log, &career.ProgressLog.SuccessionGain, trainerID, trainedChara, 1),
insertGainSkillHint(conn, log, career.ProgressLog.SuccessionGain.SkillTips, trainerID, trainedChara, 1),
)
for _, s := range career.ProgressLog.SupportCardGains {
err = errors.Join(
err,
insertGain(conn, log, &s.GainInfo, trainerID, trainedChara, int32(s.SupportCardID)),
insertGainSkillHint(conn, log, s.GainInfo.SkillTips, trainerID, trainedChara, int32(s.SupportCardID)),
)
}
err = errors.Join(
err,
insertInspirations(conn, log, career.ProgressLog.SuccessionFactorGains, trainerID, trainedChara),
insertRaces(conn, log, career.ProgressLog.RaceHistory, trainerID, trainedChara),
insertGainedSkills(conn, log, career.ProgressLog.GainSkillIDs, trainerID, trainedChara),
insertCareerSkills(conn, log, career.End.Chara.Skills, trainerID, trainedChara),
insertCareerSkillHints(conn, log, career.End.Chara.SkillTips, trainerID, trainedChara),
insertSupportCards(conn, log, career.End.Chara.SupportCards, trainerID, trainedChara),
insertStartSkills(conn, log, career.ProgressInfo.Chara.Skills, trainerID, trainedChara),
insertStartSkillHints(conn, log, career.ProgressInfo.Chara.SkillTips, trainerID, trainedChara),
)
return err
}
//go:embed insert-career.sql
var insertCareerSQL string
func insertCareer(conn *sqlite.Conn, log *slog.Logger, career *ism.WorkIdleSingleMode, trainerID int64) error {
log.Debug("insert career")
stmt := conn.Prep(insertCareerSQL)
stmt.SetInt64("$trainer_id", trainerID)
stmt.SetInt64("$trained_chara_id", int64(career.End.Chara.SingleModeCharaID))
stmt.SetInt64("$card_id", int64(career.End.Chara.CardID))
stmt.SetInt64("$scenario_id", int64(career.End.Chara.ScenarioID))
stmt.SetInt64("$route_id", int64(career.End.Chara.RouteID))
stmt.SetText("$start_time", career.ProgressInfo.StartTime)
stmt.SetText("$end_time", career.ProgressInfo.EndTime)
stmt.SetInt64("$chara_grade", int64(career.End.Chara.CharaGrade))
stmt.SetInt64("$star_level", int64(career.End.Chara.Rarity))
stmt.SetInt64("$potential_level", int64(career.End.Chara.TalentLevel))
stmt.SetInt64("$speed", int64(career.End.Chara.Speed))
stmt.SetInt64("$stamina", int64(career.End.Chara.Stamina))
stmt.SetInt64("$power", int64(career.End.Chara.Power))
stmt.SetInt64("$guts", int64(career.End.Chara.Guts))
stmt.SetInt64("$wit", int64(career.End.Chara.Wiz))
stmt.SetInt64("$energy", int64(career.End.Chara.Vital))
stmt.SetInt64("$max_speed", int64(career.End.Chara.MaxSpeed))
stmt.SetInt64("$max_stamina", int64(career.End.Chara.MaxStamina))
stmt.SetInt64("$max_power", int64(career.End.Chara.MaxPower))
stmt.SetInt64("$max_guts", int64(career.End.Chara.MaxGuts))
stmt.SetInt64("$max_wit", int64(career.End.Chara.MaxWiz))
stmt.SetInt64("$max_energy", int64(career.End.Chara.MaxVital))
stmt.SetInt64("$skill_point", int64(career.ProgressLog.TotalSkillPoint))
stmt.SetInt64("$mood", int64(career.End.Chara.Motivation))
stmt.SetInt64("$turf", int64(career.End.Chara.ProperGroundTurf))
stmt.SetInt64("$dirt", int64(career.End.Chara.ProperGroundDirt))
stmt.SetInt64("$sprint", int64(career.End.Chara.ProperDistanceShort))
stmt.SetInt64("$mile", int64(career.End.Chara.ProperDistanceMile))
stmt.SetInt64("$medium", int64(career.End.Chara.ProperDistanceMiddle))
stmt.SetInt64("$long", int64(career.End.Chara.ProperDistanceLong))
stmt.SetInt64("$front_runner", int64(career.End.Chara.ProperRunningStyleNige))
stmt.SetInt64("$pace_chaser", int64(career.End.Chara.ProperRunningStyleSenko))
stmt.SetInt64("$late_surger", int64(career.End.Chara.ProperRunningStyleSashi))
stmt.SetInt64("$end_closer", int64(career.End.Chara.ProperRunningStyleOikomi))
stmt.SetInt64("$start_speed", int64(career.ProgressInfo.Chara.Speed))
stmt.SetInt64("$start_stamina", int64(career.ProgressInfo.Chara.Stamina))
stmt.SetInt64("$start_power", int64(career.ProgressInfo.Chara.Power))
stmt.SetInt64("$start_guts", int64(career.ProgressInfo.Chara.Guts))
stmt.SetInt64("$start_wit", int64(career.ProgressInfo.Chara.Wiz))
stmt.SetInt64("$start_max_speed", int64(career.ProgressInfo.Chara.MaxSpeed))
stmt.SetInt64("$start_max_stamina", int64(career.ProgressInfo.Chara.MaxStamina))
stmt.SetInt64("$start_max_power", int64(career.ProgressInfo.Chara.MaxPower))
stmt.SetInt64("$start_max_guts", int64(career.ProgressInfo.Chara.MaxGuts))
stmt.SetInt64("$start_max_wit", int64(career.ProgressInfo.Chara.MaxWiz))
stmt.SetInt64("$start_turf", int64(career.ProgressInfo.Chara.ProperGroundTurf))
stmt.SetInt64("$start_dirt", int64(career.ProgressInfo.Chara.ProperGroundDirt))
stmt.SetInt64("$start_sprint", int64(career.ProgressInfo.Chara.ProperDistanceShort))
stmt.SetInt64("$start_mile", int64(career.ProgressInfo.Chara.ProperDistanceMile))
stmt.SetInt64("$start_medium", int64(career.ProgressInfo.Chara.ProperDistanceMiddle))
stmt.SetInt64("$start_long", int64(career.ProgressInfo.Chara.ProperDistanceLong))
stmt.SetInt64("$start_front_runner", int64(career.ProgressInfo.Chara.ProperRunningStyleNige))
stmt.SetInt64("$start_pace_chaser", int64(career.ProgressInfo.Chara.ProperRunningStyleSenko))
stmt.SetInt64("$start_late_surger", int64(career.ProgressInfo.Chara.ProperRunningStyleSashi))
stmt.SetInt64("$start_end_closer", int64(career.ProgressInfo.Chara.ProperRunningStyleOikomi))
stmt.SetInt64("$fans", int64(career.End.Chara.Fans))
stmt.SetInt64("$race_program_id", int64(career.End.Chara.RaceProgramID))
stmt.SetInt64("$reserve_race_program_id", int64(career.End.Chara.ReserveRaceProgramID))
stmt.SetInt64("$race_running_style", int64(career.End.Chara.RaceRunningStyle))
stmt.SetInt64("$is_short_race", int64(career.End.Chara.IsShortRace))
stmt.SetInt64("$succession_trained_chara_id_1", int64(career.End.Chara.SuccessionTrainedCharaID1))
stmt.SetInt64("$succession_trained_chara_id_2", int64(career.End.Chara.SuccessionTrainedCharaID2))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting career: %w", err)
}
return nil
}
func insertConditions(conn *sqlite.Conn, log *slog.Logger, cs []ism.CharaEffectLog, trainerID int64, trainedChara int32) error {
log.Debug("insert conditions")
const sql = `INSERT INTO condition VALUES (?, ?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, c := range cs {
stmt.BindInt64(3, int64(c.CharaEffectID))
stmt.BindBool(4, c.IsActive)
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting condition: %w", err)
}
}
return nil
}
//go:embed insert-gain.sql
var insertGainSQL string
func insertGain(conn *sqlite.Conn, log *slog.Logger, gain *ism.IdleSingleModeGainInfo, trainerID int64, trainedChara int32, source int32) error {
log.Debug("insert gain", slog.Int("source", int(source)))
stmt := conn.Prep(insertGainSQL)
stmt.SetInt64("$trainer_id", trainerID)
stmt.SetInt64("$trained_chara_id", int64(trainedChara))
stmt.SetInt64("$source", int64(source))
stmt.SetInt64("$gain_speed", int64(gain.Speed.Value))
stmt.SetInt64("$gain_stamina", int64(gain.Stamina.Value))
stmt.SetInt64("$gain_power", int64(gain.Power.Value))
stmt.SetInt64("$gain_guts", int64(gain.Guts.Value))
stmt.SetInt64("$gain_wit", int64(gain.Wiz.Value))
stmt.SetInt64("$gain_skill_point", int64(gain.SkillPoint))
stmt.SetInt64("$gain_max_speed", int64(gain.MaxSpeed))
stmt.SetInt64("$gain_max_stamina", int64(gain.MaxStamina))
stmt.SetInt64("$gain_max_power", int64(gain.MaxPower))
stmt.SetInt64("$gain_max_guts", int64(gain.MaxGuts))
stmt.SetInt64("$gain_max_wit", int64(gain.MaxWiz))
stmt.SetInt64("$gain_turf", int64(gain.ProperGroundTurf))
stmt.SetInt64("$gain_dirt", int64(gain.ProperGroundDirt))
stmt.SetInt64("$gain_sprint", int64(gain.ProperDistanceShort))
stmt.SetInt64("$gain_mile", int64(gain.ProperDistanceMile))
stmt.SetInt64("$gain_medium", int64(gain.ProperDistanceMiddle))
stmt.SetInt64("$gain_long", int64(gain.ProperDistanceLong))
stmt.SetInt64("$gain_front_runner", int64(gain.ProperRunningStyleNige))
stmt.SetInt64("$gain_pace_chaser", int64(gain.ProperRunningStyleSenko))
stmt.SetInt64("$gain_late_surger", int64(gain.ProperRunningStyleSashi))
stmt.SetInt64("$gain_end_closer", int64(gain.ProperRunningStyleOikomi))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting gain info for source %d: %w", source, err)
}
return nil
}
func insertGainSkillHint(conn *sqlite.Conn, log *slog.Logger, tips []ism.SkillTip, trainerID int64, trainedChara int32, source int32) error {
log.Debug("insert gain skill hints", slog.Int("source", int(source)))
stmt := conn.Prep(`INSERT INTO gain_skill_hint VALUES (?, ?, ?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
stmt.BindInt64(3, int64(source))
for _, t := range tips {
stmt.BindInt64(4, int64(t.GroupID))
stmt.BindInt64(5, int64(t.Rarity))
stmt.BindInt64(6, int64(t.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting skill hint for group %d, rarity %d from source %d: %w", t.GroupID, t.Rarity, source, err)
}
}
return nil
}
func insertInspirations(conn *sqlite.Conn, log *slog.Logger, inspos []ism.IdleSingleModeSuccessionFactorGain, trainerID int64, trainedChara int32) error {
log.Debug("insert inspirations")
stmt := conn.Prep(`INSERT INTO inspiration VALUES (?, ?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, yr := range inspos {
stmt.BindInt64(3, int64(yr.Year))
for _, f := range yr.GainFactorInfo {
stmt.BindInt64(4, int64(f.FactorID))
stmt.BindInt64(5, int64(f.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting spark: %w", err)
}
}
}
return nil
}
func insertRaces(conn *sqlite.Conn, log *slog.Logger, races []ism.IdleSingleModeRaceHistory, trainerID int64, trainedChara int32) error {
log.Debug("insert races")
raceStmt := conn.Prep(`INSERT INTO race_history VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
rewardStmt := conn.Prep(`INSERT INTO race_reward VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
raceStmt.BindInt64(1, trainerID)
raceStmt.BindInt64(2, int64(trainedChara))
rewardStmt.BindInt64(1, trainerID)
rewardStmt.BindInt64(2, int64(trainedChara))
for _, r := range races {
raceStmt.BindInt64(3, int64(r.RaceHistory.Turn))
raceStmt.BindInt64(4, int64(r.RaceHistory.ProgramID))
raceStmt.BindInt64(5, int64(r.RaceHistory.Weather))
raceStmt.BindInt64(6, int64(r.RaceHistory.GroundCondition))
raceStmt.BindInt64(7, int64(r.RaceHistory.RunningStyle))
raceStmt.BindInt64(8, int64(r.RaceHistory.ResultRank))
raceStmt.BindInt64(9, int64(r.RaceHistory.FrameOrder))
raceStmt.BindInt64(10, int64(r.RaceRewardInfo.GainedFans))
raceStmt.BindInt64(11, int64(r.LoseTipsID))
_, err := raceStmt.Step()
err = errors.Join(err, raceStmt.Reset())
if err != nil {
return fmt.Errorf("inserting race history: %w", err)
}
rewardStmt.BindInt64(3, int64(r.RaceHistory.Turn))
rewardStmt.BindBool(7, false)
rewardStmt.BindBool(8, false)
rewardStmt.BindBool(9, false)
for _, reward := range r.RaceRewardInfo.RaceReward {
rewardStmt.BindInt64(4, int64(reward.ItemType))
rewardStmt.BindInt64(5, int64(reward.ItemID))
rewardStmt.BindInt64(6, int64(reward.ItemNum))
_, err := rewardStmt.Step()
err = errors.Join(err, rewardStmt.Reset())
if err != nil {
return fmt.Errorf("inserting race reward: %w", err)
}
}
rewardStmt.BindBool(7, true)
for _, reward := range r.RaceRewardInfo.RaceRewardBonus {
rewardStmt.BindInt64(4, int64(reward.ItemType))
rewardStmt.BindInt64(5, int64(reward.ItemID))
rewardStmt.BindInt64(6, int64(reward.ItemNum))
_, err := rewardStmt.Step()
err = errors.Join(err, rewardStmt.Reset())
if err != nil {
return fmt.Errorf("inserting race reward: %w", err)
}
}
rewardStmt.BindBool(7, false)
rewardStmt.BindBool(8, true)
for _, reward := range r.RaceRewardInfo.RaceRewardPlusBonus {
rewardStmt.BindInt64(4, int64(reward.ItemType))
rewardStmt.BindInt64(5, int64(reward.ItemID))
rewardStmt.BindInt64(6, int64(reward.ItemNum))
_, err := rewardStmt.Step()
err = errors.Join(err, rewardStmt.Reset())
if err != nil {
return fmt.Errorf("inserting race reward: %w", err)
}
}
rewardStmt.BindBool(8, false)
rewardStmt.BindBool(9, true)
for _, reward := range r.RaceRewardInfo.RaceRewardBonusWin {
rewardStmt.BindInt64(4, int64(reward.ItemType))
rewardStmt.BindInt64(5, int64(reward.ItemID))
rewardStmt.BindInt64(6, int64(reward.ItemNum))
_, err := rewardStmt.Step()
err = errors.Join(err, rewardStmt.Reset())
if err != nil {
return fmt.Errorf("inserting race reward: %w", err)
}
}
}
return nil
}
func insertGainedSkills(conn *sqlite.Conn, log *slog.Logger, skills []horse.SkillID, trainerID int64, trainedChara int32) error {
log.Debug("insert gained skills")
stmt := conn.Prep(`INSERT INTO gained_skill VALUES (?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, sk := range skills {
stmt.BindInt64(3, int64(sk))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting gain skill: %w", err)
}
}
return nil
}
func insertCareerSkills(conn *sqlite.Conn, log *slog.Logger, skills []ism.Skill, trainerID int64, trainedChara int32) error {
log.Debug("insert career skills")
stmt := conn.Prep(`INSERT INTO career_skill VALUES (?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, sk := range skills {
stmt.BindInt64(3, int64(sk.ID))
stmt.BindInt64(4, int64(sk.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting skill: %w", err)
}
}
return nil
}
func insertCareerSkillHints(conn *sqlite.Conn, log *slog.Logger, hints []ism.SkillTip, trainerID int64, trainedChara int32) error {
log.Debug("insert career skill hints")
stmt := conn.Prep(`INSERT INTO career_skill_hint VALUES (?, ?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, h := range hints {
stmt.BindInt64(3, int64(h.GroupID))
stmt.BindInt64(4, int64(h.Rarity))
stmt.BindInt64(5, int64(h.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting skill hint: %w", err)
}
}
return nil
}
func insertSupportCards(conn *sqlite.Conn, log *slog.Logger, supps []ism.SingleModeSupportCard, trainerID int64, trainedChara int32) error {
log.Debug("insert support cards")
stmt := conn.Prep(`INSERT INTO career_support_card VALUES (?, ?, ?, ?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, s := range supps {
stmt.BindInt64(3, int64(s.Position))
stmt.BindInt64(4, int64(s.ID))
stmt.BindInt64(5, int64(s.LimitBreak))
stmt.BindInt64(6, int64(s.Exp))
stmt.BindInt64(7, int64(s.OwnerID))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting support card: %w", err)
}
}
return nil
}
func insertStartSkills(conn *sqlite.Conn, log *slog.Logger, skills []ism.Skill, trainerID int64, trainedChara int32) error {
log.Debug("insert starting skills")
stmt := conn.Prep(`INSERT INTO start_skill VALUES (?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, sk := range skills {
stmt.BindInt64(3, int64(sk.ID))
stmt.BindInt64(4, int64(sk.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting skill: %w", err)
}
}
return nil
}
func insertStartSkillHints(conn *sqlite.Conn, log *slog.Logger, hints []ism.SkillTip, trainerID int64, trainedChara int32) error {
log.Debug("insert starting skill hints")
stmt := conn.Prep(`INSERT INTO start_skill_hint VALUES (?, ?, ?, ?, ?)`)
stmt.BindInt64(1, trainerID)
stmt.BindInt64(2, int64(trainedChara))
for _, h := range hints {
stmt.BindInt64(3, int64(h.GroupID))
stmt.BindInt64(4, int64(h.Rarity))
stmt.BindInt64(5, int64(h.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting skill hint: %w", err)
}
}
return nil
}
+69
View File
@@ -0,0 +1,69 @@
INSERT INTO career VALUES (
$trainer_id,
$trained_chara_id,
$card_id,
$scenario_id,
$route_id,
$start_time,
$end_time,
$chara_grade,
$star_level,
$potential_level,
$speed,
$stamina,
$power,
$guts,
$wit,
$energy,
$max_speed,
$max_stamina,
$max_power,
$max_guts,
$max_wit,
$max_energy,
$skill_point,
$mood,
$turf,
$dirt,
$sprint,
$mile,
$medium,
$long,
$front_runner,
$pace_chaser,
$late_surger,
$end_closer,
$start_speed,
$start_stamina,
$start_power,
$start_guts,
$start_wit,
$start_max_speed,
$start_max_stamina,
$start_max_power,
$start_max_guts,
$start_max_wit,
$start_turf,
$start_dirt,
$start_sprint,
$start_mile,
$start_medium,
$start_long,
$start_front_runner,
$start_pace_chaser,
$start_late_surger,
$start_end_closer,
$fans,
$race_program_id,
$reserve_race_program_id,
$race_running_style,
$is_short_race,
$succession_trained_chara_id_1,
$succession_trained_chara_id_2
)
+31
View File
@@ -0,0 +1,31 @@
INSERT INTO gain VALUES (
$trainer_id,
$trained_chara_id,
$source,
$gain_speed,
$gain_stamina,
$gain_power,
$gain_guts,
$gain_wit,
$gain_skill_point,
$gain_max_speed,
$gain_max_stamina,
$gain_max_power,
$gain_max_guts,
$gain_max_wit,
$gain_turf,
$gain_dirt,
$gain_sprint,
$gain_mile,
$gain_medium,
$gain_long,
$gain_front_runner,
$gain_pace_chaser,
$gain_late_surger,
$gain_end_closer
)
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"encoding/json"
"flag"
"log/slog"
"os"
"os/signal"
"path/filepath"
"git.sunturtle.xyz/zephyr/horse/umadump/ism"
)
func main() {
var (
uri, journalMode string
fs []string
trainer int64
logLevel slog.Level
)
flag.StringVar(&uri, "db", "idle_single_mode.sqlite3", "output database `URI` or filename")
flag.StringVar(&journalMode, "journal", "memory", "journal `mode` to apply to database connections")
flag.Int64Var(&trainer, "t", 0, "trainer `ID` to associate with the data")
flag.TextVar(&logLevel, "log", slog.LevelInfo, "log `level`")
flag.Func("d", "`dir`ectory containing idle single mode dumps to load (repeatable)", func(s string) error {
es, err := os.ReadDir(s)
for _, e := range es {
if e.IsDir() {
continue
}
fs = append(fs, filepath.Join(s, e.Name()))
}
return err
})
flag.Parse()
fs = append(fs, flag.Args()...)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
db := opendb(uri, journalMode)
ok := 0
for _, n := range fs {
log := log.With(slog.String("file", n))
log.Info("begin")
f, err := os.Open(n)
if err != nil {
log.Error("open", slog.Any("err", err))
continue
}
var career ism.WorkIdleSingleMode
d := json.NewDecoder(f)
d.DisallowUnknownFields()
log.Debug("decode")
if err := d.Decode(&career); err != nil {
log.Error("load", slog.Any("err", err))
continue
}
log.Debug("insert")
err = insert(ctx, db, log, &career, trainer)
if err != nil {
log.Error("insert", slog.Any("err", err))
continue
}
ok++
}
slog.Info("done", slog.Int("careers", ok))
}
+250
View File
@@ -0,0 +1,250 @@
CREATE TABLE IF NOT EXISTS career (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
card_id INTEGER NOT NULL,
scenario_id INTEGER NOT NULL,
route_id INTEGER NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
chara_grade INTEGER NOT NULL,
star_level INTEGER NOT NULL,
potential_level INTEGER NOT NULL,
speed INTEGER NOT NULL,
stamina INTEGER NOT NULL,
power INTEGER NOT NULL,
guts INTEGER NOT NULL,
wit INTEGER NOT NULL,
energy INTEGER NOT NULL,
max_speed INTEGER NOT NULL,
max_stamina INTEGER NOT NULL,
max_power INTEGER NOT NULL,
max_guts INTEGER NOT NULL,
max_wit INTEGER NOT NULL,
max_energy INTEGER NOT NULL,
skill_point INTEGER NOT NULL,
mood INTEGER NOT NULL,
turf INTEGER NOT NULL,
dirt INTEGER NOT NULL,
sprint INTEGER NOT NULL,
mile INTEGER NOT NULL,
medium INTEGER NOT NULL,
long INTEGER NOT NULL,
front_runner INTEGER NOT NULL,
pace_chaser INTEGER NOT NULL,
late_surger INTEGER NOT NULL,
end_closer INTEGER NOT NULL,
-- These fields are 0 if the career finished while it was not actively
-- displayed on the client used to dump it.
start_speed INTEGER NOT NULL,
start_stamina INTEGER NOT NULL,
start_power INTEGER NOT NULL,
start_guts INTEGER NOT NULL,
start_wit INTEGER NOT NULL,
start_max_speed INTEGER NOT NULL,
start_max_stamina INTEGER NOT NULL,
start_max_power INTEGER NOT NULL,
start_max_guts INTEGER NOT NULL,
start_max_wit INTEGER NOT NULL,
start_turf INTEGER NOT NULL,
start_dirt INTEGER NOT NULL,
start_sprint INTEGER NOT NULL,
start_mile INTEGER NOT NULL,
start_medium INTEGER NOT NULL,
start_long INTEGER NOT NULL,
start_front_runner INTEGER NOT NULL,
start_pace_chaser INTEGER NOT NULL,
start_late_surger INTEGER NOT NULL,
start_end_closer INTEGER NOT NULL,
fans INTEGER NOT NULL,
race_program_id INTEGER NOT NULL,
reserve_race_program_id INTEGER NOT NULL,
race_running_style INTEGER NOT NULL,
is_short_race INTEGER NOT NULL,
succession_trained_chara_id_1 INTEGER NOT NULL,
succession_trained_chara_id_2 INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id)
) STRICT;
CREATE INDEX IF NOT EXISTS career_by_scenario ON career (scenario_id);
CREATE INDEX IF NOT EXISTS career_by_start_time ON career (start_time);
CREATE TABLE IF NOT EXISTS condition (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
condition INTEGER NOT NULL,
is_active INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, condition),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS gain (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
source INTEGER NOT NULL, -- 0 for events, 1 for inspiration, otherwise support card id
gain_speed INTEGER NOT NULL,
gain_stamina INTEGER NOT NULL,
gain_power INTEGER NOT NULL,
gain_guts INTEGER NOT NULL,
gain_wit INTEGER NOT NULL,
gain_skill_point INTEGER NOT NULL,
gain_max_speed INTEGER NOT NULL,
gain_max_stamina INTEGER NOT NULL,
gain_max_power INTEGER NOT NULL,
gain_max_guts INTEGER NOT NULL,
gain_max_wit INTEGER NOT NULL,
gain_turf INTEGER NOT NULL,
gain_dirt INTEGER NOT NULL,
gain_sprint INTEGER NOT NULL,
gain_mile INTEGER NOT NULL,
gain_medium INTEGER NOT NULL,
gain_long INTEGER NOT NULL,
gain_front_runner INTEGER NOT NULL,
gain_pace_chaser INTEGER NOT NULL,
gain_late_surger INTEGER NOT NULL,
gain_end_closer INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, source),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE INDEX IF NOT EXISTS gain_by_source ON gain (source);
CREATE TABLE IF NOT EXISTS gain_skill_hint (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
source INTEGER NOT NULL,
group_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, source, group_id, rarity),
FOREIGN KEY (trainer_id, trained_chara_id, source) REFERENCES gain ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS inspiration (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
year INTEGER NOT NULL,
factor_id INTEGER NOT NULL,
level INTEGER NOT NULL, -- always 0 but just in case we get it in the future
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE INDEX IF NOT EXISTS inspiration_by_career ON inspiration (trainer_id, trained_chara_id);
CREATE INDEX IF NOT EXISTS inspiration_by_factor_id ON inspiration (factor_id);
CREATE TABLE IF NOT EXISTS race_history (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
turn INTEGER NOT NULL,
program_id INTEGER NOT NULL,
weather INTEGER NOT NULL,
ground_condition INTEGER NOT NULL,
running_style INTEGER NOT NULL,
result_rank INTEGER NOT NULL,
frame_order INTEGER NOT NULL,
gained_fans INTEGER NOT NULL,
lose_tips_id INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, turn),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS race_reward (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
turn INTEGER NOT NULL,
item_type INTEGER NOT NULL,
item_id INTEGER NOT NULL,
item_num INTEGER NOT NULL,
is_bonus INTEGER NOT NULL,
is_plus_bonus INTEGER NOT NULL,
is_bonus_win INTEGER NOT NULL,
FOREIGN KEY (trainer_id, trained_chara_id, turn) REFERENCES race_history ON DELETE CASCADE
) STRICT;
CREATE INDEX IF NOT EXISTS race_reward_by_career_turn ON race_reward (trainer_id, trained_chara_id, turn);
CREATE INDEX IF NOT EXISTS race_reward_by_item ON race_reward (item_id);
CREATE TABLE IF NOT EXISTS gained_skill (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
skill_id INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, skill_id),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS career_skill (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, skill_id),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS career_skill_hint (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS career_support_card (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
position INTEGER NOT NULL,
support_card_id INTEGER NOT NULL,
limit_break_count INTEGER NOT NULL,
exp INTEGER NOT NULL,
owner_viewer_id INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, position),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE,
UNIQUE (trainer_id, trained_chara_id, support_card_id)
) STRICT;
CREATE INDEX IF NOT EXISTS career_support_card_by_card ON career_support_card (support_card_id, exp);
CREATE TABLE IF NOT EXISTS start_skill (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, skill_id),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS start_skill_hint (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
+88
View File
@@ -0,0 +1,88 @@
DROP VIEW IF EXISTS gain_event;
DROP VIEW IF EXISTS gain_inspiration;
DROP VIEW IF EXISTS gain_support_card;
CREATE VIEW gain_event AS
SELECT
trainer_id,
trained_chara_id,
gain_speed,
gain_stamina,
gain_power,
gain_guts,
gain_wit,
gain_skill_point,
gain_max_speed,
gain_max_stamina,
gain_max_power,
gain_max_guts,
gain_max_wit,
gain_turf,
gain_dirt,
gain_sprint,
gain_mile,
gain_medium,
gain_long,
gain_front_runner,
gain_pace_chaser,
gain_late_surger,
gain_end_closer
FROM gain
WHERE source = 0;
CREATE VIEW gain_inspiration AS
SELECT
trainer_id,
trained_chara_id,
gain_speed,
gain_stamina,
gain_power,
gain_guts,
gain_wit,
gain_skill_point,
gain_max_speed,
gain_max_stamina,
gain_max_power,
gain_max_guts,
gain_max_wit,
gain_turf,
gain_dirt,
gain_sprint,
gain_mile,
gain_medium,
gain_long,
gain_front_runner,
gain_pace_chaser,
gain_late_surger,
gain_end_closer
FROM gain
WHERE source = 1;
CREATE VIEW gain_support_card AS
SELECT
trainer_id,
trained_chara_id,
source AS support_card_id,
gain_speed,
gain_stamina,
gain_power,
gain_guts,
gain_wit,
gain_skill_point,
gain_max_speed,
gain_max_stamina,
gain_max_power,
gain_max_guts,
gain_max_wit,
gain_turf,
gain_dirt,
gain_sprint,
gain_mile,
gain_medium,
gain_long,
gain_front_runner,
gain_pace_chaser,
gain_late_surger,
gain_end_closer
FROM gain
WHERE source > 1;
+325
View File
@@ -0,0 +1,325 @@
package main
import (
"cmp"
"context"
_ "embed"
"errors"
"fmt"
"log/slog"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitemigration"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
const appID int32 = 0x4acedb
//go:embed schema-00.sql
var schema0 string
//go:embed schema-final.sql
var schemaFinal string
func opendb(uri, journalMode string) *sqlitemigration.Pool {
schema := sqlitemigration.Schema{
Migrations: []string{
schema0,
},
AppID: appID,
RepeatableMigration: schemaFinal,
}
j := "PRAGMA journal_mode=" + journalMode
opts := sqlitemigration.Options{
Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI,
PrepareConn: func(conn *sqlite.Conn) error {
if journalMode == "" {
return nil
}
stmt, _, err := conn.PrepareTransient(j)
if err != nil {
return err
}
_, err = stmt.Step()
return errors.Join(err, stmt.Finalize())
},
}
db := sqlitemigration.NewPool(uri, schema, opts)
return db
}
func insert(ctx context.Context, db *sqlitemigration.Pool, log *slog.Logger, race *replay.Race, simdata *replay.RaceSimulateData) (err error) {
conn, err := db.Take(ctx)
if err != nil {
return err
}
defer db.Put(conn)
defer sqlitex.Transaction(conn)(&err)
log.DebugContext(ctx, "horses",
slog.Int("num", int(race.NumRaceHorses)),
slog.Int("actual", len(race.RaceHorse)),
)
var trainedCharas [18]int64
for i := range race.RaceHorse {
c := &race.RaceHorse[i]
id, err := insertTrainedChara(conn, c)
if err != nil {
return err
}
trainedCharas[i] = id
if err := insertTrainedCharaSkill(conn, c, id); err != nil {
return err
}
log.DebugContext(ctx, "horse",
slog.String("trainer", c.TrainerName),
slog.Int("trained", int(c.ResponseHorseData.TrainedCharaID)),
slog.Int("card", int(c.ResponseHorseData.CardID)),
slog.Int64("id", id),
)
}
if err = insertRaceCourseSet(conn, race); err != nil {
return err
}
log.DebugContext(ctx, "course", slog.Int("id", int(race.RaceCourseSet.ID)))
configID, err := insertRaceConfig(conn, race)
if err != nil {
return err
}
log.DebugContext(ctx, "config", slog.Int64("id", configID))
raceID, err := insertRace(conn, race, simdata, &trainedCharas, configID)
if err != nil {
code := sqlite.ErrCode(err)
if code == sqlite.ResultConstraintUnique {
log.InfoContext(ctx, "already loaded")
return nil
}
return err
}
log.DebugContext(ctx, "race", slog.Int64("id", raceID))
err = errors.Join(
insertRaceHorses(ctx, log, conn, race, simdata, &trainedCharas, raceID),
insertRaceFrames(ctx, log, conn, simdata, raceID),
insertRaceEvents(ctx, log, conn, simdata, raceID),
)
return err
}
//go:embed insert-trained_chara.sql
var insertTrainedCharaSQL string
func insertTrainedChara(conn *sqlite.Conn, c *replay.HorseData) (int64, error) {
stmt := conn.Prep(insertTrainedCharaSQL)
trainer := cmp.Or(c.ResponseHorseData.ViewerID, c.ResponseHorseData.OwnerViewerID)
stmt.SetText("$trainer", c.TrainerName)
stmt.SetInt64("$trainer_id", trainer)
stmt.SetInt64("$trained_chara_id", int64(c.ResponseHorseData.TrainedCharaID))
stmt.SetInt64("$chara_id", int64(c.CharaID))
stmt.SetText("$chara_name", c.CharaName)
stmt.SetInt64("$card_id", int64(c.ResponseHorseData.CardID))
stmt.SetInt64("$rarity", int64(c.Rarity))
stmt.SetInt64("$mob_id", int64(c.MobID))
stmt.SetInt64("$raw_speed", int64(c.ResponseHorseData.Speed))
stmt.SetInt64("$raw_stamina", int64(c.ResponseHorseData.Stamina))
stmt.SetInt64("$raw_power", int64(c.ResponseHorseData.Pow))
stmt.SetInt64("$raw_guts", int64(c.ResponseHorseData.Guts))
stmt.SetInt64("$raw_wit", int64(c.ResponseHorseData.Wiz))
stmt.SetInt64("$turf", int64(c.ResponseHorseData.ProperGroundTurf))
stmt.SetInt64("$dirt", int64(c.ResponseHorseData.ProperGroundDirt))
stmt.SetInt64("$sprint", int64(c.ResponseHorseData.ProperDistanceShort))
stmt.SetInt64("$mile", int64(c.ResponseHorseData.ProperDistanceMile))
stmt.SetInt64("$medium", int64(c.ResponseHorseData.ProperDistanceMiddle))
stmt.SetInt64("$long", int64(c.ResponseHorseData.ProperDistanceLong))
stmt.SetInt64("$front_runner", int64(c.ResponseHorseData.ProperRunningStyleNige))
stmt.SetInt64("$pace_chaser", int64(c.ResponseHorseData.ProperRunningStyleSenko))
stmt.SetInt64("$late_surger", int64(c.ResponseHorseData.ProperRunningStyleSashi))
stmt.SetInt64("$end_closer", int64(c.ResponseHorseData.ProperRunningStyleOikomi))
stmt.SetInt64("$fans", int64(c.TrainedCharaData.Fans))
stmt.SetInt64("$rank", int64(c.TrainedCharaData.Rank))
stmt.SetInt64("$rank_score", int64(c.TrainedCharaData.RankScore))
r, err := sqlitex.ResultInt64(stmt)
err = errors.Join(err, stmt.Reset())
if err != nil {
err = fmt.Errorf("inserting trained chara: %w", err)
}
return r, err
}
func insertTrainedCharaSkill(conn *sqlite.Conn, c *replay.HorseData, id int64) error {
const sql = `INSERT OR IGNORE INTO trained_chara_skill VALUES (?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, id)
for _, sk := range c.ResponseHorseData.SkillArray {
stmt.BindInt64(2, int64(sk.SkillID))
stmt.BindInt64(3, int64(sk.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting trained chara id %d skill %d: %w", id, sk.SkillID, err)
}
}
return nil
}
func insertRaceCourseSet(conn *sqlite.Conn, race *replay.Race) error {
const sql = `INSERT OR IGNORE INTO race_course_set VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, int64(race.RaceCourseSet.ID))
stmt.BindInt64(2, int64(race.RaceCourseSet.RaceTrackID))
stmt.BindInt64(3, int64(race.RaceCourseSet.Distance))
stmt.BindInt64(4, int64(race.RaceCourseSet.Ground))
stmt.BindInt64(5, int64(race.RaceCourseSet.Inout))
stmt.BindInt64(6, int64(race.RaceCourseSet.Turn))
stmt.BindInt64(7, int64(race.RaceCourseSet.FenceSet))
stmt.BindInt64(8, int64(race.RaceCourseSet.FloatLaneMax))
stmt.BindInt64(9, int64(race.RaceCourseSet.CourseSetStatusID))
stmt.BindInt64(10, int64(race.RaceCourseSet.FinishTimeMin))
stmt.BindInt64(11, int64(race.RaceCourseSet.FinishTimeMinRandomRange))
stmt.BindInt64(12, int64(race.RaceCourseSet.FinishTimeMax))
stmt.BindInt64(13, int64(race.RaceCourseSet.FinishTimeMaxRandomRange))
stmt.BindFloat(14, float64(race.CourseSectionDistance))
stmt.BindInt64(15, int64(race.CourseDistanceType))
stmt.BindInt64(16, int64(race.CourseFurlongNum))
stmt.BindFloat(17, float64(race.BaseSpeed))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race course: %w", err)
}
return nil
}
//go:embed insert-race_config.sql
var insertRaceConfigSQL string
func insertRaceConfig(conn *sqlite.Conn, race *replay.Race) (int64, error) {
stmt := conn.Prep(insertRaceConfigSQL)
stmt.SetInt64("$type", int64(race.RaceType))
stmt.SetInt64("$course_set_id", int64(race.RaceCourseSet.ID))
stmt.SetInt64("$ground_condition", int64(race.GroundCondition))
stmt.SetInt64("$weather", int64(race.Weather))
stmt.SetInt64("$season", int64(race.Season))
stmt.SetInt64("$time", int64(race.Time))
stmt.SetInt64("$num_horse", int64(race.NumRaceHorses))
r, err := sqlitex.ResultInt64(stmt)
err = errors.Join(err, stmt.Reset())
if err != nil {
err = fmt.Errorf("inserting race config: %w", err)
}
return r, err
}
//go:embed insert-race.sql
var insertRaceSQL string
func insertRace(conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, configID int64) (int64, error) {
stmt := conn.Prep(insertRaceSQL)
stmt.SetInt64("$config_id", configID)
stmt.SetInt64("$seed", int64(race.RandomSeed))
stmt.SetFloat("$distance_diff_max", float64(simdata.DistanceDiffMax))
stmt.SetInt64("$frame_count", int64(simdata.FrameCount))
// zombiezen/sqlite doesn't seem to expose an API to find the index of a
// named parameter, so even though we were using names before, we switch
// to indices for trained chara IDs.
for i, id := range trainedCharas {
stmt.BindInt64(5+i, id)
}
r, err := sqlitex.ResultInt64(stmt)
err = errors.Join(err, stmt.Reset())
if err != nil {
err = fmt.Errorf("inserting race: %w", err)
}
return r, err
}
//go:embed insert-race_horse.sql
var insertRaceHorseSQL string
func insertRaceHorses(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, raceID int64) error {
stmt := conn.Prep(insertRaceHorseSQL)
stmt.SetInt64("$race", raceID)
for i, c := range race.RaceHorse {
log.DebugContext(ctx, "race horse", slog.Int64("trained", trainedCharas[i]), slog.Int("index", int(c.HorseIndex)))
stmt.SetInt64("$horse", trainedCharas[i])
stmt.SetInt64("$horse_index", int64(c.HorseIndex))
stmt.SetInt64("$post", int64(c.PostNumber))
stmt.SetInt64("$finish_order", int64(c.FinishOrder))
stmt.SetFloat("$finish_time_raw", float64(c.FinishTimeRaw))
stmt.SetFloat("$finish_time_scaled", float64(c.FinishTimeScaled))
stmt.SetFloat("$finish_diff_time_from_prev", float64(c.FinishDiffTimeFromPrev))
stmt.SetInt64("$running_style", int64(c.ResponseHorseData.RunningStyle))
stmt.SetInt64("$running_style_ex", int64(c.RunningStyleEx))
stmt.SetInt64("$active_distance_aptitude", int64(c.ActiveProperDistance))
stmt.SetInt64("$active_surface_aptitude", int64(c.ActiveProperGroundType))
stmt.SetInt64("$mood", int64(c.RaceParam.Motivation))
stmt.SetFloat("$mood_coef", float64(c.RaceParam.MotivationCoef))
stmt.SetFloat("$base_speed", float64(c.RaceParam.BaseSpeed))
stmt.SetFloat("$base_stamina", float64(c.RaceParam.BaseStamina))
stmt.SetFloat("$base_power", float64(c.RaceParam.BasePow))
stmt.SetFloat("$base_guts", float64(c.RaceParam.BaseGuts))
stmt.SetFloat("$base_wit", float64(c.RaceParam.BaseWiz))
stmt.SetInt64("$popularity", int64(c.Popularity))
stmt.SetInt64("$popularity_rank_left", int64(c.PopularityRankLeft))
stmt.SetInt64("$popularity_rank_center", int64(c.PopularityRankCenter))
stmt.SetInt64("$popularity_rank_right", int64(c.PopularityRankRight))
stmt.SetFloat("$start_delay", float64(simdata.HorseResult[i].StartDelayTime))
stmt.SetFloat("$spurt_start_distance", float64(simdata.HorseResult[i].LastSpurtStartDistance))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race horses: %w", err)
}
}
return nil
}
func insertRaceFrames(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
const sql = `INSERT INTO race_frame VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, raceID)
for _, f := range simdata.Frame {
log.DebugContext(ctx, "frame", slog.Float64("time", float64(f.Time)))
stmt.BindFloat(2, float64(f.Time))
for i, h := range f.HorseFrame {
stmt.BindInt64(3, int64(i))
stmt.BindFloat(4, float64(h.Distance))
stmt.BindInt64(5, int64(h.LanePosition))
stmt.BindInt64(6, int64(h.Speed))
stmt.BindInt64(7, int64(h.HP))
stmt.BindInt64(8, int64(h.TemptationMode))
stmt.BindInt64(9, int64(h.BlockFrontHorseIndex))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race frames: %w", err)
}
}
}
return nil
}
func insertRaceEvents(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
const sql = `INSERT INTO race_event VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
stmt := conn.Prep(sql)
for _, e := range simdata.Event {
log.DebugContext(ctx, "event", slog.Float64("time", float64(e.Event.FrameTime)), slog.Int("type", int(e.Event.Type)))
// Easier to clear all bindings every time now
// than to bind nulls to all the parameters that need them below.
stmt.ClearBindings()
stmt.BindInt64(1, raceID)
stmt.BindFloat(2, float64(e.Event.FrameTime))
stmt.BindInt64(3, int64(e.Event.Type))
for i, p := range e.Event.Param {
stmt.BindInt64(4+i, int64(p))
}
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race events: %w", err)
}
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
INSERT OR ROLLBACK INTO race (
config_id,
seed,
distance_diff_max,
frame_count,
horse_1,
horse_2,
horse_3,
horse_4,
horse_5,
horse_6,
horse_7,
horse_8,
horse_9,
horse_10,
horse_11,
horse_12,
horse_13,
horse_14,
horse_15,
horse_16,
horse_17,
horse_18
)
VALUES (
$config_id,
$seed,
$distance_diff_max,
$frame_count,
$horse_1,
$horse_2,
$horse_3,
$horse_4,
$horse_5,
$horse_6,
$horse_7,
$horse_8,
$horse_9,
$horse_10,
$horse_11,
$horse_12,
$horse_13,
$horse_14,
$horse_15,
$horse_16,
$horse_17,
$horse_18
)
RETURNING id
+4
View File
@@ -0,0 +1,4 @@
INSERT INTO race_config (type, course_set_id, ground_condition, weather, season, time, num_horse)
VALUES ($type, $course_set_id, $ground_condition, $weather, $season, $time, $num_horse)
ON CONFLICT DO UPDATE SET id=id
RETURNING id
+12
View File
@@ -0,0 +1,12 @@
INSERT INTO race_horse
VALUES (
$race, $horse,
$horse_index, $post,
$finish_order, $finish_time_raw, $finish_time_scaled, $finish_diff_time_from_prev,
$running_style, $running_style_ex,
$active_distance_aptitude, $active_surface_aptitude,
$mood, $mood_coef,
$base_speed, $base_stamina, $base_power, $base_guts, $base_wit,
$popularity, $popularity_rank_left, $popularity_rank_center, $popularity_rank_right,
$start_delay, $spurt_start_distance
)
+19
View File
@@ -0,0 +1,19 @@
INSERT INTO trained_chara (
trainer, trainer_id, trained_chara_id,
chara_id, chara_name, card_id, rarity, mob_id,
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
turf, dirt,
sprint, mile, medium, long,
front_runner, pace_chaser, late_surger, end_closer,
fans, rank, rank_score
) VALUES (
$trainer, $trainer_id, $trained_chara_id,
$chara_id, $chara_name, $card_id, $rarity, $mob_id,
$raw_speed, $raw_stamina, $raw_power, $raw_guts, $raw_wit,
$turf, $dirt,
$sprint, $mile, $medium, $long,
$front_runner, $pace_chaser, $late_surger, $end_closer,
$fans, $rank, $rank_score
)
ON CONFLICT DO UPDATE SET id=id
RETURNING id
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"encoding/json"
"flag"
"log/slog"
"os"
"os/signal"
"path/filepath"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
func main() {
var (
uri, journalMode string
fs []string
)
flag.StringVar(&uri, "db", "race.sqlite3", "output database `URI` or filename")
flag.StringVar(&journalMode, "journal", "memory", "journal `mode` to apply to database connections")
flag.Func("d", "`dir`ectory containing race dumps to load (repeatable)", func(s string) error {
es, err := os.ReadDir(s)
for _, e := range es {
if e.IsDir() {
continue
}
fs = append(fs, filepath.Join(s, e.Name()))
}
return err
})
flag.Parse()
fs = append(fs, flag.Args()...)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
db := opendb(uri, journalMode)
ok := 0
for _, n := range fs {
log := slog.With(slog.String("file", n))
log.Info("begin")
f, err := os.Open(n)
if err != nil {
log.Error("open", slog.Any("err", err))
continue
}
var race replay.Race
d := json.NewDecoder(f)
d.DisallowUnknownFields()
log.Debug("decode")
if err := d.Decode(&race); err != nil {
log.Error("load", slog.Any("err", err))
continue
}
log.Debug("simdata")
simdata, err := replay.DecodeSim(race.SimDataBase64)
if err != nil {
log.Error("simdata", slog.Any("err", err))
continue
}
log.Debug("insert")
err = insert(ctx, db, log, &race, simdata)
if err != nil {
log.Error("insert", slog.Any("err", err))
continue
}
ok++
}
slog.Info("done", slog.Int("races", ok))
}
+234
View File
@@ -0,0 +1,234 @@
-- trained_chara lists horses who appear in races.
CREATE TABLE IF NOT EXISTS trained_chara (
-- id is the DB-internal ID of the Uma.
-- It does not come from the game and should not be supplied in DDL.
id INTEGER PRIMARY KEY,
trainer TEXT NOT NULL,
trainer_id INTEGER NOT NULL, -- 0 for NPC umas e.g. in practice races
trained_chara_id INTEGER NOT NULL,
chara_id INTEGER NOT NULL,
chara_name TEXT NOT NULL,
-- card_id is the Uma herself, e.g. 100201 for [Innocent Silence] Silence Suzuka.
card_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
mob_id INTEGER NOT NULL, -- 0 for Umas that are not mobs
-- Raw stats.
raw_speed INTEGER NOT NULL,
raw_stamina INTEGER NOT NULL,
raw_power INTEGER NOT NULL,
raw_guts INTEGER NOT NULL,
raw_wit INTEGER NOT NULL,
-- Aptitudes.
turf INTEGER NOT NULL,
dirt INTEGER NOT NULL,
sprint INTEGER NOT NULL,
mile INTEGER NOT NULL,
medium INTEGER NOT NULL,
long INTEGER NOT NULL,
front_runner INTEGER NOT NULL,
pace_chaser INTEGER NOT NULL,
late_surger INTEGER NOT NULL,
end_closer INTEGER NOT NULL,
-- Other data.
fans INTEGER NOT NULL,
rank INTEGER NOT NULL,
rank_score INTEGER NOT NULL,
UNIQUE (trainer_id, trained_chara_id)
) STRICT;
-- Insert trained chara ID 0 to represent no Uma.
INSERT OR IGNORE INTO trained_chara (
id,
trainer,
trainer_id,
trained_chara_id,
chara_id, chara_name,
card_id,
mob_id,
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
turf, dirt,
sprint, mile, medium, long,
front_runner, pace_chaser, late_surger, end_closer,
fans,
rank, rank_score
)
VALUES (0, '', 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
-- trained_chara_skill lists the skills each observed horse owns.
CREATE TABLE IF NOT EXISTS trained_chara_skill (
trained_chara INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
UNIQUE (trained_chara, skill_id)
) STRICT;
CREATE INDEX IF NOT EXISTS trained_chara_skill_by_trained_chara ON trained_chara_skill (trained_chara);
-- race_course_set is the accumulated information of observed race courses.
CREATE TABLE IF NOT EXISTS race_course_set (
id INTEGER PRIMARY KEY,
race_track_id INTEGER NOT NULL,
distance INTEGER NOT NULL,
ground INTEGER NOT NULL,
inout INTEGER NOT NULL,
turn INTEGER NOT NULL,
fence_set INTEGER NOT NULL,
float_lane_max INTEGER NOT NULL,
course_set_status_id INTEGER NOT NULL,
finish_time_min INTEGER NOT NULL,
finish_time_min_random_range INTEGER NOT NULL,
finish_time_max INTEGER NOT NULL,
finish_time_max_random_range INTEGER NOT NULL,
section_distance REAL NOT NULL,
distance_type INTEGER NOT NULL,
furlong_number INTEGER NOT NULL,
base_speed REAL NOT NULL
) STRICT;
-- race_config is observed configurations of races including the race type.
CREATE TABLE IF NOT EXISTS race_config (
-- id is the DB-internal ID of the race config.
id INTEGER PRIMARY KEY,
type INTEGER NOT NULL,
course_set_id INTEGER NOT NULL REFERENCES race_course_set (id) ON DELETE RESTRICT,
ground_condition INTEGER NOT NULL,
weather INTEGER NOT NULL,
season INTEGER NOT NULL,
time INTEGER NOT NULL,
num_horse INTEGER NOT NULL,
UNIQUE (type, course_set_id, ground_condition, weather, season, time, num_horse)
) STRICT;
CREATE INDEX IF NOT EXISTS race_config_by_type ON race_config (type);
-- race is all observed races.
CREATE TABLE IF NOT EXISTS race (
-- id is the DB-internal ID of the race.
id INTEGER PRIMARY KEY,
config_id INTEGER NOT NULL REFERENCES race_config (id),
seed INTEGER NOT NULL,
distance_diff_max REAL NOT NULL,
frame_count INTEGER NOT NULL,
-- There is no consistent ID for races, and even race seeds have been found
-- to collide well above the expected rate for a uniform distribution.
-- In order to prevent adding duplicates of races each time the tool is run,
-- we include the list of all horse IDs present in the race.
-- Analysis on the horses in the race should instead use the race_horse table.
-- Horse entries here are 0 where there is actually no horse at that gate.
horse_1 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_2 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_3 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_4 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_5 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_6 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_7 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_8 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_9 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_10 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_11 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_12 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_13 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_14 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_15 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_16 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_17 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_18 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
UNIQUE (
config_id,
seed,
horse_1,
horse_2,
horse_3,
horse_4,
horse_5,
horse_6,
horse_7,
horse_8,
horse_9,
horse_10,
horse_11,
horse_12,
horse_13,
horse_14,
horse_15,
horse_16,
horse_17,
horse_18
)
) STRICT;
CREATE INDEX IF NOT EXISTS race_by_config ON race (config_id);
-- race_horse pairs each instance of a horse with its condition and performance
-- in a given race.
CREATE TABLE IF NOT EXISTS race_horse (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
horse INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_index INTEGER NOT NULL,
post INTEGER NOT NULL,
finish_order INTEGER NOT NULL,
finish_time_raw REAL NOT NULL,
finish_time_scaled REAL NOT NULL,
finish_diff_time_from_prev REAL NOT NULL,
running_style INTEGER NOT NULL,
running_style_ex INTEGER NOT NULL,
active_distance_aptitude INTEGER NOT NULL,
active_surface_aptitude INTEGER NOT NULL,
mood INTEGER NOT NULL,
mood_coef REAL NOT NULL,
base_speed REAL NOT NULL,
base_stamina REAL NOT NULL,
base_power REAL NOT NULL,
base_guts REAL NOT NULL,
base_wit REAL NOT NULL,
popularity INTEGER NOT NULL,
popularity_rank_left INTEGER NOT NULL,
popularity_rank_center INTEGER NOT NULL,
popularity_rank_right INTEGER NOT NULL,
start_delay REAL NOT NULL,
spurt_start_distance REAL NOT NULL,
PRIMARY KEY (race, horse),
UNIQUE (race, horse_index),
CHECK (horse != 0)
) STRICT;
-- race_frame holds the actual received frames.
CREATE TABLE IF NOT EXISTS race_frame (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
frame_time REAL NOT NULL,
horse_index INTEGER NOT NULL,
distance REAL NOT NULL,
lane INTEGER NOT NULL, -- scale -4
speed INTEGER NOT NULL, -- scale -2
hp INTEGER NOT NULL,
temptation INTEGER NOT NULL,
block_front_horse_index INTEGER NOT NULL,
PRIMARY KEY (race, frame_time, horse_index)
) STRICT;
-- race_event holds raw received race events.
-- Views such as race_event_skill_activate, race_event_spot_struggle_activate, &c.
-- interpret events per type.
CREATE TABLE IF NOT EXISTS race_event (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
frame_time REAL NOT NULL,
type INTEGER NOT NULL,
param_0 INTEGER,
param_1 INTEGER,
param_2 INTEGER,
param_3 INTEGER,
param_4 INTEGER,
param_5 INTEGER
) STRICT;
CREATE INDEX IF NOT EXISTS race_event_by_race ON race_event (race);
CREATE INDEX IF NOT EXISTS race_event_by_type ON race_event (type);
+36
View File
@@ -0,0 +1,36 @@
DROP VIEW IF EXISTS race_event_skill_activate;
DROP VIEW IF EXISTS race_event_spot_struggle_activate;
DROP VIEW IF EXISTS race_event_duel_activate;
DROP VIEW IF EXISTS race_event_release_conserve_power;
CREATE VIEW race_event_skill_activate AS
SELECT
race,
frame_time,
param_0 AS user_index,
param_1 AS skill,
param_2 * 0.0001 AS duration,
param_3 AS branch,
param_4 AS hit_bitmask
FROM race_event WHERE type = 3;
CREATE VIEW race_event_spot_struggle_activate AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 4;
CREATE VIEW race_event_duel_activate AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 5;
CREATE VIEW race_event_release_conserve_power AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 6;
+59
View File
@@ -0,0 +1,59 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
func main() {
var out string
flag.StringVar(&out, "o", "", "output `file`")
flag.Parse()
r := os.Stdin
if flag.NArg() > 0 {
f, err := os.Open(flag.Arg(0))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
r = f
defer f.Close()
}
w := os.Stdout
if out != "" {
f, err := os.Create(out)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
w = f
defer f.Close()
}
var race replay.Race
d := json.NewDecoder(r)
d.DisallowUnknownFields()
if err := d.Decode(&race); err != nil {
fmt.Fprintln(os.Stderr, "replay:", err)
os.Exit(1)
}
simdata, err := replay.DecodeSim(race.SimDataBase64)
if err != nil {
fmt.Fprintln(os.Stderr, "simdata:", err)
}
b, err := json.Marshal(simdata)
if err != nil {
panic(err)
}
_, err = w.Write(b)
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
+14 -15
View File
@@ -1,15 +1,15 @@
module git.sunturtle.xyz/zephyr/horse
go 1.25.5
go 1.27.3
require (
github.com/disgoorg/disgo v0.19.6
github.com/gen2brain/avif v0.6.0
github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/chi/v5 v5.5.0
github.com/google/go-cmp v0.7.0
github.com/junegunn/fzf v0.74.1
golang.org/x/image v0.44.0
golang.org/x/sync v0.22.0
github.com/junegunn/fzf v0.74.5
golang.org/x/image v0.47.0
golang.org/x/sync v0.24.0
zombiezen.com/go/sqlite v1.4.2
)
@@ -18,23 +18,22 @@ require (
github.com/disgoorg/json/v2 v2.0.0 // indirect
github.com/disgoorg/omit v1.0.0 // indirect
github.com/disgoorg/snowflake/v2 v2.0.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.2 // indirect
github.com/dustin/go-humanize v1.1.0 // indirect
github.com/ebitengine/purego v0.11.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/compress v1.20.3 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/ncruces/go-strftime v1.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/tools v0.48.0 // indirect
modernc.org/libc v1.74.3 // indirect
golang.org/x/crypto v0.58.0 // indirect
golang.org/x/sys v0.49.0 // indirect
modernc.org/libc v1.79.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.54.0 // indirect
modernc.org/memory v1.12.1 // indirect
modernc.org/sqlite v1.61.0 // indirect
)
+38 -38
View File
@@ -10,14 +10,14 @@ github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78=
github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ=
github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro=
github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
github.com/ebitengine/purego v0.11.2 h1:WWiTx8rD8zYdOwSj8O/NiNbsOvSAgJMTlyV1XlbUSeU=
github.com/ebitengine/purego v0.11.2/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
github.com/gen2brain/avif v0.6.0/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/chi/v5 v5.5.0 h1:xdKQ1dJmVJsmgahrXSKhUx8NyVPzpwdlyrrQFpMPJgg=
github.com/go-chi/chi/v5 v5.5.0/go.mod h1:d1IG7GtKPSjPyBP365PH8sAGVHk3qfB/IhjFNRH1wWM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -26,16 +26,16 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/junegunn/fzf v0.74.1 h1:rCGVdCSoOkfcfTxtxCO/vG2yceDjRlKSlv7IfCGkc/Y=
github.com/junegunn/fzf v0.74.1/go.mod h1:hKDkO8orBWT5VLIZOL8oyhxn7mRLXdGyu+gz1Ss58pU=
github.com/junegunn/fzf v0.74.5 h1:mpJlCdgGuddKerHrdjvV3cnezEBgyjZAVn8KCo/4OMc=
github.com/junegunn/fzf v0.74.5/go.mod h1:TBRY54j3mGjAm4JcpDyiUIuDq38QlHvAzrB3GLKxQXM=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.20.3 h1:zWNoFqq/lSuYQRim3dUXlXeA0IqwuZGicdG5ZTfY5j4=
github.com/klauspost/compress v1.20.3/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -48,46 +48,46 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys=
modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4=
modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o=
modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws=
modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4=
modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8=
modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/sqlite v1.61.0 h1:sQi63oLQ9uquAUA4CzOisH35Q3pEszkPBuHM0tFL8CY=
modernc.org/sqlite v1.61.0/go.mod h1:f4gr5tbpx2umklaT/QVtbi/50hNdM+4ng+2zMo8KXJ0=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+2 -3
View File
@@ -140,8 +140,7 @@ func spansslopes(id horse.RacecourseID, evts []courseEvent, length float32) ([]h
// Start of the straight.
span = horse.RacecourseSpan{
Start: evt.Distance,
Straight: true,
FarStraight: evt.Values[1] == 2,
Straight: int8(evt.Values[1]),
}
// Continue out of the switch.
}
@@ -154,7 +153,7 @@ func spansslopes(id horse.RacecourseID, evts []courseEvent, length float32) ([]h
// Longchamp 1000 (which doesn't appear to actually be used but w/e)
// defines straights by their start positions only, so check for the
// previous event being a straight with no end.
if prev.Straight && prev.End == 0 {
if prev.Straight != 0 && prev.End == 0 {
spans[len(spans)-1].End = span.Start
} else {
// Otherwise, insert a new span which is neither straight nor corner.
+12 -58
View File
@@ -35,7 +35,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 0,
End: 649.9000244140625,
Straight: true,
Straight: 1,
},
{
Start: 649.9000244140625,
@@ -44,7 +44,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 650,
End: 1000,
Straight: true,
Straight: 1,
},
},
Slopes: []horse.Slope{},
@@ -66,8 +66,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 0,
End: 458,
Straight: true,
FarStraight: true,
Straight: 2,
},
{
Start: 458,
@@ -82,7 +81,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 958,
End: 1450,
Straight: true,
Straight: 1,
},
{
Start: 1450,
@@ -97,8 +96,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 1850,
End: 2300,
Straight: true,
FarStraight: true,
Straight: 2,
},
{
Start: 2300,
@@ -113,7 +111,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 2797,
End: 3200,
Straight: true,
Straight: 1,
},
},
Slopes: []horse.Slope{
@@ -166,7 +164,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 0,
End: 500,
Straight: true,
Straight: 1,
},
{
Start: 500,
@@ -181,8 +179,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 800,
End: 1300,
Straight: true,
FarStraight: true,
Straight: 2,
},
{
Start: 1300,
@@ -197,54 +194,12 @@ func TestRacecourses(t *testing.T) {
{
Start: 1614,
End: 2000,
Straight: true,
Straight: 1,
},
},
Slopes: []horse.Slope{},
MoveLanePoint: 500,
},
{
ID: 11203,
TrackID: 10201,
TrackName: "Longchamp",
Distance: 2400,
Ground: 1,
InOut: 1,
Turn: 1,
LaneMax: 12000,
Threshold1: 3,
Threshold2: 0,
LaneStart: [18]horse.TenThousandths{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5000, 5555, 6111, 6666, 7222, 8811, 9366, 9922, 10477},
Spans: []horse.RacecourseSpan{
{
Start: 0,
End: 798,
Straight: true,
FarStraight: true,
},
{
Start: 798,
End: 800,
},
{
Start: 800,
End: 1200,
Corner: 3,
},
{
Start: 1200,
End: 1600,
Corner: 4,
},
{
Start: 1600,
End: 2400,
Straight: true,
},
},
Slopes: []horse.Slope{},
MoveLanePoint: 800,
},
{
ID: 11504,
TrackID: 10105,
@@ -265,7 +220,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 100,
End: 500,
Straight: true,
Straight: 1,
},
{
Start: 500,
@@ -280,8 +235,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 900,
End: 1300,
Straight: true,
FarStraight: true,
Straight: 2,
},
{
Start: 1300,
@@ -296,7 +250,7 @@ func TestRacecourses(t *testing.T) {
{
Start: 1700,
End: 2000,
Straight: true,
Straight: 1,
},
},
Slopes: []horse.Slope{
+3 -2
View File
@@ -17,6 +17,7 @@ FROM race_course_set c
JOIN track ON c.race_track_id = track.id
JOIN race_track t ON c.race_track_id = t.id
LEFT JOIN race_course_set_status ss ON c.course_set_status_id = ss.course_set_status_id
-- Exclude unused Longchamp courses with busted data.
WHERE c.id NOT IN (11201, 11202)
-- Only include courses that can be selected for room matches.
-- Some other courses exist in the game files but have busted data.
WHERE c.id IN (SELECT course_set FROM race WHERE "group" = 12)
ORDER BY c.id
+1 -1
View File
@@ -6,7 +6,7 @@ WITH skill_names AS (
FROM text_data n
JOIN text_data d ON n."index" = d."index" AND n."category" = 47 AND d."category" = 48
), skill_groups AS (
SELECT
SELECT DISTINCT
group_id,
name
FROM skill_data d
+61
View File
@@ -40,3 +40,64 @@ INSERT INTO race_track (id, initial_lane_type, enable_half_gate, horse_num_gate_
INSERT INTO race_track (id, initial_lane_type, enable_half_gate, horse_num_gate_variation, turf_vision_type, footsmoke_color_type, area, flag_type, gate_panel_type, gate_lamp_type, board_condition_type, result_board_type) VALUES (10101, 2, 0, 1, 2, 2, 2, 1, 1, 1, 3, 0);
INSERT INTO race_track (id, initial_lane_type, enable_half_gate, horse_num_gate_variation, turf_vision_type, footsmoke_color_type, area, flag_type, gate_panel_type, gate_lamp_type, board_condition_type, result_board_type) VALUES (10105, 4, 1, 0, 2, 2, 1, 1, 1, 1, 4, 0);
INSERT INTO race_track (id, initial_lane_type, enable_half_gate, horse_num_gate_variation, turf_vision_type, footsmoke_color_type, area, flag_type, gate_panel_type, gate_lamp_type, board_condition_type, result_board_type) VALUES (10201, 3, 1, 0, 1, 1, 5, 0, 2, 2, 1, 0);
CREATE TABLE IF NOT EXISTS 'race' ('id' INTEGER NOT NULL, 'group' INTEGER NOT NULL, 'grade' INTEGER NOT NULL, 'course_set' INTEGER NOT NULL, 'thumbnail_id' INTEGER NOT NULL, 'ff_cue_name' TEXT NOT NULL, 'ff_cuesheet_name' TEXT NOT NULL, 'real_ff_cue_name' TEXT NOT NULL, 'real_ff_cuesheet_name' TEXT NOT NULL, 'ff_anim' INTEGER NOT NULL, 'ff_camera' INTEGER NOT NULL, 'ff_camera_sub' INTEGER NOT NULL, 'ff_sub' INTEGER NOT NULL, 'start_gate' INTEGER NOT NULL, 'start_gate_panel' INTEGER NOT NULL, 'goal_gate' INTEGER NOT NULL, 'goal_flower' INTEGER NOT NULL, 'result_podium' INTEGER NOT NULL, 'audience_group_id' INTEGER NOT NULL, 'paddock_bg_id' INTEGER NOT NULL, 'audience' INTEGER NOT NULL, 'entry_num' INTEGER NOT NULL, 'change_full_gate' INTEGER NOT NULL, 'is_dirtgrade' INTEGER NOT NULL, 'start_date' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM race WHERE course_set IN (10301, 10811, 11103, 11203, 11504);
INSERT INTO race VALUES(3042,1,300,10301,3042,'snd_mfx_1007_CL','snd_mfx_1007_CL','snd_mfx_r_1007_CL','snd_mfx_r_1007_CL',3042,207,0,0,0,0,0,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(4055,1,400,10301,4055,'snd_mfx_1012_CL','snd_mfx_1012_CL','snd_mfx_r_1012_CL','snd_mfx_r_1012_CL',4055,312,0,0,0,0,0,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(4101,1,400,10301,4101,'snd_mfx_1012_CL','snd_mfx_1012_CL','snd_mfx_r_1012_CL','snd_mfx_r_1012_CL',4101,312,0,0,0,0,0,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(5032,6,900,10301,5002,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',5002,312,0,0,0,0,0,0,-1,0,0,1,18,0,0,1636977600);
INSERT INTO race VALUES(5226,12,999,10301,5200,'snd_mfx_2008_CL','snd_mfx_2008_CL','','',5200,121,0,0,0,0,0,0,-1,0,0,0,18,0,0,1627560000);
INSERT INTO race VALUES(6136,9,100,10301,6100,'snd_mfx_2004_CL','snd_mfx_2004_CL','','',6100,121,0,0,0,0,0,0,-1,0,0,0,12,0,0,1498870800);
INSERT INTO race VALUES(6137,9,100,10301,6101,'snd_mfx_2004_CL','snd_mfx_2004_CL','','',6101,121,0,0,0,0,0,0,-1,0,0,0,12,0,0,1498870800);
INSERT INTO race VALUES(6138,9,100,10301,6102,'snd_mfx_2004_CL','snd_mfx_2004_CL','','',6102,121,0,0,0,0,0,0,-1,0,0,0,12,0,0,1498870800);
INSERT INTO race VALUES(9307,7,800,10301,9005,'snd_mfx_1012_CL','snd_mfx_1012_CL','snd_mfx_r_1012_CL','snd_mfx_r_1012_CL',9005,312,0,0,0,0,0,0,-1,0,0,0,9,0,0,1498870800);
INSERT INTO race VALUES(9323,7,800,10301,9003,'snd_mfx_1012_CL','snd_mfx_1012_CL','snd_mfx_r_1012_CL','snd_mfx_r_1012_CL',9003,312,0,0,0,0,0,0,-1,0,0,0,9,0,0,1498870800);
INSERT INTO race VALUES(1006,1,100,10811,1006,'snd_mfx_1002_CL','snd_mfx_1002_CL','snd_mfx_r_1002_CL','snd_mfx_r_1002_CL',1006,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(4905,61,100,10811,1006,'snd_mfx_1002_CL','snd_mfx_1002_CL','','',1006,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(5070,6,900,10811,5002,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',5002,313,0,0,0,0,0,0,-1,0,0,1,18,0,0,1636977600);
INSERT INTO race VALUES(5113,6,100,10811,1006,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',1006,313,0,0,0,0,1,1,-1,0,0,1,18,0,0,1636977600);
INSERT INTO race VALUES(5257,12,999,10811,5200,'snd_mfx_2008_CL','snd_mfx_2008_CL','','',5200,121,0,0,0,0,0,0,-1,0,0,0,18,0,0,1627560000);
INSERT INTO race VALUES(5930,8,100,10811,5930,'snd_mfx_2003_CL','snd_mfx_2003_CL','','',5930,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1623931200);
INSERT INTO race VALUES(5931,8,100,10811,5930,'snd_mfx_2003_CL','snd_mfx_2003_CL','','',5930,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1623931200);
INSERT INTO race VALUES(5932,8,100,10811,5930,'snd_mfx_2003_CL','snd_mfx_2003_CL','','',5930,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1623931200);
INSERT INTO race VALUES(5933,8,100,10811,5930,'snd_mfx_2003_CL','snd_mfx_2003_CL','','',5930,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1623931200);
INSERT INTO race VALUES(6006,11,100,10811,6006,'snd_mfx_2005_CL','snd_mfx_2005_CL','','',6006,122,0,0,0,0,0,0,-1,0,0,0,9,0,0,1623326400);
INSERT INTO race VALUES(6018,11,100,10811,6006,'snd_mfx_2006_CL','snd_mfx_2006_CL','','',6006,123,0,0,0,0,0,0,-1,0,0,0,9,0,0,1623326400);
INSERT INTO race VALUES(6283,9,100,10811,6100,'snd_mfx_2004_CL','snd_mfx_2004_CL','','',6100,121,0,0,0,0,0,0,-1,0,0,0,12,0,0,1498870800);
INSERT INTO race VALUES(6284,9,100,10811,6101,'snd_mfx_2004_CL','snd_mfx_2004_CL','','',6101,121,0,0,0,0,0,0,-1,0,0,0,12,0,0,1498870800);
INSERT INTO race VALUES(6285,9,100,10811,6102,'snd_mfx_2004_CL','snd_mfx_2004_CL','','',6102,121,0,0,0,0,0,0,-1,0,0,0,12,0,0,1498870800);
INSERT INTO race VALUES(7004,4,0,10811,7004,'snd_mfx_1013_CL','snd_mfx_1013_CL','','',7004,313,0,0,0,0,0,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(7018,4,0,10811,7018,'snd_mfx_1013_CL','snd_mfx_1013_CL','','',7018,313,0,0,0,0,0,0,-1,0,0,0,12,0,0,1647777600);
INSERT INTO race VALUES(9530,7,100,10811,9020,'snd_mfx_3002_CL','snd_mfx_3002_CL','','',9020,208,0,0,0,0,302,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(9630,7,100,10811,9010,'snd_mfx_3002_CL','snd_mfx_3002_CL','','',9010,208,0,0,0,0,303,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(9730,7,100,10811,9001,'snd_mfx_3001_CL','snd_mfx_3001_CL','','',9001,119,0,0,0,0,301,0,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(9826,7,100,10811,9031,'snd_mfx_3005_CL','snd_mfx_3005_CL','','',9030,119,0,0,0,0,304,0,-1,0,0,0,16,0,0,1645704000);
INSERT INTO race VALUES(9867,7,100,10811,9032,'snd_mfx_3005_CL','snd_mfx_3005_CL','','',9030,119,0,0,0,0,304,0,-1,0,0,0,16,0,0,1645704000);
INSERT INTO race VALUES(9908,7,100,10811,9033,'snd_mfx_3005_CL','snd_mfx_3005_CL','','',9030,119,0,0,0,0,304,0,-1,0,0,0,16,0,0,1645704000);
INSERT INTO race VALUES(10165,12,100,10811,1006,'snd_mfx_1002_CL','snd_mfx_1002_CL','snd_mfx_r_1002_CL','snd_mfx_r_1002_CL',1006,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1623326400);
INSERT INTO race VALUES(12000,8,100,10811,5930,'snd_mfx_2003_CL','snd_mfx_2003_CL','','',5930,102,0,0,0,0,1,1,-1,0,0,0,18,0,0,1652184000);
INSERT INTO race VALUES(16305,10,1000,10811,6101,'snd_mfx_3003_CL','snd_mfx_3003_CL','','',6100,121,0,0,0,0,0,0,-1,0,0,3,12,0,0,1783029600);
INSERT INTO race VALUES(1101,1,100,11103,1101,'snd_mfx_1015_CL','snd_mfx_1015_CL','snd_mfx_r_1015_CL','snd_mfx_r_1015_CL',1101,313,0,0,100,0,1,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(1102,1,100,11103,1102,'snd_mfx_1015_CL','snd_mfx_1015_CL','snd_mfx_r_1015_CL','snd_mfx_r_1015_CL',1102,313,0,0,100,0,2,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(1105,1,100,11103,1105,'snd_mfx_1015_CL','snd_mfx_1015_CL','snd_mfx_r_1015_CL','snd_mfx_r_1015_CL',1105,313,0,0,100,0,5,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(1106,1,100,11103,1106,'snd_mfx_1014_CL','snd_mfx_1014_CL','snd_mfx_r_1014_CL','snd_mfx_r_1014_CL',1106,313,0,0,100,0,6,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(4924,61,100,11103,1101,'snd_mfx_1015_CL','snd_mfx_1015_CL','','',1101,313,0,0,100,0,1,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(4925,61,100,11103,1102,'snd_mfx_1015_CL','snd_mfx_1015_CL','','',1102,313,0,0,100,0,2,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(4928,61,100,11103,1105,'snd_mfx_1015_CL','snd_mfx_1015_CL','','',1105,313,0,0,100,0,5,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(4929,61,100,11103,1106,'snd_mfx_1014_CL','snd_mfx_1014_CL','','',1106,313,0,0,100,0,6,0,-1,0,0,0,16,0,1,1498870800);
INSERT INTO race VALUES(5105,6,900,11103,5002,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',5002,315,0,0,100,0,0,0,-1,0,0,1,16,0,1,1636977600);
INSERT INTO race VALUES(5132,6,100,11103,1101,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',1101,315,0,0,100,0,1,0,-1,0,0,1,16,0,1,1636977600);
INSERT INTO race VALUES(5133,6,100,11103,1102,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',1102,315,0,0,100,0,2,0,-1,0,0,1,16,0,1,1636977600);
INSERT INTO race VALUES(5136,6,100,11103,1105,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',1105,315,0,0,100,0,5,0,-1,0,0,1,16,0,1,1636977600);
INSERT INTO race VALUES(5137,6,100,11103,1106,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',1106,315,0,0,100,0,6,0,-1,0,0,1,16,0,1,1636977600);
INSERT INTO race VALUES(5290,12,999,11103,5200,'snd_mfx_2008_CL','snd_mfx_2008_CL','','',5200,121,0,0,100,0,0,0,-1,0,0,0,16,0,1,1627560000);
INSERT INTO race VALUES(10184,12,100,11103,1101,'snd_mfx_1015_CL','snd_mfx_1015_CL','snd_mfx_r_1015_CL','snd_mfx_r_1015_CL',1101,313,0,0,100,0,1,0,-1,0,0,0,16,0,1,1623326400);
INSERT INTO race VALUES(10185,12,100,11103,1102,'snd_mfx_1015_CL','snd_mfx_1015_CL','snd_mfx_r_1015_CL','snd_mfx_r_1015_CL',1102,313,0,0,100,0,2,0,-1,0,0,0,16,0,1,1623326400);
INSERT INTO race VALUES(10188,12,100,11103,1105,'snd_mfx_1015_CL','snd_mfx_1015_CL','snd_mfx_r_1015_CL','snd_mfx_r_1015_CL',1105,313,0,0,100,0,5,0,-1,0,0,0,16,0,1,1623326400);
INSERT INTO race VALUES(10189,12,100,11103,1106,'snd_mfx_1014_CL','snd_mfx_1014_CL','snd_mfx_r_1014_CL','snd_mfx_r_1014_CL',1106,313,0,0,100,0,6,0,-1,0,0,0,16,0,1,1623326400);
INSERT INTO race VALUES(1201,1,100,11203,1023,'snd_mfx_1001_CL','snd_mfx_1001_CL','','',1023,101,0,0,201,101,1,1,-1,0,0,0,18,0,0,1498870800);
INSERT INTO race VALUES(1119,1,100,11504,1105,'snd_mfx_1018_CL','snd_mfx_1018_CL','snd_mfx_r_1018_CL','snd_mfx_r_1018_CL',1119,133,0,0,105,0,2,0,-1,0,0,0,16,0,1,1660910400);
INSERT INTO race VALUES(3076,1,300,11504,3076,'snd_mfx_1019_CL','snd_mfx_1019_CL','snd_mfx_r_1019_CL','snd_mfx_r_1019_CL',3076,234,0,0,105,0,0,0,-1,0,0,0,14,0,1,1660910400);
INSERT INTO race VALUES(5149,6,900,11504,5002,'snd_mfx_2009_CL','snd_mfx_2009_CL','','',5002,234,0,0,105,0,0,0,-1,0,0,1,16,0,1,1660910400);
INSERT INTO race VALUES(5300,12,999,11504,5200,'snd_mfx_2008_CL','snd_mfx_2008_CL','','',5200,121,0,0,105,0,0,0,-1,0,0,0,16,0,1,1660910400);
+3 -5
View File
@@ -46,11 +46,9 @@ type RacecourseSpan struct {
Start float32 `json:"start"`
// End is the distance in meters along the track at which the span ends.
End float32 `json:"end"`
// Straight is whether the span is formally a straight.
Straight bool `json:"straight,omitzero"`
// FarStraight is whether the span is the backstretch.
// If true, then Straight is also true.
FarStraight bool `json:"far_straight,omitzero"`
// Straight is the straight type if the span is formally a straight.
// 1 indicates the front stretch and 2 the backstretch.
Straight int8 `json:"straight,omitzero"`
// Corner is the corner number of the span, or zero if not a corner.
Corner int8 `json:"corner,omitzero"`
}
+102 -3
View File
@@ -1,6 +1,9 @@
package ism
import "git.sunturtle.xyz/zephyr/horse"
import (
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump"
)
type Skill struct {
ID horse.SkillID `json:"skill_id"`
@@ -18,7 +21,7 @@ type SingleModeSupportCard struct {
ID horse.SupportCardID `json:"support_card_id"`
LimitBreak int32 `json:"limit_break_count"`
Exp int32 `json:"exp"`
OwnerID int32 `json:"owner_viewer_id"`
OwnerID int64 `json:"owner_viewer_id"`
}
type GroupOuting struct {
@@ -225,13 +228,107 @@ type AddItem struct {
Number int32 `json:"number"`
}
type PieceData struct {
PieceID int32 `json:"piece_id"`
PieceNum int32 `json:"piece_num"`
}
type UserCard struct {
CardID horse.UmaID `json:"card_id"`
Rarity int8 `json:"rarity"`
TalentLevel int8 `json:"talent_level"`
Skills []Skill `json:"skill_data_array"`
CreateTime string `json:"create_time"`
}
type UserSupportCard struct {
ViewerID int64 `json:"viewer_id"`
SupportCardID horse.SupportCardID `json:"support_card_id"`
Exp int32 `json:"exp"`
LimitBreak int32 `json:"limit_break_count"`
FavoriteFlag int32 `json:"favorite_flag"`
Stock int32 `json:"stock"`
PossessTime string `json:"possess_time"`
}
type RewardAddSupportCardNum struct {
SupportCardID horse.SupportCardID `json:"support_card_id"`
Number int32 `json:"number"`
ItemType int32 `json:"item_type"`
}
type HonorData struct {
HonorID int32 `json:"honor_id"`
Step int32 `json:"step"`
CreateTime string `json:"create_time"`
}
type UserChara struct {
CharaID horse.CharacterID `json:"chara_id"`
TrainingNum int32 `json:"training_num"`
LovePoint int32 `json:"love_point"`
LovePointPool int32 `json:"love_point_pool"`
Fan uint64 `json:"fan"`
}
type UserCloth struct {
ClothID int32 `json:"cloth_id"`
}
type UserMusic struct {
MusicID int32 `json:"music_id"`
AcquisitionTime string `json:"acquisition_time"`
}
type CharaProfileData struct {
CharaID horse.CharacterID `json:"chara_id"`
DataID int32 `json:"data_id"`
NewFlag int32 `json:"new_flag"`
}
type RewardSummary struct {
AddItem []AddItem `json:"add_item_list"`
AddPiece []PieceData `json:"add_piece_list"`
AddCard []UserCard `json:"add_card_list"`
AddCardBonus *RewardSummary `json:"add_card_bonus_info"`
AddSupportCard []UserSupportCard `json:"add_support_card_list"`
AddSupportCardNum []RewardAddSupportCardNum `json:"add_support_card_num_array"`
AddHonor []HonorData `json:"add_honor_list"`
AddChara []UserChara `json:"add_chara_list"`
AddCloth []UserCloth `json:"add_cloth_list"`
AddMusic []UserMusic `json:"add_music_list"`
AddStoryIDs []umadump.Never `json:"add_story_id_array"` // not in my disassembly
AddFCoin int32 `json:"add_fcoin"` // carats
AddPresents int32 `json:"add_present_num"`
AddTotalFan int32 `json:"add_total_fan"`
NewCharaProfiles []CharaProfileData `json:"new_chara_profile_array"`
ForceUpdateHonorID int32 `json:"force_update_honor_id"`
}
type End struct {
Chara SingleModeChara `json:"chara_info"`
RaceConditions []umadump.Never `json:"race_condition_array"`
UncheckedEvents []umadump.Never `json:"unchecked_event_array"`
HomeInfo *umadump.Never `json:"home_info"`
WinSaddleIDs []umadump.Never `json:"win_saddle_id_array"`
EffectedFactors []umadump.Never `json:"effected_factor_array"`
RaceStartInfo *umadump.Never `json:"race_start_info"`
RaceScenario *umadump.Never `json:"race_scenario"`
AddTrophyInfo *umadump.Never `json:"add_trophy_info"`
TrophyRewardInfo *umadump.Never `json:"trophy_reward_info"`
PrevCharaGrade *umadump.Never `json:"prev_chara_grade"`
RaceAddRewardInfo []umadump.Never `json:"race_add_reward_info"`
ReservedRaceInfo *umadump.Never `json:"reserved_race_info"`
Missions []umadump.Never `json:"mission_list"`
StoryEventMissions []umadump.Never `json:"story_event_mission_list"`
StoryEventCharaBonuses []umadump.Never `json:"story_event_chara_bonus_list"`
StartDressInfo []umadump.Never `json:"start_dress_info"`
ResumeFactorSelect any `json:"resume_factor_select"` // don't care yet
RaceRandomPrograms []umadump.Never `json:"race_random_program_array"`
RaceRewardLimitMore []umadump.Never `json:"race_reward_limit_more_list"`
SkillFilterSettings []umadump.Never `json:"skill_filter_setting_array"`
RewardSummary RewardSummary `json:"reward_summary_info"`
IsUmaplan bool `json:"is_umaplan"`
}
type ProgressInfo struct {
@@ -239,11 +336,13 @@ type ProgressInfo struct {
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
DressID int32 `json:"dress_id"`
StoryEventMissions []umadump.Never `json:"story_event_mission_list"`
StoryEventCharaBonuses []umadump.Never `json:"story_event_chara_bonus_list"`
}
type WorkIdleSingleMode struct {
ProgressInfo ProgressInfo `json:"progress_info"`
ProgressLog IdleSingleModeProgressLog `json:"progress_log_info"`
End End `json:"end_info"`
OldChara *SingleModeChara `json:"chara_info"`
Extra map[string]any `json:"extra_data"`
}
+13
View File
@@ -0,0 +1,13 @@
package umadump
type Never struct{}
func (Never) AppendText(b []byte) ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) MarshalText() ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) UnmarshalText(b []byte) error {
panic("umadump: attempt to decode value that should never exist")
}
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type CardRarity -linecomment -trimprefix Card"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CardNone-0]
_ = x[CardRare1-1]
_ = x[CardRare2-2]
_ = x[CardRare3-3]
_ = x[CardRare4-4]
_ = x[CardRare5-5]
}
const _CardRarity_name = "NONERare1Rare2Rare3Rare4Rare5"
var _CardRarity_index = [...]uint8{0, 4, 9, 14, 19, 24, 29}
func (i CardRarity) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CardRarity_index)-1 {
return "CardRarity(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CardRarity_name[_CardRarity_index[idx]:_CardRarity_index[idx+1]]
}
+35
View File
@@ -0,0 +1,35 @@
// Code generated by "stringer -type CharaGradeType -linecomment -trimprefix CharaGrade"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CharaGradeNone-0]
_ = x[CharaGradeDebut-1]
_ = x[CharaGradeNoWin-2]
_ = x[CharaGradeOpen-3]
_ = x[CharaGradeG3Silver-4]
_ = x[CharaGradeG3Gold-5]
_ = x[CharaGradeG2Silver-6]
_ = x[CharaGradeG2Gold-7]
_ = x[CharaGradeG1Bronze-8]
_ = x[CharaGradeG1Silver-9]
_ = x[CharaGradeG1Gold-10]
_ = x[charaGradeMax-11]
}
const _CharaGradeType_name = "NONEDebutNoWinOpenG3SilverG3GoldG2SilverG2GoldG1BronzeG1SilverG1GoldcharaGradeMax"
var _CharaGradeType_index = [...]uint8{0, 4, 9, 14, 18, 26, 32, 40, 46, 54, 62, 68, 81}
func (i CharaGradeType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CharaGradeType_index)-1 {
return "CharaGradeType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CharaGradeType_name[_CharaGradeType_index[idx]:_CharaGradeType_index[idx+1]]
}
@@ -0,0 +1,27 @@
// Code generated by "stringer -type CourseDistanceType -linecomment -trimprefix Distance"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[DistanceSprint-1]
_ = x[DistanceMile-2]
_ = x[DistanceMedium-3]
_ = x[DistanceLong-4]
}
const _CourseDistanceType_name = "ShortMileMiddleLong"
var _CourseDistanceType_index = [...]uint8{0, 5, 9, 15, 19}
func (i CourseDistanceType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_CourseDistanceType_index)-1 {
return "CourseDistanceType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CourseDistanceType_name[_CourseDistanceType_index[idx]:_CourseDistanceType_index[idx+1]]
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type GroundCondition -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[GroundFirm-1]
_ = x[GroundGood-2]
_ = x[GroundSoft-3]
_ = x[GroundHeavy-4]
}
const _GroundCondition_name = "GoodSoftHardBad"
var _GroundCondition_index = [...]uint8{0, 4, 8, 12, 15}
func (i GroundCondition) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_GroundCondition_index)-1 {
return "GroundCondition(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _GroundCondition_name[_GroundCondition_index[idx]:_GroundCondition_index[idx+1]]
}
+28
View File
@@ -0,0 +1,28 @@
// Code generated by "stringer -type InitialLaneType -trimprefix InitialLane"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[InitialLaneExtraSpaceAfter9-1]
_ = x[InitialLaneEquidistant-2]
_ = x[InitialLaneExtraSpaceAfter14-3]
_ = x[InitialLaneExtraSpaceAfter8-4]
_ = x[initialLaneMax-5]
}
const _InitialLaneType_name = "ExtraSpaceAfter9EquidistantExtraSpaceAfter14ExtraSpaceAfter8initialLaneMax"
var _InitialLaneType_index = [...]uint8{0, 16, 27, 44, 60, 74}
func (i InitialLaneType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_InitialLaneType_index)-1 {
return "InitialLaneType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _InitialLaneType_name[_InitialLaneType_index[idx]:_InitialLaneType_index[idx+1]]
}
@@ -0,0 +1,26 @@
// Code generated by "stringer -type MainStoryRaceGimmickType -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[GimmickNone-0]
_ = x[GimmickSpecial00-1]
_ = x[gimmickMax-2]
}
const _MainStoryRaceGimmickType_name = "NONESpecial_00gimmickMax"
var _MainStoryRaceGimmickType_index = [...]uint8{0, 4, 14, 24}
func (i MainStoryRaceGimmickType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_MainStoryRaceGimmickType_index)-1 {
return "MainStoryRaceGimmickType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _MainStoryRaceGimmickType_name[_MainStoryRaceGimmickType_index[idx]:_MainStoryRaceGimmickType_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type Motivation -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MotivationNone-0]
_ = x[MotivationAwful-1]
_ = x[MotivationBad-2]
_ = x[MotivationNormal-3]
_ = x[MotivationGood-4]
_ = x[MotivationGreat-5]
}
const _Motivation_name = "NoneMinLowMiddleHighMax"
var _Motivation_index = [...]uint8{0, 4, 7, 10, 16, 20, 23}
func (i Motivation) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Motivation_index)-1 {
return "Motivation(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Motivation_name[_Motivation_index[idx]:_Motivation_index[idx+1]]
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by "stringer -type ProperGrade -trimprefix Aptitude"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[AptitudeNull-0]
_ = x[AptitudeG-1]
_ = x[AptitudeF-2]
_ = x[AptitudeE-3]
_ = x[AptitudeD-4]
_ = x[AptitudeC-5]
_ = x[AptitudeB-6]
_ = x[AptitudeA-7]
_ = x[AptitudeS-8]
}
const _ProperGrade_name = "NullGFEDCBAS"
var _ProperGrade_index = [...]uint8{0, 4, 5, 6, 7, 8, 9, 10, 11, 12}
func (i ProperGrade) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_ProperGrade_index)-1 {
return "ProperGrade(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ProperGrade_name[_ProperGrade_index[idx]:_ProperGrade_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type RaceDifficulty -trimprefix Difficulty"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[DifficultyEasy-1]
_ = x[DifficultyNormal-2]
_ = x[DifficultyHard-3]
_ = x[DifficultyVeryHard-4]
_ = x[DifficultyExtreme-5]
_ = x[difficultyMax-6]
}
const _RaceDifficulty_name = "EasyNormalHardVeryHardExtremedifficultyMax"
var _RaceDifficulty_index = [...]uint8{0, 4, 10, 14, 22, 29, 42}
func (i RaceDifficulty) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_RaceDifficulty_index)-1 {
return "RaceDifficulty(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RaceDifficulty_name[_RaceDifficulty_index[idx]:_RaceDifficulty_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type RaceTime -trimprefix Time"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TimeMin-0]
_ = x[TimeMorning-1]
_ = x[TimeDaytime-2]
_ = x[TimeEvening-3]
_ = x[TimeNight-4]
_ = x[TimeMax-5]
}
const _RaceTime_name = "MinMorningDaytimeEveningNightMax"
var _RaceTime_index = [...]uint8{0, 3, 10, 17, 24, 29, 32}
func (i RaceTime) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RaceTime_index)-1 {
return "RaceTime(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RaceTime_name[_RaceTime_index[idx]:_RaceTime_index[idx+1]]
}
+48
View File
@@ -0,0 +1,48 @@
// Code generated by "stringer -type RaceType -linecomment -trimprefix RaceType"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[RaceTypeNone-0]
_ = x[RaceTypePvP-1]
_ = x[RaceTypeTutorial-2]
_ = x[RaceTypeStory-3]
_ = x[RaceTypeStoryCondition-4]
_ = x[RaceTypeChampions-5]
_ = x[RaceTypeSingle-6]
_ = x[RaceTypeSingleModeScenarioTeamRace-7]
_ = x[RaceTypeRoomMatch-8]
_ = x[RaceTypePractice-9]
_ = x[RaceTypeDaily-10]
_ = x[RaceTypeTeamBuilding-11]
_ = x[RaceTypeLegend-12]
_ = x[RaceTypeChallengeMatch-13]
_ = x[RaceTypeTeamStadium-14]
_ = x[RaceTypeHeroes-16]
_ = x[raceTypeMaxInclusive-16]
}
const (
_RaceType_name_0 = "NONEPvPTutorialStoryStoryConditionChampionsSingleSingleModeScenarioTeamRaceRoomMatchPracticeDailyTeamBuildingLegendChallengeMatchTeamStadium"
_RaceType_name_1 = "Heroes"
)
var (
_RaceType_index_0 = [...]uint8{0, 4, 7, 15, 20, 34, 43, 49, 75, 84, 92, 97, 109, 115, 129, 140}
)
func (i RaceType) String() string {
switch {
case 0 <= i && i <= 14:
return _RaceType_name_0[_RaceType_index_0[i]:_RaceType_index_0[i+1]]
case i == 16:
return _RaceType_name_1
default:
return "RaceType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type RaceWeather -trimprefix Weather"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[WeatherMin-0]
_ = x[WeatherSunny-1]
_ = x[WeatherCloudy-2]
_ = x[WeatherRainy-3]
_ = x[WeatherSnow-4]
_ = x[WeatherMax-5]
}
const _RaceWeather_name = "MinSunnyCloudyRainySnowMax"
var _RaceWeather_index = [...]uint8{0, 3, 8, 14, 19, 23, 26}
func (i RaceWeather) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RaceWeather_index)-1 {
return "RaceWeather(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RaceWeather_name[_RaceWeather_index[idx]:_RaceWeather_index[idx+1]]
}
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
package replay_test
import (
_ "embed"
"encoding/json"
"strings"
"testing"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
//go:embed Mihono_Bourbon-74.2421s-20260829.json
var testRace string
func TestRaceUnmarshal(t *testing.T) {
// A replay is simply too large to bother converting by hand for a unit test.
// Just test that we can unmarshal and certain important fields are populated.
d := json.NewDecoder(strings.NewReader(testRace))
d.DisallowUnknownFields()
var got replay.Race
if err := d.Decode(&got); err != nil {
t.Error(err)
}
if got.RaceType != replay.RaceTypeChampions {
t.Errorf("wrong race type: want %v, got %v", replay.RaceTypeChampions, got.RaceType)
}
if got.RandomSeed != 1667907263 {
t.Errorf("wrong random seed: want 1667907263, got %d", got.RandomSeed)
}
if got.RaceCourseSet.ID != 10903 {
t.Errorf("wrong race course: want 10903, got %d", got.RaceCourseSet.ID)
}
if got.GroundCondition != replay.GroundFirm {
t.Errorf("wrong ground condition: want %v, got %v", replay.GroundFirm, got.GroundCondition)
}
if got.Weather != replay.WeatherCloudy {
t.Errorf("wrong weather: want %v, got %v", replay.WeatherCloudy, got.Weather)
}
if got.NumRaceHorses != 9 {
t.Errorf("wrong number of race horses: want 9, got %d", got.NumRaceHorses)
}
if len(got.PlayerTeamMemberArray) != 3 {
t.Errorf("wrong number of team members: want 3, got %d", len(got.PlayerTeamMemberArray))
}
for _, m := range got.PlayerTeamMemberArray {
if m.CharaID == 0 {
t.Error("missing team member chara id")
}
if m.FinishTimeRaw == 0 {
t.Error("missing team member finish time raw")
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing team member race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing team member race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing team member response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing team member response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing team member response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing team member response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing team member response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing team member response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing team member active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing team member active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing team member trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing team member trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing team member trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing team member trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing team member trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing team member trained chara acquired skill array")
}
}
if len(got.RaceHorse) != int(got.NumRaceHorses) {
t.Errorf("wrong number of race horses: want %d, got %d", got.NumRaceHorses, len(got.RaceHorse))
}
zeroFinish, zeroDelta := 0, 0
for _, m := range got.RaceHorse {
if m.CharaID == 0 {
t.Error("missing race horse chara id")
}
if m.FinishOrder == 0 {
zeroFinish++
}
if m.FinishTimeRaw == 0 {
t.Error("missing race horse finish time raw")
}
if m.FinishDiffTimeFromPrev == 0 {
zeroDelta++
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing race horse race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing race horse race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing race horse response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing race horse response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing race horse response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing race horse response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing race horse response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing race horse response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing race horse active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing race horse active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing race horse trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing race horse trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing race horse trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing race horse trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing race horse trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing race horse trained chara acquired skill array")
}
}
if zeroFinish != 1 {
t.Errorf("%d horses in index 0 finish", zeroFinish)
}
if zeroDelta != 1 {
t.Errorf("%d horses with zero finish time delta", zeroDelta)
}
if got.SimDataBase64 != encodedSimdata {
t.Errorf("wrong simdata:\nwant %s\n got %s", encodedSimdata, got.SimDataBase64)
}
if len(got.HorseIndexByFinishOrder) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByFinishOrder (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByFinishOrder), len(got.RaceHorse))
}
if len(got.HorseIndexByPopularity) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByPopularity (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByPopularity), len(got.RaceHorse))
}
}
@@ -0,0 +1,27 @@
// Code generated by "stringer -type ResultBoardConditionType -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ResultBoardTurfNone-1]
_ = x[ResultBoardTurfDirt-2]
_ = x[ResultBoardDirtNone-3]
_ = x[ResultBoardDirtTurf-4]
}
const _ResultBoardConditionType_name = "Turf_NoneTurf_DirtDirt_NoneDirt_Turf"
var _ResultBoardConditionType_index = [...]uint8{0, 9, 18, 27, 36}
func (i ResultBoardConditionType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_ResultBoardConditionType_index)-1 {
return "ResultBoardConditionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ResultBoardConditionType_name[_ResultBoardConditionType_index[idx]:_ResultBoardConditionType_index[idx+1]]
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type Rotation -trimprefix Rotation"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[RotationRight-1]
_ = x[RotationLeft-2]
_ = x[RotationStraightRight-3]
_ = x[RotationStraightLeft-4]
}
const _Rotation_name = "RightLeftStraightRightStraightLeft"
var _Rotation_index = [...]uint8{0, 5, 9, 22, 34}
func (i Rotation) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_Rotation_index)-1 {
return "Rotation(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Rotation_name[_Rotation_index[idx]:_Rotation_index[idx+1]]
}
+25
View File
@@ -0,0 +1,25 @@
// Code generated by "stringer -type RunningStyleEx -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[StyleExNone-0]
_ = x[StyleExRunaway-1]
}
const _RunningStyleEx_name = "NONEOonige"
var _RunningStyleEx_index = [...]uint8{0, 4, 10}
func (i RunningStyleEx) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RunningStyleEx_index)-1 {
return "RunningStyleEx(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RunningStyleEx_name[_RunningStyleEx_index[idx]:_RunningStyleEx_index[idx+1]]
}
+30
View File
@@ -0,0 +1,30 @@
// Code generated by "stringer -type Season -trimprefix Season"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[SeasonMin-0]
_ = x[SeasonSpring-1]
_ = x[SeasonSummer-2]
_ = x[SeasonFall-3]
_ = x[SeasonWinter-4]
_ = x[SeasonCherryBlossom-5]
_ = x[SeasonMax-6]
}
const _Season_name = "MinSpringSummerFallWinterCherryBlossomMax"
var _Season_index = [...]uint8{0, 3, 9, 15, 19, 25, 38, 41}
func (i Season) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Season_index)-1 {
return "Season(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Season_name[_Season_index[idx]:_Season_index[idx+1]]
}
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type TurfVisionType -trimprefix TurfVision"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TurfVisionURA-1]
_ = x[TurfVisionNAU-2]
_ = x[TurfVisionStand-3]
_ = x[turfVisionMax-4]
}
const _TurfVisionType_name = "URANAUStandturfVisionMax"
var _TurfVisionType_index = [...]uint8{0, 3, 6, 11, 24}
func (i TurfVisionType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_TurfVisionType_index)-1 {
return "TurfVisionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _TurfVisionType_name[_TurfVisionType_index[idx]:_TurfVisionType_index[idx+1]]
}
+84
View File
@@ -0,0 +1,84 @@
<script lang="ts">
import * as Plot from '@observablehq/plot';
import type { Attachment } from 'svelte/attachments';
import type { ClassValue } from 'svelte/elements';
import * as race from '$lib/race';
import * as racecourse from '$lib/data/racecourse';
export interface DrawSpan {
fy: string;
x1: number;
x2: number;
color: string;
label?: string;
}
interface Props {
course: racecourse.Course;
title?: string;
class?: ClassValue | null;
plotOptions?: Omit<Plot.PlotOptions, 'marks' | 'x' | 'y'>;
draw: DrawSpan[];
}
let { course, title, class: className, plotOptions, draw }: Props = $props();
let width = $state(0);
let height = $state(0);
const phases = $derived(race.phases(course.distance));
const phaseX = $derived([
0,
phases[race.Phase.EarlyRace],
phases[race.Phase.MidRace],
phases[race.Phase.LateRace],
phases[race.Phase.LastSpurt],
]);
const sectionX = $derived([0, ...racecourse.SECTIONS.map((s) => course.distance * s.endRate)]);
const drawTitles = $derived(draw.map((d) => d.fy));
const makeChart: Attachment = (el) => {
$effect(() => {
el?.firstChild?.remove();
el?.append(
Plot.plot({
width: width,
height,
clip: true,
title,
marginLeft: 50,
x: {
domain: [0, course.distance],
ticks: phaseX,
},
fy: {
label: null,
padding: 0,
domain: drawTitles,
},
...plotOptions,
marks: [
Plot.barX(draw, {
x1: 'x1',
x2: 'x2',
fy: 'fy',
fill: 'color',
fillOpacity: 0.8,
title: 'label',
}),
Plot.text(draw, { x: 'x1', fy: 'fy', text: 'label', dx: 2, textAnchor: 'start', fontSize: 16, fill: 'black' }),
Plot.ruleX(sectionX, { opacity: 0.25 }),
Plot.ruleX([phases[0], phases[1]], { opacity: 0.5 }),
],
}),
);
});
};
</script>
<div bind:clientWidth={width} bind:clientHeight={height} class={['flex h-full w-full flex-col md:flex-row', className]}>
<div role="img" {@attach makeChart}>
<span>the chart seems to have didn't</span>
</div>
</div>
+1
View File
@@ -20,6 +20,7 @@
skill_id: 0,
name: 'Skill data is loading...',
description: 'Skill data is still loading, or else something went wrong.',
group_skills: [],
group: 0,
rarity: 1,
group_rate: 1,
+10
View File
@@ -54,6 +54,11 @@ export const PAGES = {
name: 'Stamina Calculator',
description: 'Compute stamina requirements and recovery probability distributions.',
},
{
route: resolve('/race/skillzone'),
name: 'Skill Activation Zones',
description: 'View locations where skills can activate on a given race course.',
},
] satisfies Page[],
career: [
{
@@ -106,6 +111,11 @@ export const PAGES = {
name: 'Tagged Skills Explorer',
description: 'View skills matching the tags internal to the game.',
},
{
route: resolve('/doc/fans'),
name: 'Career Fan Gain Is Solved',
description: 'The exact formula for fan gain from career races – nothing "random" about it.',
},
] satisfies Page[],
} as const;
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -76,36 +76,36 @@ export interface SectionInfo {
section: number;
phase: Phase;
endRate: number;
posKeep: boolean;
rush: boolean;
spotStruggleEnter: boolean;
posKeep?: boolean;
rush?: boolean;
spotStruggleEnter?: boolean;
}
export const SECTIONS: ReadonlyArray<SectionInfo> = [
{ section: 1, phase: Phase.EarlyRace, endRate: 1 / 24, posKeep: true, rush: false, spotStruggleEnter: false },
{ section: 1, phase: Phase.EarlyRace, endRate: 1 / 24, posKeep: true },
{ section: 2, phase: Phase.EarlyRace, endRate: 2 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 3, phase: Phase.EarlyRace, endRate: 3 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 4, phase: Phase.EarlyRace, endRate: 4 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 5, phase: Phase.MidRace, endRate: 5 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 6, phase: Phase.MidRace, endRate: 6 / 24, posKeep: true, rush: true, spotStruggleEnter: false },
{ section: 7, phase: Phase.MidRace, endRate: 7 / 24, posKeep: true, rush: true, spotStruggleEnter: false },
{ section: 8, phase: Phase.MidRace, endRate: 8 / 24, posKeep: true, rush: true, spotStruggleEnter: false },
{ section: 9, phase: Phase.MidRace, endRate: 9 / 24, posKeep: true, rush: true, spotStruggleEnter: false },
{ section: 10, phase: Phase.MidRace, endRate: 10 / 24, posKeep: true, rush: false, spotStruggleEnter: false },
{ section: 11, phase: Phase.MidRace, endRate: 11 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 12, phase: Phase.MidRace, endRate: 12 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 13, phase: Phase.MidRace, endRate: 13 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 14, phase: Phase.MidRace, endRate: 14 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 15, phase: Phase.MidRace, endRate: 15 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 16, phase: Phase.MidRace, endRate: 16 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 17, phase: Phase.LateRace, endRate: 17 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 18, phase: Phase.LateRace, endRate: 18 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 19, phase: Phase.LateRace, endRate: 19 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 20, phase: Phase.LateRace, endRate: 20 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 21, phase: Phase.LateRace, endRate: 21 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 22, phase: Phase.LateRace, endRate: 22 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 23, phase: Phase.LateRace, endRate: 23 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 24, phase: Phase.LateRace, endRate: 24 / 24, posKeep: false, rush: false, spotStruggleEnter: false },
{ section: 6, phase: Phase.MidRace, endRate: 6 / 24, posKeep: true, rush: true },
{ section: 7, phase: Phase.MidRace, endRate: 7 / 24, posKeep: true, rush: true },
{ section: 8, phase: Phase.MidRace, endRate: 8 / 24, posKeep: true, rush: true },
{ section: 9, phase: Phase.MidRace, endRate: 9 / 24, posKeep: true, rush: true },
{ section: 10, phase: Phase.MidRace, endRate: 10 / 24, posKeep: true },
{ section: 11, phase: Phase.MidRace, endRate: 11 / 24 },
{ section: 12, phase: Phase.MidRace, endRate: 12 / 24 },
{ section: 13, phase: Phase.MidRace, endRate: 13 / 24 },
{ section: 14, phase: Phase.MidRace, endRate: 14 / 24 },
{ section: 15, phase: Phase.MidRace, endRate: 15 / 24 },
{ section: 16, phase: Phase.MidRace, endRate: 16 / 24 },
{ section: 17, phase: Phase.LateRace, endRate: 17 / 24 },
{ section: 18, phase: Phase.LateRace, endRate: 18 / 24 },
{ section: 19, phase: Phase.LateRace, endRate: 19 / 24 },
{ section: 20, phase: Phase.LateRace, endRate: 20 / 24 },
{ section: 21, phase: Phase.LastSpurt, endRate: 21 / 24 },
{ section: 22, phase: Phase.LastSpurt, endRate: 22 / 24 },
{ section: 23, phase: Phase.LastSpurt, endRate: 23 / 24 },
{ section: 24, phase: Phase.LastSpurt, endRate: 24 / 24 },
];
export interface SectionSlopeSegment extends SectionInfo {
+145
View File
@@ -0,0 +1,145 @@
import * as z from 'zod';
import * as race from '$lib/race';
export enum InOut {
None = 1,
Inner = 2,
Outer = 3,
OuterToInner = 4,
}
export enum Turn {
Right = 1,
Left = 2,
RightStretch = 3,
LeftStretch = 4,
}
const Span = z.strictObject({
start: z.number(),
end: z.number(),
straight: z.literal([1, 2]).optional(),
corner: z.literal([1, 2, 3, 4]).optional(),
});
export type Span = z.infer<typeof Span>;
const Slope = z.strictObject({
per: z.int32(),
start: z.number(),
end: z.number(),
});
export type Slope = z.infer<typeof Slope>;
export const Course = z.strictObject({
race_course_id: z.int32(),
race_track_id: z.int32(),
race_track_name: z.string(),
distance: z.int32(),
ground: z.enum(race.Surface),
inout: z.enum(InOut),
turn: z.enum(Turn),
lane_max: z.int32(),
threshold1: z.enum(race.Stat).optional(),
threshold2: z.enum(race.Stat).optional(),
lane_start: z.array(z.int32()).length(18),
spans: z.array(Span),
slopes: z.array(Slope),
move_lane_point: z.number(),
});
export type Course = z.infer<typeof Course>;
export async function racecourses(): Promise<Course[]> {
const resp = await fetch('/api/global/racecourse');
return Course.array().parseAsync(await resp.json());
}
export interface SectionInfo {
section: number;
phase: race.Phase;
endRate: number;
posKeep?: boolean;
rush?: boolean;
spotStruggleEnter?: boolean;
}
export const SECTIONS: ReadonlyArray<SectionInfo> = [
{ section: 1, phase: race.Phase.EarlyRace, endRate: 1 / 24, posKeep: true },
{ section: 2, phase: race.Phase.EarlyRace, endRate: 2 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 3, phase: race.Phase.EarlyRace, endRate: 3 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 4, phase: race.Phase.EarlyRace, endRate: 4 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 5, phase: race.Phase.MidRace, endRate: 5 / 24, posKeep: true, rush: true, spotStruggleEnter: true },
{ section: 6, phase: race.Phase.MidRace, endRate: 6 / 24, posKeep: true, rush: true },
{ section: 7, phase: race.Phase.MidRace, endRate: 7 / 24, posKeep: true, rush: true },
{ section: 8, phase: race.Phase.MidRace, endRate: 8 / 24, posKeep: true, rush: true },
{ section: 9, phase: race.Phase.MidRace, endRate: 9 / 24, posKeep: true, rush: true },
{ section: 10, phase: race.Phase.MidRace, endRate: 10 / 24, posKeep: true },
{ section: 11, phase: race.Phase.MidRace, endRate: 11 / 24 },
{ section: 12, phase: race.Phase.MidRace, endRate: 12 / 24 },
{ section: 13, phase: race.Phase.MidRace, endRate: 13 / 24 },
{ section: 14, phase: race.Phase.MidRace, endRate: 14 / 24 },
{ section: 15, phase: race.Phase.MidRace, endRate: 15 / 24 },
{ section: 16, phase: race.Phase.MidRace, endRate: 16 / 24 },
{ section: 17, phase: race.Phase.LateRace, endRate: 17 / 24 },
{ section: 18, phase: race.Phase.LateRace, endRate: 18 / 24 },
{ section: 19, phase: race.Phase.LateRace, endRate: 19 / 24 },
{ section: 20, phase: race.Phase.LateRace, endRate: 20 / 24 },
{ section: 21, phase: race.Phase.LastSpurt, endRate: 21 / 24 },
{ section: 22, phase: race.Phase.LastSpurt, endRate: 22 / 24 },
{ section: 23, phase: race.Phase.LastSpurt, endRate: 23 / 24 },
{ section: 24, phase: race.Phase.LastSpurt, endRate: 24 / 24 },
];
const dirtOnly = [10101, 10103, 10104, 10105];
export function referName(course: Course): string {
const parts = [course.race_track_name, course.distance.toString()];
if (course.ground === race.Surface.Dirt && !dirtOnly.includes(course.race_track_id)) {
parts.push('Dirt');
}
switch (course.inout) {
case InOut.None:
break;
case InOut.Inner:
parts.push('(Inner)');
break;
case InOut.Outer:
parts.push('(Outer)');
break;
case InOut.OuterToInner:
parts.push('(Outer→Inner)');
break;
}
return parts.join(' ');
}
export function fillSlopes(slopes: Slope[], distance: number): Slope[] {
const r: Slope[] = [{ start: 0, end: slopes[0]?.start ?? distance, per: 0 }];
for (const [i, s] of slopes.entries()) {
r.push(s);
r.push({ start: s.end, end: slopes[i + 1]?.start ?? distance, per: 0 });
}
return r.filter((s) => s.start < s.end);
}
export enum DistanceType {
Sprint = 1,
Mile = 2,
Medium = 3,
Long = 4,
}
export function distanceType(course: Course): DistanceType {
if (course.distance < 1500) {
return DistanceType.Sprint;
}
if (course.distance < 1900) {
return DistanceType.Mile;
}
if (course.distance < 2500) {
return DistanceType.Medium;
}
return DistanceType.Long;
}
+42 -38
View File
@@ -8,11 +8,11 @@ import { binomPMF } from './prob';
* Fundamental stats of umas.
*/
export enum Stat {
Speed,
Stamina,
Power,
Guts,
Wit,
Speed = 1,
Stamina = 2,
Power = 3,
Guts = 4,
Wit = 5,
}
/**
@@ -118,14 +118,15 @@ export function distance(raceLen: number): Distance {
/**
* 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
* @returns Ends of each phase in meters, including separate late and spurt phases
*/
export function phases(raceLen: number) {
const sixth = raceLen / 6;
return {
[Phase.EarlyRace]: sixth,
[Phase.MidRace]: 4 * sixth,
[Phase.LateRace]: raceLen,
[Phase.LateRace]: 5 * sixth,
[Phase.LastSpurt]: raceLen,
};
}
@@ -133,18 +134,18 @@ export function phases(raceLen: number) {
* Race surfaces.
*/
export enum Surface {
Turf,
Dirt,
Turf = 1,
Dirt = 2,
}
/**
* Race ground condition types.
*/
export enum GroundConditions {
Firm,
Good,
Soft,
Heavy,
Firm = 1,
Good = 2,
Soft = 3,
Heavy = 4,
}
/**
@@ -327,12 +328,12 @@ export const RUNNING_STYLES = [
/**
* Race phases.
* While last spurt phase is also a phase, it is not distinguished here.
*/
export enum Phase {
EarlyRace,
MidRace,
LateRace,
EarlyRace = 0,
MidRace = 1,
LateRace = 2,
LastSpurt = 3,
}
const hpStrategyCoeff = {
@@ -407,7 +408,7 @@ export function hpPerSecond(
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 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;
}
@@ -447,11 +448,11 @@ export function baseSpeed(raceLen: number): number {
}
const speedStrategyPhaseCoeff = [
[1.0, 0.98, 0.962],
[0.978, 0.991, 0.975],
[0.938, 0.998, 0.994],
[0.931, 1.0, 1.0],
[1.063, 0.962, 0.95],
[1.0, 0.98, 0.962, 0.962],
[0.978, 0.991, 0.975, 0.975],
[0.938, 0.998, 0.994, 0.994],
[0.931, 1.0, 1.0, 1.0],
[1.063, 0.962, 0.95, 0.95],
] as const;
const distanceProficiencyMod: Readonly<Record<AptitudeLevel, number>> = {
@@ -471,7 +472,11 @@ const distanceProficiencyMod: Readonly<Record<AptitudeLevel, number>> = {
* @param style Horse's running style
* @param phase Phase to calculate for
*/
export function baseTargetSpeed(raceLen: number, style: RunningStyle, phase: Exclude<Phase, Phase.LateRace>): number;
export function baseTargetSpeed(
raceLen: number,
style: RunningStyle,
phase: Exclude<Phase, Phase.LateRace | Phase.LastSpurt>,
): number;
/**
* Get a horse's base target speed when not spurting during late race.
* @param raceLen Length of the race in meters
@@ -483,7 +488,7 @@ export function baseTargetSpeed(raceLen: number, style: RunningStyle, phase: Exc
export function baseTargetSpeed(
raceLen: number,
style: RunningStyle,
phase: Phase.LateRace,
phase: Phase.LateRace | Phase.LastSpurt,
speedStat: number,
distanceApt: AptitudeLevel,
): number;
@@ -497,7 +502,7 @@ export function baseTargetSpeed(
const base = baseSpeed(raceLen);
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
const late =
phase === Phase.LateRace ? (math.sqrt(500 * speedStat!) as number) * distanceProficiencyMod[distanceApt!] * 0.002 : 0;
phase >= Phase.LateRace ? (math.sqrt(500 * speedStat!) as number) * distanceProficiencyMod[distanceApt!] * 0.002 : 0;
return baseTarget + late;
}
@@ -512,7 +517,7 @@ export function sectionSpeed(
raceLen: number,
witStat: number,
style: RunningStyle,
phase: Exclude<Phase, Phase.LateRace>,
phase: Exclude<Phase, Phase.LateRace | Phase.LastSpurt>,
): [number, number];
/**
* Calculate the range of section speed values for a horse not spurting during late race.
@@ -529,7 +534,7 @@ export function sectionSpeed(
witStat: number,
style: RunningStyle,
distance: AptitudeLevel,
phase: Phase.LateRace,
phase: Phase.LateRace | Phase.LastSpurt,
): [number, number];
export function sectionSpeed(
raceLen: number,
@@ -537,7 +542,7 @@ export function sectionSpeed(
witStatOrStyle: number | RunningStyle,
styleOrPhase: RunningStyle | Phase,
distance?: AptitudeLevel,
lateRace?: Phase.LateRace,
lateRace?: Phase.LateRace | Phase.LastSpurt,
): [number, number] {
const speedStat = lateRace !== undefined ? speedStatOrWitStat : 0;
const witStat = lateRace !== undefined ? witStatOrStyle : speedStatOrWitStat;
@@ -546,7 +551,7 @@ export function sectionSpeed(
const base = baseSpeed(raceLen);
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
const late =
phase === Phase.LateRace
phase >= Phase.LateRace
? (math.sqrt(500 * speedStat) as number) * distanceProficiencyMod[distance ?? AptitudeLevel.A] * 0.002
: 0;
const u = (witStat / 550000) * math.log10(witStat * 0.1);
@@ -620,11 +625,11 @@ export const COURSE_WIDTH = 11.25;
export const LANE_WIDTH = COURSE_WIDTH / 18;
const accelStrategyPhaseCoeff = {
[RunningStyle.FrontRunner]: [1.0, 1.0, 0.996],
[RunningStyle.PaceChaser]: [0.985, 1.0, 0.996],
[RunningStyle.LateSurger]: [0.975, 1.0, 1.0],
[RunningStyle.EndCloser]: [0.945, 1.0, 0.997],
[RunningStyle.Runaway]: [1.17, 0.94, 0.956],
[RunningStyle.FrontRunner]: [1.0, 1.0, 0.996, 0.996],
[RunningStyle.PaceChaser]: [0.985, 1.0, 0.996, 0.996],
[RunningStyle.LateSurger]: [0.975, 1.0, 1.0, 1.0],
[RunningStyle.EndCloser]: [0.945, 1.0, 0.997, 0.997],
[RunningStyle.Runaway]: [1.17, 0.94, 0.956, 0.956],
} as const;
const surfaceProficiencyMod: Readonly<Record<AptitudeLevel, number>> = {
@@ -681,6 +686,7 @@ const phaseDecel = {
[Phase.EarlyRace]: -1.2,
[Phase.MidRace]: -0.8,
[Phase.LateRace]: -1.0,
[Phase.LastSpurt]: -1.0,
} as const;
/**
@@ -695,9 +701,7 @@ export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): numbe
return -1.2;
}
if (pdm) {
// This isn't until 1.5anni.
// return -0.5;
return phaseDecel[phase];
return -0.5;
}
return phaseDecel[phase];
}
+82
View File
@@ -0,0 +1,82 @@
import { expect, test } from 'vitest';
import * as condition from './condition';
test.for([
{
c: '',
want: [],
},
{
c: 'all_corner_random==1',
want: [[{ type: 'all_corner_random', op: '==', arg: 1 }]],
},
{
c: 'corner!=0',
want: [[{ type: 'corner', op: '!=', arg: 0 }]],
},
{
c: 'order_rate<80',
want: [[{ type: 'order_rate', op: '<', arg: 80 }]],
},
{
c: 'distance_diff_top<=5',
want: [[{ type: 'distance_diff_top', op: '<=', arg: 5 }]],
},
{
c: 'motivation>=4',
want: [[{ type: 'motivation', op: '>=', arg: 4 }]],
},
{
c: 'compete_fight_count>0',
want: [[{ type: 'compete_fight_count', op: '>', arg: 0 }]],
},
{
c: 'phase==1&change_order_onetime<0',
want: [
[
{ type: 'phase', op: '==', arg: 1 },
{ type: 'change_order_onetime', op: '<', arg: 0 },
],
],
},
{
c: 'temptation_count==0&remain_distance<=201&remain_distance>=199&order>=4&order_rate<=70',
want: [
[
{ type: 'temptation_count', op: '==', arg: 0 },
{ type: 'remain_distance', op: '<=', arg: 201 },
{ type: 'remain_distance', op: '>=', arg: 199 },
{ type: 'order', op: '>=', arg: 4 },
{ type: 'order_rate', op: '<=', arg: 70 },
],
],
},
{
c: 'track_id==10001@track_id==10002@track_id==10004@track_id==10010@track_id==10103@track_id==10104',
want: [
[{ type: 'track_id', op: '==', arg: 10001 }],
[{ type: 'track_id', op: '==', arg: 10002 }],
[{ type: 'track_id', op: '==', arg: 10004 }],
[{ type: 'track_id', op: '==', arg: 10010 }],
[{ type: 'track_id', op: '==', arg: 10103 }],
[{ type: 'track_id', op: '==', arg: 10104 }],
],
},
{
c: 'distance_type==1&phase>=2&order_rate>=50&is_overtake==1@distance_type==2&phase>=2&order_rate>=50&is_overtake==1',
want: [
[
{ type: 'distance_type', op: '==', arg: 1 },
{ type: 'phase', op: '>=', arg: 2 },
{ type: 'order_rate', op: '>=', arg: 50 },
{ type: 'is_overtake', op: '==', arg: 1 },
],
[
{ type: 'distance_type', op: '==', arg: 2 },
{ type: 'phase', op: '>=', arg: 2 },
{ type: 'order_rate', op: '>=', arg: 50 },
{ type: 'is_overtake', op: '==', arg: 1 },
],
],
},
])('$c', ({ c, want }) => expect(condition.parse(c)).toEqual(want));
+407
View File
@@ -0,0 +1,407 @@
import * as race from '$lib/race';
import * as racecourse from '$lib/data/racecourse';
export enum Op {
Eq = '==',
Ne = '!=',
Le = '<=',
Lt = '<',
Ge = '>=',
Gt = '>',
}
export interface Condition {
type: string;
op: Op;
arg: number;
}
const re = /(?<type>[a-z0-9_]+)(?<op>[=!<>]=?)(?<arg>-?\d+)/;
function parseCond(conj: string): Condition {
// Using ! assertions because we want to explode if the parse fails.
const r = re.exec(conj)!.groups!;
return { type: r.type, op: r.op as Op, arg: parseInt(r.arg) };
}
export function parse(cond: string): Condition[][] {
if (cond === '') {
return [];
}
return cond.split('@').map((disj) => disj.split('&').map(parseCond));
}
export enum SpanType {
Never = 'N/A',
Random = 'Random',
Skill = 'Triggered',
Whenever = 'Immediate',
}
export interface Span {
start: number;
end: number;
type: SpanType;
}
function coursespans(spans: racecourse.Span[] | racecourse.Slope[], type: SpanType): Span[] {
return or(spans.map((s) => ({ start: s.start, end: s.end, type })));
}
function phase(course: racecourse.Course, op: Op, phase: race.Phase, type: SpanType): Span {
const p = race.phases(course.distance);
switch (op) {
case Op.Eq:
if (phase === race.Phase.EarlyRace) {
return { start: 0, end: p[race.Phase.EarlyRace], type };
}
return { start: p[(phase - 1) as race.Phase], end: p[phase], type };
case Op.Le:
return { start: 0, end: p[phase], type };
case Op.Ge:
if (phase === race.Phase.EarlyRace) {
return { start: 0, end: course.distance, type };
}
return { start: p[(phase - 1) as race.Phase], end: course.distance, type };
default:
// Other operators aren't used.
throw new Error('weird operator on phase condition');
}
}
function endafter(course: racecourse.Course, type: SpanType): Span {
return phase(course, Op.Ge, race.Phase.LateRace, type);
}
function is_finalcorner(course: racecourse.Course, toEnd: boolean, type: SpanType): Span {
const c = course.spans.findLast((s) => s.corner != null);
if (c == null) {
return { start: 0, end: 0, type: SpanType.Never };
}
return { start: c.start, end: toEnd ? course.distance : c.end, type };
}
function laststraight(course: racecourse.Course, type: SpanType): Span {
const s = course.spans[course.spans.length - 1];
return { start: s.start, end: s.end, type };
}
function distrate(course: racecourse.Course, cond: Condition, type: SpanType): Span {
switch (cond.op) {
case Op.Le:
case Op.Lt:
return { start: 0, end: Math.round((course.distance * cond.arg) / 100), type };
case Op.Ge:
case Op.Gt:
return { start: Math.round((course.distance * cond.arg) / 100), end: course.distance, type };
}
return { start: 0, end: 0, type: SpanType.Never };
}
function remaining(course: racecourse.Course, cond: Condition, type: SpanType): Span {
switch (cond.op) {
case Op.Eq:
return { start: course.distance - cond.arg - 1, end: course.distance - cond.arg + 1, type };
case Op.Ne:
throw new Error('weird operator on remaining distance');
case Op.Le:
case Op.Lt:
return { start: course.distance - cond.arg, end: course.distance, type };
case Op.Ge:
case Op.Gt:
return { start: 0, end: course.distance - cond.arg, type };
}
}
function later(s: Span, p: number): Span {
return { ...s, start: (s.start + s.end) * p };
}
function first(s: Span, p: number): Span {
return { ...s, end: (s.start + s.end) * p };
}
function accumulatetime(course: racecourse.Course, t: number, type: SpanType): Span {
return { start: race.baseSpeed(course.distance) * t, end: course.distance, type };
}
function intersection(a: Span, b: Span): Span {
const start = Math.max(a.start, b.start);
const end = Math.min(a.end, b.end);
if (start >= end) {
return { start: 0, end: 0, type: SpanType.Never };
}
switch (a.type) {
case SpanType.Never:
return { start: 0, end: 0, type: SpanType.Never };
case SpanType.Random:
case SpanType.Skill:
if (b.type === SpanType.Never) {
return { start: 0, end: 0, type: SpanType.Never };
}
// We use a.type on the assumption that we will never intersect
// a random span with a skill-triggered span.
return { start, end, type: a.type };
case SpanType.Whenever:
return { start, end, type: b.type };
}
}
function opcompare(cond: Condition, x: number) {
switch (cond.op) {
case Op.Eq:
return x === cond.arg;
case Op.Ne:
return x !== cond.arg;
case Op.Le:
return x <= cond.arg;
case Op.Lt:
return x < cond.arg;
case Op.Ge:
return x >= cond.arg;
case Op.Gt:
return x > cond.arg;
}
}
function must(x: boolean): SpanType {
return x ? SpanType.Whenever : SpanType.Never;
}
const perCond: Readonly<Partial<Record<Condition['type'], (course: racecourse.Course, cond: Condition) => Span[]>>> = {
accumulatetime: (course, cond) => [accumulatetime(course, cond.arg, SpanType.Whenever)],
activate_count_all_team: (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
activate_count_end_after: (course) => [endafter(course, SpanType.Skill)],
activate_count_heal: (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
activate_count_later_half: (course) => [{ start: course.distance / 2, end: course.distance, type: SpanType.Skill }],
activate_count_middle: (course) => [phase(course, Op.Eq, race.Phase.MidRace, SpanType.Skill)],
activate_count_start: (course) => [phase(course, Op.Eq, race.Phase.EarlyRace, SpanType.Skill)],
all_corner_random: (course) =>
coursespans(
course.spans.filter((s) => s.corner != null),
SpanType.Random,
),
change_order_up_end_after: (course) => [endafter(course, SpanType.Whenever)],
change_order_up_finalcorner_after: (course) => [is_finalcorner(course, true, SpanType.Whenever)],
change_order_up_middle: (course) => [phase(course, Op.Eq, race.Phase.MidRace, SpanType.Whenever)],
compete_fight_count: (course) => [laststraight(course, SpanType.Whenever)],
corner: (course, cond) =>
coursespans(
course.spans.filter((s) => opcompare(cond, s.corner ?? 0)),
SpanType.Whenever,
),
corner_count: (course, cond) => [
{
start: 0,
end: course.distance,
type: opcompare(
cond,
course.spans.reduce((n, s) => n + (s.corner != null ? 1 : 0), 0),
)
? SpanType.Never
: SpanType.Whenever,
},
],
corner_random: (course, cond) => {
const c = course.spans.findLast((s) => s.corner === cond.arg);
if (c == null) {
return [{ start: 0, end: 0, type: SpanType.Never }];
}
return [{ start: c.start, end: c.end, type: SpanType.Random }];
},
course_distance: (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.distance)) }],
distance_rate: (course, cond) => [distrate(course, cond, SpanType.Whenever)],
distance_rate_after_random: (course, cond) => [distrate(course, cond, SpanType.Random)],
distance_type: (course, cond) => [
{ start: 0, end: course.distance, type: must(opcompare(cond, racecourse.distanceType(course))) },
],
down_slope_random: (course) =>
coursespans(
course.slopes.filter((s) => s.per < 0),
SpanType.Random,
),
furlong: (course, cond) => {
switch (cond.op) {
case Op.Eq:
return [{ start: cond.arg * 201.168, end: (cond.arg + 1) * 201.168, type: SpanType.Whenever }];
case Op.Ne:
return [
{ start: 0, end: cond.arg * 201.168, type: SpanType.Whenever },
{ start: (cond.arg + 1) * 201.168, end: course.distance, type: SpanType.Whenever },
];
case Op.Le:
case Op.Lt:
return [{ start: 0, end: (cond.arg + 1) * 201.168, type: SpanType.Whenever }];
case Op.Ge:
case Op.Gt:
return [{ start: cond.arg * 201.168, end: course.distance, type: SpanType.Whenever }];
}
},
ground_type: (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.ground)) }],
// hp_per would be an interesting challenge...
is_finalcorner: (course, cond) => {
// Either is_finalcorner==1 or is_finalcorner==0.
if (cond.arg === 1) {
return [is_finalcorner(course, true, SpanType.Whenever)];
}
const c = course.spans.findLast((s) => s.corner != null);
if (c == null) {
return [{ start: 0, end: course.distance, type: SpanType.Whenever }];
}
return [{ start: 0, end: c.start, type: SpanType.Whenever }];
},
// TODO(zeph): is_abroad
is_activate_any_skill: (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
is_activate_heal_skill: (course) => [{ start: 0, end: course.distance, type: SpanType.Skill }],
is_basis_distance: (course, cond) => [
{ start: 0, end: course.distance, type: must(opcompare(cond, course.distance % 400 === 0 ? 1 : 0)) },
],
is_dirtgrade: (course, cond) => {
// NAR tracks: Ooi, Kawasaki, Funabashi, Morioka
const is = [10101, 10103, 10104, 10105].includes(course.race_track_id);
return [{ start: 0, end: course.distance, type: must(opcompare(cond, is ? 1 : 0)) }];
},
is_finalcorner_laterhalf: (course) => [later(is_finalcorner(course, false, SpanType.Whenever), 0.5)],
is_finalcorner_random: (course) => [is_finalcorner(course, false, SpanType.Random)],
is_last_straight: (course) => [laststraight(course, SpanType.Whenever)],
is_last_straight_onetime: (course) => [first(laststraight(course, SpanType.Whenever), 0.02)],
is_lastspurt: (course) => [endafter(course, SpanType.Whenever)],
// could implement is_temptation!=0, but that condition isn't used
is_tight_track: (course, cond) => {
// Sapporo, Hakodate, Fukushima, Kokura, Kawasaki, Funabashi
// TODO(zeph): GameTora says "afaik" for this list...
const is = [10001, 10002, 10004, 10010, 10103, 10104];
return [{ start: 0, end: course.distance, type: must(opcompare(cond, is ? 1 : 0)) }];
},
last_straight_random: (course) => [laststraight(course, SpanType.Random)],
lastspurt: (course) => [endafter(course, SpanType.Whenever)],
order_rate_in20_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_in40_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_in50_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_in80_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_out20_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_out40_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_out50_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
order_rate_out70_continue: (course) => [accumulatetime(course, 5, SpanType.Whenever)],
phase: (course, cond) => [phase(course, cond.op, cond.arg, SpanType.Whenever)],
phase_corner_random: (course, cond) => {
const p = phase(course, Op.Eq, cond.arg, SpanType.Random);
return coursespans(
course.spans.filter((s) => s.corner != null),
SpanType.Random,
).map((s) => intersection(s, p));
},
phase_first_half_straight_random: (course, cond) => {
const p = first(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5);
return coursespans(
course.spans.filter((s) => s.straight != null),
SpanType.Random,
).map((s) => intersection(s, p));
},
phase_firsthalf: (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Whenever), 0.5)],
phase_firsthalf_random: (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5)],
phase_firstquarter: (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Whenever), 0.25)],
phase_firstquarter_random: (course, cond) => [first(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.25)],
phase_laterhalf: (course, cond) => [later(phase(course, Op.Eq, cond.arg, SpanType.Whenever), 0.5)],
phase_laterhalf_random: (course, cond) => [later(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5)],
phase_latter_half_straight_random: (course, cond) => {
const p = later(phase(course, Op.Eq, cond.arg, SpanType.Random), 0.5);
return coursespans(
course.spans.filter((s) => s.straight != null),
SpanType.Random,
).map((s) => intersection(s, p));
},
phase_random: (course, cond) => [phase(course, cond.op, cond.arg, SpanType.Random)],
phase_straight_random: (course, cond) => {
const p = phase(course, Op.Eq, cond.arg, SpanType.Random);
return coursespans(
course.spans.filter((s) => s.straight != null),
SpanType.Random,
).map((s) => intersection(s, p));
},
remain_distance: (course, cond) => [remaining(course, cond, SpanType.Whenever)],
remain_distance_viewer_id: (course, cond) => [remaining(course, cond, SpanType.Whenever)],
rotation: (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.turn)) }],
run_at_full_speed_random: (course) => [endafter(course, SpanType.Random)],
slope: (course, cond) => {
switch (cond.arg) {
case 0:
return coursespans(
racecourse.fillSlopes(course.slopes, course.distance).filter((s) => s.per === 0),
SpanType.Whenever,
);
case 1:
return coursespans(
course.slopes.filter((s) => s.per > 0),
SpanType.Whenever,
);
case 2:
return coursespans(
course.slopes.filter((s) => s.per < 0),
SpanType.Whenever,
);
default:
throw new Error('weird argument to slope condition');
}
},
straight_front_type: (course, cond) =>
coursespans(
course.spans.filter((s) => s.straight === cond.arg),
SpanType.Whenever,
),
straight_random: (course) =>
coursespans(
course.spans.filter((s) => s.straight != null),
SpanType.Random,
),
track_id: (course, cond) => [{ start: 0, end: course.distance, type: must(opcompare(cond, course.race_track_id)) }],
up_slope_random: (course) =>
coursespans(
course.slopes.filter((s) => s.per > 0),
SpanType.Random,
),
up_slope_random_later_half: (course) => {
const p = { start: course.distance / 2, end: course.distance, type: SpanType.Random };
return coursespans(
course.slopes.filter((s) => s.per > 0),
SpanType.Random,
).map((s) => intersection(s, p));
},
} as const;
export function spans(course: racecourse.Course, cond: Condition): Span[] {
const f = perCond[cond.type];
return f?.(course, cond) ?? [{ start: 0, end: course.distance, type: SpanType.Whenever }];
}
export function and(as: Span[], bs: Span[]): Span[] {
const a = as.toSorted((l, r) => l.start - r.start);
const b = bs.toSorted((l, r) => l.start - r.start);
// TODO(zeph): this is quadratic in the number of spans; we could do it linear
return a.flatMap((u) => b.map((v) => intersection(u, v)).filter((v) => v.type !== SpanType.Never));
}
export function or(spans: Span[]): Span[] {
const a = spans.filter((s) => s.type !== SpanType.Never).sort((l, r) => l.start - r.start);
if (spans.length === 0) {
return [];
}
const r: Span[] = [];
let cur = a.shift()!;
while (a.length > 0) {
const next = a.shift()!;
// If they're disjoint or different types, they can't be merged.
// Note that we only need to check next start versus current end
// because we sort by start position first.
if (next.start > cur.end || next.type !== cur.type) {
r.push(cur);
cur = next;
continue;
}
// The current start is always the minimum of the two because we sorted,
// and the merged type is the same as the current type.
cur.end = Math.max(cur.end, next.end);
}
r.push(cur);
return r;
}
+5 -1
View File
@@ -88,7 +88,11 @@
</div>
<div class="col-span-full">
<label for="talkgallery" class="block">
Select <span class="inline-block font-mono">talk_gallery_data.json</span> to hide read conversations.
Select
<a href="https://werseter.github.io/umadump/" target="_blank" rel="noopener noreferrer">
<span class="inline-block font-mono">talk_gallery_data.json</span>
</a>
to hide read conversations.
</label>
<input
type="file"
+238
View File
@@ -0,0 +1,238 @@
<script lang="ts">
import { resolve } from '$app/paths';
import CalcInfo from '../../race/CalcInfo.svelte';
import Article from '../Article.svelte';
import Sec from '../Sec.svelte';
import * as fans from './fans';
let winFans = $state(700);
let fanBonus = $state(0);
let finishOrder = $state(1);
let margin = $state(0);
let skills = $state(0);
let glowsticks = $state(false);
const baseFans = $derived(fans.baseFans(winFans, finishOrder));
const marginFans = $derived(fans.marginFans(baseFans, finishOrder, margin));
const skillFans = $derived(fans.skillFans(baseFans, skills));
const glowstickBonus = $derived(glowsticks ? 50 : 0);
const gainedFans = $derived(fans.gained(baseFans, marginFans, skillFans, fanBonus, glowstickBonus));
const marginType = $derived(finishOrder === 1 ? 'Win Margin' : 'Margin vs. Winner');
const orderPct = [
[1, 100],
[2, 40],
[3, 25],
[4, 15],
[5, 10],
[6, 9],
[7, 8],
[8, 7],
[9, 6],
[10, 5],
[11, 4],
[12, 3],
[13, 2.5],
[14, 2],
[15, 1.5],
[16, 1],
[17, 1],
[18, 1],
] as const;
const marginBonus = [
['Nose – 3/4 L', 0, 9],
['1 L – 2.5 L', 2, 6],
['3 L – 4 L', 4, 4],
['5 L – 6 L', 6, 2],
['7L – distance', 9, 0],
] as const;
const skillBonus = [
['0', 0],
['1-3', 2],
['4-6', 3],
['7-9', 5],
['10+', 8],
] as const;
</script>
<Article>
{#snippet head()}
<Sec h={1} id="top" class="text-center pointer-coarse:mb-8">Career Fan Gain Is Solved</Sec>
<p>
Thanks to work by @Moxayoru and @Werseter on the Hakuraku Discord, a precise formula for fans gained from career races has
been found.
</p>
<p>
There is no random component to fan gain, except insofar as the race itself is random. It is now known to be entirely
determined by five factors:
</p>
<ul class="mb-4 list-disc pl-4">
<li>Base fans, according to the race and finish placement.</li>
<li>Fan bonus from support cards.</li>
<li>Margin of the win, or margin behind the winner if the player's trainee did not win.</li>
<li>Number of skills the trainee activated during the race.</li>
<li>In Trackblazer/MANT, use of glowsticks.</li>
</ul>
<p>
The identified formula perfectly fits hundreds of recorded career races, including losses. These results have important
implications both for fan runs and for low fan bonus decks for which debut can fall short of fan requirements for early
career goals.
</p>
{/snippet}
<Sec h={2} id="calc-fans">Calculate Fans</Sec>
<p>
This tool will calculate your exact fan gain from a race. Use the fan value displayed in the race selection menu for the Race
Fans field.
</p>
<form class="mx-auto mb-4 grid grid-cols-1 gap-2 rounded-md p-4 text-center shadow-md ring md:max-w-2xl md:grid-cols-3">
<div>
<label for="calc-base-fans">Race Fans</label>
<input id="calc-base-fans" class="max-w-32" type="number" min="0" max="30000" step="100" bind:value={winFans} />
</div>
<div>
<label for="calc-fan-bonus">Fan Bonus</label>
<input id="calc-fan-bonus" class="max-w-32" type="number" min="0" max="150" step="5" bind:value={fanBonus} />
</div>
<div>
<label for="calc-finish-order">Finish Order</label>
<select id="calc-finish-order" class="max-w-32" bind:value={finishOrder}>
{#each orderPct as [v] (v)}
<option value={v}>{v}</option>
{/each}
</select>
</div>
<div>
<label for="calc-margin">{marginType} (L)</label>
<input id="calc-margin" class="max-w-32" type="number" min="0" step="0.25" bind:value={margin} />
</div>
<div>
<label for="calc-skills">Activated Skills</label>
<input id="calc-skills" class="max-w-32" type="number" min="0" step="1" bind:value={skills} />
</div>
<div class="my-auto">
<input id="calc-glowsticks" class="min-h-6 min-w-6 align-middle" type="checkbox" bind:checked={glowsticks} />
<label for="calc-glowsticks">Glowsticks</label>
</div>
</form>
<div class="mx-auto grid max-w-2xl grid-cols-1 md:grid-cols-3">
<CalcInfo title="Base Fans">{baseFans}</CalcInfo>
<CalcInfo title="Fans from Margin">{marginFans}</CalcInfo>
<CalcInfo title="Fans from Skills">{skillFans}</CalcInfo>
<CalcInfo title="Fans Gained" class="md:col-start-2">{gainedFans}</CalcInfo>
</div>
<Sec h={2} id="base-fans">Base Fans</Sec>
<p>
Base fans is a parameter per race and finish placement, defined in <a href={resolve('/doc/mdb')}>master data</a>. The number
shown in the career race selection menu is the base fans for first place. While it is defined separately for each race and
finish order, every race currently follows the same curve for all placements, up to rounding.
</p>
<table class="mx-auto mb-4 border text-center">
<thead>
<tr>
<th scope="col" class="px-2">Placement</th>
<th scope="col" class="px-2">Fan Ratio</th>
</tr>
</thead>
<tbody>
{#each orderPct as [o, p] (o)}
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
<td>{o}</td>
<td>{p}%</td>
</tr>
{/each}
</tbody>
</table>
<Sec h={2} id="margin">Fans from Margin</Sec>
<p>When the trainee places in the top five, she receives a bonus percentage of base fans according to the finish margin.</p>
<p>
The bonus depends on whether the trainee won the race. If so, it is calculated by her margin versus second place. Otherwise,
her total margin behind the winner is used. If she did not place in the top five, then she gains no fans from margin.
</p>
<table class="mx-auto mb-4 border text-center">
<thead>
<tr>
<th scope="col" class="px-2">Actual<br />Margin Within</th>
<th scope="col" class="px-2">Bonus for Winner</th>
<th scope="col" class="px-2">Bonus for Podium</th>
</tr>
</thead>
<tbody>
{#each marginBonus as [m, w, l] (m)}
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
<td>{m}</td>
<td>{w}%</td>
<td>{l}%</td>
</tr>
{/each}
</tbody>
</table>
<Sec h={2} id="skills">Fans from Skill Activations</Sec>
<p>The trainee receives a bonus percentage of base fans according to the number of skills she activated during the race.</p>
<table class="mx-auto mb-4 border text-center">
<thead>
<tr>
<th scope="col" class="px-2">Activated Skills</th>
<th scope="col" class="px-2">Bonus</th>
</tr>
</thead>
<tbody>
{#each skillBonus as [n, b] (n)}
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
<td>{n}</td>
<td>{b}%</td>
</tr>
{/each}
</tbody>
</table>
<Sec h={2} id="rounding">Calculation Order and Rounding</Sec>
<p>The calculation is perfomed as if in the following order.</p>
<ol class="mb-4 list-decimal pl-4">
<li>Look up base fans for the race and placement. This value is always an integer.</li>
<li>Calculate fans from win margin as a proportion of base fans and round down.</li>
<li>Calculate fans from skill activation count as a proportion of base fans and round down.</li>
<li>Calculate the sum of base fans, win margin fans, and skill activation fans.</li>
<li>Apply fan bonus to the sum in single-precision floating point, without otherwise rounding.</li>
<li>
Round to five significant figures with halfway cases rounded up, then round down. Examples are useful here.
<ul class="list-[revert] pl-8">
<li>
700 base plus 14 from margin with 88% fan bonus = <span class="inline-block">714&times;1.88</span> = 1342.32, rounded to five
significant figures becomes 1342.3, rounded down to 1342.
</li>
<li>
10500 base plus 210 from margin with 105% fan bonus =
<span class="inline-block">10710&times;2.05</span>
= 21955.5, rounded to five significant figures becomes 21956, rounded down to 21956.
</li>
<li>
100 base fans plus 2 fans from skills with 88% fan bonus = <span class="inline-block">102&times;1.88</span> = 191.76, rounded
to five significant figures becomes 191.76, rounded down to 191.
</li>
</ul>
</li>
<li>(Trackblazer/MANT) If glowsticks were used for the race, apply their multiplier and round down.</li>
</ol>
<Sec h={2} id="data">Data</Sec>
<p>
The data for this research comes from 325 races and fan results dumped using <a
href="https://werseter.github.io/umadump/"
target="_blank"
rel="noopener noreferrer">Umadump</a
>. The races come from careers run between 19 Aug and 18 Oct 2026. These careers include fan bonus values of 50, 55, 65, 70,
80, 88, and 105. The identified formula predicts the actual fan gain perfectly for all of the races. You can
<a
href="https://docs.google.com/spreadsheets/d/1_jtOv6qk9dP4jJTagJqG53Pvj48IVd6gl2zKpyT6Kvs/edit?gid=463816108#gid=463816108"
target="_blank"
rel="noopener noreferrer">view the data</a
>.
</p>
</Article>
+73
View File
@@ -0,0 +1,73 @@
const placementPms = [1000, 400, 250, 150, 100, 90, 80, 70, 60, 50, 40, 30, 25, 20, 15, 10, 10, 10] as const;
/**
* Calculate base fans for a given placement in a race.
* @param winBase Base fans for a win, as displayed in the race menu
* @param finishOrder Placement of the trainee, with winner being 1
* @returns Base fans for the race and placement together
*/
export function baseFans(winBase: number, finishOrder: number): number {
return Math.round((winBase * placementPms[finishOrder - 1]) / 1000);
}
const margins = [
[0.75, 0.0, 0.09],
[2.5, 0.02, 0.06],
[4, 0.04, 0.04],
[6, 0.06, 0.02],
[Infinity, 0.09, 0.0],
] as const;
/**
* Calculate fans gained from a career race due to the margin of the finish.
* @param baseFans Base fans from the race and finish order
* @param finishOrder Placement of the trainee, with winner being 1
* @param margin Margin in lengths between the trainee and second if a win, or the winner and trainee if a loss
* @returns Fans gained due to finish margin
*/
export function marginFans(baseFans: number, finishOrder: number, margin: number): number {
if (finishOrder > 5) {
return 0;
}
const [, wb, lb] = margins.find(([m]) => margin <= m)!;
const b = finishOrder === 1 ? wb : lb;
return Math.floor(baseFans * b);
}
const skills = [
[0, 0.0],
[3, 0.02],
[6, 0.03],
[9, 0.05],
[Infinity, 0.08],
] as const;
/**
* Calculate the fans gained from a career race due to skills activated.
* @param baseFans Base fans from the race and finish order
* @param skillsActivated Number of skills the trainee activated during the race
* @returns Fans gained due to skill activations
*/
export function skillFans(baseFans: number, skillsActivated: number): number {
const [, b] = skills.find(([n]) => skillsActivated <= n)!;
return Math.floor(baseFans * b);
}
/**
* Calculate fan gain from a career race.
* @param baseFans Base fans from the race and finish order
* @param marginFans Fans gained due to finish margin
* @param skillFans Fans gained due to skill activations
* @param fanBonus Total fan bonus from support cards
* @param itemBonus Fan bonus from items (MANT glowsticks)
* @returns Total fans gained
*/
export function gained(baseFans: number, marginFans: number, skillFans: number, fanBonus: number, itemBonus?: number): number {
const f = baseFans + marginFans + skillFans;
const s = Math.fround(f * Math.fround(1 + fanBonus / 100));
const p = Math.exp((4 - Math.floor(Math.log10(s))) * Math.LN10);
// Add half a significant figure to account for precision.
const d = Math.floor(Math.round(s * p + 5e-6) / p + 5e-6);
const b = itemBonus ?? 0;
return Math.floor(d * (1 + b / 100));
}
+39 -35
View File
@@ -7,10 +7,6 @@
import { resolve } from '$app/paths';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p;
}
let style = $state(race.RunningStyle.FrontRunner);
let surfaceApt = $state(race.AptitudeLevel.A);
let distanceApt = $state(race.AptitudeLevel.A);
@@ -84,38 +80,38 @@
];
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 secSpeed: Array<ComputedAreas | null> = $derived([
const secSpeedEarly: Array<ComputedAreas | null> = $derived([
{
label: 'Early Race',
label: styleIsFront ? 'Front S' : 'Style S',
y1: (x) => race.sectionSpeed(raceLen, x * 1.1, style, race.Phase.EarlyRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x * 1.1, style, race.Phase.EarlyRace)[1],
},
{
label: styleIsFront ? 'Front A' : 'Style A',
y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1],
},
{
label: 'Mid Race',
label: styleIsFront ? 'Front A Speed-Up Mode' : 'Style A Pace-Up Mode',
y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0] * 1.04,
y2: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1] * 1.04,
},
]);
const secSpeedMid: Array<ComputedAreas | null> = $derived([
{
label: styleIsFront ? 'Front S' : 'Style S',
y1: (x) => race.sectionSpeed(raceLen, x * 1.1, style, race.Phase.MidRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x * 1.1, style, race.Phase.MidRace)[1],
},
{
label: styleIsFront ? 'Front A' : 'Style A',
y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1],
},
styleIsFront
? {
label: 'Early Race + Mean Speed-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0], 1.04, race.frontModeEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1], 1.04, race.frontModeEnterChance(x)),
}
: {
label: 'Early Race + Mean Pace-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1], 1.04, race.paceUpEnterChance(x)),
},
styleIsFront
? {
label: 'Mid Race + Mean Speed-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0], 1.04, race.frontModeEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1], 1.04, race.frontModeEnterChance(x)),
}
: {
label: 'Mid Race + Mean Pace-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1], 1.04, race.paceUpEnterChance(x)),
{
label: styleIsFront ? 'Front A Speed-Up Mode' : 'Style A Pace-Up Mode',
y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0] * 1.04,
y2: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1] * 1.04,
},
]);
const downhill: ComputedSeries[] = [
@@ -396,16 +392,25 @@
>Section Speed</Sec
>
<p>Random variance in target speed per race section.</p>
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
<div class="mb-10 grid h-96 w-full md:grid-cols-2">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
stat={race.Stat.Wit}
yArea={secSpeed}
yLabel="Section Speed (m/s)"
range={[17, 23]}
yArea={secSpeedEarly}
yLabel="Early Race Section Speed (m/s)"
range={[17, 23.5]}
plotOptions={{ color: { legend: true } }}
class="h-96 max-h-96"
/>
<div class="mx-auto flex w-full md:max-w-2xl">
<StatChart
stat={race.Stat.Wit}
yArea={secSpeedMid}
yLabel="Mid Race Section Speed (m/s)"
range={[17, 23.5]}
plotOptions={{ color: { legend: true } }}
class="h-96 max-h-96"
/>
</div>
<div class="mx-auto mb-4 flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-1 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"
@@ -424,7 +429,6 @@
{/each}
</select>
</div>
</div>
<Sec
h={3}
+2 -2
View File
@@ -99,8 +99,8 @@
const distance = $derived([last(seggs[0]).end.x, last(seggs[1]).end.x] as const);
const time = $derived([last(seggs[0]).end.t, last(seggs[1]).end.t] as const);
const finishTime = $derived([
time[0] + (phases[race.Phase.LateRace] - distance[0]) / spurtSpeed,
time[1] + (phases[race.Phase.LateRace] - distance[1]) / spurtSpeed,
time[0] + (raceLen - distance[0]) / spurtSpeed,
time[1] + (raceLen - distance[1]) / spurtSpeed,
] as const);
function gainLengths(fast: accel.Instant, slow: accel.Instant): number {
const m = fast.v * (slow.t - fast.t) + fast.x - slow.x;
@@ -0,0 +1,250 @@
<script lang="ts">
import CourseChart from '$lib/CourseChart.svelte';
import * as racecourse from '$lib/data/racecourse';
import * as skill from '$lib/data/skill';
import * as condition from '$lib/racelib/condition';
import Pick from '$lib/Pick.svelte';
import SkillIcon from '$lib/SkillIcon.svelte';
import { onMount } from 'svelte';
import TopChart from './TopChart.svelte';
let courses: racecourse.Course[] = $state([]);
let skills: skill.Skill[] = $state([]);
onMount(async () => {
const [c, sk] = await Promise.all([racecourse.racecourses(), skill.skills()]);
courses = c;
skills = sk;
});
let course: racecourse.Course | undefined = $state();
let selSkills: skill.Skill[] = $state([]);
const spanColors: Readonly<Record<condition.SpanType, string>> = {
[condition.SpanType.Never]: 'red',
[condition.SpanType.Random]: 'yellow',
[condition.SpanType.Skill]: 'cyan',
[condition.SpanType.Whenever]: 'lime',
} as const;
const drawSkills = $derived(
selSkills.map((skill) => {
if (course == null) {
return { skill, draw: [] };
}
const cs =
skill.activations.length === 1
? [
{ fy: 'Pre', parse: condition.parse(skill.activations[0].precondition ?? '') },
{ fy: 'Trigger', parse: condition.parse(skill.activations[0].condition) },
]
: [
{ fy: 'Pre1', parse: condition.parse(skill.activations[0].precondition ?? '') },
{ fy: 'Trigger1', parse: condition.parse(skill.activations[0].condition) },
{ fy: 'Pre2', parse: condition.parse(skill.activations[1].precondition ?? '') },
{ fy: 'Trigger2', parse: condition.parse(skill.activations[1].condition) },
];
return {
skill,
draw: cs
.filter((c) => c.parse.length > 0)
.flatMap(({ fy, parse }) => {
const p = parse
.map((conj) => conj.map((c) => condition.spans(course!, c)).reduce(condition.and))
.flatMap((spans) =>
spans
.filter((sp) => sp != null)
.map((sp) => ({ fy, x1: sp.start, x2: sp.end, color: spanColors[sp.type], label: sp.type })),
);
if (p.length === 0) {
return [{ fy, x1: 0, x2: course?.distance ?? 0, label: 'N/A', color: spanColors[condition.SpanType.Never] }];
}
return p;
}),
};
}),
);
function courseKey(c: racecourse.Course) {
return { id: c.race_course_id, name: racecourse.referName(c) };
}
function skillKey(sk: skill.Skill) {
const name = sk.unique_owner != null && sk.rarity === 1 ? `${sk.name} (Inherited)` : sk.name;
return { id: sk.skill_id, name };
}
let skillToAdd: skill.Skill | undefined = $state();
function addSkill() {
if (skillToAdd == null) {
return;
}
selSkills.push(skillToAdd);
}
function delSkill(i: number) {
if (i < 0 || i >= selSkills.length) {
return;
}
selSkills.splice(i, 1);
}
function swapSkill(i: number, dir: number) {
const j = i + dir;
if (j < 0 || j >= selSkills.length) {
return;
}
const [a, b] = [selSkills[i], selSkills[j]];
[selSkills[i], selSkills[j]] = [b, a];
}
</script>
{#snippet showcond(cond: string)}
{@const disj = cond.replaceAll('&', ' & ').split('@')}
{#each disj as conj, i (conj)}
<span class="block font-mono text-sm">{conj}</span>
{#if i !== disj.length - 1}
<span class="block text-xs italic">or</span>
{/if}
{/each}
{/snippet}
<h1 class="text-4xl">Skill Activation Zones</h1>
<form class="mx-auto mt-8 rounded-md p-4 text-center shadow-md ring md:max-w-2xl">
<Pick id="course" items={courses} key={courseKey} class="w-full" bind:value={course} required />
</form>
<svelte:boundary>
<div class="mx-auto mt-8 flex max-w-6xl flex-col text-center">
<div class="h-40">
{#if course != null}
<TopChart {course} />
{/if}
</div>
{#each drawSkills as { skill, draw }, i (i)}
<div class="h-28 border-t">
{#if course != null}
<CourseChart {course} {draw} />
{/if}
</div>
<div class="flex flex-row items-center gap-2">
<SkillIcon icon_id={skill.icon_id} size={32} />
<Pick id={`skill${i}`} class="w-64" items={skills} key={skillKey} bind:value={selSkills[i]} required>
{#snippet option(sk)}
<span>
<SkillIcon icon_id={sk.icon_id} size={16} />
{skillKey(sk).name}
</span>
{/snippet}
</Pick>
<div class="flex grow flex-col items-center">
{#each skill.activations as act, i (i)}
{#if skill.activations.length > 1}
<span class="block text-xs italic">Activation {i + 1}</span>
{/if}
{#if act.precondition}
<span class="block text-sm italic">Precondition</span>
{@render showcond(act.precondition)}
<span class="block text-sm italic">Condition</span>
{/if}
{@render showcond(act.condition)}
{/each}
</div>
<div class="flex shrink flex-row items-stretch gap-1 px-2 pb-4">
<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 pointer-coarse:hidden"
onclick={() => swapSkill(i, -1)}
>
&uparrow;
</button>
<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={() => delSkill(i)}
>
&times;
</button>
<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 pointer-coarse:hidden"
onclick={() => swapSkill(i, 1)}
>
&downarrow;
</button>
</div>
</div>
{/each}
<div class="flex items-center-safe justify-center-safe gap-2 border-t pt-8">
<Pick id="addSkill" class="w-64" items={skills} key={skillKey} bind:value={skillToAdd} required>
{#snippet option(sk)}
<span>
<SkillIcon icon_id={sk.icon_id} size={16} />
{skillKey(sk).name}
</span>
{/snippet}
</Pick>
<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>
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<p>This calculator separates conditions into four categories:</p>
<ul class="mb-4 list-disc pl-4">
<li>
Immediate conditions that are satisfied continuously during their range. The skill activates as soon as all other
conditions are satisfied. These conditions are mapped in <div
class="inline-block h-4 max-h-4 min-h-4 w-4 max-w-4 min-w-4 border bg-[lime] align-middle"
>
&nbsp;
</div>
green with an "Immediate" label.
</li>
<li>
Random conditions that are satisfied at a single point within their range, or several points in the case of e.g. <span
class="font-mono">all_corner_random==1</span
>. All other conditions must be satisfied at the time the horse reaches the random points for the skill to activate. Other
conditions may exclude portions of the random range, which can cause them to become impossible to trigger; this is
particularly common with <span class="font-mono">phase_random==0&nbsp;&amp;&nbsp;accumulatetime&gt;=5</span> skills. These
conditions are mapped in
<div class="inline-block h-4 max-h-4 min-h-4 w-4 max-w-4 min-w-4 border bg-[yellow] align-middle">&nbsp;</div>
yellow with a "Random" label.
</li>
<li>
Conditions satisfied by the Uma's other skills triggering. These are mapped in <div
class="inline-block h-4 max-h-4 min-h-4 w-4 max-w-4 min-w-4 border bg-[cyan] align-middle"
>
&nbsp;
</div>
cyan with a "Triggered" label.
</li>
<li>
Unsatisfiable conditions. These are mapped in <div
class="inline-block h-4 max-h-4 min-h-4 w-4 max-w-4 min-w-4 border bg-[red] align-middle"
>
&nbsp;
</div>
red with an "N/A" label.
</li>
</ul>
<p>
<span class="font-mono">accumulatetime</span> is implemented by assuming the horse runs at the
<a
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.q3a514loz268"
target="_blank"
rel="noopener noreferrer"
>
race base speed
</a>
from the gate. In practice, ignoring the start dash this way will tend to overestimate the starting position of the condition
especially for backline running styles, and it may underestimate it for runaways with full gate accels.
</p>
<p>
Condition zones were implemented by hand, and some combinations may have been missed. If you suspect a bug, please
@zephyrtronium on the GameTora, Hakuraku, or uma.moe Discord servers.
</p>
</div>
{#snippet pending()}
<div>Loading...</div>
{/snippet}
</svelte:boundary>
@@ -0,0 +1,53 @@
<script lang="ts">
import * as race from '$lib/race';
import * as racecourse from '$lib/data/racecourse';
import CourseChart from '$lib/CourseChart.svelte';
interface Props {
course: racecourse.Course;
}
let { course }: Props = $props();
function slopeprops(s: racecourse.Slope) {
if (s.per > 0) {
return { fy: 'Slope', x1: s.start, x2: s.end, label: `+${s.per / 10000}`, color: 'white' };
}
if (s.per < 0) {
return { fy: 'Slope', x1: s.start, x2: s.end, label: `${s.per / 10000}`, color: 'cyan' };
}
return { fy: 'Slope', x1: s.start, x2: s.end, color: 'none' };
}
function spanprops(s: racecourse.Span) {
if (s.corner != null) {
if (s.corner === 4) {
return { fy: 'Span', x1: s.start, x2: s.end, label: 'C4', color: 'red' };
}
return { fy: 'Span', x1: s.start, x2: s.end, label: `C${s.corner}`, color: 'cyan' };
}
if (s.straight != null) {
return { fy: 'Span', x1: s.start, x2: s.end, label: `S${s.straight}`, color: 'white' };
}
return { fy: 'Span', x1: s.start, x2: s.end, label: '~', color: 'pink' };
}
const poskeep = $derived((course.distance * 10) / 24);
const spotStruggle = $derived((course.distance * 9) / 24);
const rushStart = $derived(course.distance / 24);
const rushEnd = $derived((course.distance * 10) / 24);
const slopes = $derived(racecourse.fillSlopes(course.slopes, course.distance));
const phases = $derived(race.phases(course.distance));
const draw = $derived([
...slopes.map(slopeprops),
...course.spans.map(spanprops),
{ fy: 'Phase', x1: 0, x2: phases[race.Phase.EarlyRace], label: 'Early', color: 'cyan' },
{ fy: 'Phase', x1: phases[race.Phase.EarlyRace], x2: phases[race.Phase.MidRace], label: 'Mid', color: 'yellow' },
{ fy: 'Phase', x1: phases[race.Phase.MidRace], x2: phases[race.Phase.LateRace], label: 'Late', color: 'magenta' },
{ fy: 'Phase', x1: phases[race.Phase.LateRace], x2: phases[race.Phase.LastSpurt], label: 'Spurt', color: 'magenta' },
{ fy: 'PosKeep', x1: 0, x2: poskeep, color: 'purple' },
{ fy: 'SpotS', x1: 150, x2: spotStruggle, color: 'green' },
{ fy: 'Rush', x1: rushStart, x2: rushEnd, color: 'red' },
]);
</script>
<CourseChart {course} {draw} />
+4
View File
@@ -103,6 +103,10 @@
race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen),
race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen),
] as [number, number],
[race.Phase.LastSpurt]: [
race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen),
race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen),
] as [number, number],
});
// const isFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.Runaway);
@@ -0,0 +1,194 @@
career,turn,scenario,character,race,program_id,place,base_fans,deck_bonus_pct,margin_lengths,skill_activations,margin_bonus_pct,skill_bonus_pct,item_bonus_pct,deck_only_fans,actual_fans,extra_fans,extra_pct,predicted_fans,residual_fans
2026-08-19 20:37:27,12,3,[Ashen Miracle] Oguri Cap,Junior Make Debut,1070,1,700,55,4.4727,0,6,0,0,1085.0,1150,65.0,5.9908,1150,0
2026-08-19 20:37:27,23,3,[Ashen Miracle] Oguri Cap,Hanshin Juvenile Fillies,623,6,585,55,1.2941,0,0,0,0,906.75,906,-0.75,-0.0827,906,0
2026-08-19 20:37:27,24,3,[Ashen Miracle] Oguri Cap,Hopeful Stakes,625,1,7000,55,1.1495,0,2,0,0,10850.0,11067,217.0,2.0,11067,0
2026-08-19 20:37:27,33,3,[Ashen Miracle] Oguri Cap,NHK Mile Cup,164,1,10500,55,1.6165,0,2,0,0,16275.0,16601,326.0,2.0031,16601,0
2026-08-19 20:37:27,46,3,[Ashen Miracle] Oguri Cap,Mile Championship,78,1,11000,55,5.3966,5,6,3,0,17050.0,18585,1535.0,9.0029,18585,0
2026-08-19 20:37:27,48,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,55,8.8065,6,9,3,0,46500.0,52080,5580.0,12.0,52080,0
2026-08-19 20:37:27,54,3,[Ashen Miracle] Oguri Cap,Osaka Hai,3,1,13500,55,3.3853,4,4,3,0,20925.0,22390,1465.0,7.0012,22390,0
2026-08-19 20:37:27,57,3,[Ashen Miracle] Oguri Cap,Victoria Mile,5,1,10500,55,4.8613,2,6,2,0,16275.0,17577,1302.0,8.0,17577,0
2026-08-19 20:37:27,68,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Autumn),76,1,15000,55,14.0515,6,9,3,0,23250.0,26040,2790.0,12.0,26040,0
2026-08-19 20:37:27,72,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,55,10.5066,7,9,5,0,46500.0,53010,6510.0,14.0,53010,0
2026-08-19 20:37:27,74,3,[Ashen Miracle] Oguri Cap,URA Finale Qualifier,2015,1,7000,55,6.1557,6,9,3,0,10850.0,12152,1302.0,12.0,12152,0
2026-08-19 20:37:27,76,3,[Ashen Miracle] Oguri Cap,URA Finale Semifinal,2112,1,10000,55,6.906,7,9,5,0,15500.0,17670,2170.0,14.0,17670,0
2026-08-19 20:37:27,78,3,[Ashen Miracle] Oguri Cap,URA Finale Finals,2213,1,30000,55,7.8077,8,9,5,0,46500.0,53010,6510.0,14.0,53010,0
2026-09-08 21:05:53,12,3,[Ashen Miracle] Oguri Cap,Junior Make Debut,1070,1,700,70,2.4747,0,2,0,0,1190.0,1213,23.0,1.9328,1213,0
2026-09-08 21:05:53,23,3,[Ashen Miracle] Oguri Cap,Hanshin Juvenile Fillies,623,3,1625,70,2.5458,0,4,0,0,2762.5,2873,110.5,4.0,2873,0
2026-09-08 21:05:53,32,3,[Ashen Miracle] Oguri Cap,Flora Stakes,173,1,5200,70,0.166,0,0,0,0,8840.0,8840,0.0,0.0,8840,0
2026-09-08 21:05:53,33,3,[Ashen Miracle] Oguri Cap,NHK Mile Cup,164,1,10500,70,0.6636,0,0,0,0,17850.0,17850,0.0,0.0,17850,0
2026-09-08 21:05:53,46,3,[Ashen Miracle] Oguri Cap,Mile Championship,78,1,11000,70,0.3634,0,0,0,0,18700.0,18700,0.0,0.0,18700,0
2026-09-08 21:05:53,48,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,3.9924,0,4,0,0,51000.0,53040,2040.0,4.0,53040,0
2026-09-08 21:05:53,54,3,[Ashen Miracle] Oguri Cap,Osaka Hai,3,1,13500,70,3.7863,0,4,0,0,22950.0,23868,918.0,4.0,23868,0
2026-09-08 21:05:53,57,3,[Ashen Miracle] Oguri Cap,Victoria Mile,5,1,10500,70,1.4385,0,2,0,0,17850.0,18207,357.0,2.0,18207,0
2026-09-08 21:05:53,68,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Autumn),76,1,15000,70,3.9784,0,4,0,0,25500.0,26520,1020.0,4.0,26520,0
2026-09-08 21:05:53,72,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,7.8092,0,9,0,0,51000.0,55590,4590.0,9.0,55590,0
2026-09-08 21:05:53,74,3,[Ashen Miracle] Oguri Cap,URA Finale Qualifier,2011,1,7000,70,4.2694,0,6,0,0,11900.0,12614,714.0,6.0,12614,0
2026-09-08 21:05:53,76,3,[Ashen Miracle] Oguri Cap,URA Finale Semifinal,2108,1,10000,70,1.7312,0,2,0,0,17000.0,17340,340.0,2.0,17340,0
2026-09-08 21:05:53,78,3,[Ashen Miracle] Oguri Cap,URA Finale Finals,2209,1,30000,70,2.9177,0,4,0,0,51000.0,53040,2040.0,4.0,53040,0
2026-09-16 13:24:41,12,3,[Ashen Miracle] Oguri Cap,Junior Make Debut,1070,1,700,70,6.3564,0,9,0,0,1190.0,1297,107.0,8.9916,1297,0
2026-09-16 13:24:41,23,3,[Ashen Miracle] Oguri Cap,Hanshin Juvenile Fillies,623,1,6500,70,0.5447,0,0,0,0,11050.0,11050,0.0,0.0,11050,0
2026-09-16 13:24:41,33,3,[Ashen Miracle] Oguri Cap,NHK Mile Cup,164,1,10500,70,1.2208,0,2,0,0,17850.0,18207,357.0,2.0,18207,0
2026-09-16 13:24:41,46,3,[Ashen Miracle] Oguri Cap,Mile Championship,78,1,11000,70,1.8764,0,2,0,0,18700.0,19074,374.0,2.0,19074,0
2026-09-16 13:24:41,48,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,7.2065,0,9,0,0,51000.0,55590,4590.0,9.0,55590,0
2026-09-16 13:24:41,54,3,[Ashen Miracle] Oguri Cap,Osaka Hai,3,1,13500,70,9.1057,0,9,0,0,22950.0,25016,2066.0,9.0022,25016,0
2026-09-16 13:24:41,59,3,[Ashen Miracle] Oguri Cap,Yasuda Kinen,73,1,13000,70,4.3705,0,6,0,0,22100.0,23426,1326.0,6.0,23426,0
2026-09-16 13:24:41,68,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Autumn),76,1,15000,70,7.933,0,9,0,0,25500.0,27795,2295.0,9.0,27795,0
2026-09-16 13:24:41,72,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,5.7931,0,6,0,0,51000.0,54060,3060.0,6.0,54060,0
2026-09-16 13:24:41,74,3,[Ashen Miracle] Oguri Cap,URA Finale Qualifier,2010,1,7000,70,5.7824,0,6,0,0,11900.0,12614,714.0,6.0,12614,0
2026-09-16 13:24:41,76,3,[Ashen Miracle] Oguri Cap,URA Finale Semifinal,2112,1,10000,70,2.7856,0,4,0,0,17000.0,17680,680.0,4.0,17680,0
2026-09-16 13:24:41,78,3,[Ashen Miracle] Oguri Cap,URA Finale Finals,2208,1,30000,70,5.4409,0,6,0,0,51000.0,54060,3060.0,6.0,54060,0
2026-09-17 20:23:11,12,3,[Ashen Miracle] Oguri Cap,Junior Make Debut,1070,1,700,70,5.3797,0,6,0,0,1190.0,1261,71.0,5.9664,1261,0
2026-09-17 20:23:11,24,3,[Ashen Miracle] Oguri Cap,Hopeful Stakes,625,1,7000,70,4.8411,2,6,2,0,11900.0,12852,952.0,8.0,12852,0
2026-09-17 20:23:11,33,3,[Ashen Miracle] Oguri Cap,NHK Mile Cup,164,1,10500,70,3.0921,1,4,2,0,17850.0,18921,1071.0,6.0,18921,0
2026-09-17 20:23:11,46,3,[Ashen Miracle] Oguri Cap,Mile Championship,78,1,11000,70,3.4861,1,4,2,0,18700.0,19822,1122.0,6.0,19822,0
2026-09-17 20:23:11,48,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,8.0058,1,9,2,0,51000.0,56610,5610.0,11.0,56610,0
2026-09-17 20:23:11,51,3,[Ashen Miracle] Oguri Cap,Kawasaki Kinen,1106,1,6000,70,5.6972,1,6,2,0,10200.0,11016,816.0,8.0,11016,0
2026-09-17 20:23:11,52,3,[Ashen Miracle] Oguri Cap,February Stakes,1,1,10000,70,0.5589,1,0,2,0,17000.0,17340,340.0,2.0,17340,0
2026-09-17 20:23:11,68,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Autumn),76,1,15000,70,4.6895,2,6,2,0,25500.0,27540,2040.0,8.0,27540,0
2026-09-17 20:23:11,72,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,9.0895,1,9,2,0,51000.0,56610,5610.0,11.0,56610,0
2026-09-17 20:23:11,74,3,[Ashen Miracle] Oguri Cap,URA Finale Qualifier,2014,1,7000,70,6.1828,1,9,2,0,11900.0,13209,1309.0,11.0,13209,0
2026-09-17 20:23:11,76,3,[Ashen Miracle] Oguri Cap,URA Finale Semifinal,2111,1,10000,70,2.8136,1,4,2,0,17000.0,18020,1020.0,6.0,18020,0
2026-09-17 20:23:11,78,3,[Ashen Miracle] Oguri Cap,URA Finale Finals,2212,1,30000,70,1.061,1,2,2,0,51000.0,53040,2040.0,4.0,53040,0
2026-09-17 22:45:31,12,3,[Ashen Miracle] Oguri Cap,Junior Make Debut,1070,1,700,70,2.2776,1,2,2,0,1190.0,1237,47.0,3.9496,1237,0
2026-09-17 22:45:31,23,3,[Ashen Miracle] Oguri Cap,Hanshin Juvenile Fillies,623,2,2600,70,0.0231,0,9,0,0,4420.0,4817,397.0,8.9819,4817,0
2026-09-17 22:45:31,33,3,[Ashen Miracle] Oguri Cap,NHK Mile Cup,164,1,10500,70,2.7073,2,4,2,0,17850.0,18921,1071.0,6.0,18921,0
2026-09-17 22:45:31,46,3,[Ashen Miracle] Oguri Cap,Mile Championship,78,1,11000,70,1.9154,2,2,2,0,18700.0,19448,748.0,4.0,19448,0
2026-09-17 22:45:31,48,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,4.8206,2,6,2,0,51000.0,55080,4080.0,8.0,55080,0
2026-09-17 22:45:31,56,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Spring),4,1,15000,70,14.398,5,9,3,0,25500.0,28560,3060.0,12.0,28560,0
2026-09-17 22:45:31,57,3,[Ashen Miracle] Oguri Cap,Victoria Mile,5,1,10500,70,2.1273,3,2,2,0,17850.0,18564,714.0,4.0,18564,0
2026-09-17 22:45:31,68,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Autumn),76,1,15000,70,7.6906,5,9,3,0,25500.0,28560,3060.0,12.0,28560,0
2026-09-17 22:45:31,72,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,7.549,6,9,3,0,51000.0,57120,6120.0,12.0,57120,0
2026-09-17 22:45:31,74,3,[Ashen Miracle] Oguri Cap,URA Finale Qualifier,2014,1,7000,70,6.4801,3,9,2,0,11900.0,13209,1309.0,11.0,13209,0
2026-09-17 22:45:31,76,3,[Ashen Miracle] Oguri Cap,URA Finale Semifinal,2109,1,10000,70,5.9469,7,6,5,0,17000.0,18870,1870.0,11.0,18870,0
2026-09-17 22:45:31,78,3,[Ashen Miracle] Oguri Cap,URA Finale Finals,2212,1,30000,70,7.2892,5,9,3,0,51000.0,57120,6120.0,12.0,57120,0
2026-09-19 20:01:05,12,3,[Ashen Miracle] Oguri Cap,Junior Make Debut,1070,1,700,70,6.6525,0,9,0,0,1190.0,1297,107.0,8.9916,1297,0
2026-09-19 20:01:05,23,3,[Ashen Miracle] Oguri Cap,Asahi Hai Futurity Stakes,624,3,1750,70,1.5022,0,6,0,0,2975.0,3153,178.0,5.9832,3153,0
2026-09-19 20:01:05,24,3,[Ashen Miracle] Oguri Cap,Hopeful Stakes,625,1,7000,70,5.6931,0,6,0,0,11900.0,12614,714.0,6.0,12614,0
2026-09-19 20:01:05,33,3,[Ashen Miracle] Oguri Cap,NHK Mile Cup,164,1,10500,70,1.488,0,2,0,0,17850.0,18207,357.0,2.0,18207,0
2026-09-19 20:01:05,46,3,[Ashen Miracle] Oguri Cap,Mile Championship,78,1,11000,70,3.8134,0,4,0,0,18700.0,19448,748.0,4.0,19448,0
2026-09-19 20:01:05,48,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,2.6633,0,4,0,0,51000.0,53040,2040.0,4.0,53040,0
2026-09-19 20:01:05,52,3,[Ashen Miracle] Oguri Cap,February Stakes,1,1,10000,70,0.6637,0,0,0,0,17000.0,17000,0.0,0.0,17000,0
2026-09-19 20:01:05,59,3,[Ashen Miracle] Oguri Cap,Yasuda Kinen,73,1,13000,70,5.9364,0,6,0,0,22100.0,23426,1326.0,6.0,23426,0
2026-09-19 20:01:05,68,3,[Ashen Miracle] Oguri Cap,Tenno Sho (Autumn),76,1,15000,70,6.0564,0,9,0,0,25500.0,27795,2295.0,9.0,27795,0
2026-09-19 20:01:05,72,3,[Ashen Miracle] Oguri Cap,Arima Kinen,81,1,30000,70,7.8703,0,9,0,0,51000.0,55590,4590.0,9.0,55590,0
2026-09-19 20:01:05,74,3,[Ashen Miracle] Oguri Cap,URA Finale Qualifier,2015,1,7000,70,5.8192,0,6,0,0,11900.0,12614,714.0,6.0,12614,0
2026-09-19 20:01:05,76,3,[Ashen Miracle] Oguri Cap,URA Finale Semifinal,2111,1,10000,70,5.2726,0,6,0,0,17000.0,18020,1020.0,6.0,18020,0
2026-09-19 20:01:05,78,3,[Ashen Miracle] Oguri Cap,URA Finale Finals,2213,1,30000,70,5.6513,0,6,0,0,51000.0,54060,3060.0,6.0,54060,0
2026-09-19 21:45:59,12,2,[Blossom in Learning] Sakura Bakushin O,Junior Make Debut,676,1,700,65,1.7631,0,2,0,0,1155.0,1178,23.0,1.9913,1178,0
2026-09-19 21:45:59,21,2,[Blossom in Learning] Sakura Bakushin O,Keio Hai Junior Stakes,626,1,3800,65,0.917,0,2,0,0,6270.0,6395,125.0,1.9936,6395,0
2026-09-19 21:45:59,30,2,[Blossom in Learning] Sakura Bakushin O,Spring Stakes,171,2,2160,65,0.7907,1,6,2,0,3564.0,3847,283.0,7.9405,3847,0
2026-09-19 21:45:59,34,2,[Blossom in Learning] Sakura Bakushin O,Aoi Stakes,210,2,1520,65,1.5851,0,6,0,0,2508.0,2658,150.0,5.9809,2658,0
2026-09-19 21:45:59,35,2,[Blossom in Learning] Sakura Bakushin O,Epsom Cup,95,1,4100,65,1.3364,1,2,2,0,6765.0,7035,270.0,3.9911,7035,0
2026-09-19 21:45:59,42,2,[Blossom in Learning] Sakura Bakushin O,Sprinters Stakes,75,3,3250,65,0.8705,0,6,0,0,5362.5,5684,321.5,5.9953,5684,0
2026-09-19 21:45:59,54,2,[Blossom in Learning] Sakura Bakushin O,Takamatsunomiya Kinen,2,2,5200,65,0.451,0,9,0,0,8580.0,9352,772.0,8.9977,9352,0
2026-09-19 21:45:59,60,2,[Blossom in Learning] Sakura Bakushin O,Hakodate Sprint Stakes,96,1,3900,65,0.9971,0,2,0,0,6435.0,6563,128.0,1.9891,6563,0
2026-09-19 21:45:59,61,2,[Blossom in Learning] Sakura Bakushin O,CBC Sho,97,1,3900,65,2.3483,0,2,0,0,6435.0,6563,128.0,1.9891,6563,0
2026-09-19 21:45:59,65,2,[Blossom in Learning] Sakura Bakushin O,Centaur Stakes,83,1,5900,65,1.5096,0,2,0,0,9735.0,9929,194.0,1.9928,9929,0
2026-09-19 21:45:59,66,2,[Blossom in Learning] Sakura Bakushin O,Sprinters Stakes,75,6,1170,65,2.5098,1,0,2,0,1930.5,1968,37.5,1.9425,1968,0
2026-09-23 23:41:07,12,1,[Blossom in Learning] Sakura Bakushin O,Junior Make Debut,676,1,700,65,2.7319,0,4,0,0,1155.0,1201,46.0,3.9827,1201,0
2026-09-23 23:41:07,21,1,[Blossom in Learning] Sakura Bakushin O,Keio Hai Junior Stakes,626,1,3800,65,0.5164,0,0,0,0,6270.0,6270,0.0,0.0,6270,0
2026-09-23 23:41:07,30,1,[Blossom in Learning] Sakura Bakushin O,Spring Stakes,171,1,5400,65,2.4521,1,2,2,0,8910.0,9266,356.0,3.9955,9266,0
2026-09-23 23:41:07,34,1,[Blossom in Learning] Sakura Bakushin O,Aoi Stakes,210,1,3800,65,0.4297,0,0,0,0,6270.0,6270,0.0,0.0,6270,0
2026-09-23 23:41:07,42,1,[Blossom in Learning] Sakura Bakushin O,Sprinters Stakes,75,6,1170,65,3.8513,0,0,0,0,1930.5,1930,-0.5,-0.0259,1930,0
2026-09-28 14:22:08,12,1,[Hot☆Summer Night] Maruzensky,Junior Make Debut,676,1,700,65,0.2547,0,0,0,0,1155.0,1155,0.0,0.0,1155,0
2026-10-01 09:39:24,12,2,[Wild Frontier] Taiki Shuttle,Junior Make Debut,846,1,700,80,2.9274,1,4,2,0,1260.0,1335,75.0,5.9524,1335,0
2026-10-01 09:39:24,25,2,[Wild Frontier] Taiki Shuttle,Shinzan Kinen,178,1,3800,80,3.297,0,4,0,0,6840.0,7113,273.0,3.9912,7113,0
2026-10-01 09:39:24,33,2,[Wild Frontier] Taiki Shuttle,NHK Mile Cup,164,1,10500,80,1.5041,0,2,0,0,18900.0,19278,378.0,2.0,19278,0
2026-10-01 09:39:24,36,2,[Wild Frontier] Taiki Shuttle,Unicorn Stakes,189,1,3500,80,0.0829,0,0,0,0,6300.0,6300,0.0,0.0,6300,0
2026-10-01 09:39:24,46,2,[Wild Frontier] Taiki Shuttle,Mile Championship,78,1,11000,80,1.3511,1,2,2,0,19800.0,20592,792.0,4.0,20592,0
2026-10-01 09:39:24,59,2,[Wild Frontier] Taiki Shuttle,Yasuda Kinen,73,1,13000,80,4.4812,0,6,0,0,23400.0,24804,1404.0,6.0,24804,0
2026-10-01 09:39:24,66,2,[Wild Frontier] Taiki Shuttle,Sprinters Stakes,75,1,13000,80,3.6966,1,4,2,0,23400.0,24804,1404.0,6.0,24804,0
2026-10-01 09:39:24,70,2,[Wild Frontier] Taiki Shuttle,Mile Championship,78,1,11000,80,3.6774,0,4,0,0,19800.0,20592,792.0,4.0,20592,0
2026-10-01 09:39:24,74,2,[Wild Frontier] Taiki Shuttle,URA Finale Qualifier,2013,1,7000,80,4.0671,0,6,0,0,12600.0,13356,756.0,6.0,13356,0
2026-10-01 09:39:24,76,2,[Wild Frontier] Taiki Shuttle,URA Finale Semifinal,2109,1,10000,80,4.9632,1,6,2,0,18000.0,19440,1440.0,8.0,19440,0
2026-10-01 09:39:24,78,2,[Wild Frontier] Taiki Shuttle,URA Finale Finals,2214,1,30000,80,1.1329,0,2,0,0,54000.0,55080,1080.0,2.0,55080,0
2026-10-04 14:36:48,12,4,[Wild Frontier] Taiki Shuttle,Junior Make Debut,846,1,700,105,2.6127,1,4,2,0,1435.0,1521,86.0,5.993,1521,0
2026-10-04 14:36:48,14,4,[Wild Frontier] Taiki Shuttle,Hakodate Junior Stakes,628,1,3100,105,4.3676,0,6,0,0,6355.0,6736,381.0,5.9953,6736,0
2026-10-04 14:36:48,16,4,[Wild Frontier] Taiki Shuttle,Niigata Junior Stakes,629,1,3100,105,2.8947,0,4,0,0,6355.0,6609,254.0,3.9969,6609,0
2026-10-04 14:36:48,17,4,[Wild Frontier] Taiki Shuttle,Sapporo Junior Stakes,630,1,3100,105,2.2675,1,2,2,0,6355.0,6609,254.0,3.9969,6609,0
2026-10-04 14:36:48,21,4,[Wild Frontier] Taiki Shuttle,Keio Hai Junior Stakes,626,1,3800,105,1.8215,1,2,2,0,7790.0,8101,311.0,3.9923,8101,0
2026-10-04 14:36:48,22,4,[Wild Frontier] Taiki Shuttle,Tokyo Sports Hai Junior Stakes,635,1,3300,105,4.1972,0,6,0,0,6765.0,7170,405.0,5.9867,7170,0
2026-10-04 14:36:48,23,4,[Wild Frontier] Taiki Shuttle,Hanshin Juvenile Fillies,623,1,6500,105,4.4133,1,6,2,0,13325.0,14391,1066.0,8.0,14391,0
2026-10-04 14:36:48,24,4,[Wild Frontier] Taiki Shuttle,Zen-Nippon Junior Yushun,1107,2,1680,105,0.6647,1,9,2,0,3444.0,3821,377.0,10.9466,3821,0
2026-10-04 14:36:48,27,4,[Wild Frontier] Taiki Shuttle,Kyodo News Hai,183,1,3800,105,3.9819,1,4,2,0,7790.0,8257,467.0,5.9949,8257,0
2026-10-04 14:36:48,29,4,[Wild Frontier] Taiki Shuttle,Fillies' Revue,170,2,2080,105,0.056,1,9,2,0,4264.0,4731,467.0,10.9522,4731,0
2026-10-04 14:36:48,30,4,[Wild Frontier] Taiki Shuttle,Spring Stakes,171,1,5400,105,1.5715,1,2,2,0,11070.0,11513,443.0,4.0018,11513,0
2026-10-04 14:36:48,31,4,[Wild Frontier] Taiki Shuttle,Oka Sho,162,1,10500,105,2.0067,0,2,0,0,21525.0,21956,431.0,2.0023,21956,0
2026-10-04 14:36:48,33,4,[Wild Frontier] Taiki Shuttle,NHK Mile Cup,164,1,10500,105,1.3928,0,2,0,0,21525.0,21956,431.0,2.0023,21956,0
2026-10-04 14:36:48,35,4,[Wild Frontier] Taiki Shuttle,Yasuda Kinen,73,1,13000,105,1.2662,1,2,2,0,26650.0,27716,1066.0,4.0,27716,0
2026-10-04 14:36:48,39,4,[Wild Frontier] Taiki Shuttle,Sekiya Kinen,105,1,3900,105,3.8619,0,4,0,0,7995.0,8314,319.0,3.99,8314,0
2026-10-04 14:36:48,41,4,[Wild Frontier] Taiki Shuttle,Rose Stakes,176,1,5200,105,3.3191,1,4,2,0,10660.0,11300,640.0,6.0038,11300,0
2026-10-04 14:36:48,42,4,[Wild Frontier] Taiki Shuttle,Sprinters Stakes,75,1,13000,105,3.2007,1,4,2,0,26650.0,28249,1599.0,6.0,28249,0
2026-10-04 14:36:48,43,4,[Wild Frontier] Taiki Shuttle,M.C. Nambu Hai,1109,1,6000,105,2.7546,0,4,0,0,12300.0,12792,492.0,4.0,12792,0
2026-10-04 14:36:48,45,4,[Wild Frontier] Taiki Shuttle,JBC Sprint,1111,1,6000,105,1.5294,1,2,2,0,12300.0,12792,492.0,4.0,12792,0
2026-10-04 14:36:48,46,4,[Wild Frontier] Taiki Shuttle,Mile Championship,78,1,11000,105,2.2484,0,2,0,0,22550.0,23001,451.0,2.0,23001,0
2026-10-04 14:36:48,47,4,[Wild Frontier] Taiki Shuttle,Champions Cup,80,1,10000,105,2.5859,0,4,0,0,20500.0,21320,820.0,4.0,21320,0
2026-10-04 14:36:48,50,4,[Wild Frontier] Taiki Shuttle,Negishi Stakes,22,2,1520,105,1.4117,0,6,0,0,3116.0,3302,186.0,5.9692,3302,0
2026-10-04 14:36:48,52,4,[Wild Frontier] Taiki Shuttle,February Stakes,1,1,10000,105,0.0968,0,0,0,0,20500.0,20500,0.0,0.0,20500,0
2026-10-04 14:36:48,54,4,[Wild Frontier] Taiki Shuttle,Takamatsunomiya Kinen,2,1,13000,105,3.2402,0,4,0,0,26650.0,27716,1066.0,4.0,27716,0
2026-10-04 14:36:48,55,4,[Wild Frontier] Taiki Shuttle,Marine Cup,1128,1,2500,105,2.6992,0,4,0,0,5125.0,5330,205.0,4.0,5330,0
2026-10-04 14:36:48,57,4,[Wild Frontier] Taiki Shuttle,Victoria Mile,5,1,10500,105,1.5024,0,2,0,0,21525.0,21956,431.0,2.0023,21956,0
2026-10-04 14:36:48,58,4,[Wild Frontier] Taiki Shuttle,Keyaki Stakes,71,1,2200,105,3.8244,1,4,2,0,4510.0,4780,270.0,5.9867,4780,0
2026-10-04 14:36:48,59,4,[Wild Frontier] Taiki Shuttle,Yasuda Kinen,73,1,13000,105,1.4361,1,2,2,0,26650.0,27716,1066.0,4.0,27716,0
2026-10-04 14:36:48,62,4,[Wild Frontier] Taiki Shuttle,Queen Stakes,103,1,3600,105,5.0515,0,6,0,0,7380.0,7822,442.0,5.9892,7822,0
2026-10-04 14:36:48,65,4,[Wild Frontier] Taiki Shuttle,Keisei Hai Autumn Handicap,110,1,3900,105,5.633,0,6,0,0,7995.0,8474,479.0,5.9912,8474,0
2026-10-04 14:36:48,66,4,[Wild Frontier] Taiki Shuttle,Sprinters Stakes,75,1,13000,105,2.6328,0,4,0,0,26650.0,27716,1066.0,4.0,27716,0
2026-10-04 14:36:48,67,4,[Wild Frontier] Taiki Shuttle,M.C. Nambu Hai,1109,1,6000,105,4.184,0,6,0,0,12300.0,13038,738.0,6.0,13038,0
2026-10-04 14:36:48,69,4,[Wild Frontier] Taiki Shuttle,JBC Sprint,1025,1,6000,105,2.1263,1,2,2,0,12300.0,12792,492.0,4.0,12792,0
2026-10-04 14:36:48,70,4,[Wild Frontier] Taiki Shuttle,Mile Championship,78,1,11000,105,2.1798,0,2,0,0,22550.0,23001,451.0,2.0,23001,0
2026-10-04 14:36:48,71,4,[Wild Frontier] Taiki Shuttle,Champions Cup,80,1,10000,105,0.8076,1,2,2,0,20500.0,21320,820.0,4.0,21320,0
2026-10-04 14:36:48,74,4,[Wild Frontier] Taiki Shuttle,Twinkle Star Climax Race 1,2313,1,7000,105,4.0466,1,6,2,0,14350.0,15498,1148.0,8.0,15498,0
2026-10-04 14:36:48,76,4,[Wild Frontier] Taiki Shuttle,Twinkle Star Climax Race 2,2410,1,10000,105,1.8345,1,2,2,0,20500.0,21320,820.0,4.0,21320,0
2026-10-04 14:36:48,78,4,[Wild Frontier] Taiki Shuttle,Twinkle Star Climax Race 3,2515,1,30000,105,5.1406,1,6,2,50,61500.0,99630,38130.0,62.0,99630,0
2026-10-08 10:00:27,12,3,[CODE: ICING] Mihono Bourbon,Junior Make Debut,1075,1,700,70,3.126,1,4,2,0,1190.0,1261,71.0,5.9664,1261,0
2026-10-08 10:00:27,23,3,[CODE: ICING] Mihono Bourbon,Asahi Hai Futurity Stakes,624,1,7000,70,3.0099,2,4,2,0,11900.0,12614,714.0,6.0,12614,0
2026-10-08 10:00:27,30,3,[CODE: ICING] Mihono Bourbon,Spring Stakes,171,1,5400,70,1.4594,1,2,2,0,9180.0,9547,367.0,3.9978,9547,0
2026-10-08 10:00:27,31,3,[CODE: ICING] Mihono Bourbon,Satsuki Sho,163,1,11000,70,6.1843,2,9,2,0,18700.0,20757,2057.0,11.0,20757,0
2026-10-08 10:00:27,34,3,[CODE: ICING] Mihono Bourbon,Tokyo Yushun (Japanese Derby),166,1,20000,70,4.682,2,6,2,0,34000.0,36720,2720.0,8.0,36720,0
2026-10-08 10:00:27,44,3,[CODE: ICING] Mihono Bourbon,Kikuka Sho,168,1,12000,70,3.5273,1,4,2,0,20400.0,21624,1224.0,6.0,21624,0
2026-10-08 10:00:27,56,3,[CODE: ICING] Mihono Bourbon,Tenno Sho (Spring),4,1,15000,70,10.0374,2,9,2,0,25500.0,28305,2805.0,11.0,28305,0
2026-10-08 10:00:27,70,3,[CODE: ICING] Mihono Bourbon,Japan Cup,79,1,30000,70,10.1623,2,9,2,0,51000.0,56610,5610.0,11.0,56610,0
2026-10-08 10:00:27,72,3,[CODE: ICING] Mihono Bourbon,Arima Kinen,81,1,30000,70,10.4669,3,9,2,0,51000.0,56610,5610.0,11.0,56610,0
2026-10-08 10:00:27,74,3,[CODE: ICING] Mihono Bourbon,URA Finale Qualifier,2010,1,7000,70,5.7274,3,6,2,0,11900.0,12852,952.0,8.0,12852,0
2026-10-08 10:00:27,76,3,[CODE: ICING] Mihono Bourbon,URA Finale Semifinal,2113,1,10000,70,3.7223,3,4,2,0,17000.0,18020,1020.0,6.0,18020,0
2026-10-08 10:00:27,78,3,[CODE: ICING] Mihono Bourbon,URA Finale Finals,2209,1,30000,70,3.873,3,4,2,0,51000.0,54060,3060.0,6.0,54060,0
2026-10-10 03:12:29,12,3,[Soirée des Chatons] Seiun Sky,Junior Make Debut,689,1,700,70,3.0303,1,4,2,0,1190.0,1261,71.0,5.9664,1261,0
2026-10-10 03:12:29,24,3,[Soirée des Chatons] Seiun Sky,Hopeful Stakes,625,1,7000,70,2.1576,1,2,2,0,11900.0,12376,476.0,4.0,12376,0
2026-10-10 03:12:29,31,3,[Soirée des Chatons] Seiun Sky,Satsuki Sho,163,1,11000,70,2.5681,2,4,2,0,18700.0,19822,1122.0,6.0,19822,0
2026-10-10 03:12:29,34,3,[Soirée des Chatons] Seiun Sky,Tokyo Yushun (Japanese Derby),166,1,20000,70,2.4678,2,2,2,0,34000.0,35360,1360.0,4.0,35360,0
2026-10-10 03:12:29,44,3,[Soirée des Chatons] Seiun Sky,Kikuka Sho,168,1,12000,70,0.6938,2,0,2,0,20400.0,20808,408.0,2.0,20808,0
2026-10-10 03:12:29,48,3,[Soirée des Chatons] Seiun Sky,Arima Kinen,81,1,30000,70,3.3828,2,4,2,0,51000.0,54060,3060.0,6.0,54060,0
2026-10-10 03:12:29,56,3,[Soirée des Chatons] Seiun Sky,Tenno Sho (Spring),4,1,15000,70,20.5803,10,9,8,0,25500.0,29835,4335.0,17.0,29835,0
2026-10-10 03:12:29,60,3,[Soirée des Chatons] Seiun Sky,Takarazuka Kinen,74,1,15000,70,6.4095,10,9,8,0,25500.0,29835,4335.0,17.0,29835,0
2026-10-10 03:12:29,68,3,[Soirée des Chatons] Seiun Sky,Tenno Sho (Autumn),76,1,15000,70,9.3985,9,9,5,0,25500.0,29070,3570.0,14.0,29070,0
2026-10-10 03:12:29,72,3,[Soirée des Chatons] Seiun Sky,Arima Kinen,81,1,30000,70,11.7606,10,9,8,0,51000.0,59670,8670.0,17.0,59670,0
2026-10-10 03:12:29,74,3,[Soirée des Chatons] Seiun Sky,URA Finale Qualifier,2018,1,7000,70,11.2054,9,9,5,0,11900.0,13566,1666.0,14.0,13566,0
2026-10-10 03:12:29,76,3,[Soirée des Chatons] Seiun Sky,URA Finale Semifinal,2123,1,10000,70,8.0601,7,9,5,0,17000.0,19380,2380.0,14.0,19380,0
2026-10-10 03:12:29,78,3,[Soirée des Chatons] Seiun Sky,URA Finale Finals,2222,1,30000,70,5.7947,8,6,5,0,51000.0,56610,5610.0,11.0,56610,0
2026-10-10 15:53:30,12,3,[Soirée des Chatons] Seiun Sky,Junior Make Debut,689,1,700,70,3.7946,2,4,2,0,1190.0,1261,71.0,5.9664,1261,0
2026-10-10 15:53:30,24,3,[Soirée des Chatons] Seiun Sky,Hopeful Stakes,625,1,7000,70,5.3394,1,6,2,0,11900.0,12852,952.0,8.0,12852,0
2026-10-10 15:53:30,31,3,[Soirée des Chatons] Seiun Sky,Satsuki Sho,163,1,11000,70,7.5947,2,9,2,0,18700.0,20757,2057.0,11.0,20757,0
2026-10-10 15:53:30,34,3,[Soirée des Chatons] Seiun Sky,Tokyo Yushun (Japanese Derby),166,1,20000,70,2.1677,4,2,3,0,34000.0,35700,1700.0,5.0,35700,0
2026-10-10 15:53:30,44,3,[Soirée des Chatons] Seiun Sky,Kikuka Sho,168,3,3000,70,3.8191,4,4,3,0,5100.0,5457,357.0,7.0,5457,0
2026-10-10 15:53:30,48,3,[Soirée des Chatons] Seiun Sky,Arima Kinen,81,1,30000,70,12.6422,3,9,2,0,51000.0,56610,5610.0,11.0,56610,0
2026-10-10 15:53:30,56,3,[Soirée des Chatons] Seiun Sky,Tenno Sho (Spring),4,1,15000,70,1.11,7,2,5,0,25500.0,27285,1785.0,7.0,27285,0
2026-10-10 15:53:30,60,3,[Soirée des Chatons] Seiun Sky,Takarazuka Kinen,74,1,15000,70,13.7279,7,9,5,0,25500.0,29070,3570.0,14.0,29070,0
2026-10-10 15:53:30,68,3,[Soirée des Chatons] Seiun Sky,Tenno Sho (Autumn),76,1,15000,70,3.6698,6,4,3,0,25500.0,27285,1785.0,7.0,27285,0
2026-10-10 15:53:30,72,3,[Soirée des Chatons] Seiun Sky,Arima Kinen,81,1,30000,70,13.5919,8,9,5,0,51000.0,58140,7140.0,14.0,58140,0
2026-10-10 15:53:30,74,3,[Soirée des Chatons] Seiun Sky,URA Finale Qualifier,2025,1,7000,70,13.045,7,9,5,0,11900.0,13566,1666.0,14.0,13566,0
2026-10-10 15:53:30,76,3,[Soirée des Chatons] Seiun Sky,URA Finale Semifinal,2122,1,10000,70,12.4002,7,9,5,0,17000.0,19380,2380.0,14.0,19380,0
2026-10-10 15:53:30,78,3,[Soirée des Chatons] Seiun Sky,URA Finale Finals,2217,1,30000,70,12.3247,7,9,5,0,51000.0,58140,7140.0,14.0,58140,0
2026-10-11 18:47:03,12,3,[Soirée des Chatons] Seiun Sky,Junior Make Debut,689,1,700,80,3.2149,2,4,2,0,1260.0,1335,75.0,5.9524,1335,0
2026-10-11 18:47:03,24,3,[Soirée des Chatons] Seiun Sky,Hopeful Stakes,625,1,7000,80,2.4816,2,2,2,0,12600.0,13104,504.0,4.0,13104,0
2026-10-11 19:13:35,12,3,[Soirée des Chatons] Seiun Sky,Junior Make Debut,689,1,700,80,4.1512,1,6,2,0,1260.0,1360,100.0,7.9365,1360,0
2026-10-11 19:13:35,24,3,[Soirée des Chatons] Seiun Sky,Hopeful Stakes,625,1,7000,80,2.8651,2,4,2,0,12600.0,13356,756.0,6.0,13356,0
2026-10-11 19:13:35,31,3,[Soirée des Chatons] Seiun Sky,Satsuki Sho,163,1,11000,80,4.6012,2,6,2,0,19800.0,21384,1584.0,8.0,21384,0
2026-10-11 19:13:35,34,3,[Soirée des Chatons] Seiun Sky,Tokyo Yushun (Japanese Derby),166,1,20000,80,4.1483,2,6,2,0,36000.0,38880,2880.0,8.0,38880,0
2026-10-11 19:13:35,44,3,[Soirée des Chatons] Seiun Sky,Kikuka Sho,168,2,4800,80,4.218,2,2,2,0,8640.0,8985,345.0,3.9931,8985,0
2026-10-11 19:13:35,48,3,[Soirée des Chatons] Seiun Sky,Arima Kinen,81,1,30000,80,10.0709,3,9,2,0,54000.0,59940,5940.0,11.0,59940,0
2026-10-11 19:13:35,56,3,[Soirée des Chatons] Seiun Sky,Tenno Sho (Spring),4,1,15000,80,6.5113,8,9,5,0,27000.0,30780,3780.0,14.0,30780,0
2026-10-11 19:13:35,60,3,[Soirée des Chatons] Seiun Sky,Takarazuka Kinen,74,1,15000,80,6.7601,6,9,3,0,27000.0,30240,3240.0,12.0,30240,0
2026-10-11 19:13:35,72,3,[Soirée des Chatons] Seiun Sky,Arima Kinen,81,1,30000,80,14.0565,7,9,5,0,54000.0,61560,7560.0,14.0,61560,0
2026-10-11 19:13:35,74,3,[Soirée des Chatons] Seiun Sky,URA Finale Qualifier,2018,1,7000,80,10.2344,7,9,5,0,12600.0,14364,1764.0,14.0,14364,0
2026-10-11 19:13:35,76,3,[Soirée des Chatons] Seiun Sky,URA Finale Semifinal,2121,1,10000,80,5.3797,5,6,3,0,18000.0,19620,1620.0,9.0,19620,0
2026-10-11 19:13:35,78,3,[Soirée des Chatons] Seiun Sky,URA Finale Finals,2222,1,30000,80,7.7281,7,9,5,0,54000.0,61560,7560.0,14.0,61560,0
1 career turn scenario character race program_id place base_fans deck_bonus_pct margin_lengths skill_activations margin_bonus_pct skill_bonus_pct item_bonus_pct deck_only_fans actual_fans extra_fans extra_pct predicted_fans residual_fans
2 2026-08-19 20:37:27 12 3 [Ashen Miracle] Oguri Cap Junior Make Debut 1070 1 700 55 4.4727 0 6 0 0 1085.0 1150 65.0 5.9908 1150 0
3 2026-08-19 20:37:27 23 3 [Ashen Miracle] Oguri Cap Hanshin Juvenile Fillies 623 6 585 55 1.2941 0 0 0 0 906.75 906 -0.75 -0.0827 906 0
4 2026-08-19 20:37:27 24 3 [Ashen Miracle] Oguri Cap Hopeful Stakes 625 1 7000 55 1.1495 0 2 0 0 10850.0 11067 217.0 2.0 11067 0
5 2026-08-19 20:37:27 33 3 [Ashen Miracle] Oguri Cap NHK Mile Cup 164 1 10500 55 1.6165 0 2 0 0 16275.0 16601 326.0 2.0031 16601 0
6 2026-08-19 20:37:27 46 3 [Ashen Miracle] Oguri Cap Mile Championship 78 1 11000 55 5.3966 5 6 3 0 17050.0 18585 1535.0 9.0029 18585 0
7 2026-08-19 20:37:27 48 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 55 8.8065 6 9 3 0 46500.0 52080 5580.0 12.0 52080 0
8 2026-08-19 20:37:27 54 3 [Ashen Miracle] Oguri Cap Osaka Hai 3 1 13500 55 3.3853 4 4 3 0 20925.0 22390 1465.0 7.0012 22390 0
9 2026-08-19 20:37:27 57 3 [Ashen Miracle] Oguri Cap Victoria Mile 5 1 10500 55 4.8613 2 6 2 0 16275.0 17577 1302.0 8.0 17577 0
10 2026-08-19 20:37:27 68 3 [Ashen Miracle] Oguri Cap Tenno Sho (Autumn) 76 1 15000 55 14.0515 6 9 3 0 23250.0 26040 2790.0 12.0 26040 0
11 2026-08-19 20:37:27 72 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 55 10.5066 7 9 5 0 46500.0 53010 6510.0 14.0 53010 0
12 2026-08-19 20:37:27 74 3 [Ashen Miracle] Oguri Cap URA Finale Qualifier 2015 1 7000 55 6.1557 6 9 3 0 10850.0 12152 1302.0 12.0 12152 0
13 2026-08-19 20:37:27 76 3 [Ashen Miracle] Oguri Cap URA Finale Semifinal 2112 1 10000 55 6.906 7 9 5 0 15500.0 17670 2170.0 14.0 17670 0
14 2026-08-19 20:37:27 78 3 [Ashen Miracle] Oguri Cap URA Finale Finals 2213 1 30000 55 7.8077 8 9 5 0 46500.0 53010 6510.0 14.0 53010 0
15 2026-09-08 21:05:53 12 3 [Ashen Miracle] Oguri Cap Junior Make Debut 1070 1 700 70 2.4747 0 2 0 0 1190.0 1213 23.0 1.9328 1213 0
16 2026-09-08 21:05:53 23 3 [Ashen Miracle] Oguri Cap Hanshin Juvenile Fillies 623 3 1625 70 2.5458 0 4 0 0 2762.5 2873 110.5 4.0 2873 0
17 2026-09-08 21:05:53 32 3 [Ashen Miracle] Oguri Cap Flora Stakes 173 1 5200 70 0.166 0 0 0 0 8840.0 8840 0.0 0.0 8840 0
18 2026-09-08 21:05:53 33 3 [Ashen Miracle] Oguri Cap NHK Mile Cup 164 1 10500 70 0.6636 0 0 0 0 17850.0 17850 0.0 0.0 17850 0
19 2026-09-08 21:05:53 46 3 [Ashen Miracle] Oguri Cap Mile Championship 78 1 11000 70 0.3634 0 0 0 0 18700.0 18700 0.0 0.0 18700 0
20 2026-09-08 21:05:53 48 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 3.9924 0 4 0 0 51000.0 53040 2040.0 4.0 53040 0
21 2026-09-08 21:05:53 54 3 [Ashen Miracle] Oguri Cap Osaka Hai 3 1 13500 70 3.7863 0 4 0 0 22950.0 23868 918.0 4.0 23868 0
22 2026-09-08 21:05:53 57 3 [Ashen Miracle] Oguri Cap Victoria Mile 5 1 10500 70 1.4385 0 2 0 0 17850.0 18207 357.0 2.0 18207 0
23 2026-09-08 21:05:53 68 3 [Ashen Miracle] Oguri Cap Tenno Sho (Autumn) 76 1 15000 70 3.9784 0 4 0 0 25500.0 26520 1020.0 4.0 26520 0
24 2026-09-08 21:05:53 72 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 7.8092 0 9 0 0 51000.0 55590 4590.0 9.0 55590 0
25 2026-09-08 21:05:53 74 3 [Ashen Miracle] Oguri Cap URA Finale Qualifier 2011 1 7000 70 4.2694 0 6 0 0 11900.0 12614 714.0 6.0 12614 0
26 2026-09-08 21:05:53 76 3 [Ashen Miracle] Oguri Cap URA Finale Semifinal 2108 1 10000 70 1.7312 0 2 0 0 17000.0 17340 340.0 2.0 17340 0
27 2026-09-08 21:05:53 78 3 [Ashen Miracle] Oguri Cap URA Finale Finals 2209 1 30000 70 2.9177 0 4 0 0 51000.0 53040 2040.0 4.0 53040 0
28 2026-09-16 13:24:41 12 3 [Ashen Miracle] Oguri Cap Junior Make Debut 1070 1 700 70 6.3564 0 9 0 0 1190.0 1297 107.0 8.9916 1297 0
29 2026-09-16 13:24:41 23 3 [Ashen Miracle] Oguri Cap Hanshin Juvenile Fillies 623 1 6500 70 0.5447 0 0 0 0 11050.0 11050 0.0 0.0 11050 0
30 2026-09-16 13:24:41 33 3 [Ashen Miracle] Oguri Cap NHK Mile Cup 164 1 10500 70 1.2208 0 2 0 0 17850.0 18207 357.0 2.0 18207 0
31 2026-09-16 13:24:41 46 3 [Ashen Miracle] Oguri Cap Mile Championship 78 1 11000 70 1.8764 0 2 0 0 18700.0 19074 374.0 2.0 19074 0
32 2026-09-16 13:24:41 48 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 7.2065 0 9 0 0 51000.0 55590 4590.0 9.0 55590 0
33 2026-09-16 13:24:41 54 3 [Ashen Miracle] Oguri Cap Osaka Hai 3 1 13500 70 9.1057 0 9 0 0 22950.0 25016 2066.0 9.0022 25016 0
34 2026-09-16 13:24:41 59 3 [Ashen Miracle] Oguri Cap Yasuda Kinen 73 1 13000 70 4.3705 0 6 0 0 22100.0 23426 1326.0 6.0 23426 0
35 2026-09-16 13:24:41 68 3 [Ashen Miracle] Oguri Cap Tenno Sho (Autumn) 76 1 15000 70 7.933 0 9 0 0 25500.0 27795 2295.0 9.0 27795 0
36 2026-09-16 13:24:41 72 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 5.7931 0 6 0 0 51000.0 54060 3060.0 6.0 54060 0
37 2026-09-16 13:24:41 74 3 [Ashen Miracle] Oguri Cap URA Finale Qualifier 2010 1 7000 70 5.7824 0 6 0 0 11900.0 12614 714.0 6.0 12614 0
38 2026-09-16 13:24:41 76 3 [Ashen Miracle] Oguri Cap URA Finale Semifinal 2112 1 10000 70 2.7856 0 4 0 0 17000.0 17680 680.0 4.0 17680 0
39 2026-09-16 13:24:41 78 3 [Ashen Miracle] Oguri Cap URA Finale Finals 2208 1 30000 70 5.4409 0 6 0 0 51000.0 54060 3060.0 6.0 54060 0
40 2026-09-17 20:23:11 12 3 [Ashen Miracle] Oguri Cap Junior Make Debut 1070 1 700 70 5.3797 0 6 0 0 1190.0 1261 71.0 5.9664 1261 0
41 2026-09-17 20:23:11 24 3 [Ashen Miracle] Oguri Cap Hopeful Stakes 625 1 7000 70 4.8411 2 6 2 0 11900.0 12852 952.0 8.0 12852 0
42 2026-09-17 20:23:11 33 3 [Ashen Miracle] Oguri Cap NHK Mile Cup 164 1 10500 70 3.0921 1 4 2 0 17850.0 18921 1071.0 6.0 18921 0
43 2026-09-17 20:23:11 46 3 [Ashen Miracle] Oguri Cap Mile Championship 78 1 11000 70 3.4861 1 4 2 0 18700.0 19822 1122.0 6.0 19822 0
44 2026-09-17 20:23:11 48 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 8.0058 1 9 2 0 51000.0 56610 5610.0 11.0 56610 0
45 2026-09-17 20:23:11 51 3 [Ashen Miracle] Oguri Cap Kawasaki Kinen 1106 1 6000 70 5.6972 1 6 2 0 10200.0 11016 816.0 8.0 11016 0
46 2026-09-17 20:23:11 52 3 [Ashen Miracle] Oguri Cap February Stakes 1 1 10000 70 0.5589 1 0 2 0 17000.0 17340 340.0 2.0 17340 0
47 2026-09-17 20:23:11 68 3 [Ashen Miracle] Oguri Cap Tenno Sho (Autumn) 76 1 15000 70 4.6895 2 6 2 0 25500.0 27540 2040.0 8.0 27540 0
48 2026-09-17 20:23:11 72 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 9.0895 1 9 2 0 51000.0 56610 5610.0 11.0 56610 0
49 2026-09-17 20:23:11 74 3 [Ashen Miracle] Oguri Cap URA Finale Qualifier 2014 1 7000 70 6.1828 1 9 2 0 11900.0 13209 1309.0 11.0 13209 0
50 2026-09-17 20:23:11 76 3 [Ashen Miracle] Oguri Cap URA Finale Semifinal 2111 1 10000 70 2.8136 1 4 2 0 17000.0 18020 1020.0 6.0 18020 0
51 2026-09-17 20:23:11 78 3 [Ashen Miracle] Oguri Cap URA Finale Finals 2212 1 30000 70 1.061 1 2 2 0 51000.0 53040 2040.0 4.0 53040 0
52 2026-09-17 22:45:31 12 3 [Ashen Miracle] Oguri Cap Junior Make Debut 1070 1 700 70 2.2776 1 2 2 0 1190.0 1237 47.0 3.9496 1237 0
53 2026-09-17 22:45:31 23 3 [Ashen Miracle] Oguri Cap Hanshin Juvenile Fillies 623 2 2600 70 0.0231 0 9 0 0 4420.0 4817 397.0 8.9819 4817 0
54 2026-09-17 22:45:31 33 3 [Ashen Miracle] Oguri Cap NHK Mile Cup 164 1 10500 70 2.7073 2 4 2 0 17850.0 18921 1071.0 6.0 18921 0
55 2026-09-17 22:45:31 46 3 [Ashen Miracle] Oguri Cap Mile Championship 78 1 11000 70 1.9154 2 2 2 0 18700.0 19448 748.0 4.0 19448 0
56 2026-09-17 22:45:31 48 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 4.8206 2 6 2 0 51000.0 55080 4080.0 8.0 55080 0
57 2026-09-17 22:45:31 56 3 [Ashen Miracle] Oguri Cap Tenno Sho (Spring) 4 1 15000 70 14.398 5 9 3 0 25500.0 28560 3060.0 12.0 28560 0
58 2026-09-17 22:45:31 57 3 [Ashen Miracle] Oguri Cap Victoria Mile 5 1 10500 70 2.1273 3 2 2 0 17850.0 18564 714.0 4.0 18564 0
59 2026-09-17 22:45:31 68 3 [Ashen Miracle] Oguri Cap Tenno Sho (Autumn) 76 1 15000 70 7.6906 5 9 3 0 25500.0 28560 3060.0 12.0 28560 0
60 2026-09-17 22:45:31 72 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 7.549 6 9 3 0 51000.0 57120 6120.0 12.0 57120 0
61 2026-09-17 22:45:31 74 3 [Ashen Miracle] Oguri Cap URA Finale Qualifier 2014 1 7000 70 6.4801 3 9 2 0 11900.0 13209 1309.0 11.0 13209 0
62 2026-09-17 22:45:31 76 3 [Ashen Miracle] Oguri Cap URA Finale Semifinal 2109 1 10000 70 5.9469 7 6 5 0 17000.0 18870 1870.0 11.0 18870 0
63 2026-09-17 22:45:31 78 3 [Ashen Miracle] Oguri Cap URA Finale Finals 2212 1 30000 70 7.2892 5 9 3 0 51000.0 57120 6120.0 12.0 57120 0
64 2026-09-19 20:01:05 12 3 [Ashen Miracle] Oguri Cap Junior Make Debut 1070 1 700 70 6.6525 0 9 0 0 1190.0 1297 107.0 8.9916 1297 0
65 2026-09-19 20:01:05 23 3 [Ashen Miracle] Oguri Cap Asahi Hai Futurity Stakes 624 3 1750 70 1.5022 0 6 0 0 2975.0 3153 178.0 5.9832 3153 0
66 2026-09-19 20:01:05 24 3 [Ashen Miracle] Oguri Cap Hopeful Stakes 625 1 7000 70 5.6931 0 6 0 0 11900.0 12614 714.0 6.0 12614 0
67 2026-09-19 20:01:05 33 3 [Ashen Miracle] Oguri Cap NHK Mile Cup 164 1 10500 70 1.488 0 2 0 0 17850.0 18207 357.0 2.0 18207 0
68 2026-09-19 20:01:05 46 3 [Ashen Miracle] Oguri Cap Mile Championship 78 1 11000 70 3.8134 0 4 0 0 18700.0 19448 748.0 4.0 19448 0
69 2026-09-19 20:01:05 48 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 2.6633 0 4 0 0 51000.0 53040 2040.0 4.0 53040 0
70 2026-09-19 20:01:05 52 3 [Ashen Miracle] Oguri Cap February Stakes 1 1 10000 70 0.6637 0 0 0 0 17000.0 17000 0.0 0.0 17000 0
71 2026-09-19 20:01:05 59 3 [Ashen Miracle] Oguri Cap Yasuda Kinen 73 1 13000 70 5.9364 0 6 0 0 22100.0 23426 1326.0 6.0 23426 0
72 2026-09-19 20:01:05 68 3 [Ashen Miracle] Oguri Cap Tenno Sho (Autumn) 76 1 15000 70 6.0564 0 9 0 0 25500.0 27795 2295.0 9.0 27795 0
73 2026-09-19 20:01:05 72 3 [Ashen Miracle] Oguri Cap Arima Kinen 81 1 30000 70 7.8703 0 9 0 0 51000.0 55590 4590.0 9.0 55590 0
74 2026-09-19 20:01:05 74 3 [Ashen Miracle] Oguri Cap URA Finale Qualifier 2015 1 7000 70 5.8192 0 6 0 0 11900.0 12614 714.0 6.0 12614 0
75 2026-09-19 20:01:05 76 3 [Ashen Miracle] Oguri Cap URA Finale Semifinal 2111 1 10000 70 5.2726 0 6 0 0 17000.0 18020 1020.0 6.0 18020 0
76 2026-09-19 20:01:05 78 3 [Ashen Miracle] Oguri Cap URA Finale Finals 2213 1 30000 70 5.6513 0 6 0 0 51000.0 54060 3060.0 6.0 54060 0
77 2026-09-19 21:45:59 12 2 [Blossom in Learning] Sakura Bakushin O Junior Make Debut 676 1 700 65 1.7631 0 2 0 0 1155.0 1178 23.0 1.9913 1178 0
78 2026-09-19 21:45:59 21 2 [Blossom in Learning] Sakura Bakushin O Keio Hai Junior Stakes 626 1 3800 65 0.917 0 2 0 0 6270.0 6395 125.0 1.9936 6395 0
79 2026-09-19 21:45:59 30 2 [Blossom in Learning] Sakura Bakushin O Spring Stakes 171 2 2160 65 0.7907 1 6 2 0 3564.0 3847 283.0 7.9405 3847 0
80 2026-09-19 21:45:59 34 2 [Blossom in Learning] Sakura Bakushin O Aoi Stakes 210 2 1520 65 1.5851 0 6 0 0 2508.0 2658 150.0 5.9809 2658 0
81 2026-09-19 21:45:59 35 2 [Blossom in Learning] Sakura Bakushin O Epsom Cup 95 1 4100 65 1.3364 1 2 2 0 6765.0 7035 270.0 3.9911 7035 0
82 2026-09-19 21:45:59 42 2 [Blossom in Learning] Sakura Bakushin O Sprinters Stakes 75 3 3250 65 0.8705 0 6 0 0 5362.5 5684 321.5 5.9953 5684 0
83 2026-09-19 21:45:59 54 2 [Blossom in Learning] Sakura Bakushin O Takamatsunomiya Kinen 2 2 5200 65 0.451 0 9 0 0 8580.0 9352 772.0 8.9977 9352 0
84 2026-09-19 21:45:59 60 2 [Blossom in Learning] Sakura Bakushin O Hakodate Sprint Stakes 96 1 3900 65 0.9971 0 2 0 0 6435.0 6563 128.0 1.9891 6563 0
85 2026-09-19 21:45:59 61 2 [Blossom in Learning] Sakura Bakushin O CBC Sho 97 1 3900 65 2.3483 0 2 0 0 6435.0 6563 128.0 1.9891 6563 0
86 2026-09-19 21:45:59 65 2 [Blossom in Learning] Sakura Bakushin O Centaur Stakes 83 1 5900 65 1.5096 0 2 0 0 9735.0 9929 194.0 1.9928 9929 0
87 2026-09-19 21:45:59 66 2 [Blossom in Learning] Sakura Bakushin O Sprinters Stakes 75 6 1170 65 2.5098 1 0 2 0 1930.5 1968 37.5 1.9425 1968 0
88 2026-09-23 23:41:07 12 1 [Blossom in Learning] Sakura Bakushin O Junior Make Debut 676 1 700 65 2.7319 0 4 0 0 1155.0 1201 46.0 3.9827 1201 0
89 2026-09-23 23:41:07 21 1 [Blossom in Learning] Sakura Bakushin O Keio Hai Junior Stakes 626 1 3800 65 0.5164 0 0 0 0 6270.0 6270 0.0 0.0 6270 0
90 2026-09-23 23:41:07 30 1 [Blossom in Learning] Sakura Bakushin O Spring Stakes 171 1 5400 65 2.4521 1 2 2 0 8910.0 9266 356.0 3.9955 9266 0
91 2026-09-23 23:41:07 34 1 [Blossom in Learning] Sakura Bakushin O Aoi Stakes 210 1 3800 65 0.4297 0 0 0 0 6270.0 6270 0.0 0.0 6270 0
92 2026-09-23 23:41:07 42 1 [Blossom in Learning] Sakura Bakushin O Sprinters Stakes 75 6 1170 65 3.8513 0 0 0 0 1930.5 1930 -0.5 -0.0259 1930 0
93 2026-09-28 14:22:08 12 1 [Hot☆Summer Night] Maruzensky Junior Make Debut 676 1 700 65 0.2547 0 0 0 0 1155.0 1155 0.0 0.0 1155 0
94 2026-10-01 09:39:24 12 2 [Wild Frontier] Taiki Shuttle Junior Make Debut 846 1 700 80 2.9274 1 4 2 0 1260.0 1335 75.0 5.9524 1335 0
95 2026-10-01 09:39:24 25 2 [Wild Frontier] Taiki Shuttle Shinzan Kinen 178 1 3800 80 3.297 0 4 0 0 6840.0 7113 273.0 3.9912 7113 0
96 2026-10-01 09:39:24 33 2 [Wild Frontier] Taiki Shuttle NHK Mile Cup 164 1 10500 80 1.5041 0 2 0 0 18900.0 19278 378.0 2.0 19278 0
97 2026-10-01 09:39:24 36 2 [Wild Frontier] Taiki Shuttle Unicorn Stakes 189 1 3500 80 0.0829 0 0 0 0 6300.0 6300 0.0 0.0 6300 0
98 2026-10-01 09:39:24 46 2 [Wild Frontier] Taiki Shuttle Mile Championship 78 1 11000 80 1.3511 1 2 2 0 19800.0 20592 792.0 4.0 20592 0
99 2026-10-01 09:39:24 59 2 [Wild Frontier] Taiki Shuttle Yasuda Kinen 73 1 13000 80 4.4812 0 6 0 0 23400.0 24804 1404.0 6.0 24804 0
100 2026-10-01 09:39:24 66 2 [Wild Frontier] Taiki Shuttle Sprinters Stakes 75 1 13000 80 3.6966 1 4 2 0 23400.0 24804 1404.0 6.0 24804 0
101 2026-10-01 09:39:24 70 2 [Wild Frontier] Taiki Shuttle Mile Championship 78 1 11000 80 3.6774 0 4 0 0 19800.0 20592 792.0 4.0 20592 0
102 2026-10-01 09:39:24 74 2 [Wild Frontier] Taiki Shuttle URA Finale Qualifier 2013 1 7000 80 4.0671 0 6 0 0 12600.0 13356 756.0 6.0 13356 0
103 2026-10-01 09:39:24 76 2 [Wild Frontier] Taiki Shuttle URA Finale Semifinal 2109 1 10000 80 4.9632 1 6 2 0 18000.0 19440 1440.0 8.0 19440 0
104 2026-10-01 09:39:24 78 2 [Wild Frontier] Taiki Shuttle URA Finale Finals 2214 1 30000 80 1.1329 0 2 0 0 54000.0 55080 1080.0 2.0 55080 0
105 2026-10-04 14:36:48 12 4 [Wild Frontier] Taiki Shuttle Junior Make Debut 846 1 700 105 2.6127 1 4 2 0 1435.0 1521 86.0 5.993 1521 0
106 2026-10-04 14:36:48 14 4 [Wild Frontier] Taiki Shuttle Hakodate Junior Stakes 628 1 3100 105 4.3676 0 6 0 0 6355.0 6736 381.0 5.9953 6736 0
107 2026-10-04 14:36:48 16 4 [Wild Frontier] Taiki Shuttle Niigata Junior Stakes 629 1 3100 105 2.8947 0 4 0 0 6355.0 6609 254.0 3.9969 6609 0
108 2026-10-04 14:36:48 17 4 [Wild Frontier] Taiki Shuttle Sapporo Junior Stakes 630 1 3100 105 2.2675 1 2 2 0 6355.0 6609 254.0 3.9969 6609 0
109 2026-10-04 14:36:48 21 4 [Wild Frontier] Taiki Shuttle Keio Hai Junior Stakes 626 1 3800 105 1.8215 1 2 2 0 7790.0 8101 311.0 3.9923 8101 0
110 2026-10-04 14:36:48 22 4 [Wild Frontier] Taiki Shuttle Tokyo Sports Hai Junior Stakes 635 1 3300 105 4.1972 0 6 0 0 6765.0 7170 405.0 5.9867 7170 0
111 2026-10-04 14:36:48 23 4 [Wild Frontier] Taiki Shuttle Hanshin Juvenile Fillies 623 1 6500 105 4.4133 1 6 2 0 13325.0 14391 1066.0 8.0 14391 0
112 2026-10-04 14:36:48 24 4 [Wild Frontier] Taiki Shuttle Zen-Nippon Junior Yushun 1107 2 1680 105 0.6647 1 9 2 0 3444.0 3821 377.0 10.9466 3821 0
113 2026-10-04 14:36:48 27 4 [Wild Frontier] Taiki Shuttle Kyodo News Hai 183 1 3800 105 3.9819 1 4 2 0 7790.0 8257 467.0 5.9949 8257 0
114 2026-10-04 14:36:48 29 4 [Wild Frontier] Taiki Shuttle Fillies' Revue 170 2 2080 105 0.056 1 9 2 0 4264.0 4731 467.0 10.9522 4731 0
115 2026-10-04 14:36:48 30 4 [Wild Frontier] Taiki Shuttle Spring Stakes 171 1 5400 105 1.5715 1 2 2 0 11070.0 11513 443.0 4.0018 11513 0
116 2026-10-04 14:36:48 31 4 [Wild Frontier] Taiki Shuttle Oka Sho 162 1 10500 105 2.0067 0 2 0 0 21525.0 21956 431.0 2.0023 21956 0
117 2026-10-04 14:36:48 33 4 [Wild Frontier] Taiki Shuttle NHK Mile Cup 164 1 10500 105 1.3928 0 2 0 0 21525.0 21956 431.0 2.0023 21956 0
118 2026-10-04 14:36:48 35 4 [Wild Frontier] Taiki Shuttle Yasuda Kinen 73 1 13000 105 1.2662 1 2 2 0 26650.0 27716 1066.0 4.0 27716 0
119 2026-10-04 14:36:48 39 4 [Wild Frontier] Taiki Shuttle Sekiya Kinen 105 1 3900 105 3.8619 0 4 0 0 7995.0 8314 319.0 3.99 8314 0
120 2026-10-04 14:36:48 41 4 [Wild Frontier] Taiki Shuttle Rose Stakes 176 1 5200 105 3.3191 1 4 2 0 10660.0 11300 640.0 6.0038 11300 0
121 2026-10-04 14:36:48 42 4 [Wild Frontier] Taiki Shuttle Sprinters Stakes 75 1 13000 105 3.2007 1 4 2 0 26650.0 28249 1599.0 6.0 28249 0
122 2026-10-04 14:36:48 43 4 [Wild Frontier] Taiki Shuttle M.C. Nambu Hai 1109 1 6000 105 2.7546 0 4 0 0 12300.0 12792 492.0 4.0 12792 0
123 2026-10-04 14:36:48 45 4 [Wild Frontier] Taiki Shuttle JBC Sprint 1111 1 6000 105 1.5294 1 2 2 0 12300.0 12792 492.0 4.0 12792 0
124 2026-10-04 14:36:48 46 4 [Wild Frontier] Taiki Shuttle Mile Championship 78 1 11000 105 2.2484 0 2 0 0 22550.0 23001 451.0 2.0 23001 0
125 2026-10-04 14:36:48 47 4 [Wild Frontier] Taiki Shuttle Champions Cup 80 1 10000 105 2.5859 0 4 0 0 20500.0 21320 820.0 4.0 21320 0
126 2026-10-04 14:36:48 50 4 [Wild Frontier] Taiki Shuttle Negishi Stakes 22 2 1520 105 1.4117 0 6 0 0 3116.0 3302 186.0 5.9692 3302 0
127 2026-10-04 14:36:48 52 4 [Wild Frontier] Taiki Shuttle February Stakes 1 1 10000 105 0.0968 0 0 0 0 20500.0 20500 0.0 0.0 20500 0
128 2026-10-04 14:36:48 54 4 [Wild Frontier] Taiki Shuttle Takamatsunomiya Kinen 2 1 13000 105 3.2402 0 4 0 0 26650.0 27716 1066.0 4.0 27716 0
129 2026-10-04 14:36:48 55 4 [Wild Frontier] Taiki Shuttle Marine Cup 1128 1 2500 105 2.6992 0 4 0 0 5125.0 5330 205.0 4.0 5330 0
130 2026-10-04 14:36:48 57 4 [Wild Frontier] Taiki Shuttle Victoria Mile 5 1 10500 105 1.5024 0 2 0 0 21525.0 21956 431.0 2.0023 21956 0
131 2026-10-04 14:36:48 58 4 [Wild Frontier] Taiki Shuttle Keyaki Stakes 71 1 2200 105 3.8244 1 4 2 0 4510.0 4780 270.0 5.9867 4780 0
132 2026-10-04 14:36:48 59 4 [Wild Frontier] Taiki Shuttle Yasuda Kinen 73 1 13000 105 1.4361 1 2 2 0 26650.0 27716 1066.0 4.0 27716 0
133 2026-10-04 14:36:48 62 4 [Wild Frontier] Taiki Shuttle Queen Stakes 103 1 3600 105 5.0515 0 6 0 0 7380.0 7822 442.0 5.9892 7822 0
134 2026-10-04 14:36:48 65 4 [Wild Frontier] Taiki Shuttle Keisei Hai Autumn Handicap 110 1 3900 105 5.633 0 6 0 0 7995.0 8474 479.0 5.9912 8474 0
135 2026-10-04 14:36:48 66 4 [Wild Frontier] Taiki Shuttle Sprinters Stakes 75 1 13000 105 2.6328 0 4 0 0 26650.0 27716 1066.0 4.0 27716 0
136 2026-10-04 14:36:48 67 4 [Wild Frontier] Taiki Shuttle M.C. Nambu Hai 1109 1 6000 105 4.184 0 6 0 0 12300.0 13038 738.0 6.0 13038 0
137 2026-10-04 14:36:48 69 4 [Wild Frontier] Taiki Shuttle JBC Sprint 1025 1 6000 105 2.1263 1 2 2 0 12300.0 12792 492.0 4.0 12792 0
138 2026-10-04 14:36:48 70 4 [Wild Frontier] Taiki Shuttle Mile Championship 78 1 11000 105 2.1798 0 2 0 0 22550.0 23001 451.0 2.0 23001 0
139 2026-10-04 14:36:48 71 4 [Wild Frontier] Taiki Shuttle Champions Cup 80 1 10000 105 0.8076 1 2 2 0 20500.0 21320 820.0 4.0 21320 0
140 2026-10-04 14:36:48 74 4 [Wild Frontier] Taiki Shuttle Twinkle Star Climax Race 1 2313 1 7000 105 4.0466 1 6 2 0 14350.0 15498 1148.0 8.0 15498 0
141 2026-10-04 14:36:48 76 4 [Wild Frontier] Taiki Shuttle Twinkle Star Climax Race 2 2410 1 10000 105 1.8345 1 2 2 0 20500.0 21320 820.0 4.0 21320 0
142 2026-10-04 14:36:48 78 4 [Wild Frontier] Taiki Shuttle Twinkle Star Climax Race 3 2515 1 30000 105 5.1406 1 6 2 50 61500.0 99630 38130.0 62.0 99630 0
143 2026-10-08 10:00:27 12 3 [CODE: ICING] Mihono Bourbon Junior Make Debut 1075 1 700 70 3.126 1 4 2 0 1190.0 1261 71.0 5.9664 1261 0
144 2026-10-08 10:00:27 23 3 [CODE: ICING] Mihono Bourbon Asahi Hai Futurity Stakes 624 1 7000 70 3.0099 2 4 2 0 11900.0 12614 714.0 6.0 12614 0
145 2026-10-08 10:00:27 30 3 [CODE: ICING] Mihono Bourbon Spring Stakes 171 1 5400 70 1.4594 1 2 2 0 9180.0 9547 367.0 3.9978 9547 0
146 2026-10-08 10:00:27 31 3 [CODE: ICING] Mihono Bourbon Satsuki Sho 163 1 11000 70 6.1843 2 9 2 0 18700.0 20757 2057.0 11.0 20757 0
147 2026-10-08 10:00:27 34 3 [CODE: ICING] Mihono Bourbon Tokyo Yushun (Japanese Derby) 166 1 20000 70 4.682 2 6 2 0 34000.0 36720 2720.0 8.0 36720 0
148 2026-10-08 10:00:27 44 3 [CODE: ICING] Mihono Bourbon Kikuka Sho 168 1 12000 70 3.5273 1 4 2 0 20400.0 21624 1224.0 6.0 21624 0
149 2026-10-08 10:00:27 56 3 [CODE: ICING] Mihono Bourbon Tenno Sho (Spring) 4 1 15000 70 10.0374 2 9 2 0 25500.0 28305 2805.0 11.0 28305 0
150 2026-10-08 10:00:27 70 3 [CODE: ICING] Mihono Bourbon Japan Cup 79 1 30000 70 10.1623 2 9 2 0 51000.0 56610 5610.0 11.0 56610 0
151 2026-10-08 10:00:27 72 3 [CODE: ICING] Mihono Bourbon Arima Kinen 81 1 30000 70 10.4669 3 9 2 0 51000.0 56610 5610.0 11.0 56610 0
152 2026-10-08 10:00:27 74 3 [CODE: ICING] Mihono Bourbon URA Finale Qualifier 2010 1 7000 70 5.7274 3 6 2 0 11900.0 12852 952.0 8.0 12852 0
153 2026-10-08 10:00:27 76 3 [CODE: ICING] Mihono Bourbon URA Finale Semifinal 2113 1 10000 70 3.7223 3 4 2 0 17000.0 18020 1020.0 6.0 18020 0
154 2026-10-08 10:00:27 78 3 [CODE: ICING] Mihono Bourbon URA Finale Finals 2209 1 30000 70 3.873 3 4 2 0 51000.0 54060 3060.0 6.0 54060 0
155 2026-10-10 03:12:29 12 3 [Soirée des Chatons] Seiun Sky Junior Make Debut 689 1 700 70 3.0303 1 4 2 0 1190.0 1261 71.0 5.9664 1261 0
156 2026-10-10 03:12:29 24 3 [Soirée des Chatons] Seiun Sky Hopeful Stakes 625 1 7000 70 2.1576 1 2 2 0 11900.0 12376 476.0 4.0 12376 0
157 2026-10-10 03:12:29 31 3 [Soirée des Chatons] Seiun Sky Satsuki Sho 163 1 11000 70 2.5681 2 4 2 0 18700.0 19822 1122.0 6.0 19822 0
158 2026-10-10 03:12:29 34 3 [Soirée des Chatons] Seiun Sky Tokyo Yushun (Japanese Derby) 166 1 20000 70 2.4678 2 2 2 0 34000.0 35360 1360.0 4.0 35360 0
159 2026-10-10 03:12:29 44 3 [Soirée des Chatons] Seiun Sky Kikuka Sho 168 1 12000 70 0.6938 2 0 2 0 20400.0 20808 408.0 2.0 20808 0
160 2026-10-10 03:12:29 48 3 [Soirée des Chatons] Seiun Sky Arima Kinen 81 1 30000 70 3.3828 2 4 2 0 51000.0 54060 3060.0 6.0 54060 0
161 2026-10-10 03:12:29 56 3 [Soirée des Chatons] Seiun Sky Tenno Sho (Spring) 4 1 15000 70 20.5803 10 9 8 0 25500.0 29835 4335.0 17.0 29835 0
162 2026-10-10 03:12:29 60 3 [Soirée des Chatons] Seiun Sky Takarazuka Kinen 74 1 15000 70 6.4095 10 9 8 0 25500.0 29835 4335.0 17.0 29835 0
163 2026-10-10 03:12:29 68 3 [Soirée des Chatons] Seiun Sky Tenno Sho (Autumn) 76 1 15000 70 9.3985 9 9 5 0 25500.0 29070 3570.0 14.0 29070 0
164 2026-10-10 03:12:29 72 3 [Soirée des Chatons] Seiun Sky Arima Kinen 81 1 30000 70 11.7606 10 9 8 0 51000.0 59670 8670.0 17.0 59670 0
165 2026-10-10 03:12:29 74 3 [Soirée des Chatons] Seiun Sky URA Finale Qualifier 2018 1 7000 70 11.2054 9 9 5 0 11900.0 13566 1666.0 14.0 13566 0
166 2026-10-10 03:12:29 76 3 [Soirée des Chatons] Seiun Sky URA Finale Semifinal 2123 1 10000 70 8.0601 7 9 5 0 17000.0 19380 2380.0 14.0 19380 0
167 2026-10-10 03:12:29 78 3 [Soirée des Chatons] Seiun Sky URA Finale Finals 2222 1 30000 70 5.7947 8 6 5 0 51000.0 56610 5610.0 11.0 56610 0
168 2026-10-10 15:53:30 12 3 [Soirée des Chatons] Seiun Sky Junior Make Debut 689 1 700 70 3.7946 2 4 2 0 1190.0 1261 71.0 5.9664 1261 0
169 2026-10-10 15:53:30 24 3 [Soirée des Chatons] Seiun Sky Hopeful Stakes 625 1 7000 70 5.3394 1 6 2 0 11900.0 12852 952.0 8.0 12852 0
170 2026-10-10 15:53:30 31 3 [Soirée des Chatons] Seiun Sky Satsuki Sho 163 1 11000 70 7.5947 2 9 2 0 18700.0 20757 2057.0 11.0 20757 0
171 2026-10-10 15:53:30 34 3 [Soirée des Chatons] Seiun Sky Tokyo Yushun (Japanese Derby) 166 1 20000 70 2.1677 4 2 3 0 34000.0 35700 1700.0 5.0 35700 0
172 2026-10-10 15:53:30 44 3 [Soirée des Chatons] Seiun Sky Kikuka Sho 168 3 3000 70 3.8191 4 4 3 0 5100.0 5457 357.0 7.0 5457 0
173 2026-10-10 15:53:30 48 3 [Soirée des Chatons] Seiun Sky Arima Kinen 81 1 30000 70 12.6422 3 9 2 0 51000.0 56610 5610.0 11.0 56610 0
174 2026-10-10 15:53:30 56 3 [Soirée des Chatons] Seiun Sky Tenno Sho (Spring) 4 1 15000 70 1.11 7 2 5 0 25500.0 27285 1785.0 7.0 27285 0
175 2026-10-10 15:53:30 60 3 [Soirée des Chatons] Seiun Sky Takarazuka Kinen 74 1 15000 70 13.7279 7 9 5 0 25500.0 29070 3570.0 14.0 29070 0
176 2026-10-10 15:53:30 68 3 [Soirée des Chatons] Seiun Sky Tenno Sho (Autumn) 76 1 15000 70 3.6698 6 4 3 0 25500.0 27285 1785.0 7.0 27285 0
177 2026-10-10 15:53:30 72 3 [Soirée des Chatons] Seiun Sky Arima Kinen 81 1 30000 70 13.5919 8 9 5 0 51000.0 58140 7140.0 14.0 58140 0
178 2026-10-10 15:53:30 74 3 [Soirée des Chatons] Seiun Sky URA Finale Qualifier 2025 1 7000 70 13.045 7 9 5 0 11900.0 13566 1666.0 14.0 13566 0
179 2026-10-10 15:53:30 76 3 [Soirée des Chatons] Seiun Sky URA Finale Semifinal 2122 1 10000 70 12.4002 7 9 5 0 17000.0 19380 2380.0 14.0 19380 0
180 2026-10-10 15:53:30 78 3 [Soirée des Chatons] Seiun Sky URA Finale Finals 2217 1 30000 70 12.3247 7 9 5 0 51000.0 58140 7140.0 14.0 58140 0
181 2026-10-11 18:47:03 12 3 [Soirée des Chatons] Seiun Sky Junior Make Debut 689 1 700 80 3.2149 2 4 2 0 1260.0 1335 75.0 5.9524 1335 0
182 2026-10-11 18:47:03 24 3 [Soirée des Chatons] Seiun Sky Hopeful Stakes 625 1 7000 80 2.4816 2 2 2 0 12600.0 13104 504.0 4.0 13104 0
183 2026-10-11 19:13:35 12 3 [Soirée des Chatons] Seiun Sky Junior Make Debut 689 1 700 80 4.1512 1 6 2 0 1260.0 1360 100.0 7.9365 1360 0
184 2026-10-11 19:13:35 24 3 [Soirée des Chatons] Seiun Sky Hopeful Stakes 625 1 7000 80 2.8651 2 4 2 0 12600.0 13356 756.0 6.0 13356 0
185 2026-10-11 19:13:35 31 3 [Soirée des Chatons] Seiun Sky Satsuki Sho 163 1 11000 80 4.6012 2 6 2 0 19800.0 21384 1584.0 8.0 21384 0
186 2026-10-11 19:13:35 34 3 [Soirée des Chatons] Seiun Sky Tokyo Yushun (Japanese Derby) 166 1 20000 80 4.1483 2 6 2 0 36000.0 38880 2880.0 8.0 38880 0
187 2026-10-11 19:13:35 44 3 [Soirée des Chatons] Seiun Sky Kikuka Sho 168 2 4800 80 4.218 2 2 2 0 8640.0 8985 345.0 3.9931 8985 0
188 2026-10-11 19:13:35 48 3 [Soirée des Chatons] Seiun Sky Arima Kinen 81 1 30000 80 10.0709 3 9 2 0 54000.0 59940 5940.0 11.0 59940 0
189 2026-10-11 19:13:35 56 3 [Soirée des Chatons] Seiun Sky Tenno Sho (Spring) 4 1 15000 80 6.5113 8 9 5 0 27000.0 30780 3780.0 14.0 30780 0
190 2026-10-11 19:13:35 60 3 [Soirée des Chatons] Seiun Sky Takarazuka Kinen 74 1 15000 80 6.7601 6 9 3 0 27000.0 30240 3240.0 12.0 30240 0
191 2026-10-11 19:13:35 72 3 [Soirée des Chatons] Seiun Sky Arima Kinen 81 1 30000 80 14.0565 7 9 5 0 54000.0 61560 7560.0 14.0 61560 0
192 2026-10-11 19:13:35 74 3 [Soirée des Chatons] Seiun Sky URA Finale Qualifier 2018 1 7000 80 10.2344 7 9 5 0 12600.0 14364 1764.0 14.0 14364 0
193 2026-10-11 19:13:35 76 3 [Soirée des Chatons] Seiun Sky URA Finale Semifinal 2121 1 10000 80 5.3797 5 6 3 0 18000.0 19620 1620.0 9.0 19620 0
194 2026-10-11 19:13:35 78 3 [Soirée des Chatons] Seiun Sky URA Finale Finals 2222 1 30000 80 7.7281 7 9 5 0 54000.0 61560 7560.0 14.0 61560 0