Compare commits
71 Commits
5a1194358b
...
astro-site
| Author | SHA1 | Date | |
|---|---|---|---|
| 012e33cded | |||
| 4429bbecd1 | |||
| 2099eeb97e | |||
| c22ed0dc83 | |||
| 494aeeb401 | |||
| 298b1368e8 | |||
| b20a1a5964 | |||
| af8e04473a | |||
| 351f606d29 | |||
| da376647af | |||
| 2ec8d9cfdb | |||
| 2dd75edc03 | |||
| e08580925d | |||
| 63659a4934 | |||
| af4e06411d | |||
| 4426925ebb | |||
| 8632bb8c3c | |||
| 7ff271ff2d | |||
| 5540bb2c4e | |||
| 9a73f2147b | |||
| 01b88994f3 | |||
| 424f65dc8a | |||
| cf814c6c72 | |||
| 7972bab46c | |||
| 3fa30903cd | |||
| 9ef568202c | |||
| b0e422ac01 | |||
| 2184515938 | |||
| 489457c63c | |||
| 9b3c9b22aa | |||
| 1f2824246f | |||
| 63e8327125 | |||
| e608363a24 | |||
| e3903e5312 | |||
| 5e7103befd | |||
| 0723fe0c6a | |||
| cbe08cd8a7 | |||
| db3e18e586 | |||
| 8fb29a953c | |||
| c00d3d0186 | |||
| a534975601 | |||
| b55e1bc200 | |||
| c58dbd19b0 | |||
| 2fcd608102 | |||
| 546f2db327 | |||
| 856c94723f | |||
| 2393bf2fa5 | |||
| bf06de0f5e | |||
| f3f070ca2b | |||
| 34edcf97a7 | |||
| 9dd18ed972 | |||
| 332cf3f13a | |||
| c5a1cdea5f | |||
| 542d4198e7 | |||
| 98afe7384a | |||
| e890108591 | |||
| 0126101b1b | |||
| 5bf2588d41 | |||
| a5f84754ea | |||
| 4bfb06b682 | |||
| 72b8bc9c6c | |||
| 1ae654c266 | |||
| 74ee76c5da | |||
| 36d27f1642 | |||
| 9469c2c7a2 | |||
| a8921e9cf6 | |||
| f9ad769d9f | |||
| ec2efee5d5 | |||
| d147d71519 | |||
| b22b77c535 | |||
| b98513864a |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
.astro
|
||||
.koka
|
||||
.vscode
|
||||
dist
|
||||
node_modules
|
||||
|
||||
6
astro.config.mjs
Normal file
6
astro.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
export default defineConfig({
|
||||
site: "https://zenno.sunturtle.xyz",
|
||||
srcDir: "./site",
|
||||
});
|
||||
1
cmd/horsebot/.gitignore
vendored
Normal file
1
cmd/horsebot/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
token
|
||||
10
cmd/horsebot/README.md
Normal file
10
cmd/horsebot/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# horsebot
|
||||
|
||||
Discord bot serving horse game data.
|
||||
|
||||
Production instance is named Zenno Rob Roy, because she has read all about Umamusume and is always happy to share her knowledge and give recommendations.
|
||||
|
||||
## Running
|
||||
|
||||
The bot always uses the Gateway API.
|
||||
If the `-http` argument is provided, it will also use the HTTP API, and `-key` must also be provided.
|
||||
56
cmd/horsebot/autocomplete/autocomplete.go
Normal file
56
cmd/horsebot/autocomplete/autocomplete.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package autocomplete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Set is an autocomplete set.
|
||||
type Set[V any] struct {
|
||||
keys []util.Chars
|
||||
vals []V
|
||||
}
|
||||
|
||||
// Add associates a value with a key in the autocomplete set.
|
||||
// The behavior is undefined if the key already has a value.
|
||||
func (s *Set[V]) Add(key string, val V) {
|
||||
k := util.ToChars([]byte(key))
|
||||
i, _ := slices.BinarySearchFunc(s.keys, k, func(a, b util.Chars) int {
|
||||
return bytes.Compare(a.Bytes(), b.Bytes())
|
||||
})
|
||||
s.keys = slices.Insert(s.keys, i, k)
|
||||
s.vals = slices.Insert(s.vals, i, val)
|
||||
}
|
||||
|
||||
// Find appends to r all values in the set with keys that key matches.
|
||||
func (s *Set[V]) Find(r []V, key string) []V {
|
||||
initFzf()
|
||||
var (
|
||||
p = []rune(key)
|
||||
|
||||
got []V
|
||||
t []algo.Result
|
||||
slab util.Slab
|
||||
)
|
||||
for i := range s.keys {
|
||||
res, _ := algo.FuzzyMatchV2(false, true, true, &s.keys[i], p, false, &slab)
|
||||
if res.Score <= 0 {
|
||||
continue
|
||||
}
|
||||
j, _ := slices.BinarySearchFunc(t, res, func(a, b algo.Result) int { return -cmp.Compare(a.Score, b.Score) })
|
||||
// Insert after all other matches with the same score for stability.
|
||||
for j < len(t) && t[j].Score == res.Score {
|
||||
j++
|
||||
}
|
||||
t = slices.Insert(t, j, res)
|
||||
got = slices.Insert(got, j, s.vals[i])
|
||||
}
|
||||
return append(r, got...)
|
||||
}
|
||||
|
||||
var initFzf = sync.OnceFunc(func() { algo.Init("default") })
|
||||
70
cmd/horsebot/autocomplete/autocomplete_test.go
Normal file
70
cmd/horsebot/autocomplete/autocomplete_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package autocomplete_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
|
||||
)
|
||||
|
||||
func these(s ...string) []string { return s }
|
||||
|
||||
func TestAutocomplete(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
add []string
|
||||
search string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
add: nil,
|
||||
search: "",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "exact",
|
||||
add: these("bocchi"),
|
||||
search: "bocchi",
|
||||
want: these("bocchi"),
|
||||
},
|
||||
{
|
||||
name: "extra",
|
||||
add: these("bocchi", "ryo", "nijika", "kita"),
|
||||
search: "bocchi",
|
||||
want: these("bocchi"),
|
||||
},
|
||||
{
|
||||
name: "short",
|
||||
add: these("bocchi", "ryo", "nijika", "kita"),
|
||||
search: "o",
|
||||
want: these("bocchi", "ryo"),
|
||||
},
|
||||
{
|
||||
name: "unrelated",
|
||||
add: these("bocchi", "ryo", "nijika", "kita"),
|
||||
search: "x",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "map",
|
||||
add: these("Corazón ☆ Ardiente"),
|
||||
search: "corazo",
|
||||
want: these("Corazón ☆ Ardiente"),
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var set autocomplete.Set[string]
|
||||
for _, s := range c.add {
|
||||
set.Add(s, s)
|
||||
}
|
||||
got := set.Find(nil, c.search)
|
||||
slices.Sort(c.want)
|
||||
slices.Sort(got)
|
||||
if !slices.Equal(c.want, got) {
|
||||
t.Errorf("wrong results: want %q, got %q", c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
cmd/horsebot/log.go
Normal file
55
cmd/horsebot/log.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/disgoorg/disgo/discord"
|
||||
"github.com/disgoorg/disgo/handler"
|
||||
)
|
||||
|
||||
func logMiddleware(next handler.Handler) handler.Handler {
|
||||
return func(e *handler.InteractionEvent) error {
|
||||
var msg string
|
||||
attrs := make([]slog.Attr, 0, 8)
|
||||
attrs = append(attrs,
|
||||
slog.Uint64("interaction", uint64(e.Interaction.ID())),
|
||||
slog.Uint64("user", uint64(e.Interaction.User().ID)),
|
||||
)
|
||||
if guild := e.Interaction.GuildID(); guild != nil {
|
||||
attrs = append(attrs, slog.String("guild", guild.String()))
|
||||
}
|
||||
switch i := e.Interaction.(type) {
|
||||
case discord.ApplicationCommandInteraction:
|
||||
msg = "command"
|
||||
attrs = append(attrs,
|
||||
slog.String("name", i.Data.CommandName()),
|
||||
slog.Int("type", int(i.Data.Type())),
|
||||
)
|
||||
switch data := i.Data.(type) {
|
||||
case discord.SlashCommandInteractionData:
|
||||
attrs = append(attrs, slog.String("path", data.CommandPath()))
|
||||
}
|
||||
|
||||
case discord.AutocompleteInteraction:
|
||||
msg = "autocomplete"
|
||||
attrs = append(attrs,
|
||||
slog.String("name", i.Data.CommandName),
|
||||
slog.String("path", i.Data.CommandPath()),
|
||||
slog.String("focus", i.Data.Focused().Name),
|
||||
)
|
||||
|
||||
case discord.ComponentInteraction:
|
||||
msg = "component"
|
||||
attrs = append(attrs,
|
||||
slog.Int("type", int(i.Data.Type())),
|
||||
slog.String("custom", i.Data.CustomID()),
|
||||
)
|
||||
|
||||
default:
|
||||
slog.WarnContext(e.Ctx, "unknown interaction", slog.Any("event", e))
|
||||
return nil
|
||||
}
|
||||
slog.LogAttrs(e.Ctx, slog.LevelInfo, msg, attrs...)
|
||||
return next(e)
|
||||
}
|
||||
}
|
||||
177
cmd/horsebot/main.go
Normal file
177
cmd/horsebot/main.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/disgoorg/disgo"
|
||||
"github.com/disgoorg/disgo/bot"
|
||||
"github.com/disgoorg/disgo/discord"
|
||||
"github.com/disgoorg/disgo/handler"
|
||||
"github.com/disgoorg/disgo/handler/middleware"
|
||||
"github.com/disgoorg/disgo/httpserver"
|
||||
"github.com/disgoorg/disgo/rest"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
dataDir string
|
||||
tokenFile string
|
||||
// http api options
|
||||
addr string
|
||||
route string
|
||||
pubkey string
|
||||
// logging options
|
||||
level slog.Level
|
||||
textfmt string
|
||||
)
|
||||
flag.StringVar(&dataDir, "data", "", "`dir`ectory containing exported json data")
|
||||
flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token")
|
||||
flag.StringVar(&addr, "http", "", "`address` to bind HTTP API server")
|
||||
flag.StringVar(&route, "route", "/interactions/callback", "`path` to serve HTTP API calls")
|
||||
flag.StringVar(&pubkey, "key", "", "Discord public key")
|
||||
flag.TextVar(&level, "log", slog.LevelInfo, "slog logging `level`")
|
||||
flag.StringVar(&textfmt, "log-format", "text", "slog logging `format`, text or json")
|
||||
flag.Parse()
|
||||
|
||||
var lh slog.Handler
|
||||
switch textfmt {
|
||||
case "text":
|
||||
lh = slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})
|
||||
case "json":
|
||||
lh = slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: level})
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "invalid log format %q, must be text or json", textfmt)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.SetDefault(slog.New(lh))
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
slog.Error("reading token", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
token = bytes.TrimSuffix(token, []byte{'\n'})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
r := handler.New()
|
||||
r.DefaultContext(func() context.Context { return ctx })
|
||||
r.Use(middleware.Go)
|
||||
r.Use(logMiddleware)
|
||||
r.Route("/skill", func(r handler.Router) {
|
||||
r.SlashCommand("/", skillSrv.slash)
|
||||
r.Autocomplete("/", skillSrv.autocomplete)
|
||||
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))
|
||||
client, err := disgo.New(string(token), opts...)
|
||||
if err != nil {
|
||||
slog.Error("building bot", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil {
|
||||
slog.Error("syncing commands", slog.Any("err", err))
|
||||
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")
|
||||
if err := client.OpenGateway(ctx); err != nil {
|
||||
slog.Error("starting gateway", slog.Any("err", err))
|
||||
stop()
|
||||
}
|
||||
slog.Info("ready")
|
||||
<-ctx.Done()
|
||||
stop()
|
||||
|
||||
ctx, stop = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer stop()
|
||||
client.Close(ctx)
|
||||
}
|
||||
|
||||
var commands = []discord.ApplicationCommandCreate{
|
||||
discord.SlashCommandCreate{
|
||||
Name: "skill",
|
||||
Description: "Umamusume skill data",
|
||||
Options: []discord.ApplicationCommandOption{
|
||||
discord.ApplicationCommandOptionString{
|
||||
Name: "query",
|
||||
Description: "Skill name or ID",
|
||||
Required: true,
|
||||
Autocomplete: true,
|
||||
},
|
||||
discord.ApplicationCommandOptionBool{
|
||||
Name: "share",
|
||||
Description: "Share the skill info",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func loadSkills(file string) ([]horse.Skill, error) {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var skills []horse.Skill
|
||||
if err := json.Unmarshal(b, &skills); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return skills, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
250
cmd/horsebot/skill.go
Normal file
250
cmd/horsebot/skill.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/disgoorg/disgo/discord"
|
||||
"github.com/disgoorg/disgo/handler"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
|
||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||
)
|
||||
|
||||
type skillServer struct {
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
top := "### " + skill.Name
|
||||
if skill.UniqueOwner != "" {
|
||||
top += "\n-# " + skill.UniqueOwner
|
||||
}
|
||||
r := discord.NewContainer(
|
||||
discord.NewTextDisplay(top),
|
||||
discord.NewSmallSeparator(),
|
||||
)
|
||||
var skilltype string
|
||||
switch {
|
||||
case skill.Rarity == 3, skill.Rarity == 4, skill.Rarity == 5:
|
||||
// unique of various star levels
|
||||
r.AccentColor = 0xaca4d4
|
||||
skilltype = "Unique Skill"
|
||||
case skill.UniqueOwner != "":
|
||||
r.AccentColor = 0xcccccc
|
||||
skilltype = "Inherited Unique"
|
||||
case skill.Rarity == 2:
|
||||
// rare (gold)
|
||||
r.AccentColor = 0xd7c25b
|
||||
skilltype = "Rare Skill"
|
||||
case skill.GroupRate == -1:
|
||||
// negative (purple) skill
|
||||
r.AccentColor = 0x9151d4
|
||||
skilltype = "Negative Skill"
|
||||
case !skill.WitCheck:
|
||||
// should be passive (green)
|
||||
r.AccentColor = 0x66ae1c
|
||||
skilltype = "Passive Skill"
|
||||
case isDebuff(skill):
|
||||
// debuff (red)
|
||||
r.AccentColor = 0xe34747
|
||||
skilltype = "Debuff Skill"
|
||||
case skill.Rarity == 1:
|
||||
// common (white)
|
||||
r.AccentColor = 0xcccccc
|
||||
skilltype = "Common Skill"
|
||||
}
|
||||
text := make([]string, 0, 3)
|
||||
abils := make([]string, 0, 3)
|
||||
for _, act := range skill.Activations {
|
||||
text, abils = text[:0], abils[:0]
|
||||
if act.Precondition != "" {
|
||||
text = append(text, "Precondition: "+formatCondition(act.Precondition))
|
||||
}
|
||||
text = append(text, "Condition: "+formatCondition(act.Condition))
|
||||
var t string
|
||||
switch {
|
||||
case act.Duration < 0:
|
||||
// passive; do nothing
|
||||
case act.Duration == 0:
|
||||
t = "Instantaneous "
|
||||
case act.Duration >= 500e4:
|
||||
t = "Permanent "
|
||||
case act.DurScale == horse.DurationDirect:
|
||||
t = "For " + act.Duration.String() + "s, "
|
||||
default:
|
||||
t = "For " + act.Duration.String() + "s " + act.DurScale.String() + ", "
|
||||
}
|
||||
for _, a := range act.Abilities {
|
||||
abils = append(abils, a.String())
|
||||
}
|
||||
t += strings.Join(abils, ", ")
|
||||
if act.Cooldown > 0 && act.Cooldown < 500e4 {
|
||||
t += " on " + act.Cooldown.String() + "s cooldown"
|
||||
}
|
||||
text = append(text, t)
|
||||
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, skill.SPCost, skill.GradeValue, skill.ID)
|
||||
r.Components = append(r.Components, discord.NewSmallSeparator(), l)
|
||||
rel := make([]horse.Skill, 0, 4)
|
||||
group := s.groups[skill.Group]
|
||||
for _, id := range [...]horse.SkillID{group.Skill1, group.Skill2, group.Skill3, group.SkillBad} {
|
||||
if id != 0 {
|
||||
rel = append(rel, s.skills[id])
|
||||
}
|
||||
}
|
||||
if len(rel) > 1 {
|
||||
opts := make([]discord.StringSelectMenuOption, 0, 4)
|
||||
for _, rs := range rel {
|
||||
name := rs.Name
|
||||
emoji := "⚪"
|
||||
switch rs.Rarity {
|
||||
case 1:
|
||||
if rs.UniqueOwner != "" {
|
||||
name += " (Inherited)"
|
||||
}
|
||||
case 2:
|
||||
emoji = "🟡"
|
||||
case 3, 4, 5:
|
||||
emoji = "🟣"
|
||||
default:
|
||||
emoji = "⁉️"
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func formatCondition(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", " & ")
|
||||
if strings.ContainsRune(s, '@') {
|
||||
return "```\n" + strings.ReplaceAll(s, "@", "\n@\n") + "```"
|
||||
}
|
||||
return "`" + s + "`"
|
||||
}
|
||||
|
||||
func isDebuff(s horse.Skill) bool {
|
||||
for _, act := range s.Activations {
|
||||
for _, a := range act.Abilities {
|
||||
if a.Value < 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
418
cmd/horsegen/generate.go
Normal file
418
cmd/horsegen/generate.go
Normal 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(®ion, "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
|
||||
}
|
||||
60
cmd/horsegen/sql/affinity.sql
Normal file
60
cmd/horsegen/sql/affinity.sql
Normal 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
|
||||
10
cmd/horsegen/sql/conversation.sql
Normal file
10
cmd/horsegen/sql/conversation.sql
Normal 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
|
||||
14
cmd/horsegen/sql/race.sql
Normal file
14
cmd/horsegen/sql/race.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
WITH race_names AS (
|
||||
SELECT "index" AS id, "text" AS name FROM text_data WHERE category = 33
|
||||
)
|
||||
SELECT
|
||||
race.id,
|
||||
race_names.name,
|
||||
race.grade,
|
||||
race.thumbnail_id,
|
||||
MIN(race.id) OVER (PARTITION BY race_names.name) AS "primary",
|
||||
ROW_NUMBER() OVER (PARTITION BY race_names.name ORDER BY race.id) - 1 AS "alternate"
|
||||
FROM race
|
||||
JOIN race_names ON race.id = race_names.id
|
||||
WHERE race."group" = 1
|
||||
ORDER BY race.id
|
||||
20
cmd/horsegen/sql/saddle.sql
Normal file
20
cmd/horsegen/sql/saddle.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
WITH saddle_names AS (
|
||||
SELECT "index" AS id, "text" AS name
|
||||
FROM text_data
|
||||
WHERE category = 111
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
n.name,
|
||||
ri1.id AS race1,
|
||||
IFNULL(ri2.id, 0) AS race2,
|
||||
IFNULL(ri3.id, 0) AS race3,
|
||||
s.win_saddle_type,
|
||||
MIN(s.id) OVER (PARTITION BY n.name) AS "primary",
|
||||
ROW_NUMBER() OVER (PARTITION BY n.name ORDER BY s.id) - 1 AS "alternate"
|
||||
FROM single_mode_wins_saddle s
|
||||
JOIN race_instance ri1 ON s.race_instance_id_1 = ri1.id
|
||||
LEFT JOIN race_instance ri2 ON s.race_instance_id_2 = ri2.id
|
||||
LEFT JOIN race_instance ri3 ON s.race_instance_id_3 = ri3.id
|
||||
LEFT JOIN saddle_names n ON s.id = n.id
|
||||
ORDER BY s.id
|
||||
17
cmd/horsegen/sql/scenario.sql
Normal file
17
cmd/horsegen/sql/scenario.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
WITH scenario_name AS (
|
||||
SELECT "index" AS id, "text" AS name
|
||||
FROM text_data
|
||||
WHERE category = 237
|
||||
), scenario_title AS (
|
||||
SELECT "index" AS id, "text" AS title
|
||||
FROM text_data
|
||||
WHERE category = 119
|
||||
)
|
||||
SELECT
|
||||
sc.id,
|
||||
n.name,
|
||||
t.title
|
||||
FROM single_mode_scenario sc
|
||||
JOIN scenario_name n ON sc.id = n.id
|
||||
JOIN scenario_title t ON sc.id = t.id
|
||||
ORDER BY sc.id
|
||||
17
cmd/horsegen/sql/skill-group.sql
Normal file
17
cmd/horsegen/sql/skill-group.sql
Normal 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
|
||||
@@ -12,6 +12,21 @@ WITH skill_names AS (
|
||||
FROM skill_data d
|
||||
JOIN skill_names n ON d.id = n.id
|
||||
WHERE group_rate = 1
|
||||
), card_name AS (
|
||||
SELECT
|
||||
"index" AS "id",
|
||||
"text" AS "name"
|
||||
FROM text_data n
|
||||
WHERE category = 4
|
||||
), card_unique AS (
|
||||
SELECT DISTINCT
|
||||
ss.skill_id1 AS unique_id,
|
||||
card_name.id AS owner_id,
|
||||
card_name.name
|
||||
FROM card_data card
|
||||
JOIN card_name ON card.id = card_name.id
|
||||
JOIN card_rarity_data rd ON card.id = rd.card_id
|
||||
JOIN skill_set ss ON rd.skill_set = ss.id
|
||||
)
|
||||
SELECT
|
||||
d.id,
|
||||
@@ -30,6 +45,7 @@ SELECT
|
||||
d.precondition_1,
|
||||
d.condition_1,
|
||||
d.float_ability_time_1,
|
||||
d.ability_time_usage_1,
|
||||
d.float_cooldown_time_1,
|
||||
d.ability_type_1_1,
|
||||
d.ability_value_usage_1_1,
|
||||
@@ -49,6 +65,7 @@ SELECT
|
||||
d.precondition_2,
|
||||
d.condition_2,
|
||||
d.float_ability_time_2,
|
||||
d.ability_time_usage_2,
|
||||
d.float_cooldown_time_2,
|
||||
d.ability_type_2_1,
|
||||
d.ability_value_usage_2_1,
|
||||
@@ -67,6 +84,8 @@ SELECT
|
||||
d.target_value_2_3,
|
||||
IFNULL(p.need_skill_point, 0) AS sp_cost,
|
||||
d.unique_skill_id_1,
|
||||
COALESCE(u.owner_id, iu.owner_id, 0) AS unique_owner_id,
|
||||
COALESCE(u.name, iu.name, '') AS unique_owner,
|
||||
d.icon_id,
|
||||
ROW_NUMBER() OVER (ORDER BY d.id) - 1 AS "index"
|
||||
FROM skill_data d
|
||||
@@ -74,4 +93,6 @@ FROM skill_data d
|
||||
LEFT JOIN skill_data ud ON d.unique_skill_id_1 = ud.id
|
||||
LEFT JOIN skill_groups g ON d.group_id = g.group_id
|
||||
LEFT JOIN single_mode_skill_need_point p ON d.id = p.id
|
||||
LEFT JOIN card_unique u ON d.id = u.unique_id
|
||||
LEFT JOIN card_unique iu ON d.unique_skill_id_1 = iu.unique_id
|
||||
ORDER BY d.id
|
||||
9
cmd/horsegen/sql/spark-effect.sql
Normal file
9
cmd/horsegen/sql/spark-effect.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
SELECT
|
||||
factor_group_id,
|
||||
effect_id,
|
||||
target_type,
|
||||
value_1,
|
||||
value_2
|
||||
FROM succession_factor_effect
|
||||
WHERE factor_group_id NOT IN (40001) -- exclude Carnival Bonus
|
||||
ORDER BY factor_group_id, effect_id, id
|
||||
20
cmd/horsegen/sql/spark.sql
Normal file
20
cmd/horsegen/sql/spark.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
WITH spark AS (
|
||||
SELECT
|
||||
n."index" AS "id",
|
||||
n."text" AS "name",
|
||||
d."text" AS "description"
|
||||
FROM text_data n
|
||||
LEFT JOIN text_data d ON n."index" = d."index" AND d."category" = 172
|
||||
WHERE n.category = 147
|
||||
)
|
||||
SELECT
|
||||
sf.factor_id,
|
||||
spark.name,
|
||||
spark.description,
|
||||
sf.factor_group_id,
|
||||
sf.rarity,
|
||||
sf.factor_type
|
||||
FROM spark
|
||||
JOIN succession_factor sf ON spark.id = sf.factor_id
|
||||
WHERE sf.factor_type != 7 -- exclude Carnival Bonus
|
||||
ORDER BY sf.factor_id
|
||||
59
cmd/horsegen/sql/uma.sql
Normal file
59
cmd/horsegen/sql/uma.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
WITH uma_name AS (
|
||||
SELECT "index" AS id, "text" AS name
|
||||
FROM text_data
|
||||
WHERE category = 4
|
||||
), uma_variant AS (
|
||||
SELECT "index" AS id, "text" AS variant
|
||||
FROM text_data
|
||||
WHERE category = 5
|
||||
), chara_name AS (
|
||||
SELECT "index" AS id, "text" AS name
|
||||
FROM text_data
|
||||
WHERE category = 6
|
||||
), skills AS (
|
||||
SELECT
|
||||
uma.id,
|
||||
s.skill_id,
|
||||
s.need_rank,
|
||||
ROW_NUMBER() OVER (PARTITION BY s.available_skill_set_id, s.need_rank) AS idx
|
||||
FROM card_data uma
|
||||
LEFT JOIN available_skill_set s ON uma.available_skill_set_id = s.available_skill_set_id
|
||||
)
|
||||
SELECT
|
||||
uma.card_id,
|
||||
card_data.chara_id,
|
||||
n.name,
|
||||
v.variant,
|
||||
c.name AS chara_name,
|
||||
uma.proper_distance_short,
|
||||
uma.proper_distance_mile,
|
||||
uma.proper_distance_middle,
|
||||
uma.proper_distance_long,
|
||||
uma.proper_running_style_nige,
|
||||
uma.proper_running_style_senko,
|
||||
uma.proper_running_style_sashi,
|
||||
uma.proper_running_style_oikomi,
|
||||
uma.proper_ground_turf,
|
||||
uma.proper_ground_dirt,
|
||||
su.skill_id1 AS unique_skill,
|
||||
s1.skill_id AS skill1,
|
||||
s2.skill_id AS skill2,
|
||||
s3.skill_id AS skill3,
|
||||
sp2.skill_id AS skill_pl2,
|
||||
sp3.skill_id AS skill_pl3,
|
||||
sp4.skill_id AS skill_pl4,
|
||||
sp5.skill_id AS skill_pl5
|
||||
FROM card_data
|
||||
JOIN card_rarity_data uma ON card_data.id = uma.card_id
|
||||
JOIN chara_name c ON card_data.chara_id = c.id
|
||||
JOIN skill_set su ON uma.skill_set = su.id
|
||||
JOIN skills s1 ON uma.card_id = s1.id AND s1.need_rank = 0 AND s1.idx = 1
|
||||
JOIN skills s2 ON uma.card_id = s2.id AND s2.need_rank = 0 AND s2.idx = 2
|
||||
JOIN skills s3 ON uma.card_id = s3.id AND s3.need_rank = 0 AND s3.idx = 3
|
||||
JOIN skills sp2 ON uma.card_id = sp2.id AND sp2.need_rank = 2
|
||||
JOIN skills sp3 ON uma.card_id = sp3.id AND sp3.need_rank = 3
|
||||
JOIN skills sp4 ON uma.card_id = sp4.id AND sp4.need_rank = 4
|
||||
JOIN skills sp5 ON uma.card_id = sp5.id AND sp5.need_rank = 5
|
||||
LEFT JOIN uma_name n ON uma.card_id = n.id
|
||||
LEFT JOIN uma_variant v ON uma.card_id = v.id
|
||||
WHERE uma.rarity = 5
|
||||
1903
doc/2026-01-22-global.diff
Normal file
1903
doc/2026-01-22-global.diff
Normal file
File diff suppressed because it is too large
Load Diff
123780
doc/2026-01-22-global.sql
Normal file
123780
doc/2026-01-22-global.sql
Normal file
File diff suppressed because it is too large
Load Diff
1574
doc/2026-01-29-global.diff
Normal file
1574
doc/2026-01-29-global.diff
Normal file
File diff suppressed because it is too large
Load Diff
137122
doc/2026-03-12-global.sql
Normal file
137122
doc/2026-03-12-global.sql
Normal file
File diff suppressed because it is too large
Load Diff
211
doc/README.md
211
doc/README.md
@@ -6,12 +6,14 @@ This file is my notes from exploring the database.
|
||||
|
||||
# text_data categories
|
||||
|
||||
- 6 is character names, 4 is [variant] character name, 5 is [variant], 14 is variant
|
||||
- 6 is character names, 4 is [variant] character name, 5 is [variant], 14 is clothing names
|
||||
- 47 is skill names, 48 is skill descriptions
|
||||
- 75 is support card names incl. variant, 76 is support card variant, 77 is support card character
|
||||
- 147 is spark names, 172 is spark descriptions
|
||||
- 33 is race names by race id, 28 is race names by race instance id, 31 is race courses
|
||||
- 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?
|
||||
- 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)
|
||||
|
||||
@@ -23,6 +25,11 @@ factor_type:
|
||||
- 5 race
|
||||
- 4 skill
|
||||
- 6 scenario
|
||||
- 7 carnival bonus
|
||||
- 10 surface gene (white, on jp)
|
||||
- 8 distance gene (white)
|
||||
- 11 style gene (white)
|
||||
- 9 hidden (white, for things like many wins in west japan, summer sprint series, &c.)
|
||||
- 3 unique
|
||||
|
||||
target_type:
|
||||
@@ -31,6 +38,8 @@ target_type:
|
||||
- 3 power
|
||||
- 4 guts
|
||||
- 5 wit
|
||||
- 6 skill points
|
||||
- 7 random stat; value 1 is amount, value 2 is always 1?
|
||||
- 11 turf; value 1 is number of levels (1 or 2), value 2 is 0
|
||||
- 12 dirt
|
||||
- 21 front
|
||||
@@ -41,16 +50,21 @@ target_type:
|
||||
- 32 mile
|
||||
- 33 medium
|
||||
- 34 long
|
||||
- 41 is skill; value 1 is skill id, value 2 is hint level (1-5)
|
||||
- 41 skill; value 1 is skill id, value 2 is hint level (1-5)
|
||||
- 51 carnival bonus; value 1 is skill id, value 2 is 1
|
||||
- 61 speed cap; value 1 is presumably amount but the numbers are very small, 1-4; value 2 is 0
|
||||
- 62 stam cap
|
||||
- 63 power cap
|
||||
- 64 guts cap
|
||||
- 65 wit cap
|
||||
|
||||
grade is 2 for unique sparks and 1 otherwise.
|
||||
|
||||
every possible result has a row in succession_factor_effect.
|
||||
effect_id distinguishes possibilities; factors with multiple effects (race and scenario sparks) have multiple rows with equal effect_id.
|
||||
effect_group_id determines the pmf, but no tables have non-empty joins with the same column name, so the distribution values are mystery.
|
||||
even searching for 51 and 52 (effect group ids for 1\* and 2\* race and scenario sparks) on consecutive lines gives nothing.
|
||||
|
||||
sf.grade = 1 unless it is a unique (green) spark, then sf.grade = 2.
|
||||
=> sf.grade = 2 iff sf.factor_type = 3
|
||||
|
||||
getting all interesting spark data, fully expanded with all effects:
|
||||
```sql
|
||||
WITH spark AS (
|
||||
@@ -128,8 +142,8 @@ race sparks with skills always give +1, skill sparks always give +1-5, unique sp
|
||||
|
||||
- single_mode_skill_need_point is base number of skill points to buy each skill
|
||||
- support card skill hints are defined in single_mode_hint_gain
|
||||
- skill_set is NOT trainee skills, seems to be npcs
|
||||
- available_skill_set has trainee starting skills
|
||||
- skill_set includes trainee unique starting skills, among many other things
|
||||
- available_skill_set has trainee starting skills other than their uniques
|
||||
|
||||
skill categories:
|
||||
- 0 passive
|
||||
@@ -179,12 +193,41 @@ target types:
|
||||
- 22 specific character, target value is character id
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
- group 1, grade: g1 100, g2 200, g3 300, op 400, pre-op 700
|
||||
@@ -193,6 +236,22 @@ seems to be activate_lot = 1 means wit check, 0 means guaranteed
|
||||
|
||||
single_mode_wins_saddle defines titles (classic triple crown, tenno sweep, &c.) using win_saddle_type = 0
|
||||
|
||||
race_instance is a combination of race, npc group, race date (month*100 + day), and time of day.
|
||||
it isn't actually anything i care about.
|
||||
|
||||
which is to say, what i do care about is mapping races to each turn they're offered, and having a "race instance" enum like Hopeful-Stakes-Junior, Yasuda-Kinen-Classic, &c.
|
||||
|
||||
single_mode_program defines the race instances available for each turn, but the year is expressed a bit weirdly in the race_permission column:
|
||||
- 1 = junior year
|
||||
- 2 = classic year
|
||||
- 3 = classic and senior year
|
||||
- 4 = senior year
|
||||
- 5 = ura finale
|
||||
|
||||
grade_rate_id appears to be consistently 800 iff maiden race and 900 iff debut race, but the values particularly for g1s are all over the place.
|
||||
recommend_class_id appears to be consistently 1 iff maiden race or debut race, 2 iff pre-op, 3 iff op; but other values are confusing.
|
||||
so, it doesn't seem like there's a particular flag that identifies maiden races, despite the restrictions on when they appear in the ui.
|
||||
|
||||
# trainee definitions
|
||||
|
||||
- card_data has universal trainee stats: base skill set, stat growth bonuses ("talent"), default running style
|
||||
@@ -200,7 +259,135 @@ single_mode_wins_saddle defines titles (classic triple crown, tenno sweep, &c.)
|
||||
- card_talent_upgrade has costs to increase potential level, but it doesn't seem to have skill sets
|
||||
- card_talent_hint_upgrade has costs to raise hint levels, but it's actually universal, only six rows
|
||||
- single_mode_route_race is career goals (not only races)
|
||||
- available_skill_set has starting skills including those unlocked by potential level given under need_rank (0 for pl1, 2 for pl2)
|
||||
|
||||
# unrelated to everything
|
||||
# lobby conversations!!!
|
||||
|
||||
try doober with E long, all-seeing eyes, gold recovery, and lots of stamina running in g3 diamond stakes senior year late february
|
||||
table is home_story_trigger.
|
||||
|
||||
pos_id values:
|
||||
- 110 right side, toward the front
|
||||
- 120 same, but two characters
|
||||
- 130 same, but three characters
|
||||
- 210 left side table
|
||||
- 220
|
||||
- 310 center back seat
|
||||
- 410 center posters
|
||||
- 420
|
||||
- 430
|
||||
- 510 left school map
|
||||
- 520
|
||||
- 530
|
||||
|
||||
num is how many characters are involved, but also can just check chara_id_{1,2,3} for nonzero.
|
||||
|
||||
unsure what condition_type is.
|
||||
values of 2 and 3 always have two or three characters, and values of 4 (jp only) always have three, but 0 and 1 can have any number.
|
||||
there's no requirement for stories like having a horse at all, much less an affinity level.
|
||||
|
||||
gallery_chara_id is the character whose conversation it is; chara_id_{1,2,3} are the characters involved.
|
||||
gallery_chara_id is always one of the three, but it can be any one of the three.
|
||||
disp_order then is the conversation number within their gallery.
|
||||
|
||||
getting all conversation data:
|
||||
```sql
|
||||
WITH chara_name AS (
|
||||
SELECT "index" AS id, "text" AS name
|
||||
FROM text_data
|
||||
WHERE category = 6
|
||||
), convo_loc_names AS (
|
||||
SELECT 110 AS pos_id, 'right side front' AS name UNION ALL
|
||||
SELECT 120 AS pos_id, 'right side front' AS name UNION ALL
|
||||
SELECT 130 AS pos_id, 'right side front' AS name UNION ALL
|
||||
SELECT 210 AS pos_id, 'left side table' AS name UNION ALL
|
||||
SELECT 220 AS pos_id, 'left side table' AS name UNION ALL
|
||||
SELECT 310 AS pos_id, 'center back seat' AS name UNION ALL
|
||||
SELECT 410 AS pos_id, 'center posters' AS name UNION ALL
|
||||
SELECT 420 AS pos_id, 'center posters' AS name UNION ALL
|
||||
SELECT 430 AS pos_id, 'center posters' AS name UNION ALL
|
||||
SELECT 510 AS pos_id, 'left side school map' AS name UNION ALL
|
||||
SELECT 520 AS pos_id, 'left side school map' AS name UNION ALL
|
||||
SELECT 530 AS pos_id, 'left side school map' AS name
|
||||
)
|
||||
SELECT
|
||||
n.name,
|
||||
s.disp_order,
|
||||
l.name,
|
||||
c1.name,
|
||||
c2.name,
|
||||
c3.name,
|
||||
s.condition_type
|
||||
FROM home_story_trigger s
|
||||
LEFT JOIN chara_name n ON s.gallery_chara_id = n.id
|
||||
LEFT JOIN chara_name c1 ON s.chara_id_1 = c1.id
|
||||
LEFT JOIN chara_name c2 ON s.chara_id_2 = c2.id
|
||||
LEFT JOIN chara_name c3 ON s.chara_id_3 = c3.id
|
||||
LEFT JOIN convo_loc_names l ON s.pos_id = l.pos_id
|
||||
ORDER BY s.gallery_chara_id, s.disp_order
|
||||
```
|
||||
|
||||
# update diffs
|
||||
|
||||
complete list of tables with inserts in both the 2026-01-15 update adding fine motion, manhattan cafe ssr, inari one sr and the 2026-01-22 update adding tamamo cross and main story 5:
|
||||
|
||||
- announce_character
|
||||
- announce_data
|
||||
- available_skill_set
|
||||
- background_data
|
||||
- banner_data
|
||||
- campaign_chara_story_schedule
|
||||
- campaign_data
|
||||
- campaign_single_race_add_data
|
||||
- campaign_single_race_add_reward
|
||||
- card_data
|
||||
- card_rarity_data
|
||||
- card_talent_upgrade
|
||||
- champions_news_chara_comment
|
||||
- chara_category_motion
|
||||
- character_system_lottery
|
||||
- character_system_text
|
||||
- chara_motion_set
|
||||
- chara_story_data
|
||||
- dress_data
|
||||
- gacha_available
|
||||
- gacha_data
|
||||
- gacha_exchange
|
||||
- gacha_top_bg
|
||||
- home_story_trigger
|
||||
- home_walk_group
|
||||
- honor_data
|
||||
- item_exchange
|
||||
- item_place
|
||||
- jukebox_chara_tag_data
|
||||
- jukebox_comment
|
||||
- jukebox_reaction_data
|
||||
- mission_data
|
||||
- nickname
|
||||
- piece_data
|
||||
- race
|
||||
- race_bgm_cutin_extension_time
|
||||
- race_instance
|
||||
- race_jikkyo_base
|
||||
- race_jikkyo_race
|
||||
- race_jikkyo_trigger
|
||||
- single_mode_chara_program
|
||||
- single_mode_conclusion_set
|
||||
- single_mode_event_production
|
||||
- single_mode_hint_gain
|
||||
- single_mode_npc
|
||||
- single_mode_rival
|
||||
- single_mode_route
|
||||
- single_mode_route_race
|
||||
- single_mode_scout_chara
|
||||
- single_mode_skill_need_point
|
||||
- single_mode_story_data
|
||||
- single_mode_tag_card_pos
|
||||
- skill_data
|
||||
- skill_set
|
||||
- succession_factor
|
||||
- succession_factor_effect
|
||||
- succession_relation_member
|
||||
- support_card_data
|
||||
- support_card_effect_table
|
||||
- support_card_unique_effect
|
||||
- text_data
|
||||
|
||||
8
generate.sh
Executable file
8
generate.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -ex
|
||||
|
||||
go run ./horsegen "$@"
|
||||
go generate ./horse/...
|
||||
go fmt ./...
|
||||
go test ./...
|
||||
136869
global/affinity.json
Normal file
136869
global/affinity.json
Normal file
File diff suppressed because it is too large
Load Diff
222
global/character.json
Normal file
222
global/character.json
Normal file
@@ -0,0 +1,222 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
2326
global/conversation.json
Normal file
2326
global/conversation.json
Normal file
File diff suppressed because it is too large
Load Diff
1712
global/race.json
Normal file
1712
global/race.json
Normal file
File diff suppressed because it is too large
Load Diff
1420
global/saddle.json
Normal file
1420
global/saddle.json
Normal file
File diff suppressed because it is too large
Load Diff
17
global/scenario.json
Normal file
17
global/scenario.json
Normal 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"
|
||||
}
|
||||
]
|
||||
1741
global/skill-group.json
Normal file
1741
global/skill-group.json
Normal file
File diff suppressed because it is too large
Load Diff
15650
global/skill.json
Normal file
15650
global/skill.json
Normal file
File diff suppressed because it is too large
Load Diff
36098
global/spark.json
Normal file
36098
global/spark.json
Normal file
File diff suppressed because it is too large
Load Diff
1706
global/uma.json
Normal file
1706
global/uma.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
16
go.mod
16
go.mod
@@ -1,20 +1,30 @@
|
||||
module git.sunturtle.xyz/zephyr/horse
|
||||
|
||||
go 1.24.1
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
golang.org/x/sync v0.14.0
|
||||
github.com/disgoorg/disgo v0.19.0-rc.15
|
||||
github.com/junegunn/fzf v0.67.0
|
||||
golang.org/x/sync v0.20.0
|
||||
zombiezen.com/go/sqlite v1.4.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/disgoorg/json/v2 v2.0.0 // indirect
|
||||
github.com/disgoorg/omit v1.0.0 // indirect
|
||||
github.com/disgoorg/snowflake/v2 v2.0.3 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
40
go.sum
40
go.sum
@@ -1,28 +1,56 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disgoorg/disgo v0.19.0-rc.15 h1:x0NsV2gcbdjwuztsg2wYXw76p1Cpc8f6ByDrkPcfQtU=
|
||||
github.com/disgoorg/disgo v0.19.0-rc.15/go.mod h1:14mgXzenkJqifkDmsEgU0zI1di6jNXodwX6L8geW33A=
|
||||
github.com/disgoorg/json/v2 v2.0.0 h1:U16yy/ARK7/aEpzjjqK1b/KaqqGHozUdeVw/DViEzQI=
|
||||
github.com/disgoorg/json/v2 v2.0.0/go.mod h1:jZTBC0nIE1WeetSEI3/Dka8g+qglb4FPVmp5I5HpEfI=
|
||||
github.com/disgoorg/omit v1.0.0 h1:y0LkVUOyUHT8ZlnhIAeOZEA22UYykeysK8bLJ0SfT78=
|
||||
github.com/disgoorg/omit v1.0.0/go.mod h1:RTmSARkf6PWT/UckwI0bV8XgWkWQoPppaT01rYKLcFQ=
|
||||
github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro=
|
||||
github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/junegunn/fzf v0.67.0 h1:naiOdIkV5/ZCfHgKQIV/f5YDWowl95G6yyOQqW8FeSo=
|
||||
github.com/junegunn/fzf v0.67.0/go.mod h1:xlXX2/rmsccKQUnr9QOXPDi5DyV9cM0UjKy/huScBeE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI=
|
||||
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
|
||||
@@ -9,21 +9,54 @@ func _() {
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[ValueUsageDirect-1]
|
||||
_ = x[ValueUsageTeamSpeed-2]
|
||||
_ = x[ValueUsageTeamStamina-3]
|
||||
_ = x[ValueUsageTeamPower-4]
|
||||
_ = x[ValueUsageTeamGuts-5]
|
||||
_ = x[ValueUsageTeamWit-6]
|
||||
_ = x[ValueUsageSkillCount-2]
|
||||
_ = x[ValueUsageTeamSpeed-3]
|
||||
_ = x[ValueUsageTeamStamina-4]
|
||||
_ = x[ValueUsageTeamPower-5]
|
||||
_ = x[ValueUsageTeamGuts-6]
|
||||
_ = x[ValueUsageTeamWit-7]
|
||||
_ = x[ValueUsageRandom-8]
|
||||
_ = x[ValueUsageRandom2-9]
|
||||
_ = x[ValueUsageClimax-10]
|
||||
_ = x[ValueUsageMaxStat-13]
|
||||
_ = x[ValueUsageGreenCount-14]
|
||||
_ = x[ValueUsageDistAdd-19]
|
||||
_ = x[ValueUsageMidSideBlock-20]
|
||||
_ = x[ValueUsageSpeed-22]
|
||||
_ = x[ValueUsageSpeed2-23]
|
||||
_ = x[ValueUsageArcPotential-24]
|
||||
_ = x[ValueUsageMaxLead-25]
|
||||
}
|
||||
|
||||
const _AbilityValueUsage_name = "directlyscaling with team Speedscaling with team Staminascaling with team Powerscaling with team Gutsscaling with team Wit"
|
||||
const (
|
||||
_AbilityValueUsage_name_0 = "directlyscaling with the number of skillsscaling with team Speedscaling with team Staminascaling with team Powerscaling with team Gutsscaling with team Witwith a random 0× to 0.04× multiplierwith a random 0× to 0.04× multiplierscaling with the number of races won in training"
|
||||
_AbilityValueUsage_name_1 = "scaling with the highest raw statscaling with the number of Passive skills activated"
|
||||
_AbilityValueUsage_name_2 = "plus extra when far from the leadscaling with mid-race phase blocked side time"
|
||||
_AbilityValueUsage_name_3 = "scaling with overall speedscaling with overall speedscaling with L'Arc global potentialscaling with the longest lead obtained in the first ⅔"
|
||||
)
|
||||
|
||||
var _AbilityValueUsage_index = [...]uint8{0, 8, 31, 56, 79, 101, 122}
|
||||
var (
|
||||
_AbilityValueUsage_index_0 = [...]uint16{0, 8, 41, 64, 89, 112, 134, 155, 193, 231, 279}
|
||||
_AbilityValueUsage_index_1 = [...]uint8{0, 33, 84}
|
||||
_AbilityValueUsage_index_2 = [...]uint8{0, 33, 78}
|
||||
_AbilityValueUsage_index_3 = [...]uint8{0, 26, 52, 87, 142}
|
||||
)
|
||||
|
||||
func (i AbilityValueUsage) String() string {
|
||||
idx := int(i) - 1
|
||||
if i < 1 || idx >= len(_AbilityValueUsage_index)-1 {
|
||||
switch {
|
||||
case 1 <= i && i <= 10:
|
||||
i -= 1
|
||||
return _AbilityValueUsage_name_0[_AbilityValueUsage_index_0[i]:_AbilityValueUsage_index_0[i+1]]
|
||||
case 13 <= i && i <= 14:
|
||||
i -= 13
|
||||
return _AbilityValueUsage_name_1[_AbilityValueUsage_index_1[i]:_AbilityValueUsage_index_1[i+1]]
|
||||
case 19 <= i && i <= 20:
|
||||
i -= 19
|
||||
return _AbilityValueUsage_name_2[_AbilityValueUsage_index_2[i]:_AbilityValueUsage_index_2[i+1]]
|
||||
case 22 <= i && i <= 25:
|
||||
i -= 22
|
||||
return _AbilityValueUsage_name_3[_AbilityValueUsage_index_3[i]:_AbilityValueUsage_index_3[i+1]]
|
||||
default:
|
||||
return "AbilityValueUsage(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _AbilityValueUsage_name[_AbilityValueUsage_index[idx]:_AbilityValueUsage_index[idx+1]]
|
||||
}
|
||||
|
||||
31
horse/aptitudelevel_string.go
Normal file
31
horse/aptitudelevel_string.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Code generated by "stringer -type AptitudeLevel -trimprefix AptitudeLv"; 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[AptitudeLvG-1]
|
||||
_ = x[AptitudeLvF-2]
|
||||
_ = x[AptitudeLvE-3]
|
||||
_ = x[AptitudeLvD-4]
|
||||
_ = x[AptitudeLvC-5]
|
||||
_ = x[AptitudeLvB-6]
|
||||
_ = x[AptitudeLvA-7]
|
||||
_ = x[AptitudeLvS-8]
|
||||
}
|
||||
|
||||
const _AptitudeLevel_name = "GFEDCBAS"
|
||||
|
||||
var _AptitudeLevel_index = [...]uint8{0, 1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
func (i AptitudeLevel) String() string {
|
||||
idx := int(i) - 1
|
||||
if i < 1 || idx >= len(_AptitudeLevel_index)-1 {
|
||||
return "AptitudeLevel(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _AptitudeLevel_name[_AptitudeLevel_index[idx]:_AptitudeLevel_index[idx+1]]
|
||||
}
|
||||
@@ -3,10 +3,57 @@ package horse
|
||||
type CharacterID int16
|
||||
|
||||
type Character struct {
|
||||
ID CharacterID
|
||||
Name string
|
||||
ID CharacterID `json:"chara_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (c Character) String() string {
|
||||
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
|
||||
)
|
||||
|
||||
17
horse/character.kk
Normal file
17
horse/character.kk
Normal file
@@ -0,0 +1,17 @@
|
||||
module horse/character
|
||||
|
||||
import horse/game-id
|
||||
|
||||
pub struct character-detail
|
||||
character-id: character-id
|
||||
name: string
|
||||
|
||||
pub fun detail(
|
||||
c: character-id,
|
||||
?character/show: (character-id) -> string
|
||||
): character-detail
|
||||
Character-detail(c, c.show)
|
||||
|
||||
pub fun character-detail/show(d: character-detail): string
|
||||
val Character-detail(Character-id(id), name) = d
|
||||
name ++ " (ID " ++ id.show ++ ")"
|
||||
38
horse/durscale_string.go
Normal file
38
horse/durscale_string.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Code generated by "stringer -type DurScale -trimprefix Duration -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[DurationDirect-1]
|
||||
_ = x[DurationFrontDistance-2]
|
||||
_ = x[DurationRemainingHP-3]
|
||||
_ = x[DurationIncrementPass-4]
|
||||
_ = x[DurationMidSideBlock-5]
|
||||
_ = x[DurationRemainingHP2-7]
|
||||
}
|
||||
|
||||
const (
|
||||
_DurScale_name_0 = "directlyscaling with distance from the frontscaling with remaining HPincreasing with each pass while activescaling with mid-race phase blocked side time"
|
||||
_DurScale_name_1 = "scaling with remaining HP"
|
||||
)
|
||||
|
||||
var (
|
||||
_DurScale_index_0 = [...]uint8{0, 8, 44, 69, 107, 152}
|
||||
)
|
||||
|
||||
func (i DurScale) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 5:
|
||||
i -= 1
|
||||
return _DurScale_name_0[_DurScale_index_0[i]:_DurScale_index_0[i+1]]
|
||||
case i == 7:
|
||||
return _DurScale_name_1
|
||||
default:
|
||||
return "DurScale(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
80
horse/game-id.kk
Normal file
80
horse/game-id.kk
Normal file
@@ -0,0 +1,80 @@
|
||||
module horse/game-id
|
||||
|
||||
// Game ID for characters, cards, skills, races, &c.
|
||||
// Values for different categories may overlap.
|
||||
pub alias game-id = int
|
||||
|
||||
// Specific game ID types.
|
||||
// I've already made mistakes with ID categories and I haven't even committed this file yet.
|
||||
|
||||
pub struct scenario-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for characters.
|
||||
// Generally numbers in the range 1000-9999.
|
||||
pub struct character-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for trainees, i.e. costume instances of characters.
|
||||
// Generally a character ID with two digits appended.
|
||||
pub struct uma-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for skills.
|
||||
pub struct skill-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for skill groups.
|
||||
pub struct skill-group-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for skill icons.
|
||||
pub struct skill-icon-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for races,
|
||||
// i.e. "Tenno Sho (Spring)" and not "Tenno Sho (Spring) at Kyoto Racecourse."
|
||||
pub struct race-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for race thumbnails.
|
||||
pub struct race-thumbnail-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for saddles,
|
||||
// i.e. one or more race wins that appear as a title.
|
||||
pub struct saddle-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for sparks,
|
||||
// i.e. succession factors.
|
||||
pub struct spark-id
|
||||
game-id: game-id
|
||||
|
||||
// Game ID for spark groups,
|
||||
// i.e. all rarities (star counts) of a single spark.
|
||||
pub struct spark-group-id
|
||||
game-id: game-id
|
||||
|
||||
// order2 comparison between any game ID types.
|
||||
pub inline fun order2(x: a, y: a, ?a/game-id: (a) -> game-id): order2<a>
|
||||
match x.game-id.cmp(y.game-id)
|
||||
Lt -> Lt2(x, y)
|
||||
Eq -> Eq2(x)
|
||||
Gt -> Gt2(x, y)
|
||||
|
||||
// Comparison between any game ID types.
|
||||
pub inline fun cmp(x: a, y: a, ?a/game-id: (a) -> game-id): order
|
||||
x.game-id.cmp(y.game-id)
|
||||
|
||||
// Equality between any game ID types.
|
||||
pub inline fun (==)(x: a, y: a, ?a/game-id: (a) -> game-id): bool
|
||||
x.game-id == y.game-id
|
||||
|
||||
// Check whether a game ID is valid, i.e. nonzero.
|
||||
pub inline fun is-valid(x: a, ?a/game-id: (a) -> game-id): bool
|
||||
x.game-id != 0
|
||||
|
||||
// Construct an invalid game ID.
|
||||
pub inline fun default/game-id(): game-id
|
||||
0
|
||||
8
horse/global.kk
Normal file
8
horse/global.kk
Normal file
@@ -0,0 +1,8 @@
|
||||
module horse/global
|
||||
|
||||
import horse/game-id
|
||||
|
||||
// Shared saddle affinity bonus.
|
||||
// `s` should be the complete list of all saddles shared between the veterans.
|
||||
pub fun saddle-bonus(s: list<saddle-id>): int
|
||||
s.length
|
||||
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
11761
horse/global/skill.go
11761
horse/global/skill.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
125
horse/legacy.kk
125
horse/legacy.kk
@@ -1,17 +1,124 @@
|
||||
module horse/legacy
|
||||
|
||||
import horse/character
|
||||
import horse/race
|
||||
import std/num/decimal
|
||||
import std/data/linearmap
|
||||
import std/data/linearset
|
||||
import horse/game-id
|
||||
import horse/spark
|
||||
import horse/prob/dist
|
||||
|
||||
// A legacy, or parent and grandparents.
|
||||
pub struct legacy
|
||||
uma: veteran
|
||||
parents: (veteran, veteran)
|
||||
sub1: veteran
|
||||
sub2: veteran
|
||||
|
||||
// A veteran, or the result of a completed career.
|
||||
pub struct veteran
|
||||
character: character
|
||||
stat: spark<stat>
|
||||
aptitude: spark<aptitude>
|
||||
unique: maybe<spark<unique>>
|
||||
generic: list<spark<generic>>
|
||||
results: list<race-result>
|
||||
uma: uma-id
|
||||
sparks: list<spark-id>
|
||||
saddles: list<saddle-id>
|
||||
|
||||
// Get all saddles shared between two lists thereof.
|
||||
pub fun shared-saddles(a: list<saddle-id>, b: list<saddle-id>): list<saddle-id>
|
||||
val sa: linearSet<saddle-id> = a.foldl(linear-set(Nil)) fn(s, id) if id.is-valid then s.add(id) else s
|
||||
val c: linearSet<saddle-id> = b.foldl(linear-set(Nil)) fn(s, id) if sa.member(id) then s.add(id) else s
|
||||
c.list
|
||||
|
||||
// Get the individual affinity for a legacy.
|
||||
// Any invalid ID is treated as giving 0.
|
||||
pub fun parent-affinity(
|
||||
trainee: uma-id,
|
||||
legacy: legacy,
|
||||
other-parent: uma-id,
|
||||
?character-id: (uma-id) -> character-id,
|
||||
?saddle-bonus: (list<saddle-id>) -> int,
|
||||
?pair-affinity: (a: character-id, b: character-id) -> int,
|
||||
?trio-affinity: (a: character-id, b: character-id, c: character-id) -> int
|
||||
): int
|
||||
val t = trainee.character-id
|
||||
val p1 = legacy.uma.uma.character-id
|
||||
val s1 = legacy.sub1.uma.character-id
|
||||
val s2 = legacy.sub2.uma.character-id
|
||||
val p2 = other-parent.character-id
|
||||
pair-affinity(t, p1) + pair-affinity(p1, p2)
|
||||
+ trio-affinity(t, p1, s1) + trio-affinity(t, p1, s2)
|
||||
+ saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub1.saddles)) + saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub2.saddles))
|
||||
|
||||
// Get the individual affinities for a legacy's sub-legacies.
|
||||
// The first value is the legacy for the `legacy.sub1` and the second is for
|
||||
// `legacy.sub2`.
|
||||
// Any invalid ID is treated as giving 0.
|
||||
pub fun sub-affinity(
|
||||
trainee: uma-id,
|
||||
legacy: legacy,
|
||||
?character-id: (uma-id) -> character-id,
|
||||
?saddle-bonus: (list<saddle-id>) -> int,
|
||||
?trio-affinity: (a: character-id, b: character-id, c: character-id) -> int
|
||||
): (int, int)
|
||||
val t = trainee.character-id
|
||||
val p = legacy.uma.uma.character-id
|
||||
val s1 = legacy.sub1.uma.character-id
|
||||
val s2 = legacy.sub2.uma.character-id
|
||||
val r1 = trio-affinity(t, p, s1) + saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub1.saddles))
|
||||
val r2 = trio-affinity(t, p, s2) + saddle-bonus(shared-saddles(legacy.uma.saddles, legacy.sub2.saddles))
|
||||
(r1, r2)
|
||||
|
||||
// Associate each spark with its actual chance to activate given an individual
|
||||
// affinity value and the possible effects when it does.
|
||||
pub fun uma/inspiration(l: list<spark-id>, affinity: int, ?spark-type: (spark-id) -> spark-type, ?rarity: (spark-id) -> rarity, ?effects: (spark-id) -> list<list<spark-effect>>): list<(spark-id, decimal, list<list<spark-effect>>)>
|
||||
val a = decimal(1 + affinity, -2)
|
||||
l.map() fn(id) (id, min(id.base-proc * a, 1.decimal), id.effects)
|
||||
|
||||
// Get the complete list of effects that may occur in an inspiration event
|
||||
// and the respective probability of activation.
|
||||
// Duplicates, i.e. multiple veterans with the same spark, are preserved.
|
||||
pub fun inspiration(
|
||||
trainee: uma-id,
|
||||
parent1: legacy,
|
||||
parent2: legacy,
|
||||
?character-id: (uma-id) -> character-id,
|
||||
?saddle-bonus: (list<saddle-id>) -> int,
|
||||
?pair-affinity: (a: character-id, b: character-id) -> int,
|
||||
?trio-affinity: (a: character-id, b: character-id, c: character-id) -> int,
|
||||
?spark-type: (spark-id) -> spark-type,
|
||||
?rarity: (spark-id) -> rarity,
|
||||
?effects: (spark-id) -> list<list<spark-effect>>
|
||||
): list<(spark-id, decimal, list<list<spark-effect>>)>
|
||||
val p1a = parent-affinity(trainee, parent1, parent2.uma.uma)
|
||||
val p2a = parent-affinity(trainee, parent2, parent1.uma.uma)
|
||||
val (s11a, s12a) = sub-affinity(trainee, parent1)
|
||||
val (s21a, s22a) = sub-affinity(trainee, parent2)
|
||||
[
|
||||
inspiration(parent1.uma.sparks, p1a),
|
||||
inspiration(parent1.sub1.sparks, s11a),
|
||||
inspiration(parent1.sub2.sparks, s12a),
|
||||
inspiration(parent2.uma.sparks, p2a),
|
||||
inspiration(parent2.sub1.sparks, s21a),
|
||||
inspiration(parent2.sub2.sparks, s22a),
|
||||
].concat
|
||||
|
||||
// Reduce a spark effect list to the skill it is able to give.
|
||||
pub fun skills(l: list<list<spark-effect>>): maybe<skill-id>
|
||||
val r: linearSet<skill-id> = l.head(Nil).foldl(linear-set(Nil)) fn(s, eff)
|
||||
match eff
|
||||
Skill-Hint(id, _) -> s + id
|
||||
_ -> s
|
||||
r.list.head
|
||||
|
||||
// Reduce a spark effect list to the aptitude it is able to give.
|
||||
pub fun aptitudes(l: list<list<spark-effect>>): maybe<aptitude>
|
||||
val r: linearSet<aptitude> = l.head(Nil).foldl(linear-set(Nil)) fn(s, eff)
|
||||
match eff
|
||||
Aptitude-Up(apt) -> s + apt
|
||||
_ -> s
|
||||
r.list.head
|
||||
|
||||
// Get the overall chance of each count of sparks, including zero, providing a
|
||||
// given type of effect activating in a single inspiration event.
|
||||
pub fun inspiration-gives(l: list<(spark-id, decimal, list<list<spark-effect>>)>, f: (list<list<spark-effect>>) -> maybe<a>, ?a/(==): (a, a) -> bool): linearMap<a, list<decimal>>
|
||||
val m: linearMap<_, list<decimal>> = l.foldl(LinearMap(Nil)) fn(m, (_, p, eff))
|
||||
match f(eff)
|
||||
Nothing -> m
|
||||
Just(a) -> m.map/update(a, [p]) fn(cur, pp) pp.append(cur)
|
||||
m.map() fn(_, v) poisson-binomial(v)
|
||||
|
||||
47
horse/lobbyconversationlocationid_string.go
Normal file
47
horse/lobbyconversationlocationid_string.go
Normal 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) + ")"
|
||||
}
|
||||
134
horse/movement.kk
Normal file
134
horse/movement.kk
Normal file
@@ -0,0 +1,134 @@
|
||||
module horse/movement
|
||||
|
||||
// Surface types.
|
||||
pub type surface
|
||||
Turf
|
||||
Dirt
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `surface` type.
|
||||
pub fun surface/show(this : surface) : e string
|
||||
match this
|
||||
Turf -> "Turf"
|
||||
Dirt -> "Dirt"
|
||||
|
||||
// Race distance types.
|
||||
pub type distance
|
||||
Sprint
|
||||
Mile
|
||||
Medium
|
||||
Long
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `distance` type.
|
||||
pub fun distance/show(this : distance) : e string
|
||||
match this
|
||||
Sprint -> "Sprint"
|
||||
Mile -> "Mile"
|
||||
Medium -> "Medium"
|
||||
Long -> "Long"
|
||||
|
||||
// Running styles.
|
||||
pub type style
|
||||
Front-Runner
|
||||
Pace-Chaser
|
||||
Late-Surger
|
||||
End-Closer
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `style` type.
|
||||
pub fun style/(==)(this : style, other : style) : e bool
|
||||
match (this, other)
|
||||
(Front-Runner, Front-Runner) -> True
|
||||
(Pace-Chaser, Pace-Chaser) -> True
|
||||
(Late-Surger, Late-Surger) -> True
|
||||
(End-Closer, End-Closer) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Shows a string representation of the `style` type.
|
||||
pub fun style/show(this : style) : e string
|
||||
match this
|
||||
Front-Runner -> "Front Runner"
|
||||
Pace-Chaser -> "Pace Chaser"
|
||||
Late-Surger -> "Late Surger"
|
||||
End-Closer -> "End Closer"
|
||||
|
||||
// Aptitude levels.
|
||||
pub type aptitude-level
|
||||
G
|
||||
F
|
||||
E
|
||||
D
|
||||
C
|
||||
B
|
||||
A
|
||||
S
|
||||
|
||||
// Get the integer value for an aptitude level, starting at G -> 1.
|
||||
pub fun aptitude-level/int(l: aptitude-level): int
|
||||
match l
|
||||
G -> 1
|
||||
F -> 2
|
||||
E -> 3
|
||||
D -> 4
|
||||
C -> 5
|
||||
B -> 6
|
||||
A -> 7
|
||||
S -> 8
|
||||
|
||||
// Get the aptitude level corresponding to an integer, starting at 1 -> G.
|
||||
pub fun int/aptitude-level(l: int): maybe<aptitude-level>
|
||||
match l
|
||||
1 -> Just(G)
|
||||
2 -> Just(F)
|
||||
3 -> Just(E)
|
||||
4 -> Just(D)
|
||||
5 -> Just(C)
|
||||
6 -> Just(B)
|
||||
7 -> Just(A)
|
||||
8 -> Just(S)
|
||||
_ -> Nothing
|
||||
|
||||
// Comparison of the `aptitude-level` type.
|
||||
pub fun aptitude-level/cmp(this : aptitude-level, other : aptitude-level) : e order
|
||||
cmp(this.int, other.int)
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `aptitude-level` type.
|
||||
pub fun aptitude-level/order2(this : aptitude-level, other : aptitude-level) : order2<aptitude-level>
|
||||
match (this, other)
|
||||
(G, G) -> Eq2(G)
|
||||
(G, other') -> Lt2(G, other')
|
||||
(this', G) -> Gt2(G, this')
|
||||
(F, F) -> Eq2(F)
|
||||
(F, other') -> Lt2(F, other')
|
||||
(this', F) -> Gt2(F, this')
|
||||
(E, E) -> Eq2(E)
|
||||
(E, other') -> Lt2(E, other')
|
||||
(this', E) -> Gt2(E, this')
|
||||
(D, D) -> Eq2(D)
|
||||
(D, other') -> Lt2(D, other')
|
||||
(this', D) -> Gt2(D, this')
|
||||
(C, C) -> Eq2(C)
|
||||
(C, other') -> Lt2(C, other')
|
||||
(this', C) -> Gt2(C, this')
|
||||
(B, B) -> Eq2(B)
|
||||
(B, other') -> Lt2(B, other')
|
||||
(this', B) -> Gt2(B, this')
|
||||
(A, A) -> Eq2(A)
|
||||
(A, other') -> Lt2(A, other')
|
||||
(this', A) -> Gt2(A, this')
|
||||
(S, S) -> Eq2(S)
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `aptitude-level` type.
|
||||
pub fun aptitude-level/show(this : aptitude-level) : string
|
||||
match this
|
||||
G -> "G"
|
||||
F -> "F"
|
||||
E -> "E"
|
||||
D -> "D"
|
||||
C -> "C"
|
||||
B -> "B"
|
||||
A -> "A"
|
||||
S -> "S"
|
||||
21
horse/prob/dist.kk
Normal file
21
horse/prob/dist.kk
Normal file
@@ -0,0 +1,21 @@
|
||||
module horse/prob/dist
|
||||
|
||||
import std/num/decimal
|
||||
|
||||
tail fun pb-step(pn: list<decimal>, pi: decimal, pmfkm1: decimal, pmf: list<decimal>, next: ctx<list<decimal>>): list<decimal>
|
||||
match pn
|
||||
Nil -> next ++. Nil // final step overall
|
||||
Cons(_, pp) -> match pmf
|
||||
Cons(pmfk, pmf') ->
|
||||
val next' = next ++ ctx Cons(pi * pmfkm1 + (1.decimal - pi) * pmfk, hole)
|
||||
pb-step(pp, pi, pmfk, pmf', next')
|
||||
Nil -> next ++. Cons(pi * pmfkm1, Nil) // last step of this iteration
|
||||
|
||||
// Given `n` different Bernoulli processes with respective probabilities in `pn`,
|
||||
// find the distribution of `k` successes for `k` ranging from 0 to `n` inclusive.
|
||||
// The index in the result list corresponds to `k`.
|
||||
pub fun pmf/poisson-binomial(pn: list<decimal>): list<decimal>
|
||||
pn.foldl([1.decimal]) fn(pmf, pi)
|
||||
match pmf
|
||||
Cons(pmf0, pmf') -> pb-step(pn, pi, pmf0, pmf', ctx Cons((1.decimal - pi) * pmf0, hole))
|
||||
Nil -> impossible("fold started with non-empty pmf but got empty pmf")
|
||||
158
horse/prob/kfl.kk
Normal file
158
horse/prob/kfl.kk
Normal file
@@ -0,0 +1,158 @@
|
||||
module horse/prob/kfl
|
||||
|
||||
// kfl is a semiring of probabilities formed by vibes.
|
||||
pub type kfl
|
||||
// Effectively if not literally impossible events.
|
||||
Impossible
|
||||
// Not worth aiming for, but can technically still happen.
|
||||
Probably-Not
|
||||
// You expect it not to happen most of the time, but it might still be worth
|
||||
// trying for it if you're being forced to play to your outs.
|
||||
Doubtful
|
||||
// More likely that it won't happen, but a success isn't surprising.
|
||||
Unlikely
|
||||
// Either it does or it doesn't.
|
||||
Mayhapsibly
|
||||
// Decent chance it doesn't happen, but you still expect it to.
|
||||
Probably
|
||||
// You expect it to happen most of the time, but accept that there will be failures.
|
||||
Most-Likely
|
||||
// Very close to guaranteed, but technically with a small chance to fail.
|
||||
Cry-If-Not
|
||||
// Absolutely guaranteed events.
|
||||
Guaranteed
|
||||
|
||||
// Automatically generated.
|
||||
// Comparison of the `kfl` type.
|
||||
pub fun cmp(this : kfl, other : kfl) : e order
|
||||
match (this, other)
|
||||
(Impossible, Impossible) -> Eq
|
||||
(Impossible, _) -> Lt
|
||||
(_, Impossible) -> Gt
|
||||
(Probably-Not, Probably-Not) -> Eq
|
||||
(Probably-Not, _) -> Lt
|
||||
(_, Probably-Not) -> Gt
|
||||
(Doubtful, Doubtful) -> Eq
|
||||
(Doubtful, _) -> Lt
|
||||
(_, Doubtful) -> Gt
|
||||
(Unlikely, Unlikely) -> Eq
|
||||
(Unlikely, _) -> Lt
|
||||
(_, Unlikely) -> Gt
|
||||
(Mayhapsibly, Mayhapsibly) -> Eq
|
||||
(Mayhapsibly, _) -> Lt
|
||||
(_, Mayhapsibly) -> Gt
|
||||
(Probably, Probably) -> Eq
|
||||
(Probably, _) -> Lt
|
||||
(_, Probably) -> Gt
|
||||
(Most-Likely, Most-Likely) -> Eq
|
||||
(Most-Likely, _) -> Lt
|
||||
(_, Most-Likely) -> Gt
|
||||
(Cry-If-Not, Cry-If-Not) -> Eq
|
||||
(Cry-If-Not, _) -> Lt
|
||||
(_, Cry-If-Not) -> Gt
|
||||
(Guaranteed, Guaranteed) -> Eq
|
||||
|
||||
// Shows a string representation of the `kfl` type.
|
||||
pub fun show(this : kfl) : e string
|
||||
match this
|
||||
Impossible -> "impossible"
|
||||
Probably-Not -> "probably not"
|
||||
Doubtful -> "doubtful"
|
||||
Unlikely -> "unlikely"
|
||||
Mayhapsibly -> "mayhapsibly"
|
||||
Probably -> "probably"
|
||||
Most-Likely -> "most likely"
|
||||
Cry-If-Not -> "cry if not"
|
||||
Guaranteed -> "guaranteed"
|
||||
|
||||
// KFL multiplication, or the probability of cooccurrence of two independent events.
|
||||
pub fun (*)(a: kfl, b: kfl): e kfl
|
||||
val (l, h) = match a.cmp(b) // this operation is commutative
|
||||
Gt -> (b, a)
|
||||
_ -> (a, b)
|
||||
match (l, h)
|
||||
(r, Guaranteed) -> r // factor out Guaranteed cases
|
||||
(Impossible, _) -> Impossible
|
||||
(Probably-Not, _) -> Impossible
|
||||
(r, Cry-If-Not) -> r // factor out further Cry-If-Not cases
|
||||
(Doubtful, Most-Likely) -> Probably-Not
|
||||
(Doubtful, _) -> Impossible
|
||||
(Unlikely, Most-Likely) -> Doubtful
|
||||
(Unlikely, Probably) -> Doubtful
|
||||
(Unlikely, Mayhapsibly) -> Probably-Not
|
||||
(Unlikely, _) -> Probably-Not // (Unlikely, Unlikely) because commutative
|
||||
(Mayhapsibly, Most-Likely) -> Unlikely
|
||||
(Mayhapsibly, Probably) -> Unlikely
|
||||
(Mayhapsibly, _) -> Unlikely
|
||||
(Probably, Most-Likely) -> Mayhapsibly
|
||||
(Probably, _) -> Unlikely
|
||||
(Most-Likely, _) -> Probably
|
||||
// These two are only needed because the type system doesn't understand commutativity.
|
||||
(Cry-If-Not, _) -> Cry-If-Not
|
||||
(Guaranteed, _) -> Guaranteed
|
||||
|
||||
// KFL addition, or the probability of occurrence of at least one of two independent events.
|
||||
pub fun (+)(a: kfl, b: kfl): e kfl
|
||||
val (l, h) = match a.cmp(b) // this operation is commutative
|
||||
Gt -> (b, a)
|
||||
_ -> (a, b)
|
||||
match (l, h)
|
||||
// Cases with _ on the right are (a, a) due to commutativity.
|
||||
// Cases with _ on the left simplify later cases that all absorb to the right.
|
||||
(Guaranteed, _) -> Guaranteed
|
||||
(_, Guaranteed) -> Guaranteed
|
||||
(Cry-If-Not, _) -> Guaranteed
|
||||
(Most-Likely, Cry-If-Not) -> Cry-If-Not
|
||||
(Most-Likely, _) -> Cry-If-Not
|
||||
(_, Cry-If-Not) -> Cry-If-Not
|
||||
(Probably, Most-Likely) -> Cry-If-Not
|
||||
(Probably, _) -> Most-Likely
|
||||
(_, Most-Likely) -> Most-Likely
|
||||
(Mayhapsibly, Probably) -> Most-Likely
|
||||
(Mayhapsibly, _) -> Probably
|
||||
(Unlikely, Probably) -> Most-Likely
|
||||
(Unlikely, Mayhapsibly) -> Probably
|
||||
(Unlikely, _) -> Mayhapsibly
|
||||
(_, Probably) -> Probably
|
||||
(Doubtful, Mayhapsibly) -> Probably
|
||||
(Doubtful, Unlikely) -> Mayhapsibly
|
||||
(Doubtful, _) -> Unlikely
|
||||
(_, Mayhapsibly) -> Mayhapsibly
|
||||
(_, Unlikely) -> Unlikely
|
||||
(Probably-Not, Doubtful) -> Unlikely
|
||||
(Probably-Not, _) -> Probably-Not
|
||||
(_, Doubtful) -> Doubtful
|
||||
(_, Probably-Not) -> Probably-Not
|
||||
(_, Impossible) -> Impossible
|
||||
|
||||
// KFL union, or the probability of occurrence of exactly one of two independent events.
|
||||
pub fun either(a: kfl, b: kfl): e kfl
|
||||
val (l, h) = match a.cmp(b) // this operation is commutative
|
||||
Gt -> (b, a)
|
||||
_ -> (a, b)
|
||||
match (l, h)
|
||||
(Impossible, r) -> r
|
||||
(Probably-Not, Guaranteed) -> Cry-If-Not
|
||||
(Probably-Not, r) -> r
|
||||
(Doubtful, Guaranteed) -> Most-Likely
|
||||
(Doubtful, Cry-If-Not) -> Most-Likely
|
||||
(Doubtful, Most-Likely) -> Probably
|
||||
(Doubtful, Probably) -> Mayhapsibly
|
||||
(Doubtful, Mayhapsibly) -> Mayhapsibly
|
||||
(Doubtful, Unlikely) -> Mayhapsibly
|
||||
(Doubtful, _) -> Unlikely
|
||||
(Unlikely, Guaranteed) -> Probably
|
||||
(Unlikely, Cry-If-Not) -> Mayhapsibly
|
||||
(Unlikely, Most-Likely) -> Mayhapsibly
|
||||
(Unlikely, _) -> Probably
|
||||
(Mayhapsibly, Guaranteed) -> Mayhapsibly
|
||||
(Mayhapsibly, Cry-If-Not) -> Mayhapsibly
|
||||
(Mayhapsibly, Most-Likely) -> Mayhapsibly
|
||||
(Mayhapsibly, _) -> Probably
|
||||
(Probably, Guaranteed) -> Unlikely
|
||||
(Probably, Cry-If-Not) -> Unlikely
|
||||
(Probably, Most-Likely) -> Unlikely
|
||||
(Probably, _) -> Mayhapsibly
|
||||
(Most-Likely, _) -> Doubtful
|
||||
(Cry-If-Not, _) -> Probably-Not
|
||||
(Guaranteed, _) -> Impossible
|
||||
58
horse/prob/pmf.kk
Normal file
58
horse/prob/pmf.kk
Normal file
@@ -0,0 +1,58 @@
|
||||
module horse/prob/pmf
|
||||
|
||||
import std/core/list
|
||||
|
||||
// Discrete-support probability distribution implemented as a list with the invariant
|
||||
// that support is always given in increasing order.
|
||||
pub type pmf<s, v>
|
||||
Event(s: s, v: v, next: pmf<s, v>)
|
||||
End
|
||||
|
||||
// Add an independent event to the distribution.
|
||||
pub fun add(p: pmf<s, v>, s: s, v: v, ?s/cmp: (a: s, b: s) -> order, ?v/(+): (new: v, old: v) -> e v): e pmf<s, v>
|
||||
match p
|
||||
End -> Event(s, v, End)
|
||||
Event(s', v', next) -> match s.cmp(s')
|
||||
Lt -> Event(s, v, Event(s', v', next))
|
||||
Eq -> Event(s, v + v', next)
|
||||
Gt -> Event(s', v', add(next, s, v))
|
||||
|
||||
// Replace an event in the distribution.
|
||||
pub inline fun set(p: pmf<s, v>, s: s, v: v, ?s/cmp: (a: s, b: s) -> order): e pmf<s, v>
|
||||
p.add(s, v, cmp, fn(new, old) new)
|
||||
|
||||
// Construct a pmf from a list of (support, value) entries.
|
||||
pub fun list/pmf(l: list<(s, v)>, ?s/cmp: (a: s, b: s) -> order, ?v/(+): (new: v, old: v) -> e v): e pmf<s, v>
|
||||
l.foldl(End) fn(p, (s, v)) p.add(s, v)
|
||||
|
||||
// Fold over the entries of the distribution.
|
||||
pub tail fun foldl(p: pmf<s, v>, init: a, f: (a, s, v) -> e a): e a
|
||||
match p
|
||||
End -> init
|
||||
Event(s, v, next) -> foldl(next, f(init, s, v), f)
|
||||
|
||||
// Convert the distribution to a list of entries.
|
||||
pub fun pmf/list(p: pmf<s, v>): list<(s, v)>
|
||||
p.foldl(Nil) fn(l, s, v) Cons((s, v), l)
|
||||
|
||||
// Distribution of cooccurrence of two events described by their distributions.
|
||||
pub fun (*)(a: pmf<s, v>, b: pmf<s, v>, ?s/cmp: (a: s, b: s) -> order, ?v/(*): (a: v, b: v) -> e v): e pmf<s, v>
|
||||
match a
|
||||
End -> End
|
||||
Event(sa, va, nexta) -> match b
|
||||
End -> End
|
||||
Event(sb, vb, nextb) -> match sa.cmp(sb)
|
||||
Lt -> nexta * b
|
||||
Eq -> Event(sa, va * vb, nexta * nextb)
|
||||
Gt -> a * nextb
|
||||
|
||||
// Distribution of occurrence of at least one of two events described by their distributions.
|
||||
pub fun (+)(a: pmf<s, v>, b: pmf<s, v>, ?s/cmp: (a: s, b: s) -> order, ?v/(+): (a: v, b: v) -> e v): e pmf<s, v>
|
||||
match a
|
||||
End -> b
|
||||
Event(sa, va, nexta) -> match b
|
||||
End -> a
|
||||
Event(sb, vb, nextb) -> match sa.cmp(sb)
|
||||
Lt -> Event(sa, va, nexta + b)
|
||||
Eq -> Event(sa, va + vb, nexta + nextb)
|
||||
Gt -> Event(sb, vb, a + nextb)
|
||||
46
horse/race.go
Normal file
46
horse/race.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package horse
|
||||
|
||||
type RaceID int32
|
||||
|
||||
// Race is the internal data about a race.
|
||||
type Race struct {
|
||||
ID RaceID `json:"race_id"`
|
||||
Name string `json:"name"`
|
||||
Thumbnail int `json:"thumbnail"`
|
||||
// Some careers contain unusual versions of races, e.g. Tenno Sho (Spring)
|
||||
// in Hanshin instead of Kyoto for Narita Taishin and Biwa Hayahide.
|
||||
// For such races, this field holds the normal race ID.
|
||||
Primary RaceID `json:"primary"`
|
||||
}
|
||||
|
||||
type SaddleID int32
|
||||
|
||||
// Saddle is the internal data about a race win saddle.
|
||||
type Saddle struct {
|
||||
ID SaddleID `json:"saddle_id"`
|
||||
Name string `json:"name"`
|
||||
Races []RaceID `json:"races"`
|
||||
Type SaddleType `json:"type"`
|
||||
// Saddles that involve alternate races are themselves alternate.
|
||||
// For such saddles, this field holds the normal saddle ID.
|
||||
Primary SaddleID `json:"primary"`
|
||||
}
|
||||
|
||||
type SaddleType int8
|
||||
|
||||
const (
|
||||
// Saddle for multiple race wins, e.g. Classic Triple Crown, Dual Grand Prix, &c.
|
||||
SaddleTypeHonor SaddleType = iota
|
||||
SaddleTypeG3
|
||||
SaddleTypeG2
|
||||
SaddleTypeG1
|
||||
)
|
||||
|
||||
type ScenarioID int8
|
||||
|
||||
// Scenario is metadata about a career scenario.
|
||||
type Scenario struct {
|
||||
ID ScenarioID `json:"scenario_id"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
752
horse/race.kk
752
horse/race.kk
@@ -1,426 +1,30 @@
|
||||
module horse/race
|
||||
|
||||
import std/data/linearset
|
||||
import horse/game-id
|
||||
|
||||
// Exhaustive enumeration of graded races that can be run in career.
|
||||
// Races that can be run in multiple years are listed only once.
|
||||
pub type career-race
|
||||
February-Stakes
|
||||
Takamatsunomiya-Kinen
|
||||
Osaka-Hai
|
||||
Oka-Sho
|
||||
Satsuki-Sho
|
||||
Tenno-Sho-Spring
|
||||
NHK-Mile-Cup
|
||||
Victoria-Mile
|
||||
Japanese-Oaks
|
||||
Japanese-Derby
|
||||
Yasuda-Kinen
|
||||
Takarazuka-Kinen
|
||||
Sprinters-Stakes
|
||||
Shuka-Sho
|
||||
Kikuka-Sho
|
||||
Tenno-Sho-Autumn
|
||||
Queen-Elizabeth-II-Cup
|
||||
Mile-Championship
|
||||
Japan-Cup
|
||||
Champions-Cup
|
||||
Hanshin-Juvenile-Fillies
|
||||
Asahi-Hai-Futurity-Stakes
|
||||
Arima-Kinen
|
||||
Hopeful-Stakes
|
||||
Tokyo-Daishoten
|
||||
JBC-Classic
|
||||
JBC-Sprint
|
||||
JBC-Ladies-Classic
|
||||
Japan-Dirt-Derby
|
||||
Teio-Sho
|
||||
Nikkei-Shinshun-Hai
|
||||
Tokai-Stakes
|
||||
American-Jockey-Club-Cup
|
||||
Kyoto-Kinen
|
||||
Nakayama-Kinen
|
||||
Tulip-Sho
|
||||
Yayoi-Sho
|
||||
Kinko-Sho
|
||||
Fillies-Revue
|
||||
Hanshin-Daishoten
|
||||
Spring-Stakes
|
||||
Nikkei-Sho
|
||||
Hanshin-Umamusume-Stakes
|
||||
New-Zealand-Trophy
|
||||
Yomiuri-Milers-Cup
|
||||
Flora-Stakes
|
||||
Aoba-Sho
|
||||
Kyoto-Shimbun-Hai
|
||||
Keio-Hai-Spring-Cup
|
||||
Meguro-Kinen
|
||||
Sapporo-Kinen
|
||||
Centaur-Stakes
|
||||
Rose-Stakes
|
||||
St-Lite-Kinen
|
||||
Kobe-Shimbun-Hai
|
||||
All-Comers
|
||||
Mainichi-Okan
|
||||
Kyoto-Daishoten
|
||||
Fuchu-Umamusume-Stakes
|
||||
Fuji-Stakes
|
||||
Swan-Stakes
|
||||
Keio-Hai-Junior-Stakes
|
||||
Copa-Republica-Argentina
|
||||
Daily-Hai-Junior-Stakes
|
||||
Stayers-Stakes
|
||||
Hanshin-Cup
|
||||
Kyoto-Kimpai
|
||||
Nakayama-Kimpai
|
||||
Shinzan-Kinen
|
||||
Fairy-Stakes
|
||||
Aichi-Hai
|
||||
Keisei-Hai
|
||||
Silk-Road-Stakes
|
||||
Negishi-Stakes
|
||||
Kisaragi-Sho
|
||||
Tokyo-Shimbun-Hai
|
||||
Queen-Cup
|
||||
Kyodo-News-Hai
|
||||
Kyoto-Umamusume-Stakes
|
||||
Diamond-Stakes
|
||||
Kokura-Daishoten
|
||||
Arlington-Cup
|
||||
Hankyu-Hai
|
||||
Ocean-Stakes
|
||||
Nakayama-Umamusume-Stakes
|
||||
Falcon-Stakes
|
||||
Flower-Cup
|
||||
Mainichi-Hai
|
||||
March-Stakes
|
||||
Lord-Derby-Challenge-Trophy
|
||||
Antares-Stakes
|
||||
Fukushima-Umamusume-Stakes
|
||||
Niigata-Daishoten
|
||||
Heian-Stakes
|
||||
Aoi-Stakes
|
||||
Naruo-Kinen
|
||||
Mermaid-Stakes
|
||||
Epsom-Cup
|
||||
Unicorn-Stakes
|
||||
Hakodate-Sprint-Stakes
|
||||
CBC-Sho
|
||||
Radio-Nikkei-Sho
|
||||
Procyon-Stakes
|
||||
Tanabata-Sho
|
||||
Hakodate-Kinen
|
||||
Chukyo-Kinen
|
||||
Hakodate-Junior-Stakes
|
||||
Ibis-Summer-Dash
|
||||
Queen-Stakes
|
||||
Kokura-Kinen
|
||||
Leopard-Stakes
|
||||
Sekiya-Kinen
|
||||
Elm-Stakes
|
||||
Kitakyushu-Kinen
|
||||
Niigata-Junior-Stakes
|
||||
Keeneland-Cup
|
||||
Sapporo-Junior-Stakes
|
||||
Kokura-Junior-Stakes
|
||||
Niigata-Kinen
|
||||
Shion-Stakes
|
||||
Keisei-Hai-Autumn-Handicap
|
||||
Sirius-Stakes
|
||||
Saudi-Arabia-Royal-Cup
|
||||
Artemis-Stakes
|
||||
Fantasy-Stakes
|
||||
Miyako-Stakes
|
||||
Musashino-Stakes
|
||||
Fukushima-Kinen
|
||||
Tokyo-Sports-Hai-Junior-Stakes
|
||||
Kyoto-Junior-Stakes
|
||||
Keihan-Hai
|
||||
Challenge-Cup
|
||||
Chunichi-Shimbun-Hai
|
||||
Capella-Stakes
|
||||
Turquoise-Stakes
|
||||
pub struct race-detail
|
||||
race-id: race-id
|
||||
name: string
|
||||
grade: grade
|
||||
thumbnail-id: race-thumbnail-id
|
||||
// Some careers contain unusual versions of races, e.g. Tenno Sho (Spring)
|
||||
// in Hanshin instead of Kyoto for Narita Taishin and Biwa Hayahide.
|
||||
// For such races, this field holds the normal race ID.
|
||||
primary: race-id
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `career-race` type.
|
||||
pub fun career-race/show(this : career-race) : e string
|
||||
match this
|
||||
February-Stakes -> "February Stakes"
|
||||
Takamatsunomiya-Kinen -> "Takamatsunomiya Kinen"
|
||||
Osaka-Hai -> "Osaka Hai"
|
||||
Oka-Sho -> "Oka Sho"
|
||||
Satsuki-Sho -> "Satsuki Sho"
|
||||
Tenno-Sho-Spring -> "Tenno Sho Spring"
|
||||
NHK-Mile-Cup -> "NHK Mile Cup"
|
||||
Victoria-Mile -> "Victoria Mile"
|
||||
Japanese-Oaks -> "Japanese Oaks"
|
||||
Japanese-Derby -> "Japanese Derby"
|
||||
Yasuda-Kinen -> "Yasuda Kinen"
|
||||
Takarazuka-Kinen -> "Takarazuka Kinen"
|
||||
Sprinters-Stakes -> "Sprinters Stakes"
|
||||
Shuka-Sho -> "Shuka Sho"
|
||||
Kikuka-Sho -> "Kikuka Sho"
|
||||
Tenno-Sho-Autumn -> "Tenno Sho Autumn"
|
||||
Queen-Elizabeth-II-Cup -> "Queen Elizabeth II Cup"
|
||||
Mile-Championship -> "Mile Championship"
|
||||
Japan-Cup -> "Japan Cup"
|
||||
Champions-Cup -> "Champions Cup"
|
||||
Hanshin-Juvenile-Fillies -> "Hanshin Juvenile Fillies"
|
||||
Asahi-Hai-Futurity-Stakes -> "Asahi Hai Futurity Stakes"
|
||||
Arima-Kinen -> "Arima Kinen"
|
||||
Hopeful-Stakes -> "Hopeful Stakes"
|
||||
Tokyo-Daishoten -> "Tokyo Daishoten"
|
||||
JBC-Classic -> "JBC Classic"
|
||||
JBC-Sprint -> "JBC Sprint"
|
||||
JBC-Ladies-Classic -> "JBC Ladies Classic"
|
||||
Japan-Dirt-Derby -> "Japan Dirt Derby"
|
||||
Teio-Sho -> "Teio Sho"
|
||||
Nikkei-Shinshun-Hai -> "Nikkei Shinshun Hai"
|
||||
Tokai-Stakes -> "Tokai Stakes"
|
||||
American-Jockey-Club-Cup -> "American Jockey Club Cup"
|
||||
Kyoto-Kinen -> "Kyoto Kinen"
|
||||
Nakayama-Kinen -> "Nakayama Kinen"
|
||||
Tulip-Sho -> "Tulip Sho"
|
||||
Yayoi-Sho -> "Yayoi Sho"
|
||||
Kinko-Sho -> "Kinko Sho"
|
||||
Fillies-Revue -> "Fillies Revue"
|
||||
Hanshin-Daishoten -> "Hanshin Daishoten"
|
||||
Spring-Stakes -> "Spring Stakes"
|
||||
Nikkei-Sho -> "Nikkei Sho"
|
||||
Hanshin-Umamusume-Stakes -> "Hanshin Umamusume Stakes"
|
||||
New-Zealand-Trophy -> "New Zealand Trophy"
|
||||
Yomiuri-Milers-Cup -> "Yomiuri Milers Cup"
|
||||
Flora-Stakes -> "Flora Stakes"
|
||||
Aoba-Sho -> "Aoba Sho"
|
||||
Kyoto-Shimbun-Hai -> "Kyoto Shimbun Hai"
|
||||
Keio-Hai-Spring-Cup -> "Keio Hai Spring Cup"
|
||||
Meguro-Kinen -> "Meguro Kinen"
|
||||
Sapporo-Kinen -> "Sapporo Kinen"
|
||||
Centaur-Stakes -> "Centaur Stakes"
|
||||
Rose-Stakes -> "Rose Stakes"
|
||||
St-Lite-Kinen -> "St Lite Kinen"
|
||||
Kobe-Shimbun-Hai -> "Kobe Shimbun Hai"
|
||||
All-Comers -> "All Comers"
|
||||
Mainichi-Okan -> "Mainichi Okan"
|
||||
Kyoto-Daishoten -> "Kyoto Daishoten"
|
||||
Fuchu-Umamusume-Stakes -> "Fuchu Umamusume Stakes"
|
||||
Fuji-Stakes -> "Fuji Stakes"
|
||||
Swan-Stakes -> "Swan Stakes"
|
||||
Keio-Hai-Junior-Stakes -> "Keio Hai Junior Stakes"
|
||||
Copa-Republica-Argentina -> "Copa Republica Argentina"
|
||||
Daily-Hai-Junior-Stakes -> "Daily Hai Junior Stakes"
|
||||
Stayers-Stakes -> "Stayers Stakes"
|
||||
Hanshin-Cup -> "Hanshin Cup"
|
||||
Kyoto-Kimpai -> "Kyoto Kimpai"
|
||||
Nakayama-Kimpai -> "Nakayama Kimpai"
|
||||
Shinzan-Kinen -> "Shinzan Kinen"
|
||||
Fairy-Stakes -> "Fairy Stakes"
|
||||
Aichi-Hai -> "Aichi Hai"
|
||||
Keisei-Hai -> "Keisei Hai"
|
||||
Silk-Road-Stakes -> "Silk Road Stakes"
|
||||
Negishi-Stakes -> "Negishi Stakes"
|
||||
Kisaragi-Sho -> "Kisaragi Sho"
|
||||
Tokyo-Shimbun-Hai -> "Tokyo Shimbun Hai"
|
||||
Queen-Cup -> "Queen Cup"
|
||||
Kyodo-News-Hai -> "Kyodo News Hai"
|
||||
Kyoto-Umamusume-Stakes -> "Kyoto Umamusume Stakes"
|
||||
Diamond-Stakes -> "Diamond Stakes"
|
||||
Kokura-Daishoten -> "Kokura Daishoten"
|
||||
Arlington-Cup -> "Arlington Cup"
|
||||
Hankyu-Hai -> "Hankyu Hai"
|
||||
Ocean-Stakes -> "Ocean Stakes"
|
||||
Nakayama-Umamusume-Stakes -> "Nakayama Umamusume Stakes"
|
||||
Falcon-Stakes -> "Falcon Stakes"
|
||||
Flower-Cup -> "Flower Cup"
|
||||
Mainichi-Hai -> "Mainichi Hai"
|
||||
March-Stakes -> "March Stakes"
|
||||
Lord-Derby-Challenge-Trophy -> "Lord Derby Challenge Trophy"
|
||||
Antares-Stakes -> "Antares Stakes"
|
||||
Fukushima-Umamusume-Stakes -> "Fukushima Umamusume Stakes"
|
||||
Niigata-Daishoten -> "Niigata Daishoten"
|
||||
Heian-Stakes -> "Heian Stakes"
|
||||
Aoi-Stakes -> "Aoi Stakes"
|
||||
Naruo-Kinen -> "Naruo Kinen"
|
||||
Mermaid-Stakes -> "Mermaid Stakes"
|
||||
Epsom-Cup -> "Epsom Cup"
|
||||
Unicorn-Stakes -> "Unicorn Stakes"
|
||||
Hakodate-Sprint-Stakes -> "Hakodate Sprint Stakes"
|
||||
CBC-Sho -> "CBC Sho"
|
||||
Radio-Nikkei-Sho -> "Radio Nikkei Sho"
|
||||
Procyon-Stakes -> "Procyon Stakes"
|
||||
Tanabata-Sho -> "Tanabata Sho"
|
||||
Hakodate-Kinen -> "Hakodate Kinen"
|
||||
Chukyo-Kinen -> "Chukyo Kinen"
|
||||
Hakodate-Junior-Stakes -> "Hakodate Junior Stakes"
|
||||
Ibis-Summer-Dash -> "Ibis Summer Dash"
|
||||
Queen-Stakes -> "Queen Stakes"
|
||||
Kokura-Kinen -> "Kokura Kinen"
|
||||
Leopard-Stakes -> "Leopard Stakes"
|
||||
Sekiya-Kinen -> "Sekiya Kinen"
|
||||
Elm-Stakes -> "Elm Stakes"
|
||||
Kitakyushu-Kinen -> "Kitakyushu Kinen"
|
||||
Niigata-Junior-Stakes -> "Niigata Junior Stakes"
|
||||
Keeneland-Cup -> "Keeneland Cup"
|
||||
Sapporo-Junior-Stakes -> "Sapporo Junior Stakes"
|
||||
Kokura-Junior-Stakes -> "Kokura Junior Stakes"
|
||||
Niigata-Kinen -> "Niigata Kinen"
|
||||
Shion-Stakes -> "Shion Stakes"
|
||||
Keisei-Hai-Autumn-Handicap -> "Keisei Hai Autumn Handicap"
|
||||
Sirius-Stakes -> "Sirius Stakes"
|
||||
Saudi-Arabia-Royal-Cup -> "Saudi Arabia Royal Cup"
|
||||
Artemis-Stakes -> "Artemis Stakes"
|
||||
Fantasy-Stakes -> "Fantasy Stakes"
|
||||
Miyako-Stakes -> "Miyako Stakes"
|
||||
Musashino-Stakes -> "Musashino Stakes"
|
||||
Fukushima-Kinen -> "Fukushima Kinen"
|
||||
Tokyo-Sports-Hai-Junior-Stakes -> "Tokyo Sports Hai Junior Stakes"
|
||||
Kyoto-Junior-Stakes -> "Kyoto Junior Stakes"
|
||||
Keihan-Hai -> "Keihan Hai"
|
||||
Challenge-Cup -> "Challenge Cup"
|
||||
Chunichi-Shimbun-Hai -> "Chunichi Shimbun Hai"
|
||||
Capella-Stakes -> "Capella Stakes"
|
||||
Turquoise-Stakes -> "Turquoise Stakes"
|
||||
pub fun detail(
|
||||
r: race-id,
|
||||
?race/show: (race-id) -> string,
|
||||
?race/grade: (race-id) -> grade,
|
||||
?race/thumbnail: (race-id) -> race-thumbnail-id,
|
||||
?race/primary: (race-id) -> race-id
|
||||
): race-detail
|
||||
Race-detail(r, r.show, r.grade, r.thumbnail, r.primary)
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `career-race` type.
|
||||
pub fun career-race/(==)(this : career-race, other : career-race) : e bool
|
||||
match (this, other)
|
||||
(February-Stakes, February-Stakes) -> True
|
||||
(Takamatsunomiya-Kinen, Takamatsunomiya-Kinen) -> True
|
||||
(Osaka-Hai, Osaka-Hai) -> True
|
||||
(Oka-Sho, Oka-Sho) -> True
|
||||
(Satsuki-Sho, Satsuki-Sho) -> True
|
||||
(Tenno-Sho-Spring, Tenno-Sho-Spring) -> True
|
||||
(NHK-Mile-Cup, NHK-Mile-Cup) -> True
|
||||
(Victoria-Mile, Victoria-Mile) -> True
|
||||
(Japanese-Oaks, Japanese-Oaks) -> True
|
||||
(Japanese-Derby, Japanese-Derby) -> True
|
||||
(Yasuda-Kinen, Yasuda-Kinen) -> True
|
||||
(Takarazuka-Kinen, Takarazuka-Kinen) -> True
|
||||
(Sprinters-Stakes, Sprinters-Stakes) -> True
|
||||
(Shuka-Sho, Shuka-Sho) -> True
|
||||
(Kikuka-Sho, Kikuka-Sho) -> True
|
||||
(Tenno-Sho-Autumn, Tenno-Sho-Autumn) -> True
|
||||
(Queen-Elizabeth-II-Cup, Queen-Elizabeth-II-Cup) -> True
|
||||
(Mile-Championship, Mile-Championship) -> True
|
||||
(Japan-Cup, Japan-Cup) -> True
|
||||
(Champions-Cup, Champions-Cup) -> True
|
||||
(Hanshin-Juvenile-Fillies, Hanshin-Juvenile-Fillies) -> True
|
||||
(Asahi-Hai-Futurity-Stakes, Asahi-Hai-Futurity-Stakes) -> True
|
||||
(Arima-Kinen, Arima-Kinen) -> True
|
||||
(Hopeful-Stakes, Hopeful-Stakes) -> True
|
||||
(Tokyo-Daishoten, Tokyo-Daishoten) -> True
|
||||
(JBC-Classic, JBC-Classic) -> True
|
||||
(JBC-Sprint, JBC-Sprint) -> True
|
||||
(JBC-Ladies-Classic, JBC-Ladies-Classic) -> True
|
||||
(Japan-Dirt-Derby, Japan-Dirt-Derby) -> True
|
||||
(Teio-Sho, Teio-Sho) -> True
|
||||
(Nikkei-Shinshun-Hai, Nikkei-Shinshun-Hai) -> True
|
||||
(Tokai-Stakes, Tokai-Stakes) -> True
|
||||
(American-Jockey-Club-Cup, American-Jockey-Club-Cup) -> True
|
||||
(Kyoto-Kinen, Kyoto-Kinen) -> True
|
||||
(Nakayama-Kinen, Nakayama-Kinen) -> True
|
||||
(Tulip-Sho, Tulip-Sho) -> True
|
||||
(Yayoi-Sho, Yayoi-Sho) -> True
|
||||
(Kinko-Sho, Kinko-Sho) -> True
|
||||
(Fillies-Revue, Fillies-Revue) -> True
|
||||
(Hanshin-Daishoten, Hanshin-Daishoten) -> True
|
||||
(Spring-Stakes, Spring-Stakes) -> True
|
||||
(Nikkei-Sho, Nikkei-Sho) -> True
|
||||
(Hanshin-Umamusume-Stakes, Hanshin-Umamusume-Stakes) -> True
|
||||
(New-Zealand-Trophy, New-Zealand-Trophy) -> True
|
||||
(Yomiuri-Milers-Cup, Yomiuri-Milers-Cup) -> True
|
||||
(Flora-Stakes, Flora-Stakes) -> True
|
||||
(Aoba-Sho, Aoba-Sho) -> True
|
||||
(Kyoto-Shimbun-Hai, Kyoto-Shimbun-Hai) -> True
|
||||
(Keio-Hai-Spring-Cup, Keio-Hai-Spring-Cup) -> True
|
||||
(Meguro-Kinen, Meguro-Kinen) -> True
|
||||
(Sapporo-Kinen, Sapporo-Kinen) -> True
|
||||
(Centaur-Stakes, Centaur-Stakes) -> True
|
||||
(Rose-Stakes, Rose-Stakes) -> True
|
||||
(St-Lite-Kinen, St-Lite-Kinen) -> True
|
||||
(Kobe-Shimbun-Hai, Kobe-Shimbun-Hai) -> True
|
||||
(All-Comers, All-Comers) -> True
|
||||
(Mainichi-Okan, Mainichi-Okan) -> True
|
||||
(Kyoto-Daishoten, Kyoto-Daishoten) -> True
|
||||
(Fuchu-Umamusume-Stakes, Fuchu-Umamusume-Stakes) -> True
|
||||
(Fuji-Stakes, Fuji-Stakes) -> True
|
||||
(Swan-Stakes, Swan-Stakes) -> True
|
||||
(Keio-Hai-Junior-Stakes, Keio-Hai-Junior-Stakes) -> True
|
||||
(Copa-Republica-Argentina, Copa-Republica-Argentina) -> True
|
||||
(Daily-Hai-Junior-Stakes, Daily-Hai-Junior-Stakes) -> True
|
||||
(Stayers-Stakes, Stayers-Stakes) -> True
|
||||
(Hanshin-Cup, Hanshin-Cup) -> True
|
||||
(Kyoto-Kimpai, Kyoto-Kimpai) -> True
|
||||
(Nakayama-Kimpai, Nakayama-Kimpai) -> True
|
||||
(Shinzan-Kinen, Shinzan-Kinen) -> True
|
||||
(Fairy-Stakes, Fairy-Stakes) -> True
|
||||
(Aichi-Hai, Aichi-Hai) -> True
|
||||
(Keisei-Hai, Keisei-Hai) -> True
|
||||
(Silk-Road-Stakes, Silk-Road-Stakes) -> True
|
||||
(Negishi-Stakes, Negishi-Stakes) -> True
|
||||
(Kisaragi-Sho, Kisaragi-Sho) -> True
|
||||
(Tokyo-Shimbun-Hai, Tokyo-Shimbun-Hai) -> True
|
||||
(Queen-Cup, Queen-Cup) -> True
|
||||
(Kyodo-News-Hai, Kyodo-News-Hai) -> True
|
||||
(Kyoto-Umamusume-Stakes, Kyoto-Umamusume-Stakes) -> True
|
||||
(Diamond-Stakes, Diamond-Stakes) -> True
|
||||
(Kokura-Daishoten, Kokura-Daishoten) -> True
|
||||
(Arlington-Cup, Arlington-Cup) -> True
|
||||
(Hankyu-Hai, Hankyu-Hai) -> True
|
||||
(Ocean-Stakes, Ocean-Stakes) -> True
|
||||
(Nakayama-Umamusume-Stakes, Nakayama-Umamusume-Stakes) -> True
|
||||
(Falcon-Stakes, Falcon-Stakes) -> True
|
||||
(Flower-Cup, Flower-Cup) -> True
|
||||
(Mainichi-Hai, Mainichi-Hai) -> True
|
||||
(March-Stakes, March-Stakes) -> True
|
||||
(Lord-Derby-Challenge-Trophy, Lord-Derby-Challenge-Trophy) -> True
|
||||
(Antares-Stakes, Antares-Stakes) -> True
|
||||
(Fukushima-Umamusume-Stakes, Fukushima-Umamusume-Stakes) -> True
|
||||
(Niigata-Daishoten, Niigata-Daishoten) -> True
|
||||
(Heian-Stakes, Heian-Stakes) -> True
|
||||
(Aoi-Stakes, Aoi-Stakes) -> True
|
||||
(Naruo-Kinen, Naruo-Kinen) -> True
|
||||
(Mermaid-Stakes, Mermaid-Stakes) -> True
|
||||
(Epsom-Cup, Epsom-Cup) -> True
|
||||
(Unicorn-Stakes, Unicorn-Stakes) -> True
|
||||
(Hakodate-Sprint-Stakes, Hakodate-Sprint-Stakes) -> True
|
||||
(CBC-Sho, CBC-Sho) -> True
|
||||
(Radio-Nikkei-Sho, Radio-Nikkei-Sho) -> True
|
||||
(Procyon-Stakes, Procyon-Stakes) -> True
|
||||
(Tanabata-Sho, Tanabata-Sho) -> True
|
||||
(Hakodate-Kinen, Hakodate-Kinen) -> True
|
||||
(Chukyo-Kinen, Chukyo-Kinen) -> True
|
||||
(Hakodate-Junior-Stakes, Hakodate-Junior-Stakes) -> True
|
||||
(Ibis-Summer-Dash, Ibis-Summer-Dash) -> True
|
||||
(Queen-Stakes, Queen-Stakes) -> True
|
||||
(Kokura-Kinen, Kokura-Kinen) -> True
|
||||
(Leopard-Stakes, Leopard-Stakes) -> True
|
||||
(Sekiya-Kinen, Sekiya-Kinen) -> True
|
||||
(Elm-Stakes, Elm-Stakes) -> True
|
||||
(Kitakyushu-Kinen, Kitakyushu-Kinen) -> True
|
||||
(Niigata-Junior-Stakes, Niigata-Junior-Stakes) -> True
|
||||
(Keeneland-Cup, Keeneland-Cup) -> True
|
||||
(Sapporo-Junior-Stakes, Sapporo-Junior-Stakes) -> True
|
||||
(Kokura-Junior-Stakes, Kokura-Junior-Stakes) -> True
|
||||
(Niigata-Kinen, Niigata-Kinen) -> True
|
||||
(Shion-Stakes, Shion-Stakes) -> True
|
||||
(Keisei-Hai-Autumn-Handicap, Keisei-Hai-Autumn-Handicap) -> True
|
||||
(Sirius-Stakes, Sirius-Stakes) -> True
|
||||
(Saudi-Arabia-Royal-Cup, Saudi-Arabia-Royal-Cup) -> True
|
||||
(Artemis-Stakes, Artemis-Stakes) -> True
|
||||
(Fantasy-Stakes, Fantasy-Stakes) -> True
|
||||
(Miyako-Stakes, Miyako-Stakes) -> True
|
||||
(Musashino-Stakes, Musashino-Stakes) -> True
|
||||
(Fukushima-Kinen, Fukushima-Kinen) -> True
|
||||
(Tokyo-Sports-Hai-Junior-Stakes, Tokyo-Sports-Hai-Junior-Stakes) -> True
|
||||
(Kyoto-Junior-Stakes, Kyoto-Junior-Stakes) -> True
|
||||
(Keihan-Hai, Keihan-Hai) -> True
|
||||
(Challenge-Cup, Challenge-Cup) -> True
|
||||
(Chunichi-Shimbun-Hai, Chunichi-Shimbun-Hai) -> True
|
||||
(Capella-Stakes, Capella-Stakes) -> True
|
||||
(Turquoise-Stakes, Turquoise-Stakes) -> True
|
||||
(_, _) -> False
|
||||
pub fun race-detail/show(r: race-detail): string
|
||||
val Race-detail(Race-id(id), name) = r
|
||||
name ++ " (ID " ++ id.show ++ ")"
|
||||
|
||||
// Race grades.
|
||||
pub type grade
|
||||
@@ -431,250 +35,86 @@ pub type grade
|
||||
G1
|
||||
EX
|
||||
|
||||
pub fun career-race/grade(r: career-race): grade
|
||||
match r
|
||||
February-Stakes -> G1
|
||||
Takamatsunomiya-Kinen -> G1
|
||||
Osaka-Hai -> G1
|
||||
Oka-Sho -> G1
|
||||
Satsuki-Sho -> G1
|
||||
Tenno-Sho-Spring -> G1
|
||||
NHK-Mile-Cup -> G1
|
||||
Victoria-Mile -> G1
|
||||
Japanese-Oaks -> G1
|
||||
Japanese-Derby -> G1
|
||||
Yasuda-Kinen -> G1
|
||||
Takarazuka-Kinen -> G1
|
||||
Sprinters-Stakes -> G1
|
||||
Shuka-Sho -> G1
|
||||
Kikuka-Sho -> G1
|
||||
Tenno-Sho-Autumn -> G1
|
||||
Queen-Elizabeth-II-Cup -> G1
|
||||
Mile-Championship -> G1
|
||||
Japan-Cup -> G1
|
||||
Champions-Cup -> G1
|
||||
Hanshin-Juvenile-Fillies -> G1
|
||||
Asahi-Hai-Futurity-Stakes -> G1
|
||||
Arima-Kinen -> G1
|
||||
Hopeful-Stakes -> G1
|
||||
Tokyo-Daishoten -> G1
|
||||
JBC-Classic -> G1
|
||||
JBC-Sprint -> G1
|
||||
JBC-Ladies-Classic -> G1
|
||||
Japan-Dirt-Derby -> G1
|
||||
Teio-Sho -> G1
|
||||
Nikkei-Shinshun-Hai -> G2
|
||||
Tokai-Stakes -> G2
|
||||
American-Jockey-Club-Cup -> G2
|
||||
Kyoto-Kinen -> G2
|
||||
Nakayama-Kinen -> G2
|
||||
Tulip-Sho -> G2
|
||||
Yayoi-Sho -> G2
|
||||
Kinko-Sho -> G2
|
||||
Fillies-Revue -> G2
|
||||
Hanshin-Daishoten -> G2
|
||||
Spring-Stakes -> G2
|
||||
Nikkei-Sho -> G2
|
||||
Hanshin-Umamusume-Stakes -> G2
|
||||
New-Zealand-Trophy -> G2
|
||||
Yomiuri-Milers-Cup -> G2
|
||||
Flora-Stakes -> G2
|
||||
Aoba-Sho -> G2
|
||||
Kyoto-Shimbun-Hai -> G2
|
||||
Keio-Hai-Spring-Cup -> G2
|
||||
Meguro-Kinen -> G2
|
||||
Sapporo-Kinen -> G2
|
||||
Centaur-Stakes -> G2
|
||||
Rose-Stakes -> G2
|
||||
St-Lite-Kinen -> G2
|
||||
Kobe-Shimbun-Hai -> G2
|
||||
All-Comers -> G2
|
||||
Mainichi-Okan -> G2
|
||||
Kyoto-Daishoten -> G2
|
||||
Fuchu-Umamusume-Stakes -> G2
|
||||
Fuji-Stakes -> G2
|
||||
Swan-Stakes -> G2
|
||||
Keio-Hai-Junior-Stakes -> G2
|
||||
Copa-Republica-Argentina -> G2
|
||||
Daily-Hai-Junior-Stakes -> G2
|
||||
Stayers-Stakes -> G2
|
||||
Hanshin-Cup -> G2
|
||||
Kyoto-Kimpai -> G3
|
||||
Nakayama-Kimpai -> G3
|
||||
Shinzan-Kinen -> G3
|
||||
Fairy-Stakes -> G3
|
||||
Aichi-Hai -> G3
|
||||
Keisei-Hai -> G3
|
||||
Silk-Road-Stakes -> G3
|
||||
Negishi-Stakes -> G3
|
||||
Kisaragi-Sho -> G3
|
||||
Tokyo-Shimbun-Hai -> G3
|
||||
Queen-Cup -> G3
|
||||
Kyodo-News-Hai -> G3
|
||||
Kyoto-Umamusume-Stakes -> G3
|
||||
Diamond-Stakes -> G3
|
||||
Kokura-Daishoten -> G3
|
||||
Arlington-Cup -> G3
|
||||
Hankyu-Hai -> G3
|
||||
Ocean-Stakes -> G3
|
||||
Nakayama-Umamusume-Stakes -> G3
|
||||
Falcon-Stakes -> G3
|
||||
Flower-Cup -> G3
|
||||
Mainichi-Hai -> G3
|
||||
March-Stakes -> G3
|
||||
Lord-Derby-Challenge-Trophy -> G3
|
||||
Antares-Stakes -> G3
|
||||
Fukushima-Umamusume-Stakes -> G3
|
||||
Niigata-Daishoten -> G3
|
||||
Heian-Stakes -> G3
|
||||
Aoi-Stakes -> G3
|
||||
Naruo-Kinen -> G3
|
||||
Mermaid-Stakes -> G3
|
||||
Epsom-Cup -> G3
|
||||
Unicorn-Stakes -> G3
|
||||
Hakodate-Sprint-Stakes -> G3
|
||||
CBC-Sho -> G3
|
||||
Radio-Nikkei-Sho -> G3
|
||||
Procyon-Stakes -> G3
|
||||
Tanabata-Sho -> G3
|
||||
Hakodate-Kinen -> G3
|
||||
Chukyo-Kinen -> G3
|
||||
Hakodate-Junior-Stakes -> G3
|
||||
Ibis-Summer-Dash -> G3
|
||||
Queen-Stakes -> G3
|
||||
Kokura-Kinen -> G3
|
||||
Leopard-Stakes -> G3
|
||||
Sekiya-Kinen -> G3
|
||||
Elm-Stakes -> G3
|
||||
Kitakyushu-Kinen -> G3
|
||||
Niigata-Junior-Stakes -> G3
|
||||
Keeneland-Cup -> G3
|
||||
Sapporo-Junior-Stakes -> G3
|
||||
Kokura-Junior-Stakes -> G3
|
||||
Niigata-Kinen -> G3
|
||||
Shion-Stakes -> G3
|
||||
Keisei-Hai-Autumn-Handicap -> G3
|
||||
Sirius-Stakes -> G3
|
||||
Saudi-Arabia-Royal-Cup -> G3
|
||||
Artemis-Stakes -> G3
|
||||
Fantasy-Stakes -> G3
|
||||
Miyako-Stakes -> G3
|
||||
Musashino-Stakes -> G3
|
||||
Fukushima-Kinen -> G3
|
||||
Tokyo-Sports-Hai-Junior-Stakes -> G3
|
||||
Kyoto-Junior-Stakes -> G3
|
||||
Keihan-Hai -> G3
|
||||
Challenge-Cup -> G3
|
||||
Chunichi-Shimbun-Hai -> G3
|
||||
Capella-Stakes -> G3
|
||||
Turquoise-Stakes -> G3
|
||||
|
||||
pub type title
|
||||
Classic-Triple-Crown
|
||||
Triple-Tiara
|
||||
Senior-Spring-Triple-Crown
|
||||
Senior-Autumn-Triple-Crown
|
||||
Tenno-Sweep
|
||||
Dual-Grand-Prix
|
||||
Dual-Miles
|
||||
Dual-Sprints
|
||||
Dual-Dirts
|
||||
|
||||
// Get the titles that a race contributes to.
|
||||
inline fun career-race/titles(r: career-race): list<title>
|
||||
match r
|
||||
Satsuki-Sho -> [Classic-Triple-Crown]
|
||||
Japanese-Derby -> [Classic-Triple-Crown]
|
||||
Kikuka-Sho -> [Classic-Triple-Crown]
|
||||
Oka-Sho -> [Triple-Tiara]
|
||||
Japanese-Oaks -> [Triple-Tiara]
|
||||
Shuka-Sho -> [Triple-Tiara]
|
||||
Osaka-Hai -> [Senior-Spring-Triple-Crown]
|
||||
Tenno-Sho-Spring -> [Senior-Spring-Triple-Crown, Tenno-Sweep]
|
||||
Takarazuka-Kinen -> [Senior-Spring-Triple-Crown, Dual-Grand-Prix]
|
||||
Tenno-Sho-Autumn -> [Senior-Autumn-Triple-Crown, Tenno-Sweep]
|
||||
Japan-Cup -> [Senior-Autumn-Triple-Crown]
|
||||
Arima-Kinen -> [Senior-Autumn-Triple-Crown, Dual-Grand-Prix]
|
||||
Yasuda-Kinen -> [Dual-Miles]
|
||||
Mile-Championship -> [Dual-Miles]
|
||||
Takamatsunomiya-Kinen -> [Dual-Sprints]
|
||||
Sprinters-Stakes -> [Dual-Sprints]
|
||||
February-Stakes -> [Dual-Dirts]
|
||||
Champions-Cup -> [Dual-Dirts]
|
||||
_ -> []
|
||||
|
||||
// Get the races that a title requires.
|
||||
inline fun title/races(t: title): list<career-race>
|
||||
match t
|
||||
Classic-Triple-Crown -> [Satsuki-Sho, Japanese-Derby, Kikuka-Sho]
|
||||
Triple-Tiara -> [Oka-Sho, Japanese-Oaks, Shuka-Sho]
|
||||
Senior-Spring-Triple-Crown -> [Osaka-Hai, Tenno-Sho-Spring, Takarazuka-Kinen]
|
||||
Senior-Autumn-Triple-Crown -> [Tenno-Sho-Autumn, Japan-Cup, Arima-Kinen]
|
||||
Tenno-Sweep -> [Tenno-Sho-Spring, Tenno-Sho-Autumn]
|
||||
Dual-Grand-Prix -> [Takarazuka-Kinen, Arima-Kinen]
|
||||
Dual-Miles -> [Yasuda-Kinen, Mile-Championship]
|
||||
Dual-Sprints -> [Takamatsunomiya-Kinen, Sprinters-Stakes]
|
||||
Dual-Dirts -> [February-Stakes, Champions-Cup]
|
||||
|
||||
// Get all titles earned by an uma.
|
||||
pub fun career/titles(results: list<race-result>): list<title>
|
||||
val wins = results.flatmap-maybe() fn(r) (if r.place == 1 then Just(r.race) else Nothing)
|
||||
val title-wins = wins.filter(_.titles.is-cons).linear-set
|
||||
val titles = title-wins.list.flatmap(_.titles).linear-set.list
|
||||
titles.filter(_.races.linear-set.is-subset-of(title-wins))
|
||||
// Automatically generated.
|
||||
// Comparison of the `grade` type.
|
||||
pub fun grade/cmp(this : grade, other : grade) : e order
|
||||
match (this, other)
|
||||
(Pre-OP, Pre-OP) -> Eq
|
||||
(Pre-OP, _) -> Lt
|
||||
(_, Pre-OP) -> Gt
|
||||
(OP, OP) -> Eq
|
||||
(OP, _) -> Lt
|
||||
(_, OP) -> Gt
|
||||
(G3, G3) -> Eq
|
||||
(G3, _) -> Lt
|
||||
(_, G3) -> Gt
|
||||
(G2, G2) -> Eq
|
||||
(G2, _) -> Lt
|
||||
(_, G2) -> Gt
|
||||
(G1, G1) -> Eq
|
||||
(G1, _) -> Lt
|
||||
(_, G1) -> Gt
|
||||
(EX, EX) -> Eq
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `title` type.
|
||||
pub fun title/(==)(this : title, other : title) : e bool
|
||||
// Shows a string representation of the `grade` type.
|
||||
pub fun grade/show(this : grade) : e string
|
||||
match this
|
||||
Pre-OP -> "Pre-OP"
|
||||
OP -> "OP"
|
||||
G3 -> "G3"
|
||||
G2 -> "G2"
|
||||
G1 -> "G1"
|
||||
EX -> "EX"
|
||||
|
||||
pub struct saddle-detail
|
||||
saddle-id: saddle-id
|
||||
name: string
|
||||
races: list<race-id>
|
||||
saddle-type: saddle-type
|
||||
// For careers with unusual races, granted saddles also differ.
|
||||
// This field holds the normal saddle's ID for such cases.
|
||||
primary: saddle-id
|
||||
|
||||
pub fun saddle/detail(
|
||||
id: saddle-id,
|
||||
?saddle/show: (saddle-id) -> string,
|
||||
?saddle/races: (saddle-id) -> list<race-id>,
|
||||
?saddle/saddle-type: (saddle-id) -> saddle-type,
|
||||
?saddle/primary: (saddle-id) -> saddle-id
|
||||
): saddle-detail
|
||||
Saddle-detail(id, id.show, id.races, id.saddle-type, id.primary)
|
||||
|
||||
pub fun saddle-detail/show(s: saddle-detail): string
|
||||
val Saddle-detail(Saddle-id(id), name, _, _, Saddle-id(primary)) = s
|
||||
if id == primary then name else name ++ " (Alternate " ++ id.show ++ ")"
|
||||
|
||||
// Types of saddles.
|
||||
pub type saddle-type
|
||||
Honor // multiple race wins: classic triple crown, dual grand prix, &c.
|
||||
G3-Win
|
||||
G2-Win
|
||||
G1-Win
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `saddle-type` type.
|
||||
pub fun saddle-type/show(this : saddle-type) : e string
|
||||
match this
|
||||
Honor -> "Honor"
|
||||
G3-Win -> "G3"
|
||||
G2-Win -> "G2"
|
||||
G1-Win -> "G1"
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `saddle-type` type.
|
||||
pub fun saddle-type/(==)(this : saddle-type, other : saddle-type) : e bool
|
||||
match (this, other)
|
||||
(Classic-Triple-Crown, Classic-Triple-Crown) -> True
|
||||
(Triple-Tiara, Triple-Tiara) -> True
|
||||
(Senior-Spring-Triple-Crown, Senior-Spring-Triple-Crown) -> True
|
||||
(Senior-Autumn-Triple-Crown, Senior-Autumn-Triple-Crown) -> True
|
||||
(Tenno-Sweep, Tenno-Sweep) -> True
|
||||
(Dual-Grand-Prix, Dual-Grand-Prix) -> True
|
||||
(Dual-Miles, Dual-Miles) -> True
|
||||
(Dual-Sprints, Dual-Sprints) -> True
|
||||
(Dual-Dirts, Dual-Dirts) -> True
|
||||
(Honor, Honor) -> True
|
||||
(G3-Win, G3-Win) -> True
|
||||
(G2-Win, G2-Win) -> True
|
||||
(G1-Win, G1-Win) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `title` type.
|
||||
pub fun title/show(this : title) : e string
|
||||
match this
|
||||
Classic-Triple-Crown -> "Classic Triple Crown"
|
||||
Triple-Tiara -> "Triple Tiara"
|
||||
Senior-Spring-Triple-Crown -> "Senior Spring Triple Crown"
|
||||
Senior-Autumn-Triple-Crown -> "Senior Autumn Triple Crown"
|
||||
Tenno-Sweep -> "Tenno Sweep"
|
||||
Dual-Grand-Prix -> "Dual Grand Prix"
|
||||
Dual-Miles -> "Dual Miles"
|
||||
Dual-Sprints -> "Dual Sprints"
|
||||
Dual-Dirts -> "Dual Dirts"
|
||||
|
||||
// Graded race that a veteran ran.
|
||||
pub struct race-result
|
||||
race: career-race
|
||||
place: int
|
||||
turn: turn
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `race-result` type.
|
||||
pub fun race-result/(==)(this : race-result, other : race-result) : e bool
|
||||
match (this, other)
|
||||
(Race-result(race, place, turn), Race-result(race', place', turn')) -> race == race' && place == place' && turn == turn'
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `race-result` type.
|
||||
pub fun race-result/show(this : race-result) : e string
|
||||
match this
|
||||
Race-result(race, place, turn) -> turn.show ++ " " ++ race.show ++ ": " ++ place.show
|
||||
|
||||
// Determine whether two race results are for the same race.
|
||||
// This differs from (==) which also requires the race to be on the same turn.
|
||||
pub fun race-result/same(a: race-result, b: race-result): bool
|
||||
a.race == b.race
|
||||
|
||||
// Turn that a race occurred.
|
||||
pub struct turn
|
||||
year: turn-year
|
||||
|
||||
138
horse/skill.go
138
horse/skill.go
@@ -27,35 +27,37 @@ func (x TenThousandths) String() string {
|
||||
|
||||
// Skill is the internal data about a skill.
|
||||
type Skill struct {
|
||||
ID SkillID
|
||||
Name string
|
||||
Description string
|
||||
Group int32
|
||||
Rarity int8
|
||||
GroupRate int8
|
||||
GradeValue int32
|
||||
WitCheck bool
|
||||
Activations []Activation
|
||||
SPCost int
|
||||
IconID int
|
||||
ID SkillID `json:"skill_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Group SkillGroupID `json:"group"`
|
||||
Rarity int8 `json:"rarity"`
|
||||
GroupRate int8 `json:"group_rate"`
|
||||
GradeValue int32 `json:"grade_value,omitzero"`
|
||||
WitCheck bool `json:"wit_check"`
|
||||
Activations []Activation `json:"activations"`
|
||||
UniqueOwner string `json:"unique_owner,omitzero"`
|
||||
SPCost int `json:"sp_cost,omitzero"`
|
||||
IconID int `json:"icon_id"`
|
||||
}
|
||||
|
||||
// Activation is the parameters controlling when a skill activates.
|
||||
type Activation struct {
|
||||
Precondition string
|
||||
Condition string
|
||||
Duration TenThousandths
|
||||
Cooldown TenThousandths
|
||||
Abilities []Ability
|
||||
Precondition string `json:"precondition,omitzero"`
|
||||
Condition string `json:"condition"`
|
||||
Duration TenThousandths `json:"duration,omitzero"`
|
||||
DurScale DurScale `json:"dur_scale"`
|
||||
Cooldown TenThousandths `json:"cooldown,omitzero"`
|
||||
Abilities []Ability `json:"abilities"`
|
||||
}
|
||||
|
||||
// Ability is an individual effect applied by a skill.
|
||||
type Ability struct {
|
||||
Type AbilityType
|
||||
ValueUsage AbilityValueUsage
|
||||
Value TenThousandths
|
||||
Target AbilityTarget
|
||||
TargetValue int32
|
||||
Type AbilityType `json:"type"`
|
||||
ValueUsage AbilityValueUsage `json:"value_usage"`
|
||||
Value TenThousandths `json:"value"`
|
||||
Target AbilityTarget `json:"target"`
|
||||
TargetValue int32 `json:"target_value,omitzero"`
|
||||
}
|
||||
|
||||
func (a Ability) String() string {
|
||||
@@ -72,7 +74,7 @@ func (a Ability) String() string {
|
||||
case AbilityVision:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, 'm')
|
||||
case AbilityHP, AbilityCurrentSpeed, AbilityTargetSpeed, AbilityLaneSpeed, AbilityAccel:
|
||||
case AbilityHP:
|
||||
r = append(r, (a.Value * 100).String()...)
|
||||
r = append(r, '%')
|
||||
case AbilityGateDelay:
|
||||
@@ -81,12 +83,36 @@ func (a Ability) String() string {
|
||||
case AbilityFrenzy:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, 's')
|
||||
case AbilityCurrentSpeed, AbilityTargetSpeed, AbilityLaneSpeed:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, " m/s"...)
|
||||
case AbilityAccel:
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, " m/s²"...)
|
||||
case AbilityLaneChange:
|
||||
r = append(r, (a.Value * 100).String()...)
|
||||
r = append(r, "% of track width"...)
|
||||
r = append(r, a.Value.String()...)
|
||||
r = append(r, " track widths"...)
|
||||
}
|
||||
}
|
||||
if a.Target != TargetSelf {
|
||||
switch a.Target {
|
||||
case TargetSelf:
|
||||
// do nothing
|
||||
case TargetStyle, TargetRushingStyle:
|
||||
// TargetValue is the style to target, not the number of targets.
|
||||
r = append(r, " to "...)
|
||||
r = append(r, a.Target.String()...)
|
||||
switch a.TargetValue {
|
||||
case 1:
|
||||
r = append(r, " Front Runner"...)
|
||||
case 2:
|
||||
r = append(r, " Pace Chaser"...)
|
||||
case 3:
|
||||
r = append(r, " Late Surger"...)
|
||||
case 4:
|
||||
r = append(r, " End Closer"...)
|
||||
}
|
||||
default:
|
||||
// For other targeting types, TargetValue is either irrelevant or limit.
|
||||
r = append(r, " to "...)
|
||||
if a.TargetValue > 1 && a.TargetValue < 18 {
|
||||
r = strconv.AppendInt(r, int64(a.TargetValue), 10)
|
||||
@@ -101,6 +127,18 @@ func (a Ability) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
type DurScale int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type DurScale -trimprefix Duration -linecomment
|
||||
const (
|
||||
DurationDirect DurScale = 1 // directly
|
||||
DurationFrontDistance DurScale = 2 // scaling with distance from the front
|
||||
DurationRemainingHP DurScale = 3 // scaling with remaining HP
|
||||
DurationIncrementPass DurScale = 4 // increasing with each pass while active
|
||||
DurationMidSideBlock DurScale = 5 // scaling with mid-race phase blocked side time
|
||||
DurationRemainingHP2 DurScale = 7 // scaling with remaining HP
|
||||
)
|
||||
|
||||
type AbilityType int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type AbilityType -trimprefix Ability -linecomment
|
||||
@@ -126,12 +164,24 @@ type AbilityValueUsage int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type AbilityValueUsage -trimprefix ValueUsage -linecomment
|
||||
const (
|
||||
ValueUsageDirect AbilityValueUsage = 1 // directly
|
||||
ValueUsageTeamSpeed AbilityValueUsage = 2 // scaling with team Speed
|
||||
ValueUsageTeamStamina AbilityValueUsage = 3 // scaling with team Stamina
|
||||
ValueUsageTeamPower AbilityValueUsage = 4 // scaling with team Power
|
||||
ValueUsageTeamGuts AbilityValueUsage = 5 // scaling with team Guts
|
||||
ValueUsageTeamWit AbilityValueUsage = 6 // scaling with team Wit
|
||||
ValueUsageDirect AbilityValueUsage = 1 // directly
|
||||
ValueUsageSkillCount AbilityValueUsage = 2 // scaling with the number of skills
|
||||
ValueUsageTeamSpeed AbilityValueUsage = 3 // scaling with team Speed
|
||||
ValueUsageTeamStamina AbilityValueUsage = 4 // scaling with team Stamina
|
||||
ValueUsageTeamPower AbilityValueUsage = 5 // scaling with team Power
|
||||
ValueUsageTeamGuts AbilityValueUsage = 6 // scaling with team Guts
|
||||
ValueUsageTeamWit AbilityValueUsage = 7 // scaling with team Wit
|
||||
ValueUsageRandom AbilityValueUsage = 8 // with a random 0× to 0.04× multiplier
|
||||
ValueUsageRandom2 AbilityValueUsage = 9 // with a random 0× to 0.04× multiplier
|
||||
ValueUsageClimax AbilityValueUsage = 10 // scaling with the number of races won in training
|
||||
ValueUsageMaxStat AbilityValueUsage = 13 // scaling with the highest raw stat
|
||||
ValueUsageGreenCount AbilityValueUsage = 14 // scaling with the number of Passive skills activated
|
||||
ValueUsageDistAdd AbilityValueUsage = 19 // plus extra when far from the lead
|
||||
ValueUsageMidSideBlock AbilityValueUsage = 20 // scaling with mid-race phase blocked side time
|
||||
ValueUsageSpeed AbilityValueUsage = 22 // scaling with overall speed
|
||||
ValueUsageSpeed2 AbilityValueUsage = 23 // scaling with overall speed
|
||||
ValueUsageArcPotential AbilityValueUsage = 24 // scaling with L'Arc global potential
|
||||
ValueUsageMaxLead AbilityValueUsage = 25 // scaling with the longest lead obtained in the first ⅔
|
||||
)
|
||||
|
||||
type AbilityTarget int8
|
||||
@@ -152,3 +202,29 @@ const (
|
||||
TargetCharacter AbilityTarget = 22 // specific character
|
||||
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"`
|
||||
}
|
||||
|
||||
249
horse/skill.kk
Normal file
249
horse/skill.kk
Normal file
@@ -0,0 +1,249 @@
|
||||
module horse/skill
|
||||
|
||||
// This module contains skill-related definitions
|
||||
// common to all versions of the game.
|
||||
|
||||
import std/num/decimal
|
||||
import horse/game-id
|
||||
import horse/movement
|
||||
|
||||
// Full details about a skill.
|
||||
pub struct skill-detail
|
||||
skill-id: skill-id
|
||||
name: string
|
||||
description: string
|
||||
group-id: skill-group-id
|
||||
rarity: rarity
|
||||
group-rate: int
|
||||
grade-value: int
|
||||
wit-check: bool
|
||||
activations: list<activation>
|
||||
owner: maybe<uma-id>
|
||||
sp-cost: int
|
||||
icon-id: skill-icon-id
|
||||
|
||||
pub fun detail(
|
||||
s: skill-id,
|
||||
?skill/show: (skill-id) -> string,
|
||||
?skill/description: (skill-id) -> string,
|
||||
?skill/group: (skill-id) -> skill-group-id,
|
||||
?skill/rarity: (skill-id) -> rarity,
|
||||
?skill/group-rate: (skill-id) -> int,
|
||||
?skill/grade-value: (skill-id) -> int,
|
||||
?skill/wit-check: (skill-id) -> bool,
|
||||
?skill/activations: (skill-id) -> list<activation>,
|
||||
?skill/unique-owner: (skill-id) -> maybe<uma-id>,
|
||||
?skill/sp-cost: (skill-id) -> int,
|
||||
?skill/icon-id: (skill-id) -> skill-icon-id
|
||||
): skill-detail
|
||||
Skill-detail(
|
||||
s,
|
||||
s.show,
|
||||
s.description,
|
||||
s.group,
|
||||
s.rarity,
|
||||
s.group-rate,
|
||||
s.grade-value,
|
||||
s.wit-check,
|
||||
s.activations,
|
||||
s.unique-owner,
|
||||
s.sp-cost,
|
||||
s.icon-id
|
||||
)
|
||||
|
||||
pub fun skill-detail/show(d: skill-detail, ?character/show: (character-id) -> string, ?uma/show: (uma-id) -> string): string
|
||||
val Skill-detail(Skill-id(id), name, desc, _, rarity, _, grade-value, wit-check, activations, owner, sp-cost, _) = d
|
||||
val r = name ++ " (ID " ++ id.show ++ "): " ++ desc ++ " " ++ activations.map(activation/show).join(". ") ++ (if wit-check then ". Wit check. " else ". No wit check. ") ++ rarity.show ++ " costing " ++ sp-cost.show ++ " SP, worth " ++ grade-value.show ++ " grade value."
|
||||
match owner
|
||||
Nothing -> r
|
||||
Just(owner-id) -> match owner-id.show
|
||||
"" -> r ++ " Unique skill of Uma with ID " ++ owner-id.show ++ "."
|
||||
owner-name -> r ++ " Unique skill of " ++ owner-name ++ "."
|
||||
|
||||
// Skill rarity levels.
|
||||
pub type rarity
|
||||
Common // white
|
||||
Rare // gold
|
||||
Unique-Low // 1*/2* unique
|
||||
Unique-Upgraded // 3*+ unique on a trainee upgraded from 1*/2*
|
||||
Unique // base 3* unique
|
||||
|
||||
pub fun rarity/show(r: rarity): string
|
||||
match r
|
||||
Common -> "Common"
|
||||
Rare -> "Rare"
|
||||
Unique-Low -> "Unique (1\u2606/2\u2606)"
|
||||
Unique-Upgraded -> "Unique (3\u2606+ from 1\u2606/2\u2606 upgraded)"
|
||||
Unique -> "Unique (3\u2606+)"
|
||||
|
||||
// Condition and precondition logic.
|
||||
pub alias condition = string
|
||||
|
||||
// Activation conditions and effects.
|
||||
// A skill has one or two activations.
|
||||
pub struct activation
|
||||
precondition: condition
|
||||
condition: condition
|
||||
duration: decimal // seconds
|
||||
dur-scale: dur-scale
|
||||
cooldown: decimal // seconds
|
||||
abilities: list<ability> // one to three elements
|
||||
|
||||
pub fun activation/show(a: activation, ?character/show: (character-id) -> string): string
|
||||
match a
|
||||
Activation("", condition, duration, _, _, abilities) | !duration.is-pos -> condition ++ " -> " ++ abilities.show
|
||||
Activation("", condition, duration, Direct-Dur, cooldown, abilities) | cooldown >= 500.decimal -> condition ++ " -> for " ++ duration.show ++ "s, " ++ abilities.show
|
||||
Activation("", condition, duration, dur-scale, cooldown, abilities) | cooldown >= 500.decimal -> condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ ", " ++ abilities.show
|
||||
Activation("", condition, duration, Direct-Dur, cooldown, abilities) -> condition ++ " -> for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
Activation("", condition, duration, dur-scale, cooldown, abilities) -> condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ " on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
Activation(precondition, condition, duration, _, _, abilities) | !duration.is-pos -> precondition ++ " -> " ++ condition ++ " -> " ++ abilities.show
|
||||
Activation(precondition, condition, duration, Direct-Dur, cooldown, abilities) | cooldown >= 500.decimal -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s, " ++ abilities.show
|
||||
Activation(precondition, condition, duration, dur-scale, cooldown, abilities) | cooldown >= 500.decimal -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ ", " ++ abilities.show
|
||||
Activation(precondition, condition, duration, Direct-Dur, cooldown, abilities) -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
Activation(precondition, condition, duration, dur-scale, cooldown, abilities) -> precondition ++ " -> " ++ condition ++ " -> for " ++ duration.show ++ "s " ++ dur-scale.show ++ " on " ++ cooldown.show ++ "s cooldown, " ++ abilities.show
|
||||
|
||||
// Special scaling types for skill activation durations.
|
||||
pub type dur-scale
|
||||
Direct-Dur
|
||||
Front-Distance-Dur
|
||||
Multiply-Remaining-HP
|
||||
Increment-Pass
|
||||
Midrace-Side-Block-Time-Dur
|
||||
Multiply-Remaining-HP2
|
||||
|
||||
pub fun dur-scale/show(s: dur-scale): string
|
||||
match s
|
||||
Direct-Dur -> "with no scaling"
|
||||
Front-Distance-Dur -> "scaling with distance from the front"
|
||||
Multiply-Remaining-HP -> "scaling with remaining HP"
|
||||
Increment-Pass -> "increasing with each pass while active"
|
||||
Midrace-Side-Block-Time-Dur -> "scaling with mid-race phase blocked side time"
|
||||
Multiply-Remaining-HP2 -> "scaling with remaining HP"
|
||||
|
||||
// Effects of activating a skill.
|
||||
pub struct ability
|
||||
ability-type: ability-type
|
||||
value-usage: value-usage
|
||||
target: target
|
||||
|
||||
pub fun ability/show(a: ability, ?character/show: (character-id) -> string): string
|
||||
match a
|
||||
Ability(t, Direct, Self) -> t.show
|
||||
Ability(t, Direct, target) -> t.show ++ " " ++ target.show
|
||||
Ability(t, v, Self) -> t.show ++ " " ++ v.show
|
||||
Ability(t, v, target) -> t.show ++ " " ++ target.show ++ " " ++ v.show
|
||||
|
||||
// Skill ability effects.
|
||||
pub type ability-type
|
||||
Passive-Speed(bonus: decimal)
|
||||
Passive-Stamina(bonus: decimal)
|
||||
Passive-Power(bonus: decimal)
|
||||
Passive-Guts(bonus: decimal)
|
||||
Passive-Wit(bonus: decimal)
|
||||
Great-Escape
|
||||
Vision(bonus: decimal)
|
||||
HP(rate: decimal)
|
||||
Gate-Delay(rate: decimal)
|
||||
Frenzy(add: decimal)
|
||||
Current-Speed(add: decimal)
|
||||
Target-Speed(add: decimal)
|
||||
Lane-Speed(add: decimal)
|
||||
Accel(add: decimal)
|
||||
Lane-Change(add: decimal)
|
||||
|
||||
pub fun ability-type/show(a: ability-type): string
|
||||
match a
|
||||
Passive-Speed(bonus) -> bonus.show ++ " Speed"
|
||||
Passive-Stamina(bonus) -> bonus.show ++ " Stamina"
|
||||
Passive-Power(bonus) -> bonus.show ++ " Power"
|
||||
Passive-Guts(bonus) -> bonus.show ++ " Guts"
|
||||
Passive-Wit(bonus) -> bonus.show ++ " Wit"
|
||||
Great-Escape -> "enable Great Escape style"
|
||||
Vision(bonus) -> bonus.show ++ " vision"
|
||||
HP(rate) | rate.is-pos -> show(rate * 100.decimal) ++ "% HP recovery"
|
||||
HP(rate) -> show(rate * 100.decimal) ++ "% HP loss"
|
||||
Gate-Delay(rate) -> rate.show ++ "× gate delay"
|
||||
Frenzy(add) -> add.show ++ "s longer Rushed"
|
||||
Current-Speed(rate) -> rate.show ++ "m/s current speed"
|
||||
Target-Speed(rate) -> rate.show ++ "m/s target speed"
|
||||
Lane-Speed(rate) -> rate.show ++ "m/s lane change speed"
|
||||
Accel(rate) -> rate.show ++ "m/s² acceleration"
|
||||
Lane-Change(rate) -> rate.show ++ " course width movement"
|
||||
|
||||
// Special scaling types for skill abilities.
|
||||
pub type value-usage
|
||||
Direct
|
||||
Team-Speed
|
||||
Team-Stamina
|
||||
Team-Power
|
||||
Team-Guts
|
||||
Team-Wit
|
||||
Multiply-Random
|
||||
Multiply-Random2
|
||||
Climax
|
||||
Max-Stat
|
||||
Passive-Count
|
||||
Front-Distance-Add
|
||||
Midrace-Side-Block-Time
|
||||
Speed-Scaling
|
||||
Speed-Scaling2
|
||||
Arc-Global-Potential
|
||||
Max-Lead-Distance
|
||||
|
||||
pub fun value-usage/show(v: value-usage): string
|
||||
match v
|
||||
Direct -> "with no scaling"
|
||||
Team-Speed -> "scaling with team Speed"
|
||||
Team-Stamina -> "scaling with team Stamina"
|
||||
Team-Power -> "scaling with team Power"
|
||||
Team-Guts -> "scaling with team Guts"
|
||||
Team-Wit -> "scaling with team Wit"
|
||||
Multiply-Random -> "scaling with a random multiplier (0×, 0.02×, or 0.04×)"
|
||||
Multiply-Random2 -> "scaling with a random multiplier (0×, 0.02×, or 0.04×)"
|
||||
Climax -> "scaling with the number of races won during training"
|
||||
Max-Stat -> "scaling with the value of the user's highest stat"
|
||||
Passive-Count -> "scaling with the number of Passive skills activated"
|
||||
Front-Distance-Add -> "scaling with distance from the leader"
|
||||
Midrace-Side-Block-Time -> "scaling with mid-race phase blocked side time"
|
||||
Speed-Scaling -> "scaling with overall speed"
|
||||
Speed-Scaling2 -> "scaling with overall speed"
|
||||
Arc-Global-Potential -> "scaling with L'Arc global potential"
|
||||
Max-Lead-Distance -> "scaling with the distance of the longest lead obtained in the first two thirds of the race"
|
||||
|
||||
// Who a skill ability targets.
|
||||
pub type target
|
||||
Self
|
||||
Sympathizers
|
||||
In-View
|
||||
Frontmost(limit: int)
|
||||
Ahead(limit: int)
|
||||
Behind(limit: int)
|
||||
All-Teammates
|
||||
Style(style: style)
|
||||
Rushing-Ahead(limit: int)
|
||||
Rushing-Behind(limit: int)
|
||||
Rushing-Style(style: style)
|
||||
Specific-Character(who: character-id)
|
||||
Triggering
|
||||
|
||||
pub fun target/show(t: target, ?character/show: (character-id) -> string): string
|
||||
match t
|
||||
Self -> "self"
|
||||
Sympathizers -> "others with Sympathy"
|
||||
In-View -> "others in field of view"
|
||||
Frontmost(limit) -> "frontmost " ++ limit.show
|
||||
Ahead(limit) | limit >= 18 -> "others ahead"
|
||||
Ahead(limit) -> "next " ++ limit.show ++ " others ahead"
|
||||
Behind(limit) | limit >= 18 -> "others behind"
|
||||
Behind(limit) -> "next " ++ limit.show ++ " others behind"
|
||||
All-Teammates -> "all teammates"
|
||||
Style(s) -> "other " ++ s.show ++ "s"
|
||||
Rushing-Ahead(limit) | limit >= 18 -> "others rushing ahead"
|
||||
Rushing-Ahead(limit) -> "next " ++ limit.show ++ " others rushing ahead"
|
||||
Rushing-Behind(limit) | limit >= 18 -> "others rushing behind"
|
||||
Rushing-Behind(limit) -> "next " ++ limit.show ++ " others rushing behind"
|
||||
Rushing-Style(s) -> "rushing " ++ s.show ++ "s"
|
||||
Specific-Character(who) -> match who.show
|
||||
"" -> "character with ID " ++ who.show
|
||||
name -> name
|
||||
Triggering -> "whosoever triggered this skill"
|
||||
@@ -1,38 +1,11 @@
|
||||
package horse_test
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
|
||||
87
horse/spark.go
Normal file
87
horse/spark.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package horse
|
||||
|
||||
type (
|
||||
SparkID int32
|
||||
SparkGroupID int32
|
||||
)
|
||||
|
||||
type Spark struct {
|
||||
ID SparkID `json:"spark_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Group SparkGroupID `json:"spark_group"`
|
||||
Rarity SparkRarity `json:"rarity"`
|
||||
Type SparkType `json:"type"`
|
||||
Effects [][]SparkEffect `json:"effects"`
|
||||
}
|
||||
|
||||
type SparkType int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type SparkType -trimprefix Spark
|
||||
const (
|
||||
SparkStat SparkType = iota + 1
|
||||
SparkAptitude
|
||||
SparkUnique
|
||||
SparkSkill
|
||||
SparkRace
|
||||
SparkScenario
|
||||
SparkCarnival
|
||||
SparkDistance
|
||||
SparkHidden
|
||||
SparkSurface
|
||||
SparkStyle
|
||||
)
|
||||
|
||||
type SparkRarity int8
|
||||
|
||||
const (
|
||||
OneStar SparkRarity = iota + 1 // ★
|
||||
TwoStar // ★★
|
||||
ThreeStar // ★★★
|
||||
)
|
||||
|
||||
func (r SparkRarity) String() string {
|
||||
const s = "★★★"
|
||||
return s[:int(r)*len("★")]
|
||||
}
|
||||
|
||||
type SparkEffect struct {
|
||||
Target SparkTarget `json:"target"`
|
||||
Value1 int32 `json:"value1,omitzero"`
|
||||
Value2 int32 `json:"value2,omitzero"`
|
||||
}
|
||||
|
||||
type SparkTarget int8
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type SparkTarget -trimprefix Spark
|
||||
const (
|
||||
SparkSpeed SparkTarget = iota + 1
|
||||
SparkStam
|
||||
SparkPower
|
||||
SparkGuts
|
||||
SparkWit
|
||||
SparkSkillPoints
|
||||
SparkRandomStat
|
||||
|
||||
SparkTurf SparkTarget = 11
|
||||
SparkDirt SparkTarget = 12
|
||||
|
||||
SparkFrontRunner SparkTarget = iota + 12
|
||||
SparkPaceChaser
|
||||
SparkLateSurger
|
||||
SparkEndCloser
|
||||
|
||||
SparkSprint SparkTarget = iota + 18
|
||||
SparkMile
|
||||
SparkMedium
|
||||
SparkLong
|
||||
|
||||
SparkSkillHint SparkTarget = 41
|
||||
SparkCarnivalBonus SparkTarget = 51
|
||||
|
||||
SparkSpeedCap SparkTarget = iota + 42
|
||||
SparkStamCap
|
||||
SparkPowerCap
|
||||
SparkGutsCap
|
||||
SparkWitCap
|
||||
)
|
||||
249
horse/spark.kk
249
horse/spark.kk
@@ -1,21 +1,81 @@
|
||||
module horse/spark
|
||||
|
||||
// A single spark.
|
||||
// Parameterized by the spark type: stat, aptitude, unique, race, or skill.
|
||||
pub struct spark<a>
|
||||
kind: a
|
||||
level: level
|
||||
import std/num/decimal
|
||||
import horse/game-id
|
||||
import horse/movement
|
||||
|
||||
pub fun spark/show(spark: spark<a>, level-fancy: string = "*", ?kind: (a) -> string): string
|
||||
kind(spark.kind) ++ " " ++ spark.level.show ++ level-fancy
|
||||
// A spark on a veteran.
|
||||
pub struct spark-detail
|
||||
spark-id: spark-id
|
||||
typ: spark-type
|
||||
rarity: rarity
|
||||
|
||||
pub type level
|
||||
pub fun detail(id: spark-id, ?spark/spark-type: (spark-id) -> spark-type, ?spark/rarity: (spark-id) -> rarity): spark-detail
|
||||
Spark-detail(id, id.spark-type, id.rarity)
|
||||
|
||||
pub fun spark-detail/show(s: spark-detail, ?spark/show: (spark-id) -> string): string
|
||||
s.spark-id.show ++ " " ++ "\u2605".repeat(s.rarity.int)
|
||||
|
||||
// The category of a spark; roughly, blue, pink, green, or white, with some
|
||||
// further subdivisions.
|
||||
pub type spark-type
|
||||
Stat // blue
|
||||
Aptitude // red/pink
|
||||
Unique // green
|
||||
Race
|
||||
Skill
|
||||
// skip Carnival Bonus
|
||||
Scenario
|
||||
Surface
|
||||
Distance
|
||||
Style
|
||||
Hidden
|
||||
|
||||
// Spark targets and effects.
|
||||
pub type spark-effect
|
||||
Stat-Up(s: stat, amount: int)
|
||||
SP-Up(amount: int)
|
||||
// skip Carnival Bonus
|
||||
Random-Stat-Up(amount: int)
|
||||
Aptitude-Up(a: aptitude, amount: int)
|
||||
Skill-Hint(s: skill-id, levels: int)
|
||||
Stat-Cap-Up(s: stat, amount: int)
|
||||
|
||||
// Get the base probability for a spark to trigger during a single inheritance.
|
||||
pub fun decimal/base-proc(id: spark-id, ?spark-type: (spark-id) -> spark-type, ?rarity: (spark-id) -> rarity): decimal
|
||||
val t = id.spark-type
|
||||
val r = id.rarity
|
||||
match (t, r)
|
||||
(Stat, One) -> 70.decimal(-2)
|
||||
(Stat, Two) -> 80.decimal(-2)
|
||||
(Stat, Three) -> 90.decimal(-2)
|
||||
(Aptitude, One) -> 1.decimal(-2)
|
||||
(Aptitude, Two) -> 3.decimal(-2)
|
||||
(Aptitude, Three) -> 5.decimal(-2)
|
||||
(Unique, One) -> 5.decimal(-2)
|
||||
(Unique, Two) -> 10.decimal(-2)
|
||||
(Unique, Three) -> 15.decimal(-2)
|
||||
(Race, One) -> 1.decimal(-2)
|
||||
(Race, Two) -> 2.decimal(-2)
|
||||
(Race, Three) -> 3.decimal(-2)
|
||||
(_, One) -> 3.decimal(-2)
|
||||
(_, Two) -> 6.decimal(-2)
|
||||
(_, Three) -> 9.decimal(-2)
|
||||
|
||||
// The level or star count of a spark.
|
||||
pub type rarity
|
||||
One
|
||||
Two
|
||||
Three
|
||||
|
||||
pub fun level/show(this: level): string
|
||||
match this
|
||||
pub fun rarity/int(l: rarity): int
|
||||
match l
|
||||
One -> 1
|
||||
Two -> 2
|
||||
Three -> 3
|
||||
|
||||
pub fun rarity/show(l: rarity): string
|
||||
match l
|
||||
One -> "1"
|
||||
Two -> "2"
|
||||
Three -> "3"
|
||||
@@ -51,6 +111,55 @@ pub type aptitude
|
||||
Late-Surger
|
||||
End-Closer
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `aptitude` type.
|
||||
pub fun aptitude/order2(this : aptitude, other : aptitude) : e order2<aptitude>
|
||||
match (this, other)
|
||||
(Turf, Turf) -> Eq2(Turf)
|
||||
(Turf, other') -> Lt2(Turf, other')
|
||||
(this', Turf) -> Gt2(Turf, this')
|
||||
(Dirt, Dirt) -> Eq2(Dirt)
|
||||
(Dirt, other') -> Lt2(Dirt, other')
|
||||
(this', Dirt) -> Gt2(Dirt, this')
|
||||
(Sprint, Sprint) -> Eq2(Sprint)
|
||||
(Sprint, other') -> Lt2(Sprint, other')
|
||||
(this', Sprint) -> Gt2(Sprint, this')
|
||||
(Mile, Mile) -> Eq2(Mile)
|
||||
(Mile, other') -> Lt2(Mile, other')
|
||||
(this', Mile) -> Gt2(Mile, this')
|
||||
(Medium, Medium) -> Eq2(Medium)
|
||||
(Medium, other') -> Lt2(Medium, other')
|
||||
(this', Medium) -> Gt2(Medium, this')
|
||||
(Long, Long) -> Eq2(Long)
|
||||
(Long, other') -> Lt2(Long, other')
|
||||
(this', Long) -> Gt2(Long, this')
|
||||
(Front-Runner, Front-Runner) -> Eq2(Front-Runner)
|
||||
(Front-Runner, other') -> Lt2(Front-Runner, other')
|
||||
(this', Front-Runner) -> Gt2(Front-Runner, this')
|
||||
(Pace-Chaser, Pace-Chaser) -> Eq2(Pace-Chaser)
|
||||
(Pace-Chaser, other') -> Lt2(Pace-Chaser, other')
|
||||
(this', Pace-Chaser) -> Gt2(Pace-Chaser, this')
|
||||
(Late-Surger, Late-Surger) -> Eq2(Late-Surger)
|
||||
(Late-Surger, other') -> Lt2(Late-Surger, other')
|
||||
(this', Late-Surger) -> Gt2(Late-Surger, this')
|
||||
(End-Closer, End-Closer) -> Eq2(End-Closer)
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `aptitude` type.
|
||||
pub fun aptitude/(==)(this : aptitude, other : aptitude) : e bool
|
||||
match (this, other)
|
||||
(Turf, Turf) -> True
|
||||
(Dirt, Dirt) -> True
|
||||
(Sprint, Sprint) -> True
|
||||
(Mile, Mile) -> True
|
||||
(Medium, Medium) -> True
|
||||
(Long, Long) -> True
|
||||
(Front-Runner, Front-Runner) -> True
|
||||
(Pace-Chaser, Pace-Chaser) -> True
|
||||
(Late-Surger, Late-Surger) -> True
|
||||
(End-Closer, End-Closer) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Shows a string representation of the `aptitude` type.
|
||||
pub fun aptitude/show(this : aptitude): string
|
||||
match this
|
||||
@@ -64,123 +173,3 @@ pub fun aptitude/show(this : aptitude): string
|
||||
Pace-Chaser -> "Pace Chaser"
|
||||
Late-Surger -> "Late Surger"
|
||||
End-Closer -> "End Closer"
|
||||
|
||||
// Unique (green) spark.
|
||||
// TODO: decide this representation; strings? umas? probably depends on skills generally
|
||||
pub type unique
|
||||
|
||||
pub fun unique/show(this: unique): string
|
||||
"TODO(zeph): unique skills"
|
||||
|
||||
// Race, skill, and scenario (white) sparks.
|
||||
pub type generic
|
||||
February-Stakes
|
||||
Takamatsunomiya-Kinen
|
||||
Osaka-Hai
|
||||
Oka-Sho
|
||||
Satsuki-Sho
|
||||
Tenno-Sho-Spring
|
||||
NHK-Mile-Cup
|
||||
Victoria-Mile
|
||||
Japanese-Oaks
|
||||
Japanese-Derby
|
||||
Yasuda-Kinen
|
||||
Takarazuka-Kinen
|
||||
Sprinters-Stakes
|
||||
Shuka-Sho
|
||||
Kikuka-Sho
|
||||
Tenno-Sho-Autumn
|
||||
Queen-Elizabeth-II-Cup
|
||||
Mile-Championship
|
||||
Japan-Cup
|
||||
Champions-Cup
|
||||
Hanshin-Juvenile-Fillies
|
||||
Asahi-Hai-Futurity-Stakes
|
||||
Arima-Kinen
|
||||
Hopeful-Stakes
|
||||
Tokyo-Daishoten
|
||||
JBC-Classic
|
||||
JBC-Sprint
|
||||
JBC-Ladies-Classic
|
||||
Japan-Dirt-Derby
|
||||
Teio-Sho
|
||||
Skill(skill: string)
|
||||
URA-Finale
|
||||
Unity-Cup
|
||||
|
||||
// Automatically generated.
|
||||
// Equality comparison of the `generic` type.
|
||||
pub fun generic/(==)(this : generic, other : generic) : e bool
|
||||
match (this, other)
|
||||
(February-Stakes, February-Stakes) -> True
|
||||
(Takamatsunomiya-Kinen, Takamatsunomiya-Kinen) -> True
|
||||
(Osaka-Hai, Osaka-Hai) -> True
|
||||
(Oka-Sho, Oka-Sho) -> True
|
||||
(Satsuki-Sho, Satsuki-Sho) -> True
|
||||
(Tenno-Sho-Spring, Tenno-Sho-Spring) -> True
|
||||
(NHK-Mile-Cup, NHK-Mile-Cup) -> True
|
||||
(Victoria-Mile, Victoria-Mile) -> True
|
||||
(Japanese-Oaks, Japanese-Oaks) -> True
|
||||
(Japanese-Derby, Japanese-Derby) -> True
|
||||
(Yasuda-Kinen, Yasuda-Kinen) -> True
|
||||
(Takarazuka-Kinen, Takarazuka-Kinen) -> True
|
||||
(Sprinters-Stakes, Sprinters-Stakes) -> True
|
||||
(Shuka-Sho, Shuka-Sho) -> True
|
||||
(Kikuka-Sho, Kikuka-Sho) -> True
|
||||
(Tenno-Sho-Autumn, Tenno-Sho-Autumn) -> True
|
||||
(Queen-Elizabeth-II-Cup, Queen-Elizabeth-II-Cup) -> True
|
||||
(Mile-Championship, Mile-Championship) -> True
|
||||
(Japan-Cup, Japan-Cup) -> True
|
||||
(Champions-Cup, Champions-Cup) -> True
|
||||
(Hanshin-Juvenile-Fillies, Hanshin-Juvenile-Fillies) -> True
|
||||
(Asahi-Hai-Futurity-Stakes, Asahi-Hai-Futurity-Stakes) -> True
|
||||
(Arima-Kinen, Arima-Kinen) -> True
|
||||
(Hopeful-Stakes, Hopeful-Stakes) -> True
|
||||
(Tokyo-Daishoten, Tokyo-Daishoten) -> True
|
||||
(JBC-Classic, JBC-Classic) -> True
|
||||
(JBC-Sprint, JBC-Sprint) -> True
|
||||
(JBC-Ladies-Classic, JBC-Ladies-Classic) -> True
|
||||
(Japan-Dirt-Derby, Japan-Dirt-Derby) -> True
|
||||
(Teio-Sho, Teio-Sho) -> True
|
||||
(Skill(skill), Skill(skill')) -> skill == skill'
|
||||
(URA-Finale, URA-Finale) -> True
|
||||
(Unity-Cup, Unity-Cup) -> True
|
||||
(_, _) -> False
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `generic` type.
|
||||
pub fun generic/show(this : generic) : e string
|
||||
match this
|
||||
February-Stakes -> "February Stakes"
|
||||
Takamatsunomiya-Kinen -> "Takamatsunomiya Kinen"
|
||||
Osaka-Hai -> "Osaka Hai"
|
||||
Oka-Sho -> "Oka Sho"
|
||||
Satsuki-Sho -> "Satsuki Sho"
|
||||
Tenno-Sho-Spring -> "Tenno Sho Spring"
|
||||
NHK-Mile-Cup -> "NHK Mile Cup"
|
||||
Victoria-Mile -> "Victoria Mile"
|
||||
Japanese-Oaks -> "Japanese Oaks"
|
||||
Japanese-Derby -> "Japanese Derby"
|
||||
Yasuda-Kinen -> "Yasuda Kinen"
|
||||
Takarazuka-Kinen -> "Takarazuka Kinen"
|
||||
Sprinters-Stakes -> "Sprinters Stakes"
|
||||
Shuka-Sho -> "Shuka Sho"
|
||||
Kikuka-Sho -> "Kikuka Sho"
|
||||
Tenno-Sho-Autumn -> "Tenno Sho Autumn"
|
||||
Queen-Elizabeth-II-Cup -> "Queen Elizabeth II Cup"
|
||||
Mile-Championship -> "Mile Championship"
|
||||
Japan-Cup -> "Japan Cup"
|
||||
Champions-Cup -> "Champions Cup"
|
||||
Hanshin-Juvenile-Fillies -> "Hanshin Juvenile Fillies"
|
||||
Asahi-Hai-Futurity-Stakes -> "Asahi Hai Futurity Stakes"
|
||||
Arima-Kinen -> "Arima Kinen"
|
||||
Hopeful-Stakes -> "Hopeful Stakes"
|
||||
Tokyo-Daishoten -> "Tokyo Daishoten"
|
||||
JBC-Classic -> "JBC Classic"
|
||||
JBC-Sprint -> "JBC Sprint"
|
||||
JBC-Ladies-Classic -> "JBC Ladies Classic"
|
||||
Japan-Dirt-Derby -> "Japan Dirt Derby"
|
||||
Teio-Sho -> "Teio Sho"
|
||||
Skill(skill) -> skill.show
|
||||
URA-Finale -> "URA Finale"
|
||||
Unity-Cup -> "Unity Cup"
|
||||
|
||||
79
horse/sparktarget_string.go
Normal file
79
horse/sparktarget_string.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Code generated by "stringer -type SparkTarget -trimprefix Spark"; 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[SparkSpeed-1]
|
||||
_ = x[SparkStam-2]
|
||||
_ = x[SparkPower-3]
|
||||
_ = x[SparkGuts-4]
|
||||
_ = x[SparkWit-5]
|
||||
_ = x[SparkSkillPoints-6]
|
||||
_ = x[SparkRandomStat-7]
|
||||
_ = x[SparkTurf-11]
|
||||
_ = x[SparkDirt-12]
|
||||
_ = x[SparkFrontRunner-21]
|
||||
_ = x[SparkPaceChaser-22]
|
||||
_ = x[SparkLateSurger-23]
|
||||
_ = x[SparkEndCloser-24]
|
||||
_ = x[SparkSprint-31]
|
||||
_ = x[SparkMile-32]
|
||||
_ = x[SparkMedium-33]
|
||||
_ = x[SparkLong-34]
|
||||
_ = x[SparkSkillHint-41]
|
||||
_ = x[SparkCarnivalBonus-51]
|
||||
_ = x[SparkSpeedCap-61]
|
||||
_ = x[SparkStamCap-62]
|
||||
_ = x[SparkPowerCap-63]
|
||||
_ = x[SparkGutsCap-64]
|
||||
_ = x[SparkWitCap-65]
|
||||
}
|
||||
|
||||
const (
|
||||
_SparkTarget_name_0 = "SpeedStamPowerGutsWitSkillPointsRandomStat"
|
||||
_SparkTarget_name_1 = "TurfDirt"
|
||||
_SparkTarget_name_2 = "FrontRunnerPaceChaserLateSurgerEndCloser"
|
||||
_SparkTarget_name_3 = "SprintMileMediumLong"
|
||||
_SparkTarget_name_4 = "SkillHint"
|
||||
_SparkTarget_name_5 = "CarnivalBonus"
|
||||
_SparkTarget_name_6 = "SpeedCapStamCapPowerCapGutsCapWitCap"
|
||||
)
|
||||
|
||||
var (
|
||||
_SparkTarget_index_0 = [...]uint8{0, 5, 9, 14, 18, 21, 32, 42}
|
||||
_SparkTarget_index_1 = [...]uint8{0, 4, 8}
|
||||
_SparkTarget_index_2 = [...]uint8{0, 11, 21, 31, 40}
|
||||
_SparkTarget_index_3 = [...]uint8{0, 6, 10, 16, 20}
|
||||
_SparkTarget_index_6 = [...]uint8{0, 8, 15, 23, 30, 36}
|
||||
)
|
||||
|
||||
func (i SparkTarget) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 7:
|
||||
i -= 1
|
||||
return _SparkTarget_name_0[_SparkTarget_index_0[i]:_SparkTarget_index_0[i+1]]
|
||||
case 11 <= i && i <= 12:
|
||||
i -= 11
|
||||
return _SparkTarget_name_1[_SparkTarget_index_1[i]:_SparkTarget_index_1[i+1]]
|
||||
case 21 <= i && i <= 24:
|
||||
i -= 21
|
||||
return _SparkTarget_name_2[_SparkTarget_index_2[i]:_SparkTarget_index_2[i+1]]
|
||||
case 31 <= i && i <= 34:
|
||||
i -= 31
|
||||
return _SparkTarget_name_3[_SparkTarget_index_3[i]:_SparkTarget_index_3[i+1]]
|
||||
case i == 41:
|
||||
return _SparkTarget_name_4
|
||||
case i == 51:
|
||||
return _SparkTarget_name_5
|
||||
case 61 <= i && i <= 65:
|
||||
i -= 61
|
||||
return _SparkTarget_name_6[_SparkTarget_index_6[i]:_SparkTarget_index_6[i+1]]
|
||||
default:
|
||||
return "SparkTarget(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
34
horse/sparktype_string.go
Normal file
34
horse/sparktype_string.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Code generated by "stringer -type SparkType -trimprefix Spark"; 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[SparkStat-1]
|
||||
_ = x[SparkAptitude-2]
|
||||
_ = x[SparkUnique-3]
|
||||
_ = x[SparkSkill-4]
|
||||
_ = x[SparkRace-5]
|
||||
_ = x[SparkScenario-6]
|
||||
_ = x[SparkCarnival-7]
|
||||
_ = x[SparkDistance-8]
|
||||
_ = x[SparkHidden-9]
|
||||
_ = x[SparkSurface-10]
|
||||
_ = x[SparkStyle-11]
|
||||
}
|
||||
|
||||
const _SparkType_name = "StatAptitudeUniqueSkillRaceScenarioCarnivalDistanceHiddenSurfaceStyle"
|
||||
|
||||
var _SparkType_index = [...]uint8{0, 4, 12, 18, 23, 27, 35, 43, 51, 57, 64, 69}
|
||||
|
||||
func (i SparkType) String() string {
|
||||
idx := int(i) - 1
|
||||
if i < 1 || idx >= len(_SparkType_index)-1 {
|
||||
return "SparkType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _SparkType_name[_SparkType_index[idx]:_SparkType_index[idx+1]]
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
module horse/trainee
|
||||
|
||||
import std/data/rb-map
|
||||
|
||||
// Aptitudes of an umamusume being trained.
|
||||
pub struct uma
|
||||
turf: aptitudes
|
||||
dirt: aptitudes
|
||||
sprint: aptitudes
|
||||
mile: aptitudes
|
||||
medium: aptitudes
|
||||
long: aptitudes
|
||||
front-runner: aptitudes
|
||||
pace-chaser: aptitudes
|
||||
late-surger: aptitudes
|
||||
end-closer: aptitudes
|
||||
|
||||
// Aptitude level distribution.
|
||||
pub alias aptitudes = rbmap<level, float64>
|
||||
|
||||
// Starting aptitude levels.
|
||||
pub type level
|
||||
G
|
||||
F
|
||||
E
|
||||
D
|
||||
C
|
||||
B
|
||||
A
|
||||
S
|
||||
|
||||
// Automatically generated.
|
||||
// Fip comparison of the `level` type.
|
||||
pub fun level/order2(this : level, other : level) : order2<level>
|
||||
match (this, other)
|
||||
(G, G) -> Eq2(G)
|
||||
(G, other') -> Lt2(G, other')
|
||||
(this', G) -> Gt2(G, this')
|
||||
(F, F) -> Eq2(F)
|
||||
(F, other') -> Lt2(F, other')
|
||||
(this', F) -> Gt2(F, this')
|
||||
(E, E) -> Eq2(E)
|
||||
(E, other') -> Lt2(E, other')
|
||||
(this', E) -> Gt2(E, this')
|
||||
(D, D) -> Eq2(D)
|
||||
(D, other') -> Lt2(D, other')
|
||||
(this', D) -> Gt2(D, this')
|
||||
(C, C) -> Eq2(C)
|
||||
(C, other') -> Lt2(C, other')
|
||||
(this', C) -> Gt2(C, this')
|
||||
(B, B) -> Eq2(B)
|
||||
(B, other') -> Lt2(B, other')
|
||||
(this', B) -> Gt2(B, this')
|
||||
(A, A) -> Eq2(A)
|
||||
(A, other') -> Lt2(A, other')
|
||||
(this', A) -> Gt2(A, this')
|
||||
(S, S) -> Eq2(S)
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `level` type.
|
||||
pub fun level/show(this : level) : string
|
||||
match this
|
||||
G -> "G"
|
||||
F -> "F"
|
||||
E -> "E"
|
||||
D -> "D"
|
||||
C -> "C"
|
||||
B -> "B"
|
||||
A -> "A"
|
||||
S -> "S"
|
||||
44
horse/uma.go
Normal file
44
horse/uma.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package horse
|
||||
|
||||
type UmaID int32
|
||||
|
||||
type Uma struct {
|
||||
ID UmaID `json:"chara_card_id"`
|
||||
CharacterID CharacterID `json:"chara_id"`
|
||||
Name string `json:"name"`
|
||||
Variant string `json:"variant"`
|
||||
|
||||
Sprint AptitudeLevel `json:"sprint"`
|
||||
Mile AptitudeLevel `json:"mile"`
|
||||
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 `json:"unique"`
|
||||
Skill1 SkillID `json:"skill1"`
|
||||
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
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type AptitudeLevel -trimprefix AptitudeLv
|
||||
const (
|
||||
AptitudeLvG AptitudeLevel = iota + 1
|
||||
AptitudeLvF
|
||||
AptitudeLvE
|
||||
AptitudeLvD
|
||||
AptitudeLvC
|
||||
AptitudeLvB
|
||||
AptitudeLvA
|
||||
AptitudeLvS
|
||||
)
|
||||
27
horse/uma.kk
Normal file
27
horse/uma.kk
Normal file
@@ -0,0 +1,27 @@
|
||||
module horse/uma
|
||||
|
||||
import horse/game-id
|
||||
import horse/movement
|
||||
|
||||
// Details of an uma, or character card.
|
||||
pub struct uma-detail
|
||||
uma-id: uma-id
|
||||
character-id: character-id
|
||||
sprint: aptitude-level
|
||||
mile: aptitude-level
|
||||
medium: aptitude-level
|
||||
long: aptitude-level
|
||||
front-runner: aptitude-level
|
||||
pace-chaser: aptitude-level
|
||||
late-surger: aptitude-level
|
||||
end-closer: aptitude-level
|
||||
turf: aptitude-level
|
||||
dirt: aptitude-level
|
||||
unique: skill-id
|
||||
skill1: skill-id
|
||||
skill2: skill-id
|
||||
skill3: skill-id
|
||||
skill-pl2: skill-id
|
||||
skill-pl3: skill-id
|
||||
skill-pl4: skill-id
|
||||
skill-pl5: skill-id
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,66 +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 }} = {{ $c.ID }} // {{ $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*{{ $.Count }} + 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*{{ $.Count }}*{{ $.Count }} + b*{{ $.Count }} + c])
|
||||
}
|
||||
{{ end }}
|
||||
@@ -1,120 +0,0 @@
|
||||
{{ define "koka-character" -}}
|
||||
module horse/{{ $.Region }}/character
|
||||
|
||||
// Automatically generated with horsegen; DO NOT EDIT
|
||||
|
||||
import std/core/vector
|
||||
import std/core-extras
|
||||
|
||||
// Character identity.
|
||||
pub type character
|
||||
{{- range $uma := $.Characters }}
|
||||
{{ kkenum $uma.Name }}
|
||||
{{- end }}
|
||||
|
||||
// The list of all characters in order by ID, for easy iterating.
|
||||
pub val character/all = [
|
||||
{{- range $uma := $.Characters }}
|
||||
{{ kkenum $uma.Name }},
|
||||
{{- end }}
|
||||
]
|
||||
|
||||
// Get the character for a character ID.
|
||||
// Generally, these are four digit numbers in the range 1000-1999.
|
||||
pub fun character/from-id(id: int): maybe<character>
|
||||
match id
|
||||
{{- range $uma := $.Characters }}
|
||||
{{ $uma.ID }} -> Just( {{- kkenum $uma.Name -}} )
|
||||
{{- end }}
|
||||
_ -> Nothing
|
||||
|
||||
// Get the ID for a character.
|
||||
pub fun character/character-id(c: character): int
|
||||
match c
|
||||
{{- range $uma := $.Characters }}
|
||||
{{ kkenum $uma.Name }} -> {{ $uma.ID }}
|
||||
{{- end }}
|
||||
|
||||
// Get the name of a character.
|
||||
pub fun character/show(c: character): string
|
||||
match c
|
||||
{{- range $uma := $.Characters }}
|
||||
{{ kkenum $uma.Name }} -> {{ printf "%q" $uma.Name }}
|
||||
{{- end }}
|
||||
|
||||
// Compare two characters.
|
||||
pub fip fun character/order2(a: character, b: character): order2<character>
|
||||
match (a, b)
|
||||
{{- range $uma := $.Characters }}{{ $e := kkenum $uma.Name }}
|
||||
( {{- $e }}, {{ $e -}} ) -> Eq2( {{- $e -}} )
|
||||
{{- if ne $uma.ID $.MaxID }}
|
||||
( {{- $e }}, b') -> Lt2( {{- $e }}, b')
|
||||
(a', {{ $e -}} ) -> Gt2( {{- $e }}, a')
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
// Character equality.
|
||||
pub fun character/(==)(a: character, b: character): bool
|
||||
match (a, b)
|
||||
{{- range $uma := $.Characters }}{{ $e := kkenum $uma.Name }}
|
||||
( {{- $e }}, {{ $e -}} ) -> True
|
||||
{{- end }}
|
||||
_ -> False
|
||||
|
||||
fip fun character/index(^c: character): int
|
||||
match c
|
||||
{{- range $uma := $.Characters }}
|
||||
{{ kkenum $uma.Name }} -> {{ $uma.Index }}
|
||||
{{- end }}
|
||||
|
||||
// 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 "kk_intx_t arr[] = {
|
||||
{{- range $a := $.Characters }}
|
||||
{{- range $b := $.Characters }}
|
||||
{{- index $.PairMaps $a.ID $b.ID }},
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
};\nkk_vector_from_cintarray(arr, (kk_ssize_t){{ $.Count }} * (kk_ssize_t){{ $.Count }}, 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, b: character): int
|
||||
global/pair-table.at(a.index * {{ $.Count }} + 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 "kk_intx_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_cintarray(arr, (kk_ssize_t){{ $.Count }} * (kk_ssize_t){{ $.Count }} * (kk_ssize_t){{ $.Count }}, 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, b: character, c: character): int
|
||||
global/trio-table.at(a.index * {{ $.Count }} * {{ $.Count }} + b.index * {{ $.Count }} + c.index).default(0)
|
||||
|
||||
{{- end }}
|
||||
186
horsegen/gen.go
186
horsegen/gen.go
@@ -1,186 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
//go:embed character.kk.template skill.kk.template character.go.template skill.go.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
|
||||
Count int
|
||||
MaxID int
|
||||
}{region, c, pairs, trios, pm, tm, len(c), maxid}
|
||||
var err error
|
||||
if kk != nil {
|
||||
err = errors.Join(t.ExecuteTemplate(kk, "koka-character", &data))
|
||||
}
|
||||
if g != nil {
|
||||
err = errors.Join(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
|
||||
}{region, groups, skills, m}
|
||||
var err error
|
||||
if kk != nil {
|
||||
err = errors.Join(t.ExecuteTemplate(kk, "koka-skill", &data))
|
||||
}
|
||||
if g != nil {
|
||||
err = errors.Join(t.ExecuteTemplate(g, "go-skill-data", &data))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ExecSkillGroupKK(t *template.Template, region string, w io.Writer, g []NamedID[SkillGroup], s []Skill) error {
|
||||
data := struct {
|
||||
Region string
|
||||
Groups []NamedID[SkillGroup]
|
||||
Skills []Skill
|
||||
}{region, g, s}
|
||||
return t.ExecuteTemplate(w, "koka-skill-group", &data)
|
||||
}
|
||||
|
||||
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",
|
||||
"♡ 3D Nail Art", "Nail-Art",
|
||||
".", "",
|
||||
"&", "-and-",
|
||||
"'s", "s",
|
||||
"ó", "o",
|
||||
"∞", "Infinity",
|
||||
"×", "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",
|
||||
".", "",
|
||||
"&", "And",
|
||||
"'s", "s",
|
||||
"∞", "Infinity",
|
||||
"×", "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])) {
|
||||
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
|
||||
}
|
||||
313
horsegen/load.go
313
horsegen/load.go
@@ -1,313 +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 skill-group.sql
|
||||
var skillGroupSQL string
|
||||
|
||||
//go:embed skill.sql
|
||||
var skillSQL 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
|
||||
IconID int
|
||||
Index int
|
||||
}
|
||||
|
||||
type SkillActivation struct {
|
||||
Precondition string
|
||||
Condition string
|
||||
Duration 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),
|
||||
Cooldown: stmt.ColumnInt(12),
|
||||
Abilities: [3]SkillAbility{
|
||||
{
|
||||
Type: stmt.ColumnInt(13),
|
||||
ValueUsage: stmt.ColumnInt(14),
|
||||
Value: stmt.ColumnInt(15),
|
||||
Target: stmt.ColumnInt(16),
|
||||
TargetValue: stmt.ColumnInt(17),
|
||||
},
|
||||
{
|
||||
Type: stmt.ColumnInt(18),
|
||||
ValueUsage: stmt.ColumnInt(19),
|
||||
Value: stmt.ColumnInt(20),
|
||||
Target: stmt.ColumnInt(21),
|
||||
TargetValue: stmt.ColumnInt(22),
|
||||
},
|
||||
{
|
||||
Type: stmt.ColumnInt(23),
|
||||
ValueUsage: stmt.ColumnInt(24),
|
||||
Value: stmt.ColumnInt(25),
|
||||
Target: stmt.ColumnInt(26),
|
||||
TargetValue: stmt.ColumnInt(27),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Precondition: stmt.ColumnText(28),
|
||||
Condition: stmt.ColumnText(29),
|
||||
Duration: stmt.ColumnInt(30),
|
||||
Cooldown: stmt.ColumnInt(31),
|
||||
Abilities: [3]SkillAbility{
|
||||
{
|
||||
Type: stmt.ColumnInt(32),
|
||||
ValueUsage: stmt.ColumnInt(33),
|
||||
Value: stmt.ColumnInt(34),
|
||||
Target: stmt.ColumnInt(35),
|
||||
TargetValue: stmt.ColumnInt(36),
|
||||
},
|
||||
{
|
||||
Type: stmt.ColumnInt(37),
|
||||
ValueUsage: stmt.ColumnInt(38),
|
||||
Value: stmt.ColumnInt(39),
|
||||
Target: stmt.ColumnInt(40),
|
||||
TargetValue: stmt.ColumnInt(41),
|
||||
},
|
||||
{
|
||||
Type: stmt.ColumnInt(42),
|
||||
ValueUsage: stmt.ColumnInt(43),
|
||||
Value: stmt.ColumnInt(44),
|
||||
Target: stmt.ColumnInt(45),
|
||||
TargetValue: stmt.ColumnInt(46),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SPCost: stmt.ColumnInt(47),
|
||||
InheritID: stmt.ColumnInt(48),
|
||||
IconID: stmt.ColumnInt(49),
|
||||
Index: stmt.ColumnInt(50),
|
||||
}
|
||||
r = append(r, s)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
129
horsegen/main.go
129
horsegen/main.go
@@ -1,129 +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(®ion, "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
|
||||
)
|
||||
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
|
||||
})
|
||||
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"))
|
||||
slog.Info("write skills")
|
||||
return ExecSkill(t, region, sf, gf, sg, skills)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
sf, err := os.Create(filepath.Join(out, region, "skill-group.kk"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("write skill groups")
|
||||
return ExecSkillGroupKK(t, region, sf, sg, skills)
|
||||
})
|
||||
if err := eg.Wait(); err != nil {
|
||||
slog.Error("generate", slog.Any("err", err))
|
||||
} else {
|
||||
slog.Info("done")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,69 +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 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 }},
|
||||
{{- 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.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 }}
|
||||
@@ -1,347 +0,0 @@
|
||||
{{- define "koka-skill-group" -}}
|
||||
module horse/{{ $.Region }}/skill-group
|
||||
|
||||
// Automatically generated with horsegen; DO NOT EDIT
|
||||
|
||||
// Skill groups.
|
||||
// A skill group may contain white, circle, double-circle, gold, and purple skills
|
||||
// for the same effect.
|
||||
// Sparks that grant skills refer to a skill group.
|
||||
pub type skill-group
|
||||
{{- range $g := $.Groups }}
|
||||
{{ kkenum $g.Name }}
|
||||
{{- end }}
|
||||
|
||||
// Map a skill group to its ID.
|
||||
pub fip fun skill-group/group-id(^sg: skill-group): int
|
||||
match sg
|
||||
{{- range $g := $.Groups }}
|
||||
{{ kkenum $g.Name }} -> {{ $g.ID }}
|
||||
{{- end }}
|
||||
|
||||
// Get the skill group for an ID.
|
||||
pub fip(1) fun skill-group/from-id(^id: int): maybe<skill-group>
|
||||
match id
|
||||
{{- range $g := $.Groups }}
|
||||
{{ $g.ID }} -> Just( {{- kkenum $g.Name -}} )
|
||||
{{- end }}
|
||||
_ -> Nothing
|
||||
|
||||
// Get the name for a skill group.
|
||||
// Skill group names are the name of the base skill in the group.
|
||||
pub fun skill-group/show(sg: skill-group): string
|
||||
match sg
|
||||
{{- range $g := $.Groups }}
|
||||
{{ kkenum $g.Name }} -> {{ printf "%q" $g.Name }}
|
||||
{{- end }}
|
||||
|
||||
// Compare two skill groups by ID order.
|
||||
pub fip fun skill-group/order2(a: skill-group, b: skill-group): order2<skill-group>
|
||||
match cmp(a.group-id, b.group-id)
|
||||
Lt -> Lt2(a, b)
|
||||
Eq -> Eq2(a)
|
||||
Gt -> Gt2(a, b)
|
||||
|
||||
pub fun skill-group/(==)(a: skill-group, b: skill-group): bool
|
||||
a.group-id == b.group-id
|
||||
|
||||
{{- end -}}
|
||||
|
||||
{{- define "koka-skill" -}}
|
||||
module horse/{{ $.Region }}/skill
|
||||
|
||||
// Automatically generated with horsegen; DO NOT EDIT
|
||||
|
||||
import std/num/decimal
|
||||
pub import horse/{{ $.Region }}/skill-group
|
||||
|
||||
// Skill instances.
|
||||
pub type skill
|
||||
{{- range $s := $.Skills }}
|
||||
{{ kkenum $s.Name }}{{ if ne $s.InheritID 0 }}-Inherit{{ end -}}
|
||||
{{- end }}
|
||||
|
||||
// Map a skill to its ID.
|
||||
pub fip fun skill/skill-id(^s: skill): int
|
||||
match s
|
||||
{{- range $s := $.Skills }}
|
||||
{{ kkenum $s.Name }}{{ if ne $s.InheritID 0 }}-Inherit{{ end }} -> {{ $s.ID }}
|
||||
{{- end }}
|
||||
|
||||
// Get the skill for an ID.
|
||||
pub fip(1) fun skill/from-id(^id: int): maybe<skill>
|
||||
match id
|
||||
{{- range $s := $.Skills }}
|
||||
{{ $s.ID }} -> Just( {{- kkenum $s.Name -}}{{ if ne $s.InheritID 0 }}-Inherit{{ end -}} )
|
||||
{{- end }}
|
||||
_ -> Nothing
|
||||
|
||||
// Get the name of a skill.
|
||||
// Inherited skills have the same names as their original counterparts.
|
||||
pub fun skill/show(s: skill): string
|
||||
match s
|
||||
{{- range $s := $.Skills }}
|
||||
{{ kkenum $s.Name }}{{ if ne $s.InheritID 0 }}-Inherit{{ end }} -> {{ printf "%q" $s.Name }}
|
||||
{{- end }}
|
||||
|
||||
// Compare two skills by ID order.
|
||||
pub fip fun skill/order2(a: skill, b: skill): order2<skill>
|
||||
match cmp(a.skill-id, b.skill-id)
|
||||
Lt -> Lt2(a, b)
|
||||
Eq -> Eq2(a)
|
||||
Gt -> Gt2(a, b)
|
||||
|
||||
pub fun skill/(==)(a: skill, b: skill): bool
|
||||
a.skill-id == b.skill-id
|
||||
|
||||
// Get the skills in a skill group.
|
||||
pub fun skill-group/skills(g: skill-group): list<skill>
|
||||
match g
|
||||
{{- range $g := $.Groups }}
|
||||
{{ kkenum $g.Name }} -> [ {{- range $s := index $.Related $g.ID }}{{ kkenum $s.Name }}{{ if ne $s.InheritID 0 }}-Inherit{{ end }}, {{ end }}]
|
||||
{{- end }}
|
||||
|
||||
// Get complete skill info.
|
||||
pub fun skill/detail(^s: skill): skill-detail
|
||||
match s
|
||||
{{- range $s := $.Skills }}
|
||||
{{ kkenum $s.Name }}{{ if ne $s.InheritID 0 }}-Inherit{{ end }} -> {{ template "kk-render-skill-detail" $s }}
|
||||
{{- end }}
|
||||
|
||||
// Details about a skill.
|
||||
pub struct skill-detail
|
||||
skill-id: int
|
||||
name: string
|
||||
description: string
|
||||
group: maybe<skill-group>
|
||||
rarity: rarity
|
||||
group-rate: int
|
||||
grade-value: int
|
||||
wit-check: bool
|
||||
activations: list<activation>
|
||||
sp-cost: int
|
||||
icon-id: int
|
||||
|
||||
// Automatically generated.
|
||||
// Shows a string representation of the `skill-detail` type.
|
||||
pub fun skill-detail/show(this : skill-detail) : e string
|
||||
match this
|
||||
Skill-detail(skill-id, name, description, group, rarity, group-rate, grade-value, wit-check, activations, sp-cost, icon-id) -> "Skill-detail(skill-id: " ++ skill-id.show ++ ", name: " ++ name.show ++ ", description: " ++ description.show ++ ", group: " ++ group.show ++ ", rarity: " ++ rarity.show ++ ", group-rate: " ++ group-rate.show ++ ", grade-value: " ++ grade-value.show ++ ", wit-check: " ++ wit-check.show ++ ", activations: " ++ activations.show ++ ", sp-cost: " ++ sp-cost.show ++ ", icon-id: " ++ icon-id.show ++ ")"
|
||||
|
||||
// Skill rarity.
|
||||
pub type rarity
|
||||
Common // white
|
||||
Rare // gold
|
||||
Unique-Low // 1*/2* unique
|
||||
Unique-Upgraded // 3*+ unique on a trainee upgraded from 1*/2*
|
||||
Unique // base 3* unique
|
||||
|
||||
pub fun rarity/show(r: rarity): string
|
||||
match r
|
||||
Common -> "Common"
|
||||
Rare -> "Rare"
|
||||
Unique-Low -> "Unique (1\u2606/2\u2606)"
|
||||
Unique-Upgraded -> "Unique (3\u2606+ from 1\u2606/2\u2606 upgraded)"
|
||||
Unique -> "Unique (3\u2606+)"
|
||||
|
||||
// Condition and precondition logic.
|
||||
pub alias condition = string
|
||||
|
||||
// Activation conditions and effects.
|
||||
// A skill has one or two activations.
|
||||
pub struct activation
|
||||
precondition: condition
|
||||
condition: condition
|
||||
duration: decimal // seconds
|
||||
cooldown: decimal // seconds
|
||||
abilities: list<ability> // one to three elements
|
||||
|
||||
pub fun activation/show(a: activation): string
|
||||
match a
|
||||
Activation("", condition, duration, _, abilities) | duration <= 0.decimal -> condition ++ " -> " ++ abilities.show
|
||||
Activation("", condition, duration, cooldown, abilities) | cooldown >= 500.decimal -> condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s"
|
||||
Activation("", condition, duration, cooldown, abilities) -> condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown"
|
||||
Activation(precondition, condition, duration, _, abilities) | duration <= 0.decimal -> precondition ++ " -> " ++ condition ++ " -> " ++ abilities.show
|
||||
Activation(precondition, condition, duration, cooldown, abilities) | cooldown >= 500.decimal -> precondition ++ " -> " ++ condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s"
|
||||
Activation(precondition, condition, duration, cooldown, abilities) -> precondition ++ "-> " ++ condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown"
|
||||
|
||||
// Effects of activating a skill.
|
||||
pub struct ability
|
||||
ability-type: ability-type
|
||||
value-usage: value-usage
|
||||
target: target
|
||||
|
||||
pub fun ability/show(a: ability): string
|
||||
match a
|
||||
Ability(t, Direct, Self) -> t.show
|
||||
Ability(t, Direct, target) -> t.show ++ " " ++ target.show
|
||||
Ability(t, v, Self) -> t.show ++ " scaling by " ++ v.show
|
||||
Ability(t, v, target) -> t.show ++ " " ++ target.show ++ " scaling by " ++ v.show
|
||||
|
||||
// Target of a skill activation effect.
|
||||
pub type ability-type
|
||||
Passive-Speed(bonus: decimal)
|
||||
Passive-Stamina(bonus: decimal)
|
||||
Passive-Power(bonus: decimal)
|
||||
Passive-Guts(bonus: decimal)
|
||||
Passive-Wit(bonus: decimal)
|
||||
Great-Escape
|
||||
Vision(bonus: decimal)
|
||||
HP(rate: decimal)
|
||||
Gate-Delay(rate: decimal)
|
||||
Frenzy(add: decimal)
|
||||
Current-Speed(rate: decimal)
|
||||
Target-Speed(rate: decimal)
|
||||
Lane-Speed(rate: decimal)
|
||||
Accel(rate: decimal)
|
||||
Lane-Change(rate: decimal)
|
||||
|
||||
pub fun ability-type/show(a: ability-type): string
|
||||
match a
|
||||
Passive-Speed(bonus) -> "passive " ++ bonus.show ++ " Speed"
|
||||
Passive-Stamina(bonus) -> "passive " ++ bonus.show ++ " Stamina"
|
||||
Passive-Power(bonus) -> "passive " ++ bonus.show ++ " Power"
|
||||
Passive-Guts(bonus) -> "passive " ++ bonus.show ++ " Guts"
|
||||
Passive-Wit(bonus) -> "passive " ++ bonus.show ++ " Wit"
|
||||
Great-Escape -> "enable Great Escape style"
|
||||
Vision(bonus) -> bonus.show ++ " vision"
|
||||
HP(rate) | rate >= 0.decimal -> show(rate * 100.decimal) ++ "% HP recovery"
|
||||
HP(rate) -> show(rate * 100.decimal) ++ "% HP loss"
|
||||
Gate-Delay(rate) -> rate.show ++ "× gate delay"
|
||||
Frenzy(add) -> add.show ++ "s longer Rushed"
|
||||
Current-Speed(rate) -> show(rate * 100.decimal) ++ "% current speed"
|
||||
Target-Speed(rate) -> show(rate * 100.decimal) ++ "% target speed"
|
||||
Lane-Speed(rate) -> show(rate * 100.decimal) ++ "% lane speed"
|
||||
Accel(rate) -> show(rate * 100.decimal) ++ "% acceleration"
|
||||
Lane-Change(rate) -> rate.show ++ " course width movement"
|
||||
|
||||
// Special scaling for skill activation effects.
|
||||
pub type value-usage
|
||||
Direct
|
||||
Team-Speed
|
||||
Team-Stamina
|
||||
Team-Power
|
||||
Team-Guts
|
||||
Team-Wit
|
||||
Multiply-Random
|
||||
|
||||
pub fun value-usage/show(v: value-usage): string
|
||||
match v
|
||||
Direct -> "no scaling"
|
||||
Team-Speed -> "team's Speed"
|
||||
Team-Stamina -> "team's Stamina"
|
||||
Team-Power -> "team's Power"
|
||||
Team-Guts -> "team's Guts"
|
||||
Team-Wit -> "team's Wit"
|
||||
Multiply-Random -> "random multiplier (0×, 0.02×, or 0.04×)"
|
||||
|
||||
// Who a skill activation targets.
|
||||
pub type target
|
||||
Self
|
||||
In-View
|
||||
Ahead(limit: int)
|
||||
Behind(limit: int)
|
||||
Style(style: style)
|
||||
Rushing-Ahead(limit: int)
|
||||
Rushing-Behind(limit: int)
|
||||
Rushing-Style(style: style)
|
||||
|
||||
pub fun target/show(t: target): string
|
||||
match t
|
||||
Self -> "self"
|
||||
In-View -> "others in field of view"
|
||||
Ahead(limit) | limit >= 18 -> "others ahead"
|
||||
Ahead(limit) -> "next " ++ limit.show ++ " others ahead"
|
||||
Behind(limit) | limit >= 18 -> "others behind"
|
||||
Behind(limit) -> "next " ++ limit.show ++ " others behind"
|
||||
Style(Front-Runner) -> "other Front Runners"
|
||||
Style(Pace-Chaser) -> "other Pace Chasers"
|
||||
Style(Late-Surger) -> "other Late Surgers"
|
||||
Style(End-Closer) -> "other End Closers"
|
||||
Rushing-Ahead(limit) | limit >= 18 -> "others rushing ahead"
|
||||
Rushing-Ahead(limit) -> "next " ++ limit.show ++ " others rushing ahead"
|
||||
Rushing-Behind(limit) | limit >= 18 -> "others rushing behind"
|
||||
Rushing-Behind(limit) -> "next " ++ limit.show ++ " others rushing behind"
|
||||
Rushing-Style(Front-Runner) -> "rushing Front Runners"
|
||||
Rushing-Style(Pace-Chaser) -> "rushing Pace Chasers"
|
||||
Rushing-Style(Late-Surger) -> "rushing Late Surgers"
|
||||
Rushing-Style(End-Closer) -> "rushing End Closers"
|
||||
|
||||
// Running style for skill targets.
|
||||
{{- /* TODO(zeph): there is definitely a better place for this to live */}}
|
||||
pub type style
|
||||
Front-Runner
|
||||
Pace-Chaser
|
||||
Late-Surger
|
||||
End-Closer
|
||||
|
||||
{{- end -}}
|
||||
|
||||
{{ define "kk-render-skill-detail" }}
|
||||
{{- /* Call with Skill structure as argument. */ -}}
|
||||
Skill-detail(skill-id = {{ $.ID -}}
|
||||
, name = {{ printf "%q" $.Name -}}
|
||||
, description = {{ printf "%q" $.Description -}}
|
||||
, group = {{ if ne $.GroupName "" }}Just({{ kkenum $.GroupName }}){{ else }}Nothing{{ end -}}
|
||||
, rarity = {{ if eq $.Rarity 1 }}Common{{ else if eq $.Rarity 2 }}Rare{{ else if eq $.Rarity 3 }}Unique-Low{{ else if eq $.Rarity 4 }}Unique-Upgraded{{ else if eq $.Rarity 5 }}Unique{{ else }}??? $.Rarity={{ $.Rarity }}{{ end -}}
|
||||
, group-rate = {{ $.GroupRate -}}
|
||||
, grade-value = {{ $.GradeValue -}}
|
||||
, wit-check = {{ if $.WitCheck }}True{{ else }}False{{ end -}}
|
||||
, activations = [
|
||||
{{- range $a := $.Activations -}}
|
||||
{{- if ne $a.Condition "" -}}
|
||||
Activation(precondition = {{ printf "%q" $a.Precondition -}}
|
||||
, condition = {{ printf "%q" $a.Condition -}}
|
||||
, duration = {{ $a.Duration -}}{{ if gt $a.Duration 0 }}.decimal(-4){{ else }}.decimal{{ end -}}
|
||||
, cooldown = {{ $a.Cooldown -}}{{ if gt $a.Cooldown 0 }}.decimal(-4){{ else }}.decimal{{ end -}}
|
||||
, abilities = [
|
||||
{{- range $abil := $a.Abilities -}}
|
||||
{{- if ne $abil.Type 0 -}}
|
||||
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 -}}??? $abil.ValueUsage={{ $abil.ValueUsage }}
|
||||
{{- end -}}
|
||||
, target =
|
||||
{{- if eq $abil.Target 1 -}}Self
|
||||
{{- else if eq $abil.Target 4 -}}In-View
|
||||
{{- else if eq $abil.Target 9 -}}Ahead({{ $abil.TargetValue }})
|
||||
{{- else if eq $abil.Target 10 -}}Behind({{ $abil.TargetValue }})
|
||||
{{- 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 }})
|
||||
{{- end -}}
|
||||
),
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
]),
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
], sp-cost = {{ $.SPCost -}}
|
||||
, icon-id = {{ $.IconID -}}
|
||||
)
|
||||
{{- end -}}
|
||||
4709
package-lock.json
generated
Normal file
4709
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "zenno",
|
||||
"version": "0.0.1",
|
||||
"description": "Zenno Rob Roy: She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!",
|
||||
"main": "index.js",
|
||||
"directories": {},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.sunturtle.xyz:zephyr/horse.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Branden J Brown <zephyrtronium@hey.com>",
|
||||
"license": "none",
|
||||
"dependencies": {
|
||||
"astro": "^6.0.8",
|
||||
"nanostores": "^1.2.0"
|
||||
}
|
||||
}
|
||||
420
schema/schema.ts
Normal file
420
schema/schema.ts
Normal 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;
|
||||
}
|
||||
35
site/components/CharaSelect.astro
Normal file
35
site/components/CharaSelect.astro
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
// Input to select a character,
|
||||
// e.g. Special Week (not [Special Dreamer] Special Week).
|
||||
|
||||
import { character, type Character } from "../data/character";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
region?: keyof typeof character;
|
||||
}
|
||||
|
||||
export interface Emits {
|
||||
"chara-change": (ev: CustomEvent<{id: string, chara?: Character}>) => void;
|
||||
}
|
||||
|
||||
const { id, label, required = false, region = "global" } = Astro.props;
|
||||
---
|
||||
{label && <label for="id">{label}</label>}
|
||||
<select class="select-chara" id={id}>
|
||||
{!required && <option value=""></option>}
|
||||
{character[region].map((chara) => (
|
||||
<option value={chara.chara_id}>{chara.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<script is:inline define:vars={{ id, region, character }}>
|
||||
document.getElementById(id).addEventListener("change", (ev) => {
|
||||
const chara_id = parseInt(ev.target.value);
|
||||
const chara = character[region].find((c) => c.chara_id === chara_id);
|
||||
const detail = chara != null ? {id, chara} : {id};
|
||||
const b = new CustomEvent("chara-change", { detail, bubbles: true });
|
||||
ev.target.dispatchEvent(b);
|
||||
});
|
||||
</script>
|
||||
2
site/components/LobbyConversations.astro
Normal file
2
site/components/LobbyConversations.astro
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
---
|
||||
27
site/data/character.ts
Normal file
27
site/data/character.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
|
||||
const global = globalJSON as Character[];
|
||||
|
||||
export const character = {
|
||||
global,
|
||||
}
|
||||
|
||||
export function searchChara(charas: Character[], id: Character["chara_id"]): Character | undefined {
|
||||
// TODO(zephyr): binary search
|
||||
return charas.find((c) => c.chara_id === id);
|
||||
}
|
||||
20
site/pages/index.astro
Normal file
20
site/pages/index.astro
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import CharaSelect from "../components/CharaSelect.astro";
|
||||
|
||||
import "../styles/normalize.css";
|
||||
import "../styles/sakura-vars.css";
|
||||
---
|
||||
<html>
|
||||
<body>
|
||||
<h1>Zenno Rob Roy</h1>
|
||||
<p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p>
|
||||
<ul>
|
||||
<li>Discord bot: Prove yourself right about skill details without switching tabs.</li>
|
||||
<li>Lobby conversations: Get recommendations on unlocking lobby conversations for the archive gallery.</li>
|
||||
</ul>
|
||||
<CharaSelect id="chara-test" label="Select character"/>
|
||||
<script>
|
||||
document.getElementById("chara-test")!.addEventListener("chara-change", (ev) => console.log("chara change", ev))
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
379
site/styles/normalize.css
vendored
Executable file
379
site/styles/normalize.css
vendored
Executable file
@@ -0,0 +1,379 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
/* 1 */
|
||||
height: 0;
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
/* 1 */
|
||||
text-decoration: underline;
|
||||
/* 2 */
|
||||
text-decoration: underline dotted;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
line-height: 1.15;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
display: table;
|
||||
/* 1 */
|
||||
max-width: 100%;
|
||||
/* 1 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
white-space: normal;
|
||||
/* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
padding: 0;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
401
site/styles/sakura-vars.css
Executable file
401
site/styles/sakura-vars.css
Executable file
@@ -0,0 +1,401 @@
|
||||
/* Sakura.css v1.5.0
|
||||
* ================
|
||||
* Minimal css theme.
|
||||
* Project: https://github.com/oxalorg/sakura/
|
||||
*/
|
||||
|
||||
/* data-theme="taiyō" */
|
||||
:root {
|
||||
--blossom: #292722;
|
||||
--fade: #7d7768;
|
||||
--bg: #ffecec;
|
||||
--bg-alt: #ffecec;
|
||||
--text: #292222;
|
||||
}
|
||||
|
||||
[data-theme="iron goddess"] {
|
||||
--blossom: #424b51;
|
||||
--fade: #64707a;
|
||||
--bg: #fff2e2;
|
||||
--bg-alt: #fffce2;
|
||||
--text: #2c2923;
|
||||
}
|
||||
|
||||
[data-theme="main sequence"] {
|
||||
--blossom: #3a5425;
|
||||
--fade: #698650;
|
||||
--bg: #fffde5;
|
||||
--bg-alt: #fff4e5;
|
||||
--text: #5e592a;
|
||||
}
|
||||
|
||||
[data-theme="sorcery"] {
|
||||
--blossom: #5a5a69;
|
||||
--fade: #868698;
|
||||
--bg: #e5f4e5;
|
||||
--bg-alt: #e6f4e6;
|
||||
--text: #323932;
|
||||
}
|
||||
|
||||
[data-theme="cirrus"] {
|
||||
--blossom: #565a4b;
|
||||
--fade: #9da587;
|
||||
--bg: #e5f6fa;
|
||||
--bg-alt: #e5f6fa;
|
||||
--text: #31393b;
|
||||
}
|
||||
|
||||
[data-theme="oxygen"] {
|
||||
--blossom: #162011;
|
||||
--fade: #343932;
|
||||
--bg: #e1e2e4;
|
||||
--bg-alt: #e3e0e3;
|
||||
--text: #27282c;
|
||||
}
|
||||
|
||||
[data-theme="dauphin"] {
|
||||
--blossom: #171e1c;
|
||||
--fade: #485b58;
|
||||
--bg: #ebe5f8;
|
||||
--bg-alt: #ebe5f8;
|
||||
--text: #1c1a20;
|
||||
}
|
||||
|
||||
[data-theme="diamond-burned"] {
|
||||
--blossom: #0f0d0b;
|
||||
--fade: #4d4743;
|
||||
--bg: #f8ebf2;
|
||||
--bg-alt: #ebe8f4;
|
||||
--text: #3e363a;
|
||||
}
|
||||
|
||||
[data-theme="chi"] {
|
||||
--blossom: #908975;
|
||||
--fade: #fff8e5;
|
||||
--bg: #110c0c;
|
||||
--bg-alt: #0a090c;
|
||||
--text: #cfa9a9;
|
||||
}
|
||||
|
||||
[data-theme="darjeeling"] {
|
||||
--blossom: #ba949c;
|
||||
--fade: #f8e1e6;
|
||||
--bg: #1c160d;
|
||||
--bg-alt: #1c160d;
|
||||
--text: #c9b9a0;
|
||||
}
|
||||
|
||||
[data-theme="subgiant"] {
|
||||
--blossom: #9fad8a;
|
||||
--fade: #e8f2d7;
|
||||
--bg: #16130b;
|
||||
--bg-alt: #16130b;
|
||||
--text: #bbb396;
|
||||
}
|
||||
|
||||
[data-theme="goblin"] {
|
||||
--blossom: #7a808e;
|
||||
--fade: #dae1ef;
|
||||
--bg: #070905;
|
||||
--bg-alt: #0a0906;
|
||||
--text: #acbd9f;
|
||||
}
|
||||
|
||||
[data-theme="altostratus"] {
|
||||
--blossom: #a8a0b7;
|
||||
--fade: #e5dbf7;
|
||||
--bg: #0c0f0f;
|
||||
--bg-alt: #1a1614;
|
||||
--text: #8da4a4;
|
||||
}
|
||||
|
||||
[data-theme="silicon"] {
|
||||
--blossom: #717f63;
|
||||
--fade: #c4d4b3;
|
||||
--bg: #050a0f;
|
||||
--bg-alt: #050a0f;
|
||||
--text: #838e9a;
|
||||
}
|
||||
|
||||
[data-theme="imperator"] {
|
||||
--blossom: #93a0a3;
|
||||
--fade: #f3fbfd;
|
||||
--bg: #0e0c12;
|
||||
--bg-alt: #0e0c12;
|
||||
--text: #a8a1b1;
|
||||
}
|
||||
|
||||
[data-theme="mædi"] {
|
||||
--blossom: #ccd3b6;
|
||||
--fade: #fdfbf3;
|
||||
--bg: #10090f;
|
||||
--bg-alt: #2f282e;
|
||||
--text: #9e889a;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.618;
|
||||
max-width: 38em;
|
||||
margin: auto;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 684px) {
|
||||
body {
|
||||
font-size: 1.53rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 382px) {
|
||||
body {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
font-weight: 700;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.35em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
small,
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--blossom);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--blossom);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--fade);
|
||||
border-bottom: 2px solid var(--text);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.4em;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
padding-left: 1em;
|
||||
padding-top: 0.8em;
|
||||
padding-bottom: 0.8em;
|
||||
padding-right: 0.8em;
|
||||
border-left: 5px solid var(--blossom);
|
||||
margin-bottom: 2.5rem;
|
||||
background-color: var(--bg-alt);
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Pre and Code */
|
||||
pre {
|
||||
background-color: var(--bg-alt);
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-size: 0.9em;
|
||||
padding: 0 0.5em;
|
||||
background-color: var(--bg-alt);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
white-space: pre;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
text-align: justify;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0.5em;
|
||||
border-bottom: 1px solid var(--bg-alt);
|
||||
}
|
||||
|
||||
/* Buttons, forms and input */
|
||||
input,
|
||||
textarea {
|
||||
border: 1px solid var(--text);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border: 1px solid var(--blossom);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
input[type=submit],
|
||||
input[type=reset],
|
||||
input[type=button],
|
||||
input[type=file]::file-selector-button {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: var(--blossom);
|
||||
color: var(--bg);
|
||||
border-radius: 1px;
|
||||
border: 1px solid var(--blossom);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button[disabled],
|
||||
button[disabled],
|
||||
input[type=submit][disabled],
|
||||
input[type=reset][disabled],
|
||||
input[type=button][disabled],
|
||||
input[type=file]::file-selector-button[disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
button:hover,
|
||||
input[type=submit]:hover,
|
||||
input[type=reset]:hover,
|
||||
input[type=button]:hover,
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: var(--fade);
|
||||
color: var(--bg);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button:focus-visible,
|
||||
button:focus-visible,
|
||||
input[type=submit]:focus-visible,
|
||||
input[type=reset]:focus-visible,
|
||||
input[type=button]:focus-visible,
|
||||
input[type=file]::file-selector-button:focus-visible {
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
}
|
||||
|
||||
textarea,
|
||||
select,
|
||||
input {
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
/* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
margin-bottom: 10px;
|
||||
background-color: var(--bg-alt);
|
||||
border: 1px solid var(--bg-alt);
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
input:focus {
|
||||
border: 1px solid var(--blossom);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox]:focus {
|
||||
outline: 1px dotted var(--blossom);
|
||||
}
|
||||
|
||||
label,
|
||||
legend,
|
||||
fieldset {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
2
std
2
std
Submodule std updated: f2e9e778f0...41b8aed39e
295
test/example.kk
Normal file
295
test/example.kk
Normal file
@@ -0,0 +1,295 @@
|
||||
module test/example
|
||||
|
||||
import std/num/decimal
|
||||
import std/data/linearmap
|
||||
import horse/game-id
|
||||
import horse/global
|
||||
import horse/global/character
|
||||
import horse/global/saddle
|
||||
import horse/global/skill
|
||||
import horse/global/spark
|
||||
import horse/global/uma
|
||||
import horse/legacy
|
||||
|
||||
val p1 = Legacy(
|
||||
uma = Veteran(
|
||||
uma = Uma-id(102001), // seiun sky
|
||||
sparks = [
|
||||
301, // 1* power
|
||||
2102, // 2* front runner
|
||||
10200103, // 3* angling and scheming
|
||||
1000302, // 2* osaka hai
|
||||
1001001, // 1* japanese derby
|
||||
1001101, // 1* yasuda kinen
|
||||
1001701, // 1* qe2
|
||||
2001402, // 2* non-standard distance
|
||||
2004301, // 1* focus
|
||||
2005301, // 1* early lead
|
||||
2012401, // 1* front runner straightaways
|
||||
2012502, // 2* front runner corners
|
||||
2015201, // 1* front runner savvy
|
||||
2016001, // 1* groundwork
|
||||
2016102, // 2* thh
|
||||
2016402, // 2* lone wolf
|
||||
3000201, // 1* unity cup
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
1, // classic triple crown
|
||||
2, // senior autumn triple crown
|
||||
4, // senior spring triple crown
|
||||
5, // tenno sweep
|
||||
6, // dual grand prix
|
||||
7, // dual miles
|
||||
10, // arima kinen
|
||||
11, // japan cup
|
||||
12, // derby
|
||||
13, // tss
|
||||
14, // takarazuka kinen
|
||||
15, // tsa
|
||||
16, // kikuka sho
|
||||
17, // osaka hai
|
||||
18, // satsuki sho
|
||||
21, // yasuda kinen
|
||||
23, // mile championship
|
||||
25, // victoria mile
|
||||
26, // qe2
|
||||
33, // asahi hai fs
|
||||
34, // hopeful stakes
|
||||
96, // mainichi hai
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub1 = Veteran(
|
||||
uma = Uma-id(102601), // mihono bourbon
|
||||
sparks = [
|
||||
302, // 2* power
|
||||
3303, // 3* medium
|
||||
10260102, // 2* g00 1st
|
||||
1001201, // 1* takarazuka kinen
|
||||
1001702, // 2* qe2
|
||||
1001901, // 1* japan cup
|
||||
2004302, // 2* focus
|
||||
2004502, // 2* prudent positioning
|
||||
2012502, // 2* front corners
|
||||
2015202, // 2* front savvy
|
||||
2016002, // 2* groundwork
|
||||
2016401, // 1* lone wolf
|
||||
3000201, // 1* unity cup
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
2, // senior autumn triple crown
|
||||
6, // dual grand prix
|
||||
7, // dual miles
|
||||
10, // arima kinen
|
||||
11, // japan cup
|
||||
12, // derby
|
||||
14, // takarazuka kinen
|
||||
15, // tsa
|
||||
17, // osaka hai
|
||||
18, // satsuki sho
|
||||
21, // yasuda kinen
|
||||
23, // mile championship
|
||||
25, // victoria mile
|
||||
26, // qe2
|
||||
27, // nhk mile cup
|
||||
33, // asahi hai fs
|
||||
34, // hopeful stakes
|
||||
49, // spring stakes
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub2 = Veteran(
|
||||
uma = Uma-id(102401), // mayano top gun
|
||||
sparks = [
|
||||
302, // 2* power
|
||||
1103, // 3* turf
|
||||
10240101, // 1* flashy landing
|
||||
1000601, // 1* tss
|
||||
1001202, // 2* takarazuka kinen
|
||||
1001502, // 2* kikuka sho
|
||||
1001601, // 1* tsa
|
||||
1002102, // 2* hanshin jf
|
||||
1002301, // 1* arima kinen
|
||||
2003503, // 3* corner recovery
|
||||
2003802, // 2* straightaway recovery
|
||||
2004602, // 2* ramp up
|
||||
2005502, // 2* final push
|
||||
2012702, // 2* leader's pride
|
||||
2016002, // 2* groundwork
|
||||
3000102, // 2* ura finale
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
1, // classic triple crown
|
||||
2, // senior autumn triple crown
|
||||
4, // senior spring triple crown
|
||||
5, // tenno sweep
|
||||
6, // dual grand prix
|
||||
7, // dual miles
|
||||
10, // arima kinen
|
||||
11, // japan cup
|
||||
12, // derby
|
||||
13, // tss
|
||||
14, // takarazuka kinen
|
||||
15, // tsa
|
||||
16, // kikuka sho
|
||||
18, // satsuki sho
|
||||
21, // yasuda kinen
|
||||
23, // mile championship
|
||||
25, // victoria mile
|
||||
26, // qe2
|
||||
34, // hopeful stakes
|
||||
35, // hanshin jf
|
||||
].map(Saddle-id(_))
|
||||
)
|
||||
)
|
||||
|
||||
val p2 = Legacy(
|
||||
uma = Veteran(
|
||||
uma = Uma-id(102601), // mihono bourbon
|
||||
sparks = [
|
||||
302,
|
||||
3303,
|
||||
1001201,
|
||||
1001702,
|
||||
1001901,
|
||||
2004302,
|
||||
2004502,
|
||||
2012502,
|
||||
2015202,
|
||||
2016002,
|
||||
2016401,
|
||||
3000201,
|
||||
10260102,
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
2,
|
||||
6,
|
||||
7,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
17,
|
||||
18,
|
||||
21,
|
||||
23,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33,
|
||||
34,
|
||||
49,
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub1 = Veteran(
|
||||
uma = Uma-id(102402), // wedding mayano
|
||||
sparks = [
|
||||
203,
|
||||
3202,
|
||||
1000701,
|
||||
1000802,
|
||||
1001201,
|
||||
1001803,
|
||||
2003502,
|
||||
2003701,
|
||||
2004301,
|
||||
2005502,
|
||||
2012401,
|
||||
2016402,
|
||||
10240202,
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
1,
|
||||
2,
|
||||
6,
|
||||
7,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
18,
|
||||
21,
|
||||
23,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33,
|
||||
34,
|
||||
48,
|
||||
].map(Saddle-id(_))
|
||||
),
|
||||
sub2 = Veteran(
|
||||
uma = Uma-id(100201), // silence suzuka
|
||||
sparks = [
|
||||
203,
|
||||
1101,
|
||||
1001901,
|
||||
1002203,
|
||||
1002302,
|
||||
2000101,
|
||||
2000201,
|
||||
2001902,
|
||||
2003501,
|
||||
2005401,
|
||||
2016001,
|
||||
3000102,
|
||||
10020101,
|
||||
].map(Spark-id(_)),
|
||||
saddles = [
|
||||
2,
|
||||
6,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
17,
|
||||
18,
|
||||
21,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
33,
|
||||
34,
|
||||
40,
|
||||
42,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
49,
|
||||
59,
|
||||
61,
|
||||
63,
|
||||
65,
|
||||
111,
|
||||
113,
|
||||
117,
|
||||
126,
|
||||
].map(Saddle-id(_))
|
||||
)
|
||||
)
|
||||
|
||||
val trainee = Uma-id(104601) // smart falcon
|
||||
|
||||
pub fun main()
|
||||
val p1a = parent-affinity(trainee, p1, p2.uma.uma)
|
||||
val p2a = parent-affinity(trainee, p2, p1.uma.uma)
|
||||
val (s11a, s12a) = sub-affinity(trainee, p1)
|
||||
val (s21a, s22a) = sub-affinity(trainee, p2)
|
||||
println("trainee: " ++ trainee.show)
|
||||
println("p1: " ++ p1.uma.uma.show ++ " affinity " ++ p1a.show)
|
||||
println("s1-1: " ++ p1.sub1.uma.show ++ " affinity " ++ s11a.show)
|
||||
println("s1-2: " ++ p1.sub2.uma.show ++ " affinity " ++ s12a.show)
|
||||
println("p2: " ++ p2.uma.uma.show ++ " affinity " ++ p1a.show)
|
||||
println("s1-1: " ++ p2.sub1.uma.show ++ " affinity " ++ s21a.show)
|
||||
println("s1-2: " ++ p2.sub2.uma.show ++ " affinity " ++ s22a.show)
|
||||
val inspo = inspiration(trainee, p1, p2)
|
||||
val s = inspiration-gives(inspo, legacy/skills)
|
||||
val a = inspiration-gives(inspo, legacy/aptitudes)
|
||||
println("\nskills:")
|
||||
s.list.foreach() fn((skill, pmf))
|
||||
println(" " ++ skill.show ++ ": " ++ pmf.show)
|
||||
println("\naptitudes:")
|
||||
a.list.foreach() fn((apt, pmf))
|
||||
println(" " ++ apt.show ++ ": " ++ pmf.show)
|
||||
12
test/global.kk
Normal file
12
test/global.kk
Normal file
@@ -0,0 +1,12 @@
|
||||
module test/global
|
||||
|
||||
import horse/global/character
|
||||
import horse/global/race
|
||||
import horse/global/saddle
|
||||
import horse/global/scenario
|
||||
import horse/global/skill
|
||||
import horse/global/spark
|
||||
import horse/global/uma
|
||||
|
||||
pub fun main()
|
||||
()
|
||||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strictest"
|
||||
}
|
||||
Reference in New Issue
Block a user