13 Commits
Author SHA1 Message Date
zephyr 2d14919f6a cmd/ismdb: start work 2026-10-16 12:30:28 -04:00
zephyr 381df5bd35 umadump/ism: new end info fields
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-07 16:35:31 -04:00
zephyr e5ebd1894c umadump/ism: new reward summary fields
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-07 13:13:30 -04:00
zephyr c43f6b8292 cmd/racedb: program to load races into an sqlite db
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-06 21:00:37 -04:00
zephyr cccf0cc838 umadump/replay: allow fan count in race horse data 2026-10-06 21:00:18 -04:00
zephyr 64f82136cb umadump/replay: trainer name is null for npcs 2026-10-04 09:08:25 -04:00
zephyr 53c7079f46 umadump/replay: decode race replays
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-03 22:34:02 -04:00
zephyr 1cfabb18f5 zenno: fix type error on fake data
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-22 22:31:12 -04:00
zephyr bf4e750825 zenno/chara/convo: hyperlink to umadump 2026-09-22 22:30:00 -04:00
zephyr 57ffadb06a zenno/chara/convo: integrate umadump talk gallery
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
Fixes #47.
2026-09-22 22:25:43 -04:00
zephyr 86583e68d1 zenno: wrapper component for race length inputs
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-09-22 10:59:29 -04:00
zephyr fde5b4fd20 zenno: update types for new api fields 2026-09-21 10:31:14 -04:00
zephyr 1cf3af5f79 zenno: update full charge info with new knowledge
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-21 09:48:56 -04:00
37 changed files with 19582 additions and 364 deletions
+188
View File
@@ -0,0 +1,188 @@
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/umadump/ism"
)
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, career *ism.WorkIdleSingleMode, trainerID int64) (err error) {
conn, err := db.Take(ctx)
if err != nil {
return err
}
defer db.Put(conn)
defer sqlitex.Transaction(conn)(&err)
}
//go:embed insert-career.sql
var insertCareerSQL string
func insertCareer(conn *sqlite.Conn, career *ism.WorkIdleSingleMode, trainerID int64) error {
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.OldChara.SuccessionTrainedCharaID1))
stmt.SetInt64("$succession_trained_chara_id_2", int64(career.OldChara.SuccessionTrainedCharaID2))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race course: %w", err)
}
return nil
}
func insertConditions(conn *sqlite.Conn, cs []ism.CharaEffectLog, trainerID int64, trainedChara int32) error {
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, gain *ism.IdleSingleModeGainInfo, trainerID int64, trainedChara int32, source int32) error {
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: %w", err)
}
return nil
}
+69
View File
@@ -0,0 +1,69 @@
INSERT INTO career VALUES (
$trainer_id,
$trained_chara_id,
$card_id,
$scenario_id,
$route_id,
$start_time,
$end_time,
$chara_grade,
$star_level,
$potential_level,
$speed,
$stamina,
$power,
$guts,
$wit,
$energy,
$max_speed,
$max_stamina,
$max_power,
$max_guts,
$max_wit,
$max_energy,
$skill_point,
$mood,
$turf,
$dirt,
$sprint,
$mile,
$medium,
$long,
$front_runner,
$pace_chaser,
$late_surger,
$end_closer,
$start_speed,
$start_stamina,
$start_power,
$start_guts,
$start_wit,
$start_max_speed,
$start_max_stamina,
$start_max_power,
$start_max_guts,
$start_max_wit,
$start_turf,
$start_dirt,
$start_sprint,
$start_mile,
$start_medium,
$start_long,
$start_front_runner,
$start_pace_chaser,
$start_late_surger,
$start_end_closer,
$fans,
$race_program_id,
$reserve_race_program_id,
$race_running_style,
$is_short_race,
$succession_trained_chara_id_1,
$succession_trained_chara_id_2
)
+31
View File
@@ -0,0 +1,31 @@
INSERT INTO gain VALUES (
$trainer_id,
$trained_chara_id,
$source,
$gain_speed,
$gain_stamina,
$gain_power,
$gain_guts,
$gain_wit,
$gain_skill_point,
$gain_max_speed,
$gain_max_stamina,
$gain_max_power,
$gain_max_guts,
$gain_max_wit,
$gain_turf,
$gain_dirt,
$gain_sprint,
$gain_mile,
$gain_medium,
$gain_long,
$gain_front_runner,
$gain_pace_chaser,
$gain_late_surger,
$gain_end_closer
)
+251
View File
@@ -0,0 +1,251 @@
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 gain_skill (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
source INTEGER NOT NULL,
skill_id INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, source, skill_id),
FOREIGN KEY (trainer_id, trained_chara_id, source) REFERENCES gain ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS career_skill (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, skill_id),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS career_skill_hint (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS career_support_card (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
position INTEGER NOT NULL,
support_card_id INTEGER NOT NULL,
limit_break_count INTEGER NOT NULL,
exp INTEGER NOT NULL,
owner_viewer_id INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, position),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE,
UNIQUE (trainer_id, trained_chara_id, support_card_id)
) STRICT;
CREATE INDEX IF NOT EXISTS career_support_card_by_card ON career_support_card (support_card_id, exp);
CREATE TABLE IF NOT EXISTS start_skill (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, skill_id),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS start_skill_hint (
trainer_id INTEGER NOT NULL DEFAULT 0,
trained_chara_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
level INTEGER NOT NULL,
PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity),
FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE
) STRICT;
+88
View File
@@ -0,0 +1,88 @@
DROP VIEW IF EXISTS gain_event;
DROP VIEW IF EXISTS gain_inspiration;
DROP VIEW IF EXISTS gain_support_card;
CREATE VIEW gain_event AS
SELECT
trainer_id,
trained_chara_id,
gain_speed,
gain_stamina,
gain_power,
gain_guts,
gain_wit,
gain_skill_point,
gain_max_speed,
gain_max_stamina,
gain_max_power,
gain_max_guts,
gain_max_wit,
gain_turf,
gain_dirt,
gain_sprint,
gain_mile,
gain_medium,
gain_long,
gain_front_runner,
gain_pace_chaser,
gain_late_surger,
gain_end_closer
FROM gain
WHERE source = 0;
CREATE VIEW gain_inspiration AS
SELECT
trainer_id,
trained_chara_id,
gain_speed,
gain_stamina,
gain_power,
gain_guts,
gain_wit,
gain_skill_point,
gain_max_speed,
gain_max_stamina,
gain_max_power,
gain_max_guts,
gain_max_wit,
gain_turf,
gain_dirt,
gain_sprint,
gain_mile,
gain_medium,
gain_long,
gain_front_runner,
gain_pace_chaser,
gain_late_surger,
gain_end_closer
FROM gain
WHERE source = 1;
CREATE VIEW gain_support_card AS
SELECT
trainer_id,
trained_chara_id,
source AS support_card_id,
gain_speed,
gain_stamina,
gain_power,
gain_guts,
gain_wit,
gain_skill_point,
gain_max_speed,
gain_max_stamina,
gain_max_power,
gain_max_guts,
gain_max_wit,
gain_turf,
gain_dirt,
gain_sprint,
gain_mile,
gain_medium,
gain_long,
gain_front_runner,
gain_pace_chaser,
gain_late_surger,
gain_end_closer
FROM gain
WHERE source > 1;
+325
View File
@@ -0,0 +1,325 @@
package main
import (
"cmp"
"context"
_ "embed"
"errors"
"fmt"
"log/slog"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitemigration"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
const appID int32 = 0x4acedb
//go:embed schema-00.sql
var schema0 string
//go:embed schema-final.sql
var schemaFinal string
func opendb(uri, journalMode string) *sqlitemigration.Pool {
schema := sqlitemigration.Schema{
Migrations: []string{
schema0,
},
AppID: appID,
RepeatableMigration: schemaFinal,
}
j := "PRAGMA journal_mode=" + journalMode
opts := sqlitemigration.Options{
Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI,
PrepareConn: func(conn *sqlite.Conn) error {
if journalMode == "" {
return nil
}
stmt, _, err := conn.PrepareTransient(j)
if err != nil {
return err
}
_, err = stmt.Step()
return errors.Join(err, stmt.Finalize())
},
}
db := sqlitemigration.NewPool(uri, schema, opts)
return db
}
func insert(ctx context.Context, db *sqlitemigration.Pool, log *slog.Logger, race *replay.Race, simdata *replay.RaceSimulateData) (err error) {
conn, err := db.Take(ctx)
if err != nil {
return err
}
defer db.Put(conn)
defer sqlitex.Transaction(conn)(&err)
log.DebugContext(ctx, "horses",
slog.Int("num", int(race.NumRaceHorses)),
slog.Int("actual", len(race.RaceHorse)),
)
var trainedCharas [18]int64
for i := range race.RaceHorse {
c := &race.RaceHorse[i]
id, err := insertTrainedChara(conn, c)
if err != nil {
return err
}
trainedCharas[i] = id
if err := insertTrainedCharaSkill(conn, c, id); err != nil {
return err
}
log.DebugContext(ctx, "horse",
slog.String("trainer", c.TrainerName),
slog.Int("trained", int(c.ResponseHorseData.TrainedCharaID)),
slog.Int("card", int(c.ResponseHorseData.CardID)),
slog.Int64("id", id),
)
}
if err = insertRaceCourseSet(conn, race); err != nil {
return err
}
log.DebugContext(ctx, "course", slog.Int("id", int(race.RaceCourseSet.ID)))
configID, err := insertRaceConfig(conn, race)
if err != nil {
return err
}
log.DebugContext(ctx, "config", slog.Int64("id", configID))
raceID, err := insertRace(conn, race, simdata, &trainedCharas, configID)
if err != nil {
code := sqlite.ErrCode(err)
if code == sqlite.ResultConstraintUnique {
log.InfoContext(ctx, "already loaded")
return nil
}
return err
}
log.DebugContext(ctx, "race", slog.Int64("id", raceID))
err = errors.Join(
insertRaceHorses(ctx, log, conn, race, simdata, &trainedCharas, raceID),
insertRaceFrames(ctx, log, conn, simdata, raceID),
insertRaceEvents(ctx, log, conn, simdata, raceID),
)
return err
}
//go:embed insert-trained_chara.sql
var insertTrainedCharaSQL string
func insertTrainedChara(conn *sqlite.Conn, c *replay.HorseData) (int64, error) {
stmt := conn.Prep(insertTrainedCharaSQL)
trainer := cmp.Or(c.ResponseHorseData.ViewerID, c.ResponseHorseData.OwnerViewerID)
stmt.SetText("$trainer", c.TrainerName)
stmt.SetInt64("$trainer_id", trainer)
stmt.SetInt64("$trained_chara_id", int64(c.ResponseHorseData.TrainedCharaID))
stmt.SetInt64("$chara_id", int64(c.CharaID))
stmt.SetText("$chara_name", c.CharaName)
stmt.SetInt64("$card_id", int64(c.ResponseHorseData.CardID))
stmt.SetInt64("$rarity", int64(c.Rarity))
stmt.SetInt64("$mob_id", int64(c.MobID))
stmt.SetInt64("$raw_speed", int64(c.ResponseHorseData.Speed))
stmt.SetInt64("$raw_stamina", int64(c.ResponseHorseData.Stamina))
stmt.SetInt64("$raw_power", int64(c.ResponseHorseData.Pow))
stmt.SetInt64("$raw_guts", int64(c.ResponseHorseData.Guts))
stmt.SetInt64("$raw_wit", int64(c.ResponseHorseData.Wiz))
stmt.SetInt64("$turf", int64(c.ResponseHorseData.ProperGroundTurf))
stmt.SetInt64("$dirt", int64(c.ResponseHorseData.ProperGroundDirt))
stmt.SetInt64("$sprint", int64(c.ResponseHorseData.ProperDistanceShort))
stmt.SetInt64("$mile", int64(c.ResponseHorseData.ProperDistanceMile))
stmt.SetInt64("$medium", int64(c.ResponseHorseData.ProperDistanceMiddle))
stmt.SetInt64("$long", int64(c.ResponseHorseData.ProperDistanceLong))
stmt.SetInt64("$front_runner", int64(c.ResponseHorseData.ProperRunningStyleNige))
stmt.SetInt64("$pace_chaser", int64(c.ResponseHorseData.ProperRunningStyleSenko))
stmt.SetInt64("$late_surger", int64(c.ResponseHorseData.ProperRunningStyleSashi))
stmt.SetInt64("$end_closer", int64(c.ResponseHorseData.ProperRunningStyleOikomi))
stmt.SetInt64("$fans", int64(c.TrainedCharaData.Fans))
stmt.SetInt64("$rank", int64(c.TrainedCharaData.Rank))
stmt.SetInt64("$rank_score", int64(c.TrainedCharaData.RankScore))
r, err := sqlitex.ResultInt64(stmt)
err = errors.Join(err, stmt.Reset())
if err != nil {
err = fmt.Errorf("inserting trained chara: %w", err)
}
return r, err
}
func insertTrainedCharaSkill(conn *sqlite.Conn, c *replay.HorseData, id int64) error {
const sql = `INSERT OR IGNORE INTO trained_chara_skill VALUES (?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, id)
for _, sk := range c.ResponseHorseData.SkillArray {
stmt.BindInt64(2, int64(sk.SkillID))
stmt.BindInt64(3, int64(sk.Level))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting trained chara id %d skill %d: %w", id, sk.SkillID, err)
}
}
return nil
}
func insertRaceCourseSet(conn *sqlite.Conn, race *replay.Race) error {
const sql = `INSERT OR IGNORE INTO race_course_set VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, int64(race.RaceCourseSet.ID))
stmt.BindInt64(2, int64(race.RaceCourseSet.RaceTrackID))
stmt.BindInt64(3, int64(race.RaceCourseSet.Distance))
stmt.BindInt64(4, int64(race.RaceCourseSet.Ground))
stmt.BindInt64(5, int64(race.RaceCourseSet.Inout))
stmt.BindInt64(6, int64(race.RaceCourseSet.Turn))
stmt.BindInt64(7, int64(race.RaceCourseSet.FenceSet))
stmt.BindInt64(8, int64(race.RaceCourseSet.FloatLaneMax))
stmt.BindInt64(9, int64(race.RaceCourseSet.CourseSetStatusID))
stmt.BindInt64(10, int64(race.RaceCourseSet.FinishTimeMin))
stmt.BindInt64(11, int64(race.RaceCourseSet.FinishTimeMinRandomRange))
stmt.BindInt64(12, int64(race.RaceCourseSet.FinishTimeMax))
stmt.BindInt64(13, int64(race.RaceCourseSet.FinishTimeMaxRandomRange))
stmt.BindFloat(14, float64(race.CourseSectionDistance))
stmt.BindInt64(15, int64(race.CourseDistanceType))
stmt.BindInt64(16, int64(race.CourseFurlongNum))
stmt.BindFloat(17, float64(race.BaseSpeed))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race course: %w", err)
}
return nil
}
//go:embed insert-race_config.sql
var insertRaceConfigSQL string
func insertRaceConfig(conn *sqlite.Conn, race *replay.Race) (int64, error) {
stmt := conn.Prep(insertRaceConfigSQL)
stmt.SetInt64("$type", int64(race.RaceType))
stmt.SetInt64("$course_set_id", int64(race.RaceCourseSet.ID))
stmt.SetInt64("$ground_condition", int64(race.GroundCondition))
stmt.SetInt64("$weather", int64(race.Weather))
stmt.SetInt64("$season", int64(race.Season))
stmt.SetInt64("$time", int64(race.Time))
stmt.SetInt64("$num_horse", int64(race.NumRaceHorses))
r, err := sqlitex.ResultInt64(stmt)
err = errors.Join(err, stmt.Reset())
if err != nil {
err = fmt.Errorf("inserting race config: %w", err)
}
return r, err
}
//go:embed insert-race.sql
var insertRaceSQL string
func insertRace(conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, configID int64) (int64, error) {
stmt := conn.Prep(insertRaceSQL)
stmt.SetInt64("$config_id", configID)
stmt.SetInt64("$seed", int64(race.RandomSeed))
stmt.SetFloat("$distance_diff_max", float64(simdata.DistanceDiffMax))
stmt.SetInt64("$frame_count", int64(simdata.FrameCount))
// zombiezen/sqlite doesn't seem to expose an API to find the index of a
// named parameter, so even though we were using names before, we switch
// to indices for trained chara IDs.
for i, id := range trainedCharas {
stmt.BindInt64(5+i, id)
}
r, err := sqlitex.ResultInt64(stmt)
err = errors.Join(err, stmt.Reset())
if err != nil {
err = fmt.Errorf("inserting race: %w", err)
}
return r, err
}
//go:embed insert-race_horse.sql
var insertRaceHorseSQL string
func insertRaceHorses(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, raceID int64) error {
stmt := conn.Prep(insertRaceHorseSQL)
stmt.SetInt64("$race", raceID)
for i, c := range race.RaceHorse {
log.DebugContext(ctx, "race horse", slog.Int64("trained", trainedCharas[i]), slog.Int("index", int(c.HorseIndex)))
stmt.SetInt64("$horse", trainedCharas[i])
stmt.SetInt64("$horse_index", int64(c.HorseIndex))
stmt.SetInt64("$post", int64(c.PostNumber))
stmt.SetInt64("$finish_order", int64(c.FinishOrder))
stmt.SetFloat("$finish_time_raw", float64(c.FinishTimeRaw))
stmt.SetFloat("$finish_time_scaled", float64(c.FinishTimeScaled))
stmt.SetFloat("$finish_diff_time_from_prev", float64(c.FinishDiffTimeFromPrev))
stmt.SetInt64("$running_style", int64(c.ResponseHorseData.RunningStyle))
stmt.SetInt64("$running_style_ex", int64(c.RunningStyleEx))
stmt.SetInt64("$active_distance_aptitude", int64(c.ActiveProperDistance))
stmt.SetInt64("$active_surface_aptitude", int64(c.ActiveProperGroundType))
stmt.SetInt64("$mood", int64(c.RaceParam.Motivation))
stmt.SetFloat("$mood_coef", float64(c.RaceParam.MotivationCoef))
stmt.SetFloat("$base_speed", float64(c.RaceParam.BaseSpeed))
stmt.SetFloat("$base_stamina", float64(c.RaceParam.BaseStamina))
stmt.SetFloat("$base_power", float64(c.RaceParam.BasePow))
stmt.SetFloat("$base_guts", float64(c.RaceParam.BaseGuts))
stmt.SetFloat("$base_wit", float64(c.RaceParam.BaseWiz))
stmt.SetInt64("$popularity", int64(c.Popularity))
stmt.SetInt64("$popularity_rank_left", int64(c.PopularityRankLeft))
stmt.SetInt64("$popularity_rank_center", int64(c.PopularityRankCenter))
stmt.SetInt64("$popularity_rank_right", int64(c.PopularityRankRight))
stmt.SetFloat("$start_delay", float64(simdata.HorseResult[i].StartDelayTime))
stmt.SetFloat("$spurt_start_distance", float64(simdata.HorseResult[i].LastSpurtStartDistance))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race horses: %w", err)
}
}
return nil
}
func insertRaceFrames(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
const sql = `INSERT INTO race_frame VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
stmt := conn.Prep(sql)
stmt.BindInt64(1, raceID)
for _, f := range simdata.Frame {
log.DebugContext(ctx, "frame", slog.Float64("time", float64(f.Time)))
stmt.BindFloat(2, float64(f.Time))
for i, h := range f.HorseFrame {
stmt.BindInt64(3, int64(i))
stmt.BindFloat(4, float64(h.Distance))
stmt.BindInt64(5, int64(h.LanePosition))
stmt.BindInt64(6, int64(h.Speed))
stmt.BindInt64(7, int64(h.HP))
stmt.BindInt64(8, int64(h.TemptationMode))
stmt.BindInt64(9, int64(h.BlockFrontHorseIndex))
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race frames: %w", err)
}
}
}
return nil
}
func insertRaceEvents(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
const sql = `INSERT INTO race_event VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
stmt := conn.Prep(sql)
for _, e := range simdata.Event {
log.DebugContext(ctx, "event", slog.Float64("time", float64(e.Event.FrameTime)), slog.Int("type", int(e.Event.Type)))
// Easier to clear all bindings every time now
// than to bind nulls to all the parameters that need them below.
stmt.ClearBindings()
stmt.BindInt64(1, raceID)
stmt.BindFloat(2, float64(e.Event.FrameTime))
stmt.BindInt64(3, int64(e.Event.Type))
for i, p := range e.Event.Param {
stmt.BindInt64(4+i, int64(p))
}
_, err := stmt.Step()
err = errors.Join(err, stmt.Reset())
if err != nil {
return fmt.Errorf("inserting race events: %w", err)
}
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
INSERT OR ROLLBACK INTO race (
config_id,
seed,
distance_diff_max,
frame_count,
horse_1,
horse_2,
horse_3,
horse_4,
horse_5,
horse_6,
horse_7,
horse_8,
horse_9,
horse_10,
horse_11,
horse_12,
horse_13,
horse_14,
horse_15,
horse_16,
horse_17,
horse_18
)
VALUES (
$config_id,
$seed,
$distance_diff_max,
$frame_count,
$horse_1,
$horse_2,
$horse_3,
$horse_4,
$horse_5,
$horse_6,
$horse_7,
$horse_8,
$horse_9,
$horse_10,
$horse_11,
$horse_12,
$horse_13,
$horse_14,
$horse_15,
$horse_16,
$horse_17,
$horse_18
)
RETURNING id
+4
View File
@@ -0,0 +1,4 @@
INSERT INTO race_config (type, course_set_id, ground_condition, weather, season, time, num_horse)
VALUES ($type, $course_set_id, $ground_condition, $weather, $season, $time, $num_horse)
ON CONFLICT DO UPDATE SET id=id
RETURNING id
+12
View File
@@ -0,0 +1,12 @@
INSERT INTO race_horse
VALUES (
$race, $horse,
$horse_index, $post,
$finish_order, $finish_time_raw, $finish_time_scaled, $finish_diff_time_from_prev,
$running_style, $running_style_ex,
$active_distance_aptitude, $active_surface_aptitude,
$mood, $mood_coef,
$base_speed, $base_stamina, $base_power, $base_guts, $base_wit,
$popularity, $popularity_rank_left, $popularity_rank_center, $popularity_rank_right,
$start_delay, $spurt_start_distance
)
+19
View File
@@ -0,0 +1,19 @@
INSERT INTO trained_chara (
trainer, trainer_id, trained_chara_id,
chara_id, chara_name, card_id, rarity, mob_id,
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
turf, dirt,
sprint, mile, medium, long,
front_runner, pace_chaser, late_surger, end_closer,
fans, rank, rank_score
) VALUES (
$trainer, $trainer_id, $trained_chara_id,
$chara_id, $chara_name, $card_id, $rarity, $mob_id,
$raw_speed, $raw_stamina, $raw_power, $raw_guts, $raw_wit,
$turf, $dirt,
$sprint, $mile, $medium, $long,
$front_runner, $pace_chaser, $late_surger, $end_closer,
$fans, $rank, $rank_score
)
ON CONFLICT DO UPDATE SET id=id
RETURNING id
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"encoding/json"
"flag"
"log/slog"
"os"
"os/signal"
"path/filepath"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
func main() {
var (
uri, journalMode string
fs []string
)
flag.StringVar(&uri, "db", "race.sqlite3", "output database `URI` or filename")
flag.StringVar(&journalMode, "journal", "memory", "journal `mode` to apply to database connections")
flag.Func("d", "`dir`ectory containing race dumps to load (repeatable)", func(s string) error {
es, err := os.ReadDir(s)
for _, e := range es {
if e.IsDir() {
continue
}
fs = append(fs, filepath.Join(s, e.Name()))
}
return err
})
flag.Parse()
fs = append(fs, flag.Args()...)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
db := opendb(uri, journalMode)
ok := 0
for _, n := range fs {
log := slog.With(slog.String("file", n))
log.Info("begin")
f, err := os.Open(n)
if err != nil {
log.Error("open", slog.Any("err", err))
continue
}
var race replay.Race
d := json.NewDecoder(f)
d.DisallowUnknownFields()
log.Debug("decode")
if err := d.Decode(&race); err != nil {
log.Error("load", slog.Any("err", err))
continue
}
log.Debug("simdata")
simdata, err := replay.DecodeSim(race.SimDataBase64)
if err != nil {
log.Error("simdata", slog.Any("err", err))
continue
}
log.Debug("insert")
err = insert(ctx, db, log, &race, simdata)
if err != nil {
log.Error("insert", slog.Any("err", err))
continue
}
ok++
}
slog.Info("done", slog.Int("races", ok))
}
+234
View File
@@ -0,0 +1,234 @@
-- trained_chara lists horses who appear in races.
CREATE TABLE IF NOT EXISTS trained_chara (
-- id is the DB-internal ID of the Uma.
-- It does not come from the game and should not be supplied in DDL.
id INTEGER PRIMARY KEY,
trainer TEXT NOT NULL,
trainer_id INTEGER NOT NULL, -- 0 for NPC umas e.g. in practice races
trained_chara_id INTEGER NOT NULL,
chara_id INTEGER NOT NULL,
chara_name TEXT NOT NULL,
-- card_id is the Uma herself, e.g. 100201 for [Innocent Silence] Silence Suzuka.
card_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
mob_id INTEGER NOT NULL, -- 0 for Umas that are not mobs
-- Raw stats.
raw_speed INTEGER NOT NULL,
raw_stamina INTEGER NOT NULL,
raw_power INTEGER NOT NULL,
raw_guts INTEGER NOT NULL,
raw_wit INTEGER NOT NULL,
-- Aptitudes.
turf INTEGER NOT NULL,
dirt INTEGER NOT NULL,
sprint INTEGER NOT NULL,
mile INTEGER NOT NULL,
medium INTEGER NOT NULL,
long INTEGER NOT NULL,
front_runner INTEGER NOT NULL,
pace_chaser INTEGER NOT NULL,
late_surger INTEGER NOT NULL,
end_closer INTEGER NOT NULL,
-- Other data.
fans INTEGER NOT NULL,
rank INTEGER NOT NULL,
rank_score INTEGER NOT NULL,
UNIQUE (trainer_id, trained_chara_id)
) STRICT;
-- Insert trained chara ID 0 to represent no Uma.
INSERT OR IGNORE INTO trained_chara (
id,
trainer,
trainer_id,
trained_chara_id,
chara_id, chara_name,
card_id,
mob_id,
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
turf, dirt,
sprint, mile, medium, long,
front_runner, pace_chaser, late_surger, end_closer,
fans,
rank, rank_score
)
VALUES (0, '', 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
-- trained_chara_skill lists the skills each observed horse owns.
CREATE TABLE IF NOT EXISTS trained_chara_skill (
trained_chara INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
UNIQUE (trained_chara, skill_id)
) STRICT;
CREATE INDEX IF NOT EXISTS trained_chara_skill_by_trained_chara ON trained_chara_skill (trained_chara);
-- race_course_set is the accumulated information of observed race courses.
CREATE TABLE IF NOT EXISTS race_course_set (
id INTEGER PRIMARY KEY,
race_track_id INTEGER NOT NULL,
distance INTEGER NOT NULL,
ground INTEGER NOT NULL,
inout INTEGER NOT NULL,
turn INTEGER NOT NULL,
fence_set INTEGER NOT NULL,
float_lane_max INTEGER NOT NULL,
course_set_status_id INTEGER NOT NULL,
finish_time_min INTEGER NOT NULL,
finish_time_min_random_range INTEGER NOT NULL,
finish_time_max INTEGER NOT NULL,
finish_time_max_random_range INTEGER NOT NULL,
section_distance REAL NOT NULL,
distance_type INTEGER NOT NULL,
furlong_number INTEGER NOT NULL,
base_speed REAL NOT NULL
) STRICT;
-- race_config is observed configurations of races including the race type.
CREATE TABLE IF NOT EXISTS race_config (
-- id is the DB-internal ID of the race config.
id INTEGER PRIMARY KEY,
type INTEGER NOT NULL,
course_set_id INTEGER NOT NULL REFERENCES race_course_set (id) ON DELETE RESTRICT,
ground_condition INTEGER NOT NULL,
weather INTEGER NOT NULL,
season INTEGER NOT NULL,
time INTEGER NOT NULL,
num_horse INTEGER NOT NULL,
UNIQUE (type, course_set_id, ground_condition, weather, season, time, num_horse)
) STRICT;
CREATE INDEX IF NOT EXISTS race_config_by_type ON race_config (type);
-- race is all observed races.
CREATE TABLE IF NOT EXISTS race (
-- id is the DB-internal ID of the race.
id INTEGER PRIMARY KEY,
config_id INTEGER NOT NULL REFERENCES race_config (id),
seed INTEGER NOT NULL,
distance_diff_max REAL NOT NULL,
frame_count INTEGER NOT NULL,
-- There is no consistent ID for races, and even race seeds have been found
-- to collide well above the expected rate for a uniform distribution.
-- In order to prevent adding duplicates of races each time the tool is run,
-- we include the list of all horse IDs present in the race.
-- Analysis on the horses in the race should instead use the race_horse table.
-- Horse entries here are 0 where there is actually no horse at that gate.
horse_1 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_2 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_3 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_4 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_5 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_6 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_7 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_8 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_9 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_10 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_11 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_12 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_13 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_14 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_15 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_16 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_17 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_18 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
UNIQUE (
config_id,
seed,
horse_1,
horse_2,
horse_3,
horse_4,
horse_5,
horse_6,
horse_7,
horse_8,
horse_9,
horse_10,
horse_11,
horse_12,
horse_13,
horse_14,
horse_15,
horse_16,
horse_17,
horse_18
)
) STRICT;
CREATE INDEX IF NOT EXISTS race_by_config ON race (config_id);
-- race_horse pairs each instance of a horse with its condition and performance
-- in a given race.
CREATE TABLE IF NOT EXISTS race_horse (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
horse INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_index INTEGER NOT NULL,
post INTEGER NOT NULL,
finish_order INTEGER NOT NULL,
finish_time_raw REAL NOT NULL,
finish_time_scaled REAL NOT NULL,
finish_diff_time_from_prev REAL NOT NULL,
running_style INTEGER NOT NULL,
running_style_ex INTEGER NOT NULL,
active_distance_aptitude INTEGER NOT NULL,
active_surface_aptitude INTEGER NOT NULL,
mood INTEGER NOT NULL,
mood_coef REAL NOT NULL,
base_speed REAL NOT NULL,
base_stamina REAL NOT NULL,
base_power REAL NOT NULL,
base_guts REAL NOT NULL,
base_wit REAL NOT NULL,
popularity INTEGER NOT NULL,
popularity_rank_left INTEGER NOT NULL,
popularity_rank_center INTEGER NOT NULL,
popularity_rank_right INTEGER NOT NULL,
start_delay REAL NOT NULL,
spurt_start_distance REAL NOT NULL,
PRIMARY KEY (race, horse),
UNIQUE (race, horse_index),
CHECK (horse != 0)
) STRICT;
-- race_frame holds the actual received frames.
CREATE TABLE IF NOT EXISTS race_frame (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
frame_time REAL NOT NULL,
horse_index INTEGER NOT NULL,
distance REAL NOT NULL,
lane INTEGER NOT NULL, -- scale -4
speed INTEGER NOT NULL, -- scale -2
hp INTEGER NOT NULL,
temptation INTEGER NOT NULL,
block_front_horse_index INTEGER NOT NULL,
PRIMARY KEY (race, frame_time, horse_index)
) STRICT;
-- race_event holds raw received race events.
-- Views such as race_event_skill_activate, race_event_spot_struggle_activate, &c.
-- interpret events per type.
CREATE TABLE IF NOT EXISTS race_event (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
frame_time REAL NOT NULL,
type INTEGER NOT NULL,
param_0 INTEGER,
param_1 INTEGER,
param_2 INTEGER,
param_3 INTEGER,
param_4 INTEGER,
param_5 INTEGER
) STRICT;
CREATE INDEX IF NOT EXISTS race_event_by_race ON race_event (race);
CREATE INDEX IF NOT EXISTS race_event_by_type ON race_event (type);
+36
View File
@@ -0,0 +1,36 @@
DROP VIEW IF EXISTS race_event_skill_activate;
DROP VIEW IF EXISTS race_event_spot_struggle_activate;
DROP VIEW IF EXISTS race_event_duel_activate;
DROP VIEW IF EXISTS race_event_release_conserve_power;
CREATE VIEW race_event_skill_activate AS
SELECT
race,
frame_time,
param_0 AS user_index,
param_1 AS skill,
param_2 * 0.0001 AS duration,
param_3 AS branch,
param_4 AS hit_bitmask
FROM race_event WHERE type = 3;
CREATE VIEW race_event_spot_struggle_activate AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 4;
CREATE VIEW race_event_duel_activate AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 5;
CREATE VIEW race_event_release_conserve_power AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 6;
+100 -4
View File
@@ -1,6 +1,9 @@
package ism package ism
import "git.sunturtle.xyz/zephyr/horse" import (
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump"
)
type Skill struct { type Skill struct {
ID horse.SkillID `json:"skill_id"` ID horse.SkillID `json:"skill_id"`
@@ -225,13 +228,106 @@ type AddItem struct {
Number int32 `json:"number"` 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 { 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 { type End struct {
Chara SingleModeChara `json:"chara_info"` Chara SingleModeChara `json:"chara_info"`
RewardSummary RewardSummary `json:"reward_summary_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"`
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 { type ProgressInfo struct {
+13
View File
@@ -0,0 +1,13 @@
package umadump
type Never struct{}
func (Never) AppendText(b []byte) ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) MarshalText() ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) UnmarshalText(b []byte) error {
panic("umadump: attempt to decode value that should never exist")
}
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type CardRarity -linecomment -trimprefix Card"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CardNone-0]
_ = x[CardRare1-1]
_ = x[CardRare2-2]
_ = x[CardRare3-3]
_ = x[CardRare4-4]
_ = x[CardRare5-5]
}
const _CardRarity_name = "NONERare1Rare2Rare3Rare4Rare5"
var _CardRarity_index = [...]uint8{0, 4, 9, 14, 19, 24, 29}
func (i CardRarity) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CardRarity_index)-1 {
return "CardRarity(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CardRarity_name[_CardRarity_index[idx]:_CardRarity_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type Motivation -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MotivationNone-0]
_ = x[MotivationAwful-1]
_ = x[MotivationBad-2]
_ = x[MotivationNormal-3]
_ = x[MotivationGood-4]
_ = x[MotivationGreat-5]
}
const _Motivation_name = "NoneMinLowMiddleHighMax"
var _Motivation_index = [...]uint8{0, 4, 7, 10, 16, 20, 23}
func (i Motivation) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Motivation_index)-1 {
return "Motivation(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Motivation_name[_Motivation_index[idx]:_Motivation_index[idx+1]]
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by "stringer -type ProperGrade -trimprefix Aptitude"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[AptitudeNull-0]
_ = x[AptitudeG-1]
_ = x[AptitudeF-2]
_ = x[AptitudeE-3]
_ = x[AptitudeD-4]
_ = x[AptitudeC-5]
_ = x[AptitudeB-6]
_ = x[AptitudeA-7]
_ = x[AptitudeS-8]
}
const _ProperGrade_name = "NullGFEDCBAS"
var _ProperGrade_index = [...]uint8{0, 4, 5, 6, 7, 8, 9, 10, 11, 12}
func (i ProperGrade) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_ProperGrade_index)-1 {
return "ProperGrade(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ProperGrade_name[_ProperGrade_index[idx]:_ProperGrade_index[idx+1]]
}
+356 -268
View File
@@ -6,6 +6,7 @@ import (
"sync" "sync"
"git.sunturtle.xyz/zephyr/horse" "git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump"
) )
type Race struct { type Race struct {
@@ -46,10 +47,10 @@ type Race struct {
PostNumberMax int32 `json:"postNumberMax"` PostNumberMax int32 `json:"postNumberMax"`
PlayerHorseIndex int32 `json:"playerHorseIndex"` PlayerHorseIndex int32 `json:"playerHorseIndex"`
OverridePlayerHorseIndex int32 `json:"overridePlayerHorseIndex"` OverridePlayerHorseIndex int32 `json:"overridePlayerHorseIndex"`
PlayerTeamMemberArray []RaceHorse `json:"playerTeamMemberArray"` PlayerTeamMemberArray []HorseData `json:"playerTeamMemberArray"`
PlayerTeamTopFinishOrderHorse RaceHorse `json:"playerTeamTopFinishOrderHorse"` PlayerTeamTopFinishOrderHorse HorseData `json:"playerTeamTopFinishOrderHorse"`
IsGateInPopularityInitialized bool `json:"isGateInPopularityInitialized"` IsGateInPopularityInitialized bool `json:"isGateInPopularityInitialized"`
RaceHorse []RaceHorse `json:"raceHorse"` RaceHorse []HorseData `json:"raceHorse"`
RaceBibMaster struct{} `json:"raceBibMaster"` RaceBibMaster struct{} `json:"raceBibMaster"`
RaceMaster struct{} `json:"raceMaster"` RaceMaster struct{} `json:"raceMaster"`
RaceInstanceMaster struct{} `json:"raceInstanceMaster"` RaceInstanceMaster struct{} `json:"raceInstanceMaster"`
@@ -64,7 +65,7 @@ type Race struct {
ReplayCheckInfoLegend struct{} `json:"replayCheckInfoLegend"` ReplayCheckInfoLegend struct{} `json:"replayCheckInfoLegend"`
IsDailyLegendRace bool `json:"isDailyLegendRace"` IsDailyLegendRace bool `json:"isDailyLegendRace"`
ReplayCheckInfoChallengeMatch struct{} `json:"replayCheckInfoChallengeMatch"` ReplayCheckInfoChallengeMatch struct{} `json:"replayCheckInfoChallengeMatch"`
RaceRewardSingle RaceRewardSingle `json:"raceRewardSingle"` RaceRewardSingle struct{} `json:"raceRewardSingle"`
ResultHorseIndex int32 `json:"resultHorseIndex"` ResultHorseIndex int32 `json:"resultHorseIndex"`
PrevGradeType CharaGradeType `json:"prevGradeType"` PrevGradeType CharaGradeType `json:"prevGradeType"`
MainStoryRaceGimmickType MainStoryRaceGimmickType `json:"mainStoryRaceGimmickType"` MainStoryRaceGimmickType MainStoryRaceGimmickType `json:"mainStoryRaceGimmickType"`
@@ -537,122 +538,335 @@ func (r *RaceDifficulty) UnmarshalText(b []byte) error {
} }
type RaceParam struct { type RaceParam struct {
RawSpeed int `json:"rawSpeed"` RawSpeed int `json:"rawSpeed"`
RawStamina int `json:"rawStamina"` RawStamina int `json:"rawStamina"`
RawPow int `json:"rawPow"` RawPow int `json:"rawPow"`
RawGuts int `json:"rawGuts"` RawGuts int `json:"rawGuts"`
RawWiz int `json:"rawWiz"` RawWiz int `json:"rawWiz"`
BaseSpeed float32 `json:"baseSpeed"` BaseSpeed float32 `json:"baseSpeed"`
BaseStamina float32 `json:"baseStamina"` BaseStamina float32 `json:"baseStamina"`
BasePow float32 `json:"basePow"` BasePow float32 `json:"basePow"`
BaseGuts float32 `json:"baseGuts"` BaseGuts float32 `json:"baseGuts"`
BaseWiz float32 `json:"baseWiz"` BaseWiz float32 `json:"baseWiz"`
Motivation string `json:"motivation"` Motivation Motivation `json:"motivation"`
MotivationCoef float32 `json:"motivationCoef"` MotivationCoef float32 `json:"motivationCoef"`
}
type Motivation int32
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type Motivation -linecomment
const (
MotivationNone Motivation = iota // None
MotivationAwful // Min
MotivationBad // Low
MotivationNormal // Middle
MotivationGood // High
MotivationGreat // Max
)
func (r Motivation) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
}
func (r Motivation) MarshalText() ([]byte, error) {
return r.AppendText(nil)
}
func (r *Motivation) UnmarshalText(b []byte) error {
switch string(b) {
case "Min":
*r = MotivationAwful
case "Low":
*r = MotivationBad
case "Middle":
*r = MotivationNormal
case "High":
*r = MotivationGood
case "Max":
*r = MotivationGreat
case "None":
*r = MotivationNone
default:
*r = 0
return fmt.Errorf("unknown motivation %q", b)
}
return nil
} }
type SkillArray struct { type SkillArray struct {
SkillID int `json:"skill_id"` SkillID horse.SkillID `json:"skill_id"`
Level int `json:"level"` Level int32 `json:"level"`
} }
type RaceResultArray struct { type RaceResult struct {
Turn int `json:"turn"` Turn int32 `json:"turn"`
ProgramID int `json:"program_id"` ProgramID int32 `json:"program_id"`
Weather int `json:"weather"` Weather int32 `json:"weather"`
GroundCondition int `json:"ground_condition"` GroundCondition int32 `json:"ground_condition"`
RunningStyle int `json:"running_style"` RunningStyle int32 `json:"running_style"`
Popularity int `json:"popularity"` Popularity int32 `json:"popularity"`
ResultRank int `json:"result_rank"` ResultRank int32 `json:"result_rank"`
ResultTime int `json:"result_time"` ResultTime int32 `json:"result_time"`
PrizeMoney int `json:"prize_money"` PrizeMoney int32 `json:"prize_money"`
} }
type ResponseHorseData struct {
FrameOrder int `json:"frame_order"` type RaceHorseData struct {
ViewerID int64 `json:"viewer_id"` FrameOrder int32 `json:"frame_order"`
TrainerName string `json:"trainer_name"` ViewerID int64 `json:"viewer_id"`
OwnerViewerID int `json:"owner_viewer_id"` TrainerName *string `json:"trainer_name"`
OwnerTrainerName string `json:"owner_trainer_name"` OwnerViewerID int64 `json:"owner_viewer_id"`
SingleModeCharaID int `json:"single_mode_chara_id"` OwnerTrainerName string `json:"owner_trainer_name"`
TrainedCharaID int `json:"trained_chara_id"` SingleModeCharaID int32 `json:"single_mode_chara_id"`
NicknameID int `json:"nickname_id"` TrainedCharaID int32 `json:"trained_chara_id"`
CharaID int `json:"chara_id"` NicknameID int32 `json:"nickname_id"`
CardID int `json:"card_id"` CharaID int32 `json:"chara_id"`
MobID int `json:"mob_id"` CardID int32 `json:"card_id"`
Rarity int `json:"rarity"` MobID int32 `json:"mob_id"`
TalentLevel int `json:"talent_level"` Rarity int32 `json:"rarity"`
SkillArray []SkillArray `json:"skill_array"` TalentLevel int32 `json:"talent_level"`
Stamina int `json:"stamina"` SkillArray []SkillArray `json:"skill_array"`
Speed int `json:"speed"` Stamina int32 `json:"stamina"`
Pow int `json:"pow"` Speed int32 `json:"speed"`
Guts int `json:"guts"` Pow int32 `json:"pow"`
Wiz int `json:"wiz"` Guts int32 `json:"guts"`
RunningStyle int `json:"running_style"` Wiz int32 `json:"wiz"`
RaceDressID int `json:"race_dress_id"` RunningStyle int32 `json:"running_style"`
CharaColorType int `json:"chara_color_type"` RaceDressID int32 `json:"race_dress_id"`
NpcType int `json:"npc_type"` CharaColorType int32 `json:"chara_color_type"`
FinalGrade int `json:"final_grade"` NpcType int32 `json:"npc_type"`
Popularity int `json:"popularity"` FinalGrade int32 `json:"final_grade"`
PopularityMarkRankArray []int `json:"popularity_mark_rank_array"` Popularity int32 `json:"popularity"`
ProperDistanceShort int `json:"proper_distance_short"` PopularityMarkRankArray []int32 `json:"popularity_mark_rank_array"`
ProperDistanceMile int `json:"proper_distance_mile"` ProperDistanceShort int32 `json:"proper_distance_short"`
ProperDistanceMiddle int `json:"proper_distance_middle"` ProperDistanceMile int32 `json:"proper_distance_mile"`
ProperDistanceLong int `json:"proper_distance_long"` ProperDistanceMiddle int32 `json:"proper_distance_middle"`
ProperRunningStyleNige int `json:"proper_running_style_nige"` ProperDistanceLong int32 `json:"proper_distance_long"`
ProperRunningStyleSenko int `json:"proper_running_style_senko"` ProperRunningStyleNige int32 `json:"proper_running_style_nige"`
ProperRunningStyleSashi int `json:"proper_running_style_sashi"` ProperRunningStyleSenko int32 `json:"proper_running_style_senko"`
ProperRunningStyleOikomi int `json:"proper_running_style_oikomi"` ProperRunningStyleSashi int32 `json:"proper_running_style_sashi"`
ProperGroundTurf int `json:"proper_ground_turf"` ProperRunningStyleOikomi int32 `json:"proper_running_style_oikomi"`
ProperGroundDirt int `json:"proper_ground_dirt"` ProperGroundTurf int32 `json:"proper_ground_turf"`
Motivation int `json:"motivation"` ProperGroundDirt int32 `json:"proper_ground_dirt"`
WinSaddleIDArray []int `json:"win_saddle_id_array"` Motivation int32 `json:"motivation"`
RaceResultArray []RaceResultArray `json:"race_result_array"` WinSaddleIDArray []int32 `json:"win_saddle_id_array"`
TeamID int `json:"team_id"` RaceResultArray []RaceResult `json:"race_result_array"`
TeamMemberID int `json:"team_member_id"` TeamID int32 `json:"team_id"`
TeamRank int `json:"team_rank"` TeamMemberID int32 `json:"team_member_id"`
SingleModeWinCount int `json:"single_mode_win_count"` TeamRank int32 `json:"team_rank"`
ItemIDArray []any `json:"item_id_array"` SingleModeWinCount int32 `json:"single_mode_win_count"`
MotivationChangeFlag int `json:"motivation_change_flag"` FanCount int32 `json:"fan_count"`
FrameOrderChangeFlag int `json:"frame_order_change_flag"` ItemIDArray []int32 `json:"item_id_array"`
MotivationChangeFlag int32 `json:"motivation_change_flag"`
FrameOrderChangeFlag int32 `json:"frame_order_change_flag"`
} }
type RaceRecord struct {
type HorseData struct {
HorseIndex int32 `json:"horseIndex"`
PostNumber int32 `json:"postNumber"`
CharaID horse.CharacterID `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int32 `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData RaceHorseData `json:"responseHorseData"`
Popularity int32 `json:"popularity"`
PopularityRankLeft int8 `json:"popularityRankLeft"`
PopularityRankCenter int8 `json:"popularityRankCenter"`
PopularityRankRight int8 `json:"popularityRankRight"`
GateInPopularity int8 `json:"gateInPopularity"`
Rarity CardRarity `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx RunningStyleEx `json:"runningStyleEx"`
Defeat string `json:"defeat"` // this is an enum, but not worth the effort
RaceDressID int32 `json:"raceDressId"`
RaceDressIDWithOption int32 `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"` // enum not worth
ActiveProperDistance ProperGrade `json:"activeProperDistance"`
ActiveProperGroundType ProperGrade `json:"activeProperGroundType"`
MobID int32 `json:"mobId"`
RaceRecord struct{} `json:"raceRecord"`
FinishOrderRawScore int32 `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData"`
} }
type FactorDataArray struct {
FactorLv int `json:"factorLv"` type RunningStyleEx int8
FactorID int `json:"factorId"`
BaseFactorID int `json:"baseFactorId"` //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type RunningStyleEx -linecomment
UpgradeHistoryList []any `json:"upgradeHistoryList"` const (
StyleExNone RunningStyleEx = iota // NONE
StyleExRunaway // Oonige
)
func (r RunningStyleEx) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
} }
type FavoriteData struct {
func (r RunningStyleEx) MarshalText() ([]byte, error) {
return r.AppendText(nil)
} }
func (r *RunningStyleEx) UnmarshalText(b []byte) error {
switch string(b) {
case "NONE":
*r = StyleExNone
case "Oonige":
*r = StyleExRunaway
default:
*r = 0
return fmt.Errorf("unknown extra running style %q", b)
}
return nil
}
type ProperGrade int8
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type ProperGrade -trimprefix Aptitude
const (
AptitudeNull ProperGrade = iota
AptitudeG
AptitudeF
AptitudeE
AptitudeD
AptitudeC
AptitudeB
AptitudeA
AptitudeS
)
func (r ProperGrade) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
}
func (r ProperGrade) MarshalText() ([]byte, error) {
return r.AppendText(nil)
}
func (r *ProperGrade) UnmarshalText(b []byte) error {
switch string(b) {
case "Null":
*r = AptitudeNull
case "G":
*r = AptitudeG
case "F":
*r = AptitudeF
case "E":
*r = AptitudeE
case "D":
*r = AptitudeD
case "C":
*r = AptitudeC
case "B":
*r = AptitudeB
case "A":
*r = AptitudeA
case "S":
*r = AptitudeS
default:
*r = 0
return fmt.Errorf("unknown motivation %q", b)
}
return nil
}
type TrainedCharaData struct {
ID int32 `json:"id"`
IsSaved bool `json:"isSaved"`
ViewerID int64 `json:"viewerId"`
OwnerViewerID int64 `json:"ownerViewerId"`
OwnerTrainedCharaID int32 `json:"ownerTrainedCharaId"`
UseType int32 `json:"useType"`
CardID horse.UmaID `json:"cardId"`
NickNameID int32 `json:"nickNameId"`
NickNameIDArray []int32 `json:"nickNameIdArray"`
Stamina int32 `json:"stamina"`
Speed int32 `json:"speed"`
Power int32 `json:"power"`
Guts int32 `json:"guts"`
Wiz int32 `json:"wiz"`
Fans int32 `json:"fans"`
Rank int32 `json:"rank"`
RankScore int32 `json:"rankScore"`
RunningStyle int32 `json:"runningStyle"`
ProperGroundTurf int32 `json:"properGroundTurf"`
ProperGroundDirt int32 `json:"properGroundDirt"`
ProperDistanceShort int32 `json:"properDistanceShort"`
ProperDistanceMile int32 `json:"properDistanceMile"`
ProperDistanceMiddle int32 `json:"properDistanceMiddle"`
ProperDistanceLong int32 `json:"properDistanceLong"`
ProperRunningStyleNige int32 `json:"properRunningStyleNige"`
ProperRunningStyleSenko int32 `json:"properRunningStyleSenko"`
ProperRunningStyleSashi int32 `json:"properRunningStyleSashi"`
ProperRunningStyleOikomi int32 `json:"properRunningStyleOikomi"`
SuccessionCount int32 `json:"successionCount"`
FactorDataArray []FactorData `json:"factorDataArray"`
CreateTime string `json:"createTime"`
ScenarioID horse.ScenarioID `json:"scenarioId"`
TalentLevel int32 `json:"talentLevel"`
CharaGrade int32 `json:"charaGrade"`
Rarity int32 `json:"rarity"`
IsLock bool `json:"isLock"`
FavoriteData struct{} `json:"favoriteData"`
CachedCreateTimeTimeStamp int64 `json:"cachedCreateTimeTimeStamp"`
SortedFactorList []umadump.Never `json:"sortedFactorList"`
SortedFactorProfileCardList []umadump.Never `json:"sortedFactorProfileCardList"`
FactorListIncludingSuccession []umadump.Never `json:"factorListIncludingSuccession"`
SuccessionCharaList []SuccessionCharaList `json:"successionCharaList"`
IsSuccessionHistoryInitialized bool `json:"isSuccessionHistoryInitialized"`
SuccessionHistoryList []umadump.Never `json:"successionHistoryList"`
AcquiredSkillArray []AcquiredSkill `json:"acquiredSkillArray"`
SupportCardArray []SupportCard `json:"supportCardArray"`
SingleModeRaceResultArray []SingleModeRaceResult `json:"singleModeRaceResultArray"`
WinSaddleArray []umadump.Never `json:"winSaddleArray"`
WinSaddleIDArray []horse.SaddleID `json:"winSaddleIdArray"`
CacheCharaID int32 `json:"cacheCharaId"`
MasterCardData struct{} `json:"masterCardData"`
MasterCharaData struct{} `json:"masterCharaData"`
MasterCardRarityData struct{} `json:"masterCardRarityData"`
SingleTotalRaceNum int32 `json:"singleTotalRaceNum"`
SingleWinNum int32 `json:"singleWinNum"`
TrainedCharaDataAccessor struct{} `json:"trainedCharaDataAccessor"`
}
type FactorData struct {
FactorLv int32 `json:"factorLv"`
FactorID horse.SparkID `json:"factorId"`
BaseFactorID horse.SparkID `json:"baseFactorId"`
UpgradeHistoryList []umadump.Never `json:"upgradeHistoryList"`
}
type SuccessionCharaList struct { type SuccessionCharaList struct {
PositionID int `json:"positionId"` PositionID int32 `json:"positionId"`
CardID int `json:"cardId"` CardID int32 `json:"cardId"`
Rarity int `json:"rarity"` Rarity int32 `json:"rarity"`
Level int `json:"level"` Level int32 `json:"level"`
Rank int `json:"rank"` Rank int32 `json:"rank"`
FactorDataArray []FactorDataArray `json:"factorDataArray"` FactorDataArray []FactorData `json:"factorDataArray"`
SortedFactorList []any `json:"sortedFactorList"` SortedFactorList []umadump.Never `json:"sortedFactorList"`
SortedFactorListForProfileCard []any `json:"sortedFactorListForProfileCard"` SortedFactorListForProfileCard []umadump.Never `json:"sortedFactorListForProfileCard"`
OwnerViewerID int `json:"ownerViewerId"` OwnerViewerID int64 `json:"ownerViewerId"`
IsPlayer bool `json:"isPlayer"` IsPlayer bool `json:"isPlayer"`
WinSaddleArray []any `json:"winSaddleArray"` WinSaddleArray []umadump.Never `json:"winSaddleArray"`
WinSaddleIDArray []int `json:"winSaddleIdArray"` WinSaddleIDArray []int32 `json:"winSaddleIdArray"`
} }
type Master struct {
type AcquiredSkill struct {
MasterID horse.SkillID `json:"masterId"`
Level int `json:"level"`
Master struct{} `json:"master"`
} }
type AcquiredSkillArray struct {
MasterID int `json:"masterId"` type SupportCard struct {
Level int `json:"level"` Position int32 `json:"position"`
Master Master `json:"master"` SupportCardID horse.SupportCardID `json:"supportCardId"`
LimitBreakCount int32 `json:"limitBreakCount"`
Exp int32 `json:"exp"`
} }
type SupportCardArray struct {
Position int `json:"position"` type SingleModeRaceResult struct {
SupportCardID int `json:"supportCardId"`
LimitBreakCount int `json:"limitBreakCount"`
Exp int `json:"exp"`
}
type SingleModeRaceResultArray struct {
Turn int `json:"turn"` Turn int `json:"turn"`
ProgramID int `json:"programId"` ProgramID int `json:"programId"`
RaceInstanceID int `json:"raceInstanceId"` RaceInstanceID int `json:"raceInstanceId"`
@@ -664,172 +878,46 @@ type SingleModeRaceResultArray struct {
ResultRank int `json:"resultRank"` ResultRank int `json:"resultRank"`
ScenarioID int `json:"scenarioId"` ScenarioID int `json:"scenarioId"`
} }
type MasterCardData struct {
} type CardRarity int8
type MasterCharaData struct {
} //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type CardRarity -linecomment -trimprefix Card
type MasterCardRarityData struct { const (
} CardNone CardRarity = iota // NONE
type TrainedCharaDataAccessor struct { CardRare1
} CardRare2
type TrainedCharaData struct { CardRare3
ID int `json:"id"` CardRare4
IsSaved bool `json:"isSaved"` CardRare5
ViewerID int64 `json:"viewerId"` )
OwnerViewerID int `json:"ownerViewerId"`
OwnerTrainedCharaID int `json:"ownerTrainedCharaId"` func (r CardRarity) AppendText(b []byte) ([]byte, error) {
UseType int `json:"useType"` return append(b, r.String()...), nil
CardID int `json:"cardId"`
NickNameID int `json:"nickNameId"`
NickNameIDArray []int `json:"nickNameIdArray"`
Stamina int `json:"stamina"`
Speed int `json:"speed"`
Power int `json:"power"`
Guts int `json:"guts"`
Wiz int `json:"wiz"`
Fans int `json:"fans"`
Rank int `json:"rank"`
RankScore int `json:"rankScore"`
RunningStyle int `json:"runningStyle"`
ProperGroundTurf int `json:"properGroundTurf"`
ProperGroundDirt int `json:"properGroundDirt"`
ProperDistanceShort int `json:"properDistanceShort"`
ProperDistanceMile int `json:"properDistanceMile"`
ProperDistanceMiddle int `json:"properDistanceMiddle"`
ProperDistanceLong int `json:"properDistanceLong"`
ProperRunningStyleNige int `json:"properRunningStyleNige"`
ProperRunningStyleSenko int `json:"properRunningStyleSenko"`
ProperRunningStyleSashi int `json:"properRunningStyleSashi"`
ProperRunningStyleOikomi int `json:"properRunningStyleOikomi"`
SuccessionCount int `json:"successionCount"`
FactorDataArray []FactorDataArray `json:"factorDataArray"`
CreateTime string `json:"createTime"`
ScenarioID int `json:"scenarioId"`
TalentLevel int `json:"talentLevel"`
CharaGrade int `json:"charaGrade"`
Rarity int `json:"rarity"`
IsLock bool `json:"isLock"`
FavoriteData FavoriteData `json:"favoriteData"`
CachedCreateTimeTimeStamp int `json:"cachedCreateTimeTimeStamp"`
SortedFactorList []any `json:"sortedFactorList"`
SortedFactorProfileCardList []any `json:"sortedFactorProfileCardList"`
FactorListIncludingSuccession []any `json:"factorListIncludingSuccession"`
SuccessionCharaList []SuccessionCharaList `json:"successionCharaList"`
IsSuccessionHistoryInitialized bool `json:"isSuccessionHistoryInitialized"`
SuccessionHistoryList []any `json:"successionHistoryList"`
AcquiredSkillArray []AcquiredSkillArray `json:"acquiredSkillArray"`
SupportCardArray []SupportCardArray `json:"supportCardArray"`
SingleModeRaceResultArray []SingleModeRaceResultArray `json:"singleModeRaceResultArray"`
WinSaddleArray []any `json:"winSaddleArray"`
WinSaddleIDArray []int `json:"winSaddleIdArray"`
CacheCharaID int `json:"cacheCharaId"`
MasterCardData MasterCardData `json:"masterCardData"`
MasterCharaData MasterCharaData `json:"masterCharaData"`
MasterCardRarityData MasterCardRarityData `json:"masterCardRarityData"`
SingleTotalRaceNum int `json:"singleTotalRaceNum"`
SingleWinNum int `json:"singleWinNum"`
TrainedCharaDataAccessor TrainedCharaDataAccessor `json:"trainedCharaDataAccessor"`
}
type PlayerTeamMember struct {
HorseIndex int `json:"horseIndex"`
PostNumber int `json:"postNumber"`
CharaID int `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData ResponseHorseData `json:"responseHorseData"`
Popularity int `json:"popularity"`
PopularityRankLeft int `json:"popularityRankLeft"`
PopularityRankCenter int `json:"popularityRankCenter"`
PopularityRankRight int `json:"popularityRankRight"`
GateInPopularity int `json:"gateInPopularity"`
Rarity string `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx string `json:"runningStyleEx"`
Defeat string `json:"defeat"`
RaceDressID int `json:"raceDressId"`
RaceDressIDWithOption int `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"`
ActiveProperDistance string `json:"activeProperDistance"`
ActiveProperGroundType string `json:"activeProperGroundType"`
MobID int `json:"mobId"`
RaceRecord RaceRecord `json:"raceRecord"`
FinishOrderRawScore int `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData"`
}
type PlayerTeamTopFinishOrderHorse struct {
HorseIndex int `json:"horseIndex"`
PostNumber int `json:"postNumber"`
CharaID int `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData ResponseHorseData `json:"responseHorseData"`
Popularity int `json:"popularity"`
PopularityRankLeft int `json:"popularityRankLeft"`
PopularityRankCenter int `json:"popularityRankCenter"`
PopularityRankRight int `json:"popularityRankRight"`
GateInPopularity int `json:"gateInPopularity"`
Rarity string `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx string `json:"runningStyleEx"`
Defeat string `json:"defeat"`
RaceDressID int `json:"raceDressId"`
RaceDressIDWithOption int `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"`
ActiveProperDistance string `json:"activeProperDistance"`
ActiveProperGroundType string `json:"activeProperGroundType"`
MobID int `json:"mobId"`
RaceRecord RaceRecord `json:"raceRecord"`
FinishOrderRawScore int `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData"`
} }
type RaceHorse struct { func (r CardRarity) MarshalText() ([]byte, error) {
HorseIndex int `json:"horseIndex"` return r.AppendText(nil)
PostNumber int `json:"postNumber"`
CharaID int `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData ResponseHorseData `json:"responseHorseData"`
Popularity int `json:"popularity"`
PopularityRankLeft int `json:"popularityRankLeft"`
PopularityRankCenter int `json:"popularityRankCenter"`
PopularityRankRight int `json:"popularityRankRight"`
GateInPopularity int `json:"gateInPopularity"`
Rarity string `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx string `json:"runningStyleEx"`
Defeat string `json:"defeat"`
RaceDressID int `json:"raceDressId"`
RaceDressIDWithOption int `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"`
ActiveProperDistance string `json:"activeProperDistance"`
ActiveProperGroundType string `json:"activeProperGroundType"`
MobID int `json:"mobId"`
RaceRecord RaceRecord `json:"raceRecord"`
FinishOrderRawScore int `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData,omitempty"`
} }
type RaceRewardSingle struct { func (r *CardRarity) UnmarshalText(b []byte) error {
// TODO(zeph): this switch string(b) {
case "NONE":
*r = CardNone
case "Rare1":
*r = CardRare1
case "Rare2":
*r = CardRare2
case "Rare3":
*r = CardRare3
case "Rare4":
*r = CardRare4
case "Rare5":
*r = CardRare5
default:
*r = 0
return fmt.Errorf("unknown card rarity %q", b)
}
return nil
} }
type CharaGradeType int8 type CharaGradeType int8
+182
View File
@@ -0,0 +1,182 @@
package replay_test
import (
_ "embed"
"encoding/json"
"strings"
"testing"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
//go:embed Mihono_Bourbon-74.2421s-20260829.json
var testRace string
func TestRaceUnmarshal(t *testing.T) {
// A replay is simply too large to bother converting by hand for a unit test.
// Just test that we can unmarshal and certain important fields are populated.
d := json.NewDecoder(strings.NewReader(testRace))
d.DisallowUnknownFields()
var got replay.Race
if err := d.Decode(&got); err != nil {
t.Error(err)
}
if got.RaceType != replay.RaceTypeChampions {
t.Errorf("wrong race type: want %v, got %v", replay.RaceTypeChampions, got.RaceType)
}
if got.RandomSeed != 1667907263 {
t.Errorf("wrong random seed: want 1667907263, got %d", got.RandomSeed)
}
if got.RaceCourseSet.ID != 10903 {
t.Errorf("wrong race course: want 10903, got %d", got.RaceCourseSet.ID)
}
if got.GroundCondition != replay.GroundFirm {
t.Errorf("wrong ground condition: want %v, got %v", replay.GroundFirm, got.GroundCondition)
}
if got.Weather != replay.WeatherCloudy {
t.Errorf("wrong weather: want %v, got %v", replay.WeatherCloudy, got.Weather)
}
if got.NumRaceHorses != 9 {
t.Errorf("wrong number of race horses: want 9, got %d", got.NumRaceHorses)
}
if len(got.PlayerTeamMemberArray) != 3 {
t.Errorf("wrong number of team members: want 3, got %d", len(got.PlayerTeamMemberArray))
}
for _, m := range got.PlayerTeamMemberArray {
if m.CharaID == 0 {
t.Error("missing team member chara id")
}
if m.FinishTimeRaw == 0 {
t.Error("missing team member finish time raw")
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing team member race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing team member race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing team member response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing team member response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing team member response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing team member response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing team member response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing team member response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing team member active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing team member active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing team member trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing team member trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing team member trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing team member trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing team member trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing team member trained chara acquired skill array")
}
}
if len(got.RaceHorse) != int(got.NumRaceHorses) {
t.Errorf("wrong number of race horses: want %d, got %d", got.NumRaceHorses, len(got.RaceHorse))
}
zeroFinish, zeroDelta := 0, 0
for _, m := range got.RaceHorse {
if m.CharaID == 0 {
t.Error("missing race horse chara id")
}
if m.FinishOrder == 0 {
zeroFinish++
}
if m.FinishTimeRaw == 0 {
t.Error("missing race horse finish time raw")
}
if m.FinishDiffTimeFromPrev == 0 {
zeroDelta++
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing race horse race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing race horse race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing race horse response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing race horse response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing race horse response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing race horse response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing race horse response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing race horse response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing race horse active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing race horse active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing race horse trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing race horse trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing race horse trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing race horse trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing race horse trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing race horse trained chara acquired skill array")
}
}
if zeroFinish != 1 {
t.Errorf("%d horses in index 0 finish", zeroFinish)
}
if zeroDelta != 1 {
t.Errorf("%d horses with zero finish time delta", zeroDelta)
}
if got.SimDataBase64 != encodedSimdata {
t.Errorf("wrong simdata:\nwant %s\n got %s", encodedSimdata, got.SimDataBase64)
}
if len(got.HorseIndexByFinishOrder) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByFinishOrder (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByFinishOrder), len(got.RaceHorse))
}
if len(got.HorseIndexByPopularity) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByPopularity (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByPopularity), len(got.RaceHorse))
}
}
+25
View File
@@ -0,0 +1,25 @@
// Code generated by "stringer -type RunningStyleEx -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[StyleExNone-0]
_ = x[StyleExRunaway-1]
}
const _RunningStyleEx_name = "NONEOonige"
var _RunningStyleEx_index = [...]uint8{0, 4, 10}
func (i RunningStyleEx) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RunningStyleEx_index)-1 {
return "RunningStyleEx(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RunningStyleEx_name[_RunningStyleEx_index[idx]:_RunningStyleEx_index[idx+1]]
}
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
<script lang="ts">
interface Props {
value: number;
}
let { value = $bindable() }: Props = $props();
</script>
<div class="flex">
<input
class="my-auto w-full grow justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value
/>
<span class="my-auto flex-none pl-4 text-sm">{value}m</span>
</div>
+1
View File
@@ -20,6 +20,7 @@
skill_id: 0, skill_id: 0,
name: 'Skill data is loading...', name: 'Skill data is loading...',
description: 'Skill data is still loading, or else something went wrong.', description: 'Skill data is still loading, or else something went wrong.',
group_skills: [],
group: 0, group: 0,
rarity: 1, rarity: 1,
group_rate: 1, group_rate: 1,
+4
View File
@@ -4,6 +4,10 @@ import type { RegionalName } from '$lib/regional-name';
* Lobby conversation data. * Lobby conversation data.
*/ */
export interface Conversation { export interface Conversation {
/**
* Gallery ID.
*/
id: number;
/** /**
* Character who owns the conversation as a gallery entry. * Character who owns the conversation as a gallery entry.
*/ */
+16
View File
@@ -14,6 +14,10 @@ export interface Skill {
* Regional skil description. * Regional skil description.
*/ */
description: string; description: string;
/**
* Other skills in the skill group.
*/
group_skills: GroupSkill[];
/** /**
* Skill group ID. * Skill group ID.
*/ */
@@ -58,6 +62,18 @@ export interface Skill {
icon_id: number; icon_id: number;
} }
/**
* Skill reference in a skill group.
*/
export interface GroupSkill {
/** Skill ID. */
skill_id: number;
/** Group rate, as in `Skill`. */
group_rate: Skill['group_rate'];
/** Rarity, as in `Skill`. */
rarity: Skill['rarity'];
}
export enum DurationScale { export enum DurationScale {
Direct = 1, Direct = 1,
FrontDistance = 2, FrontDistance = 2,
+3 -2
View File
@@ -771,16 +771,17 @@ export function conservePowerAccel(
distanceType: Distance, distanceType: Distance,
style: RunningStyle, style: RunningStyle,
rawPower: number, rawPower: number,
basePower: number,
powerSkillBonus?: number, powerSkillBonus?: number,
spotStruggled?: boolean, spotStruggled?: boolean,
rushed?: boolean, rushed?: boolean,
): number { ): number {
const power = rawPower + (powerSkillBonus ?? 0); const power = basePower + 0.5 * Math.max(rawPower - 1200, 0) + (powerSkillBonus ?? 0);
if (power <= 1200) { if (power <= 1200) {
return 0; return 0;
} }
const sdc = conserveStratDistCoef[style][distanceType]; const sdc = conserveStratDistCoef[style][distanceType];
const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1; const activity = (rushed ? 0.8 : 1) * (spotStruggled ? 0.98 : 1);
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity; return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
} }
+14
View File
@@ -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>;
+57 -8
View File
@@ -9,25 +9,51 @@
requiredCharas, requiredCharas,
convoCharas, convoCharas,
} from '$lib/data/convo'; } from '$lib/data/convo';
import * as talk from '$lib/umadump/talk';
import CharaPick from '$lib/CharaPick.svelte'; import CharaPick from '$lib/CharaPick.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import CharaIcon from '$lib/CharaIcon.svelte'; import CharaIcon from '$lib/CharaIcon.svelte';
import { SvelteSet } from 'svelte/reactivity';
let conversations: Conversation[] = $state([]); let allConvos: Conversation[] = $state([]);
const convoMap = $derived(byChara(conversations)); const popular = $derived(groupPopulars(allConvos));
const popular = $derived(groupPopulars(conversations));
let characters: Character[] = $state([]); let characters: Character[] = $state([]);
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const names = $derived(charaNames(characters)); const names = $derived(charaNames(characters));
const minSuggest = 8; const minSuggest = 8;
onMount(async () => { onMount(async () => {
[conversations, characters] = await Promise.all([conversation(), character()]); [allConvos, characters] = await Promise.all([conversation(), character()]);
}); });
let chara: Character | undefined = $state(); let chara: Character | undefined = $state();
let convo = $state(1); 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 options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
const cur = $derived(options.find((c) => c.number === convo)); const cur = $derived(options.find((c) => c.number === convo));
const charaIDs = $derived(cur != null ? convoCharas(cur) : []); const charaIDs = $derived(cur != null ? convoCharas(cur) : []);
@@ -47,12 +73,12 @@
</script> </script>
<h1 class="text-4xl">Lobby Conversations</h1> <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="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="m-4 flex-1 md:mt-3"> <div class="md:col-span-3">
<label for="chara" class="hidden md:inline">Character</label> <label for="chara" class="hidden md:inline">Character</label>
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required /> <CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
</div> </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> <label for="convo" class="hidden md:inline">Conversation</label>
<select id="convo" bind:value={convo} class="w-full"> <select id="convo" bind:value={convo} class="w-full">
{#each options as opt (opt.number)} {#each options as opt (opt.number)}
@@ -60,6 +86,29 @@
{/each} {/each}
</select> </select>
</div> </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> </div>
{#if cur} {#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md"> <div class="shadow-sm transition-shadow hover:shadow-md">
+11 -29
View File
@@ -26,6 +26,7 @@
import Article from '../Article.svelte'; import Article from '../Article.svelte';
import Sec from '../Sec.svelte'; import Sec from '../Sec.svelte';
import * as skill from '$lib/data/skill'; import * as skill from '$lib/data/skill';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let raceLen = $state(2000); let raceLen = $state(2000);
let secSpeedStyle = $state(RunningStyle.FrontRunner); let secSpeedStyle = $state(RunningStyle.FrontRunner);
@@ -224,10 +225,10 @@
}, },
]); ]);
const fcSeries: ComputedSeries[] = $derived([ const fcSeries: ComputedSeries[] = $derived([
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) }, { label: 'Front Runner', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, raw, x) },
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) }, { label: 'Pace Chaser', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, raw, x) },
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) }, { label: 'Late Surger', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, raw, x) },
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) }, { label: 'End Closer', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, raw, x) },
]); ]);
</script> </script>
@@ -514,23 +515,13 @@
plotOptions={{ color: { legend: true } }} plotOptions={{ color: { legend: true } }}
/> />
<div class="mx-auto flex w-full md:max-w-2xl"> <div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label> <label class="my-auto mr-2 hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input <RaceLenSlider bind:value={raceLen} />
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-none pl-2 text-sm">{raceLen}m</span>
</div> </div>
</div> </div>
<p> <p>
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a Rushed and <a href="#spot-struggle">spot struggle</a> reduce both the duration and effectiveness of full charge, potentially to
href="#spot-struggle">spot struggle</a zero. In practice, this appears to be a concern primarily on sprints, which already have a -55% penalty to full charge duration.
> both reduce the chance for it to fire.
</p> </p>
<Sec h={3} id="lane-combo">Lane Combo</Sec> <Sec h={3} id="lane-combo">Lane Combo</Sec>
@@ -670,17 +661,8 @@
plotOptions={{ color: { legend: true } }} plotOptions={{ color: { legend: true } }}
/> />
<div class="mx-auto flex w-full md:max-w-2xl"> <div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label> <label class="my-auto mr-2 hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input <RaceLenSlider bind:value={raceLen} />
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label> <label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label>
<select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={secSpeedStyle}> <select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={secSpeedStyle}>
{#each RUNNING_STYLES as [name, style] (style)} {#each RUNNING_STYLES as [name, style] (style)}
+16 -17
View File
@@ -5,6 +5,7 @@
import * as race from '$lib/race'; import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
function modemean(x: number, bonus: number, p: number): number { function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p; return x * (1 - p) + x * bonus * p;
@@ -58,10 +59,10 @@
]); ]);
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }]; const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
const conservePower: Array<ComputedSeries | null> = $derived([ const conservePower: Array<ComputedSeries | null> = $derived([
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) }, { label: 'Conserved Power', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, false) },
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) }, { label: 'Rushed', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, true) },
styleIsFront styleIsFront
? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) } ? { label: 'Spot Struggled', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, true, false) }
: null, : null,
]); ]);
const gutsSpurt: Array<ComputedSeries | null> = $derived([ const gutsSpurt: Array<ComputedSeries | null> = $derived([
@@ -174,16 +175,9 @@
/> />
<div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7"> <div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7">
{#if pick?.len} {#if pick?.len}
<input <div class="col-span-3 my-auto mr-4">
class="col-span-2 my-auto w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-1 my-auto pl-2 text-sm">{raceLen}m</span>
{/if} {/if}
{#if pick?.style} {#if pick?.style}
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}> <select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}>
@@ -329,11 +323,16 @@
Bonus acceleration at spurt start from the Charge Up/Fully Charged mechanic. Scales with base power plus passive skills, so Bonus acceleration at spurt start from the Charge Up/Fully Charged mechanic. Scales with base power plus passive skills, so
modifiers for ground conditions and career mode do not apply. modifiers for ground conditions and career mode do not apply.
</p> </p>
<p>
This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed
and spot struggle multipliers apply.
</p>
{@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })} {@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })}
<p>
The duration of Fully Charged does not depend directly on stats, but rather on a hidden <i>conserved power</i> value which
increases per horse throughout the race. Conserved power does not increase while <a href="#spot-struggle">spot struggling</a>,
so for front runners, the duration may scale negatively with guts. It also does not increase while running in any
<a href="#poskeep">position keep mode</a>
other than pace-down, so its duration may have a very slight negative correlation with wit.
<a href="#rushed">Rushed</a> additionally prevents accumulating conserved power, so full charge duration may scale positively with
wit amortized across races. Otherwise, power conservation rate depends on running style.
</p>
<Sec h={2} id="guts">Guts</Sec> <Sec h={2} id="guts">Guts</Sec>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel'; import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments'; import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawSpeed = $state(1200); let rawSpeed = $state(1200);
let rawPower = $state(1200); let rawPower = $state(1200);
@@ -231,16 +232,9 @@
</div> </div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6"> <div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6">
<input <div class="col-span-full m-4">
class="col-span-4 m-4 w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
<select id="surface" bind:value={surface} class="col-span-2 mx-2 mb-4"> <select id="surface" bind:value={surface} class="col-span-2 mx-2 mb-4">
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option> <option value={race.Surface.Dirt}>Dirt</option>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel'; import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments'; import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -216,16 +217,9 @@
</div> </div>
</div> </div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3"> <div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
<input <div class="col-span-6 mx-4 mt-4">
class="col-span-4 m-4 w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
<select id="surface" bind:value={surface} class="col-span-2 m-4"> <select id="surface" bind:value={surface} class="col-span-2 m-4">
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option> <option value={race.Surface.Dirt}>Dirt</option>
+2 -2
View File
@@ -18,6 +18,7 @@
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import SpeedDur from './SpeedDur.svelte'; import SpeedDur from './SpeedDur.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -112,8 +113,7 @@
</select> </select>
</div> </div>
<div class="m-4 md:col-span-2 md:col-start-2"> <div class="m-4 md:col-span-2 md:col-start-2">
<label for="raceLen">Race Distance</label> <RaceLenSlider bind:value={raceLen} />
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div> </div>
<div class="m-4 self-center md:col-span-2"> <div class="m-4 self-center md:col-span-2">
<label for="isRunaway" class="mr-1 align-middle">Runaway</label> <label for="isRunaway" class="mr-1 align-middle">Runaway</label>
+2 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComputedSeries } from '$lib/chart'; import type { ComputedSeries } from '$lib/chart';
import * as race from '$lib/race'; import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
@@ -98,8 +99,7 @@
</select> </select>
</div> </div>
<div class="m-4 md:col-start-2"> <div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label> <RaceLenSlider bind:value={raceLen} />
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div> </div>
<div class="m-4 self-center"> <div class="m-4 self-center">
<label for="isCareer" class="mr-1 align-middle">In Career</label> <label for="isCareer" class="mr-1 align-middle">In Career</label>
+2 -10
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as course from '$lib/course'; import * as course from '$lib/course';
import * as race from '$lib/race'; import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import * as recovery from '$lib/racelib/recovery'; import * as recovery from '$lib/racelib/recovery';
import HPChart from './HPChart.svelte'; import HPChart from './HPChart.svelte';
import InfoPanels from './InfoPanels.svelte'; import InfoPanels from './InfoPanels.svelte';
@@ -277,16 +278,7 @@
</div> </div>
<div class="mx-auto my-2 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6"> <div class="mx-auto my-2 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6">
<div class="m-4 flex md:col-span-2"> <div class="m-4 flex md:col-span-2">
<input <RaceLenSlider bind:value={raceLen} />
class="col-span-2 my-auto w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto pl-2 text-sm">{raceLen}m</span>
</div> </div>
<select id="surface" class="m-4" bind:value={surface}> <select id="surface" class="m-4" bind:value={surface}>
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>