From 7e4fbe5ec836f8b24f0ace29b5f805c9e5a4fc01 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:24:49 -0700 Subject: [PATCH] Add action economy split and cross-plugin chat level lookup (Adventure 2.5 steps 1-2) Action economy: replace single daily action with 1 combat + 3 harvest actions per day. Holidays grant +1 to each bucket. Rest consumes all remaining actions. Arena remains outside both buckets. - Add CombatActionsUsed/HarvestActionsUsed counters to AdventureCharacter - Add CanDoCombat/CanDoHarvest/AllActionsUsed/HasActedToday helpers - Update all 14 ActionTakenToday check sites across adventure, scheduler, babysit, twinbee, events, and render - Morning DM shows action budget and grays out exhausted categories - Activity resolution checks per-bucket availability before proceeding - Midnight reset zeros both counters - Post-action nudge shows remaining actions instead of holiday prompt Cross-plugin lookup: export XPPlugin.GetLevel(), wire into AdventurePlugin via p.chatLevel(userID) for upcoming chat level perks. Co-Authored-By: Claude Opus 4.6 --- internal/db/db.go | 2 + internal/plugin/adventure.go | 154 ++++++++++++------------- internal/plugin/adventure_babysit.go | 3 +- internal/plugin/adventure_character.go | 53 ++++++++- internal/plugin/adventure_events.go | 2 +- internal/plugin/adventure_render.go | 92 ++++++++++----- internal/plugin/adventure_scheduler.go | 9 +- internal/plugin/adventure_twinbee.go | 2 +- internal/plugin/xp.go | 10 ++ main.go | 2 +- 10 files changed, 203 insertions(+), 126 deletions(-) diff --git a/internal/db/db.go b/internal/db/db.go index 6598154..63b2e9e 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -83,6 +83,8 @@ func runMigrations(d *sql.DB) error { `ALTER TABLE adventure_characters ADD COLUMN robbie_visit_count INTEGER NOT NULL DEFAULT 0`, `ALTER TABLE wordle_stats ADD COLUMN total_earned INTEGER NOT NULL DEFAULT 0`, `ALTER TABLE adventure_characters ADD COLUMN last_death_date TEXT NOT NULL DEFAULT ''`, + `ALTER TABLE adventure_characters ADD COLUMN combat_actions_used INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE adventure_characters ADD COLUMN harvest_actions_used INTEGER NOT NULL DEFAULT 0`, } for _, stmt := range columnMigrations { if _, err := d.Exec(stmt); err != nil { diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index cf2c454..0336177 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -18,6 +18,7 @@ import ( type AdventurePlugin struct { Base euro *EuroPlugin + xp *XPPlugin achievements *AchievementsPlugin mu sync.Mutex dmToPlayer map[id.RoomID]id.UserID @@ -67,16 +68,25 @@ type advPendingTreasureDiscard struct { Existing []AdvTreasureDef } -func NewAdventurePlugin(client *mautrix.Client, euro *EuroPlugin) *AdventurePlugin { +func NewAdventurePlugin(client *mautrix.Client, euro *EuroPlugin, xp *XPPlugin) *AdventurePlugin { return &AdventurePlugin{ Base: NewBase(client), euro: euro, + xp: xp, dmToPlayer: make(map[id.RoomID]id.UserID), morningHour: envInt("ADVENTURE_MORNING_HOUR", 8), summaryHour: envInt("ADVENTURE_SUMMARY_HOUR", 20), } } +// chatLevel returns the user's chat level for perk calculations. +func (p *AdventurePlugin) chatLevel(userID id.UserID) int { + if p.xp == nil { + return 0 + } + return p.xp.GetLevel(userID) +} + func (p *AdventurePlugin) Name() string { return "adventure" } // SetAchievements wires the achievements plugin after both are initialized. @@ -285,25 +295,15 @@ func (p *AdventurePlugin) handleMenu(ctx MessageContext) error { } } - if char.ActionTakenToday { - // On holidays, allow second action if not yet taken - isHol, _ := isHolidayToday() - if isHol && !char.HolidayActionTaken { - treasures, _ := loadAdvTreasureBonuses(char.UserID) - buffs, _ := loadAdvActiveBuffs(char.UserID) - bonuses := computeAdvBonuses(treasures, buffs, char.CurrentStreak, false) - text := renderAdvHolidaySecondPrompt(char, equip, bonuses) - p.advMarkMenuSent(ctx.Sender) - return p.SendDM(ctx.Sender, text) - } - + isHol, holName := isHolidayToday() + if char.AllActionsUsed(isHol) { now := time.Now().UTC() midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) remaining := midnight.Sub(now) hours := int(remaining.Hours()) minutes := int(remaining.Minutes()) % 60 return p.SendDM(ctx.Sender, fmt.Sprintf( - "You've already taken your action today. Tomorrow awaits. Try to survive it.\n\n"+ + "You've used all your actions today. Tomorrow awaits. Try to survive it.\n\n"+ "Next action: 00:00 UTC (%dh %dm from now)\n"+ "Morning DM: %02d:00 UTC\n\n"+ "The Arena is always open: `!arena`", @@ -315,7 +315,6 @@ func (p *AdventurePlugin) handleMenu(ctx MessageContext) error { bonuses := computeAdvBonuses(treasures, buffs, char.CurrentStreak, false) balance := p.euro.GetBalance(char.UserID) - _, holName := isHolidayToday() text := renderAdvMorningDM(char, equip, balance, bonuses, holName) p.advMarkMenuSent(ctx.Sender) return p.SendDM(ctx.Sender, text) @@ -613,30 +612,26 @@ func (p *AdventurePlugin) parseAndResolveChoice(ctx MessageContext, body string) } } - if char.ActionTakenToday { - // On holidays, allow second action if not yet taken - isHol, _ := isHolidayToday() - if !isHol || char.HolidayActionTaken { - // Only send the reminder once per day — subsequent DM messages - // are silently ignored so they can be handled by other plugins (e.g. UNO). - today := time.Now().UTC().Format("2006-01-02") - if prev, ok := p.dmRemindedDate.Load(string(ctx.Sender)); ok && prev.(string) == today { - return nil - } - p.dmRemindedDate.Store(string(ctx.Sender), today) - - now := time.Now().UTC() - midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) - remaining := midnight.Sub(now) - hours := int(remaining.Hours()) - minutes := int(remaining.Minutes()) % 60 - return p.SendDM(ctx.Sender, fmt.Sprintf( - "You've already taken your action today. Rest now. Try again tomorrow.\n\n"+ - "Next action: 00:00 UTC (%dh %dm from now)\n\n"+ - "The Arena is always open: `!arena`", - hours, minutes)) + isHol, _ := isHolidayToday() + if char.AllActionsUsed(isHol) { + // Only send the reminder once per day — subsequent DM messages + // are silently ignored so they can be handled by other plugins (e.g. UNO). + today := time.Now().UTC().Format("2006-01-02") + if prev, ok := p.dmRemindedDate.Load(string(ctx.Sender)); ok && prev.(string) == today { + return nil } - // Fall through for holiday second action + p.dmRemindedDate.Store(string(ctx.Sender), today) + + now := time.Now().UTC() + midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) + remaining := midnight.Sub(now) + hours := int(remaining.Hours()) + minutes := int(remaining.Minutes()) % 60 + return p.SendDM(ctx.Sender, fmt.Sprintf( + "You've used all your actions today. Rest now. Try again tomorrow.\n\n"+ + "Next action: 00:00 UTC (%dh %dm from now)\n\n"+ + "The Arena is always open: `!arena`", + hours, minutes)) } lower := strings.ToLower(body) @@ -728,6 +723,14 @@ func (p *AdventurePlugin) parseActivityLocation(input string, char *AdventureCha // ── Activity Resolution ────────────────────────────────────────────────────── func (p *AdventurePlugin) resolveActivity(ctx MessageContext, char *AdventureCharacter, activity AdvActivityType, loc *AdvLocation) error { + isHol, _ := isHolidayToday() + if isCombatActivity(activity) && !char.CanDoCombat(isHol) { + return p.SendDM(ctx.Sender, "You've used your combat action for the day. Try a harvest activity (mining, fishing, foraging) or rest.") + } + if isHarvestActivity(activity) && !char.CanDoHarvest(isHol) { + return p.SendDM(ctx.Sender, "You've used all your harvest actions for the day. Try combat (dungeon) or rest.") + } + equip, err := loadAdvEquipment(char.UserID) if err != nil { return p.SendDM(ctx.Sender, "Failed to load your equipment.") @@ -818,22 +821,14 @@ func (p *AdventurePlugin) resolveActivity(ctx MessageContext, char *AdventureCha _ = addAdvInventoryItem(char.UserID, item) } - // Determine if this is the holiday second action - isAction2 := char.ActionTakenToday // already taken = this is the second - isHol, _ := isHolidayToday() - - // Mark action taken and record the date for streak tracking - if !isAction2 { - char.ActionTakenToday = true - char.LastActionDate = time.Now().UTC().Format("2006-01-02") - } - - // Holiday flags: mark second action done, or mark it done on death during action 1 - if isAction2 { - char.HolidayActionTaken = true - } else if isHol && result.Outcome == AdvOutcomeDeath && !deathReprieved { - char.HolidayActionTaken = true // died on action 1 — no second action + // Mark action consumed in the correct bucket + if isCombatActivity(activity) { + char.CombatActionsUsed++ + } else if isHarvestActivity(activity) { + char.HarvestActionsUsed++ } + char.ActionTakenToday = true + char.LastActionDate = time.Now().UTC().Format("2006-01-02") // Update streak info result.StreakBonus = char.CurrentStreak @@ -897,34 +892,39 @@ func (p *AdventurePlugin) resolveActivity(ctx MessageContext, char *AdventureCha p.checkMasterworkDrop(ctx.Sender, char, equip, loc, result.Outcome) } - // TODO: holiday achievement hooks - - // Holiday: offer second action if this was action 1 and player survived - if !isAction2 && isHol && (result.Outcome != AdvOutcomeDeath || deathReprieved) { - equip2, _ := loadAdvEquipment(char.UserID) - treasures2, _ := loadAdvTreasureBonuses(char.UserID) - buffs2, _ := loadAdvActiveBuffs(char.UserID) - bonuses2 := computeAdvBonuses(treasures2, buffs2, char.CurrentStreak, false) - prompt := renderAdvHolidaySecondPrompt(char, equip2, bonuses2) - if err := p.SendDM(ctx.Sender, prompt); err != nil { - slog.Error("adventure: failed to send holiday second prompt", "user", ctx.Sender, "err", err) + // If the player still has actions remaining, nudge them + if !char.AllActionsUsed(isHol) && (result.Outcome != AdvOutcomeDeath || deathReprieved) { + remaining := []string{} + if char.CanDoCombat(isHol) { + remaining = append(remaining, "combat") } + if char.CanDoHarvest(isHol) { + harvestLeft := maxHarvestActions - char.HarvestActionsUsed + if isHol { + harvestLeft = maxHarvestActions + 1 - char.HarvestActionsUsed + } + remaining = append(remaining, fmt.Sprintf("%d harvest", harvestLeft)) + } + p.SendDM(ctx.Sender, fmt.Sprintf("Actions remaining today: %s. Type `!adv` for the menu.", strings.Join(remaining, ", "))) } return nil } func (p *AdventurePlugin) resolveRest(ctx MessageContext, char *AdventureCharacter) error { - isAction2 := char.ActionTakenToday isHol, _ := isHolidayToday() - if !isAction2 { - char.ActionTakenToday = true - char.LastActionDate = time.Now().UTC().Format("2006-01-02") - } - if isAction2 { - char.HolidayActionTaken = true + // Rest consumes all remaining actions + combatMax := maxCombatActions + harvestMax := maxHarvestActions + if isHol { + combatMax++ + harvestMax++ } + char.CombatActionsUsed = combatMax + char.HarvestActionsUsed = harvestMax + char.ActionTakenToday = true + char.LastActionDate = time.Now().UTC().Format("2006-01-02") if err := saveAdvCharacter(char); err != nil { return p.SendDM(ctx.Sender, "Failed to save. Even resting is broken.") @@ -954,18 +954,6 @@ func (p *AdventurePlugin) resolveRest(ctx MessageContext, char *AdventureCharact slog.Error("adventure: failed to send rest DM", "user", ctx.Sender, "err", err) } - // Holiday: offer second action if this was action 1 - if !isAction2 && isHol { - equip, _ := loadAdvEquipment(char.UserID) - treasures, _ := loadAdvTreasureBonuses(char.UserID) - buffs, _ := loadAdvActiveBuffs(char.UserID) - bonuses := computeAdvBonuses(treasures, buffs, char.CurrentStreak, false) - prompt := renderAdvHolidaySecondPrompt(char, equip, bonuses) - if err := p.SendDM(ctx.Sender, prompt); err != nil { - slog.Error("adventure: failed to send holiday second prompt", "user", ctx.Sender, "err", err) - } - } - return nil } diff --git a/internal/plugin/adventure_babysit.go b/internal/plugin/adventure_babysit.go index 862196d..4a42e80 100644 --- a/internal/plugin/adventure_babysit.go +++ b/internal/plugin/adventure_babysit.go @@ -276,7 +276,8 @@ func (p *AdventurePlugin) runBabysitDaily(char *AdventureCharacter) { // No treasure drops during babysitting result.TreasureFound = nil - // Mark action taken + // Mark action taken (babysit always uses a harvest action) + char.HarvestActionsUsed++ char.ActionTakenToday = true char.LastActionDate = time.Now().UTC().Format("2006-01-02") diff --git a/internal/plugin/adventure_character.go b/internal/plugin/adventure_character.go index ef6ac12..b368a87 100644 --- a/internal/plugin/adventure_character.go +++ b/internal/plugin/adventure_character.go @@ -39,6 +39,8 @@ type AdventureCharacter struct { DeadUntil *time.Time ActionTakenToday bool HolidayActionTaken bool + CombatActionsUsed int + HarvestActionsUsed int ArenaWins int // v2 ArenaLosses int // v2 InvasionScore int // v2 @@ -211,6 +213,43 @@ func (c *AdventureCharacter) Kill() { c.LastDeathDate = time.Now().UTC().Format("2006-01-02") } +// ── Action Economy ────────────────────────────────────────────────────────── + +const maxCombatActions = 1 +const maxHarvestActions = 3 + +func (c *AdventureCharacter) CanDoCombat(isHoliday bool) bool { + max := maxCombatActions + if isHoliday { + max++ + } + return c.CombatActionsUsed < max +} + +func (c *AdventureCharacter) CanDoHarvest(isHoliday bool) bool { + max := maxHarvestActions + if isHoliday { + max++ + } + return c.HarvestActionsUsed < max +} + +func (c *AdventureCharacter) HasActedToday() bool { + return c.CombatActionsUsed > 0 || c.HarvestActionsUsed > 0 +} + +func (c *AdventureCharacter) AllActionsUsed(isHoliday bool) bool { + return !c.CanDoCombat(isHoliday) && !c.CanDoHarvest(isHoliday) +} + +func isCombatActivity(activity AdvActivityType) bool { + return activity == AdvActivityDungeon +} + +func isHarvestActivity(activity AdvActivityType) bool { + return activity == AdvActivityMining || activity == AdvActivityForaging || activity == AdvActivityFishing +} + // ── Equipment Score ────────────────────────────────────────────────────────── func advEquipmentScore(equip map[EquipmentSlot]*AdvEquipment) float64 { @@ -320,7 +359,8 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) { masterwork_drops_received, rival_pool, rival_unlocked_notified, babysit_active, babysit_expires_at, babysit_skill_focus, - hospital_visits, robbie_visit_count, last_death_date + hospital_visits, robbie_visit_count, last_death_date, + combat_actions_used, harvest_actions_used FROM adventure_characters WHERE user_id = ?`, string(userID)).Scan( &c.UserID, &c.DisplayName, &c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill, @@ -333,6 +373,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) { &c.RivalPool, &rivalUnlocked, &babysitAct, &babysitExp, &c.BabysitSkillFocus, &c.HospitalVisits, &c.RobbieVisitCount, &c.LastDeathDate, + &c.CombatActionsUsed, &c.HarvestActionsUsed, ) if err != nil { return nil, err @@ -417,7 +458,8 @@ func saveAdvCharacter(char *AdventureCharacter) error { masterwork_drops_received = ?, rival_pool = ?, rival_unlocked_notified = ?, babysit_active = ?, babysit_expires_at = ?, babysit_skill_focus = ?, - hospital_visits = ?, robbie_visit_count = ?, last_death_date = ? + hospital_visits = ?, robbie_visit_count = ?, last_death_date = ?, + combat_actions_used = ?, harvest_actions_used = ? WHERE user_id = ?`, char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill, char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP, @@ -428,6 +470,7 @@ func saveAdvCharacter(char *AdventureCharacter) error { char.RivalPool, rivalUnlocked, babysitAct, char.BabysitExpiresAt, char.BabysitSkillFocus, char.HospitalVisits, char.RobbieVisitCount, char.LastDeathDate, + char.CombatActionsUsed, char.HarvestActionsUsed, string(char.UserID), ) return err @@ -544,7 +587,8 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) { created_at, last_active_at, death_reprieve_last, masterwork_drops_received, rival_pool, rival_unlocked_notified, - babysit_active, babysit_expires_at, babysit_skill_focus + babysit_active, babysit_expires_at, babysit_skill_focus, + combat_actions_used, harvest_actions_used FROM adventure_characters`) if err != nil { return nil, err @@ -567,6 +611,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) { &c.MasterworkDropsReceived, &c.RivalPool, &rivalUnlocked, &babysitAct, &babysitExp, &c.BabysitSkillFocus, + &c.CombatActionsUsed, &c.HarvestActionsUsed, ); err != nil { return nil, err } @@ -594,7 +639,7 @@ func resetAllAdvDailyActions() error { // Only reset actions taken before today — protects against race if a player // resolves their action at exactly midnight. today := time.Now().UTC().Format("2006-01-02") - _, err := d.Exec(`UPDATE adventure_characters SET action_taken_today = 0, holiday_action_taken = 0 WHERE last_action_date < ? OR last_action_date IS NULL`, today) + _, err := d.Exec(`UPDATE adventure_characters SET action_taken_today = 0, holiday_action_taken = 0, combat_actions_used = 0, harvest_actions_used = 0 WHERE last_action_date < ? OR last_action_date IS NULL`, today) return err } diff --git a/internal/plugin/adventure_events.go b/internal/plugin/adventure_events.go index c7a9e9b..48e9c12 100644 --- a/internal/plugin/adventure_events.go +++ b/internal/plugin/adventure_events.go @@ -95,7 +95,7 @@ func (p *AdventurePlugin) eventTicker() { func (p *AdventurePlugin) tryTriggerEvent(userID id.UserID) { // Load character — must be alive and have acted today char, err := loadAdvCharacter(userID) - if err != nil || char == nil || !char.Alive || !char.ActionTakenToday { + if err != nil || char == nil || !char.Alive || !char.HasActedToday() { return } diff --git a/internal/plugin/adventure_render.go b/internal/plugin/adventure_render.go index e2c703c..1f4a559 100644 --- a/internal/plugin/adventure_render.go +++ b/internal/plugin/adventure_render.go @@ -201,11 +201,19 @@ func renderAdvCharacterSheet(char *AdventureCharacter, equip map[EquipmentSlot]* sb.WriteString(" — `!adventure rivals` for details\n") } - // Today's action - if char.ActionTakenToday { - sb.WriteString("\n📅 Today: Action taken") + // Today's actions + combatRemaining := maxCombatActions - char.CombatActionsUsed + if combatRemaining < 0 { + combatRemaining = 0 + } + harvestRemaining := maxHarvestActions - char.HarvestActionsUsed + if harvestRemaining < 0 { + harvestRemaining = 0 + } + if char.HasActedToday() { + sb.WriteString(fmt.Sprintf("\n📅 Today: %d combat, %d harvest actions remaining", combatRemaining, harvestRemaining)) } else { - sb.WriteString("\n📅 Today: No action yet — reply to morning DM or type `!adventure`") + sb.WriteString(fmt.Sprintf("\n📅 Today: %d combat, %d harvest actions available", combatRemaining, harvestRemaining)) } return sb.String() @@ -218,7 +226,7 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq // Holiday notice (before greeting) if holidayName != "" { - sb.WriteString(fmt.Sprintf("🎉 Happy %s! In recognition of %s, you are able to take **two actions** today.\n\n", holidayName, holidayName)) + sb.WriteString(fmt.Sprintf("🎉 Happy %s! In recognition of %s, you get an extra combat and harvest action today.\n\n", holidayName, holidayName)) } // Pick a morning greeting @@ -259,41 +267,63 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq sb.WriteString("\n\n") } + // Action budget + isHol := holidayName != "" + combatMax := maxCombatActions + harvestMax := maxHarvestActions + if isHol { + combatMax++ + harvestMax++ + } + combatLeft := combatMax - char.CombatActionsUsed + harvestLeft := harvestMax - char.HarvestActionsUsed + sb.WriteString(fmt.Sprintf("📋 **Actions:** %d/%d combat · %d/%d harvest\n\n", combatLeft, combatMax, harvestLeft, harvestMax)) + // Location choices - sb.WriteString("**1️⃣ Dungeon:**\n") - for _, el := range advEligibleLocations(char, equip, AdvActivityDungeon, bonuses) { - warn := "" - if el.InPenaltyZone { - warn = " ⚠️" + if char.CanDoCombat(isHol) { + sb.WriteString("**1️⃣ Dungeon:**\n") + for _, el := range advEligibleLocations(char, equip, AdvActivityDungeon, bonuses) { + warn := "" + if el.InPenaltyZone { + warn = " ⚠️" + } + sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) } - sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) + } else { + sb.WriteString("**1️⃣ Dungeon:** _(no combat actions remaining)_\n") } - sb.WriteString("**2️⃣ Mine:**\n") - for _, el := range advEligibleLocations(char, equip, AdvActivityMining, bonuses) { - warn := "" - if el.InPenaltyZone { - warn = " ⚠️" + if char.CanDoHarvest(isHol) { + sb.WriteString("**2️⃣ Mine:**\n") + for _, el := range advEligibleLocations(char, equip, AdvActivityMining, bonuses) { + warn := "" + if el.InPenaltyZone { + warn = " ⚠️" + } + sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) } - sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) - } - sb.WriteString("**3️⃣ Forage:**\n") - for _, el := range advEligibleLocations(char, equip, AdvActivityForaging, bonuses) { - warn := "" - if el.InPenaltyZone { - warn = " ⚠️" + sb.WriteString("**3️⃣ Forage:**\n") + for _, el := range advEligibleLocations(char, equip, AdvActivityForaging, bonuses) { + warn := "" + if el.InPenaltyZone { + warn = " ⚠️" + } + sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) } - sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) - } - sb.WriteString("**4️⃣ Fish:**\n") - for _, el := range advEligibleLocations(char, equip, AdvActivityFishing, bonuses) { - warn := "" - if el.InPenaltyZone { - warn = " ⚠️" + sb.WriteString("**4️⃣ Fish:**\n") + for _, el := range advEligibleLocations(char, equip, AdvActivityFishing, bonuses) { + warn := "" + if el.InPenaltyZone { + warn = " ⚠️" + } + sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) } - sb.WriteString(fmt.Sprintf(" • %s (Tier %d, ~%.0f%% death%s)\n", el.Location.Name, el.Location.Tier, el.DeathPct, warn)) + } else { + sb.WriteString("**2️⃣ Mine:** _(no harvest actions remaining)_\n") + sb.WriteString("**3️⃣ Forage:** _(no harvest actions remaining)_\n") + sb.WriteString("**4️⃣ Fish:** _(no harvest actions remaining)_\n") } sb.WriteString("**5️⃣ Shop** — buy/sell gear and loot\n") diff --git a/internal/plugin/adventure_scheduler.go b/internal/plugin/adventure_scheduler.go index 09f0d40..f86e7de 100644 --- a/internal/plugin/adventure_scheduler.go +++ b/internal/plugin/adventure_scheduler.go @@ -96,8 +96,9 @@ func (p *AdventurePlugin) sendMorningDMs() { continue } - // If already acted today, skip - if char.ActionTakenToday { + // If all actions used today, skip + isHol, _ := isHolidayToday() + if char.AllActionsUsed(isHol) { continue } @@ -225,7 +226,7 @@ func (p *AdventurePlugin) postDailySummary() { continue } - if !c.ActionTakenToday { + if !c.HasActedToday() { ps.IsResting = true if len(SummaryResting) > 0 { ps.SummaryLine = SummaryResting[time.Now().Nanosecond()%len(SummaryResting)] @@ -313,7 +314,7 @@ func (p *AdventurePlugin) midnightReset() error { continue } - if !char.ActionTakenToday { + if !char.HasActedToday() { // If the player died today (or yesterday — covering late-night deaths // that span midnight), grant a grace period: no shame, no streak reset. if char.LastDeathDate == today || diff --git a/internal/plugin/adventure_twinbee.go b/internal/plugin/adventure_twinbee.go index 909a137..2a2a6e8 100644 --- a/internal/plugin/adventure_twinbee.go +++ b/internal/plugin/adventure_twinbee.go @@ -236,7 +236,7 @@ func (p *AdventurePlugin) distributeTwinBeeRewards(result *TwinBeeResult) TwinBe var eligible []id.UserID for _, c := range chars { - if c.ActionTakenToday { + if c.HasActedToday() { eligible = append(eligible, c.UserID) } } diff --git a/internal/plugin/xp.go b/internal/plugin/xp.go index d42f966..68ace92 100644 --- a/internal/plugin/xp.go +++ b/internal/plugin/xp.go @@ -106,6 +106,16 @@ func (p *XPPlugin) OnMessage(ctx MessageContext) error { } // GrantXP awards XP to a user from an external plugin and returns (newXP, leveledUp, newLevel). +// GetLevel returns the chat level for the given user, or 0 if unknown. +func (p *XPPlugin) GetLevel(userID id.UserID) int { + d := db.Get() + var level int + if err := d.QueryRow(`SELECT level FROM users WHERE user_id = ?`, string(userID)).Scan(&level); err != nil { + return 0 + } + return level +} + func (p *XPPlugin) GrantXP(userID id.UserID, amount int, reason string) (int, bool, int) { return p.grantXPInternal(userID, amount, reason) } diff --git a/main.go b/main.go index f7603b7..c5add88 100644 --- a/main.go +++ b/main.go @@ -132,7 +132,7 @@ func main() { registry.Register(plugin.NewBlackjackPlugin(client, euroPlugin)) registry.Register(plugin.NewUnoPlugin(client, euroPlugin)) registry.Register(plugin.NewHoldemPlugin(client, euroPlugin)) - adventurePlugin := plugin.NewAdventurePlugin(client, euroPlugin) + adventurePlugin := plugin.NewAdventurePlugin(client, euroPlugin, xpPlugin) registry.Register(adventurePlugin) wordlePlugin := plugin.NewWordlePlugin(client, euroPlugin, dictClient) registry.Register(wordlePlugin)