package main import ( "context" "fmt" "strings" "sync" "time" "github.com/hako/durafmt" ) type achievement uint8 const ( _ achievement = iota achievementBestFeeder achievementVeryLongOnline achievementDeathless achievementPeaceable achievementMaxFrags achievementMaxLevel ) func getEmojiByAchievement(ach achievement) string { switch ach { case achievementBestFeeder: return "\xF0\x9F\x91\xBB" case achievementVeryLongOnline: return "\xF0\x9F\xA4\xAA" case achievementDeathless: return "\xF0\x9F\x98\x8E" case achievementPeaceable: return "\xF0\x9F\x98\x87" case achievementMaxFrags: return "\xF0\x9F\x98\x88" case achievementMaxLevel: return "\xF0\x9F\xA7\x99\xE2\x80\x8D\xE2\x99\x82\xEF\xB8\x8F" default: return "" } } var allAchievement = []achievement{ achievementBestFeeder, achievementVeryLongOnline, achievementDeathless, achievementPeaceable, achievementMaxFrags, achievementMaxLevel, } type playerInfo struct { playerID string isOnline bool achievements []achievement messageText string } func (p *playerInfo) setOnline(v bool) *playerInfo { p.isOnline = v return p } const ( emojiUp = "\xE2\xAC\x86" emojiDeaths = "\xF0\x9F\x92\x80" emojiTime = "\xF0\x9F\x95\x90" emojiGun = "\xF0\x9F\x94\xAB" emojiCheck = "\xE2\x9C\x85" ) func (p *playerInfo) updatePlayerInfo(ctx context.Context) error { db := getDB(ctx) lines := make([]string, 0, 10) player, err := db.getPlayerByID(ctx, p.playerID) if err != nil { return err } kills, err := db.getKillsByPlayerID(ctx, player.id) if err != nil { return err } access := "\xF0\x9F\x94\xB4 *offLine*" if p.isOnline { access = "\xF0\x9F\x9F\xA2 *onLine*" } level := "-" if player.level >= 0 { level = fmt.Sprintf("%d", player.level) } d := durafmt.Parse(player.onlineDuration).LimitFirstN(3) units, err := durafmt.DefaultUnitsCoder.Decode("мес.:мес.,нед.:нед.,дн.:дн.,чс.:чс.,мин.:мин.,cек.:cек.,-:-,-:-") if err != nil { panic(err) } lines = append(lines, fmt.Sprintf("*%s* | %s", player.name, access)) lines = append(lines, fmt.Sprintf("%s Уровень: *%s*", emojiUp, level)) lines = append(lines, fmt.Sprintf("%s Смертей: *%d*", emojiDeaths, player.deaths)) lines = append(lines, fmt.Sprintf("%s Фрагов: *%d*", emojiGun, kills)) lines = append(lines, fmt.Sprintf("%s Время в игре: *%s*", emojiTime, d.Format(units))) if !player.lastLogout.IsZero() && !p.isOnline { lines = append(lines, fmt.Sprintf("%s Был в сети: *%s*", emojiCheck, player.lastLogout.Format("02 Jan 15:04"))) } if len(p.achievements) > 0 { lines = append(lines, "-----") emojiAch := make([]string, 0, len(p.achievements)) for _, a := range p.achievements { emojiAch = append(emojiAch, getEmojiByAchievement(a)) } lines = append(lines, strings.Join(emojiAch, " ")) } text := strings.Join(lines, "\n") if text != p.messageText { messStorage := getMessageStorage(ctx) err := messStorage.apply(ctx, fmt.Sprintf("playerBoard:%s", p.playerID), 0, func(interface{}) (string, interface{}) { return text, nil }) if err != nil { return err } p.messageText = text } return nil } func (p *playerInfo) setAchievement(ach achievement) { for _, a := range p.achievements { if a == ach { return } } p.achievements = append(p.achievements, ach) } func (p *playerInfo) unsetAchievement(ach achievement) { for i, a := range p.achievements { if a == ach { p.achievements = append(p.achievements[:i], p.achievements[i+1:]...) } } } type playersBoard struct { mx sync.Mutex players []*playerInfo } func newPlayersBoard() *playersBoard { return &playersBoard{ players: make([]*playerInfo, 0), } } func (pb *playersBoard) load(ctx context.Context) error { pb.mx.Lock() defer pb.mx.Unlock() db := getDB(ctx) players, err := db.getPlayers(ctx) if err != nil { return err } pb.players = make([]*playerInfo, 0, len(players)) for _, p := range players { pi := &playerInfo{ playerID: p.id, } pb.players = append(pb.players, pi) } for _, a := range allAchievement { err := pb.updateAchievements(ctx, a) if err != nil { return err } } for _, p := range pb.players { err := p.updatePlayerInfo(ctx) if err != nil { return err } } return nil } func (pb *playersBoard) getPlayerBoard(playerID string) *playerInfo { for i := range pb.players { if pb.players[i].playerID == playerID { return pb.players[i] } } pb.mx.Lock() defer pb.mx.Unlock() pi := &playerInfo{ playerID: playerID, } pb.players = append(pb.players, pi) return pi } func (pb *playersBoard) updateAchievements(ctx context.Context, ach achievement) error { db := getDB(ctx) var value int playerIDs := make([]string, 0, len(pb.players)) setValue := func(v int, playerID string) { if value < v { value = v playerIDs = []string{playerID} } else if value == v { playerIDs = append(playerIDs, playerID) } } for _, p := range pb.players { player, err := db.getPlayerByID(ctx, p.playerID) if err != nil { return err } switch ach { case achievementBestFeeder: setValue(player.deaths, p.playerID) case achievementVeryLongOnline: setValue(int(player.onlineDuration/time.Second), p.playerID) case achievementDeathless: if player.deaths == 0 { playerIDs = append(playerIDs, p.playerID) } case achievementPeaceable: kills, err := db.getKillsByPlayerID(ctx, p.playerID) if err != nil { return err } setValue(int(^uint(0)>>1)-kills, p.playerID) case achievementMaxFrags: kills, err := db.getKillsByPlayerID(ctx, p.playerID) if err != nil { return err } setValue(kills, p.playerID) case achievementMaxLevel: setValue(player.level, p.playerID) } } mainLoop: for _, p := range pb.players { for _, pid := range playerIDs { if p.playerID == pid { p.setAchievement(ach) continue mainLoop } } p.unsetAchievement(ach) } return nil } func (pb *playersBoard) update(ctx context.Context, as ...achievement) error { pb.mx.Lock() defer pb.mx.Unlock() for _, a := range as { err := pb.updateAchievements(ctx, a) if err != nil { return err } } for _, p := range pb.players { err := p.updatePlayerInfo(ctx) if err != nil { return err } } return nil }