Compare commits
7
Commits
939012b511
..
ismdb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f758fcbe5 | ||
|
|
7f1aa00be6 | ||
|
|
16a5433594 | ||
|
|
75558f35ba | ||
|
|
cf00bb54c5 | ||
|
|
cdda88181f | ||
|
|
57a967978b |
+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,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var out string
|
||||||
|
flag.StringVar(&out, "o", "", "output `file`")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
r := os.Stdin
|
||||||
|
if flag.NArg() > 0 {
|
||||||
|
f, err := os.Open(flag.Arg(0))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
r = f
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
w := os.Stdout
|
||||||
|
if out != "" {
|
||||||
|
f, err := os.Create(out)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
w = f
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var race replay.Race
|
||||||
|
d := json.NewDecoder(r)
|
||||||
|
d.DisallowUnknownFields()
|
||||||
|
if err := d.Decode(&race); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "replay:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
simdata, err := replay.DecodeSim(race.SimDataBase64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "simdata:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(simdata)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
module git.sunturtle.xyz/zephyr/horse
|
module git.sunturtle.xyz/zephyr/horse
|
||||||
|
|
||||||
go 1.25.5
|
go 1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/disgoorg/disgo v0.19.6
|
github.com/disgoorg/disgo v0.19.6
|
||||||
github.com/gen2brain/avif v0.6.0
|
github.com/gen2brain/avif v0.6.0
|
||||||
github.com/go-chi/chi/v5 v5.3.1
|
github.com/go-chi/chi/v5 v5.5.0
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/junegunn/fzf v0.74.1
|
github.com/junegunn/fzf v0.74.5
|
||||||
golang.org/x/image v0.44.0
|
golang.org/x/image v0.47.0
|
||||||
golang.org/x/sync v0.22.0
|
golang.org/x/sync v0.24.0
|
||||||
zombiezen.com/go/sqlite v1.4.2
|
zombiezen.com/go/sqlite v1.4.2
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,23 +18,22 @@ require (
|
|||||||
github.com/disgoorg/json/v2 v2.0.0 // indirect
|
github.com/disgoorg/json/v2 v2.0.0 // indirect
|
||||||
github.com/disgoorg/omit v1.0.0 // indirect
|
github.com/disgoorg/omit v1.0.0 // indirect
|
||||||
github.com/disgoorg/snowflake/v2 v2.0.3 // indirect
|
github.com/disgoorg/snowflake/v2 v2.0.3 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.1.0 // indirect
|
||||||
github.com/ebitengine/purego v0.10.2 // indirect
|
github.com/ebitengine/purego v0.11.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
|
||||||
github.com/klauspost/compress v1.19.1 // indirect
|
github.com/klauspost/compress v1.20.3 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.1.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
|
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
|
||||||
github.com/tetratelabs/wazero v1.12.0 // indirect
|
github.com/tetratelabs/wazero v1.12.0 // indirect
|
||||||
golang.org/x/crypto v0.54.0 // indirect
|
golang.org/x/crypto v0.58.0 // indirect
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.49.0 // indirect
|
||||||
golang.org/x/tools v0.48.0 // indirect
|
modernc.org/libc v1.79.0 // indirect
|
||||||
modernc.org/libc v1.74.3 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.12.1 // indirect
|
||||||
modernc.org/sqlite v1.54.0 // indirect
|
modernc.org/sqlite v1.61.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78=
|
|||||||
github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ=
|
github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ=
|
||||||
github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro=
|
github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro=
|
||||||
github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c=
|
github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||||
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
github.com/ebitengine/purego v0.11.2 h1:WWiTx8rD8zYdOwSj8O/NiNbsOvSAgJMTlyV1XlbUSeU=
|
||||||
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.11.2/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||||
github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
|
github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
|
||||||
github.com/gen2brain/avif v0.6.0/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
|
github.com/gen2brain/avif v0.6.0/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
|
||||||
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
|
github.com/go-chi/chi/v5 v5.5.0 h1:xdKQ1dJmVJsmgahrXSKhUx8NyVPzpwdlyrrQFpMPJgg=
|
||||||
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
github.com/go-chi/chi/v5 v5.5.0/go.mod h1:d1IG7GtKPSjPyBP365PH8sAGVHk3qfB/IhjFNRH1wWM=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -26,16 +26,16 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
|||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/junegunn/fzf v0.74.1 h1:rCGVdCSoOkfcfTxtxCO/vG2yceDjRlKSlv7IfCGkc/Y=
|
github.com/junegunn/fzf v0.74.5 h1:mpJlCdgGuddKerHrdjvV3cnezEBgyjZAVn8KCo/4OMc=
|
||||||
github.com/junegunn/fzf v0.74.1/go.mod h1:hKDkO8orBWT5VLIZOL8oyhxn7mRLXdGyu+gz1Ss58pU=
|
github.com/junegunn/fzf v0.74.5/go.mod h1:TBRY54j3mGjAm4JcpDyiUIuDq38QlHvAzrB3GLKxQXM=
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
github.com/klauspost/compress v1.20.3 h1:zWNoFqq/lSuYQRim3dUXlXeA0IqwuZGicdG5ZTfY5j4=
|
||||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.20.3/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.1.0 h1:vtPmPWMkb539RGV9WpWT9EwJGvrOCKvMNI4obWL47Hk=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.1.0/go.mod h1:yVaq7iwCN09vHRrKBrrflXT0M9tTMG2KmZ9Fj7xeG/g=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
@@ -48,46 +48,46 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
|
||||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
|
||||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
||||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
|
||||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
modernc.org/cc/v4 v4.30.3 h1:aDq8gIX1X7+VFgO/JYYji6pLWL2H4tv3g8WatT3KCys=
|
||||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
modernc.org/cc/v4 v4.30.3/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
modernc.org/ccgo/v4 v4.37.0 h1:BAuJCUtIHIxFIYaFhqXIw5YaoMBlNCUlFovejHfMip4=
|
||||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
modernc.org/ccgo/v4 v4.37.0/go.mod h1:/8627VhwWtAbqUXmPORxFQU5SgLrAx2jl9seLw9GBhY=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
modernc.org/gc/v3 v3.2.0 h1:/Y3LGDMQaoYf55PebP1qsZ5FDGgAZyG1RYKEHeQzh1o=
|
||||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.2.0/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws=
|
modernc.org/libc v1.79.0 h1:9Dvvv++bAMSkSuBzuOFAatbTV5OIoq6Y6FohmD3e7q8=
|
||||||
modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4=
|
modernc.org/libc v1.79.0/go.mod h1:uT3kR61NsbeKJSLqq83GJwSnZMVLlSjyAz4DfTaBy1M=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
|
modernc.org/sqlite v1.61.0 h1:sQi63oLQ9uquAUA4CzOisH35Q3pEszkPBuHM0tFL8CY=
|
||||||
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
modernc.org/sqlite v1.61.0/go.mod h1:f4gr5tbpx2umklaT/QVtbi/50hNdM+4ng+2zMo8KXJ0=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ export const PAGES = {
|
|||||||
name: 'Tagged Skills Explorer',
|
name: 'Tagged Skills Explorer',
|
||||||
description: 'View skills matching the tags internal to the game.',
|
description: 'View skills matching the tags internal to the game.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: resolve('/doc/fans'),
|
||||||
|
name: 'Career Fan Gain Is Solved',
|
||||||
|
description: 'The exact formula for fan gain from career races – nothing "random" about it.'
|
||||||
|
},
|
||||||
] satisfies Page[],
|
] satisfies Page[],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { asset, resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import CalcInfo from '../../race/CalcInfo.svelte';
|
import CalcInfo from '../../race/CalcInfo.svelte';
|
||||||
import Article from '../Article.svelte';
|
import Article from '../Article.svelte';
|
||||||
import Sec from '../Sec.svelte';
|
import Sec from '../Sec.svelte';
|
||||||
import * as fans from './fans';
|
import * as fans from './fans';
|
||||||
|
|
||||||
let baseFans = $state(700);
|
let winFans = $state(700);
|
||||||
let fanBonus = $state(0);
|
let fanBonus = $state(0);
|
||||||
let finishOrder = $state(1);
|
let finishOrder = $state(1);
|
||||||
let margin = $state(0);
|
let margin = $state(0);
|
||||||
let skills = $state(0);
|
let skills = $state(0);
|
||||||
let glowsticks = $state(false);
|
let glowsticks = $state(false);
|
||||||
|
|
||||||
const marginFans = $derived(fans.marginFans(baseFans, finishOrder, margin / 1.18));
|
const baseFans = $derived(fans.baseFans(winFans, finishOrder));
|
||||||
|
const marginFans = $derived(fans.marginFans(baseFans, finishOrder, margin));
|
||||||
const skillFans = $derived(fans.skillFans(baseFans, skills));
|
const skillFans = $derived(fans.skillFans(baseFans, skills));
|
||||||
const glowstickBonus = $derived(glowsticks ? 50 : 0);
|
const glowstickBonus = $derived(glowsticks ? 50 : 0);
|
||||||
const gainedFans = $derived(fans.gained(baseFans, marginFans, skillFans, fanBonus, glowstickBonus));
|
const gainedFans = $derived(fans.gained(baseFans, marginFans, skillFans, fanBonus, glowstickBonus));
|
||||||
@@ -32,20 +33,20 @@
|
|||||||
[10, 5],
|
[10, 5],
|
||||||
[11, 4],
|
[11, 4],
|
||||||
[12, 3],
|
[12, 3],
|
||||||
[13, 2],
|
[13, 2.5],
|
||||||
[14, 2],
|
[14, 2],
|
||||||
[15, 1],
|
[15, 1.5],
|
||||||
[16, 1],
|
[16, 1],
|
||||||
[17, 1],
|
[17, 1],
|
||||||
[18, 1],
|
[18, 1],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const marginBonus = [
|
const marginBonus = [
|
||||||
['≤3/4 L', 0, 9],
|
['Nose – 3/4 L', 0, 9],
|
||||||
['3/4 L – 2.5 L', 2, 6],
|
['1 L – 2.5 L', 2, 6],
|
||||||
['2.5 L – 4 L', 4, 4],
|
['3 L – 4 L', 4, 4],
|
||||||
['4 L – 6 L', 6, 2],
|
['5 L – 6 L', 6, 2],
|
||||||
['>6L', 9, 0],
|
['7L – distance', 9, 0],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const skillBonus = [
|
const skillBonus = [
|
||||||
@@ -76,22 +77,21 @@
|
|||||||
<li>In Trackblazer/MANT, use of glowsticks.</li>
|
<li>In Trackblazer/MANT, use of glowsticks.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
The identified formula perfectly fits hundreds of recorded career race gains, including losses. Some questions remain, but
|
The identified formula perfectly fits hundreds of recorded career races, including losses. These results have important
|
||||||
the results so far have important implications both for fan runs and for low fan bonus decks for which debut can fall short
|
implications both for fan runs and for low fan bonus decks for which debut can fall short of fan requirements for early
|
||||||
of fan requirements for early career goals.
|
career goals.
|
||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<Sec h={2} id="calc-fans">Calculate Fans</Sec>
|
<Sec h={2} id="calc-fans">Calculate Fans</Sec>
|
||||||
<p>
|
<p>
|
||||||
This tool will calculate your exact fan gain from a race. Due to some <a href="#margin">internal details</a>, the margin
|
This tool will calculate your exact fan gain from a race. Use the fan value displayed in the race selection menu for the Race
|
||||||
displayed in game may differ by up to a length from the margin used to calculate fans. (This calculator takes displayed
|
Fans field.
|
||||||
margin, so its report can differ from actual gains in edge cases.)
|
|
||||||
</p>
|
</p>
|
||||||
<form class="mx-auto mb-4 grid grid-cols-1 gap-2 rounded-md p-4 text-center shadow-md ring md:max-w-2xl md:grid-cols-3">
|
<form class="mx-auto mb-4 grid grid-cols-1 gap-2 rounded-md p-4 text-center shadow-md ring md:max-w-2xl md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-base-fans">Base Fans</label>
|
<label for="calc-base-fans">Race Fans</label>
|
||||||
<input id="calc-base-fans" class="max-w-32" type="number" min="0" max="30000" step="100" bind:value={baseFans} />
|
<input id="calc-base-fans" class="max-w-32" type="number" min="0" max="30000" step="100" bind:value={winFans} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-fan-bonus">Fan Bonus</label>
|
<label for="calc-fan-bonus">Fan Bonus</label>
|
||||||
@@ -99,7 +99,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-finish-order">Finish Order</label>
|
<label for="calc-finish-order">Finish Order</label>
|
||||||
<input id="calc-finish-order" class="max-w-32" type="number" min="1" max="18" step="1" bind:value={finishOrder} />
|
<select id="calc-finish-order" class="max-w-32" bind:value={finishOrder}>
|
||||||
|
{#each orderPct as [v] (v)}
|
||||||
|
<option value={v}>{v}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="calc-margin">{marginType} (L)</label>
|
<label for="calc-margin">{marginType} (L)</label>
|
||||||
@@ -114,17 +118,18 @@
|
|||||||
<label for="calc-glowsticks">Glowsticks</label>
|
<label for="calc-glowsticks">Glowsticks</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-12">
|
<div class="mx-auto grid max-w-2xl grid-cols-1 md:grid-cols-3">
|
||||||
<CalcInfo title="Fans from Margin" class="md:col-span-4 md:col-start-3">{marginFans}</CalcInfo>
|
<CalcInfo title="Base Fans">{baseFans}</CalcInfo>
|
||||||
<CalcInfo title="Fans from Skills" class="md:col-span-4">{skillFans}</CalcInfo>
|
<CalcInfo title="Fans from Margin">{marginFans}</CalcInfo>
|
||||||
<CalcInfo title="Fans Gained" class="md:col-span-4 md:col-start-5">{gainedFans}</CalcInfo>
|
<CalcInfo title="Fans from Skills">{skillFans}</CalcInfo>
|
||||||
|
<CalcInfo title="Fans Gained" class="md:col-start-2">{gainedFans}</CalcInfo>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={2} id="base-fans">Base Fans</Sec>
|
<Sec h={2} id="base-fans">Base Fans</Sec>
|
||||||
<p>
|
<p>
|
||||||
Base fans is a parameter per race and finish placement, defined in <a href={resolve('/doc/mdb')}>master data</a>. The number
|
Base fans is a parameter per race and finish placement, defined in <a href={resolve('/doc/mdb')}>master data</a>. The number
|
||||||
shown in the career race selection menu is the base fans for first place. While it is defined separately for each race and
|
shown in the career race selection menu is the base fans for first place. While it is defined separately for each race and
|
||||||
finish order, every race currently follows the same curve for all placements.
|
finish order, every race currently follows the same curve for all placements, up to rounding.
|
||||||
</p>
|
</p>
|
||||||
<table class="mx-auto mb-4 border text-center">
|
<table class="mx-auto mb-4 border text-center">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -144,28 +149,10 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<Sec h={2} id="margin">Fans from Margin</Sec>
|
<Sec h={2} id="margin">Fans from Margin</Sec>
|
||||||
<p>
|
<p>When the trainee places in the top five, she receives a bonus percentage of base fans according to the finish margin.</p>
|
||||||
When the trainee places in the top three (top five?), she receives a bonus percentage of base fans according to the finish
|
|
||||||
margin.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
For the purpose of this calculation, "margin" is defined as the distance covered moving at 15 m/s since the time the first
|
|
||||||
horse crossed the goal line. I.e., we observe the exact margin used to calculate fans from race replay data. The <i
|
|
||||||
>displayed</i
|
|
||||||
>
|
|
||||||
margin in the game is
|
|
||||||
<a
|
|
||||||
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.p0458fiye6y9"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">scaled by a factor of 1.18</a
|
|
||||||
>
|
|
||||||
to derive the margin label (e.g. ½L instead of 0.7L). Because of this scaling and rounding, using the margin displayed in game
|
|
||||||
to calculate fans may underestimate the gain, particularly for displayed margins of 2½L, 5L, or 6L.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
The bonus depends on whether the trainee won the race. If so, it is calculated by her margin versus second place. Otherwise,
|
The bonus depends on whether the trainee won the race. If so, it is calculated by her margin versus second place. Otherwise,
|
||||||
her total margin behind the winner is used. If she did not place in the top three (top five?), then she gains no fans from
|
her total margin behind the winner is used. If she did not place in the top five, then she gains no fans from margin.
|
||||||
margin.
|
|
||||||
</p>
|
</p>
|
||||||
<table class="mx-auto mb-4 border text-center">
|
<table class="mx-auto mb-4 border text-center">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -208,32 +195,44 @@
|
|||||||
<Sec h={2} id="rounding">Calculation Order and Rounding</Sec>
|
<Sec h={2} id="rounding">Calculation Order and Rounding</Sec>
|
||||||
<p>The calculation is perfomed as if in the following order.</p>
|
<p>The calculation is perfomed as if in the following order.</p>
|
||||||
<ol class="mb-4 list-decimal pl-4">
|
<ol class="mb-4 list-decimal pl-4">
|
||||||
<li>Calculate fans from win margin and round down.</li>
|
<li>Look up base fans for the race and placement. This value is always an integer.</li>
|
||||||
<li>Calculate fans from skill activation count and round down.</li>
|
<li>Calculate fans from win margin as a proportion of base fans and round down.</li>
|
||||||
|
<li>Calculate fans from skill activation count as a proportion of base fans and round down.</li>
|
||||||
<li>Calculate the sum of base fans, win margin fans, and skill activation fans.</li>
|
<li>Calculate the sum of base fans, win margin fans, and skill activation fans.</li>
|
||||||
<li>Apply fan bonus to the sum and round to the nearest integer with halfway cases away from zero.</li>
|
<li>Apply fan bonus to the sum in single-precision floating point, without otherwise rounding.</li>
|
||||||
|
<li>
|
||||||
|
Round to five significant figures with halfway cases rounded up, then round down. Examples are useful here.
|
||||||
|
<ul class="list-[revert] pl-8">
|
||||||
|
<li>
|
||||||
|
700 base plus 14 from margin with 88% fan bonus = <span class="inline-block">714×1.88</span> = 1342.32, rounded to five
|
||||||
|
significant figures becomes 1342.3, rounded down to 1342.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
10500 base plus 210 from margin with 105% fan bonus =
|
||||||
|
<span class="inline-block">10710×2.05</span>
|
||||||
|
= 21955.5, rounded to five significant figures becomes 21956, rounded down to 21956.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
100 base fans plus 2 fans from skills with 88% fan bonus = <span class="inline-block">102×1.88</span> = 191.76, rounded
|
||||||
|
to five significant figures becomes 191.76, rounded down to 191.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>(Trackblazer/MANT) If glowsticks were used for the race, apply their multiplier and round down.</li>
|
<li>(Trackblazer/MANT) If glowsticks were used for the race, apply their multiplier and round down.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<Sec h={2} id="data">Data and Limitations</Sec>
|
<Sec h={2} id="data">Data</Sec>
|
||||||
<p>
|
<p>
|
||||||
The data for this research comes from 193 races and fan results dumped using <a
|
The data for this research comes from 325 races and fan results dumped using <a
|
||||||
href="https://werseter.github.io/umadump/"
|
href="https://werseter.github.io/umadump/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">Umadump</a
|
rel="noopener noreferrer">Umadump</a
|
||||||
>. The races come from sixteen careers run between 19 Aug and 11 Oct 2026. These careers include five different values of race
|
>. The races come from careers run between 19 Aug and 18 Oct 2026. These careers include fan bonus values of 50, 55, 65, 70,
|
||||||
bonus ranging from 55 to 105. The identified formula predicts the actual fan gain perfectly for all of the races. You can
|
80, 88, and 105. The identified formula predicts the actual fan gain perfectly for all of the races. You can
|
||||||
<a href={asset('/career_fan_gain_summary_193.csv')} download>download a summary</a>.
|
<a
|
||||||
</p>
|
href="https://docs.google.com/spreadsheets/d/1_jtOv6qk9dP4jJTagJqG53Pvj48IVd6gl2zKpyT6Kvs/edit?gid=463816108#gid=463816108"
|
||||||
<p>
|
target="_blank"
|
||||||
There are some gaps in the data. Moxa's original data that was used to establish that the margin bonus uses actual rather than
|
rel="noopener noreferrer">view the data</a
|
||||||
displayed margin was lost, so that conclusion is based on past discussion rather than available data. No collected race had a
|
>.
|
||||||
finish placement of 4th or 5th, nor lower than 6th, so the exact details of the win margin fan bonus remain open. Similarly,
|
|
||||||
no race featured a trainee who activated more than ten skills, so it is unknown whether there are further tiers of skill
|
|
||||||
activation count fan bonus.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Once these edge cases are covered, this article will be updated. If you'd like to contribute, please contact me
|
|
||||||
(@zephyrtronium) via Discord on the Hakuraku, uma.moe, GameTora, or EoG servers.
|
|
||||||
</p>
|
</p>
|
||||||
</Article>
|
</Article>
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
const placementPms = [1000, 400, 250, 150, 100, 90, 80, 70, 60, 50, 40, 30, 25, 20, 15, 10, 10, 10] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate base fans for a given placement in a race.
|
||||||
|
* @param winBase Base fans for a win, as displayed in the race menu
|
||||||
|
* @param finishOrder Placement of the trainee, with winner being 1
|
||||||
|
* @returns Base fans for the race and placement together
|
||||||
|
*/
|
||||||
|
export function baseFans(winBase: number, finishOrder: number): number {
|
||||||
|
return Math.round(winBase * placementPms[finishOrder - 1] / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
const margins = [
|
const margins = [
|
||||||
[0.75, 0.0, 0.09],
|
[0.75, 0.0, 0.09],
|
||||||
[2.5, 0.02, 0.06],
|
[2.5, 0.02, 0.06],
|
||||||
@@ -9,15 +21,15 @@ const margins = [
|
|||||||
/**
|
/**
|
||||||
* Calculate fans gained from a career race due to the margin of the finish.
|
* Calculate fans gained from a career race due to the margin of the finish.
|
||||||
* @param baseFans Base fans from the race and finish order
|
* @param baseFans Base fans from the race and finish order
|
||||||
* @param finishOrder Placement of the trainee
|
* @param finishOrder Placement of the trainee, with winner being 1
|
||||||
* @param margin Margin in lengths between the trainee and second if a win, or the winner and trainee if a loss
|
* @param margin Margin in lengths between the trainee and second if a win, or the winner and trainee if a loss
|
||||||
* @returns Fans gained due to finish margin
|
* @returns Fans gained due to finish margin
|
||||||
*/
|
*/
|
||||||
export function marginFans(baseFans: number, finishOrder: number, margin: number): number {
|
export function marginFans(baseFans: number, finishOrder: number, margin: number): number {
|
||||||
if (finishOrder > 3) {
|
if (finishOrder > 5) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const [, wb, lb] = margins.find(([m]) => margin < m)!;
|
const [, wb, lb] = margins.find(([m]) => margin <= m)!;
|
||||||
const b = finishOrder === 1 ? wb : lb;
|
const b = finishOrder === 1 ? wb : lb;
|
||||||
return Math.floor(baseFans * b);
|
return Math.floor(baseFans * b);
|
||||||
}
|
}
|
||||||
@@ -52,7 +64,10 @@ export function skillFans(baseFans: number, skillsActivated: number): number {
|
|||||||
*/
|
*/
|
||||||
export function gained(baseFans: number, marginFans: number, skillFans: number, fanBonus: number, itemBonus?: number): number {
|
export function gained(baseFans: number, marginFans: number, skillFans: number, fanBonus: number, itemBonus?: number): number {
|
||||||
const f = baseFans + marginFans + skillFans;
|
const f = baseFans + marginFans + skillFans;
|
||||||
const d = Math.round(f * (1 + fanBonus / 100));
|
const s = Math.fround(f * Math.fround(1 + fanBonus / 100));
|
||||||
|
const p = Math.exp((4 - Math.floor(Math.log10(s))) * Math.LN10);
|
||||||
|
// Add half a significant figure to account for precision.
|
||||||
|
const d = Math.floor(Math.round(s * p + 5e-6) / p + 5e-6);
|
||||||
const b = itemBonus ?? 0;
|
const b = itemBonus ?? 0;
|
||||||
return Math.floor(d * (1 + b / 100));
|
return Math.floor(d * (1 + b / 100));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user