Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04cb6388c3 | ||
|
|
de2b871862 | ||
|
|
c95bffbcb8 | ||
|
|
04be0b75a9 | ||
|
|
5a2b9b7bf4 | ||
|
|
2d935b87ec | ||
|
|
f623388291 | ||
|
|
74185d92d4 | ||
|
|
7f1aa00be6 | ||
|
|
16a5433594 | ||
|
|
75558f35ba | ||
|
|
cf00bb54c5 | ||
|
|
cdda88181f | ||
|
|
57a967978b | ||
|
|
939012b511 | ||
|
|
888a6dbf3c | ||
|
|
fb27817038 | ||
|
|
f27b9dcaa5 | ||
|
|
645abb2f66 | ||
|
|
cf301745f1 | ||
|
|
3a39794312 | ||
|
|
381df5bd35 | ||
|
|
e5ebd1894c | ||
|
|
c43f6b8292 | ||
|
|
cccf0cc838 | ||
|
|
64f82136cb | ||
|
|
53c7079f46 | ||
|
|
1cfabb18f5 | ||
|
|
bf4e750825 | ||
|
|
57ffadb06a |
+455
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
+3
-4
@@ -139,9 +139,8 @@ 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,
|
||||
Start: evt.Distance,
|
||||
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.
|
||||
|
||||
+20
-66
@@ -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{},
|
||||
@@ -64,10 +64,9 @@ func TestRacecourses(t *testing.T) {
|
||||
LaneStart: [18]horse.TenThousandths{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5333, 5888, 6444, 7000, 7555, 8111, 8666, 9222, 9777},
|
||||
Spans: []horse.RacecourseSpan{
|
||||
{
|
||||
Start: 0,
|
||||
End: 458,
|
||||
Straight: true,
|
||||
FarStraight: true,
|
||||
Start: 0,
|
||||
End: 458,
|
||||
Straight: 2,
|
||||
},
|
||||
{
|
||||
Start: 458,
|
||||
@@ -82,7 +81,7 @@ func TestRacecourses(t *testing.T) {
|
||||
{
|
||||
Start: 958,
|
||||
End: 1450,
|
||||
Straight: true,
|
||||
Straight: 1,
|
||||
},
|
||||
{
|
||||
Start: 1450,
|
||||
@@ -95,10 +94,9 @@ func TestRacecourses(t *testing.T) {
|
||||
Corner: 2,
|
||||
},
|
||||
{
|
||||
Start: 1850,
|
||||
End: 2300,
|
||||
Straight: true,
|
||||
FarStraight: true,
|
||||
Start: 1850,
|
||||
End: 2300,
|
||||
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,
|
||||
@@ -179,10 +177,9 @@ func TestRacecourses(t *testing.T) {
|
||||
Corner: 2,
|
||||
},
|
||||
{
|
||||
Start: 800,
|
||||
End: 1300,
|
||||
Straight: true,
|
||||
FarStraight: true,
|
||||
Start: 800,
|
||||
End: 1300,
|
||||
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,
|
||||
@@ -278,10 +233,9 @@ func TestRacecourses(t *testing.T) {
|
||||
Corner: 2,
|
||||
},
|
||||
{
|
||||
Start: 900,
|
||||
End: 1300,
|
||||
Straight: true,
|
||||
FarStraight: true,
|
||||
Start: 900,
|
||||
End: 1300,
|
||||
Straight: 2,
|
||||
},
|
||||
{
|
||||
Start: 1300,
|
||||
@@ -296,7 +250,7 @@ func TestRacecourses(t *testing.T) {
|
||||
{
|
||||
Start: 1700,
|
||||
End: 2000,
|
||||
Straight: true,
|
||||
Straight: 1,
|
||||
},
|
||||
},
|
||||
Slopes: []horse.Slope{
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Vendored
+61
@@ -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);
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
+109
-9
@@ -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,20 +228,116 @@ 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"`
|
||||
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"`
|
||||
RewardSummary RewardSummary `json:"reward_summary_info"`
|
||||
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 *umadump.Never `json:"resume_factor_select"`
|
||||
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 {
|
||||
Chara SingleModeChara `json:"chara_info"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
DressID int32 `json:"dress_id"`
|
||||
Chara SingleModeChara `json:"chara_info"`
|
||||
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 {
|
||||
@@ -246,4 +345,5 @@ type WorkIdleSingleMode struct {
|
||||
ProgressLog IdleSingleModeProgressLog `json:"progress_log_info"`
|
||||
End End `json:"end_info"`
|
||||
OldChara *SingleModeChara `json:"chara_info"`
|
||||
Extra map[string]any `json:"extra_data"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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) + ")"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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]]
|
||||
}
|
||||
@@ -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
@@ -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]]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+24
-336
File diff suppressed because it is too large
Load Diff
+23
-23
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
const Entry = z.strictObject({
|
||||
home_story_trigger_id: z.int32(),
|
||||
new_flag: z.literal(0).or(z.literal(1)),
|
||||
});
|
||||
|
||||
export type Entry = z.infer<typeof Entry>;
|
||||
|
||||
export const Gallery = z.object({
|
||||
talk_gallery_list: z.array(Entry),
|
||||
});
|
||||
|
||||
export type Gallery = z.infer<typeof Gallery>;
|
||||
@@ -9,25 +9,51 @@
|
||||
requiredCharas,
|
||||
convoCharas,
|
||||
} from '$lib/data/convo';
|
||||
import * as talk from '$lib/umadump/talk';
|
||||
import CharaPick from '$lib/CharaPick.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import CharaIcon from '$lib/CharaIcon.svelte';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
let conversations: Conversation[] = $state([]);
|
||||
const convoMap = $derived(byChara(conversations));
|
||||
const popular = $derived(groupPopulars(conversations));
|
||||
let allConvos: Conversation[] = $state([]);
|
||||
const popular = $derived(groupPopulars(allConvos));
|
||||
let characters: Character[] = $state([]);
|
||||
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
|
||||
const names = $derived(charaNames(characters));
|
||||
const minSuggest = 8;
|
||||
|
||||
onMount(async () => {
|
||||
[conversations, characters] = await Promise.all([conversation(), character()]);
|
||||
[allConvos, characters] = await Promise.all([conversation(), character()]);
|
||||
});
|
||||
|
||||
let chara: Character | undefined = $state();
|
||||
let convo = $state(1);
|
||||
|
||||
let talkFiles: FileList | undefined = $state();
|
||||
let readIDs = new SvelteSet<number>();
|
||||
let talkFileInvalid = $state(false);
|
||||
async function setRead(f: FileList) {
|
||||
talkFiles = f;
|
||||
readIDs.clear();
|
||||
talkFileInvalid = false;
|
||||
if (talkFiles == null || talkFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
const t = await talkFiles[0].text();
|
||||
const d = JSON.parse(t);
|
||||
const g = talk.Gallery.safeDecode(d);
|
||||
if (!g.success) {
|
||||
talkFileInvalid = true;
|
||||
console.warn(g.error);
|
||||
return;
|
||||
}
|
||||
for (const e of g.data.talk_gallery_list) {
|
||||
readIDs.add(e.home_story_trigger_id);
|
||||
}
|
||||
}
|
||||
const conversations = $derived(allConvos.filter((c) => !readIDs.has(c.id)));
|
||||
const convoMap = $derived(byChara(conversations));
|
||||
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
|
||||
|
||||
const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
|
||||
const cur = $derived(options.find((c) => c.number === convo));
|
||||
const charaIDs = $derived(cur != null ? convoCharas(cur) : []);
|
||||
@@ -47,12 +73,12 @@
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Lobby Conversations</h1>
|
||||
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row">
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<div class="mx-auto mt-8 grid grid-cols-1 gap-4 rounded-md p-4 text-center shadow-md ring md:max-w-xl md:grid-cols-6">
|
||||
<div class="md:col-span-3">
|
||||
<label for="chara" class="hidden md:inline">Character</label>
|
||||
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
|
||||
</div>
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<div class="md:col-span-3">
|
||||
<label for="convo" class="hidden md:inline">Conversation</label>
|
||||
<select id="convo" bind:value={convo} class="w-full">
|
||||
{#each options as opt (opt.number)}
|
||||
@@ -60,6 +86,29 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label for="talkgallery" class="block">
|
||||
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"
|
||||
class="rounded-full p-4"
|
||||
accept="application/json"
|
||||
id="talkgallery"
|
||||
bind:files={() => talkFiles ?? new DataTransfer().files, setRead}
|
||||
/>
|
||||
<button
|
||||
class="ml-4 rounded-full bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
|
||||
onclick={() => setRead(new DataTransfer().files)}>Reset</button
|
||||
>
|
||||
</div>
|
||||
{#if talkFileInvalid}
|
||||
<div class="col-span-full">Invalid File</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if cur}
|
||||
<div class="shadow-sm transition-shadow hover:shadow-md">
|
||||
|
||||
@@ -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×1.88</span> = 1342.32, rounded to five
|
||||
significant figures becomes 1342.3, rounded down to 1342.
|
||||
</li>
|
||||
<li>
|
||||
10500 base plus 210 from margin with 105% fan bonus =
|
||||
<span class="inline-block">10710×2.05</span>
|
||||
= 21955.5, rounded to five significant figures becomes 21956, rounded down to 21956.
|
||||
</li>
|
||||
<li>
|
||||
100 base fans plus 2 fans from skills with 88% fan bonus = <span class="inline-block">102×1.88</span> = 191.76, rounded
|
||||
to five significant figures becomes 191.76, rounded down to 191.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>(Trackblazer/MANT) If glowsticks were used for the race, apply their multiplier and round down.</li>
|
||||
</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>
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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)}
|
||||
>
|
||||
↑
|
||||
</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)}
|
||||
>
|
||||
×
|
||||
</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)}
|
||||
>
|
||||
↓
|
||||
</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"
|
||||
>
|
||||
|
||||
</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 & accumulatetime>=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"> </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"
|
||||
>
|
||||
|
||||
</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"
|
||||
>
|
||||
|
||||
</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} />
|
||||
@@ -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
|
||||
|
Reference in New Issue
Block a user