Compare commits

...

77 Commits

Author SHA1 Message Date
zephyr 48118efeeb doc: document grand live mechanics
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-28 00:19:14 -04:00
zephyr 79d4e76090 mdb: sort scenarios by sort id
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #33.
2026-07-25 11:57:24 -04:00
zephyr ea39be0510 zenno: include character icons in character picks
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-24 23:09:24 -04:00
zephyr 7bac55ca43 zenno: show icons on skills 2026-07-24 22:56:31 -04:00
zephyr 435da61405 cmd: move toradl into horsebot
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #31.
2026-07-24 17:18:44 -04:00
zephyr f3e7c7b43b all: update deps
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-23 22:55:17 -04:00
zephyr eded415df4 cmd/toradl: program for downloading icons from gametora 2026-07-23 22:48:12 -04:00
zephyr 34029fe001 zenno/doc/tagged-skills: more skill info
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-21 17:01:23 -04:00
zephyr ffb700545f zenno/doc/tagged-skills: new page
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-19 22:29:15 -04:00
zephyr 70ad09a13e zenno/chara/convo: list characters in convos even when they don't have their own
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #24.
2026-07-19 15:46:41 -04:00
zephyr 74c22c7313 zenno: rename Great Escape -> Runaway everywhere
Fixes #26.
2026-07-19 15:38:01 -04:00
zephyr 2c1b4b9261 zenno/doc/race: lint fix
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-18 16:58:20 -04:00
zephyr e5128b6783 zenno/race/spurt: add dueling info 2026-07-18 16:58:12 -04:00
zephyr b22c8307fd zenno/race: common component for calculator outputs 2026-07-18 16:38:07 -04:00
zephyr cb54bbe414 zenno/doc/frbm: add doccy references to some sections
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-07-13 18:59:52 -04:00
zephyr 096cff28b6 zenno/doc/frbm: change of cm16 plans 2026-07-13 18:45:51 -04:00
zephyr 197df8f022 zenno/doc/race: add reference links to doccy 2026-07-13 18:41:18 -04:00
zephyr 009dd1d695 zenno: add page titles and descriptions
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-12 14:37:43 -04:00
zephyr bdafc93315 zenno: lint fix
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-10 20:01:20 -04:00
zephyr ef5b06d66e zenno: pull vite config from stam calc branch
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
ci/woodpecker/manual/horsebot Pipeline was successful
ci/woodpecker/manual/zenno Pipeline failed
2026-07-10 17:34:02 -04:00
zephyr 65b81612bc ci: allow running manually
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-07-10 17:29:35 -04:00
zephyr 6269665e79 ci: always run zenno workflow 2026-07-10 16:20:02 -04:00
zephyr cae7d5b8f4 zenno: format 2026-07-10 15:37:29 -04:00
zephyr fca43e64b5 zenno/doc/spark: make spark select searchable 2026-07-09 15:40:07 -04:00
zephyr bc395a0788 zenno: refactor CharaPick to generic component 2026-07-09 14:54:03 -04:00
zephyr 1d879cbf9f zenno/race/gate: add velocity/time chart 2026-07-07 20:47:20 -04:00
zephyr 240a3d9a73 zenno/doc/spark: decribe & clarify 2026-07-07 16:23:15 -04:00
zephyr 3d73460934 zenno: link up doc/spark 2026-07-07 15:55:31 -04:00
zephyr 0c518a9ce2 zenno/doc/spark: spark explorer 2026-07-07 15:51:28 -04:00
zephyr 5cbb6d7b1d zenno/doc/frbm: updates for 7-1 patch 2026-07-07 11:16:09 -04:00
zephyr b0bd16fd04 zenno/index: move discord bot to its own section 2026-07-06 09:45:07 -04:00
zephyr 55175c9c40 zenno: format & lint 2026-07-06 09:41:11 -04:00
zephyr 06e5a06680 zenno/race/gate: caveats 2026-07-06 09:39:36 -04:00
zephyr dbb0dcd2dc zenno: link up race/gate 2026-07-06 09:23:37 -04:00
zephyr 02f723b83a zenno/race/gate: new calculator 2026-07-06 09:01:11 -04:00
zephyr 1f58dec491 zenno/doc/race: conserve power 2026-07-01 23:39:31 -04:00
zephyr a6d264c79d zenno: pull race mechanics lib update from stam-calc 2026-07-01 22:40:11 -04:00
zephyr d62e729052 zenno: update stat charts for new limits 2026-07-01 22:12:10 -04:00
zephyr 9ce665865e zenno/doc/race: fix 4 skill activation chance chart 2026-06-26 13:13:30 -04:00
zephyr 7d395b2cc7 ci: set up
ci/woodpecker/push/horsebot Pipeline was successful
2026-06-17 10:50:04 -04:00
zephyr 2b69211a6c horse: update deps 2026-06-17 09:58:02 -04:00
zephyr 67377c4690 zenno: format 2026-06-16 14:20:03 -04:00
zephyr 1d65a0ec14 zenno/doc/race: add missing chart controls
Fixes #21.
2026-06-16 13:40:16 -04:00
zephyr ad58edbfe1 zenno: make character selects searchable
Fixes #20.
2026-06-16 13:37:59 -04:00
zephyr f8e7bfbec6 zenno/doc/race: skill activation chance is not integer percent 2026-06-14 20:00:27 -04:00
zephyr 2e618b613f zenno/lib/race: constrain poskeep chances to hundredths 2026-06-14 19:05:52 -04:00
zephyr cc2b5fcfcb zenno: make stat chart tooltips attach vertically 2026-06-14 18:59:33 -04:00
zephyr 8ef3936be5 zenno/doc/race: fix mobile 2026-06-14 18:57:43 -04:00
zephyr b06e4b537b zenno: link up doc/race
Fixes #16.
2026-06-14 17:55:21 -04:00
zephyr 7b4cd41d3b zenno/doc/frbm: remove stat summaries
For #16.
2026-06-14 17:45:29 -04:00
zephyr 2c9434729a zenno/doc/race: interactive graphs for race mechanics
For #16.
2026-06-14 17:36:00 -04:00
zephyr 7f02925ff7 horse: define rushed chance mod and current speed up ability types
Fixes #17.
2026-06-14 12:04:07 -04:00
zephyr b217691c8a zenno/doc/lanecalc: lint fixes 2026-06-14 10:45:39 -04:00
zephyr 4bce196c09 zenno/doc/frbm: effects on other styles in cm comps
Fixes #18.
2026-06-14 10:34:34 -04:00
zephyr aa17d8ccf3 zenno: beg
Fixes #15.
2026-06-13 22:59:49 -04:00
zephyr 26a5ca4496 zenno/doc/lanecalc: typo 2026-06-13 22:12:59 -04:00
zephyr 51f942ee18 zenno: mention doc/lanecalc as appropriate
Fixes #14.
2026-06-13 22:09:09 -04:00
zephyr 2c773391f0 zenno: link up doc/lanecalc 2026-06-13 22:04:49 -04:00
zephyr c66d969c82 zenno/doc/lanecalc: english manual for lanecalc
For #14.
2026-06-13 22:02:16 -04:00
zephyr bc9d722ee0 zenno/doc/frbm: cm14 results and cm15 plans 2026-06-13 13:20:54 -04:00
zephyr 6482f11620 zenno/doc/frbm: rename chariot -> rabbit
jern on the Hakuraku discord informs me that chariot is a different
style of build.

Fixes #19.
2026-06-13 12:35:07 -04:00
zephyr 75024c7c11 zenno: categorize tools 2026-06-10 14:03:42 -04:00
zephyr dc78d51def zenno: link up affinity tool 2026-06-08 16:45:12 -04:00
zephyr 5f0ad1b0e0 zenno/affinity: new tool 2026-06-08 16:42:04 -04:00
zephyr 6db0ca5230 mdb, cmd/horsebot: affinity details 2026-06-07 15:58:29 -04:00
zephyr 049617c22e zenno/doc/frbm: cm14 round 1 win rates 2026-06-07 15:04:29 -04:00
zephyr d431d3382a global, cmd/horsegen: don't generate json anymore
Everything gets the data from the mdb directly instead now.
2026-06-05 15:13:36 -04:00
zephyr a848c8eb72 zenno: use data from api 2026-06-05 15:13:11 -04:00
zephyr 871fd5fdcc cmd/horsebot: allow acting only as api server 2026-06-05 15:13:11 -04:00
zephyr e3dd37f515 cmd/horsebot: serve data under /api/global 2026-06-05 15:13:11 -04:00
zephyr 3557b32298 cmd/horsebot: serve data from mdb instead of json 2026-06-05 15:13:11 -04:00
zephyr 20db674d25 cmd/horsegen: generate using package mdb 2026-06-05 15:13:11 -04:00
zephyr 2a4da0a73e mdb: don't list carnival bonus sparks to exclude them 2026-06-05 15:13:11 -04:00
zephyr e6335c1d6e global: generate with deleted lobby location names 2026-06-05 15:13:11 -04:00
zephyr f40fea0ec5 mdb: package for interacting with game's local database 2026-06-05 15:13:10 -04:00
zephyr 96503e40f6 doc: add query to get skill condition types 2026-06-05 15:13:10 -04:00
zephyr c95e9bb4cc global: generate with 2026-06-04 db 2026-06-04 14:53:28 -04:00
109 changed files with 8117 additions and 259431 deletions
+12
View File
@@ -0,0 +1,12 @@
when:
- event: push
branch: main
- event: pull_request
- event: manual
steps:
- name: test
image: golang:1.26-alpine
commands:
- go vet ./...
- go test ./...
+15
View File
@@ -0,0 +1,15 @@
when:
- event: push
branch: main
- event: pull_request
- event: manual
steps:
- name: test
image: node:24-alpine
commands:
- cd zenno
- npm ci
- npm run check
- npm run lint
- npm run test
+10 -6
View File
@@ -20,8 +20,10 @@ func _() {
_ = x[AbilityFrenzy-13] _ = x[AbilityFrenzy-13]
_ = x[AbilityAddGateDelay-14] _ = x[AbilityAddGateDelay-14]
_ = x[AbilityCurrentSpeed-21] _ = x[AbilityCurrentSpeed-21]
_ = x[AbilityCurrentSpeedUp-22]
_ = x[AbilityTargetSpeed-27] _ = x[AbilityTargetSpeed-27]
_ = x[AbilityLaneSpeed-28] _ = x[AbilityLaneSpeed-28]
_ = x[AbilityRushedChance-29]
_ = x[AbilityAccel-31] _ = x[AbilityAccel-31]
_ = x[AbilityLaneChange-35] _ = x[AbilityLaneChange-35]
} }
@@ -30,8 +32,8 @@ const (
_AbilityType_name_0 = "SpeedStaminaPowerGutsWitEnable Great Escape" _AbilityType_name_0 = "SpeedStaminaPowerGutsWitEnable Great Escape"
_AbilityType_name_1 = "VisionHPGate delay multiplier" _AbilityType_name_1 = "VisionHPGate delay multiplier"
_AbilityType_name_2 = "FrenzyAdded gate delay" _AbilityType_name_2 = "FrenzyAdded gate delay"
_AbilityType_name_3 = "Current speed" _AbilityType_name_3 = "Current speedCurrent speed"
_AbilityType_name_4 = "Target speedLane change speed" _AbilityType_name_4 = "Target speedLane change speedRushed chance"
_AbilityType_name_5 = "Acceleration" _AbilityType_name_5 = "Acceleration"
_AbilityType_name_6 = "Forced lane change" _AbilityType_name_6 = "Forced lane change"
) )
@@ -40,7 +42,8 @@ var (
_AbilityType_index_0 = [...]uint8{0, 5, 12, 17, 21, 24, 43} _AbilityType_index_0 = [...]uint8{0, 5, 12, 17, 21, 24, 43}
_AbilityType_index_1 = [...]uint8{0, 6, 8, 29} _AbilityType_index_1 = [...]uint8{0, 6, 8, 29}
_AbilityType_index_2 = [...]uint8{0, 6, 22} _AbilityType_index_2 = [...]uint8{0, 6, 22}
_AbilityType_index_4 = [...]uint8{0, 12, 29} _AbilityType_index_3 = [...]uint8{0, 13, 26}
_AbilityType_index_4 = [...]uint8{0, 12, 29, 42}
) )
func (i AbilityType) String() string { func (i AbilityType) String() string {
@@ -54,9 +57,10 @@ func (i AbilityType) String() string {
case 13 <= i && i <= 14: case 13 <= i && i <= 14:
i -= 13 i -= 13
return _AbilityType_name_2[_AbilityType_index_2[i]:_AbilityType_index_2[i+1]] return _AbilityType_name_2[_AbilityType_index_2[i]:_AbilityType_index_2[i+1]]
case i == 21: case 21 <= i && i <= 22:
return _AbilityType_name_3 i -= 21
case 27 <= i && i <= 28: return _AbilityType_name_3[_AbilityType_index_3[i]:_AbilityType_index_3[i+1]]
case 27 <= i && i <= 29:
i -= 27 i -= 27
return _AbilityType_name_4[_AbilityType_index_4[i]:_AbilityType_index_4[i+1]] return _AbilityType_name_4[_AbilityType_index_4[i]:_AbilityType_index_4[i+1]]
case i == 31: case i == 31:
+11 -5
View File
@@ -11,13 +11,21 @@ func (c Character) String() string {
return c.Name return c.Name
} }
// AffinityRelation holds precomputed affinity values for a pair or trio.
type AffinityRelation struct { type AffinityRelation struct {
IDA int `json:"chara_a"` IDA CharacterID `json:"chara_a"`
IDB int `json:"chara_b"` IDB CharacterID `json:"chara_b"`
IDC int `json:"chara_c,omitzero"` IDC CharacterID `json:"chara_c,omitzero"`
Affinity int `json:"affinity"` Affinity int `json:"affinity"`
} }
// AffinityDetail holds a single affinity entry.
type AffinityDetail struct {
CharaID CharacterID `json:"chara_id"`
Relation uint16 `json:"relation"`
Affinity uint16 `json:"affinity"`
}
// Conversation describes a lobby conversation. // Conversation describes a lobby conversation.
type Conversation struct { type Conversation struct {
// CharacterID is the ID of the character who has the conversation as // CharacterID is the ID of the character who has the conversation as
@@ -27,8 +35,6 @@ type Conversation struct {
Number int `json:"number"` Number int `json:"number"`
// Location is the ID of the location the conversation occurs in the lobby. // Location is the ID of the location the conversation occurs in the lobby.
Location LobbyConversationLocationID `json:"location"` Location LobbyConversationLocationID `json:"location"`
// LocationName is the name of the lobby location, for convenience.
LocationName string `json:"location_name"`
// Chara1 is the first conversation participant ID. // Chara1 is the first conversation participant ID.
// It does not necessarily match CharacterID. // It does not necessarily match CharacterID.
Chara1 CharacterID `json:"chara_1"` Chara1 CharacterID `json:"chara_1"`
+12 -2
View File
@@ -1,10 +1,20 @@
# horsebot # horsebot
Discord bot serving horse game data. Website and Discord bot serving horse game data.
Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations. Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations.
## Running The `-mdb` flag is mandatory for both website and Discord bot modes.
It provides data for both the horsebot API and the discord bot.
## Running the Website
If the `-public` flag is provided, its value gives the directory to serve at `/`.
The `-img` flag gives a separate directory into which images are synced from GameTora; obtain permission from Gertas prior to using it.
The images are served on `/img/`.
## Running the Discord Bot
Provide the `-token` flag pointing to a file containing the token.
The bot always uses the Gateway API. The bot always uses the Gateway API.
If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided. If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided.
+78 -36
View File
@@ -13,7 +13,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "strconv"
"time" "time"
"github.com/disgoorg/disgo" "github.com/disgoorg/disgo"
@@ -24,16 +24,19 @@ import (
"github.com/disgoorg/disgo/httpserver" "github.com/disgoorg/disgo/httpserver"
"github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/rest"
httpmiddle "github.com/go-chi/chi/v5/middleware" httpmiddle "github.com/go-chi/chi/v5/middleware"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse" "git.sunturtle.xyz/zephyr/horse/mdb"
) )
func main() { func main() {
var ( var (
// public site // public site
addr string addr string
dataDir string mdbf string
public string public string
img string
// discord // discord
tokenFile string tokenFile string
apiRoute string apiRoute string
@@ -42,9 +45,10 @@ func main() {
level slog.Level level slog.Level
textfmt string textfmt string
) )
flag.StringVar(&addr, "http", ":80", "`address` to bind HTTP server") flag.StringVar(&addr, "http", ":13669", "`address` to bind HTTP server")
flag.StringVar(&dataDir, "data", "", "`dir`ectory containing exported json data") flag.StringVar(&mdbf, "mdb", "", "`path` to master.mdb")
flag.StringVar(&public, "public", "", "`dir`ectory containing the website to serve") flag.StringVar(&public, "public", "", "`dir`ectory containing the website to serve")
flag.StringVar(&img, "img", "", "`dir`ectory to save and serve images")
flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token") flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token")
flag.StringVar(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls") flag.StringVar(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls")
flag.StringVar(&pubkey, "key", "", "Discord public key") flag.StringVar(&pubkey, "key", "", "Discord public key")
@@ -64,6 +68,7 @@ func main() {
} }
slog.SetDefault(slog.New(lh)) slog.SetDefault(slog.New(lh))
if public != "" {
stat, err := os.Stat(public) stat, err := os.Stat(public)
if err != nil { if err != nil {
slog.Error("public", slog.Any("err", err)) slog.Error("public", slog.Any("err", err))
@@ -73,10 +78,32 @@ func main() {
slog.Error("public", slog.String("err", "not a directory")) slog.Error("public", slog.String("err", "not a directory"))
os.Exit(1) os.Exit(1)
} }
}
skills, err := loadSkills(filepath.Join(dataDir, "skill.json")) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
db, err := sqlitex.NewPool(mdbf, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil {
slog.Error("mdb", slog.Any("err", err))
os.Exit(1)
}
if img != "" {
// Ensure the img directory is in fact a directory
// before we start trying to fill it out.
err := os.MkdirAll(img, 0777)
if err != nil {
slog.Error("img", slog.Any("err", err))
os.Exit(1)
}
go saveImgs(ctx, db, img)
}
var client *bot.Client
if tokenFile != "" {
skills, err := mdb.Skills(ctx, db)
slog.Info("loaded skills", slog.Int("count", len(skills))) slog.Info("loaded skills", slog.Int("count", len(skills)))
groups, err2 := loadSkillGroups(filepath.Join(dataDir, "skill-group.json")) groups, err2 := mdb.SkillGroups(ctx, db)
slog.Info("loaded skill groups", slog.Int("count", len(groups))) slog.Info("loaded skill groups", slog.Int("count", len(groups)))
if err = errors.Join(err, err2); err != nil { if err = errors.Join(err, err2); err != nil {
slog.Error("loading data", slog.Any("err", err)) slog.Error("loading data", slog.Any("err", err))
@@ -85,15 +112,6 @@ func main() {
skillSrv := newSkillServer(skills, groups) skillSrv := newSkillServer(skills, groups)
slog.Info("skill server ready") slog.Info("skill server ready")
token, err := os.ReadFile(tokenFile)
if err != nil {
slog.Error("reading token", slog.Any("err", err))
os.Exit(1)
}
token = bytes.TrimSuffix(token, []byte{'\n'})
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
r := handler.New() r := handler.New()
r.DefaultContext(func() context.Context { return ctx }) r.DefaultContext(func() context.Context { return ctx })
r.Use(middleware.Go) r.Use(middleware.Go)
@@ -104,16 +122,39 @@ func main() {
r.SelectMenuComponent("/swap", skillSrv.menu) r.SelectMenuComponent("/swap", skillSrv.menu)
r.ButtonComponent("/swap/{id}", skillSrv.button) r.ButtonComponent("/swap/{id}", skillSrv.button)
}) })
slog.Info("connect", slog.String("disgo", disgo.Version)) slog.Info("connect", slog.String("disgo", disgo.Version))
client, err := disgo.New(string(token), bot.WithDefaultGateway(), bot.WithEventListeners(r)) token, err := os.ReadFile(tokenFile)
if err != nil {
slog.Error("reading token", slog.Any("err", err))
os.Exit(1)
}
token = bytes.TrimSuffix(token, []byte{'\n'})
client, err = disgo.New(string(token), bot.WithDefaultGateway(), bot.WithEventListeners(r))
if err != nil { if err != nil {
slog.Error("building bot", slog.Any("err", err)) slog.Error("building bot", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
}
mux := http.NewServeMux() mux := http.NewServeMux()
if public != "" {
mux.Handle("GET /", httpmiddle.Compress(5)(http.FileServerFS(os.DirFS(public)))) mux.Handle("GET /", httpmiddle.Compress(5)(http.FileServerFS(os.DirFS(public))))
mux.Handle("GET /api/data/", httpmiddle.Compress(5)(http.StripPrefix("/api/data", http.FileServerFS(os.DirFS(dataDir))))) }
if img != "" {
mux.Handle("GET /img/", http.StripPrefix("/img", http.FileServerFS(os.DirFS(img))))
}
mux.Handle("GET /api/global/affinity", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.AffinitySummary)))
mux.Handle("GET /api/global/affinity-detail", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.AffinityDetail)))
mux.Handle("GET /api/global/character", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Characters)))
mux.Handle("GET /api/global/conversation", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Conversations)))
mux.Handle("GET /api/global/race", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Races)))
mux.Handle("GET /api/global/saddle", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Saddles)))
mux.Handle("GET /api/global/scenario", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Scenarios)))
mux.Handle("GET /api/global/skill", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Skills)))
mux.Handle("GET /api/global/skill-group", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.SkillGroups)))
mux.Handle("GET /api/global/spark", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Sparks)))
mux.Handle("GET /api/global/uma", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Umas)))
if pubkey != "" { if pubkey != "" {
pk, err := hex.DecodeString(pubkey) pk, err := hex.DecodeString(pubkey)
if err != nil { if err != nil {
@@ -130,7 +171,7 @@ func main() {
} }
srv := http.Server{ srv := http.Server{
Addr: addr, Addr: addr,
Handler: mux, Handler: httpmiddle.Logger(mux),
ReadTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second,
BaseContext: func(l net.Listener) context.Context { return ctx }, BaseContext: func(l net.Listener) context.Context { return ctx },
@@ -144,6 +185,7 @@ func main() {
slog.Error("HTTP server closed", slog.Any("err", err)) slog.Error("HTTP server closed", slog.Any("err", err))
}() }()
if client != nil {
if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil { if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil {
slog.Error("syncing commands", slog.Any("err", err)) slog.Error("syncing commands", slog.Any("err", err))
os.Exit(1) os.Exit(1)
@@ -154,13 +196,17 @@ func main() {
slog.Error("starting gateway", slog.Any("err", err)) slog.Error("starting gateway", slog.Any("err", err))
stop() stop()
} }
}
slog.Info("ready") slog.Info("ready")
<-ctx.Done() <-ctx.Done()
stop() stop()
ctx, stop = context.WithTimeout(context.Background(), 5*time.Second) ctx, stop = context.WithTimeout(context.Background(), 5*time.Second)
defer stop() defer stop()
if client != nil {
client.Close(ctx) client.Close(ctx)
}
if err := srv.Shutdown(ctx); err != nil { if err := srv.Shutdown(ctx); err != nil {
slog.Error("HTTP API shutdown", slog.Any("err", err)) slog.Error("HTTP API shutdown", slog.Any("err", err))
os.Exit(1) os.Exit(1)
@@ -186,26 +232,22 @@ var commands = []discord.ApplicationCommandCreate{
}, },
} }
func loadSkills(file string) ([]horse.Skill, error) { func dataroute[T any](ctx context.Context, db *sqlitex.Pool, f func(context.Context, *sqlitex.Pool) ([]T, error)) http.HandlerFunc {
b, err := os.ReadFile(file) return func(w http.ResponseWriter, r *http.Request) {
data, err := f(ctx, db)
if err != nil { if err != nil {
return nil, err http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
var skills []horse.Skill b, err := json.Marshal(data)
if err := json.Unmarshal(b, &skills); err != nil {
return nil, err
}
return skills, nil
}
func loadSkillGroups(file string) ([]horse.SkillGroup, error) {
b, err := os.ReadFile(file)
if err != nil { if err != nil {
return nil, err panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
if _, err := w.Write(b); err != nil {
slog.Error("writing response", slog.Any("err", err))
return
} }
var groups []horse.SkillGroup
if err := json.Unmarshal(b, &groups); err != nil {
return nil, err
} }
return groups, nil
} }
+173
View File
@@ -0,0 +1,173 @@
package main
import (
"context"
"errors"
"fmt"
"image"
"image/png"
"log/slog"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gen2brain/avif"
"golang.org/x/image/draw"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
func saveImgs(ctx context.Context, db *sqlitex.Pool, out string) {
skill, err := skillIconIDs(ctx, db)
if err != nil {
slog.Error("skills", slog.Any("err", err))
}
chara, err := charaIDs(ctx, db)
if err != nil {
slog.Error("characters", slog.Any("err", err))
}
rank, err := rankIDs(ctx, db)
if err != nil {
slog.Error("ranks", slog.Any("err", err))
}
g, ctx := errgroup.WithContext(ctx)
tick := time.NewTicker(250 * time.Millisecond)
idouts(ctx, g, tick, skill, filepath.Join(out, "skill"), "https://media.gametora.com/umamusume/skills/icon/%d.png", image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16))
idouts(ctx, g, tick, chara, filepath.Join(out, "chara"), "https://gametora.com/images/umamusume/characters/icons/chr_icon_%d.png", image.Pt(256, 256), image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16))
idouts(ctx, g, tick, rank, filepath.Join(out, "rank"), "https://media.gametora.com/umamusume/ui/rank/%02d.png", image.Pt(128, 128), image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16))
idouts(ctx, g, tick, rank, filepath.Join(out, "rank", "simple"), "https://media.gametora.com/umamusume/ui/rank/simple/%02d.png", image.Pt(50, 50), image.Pt(32, 32), image.Pt(16, 16))
if err := g.Wait(); err != nil {
slog.Error("output", slog.Any("err", err))
}
}
// load scans all results of sql and appends them to r.
func load[T any](ctx context.Context, db *sqlitex.Pool, r []T, sql string, row func(*sqlite.Stmt) T) ([]T, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, err
}
stmt, err := conn.Prepare(sql)
if err != nil {
return nil, err
}
for {
ok, err := stmt.Step()
if err != nil {
return r, err
}
if !ok {
break
}
r = append(r, row(stmt))
}
return r, err
}
func skillIconIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) {
return load(ctx, db, nil, "SELECT DISTINCT icon_id FROM skill_data", func(s *sqlite.Stmt) int32 {
return s.ColumnInt32(0)
})
}
func charaIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) {
return load(ctx, db, nil, "SELECT id FROM chara_data", func(s *sqlite.Stmt) int32 {
return s.ColumnInt32(0)
})
}
func rankIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) {
return load(ctx, db, nil, "SELECT id-1 FROM single_mode_rank", func(s *sqlite.Stmt) int32 {
return s.ColumnInt32(0)
})
}
var pngenc = &png.Encoder{CompressionLevel: png.BestCompression}
func idouts(ctx context.Context, g *errgroup.Group, tick *time.Ticker, ids []int32, p, urlf string, sizes ...image.Point) {
for _, id := range ids {
g.Go(func() error {
n := strconv.Itoa(int(id))
p := filepath.Join(p, n)
_, err := os.Stat(p)
if !errors.Is(err, os.ErrNotExist) {
slog.DebugContext(ctx, "exists", slog.String("path", p))
return nil
}
if err := os.MkdirAll(p, 0777); err != nil {
return err
}
url := fmt.Sprintf(urlf, id)
select {
case <-ctx.Done():
return ctx.Err()
case <-tick.C: // continue on
}
img, err := dlimg(ctx, url)
if err != nil {
return err
}
slog.InfoContext(ctx, "resize", slog.String("url", url))
rs := resize(img, sizes...)
for _, r := range rs {
g.Go(func() error {
m := r.Bounds().Max
o := filepath.Join(p, fmt.Sprintf("%dx%d.png", m.X, m.Y))
slog.InfoContext(ctx, "write", slog.String("path", o))
f, err := os.Create(o)
if err != nil {
return err
}
defer f.Close()
return pngenc.Encode(f, r)
})
g.Go(func() error {
m := r.Bounds().Max
o := filepath.Join(p, fmt.Sprintf("%dx%d.avif", m.X, m.Y))
slog.InfoContext(ctx, "write", slog.String("path", o))
f, err := os.Create(o)
if err != nil {
return err
}
defer f.Close()
return avif.Encode(f, r)
})
}
return nil
})
}
}
func dlimg(ctx context.Context, url string) (image.Image, error) {
slog.InfoContext(ctx, "download", slog.String("url", url))
ctx, stop := context.WithTimeout(ctx, 5*time.Second)
defer stop()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
slog.InfoContext(ctx, "decode", slog.String("url", url))
img, _, err := image.Decode(resp.Body)
return img, err
}
func resize(img image.Image, sizes ...image.Point) []image.Image {
rs := make([]image.Image, 0, len(sizes))
for _, sz := range sizes {
d := image.NewNRGBA(image.Rectangle{Min: image.Point{}, Max: sz})
draw.CatmullRom.Scale(d, d.Bounds(), img, img.Bounds(), draw.Src, nil)
rs = append(rs, d)
}
return rs
}
-435
View File
@@ -1,435 +0,0 @@
package main
import (
"bufio"
"cmp"
"context"
_ "embed"
"encoding/json"
"errors"
"flag"
"fmt"
"log/slog"
"maps"
"os"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
)
func main() {
var (
mdb string
out string
region string
)
flag.StringVar(&mdb, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb")
flag.StringVar(&out, "o", `.`, "`dir`ectory for output files")
flag.StringVar(&region, "region", "global", "region the database is for (global, jp)")
flag.Parse()
slog.Info("open", slog.String("mdb", mdb))
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil {
slog.Error("opening mdb", slog.String("mdb", mdb), slog.Any("err", err))
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
loadgroup, ctx1 := errgroup.WithContext(ctx)
charas := load(ctx1, loadgroup, db, "characters", characterSQL, func(s *sqlite.Stmt) horse.Character {
return horse.Character{
ID: horse.CharacterID(s.ColumnInt(0)),
Name: s.ColumnText(1),
}
})
aff := load(ctx1, loadgroup, db, "pair affinity", affinitySQL, func(s *sqlite.Stmt) horse.AffinityRelation {
return horse.AffinityRelation{
IDA: s.ColumnInt(0),
IDB: s.ColumnInt(1),
IDC: s.ColumnInt(2),
Affinity: s.ColumnInt(3),
}
})
umas := load(ctx1, loadgroup, db, "umas", umaSQL, func(s *sqlite.Stmt) horse.Uma {
return horse.Uma{
ID: horse.UmaID(s.ColumnInt(0)),
CharacterID: horse.CharacterID(s.ColumnInt(1)),
Name: s.ColumnText(2),
Variant: s.ColumnText(3),
Sprint: horse.AptitudeLevel(s.ColumnInt(4)),
Mile: horse.AptitudeLevel(s.ColumnInt(6)),
Medium: horse.AptitudeLevel(s.ColumnInt(7)),
Long: horse.AptitudeLevel(s.ColumnInt(8)),
Front: horse.AptitudeLevel(s.ColumnInt(9)),
Pace: horse.AptitudeLevel(s.ColumnInt(10)),
Late: horse.AptitudeLevel(s.ColumnInt(11)),
End: horse.AptitudeLevel(s.ColumnInt(12)),
Turf: horse.AptitudeLevel(s.ColumnInt(13)),
Dirt: horse.AptitudeLevel(s.ColumnInt(14)),
Unique: horse.SkillID(s.ColumnInt(15)),
Skill1: horse.SkillID(s.ColumnInt(16)),
Skill2: horse.SkillID(s.ColumnInt(17)),
Skill3: horse.SkillID(s.ColumnInt(18)),
SkillPL2: horse.SkillID(s.ColumnInt(19)),
SkillPL3: horse.SkillID(s.ColumnInt(20)),
SkillPL4: horse.SkillID(s.ColumnInt(21)),
SkillPL5: horse.SkillID(s.ColumnInt(22)),
}
})
sg := load(ctx1, loadgroup, db, "skill groups", skillGroupSQL, func(s *sqlite.Stmt) horse.SkillGroup {
return horse.SkillGroup{
ID: horse.SkillGroupID(s.ColumnInt(0)),
Skill1: horse.SkillID(s.ColumnInt(1)),
Skill2: horse.SkillID(s.ColumnInt(2)),
Skill3: horse.SkillID(s.ColumnInt(3)),
SkillBad: horse.SkillID(s.ColumnInt(4)),
}
})
skills := load(ctx1, loadgroup, db, "skills", skillSQL, func(s *sqlite.Stmt) horse.Skill {
return horse.Skill{
ID: horse.SkillID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SkillGroupID(s.ColumnInt32(3)),
Rarity: int8(s.ColumnInt(5)),
GroupRate: int8(s.ColumnInt(6)),
GradeValue: s.ColumnInt32(7),
WitCheck: s.ColumnBool(8),
Activations: trimActivations([]horse.Activation{
{
Precondition: s.ColumnText(9),
Condition: s.ColumnText(10),
Duration: horse.TenThousandths(s.ColumnInt(11)),
DurScale: horse.DurScale(s.ColumnInt(12)),
Cooldown: horse.TenThousandths(s.ColumnInt(13)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(14)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(15)),
Value: horse.TenThousandths(s.ColumnInt(16)),
Target: horse.AbilityTarget(s.ColumnInt(17)),
TargetValue: s.ColumnInt32(18),
},
{
Type: horse.AbilityType(s.ColumnInt(19)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(20)),
Value: horse.TenThousandths(s.ColumnInt(21)),
Target: horse.AbilityTarget(s.ColumnInt(22)),
TargetValue: s.ColumnInt32(23),
},
{
Type: horse.AbilityType(s.ColumnInt(24)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(25)),
Value: horse.TenThousandths(s.ColumnInt(26)),
Target: horse.AbilityTarget(s.ColumnInt(27)),
TargetValue: s.ColumnInt32(28),
},
}),
},
{
Precondition: s.ColumnText(29),
Condition: s.ColumnText(30),
Duration: horse.TenThousandths(s.ColumnInt(31)),
DurScale: horse.DurScale(s.ColumnInt(32)),
Cooldown: horse.TenThousandths(s.ColumnInt(33)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(34)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(35)),
Value: horse.TenThousandths(s.ColumnInt(36)),
Target: horse.AbilityTarget(s.ColumnInt(37)),
TargetValue: s.ColumnInt32(38),
},
{
Type: horse.AbilityType(s.ColumnInt(39)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(40)),
Value: horse.TenThousandths(s.ColumnInt(41)),
Target: horse.AbilityTarget(s.ColumnInt(42)),
TargetValue: s.ColumnInt32(43),
},
{
Type: horse.AbilityType(s.ColumnInt(44)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(45)),
Value: horse.TenThousandths(s.ColumnInt(46)),
Target: horse.AbilityTarget(s.ColumnInt(47)),
TargetValue: s.ColumnInt32(48),
},
}),
},
}),
UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name
Tags: parseTags(s.ColumnText(54)),
SPCost: s.ColumnInt(49),
IconID: s.ColumnInt(53),
}
})
races := load(ctx1, loadgroup, db, "races", raceSQL, func(s *sqlite.Stmt) horse.Race {
return horse.Race{
ID: horse.RaceID(s.ColumnInt(0)),
Name: s.ColumnText(1),
// TODO(zeph): grade
Thumbnail: s.ColumnInt(3),
Primary: horse.RaceID(s.ColumnInt(4)),
}
})
saddles := load(ctx1, loadgroup, db, "saddles", saddleSQL, func(s *sqlite.Stmt) horse.Saddle {
return horse.Saddle{
ID: horse.SaddleID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Races: trimZeros(
horse.RaceID(s.ColumnInt(2)),
horse.RaceID(s.ColumnInt(3)),
horse.RaceID(s.ColumnInt(4)),
),
Type: horse.SaddleType(s.ColumnInt(5)),
Primary: horse.SaddleID(s.ColumnInt(6)),
}
})
scenarios := load(ctx1, loadgroup, db, "scenarios", scenarioSQL, func(s *sqlite.Stmt) horse.Scenario {
return horse.Scenario{
ID: horse.ScenarioID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Title: s.ColumnText(2),
}
})
sparks := load(ctx1, loadgroup, db, "sparks", sparkSQL, func(s *sqlite.Stmt) horse.Spark {
return horse.Spark{
ID: horse.SparkID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SparkGroupID(s.ColumnInt(3)),
Rarity: horse.SparkRarity(s.ColumnInt(4)),
Type: horse.SparkType(s.ColumnInt(5)),
// Effects filled in later.
}
})
sparkeffs := load(ctx1, loadgroup, db, "spark effects", sparkEffectSQL, func(s *sqlite.Stmt) SparkEffImm {
return SparkEffImm{
Group: horse.SparkGroupID(s.ColumnInt(0)),
Effect: s.ColumnInt(1),
Target: horse.SparkTarget(s.ColumnInt(2)),
Value1: s.ColumnInt32(3),
Value2: s.ColumnInt32(4),
}
})
convos := load(ctx1, loadgroup, db, "lobby conversations", conversationSQL, func(s *sqlite.Stmt) horse.Conversation {
return horse.Conversation{
CharacterID: horse.CharacterID(s.ColumnInt(0)),
Number: s.ColumnInt(1),
Location: horse.LobbyConversationLocationID(s.ColumnInt(2)),
LocationName: horse.LobbyConversationLocationID(s.ColumnInt(2)).String(),
Chara1: horse.CharacterID(s.ColumnInt(3)),
Chara2: horse.CharacterID(s.ColumnInt(4)),
Chara3: horse.CharacterID(s.ColumnInt(5)),
ConditionType: s.ColumnInt(6),
}
})
if err := os.MkdirAll(filepath.Join(out, region), 0775); err != nil {
slog.Error("create output dir", slog.Any("err", err))
os.Exit(1)
}
writegroup, ctx2 := errgroup.WithContext(ctx)
writegroup.Go(func() error { return write(ctx2, out, region, "character.json", charas) })
writegroup.Go(func() error { return write(ctx2, out, region, "affinity.json", aff) })
writegroup.Go(func() error { return write(ctx2, out, region, "uma.json", umas) })
writegroup.Go(func() error { return write(ctx2, out, region, "skill-group.json", sg) })
writegroup.Go(func() error { return write(ctx2, out, region, "skill.json", skills) })
writegroup.Go(func() error { return write(ctx2, out, region, "race.json", races) })
writegroup.Go(func() error { return write(ctx2, out, region, "saddle.json", saddles) })
writegroup.Go(func() error { return write(ctx2, out, region, "scenario.json", scenarios) })
writegroup.Go(func() error { return write(ctx2, out, region, "spark.json", mergesparks(sparks, sparkeffs)) })
writegroup.Go(func() error { return write(ctx2, out, region, "conversation.json", convos) })
if err := writegroup.Wait(); err != nil {
slog.ErrorContext(ctx, "write", slog.Any("err", err))
os.Exit(1)
}
slog.InfoContext(ctx, "done")
}
var (
//go:embed sql/character.sql
characterSQL string
//go:embed sql/affinity.sql
affinitySQL string
//go:embed sql/uma.sql
umaSQL string
//go:embed sql/skill-group.sql
skillGroupSQL string
//go:embed sql/skill.sql
skillSQL string
//go:embed sql/race.sql
raceSQL string
//go:embed sql/saddle.sql
saddleSQL string
//go:embed sql/scenario.sql
scenarioSQL string
//go:embed sql/spark.sql
sparkSQL string
//go:embed sql/spark-effect.sql
sparkEffectSQL string
//go:embed sql/conversation.sql
conversationSQL string
)
func load[T any](ctx context.Context, group *errgroup.Group, db *sqlitex.Pool, kind, sql string, row func(*sqlite.Stmt) T) func() ([]T, error) {
slog.InfoContext(ctx, "load", slog.String("kind", kind))
var r []T
group.Go(func() error {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return fmt.Errorf("couldn't get connection for %s: %w", kind, err)
}
stmt, _, err := conn.PrepareTransient(sql)
if err != nil {
return fmt.Errorf("couldn't prepare statement for %s: %w", kind, err)
}
for {
ok, err := stmt.Step()
if err != nil {
return fmt.Errorf("error stepping %s: %w", kind, err)
}
if !ok {
break
}
r = append(r, row(stmt))
}
return nil
})
return func() ([]T, error) {
err := group.Wait()
if err == context.Canceled {
// After the first wait, all future ones return context.Canceled.
// We want to be able to wait any number of times, so hide it.
err = nil
}
return r, err
}
}
func write[T any](ctx context.Context, out, region, name string, v func() (T, error)) error {
p := filepath.Join(out, region, name)
r, err := v()
if err != nil {
return err
}
slog.InfoContext(ctx, "write", slog.String("path", p))
f, err := os.Create(p)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
enc.SetIndent("", "\t")
err = enc.Encode(r)
err = errors.Join(err, w.Flush())
slog.InfoContext(ctx, "marshaled", slog.String("path", p))
return err
}
func mergesparks(sparks func() ([]horse.Spark, error), effs func() ([]SparkEffImm, error)) func() ([]horse.Spark, error) {
return func() ([]horse.Spark, error) {
sp, err := sparks()
if err != nil {
return nil, err
}
ef, err := effs()
if err != nil {
return nil, err
}
// Spark effects are sorted by group ID, but groups apply to multiple
// sparks, and we don't rely on sparks and groups being in the same order.
// It is possible to merge in linear time, but not worth the effort:
// n log n is fine since this is an AOT step.
for i := range sp {
k, ok := slices.BinarySearchFunc(ef, sp[i].Group, func(e SparkEffImm, v horse.SparkGroupID) int { return cmp.Compare(e.Group, v) })
if !ok {
panic(fmt.Errorf("mergesparks: no spark group for %+v", &sp[i]))
}
// Back up to the first effect in the group.
for k > 0 && ef[k-1].Group == sp[i].Group {
k--
}
// Map effect IDs to the lists of their effects.
m := make(map[int][]horse.SparkEffect)
for _, e := range ef[k:] {
if e.Group != sp[i].Group {
// Done with this group.
break
}
m[e.Effect] = append(m[e.Effect], horse.SparkEffect{Target: e.Target, Value1: e.Value1, Value2: e.Value2})
}
// Now get effects in order.
keys := slices.Sorted(maps.Keys(m))
sp[i].Effects = make([][]horse.SparkEffect, 0, len(keys))
for _, key := range keys {
sp[i].Effects = append(sp[i].Effects, m[key])
}
}
return sp, nil
}
}
type SparkEffImm struct {
Group horse.SparkGroupID
Effect int
Target horse.SparkTarget
Value1 int32
Value2 int32
}
func parseTags(s string) []uint16 {
r := make([]uint16, 0, 8)
for s != "" {
t, u, _ := strings.Cut(s, "/")
s = u
v, err := strconv.ParseUint(t, 10, 16)
if err != nil {
panic(fmt.Errorf("parsing skill tags: %w", err))
}
r = append(r, uint16(v))
}
return trimZeros(r...)
}
func trimAbilities(s []horse.Ability) []horse.Ability {
for len(s) > 0 && s[len(s)-1].Type == 0 {
s = s[:len(s)-1]
}
return s
}
func trimActivations(s []horse.Activation) []horse.Activation {
for len(s) > 0 && s[len(s)-1].Condition == "" {
s = s[:len(s)-1]
}
return s
}
func trimZeros[T comparable](s ...T) []T {
var zero T
for len(s) > 0 && s[len(s)-1] == zero {
s = s[:len(s)-1]
}
return s
}
+91
View File
@@ -15,6 +15,9 @@ This file is my notes from exploring the database.
- 119 is scenario full titles (e.g. The Beginning: URA Finale), 120 is scenario descriptions, 237 is scenario names (e.g. URA Finale) - 119 is scenario full titles (e.g. The Beginning: URA Finale), 120 is scenario descriptions, 237 is scenario names (e.g. URA Finale)
- 130 is epithet names, 131 is epithet descriptions - 130 is epithet names, 131 is epithet descriptions
in the decomp, text data categories are named in `public enum MasterString.Category`.
some categories seem to be missing, though.
# succession factor (sparks) # succession factor (sparks)
tables are succession_factor and succession_factor_effect tables are succession_factor and succession_factor_effect
@@ -228,6 +231,63 @@ tag_id is a slash-separated list of numeric IDs that roughly describe all effect
- 614 base stat threshold passive - 614 base stat threshold passive
- 615 mood passive - 615 mood passive
unrelated, query to get all condition types:
```sql
WITH RECURSIVE condition AS (
SELECT DISTINCT precondition_1 AS c FROM skill_data
UNION
SELECT DISTINCT condition_1 AS c FROM skill_data
UNION
SELECT DISTINCT precondition_2 AS c FROM skill_data
UNION
SELECT DISTINCT condition_2 AS c FROM skill_data
), disj AS (
SELECT
IIF(INSTR(c, '@') != 0, SUBSTR(c, 1, INSTR(c, '@')-1), c) AS c,
IIF(INSTR(c, '@') != 0, SUBSTR(c, 1 + INSTR(c, '@')), '') AS rest
FROM condition
UNION
SELECT
IIF(INSTR(rest, '@') != 0, SUBSTR(rest, 1, INSTR(rest, '@')-1), rest) AS c,
IIF(INSTR(rest, '@') != 0, SUBSTR(rest, 1 + INSTR(rest, '@')), '') AS rest
FROM disj
WHERE rest != ''
), term AS (
SELECT
IIF(INSTR(c, '&') != 0, SUBSTR(c, 1, INSTR(c, '&')-1), c) AS term,
IIF(INSTR(c, '&') != 0, SUBSTR(c, 1 + INSTR(c, '&')), '') AS rest
FROM disj
UNION
SELECT
IIF(INSTR(rest, '&') != 0, SUBSTR(rest, 1, INSTR(rest, '&')-1), rest) AS term,
IIF(INSTR(rest, '&') != 0, SUBSTR(rest, 1 + INSTR(rest, '&')), '') AS rest
FROM term
WHERE rest != ''
), op AS (
SELECT
term,
CASE
WHEN INSTR(term, '==') != 0 THEN '=='
WHEN INSTR(term, '!=') != 0 THEN '!='
WHEN INSTR(term, '<=') != 0 THEN '<='
WHEN INSTR(term, '>=') != 0 THEN '>='
WHEN INSTR(term, '<') != 0 THEN '<'
WHEN INSTR(term, '>') != 0 THEN '>'
END AS op
FROM term WHERE term != ''
), type_op_arg AS (
SELECT DISTINCT
term,
SUBSTR(term, 1, INSTR(term, op) - 1) AS "type",
op,
CAST(SUBSTR(term, INSTR(term, op) + LENGTH(op)) AS INTEGER) AS arg
FROM op
)
SELECT DISTINCT CONCAT(type, op), arg FROM type_op_arg
```
final select can be adjusted to get different info, but type+op together seems most useful
# races # races
- group 1, grade: g1 100, g2 200, g3 300, op 400, pre-op 700 - group 1, grade: g1 100, g2 200, g3 300, op 400, pre-op 700
@@ -260,6 +320,37 @@ so, it doesn't seem like there's a particular flag that identifies maiden races,
- card_talent_hint_upgrade has costs to raise hint levels, but it's actually universal, only six rows - card_talent_hint_upgrade has costs to raise hint levels, but it's actually universal, only six rows
- single_mode_route_race is career goals (not only races) - single_mode_route_race is career goals (not only races)
- available_skill_set has starting skills including those unlocked by potential level given under need_rank (0 for pl1, 2 for pl2) - available_skill_set has starting skills including those unlocked by potential level given under need_rank (0 for pl1, 2 for pl2)
- single_mode_special_chara is scenario links
# gl
single_mode_live_song_list has the song list for lives ha ha.
this is defined from the perspective of bonuses they grant when you have them, not the shop per se.
- text_data category for song names is 210, look up by live_id; for post-concert effect text, category 208 indexed by song list id
- command_id and live_id are always equal
- level is 1 for all songs except the two instances of girls' legend u
- master_bonus_content_text_id corresponds to text_data categories 207 for pickup effect text and 209 for song name
- live_bonus_type is 1 for specialty prio, 2 for chain frequency, 3 for fb
- girls' legend u is defined twice as ids 22 and 24
costs are in single_mode_live_square.
- square_type is 1 for stat up, 2 for skill up, 3 for energy up, 4 for live
- perf_type_n is token type: 1 for dance, 2 for passion, 3 for vocals, 4 for visuals, 5 for composure
- perf_value_n is number of tokens
immediate bonuses are in single_mode_live_master_bonus.
- master_bonus_type is 1 for stat up, 2 for skill hint, 3 for energy up, 4 for song
- master_bonus_type_value is 1 for type 1, live id for type 4, 0 otherwise
- master_bonus_gain_type_n is 1 for stat, 2 for skill hint, 3 for energy, 5 for permanent training bonus
+ 1 for stat: then master_bonus_gain_value_n_1 is 1-5 for stat id or 6 for sp, n_2 is 1 for fixed or 2 for random (not actually in the game), n_3 is lower bound on gain, n_4 is 0 for fixed or upper bound for random
+ 2 for skill hint: then gain_value_n_1 is 3(*), n_2 is skill tag, n_3 is hint level
+ 3 for energy: then gain_value_n_1 is amount
+ 5 for permanent training bonus: then gain_value_n_1 is stat id (6 for sp), n_2 is amount
(*) MasterSingleModeLiveMasterBonus.SingleModeLiveMasterBonus.GainSkillHintTypeEnum defines values of Random = 1 and SkillId = 2, but they are unused in the db.
there is some ostensibly unused data.
- master bonus ids 12001-12206 grant random amounts of stats
- id 20000 corresponds to "Group Lesson" "Skill hint appropriate for aptitude"
# lobby conversations!!! # lobby conversations!!!
-189583
View File
File diff suppressed because it is too large Load Diff
-346
View File
@@ -1,346 +0,0 @@
[
{
"chara_id": 1001,
"name": "Special Week"
},
{
"chara_id": 1002,
"name": "Silence Suzuka"
},
{
"chara_id": 1003,
"name": "Tokai Teio"
},
{
"chara_id": 1004,
"name": "Maruzensky"
},
{
"chara_id": 1005,
"name": "Fuji Kiseki"
},
{
"chara_id": 1006,
"name": "Oguri Cap"
},
{
"chara_id": 1007,
"name": "Gold Ship"
},
{
"chara_id": 1008,
"name": "Vodka"
},
{
"chara_id": 1009,
"name": "Daiwa Scarlet"
},
{
"chara_id": 1010,
"name": "Taiki Shuttle"
},
{
"chara_id": 1011,
"name": "Grass Wonder"
},
{
"chara_id": 1012,
"name": "Hishi Amazon"
},
{
"chara_id": 1013,
"name": "Mejiro McQueen"
},
{
"chara_id": 1014,
"name": "El Condor Pasa"
},
{
"chara_id": 1015,
"name": "T.M. Opera O"
},
{
"chara_id": 1016,
"name": "Narita Brian"
},
{
"chara_id": 1017,
"name": "Symboli Rudolf"
},
{
"chara_id": 1018,
"name": "Air Groove"
},
{
"chara_id": 1019,
"name": "Agnes Digital"
},
{
"chara_id": 1020,
"name": "Seiun Sky"
},
{
"chara_id": 1021,
"name": "Tamamo Cross"
},
{
"chara_id": 1022,
"name": "Fine Motion"
},
{
"chara_id": 1023,
"name": "Biwa Hayahide"
},
{
"chara_id": 1024,
"name": "Mayano Top Gun"
},
{
"chara_id": 1025,
"name": "Manhattan Cafe"
},
{
"chara_id": 1026,
"name": "Mihono Bourbon"
},
{
"chara_id": 1027,
"name": "Mejiro Ryan"
},
{
"chara_id": 1028,
"name": "Hishi Akebono"
},
{
"chara_id": 1029,
"name": "Yukino Bijin"
},
{
"chara_id": 1030,
"name": "Rice Shower"
},
{
"chara_id": 1031,
"name": "Ines Fujin"
},
{
"chara_id": 1032,
"name": "Agnes Tachyon"
},
{
"chara_id": 1033,
"name": "Admire Vega"
},
{
"chara_id": 1034,
"name": "Inari One"
},
{
"chara_id": 1035,
"name": "Winning Ticket"
},
{
"chara_id": 1036,
"name": "Air Shakur"
},
{
"chara_id": 1037,
"name": "Eishin Flash"
},
{
"chara_id": 1038,
"name": "Curren Chan"
},
{
"chara_id": 1039,
"name": "Kawakami Princess"
},
{
"chara_id": 1040,
"name": "Gold City"
},
{
"chara_id": 1041,
"name": "Sakura Bakushin O"
},
{
"chara_id": 1042,
"name": "Seeking the Pearl"
},
{
"chara_id": 1043,
"name": "Shinko Windy"
},
{
"chara_id": 1044,
"name": "Sweep Tosho"
},
{
"chara_id": 1045,
"name": "Super Creek"
},
{
"chara_id": 1046,
"name": "Smart Falcon"
},
{
"chara_id": 1047,
"name": "Zenno Rob Roy"
},
{
"chara_id": 1048,
"name": "Tosen Jordan"
},
{
"chara_id": 1049,
"name": "Nakayama Festa"
},
{
"chara_id": 1050,
"name": "Narita Taishin"
},
{
"chara_id": 1051,
"name": "Nishino Flower"
},
{
"chara_id": 1052,
"name": "Haru Urara"
},
{
"chara_id": 1053,
"name": "Bamboo Memory"
},
{
"chara_id": 1054,
"name": "Biko Pegasus"
},
{
"chara_id": 1055,
"name": "Marvelous Sunday"
},
{
"chara_id": 1056,
"name": "Matikanefukukitaru"
},
{
"chara_id": 1057,
"name": "Mr. C.B."
},
{
"chara_id": 1058,
"name": "Meisho Doto"
},
{
"chara_id": 1059,
"name": "Mejiro Dober"
},
{
"chara_id": 1060,
"name": "Nice Nature"
},
{
"chara_id": 1061,
"name": "King Halo"
},
{
"chara_id": 1062,
"name": "Matikanetannhauser"
},
{
"chara_id": 1063,
"name": "Ikuno Dictus"
},
{
"chara_id": 1064,
"name": "Mejiro Palmer"
},
{
"chara_id": 1065,
"name": "Daitaku Helios"
},
{
"chara_id": 1066,
"name": "Twin Turbo"
},
{
"chara_id": 1067,
"name": "Satono Diamond"
},
{
"chara_id": 1068,
"name": "Kitasan Black"
},
{
"chara_id": 1069,
"name": "Sakura Chiyono O"
},
{
"chara_id": 1070,
"name": "Sirius Symboli"
},
{
"chara_id": 1071,
"name": "Mejiro Ardan"
},
{
"chara_id": 1072,
"name": "Yaeno Muteki"
},
{
"chara_id": 1073,
"name": "Tsurumaru Tsuyoshi"
},
{
"chara_id": 1074,
"name": "Mejiro Bright"
},
{
"chara_id": 1077,
"name": "Narita Top Road"
},
{
"chara_id": 1078,
"name": "Yamanin Zephyr"
},
{
"chara_id": 2001,
"name": "Happy Meek"
},
{
"chara_id": 2002,
"name": "Bitter Glasse"
},
{
"chara_id": 2003,
"name": "Little Cocon"
},
{
"chara_id": 2004,
"name": "Montjeu"
},
{
"chara_id": 9001,
"name": "Tazuna Hayakawa"
},
{
"chara_id": 9002,
"name": "Director Akikawa"
},
{
"chara_id": 9003,
"name": "Etsuko Otonashi"
},
{
"chara_id": 9004,
"name": "Trainer Kiryuin"
},
{
"chara_id": 9005,
"name": "Sasami Anshinzawa"
},
{
"chara_id": 9006,
"name": "Riko Kashimoto"
}
]
File diff suppressed because it is too large Load Diff
-1712
View File
File diff suppressed because it is too large Load Diff
-1420
View File
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +0,0 @@
[
{
"scenario_id": 1,
"name": "URA Finale",
"title": "The Beginning: URA Finale"
},
{
"scenario_id": 2,
"name": "Unity Cup",
"title": "Unity Cup: Shine On, Team Spirit!"
},
{
"scenario_id": 4,
"name": "TS Climax",
"title": "Trackblazer: Start of the Climax"
}
]
File diff suppressed because it is too large Load Diff
-19511
View File
File diff suppressed because it is too large Load Diff
-38294
View File
File diff suppressed because it is too large Load Diff
-1994
View File
File diff suppressed because it is too large Load Diff
+19 -12
View File
@@ -3,31 +3,38 @@ module git.sunturtle.xyz/zephyr/horse
go 1.25.5 go 1.25.5
require ( require (
github.com/disgoorg/disgo v0.19.0-rc.15 github.com/disgoorg/disgo v0.19.6
github.com/go-chi/chi/v5 v5.2.5 github.com/gen2brain/avif v0.6.0
github.com/junegunn/fzf v0.67.0 github.com/go-chi/chi/v5 v5.3.1
golang.org/x/sync v0.20.0 github.com/google/go-cmp v0.7.0
github.com/junegunn/fzf v0.74.1
golang.org/x/image v0.44.0
golang.org/x/sync v0.22.0
zombiezen.com/go/sqlite v1.4.2 zombiezen.com/go/sqlite v1.4.2
) )
require ( require (
github.com/disgoorg/godave v0.3.0 // indirect
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.0.1 // indirect
github.com/ebitengine/purego v0.10.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/klauspost/compress v1.18.2 // indirect github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/klauspost/compress v1.19.1 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/mattn/go-isatty v0.0.24 // indirect
github.com/ncruces/go-strftime v1.0.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
golang.org/x/crypto v0.46.0 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/crypto v0.54.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.47.0 // indirect
modernc.org/libc v1.65.7 // indirect golang.org/x/tools v0.48.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.11.0 // indirect
modernc.org/sqlite v1.37.1 // indirect modernc.org/sqlite v1.54.0 // indirect
) )
+56 -41
View File
@@ -1,7 +1,9 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disgoorg/disgo v0.19.0-rc.15 h1:x0NsV2gcbdjwuztsg2wYXw76p1Cpc8f6ByDrkPcfQtU= github.com/disgoorg/disgo v0.19.6 h1:1xJQt6KZgiqvBOnjuziG1gE4Iqd0aJn8DNfE6eE+0qw=
github.com/disgoorg/disgo v0.19.0-rc.15/go.mod h1:14mgXzenkJqifkDmsEgU0zI1di6jNXodwX6L8geW33A= github.com/disgoorg/disgo v0.19.6/go.mod h1:NnV63iw4lJdF1fnV0gX27XR43ZgRGqnL122svRMTgTE=
github.com/disgoorg/godave v0.3.0 h1:37F3ZuiMd8/EXeoCtTeE8iP2eT2nWsfEa4/ITwoKfe4=
github.com/disgoorg/godave v0.3.0/go.mod h1:OreAC3hpabr39bMVA+jwOVDq1EUPXH5A0XUBiZaDI1Y=
github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI= github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI=
github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI= github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI=
github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78= github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78=
@@ -10,22 +12,30 @@ github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzg
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.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gen2brain/avif v0.6.0/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
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/junegunn/fzf v0.67.0 h1:naiOdIkV5/ZCfHgKQIV/f5YDWowl95G6yyOQqW8FeSo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/junegunn/fzf v0.67.0/go.mod h1:xlXX2/rmsccKQUnr9QOXPDi5DyV9cM0UjKy/huScBeE= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/junegunn/fzf v0.74.1 h1:rCGVdCSoOkfcfTxtxCO/vG2yceDjRlKSlv7IfCGkc/Y=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/junegunn/fzf v0.74.1/go.mod h1:hKDkO8orBWT5VLIZOL8oyhxn7mRLXdGyu+gz1Ss58pU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/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=
@@ -36,43 +46,48 @@ github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxE
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
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.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 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/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws=
modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4=
modernc.org/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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.1.4/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.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
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=
+101
View File
@@ -0,0 +1,101 @@
package mdb
import (
"context"
_ "embed"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
)
var (
//go:embed sql/character.sql
characterSQL string
//go:embed sql/affinity.sql
affinitySQL string
//go:embed sql/affinity-detail.sql
affinityDetailSQL string
//go:embed sql/uma.sql
umaSQL string
//go:embed sql/conversation.sql
conversationSQL string
)
// Characters retrieves all named characters.
func Characters(ctx context.Context, db *sqlitex.Pool) ([]horse.Character, error) {
return load(ctx, db, make([]horse.Character, 0, 128), characterSQL, func(s *sqlite.Stmt) horse.Character {
return horse.Character{
ID: horse.CharacterID(s.ColumnInt(0)),
Name: s.ColumnText(1),
}
})
}
// AffinitySummary gets precomputed base affinity for all pairs and trios of characters.
func AffinitySummary(ctx context.Context, db *sqlitex.Pool) ([]horse.AffinityRelation, error) {
return load(ctx, db, nil, affinitySQL, func(s *sqlite.Stmt) horse.AffinityRelation {
return horse.AffinityRelation{
IDA: horse.CharacterID(s.ColumnInt(0)),
IDB: horse.CharacterID(s.ColumnInt(1)),
IDC: horse.CharacterID(s.ColumnInt(2)),
Affinity: s.ColumnInt(3),
}
})
}
// AffinityDetail gets all individual affinity group entries.
func AffinityDetail(ctx context.Context, db *sqlitex.Pool) ([]horse.AffinityDetail, error) {
return load(ctx, db, nil, affinityDetailSQL, func(s *sqlite.Stmt) horse.AffinityDetail {
return horse.AffinityDetail{
CharaID: horse.CharacterID(s.ColumnInt(0)),
Relation: uint16(s.ColumnInt(1)),
Affinity: uint16(s.ColumnInt(2)),
}
})
}
// Umas retrieves all trainable Uma variants.
func Umas(ctx context.Context, db *sqlitex.Pool) ([]horse.Uma, error) {
return load(ctx, db, make([]horse.Uma, 0, 128), umaSQL, func(s *sqlite.Stmt) horse.Uma {
return horse.Uma{
ID: horse.UmaID(s.ColumnInt(0)),
CharacterID: horse.CharacterID(s.ColumnInt(1)),
Name: s.ColumnText(2),
Variant: s.ColumnText(3),
Sprint: horse.AptitudeLevel(s.ColumnInt(5)),
Mile: horse.AptitudeLevel(s.ColumnInt(6)),
Medium: horse.AptitudeLevel(s.ColumnInt(7)),
Long: horse.AptitudeLevel(s.ColumnInt(8)),
Front: horse.AptitudeLevel(s.ColumnInt(9)),
Pace: horse.AptitudeLevel(s.ColumnInt(10)),
Late: horse.AptitudeLevel(s.ColumnInt(11)),
End: horse.AptitudeLevel(s.ColumnInt(12)),
Turf: horse.AptitudeLevel(s.ColumnInt(13)),
Dirt: horse.AptitudeLevel(s.ColumnInt(14)),
Unique: horse.SkillID(s.ColumnInt(15)),
Skill1: horse.SkillID(s.ColumnInt(16)),
Skill2: horse.SkillID(s.ColumnInt(17)),
Skill3: horse.SkillID(s.ColumnInt(18)),
SkillPL2: horse.SkillID(s.ColumnInt(19)),
SkillPL3: horse.SkillID(s.ColumnInt(20)),
SkillPL4: horse.SkillID(s.ColumnInt(21)),
SkillPL5: horse.SkillID(s.ColumnInt(22)),
}
})
}
func Conversations(ctx context.Context, db *sqlitex.Pool) ([]horse.Conversation, error) {
return load(ctx, db, make([]horse.Conversation, 0, 1024), conversationSQL, func(s *sqlite.Stmt) horse.Conversation {
return horse.Conversation{
CharacterID: horse.CharacterID(s.ColumnInt(0)),
Number: s.ColumnInt(1),
Location: horse.LobbyConversationLocationID(s.ColumnInt(2)),
Chara1: horse.CharacterID(s.ColumnInt(3)),
Chara2: horse.CharacterID(s.ColumnInt(4)),
Chara3: horse.CharacterID(s.ColumnInt(5)),
ConditionType: s.ColumnInt(6),
}
})
}
+325
View File
@@ -0,0 +1,325 @@
package mdb_test
import (
_ "embed"
"testing"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/mdb"
"github.com/google/go-cmp/cmp"
)
//go:embed testdata/character.sql
var characterSQL string
func TestCharacters(t *testing.T) {
db := testdb(t.Context(), "file:TestCharacters?mode=memory&cache=shared", characterSQL)
got, err := mdb.Characters(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Character{
{ID: 1001, Name: "Special Week"},
{ID: 1002, Name: "Silence Suzuka"},
{ID: 1078, Name: "Yamanin Zephyr"},
{ID: 2001, Name: "Happy Meek"},
{ID: 9001, Name: "Tazuna Hayakawa"},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong characters (+got/-want):\n%s", diff)
}
}
func TestAffinitySummary(t *testing.T) {
db := testdb(t.Context(), "file:TestAffinitySummary?mode=memory&cache=shared", characterSQL)
got, err := mdb.AffinitySummary(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.AffinityRelation{
{IDA: 1001, IDB: 1002, Affinity: 23},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong affinity (+got/-want):\n%s", diff)
}
}
func TestAffinityDetail(t *testing.T) {
db := testdb(t.Context(), "file:TestAffinityDetail?mode=memory&cache=shared", characterSQL)
got, err := mdb.AffinityDetail(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.AffinityDetail{
{CharaID: 1001, Relation: 103, Affinity: 2},
{CharaID: 1001, Relation: 202, Affinity: 2},
{CharaID: 1001, Relation: 301, Affinity: 2},
{CharaID: 1001, Relation: 401, Affinity: 2},
{CharaID: 1001, Relation: 502, Affinity: 2},
{CharaID: 1001, Relation: 510, Affinity: 2},
{CharaID: 1001, Relation: 2001, Affinity: 1},
{CharaID: 1001, Relation: 2102, Affinity: 1},
{CharaID: 1001, Relation: 2301, Affinity: 1},
{CharaID: 1001, Relation: 2406, Affinity: 1},
{CharaID: 1001, Relation: 2506, Affinity: 1},
{CharaID: 1001, Relation: 2510, Affinity: 1},
{CharaID: 1001, Relation: 2516, Affinity: 1},
{CharaID: 1001, Relation: 2519, Affinity: 1},
{CharaID: 1001, Relation: 2601, Affinity: 1},
{CharaID: 1001, Relation: 2603, Affinity: 1},
{CharaID: 1001, Relation: 2811, Affinity: 1},
{CharaID: 1001, Relation: 2903, Affinity: 7},
{CharaID: 1001, Relation: 3003, Affinity: 7},
{CharaID: 1001, Relation: 3004, Affinity: 7},
{CharaID: 1001, Relation: 3101, Affinity: 7},
{CharaID: 1001, Relation: 3205, Affinity: 1},
{CharaID: 1001, Relation: 7003, Affinity: 1},
{CharaID: 1001, Relation: 7004, Affinity: 1},
{CharaID: 1001, Relation: 7005, Affinity: 1},
{CharaID: 1001, Relation: 7006, Affinity: 1},
{CharaID: 1001, Relation: 7007, Affinity: 1},
{CharaID: 1001, Relation: 8001, Affinity: 1},
{CharaID: 1001, Relation: 8002, Affinity: 1},
{CharaID: 1001, Relation: 8003, Affinity: 1},
{CharaID: 1001, Relation: 8004, Affinity: 1},
{CharaID: 1001, Relation: 8005, Affinity: 1},
{CharaID: 1001, Relation: 8006, Affinity: 1},
{CharaID: 1001, Relation: 8007, Affinity: 1},
{CharaID: 1001, Relation: 8008, Affinity: 1},
{CharaID: 1001, Relation: 8009, Affinity: 1},
{CharaID: 1001, Relation: 8010, Affinity: 1},
{CharaID: 1001, Relation: 8011, Affinity: 1},
{CharaID: 1001, Relation: 8012, Affinity: 1},
{CharaID: 1001, Relation: 8013, Affinity: 1},
{CharaID: 1001, Relation: 8014, Affinity: 1},
{CharaID: 1001, Relation: 8015, Affinity: 1},
{CharaID: 1001, Relation: 8016, Affinity: 1},
{CharaID: 1001, Relation: 8443, Affinity: 1},
{CharaID: 1001, Relation: 8479, Affinity: 1},
{CharaID: 1001, Relation: 8511, Affinity: 1},
{CharaID: 1001, Relation: 8536, Affinity: 1},
{CharaID: 1001, Relation: 8640, Affinity: 1},
{CharaID: 1002, Relation: 104, Affinity: 2},
{CharaID: 1002, Relation: 202, Affinity: 2},
{CharaID: 1002, Relation: 301, Affinity: 2},
{CharaID: 1002, Relation: 402, Affinity: 2},
{CharaID: 1002, Relation: 2001, Affinity: 1},
{CharaID: 1002, Relation: 2406, Affinity: 1},
{CharaID: 1002, Relation: 2512, Affinity: 1},
{CharaID: 1002, Relation: 2601, Affinity: 1},
{CharaID: 1002, Relation: 2603, Affinity: 1},
{CharaID: 1002, Relation: 2810, Affinity: 1},
{CharaID: 1002, Relation: 2901, Affinity: 7},
{CharaID: 1002, Relation: 3003, Affinity: 7},
{CharaID: 1002, Relation: 3101, Affinity: 7},
{CharaID: 1002, Relation: 3205, Affinity: 1},
{CharaID: 1002, Relation: 7000, Affinity: 1},
{CharaID: 1002, Relation: 7001, Affinity: 1},
{CharaID: 1002, Relation: 7002, Affinity: 1},
{CharaID: 1002, Relation: 7013, Affinity: 1},
{CharaID: 1002, Relation: 7015, Affinity: 1},
{CharaID: 1002, Relation: 8017, Affinity: 1},
{CharaID: 1002, Relation: 8018, Affinity: 1},
{CharaID: 1002, Relation: 8019, Affinity: 1},
{CharaID: 1002, Relation: 8020, Affinity: 1},
{CharaID: 1002, Relation: 8021, Affinity: 1},
{CharaID: 1002, Relation: 8022, Affinity: 1},
{CharaID: 1002, Relation: 8023, Affinity: 1},
{CharaID: 1002, Relation: 8024, Affinity: 1},
{CharaID: 1002, Relation: 8025, Affinity: 1},
{CharaID: 1002, Relation: 8026, Affinity: 1},
{CharaID: 1002, Relation: 8027, Affinity: 1},
{CharaID: 1002, Relation: 8028, Affinity: 1},
{CharaID: 1002, Relation: 8029, Affinity: 1},
{CharaID: 1002, Relation: 8030, Affinity: 1},
{CharaID: 1002, Relation: 8031, Affinity: 1},
{CharaID: 1002, Relation: 8032, Affinity: 1},
{CharaID: 1002, Relation: 8444, Affinity: 1},
{CharaID: 1002, Relation: 8480, Affinity: 1},
{CharaID: 1002, Relation: 8512, Affinity: 1},
{CharaID: 1002, Relation: 8537, Affinity: 1},
{CharaID: 1002, Relation: 8641, Affinity: 1},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong affinity details (+got/-want):\n%s", diff)
}
}
func TestUmas(t *testing.T) {
db := testdb(t.Context(), "file:TestUmas?mode=memory&cache=shared", characterSQL)
got, err := mdb.Umas(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Uma{
{
ID: 100101,
CharacterID: 1001,
Name: `[Special Dreamer] Special Week`,
Variant: `[Special Dreamer]`,
Sprint: 2,
Mile: 5,
Medium: 7,
Long: 7,
Front: 1,
Pace: 7,
Late: 7,
End: 5,
Turf: 7,
Dirt: 1,
Unique: 100011,
Skill1: 200512,
Skill2: 201352,
Skill3: 200732,
SkillPL2: 200162,
SkillPL3: 201351,
SkillPL4: 200612,
SkillPL5: 200511,
},
{
ID: 100102,
CharacterID: 1001,
Name: `[Hopp'n♪Happy Heart] Special Week`,
Variant: `[Hopp'n♪Happy Heart]`,
Sprint: 2,
Mile: 5,
Medium: 7,
Long: 7,
Front: 1,
Pace: 7,
Late: 7,
End: 5,
Turf: 7,
Dirt: 1,
Unique: 110011,
Skill1: 200462,
Skill2: 200592,
Skill3: 201132,
SkillPL2: 200212,
SkillPL3: 200591,
SkillPL4: 201611,
SkillPL5: 200461,
},
{
ID: 100201,
CharacterID: 1002,
Name: `[Innocent Silence] Silence Suzuka`,
Variant: `[Innocent Silence]`,
Sprint: 4,
Mile: 7,
Medium: 7,
Long: 3,
Front: 7,
Pace: 5,
Late: 3,
End: 1,
Turf: 7,
Dirt: 1,
Unique: 100021,
Skill1: 200432,
Skill2: 200552,
Skill3: 200712,
SkillPL2: 200022,
SkillPL3: 200431,
SkillPL4: 200542,
SkillPL5: 200551,
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong umas (+got/-want):\n%s", diff)
}
}
func TestConversations(t *testing.T) {
db := testdb(t.Context(), "file:TestConversations?mode=memory&cache=shared", characterSQL)
got, err := mdb.Conversations(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Conversation{
{
CharacterID: 1001,
Number: 1,
Location: 410,
Chara1: 1001,
},
{
CharacterID: 1001,
Number: 2,
Location: 510,
Chara1: 1001,
ConditionType: 1,
},
{
CharacterID: 1001,
Number: 3,
Location: 310,
Chara1: 1001,
ConditionType: 1,
},
{
CharacterID: 1001,
Number: 4,
Location: 120,
Chara1: 1001,
Chara2: 1002,
ConditionType: 2,
},
{
CharacterID: 1001,
Number: 5,
Location: 520,
Chara1: 1003,
Chara2: 1001,
ConditionType: 3,
},
{
CharacterID: 1001,
Number: 6,
Location: 430,
Chara1: 1001,
Chara2: 1014,
Chara3: 1011,
ConditionType: 1,
},
{
CharacterID: 1002,
Number: 1,
Location: 310,
Chara1: 1002,
},
{
CharacterID: 1002,
Number: 2,
Location: 210,
Chara1: 1002,
ConditionType: 1,
},
{
CharacterID: 1002,
Number: 3,
Location: 110,
Chara1: 1002,
ConditionType: 1,
},
{
CharacterID: 1002,
Number: 4,
Location: 520,
Chara1: 1010,
Chara2: 1002,
ConditionType: 3,
},
{
CharacterID: 1002,
Number: 5,
Location: 220,
Chara1: 1002,
Chara2: 1018,
ConditionType: 2,
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong conversations (+got/-want):\n%s", diff)
}
}
+33
View File
@@ -0,0 +1,33 @@
package mdb
import (
"context"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// load scans all results of sql and appends them to r.
func load[T any](ctx context.Context, db *sqlitex.Pool, r []T, sql string, row func(*sqlite.Stmt) T) ([]T, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, err
}
stmt, err := conn.Prepare(sql)
if err != nil {
return nil, err
}
for {
ok, err := stmt.Step()
if err != nil {
return r, err
}
if !ok {
break
}
r = append(r, row(stmt))
}
return r, err
}
+23
View File
@@ -0,0 +1,23 @@
package mdb_test
import (
"context"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
func testdb(ctx context.Context, uri, schema string) *sqlitex.Pool {
db, err := sqlitex.NewPool(uri, sqlitex.PoolOptions{Flags: sqlite.OpenCreate | sqlite.OpenReadWrite | sqlite.OpenMemory | sqlite.OpenSharedCache | sqlite.OpenURI})
if err != nil {
panic(err)
}
conn, err := db.Take(ctx)
if err != nil {
panic(err)
}
if err := sqlitex.ExecScript(conn, schema); err != nil {
panic(err)
}
return db
}
+61
View File
@@ -0,0 +1,61 @@
package mdb
import (
"context"
_ "embed"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
)
var (
//go:embed sql/race.sql
raceSQL string
//go:embed sql/saddle.sql
saddleSQL string
//go:embed sql/scenario.sql
scenarioSQL string
)
// Races retrieves all races.
func Races(ctx context.Context, db *sqlitex.Pool) ([]horse.Race, error) {
return load(ctx, db, nil, raceSQL, func(s *sqlite.Stmt) horse.Race {
return horse.Race{
ID: horse.RaceID(s.ColumnInt(0)),
Name: s.ColumnText(1),
// TODO(zeph): grade
Thumbnail: s.ColumnInt(3),
Primary: horse.RaceID(s.ColumnInt(4)),
}
})
}
// Saddles retrieves all saddles.
func Saddles(ctx context.Context, db *sqlitex.Pool) ([]horse.Saddle, error) {
return load(ctx, db, nil, saddleSQL, func(s *sqlite.Stmt) horse.Saddle {
return horse.Saddle{
ID: horse.SaddleID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Races: trimZeros(
horse.RaceID(s.ColumnInt(2)),
horse.RaceID(s.ColumnInt(3)),
horse.RaceID(s.ColumnInt(4)),
),
Type: horse.SaddleType(s.ColumnInt(5)),
Primary: horse.SaddleID(s.ColumnInt(6)),
}
})
}
// Scenarios retrieves all scenarios.
func Scenarios(ctx context.Context, db *sqlitex.Pool) ([]horse.Scenario, error) {
return load(ctx, db, nil, scenarioSQL, func(s *sqlite.Stmt) horse.Scenario {
return horse.Scenario{
ID: horse.ScenarioID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Title: s.ColumnText(2),
}
})
}
+230
View File
@@ -0,0 +1,230 @@
package mdb_test
import (
_ "embed"
"testing"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/mdb"
"github.com/google/go-cmp/cmp"
)
//go:embed testdata/race.sql
var raceSQL string
func TestRaces(t *testing.T) {
db := testdb(t.Context(), "file:TestRaces?mode=memory&cache=shared", raceSQL)
got, err := mdb.Races(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Race{
{
ID: 1005,
Name: "Satsuki Sho",
Thumbnail: 1005,
Primary: 1005,
},
{
ID: 1010,
Name: "Tokyo Yushun (Japanese Derby)",
Thumbnail: 1010,
Primary: 1010,
},
{
ID: 1015,
Name: "Kikuka Sho",
Thumbnail: 1015,
Primary: 1015,
},
{
ID: 1026,
Name: "Kikuka Sho",
Thumbnail: 1015,
Primary: 1015,
},
{
ID: 1028,
Name: "Satsuki Sho",
Thumbnail: 1028,
Primary: 1005,
},
{
ID: 1101,
Name: "Teio Sho",
Thumbnail: 1101,
Primary: 1101,
},
{
ID: 2001,
Name: "Nikkei Shinshun Hai",
Thumbnail: 2001,
Primary: 2001,
},
{
ID: 2010,
Name: "Spring Stakes",
Thumbnail: 2010,
Primary: 2010,
},
{
ID: 2035,
Name: "Spring Stakes",
Thumbnail: 2010,
Primary: 2010,
},
{
ID: 3001,
Name: "Kyoto Kimpai",
Thumbnail: 3001,
Primary: 3001,
},
{
ID: 4001,
Name: "Manyo Stakes",
Thumbnail: 4001,
Primary: 4001,
},
{
ID: 4501,
Name: "Aster Sho",
Thumbnail: 4501,
Primary: 4501,
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong races (+got/-want):\n%s", diff)
}
}
func TestSaddles(t *testing.T) {
db := testdb(t.Context(), "file:TestSaddles?mode=memory&cache=shared", raceSQL)
got, err := mdb.Saddles(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Saddle{
{
ID: 1,
Name: "Classic Triple Crown",
Races: []horse.RaceID{100501, 101001, 101501},
Type: 0,
Primary: 1,
},
{
ID: 12,
Name: "Japanese Derby",
Races: []horse.RaceID{101001},
Type: 3,
Primary: 12,
},
{
ID: 16,
Name: "Kikuka Sho",
Races: []horse.RaceID{101501},
Type: 3,
Primary: 16,
},
{
ID: 18,
Name: "Satsuki Sho",
Races: []horse.RaceID{100501},
Type: 3,
Primary: 18,
},
{
ID: 36,
Name: "Teio Sho",
Races: []horse.RaceID{110101},
Type: 3,
Primary: 36,
},
{
ID: 40,
Name: "Nikkei Shinshun Hai",
Races: []horse.RaceID{200101},
Type: 2,
Primary: 40,
},
{
ID: 49,
Name: "Spring S.",
Races: []horse.RaceID{201001},
Type: 2,
Primary: 49,
},
{
ID: 74,
Name: "Kyoto Kimpai",
Races: []horse.RaceID{300101},
Type: 1,
Primary: 74,
},
{
ID: 144,
Name: "Classic Triple Crown",
Races: []horse.RaceID{100501, 101001, 102601},
Type: 0,
Primary: 1,
},
{
ID: 148,
Name: "Kikuka Sho",
Races: []horse.RaceID{102601},
Type: 3,
Primary: 16,
},
{
ID: 149,
Name: "Spring S.",
Races: []horse.RaceID{203501},
Type: 2,
Primary: 49,
},
{
ID: 154,
Name: "Classic Triple Crown",
Races: []horse.RaceID{102801, 101001, 101501},
Type: 0,
Primary: 1,
},
{
ID: 155,
Name: "Satsuki Sho",
Races: []horse.RaceID{102801},
Type: 3,
Primary: 18,
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong saddles (+got/-want):\n%s", diff)
}
}
func TestScenarios(t *testing.T) {
db := testdb(t.Context(), "file:TestScenarios?mode=memory&cache=shared", raceSQL)
got, err := mdb.Scenarios(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Scenario{
{
ID: 1,
Name: "URA Finale",
Title: "The Beginning: URA Finale",
},
{
ID: 2,
Name: "Unity Cup",
Title: "Unity Cup: Shine On, Team Spirit!",
},
{
ID: 4,
Name: "TS Climax",
Title: "Trackblazer: Start of the Climax",
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong scenarios (+got/-want):\n%s", diff)
}
}
+152
View File
@@ -0,0 +1,152 @@
package mdb
import (
"context"
_ "embed"
"fmt"
"strconv"
"strings"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
)
var (
//go:embed sql/skill-group.sql
skillGroupSQL string
//go:embed sql/skill.sql
skillSQL string
)
// SkillGroups retrieves all skill groups.
func SkillGroups(ctx context.Context, db *sqlitex.Pool) ([]horse.SkillGroup, error) {
return load(ctx, db, nil, skillGroupSQL, func(s *sqlite.Stmt) horse.SkillGroup {
return horse.SkillGroup{
ID: horse.SkillGroupID(s.ColumnInt(0)),
Skill1: horse.SkillID(s.ColumnInt(1)),
Skill2: horse.SkillID(s.ColumnInt(2)),
Skill3: horse.SkillID(s.ColumnInt(3)),
SkillBad: horse.SkillID(s.ColumnInt(4)),
}
})
}
// Skills retrieves all skills.
func Skills(ctx context.Context, db *sqlitex.Pool) ([]horse.Skill, error) {
return load(ctx, db, nil, skillSQL, func(s *sqlite.Stmt) horse.Skill {
return horse.Skill{
ID: horse.SkillID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SkillGroupID(s.ColumnInt32(3)),
Rarity: int8(s.ColumnInt(5)),
GroupRate: int8(s.ColumnInt(6)),
GradeValue: s.ColumnInt32(7),
WitCheck: s.ColumnBool(8),
Activations: trimActivations([]horse.Activation{
{
Precondition: s.ColumnText(9),
Condition: s.ColumnText(10),
Duration: horse.TenThousandths(s.ColumnInt(11)),
DurScale: horse.DurScale(s.ColumnInt(12)),
Cooldown: horse.TenThousandths(s.ColumnInt(13)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(14)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(15)),
Value: horse.TenThousandths(s.ColumnInt(16)),
Target: horse.AbilityTarget(s.ColumnInt(17)),
TargetValue: s.ColumnInt32(18),
},
{
Type: horse.AbilityType(s.ColumnInt(19)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(20)),
Value: horse.TenThousandths(s.ColumnInt(21)),
Target: horse.AbilityTarget(s.ColumnInt(22)),
TargetValue: s.ColumnInt32(23),
},
{
Type: horse.AbilityType(s.ColumnInt(24)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(25)),
Value: horse.TenThousandths(s.ColumnInt(26)),
Target: horse.AbilityTarget(s.ColumnInt(27)),
TargetValue: s.ColumnInt32(28),
},
}),
},
{
Precondition: s.ColumnText(29),
Condition: s.ColumnText(30),
Duration: horse.TenThousandths(s.ColumnInt(31)),
DurScale: horse.DurScale(s.ColumnInt(32)),
Cooldown: horse.TenThousandths(s.ColumnInt(33)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(34)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(35)),
Value: horse.TenThousandths(s.ColumnInt(36)),
Target: horse.AbilityTarget(s.ColumnInt(37)),
TargetValue: s.ColumnInt32(38),
},
{
Type: horse.AbilityType(s.ColumnInt(39)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(40)),
Value: horse.TenThousandths(s.ColumnInt(41)),
Target: horse.AbilityTarget(s.ColumnInt(42)),
TargetValue: s.ColumnInt32(43),
},
{
Type: horse.AbilityType(s.ColumnInt(44)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(45)),
Value: horse.TenThousandths(s.ColumnInt(46)),
Target: horse.AbilityTarget(s.ColumnInt(47)),
TargetValue: s.ColumnInt32(48),
},
}),
},
}),
UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name
Tags: parseTags(s.ColumnText(54)),
SPCost: s.ColumnInt(49),
IconID: s.ColumnInt(53),
}
})
}
func parseTags(s string) []uint16 {
r := make([]uint16, 0, 8)
for s != "" {
t, u, _ := strings.Cut(s, "/")
s = u
v, err := strconv.ParseUint(t, 10, 16)
if err != nil {
panic(fmt.Errorf("parsing skill tags: %w", err))
}
r = append(r, uint16(v))
}
return trimZeros(r...)
}
func trimAbilities(s []horse.Ability) []horse.Ability {
for len(s) > 0 && s[len(s)-1].Type == 0 {
s = s[:len(s)-1]
}
return s
}
func trimActivations(s []horse.Activation) []horse.Activation {
for len(s) > 0 && s[len(s)-1].Condition == "" {
s = s[:len(s)-1]
}
return s
}
func trimZeros[T comparable](s ...T) []T {
var zero T
for len(s) > 0 && s[len(s)-1] == zero {
s = s[:len(s)-1]
}
return s
}
+701
View File
@@ -0,0 +1,701 @@
package mdb_test
import (
_ "embed"
"testing"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/mdb"
"github.com/google/go-cmp/cmp"
)
//go:embed testdata/skill.sql
var skillSQL string
func TestSkillGroups(t *testing.T) {
db := testdb(t.Context(), "file:TestSkillGroups?mode=memory&cache=shared", skillSQL)
got, err := mdb.SkillGroups(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.SkillGroup{
{ID: 1035, Skill1: 10351},
{ID: 10001, Skill1: 100011, Skill2: 900011},
{ID: 10035, Skill1: 100351, Skill2: 900351},
{ID: 11024, Skill1: 110241, Skill2: 910241},
{ID: 20001, Skill1: 200012, Skill2: 200011, Skill3: 200014, SkillBad: 200013},
{ID: 20002, Skill1: 200022, Skill2: 200021, SkillBad: 200023},
{ID: 20036, Skill1: 200362, Skill2: 200361},
{ID: 20083, Skill1: 200831},
{ID: 20180, SkillBad: 201801},
{ID: 30001, Skill1: 300011},
{ID: 90001, Skill1: 100011, Skill2: 900011},
{ID: 90035, Skill1: 100351, Skill2: 900351},
{ID: 91024, Skill1: 110241, Skill2: 910241},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong skill groups (+got/-want):\n%s", diff)
}
}
func TestSkills(t *testing.T) {
db := testdb(t.Context(), "file:TestSkills?mode=memory&cache=shared", skillSQL)
got, err := mdb.Skills(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Skill{
{
ID: 10351,
Name: "V Is for Victory!",
Description: "Moderately increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.",
Group: 1035,
Rarity: 3,
GroupRate: 1,
GradeValue: 240,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "is_finalcorner==1&blocked_side_continuetime>=2",
Condition: "is_finalcorner==1&corner==0&order<=5",
Duration: 50000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 2500,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Get to Winning!] Winning Ticket",
Tags: []uint16{401},
SPCost: 0,
IconID: 20013,
},
{
ID: 100011,
Name: "Shooting Star",
Description: "Ride the momentum to increase velocity and very slightly increase acceleration after passing another runner toward the front late-race.",
Group: 10001,
Rarity: 5,
GroupRate: 1,
GradeValue: 340,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "phase>=2&order>=1&order_rate<=50&change_order_onetime<0",
Duration: 50000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 3500,
Target: 1,
TargetValue: 0,
},
{
Type: 31,
ValueUsage: 1,
Value: 1000,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Special Dreamer] Special Week",
Tags: []uint16{401, 403},
SPCost: 0,
IconID: 20013,
},
{
ID: 100351,
Name: "Our Ticket to Win!",
Description: "Increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.",
Group: 10035,
Rarity: 4,
GroupRate: 1,
GradeValue: 340,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "is_finalcorner==1&blocked_side_continuetime>=2",
Condition: "is_finalcorner==1&corner==0&order<=5",
Duration: 50000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 3500,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Get to Winning!] Winning Ticket",
Tags: []uint16{401},
SPCost: 0,
IconID: 20013,
},
{
ID: 110241,
Name: "Flowery☆Maneuver",
Description: "Increase velocity when passing another runner toward the front on the final corner. If passing toward the back, increase acceleration instead.",
Group: 11024,
Rarity: 5,
GroupRate: 1,
GradeValue: 340,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "is_finalcorner==1&corner!=0&order_rate<=40&change_order_onetime<0",
Duration: 50000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 3500,
Target: 1,
TargetValue: 0,
},
},
},
{
Precondition: "",
Condition: "is_finalcorner==1&corner!=0&order_rate>=50&order_rate<=80&change_order_onetime<0",
Duration: 40000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 31,
ValueUsage: 1,
Value: 4000,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Sunlight Bouquet] Mayano Top Gun",
Tags: []uint16{401, 403},
SPCost: 0,
IconID: 20013,
},
{
ID: 200011,
Name: "Right-Handed ◎",
Description: "Increase performance on right-handed tracks.",
Group: 20001,
Rarity: 1,
GroupRate: 2,
GradeValue: 174,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==1",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: 600000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 110,
IconID: 10011,
},
{
ID: 200012,
Name: "Right-Handed ○",
Description: "Moderately increase performance on right-handed tracks.",
Group: 20001,
Rarity: 1,
GroupRate: 1,
GradeValue: 129,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==1",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: 400000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 90,
IconID: 10011,
},
{
ID: 200013,
Name: "Right-Handed ×",
Description: "Moderately decrease performance on right-handed tracks.",
Group: 20001,
Rarity: 1,
GroupRate: -1,
GradeValue: -129,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==1",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: -400000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 50,
IconID: 10014,
},
{
ID: 200014,
Name: "Right-Handed Demon",
Description: "Increase proficiency in right-handed tracks, increasing Speed and Power.",
Group: 20001,
Rarity: 2,
GroupRate: 3,
GradeValue: 461,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==1",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: 600000,
Target: 1,
TargetValue: 0,
},
{
Type: 3,
ValueUsage: 1,
Value: 600000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401, 403},
SPCost: 130,
IconID: 10012,
},
{
ID: 200021,
Name: "Left-Handed ◎",
Description: "Increase performance on left-handed tracks.",
Group: 20002,
Rarity: 1,
GroupRate: 2,
GradeValue: 174,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==2",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: 600000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 110,
IconID: 10011,
},
{
ID: 200022,
Name: "Left-Handed ○",
Description: "Moderately increase performance on left-handed tracks.",
Group: 20002,
Rarity: 1,
GroupRate: 1,
GradeValue: 129,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==2",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: 400000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 90,
IconID: 10011,
},
{
ID: 200023,
Name: "Left-Handed ×",
Description: "Moderately decrease performance on left-handed tracks.",
Group: 20002,
Rarity: 1,
GroupRate: -1,
GradeValue: -129,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "rotation==2",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: -400000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 50,
IconID: 10014,
},
{
ID: 200361,
Name: "Beeline Burst",
Description: "Increase velocity on a straight.",
Group: 20036,
Rarity: 2,
GroupRate: 2,
GradeValue: 508,
WitCheck: true,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "straight_random==1",
Duration: 24000,
DurScale: 1,
Cooldown: 300000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 3500,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 170,
IconID: 20012,
},
{
ID: 200362,
Name: "Straightaway Adept",
Description: "Slightly increase velocity on a straight.",
Group: 20036,
Rarity: 1,
GroupRate: 1,
GradeValue: 217,
WitCheck: true,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "straight_random==1",
Duration: 24000,
DurScale: 1,
Cooldown: 300000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 1500,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 170,
IconID: 20011,
},
{
ID: 200831,
Name: "Subdued Front Runners",
Description: "Slightly increase fatigue for front runners early-race.",
Group: 20083,
Rarity: 1,
GroupRate: 1,
GradeValue: 217,
WitCheck: true,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "running_style_count_nige_otherself>=1&phase_random==0&accumulatetime>=5",
Duration: 0,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 9,
ValueUsage: 1,
Value: -100,
Target: 18,
TargetValue: 1,
},
},
},
},
Tags: []uint16{301, 406},
SPCost: 130,
IconID: 30051,
},
{
ID: 201801,
Name: "♡ 3D Nail Art",
Description: "Moderately decrease performance on firm ground.",
Group: 20180,
Rarity: 1,
GroupRate: -1,
GradeValue: -129,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "ground_condition==1",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 1,
ValueUsage: 1,
Value: -400000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{401},
SPCost: 50,
IconID: 10014,
},
{
ID: 300011,
Name: "Unquenched Thirst",
Description: "Moderately increase performance with the desire to race.",
Group: 30001,
Rarity: 1,
GroupRate: 1,
GradeValue: 0,
WitCheck: false,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "track_id==10008",
Duration: -1,
DurScale: 1,
Cooldown: 0,
Abilities: []horse.Ability{
{
Type: 2,
ValueUsage: 1,
Value: 400000,
Target: 1,
TargetValue: 0,
},
},
},
},
Tags: []uint16{402},
SPCost: 0,
IconID: 10021,
},
{
ID: 900011,
Name: "Shooting Star",
Description: "Slightly increase velocity and very minimally increase acceleration after passing another runner toward the front late-race.",
Group: 10001,
Rarity: 1,
GroupRate: 2,
GradeValue: 180,
WitCheck: true,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "phase>=2&order>=1&order_rate<=50&change_order_onetime<0",
Duration: 30000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 1500,
Target: 1,
TargetValue: 0,
},
{
Type: 31,
ValueUsage: 1,
Value: 500,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Special Dreamer] Special Week",
Tags: []uint16{401, 403},
SPCost: 200,
IconID: 20011,
},
{
ID: 900351,
Name: "Our Ticket to Win!",
Description: "Slightly increase velocity when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.",
Group: 10035,
Rarity: 1,
GroupRate: 2,
GradeValue: 180,
WitCheck: true,
Activations: []horse.Activation{
{
Precondition: "is_finalcorner==1&blocked_side_continuetime>=2",
Condition: "is_finalcorner==1&corner==0&order<=5",
Duration: 30000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 1500,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Get to Winning!] Winning Ticket",
Tags: []uint16{401},
SPCost: 200,
IconID: 20011,
},
{
ID: 910241,
Name: "Flowery☆Maneuver",
Description: "Slightly increase velocity when passing another runner toward the front on the final corner. If passing toward the back, slightly increase acceleration instead.",
Group: 11024,
Rarity: 1,
GroupRate: 2,
GradeValue: 180,
WitCheck: true,
Activations: []horse.Activation{
{
Precondition: "",
Condition: "is_finalcorner==1&corner!=0&order_rate<=40&change_order_onetime<0",
Duration: 30000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 27,
ValueUsage: 1,
Value: 1500,
Target: 1,
TargetValue: 0,
},
},
},
{
Precondition: "",
Condition: "is_finalcorner==1&corner!=0&order_rate>=50&order_rate<=80&change_order_onetime<0",
Duration: 24000,
DurScale: 1,
Cooldown: 5000000,
Abilities: []horse.Ability{
{
Type: 31,
ValueUsage: 1,
Value: 2000,
Target: 1,
TargetValue: 0,
},
},
},
},
UniqueOwner: "[Sunlight Bouquet] Mayano Top Gun",
Tags: []uint16{401, 403},
SPCost: 200,
IconID: 20011,
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong skills (+got/-want):\n%s", diff)
}
}
+95
View File
@@ -0,0 +1,95 @@
package mdb
import (
"context"
_ "embed"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
)
var (
//go:embed sql/spark.sql
sparkSQL string
//go:embed sql/spark-effect.sql
sparkEffectSQL string
)
func Sparks(ctx context.Context, db *sqlitex.Pool) ([]horse.Spark, error) {
// We don't use func load here because we have multiple queries to run.
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, err
}
var sp []horse.Spark
{
stmt := conn.Prep(sparkSQL)
defer stmt.Reset()
for {
ok, err := stmt.Step()
if err != nil {
return nil, err
}
if !ok {
break
}
sp = append(sp, horse.Spark{
ID: horse.SparkID(stmt.ColumnInt(0)),
Name: stmt.ColumnText(1),
Description: stmt.ColumnText(2),
Group: horse.SparkGroupID(stmt.ColumnInt(3)),
Rarity: horse.SparkRarity(stmt.ColumnInt(4)),
Type: horse.SparkType(stmt.ColumnInt(5)),
// Effects filled in later, but we can start with space.
// The vast majority of sparks are skill sparks,
// which have five rolls.
Effects: make([][]horse.SparkEffect, 0, 5),
})
}
}
stmt := conn.Prep(sparkEffectSQL)
defer stmt.Reset()
cur := sp
last := 0
for {
ok, err := stmt.Step()
if err != nil {
return nil, err
}
if !ok {
break
}
// We sort sparks by group ID first, so we can add effects in a single pass.
group := stmt.ColumnInt(0)
for len(cur) > 0 && cur[0].Group != horse.SparkGroupID(group) {
cur = cur[1:]
last = 0 // reset whenever we change group IDs
}
effect := stmt.ColumnInt(1)
if effect != last {
// This effect is a separate roll from the previous one.
// Create a new slot for the effects to go into.
for i := range cur {
if cur[i].Group != horse.SparkGroupID(group) {
break
}
cur[i].Effects = append(cur[i].Effects, make([]horse.SparkEffect, 0, 1))
}
last = effect
}
for i := range cur {
if cur[i].Group != horse.SparkGroupID(group) {
break
}
e := horse.SparkEffect{
Target: horse.SparkTarget(stmt.ColumnInt(2)),
Value1: stmt.ColumnInt32(3),
Value2: stmt.ColumnInt32(4),
}
cur[i].Effects[len(cur[i].Effects)-1] = append(cur[i].Effects[len(cur[i].Effects)-1], e)
}
}
return sp, nil
}
+345
View File
@@ -0,0 +1,345 @@
package mdb_test
import (
_ "embed"
"testing"
"github.com/google/go-cmp/cmp"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/mdb"
)
//go:embed testdata/spark.sql
var sparkSQL string
func TestSparks(t *testing.T) {
db := testdb(t.Context(), "file:TestSparks?mode=memory&cache=shared", sparkSQL)
got, err := mdb.Sparks(t.Context(), db)
if err != nil {
t.Error(err)
}
want := []horse.Spark{
{
ID: 101,
Name: "Speed",
Description: "A Spark that increases Speed.",
Group: 1,
Rarity: 1,
Type: 1,
Effects: [][]horse.SparkEffect{
{{Target: 1, Value1: 1}},
{{Target: 1, Value1: 4}},
{{Target: 1, Value1: 7}},
{{Target: 1, Value1: 10}},
{{Target: 1, Value1: 13}},
{{Target: 1, Value1: 16}},
{{Target: 1, Value1: 19}},
{{Target: 1, Value1: 22}},
{{Target: 1, Value1: 25}},
{{Target: 1, Value1: 28}},
},
},
{
ID: 102,
Name: "Speed",
Description: "A Spark that increases Speed.",
Group: 1,
Rarity: 2,
Type: 1,
Effects: [][]horse.SparkEffect{
{{Target: 1, Value1: 1}},
{{Target: 1, Value1: 4}},
{{Target: 1, Value1: 7}},
{{Target: 1, Value1: 10}},
{{Target: 1, Value1: 13}},
{{Target: 1, Value1: 16}},
{{Target: 1, Value1: 19}},
{{Target: 1, Value1: 22}},
{{Target: 1, Value1: 25}},
{{Target: 1, Value1: 28}},
},
},
{
ID: 103,
Name: "Speed",
Description: "A Spark that increases Speed.",
Group: 1,
Rarity: 3,
Type: 1,
Effects: [][]horse.SparkEffect{
{{Target: 1, Value1: 1}},
{{Target: 1, Value1: 4}},
{{Target: 1, Value1: 7}},
{{Target: 1, Value1: 10}},
{{Target: 1, Value1: 13}},
{{Target: 1, Value1: 16}},
{{Target: 1, Value1: 19}},
{{Target: 1, Value1: 22}},
{{Target: 1, Value1: 25}},
{{Target: 1, Value1: 28}},
},
},
{
ID: 201,
Name: "Stamina",
Description: "A Spark that increases Stamina.",
Group: 2,
Rarity: 1,
Type: 1,
Effects: [][]horse.SparkEffect{
{{Target: 2, Value1: 1}},
{{Target: 2, Value1: 4}},
{{Target: 2, Value1: 7}},
{{Target: 2, Value1: 10}},
{{Target: 2, Value1: 13}},
{{Target: 2, Value1: 16}},
{{Target: 2, Value1: 19}},
{{Target: 2, Value1: 22}},
{{Target: 2, Value1: 25}},
{{Target: 2, Value1: 28}},
},
},
{
ID: 202,
Name: "Stamina",
Description: "A Spark that increases Stamina.",
Group: 2,
Rarity: 2,
Type: 1,
Effects: [][]horse.SparkEffect{
{{Target: 2, Value1: 1}},
{{Target: 2, Value1: 4}},
{{Target: 2, Value1: 7}},
{{Target: 2, Value1: 10}},
{{Target: 2, Value1: 13}},
{{Target: 2, Value1: 16}},
{{Target: 2, Value1: 19}},
{{Target: 2, Value1: 22}},
{{Target: 2, Value1: 25}},
{{Target: 2, Value1: 28}},
},
},
{
ID: 203,
Name: "Stamina",
Description: "A Spark that increases Stamina.",
Group: 2,
Rarity: 3,
Type: 1,
Effects: [][]horse.SparkEffect{
{{Target: 2, Value1: 1}},
{{Target: 2, Value1: 4}},
{{Target: 2, Value1: 7}},
{{Target: 2, Value1: 10}},
{{Target: 2, Value1: 13}},
{{Target: 2, Value1: 16}},
{{Target: 2, Value1: 19}},
{{Target: 2, Value1: 22}},
{{Target: 2, Value1: 25}},
{{Target: 2, Value1: 28}},
},
},
{
ID: 1101,
Name: "Turf",
Description: "A Spark that increases Turf Aptitude.",
Group: 11,
Rarity: 1,
Type: 2,
Effects: [][]horse.SparkEffect{
{{Target: 11, Value1: 1}},
{{Target: 11, Value1: 2}},
},
},
{
ID: 1102,
Name: "Turf",
Description: "A Spark that increases Turf Aptitude.",
Group: 11,
Rarity: 2,
Type: 2,
Effects: [][]horse.SparkEffect{
{{Target: 11, Value1: 1}},
{{Target: 11, Value1: 2}},
},
},
{
ID: 1103,
Name: "Turf",
Description: "A Spark that increases Turf Aptitude.",
Group: 11,
Rarity: 3,
Type: 2,
Effects: [][]horse.SparkEffect{
{{Target: 11, Value1: 1}},
{{Target: 11, Value1: 2}},
},
},
{
ID: 1000101,
Name: "February S.",
Description: `A Spark that increases Power and gives a skill hint for "Winter Runner ○".`,
Group: 10001,
Rarity: 1,
Type: 5,
Effects: [][]horse.SparkEffect{
{{Target: 3, Value1: 3}, {Target: 41, Value1: 200202, Value2: 1}},
{{Target: 3, Value1: 6}, {Target: 41, Value1: 200202, Value2: 1}},
{{Target: 3, Value1: 9}, {Target: 41, Value1: 200202, Value2: 1}},
},
},
{
ID: 1000102,
Name: "February S.",
Description: `A Spark that increases Power and gives a skill hint for "Winter Runner ○".`,
Group: 10001,
Rarity: 2,
Type: 5,
Effects: [][]horse.SparkEffect{
{{Target: 3, Value1: 3}, {Target: 41, Value1: 200202, Value2: 1}},
{{Target: 3, Value1: 6}, {Target: 41, Value1: 200202, Value2: 1}},
{{Target: 3, Value1: 9}, {Target: 41, Value1: 200202, Value2: 1}},
},
},
{
ID: 1000103,
Name: "February S.",
Description: `A Spark that increases Power and gives a skill hint for "Winter Runner ○".`,
Group: 10001,
Rarity: 3,
Type: 5,
Effects: [][]horse.SparkEffect{
{{Target: 3, Value1: 3}, {Target: 41, Value1: 200202, Value2: 1}},
{{Target: 3, Value1: 6}, {Target: 41, Value1: 200202, Value2: 1}},
{{Target: 3, Value1: 9}, {Target: 41, Value1: 200202, Value2: 1}},
},
},
{
ID: 2000101,
Name: "Right-Handed ○",
Description: `A Spark that gives a skill hint for "Right-Handed ○".`,
Group: 20001,
Rarity: 1,
Type: 4,
Effects: [][]horse.SparkEffect{
{{Target: 41, Value1: 200012, Value2: 1}},
{{Target: 41, Value1: 200012, Value2: 2}},
{{Target: 41, Value1: 200012, Value2: 3}},
{{Target: 41, Value1: 200012, Value2: 4}},
{{Target: 41, Value1: 200012, Value2: 5}},
},
},
{
ID: 2000102,
Name: "Right-Handed ○",
Description: `A Spark that gives a skill hint for "Right-Handed ○".`,
Group: 20001,
Rarity: 2,
Type: 4,
Effects: [][]horse.SparkEffect{
{{Target: 41, Value1: 200012, Value2: 1}},
{{Target: 41, Value1: 200012, Value2: 2}},
{{Target: 41, Value1: 200012, Value2: 3}},
{{Target: 41, Value1: 200012, Value2: 4}},
{{Target: 41, Value1: 200012, Value2: 5}},
},
},
{
ID: 2000103,
Name: "Right-Handed ○",
Description: `A Spark that gives a skill hint for "Right-Handed ○".`,
Group: 20001,
Rarity: 3,
Type: 4,
Effects: [][]horse.SparkEffect{
{{Target: 41, Value1: 200012, Value2: 1}},
{{Target: 41, Value1: 200012, Value2: 2}},
{{Target: 41, Value1: 200012, Value2: 3}},
{{Target: 41, Value1: 200012, Value2: 4}},
{{Target: 41, Value1: 200012, Value2: 5}},
},
},
{
ID: 3000101,
Name: "URA Finale",
Description: `A Spark that increases Speed and Stamina.`,
Group: 30001,
Rarity: 1,
Type: 6,
Effects: [][]horse.SparkEffect{
{{Target: 1, Value1: 10}, {Target: 2, Value1: 10}},
{{Target: 1, Value1: 20}, {Target: 2, Value1: 20}},
{{Target: 1, Value1: 30}, {Target: 2, Value1: 30}},
},
},
{
ID: 3000102,
Name: "URA Finale",
Description: `A Spark that increases Speed and Stamina.`,
Group: 30001,
Rarity: 2,
Type: 6,
Effects: [][]horse.SparkEffect{
{{Target: 1, Value1: 10}, {Target: 2, Value1: 10}},
{{Target: 1, Value1: 20}, {Target: 2, Value1: 20}},
{{Target: 1, Value1: 30}, {Target: 2, Value1: 30}},
},
},
{
ID: 3000103,
Name: "URA Finale",
Description: `A Spark that increases Speed and Stamina.`,
Group: 30001,
Rarity: 3,
Type: 6,
Effects: [][]horse.SparkEffect{
{{Target: 1, Value1: 10}, {Target: 2, Value1: 10}},
{{Target: 1, Value1: 20}, {Target: 2, Value1: 20}},
{{Target: 1, Value1: 30}, {Target: 2, Value1: 30}},
},
},
{
ID: 10010101,
Name: "Shooting Star",
Description: `A Spark that gives a skill hint for "Shooting Star".`,
Group: 100101,
Rarity: 1,
Type: 3,
Effects: [][]horse.SparkEffect{
{{Target: 41, Value1: 900011, Value2: 1}},
{{Target: 41, Value1: 900011, Value2: 2}},
{{Target: 41, Value1: 900011, Value2: 3}},
},
},
{
ID: 10010102,
Name: "Shooting Star",
Description: `A Spark that gives a skill hint for "Shooting Star".`,
Group: 100101,
Rarity: 2,
Type: 3,
Effects: [][]horse.SparkEffect{
{{Target: 41, Value1: 900011, Value2: 1}},
{{Target: 41, Value1: 900011, Value2: 2}},
{{Target: 41, Value1: 900011, Value2: 3}},
},
},
{
ID: 10010103,
Name: "Shooting Star",
Description: `A Spark that gives a skill hint for "Shooting Star".`,
Group: 100101,
Rarity: 3,
Type: 3,
Effects: [][]horse.SparkEffect{
{{Target: 41, Value1: 900011, Value2: 1}},
{{Target: 41, Value1: 900011, Value2: 2}},
{{Target: 41, Value1: 900011, Value2: 3}},
},
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong sparks (+got/-want):\n%s", diff)
}
}
+7
View File
@@ -0,0 +1,7 @@
SELECT
m.chara_id,
m.relation_type,
r.relation_point
FROM succession_relation_member m
JOIN succession_relation r ON m.relation_type = r.relation_type
ORDER BY m.chara_id, m.relation_type
@@ -14,4 +14,4 @@ SELECT
FROM single_mode_scenario sc FROM single_mode_scenario sc
JOIN scenario_name n ON sc.id = n.id JOIN scenario_name n ON sc.id = n.id
JOIN scenario_title t ON sc.id = t.id JOIN scenario_title t ON sc.id = t.id
ORDER BY sc.id ORDER BY sc.sort_id
@@ -5,5 +5,10 @@ SELECT
value_1, value_1,
value_2 value_2
FROM succession_factor_effect FROM succession_factor_effect
WHERE factor_group_id NOT IN (40001) -- exclude Carnival Bonus -- exclude Carnival Bonus
WHERE factor_group_id NOT IN (
SELECT factor_group_id
FROM succession_factor
WHERE factor_type = 7
)
ORDER BY factor_group_id, effect_id, id ORDER BY factor_group_id, effect_id, id
@@ -17,4 +17,4 @@ SELECT
FROM spark FROM spark
JOIN succession_factor sf ON spark.id = sf.factor_id JOIN succession_factor sf ON spark.id = sf.factor_id
WHERE sf.factor_type != 7 -- exclude Carnival Bonus WHERE sf.factor_type != 7 -- exclude Carnival Bonus
ORDER BY sf.factor_id ORDER BY sf.factor_group_id, sf.factor_id
+1 -1
View File
@@ -15,7 +15,7 @@ WITH uma_name AS (
uma.id, uma.id,
s.skill_id, s.skill_id,
s.need_rank, s.need_rank,
ROW_NUMBER() OVER (PARTITION BY s.available_skill_set_id, s.need_rank) AS idx ROW_NUMBER() OVER (PARTITION BY s.available_skill_set_id, s.need_rank ORDER BY s.id) AS idx
FROM card_data uma FROM card_data uma
LEFT JOIN available_skill_set s ON uma.available_skill_set_id = s.available_skill_set_id LEFT JOIN available_skill_set s ON uma.available_skill_set_id = s.available_skill_set_id
) )
+262
View File
@@ -0,0 +1,262 @@
CREATE TABLE IF NOT EXISTS 'text_data' ('id' INTEGER NOT NULL, 'category' INTEGER NOT NULL, 'index' INTEGER NOT NULL, 'text' TEXT NOT NULL, PRIMARY KEY('category','index'));
-- SELECT * FROM text_data WHERE category = 6 AND "index" IN (1001, 1002, 1078, 2001, 9001) OR category IN (4, 5) AND "index" IN (SELECT id FROM card_data WHERE chara_id IN (1001, 1002, 1078, 2001, 9001));
INSERT INTO text_data VALUES(6,6,1001,'Special Week');
INSERT INTO text_data VALUES(6,6,1002,'Silence Suzuka');
INSERT INTO text_data VALUES(6,6,1078,'Yamanin Zephyr');
INSERT INTO text_data VALUES(6,6,2001,'Happy Meek');
INSERT INTO text_data VALUES(6,6,9001,'Tazuna Hayakawa');
INSERT INTO text_data VALUES(4,4,100101,'[Special Dreamer] Special Week');
INSERT INTO text_data VALUES(4,4,100102,'[Hopp''n♪Happy Heart] Special Week');
INSERT INTO text_data VALUES(4,4,100201,'[Innocent Silence] Silence Suzuka');
INSERT INTO text_data VALUES(5,5,100101,'[Special Dreamer]');
INSERT INTO text_data VALUES(5,5,100102,'[Hopp''n♪Happy Heart]');
INSERT INTO text_data VALUES(5,5,100201,'[Innocent Silence]');
CREATE TABLE IF NOT EXISTS 'chara_data' ('id' INTEGER NOT NULL, 'birth_year' INTEGER NOT NULL, 'birth_month' INTEGER NOT NULL, 'birth_day' INTEGER NOT NULL, 'sex' INTEGER NOT NULL, 'image_color_main' TEXT NOT NULL, 'image_color_sub' TEXT NOT NULL, 'ui_color_main' TEXT NOT NULL, 'ui_color_sub' TEXT NOT NULL, 'ui_training_color_1' TEXT NOT NULL, 'ui_training_color_2' TEXT NOT NULL, 'ui_border_color' TEXT NOT NULL, 'ui_num_color_1' TEXT NOT NULL, 'ui_num_color_2' TEXT NOT NULL, 'ui_turn_color' TEXT NOT NULL, 'ui_wipe_color_1' TEXT NOT NULL, 'ui_wipe_color_2' TEXT NOT NULL, 'ui_wipe_color_3' TEXT NOT NULL, 'ui_speech_color_1' TEXT NOT NULL, 'ui_speech_color_2' TEXT NOT NULL, 'ui_nameplate_color_1' TEXT NOT NULL, 'ui_nameplate_color_2' TEXT NOT NULL, 'height' INTEGER NOT NULL, 'bust' INTEGER NOT NULL, 'scale' INTEGER NOT NULL, 'skin' INTEGER NOT NULL, 'shape' INTEGER NOT NULL, 'socks' INTEGER NOT NULL, 'personal_dress' INTEGER NOT NULL, 'tail_model_id' INTEGER NOT NULL, 'race_running_type' INTEGER NOT NULL, 'ear_random_time_min' INTEGER NOT NULL, 'ear_random_time_max' INTEGER NOT NULL, 'tail_random_time_min' INTEGER NOT NULL, 'tail_random_time_max' INTEGER NOT NULL, 'story_ear_random_time_min' INTEGER NOT NULL, 'story_ear_random_time_max' INTEGER NOT NULL, 'story_tail_random_time_min' INTEGER NOT NULL, 'story_tail_random_time_max' INTEGER NOT NULL, 'attachment_model_id' INTEGER NOT NULL, 'mini_mayu_shader_type' INTEGER NOT NULL, 'start_date' INTEGER NOT NULL, 'chara_category' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM chara_data WHERE id IN (1001, 1002, 1078, 2001, 9001);
INSERT INTO chara_data VALUES(1001,1995,5,2,1,'FCA7FF','FF58D9','EE6DCB','FFDEF9','FF7FDD','F759CD','F759CD','FF7FDD','FA50CD','FA50CD','FCCAEE','FCE3F5','FDEDFE','FF7FDD','FF7FDD','FF7CDC','FEB4EA',1,2,158,1,0,3,0,1,1,110,220,30,90,275,425,130,250,-1,0,1483228800,0);
INSERT INTO chara_data VALUES(1002,1994,5,1,1,'8FE78D','FBFF3B','29BD70','FFCE48','4BD18C','27B36B','27B36B','4BD18C','27B36B','27B36B','BDF2D6','D4F2E2','FFF5CC','4BD18C','4BD18C','43C883','82ECB5',1,0,161,1,1,6,0,1,1,170,270,180,310,275,425,180,310,-1,0,1483228800,0);
INSERT INTO chara_data VALUES(1078,1988,5,27,1,'A8C6FD','5277D6','da483a','ffda93','4C91F1','2B75DD','2B75DD','71ADFF','1667D9','1667D9','BCD5F5','D6E4F6','FFEBF6','4C91F1','4C91F1','4C91F1','8DBCFD',1,3,154,1,0,1,0,1,1,170,270,180,310,275,425,180,310,-1,0,2524608000,0);
INSERT INTO chara_data VALUES(2001,1971,3,15,1,'FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','68D25D','68D25D','66D713','25C4A4',2,2,163,0,0,3,0,2,1,170,270,180,310,275,425,180,310,-1,0,2524608000,0);
INSERT INTO chara_data VALUES(9001,1948,5,2,2,'44A705','FFFFFF','44A705','F3CD00','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','FFFFFF','68D25D','68D25D','66D713','25C4A4',2,2,166,1,2,7,0,-1,1,170,270,180,310,275,425,180,310,-1,0,2524608000,1);
CREATE TABLE IF NOT EXISTS 'card_data' ('id' INTEGER NOT NULL, 'chara_id' INTEGER NOT NULL, 'default_rarity' INTEGER NOT NULL, 'limited_chara' INTEGER NOT NULL, 'available_skill_set_id' INTEGER NOT NULL, 'talent_speed' INTEGER NOT NULL, 'talent_stamina' INTEGER NOT NULL, 'talent_pow' INTEGER NOT NULL, 'talent_guts' INTEGER NOT NULL, 'talent_wiz' INTEGER NOT NULL, 'talent_group_id' INTEGER NOT NULL, 'bg_id' INTEGER NOT NULL, 'get_piece_id' INTEGER NOT NULL, 'running_style' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM card_data WHERE chara_id IN (1001, 1002, 1078, 2001, 9001);
INSERT INTO card_data VALUES(100101,1001,3,0,100101,0,20,0,0,10,100101,8,100101,3);
INSERT INTO card_data VALUES(100102,1001,3,0,100102,0,10,10,10,0,100102,317,100102,3);
INSERT INTO card_data VALUES(100201,1002,3,0,100201,20,0,0,10,0,100201,30,100201,1);
CREATE TABLE IF NOT EXISTS 'card_rarity_data' ('id' INTEGER NOT NULL, 'card_id' INTEGER NOT NULL, 'rarity' INTEGER NOT NULL, 'race_dress_id' INTEGER NOT NULL, 'skill_set' INTEGER NOT NULL, 'speed' INTEGER NOT NULL, 'stamina' INTEGER NOT NULL, 'pow' INTEGER NOT NULL, 'guts' INTEGER NOT NULL, 'wiz' INTEGER NOT NULL, 'max_speed' INTEGER NOT NULL, 'max_stamina' INTEGER NOT NULL, 'max_pow' INTEGER NOT NULL, 'max_guts' INTEGER NOT NULL, 'max_wiz' INTEGER NOT NULL, 'proper_distance_short' INTEGER NOT NULL, 'proper_distance_mile' INTEGER NOT NULL, 'proper_distance_middle' INTEGER NOT NULL, 'proper_distance_long' INTEGER NOT NULL, 'proper_running_style_nige' INTEGER NOT NULL, 'proper_running_style_senko' INTEGER NOT NULL, 'proper_running_style_sashi' INTEGER NOT NULL, 'proper_running_style_oikomi' INTEGER NOT NULL, 'proper_ground_turf' INTEGER NOT NULL, 'proper_ground_dirt' INTEGER NOT NULL, 'get_dress_id_1' INTEGER NOT NULL, 'get_dress_id_2' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM card_rarity_data WHERE card_id IN (SELECT id FROM card_data WHERE chara_id IN (1001, 1002, 1078, 2001, 9001));
INSERT INTO card_rarity_data VALUES(10010103,100101,3,100101,10010103,83,88,98,90,91,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100101);
INSERT INTO card_rarity_data VALUES(10010104,100101,4,100101,10010104,92,98,109,100,101,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100101);
INSERT INTO card_rarity_data VALUES(10010105,100101,5,100101,10010105,102,108,120,110,110,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100101);
INSERT INTO card_rarity_data VALUES(10010203,100102,3,100130,11010103,77,90,103,98,82,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100130);
INSERT INTO card_rarity_data VALUES(10010204,100102,4,100130,11010104,86,100,114,109,91,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100130);
INSERT INTO card_rarity_data VALUES(10010205,100102,5,100130,11010105,94,110,125,119,102,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100130);
INSERT INTO card_rarity_data VALUES(10020103,100201,3,100201,10020103,101,84,77,100,88,1200,1200,1200,1200,1200,4,7,7,3,7,5,3,1,7,1,101,100201);
INSERT INTO card_rarity_data VALUES(10020104,100201,4,100201,10020104,112,93,85,111,99,1200,1200,1200,1200,1200,4,7,7,3,7,5,3,1,7,1,101,100201);
INSERT INTO card_rarity_data VALUES(10020105,100201,5,100201,10020105,124,102,94,122,108,1200,1200,1200,1200,1200,4,7,7,3,7,5,3,1,7,1,101,100201);
CREATE TABLE IF NOT EXISTS 'available_skill_set' ('id' INTEGER NOT NULL, 'available_skill_set_id' INTEGER NOT NULL, 'skill_id' INTEGER NOT NULL, 'need_rank' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM available_skill_set WHERE available_skill_set_id IN (SELECT available_skill_set_id FROM card_data WHERE chara_id IN (1001, 1002, 1078, 2001, 9001));
INSERT INTO available_skill_set VALUES(1,100101,200512,0);
INSERT INTO available_skill_set VALUES(2,100101,201352,0);
INSERT INTO available_skill_set VALUES(3,100101,200732,0);
INSERT INTO available_skill_set VALUES(11,100101,200162,2);
INSERT INTO available_skill_set VALUES(12,100101,201351,3);
INSERT INTO available_skill_set VALUES(13,100101,200612,4);
INSERT INTO available_skill_set VALUES(14,100101,200511,5);
INSERT INTO available_skill_set VALUES(715,100102,200462,0);
INSERT INTO available_skill_set VALUES(716,100102,200592,0);
INSERT INTO available_skill_set VALUES(717,100102,201132,0);
INSERT INTO available_skill_set VALUES(718,100102,200212,2);
INSERT INTO available_skill_set VALUES(719,100102,200591,3);
INSERT INTO available_skill_set VALUES(720,100102,201611,4);
INSERT INTO available_skill_set VALUES(721,100102,200461,5);
INSERT INTO available_skill_set VALUES(15,100201,200432,0);
INSERT INTO available_skill_set VALUES(16,100201,200552,0);
INSERT INTO available_skill_set VALUES(17,100201,200712,0);
INSERT INTO available_skill_set VALUES(25,100201,200022,2);
INSERT INTO available_skill_set VALUES(26,100201,200431,3);
INSERT INTO available_skill_set VALUES(27,100201,200542,4);
INSERT INTO available_skill_set VALUES(28,100201,200551,5);
CREATE TABLE IF NOT EXISTS 'skill_set' ('id' INTEGER NOT NULL, 'skill_id1' INTEGER NOT NULL, 'skill_level1' INTEGER NOT NULL, 'skill_id2' INTEGER NOT NULL, 'skill_level2' INTEGER NOT NULL, 'skill_id3' INTEGER NOT NULL, 'skill_level3' INTEGER NOT NULL, 'skill_id4' INTEGER NOT NULL, 'skill_level4' INTEGER NOT NULL, 'skill_id5' INTEGER NOT NULL, 'skill_level5' INTEGER NOT NULL, 'skill_id6' INTEGER NOT NULL, 'skill_level6' INTEGER NOT NULL, 'skill_id7' INTEGER NOT NULL, 'skill_level7' INTEGER NOT NULL, 'skill_id8' INTEGER NOT NULL, 'skill_level8' INTEGER NOT NULL, 'skill_id9' INTEGER NOT NULL, 'skill_level9' INTEGER NOT NULL, 'skill_id10' INTEGER NOT NULL, 'skill_level10' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM skill_set WHERE id IN (SELECT skill_set FROM card_rarity_data WHERE card_id IN (SELECT id FROM card_data WHERE chara_id IN (1001, 1002, 1078, 2001, 9001)));
INSERT INTO skill_set VALUES(10010103,100011,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10010104,100011,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10010105,100011,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10020103,100021,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10020104,100021,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10020105,100021,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(11010103,110011,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(11010104,110011,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(11010105,110011,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
CREATE TABLE IF NOT EXISTS 'succession_relation_member' ('id' INTEGER NOT NULL, 'relation_type' INTEGER NOT NULL, 'chara_id' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM succession_relation_member WHERE chara_id IN (1001, 1002, 1078, 2001, 9001);
INSERT INTO succession_relation_member VALUES(18,103,1001);
INSERT INTO succession_relation_member VALUES(80,202,1001);
INSERT INTO succession_relation_member VALUES(123,301,1001);
INSERT INTO succession_relation_member VALUES(169,401,1001);
INSERT INTO succession_relation_member VALUES(188,502,1001);
INSERT INTO succession_relation_member VALUES(208,2001,1001);
INSERT INTO succession_relation_member VALUES(220,2102,1001);
INSERT INTO succession_relation_member VALUES(235,2301,1001);
INSERT INTO succession_relation_member VALUES(241,2601,1001);
INSERT INTO succession_relation_member VALUES(308,7003,1001);
INSERT INTO succession_relation_member VALUES(310,7004,1001);
INSERT INTO succession_relation_member VALUES(312,7005,1001);
INSERT INTO succession_relation_member VALUES(314,7006,1001);
INSERT INTO succession_relation_member VALUES(316,7007,1001);
INSERT INTO succession_relation_member VALUES(397,2811,1001);
INSERT INTO succession_relation_member VALUES(448,2903,1001);
INSERT INTO succession_relation_member VALUES(509,3003,1001);
INSERT INTO succession_relation_member VALUES(555,3004,1001);
INSERT INTO succession_relation_member VALUES(570,3101,1001);
INSERT INTO succession_relation_member VALUES(662,2510,1001);
INSERT INTO succession_relation_member VALUES(674,2506,1001);
INSERT INTO succession_relation_member VALUES(686,2516,1001);
INSERT INTO succession_relation_member VALUES(758,2519,1001);
INSERT INTO succession_relation_member VALUES(825,3205,1001);
INSERT INTO succession_relation_member VALUES(835,8001,1001);
INSERT INTO succession_relation_member VALUES(837,8002,1001);
INSERT INTO succession_relation_member VALUES(839,8003,1001);
INSERT INTO succession_relation_member VALUES(841,8004,1001);
INSERT INTO succession_relation_member VALUES(843,8005,1001);
INSERT INTO succession_relation_member VALUES(845,8006,1001);
INSERT INTO succession_relation_member VALUES(847,8007,1001);
INSERT INTO succession_relation_member VALUES(849,8008,1001);
INSERT INTO succession_relation_member VALUES(851,8009,1001);
INSERT INTO succession_relation_member VALUES(853,8010,1001);
INSERT INTO succession_relation_member VALUES(855,8011,1001);
INSERT INTO succession_relation_member VALUES(857,8012,1001);
INSERT INTO succession_relation_member VALUES(859,8013,1001);
INSERT INTO succession_relation_member VALUES(861,8014,1001);
INSERT INTO succession_relation_member VALUES(863,8015,1001);
INSERT INTO succession_relation_member VALUES(865,8016,1001);
INSERT INTO succession_relation_member VALUES(1703,2406,1001);
INSERT INTO succession_relation_member VALUES(1715,2603,1001);
INSERT INTO succession_relation_member VALUES(1740,510,1001);
INSERT INTO succession_relation_member VALUES(1802,8443,1001);
INSERT INTO succession_relation_member VALUES(1876,8479,1001);
INSERT INTO succession_relation_member VALUES(1940,8511,1001);
INSERT INTO succession_relation_member VALUES(1990,8536,1001);
INSERT INTO succession_relation_member VALUES(2198,8640,1001);
INSERT INTO succession_relation_member VALUES(31,104,1002);
INSERT INTO succession_relation_member VALUES(81,202,1002);
INSERT INTO succession_relation_member VALUES(124,301,1002);
INSERT INTO succession_relation_member VALUES(174,402,1002);
INSERT INTO succession_relation_member VALUES(207,2001,1002);
INSERT INTO succession_relation_member VALUES(242,2601,1002);
INSERT INTO succession_relation_member VALUES(302,7000,1002);
INSERT INTO succession_relation_member VALUES(304,7001,1002);
INSERT INTO succession_relation_member VALUES(306,7002,1002);
INSERT INTO succession_relation_member VALUES(329,7013,1002);
INSERT INTO succession_relation_member VALUES(333,7015,1002);
INSERT INTO succession_relation_member VALUES(392,2810,1002);
INSERT INTO succession_relation_member VALUES(416,2901,1002);
INSERT INTO succession_relation_member VALUES(510,3003,1002);
INSERT INTO succession_relation_member VALUES(571,3101,1002);
INSERT INTO succession_relation_member VALUES(648,2512,1002);
INSERT INTO succession_relation_member VALUES(826,3205,1002);
INSERT INTO succession_relation_member VALUES(867,8017,1002);
INSERT INTO succession_relation_member VALUES(869,8018,1002);
INSERT INTO succession_relation_member VALUES(871,8019,1002);
INSERT INTO succession_relation_member VALUES(873,8020,1002);
INSERT INTO succession_relation_member VALUES(875,8021,1002);
INSERT INTO succession_relation_member VALUES(877,8022,1002);
INSERT INTO succession_relation_member VALUES(879,8023,1002);
INSERT INTO succession_relation_member VALUES(881,8024,1002);
INSERT INTO succession_relation_member VALUES(883,8025,1002);
INSERT INTO succession_relation_member VALUES(885,8026,1002);
INSERT INTO succession_relation_member VALUES(887,8027,1002);
INSERT INTO succession_relation_member VALUES(889,8028,1002);
INSERT INTO succession_relation_member VALUES(891,8029,1002);
INSERT INTO succession_relation_member VALUES(893,8030,1002);
INSERT INTO succession_relation_member VALUES(895,8031,1002);
INSERT INTO succession_relation_member VALUES(897,8032,1002);
INSERT INTO succession_relation_member VALUES(1704,2406,1002);
INSERT INTO succession_relation_member VALUES(1716,2603,1002);
INSERT INTO succession_relation_member VALUES(1804,8444,1002);
INSERT INTO succession_relation_member VALUES(1878,8480,1002);
INSERT INTO succession_relation_member VALUES(1942,8512,1002);
INSERT INTO succession_relation_member VALUES(1992,8537,1002);
INSERT INTO succession_relation_member VALUES(2200,8641,1002);
CREATE TABLE IF NOT EXISTS 'succession_relation' ('relation_type' INTEGER NOT NULL, 'relation_point' INTEGER NOT NULL, PRIMARY KEY('relation_type'));
-- SELECT * FROM succession_relation WHERE relation_type IN (SELECT relation_type FROM succession_relation_member WHERE chara_id IN (1001, 1002, 1078, 2001, 9001));
INSERT INTO succession_relation VALUES(103,2);
INSERT INTO succession_relation VALUES(104,2);
INSERT INTO succession_relation VALUES(202,2);
INSERT INTO succession_relation VALUES(301,2);
INSERT INTO succession_relation VALUES(401,2);
INSERT INTO succession_relation VALUES(402,2);
INSERT INTO succession_relation VALUES(502,2);
INSERT INTO succession_relation VALUES(510,2);
INSERT INTO succession_relation VALUES(2001,1);
INSERT INTO succession_relation VALUES(2102,1);
INSERT INTO succession_relation VALUES(2301,1);
INSERT INTO succession_relation VALUES(2406,1);
INSERT INTO succession_relation VALUES(2506,1);
INSERT INTO succession_relation VALUES(2510,1);
INSERT INTO succession_relation VALUES(2512,1);
INSERT INTO succession_relation VALUES(2516,1);
INSERT INTO succession_relation VALUES(2519,1);
INSERT INTO succession_relation VALUES(2601,1);
INSERT INTO succession_relation VALUES(2603,1);
INSERT INTO succession_relation VALUES(2810,1);
INSERT INTO succession_relation VALUES(2811,1);
INSERT INTO succession_relation VALUES(2901,7);
INSERT INTO succession_relation VALUES(2903,7);
INSERT INTO succession_relation VALUES(3003,7);
INSERT INTO succession_relation VALUES(3004,7);
INSERT INTO succession_relation VALUES(3101,7);
INSERT INTO succession_relation VALUES(3205,1);
INSERT INTO succession_relation VALUES(7000,1);
INSERT INTO succession_relation VALUES(7001,1);
INSERT INTO succession_relation VALUES(7002,1);
INSERT INTO succession_relation VALUES(7003,1);
INSERT INTO succession_relation VALUES(7004,1);
INSERT INTO succession_relation VALUES(7005,1);
INSERT INTO succession_relation VALUES(7006,1);
INSERT INTO succession_relation VALUES(7007,1);
INSERT INTO succession_relation VALUES(7013,1);
INSERT INTO succession_relation VALUES(7015,1);
INSERT INTO succession_relation VALUES(8001,1);
INSERT INTO succession_relation VALUES(8002,1);
INSERT INTO succession_relation VALUES(8003,1);
INSERT INTO succession_relation VALUES(8004,1);
INSERT INTO succession_relation VALUES(8005,1);
INSERT INTO succession_relation VALUES(8006,1);
INSERT INTO succession_relation VALUES(8007,1);
INSERT INTO succession_relation VALUES(8008,1);
INSERT INTO succession_relation VALUES(8009,1);
INSERT INTO succession_relation VALUES(8010,1);
INSERT INTO succession_relation VALUES(8011,1);
INSERT INTO succession_relation VALUES(8012,1);
INSERT INTO succession_relation VALUES(8013,1);
INSERT INTO succession_relation VALUES(8014,1);
INSERT INTO succession_relation VALUES(8015,1);
INSERT INTO succession_relation VALUES(8016,1);
INSERT INTO succession_relation VALUES(8017,1);
INSERT INTO succession_relation VALUES(8018,1);
INSERT INTO succession_relation VALUES(8019,1);
INSERT INTO succession_relation VALUES(8020,1);
INSERT INTO succession_relation VALUES(8021,1);
INSERT INTO succession_relation VALUES(8022,1);
INSERT INTO succession_relation VALUES(8023,1);
INSERT INTO succession_relation VALUES(8024,1);
INSERT INTO succession_relation VALUES(8025,1);
INSERT INTO succession_relation VALUES(8026,1);
INSERT INTO succession_relation VALUES(8027,1);
INSERT INTO succession_relation VALUES(8028,1);
INSERT INTO succession_relation VALUES(8029,1);
INSERT INTO succession_relation VALUES(8030,1);
INSERT INTO succession_relation VALUES(8031,1);
INSERT INTO succession_relation VALUES(8032,1);
INSERT INTO succession_relation VALUES(8443,1);
INSERT INTO succession_relation VALUES(8444,1);
INSERT INTO succession_relation VALUES(8479,1);
INSERT INTO succession_relation VALUES(8480,1);
INSERT INTO succession_relation VALUES(8511,1);
INSERT INTO succession_relation VALUES(8512,1);
INSERT INTO succession_relation VALUES(8536,1);
INSERT INTO succession_relation VALUES(8537,1);
INSERT INTO succession_relation VALUES(8640,1);
INSERT INTO succession_relation VALUES(8641,1);
CREATE TABLE IF NOT EXISTS 'home_story_trigger' ('id' INTEGER NOT NULL, 'pos_id' INTEGER NOT NULL, 'home_event_type' INTEGER NOT NULL, 'num' INTEGER NOT NULL, 'story_id' INTEGER NOT NULL, 'chara_id_1' INTEGER NOT NULL, 'chara_id_2' INTEGER NOT NULL, 'chara_id_3' INTEGER NOT NULL, 'condition_type' INTEGER NOT NULL, 'gallery_chara_id' INTEGER NOT NULL, 'disp_order' INTEGER NOT NULL, PRIMARY KEY('id'), UNIQUE('gallery_chara_id','disp_order'));
-- SELECT * FROM home_story_trigger WHERE gallery_chara_id IN (1001, 1002, 1078, 2001, 9001);
INSERT INTO home_story_trigger VALUES(1,410,0,1,1001001,1001,0,0,0,1001,1);
INSERT INTO home_story_trigger VALUES(2,510,0,1,1001002,1001,0,0,1,1001,2);
INSERT INTO home_story_trigger VALUES(3,310,0,1,1001003,1001,0,0,1,1001,3);
INSERT INTO home_story_trigger VALUES(76,120,0,2,1001001,1001,1002,0,2,1001,4);
INSERT INTO home_story_trigger VALUES(77,520,0,2,1001002,1003,1001,0,3,1001,5);
INSERT INTO home_story_trigger VALUES(126,430,0,3,1,1001,1014,1011,1,1001,6);
INSERT INTO home_story_trigger VALUES(4,310,0,1,1002001,1002,0,0,0,1002,1);
INSERT INTO home_story_trigger VALUES(5,210,0,1,1002002,1002,0,0,1,1002,2);
INSERT INTO home_story_trigger VALUES(6,110,0,1,1002003,1002,0,0,1,1002,3);
INSERT INTO home_story_trigger VALUES(78,520,0,2,1002001,1010,1002,0,3,1002,4);
INSERT INTO home_story_trigger VALUES(79,220,0,2,1002002,1002,1018,0,2,1002,5);
+86
View File
@@ -0,0 +1,86 @@
CREATE TABLE IF NOT EXISTS 'text_data' ('id' INTEGER NOT NULL, 'category' INTEGER NOT NULL, 'index' INTEGER NOT NULL, 'text' TEXT NOT NULL, PRIMARY KEY('category','index'));
-- SELECT * FROM text_data WHERE category = 33 AND "index" IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501) OR category = 111 AND "index" IN (SELECT id FROM single_mode_wins_saddle WHERE race_instance_id_1 IN (SELECT id FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501)) UNION SELECT id FROM single_mode_wins_saddle WHERE race_instance_id_2 IN (SELECT id FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501)) UNION SELECT id FROM single_mode_wins_saddle WHERE race_instance_id_3 IN (SELECT id FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501))) OR category IN (237, 119) AND "index" IN (1, 2, 4);
INSERT INTO text_data VALUES(33,33,1005,'Satsuki Sho');
INSERT INTO text_data VALUES(33,33,1010,'Tokyo Yushun (Japanese Derby)');
INSERT INTO text_data VALUES(33,33,1015,'Kikuka Sho');
INSERT INTO text_data VALUES(33,33,1026,'Kikuka Sho');
INSERT INTO text_data VALUES(33,33,1028,'Satsuki Sho');
INSERT INTO text_data VALUES(33,33,1101,'Teio Sho');
INSERT INTO text_data VALUES(33,33,2001,'Nikkei Shinshun Hai');
INSERT INTO text_data VALUES(33,33,2010,'Spring Stakes');
INSERT INTO text_data VALUES(33,33,2035,'Spring Stakes');
INSERT INTO text_data VALUES(33,33,3001,'Kyoto Kimpai');
INSERT INTO text_data VALUES(33,33,4001,'Manyo Stakes');
INSERT INTO text_data VALUES(33,33,4501,'Aster Sho');
INSERT INTO text_data VALUES(111,111,1,'Classic Triple Crown');
INSERT INTO text_data VALUES(111,111,12,'Japanese Derby');
INSERT INTO text_data VALUES(111,111,16,'Kikuka Sho');
INSERT INTO text_data VALUES(111,111,18,'Satsuki Sho');
INSERT INTO text_data VALUES(111,111,36,'Teio Sho');
INSERT INTO text_data VALUES(111,111,40,'Nikkei Shinshun Hai');
INSERT INTO text_data VALUES(111,111,49,'Spring S.');
INSERT INTO text_data VALUES(111,111,74,'Kyoto Kimpai');
INSERT INTO text_data VALUES(111,111,144,'Classic Triple Crown');
INSERT INTO text_data VALUES(111,111,148,'Kikuka Sho');
INSERT INTO text_data VALUES(111,111,149,'Spring S.');
INSERT INTO text_data VALUES(111,111,154,'Classic Triple Crown');
INSERT INTO text_data VALUES(111,111,155,'Satsuki Sho');
INSERT INTO text_data VALUES(119,119,1,'The Beginning: URA Finale');
INSERT INTO text_data VALUES(119,119,2,'Unity Cup: Shine On, Team Spirit!');
INSERT INTO text_data VALUES(119,119,4,'Trackblazer: Start of the Climax');
INSERT INTO text_data VALUES(237,237,1,'URA Finale');
INSERT INTO text_data VALUES(237,237,2,'Unity Cup');
INSERT INTO text_data VALUES(237,237,4,'TS Climax');
CREATE TABLE IF NOT EXISTS 'race' ('id' INTEGER NOT NULL, 'group' INTEGER NOT NULL, 'grade' INTEGER NOT NULL, 'course_set' INTEGER NOT NULL, 'thumbnail_id' INTEGER NOT NULL, 'ff_cue_name' TEXT NOT NULL, 'ff_cuesheet_name' TEXT NOT NULL, 'ff_anim' INTEGER NOT NULL, 'ff_camera' INTEGER NOT NULL, 'ff_camera_sub' INTEGER NOT NULL, 'ff_sub' INTEGER NOT NULL, 'goal_gate' INTEGER NOT NULL, 'goal_flower' INTEGER NOT NULL, 'audience' INTEGER NOT NULL, 'entry_num' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM race WHERE id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501);
INSERT INTO race VALUES(1005,1,100,10504,1005,'snd_mfx_1001_CL','snd_mfx_1001_CL',1005,101,0,0,1,1,0,18);
INSERT INTO race VALUES(1010,1,100,10606,1010,'snd_mfx_1001_CL','snd_mfx_1001_CL',1010,101,0,0,5,5,0,18);
INSERT INTO race VALUES(1015,1,100,10810,1015,'snd_mfx_1002_CL','snd_mfx_1002_CL',1015,102,0,0,3,3,0,18);
INSERT INTO race VALUES(1026,1,100,10810,1015,'snd_mfx_1002_CL','snd_mfx_1002_CL',1015,102,0,0,3,3,0,17);
INSERT INTO race VALUES(1028,1,100,10604,1028,'snd_mfx_1001_CL','snd_mfx_1001_CL',1028,101,0,0,9,0,0,18);
INSERT INTO race VALUES(1101,1,100,11103,1101,'snd_mfx_1015_CL','snd_mfx_1015_CL',1101,313,0,0,1,0,0,16);
INSERT INTO race VALUES(2001,1,200,10809,2001,'snd_mfx_1008_CL','snd_mfx_1008_CL',2001,208,0,0,0,0,0,18);
INSERT INTO race VALUES(2010,1,200,10503,2010,'snd_mfx_1005_CL','snd_mfx_1005_CL',2010,205,0,0,0,0,0,16);
INSERT INTO race VALUES(2035,1,200,10503,2010,'snd_mfx_1005_CL','snd_mfx_1005_CL',2010,205,0,0,0,0,0,5);
INSERT INTO race VALUES(3001,1,300,10805,3001,'snd_mfx_1008_CL','snd_mfx_1008_CL',3001,208,0,0,0,0,0,18);
INSERT INTO race VALUES(4001,1,400,10810,4001,'snd_mfx_1013_CL','snd_mfx_1013_CL',4001,313,0,0,0,0,0,18);
INSERT INTO race VALUES(4501,1,700,10502,4501,'snd_mfx_1010_CL','snd_mfx_1010_CL',4501,310,0,0,0,0,0,16);
CREATE TABLE IF NOT EXISTS 'race_instance' ('id' INTEGER NOT NULL, 'race_id' INTEGER NOT NULL, 'npc_group_id' INTEGER NOT NULL, 'date' INTEGER NOT NULL, 'time' INTEGER NOT NULL, 'clock_time' INTEGER NOT NULL, 'race_number' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501);
INSERT INTO race_instance VALUES(100501,1005,199,414,2,0,11);
INSERT INTO race_instance VALUES(101001,1010,199,526,2,0,11);
INSERT INTO race_instance VALUES(701301,1010,0,0,2,0,11);
INSERT INTO race_instance VALUES(101501,1015,199,1020,2,0,11);
INSERT INTO race_instance VALUES(102601,1026,199,1020,2,0,11);
INSERT INTO race_instance VALUES(102801,1028,199,414,2,0,11);
INSERT INTO race_instance VALUES(110101,1101,303,626,4,0,11);
INSERT INTO race_instance VALUES(200101,2001,103,117,2,0,11);
INSERT INTO race_instance VALUES(201001,2010,102,317,2,0,11);
INSERT INTO race_instance VALUES(203501,2035,102,317,2,0,11);
INSERT INTO race_instance VALUES(300101,3001,102,105,2,0,11);
INSERT INTO race_instance VALUES(400101,4001,104,105,2,0,10);
INSERT INTO race_instance VALUES(450101,4501,102,907,2,0,9);
CREATE TABLE IF NOT EXISTS 'single_mode_wins_saddle' ('id' INTEGER NOT NULL, 'priority' INTEGER NOT NULL, 'group_id' INTEGER NOT NULL, 'condition' INTEGER NOT NULL, 'win_saddle_type' INTEGER NOT NULL, 'race_instance_id_1' INTEGER NOT NULL, 'race_instance_id_2' INTEGER NOT NULL, 'race_instance_id_3' INTEGER NOT NULL, 'race_instance_id_4' INTEGER NOT NULL, 'race_instance_id_5' INTEGER NOT NULL, 'race_instance_id_6' INTEGER NOT NULL, 'race_instance_id_7' INTEGER NOT NULL, 'race_instance_id_8' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM single_mode_wins_saddle WHERE id IN (SELECT id FROM single_mode_wins_saddle WHERE race_instance_id_1 IN (SELECT id FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501)) UNION SELECT id FROM single_mode_wins_saddle WHERE race_instance_id_2 IN (SELECT id FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501)) UNION SELECT id FROM single_mode_wins_saddle WHERE race_instance_id_3 IN (SELECT id FROM race_instance WHERE race_id IN (1005, 1010, 1015, 1026, 1028, 1101, 2001, 2010, 2035, 3001, 4001, 4501)));
INSERT INTO single_mode_wins_saddle VALUES(1,1,1,0,0,100501,101001,101501,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(12,15,22,0,3,101001,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(16,20,28,0,3,101501,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(18,23,17,0,3,100501,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(36,41,39,0,3,110101,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(40,45,45,0,2,200101,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(49,54,54,0,2,201001,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(74,80,80,0,1,300101,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(144,2,1,0,0,100501,101001,102601,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(148,21,28,0,3,102601,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(149,55,54,0,2,203501,0,0,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(154,2,1,0,0,102801,101001,101501,0,0,0,0,0);
INSERT INTO single_mode_wins_saddle VALUES(155,23,17,0,3,102801,0,0,0,0,0,0,0);
CREATE TABLE IF NOT EXISTS 'single_mode_scenario' ('id' INTEGER NOT NULL, 'sort_id' INTEGER NOT NULL, 'scenario_image_id' INTEGER NOT NULL, 'prologue_id' INTEGER NOT NULL, 'turn_set_id' INTEGER NOT NULL, 'hint_rate' INTEGER NOT NULL, 'start_date' INTEGER NOT NULL, 'end_date' INTEGER NOT NULL, 'bg_id' INTEGER NOT NULL, 'bg_sub_id' INTEGER NOT NULL, 'bg_offset_x' INTEGER NOT NULL, 'sec_bg_id' INTEGER NOT NULL, 'sec_bg_sub_id' INTEGER NOT NULL, 'label_font_color' TEXT NOT NULL, 'label_bg_color' TEXT NOT NULL, 'chara_program_change_flag' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM single_mode_scenario WHERE id IN (1, 2, 4);
INSERT INTO single_mode_scenario VALUES(1,1,1,80000001,1,0,1577836800,2524608000,5,5110,375,37,110,'FFFFFF','9CD127',0);
INSERT INTO single_mode_scenario VALUES(2,2,2,80000002,2,0,1762466400,2524608000,124,2111,0,79,110,'FFFFFF','18B1FF',0);
INSERT INTO single_mode_scenario VALUES(4,3,4,80000004,4,0,1773352800,2524608000,24,110,0,56,111,'FFFFFF','EABA00',1);
+164
View File
@@ -0,0 +1,164 @@
CREATE TABLE IF NOT EXISTS 'text_data' ('id' INTEGER NOT NULL, 'category' INTEGER NOT NULL, 'index' INTEGER NOT NULL, 'text' TEXT NOT NULL, PRIMARY KEY('category','index'));
-- SELECT * FROM text_data WHERE category IN (47, 48) AND "index" IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241) OR category = 4 AND "index" IN (SELECT card_id FROM card_rarity_data WHERE skill_set IN (SELECT id FROM skill_set WHERE skill_id1 IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241)));
INSERT INTO text_data VALUES(47,47,10351,'V Is for Victory!');
INSERT INTO text_data VALUES(47,47,100011,'Shooting Star');
INSERT INTO text_data VALUES(47,47,100351,'Our Ticket to Win!');
INSERT INTO text_data VALUES(47,47,110241,'Flowery☆Maneuver');
INSERT INTO text_data VALUES(47,47,200011,'Right-Handed ◎');
INSERT INTO text_data VALUES(47,47,200012,'Right-Handed ○');
INSERT INTO text_data VALUES(47,47,200013,'Right-Handed ×');
INSERT INTO text_data VALUES(47,47,200014,'Right-Handed Demon');
INSERT INTO text_data VALUES(47,47,200021,'Left-Handed ◎');
INSERT INTO text_data VALUES(47,47,200022,'Left-Handed ○');
INSERT INTO text_data VALUES(47,47,200023,'Left-Handed ×');
INSERT INTO text_data VALUES(47,47,200361,'Beeline Burst');
INSERT INTO text_data VALUES(47,47,200362,'Straightaway Adept');
INSERT INTO text_data VALUES(47,47,200831,'Subdued Front Runners');
INSERT INTO text_data VALUES(47,47,201801,'♡ 3D Nail Art');
INSERT INTO text_data VALUES(47,47,300011,'Unquenched Thirst');
INSERT INTO text_data VALUES(47,47,900011,'Shooting Star');
INSERT INTO text_data VALUES(47,47,900351,'Our Ticket to Win!');
INSERT INTO text_data VALUES(47,47,910241,'Flowery☆Maneuver');
INSERT INTO text_data VALUES(48,48,10351,'Moderately increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.');
INSERT INTO text_data VALUES(48,48,100011,'Ride the momentum to increase velocity and very slightly increase acceleration after passing another runner toward the front late-race.');
INSERT INTO text_data VALUES(48,48,100351,'Increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.');
INSERT INTO text_data VALUES(48,48,110241,'Increase velocity when passing another runner toward the front on the final corner. If passing toward the back, increase acceleration instead.');
INSERT INTO text_data VALUES(48,48,200011,'Increase performance on right-handed tracks.');
INSERT INTO text_data VALUES(48,48,200012,'Moderately increase performance on right-handed tracks.');
INSERT INTO text_data VALUES(48,48,200013,'Moderately decrease performance on right-handed tracks.');
INSERT INTO text_data VALUES(48,48,200014,'Increase proficiency in right-handed tracks, increasing Speed and Power.');
INSERT INTO text_data VALUES(48,48,200021,'Increase performance on left-handed tracks.');
INSERT INTO text_data VALUES(48,48,200022,'Moderately increase performance on left-handed tracks.');
INSERT INTO text_data VALUES(48,48,200023,'Moderately decrease performance on left-handed tracks.');
INSERT INTO text_data VALUES(48,48,200361,'Increase velocity on a straight.');
INSERT INTO text_data VALUES(48,48,200362,'Slightly increase velocity on a straight.');
INSERT INTO text_data VALUES(48,48,200831,'Slightly increase fatigue for front runners early-race.');
INSERT INTO text_data VALUES(48,48,201801,'Moderately decrease performance on firm ground.');
INSERT INTO text_data VALUES(48,48,300011,'Moderately increase performance with the desire to race.');
INSERT INTO text_data VALUES(48,48,900011,'Slightly increase velocity and very minimally increase acceleration after passing another runner toward the front late-race.');
INSERT INTO text_data VALUES(48,48,900351,'Slightly increase velocity when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.');
INSERT INTO text_data VALUES(48,48,910241,'Slightly increase velocity when passing another runner toward the front on the final corner. If passing toward the back, slightly increase acceleration instead.');
INSERT INTO text_data VALUES(4,4,100101,'[Special Dreamer] Special Week');
INSERT INTO text_data VALUES(4,4,102402,'[Sunlight Bouquet] Mayano Top Gun');
INSERT INTO text_data VALUES(4,4,103501,'[Get to Winning!] Winning Ticket');
CREATE TABLE IF NOT EXISTS 'skill_data' ('id' INTEGER NOT NULL, 'rarity' INTEGER NOT NULL, 'group_id' INTEGER NOT NULL, 'group_rate' INTEGER NOT NULL, 'filter_switch' INTEGER NOT NULL, 'grade_value' INTEGER NOT NULL, 'skill_category' INTEGER NOT NULL, 'tag_id' TEXT NOT NULL, 'unique_skill_id_1' INTEGER NOT NULL, 'unique_skill_id_2' INTEGER NOT NULL, 'exp_type' INTEGER NOT NULL, 'potential_per_default' INTEGER NOT NULL, 'activate_lot' INTEGER NOT NULL, 'precondition_1' TEXT NOT NULL, 'condition_1' TEXT NOT NULL, 'float_ability_time_1' INTEGER NOT NULL, 'ability_time_usage_1' INTEGER NOT NULL, 'float_cooldown_time_1' INTEGER NOT NULL, 'ability_type_1_1' INTEGER NOT NULL, 'ability_value_usage_1_1' INTEGER NOT NULL, 'ability_value_level_usage_1_1' INTEGER NOT NULL, 'float_ability_value_1_1' INTEGER NOT NULL, 'target_type_1_1' INTEGER NOT NULL, 'target_value_1_1' INTEGER NOT NULL, 'ability_type_1_2' INTEGER NOT NULL, 'ability_value_usage_1_2' INTEGER NOT NULL, 'ability_value_level_usage_1_2' INTEGER NOT NULL, 'float_ability_value_1_2' INTEGER NOT NULL, 'target_type_1_2' INTEGER NOT NULL, 'target_value_1_2' INTEGER NOT NULL, 'ability_type_1_3' INTEGER NOT NULL, 'ability_value_usage_1_3' INTEGER NOT NULL, 'ability_value_level_usage_1_3' INTEGER NOT NULL, 'float_ability_value_1_3' INTEGER NOT NULL, 'target_type_1_3' INTEGER NOT NULL, 'target_value_1_3' INTEGER NOT NULL, 'precondition_2' TEXT NOT NULL, 'condition_2' TEXT NOT NULL, 'float_ability_time_2' INTEGER NOT NULL, 'ability_time_usage_2' INTEGER NOT NULL, 'float_cooldown_time_2' INTEGER NOT NULL, 'ability_type_2_1' INTEGER NOT NULL, 'ability_value_usage_2_1' INTEGER NOT NULL, 'ability_value_level_usage_2_1' INTEGER NOT NULL, 'float_ability_value_2_1' INTEGER NOT NULL, 'target_type_2_1' INTEGER NOT NULL, 'target_value_2_1' INTEGER NOT NULL, 'ability_type_2_2' INTEGER NOT NULL, 'ability_value_usage_2_2' INTEGER NOT NULL, 'ability_value_level_usage_2_2' INTEGER NOT NULL, 'float_ability_value_2_2' INTEGER NOT NULL, 'target_type_2_2' INTEGER NOT NULL, 'target_value_2_2' INTEGER NOT NULL, 'ability_type_2_3' INTEGER NOT NULL, 'ability_value_usage_2_3' INTEGER NOT NULL, 'ability_value_level_usage_2_3' INTEGER NOT NULL, 'float_ability_value_2_3' INTEGER NOT NULL, 'target_type_2_3' INTEGER NOT NULL, 'target_value_2_3' INTEGER NOT NULL, 'popularity_add_param_1' INTEGER NOT NULL, 'popularity_add_value_1' INTEGER NOT NULL, 'popularity_add_param_2' INTEGER NOT NULL, 'popularity_add_value_2' INTEGER NOT NULL, 'disp_order' INTEGER NOT NULL, 'icon_id' INTEGER NOT NULL, 'plate_type' INTEGER NOT NULL, 'disable_singlemode' INTEGER NOT NULL, 'disable_count_condition' INTEGER NOT NULL, 'is_general_skill' INTEGER NOT NULL, 'start_date' INTEGER NOT NULL, 'end_date' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM skill_data WHERE id IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241);
INSERT INTO skill_data VALUES(10351,3,1035,1,0,240,5,'401',0,0,1,0,0,'is_finalcorner==1&blocked_side_continuetime>=2','is_finalcorner==1&corner==0&order<=5',50000,1,5000000,27,1,1,2500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,30,0,0,10,20013,0,1,0,0,1483228800,2524608000);
INSERT INTO skill_data VALUES(100011,5,10001,1,0,340,5,'401/403',0,0,1,0,0,'','phase>=2&order>=1&order_rate<=50&change_order_onetime<0',50000,1,5000000,27,1,1,3500,1,0,31,1,1,1000,1,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,60,0,0,10,20013,0,1,0,0,1483228800,2524608000);
INSERT INTO skill_data VALUES(100351,4,10035,1,0,340,5,'401',0,0,1,0,0,'is_finalcorner==1&blocked_side_continuetime>=2','is_finalcorner==1&corner==0&order<=5',50000,1,5000000,27,1,1,3500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,60,0,0,10,20013,0,1,0,0,1483228800,2524608000);
INSERT INTO skill_data VALUES(110241,5,11024,1,0,340,5,'401/403',0,0,1,0,0,'','is_finalcorner==1&corner!=0&order_rate<=40&change_order_onetime<0',50000,1,5000000,27,1,1,3500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','is_finalcorner==1&corner!=0&order_rate>=50&order_rate<=80&change_order_onetime<0',40000,1,5000000,31,1,1,4000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,1,30,3,30,10,20013,0,1,0,0,1483228800,2524608000);
INSERT INTO skill_data VALUES(200011,1,20001,2,0,174,0,'401',0,0,0,10,0,'','rotation==1',-1,1,0,1,1,1,600000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,45,0,0,1005,10011,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200012,1,20001,1,0,129,0,'401',0,0,0,10,0,'','rotation==1',-1,1,0,1,1,1,400000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,30,0,0,1010,10011,0,0,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200013,1,20001,-1,0,-129,0,'401',0,0,0,10,0,'','rotation==1',-1,1,0,1,1,1,-400000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,-30,0,0,1020,10014,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200014,2,20001,3,0,461,0,'401/403',0,0,0,10,0,'','rotation==1',-1,1,0,1,1,1,600000,1,0,3,1,1,600000,1,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,30,3,30,1000,10012,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200021,1,20002,2,0,174,0,'401',0,0,0,10,0,'','rotation==2',-1,1,0,1,1,1,600000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,45,0,0,1030,10011,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200022,1,20002,1,0,129,0,'401',0,0,0,10,0,'','rotation==2',-1,1,0,1,1,1,400000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,30,0,0,1040,10011,0,0,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200023,1,20002,-1,0,-129,0,'401',0,0,0,10,0,'','rotation==2',-1,1,0,1,1,1,-400000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,-30,0,0,1050,10014,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200361,2,20036,2,0,508,4,'401',0,0,0,10,1,'','straight_random==1',24000,1,300000,27,1,1,3500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,60,0,0,1980,20012,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200362,1,20036,1,0,217,4,'401',0,0,0,10,1,'','straight_random==1',24000,1,300000,27,1,1,1500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,20,0,0,1990,20011,0,0,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(200831,1,20083,1,0,217,1,'301/406',0,0,0,10,1,'','running_style_count_nige_otherself>=1&phase_random==0&accumulatetime>=5',0,1,5000000,9,1,1,-100,18,1,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,2,10,5,10,2820,30051,0,0,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(201801,1,20180,-1,0,-129,0,'401',0,0,0,10,0,'','ground_condition==1',-1,1,0,1,1,1,-400000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,-30,0,0,20,10014,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(300011,1,30001,1,0,0,0,'402',0,0,0,0,0,'','track_id==10008',-1,1,0,2,1,1,400000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,2,30,0,0,70000,10021,1,1,0,0,1483228800,2524608000);
INSERT INTO skill_data VALUES(900011,1,90001,2,0,180,5,'401/403',100011,0,0,0,1,'','phase>=2&order>=1&order_rate<=50&change_order_onetime<0',30000,1,5000000,27,1,1,1500,1,0,31,1,1,500,1,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,15,0,0,30,20011,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(900351,1,90035,2,0,180,5,'401',100351,10351,0,0,1,'is_finalcorner==1&blocked_side_continuetime>=2','is_finalcorner==1&corner==0&order<=5',30000,1,5000000,27,1,1,1500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','',0,1,0,0,1,1,0,0,0,0,1,1,0,0,0,0,1,1,0,0,0,1,15,0,0,30,20011,0,1,0,1,1483228800,2524608000);
INSERT INTO skill_data VALUES(910241,1,91024,2,0,180,5,'401/403',110241,0,0,0,1,'','is_finalcorner==1&corner!=0&order_rate<=40&change_order_onetime<0',30000,1,5000000,27,1,1,1500,1,0,0,1,1,0,0,0,0,1,1,0,0,0,'','is_finalcorner==1&corner!=0&order_rate>=50&order_rate<=80&change_order_onetime<0',24000,1,5000000,31,1,1,2000,1,0,0,1,1,0,0,0,0,1,1,0,0,0,1,7,3,7,30,20011,0,1,0,1,1483228800,2524608000);
CREATE TABLE IF NOT EXISTS 'single_mode_skill_need_point' ('id' INTEGER NOT NULL, 'need_skill_point' INTEGER NOT NULL, 'status_type' INTEGER NOT NULL, 'status_value' INTEGER NOT NULL, 'solvable_type' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM single_mode_skill_need_point WHERE id IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241);
INSERT INTO single_mode_skill_need_point VALUES(200011,110,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200012,90,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200013,50,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200014,130,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200021,110,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200022,90,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200023,50,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200361,170,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200362,170,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(200831,130,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(201801,50,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(900011,200,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(900351,200,0,0,0);
INSERT INTO single_mode_skill_need_point VALUES(910241,200,0,0,0);
CREATE TABLE IF NOT EXISTS 'card_data' ('id' INTEGER NOT NULL, 'chara_id' INTEGER NOT NULL, 'default_rarity' INTEGER NOT NULL, 'limited_chara' INTEGER NOT NULL, 'available_skill_set_id' INTEGER NOT NULL, 'talent_speed' INTEGER NOT NULL, 'talent_stamina' INTEGER NOT NULL, 'talent_pow' INTEGER NOT NULL, 'talent_guts' INTEGER NOT NULL, 'talent_wiz' INTEGER NOT NULL, 'talent_group_id' INTEGER NOT NULL, 'bg_id' INTEGER NOT NULL, 'get_piece_id' INTEGER NOT NULL, 'running_style' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM card_data WHERE id IN (SELECT card_id FROM card_rarity_data WHERE skill_set IN (SELECT id FROM skill_set WHERE skill_id1 IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241)));
INSERT INTO card_data VALUES(100101,1001,3,0,100101,0,20,0,0,10,100101,8,100101,3);
INSERT INTO card_data VALUES(102402,1024,3,0,102402,10,10,0,0,10,102402,291,102402,2);
INSERT INTO card_data VALUES(103501,1035,1,0,103501,0,10,20,0,0,103501,8,103501,3);
CREATE TABLE IF NOT EXISTS 'card_rarity_data' ('id' INTEGER NOT NULL, 'card_id' INTEGER NOT NULL, 'rarity' INTEGER NOT NULL, 'race_dress_id' INTEGER NOT NULL, 'skill_set' INTEGER NOT NULL, 'speed' INTEGER NOT NULL, 'stamina' INTEGER NOT NULL, 'pow' INTEGER NOT NULL, 'guts' INTEGER NOT NULL, 'wiz' INTEGER NOT NULL, 'max_speed' INTEGER NOT NULL, 'max_stamina' INTEGER NOT NULL, 'max_pow' INTEGER NOT NULL, 'max_guts' INTEGER NOT NULL, 'max_wiz' INTEGER NOT NULL, 'proper_distance_short' INTEGER NOT NULL, 'proper_distance_mile' INTEGER NOT NULL, 'proper_distance_middle' INTEGER NOT NULL, 'proper_distance_long' INTEGER NOT NULL, 'proper_running_style_nige' INTEGER NOT NULL, 'proper_running_style_senko' INTEGER NOT NULL, 'proper_running_style_sashi' INTEGER NOT NULL, 'proper_running_style_oikomi' INTEGER NOT NULL, 'proper_ground_turf' INTEGER NOT NULL, 'proper_ground_dirt' INTEGER NOT NULL, 'get_dress_id_1' INTEGER NOT NULL, 'get_dress_id_2' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM card_rarity_data WHERE skill_set IN (SELECT id FROM skill_set WHERE skill_id1 IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241));
INSERT INTO card_rarity_data VALUES(10010103,100101,3,100101,10010103,83,88,98,90,91,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100101);
INSERT INTO card_rarity_data VALUES(10010104,100101,4,100101,10010104,92,98,109,100,101,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100101);
INSERT INTO card_rarity_data VALUES(10010105,100101,5,100101,10010105,102,108,120,110,110,1200,1200,1200,1200,1200,2,5,7,7,1,7,7,5,7,1,101,100101);
INSERT INTO card_rarity_data VALUES(10240203,102402,3,102426,11240103,83,100,82,90,95,1200,1200,1200,1200,1200,4,4,7,7,7,7,6,6,7,3,101,102426);
INSERT INTO card_rarity_data VALUES(10240204,102402,4,102426,11240104,92,112,91,100,105,1200,1200,1200,1200,1200,4,4,7,7,7,7,6,6,7,3,101,102426);
INSERT INTO card_rarity_data VALUES(10240205,102402,5,102426,11240105,102,123,100,110,115,1200,1200,1200,1200,1200,4,4,7,7,7,7,6,6,7,3,101,102426);
INSERT INTO card_rarity_data VALUES(10350101,103501,1,101,10350101,87,68,91,74,80,1200,1200,1200,1200,1200,1,2,7,6,1,6,7,1,7,1,101,0);
INSERT INTO card_rarity_data VALUES(10350102,103501,2,101,10350102,93,72,97,78,85,1200,1200,1200,1200,1200,1,2,7,6,1,6,7,1,7,1,101,0);
INSERT INTO card_rarity_data VALUES(10350103,103501,3,103501,10350103,98,76,102,83,91,1200,1200,1200,1200,1200,1,2,7,6,1,6,7,1,7,1,101,103501);
INSERT INTO card_rarity_data VALUES(10350104,103501,4,103501,10350104,109,85,114,92,100,1200,1200,1200,1200,1200,1,2,7,6,1,6,7,1,7,1,101,103501);
INSERT INTO card_rarity_data VALUES(10350105,103501,5,103501,10350105,120,93,125,101,111,1200,1200,1200,1200,1200,1,2,7,6,1,6,7,1,7,1,101,103501);
CREATE TABLE IF NOT EXISTS 'skill_set' ('id' INTEGER NOT NULL, 'skill_id1' INTEGER NOT NULL, 'skill_level1' INTEGER NOT NULL, 'skill_id2' INTEGER NOT NULL, 'skill_level2' INTEGER NOT NULL, 'skill_id3' INTEGER NOT NULL, 'skill_level3' INTEGER NOT NULL, 'skill_id4' INTEGER NOT NULL, 'skill_level4' INTEGER NOT NULL, 'skill_id5' INTEGER NOT NULL, 'skill_level5' INTEGER NOT NULL, 'skill_id6' INTEGER NOT NULL, 'skill_level6' INTEGER NOT NULL, 'skill_id7' INTEGER NOT NULL, 'skill_level7' INTEGER NOT NULL, 'skill_id8' INTEGER NOT NULL, 'skill_level8' INTEGER NOT NULL, 'skill_id9' INTEGER NOT NULL, 'skill_level9' INTEGER NOT NULL, 'skill_id10' INTEGER NOT NULL, 'skill_level10' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM skill_set WHERE skill_id1 IN (10351, 100011, 100351, 110241, 200011, 200012, 200013, 200014, 200021, 200022, 200023, 200361, 200362, 200831, 201801, 300011, 900011, 900351, 910241);
INSERT INTO skill_set VALUES(8001,200012,1,200433,1,200532,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8007,200022,1,200572,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8008,200362,1,200562,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8011,200012,1,200592,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8016,200022,1,200472,1,200502,1,200632,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8028,200012,1,200492,1,200572,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8034,200022,1,200612,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8037,200362,1,200632,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8043,200012,1,200401,1,200532,1,200712,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8053,200362,1,200502,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8054,200022,1,200352,1,200612,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8057,200012,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8068,200022,1,200572,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8072,200022,1,200612,1,200752,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8078,200012,1,200401,1,200202,1,200622,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8083,200022,1,200332,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8101,200362,1,200352,1,200552,1,200682,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8103,200012,1,200052,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8107,200012,1,200372,1,200582,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8114,200012,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8117,200022,1,200332,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8123,200362,1,200532,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8124,200012,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(8133,200022,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(100013,100351,1,200591,1,201401,1,201392,1,201422,1,200492,1,200432,1,200612,1,200032,1,200732,1);
INSERT INTO skill_set VALUES(100037,100351,1,201401,1,200612,1,200462,1,200732,1,200032,1,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(100046,100351,1,201401,1,200612,1,200462,1,200732,1,200032,1,201691,1,200441,1,0,0,0,0);
INSERT INTO skill_set VALUES(400007,200012,1,200492,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(400012,200022,1,200612,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(400019,200012,1,200401,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(400028,200022,1,200352,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(400030,200012,1,200871,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(1100101,100011,1,200511,1,200612,1,200602,1,200032,1,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(1100102,100011,1,200511,1,200491,1,200602,1,200032,1,200132,1,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(1103401,200361,1,200461,1,202002,1,200172,1,200292,1,200752,1,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(1103501,100351,1,200282,1,200612,1,200462,1,200732,1,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(1106701,200361,1,201242,1,201262,1,201522,1,200352,1,200382,1,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10010103,100011,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10010104,100011,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10010105,100011,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10350101,10351,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10350102,10351,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10350103,100351,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10350104,100351,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(10350105,100351,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(11240103,110241,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(11240104,110241,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(11240105,110241,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32100101,100011,1,200511,1,200612,1,201402,1,200592,1,200342,1,200562,1,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32100102,100011,1,200511,1,200611,1,201401,1,200592,1,200342,1,200332,1,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32100106,100011,1,200511,1,201401,1,200592,1,200342,1,200332,1,200732,1,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32101301,200361,1,200562,1,200462,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32101304,200361,1,200462,1,200742,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32103502,100351,1,200591,1,201401,1,201392,1,200732,1,201422,1,200492,1,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32801301,200012,1,201352,1,200432,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32802801,200362,1,200992,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(32804001,200362,1,200622,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(51100132,100011,1,200511,1,200612,1,200732,1,200592,1,0,0,0,0,0,0,0,0,0,0);
INSERT INTO skill_set VALUES(51103531,100351,1,202081,1,201402,1,200612,1,200602,1,0,0,0,0,0,0,0,0,0,0);
+113
View File
@@ -0,0 +1,113 @@
CREATE TABLE IF NOT EXISTS 'text_data' ('id' INTEGER NOT NULL, 'category' INTEGER NOT NULL, 'index' INTEGER NOT NULL, 'text' TEXT NOT NULL, PRIMARY KEY('category','index'));
-- SELECT * FROM text_data WHERE category IN (147, 172) AND "index" in (101, 102, 103, 201, 202, 203, 1101, 1102, 1103, 1000101, 1000102, 1000103, 2000101, 2000102, 2000103, 3000101, 3000102, 3000103, 10010101, 10010102, 10010103);
INSERT INTO text_data VALUES(147,147,101,'Speed');
INSERT INTO text_data VALUES(147,147,102,'Speed');
INSERT INTO text_data VALUES(147,147,103,'Speed');
INSERT INTO text_data VALUES(147,147,201,'Stamina');
INSERT INTO text_data VALUES(147,147,202,'Stamina');
INSERT INTO text_data VALUES(147,147,203,'Stamina');
INSERT INTO text_data VALUES(147,147,1101,'Turf');
INSERT INTO text_data VALUES(147,147,1102,'Turf');
INSERT INTO text_data VALUES(147,147,1103,'Turf');
INSERT INTO text_data VALUES(147,147,1000101,'February S.');
INSERT INTO text_data VALUES(147,147,1000102,'February S.');
INSERT INTO text_data VALUES(147,147,1000103,'February S.');
INSERT INTO text_data VALUES(147,147,2000101,'Right-Handed ○');
INSERT INTO text_data VALUES(147,147,2000102,'Right-Handed ○');
INSERT INTO text_data VALUES(147,147,2000103,'Right-Handed ○');
INSERT INTO text_data VALUES(147,147,3000101,'URA Finale');
INSERT INTO text_data VALUES(147,147,3000102,'URA Finale');
INSERT INTO text_data VALUES(147,147,3000103,'URA Finale');
INSERT INTO text_data VALUES(147,147,10010101,'Shooting Star');
INSERT INTO text_data VALUES(147,147,10010102,'Shooting Star');
INSERT INTO text_data VALUES(147,147,10010103,'Shooting Star');
INSERT INTO text_data VALUES(172,172,101,'A Spark that increases Speed.');
INSERT INTO text_data VALUES(172,172,102,'A Spark that increases Speed.');
INSERT INTO text_data VALUES(172,172,103,'A Spark that increases Speed.');
INSERT INTO text_data VALUES(172,172,201,'A Spark that increases Stamina.');
INSERT INTO text_data VALUES(172,172,202,'A Spark that increases Stamina.');
INSERT INTO text_data VALUES(172,172,203,'A Spark that increases Stamina.');
INSERT INTO text_data VALUES(172,172,1101,'A Spark that increases Turf Aptitude.');
INSERT INTO text_data VALUES(172,172,1102,'A Spark that increases Turf Aptitude.');
INSERT INTO text_data VALUES(172,172,1103,'A Spark that increases Turf Aptitude.');
INSERT INTO text_data VALUES(172,172,1000101,'A Spark that increases Power and gives a skill hint for "Winter Runner ○".');
INSERT INTO text_data VALUES(172,172,1000102,'A Spark that increases Power and gives a skill hint for "Winter Runner ○".');
INSERT INTO text_data VALUES(172,172,1000103,'A Spark that increases Power and gives a skill hint for "Winter Runner ○".');
INSERT INTO text_data VALUES(172,172,2000101,'A Spark that gives a skill hint for "Right-Handed ○".');
INSERT INTO text_data VALUES(172,172,2000102,'A Spark that gives a skill hint for "Right-Handed ○".');
INSERT INTO text_data VALUES(172,172,2000103,'A Spark that gives a skill hint for "Right-Handed ○".');
INSERT INTO text_data VALUES(172,172,3000101,'A Spark that increases Speed and Stamina.');
INSERT INTO text_data VALUES(172,172,3000102,'A Spark that increases Speed and Stamina.');
INSERT INTO text_data VALUES(172,172,3000103,'A Spark that increases Speed and Stamina.');
INSERT INTO text_data VALUES(172,172,10010101,'A Spark that gives a skill hint for "Shooting Star".');
INSERT INTO text_data VALUES(172,172,10010102,'A Spark that gives a skill hint for "Shooting Star".');
INSERT INTO text_data VALUES(172,172,10010103,'A Spark that gives a skill hint for "Shooting Star".');
CREATE TABLE IF NOT EXISTS 'succession_factor' ('factor_id' INTEGER NOT NULL, 'factor_group_id' INTEGER NOT NULL, 'rarity' INTEGER NOT NULL, 'grade' INTEGER NOT NULL, 'factor_type' INTEGER NOT NULL, 'effect_group_id' INTEGER NOT NULL, 'start_date' INTEGER NOT NULL, 'end_date' INTEGER NOT NULL, PRIMARY KEY('factor_id'));
-- SELECT * FROM succession_factor WHERE factor_id IN (101, 102, 103, 201, 202, 203, 1101, 1102, 1103, 1000101, 1000102, 1000103, 2000101, 2000102, 2000103, 3000101, 3000102, 3000103, 10010101, 10010102, 10010103);
INSERT INTO succession_factor VALUES(101,1,1,1,1,11,1483228800,2524608000);
INSERT INTO succession_factor VALUES(102,1,2,1,1,12,1483228800,2524608000);
INSERT INTO succession_factor VALUES(103,1,3,1,1,13,1483228800,2524608000);
INSERT INTO succession_factor VALUES(201,2,1,1,1,11,1483228800,2524608000);
INSERT INTO succession_factor VALUES(202,2,2,1,1,12,1483228800,2524608000);
INSERT INTO succession_factor VALUES(203,2,3,1,1,13,1483228800,2524608000);
INSERT INTO succession_factor VALUES(1101,11,1,1,2,21,1483228800,2524608000);
INSERT INTO succession_factor VALUES(1102,11,2,1,2,22,1483228800,2524608000);
INSERT INTO succession_factor VALUES(1103,11,3,1,2,23,1483228800,2524608000);
INSERT INTO succession_factor VALUES(1000101,10001,1,1,5,51,1483228800,2524608000);
INSERT INTO succession_factor VALUES(1000102,10001,2,1,5,52,1483228800,2524608000);
INSERT INTO succession_factor VALUES(1000103,10001,3,1,5,53,1483228800,2524608000);
INSERT INTO succession_factor VALUES(2000101,20001,1,1,4,41,1483228800,2524608000);
INSERT INTO succession_factor VALUES(2000102,20001,2,1,4,42,1483228800,2524608000);
INSERT INTO succession_factor VALUES(2000103,20001,3,1,4,43,1483228800,2524608000);
INSERT INTO succession_factor VALUES(3000101,30001,1,1,6,51,1483228800,2524608000);
INSERT INTO succession_factor VALUES(3000102,30001,2,1,6,52,1483228800,2524608000);
INSERT INTO succession_factor VALUES(3000103,30001,3,1,6,53,1483228800,2524608000);
INSERT INTO succession_factor VALUES(10010101,100101,1,2,3,31,1483228800,2524608000);
INSERT INTO succession_factor VALUES(10010102,100101,2,2,3,32,1483228800,2524608000);
INSERT INTO succession_factor VALUES(10010103,100101,3,2,3,33,1483228800,2524608000);
CREATE TABLE IF NOT EXISTS 'succession_factor_effect' ('id' INTEGER NOT NULL, 'factor_group_id' INTEGER NOT NULL, 'effect_id' INTEGER NOT NULL, 'target_type' INTEGER NOT NULL, 'value_1' INTEGER NOT NULL, 'value_2' INTEGER NOT NULL, PRIMARY KEY('id'));
-- SELECT * FROM succession_factor_effect WHERE factor_group_id IN (SELECT factor_group_id FROM succession_factor WHERE factor_id IN (101, 102, 103, 201, 202, 203, 1101, 1102, 1103, 1000101, 1000102, 1000103, 2000101, 2000102, 2000103, 3000101, 3000102, 3000103, 10010101, 10010102, 10010103));
INSERT INTO succession_factor_effect VALUES(1,1,1,1,1,0);
INSERT INTO succession_factor_effect VALUES(2,1,2,1,4,0);
INSERT INTO succession_factor_effect VALUES(3,1,3,1,7,0);
INSERT INTO succession_factor_effect VALUES(4,1,4,1,10,0);
INSERT INTO succession_factor_effect VALUES(5,1,5,1,13,0);
INSERT INTO succession_factor_effect VALUES(6,1,6,1,16,0);
INSERT INTO succession_factor_effect VALUES(7,1,7,1,19,0);
INSERT INTO succession_factor_effect VALUES(8,1,8,1,22,0);
INSERT INTO succession_factor_effect VALUES(9,1,9,1,25,0);
INSERT INTO succession_factor_effect VALUES(10,1,10,1,28,0);
INSERT INTO succession_factor_effect VALUES(11,2,1,2,1,0);
INSERT INTO succession_factor_effect VALUES(12,2,2,2,4,0);
INSERT INTO succession_factor_effect VALUES(13,2,3,2,7,0);
INSERT INTO succession_factor_effect VALUES(14,2,4,2,10,0);
INSERT INTO succession_factor_effect VALUES(15,2,5,2,13,0);
INSERT INTO succession_factor_effect VALUES(16,2,6,2,16,0);
INSERT INTO succession_factor_effect VALUES(17,2,7,2,19,0);
INSERT INTO succession_factor_effect VALUES(18,2,8,2,22,0);
INSERT INTO succession_factor_effect VALUES(19,2,9,2,25,0);
INSERT INTO succession_factor_effect VALUES(20,2,10,2,28,0);
INSERT INTO succession_factor_effect VALUES(51,11,1,11,1,0);
INSERT INTO succession_factor_effect VALUES(52,11,2,11,2,0);
INSERT INTO succession_factor_effect VALUES(1144,10001,1,3,3,0);
INSERT INTO succession_factor_effect VALUES(1145,10001,1,41,200202,1);
INSERT INTO succession_factor_effect VALUES(1146,10001,2,3,6,0);
INSERT INTO succession_factor_effect VALUES(1147,10001,2,41,200202,1);
INSERT INTO succession_factor_effect VALUES(1148,10001,3,3,9,0);
INSERT INTO succession_factor_effect VALUES(1149,10001,3,41,200202,1);
INSERT INTO succession_factor_effect VALUES(344,20001,1,41,200012,1);
INSERT INTO succession_factor_effect VALUES(345,20001,2,41,200012,2);
INSERT INTO succession_factor_effect VALUES(346,20001,3,41,200012,3);
INSERT INTO succession_factor_effect VALUES(347,20001,4,41,200012,4);
INSERT INTO succession_factor_effect VALUES(348,20001,5,41,200012,5);
INSERT INTO succession_factor_effect VALUES(1324,30001,1,1,10,0);
INSERT INTO succession_factor_effect VALUES(1325,30001,1,2,10,0);
INSERT INTO succession_factor_effect VALUES(1326,30001,2,1,20,0);
INSERT INTO succession_factor_effect VALUES(1327,30001,2,2,20,0);
INSERT INTO succession_factor_effect VALUES(1328,30001,3,1,30,0);
INSERT INTO succession_factor_effect VALUES(1329,30001,3,2,30,0);
INSERT INTO succession_factor_effect VALUES(71,100101,1,41,900011,1);
INSERT INTO succession_factor_effect VALUES(73,100101,2,41,900011,2);
INSERT INTO succession_factor_effect VALUES(75,100101,3,41,900011,3);
+6 -1
View File
@@ -78,6 +78,9 @@ func (a Ability) String() string {
case AbilityHP: case AbilityHP:
r = append(r, (a.Value * 100).String()...) r = append(r, (a.Value * 100).String()...)
r = append(r, '%') r = append(r, '%')
case AbilityRushedChance:
r = append(r, a.Value.String()...)
r = append(r, '%')
case AbilityGateDelay: case AbilityGateDelay:
// This skill type in particular should be × instead of +. // This skill type in particular should be × instead of +.
r = append(r[:len(r)-1], "×"...) r = append(r[:len(r)-1], "×"...)
@@ -85,7 +88,7 @@ func (a Ability) String() string {
case AbilityFrenzy, AbilityAddGateDelay: case AbilityFrenzy, AbilityAddGateDelay:
r = append(r, a.Value.String()...) r = append(r, a.Value.String()...)
r = append(r, 's') r = append(r, 's')
case AbilityCurrentSpeed, AbilityTargetSpeed, AbilityLaneSpeed: case AbilityCurrentSpeed, AbilityCurrentSpeedUp, AbilityTargetSpeed, AbilityLaneSpeed:
r = append(r, a.Value.String()...) r = append(r, a.Value.String()...)
r = append(r, " m/s"...) r = append(r, " m/s"...)
case AbilityAccel: case AbilityAccel:
@@ -160,8 +163,10 @@ const (
AbilityFrenzy AbilityType = 13 // Frenzy AbilityFrenzy AbilityType = 13 // Frenzy
AbilityAddGateDelay AbilityType = 14 // Added gate delay AbilityAddGateDelay AbilityType = 14 // Added gate delay
AbilityCurrentSpeed AbilityType = 21 // Current speed AbilityCurrentSpeed AbilityType = 21 // Current speed
AbilityCurrentSpeedUp AbilityType = 22 // Current speed
AbilityTargetSpeed AbilityType = 27 // Target speed AbilityTargetSpeed AbilityType = 27 // Target speed
AbilityLaneSpeed AbilityType = 28 // Lane change speed AbilityLaneSpeed AbilityType = 28 // Lane change speed
AbilityRushedChance AbilityType = 29 // Rushed chance
AbilityAccel AbilityType = 31 // Acceleration AbilityAccel AbilityType = 31 // Acceleration
AbilityLaneChange AbilityType = 35 // Forced lane change AbilityLaneChange AbilityType = 35 // Forced lane change
) )
+621 -566
View File
File diff suppressed because it is too large Load Diff
+16 -13
View File
@@ -20,32 +20,35 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.60.1", "@sveltejs/kit": "^2.70.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.3.0", "@tailwindcss/vite": "^4.3.3",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/node": "^22.19.19", "@types/node": "^22.20.1",
"@vitest/browser-playwright": "^4.1.0", "@vitest/browser-playwright": "^4.1.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"eslint": "^10.4.0", "eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.1", "eslint-plugin-svelte": "^3.22.0",
"globals": "^17.6.0", "globals": "^17.7.0",
"playwright": "^1.60.0", "playwright": "^1.61.1",
"prettier": "^3.8.3", "prettier": "^3.9.6",
"prettier-plugin-svelte": "^3.5.2", "prettier-plugin-svelte": "^3.5.2",
"prettier-plugin-tailwindcss": "^0.7.4", "prettier-plugin-tailwindcss": "^0.7.4",
"svelte": "^5.55.7", "svelte": "^5.56.7",
"svelte-check": "^4.4.8", "svelte-check": "^4.7.3",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.59.3", "typescript-eslint": "^8.65.0",
"vite": "^7.3.3", "vite": "^7.3.6",
"vitest": "^4.1.0", "vitest": "^4.1.0",
"vitest-browser-svelte": "^2.1.1" "vitest-browser-svelte": "^2.2.1"
}, },
"dependencies": { "dependencies": {
"@observablehq/plot": "^0.6.17", "@observablehq/plot": "^0.6.17",
"mathjs": "^15.2.0" "mathjs": "^15.2.0"
},
"allowScripts": {
"esbuild@0.28.1": true
} }
} }
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
const sizes = [256, 64, 32, 16] as const;
interface Props {
chara_id: number | null | undefined;
size: (typeof sizes)[number];
}
let { chara_id, size, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
const icon = $derived(chara_id ?? 0);
const avifs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/chara/${icon}/${sz}x${sz}.avif ${sz / size}x`)
.join(', '),
);
const pngs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/chara/${icon}/${sz}x${sz}.png ${sz / size}x`)
.join(', '),
);
const src = $derived(icon !== 0 ? `/img/chara/${icon}/${sizes[0]}x${sizes[0]}.png` : `/img/skill/10011`);
</script>
{#if icon !== 0}
<picture {...rest}>
<source srcset={avifs} type="image/avif" />
<source srcset={pngs} type="image/png" />
<img {src} alt={`Icon for character ID ${chara_id}`} class="inline" width={size} height={size} loading="lazy" />
</picture>
{:else}
<div class={`w-[${size}px] h-[${size}px]`}></div>
{/if}
+18 -13
View File
@@ -1,24 +1,29 @@
<script lang="ts"> <script lang="ts">
import { type Character } from '$lib/data/character'; import type { Character } from '$lib/data/character';
import type { ClassValue } from 'svelte/elements'; import type { ClassValue } from 'svelte/elements';
import Pick from './Pick.svelte';
import CharaIcon from './CharaIcon.svelte';
interface Props { interface Props {
id: string; id: string;
characters: Character[]; characters: Character[] | null;
value: number; value: Character | undefined;
class?: ClassValue | null; class?: ClassValue | null;
optionClass?: ClassValue | null;
required?: boolean; required?: boolean;
} }
let { id, characters, value = $bindable(), class: className, optionClass, required = false }: Props = $props(); let { id, characters, value = $bindable(), class: className, required = false }: Props = $props();
const charas = $derived(characters ?? []);
const key = (v: Character) => ({ id: v.chara_id, name: v.name });
</script> </script>
<select {id} class={className} bind:value {required}> <Pick {id} items={charas} {key} bind:value class={className} {required}>
{#if !required} {#snippet option(c: Character)}
<option value="0" class={optionClass}></option> <span>
{/if} <CharaIcon chara_id={c.chara_id} size={32} />
{#each characters as c (c.chara_id)} {c.name}
<option value={c.chara_id} class={optionClass}>{c.name}</option> </span>
{/each} {/snippet}
</select> </Pick>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import * as all from '$lib/all';
interface Props {
title?: string;
pages: all.Page[];
}
let { title, pages }: Props = $props();
</script>
{#if title != null}
<h2 class="mt-8 mb-4 text-4xl">{title}</h2>
{/if}
<ul class="mb-4 list-disc pl-4">
{#each pages as page (page.route)}
<li>
<a href={page.route}>{page.name}</a> &mdash; {page.description}
</li>
{/each}
</ul>
+155
View File
@@ -0,0 +1,155 @@
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { stringsearch } from './stringsearch';
interface Props {
id: string;
items: T[];
key: (t: T) => { id: number; name: string };
value: T | undefined;
option?: Snippet<[T]>;
class?: ClassValue | null;
required?: boolean;
}
let { id, items, key, value = $bindable(), option, class: className, required = false }: Props = $props();
let popover: HTMLElement | undefined = $state();
let optionsContainer: HTMLElement | undefined = $state();
const expanded = $derived(!popover?.hidden);
let search = $state('');
const its = $derived(items ?? []);
const searched = $derived(stringsearch(search, its, (v) => key(v).name) || its);
$effect(() => {
if (required && value == null && its.length > 0) {
value = its[0];
}
});
function setByElem(o: HTMLElement) {
const j = parseInt(o.dataset.pickId ?? '');
value = its.find((c) => key(c).id === j) ?? value;
o.focus();
}
function onkeydown(evt: KeyboardEvent) {
switch (evt.key) {
case 'ArrowDown': {
const opts = [...optionsContainer!.children] as HTMLElement[];
const i = opts.findIndex((n) => value != null && parseInt(n.dataset.pickId ?? '') === key(value).id);
const o = i >= 0 ? opts[Math.min(i + 1, opts.length - 1)] : opts[0];
setByElem(o);
break;
}
case 'ArrowUp': {
const opts = [...optionsContainer!.children] as HTMLElement[];
const i = opts.findIndex((n) => value != null && parseInt(n.dataset.pickId ?? '') === key(value).id);
const o = i > 0 ? opts[i - 1] : opts[0];
setByElem(o);
break;
}
case 'Home': {
search = '';
setByElem(optionsContainer!.children[0] as HTMLElement);
break;
}
case 'End': {
search = '';
setByElem(optionsContainer!.children[optionsContainer!.childElementCount - 1] as HTMLElement);
break;
}
case ' ':
popover!.showPopover();
break;
case 'Escape':
// TODO(zeph): restore chara that was selected when the popup opened?
// for now just hide
popover!.hidePopover();
break;
case 'Enter':
popover!.togglePopover();
break;
default:
return;
}
evt.preventDefault();
}
</script>
<div class={className} style={`anchor-name: --anchor-${id}`}>
<span
{id}
class="block h-9 content-center bg-mist-300 hover:cursor-pointer dark:bg-mist-900"
role="combobox"
aria-expanded={expanded}
aria-controls="charaOptions"
aria-haspopup="listbox"
onclick={() => popover?.togglePopover()}
{onkeydown}
tabindex="0">{value != null ? key(value).name : ''}</span
>
<div
class="skill-tip absolute top-2 flex-col px-2 shadow-lg open:flex"
style={`position-anchor: --anchor-${id}; position-area: bottom;`}
id="charaOptions"
role="listbox"
popover
bind:this={popover}
>
<input
class="my-2 min-h-8 rounded-md border pointer-coarse:min-h-12"
placeholder=" Search"
role="searchbox"
bind:value={search}
/>
<div class="max-h-72 overflow-y-scroll" bind:this={optionsContainer}>
{#if !required}
<div
class="h-8 w-full text-lg hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900"
role="option"
aria-selected={value == undefined}
tabindex="0"
data-pick-id=""
onmousedown={() => {
value = undefined;
search = '';
popover!.hidePopover();
}}
onfocus={() => (value = undefined)}
{onkeydown}
>
<span class="text-sm italic">Reset</span>
</div>
{/if}
{#each searched as c (key(c).id)}
{@const v = value != null ? key(value).id : 0}
{@const k = key(c)}
<div
class="h-8 w-full text-lg hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900"
role="option"
aria-selected={v === k.id}
tabindex="0"
data-pick-id={k.id}
onmousedown={() => {
value = c;
popover!.hidePopover();
}}
onfocus={() => (value = c)}
{onkeydown}
>
{#if option != null}
{@render option(c)}
{:else}
<span>{k.name}</span>
{/if}
</div>
{:else}
<div class="w-full h-8 text-lg italic">No matches.</div>
{/each}
</div>
</div>
</div>
+23 -18
View File
@@ -3,39 +3,39 @@
ABILITY_SCALE_NAME, ABILITY_SCALE_NAME,
ABILITY_TYPE_FORMAT, ABILITY_TYPE_FORMAT,
DURATION_SCALE_NAME, DURATION_SCALE_NAME,
skills,
Target, Target,
TARGET_FORMAT, TARGET_FORMAT,
tenThousandths, tenThousandths,
ZERO_SKILL,
type Ability, type Ability,
type Skill, type Skill,
} from './data/skill'; } from './data/skill';
import SkillIcon from './SkillIcon.svelte';
interface CommonProps { interface Props {
hint?: string; skill: Skill | null | undefined;
mention?: boolean; mention?: boolean;
} }
type Props = CommonProps & ({ skill: number; name?: never } | { name: string; skill?: never }); const ZERO_SKILL: Skill = {
skill_id: 0,
name: 'Skill data is loading...',
description: 'Skill data is still loading, or else something went wrong.',
group: 0,
rarity: 1,
group_rate: 1,
wit_check: false,
activations: [],
tags: [],
icon_id: 0,
};
let { hint, mention, skill, name }: Props = $props(); let { mention, skill }: Props = $props();
function splitCond(c: string): string[] { function splitCond(c: string): string[] {
return c.replaceAll('&', ' & ').split('@'); return c.replaceAll('&', ' & ').split('@');
} }
const s: Readonly<Skill> = $derived.by(() => { const s = $derived(skill ?? ZERO_SKILL);
const l =
skill != null ? skills.global.filter((s) => s.skill_id === skill) : skills.global.filter((s) => s.name.includes(name!));
if (name != null) {
console.warn(`skills specified as ${name} (${hint}):`, l);
}
if (l.length === 0) {
return ZERO_SKILL;
}
return l[0];
});
const activationClass = $derived(s.activations.length === 1 ? null : 'block my-2 border rounded-md'); const activationClass = $derived(s.activations.length === 1 ? null : 'block my-2 border rounded-md');
const spanClass = $derived(mention ? 'italic' : 'font-bold'); const spanClass = $derived(mention ? 'italic' : 'font-bold');
@@ -64,9 +64,13 @@
<span class="group relative inline-block hyphens-none"> <span class="group relative inline-block hyphens-none">
<span <span
class="skill-tip invisible absolute bottom-4 -left-1/2 flex w-80 flex-col rounded-lg border px-2 text-center opacity-0 shadow-lg transition-all group-hover:visible group-hover:bottom-6 group-hover:opacity-100" class="skill-tip invisible absolute bottom-4 -left-1/2 flex min-w-80 flex-col rounded-lg border px-2 pt-2 text-center opacity-0 shadow-lg transition-all group-hover:visible group-hover:bottom-6 group-hover:opacity-100"
role="tooltip" role="tooltip"
> >
<span class="block text-xl">
<SkillIcon icon_id={skill?.icon_id} size={32} class="inline" />
{s.name}
</span>
<span class="block border-b py-1 hyphens-auto"> <span class="block border-b py-1 hyphens-auto">
{s.description} {s.description}
</span> </span>
@@ -93,5 +97,6 @@
</span> </span>
{/each} {/each}
</span> </span>
<SkillIcon icon_id={skill?.icon_id} size={16} class="inline max-h-lh align-text-bottom" />
<span class={[spanClass, 'cursor-pointer']}>{s.name}</span> <span class={[spanClass, 'cursor-pointer']}>{s.name}</span>
</span> </span>
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
const sizes = [64, 32, 16] as const;
interface Props {
icon_id: number | null | undefined;
size: (typeof sizes)[number];
}
let { icon_id, size, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
const icon = $derived(icon_id ?? 0);
const avifs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/skill/${icon}/${sz}x${sz}.avif ${sz / size}x`)
.join(', '),
);
const pngs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/skill/${icon}/${sz}x${sz}.png ${sz / size}x`)
.join(', '),
);
const src = $derived(icon !== 0 ? `/img/skill/${icon}/${sizes[0]}x${sizes[0]}.png` : `/img/skill/10011`);
</script>
{#if icon !== 0}
<picture {...rest}>
<source srcset={avifs} type="image/avif" />
<source srcset={pngs} type="image/png" />
<img {src} alt={`Skill icon ID ${icon_id}`} class="inline" width={size} height={size} loading="lazy" />
</picture>
{:else}
<div class={`w-[${size}px] h-[${size}px]`}></div>
{/if}
+8 -10
View File
@@ -1,19 +1,17 @@
<script lang="ts"> <script lang="ts">
import { sparks } from '$lib/data/spark'; import { type Spark } from '$lib/data/spark';
interface Props { interface Props {
spark: number; spark: Spark | null | undefined;
region?: keyof typeof sparks;
} }
const { spark, region = 'global' }: Props = $props(); const { spark }: Props = $props();
const cur = $derived(sparks[region].find((s) => s.spark_id === spark));
const curClass = $derived.by(() => { const curClass = $derived.by(() => {
if (cur == null) { if (spark == null) {
return []; return [];
} }
switch (cur.type) { switch (spark.type) {
case 1: case 1:
return ['stat']; return ['stat'];
case 2: case 2:
@@ -32,17 +30,17 @@
return []; return [];
} }
}); });
const stars = $derived('★'.repeat(cur?.rarity ?? 0)); const stars = $derived('★'.repeat(spark?.rarity ?? 0));
</script> </script>
{#if cur != null} {#if spark != null}
<div <div
class={[ class={[
curClass, curClass,
'spark mx-1 flex items-center rounded-xl px-2 py-1 transition ease-in hover:shadow-md hover:ease-out motion-safe:duration-75 motion-safe:hover:-translate-y-0.5 dark:shadow-neutral-950', 'spark mx-1 flex items-center rounded-xl px-2 py-1 transition ease-in hover:shadow-md hover:ease-out motion-safe:duration-75 motion-safe:hover:-translate-y-0.5 dark:shadow-neutral-950',
]} ]}
> >
<span>{cur.name}</span> <span>{spark.name}</span>
<span class="ml-2 text-xl text-amber-800 text-shadow-md dark:text-amber-300">{stars}</span> <span class="ml-2 text-xl text-amber-800 text-shadow-md dark:text-amber-300">{stars}</span>
</div> </div>
{/if} {/if}
+12 -12
View File
@@ -55,39 +55,39 @@
let width = $state(0); let width = $state(0);
let height = $state(0); let height = $state(0);
let expand = $state(false); const xLabel = $derived(`Raw ${Stat[stat]}`);
const xLabel = $derived(Stat[stat]);
const xLines = $derived([xRule].flat(1)); const xLines = $derived([xRule].flat(1));
const xMin = $derived(xRange?.[0] ?? 200); const xMin = $derived(xRange?.[0] ?? 200);
const xMax = $derived.by(() => { const xMax = $derived.by(() => {
if (xRange?.[1] != null) { if (xRange?.[1] != null) {
return xRange[1]; return xRange[1];
} }
if (expand) { return 100 * Math.ceil(Math.max(2000, ...xLines) / 100);
return 2000;
}
return 100 * Math.ceil(Math.max(1200, ...xLines) / 100);
}); });
const xVal = $derived(d3.range(xMin, xMax + 1, 5)); // +1 because d3.range does not include the max const xVal = $derived(d3.range(xMin, xMax + 1, 5)); // +1 because d3.range does not include the max
const thrX = 1200; const thrX = 1200;
function adj(x: number): number {
return Math.min(x, 1200) + 0.5 * (Math.max(x, 1200) - 1200);
}
const series = $derived([y].flat(1).filter((s) => s != null)); const series = $derived([y].flat(1).filter((s) => s != null));
const areas = $derived([yArea].flat(1).filter((s) => s != null)); const areas = $derived([yArea].flat(1).filter((s) => s != null));
const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(x), label })))); const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(adj(x), x), label }))));
const areaVals = $derived(areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(x), y2: y2(x), label })))); const areaVals = $derived(
areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(adj(x), x), y2: y2(adj(x), x), label }))),
);
const yRange: [number, number] = $derived.by(() => { const yRange: [number, number] = $derived.by(() => {
if (range != null) { if (range != null) {
if (range.length === 2) { if (range.length === 2) {
return range; return range;
} }
return [range[0], expand ? range[2] : range[1]]; return [range[0], range[2]];
} }
const l = d3.min(vals, ({ y }) => y) ?? 0; const l = d3.min(vals, ({ y }) => y) ?? 0;
const r = d3.max(vals, ({ y }) => y) ?? 1; const r = d3.max(vals, ({ y }) => y) ?? 1;
return [l, r]; return [l, r];
}); });
const yLines = $derived(xLines.flatMap((x) => series.map(({ y, label }) => ({ y: y(x), label })))); const yLines = $derived(xLines.flatMap((x) => series.map(({ y, label }) => ({ y: y(adj(x), x), label }))));
const makeChart: Attachment = (el) => { const makeChart: Attachment = (el) => {
$effect(() => { $effect(() => {
@@ -122,7 +122,7 @@
Plot.line(areaVals, { x: 'x', y: 'y1', stroke: 'label' }), Plot.line(areaVals, { x: 'x', y: 'y1', stroke: 'label' }),
Plot.line(areaVals, { x: 'x', y: 'y2', stroke: 'label' }), Plot.line(areaVals, { x: 'x', y: 'y2', stroke: 'label' }),
Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }), Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
vals.length > 0 ? Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })) : null, vals.length > 0 ? Plot.tip(vals, Plot.pointerX({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })) : null,
], ],
}), }),
); );
+103
View File
@@ -0,0 +1,103 @@
import { resolve } from '$app/paths';
import type { ResolvedPathname } from '$app/types';
export interface Page {
route: ResolvedPathname;
name: string;
description: string;
}
/**
* All tool and doc pages in the site.
*/
export const PAGES = {
index: [
{
route: resolve('/race'),
name: 'Race Mechanics Tools',
description: 'Tools for calculating Umamusume race characteristics.',
},
{
route: resolve('/chara'),
name: 'Character Tools',
description: 'Tools for understanding Umamusume characters.',
},
{
route: resolve('/doc'),
name: 'Documents',
description: 'Articles about Umamusume, including research and editorials.',
},
] satisfies Page[],
race: [
{
route: resolve('/race/spurt'),
name: 'Spurt Speed',
description: 'Calculate target speed in the last spurt and compare to other distance aptitudes and running styles.',
},
{
route: resolve('/race/mspeed'),
name: 'Front Runner Mechanical Speed Comparator',
description: 'Compare spot struggle and lane combo to the effects of individual skills.',
},
{
route: resolve('/race/gate'),
name: 'Gate Calculator',
description: 'Calculate the importance of acceleration effects at the starting gate.',
},
] satisfies Page[],
chara: [
{
route: resolve('/chara/affinity'),
name: 'Affinity Details',
description: 'Details of why characters have the base compatibility numbers they have.',
},
{
route: resolve('/chara/convo'),
name: 'Lobby Conversations',
description: 'Check participants in lobby conversations and get recommendations on unlocking them quickly.',
},
] satisfies Page[],
doc: [
{
route: resolve('/doc/frbm'),
name: 'Front Runner Black Magic',
description: 'Detailed analysis of how front runners interact with race mechanics.',
},
{
route: resolve('/doc/lanecalc'),
name: "Gaikokujin's Guide to Lanecalc",
description: 'English language manual for 危険回避シミュ, or Lanecalc, for precise lane combo simulation.',
},
{
route: resolve('/doc/race'),
name: 'Race Mechanics Charts',
description:
"Interactive adaptation of KuromiAK's race mechanics documentation for easier visualization of how mechanics scale.",
},
{
route: resolve('/doc/spark'),
name: 'Spark Explorer',
description: 'View all possible inheritance effects of any spark.',
},
{
route: resolve('/doc/tagged-skills'),
name: 'Tagged Skills Explorer',
description: 'View skills matching the tags internal to the game.',
},
] satisfies Page[],
} as const;
/**
* Get the page object for a route.
* @param route Route ID for the page
* @returns Matching page, or an empty object if none match
*/
export function page(route: string | null): Partial<Page> {
for (const p of Object.values(PAGES)) {
const r = p.find((v) => v.route === route);
if (r != null) {
return r;
}
}
return {};
}
+1 -1
View File
@@ -18,7 +18,7 @@ const styleMap = {
Sentou: RunningStyle.PaceChaser, Sentou: RunningStyle.PaceChaser,
Sasi: RunningStyle.LateSurger, Sasi: RunningStyle.LateSurger,
Oikomi: RunningStyle.EndCloser, Oikomi: RunningStyle.EndCloser,
Oonige: RunningStyle.GreatEscape, Oonige: RunningStyle.Runaway,
} as const; } as const;
export interface ImportUma { export interface ImportUma {
+3 -3
View File
@@ -1,11 +1,11 @@
export interface ComputedSeries { export interface ComputedSeries {
y: (x: number) => number; y: (base: number, raw: number) => number;
label: string; label: string;
} }
export interface ComputedAreas { export interface ComputedAreas {
y1: (x: number) => number; y1: (base: number, raw: number) => number;
y2: (x: number) => number; y2: (base: number, raw: number) => number;
label: string; label: string;
} }
+353 -5
View File
@@ -1,5 +1,3 @@
import globalJSON from '../../../../global/affinity.json';
/** /**
* Precomputed character pair and trio affinity. * Precomputed character pair and trio affinity.
*/ */
@@ -24,12 +22,362 @@ export interface Affinity {
affinity: number; affinity: number;
} }
export const affinity = { export async function affinity(): Promise<Affinity[]> {
global: globalJSON as Affinity[], const resp = await fetch('/api/global/affinity');
} as const; return resp.json();
}
export function lookup(aff: Affinity[], chara_a: number, chara_b: number, chara_c?: number): number { export function lookup(aff: Affinity[], chara_a: number, chara_b: number, chara_c?: number): number {
const [a, b, c] = [chara_a, chara_b, chara_c ?? Infinity].sort((a, b) => a - b); const [a, b, c] = [chara_a, chara_b, chara_c ?? Infinity].sort((a, b) => a - b);
const r = aff.find((v) => v.chara_a === a && v.chara_b === b && (v.chara_c ?? Infinity) === c); const r = aff.find((v) => v.chara_a === a && v.chara_b === b && (v.chara_c ?? Infinity) === c);
return r?.affinity ?? 0; return r?.affinity ?? 0;
} }
/**
* Individual affinity entry.
*/
export interface AffinityDetail {
/** Character to which the entry applies. */
chara_id: number;
/** Succession relation that the character has. */
relation: number;
/** Value of the succession relation. */
affinity: number;
}
export async function affinityDetail(): Promise<AffinityDetail[]> {
const resp = await fetch('/api/global/affinity-detail');
return resp.json();
}
export interface AffinityRelationDescription {
name: string;
note?: string;
}
export const AFFINITY_RELATION_DESCRIPTIONS = new Map<number, AffinityRelationDescription>([
[2001, { name: 'Shared Parent', note: 'Sunday Silence' }],
[2002, { name: 'Shared Parent', note: 'Tony Bin' }],
[2003, { name: 'Shared Parent', note: 'Sakura Yutaka O' }],
[2004, { name: 'Shared Parent', note: 'Mejiro Ryan' }],
[2005, { name: 'Shared Parent' }],
[2006, { name: 'Shared Parent' }],
[2007, { name: 'Shared Parent', note: 'Symboli Rudolf' }],
[2008, { name: 'Shared Parent', note: 'Tosho Boy' }],
[2009, { name: 'Shared Parent', note: "Brian's Time" }],
[2010, { name: 'Shared Parent', note: 'Gold Allure' }],
[2011, { name: 'Shared Parent', note: 'Soccer Boy' }],
[2101, { name: 'Shared Grandparent', note: 'Sunday Silence (grandsire)' }],
[2102, { name: 'Shared Grandparent', note: 'Maruzensky (damsire)' }],
[2103, { name: 'Shared Grandparent', note: 'Roberto (grandsire)' }],
[2104, { name: 'Shared Grandparent', note: 'Blushing Groom (damsire)' }],
[2105, { name: 'Shared Grandparent', note: 'Tesco Boy (damsire)' }],
[2106, { name: 'Shared Grandparent', note: 'Northern Taste (damsire)' }],
[2107, { name: 'Shared Grandparent', note: 'Royal Ski (damsire)' }],
[2108, { name: 'Shared Grandparent', note: 'Never Beat (damsire)' }],
[2109, { name: 'Shared Grandparent', note: 'Affirmed (damsire)' }],
[2110, { name: 'Shared Grandparent', note: 'Sancy (damsire)' }],
[2111, { name: 'Shared Grandparent', note: 'Last Tycoon (grandsire)' }],
[2201, { name: 'Parent/Child' }],
[2202, { name: 'Parent/Child' }],
[2203, { name: 'Grandparent/Grandchild' }],
[2204, { name: 'Parent/Child' }],
[2205, { name: 'Grandparent/Grandchild' }],
[2206, { name: 'Parent/Child' }],
[2208, { name: 'Parent/Child' }],
[2209, { name: 'Parent/Child' }],
[2210, { name: 'Parent/Child' }],
[2211, { name: 'Grandparent/Grandchild' }],
[2212, { name: 'Parent/Child' }],
[2213, { name: 'Parent/Child' }],
[2214, { name: 'Grandparent/Grandchild' }],
[2215, { name: 'Grandparent/Grandchild' }],
[2216, { name: 'Grandparent/Grandchild' }],
[2217, { name: 'Grandparent/Grandchild' }],
[2218, { name: 'Parent/Child' }],
[2301, { name: 'Grandparent/Grandchild' }],
[2302, { name: 'Grandparent/Grandchild' }],
[2303, { name: 'Grandparent/Grandchild' }],
[2401, { name: 'Racing Generation', note: 'Pre-1980' }],
[2402, { name: 'Racing Generation', note: '1983-86' }],
[2403, { name: 'Racing Generation', note: '1987-90' }],
[2404, { name: 'Racing Generation', note: '1990-93' }],
[2405, { name: 'Racing Generation', note: '1995-97' }],
[2406, { name: 'Racing Generation', note: '1997-99' }],
[2407, { name: 'Racing Generation', note: '1998-2004' }],
[2408, { name: 'Racing Generation', note: '2001-04' }],
[2409, { name: 'Racing Generation', note: '2006-10' }],
[2410, { name: 'Racing Generation' }],
[2411, { name: 'Racing Generation' }],
[2412, { name: 'Racing Generation' }],
[2413, { name: 'Racing Generation' }],
[2501, { name: 'G1 Winner', note: 'February Stakes' }],
[2502, { name: 'G1 Winner', note: 'Takamatsunomiya Kinen' }],
[2503, { name: 'G1 Winner', note: 'Osaka Hai' }],
[2504, { name: 'G1 Winner', note: 'Oka Sho' }],
[2505, { name: 'G1 Winner', note: 'Satsuki Sho' }],
[2506, { name: 'G1 Winner', note: 'Tenno Sho (Spring)' }],
[2507, { name: 'G1 Winner', note: 'NHK Mile Cup' }],
[2508, { name: 'G1 Winner', note: 'Victoria Mile' }],
[2509, { name: 'G1 Winner', note: 'Japanese Oaks' }],
[2510, { name: 'G1 Winner', note: 'Tokyo Yushun (Japanese Derby)' }],
[2511, { name: 'G1 Winner', note: 'Yasuda Kinen' }],
[2512, { name: 'G1 Winner', note: 'Takarazuka Kinen' }],
[2513, { name: 'G1 Winner', note: 'Sprinters Stakes' }],
[2514, { name: 'G1 Winner', note: 'Shuka Sho' }],
[2515, { name: 'G1 Winner', note: 'Kikuka Sho' }],
[2516, { name: 'G1 Winner', note: 'Tenno Sho (Autumn)' }],
[2517, { name: 'G1 Winner', note: 'Queen Elizabeth II Cup' }],
[2518, { name: 'G1 Winner', note: 'Mile Championship' }],
[2519, { name: 'G1 Winner', note: 'Japan Cup' }],
[2520, { name: 'G1 Winner', note: 'Champions Cup' }],
[2521, { name: 'G1 Winner', note: 'Hanshin Juvenile Fillies' }],
[2522, { name: 'G1 Winner', note: 'Asahi Hai Futurity Stakes' }],
[2523, { name: 'G1 Winner', note: 'Arima Kinen' }],
[2524, { name: 'G1 Winner', note: 'Hopeful Stakes' }],
[2525, { name: 'G1 Winner', note: 'Teio Sho' }],
[2526, { name: 'G1 Winner', note: 'Japan Dirt Derby' }],
[2527, { name: 'G1 Winner', note: 'JBC Ladies Classic' }],
[2528, { name: 'G1 Winner', note: 'JBC Sprint' }],
[2529, { name: 'G1 Winner', note: 'JBC Classic' }],
[2530, { name: 'G1 Winner', note: 'Tokyo Daishoten' }],
[2531, { name: 'G1 Winner', note: 'Kawasaki Kinen' }],
[2532, { name: 'G1 Winner', note: 'Mile Championship Nambu Hai' }],
[2533, { name: 'G1 Winner', note: 'Kashiwa Kinen' }],
[2534, { name: 'G1 Winner', note: 'Zen-Nippon Junior Yushun' }],
[2601, { name: 'Stallion' }],
[2602, { name: 'Mare' }],
[2702, { name: '7 G1 + Triple Crown winner' }],
[2801, { name: 'Classic Year', note: '1986' }],
[2802, { name: 'Classic Year', note: '1987' }],
[2803, { name: 'Classic Year', note: '1990' }],
[2804, { name: 'Classic Year', note: '1991' }],
[2805, { name: 'Classic Year', note: '1992' }],
[2806, { name: 'Classic Year', note: '1993' }],
[2807, { name: 'Classic Year', note: '1994' }],
[2808, { name: 'Classic Year', note: '1995' }],
[2809, { name: 'Classic Year', note: '1996' }],
[2810, { name: 'Classic Year', note: '1997' }],
[2811, { name: 'Classic Year', note: '1998' }],
[2812, { name: 'Classic Year', note: '1999' }],
[2813, { name: 'Classic Year', note: '2000' }],
[2814, { name: 'Classic Year', note: '2001' }],
[2815, { name: 'Classic Year', note: '2007' }],
[2816, { name: 'Classic Year', note: '2009' }],
[2817, { name: 'Classic Year', note: '2010' }],
[2818, { name: 'Classic Year' }],
[2819, { name: 'Classic Year' }],
[2820, { name: 'Classic Year' }],
[2821, { name: 'Classic Year' }],
[2822, { name: 'Classic Year', note: '2002' }],
[2823, { name: 'Classic Year' }],
[2824, { name: 'Classic Year' }],
[2825, { name: 'Classic Year' }],
[2826, { name: 'Classic Year' }],
[2827, { name: 'Classic Year' }],
[2828, { name: 'Classic Year' }],
[2829, { name: 'Classic Year' }],
[2830, { name: 'Classic Year' }],
[2831, { name: 'Classic Year' }],
[2832, { name: 'Classic Year' }],
[2833, { name: 'Classic Year' }],
[3201, { name: 'Birth Month', note: 'January' }],
[3202, { name: 'Birth Month', note: 'February' }],
[3203, { name: 'Birth Month', note: 'March' }],
[3204, { name: 'Birth Month', note: 'April' }],
[3205, { name: 'Birth Month', note: 'May' }],
[3206, { name: 'Birth Month', note: 'June' }],
[3301, { name: 'No G1 Wins' }],
[3501, { name: 'Classic Triple Crown Runner' }],
[3502, { name: 'Triple Tiara Runner' }],
[3503, { name: 'Spring Triple Crown Runner' }],
[3504, { name: 'Autumn Triple Crown Runner' }],
[101, { name: 'Tracen Class' }],
[102, { name: 'Tracen Class' }],
[103, { name: 'Tracen Class' }],
[104, { name: 'Tracen Class' }],
[105, { name: 'Tracen Class' }],
[106, { name: 'Tracen Class' }],
[107, { name: 'Tracen Class' }],
[201, { name: 'Residence', note: 'Miho Dorm' }],
[202, { name: 'Residence', note: 'Ritto Dorm' }],
[203, { name: 'Residence', note: 'Lives Alone' }],
[204, { name: 'Residence' }],
[205, { name: 'Residence', note: 'Not In Dorm' }],
[301, { name: 'Roommate' }],
[302, { name: 'Roommate' }],
[303, { name: 'Roommate' }],
[304, { name: 'Roommate' }],
[305, { name: 'Roommate' }],
[306, { name: 'Roommate' }],
[307, { name: 'Roommate' }],
[308, { name: 'Roommate' }],
[309, { name: 'Roommate' }],
[310, { name: 'Roommate' }],
[311, { name: 'Roommate' }],
[312, { name: 'Roommate' }],
[313, { name: 'Roommate' }],
[314, { name: 'Roommate' }],
[315, { name: 'Roommate' }],
[316, { name: 'Roommate' }],
[317, { name: 'Roommate' }],
[318, { name: 'Roommate' }],
[319, { name: 'Roommate' }],
[320, { name: 'Roommate' }],
[321, { name: 'Roommate' }],
[322, { name: 'Roommate' }],
[323, { name: 'Roommate' }],
[324, { name: 'Roommate' }],
[325, { name: 'Roommate' }],
[326, { name: 'Roommate' }],
[327, { name: 'Roommate' }],
[328, { name: 'Roommate' }],
[329, { name: 'Roommate' }],
[330, { name: 'Roommate' }],
[331, { name: 'Roommate' }],
[332, { name: 'Roommate' }],
[333, { name: 'Roommate' }],
[334, { name: 'Roommate' }],
[335, { name: 'Roommate' }],
[336, { name: 'Roommate' }],
[337, { name: 'Roommate' }],
[338, { name: 'Roommate' }],
[339, { name: 'Roommate' }],
[340, { name: 'Roommate' }],
[341, { name: 'Roommate' }],
[342, { name: 'Roommate' }],
[343, { name: 'Roommate' }],
[344, { name: 'Roommate' }],
[345, { name: 'Roommate' }],
[346, { name: 'Roommate' }],
[347, { name: 'Roommate' }],
[348, { name: 'Roommate' }],
[349, { name: 'Roommate' }],
[350, { name: 'Roommate' }],
[351, { name: 'Roommate' }],
[352, { name: 'Roommate' }],
[353, { name: 'Roommate' }],
[354, { name: 'Roommate' }],
[355, { name: 'Roommate' }],
[356, { name: 'Roommate' }],
[357, { name: 'Roommate' }],
[358, { name: 'Roommate' }],
[359, { name: 'Roommate' }],
[360, { name: 'Roommate' }],
[361, { name: 'Roommate' }],
[362, { name: 'Roommate' }],
[363, { name: 'Roommate' }],
[364, { name: 'Roommate' }],
[365, { name: 'Roommate' }],
[366, { name: 'Roommate' }],
[367, { name: 'Roommate' }],
[368, { name: 'Roommate' }],
[369, { name: 'Roommate' }],
[370, { name: 'Roommate' }],
[371, { name: 'Roommate' }],
[372, { name: 'Roommate' }],
[373, { name: 'Roommate' }],
[374, { name: 'Roommate' }],
[375, { name: 'Roommate' }],
[376, { name: 'Roommate' }],
[377, { name: 'Roommate' }],
[378, { name: 'Roommate' }],
[379, { name: 'Roommate' }],
[380, { name: 'Roommate' }],
[381, { name: 'Roommate' }],
[382, { name: 'Roommate' }],
[383, { name: 'Roommate' }],
[384, { name: 'Roommate' }],
[385, { name: 'Roommate' }],
[386, { name: 'Roommate' }],
[387, { name: 'Roommate' }],
[388, { name: 'Roommate' }],
[401, { name: 'Clique', note: 'Golden Generation' }],
[402, { name: 'Clique' }],
[403, { name: 'Clique' }],
[404, { name: 'Clique' }],
[405, { name: 'Clique' }],
[406, { name: 'Clique', note: 'Old People' }],
[407, { name: 'Clique' }],
[408, { name: 'Clique' }],
[409, { name: 'Clique' }],
[410, { name: 'Clique' }],
[411, { name: 'Clique' }],
[412, { name: 'Clique' }],
[413, { name: 'Clique' }],
[414, { name: 'Clique' }],
[415, { name: 'Clique' }],
[416, { name: 'Clique' }],
[417, { name: 'Clique' }],
[418, { name: 'Clique' }],
[419, { name: 'Clique' }],
[420, { name: 'Clique' }],
[421, { name: 'Clique' }],
[422, { name: 'Clique' }],
[423, { name: 'Clique' }],
[424, { name: 'Clique' }],
[501, { name: 'Story Group', note: 'Student Council' }],
[502, { name: 'Story Group', note: 'Golden Generation' }],
[503, { name: 'Story Group', note: 'Mejiro Family' }],
[504, { name: 'Story Group', note: 'Rich People' }],
[505, { name: 'Story Group', note: 'Dorm Leaders' }],
[506, { name: 'Story Group', note: 'Heisei Big Three' }],
[507, { name: 'Story Group', note: 'BNW' }],
[511, { name: 'Story Group', note: '"Sakura"' }],
[512, { name: 'Story Group' }],
[513, { name: 'Story Group', note: 'Team Canopus' }],
[514, { name: 'Story Group', note: 'Runaway Bakas' }],
[515, { name: 'Story Group', note: 'Kita and Dia' }],
[516, { name: 'Story Group', note: 'McQueen and Dia' }],
[517, { name: 'Story Group', note: 'Teio and Kitasan' }],
[518, { name: 'Story Group', note: 'Notable Front Runners' }],
[519, { name: 'Story Group' }],
[520, { name: 'Story Group', note: 'Notable Dirt Runners' }],
[521, { name: 'Story Group' }],
[522, { name: 'Story Group', note: 'Satono Family' }],
[523, { name: 'Story Group', note: 'Team Sirius' }],
[524, { name: 'Story Group', note: 'Team Rigil' }],
[525, { name: 'Story Group', note: 'Team Spica' }],
[526, { name: 'Story Group', note: 'Notable Runaways' }],
[3505, { name: 'Ran on Dirt' }],
[3506, { name: 'Overseas' }],
[3508, { name: 'Ran a Sprint' }],
[2901, { name: 'Strategy', note: 'Front Runner' }],
[2902, { name: 'Strategy', note: 'Pace Chaser' }],
[2903, { name: 'Strategy', note: 'Late Surger' }],
[2904, { name: 'Strategy', note: 'End Closer' }],
[3001, { name: 'Distance', note: 'Sprint' }],
[3002, { name: 'Distance', note: 'Mile' }],
[3003, { name: 'Distance', note: 'Medium' }],
[3004, { name: 'Distance', note: 'Long' }],
[3101, { name: 'Surface', note: 'Turf' }],
[3102, { name: 'Surface', note: 'Dirt' }],
[508, { name: 'Haru Urara', note: 'Haru Urara' }],
[509, { name: 'Haru Urara', note: 'Haru Urara' }],
[510, { name: 'Haru Urara', note: 'Haru Urara' }],
[527, { name: 'Haru Urara', note: 'Haru Urara' }],
[528, { name: 'Haru Urara', note: 'Haru Urara' }],
[529, { name: 'Haru Urara', note: 'Haru Urara' }],
[530, { name: 'Haru Urara', note: 'Haru Urara' }],
[531, { name: 'Haru Urara', note: 'Haru Urara' }],
[532, { name: 'Haru Urara', note: 'Haru Urara' }],
[533, { name: 'Haru Urara', note: 'Haru Urara' }],
[534, { name: 'Haru Urara', note: 'Haru Urara' }],
[535, { name: 'Haru Urara', note: 'Haru Urara' }],
[536, { name: 'Haru Urara', note: 'Haru Urara' }],
[537, { name: 'Haru Urara', note: 'Haru Urara' }],
[538, { name: 'Haru Urara', note: 'Haru Urara' }],
[539, { name: 'Haru Urara', note: 'Haru Urara' }],
[540, { name: 'Haru Urara', note: 'Haru Urara' }],
[541, { name: 'Haru Urara', note: 'Haru Urara' }],
[542, { name: 'Haru Urara', note: 'Haru Urara' }],
[543, { name: 'Haru Urara', note: 'Haru Urara' }],
[544, { name: 'Haru Urara', note: 'Haru Urara' }],
[545, { name: 'Haru Urara', note: 'Haru Urara' }],
[546, { name: 'Haru Urara', note: 'Haru Urara' }],
[547, { name: 'Haru Urara', note: 'Haru Urara' }],
[548, { name: 'Haru Urara', note: 'Haru Urara' }],
[549, { name: 'Haru Urara', note: 'Haru Urara' }],
[550, { name: 'Haru Urara', note: 'Haru Urara' }],
[551, { name: 'Haru Urara', note: 'Haru Urara' }],
]);
+7 -8
View File
@@ -1,5 +1,4 @@
import type { RegionalName } from '$lib/regional-name'; import type { RegionalName } from '$lib/regional-name';
import globalJSON from '../../../../global/character.json';
/** /**
* Character definitions. * Character definitions.
@@ -16,11 +15,11 @@ export interface Character {
name: string; name: string;
} }
export const character = { export async function character(): Promise<Character[]> {
global: globalJSON as Character[], const resp = await fetch('/api/global/character');
}; return resp.json();
}
export const charaNames = globalJSON.reduce( export function charaNames(charas: Character[]) {
(m, c) => m.set(c.chara_id, { en: c.name }), return charas.reduce((m, c) => m.set(c.chara_id, { en: c.name }), new Map<number, RegionalName>());
new Map<Character['chara_id'], RegionalName>(), }
);
+18 -22
View File
@@ -1,5 +1,4 @@
import type { RegionalName } from '$lib/regional-name'; import type { RegionalName } from '$lib/regional-name';
import globalJSON from '../../../../global/conversation.json';
/** /**
* Lobby conversation data. * Lobby conversation data.
@@ -17,10 +16,6 @@ export interface Conversation {
* Location ID of the conversation. * Location ID of the conversation.
*/ */
location: 110 | 120 | 130 | 210 | 220 | 310 | 410 | 420 | 430 | 510 | 520 | 530; location: 110 | 120 | 130 | 210 | 220 | 310 | 410 | 420 | 430 | 510 | 520 | 530;
/**
* English name of the location, for convenience.
*/
location_name: string;
/** /**
* First character in the conversation. * First character in the conversation.
* Not necessarily equal to chara_id. * Not necessarily equal to chara_id.
@@ -40,16 +35,17 @@ export interface Conversation {
condition_type: 0 | 1 | 2 | 3 | 4; condition_type: 0 | 1 | 2 | 3 | 4;
} }
export const conversation = { export async function conversation(): Promise<Conversation[]> {
global: globalJSON as Conversation[], const resp = await fetch('/api/global/conversation');
}; return resp.json();
}
export const byChara = { export function byChara(convos: Conversation[]) {
global: globalJSON.reduce( return convos.reduce(
(m, c) => m.set(c.chara_id, (m.get(c.chara_id) ?? []).concat(c as Conversation)), (m, c) => m.set(c.chara_id, (m.get(c.chara_id) ?? []).concat(c as Conversation)),
new Map<Conversation['chara_id'], Conversation[]>(), new Map<number, Conversation[]>(),
), );
}; }
export const locations: Record<Conversation['location'], { name: RegionalName; group: 1 | 2 | 3 | 4 | 5 }> = { export const locations: Record<Conversation['location'], { name: RegionalName; group: 1 | 2 | 3 | 4 | 5 }> = {
110: { name: { en: 'right side front' }, group: 1 }, 110: { name: { en: 'right side front' }, group: 1 },
@@ -74,12 +70,12 @@ function locCharas(convos: Conversation[], locGroup: 1 | 2 | 3 | 4 | 5) {
return [...m].toSorted((a, b) => b[1] - a[1]); // descending return [...m].toSorted((a, b) => b[1] - a[1]); // descending
} }
export const groupPopulars = { export function groupPopulars(convos: Conversation[]) {
global: { return {
1: locCharas(conversation.global, 1), 1: locCharas(convos, 1),
2: locCharas(conversation.global, 2), 2: locCharas(convos, 2),
3: locCharas(conversation.global, 3), 3: locCharas(convos, 3),
4: locCharas(conversation.global, 4), 4: locCharas(convos, 4),
5: locCharas(conversation.global, 5), 5: locCharas(convos, 5),
}, };
}; }
+69 -20
View File
@@ -1,6 +1,3 @@
import skillGlobal from '../../../../global/skill.json';
import groupGlobal from '../../../../global/skill-group.json';
/** /**
* Skill data. * Skill data.
*/ */
@@ -51,6 +48,10 @@ export interface Skill {
* SP cost to purchase the skill, if applicable. * SP cost to purchase the skill, if applicable.
*/ */
sp_cost?: number; sp_cost?: number;
/**
* Numeric tags applied to the skill, for effects which select skill types.
*/
tags: number[];
/** /**
* Skill icon ID. * Skill icon ID.
*/ */
@@ -136,10 +137,13 @@ export enum AbilityType {
Frenzy = 13, Frenzy = 13,
AddGateDelay = 14, AddGateDelay = 14,
CurrentSpeed = 21, CurrentSpeed = 21,
CurrentSpeedWithDecel = 22,
TargetSpeed = 27, TargetSpeed = 27,
LaneSpeed = 28, LaneSpeed = 28,
RushedChance = 29,
Accel = 31, Accel = 31,
LaneChange = 35, LaneChange = 35,
ActivateRare = 37,
} }
export const ABILITY_TYPE_FORMAT: Readonly<Record<AbilityType, (v: number) => string>> = { export const ABILITY_TYPE_FORMAT: Readonly<Record<AbilityType, (v: number) => string>> = {
@@ -155,10 +159,13 @@ export const ABILITY_TYPE_FORMAT: Readonly<Record<AbilityType, (v: number) => st
[AbilityType.Frenzy]: (v) => tenThousandths(v) + 's Frenzy', [AbilityType.Frenzy]: (v) => tenThousandths(v) + 's Frenzy',
[AbilityType.AddGateDelay]: (v) => plusBonus(v) + 's Gate Delay', [AbilityType.AddGateDelay]: (v) => plusBonus(v) + 's Gate Delay',
[AbilityType.CurrentSpeed]: (v) => plusBonus(v) + ' m/s Current Speed', [AbilityType.CurrentSpeed]: (v) => plusBonus(v) + ' m/s Current Speed',
[AbilityType.CurrentSpeedWithDecel]: (v) => plusBonus(v) + ' m/s Current Speed',
[AbilityType.TargetSpeed]: (v) => plusBonus(v) + ' m/s Target Speed', [AbilityType.TargetSpeed]: (v) => plusBonus(v) + ' m/s Target Speed',
[AbilityType.LaneSpeed]: (v) => plusBonus(v) + ' CW/s Lane Change Speed', [AbilityType.LaneSpeed]: (v) => plusBonus(v) + ' CW/s Lane Change Speed',
[AbilityType.RushedChance]: (v) => plusBonus(v) + '% Rushed Chance',
[AbilityType.Accel]: (v) => plusBonus(v) + ' m/s² Acceleration', [AbilityType.Accel]: (v) => plusBonus(v) + ' m/s² Acceleration',
[AbilityType.LaneChange]: (v) => 'Target Lane = ' + tenThousandths(v) + ' CW', [AbilityType.LaneChange]: (v) => 'Target Lane = ' + tenThousandths(v) + ' CW',
[AbilityType.ActivateRare]: (v) => 'Activate Rare Skills ×' + tenThousandths(v),
} as const; } as const;
export enum AbilityValueUsage { export enum AbilityValueUsage {
@@ -323,22 +330,64 @@ export interface SkillGroup {
skill_bad?: number; skill_bad?: number;
} }
export const skills = { export async function skills(): Promise<Skill[]> {
global: skillGlobal as Skill[], const resp = await fetch('/api/global/skill');
} as const; return resp.json();
}
export const skillGroups = { export async function skillGroups(): Promise<SkillGroup[]> {
global: groupGlobal as SkillGroup[], const resp = await fetch('/api/global/skill-group');
} as const; return resp.json();
}
export const ZERO_SKILL: Readonly<Skill> = { export const TAGS = [
skill_id: 0, { name: 'Front Runner', tag: 101 },
name: 'invalid skill', { name: 'Pace Chaser', tag: 102 },
description: 'an invalid skill was specified', { name: 'Late Surger', tag: 103 },
group: 0, { name: 'End Closer', tag: 104 },
rarity: 1, { name: 'Sprint', tag: 201 },
group_rate: 1, { name: 'Mile', tag: 202 },
wit_check: false, { name: 'Medium', tag: 203 },
activations: [], { name: 'Long', tag: 204 },
icon_id: 0, { name: 'Early Race', tag: 301 },
} as const; { name: 'Mid Race', tag: 302 },
{ name: 'Late Race', tag: 303 },
{ name: 'Speed Stat or Velocity', tag: 401 },
{ name: 'Stamina Stat or HP', tag: 402 },
{ name: 'Power Stat or Acceleration', tag: 403 },
{ name: 'Guts Stat', tag: 404 },
{ name: 'Wit Stat or Vision', tag: 405 },
{ name: 'Debuff', tag: 406 },
{ name: 'Gate Delay', tag: 407 },
{ name: 'Turf', tag: 501 },
{ name: 'Dirt', tag: 502 },
{ name: 'Ground Condition Passive', tag: 601 },
{ name: 'Weather Passive', tag: 602 },
{ name: 'Season Passive', tag: 603 },
{ name: 'Racecourse Passive', tag: 604 },
{ name: 'Time of Day Passive', tag: 605 },
{ name: 'Exchange Race Passive', tag: 606 },
{ name: 'Distance Passive', tag: 607 },
{ name: 'Corner-Based Passive', tag: 608 },
{ name: 'Gate Position Passive', tag: 609 },
{ name: 'Ground Type Passive', tag: 610 },
{ name: 'Popularity Passive', tag: 611 },
{ name: 'Running Style Passive', tag: 612 },
{ name: "Passive Depending on Other Horses' Styles or Skills", tag: 613 },
{ name: 'Base Stat Threshold Passive', tag: 614 },
{ name: 'Mood Passive', tag: 615 },
{ name: 'URA', tag: 801 },
{ name: 'Unity Cup', tag: 802 },
{ name: 'Trackblazer', tag: 804 },
// { name: 'Our Grand Concert', tag: ? },
// { name: 'Grand Masters', tag: ? },
// { name: "Project L'Arc", tag: ? },
// { name: 'U.A.F.', tag: ? },
// { name: 'Great Food Festival', tag: ? },
// { name: 'Run, Mecha Umamusume!', tag: ? },
// { name: 'The Twinkle Legends', tag: ? },
// { name: 'Design Your Island', tag: ? },
// { name: 'Yukoma Hot Springs', tag: ? },
// { name: 'Beyond Dreams', tag: ? },
// { name: 'Tracen-Ken', tag: ? },
] as const;
+126 -11
View File
@@ -1,5 +1,3 @@
import globalJSON from '../../../../global/spark.json';
/** /**
* Sparks, or succession factors. * Sparks, or succession factors.
*/ */
@@ -30,24 +28,141 @@ export interface Spark {
* Spark type. * Spark type.
* Roughly the spark color, with extra subdivisions for white sparks. * Roughly the spark color, with extra subdivisions for white sparks.
*/ */
type: 1 | 2 | 5 | 4 | 6 | 7 | 10 | 8 | 11 | 9 | 3; type: Type;
/** /**
* Possible effects applied by the spark during inspiration. * Possible effects applied by the spark during inspiration.
* A random element is selected from this list according to unknown * A random element is selected from this list according to unknown
* distributions, then all effects in that selection are applied. * distributions, then all effects in that selection are applied.
*/ */
effects: SparkEffect[][]; effects: Effect[][];
} }
/**
* Spark types.
* Roughly the spark color, with extra subdivisions for white sparks.
*/
export enum Type {
Stat = 1,
Aptitude = 2,
Unique = 3,
Skill = 4,
Race = 5,
Scenario = 6,
CarnivalBonus = 7,
Distance = 8,
Hidden = 9,
Surface = 10,
Style = 11,
}
/** Mapping of spark types to their names. */
export const TYPE_NAMES: Readonly<Record<Type, string>> = {
[Type.Stat]: 'Stat (Blue)',
[Type.Aptitude]: 'Aptitude (Pink)',
[Type.Unique]: 'Unique (Green)',
[Type.Skill]: 'Skill',
[Type.Race]: 'Race',
[Type.Scenario]: 'Scenario',
[Type.CarnivalBonus]: 'Carnival Bonus (Scarlet)',
[Type.Distance]: 'Distance (White)',
[Type.Hidden]: 'Hidden (White)',
[Type.Surface]: 'Surface (White)',
[Type.Style]: 'Style (White)',
};
/**
* Spark effect types.
*/
export enum Target {
Speed = 1,
Stamina = 2,
Power = 3,
Guts = 4,
Wit = 5,
SkillPoints = 6,
RandomStat = 7,
Turf = 11,
Dirt = 12,
FrontRunner = 21,
PaceChaser = 22,
LateSurger = 23,
EndCloser = 24,
Sprint = 31,
Mile = 32,
Medium = 33,
Long = 34,
Skill = 41,
CarnivalBonus = 51,
SpeedCap = 61,
StaminaCap = 62,
PowerCap = 63,
GutsCap = 64,
WitCap = 65,
}
/**
* Mapping of spark target types to their names.
* Note that Skill does not include the skill name.
*/
export const TARGET_NAMES: Readonly<Record<Target, string>> = {
[Target.Speed]: 'Speed',
[Target.Stamina]: 'Stamina',
[Target.Power]: 'Power',
[Target.Guts]: 'Guts',
[Target.Wit]: 'Wit',
[Target.SkillPoints]: 'SP',
[Target.RandomStat]: 'Random Stat',
[Target.Turf]: 'Turf aptitude',
[Target.Dirt]: 'Dirt aptitude',
[Target.FrontRunner]: 'Front Runner aptitude',
[Target.PaceChaser]: 'Pace Chaser aptitude',
[Target.LateSurger]: 'Late Surger aptitude',
[Target.EndCloser]: 'End Closer aptitude',
[Target.Sprint]: 'Sprint aptitude',
[Target.Mile]: 'Mile aptitude',
[Target.Medium]: 'Medium aptitude',
[Target.Long]: 'Long aptitude',
[Target.Skill]: 'hint level',
[Target.CarnivalBonus]: 'Carnival Bonus level',
[Target.SpeedCap]: 'Speed cap',
[Target.StaminaCap]: 'Stamina cap',
[Target.PowerCap]: 'Power cap',
[Target.GutsCap]: 'Guts cap',
[Target.WitCap]: 'Wit cap',
};
/** /**
* Effects that a spark can apply. * Effects that a spark can apply.
*/ */
export interface SparkEffect { export interface Effect {
target: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 41 | 51 | 61 | 62 | 63 | 64 | 65; target: Target;
value1?: number; value1: number;
value2: number; value2?: number;
} }
export const sparks = { export async function sparks(): Promise<Spark[]> {
global: globalJSON as Spark[], const resp = await fetch('/api/global/spark');
} as const; return resp.json();
}
export interface SparkGroup {
spark_group: number;
name: string;
description: string;
type: Type;
effects: Effect[][];
spark_ids: number[];
}
export function byGroup(sparks: Spark[]): SparkGroup[] {
const r = new Map<number, SparkGroup>();
for (const { spark_id, name, description, spark_group, type, effects } of sparks) {
let g = r.get(spark_group);
if (g == null) {
g = { spark_group, name, description, type, effects, spark_ids: [] };
r.set(spark_group, g);
}
g.spark_ids.push(spark_id);
}
return [...r.values()];
}
+4 -5
View File
@@ -1,5 +1,3 @@
import globalJSON from '../../../../global/uma.json';
/** /**
* Uma or character card definitions. * Uma or character card definitions.
*/ */
@@ -70,6 +68,7 @@ export interface Uma {
export type AptitudeLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; export type AptitudeLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
export const uma = { export async function uma(): Promise<Uma[]> {
global: globalJSON as Uma[], const resp = await fetch('/api/global/uma');
}; return resp.json();
}
+5 -6
View File
@@ -1,6 +1,3 @@
import { affinity, lookup } from './data/affinity';
import { uma, type Uma } from './data/uma';
/** /**
* A legacy, or parents and grandparents. * A legacy, or parents and grandparents.
* Defined as an applicative over generic types to reduce the API surface. * Defined as an applicative over generic types to reduce the API surface.
@@ -41,9 +38,9 @@ export interface Veteran {
saddles: number[]; saddles: number[];
} }
function findUma(umas: Uma[], u: number): Uma | null { // function findUma(umas: Uma[], u: number): Uma | null {
return umas.find((v) => v.chara_card_id === u) ?? null; // return umas.find((v) => v.chara_card_id === u) ?? null;
} // }
/** /**
* Compute individual affinities for a legacy using the global region ruleset. * Compute individual affinities for a legacy using the global region ruleset.
@@ -51,6 +48,7 @@ function findUma(umas: Uma[], u: number): Uma | null {
* @param legacy Legacy veterans * @param legacy Legacy veterans
* @returns Individual affinities * @returns Individual affinities
*/ */
/*
export function globalAffinity(trainee: number, legacy: Legacy<Veteran>): Legacy<number> { export function globalAffinity(trainee: number, legacy: Legacy<Veteran>): Legacy<number> {
const t = findUma(uma.global, trainee)?.chara_id ?? 0; const t = findUma(uma.global, trainee)?.chara_id ?? 0;
const charas = mapLegacy(legacy, (u) => findUma(uma.global, u.uma)?.chara_id ?? 0); const charas = mapLegacy(legacy, (u) => findUma(uma.global, u.uma)?.chara_id ?? 0);
@@ -70,3 +68,4 @@ export function globalAffinity(trainee: number, legacy: Legacy<Veteran>): Legacy
s22, s22,
}; };
} }
*/
+515 -10
View File
@@ -31,9 +31,236 @@ export enum Mood {
Great, Great,
} }
/**
* Moods as a descending list for easy iteration.
*/
export const MOODS = [Mood.Great, Mood.Good, Mood.Normal, Mood.Bad, Mood.Awful] as const;
const MOOD_COEF = {
[Mood.Awful]: 0.96,
[Mood.Bad]: 0.98,
[Mood.Normal]: 1,
[Mood.Good]: 1.02,
[Mood.Great]: 1.04,
} as const;
function statClamp(x: number): number {
return Math.round(math.max(1, math.min(2000, x)));
}
/**
* Calculate a horse's base stat value from her mood and raw stat value.
* @param mood Horse's mood
* @param rawStat Raw value of the stat, i.e. the displayed stat
* @returns Base stat value
*/
export function baseStat(mood: Mood, rawStat: number): number {
const r = (math.min(rawStat, 1200) + math.max(rawStat - 1200, 0) * 0.5) * MOOD_COEF[mood];
return statClamp(r);
}
/**
* Race distance types.
*/
export enum Distance {
Sprint,
Mile,
Medium,
Long,
}
/**
* Get the race distance type for a race length.
* @param raceLen Length of the race in meters
* @returns Distance type corresponding to the race length
*/
export function distance(raceLen: number): Distance {
if (raceLen < 1500) {
return Distance.Sprint;
}
if (raceLen < 1900) {
return Distance.Mile;
}
if (raceLen < 2500) {
return Distance.Medium;
}
return Distance.Long;
}
/**
* Get the distances at which each phase of a race ends.
* @param raceLen Length of the race in meters
* @returns Ends of each phase in meters
*/
export function phases(raceLen: number) {
const sixth = raceLen / 6;
return {
[Phase.EarlyRace]: sixth,
[Phase.MidRace]: 4 * sixth,
[Phase.LateRace]: raceLen,
};
}
/**
* Race surfaces.
*/
export enum Surface {
Turf,
Dirt,
}
/**
* Race ground condition types.
*/
export enum GroundConditions {
Firm,
Good,
Soft,
Heavy,
}
/**
* Ground condition types as a list for easy iterating.
*/
export const GROUND_CONDITONS = [
GroundConditions.Firm,
GroundConditions.Good,
GroundConditions.Soft,
GroundConditions.Heavy,
] as const;
const speedStatGroundMod = {
[Surface.Turf]: {
[GroundConditions.Firm]: 0,
[GroundConditions.Good]: 0,
[GroundConditions.Soft]: 0,
[GroundConditions.Heavy]: -50,
},
[Surface.Dirt]: {
[GroundConditions.Firm]: 0,
[GroundConditions.Good]: 0,
[GroundConditions.Soft]: 0,
[GroundConditions.Heavy]: -50,
},
} as const;
const powerStatGroundMod = {
[Surface.Turf]: {
[GroundConditions.Firm]: 0,
[GroundConditions.Good]: -50,
[GroundConditions.Soft]: -50,
[GroundConditions.Heavy]: -50,
},
[Surface.Dirt]: {
[GroundConditions.Firm]: -100,
[GroundConditions.Good]: -50,
[GroundConditions.Soft]: -100,
[GroundConditions.Heavy]: -100,
},
} as const;
const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const;
/**
* Compute adjusted speed for a race.
* @param speed
* @param baseSpeed Base speed stat as computed from raw speed with baseStat
* @param career Whether the horse is currently training in career
* @param surface Race surface type
* @param conditions Race ground conditions
* @param thresholdMod Achieved stat threshold modifier for the race track
*/
export function adjustedStat(
speed: Stat.Speed,
baseSpeed: number,
career: boolean,
surface: Surface,
conditions: GroundConditions,
thresholdMod: number,
): number;
/**
* Compute adjusted stamina for a race.
* @param stam
* @param baseStam Base stamina stat as computed from raw stamina with baseStat
* @param career Whether the horse is currently training in career
*/
export function adjustedStat(stam: Stat.Stamina, baseStam: number, career: boolean): number;
/**
* Compute adjusted power for a race.
* @param power
* @param basePower Base power stat as computed from raw power with baseStat
* @param career Whether the horse is currently training in career
* @param surface Race surface type
* @param conditions Race ground conditions
*/
export function adjustedStat(
power: Stat.Power,
basePower: number,
career: boolean,
surface: Surface,
conditions: GroundConditions,
): number;
/**
* Compute adjusted guts for a race.
* @param guts
* @param baseGuts Base guts stat as computed from raw guts with baseStat
* @param career Whether the horse is currently training in career
*/
export function adjustedStat(guts: Stat.Guts, baseGuts: number, career: boolean): number;
/**
* Compute adjusted wit for a race.
* @param wit
* @param baseWit Base wit stat as computed from raw wit with baseStat
* @param career Whether the horse is currently training in career
* @param styleApt Aptitude for the horse's current running style
*/
export function adjustedStat(wit: Stat.Wit, baseWit: number, career: boolean, styleApt: AptitudeLevel): number;
export function adjustedStat(
stat: Stat,
baseStat: number,
career: boolean,
surfaceOrStyle?: Surface | AptitudeLevel,
conditions?: GroundConditions,
thresholdMod?: number,
): number {
const careerMod = career ? 400 : 0;
switch (stat) {
case Stat.Speed:
return statClamp(baseStat * thresholdMod! + speedStatGroundMod[surfaceOrStyle as Surface][conditions!] + careerMod);
case Stat.Stamina:
return statClamp(baseStat + careerMod);
case Stat.Power:
return statClamp(baseStat + powerStatGroundMod[surfaceOrStyle as Surface][conditions!] + careerMod);
case Stat.Guts:
return statClamp(baseStat + careerMod);
case Stat.Wit:
return statClamp(baseStat * strategyProficiencyMod[surfaceOrStyle as AptitudeLevel] + careerMod);
}
}
export function thresholdMod(stat1: number, stat2?: number): number {
if (stat2 != null) {
return 0.5 * (thresholdMod(stat1) + thresholdMod(stat2));
}
if (stat1 > 900) {
return 1.2;
}
if (stat1 > 600) {
return 1.15;
}
if (stat1 > 300) {
return 1.1;
}
return 1.05;
}
export function finalStat(adjStat: number, skillPassive: number): number {
return statClamp(adjStat + skillPassive);
}
/** /**
* Running styles for strategyphase coefficients. * Running styles for strategyphase coefficients.
* Great Escape is distinguished as a separate style even though it is * Runaway is distinguished as a separate style even though it is
* mechanically identical to Front Runner. * mechanically identical to Front Runner.
*/ */
export enum RunningStyle { export enum RunningStyle {
@@ -41,7 +268,7 @@ export enum RunningStyle {
PaceChaser, PaceChaser,
LateSurger, LateSurger,
EndCloser, EndCloser,
GreatEscape, Runaway,
} }
/** /**
@@ -52,7 +279,7 @@ export const RUNNING_STYLES = [
['Pace Chaser', RunningStyle.PaceChaser], ['Pace Chaser', RunningStyle.PaceChaser],
['Late Surger', RunningStyle.LateSurger], ['Late Surger', RunningStyle.LateSurger],
['End Closer', RunningStyle.EndCloser], ['End Closer', RunningStyle.EndCloser],
['Great Escape', RunningStyle.GreatEscape], ['Runaway', RunningStyle.Runaway],
] as const; ] as const;
/** /**
@@ -93,7 +320,103 @@ export enum Phase {
LateRace, LateRace,
} }
function baseSpeed(raceLen: number): number { const hpStrategyCoeff = {
[RunningStyle.FrontRunner]: 0.95,
[RunningStyle.PaceChaser]: 0.89,
[RunningStyle.LateSurger]: 1.0,
[RunningStyle.EndCloser]: 0.995,
[RunningStyle.Runaway]: 0.86,
} as const;
/**
* Calculate a horse's max (starting) HP for a race.
* @param raceLen Length of the race in meters
* @param style Horse's running style
* @param stamStat Horse's final stamina stat
* @returns Max HP
*/
export function maxHP(raceLen: number, style: RunningStyle, stamStat: number): number {
return 0.8 * hpStrategyCoeff[style] * stamStat + raceLen;
}
const groundHPRateMod = {
[Surface.Turf]: {
[GroundConditions.Firm]: 1,
[GroundConditions.Good]: 1,
[GroundConditions.Soft]: 1.02,
[GroundConditions.Heavy]: 1.02,
},
[Surface.Dirt]: {
[GroundConditions.Firm]: 1,
[GroundConditions.Good]: 1,
[GroundConditions.Soft]: 1.01,
[GroundConditions.Heavy]: 1.02,
},
} as const;
/**
* Calculate HP consumption in HP/s.
* @param raceLen Length of the race in meters
* @param surface Surface type of the race
* @param conditions Race ground conditions
* @param gutsStat Final guts stat, ignored outside of late race
* @param phase Current race phase
* @param currentSpeed Current speed in m/s, not including the effects of current speed skills
* @param dam Whether downhill accel mode is active (default false)
* @param pdm Whether pace-down mode is active (default false)
* @param rushed Whether the horse is currently rushed (default false)
*/
export function hpPerSecond(
raceLen: number,
surface: Surface,
conditions: GroundConditions,
gutsStat: number,
phase: Phase,
currentSpeed: number,
dam?: boolean,
pdm?: boolean,
rushed?: boolean,
): number {
const rushedMod = rushed ? 1.6 : 1;
const pdmMod = pdm ? 0.6 : 1;
const damMod = dam ? 0.4 : 1;
const groundMod = groundHPRateMod[surface][conditions];
const gutsMod = phase === Phase.LateRace ? 1 + 200 / (math.sqrt(600 * gutsStat) as number) : 1;
const sp = currentSpeed - baseSpeed(raceLen) + 12;
return (20 / 144) * sp * sp * rushedMod * pdmMod * damMod * groundMod * gutsMod;
}
/**
* Calculate the HP required for a full spurt.
* @param raceLen Length of the race in meters
* @param spurtSpeed Target speed that a full spurt would produce
* @param hpRate HP usage per second during late race
* @returns HP threshold required for a full spurt
*/
export function fullSpurtHP(raceLen: number, spurtSpeed: number, hpRate: number): number {
// Per Aya:
/*
on hakuraku I do 62 here over 60 since we need the distance from last
spurt start to 60m before the end, not late race start to 60m to the
end, so since umas take an average of 1.5 frames to check their spurt
after late race and run at about 20 m/s mid race on most courses I
shifted it over by 1.5/15*20=2
forgot which CM it was but I checked what would be the best fit
empirically one time and got like 62.1
*/
// https://discord.com/channels/1493222996662419576/1493222998688137301/1517348759229436036
const spurtLen = raceLen / 3 - 62;
const spurtTime = spurtLen / spurtSpeed;
return spurtTime * hpRate;
}
/**
* Calculate the base speed for a race.
* Note that this is not the base target speed for a horse.
* @param raceLen Race length in meters
* @returns Base speed for the race
*/
export function baseSpeed(raceLen: number): number {
return 20 - (raceLen - 2000) / 1000; return 20 - (raceLen - 2000) / 1000;
} }
@@ -107,6 +430,42 @@ const speedStrategyPhaseCoeff = [
const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as const; const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as const;
/**
* Get a horse's base target speed outside of late race.
* @param raceLen Length of the race in meters
* @param style Horse's running style
* @param phase Phase to calculate for
*/
export function baseTargetSpeed(raceLen: number, style: RunningStyle, phase: Exclude<Phase, Phase.LateRace>): number;
/**
* Get a horse's base target speed when not spurting during late race.
* @param raceLen Length of the race in meters
* @param style Horse's running style
* @param phase Phase to calculate for
* @param speedStat Final speed stat
* @param distanceApt Horse's aptitude for the distance
*/
export function baseTargetSpeed(
raceLen: number,
style: RunningStyle,
phase: Phase.LateRace,
speedStat: number,
distanceApt: AptitudeLevel,
): number;
export function baseTargetSpeed(
raceLen: number,
style: RunningStyle,
phase: Phase,
speedStat?: number,
distanceApt?: number,
): number {
const base = baseSpeed(raceLen);
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
const late =
phase === Phase.LateRace ? (math.sqrt(500 * speedStat!) as number) * distanceProficiencyMod[distanceApt!] * 0.002 : 0;
return baseTarget + late;
}
/** /**
* Calculate the range of section speed values for a horse in early or mid race. * Calculate the range of section speed values for a horse in early or mid race.
* @param raceLen Length of the race in meters * @param raceLen Length of the race in meters
@@ -236,7 +595,7 @@ const accelStrategyPhaseCoeff = {
[RunningStyle.PaceChaser]: [0.985, 1.0, 0.996], [RunningStyle.PaceChaser]: [0.985, 1.0, 0.996],
[RunningStyle.LateSurger]: [0.975, 1.0, 1.0], [RunningStyle.LateSurger]: [0.975, 1.0, 1.0],
[RunningStyle.EndCloser]: [0.945, 1.0, 0.997], [RunningStyle.EndCloser]: [0.945, 1.0, 0.997],
[RunningStyle.GreatEscape]: [1.17, 0.94, 0.956], [RunningStyle.Runaway]: [1.17, 0.94, 0.956],
} as const; } as const;
const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const; const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
@@ -295,6 +654,107 @@ export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): numbe
return phaseDecel[phase]; return phaseDecel[phase];
} }
/**
* Calculate a horse's lane change target speed before the first move lane point.
* @param powerStat Final power stat
* @param maxLane Max lane value on the race track in CW
* @param currentLane Current lane on the race track in CW
* @returns Lane change speed in CW/s
*/
export function laneChangeSpeed(powerStat: number, maxLane: number, currentLane: number): number;
/**
* Calculate a horse's lane change target speed during late race and final spurt phase.
* @param powerStat Final power stat
* @param order Current order on the field
* @returns Lane change speed in CW/s
*/
export function laneChangeSpeed(powerStat: number, order: number): number;
/**
* Calculate a horse's lane change target speed between the first move lane point and late race.
* @param powerStat Final power stat
* @returns Lane change speed in CW/s
*/
export function laneChangeSpeed(powerStat: number): number;
export function laneChangeSpeed(powerStat: number, maxLaneOrOrder?: number, currentLane?: number): number {
const laneMod = currentLane != null ? 1 + (currentLane / maxLaneOrOrder!) * 0.05 : 1;
const orderMod = currentLane == null ? 1 + (maxLaneOrOrder ?? 0) * 0.05 : 1;
return 0.02 * (0.3 + 0.001 * powerStat) * laneMod * orderMod;
}
/**
* Acceleration of lane change (horizontal) current speed.
*/
export const LANE_CHANGE_ACCEL = 0.03;
const conserveStratDistCoef = {
[RunningStyle.FrontRunner]: {
[Distance.Sprint]: 1,
[Distance.Mile]: 1,
[Distance.Medium]: 1,
[Distance.Long]: 1,
},
[RunningStyle.PaceChaser]: {
[Distance.Sprint]: 0.7,
[Distance.Mile]: 0.8,
[Distance.Medium]: 0.9,
[Distance.Long]: 0.9,
},
[RunningStyle.LateSurger]: {
[Distance.Sprint]: 0.75,
[Distance.Mile]: 0.7,
[Distance.Medium]: 0.875,
[Distance.Long]: 1,
},
[RunningStyle.EndCloser]: {
[Distance.Sprint]: 0.7,
[Distance.Mile]: 0.75,
[Distance.Medium]: 0.86,
[Distance.Long]: 0.9,
},
[RunningStyle.Runaway]: {
[Distance.Sprint]: 1,
[Distance.Mile]: 1,
[Distance.Medium]: 1,
[Distance.Long]: 1,
},
} as const;
export function conservePowerAccel(
distanceType: Distance,
style: RunningStyle,
rawPower: number,
powerSkillBonus?: number,
spotStruggled?: boolean,
rushed?: boolean,
): number {
const power = rawPower + (powerSkillBonus ?? 0);
if (power <= 1200) {
return 0;
}
const sdc = conserveStratDistCoef[style][distanceType];
const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1;
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
}
/**
* Calculate a horse's minimum speed for a race.
* @param raceLen Length of the race in meters
* @param gutsStat Final guts stat
* @returns Minimum speed in m/s
*/
export function minSpeed(raceLen: number, gutsStat: number): number {
return 0.85 * baseSpeed(raceLen) + Math.sqrt(200 * gutsStat) * 0.001;
}
/**
* Calculate a horse's HP consumption multiplier for late race and last spurt phase.
* @param gutsStat Final guts stat
* @returns HP consumption multiplier
*/
export function spurtHPRateMod(gutsStat: number): number {
return 1 + 200 / Math.sqrt(600 * gutsStat);
}
/** /**
* Calculate the speed boost gained from spot struggle. * Calculate the speed boost gained from spot struggle.
* @param gutsStat Final guts stat * @param gutsStat Final guts stat
@@ -304,8 +764,6 @@ export function spotStruggleSpeed(gutsStat: number): number {
return Math.pow(500 * gutsStat, 0.6) * 0.0001; return Math.pow(500 * gutsStat, 0.6) * 0.0001;
} }
const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const;
/** /**
* Calculate the max duration of spot struggle. * Calculate the max duration of spot struggle.
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead, * Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,
@@ -319,17 +777,44 @@ export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLe
return Math.sqrt(700 * gutsStat) * 0.012 * strategyProficiencyMod[frontAptitude]; return Math.sqrt(700 * gutsStat) * 0.012 * strategyProficiencyMod[frontAptitude];
} }
/**
* Calculate the target speed bonus of dueling.
* @param gutsStat Final guts stat
* @returns Modifier to target speed while dueling in m/s
*/
export function duelSpeedMod(gutsStat: number): number {
return Math.pow(200 * gutsStat, 0.708) * 0.0001;
}
/**
* Calculate the acceleration bonus of dueling.
* @param gutsStat Final guts stat
* @returns Modifier to acceleration while dueling in m/s²
*/
export function duelAccelMod(gutsStat: number): number {
return Math.pow(160 * gutsStat, 0.59) * 0.0001;
}
/** /**
* Calculate the speed modifier for running uphill. * Calculate the speed modifier for running uphill.
* Contrary to the race mechanics document, this is expressed as a negative number. * Contrary to the race mechanics document, this is expressed as a negative number.
* @param powerStat Final power stat * @param powerStat Final power stat
* @param slopePer Slope percentage, generally one of 0.5, 1.0, 1.5, or 2.0 * @param slopePer Slope percentage, one of 1.0, 1.5, or 2.0
* @returns Speed modifier for running uphill, a negative value * @returns Speed modifier for running uphill, a negative value
*/ */
export function uphillMod(powerStat: number, slopePer: number): number { export function uphillMod(powerStat: number, slopePer: number): number {
return (slopePer * -200) / powerStat; return (slopePer * -200) / powerStat;
} }
/**
* Calculate the speed modifier while running in downhill accel mode.
* @param slopePer Slope percentage, one of -1.0, -1.5, or -2.0
* @returns Speed modifier for downhill accel mode
*/
export function downhillAccelMod(slopePer: number): number {
return 0.3 - slopePer / 10;
}
/** /**
* Calculate the forward speed boost given when moving lanewise while a skill * Calculate the forward speed boost given when moving lanewise while a skill
* that grants a lane change speed boost is active. * that grants a lane change speed boost is active.
@@ -389,6 +874,16 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null,
return speedBonus * (dur + 0.5 * (decelTime - accelTime)); return speedBonus * (dur + 0.5 * (decelTime - accelTime));
} }
/**
* Calculate the probability of accepting a reduced spurt candidate.
* @param witStat Final wit stat
* @returns Probability per candidate to accept
*/
export function reducedSpurtChance(witStat: number): number {
// 1900 wit style S exceeds 100% chance if we don't explicitly cap.
return Math.min(0.15 + 0.0005 * witStat, 1);
}
/** /**
* Calculate the chance to enter downhill accel mode each second while running downhill. * Calculate the chance to enter downhill accel mode each second while running downhill.
* @param witStat Final wit stat, including style aptitude modifier * @param witStat Final wit stat, including style aptitude modifier
@@ -404,7 +899,7 @@ export function downhillAccelEnterChance(witStat: number): number {
* @returns Probability each eligible tick to enter speed-up or overtake mode * @returns Probability each eligible tick to enter speed-up or overtake mode
*/ */
export function frontModeEnterChance(witStat: number): number { export function frontModeEnterChance(witStat: number): number {
return 0.2 * math.log10(witStat) - 0.2; return 0.2 * math.log10(witStat * 0.1);
} }
/** /**
@@ -413,5 +908,15 @@ export function frontModeEnterChance(witStat: number): number {
* @returns Probability each eligible tick to enter pace-up mode * @returns Probability each eligible tick to enter pace-up mode
*/ */
export function paceUpEnterChance(witStat: number): number { export function paceUpEnterChance(witStat: number): number {
return 0.15 * math.log10(witStat) - 0.15; return 0.15 * math.log10(witStat * 0.1);
}
/**
* Calculate the chance for a horse to become rushed during a given race.
* @param witStat Final wit stat, including style aptitude modifier
* @returns Probability to become rushed during the race
*/
export function rushedChance(witStat: number): number {
const r = 6.5 / Math.log10(0.1 * witStat + 1);
return Math.ceil(r * r) / 100;
} }
+97
View File
@@ -0,0 +1,97 @@
/**
* Instantaneous characteristics of a horse in motion.
*/
export interface Instant {
/** Timestamp of the instant. */
t: number;
/** Forward position along the course. */
x: number;
/** Actual speed. */
v: number;
}
/**
* Forward travel characteristics under the effects of acceleration.
*/
export interface Travel {
start: Instant;
end: Instant;
/** Actual acceleration value applied between start and end. */
a: number;
}
/**
* Advance a horse according to its forward motion characteristics.
* @param start Motion at the beginning of the segment
* @param targetSpeed Target speed to accelerate to
* @param totalAccel Total (positive) acceleration including skill bonuses
* @param maxDur Max duration of the acceleration, or infinite if not given
* @param maxDist Max distance of the acceleration, or infinite if not given
* @param decel Negative acceleration value for cases where target speed is below start speed
* @returns Travel segment after reaching the first of the given limits
*/
export function travelSegment(
start: Instant,
targetSpeed: number,
totalAccel: number,
maxDur?: number,
maxDist?: number,
decel?: number,
): Travel {
const a = start.v < targetSpeed ? totalAccel : (decel ?? -1);
const dvt = (targetSpeed - start.v) / a;
const dxt = maxDist != null ? (-start.v + Math.sqrt(start.v * start.v + 4 * a * maxDist)) / (2 * a) : dvt;
const time = maxDur != null ? Math.min(dvt, dxt, maxDur) : Math.min(dvt, dxt);
// To address numerical imprecision, if accel time comes from velocity or distance,
// set the reached values directly.
const v = time === dvt ? targetSpeed : start.v + time * a;
const dist = maxDist != null && time === dxt ? maxDist : (start.v + 0.5 * a * time) * time;
const end = { t: start.t + time, x: start.x + dist, v };
return { start, end, a };
}
/**
* Acceleration bonus from skills.
*/
export interface Skill {
/**
* Acceleration modifier in m/s².
*/
accel: number;
/**
* Remaining actual duration in seconds.
*/
dur: number;
}
/**
* Accelerate up to a new target speed while under the effects of acceleration skills.
* @param startSpeed Speed at the start of the acceleration in m/s
* @param targetSpeed Speed at which the acceleration ends in m/s
* @param baseAccel Base acceleration value to which skills add, in m/s²
* @param skills Skills that are active for the acceleration period
* @returns Acceleration segments and remainders of skills upon reaching the target speed
*/
export function up(start: Instant, targetSpeed: number, baseAccel: number, skills: Skill[]) {
const segments: Travel[] = [];
const sk = skills.map((s) => ({ ...s })).filter((s) => s.dur > 0);
while (start.v < targetSpeed) {
const active = sk.filter((s) => s.dur > 0);
if (active.length === 0) {
segments.push(travelSegment(start, targetSpeed, baseAccel));
break;
}
const boost = active.reduce((a, s) => a + s.accel, 0);
const time = Math.min(...active.map((s) => s.dur));
const seg = travelSegment(start, targetSpeed, baseAccel + boost, time);
segments.push(seg);
start = seg.end;
for (const s of active) {
s.dur -= seg.end.t - seg.start.t;
if (s.dur < 1e-5) {
s.dur = 0;
}
}
}
return { segments, skills: sk };
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Fundamental stats of umas.
*/
export enum Stat {
speed,
stamina,
power,
guts,
wit,
}
/**
* Stats as a list for easy iteration.
*/
export const LIST = [Stat.speed, Stat.stamina, Stat.power, Stat.guts, Stat.wit] as const;
declare const __stage: unique symbol;
export type Stage = 'raw' | 'base' | 'adjusted' | 'final' | 'skills';
export type StatValue<T extends Stage> = number & { [__stage]: T };
export interface Stats<T extends Stage> {
speed: StatValue<T>;
stamina: StatValue<T>;
power: StatValue<T>;
guts: StatValue<T>;
wit: StatValue<T>;
}
+56
View File
@@ -0,0 +1,56 @@
const WORD_BOUNDARY = " ,!?/-+();#○☆♡'=♪∀゚∴";
function score(s: string, tt: string): number {
let k: number | undefined;
let r = 0;
let run = 0;
for (const c of s) {
const j = tt.indexOf(c, k);
// If the character isn't in the string, there's a major penalty.
if (j < 0) {
// The penalty scales with run length, on the assumption that we're
// typing something else.
// Really this should scale with the longest current run among all
// search terms, but that's infeasible to implement.
r -= 6 + run * run;
run = 0;
continue;
}
run++;
// Characters at word boundaries get extra score.
if (j == 0 || WORD_BOUNDARY.includes(tt[j - 1])) {
r += 2;
}
// As do characters that *are* word boundaries.
if (WORD_BOUNDARY.includes(c)) {
r += 2;
}
// And runs of matches scale with run length.
if (j === k) {
r += (run + 1) * (run + 1);
} else {
run = 0;
}
k = j + 1;
}
return r;
}
/**
* Fuzzy string search.
* @param sub Substring to search for
* @param terms Iterable of values to search among
* @param map Mapping from term to string to search
* @returns Matching terms in decreasing match quality order
*/
export function stringsearch<T>(sub: string, terms: Iterable<T>, map: (t: T) => string): T[] {
const s = sub.toLocaleLowerCase();
const scored: [T, number][] = [];
for (const t of terms) {
const sc = score(s, map(t).toLocaleLowerCase());
if (sc >= 0) {
scored.push([t, sc]);
}
}
return scored.sort(([, a], [, b]) => b - a).map(([t]) => t);
}
@@ -0,0 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
onMount(() => {
goto(resolve('/chara/affinity'), { replaceState: true });
});
</script>
<p class="mt-8 block w-full text-center text-4xl">
<a href={resolve('/chara/affinity')}>This page has moved.</a>
</p>
<p class="mt-8 block w-full text-center">You should be redirected momentarily.</p>
@@ -0,0 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
onMount(() => {
goto(resolve('/chara/convo'), { replaceState: true });
});
</script>
<p class="mt-8 block w-full text-center text-4xl">
<a href={resolve('/chara/convo')}>This page has moved.</a>
</p>
<p class="mt-8 block w-full text-center">You should be redirected momentarily.</p>
@@ -0,0 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
onMount(() => {
goto(resolve('/race/mspeed'), { replaceState: true });
});
</script>
<p class="mt-8 block w-full text-center text-4xl">
<a href={resolve('/race/mspeed')}>This page has moved.</a>
</p>
<p class="mt-8 block w-full text-center">You should be redirected momentarily.</p>
@@ -0,0 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
onMount(() => {
goto(resolve('/race/spurt'), { replaceState: true });
});
</script>
<p class="mt-8 block w-full text-center text-4xl">
<a href={resolve('/race/spurt')}>This page has moved.</a>
</p>
<p class="mt-8 block w-full text-center">You should be redirected momentarily.</p>
+26 -7
View File
@@ -1,13 +1,31 @@
<script lang="ts"> <script lang="ts">
import './layout.css'; import './layout.css';
import favicon from '$lib/assets/favicon.png'; import favicon from '$lib/assets/favicon.png';
import * as all from '$lib/all';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import type { LayoutProps } from './$types';
import { page } from '$app/state';
let { children } = $props(); let { children }: LayoutProps = $props();
const cur = $derived(all.page(page.route.id));
const title = $derived(cur.name);
const desc = $derived(
cur.description ??
"She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations! Tools and editorials for Umamusume optimization.",
);
</script> </script>
<svelte:head> <svelte:head>
{#if title != null}
<title>Zenno Rob Roy &ndash; {title}</title>
{:else}
<title>Zenno Rob Roy</title> <title>Zenno Rob Roy</title>
{/if}
<meta name="description" content={desc} />
<meta name="color-scheme" content="light dark" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<meta name="robots" content="noimageindex" />
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
</svelte:head> </svelte:head>
@@ -18,15 +36,14 @@
</span> </span>
<span class="flex-1 text-center"> <span class="flex-1 text-center">
<a href={resolve('/')} class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a> <a href={resolve('/')} class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
<a href={resolve('/spurt')} class="mx-8 my-1 inline-block">Spurt Speed</a> <a href={resolve('/race')} class="mx-8 my-1 inline-block">Race Tools</a>
<a href={resolve('/mspeed')} class="mx-8 my-1 inline-block">Mechanical Speed</a> <a href={resolve('/chara')} class="mx-8 my-1 inline-block">Character Tools</a>
<a href={resolve('/convo')} class="mx-8 my-1 inline-block">Lobby Conversations</a>
<a href={resolve('/doc')} class="mx-8 my-1 inline-block">Documents</a> <a href={resolve('/doc')} class="mx-8 my-1 inline-block">Documents</a>
</span> </span>
</nav> </nav>
<div class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl"> <main class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
{@render children()} {@render children()}
</div> </main>
<footer <footer
class="inset-x-0 bottom-0 mt-12 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900" class="inset-x-0 bottom-0 mt-12 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900"
> >
@@ -36,6 +53,8 @@
All game data is auto-generated from the All game data is auto-generated from the
<a href="https://git.sunturtle.xyz/zephyr/horse/src/branch/main/doc/README.md" target="_blank" rel="noopener noreferrer" <a href="https://git.sunturtle.xyz/zephyr/horse/src/branch/main/doc/README.md" target="_blank" rel="noopener noreferrer"
>game's local database</a >game's local database</a
>. >.<br />
If you find this site helpful, please consider
<a href="https://ko-fi.com/zephyrtronium" target="_blank" rel="noopener noreferrer">supporting my Ko-fi</a>.
</footer> </footer>
</div> </div>
+9 -22
View File
@@ -1,35 +1,22 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths'; import * as all from '$lib/all';
import IndexSection from '$lib/IndexSection.svelte';
</script> </script>
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy</h1> <h1 class="m-8 text-center text-7xl">Zenno Rob Roy</h1>
<p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p> <p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p>
<h2 class="mt-8 mb-4 text-4xl">Tools</h2> <IndexSection title="Race Mechanics Tools" pages={all.PAGES.race} />
<IndexSection title="Character Tools" pages={all.PAGES.chara} />
<IndexSection title="Documents" pages={all.PAGES.doc} />
<!-- Not a real index section in, terms of types. -->
<h2 class="my-4 text-4xl">&amp;c.</h2>
<ul class="mb-4 list-disc pl-4"> <ul class="mb-4 list-disc pl-4">
<li>
<a href={resolve('/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other distance aptitudes
and running styles.
</li>
<li>
<a href={resolve('/mspeed')}>Front Runner Mechanical Speed Comparator</a> &mdash; Compare spot struggle and lane combo to the effects
of individual skills.
</li>
<li>
<a href={resolve('/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking
them quickly.
</li>
<li> <li>
<a href="https://discord.com/oauth2/authorize?client_id=1461931240264568994" target="_blank" rel="noopener noreferrer" <a href="https://discord.com/oauth2/authorize?client_id=1461931240264568994" target="_blank" rel="noopener noreferrer"
>Discord Bot</a >Discord Bot</a
> Skill search by name or unique owner within Discord. Install to a server or user. > &mdash; Skill search by name or unique owner within Discord. Install to a server or user.
</li>
</ul>
<h2 class="my-4 text-4xl">Documents</h2>
<ul class="mb-4 list-disc pl-4">
<li>
<a href={resolve('/doc/frbm')}>Front Runner Black Magic</a> &mdash; Detailed analysis of how front runners interact with race mechanics.
</li> </li>
</ul> </ul>
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import { page } from '$app/state';
import * as all from '$lib/all';
import IndexSection from '$lib/IndexSection.svelte';
const cur = $derived(all.page(page.route.id));
</script>
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy &mdash; {cur.name}</h1>
<p>{cur.description}</p>
<IndexSection pages={all.PAGES.chara} />
@@ -0,0 +1,141 @@
<script lang="ts">
import CharaPick from '$lib/CharaPick.svelte';
import { AFFINITY_RELATION_DESCRIPTIONS, affinityDetail, type AffinityDetail } from '$lib/data/affinity';
import { character, type Character } from '$lib/data/character';
import { onMount } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
let characters: Character[] = $state([]);
let affs: AffinityDetail[] = $state([]);
onMount(async () => {
const [charas, affinities] = await Promise.all([character(), affinityDetail()]);
affs = affinities;
characters = charas.filter((c) => affs.some((d) => d.chara_id === c.chara_id));
});
let charA: Character | undefined = $state();
let charB: Character | undefined = $state();
let charC: Character | undefined = $state();
const charaListA = $derived(characters.filter((c) => c.chara_id != charB?.chara_id && c.chara_id != charC?.chara_id));
const charaListB = $derived(characters.filter((c) => c.chara_id != charA?.chara_id && c.chara_id != charC?.chara_id));
const charaListC = $derived(characters.filter((c) => c.chara_id != charA?.chara_id && c.chara_id != charB?.chara_id));
function groupsFor(affs: AffinityDetail[], chara: number): AffinityDetail[] {
return chara !== 0 ? affs.filter((d) => d.chara_id === chara) : [];
}
const groupsA = $derived(groupsFor(affs, charA?.chara_id ?? 0));
const groupsB = $derived(groupsFor(affs, charB?.chara_id ?? 0));
const groupsC = $derived(groupsFor(affs, charC?.chara_id ?? 0));
const allGroups = $derived.by(() => {
const seen = new SvelteSet<number>();
const r = [];
for (const g of [groupsA, groupsB, groupsC]) {
for (const d of g) {
if (seen.has(d.relation)) {
continue;
}
seen.add(d.relation);
r.push(d);
}
}
return r;
});
const selCount = $derived(1 + (charB == null ? 0 : 1) + (charC == null ? 0 : 1));
const infos = $derived(
allGroups
.map((d) => ({
...d,
description: AFFINITY_RELATION_DESCRIPTIONS.get(d.relation),
a: groupsA.some(({ relation }) => relation === d.relation),
b: groupsB.some(({ relation }) => relation === d.relation),
c: groupsC.some(({ relation }) => relation === d.relation),
}))
.map((d) => ({
...d,
counted: (d.a ? 1 : 0) + (d.b ? 1 : 0) + (d.c ? 1 : 0),
}))
.toSorted((a, b) => (a.counted !== b.counted ? b.counted - a.counted : a.relation - b.relation)),
);
const total = $derived(infos.filter((d) => d.counted === selCount).reduce((t, d) => t + d.affinity, 0));
</script>
<h1 class="text-4xl">Affinity Details</h1>
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-2xl md:flex-row">
<div class="m-4 flex-1 md:mt-3">
<label for="charA" class="hidden md:inline">Character 1</label>
<CharaPick id="charA" characters={charaListA} class="w-full" bind:value={charA} required />
</div>
<div class="m-4 flex-1 md:mt-3">
<label for="charB" class="hidden md:inline">Character 2</label>
<CharaPick id="charB" characters={charaListB} class="w-full" bind:value={charB} />
</div>
<div class="m-4 flex-1 md:mt-3">
<label for="charC" class="hidden md:inline">Character 3</label>
<CharaPick id="charC" characters={charaListC} class="w-full" bind:value={charC} />
</div>
</div>
<svelte:boundary>
{#if selCount > 1}
<span class="mt-8 block w-full text-center text-lg">Total {selCount === 2 ? 'pair' : 'trio'} affinity: {total}</span>
{/if}
<table class="mx-auto mt-8 table-fixed">
<thead>
<tr class="py-2">
<th class="w-32 px-2" scope="col">Group ID</th>
<th class="w-96 px-2" scope="col">Description</th>
{#if selCount > 1}
<th class="w-56 px-2" scope="col">{charA!.name}</th>
{#if charB != null}
<th class="w-56 px-2" scope="col">{charB.name}</th>
{/if}
{#if charC != null}
<th class="w-56 px-2" scope="col">{charC.name}</th>
{/if}
{/if}
<th class="w-32 px-2" scope="col">Affinity</th>
</tr>
</thead>
<tbody>
{#each infos as { relation, affinity, description, a, b, c, counted } (relation)}
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
<td class="px-4 text-center">{relation}</td>
{#if description != null}
<td class="px-4">{[description.name, description.note].filter((s) => s != null).join(' ')}</td>
{:else}
<td class="px-4"></td>
{/if}
{#if selCount > 1}
<td class="px-4 text-center">{a ? '✓' : ''}</td>
{#if charB != null}
<td class="px-4 text-center">{b ? '✓' : ''}</td>
{/if}
{#if charC != null}
<td class="px-4 text-center">{c ? '✓' : ''}</td>
{/if}
{/if}
<td class="px-4 text-center">{selCount > 1 && counted === selCount ? '+' : ''}{affinity}</td>
</tr>
{/each}
</tbody>
</table>
{#snippet pending()}
<div>Loading data...</div>
{/snippet}
</svelte:boundary>
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<p class="text-center">
Descriptions are inferred from the horses in the respective groups. They are not included in the game data.
</p>
<p class="text-center">
Some characters are not in affinity groups that it seems like they should be in, e.g. parent/child pairs and G1 winners for
various races. Other characters are in affinity groups that it seems like they should not be in, e.g. Mihono Bourbon in the
sprint group. Inclusion in a group is automatically generated from the game data and hence is known to correspond to game
calculations, even where they don't make sense.
</p>
<p class="text-center">
Thanks to princess_fox, hell259, Werseter, thegenosys, Lofi (Class: Voyager), and Ordalca on the GameTora Discord server, and
Damhain on the CirnoTV server, for figuring out most of the groups.
</p>
</div>
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import { character, charaNames, type Character } from '$lib/data/character';
import { byChara, locations, groupPopulars, conversation, type Conversation } from '$lib/data/convo';
import CharaPick from '$lib/CharaPick.svelte';
import { onMount } from 'svelte';
let conversations: Conversation[] = $state([]);
const convoMap = $derived(byChara(conversations));
const popular = $derived(groupPopulars(conversations));
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()]);
});
let chara: Character | undefined = $state();
let convo = $state(1);
const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
const cur = $derived(options.find((c) => c.number === convo));
const cur1Name = $derived(cur?.chara_1 && names.get(cur.chara_1)?.en);
const cur2Name = $derived(cur?.chara_2 && names.get(cur.chara_2)?.en);
const cur3Name = $derived(cur?.chara_3 && names.get(cur.chara_3)?.en);
const alone = $derived([cur?.chara_1, cur?.chara_2, cur?.chara_3].filter((x) => x != null).length == 1 ? ' alone' : '');
const suggested = $derived.by(async () => {
if (cur == null) {
return [];
}
const u = popular[locations[cur.location].group].filter(
(s) => names.get(s[0]) != null && s[0] !== cur.chara_1 && s[0] !== cur.chara_2 && s[0] !== cur.chara_3,
);
const r = u.length <= minSuggest ? u : u.filter((s) => s[1] >= u[minSuggest][1]);
return r.map(([chara_id, count]) => ({ chara_id, chara_name: names.get(chara_id), count }));
});
const curLoc = $derived(cur != null ? locations[cur.location] : null);
</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">
<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">
<label for="convo" class="hidden md:inline">Conversation</label>
<select id="convo" bind:value={convo} class="w-full">
{#each options as opt (opt.number)}
<option value={opt.number}>Slice of Life {opt.number}</option>
{/each}
</select>
</div>
</div>
{#await suggested}
<div class="mt-4 w-full text-center text-3xl">Loading...</div>
{:then suggested}
{#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md">
<div class="mt-8 flex text-center text-lg">
<span class="flex-1">{cur1Name}{alone}</span>
{#if cur2Name}
<span class="flex-1">{cur2Name}</span>
{/if}
{#if cur3Name}
<span class="flex-1">{cur3Name}</span>
{/if}
</div>
<div class="flex w-full text-center text-lg">
<span class="flex-1">at {curLoc?.name.en}</span>
</div>
</div>
<div class="mt-4 block text-center">
<span>Other characters who appear here most often:</span>
</div>
<div class="mt-4 grid text-center shadow-sm transition-shadow ease-in hover:shadow-md hover:ease-out md:grid-cols-4">
{#each suggested as s (s.chara_id)}
<span>{s.chara_name?.en}: {s.count}&#xd7;</span>
{/each}
</div>
<div class="mt-4 block text-center">
<span>
Set these characters to fixed positions (main, upgrades, story, races) to maximize the chance of getting this
conversation.
</span>
</div>
{/if}
{/await}
-73
View File
@@ -1,73 +0,0 @@
<script lang="ts">
import { character, charaNames } from '$lib/data/character';
import { byChara, locations, groupPopulars } from '$lib/data/convo';
import CharaPick from '$lib/CharaPick.svelte';
const characters = character.global.filter((c) => byChara.global.has(c.chara_id));
const minSuggest = 8;
let charaID = $state(1001);
let convo = $state(1);
const options = $derived(byChara.global.get(charaID) ?? []);
const cur = $derived(options.find((c) => c.number === convo));
const cur1Name = $derived(cur?.chara_1 && charaNames.get(cur.chara_1)?.en);
const cur2Name = $derived(cur?.chara_2 && charaNames.get(cur.chara_2)?.en);
const cur3Name = $derived(cur?.chara_3 && charaNames.get(cur.chara_3)?.en);
const alone = $derived([cur?.chara_1, cur?.chara_2, cur?.chara_3].filter((x) => x != null).length == 1 ? ' alone' : '');
const suggested = $derived.by(() => {
if (cur == null) {
return [];
}
const u = groupPopulars.global[locations[cur.location].group].filter(
(s) => charaNames.get(s[0]) != null && s[0] !== cur.chara_1 && s[0] !== cur.chara_2 && s[0] !== cur.chara_3,
);
const r = u.length <= minSuggest ? u : u.filter((s) => s[1] >= u[minSuggest][1]);
return r.map(([chara_id, count]) => ({ chara_id, count }));
});
</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">
<label for="chara" class="hidden md:inline">Character</label>
<CharaPick id="chara" {characters} class="w-full" bind:value={charaID} required />
</div>
<div class="m-4 flex-1 md:mt-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)}
<option value={opt.number}>Slice of Life {opt.number}</option>
{/each}
</select>
</div>
</div>
{#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md">
<div class="mt-8 flex text-center text-lg">
<span class="flex-1">{cur1Name}{alone}</span>
{#if cur2Name}
<span class="flex-1">{cur2Name}</span>
{/if}
{#if cur3Name}
<span class="flex-1">{cur3Name}</span>
{/if}
</div>
<div class="flex w-full text-center text-lg">
<span class="flex-1">at {locations[cur.location].name.en}</span>
</div>
</div>
<div class="mt-4 block text-center">
<span>Other characters who appear here most often:</span>
</div>
<div class="mt-4 grid text-center shadow-sm transition-shadow ease-in hover:shadow-md hover:ease-out md:grid-cols-4">
{#each suggested as s (s.chara_id)}
<span>{charaNames.get(s.chara_id)?.en}: {s.count}&#xd7;</span>
{/each}
</div>
<div class="mt-4 block text-center">
<span>
Set these characters to fixed positions (main, upgrades, story, races) to maximize the chance of getting this conversation.
</span>
</div>
{/if}
+8 -9
View File
@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths'; import { page } from '$app/state';
import * as all from '$lib/all';
import IndexSection from '$lib/IndexSection.svelte';
const cur = $derived(all.page(page.route.id));
</script> </script>
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy &mdash; Documents</h1> <h1 class="m-8 text-center text-7xl">Zenno Rob Roy &mdash; {cur.name}</h1>
<p>Articles about Umamusume, including editorials and research.</p> <p>{cur.description}</p>
<IndexSection pages={all.PAGES.doc} />
<ul class="mb-4 list-disc pl-4">
<li>
<a href={resolve('/doc/frbm')}>Front Runner Black Magic</a> &mdash; Detailed analysis of how front runners interact with race mechanics.
</li>
</ul>
+2 -2
View File
@@ -20,13 +20,13 @@
tocViewed = true; tocViewed = true;
obs.disconnect(); obs.disconnect();
} }
} };
$effect(() => { $effect(() => {
if (tocElem == null) { if (tocElem == null) {
return; return;
} }
const obs = new IntersectionObserver(observeTOC, {rootMargin: "20% 0px"}); const obs = new IntersectionObserver(observeTOC, { rootMargin: '20% 0px' });
obs.observe(tocElem); obs.observe(tocElem);
}); });
+9 -5
View File
@@ -7,12 +7,13 @@
h?: 1 | 2 | 3 | 4 | 5 | 6; h?: 1 | 2 | 3 | 4 | 5 | 6;
id: string; id: string;
children: Snippet; children: Snippet;
ref?: string;
top?: string; top?: string;
toc?: boolean; toc?: boolean;
class?: ClassValue | null; class?: ClassValue | null;
} }
let { h = 1, id, children, top = '#top', toc = true, class: className }: Props = $props(); let { h = 1, id, children, ref, top = '#top', toc = true, class: className }: Props = $props();
const tag = $derived(`h${h}`); const tag = $derived(`h${h}`);
const href = $derived(`#${id}`); const href = $derived(`#${id}`);
@@ -39,9 +40,12 @@
<svelte:element this={tag} {id} class={['relative w-full', className]}> <svelte:element this={tag} {id} class={['relative w-full', className]}>
<!-- eslint-disable svelte/no-navigation-without-resolve --> <!-- eslint-disable svelte/no-navigation-without-resolve -->
<a {href}><span class="hidden md:inline">{sign}</span> {@render children()}</a> <a {href}><span class="hidden md:inline">{sign}</span> {@render children()}</a>
{#if h >= 2} <div class="absolute right-0 bottom-0 inline-block text-end align-bottom text-sm whitespace-nowrap">
<a href={top} class="absolute right-0 bottom-0 text-end align-bottom text-sm whitespace-nowrap transition-all hover:bottom-px" {#if ref != null}
>Top ▲</a <a href={ref} target="_blank" rel="noopener noreferrer">Ref &UpperRightArrow;</a>
>
{/if} {/if}
{#if h >= 2}
<a href={top} class="ml-4">Top ▲</a>
{/if}
</div>
</svelte:element> </svelte:element>
File diff suppressed because it is too large Load Diff
+279
View File
@@ -0,0 +1,279 @@
<script lang="ts">
import { asset, resolve } from '$app/paths';
import Skill from '$lib/Skill.svelte';
import Sec from '../Sec.svelte';
import * as skill from '$lib/data/skill';
import { onMount } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import courseSetupHoriz from './course-setup.horiz.png';
import courseSetupVert from './course-setup.vert.png';
import umaSetupHoriz from './uma-setup.horiz.png';
import umaSetupVert from './uma-setup.vert.png';
import resultsChart from './results-chart.png';
import resultsHoriz from './results.horiz.png';
import resultsVert from './results.vert.png';
import Article from '../Article.svelte';
let allSkillsList: skill.Skill[] = $state([]);
onMount(async () => {
allSkillsList = await skill.skills();
});
const allSkills = $derived(allSkillsList.reduce((m, s) => m.set(s.skill_id, s), new Map<number, skill.Skill>()));
const skills = $derived({
goldpp: allSkills.get(200451),
pp: allSkills.get(200452),
ttl: allSkills.get(200531),
earlyLead: allSkills.get(200532),
dd: allSkills.get(201262),
gw: allSkills.get(201601),
ignitedWIT: allSkills.get(210052),
});
const locations = [
['札幌', 'Sapporo'],
['函館', 'Hakodate'],
['新潟', 'Niigata'],
['福島', 'Fukushima'],
['中山', 'Nakayama'],
['東京', 'Tokyo'],
['中京', 'Chukyo'],
['京都', 'Kyoto'],
['阪神', 'Hanshin'],
['小倉', 'Kokura'],
['大井', 'Ooi'],
['川崎', 'Kawasaki'],
['船橋', 'Funabashi'],
['盛岡', 'Morioka'],
] as const;
const courseinfos = [
['芝', 'Turf'],
['ダート', 'Dirt'],
['内', 'Inner'],
['外', 'Outer'],
] as const;
const conditionses = [
['良', 'Firm'],
['稍重', 'Good'],
['重', 'Soft'],
['不良', 'Heavy'],
] as const;
const styles = [
['大逃げ', 'Runaway'],
['逃げ', 'Front Runner'],
] as const;
const moods = [
['絶好調', 'Great'],
['好調', 'Good'],
['普通', 'Normal'],
['不調', 'Bad'],
['絶不調', 'Awful'],
] as const;
const accels = [
['地固め', 'Groundwork'],
['盤石', 'Unshakable Stance'],
['先駆け', 'Early Lead'],
['先手', 'Taking the Lead'],
['進化先手', 'Evolved Taking the Lead'],
] as const;
const arinashi = [
['あり', 'Enabled'],
['なし', 'Disabled'],
] as const;
const hills = [
['上り坂 (Red)', 'Uphill'],
['下り坂 (Blue)', 'Downhill'],
] as const;
const times = [
['危険回避', 'Dodging Danger'],
['スキル', 'Lane Skill Activation'],
['中盤', 'Mid Race'],
] as const;
const straco = [
['直線 (Yellow)', 'Straight'],
['コーナー (Pink)', 'Corner'],
] as const;
const laneskills = [
['スキルなし', 'No Skills'],
['危険回避のみ', 'Dodging Danger'],
['+注目の踊り子', 'DD + Center Stage'],
['+ポジションセンス', 'DD + Prudent Positioning'],
['+アオハル点火・賢', 'DD + Ignited Spirit WIT'],
] as const;
const tooltip = [
['時間', 'Time (s)'],
['レーン', 'Lane (CW)'],
['走行距離', 'Distance (m)'],
['距離ロス', 'Distance Loss (m)'],
] as const;
const summaryText = [
['バ身', 'Lengths'],
['アド', 'Add'],
['ロス', 'Loss'],
] as const;
</script>
{#snippet transbody(heads: string | [string, string], cells: ReadonlyArray<readonly [string, string]>, cls?: ClassValue | null)}
<table class={['mx-1 mt-4 table-fixed border md:mb-auto', cls]}>
{#if typeof heads === 'string'}
<caption class="border border-b-0">{heads}</caption>
{:else}
<thead class="border-b">
<tr>
<th class="w-1/2 px-2" scope="col">{heads[0]}</th>
<th class="w-1/2 px-2" scope="col">{heads[1]}</th>
</tr>
</thead>
{/if}
<tbody>
{#each cells as [jp, en] (jp)}
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
<td>{jp}</td>
<td>{en}</td>
</tr>
{/each}
</tbody>
</table>
{/snippet}
<Article>
{#snippet head()}
<Sec h={1} id="top" class="text-center pointer-coarse:mb-8">Gaikokujin's Guide to Lanecalc</Sec>
<p>
This is an English usage manual for <a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer"
>危険回避シミュ</a
>, or Lanecalc.
</p>
<p>
Lanecalc is a precise calculator for the effect of <a href={resolve('/doc/frbm#lane-combo')}>lane combo</a>, the use of <Skill
skill={skills.dd}
/> along with some combination of <Skill skill={skills.pp} />, <Skill skill={skills.goldpp} />, and <Skill
skill={skills.ignitedWIT}
/>
for a substantial speed boost in early race. Lanecalc helps with deciding whether lane combo (and DD alone) is applicable to a
given track.
</p>
<p>
The translations presented in this manual are contextualized to an English speaker using Lanecalc, rather than being direct
translations. E.g., スキル literally means "skill," but I am translating it as "lane skill activation time" where that is
what スキル signifies.
</p>
{/snippet}
<Sec h={2} id="course">Course Setup</Sec>
<p>
The top section of lane calc is the course setup section. Set the race location, course at the location, and ground condition
here.
</p>
<a href={asset(courseSetupHoriz)}>
<picture>
<source srcset={asset(courseSetupHoriz)} media="(orientation: landscape)" width="882" height="111" />
<img
class="mx-auto"
src={asset(courseSetupVert)}
width="412"
height="214"
alt="Lanecalc's course setup section with English labels"
/>
</picture>
</a>
<div class="mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
{@render transbody(['レース場', 'Location'], locations)}
{@render transbody(['コース', 'Race Course'], courseinfos)}
{@render transbody(['バ場状態', 'Conditions'], conditionses)}
</div>
<p>
The race course menu shows only courses available at the selected race location. Changing the location will reset the course.
</p>
<Sec h={2} id="uma">Uma Setup</Sec>
<p>The next section is the setup for the Uma.</p>
<a href={asset(umaSetupHoriz)}>
<picture>
<source srcset={asset(umaSetupHoriz)} media="(orientation: landscape)" width="878" height="218" />
<img
class="mx-auto"
src={asset(umaSetupVert)}
width="412"
height="456"
alt="Lanecalc's Uma setup section with English labels"
/>
</picture>
</a>
<div class="mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
{@render transbody(['脚質', 'Running Style'], styles)}
{@render transbody(['パワー', 'Power'], [])}
{@render transbody(['調子', 'Mood'], moods)}
{@render transbody(['ウマ番', 'Gate'], [])}
{@render transbody(['危険回避発動', 'Dodging Danger'], arinashi)}
{@render transbody(['横ブロック', 'Side Block'], arinashi)}
{@render transbody(['レーンスキル発動位置', 'Lane Skill Activation Time'], [['秒', 'seconds']])}
{@render transbody(['加速スキル', 'Acceleration Skills'], accels, 'md:col-span-2')}
</div>
<p>
Power should be entered as the raw power stat, i.e. the number shown on your Uma's stat screen. (There doesn't appear to be a
way to apply power passives.)
</p>
<p>
Unshakable Stance is the gold version of <Skill skill={skills.gw} mention /> and is not yet available in Global. "Evolved Taking
the Lead" means any of the evolutions of <Skill skill={skills.ttl} mention /> that turn it into a 0.5 accel skill. (Aston Machan
will have a TTL evolution that instead turns it into a 4s 0.4 accel. There doesn't appear to be a way to apply it in the calculator.)
</p>
<p>
Selecting another skill in the same group as a selected one will remove the existing skill. Selecting an already selected
skill will disable it entirely in the simulator.
</p>
<Sec h={2} id="chart">Results Chart</Sec>
<p>Once the course and Uma are configured, click the green 計算 button. After a moment, the results appear.</p>
<a href={asset(resultsChart)}>
<picture>
<img class="mx-auto" src={asset(resultsChart)} width="800" height="600" alt="Lanecalc results chart" />
</picture>
</a>
<p class="mt-4">
The chart shows the Uma's simulated lane (horizontal distance from the rail) and forward velocity under the effects of various
lane combo skills.
</p>
<p>
The example above is Hanshin 2200, firm, with a front runner with 1200 power in great condition from gate 6. She has <Skill
skill={skills.gw}
mention
/> and <Skill skill={skills.earlyLead} mention />, and the lane change speed skills are set to activate at 7 seconds.
</p>
<div class="-mt-4 mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-6">
{@render transbody('Hill Indicators', hills, 'md:col-span-2')}
{@render transbody('Time Indicators', times, 'md:col-span-2')}
{@render transbody('Straights/Corners', straco, 'md:col-span-2')}
{@render transbody('Lane Speed Skills', laneskills, 'md:col-span-4 md:col-start-2')}
</div>
<p>
You can click the labels underneath the chart to hide the corresponding line. The top row is for lane position lines, and the
bottom row is for velocity lines.
</p>
<p>
You can also hover the mouse over the lines for details. The first portion of the details are the skills that produce the
exact corresponding point, with a color square for each. Underneath is the simulation state.
</p>
<div class="-mt-4 mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
{@render transbody('Hover Info', tooltip, 'md:col-start-2')}
</div>
<Sec h={2} id="summary">Results Summary</Sec>
<p>
Last is the summary, shown as circles showing the gain from each combination of skills:
<Skill skill={skills.dd} /> alone, DD plus <Skill skill={skills.goldpp} />, DD plus <Skill skill={skills.pp} />, and DD plus <Skill
skill={skills.ignitedWIT}
/>.
</p>
<a href={asset(resultsHoriz)}>
<picture>
<source srcset={asset(resultsHoriz)} media="(orientation: landscape)" width="700" height="176" />
<img class="mx-auto" src={asset(resultsVert)} width="380" height="703" alt="Lanecalc results summary" />
</picture>
</a>
<div class="mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
{@render transbody('Lane Speed Skills', laneskills.slice(1), 'md:col-span-2')}
{@render transbody('Labels', summaryText)}
</div>
</Article>
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+482
View File
@@ -0,0 +1,482 @@
<script lang="ts">
import type { ComputedAreas, ComputedSeries } from '$lib/chart';
import Article from '../Article.svelte';
import Sec from '../Sec.svelte';
import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths';
function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p;
}
let style = $state(race.RunningStyle.FrontRunner);
let surfaceApt = $state(race.AptitudeLevel.A);
let distanceApt = $state(race.AptitudeLevel.A);
let raceLen = $state(2000);
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.Runaway);
const distanceType = $derived(race.distance(raceLen));
const raceLenType = $derived(race.Distance[distanceType]);
const spurtSpeed: Array<ComputedSeries | null> = $derived([
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
{ label: `Guts 600`, y: (x) => race.spurtSpeed(x, 600, style, distanceApt, raceLen) },
distanceApt !== race.AptitudeLevel.A
? { label: `${raceLenType} A, Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, race.AptitudeLevel.A, raceLen) }
: null,
]);
const nonSpurtSpeed: Array<ComputedSeries | null> = $derived([
{ label: `Base Target Speed`, y: (x) => race.baseTargetSpeed(raceLen, style, race.Phase.LateRace, x, distanceApt) },
distanceApt !== race.AptitudeLevel.A
? {
label: `${raceLenType} A`,
y: (x) => race.baseTargetSpeed(raceLen, style, race.Phase.LateRace, x, race.AptitudeLevel.A),
}
: null,
]);
const hp: ComputedSeries[] = $derived([
{ label: 'Front Runner', y: (x) => race.maxHP(raceLen, race.RunningStyle.FrontRunner, x) },
{ label: 'Pace Chaser', y: (x) => race.maxHP(raceLen, race.RunningStyle.PaceChaser, x) },
{ label: 'Late Surger', y: (x) => race.maxHP(raceLen, race.RunningStyle.LateSurger, x) },
{ label: 'End Closer', y: (x) => race.maxHP(raceLen, race.RunningStyle.EndCloser, x) },
{ label: 'Runaway', y: (x) => race.maxHP(raceLen, race.RunningStyle.Runaway, x) },
]);
const moveLane: ComputedSeries[] = [{ label: 'Move Lane Modifier', y: (x) => race.moveLaneModifier(x) }];
const uphill: ComputedSeries[] = [
{ label: '+2 Hill', y: (x) => race.uphillMod(x, 2.0) },
{ label: '+1.5 Hill', y: (x) => race.uphillMod(x, 1.5) },
{ label: '+1 Hill', y: (x) => race.uphillMod(x, 1.0) },
];
const accel: Array<ComputedSeries | null> = $derived([
{ label: 'Early Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.EarlyRace) },
{ label: 'Mid Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.MidRace) },
{ label: 'Late Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.LateRace) },
{
label: 'Early Race Uphill',
y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.EarlyRace, race.AptitudeLevel.A, true),
},
]);
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) },
styleIsFront
? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) }
: null,
]);
const gutsSpurt: Array<ComputedSeries | null> = $derived([
{ label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) },
{ label: `Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, distanceApt, raceLen) },
// NOTE(zeph): The label is 1600 despite the input being 1400
// for raw vs final difference.
{ label: `Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, distanceApt, raceLen) },
distanceApt !== race.AptitudeLevel.A
? { label: `${raceLenType} A, Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, race.AptitudeLevel.A, raceLen) }
: null,
]);
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
const gutsHPRate: ComputedSeries[] = [{ label: 'HP Consumption Rate Multiplier', y: (x) => race.spurtHPRateMod(x) }];
const ssBoost: ComputedSeries[] = [{ label: 'Target Speed Boost', y: (x) => race.spotStruggleSpeed(x) }];
const ssDur: ComputedSeries[] = [
{ label: 'Front Runner S', y: (x) => race.spotStruggleDuration(x, race.AptitudeLevel.S) },
{ label: 'Front Runner A', y: (x) => race.spotStruggleDuration(x, race.AptitudeLevel.A) },
];
const duelSpeed: ComputedSeries[] = [{ label: 'Dueling Speed Boost', y: (x) => race.duelSpeedMod(x) }];
const duelAccel: ComputedSeries[] = [{ label: 'Dueling Acceleration Boost', y: (x) => race.duelAccelMod(x) }];
const secSpeed: Array<ComputedAreas | null> = $derived([
{
label: 'Early Race',
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1],
},
{
label: 'Mid Race',
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1],
},
styleIsFront
? {
label: 'Early Race + Mean Speed-Up Mode',
y1: (x) =>
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.frontModeEnterChance(x)),
y2: (x) =>
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], 1.04, race.frontModeEnterChance(x)),
}
: {
label: 'Early Race + Mean Pace-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], 1.04, race.paceUpEnterChance(x)),
},
styleIsFront
? {
label: 'Mid Race + Mean Speed-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], 1.04, race.frontModeEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], 1.04, race.frontModeEnterChance(x)),
}
: {
label: 'Mid Race + Mean Pace-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], 1.04, race.paceUpEnterChance(x)),
},
]);
const downhill: ComputedSeries[] = [
{ label: 'Style S', y: (x) => race.downhillAccelEnterChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => race.downhillAccelEnterChance(x) * 100 },
];
const reducedSpurt: ComputedSeries[] = [
{ label: 'Style S', y: (x) => race.reducedSpurtChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => race.reducedSpurtChance(x) * 100 },
];
const skillChance: ComputedSeries[] = [
{ label: 'Per Skill', y: (x) => race.skillWitCheck(x) * 100 },
{ label: '1 of 2 or 2 of 2', y: (x) => (race.skillWitCheck(x, 2, 1) + race.skillWitCheck(x, 2, 2)) * 100 },
{ label: '2 of 2', y: (x) => race.skillWitCheck(x, 2, 2) * 100 },
{ label: '3 of 3', y: (x) => race.skillWitCheck(x, 3, 3) * 100 },
{ label: '3 of 4 or 4 of 4', y: (x) => (race.skillWitCheck(x, 4, 3) + race.skillWitCheck(x, 4, 4)) * 100 },
];
const poskeep: ComputedSeries[] = [
{ label: 'Front Runner S', y: (x) => 100 * race.frontModeEnterChance(x * 1.1) },
{ label: 'Front Runner A', y: (x) => 100 * race.frontModeEnterChance(x) },
{ label: 'Style S', y: (x) => 100 * race.paceUpEnterChance(x * 1.1) },
{ label: 'Style A', y: (x) => 100 * race.paceUpEnterChance(x) },
];
const rushed: ComputedSeries[] = [
{ label: 'Style S', y: (x) => race.rushedChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => race.rushedChance(x) * 100 },
];
</script>
{#snippet statChart(
stat: race.Stat,
y: Array<ComputedSeries | null>,
yLabel: string,
range?: [number, number],
pick?: { len?: boolean; style?: boolean; dist?: boolean; surf?: boolean },
)}
{#if !pick?.len && !pick?.style && !pick?.dist && !pick?.surf}
<div class="mb-12 h-60 w-full md:mb-20 md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
{stat}
{y}
{yLabel}
{range}
plotOptions={{ color: { legend: true } }}
/>
</div>
{:else}
<div class="mb-36 h-60 w-full md:mb-28 md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
{stat}
{y}
{yLabel}
{range}
plotOptions={{ color: { legend: true } }}
/>
<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>
{/if}
{#if pick?.style}
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}>
{#each race.RUNNING_STYLES as [name, style] (style)}
<option value={style}>{name}</option>
{/each}
</select>
{/if}
{#if pick?.dist}
<label class="col-span-1 my-auto pr-2 text-right text-sm md:inline" for="distPick">{raceLenType}</label>
<select class="col-span-1 my-auto text-sm" id="distPick" bind:value={distanceApt}>
{#each race.APTITUDE_LEVELS as l (l)}
<option value={l}>{race.AptitudeLevel[l]}</option>
{/each}
</select>
{/if}
{#if pick?.surf}
<label class="col-span-1 my-auto pr-2 text-right text-sm md:inline" for="surfPick">Surface</label>
<select class="col-span-1 my-auto text-sm" id="surfPick" bind:value={surfaceApt}>
{#each race.APTITUDE_LEVELS as l (l)}
<option value={l}>{l}</option>
{/each}
</select>
{/if}
</div>
</div>
{/if}
{/snippet}
<Article>
{#snippet head()}
<Sec h={1} id="top" class="text-center">Race Mechanics Charts</Sec>
<p>
This article is an interactive version of <a
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
target="_blank"
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
>
to give a better sense of how mechanics scale with stats and aptitudes.
</p>
<p>
Mechanics are organized in this article according to the stats that determine them. Those that vary with multiple stats,
e.g. spurt speed, appear in all relevant sections with differing horizontal axes. Mechanics that vary with race distance,
distance aptitudes, and surface aptitudes have controls to change each by their charts.
</p>
<p>
Only numeric mechanics that vary with stats and aptitudes are shown. Descriptions of concepts and conditions are left to the
original source.
</p>
{/snippet}
<Sec h={2} id="speed">Speed</Sec>
<Sec
h={3}
id="spurt-speed"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.mpfjmgpzqalr"
>Spurt Speed</Sec
>
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed-guts">the effect of Guts</a>.</p>
{@render statChart(race.Stat.Speed, spurtSpeed, 'Spurt Speed (m/s)', [20, 26.5], { len: true, style: true, dist: true })}
<Sec
h={3}
id="late-race-speed"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.43unjgrhmm7e"
>Late Race Speed</Sec
>
<p>Base target speed during late race when the Uma is not spurting (but not dead yet).</p>
{@render statChart(race.Stat.Speed, nonSpurtSpeed, 'Base Target Speed (m/s)', [18, 23], { len: true, style: true, dist: true })}
<Sec h={2} id="stamina">Stamina</Sec>
<Sec
h={3}
id="hp"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.fnd75v9k5dx3"
>HP</Sec
>
<p>Max HP, i.e. starting HP.</p>
{@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000], { len: true })}
<Sec h={2} id="power">Power</Sec>
<Sec
h={3}
id="move-lane-mod"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.s5glz57t0wzd"
>Move Lane Modifier</Sec
>
<p>
Target speed bonus when changing lanes while under the effect of a lane change speed skill. See <a
href={resolve('/doc/frbm#lane-combo')}>lane combo</a
>.
</p>
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.6])}
<Sec
h={3}
id="uphill"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.wdj69udyo340"
>Uphill Target Speed Loss</Sec
>
<p>Target speed modifier while running uphill.</p>
{@render statChart(race.Stat.Power, uphill, 'Target Speed Modifier (m/s)', [-2, 0])}
<Sec
h={3}
id="acceleration"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.t9fv1oq5lsu9"
>Acceleration</Sec
>
<p>
Acceleration, i.e. the rate of change of forward velocity when target speed is greater than current speed. Stats do not affect
deceleration (acceleration when target speed is less than current speed).
</p>
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.55], { style: true })}
<Sec
h={3}
id="lane-change-target-speed"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.8brkec5lv1fh"
>Lane Change Target Speed</Sec
>
<p>Horizontal (rather than forward) target speed of changing lanes.</p>
<div class="mb-4 h-60 w-full md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
stat={race.Stat.Power}
y={laneChange}
yLabel="Lane Change Target Speed (CW/s)"
range={[0.01, 0.04]}
/>
</div>
<Sec
h={3}
id="conserve-power"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.9edgwx6l733b"
>Fully Charged Acceleration</Sec
>
<p>
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 })}
<Sec h={2} id="guts">Guts</Sec>
<Sec
h={3}
id="spurt-speed-guts"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.mpfjmgpzqalr"
>Spurt Speed</Sec
>
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed">the effect of Speed</a>.</p>
{@render statChart(race.Stat.Guts, gutsSpurt, 'Spurt Speed (m/s)', [20, 26.5], { len: true, style: true, dist: true })}
<Sec
h={3}
id="min-speed"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.nn1kp5g8fdhu"
>Minimum Speed</Sec
>
<p>Forced minimum speed, as well as target speed when out of HP.</p>
{@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.5], { len: true })}
<Sec
h={3}
id="hp-rate-mod"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.o0be6rj0myb1"
>Late Race HP Consumption Rate Modifier</Sec
>
<p>Multiplier on HP consumption rate during late race and last spurt phase.</p>
{@render statChart(race.Stat.Guts, gutsHPRate, 'HP Consumption Multiplier', [1.2, 1.6])}
<Sec
h={3}
id="spot-struggle"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.5l523bk8k3vz"
>Spot Struggle</Sec
>
<p>Speed boost and duration of spot struggle.</p>
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
<StatChart stat={race.Stat.Guts} y={ssBoost} yLabel="Speed Bonus (m/s)" range={[0, 0.4]} />
<StatChart stat={race.Stat.Guts} y={ssDur} yLabel="Duration (s)" range={[0, 14]} />
</div>
<Sec
h={3}
id="dueling"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.98rgrb7ww1td"
>Dueling</Sec
>
<p>Speed boost and acceleration boost of dueling.</p>
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
<StatChart stat={race.Stat.Guts} y={duelSpeed} yLabel="Speed Bonus (m/s)" range={[0, 0.8]} />
<StatChart stat={race.Stat.Guts} y={duelAccel} yLabel="Acceleration Bonus (m/s²)" range={[0, 0.16]} />
</div>
<Sec h={2} id="wit">Wit</Sec>
<Sec
h={3}
id="section-speed"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.xdha40q2176g"
>Section Speed</Sec
>
<p>Random variance in target speed per race section.</p>
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
stat={race.Stat.Wit}
yArea={secSpeed}
yLabel="Section Speed (m/s)"
range={[17, 23]}
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 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={style}>
{#each race.RUNNING_STYLES as [name, style] (style)}
<option value={style}>{name}</option>
{/each}
</select>
</div>
</div>
<Sec
h={3}
id="downhill"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.wdj69udyo340"
>Downhill Accel Mode Chance</Sec
>
<p>Chance each second to enter downhill accel mode when eligible.</p>
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 75])}
<Sec
h={3}
id="spurt-accept"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.mpfjmgpzqalr"
>Reduced Spurt Accept Chance</Sec
>
<p>Chance to accept each checked spurt delay and speed when not full spurting.</p>
{@render statChart(race.Stat.Wit, reducedSpurt, 'Accept Chance (% each candidate)', [0, 100])}
<Sec
h={3}
id="skill"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.23v0k7wyzcmb"
>Skill Activation Chance</Sec
>
<p>
Chance that each skill that has a wit check will be eligible to activate. This uses base wit, so style aptitude and passive
wit skills do not affect the chance, although mood does.
</p>
{@render statChart(race.Stat.Wit, skillChance, 'Skill Activation Chance (%)', [0, 100])}
<Sec
h={3}
id="poskeep"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.tulele26d1pm"
>Position Keep Mode Chance</Sec
>
<p>
Chance for front runners to enter speed-up or overtake mode, or for non-front runners to enter pace-up mode (but no effect on
pace-down mode), each 2 seconds when eligible.
</p>
{@render statChart(race.Stat.Wit, poskeep, 'Mode Entry Chance (%)', [0, 50])}
<Sec
h={3}
id="rushed"
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.awbjufvej6ud"
>Rushed Chance</Sec
>
<p>Chance for runners to become rushed at some point during the race. Rushed chance is bracketed to integer percentages.</p>
{@render statChart(race.Stat.Wit, rushed, 'Rushed Chance (%)', [0, 30])}
</Article>
+127
View File
@@ -0,0 +1,127 @@
<script lang="ts">
import * as skill from '$lib/data/skill';
import * as spark from '$lib/data/spark';
import Pick from '$lib/Pick.svelte';
import { onMount } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
let sparks: spark.Spark[] = $state([]);
let skills: SvelteMap<number, skill.Skill> = new SvelteMap();
onMount(async () => {
const [sp, sk] = await Promise.all([spark.sparks(), skill.skills()]);
sparks = sp;
for (const s of sk) {
skills.set(s.skill_id, s);
}
});
let group: spark.SparkGroup | undefined = $state();
const groups = $derived(spark.byGroup(sparks));
function describe(eff: spark.Effect) {
if (eff.target === spark.Target.Skill) {
const sk = skills.get(eff.value1);
return `${sk?.name} ${spark.TARGET_NAMES[eff.target]} +${eff.value2}`;
}
return `${spark.TARGET_NAMES[eff.target]} +${eff.value1}`;
}
const curEffectGroups = $derived(group?.effects ?? []);
const curDescriptions = $derived(curEffectGroups.map((g) => g.map((e) => describe(e))));
function sparkClass(t: spark.Type | null | undefined): string[] {
if (t == null) {
return [];
}
switch (t) {
case 1:
return ['stat'];
case 2:
return ['aptitude'];
case 3:
return ['unique'];
case 5:
case 4:
case 6:
case 10:
case 8:
case 11:
case 9:
return ['white-spark'];
default:
return [];
}
}
const curClass = $derived(sparkClass(group?.type));
const key = (v: spark.SparkGroup) => ({ id: v.spark_group, name: v.name });
</script>
<h1 class="text-4xl">Spark Explorer</h1>
<form class="mx-auto mt-8 rounded-md p-4 text-center shadow-md ring md:max-w-2xl">
<Pick id="spark" items={groups} class="w-full" {key} bind:value={group} required>
{#snippet option(g)}
<span class={['spark rounded px-2 py-1', sparkClass(g.type)]}>{g.name}</span>
{/snippet}
</Pick>
</form>
<svelte:boundary>
<div
class={[
'spark mx-auto mt-8 max-w-md flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md',
curClass,
]}
>
<span class="block text-xl">{group?.name}</span>
{#if group != null}
<span class="block">
{spark.TYPE_NAMES[group.type]} Spark
</span>
{/if}
<span class="block">{group?.description}</span>
</div>
<table class="mx-auto mt-8 w-full max-w-xl table-fixed border-separate border-spacing-2">
<caption class="text-xl">Possible Effects</caption>
<tbody>
{#each curDescriptions as s (s)}
<tr>
<td class="flex gap-2 border border-mist-400 p-1 text-center dark:border-mist-950">
{#each s as n (n)}
<span class="my-auto flex-1">{n}</span>
{/each}
</td>
</tr>
{/each}
</tbody>
</table>
{#snippet pending()}
<div>Loading data...</div>
{/snippet}
</svelte:boundary>
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<p>
When a spark activates during inspiration, it has an additional random roll for one of the effects listed in the table here.
All star levels of each spark have the same possible effects. Higher star levels are more likely to give better rewards.
</p>
</div>
<style>
.white-spark {
--spark-color: light-dark(var(--color-mist-200), var(--color-mist-800));
}
.stat {
--spark-color: light-dark(var(--color-sky-200), var(--color-sky-800));
}
.aptitude {
--spark-color: light-dark(var(--color-rose-300), var(--color-rose-800));
}
.unique {
--spark-color: light-dark(var(--color-lime-400), var(--color-lime-800));
}
.spark {
background-color: var(--spark-color);
}
</style>
@@ -0,0 +1,158 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as skill from '$lib/data/skill';
import SkillIcon from '$lib/SkillIcon.svelte';
let rare = $state(false);
let unique = $state(false);
let allSkillsList: skill.Skill[] = $state([]);
onMount(async () => {
allSkillsList = await skill.skills();
});
const tagTypes = $state([
{
name: 'Running Style',
includes: skill.TAGS.filter((t) => 101 <= t.tag && t.tag <= 199).map((t) => ({ ...t, on: false })),
},
{
name: 'Distance',
includes: skill.TAGS.filter((t) => 201 <= t.tag && t.tag <= 299).map((t) => ({ ...t, on: false })),
},
{
name: 'Surface',
includes: skill.TAGS.filter((t) => 501 <= t.tag && t.tag <= 599).map((t) => ({ ...t, on: false })),
},
{
name: 'Timing',
includes: skill.TAGS.filter((t) => 301 <= t.tag && t.tag <= 399).map((t) => ({ ...t, on: false })),
},
{
name: 'Effects',
includes: skill.TAGS.filter((t) => 401 <= t.tag && t.tag <= 499).map((t) => ({ ...t, on: false })),
},
{
name: 'Passive Types',
includes: skill.TAGS.filter((t) => 601 <= t.tag && t.tag <= 699).map((t) => ({ ...t, on: false })),
},
{
name: 'Scenario Skills',
includes: skill.TAGS.filter((t) => 801 <= t.tag && t.tag <= 899).map((t) => ({ ...t, on: false })),
},
]);
const tags = $derived(tagTypes.flatMap((v) => v.includes.filter((t) => t.on).map((t) => t.tag)));
const skills = $derived(
allSkillsList.filter(
(s) =>
(rare || s.group_rate === 1) &&
(unique || (s.rarity < 3 && s.unique_owner == null)) &&
tags.some((t) => s.tags.includes(t)),
),
);
</script>
<h1 class="text-4xl">Tagged Skills Explorer</h1>
<form class="mx-auto my-8 rounded-md p-4 text-center shadow-md ring md:max-w-4xl">
<div class="flex w-full gap-4">
<div class="my-auto inline-block flex-1">
<input id="rare" type="checkbox" role="switch" class="min-h-6 min-w-6 align-middle" bind:checked={rare} />
<label for="rare">Include All Levels and Rarities</label>
</div>
<div class="my-auto inline-block flex-1">
<input id="unique" type="checkbox" role="switch" class="min-h-6 min-w-6 align-middle" bind:checked={unique} />
<label for="unique">Include Uniques</label>
</div>
</div>
{#each tagTypes as { name, includes } (name)}
<div class="mt-1 flex flex-row">
<span class="my-auto h-0 flex-1 border"></span>
<span class="m-2 flex-none text-sm italic">{name}</span>
<span class="my-auto h-0 flex-1 border"></span>
</div>
<div class="flex w-full flex-wrap place-content-center-safe gap-6 md:grid-cols-4">
{#each includes as t (t.tag)}
<div class="inline-block">
<input id={t.name} type="checkbox" class="min-h-6 min-w-6 align-middle" bind:checked={t.on} />
<label for={t.name} class="align-middle">{t.name}</label>
</div>
{/each}
</div>
{/each}
</form>
{#snippet condition(cond: string)}
{@const disj = cond.replaceAll('&', ' & ').split('@')}
{#each disj as conj, i (conj)}
<span class="block font-mono text-sm">{conj}</span>
{#if i !== disj.length - 1}
<span class="block text-xs italic">or</span>
{/if}
{/each}
{/snippet}
<span class="mb-4 block text-center text-2xl">{skills.length} skills selected</span>
<div class="grid max-w-7xl grid-cols-1 gap-2 md:grid-cols-3">
{#each skills as s (s.skill_id)}
<div class="rounded-md border px-2 pt-1 pb-2 text-center shadow-sm transition-shadow hover:shadow-md">
<span class="block text-xl">
<SkillIcon icon_id={s?.icon_id} size={32} />
{s.name}
</span>
<span class="block italic">{s.description}</span>
{#each s.activations as act, i (i)}
<span class="my-2 block rounded-md border">
{#if act.precondition}
<span class="block border-b py-1">
<span class="text-xs italic">Precondition</span>
{@render condition(act.precondition)}
</span>
{/if}
<span class="block border-b py-1">
{@render condition(act.condition)}
</span>
<span class="block py-1">
{#each act.abilities as a, i (i)}
<span class="block text-sm">
{console.log(a.type)}
{skill.ABILITY_TYPE_FORMAT[a.type](a.value)}
{skill.ABILITY_SCALE_NAME[a.value_usage]}
{#if a.target && a.target !== skill.Target.Self}
{skill.TARGET_FORMAT[a.target](a.target_value)}
{/if}
</span>
{/each}
{#if act.duration != null && act.duration > 0}
<span class="block text-sm">
for {skill.tenThousandths(act.duration)}s
{skill.DURATION_SCALE_NAME[act.dur_scale]}
</span>
{/if}
</span>
</span>
{/each}
<div class="flex w-full text-xs">
<span class="flex-1">ID {s.skill_id}</span>
{#if s.unique_owner}
<span class="flex-1">{s.unique_owner}</span>
{/if}
{#if s.sp_cost}
<span class="flex-1">{s.sp_cost} SP</span>
{/if}
<span class="flex-1">Grade Value {s.grade_value}</span>
</div>
</div>
{/each}
</div>
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<ul class="ml-4 list-disc">
<li>
To see skills that can be received from MANT/Trackblazer rivals, select the running style used and the distance and surface
of the race. Some skills require meeting multiple conditions to be eligible hints, e.g. both Pace and Long; they will
instead be shown in the tool when either condition is selected.
</li>
<li>Skills for each tag are auto-generated from the game's local database.</li>
<li>Tag names are inferred from the skills that have them.</li>
</ul>
</div>
-3
View File
@@ -1,3 +0,0 @@
<h1>Inheritance Chance</h1>
<p>Given a legacy, calculate the probability distribution of activation counts for each spark.</p>
<p>TODO</p>
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import { page } from '$app/state';
import * as all from '$lib/all';
import IndexSection from '$lib/IndexSection.svelte';
const cur = $derived(all.page(page.route.id));
</script>
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy &mdash; {cur.name}</h1>
<p>{cur.description}</p>
<IndexSection pages={all.PAGES.race} />
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
interface Props {
title: Snippet | string;
children: Snippet;
additional?: Snippet;
class?: ClassValue | null;
}
let { title, children, additional, class: className }: Props = $props();
const cl = $derived([
'm-2 flex flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md',
className,
]);
</script>
<div class={cl}>
<span class="block">
{#if typeof title === 'string'}
{title}
{:else}
{@render title()}
{/if}
</span>
<span class="block text-xl">
{@render children()}
</span>
{@render additional?.()}
</div>

Some files were not shown because too many files have changed in this diff Show More