horsebot: refactor skill server
This commit is contained in:
@@ -11,7 +11,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/disgoorg/disgo"
|
"github.com/disgoorg/disgo"
|
||||||
@@ -58,13 +57,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
slog.SetDefault(slog.New(lh))
|
slog.SetDefault(slog.New(lh))
|
||||||
|
|
||||||
byID, byName, err := loadSkills(filepath.Join(dataDir, "skill.json"))
|
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"))
|
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 {
|
if err = errors.Join(err, err2); err != nil {
|
||||||
slog.Error("loading data", slog.Any("err", err))
|
slog.Error("loading data", slog.Any("err", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
skillsByID, skillsByName, skillGroupMap = byID, byName, groups
|
skillSrv := newSkillServer(skills, groups)
|
||||||
|
slog.Info("skill server ready")
|
||||||
|
|
||||||
token, err := os.ReadFile(tokenFile)
|
token, err := os.ReadFile(tokenFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,9 +82,9 @@ func main() {
|
|||||||
r.Use(middleware.Go)
|
r.Use(middleware.Go)
|
||||||
r.Use(logMiddleware)
|
r.Use(logMiddleware)
|
||||||
r.Route("/skill", func(r handler.Router) {
|
r.Route("/skill", func(r handler.Router) {
|
||||||
r.SlashCommand("/", skillHandler)
|
r.SlashCommand("/", skillSrv.slash)
|
||||||
r.Autocomplete("/", skillAutocomplete)
|
r.Autocomplete("/", skillSrv.autocomplete)
|
||||||
r.ButtonComponent("/{id}", skillButton)
|
r.ButtonComponent("/{id}", skillSrv.button)
|
||||||
})
|
})
|
||||||
|
|
||||||
opts := []bot.ConfigOpt{bot.WithDefaultGateway(), bot.WithEventListeners(r)}
|
opts := []bot.ConfigOpt{bot.WithDefaultGateway(), bot.WithEventListeners(r)}
|
||||||
@@ -145,33 +147,19 @@ var commands = []discord.ApplicationCommandCreate{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(zeph): these globals could go away, but there's a bit of ceremony to doing that
|
func loadSkills(file string) ([]horse.Skill, error) {
|
||||||
var (
|
|
||||||
skillsByID map[horse.SkillID]horse.Skill
|
|
||||||
skillsByName map[string]horse.SkillID
|
|
||||||
skillGroupMap map[horse.SkillGroupID]horse.SkillGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
func loadSkills(file string) (map[horse.SkillID]horse.Skill, map[string]horse.SkillID, error) {
|
|
||||||
b, err := os.ReadFile(file)
|
b, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var skills []horse.Skill
|
var skills []horse.Skill
|
||||||
if err := json.Unmarshal(b, &skills); err != nil {
|
if err := json.Unmarshal(b, &skills); err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
byID := make(map[horse.SkillID]horse.Skill, len(skills))
|
return skills, nil
|
||||||
byName := make(map[string]horse.SkillID, len(skills))
|
|
||||||
for _, s := range skills {
|
|
||||||
byID[s.ID] = s
|
|
||||||
byName[s.Name] = s.ID
|
|
||||||
}
|
|
||||||
slog.Info("loaded skills", slog.Int("count", len(skills)))
|
|
||||||
return byID, byName, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSkillGroups(file string) (map[horse.SkillGroupID]horse.SkillGroup, error) {
|
func loadSkillGroups(file string) ([]horse.SkillGroup, error) {
|
||||||
b, err := os.ReadFile(file)
|
b, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -180,59 +168,5 @@ func loadSkillGroups(file string) (map[horse.SkillGroupID]horse.SkillGroup, erro
|
|||||||
if err := json.Unmarshal(b, &groups); err != nil {
|
if err := json.Unmarshal(b, &groups); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
m := make(map[horse.SkillGroupID]horse.SkillGroup, len(groups))
|
return groups, nil
|
||||||
for _, s := range groups {
|
|
||||||
m[s.ID] = s
|
|
||||||
}
|
|
||||||
slog.Info("loaded skill groups", slog.Int("count", len(groups)))
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func skillHandler(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(skillsByID[horse.SkillID(id)].ID)
|
|
||||||
}
|
|
||||||
if id == 0 {
|
|
||||||
// Either we weren't given a number or the number doesn't match any skill ID.
|
|
||||||
v := skillsByName[q]
|
|
||||||
if v == 0 {
|
|
||||||
// No such skill.
|
|
||||||
m := discord.MessageCreate{
|
|
||||||
Content: "No such skill.",
|
|
||||||
Flags: discord.MessageFlagEphemeral,
|
|
||||||
}
|
|
||||||
return e.CreateMessage(m)
|
|
||||||
}
|
|
||||||
id = int64(v)
|
|
||||||
}
|
|
||||||
// TODO(zeph): search conditions and effects, give a list
|
|
||||||
m := discord.MessageCreate{
|
|
||||||
Components: []discord.LayoutComponent{RenderSkill(horse.SkillID(id), skillsByID, skillGroupMap)},
|
|
||||||
Flags: discord.MessageFlagIsComponentsV2,
|
|
||||||
}
|
|
||||||
return e.CreateMessage(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func skillAutocomplete(e *handler.AutocompleteEvent) error {
|
|
||||||
q := e.Data.String("query")
|
|
||||||
opts := skillGlobalAuto().Find(nil, q)
|
|
||||||
return e.AutocompleteResult(opts[:min(len(opts), 25)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func skillButton(data discord.ButtonInteractionData, e *handler.ComponentEvent) error {
|
|
||||||
id, err := strconv.ParseInt(e.Vars["id"], 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
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{RenderSkill(horse.SkillID(id), skillsByID, skillGroupMap)},
|
|
||||||
}
|
|
||||||
return e.UpdateMessage(m)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,58 +2,138 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/disgoorg/disgo/discord"
|
"github.com/disgoorg/disgo/discord"
|
||||||
|
"github.com/disgoorg/disgo/handler"
|
||||||
|
|
||||||
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
|
"git.sunturtle.xyz/zephyr/horse/cmd/horsebot/autocomplete"
|
||||||
"git.sunturtle.xyz/zephyr/horse/horse"
|
"git.sunturtle.xyz/zephyr/horse/horse"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderSkill(id horse.SkillID, all map[horse.SkillID]horse.Skill, groups map[horse.SkillGroupID]horse.SkillGroup) discord.ContainerComponent {
|
type skillServer struct {
|
||||||
s, ok := all[id]
|
skills map[horse.SkillID]horse.Skill
|
||||||
|
byName map[string]horse.SkillID
|
||||||
|
groups map[horse.SkillGroupID]horse.SkillGroup
|
||||||
|
autocom autocomplete.Set[discord.AutocompleteChoice]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSkillServer(skills []horse.Skill, groups []horse.SkillGroup) *skillServer {
|
||||||
|
s := skillServer{
|
||||||
|
skills: make(map[horse.SkillID]horse.Skill, len(skills)),
|
||||||
|
byName: make(map[string]horse.SkillID, len(skills)),
|
||||||
|
groups: make(map[horse.SkillGroupID]horse.SkillGroup, len(groups)),
|
||||||
|
}
|
||||||
|
for _, skill := range skills {
|
||||||
|
s.skills[skill.ID] = skill
|
||||||
|
s.byName[skill.Name] = skill.ID
|
||||||
|
s.autocom.Add(skill.Name, discord.AutocompleteChoiceString{Name: skill.Name, Value: skill.Name})
|
||||||
|
if skill.UniqueOwner != "" {
|
||||||
|
if skill.Rarity >= 3 {
|
||||||
|
s.autocom.Add(skill.UniqueOwner, discord.AutocompleteChoiceString{Name: "Unique: " + skill.UniqueOwner, Value: skill.Name})
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
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) render(id horse.SkillID) discord.ContainerComponent {
|
||||||
|
skill, ok := s.skills[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
return discord.NewContainer(discord.NewTextDisplayf("invalid skill ID %v made it to RenderSkill", id))
|
return discord.NewContainer(discord.NewTextDisplayf("invalid skill ID %v made it to RenderSkill", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
thumburl := fmt.Sprintf("https://gametora.com/images/umamusume/skill_icons/utx_ico_skill_%d.png", s.IconID)
|
thumburl := fmt.Sprintf("https://gametora.com/images/umamusume/skill_icons/utx_ico_skill_%d.png", skill.IconID)
|
||||||
top := "## " + s.Name
|
top := "## " + skill.Name
|
||||||
if s.UniqueOwner != "" {
|
if skill.UniqueOwner != "" {
|
||||||
top += "\n-# " + s.UniqueOwner
|
top += "\n-# " + skill.UniqueOwner
|
||||||
}
|
}
|
||||||
r := discord.NewContainer(
|
r := discord.NewContainer(
|
||||||
discord.NewSection(
|
discord.NewSection(
|
||||||
discord.NewTextDisplay(top),
|
discord.NewTextDisplay(top),
|
||||||
discord.NewTextDisplay(s.Description),
|
discord.NewTextDisplay(skill.Description),
|
||||||
).WithAccessory(discord.NewThumbnail(thumburl)),
|
).WithAccessory(discord.NewThumbnail(thumburl)),
|
||||||
)
|
)
|
||||||
var skilltype string
|
var skilltype string
|
||||||
switch {
|
switch {
|
||||||
case s.Rarity == 3, s.Rarity == 4, s.Rarity == 5:
|
case skill.Rarity == 3, skill.Rarity == 4, skill.Rarity == 5:
|
||||||
// unique of various star levels
|
// unique of various star levels
|
||||||
r.AccentColor = 0xaca4d4
|
r.AccentColor = 0xaca4d4
|
||||||
skilltype = "Unique Skill"
|
skilltype = "Unique Skill"
|
||||||
case s.UniqueOwner != "":
|
case skill.UniqueOwner != "":
|
||||||
r.AccentColor = 0xcccccc
|
r.AccentColor = 0xcccccc
|
||||||
skilltype = "Inherited Unique"
|
skilltype = "Inherited Unique"
|
||||||
case s.Rarity == 2:
|
case skill.Rarity == 2:
|
||||||
// rare (gold)
|
// rare (gold)
|
||||||
r.AccentColor = 0xd7c25b
|
r.AccentColor = 0xd7c25b
|
||||||
skilltype = "Rare Skill"
|
skilltype = "Rare Skill"
|
||||||
case s.GroupRate == -1:
|
case skill.GroupRate == -1:
|
||||||
// negative (purple) skill
|
// negative (purple) skill
|
||||||
r.AccentColor = 0x9151d4
|
r.AccentColor = 0x9151d4
|
||||||
skilltype = "Negative Skill"
|
skilltype = "Negative Skill"
|
||||||
case !s.WitCheck:
|
case !skill.WitCheck:
|
||||||
// should be passive (green)
|
// should be passive (green)
|
||||||
r.AccentColor = 0x66ae1c
|
r.AccentColor = 0x66ae1c
|
||||||
skilltype = "Passive Skill"
|
skilltype = "Passive Skill"
|
||||||
case isDebuff(s):
|
case isDebuff(skill):
|
||||||
// debuff (red)
|
// debuff (red)
|
||||||
r.AccentColor = 0xe34747
|
r.AccentColor = 0xe34747
|
||||||
skilltype = "Debuff Skill"
|
skilltype = "Debuff Skill"
|
||||||
case s.Rarity == 1:
|
case skill.Rarity == 1:
|
||||||
// common (white)
|
// common (white)
|
||||||
r.AccentColor = 0xcccccc
|
r.AccentColor = 0xcccccc
|
||||||
skilltype = "Common Skill"
|
skilltype = "Common Skill"
|
||||||
@@ -61,7 +141,7 @@ func RenderSkill(id horse.SkillID, all map[horse.SkillID]horse.Skill, groups map
|
|||||||
r.Components = append(r.Components, discord.NewSmallSeparator())
|
r.Components = append(r.Components, discord.NewSmallSeparator())
|
||||||
text := make([]string, 0, 3)
|
text := make([]string, 0, 3)
|
||||||
abils := make([]string, 0, 3)
|
abils := make([]string, 0, 3)
|
||||||
for _, act := range s.Activations {
|
for _, act := range skill.Activations {
|
||||||
text, abils = text[:0], abils[:0]
|
text, abils = text[:0], abils[:0]
|
||||||
if act.Precondition != "" {
|
if act.Precondition != "" {
|
||||||
text = append(text, "Precondition: "+formatCondition(act.Precondition))
|
text = append(text, "Precondition: "+formatCondition(act.Precondition))
|
||||||
@@ -91,13 +171,13 @@ func RenderSkill(id horse.SkillID, all map[horse.SkillID]horse.Skill, groups map
|
|||||||
r.Components = append(r.Components, discord.NewTextDisplay(strings.Join(text, "\n")))
|
r.Components = append(r.Components, discord.NewTextDisplay(strings.Join(text, "\n")))
|
||||||
}
|
}
|
||||||
|
|
||||||
l := discord.NewTextDisplayf("%s ・ SP cost %d ・ Grade value %d ・ [Conditions on GameTora](https://gametora.com/umamusume/skill-condition-viewer?skill=%d)", skilltype, s.SPCost, s.GradeValue, s.ID)
|
l := discord.NewTextDisplayf("%s ・ SP cost %d ・ Grade value %d ・ [Conditions on GameTora](https://gametora.com/umamusume/skill-condition-viewer?skill=%d)", skilltype, skill.SPCost, skill.GradeValue, skill.ID)
|
||||||
r.Components = append(r.Components, discord.NewSmallSeparator(), l)
|
r.Components = append(r.Components, discord.NewSmallSeparator(), l)
|
||||||
rel := make([]horse.Skill, 0, 4)
|
rel := make([]horse.Skill, 0, 4)
|
||||||
group := groups[s.Group]
|
group := s.groups[skill.Group]
|
||||||
for _, id := range [...]horse.SkillID{group.Skill1, group.Skill2, group.Skill3, group.SkillBad} {
|
for _, id := range [...]horse.SkillID{group.Skill1, group.Skill2, group.Skill3, group.SkillBad} {
|
||||||
if id != 0 {
|
if id != 0 {
|
||||||
rel = append(rel, all[id])
|
rel = append(rel, s.skills[id])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(rel) > 1 {
|
if len(rel) > 1 {
|
||||||
@@ -132,19 +212,3 @@ func isDebuff(s horse.Skill) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var skillGlobalAuto = sync.OnceValue(func() *autocomplete.Set[discord.AutocompleteChoice] {
|
|
||||||
var set autocomplete.Set[discord.AutocompleteChoice]
|
|
||||||
// NOTE(zeph): we're using global variables here
|
|
||||||
for _, s := range skillsByID {
|
|
||||||
set.Add(s.Name, discord.AutocompleteChoiceString{Name: s.Name, Value: s.Name})
|
|
||||||
if s.UniqueOwner != "" {
|
|
||||||
if s.Rarity >= 3 {
|
|
||||||
set.Add(s.UniqueOwner, discord.AutocompleteChoiceString{Name: "Unique: " + s.UniqueOwner, Value: s.Name})
|
|
||||||
} else {
|
|
||||||
set.Add(s.UniqueOwner, discord.AutocompleteChoiceString{Name: "Inherited unique: " + s.UniqueOwner, Value: s.Name})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &set
|
|
||||||
})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user