Compare commits
96 Commits
30a72d91b8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 48118efeeb | |||
| 79d4e76090 | |||
| ea39be0510 | |||
| 7bac55ca43 | |||
| 435da61405 | |||
| f3e7c7b43b | |||
| eded415df4 | |||
| 34029fe001 | |||
| ffb700545f | |||
| 70ad09a13e | |||
| 74c22c7313 | |||
| 2c1b4b9261 | |||
| e5128b6783 | |||
| b22c8307fd | |||
| cb54bbe414 | |||
| 096cff28b6 | |||
| 197df8f022 | |||
| 009dd1d695 | |||
| bdafc93315 | |||
| ef5b06d66e | |||
| 65b81612bc | |||
| 6269665e79 | |||
| cae7d5b8f4 | |||
| fca43e64b5 | |||
| bc395a0788 | |||
| 1d879cbf9f | |||
| 240a3d9a73 | |||
| 3d73460934 | |||
| 0c518a9ce2 | |||
| 5cbb6d7b1d | |||
| b0bd16fd04 | |||
| 55175c9c40 | |||
| 06e5a06680 | |||
| dbb0dcd2dc | |||
| 02f723b83a | |||
| 1f58dec491 | |||
| a6d264c79d | |||
| d62e729052 | |||
| 9ce665865e | |||
| 7d395b2cc7 | |||
| 2b69211a6c | |||
| 67377c4690 | |||
| 1d65a0ec14 | |||
| ad58edbfe1 | |||
| f8e7bfbec6 | |||
| 2e618b613f | |||
| cc2b5fcfcb | |||
| 8ef3936be5 | |||
| b06e4b537b | |||
| 7b4cd41d3b | |||
| 2c9434729a | |||
| 7f02925ff7 | |||
| b217691c8a | |||
| 4bce196c09 | |||
| aa17d8ccf3 | |||
| 26a5ca4496 | |||
| 51f942ee18 | |||
| 2c773391f0 | |||
| c66d969c82 | |||
| bc9d722ee0 | |||
| 6482f11620 | |||
| 75024c7c11 | |||
| dc78d51def | |||
| 5f0ad1b0e0 | |||
| 6db0ca5230 | |||
| 049617c22e | |||
| d431d3382a | |||
| a848c8eb72 | |||
| 871fd5fdcc | |||
| e3dd37f515 | |||
| 3557b32298 | |||
| 20db674d25 | |||
| 2a4da0a73e | |||
| e6335c1d6e | |||
| f40fea0ec5 | |||
| 96503e40f6 | |||
| c95e9bb4cc | |||
| 825fe0f2fb | |||
| 4ea689f426 | |||
| f0bb6d1926 | |||
| 13436abd1a | |||
| bed60a8a67 | |||
| 2b8ef759df | |||
| 9318bb9091 | |||
| 847f2eb8ca | |||
| 0b7201779c | |||
| 2b37c00ed0 | |||
| 2e5a78e1d8 | |||
| 277c904bc5 | |||
| 8945a3943b | |||
| 1cfd80ac3d | |||
| b4e6a34aca | |||
| b3c8699635 | |||
| 223495a04a | |||
| 40a015363c | |||
| 2869e7c160 |
@@ -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 ./...
|
||||||
@@ -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
|
||||||
@@ -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,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"`
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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(®ion, "region", "global", "region the database is for (global, jp)")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
slog.Info("open", slog.String("mdb", mdb))
|
|
||||||
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -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!!!
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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> — {page.description}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
@@ -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>
|
||||||
@@ -3,95 +3,100 @@
|
|||||||
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 =
|
const activationClass = $derived(s.activations.length === 1 ? null : 'block my-2 border rounded-md');
|
||||||
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 : 'm2 border rounded-md');
|
|
||||||
|
|
||||||
const spanClass = $derived(mention ? 'italic' : 'font-bold');
|
const spanClass = $derived(mention ? 'italic' : 'font-bold');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet condition(disj: string[])}
|
{#snippet condition(disj: string[])}
|
||||||
{#each disj as conj, i (conj)}
|
{#each disj as conj, i (conj)}
|
||||||
<div class="font-mono text-sm">{conj}</div>
|
<span class="-mb-1 block font-mono text-sm">{conj}</span>
|
||||||
{#if i !== disj.length - 1}
|
{#if i !== disj.length - 1}
|
||||||
<div class="text-xs italic">or</div>
|
<span class="text-xs italic">or</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet abilities(abils: Ability[])}
|
{#snippet abilities(abils: Ability[])}
|
||||||
{#each abils as a (a.type)}
|
{#each abils as a, i (i)}
|
||||||
<div class="text-sm">
|
<span class="block text-sm">
|
||||||
{ABILITY_TYPE_FORMAT[a.type](a.value)}
|
{ABILITY_TYPE_FORMAT[a.type](a.value)}
|
||||||
{ABILITY_SCALE_NAME[a.value_usage]}
|
{ABILITY_SCALE_NAME[a.value_usage]}
|
||||||
{#if a.target && a.target !== Target.Self}
|
{#if a.target && a.target !== Target.Self}
|
||||||
{TARGET_FORMAT[a.target](a.target_value)}
|
{TARGET_FORMAT[a.target](a.target_value)}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="group relative inline-block">
|
<span class="group relative inline-block hyphens-none">
|
||||||
<div
|
<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"
|
||||||
>
|
>
|
||||||
<div class="border-b py-1">
|
<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">
|
||||||
{s.description}
|
{s.description}
|
||||||
</div>
|
</span>
|
||||||
{#each s.activations as act, i (i)}
|
{#each s.activations as act, i (i)}
|
||||||
<div class={activationClass}>
|
<span class={activationClass}>
|
||||||
{#if act.precondition}
|
{#if act.precondition}
|
||||||
<div class="border-b py-1">
|
<span class="block border-b py-1">
|
||||||
<div class="text-xs italic">Precondition</div>
|
<span class="text-xs italic">Precondition</span>
|
||||||
{@render condition(splitCond(act.precondition))}
|
{@render condition(splitCond(act.precondition))}
|
||||||
</div>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="border-b py-1">
|
<span class="block border-b py-1">
|
||||||
{@render condition(splitCond(act.condition))}
|
{@render condition(splitCond(act.condition))}
|
||||||
</div>
|
</span>
|
||||||
<div class="py-1">
|
<span class="block py-1">
|
||||||
{@render abilities(act.abilities)}
|
{@render abilities(act.abilities)}
|
||||||
{#if act.duration}
|
{#if act.duration != null && act.duration > 0}
|
||||||
<div class="text-sm">
|
<span class="block text-sm">
|
||||||
for {tenThousandths(act.duration)}s
|
for {tenThousandths(act.duration)}s
|
||||||
{DURATION_SCALE_NAME[act.dur_scale]}
|
{DURATION_SCALE_NAME[act.dur_scale]}
|
||||||
</div>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</span>
|
||||||
<span class={spanClass}>{s.name}</span>
|
<SkillIcon icon_id={skill?.icon_id} size={16} class="inline max-h-lh align-text-bottom" />
|
||||||
</div>
|
<span class={[spanClass, 'cursor-pointer']}>{s.name}</span>
|
||||||
|
</span>
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' }],
|
||||||
|
]);
|
||||||
|
|||||||
@@ -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>(),
|
}
|
||||||
);
|
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
};
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -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 strategy–phase coefficients.
|
* Running styles for strategy–phase 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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 – {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,14 +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>
|
||||||
</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"
|
||||||
>
|
>
|
||||||
@@ -35,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>
|
||||||
|
|||||||
@@ -1,40 +1,24 @@
|
|||||||
<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>
|
|
||||||
<ul class="list-disc pl-4">
|
<IndexSection title="Race Mechanics Tools" pages={all.PAGES.race} />
|
||||||
<li>
|
<IndexSection title="Character Tools" pages={all.PAGES.chara} />
|
||||||
<a href={resolve('/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other distance aptitudes
|
<IndexSection title="Documents" pages={all.PAGES.doc} />
|
||||||
and running styles.
|
|
||||||
</li>
|
<!-- Not a real index section in, terms of types. -->
|
||||||
<li>
|
<h2 class="my-4 text-4xl">&c.</h2>
|
||||||
<a href={resolve('/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the effects
|
<ul class="mb-4 list-disc pl-4">
|
||||||
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.
|
> — Skill search by name or unique owner within Discord. Install to a server or user.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 class="mt-8 mb-4 text-4xl">About</h2>
|
|
||||||
<p>Tools to fill some gaps I've felt in Umamusume optimization.</p>
|
<h2 class="my-4 text-4xl">About</h2>
|
||||||
<p>This site is very under construction. To demonstrate just how under construction it is, here is lorem ipsum:</p>
|
<p>My repository of tools to fill some gaps I've felt in Umamusume optimization, as well as original research and editorials.</p>
|
||||||
<p>
|
|
||||||
Lorem ipsum (/ ˌ l ɔː. r ə m ˈ ɪ p. s ə m/ LOR-əm IP-səm) is a dummy or placeholder text commonly used in graphic design,
|
|
||||||
publishing, and web development. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by
|
|
||||||
the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin.
|
|
||||||
The first two words are the truncation of dolorem ipsum ("pain itself"). Lorem ipsum's purpose is to permit a page layout to be
|
|
||||||
designed, independently of the copy that will subsequently populate it, or to demonstrate various fonts of a typeface without
|
|
||||||
meaningful text that could be distracting. Versions of the Lorem ipsum text have been used in typesetting since the 1960s, when
|
|
||||||
advertisements for Letraset transfer sheets popularized it. Lorem ipsum was introduced to the digital world in the mid-1980s,
|
|
||||||
when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word
|
|
||||||
processors, including Pages and Microsoft Word, have since adopted Lorem ipsum, as have many LaTeX packages, web content
|
|
||||||
</p>
|
|
||||||
|
|||||||
@@ -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 — {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>
|
||||||
@@ -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}×</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}
|
||||||
@@ -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}×</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}
|
|
||||||
@@ -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 — {cur.name}</h1>
|
||||||
|
<p>{cur.description}</p>
|
||||||
|
<IndexSection pages={all.PAGES.doc} />
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type Snippet } from 'svelte';
|
||||||
|
import Sec from './Sec.svelte';
|
||||||
|
import { sections, type Section } from './article.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
head: Snippet;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { head, children }: Props = $props();
|
||||||
|
|
||||||
|
let tocViewed = $state(false);
|
||||||
|
let tocElem: HTMLElement | undefined = $state();
|
||||||
|
const observeTOC: IntersectionObserverCallback = (entries, obs) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
if (!e.isIntersecting) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tocViewed = true;
|
||||||
|
obs.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (tocElem == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const obs = new IntersectionObserver(observeTOC, { rootMargin: '20% 0px' });
|
||||||
|
obs.observe(tocElem);
|
||||||
|
});
|
||||||
|
|
||||||
|
function tocClass(s: Section) {
|
||||||
|
switch (s.h) {
|
||||||
|
case 1:
|
||||||
|
return null;
|
||||||
|
case 2:
|
||||||
|
return null;
|
||||||
|
case 3:
|
||||||
|
return 'ml-4';
|
||||||
|
case 4:
|
||||||
|
return 'ml-8';
|
||||||
|
case 5:
|
||||||
|
return 'ml-12';
|
||||||
|
case 6:
|
||||||
|
return 'ml-16';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article class="mx-auto max-w-4xl text-justify hyphens-auto">
|
||||||
|
{@render head()}
|
||||||
|
{#if sections.length > 0}
|
||||||
|
<Sec h={2} id="toc" toc={false}>Table of Contents</Sec>
|
||||||
|
<ul class="mb-4 list-disc pl-4" bind:this={tocElem}>
|
||||||
|
{#if tocViewed}
|
||||||
|
{#each sections as s (s.id)}
|
||||||
|
<li class={tocClass(s)}><a href={`#${s.id}`}>{@render s.children()}</a></li>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{@render children()}
|
||||||
|
</article>
|
||||||
@@ -1,31 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import type { ClassValue } from 'svelte/elements';
|
import type { ClassValue } from 'svelte/elements';
|
||||||
|
import { sections } from './article.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
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;
|
||||||
|
toc?: boolean;
|
||||||
class?: ClassValue | null;
|
class?: ClassValue | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { h = '1', id, children, 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}`);
|
||||||
const sign = $derived.by(() => {
|
const sign = $derived.by(() => {
|
||||||
switch (h) {
|
switch (h) {
|
||||||
case '1':
|
case 1:
|
||||||
return '';
|
return '';
|
||||||
case '2':
|
case 2:
|
||||||
return '§ ';
|
return '§ ';
|
||||||
default:
|
default:
|
||||||
return '¶ ';
|
return '¶ ';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!toc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sections.push({ id, h, children });
|
||||||
|
return () => sections.pop();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element this={tag} {id} class={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}>{sign} {@render children()}</a>
|
<a {href}><span class="hidden md:inline">{sign}</span> {@render children()}</a>
|
||||||
|
<div class="absolute right-0 bottom-0 inline-block text-end align-bottom text-sm whitespace-nowrap">
|
||||||
|
{#if ref != null}
|
||||||
|
<a href={ref} target="_blank" rel="noopener noreferrer">Ref ↗</a>
|
||||||
|
{/if}
|
||||||
|
{#if h >= 2}
|
||||||
|
<a href={top} class="ml-4">Top ▲</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { type Snippet } from 'svelte';
|
||||||
|
|
||||||
|
export interface Section {
|
||||||
|
id: string;
|
||||||
|
h: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sections: Section[] = $state([]);
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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 — {cur.name}</h1>
|
||||||
|
<p>{cur.description}</p>
|
||||||
|
<IndexSection pages={all.PAGES.race} />
|
||||||