Files
horse/cmd/horsebot/skill.go

236 lines
7.2 KiB
Go

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
}