Compare commits
13
Commits
replay-json
..
ismdb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d14919f6a | ||
|
|
381df5bd35 | ||
|
|
e5ebd1894c | ||
|
|
c43f6b8292 | ||
|
|
cccf0cc838 | ||
|
|
64f82136cb | ||
|
|
53c7079f46 | ||
|
|
1cfabb18f5 | ||
|
|
bf4e750825 | ||
|
|
57ffadb06a | ||
|
|
86583e68d1 | ||
|
|
fde5b4fd20 | ||
|
|
1cf3af5f79 |
+188
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
INSERT INTO career VALUES (
|
||||
$trainer_id,
|
||||
$trained_chara_id,
|
||||
$card_id,
|
||||
$scenario_id,
|
||||
$route_id,
|
||||
$start_time,
|
||||
$end_time,
|
||||
|
||||
$chara_grade,
|
||||
$star_level,
|
||||
$potential_level,
|
||||
|
||||
$speed,
|
||||
$stamina,
|
||||
$power,
|
||||
$guts,
|
||||
$wit,
|
||||
$energy,
|
||||
$max_speed,
|
||||
$max_stamina,
|
||||
$max_power,
|
||||
$max_guts,
|
||||
$max_wit,
|
||||
$max_energy,
|
||||
$skill_point,
|
||||
$mood,
|
||||
|
||||
$turf,
|
||||
$dirt,
|
||||
$sprint,
|
||||
$mile,
|
||||
$medium,
|
||||
$long,
|
||||
$front_runner,
|
||||
$pace_chaser,
|
||||
$late_surger,
|
||||
$end_closer,
|
||||
|
||||
$start_speed,
|
||||
$start_stamina,
|
||||
$start_power,
|
||||
$start_guts,
|
||||
$start_wit,
|
||||
$start_max_speed,
|
||||
$start_max_stamina,
|
||||
$start_max_power,
|
||||
$start_max_guts,
|
||||
$start_max_wit,
|
||||
$start_turf,
|
||||
$start_dirt,
|
||||
$start_sprint,
|
||||
$start_mile,
|
||||
$start_medium,
|
||||
$start_long,
|
||||
$start_front_runner,
|
||||
$start_pace_chaser,
|
||||
$start_late_surger,
|
||||
$start_end_closer,
|
||||
|
||||
$fans,
|
||||
$race_program_id,
|
||||
$reserve_race_program_id,
|
||||
$race_running_style,
|
||||
$is_short_race,
|
||||
|
||||
$succession_trained_chara_id_1,
|
||||
$succession_trained_chara_id_2
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
INSERT INTO gain VALUES (
|
||||
$trainer_id,
|
||||
$trained_chara_id,
|
||||
$source,
|
||||
|
||||
$gain_speed,
|
||||
$gain_stamina,
|
||||
$gain_power,
|
||||
$gain_guts,
|
||||
$gain_wit,
|
||||
$gain_skill_point,
|
||||
|
||||
$gain_max_speed,
|
||||
$gain_max_stamina,
|
||||
$gain_max_power,
|
||||
$gain_max_guts,
|
||||
$gain_max_wit,
|
||||
|
||||
$gain_turf,
|
||||
$gain_dirt,
|
||||
|
||||
$gain_sprint,
|
||||
$gain_mile,
|
||||
$gain_medium,
|
||||
$gain_long,
|
||||
|
||||
$gain_front_runner,
|
||||
$gain_pace_chaser,
|
||||
$gain_late_surger,
|
||||
$gain_end_closer
|
||||
)
|
||||
@@ -0,0 +1,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;
|
||||
@@ -0,0 +1,88 @@
|
||||
DROP VIEW IF EXISTS gain_event;
|
||||
DROP VIEW IF EXISTS gain_inspiration;
|
||||
DROP VIEW IF EXISTS gain_support_card;
|
||||
|
||||
CREATE VIEW gain_event AS
|
||||
SELECT
|
||||
trainer_id,
|
||||
trained_chara_id,
|
||||
gain_speed,
|
||||
gain_stamina,
|
||||
gain_power,
|
||||
gain_guts,
|
||||
gain_wit,
|
||||
gain_skill_point,
|
||||
gain_max_speed,
|
||||
gain_max_stamina,
|
||||
gain_max_power,
|
||||
gain_max_guts,
|
||||
gain_max_wit,
|
||||
gain_turf,
|
||||
gain_dirt,
|
||||
gain_sprint,
|
||||
gain_mile,
|
||||
gain_medium,
|
||||
gain_long,
|
||||
gain_front_runner,
|
||||
gain_pace_chaser,
|
||||
gain_late_surger,
|
||||
gain_end_closer
|
||||
FROM gain
|
||||
WHERE source = 0;
|
||||
|
||||
CREATE VIEW gain_inspiration AS
|
||||
SELECT
|
||||
trainer_id,
|
||||
trained_chara_id,
|
||||
gain_speed,
|
||||
gain_stamina,
|
||||
gain_power,
|
||||
gain_guts,
|
||||
gain_wit,
|
||||
gain_skill_point,
|
||||
gain_max_speed,
|
||||
gain_max_stamina,
|
||||
gain_max_power,
|
||||
gain_max_guts,
|
||||
gain_max_wit,
|
||||
gain_turf,
|
||||
gain_dirt,
|
||||
gain_sprint,
|
||||
gain_mile,
|
||||
gain_medium,
|
||||
gain_long,
|
||||
gain_front_runner,
|
||||
gain_pace_chaser,
|
||||
gain_late_surger,
|
||||
gain_end_closer
|
||||
FROM gain
|
||||
WHERE source = 1;
|
||||
|
||||
CREATE VIEW gain_support_card AS
|
||||
SELECT
|
||||
trainer_id,
|
||||
trained_chara_id,
|
||||
source AS support_card_id,
|
||||
gain_speed,
|
||||
gain_stamina,
|
||||
gain_power,
|
||||
gain_guts,
|
||||
gain_wit,
|
||||
gain_skill_point,
|
||||
gain_max_speed,
|
||||
gain_max_stamina,
|
||||
gain_max_power,
|
||||
gain_max_guts,
|
||||
gain_max_wit,
|
||||
gain_turf,
|
||||
gain_dirt,
|
||||
gain_sprint,
|
||||
gain_mile,
|
||||
gain_medium,
|
||||
gain_long,
|
||||
gain_front_runner,
|
||||
gain_pace_chaser,
|
||||
gain_late_surger,
|
||||
gain_end_closer
|
||||
FROM gain
|
||||
WHERE source > 1;
|
||||
@@ -0,0 +1,325 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"zombiezen.com/go/sqlite"
|
||||
"zombiezen.com/go/sqlite/sqlitemigration"
|
||||
"zombiezen.com/go/sqlite/sqlitex"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
|
||||
)
|
||||
|
||||
const appID int32 = 0x4acedb
|
||||
|
||||
//go:embed schema-00.sql
|
||||
var schema0 string
|
||||
|
||||
//go:embed schema-final.sql
|
||||
var schemaFinal string
|
||||
|
||||
func opendb(uri, journalMode string) *sqlitemigration.Pool {
|
||||
schema := sqlitemigration.Schema{
|
||||
Migrations: []string{
|
||||
schema0,
|
||||
},
|
||||
AppID: appID,
|
||||
RepeatableMigration: schemaFinal,
|
||||
}
|
||||
j := "PRAGMA journal_mode=" + journalMode
|
||||
opts := sqlitemigration.Options{
|
||||
Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI,
|
||||
PrepareConn: func(conn *sqlite.Conn) error {
|
||||
if journalMode == "" {
|
||||
return nil
|
||||
}
|
||||
stmt, _, err := conn.PrepareTransient(j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.Step()
|
||||
return errors.Join(err, stmt.Finalize())
|
||||
},
|
||||
}
|
||||
db := sqlitemigration.NewPool(uri, schema, opts)
|
||||
return db
|
||||
}
|
||||
|
||||
func insert(ctx context.Context, db *sqlitemigration.Pool, log *slog.Logger, race *replay.Race, simdata *replay.RaceSimulateData) (err error) {
|
||||
conn, err := db.Take(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Put(conn)
|
||||
|
||||
defer sqlitex.Transaction(conn)(&err)
|
||||
|
||||
log.DebugContext(ctx, "horses",
|
||||
slog.Int("num", int(race.NumRaceHorses)),
|
||||
slog.Int("actual", len(race.RaceHorse)),
|
||||
)
|
||||
var trainedCharas [18]int64
|
||||
for i := range race.RaceHorse {
|
||||
c := &race.RaceHorse[i]
|
||||
id, err := insertTrainedChara(conn, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trainedCharas[i] = id
|
||||
if err := insertTrainedCharaSkill(conn, c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "horse",
|
||||
slog.String("trainer", c.TrainerName),
|
||||
slog.Int("trained", int(c.ResponseHorseData.TrainedCharaID)),
|
||||
slog.Int("card", int(c.ResponseHorseData.CardID)),
|
||||
slog.Int64("id", id),
|
||||
)
|
||||
}
|
||||
if err = insertRaceCourseSet(conn, race); err != nil {
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "course", slog.Int("id", int(race.RaceCourseSet.ID)))
|
||||
configID, err := insertRaceConfig(conn, race)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "config", slog.Int64("id", configID))
|
||||
raceID, err := insertRace(conn, race, simdata, &trainedCharas, configID)
|
||||
if err != nil {
|
||||
code := sqlite.ErrCode(err)
|
||||
if code == sqlite.ResultConstraintUnique {
|
||||
log.InfoContext(ctx, "already loaded")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "race", slog.Int64("id", raceID))
|
||||
err = errors.Join(
|
||||
insertRaceHorses(ctx, log, conn, race, simdata, &trainedCharas, raceID),
|
||||
insertRaceFrames(ctx, log, conn, simdata, raceID),
|
||||
insertRaceEvents(ctx, log, conn, simdata, raceID),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
//go:embed insert-trained_chara.sql
|
||||
var insertTrainedCharaSQL string
|
||||
|
||||
func insertTrainedChara(conn *sqlite.Conn, c *replay.HorseData) (int64, error) {
|
||||
stmt := conn.Prep(insertTrainedCharaSQL)
|
||||
trainer := cmp.Or(c.ResponseHorseData.ViewerID, c.ResponseHorseData.OwnerViewerID)
|
||||
stmt.SetText("$trainer", c.TrainerName)
|
||||
stmt.SetInt64("$trainer_id", trainer)
|
||||
stmt.SetInt64("$trained_chara_id", int64(c.ResponseHorseData.TrainedCharaID))
|
||||
stmt.SetInt64("$chara_id", int64(c.CharaID))
|
||||
stmt.SetText("$chara_name", c.CharaName)
|
||||
stmt.SetInt64("$card_id", int64(c.ResponseHorseData.CardID))
|
||||
stmt.SetInt64("$rarity", int64(c.Rarity))
|
||||
stmt.SetInt64("$mob_id", int64(c.MobID))
|
||||
stmt.SetInt64("$raw_speed", int64(c.ResponseHorseData.Speed))
|
||||
stmt.SetInt64("$raw_stamina", int64(c.ResponseHorseData.Stamina))
|
||||
stmt.SetInt64("$raw_power", int64(c.ResponseHorseData.Pow))
|
||||
stmt.SetInt64("$raw_guts", int64(c.ResponseHorseData.Guts))
|
||||
stmt.SetInt64("$raw_wit", int64(c.ResponseHorseData.Wiz))
|
||||
stmt.SetInt64("$turf", int64(c.ResponseHorseData.ProperGroundTurf))
|
||||
stmt.SetInt64("$dirt", int64(c.ResponseHorseData.ProperGroundDirt))
|
||||
stmt.SetInt64("$sprint", int64(c.ResponseHorseData.ProperDistanceShort))
|
||||
stmt.SetInt64("$mile", int64(c.ResponseHorseData.ProperDistanceMile))
|
||||
stmt.SetInt64("$medium", int64(c.ResponseHorseData.ProperDistanceMiddle))
|
||||
stmt.SetInt64("$long", int64(c.ResponseHorseData.ProperDistanceLong))
|
||||
stmt.SetInt64("$front_runner", int64(c.ResponseHorseData.ProperRunningStyleNige))
|
||||
stmt.SetInt64("$pace_chaser", int64(c.ResponseHorseData.ProperRunningStyleSenko))
|
||||
stmt.SetInt64("$late_surger", int64(c.ResponseHorseData.ProperRunningStyleSashi))
|
||||
stmt.SetInt64("$end_closer", int64(c.ResponseHorseData.ProperRunningStyleOikomi))
|
||||
stmt.SetInt64("$fans", int64(c.TrainedCharaData.Fans))
|
||||
stmt.SetInt64("$rank", int64(c.TrainedCharaData.Rank))
|
||||
stmt.SetInt64("$rank_score", int64(c.TrainedCharaData.RankScore))
|
||||
r, err := sqlitex.ResultInt64(stmt)
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("inserting trained chara: %w", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
func insertTrainedCharaSkill(conn *sqlite.Conn, c *replay.HorseData, id int64) error {
|
||||
const sql = `INSERT OR IGNORE INTO trained_chara_skill VALUES (?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
stmt.BindInt64(1, id)
|
||||
for _, sk := range c.ResponseHorseData.SkillArray {
|
||||
stmt.BindInt64(2, int64(sk.SkillID))
|
||||
stmt.BindInt64(3, int64(sk.Level))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting trained chara id %d skill %d: %w", id, sk.SkillID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRaceCourseSet(conn *sqlite.Conn, race *replay.Race) error {
|
||||
const sql = `INSERT OR IGNORE INTO race_course_set VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
stmt.BindInt64(1, int64(race.RaceCourseSet.ID))
|
||||
stmt.BindInt64(2, int64(race.RaceCourseSet.RaceTrackID))
|
||||
stmt.BindInt64(3, int64(race.RaceCourseSet.Distance))
|
||||
stmt.BindInt64(4, int64(race.RaceCourseSet.Ground))
|
||||
stmt.BindInt64(5, int64(race.RaceCourseSet.Inout))
|
||||
stmt.BindInt64(6, int64(race.RaceCourseSet.Turn))
|
||||
stmt.BindInt64(7, int64(race.RaceCourseSet.FenceSet))
|
||||
stmt.BindInt64(8, int64(race.RaceCourseSet.FloatLaneMax))
|
||||
stmt.BindInt64(9, int64(race.RaceCourseSet.CourseSetStatusID))
|
||||
stmt.BindInt64(10, int64(race.RaceCourseSet.FinishTimeMin))
|
||||
stmt.BindInt64(11, int64(race.RaceCourseSet.FinishTimeMinRandomRange))
|
||||
stmt.BindInt64(12, int64(race.RaceCourseSet.FinishTimeMax))
|
||||
stmt.BindInt64(13, int64(race.RaceCourseSet.FinishTimeMaxRandomRange))
|
||||
stmt.BindFloat(14, float64(race.CourseSectionDistance))
|
||||
stmt.BindInt64(15, int64(race.CourseDistanceType))
|
||||
stmt.BindInt64(16, int64(race.CourseFurlongNum))
|
||||
stmt.BindFloat(17, float64(race.BaseSpeed))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race course: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed insert-race_config.sql
|
||||
var insertRaceConfigSQL string
|
||||
|
||||
func insertRaceConfig(conn *sqlite.Conn, race *replay.Race) (int64, error) {
|
||||
stmt := conn.Prep(insertRaceConfigSQL)
|
||||
stmt.SetInt64("$type", int64(race.RaceType))
|
||||
stmt.SetInt64("$course_set_id", int64(race.RaceCourseSet.ID))
|
||||
stmt.SetInt64("$ground_condition", int64(race.GroundCondition))
|
||||
stmt.SetInt64("$weather", int64(race.Weather))
|
||||
stmt.SetInt64("$season", int64(race.Season))
|
||||
stmt.SetInt64("$time", int64(race.Time))
|
||||
stmt.SetInt64("$num_horse", int64(race.NumRaceHorses))
|
||||
r, err := sqlitex.ResultInt64(stmt)
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("inserting race config: %w", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
//go:embed insert-race.sql
|
||||
var insertRaceSQL string
|
||||
|
||||
func insertRace(conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, configID int64) (int64, error) {
|
||||
stmt := conn.Prep(insertRaceSQL)
|
||||
stmt.SetInt64("$config_id", configID)
|
||||
stmt.SetInt64("$seed", int64(race.RandomSeed))
|
||||
stmt.SetFloat("$distance_diff_max", float64(simdata.DistanceDiffMax))
|
||||
stmt.SetInt64("$frame_count", int64(simdata.FrameCount))
|
||||
// zombiezen/sqlite doesn't seem to expose an API to find the index of a
|
||||
// named parameter, so even though we were using names before, we switch
|
||||
// to indices for trained chara IDs.
|
||||
for i, id := range trainedCharas {
|
||||
stmt.BindInt64(5+i, id)
|
||||
}
|
||||
r, err := sqlitex.ResultInt64(stmt)
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("inserting race: %w", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
//go:embed insert-race_horse.sql
|
||||
var insertRaceHorseSQL string
|
||||
|
||||
func insertRaceHorses(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, raceID int64) error {
|
||||
stmt := conn.Prep(insertRaceHorseSQL)
|
||||
stmt.SetInt64("$race", raceID)
|
||||
for i, c := range race.RaceHorse {
|
||||
log.DebugContext(ctx, "race horse", slog.Int64("trained", trainedCharas[i]), slog.Int("index", int(c.HorseIndex)))
|
||||
stmt.SetInt64("$horse", trainedCharas[i])
|
||||
stmt.SetInt64("$horse_index", int64(c.HorseIndex))
|
||||
stmt.SetInt64("$post", int64(c.PostNumber))
|
||||
stmt.SetInt64("$finish_order", int64(c.FinishOrder))
|
||||
stmt.SetFloat("$finish_time_raw", float64(c.FinishTimeRaw))
|
||||
stmt.SetFloat("$finish_time_scaled", float64(c.FinishTimeScaled))
|
||||
stmt.SetFloat("$finish_diff_time_from_prev", float64(c.FinishDiffTimeFromPrev))
|
||||
stmt.SetInt64("$running_style", int64(c.ResponseHorseData.RunningStyle))
|
||||
stmt.SetInt64("$running_style_ex", int64(c.RunningStyleEx))
|
||||
stmt.SetInt64("$active_distance_aptitude", int64(c.ActiveProperDistance))
|
||||
stmt.SetInt64("$active_surface_aptitude", int64(c.ActiveProperGroundType))
|
||||
stmt.SetInt64("$mood", int64(c.RaceParam.Motivation))
|
||||
stmt.SetFloat("$mood_coef", float64(c.RaceParam.MotivationCoef))
|
||||
stmt.SetFloat("$base_speed", float64(c.RaceParam.BaseSpeed))
|
||||
stmt.SetFloat("$base_stamina", float64(c.RaceParam.BaseStamina))
|
||||
stmt.SetFloat("$base_power", float64(c.RaceParam.BasePow))
|
||||
stmt.SetFloat("$base_guts", float64(c.RaceParam.BaseGuts))
|
||||
stmt.SetFloat("$base_wit", float64(c.RaceParam.BaseWiz))
|
||||
stmt.SetInt64("$popularity", int64(c.Popularity))
|
||||
stmt.SetInt64("$popularity_rank_left", int64(c.PopularityRankLeft))
|
||||
stmt.SetInt64("$popularity_rank_center", int64(c.PopularityRankCenter))
|
||||
stmt.SetInt64("$popularity_rank_right", int64(c.PopularityRankRight))
|
||||
stmt.SetFloat("$start_delay", float64(simdata.HorseResult[i].StartDelayTime))
|
||||
stmt.SetFloat("$spurt_start_distance", float64(simdata.HorseResult[i].LastSpurtStartDistance))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race horses: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRaceFrames(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
|
||||
const sql = `INSERT INTO race_frame VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
stmt.BindInt64(1, raceID)
|
||||
for _, f := range simdata.Frame {
|
||||
log.DebugContext(ctx, "frame", slog.Float64("time", float64(f.Time)))
|
||||
stmt.BindFloat(2, float64(f.Time))
|
||||
for i, h := range f.HorseFrame {
|
||||
stmt.BindInt64(3, int64(i))
|
||||
stmt.BindFloat(4, float64(h.Distance))
|
||||
stmt.BindInt64(5, int64(h.LanePosition))
|
||||
stmt.BindInt64(6, int64(h.Speed))
|
||||
stmt.BindInt64(7, int64(h.HP))
|
||||
stmt.BindInt64(8, int64(h.TemptationMode))
|
||||
stmt.BindInt64(9, int64(h.BlockFrontHorseIndex))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race frames: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRaceEvents(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
|
||||
const sql = `INSERT INTO race_event VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
for _, e := range simdata.Event {
|
||||
log.DebugContext(ctx, "event", slog.Float64("time", float64(e.Event.FrameTime)), slog.Int("type", int(e.Event.Type)))
|
||||
// Easier to clear all bindings every time now
|
||||
// than to bind nulls to all the parameters that need them below.
|
||||
stmt.ClearBindings()
|
||||
stmt.BindInt64(1, raceID)
|
||||
stmt.BindFloat(2, float64(e.Event.FrameTime))
|
||||
stmt.BindInt64(3, int64(e.Event.Type))
|
||||
for i, p := range e.Event.Param {
|
||||
stmt.BindInt64(4+i, int64(p))
|
||||
}
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race events: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
INSERT OR ROLLBACK INTO race (
|
||||
config_id,
|
||||
seed,
|
||||
distance_diff_max,
|
||||
frame_count,
|
||||
horse_1,
|
||||
horse_2,
|
||||
horse_3,
|
||||
horse_4,
|
||||
horse_5,
|
||||
horse_6,
|
||||
horse_7,
|
||||
horse_8,
|
||||
horse_9,
|
||||
horse_10,
|
||||
horse_11,
|
||||
horse_12,
|
||||
horse_13,
|
||||
horse_14,
|
||||
horse_15,
|
||||
horse_16,
|
||||
horse_17,
|
||||
horse_18
|
||||
)
|
||||
VALUES (
|
||||
$config_id,
|
||||
$seed,
|
||||
$distance_diff_max,
|
||||
$frame_count,
|
||||
$horse_1,
|
||||
$horse_2,
|
||||
$horse_3,
|
||||
$horse_4,
|
||||
$horse_5,
|
||||
$horse_6,
|
||||
$horse_7,
|
||||
$horse_8,
|
||||
$horse_9,
|
||||
$horse_10,
|
||||
$horse_11,
|
||||
$horse_12,
|
||||
$horse_13,
|
||||
$horse_14,
|
||||
$horse_15,
|
||||
$horse_16,
|
||||
$horse_17,
|
||||
$horse_18
|
||||
)
|
||||
RETURNING id
|
||||
@@ -0,0 +1,4 @@
|
||||
INSERT INTO race_config (type, course_set_id, ground_condition, weather, season, time, num_horse)
|
||||
VALUES ($type, $course_set_id, $ground_condition, $weather, $season, $time, $num_horse)
|
||||
ON CONFLICT DO UPDATE SET id=id
|
||||
RETURNING id
|
||||
@@ -0,0 +1,12 @@
|
||||
INSERT INTO race_horse
|
||||
VALUES (
|
||||
$race, $horse,
|
||||
$horse_index, $post,
|
||||
$finish_order, $finish_time_raw, $finish_time_scaled, $finish_diff_time_from_prev,
|
||||
$running_style, $running_style_ex,
|
||||
$active_distance_aptitude, $active_surface_aptitude,
|
||||
$mood, $mood_coef,
|
||||
$base_speed, $base_stamina, $base_power, $base_guts, $base_wit,
|
||||
$popularity, $popularity_rank_left, $popularity_rank_center, $popularity_rank_right,
|
||||
$start_delay, $spurt_start_distance
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
INSERT INTO trained_chara (
|
||||
trainer, trainer_id, trained_chara_id,
|
||||
chara_id, chara_name, card_id, rarity, mob_id,
|
||||
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
|
||||
turf, dirt,
|
||||
sprint, mile, medium, long,
|
||||
front_runner, pace_chaser, late_surger, end_closer,
|
||||
fans, rank, rank_score
|
||||
) VALUES (
|
||||
$trainer, $trainer_id, $trained_chara_id,
|
||||
$chara_id, $chara_name, $card_id, $rarity, $mob_id,
|
||||
$raw_speed, $raw_stamina, $raw_power, $raw_guts, $raw_wit,
|
||||
$turf, $dirt,
|
||||
$sprint, $mile, $medium, $long,
|
||||
$front_runner, $pace_chaser, $late_surger, $end_closer,
|
||||
$fans, $rank, $rank_score
|
||||
)
|
||||
ON CONFLICT DO UPDATE SET id=id
|
||||
RETURNING id
|
||||
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
uri, journalMode string
|
||||
fs []string
|
||||
)
|
||||
flag.StringVar(&uri, "db", "race.sqlite3", "output database `URI` or filename")
|
||||
flag.StringVar(&journalMode, "journal", "memory", "journal `mode` to apply to database connections")
|
||||
flag.Func("d", "`dir`ectory containing race dumps to load (repeatable)", func(s string) error {
|
||||
es, err := os.ReadDir(s)
|
||||
for _, e := range es {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
fs = append(fs, filepath.Join(s, e.Name()))
|
||||
}
|
||||
return err
|
||||
})
|
||||
flag.Parse()
|
||||
fs = append(fs, flag.Args()...)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
stop()
|
||||
}()
|
||||
|
||||
db := opendb(uri, journalMode)
|
||||
ok := 0
|
||||
for _, n := range fs {
|
||||
log := slog.With(slog.String("file", n))
|
||||
log.Info("begin")
|
||||
f, err := os.Open(n)
|
||||
if err != nil {
|
||||
log.Error("open", slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
var race replay.Race
|
||||
d := json.NewDecoder(f)
|
||||
d.DisallowUnknownFields()
|
||||
log.Debug("decode")
|
||||
if err := d.Decode(&race); err != nil {
|
||||
log.Error("load", slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
log.Debug("simdata")
|
||||
simdata, err := replay.DecodeSim(race.SimDataBase64)
|
||||
if err != nil {
|
||||
log.Error("simdata", slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
log.Debug("insert")
|
||||
err = insert(ctx, db, log, &race, simdata)
|
||||
if err != nil {
|
||||
log.Error("insert", slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
ok++
|
||||
}
|
||||
slog.Info("done", slog.Int("races", ok))
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
-- trained_chara lists horses who appear in races.
|
||||
CREATE TABLE IF NOT EXISTS trained_chara (
|
||||
-- id is the DB-internal ID of the Uma.
|
||||
-- It does not come from the game and should not be supplied in DDL.
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
||||
trainer TEXT NOT NULL,
|
||||
trainer_id INTEGER NOT NULL, -- 0 for NPC umas e.g. in practice races
|
||||
trained_chara_id INTEGER NOT NULL,
|
||||
chara_id INTEGER NOT NULL,
|
||||
chara_name TEXT NOT NULL,
|
||||
-- card_id is the Uma herself, e.g. 100201 for [Innocent Silence] Silence Suzuka.
|
||||
card_id INTEGER NOT NULL,
|
||||
rarity INTEGER NOT NULL,
|
||||
mob_id INTEGER NOT NULL, -- 0 for Umas that are not mobs
|
||||
|
||||
-- Raw stats.
|
||||
raw_speed INTEGER NOT NULL,
|
||||
raw_stamina INTEGER NOT NULL,
|
||||
raw_power INTEGER NOT NULL,
|
||||
raw_guts INTEGER NOT NULL,
|
||||
raw_wit INTEGER NOT NULL,
|
||||
|
||||
-- Aptitudes.
|
||||
turf INTEGER NOT NULL,
|
||||
dirt INTEGER NOT NULL,
|
||||
sprint INTEGER NOT NULL,
|
||||
mile INTEGER NOT NULL,
|
||||
medium INTEGER NOT NULL,
|
||||
long INTEGER NOT NULL,
|
||||
front_runner INTEGER NOT NULL,
|
||||
pace_chaser INTEGER NOT NULL,
|
||||
late_surger INTEGER NOT NULL,
|
||||
end_closer INTEGER NOT NULL,
|
||||
|
||||
-- Other data.
|
||||
fans INTEGER NOT NULL,
|
||||
rank INTEGER NOT NULL,
|
||||
rank_score INTEGER NOT NULL,
|
||||
|
||||
UNIQUE (trainer_id, trained_chara_id)
|
||||
) STRICT;
|
||||
|
||||
-- Insert trained chara ID 0 to represent no Uma.
|
||||
INSERT OR IGNORE INTO trained_chara (
|
||||
id,
|
||||
trainer,
|
||||
trainer_id,
|
||||
trained_chara_id,
|
||||
chara_id, chara_name,
|
||||
card_id,
|
||||
mob_id,
|
||||
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
|
||||
turf, dirt,
|
||||
sprint, mile, medium, long,
|
||||
front_runner, pace_chaser, late_surger, end_closer,
|
||||
fans,
|
||||
rank, rank_score
|
||||
)
|
||||
VALUES (0, '', 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
-- trained_chara_skill lists the skills each observed horse owns.
|
||||
CREATE TABLE IF NOT EXISTS trained_chara_skill (
|
||||
trained_chara INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
skill_id INTEGER NOT NULL,
|
||||
level INTEGER NOT NULL,
|
||||
|
||||
UNIQUE (trained_chara, skill_id)
|
||||
) STRICT;
|
||||
CREATE INDEX IF NOT EXISTS trained_chara_skill_by_trained_chara ON trained_chara_skill (trained_chara);
|
||||
|
||||
-- race_course_set is the accumulated information of observed race courses.
|
||||
CREATE TABLE IF NOT EXISTS race_course_set (
|
||||
id INTEGER PRIMARY KEY,
|
||||
race_track_id INTEGER NOT NULL,
|
||||
distance INTEGER NOT NULL,
|
||||
ground INTEGER NOT NULL,
|
||||
inout INTEGER NOT NULL,
|
||||
turn INTEGER NOT NULL,
|
||||
fence_set INTEGER NOT NULL,
|
||||
float_lane_max INTEGER NOT NULL,
|
||||
course_set_status_id INTEGER NOT NULL,
|
||||
finish_time_min INTEGER NOT NULL,
|
||||
finish_time_min_random_range INTEGER NOT NULL,
|
||||
finish_time_max INTEGER NOT NULL,
|
||||
finish_time_max_random_range INTEGER NOT NULL,
|
||||
|
||||
section_distance REAL NOT NULL,
|
||||
distance_type INTEGER NOT NULL,
|
||||
furlong_number INTEGER NOT NULL,
|
||||
base_speed REAL NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- race_config is observed configurations of races including the race type.
|
||||
CREATE TABLE IF NOT EXISTS race_config (
|
||||
-- id is the DB-internal ID of the race config.
|
||||
id INTEGER PRIMARY KEY,
|
||||
type INTEGER NOT NULL,
|
||||
course_set_id INTEGER NOT NULL REFERENCES race_course_set (id) ON DELETE RESTRICT,
|
||||
ground_condition INTEGER NOT NULL,
|
||||
weather INTEGER NOT NULL,
|
||||
season INTEGER NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
num_horse INTEGER NOT NULL,
|
||||
|
||||
UNIQUE (type, course_set_id, ground_condition, weather, season, time, num_horse)
|
||||
) STRICT;
|
||||
CREATE INDEX IF NOT EXISTS race_config_by_type ON race_config (type);
|
||||
|
||||
-- race is all observed races.
|
||||
CREATE TABLE IF NOT EXISTS race (
|
||||
-- id is the DB-internal ID of the race.
|
||||
id INTEGER PRIMARY KEY,
|
||||
config_id INTEGER NOT NULL REFERENCES race_config (id),
|
||||
seed INTEGER NOT NULL,
|
||||
|
||||
distance_diff_max REAL NOT NULL,
|
||||
frame_count INTEGER NOT NULL,
|
||||
|
||||
-- There is no consistent ID for races, and even race seeds have been found
|
||||
-- to collide well above the expected rate for a uniform distribution.
|
||||
-- In order to prevent adding duplicates of races each time the tool is run,
|
||||
-- we include the list of all horse IDs present in the race.
|
||||
-- Analysis on the horses in the race should instead use the race_horse table.
|
||||
-- Horse entries here are 0 where there is actually no horse at that gate.
|
||||
horse_1 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_2 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_3 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_4 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_5 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_6 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_7 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_8 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_9 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_10 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_11 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_12 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_13 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_14 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_15 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_16 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_17 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_18 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
|
||||
UNIQUE (
|
||||
config_id,
|
||||
seed,
|
||||
horse_1,
|
||||
horse_2,
|
||||
horse_3,
|
||||
horse_4,
|
||||
horse_5,
|
||||
horse_6,
|
||||
horse_7,
|
||||
horse_8,
|
||||
horse_9,
|
||||
horse_10,
|
||||
horse_11,
|
||||
horse_12,
|
||||
horse_13,
|
||||
horse_14,
|
||||
horse_15,
|
||||
horse_16,
|
||||
horse_17,
|
||||
horse_18
|
||||
)
|
||||
) STRICT;
|
||||
CREATE INDEX IF NOT EXISTS race_by_config ON race (config_id);
|
||||
|
||||
-- race_horse pairs each instance of a horse with its condition and performance
|
||||
-- in a given race.
|
||||
CREATE TABLE IF NOT EXISTS race_horse (
|
||||
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
|
||||
horse INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
|
||||
horse_index INTEGER NOT NULL,
|
||||
post INTEGER NOT NULL,
|
||||
finish_order INTEGER NOT NULL,
|
||||
finish_time_raw REAL NOT NULL,
|
||||
finish_time_scaled REAL NOT NULL,
|
||||
finish_diff_time_from_prev REAL NOT NULL,
|
||||
running_style INTEGER NOT NULL,
|
||||
running_style_ex INTEGER NOT NULL,
|
||||
active_distance_aptitude INTEGER NOT NULL,
|
||||
active_surface_aptitude INTEGER NOT NULL,
|
||||
mood INTEGER NOT NULL,
|
||||
mood_coef REAL NOT NULL,
|
||||
base_speed REAL NOT NULL,
|
||||
base_stamina REAL NOT NULL,
|
||||
base_power REAL NOT NULL,
|
||||
base_guts REAL NOT NULL,
|
||||
base_wit REAL NOT NULL,
|
||||
popularity INTEGER NOT NULL,
|
||||
popularity_rank_left INTEGER NOT NULL,
|
||||
popularity_rank_center INTEGER NOT NULL,
|
||||
popularity_rank_right INTEGER NOT NULL,
|
||||
start_delay REAL NOT NULL,
|
||||
spurt_start_distance REAL NOT NULL,
|
||||
|
||||
PRIMARY KEY (race, horse),
|
||||
UNIQUE (race, horse_index),
|
||||
CHECK (horse != 0)
|
||||
) STRICT;
|
||||
|
||||
-- race_frame holds the actual received frames.
|
||||
CREATE TABLE IF NOT EXISTS race_frame (
|
||||
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
|
||||
frame_time REAL NOT NULL,
|
||||
horse_index INTEGER NOT NULL,
|
||||
distance REAL NOT NULL,
|
||||
lane INTEGER NOT NULL, -- scale -4
|
||||
speed INTEGER NOT NULL, -- scale -2
|
||||
hp INTEGER NOT NULL,
|
||||
temptation INTEGER NOT NULL,
|
||||
block_front_horse_index INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (race, frame_time, horse_index)
|
||||
) STRICT;
|
||||
|
||||
-- race_event holds raw received race events.
|
||||
-- Views such as race_event_skill_activate, race_event_spot_struggle_activate, &c.
|
||||
-- interpret events per type.
|
||||
CREATE TABLE IF NOT EXISTS race_event (
|
||||
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
|
||||
frame_time REAL NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
param_0 INTEGER,
|
||||
param_1 INTEGER,
|
||||
param_2 INTEGER,
|
||||
param_3 INTEGER,
|
||||
param_4 INTEGER,
|
||||
param_5 INTEGER
|
||||
) STRICT;
|
||||
CREATE INDEX IF NOT EXISTS race_event_by_race ON race_event (race);
|
||||
CREATE INDEX IF NOT EXISTS race_event_by_type ON race_event (type);
|
||||
@@ -0,0 +1,36 @@
|
||||
DROP VIEW IF EXISTS race_event_skill_activate;
|
||||
DROP VIEW IF EXISTS race_event_spot_struggle_activate;
|
||||
DROP VIEW IF EXISTS race_event_duel_activate;
|
||||
DROP VIEW IF EXISTS race_event_release_conserve_power;
|
||||
|
||||
CREATE VIEW race_event_skill_activate AS
|
||||
SELECT
|
||||
race,
|
||||
frame_time,
|
||||
param_0 AS user_index,
|
||||
param_1 AS skill,
|
||||
param_2 * 0.0001 AS duration,
|
||||
param_3 AS branch,
|
||||
param_4 AS hit_bitmask
|
||||
FROM race_event WHERE type = 3;
|
||||
|
||||
CREATE VIEW race_event_spot_struggle_activate AS
|
||||
SELECT
|
||||
race,
|
||||
frame_time,
|
||||
param_0 AS user_index
|
||||
FROM race_event WHERE type = 4;
|
||||
|
||||
CREATE VIEW race_event_duel_activate AS
|
||||
SELECT
|
||||
race,
|
||||
frame_time,
|
||||
param_0 AS user_index
|
||||
FROM race_event WHERE type = 5;
|
||||
|
||||
CREATE VIEW race_event_release_conserve_power AS
|
||||
SELECT
|
||||
race,
|
||||
frame_time,
|
||||
param_0 AS user_index
|
||||
FROM race_event WHERE type = 6;
|
||||
+97
-1
@@ -1,6 +1,9 @@
|
||||
package ism
|
||||
|
||||
import "git.sunturtle.xyz/zephyr/horse"
|
||||
import (
|
||||
"git.sunturtle.xyz/zephyr/horse"
|
||||
"git.sunturtle.xyz/zephyr/horse/umadump"
|
||||
)
|
||||
|
||||
type Skill struct {
|
||||
ID horse.SkillID `json:"skill_id"`
|
||||
@@ -225,13 +228,106 @@ type AddItem struct {
|
||||
Number int32 `json:"number"`
|
||||
}
|
||||
|
||||
type PieceData struct {
|
||||
PieceID int32 `json:"piece_id"`
|
||||
PieceNum int32 `json:"piece_num"`
|
||||
}
|
||||
|
||||
type UserCard struct {
|
||||
CardID horse.UmaID `json:"card_id"`
|
||||
Rarity int8 `json:"rarity"`
|
||||
TalentLevel int8 `json:"talent_level"`
|
||||
Skills []Skill `json:"skill_data_array"`
|
||||
CreateTime string `json:"create_time"`
|
||||
}
|
||||
|
||||
type UserSupportCard struct {
|
||||
ViewerID int64 `json:"viewer_id"`
|
||||
SupportCardID horse.SupportCardID `json:"support_card_id"`
|
||||
Exp int32 `json:"exp"`
|
||||
LimitBreak int32 `json:"limit_break_count"`
|
||||
FavoriteFlag int32 `json:"favorite_flag"`
|
||||
Stock int32 `json:"stock"`
|
||||
PossessTime string `json:"possess_time"`
|
||||
}
|
||||
|
||||
type RewardAddSupportCardNum struct {
|
||||
SupportCardID horse.SupportCardID `json:"support_card_id"`
|
||||
Number int32 `json:"number"`
|
||||
ItemType int32 `json:"item_type"`
|
||||
}
|
||||
|
||||
type HonorData struct {
|
||||
HonorID int32 `json:"honor_id"`
|
||||
Step int32 `json:"step"`
|
||||
CreateTime string `json:"create_time"`
|
||||
}
|
||||
|
||||
type UserChara struct {
|
||||
CharaID horse.CharacterID `json:"chara_id"`
|
||||
TrainingNum int32 `json:"training_num"`
|
||||
LovePoint int32 `json:"love_point"`
|
||||
LovePointPool int32 `json:"love_point_pool"`
|
||||
Fan uint64 `json:"fan"`
|
||||
}
|
||||
|
||||
type UserCloth struct {
|
||||
ClothID int32 `json:"cloth_id"`
|
||||
}
|
||||
|
||||
type UserMusic struct {
|
||||
MusicID int32 `json:"music_id"`
|
||||
AcquisitionTime string `json:"acquisition_time"`
|
||||
}
|
||||
|
||||
type CharaProfileData struct {
|
||||
CharaID horse.CharacterID `json:"chara_id"`
|
||||
DataID int32 `json:"data_id"`
|
||||
NewFlag int32 `json:"new_flag"`
|
||||
}
|
||||
|
||||
type RewardSummary struct {
|
||||
AddItem []AddItem `json:"add_item_list"`
|
||||
AddPiece []PieceData `json:"add_piece_list"`
|
||||
AddCard []UserCard `json:"add_card_list"`
|
||||
AddCardBonus *RewardSummary `json:"add_card_bonus_info"`
|
||||
AddSupportCard []UserSupportCard `json:"add_support_card_list"`
|
||||
AddSupportCardNum []RewardAddSupportCardNum `json:"add_support_card_num_array"`
|
||||
AddHonor []HonorData `json:"add_honor_list"`
|
||||
AddChara []UserChara `json:"add_chara_list"`
|
||||
AddCloth []UserCloth `json:"add_cloth_list"`
|
||||
AddMusic []UserMusic `json:"add_music_list"`
|
||||
AddStoryIDs []umadump.Never `json:"add_story_id_array"` // not in my disassembly
|
||||
AddFCoin int32 `json:"add_fcoin"` // carats
|
||||
AddPresents int32 `json:"add_present_num"`
|
||||
AddTotalFan int32 `json:"add_total_fan"`
|
||||
NewCharaProfiles []CharaProfileData `json:"new_chara_profile_array"`
|
||||
ForceUpdateHonorID int32 `json:"force_update_honor_id"`
|
||||
}
|
||||
|
||||
type End struct {
|
||||
Chara SingleModeChara `json:"chara_info"`
|
||||
RaceConditions []umadump.Never `json:"race_condition_array"`
|
||||
UncheckedEvents []umadump.Never `json:"unchecked_event_array"`
|
||||
HomeInfo *umadump.Never `json:"home_info"`
|
||||
WinSaddleIDs []umadump.Never `json:"win_saddle_id_array"`
|
||||
EffectedFactors []umadump.Never `json:"effected_factor_array"`
|
||||
RaceStartInfo *umadump.Never `json:"race_start_info"`
|
||||
RaceScenario *umadump.Never `json:"race_scenario"`
|
||||
AddTrophyInfo *umadump.Never `json:"add_trophy_info"`
|
||||
TrophyRewardInfo *umadump.Never `json:"trophy_reward_info"`
|
||||
PrevCharaGrade *umadump.Never `json:"prev_chara_grade"`
|
||||
RaceAddRewardInfo []umadump.Never `json:"race_add_reward_info"`
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package umadump
|
||||
|
||||
type Never struct{}
|
||||
|
||||
func (Never) AppendText(b []byte) ([]byte, error) {
|
||||
panic("umadump: attempt to encode value that should never exist")
|
||||
}
|
||||
func (Never) MarshalText() ([]byte, error) {
|
||||
panic("umadump: attempt to encode value that should never exist")
|
||||
}
|
||||
func (Never) UnmarshalText(b []byte) error {
|
||||
panic("umadump: attempt to decode value that should never exist")
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
||||
// Code generated by "stringer -type CardRarity -linecomment -trimprefix Card"; DO NOT EDIT.
|
||||
|
||||
package replay
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CardNone-0]
|
||||
_ = x[CardRare1-1]
|
||||
_ = x[CardRare2-2]
|
||||
_ = x[CardRare3-3]
|
||||
_ = x[CardRare4-4]
|
||||
_ = x[CardRare5-5]
|
||||
}
|
||||
|
||||
const _CardRarity_name = "NONERare1Rare2Rare3Rare4Rare5"
|
||||
|
||||
var _CardRarity_index = [...]uint8{0, 4, 9, 14, 19, 24, 29}
|
||||
|
||||
func (i CardRarity) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_CardRarity_index)-1 {
|
||||
return "CardRarity(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _CardRarity_name[_CardRarity_index[idx]:_CardRarity_index[idx+1]]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Code generated by "stringer -type Motivation -linecomment"; DO NOT EDIT.
|
||||
|
||||
package replay
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[MotivationNone-0]
|
||||
_ = x[MotivationAwful-1]
|
||||
_ = x[MotivationBad-2]
|
||||
_ = x[MotivationNormal-3]
|
||||
_ = x[MotivationGood-4]
|
||||
_ = x[MotivationGreat-5]
|
||||
}
|
||||
|
||||
const _Motivation_name = "NoneMinLowMiddleHighMax"
|
||||
|
||||
var _Motivation_index = [...]uint8{0, 4, 7, 10, 16, 20, 23}
|
||||
|
||||
func (i Motivation) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_Motivation_index)-1 {
|
||||
return "Motivation(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Motivation_name[_Motivation_index[idx]:_Motivation_index[idx+1]]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by "stringer -type ProperGrade -trimprefix Aptitude"; DO NOT EDIT.
|
||||
|
||||
package replay
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[AptitudeNull-0]
|
||||
_ = x[AptitudeG-1]
|
||||
_ = x[AptitudeF-2]
|
||||
_ = x[AptitudeE-3]
|
||||
_ = x[AptitudeD-4]
|
||||
_ = x[AptitudeC-5]
|
||||
_ = x[AptitudeB-6]
|
||||
_ = x[AptitudeA-7]
|
||||
_ = x[AptitudeS-8]
|
||||
}
|
||||
|
||||
const _ProperGrade_name = "NullGFEDCBAS"
|
||||
|
||||
var _ProperGrade_index = [...]uint8{0, 4, 5, 6, 7, 8, 9, 10, 11, 12}
|
||||
|
||||
func (i ProperGrade) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_ProperGrade_index)-1 {
|
||||
return "ProperGrade(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _ProperGrade_name[_ProperGrade_index[idx]:_ProperGrade_index[idx+1]]
|
||||
}
|
||||
+341
-253
@@ -6,6 +6,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse"
|
||||
"git.sunturtle.xyz/zephyr/horse/umadump"
|
||||
)
|
||||
|
||||
type Race struct {
|
||||
@@ -46,10 +47,10 @@ type Race struct {
|
||||
PostNumberMax int32 `json:"postNumberMax"`
|
||||
PlayerHorseIndex int32 `json:"playerHorseIndex"`
|
||||
OverridePlayerHorseIndex int32 `json:"overridePlayerHorseIndex"`
|
||||
PlayerTeamMemberArray []RaceHorse `json:"playerTeamMemberArray"`
|
||||
PlayerTeamTopFinishOrderHorse RaceHorse `json:"playerTeamTopFinishOrderHorse"`
|
||||
PlayerTeamMemberArray []HorseData `json:"playerTeamMemberArray"`
|
||||
PlayerTeamTopFinishOrderHorse HorseData `json:"playerTeamTopFinishOrderHorse"`
|
||||
IsGateInPopularityInitialized bool `json:"isGateInPopularityInitialized"`
|
||||
RaceHorse []RaceHorse `json:"raceHorse"`
|
||||
RaceHorse []HorseData `json:"raceHorse"`
|
||||
RaceBibMaster struct{} `json:"raceBibMaster"`
|
||||
RaceMaster struct{} `json:"raceMaster"`
|
||||
RaceInstanceMaster struct{} `json:"raceInstanceMaster"`
|
||||
@@ -64,7 +65,7 @@ type Race struct {
|
||||
ReplayCheckInfoLegend struct{} `json:"replayCheckInfoLegend"`
|
||||
IsDailyLegendRace bool `json:"isDailyLegendRace"`
|
||||
ReplayCheckInfoChallengeMatch struct{} `json:"replayCheckInfoChallengeMatch"`
|
||||
RaceRewardSingle RaceRewardSingle `json:"raceRewardSingle"`
|
||||
RaceRewardSingle struct{} `json:"raceRewardSingle"`
|
||||
ResultHorseIndex int32 `json:"resultHorseIndex"`
|
||||
PrevGradeType CharaGradeType `json:"prevGradeType"`
|
||||
MainStoryRaceGimmickType MainStoryRaceGimmickType `json:"mainStoryRaceGimmickType"`
|
||||
@@ -547,112 +548,325 @@ type RaceParam struct {
|
||||
BasePow float32 `json:"basePow"`
|
||||
BaseGuts float32 `json:"baseGuts"`
|
||||
BaseWiz float32 `json:"baseWiz"`
|
||||
Motivation string `json:"motivation"`
|
||||
Motivation Motivation `json:"motivation"`
|
||||
MotivationCoef float32 `json:"motivationCoef"`
|
||||
}
|
||||
|
||||
type SkillArray struct {
|
||||
SkillID int `json:"skill_id"`
|
||||
Level int `json:"level"`
|
||||
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
|
||||
}
|
||||
|
||||
type RaceResultArray struct {
|
||||
Turn int `json:"turn"`
|
||||
ProgramID int `json:"program_id"`
|
||||
Weather int `json:"weather"`
|
||||
GroundCondition int `json:"ground_condition"`
|
||||
RunningStyle int `json:"running_style"`
|
||||
Popularity int `json:"popularity"`
|
||||
ResultRank int `json:"result_rank"`
|
||||
ResultTime int `json:"result_time"`
|
||||
PrizeMoney int `json:"prize_money"`
|
||||
func (r Motivation) MarshalText() ([]byte, error) {
|
||||
return r.AppendText(nil)
|
||||
}
|
||||
type ResponseHorseData struct {
|
||||
FrameOrder int `json:"frame_order"`
|
||||
|
||||
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 {
|
||||
SkillID horse.SkillID `json:"skill_id"`
|
||||
Level int32 `json:"level"`
|
||||
}
|
||||
|
||||
type RaceResult struct {
|
||||
Turn int32 `json:"turn"`
|
||||
ProgramID int32 `json:"program_id"`
|
||||
Weather int32 `json:"weather"`
|
||||
GroundCondition int32 `json:"ground_condition"`
|
||||
RunningStyle int32 `json:"running_style"`
|
||||
Popularity int32 `json:"popularity"`
|
||||
ResultRank int32 `json:"result_rank"`
|
||||
ResultTime int32 `json:"result_time"`
|
||||
PrizeMoney int32 `json:"prize_money"`
|
||||
}
|
||||
|
||||
type RaceHorseData struct {
|
||||
FrameOrder int32 `json:"frame_order"`
|
||||
ViewerID int64 `json:"viewer_id"`
|
||||
TrainerName string `json:"trainer_name"`
|
||||
OwnerViewerID int `json:"owner_viewer_id"`
|
||||
TrainerName *string `json:"trainer_name"`
|
||||
OwnerViewerID int64 `json:"owner_viewer_id"`
|
||||
OwnerTrainerName string `json:"owner_trainer_name"`
|
||||
SingleModeCharaID int `json:"single_mode_chara_id"`
|
||||
TrainedCharaID int `json:"trained_chara_id"`
|
||||
NicknameID int `json:"nickname_id"`
|
||||
CharaID int `json:"chara_id"`
|
||||
CardID int `json:"card_id"`
|
||||
MobID int `json:"mob_id"`
|
||||
Rarity int `json:"rarity"`
|
||||
TalentLevel int `json:"talent_level"`
|
||||
SingleModeCharaID int32 `json:"single_mode_chara_id"`
|
||||
TrainedCharaID int32 `json:"trained_chara_id"`
|
||||
NicknameID int32 `json:"nickname_id"`
|
||||
CharaID int32 `json:"chara_id"`
|
||||
CardID int32 `json:"card_id"`
|
||||
MobID int32 `json:"mob_id"`
|
||||
Rarity int32 `json:"rarity"`
|
||||
TalentLevel int32 `json:"talent_level"`
|
||||
SkillArray []SkillArray `json:"skill_array"`
|
||||
Stamina int `json:"stamina"`
|
||||
Speed int `json:"speed"`
|
||||
Pow int `json:"pow"`
|
||||
Guts int `json:"guts"`
|
||||
Wiz int `json:"wiz"`
|
||||
RunningStyle int `json:"running_style"`
|
||||
RaceDressID int `json:"race_dress_id"`
|
||||
CharaColorType int `json:"chara_color_type"`
|
||||
NpcType int `json:"npc_type"`
|
||||
FinalGrade int `json:"final_grade"`
|
||||
Popularity int `json:"popularity"`
|
||||
PopularityMarkRankArray []int `json:"popularity_mark_rank_array"`
|
||||
ProperDistanceShort int `json:"proper_distance_short"`
|
||||
ProperDistanceMile int `json:"proper_distance_mile"`
|
||||
ProperDistanceMiddle int `json:"proper_distance_middle"`
|
||||
ProperDistanceLong int `json:"proper_distance_long"`
|
||||
ProperRunningStyleNige int `json:"proper_running_style_nige"`
|
||||
ProperRunningStyleSenko int `json:"proper_running_style_senko"`
|
||||
ProperRunningStyleSashi int `json:"proper_running_style_sashi"`
|
||||
ProperRunningStyleOikomi int `json:"proper_running_style_oikomi"`
|
||||
ProperGroundTurf int `json:"proper_ground_turf"`
|
||||
ProperGroundDirt int `json:"proper_ground_dirt"`
|
||||
Motivation int `json:"motivation"`
|
||||
WinSaddleIDArray []int `json:"win_saddle_id_array"`
|
||||
RaceResultArray []RaceResultArray `json:"race_result_array"`
|
||||
TeamID int `json:"team_id"`
|
||||
TeamMemberID int `json:"team_member_id"`
|
||||
TeamRank int `json:"team_rank"`
|
||||
SingleModeWinCount int `json:"single_mode_win_count"`
|
||||
ItemIDArray []any `json:"item_id_array"`
|
||||
MotivationChangeFlag int `json:"motivation_change_flag"`
|
||||
FrameOrderChangeFlag int `json:"frame_order_change_flag"`
|
||||
Stamina int32 `json:"stamina"`
|
||||
Speed int32 `json:"speed"`
|
||||
Pow int32 `json:"pow"`
|
||||
Guts int32 `json:"guts"`
|
||||
Wiz int32 `json:"wiz"`
|
||||
RunningStyle int32 `json:"running_style"`
|
||||
RaceDressID int32 `json:"race_dress_id"`
|
||||
CharaColorType int32 `json:"chara_color_type"`
|
||||
NpcType int32 `json:"npc_type"`
|
||||
FinalGrade int32 `json:"final_grade"`
|
||||
Popularity int32 `json:"popularity"`
|
||||
PopularityMarkRankArray []int32 `json:"popularity_mark_rank_array"`
|
||||
ProperDistanceShort int32 `json:"proper_distance_short"`
|
||||
ProperDistanceMile int32 `json:"proper_distance_mile"`
|
||||
ProperDistanceMiddle int32 `json:"proper_distance_middle"`
|
||||
ProperDistanceLong int32 `json:"proper_distance_long"`
|
||||
ProperRunningStyleNige int32 `json:"proper_running_style_nige"`
|
||||
ProperRunningStyleSenko int32 `json:"proper_running_style_senko"`
|
||||
ProperRunningStyleSashi int32 `json:"proper_running_style_sashi"`
|
||||
ProperRunningStyleOikomi int32 `json:"proper_running_style_oikomi"`
|
||||
ProperGroundTurf int32 `json:"proper_ground_turf"`
|
||||
ProperGroundDirt int32 `json:"proper_ground_dirt"`
|
||||
Motivation int32 `json:"motivation"`
|
||||
WinSaddleIDArray []int32 `json:"win_saddle_id_array"`
|
||||
RaceResultArray []RaceResult `json:"race_result_array"`
|
||||
TeamID int32 `json:"team_id"`
|
||||
TeamMemberID int32 `json:"team_member_id"`
|
||||
TeamRank int32 `json:"team_rank"`
|
||||
SingleModeWinCount int32 `json:"single_mode_win_count"`
|
||||
FanCount int32 `json:"fan_count"`
|
||||
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"`
|
||||
FactorID int `json:"factorId"`
|
||||
BaseFactorID int `json:"baseFactorId"`
|
||||
UpgradeHistoryList []any `json:"upgradeHistoryList"`
|
||||
|
||||
type RunningStyleEx int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type RunningStyleEx -linecomment
|
||||
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 {
|
||||
PositionID int `json:"positionId"`
|
||||
CardID int `json:"cardId"`
|
||||
Rarity int `json:"rarity"`
|
||||
Level int `json:"level"`
|
||||
Rank int `json:"rank"`
|
||||
FactorDataArray []FactorDataArray `json:"factorDataArray"`
|
||||
SortedFactorList []any `json:"sortedFactorList"`
|
||||
SortedFactorListForProfileCard []any `json:"sortedFactorListForProfileCard"`
|
||||
OwnerViewerID int `json:"ownerViewerId"`
|
||||
PositionID int32 `json:"positionId"`
|
||||
CardID int32 `json:"cardId"`
|
||||
Rarity int32 `json:"rarity"`
|
||||
Level int32 `json:"level"`
|
||||
Rank int32 `json:"rank"`
|
||||
FactorDataArray []FactorData `json:"factorDataArray"`
|
||||
SortedFactorList []umadump.Never `json:"sortedFactorList"`
|
||||
SortedFactorListForProfileCard []umadump.Never `json:"sortedFactorListForProfileCard"`
|
||||
OwnerViewerID int64 `json:"ownerViewerId"`
|
||||
IsPlayer bool `json:"isPlayer"`
|
||||
WinSaddleArray []any `json:"winSaddleArray"`
|
||||
WinSaddleIDArray []int `json:"winSaddleIdArray"`
|
||||
WinSaddleArray []umadump.Never `json:"winSaddleArray"`
|
||||
WinSaddleIDArray []int32 `json:"winSaddleIdArray"`
|
||||
}
|
||||
type Master struct {
|
||||
}
|
||||
type AcquiredSkillArray struct {
|
||||
MasterID int `json:"masterId"`
|
||||
|
||||
type AcquiredSkill struct {
|
||||
MasterID horse.SkillID `json:"masterId"`
|
||||
Level int `json:"level"`
|
||||
Master Master `json:"master"`
|
||||
Master struct{} `json:"master"`
|
||||
}
|
||||
type SupportCardArray struct {
|
||||
Position int `json:"position"`
|
||||
SupportCardID int `json:"supportCardId"`
|
||||
LimitBreakCount int `json:"limitBreakCount"`
|
||||
Exp int `json:"exp"`
|
||||
|
||||
type SupportCard struct {
|
||||
Position int32 `json:"position"`
|
||||
SupportCardID horse.SupportCardID `json:"supportCardId"`
|
||||
LimitBreakCount int32 `json:"limitBreakCount"`
|
||||
Exp int32 `json:"exp"`
|
||||
}
|
||||
type SingleModeRaceResultArray struct {
|
||||
|
||||
type SingleModeRaceResult struct {
|
||||
Turn int `json:"turn"`
|
||||
ProgramID int `json:"programId"`
|
||||
RaceInstanceID int `json:"raceInstanceId"`
|
||||
@@ -664,172 +878,46 @@ type SingleModeRaceResultArray struct {
|
||||
ResultRank int `json:"resultRank"`
|
||||
ScenarioID int `json:"scenarioId"`
|
||||
}
|
||||
type MasterCardData struct {
|
||||
}
|
||||
type MasterCharaData struct {
|
||||
}
|
||||
type MasterCardRarityData struct {
|
||||
}
|
||||
type TrainedCharaDataAccessor struct {
|
||||
}
|
||||
type TrainedCharaData struct {
|
||||
ID int `json:"id"`
|
||||
IsSaved bool `json:"isSaved"`
|
||||
ViewerID int64 `json:"viewerId"`
|
||||
OwnerViewerID int `json:"ownerViewerId"`
|
||||
OwnerTrainedCharaID int `json:"ownerTrainedCharaId"`
|
||||
UseType int `json:"useType"`
|
||||
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 CardRarity int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type CardRarity -linecomment -trimprefix Card
|
||||
const (
|
||||
CardNone CardRarity = iota // NONE
|
||||
CardRare1
|
||||
CardRare2
|
||||
CardRare3
|
||||
CardRare4
|
||||
CardRare5
|
||||
)
|
||||
|
||||
func (r CardRarity) AppendText(b []byte) ([]byte, error) {
|
||||
return append(b, r.String()...), nil
|
||||
}
|
||||
|
||||
type RaceHorse 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,omitempty"`
|
||||
func (r CardRarity) MarshalText() ([]byte, error) {
|
||||
return r.AppendText(nil)
|
||||
}
|
||||
|
||||
type RaceRewardSingle struct {
|
||||
// TODO(zeph): this
|
||||
func (r *CardRarity) UnmarshalText(b []byte) error {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package replay_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
|
||||
)
|
||||
|
||||
//go:embed Mihono_Bourbon-74.2421s-20260829.json
|
||||
var testRace string
|
||||
|
||||
func TestRaceUnmarshal(t *testing.T) {
|
||||
// A replay is simply too large to bother converting by hand for a unit test.
|
||||
// Just test that we can unmarshal and certain important fields are populated.
|
||||
d := json.NewDecoder(strings.NewReader(testRace))
|
||||
d.DisallowUnknownFields()
|
||||
var got replay.Race
|
||||
if err := d.Decode(&got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got.RaceType != replay.RaceTypeChampions {
|
||||
t.Errorf("wrong race type: want %v, got %v", replay.RaceTypeChampions, got.RaceType)
|
||||
}
|
||||
if got.RandomSeed != 1667907263 {
|
||||
t.Errorf("wrong random seed: want 1667907263, got %d", got.RandomSeed)
|
||||
}
|
||||
if got.RaceCourseSet.ID != 10903 {
|
||||
t.Errorf("wrong race course: want 10903, got %d", got.RaceCourseSet.ID)
|
||||
}
|
||||
if got.GroundCondition != replay.GroundFirm {
|
||||
t.Errorf("wrong ground condition: want %v, got %v", replay.GroundFirm, got.GroundCondition)
|
||||
}
|
||||
if got.Weather != replay.WeatherCloudy {
|
||||
t.Errorf("wrong weather: want %v, got %v", replay.WeatherCloudy, got.Weather)
|
||||
}
|
||||
if got.NumRaceHorses != 9 {
|
||||
t.Errorf("wrong number of race horses: want 9, got %d", got.NumRaceHorses)
|
||||
}
|
||||
if len(got.PlayerTeamMemberArray) != 3 {
|
||||
t.Errorf("wrong number of team members: want 3, got %d", len(got.PlayerTeamMemberArray))
|
||||
}
|
||||
for _, m := range got.PlayerTeamMemberArray {
|
||||
if m.CharaID == 0 {
|
||||
t.Error("missing team member chara id")
|
||||
}
|
||||
if m.FinishTimeRaw == 0 {
|
||||
t.Error("missing team member finish time raw")
|
||||
}
|
||||
if m.RaceParam.RawSpeed == 0 {
|
||||
t.Error("missing team member race param raw speed")
|
||||
}
|
||||
if m.RaceParam.BaseSpeed == 0 {
|
||||
t.Error("missing team member race param base speed")
|
||||
}
|
||||
if m.ResponseHorseData.ViewerID == 0 {
|
||||
t.Error("missing team member response horse data viewer id")
|
||||
}
|
||||
if m.ResponseHorseData.TrainedCharaID == 0 {
|
||||
t.Error("missing team member response horse data trained chara id")
|
||||
}
|
||||
if m.ResponseHorseData.CardID == 0 {
|
||||
t.Error("missing team member response horse data card id")
|
||||
}
|
||||
if len(m.ResponseHorseData.SkillArray) == 0 {
|
||||
t.Error("missing team member response horse data skill array")
|
||||
}
|
||||
if m.ResponseHorseData.Speed == 0 {
|
||||
t.Error("missing team member response horse data speed")
|
||||
}
|
||||
if m.ResponseHorseData.RunningStyle == 0 {
|
||||
t.Error("missing team member response horse data running style")
|
||||
}
|
||||
if m.ActiveProperDistance == 0 {
|
||||
t.Error("missing team member active distance aptitude")
|
||||
}
|
||||
if m.ActiveProperGroundType == 0 {
|
||||
t.Error("missing team member active surface aptitude")
|
||||
}
|
||||
if m.TrainedCharaData.ID == 0 {
|
||||
t.Error("missing team member trained chara id")
|
||||
}
|
||||
if m.TrainedCharaData.CardID == 0 {
|
||||
t.Error("missing team member trained chara card id")
|
||||
}
|
||||
if m.TrainedCharaData.Speed == 0 {
|
||||
t.Error("missing team member trained chara speed")
|
||||
}
|
||||
if m.TrainedCharaData.RunningStyle == 0 {
|
||||
t.Error("missing team member trained chara running style")
|
||||
}
|
||||
if m.TrainedCharaData.CreateTime == "" {
|
||||
t.Error("missing team member trained chara create time")
|
||||
}
|
||||
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
|
||||
t.Error("missing team member trained chara acquired skill array")
|
||||
}
|
||||
}
|
||||
if len(got.RaceHorse) != int(got.NumRaceHorses) {
|
||||
t.Errorf("wrong number of race horses: want %d, got %d", got.NumRaceHorses, len(got.RaceHorse))
|
||||
}
|
||||
zeroFinish, zeroDelta := 0, 0
|
||||
for _, m := range got.RaceHorse {
|
||||
if m.CharaID == 0 {
|
||||
t.Error("missing race horse chara id")
|
||||
}
|
||||
if m.FinishOrder == 0 {
|
||||
zeroFinish++
|
||||
}
|
||||
if m.FinishTimeRaw == 0 {
|
||||
t.Error("missing race horse finish time raw")
|
||||
}
|
||||
if m.FinishDiffTimeFromPrev == 0 {
|
||||
zeroDelta++
|
||||
}
|
||||
if m.RaceParam.RawSpeed == 0 {
|
||||
t.Error("missing race horse race param raw speed")
|
||||
}
|
||||
if m.RaceParam.BaseSpeed == 0 {
|
||||
t.Error("missing race horse race param base speed")
|
||||
}
|
||||
if m.ResponseHorseData.ViewerID == 0 {
|
||||
t.Error("missing race horse response horse data viewer id")
|
||||
}
|
||||
if m.ResponseHorseData.TrainedCharaID == 0 {
|
||||
t.Error("missing race horse response horse data trained chara id")
|
||||
}
|
||||
if m.ResponseHorseData.CardID == 0 {
|
||||
t.Error("missing race horse response horse data card id")
|
||||
}
|
||||
if len(m.ResponseHorseData.SkillArray) == 0 {
|
||||
t.Error("missing race horse response horse data skill array")
|
||||
}
|
||||
if m.ResponseHorseData.Speed == 0 {
|
||||
t.Error("missing race horse response horse data speed")
|
||||
}
|
||||
if m.ResponseHorseData.RunningStyle == 0 {
|
||||
t.Error("missing race horse response horse data running style")
|
||||
}
|
||||
if m.ActiveProperDistance == 0 {
|
||||
t.Error("missing race horse active distance aptitude")
|
||||
}
|
||||
if m.ActiveProperGroundType == 0 {
|
||||
t.Error("missing race horse active surface aptitude")
|
||||
}
|
||||
if m.TrainedCharaData.ID == 0 {
|
||||
t.Error("missing race horse trained chara id")
|
||||
}
|
||||
if m.TrainedCharaData.CardID == 0 {
|
||||
t.Error("missing race horse trained chara card id")
|
||||
}
|
||||
if m.TrainedCharaData.Speed == 0 {
|
||||
t.Error("missing race horse trained chara speed")
|
||||
}
|
||||
if m.TrainedCharaData.RunningStyle == 0 {
|
||||
t.Error("missing race horse trained chara running style")
|
||||
}
|
||||
if m.TrainedCharaData.CreateTime == "" {
|
||||
t.Error("missing race horse trained chara create time")
|
||||
}
|
||||
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
|
||||
t.Error("missing race horse trained chara acquired skill array")
|
||||
}
|
||||
}
|
||||
if zeroFinish != 1 {
|
||||
t.Errorf("%d horses in index 0 finish", zeroFinish)
|
||||
}
|
||||
if zeroDelta != 1 {
|
||||
t.Errorf("%d horses with zero finish time delta", zeroDelta)
|
||||
}
|
||||
if got.SimDataBase64 != encodedSimdata {
|
||||
t.Errorf("wrong simdata:\nwant %s\n got %s", encodedSimdata, got.SimDataBase64)
|
||||
}
|
||||
if len(got.HorseIndexByFinishOrder) != len(got.RaceHorse) {
|
||||
t.Errorf("length of horseIndexByFinishOrder (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByFinishOrder), len(got.RaceHorse))
|
||||
}
|
||||
if len(got.HorseIndexByPopularity) != len(got.RaceHorse) {
|
||||
t.Errorf("length of horseIndexByPopularity (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByPopularity), len(got.RaceHorse))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
@@ -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>
|
||||
@@ -20,6 +20,7 @@
|
||||
skill_id: 0,
|
||||
name: 'Skill data is loading...',
|
||||
description: 'Skill data is still loading, or else something went wrong.',
|
||||
group_skills: [],
|
||||
group: 0,
|
||||
rarity: 1,
|
||||
group_rate: 1,
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { RegionalName } from '$lib/regional-name';
|
||||
* Lobby conversation data.
|
||||
*/
|
||||
export interface Conversation {
|
||||
/**
|
||||
* Gallery ID.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Character who owns the conversation as a gallery entry.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,10 @@ export interface Skill {
|
||||
* Regional skil description.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Other skills in the skill group.
|
||||
*/
|
||||
group_skills: GroupSkill[];
|
||||
/**
|
||||
* Skill group ID.
|
||||
*/
|
||||
@@ -58,6 +62,18 @@ export interface Skill {
|
||||
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 {
|
||||
Direct = 1,
|
||||
FrontDistance = 2,
|
||||
|
||||
@@ -771,16 +771,17 @@ export function conservePowerAccel(
|
||||
distanceType: Distance,
|
||||
style: RunningStyle,
|
||||
rawPower: number,
|
||||
basePower: number,
|
||||
powerSkillBonus?: number,
|
||||
spotStruggled?: boolean,
|
||||
rushed?: boolean,
|
||||
): number {
|
||||
const power = rawPower + (powerSkillBonus ?? 0);
|
||||
const power = basePower + 0.5 * Math.max(rawPower - 1200, 0) + (powerSkillBonus ?? 0);
|
||||
if (power <= 1200) {
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
const Entry = z.strictObject({
|
||||
home_story_trigger_id: z.int32(),
|
||||
new_flag: z.literal(0).or(z.literal(1)),
|
||||
});
|
||||
|
||||
export type Entry = z.infer<typeof Entry>;
|
||||
|
||||
export const Gallery = z.object({
|
||||
talk_gallery_list: z.array(Entry),
|
||||
});
|
||||
|
||||
export type Gallery = z.infer<typeof Gallery>;
|
||||
@@ -9,25 +9,51 @@
|
||||
requiredCharas,
|
||||
convoCharas,
|
||||
} from '$lib/data/convo';
|
||||
import * as talk from '$lib/umadump/talk';
|
||||
import CharaPick from '$lib/CharaPick.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import CharaIcon from '$lib/CharaIcon.svelte';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
let conversations: Conversation[] = $state([]);
|
||||
const convoMap = $derived(byChara(conversations));
|
||||
const popular = $derived(groupPopulars(conversations));
|
||||
let allConvos: Conversation[] = $state([]);
|
||||
const popular = $derived(groupPopulars(allConvos));
|
||||
let characters: Character[] = $state([]);
|
||||
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
|
||||
const names = $derived(charaNames(characters));
|
||||
const minSuggest = 8;
|
||||
|
||||
onMount(async () => {
|
||||
[conversations, characters] = await Promise.all([conversation(), character()]);
|
||||
[allConvos, characters] = await Promise.all([conversation(), character()]);
|
||||
});
|
||||
|
||||
let chara: Character | undefined = $state();
|
||||
let convo = $state(1);
|
||||
|
||||
let talkFiles: FileList | undefined = $state();
|
||||
let readIDs = new SvelteSet<number>();
|
||||
let talkFileInvalid = $state(false);
|
||||
async function setRead(f: FileList) {
|
||||
talkFiles = f;
|
||||
readIDs.clear();
|
||||
talkFileInvalid = false;
|
||||
if (talkFiles == null || talkFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
const t = await talkFiles[0].text();
|
||||
const d = JSON.parse(t);
|
||||
const g = talk.Gallery.safeDecode(d);
|
||||
if (!g.success) {
|
||||
talkFileInvalid = true;
|
||||
console.warn(g.error);
|
||||
return;
|
||||
}
|
||||
for (const e of g.data.talk_gallery_list) {
|
||||
readIDs.add(e.home_story_trigger_id);
|
||||
}
|
||||
}
|
||||
const conversations = $derived(allConvos.filter((c) => !readIDs.has(c.id)));
|
||||
const convoMap = $derived(byChara(conversations));
|
||||
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
|
||||
|
||||
const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
|
||||
const cur = $derived(options.find((c) => c.number === convo));
|
||||
const charaIDs = $derived(cur != null ? convoCharas(cur) : []);
|
||||
@@ -47,12 +73,12 @@
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Lobby Conversations</h1>
|
||||
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row">
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<div class="mx-auto mt-8 grid grid-cols-1 gap-4 rounded-md p-4 text-center shadow-md ring md:max-w-xl md:grid-cols-6">
|
||||
<div class="md:col-span-3">
|
||||
<label for="chara" class="hidden md:inline">Character</label>
|
||||
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
|
||||
</div>
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<div class="md:col-span-3">
|
||||
<label for="convo" class="hidden md:inline">Conversation</label>
|
||||
<select id="convo" bind:value={convo} class="w-full">
|
||||
{#each options as opt (opt.number)}
|
||||
@@ -60,6 +86,29 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<label for="talkgallery" class="block">
|
||||
Select
|
||||
<a href="https://werseter.github.io/umadump/" target="_blank" rel="noopener noreferrer">
|
||||
<span class="inline-block font-mono">talk_gallery_data.json</span>
|
||||
</a>
|
||||
to hide read conversations.
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
class="rounded-full p-4"
|
||||
accept="application/json"
|
||||
id="talkgallery"
|
||||
bind:files={() => talkFiles ?? new DataTransfer().files, setRead}
|
||||
/>
|
||||
<button
|
||||
class="ml-4 rounded-full bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
|
||||
onclick={() => setRead(new DataTransfer().files)}>Reset</button
|
||||
>
|
||||
</div>
|
||||
{#if talkFileInvalid}
|
||||
<div class="col-span-full">Invalid File</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if cur}
|
||||
<div class="shadow-sm transition-shadow hover:shadow-md">
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import Article from '../Article.svelte';
|
||||
import Sec from '../Sec.svelte';
|
||||
import * as skill from '$lib/data/skill';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
|
||||
let raceLen = $state(2000);
|
||||
let secSpeedStyle = $state(RunningStyle.FrontRunner);
|
||||
@@ -224,10 +225,10 @@
|
||||
},
|
||||
]);
|
||||
const fcSeries: ComputedSeries[] = $derived([
|
||||
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) },
|
||||
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) },
|
||||
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) },
|
||||
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) },
|
||||
{ label: 'Front Runner', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, raw, x) },
|
||||
{ label: 'Pace Chaser', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, raw, x) },
|
||||
{ label: 'Late Surger', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, raw, x) },
|
||||
{ label: 'End Closer', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, raw, x) },
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -514,23 +515,13 @@
|
||||
plotOptions={{ color: { legend: true } }}
|
||||
/>
|
||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||
<label class="my-auto hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
|
||||
<input
|
||||
class="my-auto max-w-40 flex-1 md:flex-2"
|
||||
type="range"
|
||||
id="secSpeedRaceLen"
|
||||
min="1000"
|
||||
max="3600"
|
||||
step="100"
|
||||
bind:value={raceLen}
|
||||
/>
|
||||
<span class="my-auto flex-none pl-2 text-sm">{raceLen}m</span>
|
||||
<label class="my-auto mr-2 hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a
|
||||
href="#spot-struggle">spot struggle</a
|
||||
> both reduce the chance for it to fire.
|
||||
Rushed and <a href="#spot-struggle">spot struggle</a> reduce both the duration and effectiveness of full charge, potentially to
|
||||
zero. In practice, this appears to be a concern primarily on sprints, which already have a -55% penalty to full charge duration.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="lane-combo">Lane Combo</Sec>
|
||||
@@ -670,17 +661,8 @@
|
||||
plotOptions={{ color: { legend: true } }}
|
||||
/>
|
||||
<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>
|
||||
<input
|
||||
class="my-auto max-w-40 flex-1 md:flex-2"
|
||||
type="range"
|
||||
id="secSpeedRaceLen"
|
||||
min="1000"
|
||||
max="3600"
|
||||
step="100"
|
||||
bind:value={raceLen}
|
||||
/>
|
||||
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
|
||||
<label class="my-auto mr-2 hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
<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}>
|
||||
{#each RUNNING_STYLES as [name, style] (style)}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import * as race from '$lib/race';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
|
||||
function modemean(x: number, bonus: number, p: number): number {
|
||||
return x * (1 - p) + x * bonus * p;
|
||||
@@ -58,10 +59,10 @@
|
||||
]);
|
||||
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
|
||||
const conservePower: Array<ComputedSeries | null> = $derived([
|
||||
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) },
|
||||
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) },
|
||||
{ label: 'Conserved Power', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, false) },
|
||||
{ label: 'Rushed', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, true) },
|
||||
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,
|
||||
]);
|
||||
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">
|
||||
{#if pick?.len}
|
||||
<input
|
||||
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="col-span-1 my-auto pl-2 text-sm">{raceLen}m</span>
|
||||
<div class="col-span-3 my-auto mr-4">
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if pick?.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
|
||||
modifiers for ground conditions and career mode do not apply.
|
||||
</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 })}
|
||||
<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>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as accel from '$lib/racelib/accel';
|
||||
import type { Attachment } from 'svelte/attachments';
|
||||
import CalcInfo from '../CalcInfo.svelte';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
|
||||
let rawSpeed = $state(1200);
|
||||
let rawPower = $state(1200);
|
||||
@@ -231,16 +232,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6">
|
||||
<input
|
||||
class="col-span-4 m-4 w-full justify-self-center-safe"
|
||||
type="range"
|
||||
id="raceLen"
|
||||
min="1000"
|
||||
max="3600"
|
||||
step="100"
|
||||
bind:value={raceLen}
|
||||
/>
|
||||
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
|
||||
<div class="col-span-full m-4">
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
<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.Dirt}>Dirt</option>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as accel from '$lib/racelib/accel';
|
||||
import type { Attachment } from 'svelte/attachments';
|
||||
import CalcInfo from '../CalcInfo.svelte';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
|
||||
let rawPower = $state(1200);
|
||||
let rawGuts = $state(1200);
|
||||
@@ -216,16 +217,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
|
||||
<input
|
||||
class="col-span-4 m-4 w-full justify-self-center-safe"
|
||||
type="range"
|
||||
id="raceLen"
|
||||
min="1000"
|
||||
max="3600"
|
||||
step="100"
|
||||
bind:value={raceLen}
|
||||
/>
|
||||
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
|
||||
<div class="col-span-6 mx-4 mt-4">
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
<select id="surface" bind:value={surface} class="col-span-2 m-4">
|
||||
<option value={race.Surface.Turf}>Turf</option>
|
||||
<option value={race.Surface.Dirt}>Dirt</option>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import SpeedDur from './SpeedDur.svelte';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
|
||||
let rawPower = $state(1200);
|
||||
let rawGuts = $state(1200);
|
||||
@@ -112,8 +113,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4 md:col-span-2 md:col-start-2">
|
||||
<label for="raceLen">Race Distance</label>
|
||||
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
<div class="m-4 self-center md:col-span-2">
|
||||
<label for="isRunaway" class="mr-1 align-middle">Runaway</label>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { ComputedSeries } from '$lib/chart';
|
||||
import * as race from '$lib/race';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import CalcInfo from '../CalcInfo.svelte';
|
||||
|
||||
@@ -98,8 +99,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4 md:col-start-2">
|
||||
<label for="raceLen">Race Distance</label>
|
||||
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
<div class="m-4 self-center">
|
||||
<label for="isCareer" class="mr-1 align-middle">In Career</label>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as course from '$lib/course';
|
||||
import * as race from '$lib/race';
|
||||
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
|
||||
import * as recovery from '$lib/racelib/recovery';
|
||||
import HPChart from './HPChart.svelte';
|
||||
import InfoPanels from './InfoPanels.svelte';
|
||||
@@ -277,16 +278,7 @@
|
||||
</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="m-4 flex md:col-span-2">
|
||||
<input
|
||||
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>
|
||||
<RaceLenSlider bind:value={raceLen} />
|
||||
</div>
|
||||
<select id="surface" class="m-4" bind:value={surface}>
|
||||
<option value={race.Surface.Turf}>Turf</option>
|
||||
|
||||
Reference in New Issue
Block a user