Compare commits

...

35 Commits

Author SHA1 Message Date
0ad064725f zenno: link to discord bot 2026-03-31 23:21:29 -04:00
886dccb6b8 zenno: title and favicon 2026-03-31 19:33:24 -04:00
57e8a06383 zenno: generate pages as directory/index.html 2026-03-31 19:23:10 -04:00
ab14f58079 cmd/horsebot: fix http server in http interactions mode 2026-03-31 19:03:51 -04:00
4106215180 zenno, cmd/horsebot: host website in horsebot 2026-03-31 18:43:17 -04:00
cdea376f94 zenno: make footer links open new tabs 2026-03-31 17:27:45 -04:00
d157dfc9b6 zenno: don't use sakura 2026-03-31 16:54:51 -04:00
773625b842 zenno: allow styling CharaPick 2026-03-31 12:32:04 -04:00
22ca5c98f3 zenno: format 2026-03-31 12:08:36 -04:00
08deedea8f zenno: nicer lobby conversation page 2026-03-31 01:19:13 -04:00
86b769d7ed zenno: lobby conversation page 2026-03-30 23:22:43 -04:00
e139eae06d zenno: start conversations tool 2026-03-30 21:53:03 -04:00
34e8c1f812 zenno: implement character picker 2026-03-30 12:13:20 -04:00
cc3128d65a zenno: better mobile nav 2026-03-30 10:13:17 -04:00
d04544030a zenno: create sveltekit website 2026-03-29 23:14:55 -04:00
e13c435afa global: generate with 2026-03-26 db 2026-03-26 04:05:39 -04:00
4429bbecd1 global: generate with 2026-03-19 db 2026-03-19 11:29:46 -04:00
2099eeb97e schema: describe json schemas in typescript 2026-03-18 00:43:51 -04:00
c22ed0dc83 horse: adjust fields omitted from skill json 2026-03-18 00:42:08 -04:00
494aeeb401 doc: document skill tag meanings 2026-03-17 19:09:04 -04:00
298b1368e8 horse, horsegen: generate lobby conversation data 2026-03-17 17:15:07 -04:00
b20a1a5964 doc: add mant db dump, document epithet text_data 2026-03-17 17:08:06 -04:00
af8e04473a horsebot: shrink skill displays 2026-03-12 18:35:51 -04:00
351f606d29 global: generate with 2026-03-12 db adding mant 2026-03-12 10:42:14 -04:00
da376647af horsebot: restore inherit indicators on skill names 2026-03-09 13:48:28 -04:00
2ec8d9cfdb horsegen: include uniques and inherits in each others' skill groups 2026-03-09 13:00:29 -04:00
2dd75edc03 horsebot: use a command option to decide whether ephemeral 2026-03-09 12:35:39 -04:00
e08580925d horsebot: make skill responses ephemeral with share button 2026-03-09 11:17:59 -04:00
63659a4934 horsebot: refactor skill server 2026-03-09 10:05:07 -04:00
af4e06411d horsebot: take horsegen output directory instead of each json file 2026-03-09 09:04:02 -04:00
4426925ebb horsegen: output to ./global instead of horse/global 2026-03-09 08:58:49 -04:00
8632bb8c3c all: generate json, not code
This includes modifying horsebot to use the generated JSON, as well as
moving the generator to another cmd/ directory.

Remove the generated code while we're here.
Koka tests still have to be updated, but it requires a JSON parser.
2026-03-08 21:33:46 -04:00
7ff271ff2d horse: generate json 2026-03-08 14:04:28 -04:00
5540bb2c4e horse: generate with 2026-03-05 global db 2026-03-05 10:27:47 -05:00
9a73f2147b test: change example to my current labor 2026-03-04 21:15:54 -05:00
105 changed files with 350087 additions and 62084 deletions

View File

@@ -3,12 +3,17 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"encoding/json"
"errors"
"flag" "flag"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strconv" "path/filepath"
"time" "time"
"github.com/disgoorg/disgo" "github.com/disgoorg/disgo"
@@ -20,23 +25,27 @@ import (
"github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/rest"
"git.sunturtle.xyz/zephyr/horse/horse" "git.sunturtle.xyz/zephyr/horse/horse"
"git.sunturtle.xyz/zephyr/horse/horse/global"
) )
func main() { func main() {
var ( var (
tokenFile string // public site
// http api options
addr string addr string
route string dataDir string
public string
// discord
tokenFile string
apiRoute string
pubkey string pubkey string
// logging options // logging
level slog.Level level slog.Level
textfmt string textfmt string
) )
flag.StringVar(&addr, "http", ":80", "`address` to bind HTTP server")
flag.StringVar(&dataDir, "data", "", "`dir`ectory containing exported json data")
flag.StringVar(&public, "public", "", "`dir`ectory containing the website to serve")
flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token") flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token")
flag.StringVar(&addr, "http", "", "`address` to bind HTTP API server") flag.StringVar(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls")
flag.StringVar(&route, "route", "/interactions/callback", "`path` to serve HTTP API calls")
flag.StringVar(&pubkey, "key", "", "Discord public key") flag.StringVar(&pubkey, "key", "", "Discord public key")
flag.TextVar(&level, "log", slog.LevelInfo, "slog logging `level`") flag.TextVar(&level, "log", slog.LevelInfo, "slog logging `level`")
flag.StringVar(&textfmt, "log-format", "text", "slog logging `format`, text or json") flag.StringVar(&textfmt, "log-format", "text", "slog logging `format`, text or json")
@@ -54,6 +63,27 @@ func main() {
} }
slog.SetDefault(slog.New(lh)) slog.SetDefault(slog.New(lh))
stat, err := os.Stat(public)
if err != nil {
slog.Error("public", slog.Any("err", err))
os.Exit(1)
}
if !stat.IsDir() {
slog.Error("public", slog.String("err", "not a directory"))
os.Exit(1)
}
skills, err := loadSkills(filepath.Join(dataDir, "skill.json"))
slog.Info("loaded skills", slog.Int("count", len(skills)))
groups, err2 := loadSkillGroups(filepath.Join(dataDir, "skill-group.json"))
slog.Info("loaded skill groups", slog.Int("count", len(groups)))
if err = errors.Join(err, err2); err != nil {
slog.Error("loading data", slog.Any("err", err))
os.Exit(1)
}
skillSrv := newSkillServer(skills, groups)
slog.Info("skill server ready")
token, err := os.ReadFile(tokenFile) token, err := os.ReadFile(tokenFile)
if err != nil { if err != nil {
slog.Error("reading token", slog.Any("err", err)) slog.Error("reading token", slog.Any("err", err))
@@ -68,42 +98,55 @@ func main() {
r.Use(middleware.Go) r.Use(middleware.Go)
r.Use(logMiddleware) r.Use(logMiddleware)
r.Route("/skill", func(r handler.Router) { r.Route("/skill", func(r handler.Router) {
r.SlashCommand("/", skillHandler) r.SlashCommand("/", skillSrv.slash)
r.Autocomplete("/", skillAutocomplete) r.Autocomplete("/", skillSrv.autocomplete)
r.ButtonComponent("/{id}", skillButton) r.SelectMenuComponent("/swap", skillSrv.menu)
r.ButtonComponent("/swap/{id}", skillSrv.button)
}) })
opts := []bot.ConfigOpt{bot.WithDefaultGateway(), bot.WithEventListeners(r)}
if addr != "" {
if pubkey == "" {
slog.Error("Discord public key must be provided when using HTTP API")
os.Exit(1)
}
opts = append(opts, bot.WithHTTPServerConfigOpts(pubkey,
httpserver.WithAddress(addr),
httpserver.WithURL(route),
))
}
slog.Info("connect", slog.String("disgo", disgo.Version)) slog.Info("connect", slog.String("disgo", disgo.Version))
client, err := disgo.New(string(token), opts...) client, err := disgo.New(string(token), bot.WithDefaultGateway(), bot.WithEventListeners(r))
if err != nil { if err != nil {
slog.Error("building bot", slog.Any("err", err)) slog.Error("building bot", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
mux := http.NewServeMux()
mux.Handle("GET /", http.FileServerFS(os.DirFS(public)))
if pubkey != "" {
pk, err := hex.DecodeString(pubkey)
if err != nil {
slog.Error("pubkey", slog.Any("err", err))
os.Exit(1)
}
mux.Handle("POST "+apiRoute, httpserver.HandleInteraction(httpserver.DefaultVerifier{}, pk, slog.Default(), client.EventManager.HandleHTTPEvent))
slog.Info("Discord HTTP API enabled", slog.String("pubkey", pubkey))
}
l, err := net.Listen("tcp", addr)
if err != nil {
slog.Error("listen", slog.String("addr", addr), slog.Any("err", err))
os.Exit(1)
}
srv := http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
BaseContext: func(l net.Listener) context.Context { return ctx },
}
go func() {
slog.Info("HTTP", slog.Any("addr", l.Addr()))
err := srv.Serve(l)
if err == http.ErrServerClosed {
return
}
slog.Error("HTTP server closed", slog.Any("err", err))
}()
if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil { if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil {
slog.Error("syncing commands", slog.Any("err", err)) slog.Error("syncing commands", slog.Any("err", err))
os.Exit(1) os.Exit(1)
} }
if addr != "" {
slog.Info("start HTTP server", slog.String("address", addr), slog.String("route", route))
if err := client.OpenHTTPServer(); err != nil {
slog.Error("starting HTTP server", slog.Any("err", err))
stop()
}
}
slog.Info("start gateway") slog.Info("start gateway")
if err := client.OpenGateway(ctx); err != nil { if err := client.OpenGateway(ctx); err != nil {
slog.Error("starting gateway", slog.Any("err", err)) slog.Error("starting gateway", slog.Any("err", err))
@@ -116,6 +159,10 @@ func main() {
ctx, stop = context.WithTimeout(context.Background(), 5*time.Second) ctx, stop = context.WithTimeout(context.Background(), 5*time.Second)
defer stop() defer stop()
client.Close(ctx) client.Close(ctx)
if err := srv.Shutdown(ctx); err != nil {
slog.Error("HTTP API shutdown", slog.Any("err", err))
os.Exit(1)
}
} }
var commands = []discord.ApplicationCommandCreate{ var commands = []discord.ApplicationCommandCreate{
@@ -129,55 +176,34 @@ var commands = []discord.ApplicationCommandCreate{
Required: true, Required: true,
Autocomplete: true, Autocomplete: true,
}, },
discord.ApplicationCommandOptionBool{
Name: "share",
Description: "Share the skill info",
},
}, },
}, },
} }
func skillHandler(data discord.SlashCommandInteractionData, e *handler.CommandEvent) error { func loadSkills(file string) ([]horse.Skill, error) {
q := data.String("query") b, err := os.ReadFile(file)
id, err := strconv.ParseInt(q, 10, 32)
if err == nil {
// note inverted condition; this is when we have an id
id = int64(global.AllSkills[horse.SkillID(id)].ID)
}
if id == 0 {
// Either we weren't given a number or the number doesn't match any skill ID.
v := global.SkillNameToID[q]
if v == 0 {
// No such skill.
m := discord.MessageCreate{
Content: "No such skill.",
Flags: discord.MessageFlagEphemeral,
}
return e.CreateMessage(m)
}
id = int64(v)
}
// TODO(zeph): search conditions and effects, give a list
m := discord.MessageCreate{
Components: []discord.LayoutComponent{RenderSkill(horse.SkillID(id), global.AllSkills, global.SkillGroups)},
Flags: discord.MessageFlagIsComponentsV2,
}
return e.CreateMessage(m)
}
func skillAutocomplete(e *handler.AutocompleteEvent) error {
q := e.Data.String("query")
opts := skillGlobalAuto().Find(nil, q)
return e.AutocompleteResult(opts[:min(len(opts), 25)])
}
func skillButton(data discord.ButtonInteractionData, e *handler.ComponentEvent) error {
id, err := strconv.ParseInt(e.Vars["id"], 10, 32)
if err != nil { if err != nil {
m := discord.MessageCreate{ return nil, err
Content: "That button produced an invalid skill ID. That's not supposed to happen.",
Flags: discord.MessageFlagEphemeral,
} }
return e.CreateMessage(m) var skills []horse.Skill
if err := json.Unmarshal(b, &skills); err != nil {
return nil, err
} }
m := discord.MessageUpdate{ return skills, nil
Components: &[]discord.LayoutComponent{RenderSkill(horse.SkillID(id), global.AllSkills, global.SkillGroups)}, }
}
return e.UpdateMessage(m) func loadSkillGroups(file string) ([]horse.SkillGroup, error) {
b, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var groups []horse.SkillGroup
if err := json.Unmarshal(b, &groups); err != nil {
return nil, err
}
return groups, nil
} }

View File

@@ -2,67 +2,167 @@ package main
import ( import (
"fmt" "fmt"
"log/slog"
"strconv"
"strings" "strings"
"sync"
"github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/handler"
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete" "git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
"git.sunturtle.xyz/zephyr/horse/horse" "git.sunturtle.xyz/zephyr/horse/horse"
"git.sunturtle.xyz/zephyr/horse/horse/global"
) )
func RenderSkill(id horse.SkillID, all map[horse.SkillID]horse.Skill, groups map[int32][4]horse.SkillID) discord.ContainerComponent { type skillServer struct {
s, ok := all[id] skills map[horse.SkillID]horse.Skill
byName map[string]horse.SkillID
groups map[horse.SkillGroupID]horse.SkillGroup
autocom autocomplete.Set[discord.AutocompleteChoice]
}
func newSkillServer(skills []horse.Skill, groups []horse.SkillGroup) *skillServer {
s := skillServer{
skills: make(map[horse.SkillID]horse.Skill, len(skills)),
byName: make(map[string]horse.SkillID, len(skills)),
groups: make(map[horse.SkillGroupID]horse.SkillGroup, len(groups)),
}
for _, skill := range skills {
s.skills[skill.ID] = skill
s.byName[skill.Name] = skill.ID
switch {
case skill.UniqueOwner == "":
s.autocom.Add(skill.Name, discord.AutocompleteChoiceString{Name: skill.Name, Value: strconv.Itoa(int(skill.ID))})
case skill.Rarity >= 3:
s.autocom.Add(skill.Name, discord.AutocompleteChoiceString{Name: skill.Name, Value: skill.Name})
s.autocom.Add(skill.UniqueOwner, discord.AutocompleteChoiceString{Name: "Unique: " + skill.UniqueOwner, Value: strconv.Itoa(int(skill.ID))})
default:
s.autocom.Add(skill.Name, discord.AutocompleteChoiceString{Name: skill.Name + " (Inherited)", Value: strconv.Itoa(int(skill.ID))})
s.autocom.Add(skill.UniqueOwner, discord.AutocompleteChoiceString{Name: "Inherited unique: " + skill.UniqueOwner, Value: skill.Name})
}
}
for _, g := range groups {
s.groups[g.ID] = g
}
return &s
}
func (s *skillServer) slash(data discord.SlashCommandInteractionData, e *handler.CommandEvent) error {
q := data.String("query")
id, err := strconv.ParseInt(q, 10, 32)
if err == nil {
// note inverted condition; this is when we have an id
id = int64(s.skills[horse.SkillID(id)].ID)
}
if id == 0 {
// Either we weren't given a number or the number doesn't match any skill ID.
v := s.byName[q]
if v == 0 {
// No such skill.
m := discord.MessageCreate{
Content: "No such skill.",
Flags: discord.MessageFlagEphemeral,
}
return e.CreateMessage(m)
}
id = int64(v)
}
m := discord.MessageCreate{
Components: []discord.LayoutComponent{s.render(horse.SkillID(id))},
Flags: discord.MessageFlagIsComponentsV2,
}
if !data.Bool("share") {
m.Flags |= discord.MessageFlagEphemeral
}
return e.CreateMessage(m)
}
func (s *skillServer) autocomplete(e *handler.AutocompleteEvent) error {
q := e.Data.String("query")
opts := s.autocom.Find(nil, q)
return e.AutocompleteResult(opts[:min(len(opts), 25)])
}
func (s *skillServer) button(data discord.ButtonInteractionData, e *handler.ComponentEvent) error {
id, err := strconv.ParseInt(e.Vars["id"], 10, 32)
if err != nil {
m := discord.MessageCreate{
Content: "That button produced an invalid skill ID. That's not supposed to happen.",
Flags: discord.MessageFlagEphemeral,
}
return e.CreateMessage(m)
}
m := discord.MessageUpdate{
Components: &[]discord.LayoutComponent{s.render(horse.SkillID(id))},
}
return e.UpdateMessage(m)
}
func (s *skillServer) menu(data discord.SelectMenuInteractionData, e *handler.ComponentEvent) error {
d, ok := data.(discord.StringSelectMenuInteractionData)
if !ok { if !ok {
return discord.NewContainer(discord.NewTextDisplayf("invalid skill ID %v made it to RenderSkill", id)) return fmt.Errorf("wrong select menu type %T", data)
}
if len(d.Values) != 1 {
return fmt.Errorf("wrong number of values: %q", d.Values)
}
id, err := strconv.ParseInt(d.Values[0], 10, 32)
if err != nil {
return err
}
m := discord.MessageUpdate{
Components: &[]discord.LayoutComponent{s.render(horse.SkillID(id))},
}
return e.UpdateMessage(m)
}
func (s *skillServer) render(id horse.SkillID) discord.ContainerComponent {
skill, ok := s.skills[id]
if !ok {
slog.Error("invalid skill id", slog.Int("id", int(id)))
return discord.NewContainer(discord.NewTextDisplayf("invalid skill ID %v made it to render", id))
} }
thumburl := fmt.Sprintf("https://gametora.com/images/umamusume/skill_icons/utx_ico_skill_%d.png", s.IconID) top := "### " + skill.Name
top := "## " + s.Name if skill.UniqueOwner != "" {
if s.UniqueOwner != "" { top += "\n-# " + skill.UniqueOwner
top += "\n-# " + s.UniqueOwner
} }
r := discord.NewContainer( r := discord.NewContainer(
discord.NewSection(
discord.NewTextDisplay(top), discord.NewTextDisplay(top),
discord.NewTextDisplay(s.Description), discord.NewSmallSeparator(),
).WithAccessory(discord.NewThumbnail(thumburl)),
) )
var skilltype string var skilltype string
switch { switch {
case s.Rarity == 3, s.Rarity == 4, s.Rarity == 5: case skill.Rarity == 3, skill.Rarity == 4, skill.Rarity == 5:
// unique of various star levels // unique of various star levels
r.AccentColor = 0xaca4d4 r.AccentColor = 0xaca4d4
skilltype = "Unique Skill" skilltype = "Unique Skill"
case s.UniqueOwner != "": case skill.UniqueOwner != "":
r.AccentColor = 0xcccccc r.AccentColor = 0xcccccc
skilltype = "Inherited Unique" skilltype = "Inherited Unique"
case s.Rarity == 2: case skill.Rarity == 2:
// rare (gold) // rare (gold)
r.AccentColor = 0xd7c25b r.AccentColor = 0xd7c25b
skilltype = "Rare Skill" skilltype = "Rare Skill"
case s.GroupRate == -1: case skill.GroupRate == -1:
// negative (purple) skill // negative (purple) skill
r.AccentColor = 0x9151d4 r.AccentColor = 0x9151d4
skilltype = "Negative Skill" skilltype = "Negative Skill"
case !s.WitCheck: case !skill.WitCheck:
// should be passive (green) // should be passive (green)
r.AccentColor = 0x66ae1c r.AccentColor = 0x66ae1c
skilltype = "Passive Skill" skilltype = "Passive Skill"
case isDebuff(s): case isDebuff(skill):
// debuff (red) // debuff (red)
r.AccentColor = 0xe34747 r.AccentColor = 0xe34747
skilltype = "Debuff Skill" skilltype = "Debuff Skill"
case s.Rarity == 1: case skill.Rarity == 1:
// common (white) // common (white)
r.AccentColor = 0xcccccc r.AccentColor = 0xcccccc
skilltype = "Common Skill" skilltype = "Common Skill"
} }
r.Components = append(r.Components, discord.NewSmallSeparator())
text := make([]string, 0, 3) text := make([]string, 0, 3)
abils := make([]string, 0, 3) abils := make([]string, 0, 3)
for _, act := range s.Activations { for _, act := range skill.Activations {
text, abils = text[:0], abils[:0] text, abils = text[:0], abils[:0]
if act.Precondition != "" { if act.Precondition != "" {
text = append(text, "Precondition: "+formatCondition(act.Precondition)) text = append(text, "Precondition: "+formatCondition(act.Precondition))
@@ -92,24 +192,40 @@ func RenderSkill(id horse.SkillID, all map[horse.SkillID]horse.Skill, groups map
r.Components = append(r.Components, discord.NewTextDisplay(strings.Join(text, "\n"))) r.Components = append(r.Components, discord.NewTextDisplay(strings.Join(text, "\n")))
} }
l := discord.NewTextDisplayf("%s ・ SP cost %d ・ Grade value %d ・ [Conditions on GameTora](https://gametora.com/umamusume/skill-condition-viewer?skill=%d)", skilltype, s.SPCost, s.GradeValue, s.ID) l := discord.NewTextDisplayf("%s ・ SP cost %d ・ Grade value %d ・ [Conditions on GameTora](https://gametora.com/umamusume/skill-condition-viewer?skill=%d)", skilltype, skill.SPCost, skill.GradeValue, skill.ID)
r.Components = append(r.Components, discord.NewSmallSeparator(), l) r.Components = append(r.Components, discord.NewSmallSeparator(), l)
rel := make([]horse.Skill, 0, 4) rel := make([]horse.Skill, 0, 4)
for _, id := range groups[s.Group] { group := s.groups[skill.Group]
for _, id := range [...]horse.SkillID{group.Skill1, group.Skill2, group.Skill3, group.SkillBad} {
if id != 0 { if id != 0 {
rel = append(rel, all[id]) rel = append(rel, s.skills[id])
} }
} }
if len(rel) > 1 { if len(rel) > 1 {
buttons := make([]discord.InteractiveComponent, 0, 4) opts := make([]discord.StringSelectMenuOption, 0, 4)
for _, rs := range rel { for _, rs := range rel {
b := discord.NewSecondaryButton(rs.Name, fmt.Sprintf("/skill/%d", rs.ID)) name := rs.Name
if rs.ID == id { emoji := "⚪"
b = b.AsDisabled() switch rs.Rarity {
case 1:
if rs.UniqueOwner != "" {
name += " (Inherited)"
} }
buttons = append(buttons, b) case 2:
emoji = "🟡"
case 3, 4, 5:
emoji = "🟣"
default:
emoji = "⁉️"
} }
r.Components = append(r.Components, discord.NewActionRow(buttons...)) b := discord.NewStringSelectMenuOption(name, strconv.Itoa(int(rs.ID))).WithEmoji(discord.NewComponentEmoji(emoji))
if rs.ID == skill.ID {
b = b.WithDefault(true)
}
opts = append(opts, b)
}
row := discord.NewActionRow(discord.NewStringSelectMenu("/skill/swap", "Related skills", opts...))
r.Components = append(r.Components, row)
} }
return r return r
} }
@@ -132,19 +248,3 @@ func isDebuff(s horse.Skill) bool {
} }
return false return false
} }
var skillGlobalAuto = sync.OnceValue(func() *autocomplete.Set[discord.AutocompleteChoice] {
var set autocomplete.Set[discord.AutocompleteChoice]
for _, id := range global.OrderedSkills {
s := global.AllSkills[id]
set.Add(s.Name, discord.AutocompleteChoiceString{Name: s.Name, Value: s.Name})
if s.UniqueOwner != "" {
if s.Rarity >= 3 {
set.Add(s.UniqueOwner, discord.AutocompleteChoiceString{Name: "Unique: " + s.UniqueOwner, Value: s.Name})
} else {
set.Add(s.UniqueOwner, discord.AutocompleteChoiceString{Name: "Inherited unique: " + s.UniqueOwner, Value: s.Name})
}
}
}
return &set
})

418
cmd/horsegen/generate.go Normal file
View File

@@ -0,0 +1,418 @@
package main
import (
"bufio"
"cmp"
"context"
_ "embed"
"encoding/json"
"errors"
"flag"
"fmt"
"log/slog"
"maps"
"os"
"os/signal"
"path/filepath"
"slices"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse/horse"
)
func main() {
var (
mdb string
out string
region string
)
flag.StringVar(&mdb, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb")
flag.StringVar(&out, "o", `.`, "`dir`ectory for output files")
flag.StringVar(&region, "region", "global", "region the database is for (global, jp)")
flag.Parse()
slog.Info("open", slog.String("mdb", mdb))
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil {
slog.Error("opening mdb", slog.String("mdb", mdb), slog.Any("err", err))
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
loadgroup, ctx1 := errgroup.WithContext(ctx)
charas := load(ctx1, loadgroup, db, "characters", characterSQL, func(s *sqlite.Stmt) horse.Character {
return horse.Character{
ID: horse.CharacterID(s.ColumnInt(0)),
Name: s.ColumnText(1),
}
})
aff := load(ctx1, loadgroup, db, "pair affinity", affinitySQL, func(s *sqlite.Stmt) horse.AffinityRelation {
return horse.AffinityRelation{
IDA: s.ColumnInt(0),
IDB: s.ColumnInt(1),
IDC: s.ColumnInt(2),
Affinity: s.ColumnInt(3),
}
})
umas := load(ctx1, loadgroup, db, "umas", umaSQL, func(s *sqlite.Stmt) horse.Uma {
return horse.Uma{
ID: horse.UmaID(s.ColumnInt(0)),
CharacterID: horse.CharacterID(s.ColumnInt(1)),
Name: s.ColumnText(2),
Variant: s.ColumnText(3),
Sprint: horse.AptitudeLevel(s.ColumnInt(4)),
Mile: horse.AptitudeLevel(s.ColumnInt(6)),
Medium: horse.AptitudeLevel(s.ColumnInt(7)),
Long: horse.AptitudeLevel(s.ColumnInt(8)),
Front: horse.AptitudeLevel(s.ColumnInt(9)),
Pace: horse.AptitudeLevel(s.ColumnInt(10)),
Late: horse.AptitudeLevel(s.ColumnInt(11)),
End: horse.AptitudeLevel(s.ColumnInt(12)),
Turf: horse.AptitudeLevel(s.ColumnInt(13)),
Dirt: horse.AptitudeLevel(s.ColumnInt(14)),
Unique: horse.SkillID(s.ColumnInt(15)),
Skill1: horse.SkillID(s.ColumnInt(16)),
Skill2: horse.SkillID(s.ColumnInt(17)),
Skill3: horse.SkillID(s.ColumnInt(18)),
SkillPL2: horse.SkillID(s.ColumnInt(19)),
SkillPL3: horse.SkillID(s.ColumnInt(20)),
SkillPL4: horse.SkillID(s.ColumnInt(21)),
SkillPL5: horse.SkillID(s.ColumnInt(22)),
}
})
sg := load(ctx1, loadgroup, db, "skill groups", skillGroupSQL, func(s *sqlite.Stmt) horse.SkillGroup {
return horse.SkillGroup{
ID: horse.SkillGroupID(s.ColumnInt(0)),
Skill1: horse.SkillID(s.ColumnInt(1)),
Skill2: horse.SkillID(s.ColumnInt(2)),
Skill3: horse.SkillID(s.ColumnInt(3)),
SkillBad: horse.SkillID(s.ColumnInt(4)),
}
})
skills := load(ctx1, loadgroup, db, "skills", skillSQL, func(s *sqlite.Stmt) horse.Skill {
return horse.Skill{
ID: horse.SkillID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SkillGroupID(s.ColumnInt32(3)),
Rarity: int8(s.ColumnInt(5)),
GroupRate: int8(s.ColumnInt(6)),
GradeValue: s.ColumnInt32(7),
WitCheck: s.ColumnBool(8),
Activations: trimActivations([]horse.Activation{
{
Precondition: s.ColumnText(9),
Condition: s.ColumnText(10),
Duration: horse.TenThousandths(s.ColumnInt(11)),
DurScale: horse.DurScale(s.ColumnInt(12)),
Cooldown: horse.TenThousandths(s.ColumnInt(13)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(14)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(15)),
Value: horse.TenThousandths(s.ColumnInt(16)),
Target: horse.AbilityTarget(s.ColumnInt(17)),
TargetValue: s.ColumnInt32(18),
},
{
Type: horse.AbilityType(s.ColumnInt(19)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(20)),
Value: horse.TenThousandths(s.ColumnInt(21)),
Target: horse.AbilityTarget(s.ColumnInt(22)),
TargetValue: s.ColumnInt32(23),
},
{
Type: horse.AbilityType(s.ColumnInt(24)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(25)),
Value: horse.TenThousandths(s.ColumnInt(26)),
Target: horse.AbilityTarget(s.ColumnInt(27)),
TargetValue: s.ColumnInt32(28),
},
}),
},
{
Precondition: s.ColumnText(29),
Condition: s.ColumnText(30),
Duration: horse.TenThousandths(s.ColumnInt(31)),
DurScale: horse.DurScale(s.ColumnInt(32)),
Cooldown: horse.TenThousandths(s.ColumnInt(33)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(34)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(35)),
Value: horse.TenThousandths(s.ColumnInt(36)),
Target: horse.AbilityTarget(s.ColumnInt(37)),
TargetValue: s.ColumnInt32(38),
},
{
Type: horse.AbilityType(s.ColumnInt(39)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(40)),
Value: horse.TenThousandths(s.ColumnInt(41)),
Target: horse.AbilityTarget(s.ColumnInt(42)),
TargetValue: s.ColumnInt32(43),
},
{
Type: horse.AbilityType(s.ColumnInt(44)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(45)),
Value: horse.TenThousandths(s.ColumnInt(46)),
Target: horse.AbilityTarget(s.ColumnInt(47)),
TargetValue: s.ColumnInt32(48),
},
}),
},
}),
UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name
SPCost: s.ColumnInt(49),
IconID: s.ColumnInt(53),
}
})
races := load(ctx1, loadgroup, db, "races", raceSQL, func(s *sqlite.Stmt) horse.Race {
return horse.Race{
ID: horse.RaceID(s.ColumnInt(0)),
Name: s.ColumnText(1),
// TODO(zeph): grade
Thumbnail: s.ColumnInt(3),
Primary: horse.RaceID(s.ColumnInt(4)),
}
})
saddles := load(ctx1, loadgroup, db, "saddles", saddleSQL, func(s *sqlite.Stmt) horse.Saddle {
return horse.Saddle{
ID: horse.SaddleID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Races: trimZeros(
horse.RaceID(s.ColumnInt(2)),
horse.RaceID(s.ColumnInt(3)),
horse.RaceID(s.ColumnInt(4)),
),
Type: horse.SaddleType(s.ColumnInt(5)),
Primary: horse.SaddleID(s.ColumnInt(6)),
}
})
scenarios := load(ctx1, loadgroup, db, "scenarios", scenarioSQL, func(s *sqlite.Stmt) horse.Scenario {
return horse.Scenario{
ID: horse.ScenarioID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Title: s.ColumnText(2),
}
})
sparks := load(ctx1, loadgroup, db, "sparks", sparkSQL, func(s *sqlite.Stmt) horse.Spark {
return horse.Spark{
ID: horse.SparkID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SparkGroupID(s.ColumnInt(3)),
Rarity: horse.SparkRarity(s.ColumnInt(4)),
Type: horse.SparkType(s.ColumnInt(5)),
// Effects filled in later.
}
})
sparkeffs := load(ctx1, loadgroup, db, "spark effects", sparkEffectSQL, func(s *sqlite.Stmt) SparkEffImm {
return SparkEffImm{
Group: horse.SparkGroupID(s.ColumnInt(0)),
Effect: s.ColumnInt(1),
Target: horse.SparkTarget(s.ColumnInt(2)),
Value1: s.ColumnInt32(3),
Value2: s.ColumnInt32(4),
}
})
convos := load(ctx1, loadgroup, db, "lobby conversations", conversationSQL, func(s *sqlite.Stmt) horse.Conversation {
return horse.Conversation{
CharacterID: horse.CharacterID(s.ColumnInt(0)),
Number: s.ColumnInt(1),
Location: horse.LobbyConversationLocationID(s.ColumnInt(2)),
LocationName: horse.LobbyConversationLocationID(s.ColumnInt(2)).String(),
Chara1: horse.CharacterID(s.ColumnInt(3)),
Chara2: horse.CharacterID(s.ColumnInt(4)),
Chara3: horse.CharacterID(s.ColumnInt(5)),
ConditionType: s.ColumnInt(6),
}
})
if err := os.MkdirAll(filepath.Join(out, region), 0775); err != nil {
slog.Error("create output dir", slog.Any("err", err))
os.Exit(1)
}
writegroup, ctx2 := errgroup.WithContext(ctx)
writegroup.Go(func() error { return write(ctx2, out, region, "character.json", charas) })
writegroup.Go(func() error { return write(ctx2, out, region, "affinity.json", aff) })
writegroup.Go(func() error { return write(ctx2, out, region, "uma.json", umas) })
writegroup.Go(func() error { return write(ctx2, out, region, "skill-group.json", sg) })
writegroup.Go(func() error { return write(ctx2, out, region, "skill.json", skills) })
writegroup.Go(func() error { return write(ctx2, out, region, "race.json", races) })
writegroup.Go(func() error { return write(ctx2, out, region, "saddle.json", saddles) })
writegroup.Go(func() error { return write(ctx2, out, region, "scenario.json", scenarios) })
writegroup.Go(func() error { return write(ctx2, out, region, "spark.json", mergesparks(sparks, sparkeffs)) })
writegroup.Go(func() error { return write(ctx2, out, region, "conversation.json", convos) })
if err := writegroup.Wait(); err != nil {
slog.ErrorContext(ctx, "write", slog.Any("err", err))
os.Exit(1)
}
slog.InfoContext(ctx, "done")
}
var (
//go:embed sql/character.sql
characterSQL string
//go:embed sql/affinity.sql
affinitySQL string
//go:embed sql/uma.sql
umaSQL string
//go:embed sql/skill-group.sql
skillGroupSQL string
//go:embed sql/skill.sql
skillSQL string
//go:embed sql/race.sql
raceSQL string
//go:embed sql/saddle.sql
saddleSQL string
//go:embed sql/scenario.sql
scenarioSQL string
//go:embed sql/spark.sql
sparkSQL string
//go:embed sql/spark-effect.sql
sparkEffectSQL string
//go:embed sql/conversation.sql
conversationSQL string
)
func load[T any](ctx context.Context, group *errgroup.Group, db *sqlitex.Pool, kind, sql string, row func(*sqlite.Stmt) T) func() ([]T, error) {
slog.InfoContext(ctx, "load", slog.String("kind", kind))
var r []T
group.Go(func() error {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return fmt.Errorf("couldn't get connection for %s: %w", kind, err)
}
stmt, _, err := conn.PrepareTransient(sql)
if err != nil {
return fmt.Errorf("couldn't prepare statement for %s: %w", kind, err)
}
for {
ok, err := stmt.Step()
if err != nil {
return fmt.Errorf("error stepping %s: %w", kind, err)
}
if !ok {
break
}
r = append(r, row(stmt))
}
return nil
})
return func() ([]T, error) {
err := group.Wait()
if err == context.Canceled {
// After the first wait, all future ones return context.Canceled.
// We want to be able to wait any number of times, so hide it.
err = nil
}
return r, err
}
}
func write[T any](ctx context.Context, out, region, name string, v func() (T, error)) error {
p := filepath.Join(out, region, name)
r, err := v()
if err != nil {
return err
}
slog.InfoContext(ctx, "write", slog.String("path", p))
f, err := os.Create(p)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
enc.SetIndent("", "\t")
err = enc.Encode(r)
err = errors.Join(err, w.Flush())
slog.InfoContext(ctx, "marshaled", slog.String("path", p))
return err
}
func mergesparks(sparks func() ([]horse.Spark, error), effs func() ([]SparkEffImm, error)) func() ([]horse.Spark, error) {
return func() ([]horse.Spark, error) {
sp, err := sparks()
if err != nil {
return nil, err
}
ef, err := effs()
if err != nil {
return nil, err
}
// Spark effects are sorted by group ID, but groups apply to multiple
// sparks, and we don't rely on sparks and groups being in the same order.
// It is possible to merge in linear time, but not worth the effort:
// n log n is fine since this is an AOT step.
for i := range sp {
k, ok := slices.BinarySearchFunc(ef, sp[i].Group, func(e SparkEffImm, v horse.SparkGroupID) int { return cmp.Compare(e.Group, v) })
if !ok {
panic(fmt.Errorf("mergesparks: no spark group for %+v", &sp[i]))
}
// Back up to the first effect in the group.
for k > 0 && ef[k-1].Group == sp[i].Group {
k--
}
// Map effect IDs to the lists of their effects.
m := make(map[int][]horse.SparkEffect)
for _, e := range ef[k:] {
if e.Group != sp[i].Group {
// Done with this group.
break
}
m[e.Effect] = append(m[e.Effect], horse.SparkEffect{Target: e.Target, Value1: e.Value1, Value2: e.Value2})
}
// Now get effects in order.
keys := slices.Sorted(maps.Keys(m))
sp[i].Effects = make([][]horse.SparkEffect, 0, len(keys))
for _, key := range keys {
sp[i].Effects = append(sp[i].Effects, m[key])
}
}
return sp, nil
}
}
type SparkEffImm struct {
Group horse.SparkGroupID
Effect int
Target horse.SparkTarget
Value1 int32
Value2 int32
}
func trimAbilities(s []horse.Ability) []horse.Ability {
for len(s) > 0 && s[len(s)-1].Type == 0 {
s = s[:len(s)-1]
}
return s
}
func trimActivations(s []horse.Activation) []horse.Activation {
for len(s) > 0 && s[len(s)-1].Condition == "" {
s = s[:len(s)-1]
}
return s
}
func trimZeros[T comparable](s ...T) []T {
var zero T
for len(s) > 0 && s[len(s)-1] == zero {
s = s[:len(s)-1]
}
return s
}

View File

@@ -0,0 +1,60 @@
WITH pairs AS (
SELECT
a.id AS id_a,
b.id AS id_b
FROM chara_data a
JOIN chara_data b ON a.id < b.id
-- Exclude characters who have no succession relations defined.
WHERE a.id IN (SELECT chara_id FROM succession_relation_member)
AND b.id IN (SELECT chara_id FROM succession_relation_member)
), trios AS (
SELECT
a.id AS id_a,
b.id AS id_b,
c.id AS id_c
FROM chara_data a
JOIN chara_data b ON a.id < b.id
JOIN chara_data c ON a.id < c.id AND b.id < c.id
-- Exclude characters who have no succession relations defined.
WHERE a.id IN (SELECT chara_id FROM succession_relation_member)
AND b.id IN (SELECT chara_id FROM succession_relation_member)
AND c.id IN (SELECT chara_id FROM succession_relation_member)
), pair_relations AS (
SELECT
ra.relation_type,
ra.chara_id AS id_a,
rb.chara_id AS id_b
FROM succession_relation_member ra
JOIN succession_relation_member rb ON ra.relation_type = rb.relation_type
), trio_relations AS (
SELECT
ra.relation_type,
ra.chara_id AS id_a,
rb.chara_id AS id_b,
rc.chara_id AS id_c
FROM succession_relation_member ra
JOIN succession_relation_member rb ON ra.relation_type = rb.relation_type
JOIN succession_relation_member rc ON ra.relation_type = rc.relation_type
), affinity AS (
SELECT
pairs.*,
0 AS id_c,
SUM(IFNULL(relation_point, 0)) AS base_affinity
FROM pairs
LEFT JOIN pair_relations rp ON pairs.id_a = rp.id_a AND pairs.id_b = rp.id_b
LEFT JOIN succession_relation sr ON rp.relation_type = sr.relation_type
GROUP BY pairs.id_a, pairs.id_b
UNION ALL
SELECT
trios.*,
SUM(IFNULL(relation_point, 0)) AS base_affinity
FROM trios
LEFT JOIN trio_relations rt ON trios.id_a = rt.id_a AND trios.id_b = rt.id_b AND trios.id_c = rt.id_c
LEFT JOIN succession_relation sr ON rt.relation_type = sr.relation_type
GROUP BY trios.id_a, trios.id_b, trios.id_c
)
SELECT * FROM affinity
WHERE base_affinity != 0
ORDER BY id_a, id_b, id_c

View File

@@ -0,0 +1,10 @@
SELECT
gallery_chara_id,
disp_order,
pos_id,
chara_id_1,
chara_id_2,
chara_id_3,
condition_type
FROM home_story_trigger
ORDER BY gallery_chara_id, disp_order

View File

@@ -0,0 +1,17 @@
WITH skill_groups AS (
SELECT DISTINCT group_id FROM skill_data
)
SELECT
g.group_id,
COALESCE(inh_from.id, s1.id, 0) AS skill1,
COALESCE(inh_to.id, s2.id, 0) AS skill2,
IFNULL(s3.id, 0) AS skill3,
IFNULL(m1.id, 0) AS skill_bad
FROM skill_groups g
LEFT JOIN skill_data s1 ON g.group_id = s1.group_id AND s1.group_rate = 1
LEFT JOIN skill_data s2 ON g.group_id = s2.group_id AND s2.group_rate = 2
LEFT JOIN skill_data s3 ON g.group_id = s3.group_id AND s3.group_rate = 3
LEFT JOIN skill_data m1 ON g.group_id = m1.group_id AND m1.group_rate = -1
LEFT JOIN skill_data inh_to ON s1.id = inh_to.unique_skill_id_1
LEFT JOIN skill_data inh_from ON s2.unique_skill_id_1 = inh_from.id
ORDER BY g.group_id

View File

@@ -6,4 +6,4 @@ SELECT
value_2 value_2
FROM succession_factor_effect FROM succession_factor_effect
WHERE factor_group_id NOT IN (40001) -- exclude Carnival Bonus WHERE factor_group_id NOT IN (40001) -- exclude Carnival Bonus
ORDER BY factor_group_id, effect_id, target_type ORDER BY factor_group_id, effect_id, id

137122
doc/2026-03-12-global.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@ This file is my notes from exploring the database.
- 33 is race names by race id, 28 is race names by race instance id, 31 is race courses, 111 is saddle names - 33 is race names by race id, 28 is race names by race instance id, 31 is race courses, 111 is saddle names
- 65 is player titles, 66 is title descriptions - ties with honor_data? - 65 is player titles, 66 is title descriptions - ties with honor_data?
- 119 is scenario full titles (e.g. The Beginning: URA Finale), 120 is scenario descriptions, 237 is scenario names (e.g. URA Finale) - 119 is scenario full titles (e.g. The Beginning: URA Finale), 120 is scenario descriptions, 237 is scenario names (e.g. URA Finale)
- 130 is epithet names, 131 is epithet descriptions
# succession factor (sparks) # succession factor (sparks)
@@ -192,12 +193,41 @@ target types:
- 22 specific character, target value is character id - 22 specific character, target value is character id
- 23 other who triggered the skill - 23 other who triggered the skill
TODO target types only in jp: 2, 7, 11, 22, 23
ability_value_usage can be 1 for plain or 2-6 for aoharu stat skill stat scaling ability_value_usage can be 1 for plain or 2-6 for aoharu stat skill stat scaling
seems to be activate_lot = 1 means wit check, 0 means guaranteed seems to be activate_lot = 1 means wit check, 0 means guaranteed
tag_id is a slash-separated list of numeric IDs that roughly describe all effects relevant to the skill.
- 101..104 style front, pace, late, end
- 201..204 distance sprint, mile, medium, long
- 301..303 timing early, mid, late race
- 401 affects speed stat or target speed
- 402 affects stamina stat or hp
- 403 affects power stat or acceleration
- 404 affects guts stat
- 405 affects wit stat or vision
- 406 is a debuff of any type, or is a main story self-debuff (creeping anxiety, blatant fear), or is Behold Thine Emperor's Divine Might (both own unique and inherited versions are tagged only 406??)
- 407 modifies gate delay
- 501..502 turf/dirt
- 801..812 scenario skill
600 series applies to skills available in global but are only used in jp.
- 601 ground condition passive
- 602 weather passive
- 603 season passive
- 604 race track passive
- 605 time of day passive
- 606 exchange race passive (kawasaki, funabashi, morioka, ooi)
- 607 distance passive (non/standard)
- 608 track direction or corner count passive
- 609 gate position passive
- 610 ground type passive
- 611 popularity passive
- 612 running style passive
- 613 passive depending on other horses' styles or skills
- 614 base stat threshold passive
- 615 mood passive
# races # races
- group 1, grade: g1 100, g2 200, g3 300, op 400, pre-op 700 - group 1, grade: g1 100, g2 200, g3 300, op 400, pre-op 700

145007
global/affinity.json Normal file

File diff suppressed because it is too large Load Diff

226
global/character.json Normal file
View File

@@ -0,0 +1,226 @@
[
{
"chara_id": 1001,
"name": "Special Week"
},
{
"chara_id": 1002,
"name": "Silence Suzuka"
},
{
"chara_id": 1003,
"name": "Tokai Teio"
},
{
"chara_id": 1004,
"name": "Maruzensky"
},
{
"chara_id": 1005,
"name": "Fuji Kiseki"
},
{
"chara_id": 1006,
"name": "Oguri Cap"
},
{
"chara_id": 1007,
"name": "Gold Ship"
},
{
"chara_id": 1008,
"name": "Vodka"
},
{
"chara_id": 1009,
"name": "Daiwa Scarlet"
},
{
"chara_id": 1010,
"name": "Taiki Shuttle"
},
{
"chara_id": 1011,
"name": "Grass Wonder"
},
{
"chara_id": 1012,
"name": "Hishi Amazon"
},
{
"chara_id": 1013,
"name": "Mejiro McQueen"
},
{
"chara_id": 1014,
"name": "El Condor Pasa"
},
{
"chara_id": 1015,
"name": "T.M. Opera O"
},
{
"chara_id": 1016,
"name": "Narita Brian"
},
{
"chara_id": 1017,
"name": "Symboli Rudolf"
},
{
"chara_id": 1018,
"name": "Air Groove"
},
{
"chara_id": 1019,
"name": "Agnes Digital"
},
{
"chara_id": 1020,
"name": "Seiun Sky"
},
{
"chara_id": 1021,
"name": "Tamamo Cross"
},
{
"chara_id": 1022,
"name": "Fine Motion"
},
{
"chara_id": 1023,
"name": "Biwa Hayahide"
},
{
"chara_id": 1024,
"name": "Mayano Top Gun"
},
{
"chara_id": 1025,
"name": "Manhattan Cafe"
},
{
"chara_id": 1026,
"name": "Mihono Bourbon"
},
{
"chara_id": 1027,
"name": "Mejiro Ryan"
},
{
"chara_id": 1028,
"name": "Hishi Akebono"
},
{
"chara_id": 1030,
"name": "Rice Shower"
},
{
"chara_id": 1032,
"name": "Agnes Tachyon"
},
{
"chara_id": 1033,
"name": "Admire Vega"
},
{
"chara_id": 1034,
"name": "Inari One"
},
{
"chara_id": 1035,
"name": "Winning Ticket"
},
{
"chara_id": 1037,
"name": "Eishin Flash"
},
{
"chara_id": 1038,
"name": "Curren Chan"
},
{
"chara_id": 1039,
"name": "Kawakami Princess"
},
{
"chara_id": 1040,
"name": "Gold City"
},
{
"chara_id": 1041,
"name": "Sakura Bakushin O"
},
{
"chara_id": 1044,
"name": "Sweep Tosho"
},
{
"chara_id": 1045,
"name": "Super Creek"
},
{
"chara_id": 1046,
"name": "Smart Falcon"
},
{
"chara_id": 1048,
"name": "Tosen Jordan"
},
{
"chara_id": 1050,
"name": "Narita Taishin"
},
{
"chara_id": 1051,
"name": "Nishino Flower"
},
{
"chara_id": 1052,
"name": "Haru Urara"
},
{
"chara_id": 1056,
"name": "Matikanefukukitaru"
},
{
"chara_id": 1058,
"name": "Meisho Doto"
},
{
"chara_id": 1059,
"name": "Mejiro Dober"
},
{
"chara_id": 1060,
"name": "Nice Nature"
},
{
"chara_id": 1061,
"name": "King Halo"
},
{
"chara_id": 1062,
"name": "Matikanetannhauser"
},
{
"chara_id": 1067,
"name": "Satono Diamond"
},
{
"chara_id": 1068,
"name": "Kitasan Black"
},
{
"chara_id": 1069,
"name": "Sakura Chiyono O"
},
{
"chara_id": 1071,
"name": "Mejiro Ardan"
},
{
"chara_id": 1074,
"name": "Mejiro Bright"
}
]

2378
global/conversation.json Normal file

File diff suppressed because it is too large Load Diff

1712
global/race.json Normal file

File diff suppressed because it is too large Load Diff

1420
global/saddle.json Normal file

File diff suppressed because it is too large Load Diff

17
global/scenario.json Normal file
View File

@@ -0,0 +1,17 @@
[
{
"scenario_id": 1,
"name": "URA Finale",
"title": "The Beginning: URA Finale"
},
{
"scenario_id": 2,
"name": "Unity Cup",
"title": "Unity Cup: Shine On, Team Spirit!"
},
{
"scenario_id": 4,
"name": "TS Climax",
"title": "Trackblazer: Start of the Climax"
}
]

1768
global/skill-group.json Normal file

File diff suppressed because it is too large Load Diff

15853
global/skill.json Normal file

File diff suppressed because it is too large Load Diff

36326
global/spark.json Normal file

File diff suppressed because it is too large Load Diff

1730
global/uma.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
package main
import "os"
//go:generate go run ./horsegen
//go:generate go generate ./horse/...
//go:generate go fmt ./...
//go:generate go test ./...
func main() {
os.Stderr.WriteString("go generate, not go run\n")
os.Exit(2)
}

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.25.5
require ( require (
github.com/disgoorg/disgo v0.19.0-rc.15 github.com/disgoorg/disgo v0.19.0-rc.15
github.com/junegunn/fzf v0.67.0 github.com/junegunn/fzf v0.67.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.20.0
zombiezen.com/go/sqlite v1.4.2 zombiezen.com/go/sqlite v1.4.2
) )

4
go.sum
View File

@@ -40,8 +40,8 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5Z
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

View File

@@ -3,10 +3,57 @@ package horse
type CharacterID int16 type CharacterID int16
type Character struct { type Character struct {
ID CharacterID ID CharacterID `json:"chara_id"`
Name string Name string `json:"name"`
} }
func (c Character) String() string { func (c Character) String() string {
return c.Name return c.Name
} }
type AffinityRelation struct {
IDA int `json:"chara_a"`
IDB int `json:"chara_b"`
IDC int `json:"chara_c,omitzero"`
Affinity int `json:"affinity"`
}
// Conversation describes a lobby conversation.
type Conversation struct {
// CharacterID is the ID of the character who has the conversation as
// a gallery entry.
CharacterID CharacterID `json:"chara_id"`
// Number is the conversation number within the character's gallery.
Number int `json:"number"`
// Location is the ID of the location the conversation occurs in the lobby.
Location LobbyConversationLocationID `json:"location"`
// LocationName is the name of the lobby location, for convenience.
LocationName string `json:"location_name"`
// Chara1 is the first conversation participant ID.
// It does not necessarily match CharacterID.
Chara1 CharacterID `json:"chara_1"`
// Chara2 is the second conversation participant ID.
Chara2 CharacterID `json:"chara_2,omitzero"`
// Chara3 is the third conversation participant ID.
Chara3 CharacterID `json:"chara_3,omitzero"`
// ConditionType is a complete mystery to me.
ConditionType int `json:"condition_type"`
}
type LobbyConversationLocationID int
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type LobbyConversationLocationID -trimprefix Lobby -linecomment
const (
LobbyRightFront1 LobbyConversationLocationID = 110 // right side front
LobbyRightFront2 LobbyConversationLocationID = 120 // right side front
LobbyRightFront3 LobbyConversationLocationID = 130 // right side front
LobbyLeftTable1 LobbyConversationLocationID = 210 // left side table
LobbyLeftTable2 LobbyConversationLocationID = 220 // left side table
LobbyBackSeat LobbyConversationLocationID = 310 // center back seat
LobbyPosters1 LobbyConversationLocationID = 410 // center posters
LobbyPosters2 LobbyConversationLocationID = 420 // center posters
LobbyPosters3 LobbyConversationLocationID = 430 // center posters
LobbySchoolMap1 LobbyConversationLocationID = 510 // left side school map
LobbySchoolMap2 LobbyConversationLocationID = 520 // left side school map
LobbySchoolMap3 LobbyConversationLocationID = 530 // left side school map
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,913 +0,0 @@
module horse/global/race
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import std/data/rb-map
import horse/game-id
pub import horse/race
extern create-id-table(): vector<int>
c inline "int32_t arr[] = {1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1101,1102,1103,1104,1105,1106,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4030,4031,4032,4033,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,4049,4050,4051,4052,4053,4054,4055,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4068,4069,4070,4071,4072,4073,4074,4075,4076,4077,4078,4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4118,4119,4120,4121,4122,4123,4124,4501,4502,4503,4504,4505,4506,4507,4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,4519,4520,4521,4522,4523,4524,4525,4526,};\nkk_vector_from_cint32array(arr, (kk_ssize_t)285, kk_context())"
js inline "[1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1101,1102,1103,1104,1105,1106,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4030,4031,4032,4033,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,4049,4050,4051,4052,4053,4054,4055,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4068,4069,4070,4071,4072,4073,4074,4075,4076,4077,4078,4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4118,4119,4120,4121,4122,4123,4124,4501,4502,4503,4504,4505,4506,4507,4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,4519,4520,4521,4522,4523,4524,4525,4526,]"
// Vector of all race IDs in order for easy iterating.
pub val all = once(create-id-table)
val name2id = once()
var m: rbmap<string, int> := empty()
all().foreach() fn(id) m := m.set(Race-id(id).show, id)
m
// Get the race ID that has the given exact name.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the name, the result is an invalid ID.
pub fun from-name(name: string): race-id
Race-id(name2id().rb-map/lookup(name).default(0))
// Get the name for a race.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the ID, the result is the numeric ID.
pub fun show(r: race-id): string
match r.game-id
1001 -> "February Stakes"
1002 -> "Takamatsunomiya Kinen"
1003 -> "Osaka Hai"
1004 -> "Oka Sho"
1005 -> "Satsuki Sho"
1006 -> "Tenno Sho (Spring)"
1007 -> "NHK Mile Cup"
1008 -> "Victoria Mile"
1009 -> "Japanese Oaks"
1010 -> "Tokyo Yushun (Japanese Derby)"
1011 -> "Yasuda Kinen"
1012 -> "Takarazuka Kinen"
1013 -> "Sprinters Stakes"
1014 -> "Shuka Sho"
1015 -> "Kikuka Sho"
1016 -> "Tenno Sho (Autumn)"
1017 -> "Queen Elizabeth II Cup"
1018 -> "Mile Championship"
1019 -> "Japan Cup"
1020 -> "Champions Cup"
1021 -> "Hanshin Juvenile Fillies"
1022 -> "Asahi Hai Futurity Stakes"
1023 -> "Arima Kinen"
1024 -> "Hopeful Stakes"
1025 -> "Takarazuka Kinen" ++ " (Alternate 1025)"
1026 -> "Kikuka Sho" ++ " (Alternate 1026)"
1027 -> "Tenno Sho (Spring)" ++ " (Alternate 1027)"
1028 -> "Satsuki Sho" ++ " (Alternate 1028)"
1101 -> "Teio Sho"
1102 -> "Japan Dirt Derby"
1103 -> "JBC Ladies Classic"
1104 -> "JBC Sprint"
1105 -> "JBC Classic"
1106 -> "Tokyo Daishoten"
2001 -> "Nikkei Shinshun Hai"
2002 -> "Tokai Stakes"
2003 -> "American JCC"
2004 -> "Kyoto Kinen"
2005 -> "Nakayama Kinen"
2006 -> "Yayoi Sho"
2007 -> "Kinko Sho"
2008 -> "Fillies' Revue"
2009 -> "Hanshin Daishoten"
2010 -> "Spring Stakes"
2011 -> "Nikkei Sho"
2012 -> "Hanshin Umamusume Stakes"
2013 -> "New Zealand Trophy"
2014 -> "Milers Cup"
2015 -> "Flora Stakes"
2016 -> "Aoba Sho"
2017 -> "Kyoto Shimbun Hai"
2018 -> "Keio Hai Spring Cup"
2019 -> "Meguro Kinen"
2020 -> "Sapporo Kinen"
2021 -> "Centaur Stakes"
2022 -> "Rose Stakes"
2023 -> "St. Lite Kinen"
2024 -> "Kobe Shimbun Hai"
2025 -> "All Comers"
2026 -> "Mainichi Okan"
2027 -> "Kyoto Daishoten"
2028 -> "Fuchu Umamusume Stakes"
2029 -> "Swan Stakes"
2030 -> "Keio Hai Junior Stakes"
2031 -> "Copa Republica Argentina"
2032 -> "Daily Hai Junior Stakes"
2033 -> "Stayers Stakes"
2034 -> "Hanshin Cup"
2035 -> "Spring Stakes" ++ " (Alternate 2035)"
3001 -> "Kyoto Kimpai"
3002 -> "Nakayama Kimpai"
3003 -> "Shinzan Kinen"
3004 -> "Fairy Stakes"
3005 -> "Aichi Hai"
3006 -> "Keisei Hai"
3007 -> "Silk Road Stakes"
3008 -> "Negishi Stakes"
3009 -> "Kisaragi Sho"
3010 -> "Tokyo Shimbun Hai"
3011 -> "Queen Cup"
3012 -> "Kyodo News Hai"
3013 -> "Kyoto Umamusume Stakes"
3014 -> "Diamond Stakes"
3015 -> "Kokura Daishoten"
3016 -> "Arlington Cup"
3017 -> "Hankyu Hai"
3018 -> "Tulip Sho"
3019 -> "Ocean Stakes"
3020 -> "Nakayama Umamusume Stakes"
3021 -> "Falcon Stakes"
3022 -> "Flower Cup"
3023 -> "Mainichi Hai"
3024 -> "March Stakes"
3025 -> "Lord Derby Challenge Trophy"
3026 -> "Antares Stakes"
3027 -> "Fukushima Umamusume Stakes"
3028 -> "Niigata Daishoten"
3029 -> "Heian Stakes"
3030 -> "Naruo Kinen"
3031 -> "Mermaid Stakes"
3032 -> "Epsom Cup"
3033 -> "Unicorn Stakes"
3034 -> "Hakodate Sprint Stakes"
3035 -> "CBC Sho"
3036 -> "Radio Nikkei Sho"
3037 -> "Procyon Stakes"
3038 -> "Tanabata Sho"
3039 -> "Hakodate Kinen"
3040 -> "Chukyo Kinen"
3041 -> "Hakodate Junior Stakes"
3042 -> "Ibis Summer Dash"
3043 -> "Queen Stakes"
3044 -> "Kokura Kinen"
3045 -> "Leopard Stakes"
3046 -> "Sekiya Kinen"
3047 -> "Elm Stakes"
3048 -> "Kitakyushu Kinen"
3049 -> "Niigata Junior Stakes"
3050 -> "Keeneland Cup"
3051 -> "Sapporo Junior Stakes"
3052 -> "Kokura Junior Stakes"
3053 -> "Niigata Kinen"
3054 -> "Shion Stakes"
3055 -> "Keisei Hai Autumn Handicap"
3056 -> "Sirius Stakes"
3057 -> "Saudi Arabia Royal Cup"
3058 -> "Fuji Stakes"
3059 -> "Artemis Stakes"
3060 -> "Fantasy Stakes"
3061 -> "Miyako Stakes"
3062 -> "Musashino Stakes"
3063 -> "Fukushima Kinen"
3064 -> "Tokyo Sports Hai Junior Stakes"
3065 -> "Kyoto Junior Stakes"
3066 -> "Keihan Hai"
3067 -> "Challenge Cup"
3068 -> "Chunichi Shimbun Hai"
3069 -> "Capella Stakes"
3070 -> "Turquoise Stakes"
4001 -> "Manyo Stakes"
4002 -> "Junior Cup"
4003 -> "Yodo Tankyori Stakes"
4004 -> "Pollux Stakes"
4005 -> "January Stakes"
4006 -> "New Year Stakes"
4007 -> "Kobai Stakes"
4008 -> "Subaru Stakes"
4009 -> "Wakagoma Stakes"
4010 -> "Carbuncle Stakes"
4011 -> "Shirafuji Stakes"
4012 -> "Crocus Stakes"
4013 -> "Yamato Stakes"
4014 -> "Elfin Stakes"
4015 -> "Rakuyo Stakes"
4016 -> "Aldebaran Stakes"
4017 -> "Valentine Stakes"
4018 -> "Hyacinth Stakes"
4019 -> "Sobu Stakes"
4020 -> "Sumire Stakes"
4021 -> "Osakajo Stakes"
4022 -> "Polaris Stakes"
4023 -> "Nigawa Stakes"
4024 -> "Anemone Stakes"
4025 -> "Shoryu Stakes"
4026 -> "Kochi Stakes"
4027 -> "Wakaba Stakes"
4028 -> "Chiba Stakes"
4030 -> "Rokko Stakes"
4031 -> "Coral Stakes"
4032 -> "Marguerite Stakes"
4033 -> "Fukuryu Stakes"
4035 -> "Wasurenagusa Sho"
4036 -> "Keiyo Stakes"
4037 -> "Shunrai Stakes"
4038 -> "Fukushima Mimpo Hai"
4039 -> "Tachibana Stakes"
4040 -> "Oasis Stakes"
4041 -> "Tennozan Stakes"
4042 -> "Tango Stakes"
4043 -> "Sweetpea Stakes"
4044 -> "Tanigawadake Stakes"
4045 -> "Principal Stakes"
4046 -> "Metropolitan Stakes"
4047 -> "Kurama Stakes"
4048 -> "Brilliant Stakes"
4049 -> "Miyakooji Stakes"
4050 -> "Aoi Stakes"
4051 -> "Ritto Stakes"
4052 -> "Seiryu Stakes"
4053 -> "May Stakes"
4054 -> "Hosu Stakes"
4055 -> "Idaten Stakes"
4056 -> "Shirayuri Stakes"
4057 -> "Keyaki Stakes"
4058 -> "Azuchijo Stakes"
4059 -> "Akhalteke Stakes"
4060 -> "Tempozan Stakes"
4061 -> "Yonago Stakes"
4062 -> "Onuma Stakes"
4063 -> "Paradise Stakes"
4064 -> "Tomoe Sho"
4065 -> "Marine Stakes"
4066 -> "Meitetsu Hai"
4068 -> "Chukyo Junior Stakes"
4069 -> "Fukushima TV Open"
4070 -> "Dahlia Sho"
4071 -> "Sapporo Nikkei Open"
4072 -> "UHB Sho"
4073 -> "Aso Stakes"
4074 -> "Phoenix Sho"
4075 -> "Cosmos Sho"
4076 -> "NST Sho"
4077 -> "Clover Sho"
4078 -> "Himawari Sho"
4079 -> "BSN Sho"
4080 -> "Kokura Nikkei Open"
4081 -> "Toki Stakes"
4082 -> "Tancho Stakes"
4083 -> "Suzuran Sho"
4084 -> "Enif Stakes"
4085 -> "Nojigiku Stakes"
4086 -> "Radio Nippon Sho"
4087 -> "Kikyo Stakes"
4088 -> "Fuyo Stakes"
4089 -> "Canna Stakes"
4090 -> "Port Island Stakes"
4091 -> "Opal Stakes"
4092 -> "Green Channel Cup"
4093 -> "Momiji Stakes"
4094 -> "October Stakes"
4095 -> "Shinetsu Stakes"
4096 -> "Ivy Stakes"
4097 -> "Muromachi Stakes"
4098 -> "Brazil Cup"
4099 -> "Hagi Stakes"
4100 -> "Cassiopeia Stakes"
4101 -> "Lumiere Autumn Dash"
4102 -> "Oro Cup"
4103 -> "Fukushima Junior Stakes"
4104 -> "Andromeda Stakes"
4105 -> "Shimotsuki Stakes"
4106 -> "Fukushima Minyu Cup"
4107 -> "Capital Stakes"
4108 -> "Autumn Leaf Stakes"
4109 -> "Lapis Lazuli Stakes"
4110 -> "Shiwasu Stakes"
4111 -> "Rigel Stakes"
4112 -> "Tanzanite Stakes"
4113 -> "December Stakes"
4114 -> "Christmas Rose Stakes"
4115 -> "Galaxy Stakes"
4116 -> "Betelgeuse Stakes"
4118 -> "Kitakyushu Tankyori Stakes"
4119 -> "Azumakofuji Stakes"
4120 -> "Sleipnir Stakes"
4121 -> "Sannomiya Stakes"
4122 -> "Kanetsu Stakes"
4123 -> "Nagatsuki Stakes"
4124 -> "Uzumasa Stakes"
4501 -> "Aster Sho"
4502 -> "Saffron Sho"
4503 -> "Rindo Sho"
4504 -> "Shigiku Sho"
4505 -> "Platanus Sho"
4506 -> "Nadeshiko Sho"
4507 -> "Hyakunichiso Tokubetsu"
4508 -> "Kimmokusei Tokubetsu"
4509 -> "Oxalis Sho"
4510 -> "Kigiku Sho"
4511 -> "Mochinoki Sho"
4512 -> "Akamatsu Sho"
4513 -> "Shumeigiku Sho"
4514 -> "Cattleya Sho"
4515 -> "Begonia Sho"
4516 -> "Shiragiku Sho"
4517 -> "Habotan Sho"
4518 -> "Koyamaki Sho"
4519 -> "Manryo Sho"
4520 -> "Kuromatsu Sho"
4521 -> "Erica Sho"
4522 -> "Tsuwabuki Sho"
4523 -> "Hiiragi Sho"
4524 -> "Sazanka Sho"
4525 -> "Kantsubaki Sho"
4526 -> "Senryo Sho"
x -> "race " ++ x.show
// Get the grade for a race.
// If no race matches the ID, the result is Pre-OP.
pub fun grade(r: race-id): grade
match r.game-id
1001 -> G1
1002 -> G1
1003 -> G1
1004 -> G1
1005 -> G1
1006 -> G1
1007 -> G1
1008 -> G1
1009 -> G1
1010 -> G1
1011 -> G1
1012 -> G1
1013 -> G1
1014 -> G1
1015 -> G1
1016 -> G1
1017 -> G1
1018 -> G1
1019 -> G1
1020 -> G1
1021 -> G1
1022 -> G1
1023 -> G1
1024 -> G1
1025 -> G1
1026 -> G1
1027 -> G1
1028 -> G1
1101 -> G1
1102 -> G1
1103 -> G1
1104 -> G1
1105 -> G1
1106 -> G1
2001 -> G2
2002 -> G2
2003 -> G2
2004 -> G2
2005 -> G2
2006 -> G2
2007 -> G2
2008 -> G2
2009 -> G2
2010 -> G2
2011 -> G2
2012 -> G2
2013 -> G2
2014 -> G2
2015 -> G2
2016 -> G2
2017 -> G2
2018 -> G2
2019 -> G2
2020 -> G2
2021 -> G2
2022 -> G2
2023 -> G2
2024 -> G2
2025 -> G2
2026 -> G2
2027 -> G2
2028 -> G2
2029 -> G2
2030 -> G2
2031 -> G2
2032 -> G2
2033 -> G2
2034 -> G2
2035 -> G2
3001 -> G3
3002 -> G3
3003 -> G3
3004 -> G3
3005 -> G3
3006 -> G3
3007 -> G3
3008 -> G3
3009 -> G3
3010 -> G3
3011 -> G3
3012 -> G3
3013 -> G3
3014 -> G3
3015 -> G3
3016 -> G3
3017 -> G3
3018 -> G2
3019 -> G3
3020 -> G3
3021 -> G3
3022 -> G3
3023 -> G3
3024 -> G3
3025 -> G3
3026 -> G3
3027 -> G3
3028 -> G3
3029 -> G3
3030 -> G3
3031 -> G3
3032 -> G3
3033 -> G3
3034 -> G3
3035 -> G3
3036 -> G3
3037 -> G3
3038 -> G3
3039 -> G3
3040 -> G3
3041 -> G3
3042 -> G3
3043 -> G3
3044 -> G3
3045 -> G3
3046 -> G3
3047 -> G3
3048 -> G3
3049 -> G3
3050 -> G3
3051 -> G3
3052 -> G3
3053 -> G3
3054 -> G3
3055 -> G3
3056 -> G3
3057 -> G3
3058 -> G2
3059 -> G3
3060 -> G3
3061 -> G3
3062 -> G3
3063 -> G3
3064 -> G3
3065 -> G3
3066 -> G3
3067 -> G3
3068 -> G3
3069 -> G3
3070 -> G3
4001 -> OP
4002 -> OP
4003 -> OP
4004 -> OP
4005 -> OP
4006 -> OP
4007 -> OP
4008 -> OP
4009 -> OP
4010 -> OP
4011 -> OP
4012 -> OP
4013 -> OP
4014 -> OP
4015 -> OP
4016 -> OP
4017 -> OP
4018 -> OP
4019 -> OP
4020 -> OP
4021 -> OP
4022 -> OP
4023 -> OP
4024 -> OP
4025 -> OP
4026 -> OP
4027 -> OP
4028 -> OP
4030 -> OP
4031 -> OP
4032 -> OP
4033 -> OP
4035 -> OP
4036 -> OP
4037 -> OP
4038 -> OP
4039 -> OP
4040 -> OP
4041 -> OP
4042 -> OP
4043 -> OP
4044 -> OP
4045 -> OP
4046 -> OP
4047 -> OP
4048 -> OP
4049 -> OP
4050 -> G3
4051 -> OP
4052 -> OP
4053 -> OP
4054 -> OP
4055 -> OP
4056 -> OP
4057 -> OP
4058 -> OP
4059 -> OP
4060 -> OP
4061 -> OP
4062 -> OP
4063 -> OP
4064 -> OP
4065 -> OP
4066 -> OP
4068 -> OP
4069 -> OP
4070 -> OP
4071 -> OP
4072 -> OP
4073 -> OP
4074 -> OP
4075 -> OP
4076 -> OP
4077 -> OP
4078 -> OP
4079 -> OP
4080 -> OP
4081 -> OP
4082 -> OP
4083 -> OP
4084 -> OP
4085 -> OP
4086 -> OP
4087 -> OP
4088 -> OP
4089 -> OP
4090 -> OP
4091 -> OP
4092 -> OP
4093 -> OP
4094 -> OP
4095 -> OP
4096 -> OP
4097 -> OP
4098 -> OP
4099 -> OP
4100 -> OP
4101 -> OP
4102 -> OP
4103 -> OP
4104 -> OP
4105 -> OP
4106 -> OP
4107 -> OP
4108 -> OP
4109 -> OP
4110 -> OP
4111 -> OP
4112 -> OP
4113 -> OP
4114 -> OP
4115 -> OP
4116 -> OP
4118 -> OP
4119 -> OP
4120 -> OP
4121 -> OP
4122 -> OP
4123 -> OP
4124 -> OP
4501 -> Pre-OP
4502 -> Pre-OP
4503 -> Pre-OP
4504 -> Pre-OP
4505 -> Pre-OP
4506 -> Pre-OP
4507 -> Pre-OP
4508 -> Pre-OP
4509 -> Pre-OP
4510 -> Pre-OP
4511 -> Pre-OP
4512 -> Pre-OP
4513 -> Pre-OP
4514 -> Pre-OP
4515 -> Pre-OP
4516 -> Pre-OP
4517 -> Pre-OP
4518 -> Pre-OP
4519 -> Pre-OP
4520 -> Pre-OP
4521 -> Pre-OP
4522 -> Pre-OP
4523 -> Pre-OP
4524 -> Pre-OP
4525 -> Pre-OP
4526 -> Pre-OP
_ -> Pre-OP
// Get the thumbnail ID for a race.
// If no race matches the ID, the result is an invalid ID.
pub fun thumbnail(r: race-id): race-thumbnail-id
match r.game-id
1001 -> Race-thumbnail-id(1001)
1002 -> Race-thumbnail-id(1002)
1003 -> Race-thumbnail-id(1003)
1004 -> Race-thumbnail-id(1004)
1005 -> Race-thumbnail-id(1005)
1006 -> Race-thumbnail-id(1006)
1007 -> Race-thumbnail-id(1007)
1008 -> Race-thumbnail-id(1008)
1009 -> Race-thumbnail-id(1009)
1010 -> Race-thumbnail-id(1010)
1011 -> Race-thumbnail-id(1011)
1012 -> Race-thumbnail-id(1012)
1013 -> Race-thumbnail-id(1013)
1014 -> Race-thumbnail-id(1014)
1015 -> Race-thumbnail-id(1015)
1016 -> Race-thumbnail-id(1016)
1017 -> Race-thumbnail-id(1017)
1018 -> Race-thumbnail-id(1018)
1019 -> Race-thumbnail-id(1019)
1020 -> Race-thumbnail-id(1020)
1021 -> Race-thumbnail-id(1021)
1022 -> Race-thumbnail-id(1022)
1023 -> Race-thumbnail-id(1023)
1024 -> Race-thumbnail-id(1024)
1025 -> Race-thumbnail-id(1012)
1026 -> Race-thumbnail-id(1015)
1027 -> Race-thumbnail-id(1027)
1028 -> Race-thumbnail-id(1028)
1101 -> Race-thumbnail-id(1101)
1102 -> Race-thumbnail-id(1102)
1103 -> Race-thumbnail-id(1103)
1104 -> Race-thumbnail-id(1104)
1105 -> Race-thumbnail-id(1105)
1106 -> Race-thumbnail-id(1106)
2001 -> Race-thumbnail-id(2001)
2002 -> Race-thumbnail-id(2002)
2003 -> Race-thumbnail-id(2003)
2004 -> Race-thumbnail-id(2004)
2005 -> Race-thumbnail-id(2005)
2006 -> Race-thumbnail-id(2006)
2007 -> Race-thumbnail-id(2007)
2008 -> Race-thumbnail-id(2008)
2009 -> Race-thumbnail-id(2009)
2010 -> Race-thumbnail-id(2010)
2011 -> Race-thumbnail-id(2011)
2012 -> Race-thumbnail-id(2012)
2013 -> Race-thumbnail-id(2013)
2014 -> Race-thumbnail-id(2014)
2015 -> Race-thumbnail-id(2015)
2016 -> Race-thumbnail-id(2016)
2017 -> Race-thumbnail-id(2017)
2018 -> Race-thumbnail-id(2018)
2019 -> Race-thumbnail-id(2019)
2020 -> Race-thumbnail-id(2020)
2021 -> Race-thumbnail-id(2021)
2022 -> Race-thumbnail-id(2022)
2023 -> Race-thumbnail-id(2023)
2024 -> Race-thumbnail-id(2024)
2025 -> Race-thumbnail-id(2025)
2026 -> Race-thumbnail-id(2026)
2027 -> Race-thumbnail-id(2027)
2028 -> Race-thumbnail-id(2028)
2029 -> Race-thumbnail-id(2029)
2030 -> Race-thumbnail-id(2030)
2031 -> Race-thumbnail-id(2031)
2032 -> Race-thumbnail-id(2032)
2033 -> Race-thumbnail-id(2033)
2034 -> Race-thumbnail-id(2034)
2035 -> Race-thumbnail-id(2010)
3001 -> Race-thumbnail-id(3001)
3002 -> Race-thumbnail-id(3002)
3003 -> Race-thumbnail-id(3003)
3004 -> Race-thumbnail-id(3004)
3005 -> Race-thumbnail-id(3005)
3006 -> Race-thumbnail-id(3006)
3007 -> Race-thumbnail-id(3007)
3008 -> Race-thumbnail-id(3008)
3009 -> Race-thumbnail-id(3009)
3010 -> Race-thumbnail-id(3010)
3011 -> Race-thumbnail-id(3011)
3012 -> Race-thumbnail-id(3012)
3013 -> Race-thumbnail-id(3013)
3014 -> Race-thumbnail-id(3014)
3015 -> Race-thumbnail-id(3015)
3016 -> Race-thumbnail-id(3016)
3017 -> Race-thumbnail-id(3017)
3018 -> Race-thumbnail-id(3018)
3019 -> Race-thumbnail-id(3019)
3020 -> Race-thumbnail-id(3020)
3021 -> Race-thumbnail-id(3021)
3022 -> Race-thumbnail-id(3022)
3023 -> Race-thumbnail-id(3023)
3024 -> Race-thumbnail-id(3024)
3025 -> Race-thumbnail-id(3025)
3026 -> Race-thumbnail-id(3026)
3027 -> Race-thumbnail-id(3027)
3028 -> Race-thumbnail-id(3028)
3029 -> Race-thumbnail-id(3029)
3030 -> Race-thumbnail-id(3030)
3031 -> Race-thumbnail-id(3031)
3032 -> Race-thumbnail-id(3032)
3033 -> Race-thumbnail-id(3033)
3034 -> Race-thumbnail-id(3034)
3035 -> Race-thumbnail-id(3035)
3036 -> Race-thumbnail-id(3036)
3037 -> Race-thumbnail-id(3037)
3038 -> Race-thumbnail-id(3038)
3039 -> Race-thumbnail-id(3039)
3040 -> Race-thumbnail-id(3040)
3041 -> Race-thumbnail-id(3041)
3042 -> Race-thumbnail-id(3042)
3043 -> Race-thumbnail-id(3043)
3044 -> Race-thumbnail-id(3044)
3045 -> Race-thumbnail-id(3045)
3046 -> Race-thumbnail-id(3046)
3047 -> Race-thumbnail-id(3047)
3048 -> Race-thumbnail-id(3048)
3049 -> Race-thumbnail-id(3049)
3050 -> Race-thumbnail-id(3050)
3051 -> Race-thumbnail-id(3051)
3052 -> Race-thumbnail-id(3052)
3053 -> Race-thumbnail-id(3053)
3054 -> Race-thumbnail-id(3054)
3055 -> Race-thumbnail-id(3055)
3056 -> Race-thumbnail-id(3056)
3057 -> Race-thumbnail-id(3057)
3058 -> Race-thumbnail-id(3058)
3059 -> Race-thumbnail-id(3059)
3060 -> Race-thumbnail-id(3060)
3061 -> Race-thumbnail-id(3061)
3062 -> Race-thumbnail-id(3062)
3063 -> Race-thumbnail-id(3063)
3064 -> Race-thumbnail-id(3064)
3065 -> Race-thumbnail-id(3065)
3066 -> Race-thumbnail-id(3066)
3067 -> Race-thumbnail-id(3067)
3068 -> Race-thumbnail-id(3068)
3069 -> Race-thumbnail-id(3069)
3070 -> Race-thumbnail-id(3070)
4001 -> Race-thumbnail-id(4001)
4002 -> Race-thumbnail-id(4002)
4003 -> Race-thumbnail-id(4003)
4004 -> Race-thumbnail-id(4004)
4005 -> Race-thumbnail-id(4005)
4006 -> Race-thumbnail-id(4006)
4007 -> Race-thumbnail-id(4007)
4008 -> Race-thumbnail-id(4008)
4009 -> Race-thumbnail-id(4009)
4010 -> Race-thumbnail-id(4010)
4011 -> Race-thumbnail-id(4011)
4012 -> Race-thumbnail-id(4012)
4013 -> Race-thumbnail-id(4013)
4014 -> Race-thumbnail-id(4014)
4015 -> Race-thumbnail-id(4015)
4016 -> Race-thumbnail-id(4016)
4017 -> Race-thumbnail-id(4017)
4018 -> Race-thumbnail-id(4018)
4019 -> Race-thumbnail-id(4019)
4020 -> Race-thumbnail-id(4020)
4021 -> Race-thumbnail-id(4021)
4022 -> Race-thumbnail-id(4022)
4023 -> Race-thumbnail-id(4023)
4024 -> Race-thumbnail-id(4024)
4025 -> Race-thumbnail-id(4025)
4026 -> Race-thumbnail-id(4026)
4027 -> Race-thumbnail-id(4027)
4028 -> Race-thumbnail-id(4028)
4030 -> Race-thumbnail-id(4030)
4031 -> Race-thumbnail-id(4031)
4032 -> Race-thumbnail-id(4032)
4033 -> Race-thumbnail-id(4033)
4035 -> Race-thumbnail-id(4035)
4036 -> Race-thumbnail-id(4036)
4037 -> Race-thumbnail-id(4037)
4038 -> Race-thumbnail-id(4038)
4039 -> Race-thumbnail-id(4039)
4040 -> Race-thumbnail-id(4040)
4041 -> Race-thumbnail-id(4041)
4042 -> Race-thumbnail-id(4042)
4043 -> Race-thumbnail-id(4043)
4044 -> Race-thumbnail-id(4044)
4045 -> Race-thumbnail-id(4045)
4046 -> Race-thumbnail-id(4046)
4047 -> Race-thumbnail-id(4047)
4048 -> Race-thumbnail-id(4048)
4049 -> Race-thumbnail-id(4049)
4050 -> Race-thumbnail-id(4050)
4051 -> Race-thumbnail-id(4051)
4052 -> Race-thumbnail-id(4052)
4053 -> Race-thumbnail-id(4053)
4054 -> Race-thumbnail-id(4054)
4055 -> Race-thumbnail-id(4055)
4056 -> Race-thumbnail-id(4056)
4057 -> Race-thumbnail-id(4057)
4058 -> Race-thumbnail-id(4058)
4059 -> Race-thumbnail-id(4059)
4060 -> Race-thumbnail-id(4060)
4061 -> Race-thumbnail-id(4061)
4062 -> Race-thumbnail-id(4062)
4063 -> Race-thumbnail-id(4063)
4064 -> Race-thumbnail-id(4064)
4065 -> Race-thumbnail-id(4065)
4066 -> Race-thumbnail-id(4066)
4068 -> Race-thumbnail-id(4068)
4069 -> Race-thumbnail-id(4069)
4070 -> Race-thumbnail-id(4070)
4071 -> Race-thumbnail-id(4071)
4072 -> Race-thumbnail-id(4072)
4073 -> Race-thumbnail-id(4073)
4074 -> Race-thumbnail-id(4074)
4075 -> Race-thumbnail-id(4075)
4076 -> Race-thumbnail-id(4076)
4077 -> Race-thumbnail-id(4077)
4078 -> Race-thumbnail-id(4078)
4079 -> Race-thumbnail-id(4079)
4080 -> Race-thumbnail-id(4080)
4081 -> Race-thumbnail-id(4081)
4082 -> Race-thumbnail-id(4082)
4083 -> Race-thumbnail-id(4083)
4084 -> Race-thumbnail-id(4084)
4085 -> Race-thumbnail-id(4085)
4086 -> Race-thumbnail-id(4086)
4087 -> Race-thumbnail-id(4087)
4088 -> Race-thumbnail-id(4088)
4089 -> Race-thumbnail-id(4089)
4090 -> Race-thumbnail-id(4090)
4091 -> Race-thumbnail-id(4091)
4092 -> Race-thumbnail-id(4092)
4093 -> Race-thumbnail-id(4093)
4094 -> Race-thumbnail-id(4094)
4095 -> Race-thumbnail-id(4095)
4096 -> Race-thumbnail-id(4096)
4097 -> Race-thumbnail-id(4097)
4098 -> Race-thumbnail-id(4098)
4099 -> Race-thumbnail-id(4099)
4100 -> Race-thumbnail-id(4100)
4101 -> Race-thumbnail-id(4101)
4102 -> Race-thumbnail-id(4102)
4103 -> Race-thumbnail-id(4103)
4104 -> Race-thumbnail-id(4104)
4105 -> Race-thumbnail-id(4105)
4106 -> Race-thumbnail-id(4106)
4107 -> Race-thumbnail-id(4107)
4108 -> Race-thumbnail-id(4108)
4109 -> Race-thumbnail-id(4109)
4110 -> Race-thumbnail-id(4110)
4111 -> Race-thumbnail-id(4111)
4112 -> Race-thumbnail-id(4112)
4113 -> Race-thumbnail-id(4113)
4114 -> Race-thumbnail-id(4114)
4115 -> Race-thumbnail-id(4115)
4116 -> Race-thumbnail-id(4116)
4118 -> Race-thumbnail-id(4117)
4119 -> Race-thumbnail-id(4118)
4120 -> Race-thumbnail-id(4119)
4121 -> Race-thumbnail-id(4120)
4122 -> Race-thumbnail-id(4121)
4123 -> Race-thumbnail-id(4122)
4124 -> Race-thumbnail-id(4123)
4501 -> Race-thumbnail-id(4501)
4502 -> Race-thumbnail-id(4502)
4503 -> Race-thumbnail-id(4503)
4504 -> Race-thumbnail-id(4504)
4505 -> Race-thumbnail-id(4505)
4506 -> Race-thumbnail-id(4506)
4507 -> Race-thumbnail-id(4507)
4508 -> Race-thumbnail-id(4508)
4509 -> Race-thumbnail-id(4509)
4510 -> Race-thumbnail-id(4510)
4511 -> Race-thumbnail-id(4511)
4512 -> Race-thumbnail-id(4512)
4513 -> Race-thumbnail-id(4513)
4514 -> Race-thumbnail-id(4514)
4515 -> Race-thumbnail-id(4515)
4516 -> Race-thumbnail-id(4516)
4517 -> Race-thumbnail-id(4517)
4518 -> Race-thumbnail-id(4518)
4519 -> Race-thumbnail-id(4519)
4520 -> Race-thumbnail-id(4520)
4521 -> Race-thumbnail-id(4521)
4522 -> Race-thumbnail-id(4522)
4523 -> Race-thumbnail-id(4523)
4524 -> Race-thumbnail-id(4524)
4525 -> Race-thumbnail-id(4525)
4526 -> Race-thumbnail-id(4526)
_ -> Race-thumbnail-id(0)
// Get the primary ID for a race.
// For races which are the primary version, or if no race matches the given ID,
// the result is the input.
pub fun primary(r: race-id): race-id
match r.game-id
1025 -> Race-id(1012)
1026 -> Race-id(1015)
1027 -> Race-id(1006)
1028 -> Race-id(1005)
2035 -> Race-id(2010)
_ -> r

File diff suppressed because it is too large Load Diff

View File

@@ -1,518 +0,0 @@
module horse/global/saddle
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import horse/game-id
pub import horse/race
pub import horse/global/race
extern create-id-table(): vector<int>
c inline "int32_t arr[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,};\nkk_vector_from_cint32array(arr, (kk_ssize_t)155, kk_context())"
js inline "[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,]"
// Vector of all saddle IDs in order for easy iterating.
pub val all = once(create-id-table)
// Get the name for a saddle.
// Alternate versions of saddles have an indication of their ID in their names.
// If no saddle matches the ID, the result contains the numeric ID.
pub fun show(s: saddle-id): string
match s.game-id
1 -> "Classic Triple Crown"
2 -> "Senior Autumn Triple Crown"
3 -> "Triple Tiara"
4 -> "Senior Spring Triple Crown"
5 -> "Tenno Sweep"
6 -> "Dual Grand Prix"
7 -> "Dual Miles"
8 -> "Dual Sprints"
9 -> "Dual Dirts"
10 -> "Arima Kinen"
11 -> "Japan C."
12 -> "Japanese Derby"
13 -> "Tenno Sho (Spring)"
14 -> "Takarazuka Kinen"
15 -> "Tenno Sho (Autumn)"
16 -> "Kikuka Sho"
17 -> "Osaka Hai"
18 -> "Satsuki Sho"
19 -> "Japanese Oaks"
20 -> "Takamatsunomiya Kinen"
21 -> "Yasuda Kinen"
22 -> "Sprinters S."
23 -> "Mile Ch."
24 -> "Oka Sho"
25 -> "Victoria Mile"
26 -> "Queen Elizabeth II Cup"
27 -> "NHK Mile C."
28 -> "Shuka Sho"
29 -> "Champions C."
30 -> "February S."
31 -> "JBC Classic"
32 -> "Tokyo Daishoten"
33 -> "Asahi Hai F.S."
34 -> "Hopeful S."
35 -> "Hanshin J.F."
36 -> "Teio Sho"
37 -> "JBC Sprint"
38 -> "J.D. Derby"
39 -> "JBC L. Classic"
40 -> "Nikkei Shinshun Hai"
41 -> "Tokai S."
42 -> "American JCC"
43 -> "Kyoto Kinen"
44 -> "Nakayama Kinen"
45 -> "Yayoi Sho"
46 -> "Kinko Sho"
47 -> "Fillies' Revue"
48 -> "Hanshin Daishoten"
49 -> "Spring S."
50 -> "Nikkei Sho"
51 -> "Hanshin Umamusume S."
52 -> "New Zealand T."
53 -> "Yomiuri Milers C."
54 -> "Flora S."
55 -> "Aoba Sho"
56 -> "Kyoto Shimbun Hai"
57 -> "Keio Hai Spring C."
58 -> "Meguro Kinen"
59 -> "Sapporo Kinen"
60 -> "Centaur S."
61 -> "Rose S."
62 -> "St. Lite Kinen"
63 -> "Kobe Shimbun Hai"
64 -> "All Comers"
65 -> "Mainichi Okan"
66 -> "Kyoto Daishoten"
67 -> "Fuchu Umamusume S."
68 -> "Swan S."
69 -> "Keio Hai Junior S."
70 -> "Copa Republica Argentina"
71 -> "Daily Hai Junior S."
72 -> "Stayers S."
73 -> "Hanshin C."
74 -> "Kyoto Kimpai"
75 -> "Nakayama Kimpai"
76 -> "Shinzan Kinen"
77 -> "Fairy S."
78 -> "Aichi Hai"
79 -> "Keisei Hai"
80 -> "Silk Road S."
81 -> "Negishi S."
82 -> "Kisaragi Sho"
83 -> "Tokyo Shimbun Hai"
84 -> "Queen C."
85 -> "Kyodo News Hai"
86 -> "Kyoto Umamusume S."
87 -> "Diamond S."
88 -> "Kokura Daishoten"
89 -> "Arlington C."
90 -> "Hankyu Hai"
91 -> "Tulip Sho"
92 -> "Ocean S."
93 -> "Nakayama Umamusume S."
94 -> "Falcon S."
95 -> "Flower C."
96 -> "Mainichi Hai"
97 -> "March S."
98 -> "Lord Derby C.T."
99 -> "Antares S."
100 -> "Fukushima Umamusume S."
101 -> "Niigata Daishoten"
102 -> "Heian S."
103 -> "Naruo Kinen"
104 -> "Mermaid S."
105 -> "Epsom C."
106 -> "Unicorn S."
107 -> "Hakodate Sprint S."
108 -> "CBC Sho"
109 -> "Radio Nikkei Sho"
110 -> "Procyon S."
111 -> "Tanabata Sho"
112 -> "Hakodate Kinen"
113 -> "Chukyo Kinen"
114 -> "Hakodate Junior S."
115 -> "Ibis Summer D."
116 -> "Queen S."
117 -> "Kokura Kinen"
118 -> "Leopard S."
119 -> "Sekiya Kinen"
120 -> "Elm S."
121 -> "Kitakyushu Kinen"
122 -> "Niigata Junior S."
123 -> "Keeneland C."
124 -> "Sapporo Junior S."
125 -> "Kokura Junior S."
126 -> "Niigata Kinen"
127 -> "Shion S."
128 -> "Keisei Hai A.H."
129 -> "Sirius S."
130 -> "Saudi Arabia R.C."
131 -> "Fuji S."
132 -> "Artemis S."
133 -> "Fantasy S."
134 -> "Miyako S."
135 -> "Musashino S."
136 -> "Fukushima Kinen"
137 -> "Tokyo Sports Hai Junior S."
138 -> "Kyoto Junior S."
139 -> "Keihan Hai"
140 -> "Challenge C."
141 -> "Chunichi Shimbun Hai"
142 -> "Capella S."
143 -> "Turquoise S."
144 -> "Classic Triple Crown" ++ " (Alternate 144)"
145 -> "Senior Spring Triple Crown" ++ " (Alternate 145)"
146 -> "Dual Grand Prix" ++ " (Alternate 146)"
147 -> "Takarazuka Kinen" ++ " (Alternate 147)"
148 -> "Kikuka Sho" ++ " (Alternate 148)"
149 -> "Spring S." ++ " (Alternate 149)"
150 -> "Aoi S."
151 -> "Senior Spring Triple Crown" ++ " (Alternate 151)"
152 -> "Tenno Sweep" ++ " (Alternate 152)"
153 -> "Tenno Sho (Spring)" ++ " (Alternate 153)"
154 -> "Classic Triple Crown" ++ " (Alternate 154)"
155 -> "Satsuki Sho" ++ " (Alternate 155)"
x -> "saddle " ++ x.show
// Get the list of races that entitle a horse to a saddle.
// If no saddle matches the ID, the result is the empty list.
pub fun races(s: saddle-id): list<race-id>
match s.game-id
1 -> [Race-id(100501), Race-id(101001), Race-id(101501), ]
2 -> [Race-id(101601), Race-id(101901), Race-id(102301), ]
3 -> [Race-id(100401), Race-id(100901), Race-id(101401), ]
4 -> [Race-id(100301), Race-id(100601), Race-id(101201), ]
5 -> [Race-id(100601), Race-id(101601), ]
6 -> [Race-id(101201), Race-id(102301), ]
7 -> [Race-id(101101), Race-id(101801), ]
8 -> [Race-id(101301), Race-id(100201), ]
9 -> [Race-id(100101), Race-id(102001), ]
10 -> [Race-id(102301), ]
11 -> [Race-id(101901), ]
12 -> [Race-id(101001), ]
13 -> [Race-id(100601), ]
14 -> [Race-id(101201), ]
15 -> [Race-id(101601), ]
16 -> [Race-id(101501), ]
17 -> [Race-id(100301), ]
18 -> [Race-id(100501), ]
19 -> [Race-id(100901), ]
20 -> [Race-id(100201), ]
21 -> [Race-id(101101), ]
22 -> [Race-id(101301), ]
23 -> [Race-id(101801), ]
24 -> [Race-id(100401), ]
25 -> [Race-id(100801), ]
26 -> [Race-id(101701), ]
27 -> [Race-id(100701), ]
28 -> [Race-id(101401), ]
29 -> [Race-id(102001), ]
30 -> [Race-id(100101), ]
31 -> [Race-id(110501), ]
32 -> [Race-id(110601), ]
33 -> [Race-id(102201), ]
34 -> [Race-id(102401), ]
35 -> [Race-id(102101), ]
36 -> [Race-id(110101), ]
37 -> [Race-id(110401), ]
38 -> [Race-id(110201), ]
39 -> [Race-id(110301), ]
40 -> [Race-id(200101), ]
41 -> [Race-id(200201), ]
42 -> [Race-id(200301), ]
43 -> [Race-id(200401), ]
44 -> [Race-id(200501), ]
45 -> [Race-id(200601), ]
46 -> [Race-id(200701), ]
47 -> [Race-id(200801), ]
48 -> [Race-id(200901), ]
49 -> [Race-id(201001), ]
50 -> [Race-id(201101), ]
51 -> [Race-id(201201), ]
52 -> [Race-id(201301), ]
53 -> [Race-id(201401), ]
54 -> [Race-id(201501), ]
55 -> [Race-id(201601), ]
56 -> [Race-id(201701), ]
57 -> [Race-id(201801), ]
58 -> [Race-id(201901), ]
59 -> [Race-id(202001), ]
60 -> [Race-id(202101), ]
61 -> [Race-id(202201), ]
62 -> [Race-id(202301), ]
63 -> [Race-id(202401), ]
64 -> [Race-id(202501), ]
65 -> [Race-id(202601), ]
66 -> [Race-id(202701), ]
67 -> [Race-id(202801), ]
68 -> [Race-id(202901), ]
69 -> [Race-id(203001), ]
70 -> [Race-id(203101), ]
71 -> [Race-id(203201), ]
72 -> [Race-id(203301), ]
73 -> [Race-id(203401), ]
74 -> [Race-id(300101), ]
75 -> [Race-id(300201), ]
76 -> [Race-id(300301), ]
77 -> [Race-id(300401), ]
78 -> [Race-id(300501), ]
79 -> [Race-id(300601), ]
80 -> [Race-id(300701), ]
81 -> [Race-id(300801), ]
82 -> [Race-id(300901), ]
83 -> [Race-id(301001), ]
84 -> [Race-id(301101), ]
85 -> [Race-id(301201), ]
86 -> [Race-id(301301), ]
87 -> [Race-id(301401), ]
88 -> [Race-id(301501), ]
89 -> [Race-id(301601), ]
90 -> [Race-id(301701), ]
91 -> [Race-id(301801), ]
92 -> [Race-id(301901), ]
93 -> [Race-id(302001), ]
94 -> [Race-id(302101), ]
95 -> [Race-id(302201), ]
96 -> [Race-id(302301), ]
97 -> [Race-id(302401), ]
98 -> [Race-id(302501), ]
99 -> [Race-id(302601), ]
100 -> [Race-id(302701), ]
101 -> [Race-id(302801), ]
102 -> [Race-id(302901), ]
103 -> [Race-id(303001), ]
104 -> [Race-id(303101), ]
105 -> [Race-id(303201), ]
106 -> [Race-id(303301), ]
107 -> [Race-id(303401), ]
108 -> [Race-id(303501), ]
109 -> [Race-id(303601), ]
110 -> [Race-id(303701), ]
111 -> [Race-id(303801), ]
112 -> [Race-id(303901), ]
113 -> [Race-id(304001), ]
114 -> [Race-id(304101), ]
115 -> [Race-id(304201), ]
116 -> [Race-id(304301), ]
117 -> [Race-id(304401), ]
118 -> [Race-id(304501), ]
119 -> [Race-id(304601), ]
120 -> [Race-id(304701), ]
121 -> [Race-id(304801), ]
122 -> [Race-id(304901), ]
123 -> [Race-id(305001), ]
124 -> [Race-id(305101), ]
125 -> [Race-id(305201), ]
126 -> [Race-id(305301), ]
127 -> [Race-id(305401), ]
128 -> [Race-id(305501), ]
129 -> [Race-id(305601), ]
130 -> [Race-id(305701), ]
131 -> [Race-id(305801), ]
132 -> [Race-id(305901), ]
133 -> [Race-id(306001), ]
134 -> [Race-id(306101), ]
135 -> [Race-id(306201), ]
136 -> [Race-id(306301), ]
137 -> [Race-id(306401), ]
138 -> [Race-id(306501), ]
139 -> [Race-id(306601), ]
140 -> [Race-id(306701), ]
141 -> [Race-id(306801), ]
142 -> [Race-id(306901), ]
143 -> [Race-id(307001), ]
144 -> [Race-id(100501), Race-id(101001), Race-id(102601), ]
145 -> [Race-id(100301), Race-id(100601), Race-id(102501), ]
146 -> [Race-id(102501), Race-id(102301), ]
147 -> [Race-id(102501), ]
148 -> [Race-id(102601), ]
149 -> [Race-id(203501), ]
150 -> [Race-id(405001), ]
151 -> [Race-id(100301), Race-id(102701), Race-id(101201), ]
152 -> [Race-id(102701), Race-id(101601), ]
153 -> [Race-id(102701), ]
154 -> [Race-id(102801), Race-id(101001), Race-id(101501), ]
155 -> [Race-id(102801), ]
_ -> []
// Get a saddle's type.
// If no saddle matches the ID, the result is Honor.
pub fun saddle-type(s: saddle-id): saddle-type
match s.game-id
1 -> Honor
2 -> Honor
3 -> Honor
4 -> Honor
5 -> Honor
6 -> Honor
7 -> Honor
8 -> Honor
9 -> Honor
10 -> G1-Win
11 -> G1-Win
12 -> G1-Win
13 -> G1-Win
14 -> G1-Win
15 -> G1-Win
16 -> G1-Win
17 -> G1-Win
18 -> G1-Win
19 -> G1-Win
20 -> G1-Win
21 -> G1-Win
22 -> G1-Win
23 -> G1-Win
24 -> G1-Win
25 -> G1-Win
26 -> G1-Win
27 -> G1-Win
28 -> G1-Win
29 -> G1-Win
30 -> G1-Win
31 -> G1-Win
32 -> G1-Win
33 -> G1-Win
34 -> G1-Win
35 -> G1-Win
36 -> G1-Win
37 -> G1-Win
38 -> G1-Win
39 -> G1-Win
40 -> G2-Win
41 -> G2-Win
42 -> G2-Win
43 -> G2-Win
44 -> G2-Win
45 -> G2-Win
46 -> G2-Win
47 -> G2-Win
48 -> G2-Win
49 -> G2-Win
50 -> G2-Win
51 -> G2-Win
52 -> G2-Win
53 -> G2-Win
54 -> G2-Win
55 -> G2-Win
56 -> G2-Win
57 -> G2-Win
58 -> G2-Win
59 -> G2-Win
60 -> G2-Win
61 -> G2-Win
62 -> G2-Win
63 -> G2-Win
64 -> G2-Win
65 -> G2-Win
66 -> G2-Win
67 -> G2-Win
68 -> G2-Win
69 -> G2-Win
70 -> G2-Win
71 -> G2-Win
72 -> G2-Win
73 -> G2-Win
74 -> G3-Win
75 -> G3-Win
76 -> G3-Win
77 -> G3-Win
78 -> G3-Win
79 -> G3-Win
80 -> G3-Win
81 -> G3-Win
82 -> G3-Win
83 -> G3-Win
84 -> G3-Win
85 -> G3-Win
86 -> G3-Win
87 -> G3-Win
88 -> G3-Win
89 -> G3-Win
90 -> G3-Win
91 -> G2-Win
92 -> G3-Win
93 -> G3-Win
94 -> G3-Win
95 -> G3-Win
96 -> G3-Win
97 -> G3-Win
98 -> G3-Win
99 -> G3-Win
100 -> G3-Win
101 -> G3-Win
102 -> G3-Win
103 -> G3-Win
104 -> G3-Win
105 -> G3-Win
106 -> G3-Win
107 -> G3-Win
108 -> G3-Win
109 -> G3-Win
110 -> G3-Win
111 -> G3-Win
112 -> G3-Win
113 -> G3-Win
114 -> G3-Win
115 -> G3-Win
116 -> G3-Win
117 -> G3-Win
118 -> G3-Win
119 -> G3-Win
120 -> G3-Win
121 -> G3-Win
122 -> G3-Win
123 -> G3-Win
124 -> G3-Win
125 -> G3-Win
126 -> G3-Win
127 -> G3-Win
128 -> G3-Win
129 -> G3-Win
130 -> G3-Win
131 -> G2-Win
132 -> G3-Win
133 -> G3-Win
134 -> G3-Win
135 -> G3-Win
136 -> G3-Win
137 -> G3-Win
138 -> G3-Win
139 -> G3-Win
140 -> G3-Win
141 -> G3-Win
142 -> G3-Win
143 -> G3-Win
144 -> Honor
145 -> Honor
146 -> Honor
147 -> G1-Win
148 -> G1-Win
149 -> G2-Win
150 -> G3-Win
151 -> Honor
152 -> Honor
153 -> G1-Win
154 -> Honor
155 -> G1-Win
_ -> Honor
// Get the primary ID for a saddle.
// For saddles which are the primary version, or if no saddle matches the given ID,
// the result is the input.
pub fun primary(s: saddle-id): saddle-id
match s.game-id
144 -> Saddle-id(1)
145 -> Saddle-id(4)
146 -> Saddle-id(6)
147 -> Saddle-id(14)
148 -> Saddle-id(16)
149 -> Saddle-id(49)
151 -> Saddle-id(4)
152 -> Saddle-id(5)
153 -> Saddle-id(13)
154 -> Saddle-id(1)
155 -> Saddle-id(18)
_ -> s

View File

@@ -1,23 +0,0 @@
package global
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
ScenarioURAFinale ScenarioID = 1 // URA Finale
ScenarioUnityCup ScenarioID = 2 // Unity Cup
)
var AllScenarios = map[ScenarioID]Scenario{
ScenarioURAFinale: {
ID: 1,
Name: "URA Finale",
Title: "The Beginning: URA Finale",
},
ScenarioUnityCup: {
ID: 2,
Name: "Unity Cup",
Title: "Unity Cup: Shine On, Team Spirit!",
},
}

View File

@@ -1,30 +0,0 @@
module horse/global/scenario
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import horse/game-id
extern create-id-table(): vector<int>
c inline "int32_t arr[] = {1,2,};\nkk_vector_from_cint32array(arr, (kk_ssize_t)2, kk_context())"
js inline "[1,2,]"
// Vector of all scenario IDs in order for easy iterating.
pub val all = once(create-id-table)
// Get the name for a scenario.
// If no scenario matches the ID, the result contains the numeric ID.
pub fun show(s: scenario-id): string
match s.game-id
1 -> "URA Finale"
2 -> "Unity Cup"
x -> "scenario " ++ x.show
// Get the full title for a scenario, e.g. "The Beginning: URA Finale".
// If no scenario matches the ID, the result contains the numeric ID.
pub fun title(s: scenario-id): string
match s.game-id
1 -> "The Beginning: URA Finale"
2 -> "Unity Cup: Shine On, Team Spirit!"
x -> "scenario " ++ x.show

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
// Code generated by "stringer -type LobbyConversationLocationID -trimprefix Lobby -linecomment"; DO NOT EDIT.
package horse
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[LobbyRightFront1-110]
_ = x[LobbyRightFront2-120]
_ = x[LobbyRightFront3-130]
_ = x[LobbyLeftTable1-210]
_ = x[LobbyLeftTable2-220]
_ = x[LobbyBackSeat-310]
_ = x[LobbyPosters1-410]
_ = x[LobbyPosters2-420]
_ = x[LobbyPosters3-430]
_ = x[LobbySchoolMap1-510]
_ = x[LobbySchoolMap2-520]
_ = x[LobbySchoolMap3-530]
}
const _LobbyConversationLocationID_name = "right side frontright side frontright side frontleft side tableleft side tablecenter back seatcenter posterscenter posterscenter postersleft side school mapleft side school mapleft side school map"
var _LobbyConversationLocationID_map = map[LobbyConversationLocationID]string{
110: _LobbyConversationLocationID_name[0:16],
120: _LobbyConversationLocationID_name[16:32],
130: _LobbyConversationLocationID_name[32:48],
210: _LobbyConversationLocationID_name[48:63],
220: _LobbyConversationLocationID_name[63:78],
310: _LobbyConversationLocationID_name[78:94],
410: _LobbyConversationLocationID_name[94:108],
420: _LobbyConversationLocationID_name[108:122],
430: _LobbyConversationLocationID_name[122:136],
510: _LobbyConversationLocationID_name[136:156],
520: _LobbyConversationLocationID_name[156:176],
530: _LobbyConversationLocationID_name[176:196],
}
func (i LobbyConversationLocationID) String() string {
if str, ok := _LobbyConversationLocationID_map[i]; ok {
return str
}
return "LobbyConversationLocationID(" + strconv.FormatInt(int64(i), 10) + ")"
}

View File

@@ -4,26 +4,26 @@ type RaceID int32
// Race is the internal data about a race. // Race is the internal data about a race.
type Race struct { type Race struct {
ID RaceID ID RaceID `json:"race_id"`
Name string Name string `json:"name"`
Thumbnail int Thumbnail int `json:"thumbnail"`
// Some careers contain unusual versions of races, e.g. Tenno Sho (Spring) // Some careers contain unusual versions of races, e.g. Tenno Sho (Spring)
// in Hanshin instead of Kyoto for Narita Taishin and Biwa Hayahide. // in Hanshin instead of Kyoto for Narita Taishin and Biwa Hayahide.
// For such races, this field holds the normal race ID. // For such races, this field holds the normal race ID.
Primary RaceID Primary RaceID `json:"primary"`
} }
type SaddleID int32 type SaddleID int32
// Saddle is the internal data about a race win saddle. // Saddle is the internal data about a race win saddle.
type Saddle struct { type Saddle struct {
ID SaddleID ID SaddleID `json:"saddle_id"`
Name string Name string `json:"name"`
Races []RaceID Races []RaceID `json:"races"`
Type SaddleType Type SaddleType `json:"type"`
// Saddles that involve alternate races are themselves alternate. // Saddles that involve alternate races are themselves alternate.
// For such saddles, this field holds the normal saddle ID. // For such saddles, this field holds the normal saddle ID.
Primary SaddleID Primary SaddleID `json:"primary"`
} }
type SaddleType int8 type SaddleType int8
@@ -40,7 +40,7 @@ type ScenarioID int8
// Scenario is metadata about a career scenario. // Scenario is metadata about a career scenario.
type Scenario struct { type Scenario struct {
ID ScenarioID ID ScenarioID `json:"scenario_id"`
Name string Name string `json:"name"`
Title string Title string `json:"title"`
} }

View File

@@ -27,37 +27,37 @@ func (x TenThousandths) String() string {
// Skill is the internal data about a skill. // Skill is the internal data about a skill.
type Skill struct { type Skill struct {
ID SkillID ID SkillID `json:"skill_id"`
Name string Name string `json:"name"`
Description string Description string `json:"description"`
Group int32 Group SkillGroupID `json:"group"`
Rarity int8 Rarity int8 `json:"rarity"`
GroupRate int8 GroupRate int8 `json:"group_rate"`
GradeValue int32 GradeValue int32 `json:"grade_value,omitzero"`
WitCheck bool WitCheck bool `json:"wit_check"`
Activations []Activation Activations []Activation `json:"activations"`
UniqueOwner string UniqueOwner string `json:"unique_owner,omitzero"`
SPCost int SPCost int `json:"sp_cost,omitzero"`
IconID int IconID int `json:"icon_id"`
} }
// Activation is the parameters controlling when a skill activates. // Activation is the parameters controlling when a skill activates.
type Activation struct { type Activation struct {
Precondition string Precondition string `json:"precondition,omitzero"`
Condition string Condition string `json:"condition"`
Duration TenThousandths Duration TenThousandths `json:"duration,omitzero"`
DurScale DurScale DurScale DurScale `json:"dur_scale"`
Cooldown TenThousandths Cooldown TenThousandths `json:"cooldown,omitzero"`
Abilities []Ability Abilities []Ability `json:"abilities"`
} }
// Ability is an individual effect applied by a skill. // Ability is an individual effect applied by a skill.
type Ability struct { type Ability struct {
Type AbilityType Type AbilityType `json:"type"`
ValueUsage AbilityValueUsage ValueUsage AbilityValueUsage `json:"value_usage"`
Value TenThousandths Value TenThousandths `json:"value"`
Target AbilityTarget Target AbilityTarget `json:"target"`
TargetValue int32 TargetValue int32 `json:"target_value,omitzero"`
} }
func (a Ability) String() string { func (a Ability) String() string {
@@ -202,3 +202,29 @@ const (
TargetCharacter AbilityTarget = 22 // specific character TargetCharacter AbilityTarget = 22 // specific character
TargetTriggering AbilityTarget = 23 // whosoever triggered this skill TargetTriggering AbilityTarget = 23 // whosoever triggered this skill
) )
type SkillGroupID int32
// SkillGroup is a group of skills which are alternate versions of each other.
//
// Any of the skill IDs in a group may be zero, indicating that there is no
// skill with the corresponding group rate.
// Some skill groups contain only Skill2 or SkillBad, while others may have all
// four skills.
//
// As a special case, horsegen lists both unique skills and their inherited
// versions in the skill groups for both.
type SkillGroup struct {
ID SkillGroupID `json:"skill_group"`
// Skill1 is the base version of the skill, either a common (white) skill
// or an Uma's own unique.
Skill1 SkillID `json:"skill1,omitzero"`
// Skill2 is the first upgraded version of the skill: a rare (gold)
// skill, a double circle skill, or an inherited unique skill.
Skill2 SkillID `json:"skill2,omitzero"`
// Skill3 is the highest upgraded version, a gold version of a skill with
// a double circle version.
Skill3 SkillID `json:"skill3,omitzero"`
// SkillBad is a negative (purple) skill.
SkillBad SkillID `json:"skill_bad,omitzero"`
}

View File

@@ -1,38 +1,11 @@
package horse_test package horse_test
import ( import (
"cmp"
"slices"
"strings"
"sync"
"testing" "testing"
"git.sunturtle.xyz/zephyr/horse/horse" "git.sunturtle.xyz/zephyr/horse/horse"
"git.sunturtle.xyz/zephyr/horse/horse/global"
) )
var SortedSkills = sync.OnceValue(func() []horse.Skill {
skills := make([]horse.Skill, 0, len(global.AllSkills))
for _, v := range global.AllSkills {
skills = append(skills, v)
}
slices.SortFunc(skills, func(a, b horse.Skill) int { return cmp.Compare(a.ID, b.ID) })
return skills
})
func TestSkillStrings(t *testing.T) {
t.Parallel()
for _, s := range SortedSkills() {
for _, a := range s.Activations {
for _, abil := range a.Abilities {
if n := abil.Type.String(); strings.HasPrefix(n, "AbilityType(") {
t.Errorf("%v %s: %s", s.ID, s.Name, n)
}
}
}
}
}
func TestTenThousandthsString(t *testing.T) { func TestTenThousandthsString(t *testing.T) {
t.Parallel() t.Parallel()
cases := []struct { cases := []struct {

View File

@@ -6,13 +6,13 @@ type (
) )
type Spark struct { type Spark struct {
ID SparkID ID SparkID `json:"spark_id"`
Name string Name string `json:"name"`
Description string Description string `json:"description"`
Group SparkGroupID Group SparkGroupID `json:"spark_group"`
Rarity SparkRarity Rarity SparkRarity `json:"rarity"`
Type SparkType Type SparkType `json:"type"`
Effects [][]SparkEffect Effects [][]SparkEffect `json:"effects"`
} }
type SparkType int8 type SparkType int8
@@ -46,9 +46,9 @@ func (r SparkRarity) String() string {
} }
type SparkEffect struct { type SparkEffect struct {
Target SparkTarget Target SparkTarget `json:"target"`
Value1 int32 Value1 int32 `json:"value1,omitzero"`
Value2 int32 Value2 int32 `json:"value2,omitzero"`
} }
type SparkTarget int8 type SparkTarget int8

View File

@@ -3,18 +3,30 @@ package horse
type UmaID int32 type UmaID int32
type Uma struct { type Uma struct {
ID UmaID ID UmaID `json:"chara_card_id"`
CharacterID CharacterID CharacterID CharacterID `json:"chara_id"`
Name string Name string `json:"name"`
Variant string Variant string `json:"variant"`
Sprint, Mile, Medium, Long AptitudeLevel Sprint AptitudeLevel `json:"sprint"`
Front, Pace, Late, End AptitudeLevel Mile AptitudeLevel `json:"mile"`
Turf, Dirt AptitudeLevel Medium AptitudeLevel `json:"medium"`
Long AptitudeLevel `json:"long"`
Front AptitudeLevel `json:"front"`
Pace AptitudeLevel `json:"pace"`
Late AptitudeLevel `json:"late"`
End AptitudeLevel `json:"end"`
Turf AptitudeLevel `json:"turf"`
Dirt AptitudeLevel `json:"dirt"`
Unique SkillID Unique SkillID `json:"unique"`
Skill1, Skill2, Skill3 SkillID Skill1 SkillID `json:"skill1"`
SkillPL2, SkillPL3, SkillPL4, SkillPL5 SkillID Skill2 SkillID `json:"skill2"`
Skill3 SkillID `json:"skill3"`
SkillPL2 SkillID `json:"skill_pl2"`
SkillPL3 SkillID `json:"skill_pl3"`
SkillPL4 SkillID `json:"skill_pl4"`
SkillPL5 SkillID `json:"skill_pl5"`
} }
type AptitudeLevel int8 type AptitudeLevel int8

View File

@@ -1,6 +0,0 @@
# gen
Go tool to generate the Koka source code from the game's SQLite database.
Code is generated using Go templates.
Templates use a `kkenum` function which converts a name `Mr. C.B.` to a Koka enumerant name `Mr-CB`.

View File

@@ -1,44 +0,0 @@
WITH uma_names AS (
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 6 AND "index" BETWEEN 1000 AND 1999
-- Exclude characters who have no succession relations defined.
AND "index" IN (SELECT chara_id FROM succession_relation_member)
), pairs AS (
SELECT
a.id AS id_a,
a.name AS name_a,
b.id AS id_b,
b.name AS name_b
FROM uma_names a
JOIN uma_names b ON a.id != b.id -- exclude reflexive cases
), relation_pairs AS (
SELECT
ra.relation_type,
ra.chara_id AS id_a,
rb.chara_id AS id_b
FROM succession_relation_member ra
JOIN succession_relation_member rb ON ra.relation_type = rb.relation_type
), affinity AS (
SELECT
pairs.*,
SUM(IFNULL(relation_point, 0)) AS base_affinity
FROM pairs
LEFT JOIN relation_pairs rp ON pairs.id_a = rp.id_a AND pairs.id_b = rp.id_b
LEFT JOIN succession_relation sr ON rp.relation_type = sr.relation_type
GROUP BY pairs.id_a, pairs.id_b
UNION ALL
-- Reflexive cases.
SELECT
uma_names.id AS id_a,
uma_names.name AS name_a,
uma_names.id AS id_b,
uma_names.name AS name_b,
0 AS base_affinity
FROM uma_names
)
SELECT * FROM affinity
ORDER BY id_a, id_b

View File

@@ -1,87 +0,0 @@
WITH uma_names AS (
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 6 AND "index" BETWEEN 1000 AND 1999
-- Exclude characters who have no succession relations defined.
AND "index" IN (SELECT chara_id FROM succession_relation_member)
), trios AS (
SELECT
a.id AS id_a,
a.name AS name_a,
b.id AS id_b,
b.name AS name_b,
c.id AS id_c,
c.name AS name_c
FROM uma_names a
JOIN uma_names b ON a.id != b.id -- exclude pairwise reflexive cases
JOIN uma_names c ON a.id != c.id AND b.id != c.id
), relation_trios AS (
SELECT
ra.relation_type,
ra.chara_id AS id_a,
rb.chara_id AS id_b,
rc.chara_id AS id_c
FROM succession_relation_member ra
JOIN succession_relation_member rb ON ra.relation_type = rb.relation_type
JOIN succession_relation_member rc ON ra.relation_type = rc.relation_type
), affinity AS (
SELECT
trios.*,
SUM(IFNULL(relation_point, 0)) AS base_affinity
FROM trios
LEFT JOIN relation_trios rt ON trios.id_a = rt.id_a AND trios.id_b = rt.id_b AND trios.id_c = rt.id_c
LEFT JOIN succession_relation sr ON rt.relation_type = sr.relation_type
GROUP BY trios.id_a, trios.id_b, trios.id_c
UNION ALL
-- A = B = C
SELECT
n.id AS id_a,
n.name AS name_a,
n.id AS id_b,
n.name AS name_b,
n.id AS id_c,
n.name AS name_c,
0 AS base_affinity
FROM uma_names n
UNION ALL
-- A = B
SELECT
n.id AS id_a,
n.name AS name_a,
n.id AS id_a,
n.name AS id_b,
m.id AS id_c,
m.name AS name_c,
0 AS base_affinity
FROM uma_names n JOIN uma_names m ON n.id != m.id
UNION ALL
-- A = C
SELECT
n.id AS id_a,
n.name AS name_a,
m.id AS id_a,
m.name AS id_b,
n.id AS id_c,
n.name AS name_c,
0 AS base_affinity
FROM uma_names n JOIN uma_names m ON n.id != m.id
UNION ALL
-- B = C
SELECT
m.id AS id_a,
m.name AS name_a,
n.id AS id_a,
n.name AS id_b,
n.id AS id_c,
n.name AS name_c,
0 AS base_affinity
FROM uma_names n JOIN uma_names m ON n.id != m.id
)
SELECT * FROM affinity
ORDER BY id_a, id_b, id_c

View File

@@ -1,72 +0,0 @@
{{ define "go-character" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $c := $.Characters }}
Character{{ goenum $c.Name }} CharacterID = {{ $c.ID }} // {{ $c.Name }}
{{- end }}
)
var OrderedCharacters = [...]CharacterID{
{{- range $c := $.Characters }}
Character{{ goenum $c.Name }},
{{- end }}
}
var Characters = map[CharacterID]Character{
{{- range $c := $.Characters }}
Character{{ goenum $c.Name }}: {ID: {{ $c.ID }}, Name: {{ printf "%q" $c.Name -}} },
{{- end }}
}
var CharacterNameToID = map[string]CharacterID{
{{- range $c := $.Characters }}
{{ printf "%q" $c.Name }}: {{ $c.ID }},
{{- end }}
}
var pairAffinity = []int8{
{{- range $a := $.Characters -}}
{{- range $b := $.Characters -}}
{{- index $.PairMaps $a.ID $b.ID -}},
{{- end -}}
{{- end -}}
}
var trioAffinity = []int8{
{{- range $a := $.Characters -}}
{{- range $b := $.Characters -}}
{{- range $c := $.Characters -}}
{{- index $.TrioMaps $a.ID $b.ID $c.ID -}},
{{- end -}}
{{- end -}}
{{- end -}}
}
func PairAffinity(a, b CharacterID) int {
if _, ok := Characters[a]; !ok {
return 0
}
if _, ok := Characters[b]; !ok {
return 0
}
return int(pairAffinity[a*{{ $.CharaCount }} + b])
}
func TrioAffinity(a, b, c CharacterID) int {
if _, ok := Characters[a]; !ok {
return 0
}
if _, ok := Characters[b]; !ok {
return 0
}
if _, ok := Characters[c]; !ok {
return 0
}
return int(trioAffinity[a*{{ $.CharaCount }}*{{ $.CharaCount }} + b*{{ $.CharaCount }} + c])
}
{{ end }}

View File

@@ -1,94 +0,0 @@
{{ define "koka-character" -}}
module horse/{{ $.Region }}/character
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import std/data/rb-map
import horse/game-id
pub import horse/character
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $chara := $.Characters }}{{ $chara.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.CharaCount }}, kk_context())"
js inline "[{{ range $chara := $.Characters }}{{ $chara.ID }},{{ end }}]"
// Vector of all character ID values in order for easy iterating.
pub val all = once(create-id-table)
val name2id = once()
var m: rbmap<string, int> := empty()
all().foreach() fn(id) m := m.set(Character-id(id).show, id)
m
// Get the character ID that has the given exact name.
// If no character matches the name, the result is an invalid ID.
pub fun from-name(name: string): character-id
Character-id(name2id().rb-map/lookup(name).default(0))
// Get the name for a character.
// If no character matches the ID, the result is the numeric ID.
pub fun show(c: character-id): string
match c.game-id
{{- range $chara := $.Characters }}
{{ $chara.ID }} -> {{ printf "%q" $chara.Name }}
{{- end }}
x -> "character " ++ x.show
fun character/index(c: character-id): int
match c.game-id
{{- range $chara := $.Characters }}
{{ $chara.ID }} -> {{ $chara.Index }}
{{- end }}
_ -> -99999999
// Create the table of all pair affinities.
// The affinity is the value at a.index*count + b.index.
extern global/create-pair-table(): vector<int>
c inline "int32_t arr[] = {
{{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- index $.PairMaps $a.ID $b.ID }},
{{- end }}
{{- end -}}
};\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.CharaCount }} * (kk_ssize_t){{ $.CharaCount }}, kk_context())"
js inline "[
{{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- index $.PairMaps $a.ID $b.ID }},
{{- end }}
{{- end -}}
]"
val global/pair-table = global/create-pair-table()
// Base affinity between a pair using the global ruleset.
pub fun global/pair-affinity(a: character-id, b: character-id): int
global/pair-table.at(a.index * {{ $.CharaCount }} + b.index).default(0)
// Create the table of all trio affinities.
// The affinity is the value at a.index*count*count + b.index*count + c.index.
extern global/create-trio-table(): vector<int>
c inline "int32_t arr[] = {
{{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- range $c := $.Characters }}
{{- index $.TrioMaps $a.ID $b.ID $c.ID }},
{{- end }}
{{- end }}
{{- end -}}
};\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.CharaCount }} * (kk_ssize_t){{ $.CharaCount }} * (kk_ssize_t){{ $.CharaCount }}, kk_context())"
js inline "[
{{- range $a := $.Characters }}
{{- range $b := $.Characters }}
{{- range $c := $.Characters }}
{{- index $.TrioMaps $a.ID $b.ID $c.ID }},
{{- end }}
{{- end }}
{{- end -}}
]"
val global/trio-table = global/create-trio-table()
// Base affinity for a trio using the global ruleset.
pub fun global/trio-affinity(a: character-id, b: character-id, c: character-id): int
global/trio-table.at(a.index * {{ $.CharaCount }} * {{ $.CharaCount }} + b.index * {{ $.CharaCount }} + c.index).default(0)
{{ end }}

View File

@@ -1,263 +0,0 @@
package main
import (
"embed"
"errors"
"fmt"
"io"
"regexp"
"strings"
"text/template"
"unicode"
)
//go:embed *.template
var templates embed.FS
// LoadTemplates sets up templates to render game data to source code.
func LoadTemplates() (*template.Template, error) {
t := template.New("root")
t.Funcs(template.FuncMap{
"kkenum": kkenum,
"goenum": goenum,
})
return t.ParseFS(templates, "*")
}
// ExecCharacter renders the Koka character module to kk and the Go character file to g.
// If either is nil, it is skipped.
func ExecCharacter(t *template.Template, region string, kk, g io.Writer, c []NamedID[Character], pairs, trios []AffinityRelation) error {
if len(pairs) != len(c)*len(c) {
return fmt.Errorf("there are %d pairs but there must be %d for %d characters", len(pairs), len(c)*len(c), len(c))
}
if len(trios) != len(c)*len(c)*len(c) {
return fmt.Errorf("there are %d trios but there must be %d for %d characters", len(trios), len(c)*len(c)*len(c), len(c))
}
maxid := 0
pm := make(map[int]map[int]int, len(c))
tm := make(map[int]map[int]map[int]int, len(c))
for _, u := range c {
maxid = max(maxid, u.ID)
pm[u.ID] = make(map[int]int, len(c))
tm[u.ID] = make(map[int]map[int]int, len(c))
for _, v := range c {
tm[u.ID][v.ID] = make(map[int]int, len(c))
}
}
for _, p := range pairs {
pm[p.IDA][p.IDB] = p.Affinity
}
for _, t := range trios {
tm[t.IDA][t.IDB][t.IDC] = t.Affinity
}
data := struct {
Region string
Characters []NamedID[Character]
Pairs []AffinityRelation
Trios []AffinityRelation
PairMaps map[int]map[int]int
TrioMaps map[int]map[int]map[int]int
CharaCount int
MaxID int
}{region, c, pairs, trios, pm, tm, len(c), maxid}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-character", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-character", &data))
}
return err
}
func ExecSkill(t *template.Template, region string, kk, g io.Writer, groups []NamedID[SkillGroup], skills []Skill) error {
m := make(map[int][]Skill, len(groups))
for _, t := range skills {
m[t.GroupID] = append(m[t.GroupID], t)
}
data := struct {
Region string
Groups []NamedID[SkillGroup]
Skills []Skill
Related map[int][]Skill
SkillCount int
}{region, groups, skills, m, len(skills)}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-skill", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-skill-data", &data))
}
return err
}
func ExecRace(t *template.Template, region string, kk, g io.Writer, races []Race) error {
data := struct {
Region string
Races []Race
RaceCount int
}{region, races, len(races)}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-race", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-race", &data))
}
return err
}
func ExecSaddle(t *template.Template, region string, kk, g io.Writer, saddles []Saddle) error {
data := struct {
Region string
Saddles []Saddle
SaddleCount int
}{region, saddles, len(saddles)}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-saddle", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-saddle", &data))
}
return err
}
func ExecScenario(t *template.Template, region string, kk, g io.Writer, scen []Scenario) error {
data := struct {
Region string
Scenarios []Scenario
ScenarioCount int
}{region, scen, len(scen)}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-scenario", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-scenario", &data))
}
return err
}
func ExecSparks(t *template.Template, region string, kk, g io.Writer, sparks []Spark, effects map[int]map[int][]SparkEffect) error {
data := struct {
Region string
Sparks []Spark
SparkEffects map[int]map[int][]SparkEffect
SparkCount int
}{region, sparks, effects, len(sparks)}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-spark", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-spark", &data))
}
return err
}
func ExecUmas(t *template.Template, region string, kk, g io.Writer, umas []Uma) error {
data := struct {
Region string
Umas []Uma
UmaCount int
}{region, umas, len(umas)}
var err error
if kk != nil {
err = errors.Join(err, t.ExecuteTemplate(kk, "koka-uma", &data))
}
if g != nil {
err = errors.Join(err, t.ExecuteTemplate(g, "go-uma", &data))
}
return err
}
const wordSeps = " ,!?/-+();#○☆♡'=♪∀゚∴[]:"
var (
kkReplace = func() *strings.Replacer {
r := []string{
"Triple 7s", "Triple-Sevens", // hard to replace with the right thing automatically
"1,500,000 CC", "One-Million-CC",
"15,000,000 CC", "Fifteen-Million-CC",
"1st", "First",
"114th", "Hundred-Fourteenth",
"♡ 3D Nail Art", "Nail-Art",
".", "",
"\u2019", "",
"&", "-and-",
"'s", "s",
"ó", "o",
"∞", "Infinity",
"\u00d7", "x",
"◎", "Lv2",
}
for _, c := range wordSeps {
r = append(r, string(c), "-")
}
return strings.NewReplacer(r...)
}()
kkMultidash = regexp.MustCompile(`-+`)
kkDashNonletter = regexp.MustCompile(`-[^A-Za-z]`)
goReplace = func() *strings.Replacer {
r := []string{
"Triple 7s", "TripleSevens",
"1,500,000 CC", "OneMillionCC",
"15,000,000 CC", "FifteenMillionCC",
"1st", "First",
"♡ 3D Nail Art", "NailArt",
".", "",
"\u2019", "",
"&", "And",
"'s", "s",
"∞", "Infinity",
"\u00d7", "X",
"◎", "Lv2",
}
for _, c := range wordSeps {
r = append(r, string(c), "")
}
return strings.NewReplacer(r...)
}()
)
func kkenum(name string) string {
orig := name
name = kkReplace.Replace(name)
name = kkMultidash.ReplaceAllLiteralString(name, "-")
name = strings.Trim(name, "-")
if len(name) == 0 {
panic(fmt.Errorf("%q became empty as Koka enum variant", orig))
}
name = strings.ToUpper(name[:1]) + name[1:]
if !unicode.IsLetter(rune(name[0])) {
//lint:ignore ST1005 proper name
panic(fmt.Errorf("Koka enum variant %q (from %q) starts with a non-letter", name, orig))
}
for _, c := range name {
if c > 127 {
// Koka does not allow non-ASCII characters in source code.
// Don't proceed if we've missed one.
panic(fmt.Errorf("non-ASCII character %q (%[1]U) in Koka enum variant %q (from %q)", c, name, orig))
}
}
if kkDashNonletter.MatchString(name) {
panic(fmt.Errorf("non-letter character after a dash in Koka enum variant %q (from %q)", name, orig))
}
return name
}
func goenum(name string) string {
// go names are a bit more lax, so we need fewer checks
orig := name
name = goReplace.Replace(name)
if len(name) == 0 {
panic(fmt.Errorf("%q became empty as Go enum variant", orig))
}
name = strings.ToUpper(name[:1]) + name[1:]
return name
}

View File

@@ -1,613 +0,0 @@
package main
import (
"context"
_ "embed"
"fmt"
"zombiezen.com/go/sqlite/sqlitex"
)
//go:embed character.sql
var characterSQL string
//go:embed character.affinity2.sql
var characterAffinity2SQL string
//go:embed character.affinity3.sql
var characterAffinity3SQL string
//go:embed uma.sql
var umaSQL string
//go:embed skill-group.sql
var skillGroupSQL string
//go:embed skill.sql
var skillSQL string
//go:embed race.sql
var raceSQL string
//go:embed saddle.sql
var saddleSQL string
//go:embed scenario.sql
var scenarioSQL string
//go:embed spark.sql
var sparkSQL string
//go:embed spark-effect.sql
var sparkEffectSQL string
type (
Character struct{}
SkillGroup struct{}
)
type NamedID[T any] struct {
// Disallow conversions between NamedID types.
_ [0]*T
ID int
Name string
// For internal use, the index of the identity, when it's needed.
// We don't show this in public API, but it lets us use vectors for lookups.
Index int
}
func Characters(ctx context.Context, db *sqlitex.Pool) ([]NamedID[Character], error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for characters: %w", err)
}
stmt, _, err := conn.PrepareTransient(characterSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for characters: %w", err)
}
defer stmt.Finalize()
var r []NamedID[Character]
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping characters: %w", err)
}
if !ok {
break
}
c := NamedID[Character]{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Index: stmt.ColumnInt(2),
}
r = append(r, c)
}
return r, nil
}
type AffinityRelation struct {
IDA int
NameA string
IDB int
NameB string
IDC int
NameC string
Affinity int
}
func CharacterPairs(ctx context.Context, db *sqlitex.Pool) ([]AffinityRelation, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for character pairs: %w", err)
}
stmt, _, err := conn.PrepareTransient(characterAffinity2SQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for character pairs: %w", err)
}
defer stmt.Finalize()
var r []AffinityRelation
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping character pairs: %w", err)
}
if !ok {
break
}
p := AffinityRelation{
IDA: stmt.ColumnInt(0),
NameA: stmt.ColumnText(1),
IDB: stmt.ColumnInt(2),
NameB: stmt.ColumnText(3),
Affinity: stmt.ColumnInt(4),
}
r = append(r, p)
}
return r, nil
}
func CharacterTrios(ctx context.Context, db *sqlitex.Pool) ([]AffinityRelation, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for character trios: %w", err)
}
stmt, _, err := conn.PrepareTransient(characterAffinity3SQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for character trios: %w", err)
}
defer stmt.Finalize()
var r []AffinityRelation
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping character trios: %w", err)
}
if !ok {
break
}
p := AffinityRelation{
IDA: stmt.ColumnInt(0),
NameA: stmt.ColumnText(1),
IDB: stmt.ColumnInt(2),
NameB: stmt.ColumnText(3),
IDC: stmt.ColumnInt(4),
NameC: stmt.ColumnText(5),
Affinity: stmt.ColumnInt(6),
}
r = append(r, p)
}
return r, nil
}
func SkillGroups(ctx context.Context, db *sqlitex.Pool) ([]NamedID[SkillGroup], error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for skill groups: %w", err)
}
stmt, _, err := conn.PrepareTransient(skillGroupSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for skill groups: %w", err)
}
defer stmt.Finalize()
var r []NamedID[SkillGroup]
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping skill groups: %w", err)
}
if !ok {
break
}
g := NamedID[SkillGroup]{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
}
r = append(r, g)
}
return r, nil
}
type Skill struct {
ID int
Name string
Description string
GroupID int
GroupName string
Rarity int
GroupRate int
GradeValue int
WitCheck bool
Activations [2]SkillActivation
SPCost int
InheritID int
UniqueOwnerID int
UniqueOwner string
IconID int
Index int
}
type SkillActivation struct {
Precondition string
Condition string
Duration int
DurScale int
Cooldown int
Abilities [3]SkillAbility
}
type SkillAbility struct {
Type int
ValueUsage int
Value int
Target int
TargetValue int
}
func Skills(ctx context.Context, db *sqlitex.Pool) ([]Skill, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for skills: %w", err)
}
stmt, _, err := conn.PrepareTransient(skillSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for skills: %w", err)
}
defer stmt.Finalize()
var r []Skill
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping skills: %w", err)
}
if !ok {
break
}
s := Skill{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Description: stmt.ColumnText(2),
GroupID: stmt.ColumnInt(3),
GroupName: stmt.ColumnText(4),
Rarity: stmt.ColumnInt(5),
GroupRate: stmt.ColumnInt(6),
GradeValue: stmt.ColumnInt(7),
WitCheck: stmt.ColumnInt(8) != 0,
Activations: [2]SkillActivation{
{
Precondition: stmt.ColumnText(9),
Condition: stmt.ColumnText(10),
Duration: stmt.ColumnInt(11),
DurScale: stmt.ColumnInt(12),
Cooldown: stmt.ColumnInt(13),
Abilities: [3]SkillAbility{
{
Type: stmt.ColumnInt(14),
ValueUsage: stmt.ColumnInt(15),
Value: stmt.ColumnInt(16),
Target: stmt.ColumnInt(17),
TargetValue: stmt.ColumnInt(18),
},
{
Type: stmt.ColumnInt(19),
ValueUsage: stmt.ColumnInt(20),
Value: stmt.ColumnInt(21),
Target: stmt.ColumnInt(22),
TargetValue: stmt.ColumnInt(23),
},
{
Type: stmt.ColumnInt(24),
ValueUsage: stmt.ColumnInt(25),
Value: stmt.ColumnInt(26),
Target: stmt.ColumnInt(27),
TargetValue: stmt.ColumnInt(28),
},
},
},
{
Precondition: stmt.ColumnText(29),
Condition: stmt.ColumnText(30),
Duration: stmt.ColumnInt(31),
DurScale: stmt.ColumnInt(32),
Cooldown: stmt.ColumnInt(33),
Abilities: [3]SkillAbility{
{
Type: stmt.ColumnInt(34),
ValueUsage: stmt.ColumnInt(35),
Value: stmt.ColumnInt(36),
Target: stmt.ColumnInt(37),
TargetValue: stmt.ColumnInt(38),
},
{
Type: stmt.ColumnInt(39),
ValueUsage: stmt.ColumnInt(40),
Value: stmt.ColumnInt(41),
Target: stmt.ColumnInt(42),
TargetValue: stmt.ColumnInt(43),
},
{
Type: stmt.ColumnInt(44),
ValueUsage: stmt.ColumnInt(45),
Value: stmt.ColumnInt(46),
Target: stmt.ColumnInt(47),
TargetValue: stmt.ColumnInt(48),
},
},
},
},
SPCost: stmt.ColumnInt(49),
InheritID: stmt.ColumnInt(50),
UniqueOwnerID: stmt.ColumnInt(51),
UniqueOwner: stmt.ColumnText(52),
IconID: stmt.ColumnInt(53),
Index: stmt.ColumnInt(54),
}
r = append(r, s)
}
return r, nil
}
type Race struct {
ID int
Name string
Grade int
ThumbnailID int
Primary int
Alternate int
}
func Races(ctx context.Context, db *sqlitex.Pool) ([]Race, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for races: %w", err)
}
stmt, _, err := conn.PrepareTransient(raceSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for races: %w", err)
}
defer stmt.Finalize()
var r []Race
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping races: %w", err)
}
if !ok {
break
}
race := Race{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Grade: stmt.ColumnInt(2),
ThumbnailID: stmt.ColumnInt(3),
Primary: stmt.ColumnInt(4),
Alternate: stmt.ColumnInt(5),
}
r = append(r, race)
}
return r, nil
}
type Saddle struct {
ID int
Name string
Races [3]int
Type int
Primary int
Alternate int
}
func Saddles(ctx context.Context, db *sqlitex.Pool) ([]Saddle, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for saddles: %w", err)
}
stmt, _, err := conn.PrepareTransient(saddleSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for saddles: %w", err)
}
defer stmt.Finalize()
var r []Saddle
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping saddles: %w", err)
}
if !ok {
break
}
s := Saddle{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Races: [3]int{stmt.ColumnInt(2), stmt.ColumnInt(3), stmt.ColumnInt(4)},
Type: stmt.ColumnInt(5),
Primary: stmt.ColumnInt(6),
Alternate: stmt.ColumnInt(7),
}
r = append(r, s)
}
return r, nil
}
type Scenario struct {
ID int
Name string
Title string
}
func Scenarios(ctx context.Context, db *sqlitex.Pool) ([]Scenario, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for scenario: %w", err)
}
stmt, _, err := conn.PrepareTransient(scenarioSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for scenario: %w", err)
}
defer stmt.Finalize()
var r []Scenario
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping scenarios: %w", err)
}
if !ok {
break
}
s := Scenario{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Title: stmt.ColumnText(2),
}
r = append(r, s)
}
return r, nil
}
type Spark struct {
ID int
Name string
Description string
Group int
Rarity int
Type int
}
type SparkEffect struct {
Target int
Value1 int
Value2 int
}
func Sparks(ctx context.Context, db *sqlitex.Pool) ([]Spark, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for sparks: %w", err)
}
stmt, _, err := conn.PrepareTransient(sparkSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for sparks: %w", err)
}
defer stmt.Finalize()
var r []Spark
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping sparks: %w", err)
}
if !ok {
break
}
s := Spark{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Description: stmt.ColumnText(2),
Group: stmt.ColumnInt(3),
Rarity: stmt.ColumnInt(4),
Type: stmt.ColumnInt(5),
}
r = append(r, s)
}
return r, nil
}
func SparkEffects(ctx context.Context, db *sqlitex.Pool) (map[int]map[int][]SparkEffect, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for spark effects: %w", err)
}
stmt, _, err := conn.PrepareTransient(sparkEffectSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for spark effects: %w", err)
}
defer stmt.Finalize()
r := make(map[int]map[int][]SparkEffect)
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping spark effects: %w", err)
}
if !ok {
break
}
group := stmt.ColumnInt(0)
eff := stmt.ColumnInt(1)
s := SparkEffect{
Target: stmt.ColumnInt(2),
Value1: stmt.ColumnInt(3),
Value2: stmt.ColumnInt(4),
}
if r[group] == nil {
r[group] = make(map[int][]SparkEffect)
}
r[group][eff] = append(r[group][eff], s)
}
return r, nil
}
type Uma struct {
ID int
CharacterID int
Name string
Variant string
CharacterName string
Sprint, Mile, Medium, Long int
Front, Pace, Late, End int
Turf, Dirt int
UniqueID int
Skill1, Skill2, Skill3 int
SkillPL2, SkillPL3, SkillPL4, SkillPL5 int
}
func Umas(ctx context.Context, db *sqlitex.Pool) ([]Uma, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for umas: %w", err)
}
stmt, _, err := conn.PrepareTransient(umaSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for umas: %w", err)
}
defer stmt.Finalize()
var r []Uma
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping umas: %w", err)
}
if !ok {
break
}
uma := Uma{
ID: stmt.ColumnInt(0),
CharacterID: stmt.ColumnInt(1),
Name: stmt.ColumnText(2),
Variant: stmt.ColumnText(3),
CharacterName: stmt.ColumnText(4),
Sprint: stmt.ColumnInt(5),
Mile: stmt.ColumnInt(6),
Medium: stmt.ColumnInt(7),
Long: stmt.ColumnInt(8),
Front: stmt.ColumnInt(9),
Pace: stmt.ColumnInt(10),
Late: stmt.ColumnInt(11),
End: stmt.ColumnInt(12),
Turf: stmt.ColumnInt(13),
Dirt: stmt.ColumnInt(14),
UniqueID: stmt.ColumnInt(15),
Skill1: stmt.ColumnInt(16),
Skill2: stmt.ColumnInt(17),
Skill3: stmt.ColumnInt(18),
SkillPL2: stmt.ColumnInt(19),
SkillPL3: stmt.ColumnInt(20),
SkillPL4: stmt.ColumnInt(21),
SkillPL5: stmt.ColumnInt(22),
}
r = append(r, uma)
}
return r, nil
}

View File

@@ -1,227 +0,0 @@
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"path/filepath"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
func main() {
var (
mdb string
out string
region string
)
flag.StringVar(&mdb, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb")
flag.StringVar(&out, "o", `horse`, "`dir`ectory for output files")
flag.StringVar(&region, "region", "global", "region the database is for (global, jp)")
flag.Parse()
pctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-pctx.Done()
stop()
}()
t, err := LoadTemplates()
if err != nil {
slog.Error("loading templates", slog.Any("err", err))
os.Exit(2)
}
slog.Info("open", slog.String("mdb", mdb))
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil {
slog.Error("opening mdb", slog.String("mdb", mdb), slog.Any("err", err))
os.Exit(1)
}
eg, ctx := errgroup.WithContext(pctx)
var (
charas []NamedID[Character]
pairs []AffinityRelation
trios []AffinityRelation
sg []NamedID[SkillGroup]
skills []Skill
races []Race
saddles []Saddle
scens []Scenario
sparks []Spark
sparkeff map[int]map[int][]SparkEffect
umas []Uma
)
eg.Go(func() error {
slog.Info("get characters")
r, err := Characters(ctx, db)
charas = r
return err
})
eg.Go(func() error {
slog.Info("get pairs")
r, err := CharacterPairs(ctx, db)
pairs = r
return err
})
eg.Go(func() error {
slog.Info("get trios")
r, err := CharacterTrios(ctx, db)
trios = r
return err
})
eg.Go(func() error {
slog.Info("get skill groups")
r, err := SkillGroups(ctx, db)
sg = r
return err
})
eg.Go(func() error {
slog.Info("get skills")
r, err := Skills(ctx, db)
skills = r
return err
})
eg.Go(func() error {
slog.Info("get races")
r, err := Races(ctx, db)
races = r
return err
})
eg.Go(func() error {
slog.Info("get saddles")
r, err := Saddles(ctx, db)
saddles = r
return err
})
eg.Go(func() error {
slog.Info("get scenarios")
r, err := Scenarios(ctx, db)
scens = r
return err
})
eg.Go(func() error {
slog.Info("get sparks")
r, err := Sparks(ctx, db)
sparks = r
return err
})
eg.Go(func() error {
slog.Info("get spark effects")
r, err := SparkEffects(ctx, db)
sparkeff = r
return err
})
eg.Go(func() error {
slog.Info("get umas")
r, err := Umas(ctx, db)
umas = r
return err
})
if err := eg.Wait(); err != nil {
slog.Error("load", slog.Any("err", err))
os.Exit(1)
}
if err := os.MkdirAll(filepath.Join(out, region), 0775); err != nil {
slog.Error("create output dir", slog.Any("err", err))
os.Exit(1)
}
eg, ctx = errgroup.WithContext(pctx)
eg.Go(func() error {
cf, err := os.Create(filepath.Join(out, region, "character.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "character.go"))
if err != nil {
return err
}
slog.Info("write characters")
return ExecCharacter(t, region, cf, gf, charas, pairs, trios)
})
eg.Go(func() error {
sf, err := os.Create(filepath.Join(out, region, "skill.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "skill.go"))
if err != nil {
return err
}
slog.Info("write skills")
return ExecSkill(t, region, sf, gf, sg, skills)
})
eg.Go(func() error {
kf, err := os.Create(filepath.Join(out, region, "race.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "race.go"))
if err != nil {
return err
}
slog.Info("write races")
return ExecRace(t, region, kf, gf, races)
})
eg.Go(func() error {
kf, err := os.Create(filepath.Join(out, region, "saddle.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "saddle.go"))
if err != nil {
return err
}
slog.Info("write saddles")
return ExecSaddle(t, region, kf, gf, saddles)
})
eg.Go(func() error {
kf, err := os.Create(filepath.Join(out, region, "scenario.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "scenario.go"))
if err != nil {
return err
}
slog.Info("write scenarios")
return ExecScenario(t, region, kf, gf, scens)
})
eg.Go(func() error {
kf, err := os.Create(filepath.Join(out, region, "spark.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "spark.go"))
if err != nil {
return err
}
slog.Info("write sparks")
return ExecSparks(t, region, kf, gf, sparks, sparkeff)
})
eg.Go(func() error {
kf, err := os.Create(filepath.Join(out, region, "uma.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(out, region, "uma.go"))
if err != nil {
return err
}
slog.Info("write umas")
return ExecUmas(t, region, kf, gf, umas)
})
if err := eg.Wait(); err != nil {
slog.Error("generate", slog.Any("err", err))
os.Exit(1)
} else {
slog.Info("done")
}
}

View File

@@ -1,32 +0,0 @@
{{- define "go-race" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $r := $.Races }}
Race{{ goenum $r.Name }}{{ if $r.Alternate }}Alt{{ $r.ID }}{{ end }} RaceID = {{ $r.ID }} // {{ $r.Name }}
{{- end }}
)
var AllRaces = map[RaceID]Race{
{{- range $r := $.Races }}
Race{{ goenum $r.Name }}{{ if $r.Alternate }}Alt{{ $r.ID }}{{ end }}: {
ID: {{ $r.ID }},
Name: {{ printf "%q" $r.Name }}{{ if $r.Alternate }} + " (Alternate {{ $r.ID }})"{{ end }},
Thumbnail: {{ $r.ThumbnailID }},
{{- if ne $r.Primary $r.ID }}
Primary: {{ $r.Primary }},
{{- end }}
},
{{- end }}
}
var RaceNameToID = map[string]RaceID{
{{- range $r := $.Races }}
{{ printf "%q" $r.Name }}{{ if $r.Alternate }} + " (Alternate {{ $r.ID }})"{{ end }}: {{ $r.ID }},
{{- end }}
}
{{ end }}

View File

@@ -1,69 +0,0 @@
{{- define "koka-race" -}}
module horse/{{ $.Region }}/race
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import std/data/rb-map
import horse/game-id
pub import horse/race
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $r := $.Races }}{{ $r.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.RaceCount }}, kk_context())"
js inline "[{{ range $r := $.Races }}{{ $r.ID }},{{ end }}]"
// Vector of all race IDs in order for easy iterating.
pub val all = once(create-id-table)
val name2id = once()
var m: rbmap<string, int> := empty()
all().foreach() fn(id) m := m.set(Race-id(id).show, id)
m
// Get the race ID that has the given exact name.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the name, the result is an invalid ID.
pub fun from-name(name: string): race-id
Race-id(name2id().rb-map/lookup(name).default(0))
// Get the name for a race.
// Alternate versions of races have an indication of their ID in their names.
// If no race matches the ID, the result is the numeric ID.
pub fun show(r: race-id): string
match r.game-id
{{- range $r := $.Races }}
{{ $r.ID }} -> {{ printf "%q" $r.Name }}{{ if $r.Alternate }} ++ " (Alternate {{ $r.ID }})"{{ end }}
{{- end }}
x -> "race " ++ x.show
// Get the grade for a race.
// If no race matches the ID, the result is Pre-OP.
pub fun grade(r: race-id): grade
match r.game-id
{{- range $r := $.Races }}
{{ $r.ID }} -> {{ if eq $r.Grade 100 }}G1{{ else if eq $r.Grade 200 }}G2{{ else if eq $r.Grade 300 }}G3{{ else if eq $r.Grade 400 }}OP{{ else if eq $r.Grade 700 }}Pre-OP{{ else }}??? $r.Grade={{ $r.Grade }}{{ end }}
{{- end }}
_ -> Pre-OP
// Get the thumbnail ID for a race.
// If no race matches the ID, the result is an invalid ID.
pub fun thumbnail(r: race-id): race-thumbnail-id
match r.game-id
{{- range $r := $.Races }}
{{ $r.ID }} -> Race-thumbnail-id({{ $r.ThumbnailID }})
{{- end }}
_ -> Race-thumbnail-id(0)
// Get the primary ID for a race.
// For races which are the primary version, or if no race matches the given ID,
// the result is the input.
pub fun primary(r: race-id): race-id
match r.game-id
{{- range $r := $.Races }}
{{- if $r.Alternate }}
{{ $r.ID }} -> Race-id({{ $r.Primary }})
{{- end }}
{{- end }}
_ -> r
{{ end }}

View File

@@ -1,27 +0,0 @@
{{- define "go-saddle" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $s := $.Saddles }}
Saddle{{ goenum $s.Name }}{{ if $s.Alternate }}Alt{{ $s.Alternate }}{{ end }} SaddleID = {{ $s.ID }} // {{ $s.Name }}
{{- end }}
)
var AllSaddles = map[SaddleID]Saddle{
{{- range $s := $.Saddles }}
Saddle{{ goenum $s.Name }}{{ if $s.Alternate }}Alt{{ $s.Alternate }}{{ end }}: {
ID: {{ $s.ID }},
Name: {{ printf "%q" $s.Name }}{{ if $s.Alternate }} + " (Alternate {{ $s.Alternate }})"{{ end }},
Races: []RaceID{ {{- range $id := $s.Races }}{{ if $id }}{{ $id }}, {{ end }}{{ end -}} },
Type: SaddleType{{ if eq $s.Type 0 }}Honor{{ else if eq $s.Type 1 }}G3{{ else if eq $s.Type 2 }}G2{{ else if eq $s.Type 3 }}G1{{ else }}??? $s.Type={{ $s.Type }}{{ end }},
{{- if $s.Alternate }}
Primary: {{ $s.Primary }},
{{- end }}
},
{{- end }}
}
{{ end }}

View File

@@ -1,58 +0,0 @@
{{- define "koka-saddle" -}}
module horse/{{ $.Region }}/saddle
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import horse/game-id
pub import horse/race
pub import horse/{{ $.Region }}/race
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $s := $.Saddles }}{{ $s.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.SaddleCount }}, kk_context())"
js inline "[{{ range $s := $.Saddles }}{{ $s.ID }},{{ end }}]"
// Vector of all saddle IDs in order for easy iterating.
pub val all = once(create-id-table)
// Get the name for a saddle.
// Alternate versions of saddles have an indication of their ID in their names.
// If no saddle matches the ID, the result contains the numeric ID.
pub fun show(s: saddle-id): string
match s.game-id
{{- range $s := $.Saddles }}
{{ $s.ID }} -> {{ printf "%q" $s.Name }}{{ if $s.Alternate }} ++ " (Alternate {{ $s.ID }})"{{ end }}
{{- end }}
x -> "saddle " ++ x.show
// Get the list of races that entitle a horse to a saddle.
// If no saddle matches the ID, the result is the empty list.
pub fun races(s: saddle-id): list<race-id>
match s.game-id
{{- range $s := $.Saddles }}
{{ $s.ID }} -> [{{ range $id := $s.Races }}{{ if $id }}Race-id({{ $id }}), {{ end }}{{ end }}]
{{- end }}
_ -> []
// Get a saddle's type.
// If no saddle matches the ID, the result is Honor.
pub fun saddle-type(s: saddle-id): saddle-type
match s.game-id
{{- range $s := $.Saddles }}
{{ $s.ID }} -> {{ if eq $s.Type 0 }}Honor{{ else if eq $s.Type 1 }}G3-Win{{ else if eq $s.Type 2 }}G2-Win{{ else if eq $s.Type 3 }}G1-Win{{ else }}??? $s.Type={{ $s.Type }}{{ end }}
{{- end }}
_ -> Honor
// Get the primary ID for a saddle.
// For saddles which are the primary version, or if no saddle matches the given ID,
// the result is the input.
pub fun primary(s: saddle-id): saddle-id
match s.game-id
{{- range $s := $.Saddles }}
{{- if $s.Alternate }}
{{ $s.ID }} -> Saddle-id({{ $s.Primary }})
{{- end }}
{{- end }}
_ -> s
{{ end }}

View File

@@ -1,23 +0,0 @@
{{- define "go-scenario" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $s := $.Scenarios }}
Scenario{{ goenum $s.Name }} ScenarioID = {{ $s.ID }} // {{ $s.Name }}
{{- end }}
)
var AllScenarios = map[ScenarioID]Scenario{
{{- range $s := $.Scenarios }}
Scenario{{ goenum $s.Name }}: {
ID: {{ $s.ID }},
Name: {{ printf "%q" $s.Name }},
Title: {{ printf "%q" $s.Title }},
},
{{- end }}
}
{{ end }}

View File

@@ -1,34 +0,0 @@
{{- define "koka-scenario" -}}
module horse/{{ $.Region }}/scenario
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import horse/game-id
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $s := $.Scenarios }}{{ $s.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.ScenarioCount }}, kk_context())"
js inline "[{{ range $s := $.Scenarios }}{{ $s.ID }},{{ end }}]"
// Vector of all scenario IDs in order for easy iterating.
pub val all = once(create-id-table)
// Get the name for a scenario.
// If no scenario matches the ID, the result contains the numeric ID.
pub fun show(s: scenario-id): string
match s.game-id
{{- range $s := $.Scenarios }}
{{ $s.ID }} -> {{ printf "%q" $s.Name }}
{{- end }}
x -> "scenario " ++ x.show
// Get the full title for a scenario, e.g. "The Beginning: URA Finale".
// If no scenario matches the ID, the result contains the numeric ID.
pub fun title(s: scenario-id): string
match s.game-id
{{- range $s := $.Scenarios }}
{{ $s.ID }} -> {{ printf "%q" $s.Title }}
{{- end }}
x -> "scenario " ++ x.show
{{ end }}

View File

@@ -1,14 +0,0 @@
WITH skill_names AS (
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 47
)
SELECT
group_id,
name
FROM skill_data d
JOIN skill_names n ON d.id = n.id
WHERE d.group_rate = 1
ORDER BY group_id

View File

@@ -1,79 +0,0 @@
{{- define "go-skill-data" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $s := $.Skills }}
Skill{{ goenum $s.Name }}{{ if ne $s.InheritID 0 }}Inherit{{ end }} SkillID = {{ $s.ID }} // {{ $s.Name }}
{{- end }}
)
var OrderedSkills = [...]SkillID{
{{- range $s := $.Skills }}
Skill{{ goenum $s.Name }}{{ if ne $s.InheritID 0 }}Inherit{{ end }},
{{- end }}
}
var AllSkills = map[SkillID]Skill{
{{- range $s := $.Skills }}
Skill{{ goenum $s.Name }}{{ if ne $s.InheritID 0 }}Inherit{{ end }}: {
ID: {{ $s.ID }},
Name: {{ printf "%q" $s.Name }}{{ if ne $s.InheritID 0 }} + " (Inherited)"{{ end }},
Description: {{ printf "%q" $s.Description }},
Group: {{ $s.GroupID }},
Rarity: {{ $s.Rarity }},
GroupRate: {{ $s.GroupRate }},
GradeValue: {{ $s.GradeValue }},
{{- if $s.WitCheck }}
WitCheck: {{ $s.WitCheck }},
{{- end }}
Activations: []Activation{
{{- range $a := $s.Activations }}
{{- if ne $a.Condition "" }}
{
{{- if $a.Precondition }}
Precondition: {{ printf "%q" $a.Precondition }},
{{- end }}
Condition: {{ printf "%q" $a.Condition }},
Duration: {{ $a.Duration }},
DurScale: {{ $a.DurScale }},
{{- if $a.Cooldown }}
Cooldown: {{ $a.Cooldown }},
{{- end }}
Abilities: []Ability{
{{- range $abil := $a.Abilities }}
{{- if ne $abil.Type 0 }}
{Type: {{ $abil.Type }}, ValueUsage: {{ $abil.ValueUsage }}, Value: {{ $abil.Value }}, Target: {{ $abil.Target }}, TargetValue: {{ $abil.TargetValue -}} },
{{- end }}
{{- end }}
},
},
{{- end }}
{{- end }}
},
{{- if $s.UniqueOwner }}
UniqueOwner: {{ printf "%q" $s.UniqueOwner }},
{{- end }}
{{- if $s.SPCost }}
SPCost: {{ $s.SPCost }},
{{- end }}
IconID: {{ $s.IconID }},
},
{{- end }}
}
var SkillNameToID = map[string]SkillID{
{{- range $s := $.Skills }}
{{ printf "%q" $s.Name }}{{ if ne $s.InheritID 0 }} + " (Inherited)"{{ end }}: {{ $s.ID }},
{{- end }}
}
var SkillGroups = map[int32][4]SkillID{
{{- range $g := $.Groups }}
{{ $g.ID }}: { {{- range $s := index $.Related $g.ID }}Skill{{ goenum $s.Name }}{{ if ne $s.InheritID 0 }}Inherit{{ end }}, {{ end -}} },
{{- end }}
}
{{ end }}

View File

@@ -1,230 +0,0 @@
{{- define "koka-skill" -}}
module horse/{{ $.Region }}/skill
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import std/data/rb-map
import std/num/decimal
import horse/game-id
import horse/movement
pub import horse/skill
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $s := $.Skills }}{{ $s.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.SkillCount }}, kk_context())"
js inline "[{{ range $s := $.Skills }}{{ $s.ID }},{{ end }}]"
// Vector of all skill ID values in order for easy iterating.
pub val all = once(create-id-table)
val name2id = once()
var m: rbmap<string, int> := empty()
all().foreach() fn(id) m := m.set(Skill-id(id).show, id)
m
// Get the skill ID that has the given exact name.
// Inherited skills have `" (Inherited)"` appended to their names.
// If no skill matches the name, the result is an invalid ID.
pub fun from-name(name: string): skill-id
Skill-id(name2id().rb-map/lookup(name).default(0))
// Get the name for a skill.
// Inherited skills have `" (Inherited)"` appended to their names.
// If no skill matches the ID, the result is the numeric ID.
pub fun show(s: skill-id): string
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ printf "%q" $s.Name }}{{ if $s.InheritID }} ++ " (Inherited)"{{ end }}
{{- end }}
x -> "skill " ++ x.show
// Get the description for a skill.
// If no skill matches the ID, the result is the empty string.
pub fun description(s: skill-id): string
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ printf "%q" $s.Description }}
{{- end }}
_ -> ""
// Get the skill group ID for a skill.
// If no skill matches the ID, the result is an invalid ID.
pub fun group(s: skill-id): skill-group-id
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> Skill-group-id( {{- $s.GroupID -}} )
{{- end }}
_ -> Skill-group-id(0)
// Get the rarity of a skill.
// If no skill matches the ID, the result is Common.
pub fun rarity(s: skill-id): rarity
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ if eq $s.Rarity 1 }}Common{{ else if eq $s.Rarity 2 }}Rare{{ else if eq $s.Rarity 3 }}Unique-Low{{ else if eq $s.Rarity 4 }}Unique-Upgraded{{ else if eq $s.Rarity 5 }}Unique{{ else }}??? $s.Rarity={{ $s.Rarity }}{{ end }}
{{- end }}
_ -> Common
// Get the group rate of a skill.
// If no skill matches the ID, the result is 0.
pub fun group-rate(s: skill-id): int
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ $s.GroupRate }}
{{- end }}
_ -> 0
// Get the grade value of a skill.
// If no skill matches the ID, the result is 0.
pub fun grade-value(s: skill-id): int
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ $s.GradeValue }}
{{- end }}
_ -> 0
// Get whether a skill is a wit check.
// If no skill matches the ID, the result is False.
pub fun wit-check(s: skill-id): bool
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ if $s.WitCheck }}True{{ else }}False{{ end }}
{{- end }}
_ -> False
// Get the activations of a skill.
// If no skill matches the ID, the result is an empty list.
pub fun activations(s: skill-id): list<activation>
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> [
{{- range $a := $s.Activations }}
{{- if $a.Condition }}
Activation(
precondition = {{ printf "%q" $a.Precondition }},
condition = {{ printf "%q" $a.Condition }},
duration = {{ $a.Duration }}.decimal{{ if gt $a.Duration 0 }}(-4){{ end }},
dur-scale = {{ if eq $a.DurScale 1 }}Direct-Dur
{{- else if eq $a.DurScale 2 }}Front-Distance-Dur
{{- else if eq $a.DurScale 3 }}Multiply-Remaining-HP
{{- else if eq $a.DurScale 4 }}Increment-Pass
{{- else if eq $a.DurScale 5 }}Midrace-Side-Block-Time-Dur
{{- else if eq $a.DurScale 7 }}Multiply-Remaining-HP2
{{- else }}??? $a.DurScale={{ $a.DurScale }}
{{- end }},
cooldown = {{ $a.Cooldown }}.decimal{{ if gt $a.Cooldown 0 }}(-4){{ end }},
abilities = [
{{- range $abil := $a.Abilities }}
{{- if $abil.Type }}
Ability(
ability-type = {{ if eq $abil.Type 1 }}Passive-Speed({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 2 }}Passive-Stamina({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 3 }}Passive-Power({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 4 }}Passive-Guts({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 5 }}Passive-Wit({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 6 }}Great-Escape
{{- else if eq $abil.Type 8 }}Vision({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 9 }}HP({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 10 }}Gate-Delay({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 13 }}Frenzy({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 21 }}Current-Speed({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 27 }}Target-Speed({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 28 }}Lane-Speed({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 31 }}Accel({{ $abil.Value }}.decimal(-4))
{{- else if eq $abil.Type 35 }}Lane-Change({{ $abil.Value }}.decimal(-4))
{{- else }}??? $abil.Type={{$abil.Type}}
{{- end }},
value-usage = {{ if eq $abil.ValueUsage 1 }}Direct
{{- else if eq $abil.ValueUsage 3 }}Team-Speed
{{- else if eq $abil.ValueUsage 4 }}Team-Stamina
{{- else if eq $abil.ValueUsage 5 }}Team-Power
{{- else if eq $abil.ValueUsage 6 }}Team-Guts
{{- else if eq $abil.ValueUsage 7 }}Team-Wit
{{- else if eq $abil.ValueUsage 8 }}Multiply-Random
{{- else if eq $abil.ValueUsage 9 }}Multiply-Random2
{{- else if eq $abil.ValueUsage 10 }}Climax
{{- else if eq $abil.ValueUsage 13 }}Max-Stat
{{- else if eq $abil.ValueUsage 14 }}Passive-Count
{{- else if eq $abil.ValueUsage 19 }}Front-Distance-Add
{{- else if eq $abil.ValueUsage 20 }}Midrace-Side-Block-Time
{{- else if eq $abil.ValueUsage 22 }}Speed-Scaling
{{- else if eq $abil.ValueUsage 23 }}Speed-Scaling2
{{- else if eq $abil.ValueUsage 24 }}Arc-Global-Potential
{{- else if eq $abil.ValueUsage 25 }}Max-Lead-Distance
{{- else }}??? $abil.ValueUsage={{ $abil.ValueUsage }}
{{- end }},
target = {{ if eq $abil.Target 1}}Self
{{- else if eq $abil.Target 4 }}Sympathizers
{{- else if eq $abil.Target 4 }}In-View
{{- else if eq $abil.Target 4 }}Frontmost
{{- else if eq $abil.Target 9 }}Ahead({{ $abil.TargetValue }})
{{- else if eq $abil.Target 10 }}Behind({{ $abil.TargetValue }})
{{- else if eq $abil.Target 4 }}All-Teammates
{{- else if eq $abil.Target 18 }}Style({{ if eq $abil.TargetValue 1 }}Front-Runner{{ else if eq $abil.TargetValue 2 }}Pace-Chaser{{ else if eq $abil.TargetValue 3 }}Late-Surger{{ else if eq $abil.TargetValue 4 }}End-Closer{{ else }}??? $abil.TargetValue={{ $abil.TargetValue }}{{ end }})
{{- else if eq $abil.Target 19 }}Rushing-Ahead({{ $abil.TargetValue }})
{{- else if eq $abil.Target 20 }}Rushing-Behind({{ $abil.TargetValue }})
{{- else if eq $abil.Target 21 }}Rushing-Style({{ if eq $abil.TargetValue 1 }}Front-Runner{{ else if eq $abil.TargetValue 2 }}Pace-Chaser{{ else if eq $abil.TargetValue 3 }}Late-Surger{{ else if eq $abil.TargetValue 4 }}End-Closer{{ else }}??? $abil.TargetValue={{ $abil.TargetValue }}{{ end }})
{{- else if eq $abil.Target 22 }}Specific-Character(Character-id({{ $abil.TargetValue }}))
{{- else if eq $abil.Target 23 }}Triggering
{{- end }}
),
{{- end }}
{{- end }}
]
),
{{- end }}
{{- end }}
]
{{- end }}
_ -> Nil
// Get the owner of a unique skill.
// If the skill is not unique, or if there is no skill with the given ID,
// the result is Nothing.
pub fun unique-owner(s: skill-id): maybe<uma-id>
match s.game-id
{{- range $s := $.Skills }}
{{- if $s.UniqueOwnerID }}
{{ $s.ID }} -> Just(Uma-id({{ $s.UniqueOwnerID }}))
{{- end }}
{{- end }}
_ -> Nothing
// Get the SP cost of a skill.
// If there is no skill with the given ID, the result is 0.
pub fun sp-cost(s: skill-id): int
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> {{ $s.SPCost }}
{{- end }}
_ -> 0
// Get the icon ID of a skill.
// If there is no skill with the given ID, the result is an invalid ID.
pub fun icon-id(s: skill-id): skill-icon-id
match s.game-id
{{- range $s := $.Skills }}
{{ $s.ID }} -> Skill-icon-id({{ $s.IconID }})
{{- end }}
_ -> Skill-icon-id(0)
// Get the name for a skill group.
// Skill group names are the name of the base skill in the group.
// If there is no skill group with the given ID, the result is the numeric ID.
pub fun skill-group/show(sg: skill-group-id): string
match sg.game-id
{{- range $g := $.Groups }}
{{ $g.ID }} -> {{- printf "%q" $g.Name -}}
{{- end }}
x -> "skill group " ++ x.show
// Get the list of skills in a skill group.
pub fun skill-group/skills(sg: skill-group-id): list<skill-id>
match sg.game-id
{{- range $g := $.Groups }}
{{ $g.ID }} -> [ {{- range $s := index $.Related $g.ID }}Skill-id({{ $s.ID }}), {{ end -}} ]
{{- end }}
_ -> Nil
{{- end }}

View File

@@ -1,35 +0,0 @@
{{- define "go-spark" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $s := $.Sparks }}
Spark{{ goenum $s.Name }}Lv{{ $s.Rarity }} SparkID = {{ $s.ID }} // {{ $s.Name }}
{{- end }}
)
var AllSparks = map[SparkID]Spark{
{{- range $s := $.Sparks }}
Spark{{ goenum $s.Name }}Lv{{ $s.Rarity }}: {
ID: {{ $s.ID }},
Name: {{ printf "%q" $s.Name }},
Description: {{ printf "%q" $s.Description }},
Group: {{ $s.Group }},
Rarity: {{ $s.Rarity }},
Type: {{ $s.Type }},
Effects: [][]SparkEffect{
{{- range $r := index $.SparkEffects $s.Group }}
{
{{- range $e := $r -}}
{ {{- $e.Target }}, {{ $e.Value1 }}, {{ $e.Value2 -}} },
{{- end -}}
},
{{- end }}
},
},
{{- end }}
}
{{ end }}

View File

@@ -1,121 +0,0 @@
{{- define "koka-spark" -}}
module horse/{{ $.Region }}/spark
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import horse/game-id
pub import horse/spark
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $s := $.Sparks }}{{ $s.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.SparkCount }}, kk_context())"
js inline "[{{ range $s := $.Sparks }}{{ $s.ID }},{{ end }}]"
// Vector of all spark IDs in order for easy iterating.
pub val all = once(create-id-table)
// Get the name for a spark.
// The name does not indicate the spark level.
// If no spark matches the ID, the result contains the numeric ID.
pub fun show(s: spark-id): string
match s.game-id
{{- range $s := $.Sparks }}
{{ $s.ID }} -> {{ printf "%q" $s.Name }}
{{- end }}
x -> "spark " ++ x.show
// Get the description for a spark.
// The description does not indicate the spark level.
// If no spark matches the ID, the result contains the numeric ID.
pub fun description(s: spark-id): string
match s.game-id
{{- range $s := $.Sparks }}
{{ $s.ID }} -> {{ printf "%q" $s.Description }}
{{- end }}
x -> "spark " ++ x.show
// Get the spark group ID of a spark.
// If no spark matches the ID, the result is an invalid ID.
pub fun spark-group(s: spark-id): spark-group-id
match s.game-id
{{- range $s := $.Sparks }}
{{ $s.ID }} -> Spark-group-id({{ $s.Group }})
{{- end }}
_ -> Spark-group-id(0)
// Get the rarity (level or star count) of a spark.
// If no spark matches the ID, the result is One.
pub fun rarity(s: spark-id): rarity
match s.game-id
{{- range $s := $.Sparks }}
{{ $s.ID }} -> {{ if eq $s.Rarity 1 }}One{{ else if eq $s.Rarity 2 }}Two{{ else if eq $s.Rarity 3 }}Three{{ else }}??? $s.Rarity={{ $s.Rarity }}{{ end }}
{{- end }}
_ -> One
// Get the type of a spark.
// If no spark matches the ID, the result is Stat.
pub fun spark-type(s: spark-id): spark-type
match s.game-id
{{- range $s := $.Sparks }}
{{ $s.ID }} -> {{ if eq $s.Type 1 }}Stat
{{- else if eq $s.Type 2 }}Aptitude
{{- else if eq $s.Type 5 }}Race
{{- else if eq $s.Type 4 }}Skill
{{- else if eq $s.Type 6 }}Scenario
{{- else if eq $s.Type 7 }}Carnival-Bonus
{{- else if eq $s.Type 10 }}Surface
{{- else if eq $s.Type 8 }}Distance
{{- else if eq $s.Type 11 }}Style
{{- else if eq $s.Type 9 }}Hidden
{{- else if eq $s.Type 3 }}Unique
{{- else }}??? $s.Type={{ $s.Type }}
{{- end }}
{{- end }}
_ -> Stat
// Get the list of all effects a spark can apply during inheritance.
// When a spark procs, a random element is chosen from the list yielded by this
// function according to a hidden distribution, then all effects in that are applied.
// If no spark matches the ID, the result is the empty list.
pub fun effects(s: spark-id): list<list<spark-effect>>
match s.game-id
{{- range $s := $.Sparks }}
{{ $s.ID }} -> [
{{- range $r := index $.SparkEffects $s.Group }}
[
{{- range $e := $r -}}
{{- if eq $e.Target 1 -}}Stat-Up(Speed, {{ $e.Value1 }}),
{{- else if eq $e.Target 2 -}}Stat-Up(Stamina, {{ $e.Value1 }}),
{{- else if eq $e.Target 3 -}}Stat-Up(Power, {{ $e.Value1 }}),
{{- else if eq $e.Target 4 -}}Stat-Up(Guts, {{ $e.Value1 }}),
{{- else if eq $e.Target 5 -}}Stat-Up(Wit, {{ $e.Value1 }}),
{{- else if eq $e.Target 6 -}}SP-Up({{ $e.Value1 }}),
{{- else if eq $e.Target 7 -}}Random-Stat-Up({{ $e.Value1 }}),
{{- else if eq $e.Target 11 -}}Aptitude-Up(Turf, {{ $e.Value1 }}),
{{- else if eq $e.Target 12 -}}Aptitude-Up(Dirt, {{ $e.Value1 }}),
{{- else if eq $e.Target 21 -}}Aptitude-Up(Front-Runner, {{ $e.Value1 }}),
{{- else if eq $e.Target 22 -}}Aptitude-Up(Pace-Chaser, {{ $e.Value1 }}),
{{- else if eq $e.Target 23 -}}Aptitude-Up(Late-Surger, {{ $e.Value1 }}),
{{- else if eq $e.Target 24 -}}Aptitude-Up(End-Closer, {{ $e.Value1 }}),
{{- else if eq $e.Target 31 -}}Aptitude-Up(Sprint, {{ $e.Value1 }}),
{{- else if eq $e.Target 32 -}}Aptitude-Up(Mile, {{ $e.Value1 }}),
{{- else if eq $e.Target 33 -}}Aptitude-Up(Medium, {{ $e.Value1 }}),
{{- else if eq $e.Target 34 -}}Aptitude-Up(Long, {{ $e.Value1 }}),
{{- else if eq $e.Target 41 -}}Skill-Hint(Skill-id({{ $e.Value1 }}), {{ $e.Value2 }}),
{{- else if eq $e.Target 51 -}}Carnival-Bonus {{/*- skipped, but doesn't hurt to put it here -*/}}
{{- else if eq $e.Target 61 -}}Stat-Cap-Up(Speed, {{ $e.Value1 }}),
{{- else if eq $e.Target 62 -}}Stat-Cap-Up(Stamina, {{ $e.Value1 }}),
{{- else if eq $e.Target 63 -}}Stat-Cap-Up(Power, {{ $e.Value1 }}),
{{- else if eq $e.Target 64 -}}Stat-Cap-Up(Guts, {{ $e.Value1 }}),
{{- else if eq $e.Target 65 -}}Stat-Cap-Up(Wit, {{ $e.Value1 }}),
{{- else -}}
??? $e.Target={{- $e.Target -}}
{{- end -}}
{{- end -}}
],
{{- end }}
]
{{- end }}
_ -> []
{{ end }}

View File

@@ -1,42 +0,0 @@
{{- define "go-uma" -}}
package {{ $.Region }}
// Automatically generated with horsegen; DO NOT EDIT
import . "git.sunturtle.xyz/zephyr/horse/horse"
const (
{{- range $uma := $.Umas }}
{{ goenum $uma.CharacterName }}{{ goenum $uma.Variant }} UmaID = {{ $uma.ID }} // {{ $uma.Name }}
{{- end }}
)
var AllUmas = map[UmaID]Uma{
{{- range $uma := $.Umas }}
{{ goenum $uma.CharacterName }}{{ goenum $uma.Variant }}: {
ID: {{ $uma.ID }},
CharacterID: {{ $uma.CharacterID }},
Name: {{ printf "%q" $uma.Name }},
Variant: {{ printf "%q" $uma.Variant }},
Sprint: {{ $uma.Sprint }},
Mile: {{ $uma.Mile }},
Medium: {{ $uma.Medium }},
Long: {{ $uma.Long }},
Front: {{ $uma.Front }},
Pace: {{ $uma.Pace }},
Late: {{ $uma.Late }},
End: {{ $uma.End }},
Turf: {{ $uma.Turf }},
Dirt: {{ $uma.Dirt }},
Unique: {{ $uma.UniqueID }},
Skill1: {{ $uma.Skill1 }},
Skill2: {{ $uma.Skill2 }},
Skill3: {{ $uma.Skill3 }},
SkillPL2: {{ $uma.SkillPL2 }},
SkillPL3: {{ $uma.SkillPL3 }},
SkillPL4: {{ $uma.SkillPL4 }},
SkillPL5: {{ $uma.SkillPL5 }},
},
{{- end }}
}
{{ end }}

View File

@@ -1,221 +0,0 @@
{{- define "koka-uma" -}}
module horse/{{ $.Region }}/uma
// Automatically generated with horsegen; DO NOT EDIT
import std/core/delayed
import std/core/vector
import std/core-extras
import horse/game-id
import horse/movement
pub import horse/uma
extern create-id-table(): vector<int>
c inline "int32_t arr[] = { {{- range $uma := $.Umas }}{{ $uma.ID }},{{ end -}} };\nkk_vector_from_cint32array(arr, (kk_ssize_t){{ $.UmaCount }}, kk_context())"
js inline "[{{ range $uma := $.Umas }}{{ $uma.ID }},{{ end }}]"
// Vector of all Uma IDs in order for easy iterating.
pub val all = once(create-id-table)
// Get the name for an Uma.
// The name includes the costume variant, e.g. `[Special Dreamer] Special Week`.
// If no Uma matches the ID, the result contains the numeric ID.
pub fun show(uma: uma-id): string
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ printf "%q" $uma.Name }}
{{- end }}
x -> "uma " ++ x.show
// Get the costume variant for an Uma, e.g. `[Special Dreamer]`.
// If no Uma matches the ID, the result contains the numeric ID.
pub fun variant(uma: uma-id): string
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ printf "%q" $uma.Variant }}
{{- end }}
x -> "uma " ++ x.show
// Get the character ID for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun character-id(uma: uma-id): character-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Character-id({{ $uma.CharacterID }})
{{- end }}
_ -> Character-id(0)
// Get the sprint aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun sprint(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Sprint }}
{{- end }}
_ -> G
// Get the mile aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun mile(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Mile }}
{{- end }}
_ -> G
// Get the medium aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun medium(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Medium }}
{{- end }}
_ -> G
// Get the long aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun long(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Long }}
{{- end }}
_ -> G
// Get the front runner aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun front-runner(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Front }}
{{- end }}
_ -> G
// Get the pace chaser aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun pace-chaser(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Pace }}
{{- end }}
_ -> G
// Get the late surger aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun late-surger(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Late }}
{{- end }}
_ -> G
// Get the end closer aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun end-closer(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.End }}
{{- end }}
_ -> G
// Get the turf aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun turf(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Turf }}
{{- end }}
_ -> G
// Get the dirt aptitude for an Uma.
// If no Uma matches the ID, the result is G.
pub fun dirt(uma: uma-id): aptitude-level
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> {{ template "koka-aptitude-level" $uma.Dirt }}
{{- end }}
_ -> G
// Get the unique skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun unique(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.UniqueID }})
{{- end }}
_ -> Skill-id(0)
// Get the first built-in skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill1(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.Skill1 }})
{{- end }}
_ -> Skill-id(0)
// Get the second built-in skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill2(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.Skill2 }})
{{- end }}
_ -> Skill-id(0)
// Get the third built-in skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill3(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.Skill3 }})
{{- end }}
_ -> Skill-id(0)
// Get the potential level 2 skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill-pl2(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.SkillPL2 }})
{{- end }}
_ -> Skill-id(0)
// Get the potential level 3 skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill-pl3(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.SkillPL3 }})
{{- end }}
_ -> Skill-id(0)
// Get the potential level 4 skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill-pl4(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.SkillPL4 }})
{{- end }}
_ -> Skill-id(0)
// Get the potential level 5 skill for an Uma.
// If no Uma matches the ID, the result is an invalid ID.
pub fun skill-pl5(uma: uma-id): skill-id
match uma.game-id
{{- range $uma := $.Umas }}
{{ $uma.ID }} -> Skill-id({{ $uma.SkillPL5 }})
{{- end }}
_ -> Skill-id(0)
{{ end }}
{{- define "koka-aptitude-level" -}}
{{- if eq . 1 -}} G
{{- else if eq . 2 -}} F
{{- else if eq . 3 -}} E
{{- else if eq . 4 -}} D
{{- else if eq . 5 -}} C
{{- else if eq . 6 -}} B
{{- else if eq . 7 -}} A
{{- else if eq . 8 -}} S
{{- else -}} ??? aptitude={{ . }}
{{- end -}}
{{- end -}}

420
schema/schema.ts Normal file
View File

@@ -0,0 +1,420 @@
/**
* TypeScript schema for JSON files generated by horsegen.
*/
/**
* Character definitions.
*/
export interface Character {
/**
* Character ID.
*/
chara_id: number;
/**
* Regional name of the character.
* E.g., Special Week for Global, or スペシャルウィーク for JP.
*/
name: string;
}
/**
* Precomputed character pair and trio affinity.
*/
export interface Affinity {
/**
* First character in the relation.
*/
chara_a: number;
/**
* Second character in the relation.
* chara_a < chara_b is an invariant.
*/
chara_b: number;
/**
* Third character in the relation, if it is a trio relation.
* If defined, chara_b < chara_c is an invariant.
*/
chara_c?: number;
/**
* Total base compatibility between characters in the relation.
*/
affinity: number;
}
/**
* Uma or character card definitions.
*/
export interface Uma {
/**
* Uma ID.
*/
chara_card_id: number;
/**
* Character ID that the Uma is a variant of.
*/
chara_id: number;
/**
* Regional name of the Uma, comprised of the variant name and the character name.
* E.g. "[Special Dreamer] Special Week".
*/
name: string;
/**
* Regional variant name.
* E.g. "[Special Dreamer]".
*/
variant: string;
sprint: AptitudeLevel;
mile: AptitudeLevel;
medium: AptitudeLevel;
long: AptitudeLevel;
front: AptitudeLevel;
pace: AptitudeLevel;
late: AptitudeLevel;
end: AptitudeLevel;
turf: AptitudeLevel;
dirt: AptitudeLevel;
/**
* ID of the Uma's unique skill.
*/
unique: number;
/**
* ID of the Uma's first built-in skill.
*/
skill1: number;
/**
* ID of the Uma's second built-in skill.
*/
skill2: number;
/**
* ID of the Uma's third built-in skill.
*/
skill3: number;
/**
* ID of the skill unlocked at potential level 2.
*/
skill_pl2: number;
/**
* ID of the skill unlocked at potential level 3.
*/
skill_pl3: number;
/**
* ID of the skill unlocked at potential level 4.
*/
skill_pl4: number;
/**
* ID of the skill unlocked at potential level 5.
*/
skill_pl5: number;
}
export type AptitudeLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
/**
* Race data.
*/
export interface Race {
/**
* Race ID.
*/
race_id: number;
/**
* Regional name of the race.
*/
name: string;
/**
* Thumbnail asset ID number.
*/
thumbnail: number;
/**
* Primary race ID.
* For most races, this is the same as race_id. Some races are alternate
* versions for certain careers; this holds the ID of the normal version of
* the race.
*/
primary: number;
}
/**
* Race saddle data.
*/
export interface Saddle {
/**
* Saddle ID.
*/
saddle_id: number;
/**
* Regional name of the saddle.
*/
name: string;
/**
* IDs of race wins required to earn the saddle.
*/
races: number[];
/**
* Saddle type: 0 for multi-race honors, 3 for G1, 2 for G2, 1 for G3.
*/
type: 0 | 1 | 2 | 3;
/**
* Primary saddle ID.
* Respective for races.
*/
primary: number;
}
/**
* Scenario data.
*/
export interface Scenario {
/**
* Scenario ID.
*/
scenario_id: number;
/**
* Regional scenario name, e.g. "TS Climax".
*/
name: string;
/**
* Regional full title, e.g. "Trackblazer: Start of the Climax".
*/
title: string;
}
/**
* 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] | [Activation, 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] | [Ability, Ability] | [Ability, Ability, 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;
}
/**
* Sparks, or succession factors.
*/
export interface Spark {
/**
* Spark ID.
*/
spark_id: number;
/**
* Regional spark name.
*/
name: string;
/**
* Regional spark description.
*/
description: string;
/**
* Spark group.
* Different star levels of a given spark are different spark IDs but
* share a spark group.
*/
spark_group: number;
/**
* Spark rarity, or star level.
*/
rarity: 1 | 2 | 3;
/**
* Spark type.
* Roughly the spark color, with extra subdivisions for white sparks.
*/
type: 1 | 2 | 5 | 4 | 6 | 7 | 10 | 8 | 11 | 9 | 3;
/**
* Possible effects applied by the spark during inspiration.
* A random element is selected from this list according to unknown
* distributions, then all effects in that selection are applied.
*/
effects: SparkEffect[][];
}
/**
* Effects that a spark can apply.
*/
export interface SparkEffect {
target: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 41 | 51 | 61 | 62 | 63 | 64 | 65;
value1?: number;
value2: number;
}
/**
* Lobby conversation data.
*/
export interface Conversation {
/**
* Character who owns the conversation as a gallery entry.
*/
chara_id: number;
/**
* Number of the conversation within the character's conversation gallery.
*/
number: number;
/**
* Location ID of the conversation.
*/
location: 110 | 120 | 130 | 210 | 220 | 310 | 410 | 420 | 430 | 510 | 520 | 530;
/**
* English name of the location, for convenience.
*/
location_name: string;
/**
* First character in the conversation.
* Not necessarily equal to chara_id.
*/
chara_1: number;
/**
* Second character, if present.
*/
chara_2?: number;
/**
* Third character, if present.
*/
chara_3?: number;
/**
* Some unknown number in the game's local database.
*/
condition_type: 0 | 1 | 2 | 3 | 4;
}

View File

@@ -143,132 +143,134 @@ val p1 = Legacy(
val p2 = Legacy( val p2 = Legacy(
uma = Veteran( uma = Veteran(
uma = Uma-id(100201), // silence suzuka uma = Uma-id(102601), // mihono bourbon
sparks = [ sparks = [
202, // 2* stamina 302,
3301, // 1* medium 3303,
10020101, // 1* view from the lead 1001201,
1000302, // 2* osaka hai 1001702,
1001201, // 1* takarazuka kinen 1001901,
1001502, // 2* kikuka sho 2004302,
1001601, // 1* tsa 2004502,
1002201, // 1* asahi hai fs 2012502,
1002301, // 1* arima kinen 2015202,
2003302, // 2* corner adept 2016002,
2004302, // 2* focus 2016401,
2004502, // 2* prudent positioning 3000201,
2005301, // 1* early lead 10260102,
2012602, // 2* dodging danger
2012802, // 2* moxie
2016002, // 2* groundwork
].map(Spark-id(_)), ].map(Spark-id(_)),
saddles = [ saddles = [
1, // classic triple crown 2,
2, // senior autumn triple crown 6,
6, // dual grand prix 7,
10, // arima kinen 10,
11, // japan cup 11,
12, // derby 12,
14, // takarazuka kinen 14,
15, // tsa 15,
16, // kikuka sho 17,
17, // osaka hai 18,
18, // satsuki sho 21,
21, // yasuda kinen 23,
25, // victoria mile 25,
26, // qe2 26,
33, // asahi hai fs 27,
34, // hopeful stakes 33,
45, // yayoi sho 34,
46, // kinko sho 49,
63, // kobe shimbun hai
65, // mainichi okan
].map(Saddle-id(_)) ].map(Saddle-id(_))
), ),
sub1 = Veteran( sub1 = Veteran(
uma = Uma-id(102001), // seiun sky uma = Uma-id(102402), // wedding mayano
sparks = [ sparks = [
301, // 1* power 203,
2102, // 2* front runner 3202,
10200103, // 3* angling and scheming 1000701,
1000302, // 2* osaka hai 1000802,
1001001, // 1* japanese derby 1001201,
1001101, // 1* yasuda kinen 1001803,
1001701, // 1* qe2 2003502,
2001402, // 2* non-standard distance 2003701,
2004301, // 1* focus 2004301,
2005301, // 1* early lead 2005502,
2012401, // 1* front runner straightaways 2012401,
2012502, // 2* front runner corners 2016402,
2015201, // 1* front runner savvy 10240202,
2016001, // 1* groundwork
2016102, // 2* thh
2016402, // 2* lone wolf
3000201, // 1* unity cup
].map(Spark-id(_)), ].map(Spark-id(_)),
saddles = [ saddles = [
1, // classic triple crown 1,
2, // senior autumn triple crown 2,
4, // senior spring triple crown 6,
5, // tenno sweep 7,
6, // dual grand prix 10,
7, // dual miles 11,
10, // arima kinen 12,
11, // japan cup 14,
12, // derby 15,
13, // tss 16,
14, // takarazuka kinen 18,
15, // tsa 21,
16, // kikuka sho 23,
17, // osaka hai 25,
18, // satsuki sho 26,
21, // yasuda kinen 27,
23, // mile championship 33,
25, // victoria mile 34,
26, // qe2 48,
33, // asahi hai fs
34, // hopeful stakes
96, // mainichi hai
].map(Saddle-id(_)) ].map(Saddle-id(_))
), ),
sub2 = Veteran( sub2 = Veteran(
uma = Uma-id(102601), // mihono bourbon uma = Uma-id(100201), // silence suzuka
sparks = [ sparks = [
102, // 2* speed 203,
3402, // 2* long 1101,
10260101, // 1* g00 1st 1001901,
1000502, // 2* satsuki sho 1002203,
1000701, // 1* nhk mile cup 1002302,
1001201, // 1* takarazuka kinen 2000101,
1001702, // 2* qe2 2000201,
2000101, // 1* right-handed 2001902,
2002102, // 2* sunny days 2003501,
2004302, // 2* focus 2005401,
2005301, // 1* early lead 2016001,
2012502, // 2* front corners 3000102,
2016001, // 1* groundwork 10020101,
].map(Spark-id(_)), ].map(Spark-id(_)),
saddles = [ saddles = [
1, // classic triple crown 2,
2, // senior autumn triple crown 6,
6, // dual grand prix 10,
10, // arima kinen 11,
11, // japan cup 12,
12, // derby 14,
14, // takarazuka kinen 15,
15, // tsa 17,
16, // kikuka sho 18,
17, // osaka hai 21,
18, // satsuki sho 25,
26, // qe2 26,
27, // nhk mile cup 27,
33, // asahi hai fs 33,
49, // spring stakes 34,
40,
42,
44,
45,
46,
49,
59,
61,
63,
65,
111,
113,
117,
126,
].map(Saddle-id(_)) ].map(Saddle-id(_))
) )
) )
val trainee = Uma-id(102402) // wedding mayano val trainee = Uma-id(104601) // smart falcon
pub fun main() pub fun main()
val p1a = parent-affinity(trainee, p1, p2.uma.uma) val p1a = parent-affinity(trainee, p1, p2.uma.uma)

23
zenno/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
zenno/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

9
zenno/.prettierignore Normal file
View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

16
zenno/.prettierrc Normal file
View File

@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 130,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
}

42
zenno/README.md Normal file
View File

@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv@0.13.0 create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" tailwindcss="plugins:none" --install npm zenno
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

39
zenno/eslint.config.js Normal file
View File

@@ -0,0 +1,39 @@
import prettier from 'eslint-config-prettier';
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
ts.configs.recommended,
svelte.configs.recommended,
prettier,
svelte.configs.prettier,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off',
},
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig,
},
},
},
);

4347
zenno/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
zenno/package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "zenno",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run"
},
"devDependencies": {
"@eslint/compat": "^2.0.3",
"@eslint/js": "^10.0.1",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^22",
"@vitest/browser-playwright": "^4.1.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.15.2",
"globals": "^17.4.0",
"playwright": "^1.58.2",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.54.0",
"svelte-check": "^4.4.2",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.0",
"vite": "^7.3.1",
"vitest": "^4.1.0",
"vitest-browser-svelte": "^2.0.2"
}
}

13
zenno/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
zenno/src/app.html Normal file
View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { character } from '$lib/data/character';
import type { ClassValue } from 'svelte/elements';
interface Props {
id: string;
value: number;
label?: string;
class?: ClassValue | null;
optionClass?: ClassValue | null;
labelClass?: ClassValue | null;
region?: keyof typeof character;
required?: boolean;
}
let {
id,
value = $bindable(),
label,
class: className,
optionClass,
labelClass,
region = 'global',
required = false,
}: Props = $props();
</script>
{#if label}
<label for={id} class={labelClass}>{label}</label>
{/if}
<select {id} class={className} bind:value {required}>
{#if !required}
<option value="0" class={optionClass}></option>
{/if}
{#each character[region] as c}
<option value={c.chara_id} class={optionClass}>{c.name}</option>
{/each}
</select>

BIN
zenno/src/lib/assets/favicon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -0,0 +1,26 @@
import type { RegionalName } from '$lib/regional-name';
import globalJSON from '../../../../global/character.json';
/**
* Character definitions.
*/
export interface Character {
/**
* Character ID.
*/
chara_id: number;
/**
* Regional name of the character.
* E.g., Special Week for Global, or スペシャルウィーク for JP.
*/
name: string;
}
export const character = {
global: globalJSON as Character[],
};
export const charaNames = globalJSON.reduce(
(m, c) => m.set(c.chara_id, { en: c.name }),
new Map<Character['chara_id'], RegionalName>(),
);

View File

@@ -0,0 +1,85 @@
import type { RegionalName } from '$lib/regional-name';
import globalJSON from '../../../../global/conversation.json';
/**
* Lobby conversation data.
*/
export interface Conversation {
/**
* Character who owns the conversation as a gallery entry.
*/
chara_id: number;
/**
* Number of the conversation within the character's conversation gallery.
*/
number: number;
/**
* Location ID of the conversation.
*/
location: 110 | 120 | 130 | 210 | 220 | 310 | 410 | 420 | 430 | 510 | 520 | 530;
/**
* English name of the location, for convenience.
*/
location_name: string;
/**
* First character in the conversation.
* Not necessarily equal to chara_id.
*/
chara_1: number;
/**
* Second character, if present.
*/
chara_2?: number;
/**
* Third character, if present.
*/
chara_3?: number;
/**
* Some unknown number in the game's local database.
*/
condition_type: 0 | 1 | 2 | 3 | 4;
}
export const conversation = {
global: globalJSON as Conversation[],
};
export const byChara = {
global: globalJSON.reduce(
(m, c) => m.set(c.chara_id, (m.get(c.chara_id) ?? []).concat(c as Conversation)),
new Map<Conversation['chara_id'], Conversation[]>(),
),
};
export const locations: Record<Conversation['location'], { name: RegionalName; group: 1 | 2 | 3 | 4 | 5 }> = {
110: { name: { en: 'right side front' }, group: 1 },
120: { name: { en: 'right side front' }, group: 1 },
130: { name: { en: 'right side front' }, group: 1 },
210: { name: { en: 'left side table' }, group: 2 },
220: { name: { en: 'left side table' }, group: 2 },
310: { name: { en: 'center back seat' }, group: 3 },
410: { name: { en: 'center posters' }, group: 4 },
420: { name: { en: 'center posters' }, group: 4 },
430: { name: { en: 'center posters' }, group: 4 },
510: { name: { en: 'left side school map' }, group: 5 },
520: { name: { en: 'left side school map' }, group: 5 },
530: { name: { en: 'left side school map' }, group: 5 },
};
function locCharas(convos: Conversation[], locGroup: 1 | 2 | 3 | 4 | 5) {
const m = convos
.filter((c) => locations[c.location].group === locGroup)
.flatMap((c) => [c.chara_1, c.chara_2, c.chara_3].filter((x) => x != null))
.reduce((m, id) => m.set(id, 1 + (m.get(id) ?? 0)), new Map<number, number>());
return [...m].toSorted((a, b) => b[1] - a[1]); // descending
}
export const groupPopulars = {
global: {
1: locCharas(conversation.global, 1),
2: locCharas(conversation.global, 2),
3: locCharas(conversation.global, 3),
4: locCharas(conversation.global, 4),
5: locCharas(conversation.global, 5),
},
};

1
zenno/src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,7 @@
/**
* Names accounting for regions.
* Currently English is the only supported language.
*/
export interface RegionalName {
en: string;
}

View File

@@ -0,0 +1,8 @@
<script>
import { greet } from './greet';
let { host = 'SvelteKit', guest = 'Vitest' } = $props();
</script>
<h1>{greet(host)}</h1>
<p>{greet(guest)}</p>

View File

@@ -0,0 +1,13 @@
import { page } from 'vitest/browser';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import Welcome from './Welcome.svelte';
describe('Welcome.svelte', () => {
it('renders greetings for host and guest', async () => {
render(Welcome, { host: 'SvelteKit', guest: 'Vitest' });
await expect.element(page.getByRole('heading', { level: 1 })).toHaveTextContent('Hello, SvelteKit!');
await expect.element(page.getByText('Hello, Vitest!')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,8 @@
import { describe, it, expect } from 'vitest';
import { greet } from './greet';
describe('greet', () => {
it('returns a greeting', () => {
expect(greet('Svelte')).toBe('Hello, Svelte!');
});
});

View File

@@ -0,0 +1,3 @@
export function greet(name: string): string {
return 'Hello, ' + name + '!';
}

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.png';
let { children } = $props();
</script>
<svelte:head>
<title>Zenno Rob Roy</title>
<link rel="icon" href={favicon} />
</svelte:head>
<div class="flex h-screen flex-col">
<nav class="mb-4 flex min-w-full bg-mist-300 p-4 shadow-md dark:bg-mist-900">
<span class="hidden flex-1 md:inline">
<a href="/" class="text-4xl">Zenno Rob Roy</a>
</span>
<span class="flex-1 text-center">
<a href="/" class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
<a href="/inherit" class="mx-8 my-1 inline-block">Inheritance Chance</a>
<a href="/spark" class="mx-8 my-1 inline-block">Spark Chance</a>
<a href="/vet" class="mx-8 my-1 inline-block">My Veterans</a>
<a href="/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">
Umamusume: Pretty Derby tools by <a href="https://zephyrtronium.date/" target="_blank" rel="noopener noreferrer"
>zephyrtronium</a
>.<br />
All game data is auto-generated from the
<a href="https://git.sunturtle.xyz/zephyr/horse/src/branch/main/doc/README.md" target="_blank" rel="noopener noreferrer"
>game's local database</a
>.
</footer>
</div>

View File

@@ -0,0 +1,2 @@
export const prerender = true;
export const trailingSlash = 'always';

View File

@@ -0,0 +1,40 @@
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy</h1>
<p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p>
<h2 class="mt-8 mb-4 text-4xl">Tools</h2>
<ul class="list-disc pl-4">
<li>
<a href="/inherit">Inheritance Chance</a><i>Not yet implemented</i> — Given a legacy, calculate the probability distribution
of activation counts for each spark.
</li>
<li>
<a href="/spark">Spark Chance</a><i>Not yet implemented</i> — Given a legacy, calculate the chance of generating each spark if
you fulfill the conditions to do so, and the distribution of total spark counts.
</li>
<li>
<a href="/vet">My Veterans</a><i>Not yet implemented</i> — Set up and track your veterans for Zenno Rob Roy's inspiration and
spark calculators.
</li>
<li>
<a href="/convo">Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking them quickly.
</li>
<li>
<a href="https://discord.com/oauth2/authorize?client_id=1461931240264568994" target="_blank" rel="noopener noreferrer"
>Discord Bot</a
>
— Skill search by name or unique owner within Discord. Install to a server or user.
</li>
</ul>
<h2 class="mt-8 mb-4 text-4xl">About</h2>
<p>Tools to fill some gaps I've felt in Umamusume optimization.</p>
<p>This site is very under construction. To demonstrate just how under construction it is, here is lorem ipsum:</p>
<p>
Lorem ipsum (/ ˌ l ɔː. r ə m ˈ ɪ p. s ə m/ LOR-əm IP-səm) is a dummy or placeholder text commonly used in graphic design,
publishing, and web development. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by
the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin.
The first two words are the truncation of dolorem ipsum ("pain itself"). Lorem ipsum's purpose is to permit a page layout to be
designed, independently of the copy that will subsequently populate it, or to demonstrate various fonts of a typeface without
meaningful text that could be distracting. Versions of the Lorem ipsum text have been used in typesetting since the 1960s, when
advertisements for Letraset transfer sheets popularized it. Lorem ipsum was introduced to the digital world in the mid-1980s,
when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word
processors, including Pages and Microsoft Word, have since adopted Lorem ipsum, as have many LaTeX packages, web content
</p>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { charaNames } from '$lib/data/character';
import { byChara, locations, groupPopulars } from '$lib/data/convo';
import CharaPick from '$lib/CharaPick.svelte';
const minSuggest = 8;
let charaID = $state(1001);
let convo = $state(1);
let options = $derived(byChara.global.get(charaID) ?? []);
let cur = $derived(options.find((c) => c.number === convo));
let suggested = $derived.by(() => {
if (cur == null) {
return [];
}
const u = groupPopulars.global[locations[cur.location].group].filter(
(s) => charaNames.get(s[0]) != null && s[0] !== cur.chara_1 && s[0] !== cur.chara_2 && s[0] !== cur.chara_3,
);
const r = u.length <= minSuggest ? u : u.filter((s) => s[1] >= u[minSuggest][1]);
return r.map(([chara_id, count]) => ({ chara_id, count }));
});
</script>
<h1 class="text-4xl">Lobby Conversations</h1>
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row">
<div class="m-4 flex-1 md:mt-3">
<CharaPick id="chara" class="w-full" label="Character" labelClass="hidden md:inline" bind:value={charaID} required />
</div>
<div class="m-4 flex-1 md:mt-3">
<label for="convo" class="hidden md:inline">Conversation</label>
<select id="convo" bind:value={convo} class="w-full">
{#each options as opt}
<option value={opt.number}>Slice of Life {opt.number}</option>
{/each}
</select>
</div>
</div>
{#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md">
<div class="mt-8 flex text-center text-lg">
<span class="flex-1"
>{charaNames.get(cur.chara_1)?.en ?? 'someone not a trainee'}{(cur.chara_2 ?? cur.chara_3) == null ? ' alone' : ''}</span
>
{#if cur.chara_2}
<span class="flex-1">{charaNames.get(cur.chara_2)?.en ?? 'someone not a trainee'}</span>
{/if}
{#if cur.chara_3}
<span class="flex-1">{charaNames.get(cur.chara_3)?.en ?? 'someone not a trainee'}</span>
{/if}
</div>
<div class="flex w-full text-center text-lg">
<span class="flex-1">at {locations[cur.location].name.en}</span>
</div>
</div>
<div class="mt-4 block text-center">
<span>Other characters who appear here most often:</span>
</div>
<div class="mt-4 grid text-center shadow-sm transition-shadow ease-in hover:shadow-md hover:ease-out md:grid-cols-4">
{#each suggested as s}
<span>{charaNames.get(s.chara_id)?.en}: {s.count}&#xd7;</span>
{/each}
</div>
<div class="mt-4 block text-center">
<span>
Set these characters to fixed positions (main, upgrades, story, races) to maximize the chance of getting this conversation.
</span>
</div>
{/if}

View File

@@ -0,0 +1,3 @@
<h1>Inheritance Chance</h1>
<p>Given a legacy, calculate the probability distribution of activation counts for each spark.</p>
<p>TODO</p>

View File

@@ -0,0 +1,42 @@
@import 'tailwindcss';
:root {
color-scheme: light dark;
}
html,
body {
height: 100%;
padding: 0;
margin: 0;
}
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
body {
background-color: light-dark(var(--color-mist-200), var(--color-mist-800));
color: light-dark(var(--color-amber-950), var(--color-amber-50));
}
p {
margin-bottom: calc(var(--spacing) * 4);
}
a {
color: light-dark(var(--color-sky-900), var(--color-sky-100));
}
nav > span > a {
color: light-dark(var(--color-amber-950), var(--color-amber-50));
}
a:hover {
border-bottom-width: 1px;
}
select {
background-color: light-dark(var(--color-mist-300), var(--color-mist-900));
padding: 0.5rem 0.75rem;
}

View File

@@ -0,0 +1,6 @@
<h1>Spark Generation Chance</h1>
<p>
Given a legacy, calculate the chance of generating each spark if you fulfill the conditions to do so, and the distribution of
total spark counts.
</p>
<p>TODO</p>

Some files were not shown because too many files have changed in this diff Show More