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 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), false)}, Flags: discord.MessageFlagIsComponentsV2 | 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), false)}, } return e.UpdateMessage(m) } func (s *skillServer) share(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.MessageCreate{ Components: []discord.LayoutComponent{s.render(horse.SkillID(id), true)}, Flags: discord.MessageFlagIsComponentsV2, } return e.CreateMessage(m) } func (s *skillServer) render(id horse.SkillID, share bool) discord.ContainerComponent { skill, ok := s.skills[id] if !ok { slog.Error("invalid skill id", slog.Int("id", int(id)), slog.Bool("share", share)) return discord.NewContainer(discord.NewTextDisplayf("invalid skill ID %v made it to render", id)) } thumburl := fmt.Sprintf("https://gametora.com/images/umamusume/skill_icons/utx_ico_skill_%d.png", skill.IconID) top := "## " + skill.Name if skill.UniqueOwner != "" { top += "\n-# " + skill.UniqueOwner } r := discord.NewContainer( discord.NewSection( discord.NewTextDisplay(top), discord.NewTextDisplay(skill.Description), ).WithAccessory(discord.NewThumbnail(thumburl)), ) 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" } r.Components = append(r.Components, discord.NewSmallSeparator()) 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 || !share { buttons := make([]discord.InteractiveComponent, 0, 4) for _, rs := range rel { b := discord.NewSecondaryButton(rs.Name, fmt.Sprintf("/skill/swap/%d", rs.ID)) if rs.ID == id { b = b.AsDisabled() } buttons = append(buttons, b) } if !share { buttons = append(buttons, discord.NewPrimaryButton("Share", fmt.Sprintf("/skill/share/%d", skill.ID))) } r.Components = append(r.Components, discord.NewActionRow(buttons...)) } 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 }