fix: compute work hours from events at runtime; add Achievements button to RPG view
This commit is contained in:
@@ -73,7 +73,8 @@ func (h *Handler) clockOut(chatID int64) (string, error) {
|
|||||||
streak, _ := h.updateStreak(user.ID, today, true)
|
streak, _ := h.updateStreak(user.ID, today, true)
|
||||||
xp, leveledUp, newLevel, _ := h.computeAndAwardXP(user.ID, sessionWork, streak, today)
|
xp, leveledUp, newLevel, _ := h.computeAndAwardXP(user.ID, sessionWork, streak, today)
|
||||||
stats, _ := h.DB.GetOrCreateRPGStats(user.ID)
|
stats, _ := h.DB.GetOrCreateRPGStats(user.ID)
|
||||||
unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend)
|
totalHours := float64(h.totalWorkSeconds(user.ID, loc)) / secondsPerHour
|
||||||
|
unlocked := h.checkAchievements(user.ID, stats, sessionWork, today, isWeekend, totalHours)
|
||||||
|
|
||||||
text += fmt.Sprintf("\n\n+%d XP (Lv.%d)", xp, newLevel)
|
text += fmt.Sprintf("\n\n+%d XP (Lv.%d)", xp, newLevel)
|
||||||
if leveledUp {
|
if leveledUp {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func (h *Handler) HandleMessage(ctx context.Context, msg *models.Message) {
|
|||||||
case "/setbreak":
|
case "/setbreak":
|
||||||
h.handleSetBreakMsg(ctx, msg)
|
h.handleSetBreakMsg(ctx, msg)
|
||||||
case "/rpg":
|
case "/rpg":
|
||||||
h.handleActionMsg(ctx, msg, h.rpgStatus)
|
h.handleRPGCmd(ctx, msg)
|
||||||
case "/achievements":
|
case "/achievements":
|
||||||
h.handleActionMsg(ctx, msg, h.achievementsList)
|
h.handleActionMsg(ctx, msg, h.achievementsList)
|
||||||
case "/salary":
|
case "/salary":
|
||||||
@@ -298,6 +298,10 @@ func (h *Handler) HandleCallback(ctx context.Context, cb *models.CallbackQuery)
|
|||||||
h.leagueToggleCallback(ctx, chatID, msgID, cb.ID)
|
h.leagueToggleCallback(ctx, chatID, msgID, cb.ID)
|
||||||
case "reporttoggle":
|
case "reporttoggle":
|
||||||
h.reportToggleCallback(ctx, chatID, msgID, cb.ID)
|
h.reportToggleCallback(ctx, chatID, msgID, cb.ID)
|
||||||
|
case "rpg_achievements":
|
||||||
|
h.rpgAchievementsCallback(ctx, chatID, msgID, cb.ID)
|
||||||
|
case "rpg_back":
|
||||||
|
h.rpgBackCallback(ctx, chatID, msgID, cb.ID)
|
||||||
case "username":
|
case "username":
|
||||||
h.usernameCallback(ctx, chatID, msgID, cb.ID)
|
h.usernameCallback(ctx, chatID, msgID, cb.ID)
|
||||||
case "currency":
|
case "currency":
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
|
||||||
"worktimeBot/internal/db"
|
"worktimeBot/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,12 +124,12 @@ func (h *Handler) updateStreak(userID int64, date string, hasWorkToday bool) (in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkAchievements evaluates and unlocks any newly earned achievements.
|
// checkAchievements evaluates and unlocks any newly earned achievements.
|
||||||
func (h *Handler) checkAchievements(userID int64, stats *db.RPGStats, workSeconds int64, date string, isWeekend bool) []string {
|
func (h *Handler) checkAchievements(userID int64, stats *db.RPGStats, workSeconds int64, date string, isWeekend bool, totalHours float64) []string {
|
||||||
unlocker := newAchievementUnlocker(h, userID)
|
unlocker := newAchievementUnlocker(h, userID)
|
||||||
h.checkEarlyAchievements(unlocker, workSeconds)
|
h.checkEarlyAchievements(unlocker, workSeconds)
|
||||||
h.checkEventTimeAchievements(unlocker, userID, date)
|
h.checkEventTimeAchievements(unlocker, userID, date)
|
||||||
h.checkStreakAchievements(unlocker, stats)
|
h.checkStreakAchievements(unlocker, stats)
|
||||||
h.checkHourAchievements(unlocker, stats)
|
h.checkHourAchievements(unlocker, stats, totalHours)
|
||||||
h.checkSessionAchievements(unlocker, workSeconds, isWeekend)
|
h.checkSessionAchievements(unlocker, workSeconds, isWeekend)
|
||||||
h.checkLevelAchievements(unlocker, stats)
|
h.checkLevelAchievements(unlocker, stats)
|
||||||
return unlocker.unlocked
|
return unlocker.unlocked
|
||||||
@@ -201,8 +204,7 @@ func (h *Handler) checkStreakAchievements(u *achievementUnlocker, stats *db.RPGS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) checkHourAchievements(u *achievementUnlocker, stats *db.RPGStats) {
|
func (h *Handler) checkHourAchievements(u *achievementUnlocker, stats *db.RPGStats, hoursWorked float64) {
|
||||||
hoursWorked := float64(stats.TotalWorkSeconds) / secondsPerHour
|
|
||||||
if hoursWorked >= 100.0 {
|
if hoursWorked >= 100.0 {
|
||||||
u.unlock("hundred_hours")
|
u.unlock("hundred_hours")
|
||||||
}
|
}
|
||||||
@@ -235,8 +237,34 @@ func (h *Handler) checkLevelAchievements(u *achievementUnlocker, stats *db.RPGSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// totalWorkSeconds computes actual total work seconds from all events for the user.
|
||||||
|
// Calculated at runtime to stay in sync with event edits.
|
||||||
|
func (h *Handler) totalWorkSeconds(userID int64, loc *time.Location) int64 {
|
||||||
|
days, err := h.DB.GetUserDaysInRange(userID, "2000-01-01", "2100-01-01")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
for _, day := range days {
|
||||||
|
if day.IsDayOff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
events, err := h.DB.EventsForDayByDayID(day.ID)
|
||||||
|
if err != nil || len(events) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
y, m, d := parseGregorianDateKey(day.Date)
|
||||||
|
dayStart := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc).Unix()
|
||||||
|
dayEnd := time.Date(y, time.Month(m), d, 23, 59, 59, 0, loc).Unix()
|
||||||
|
totals := ComputeDailyTotals(events, day.MinBreakThreshold, dayStart, dayEnd)
|
||||||
|
total += totals.TotalSeconds
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
// buildRPGStatus returns a formatted string showing the user's RPG stats.
|
// buildRPGStatus returns a formatted string showing the user's RPG stats.
|
||||||
func (h *Handler) buildRPGStatus(userID int64) string {
|
func (h *Handler) buildRPGStatus(userID int64, loc *time.Location) string {
|
||||||
stats, err := h.DB.GetOrCreateRPGStats(userID)
|
stats, err := h.DB.GetOrCreateRPGStats(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "RPG stats unavailable"
|
return "RPG stats unavailable"
|
||||||
@@ -252,7 +280,7 @@ func (h *Handler) buildRPGStatus(userID int64) string {
|
|||||||
b.WriteString(fmt.Sprintf("XP: %d / %d (%d%%)\n", stats.XP, nextLevelXP, xpIntoLevel*100/xpNeeded))
|
b.WriteString(fmt.Sprintf("XP: %d / %d (%d%%)\n", stats.XP, nextLevelXP, xpIntoLevel*100/xpNeeded))
|
||||||
b.WriteString(fmt.Sprintf("Streak: %d days (best: %d)\n", stats.CurrentStreak, stats.LongestStreak))
|
b.WriteString(fmt.Sprintf("Streak: %d days (best: %d)\n", stats.CurrentStreak, stats.LongestStreak))
|
||||||
|
|
||||||
hoursTotal := float64(stats.TotalWorkSeconds) / secondsPerHour
|
hoursTotal := float64(h.totalWorkSeconds(userID, loc)) / secondsPerHour
|
||||||
b.WriteString(fmt.Sprintf("Total work: %.1f hours\n", hoursTotal))
|
b.WriteString(fmt.Sprintf("Total work: %.1f hours\n", hoursTotal))
|
||||||
|
|
||||||
// Show achievements count
|
// Show achievements count
|
||||||
@@ -296,7 +324,69 @@ func (h *Handler) rpgStatus(chatID int64) (string, error) {
|
|||||||
if !st.RPGEnabled {
|
if !st.RPGEnabled {
|
||||||
return "RPG system is disabled. Enable it in Settings or use /rpgtoggle.", nil
|
return "RPG system is disabled. Enable it in Settings or use /rpgtoggle.", nil
|
||||||
}
|
}
|
||||||
return h.buildRPGStatus(user.ID), nil
|
loc := loadLocation(user.Timezone)
|
||||||
|
return h.buildRPGStatus(user.ID, loc), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRPGCmd handles /rpg — shows stats with an Achievements button.
|
||||||
|
func (h *Handler) handleRPGCmd(ctx context.Context, msg *models.Message) {
|
||||||
|
user, err := h.getOrCreateUser(msg.Chat.ID)
|
||||||
|
if err != nil {
|
||||||
|
h.sendText(ctx, msg.Chat.ID, "Error loading profile")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st, err := h.DB.GetOrCreateSettings(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
h.sendText(ctx, msg.Chat.ID, "Error loading settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !st.RPGEnabled {
|
||||||
|
h.sendText(ctx, msg.Chat.ID, "RPG system is disabled. Enable it in Settings or use /rpgtoggle.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loc := loadLocation(user.Timezone)
|
||||||
|
text := h.buildRPGStatus(user.ID, loc)
|
||||||
|
kb := models.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||||
|
{{Text: "Achievements", CallbackData: "rpg_achievements"}},
|
||||||
|
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
h.sendWithKB(ctx, msg.Chat.ID, text, kb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpgAchievementsCallback shows achievements from the RPG view.
|
||||||
|
func (h *Handler) rpgAchievementsCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||||
|
defer h.answerCb(ctx, callbackID)
|
||||||
|
user, err := h.getOrCreateUser(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text := h.buildAchievementsList(user.ID)
|
||||||
|
kb := models.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||||
|
{{Text: "Back to RPG", CallbackData: "rpg_back"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
h.editText(ctx, chatID, msgID, text, &kb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpgBackCallback returns from achievements to the RPG stats view.
|
||||||
|
func (h *Handler) rpgBackCallback(ctx context.Context, chatID int64, msgID int, callbackID string) {
|
||||||
|
defer h.answerCb(ctx, callbackID)
|
||||||
|
user, err := h.getOrCreateUser(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loc := loadLocation(user.Timezone)
|
||||||
|
text := h.buildRPGStatus(user.ID, loc)
|
||||||
|
kb := models.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||||
|
{{Text: "Achievements", CallbackData: "rpg_achievements"}},
|
||||||
|
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
h.editText(ctx, chatID, msgID, text, &kb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// achievementsList returns the user's unlocked achievements.
|
// achievementsList returns the user's unlocked achievements.
|
||||||
|
|||||||
Reference in New Issue
Block a user