Compare commits
42 Commits
1d65a0ec14
...
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 |
@@ -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
|
||||||
+12
-2
@@ -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.
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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!!!
|
||||||
|
|
||||||
|
|||||||
@@ -3,32 +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/google/go-cmp v0.6.0
|
github.com/go-chi/chi/v5 v5.3.1
|
||||||
github.com/junegunn/fzf v0.67.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/sync v0.20.0 // indirect
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/tools v0.48.0 // indirect
|
||||||
modernc.org/libc v1.65.7 // 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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Generated
+621
-566
File diff suppressed because it is too large
Load Diff
+16
-13
@@ -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}
|
||||||
+12
-124
@@ -1,141 +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 { stringsearch } from './stringsearch';
|
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();
|
||||||
|
|
||||||
let popover: HTMLElement | undefined = $state();
|
|
||||||
let optionsContainer: HTMLElement | undefined = $state();
|
|
||||||
const expanded = $derived(!popover?.hidden);
|
|
||||||
|
|
||||||
let search = $state('');
|
|
||||||
|
|
||||||
const charas = $derived(characters ?? []);
|
const charas = $derived(characters ?? []);
|
||||||
const searchedCharas = $derived(stringsearch(search, charas, ({name}) => name) || charas);
|
|
||||||
|
|
||||||
$effect(() => {
|
const key = (v: Character) => ({ id: v.chara_id, name: v.name });
|
||||||
if (required && value == null && charas.length > 0) {
|
|
||||||
value = charas[0];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setByElem(o: HTMLElement) {
|
|
||||||
const j = parseInt(o.dataset.charaId ?? '');
|
|
||||||
value = charas.find((c) => c.chara_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) => parseInt(n.dataset.charaId ?? '') === value?.chara_id);
|
|
||||||
const o = i != null && 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) => parseInt(n.dataset.charaId ?? '') === value?.chara_id);
|
|
||||||
const o = i != null && 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>
|
</script>
|
||||||
|
|
||||||
<div class={className} style={`anchor-name: --anchor-${id}`}>
|
<Pick {id} items={charas} {key} bind:value class={className} {required}>
|
||||||
<span
|
{#snippet option(c: Character)}
|
||||||
{id}
|
<span>
|
||||||
class="block h-9 content-center hover:cursor-pointer bg-mist-300 dark:bg-mist-900"
|
<CharaIcon chara_id={c.chara_id} size={32} />
|
||||||
role="combobox"
|
{c.name}
|
||||||
aria-expanded={expanded}
|
</span>
|
||||||
aria-controls="charaOptions"
|
{/snippet}
|
||||||
aria-haspopup="listbox"
|
</Pick>
|
||||||
onclick={() => popover?.togglePopover()}
|
|
||||||
{onkeydown}
|
|
||||||
tabindex="0"
|
|
||||||
>{value?.name ?? ''}</span>
|
|
||||||
<div
|
|
||||||
class="absolute top-2 shadow-lg open:flex flex-col px-2 skill-tip"
|
|
||||||
style={`position-anchor: --anchor-${id}; position-area: bottom;`}
|
|
||||||
id="charaOptions"
|
|
||||||
role="listbox"
|
|
||||||
popover
|
|
||||||
bind:this={popover}
|
|
||||||
>
|
|
||||||
<input class="my-2 border rounded-md min-h-8 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="w-full h-8 hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900 text-lg"
|
|
||||||
role="option"
|
|
||||||
aria-selected={value == undefined}
|
|
||||||
tabindex="0"
|
|
||||||
data-chara-id=""
|
|
||||||
onmousedown={() => {value = undefined; search = ''; popover!.hidePopover()}}
|
|
||||||
onfocus={() => value = undefined}
|
|
||||||
{onkeydown}
|
|
||||||
>
|
|
||||||
<span class="italic text-sm">Reset</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#each searchedCharas as c (c.chara_id)}
|
|
||||||
<div
|
|
||||||
class="w-full h-8 hover:cursor-pointer hover:bg-mist-300 hover:dark:bg-mist-900 text-lg"
|
|
||||||
role="option"
|
|
||||||
aria-selected={value?.chara_id === c.chara_id}
|
|
||||||
tabindex="0"
|
|
||||||
data-chara-id={c.chara_id}
|
|
||||||
onmousedown={() => {value = c; popover!.hidePopover()}}
|
|
||||||
onfocus={() => value = c}
|
|
||||||
{onkeydown}
|
|
||||||
>
|
|
||||||
{#if option != null}
|
|
||||||
{@render option(c)}
|
|
||||||
{:else}
|
|
||||||
<span>{c.name}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="w-full h-8 text-lg italic">No matches.</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+122
-6
@@ -28,25 +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 async function sparks(): Promise<Spark[]> {
|
export async function sparks(): Promise<Spark[]> {
|
||||||
const resp = await fetch('/api/global/spark');
|
const resp = await fetch('/api/global/spark');
|
||||||
return resp.json();
|
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()];
|
||||||
|
}
|
||||||
|
|||||||
+390
-28
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,25 +583,6 @@ export function inverseSpurtSpeed(
|
|||||||
return Math.round(r);
|
return Math.round(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hpStrategyCoeff = {
|
|
||||||
[RunningStyle.FrontRunner]: 0.95,
|
|
||||||
[RunningStyle.PaceChaser]: 0.89,
|
|
||||||
[RunningStyle.LateSurger]: 1.0,
|
|
||||||
[RunningStyle.EndCloser]: 0.995,
|
|
||||||
[RunningStyle.GreatEscape]: 0.86,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate a horse's max HP (starting HP) for a race.
|
|
||||||
* @param raceLen Length of the race in meters
|
|
||||||
* @param style Horse's running style
|
|
||||||
* @param stamStat Final stamina stat
|
|
||||||
* @returns Max HP
|
|
||||||
*/
|
|
||||||
export function maxHP(raceLen: number, style: RunningStyle, stamStat: number): number {
|
|
||||||
return 0.8 * hpStrategyCoeff[style] * stamStat + raceLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Meters per horse length (馬身). */
|
/** Meters per horse length (馬身). */
|
||||||
export const HORSE_LENGTH = 2.5;
|
export const HORSE_LENGTH = 2.5;
|
||||||
/** Meters per course width (a constant unit of measure). */
|
/** Meters per course width (a constant unit of measure). */
|
||||||
@@ -291,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;
|
||||||
@@ -382,6 +686,56 @@ export function laneChangeSpeed(powerStat: number, maxLaneOrOrder?: number, curr
|
|||||||
*/
|
*/
|
||||||
export const LANE_CHANGE_ACCEL = 0.03;
|
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.
|
* Calculate a horse's minimum speed for a race.
|
||||||
* @param raceLen Length of the race in meters
|
* @param raceLen Length of the race in meters
|
||||||
@@ -410,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,
|
||||||
@@ -447,13 +799,22 @@ export function duelAccelMod(gutsStat: number): number {
|
|||||||
* 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.
|
||||||
@@ -519,7 +880,8 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null,
|
|||||||
* @returns Probability per candidate to accept
|
* @returns Probability per candidate to accept
|
||||||
*/
|
*/
|
||||||
export function reducedSpurtChance(witStat: number): number {
|
export function reducedSpurtChance(witStat: number): number {
|
||||||
return 0.15 + 0.0005 * witStat;
|
// 1900 wit style S exceeds 100% chance if we don't explicitly cap.
|
||||||
|
return Math.min(0.15 + 0.0005 * witStat, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -52,5 +52,5 @@ export function stringsearch<T>(sub: string, terms: Iterable<T>, map: (t: T) =>
|
|||||||
scored.push([t, sc]);
|
scored.push([t, sc]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scored.sort(([, a], [, b]) => b - a).map(([t,]) => t);
|
return scored.sort(([, a], [, b]) => b - a).map(([t]) => t);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -23,9 +41,9 @@
|
|||||||
<a href={resolve('/doc')} class="mx-8 my-1 inline-block">Documents</a>
|
<a href={resolve('/doc')} class="mx-8 my-1 inline-block">Documents</a>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
|
<main class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</main>
|
||||||
<footer
|
<footer
|
||||||
class="inset-x-0 bottom-0 mt-12 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900"
|
class="inset-x-0 bottom-0 mt-12 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
<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">Race Mechanics Tools</h2>
|
<IndexSection title="Race Mechanics Tools" pages={all.PAGES.race} />
|
||||||
|
<IndexSection title="Character Tools" pages={all.PAGES.chara} />
|
||||||
|
<IndexSection title="Documents" pages={all.PAGES.doc} />
|
||||||
|
|
||||||
|
<!-- Not a real index section in, terms of types. -->
|
||||||
|
<h2 class="my-4 text-4xl">&c.</h2>
|
||||||
<ul class="mb-4 list-disc pl-4">
|
<ul class="mb-4 list-disc pl-4">
|
||||||
<li>
|
|
||||||
<a href={resolve('/race/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other
|
|
||||||
distance aptitudes and running styles.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the
|
|
||||||
effects of individual skills.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h2 class="mt-8 mb-4 text-4xl">Character Tools</h2>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/chara/affinity')}>Affinity Details</a> — Details of why characters have the base compatibility numbers
|
|
||||||
they have.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/chara/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
|
||||||
@@ -33,25 +20,5 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="my-4 text-4xl">Documents</h2>
|
|
||||||
<ul class="mb-4 list-disc pl-4">
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/doc/frbm')}>Front Runner Black Magic</a> — Detailed analysis of how front runners interact with race mechanics.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/doc/lanecalc')}>Gaikokujin's Guide to Lanecalc</a> — English language manual for
|
|
||||||
<a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">危険回避シミュ</a>, or Lanecalc, for precise
|
|
||||||
lane combo simulation.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/doc/race')}>Race Mechanics Charts</a> — Interactive adaptation of
|
|
||||||
<a
|
|
||||||
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
|
||||||
> for easier visualization of how mechanics scale.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2 class="my-4 text-4xl">About</h2>
|
<h2 class="my-4 text-4xl">About</h2>
|
||||||
<p>My repository of tools to fill some gaps I've felt in Umamusume optimization, as well as original research and editorials.</p>
|
<p>My repository of tools to fill some gaps I've felt in Umamusume optimization, as well as original research and editorials.</p>
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from '$app/paths';
|
import { page } from '$app/state';
|
||||||
|
import * as all from '$lib/all';
|
||||||
|
import IndexSection from '$lib/IndexSection.svelte';
|
||||||
|
|
||||||
|
const cur = $derived(all.page(page.route.id));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — Character Tools</h1>
|
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — {cur.name}</h1>
|
||||||
<p>Tools related to understanding Umamusume characters.</p>
|
<p>{cur.description}</p>
|
||||||
|
<IndexSection pages={all.PAGES.chara} />
|
||||||
<ul class="mb-4 list-disc pl-4">
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/chara/affinity')}>Affinity Details</a> — Details of why characters have the base compatibility numbers
|
|
||||||
they have.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/chara/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations
|
|
||||||
on unlocking them quickly.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from '$app/paths';
|
import { page } from '$app/state';
|
||||||
|
import * as all from '$lib/all';
|
||||||
|
import IndexSection from '$lib/IndexSection.svelte';
|
||||||
|
|
||||||
|
const cur = $derived(all.page(page.route.id));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — Documents</h1>
|
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — {cur.name}</h1>
|
||||||
<p>Articles about Umamusume, including editorials and research.</p>
|
<p>{cur.description}</p>
|
||||||
|
<IndexSection pages={all.PAGES.doc} />
|
||||||
<ul class="mb-4 list-disc pl-4">
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/doc/frbm')}>Front Runner Black Magic</a> — Detailed analysis of how front runners interact with race mechanics.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/doc/lanecalc')}>Gaikokujin's Guide to Lanecalc</a> — English language manual for
|
|
||||||
<a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">危険回避シミュ</a>, or Lanecalc, for precise
|
|
||||||
lane combo simulation.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/doc/race')}>Race Mechanics Charts</a> — Interactive adaptation of
|
|
||||||
<a
|
|
||||||
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
|
||||||
> for easier visualization of how mechanics scale.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
h?: 1 | 2 | 3 | 4 | 5 | 6;
|
h?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
id: string;
|
id: string;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
|
ref?: string;
|
||||||
top?: string;
|
top?: string;
|
||||||
toc?: boolean;
|
toc?: boolean;
|
||||||
class?: ClassValue | null;
|
class?: ClassValue | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { h = 1, id, children, top = '#top', toc = true, class: className }: Props = $props();
|
let { h = 1, id, children, ref, top = '#top', toc = true, class: className }: Props = $props();
|
||||||
|
|
||||||
const tag = $derived(`h${h}`);
|
const tag = $derived(`h${h}`);
|
||||||
const href = $derived(`#${id}`);
|
const href = $derived(`#${id}`);
|
||||||
@@ -39,9 +40,12 @@
|
|||||||
<svelte:element this={tag} {id} class={['relative w-full', className]}>
|
<svelte:element this={tag} {id} class={['relative w-full', className]}>
|
||||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||||
<a {href}><span class="hidden md:inline">{sign}</span> {@render children()}</a>
|
<a {href}><span class="hidden md:inline">{sign}</span> {@render children()}</a>
|
||||||
{#if h >= 2}
|
<div class="absolute right-0 bottom-0 inline-block text-end align-bottom text-sm whitespace-nowrap">
|
||||||
<a href={top} class="absolute right-0 bottom-0 text-end align-bottom text-sm whitespace-nowrap transition-all hover:bottom-px"
|
{#if ref != null}
|
||||||
>Top ▲</a
|
<a href={ref} target="_blank" rel="noopener noreferrer">Ref ↗</a>
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if h >= 2}
|
||||||
|
<a href={top} class="ml-4">Top ▲</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</svelte:element>
|
</svelte:element>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import type { ComputedAreas, ComputedSeries, HorizontalRule } from '$lib/chart';
|
import type { ComputedAreas, ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||||
import {
|
import {
|
||||||
AptitudeLevel,
|
AptitudeLevel,
|
||||||
|
conservePowerAccel,
|
||||||
|
distance,
|
||||||
downhillAccelEnterChance,
|
downhillAccelEnterChance,
|
||||||
frontModeEnterChance,
|
frontModeEnterChance,
|
||||||
HORSE_LENGTH,
|
HORSE_LENGTH,
|
||||||
@@ -89,8 +91,11 @@
|
|||||||
loneWolf: allSkills.get(201641),
|
loneWolf: allSkills.get(201641),
|
||||||
slipstream: allSkills.get(201651),
|
slipstream: allSkills.get(201651),
|
||||||
pto: allSkills.get(201661),
|
pto: allSkills.get(201661),
|
||||||
|
headon: allSkills.get(201902),
|
||||||
risky: allSkills.get(202032),
|
risky: allSkills.get(202032),
|
||||||
runaway: allSkills.get(202051),
|
runaway: allSkills.get(202051),
|
||||||
|
fr: allSkills.get(202462),
|
||||||
|
burningPOW: allSkills.get(210031),
|
||||||
burningWIT: allSkills.get(210051),
|
burningWIT: allSkills.get(210051),
|
||||||
ignitedWIT: allSkills.get(210052),
|
ignitedWIT: allSkills.get(210052),
|
||||||
radiant: allSkills.get(210061),
|
radiant: allSkills.get(210061),
|
||||||
@@ -174,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',
|
||||||
@@ -209,6 +214,12 @@
|
|||||||
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
|
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
const fcSeries: ComputedSeries[] = $derived([
|
||||||
|
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) },
|
||||||
|
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) },
|
||||||
|
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) },
|
||||||
|
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Article>
|
<Article>
|
||||||
@@ -233,6 +244,28 @@
|
|||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
<Sec h={2} id="7-1">1 Jul Balance Update</Sec>
|
||||||
|
<p>A quick summary of how the 1 Jul balance update affects front runners:</p>
|
||||||
|
<ul class="mb-4 list-disc pl-4">
|
||||||
|
<li>
|
||||||
|
Pace chasers and late surgers gained access to much more consistent accel skills. Still not as good as Angling, but good
|
||||||
|
enough that front runners can be considered substantially nerfed.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#full-charge">Conserving Power/Fully Charged</a> is in, along with stat uncaps, which means that about 1270 power
|
||||||
|
(base + skills) is now worth a perfectly timed 0.1 accel skill on front runners. The exact mechanics of conserving power
|
||||||
|
aren't yet known, but front runners show a <i>negative correlation</i> between guts stat and probability of activating full charge.
|
||||||
|
As good as spot struggle may be in sprint, a perfect accel is always going to be better, so we are now actively minimizing guts.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
A solo front no longer loses access to speed-up mode. However, having watched a few dozen replays, it seems that while a
|
||||||
|
solo front is typically about 2 to 4 meters ahead of the best pace at position keep end, the best front in multi-front is
|
||||||
|
typically about 6 to 10 meters ahead. Solo front still overwhelmingly benefits other styles. The main impact of the position
|
||||||
|
keep changes in practice seems to be that pace promotion can happen <i>even if there are front runners in the lobby</i>,
|
||||||
|
which means you can't skimp on gate skills when running solo front as a pace promo blocker.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<Sec h={2} id="me">About Me</Sec>
|
<Sec h={2} id="me">About Me</Sec>
|
||||||
<p>
|
<p>
|
||||||
About three weeks after Global launched, my friend told me to get a job, so I sent him a screenshot of me clicking the install
|
About three weeks after Global launched, my friend told me to get a job, so I sent him a screenshot of me clicking the install
|
||||||
@@ -257,18 +290,16 @@
|
|||||||
>. Many of those interpretations are also informed by the exceptionally knowledgeable folks on the
|
>. Many of those interpretations are also informed by the exceptionally knowledgeable folks on the
|
||||||
<a href="https://discord.gg/SyAVkbBSkx" target="_blank" rel="noopener noreferrer">GameTora Discord server</a>. I may present
|
<a href="https://discord.gg/SyAVkbBSkx" target="_blank" rel="noopener noreferrer">GameTora Discord server</a>. I may present
|
||||||
some of the information from the race mechanics doc in chart form, but I will generally leave out exact mechanic numbers and
|
some of the information from the race mechanics doc in chart form, but I will generally leave out exact mechanic numbers and
|
||||||
conditions; the doc is already the place for that information.
|
conditions; the doc is already the place for that information. Some sections have "Ref ↗" links that will take
|
||||||
</p>
|
you to the corresponding section of the mechanics doc.
|
||||||
<p>
|
|
||||||
Also worth mentioning is that I've been unemployed for over two years, with a computer science degree that is increasingly
|
|
||||||
difficult for an anti-AI person to apply. If this article helps you, I'd be tremendously grateful if you could <a
|
|
||||||
href="https://ko-fi.com/zephyrtronium"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">drop me some spare change</a
|
|
||||||
>.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={2} id="mechanics">Race Mechanics</Sec>
|
<Sec
|
||||||
|
h={2}
|
||||||
|
id="mechanics"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.3q7x98goz5cu"
|
||||||
|
>Race Mechanics</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Very quick gloss of race fundamentals. Races are divided into four phases: early race, mid race, late race, and last spurt
|
Very quick gloss of race fundamentals. Races are divided into four phases: early race, mid race, late race, and last spurt
|
||||||
phase. They are also divided into twenty-four equal length sections. Early race is sections 1 to 4, mid race is sections 5 to
|
phase. They are also divided into twenty-four equal length sections. Early race is sections 1 to 4, mid race is sections 5 to
|
||||||
@@ -295,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
|
||||||
@@ -306,7 +334,12 @@
|
|||||||
runners also apply to runaways.
|
runners also apply to runaways.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="phase-speed">Phase Speed</Sec>
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="phase-speed"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.40iug3hm8429"
|
||||||
|
>Phase Speed</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Race base speed is multiplied by the speed strategy–phase coefficient for each horse. As the name suggests, SSPC is
|
Race base speed is multiplied by the speed strategy–phase coefficient for each horse. As the name suggests, SSPC is
|
||||||
different per running style and per race phase. It's the thing that makes runaways take off in early race, and the thing that
|
different per running style and per race phase. It's the thing that makes runaways take off in early race, and the thing that
|
||||||
@@ -319,8 +352,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The late race speed difference means that, in a competitive setting, the speed stat (and, correspondingly, distance aptitude)
|
The late race speed difference means that, in a competitive setting, the speed stat (and, correspondingly, distance aptitude)
|
||||||
aren't what make front runners win most of the time. Capped speed with distance S is ideal, but 1100 speed with distance A
|
aren't what make front runners win most of the time. Capped speed with distance S is ideal, but being a hundred speed short
|
||||||
will likely lose only a couple races within the difference throughout a CM event.
|
with distance A will likely lose only a couple races within the difference throughout a CM event.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="win-cons">Win Conditions</Sec>
|
<Sec h={3} id="win-cons">Win Conditions</Sec>
|
||||||
@@ -364,16 +397,20 @@
|
|||||||
mid race.
|
mid race.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="front-modes">Speed-Up and Overtake Modes</Sec>
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="front-modes"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.tn8irhl5bjhv"
|
||||||
|
>Speed-Up and Overtake Modes</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
During the first 41.67% of the race, <i>position keep</i> is busy arranging each running style into their respective packs.
|
During the first 41.67% of the race, <i>position keep</i> is busy arranging each running style into their respective packs.
|
||||||
During position keep, all horses have access to <i>running modes</i> that influence how they run.
|
During position keep, all horses have access to <i>running modes</i> that influence how they run.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The running modes for front runners are speed-up (+4% target speed for first among that front type) and overtake (+5% for
|
The running modes for front runners are speed-up (+4% target speed for first among that front type) and overtake (+5% for
|
||||||
not-first). Entering these modes requires meeting certain conditions relating to positioning, which collectively can be read
|
not-first). Entering these modes requires meeting certain conditions relating to positioning. They also require passing a wit
|
||||||
as "solo fronts are heavily penalized." (With the 1.5 anni balance patch, we expect that solo fronts will be given more
|
check, with the same chance for both modes.
|
||||||
forgiving running mode conditions.) They also require passing a wit check, with the same chance for both modes.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="mx-auto h-60 w-full max-w-3xl md:h-96">
|
<div class="mx-auto h-60 w-full max-w-3xl md:h-96">
|
||||||
<StatChart
|
<StatChart
|
||||||
@@ -386,24 +423,34 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={3} id="pdm">Pace Down Mode</Sec>
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="pdm"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.5x2q33mxvumk"
|
||||||
|
>Pace Down Mode</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
The running modes for all other running styles are pace-up, which is similar to speed-up, and paced-down, which activates
|
The running modes for all other running styles are pace-up, which is similar to speed-up, and paced-down, which activates
|
||||||
whenever a horse gets what their style defines as too close to first place.
|
whenever a horse gets what their style defines as too close to first place.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Watch a MANT late surger with 1000+ power and wit in a daily legend race. As long as they don't get blocked, they should <a
|
Watch a UC late surger with 1200+ power and wit in a daily legend race. As long as they don't get blocked, they should <a
|
||||||
href="#section-speed">slide forward</a
|
href="#section-speed">slide forward</a
|
||||||
> throughout the early race. Then, around when they reach the pace chaser pack, they'll suddenly start moonwalking back to the rest
|
> throughout the early race. Then, around when they reach the pace chaser pack, they'll suddenly start moonwalking back to the rest
|
||||||
of the late surgers, often near the back of the group. That's PDM.
|
of the late surgers, often near the back of the group. That's PDM.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
On styles with PDM, early race and sometimes mid race speed skills are effectively converted from distance gain into HP
|
On styles with PDM, early race and sometimes mid race speed skills are effectively converted from distance gain into HP
|
||||||
conservation. The thing that really makes front runners good is that they don't have to worry about that – they aren't
|
conservation. The thing that really makes front runners strong is that they don't have to worry about that – they aren't
|
||||||
subject to PDM at all. Their mid race speed skills always gain distance.
|
subject to PDM at all. Their mid race speed skills always gain distance.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="spot-struggle">Spot Struggle</Sec>
|
<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>
|
<p>
|
||||||
For each of runaways and non-runaways, there is at most one spot struggle per race. Runaways will not spot struggle with
|
For each of runaways and non-runaways, there is at most one spot struggle per race. Runaways will not spot struggle with
|
||||||
non-runaways, nor vice-versa. When a spot struggle triggers, all front runnners of that type within range participate; I've
|
non-runaways, nor vice-versa. When a spot struggle triggers, all front runnners of that type within range participate; I've
|
||||||
@@ -411,17 +458,18 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Spot struggle provides a target speed bonus that scales with the guts stat. If it isn't cut short, which will approximately
|
Spot struggle provides a target speed bonus that scales with the guts stat. If it isn't cut short, which will approximately
|
||||||
never happen, its duration also scales with the guts stat. Unlike skills, its duration <i>does not</i> scale with race distance.
|
never happen outside of sprint, its duration also scales with the guts stat. Unlike skills, its duration <i>does not</i> scale with
|
||||||
|
race distance.
|
||||||
</p>
|
</p>
|
||||||
<div class="mb-4 grid h-60 w-full grid-cols-2 md:h-96">
|
<div class="mb-4 grid h-60 w-full grid-cols-2 md:h-96">
|
||||||
<StatChart stat={Stat.Guts} y={ssBoostSeries} yLabel="Speed Bonus (m/s)" range={[0, 0.3]} />
|
<StatChart stat={Stat.Guts} y={ssBoostSeries} yLabel="Speed Bonus (m/s)" range={[0, 0.4]} />
|
||||||
<StatChart stat={Stat.Guts} y={ssDurSeries} yLabel="Duration (s)" range={[0, 12]} />
|
<StatChart stat={Stat.Guts} y={ssDurSeries} yLabel="Duration (s)" range={[0, 14]} />
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Spot struggle also greatly increases HP consumption. For normal front runners, the rate is slightly less than Rushed. For
|
Spot struggle also greatly increases HP consumption. For normal front runners, the rate is slightly less than Rushed. For
|
||||||
runaways, it's more than double Rushed. (This is the reason people say you can't get enough stamina for runaways on Global.)
|
runaways, it's more than double Rushed. (This is the reason people say you can't get enough stamina for runaways on Global.)
|
||||||
Actually getting Rushed during spot struggle dramatically increases HP consumption, much more than just adding them together;
|
Actually getting Rushed during spot struggle dramatically increases HP consumption, much more than just adding them together;
|
||||||
red-light green-light pretty much guarantees that horse won't spurt.
|
red-light green-light pretty much guarantees that horse won't full spurt.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than
|
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than
|
||||||
@@ -429,6 +477,52 @@
|
|||||||
multiple gold speed skills in total distance gained. See the <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for
|
multiple gold speed skills in total distance gained. See the <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for
|
||||||
precise analysis.
|
precise analysis.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
However, while the details aren't yet known, it appears that spot struggle reduces <a href="#full-charge">conserved power</a>,
|
||||||
|
which means that front runners who spot struggle are less likely to full charge, and the probability falls as the spot
|
||||||
|
struggle duration rises.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="full-charge"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.9edgwx6l733b"
|
||||||
|
>Conserve Power & Fully Charged</Sec
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
New in the 1 Jul balance patch is the conserve power mechanic. Horses with base power plus power passive skills greater than
|
||||||
|
1200 effectively gain access to a free perfectly-timed accel skill, Fully Charged. The effectiveness of full charge varies
|
||||||
|
with running style and distance, but it is greatest for front runners at all distances.
|
||||||
|
</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={Stat.Power}
|
||||||
|
y={fcSeries}
|
||||||
|
yLabel="Accel Bonus (m/s²)"
|
||||||
|
range={[0, 0.25]}
|
||||||
|
plotOptions={{ color: { legend: true } }}
|
||||||
|
/>
|
||||||
|
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||||
|
<label class="my-auto hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
|
||||||
|
<input
|
||||||
|
class="my-auto max-w-40 flex-1 md:flex-2"
|
||||||
|
type="range"
|
||||||
|
id="secSpeedRaceLen"
|
||||||
|
min="1000"
|
||||||
|
max="3600"
|
||||||
|
step="100"
|
||||||
|
bind:value={raceLen}
|
||||||
|
/>
|
||||||
|
<span class="my-auto flex-none pl-2 text-sm">{raceLen}m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a
|
||||||
|
href="#spot-struggle">spot struggle</a
|
||||||
|
> both reduce the chance for it to fire. Since spot struggle duration scales with guts, it appears that taking high guts to benefit
|
||||||
|
from the strength of spot struggle has become likely to deprive you of an even stronger mechanic.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="lane-combo">Lane Combo</Sec>
|
<Sec h={3} id="lane-combo">Lane Combo</Sec>
|
||||||
<p>
|
<p>
|
||||||
@@ -443,7 +537,7 @@
|
|||||||
y={laneComboSeries}
|
y={laneComboSeries}
|
||||||
yLabel="Speed Boost"
|
yLabel="Speed Boost"
|
||||||
yRule={lcYRule}
|
yRule={lcYRule}
|
||||||
range={[0.2, 0.5]}
|
range={[0.2, 0.6]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
@@ -488,7 +582,12 @@
|
|||||||
<a href={resolve('/doc/lanecalc')}>my guide</a> for English instructions on how to use it.
|
<a href={resolve('/doc/lanecalc')}>my guide</a> for English instructions on how to use it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="uphills">Uphills</Sec>
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="uphills"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.wdj69udyo340"
|
||||||
|
>Uphills</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Running uphill carries a penalty to target speed. This penalty scales negatively with the power stat; that is, higher power
|
Running uphill carries a penalty to target speed. This penalty scales negatively with the power stat; that is, higher power
|
||||||
means faster uphill running. It scales positively with slope angle. There is also a flat reduction in base acceleration for
|
means faster uphill running. It scales positively with slope angle. There is also a flat reduction in base acceleration for
|
||||||
@@ -513,7 +612,12 @@
|
|||||||
power on the Arima Kinen mid-race hills.
|
power on the Arima Kinen mid-race hills.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="downhills">Downhills</Sec>
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="downhills"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.wdj69udyo340"
|
||||||
|
>Downhills</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
Running downhill allows horses to enter <i>downhill accel mode</i>. Contrary to its name, downhill accel mode does not affect
|
Running downhill allows horses to enter <i>downhill accel mode</i>. Contrary to its name, downhill accel mode does not affect
|
||||||
acceleration at all; it gives horses a target speed boost that scales with the slope angle, plus lowered HP consumption via a
|
acceleration at all; it gives horses a target speed boost that scales with the slope angle, plus lowered HP consumption via a
|
||||||
@@ -530,7 +634,7 @@
|
|||||||
stat={Stat.Wit}
|
stat={Stat.Wit}
|
||||||
y={downhillSeries}
|
y={downhillSeries}
|
||||||
yLabel="Entry Chance (% each second)"
|
yLabel="Entry Chance (% each second)"
|
||||||
range={[0, 60]}
|
range={[0, 75]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
@@ -539,7 +643,12 @@
|
|||||||
Conversely, the HP savings on long downhills can be enough to drop a recovery skill or two on some tracks.
|
Conversely, the HP savings on long downhills can be enough to drop a recovery skill or two on some tracks.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="section-speed">Section Speed</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>
|
<p>
|
||||||
Each section, each horse gets a random modifier to target speed. The modifier's range is determined by the wit stat.
|
Each section, each horse gets a random modifier to target speed. The modifier's range is determined by the wit stat.
|
||||||
(Curiously, the calculation uses both wit as modified by style proficiency and green skills as well as base wit.)
|
(Curiously, the calculation uses both wit as modified by style proficiency and green skills as well as base wit.)
|
||||||
@@ -550,7 +659,7 @@
|
|||||||
stat={Stat.Wit}
|
stat={Stat.Wit}
|
||||||
yArea={secSpeedSeries}
|
yArea={secSpeedSeries}
|
||||||
yLabel="Section Speed (m/s)"
|
yLabel="Section Speed (m/s)"
|
||||||
range={[17.5, 22.5]}
|
range={[17, 23]}
|
||||||
plotOptions={{ color: { legend: true } }}
|
plotOptions={{ color: { legend: true } }}
|
||||||
/>
|
/>
|
||||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||||
@@ -583,7 +692,12 @@
|
|||||||
wit front A horse will pass in {secSpeedPassTime} seconds on average at mid race speeds.
|
wit front A horse will pass in {secSpeedPassTime} seconds on average at mid race speeds.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="no-zone">No-Overtake Zone</Sec>
|
<Sec
|
||||||
|
h={3}
|
||||||
|
id="no-zone"
|
||||||
|
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.7l02ti4pvzxp"
|
||||||
|
>No-Overtake Zone</Sec
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
The no-overtake zone is the 200m portion of the race prior to the first corner. For unclear reasons, while in the no-overtake
|
The no-overtake zone is the 200m portion of the race prior to the first corner. For unclear reasons, while in the no-overtake
|
||||||
zone, horses cannot enter overtake lane mode, which is what allows them to move away from the rail. (Overtake lane mode is
|
zone, horses cannot enter overtake lane mode, which is what allows them to move away from the rail. (Overtake lane mode is
|
||||||
@@ -674,7 +788,7 @@
|
|||||||
<Sec h={3} id="spurt-skills">Spurt Skills</Sec>
|
<Sec h={3} id="spurt-skills">Spurt Skills</Sec>
|
||||||
<p>
|
<p>
|
||||||
Because they are post-Angling, skills that activate in the final spurt are typically less interesting to front runners.
|
Because they are post-Angling, skills that activate in the final spurt are typically less interesting to front runners.
|
||||||
Notable exceptions are <Skill skill={skills.barcaroleInherit} /> and, on Tokyo turf, <Skill
|
Notable exceptions are <Skill skill={skills.fr} />, <Skill skill={skills.barcaroleInherit} /> and, on Tokyo turf, <Skill
|
||||||
skill={skills.inesInherit}
|
skill={skills.inesInherit}
|
||||||
/><!-- skill name ends with ! -->
|
/><!-- skill name ends with ! -->
|
||||||
These are good inherits for those who don't have easy access to <Skill skill={skills.cacaoInherit} mention /> and <Skill
|
These are good inherits for those who don't have easy access to <Skill skill={skills.cacaoInherit} mention /> and <Skill
|
||||||
@@ -708,8 +822,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Eyes especially has the potential to be threatening, since it's roughly unconditional. However, it hits others <i>in vision</i
|
Eyes especially has the potential to be threatening, since it's roughly unconditional. However, it hits others <i>in vision</i
|
||||||
>, which is essentially always a range of 20m. Debuffers are typically built with very little stamina and power, so Eyes is
|
>, which is essentially always a range of 20m, or 8 lengths. Debuffers are typically built with very little stamina and power,
|
||||||
almost never going to reach front runners, especially the front who got Angling.
|
so Eyes is almost never going to reach front runners, especially the front who got Angling.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The other category of skills to think about is other horses' uniques.
|
The other category of skills to think about is other horses' uniques.
|
||||||
@@ -753,6 +867,10 @@
|
|||||||
Obviously, CM is run in teams of three. While spamming the highest stats you can manage is a valid strategy that will get
|
Obviously, CM is run in teams of three. While spamming the highest stats you can manage is a valid strategy that will get
|
||||||
wins, building a team holistically will lead to higher win rates.
|
wins, building a team holistically will lead to higher win rates.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
N.B. This section has only partially been updated for the 1 Jul balance patch, which means that some of the advice especially
|
||||||
|
relating to runaways and spot struggle is out of date. A full update here probably won't come until after CM17 Virgo Cup.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="pace-promo">Zero Front</Sec>
|
<Sec h={3} id="pace-promo">Zero Front</Sec>
|
||||||
<p>
|
<p>
|
||||||
@@ -790,21 +908,20 @@
|
|||||||
<Sec h={3} id="solo-front">Solo Front</Sec>
|
<Sec h={3} id="solo-front">Solo Front</Sec>
|
||||||
<p>
|
<p>
|
||||||
Because solo fronts essentially don't have access to their <a href="#front-modes">running modes</a>, they are unlikely to
|
Because solo fronts essentially don't have access to their <a href="#front-modes">running modes</a>, they are unlikely to
|
||||||
build enough of a lead over paces to win, even with Angling. (This should change to some degree with the release of Grand
|
build enough of a lead over paces to win, even with Angling. (This changed a bit with the 1 Jul balance patch, as solo fronts
|
||||||
Live, as solo fronts will get an extra long speed-up zone.)
|
now get an extra-long speed-up range, but it doesn't appear that they're maintaining much of a gap over paces in practice.)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In particularly front-dominated metas, this is manageable, since you'll have opponent fronts to run against. When any other
|
In particularly front-dominated metas, this is manageable, since you'll have opponent fronts to run against. When any other
|
||||||
style is viable, which is probably always now that Nishino Flower exists, you'll have matches where your team's front runner
|
style is viable, which is always now that <Skill skill={skills.headon} mention /> is buffed, you'll have matches where your team's
|
||||||
is solo. The best choice for how to design a solo front depends on the rest of your team composition, because ultimately the
|
front runner is solo. The best choice for how to design a solo front depends on the rest of your team composition, because ultimately
|
||||||
choice you are faced with is whether to use a runaway blocker.
|
the choice you are faced with is whether to use a runaway blocker.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In the MANT era, building a runaway that can defeat normal front runners is difficult. For one thing, a runaway existing means
|
The ideal runaway blocker has capped power, capped wit, and surface S, along with full gate and <a href="#lane-combo"
|
||||||
that all fronts have access to overtake mode for position keep, which is better than speed-up mode. Moreover, beating their
|
>lane combos</a
|
||||||
mid race skill list requires some lucky hints in Unity Cup. The ideal runaway blocker has capped power, capped wit, and
|
> where applicable, to build as much of an early race lead as possible. Her speed stat doesn't particularly matter, because her
|
||||||
surface S, along with full gate and <a href="#lane-combo">lane combos</a> where applicable, to build as much of an early race lead
|
job is to block, not to win.
|
||||||
as possible.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Because runaways get so far ahead during the first half of the race, they essentially disable <a href="#pdm">pace-down mode</a
|
Because runaways get so far ahead during the first half of the race, they essentially disable <a href="#pdm">pace-down mode</a
|
||||||
@@ -826,12 +943,12 @@
|
|||||||
<p>
|
<p>
|
||||||
There is still a difference between dual ace double front and double front with an ace and a backup. The ideal stat lines and
|
There is still a difference between dual ace double front and double front with an ace and a backup. The ideal stat lines and
|
||||||
skill lists will be the same between the two styles, but character choice will be different. In particular, dual ace wants
|
skill lists will be the same between the two styles, but character choice will be different. In particular, dual ace wants
|
||||||
both to have strong speed uniques – VBourbon, SMaru, Ines Fujin on Tokyo turf.
|
both to have strong speed uniques – VBourbon or SMaru.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
With an ace and a backup, the ace is the one with the speed unique, and the other is a horse who gambles on winning mid race
|
With an ace and a backup, the ace is the one with the speed unique, and the other is a horse who gambles on winning mid race
|
||||||
and turns that into a first place finish. Seiun Sky is the strongest such option; as an own unique rather than inherited,
|
and turns that into a first place finish. Seiun Sky is the strongest such option; as an own unique rather than inherited,
|
||||||
Angling gives so much perfectly timed accel that speed inherits give her more lengths than other accel skills.
|
Angling gives so much perfectly timed acceleration that speed inherits give her more lengths than other accel skills.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Since your two front runners are both meant to be winners, making your third horse a support type is likely to give the best
|
Since your two front runners are both meant to be winners, making your third horse a support type is likely to give the best
|
||||||
@@ -853,12 +970,6 @@
|
|||||||
long distance; front runners are common generally here.) So, double front for long is ultimately about the same as double front
|
long distance; front runners are common generally here.) So, double front for long is ultimately about the same as double front
|
||||||
for other distances, just with different goals when choosing the legacy.
|
for other distances, just with different goals when choosing the legacy.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
Given that NN is good, it's also worth considering the gold version, <Skill skill={skills.nsm} /><!-- ends with ! -->
|
|
||||||
NSM is a better skill for the long double front build, but for now, the only way to get it while running MANT is from Yukino Bijin
|
|
||||||
Wit. That card's numbers are not great. It's still possible to get a good stat line using her, but it will take more careers to
|
|
||||||
get there than to just get NN from Fine Motion.
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
If you're feeling adventurous, runaway is another consideration for double (and triple) front on long distances. Your ace
|
If you're feeling adventurous, runaway is another consideration for double (and triple) front on long distances. Your ace
|
||||||
(hopefully) still gets VC, but the runaway paces up the race. As mentioned in the <a href="#solo-front">solo front</a>
|
(hopefully) still gets VC, but the runaway paces up the race. As mentioned in the <a href="#solo-front">solo front</a>
|
||||||
@@ -888,10 +999,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The fact that triple front tries to occupy positions 3-4 lets you do some very interesting team comps where you run pace
|
The fact that triple front tries to occupy positions 3-4 lets you do some very interesting team comps where you run pace
|
||||||
chasers as front runners. E.g., I ran Taiki Shuttle as front in dirt CM and Curren Chan as front in sprint CM. This is not
|
chasers as front runners. E.g., I ran Taiki Shuttle as front in CM10 on dirt and Curren Chan as front in the CM9 sprint. This
|
||||||
terribly consistent because they're readily equipped to get into first just as easily as third, but then they just get Angling
|
is not terribly consistent because they're readily equipped to get into first just as easily as third, but then they just get
|
||||||
instead and follow the normal front runner game plan. If you see people talking about a pace chaser being especially strong
|
Angling instead and follow the normal front runner game plan. If you see people talking about a given pace chaser being
|
||||||
for a given CM, consider running them as a front runner in a triple front build instead.
|
especially strong for a CM, consider running them as a front runner in a triple front build instead.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
A more mechanical type of support is to build for <a href="#spot-struggle">spot struggle</a> and have her pull your less gutsy fronts
|
A more mechanical type of support is to build for <a href="#spot-struggle">spot struggle</a> and have her pull your less gutsy fronts
|
||||||
@@ -905,13 +1016,13 @@
|
|||||||
showing. In addition to the normal front win cons, it's potentially strong to take unconditional gambles like <Skill
|
showing. In addition to the normal front win cons, it's potentially strong to take unconditional gambles like <Skill
|
||||||
skill={skills.cornerConn}
|
skill={skills.cornerConn}
|
||||||
mention
|
mention
|
||||||
/> and <Skill skill={skills.radiant} mention />.
|
/>, <Skill skill={skills.burningPOW} mention />, and <Skill skill={skills.radiant} mention />.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
SS supports do best on medium and long tracks, because you want to be building <i>everyone</i> for spot struggle on miles and
|
Now that <a href="#full-charge">conserving power</a> is a game mechanic, SS supports have the problem that spot struggle risks
|
||||||
sprints. However, spot struggle is the only mechanic in the game that scales superlinearly with stats, so even the difference
|
taking away full charge, which means losing a strong accel. This can be alleviated to some degree by managing
|
||||||
between 1200 and 1000 guts can matter enough to justify an extra guts focus on the SS support in miles. See the
|
<a href="#lane-combo">Dodging Danger</a> to keep your ace out of the spot struggle – either the ace has lane combo and the
|
||||||
<a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for details.
|
other two don't, or the ace doesn't have it and the other two do.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="hidden" id="chariot"></div>
|
<div class="hidden" id="chariot"></div>
|
||||||
@@ -934,15 +1045,17 @@
|
|||||||
/>, so the normal fronts (typically UG1 or better) pass them during early race.
|
/>, so the normal fronts (typically UG1 or better) pass them during early race.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If this build ever had an era, it was during Unity Cup, when NSM was essentially free and there was no VC. I don't think it's
|
If this build ever had an era, it was during the first pass of Unity Cup, when NSM was essentially free and there was no VC.
|
||||||
viable now.
|
We have reworked UC for one CM before Our Grand Concert releases, but it's a sprint CM that overwhelmingly favors pace
|
||||||
|
chasers. I don't think it's viable now.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={2} id="career">Career</Sec>
|
<Sec h={2} id="career">Career</Sec>
|
||||||
<p>Most front runners enjoy easy careers thanks to strong kits and little chance to be blocked.</p>
|
<p>Most front runners enjoy easy careers thanks to strong kits and little chance to be blocked.</p>
|
||||||
<p>
|
<p>
|
||||||
Currently, this chapter is about MANT (a.k.a. Trackblazer). Unity Cup is still useful – obviously for runaways, but also
|
Currently, this chapter is about MANT (a.k.a. Trackblazer). Current data for CM16 following the URA and UC reworks suggests
|
||||||
for spinning up an <Skill skill={skills.ignitedWIT} mention /> legacy.
|
that MANT horses win approximately just as much as UC horses relative to their respective populations, despite the lower stat
|
||||||
|
caps. So, I am going to leave this section largely untouched until the next scenario releases.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="support-cards">Support Cards</Sec>
|
<Sec h={3} id="support-cards">Support Cards</Sec>
|
||||||
@@ -1170,7 +1283,8 @@
|
|||||||
<Sec h={3} id="taiki-shuttle">Taiki Shuttle & Curren Chan</Sec>
|
<Sec h={3} id="taiki-shuttle">Taiki Shuttle & Curren Chan</Sec>
|
||||||
<p>
|
<p>
|
||||||
When building <a href="#triple-front">triple fronts</a>, you will naturally have a horse in third. This presents an
|
When building <a href="#triple-front">triple fronts</a>, you will naturally have a horse in third. This presents an
|
||||||
opportunity to use a horse who is normally a pace chaser as a type of gambler.
|
opportunity to use a horse who is normally a pace chaser, with a strong unique that requires position 3 or behind, as a type
|
||||||
|
of gambler.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Taiki Shuttle and original Curren Chan are notable for having C and B front, respectively, which is quite reasonable to fix.
|
Taiki Shuttle and original Curren Chan are notable for having C and B front, respectively, which is quite reasonable to fix.
|
||||||
@@ -1196,14 +1310,15 @@
|
|||||||
|
|
||||||
<Sec h={3} id="sparks">Important Sparks</Sec>
|
<Sec h={3} id="sparks">Important Sparks</Sec>
|
||||||
<p>
|
<p>
|
||||||
Front runners are uniquely capable of making good use of all stat (blue) sparks, since guts is unusually important for them.
|
Front runners are uniquely capable of making good use of wit sparks, since wit governs early- and mid-race speed, which is
|
||||||
Power sparks are typically still best, and stamina sparks can help to forgo a stamina card or two for 2400m+ builds. Fronts
|
unusually important for them. Power sparks are typically still best, and stamina sparks can help to forgo a stamina card or
|
||||||
have a disproportionate representation of horses with high speed talent, which makes speed sparks a bit less valuable.
|
two for 2400m+ builds. Fronts have a disproportionate representation of horses with high speed talent, which makes speed
|
||||||
|
sparks a bit less valuable.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Pink sparks are more interesting, because – at least until other styles catch up on accels – they're less reliant
|
Pink sparks are more interesting, because – at least until other styles finish catching up on accels – they're
|
||||||
on distance S to win. Building a high affinity legacy tree requires keeping some mile and long sparks on hand. Front sparks
|
less reliant on distance S to win. Building a high affinity legacy tree requires keeping some mile and long sparks on hand.
|
||||||
are better than other style sparks, especially if you do <a href="#triple-front">triple front</a> CM builds using
|
Front sparks are better than other style sparks, especially if you do <a href="#triple-front">triple front</a> CM builds using
|
||||||
<a href="#taiki-shuttle">fixed pace chasers</a>.
|
<a href="#taiki-shuttle">fixed pace chasers</a>.
|
||||||
</p>
|
</p>
|
||||||
<p>Front runners are more reliant than other styles on getting certain skills from inspiration:</p>
|
<p>Front runners are more reliant than other styles on getting certain skills from inspiration:</p>
|
||||||
@@ -1221,7 +1336,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<Skill skill={skills.ignitedWIT} /> is the stronger lane combo piece. The only sources of it are inheritance and Unity Cup; which
|
<Skill skill={skills.ignitedWIT} /> is the stronger lane combo piece. The only sources of it are inheritance and Unity Cup; which
|
||||||
is to say, the source of it is inheritance. When searching databases for borrows, looking for Ignited WIT is a shortcut for listing
|
is to say, the source of it is inheritance. When searching databases for borrows, looking for Ignited WIT is a shortcut for listing
|
||||||
good front runner legacies, since it's only valuable to fronts and somewhat hard to get even when you focus on it.
|
good front runner legacies, since it's only valuable to fronts.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Almost all green skills are valuable to inherit at one point or another as GW triggers.
|
Almost all green skills are valuable to inherit at one point or another as GW triggers.
|
||||||
@@ -1236,12 +1351,17 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
All generic speed skills that aren't strictly late race are good inherits.
|
All generic speed skills that aren't strictly late race are good inherits.
|
||||||
<Skill skill={skills.risky} /> is the best one by ratio of strength to difficulty to acquire.
|
<Skill skill={skills.risky} /> is the best one by ratio of strength to difficulty to acquire; if you aren't forced to borrow Smart
|
||||||
|
Falcon, you can get the gold version of this from Nakayama Festa Wit.
|
||||||
<Skill skill={skills.thh} />, <Skill skill={skills.pto} />, <Skill skill={skills.ramp} />, and <Skill
|
<Skill skill={skills.thh} />, <Skill skill={skills.pto} />, <Skill skill={skills.ramp} />, and <Skill
|
||||||
skill={skills.cornerAdept}
|
skill={skills.cornerAdept}
|
||||||
/> are also strong, but available as hints or chain skills from some usable-to-good cards.
|
/> are also strong, but available as hints or chain skills from some usable-to-good cards.
|
||||||
</li>
|
</li>
|
||||||
<li>Obviously, getting front-runner-specific skills from inheritance makes them cheaper and more consistent to get.</li>
|
<li>
|
||||||
|
Getting front-runner-specific skills from inheritance has become especially important now that reworked UC doesn't give
|
||||||
|
skills according to aptitudes. This is a significant reason for MANT to remain the parenting scenario, arguably even more so
|
||||||
|
than the higher affinity potential.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Sec h={3} id="parents">Parents</Sec>
|
<Sec h={3} id="parents">Parents</Sec>
|
||||||
@@ -1270,7 +1390,8 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Mayano Top Gun doesn't have an actual front runner variant (until Halloween Mayano in 2028 or so), but she acts like a front
|
Mayano Top Gun doesn't have an actual front runner variant (until Halloween Mayano in 2028 or so), but she acts like a front
|
||||||
runner for affinity. In particular, Maya and Kitasan is the highest base compatibility pair currently in the game.
|
runner for affinity. In particular, Maya and Kitasan is among the highest base compatibility pairs currently in the game.
|
||||||
|
(They were #1 prior to the 1 Jul balance patch, but now that spot goes to Tamamo Cross and Inari One.)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
TM Opera O is technically a pace chaser parent, but she still has good compatibility with most front runners, and her unique
|
TM Opera O is technically a pace chaser parent, but she still has good compatibility with most front runners, and her unique
|
||||||
@@ -1286,6 +1407,37 @@
|
|||||||
|
|
||||||
<Sec h={2} id="cm">My CM Teams</Sec>
|
<Sec h={2} id="cm">My CM Teams</Sec>
|
||||||
|
|
||||||
|
<Sec h={3} id="cm16">CM16 – Leo Cup (Sprinters Stakes)</Sec>
|
||||||
|
<p>
|
||||||
|
The <a href="#7-1">1 Jul balance patch</a> redefined everything for everyone, and front runners are suddenly actually quite bad.
|
||||||
|
I guess the stakes are lower for my triple front mandate.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
An additional interesting detail is that this track has no straights before late race. That means straight velos are much less
|
||||||
|
important.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I had some amazing luck with support card pulls and got both Maruzensky and Nakayama Festa to MLB in 400. Are these runs what
|
||||||
|
it feels like to not be F2P?
|
||||||
|
</p>
|
||||||
|
<ol class="mb-4 list-decimal pl-4">
|
||||||
|
<li>Suzuka gets to be a runaway again!</li>
|
||||||
|
<li>Chocobon's Sprint C is easy to fix!</li>
|
||||||
|
<li>
|
||||||
|
Taiki Shuttle has a very-nearly-perfect 3-4 unique, which will be great with her as a front runner. Given her lack of
|
||||||
|
relevant built-in skills, I'm building her in MANT.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p>
|
||||||
|
This CM is interesting as the first opportunity to use multiple runaways and not have them be vaporized by spot struggle.
|
||||||
|
However, after getting a high roll with an S Sprint Palmer, I found that using two runaways was actually pretty bad. Even with
|
||||||
|
1300 power, <Skill skill={skills.gw} mention />, and <Skill skill={skills.ttl} mention />, runaways spend over two thirds of
|
||||||
|
early race accelerating, so they lose the majority of their strongest race phase. Normal front runners can hardly compete with
|
||||||
|
the newly buffed <Skill skill={skills.headon} mention /> in late race, which means runaways who never got the lead they're known
|
||||||
|
for have virtually no chance. Suzuka can still fight because <Skill skill={skills.unrestrained} mention /> is also strong, but Palmer
|
||||||
|
doesn't make the cut.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Sec h={3} id="cm15">CM15 – Cancer Cup (Takarazuka Kinen)</Sec>
|
<Sec h={3} id="cm15">CM15 – Cancer Cup (Takarazuka Kinen)</Sec>
|
||||||
<p>
|
<p>
|
||||||
Kitasan's unique ends about a sixth of a second before late race starts, which means massive carryover potential (although
|
Kitasan's unique ends about a sixth of a second before late race starts, which means massive carryover potential (although
|
||||||
@@ -1309,6 +1461,14 @@
|
|||||||
ace, and that's what Chocobon does best.
|
ace, and that's what Chocobon does best.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<p>Win rates are a bit messy because I experimented with a few different comps and Throne reworks. After 80:</p>
|
||||||
|
<ul class="mb-4 list-disc pl-4">
|
||||||
|
<li>VBourbon: 28-15-17-20</li>
|
||||||
|
<li>Kitasan: 12-14-11-38 (of 75)</li>
|
||||||
|
<li>Suzuka: 8-5-7-60 = 8-4-6-37 + 0-1-1-23 (replaced after 25; I ended up not using a runaway either time)</li>
|
||||||
|
<li>Seiun Sky: 0-0-2-3 (of 5)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Lost finals to two COCs, one getting Pump and the other NSM, while Chocobon blew velos during accel.</p>
|
||||||
|
|
||||||
<Sec h={3} id="cm14">CM14 – Gemini Cup (Yasuda Kinen, NHK Mile Cup, Victoria Mile)</Sec>
|
<Sec h={3} id="cm14">CM14 – Gemini Cup (Yasuda Kinen, NHK Mile Cup, Victoria Mile)</Sec>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -14,18 +14,9 @@
|
|||||||
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 raceLenType = $derived.by(() => {
|
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.Runaway);
|
||||||
if (raceLen < 1500) {
|
const distanceType = $derived(race.distance(raceLen));
|
||||||
return 'Sprint';
|
const raceLenType = $derived(race.Distance[distanceType]);
|
||||||
}
|
|
||||||
if (raceLen < 1900) {
|
|
||||||
return 'Mile';
|
|
||||||
}
|
|
||||||
if (raceLen < 2500) {
|
|
||||||
return 'Medium';
|
|
||||||
}
|
|
||||||
return 'Long';
|
|
||||||
});
|
|
||||||
|
|
||||||
const spurtSpeed: Array<ComputedSeries | null> = $derived([
|
const spurtSpeed: Array<ComputedSeries | null> = $derived([
|
||||||
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
|
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
|
||||||
@@ -48,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[] = [
|
||||||
@@ -66,11 +57,21 @@
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
|
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([
|
const gutsSpurt: Array<ComputedSeries | null> = $derived([
|
||||||
{ label: `Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, distanceApt, raceLen) },
|
|
||||||
{ label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) },
|
{ 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
|
distanceApt !== race.AptitudeLevel.A
|
||||||
? { label: `${raceLenType} A, Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, race.AptitudeLevel.A, raceLen) }
|
? { label: `${raceLenType} A, Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, race.AptitudeLevel.A, raceLen) }
|
||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
|
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
|
||||||
@@ -82,7 +83,6 @@
|
|||||||
];
|
];
|
||||||
const duelSpeed: ComputedSeries[] = [{ label: 'Dueling Speed Boost', y: (x) => race.duelSpeedMod(x) }];
|
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 duelAccel: ComputedSeries[] = [{ label: 'Dueling Acceleration Boost', y: (x) => race.duelAccelMod(x) }];
|
||||||
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.GreatEscape);
|
|
||||||
const secSpeed: Array<ComputedAreas | null> = $derived([
|
const secSpeed: Array<ComputedAreas | null> = $derived([
|
||||||
{
|
{
|
||||||
label: 'Early Race',
|
label: 'Early Race',
|
||||||
@@ -132,8 +132,7 @@
|
|||||||
{ label: '1 of 2 or 2 of 2', y: (x) => (race.skillWitCheck(x, 2, 1) + race.skillWitCheck(x, 2, 2)) * 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: '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 3', y: (x) => race.skillWitCheck(x, 3, 3) * 100 },
|
||||||
// TODO(zeph): why is this wrong?
|
{ label: '3 of 4 or 4 of 4', y: (x) => (race.skillWitCheck(x, 4, 3) + race.skillWitCheck(x, 4, 4)) * 100 },
|
||||||
{ label: '3 of 3 or 3 of 4', y: (x) => (race.skillWitCheck(x, 4, 3) + race.skillWitCheck(x, 3, 3)) * 100 },
|
|
||||||
];
|
];
|
||||||
const poskeep: ComputedSeries[] = [
|
const poskeep: ComputedSeries[] = [
|
||||||
{ label: 'Front Runner S', y: (x) => 100 * race.frontModeEnterChance(x * 1.1) },
|
{ label: 'Front Runner S', y: (x) => 100 * race.frontModeEnterChance(x * 1.1) },
|
||||||
@@ -240,39 +239,77 @@
|
|||||||
|
|
||||||
<Sec h={2} id="speed">Speed</Sec>
|
<Sec h={2} id="speed">Speed</Sec>
|
||||||
|
|
||||||
<Sec h={3} id="spurt-speed">Spurt 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>
|
<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], { len: true, style: true, dist: true })}
|
{@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">Late Race Speed</Sec>
|
<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>
|
<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 })}
|
{@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={2} id="stamina">Stamina</Sec>
|
||||||
|
|
||||||
<Sec h={3} id="hp">HP</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>
|
<p>Max HP, i.e. starting HP.</p>
|
||||||
{@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000], { len: true })}
|
{@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000], { len: true })}
|
||||||
|
|
||||||
<Sec h={2} id="power">Power</Sec>
|
<Sec h={2} id="power">Power</Sec>
|
||||||
|
|
||||||
<Sec h={3} id="move-lane-mod">Move Lane Modifier</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>
|
<p>
|
||||||
Target speed bonus when changing lanes while under the effect of a lane change speed skill. See <a
|
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
|
href={resolve('/doc/frbm#lane-combo')}>lane combo</a
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.5])}
|
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.6])}
|
||||||
|
|
||||||
<Sec h={3} id="uphill">Uphill Target Speed Loss</Sec>
|
<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>
|
<p>Target speed modifier while running uphill.</p>
|
||||||
{@render statChart(race.Stat.Power, uphill, 'Target Speed Modifier (m/s)', [-2, 0])}
|
{@render statChart(race.Stat.Power, uphill, 'Target Speed Modifier (m/s)', [-2, 0])}
|
||||||
|
|
||||||
<Sec h={3} id="acceleration">Acceleration</Sec>
|
<Sec
|
||||||
<p>Acceleration.</p>
|
h={3}
|
||||||
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.5], {style: true})}
|
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">Lane Change Target Speed</Sec>
|
<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>
|
<p>Horizontal (rather than forward) target speed of changing lanes.</p>
|
||||||
<div class="mb-4 h-60 w-full md:h-96">
|
<div class="mb-4 h-60 w-full md:h-96">
|
||||||
<StatChart
|
<StatChart
|
||||||
@@ -280,41 +317,87 @@
|
|||||||
stat={race.Stat.Power}
|
stat={race.Stat.Power}
|
||||||
y={laneChange}
|
y={laneChange}
|
||||||
yLabel="Lane Change Target Speed (CW/s)"
|
yLabel="Lane Change Target Speed (CW/s)"
|
||||||
range={[0.01, 0.03]}
|
range={[0.01, 0.04]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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={2} id="guts">Guts</Sec>
|
||||||
|
|
||||||
<Sec h={3} id="spurt-speed-guts">Spurt Speed</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>
|
<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], { len: true, style: true, dist: true })}
|
{@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">Minimum Speed</Sec>
|
<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>
|
<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.4], { len: true })}
|
{@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.5], { len: true })}
|
||||||
|
|
||||||
<Sec h={3} id="hp-rate-mod">Late Race HP Consumption Rate Modifier</Sec>
|
<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>
|
<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])}
|
{@render statChart(race.Stat.Guts, gutsHPRate, 'HP Consumption Multiplier', [1.2, 1.6])}
|
||||||
|
|
||||||
<Sec h={3} id="spot-struggle">Spot Struggle</Sec>
|
<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>
|
<p>Speed boost and duration of spot struggle.</p>
|
||||||
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
<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.3]} />
|
<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, 12]} />
|
<StatChart stat={race.Stat.Guts} y={ssDur} yLabel="Duration (s)" range={[0, 14]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={3} id="dueling">Dueling</Sec>
|
<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>
|
<p>Speed boost and acceleration boost of dueling.</p>
|
||||||
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
<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.65]} />
|
<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.14]} />
|
<StatChart stat={race.Stat.Guts} y={duelAccel} yLabel="Acceleration Bonus (m/s²)" range={[0, 0.16]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={2} id="wit">Wit</Sec>
|
<Sec h={2} id="wit">Wit</Sec>
|
||||||
|
|
||||||
<Sec h={3} id="section-speed">Section Speed</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>
|
<p>Random variance in target speed per race section.</p>
|
||||||
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
|
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
|
||||||
<StatChart
|
<StatChart
|
||||||
@@ -322,7 +405,7 @@
|
|||||||
stat={race.Stat.Wit}
|
stat={race.Stat.Wit}
|
||||||
yArea={secSpeed}
|
yArea={secSpeed}
|
||||||
yLabel="Section Speed (m/s)"
|
yLabel="Section Speed (m/s)"
|
||||||
range={[17.5, 22.5]}
|
range={[17, 23]}
|
||||||
plotOptions={{ color: { legend: true } }}
|
plotOptions={{ color: { legend: true } }}
|
||||||
/>
|
/>
|
||||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||||
@@ -346,30 +429,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec h={3} id="downhill">Downhill Accel Mode Chance</Sec>
|
<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>
|
<p>Chance each second to enter downhill accel mode when eligible.</p>
|
||||||
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 60])}
|
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 75])}
|
||||||
|
|
||||||
<Sec h={3} id="spurt-accept">Reduced Spurt Accept Chance</Sec>
|
<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>
|
<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])}
|
{@render statChart(race.Stat.Wit, reducedSpurt, 'Accept Chance (% each candidate)', [0, 100])}
|
||||||
|
|
||||||
<Sec h={3} id="skill">Skill Activation Chance</Sec>
|
<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>
|
<p>
|
||||||
Chance that each skill that has a wit check will be eligible to activate. This uses base wit, so style aptitude and passive
|
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.
|
wit skills do not affect the chance, although mood does.
|
||||||
</p>
|
</p>
|
||||||
<p>TODO(zeph): Why is the 3 of 4 series getting values over 100%? Is it not just binom(3, 4, p) + binom(4, 4, p)?</p>
|
|
||||||
{@render statChart(race.Stat.Wit, skillChance, 'Skill Activation Chance (%)', [0, 100])}
|
{@render statChart(race.Stat.Wit, skillChance, 'Skill Activation Chance (%)', [0, 100])}
|
||||||
|
|
||||||
<Sec h={3} id="poskeep">Position Keep Mode Chance</Sec>
|
<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>
|
<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
|
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.
|
pace-down mode), each 2 seconds when eligible.
|
||||||
</p>
|
</p>
|
||||||
{@render statChart(race.Stat.Wit, poskeep, 'Mode Entry Chance (%)', [0, 50])}
|
{@render statChart(race.Stat.Wit, poskeep, 'Mode Entry Chance (%)', [0, 50])}
|
||||||
|
|
||||||
<Sec h={3} id="rushed">Rushed Chance</Sec>
|
<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>
|
<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])}
|
{@render statChart(race.Stat.Wit, rushed, 'Rushed Chance (%)', [0, 30])}
|
||||||
</Article>
|
</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,17 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolve } from '$app/paths';
|
import { page } from '$app/state';
|
||||||
|
import * as all from '$lib/all';
|
||||||
|
import IndexSection from '$lib/IndexSection.svelte';
|
||||||
|
|
||||||
|
const cur = $derived(all.page(page.route.id));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — Character Tools</h1>
|
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — {cur.name}</h1>
|
||||||
<p>Tools related to understanding Umamusume characters.</p>
|
<p>{cur.description}</p>
|
||||||
|
<IndexSection pages={all.PAGES.race} />
|
||||||
<ul class="mb-4 list-disc pl-4">
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/race/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other
|
|
||||||
distance aptitudes and running styles.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the
|
|
||||||
effects of individual skills.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { ClassValue } from 'svelte/elements';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: Snippet | string;
|
||||||
|
children: Snippet;
|
||||||
|
additional?: Snippet;
|
||||||
|
class?: ClassValue | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, children, additional, class: className }: Props = $props();
|
||||||
|
|
||||||
|
const cl = $derived([
|
||||||
|
'm-2 flex flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md',
|
||||||
|
className,
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cl}>
|
||||||
|
<span class="block">
|
||||||
|
{#if typeof title === 'string'}
|
||||||
|
{title}
|
||||||
|
{:else}
|
||||||
|
{@render title()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span class="block text-xl">
|
||||||
|
{@render children()}
|
||||||
|
</span>
|
||||||
|
{@render additional?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Plot from '@observablehq/plot';
|
||||||
|
import * as race from '$lib/race';
|
||||||
|
import * as accel from '$lib/racelib/accel';
|
||||||
|
import type { Attachment } from 'svelte/attachments';
|
||||||
|
import CalcInfo from '../CalcInfo.svelte';
|
||||||
|
|
||||||
|
let rawPower = $state(1200);
|
||||||
|
let rawGuts = $state(1200);
|
||||||
|
let rawWit = $state(1200);
|
||||||
|
let powerBonus = $state(0);
|
||||||
|
let gutsBonus = $state(0);
|
||||||
|
let witBonus = $state(0);
|
||||||
|
let style = $state(race.RunningStyle.FrontRunner);
|
||||||
|
let surfApt = $state(race.AptitudeLevel.A);
|
||||||
|
let styleApt = $state(race.AptitudeLevel.A);
|
||||||
|
let mood = $state(race.Mood.Normal);
|
||||||
|
let isCareer = $state(false);
|
||||||
|
|
||||||
|
let raceLen = $state(2000);
|
||||||
|
let surface = $state(race.Surface.Turf);
|
||||||
|
let conditions = $state(race.GroundConditions.Firm);
|
||||||
|
let slope = $state(0);
|
||||||
|
|
||||||
|
let rawSkills: accel.Skill[] = $state([]);
|
||||||
|
let skillToAdd: accel.Skill = $state({ accel: 0.2, dur: 1.2 });
|
||||||
|
const quick = [
|
||||||
|
{
|
||||||
|
name: 'GW+TTL',
|
||||||
|
skills: [
|
||||||
|
{ accel: 0.2, dur: 3 },
|
||||||
|
{ accel: 0.4, dur: 1.2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GW+EL',
|
||||||
|
skills: [
|
||||||
|
{ accel: 0.2, dur: 3 },
|
||||||
|
{ accel: 0.2, dur: 1.2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'Clear', skills: [] },
|
||||||
|
];
|
||||||
|
function addSkill() {
|
||||||
|
rawSkills.push(skillToAdd);
|
||||||
|
skillToAdd = { accel: 0.2, dur: 1.2 };
|
||||||
|
}
|
||||||
|
function removeSkill(i: number) {
|
||||||
|
rawSkills.splice(i, 1);
|
||||||
|
}
|
||||||
|
function setSkillList(l: accel.Skill[]) {
|
||||||
|
rawSkills = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
const phases = $derived(race.phases(raceLen));
|
||||||
|
|
||||||
|
const basePower = $derived(race.baseStat(mood, rawPower));
|
||||||
|
const adjPower = $derived(race.adjustedStat(race.Stat.Power, basePower, isCareer, surface, conditions));
|
||||||
|
const power = $derived(race.finalStat(adjPower, powerBonus));
|
||||||
|
const baseGuts = $derived(race.baseStat(mood, rawGuts));
|
||||||
|
const adjGuts = $derived(race.adjustedStat(race.Stat.Guts, baseGuts, isCareer));
|
||||||
|
const guts = $derived(race.finalStat(adjGuts, gutsBonus));
|
||||||
|
const baseWit = $derived(race.baseStat(mood, rawWit));
|
||||||
|
const adjWit = $derived(race.adjustedStat(race.Stat.Wit, baseWit, isCareer, styleApt));
|
||||||
|
const wit = $derived(race.finalStat(adjWit, witBonus));
|
||||||
|
|
||||||
|
const dashAccel = $derived(
|
||||||
|
race.acceleration(power, style, surfApt, race.Phase.EarlyRace, race.AptitudeLevel.A, slope > 0, true),
|
||||||
|
);
|
||||||
|
const postAccel = $derived(
|
||||||
|
race.acceleration(power, style, surfApt, race.Phase.EarlyRace, race.AptitudeLevel.A, slope > 0, false),
|
||||||
|
);
|
||||||
|
const minSpeed = $derived(race.minSpeed(raceLen, guts));
|
||||||
|
const baseSpeed = $derived(race.baseSpeed(raceLen));
|
||||||
|
const dashTarget = $derived(baseSpeed * 0.85);
|
||||||
|
const hillMod: [number, number] = $derived.by(() => {
|
||||||
|
if (slope > 0) {
|
||||||
|
const m = race.uphillMod(power, slope);
|
||||||
|
return [m, m];
|
||||||
|
}
|
||||||
|
if (slope < 0) {
|
||||||
|
return [0, race.downhillAccelMod(slope)];
|
||||||
|
}
|
||||||
|
return [0, 0];
|
||||||
|
});
|
||||||
|
const earlyTarget = $derived(race.sectionSpeed(raceLen, baseWit, wit, style, race.Phase.EarlyRace));
|
||||||
|
const speeds: [number, number] = $derived([earlyTarget[0] + hillMod[0], earlyTarget[1] + hillMod[1]]);
|
||||||
|
|
||||||
|
const startInstant: accel.Instant = { t: 0, x: 0, v: 3 };
|
||||||
|
function doSeggsToIt(targetSpeed: number, skills: accel.Skill[]) {
|
||||||
|
const startDash = accel.up(startInstant, dashTarget, dashAccel, skills);
|
||||||
|
const postStart = { ...last(startDash.segments).end, v: minSpeed };
|
||||||
|
const post = accel.up(postStart, targetSpeed, postAccel, startDash.skills);
|
||||||
|
const r = [...startDash.segments, ...post.segments];
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
function last<T>(l: T[]): T {
|
||||||
|
return l[l.length - 1];
|
||||||
|
}
|
||||||
|
const skills = $derived(rawSkills.map((s) => ({ ...s, dur: race.skillDuration(s.dur, raceLen) })));
|
||||||
|
const seggs = $derived([doSeggsToIt(speeds[0], skills), doSeggsToIt(speeds[1], skills)]);
|
||||||
|
const unskilledSeggs = $derived([doSeggsToIt(speeds[0], []), doSeggsToIt(speeds[1], [])]);
|
||||||
|
|
||||||
|
const distance = $derived([last(seggs[0]).end.x, last(seggs[1]).end.x]);
|
||||||
|
const time = $derived([last(seggs[0]).end.t, last(seggs[1]).end.t]);
|
||||||
|
const midTime = $derived([
|
||||||
|
time[0] + (phases[race.Phase.EarlyRace] - distance[0]) / speeds[1],
|
||||||
|
time[1] + (phases[race.Phase.EarlyRace] - distance[1]) / speeds[0],
|
||||||
|
]);
|
||||||
|
function gainLengths(fast: accel.Instant, slow: accel.Instant): number {
|
||||||
|
const m = fast.v * (slow.t - fast.t) + fast.x - slow.x;
|
||||||
|
return m / race.HORSE_LENGTH;
|
||||||
|
}
|
||||||
|
const gain = $derived([
|
||||||
|
gainLengths(last(seggs[0]).end, last(unskilledSeggs[0]).end),
|
||||||
|
gainLengths(last(seggs[1]).end, last(unskilledSeggs[1]).end),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let width = $state(1);
|
||||||
|
let height = $state(1);
|
||||||
|
const plotLines = $derived([
|
||||||
|
{ t: 0, v: 3, x: 0, label: 'Slowest Speed' },
|
||||||
|
...seggs[0].map((t) => ({ t: t.end.t, v: t.end.v, x: t.end.x, label: 'Slowest Speed' })),
|
||||||
|
{ t: midTime[1], v: speeds[0], label: 'Slowest Speed' },
|
||||||
|
{ t: 0, v: 3, x: 0, label: 'Fastest Speed' },
|
||||||
|
...seggs[1].map((t) => ({ t: t.end.t, v: t.end.v, x: t.end.x, label: 'Fastest Speed' })),
|
||||||
|
{ t: midTime[1], v: speeds[1], label: 'Fastest Speed' },
|
||||||
|
{ t: 0, v: 3, x: 0, label: 'Without Skills' },
|
||||||
|
...unskilledSeggs[1].map((t) => ({ t: t.end.t, v: t.end.v, x: t.end.x, label: 'Without Skills' })),
|
||||||
|
{ t: midTime[1], v: speeds[1], label: 'Without Skills' },
|
||||||
|
]);
|
||||||
|
const makeChart: Attachment = (el) => {
|
||||||
|
$effect(() => {
|
||||||
|
el?.firstChild?.remove();
|
||||||
|
el?.append(
|
||||||
|
Plot.plot({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
clip: true,
|
||||||
|
x: {
|
||||||
|
domain: [0, midTime[1]],
|
||||||
|
label: 'Time (s)',
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
domain: [0, 23],
|
||||||
|
label: 'Speed (m/s)',
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
color: { legend: true },
|
||||||
|
marks: [Plot.frame(), Plot.line(plotLines, { x: 't', y: 'v', stroke: 'label', strokeWidth: 3 })],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-4xl">Gate Calculator</h1>
|
||||||
|
<form class="mx-auto my-4 grid max-w-4xl grid-cols-1 gap-4 rounded-md p-4 shadow-sm md:grid-cols-5">
|
||||||
|
<div class="grid rounded-md text-center shadow-md ring md:col-span-5 md:grid-cols-5">
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="powerStat">Power</label>
|
||||||
|
<input type="number" id="powerStat" bind:value={rawPower} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="gutsStat">Guts</label>
|
||||||
|
<input type="number" id="gutsStat" bind:value={rawGuts} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="witStat">Wit</label>
|
||||||
|
<input type="number" id="witStat" bind:value={rawWit} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="style">Style</label>
|
||||||
|
<select id="style" required bind:value={style} class="w-full">
|
||||||
|
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||||
|
<option value={style}>{name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="mood">Mood</label>
|
||||||
|
<select id="mood" required bind:value={mood} class="w-full">
|
||||||
|
{#each race.MOODS as mood (mood)}
|
||||||
|
<option value={mood}>{race.Mood[mood]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="powerBonus">Power Greens</label>
|
||||||
|
<input type="number" id="powerBonus" bind:value={powerBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="gutsBonus">Guts Greens</label>
|
||||||
|
<input type="number" id="gutsBonus" bind:value={gutsBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="witBonus">Wit Greens</label>
|
||||||
|
<input type="number" id="witBonus" bind:value={witBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="surfaceApt">{race.Surface[surface]}</label>
|
||||||
|
<select id="surfaceApt" required bind:value={surfApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="styleApt">Style</label>
|
||||||
|
<select id="styleApt" required bind:value={styleApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
|
||||||
|
<input
|
||||||
|
class="col-span-4 m-4 w-full justify-self-center-safe"
|
||||||
|
type="range"
|
||||||
|
id="raceLen"
|
||||||
|
min="1000"
|
||||||
|
max="3600"
|
||||||
|
step="100"
|
||||||
|
bind:value={raceLen}
|
||||||
|
/>
|
||||||
|
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
|
||||||
|
<select id="surface" bind:value={surface} class="col-span-2 m-4">
|
||||||
|
<option value={race.Surface.Turf}>Turf</option>
|
||||||
|
<option value={race.Surface.Dirt}>Dirt</option>
|
||||||
|
</select>
|
||||||
|
<select id="conditions" bind:value={conditions} class="col-span-2 m-4">
|
||||||
|
{#each race.GROUND_CONDITONS as cond (cond)}
|
||||||
|
<option value={cond}>{race.GroundConditions[cond]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select id="slope" bind:value={slope} class="col-span-2 m-4">
|
||||||
|
{#each [2, 1.5, 1, 0, -1, -1.5, -2] as per (per)}
|
||||||
|
<option value={per}>{per > 0 ? `Uphill +${per}` : per < 0 ? `Downhill ${per}` : 'Flat'}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-auto flex w-full max-w-4xl flex-col rounded-md text-center shadow-md ring md:col-span-2">
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<span class="w-24 flex-1">Duration</span>
|
||||||
|
<span class="w-24 flex-1">Accel</span>
|
||||||
|
<span class="w-8 flex-none"><!-- filler to align other columns --></span>
|
||||||
|
</div>
|
||||||
|
{#each rawSkills as skill, i (i)}
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.dur}s</span>
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.accel}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="my-auto h-8 w-8 flex-none rounded-full bg-mist-300 text-2xl hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={() => removeSkill(i)}>×</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0.9" step="0.3" bind:value={skillToAdd.dur} />
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0" step="0.05" bind:value={skillToAdd.accel} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="my-auto h-8 w-8 flex-none rounded-full bg-mist-300 text-2xl hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={addSkill}>+</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex flex-row">
|
||||||
|
<span class="my-auto h-0 flex-1 border"></span>
|
||||||
|
<span class="mx-2 flex-none">Quick Setup</span>
|
||||||
|
<span class="my-auto h-0 flex-1 border"></span>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 flex flex-row place-content-center-safe gap-2">
|
||||||
|
{#each quick as q (q.name)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={() => setSkillList(q.skills)}>{q.name}</button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#snippet info(name: string, unit: string, val: number, val2?: number)}
|
||||||
|
<CalcInfo title={name} class="max-w-72 flex-1">
|
||||||
|
{#if val2 == null}
|
||||||
|
<span class="block text-xl">{val.toFixed(3)} {unit}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="block text-xl">{val.toFixed(3)} – {val2.toFixed(3)} {unit}</span>
|
||||||
|
{/if}
|
||||||
|
</CalcInfo>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Base Acceleration', 'm/s²', postAccel)}
|
||||||
|
{@render info('Target Speed', 'm/s', speeds[0], speeds[1])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Accel Time', 's', time[0], time[1])}
|
||||||
|
{@render info('Accel Distance', 'm', distance[0], distance[1])}
|
||||||
|
{@render info('Time to Mid-Race', 's', midTime[0], midTime[1])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Skill Gain', 'L', gain[0], gain[1])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mb-12 h-72 max-w-4xl" bind:clientWidth={width} bind:clientHeight={height}>
|
||||||
|
<div role="img" {@attach makeChart}>the chart seems to have didn't</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#snippet seggsTable(name: string, s: accel.Travel[])}
|
||||||
|
<table class="table-fixed border">
|
||||||
|
<caption class="text-xl">{name}</caption>
|
||||||
|
<thead>
|
||||||
|
<tr class="py-2">
|
||||||
|
<th class="px-2" scope="col">Time</th>
|
||||||
|
<th class="px-2" scope="col">Accel</th>
|
||||||
|
<th class="px-2" scope="col">Distance</th>
|
||||||
|
<th class="px-2" scope="col">Speed</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each s as { start, end, a } (start.t)}
|
||||||
|
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
|
||||||
|
<td class="px-4 text-center">{end.t.toFixed(3)}</td>
|
||||||
|
<td class="px-4 text-center">{a.toFixed(3)}</td>
|
||||||
|
<td class="px-4 text-center">{end.x.toFixed(3)}</td>
|
||||||
|
<td class="px-4 text-center">{end.v.toFixed(3)}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/snippet}
|
||||||
|
<div class="mx-auto mt-8 grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
|
{@render seggsTable(slope < 0 ? 'Fastest Speed (Downhill)' : 'Fastest Speed', seggs[1])}
|
||||||
|
{@render seggsTable('Slowest Speed', seggs[0])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
|
||||||
|
<p>
|
||||||
|
This calculator integrates acceleration analytically for efficiency, but the game uses a finite step approximation. The
|
||||||
|
difference has some implications:
|
||||||
|
</p>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li>Late starts are dramatically more punishing in the game, as acceleration is not applied for the first frame.</li>
|
||||||
|
<li>
|
||||||
|
Start dash always completes a single frame at the dash target speed. This calculator finds the exact moment start dash
|
||||||
|
target speed would be reached and continues integrating from there, leading to a slightly higher speed.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -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)));
|
||||||
@@ -164,21 +164,13 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</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
|
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 2]} xRule={gutsStat} yRule={ssYRule} />
|
||||||
class="flex-1"
|
|
||||||
stat={Stat.Guts}
|
|
||||||
y={ssY}
|
|
||||||
yLabel="Lengths Gained"
|
|
||||||
range={[0, 1.5, 2.5]}
|
|
||||||
xRule={gutsStat}
|
|
||||||
yRule={ssYRule}
|
|
||||||
/>
|
|
||||||
<StatChart
|
<StatChart
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
stat={Stat.Power}
|
stat={Stat.Power}
|
||||||
y={lcY}
|
y={lcY}
|
||||||
yLabel="Lengths Gained"
|
yLabel="Lengths Gained"
|
||||||
range={[0, 1.5, 2.5]}
|
range={[0, 1.1]}
|
||||||
xRule={powerStat}
|
xRule={powerStat}
|
||||||
yRule={lcYRule}
|
yRule={lcYRule}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { HORSE_LENGTH, speedGain } from '$lib/race';
|
import { HORSE_LENGTH, speedGain } from '$lib/race';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import CalcInfo from '../CalcInfo.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
@@ -21,13 +22,12 @@
|
|||||||
const text = $derived(gain.map(fmtp).join(' – '));
|
const text = $derived(gain.map(fmtp).join(' – '));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<CalcInfo title={children} class="max-w-80 flex-1">
|
||||||
class="m-2 flex h-full w-full max-w-80 flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md"
|
{text} L
|
||||||
>
|
{#snippet additional()}
|
||||||
<div class="block">{@render children()}</div>
|
|
||||||
<span class="block text-xl">{text} L</span>
|
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<span class="flex-1 text-xs">{fmtp(speed)} m/s</span>
|
<span class="flex-1 text-xs">{fmtp(speed)} m/s</span>
|
||||||
<span class="flex-1 text-xs">{dur.toFixed(3)} s</span>
|
<span class="flex-1 text-xs">{dur.toFixed(3)} s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/snippet}
|
||||||
|
</CalcInfo>
|
||||||
|
|||||||
@@ -1,61 +1,74 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComputedSeries } from '$lib/chart';
|
import type { ComputedSeries } from '$lib/chart';
|
||||||
import { AptitudeLevel, 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';
|
||||||
|
|
||||||
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(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 y: 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 A', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.A, raceLen) },
|
{
|
||||||
distanceApt < AptitudeLevel.A
|
label: `Aptitude ${race.AptitudeLevel[distanceApt]} + Dueling`,
|
||||||
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) }
|
y: (x) => race.spurtSpeed(x, gutsStat, style, distanceApt, raceLen) + duelBonus,
|
||||||
|
},
|
||||||
|
distanceApt !== race.AptitudeLevel.S
|
||||||
|
? {
|
||||||
|
label: `Aptitude ${race.AptitudeLevel[distanceApt]}`,
|
||||||
|
y: (x) => race.spurtSpeed(x, gutsStat, style, distanceApt, raceLen),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
]);
|
||||||
|
const gutsChart: Array<ComputedSeries | null> = $derived([
|
||||||
|
{ label: 'Aptitude S', y: (x) => race.spurtSpeed(speedStat, x, style, race.AptitudeLevel.S, raceLen) },
|
||||||
|
{
|
||||||
|
label: `Aptitude ${race.AptitudeLevel[distanceApt]} + Dueling`,
|
||||||
|
y: (x) => race.spurtSpeed(speedStat, x, style, distanceApt, raceLen) + race.duelSpeedMod(x),
|
||||||
|
},
|
||||||
|
distanceApt !== race.AptitudeLevel.S
|
||||||
|
? {
|
||||||
|
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),
|
|
||||||
]);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-4xl">Spurt Speed Calculator</h1>
|
<h1 class="text-4xl">Spurt Speed Calculator</h1>
|
||||||
@@ -71,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>
|
||||||
@@ -93,24 +106,18 @@
|
|||||||
<input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" />
|
<input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="mt-8 block w-full text-center text-lg">
|
<div class="mx-auto mt-8 flex max-w-3xl flex-col md:flex-row">
|
||||||
Target spurt speed: {speed.toFixed(3)} m/s
|
<CalcInfo title="Target Spurt Speed" class="flex-1">{speed.toFixed(3)} m/s</CalcInfo>
|
||||||
</span>
|
<CalcInfo title="Dueling Bonus" class="flex-1">+{duelBonus.toFixed(3)} m/s</CalcInfo>
|
||||||
{#each [[AptitudeLevel.A, aProf] as const, [AptitudeLevel.S, sProf] as const] as [level, inv] (level)}
|
</div>
|
||||||
|
{#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)}
|
||||||
<div
|
<CalcInfo title={styleName} class={['flex-1', { 'border-2': s === style }]}>{inv[s]}</CalcInfo>
|
||||||
class={['m-2 flex-1 rounded-md border shadow-sm transition-shadow hover:shadow-md', s === style ? 'border-2' : null]}
|
|
||||||
>
|
|
||||||
<div class="h-full w-full flex-col text-center">
|
|
||||||
<span class="block text-lg">{styleName}</span>
|
|
||||||
<span class="block text-2xl">{inv[s]}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,23 +126,19 @@
|
|||||||
<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
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col md:flex-row">
|
<div class="flex flex-col md:flex-row">
|
||||||
{#each skillProf as [v, inv] (v)}
|
{#each skillProf as [v, inv] (v)}
|
||||||
<div class="m-2 flex-1 rounded-md border shadow-sm transition-shadow hover:shadow-md">
|
<CalcInfo title={`+${v.toFixed(2)}`} class="flex-1">{inv}</CalcInfo>
|
||||||
<div class="h-full w-full flex-col text-center">
|
|
||||||
<span class="block text-lg">+{v.toFixed(2)}</span>
|
|
||||||
<span class="block text-2xl">{inv}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-auto h-60 max-w-3xl place-content-center py-4 md:h-96">
|
<div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
|
||||||
<StatChart stat={Stat.Speed} {y} 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={race.Stat.Guts} y={gutsChart} yLabel="Spurt Speed (m/s)" xRule={gutsStat} {range} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+17
-14
@@ -1,6 +1,5 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
import { playwright } from '@vitest/browser-playwright';
|
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -9,19 +8,20 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
expect: { requireAssertions: true },
|
expect: { requireAssertions: true },
|
||||||
projects: [
|
projects: [
|
||||||
{
|
// Playwright tests are disabled because I don't feel like setting it up in NixOS.
|
||||||
extends: './vite.config.ts',
|
// {
|
||||||
test: {
|
// extends: './vite.config.ts',
|
||||||
name: 'client',
|
// test: {
|
||||||
browser: {
|
// name: 'client',
|
||||||
enabled: true,
|
// browser: {
|
||||||
provider: playwright(),
|
// enabled: true,
|
||||||
instances: [{ browser: 'chromium', headless: true }],
|
// provider: playwright(),
|
||||||
},
|
// instances: [{ browser: 'chromium', headless: true }],
|
||||||
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
// },
|
||||||
exclude: ['src/lib/server/**'],
|
// include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||||
},
|
// exclude: ['src/lib/server/**'],
|
||||||
},
|
// },
|
||||||
|
// },
|
||||||
|
|
||||||
{
|
{
|
||||||
extends: './vite.config.ts',
|
extends: './vite.config.ts',
|
||||||
@@ -39,6 +39,9 @@ export default defineConfig({
|
|||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:13669',
|
target: 'http://localhost:13669',
|
||||||
},
|
},
|
||||||
|
'/img': {
|
||||||
|
target: 'http://localhost:13669',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user