Compare commits

...

11 Commits

Author SHA1 Message Date
zephyr 48118efeeb doc: document grand live mechanics
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-28 00:19:14 -04:00
zephyr 79d4e76090 mdb: sort scenarios by sort id
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #33.
2026-07-25 11:57:24 -04:00
zephyr ea39be0510 zenno: include character icons in character picks
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-24 23:09:24 -04:00
zephyr 7bac55ca43 zenno: show icons on skills 2026-07-24 22:56:31 -04:00
zephyr 435da61405 cmd: move toradl into horsebot
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #31.
2026-07-24 17:18:44 -04:00
zephyr f3e7c7b43b all: update deps
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-23 22:55:17 -04:00
zephyr eded415df4 cmd/toradl: program for downloading icons from gametora 2026-07-23 22:48:12 -04:00
zephyr 34029fe001 zenno/doc/tagged-skills: more skill info
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-21 17:01:23 -04:00
zephyr ffb700545f zenno/doc/tagged-skills: new page
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-19 22:29:15 -04:00
zephyr 70ad09a13e zenno/chara/convo: list characters in convos even when they don't have their own
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #24.
2026-07-19 15:46:41 -04:00
zephyr 74c22c7313 zenno: rename Great Escape -> Runaway everywhere
Fixes #26.
2026-07-19 15:38:01 -04:00
24 changed files with 1315 additions and 699 deletions
+12 -2
View File
@@ -1,10 +1,20 @@
# horsebot # horsebot
Discord bot serving horse game data. Website and Discord bot serving horse game data.
Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations. Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations.
## Running The `-mdb` flag is mandatory for both website and Discord bot modes.
It provides data for both the horsebot API and the discord bot.
## Running the Website
If the `-public` flag is provided, its value gives the directory to serve at `/`.
The `-img` flag gives a separate directory into which images are synced from GameTora; obtain permission from Gertas prior to using it.
The images are served on `/img/`.
## Running the Discord Bot
Provide the `-token` flag pointing to a file containing the token.
The bot always uses the Gateway API. The bot always uses the Gateway API.
If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided. If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided.
+16
View File
@@ -36,6 +36,7 @@ func main() {
addr string addr string
mdbf string mdbf string
public string public string
img string
// discord // discord
tokenFile string tokenFile string
apiRoute string apiRoute string
@@ -47,6 +48,7 @@ func main() {
flag.StringVar(&addr, "http", ":13669", "`address` to bind HTTP server") flag.StringVar(&addr, "http", ":13669", "`address` to bind HTTP server")
flag.StringVar(&mdbf, "mdb", "", "`path` to master.mdb") 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")
@@ -86,6 +88,17 @@ func main() {
os.Exit(1) 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 var client *bot.Client
if tokenFile != "" { if tokenFile != "" {
skills, err := mdb.Skills(ctx, db) skills, err := mdb.Skills(ctx, db)
@@ -128,6 +141,9 @@ func main() {
if public != "" { if public != "" {
mux.Handle("GET /", httpmiddle.Compress(5)(http.FileServerFS(os.DirFS(public)))) mux.Handle("GET /", httpmiddle.Compress(5)(http.FileServerFS(os.DirFS(public))))
} }
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", 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/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/character", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.Characters)))
+173
View File
@@ -0,0 +1,173 @@
package main
import (
"context"
"errors"
"fmt"
"image"
"image/png"
"log/slog"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gen2brain/avif"
"golang.org/x/image/draw"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
func saveImgs(ctx context.Context, db *sqlitex.Pool, out string) {
skill, err := skillIconIDs(ctx, db)
if err != nil {
slog.Error("skills", slog.Any("err", err))
}
chara, err := charaIDs(ctx, db)
if err != nil {
slog.Error("characters", slog.Any("err", err))
}
rank, err := rankIDs(ctx, db)
if err != nil {
slog.Error("ranks", slog.Any("err", err))
}
g, ctx := errgroup.WithContext(ctx)
tick := time.NewTicker(250 * time.Millisecond)
idouts(ctx, g, tick, skill, filepath.Join(out, "skill"), "https://media.gametora.com/umamusume/skills/icon/%d.png", image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16))
idouts(ctx, g, tick, chara, filepath.Join(out, "chara"), "https://gametora.com/images/umamusume/characters/icons/chr_icon_%d.png", image.Pt(256, 256), image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16))
idouts(ctx, g, tick, rank, filepath.Join(out, "rank"), "https://media.gametora.com/umamusume/ui/rank/%02d.png", image.Pt(128, 128), image.Pt(64, 64), image.Pt(32, 32), image.Pt(16, 16))
idouts(ctx, g, tick, rank, filepath.Join(out, "rank", "simple"), "https://media.gametora.com/umamusume/ui/rank/simple/%02d.png", image.Pt(50, 50), image.Pt(32, 32), image.Pt(16, 16))
if err := g.Wait(); err != nil {
slog.Error("output", slog.Any("err", err))
}
}
// load scans all results of sql and appends them to r.
func load[T any](ctx context.Context, db *sqlitex.Pool, r []T, sql string, row func(*sqlite.Stmt) T) ([]T, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, err
}
stmt, err := conn.Prepare(sql)
if err != nil {
return nil, err
}
for {
ok, err := stmt.Step()
if err != nil {
return r, err
}
if !ok {
break
}
r = append(r, row(stmt))
}
return r, err
}
func skillIconIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) {
return load(ctx, db, nil, "SELECT DISTINCT icon_id FROM skill_data", func(s *sqlite.Stmt) int32 {
return s.ColumnInt32(0)
})
}
func charaIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) {
return load(ctx, db, nil, "SELECT id FROM chara_data", func(s *sqlite.Stmt) int32 {
return s.ColumnInt32(0)
})
}
func rankIDs(ctx context.Context, db *sqlitex.Pool) ([]int32, error) {
return load(ctx, db, nil, "SELECT id-1 FROM single_mode_rank", func(s *sqlite.Stmt) int32 {
return s.ColumnInt32(0)
})
}
var pngenc = &png.Encoder{CompressionLevel: png.BestCompression}
func idouts(ctx context.Context, g *errgroup.Group, tick *time.Ticker, ids []int32, p, urlf string, sizes ...image.Point) {
for _, id := range ids {
g.Go(func() error {
n := strconv.Itoa(int(id))
p := filepath.Join(p, n)
_, err := os.Stat(p)
if !errors.Is(err, os.ErrNotExist) {
slog.DebugContext(ctx, "exists", slog.String("path", p))
return nil
}
if err := os.MkdirAll(p, 0777); err != nil {
return err
}
url := fmt.Sprintf(urlf, id)
select {
case <-ctx.Done():
return ctx.Err()
case <-tick.C: // continue on
}
img, err := dlimg(ctx, url)
if err != nil {
return err
}
slog.InfoContext(ctx, "resize", slog.String("url", url))
rs := resize(img, sizes...)
for _, r := range rs {
g.Go(func() error {
m := r.Bounds().Max
o := filepath.Join(p, fmt.Sprintf("%dx%d.png", m.X, m.Y))
slog.InfoContext(ctx, "write", slog.String("path", o))
f, err := os.Create(o)
if err != nil {
return err
}
defer f.Close()
return pngenc.Encode(f, r)
})
g.Go(func() error {
m := r.Bounds().Max
o := filepath.Join(p, fmt.Sprintf("%dx%d.avif", m.X, m.Y))
slog.InfoContext(ctx, "write", slog.String("path", o))
f, err := os.Create(o)
if err != nil {
return err
}
defer f.Close()
return avif.Encode(f, r)
})
}
return nil
})
}
}
func dlimg(ctx context.Context, url string) (image.Image, error) {
slog.InfoContext(ctx, "download", slog.String("url", url))
ctx, stop := context.WithTimeout(ctx, 5*time.Second)
defer stop()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
slog.InfoContext(ctx, "decode", slog.String("url", url))
img, _, err := image.Decode(resp.Body)
return img, err
}
func resize(img image.Image, sizes ...image.Point) []image.Image {
rs := make([]image.Image, 0, len(sizes))
for _, sz := range sizes {
d := image.NewNRGBA(image.Rectangle{Min: image.Point{}, Max: sz})
draw.CatmullRom.Scale(d, d.Bounds(), img, img.Bounds(), draw.Src, nil)
rs = append(rs, d)
}
return rs
}
+34
View File
@@ -15,6 +15,9 @@ This file is my notes from exploring the database.
- 119 is scenario full titles (e.g. The Beginning: URA Finale), 120 is scenario descriptions, 237 is scenario names (e.g. URA Finale) - 119 is scenario full titles (e.g. The Beginning: URA Finale), 120 is scenario descriptions, 237 is scenario names (e.g. URA Finale)
- 130 is epithet names, 131 is epithet descriptions - 130 is epithet names, 131 is epithet descriptions
in the decomp, text data categories are named in `public enum MasterString.Category`.
some categories seem to be missing, though.
# succession factor (sparks) # succession factor (sparks)
tables are succession_factor and succession_factor_effect tables are succession_factor and succession_factor_effect
@@ -317,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!!!
+15 -10
View File
@@ -4,32 +4,37 @@ go 1.25.5
require ( require (
github.com/disgoorg/disgo v0.19.6 github.com/disgoorg/disgo v0.19.6
github.com/go-chi/chi/v5 v5.3.0 github.com/gen2brain/avif v0.6.0
github.com/go-chi/chi/v5 v5.3.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/junegunn/fzf v0.73.1 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.1.0 // indirect 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/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/compress v1.19.1 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.24 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.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.53.0 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/crypto v0.54.0 // indirect
golang.org/x/tools v0.46.0 // indirect golang.org/x/sys v0.47.0 // indirect
modernc.org/libc v1.73.4 // 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.52.0 // indirect modernc.org/sqlite v1.54.0 // indirect
) )
+40 -32
View File
@@ -2,8 +2,8 @@ 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.6 h1:1xJQt6KZgiqvBOnjuziG1gE4Iqd0aJn8DNfE6eE+0qw= github.com/disgoorg/disgo v0.19.6 h1:1xJQt6KZgiqvBOnjuziG1gE4Iqd0aJn8DNfE6eE+0qw=
github.com/disgoorg/disgo v0.19.6/go.mod h1:NnV63iw4lJdF1fnV0gX27XR43ZgRGqnL122svRMTgTE= github.com/disgoorg/disgo v0.19.6/go.mod h1:NnV63iw4lJdF1fnV0gX27XR43ZgRGqnL122svRMTgTE=
github.com/disgoorg/godave v0.1.0 h1:3g0Zqzz+zNaxQTVLfCnl5eZKGqZk6cM/JLLbMKCtZQQ= github.com/disgoorg/godave v0.3.0 h1:37F3ZuiMd8/EXeoCtTeE8iP2eT2nWsfEa4/ITwoKfe4=
github.com/disgoorg/godave v0.1.0/go.mod h1:OreAC3hpabr39bMVA+jwOVDq1EUPXH5A0XUBiZaDI1Y= 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=
@@ -12,8 +12,12 @@ 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.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -22,14 +26,14 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/junegunn/fzf v0.73.1 h1:Ni9jaVMxsehcx9IwJ4ZAL/sr2c4dOEg1DHIAwPUVlyo= github.com/junegunn/fzf v0.74.1 h1:rCGVdCSoOkfcfTxtxCO/vG2yceDjRlKSlv7IfCGkc/Y=
github.com/junegunn/fzf v0.73.1/go.mod h1:hKDkO8orBWT5VLIZOL8oyhxn7mRLXdGyu+gz1Ss58pU= github.com/junegunn/fzf v0.74.1/go.mod h1:hKDkO8orBWT5VLIZOL8oyhxn7mRLXdGyu+gz1Ss58pU=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= 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 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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=
@@ -42,34 +46,38 @@ 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
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.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= 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 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= 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=
@@ -78,8 +86,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= 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=
+1 -1
View File
@@ -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
+621 -566
View File
File diff suppressed because it is too large Load Diff
+16 -13
View File
@@ -20,32 +20,35 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.60.1", "@sveltejs/kit": "^2.70.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.3.0", "@tailwindcss/vite": "^4.3.3",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/node": "^22.19.19", "@types/node": "^22.20.1",
"@vitest/browser-playwright": "^4.1.0", "@vitest/browser-playwright": "^4.1.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"eslint": "^10.4.0", "eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.17.1", "eslint-plugin-svelte": "^3.22.0",
"globals": "^17.6.0", "globals": "^17.7.0",
"playwright": "^1.60.0", "playwright": "^1.61.1",
"prettier": "^3.8.3", "prettier": "^3.9.6",
"prettier-plugin-svelte": "^3.5.2", "prettier-plugin-svelte": "^3.5.2",
"prettier-plugin-tailwindcss": "^0.7.4", "prettier-plugin-tailwindcss": "^0.7.4",
"svelte": "^5.55.7", "svelte": "^5.56.7",
"svelte-check": "^4.4.8", "svelte-check": "^4.7.3",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.59.3", "typescript-eslint": "^8.65.0",
"vite": "^7.3.3", "vite": "^7.3.6",
"vitest": "^4.1.0", "vitest": "^4.1.0",
"vitest-browser-svelte": "^2.1.1" "vitest-browser-svelte": "^2.2.1"
}, },
"dependencies": { "dependencies": {
"@observablehq/plot": "^0.6.17", "@observablehq/plot": "^0.6.17",
"mathjs": "^15.2.0" "mathjs": "^15.2.0"
},
"allowScripts": {
"esbuild@0.28.1": true
} }
} }
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
const sizes = [256, 64, 32, 16] as const;
interface Props {
chara_id: number | null | undefined;
size: (typeof sizes)[number];
}
let { chara_id, size, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
const icon = $derived(chara_id ?? 0);
const avifs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/chara/${icon}/${sz}x${sz}.avif ${sz / size}x`)
.join(', '),
);
const pngs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/chara/${icon}/${sz}x${sz}.png ${sz / size}x`)
.join(', '),
);
const src = $derived(icon !== 0 ? `/img/chara/${icon}/${sizes[0]}x${sizes[0]}.png` : `/img/skill/10011`);
</script>
{#if icon !== 0}
<picture {...rest}>
<source srcset={avifs} type="image/avif" />
<source srcset={pngs} type="image/png" />
<img {src} alt={`Icon for character ID ${chara_id}`} class="inline" width={size} height={size} loading="lazy" />
</picture>
{:else}
<div class={`w-[${size}px] h-[${size}px]`}></div>
{/if}
+10 -4
View File
@@ -1,23 +1,29 @@
<script lang="ts"> <script lang="ts">
import type { Character } from '$lib/data/character'; import type { Character } from '$lib/data/character';
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements'; import type { ClassValue } from 'svelte/elements';
import Pick from './Pick.svelte'; import Pick from './Pick.svelte';
import CharaIcon from './CharaIcon.svelte';
interface Props { interface Props {
id: string; id: string;
characters: Character[] | null; characters: Character[] | null;
value: Character | undefined; value: Character | undefined;
class?: ClassValue | null; class?: ClassValue | null;
option?: Snippet<[Character]>;
required?: boolean; required?: boolean;
} }
let { id, characters, value = $bindable(), class: className, option, required = false }: Props = $props(); let { id, characters, value = $bindable(), class: className, required = false }: Props = $props();
const charas = $derived(characters ?? []); const charas = $derived(characters ?? []);
const key = (v: Character) => ({ id: v.chara_id, name: v.name }); const key = (v: Character) => ({ id: v.chara_id, name: v.name });
</script> </script>
<Pick {id} items={charas} {key} bind:value {option} class={className} {required} /> <Pick {id} items={charas} {key} bind:value class={className} {required}>
{#snippet option(c: Character)}
<span>
<CharaIcon chara_id={c.chara_id} size={32} />
{c.name}
</span>
{/snippet}
</Pick>
+8 -1
View File
@@ -9,6 +9,7 @@
type Ability, type Ability,
type Skill, type Skill,
} from './data/skill'; } from './data/skill';
import SkillIcon from './SkillIcon.svelte';
interface Props { interface Props {
skill: Skill | null | undefined; skill: Skill | null | undefined;
@@ -24,6 +25,7 @@
group_rate: 1, group_rate: 1,
wit_check: false, wit_check: false,
activations: [], activations: [],
tags: [],
icon_id: 0, icon_id: 0,
}; };
@@ -62,9 +64,13 @@
<span class="group relative inline-block hyphens-none"> <span class="group relative inline-block hyphens-none">
<span <span
class="skill-tip invisible absolute bottom-4 -left-1/2 flex w-80 flex-col rounded-lg border px-2 text-center opacity-0 shadow-lg transition-all group-hover:visible group-hover:bottom-6 group-hover:opacity-100" class="skill-tip invisible absolute bottom-4 -left-1/2 flex min-w-80 flex-col rounded-lg border px-2 pt-2 text-center opacity-0 shadow-lg transition-all group-hover:visible group-hover:bottom-6 group-hover:opacity-100"
role="tooltip" role="tooltip"
> >
<span class="block text-xl">
<SkillIcon icon_id={skill?.icon_id} size={32} class="inline" />
{s.name}
</span>
<span class="block border-b py-1 hyphens-auto"> <span class="block border-b py-1 hyphens-auto">
{s.description} {s.description}
</span> </span>
@@ -91,5 +97,6 @@
</span> </span>
{/each} {/each}
</span> </span>
<SkillIcon icon_id={skill?.icon_id} size={16} class="inline max-h-lh align-text-bottom" />
<span class={[spanClass, 'cursor-pointer']}>{s.name}</span> <span class={[spanClass, 'cursor-pointer']}>{s.name}</span>
</span> </span>
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
const sizes = [64, 32, 16] as const;
interface Props {
icon_id: number | null | undefined;
size: (typeof sizes)[number];
}
let { icon_id, size, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
const icon = $derived(icon_id ?? 0);
const avifs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/skill/${icon}/${sz}x${sz}.avif ${sz / size}x`)
.join(', '),
);
const pngs = $derived(
sizes
.filter((sz) => sz >= size)
.map((sz) => `/img/skill/${icon}/${sz}x${sz}.png ${sz / size}x`)
.join(', '),
);
const src = $derived(icon !== 0 ? `/img/skill/${icon}/${sizes[0]}x${sizes[0]}.png` : `/img/skill/10011`);
</script>
{#if icon !== 0}
<picture {...rest}>
<source srcset={avifs} type="image/avif" />
<source srcset={pngs} type="image/png" />
<img {src} alt={`Skill icon ID ${icon_id}`} class="inline" width={size} height={size} loading="lazy" />
</picture>
{:else}
<div class={`w-[${size}px] h-[${size}px]`}></div>
{/if}
+5
View File
@@ -79,6 +79,11 @@ export const PAGES = {
name: 'Spark Explorer', name: 'Spark Explorer',
description: 'View all possible inheritance effects of any spark.', 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[], ] satisfies Page[],
} as const; } as const;
+1 -1
View File
@@ -18,7 +18,7 @@ const styleMap = {
Sentou: RunningStyle.PaceChaser, Sentou: RunningStyle.PaceChaser,
Sasi: RunningStyle.LateSurger, Sasi: RunningStyle.LateSurger,
Oikomi: RunningStyle.EndCloser, Oikomi: RunningStyle.EndCloser,
Oonige: RunningStyle.GreatEscape, Oonige: RunningStyle.Runaway,
} as const; } as const;
export interface ImportUma { export interface ImportUma {
+62
View File
@@ -48,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.
*/ */
@@ -133,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>> = {
@@ -152,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 {
@@ -329,3 +339,55 @@ export async function skillGroups(): Promise<SkillGroup[]> {
const resp = await fetch('/api/global/skill-group'); const resp = await fetch('/api/global/skill-group');
return resp.json(); return resp.json();
} }
export const TAGS = [
{ name: 'Front Runner', tag: 101 },
{ name: 'Pace Chaser', tag: 102 },
{ name: 'Late Surger', tag: 103 },
{ name: 'End Closer', tag: 104 },
{ name: 'Sprint', tag: 201 },
{ name: 'Mile', tag: 202 },
{ name: 'Medium', tag: 203 },
{ name: 'Long', tag: 204 },
{ name: 'Early Race', tag: 301 },
{ 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;
+6 -6
View File
@@ -260,7 +260,7 @@ export function finalStat(adjStat: number, skillPassive: number): number {
/** /**
* Running styles for strategyphase coefficients. * Running styles for strategyphase coefficients.
* Great Escape is distinguished as a separate style even though it is * Runaway is distinguished as a separate style even though it is
* mechanically identical to Front Runner. * mechanically identical to Front Runner.
*/ */
export enum RunningStyle { export enum RunningStyle {
@@ -268,7 +268,7 @@ export enum RunningStyle {
PaceChaser, PaceChaser,
LateSurger, LateSurger,
EndCloser, EndCloser,
GreatEscape, Runaway,
} }
/** /**
@@ -279,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;
/** /**
@@ -325,7 +325,7 @@ const hpStrategyCoeff = {
[RunningStyle.PaceChaser]: 0.89, [RunningStyle.PaceChaser]: 0.89,
[RunningStyle.LateSurger]: 1.0, [RunningStyle.LateSurger]: 1.0,
[RunningStyle.EndCloser]: 0.995, [RunningStyle.EndCloser]: 0.995,
[RunningStyle.GreatEscape]: 0.86, [RunningStyle.Runaway]: 0.86,
} as const; } as const;
/** /**
@@ -595,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;
@@ -711,7 +711,7 @@ const conserveStratDistCoef = {
[Distance.Medium]: 0.86, [Distance.Medium]: 0.86,
[Distance.Long]: 0.9, [Distance.Long]: 0.9,
}, },
[RunningStyle.GreatEscape]: { [RunningStyle.Runaway]: {
[Distance.Sprint]: 1, [Distance.Sprint]: 1,
[Distance.Mile]: 1, [Distance.Mile]: 1,
[Distance.Medium]: 1, [Distance.Medium]: 1,
+3 -4
View File
@@ -8,13 +8,12 @@
const convoMap = $derived(byChara(conversations)); const convoMap = $derived(byChara(conversations));
const popular = $derived(groupPopulars(conversations)); const popular = $derived(groupPopulars(conversations));
let characters: Character[] = $state([]); let characters: Character[] = $state([]);
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const names = $derived(charaNames(characters ?? [])); const names = $derived(charaNames(characters ?? []));
const minSuggest = 8; const minSuggest = 8;
onMount(async () => { onMount(async () => {
const [convos, charas] = await Promise.all([conversation(), character()]); [conversations, characters] = await Promise.all([conversation(), character()]);
conversations = convos;
characters = charas.filter((c) => convoMap.has(c.chara_id));
}); });
let chara: Character | undefined = $state(); let chara: Character | undefined = $state();
@@ -43,7 +42,7 @@
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row"> <div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row">
<div class="m-4 flex-1 md:mt-3"> <div class="m-4 flex-1 md:mt-3">
<label for="chara" class="hidden md:inline">Character</label> <label for="chara" class="hidden md:inline">Character</label>
<CharaPick id="chara" {characters} class="w-full" bind:value={chara} required /> <CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
</div> </div>
<div class="m-4 flex-1 md:mt-3"> <div class="m-4 flex-1 md:mt-3">
<label for="convo" class="hidden md:inline">Conversation</label> <label for="convo" class="hidden md:inline">Conversation</label>
+2 -5
View File
@@ -179,7 +179,7 @@
{ label: 'Style S', y: (x) => downhillAccelEnterChance(x * 1.1) * 100 }, { label: 'Style S', y: (x) => downhillAccelEnterChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => downhillAccelEnterChance(x) * 100 }, { label: 'Style A', y: (x) => downhillAccelEnterChance(x) * 100 },
]; ];
const secIsFront = $derived(secSpeedStyle === RunningStyle.FrontRunner || secSpeedStyle === RunningStyle.GreatEscape); const secIsFront = $derived(secSpeedStyle === RunningStyle.FrontRunner || secSpeedStyle === RunningStyle.Runaway);
const secSpeedSeries: Array<ComputedAreas | null> = $derived([ const secSpeedSeries: Array<ComputedAreas | null> = $derived([
{ {
label: 'Early Race', label: 'Early Race',
@@ -326,10 +326,7 @@
<Sec h={3} id="runaway">Runaway</Sec> <Sec h={3} id="runaway">Runaway</Sec>
<p> <p>
The skill <Skill skill={skills.runaway} /> converts front runners into the <i>Great Escape</i> running style. However, no The skill <Skill skill={skills.runaway} /> converts front runners into the Runaway running style.
player has ever uttered the words "Great Escape" when talking about Umamusume, presumably because Runaway is a much cooler
name. ("Great Escape" is a direct translation of Japanese 大逃げ <i>oonige</i>, whereas "Front Runner" is a more liberal
localization of 逃げ <i>nige</i> that technically just means "escape.")
</p> </p>
<p> <p>
Runaways are still front runners for all purposes. The difference is just different numbers for things like base speed and Runaways are still front runners for all purposes. The difference is just different numbers for things like base speed and
+6 -3
View File
@@ -14,7 +14,7 @@
let surfaceApt = $state(race.AptitudeLevel.A); let surfaceApt = $state(race.AptitudeLevel.A);
let distanceApt = $state(race.AptitudeLevel.A); let distanceApt = $state(race.AptitudeLevel.A);
let raceLen = $state(2000); let raceLen = $state(2000);
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.GreatEscape); const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.Runaway);
const distanceType = $derived(race.distance(raceLen)); const distanceType = $derived(race.distance(raceLen));
const raceLenType = $derived(race.Distance[distanceType]); const raceLenType = $derived(race.Distance[distanceType]);
@@ -39,7 +39,7 @@
{ label: 'Pace Chaser', y: (x) => race.maxHP(raceLen, race.RunningStyle.PaceChaser, 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: 'Late Surger', y: (x) => race.maxHP(raceLen, race.RunningStyle.LateSurger, x) },
{ label: 'End Closer', y: (x) => race.maxHP(raceLen, race.RunningStyle.EndCloser, x) }, { label: 'End Closer', y: (x) => race.maxHP(raceLen, race.RunningStyle.EndCloser, x) },
{ label: 'Runaway', y: (x) => race.maxHP(raceLen, race.RunningStyle.GreatEscape, 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 moveLane: ComputedSeries[] = [{ label: 'Move Lane Modifier', y: (x) => race.moveLaneModifier(x) }];
const uphill: ComputedSeries[] = [ const uphill: ComputedSeries[] = [
@@ -298,7 +298,10 @@
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.t9fv1oq5lsu9" ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.t9fv1oq5lsu9"
>Acceleration</Sec >Acceleration</Sec
> >
<p>Acceleration.</p> <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 })} {@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.55], { style: true })}
<Sec <Sec
@@ -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 -1
View File
@@ -30,7 +30,7 @@
const careerMod = $derived(isCareer ? 400 : 0); const careerMod = $derived(isCareer ? 400 : 0);
const powerStat = $derived(rawPower + careerMod); const powerStat = $derived(rawPower + careerMod);
const gutsStat = $derived(rawGuts + careerMod); const gutsStat = $derived(rawGuts + careerMod);
const style = $derived(isRunaway ? RunningStyle.GreatEscape : RunningStyle.FrontRunner); const style = $derived(isRunaway ? RunningStyle.Runaway : RunningStyle.FrontRunner);
const phases = [Phase.EarlyRace, Phase.MidRace, Phase.LateRace] as const; const phases = [Phase.EarlyRace, Phase.MidRace, Phase.LateRace] as const;
const accel = $derived(phases.map((p) => acceleration(powerStat, style, surfApt, p))); const accel = $derived(phases.map((p) => acceleration(powerStat, style, surfApt, p)));
+48 -50
View File
@@ -1,76 +1,74 @@
<script lang="ts"> <script lang="ts">
import type { ComputedSeries } from '$lib/chart'; import type { ComputedSeries } from '$lib/chart';
import { AptitudeLevel, duelSpeedMod, inverseSpurtSpeed, RunningStyle, spurtSpeed, Stat } from '$lib/race'; import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number');
const stylesList = [
['Front Runner', RunningStyle.FrontRunner],
['Pace Chaser', RunningStyle.PaceChaser],
['Late Surger', RunningStyle.LateSurger],
['End Closer', RunningStyle.EndCloser],
['Great Escape', RunningStyle.GreatEscape],
] as const;
const skillSpeeds = [0.45, 0.35, 0.25, 0.15] as const; const skillSpeeds = [0.45, 0.35, 0.25, 0.15] as const;
let rawSpeed: number = $state(1200); let rawSpeed: number = $state(1200);
let rawGuts: number = $state(1200); let rawGuts: number = $state(1200);
let style: RunningStyle = $state(RunningStyle.FrontRunner); let style: race.RunningStyle = $state(race.RunningStyle.FrontRunner);
let opponentStyle: RunningStyle = $state(RunningStyle.PaceChaser); let opponentStyle: race.RunningStyle = $state(race.RunningStyle.PaceChaser);
let distanceApt: AptitudeLevel = $state(AptitudeLevel.S); let distanceApt: race.AptitudeLevel = $state(race.AptitudeLevel.S);
let raceLen: number = $state(2000); let raceLen: number = $state(2000);
let isCareer: boolean = $state(false); let isCareer: boolean = $state(false);
const careerMod = $derived(isCareer ? 400 : 0); const careerMod = $derived(isCareer ? 400 : 0);
const speedStat = $derived(rawSpeed + careerMod); const speedStat = $derived(rawSpeed + careerMod);
const gutsStat = $derived(rawGuts + careerMod); const gutsStat = $derived(rawGuts + careerMod);
const speed = $derived(spurtSpeed(speedStat, gutsStat, style, distanceApt, raceLen)); const speed = $derived(race.spurtSpeed(speedStat, gutsStat, style, distanceApt, raceLen));
const duelBonus = $derived(duelSpeedMod(gutsStat)); const duelBonus = $derived(race.duelSpeedMod(gutsStat));
const sProf = $derived([ const sProf = $derived([
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.S, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.FrontRunner, race.AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.PaceChaser, AptitudeLevel.S, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.PaceChaser, race.AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.LateSurger, AptitudeLevel.S, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.LateSurger, race.AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.EndCloser, race.AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.S, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.Runaway, race.AptitudeLevel.S, raceLen) - careerMod,
]); ]);
const aProf = $derived([ const aProf = $derived([
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.A, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.FrontRunner, race.AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.PaceChaser, AptitudeLevel.A, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.PaceChaser, race.AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.LateSurger, AptitudeLevel.A, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.LateSurger, race.AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.EndCloser, AptitudeLevel.A, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.EndCloser, race.AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.A, raceLen) - careerMod, race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.Runaway, race.AptitudeLevel.A, raceLen) - careerMod,
]); ]);
const skillProf = $derived( const skillProf = $derived(
skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, AptitudeLevel.S, raceLen) - careerMod]), skillSpeeds.map((v) => [
v,
race.inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, race.AptitudeLevel.S, raceLen) - careerMod,
]),
); );
const speedChart: Array<ComputedSeries | null> = $derived([ const speedChart: Array<ComputedSeries | null> = $derived([
{ label: 'Aptitude S', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.S, raceLen) }, { label: 'Aptitude S', y: (x) => race.spurtSpeed(x, gutsStat, style, race.AptitudeLevel.S, raceLen) },
{ {
label: `Aptitude ${AptitudeLevel[distanceApt]} + Dueling`, label: `Aptitude ${race.AptitudeLevel[distanceApt]} + Dueling`,
y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) + duelBonus, y: (x) => race.spurtSpeed(x, gutsStat, style, distanceApt, raceLen) + duelBonus,
}, },
distanceApt !== AptitudeLevel.S distanceApt !== race.AptitudeLevel.S
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) } ? {
label: `Aptitude ${race.AptitudeLevel[distanceApt]}`,
y: (x) => race.spurtSpeed(x, gutsStat, style, distanceApt, raceLen),
}
: null, : null,
]); ]);
const gutsChart: Array<ComputedSeries | null> = $derived([ const gutsChart: Array<ComputedSeries | null> = $derived([
{ label: 'Aptitude S', y: (x) => spurtSpeed(speedStat, x, style, AptitudeLevel.S, raceLen) }, { label: 'Aptitude S', y: (x) => race.spurtSpeed(speedStat, x, style, race.AptitudeLevel.S, raceLen) },
{ {
label: `Aptitude ${AptitudeLevel[distanceApt]} + Dueling`, label: `Aptitude ${race.AptitudeLevel[distanceApt]} + Dueling`,
y: (x) => spurtSpeed(speedStat, x, style, distanceApt, raceLen) + duelSpeedMod(x), y: (x) => race.spurtSpeed(speedStat, x, style, distanceApt, raceLen) + race.duelSpeedMod(x),
}, },
distanceApt !== AptitudeLevel.S distanceApt !== race.AptitudeLevel.S
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(speedStat, x, style, distanceApt, raceLen) } ? {
label: `Aptitude ${race.AptitudeLevel[distanceApt]}`,
y: (x) => race.spurtSpeed(speedStat, x, style, distanceApt, raceLen),
}
: null, : null,
]); ]);
const range: [number, number] = $derived([ const range: [number, number] = [20, 27.5];
spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen),
spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen) + duelBonus,
]);
</script> </script>
<h1 class="text-4xl">Spurt Speed Calculator</h1> <h1 class="text-4xl">Spurt Speed Calculator</h1>
@@ -86,16 +84,16 @@
<div class="m-4"> <div class="m-4">
<label for="style">Style</label> <label for="style">Style</label>
<select id="style" required bind:value={style} class="w-full"> <select id="style" required bind:value={style} class="w-full">
{#each stylesList as [name, style] (style)} {#each race.RUNNING_STYLES as [name, style] (style)}
<option value={style}>{style === RunningStyle.GreatEscape ? 'Great Escape (Runaway)' : name}</option> <option value={style}>{name}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="m-4"> <div class="m-4">
<label for="distanceApt">Distance Aptitude</label> <label for="distanceApt">Distance Aptitude</label>
<select id="distanceApt" required bind:value={distanceApt} class="w-full"> <select id="distanceApt" required bind:value={distanceApt} class="w-full">
{#each aptsList.toReversed() as [name, val] (val)} {#each race.APTITUDE_LEVELS as apt (apt)}
<option value={val}>{name}</option> <option value={apt}>{race.AptitudeLevel[apt]}</option>
{/each} {/each}
</select> </select>
</div> </div>
@@ -112,13 +110,13 @@
<CalcInfo title="Target Spurt Speed" class="flex-1">{speed.toFixed(3)} m/s</CalcInfo> <CalcInfo title="Target Spurt Speed" class="flex-1">{speed.toFixed(3)} m/s</CalcInfo>
<CalcInfo title="Dueling Bonus" class="flex-1">+{duelBonus.toFixed(3)} m/s</CalcInfo> <CalcInfo title="Dueling Bonus" class="flex-1">+{duelBonus.toFixed(3)} m/s</CalcInfo>
</div> </div>
{#each [[AptitudeLevel.A, aProf] as const, [AptitudeLevel.S, sProf] as const] as [level, inv] (level)} {#each [[race.AptitudeLevel.A, aProf] as const, [race.AptitudeLevel.S, sProf] as const] as [level, inv] (level)}
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<span class="mt-8 block w-full"> <span class="mt-8 block w-full">
With {AptitudeLevel[level]} proficiency, the equivalent speed is With {race.AptitudeLevel[level]} proficiency, the equivalent speed is
</span> </span>
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
{#each stylesList as [styleName, s] (s)} {#each race.RUNNING_STYLES as [styleName, s] (s)}
<CalcInfo title={styleName} class={['flex-1', { 'border-2': s === style }]}>{inv[s]}</CalcInfo> <CalcInfo title={styleName} class={['flex-1', { 'border-2': s === style }]}>{inv[s]}</CalcInfo>
{/each} {/each}
</div> </div>
@@ -128,8 +126,8 @@
<span class="mt-8 block w-full"> <span class="mt-8 block w-full">
While a speed skill is active, the equivalent speed for a distance S While a speed skill is active, the equivalent speed for a distance S
<select id="opponentStyle" required bind:value={opponentStyle}> <select id="opponentStyle" required bind:value={opponentStyle}>
{#each stylesList as [name, style] (style)} {#each race.RUNNING_STYLES as [name, style] (style)}
<option value={style}>{style === RunningStyle.GreatEscape ? 'Great Escape (Runaway)' : name}</option> <option value={style}>{style === race.RunningStyle.Runaway ? 'Great Escape (Runaway)' : name}</option>
{/each} {/each}
</select> </select>
is is
@@ -141,6 +139,6 @@
</div> </div>
</div> </div>
<div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2"> <div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
<StatChart stat={Stat.Speed} y={speedChart} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} /> <StatChart stat={race.Stat.Speed} y={speedChart} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} />
<StatChart stat={Stat.Guts} y={gutsChart} yLabel="Spurt Speed (m/s)" xRule={gutsStat} {range} /> <StatChart stat={race.Stat.Guts} y={gutsChart} yLabel="Spurt Speed (m/s)" xRule={gutsStat} {range} />
</div> </div>
+3
View File
@@ -39,6 +39,9 @@ export default defineConfig({
'/api': { '/api': {
target: 'http://localhost:13669', target: 'http://localhost:13669',
}, },
'/img': {
target: 'http://localhost:13669',
},
}, },
}, },
}); });