diff --git a/cmd/horsegen/generate.go b/cmd/horsegen/generate.go index 1519b3d..c0b6ce6 100644 --- a/cmd/horsegen/generate.go +++ b/cmd/horsegen/generate.go @@ -2,44 +2,38 @@ package main import ( "bufio" - "cmp" "context" _ "embed" "encoding/json" "errors" "flag" - "fmt" "log/slog" - "maps" "os" "os/signal" "path/filepath" - "slices" - "strconv" - "strings" "golang.org/x/sync/errgroup" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" - "git.sunturtle.xyz/zephyr/horse" + "git.sunturtle.xyz/zephyr/horse/mdb" ) func main() { var ( - mdb string + mdbf string out string region string ) - flag.StringVar(&mdb, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb") + flag.StringVar(&mdbf, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb") flag.StringVar(&out, "o", `.`, "`dir`ectory for output files") flag.StringVar(®ion, "region", "global", "region the database is for (global, jp)") flag.Parse() - slog.Info("open", slog.String("mdb", mdb)) - db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) + slog.Info("open", slog.String("mdb", mdbf)) + db, err := sqlitex.NewPool(mdbf, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) if err != nil { - slog.Error("opening mdb", slog.String("mdb", mdb), slog.Any("err", err)) + slog.Error("opening mdb", slog.String("mdb", mdbf), slog.Any("err", err)) os.Exit(1) } @@ -50,193 +44,16 @@ func main() { }() loadgroup, ctx1 := errgroup.WithContext(ctx) - charas := load(ctx1, loadgroup, db, "characters", characterSQL, func(s *sqlite.Stmt) horse.Character { - return horse.Character{ - ID: horse.CharacterID(s.ColumnInt(0)), - Name: s.ColumnText(1), - } - }) - aff := load(ctx1, loadgroup, db, "pair affinity", affinitySQL, func(s *sqlite.Stmt) horse.AffinityRelation { - return horse.AffinityRelation{ - IDA: s.ColumnInt(0), - IDB: s.ColumnInt(1), - IDC: s.ColumnInt(2), - Affinity: s.ColumnInt(3), - } - }) - umas := load(ctx1, loadgroup, db, "umas", umaSQL, func(s *sqlite.Stmt) horse.Uma { - return horse.Uma{ - ID: horse.UmaID(s.ColumnInt(0)), - CharacterID: horse.CharacterID(s.ColumnInt(1)), - Name: s.ColumnText(2), - Variant: s.ColumnText(3), - Sprint: horse.AptitudeLevel(s.ColumnInt(4)), - Mile: horse.AptitudeLevel(s.ColumnInt(6)), - Medium: horse.AptitudeLevel(s.ColumnInt(7)), - Long: horse.AptitudeLevel(s.ColumnInt(8)), - Front: horse.AptitudeLevel(s.ColumnInt(9)), - Pace: horse.AptitudeLevel(s.ColumnInt(10)), - Late: horse.AptitudeLevel(s.ColumnInt(11)), - End: horse.AptitudeLevel(s.ColumnInt(12)), - Turf: horse.AptitudeLevel(s.ColumnInt(13)), - Dirt: horse.AptitudeLevel(s.ColumnInt(14)), - Unique: horse.SkillID(s.ColumnInt(15)), - Skill1: horse.SkillID(s.ColumnInt(16)), - Skill2: horse.SkillID(s.ColumnInt(17)), - Skill3: horse.SkillID(s.ColumnInt(18)), - SkillPL2: horse.SkillID(s.ColumnInt(19)), - SkillPL3: horse.SkillID(s.ColumnInt(20)), - SkillPL4: horse.SkillID(s.ColumnInt(21)), - SkillPL5: horse.SkillID(s.ColumnInt(22)), - } - }) - sg := load(ctx1, loadgroup, db, "skill groups", skillGroupSQL, func(s *sqlite.Stmt) horse.SkillGroup { - return horse.SkillGroup{ - ID: horse.SkillGroupID(s.ColumnInt(0)), - Skill1: horse.SkillID(s.ColumnInt(1)), - Skill2: horse.SkillID(s.ColumnInt(2)), - Skill3: horse.SkillID(s.ColumnInt(3)), - SkillBad: horse.SkillID(s.ColumnInt(4)), - } - }) - skills := load(ctx1, loadgroup, db, "skills", skillSQL, func(s *sqlite.Stmt) horse.Skill { - return horse.Skill{ - ID: horse.SkillID(s.ColumnInt(0)), - Name: s.ColumnText(1), - Description: s.ColumnText(2), - Group: horse.SkillGroupID(s.ColumnInt32(3)), - Rarity: int8(s.ColumnInt(5)), - GroupRate: int8(s.ColumnInt(6)), - GradeValue: s.ColumnInt32(7), - WitCheck: s.ColumnBool(8), - Activations: trimActivations([]horse.Activation{ - { - Precondition: s.ColumnText(9), - Condition: s.ColumnText(10), - Duration: horse.TenThousandths(s.ColumnInt(11)), - DurScale: horse.DurScale(s.ColumnInt(12)), - Cooldown: horse.TenThousandths(s.ColumnInt(13)), - Abilities: trimAbilities([]horse.Ability{ - { - Type: horse.AbilityType(s.ColumnInt(14)), - ValueUsage: horse.AbilityValueUsage(s.ColumnInt(15)), - Value: horse.TenThousandths(s.ColumnInt(16)), - Target: horse.AbilityTarget(s.ColumnInt(17)), - TargetValue: s.ColumnInt32(18), - }, - { - Type: horse.AbilityType(s.ColumnInt(19)), - ValueUsage: horse.AbilityValueUsage(s.ColumnInt(20)), - Value: horse.TenThousandths(s.ColumnInt(21)), - Target: horse.AbilityTarget(s.ColumnInt(22)), - TargetValue: s.ColumnInt32(23), - }, - { - Type: horse.AbilityType(s.ColumnInt(24)), - ValueUsage: horse.AbilityValueUsage(s.ColumnInt(25)), - Value: horse.TenThousandths(s.ColumnInt(26)), - Target: horse.AbilityTarget(s.ColumnInt(27)), - TargetValue: s.ColumnInt32(28), - }, - }), - }, - { - Precondition: s.ColumnText(29), - Condition: s.ColumnText(30), - Duration: horse.TenThousandths(s.ColumnInt(31)), - DurScale: horse.DurScale(s.ColumnInt(32)), - Cooldown: horse.TenThousandths(s.ColumnInt(33)), - Abilities: trimAbilities([]horse.Ability{ - { - Type: horse.AbilityType(s.ColumnInt(34)), - ValueUsage: horse.AbilityValueUsage(s.ColumnInt(35)), - Value: horse.TenThousandths(s.ColumnInt(36)), - Target: horse.AbilityTarget(s.ColumnInt(37)), - TargetValue: s.ColumnInt32(38), - }, - { - Type: horse.AbilityType(s.ColumnInt(39)), - ValueUsage: horse.AbilityValueUsage(s.ColumnInt(40)), - Value: horse.TenThousandths(s.ColumnInt(41)), - Target: horse.AbilityTarget(s.ColumnInt(42)), - TargetValue: s.ColumnInt32(43), - }, - { - Type: horse.AbilityType(s.ColumnInt(44)), - ValueUsage: horse.AbilityValueUsage(s.ColumnInt(45)), - Value: horse.TenThousandths(s.ColumnInt(46)), - Target: horse.AbilityTarget(s.ColumnInt(47)), - TargetValue: s.ColumnInt32(48), - }, - }), - }, - }), - UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name - Tags: parseTags(s.ColumnText(54)), - SPCost: s.ColumnInt(49), - IconID: s.ColumnInt(53), - } - }) - races := load(ctx1, loadgroup, db, "races", raceSQL, func(s *sqlite.Stmt) horse.Race { - return horse.Race{ - ID: horse.RaceID(s.ColumnInt(0)), - Name: s.ColumnText(1), - // TODO(zeph): grade - Thumbnail: s.ColumnInt(3), - Primary: horse.RaceID(s.ColumnInt(4)), - } - }) - saddles := load(ctx1, loadgroup, db, "saddles", saddleSQL, func(s *sqlite.Stmt) horse.Saddle { - return horse.Saddle{ - ID: horse.SaddleID(s.ColumnInt(0)), - Name: s.ColumnText(1), - Races: trimZeros( - horse.RaceID(s.ColumnInt(2)), - horse.RaceID(s.ColumnInt(3)), - horse.RaceID(s.ColumnInt(4)), - ), - Type: horse.SaddleType(s.ColumnInt(5)), - Primary: horse.SaddleID(s.ColumnInt(6)), - } - }) - scenarios := load(ctx1, loadgroup, db, "scenarios", scenarioSQL, func(s *sqlite.Stmt) horse.Scenario { - return horse.Scenario{ - ID: horse.ScenarioID(s.ColumnInt(0)), - Name: s.ColumnText(1), - Title: s.ColumnText(2), - } - }) - sparks := load(ctx1, loadgroup, db, "sparks", sparkSQL, func(s *sqlite.Stmt) horse.Spark { - return horse.Spark{ - ID: horse.SparkID(s.ColumnInt(0)), - Name: s.ColumnText(1), - Description: s.ColumnText(2), - Group: horse.SparkGroupID(s.ColumnInt(3)), - Rarity: horse.SparkRarity(s.ColumnInt(4)), - Type: horse.SparkType(s.ColumnInt(5)), - // Effects filled in later. - } - }) - sparkeffs := load(ctx1, loadgroup, db, "spark effects", sparkEffectSQL, func(s *sqlite.Stmt) SparkEffImm { - return SparkEffImm{ - Group: horse.SparkGroupID(s.ColumnInt(0)), - Effect: s.ColumnInt(1), - Target: horse.SparkTarget(s.ColumnInt(2)), - Value1: s.ColumnInt32(3), - Value2: s.ColumnInt32(4), - } - }) - convos := load(ctx1, loadgroup, db, "lobby conversations", conversationSQL, func(s *sqlite.Stmt) horse.Conversation { - return horse.Conversation{ - CharacterID: horse.CharacterID(s.ColumnInt(0)), - Number: s.ColumnInt(1), - Location: horse.LobbyConversationLocationID(s.ColumnInt(2)), - Chara1: horse.CharacterID(s.ColumnInt(3)), - Chara2: horse.CharacterID(s.ColumnInt(4)), - Chara3: horse.CharacterID(s.ColumnInt(5)), - ConditionType: s.ColumnInt(6), - } - }) + charas := load(ctx1, loadgroup, db, "characters", mdb.Characters) + aff := load(ctx1, loadgroup, db, "pair affinity", mdb.AffinitySummary) + umas := load(ctx1, loadgroup, db, "umas", mdb.Umas) + sg := load(ctx1, loadgroup, db, "skill groups", mdb.SkillGroups) + skills := load(ctx1, loadgroup, db, "skills", mdb.Skills) + races := load(ctx1, loadgroup, db, "races", mdb.Races) + saddles := load(ctx1, loadgroup, db, "saddles", mdb.Saddles) + scenarios := load(ctx1, loadgroup, db, "scenarios", mdb.Scenarios) + sparks := load(ctx1, loadgroup, db, "sparks", mdb.Sparks) + convos := load(ctx1, loadgroup, db, "lobby conversations", mdb.Conversations) if err := os.MkdirAll(filepath.Join(out, region), 0775); err != nil { slog.Error("create output dir", slog.Any("err", err)) @@ -252,7 +69,7 @@ func main() { writegroup.Go(func() error { return write(ctx2, out, region, "race.json", races) }) writegroup.Go(func() error { return write(ctx2, out, region, "saddle.json", saddles) }) writegroup.Go(func() error { return write(ctx2, out, region, "scenario.json", scenarios) }) - writegroup.Go(func() error { return write(ctx2, out, region, "spark.json", mergesparks(sparks, sparkeffs)) }) + writegroup.Go(func() error { return write(ctx2, out, region, "spark.json", sparks) }) writegroup.Go(func() error { return write(ctx2, out, region, "conversation.json", convos) }) if err := writegroup.Wait(); err != nil { slog.ErrorContext(ctx, "write", slog.Any("err", err)) @@ -262,56 +79,13 @@ func main() { slog.InfoContext(ctx, "done") } -var ( - //go:embed sql/character.sql - characterSQL string - //go:embed sql/affinity.sql - affinitySQL string - //go:embed sql/uma.sql - umaSQL string - //go:embed sql/skill-group.sql - skillGroupSQL string - //go:embed sql/skill.sql - skillSQL string - //go:embed sql/race.sql - raceSQL string - //go:embed sql/saddle.sql - saddleSQL string - //go:embed sql/scenario.sql - scenarioSQL string - //go:embed sql/spark.sql - sparkSQL string - //go:embed sql/spark-effect.sql - sparkEffectSQL string - //go:embed sql/conversation.sql - conversationSQL string -) - -func load[T any](ctx context.Context, group *errgroup.Group, db *sqlitex.Pool, kind, sql string, row func(*sqlite.Stmt) T) func() ([]T, error) { +func load[T any](ctx context.Context, group *errgroup.Group, db *sqlitex.Pool, kind string, get func(context.Context, *sqlitex.Pool) ([]T, error)) func() ([]T, error) { slog.InfoContext(ctx, "load", slog.String("kind", kind)) var r []T group.Go(func() error { - conn, err := db.Take(ctx) - defer db.Put(conn) - if err != nil { - return fmt.Errorf("couldn't get connection for %s: %w", kind, err) - } - stmt, _, err := conn.PrepareTransient(sql) - if err != nil { - return fmt.Errorf("couldn't prepare statement for %s: %w", kind, err) - } - - for { - ok, err := stmt.Step() - if err != nil { - return fmt.Errorf("error stepping %s: %w", kind, err) - } - if !ok { - break - } - r = append(r, row(stmt)) - } - return nil + got, err := get(ctx, db) + r = got + return err }) return func() ([]T, error) { err := group.Wait() @@ -345,90 +119,3 @@ func write[T any](ctx context.Context, out, region, name string, v func() (T, er slog.InfoContext(ctx, "marshaled", slog.String("path", p)) return err } - -func mergesparks(sparks func() ([]horse.Spark, error), effs func() ([]SparkEffImm, error)) func() ([]horse.Spark, error) { - return func() ([]horse.Spark, error) { - sp, err := sparks() - if err != nil { - return nil, err - } - ef, err := effs() - if err != nil { - return nil, err - } - // Spark effects are sorted by group ID, but groups apply to multiple - // sparks, and we don't rely on sparks and groups being in the same order. - // It is possible to merge in linear time, but not worth the effort: - // n log n is fine since this is an AOT step. - for i := range sp { - k, ok := slices.BinarySearchFunc(ef, sp[i].Group, func(e SparkEffImm, v horse.SparkGroupID) int { return cmp.Compare(e.Group, v) }) - if !ok { - panic(fmt.Errorf("mergesparks: no spark group for %+v", &sp[i])) - } - // Back up to the first effect in the group. - for k > 0 && ef[k-1].Group == sp[i].Group { - k-- - } - // Map effect IDs to the lists of their effects. - m := make(map[int][]horse.SparkEffect) - for _, e := range ef[k:] { - if e.Group != sp[i].Group { - // Done with this group. - break - } - m[e.Effect] = append(m[e.Effect], horse.SparkEffect{Target: e.Target, Value1: e.Value1, Value2: e.Value2}) - } - // Now get effects in order. - keys := slices.Sorted(maps.Keys(m)) - sp[i].Effects = make([][]horse.SparkEffect, 0, len(keys)) - for _, key := range keys { - sp[i].Effects = append(sp[i].Effects, m[key]) - } - } - return sp, nil - } -} - -type SparkEffImm struct { - Group horse.SparkGroupID - Effect int - Target horse.SparkTarget - Value1 int32 - Value2 int32 -} - -func parseTags(s string) []uint16 { - r := make([]uint16, 0, 8) - for s != "" { - t, u, _ := strings.Cut(s, "/") - s = u - v, err := strconv.ParseUint(t, 10, 16) - if err != nil { - panic(fmt.Errorf("parsing skill tags: %w", err)) - } - r = append(r, uint16(v)) - } - return trimZeros(r...) -} - -func trimAbilities(s []horse.Ability) []horse.Ability { - for len(s) > 0 && s[len(s)-1].Type == 0 { - s = s[:len(s)-1] - } - return s -} - -func trimActivations(s []horse.Activation) []horse.Activation { - for len(s) > 0 && s[len(s)-1].Condition == "" { - s = s[:len(s)-1] - } - return s -} - -func trimZeros[T comparable](s ...T) []T { - var zero T - for len(s) > 0 && s[len(s)-1] == zero { - s = s[:len(s)-1] - } - return s -} diff --git a/global/uma.json b/global/uma.json index 8e4894c..d6a8834 100644 --- a/global/uma.json +++ b/global/uma.json @@ -4,7 +4,7 @@ "chara_id": 1001, "name": "[Special Dreamer] Special Week", "variant": "[Special Dreamer]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7, @@ -28,7 +28,7 @@ "chara_id": 1001, "name": "[Hopp'n♪Happy Heart] Special Week", "variant": "[Hopp'n♪Happy Heart]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7, @@ -52,7 +52,7 @@ "chara_id": 1002, "name": "[Innocent Silence] Silence Suzuka", "variant": "[Innocent Silence]", - "sprint": 0, + "sprint": 4, "mile": 7, "medium": 7, "long": 3, @@ -76,7 +76,7 @@ "chara_id": 1003, "name": "[Peak Joy] Tokai Teio", "variant": "[Peak Joy]", - "sprint": 0, + "sprint": 2, "mile": 3, "medium": 7, "long": 6, @@ -100,7 +100,7 @@ "chara_id": 1003, "name": "[Beyond the Horizon] Tokai Teio", "variant": "[Beyond the Horizon]", - "sprint": 0, + "sprint": 2, "mile": 3, "medium": 7, "long": 6, @@ -124,7 +124,7 @@ "chara_id": 1004, "name": "[Formula R] Maruzensky", "variant": "[Formula R]", - "sprint": 0, + "sprint": 6, "mile": 7, "medium": 6, "long": 5, @@ -148,7 +148,7 @@ "chara_id": 1004, "name": "[Hot☆Summer Night] Maruzensky", "variant": "[Hot☆Summer Night]", - "sprint": 0, + "sprint": 6, "mile": 7, "medium": 6, "long": 5, @@ -172,7 +172,7 @@ "chara_id": 1005, "name": "[Shooting Star Revue] Fuji Kiseki", "variant": "[Shooting Star Revue]", - "sprint": 0, + "sprint": 6, "mile": 7, "medium": 6, "long": 3, @@ -196,7 +196,7 @@ "chara_id": 1005, "name": "[Succès Étoilé] Fuji Kiseki", "variant": "[Succès Étoilé]", - "sprint": 0, + "sprint": 6, "mile": 7, "medium": 6, "long": 3, @@ -220,7 +220,7 @@ "chara_id": 1006, "name": "[Starlight Beat] Oguri Cap", "variant": "[Starlight Beat]", - "sprint": 0, + "sprint": 3, "mile": 7, "medium": 7, "long": 6, @@ -244,7 +244,7 @@ "chara_id": 1006, "name": "[Ashen Miracle] Oguri Cap", "variant": "[Ashen Miracle]", - "sprint": 0, + "sprint": 3, "mile": 7, "medium": 7, "long": 6, @@ -268,7 +268,7 @@ "chara_id": 1007, "name": "[Red Strife] Gold Ship", "variant": "[Red Strife]", - "sprint": 0, + "sprint": 1, "mile": 5, "medium": 7, "long": 7, @@ -292,7 +292,7 @@ "chara_id": 1008, "name": "[Wild Top Gear] Vodka", "variant": "[Wild Top Gear]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 2, @@ -316,7 +316,7 @@ "chara_id": 1009, "name": "[Peak Blue] Daiwa Scarlet", "variant": "[Peak Blue]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 6, @@ -340,7 +340,7 @@ "chara_id": 1010, "name": "[Wild Frontier] Taiki Shuttle", "variant": "[Wild Frontier]", - "sprint": 0, + "sprint": 7, "mile": 7, "medium": 3, "long": 1, @@ -364,7 +364,7 @@ "chara_id": 1011, "name": "[Stone-Piercing Blue] Grass Wonder", "variant": "[Stone-Piercing Blue]", - "sprint": 0, + "sprint": 1, "mile": 7, "medium": 6, "long": 7, @@ -388,7 +388,7 @@ "chara_id": 1011, "name": "[Saintly Jade Cleric] Grass Wonder", "variant": "[Saintly Jade Cleric ]", - "sprint": 0, + "sprint": 1, "mile": 7, "medium": 6, "long": 7, @@ -412,7 +412,7 @@ "chara_id": 1012, "name": "[Azure Amazon] Hishi Amazon", "variant": "[Azure Amazon]", - "sprint": 0, + "sprint": 4, "mile": 7, "medium": 7, "long": 6, @@ -436,7 +436,7 @@ "chara_id": 1013, "name": "[Frontline Elegance] Mejiro McQueen", "variant": "[Frontline Elegance]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 7, @@ -460,7 +460,7 @@ "chara_id": 1013, "name": "[End of the Skies] Mejiro McQueen", "variant": "[End of the Skies]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 7, @@ -484,7 +484,7 @@ "chara_id": 1014, "name": "[El☆Número 1] El Condor Pasa", "variant": "[El☆Número 1]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 6, @@ -508,7 +508,7 @@ "chara_id": 1014, "name": "[Kukulkan Warrior] El Condor Pasa", "variant": "[Kukulkan Warrior]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 6, @@ -532,7 +532,7 @@ "chara_id": 1015, "name": "[O Sole Suo!] T.M. Opera O", "variant": "[O Sole Suo!]", - "sprint": 0, + "sprint": 1, "mile": 3, "medium": 7, "long": 7, @@ -556,7 +556,7 @@ "chara_id": 1015, "name": "[New Year, Same Radiance!] T.M. Opera O", "variant": "[New Year, Same Radiance!]", - "sprint": 0, + "sprint": 1, "mile": 3, "medium": 7, "long": 7, @@ -580,7 +580,7 @@ "chara_id": 1016, "name": "[Maverick] Narita Brian", "variant": "[Maverick]", - "sprint": 0, + "sprint": 2, "mile": 6, "medium": 7, "long": 7, @@ -604,7 +604,7 @@ "chara_id": 1017, "name": "[Emperor's Path] Symboli Rudolf", "variant": "[Emperor's Path]", - "sprint": 0, + "sprint": 3, "mile": 5, "medium": 7, "long": 7, @@ -628,7 +628,7 @@ "chara_id": 1017, "name": "[Archer by Moonlight] Symboli Rudolf", "variant": "[Archer by Moonlight]", - "sprint": 0, + "sprint": 3, "mile": 5, "medium": 7, "long": 7, @@ -652,7 +652,7 @@ "chara_id": 1018, "name": "[Empress Road] Air Groove", "variant": "[Empress Road]", - "sprint": 0, + "sprint": 5, "mile": 6, "medium": 7, "long": 3, @@ -676,7 +676,7 @@ "chara_id": 1018, "name": "[Quercus Civilis] Air Groove", "variant": "[Quercus Civilis]", - "sprint": 0, + "sprint": 5, "mile": 6, "medium": 7, "long": 3, @@ -700,7 +700,7 @@ "chara_id": 1019, "name": "[Full-Color Fangirling] Agnes Digital", "variant": "[Full-Color Fangirling]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 1, @@ -724,7 +724,7 @@ "chara_id": 1020, "name": "[Reeling in the Big One] Seiun Sky", "variant": "[Reeling in the Big One]", - "sprint": 0, + "sprint": 1, "mile": 5, "medium": 7, "long": 7, @@ -748,7 +748,7 @@ "chara_id": 1020, "name": "[Soirée des Chatons] Seiun Sky", "variant": "[Soirée des Chatons]", - "sprint": 0, + "sprint": 1, "mile": 5, "medium": 7, "long": 7, @@ -772,7 +772,7 @@ "chara_id": 1021, "name": "[Fast as Lightning] Tamamo Cross", "variant": "[Fast as Lightning]", - "sprint": 0, + "sprint": 1, "mile": 3, "medium": 7, "long": 7, @@ -796,7 +796,7 @@ "chara_id": 1022, "name": "[Noble Seamair] Fine Motion", "variant": "[Noble Seamair]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 5, @@ -820,7 +820,7 @@ "chara_id": 1022, "name": "[Titania] Fine Motion", "variant": "[Titania]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 7, "long": 5, @@ -844,7 +844,7 @@ "chara_id": 1023, "name": "[pf. Winning Equation...] Biwa Hayahide", "variant": "[pf. Winning Equation...]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7, @@ -868,7 +868,7 @@ "chara_id": 1023, "name": "[Rouge Caroler] Biwa Hayahide", "variant": "[Rouge Caroler]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7, @@ -892,7 +892,7 @@ "chara_id": 1024, "name": "[Scramble☆Zone] Mayano Top Gun", "variant": "[Scramble☆Zone]", - "sprint": 0, + "sprint": 4, "mile": 4, "medium": 7, "long": 7, @@ -916,7 +916,7 @@ "chara_id": 1024, "name": "[Sunlight Bouquet] Mayano Top Gun", "variant": "[Sunlight Bouquet]", - "sprint": 0, + "sprint": 4, "mile": 4, "medium": 7, "long": 7, @@ -940,7 +940,7 @@ "chara_id": 1025, "name": "[Creeping Shadow] Manhattan Cafe", "variant": "[Creeping Shadow]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 6, "long": 7, @@ -964,7 +964,7 @@ "chara_id": 1026, "name": "[MB-19890425] Mihono Bourbon", "variant": "[MB-19890425]", - "sprint": 0, + "sprint": 5, "mile": 6, "medium": 7, "long": 6, @@ -988,7 +988,7 @@ "chara_id": 1026, "name": "[CODE: ICING] Mihono Bourbon", "variant": "[CODE: ICING]", - "sprint": 0, + "sprint": 5, "mile": 6, "medium": 7, "long": 6, @@ -1012,7 +1012,7 @@ "chara_id": 1027, "name": "[Down the Line] Mejiro Ryan", "variant": "[Down the Line]", - "sprint": 0, + "sprint": 3, "mile": 5, "medium": 7, "long": 6, @@ -1036,7 +1036,7 @@ "chara_id": 1028, "name": "[Buono ☆ Alla Moda] Hishi Akebono", "variant": "[Buono ☆ Alla Moda]", - "sprint": 0, + "sprint": 7, "mile": 6, "medium": 2, "long": 1, @@ -1060,7 +1060,7 @@ "chara_id": 1030, "name": "[Rosy Dreams] Rice Shower", "variant": "[Rosy Dreams]", - "sprint": 0, + "sprint": 3, "mile": 5, "medium": 7, "long": 7, @@ -1084,7 +1084,7 @@ "chara_id": 1030, "name": "[Vampire Makeover!] Rice Shower", "variant": "[Vampire Makeover!]", - "sprint": 0, + "sprint": 3, "mile": 5, "medium": 7, "long": 7, @@ -1108,7 +1108,7 @@ "chara_id": 1031, "name": "[Always Electrifying] Ines Fujin", "variant": "[Always Electrifying]", - "sprint": 0, + "sprint": 1, "mile": 7, "medium": 7, "long": 5, @@ -1132,7 +1132,7 @@ "chara_id": 1032, "name": "[Tach-nology] Agnes Tachyon", "variant": "[Tach-nology]", - "sprint": 0, + "sprint": 1, "mile": 4, "medium": 7, "long": 6, @@ -1156,7 +1156,7 @@ "chara_id": 1033, "name": "[Starry Nocturne] Admire Vega", "variant": "[Starry Nocturne]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 5, @@ -1180,7 +1180,7 @@ "chara_id": 1034, "name": "[Edomurasaki] Inari One", "variant": "[Edomurasaki]", - "sprint": 0, + "sprint": 2, "mile": 6, "medium": 7, "long": 7, @@ -1204,7 +1204,7 @@ "chara_id": 1035, "name": "[Get to Winning!] Winning Ticket", "variant": "[Get to Winning!]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 6, @@ -1228,7 +1228,7 @@ "chara_id": 1037, "name": "[Meisterschaft] Eishin Flash", "variant": "[Meisterschaft]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 7, @@ -1252,7 +1252,7 @@ "chara_id": 1037, "name": "[Precise Chocolatier] Eishin Flash", "variant": "[Precise Chocolatier]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 7, @@ -1276,7 +1276,7 @@ "chara_id": 1038, "name": "[Fille Éclair] Curren Chan", "variant": "[Fille Éclair]", - "sprint": 0, + "sprint": 7, "mile": 4, "medium": 1, "long": 1, @@ -1300,7 +1300,7 @@ "chara_id": 1038, "name": "[Ma Chérie of the New Moon] Curren Chan", "variant": "[Ma Chérie of the New Moon]", - "sprint": 0, + "sprint": 7, "mile": 4, "medium": 1, "long": 1, @@ -1324,7 +1324,7 @@ "chara_id": 1039, "name": "[Princess of Pink] Kawakami Princess", "variant": "[Princess of Pink]", - "sprint": 0, + "sprint": 4, "mile": 6, "medium": 7, "long": 2, @@ -1348,7 +1348,7 @@ "chara_id": 1040, "name": "[Authentic / 1928] Gold City", "variant": "[Authentic / 1928]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 6, "long": 6, @@ -1372,7 +1372,7 @@ "chara_id": 1040, "name": "[Autumn Cosmos] Gold City", "variant": "[Autumn Cosmos]", - "sprint": 0, + "sprint": 2, "mile": 7, "medium": 6, "long": 6, @@ -1396,7 +1396,7 @@ "chara_id": 1041, "name": "[Blossom in Learning] Sakura Bakushin O", "variant": "[Blossom in Learning]", - "sprint": 0, + "sprint": 7, "mile": 6, "medium": 1, "long": 1, @@ -1444,7 +1444,7 @@ "chara_id": 1045, "name": "[Murmuring Stream] Super Creek", "variant": "[Murmuring Stream]", - "sprint": 0, + "sprint": 1, "mile": 1, "medium": 7, "long": 7, @@ -1468,7 +1468,7 @@ "chara_id": 1045, "name": "[Chiffon-Wrapped Mummy] Super Creek", "variant": "[Chiffon-Wrapped Mummy]", - "sprint": 0, + "sprint": 1, "mile": 1, "medium": 7, "long": 7, @@ -1492,7 +1492,7 @@ "chara_id": 1046, "name": "[LOVE☆4EVER] Smart Falcon", "variant": "[LOVE☆4EVER]", - "sprint": 0, + "sprint": 6, "mile": 7, "medium": 7, "long": 3, @@ -1516,7 +1516,7 @@ "chara_id": 1048, "name": "[Jokester ☆ Vibes] Tosen Jordan", "variant": "[Jokester ☆ Vibes]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 6, @@ -1540,7 +1540,7 @@ "chara_id": 1050, "name": "[Nevertheless] Narita Taishin", "variant": "[Nevertheless]", - "sprint": 0, + "sprint": 2, "mile": 4, "medium": 7, "long": 7, @@ -1564,7 +1564,7 @@ "chara_id": 1051, "name": "[Layered Petals] Nishino Flower", "variant": "[Layered Petals]", - "sprint": 0, + "sprint": 7, "mile": 7, "medium": 3, "long": 1, @@ -1588,7 +1588,7 @@ "chara_id": 1052, "name": "[Bestest Prize ♪] Haru Urara", "variant": "[Bestest Prize ♪]", - "sprint": 0, + "sprint": 7, "mile": 6, "medium": 1, "long": 1, @@ -1612,7 +1612,7 @@ "chara_id": 1052, "name": "[New Year ♪ New Urara!] Haru Urara", "variant": "[New Year ♪ New Urara!]", - "sprint": 0, + "sprint": 7, "mile": 7, "medium": 1, "long": 1, @@ -1636,7 +1636,7 @@ "chara_id": 1056, "name": "[Rising☆Fortune] Matikanefukukitaru", "variant": "[Rising☆Fortune]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7, @@ -1660,7 +1660,7 @@ "chara_id": 1056, "name": "[Lucky Tidings] Matikanefukukitaru", "variant": "[Lucky Tidings]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7, @@ -1684,7 +1684,7 @@ "chara_id": 1058, "name": "[Turbulent Blue] Meisho Doto", "variant": "[Turbulent Blue]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 7, @@ -1708,7 +1708,7 @@ "chara_id": 1059, "name": "[Off the Line] Mejiro Dober", "variant": "[Off the Line]", - "sprint": 0, + "sprint": 3, "mile": 7, "medium": 7, "long": 2, @@ -1732,7 +1732,7 @@ "chara_id": 1060, "name": "[Poinsettia Ribbon] Nice Nature", "variant": "[Poinsettia Ribbon]", - "sprint": 0, + "sprint": 1, "mile": 5, "medium": 7, "long": 7, @@ -1756,7 +1756,7 @@ "chara_id": 1060, "name": "[Run & Win] Nice Nature", "variant": "[Run & Win]", - "sprint": 0, + "sprint": 1, "mile": 5, "medium": 7, "long": 7, @@ -1780,7 +1780,7 @@ "chara_id": 1061, "name": "[King of Emeralds] King Halo", "variant": "[King of Emeralds]", - "sprint": 0, + "sprint": 7, "mile": 6, "medium": 6, "long": 5, @@ -1804,7 +1804,7 @@ "chara_id": 1061, "name": "[Cheerleader in Noble White] King Halo", "variant": "[Cheerleader in Noble White]", - "sprint": 0, + "sprint": 7, "mile": 6, "medium": 6, "long": 5, @@ -1828,7 +1828,7 @@ "chara_id": 1062, "name": "[Clippety-Tippety-Clop] Matikanetannhauser", "variant": "[Clippety-Tippety-Clop]", - "sprint": 0, + "sprint": 1, "mile": 4, "medium": 7, "long": 7, @@ -1852,7 +1852,7 @@ "chara_id": 1064, "name": "[Line Breakthrough] Mejiro Palmer", "variant": "[Line Breakthrough]", - "sprint": 0, + "sprint": 1, "mile": 2, "medium": 7, "long": 7, @@ -1876,7 +1876,7 @@ "chara_id": 1067, "name": "[Natural Brilliance] Satono Diamond", "variant": "[Natural Brilliance]", - "sprint": 0, + "sprint": 1, "mile": 5, "medium": 7, "long": 7, @@ -1900,7 +1900,7 @@ "chara_id": 1068, "name": "[Gilded Shrine to Glory] Kitasan Black", "variant": "[Gilded Shrine to Glory]", - "sprint": 0, + "sprint": 3, "mile": 5, "medium": 7, "long": 7, @@ -1924,7 +1924,7 @@ "chara_id": 1069, "name": "[Strength in Full Bloom] Sakura Chiyono O", "variant": "[Strength in Full Bloom]", - "sprint": 0, + "sprint": 3, "mile": 7, "medium": 7, "long": 3, @@ -1948,7 +1948,7 @@ "chara_id": 1071, "name": "[Crystalline] Mejiro Ardan", "variant": "[Crystalline]", - "sprint": 0, + "sprint": 3, "mile": 6, "medium": 7, "long": 4, @@ -1972,7 +1972,7 @@ "chara_id": 1072, "name": "[Blazed Head, Covered Fists] Yaeno Muteki", "variant": "[Blazed Head, Covered Fists]", - "sprint": 0, + "sprint": 1, "mile": 6, "medium": 7, "long": 3, @@ -1996,7 +1996,7 @@ "chara_id": 1074, "name": "[Brunissage Line] Mejiro Bright", "variant": "[Brunissage Line]", - "sprint": 0, + "sprint": 2, "mile": 5, "medium": 7, "long": 7,