diff --git a/plugins/velouremon/ability_db.go b/plugins/velouremon/ability_db.go index bc24e9f..6ce6000 100644 --- a/plugins/velouremon/ability_db.go +++ b/plugins/velouremon/ability_db.go @@ -16,7 +16,7 @@ func (vp *VelouremonPlugin) loadAbilities() error { for rows.Next() { ability := &Ability{} - err := rows.Scan(ability) + err := rows.StructScan(ability) if err != nil { log.Error().Err(err) return err @@ -49,7 +49,7 @@ func (vp *VelouremonPlugin) loadAbilityRefsForCreature(ref *CreatureRef) ([]*Abi abilities := []*AbilityRef{} for rows.Next() { ability := &AbilityRef{} - err := rows.Scan(ability) + err := rows.StructScan(ability) if err != nil { log.Error().Err(err) @@ -82,3 +82,12 @@ func (vp *VelouremonPlugin) loadAbilityFromRef(ref *AbilityRef) (*Ability, error } return ability, nil } + +func (vp *VelouremonPlugin) saveNewAbility(ability *Ability) error { + _, err := vp.db.Exec(`insert into velouremon_abilities (name, damage, heal, shield, weaken, critical) values (?, ?, ?, ?, ?, ?);`, ability.Name, ability.Damage, ability.Heal, ability.Shield, ability.Weaken, ability.Critical) + if err != nil { + log.Error().Err(err) + return err + } + return nil +} diff --git a/plugins/velouremon/commands.go b/plugins/velouremon/commands.go index 5da5f57..125c4ef 100644 --- a/plugins/velouremon/commands.go +++ b/plugins/velouremon/commands.go @@ -1,10 +1,89 @@ package velouremon import ( + "strconv" + "github.com/velour/catbase/bot" ) -func (vp *VelouremonPlugin) handleStatus(c bot.Connector, player *Player, tokens []string) bool { +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func (vp *VelouremonPlugin) handleStatus(c bot.Connector, player *Player) bool { vp.bot.Send(c, bot.Message, vp.channel, player.string()) return true } + +func (vp *VelouremonPlugin) handleAddCreature(c bot.Connector, tokens []string) bool { + if len(tokens) == 3 { + name := tokens[0] + stats := make([]int, 2) + fail := false + for i := range stats { + stat, err := strconv.Atoi(tokens[i+1]) + if err != nil { + fail = true + break + } + stats[i] = min(max(stat, 0), 255) + } + if !fail { + err := vp.saveNewCreature(&Creature{ + Name: name, + Defense: stats[0], + Attack: stats[1], + }) + if err == nil { + vp.bot.Send(c, bot.Message, vp.channel, "Added " + name) + return true + } + } + } + + vp.bot.Send(c, bot.Message, vp.channel, "!add_creature [name] [defense 0-255] [attack 0-255]") + return true +} + +func (vp *VelouremonPlugin) handleAddAbility(c bot.Connector, tokens []string) bool { + if len(tokens) == 6 { + name := tokens[0] + stats := make([]int, 5) + fail := false + for i := range stats { + stat, err := strconv.Atoi(tokens[i+1]) + if err != nil { + fail = true + break + } + stats[i] = min(max(stat, 0), 255) + } + if !fail { + err := vp.saveNewAbility(&Ability{ + Name: name, + Damage: stats[0], + Heal: stats[1], + Shield: stats[2], + Weaken: stats[3], + Critical: stats[4], + }) + if err == nil { + vp.bot.Send(c, bot.Message, vp.channel, "Added " + name) + return true + } + } + } + + vp.bot.Send(c, bot.Message, vp.channel, "!add_ability [name] [damage 0-255] [heal 0-255] [shield 0-255] [weaken 0-255] [critical 0-255]") + return true +} diff --git a/plugins/velouremon/creature.go b/plugins/velouremon/creature.go index 900e10b..e6c41b5 100644 --- a/plugins/velouremon/creature.go +++ b/plugins/velouremon/creature.go @@ -1,6 +1,8 @@ package velouremon import ( + "math/rand" + "math" "fmt" ) @@ -28,3 +30,25 @@ func (c *Creature) string() string { } return message } + +func (vp *VelouremonPlugin) buildOutCreature(c *Creature) *Creature { + creature := &Creature{ + ID: c.ID, + Name: c.Name, + Health: 255, + Experience: int(math.Max(1, rand.NormFloat64() * 100 + 250)), + Defense: c.Defense, + Attack: c.Attack, + Abilities: make([]*Ability, rand.Intn(4)), + } + + used := map[int]int{} + for i := range creature.Abilities { + index := rand.Intn(len(vp.abilities)) + for _, ok := used[index]; ok; _, ok = used[index] { + index = rand.Intn(len(vp.abilities)) + } + creature.Abilities[i] = vp.abilities[index] + } + return creature +} diff --git a/plugins/velouremon/creature_db.go b/plugins/velouremon/creature_db.go index 8f06b74..75ff8f3 100644 --- a/plugins/velouremon/creature_db.go +++ b/plugins/velouremon/creature_db.go @@ -19,11 +19,13 @@ func (vp *VelouremonPlugin) loadCreatures() error { Health: 255, Experience: 0, } - err := rows.Scan(creature) + err := rows.StructScan(creature) + log.Print(err) if err != nil { log.Error().Err(err) return err } + vp.creatures = append(vp.creatures, creature) } return nil @@ -52,7 +54,7 @@ func (vp *VelouremonPlugin) loadCreatureRefsForPlayer(player *Player) ([]*Creatu creatures := []*CreatureRef{} for rows.Next() { creature := &CreatureRef{} - err := rows.Scan(creature) + err := rows.StructScan(creature) if err != nil { log.Error().Err(err) @@ -114,3 +116,12 @@ func (vp *VelouremonPlugin) saveCreatureForPlayer(player *Player, creature *Crea } return nil } + +func (vp *VelouremonPlugin) saveNewCreature(creature *Creature) error { + _, err := vp.db.Exec(`insert into velouremon_creatures (name, defense, attack) values (?, ?, ?);`, creature.Name, creature.Defense, creature.Attack) + if err != nil { + log.Error().Err(err) + return err + } + return nil +} diff --git a/plugins/velouremon/database.go b/plugins/velouremon/database.go index ab7482e..66153fb 100644 --- a/plugins/velouremon/database.go +++ b/plugins/velouremon/database.go @@ -5,54 +5,65 @@ import ( ) func (vp *VelouremonPlugin) checkAndBuildDBOrFail() { + dbNeedsPopulating := false + if rows, err := vp.db.Queryx(`select name from sqlite_master where type='table' and name='velouremon_players';`); err != nil { + log.Fatal().Err(err) + } else { + dbNeedsPopulating = rows.Next() + } + if _, err := vp.db.Exec(`create table if not exists velouremon_players ( - id integer primary key, - chatid string, - player string, - health integer, - experience integer + id integer primary key, + chatid string, + player string, + health integer, + experience integer );`); err != nil { log.Fatal().Err(err) } if _, err := vp.db.Exec(`create table if not exists velouremon_creature_ref ( - id integer primary key, - player integer, - creature integer, - health integer, + id integer primary key, + player integer, + creature integer, + health integer, experience integer );`); err != nil { log.Fatal().Err(err) } if _, err := vp.db.Exec(`create table if not exists velouremon_creatures ( - id integer primary key, - creature string, - defense integer, - attack integer + id integer primary key, + name string, + defense integer, + attack integer );`); err != nil { log.Fatal().Err(err) } if _, err := vp.db.Exec(`create table if not exists velouremon_ability_ref ( - id integer primary key, + id integer primary key, creatureref integer, - ability integer + ability integer );`); err != nil { log.Fatal().Err(err) } if _, err := vp.db.Exec(`create table if not exists velouremon_abilities ( - id integer primary key, - name string, - damage int, - heal int, - shield int, - weaken int, + id integer primary key, + name string, + damage int, + heal int, + shield int, + weaken int, critical int );`); err != nil { log.Fatal().Err(err) } + + if dbNeedsPopulating { + vp.populateDBWithBaseData() + } } func (vp *VelouremonPlugin) loadFromDB() { diff --git a/plugins/velouremon/interaction.go b/plugins/velouremon/interaction.go index b3b519e..135efb4 100644 --- a/plugins/velouremon/interaction.go +++ b/plugins/velouremon/interaction.go @@ -2,6 +2,7 @@ package velouremon import ( "fmt" + "strings" "math/rand" "time" @@ -9,8 +10,10 @@ import ( ) type Interaction struct { + id string players []*Player creatures []*Creature + started bool } func randomInteraction(c bot.Connector, vp *VelouremonPlugin) { @@ -19,10 +22,56 @@ func randomInteraction(c bot.Connector, vp *VelouremonPlugin) { if vp.channel != "" { creature := vp.creatures[rand.Intn(len(vp.creatures))] message := fmt.Sprintf("A wild %s appeared.", creature.Name) - vp.bot.Send(c, bot.Message, vp.channel, message) + id, _ := vp.bot.Send(c, bot.Message, vp.channel, message) + + vp.threads[id] = &Interaction { + id: id, + players: []*Player{}, + creatures: []*Creature{ + vp.buildOutCreature(creature), + }, + started: false, + } + + vp.bot.Send(c, bot.Reply, vp.channel, "A wild %s appeared.", id) } - dur, _ := time.ParseDuration("1h") - vp.timer.Reset(dur) + vp.timer.Reset(1 * time.Hour) } } + +func (i *Interaction) handleMessage(vp *VelouremonPlugin, c bot.Connector, player *Player, tokens []string) bool { + if len(tokens) > 0 { + command := strings.ToLower(tokens[0]) + if command == "join" { + return i.handleJoin(vp, c, player) + } else if command == "run" { + return i.handleRun(vp, c, player) + } + } + return false +} + +func (i *Interaction) handleJoin(vp *VelouremonPlugin, c bot.Connector, player *Player) bool { + for _, p := range i.players { + if player == p { + vp.bot.Send(c, bot.Reply, vp.channel, player.Name + " is already in the party.", i.id) + return true + } + } + i.players = append(i.players, player) + vp.bot.Send(c, bot.Reply, vp.channel, player.Name + " has just joined the party.", i.id) + return true +} + +func (i *Interaction) handleRun(vp *VelouremonPlugin, c bot.Connector, player *Player) bool { + for index, p := range i.players { + if player == p { + i.players = append(i.players[:index], i.players[index+1:]...) + vp.bot.Send(c, bot.Reply, vp.channel, player.Name + " has just left the party.", i.id) + return true + } + } + vp.bot.Send(c, bot.Reply, vp.channel, player.Name + " is not currently in the party.", i.id) + return true +} diff --git a/plugins/velouremon/player.go b/plugins/velouremon/player.go index a5fe82c..00b088f 100644 --- a/plugins/velouremon/player.go +++ b/plugins/velouremon/player.go @@ -27,7 +27,7 @@ func (vp *VelouremonPlugin) getOrAddPlayer(u *user.User) (*Player, error) { } func (p *Player) string() string { - message := fmt.Sprintf("%s : %d HP, %d XP\n", p.Name, p.Health, p.Experience) + message := fmt.Sprintf("%s: %d HP, %d XP\n", p.Name, p.Health, p.Experience) for _, creature := range p.Creatures { message += "\t" + strings.ReplaceAll(creature.string(), "\n", "\n\t") message = strings.TrimSuffix(message, "\t") diff --git a/plugins/velouremon/player_db.go b/plugins/velouremon/player_db.go index 72dc3e8..7a7190b 100644 --- a/plugins/velouremon/player_db.go +++ b/plugins/velouremon/player_db.go @@ -16,7 +16,7 @@ func (vp *VelouremonPlugin) loadPlayers() error { for rows.Next() { player := &Player{} - err := rows.Scan(player) + err := rows.StructScan(player) if err != nil { log.Error().Err(err) return err @@ -35,7 +35,7 @@ func (vp *VelouremonPlugin) addPlayer(p *user.User) (*Player, error) { player := &Player{ ChatID: p.ID, Name: p.Name, - Health: 128, + Health: 255, Experience: 0, Creatures: []*Creature{}, } diff --git a/plugins/velouremon/populate_db.go b/plugins/velouremon/populate_db.go new file mode 100644 index 0000000..29a9322 --- /dev/null +++ b/plugins/velouremon/populate_db.go @@ -0,0 +1,17 @@ +package velouremon + +func (vp *VelouremonPlugin) populateDBWithBaseData() { + vp.db.Exec(`insert into velouremon_creatures (name, defense, attack) values (?, ?, ?);`, + "Lap Sprite", 10, 5) + vp.db.Exec(`insert into velouremon_creatures (name, defense, attack) values (?, ?, ?);`, + "Industry Rep", 5, 10) + vp.db.Exec(`insert into velouremon_creatures (name, defense, attack) values (?, ?, ?);`, + "Charpov", 10, 10) + + vp.db.Exec(`insert into velouremon_abilities (name, damage, heal, shield, weaken, critical) values (?, ?, ?, ?, ?, ?);`, + "Procrastinate", 0, 0, 10, 0, 0) + vp.db.Exec(`insert into velouremon_abilities (name, damage, heal, shield, weaken, critical) values (?, ?, ?, ?, ?, ?);`, + "Defend", 0, 0, 5, 0, 0) + vp.db.Exec(`insert into velouremon_abilities (name, damage, heal, shield, weaken, critical) values (?, ?, ?, ?, ?, ?);`, + "Graduate", 0, 255, 0, 0, 0) +} diff --git a/plugins/velouremon/velouremon.go b/plugins/velouremon/velouremon.go index e9ebff1..5449e86 100644 --- a/plugins/velouremon/velouremon.go +++ b/plugins/velouremon/velouremon.go @@ -10,13 +10,10 @@ import ( "github.com/velour/catbase/bot/msg" ) -type VelouremonHandler func(bot.Connector, *Player, []string) bool - type VelouremonPlugin struct { bot bot.Bot db *sqlx.DB channel string - handlers map[string]VelouremonHandler threads map[string]*Interaction players []*Player creatures []*Creature @@ -25,29 +22,26 @@ type VelouremonPlugin struct { } func New(b bot.Bot) *VelouremonPlugin { - dur, _ := time.ParseDuration("15m") - timer := time.NewTimer(dur) - vp := &VelouremonPlugin{ bot: b, db: b.DB(), channel: "", - handlers: map[string]VelouremonHandler{}, threads: map[string]*Interaction{}, players: []*Player{}, creatures: []*Creature{}, abilities: []*Ability{}, - timer: timer, + timer: time.NewTimer(15 * time.Minute), } vp.checkAndBuildDBOrFail() vp.loadFromDB() - vp.handlers["status"] = vp.handleStatus - b.Register(vp, bot.Message, vp.message) + b.Register(vp, bot.Reply, vp.replyMessage) b.Register(vp, bot.Help, vp.help) + go randomInteraction(b.DefaultConnector(), vp) + return vp } @@ -63,17 +57,43 @@ func (vp *VelouremonPlugin) message(c bot.Connector, kind bot.Kind, message msg. tokens := strings.Fields(message.Body) command := strings.ToLower(tokens[0]) - if fun, ok := vp.handlers[command]; ok { + if command == "status" { player, err := vp.getOrAddPlayer(message.User) if err != nil { return false } - return fun(c, player, tokens[1:]) + return vp.handleStatus(c, player) + } else if len(tokens) > 1 { + if command == "add_creature" { + return vp.handleAddCreature(c, tokens[1:]) + } else if command == "add_ability" { + return vp.handleAddAbility(c, tokens[1:]) + } } return false } +func (vp *VelouremonPlugin) replyMessage(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { + if !message.Command { + return false + } + + identifier := args[0].(string) + if strings.ToLower(message.User.Name) != strings.ToLower(vp.bot.Config().Get("Nick", "bot")) { + if interaction, ok := vp.threads[identifier]; ok { + player, err := vp.getOrAddPlayer(message.User) + if err != nil { + return false + } + tokens := strings.Fields(message.Body) + return interaction.handleMessage(vp, c, player, tokens) + } + } + return false +} + + func (vp *VelouremonPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { vp.bot.Send(c, bot.Message, message.Channel, "try something else, this is too complicated for you.") return true diff --git a/plugins/velouremon/velouremon_test.go b/plugins/velouremon/velouremon_test.go new file mode 100644 index 0000000..5130126 --- /dev/null +++ b/plugins/velouremon/velouremon_test.go @@ -0,0 +1,76 @@ +package velouremon + +import ( + "github.com/velour/catbase/plugins/cli" + "os" + "strings" + "time" + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/bot/user" +) + +func init() { + log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) +} + +func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) { + isCmd := strings.HasPrefix(payload, "!") + if isCmd { + payload = payload[1:] + } + return &cli.CliPlugin{}, bot.Message, msg.Message{ + User: &user.User{Name: by}, + Channel: "test", + Body: payload, + Command: isCmd, + } +} + +func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) { + return makeMessageBy(payload, "tester") +} + +func setup(t *testing.T) (*VelouremonPlugin, *bot.MockBot) { + mb := bot.NewMockBot() + r := New(mb) + r.channel = "test" + return r, mb +} + +func TestStatus(t *testing.T) { + c, mb := setup(t) + res := c.message(makeMessage("!status")) + assert.True(t, res) + assert.Len(t, mb.Messages, 1) + assert.Contains(t, mb.Messages[0], "tester: 255 HP, 0 XP") +} + +func TestSimpleAppeared(t *testing.T) { + c, mb := setup(t) + c.timer.Reset(1 * time.Nanosecond) + time.Sleep(1 * time.Millisecond) + assert.Len(t, mb.Messages, 1) + assert.Contains(t, mb.Messages[0], "A wild") +} + +func TestAddCreature(t *testing.T) { + c, mb := setup(t) + res := c.message(makeMessage("!add_creature NewCreature 0 0")) + assert.True(t, res) + assert.Len(t, mb.Messages, 1) + assert.Contains(t, mb.Messages[0], "Added NewCreature") +} + +func TestAddAbility(t *testing.T) { + c, mb := setup(t) + res := c.message(makeMessage("!add_ability NewAbility 0 0 0 0 0")) + assert.True(t, res) + assert.Len(t, mb.Messages, 1) + assert.Contains(t, mb.Messages[0], "Added NewAbility") +}