Compare commits
61 Commits
75024c7c11
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 48118efeeb | |||
| 79d4e76090 | |||
| ea39be0510 | |||
| 7bac55ca43 | |||
| 435da61405 | |||
| f3e7c7b43b | |||
| eded415df4 | |||
| 34029fe001 | |||
| ffb700545f | |||
| 70ad09a13e | |||
| 74c22c7313 | |||
| 2c1b4b9261 | |||
| e5128b6783 | |||
| b22c8307fd | |||
| cb54bbe414 | |||
| 096cff28b6 | |||
| 197df8f022 | |||
| 009dd1d695 | |||
| bdafc93315 | |||
| ef5b06d66e | |||
| 65b81612bc | |||
| 6269665e79 | |||
| cae7d5b8f4 | |||
| fca43e64b5 | |||
| bc395a0788 | |||
| 1d879cbf9f | |||
| 240a3d9a73 | |||
| 3d73460934 | |||
| 0c518a9ce2 | |||
| 5cbb6d7b1d | |||
| b0bd16fd04 | |||
| 55175c9c40 | |||
| 06e5a06680 | |||
| dbb0dcd2dc | |||
| 02f723b83a | |||
| 1f58dec491 | |||
| a6d264c79d | |||
| d62e729052 | |||
| 9ce665865e | |||
| 7d395b2cc7 | |||
| 2b69211a6c | |||
| 67377c4690 | |||
| 1d65a0ec14 | |||
| ad58edbfe1 | |||
| f8e7bfbec6 | |||
| 2e618b613f | |||
| cc2b5fcfcb | |||
| 8ef3936be5 | |||
| b06e4b537b | |||
| 7b4cd41d3b | |||
| 2c9434729a | |||
| 7f02925ff7 | |||
| b217691c8a | |||
| 4bce196c09 | |||
| aa17d8ccf3 | |||
| 26a5ca4496 | |||
| 51f942ee18 | |||
| 2c773391f0 | |||
| c66d969c82 | |||
| bc9d722ee0 | |||
| 6482f11620 |
@@ -0,0 +1,12 @@
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
- event: pull_request
|
||||
- event: manual
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: golang:1.26-alpine
|
||||
commands:
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
@@ -0,0 +1,15 @@
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
- event: pull_request
|
||||
- event: manual
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: node:24-alpine
|
||||
commands:
|
||||
- cd zenno
|
||||
- npm ci
|
||||
- npm run check
|
||||
- npm run lint
|
||||
- npm run test
|
||||
@@ -20,8 +20,10 @@ func _() {
|
||||
_ = x[AbilityFrenzy-13]
|
||||
_ = x[AbilityAddGateDelay-14]
|
||||
_ = x[AbilityCurrentSpeed-21]
|
||||
_ = x[AbilityCurrentSpeedUp-22]
|
||||
_ = x[AbilityTargetSpeed-27]
|
||||
_ = x[AbilityLaneSpeed-28]
|
||||
_ = x[AbilityRushedChance-29]
|
||||
_ = x[AbilityAccel-31]
|
||||
_ = x[AbilityLaneChange-35]
|
||||
}
|
||||
@@ -30,8 +32,8 @@ const (
|
||||
_AbilityType_name_0 = "SpeedStaminaPowerGutsWitEnable Great Escape"
|
||||
_AbilityType_name_1 = "VisionHPGate delay multiplier"
|
||||
_AbilityType_name_2 = "FrenzyAdded gate delay"
|
||||
_AbilityType_name_3 = "Current speed"
|
||||
_AbilityType_name_4 = "Target speedLane change speed"
|
||||
_AbilityType_name_3 = "Current speedCurrent speed"
|
||||
_AbilityType_name_4 = "Target speedLane change speedRushed chance"
|
||||
_AbilityType_name_5 = "Acceleration"
|
||||
_AbilityType_name_6 = "Forced lane change"
|
||||
)
|
||||
@@ -40,7 +42,8 @@ var (
|
||||
_AbilityType_index_0 = [...]uint8{0, 5, 12, 17, 21, 24, 43}
|
||||
_AbilityType_index_1 = [...]uint8{0, 6, 8, 29}
|
||||
_AbilityType_index_2 = [...]uint8{0, 6, 22}
|
||||
_AbilityType_index_4 = [...]uint8{0, 12, 29}
|
||||
_AbilityType_index_3 = [...]uint8{0, 13, 26}
|
||||
_AbilityType_index_4 = [...]uint8{0, 12, 29, 42}
|
||||
)
|
||||
|
||||
func (i AbilityType) String() string {
|
||||
@@ -54,9 +57,10 @@ func (i AbilityType) String() string {
|
||||
case 13 <= i && i <= 14:
|
||||
i -= 13
|
||||
return _AbilityType_name_2[_AbilityType_index_2[i]:_AbilityType_index_2[i+1]]
|
||||
case i == 21:
|
||||
return _AbilityType_name_3
|
||||
case 27 <= i && i <= 28:
|
||||
case 21 <= i && i <= 22:
|
||||
i -= 21
|
||||
return _AbilityType_name_3[_AbilityType_index_3[i]:_AbilityType_index_3[i+1]]
|
||||
case 27 <= i && i <= 29:
|
||||
i -= 27
|
||||
return _AbilityType_name_4[_AbilityType_index_4[i]:_AbilityType_index_4[i+1]]
|
||||
case i == 31:
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
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
|
||||
mdbf string
|
||||
public string
|
||||
img string
|
||||
// discord
|
||||
tokenFile string
|
||||
apiRoute string
|
||||
@@ -47,6 +48,7 @@ func main() {
|
||||
flag.StringVar(&addr, "http", ":13669", "`address` to bind HTTP server")
|
||||
flag.StringVar(&mdbf, "mdb", "", "`path` to master.mdb")
|
||||
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(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls")
|
||||
flag.StringVar(&pubkey, "key", "", "Discord public key")
|
||||
@@ -86,6 +88,17 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if img != "" {
|
||||
// Ensure the img directory is in fact a directory
|
||||
// before we start trying to fill it out.
|
||||
err := os.MkdirAll(img, 0777)
|
||||
if err != nil {
|
||||
slog.Error("img", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
go saveImgs(ctx, db, img)
|
||||
}
|
||||
|
||||
var client *bot.Client
|
||||
if tokenFile != "" {
|
||||
skills, err := mdb.Skills(ctx, db)
|
||||
@@ -128,6 +141,9 @@ func main() {
|
||||
if 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-detail", httpmiddle.Compress(5)(dataroute(ctx, db, mdb.AffinityDetail)))
|
||||
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)
|
||||
- 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)
|
||||
|
||||
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
|
||||
- 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)
|
||||
- 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!!!
|
||||
|
||||
|
||||
@@ -3,32 +3,38 @@ module git.sunturtle.xyz/zephyr/horse
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/disgoorg/disgo v0.19.0-rc.15
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/junegunn/fzf v0.67.0
|
||||
github.com/disgoorg/disgo v0.19.6
|
||||
github.com/gen2brain/avif v0.6.0
|
||||
github.com/go-chi/chi/v5 v5.3.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/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
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/disgoorg/godave v0.3.0 // indirect
|
||||
github.com/disgoorg/json/v2 v2.0.0 // indirect
|
||||
github.com/disgoorg/omit v1.0.0 // indirect
|
||||
github.com/disgoorg/snowflake/v2 v2.0.3 // 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/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 // indirect
|
||||
github.com/klauspost/compress v1.19.1 // 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/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
github.com/tetratelabs/wazero v1.12.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
modernc.org/libc v1.74.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disgoorg/disgo v0.19.0-rc.15 h1:x0NsV2gcbdjwuztsg2wYXw76p1Cpc8f6ByDrkPcfQtU=
|
||||
github.com/disgoorg/disgo v0.19.0-rc.15/go.mod h1:14mgXzenkJqifkDmsEgU0zI1di6jNXodwX6L8geW33A=
|
||||
github.com/disgoorg/disgo v0.19.6 h1:1xJQt6KZgiqvBOnjuziG1gE4Iqd0aJn8DNfE6eE+0qw=
|
||||
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/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI=
|
||||
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/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/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gen2brain/avif v0.6.0 h1:/8WSgcU+IEF0jhKYsUZ/mzlziFuTeJFpIKBj2siTQps=
|
||||
github.com/gen2brain/avif v0.6.0/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
|
||||
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
|
||||
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
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/junegunn/fzf v0.67.0/go.mod h1:xlXX2/rmsccKQUnr9QOXPDi5DyV9cM0UjKy/huScBeE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/junegunn/fzf v0.74.1 h1:rCGVdCSoOkfcfTxtxCO/vG2yceDjRlKSlv7IfCGkc/Y=
|
||||
github.com/junegunn/fzf v0.74.1/go.mod h1:hKDkO8orBWT5VLIZOL8oyhxn7mRLXdGyu+gz1Ss58pU=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
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/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
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/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
|
||||
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/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -14,4 +14,4 @@ SELECT
|
||||
FROM single_mode_scenario sc
|
||||
JOIN scenario_name n ON sc.id = n.id
|
||||
JOIN scenario_title t ON sc.id = t.id
|
||||
ORDER BY sc.id
|
||||
ORDER BY sc.sort_id
|
||||
|
||||
@@ -78,6 +78,9 @@ func (a Ability) String() string {
|
||||
case AbilityHP:
|
||||
r = append(r, (a.Value * 100).String()...)
|
||||
r = append(r, '%')
|
||||
case AbilityRushedChance:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, '%')
|
||||
case AbilityGateDelay:
|
||||
// This skill type in particular should be × instead of +.
|
||||
r = append(r[:len(r)-1], "×"...)
|
||||
@@ -85,7 +88,7 @@ func (a Ability) String() string {
|
||||
case AbilityFrenzy, AbilityAddGateDelay:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, 's')
|
||||
case AbilityCurrentSpeed, AbilityTargetSpeed, AbilityLaneSpeed:
|
||||
case AbilityCurrentSpeed, AbilityCurrentSpeedUp, AbilityTargetSpeed, AbilityLaneSpeed:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, " m/s"...)
|
||||
case AbilityAccel:
|
||||
@@ -160,8 +163,10 @@ const (
|
||||
AbilityFrenzy AbilityType = 13 // Frenzy
|
||||
AbilityAddGateDelay AbilityType = 14 // Added gate delay
|
||||
AbilityCurrentSpeed AbilityType = 21 // Current speed
|
||||
AbilityCurrentSpeedUp AbilityType = 22 // Current speed
|
||||
AbilityTargetSpeed AbilityType = 27 // Target speed
|
||||
AbilityLaneSpeed AbilityType = 28 // Lane change speed
|
||||
AbilityRushedChance AbilityType = 29 // Rushed chance
|
||||
AbilityAccel AbilityType = 31 // Acceleration
|
||||
AbilityLaneChange AbilityType = 35 // Forced lane change
|
||||
)
|
||||
|
||||
@@ -20,32 +20,35 @@
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.60.1",
|
||||
"@sveltejs/kit": "^2.70.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/node": "^22.19.19",
|
||||
"@types/node": "^22.20.1",
|
||||
"@vitest/browser-playwright": "^4.1.0",
|
||||
"d3": "^7.9.0",
|
||||
"eslint": "^10.4.0",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.17.1",
|
||||
"globals": "^17.6.0",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "^3.8.3",
|
||||
"eslint-plugin-svelte": "^3.22.0",
|
||||
"globals": "^17.7.0",
|
||||
"playwright": "^1.61.1",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier-plugin-svelte": "^3.5.2",
|
||||
"prettier-plugin-tailwindcss": "^0.7.4",
|
||||
"svelte": "^5.55.7",
|
||||
"svelte-check": "^4.4.8",
|
||||
"svelte": "^5.56.7",
|
||||
"svelte-check": "^4.7.3",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.59.3",
|
||||
"vite": "^7.3.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^7.3.6",
|
||||
"vitest": "^4.1.0",
|
||||
"vitest-browser-svelte": "^2.1.1"
|
||||
"vitest-browser-svelte": "^2.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@observablehq/plot": "^0.6.17",
|
||||
"mathjs": "^15.2.0"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild@0.28.1": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
const sizes = [256, 64, 32, 16] as const;
|
||||
|
||||
interface Props {
|
||||
chara_id: number | null | undefined;
|
||||
size: (typeof sizes)[number];
|
||||
}
|
||||
|
||||
let { chara_id, size, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
|
||||
|
||||
const icon = $derived(chara_id ?? 0);
|
||||
const avifs = $derived(
|
||||
sizes
|
||||
.filter((sz) => sz >= size)
|
||||
.map((sz) => `/img/chara/${icon}/${sz}x${sz}.avif ${sz / size}x`)
|
||||
.join(', '),
|
||||
);
|
||||
const pngs = $derived(
|
||||
sizes
|
||||
.filter((sz) => sz >= size)
|
||||
.map((sz) => `/img/chara/${icon}/${sz}x${sz}.png ${sz / size}x`)
|
||||
.join(', '),
|
||||
);
|
||||
const src = $derived(icon !== 0 ? `/img/chara/${icon}/${sizes[0]}x${sizes[0]}.png` : `/img/skill/10011`);
|
||||
</script>
|
||||
|
||||
{#if icon !== 0}
|
||||
<picture {...rest}>
|
||||
<source srcset={avifs} type="image/avif" />
|
||||
<source srcset={pngs} type="image/png" />
|
||||
<img {src} alt={`Icon for character ID ${chara_id}`} class="inline" width={size} height={size} loading="lazy" />
|
||||
</picture>
|
||||
{:else}
|
||||
<div class={`w-[${size}px] h-[${size}px]`}></div>
|
||||
{/if}
|
||||
@@ -1,24 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { type Character } from '$lib/data/character';
|
||||
import type { Character } from '$lib/data/character';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import Pick from './Pick.svelte';
|
||||
import CharaIcon from './CharaIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
characters: Character[] | null;
|
||||
value: number;
|
||||
value: Character | undefined;
|
||||
class?: ClassValue | null;
|
||||
optionClass?: ClassValue | null;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
let { id, characters, value = $bindable(), class: className, optionClass, required = false }: Props = $props();
|
||||
let { id, characters, value = $bindable(), class: className, required = false }: Props = $props();
|
||||
|
||||
const charas = $derived(characters ?? []);
|
||||
|
||||
const key = (v: Character) => ({ id: v.chara_id, name: v.name });
|
||||
</script>
|
||||
|
||||
<select {id} class={className} bind:value {required}>
|
||||
{#if !required}
|
||||
<option value={0} class={optionClass}></option>
|
||||
{/if}
|
||||
{#each characters ?? [] as c (c.chara_id)}
|
||||
<option value={c.chara_id} class={optionClass}>{c.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Pick {id} items={charas} {key} bind:value class={className} {required}>
|
||||
{#snippet option(c: Character)}
|
||||
<span>
|
||||
<CharaIcon chara_id={c.chara_id} size={32} />
|
||||
{c.name}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Pick>
|
||||
|
||||
@@ -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 Skill,
|
||||
} from './data/skill';
|
||||
import SkillIcon from './SkillIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
skill: Skill | null | undefined;
|
||||
@@ -24,6 +25,7 @@
|
||||
group_rate: 1,
|
||||
wit_check: false,
|
||||
activations: [],
|
||||
tags: [],
|
||||
icon_id: 0,
|
||||
};
|
||||
|
||||
@@ -62,9 +64,13 @@
|
||||
|
||||
<span class="group relative inline-block hyphens-none">
|
||||
<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"
|
||||
>
|
||||
<span class="block text-xl">
|
||||
<SkillIcon icon_id={skill?.icon_id} size={32} class="inline" />
|
||||
{s.name}
|
||||
</span>
|
||||
<span class="block border-b py-1 hyphens-auto">
|
||||
{s.description}
|
||||
</span>
|
||||
@@ -91,5 +97,6 @@
|
||||
</span>
|
||||
{/each}
|
||||
</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>
|
||||
|
||||
@@ -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 height = $state(0);
|
||||
|
||||
let expand = $state(false);
|
||||
|
||||
const xLabel = $derived(Stat[stat]);
|
||||
const xLabel = $derived(`Raw ${Stat[stat]}`);
|
||||
const xLines = $derived([xRule].flat(1));
|
||||
const xMin = $derived(xRange?.[0] ?? 200);
|
||||
const xMax = $derived.by(() => {
|
||||
if (xRange?.[1] != null) {
|
||||
return xRange[1];
|
||||
}
|
||||
if (expand) {
|
||||
return 2000;
|
||||
}
|
||||
return 100 * Math.ceil(Math.max(1200, ...xLines) / 100);
|
||||
return 100 * Math.ceil(Math.max(2000, ...xLines) / 100);
|
||||
});
|
||||
const xVal = $derived(d3.range(xMin, xMax + 1, 5)); // +1 because d3.range does not include the max
|
||||
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 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 areaVals = $derived(areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(x), y2: y2(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(adj(x), x), y2: y2(adj(x), x), label }))),
|
||||
);
|
||||
const yRange: [number, number] = $derived.by(() => {
|
||||
if (range != null) {
|
||||
if (range.length === 2) {
|
||||
return range;
|
||||
}
|
||||
return [range[0], expand ? range[2] : range[1]];
|
||||
return [range[0], range[2]];
|
||||
}
|
||||
const l = d3.min(vals, ({ y }) => y) ?? 0;
|
||||
const r = d3.max(vals, ({ y }) => y) ?? 1;
|
||||
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) => {
|
||||
$effect(() => {
|
||||
@@ -122,7 +122,7 @@
|
||||
Plot.line(areaVals, { x: 'x', y: 'y1', stroke: 'label' }),
|
||||
Plot.line(areaVals, { x: 'x', y: 'y2', stroke: 'label' }),
|
||||
Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
|
||||
vals.length > 0 ? Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })) : null,
|
||||
vals.length > 0 ? Plot.tip(vals, Plot.pointerX({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })) : null,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ResolvedPathname } from '$app/types';
|
||||
|
||||
export interface Page {
|
||||
route: ResolvedPathname;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* All tool and doc pages in the site.
|
||||
*/
|
||||
export const PAGES = {
|
||||
index: [
|
||||
{
|
||||
route: resolve('/race'),
|
||||
name: 'Race Mechanics Tools',
|
||||
description: 'Tools for calculating Umamusume race characteristics.',
|
||||
},
|
||||
{
|
||||
route: resolve('/chara'),
|
||||
name: 'Character Tools',
|
||||
description: 'Tools for understanding Umamusume characters.',
|
||||
},
|
||||
{
|
||||
route: resolve('/doc'),
|
||||
name: 'Documents',
|
||||
description: 'Articles about Umamusume, including research and editorials.',
|
||||
},
|
||||
] satisfies Page[],
|
||||
race: [
|
||||
{
|
||||
route: resolve('/race/spurt'),
|
||||
name: 'Spurt Speed',
|
||||
description: 'Calculate target speed in the last spurt and compare to other distance aptitudes and running styles.',
|
||||
},
|
||||
{
|
||||
route: resolve('/race/mspeed'),
|
||||
name: 'Front Runner Mechanical Speed Comparator',
|
||||
description: 'Compare spot struggle and lane combo to the effects of individual skills.',
|
||||
},
|
||||
{
|
||||
route: resolve('/race/gate'),
|
||||
name: 'Gate Calculator',
|
||||
description: 'Calculate the importance of acceleration effects at the starting gate.',
|
||||
},
|
||||
] satisfies Page[],
|
||||
chara: [
|
||||
{
|
||||
route: resolve('/chara/affinity'),
|
||||
name: 'Affinity Details',
|
||||
description: 'Details of why characters have the base compatibility numbers they have.',
|
||||
},
|
||||
{
|
||||
route: resolve('/chara/convo'),
|
||||
name: 'Lobby Conversations',
|
||||
description: 'Check participants in lobby conversations and get recommendations on unlocking them quickly.',
|
||||
},
|
||||
] satisfies Page[],
|
||||
doc: [
|
||||
{
|
||||
route: resolve('/doc/frbm'),
|
||||
name: 'Front Runner Black Magic',
|
||||
description: 'Detailed analysis of how front runners interact with race mechanics.',
|
||||
},
|
||||
{
|
||||
route: resolve('/doc/lanecalc'),
|
||||
name: "Gaikokujin's Guide to Lanecalc",
|
||||
description: 'English language manual for 危険回避シミュ, or Lanecalc, for precise lane combo simulation.',
|
||||
},
|
||||
{
|
||||
route: resolve('/doc/race'),
|
||||
name: 'Race Mechanics Charts',
|
||||
description:
|
||||
"Interactive adaptation of KuromiAK's race mechanics documentation for easier visualization of how mechanics scale.",
|
||||
},
|
||||
{
|
||||
route: resolve('/doc/spark'),
|
||||
name: 'Spark Explorer',
|
||||
description: 'View all possible inheritance effects of any spark.',
|
||||
},
|
||||
{
|
||||
route: resolve('/doc/tagged-skills'),
|
||||
name: 'Tagged Skills Explorer',
|
||||
description: 'View skills matching the tags internal to the game.',
|
||||
},
|
||||
] satisfies Page[],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get the page object for a route.
|
||||
* @param route Route ID for the page
|
||||
* @returns Matching page, or an empty object if none match
|
||||
*/
|
||||
export function page(route: string | null): Partial<Page> {
|
||||
for (const p of Object.values(PAGES)) {
|
||||
const r = p.find((v) => v.route === route);
|
||||
if (r != null) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const styleMap = {
|
||||
Sentou: RunningStyle.PaceChaser,
|
||||
Sasi: RunningStyle.LateSurger,
|
||||
Oikomi: RunningStyle.EndCloser,
|
||||
Oonige: RunningStyle.GreatEscape,
|
||||
Oonige: RunningStyle.Runaway,
|
||||
} as const;
|
||||
|
||||
export interface ImportUma {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export interface ComputedSeries {
|
||||
y: (x: number) => number;
|
||||
y: (base: number, raw: number) => number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ComputedAreas {
|
||||
y1: (x: number) => number;
|
||||
y2: (x: number) => number;
|
||||
y1: (base: number, raw: number) => number;
|
||||
y2: (base: number, raw: number) => number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ export interface Skill {
|
||||
* SP cost to purchase the skill, if applicable.
|
||||
*/
|
||||
sp_cost?: number;
|
||||
/**
|
||||
* Numeric tags applied to the skill, for effects which select skill types.
|
||||
*/
|
||||
tags: number[];
|
||||
/**
|
||||
* Skill icon ID.
|
||||
*/
|
||||
@@ -133,10 +137,13 @@ export enum AbilityType {
|
||||
Frenzy = 13,
|
||||
AddGateDelay = 14,
|
||||
CurrentSpeed = 21,
|
||||
CurrentSpeedWithDecel = 22,
|
||||
TargetSpeed = 27,
|
||||
LaneSpeed = 28,
|
||||
RushedChance = 29,
|
||||
Accel = 31,
|
||||
LaneChange = 35,
|
||||
ActivateRare = 37,
|
||||
}
|
||||
|
||||
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.AddGateDelay]: (v) => plusBonus(v) + 's Gate Delay',
|
||||
[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.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.LaneChange]: (v) => 'Target Lane = ' + tenThousandths(v) + ' CW',
|
||||
[AbilityType.ActivateRare]: (v) => 'Activate Rare Skills ×' + tenThousandths(v),
|
||||
} as const;
|
||||
|
||||
export enum AbilityValueUsage {
|
||||
@@ -329,3 +339,55 @@ export async function skillGroups(): Promise<SkillGroup[]> {
|
||||
const resp = await fetch('/api/global/skill-group');
|
||||
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;
|
||||
|
||||
@@ -28,25 +28,141 @@ export interface Spark {
|
||||
* Spark type.
|
||||
* 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.
|
||||
* A random element is selected from this list according to unknown
|
||||
* 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.
|
||||
*/
|
||||
export interface SparkEffect {
|
||||
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;
|
||||
value1?: number;
|
||||
value2: number;
|
||||
export interface Effect {
|
||||
target: Target;
|
||||
value1: number;
|
||||
value2?: number;
|
||||
}
|
||||
|
||||
export async function sparks(): Promise<Spark[]> {
|
||||
const resp = await fetch('/api/global/spark');
|
||||
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()];
|
||||
}
|
||||
|
||||
@@ -31,9 +31,236 @@ export enum Mood {
|
||||
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.
|
||||
* 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.
|
||||
*/
|
||||
export enum RunningStyle {
|
||||
@@ -41,7 +268,7 @@ export enum RunningStyle {
|
||||
PaceChaser,
|
||||
LateSurger,
|
||||
EndCloser,
|
||||
GreatEscape,
|
||||
Runaway,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +279,7 @@ export const RUNNING_STYLES = [
|
||||
['Pace Chaser', RunningStyle.PaceChaser],
|
||||
['Late Surger', RunningStyle.LateSurger],
|
||||
['End Closer', RunningStyle.EndCloser],
|
||||
['Great Escape', RunningStyle.GreatEscape],
|
||||
['Runaway', RunningStyle.Runaway],
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -93,7 +320,103 @@ export enum Phase {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -107,6 +430,42 @@ const speedStrategyPhaseCoeff = [
|
||||
|
||||
const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as const;
|
||||
|
||||
/**
|
||||
* Get a horse's base target speed outside of late race.
|
||||
* @param raceLen Length of the race in meters
|
||||
* @param style Horse's running style
|
||||
* @param phase Phase to calculate for
|
||||
*/
|
||||
export function baseTargetSpeed(raceLen: number, style: RunningStyle, phase: Exclude<Phase, Phase.LateRace>): number;
|
||||
/**
|
||||
* Get a horse's base target speed when not spurting during late race.
|
||||
* @param raceLen Length of the race in meters
|
||||
* @param style Horse's running style
|
||||
* @param phase Phase to calculate for
|
||||
* @param speedStat Final speed stat
|
||||
* @param distanceApt Horse's aptitude for the distance
|
||||
*/
|
||||
export function baseTargetSpeed(
|
||||
raceLen: number,
|
||||
style: RunningStyle,
|
||||
phase: Phase.LateRace,
|
||||
speedStat: number,
|
||||
distanceApt: AptitudeLevel,
|
||||
): number;
|
||||
export function baseTargetSpeed(
|
||||
raceLen: number,
|
||||
style: RunningStyle,
|
||||
phase: Phase,
|
||||
speedStat?: number,
|
||||
distanceApt?: number,
|
||||
): number {
|
||||
const base = baseSpeed(raceLen);
|
||||
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
|
||||
const late =
|
||||
phase === Phase.LateRace ? (math.sqrt(500 * speedStat!) as number) * distanceProficiencyMod[distanceApt!] * 0.002 : 0;
|
||||
return baseTarget + late;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the range of section speed values for a horse in early or mid race.
|
||||
* @param raceLen Length of the race in meters
|
||||
@@ -236,7 +595,7 @@ const accelStrategyPhaseCoeff = {
|
||||
[RunningStyle.PaceChaser]: [0.985, 1.0, 0.996],
|
||||
[RunningStyle.LateSurger]: [0.975, 1.0, 1.0],
|
||||
[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;
|
||||
|
||||
const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
|
||||
@@ -295,6 +654,107 @@ export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): numbe
|
||||
return phaseDecel[phase];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a horse's lane change target speed before the first move lane point.
|
||||
* @param powerStat Final power stat
|
||||
* @param maxLane Max lane value on the race track in CW
|
||||
* @param currentLane Current lane on the race track in CW
|
||||
* @returns Lane change speed in CW/s
|
||||
*/
|
||||
export function laneChangeSpeed(powerStat: number, maxLane: number, currentLane: number): number;
|
||||
/**
|
||||
* Calculate a horse's lane change target speed during late race and final spurt phase.
|
||||
* @param powerStat Final power stat
|
||||
* @param order Current order on the field
|
||||
* @returns Lane change speed in CW/s
|
||||
*/
|
||||
export function laneChangeSpeed(powerStat: number, order: number): number;
|
||||
/**
|
||||
* Calculate a horse's lane change target speed between the first move lane point and late race.
|
||||
* @param powerStat Final power stat
|
||||
* @returns Lane change speed in CW/s
|
||||
*/
|
||||
export function laneChangeSpeed(powerStat: number): number;
|
||||
export function laneChangeSpeed(powerStat: number, maxLaneOrOrder?: number, currentLane?: number): number {
|
||||
const laneMod = currentLane != null ? 1 + (currentLane / maxLaneOrOrder!) * 0.05 : 1;
|
||||
const orderMod = currentLane == null ? 1 + (maxLaneOrOrder ?? 0) * 0.05 : 1;
|
||||
return 0.02 * (0.3 + 0.001 * powerStat) * laneMod * orderMod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acceleration of lane change (horizontal) current speed.
|
||||
*/
|
||||
export const LANE_CHANGE_ACCEL = 0.03;
|
||||
|
||||
const conserveStratDistCoef = {
|
||||
[RunningStyle.FrontRunner]: {
|
||||
[Distance.Sprint]: 1,
|
||||
[Distance.Mile]: 1,
|
||||
[Distance.Medium]: 1,
|
||||
[Distance.Long]: 1,
|
||||
},
|
||||
[RunningStyle.PaceChaser]: {
|
||||
[Distance.Sprint]: 0.7,
|
||||
[Distance.Mile]: 0.8,
|
||||
[Distance.Medium]: 0.9,
|
||||
[Distance.Long]: 0.9,
|
||||
},
|
||||
[RunningStyle.LateSurger]: {
|
||||
[Distance.Sprint]: 0.75,
|
||||
[Distance.Mile]: 0.7,
|
||||
[Distance.Medium]: 0.875,
|
||||
[Distance.Long]: 1,
|
||||
},
|
||||
[RunningStyle.EndCloser]: {
|
||||
[Distance.Sprint]: 0.7,
|
||||
[Distance.Mile]: 0.75,
|
||||
[Distance.Medium]: 0.86,
|
||||
[Distance.Long]: 0.9,
|
||||
},
|
||||
[RunningStyle.Runaway]: {
|
||||
[Distance.Sprint]: 1,
|
||||
[Distance.Mile]: 1,
|
||||
[Distance.Medium]: 1,
|
||||
[Distance.Long]: 1,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function conservePowerAccel(
|
||||
distanceType: Distance,
|
||||
style: RunningStyle,
|
||||
rawPower: number,
|
||||
powerSkillBonus?: number,
|
||||
spotStruggled?: boolean,
|
||||
rushed?: boolean,
|
||||
): number {
|
||||
const power = rawPower + (powerSkillBonus ?? 0);
|
||||
if (power <= 1200) {
|
||||
return 0;
|
||||
}
|
||||
const sdc = conserveStratDistCoef[style][distanceType];
|
||||
const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1;
|
||||
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a horse's minimum speed for a race.
|
||||
* @param raceLen Length of the race in meters
|
||||
* @param gutsStat Final guts stat
|
||||
* @returns Minimum speed in m/s
|
||||
*/
|
||||
export function minSpeed(raceLen: number, gutsStat: number): number {
|
||||
return 0.85 * baseSpeed(raceLen) + Math.sqrt(200 * gutsStat) * 0.001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a horse's HP consumption multiplier for late race and last spurt phase.
|
||||
* @param gutsStat Final guts stat
|
||||
* @returns HP consumption multiplier
|
||||
*/
|
||||
export function spurtHPRateMod(gutsStat: number): number {
|
||||
return 1 + 200 / Math.sqrt(600 * gutsStat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the speed boost gained from spot struggle.
|
||||
* @param gutsStat Final guts stat
|
||||
@@ -304,8 +764,6 @@ export function spotStruggleSpeed(gutsStat: number): number {
|
||||
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.
|
||||
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,
|
||||
@@ -319,17 +777,44 @@ export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLe
|
||||
return Math.sqrt(700 * gutsStat) * 0.012 * strategyProficiencyMod[frontAptitude];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the target speed bonus of dueling.
|
||||
* @param gutsStat Final guts stat
|
||||
* @returns Modifier to target speed while dueling in m/s
|
||||
*/
|
||||
export function duelSpeedMod(gutsStat: number): number {
|
||||
return Math.pow(200 * gutsStat, 0.708) * 0.0001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the acceleration bonus of dueling.
|
||||
* @param gutsStat Final guts stat
|
||||
* @returns Modifier to acceleration while dueling in m/s²
|
||||
*/
|
||||
export function duelAccelMod(gutsStat: number): number {
|
||||
return Math.pow(160 * gutsStat, 0.59) * 0.0001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the speed modifier for running uphill.
|
||||
* Contrary to the race mechanics document, this is expressed as a negative number.
|
||||
* @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
|
||||
*/
|
||||
export function uphillMod(powerStat: number, slopePer: number): number {
|
||||
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
|
||||
* that grants a lane change speed boost is active.
|
||||
@@ -389,6 +874,16 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null,
|
||||
return speedBonus * (dur + 0.5 * (decelTime - accelTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the probability of accepting a reduced spurt candidate.
|
||||
* @param witStat Final wit stat
|
||||
* @returns Probability per candidate to accept
|
||||
*/
|
||||
export function reducedSpurtChance(witStat: number): number {
|
||||
// 1900 wit style S exceeds 100% chance if we don't explicitly cap.
|
||||
return Math.min(0.15 + 0.0005 * witStat, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the chance to enter downhill accel mode each second while running downhill.
|
||||
* @param witStat Final wit stat, including style aptitude modifier
|
||||
@@ -404,7 +899,7 @@ export function downhillAccelEnterChance(witStat: number): number {
|
||||
* @returns Probability each eligible tick to enter speed-up or overtake mode
|
||||
*/
|
||||
export function frontModeEnterChance(witStat: number): number {
|
||||
return 0.2 * math.log10(witStat) - 0.2;
|
||||
return 0.2 * math.log10(witStat * 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,5 +908,15 @@ export function frontModeEnterChance(witStat: number): number {
|
||||
* @returns Probability each eligible tick to enter pace-up mode
|
||||
*/
|
||||
export function paceUpEnterChance(witStat: number): number {
|
||||
return 0.15 * math.log10(witStat) - 0.15;
|
||||
return 0.15 * math.log10(witStat * 0.1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the chance for a horse to become rushed during a given race.
|
||||
* @param witStat Final wit stat, including style aptitude modifier
|
||||
* @returns Probability to become rushed during the race
|
||||
*/
|
||||
export function rushedChance(witStat: number): number {
|
||||
const r = 6.5 / Math.log10(0.1 * witStat + 1);
|
||||
return Math.ceil(r * r) / 100;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Instantaneous characteristics of a horse in motion.
|
||||
*/
|
||||
export interface Instant {
|
||||
/** Timestamp of the instant. */
|
||||
t: number;
|
||||
/** Forward position along the course. */
|
||||
x: number;
|
||||
/** Actual speed. */
|
||||
v: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward travel characteristics under the effects of acceleration.
|
||||
*/
|
||||
export interface Travel {
|
||||
start: Instant;
|
||||
end: Instant;
|
||||
/** Actual acceleration value applied between start and end. */
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance a horse according to its forward motion characteristics.
|
||||
* @param start Motion at the beginning of the segment
|
||||
* @param targetSpeed Target speed to accelerate to
|
||||
* @param totalAccel Total (positive) acceleration including skill bonuses
|
||||
* @param maxDur Max duration of the acceleration, or infinite if not given
|
||||
* @param maxDist Max distance of the acceleration, or infinite if not given
|
||||
* @param decel Negative acceleration value for cases where target speed is below start speed
|
||||
* @returns Travel segment after reaching the first of the given limits
|
||||
*/
|
||||
export function travelSegment(
|
||||
start: Instant,
|
||||
targetSpeed: number,
|
||||
totalAccel: number,
|
||||
maxDur?: number,
|
||||
maxDist?: number,
|
||||
decel?: number,
|
||||
): Travel {
|
||||
const a = start.v < targetSpeed ? totalAccel : (decel ?? -1);
|
||||
const dvt = (targetSpeed - start.v) / a;
|
||||
const dxt = maxDist != null ? (-start.v + Math.sqrt(start.v * start.v + 4 * a * maxDist)) / (2 * a) : dvt;
|
||||
const time = maxDur != null ? Math.min(dvt, dxt, maxDur) : Math.min(dvt, dxt);
|
||||
// To address numerical imprecision, if accel time comes from velocity or distance,
|
||||
// set the reached values directly.
|
||||
const v = time === dvt ? targetSpeed : start.v + time * a;
|
||||
const dist = maxDist != null && time === dxt ? maxDist : (start.v + 0.5 * a * time) * time;
|
||||
const end = { t: start.t + time, x: start.x + dist, v };
|
||||
return { start, end, a };
|
||||
}
|
||||
|
||||
/**
|
||||
* Acceleration bonus from skills.
|
||||
*/
|
||||
export interface Skill {
|
||||
/**
|
||||
* Acceleration modifier in m/s².
|
||||
*/
|
||||
accel: number;
|
||||
/**
|
||||
* Remaining actual duration in seconds.
|
||||
*/
|
||||
dur: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accelerate up to a new target speed while under the effects of acceleration skills.
|
||||
* @param startSpeed Speed at the start of the acceleration in m/s
|
||||
* @param targetSpeed Speed at which the acceleration ends in m/s
|
||||
* @param baseAccel Base acceleration value to which skills add, in m/s²
|
||||
* @param skills Skills that are active for the acceleration period
|
||||
* @returns Acceleration segments and remainders of skills upon reaching the target speed
|
||||
*/
|
||||
export function up(start: Instant, targetSpeed: number, baseAccel: number, skills: Skill[]) {
|
||||
const segments: Travel[] = [];
|
||||
const sk = skills.map((s) => ({ ...s })).filter((s) => s.dur > 0);
|
||||
while (start.v < targetSpeed) {
|
||||
const active = sk.filter((s) => s.dur > 0);
|
||||
if (active.length === 0) {
|
||||
segments.push(travelSegment(start, targetSpeed, baseAccel));
|
||||
break;
|
||||
}
|
||||
const boost = active.reduce((a, s) => a + s.accel, 0);
|
||||
const time = Math.min(...active.map((s) => s.dur));
|
||||
const seg = travelSegment(start, targetSpeed, baseAccel + boost, time);
|
||||
segments.push(seg);
|
||||
start = seg.end;
|
||||
for (const s of active) {
|
||||
s.dur -= seg.end.t - seg.start.t;
|
||||
if (s.dur < 1e-5) {
|
||||
s.dur = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { segments, skills: sk };
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Fundamental stats of umas.
|
||||
*/
|
||||
export enum Stat {
|
||||
speed,
|
||||
stamina,
|
||||
power,
|
||||
guts,
|
||||
wit,
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats as a list for easy iteration.
|
||||
*/
|
||||
export const LIST = [Stat.speed, Stat.stamina, Stat.power, Stat.guts, Stat.wit] as const;
|
||||
|
||||
declare const __stage: unique symbol;
|
||||
|
||||
export type Stage = 'raw' | 'base' | 'adjusted' | 'final' | 'skills';
|
||||
|
||||
export type StatValue<T extends Stage> = number & { [__stage]: T };
|
||||
|
||||
export interface Stats<T extends Stage> {
|
||||
speed: StatValue<T>;
|
||||
stamina: StatValue<T>;
|
||||
power: StatValue<T>;
|
||||
guts: StatValue<T>;
|
||||
wit: StatValue<T>;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
const WORD_BOUNDARY = " ,!?/-+();#○☆♡'=♪∀゚∴";
|
||||
|
||||
function score(s: string, tt: string): number {
|
||||
let k: number | undefined;
|
||||
let r = 0;
|
||||
let run = 0;
|
||||
for (const c of s) {
|
||||
const j = tt.indexOf(c, k);
|
||||
// If the character isn't in the string, there's a major penalty.
|
||||
if (j < 0) {
|
||||
// The penalty scales with run length, on the assumption that we're
|
||||
// typing something else.
|
||||
// Really this should scale with the longest current run among all
|
||||
// search terms, but that's infeasible to implement.
|
||||
r -= 6 + run * run;
|
||||
run = 0;
|
||||
continue;
|
||||
}
|
||||
run++;
|
||||
// Characters at word boundaries get extra score.
|
||||
if (j == 0 || WORD_BOUNDARY.includes(tt[j - 1])) {
|
||||
r += 2;
|
||||
}
|
||||
// As do characters that *are* word boundaries.
|
||||
if (WORD_BOUNDARY.includes(c)) {
|
||||
r += 2;
|
||||
}
|
||||
// And runs of matches scale with run length.
|
||||
if (j === k) {
|
||||
r += (run + 1) * (run + 1);
|
||||
} else {
|
||||
run = 0;
|
||||
}
|
||||
k = j + 1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy string search.
|
||||
* @param sub Substring to search for
|
||||
* @param terms Iterable of values to search among
|
||||
* @param map Mapping from term to string to search
|
||||
* @returns Matching terms in decreasing match quality order
|
||||
*/
|
||||
export function stringsearch<T>(sub: string, terms: Iterable<T>, map: (t: T) => string): T[] {
|
||||
const s = sub.toLocaleLowerCase();
|
||||
const scored: [T, number][] = [];
|
||||
for (const t of terms) {
|
||||
const sc = score(s, map(t).toLocaleLowerCase());
|
||||
if (sc >= 0) {
|
||||
scored.push([t, sc]);
|
||||
}
|
||||
}
|
||||
return scored.sort(([, a], [, b]) => b - a).map(([t]) => t);
|
||||
}
|
||||
@@ -1,13 +1,31 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.png';
|
||||
import * as all from '$lib/all';
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
{#if title != null}
|
||||
<title>Zenno Rob Roy – {title}</title>
|
||||
{:else}
|
||||
<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} />
|
||||
</svelte:head>
|
||||
|
||||
@@ -23,9 +41,9 @@
|
||||
<a href={resolve('/doc')} class="mx-8 my-1 inline-block">Documents</a>
|
||||
</span>
|
||||
</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()}
|
||||
</div>
|
||||
</main>
|
||||
<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"
|
||||
>
|
||||
@@ -35,6 +53,8 @@
|
||||
All game data is auto-generated from the
|
||||
<a href="https://git.sunturtle.xyz/zephyr/horse/src/branch/main/doc/README.md" target="_blank" rel="noopener noreferrer"
|
||||
>game's local database</a
|
||||
>.
|
||||
>.<br />
|
||||
If you find this site helpful, please consider
|
||||
<a href="https://ko-fi.com/zephyrtronium" target="_blank" rel="noopener noreferrer">supporting my Ko-fi</a>.
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import * as all from '$lib/all';
|
||||
import IndexSection from '$lib/IndexSection.svelte';
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<a href="https://discord.com/oauth2/authorize?client_id=1461931240264568994" target="_blank" rel="noopener noreferrer"
|
||||
>Discord Bot</a
|
||||
@@ -33,12 +20,5 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="my-4 text-4xl">Documents</h2>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li>
|
||||
<a href={resolve('/doc/frbm')}>Front Runner Black Magic</a> — Detailed analysis of how front runners interact with race mechanics.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
<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>
|
||||
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — Character Tools</h1>
|
||||
<p>Tools related to understanding Umamusume characters.</p>
|
||||
|
||||
<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>
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — {cur.name}</h1>
|
||||
<p>{cur.description}</p>
|
||||
<IndexSection pages={all.PAGES.chara} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import CharaPick from '$lib/CharaPick.svelte';
|
||||
import { AFFINITY_RELATION_DESCRIPTIONS, affinityDetail, type AffinityDetail } from '$lib/data/affinity';
|
||||
import { character, charaNames, type Character } from '$lib/data/character';
|
||||
import { character, type Character } from '$lib/data/character';
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
@@ -12,21 +12,21 @@
|
||||
affs = affinities;
|
||||
characters = charas.filter((c) => affs.some((d) => d.chara_id === c.chara_id));
|
||||
});
|
||||
const names = $derived(charaNames(characters));
|
||||
|
||||
let charA: number = $state(1001);
|
||||
let charB: number = $state(0);
|
||||
let charC: number = $state(0);
|
||||
const nameA = $derived(names.get(charA)?.en ?? `Character ${charA}`);
|
||||
const nameB = $derived(names.get(charB)?.en ?? `Character ${charB}`);
|
||||
const nameC = $derived(names.get(charC)?.en ?? `Character ${charC}`);
|
||||
let charA: Character | undefined = $state();
|
||||
let charB: Character | undefined = $state();
|
||||
let charC: Character | undefined = $state();
|
||||
|
||||
const charaListA = $derived(characters.filter((c) => c.chara_id != charB?.chara_id && c.chara_id != charC?.chara_id));
|
||||
const charaListB = $derived(characters.filter((c) => c.chara_id != charA?.chara_id && c.chara_id != charC?.chara_id));
|
||||
const charaListC = $derived(characters.filter((c) => c.chara_id != charA?.chara_id && c.chara_id != charB?.chara_id));
|
||||
|
||||
function groupsFor(affs: AffinityDetail[], chara: number): AffinityDetail[] {
|
||||
return chara !== 0 ? affs.filter((d) => d.chara_id === chara) : [];
|
||||
}
|
||||
const groupsA = $derived(groupsFor(affs, charA));
|
||||
const groupsB = $derived(groupsFor(affs, charB));
|
||||
const groupsC = $derived(groupsFor(affs, charC));
|
||||
const groupsA = $derived(groupsFor(affs, charA?.chara_id ?? 0));
|
||||
const groupsB = $derived(groupsFor(affs, charB?.chara_id ?? 0));
|
||||
const groupsC = $derived(groupsFor(affs, charC?.chara_id ?? 0));
|
||||
const allGroups = $derived.by(() => {
|
||||
const seen = new SvelteSet<number>();
|
||||
const r = [];
|
||||
@@ -41,8 +41,7 @@
|
||||
}
|
||||
return r;
|
||||
});
|
||||
const selCount = $derived(1 + (charB === 0 ? 0 : 1) + (charC === 0 ? 0 : 1));
|
||||
const duplicate = $derived(charA === charB || charA === charC || (charB !== 0 && charB === charC));
|
||||
const selCount = $derived(1 + (charB == null ? 0 : 1) + (charC == null ? 0 : 1));
|
||||
const infos = $derived(
|
||||
allGroups
|
||||
.map((d) => ({
|
||||
@@ -65,21 +64,19 @@
|
||||
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-2xl md:flex-row">
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<label for="charA" class="hidden md:inline">Character 1</label>
|
||||
<CharaPick id="charA" {characters} class="w-full" bind:value={charA} required />
|
||||
<CharaPick id="charA" characters={charaListA} class="w-full" bind:value={charA} required />
|
||||
</div>
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<label for="charB" class="hidden md:inline">Character 2</label>
|
||||
<CharaPick id="charB" {characters} class="w-full" bind:value={charB} />
|
||||
<CharaPick id="charB" characters={charaListB} class="w-full" bind:value={charB} />
|
||||
</div>
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<label for="charC" class="hidden md:inline">Character 3</label>
|
||||
<CharaPick id="charC" {characters} class="w-full" bind:value={charC} />
|
||||
<CharaPick id="charC" characters={charaListC} class="w-full" bind:value={charC} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:boundary>
|
||||
{#if duplicate}
|
||||
<span class="mt-8 block w-full text-center text-lg">Duplicate characters always have zero affinity.</span>
|
||||
{:else if selCount > 1}
|
||||
{#if selCount > 1}
|
||||
<span class="mt-8 block w-full text-center text-lg">Total {selCount === 2 ? 'pair' : 'trio'} affinity: {total}</span>
|
||||
{/if}
|
||||
<table class="mx-auto mt-8 table-fixed">
|
||||
@@ -88,12 +85,12 @@
|
||||
<th class="w-32 px-2" scope="col">Group ID</th>
|
||||
<th class="w-96 px-2" scope="col">Description</th>
|
||||
{#if selCount > 1}
|
||||
<th class="w-56 px-2" scope="col">{nameA}</th>
|
||||
{#if charB !== 0}
|
||||
<th class="w-56 px-2" scope="col">{nameB}</th>
|
||||
<th class="w-56 px-2" scope="col">{charA!.name}</th>
|
||||
{#if charB != null}
|
||||
<th class="w-56 px-2" scope="col">{charB.name}</th>
|
||||
{/if}
|
||||
{#if charC !== 0}
|
||||
<th class="w-56 px-2" scope="col">{nameC}</th>
|
||||
{#if charC != null}
|
||||
<th class="w-56 px-2" scope="col">{charC.name}</th>
|
||||
{/if}
|
||||
{/if}
|
||||
<th class="w-32 px-2" scope="col">Affinity</th>
|
||||
@@ -110,14 +107,14 @@
|
||||
{/if}
|
||||
{#if selCount > 1}
|
||||
<td class="px-4 text-center">{a ? '✓' : ''}</td>
|
||||
{#if charB !== 0}
|
||||
{#if charB != null}
|
||||
<td class="px-4 text-center">{b ? '✓' : ''}</td>
|
||||
{/if}
|
||||
{#if charC !== 0}
|
||||
{#if charC != null}
|
||||
<td class="px-4 text-center">{c ? '✓' : ''}</td>
|
||||
{/if}
|
||||
{/if}
|
||||
<td class="px-4 text-center">{!duplicate && selCount > 1 && counted === selCount ? '+' : ''}{affinity}</td>
|
||||
<td class="px-4 text-center">{selCount > 1 && counted === selCount ? '+' : ''}{affinity}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
@@ -8,19 +8,18 @@
|
||||
const convoMap = $derived(byChara(conversations));
|
||||
const popular = $derived(groupPopulars(conversations));
|
||||
let characters: Character[] = $state([]);
|
||||
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
|
||||
const names = $derived(charaNames(characters ?? []));
|
||||
const minSuggest = 8;
|
||||
|
||||
onMount(async () => {
|
||||
const [convos, charas] = await Promise.all([conversation(), character()]);
|
||||
conversations = convos;
|
||||
characters = charas.filter((c) => convoMap.has(c.chara_id));
|
||||
[conversations, characters] = await Promise.all([conversation(), character()]);
|
||||
});
|
||||
|
||||
let charaID = $state(1001);
|
||||
let chara: Character | undefined = $state();
|
||||
let convo = $state(1);
|
||||
|
||||
const options = $derived(convoMap.get(charaID) ?? []);
|
||||
const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
|
||||
const cur = $derived(options.find((c) => c.number === convo));
|
||||
const cur1Name = $derived(cur?.chara_1 && names.get(cur.chara_1)?.en);
|
||||
const cur2Name = $derived(cur?.chara_2 && names.get(cur.chara_2)?.en);
|
||||
@@ -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="m-4 flex-1 md:mt-3">
|
||||
<label for="chara" class="hidden md:inline">Character</label>
|
||||
<CharaPick id="chara" {characters} class="w-full" bind:value={charaID} required />
|
||||
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
|
||||
</div>
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<label for="convo" class="hidden md:inline">Conversation</label>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<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>
|
||||
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — Documents</h1>
|
||||
<p>Articles about Umamusume, including editorials and research.</p>
|
||||
|
||||
<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>
|
||||
</ul>
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — {cur.name}</h1>
|
||||
<p>{cur.description}</p>
|
||||
<IndexSection pages={all.PAGES.doc} />
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
h?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
id: string;
|
||||
children: Snippet;
|
||||
ref?: string;
|
||||
top?: string;
|
||||
toc?: boolean;
|
||||
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 href = $derived(`#${id}`);
|
||||
@@ -39,9 +40,12 @@
|
||||
<svelte:element this={tag} {id} class={['relative w-full', className]}>
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||
<a {href}><span class="hidden md:inline">{sign}</span> {@render children()}</a>
|
||||
{#if h >= 2}
|
||||
<a href={top} class="absolute right-0 bottom-0 text-end align-bottom text-sm whitespace-nowrap transition-all hover:bottom-px"
|
||||
>Top ▲</a
|
||||
>
|
||||
<div class="absolute right-0 bottom-0 inline-block text-end align-bottom text-sm whitespace-nowrap">
|
||||
{#if ref != null}
|
||||
<a href={ref} target="_blank" rel="noopener noreferrer">Ref ↗</a>
|
||||
{/if}
|
||||
{#if h >= 2}
|
||||
<a href={top} class="ml-4">Top ▲</a>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:element>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import type { ComputedAreas, ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||
import {
|
||||
AptitudeLevel,
|
||||
conservePowerAccel,
|
||||
distance,
|
||||
downhillAccelEnterChance,
|
||||
frontModeEnterChance,
|
||||
HORSE_LENGTH,
|
||||
@@ -79,16 +81,21 @@
|
||||
topRunner: allSkills.get(201271),
|
||||
leadersPride: allSkills.get(201272),
|
||||
moxie: allSkills.get(201282),
|
||||
shrewd: allSkills.get(201332),
|
||||
eyes: allSkills.get(201441),
|
||||
savvy: allSkills.get(201522),
|
||||
stan: allSkills.get(201591),
|
||||
lucky7: allSkills.get(201562),
|
||||
gw: allSkills.get(201601),
|
||||
thh: allSkills.get(201611),
|
||||
loneWolf: allSkills.get(201641),
|
||||
slipstream: allSkills.get(201651),
|
||||
pto: allSkills.get(201661),
|
||||
headon: allSkills.get(201902),
|
||||
risky: allSkills.get(202032),
|
||||
runaway: allSkills.get(202051),
|
||||
fr: allSkills.get(202462),
|
||||
burningPOW: allSkills.get(210031),
|
||||
burningWIT: allSkills.get(210051),
|
||||
ignitedWIT: allSkills.get(210052),
|
||||
radiant: allSkills.get(210061),
|
||||
@@ -172,7 +179,7 @@
|
||||
{ label: 'Style S', y: (x) => downhillAccelEnterChance(x * 1.1) * 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([
|
||||
{
|
||||
label: 'Early Race',
|
||||
@@ -207,6 +214,12 @@
|
||||
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>
|
||||
|
||||
<Article>
|
||||
@@ -231,6 +244,28 @@
|
||||
</p>
|
||||
{/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>
|
||||
<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
|
||||
@@ -255,10 +290,16 @@
|
||||
>. 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
|
||||
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
|
||||
you to the corresponding section of the mechanics doc.
|
||||
</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>
|
||||
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
|
||||
@@ -285,10 +326,7 @@
|
||||
|
||||
<Sec h={3} id="runaway">Runaway</Sec>
|
||||
<p>
|
||||
The skill <Skill skill={skills.runaway} /> converts front runners into the <i>Great Escape</i> running style. However, no
|
||||
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.")
|
||||
The skill <Skill skill={skills.runaway} /> converts front runners into the Runaway running style.
|
||||
</p>
|
||||
<p>
|
||||
Runaways are still front runners for all purposes. The difference is just different numbers for things like base speed and
|
||||
@@ -296,7 +334,12 @@
|
||||
runners also apply to runaways.
|
||||
</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>
|
||||
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
|
||||
@@ -309,8 +352,8 @@
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
will likely lose only a couple races within the difference throughout a CM event.
|
||||
aren't what make front runners win most of the time. Capped speed with distance S is ideal, but being a hundred speed short
|
||||
with distance A will likely lose only a couple races within the difference throughout a CM event.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="win-cons">Win Conditions</Sec>
|
||||
@@ -354,16 +397,20 @@
|
||||
mid race.
|
||||
</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>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
as "solo fronts are heavily penalized." (With the 1.5 anni balance patch, we expect that solo fronts will be given more
|
||||
forgiving running mode conditions.) They also require passing a wit check, with the same chance for both modes.
|
||||
not-first). Entering these modes requires meeting certain conditions relating to positioning. They also require passing a wit
|
||||
check, with the same chance for both modes.
|
||||
</p>
|
||||
<div class="mx-auto h-60 w-full max-w-3xl md:h-96">
|
||||
<StatChart
|
||||
@@ -376,24 +423,34 @@
|
||||
/>
|
||||
</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>
|
||||
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.
|
||||
</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
|
||||
> 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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</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>
|
||||
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
|
||||
@@ -401,17 +458,18 @@
|
||||
</p>
|
||||
<p>
|
||||
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>
|
||||
<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={ssDurSeries} yLabel="Duration (s)" range={[0, 12]} />
|
||||
<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, 14]} />
|
||||
</div>
|
||||
<p>
|
||||
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.)
|
||||
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>
|
||||
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than
|
||||
@@ -419,6 +477,52 @@
|
||||
multiple gold speed skills in total distance gained. See the <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for
|
||||
precise analysis.
|
||||
</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>
|
||||
<p>
|
||||
@@ -433,13 +537,14 @@
|
||||
y={laneComboSeries}
|
||||
yLabel="Speed Boost"
|
||||
yRule={lcYRule}
|
||||
range={[0.2, 0.5]}
|
||||
range={[0.2, 0.6]}
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Front runners have access to the skill <Skill skill={skills.dd} />, which forces a horse who uses it to move outward to a
|
||||
specific distance from the rail. DD almost always ends shortly before the horse has finished accelerating to early race speed,
|
||||
so it does not convert the move lane speed modifier into distance.
|
||||
specific distance from the rail. DD almost always activates while the horse is still accelerating to early race speed, so it
|
||||
does not convert much of the move lane speed modifier into distance (although still on the order of a gold speed skill worth
|
||||
of gain on most medium races).
|
||||
</p>
|
||||
<p>
|
||||
We get advantage from move lane speed modifier by following DD with <Skill skill={skills.pp} /> or <Skill
|
||||
@@ -473,12 +578,16 @@
|
||||
<p>
|
||||
The <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> has an approximation of lane combo's benefit. A more
|
||||
precise lane combo simulator exists at
|
||||
<a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">危険回避シミュ</a>, but I am not sufficiently
|
||||
confident in my Japanese to try to guide readers through it.
|
||||
<!-- TODO(zeph): i could totally annotate a picture though, or find someone else's explanation -->
|
||||
<a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">危険回避シミュ</a>. See
|
||||
<a href={resolve('/doc/lanecalc')}>my guide</a> for English instructions on how to use it.
|
||||
</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>
|
||||
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
|
||||
@@ -503,7 +612,12 @@
|
||||
power on the Arima Kinen mid-race hills.
|
||||
</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>
|
||||
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
|
||||
@@ -520,7 +634,7 @@
|
||||
stat={Stat.Wit}
|
||||
y={downhillSeries}
|
||||
yLabel="Entry Chance (% each second)"
|
||||
range={[0, 60]}
|
||||
range={[0, 75]}
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
@@ -529,7 +643,12 @@
|
||||
Conversely, the HP savings on long downhills can be enough to drop a recovery skill or two on some tracks.
|
||||
</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>
|
||||
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.)
|
||||
@@ -540,7 +659,7 @@
|
||||
stat={Stat.Wit}
|
||||
yArea={secSpeedSeries}
|
||||
yLabel="Section Speed (m/s)"
|
||||
range={[17.5, 22.5]}
|
||||
range={[17, 23]}
|
||||
plotOptions={{ color: { legend: true } }}
|
||||
/>
|
||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||
@@ -573,7 +692,12 @@
|
||||
wit front A horse will pass in {secSpeedPassTime} seconds on average at mid race speeds.
|
||||
</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>
|
||||
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
|
||||
@@ -664,7 +788,7 @@
|
||||
<Sec h={3} id="spurt-skills">Spurt Skills</Sec>
|
||||
<p>
|
||||
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 name ends with ! -->
|
||||
These are good inherits for those who don't have easy access to <Skill skill={skills.cacaoInherit} mention /> and <Skill
|
||||
@@ -698,8 +822,8 @@
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
almost never going to reach front runners, especially the front who got Angling.
|
||||
>, which is essentially always a range of 20m, or 8 lengths. Debuffers are typically built with very little stamina and power,
|
||||
so Eyes is almost never going to reach front runners, especially the front who got Angling.
|
||||
</p>
|
||||
<p>
|
||||
The other category of skills to think about is other horses' uniques.
|
||||
@@ -743,37 +867,72 @@
|
||||
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.
|
||||
</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>
|
||||
<p>
|
||||
While this is a front runner guide, it's still valuable to think about the effects that zero front races have on other styles;
|
||||
which is to say, <i>pace promotion</i>.
|
||||
</p>
|
||||
<p>
|
||||
Whichever pace chaser (or late surger if there are also no paces, or end closer if it's all ends) happens to get in front of
|
||||
the pack by the first few dozen meters or so becomes the pace setter for the race. She runs as if she were a front runner with
|
||||
no position keep modes, but she retains the pace chaser coefficients for target speed and acceleration. For this reason, pace
|
||||
chasers tend to see their highest win rates in zero front races.
|
||||
</p>
|
||||
<p>
|
||||
If you feel a given CM will have a low population of front runners, you can build specifically <i>for</i> pace promo. Burgh on
|
||||
the GameTora Discord server
|
||||
<a
|
||||
href="https://discord.com/channels/879780247137050684/879780820116721665/1472986531529752637"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">ran extensive tests</a
|
||||
>
|
||||
and found that <Skill skill={skills.conc} mention /> and <Skill skill={skills.stan} mention /> were the only skills to produce a
|
||||
statistically significant difference in pace promotion ability. More precisely, achieving pace promo is decided by start delay,
|
||||
any target speed skill activating, and <a href="#section-speed">section speed</a> for the first section (i.e. wit stat), in
|
||||
decreasing order of importance. (Notably, contrary to common advice, <Skill skill={skills.gw} mention /> and <Skill
|
||||
skill={skills.shrewd}
|
||||
mention
|
||||
/> are <i>not</i> effective for pace promo.)
|
||||
</p>
|
||||
<p>
|
||||
On the other hand, if you want to run an end closer or late surger as your ace, but front runners may be rare and <s
|
||||
>Christmas Oguri Cap</s
|
||||
> a pace chaser is meta for the race, preventing pace promotion could be a reason to run a solo front build instead.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="solo-front">Solo Front</Sec>
|
||||
<p>
|
||||
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.
|
||||
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
|
||||
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>
|
||||
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 solo front is
|
||||
truly solo.
|
||||
style is viable, which is always now that <Skill skill={skills.headon} mention /> is buffed, you'll have matches where your team's
|
||||
front runner is solo. The best choice for how to design a solo front depends on the rest of your team composition, because ultimately
|
||||
the choice you are faced with is whether to use a runaway blocker.
|
||||
</p>
|
||||
<p>
|
||||
For that reason, it is my belief that solo fronts should not be built as aces; they're better left to a support role. And,
|
||||
generally, the best support front runners are runaways, to block other teams' front runners from Angling and to keep pace up
|
||||
to mitigate PDM on your other horses.
|
||||
The ideal runaway blocker has capped power, capped wit, and 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 as possible. Her speed stat doesn't particularly matter, because her
|
||||
job is to block, not to win.
|
||||
</p>
|
||||
<p>
|
||||
I say that they shouldn't be built as aces, but right now, building a runaway that can block MANT front runners requires all
|
||||
the early and mid race skills you can manage.
|
||||
<a href="#gate-skills">Gate skills</a> are especially important, because runaways have a tremendously higher early race target
|
||||
speed. If there is a real difference between a correctly built runaway blocker and a runaway ace, it's that <Skill
|
||||
skill={skills.cacaoInherit}
|
||||
mention
|
||||
/>
|
||||
and <Skill skill={skills.kfcInherit} mention /> are arguably more appropriate inherits than Angling, but the argument isn't strong
|
||||
if you're still able to hit a good stat line on your runaway.
|
||||
Because runaways get so far ahead during the first half of the race, they essentially disable <a href="#pdm">pace-down mode</a
|
||||
> for everyone. That especially benefits pace chasers, who tend to lose access to pace-up mode with slower pacesetters, and it disfavors
|
||||
late surgers and end closers who normally don't keep up with their pace zones either way but now have to fight better paces.
|
||||
</p>
|
||||
<p>
|
||||
Runaway blockers come with some caveats. Because they're so far ahead during the first half of the race, they essentially
|
||||
disable <a href="#pdm">pace-down mode</a> for everyone. That especially benefits pace chasers, who have more forgiving PDM limits
|
||||
in the first place, and it disfavors late surgers and end closers who are less equipped to move forward during position keep.
|
||||
So, if you want to run a composition like one front and two end, it might serve you to choose against building a runaway
|
||||
blocker. This kind of composition probably should build the front runner for the purpose of winning against other front
|
||||
runners, and simply accept that solo front matches will be up to the other two instead. That leaves little room for a
|
||||
dedicated support like a debuffer.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="double-front">Double Front</Sec>
|
||||
@@ -784,12 +943,12 @@
|
||||
<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
|
||||
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>
|
||||
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,
|
||||
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>
|
||||
Since your two front runners are both meant to be winners, making your third horse a support type is likely to give the best
|
||||
@@ -808,14 +967,8 @@
|
||||
Building for second place to bunny with NN does not mean manipulating stats and skills to force one of the front runners into
|
||||
second. Both fronts need to be strong enough to be adjacent at late race start, and you have to beat other people's front
|
||||
runners, too. (Especially since VC and <Skill skill={skills.encroaching} mention /> are currently the only consistent accels for
|
||||
long distance.) So, double front for long is ultimately about the same as double front for other distances, just with different
|
||||
goals when choosing the legacy.
|
||||
</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.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
If you're feeling adventurous, runaway is another consideration for double (and triple) front on long distances. Your ace
|
||||
@@ -839,17 +992,17 @@
|
||||
One type of support that all front runners do automatically is helping to kill position-based pace chaser skills. Such skills
|
||||
have conditions that translate into needing to be in 3rd or 4th, or sometimes 2nd through 4th, to activate. When you're
|
||||
bringing three front runners, if anyone else brings a single other one, they're going to occupy 2-4 naturally. If there happen
|
||||
to be three others, then even late/end win cons like <Skill skill={skills.beyondInherit} mention /> and <Skill
|
||||
to be three others between front and pace, then even late/end win cons like <Skill skill={skills.beyondInherit} mention /> and <Skill
|
||||
skill={skills.pumpInherit}
|
||||
mention
|
||||
/> are dead.
|
||||
</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
|
||||
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
|
||||
terribly consistent because they're readily equipped to get into first just as easily as third, but then they just get Angling
|
||||
instead and follow the normal front runner game plan. If you see people talking about a pace chaser being especially strong
|
||||
for a given CM, consider running them as a front runner in a triple front build instead.
|
||||
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
|
||||
is not terribly consistent because they're readily equipped to get into first just as easily as third, but then they just get
|
||||
Angling instead and follow the normal front runner game plan. If you see people talking about a given pace chaser being
|
||||
especially strong for a CM, consider running them as a front runner in a triple front build instead.
|
||||
</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
|
||||
@@ -863,34 +1016,46 @@
|
||||
showing. In addition to the normal front win cons, it's potentially strong to take unconditional gambles like <Skill
|
||||
skill={skills.cornerConn}
|
||||
mention
|
||||
/> and <Skill skill={skills.radiant} mention />.
|
||||
/>, <Skill skill={skills.burningPOW} mention />, and <Skill skill={skills.radiant} mention />.
|
||||
</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
|
||||
sprints. However, spot struggle is the only mechanic in the game that scales superlinearly with stats, so even the difference
|
||||
between 1200 and 1000 guts can matter. See the <a href={resolve('/race/mspeed')}>mechanical speed calculator</a> for details.
|
||||
Now that <a href="#full-charge">conserving power</a> is a game mechanic, SS supports have the problem that spot struggle risks
|
||||
taking away full charge, which means losing a strong accel. This can be alleviated to some degree by managing
|
||||
<a href="#lane-combo">Dodging Danger</a> to keep your ace out of the spot struggle – either the ace has lane combo and the
|
||||
other two don't, or the ace doesn't have it and the other two do.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="chariot">Chariot</Sec>
|
||||
<div class="hidden" id="chariot"></div>
|
||||
<Sec h={3} id="rabbit">Rabbit</Sec>
|
||||
<p>
|
||||
A front runner build that I have seen but not attempted is the chariot: a runaway with a strong mid race but insufficient HP
|
||||
A front runner build that I have seen but not attempted is the rabbit: a runaway with a strong early race but insufficient HP
|
||||
to spurt stays in front of one or two other front runners who have <Skill skill={skills.nsm} mention /><!-- ends with ! -->
|
||||
</p>
|
||||
<p>
|
||||
I saw a team using a chariot in CM13 round 2 day 2; that team had an overall eleven wins at the time. Maybe their horses just
|
||||
I saw a team using a rabbit in CM13 round 2 day 2; that team had an overall eleven wins at the time. Maybe their horses just
|
||||
weren't strong enough. I'm not sure. My opinion is that this build gives up the consistency of front runners to pursue a game
|
||||
plan that is hardly stronger when it does work.
|
||||
</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
|
||||
viable now.
|
||||
About two weeks ahead of CM15, Hakuraku's replay data is showing teams using a rabbit build, or even double runaways, with
|
||||
high win rates. I've watched a few dozen of those races and have found that the runaways are not functionally doing anything
|
||||
specific to runaways: they're being built without <Skill skill={skills.gw} mention /> and <Skill
|
||||
skill={skills.earlyLead}
|
||||
mention
|
||||
/>, so the normal fronts (typically UG1 or better) pass them during early race.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
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>
|
||||
|
||||
<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>
|
||||
Currently, this chapter is about MANT (a.k.a. Trackblazer). Unity Cup is still useful – obviously for runaways, but also
|
||||
for spinning up an <Skill skill={skills.ignitedWIT} mention /> legacy.
|
||||
Currently, this chapter is about MANT (a.k.a. Trackblazer). Current data for CM16 following the URA and UC reworks suggests
|
||||
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>
|
||||
|
||||
<Sec h={3} id="support-cards">Support Cards</Sec>
|
||||
@@ -1059,7 +1224,7 @@
|
||||
<Sec h={2} id="umas">Front Runners</Sec>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li>
|
||||
Valentine's Mihono Bourbon (VBourbon) is the easiest font runner to train because she has <Skill
|
||||
Valentine's Mihono Bourbon (VBourbon or Chocobon) is the easiest font runner to train because she has <Skill
|
||||
skill={skills.gw}
|
||||
mention
|
||||
/> built in, whereas most others have to get it from either inheritance or cards that aren't terribly strong. She is also the
|
||||
@@ -1118,7 +1283,8 @@
|
||||
<Sec h={3} id="taiki-shuttle">Taiki Shuttle & Curren Chan</Sec>
|
||||
<p>
|
||||
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>
|
||||
Taiki Shuttle and original Curren Chan are notable for having C and B front, respectively, which is quite reasonable to fix.
|
||||
@@ -1144,14 +1310,15 @@
|
||||
|
||||
<Sec h={3} id="sparks">Important Sparks</Sec>
|
||||
<p>
|
||||
Front runners are uniquely capable of making good use of all stat (blue) sparks, since guts is unusually important for them.
|
||||
Power sparks are typically still best, and stamina sparks can help to forgo a stamina card or two for 2400m+ builds. Fronts
|
||||
have a disproportionate representation of horses with high speed talent, which makes speed sparks a bit less valuable.
|
||||
Front runners are uniquely capable of making good use of wit sparks, since wit governs early- and mid-race speed, which is
|
||||
unusually important for them. Power sparks are typically still best, and stamina sparks can help to forgo a stamina card or
|
||||
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>
|
||||
Pink sparks are more interesting, because – at least until other styles catch up on accels – they're less reliant
|
||||
on distance S to win. Building a high affinity legacy tree requires keeping some mile and long sparks on hand. Front sparks
|
||||
are better than other style sparks, especially if you do <a href="#triple-front">triple front</a> CM builds using
|
||||
Pink sparks are more interesting, because – at least until other styles finish catching up on accels – they're
|
||||
less reliant on distance S to win. Building a high affinity legacy tree requires keeping some mile and long sparks on hand.
|
||||
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>.
|
||||
</p>
|
||||
<p>Front runners are more reliant than other styles on getting certain skills from inspiration:</p>
|
||||
@@ -1169,7 +1336,7 @@
|
||||
<li>
|
||||
<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
|
||||
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>
|
||||
Almost all green skills are valuable to inherit at one point or another as GW triggers.
|
||||
@@ -1184,12 +1351,17 @@
|
||||
</li>
|
||||
<li>
|
||||
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={skills.cornerAdept}
|
||||
/> are also strong, but available as hints or chain skills from some usable-to-good cards.
|
||||
</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>
|
||||
|
||||
<Sec h={3} id="parents">Parents</Sec>
|
||||
@@ -1218,7 +1390,8 @@
|
||||
</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
|
||||
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>
|
||||
TM Opera O is technically a pace chaser parent, but she still has good compatibility with most front runners, and her unique
|
||||
@@ -1226,100 +1399,77 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Sec h={2} id="stats">Stat Reference</Sec>
|
||||
<p>
|
||||
This chapter is an extremely brief reference of what front runner race mechanics each stat and aptitude applies to and how
|
||||
they scale.
|
||||
</p>
|
||||
<p>There are four types of each stat.</p>
|
||||
<ol class="mb-4 list-decimal pl-4">
|
||||
<li>
|
||||
<i>Raw stat</i> is the number displayed in the horse's info. Once stat caps are raised, any portion of raw stats above 1200 is
|
||||
halved.
|
||||
</li>
|
||||
<li><i>Base stat</i> is raw stat modified by mood.</li>
|
||||
<li>
|
||||
<i>Adjusted stat</i> is base stat modified by surface conditions (for speed and power), track stat thresholds (for speed), and
|
||||
style proficiency (for wit).
|
||||
</li>
|
||||
<li><i>Final stat</i> or just <i>stat</i> is adjusted stat plus the effects of green skills.</li>
|
||||
</ol>
|
||||
<p>
|
||||
For example, a 1200 wit style S horse in great mood with the corresponding savvy skill at level 1 has 1200 raw wit, 1248 base
|
||||
wit, 1372 adjusted wit, and 1412 (final) wit.
|
||||
</p>
|
||||
<p>
|
||||
<i>Linear scaling</i> means that a given increase to a stat produces the same change in the mechanical value regardless of the
|
||||
stat value.
|
||||
<i>Root scaling</i> means that the mechanical value scales with the square root, 0.6 power, or 0.7 power of the stat, which
|
||||
means the increase becomes smaller as the stat grows.
|
||||
<i>Log scaling</i> means it scales with the logarithm of the stat; this is the fastest diminishing scaling type.
|
||||
<i>Log-linear scaling</i> means it scales with the stat value multiplied by its logarithm.
|
||||
<i>Negative scaling</i> of any type means that the mechanic instead decreases as the stat increases.
|
||||
<i>Inverse scaling</i> means it scales with the multiplicative inverse of the stat, i.e. <span class="font-mono">1/x</span>.
|
||||
Inverse scaling is negative scaling by definition, which means that negative inverse scaling (e.g. skill activation chance)
|
||||
flips back to positive.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="stats-speed">Speed</Sec>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li>Spurt speed, root scaling</li>
|
||||
<li>Late race speed when not spurting, root scaling</li>
|
||||
</ul>
|
||||
|
||||
<Sec h={3} id="stats-stamina">Stamina</Sec>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li>HP, linear scaling</li>
|
||||
</ul>
|
||||
|
||||
<Sec h={3} id="stats-power">Power</Sec>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li>Move lane modifier (<a href="#lane-combo">lane combo</a>), root scaling</li>
|
||||
<li><a href="#uphills">Uphill target speed loss</a>, negative inverse scaling</li>
|
||||
<li>Acceleration, root scaling</li>
|
||||
<li>Lane change target speed (i.e. the horizontal rather than forward speed of changing lanes), linear</li>
|
||||
</ul>
|
||||
|
||||
<Sec h={3} id="stats-guts">Guts</Sec>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li>Spurt speed, root scaling (roughly 5% of the effect of the speed stat)</li>
|
||||
<li>Minimum speed, root scaling</li>
|
||||
<li>HP consumption multiplier during last spurt, inverse root scaling</li>
|
||||
<li>Target speed gain from spot struggle, root scaling</li>
|
||||
<li>Duration of spot struggle, root scaling</li>
|
||||
<li>Target speed gain from dueling, root scaling</li>
|
||||
<li>Acceleration gain from dueling, root scaling</li>
|
||||
</ul>
|
||||
|
||||
<Sec h={3} id="stats-wit">Wit</Sec>
|
||||
<ul class="mb-4 list-disc pl-4">
|
||||
<li><a href="#section-speed">Section speed</a>, log-linear scaling</li>
|
||||
<li>Chance to enter <a href="#downhills">downhill accel mode</a>, linear scaling</li>
|
||||
<li>Chance to accept a spurt speed and delay when not full spurting, linear scaling</li>
|
||||
<li>Chance to activate skills, negative inverse scaling, base stat</li>
|
||||
<li>Chance to enter <a href="#front-modes">speed-up and overtake modes</a> during position keep, log scaling</li>
|
||||
<li>
|
||||
For non-fronts, chance to enter pace-up mode, log scaling; but no effect on <a href="#pdm">pace-down mode</a>, which is
|
||||
purely controlled by distance to the pacemaker
|
||||
</li>
|
||||
<li>Chance to become rushed, inverse log-squared scaling</li>
|
||||
</ul>
|
||||
|
||||
<Sec h={3} id="stats-surface">Surface (Turf/Dirt) Aptitude</Sec>
|
||||
<p>Surface aptitude is a multiplier on acceleration. (Not the power stat, for the purposes of other mechanics using power.)</p>
|
||||
|
||||
<Sec h={3} id="stats-distance">Distance Aptitude</Sec>
|
||||
<p>Distance aptitude is a multiplier on spurt speed. At distance E and below, it is also a multiplier on acceleration.</p>
|
||||
|
||||
<Sec h={3} id="stats-style">Style Aptitude</Sec>
|
||||
<p>
|
||||
Style aptitude directly multiplies wit in the computation of the wit stat, which means it affects everything in <a
|
||||
href="#stats-wit">the wit section</a
|
||||
> except for mechanics that use base wit.
|
||||
</p>
|
||||
{#each ['stats', 'stats-speed', 'stats-stamina', 'stats-power', 'stats-guts', 'stats-wit', 'stats-surface', 'stats-style'] as id (id)}
|
||||
<h2 class="hidden target:block target:py-32" {id}>
|
||||
Stat Reference: This chapter moved to <a href={resolve('/doc/race')}>its own article</a>.
|
||||
</h2>
|
||||
{/each}
|
||||
|
||||
<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>
|
||||
<p>
|
||||
Kitasan's unique ends about a sixth of a second before late race starts, which means massive carryover potential (although
|
||||
being just a 0.25 skill is sad). I see on Hakuraku that runaways are unusually prevalent, and as much as I think it's a bad
|
||||
build, I kind of want to try rabbiting this time.
|
||||
</p>
|
||||
<ol class="mb-4 list-decimal pl-4">
|
||||
<li>
|
||||
Suzuka. I'll try building her as a runaway with TTL, GW, Conc, lane combo, and no other skills except debuffs, and see
|
||||
whether I can make two aces that can consistently pass in mid race. If that doesn't work out, I'll do a spot struggle
|
||||
support instead.
|
||||
</li>
|
||||
<li>
|
||||
Kitasan Black. She won't get to run often, so I'm going to run her while I have the opportunity. She's pretty difficult to
|
||||
build a deck for, though. No Professor, and only one of Tunes and Escape Artist, since I have neither Pasta nor Helios SSR.
|
||||
(The latter is proving non-functional anyway. Five runs with agemasen every time.)
|
||||
</li>
|
||||
<li>
|
||||
Chocobon. Part of me wants to do Seiun Sky here, but if Kitasan has carryover potential on her unique, then so does Bourbon
|
||||
if she whiffs on the first half corners. Since Kitasan won't have as good of a mid race, I really need a proper front runner
|
||||
ace, and that's what Chocobon does best.
|
||||
</li>
|
||||
</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>
|
||||
<p>
|
||||
The ultimate lane combo map, and also the ultimate <a href="#no-zone">no-overtake zone</a> map, and also the ultimate dueling map.
|
||||
@@ -1346,6 +1496,14 @@
|
||||
Win rates after 40: Falco 47.5%, Bourbon 27.5%, Suzuka 5%. While this is my best round 1 by a substantial margin, I'm pretty
|
||||
sure I'm going to get torn up in round 2; my stats aren't good enough to beat correctly built end closers.
|
||||
</p>
|
||||
<p>
|
||||
Win rates after 80: Falco 36.25%, Bourbon 22.5%, Suzuka 6.25%. Not as bad as I expected; most round 2 entries finished with
|
||||
three wins. I'll take it.
|
||||
</p>
|
||||
<p>
|
||||
Unfortunately lost to a bad mid race and a Taishin getting a perfect Radiant. At least it was only one this time. Second
|
||||
place.
|
||||
</p>
|
||||
|
||||
<Sec h={3} id="cm13">CM13 – Taurus Cup (Tokyo Derby)</Sec>
|
||||
<p>
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<script lang="ts">
|
||||
import { asset, resolve } from '$app/paths';
|
||||
import Skill from '$lib/Skill.svelte';
|
||||
import Sec from '../Sec.svelte';
|
||||
import * as skill from '$lib/data/skill';
|
||||
import { onMount } from 'svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
import courseSetupHoriz from './course-setup.horiz.png';
|
||||
import courseSetupVert from './course-setup.vert.png';
|
||||
import umaSetupHoriz from './uma-setup.horiz.png';
|
||||
import umaSetupVert from './uma-setup.vert.png';
|
||||
import resultsChart from './results-chart.png';
|
||||
import resultsHoriz from './results.horiz.png';
|
||||
import resultsVert from './results.vert.png';
|
||||
import Article from '../Article.svelte';
|
||||
|
||||
let allSkillsList: skill.Skill[] = $state([]);
|
||||
onMount(async () => {
|
||||
allSkillsList = await skill.skills();
|
||||
});
|
||||
const allSkills = $derived(allSkillsList.reduce((m, s) => m.set(s.skill_id, s), new Map<number, skill.Skill>()));
|
||||
const skills = $derived({
|
||||
goldpp: allSkills.get(200451),
|
||||
pp: allSkills.get(200452),
|
||||
ttl: allSkills.get(200531),
|
||||
earlyLead: allSkills.get(200532),
|
||||
dd: allSkills.get(201262),
|
||||
gw: allSkills.get(201601),
|
||||
ignitedWIT: allSkills.get(210052),
|
||||
});
|
||||
|
||||
const locations = [
|
||||
['札幌', 'Sapporo'],
|
||||
['函館', 'Hakodate'],
|
||||
['新潟', 'Niigata'],
|
||||
['福島', 'Fukushima'],
|
||||
['中山', 'Nakayama'],
|
||||
['東京', 'Tokyo'],
|
||||
['中京', 'Chukyo'],
|
||||
['京都', 'Kyoto'],
|
||||
['阪神', 'Hanshin'],
|
||||
['小倉', 'Kokura'],
|
||||
['大井', 'Ooi'],
|
||||
['川崎', 'Kawasaki'],
|
||||
['船橋', 'Funabashi'],
|
||||
['盛岡', 'Morioka'],
|
||||
] as const;
|
||||
const courseinfos = [
|
||||
['芝', 'Turf'],
|
||||
['ダート', 'Dirt'],
|
||||
['内', 'Inner'],
|
||||
['外', 'Outer'],
|
||||
] as const;
|
||||
const conditionses = [
|
||||
['良', 'Firm'],
|
||||
['稍重', 'Good'],
|
||||
['重', 'Soft'],
|
||||
['不良', 'Heavy'],
|
||||
] as const;
|
||||
const styles = [
|
||||
['大逃げ', 'Runaway'],
|
||||
['逃げ', 'Front Runner'],
|
||||
] as const;
|
||||
const moods = [
|
||||
['絶好調', 'Great'],
|
||||
['好調', 'Good'],
|
||||
['普通', 'Normal'],
|
||||
['不調', 'Bad'],
|
||||
['絶不調', 'Awful'],
|
||||
] as const;
|
||||
const accels = [
|
||||
['地固め', 'Groundwork'],
|
||||
['盤石', 'Unshakable Stance'],
|
||||
['先駆け', 'Early Lead'],
|
||||
['先手', 'Taking the Lead'],
|
||||
['進化先手', 'Evolved Taking the Lead'],
|
||||
] as const;
|
||||
const arinashi = [
|
||||
['あり', 'Enabled'],
|
||||
['なし', 'Disabled'],
|
||||
] as const;
|
||||
const hills = [
|
||||
['上り坂 (Red)', 'Uphill'],
|
||||
['下り坂 (Blue)', 'Downhill'],
|
||||
] as const;
|
||||
const times = [
|
||||
['危険回避', 'Dodging Danger'],
|
||||
['スキル', 'Lane Skill Activation'],
|
||||
['中盤', 'Mid Race'],
|
||||
] as const;
|
||||
const straco = [
|
||||
['直線 (Yellow)', 'Straight'],
|
||||
['コーナー (Pink)', 'Corner'],
|
||||
] as const;
|
||||
const laneskills = [
|
||||
['スキルなし', 'No Skills'],
|
||||
['危険回避のみ', 'Dodging Danger'],
|
||||
['+注目の踊り子', 'DD + Center Stage'],
|
||||
['+ポジションセンス', 'DD + Prudent Positioning'],
|
||||
['+アオハル点火・賢', 'DD + Ignited Spirit WIT'],
|
||||
] as const;
|
||||
const tooltip = [
|
||||
['時間', 'Time (s)'],
|
||||
['レーン', 'Lane (CW)'],
|
||||
['走行距離', 'Distance (m)'],
|
||||
['距離ロス', 'Distance Loss (m)'],
|
||||
] as const;
|
||||
const summaryText = [
|
||||
['バ身', 'Lengths'],
|
||||
['アド', 'Add'],
|
||||
['ロス', 'Loss'],
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
{#snippet transbody(heads: string | [string, string], cells: ReadonlyArray<readonly [string, string]>, cls?: ClassValue | null)}
|
||||
<table class={['mx-1 mt-4 table-fixed border md:mb-auto', cls]}>
|
||||
{#if typeof heads === 'string'}
|
||||
<caption class="border border-b-0">{heads}</caption>
|
||||
{:else}
|
||||
<thead class="border-b">
|
||||
<tr>
|
||||
<th class="w-1/2 px-2" scope="col">{heads[0]}</th>
|
||||
<th class="w-1/2 px-2" scope="col">{heads[1]}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
<tbody>
|
||||
{#each cells as [jp, en] (jp)}
|
||||
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
|
||||
<td>{jp}</td>
|
||||
<td>{en}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/snippet}
|
||||
|
||||
<Article>
|
||||
{#snippet head()}
|
||||
<Sec h={1} id="top" class="text-center pointer-coarse:mb-8">Gaikokujin's Guide to Lanecalc</Sec>
|
||||
<p>
|
||||
This is an English usage manual for <a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer"
|
||||
>危険回避シミュ</a
|
||||
>, or Lanecalc.
|
||||
</p>
|
||||
<p>
|
||||
Lanecalc is a precise calculator for the effect of <a href={resolve('/doc/frbm#lane-combo')}>lane combo</a>, the use of <Skill
|
||||
skill={skills.dd}
|
||||
/> along with some combination of <Skill skill={skills.pp} />, <Skill skill={skills.goldpp} />, and <Skill
|
||||
skill={skills.ignitedWIT}
|
||||
/>
|
||||
for a substantial speed boost in early race. Lanecalc helps with deciding whether lane combo (and DD alone) is applicable to a
|
||||
given track.
|
||||
</p>
|
||||
<p>
|
||||
The translations presented in this manual are contextualized to an English speaker using Lanecalc, rather than being direct
|
||||
translations. E.g., スキル literally means "skill," but I am translating it as "lane skill activation time" where that is
|
||||
what スキル signifies.
|
||||
</p>
|
||||
{/snippet}
|
||||
|
||||
<Sec h={2} id="course">Course Setup</Sec>
|
||||
<p>
|
||||
The top section of lane calc is the course setup section. Set the race location, course at the location, and ground condition
|
||||
here.
|
||||
</p>
|
||||
<a href={asset(courseSetupHoriz)}>
|
||||
<picture>
|
||||
<source srcset={asset(courseSetupHoriz)} media="(orientation: landscape)" width="882" height="111" />
|
||||
<img
|
||||
class="mx-auto"
|
||||
src={asset(courseSetupVert)}
|
||||
width="412"
|
||||
height="214"
|
||||
alt="Lanecalc's course setup section with English labels"
|
||||
/>
|
||||
</picture>
|
||||
</a>
|
||||
<div class="mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
|
||||
{@render transbody(['レース場', 'Location'], locations)}
|
||||
{@render transbody(['コース', 'Race Course'], courseinfos)}
|
||||
{@render transbody(['バ場状態', 'Conditions'], conditionses)}
|
||||
</div>
|
||||
<p>
|
||||
The race course menu shows only courses available at the selected race location. Changing the location will reset the course.
|
||||
</p>
|
||||
|
||||
<Sec h={2} id="uma">Uma Setup</Sec>
|
||||
<p>The next section is the setup for the Uma.</p>
|
||||
<a href={asset(umaSetupHoriz)}>
|
||||
<picture>
|
||||
<source srcset={asset(umaSetupHoriz)} media="(orientation: landscape)" width="878" height="218" />
|
||||
<img
|
||||
class="mx-auto"
|
||||
src={asset(umaSetupVert)}
|
||||
width="412"
|
||||
height="456"
|
||||
alt="Lanecalc's Uma setup section with English labels"
|
||||
/>
|
||||
</picture>
|
||||
</a>
|
||||
<div class="mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
|
||||
{@render transbody(['脚質', 'Running Style'], styles)}
|
||||
{@render transbody(['パワー', 'Power'], [])}
|
||||
{@render transbody(['調子', 'Mood'], moods)}
|
||||
{@render transbody(['ウマ番', 'Gate'], [])}
|
||||
{@render transbody(['危険回避発動', 'Dodging Danger'], arinashi)}
|
||||
{@render transbody(['横ブロック', 'Side Block'], arinashi)}
|
||||
{@render transbody(['レーンスキル発動位置', 'Lane Skill Activation Time'], [['秒', 'seconds']])}
|
||||
{@render transbody(['加速スキル', 'Acceleration Skills'], accels, 'md:col-span-2')}
|
||||
</div>
|
||||
<p>
|
||||
Power should be entered as the raw power stat, i.e. the number shown on your Uma's stat screen. (There doesn't appear to be a
|
||||
way to apply power passives.)
|
||||
</p>
|
||||
<p>
|
||||
Unshakable Stance is the gold version of <Skill skill={skills.gw} mention /> and is not yet available in Global. "Evolved Taking
|
||||
the Lead" means any of the evolutions of <Skill skill={skills.ttl} mention /> that turn it into a 0.5 accel skill. (Aston Machan
|
||||
will have a TTL evolution that instead turns it into a 4s 0.4 accel. There doesn't appear to be a way to apply it in the calculator.)
|
||||
</p>
|
||||
<p>
|
||||
Selecting another skill in the same group as a selected one will remove the existing skill. Selecting an already selected
|
||||
skill will disable it entirely in the simulator.
|
||||
</p>
|
||||
|
||||
<Sec h={2} id="chart">Results Chart</Sec>
|
||||
<p>Once the course and Uma are configured, click the green 計算 button. After a moment, the results appear.</p>
|
||||
<a href={asset(resultsChart)}>
|
||||
<picture>
|
||||
<img class="mx-auto" src={asset(resultsChart)} width="800" height="600" alt="Lanecalc results chart" />
|
||||
</picture>
|
||||
</a>
|
||||
<p class="mt-4">
|
||||
The chart shows the Uma's simulated lane (horizontal distance from the rail) and forward velocity under the effects of various
|
||||
lane combo skills.
|
||||
</p>
|
||||
<p>
|
||||
The example above is Hanshin 2200, firm, with a front runner with 1200 power in great condition from gate 6. She has <Skill
|
||||
skill={skills.gw}
|
||||
mention
|
||||
/> and <Skill skill={skills.earlyLead} mention />, and the lane change speed skills are set to activate at 7 seconds.
|
||||
</p>
|
||||
<div class="-mt-4 mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-6">
|
||||
{@render transbody('Hill Indicators', hills, 'md:col-span-2')}
|
||||
{@render transbody('Time Indicators', times, 'md:col-span-2')}
|
||||
{@render transbody('Straights/Corners', straco, 'md:col-span-2')}
|
||||
{@render transbody('Lane Speed Skills', laneskills, 'md:col-span-4 md:col-start-2')}
|
||||
</div>
|
||||
<p>
|
||||
You can click the labels underneath the chart to hide the corresponding line. The top row is for lane position lines, and the
|
||||
bottom row is for velocity lines.
|
||||
</p>
|
||||
<p>
|
||||
You can also hover the mouse over the lines for details. The first portion of the details are the skills that produce the
|
||||
exact corresponding point, with a color square for each. Underneath is the simulation state.
|
||||
</p>
|
||||
<div class="-mt-4 mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
|
||||
{@render transbody('Hover Info', tooltip, 'md:col-start-2')}
|
||||
</div>
|
||||
|
||||
<Sec h={2} id="summary">Results Summary</Sec>
|
||||
<p>
|
||||
Last is the summary, shown as circles showing the gain from each combination of skills:
|
||||
<Skill skill={skills.dd} /> alone, DD plus <Skill skill={skills.goldpp} />, DD plus <Skill skill={skills.pp} />, and DD plus <Skill
|
||||
skill={skills.ignitedWIT}
|
||||
/>.
|
||||
</p>
|
||||
<a href={asset(resultsHoriz)}>
|
||||
<picture>
|
||||
<source srcset={asset(resultsHoriz)} media="(orientation: landscape)" width="700" height="176" />
|
||||
<img class="mx-auto" src={asset(resultsVert)} width="380" height="703" alt="Lanecalc results summary" />
|
||||
</picture>
|
||||
</a>
|
||||
<div class="mb-4 grid grid-cols-1 text-center text-xl md:grid-cols-3">
|
||||
{@render transbody('Lane Speed Skills', laneskills.slice(1), 'md:col-span-2')}
|
||||
{@render transbody('Labels', summaryText)}
|
||||
</div>
|
||||
</Article>
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,482 @@
|
||||
<script lang="ts">
|
||||
import type { ComputedAreas, ComputedSeries } from '$lib/chart';
|
||||
import Article from '../Article.svelte';
|
||||
import Sec from '../Sec.svelte';
|
||||
import * as race from '$lib/race';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
function modemean(x: number, bonus: number, p: number): number {
|
||||
return x * (1 - p) + x * bonus * p;
|
||||
}
|
||||
|
||||
let style = $state(race.RunningStyle.FrontRunner);
|
||||
let surfaceApt = $state(race.AptitudeLevel.A);
|
||||
let distanceApt = $state(race.AptitudeLevel.A);
|
||||
let raceLen = $state(2000);
|
||||
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.Runaway);
|
||||
const distanceType = $derived(race.distance(raceLen));
|
||||
const raceLenType = $derived(race.Distance[distanceType]);
|
||||
|
||||
const spurtSpeed: Array<ComputedSeries | null> = $derived([
|
||||
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
|
||||
{ label: `Guts 600`, y: (x) => race.spurtSpeed(x, 600, style, distanceApt, raceLen) },
|
||||
distanceApt !== race.AptitudeLevel.A
|
||||
? { label: `${raceLenType} A, Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, race.AptitudeLevel.A, raceLen) }
|
||||
: null,
|
||||
]);
|
||||
const nonSpurtSpeed: Array<ComputedSeries | null> = $derived([
|
||||
{ label: `Base Target Speed`, y: (x) => race.baseTargetSpeed(raceLen, style, race.Phase.LateRace, x, distanceApt) },
|
||||
distanceApt !== race.AptitudeLevel.A
|
||||
? {
|
||||
label: `${raceLenType} A`,
|
||||
y: (x) => race.baseTargetSpeed(raceLen, style, race.Phase.LateRace, x, race.AptitudeLevel.A),
|
||||
}
|
||||
: null,
|
||||
]);
|
||||
const hp: ComputedSeries[] = $derived([
|
||||
{ label: 'Front Runner', y: (x) => race.maxHP(raceLen, race.RunningStyle.FrontRunner, x) },
|
||||
{ label: 'Pace Chaser', y: (x) => race.maxHP(raceLen, race.RunningStyle.PaceChaser, x) },
|
||||
{ label: 'Late Surger', y: (x) => race.maxHP(raceLen, race.RunningStyle.LateSurger, x) },
|
||||
{ label: 'End Closer', y: (x) => race.maxHP(raceLen, race.RunningStyle.EndCloser, x) },
|
||||
{ label: 'Runaway', y: (x) => race.maxHP(raceLen, race.RunningStyle.Runaway, x) },
|
||||
]);
|
||||
const moveLane: ComputedSeries[] = [{ label: 'Move Lane Modifier', y: (x) => race.moveLaneModifier(x) }];
|
||||
const uphill: ComputedSeries[] = [
|
||||
{ label: '+2 Hill', y: (x) => race.uphillMod(x, 2.0) },
|
||||
{ label: '+1.5 Hill', y: (x) => race.uphillMod(x, 1.5) },
|
||||
{ label: '+1 Hill', y: (x) => race.uphillMod(x, 1.0) },
|
||||
];
|
||||
const accel: Array<ComputedSeries | null> = $derived([
|
||||
{ label: 'Early Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.EarlyRace) },
|
||||
{ label: 'Mid Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.MidRace) },
|
||||
{ label: 'Late Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.LateRace) },
|
||||
{
|
||||
label: 'Early Race Uphill',
|
||||
y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.EarlyRace, race.AptitudeLevel.A, true),
|
||||
},
|
||||
]);
|
||||
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
|
||||
const conservePower: Array<ComputedSeries | null> = $derived([
|
||||
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) },
|
||||
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) },
|
||||
styleIsFront
|
||||
? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) }
|
||||
: null,
|
||||
]);
|
||||
const gutsSpurt: Array<ComputedSeries | null> = $derived([
|
||||
{ label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) },
|
||||
{ label: `Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, distanceApt, raceLen) },
|
||||
// NOTE(zeph): The label is 1600 despite the input being 1400
|
||||
// for raw vs final difference.
|
||||
{ label: `Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, distanceApt, raceLen) },
|
||||
distanceApt !== race.AptitudeLevel.A
|
||||
? { label: `${raceLenType} A, Speed 1600`, y: (x) => race.spurtSpeed(1400, x, style, race.AptitudeLevel.A, raceLen) }
|
||||
: null,
|
||||
]);
|
||||
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
|
||||
const gutsHPRate: ComputedSeries[] = [{ label: 'HP Consumption Rate Multiplier', y: (x) => race.spurtHPRateMod(x) }];
|
||||
const ssBoost: ComputedSeries[] = [{ label: 'Target Speed Boost', y: (x) => race.spotStruggleSpeed(x) }];
|
||||
const ssDur: ComputedSeries[] = [
|
||||
{ label: 'Front Runner S', y: (x) => race.spotStruggleDuration(x, race.AptitudeLevel.S) },
|
||||
{ label: 'Front Runner A', y: (x) => race.spotStruggleDuration(x, race.AptitudeLevel.A) },
|
||||
];
|
||||
const duelSpeed: ComputedSeries[] = [{ label: 'Dueling Speed Boost', y: (x) => race.duelSpeedMod(x) }];
|
||||
const duelAccel: ComputedSeries[] = [{ label: 'Dueling Acceleration Boost', y: (x) => race.duelAccelMod(x) }];
|
||||
const secSpeed: Array<ComputedAreas | null> = $derived([
|
||||
{
|
||||
label: 'Early Race',
|
||||
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0],
|
||||
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1],
|
||||
},
|
||||
{
|
||||
label: 'Mid Race',
|
||||
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0],
|
||||
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1],
|
||||
},
|
||||
styleIsFront
|
||||
? {
|
||||
label: 'Early Race + Mean Speed-Up Mode',
|
||||
y1: (x) =>
|
||||
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.frontModeEnterChance(x)),
|
||||
y2: (x) =>
|
||||
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], 1.04, race.frontModeEnterChance(x)),
|
||||
}
|
||||
: {
|
||||
label: 'Early Race + Mean Pace-Up Mode',
|
||||
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.paceUpEnterChance(x)),
|
||||
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], 1.04, race.paceUpEnterChance(x)),
|
||||
},
|
||||
styleIsFront
|
||||
? {
|
||||
label: 'Mid Race + Mean Speed-Up Mode',
|
||||
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], 1.04, race.frontModeEnterChance(x)),
|
||||
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], 1.04, race.frontModeEnterChance(x)),
|
||||
}
|
||||
: {
|
||||
label: 'Mid Race + Mean Pace-Up Mode',
|
||||
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], 1.04, race.paceUpEnterChance(x)),
|
||||
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], 1.04, race.paceUpEnterChance(x)),
|
||||
},
|
||||
]);
|
||||
const downhill: ComputedSeries[] = [
|
||||
{ label: 'Style S', y: (x) => race.downhillAccelEnterChance(x * 1.1) * 100 },
|
||||
{ label: 'Style A', y: (x) => race.downhillAccelEnterChance(x) * 100 },
|
||||
];
|
||||
const reducedSpurt: ComputedSeries[] = [
|
||||
{ label: 'Style S', y: (x) => race.reducedSpurtChance(x * 1.1) * 100 },
|
||||
{ label: 'Style A', y: (x) => race.reducedSpurtChance(x) * 100 },
|
||||
];
|
||||
const skillChance: ComputedSeries[] = [
|
||||
{ label: 'Per Skill', y: (x) => race.skillWitCheck(x) * 100 },
|
||||
{ label: '1 of 2 or 2 of 2', y: (x) => (race.skillWitCheck(x, 2, 1) + race.skillWitCheck(x, 2, 2)) * 100 },
|
||||
{ label: '2 of 2', y: (x) => race.skillWitCheck(x, 2, 2) * 100 },
|
||||
{ label: '3 of 3', y: (x) => race.skillWitCheck(x, 3, 3) * 100 },
|
||||
{ label: '3 of 4 or 4 of 4', y: (x) => (race.skillWitCheck(x, 4, 3) + race.skillWitCheck(x, 4, 4)) * 100 },
|
||||
];
|
||||
const poskeep: ComputedSeries[] = [
|
||||
{ label: 'Front Runner S', y: (x) => 100 * race.frontModeEnterChance(x * 1.1) },
|
||||
{ label: 'Front Runner A', y: (x) => 100 * race.frontModeEnterChance(x) },
|
||||
{ label: 'Style S', y: (x) => 100 * race.paceUpEnterChance(x * 1.1) },
|
||||
{ label: 'Style A', y: (x) => 100 * race.paceUpEnterChance(x) },
|
||||
];
|
||||
const rushed: ComputedSeries[] = [
|
||||
{ label: 'Style S', y: (x) => race.rushedChance(x * 1.1) * 100 },
|
||||
{ label: 'Style A', y: (x) => race.rushedChance(x) * 100 },
|
||||
];
|
||||
</script>
|
||||
|
||||
{#snippet statChart(
|
||||
stat: race.Stat,
|
||||
y: Array<ComputedSeries | null>,
|
||||
yLabel: string,
|
||||
range?: [number, number],
|
||||
pick?: { len?: boolean; style?: boolean; dist?: boolean; surf?: boolean },
|
||||
)}
|
||||
{#if !pick?.len && !pick?.style && !pick?.dist && !pick?.surf}
|
||||
<div class="mb-12 h-60 w-full md:mb-20 md:h-96">
|
||||
<StatChart
|
||||
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
|
||||
{stat}
|
||||
{y}
|
||||
{yLabel}
|
||||
{range}
|
||||
plotOptions={{ color: { legend: true } }}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-36 h-60 w-full md:mb-28 md:h-96">
|
||||
<StatChart
|
||||
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
|
||||
{stat}
|
||||
{y}
|
||||
{yLabel}
|
||||
{range}
|
||||
plotOptions={{ color: { legend: true } }}
|
||||
/>
|
||||
<div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7">
|
||||
{#if pick?.len}
|
||||
<input
|
||||
class="col-span-2 my-auto w-full justify-self-center-safe"
|
||||
type="range"
|
||||
id="raceLen"
|
||||
min="1000"
|
||||
max="3600"
|
||||
step="100"
|
||||
bind:value={raceLen}
|
||||
/>
|
||||
<span class="col-span-1 my-auto pl-2 text-sm">{raceLen}m</span>
|
||||
{/if}
|
||||
{#if pick?.style}
|
||||
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}>
|
||||
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||
<option value={style}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
{#if pick?.dist}
|
||||
<label class="col-span-1 my-auto pr-2 text-right text-sm md:inline" for="distPick">{raceLenType}</label>
|
||||
<select class="col-span-1 my-auto text-sm" id="distPick" bind:value={distanceApt}>
|
||||
{#each race.APTITUDE_LEVELS as l (l)}
|
||||
<option value={l}>{race.AptitudeLevel[l]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
{#if pick?.surf}
|
||||
<label class="col-span-1 my-auto pr-2 text-right text-sm md:inline" for="surfPick">Surface</label>
|
||||
<select class="col-span-1 my-auto text-sm" id="surfPick" bind:value={surfaceApt}>
|
||||
{#each race.APTITUDE_LEVELS as l (l)}
|
||||
<option value={l}>{l}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<Article>
|
||||
{#snippet head()}
|
||||
<Sec h={1} id="top" class="text-center">Race Mechanics Charts</Sec>
|
||||
<p>
|
||||
This article is an interactive version of <a
|
||||
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
|
||||
>
|
||||
to give a better sense of how mechanics scale with stats and aptitudes.
|
||||
</p>
|
||||
<p>
|
||||
Mechanics are organized in this article according to the stats that determine them. Those that vary with multiple stats,
|
||||
e.g. spurt speed, appear in all relevant sections with differing horizontal axes. Mechanics that vary with race distance,
|
||||
distance aptitudes, and surface aptitudes have controls to change each by their charts.
|
||||
</p>
|
||||
<p>
|
||||
Only numeric mechanics that vary with stats and aptitudes are shown. Descriptions of concepts and conditions are left to the
|
||||
original source.
|
||||
</p>
|
||||
{/snippet}
|
||||
|
||||
<Sec h={2} id="speed">Speed</Sec>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="spurt-speed"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.mpfjmgpzqalr"
|
||||
>Spurt Speed</Sec
|
||||
>
|
||||
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed-guts">the effect of Guts</a>.</p>
|
||||
{@render statChart(race.Stat.Speed, spurtSpeed, 'Spurt Speed (m/s)', [20, 26.5], { len: true, style: true, dist: true })}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="late-race-speed"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.43unjgrhmm7e"
|
||||
>Late Race Speed</Sec
|
||||
>
|
||||
<p>Base target speed during late race when the Uma is not spurting (but not dead yet).</p>
|
||||
{@render statChart(race.Stat.Speed, nonSpurtSpeed, 'Base Target Speed (m/s)', [18, 23], { len: true, style: true, dist: true })}
|
||||
|
||||
<Sec h={2} id="stamina">Stamina</Sec>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="hp"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.fnd75v9k5dx3"
|
||||
>HP</Sec
|
||||
>
|
||||
<p>Max HP, i.e. starting HP.</p>
|
||||
{@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000], { len: true })}
|
||||
|
||||
<Sec h={2} id="power">Power</Sec>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="move-lane-mod"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.s5glz57t0wzd"
|
||||
>Move Lane Modifier</Sec
|
||||
>
|
||||
<p>
|
||||
Target speed bonus when changing lanes while under the effect of a lane change speed skill. See <a
|
||||
href={resolve('/doc/frbm#lane-combo')}>lane combo</a
|
||||
>.
|
||||
</p>
|
||||
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.6])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="uphill"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.wdj69udyo340"
|
||||
>Uphill Target Speed Loss</Sec
|
||||
>
|
||||
<p>Target speed modifier while running uphill.</p>
|
||||
{@render statChart(race.Stat.Power, uphill, 'Target Speed Modifier (m/s)', [-2, 0])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="acceleration"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.t9fv1oq5lsu9"
|
||||
>Acceleration</Sec
|
||||
>
|
||||
<p>
|
||||
Acceleration, i.e. the rate of change of forward velocity when target speed is greater than current speed. Stats do not affect
|
||||
deceleration (acceleration when target speed is less than current speed).
|
||||
</p>
|
||||
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.55], { style: true })}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="lane-change-target-speed"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.8brkec5lv1fh"
|
||||
>Lane Change Target Speed</Sec
|
||||
>
|
||||
<p>Horizontal (rather than forward) target speed of changing lanes.</p>
|
||||
<div class="mb-4 h-60 w-full md:h-96">
|
||||
<StatChart
|
||||
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
|
||||
stat={race.Stat.Power}
|
||||
y={laneChange}
|
||||
yLabel="Lane Change Target Speed (CW/s)"
|
||||
range={[0.01, 0.04]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="conserve-power"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.9edgwx6l733b"
|
||||
>Fully Charged Acceleration</Sec
|
||||
>
|
||||
<p>
|
||||
Bonus acceleration at spurt start from the Charge Up/Fully Charged mechanic. Scales with base power plus passive skills, so
|
||||
modifiers for ground conditions and career mode do not apply.
|
||||
</p>
|
||||
<p>
|
||||
This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed
|
||||
and spot struggle multipliers apply.
|
||||
</p>
|
||||
{@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })}
|
||||
|
||||
<Sec h={2} id="guts">Guts</Sec>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="spurt-speed-guts"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.mpfjmgpzqalr"
|
||||
>Spurt Speed</Sec
|
||||
>
|
||||
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed">the effect of Speed</a>.</p>
|
||||
{@render statChart(race.Stat.Guts, gutsSpurt, 'Spurt Speed (m/s)', [20, 26.5], { len: true, style: true, dist: true })}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="min-speed"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.nn1kp5g8fdhu"
|
||||
>Minimum Speed</Sec
|
||||
>
|
||||
<p>Forced minimum speed, as well as target speed when out of HP.</p>
|
||||
{@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.5], { len: true })}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="hp-rate-mod"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.o0be6rj0myb1"
|
||||
>Late Race HP Consumption Rate Modifier</Sec
|
||||
>
|
||||
<p>Multiplier on HP consumption rate during late race and last spurt phase.</p>
|
||||
{@render statChart(race.Stat.Guts, gutsHPRate, 'HP Consumption Multiplier', [1.2, 1.6])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="spot-struggle"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.5l523bk8k3vz"
|
||||
>Spot Struggle</Sec
|
||||
>
|
||||
<p>Speed boost and duration of spot struggle.</p>
|
||||
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
||||
<StatChart stat={race.Stat.Guts} y={ssBoost} yLabel="Speed Bonus (m/s)" range={[0, 0.4]} />
|
||||
<StatChart stat={race.Stat.Guts} y={ssDur} yLabel="Duration (s)" range={[0, 14]} />
|
||||
</div>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="dueling"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.98rgrb7ww1td"
|
||||
>Dueling</Sec
|
||||
>
|
||||
<p>Speed boost and acceleration boost of dueling.</p>
|
||||
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
|
||||
<StatChart stat={race.Stat.Guts} y={duelSpeed} yLabel="Speed Bonus (m/s)" range={[0, 0.8]} />
|
||||
<StatChart stat={race.Stat.Guts} y={duelAccel} yLabel="Acceleration Bonus (m/s²)" range={[0, 0.16]} />
|
||||
</div>
|
||||
|
||||
<Sec h={2} id="wit">Wit</Sec>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="section-speed"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.xdha40q2176g"
|
||||
>Section Speed</Sec
|
||||
>
|
||||
<p>Random variance in target speed per race section.</p>
|
||||
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
|
||||
<StatChart
|
||||
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
|
||||
stat={race.Stat.Wit}
|
||||
yArea={secSpeed}
|
||||
yLabel="Section Speed (m/s)"
|
||||
range={[17, 23]}
|
||||
plotOptions={{ color: { legend: true } }}
|
||||
/>
|
||||
<div class="mx-auto flex w-full md:max-w-2xl">
|
||||
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
|
||||
<input
|
||||
class="my-auto max-w-40 flex-1 md:flex-2"
|
||||
type="range"
|
||||
id="secSpeedRaceLen"
|
||||
min="1000"
|
||||
max="3600"
|
||||
step="100"
|
||||
bind:value={raceLen}
|
||||
/>
|
||||
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
|
||||
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label>
|
||||
<select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={style}>
|
||||
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||
<option value={style}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="downhill"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.wdj69udyo340"
|
||||
>Downhill Accel Mode Chance</Sec
|
||||
>
|
||||
<p>Chance each second to enter downhill accel mode when eligible.</p>
|
||||
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 75])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="spurt-accept"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.mpfjmgpzqalr"
|
||||
>Reduced Spurt Accept Chance</Sec
|
||||
>
|
||||
<p>Chance to accept each checked spurt delay and speed when not full spurting.</p>
|
||||
{@render statChart(race.Stat.Wit, reducedSpurt, 'Accept Chance (% each candidate)', [0, 100])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="skill"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.23v0k7wyzcmb"
|
||||
>Skill Activation Chance</Sec
|
||||
>
|
||||
<p>
|
||||
Chance that each skill that has a wit check will be eligible to activate. This uses base wit, so style aptitude and passive
|
||||
wit skills do not affect the chance, although mood does.
|
||||
</p>
|
||||
{@render statChart(race.Stat.Wit, skillChance, 'Skill Activation Chance (%)', [0, 100])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="poskeep"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.tulele26d1pm"
|
||||
>Position Keep Mode Chance</Sec
|
||||
>
|
||||
<p>
|
||||
Chance for front runners to enter speed-up or overtake mode, or for non-front runners to enter pace-up mode (but no effect on
|
||||
pace-down mode), each 2 seconds when eligible.
|
||||
</p>
|
||||
{@render statChart(race.Stat.Wit, poskeep, 'Mode Entry Chance (%)', [0, 50])}
|
||||
|
||||
<Sec
|
||||
h={3}
|
||||
id="rushed"
|
||||
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.awbjufvej6ud"
|
||||
>Rushed Chance</Sec
|
||||
>
|
||||
<p>Chance for runners to become rushed at some point during the race. Rushed chance is bracketed to integer percentages.</p>
|
||||
{@render statChart(race.Stat.Wit, rushed, 'Rushed Chance (%)', [0, 30])}
|
||||
</Article>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import * as skill from '$lib/data/skill';
|
||||
import * as spark from '$lib/data/spark';
|
||||
import Pick from '$lib/Pick.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
let sparks: spark.Spark[] = $state([]);
|
||||
let skills: SvelteMap<number, skill.Skill> = new SvelteMap();
|
||||
onMount(async () => {
|
||||
const [sp, sk] = await Promise.all([spark.sparks(), skill.skills()]);
|
||||
sparks = sp;
|
||||
for (const s of sk) {
|
||||
skills.set(s.skill_id, s);
|
||||
}
|
||||
});
|
||||
|
||||
let group: spark.SparkGroup | undefined = $state();
|
||||
|
||||
const groups = $derived(spark.byGroup(sparks));
|
||||
function describe(eff: spark.Effect) {
|
||||
if (eff.target === spark.Target.Skill) {
|
||||
const sk = skills.get(eff.value1);
|
||||
return `${sk?.name} ${spark.TARGET_NAMES[eff.target]} +${eff.value2}`;
|
||||
}
|
||||
return `${spark.TARGET_NAMES[eff.target]} +${eff.value1}`;
|
||||
}
|
||||
const curEffectGroups = $derived(group?.effects ?? []);
|
||||
const curDescriptions = $derived(curEffectGroups.map((g) => g.map((e) => describe(e))));
|
||||
function sparkClass(t: spark.Type | null | undefined): string[] {
|
||||
if (t == null) {
|
||||
return [];
|
||||
}
|
||||
switch (t) {
|
||||
case 1:
|
||||
return ['stat'];
|
||||
case 2:
|
||||
return ['aptitude'];
|
||||
case 3:
|
||||
return ['unique'];
|
||||
case 5:
|
||||
case 4:
|
||||
case 6:
|
||||
case 10:
|
||||
case 8:
|
||||
case 11:
|
||||
case 9:
|
||||
return ['white-spark'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const curClass = $derived(sparkClass(group?.type));
|
||||
|
||||
const key = (v: spark.SparkGroup) => ({ id: v.spark_group, name: v.name });
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Spark Explorer</h1>
|
||||
<form class="mx-auto mt-8 rounded-md p-4 text-center shadow-md ring md:max-w-2xl">
|
||||
<Pick id="spark" items={groups} class="w-full" {key} bind:value={group} required>
|
||||
{#snippet option(g)}
|
||||
<span class={['spark rounded px-2 py-1', sparkClass(g.type)]}>{g.name}</span>
|
||||
{/snippet}
|
||||
</Pick>
|
||||
</form>
|
||||
<svelte:boundary>
|
||||
<div
|
||||
class={[
|
||||
'spark mx-auto mt-8 max-w-md flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md',
|
||||
curClass,
|
||||
]}
|
||||
>
|
||||
<span class="block text-xl">{group?.name}</span>
|
||||
{#if group != null}
|
||||
<span class="block">
|
||||
{spark.TYPE_NAMES[group.type]} Spark
|
||||
</span>
|
||||
{/if}
|
||||
<span class="block">{group?.description}</span>
|
||||
</div>
|
||||
<table class="mx-auto mt-8 w-full max-w-xl table-fixed border-separate border-spacing-2">
|
||||
<caption class="text-xl">Possible Effects</caption>
|
||||
<tbody>
|
||||
{#each curDescriptions as s (s)}
|
||||
<tr>
|
||||
<td class="flex gap-2 border border-mist-400 p-1 text-center dark:border-mist-950">
|
||||
{#each s as n (n)}
|
||||
<span class="my-auto flex-1">{n}</span>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{#snippet pending()}
|
||||
<div>Loading data...</div>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
|
||||
<p>
|
||||
When a spark activates during inspiration, it has an additional random roll for one of the effects listed in the table here.
|
||||
All star levels of each spark have the same possible effects. Higher star levels are more likely to give better rewards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.white-spark {
|
||||
--spark-color: light-dark(var(--color-mist-200), var(--color-mist-800));
|
||||
}
|
||||
|
||||
.stat {
|
||||
--spark-color: light-dark(var(--color-sky-200), var(--color-sky-800));
|
||||
}
|
||||
|
||||
.aptitude {
|
||||
--spark-color: light-dark(var(--color-rose-300), var(--color-rose-800));
|
||||
}
|
||||
|
||||
.unique {
|
||||
--spark-color: light-dark(var(--color-lime-400), var(--color-lime-800));
|
||||
}
|
||||
|
||||
.spark {
|
||||
background-color: var(--spark-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import * as skill from '$lib/data/skill';
|
||||
import SkillIcon from '$lib/SkillIcon.svelte';
|
||||
|
||||
let rare = $state(false);
|
||||
let unique = $state(false);
|
||||
|
||||
let allSkillsList: skill.Skill[] = $state([]);
|
||||
onMount(async () => {
|
||||
allSkillsList = await skill.skills();
|
||||
});
|
||||
|
||||
const tagTypes = $state([
|
||||
{
|
||||
name: 'Running Style',
|
||||
includes: skill.TAGS.filter((t) => 101 <= t.tag && t.tag <= 199).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
{
|
||||
name: 'Distance',
|
||||
includes: skill.TAGS.filter((t) => 201 <= t.tag && t.tag <= 299).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
{
|
||||
name: 'Surface',
|
||||
includes: skill.TAGS.filter((t) => 501 <= t.tag && t.tag <= 599).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
{
|
||||
name: 'Timing',
|
||||
includes: skill.TAGS.filter((t) => 301 <= t.tag && t.tag <= 399).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
{
|
||||
name: 'Effects',
|
||||
includes: skill.TAGS.filter((t) => 401 <= t.tag && t.tag <= 499).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
{
|
||||
name: 'Passive Types',
|
||||
includes: skill.TAGS.filter((t) => 601 <= t.tag && t.tag <= 699).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
{
|
||||
name: 'Scenario Skills',
|
||||
includes: skill.TAGS.filter((t) => 801 <= t.tag && t.tag <= 899).map((t) => ({ ...t, on: false })),
|
||||
},
|
||||
]);
|
||||
const tags = $derived(tagTypes.flatMap((v) => v.includes.filter((t) => t.on).map((t) => t.tag)));
|
||||
const skills = $derived(
|
||||
allSkillsList.filter(
|
||||
(s) =>
|
||||
(rare || s.group_rate === 1) &&
|
||||
(unique || (s.rarity < 3 && s.unique_owner == null)) &&
|
||||
tags.some((t) => s.tags.includes(t)),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Tagged Skills Explorer</h1>
|
||||
<form class="mx-auto my-8 rounded-md p-4 text-center shadow-md ring md:max-w-4xl">
|
||||
<div class="flex w-full gap-4">
|
||||
<div class="my-auto inline-block flex-1">
|
||||
<input id="rare" type="checkbox" role="switch" class="min-h-6 min-w-6 align-middle" bind:checked={rare} />
|
||||
<label for="rare">Include All Levels and Rarities</label>
|
||||
</div>
|
||||
<div class="my-auto inline-block flex-1">
|
||||
<input id="unique" type="checkbox" role="switch" class="min-h-6 min-w-6 align-middle" bind:checked={unique} />
|
||||
<label for="unique">Include Uniques</label>
|
||||
</div>
|
||||
</div>
|
||||
{#each tagTypes as { name, includes } (name)}
|
||||
<div class="mt-1 flex flex-row">
|
||||
<span class="my-auto h-0 flex-1 border"></span>
|
||||
<span class="m-2 flex-none text-sm italic">{name}</span>
|
||||
<span class="my-auto h-0 flex-1 border"></span>
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap place-content-center-safe gap-6 md:grid-cols-4">
|
||||
{#each includes as t (t.tag)}
|
||||
<div class="inline-block">
|
||||
<input id={t.name} type="checkbox" class="min-h-6 min-w-6 align-middle" bind:checked={t.on} />
|
||||
<label for={t.name} class="align-middle">{t.name}</label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
{#snippet condition(cond: string)}
|
||||
{@const disj = cond.replaceAll('&', ' & ').split('@')}
|
||||
{#each disj as conj, i (conj)}
|
||||
<span class="block font-mono text-sm">{conj}</span>
|
||||
{#if i !== disj.length - 1}
|
||||
<span class="block text-xs italic">or</span>
|
||||
{/if}
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<span class="mb-4 block text-center text-2xl">{skills.length} skills selected</span>
|
||||
<div class="grid max-w-7xl grid-cols-1 gap-2 md:grid-cols-3">
|
||||
{#each skills as s (s.skill_id)}
|
||||
<div class="rounded-md border px-2 pt-1 pb-2 text-center shadow-sm transition-shadow hover:shadow-md">
|
||||
<span class="block text-xl">
|
||||
<SkillIcon icon_id={s?.icon_id} size={32} />
|
||||
{s.name}
|
||||
</span>
|
||||
<span class="block italic">{s.description}</span>
|
||||
{#each s.activations as act, i (i)}
|
||||
<span class="my-2 block rounded-md border">
|
||||
{#if act.precondition}
|
||||
<span class="block border-b py-1">
|
||||
<span class="text-xs italic">Precondition</span>
|
||||
{@render condition(act.precondition)}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="block border-b py-1">
|
||||
{@render condition(act.condition)}
|
||||
</span>
|
||||
<span class="block py-1">
|
||||
{#each act.abilities as a, i (i)}
|
||||
<span class="block text-sm">
|
||||
{console.log(a.type)}
|
||||
{skill.ABILITY_TYPE_FORMAT[a.type](a.value)}
|
||||
{skill.ABILITY_SCALE_NAME[a.value_usage]}
|
||||
{#if a.target && a.target !== skill.Target.Self}
|
||||
{skill.TARGET_FORMAT[a.target](a.target_value)}
|
||||
{/if}
|
||||
</span>
|
||||
{/each}
|
||||
{#if act.duration != null && act.duration > 0}
|
||||
<span class="block text-sm">
|
||||
for {skill.tenThousandths(act.duration)}s
|
||||
{skill.DURATION_SCALE_NAME[act.dur_scale]}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
{/each}
|
||||
<div class="flex w-full text-xs">
|
||||
<span class="flex-1">ID {s.skill_id}</span>
|
||||
{#if s.unique_owner}
|
||||
<span class="flex-1">{s.unique_owner}</span>
|
||||
{/if}
|
||||
{#if s.sp_cost}
|
||||
<span class="flex-1">{s.sp_cost} SP</span>
|
||||
{/if}
|
||||
<span class="flex-1">Grade Value {s.grade_value}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
|
||||
<ul class="ml-4 list-disc">
|
||||
<li>
|
||||
To see skills that can be received from MANT/Trackblazer rivals, select the running style used and the distance and surface
|
||||
of the race. Some skills require meeting multiple conditions to be eligible hints, e.g. both Pace and Long; they will
|
||||
instead be shown in the tool when either condition is selected.
|
||||
</li>
|
||||
<li>Skills for each tag are auto-generated from the game's local database.</li>
|
||||
<li>Tag names are inferred from the skills that have them.</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,17 +1,11 @@
|
||||
<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>
|
||||
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — Character Tools</h1>
|
||||
<p>Tools related to understanding Umamusume characters.</p>
|
||||
|
||||
<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>
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy — {cur.name}</h1>
|
||||
<p>{cur.description}</p>
|
||||
<IndexSection pages={all.PAGES.race} />
|
||||
|
||||
@@ -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>
|
||||
@@ -16,6 +16,7 @@
|
||||
Stat,
|
||||
} from '$lib/race';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import SpeedDur from './SpeedDur.svelte';
|
||||
|
||||
let rawPower = $state(1200);
|
||||
@@ -29,7 +30,7 @@
|
||||
const careerMod = $derived(isCareer ? 400 : 0);
|
||||
const powerStat = $derived(rawPower + 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 accel = $derived(phases.map((p) => acceleration(powerStat, style, surfApt, p)));
|
||||
@@ -163,21 +164,13 @@
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
|
||||
<StatChart
|
||||
class="flex-1"
|
||||
stat={Stat.Guts}
|
||||
y={ssY}
|
||||
yLabel="Lengths Gained"
|
||||
range={[0, 1.5, 2.5]}
|
||||
xRule={gutsStat}
|
||||
yRule={ssYRule}
|
||||
/>
|
||||
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 2]} xRule={gutsStat} yRule={ssYRule} />
|
||||
<StatChart
|
||||
class="flex-1"
|
||||
stat={Stat.Power}
|
||||
y={lcY}
|
||||
yLabel="Lengths Gained"
|
||||
range={[0, 1.5, 2.5]}
|
||||
range={[0, 1.1]}
|
||||
xRule={powerStat}
|
||||
yRule={lcYRule}
|
||||
/>
|
||||
@@ -212,7 +205,7 @@
|
||||
href="https://lanecalc.hf-uma.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">危険回避シミュ</a
|
||||
>.
|
||||
>, or <a href={resolve('/doc/lanecalc')}>Gaikokujin's Guide to Lanecalc</a> for English instructions on how to use it.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { HORSE_LENGTH, speedGain } from '$lib/race';
|
||||
import type { Snippet } from 'svelte';
|
||||
import CalcInfo from '../CalcInfo.svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
@@ -21,13 +22,12 @@
|
||||
const text = $derived(gain.map(fmtp).join(' – '));
|
||||
</script>
|
||||
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<div class="block">{@render children()}</div>
|
||||
<span class="block text-xl">{text} L</span>
|
||||
<CalcInfo title={children} class="max-w-80 flex-1">
|
||||
{text} L
|
||||
{#snippet additional()}
|
||||
<div class="flex flex-row">
|
||||
<span class="flex-1 text-xs">{fmtp(speed)} m/s</span>
|
||||
<span class="flex-1 text-xs">{dur.toFixed(3)} s</span>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</CalcInfo>
|
||||
|
||||
@@ -1,61 +1,74 @@
|
||||
<script lang="ts">
|
||||
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 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;
|
||||
|
||||
let rawSpeed: number = $state(1200);
|
||||
let rawGuts: number = $state(1200);
|
||||
let style: RunningStyle = $state(RunningStyle.FrontRunner);
|
||||
let opponentStyle: RunningStyle = $state(RunningStyle.PaceChaser);
|
||||
let distanceApt: AptitudeLevel = $state(AptitudeLevel.S);
|
||||
let style: race.RunningStyle = $state(race.RunningStyle.FrontRunner);
|
||||
let opponentStyle: race.RunningStyle = $state(race.RunningStyle.PaceChaser);
|
||||
let distanceApt: race.AptitudeLevel = $state(race.AptitudeLevel.S);
|
||||
let raceLen: number = $state(2000);
|
||||
let isCareer: boolean = $state(false);
|
||||
|
||||
const careerMod = $derived(isCareer ? 400 : 0);
|
||||
const speedStat = $derived(rawSpeed + 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([
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.S, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.PaceChaser, AptitudeLevel.S, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.LateSurger, AptitudeLevel.S, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.S, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.FrontRunner, race.AptitudeLevel.S, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.PaceChaser, race.AptitudeLevel.S, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.LateSurger, race.AptitudeLevel.S, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.EndCloser, race.AptitudeLevel.S, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.Runaway, race.AptitudeLevel.S, raceLen) - careerMod,
|
||||
]);
|
||||
const aProf = $derived([
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.A, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.PaceChaser, AptitudeLevel.A, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.LateSurger, AptitudeLevel.A, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.EndCloser, AptitudeLevel.A, raceLen) - careerMod,
|
||||
inverseSpurtSpeed(speed, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.A, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.FrontRunner, race.AptitudeLevel.A, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.PaceChaser, race.AptitudeLevel.A, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.LateSurger, race.AptitudeLevel.A, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.EndCloser, race.AptitudeLevel.A, raceLen) - careerMod,
|
||||
race.inverseSpurtSpeed(speed, gutsStat, race.RunningStyle.Runaway, race.AptitudeLevel.A, raceLen) - careerMod,
|
||||
]);
|
||||
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([
|
||||
{ label: 'Aptitude S', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.S, raceLen) },
|
||||
{ label: 'Aptitude A', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.A, raceLen) },
|
||||
distanceApt < AptitudeLevel.A
|
||||
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) }
|
||||
const speedChart: Array<ComputedSeries | null> = $derived([
|
||||
{ label: 'Aptitude S', y: (x) => race.spurtSpeed(x, gutsStat, style, race.AptitudeLevel.S, raceLen) },
|
||||
{
|
||||
label: `Aptitude ${race.AptitudeLevel[distanceApt]} + Dueling`,
|
||||
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,
|
||||
]);
|
||||
|
||||
const range: [number, number] = $derived([
|
||||
spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen),
|
||||
spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen),
|
||||
]);
|
||||
const range: [number, number] = [20, 27.5];
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Spurt Speed Calculator</h1>
|
||||
@@ -71,16 +84,16 @@
|
||||
<div class="m-4">
|
||||
<label for="style">Style</label>
|
||||
<select id="style" required bind:value={style} class="w-full">
|
||||
{#each stylesList as [name, style] (style)}
|
||||
<option value={style}>{style === RunningStyle.GreatEscape ? 'Great Escape (Runaway)' : name}</option>
|
||||
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||
<option value={style}>{name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<label for="distanceApt">Distance Aptitude</label>
|
||||
<select id="distanceApt" required bind:value={distanceApt} class="w-full">
|
||||
{#each aptsList.toReversed() as [name, val] (val)}
|
||||
<option value={val}>{name}</option>
|
||||
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -93,24 +106,18 @@
|
||||
<input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="mt-8 block w-full text-center text-lg">
|
||||
Target spurt speed: {speed.toFixed(3)} m/s
|
||||
</span>
|
||||
{#each [[AptitudeLevel.A, aProf] as const, [AptitudeLevel.S, sProf] as const] as [level, inv] (level)}
|
||||
<div class="mx-auto mt-8 flex max-w-3xl flex-col md:flex-row">
|
||||
<CalcInfo title="Target Spurt Speed" class="flex-1">{speed.toFixed(3)} m/s</CalcInfo>
|
||||
<CalcInfo title="Dueling Bonus" class="flex-1">+{duelBonus.toFixed(3)} m/s</CalcInfo>
|
||||
</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">
|
||||
<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>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
{#each stylesList as [styleName, s] (s)}
|
||||
<div
|
||||
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 race.RUNNING_STYLES as [styleName, s] (s)}
|
||||
<CalcInfo title={styleName} class={['flex-1', { 'border-2': s === style }]}>{inv[s]}</CalcInfo>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,23 +126,19 @@
|
||||
<span class="mt-8 block w-full">
|
||||
While a speed skill is active, the equivalent speed for a distance S
|
||||
<select id="opponentStyle" required bind:value={opponentStyle}>
|
||||
{#each stylesList as [name, style] (style)}
|
||||
<option value={style}>{style === RunningStyle.GreatEscape ? 'Great Escape (Runaway)' : name}</option>
|
||||
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||
<option value={style}>{style === race.RunningStyle.Runaway ? 'Great Escape (Runaway)' : name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
is
|
||||
</span>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
{#each skillProf as [v, inv] (v)}
|
||||
<div class="m-2 flex-1 rounded-md border shadow-sm transition-shadow hover:shadow-md">
|
||||
<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>
|
||||
<CalcInfo title={`+${v.toFixed(2)}`} class="flex-1">{inv}</CalcInfo>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto h-60 max-w-3xl place-content-center py-4 md:h-96">
|
||||
<StatChart stat={Stat.Speed} {y} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} />
|
||||
<div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -9,19 +8,20 @@ export default defineConfig({
|
||||
test: {
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
{
|
||||
extends: './vite.config.ts',
|
||||
test: {
|
||||
name: 'client',
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium', headless: true }],
|
||||
},
|
||||
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/lib/server/**'],
|
||||
},
|
||||
},
|
||||
// Playwright tests are disabled because I don't feel like setting it up in NixOS.
|
||||
// {
|
||||
// extends: './vite.config.ts',
|
||||
// test: {
|
||||
// name: 'client',
|
||||
// browser: {
|
||||
// enabled: true,
|
||||
// provider: playwright(),
|
||||
// instances: [{ browser: 'chromium', headless: true }],
|
||||
// },
|
||||
// include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
// exclude: ['src/lib/server/**'],
|
||||
// },
|
||||
// },
|
||||
|
||||
{
|
||||
extends: './vite.config.ts',
|
||||
@@ -39,6 +39,9 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:13669',
|
||||
},
|
||||
'/img': {
|
||||
target: 'http://localhost:13669',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||