mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user