Compare commits
29 Commits
3e879f3687
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bd99cfaa6d | |||
| 3ab17cf9b0 | |||
| 4bd7962182 | |||
| 09f099171b | |||
| 4fff7069a8 | |||
| 3e2153b39c | |||
| d0fa6ab15c | |||
| 9f8024d488 | |||
| 1df3bc1db9 | |||
| a8c1b9c754 | |||
| 7600c48cc7 | |||
| 2e31560d6c | |||
| 2a07f193ec | |||
| b720b325b3 | |||
| 80573a84ea | |||
| bc94d66002 | |||
| aca5fccaa7 | |||
| ee6ced1390 | |||
| af8e5907b9 | |||
| 9cf9fd198f | |||
| 0c2db10082 | |||
| fae9c38098 | |||
| 0799bf658f | |||
| 2cec7c5699 | |||
| b10a2572ec | |||
| 657cf22f71 | |||
| dce58357ae | |||
| 7abe427c03 | |||
| 1a55150ddd |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "std"]
|
||||
path = std
|
||||
url = git@github.com:koka-community/std.git
|
||||
@@ -18,6 +18,7 @@ func _() {
|
||||
_ = x[AbilityHP-9]
|
||||
_ = x[AbilityGateDelay-10]
|
||||
_ = x[AbilityFrenzy-13]
|
||||
_ = x[AbilityAddGateDelay-14]
|
||||
_ = x[AbilityCurrentSpeed-21]
|
||||
_ = x[AbilityTargetSpeed-27]
|
||||
_ = x[AbilityLaneSpeed-28]
|
||||
@@ -28,7 +29,7 @@ func _() {
|
||||
const (
|
||||
_AbilityType_name_0 = "SpeedStaminaPowerGutsWitEnable Great Escape"
|
||||
_AbilityType_name_1 = "VisionHPGate delay multiplier"
|
||||
_AbilityType_name_2 = "Frenzy"
|
||||
_AbilityType_name_2 = "FrenzyAdded gate delay"
|
||||
_AbilityType_name_3 = "Current speed"
|
||||
_AbilityType_name_4 = "Target speedLane change speed"
|
||||
_AbilityType_name_5 = "Acceleration"
|
||||
@@ -38,6 +39,7 @@ const (
|
||||
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}
|
||||
)
|
||||
|
||||
@@ -49,8 +51,9 @@ func (i AbilityType) String() string {
|
||||
case 8 <= i && i <= 10:
|
||||
i -= 8
|
||||
return _AbilityType_name_1[_AbilityType_index_1[i]:_AbilityType_index_1[i+1]]
|
||||
case i == 13:
|
||||
return _AbilityType_name_2
|
||||
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:
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/disgoorg/disgo/rest"
|
||||
httpmiddle "github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||
"git.sunturtle.xyz/zephyr/horse"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -113,6 +113,7 @@ func main() {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /", httpmiddle.Compress(5)(http.FileServerFS(os.DirFS(public))))
|
||||
mux.Handle("GET /api/data/", httpmiddle.Compress(5)(http.StripPrefix("/api/data", http.FileServerFS(os.DirFS(dataDir)))))
|
||||
if pubkey != "" {
|
||||
pk, err := hex.DecodeString(pubkey)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/disgoorg/disgo/discord"
|
||||
"github.com/disgoorg/disgo/handler"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse"
|
||||
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
|
||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||
)
|
||||
|
||||
type skillServer struct {
|
||||
@@ -208,17 +208,15 @@ func (s *skillServer) render(id horse.SkillID) discord.ContainerComponent {
|
||||
for _, rs := range rel {
|
||||
name := rs.Name
|
||||
emoji := "⚪"
|
||||
switch rs.Rarity {
|
||||
case 1:
|
||||
if rs.UniqueOwner != "" {
|
||||
name += " (Inherited)"
|
||||
}
|
||||
case 2:
|
||||
switch {
|
||||
case rs.Rarity == 3, rs.Rarity == 4, rs.Rarity == 5:
|
||||
emoji = "🟠"
|
||||
case rs.UniqueOwner != "":
|
||||
name += " (Inherited)"
|
||||
case rs.Rarity == 2:
|
||||
emoji = "🟡"
|
||||
case 3, 4, 5:
|
||||
case rs.GroupRate == -1:
|
||||
emoji = "🟣"
|
||||
default:
|
||||
emoji = "⁉️"
|
||||
}
|
||||
b := discord.NewStringSelectMenuOption(name, strconv.Itoa(int(rs.ID))).WithEmoji(discord.NewComponentEmoji(emoji))
|
||||
if rs.ID == skill.ID {
|
||||
|
||||
@@ -15,12 +15,14 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"zombiezen.com/go/sqlite"
|
||||
"zombiezen.com/go/sqlite/sqlitex"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||
"git.sunturtle.xyz/zephyr/horse"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -170,6 +172,7 @@ func main() {
|
||||
},
|
||||
}),
|
||||
UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name
|
||||
Tags: parseTags(s.ColumnText(54)),
|
||||
SPCost: s.ColumnInt(49),
|
||||
IconID: s.ColumnInt(53),
|
||||
}
|
||||
@@ -395,6 +398,20 @@ type SparkEffImm struct {
|
||||
Value2 int32
|
||||
}
|
||||
|
||||
func parseTags(s string) []uint16 {
|
||||
r := make([]uint16, 0, 8)
|
||||
for s != "" {
|
||||
t, u, _ := strings.Cut(s, "/")
|
||||
s = u
|
||||
v, err := strconv.ParseUint(t, 10, 16)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parsing skill tags: %w", err))
|
||||
}
|
||||
r = append(r, uint16(v))
|
||||
}
|
||||
return trimZeros(r...)
|
||||
}
|
||||
|
||||
func trimAbilities(s []horse.Ability) []horse.Ability {
|
||||
for len(s) > 0 && s[len(s)-1].Type == 0 {
|
||||
s = s[:len(s)-1]
|
||||
|
||||
@@ -87,7 +87,7 @@ SELECT
|
||||
COALESCE(u.owner_id, iu.owner_id, 0) AS unique_owner_id,
|
||||
COALESCE(u.name, iu.name, '') AS unique_owner,
|
||||
d.icon_id,
|
||||
ROW_NUMBER() OVER (ORDER BY d.id) - 1 AS "index"
|
||||
d.tag_id
|
||||
FROM skill_data d
|
||||
JOIN skill_names n ON d.id = n.id
|
||||
LEFT JOIN skill_data ud ON d.unique_skill_id_1 = ud.id
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -ex
|
||||
|
||||
go run ./horsegen "$@"
|
||||
go generate ./horse/...
|
||||
go fmt ./...
|
||||
go test ./...
|
||||
18399
global/affinity.json
18399
global/affinity.json
File diff suppressed because it is too large
Load Diff
@@ -177,6 +177,16 @@
|
||||
"chara_2": 1004,
|
||||
"condition_type": 3
|
||||
},
|
||||
{
|
||||
"chara_id": 1004,
|
||||
"number": 6,
|
||||
"location": 430,
|
||||
"location_name": "center posters",
|
||||
"chara_1": 1046,
|
||||
"chara_2": 1031,
|
||||
"chara_3": 1004,
|
||||
"condition_type": 4
|
||||
},
|
||||
{
|
||||
"chara_id": 1005,
|
||||
"number": 1,
|
||||
@@ -1277,6 +1287,58 @@
|
||||
"chara_2": 1052,
|
||||
"condition_type": 2
|
||||
},
|
||||
{
|
||||
"chara_id": 1031,
|
||||
"number": 1,
|
||||
"location": 310,
|
||||
"location_name": "center back seat",
|
||||
"chara_1": 1031,
|
||||
"condition_type": 0
|
||||
},
|
||||
{
|
||||
"chara_id": 1031,
|
||||
"number": 2,
|
||||
"location": 110,
|
||||
"location_name": "right side front",
|
||||
"chara_1": 1031,
|
||||
"condition_type": 1
|
||||
},
|
||||
{
|
||||
"chara_id": 1031,
|
||||
"number": 3,
|
||||
"location": 510,
|
||||
"location_name": "left side school map",
|
||||
"chara_1": 1031,
|
||||
"condition_type": 1
|
||||
},
|
||||
{
|
||||
"chara_id": 1031,
|
||||
"number": 4,
|
||||
"location": 420,
|
||||
"location_name": "center posters",
|
||||
"chara_1": 1031,
|
||||
"chara_2": 1027,
|
||||
"condition_type": 2
|
||||
},
|
||||
{
|
||||
"chara_id": 1031,
|
||||
"number": 5,
|
||||
"location": 120,
|
||||
"location_name": "right side front",
|
||||
"chara_1": 1031,
|
||||
"chara_2": 1013,
|
||||
"condition_type": 2
|
||||
},
|
||||
{
|
||||
"chara_id": 1031,
|
||||
"number": 6,
|
||||
"location": 430,
|
||||
"location_name": "center posters",
|
||||
"chara_1": 1031,
|
||||
"chara_2": 1066,
|
||||
"chara_3": 1002,
|
||||
"condition_type": 2
|
||||
},
|
||||
{
|
||||
"chara_id": 1032,
|
||||
"number": 1,
|
||||
@@ -2197,6 +2259,48 @@
|
||||
"chara_3": 1048,
|
||||
"condition_type": 1
|
||||
},
|
||||
{
|
||||
"chara_id": 1064,
|
||||
"number": 1,
|
||||
"location": 310,
|
||||
"location_name": "center back seat",
|
||||
"chara_1": 1064,
|
||||
"condition_type": 0
|
||||
},
|
||||
{
|
||||
"chara_id": 1064,
|
||||
"number": 2,
|
||||
"location": 510,
|
||||
"location_name": "left side school map",
|
||||
"chara_1": 1064,
|
||||
"condition_type": 1
|
||||
},
|
||||
{
|
||||
"chara_id": 1064,
|
||||
"number": 3,
|
||||
"location": 410,
|
||||
"location_name": "center posters",
|
||||
"chara_1": 1064,
|
||||
"condition_type": 1
|
||||
},
|
||||
{
|
||||
"chara_id": 1064,
|
||||
"number": 4,
|
||||
"location": 120,
|
||||
"location_name": "right side front",
|
||||
"chara_1": 1065,
|
||||
"chara_2": 1064,
|
||||
"condition_type": 3
|
||||
},
|
||||
{
|
||||
"chara_id": 1064,
|
||||
"number": 5,
|
||||
"location": 520,
|
||||
"location_name": "left side school map",
|
||||
"chara_1": 1024,
|
||||
"chara_2": 1064,
|
||||
"condition_type": 3
|
||||
},
|
||||
{
|
||||
"chara_id": 1067,
|
||||
"number": 1,
|
||||
|
||||
@@ -212,6 +212,11 @@
|
||||
"skill1": 100301,
|
||||
"skill2": 900301
|
||||
},
|
||||
{
|
||||
"skill_group": 10031,
|
||||
"skill1": 100311,
|
||||
"skill2": 900311
|
||||
},
|
||||
{
|
||||
"skill_group": 10032,
|
||||
"skill1": 100321,
|
||||
@@ -312,6 +317,11 @@
|
||||
"skill1": 100621,
|
||||
"skill2": 900621
|
||||
},
|
||||
{
|
||||
"skill_group": 10064,
|
||||
"skill1": 100641,
|
||||
"skill2": 900641
|
||||
},
|
||||
{
|
||||
"skill_group": 10067,
|
||||
"skill1": 100671,
|
||||
@@ -402,6 +412,11 @@
|
||||
"skill1": 110201,
|
||||
"skill2": 910201
|
||||
},
|
||||
{
|
||||
"skill_group": 11022,
|
||||
"skill1": 110221,
|
||||
"skill2": 910221
|
||||
},
|
||||
{
|
||||
"skill_group": 11023,
|
||||
"skill1": 110231,
|
||||
@@ -427,6 +442,11 @@
|
||||
"skill1": 110371,
|
||||
"skill2": 910371
|
||||
},
|
||||
{
|
||||
"skill_group": 11038,
|
||||
"skill1": 110381,
|
||||
"skill2": 910381
|
||||
},
|
||||
{
|
||||
"skill_group": 11040,
|
||||
"skill1": 110401,
|
||||
@@ -447,6 +467,16 @@
|
||||
"skill1": 110561,
|
||||
"skill2": 910561
|
||||
},
|
||||
{
|
||||
"skill_group": 11060,
|
||||
"skill1": 110601,
|
||||
"skill2": 910601
|
||||
},
|
||||
{
|
||||
"skill_group": 11061,
|
||||
"skill1": 110611,
|
||||
"skill2": 910611
|
||||
},
|
||||
{
|
||||
"skill_group": 20001,
|
||||
"skill1": 200012,
|
||||
@@ -1277,7 +1307,8 @@
|
||||
},
|
||||
{
|
||||
"skill_group": 20166,
|
||||
"skill1": 201661
|
||||
"skill1": 201661,
|
||||
"skill2": 201662
|
||||
},
|
||||
{
|
||||
"skill_group": 20167,
|
||||
@@ -1360,6 +1391,26 @@
|
||||
"skill1": 202102,
|
||||
"skill2": 202101
|
||||
},
|
||||
{
|
||||
"skill_group": 20211,
|
||||
"skill1": 202112,
|
||||
"skill2": 202111
|
||||
},
|
||||
{
|
||||
"skill_group": 20212,
|
||||
"skill1": 202122,
|
||||
"skill2": 202121
|
||||
},
|
||||
{
|
||||
"skill_group": 20213,
|
||||
"skill1": 202132,
|
||||
"skill2": 202131
|
||||
},
|
||||
{
|
||||
"skill_group": 20215,
|
||||
"skill1": 202152,
|
||||
"skill2": 202151
|
||||
},
|
||||
{
|
||||
"skill_group": 21001,
|
||||
"skill1": 210012,
|
||||
@@ -1583,6 +1634,11 @@
|
||||
"skill1": 100301,
|
||||
"skill2": 900301
|
||||
},
|
||||
{
|
||||
"skill_group": 90031,
|
||||
"skill1": 100311,
|
||||
"skill2": 900311
|
||||
},
|
||||
{
|
||||
"skill_group": 90032,
|
||||
"skill1": 100321,
|
||||
@@ -1683,6 +1739,11 @@
|
||||
"skill1": 100621,
|
||||
"skill2": 900621
|
||||
},
|
||||
{
|
||||
"skill_group": 90064,
|
||||
"skill1": 100641,
|
||||
"skill2": 900641
|
||||
},
|
||||
{
|
||||
"skill_group": 90067,
|
||||
"skill1": 100671,
|
||||
@@ -1773,6 +1834,11 @@
|
||||
"skill1": 110201,
|
||||
"skill2": 910201
|
||||
},
|
||||
{
|
||||
"skill_group": 91022,
|
||||
"skill1": 110221,
|
||||
"skill2": 910221
|
||||
},
|
||||
{
|
||||
"skill_group": 91023,
|
||||
"skill1": 110231,
|
||||
@@ -1798,6 +1864,11 @@
|
||||
"skill1": 110371,
|
||||
"skill2": 910371
|
||||
},
|
||||
{
|
||||
"skill_group": 91038,
|
||||
"skill1": 110381,
|
||||
"skill2": 910381
|
||||
},
|
||||
{
|
||||
"skill_group": 91040,
|
||||
"skill1": 110401,
|
||||
@@ -1818,6 +1889,16 @@
|
||||
"skill1": 110561,
|
||||
"skill2": 910561
|
||||
},
|
||||
{
|
||||
"skill_group": 91060,
|
||||
"skill1": 110601,
|
||||
"skill2": 910601
|
||||
},
|
||||
{
|
||||
"skill_group": 91061,
|
||||
"skill1": 110611,
|
||||
"skill2": 910611
|
||||
},
|
||||
{
|
||||
"skill_group": 100001,
|
||||
"skill1": 1000011
|
||||
@@ -1825,5 +1906,9 @@
|
||||
{
|
||||
"skill_group": 100001,
|
||||
"skill1": 1000012
|
||||
},
|
||||
{
|
||||
"skill_group": 110001,
|
||||
"skill1": 1100011
|
||||
}
|
||||
]
|
||||
|
||||
3012
global/skill.json
3012
global/skill.json
File diff suppressed because it is too large
Load Diff
1098
global/spark.json
1098
global/spark.json
File diff suppressed because it is too large
Load Diff
144
global/uma.json
144
global/uma.json
@@ -815,6 +815,30 @@
|
||||
"skill_pl4": 200012,
|
||||
"skill_pl5": 201341
|
||||
},
|
||||
{
|
||||
"chara_card_id": 102202,
|
||||
"chara_id": 1022,
|
||||
"name": "[Titania] Fine Motion",
|
||||
"variant": "[Titania]",
|
||||
"sprint": 0,
|
||||
"mile": 7,
|
||||
"medium": 7,
|
||||
"long": 5,
|
||||
"front": 4,
|
||||
"pace": 7,
|
||||
"late": 3,
|
||||
"end": 5,
|
||||
"turf": 7,
|
||||
"dirt": 1,
|
||||
"unique": 110221,
|
||||
"skill1": 200152,
|
||||
"skill2": 201902,
|
||||
"skill3": 201052,
|
||||
"skill_pl2": 201042,
|
||||
"skill_pl3": 201051,
|
||||
"skill_pl4": 200192,
|
||||
"skill_pl5": 201901
|
||||
},
|
||||
{
|
||||
"chara_card_id": 102301,
|
||||
"chara_id": 1023,
|
||||
@@ -1079,6 +1103,30 @@
|
||||
"skill_pl4": 200851,
|
||||
"skill_pl5": 200772
|
||||
},
|
||||
{
|
||||
"chara_card_id": 103101,
|
||||
"chara_id": 1031,
|
||||
"name": "[Always Electrifying] Ines Fujin",
|
||||
"variant": "[Always Electrifying]",
|
||||
"sprint": 0,
|
||||
"mile": 7,
|
||||
"medium": 7,
|
||||
"long": 5,
|
||||
"front": 7,
|
||||
"pace": 5,
|
||||
"late": 1,
|
||||
"end": 1,
|
||||
"turf": 7,
|
||||
"dirt": 1,
|
||||
"unique": 100311,
|
||||
"skill1": 201651,
|
||||
"skill2": 201282,
|
||||
"skill3": 202132,
|
||||
"skill_pl2": 200032,
|
||||
"skill_pl3": 201281,
|
||||
"skill_pl4": 201082,
|
||||
"skill_pl5": 202131
|
||||
},
|
||||
{
|
||||
"chara_card_id": 103201,
|
||||
"chara_id": 1032,
|
||||
@@ -1223,6 +1271,30 @@
|
||||
"skill_pl4": 200582,
|
||||
"skill_pl5": 201011
|
||||
},
|
||||
{
|
||||
"chara_card_id": 103802,
|
||||
"chara_id": 1038,
|
||||
"name": "[Ma Chérie of the New Moon] Curren Chan",
|
||||
"variant": "[Ma Chérie of the New Moon]",
|
||||
"sprint": 0,
|
||||
"mile": 4,
|
||||
"medium": 1,
|
||||
"long": 1,
|
||||
"front": 6,
|
||||
"pace": 7,
|
||||
"late": 3,
|
||||
"end": 1,
|
||||
"turf": 7,
|
||||
"dirt": 2,
|
||||
"unique": 110381,
|
||||
"skill1": 200851,
|
||||
"skill2": 201322,
|
||||
"skill3": 201012,
|
||||
"skill_pl2": 200652,
|
||||
"skill_pl3": 201011,
|
||||
"skill_pl4": 201532,
|
||||
"skill_pl5": 200651
|
||||
},
|
||||
{
|
||||
"chara_card_id": 103901,
|
||||
"chara_id": 1039,
|
||||
@@ -1631,6 +1703,30 @@
|
||||
"skill_pl4": 201422,
|
||||
"skill_pl5": 201441
|
||||
},
|
||||
{
|
||||
"chara_card_id": 106002,
|
||||
"chara_id": 1060,
|
||||
"name": "[Run & Win] Nice Nature",
|
||||
"variant": "[Run & Win]",
|
||||
"sprint": 0,
|
||||
"mile": 5,
|
||||
"medium": 7,
|
||||
"long": 7,
|
||||
"front": 2,
|
||||
"pace": 6,
|
||||
"late": 7,
|
||||
"end": 4,
|
||||
"turf": 7,
|
||||
"dirt": 1,
|
||||
"unique": 110601,
|
||||
"skill1": 200492,
|
||||
"skill2": 201152,
|
||||
"skill3": 201542,
|
||||
"skill_pl2": 202082,
|
||||
"skill_pl3": 201151,
|
||||
"skill_pl4": 200302,
|
||||
"skill_pl5": 200491
|
||||
},
|
||||
{
|
||||
"chara_card_id": 106101,
|
||||
"chara_id": 1061,
|
||||
@@ -1655,6 +1751,30 @@
|
||||
"skill_pl4": 201072,
|
||||
"skill_pl5": 201431
|
||||
},
|
||||
{
|
||||
"chara_card_id": 106102,
|
||||
"chara_id": 1061,
|
||||
"name": "[Cheerleader in Noble White] King Halo",
|
||||
"variant": "[Cheerleader in Noble White]",
|
||||
"sprint": 0,
|
||||
"mile": 6,
|
||||
"medium": 6,
|
||||
"long": 5,
|
||||
"front": 1,
|
||||
"pace": 6,
|
||||
"late": 7,
|
||||
"end": 4,
|
||||
"turf": 7,
|
||||
"dirt": 1,
|
||||
"unique": 110611,
|
||||
"skill1": 200172,
|
||||
"skill2": 200672,
|
||||
"skill3": 200612,
|
||||
"skill_pl2": 200692,
|
||||
"skill_pl3": 200611,
|
||||
"skill_pl4": 201382,
|
||||
"skill_pl5": 200174
|
||||
},
|
||||
{
|
||||
"chara_card_id": 106201,
|
||||
"chara_id": 1062,
|
||||
@@ -1679,6 +1799,30 @@
|
||||
"skill_pl4": 200512,
|
||||
"skill_pl5": 201211
|
||||
},
|
||||
{
|
||||
"chara_card_id": 106401,
|
||||
"chara_id": 1064,
|
||||
"name": "[Line Breakthrough] Mejiro Palmer",
|
||||
"variant": "[Line Breakthrough]",
|
||||
"sprint": 0,
|
||||
"mile": 2,
|
||||
"medium": 7,
|
||||
"long": 7,
|
||||
"front": 7,
|
||||
"pace": 3,
|
||||
"late": 2,
|
||||
"end": 1,
|
||||
"turf": 7,
|
||||
"dirt": 1,
|
||||
"unique": 100641,
|
||||
"skill1": 201661,
|
||||
"skill2": 201242,
|
||||
"skill3": 201192,
|
||||
"skill_pl2": 200532,
|
||||
"skill_pl3": 201662,
|
||||
"skill_pl4": 200302,
|
||||
"skill_pl5": 201191
|
||||
},
|
||||
{
|
||||
"chara_card_id": 106701,
|
||||
"chara_id": 1067,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# horse
|
||||
|
||||
This directory contains manually written code and types on which the generated code depends.
|
||||
|
||||
The generated code is in ./global; other regions will follow the same convention once they are supported.
|
||||
It is always safe to delete the entire directories and regenerate them.
|
||||
@@ -1,17 +0,0 @@
|
||||
module horse/character
|
||||
|
||||
import horse/game-id
|
||||
|
||||
pub struct character-detail
|
||||
character-id: character-id
|
||||
name: string
|
||||
|
||||
pub fun detail(
|
||||
c: character-id,
|
||||
?character/show: (character-id) -> string
|
||||
): character-detail
|
||||
Character-detail(c, c.show)
|
||||
|
||||
pub fun character-detail/show(d: character-detail): string
|
||||
val Character-detail(Character-id(id), name) = d
|
||||
name ++ " (ID " ++ id.show ++ ")"
|
||||
@@ -1,80 +0,0 @@
|
||||
module horse/game-id
|
||||
|
||||
// Game ID for characters, cards, skills, races, &c.
|
||||
// Values for different categories may overlap.
|
||||
pub alias game-id = int
|
||||
|
||||
// Specific game ID types.
|
||||
// I've already made mistakes with ID categories and I haven't even committed this file yet.
|
||||
|
||||
pub struct scenario-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for characters.
|
||||
// Generally numbers in the range 1000-9999.
|
||||
pub struct character-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for trainees, i.e. costume instances of characters.
|
||||
// Generally a character ID with two digits appended.
|
||||
pub struct uma-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for skills.
|
||||
pub struct skill-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for skill groups.
|
||||
pub struct skill-group-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for skill icons.
|
||||
pub struct skill-icon-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for races,
|
||||
// i.e. "Tenno Sho (Spring)" and not "Tenno Sho (Spring) at Kyoto Racecourse."
|
||||
pub struct race-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for race thumbnails.
|
||||
pub struct race-thumbnail-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for saddles,
|
||||
// i.e. one or more race wins that appear as a title.
|
||||
pub struct saddle-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for sparks,
|
||||
// i.e. succession factors.
|
||||
pub struct spark-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for spark groups,
|
||||
// i.e. all rarities (star counts) of a single spark.
|
||||
pub struct spark-group-id
|
||||
game-id: game-id
|
||||
|
||||
// order2 comparison between any game ID types.
|
||||
pub inline fun order2(x: a, y: a, ?a/game-id: (a) -> game-id): order2<a>
|
||||
match x.game-id.cmp(y.game-id)
|
||||
Lt -> Lt2(x, y)
|
||||
Eq -> Eq2(x)
|
||||
Gt -> Gt2(x, y)
|
||||
|
||||
// Comparison between any game ID types.
|
||||
pub inline fun cmp(x: a, y: a, ?a/game-id: (a) -> game-id): order
|
||||
x.game-id.cmp(y.game-id)
|
||||
|
||||
// Equality between any game ID types.
|
||||
pub inline fun (==)(x: a, y: a, ?a/game-id: (a) -> game-id): bool
|
||||
x.game-id == y.game-id
|
||||
|
||||
// Check whether a game ID is valid, i.e. nonzero.
|
||||
pub inline fun is-valid(x: a, ?a/game-id: (a) -> game-id): bool
|
||||
x.game-id != 0
|
||||
|
||||
// Construct an invalid game ID.
|
||||
pub inline fun default/game-id(): game-id
|
||||
0
|
||||
@@ -1,8 +0,0 @@
|
||||
module horse/global
|
||||
|
||||
import horse/game-id
|
||||
|
||||
// Shared saddle affinity bonus.
|
||||
// `s` should be the complete list of all saddles shared between the veterans.
|
||||
pub fun saddle-bonus(s: list<saddle-id>): int
|
||||
s.length
|
||||
124
horse/legacy.kk
124
horse/legacy.kk
@@ -1,124 +0,0 @@
|
||||
module horse/legacy
|
||||
|
||||
import std/num/decimal
|
||||
import std/data/linearmap
|
||||
import std/data/linearset
|
||||
import horse/game-id
|
||||
import horse/spark
|
||||
import horse/prob/dist
|
||||
|
||||
// A legacy, or parent and grandparents.
|
||||
pub struct legacy
|
||||
uma: veteran
|
||||
sub1: veteran
|
||||
sub2: veteran
|
||||
|
||||
// A veteran, or the result of a completed career.
|
||||
pub struct veteran
|
||||
uma: uma-id
|
||||
sparks: list<spark-id>
|
||||
saddles: list<saddle-id>
|
||||
|
||||
// Get all saddles shared between two lists thereof.
|
||||
pub fun shared-saddles(a: list<saddle-id>, b: list<saddle-id>): list<saddle-id>
|
||||
val sa: linearSet<saddle-id> = a.foldl(linear-set(Nil)) fn(s, id) if id.is-valid then s.add(id) else s
|
||||
val c: linearSet<saddle-id> = b.foldl(linear-set(Nil)) fn(s, id) if sa.member(id) then s.add(id) else s
|
||||
c.list
|
||||
|
||||
// Get the individual affinity for a legacy.
|
||||
// Any invalid ID is treated as giving 0.
|
||||
pub fun parent-affinity(
|
||||
trainee: uma-id,
|
||||
legacy: legacy,
|
||||
other-parent: uma-id,
|
||||
?character-id: (uma-id) -> character-id,
|
||||
?saddle-bonus: (list<saddle-id>) -> int,
|
||||
?pair-affinity: (a: character-id, b: character-id) -> int,
|
||||
?trio-affinity: (a: character-id, b: character-id, c: character-id) -> int
|
||||
): int
|
||||
val t = trainee.character-id
|
||||
val p1 = legacy.uma.uma.character-id
|
||||
val s1 = legacy.sub1.uma.character-id
|
||||
val s2 = legacy.sub2.uma.character-id
|
||||
val p2 = other-parent.character-id
|
||||
pair-affinity(t, p1) + pair-affinity(p1, p2)
|
||||
+ trio-affinity(t, p1, s1) + trio-affinity(t, p1, s2)
|
||||
+ saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub1.saddles)) + saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub2.saddles))
|
||||
|
||||
// Get the individual affinities for a legacy's sub-legacies.
|
||||
// The first value is the legacy for the `legacy.sub1` and the second is for
|
||||
// `legacy.sub2`.
|
||||
// Any invalid ID is treated as giving 0.
|
||||
pub fun sub-affinity(
|
||||
trainee: uma-id,
|
||||
legacy: legacy,
|
||||
?character-id: (uma-id) -> character-id,
|
||||
?saddle-bonus: (list<saddle-id>) -> int,
|
||||
?trio-affinity: (a: character-id, b: character-id, c: character-id) -> int
|
||||
): (int, int)
|
||||
val t = trainee.character-id
|
||||
val p = legacy.uma.uma.character-id
|
||||
val s1 = legacy.sub1.uma.character-id
|
||||
val s2 = legacy.sub2.uma.character-id
|
||||
val r1 = trio-affinity(t, p, s1) + saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub1.saddles))
|
||||
val r2 = trio-affinity(t, p, s2) + saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub2.saddles))
|
||||
(r1, r2)
|
||||
|
||||
// Associate each spark with its actual chance to activate given an individual
|
||||
// affinity value and the possible effects when it does.
|
||||
pub fun uma/inspiration(l: list<spark-id>, affinity: int, ?spark-type: (spark-id) -> spark-type, ?rarity: (spark-id) -> rarity, ?effects: (spark-id) -> list<list<spark-effect>>): list<(spark-id, decimal, list<list<spark-effect>>)>
|
||||
val a = decimal(1 + affinity, -2)
|
||||
l.map() fn(id) (id, min(id.base-proc * a, 1.decimal), id.effects)
|
||||
|
||||
// Get the complete list of effects that may occur in an inspiration event
|
||||
// and the respective probability of activation.
|
||||
// Duplicates, i.e. multiple veterans with the same spark, are preserved.
|
||||
pub fun inspiration(
|
||||
trainee: uma-id,
|
||||
parent1: legacy,
|
||||
parent2: legacy,
|
||||
?character-id: (uma-id) -> character-id,
|
||||
?saddle-bonus: (list<saddle-id>) -> int,
|
||||
?pair-affinity: (a: character-id, b: character-id) -> int,
|
||||
?trio-affinity: (a: character-id, b: character-id, c: character-id) -> int,
|
||||
?spark-type: (spark-id) -> spark-type,
|
||||
?rarity: (spark-id) -> rarity,
|
||||
?effects: (spark-id) -> list<list<spark-effect>>
|
||||
): list<(spark-id, decimal, list<list<spark-effect>>)>
|
||||
val p1a = parent-affinity(trainee, parent1, parent2.uma.uma)
|
||||
val p2a = parent-affinity(trainee, parent2, parent1.uma.uma)
|
||||
val (s11a, s12a) = sub-affinity(trainee, parent1)
|
||||
val (s21a, s22a) = sub-affinity(trainee, parent2)
|
||||
[
|
||||
inspiration(parent1.uma.sparks, p1a),
|
||||
inspiration(parent1.sub1.sparks, s11a),
|
||||
inspiration(parent1.sub2.sparks, s12a),
|
||||
inspiration(parent2.uma.sparks, p2a),
|
||||
inspiration(parent2.sub1.sparks, s21a),
|
||||
inspiration(parent2.sub2.sparks, s22a),
|
||||
].concat
|
||||
|
||||
// Reduce a spark effect list to the skill it is able to give.
|
||||
pub fun skills(l: list<list<spark-effect>>): maybe<skill-id>
|
||||
val r: linearSet<skill-id> = l.head(Nil).foldl(linear-set(Nil)) fn(s, eff)
|
||||
match eff
|
||||
Skill-Hint(id, _) -> s + id
|
||||
_ -> s
|
||||
r.list.head
|
||||
|
||||
// Reduce a spark effect list to the aptitude it is able to give.
|
||||
pub fun aptitudes(l: list<list<spark-effect>>): maybe<aptitude>
|
||||
val r: linearSet<aptitude> = l.head(Nil).foldl(linear-set(Nil)) fn(s, eff)
|
||||
match eff
|
||||
Aptitude-Up(apt) -> s + apt
|
||||
_ -> s
|
||||
r.list.head
|
||||
|
||||
// Get the overall chance of each count of sparks, including zero, providing a
|
||||
// given type of effect activating in a single inspiration event.
|
||||
pub fun inspiration-gives(l: list<(spark-id, decimal, list<list<spark-effect>>)>, f: (list<list<spark-effect>>) -> maybe<a>, ?a/(==): (a, a) -> bool): linearMap<a, list<decimal>>
|
||||
val m: linearMap<_, list<decimal>> = l.foldl(LinearMap(Nil)) fn(m, (_, p, eff))
|
||||
match f(eff)
|
||||
Nothing -> m
|
||||
Just(a) -> m.map/update(a, [p]) fn(cur, pp) pp.append(cur)
|
||||
m.map() fn(_, v) poisson-binomial(v)
|
||||
@@ -1,134 +0,0 @@
|
||||
module horse/movement
|
||||
|
||||
// Surface types.
|
||||
pub type surface
|
||||
Turf
|
||||
Dirt
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `surface` type.
|
||||
pub fun surface/show(this : surface) : e string
|
||||
match this
|
||||
Turf -> "Turf"
|
||||
Dirt -> "Dirt"
|
||||
|
||||
// Race distance types.
|
||||
pub type distance
|
||||
Sprint
|
||||
Mile
|
||||
Medium
|
||||
Long
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `distance` type.
|
||||
pub fun distance/show(this : distance) : e string
|
||||
match this
|
||||
Sprint -> "Sprint"
|
||||
Mile -> "Mile"
|
||||
Medium -> "Medium"
|
||||
Long -> "Long"
|
||||
|
||||
// Running styles.
|
||||
pub type style
|
||||
Front-Runner
|
||||
Pace-Chaser
|
||||
Late-Surger
|
||||
End-Closer
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `style` type.
|
||||
pub fun style/(==)(this : style, other : style) : e bool
|
||||
match (this, other)
|
||||
(Front-Runner, Front-Runner) -> True
|
||||
(Pace-Chaser, Pace-Chaser) -> True
|
||||
(Late-Surger, Late-Surger) -> True
|
||||
(End-Closer, End-Closer) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Shows a string representation of the `style` type.
|
||||
pub fun style/show(this : style) : e string
|
||||
match this
|
||||
Front-Runner -> "Front Runner"
|
||||
Pace-Chaser -> "Pace Chaser"
|
||||
Late-Surger -> "Late Surger"
|
||||
End-Closer -> "End Closer"
|
||||
|
||||
// Aptitude levels.
|
||||
pub type aptitude-level
|
||||
G
|
||||
F
|
||||
E
|
||||
D
|
||||
C
|
||||
B
|
||||
A
|
||||
S
|
||||
|
||||
// Get the integer value for an aptitude level, starting at G -> 1.
|
||||
pub fun aptitude-level/int(l: aptitude-level): int
|
||||
match l
|
||||
G -> 1
|
||||
F -> 2
|
||||
E -> 3
|
||||
D -> 4
|
||||
C -> 5
|
||||
B -> 6
|
||||
A -> 7
|
||||
S -> 8
|
||||
|
||||
// Get the aptitude level corresponding to an integer, starting at 1 -> G.
|
||||
pub fun int/aptitude-level(l: int): maybe<aptitude-level>
|
||||
match l
|
||||
1 -> Just(G)
|
||||
2 -> Just(F)
|
||||
3 -> Just(E)
|
||||
4 -> Just(D)
|
||||
5 -> Just(C)
|
||||
6 -> Just(B)
|
||||
7 -> Just(A)
|
||||
8 -> Just(S)
|
||||
_ -> Nothing
|
||||
|
||||
// Comparison of the `aptitude-level` type.
|
||||
pub fun aptitude-level/cmp(this : aptitude-level, other : aptitude-level) : e order
|
||||
cmp(this.int, other.int)
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `aptitude-level` type.
|
||||
pub fun aptitude-level/order2(this : aptitude-level, other : aptitude-level) : order2<aptitude-level>
|
||||
match (this, other)
|
||||
(G, G) -> Eq2(G)
|
||||
(G, other') -> Lt2(G, other')
|
||||
(this', G) -> Gt2(G, this')
|
||||
(F, F) -> Eq2(F)
|
||||
(F, other') -> Lt2(F, other')
|
||||
(this', F) -> Gt2(F, this')
|
||||
(E, E) -> Eq2(E)
|
||||
(E, other') -> Lt2(E, other')
|
||||
(this', E) -> Gt2(E, this')
|
||||
(D, D) -> Eq2(D)
|
||||
(D, other') -> Lt2(D, other')
|
||||
(this', D) -> Gt2(D, this')
|
||||
(C, C) -> Eq2(C)
|
||||
(C, other') -> Lt2(C, other')
|
||||
(this', C) -> Gt2(C, this')
|
||||
(B, B) -> Eq2(B)
|
||||
(B, other') -> Lt2(B, other')
|
||||
(this', B) -> Gt2(B, this')
|
||||
(A, A) -> Eq2(A)
|
||||
(A, other') -> Lt2(A, other')
|
||||
(this', A) -> Gt2(A, this')
|
||||
(S, S) -> Eq2(S)
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `aptitude-level` type.
|
||||
pub fun aptitude-level/show(this : aptitude-level) : string
|
||||
match this
|
||||
G -> "G"
|
||||
F -> "F"
|
||||
E -> "E"
|
||||
D -> "D"
|
||||
C -> "C"
|
||||
B -> "B"
|
||||
A -> "A"
|
||||
S -> "S"
|
||||
@@ -1,21 +0,0 @@
|
||||
module horse/prob/dist
|
||||
|
||||
import std/num/decimal
|
||||
|
||||
tail fun pb-step(pn: list<decimal>, pi: decimal, pmfkm1: decimal, pmf: list<decimal>, next: ctx<list<decimal>>): list<decimal>
|
||||
match pn
|
||||
Nil -> next ++. Nil // final step overall
|
||||
Cons(_, pp) -> match pmf
|
||||
Cons(pmfk, pmf') ->
|
||||
val next' = next ++ ctx Cons(pi * pmfkm1 + (1.decimal - pi) * pmfk, hole)
|
||||
pb-step(pp, pi, pmfk, pmf', next')
|
||||
Nil -> next ++. Cons(pi * pmfkm1, Nil) // last step of this iteration
|
||||
|
||||
// Given `n` different Bernoulli processes with respective probabilities in `pn`,
|
||||
// find the distribution of `k` successes for `k` ranging from 0 to `n` inclusive.
|
||||
// The index in the result list corresponds to `k`.
|
||||
pub fun pmf/poisson-binomial(pn: list<decimal>): list<decimal>
|
||||
pn.foldl([1.decimal]) fn(pmf, pi)
|
||||
match pmf
|
||||
Cons(pmf0, pmf') -> pb-step(pn, pi, pmf0, pmf', ctx Cons((1.decimal - pi) * pmf0, hole))
|
||||
Nil -> impossible("fold started with non-empty pmf but got empty pmf")
|
||||
@@ -1,158 +0,0 @@
|
||||
module horse/prob/kfl
|
||||
|
||||
// kfl is a semiring of probabilities formed by vibes.
|
||||
pub type kfl
|
||||
// Effectively if not literally impossible events.
|
||||
Impossible
|
||||
// Not worth aiming for, but can technically still happen.
|
||||
Probably-Not
|
||||
// You expect it not to happen most of the time, but it might still be worth
|
||||
// trying for it if you're being forced to play to your outs.
|
||||
Doubtful
|
||||
// More likely that it won't happen, but a success isn't surprising.
|
||||
Unlikely
|
||||
// Either it does or it doesn't.
|
||||
Mayhapsibly
|
||||
// Decent chance it doesn't happen, but you still expect it to.
|
||||
Probably
|
||||
// You expect it to happen most of the time, but accept that there will be failures.
|
||||
Most-Likely
|
||||
// Very close to guaranteed, but technically with a small chance to fail.
|
||||
Cry-If-Not
|
||||
// Absolutely guaranteed events.
|
||||
Guaranteed
|
||||
|
||||
// Automatically generated.
|
||||
// Comparison of the `kfl` type.
|
||||
pub fun cmp(this : kfl, other : kfl) : e order
|
||||
match (this, other)
|
||||
(Impossible, Impossible) -> Eq
|
||||
(Impossible, _) -> Lt
|
||||
(_, Impossible) -> Gt
|
||||
(Probably-Not, Probably-Not) -> Eq
|
||||
(Probably-Not, _) -> Lt
|
||||
(_, Probably-Not) -> Gt
|
||||
(Doubtful, Doubtful) -> Eq
|
||||
(Doubtful, _) -> Lt
|
||||
(_, Doubtful) -> Gt
|
||||
(Unlikely, Unlikely) -> Eq
|
||||
(Unlikely, _) -> Lt
|
||||
(_, Unlikely) -> Gt
|
||||
(Mayhapsibly, Mayhapsibly) -> Eq
|
||||
(Mayhapsibly, _) -> Lt
|
||||
(_, Mayhapsibly) -> Gt
|
||||
(Probably, Probably) -> Eq
|
||||
(Probably, _) -> Lt
|
||||
(_, Probably) -> Gt
|
||||
(Most-Likely, Most-Likely) -> Eq
|
||||
(Most-Likely, _) -> Lt
|
||||
(_, Most-Likely) -> Gt
|
||||
(Cry-If-Not, Cry-If-Not) -> Eq
|
||||
(Cry-If-Not, _) -> Lt
|
||||
(_, Cry-If-Not) -> Gt
|
||||
(Guaranteed, Guaranteed) -> Eq
|
||||
|
||||
// Shows a string representation of the `kfl` type.
|
||||
pub fun show(this : kfl) : e string
|
||||
match this
|
||||
Impossible -> "impossible"
|
||||
Probably-Not -> "probably not"
|
||||
Doubtful -> "doubtful"
|
||||
Unlikely -> "unlikely"
|
||||
Mayhapsibly -> "mayhapsibly"
|
||||
Probably -> "probably"
|
||||
Most-Likely -> "most likely"
|
||||
Cry-If-Not -> "cry if not"
|
||||
Guaranteed -> "guaranteed"
|
||||
|
||||
// KFL multiplication, or the probability of cooccurrence of two independent events.
|
||||
pub fun (*)(a: kfl, b: kfl): e kfl
|
||||
val (l, h) = match a.cmp(b) // this operation is commutative
|
||||
Gt -> (b, a)
|
||||
_ -> (a, b)
|
||||
match (l, h)
|
||||
(r, Guaranteed) -> r // factor out Guaranteed cases
|
||||
(Impossible, _) -> Impossible
|
||||
(Probably-Not, _) -> Impossible
|
||||
(r, Cry-If-Not) -> r // factor out further Cry-If-Not cases
|
||||
(Doubtful, Most-Likely) -> Probably-Not
|
||||
(Doubtful, _) -> Impossible
|
||||
(Unlikely, Most-Likely) -> Doubtful
|
||||
(Unlikely, Probably) -> Doubtful
|
||||
(Unlikely, Mayhapsibly) -> Probably-Not
|
||||
(Unlikely, _) -> Probably-Not // (Unlikely, Unlikely) because commutative
|
||||
(Mayhapsibly, Most-Likely) -> Unlikely
|
||||
(Mayhapsibly, Probably) -> Unlikely
|
||||
(Mayhapsibly, _) -> Unlikely
|
||||
(Probably, Most-Likely) -> Mayhapsibly
|
||||
(Probably, _) -> Unlikely
|
||||
(Most-Likely, _) -> Probably
|
||||
// These two are only needed because the type system doesn't understand commutativity.
|
||||
(Cry-If-Not, _) -> Cry-If-Not
|
||||
(Guaranteed, _) -> Guaranteed
|
||||
|
||||
// KFL addition, or the probability of occurrence of at least one of two independent events.
|
||||
pub fun (+)(a: kfl, b: kfl): e kfl
|
||||
val (l, h) = match a.cmp(b) // this operation is commutative
|
||||
Gt -> (b, a)
|
||||
_ -> (a, b)
|
||||
match (l, h)
|
||||
// Cases with _ on the right are (a, a) due to commutativity.
|
||||
// Cases with _ on the left simplify later cases that all absorb to the right.
|
||||
(Guaranteed, _) -> Guaranteed
|
||||
(_, Guaranteed) -> Guaranteed
|
||||
(Cry-If-Not, _) -> Guaranteed
|
||||
(Most-Likely, Cry-If-Not) -> Cry-If-Not
|
||||
(Most-Likely, _) -> Cry-If-Not
|
||||
(_, Cry-If-Not) -> Cry-If-Not
|
||||
(Probably, Most-Likely) -> Cry-If-Not
|
||||
(Probably, _) -> Most-Likely
|
||||
(_, Most-Likely) -> Most-Likely
|
||||
(Mayhapsibly, Probably) -> Most-Likely
|
||||
(Mayhapsibly, _) -> Probably
|
||||
(Unlikely, Probably) -> Most-Likely
|
||||
(Unlikely, Mayhapsibly) -> Probably
|
||||
(Unlikely, _) -> Mayhapsibly
|
||||
(_, Probably) -> Probably
|
||||
(Doubtful, Mayhapsibly) -> Probably
|
||||
(Doubtful, Unlikely) -> Mayhapsibly
|
||||
(Doubtful, _) -> Unlikely
|
||||
(_, Mayhapsibly) -> Mayhapsibly
|
||||
(_, Unlikely) -> Unlikely
|
||||
(Probably-Not, Doubtful) -> Unlikely
|
||||
(Probably-Not, _) -> Probably-Not
|
||||
(_, Doubtful) -> Doubtful
|
||||
(_, Probably-Not) -> Probably-Not
|
||||
(_, Impossible) -> Impossible
|
||||
|
||||
// KFL union, or the probability of occurrence of exactly one of two independent events.
|
||||
pub fun either(a: kfl, b: kfl): e kfl
|
||||
val (l, h) = match a.cmp(b) // this operation is commutative
|
||||
Gt -> (b, a)
|
||||
_ -> (a, b)
|
||||
match (l, h)
|
||||
(Impossible, r) -> r
|
||||
(Probably-Not, Guaranteed) -> Cry-If-Not
|
||||
(Probably-Not, r) -> r
|
||||
(Doubtful, Guaranteed) -> Most-Likely
|
||||
(Doubtful, Cry-If-Not) -> Most-Likely
|
||||
(Doubtful, Most-Likely) -> Probably
|
||||
(Doubtful, Probably) -> Mayhapsibly
|
||||
(Doubtful, Mayhapsibly) -> Mayhapsibly
|
||||
(Doubtful, Unlikely) -> Mayhapsibly
|
||||
(Doubtful, _) -> Unlikely
|
||||
(Unlikely, Guaranteed) -> Probably
|
||||
(Unlikely, Cry-If-Not) -> Mayhapsibly
|
||||
(Unlikely, Most-Likely) -> Mayhapsibly
|
||||
(Unlikely, _) -> Probably
|
||||
(Mayhapsibly, Guaranteed) -> Mayhapsibly
|
||||
(Mayhapsibly, Cry-If-Not) -> Mayhapsibly
|
||||
(Mayhapsibly, Most-Likely) -> Mayhapsibly
|
||||
(Mayhapsibly, _) -> Probably
|
||||
(Probably, Guaranteed) -> Unlikely
|
||||
(Probably, Cry-If-Not) -> Unlikely
|
||||
(Probably, Most-Likely) -> Unlikely
|
||||
(Probably, _) -> Mayhapsibly
|
||||
(Most-Likely, _) -> Doubtful
|
||||
(Cry-If-Not, _) -> Probably-Not
|
||||
(Guaranteed, _) -> Impossible
|
||||
@@ -1,58 +0,0 @@
|
||||
module horse/prob/pmf
|
||||
|
||||
import std/core/list
|
||||
|
||||
// Discrete-support probability distribution implemented as a list with the invariant
|
||||
// that support is always given in increasing order.
|
||||
pub type pmf<s, v>
|
||||
Event(s: s, v: v, next: pmf<s, v>)
|
||||
End
|
||||
|
||||
// Add an independent event to the distribution.
|
||||
pub fun add(p: pmf<s, v>, s: s, v: v, ?s/cmp: (a: s, b: s) -> order, ?v/(+): (new: v, old: v) -> e v): e pmf<s, v>
|
||||
match p
|
||||
End -> Event(s, v, End)
|
||||
Event(s', v', next) -> match s.cmp(s')
|
||||
Lt -> Event(s, v, Event(s', v', next))
|
||||
Eq -> Event(s, v + v', next)
|
||||
Gt -> Event(s', v', add(next, s, v))
|
||||
|
||||
// Replace an event in the distribution.
|
||||
pub inline fun set(p: pmf<s, v>, s: s, v: v, ?s/cmp: (a: s, b: s) -> order): e pmf<s, v>
|
||||
p.add(s, v, cmp, fn(new, old) new)
|
||||
|
||||
// Construct a pmf from a list of (support, value) entries.
|
||||
pub fun list/pmf(l: list<(s, v)>, ?s/cmp: (a: s, b: s) -> order, ?v/(+): (new: v, old: v) -> e v): e pmf<s, v>
|
||||
l.foldl(End) fn(p, (s, v)) p.add(s, v)
|
||||
|
||||
// Fold over the entries of the distribution.
|
||||
pub tail fun foldl(p: pmf<s, v>, init: a, f: (a, s, v) -> e a): e a
|
||||
match p
|
||||
End -> init
|
||||
Event(s, v, next) -> foldl(next, f(init, s, v), f)
|
||||
|
||||
// Convert the distribution to a list of entries.
|
||||
pub fun pmf/list(p: pmf<s, v>): list<(s, v)>
|
||||
p.foldl(Nil) fn(l, s, v) Cons((s, v), l)
|
||||
|
||||
// Distribution of cooccurrence of two events described by their distributions.
|
||||
pub fun (*)(a: pmf<s, v>, b: pmf<s, v>, ?s/cmp: (a: s, b: s) -> order, ?v/(*): (a: v, b: v) -> e v): e pmf<s, v>
|
||||
match a
|
||||
End -> End
|
||||
Event(sa, va, nexta) -> match b
|
||||
End -> End
|
||||
Event(sb, vb, nextb) -> match sa.cmp(sb)
|
||||
Lt -> nexta * b
|
||||
Eq -> Event(sa, va * vb, nexta * nextb)
|
||||
Gt -> a * nextb
|
||||
|
||||
// Distribution of occurrence of at least one of two events described by their distributions.
|
||||
pub fun (+)(a: pmf<s, v>, b: pmf<s, v>, ?s/cmp: (a: s, b: s) -> order, ?v/(+): (a: v, b: v) -> e v): e pmf<s, v>
|
||||
match a
|
||||
End -> b
|
||||
Event(sa, va, nexta) -> match b
|
||||
End -> a
|
||||
Event(sb, vb, nextb) -> match sa.cmp(sb)
|
||||
Lt -> Event(sa, va, nexta + b)
|
||||
Eq -> Event(sa, va + vb, nexta + nextb)
|
||||
Gt -> Event(sb, vb, a + nextb)
|
||||
301
horse/race.kk
301
horse/race.kk
@@ -1,301 +0,0 @@
|
||||
module horse/race
|
||||
|
||||
import std/data/linearset
|
||||
import horse/game-id
|
||||
|
||||
pub struct race-detail
|
||||
race-id: race-id
|
||||
name: string
|
||||
grade: grade
|
||||
thumbnail-id: race-thumbnail-id
|
||||
// Some careers contain unusual versions of races, e.g. Tenno Sho (Spring)
|
||||
// in Hanshin instead of Kyoto for Narita Taishin and Biwa Hayahide.
|
||||
// For such races, this field holds the normal race ID.
|
||||
primary: race-id
|
||||
|
||||
pub fun detail(
|
||||
r: race-id,
|
||||
?race/show: (race-id) -> string,
|
||||
?race/grade: (race-id) -> grade,
|
||||
?race/thumbnail: (race-id) -> race-thumbnail-id,
|
||||
?race/primary: (race-id) -> race-id
|
||||
): race-detail
|
||||
Race-detail(r, r.show, r.grade, r.thumbnail, r.primary)
|
||||
|
||||
pub fun race-detail/show(r: race-detail): string
|
||||
val Race-detail(Race-id(id), name) = r
|
||||
name ++ " (ID " ++ id.show ++ ")"
|
||||
|
||||
// Race grades.
|
||||
pub type grade
|
||||
Pre-OP
|
||||
OP
|
||||
G3
|
||||
G2
|
||||
G1
|
||||
EX
|
||||
|
||||
// Automatically generated.
|
||||
// Comparison of the `grade` type.
|
||||
pub fun grade/cmp(this : grade, other : grade) : e order
|
||||
match (this, other)
|
||||
(Pre-OP, Pre-OP) -> Eq
|
||||
(Pre-OP, _) -> Lt
|
||||
(_, Pre-OP) -> Gt
|
||||
(OP, OP) -> Eq
|
||||
(OP, _) -> Lt
|
||||
(_, OP) -> Gt
|
||||
(G3, G3) -> Eq
|
||||
(G3, _) -> Lt
|
||||
(_, G3) -> Gt
|
||||
(G2, G2) -> Eq
|
||||
(G2, _) -> Lt
|
||||
(_, G2) -> Gt
|
||||
(G1, G1) -> Eq
|
||||
(G1, _) -> Lt
|
||||
(_, G1) -> Gt
|
||||
(EX, EX) -> Eq
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `grade` type.
|
||||
pub fun grade/show(this : grade) : e string
|
||||
match this
|
||||
Pre-OP -> "Pre-OP"
|
||||
OP -> "OP"
|
||||
G3 -> "G3"
|
||||
G2 -> "G2"
|
||||
G1 -> "G1"
|
||||
EX -> "EX"
|
||||
|
||||
pub struct saddle-detail
|
||||
saddle-id: saddle-id
|
||||
name: string
|
||||
races: list<race-id>
|
||||
saddle-type: saddle-type
|
||||
// For careers with unusual races, granted saddles also differ.
|
||||
// This field holds the normal saddle's ID for such cases.
|
||||
primary: saddle-id
|
||||
|
||||
pub fun saddle/detail(
|
||||
id: saddle-id,
|
||||
?saddle/show: (saddle-id) -> string,
|
||||
?saddle/races: (saddle-id) -> list<race-id>,
|
||||
?saddle/saddle-type: (saddle-id) -> saddle-type,
|
||||
?saddle/primary: (saddle-id) -> saddle-id
|
||||
): saddle-detail
|
||||
Saddle-detail(id, id.show, id.races, id.saddle-type, id.primary)
|
||||
|
||||
pub fun saddle-detail/show(s: saddle-detail): string
|
||||
val Saddle-detail(Saddle-id(id), name, _, _, Saddle-id(primary)) = s
|
||||
if id == primary then name else name ++ " (Alternate " ++ id.show ++ ")"
|
||||
|
||||
// Types of saddles.
|
||||
pub type saddle-type
|
||||
Honor // multiple race wins: classic triple crown, dual grand prix, &c.
|
||||
G3-Win
|
||||
G2-Win
|
||||
G1-Win
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `saddle-type` type.
|
||||
pub fun saddle-type/show(this : saddle-type) : e string
|
||||
match this
|
||||
Honor -> "Honor"
|
||||
G3-Win -> "G3"
|
||||
G2-Win -> "G2"
|
||||
G1-Win -> "G1"
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `saddle-type` type.
|
||||
pub fun saddle-type/(==)(this : saddle-type, other : saddle-type) : e bool
|
||||
match (this, other)
|
||||
(Honor, Honor) -> True
|
||||
(G3-Win, G3-Win) -> True
|
||||
(G2-Win, G2-Win) -> True
|
||||
(G1-Win, G1-Win) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Turn that a race occurred.
|
||||
pub struct turn
|
||||
year: turn-year
|
||||
month: turn-month
|
||||
half: turn-half
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `turn` type.
|
||||
pub fun turn/(==)(this : turn, other : turn) : e bool
|
||||
match (this, other)
|
||||
(Turn(year, month, half), Turn(year', month', half')) -> year == year' && month == month' && half == half'
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `turn` type.
|
||||
pub fun turn/order2(this : turn, other : turn) : e order2<turn>
|
||||
match (this, other)
|
||||
(Turn(year, month, half), Turn(year', month', half')) ->
|
||||
match year.order2(year')
|
||||
Eq2(year_eq) ->
|
||||
match month.order2(month')
|
||||
Eq2(month_eq) ->
|
||||
match half.order2(half')
|
||||
Eq2(half_eq) -> Eq2(Turn(year_eq, month_eq, half_eq))
|
||||
Lt2(half_lt, half_gt) -> Lt2(Turn(year_eq, month_eq, half_lt), Turn(year_eq, month_eq, half_gt))
|
||||
Gt2(half_lt, half_gt) -> Gt2(Turn(year_eq, month_eq, half_lt), Turn(year_eq, month_eq, half_gt))
|
||||
Lt2(month_lt, month_gt) -> Lt2(Turn(year_eq, month_lt, half), Turn(year_eq, month_gt, half'))
|
||||
Gt2(month_lt, month_gt) -> Gt2(Turn(year_eq, month_lt, half'), Turn(year_eq, month_gt, half))
|
||||
Lt2(year_lt, year_gt) -> Lt2(Turn(year_lt, month, half), Turn(year_gt, month', half'))
|
||||
Gt2(year_lt, year_gt) -> Gt2(Turn(year_lt, month', half'), Turn(year_gt, month, half))
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `turn` type.
|
||||
pub fun turn/show(this : turn) : e string
|
||||
this.year.show ++ " " ++ this.half.show ++ " " ++ this.month.show
|
||||
|
||||
pub type turn-year
|
||||
Junior
|
||||
Classic
|
||||
Senior
|
||||
Finale
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `turn-year` type.
|
||||
pub fun turn-year/(==)(this : turn-year, other : turn-year) : e bool
|
||||
match (this, other)
|
||||
(Junior, Junior) -> True
|
||||
(Classic, Classic) -> True
|
||||
(Senior, Senior) -> True
|
||||
(Finale, Finale) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `turn-year` type.
|
||||
pub fun turn-year/order2(this : turn-year, other : turn-year) : e order2<turn-year>
|
||||
match (this, other)
|
||||
(Junior, Junior) -> Eq2(Junior)
|
||||
(Junior, other') -> Lt2(Junior, other')
|
||||
(this', Junior) -> Gt2(Junior, this')
|
||||
(Classic, Classic) -> Eq2(Classic)
|
||||
(Classic, other') -> Lt2(Classic, other')
|
||||
(this', Classic) -> Gt2(Classic, this')
|
||||
(Senior, Senior) -> Eq2(Senior)
|
||||
(Senior, other') -> Lt2(Senior, other')
|
||||
(this', Senior) -> Gt2(Senior, this')
|
||||
(Finale, Finale) -> Eq2(Finale)
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `turn-year` type.
|
||||
pub fun turn-year/show(this : turn-year) : e string
|
||||
match this
|
||||
Junior -> "Junior Year"
|
||||
Classic -> "Classic Year"
|
||||
Senior -> "Senior Year"
|
||||
Finale -> "Finale 1" // the 1 is in the game
|
||||
|
||||
pub type turn-month
|
||||
January
|
||||
February
|
||||
March
|
||||
April
|
||||
May
|
||||
June
|
||||
July
|
||||
August
|
||||
September
|
||||
November
|
||||
December
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `turn-month` type.
|
||||
pub fun turn-month/(==)(this : turn-month, other : turn-month) : e bool
|
||||
match (this, other)
|
||||
(January, January) -> True
|
||||
(February, February) -> True
|
||||
(March, March) -> True
|
||||
(April, April) -> True
|
||||
(May, May) -> True
|
||||
(June, June) -> True
|
||||
(July, July) -> True
|
||||
(August, August) -> True
|
||||
(September, September) -> True
|
||||
(November, November) -> True
|
||||
(December, December) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `turn-month` type.
|
||||
pub fun turn-month/order2(this : turn-month, other : turn-month) : e order2<turn-month>
|
||||
match (this, other)
|
||||
(January, January) -> Eq2(January)
|
||||
(January, other') -> Lt2(January, other')
|
||||
(this', January) -> Gt2(January, this')
|
||||
(February, February) -> Eq2(February)
|
||||
(February, other') -> Lt2(February, other')
|
||||
(this', February) -> Gt2(February, this')
|
||||
(March, March) -> Eq2(March)
|
||||
(March, other') -> Lt2(March, other')
|
||||
(this', March) -> Gt2(March, this')
|
||||
(April, April) -> Eq2(April)
|
||||
(April, other') -> Lt2(April, other')
|
||||
(this', April) -> Gt2(April, this')
|
||||
(May, May) -> Eq2(May)
|
||||
(May, other') -> Lt2(May, other')
|
||||
(this', May) -> Gt2(May, this')
|
||||
(June, June) -> Eq2(June)
|
||||
(June, other') -> Lt2(June, other')
|
||||
(this', June) -> Gt2(June, this')
|
||||
(July, July) -> Eq2(July)
|
||||
(July, other') -> Lt2(July, other')
|
||||
(this', July) -> Gt2(July, this')
|
||||
(August, August) -> Eq2(August)
|
||||
(August, other') -> Lt2(August, other')
|
||||
(this', August) -> Gt2(August, this')
|
||||
(September, September) -> Eq2(September)
|
||||
(September, other') -> Lt2(September, other')
|
||||
(this', September) -> Gt2(September, this')
|
||||
(November, November) -> Eq2(November)
|
||||
(November, other') -> Lt2(November, other')
|
||||
(this', November) -> Gt2(November, this')
|
||||
(December, December) -> Eq2(December)
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `turn-month` type.
|
||||
pub fun turn-month/show(this : turn-month) : e string
|
||||
match this
|
||||
January -> "January"
|
||||
February -> "February"
|
||||
March -> "March"
|
||||
April -> "April"
|
||||
May -> "May"
|
||||
June -> "June"
|
||||
July -> "July"
|
||||
August -> "August"
|
||||
September -> "September"
|
||||
November -> "November"
|
||||
December -> "December"
|
||||
|
||||
pub type turn-half
|
||||
Early
|
||||
Late
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `turn-half` type.
|
||||
pub fun turn-half/(==)(this : turn-half, other : turn-half) : e bool
|
||||
match (this, other)
|
||||
(Early, Early) -> True
|
||||
(Late, Late) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `turn-half` type.
|
||||
pub fun turn-half/order2(this : turn-half, other : turn-half) : e order2<turn-half>
|
||||
match (this, other)
|
||||
(Early, Early) -> Eq2(Early)
|
||||
(Early, other') -> Lt2(Early, other')
|
||||
(this', Early) -> Gt2(Early, this')
|
||||
(Late, Late) -> Eq2(Late)
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `turn-half` type.
|
||||
pub fun turn-half/show(this : turn-half) : e string
|
||||
match this
|
||||
Early -> "Early"
|
||||
Late -> "Late"
|
||||
249
horse/skill.kk
249
horse/skill.kk
@@ -1,249 +0,0 @@
|
||||
module horse/skill
|
||||
|
||||
// This module contains skill-related definitions
|
||||
// common to all versions of the game.
|
||||
|
||||
import std/num/decimal
|
||||
import horse/game-id
|
||||
import horse/movement
|
||||
|
||||
// Full details about a skill.
|
||||
pub struct skill-detail
|
||||
skill-id: skill-id
|
||||
name: string
|
||||
description: string
|
||||
group-id: skill-group-id
|
||||
rarity: rarity
|
||||
group-rate: int
|
||||
grade-value: int
|
||||
wit-check: bool
|
||||
activations: list<activation>
|
||||
owner: maybe<uma-id>
|
||||
sp-cost: int
|
||||
icon-id: skill-icon-id
|
||||
|
||||
pub fun detail(
|
||||
s: skill-id,
|
||||
?skill/show: (skill-id) -> string,
|
||||
?skill/description: (skill-id) -> string,
|
||||
?skill/group: (skill-id) -> skill-group-id,
|
||||
?skill/rarity: (skill-id) -> rarity,
|
||||
?skill/group-rate: (skill-id) -> int,
|
||||
?skill/grade-value: (skill-id) -> int,
|
||||
?skill/wit-check: (skill-id) -> bool,
|
||||
?skill/activations: (skill-id) -> list<activation>,
|
||||
?skill/unique-owner: (skill-id) -> maybe<uma-id>,
|
||||
?skill/sp-cost: (skill-id) -> int,
|
||||
?skill/icon-id: (skill-id) -> skill-icon-id
|
||||
): skill-detail
|
||||
Skill-detail(
|
||||
s,
|
||||
s.show,
|
||||
s.description,
|
||||
s.group,
|
||||
s.rarity,
|
||||
s.group-rate,
|
||||
s.grade-value,
|
||||
s.wit-check,
|
||||
s.activations,
|
||||
s.unique-owner,
|
||||
s.sp-cost,
|
||||
s.icon-id
|
||||
)
|
||||
|
||||
pub fun skill-detail/show(d: skill-detail, ?character/show: (character-id) -> string, ?uma/show: (uma-id) -> string): string
|
||||
val Skill-detail(Skill-id(id), name, desc, _, rarity, _, grade-value, wit-check, activations, owner, sp-cost, _) = d
|
||||
val r = name ++ " (ID " ++ id.show ++ "): " ++ desc ++ " " ++ activations.map(activation/show).join(". ") ++ (if wit-check then ". Wit check. " else ". No wit check. ") ++ rarity.show ++ " costing " ++ sp-cost.show ++ " SP, worth " ++ grade-value.show ++ " grade value."
|
||||
match owner
|
||||
Nothing -> r
|
||||
Just(owner-id) -> match owner-id.show
|
||||
"" -> r ++ " Unique skill of Uma with ID " ++ owner-id.show ++ "."
|
||||
owner-name -> r ++ " Unique skill of " ++ owner-name ++ "."
|
||||
|
||||
// Skill rarity levels.
|
||||
pub type rarity
|
||||
Common // white
|
||||
Rare // gold
|
||||
Unique-Low // 1*/2* unique
|
||||
Unique-Upgraded // 3*+ unique on a trainee upgraded from 1*/2*
|
||||
Unique // base 3* unique
|
||||
|
||||
pub fun rarity/show(r: rarity): string
|
||||
match r
|
||||
Common -> "Common"
|
||||
Rare -> "Rare"
|
||||
Unique-Low -> "Unique (1\u2606/2\u2606)"
|
||||
Unique-Upgraded -> "Unique (3\u2606+ from 1\u2606/2\u2606 upgraded)"
|
||||
Unique -> "Unique (3\u2606+)"
|
||||
|
||||
// Condition and precondition logic.
|
||||
pub alias condition = string
|
||||
|
||||
// Activation conditions and effects.
|
||||
// A skill has one or two activations.
|
||||
pub struct activation
|
||||
precondition: condition
|
||||
condition: condition
|
||||
duration: decimal // seconds
|
||||
dur-scale: dur-scale
|
||||
cooldown: decimal // seconds
|
||||
abilities: list<ability> // one to three elements
|
||||
|
||||
pub fun activation/show(a: activation, ?character/show: (character-id) -> string): string
|
||||
match a
|
||||
Activation("", condition, duration, _, _, abilities) | !duration.is-pos -> condition ++ " -> " ++ abilities.show
|
||||
Activation("", condition, duration, Direct-Dur, cooldown, abilities) | cooldown >= 500.decimal -> condition ++ " -> for " ++ duration.show ++ "s, " ++ abilities.show
|
||||
Activation("", condition, duration, dur-scale, cooldown, abilities) | cooldown >= 500.decimal -> condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ ", " ++ abilities.show
|
||||
Activation("", condition, duration, Direct-Dur, cooldown, abilities) -> condition ++ " -> for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
Activation("", condition, duration, dur-scale, cooldown, abilities) -> condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ " on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
Activation(precondition, condition, duration, _, _, abilities) | !duration.is-pos -> precondition ++ " -> " ++ condition ++ " -> " ++ abilities.show
|
||||
Activation(precondition, condition, duration, Direct-Dur, cooldown, abilities) | cooldown >= 500.decimal -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s, " ++ abilities.show
|
||||
Activation(precondition, condition, duration, dur-scale, cooldown, abilities) | cooldown >= 500.decimal -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ ", " ++ abilities.show
|
||||
Activation(precondition, condition, duration, Direct-Dur, cooldown, abilities) -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
Activation(precondition, condition, duration, dur-scale, cooldown, abilities) -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ " on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
|
||||
// Special scaling types for skill activation durations.
|
||||
pub type dur-scale
|
||||
Direct-Dur
|
||||
Front-Distance-Dur
|
||||
Multiply-Remaining-HP
|
||||
Increment-Pass
|
||||
Midrace-Side-Block-Time-Dur
|
||||
Multiply-Remaining-HP2
|
||||
|
||||
pub fun dur-scale/show(s: dur-scale): string
|
||||
match s
|
||||
Direct-Dur -> "with no scaling"
|
||||
Front-Distance-Dur -> "scaling with distance from the front"
|
||||
Multiply-Remaining-HP -> "scaling with remaining HP"
|
||||
Increment-Pass -> "increasing with each pass while active"
|
||||
Midrace-Side-Block-Time-Dur -> "scaling with mid-race phase blocked side time"
|
||||
Multiply-Remaining-HP2 -> "scaling with remaining HP"
|
||||
|
||||
// Effects of activating a skill.
|
||||
pub struct ability
|
||||
ability-type: ability-type
|
||||
value-usage: value-usage
|
||||
target: target
|
||||
|
||||
pub fun ability/show(a: ability, ?character/show: (character-id) -> string): string
|
||||
match a
|
||||
Ability(t, Direct, Self) -> t.show
|
||||
Ability(t, Direct, target) -> t.show ++ " " ++ target.show
|
||||
Ability(t, v, Self) -> t.show ++ " " ++ v.show
|
||||
Ability(t, v, target) -> t.show ++ " " ++ target.show ++ " " ++ v.show
|
||||
|
||||
// Skill ability effects.
|
||||
pub type ability-type
|
||||
Passive-Speed(bonus: decimal)
|
||||
Passive-Stamina(bonus: decimal)
|
||||
Passive-Power(bonus: decimal)
|
||||
Passive-Guts(bonus: decimal)
|
||||
Passive-Wit(bonus: decimal)
|
||||
Great-Escape
|
||||
Vision(bonus: decimal)
|
||||
HP(rate: decimal)
|
||||
Gate-Delay(rate: decimal)
|
||||
Frenzy(add: decimal)
|
||||
Current-Speed(add: decimal)
|
||||
Target-Speed(add: decimal)
|
||||
Lane-Speed(add: decimal)
|
||||
Accel(add: decimal)
|
||||
Lane-Change(add: decimal)
|
||||
|
||||
pub fun ability-type/show(a: ability-type): string
|
||||
match a
|
||||
Passive-Speed(bonus) -> bonus.show ++ " Speed"
|
||||
Passive-Stamina(bonus) -> bonus.show ++ " Stamina"
|
||||
Passive-Power(bonus) -> bonus.show ++ " Power"
|
||||
Passive-Guts(bonus) -> bonus.show ++ " Guts"
|
||||
Passive-Wit(bonus) -> bonus.show ++ " Wit"
|
||||
Great-Escape -> "enable Great Escape style"
|
||||
Vision(bonus) -> bonus.show ++ " vision"
|
||||
HP(rate) | rate.is-pos -> show(rate * 100.decimal) ++ "% HP recovery"
|
||||
HP(rate) -> show(rate * 100.decimal) ++ "% HP loss"
|
||||
Gate-Delay(rate) -> rate.show ++ "× gate delay"
|
||||
Frenzy(add) -> add.show ++ "s longer Rushed"
|
||||
Current-Speed(rate) -> rate.show ++ "m/s current speed"
|
||||
Target-Speed(rate) -> rate.show ++ "m/s target speed"
|
||||
Lane-Speed(rate) -> rate.show ++ "m/s lane change speed"
|
||||
Accel(rate) -> rate.show ++ "m/s² acceleration"
|
||||
Lane-Change(rate) -> rate.show ++ " course width movement"
|
||||
|
||||
// Special scaling types for skill abilities.
|
||||
pub type value-usage
|
||||
Direct
|
||||
Team-Speed
|
||||
Team-Stamina
|
||||
Team-Power
|
||||
Team-Guts
|
||||
Team-Wit
|
||||
Multiply-Random
|
||||
Multiply-Random2
|
||||
Climax
|
||||
Max-Stat
|
||||
Passive-Count
|
||||
Front-Distance-Add
|
||||
Midrace-Side-Block-Time
|
||||
Speed-Scaling
|
||||
Speed-Scaling2
|
||||
Arc-Global-Potential
|
||||
Max-Lead-Distance
|
||||
|
||||
pub fun value-usage/show(v: value-usage): string
|
||||
match v
|
||||
Direct -> "with no scaling"
|
||||
Team-Speed -> "scaling with team Speed"
|
||||
Team-Stamina -> "scaling with team Stamina"
|
||||
Team-Power -> "scaling with team Power"
|
||||
Team-Guts -> "scaling with team Guts"
|
||||
Team-Wit -> "scaling with team Wit"
|
||||
Multiply-Random -> "scaling with a random multiplier (0×, 0.02×, or 0.04×)"
|
||||
Multiply-Random2 -> "scaling with a random multiplier (0×, 0.02×, or 0.04×)"
|
||||
Climax -> "scaling with the number of races won during training"
|
||||
Max-Stat -> "scaling with the value of the user's highest stat"
|
||||
Passive-Count -> "scaling with the number of Passive skills activated"
|
||||
Front-Distance-Add -> "scaling with distance from the leader"
|
||||
Midrace-Side-Block-Time -> "scaling with mid-race phase blocked side time"
|
||||
Speed-Scaling -> "scaling with overall speed"
|
||||
Speed-Scaling2 -> "scaling with overall speed"
|
||||
Arc-Global-Potential -> "scaling with L'Arc global potential"
|
||||
Max-Lead-Distance -> "scaling with the distance of the longest lead obtained in the first two thirds of the race"
|
||||
|
||||
// Who a skill ability targets.
|
||||
pub type target
|
||||
Self
|
||||
Sympathizers
|
||||
In-View
|
||||
Frontmost(limit: int)
|
||||
Ahead(limit: int)
|
||||
Behind(limit: int)
|
||||
All-Teammates
|
||||
Style(style: style)
|
||||
Rushing-Ahead(limit: int)
|
||||
Rushing-Behind(limit: int)
|
||||
Rushing-Style(style: style)
|
||||
Specific-Character(who: character-id)
|
||||
Triggering
|
||||
|
||||
pub fun target/show(t: target, ?character/show: (character-id) -> string): string
|
||||
match t
|
||||
Self -> "self"
|
||||
Sympathizers -> "others with Sympathy"
|
||||
In-View -> "others in field of view"
|
||||
Frontmost(limit) -> "frontmost " ++ limit.show
|
||||
Ahead(limit) | limit >= 18 -> "others ahead"
|
||||
Ahead(limit) -> "next " ++ limit.show ++ " others ahead"
|
||||
Behind(limit) | limit >= 18 -> "others behind"
|
||||
Behind(limit) -> "next " ++ limit.show ++ " others behind"
|
||||
All-Teammates -> "all teammates"
|
||||
Style(s) -> "other " ++ s.show ++ "s"
|
||||
Rushing-Ahead(limit) | limit >= 18 -> "others rushing ahead"
|
||||
Rushing-Ahead(limit) -> "next " ++ limit.show ++ " others rushing ahead"
|
||||
Rushing-Behind(limit) | limit >= 18 -> "others rushing behind"
|
||||
Rushing-Behind(limit) -> "next " ++ limit.show ++ " others rushing behind"
|
||||
Rushing-Style(s) -> "rushing " ++ s.show ++ "s"
|
||||
Specific-Character(who) -> match who.show
|
||||
"" -> "character with ID " ++ who.show
|
||||
name -> name
|
||||
Triggering -> "whosoever triggered this skill"
|
||||
175
horse/spark.kk
175
horse/spark.kk
@@ -1,175 +0,0 @@
|
||||
module horse/spark
|
||||
|
||||
import std/num/decimal
|
||||
import horse/game-id
|
||||
import horse/movement
|
||||
|
||||
// A spark on a veteran.
|
||||
pub struct spark-detail
|
||||
spark-id: spark-id
|
||||
typ: spark-type
|
||||
rarity: rarity
|
||||
|
||||
pub fun detail(id: spark-id, ?spark/spark-type: (spark-id) -> spark-type, ?spark/rarity: (spark-id) -> rarity): spark-detail
|
||||
Spark-detail(id, id.spark-type, id.rarity)
|
||||
|
||||
pub fun spark-detail/show(s: spark-detail, ?spark/show: (spark-id) -> string): string
|
||||
s.spark-id.show ++ " " ++ "\u2605".repeat(s.rarity.int)
|
||||
|
||||
// The category of a spark; roughly, blue, pink, green, or white, with some
|
||||
// further subdivisions.
|
||||
pub type spark-type
|
||||
Stat // blue
|
||||
Aptitude // red/pink
|
||||
Unique // green
|
||||
Race
|
||||
Skill
|
||||
// skip Carnival Bonus
|
||||
Scenario
|
||||
Surface
|
||||
Distance
|
||||
Style
|
||||
Hidden
|
||||
|
||||
// Spark targets and effects.
|
||||
pub type spark-effect
|
||||
Stat-Up(s: stat, amount: int)
|
||||
SP-Up(amount: int)
|
||||
// skip Carnival Bonus
|
||||
Random-Stat-Up(amount: int)
|
||||
Aptitude-Up(a: aptitude, amount: int)
|
||||
Skill-Hint(s: skill-id, levels: int)
|
||||
Stat-Cap-Up(s: stat, amount: int)
|
||||
|
||||
// Get the base probability for a spark to trigger during a single inheritance.
|
||||
pub fun decimal/base-proc(id: spark-id, ?spark-type: (spark-id) -> spark-type, ?rarity: (spark-id) -> rarity): decimal
|
||||
val t = id.spark-type
|
||||
val r = id.rarity
|
||||
match (t, r)
|
||||
(Stat, One) -> 70.decimal(-2)
|
||||
(Stat, Two) -> 80.decimal(-2)
|
||||
(Stat, Three) -> 90.decimal(-2)
|
||||
(Aptitude, One) -> 1.decimal(-2)
|
||||
(Aptitude, Two) -> 3.decimal(-2)
|
||||
(Aptitude, Three) -> 5.decimal(-2)
|
||||
(Unique, One) -> 5.decimal(-2)
|
||||
(Unique, Two) -> 10.decimal(-2)
|
||||
(Unique, Three) -> 15.decimal(-2)
|
||||
(Race, One) -> 1.decimal(-2)
|
||||
(Race, Two) -> 2.decimal(-2)
|
||||
(Race, Three) -> 3.decimal(-2)
|
||||
(_, One) -> 3.decimal(-2)
|
||||
(_, Two) -> 6.decimal(-2)
|
||||
(_, Three) -> 9.decimal(-2)
|
||||
|
||||
// The level or star count of a spark.
|
||||
pub type rarity
|
||||
One
|
||||
Two
|
||||
Three
|
||||
|
||||
pub fun rarity/int(l: rarity): int
|
||||
match l
|
||||
One -> 1
|
||||
Two -> 2
|
||||
Three -> 3
|
||||
|
||||
pub fun rarity/show(l: rarity): string
|
||||
match l
|
||||
One -> "1"
|
||||
Two -> "2"
|
||||
Three -> "3"
|
||||
|
||||
// Stat (blue) spark.
|
||||
pub type stat
|
||||
Speed
|
||||
Stamina
|
||||
Power
|
||||
Guts
|
||||
Wit
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `stat` type.
|
||||
pub fun stat/show(this : stat) : e string
|
||||
match this
|
||||
Speed -> "Speed"
|
||||
Stamina -> "Stamina"
|
||||
Power -> "Power"
|
||||
Guts -> "Guts"
|
||||
Wit -> "Wit"
|
||||
|
||||
// Aptitude (red/pink) spark.
|
||||
pub type aptitude
|
||||
Turf
|
||||
Dirt
|
||||
Sprint
|
||||
Mile
|
||||
Medium
|
||||
Long
|
||||
Front-Runner
|
||||
Pace-Chaser
|
||||
Late-Surger
|
||||
End-Closer
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `aptitude` type.
|
||||
pub fun aptitude/order2(this : aptitude, other : aptitude) : e order2<aptitude>
|
||||
match (this, other)
|
||||
(Turf, Turf) -> Eq2(Turf)
|
||||
(Turf, other') -> Lt2(Turf, other')
|
||||
(this', Turf) -> Gt2(Turf, this')
|
||||
(Dirt, Dirt) -> Eq2(Dirt)
|
||||
(Dirt, other') -> Lt2(Dirt, other')
|
||||
(this', Dirt) -> Gt2(Dirt, this')
|
||||
(Sprint, Sprint) -> Eq2(Sprint)
|
||||
(Sprint, other') -> Lt2(Sprint, other')
|
||||
(this', Sprint) -> Gt2(Sprint, this')
|
||||
(Mile, Mile) -> Eq2(Mile)
|
||||
(Mile, other') -> Lt2(Mile, other')
|
||||
(this', Mile) -> Gt2(Mile, this')
|
||||
(Medium, Medium) -> Eq2(Medium)
|
||||
(Medium, other') -> Lt2(Medium, other')
|
||||
(this', Medium) -> Gt2(Medium, this')
|
||||
(Long, Long) -> Eq2(Long)
|
||||
(Long, other') -> Lt2(Long, other')
|
||||
(this', Long) -> Gt2(Long, this')
|
||||
(Front-Runner, Front-Runner) -> Eq2(Front-Runner)
|
||||
(Front-Runner, other') -> Lt2(Front-Runner, other')
|
||||
(this', Front-Runner) -> Gt2(Front-Runner, this')
|
||||
(Pace-Chaser, Pace-Chaser) -> Eq2(Pace-Chaser)
|
||||
(Pace-Chaser, other') -> Lt2(Pace-Chaser, other')
|
||||
(this', Pace-Chaser) -> Gt2(Pace-Chaser, this')
|
||||
(Late-Surger, Late-Surger) -> Eq2(Late-Surger)
|
||||
(Late-Surger, other') -> Lt2(Late-Surger, other')
|
||||
(this', Late-Surger) -> Gt2(Late-Surger, this')
|
||||
(End-Closer, End-Closer) -> Eq2(End-Closer)
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `aptitude` type.
|
||||
pub fun aptitude/(==)(this : aptitude, other : aptitude) : e bool
|
||||
match (this, other)
|
||||
(Turf, Turf) -> True
|
||||
(Dirt, Dirt) -> True
|
||||
(Sprint, Sprint) -> True
|
||||
(Mile, Mile) -> True
|
||||
(Medium, Medium) -> True
|
||||
(Long, Long) -> True
|
||||
(Front-Runner, Front-Runner) -> True
|
||||
(Pace-Chaser, Pace-Chaser) -> True
|
||||
(Late-Surger, Late-Surger) -> True
|
||||
(End-Closer, End-Closer) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Shows a string representation of the `aptitude` type.
|
||||
pub fun aptitude/show(this : aptitude): string
|
||||
match this
|
||||
Turf -> "Turf"
|
||||
Dirt -> "Dirt"
|
||||
Sprint -> "Sprint"
|
||||
Mile -> "Mile"
|
||||
Medium -> "Medium"
|
||||
Long -> "Long"
|
||||
Front-Runner -> "Front Runner"
|
||||
Pace-Chaser -> "Pace Chaser"
|
||||
Late-Surger -> "Late Surger"
|
||||
End-Closer -> "End Closer"
|
||||
27
horse/uma.kk
27
horse/uma.kk
@@ -1,27 +0,0 @@
|
||||
module horse/uma
|
||||
|
||||
import horse/game-id
|
||||
import horse/movement
|
||||
|
||||
// Details of an uma, or character card.
|
||||
pub struct uma-detail
|
||||
uma-id: uma-id
|
||||
character-id: character-id
|
||||
sprint: aptitude-level
|
||||
mile: aptitude-level
|
||||
medium: aptitude-level
|
||||
long: aptitude-level
|
||||
front-runner: aptitude-level
|
||||
pace-chaser: aptitude-level
|
||||
late-surger: aptitude-level
|
||||
end-closer: aptitude-level
|
||||
turf: aptitude-level
|
||||
dirt: aptitude-level
|
||||
unique: skill-id
|
||||
skill1: skill-id
|
||||
skill2: skill-id
|
||||
skill3: skill-id
|
||||
skill-pl2: skill-id
|
||||
skill-pl3: skill-id
|
||||
skill-pl4: skill-id
|
||||
skill-pl5: skill-id
|
||||
@@ -227,6 +227,10 @@ export interface Skill {
|
||||
* Name of the Uma which owns this skill as a unique, if applicable.
|
||||
*/
|
||||
unique_owner?: string;
|
||||
/**
|
||||
* Skill tags, numeric IDs for matching against other effects.
|
||||
*/
|
||||
tags: number[];
|
||||
/**
|
||||
* SP cost to purchase the skill, if applicable.
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,7 @@ type Skill struct {
|
||||
WitCheck bool `json:"wit_check"`
|
||||
Activations []Activation `json:"activations"`
|
||||
UniqueOwner string `json:"unique_owner,omitzero"`
|
||||
Tags []uint16 `json:"tags"`
|
||||
SPCost int `json:"sp_cost,omitzero"`
|
||||
IconID int `json:"icon_id"`
|
||||
}
|
||||
@@ -78,9 +79,10 @@ func (a Ability) String() string {
|
||||
r = append(r, (a.Value * 100).String()...)
|
||||
r = append(r, '%')
|
||||
case AbilityGateDelay:
|
||||
// This skill type in particular should be × instead of +.
|
||||
r = append(r[:len(r)-1], "×"...)
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, "×"...)
|
||||
case AbilityFrenzy:
|
||||
case AbilityFrenzy, AbilityAddGateDelay:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, 's')
|
||||
case AbilityCurrentSpeed, AbilityTargetSpeed, AbilityLaneSpeed:
|
||||
@@ -90,6 +92,9 @@ func (a Ability) String() string {
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, " m/s²"...)
|
||||
case AbilityLaneChange:
|
||||
// This skill type should be "to 0.5 track widths."
|
||||
// (The only skill that has it is Dodging Danger/Sixth Sense.)
|
||||
r = append(r[:len(r)-1], "to "...)
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, " track widths"...)
|
||||
}
|
||||
@@ -153,6 +158,7 @@ const (
|
||||
AbilityHP AbilityType = 9 // HP
|
||||
AbilityGateDelay AbilityType = 10 // Gate delay multiplier
|
||||
AbilityFrenzy AbilityType = 13 // Frenzy
|
||||
AbilityAddGateDelay AbilityType = 14 // Added gate delay
|
||||
AbilityCurrentSpeed AbilityType = 21 // Current speed
|
||||
AbilityTargetSpeed AbilityType = 27 // Target speed
|
||||
AbilityLaneSpeed AbilityType = 28 // Lane change speed
|
||||
@@ -3,7 +3,7 @@ package horse_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||
"git.sunturtle.xyz/zephyr/horse"
|
||||
)
|
||||
|
||||
func TestTenThousandthsString(t *testing.T) {
|
||||
1
std
1
std
Submodule std deleted from 41b8aed39e
295
test/example.kk
295
test/example.kk
@@ -1,295 +0,0 @@
|
||||
module test/example
|
||||
|
||||
import std/num/decimal
|
||||
import std/data/linearmap
|
||||
import horse/game-id
|
||||
import horse/global
|
||||
import horse/global/character
|
||||
import horse/global/saddle
|
||||
import horse/global/skill
|
||||
import horse/global/spark
|
||||
import horse/global/uma
|
||||
import horse/legacy
|
||||
|
||||
val p1 = Legacy(
|
||||
uma = Veteran(
|
||||
uma = Uma-id(102001), // seiun sky
|
||||
sparks = [
|
||||
301, // 1* power
|
||||
2102, // 2* front runner
|
||||
10200103, // 3* angling and scheming
|
||||
1000302, // 2* osaka hai
|
||||
1001001, // 1* japanese derby
|
||||
1001101, // 1* yasuda kinen
|
||||
1001701, // 1* qe2
|
||||
2001402, // 2* non-standard distance
|
||||
2004301, // 1* focus
|
||||
2005301, // 1* early lead
|
||||
2012401, // 1* front runner straightaways
|
||||
2012502, // 2* front runner corners
|
||||
2015201, // 1* front runner savvy
|
||||
2016001, // 1* groundwork
|
||||
2016102, // 2* thh
|
||||
2016402, // 2* lone wolf
|
||||
3000201, // 1* unity cup
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
1, // classic triple crown
|
||||
2, // senior autumn triple crown
|
||||
4, // senior spring triple crown
|
||||
5, // tenno sweep
|
||||
6, // dual grand prix
|
||||
7, // dual miles
|
||||
10, // arima kinen
|
||||
11, // japan cup
|
||||
12, // derby
|
||||
13, // tss
|
||||
14, // takarazuka kinen
|
||||
15, // tsa
|
||||
16, // kikuka sho
|
||||
17, // osaka hai
|
||||
18, // satsuki sho
|
||||
21, // yasuda kinen
|
||||
23, // mile championship
|
||||
25, // victoria mile
|
||||
26, // qe2
|
||||
33, // asahi hai fs
|
||||
34, // hopeful stakes
|
||||
96, // mainichi hai
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub1 = Veteran(
|
||||
uma = Uma-id(102601), // mihono bourbon
|
||||
sparks = [
|
||||
302, // 2* power
|
||||
3303, // 3* medium
|
||||
10260102, // 2* g00 1st
|
||||
1001201, // 1* takarazuka kinen
|
||||
1001702, // 2* qe2
|
||||
1001901, // 1* japan cup
|
||||
2004302, // 2* focus
|
||||
2004502, // 2* prudent positioning
|
||||
2012502, // 2* front corners
|
||||
2015202, // 2* front savvy
|
||||
2016002, // 2* groundwork
|
||||
2016401, // 1* lone wolf
|
||||
3000201, // 1* unity cup
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
2, // senior autumn triple crown
|
||||
6, // dual grand prix
|
||||
7, // dual miles
|
||||
10, // arima kinen
|
||||
11, // japan cup
|
||||
12, // derby
|
||||
14, // takarazuka kinen
|
||||
15, // tsa
|
||||
17, // osaka hai
|
||||
18, // satsuki sho
|
||||
21, // yasuda kinen
|
||||
23, // mile championship
|
||||
25, // victoria mile
|
||||
26, // qe2
|
||||
27, // nhk mile cup
|
||||
33, // asahi hai fs
|
||||
34, // hopeful stakes
|
||||
49, // spring stakes
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub2 = Veteran(
|
||||
uma = Uma-id(102401), // mayano top gun
|
||||
sparks = [
|
||||
302, // 2* power
|
||||
1103, // 3* turf
|
||||
10240101, // 1* flashy landing
|
||||
1000601, // 1* tss
|
||||
1001202, // 2* takarazuka kinen
|
||||
1001502, // 2* kikuka sho
|
||||
1001601, // 1* tsa
|
||||
1002102, // 2* hanshin jf
|
||||
1002301, // 1* arima kinen
|
||||
2003503, // 3* corner recovery
|
||||
2003802, // 2* straightaway recovery
|
||||
2004602, // 2* ramp up
|
||||
2005502, // 2* final push
|
||||
2012702, // 2* leader's pride
|
||||
2016002, // 2* groundwork
|
||||
3000102, // 2* ura finale
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
1, // classic triple crown
|
||||
2, // senior autumn triple crown
|
||||
4, // senior spring triple crown
|
||||
5, // tenno sweep
|
||||
6, // dual grand prix
|
||||
7, // dual miles
|
||||
10, // arima kinen
|
||||
11, // japan cup
|
||||
12, // derby
|
||||
13, // tss
|
||||
14, // takarazuka kinen
|
||||
15, // tsa
|
||||
16, // kikuka sho
|
||||
18, // satsuki sho
|
||||
21, // yasuda kinen
|
||||
23, // mile championship
|
||||
25, // victoria mile
|
||||
26, // qe2
|
||||
34, // hopeful stakes
|
||||
35, // hanshin jf
|
||||
].map(Saddle-id(_))
|
||||
)
|
||||
)
|
||||
|
||||
val p2 = Legacy(
|
||||
uma = Veteran(
|
||||
uma = Uma-id(102601), // mihono bourbon
|
||||
sparks = [
|
||||
302,
|
||||
3303,
|
||||
1001201,
|
||||
1001702,
|
||||
1001901,
|
||||
2004302,
|
||||
2004502,
|
||||
2012502,
|
||||
2015202,
|
||||
2016002,
|
||||
2016401,
|
||||
3000201,
|
||||
10260102,
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
2,
|
||||
6,
|
||||
7,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
17,
|
||||
18,
|
||||
21,
|
||||
23,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33,
|
||||
34,
|
||||
49,
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub1 = Veteran(
|
||||
uma = Uma-id(102402), // wedding mayano
|
||||
sparks = [
|
||||
203,
|
||||
3202,
|
||||
1000701,
|
||||
1000802,
|
||||
1001201,
|
||||
1001803,
|
||||
2003502,
|
||||
2003701,
|
||||
2004301,
|
||||
2005502,
|
||||
2012401,
|
||||
2016402,
|
||||
10240202,
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
1,
|
||||
2,
|
||||
6,
|
||||
7,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
18,
|
||||
21,
|
||||
23,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33,
|
||||
34,
|
||||
48,
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub2 = Veteran(
|
||||
uma = Uma-id(100201), // silence suzuka
|
||||
sparks = [
|
||||
203,
|
||||
1101,
|
||||
1001901,
|
||||
1002203,
|
||||
1002302,
|
||||
2000101,
|
||||
2000201,
|
||||
2001902,
|
||||
2003501,
|
||||
2005401,
|
||||
2016001,
|
||||
3000102,
|
||||
10020101,
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
2,
|
||||
6,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
17,
|
||||
18,
|
||||
21,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33,
|
||||
34,
|
||||
40,
|
||||
42,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
49,
|
||||
59,
|
||||
61,
|
||||
63,
|
||||
65,
|
||||
111,
|
||||
113,
|
||||
117,
|
||||
126,
|
||||
].map(Saddle-id(_))
|
||||
)
|
||||
)
|
||||
|
||||
val trainee = Uma-id(104601) // smart falcon
|
||||
|
||||
pub fun main()
|
||||
val p1a = parent-affinity(trainee, p1, p2.uma.uma)
|
||||
val p2a = parent-affinity(trainee, p2, p1.uma.uma)
|
||||
val (s11a, s12a) = sub-affinity(trainee, p1)
|
||||
val (s21a, s22a) = sub-affinity(trainee, p2)
|
||||
println("trainee: " ++ trainee.show)
|
||||
println("p1: " ++ p1.uma.uma.show ++ " affinity " ++ p1a.show)
|
||||
println("s1-1: " ++ p1.sub1.uma.show ++ " affinity " ++ s11a.show)
|
||||
println("s1-2: " ++ p1.sub2.uma.show ++ " affinity " ++ s12a.show)
|
||||
println("p2: " ++ p2.uma.uma.show ++ " affinity " ++ p1a.show)
|
||||
println("s1-1: " ++ p2.sub1.uma.show ++ " affinity " ++ s21a.show)
|
||||
println("s1-2: " ++ p2.sub2.uma.show ++ " affinity " ++ s22a.show)
|
||||
val inspo = inspiration(trainee, p1, p2)
|
||||
val s = inspiration-gives(inspo, legacy/skills)
|
||||
val a = inspiration-gives(inspo, legacy/aptitudes)
|
||||
println("\nskills:")
|
||||
s.list.foreach() fn((skill, pmf))
|
||||
println(" " ++ skill.show ++ ": " ++ pmf.show)
|
||||
println("\naptitudes:")
|
||||
a.list.foreach() fn((apt, pmf))
|
||||
println(" " ++ apt.show ++ ": " ++ pmf.show)
|
||||
@@ -1,12 +0,0 @@
|
||||
module test/global
|
||||
|
||||
import horse/global/character
|
||||
import horse/global/race
|
||||
import horse/global/saddle
|
||||
import horse/global/scenario
|
||||
import horse/global/skill
|
||||
import horse/global/spark
|
||||
import horse/global/uma
|
||||
|
||||
pub fun main()
|
||||
()
|
||||
1808
zenno/package-lock.json
generated
1808
zenno/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,30 +16,36 @@
|
||||
"test": "npm run test:unit -- --run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.4",
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.56.1",
|
||||
"@sveltejs/kit": "^2.60.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^22.19.17",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/node": "^22.19.19",
|
||||
"@vitest/browser-playwright": "^4.1.0",
|
||||
"eslint": "^10.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"eslint": "^10.4.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"globals": "^17.4.0",
|
||||
"playwright": "^1.58.2",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.55.1",
|
||||
"svelte-check": "^4.4.6",
|
||||
"eslint-plugin-svelte": "^3.17.1",
|
||||
"globals": "^17.6.0",
|
||||
"playwright": "^1.60.0",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-svelte": "^3.5.2",
|
||||
"prettier-plugin-tailwindcss": "^0.7.4",
|
||||
"svelte": "^5.55.7",
|
||||
"svelte-check": "^4.4.8",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^7.3.2",
|
||||
"typescript-eslint": "^8.59.3",
|
||||
"vite": "^7.3.3",
|
||||
"vitest": "^4.1.0",
|
||||
"vitest-browser-svelte": "^2.1.0"
|
||||
"vitest-browser-svelte": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@observablehq/plot": "^0.6.17",
|
||||
"mathjs": "^15.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
27
zenno/src/lib/Skill.svelte
Normal file
27
zenno/src/lib/Skill.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { skills, ZERO_SKILL, type Skill } from "./data/skill";
|
||||
|
||||
interface CommonProps {
|
||||
hint?: string;
|
||||
mention?: boolean;
|
||||
}
|
||||
|
||||
type Props = CommonProps & ({skill: number, name?: never} | {name: string, skill?: never});
|
||||
|
||||
let {hint, mention, skill, name}: Props = $props();
|
||||
|
||||
const s: Readonly<Skill> = $derived.by(() => {
|
||||
const l = skill != null ? skills.global.filter((s) => s.skill_id === skill) : skills.global.filter((s) => s.name.includes(name!));
|
||||
if (name != null) {
|
||||
console.warn(`skills specified as ${name} (${hint}):`, l);
|
||||
}
|
||||
if (l.length === 0) {
|
||||
return ZERO_SKILL;
|
||||
}
|
||||
return l[0];
|
||||
});
|
||||
|
||||
const spanClass = $derived(mention ? 'italic' : 'font-bold')
|
||||
</script>
|
||||
|
||||
<span class={spanClass}>{s.name}</span>
|
||||
125
zenno/src/lib/StatChart.svelte
Normal file
125
zenno/src/lib/StatChart.svelte
Normal file
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import * as Plot from '@observablehq/plot';
|
||||
import * as d3 from 'd3';
|
||||
import { Stat } from './race';
|
||||
import type { ComputedSeries, HorizontalRule } from './chart';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import type { Attachment } from 'svelte/attachments';
|
||||
|
||||
interface Props {
|
||||
/** The stat the chart shows. */
|
||||
stat: Stat;
|
||||
/** Series to show in the chart. */
|
||||
y: ComputedSeries | Array<ComputedSeries | null>;
|
||||
/** Label for the dependent variable. */
|
||||
yLabel: string;
|
||||
/**
|
||||
* Range of the dependent variable to show.
|
||||
* If not given, the limits are the minimum and maximum values plotted.
|
||||
* If given as a triple, the second value is the default maximum and
|
||||
* the third is the maximum when the chart is expanded to 2000.
|
||||
*/
|
||||
range?: [number, number] | [number, number, number];
|
||||
/** Range of the stat to plot. */
|
||||
xRange?: [number, number] | null;
|
||||
/**
|
||||
* Vertical rules to place on the graph.
|
||||
* Each rule gets a corresponding horizontal rule at the intersection
|
||||
* with each series.
|
||||
*/
|
||||
xRule?: number | number[];
|
||||
/**
|
||||
* Horizontal rules to place on the graph.
|
||||
*/
|
||||
yRule?: HorizontalRule[];
|
||||
|
||||
class?: ClassValue | null;
|
||||
plotOptions?: Omit<Plot.PlotOptions, 'marks' | 'x' | 'y'>;
|
||||
}
|
||||
|
||||
let { stat, y, yLabel, range, xRange, xRule = [], yRule = [], class: className, plotOptions = {} }: Props = $props();
|
||||
|
||||
let width = $state(0);
|
||||
let height = $state(0);
|
||||
|
||||
let expand = $state(false);
|
||||
|
||||
const xLabel = $derived(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);
|
||||
});
|
||||
const xVal = $derived(d3.range(xMin, xMax, 5));
|
||||
const thrX = 1200;
|
||||
|
||||
const series = $derived([y].flat(1).filter((s) => s != null));
|
||||
const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(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]];
|
||||
}
|
||||
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 makeChart: Attachment = (el) => {
|
||||
$effect(() => {
|
||||
el?.firstChild?.remove();
|
||||
el?.append(
|
||||
Plot.plot({
|
||||
width,
|
||||
height,
|
||||
clip: true,
|
||||
...plotOptions,
|
||||
x: {
|
||||
domain: [xMin, xMax],
|
||||
interval: 5,
|
||||
ticks: d3.range(2000, 0, -200).filter((x) => xMin <= x && x <= xMax),
|
||||
label: xLabel,
|
||||
line: true,
|
||||
},
|
||||
y: {
|
||||
domain: yRange,
|
||||
grid: true,
|
||||
label: yLabel,
|
||||
line: true,
|
||||
},
|
||||
marks: [
|
||||
Plot.ruleX([thrX], { strokeOpacity: 0.25 }),
|
||||
Plot.ruleX(xLines, { strokeOpacity: 0.5 }),
|
||||
Plot.ruleY(yLines, { y: 'y', stroke: 'label', strokeOpacity: 0.5 }),
|
||||
Plot.ruleY(yRule, { y: 'y', strokeOpacity: 0.75 }),
|
||||
Plot.tip(yRule, { x: xMax, y: 'y', title: 'label', anchor: 'top-right', className: 'plot-tip' }),
|
||||
Plot.frame(),
|
||||
Plot.line(vals, { x: 'x', y: 'y', stroke: 'label', strokeWidth: 3 }),
|
||||
Plot.tip(vals, Plot.pointerY({ x: 'x', y: 'y', stroke: 'label', className: 'plot-tip' })),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div bind:clientWidth={width} bind:clientHeight={height} class={['flex h-full w-full flex-col md:flex-row', className]}>
|
||||
<div role="img" {@attach makeChart}>
|
||||
<span>the chart seems to have didn't</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.plot-tip) {
|
||||
--plot-background: light-dark(var(--color-mist-200), var(--color-mist-800));
|
||||
}
|
||||
</style>
|
||||
78
zenno/src/lib/alpha123Umalator.ts
Normal file
78
zenno/src/lib/alpha123Umalator.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { AptitudeLevel, Mood, RunningStyle } from './race';
|
||||
import type { Runner } from './runner';
|
||||
|
||||
const aptMap = {
|
||||
G: AptitudeLevel.G,
|
||||
F: AptitudeLevel.F,
|
||||
E: AptitudeLevel.E,
|
||||
D: AptitudeLevel.D,
|
||||
C: AptitudeLevel.C,
|
||||
B: AptitudeLevel.B,
|
||||
A: AptitudeLevel.A,
|
||||
S: AptitudeLevel.S,
|
||||
} as const;
|
||||
type AptitudeString = keyof typeof aptMap;
|
||||
|
||||
const styleMap = {
|
||||
Nige: RunningStyle.FrontRunner,
|
||||
Sentou: RunningStyle.PaceChaser,
|
||||
Sasi: RunningStyle.LateSurger,
|
||||
Oikomi: RunningStyle.EndCloser,
|
||||
Oonige: RunningStyle.GreatEscape,
|
||||
} as const;
|
||||
|
||||
export interface ImportUma {
|
||||
outfitId: string;
|
||||
starCount: number;
|
||||
speed: number;
|
||||
stamina: number;
|
||||
power: number;
|
||||
guts: number;
|
||||
wisdom: number;
|
||||
strategy: keyof typeof styleMap;
|
||||
distanceAptitude: AptitudeString;
|
||||
surfaceAptitude: AptitudeString;
|
||||
strategyAptitude: AptitudeString;
|
||||
aptitudes: [
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
AptitudeString,
|
||||
];
|
||||
skills: string[];
|
||||
uniqueLv: number;
|
||||
mood: Mood;
|
||||
popularity: number;
|
||||
}
|
||||
|
||||
export function load(obj: ImportUma, name?: string): Runner {
|
||||
return {
|
||||
name: name ?? '',
|
||||
chara_card_id: obj.outfitId !== '' ? parseInt(obj.outfitId) : 0,
|
||||
style: styleMap[obj.strategy],
|
||||
mood: obj.mood,
|
||||
speed: obj.speed,
|
||||
stamina: obj.stamina,
|
||||
power: obj.power,
|
||||
guts: obj.guts,
|
||||
wit: obj.wisdom,
|
||||
sprint: aptMap[obj.aptitudes[0]],
|
||||
mile: aptMap[obj.aptitudes[1]],
|
||||
medium: aptMap[obj.aptitudes[2]],
|
||||
long: aptMap[obj.aptitudes[3]],
|
||||
front: aptMap[obj.aptitudes[4]],
|
||||
pace: aptMap[obj.aptitudes[5]],
|
||||
late: aptMap[obj.aptitudes[6]],
|
||||
end: aptMap[obj.aptitudes[7]],
|
||||
turf: aptMap[obj.aptitudes[8]],
|
||||
dirt: aptMap[obj.aptitudes[9]],
|
||||
skills: obj.skills.map((s) => parseInt(s)),
|
||||
unique_level: obj.uniqueLv,
|
||||
};
|
||||
}
|
||||
9
zenno/src/lib/chart.ts
Normal file
9
zenno/src/lib/chart.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ComputedSeries {
|
||||
y: (x: number) => number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface HorizontalRule {
|
||||
y: number;
|
||||
label: string;
|
||||
}
|
||||
173
zenno/src/lib/data/skill.ts
Normal file
173
zenno/src/lib/data/skill.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import skillGlobal from '../../../../global/skill.json'
|
||||
import groupGlobal from '../../../../global/skill-group.json'
|
||||
|
||||
/**
|
||||
* Skill data.
|
||||
*/
|
||||
export interface Skill {
|
||||
/**
|
||||
* Skill ID.
|
||||
*/
|
||||
skill_id: number;
|
||||
/**
|
||||
* Regional skill name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Regional skil description.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Skill group ID.
|
||||
*/
|
||||
group: number;
|
||||
/**
|
||||
* Skill rarity. 3-5 are uniques for various star levels.
|
||||
*/
|
||||
rarity: 1 | 2 | 3 | 4 | 5;
|
||||
/**
|
||||
* Upgrade position within the skill's group.
|
||||
* -1 is for negative (purple) skills.
|
||||
*/
|
||||
group_rate: 1 | 2 | 3 | -1;
|
||||
/**
|
||||
* Grade value, or the amount of rating gained for having the skill with
|
||||
* appropriate aptitude.
|
||||
*/
|
||||
grade_value?: number;
|
||||
/**
|
||||
* Whether the skill requires a wit check.
|
||||
*/
|
||||
wit_check: boolean;
|
||||
/**
|
||||
* Conditions and results of skill activation.
|
||||
*/
|
||||
activations: Activation[];
|
||||
/**
|
||||
* Name of the Uma which owns this skill as a unique, if applicable.
|
||||
*/
|
||||
unique_owner?: string;
|
||||
/**
|
||||
* SP cost to purchase the skill, if applicable.
|
||||
*/
|
||||
sp_cost?: number;
|
||||
/**
|
||||
* Skill icon ID.
|
||||
*/
|
||||
icon_id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditions and results of skill activation.
|
||||
*/
|
||||
export interface Activation {
|
||||
/**
|
||||
* Precondition which must be satisfied before the condition is checked.
|
||||
*/
|
||||
precondition?: string;
|
||||
/**
|
||||
* Activation conditions.
|
||||
*/
|
||||
condition: string;
|
||||
/**
|
||||
* Skill duration in ten thousandths of a second.
|
||||
* Generally undefined for activations which only affect HP.
|
||||
*/
|
||||
duration?: number;
|
||||
/**
|
||||
* Special skill duration scaling mode.
|
||||
*/
|
||||
dur_scale: 1 | 2 | 3 | 4 | 5 | 7;
|
||||
/**
|
||||
* Skill cooldown in ten thousandths of a second.
|
||||
* A value of 5000000 indicates that the cooldown is forever.
|
||||
* Generally undefined for passive skills.
|
||||
*/
|
||||
cooldown?: number;
|
||||
/**
|
||||
* Results applied when the skill's conditions are met.
|
||||
*/
|
||||
abilities: Ability[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Effects applied when a skill activates.
|
||||
*/
|
||||
export interface Ability {
|
||||
/**
|
||||
* Race mechanic affected by the ability.
|
||||
*/
|
||||
type: 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 13 | 21 | 27 | 28 | 31 | 35;
|
||||
/**
|
||||
* Special scaling type of the skill value.
|
||||
*/
|
||||
value_usage: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 19 | 20 | 22 | 23 | 24 | 25;
|
||||
/**
|
||||
* Amount that the skill modifies the race mechanic in ten thousandths of
|
||||
* whatever is the appropriate unit.
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* Selector for horses targeted by the ability.
|
||||
*/
|
||||
target: 1 | 2 | 4 | 7 | 9 | 10 | 11 | 18 | 19 | 20 | 21 | 22 | 23;
|
||||
/**
|
||||
* Argument value for the ability target, when appropriate.
|
||||
*/
|
||||
target_value?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill groups.
|
||||
* Skills in a skill group replace each other when purchased.
|
||||
*
|
||||
* As a special case, horsegen lists both unique skills and their inherited
|
||||
* versions in the skill groups for both.
|
||||
*/
|
||||
export interface SkillGroup {
|
||||
/**
|
||||
* Skill group ID.
|
||||
*/
|
||||
skill_group: number;
|
||||
/**
|
||||
* Base skill in the skill group, if any.
|
||||
* Either a common (white) skill or an Uma's own unique.
|
||||
*
|
||||
* Some skill groups, e.g. for G1 Averseness, have no base skill.
|
||||
*/
|
||||
skill1?: number;
|
||||
/**
|
||||
* First upgraded version of a skill, if any.
|
||||
* A rare (gold) skill, double circle skill, or an inherited unique skill.
|
||||
*/
|
||||
skill2?: number;
|
||||
/**
|
||||
* Highest upgraded version of a skill, if any.
|
||||
* Gold version of a skill with a double circle version.
|
||||
*/
|
||||
skill3?: number;
|
||||
/**
|
||||
* Negative (purple) version of a skill, if any.
|
||||
*/
|
||||
skill_bad?: number;
|
||||
}
|
||||
|
||||
export const skills = {
|
||||
global: skillGlobal as Skill[],
|
||||
} as const;
|
||||
|
||||
export const skillGroups = {
|
||||
global: groupGlobal as SkillGroup[],
|
||||
} as const;
|
||||
|
||||
export const ZERO_SKILL: Readonly<Skill> = {
|
||||
skill_id: 0,
|
||||
name: "invalid skill",
|
||||
description: "an invalid skill was specified",
|
||||
group: 0,
|
||||
rarity: 1,
|
||||
group_rate: 1,
|
||||
wit_check: false,
|
||||
activations: [],
|
||||
icon_id: 0,
|
||||
} as const;
|
||||
9
zenno/src/lib/prob.ts
Normal file
9
zenno/src/lib/prob.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as math from "mathjs";
|
||||
|
||||
export function binomPMF(p: number, n: number, k: number): number {
|
||||
// Operate in log domain for precision.
|
||||
const lc = math.lgamma(n+1) - math.lgamma(k+1) - math.lgamma(n-k+1);
|
||||
const lpk = k * math.log(p);
|
||||
const lr = (n - k) * math.log(1 - p);
|
||||
return math.exp(lc + lpk + lr);
|
||||
}
|
||||
@@ -1,6 +1,40 @@
|
||||
// Umamusume race mechanics adapted from KuromiAK's doc:
|
||||
// https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?usp=sharing
|
||||
|
||||
import { binomPMF } from "./prob";
|
||||
|
||||
/**
|
||||
* Fundamental stats of umas.
|
||||
*/
|
||||
export enum Stat {
|
||||
Speed,
|
||||
Stamina,
|
||||
Power,
|
||||
Guts,
|
||||
Wit,
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats as a list for easy iteration.
|
||||
*/
|
||||
export const StatList = [Stat.Speed, Stat.Stamina, Stat.Power, Stat.Guts, Stat.Wit] as const;
|
||||
|
||||
/**
|
||||
* Mood levels.
|
||||
*/
|
||||
export enum Mood {
|
||||
Awful = -2,
|
||||
Bad,
|
||||
Normal,
|
||||
Good,
|
||||
Great,
|
||||
}
|
||||
|
||||
/**
|
||||
* Running styles for strategy–phase coefficients.
|
||||
* Great Escape is distinguished as a separate style even though it is
|
||||
* mechanically identical to Front Runner.
|
||||
*/
|
||||
export enum RunningStyle {
|
||||
FrontRunner,
|
||||
PaceChaser,
|
||||
@@ -9,6 +43,9 @@ export enum RunningStyle {
|
||||
GreatEscape,
|
||||
}
|
||||
|
||||
/**
|
||||
* Aptitude or proficiency levels.
|
||||
*/
|
||||
export enum AptitudeLevel {
|
||||
G,
|
||||
F,
|
||||
@@ -20,6 +57,24 @@ export enum AptitudeLevel {
|
||||
S,
|
||||
}
|
||||
|
||||
/**
|
||||
* Aptitude levels as a descending list for easy iterating.
|
||||
*/
|
||||
export const APTITUDE_LEVELS = [
|
||||
AptitudeLevel.S,
|
||||
AptitudeLevel.A,
|
||||
AptitudeLevel.B,
|
||||
AptitudeLevel.C,
|
||||
AptitudeLevel.D,
|
||||
AptitudeLevel.E,
|
||||
AptitudeLevel.F,
|
||||
AptitudeLevel.G,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Race phases.
|
||||
* While last spurt phase is also a phase, it is not distinguished here.
|
||||
*/
|
||||
export enum Phase {
|
||||
EarlyRace,
|
||||
MidRace,
|
||||
@@ -30,7 +85,7 @@ function baseSpeed(raceLen: number): number {
|
||||
return 20 - (raceLen - 2000) / 1000;
|
||||
}
|
||||
|
||||
const strategyPhaseCoeff = [
|
||||
const speedStrategyPhaseCoeff = [
|
||||
[1.0, 0.98, 0.962],
|
||||
[0.978, 0.991, 0.975],
|
||||
[0.938, 0.998, 0.994],
|
||||
@@ -42,22 +97,21 @@ const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as cons
|
||||
|
||||
/**
|
||||
* Calculate an Uma's last spurt target speed.
|
||||
* @param rawSpeed Uma's speed stat. No accounting for mood lol.
|
||||
* @param gutsStat Uma's guts stat.
|
||||
* @param speedStat Adjusted speed stat
|
||||
* @param gutsStat Adjusted guts stat
|
||||
* @param style Running style
|
||||
* @param distance Distance aptitude
|
||||
* @param raceLen Length of the race
|
||||
* @returns Target speed in the last spurt in m/s
|
||||
*/
|
||||
export function spurtSpeed(
|
||||
rawSpeed: number,
|
||||
speedStat: number,
|
||||
gutsStat: number,
|
||||
style: RunningStyle,
|
||||
distance: AptitudeLevel,
|
||||
raceLen: number,
|
||||
): number {
|
||||
const speedStat = rawSpeed <= 1200 ? rawSpeed : 1200 + (rawSpeed - 1200) * 0.5;
|
||||
const spc = strategyPhaseCoeff[style][Phase.LateRace];
|
||||
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
|
||||
const dpm = distanceProficiencyMod[distance];
|
||||
const base = baseSpeed(raceLen);
|
||||
// Expand and rearrange terms from the doccy to make solving for the inverse easier.
|
||||
@@ -91,13 +145,184 @@ export function inverseSpurtSpeed(
|
||||
// spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001 = 0.0041*sqrt(500*speedStat)*dpm
|
||||
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.0041²*dpm²*500) = speedStat
|
||||
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.008405*dpm²) = speedStat
|
||||
const spc = strategyPhaseCoeff[style][Phase.LateRace];
|
||||
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
|
||||
const dpm = distanceProficiencyMod[distance];
|
||||
const base = baseSpeed(raceLen);
|
||||
const nr = spurtSpeed - base * (1.05 * spc + 0.0105) - Math.pow(450 * gutsStat, 0.597) * 0.0001;
|
||||
const r = (nr * nr) / (0.008405 * dpm * dpm);
|
||||
if (r > 1200) {
|
||||
return Math.round(2 * r - 1200);
|
||||
}
|
||||
return Math.round(r);
|
||||
}
|
||||
|
||||
/** Meters per horse length (馬身). */
|
||||
export const HORSE_LENGTH = 2.5;
|
||||
/** Meters per course width (a constant unit of measure). */
|
||||
export const COURSE_WIDTH = 11.25;
|
||||
/** Meters per lane width. */
|
||||
export const LANE_WIDTH = COURSE_WIDTH / 18;
|
||||
|
||||
const accelStrategyPhaseCoeff = {
|
||||
[RunningStyle.FrontRunner]: [1.0, 1.0, 0.996],
|
||||
[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],
|
||||
} as const;
|
||||
|
||||
const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
|
||||
const accelDistanceProficiencyMod = [0.4, 0.5, 0.6, 1, 1, 1, 1, 1] as const;
|
||||
|
||||
/**
|
||||
* Calculate a horse's instantaneous acceleration value.
|
||||
* @param powerStat Final power stat
|
||||
* @param style Running style
|
||||
* @param phase Current race phase for this frame
|
||||
* @param surfaceAptitude Surface aptitude
|
||||
* @param distanceAptitude Distance aptitude; no effect if not given
|
||||
* @param uphill Whether this frame has a positive SlopePer value
|
||||
* @param startDash Whether this frame is in the start dash period, i.e. current speed has not yet reached 85% of the race's base speed
|
||||
* @returns Acceleration in m/s²
|
||||
*/
|
||||
export function acceleration(
|
||||
powerStat: number,
|
||||
style: RunningStyle,
|
||||
surfaceAptitude: AptitudeLevel,
|
||||
phase: Phase,
|
||||
distanceAptitude?: AptitudeLevel,
|
||||
uphill?: boolean,
|
||||
startDash?: boolean,
|
||||
): number {
|
||||
const baseAccel = uphill ? 0.0004 : 0.0006;
|
||||
const startDashMod = startDash ? 24.0 : 0;
|
||||
const spc = accelStrategyPhaseCoeff[style][phase];
|
||||
const spm = surfaceProficiencyMod[surfaceAptitude];
|
||||
const dpm = accelDistanceProficiencyMod[distanceAptitude ?? AptitudeLevel.A];
|
||||
return baseAccel * Math.sqrt(500 * powerStat) * spc * spm * dpm + startDashMod;
|
||||
}
|
||||
|
||||
const phaseDecel = {
|
||||
[Phase.EarlyRace]: -1.2,
|
||||
[Phase.MidRace]: -0.8,
|
||||
[Phase.LateRace]: -1.0,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get the phase-based deceleration value.
|
||||
* @param phase Race phase that the horse is running this frame
|
||||
* @param pdm Whether the horse is currently running in pace-down mode
|
||||
* @param dead Whether the horse has zero or less HP
|
||||
* @returns Current deceleration value in m/s², a negative value
|
||||
*/
|
||||
export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): number {
|
||||
if (dead) {
|
||||
return -1.2;
|
||||
}
|
||||
if (pdm) {
|
||||
// This isn't until 1.5anni.
|
||||
// return -0.5;
|
||||
return phaseDecel[phase];
|
||||
}
|
||||
return phaseDecel[phase];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the speed boost gained from spot struggle.
|
||||
* @param gutsStat Final guts stat
|
||||
* @returns Spot struggle speed boost in m/s
|
||||
*/
|
||||
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,
|
||||
* or at the start of section 9.
|
||||
* @param gutsStat Final guts stat
|
||||
* @param frontAptitude Front runner aptitude level
|
||||
* @returns Spot struggle duration in s
|
||||
*/
|
||||
export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLevel): number {
|
||||
// https://hakuraku.moe/notes/spot-struggle
|
||||
return Math.sqrt(700 * gutsStat) * 0.012 * strategyProficiencyMod[frontAptitude];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns Speed modifier for running uphill, a negative value
|
||||
*/
|
||||
export function uphillMod(powerStat: number, slopePer: number): number {
|
||||
return slopePer * -200/powerStat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the forward speed boost given when moving lanewise while a skill
|
||||
* that grants a lane change speed boost is active.
|
||||
* @param powerStat Final power stat
|
||||
* @returns Move-lane speed modifier in m/s
|
||||
*/
|
||||
export function moveLaneModifier(powerStat: number): number {
|
||||
return Math.sqrt(0.0002 * powerStat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the probability of n of N skills activating.
|
||||
* @param baseWit Base wit stat
|
||||
* @param N Number of skills available, default 1
|
||||
* @param n Number of skills activating, default 1
|
||||
* @returns Probability of exactly n skills out of N passing wit checks
|
||||
*/
|
||||
export function skillWitCheck(baseWit: number, N?: number, n?: number): number {
|
||||
const p = Math.max(0.2, 1 - 90/baseWit);
|
||||
return binomPMF(p, N ?? 1, n ?? 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a skill's actual duration scaled to race length.
|
||||
* @param baseDur Skill's listed duration in s
|
||||
* @param raceLen Length of the race in m
|
||||
* @returns Actual skill duration in s
|
||||
*/
|
||||
export function skillDuration(baseDur: number, raceLen: number): number {
|
||||
return baseDur * raceLen * 0.001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the distance gained from a target speed boost, including
|
||||
* acceleration to the boosted target speed and deceleration back to baseline.
|
||||
* @param speedBonus Difference between baseline and boosted speed in m/s
|
||||
* @param accel Current acceleration value in m/s², or null for instant acceleration
|
||||
* @param decel Current phase-based deceleration value in m/s², a negative value; or null for instant deceleration
|
||||
* @param dur Duration of the boosted speed
|
||||
* @returns Distance gained from the speed boost in m
|
||||
*/
|
||||
export function speedGain(speedBonus: number, dur: number, accel: number | null, decel: number | null): number {
|
||||
// Actual effect of a target speed bonus looks like
|
||||
// speed: __/-----\__
|
||||
// bonus: ======
|
||||
// I.e., the speed bonus duration includes acceleration to the new speed
|
||||
// and does not include the acceleration back to baseline after it ends.
|
||||
const accelTime = accel !== null ? speedBonus / accel : 0;
|
||||
const decelTime = decel !== null ? -speedBonus / decel : 0;
|
||||
if (accelTime >= dur) {
|
||||
// Acceleration is so low that the horse won't reach the boosted target
|
||||
// speed before the effect ends. E.g., G surface aptitude.
|
||||
const peakSpeed = (accel ?? 0) * dur;
|
||||
return 0.5 * (peakSpeed * dur - peakSpeed / (decel ?? 0));
|
||||
}
|
||||
// speedBonus*(dur-accelTime) + speedBonus*accelTime/2 + speedBonus*decelTime/2
|
||||
return speedBonus * (dur + 0.5 * (decelTime - accelTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the chance to enter downhill accel mode each second while running downhill.
|
||||
* @param witStat Final wit stat, including style aptitude modifier
|
||||
* @returns Probability each eligible tick to enter downhill accel mode
|
||||
*/
|
||||
export function downhillAccelEnterChance(witStat: number): number {
|
||||
return witStat * 0.0004;
|
||||
}
|
||||
|
||||
89
zenno/src/lib/runner.ts
Normal file
89
zenno/src/lib/runner.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { Uma } from './data/uma';
|
||||
import { AptitudeLevel, Mood, RunningStyle } from './race';
|
||||
import * as alpha123Umalator from './alpha123Umalator';
|
||||
|
||||
/**
|
||||
* Race runner, i.e. a trained horse.
|
||||
*/
|
||||
export interface Runner {
|
||||
name: string;
|
||||
|
||||
chara_card_id: number;
|
||||
style: RunningStyle;
|
||||
mood: Mood;
|
||||
|
||||
speed: number;
|
||||
stamina: number;
|
||||
power: number;
|
||||
guts: number;
|
||||
wit: number;
|
||||
|
||||
sprint: AptitudeLevel;
|
||||
mile: AptitudeLevel;
|
||||
medium: AptitudeLevel;
|
||||
long: AptitudeLevel;
|
||||
front: AptitudeLevel;
|
||||
pace: AptitudeLevel;
|
||||
late: AptitudeLevel;
|
||||
end: AptitudeLevel;
|
||||
turf: AptitudeLevel;
|
||||
dirt: AptitudeLevel;
|
||||
|
||||
skills: number[];
|
||||
unique_level: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new runner with baseline stats.
|
||||
* @param name Name to apply to the runner
|
||||
* @param base_uma Character card (trainee or uma) to use for aptitudes; otherwise all aptitudes are A
|
||||
* @returns Baseline runner
|
||||
*/
|
||||
export function newRunner(name?: string, base_uma?: Uma): Runner {
|
||||
return {
|
||||
name: name ?? '',
|
||||
chara_card_id: base_uma?.chara_card_id ?? 0,
|
||||
// TODO(zeph): default running style
|
||||
style: RunningStyle.FrontRunner,
|
||||
mood: Mood.Normal,
|
||||
speed: 1200,
|
||||
stamina: 1200,
|
||||
power: 1200,
|
||||
guts: 1200,
|
||||
wit: 1200,
|
||||
sprint: base_uma?.sprint ?? AptitudeLevel.A,
|
||||
mile: base_uma?.mile ?? AptitudeLevel.A,
|
||||
medium: base_uma?.medium ?? AptitudeLevel.A,
|
||||
long: base_uma?.long ?? AptitudeLevel.A,
|
||||
front: base_uma?.front ?? AptitudeLevel.A,
|
||||
pace: base_uma?.pace ?? AptitudeLevel.A,
|
||||
late: base_uma?.late ?? AptitudeLevel.A,
|
||||
end: base_uma?.end ?? AptitudeLevel.A,
|
||||
turf: base_uma?.turf ?? AptitudeLevel.A,
|
||||
dirt: base_uma?.dirt ?? AptitudeLevel.A,
|
||||
skills: base_uma != null ? [base_uma.unique] : [],
|
||||
unique_level: 4,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a runner from an external source.
|
||||
* @param obj Decoded object to import
|
||||
* @param name Name or memo to apply to the runner
|
||||
* @returns Imported runner, or null if import was not possible
|
||||
*/
|
||||
export function importRunner(obj: unknown, name?: string): Runner | null {
|
||||
// TODO(zeph): check for keys that identify the uma source
|
||||
if (typeof obj === 'object') {
|
||||
try {
|
||||
const r = alpha123Umalator.load(obj as alpha123Umalator.ImportUma, name);
|
||||
// TODO(zeph): validate?
|
||||
return r;
|
||||
} catch (exc) {
|
||||
console.warn('failed to import', obj, 'as alpha123 umalator:', exc);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
console.warn('no guess on how to import', obj);
|
||||
return null;
|
||||
}
|
||||
@@ -19,13 +19,16 @@
|
||||
<span class="flex-1 text-center">
|
||||
<a href={resolve('/')} class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
|
||||
<a href={resolve('/spurt')} class="mx-8 my-1 inline-block">Spurt Speed</a>
|
||||
<a href={resolve('/mspeed')} class="mx-8 my-1 inline-block">Mechanical Speed</a>
|
||||
<a href={resolve('/convo')} class="mx-8 my-1 inline-block">Lobby Conversations</a>
|
||||
</span>
|
||||
</nav>
|
||||
<div class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
|
||||
{@render children()}
|
||||
</div>
|
||||
<footer class="inset-x-0 bottom-0 mt-8 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900">
|
||||
<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"
|
||||
>
|
||||
Umamusume: Pretty Derby tools by <a href="https://zephyrtronium.date/" target="_blank" rel="noopener noreferrer"
|
||||
>zephyrtronium</a
|
||||
>.<br />
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
<a href={resolve('/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other distance aptitudes
|
||||
and running styles.
|
||||
</li>
|
||||
<li>
|
||||
<a href={resolve('/mspeed')}>Front Runner Mechanical Speed Comparator</a> — Compare spot struggle and lane combo to the effects
|
||||
of individual skills.
|
||||
</li>
|
||||
<li>
|
||||
<a href={resolve('/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking
|
||||
them quickly.
|
||||
|
||||
31
zenno/src/routes/doc/Sec.svelte
Normal file
31
zenno/src/routes/doc/Sec.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
h?: `${1 | 2 | 3 | 4 | 5 | 6}`;
|
||||
id: string;
|
||||
children: Snippet;
|
||||
class?: ClassValue | null;
|
||||
}
|
||||
|
||||
let { h = '1', id, children, class: className }: Props = $props();
|
||||
|
||||
const tag = $derived('h' + h);
|
||||
const href = $derived('#' + id);
|
||||
const sign = $derived.by(() => {
|
||||
switch (h) {
|
||||
case '1':
|
||||
return '';
|
||||
case '2':
|
||||
return '§ ';
|
||||
default:
|
||||
return '¶ ';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:element this={tag} {id} class={className}>
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||
<a {href}>{sign} {@render children()}</a>
|
||||
</svelte:element>
|
||||
665
zenno/src/routes/doc/frbm/+page.svelte
Normal file
665
zenno/src/routes/doc/frbm/+page.svelte
Normal file
@@ -0,0 +1,665 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||
import { AptitudeLevel, downhillAccelEnterChance, moveLaneModifier, skillWitCheck, spotStruggleDuration, spotStruggleSpeed, Stat, uphillMod } from '$lib/race';
|
||||
import Skill from '$lib/Skill.svelte';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import Sec from '../Sec.svelte';
|
||||
|
||||
const witCheckSeries: ComputedSeries[] = [
|
||||
{ label: "1/1", y: (x) => 100*skillWitCheck(x, 1, 1) },
|
||||
{ label: "1/2 or 2/2", y: (x) => 100*(skillWitCheck(x, 2, 1) + skillWitCheck(x, 2, 2)) },
|
||||
{ label: "2/2", y: (x) => 100*skillWitCheck(x, 2, 2) },
|
||||
];
|
||||
const ssBoostSeries: ComputedSeries = { label: "Target Speed Boost", y: (x) => spotStruggleSpeed(x) };
|
||||
const ssDurSeries: ComputedSeries[] = [
|
||||
{ label: "Front Runner S", y: (x) => spotStruggleDuration(x, AptitudeLevel.S) },
|
||||
{ label: "Front Runner A", y: (x) => spotStruggleDuration(x, AptitudeLevel.A) },
|
||||
];
|
||||
const laneComboSeries: ComputedSeries = {label: "Target Speed Boost", y: (x) => moveLaneModifier(x) };
|
||||
const lcYRule: HorizontalRule[] = [
|
||||
{ label: "+0.35", y: 0.35 },
|
||||
{ label: "+0.45", y: 0.45 },
|
||||
];
|
||||
const uphillSeries: ComputedSeries[] = [
|
||||
{ label: "+2 Hill", y: (x) => uphillMod(x, 2.0) },
|
||||
{ label: "+1.5 Hill", y: (x) => uphillMod(x, 1.5) },
|
||||
{ label: "+1 Hill", y: (x) => uphillMod(x, 1.0) },
|
||||
];
|
||||
const uphillYRule: HorizontalRule[] = [
|
||||
{ label: "Dominator", y: -0.25 },
|
||||
];
|
||||
const downhillSeries: ComputedSeries[] = [
|
||||
{ label: "Style S", y: (x) => downhillAccelEnterChance(x * 1.1) * 100 },
|
||||
{ label: "Style A", y: (x) => downhillAccelEnterChance(x) * 100 },
|
||||
];
|
||||
</script>
|
||||
|
||||
<article class="mx-auto max-w-4xl text-justify">
|
||||
<Sec h="1" id="top" class="text-center">Front Runner Black Magic</Sec>
|
||||
<p>
|
||||
Front runners are playing a fundamentally different game versus other running styles. Building them isn't too hard, and their
|
||||
careers tend to be easy, but some of the things you need (and don't need) to make a <i>really good</i> front runner are surprising.
|
||||
</p>
|
||||
<p>
|
||||
This document is advanced material. The target audience intends to win Champions Meet Group A Finals and either wants to use
|
||||
front runners to do it or wants to understand what front runners they need to beat. This is meant for players who are already
|
||||
strong at training: players who can take a target stat line and skill set and turn it into a horse. This document is about the
|
||||
mechanics that determine what those stat lines and skill sets should be.
|
||||
</p>
|
||||
|
||||
<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
|
||||
button on Umamusume. Since then, I have been a mostly-F2P player, with the single exception of the First Anniversary SSR pick
|
||||
ticket. (I haven't even spent the accompanying paid carats.)
|
||||
</p>
|
||||
<p>
|
||||
I'm committed to running exclusively triple fronts for every Champions' Meet, starting since CM8 Sagittarius Cup (Arima
|
||||
Kinen). When I'm not training for CM, I'm usually making front runner parents, and was at one time the owner of the Seiun Sky
|
||||
with the most white sparks on global. I have a lot of experience training, running, and watching front runners.
|
||||
</p>
|
||||
<p>
|
||||
That said, most of the information here is ultimately my interpretations of <a
|
||||
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?usp=sharing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">KuromiAK's Race Mechanics doc</a
|
||||
>. 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>.
|
||||
</p>
|
||||
<p>
|
||||
I want to share the knowledge I've accrued about front runners, because teaching is my favorite thing. Definitely not just to
|
||||
rationalize running triple fronts for every CM even though it's not actually very good and most of my favorite horses are late
|
||||
surgers.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="mechanics">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
|
||||
16, late race is sections 17 to 20, and last spurt phase is sections 21 to 24. Spot Struggle can start between 150m and the
|
||||
end of section 5, and is forced to end at the start of section 9. Position Keep ends after section 10.
|
||||
</p>
|
||||
<p>
|
||||
The numeric value of acceleration depends on the Power stat, dueling, surface aptitude, uphills, race phase, running style. At
|
||||
the start of early race, horses accelerate from 3 m/s to the early race <i>base target speed</i>, which varies by race
|
||||
distance and running style but is generally on the order of 20 m/s. At the start of late race, if they have enough HP remaining for their last spurt, horses
|
||||
accelerate from the mid race base target speed to their spurt speed, which varies by speed stat, distance aptitude, running style, race
|
||||
distance, and guts stat, in decreasing order of effect. "Last spurt" and "last spurt phase" are different and
|
||||
unrelated things; the latter is only used in the condition for <Skill skill={200512} hint="homestretch haste" mention />.
|
||||
</p>
|
||||
<p>
|
||||
Speed skills add a flat amount of target speed, generally +0.15 m/s for white skills, +0.25 m/s for double circle skills and
|
||||
some inherited uniques, +0.35 m/s for gold skills and most speed uniques, and +0.45 m/s for a handful of speed uniques. Accel
|
||||
skills similarly add a flat amount of acceleration, typically +0.1 or +0.2 m/s² for white skills and inherited uniques, or +0.3
|
||||
or +0.4 m/s² for gold skills and uniques.
|
||||
</p>
|
||||
|
||||
<Sec h="3" id="runaway">Runaway</Sec>
|
||||
<p>
|
||||
The skill <Skill skill={202051} hint="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.")
|
||||
</p>
|
||||
<p>
|
||||
Runaways are still front runners for all purposes.
|
||||
The main difference is just different numbers for things like base speed and acceleration, stamina to HP conversion, and distance thresholds for running modes.
|
||||
Other mechanics that are specific to front runners also apply to runaways.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="win-cons">Win Conditions</Sec>
|
||||
<p>
|
||||
On Global today, competitive horses usually have stat lines that are pretty similar to each other.
|
||||
Races, therefore, are more often won by skills – typically acceleration skills that activate at the start of late race.
|
||||
Front runners have strong options.
|
||||
</p>
|
||||
<ul class="list-disc pl-4 mb-4">
|
||||
<li>
|
||||
<Skill skill={900201} hint="angling" />, sometimes called Rod, is the second best skill in the game.
|
||||
Because only the horse in first place gets it, everything about training front runners becomes a matter of being in front at the start of late race, true to name.
|
||||
</li>
|
||||
<li>
|
||||
On long distance tracks, <Skill skill={900681} hint="vc" /> takes that role instead.
|
||||
The front two horses get it, which opens the opportunity for multi-front builds using <Skill skill={200492} hint="nn" mention />/<Skill skill={200491} hint="nsm" mention /> –
|
||||
especially because VC tracks aren't subject to the final corner spread that makes those skills worse on sprints and miles –
|
||||
but otherwise the function is the same.
|
||||
</li>
|
||||
<li>
|
||||
On those sprints where Angling is dead, the front-specific options include <Skill skill={900141} hint="pasta" /> (VPP, or Pasta) and <Skill skill={910451} hint="mummy creek" /> (HCreek),
|
||||
It takes both of them to equal Angling, so such sprints may be better served gambling on <Skill skill={200651} hint="turbo sprint" mention />, <Skill skill={200371} hint="rushing gale" mention />, and possibly <Skill skill={200551} hint="unrestrained" mention /> instead.
|
||||
Front runners are especially strong on sprints for <a href="#spot-struggle">other reasons</a> anyway.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<Skill skill={200491} hint="nsm" /> is the best skill in the game.
|
||||
Unfortunately, for the most part, it's bad on front runners; generally not a win condition.
|
||||
Activating NSM requires not being in first, which means whoever <i>was</i> used Angling and is pulling away from you before you accumulate the blocked time to activate it.
|
||||
Again, VC tracks may be an exception if you specifically build for it.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="pdm">Pace Down Mode</Sec>
|
||||
<p>
|
||||
During the first 41.67% of the race, <i>position keep</i> is busy arranging each running style into their respective packs.
|
||||
The primary mechanism for this is pace down mode (PDM), 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 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 lesser running styles, early race and sometimes mid race speed skills are effectively converted from distance gain into HP conservation via PDM.
|
||||
The thing that really makes front runners good 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="2" id="skill-timing">Skill Timing</Sec>
|
||||
<p>Thought experiment.</p>
|
||||
<p>
|
||||
Picture two cars driving on a straight freeway, both at exactly 59 mph because I am American, adjacent lanes, keeping exactly
|
||||
side by side.
|
||||
</p>
|
||||
<p>
|
||||
The one on the right then drives 1 mph faster for three seconds, creating a slight gap between them before returning to the
|
||||
previous speed. They now maintain this new gap.
|
||||
</p>
|
||||
<p>
|
||||
There is a 65 mph speed limit sign. As each of the cars pass it, they accelerate at identical rates from 59 to 69 mph over a
|
||||
duration of exactly 10.2 seconds.
|
||||
</p>
|
||||
<p>
|
||||
Since the car on the right is slightly ahead from the speed skill it used, it reaches the speed limit sign first, so it starts
|
||||
accelerating first.
|
||||
</p>
|
||||
<p>
|
||||
Until the left car reaches the sign, the right car is building a speed advantage. Having a higher speed during the accel
|
||||
period, it continually increases the gap it had, until both of them have reached the new target speed.
|
||||
</p>
|
||||
<p>
|
||||
Now the left car drives 1 mph faster for three seconds. It closes the gap between them by the same distance that the right
|
||||
car's speed skill had done prior to the speed limit change.
|
||||
</p>
|
||||
<p>
|
||||
However, since the right car also added a distance advantage over the accel period, it remains slightly ahead of the left car.
|
||||
</p>
|
||||
<p>
|
||||
This thought experiment shows that speed skills are actually more valuable before late race than during it. Thus, front
|
||||
runners not having to worry about PDM is even more of an advantage.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="gate-skills">Gate Skills</Sec>
|
||||
<p>
|
||||
Gate skills are <Skill skill={201601} hint="gw" /> (GW), <Skill skill={200531} hint="ttl" /> (TTL), and <Skill skill={200431} hint="conc" /> (Conc), as well as all green skills including <Skill skill={202051} hint="runaway" mention />.
|
||||
These skills activate the moment the race starts.
|
||||
</p>
|
||||
<p>
|
||||
GW is an absolutely mandatory skill for all front runners.
|
||||
Even runaway blockers should have it, otherwise they will be passed by the normal fronts they're trying to block.
|
||||
It requires three other gate skills, which should be active greens to avoid overreliance on wit checks.
|
||||
For reference, the chart below shows proc chances of one of one, one of two, or two of two skills with wit checks.
|
||||
</p>
|
||||
<div class="w-full h-60 md:h-96 mb-4">
|
||||
<StatChart class="h-full w-full max-w-3xl mx-auto" stat={Stat.Wit} y={witCheckSeries} yLabel="% Chance" range={[50, 100]} xRule={1000} />
|
||||
</div>
|
||||
<p>
|
||||
TTL must be combined with GW if they want any chance of being first out of early race.
|
||||
Since the main source of it is the Mihono Bourbon Wit SSR from the first Halloween event, VBourbon can suffice with its white version <Skill skill={200532} hint="early lead" mention /> and get to the front with her unique instead.
|
||||
(Her other option is the Twin Turbo SSR that does generate a lot of stats but requires winning three 50/50s to get the gold skill.)
|
||||
</p>
|
||||
<p>
|
||||
Conc is less critical.
|
||||
It's worth taking on horses who have it, but it isn't worth using support card slots just to get it.
|
||||
On the other hand, its white version <Skill skill={200432} hint="focus" /> is bad; its only real use is as a backup gate skill for GW when you don't have enough greens available.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="spot-struggle">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
|
||||
had a horse join while in 6th a couple times.
|
||||
</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.
|
||||
</p>
|
||||
<div class="grid w-full h-60 md:h-96 grid-cols-2 mb-4">
|
||||
<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]} />
|
||||
</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.
|
||||
</p>
|
||||
<p>
|
||||
In medium+ races, the extra HP consumption is a serious consideration; front runners need more stamina and recoveries than other styles.
|
||||
At 1600m and shorter, the fact that Spot Struggle doesn't scale with race distance means that it can be worth multiple gold speed skills in total distance gained.
|
||||
See the <a href={resolve('/mspeed')}>mechanical speed calculator</a> for precise analysis.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="lane-combo">Lane Combo</Sec>
|
||||
<p>
|
||||
While under the influence of a skill that increases lane movement speed (shoe icon skills), and while actively changing lanes (i.e. moving sideways), horses gain a (forward) target speed boost that scales with power.
|
||||
This was a change Global received with the Unity Cup scenario.
|
||||
</p>
|
||||
<div class="w-full h-60 md:h-96 mb-4">
|
||||
<StatChart class="h-full w-full max-w-3xl mx-auto" stat={Stat.Power} y={laneComboSeries} yLabel="Speed Boost" yRule={lcYRule} range={[0.2, 0.5]} />
|
||||
</div>
|
||||
<p>
|
||||
Front runners have access to the skill <Skill skill={201262} hint="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.
|
||||
</p>
|
||||
<p>
|
||||
We get advantage from move lane speed modifier by following DD with <Skill skill={200452} hint="pp" /> or <Skill skill={210052} hint="ignited wit" />.
|
||||
DD created an opportunity for those return skills to convert into huge forward speed.
|
||||
This setup is called <i>lane combo</i>.
|
||||
</p>
|
||||
<p>
|
||||
Lane combo is only viable on tracks where early race ends before or at most very early into the first corner.
|
||||
Since PP and Ignited WIT are <span class="font-mono">phase_random==0</span> skills, they can activate at the very end of late race.
|
||||
If there's a corner there, and your horse is still on the outside from DD, you are now physically running a longer distance than those on the inside.
|
||||
That can more than undo the gain from the lane combo itself.
|
||||
</p>
|
||||
<p>
|
||||
The <a href={resolve('/mspeed')}>mechanical speed calculator</a> has an approximation of lane combo's benefit.
|
||||
A more precise lane combo simulator <a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">exists</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 -->
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="slopes">Slopes</Sec>
|
||||
<p>
|
||||
Different slopes can be of different angles; the <i>SlopePer</i> parameter is positive for uphills and negative for downhills.
|
||||
SlopePer values that currently exist on tracks include 1, 1.5, and 2, positive or negative.
|
||||
</p>
|
||||
|
||||
<Sec h="3" id="uphills">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.
|
||||
</p>
|
||||
<div class="w-full h-60 md:h-96 mb-4">
|
||||
<StatChart class="w-full max-w-3xl h-full mx-auto" stat={Stat.Power} y={uphillSeries} yLabel="Speed Modifier (m/s)" yRule={uphillYRule} range={[-2, 0]} />
|
||||
</div>
|
||||
<p>
|
||||
Note that surface aptitude <i>does not</i> affect uphill speed, nor power generally.
|
||||
It only affects acceleration.
|
||||
</p>
|
||||
<p>
|
||||
The practical impact is that steep early- and mid-race hills filter out front runners with low power.
|
||||
Even with an otherwise perfect build, an 800 power VBourbon is likely to be passed by a 1280 power (<Skill skill={200152} hint="firm" mention /> + <Skill skill={200282} hint="comp spirit" mention />) Seiun Sky.
|
||||
</p>
|
||||
|
||||
<Sec h="3" id="downhills">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 flat multiplier.
|
||||
</p>
|
||||
<p>
|
||||
Entering downhill accel mode requires passing a wit check.
|
||||
The success rate scales linearly with wit.
|
||||
Style aptitude <i>does</i> affect the chance to pass the check.
|
||||
Its duration is random with a geometric distribution; it does not scale with stats.
|
||||
</p>
|
||||
<div class="w-full h-60 md:h-96 mb-4">
|
||||
<StatChart class="w-full max-w-3xl h-full mx-auto" stat={Stat.Wit} y={downhillSeries} yLabel="Entry Chance (% each second)" range={[0, 60]} />
|
||||
</div>
|
||||
<p>
|
||||
Similar to uphills disproportionately rewarding front runners with higher power, downhills tend to reward high wit.
|
||||
However, the random elements of downhill accel mode mean that lower wit horses may still keep up on downhills, depending on luck.
|
||||
Conversely, the HP savings on long downhills can be enough to drop a recovery skill or two on some tracks.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="section-speed">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.)
|
||||
</p>
|
||||
<p>
|
||||
Section speed is generally very small; at 1200 wit with style S, it has a range of about -0.15% to 0.5% of race base speed.
|
||||
At 2000m, that translates to an actual speed range of 19.97 to 20.10 m/s.
|
||||
</p>
|
||||
<p>
|
||||
Unlike anything affected by the speed stat, though, it applies during the early and mid race, where front runners are trying to become frontest runners.
|
||||
Wit difference alone can
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="phase-speed">Phase Speed</Sec>
|
||||
<p>
|
||||
Race base speed is multiplied by the strategy–phase coefficient for each horse.
|
||||
As the name suggests, SPC 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 makes pace chaser promotion scary in late race (for those not using any of the correct running style).
|
||||
</p>
|
||||
<p>
|
||||
Front runners, and even moreso runaways, have particularly punishing SPC for late race.
|
||||
This makes sense; if they weren't forced to be substantially slower than the late surgers they're thirty meters ahead of at late race start, then they would be guaranteed to win every time.
|
||||
</p>
|
||||
<p>
|
||||
Late race, or more precisely the last spurt, is also the only place where the speed stat and distance aptitude apply.
|
||||
In terms of lengths gained, distance S actually does more for front runners than any other style due to SPC.
|
||||
</p>
|
||||
|
||||
<Sec h="2" id="stats">Stats</Sec>
|
||||
<Sec h="3" id="speed">Speed</Sec>
|
||||
<!-- speed matters less than for other styles; corollary: distance S matters less -->
|
||||
<Sec h="3" id="power">Power</Sec>
|
||||
<!-- uphills; surface S -->
|
||||
<Sec h="3" id="guts">Guts Actually Matters (Sometimes)</Sec>
|
||||
<!-- spot struggle (again?) -->
|
||||
<Sec h="3" id="wit">Wit</Sec>
|
||||
<!-- position keep; front S -->
|
||||
<Sec h="2" id="skills">Skills</Sec>
|
||||
<Sec h="3" id="gate-skills">Gate Skills</Sec>
|
||||
<!-- gw, ttl, conc -->
|
||||
<Sec h="3" id="lane-combo">Lane Combo</Sec>
|
||||
<!-- dd, pp, ignited wit -->
|
||||
<Sec h="3" id="front-speed-skills">Speed Skills – Front Runner Exclusives</Sec>
|
||||
<!-- escape artist, front/distance corners/straights, leader's pride, speed eater (mile) -->
|
||||
<Sec h="3" id="generic-speed-skills">Speed Skills – Generic</Sec>
|
||||
<!-- thh, ramp up, pto, slipstream -->
|
||||
<Sec h="3" id="spurt-skills">Spurt Skills</Sec>
|
||||
<!-- barcarole, triumphant pulse, all i've got -->
|
||||
<Sec h="3" id="others-skills">Other Horses' Skills</Sec>
|
||||
<!-- all-seeing eyes; sfv, u=ma2, generally pos 3-4 skills -->
|
||||
<Sec h="2" id="teams">CM Teams</Sec>
|
||||
<Sec h="3" id="solo-front">Solo Front</Sec>
|
||||
<!-- front is not an ace; should probably be a runaway -->
|
||||
<Sec h="3" id="double-front">Double Front</Sec>
|
||||
<!-- ace front and backup -->
|
||||
<Sec h="3" id="triple-front">Triple Front</Sec>
|
||||
<!-- ace front, support, and backup; killing pos 3-4 skills -->
|
||||
<Sec h="2" id="umas">Front Runners</Sec>
|
||||
<Sec h="3" id="coc">Christmas Oguri Cap</Sec>
|
||||
<Sec h="3" id="taiki-shuttle">Taiki Shuttle & Curren Chan</Sec>
|
||||
<Sec h="3" id="future-fronts">Future Important Fronts</Sec>
|
||||
<!-- rickey, kitasan white, halloween mayano -->
|
||||
<Sec h="3" id="future-runaways">Future Runaways</Sec>
|
||||
<Sec h="2" id="career">Career</Sec>
|
||||
<p>
|
||||
Most front runners enjoy easy careers thanks to strong kits and little chance to be blocked. This chapter details the minutia
|
||||
of career races, especially in MANT.
|
||||
</p>
|
||||
<Sec h="3" id="career-skills">Taking Skills</Sec>
|
||||
<p>
|
||||
Contrary to advice I sometimes see, you can, in fact, take skills during career without Fast Learner. When you take skills
|
||||
mid-run without Fast Learner, and you happen to get it later, you effectively lose 10% of the SP you spent to that point.
|
||||
However, you also <i>prune</i> hints: taking the first level of a skill removes it from the pools for support card hint bubbles,
|
||||
MANT rivals, UC bursts, and everything else except events that grant specific hints. Each front-specific skill you own improves
|
||||
your chance of getting skills you don't have hints for.
|
||||
</p>
|
||||
<p>
|
||||
<b>Early Lead</b> is a snap take skill. The <i>only</i> time to sit on Early Lead is when you happen to get the first +1 hint
|
||||
the turn before inspiration. (Even then, I'd probably still take it.) Early Lead is one of the strongest skills in terms of
|
||||
lengths gained, it applies to all tracks and conditions, and <i>it saves late starts</i>, which are your only source of losses
|
||||
on most races after junior year. Moreover, it has a base cost of only 120 SP; even if you do get Fast Learner after taking it,
|
||||
your opportunity cost was 12 SP. If you prune Early Lead and a hint lands on Fast-Paced or Leader's Pride instead, you gave up
|
||||
that potential 12 SP to save 18. It's incredibly good to take early.
|
||||
</p>
|
||||
<p>
|
||||
On parent runs, or exactly one of your three CM horses, <b>Lone Wolf</b> is another snap take. Base cost of 60 SP for +40 speed,
|
||||
which can secure a lot of races, especially early in career. Be extremely careful not to take it on multiple horses on a team. Save
|
||||
and quit from the career if you need to check. It's technically better to have it on two horses than zero, but it's tremendously
|
||||
better than that to have it on one.
|
||||
</p>
|
||||
<p>
|
||||
<b>Angling and Scheming</b> is a strong consideration as a mid-career take. Inheritance events are more likely to activate green
|
||||
sparks than white sparks, so the risk of missing out on SP by taking it early is higher. However, Angling is an almost automatic
|
||||
win condition for career (outside a few certain tracks). Taking Angling early can save a lot of clocks, and it can rescue runs that
|
||||
don't get what is normally the minimum speed to win races before summer.
|
||||
</p>
|
||||
<p>
|
||||
<b>Front Runner Savvy</b> is a skill you will pretty much always want at least the first level of. Wit is a strong stat for front
|
||||
runners, and Savvy is a guaranteed Groundwork trigger. It's also the second cheapest front-specific skill, after Dodging Danger.
|
||||
On parent runs, it might be worth sitting on it until the +2 or +3 hint, because taking the second level gives a slightly boosted
|
||||
chance to generate the spark, and hints save twice as much SP on the double circle.
|
||||
</p>
|
||||
<p>
|
||||
<b>Front Runner Straightaways</b> and <b>Corners</b> are strong and cheap. If you've taken Early Lead and Angling, they probably
|
||||
won't change the outcomes of any races, but it's still reasonable to take the first level to prune. As a corollary, outside parent
|
||||
runs, you should have a specific distance in mind, so your distance straights/corners should also be quick takes.
|
||||
</p>
|
||||
<p>
|
||||
If you get a +3 hint, <b>Focus</b> can act as a backup to Early Lead to prevent late starts that can kill your horse. Without a
|
||||
+3 hint, it's expensive for the magnitude of its effect. It's also not a great skill for an ace on its own, so it's pretty skippable
|
||||
in general if you aren't expecting Conc.
|
||||
</p>
|
||||
<Sec h="3" id="niigata-1600">Niigata Junior Stakes</Sec>
|
||||
<p>
|
||||
Niigata Junior Stakes is the first non-sprint graded race in career, which means it's very likely to be one you run. It's also
|
||||
an oddly anti-front race. Late race starts a good bit past the final corner, which means front runners don't have any skills
|
||||
that can secure a win. (Unless you're inheriting Pasta? But VPP isn't really a good take mid-career.)
|
||||
</p>
|
||||
<p>
|
||||
An interesting consequence of the shape of Niigata 1600 is that duels can start before late race. If you're doing a guts
|
||||
build, having that happen will give quite a good chunk of accel and speed for the entire spurt, which is usually enough to get
|
||||
the win. Duels are also pretty likely, because career races have more runners—as long as you're not a solo front.
|
||||
</p>
|
||||
<p>
|
||||
If you do commit to the race and find yourself as the only front runner, switch to pace. You cannot win this race as a solo
|
||||
front.
|
||||
</p>
|
||||
<p>
|
||||
As a corollary, you also cannot win this race with B mile. A miraculous start can get to 350 speed for this race; a B mile
|
||||
runner with 350 speed is equivalent to an A mile runner with only 207 speed in career. See the <a href={resolve('/spurt')}
|
||||
>spurt calculator</a
|
||||
>. (This race is why I made it.)
|
||||
</p>
|
||||
<Sec h="3" id="jbc-sprint">JBC Sprint</Sec>
|
||||
<p>
|
||||
An even more anti-front track is Ooi 1200 Dirt. This one is actively malicious. Visually, it looks like late race starts on a
|
||||
corner, but the portion before the stretch is a special <i>neither corner nor straight</i> property. That means Angling won't activate,
|
||||
and Pasta is delayed (though still within the accel period).
|
||||
</p>
|
||||
<p>
|
||||
Fortunately, JBC Sprint is after summer, which means you should be able to stat diff your opponents. If you are planning to
|
||||
win this race, e.g. for the +30 stat epithet for doing it twice, you may want to prioritize a bit of extra speed training, or
|
||||
take Front Straights/Corners. Don't be too surprised if you lose a clock or five to a Taiki Shuttle rival.
|
||||
</p>
|
||||
<Sec h="3" id="3k">Kikuka Sho & Tenno Sho (Spring)</Sec>
|
||||
<p>
|
||||
The main concern with 3K races is always stamina. Front runners are punished on long distances because they convert stamina to
|
||||
HP less efficiently than other styles.
|
||||
</p>
|
||||
<p>
|
||||
A stat line like x/450/x/500/600 should be enough for a guts/wit build to win Kikuka Sho against most rivals, possibly at the
|
||||
expense of a clock or two. TSS seems to need something more like x/700/x/700/700 if you don't have any recoveries; I haven't
|
||||
tested much without them, because I don't like throwing away my runs.
|
||||
</p>
|
||||
<p>
|
||||
Since approximately every front runner career will either be Valentine's Bourbon, whose unique skill has a recovery component,
|
||||
or have Bourbon Wit, who gives the option for a guaranteed Moxie hint on her first chain event, you'll almost always have a
|
||||
recovery available to take. Kitasan Black is the exception, since she has TTL built in. Regardless, if you end up overstam at
|
||||
the end of the run, buying Moxie can be -162 SP, which is certainly not a trivial amount.
|
||||
</p>
|
||||
<p>
|
||||
An alternative option to buying a recovery is to switch to Late Surger for those races. I've won Kikuka Sho with circa 300
|
||||
stamina this way. Personally, I've decided I prefer buying Moxie in MANT so that rivals have a chance to give a useful skill,
|
||||
but it's probably the wrong choice.
|
||||
</p>
|
||||
<p>
|
||||
All that said, the stamina requirement is instantly much higher if the rival is Super Creek, Mejiro McQueen, or Rice Shower.
|
||||
As rivals, those three will have very strong HP-oriented builds: on TSS, 650 stamina and at least one gold recovery. You will
|
||||
burn clocks rolling for them to fail wit checks unless you also have a gold.
|
||||
</p>
|
||||
<p>
|
||||
One last consideration for front runners on 3K races: Angling is a dead skill. If you're borderline on stamina, you'll have a
|
||||
hard time if you're on Long C.
|
||||
</p>
|
||||
<Sec h="3" id="kitasan-black">Kitasan Black</Sec>
|
||||
<p>
|
||||
Kitasan Black doesn't get the easy careers that other front runners do. For one thing, if you're training Kitasan, you
|
||||
probably don't have Sei as a parent since their uniques don't mesh (except on Nakayama 2500). Kitasan's own unique is also
|
||||
very weak outside of long races and unable to even activate on some miles, notably Niigata 1600. And for early races, notably
|
||||
Niigata 1600, you likely won't even have the opportunity to get Front Straights/Corners, especially since you don't need
|
||||
Bourbon Wit.
|
||||
</p>
|
||||
<p>
|
||||
Kitasan benefits a lot from running as a Pace Chaser early on, just for the higher effective speed. Rivals are best defeated
|
||||
with your intended style, but in career, winning is more important than front running.
|
||||
</p>
|
||||
<Sec h="2" id="cm">My CM Teams</Sec>
|
||||
<Sec h="3" id="cm13">CM13 – Taurus Cup (Tokyo Derby)</Sec>
|
||||
<p>
|
||||
Maruzensky's unique is live as an order≤5 for approximately everyone.
|
||||
Filling the ranks with front runners should be a strong means to delay it for later positions, especially COC.
|
||||
</p>
|
||||
<p>
|
||||
I even considered using Maruzensky herself, since she's a front runner.
|
||||
That line of thought led me to some interesting experiments in Umalator.
|
||||
Redshift hits 25m into the start of late race, as a 0.4 accel on Maruzen and 0.2 inherited, just like Angling.
|
||||
It turns out that that delay has a substantial impact.
|
||||
Sei with Redshift beats Maruzen with Angling by about 0.4 lengths.
|
||||
</p>
|
||||
<p>
|
||||
The story doesn't end there, either.
|
||||
As it turns out, Redshift gains less than half a length for Sei.
|
||||
Ines Fujin's unique (which has a strong version on Tokyo turf specifically) is worth about 0.2 lengths more!
|
||||
So, for Sei specifically, Ines Fujin is the ideal inherit, not Maruzensky.
|
||||
</p>
|
||||
<p>
|
||||
Realistically, my team comp probably should be Seiun Sky, VBourbon, and Maruzensky.
|
||||
However, we run our oshis, and Silence Suzuka is my favorite front runner, so she's going in.
|
||||
The question then becomes whether to run VBourbon or Maruzen.
|
||||
</p>
|
||||
<p>
|
||||
Maruzensky has the advantage of working even as far back as 5th place.
|
||||
However, what does that actually beat?
|
||||
She's a front runner, so she can only outrun another front runner, and only if she has a significantly higher spurt speed than whoever got Angling.
|
||||
That basically means she needs to be a guts horse hoping for duels, which in turn means probably both of Professor and Escape Artist aren't happening. That's tough.
|
||||
</p>
|
||||
<p>
|
||||
On the other hand, Maruzen isn't relying on a wit check for her big accel.
|
||||
She's also free to take VBourbon or Ines Fujin as her non-Angling inherit, whereas VBourbon is forced into Sei and Maruzen parents.
|
||||
So, basically, what Maruzen would be trying to beat is a VBourbon who hits both Angling and Redshift (>80% chance), matching 0.4 accels but winning in spurt speed.
|
||||
</p>
|
||||
<p>
|
||||
I'm not convinced that's good for my comp.
|
||||
I'd rather just be that VBourbon, having approximately every good front runner skill built in.
|
||||
So, final team comp:
|
||||
</p>
|
||||
<ol class="list-decimal pl-4 mb-4">
|
||||
<li>
|
||||
Seiun Sky as a gambler, where the gamble is getting into first in midrace.
|
||||
</li>
|
||||
<li>
|
||||
VBourbon as an ace.
|
||||
1200 wit is basically mandatory thanks to the requirement of double accels.
|
||||
Final Push won't be a bad take as a gamble-y backup.
|
||||
</li>
|
||||
<li>
|
||||
Silence Suzuka as Silence Suzuka.
|
||||
If you prefer winning over running your favorites, this should be Maruzensky instead.
|
||||
</li>
|
||||
</ol>
|
||||
<Sec h="3" id="cm12">CM12 – Aries Cup (Satsuki Sho)</Sec>
|
||||
<p>
|
||||
One of COC's best tracks, because U=ma2 is at worst only slightly less good than 777 as a trigger.
|
||||
If there is any other front runner, triple front pushes pace COC out of range for U=ma2, making her at best as reliable as the usual.
|
||||
</p>
|
||||
<ol class="list-decimal pl-4 mb-4">
|
||||
<li>
|
||||
Seiun Sky's Angling is a 0.4 accel that lasts for the entire accel period, better than COC's 0.3 that's only up for 2/3 of it.
|
||||
I want her to be my ace in front, so capped wit, high power, strong spot struggles, huge mid-race skills.
|
||||
Didn't get a guts build to come together after three weeks of attempts, so switched to a standard speed/power/wit build and got a high roll on the first try.
|
||||
1181/786/1185/474/1185 A/A/S.
|
||||
</li>
|
||||
<li>
|
||||
VBourbon is a horse that exists. She can beat other people's front runners, so great as a backup.
|
||||
Ideally she lets Sei in front, but it's better to let this happen naturally off the lack of TTL than to force low stats.
|
||||
Second attempt got charming and fast learner for free, medium S, and manageable stats. Skill hints were a bit sparse, but not worth rolling more.
|
||||
1164/662/1010/599/1167 A/S/A.
|
||||
</li>
|
||||
<li>
|
||||
Silence Suzuka is my favorite front runner, so I will run her.
|
||||
Her primary task is to be in third or fourth so COC can't be, so I don't need amazing stats.
|
||||
To maximize her effectiveness, there are two possible plans:
|
||||
I could make her a debuffer, which needs 1200 power and wit but no other stats matter,
|
||||
or I could experiment with something wacky like NSM into duels.
|
||||
The latter sounds more fun, even if it is obviously bad.
|
||||
First attempt didn't get aptitudes but did get Lone Wolf to disable it for everyone else and surprisingly decent stats, which is good enough for me;
|
||||
her job isn't to win anyway.
|
||||
<!-- TODO: stat line -->
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
Win rates after 40: VBourbon 35%, Sei 17.5%, Suzuka 15%. Not quite executing the plan, but I'll take the wins.
|
||||
</p>
|
||||
<p>
|
||||
Win rates after 80: VBourbon 30%, Sei 22.5%, Suzuka 12.5%. I believe this is my best round 2 performance ever.
|
||||
I lose more to other fronts than to COC. "Most dominant racing horse for a year" continues to get trounced by the wacky triple front build.
|
||||
</p>
|
||||
<Sec h="3" id="cm11">CM11 – Pisces Cup (Hanshin 3200 Heavy Rain)</Sec>
|
||||
<p>
|
||||
N.B. This CM was before I started writing this document, so henceforth, there is much less info.
|
||||
</p>
|
||||
<p>
|
||||
Late race starts on the back stretch, which means the end closers are out to play.
|
||||
</p>
|
||||
<ol class="list-decimal pl-4 mb-4">
|
||||
<li>
|
||||
Kitasan Black is a snap take.
|
||||
Her unique is the only reliable accel outside of Straightaway Spurt, and it's quite a lot better.
|
||||
1200/1200/816/777/742 A/S/A.
|
||||
</li>
|
||||
<li>
|
||||
VBourbon's unique has a built-in recovery, which makes her the perfect choice as the survivor if stamina debuffers show up.
|
||||
<!-- TODO: stat line -->
|
||||
</li>
|
||||
<li>
|
||||
Silence Suzuka is coming.
|
||||
1200/1145/653/608/1000 A/A/A.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
I floundered on parenting and ended up with not enough time to make runners.
|
||||
Suzuka had more wit than Kitasan could handle, so I rarely got Kitasan uniques.
|
||||
</p>
|
||||
<p>
|
||||
Win rates after 80: VBourbon 31.25%, Kitasan 21.25%, Suzuka 2.5%.
|
||||
</p>
|
||||
<p>
|
||||
Extremely unlucky finals gave me third place for the first time ever.
|
||||
</p>
|
||||
<Sec h="3" id="cm10">CM10 – Aquarius Cup (February Stakes)</Sec>
|
||||
<p>
|
||||
Everyone is terrified of Taiki Shuttle, who has a 3-4 ult.
|
||||
Triple fronts would like to have a word.
|
||||
It's a dirt track, but every horse can run dirt if you're brave enough.
|
||||
</p>
|
||||
<ol class="list-decimal pl-4 mb-4">
|
||||
<li>
|
||||
Smart Falcon is the obvious choice, being the only actual dirt front runner to exist.
|
||||
Her unique isn't terribly strong for this track, but her gold skills are – Trending makes it extremely difficult for others to overtake her.
|
||||
1200/467/920/410/930 A/S/A.
|
||||
</li>
|
||||
<li>
|
||||
Silence Suzuka in runaway mode will make positioning much easier.
|
||||
I don't have to think about Unrestrained on my other horses because they won't be able to get in position for it anyway.
|
||||
Other Suzukas will be rare because she has G dirt and people don't realize distance aptitude hardly matters for runaways.
|
||||
1200/674/820/470/774 B/A/A.
|
||||
</li>
|
||||
<li>
|
||||
Taiki Shuttle is a front runner now.
|
||||
She has B dirt and C front at base. Very easy to fix.
|
||||
Falco's mid-race is probably stronger than Taiki's between her unique and Trending, so Taiki should often be in position for her ult in this build.
|
||||
<!-- TODO: stat line -->
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
This is probably the strongest gameplan I've been able to use, but I failed to execute it properly.
|
||||
In particular, this was the CM that taught me through experience how important mid race speed skills are for front runners.
|
||||
Final win rate was a bit over 50%, including my first ever five win round 2 entry.
|
||||
Insane luck with Unrestrained at the same time as Angling made Suzuka the champion of the Aquarius Cup.
|
||||
</p>
|
||||
<Sec h="2" id="history">Version History</Sec>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>2026-05-03: CM13 planning.</li>
|
||||
<li>2026-04-27: First draft of mechanics section, had the thought to add my CM plans and results.</li>
|
||||
<li>2026-04-27: First draft of intro and career sections</li>
|
||||
</ul>
|
||||
</article>
|
||||
@@ -50,3 +50,22 @@ input {
|
||||
input[type='number'] {
|
||||
min-height: 1.5lh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-5xl);
|
||||
line-height: var(--tw-leading, var(--text-5xl--line-height));
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
181
zenno/src/routes/mspeed/+page.svelte
Normal file
181
zenno/src/routes/mspeed/+page.svelte
Normal file
@@ -0,0 +1,181 @@
|
||||
<script lang="ts">
|
||||
import type { ComputedSeries, HorizontalRule } from '$lib/chart';
|
||||
import {
|
||||
acceleration,
|
||||
APTITUDE_LEVELS,
|
||||
AptitudeLevel,
|
||||
deceleration,
|
||||
HORSE_LENGTH,
|
||||
moveLaneModifier,
|
||||
Phase,
|
||||
RunningStyle,
|
||||
skillDuration,
|
||||
speedGain,
|
||||
spotStruggleDuration,
|
||||
spotStruggleSpeed,
|
||||
Stat,
|
||||
} from '$lib/race';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
import SpeedDur from './SpeedDur.svelte';
|
||||
|
||||
let rawPower = $state(1200);
|
||||
let rawGuts = $state(1200);
|
||||
let raceLen = $state(1600);
|
||||
let surfApt = $state(AptitudeLevel.A);
|
||||
let frontApt = $state(AptitudeLevel.A);
|
||||
let isRunaway = $state(false);
|
||||
let isCareer = $state(false);
|
||||
|
||||
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 phases = [Phase.EarlyRace, Phase.MidRace, Phase.LateRace] as const;
|
||||
const accel = $derived(phases.map((p) => acceleration(powerStat, style, surfApt, p)));
|
||||
const decel = phases.map((p) => deceleration(p));
|
||||
|
||||
const ssBoost = $derived(spotStruggleSpeed(gutsStat));
|
||||
const ssDur = $derived(spotStruggleDuration(gutsStat, frontApt));
|
||||
|
||||
const lcBoost = $derived(moveLaneModifier(powerStat));
|
||||
const lcDur = $derived(Math.min(skillDuration(3, raceLen), 6));
|
||||
|
||||
const uniques = [
|
||||
['Operation Cacao', 0.35, 5, Phase.MidRace],
|
||||
["All Charged! It's Go Time! (Tokyo turf)", 0.45, 5, Phase.LateRace],
|
||||
] as const;
|
||||
const skills = [
|
||||
['Fast-Paced', 0.15, 3, Phase.MidRace],
|
||||
['Professor of Curvature (mid race)', 0.35, 2.4, Phase.MidRace],
|
||||
["All Charged! It's Go Time! (inherited)", 0.25, 3, Phase.LateRace],
|
||||
] as const;
|
||||
|
||||
const ssY: Array<ComputedSeries | null> = $derived([
|
||||
{
|
||||
label: 'Aptitude S',
|
||||
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, AptitudeLevel.S), accel[0], decel[0]) / HORSE_LENGTH,
|
||||
},
|
||||
{
|
||||
label: 'Aptitude A',
|
||||
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, AptitudeLevel.A), accel[0], decel[0]) / HORSE_LENGTH,
|
||||
},
|
||||
frontApt < AptitudeLevel.A
|
||||
? {
|
||||
label: `Aptitude ${AptitudeLevel[frontApt]}`,
|
||||
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, frontApt), accel[0], decel[0]) / HORSE_LENGTH,
|
||||
}
|
||||
: null,
|
||||
]);
|
||||
const lcY: Array<ComputedSeries | null> = $derived([
|
||||
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), lcDur, accel[0], decel[0]) / HORSE_LENGTH },
|
||||
]);
|
||||
const pcRuler = $derived({ y: speedGain(0.35, skillDuration(2.4, raceLen), accel[1], decel[1]) / HORSE_LENGTH, label: 'Professor of Curvature' });
|
||||
const ssYRule = $derived([
|
||||
pcRuler,
|
||||
{ y: speedGain(lcBoost, lcDur, accel[0], decel[0]) / HORSE_LENGTH, label: 'Lane Combo' },
|
||||
]);
|
||||
const lcYRule = $derived([
|
||||
pcRuler,
|
||||
{ y: speedGain(ssBoost, ssDur, accel[0], decel[1]) / HORSE_LENGTH, label: 'Spot Struggle' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Front Runner Mechanical Speed Comparator</h1>
|
||||
<div class="mx-auto mt-8 grid max-w-4xl grid-cols-1 rounded-md text-center shadow-md ring md:grid-cols-8">
|
||||
<div class="m-4 md:col-span-2">
|
||||
<label for="powerStat">Power Stat</label>
|
||||
<input type="number" id="powerStat" bind:value={rawPower} class="w-full" />
|
||||
</div>
|
||||
<div class="m-4 md:col-span-2">
|
||||
<label for="gutsStat">Guts Stat</label>
|
||||
<input type="number" id="gutsStat" bind:value={rawGuts} class="w-full" />
|
||||
</div>
|
||||
<div class="m-4 md:col-span-2">
|
||||
<label for="surfaceApt">Surface Aptitude</label>
|
||||
<select id="surfaceApt" required bind:value={surfApt} class="w-full">
|
||||
{#each APTITUDE_LEVELS as apt (apt)}
|
||||
<option value={apt}>{AptitudeLevel[apt]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4 md:col-span-2">
|
||||
<label for="frontApt">Front Runner Aptitude</label>
|
||||
<select id="frontApt" required bind:value={frontApt} class="w-full">
|
||||
{#each APTITUDE_LEVELS as apt (apt)}
|
||||
<option value={apt}>{AptitudeLevel[apt]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="m-4 md:col-span-2 md:col-start-2">
|
||||
<label for="raceLen">Race Distance</label>
|
||||
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
|
||||
</div>
|
||||
<div class="m-4 self-center md:col-span-2">
|
||||
<label for="isRunaway" class="mr-1 align-middle">Runaway</label>
|
||||
<input type="checkbox" id="isRunaway" role="switch" bind:checked={isRunaway} class="min-h-6 min-w-6 align-middle" />
|
||||
</div>
|
||||
<div class="m-4 self-center md:col-span-2">
|
||||
<label for="isCareer" class="mr-1 align-middle">In Career</label>
|
||||
<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">Mechanics</span>
|
||||
<div class="mx-auto flex w-full flex-col md:flex-row md:justify-center">
|
||||
<SpeedDur speed={ssBoost} dur={ssDur} accel={accel[0]} decel={[decel[0], decel[1]]}>Spot Struggle</SpeedDur>
|
||||
<SpeedDur speed={lcBoost} dur={lcDur} accel={accel[0]} decel={decel[0]}>Idealized Lane Combo (DDPP)</SpeedDur>
|
||||
</div>
|
||||
<span class="mt-8 block w-full text-center text-lg">Unique Skills</span>
|
||||
<div class="mx-auto flex flex-col md:flex-row md:justify-center">
|
||||
{#each uniques as [name, boost, dur, phase] (name)}
|
||||
<SpeedDur speed={boost} dur={skillDuration(dur, raceLen)} accel={accel[phase]} decel={decel[phase]}>{name}</SpeedDur>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="mt-8 block w-full text-center text-lg">Inherited Uniques & Other Skills</span>
|
||||
<div class="mx-auto flex flex-col md:flex-row md:justify-center">
|
||||
{#each skills as [name, boost, dur, phase] (name)}
|
||||
<SpeedDur speed={boost} dur={skillDuration(dur, raceLen)} accel={accel[phase]} decel={decel[phase]}>{name}</SpeedDur>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mx-auto h-60 py-4 md:h-96 md:w-3xl">
|
||||
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={gutsStat} yRule={ssYRule} />
|
||||
</div>
|
||||
<div class="mx-auto mt-4 h-60 py-4 md:mt-0 md:h-96 md:w-3xl">
|
||||
<StatChart class="flex-1" stat={Stat.Power} y={lcY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={powerStat} yRule={lcYRule} />
|
||||
</div>
|
||||
<div class="mx-auto mt-12 w-full max-w-4xl border-t md:mt-8">
|
||||
<ul class="ml-4 list-disc">
|
||||
<li>All lengths gained include acceleration at the beginning of each speed boost and deceleration after its end.</li>
|
||||
<li>Each effect is assumed to be isolated and executed on level ground.</li>
|
||||
<li>
|
||||
Spot struggle has two numbers to distinguish ending in early race versus ending in mid race, which gives different
|
||||
deceleration values. Since spot struggle duration does not scale with race length, it is more likely to end in mid race on
|
||||
shorter races.
|
||||
</li>
|
||||
<li>
|
||||
Lane combo is idealized in the sense of assuming second lane change speed skill executes immediately after Dodging Danger
|
||||
completes, the horse is never blocked, and the horse returns to the rail in early race before the first corner.
|
||||
<ul class="ml-8 list-[revert]">
|
||||
<li>
|
||||
The move lane modifier is capped to 6 seconds, which is the approximate observed time to move from the Dodging Danger
|
||||
fixed lane back to the rail under the effect of Prudent Positioning.
|
||||
</li>
|
||||
<li>
|
||||
On medium+ tracks, with a proper gate acceleration build, Dodging Danger should realize some lane movement speed
|
||||
modifier, so the actual benefit will be more than the idealized number.
|
||||
</li>
|
||||
<li>
|
||||
Ignited Spirit WIT has a longer duration and lower lane change speed boost than Prudent Positioning, so it is likely to
|
||||
give more benefit than the idealized number.
|
||||
</li>
|
||||
<li>
|
||||
For full simulated analysis of lane combo, see <a
|
||||
href="https://lanecalc.hf-uma.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">危険回避シミュ</a
|
||||
>.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
33
zenno/src/routes/mspeed/SpeedDur.svelte
Normal file
33
zenno/src/routes/mspeed/SpeedDur.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { HORSE_LENGTH, speedGain } from '$lib/race';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
speed: number;
|
||||
dur: number;
|
||||
accel: number;
|
||||
decel: number | number[];
|
||||
}
|
||||
|
||||
function fmtp(x: number): string {
|
||||
return x >= 0 ? '+' + x.toFixed(3) : x.toFixed(3);
|
||||
}
|
||||
|
||||
const { children, speed, dur, accel, decel }: Props = $props();
|
||||
const decels = $derived([decel].flat(1));
|
||||
|
||||
const gain = $derived(decels.map((d) => speedGain(speed, dur, accel, d) / HORSE_LENGTH));
|
||||
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>
|
||||
<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>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AptitudeLevel, inverseSpurtSpeed, RunningStyle, spurtSpeed } from '$lib/race';
|
||||
import type { ComputedSeries } from '$lib/chart';
|
||||
import { AptitudeLevel, inverseSpurtSpeed, RunningStyle, spurtSpeed, Stat } from '$lib/race';
|
||||
import StatChart from '$lib/StatChart.svelte';
|
||||
|
||||
const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number');
|
||||
const stylesList = [
|
||||
@@ -41,6 +43,19 @@
|
||||
const skillProf = $derived(
|
||||
skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, 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) }
|
||||
: null,
|
||||
]);
|
||||
|
||||
const range: [number, number] = $derived([
|
||||
spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen),
|
||||
spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen),
|
||||
]);
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Spurt Speed Calculator</h1>
|
||||
@@ -71,7 +86,7 @@
|
||||
</div>
|
||||
<div class="m-4 md:col-start-2">
|
||||
<label for="raceLen">Race Distance</label>
|
||||
<input type="number" id="raceLen" required bind:value={raceLen} class="w-full" />
|
||||
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
|
||||
</div>
|
||||
<div class="m-4 self-center">
|
||||
<label for="isCareer" class="mr-1 align-middle">In Career</label>
|
||||
@@ -121,3 +136,6 @@
|
||||
{/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>
|
||||
|
||||
Reference in New Issue
Block a user