implement skill search by name or id
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
token
|
||||||
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module git.sunturtle.xyz/zephyr/horsebot
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.sunturtle.xyz/zephyr/horse v0.0.0-20260116044610-b0c555f547e4
|
||||||
|
github.com/disgoorg/disgo v0.19.0-rc.15
|
||||||
|
)
|
||||||
|
|
||||||
|
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/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
|
github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect
|
||||||
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
|
)
|
||||||
28
go.sum
Normal file
28
go.sum
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
git.sunturtle.xyz/zephyr/horse v0.0.0-20260116044610-b0c555f547e4 h1:YJEGZG/EnxE5Tr6EZMGxikXDF6QKKHrH9Bp1dLoWXkk=
|
||||||
|
git.sunturtle.xyz/zephyr/horse v0.0.0-20260116044610-b0c555f547e4/go.mod h1:qGXO/93EfCOI1oGSLqrRkPDF/EAdsgLNZJjRKx+i4Lk=
|
||||||
|
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
135
main.go
Normal file
135
main.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||||
|
"git.sunturtle.xyz/zephyr/horse/horse/global"
|
||||||
|
"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/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
appID string
|
||||||
|
pubkey string
|
||||||
|
tokenFile string
|
||||||
|
)
|
||||||
|
flag.StringVar(&appID, "id", "", "Discord application ID")
|
||||||
|
flag.StringVar(&pubkey, "key", "", "Discord public key")
|
||||||
|
flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
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(middleware.Logger)
|
||||||
|
r.Route("/skill", func(r handler.Router) {
|
||||||
|
r.SlashCommand("/", skillHandler)
|
||||||
|
r.Autocomplete("/", skillAutocomplete)
|
||||||
|
// TODO(zeph): button handler
|
||||||
|
})
|
||||||
|
|
||||||
|
slog.Info("connect", slog.String("disgo", disgo.Version))
|
||||||
|
client, err := disgo.New(string(token), bot.WithDefaultGateway(), bot.WithEventListeners(r))
|
||||||
|
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 err := client.OpenGateway(ctx); err != nil {
|
||||||
|
slog.Error("opening gateway", slog.Any("err", err))
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
slog.Info("running")
|
||||||
|
<-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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func skillHandler(data discord.SlashCommandInteractionData, e *handler.CommandEvent) error {
|
||||||
|
q := data.String("query")
|
||||||
|
id, err := strconv.ParseInt(q, 10, 32)
|
||||||
|
var s horse.Skill
|
||||||
|
if err == nil {
|
||||||
|
// note inverted condition; this is when we have an id
|
||||||
|
s = global.AllSkills[horse.SkillID(id)]
|
||||||
|
}
|
||||||
|
if s.ID == 0 {
|
||||||
|
// Either we weren't given a number or the number doesn't match any skill ID.
|
||||||
|
id, ok := global.SkillNameToID[q]
|
||||||
|
if !ok {
|
||||||
|
// No such skill.
|
||||||
|
m := discord.MessageCreate{
|
||||||
|
Content: "No such skill.",
|
||||||
|
Flags: discord.MessageFlagEphemeral,
|
||||||
|
}
|
||||||
|
return e.CreateMessage(m)
|
||||||
|
}
|
||||||
|
s = global.AllSkills[id]
|
||||||
|
}
|
||||||
|
// TODO(zeph): search conditions and effects, give a list
|
||||||
|
m := discord.MessageCreate{
|
||||||
|
Components: []discord.LayoutComponent{RenderSkill(s)},
|
||||||
|
Flags: discord.MessageFlagIsComponentsV2,
|
||||||
|
}
|
||||||
|
return e.CreateMessage(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func skillAutocomplete(e *handler.AutocompleteEvent) error {
|
||||||
|
q := strings.ToLower(e.Data.String("query"))
|
||||||
|
r := make([]discord.AutocompleteChoice, 0, 25)
|
||||||
|
for k, _ := range global.SkillNameToID {
|
||||||
|
if strings.HasPrefix(strings.ToLower(k), q) {
|
||||||
|
r = append(r, discord.AutocompleteChoiceString{Name: k, Value: k})
|
||||||
|
if len(r) == cap(r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.AutocompleteResult(r)
|
||||||
|
}
|
||||||
107
skill.go
Normal file
107
skill.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||||
|
"github.com/disgoorg/disgo/discord"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderSkill(s horse.Skill) discord.ContainerComponent {
|
||||||
|
skilltype := discord.TextDisplayComponent{
|
||||||
|
ID: 4,
|
||||||
|
Content: "Skill Issue",
|
||||||
|
}
|
||||||
|
r := discord.ContainerComponent{
|
||||||
|
ID: 1, // specify ids so we guarantee we don't get collisions with related skill buttons
|
||||||
|
Components: []discord.ContainerSubComponent{
|
||||||
|
discord.SectionComponent{
|
||||||
|
ID: 2,
|
||||||
|
Components: []discord.SectionSubComponent{
|
||||||
|
discord.TextDisplayComponent{
|
||||||
|
ID: 3,
|
||||||
|
Content: "## " + s.Name + "\n" + s.Description,
|
||||||
|
},
|
||||||
|
&skilltype, // doing something evil :)
|
||||||
|
},
|
||||||
|
Accessory: discord.NewThumbnail(fmt.Sprintf("https://gametora.com/images/umamusume/skill_icons/utx_ico_skill_%d.png", s.IconID)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case s.Rarity == 3, s.Rarity == 4, s.Rarity == 5:
|
||||||
|
// unique of various star levels
|
||||||
|
r.AccentColor = 0xaca4d4
|
||||||
|
skilltype.Content = "Unique Skill"
|
||||||
|
case s.Rarity == 2:
|
||||||
|
// rare (gold)
|
||||||
|
r.AccentColor = 0xd7c25b
|
||||||
|
skilltype.Content = "Rare Skill"
|
||||||
|
case s.GroupRate == -1:
|
||||||
|
// negative (purple) skill
|
||||||
|
r.AccentColor = 0x9151d4
|
||||||
|
skilltype.Content = "Negative Skill"
|
||||||
|
case !s.WitCheck:
|
||||||
|
// should be passive (green)
|
||||||
|
r.AccentColor = 0x66ae1c
|
||||||
|
skilltype.Content = "Passive Skill"
|
||||||
|
// TODO(zeph): debuff (red)
|
||||||
|
case s.Rarity == 1:
|
||||||
|
// common (white)
|
||||||
|
r.AccentColor = 0xcccccc
|
||||||
|
skilltype.Content = "Common Skill"
|
||||||
|
}
|
||||||
|
r.Components = append(r.Components, discord.SeparatorComponent{ID: 5, Spacing: discord.SeparatorSpacingSizeSmall})
|
||||||
|
text := make([]string, 0, 3)
|
||||||
|
abils := make([]string, 0, 3)
|
||||||
|
for i, act := range s.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 "
|
||||||
|
default:
|
||||||
|
t = act.Duration.String() + "s "
|
||||||
|
}
|
||||||
|
for _, a := range act.Abilities {
|
||||||
|
abils = append(abils, a.String())
|
||||||
|
}
|
||||||
|
t += strings.Join(abils, ", ")
|
||||||
|
if act.Cooldown < 500e4 {
|
||||||
|
t += " on " + act.Cooldown.String() + "s cooldown"
|
||||||
|
}
|
||||||
|
text = append(text, t)
|
||||||
|
r.Components = append(r.Components, discord.TextDisplayComponent{
|
||||||
|
ID: 10 + i,
|
||||||
|
Content: strings.Join(text, "\n"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r.Components = append(r.Components, discord.SeparatorComponent{ID: 50, Spacing: discord.SeparatorSpacingSizeSmall})
|
||||||
|
r.Components = append(r.Components, discord.TextDisplayComponent{
|
||||||
|
ID: 51,
|
||||||
|
Content: fmt.Sprintf("SP cost %d. Grade value %d.", s.SPCost, s.GradeValue),
|
||||||
|
})
|
||||||
|
// TODO(zeph): related skills, use a row of buttons with skill numbers for the ids and edit the message when clicked?
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatCondition(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "&", " & ")
|
||||||
|
if strings.ContainsRune(s, '@') {
|
||||||
|
return "```\n" + strings.ReplaceAll(s, "@", "\n@\n") + "```"
|
||||||
|
}
|
||||||
|
return "`" + s + "`"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(zeph): autocomplete
|
||||||
|
// if we want to backgroundify construction of an autocomplete map,
|
||||||
|
// use sync.OnceValue and launch a goroutine in main to get the value and discard it
|
||||||
Reference in New Issue
Block a user