mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32: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 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 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 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 {
|
for _, stmt := range columnMigrations {
|
||||||
if _, err := d.Exec(stmt); err != nil {
|
if _, err := d.Exec(stmt); err != nil {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
type AdventurePlugin struct {
|
type AdventurePlugin struct {
|
||||||
Base
|
Base
|
||||||
euro *EuroPlugin
|
euro *EuroPlugin
|
||||||
|
xp *XPPlugin
|
||||||
achievements *AchievementsPlugin
|
achievements *AchievementsPlugin
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
dmToPlayer map[id.RoomID]id.UserID
|
dmToPlayer map[id.RoomID]id.UserID
|
||||||
@@ -67,16 +68,25 @@ type advPendingTreasureDiscard struct {
|
|||||||
Existing []AdvTreasureDef
|
Existing []AdvTreasureDef
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdventurePlugin(client *mautrix.Client, euro *EuroPlugin) *AdventurePlugin {
|
func NewAdventurePlugin(client *mautrix.Client, euro *EuroPlugin, xp *XPPlugin) *AdventurePlugin {
|
||||||
return &AdventurePlugin{
|
return &AdventurePlugin{
|
||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
euro: euro,
|
euro: euro,
|
||||||
|
xp: xp,
|
||||||
dmToPlayer: make(map[id.RoomID]id.UserID),
|
dmToPlayer: make(map[id.RoomID]id.UserID),
|
||||||
morningHour: envInt("ADVENTURE_MORNING_HOUR", 8),
|
morningHour: envInt("ADVENTURE_MORNING_HOUR", 8),
|
||||||
summaryHour: envInt("ADVENTURE_SUMMARY_HOUR", 20),
|
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" }
|
func (p *AdventurePlugin) Name() string { return "adventure" }
|
||||||
|
|
||||||
// SetAchievements wires the achievements plugin after both are initialized.
|
// SetAchievements wires the achievements plugin after both are initialized.
|
||||||
@@ -285,25 +295,15 @@ func (p *AdventurePlugin) handleMenu(ctx MessageContext) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if char.ActionTakenToday {
|
isHol, holName := isHolidayToday()
|
||||||
// On holidays, allow second action if not yet taken
|
if char.AllActionsUsed(isHol) {
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
|
midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC)
|
||||||
remaining := midnight.Sub(now)
|
remaining := midnight.Sub(now)
|
||||||
hours := int(remaining.Hours())
|
hours := int(remaining.Hours())
|
||||||
minutes := int(remaining.Minutes()) % 60
|
minutes := int(remaining.Minutes()) % 60
|
||||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
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"+
|
"Next action: 00:00 UTC (%dh %dm from now)\n"+
|
||||||
"Morning DM: %02d:00 UTC\n\n"+
|
"Morning DM: %02d:00 UTC\n\n"+
|
||||||
"The Arena is always open: `!arena`",
|
"The Arena is always open: `!arena`",
|
||||||
@@ -315,7 +315,6 @@ func (p *AdventurePlugin) handleMenu(ctx MessageContext) error {
|
|||||||
bonuses := computeAdvBonuses(treasures, buffs, char.CurrentStreak, false)
|
bonuses := computeAdvBonuses(treasures, buffs, char.CurrentStreak, false)
|
||||||
balance := p.euro.GetBalance(char.UserID)
|
balance := p.euro.GetBalance(char.UserID)
|
||||||
|
|
||||||
_, holName := isHolidayToday()
|
|
||||||
text := renderAdvMorningDM(char, equip, balance, bonuses, holName)
|
text := renderAdvMorningDM(char, equip, balance, bonuses, holName)
|
||||||
p.advMarkMenuSent(ctx.Sender)
|
p.advMarkMenuSent(ctx.Sender)
|
||||||
return p.SendDM(ctx.Sender, text)
|
return p.SendDM(ctx.Sender, text)
|
||||||
@@ -613,30 +612,26 @@ func (p *AdventurePlugin) parseAndResolveChoice(ctx MessageContext, body string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if char.ActionTakenToday {
|
isHol, _ := isHolidayToday()
|
||||||
// On holidays, allow second action if not yet taken
|
if char.AllActionsUsed(isHol) {
|
||||||
isHol, _ := isHolidayToday()
|
// Only send the reminder once per day — subsequent DM messages
|
||||||
if !isHol || char.HolidayActionTaken {
|
// are silently ignored so they can be handled by other plugins (e.g. UNO).
|
||||||
// Only send the reminder once per day — subsequent DM messages
|
today := time.Now().UTC().Format("2006-01-02")
|
||||||
// are silently ignored so they can be handled by other plugins (e.g. UNO).
|
if prev, ok := p.dmRemindedDate.Load(string(ctx.Sender)); ok && prev.(string) == today {
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
return nil
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
// 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)
|
lower := strings.ToLower(body)
|
||||||
@@ -728,6 +723,14 @@ func (p *AdventurePlugin) parseActivityLocation(input string, char *AdventureCha
|
|||||||
// ── Activity Resolution ──────────────────────────────────────────────────────
|
// ── Activity Resolution ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (p *AdventurePlugin) resolveActivity(ctx MessageContext, char *AdventureCharacter, activity AdvActivityType, loc *AdvLocation) error {
|
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)
|
equip, err := loadAdvEquipment(char.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender, "Failed to load your equipment.")
|
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)
|
_ = addAdvInventoryItem(char.UserID, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this is the holiday second action
|
// Mark action consumed in the correct bucket
|
||||||
isAction2 := char.ActionTakenToday // already taken = this is the second
|
if isCombatActivity(activity) {
|
||||||
isHol, _ := isHolidayToday()
|
char.CombatActionsUsed++
|
||||||
|
} else if isHarvestActivity(activity) {
|
||||||
// Mark action taken and record the date for streak tracking
|
char.HarvestActionsUsed++
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
char.ActionTakenToday = true
|
||||||
|
char.LastActionDate = time.Now().UTC().Format("2006-01-02")
|
||||||
|
|
||||||
// Update streak info
|
// Update streak info
|
||||||
result.StreakBonus = char.CurrentStreak
|
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)
|
p.checkMasterworkDrop(ctx.Sender, char, equip, loc, result.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: holiday achievement hooks
|
// If the player still has actions remaining, nudge them
|
||||||
|
if !char.AllActionsUsed(isHol) && (result.Outcome != AdvOutcomeDeath || deathReprieved) {
|
||||||
// Holiday: offer second action if this was action 1 and player survived
|
remaining := []string{}
|
||||||
if !isAction2 && isHol && (result.Outcome != AdvOutcomeDeath || deathReprieved) {
|
if char.CanDoCombat(isHol) {
|
||||||
equip2, _ := loadAdvEquipment(char.UserID)
|
remaining = append(remaining, "combat")
|
||||||
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 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) resolveRest(ctx MessageContext, char *AdventureCharacter) error {
|
func (p *AdventurePlugin) resolveRest(ctx MessageContext, char *AdventureCharacter) error {
|
||||||
isAction2 := char.ActionTakenToday
|
|
||||||
isHol, _ := isHolidayToday()
|
isHol, _ := isHolidayToday()
|
||||||
|
|
||||||
if !isAction2 {
|
// Rest consumes all remaining actions
|
||||||
char.ActionTakenToday = true
|
combatMax := maxCombatActions
|
||||||
char.LastActionDate = time.Now().UTC().Format("2006-01-02")
|
harvestMax := maxHarvestActions
|
||||||
}
|
if isHol {
|
||||||
if isAction2 {
|
combatMax++
|
||||||
char.HolidayActionTaken = true
|
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 {
|
if err := saveAdvCharacter(char); err != nil {
|
||||||
return p.SendDM(ctx.Sender, "Failed to save. Even resting is broken.")
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,8 @@ func (p *AdventurePlugin) runBabysitDaily(char *AdventureCharacter) {
|
|||||||
// No treasure drops during babysitting
|
// No treasure drops during babysitting
|
||||||
result.TreasureFound = nil
|
result.TreasureFound = nil
|
||||||
|
|
||||||
// Mark action taken
|
// Mark action taken (babysit always uses a harvest action)
|
||||||
|
char.HarvestActionsUsed++
|
||||||
char.ActionTakenToday = true
|
char.ActionTakenToday = true
|
||||||
char.LastActionDate = time.Now().UTC().Format("2006-01-02")
|
char.LastActionDate = time.Now().UTC().Format("2006-01-02")
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ type AdventureCharacter struct {
|
|||||||
DeadUntil *time.Time
|
DeadUntil *time.Time
|
||||||
ActionTakenToday bool
|
ActionTakenToday bool
|
||||||
HolidayActionTaken bool
|
HolidayActionTaken bool
|
||||||
|
CombatActionsUsed int
|
||||||
|
HarvestActionsUsed int
|
||||||
ArenaWins int // v2
|
ArenaWins int // v2
|
||||||
ArenaLosses int // v2
|
ArenaLosses int // v2
|
||||||
InvasionScore int // v2
|
InvasionScore int // v2
|
||||||
@@ -211,6 +213,43 @@ func (c *AdventureCharacter) Kill() {
|
|||||||
c.LastDeathDate = time.Now().UTC().Format("2006-01-02")
|
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 ──────────────────────────────────────────────────────────
|
// ── Equipment Score ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func advEquipmentScore(equip map[EquipmentSlot]*AdvEquipment) float64 {
|
func advEquipmentScore(equip map[EquipmentSlot]*AdvEquipment) float64 {
|
||||||
@@ -320,7 +359,8 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
|
|||||||
masterwork_drops_received,
|
masterwork_drops_received,
|
||||||
rival_pool, rival_unlocked_notified,
|
rival_pool, rival_unlocked_notified,
|
||||||
babysit_active, babysit_expires_at, babysit_skill_focus,
|
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(
|
FROM adventure_characters WHERE user_id = ?`, string(userID)).Scan(
|
||||||
&c.UserID, &c.DisplayName,
|
&c.UserID, &c.DisplayName,
|
||||||
&c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill,
|
&c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill,
|
||||||
@@ -333,6 +373,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
|
|||||||
&c.RivalPool, &rivalUnlocked,
|
&c.RivalPool, &rivalUnlocked,
|
||||||
&babysitAct, &babysitExp, &c.BabysitSkillFocus,
|
&babysitAct, &babysitExp, &c.BabysitSkillFocus,
|
||||||
&c.HospitalVisits, &c.RobbieVisitCount, &c.LastDeathDate,
|
&c.HospitalVisits, &c.RobbieVisitCount, &c.LastDeathDate,
|
||||||
|
&c.CombatActionsUsed, &c.HarvestActionsUsed,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -417,7 +458,8 @@ func saveAdvCharacter(char *AdventureCharacter) error {
|
|||||||
masterwork_drops_received = ?,
|
masterwork_drops_received = ?,
|
||||||
rival_pool = ?, rival_unlocked_notified = ?,
|
rival_pool = ?, rival_unlocked_notified = ?,
|
||||||
babysit_active = ?, babysit_expires_at = ?, babysit_skill_focus = ?,
|
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 = ?`,
|
WHERE user_id = ?`,
|
||||||
char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill,
|
char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill,
|
||||||
char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP,
|
char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP,
|
||||||
@@ -428,6 +470,7 @@ func saveAdvCharacter(char *AdventureCharacter) error {
|
|||||||
char.RivalPool, rivalUnlocked,
|
char.RivalPool, rivalUnlocked,
|
||||||
babysitAct, char.BabysitExpiresAt, char.BabysitSkillFocus,
|
babysitAct, char.BabysitExpiresAt, char.BabysitSkillFocus,
|
||||||
char.HospitalVisits, char.RobbieVisitCount, char.LastDeathDate,
|
char.HospitalVisits, char.RobbieVisitCount, char.LastDeathDate,
|
||||||
|
char.CombatActionsUsed, char.HarvestActionsUsed,
|
||||||
string(char.UserID),
|
string(char.UserID),
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
@@ -544,7 +587,8 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
|
|||||||
created_at, last_active_at, death_reprieve_last,
|
created_at, last_active_at, death_reprieve_last,
|
||||||
masterwork_drops_received,
|
masterwork_drops_received,
|
||||||
rival_pool, rival_unlocked_notified,
|
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`)
|
FROM adventure_characters`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -567,6 +611,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
|
|||||||
&c.MasterworkDropsReceived,
|
&c.MasterworkDropsReceived,
|
||||||
&c.RivalPool, &rivalUnlocked,
|
&c.RivalPool, &rivalUnlocked,
|
||||||
&babysitAct, &babysitExp, &c.BabysitSkillFocus,
|
&babysitAct, &babysitExp, &c.BabysitSkillFocus,
|
||||||
|
&c.CombatActionsUsed, &c.HarvestActionsUsed,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -594,7 +639,7 @@ func resetAllAdvDailyActions() error {
|
|||||||
// Only reset actions taken before today — protects against race if a player
|
// Only reset actions taken before today — protects against race if a player
|
||||||
// resolves their action at exactly midnight.
|
// resolves their action at exactly midnight.
|
||||||
today := time.Now().UTC().Format("2006-01-02")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (p *AdventurePlugin) eventTicker() {
|
|||||||
func (p *AdventurePlugin) tryTriggerEvent(userID id.UserID) {
|
func (p *AdventurePlugin) tryTriggerEvent(userID id.UserID) {
|
||||||
// Load character — must be alive and have acted today
|
// Load character — must be alive and have acted today
|
||||||
char, err := loadAdvCharacter(userID)
|
char, err := loadAdvCharacter(userID)
|
||||||
if err != nil || char == nil || !char.Alive || !char.ActionTakenToday {
|
if err != nil || char == nil || !char.Alive || !char.HasActedToday() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,11 +201,19 @@ func renderAdvCharacterSheet(char *AdventureCharacter, equip map[EquipmentSlot]*
|
|||||||
sb.WriteString(" — `!adventure rivals` for details\n")
|
sb.WriteString(" — `!adventure rivals` for details\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Today's action
|
// Today's actions
|
||||||
if char.ActionTakenToday {
|
combatRemaining := maxCombatActions - char.CombatActionsUsed
|
||||||
sb.WriteString("\n📅 Today: Action taken")
|
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 {
|
} 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()
|
return sb.String()
|
||||||
@@ -218,7 +226,7 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq
|
|||||||
|
|
||||||
// Holiday notice (before greeting)
|
// Holiday notice (before greeting)
|
||||||
if holidayName != "" {
|
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
|
// Pick a morning greeting
|
||||||
@@ -259,41 +267,63 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq
|
|||||||
sb.WriteString("\n\n")
|
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
|
// Location choices
|
||||||
sb.WriteString("**1️⃣ Dungeon:**\n")
|
if char.CanDoCombat(isHol) {
|
||||||
for _, el := range advEligibleLocations(char, equip, AdvActivityDungeon, bonuses) {
|
sb.WriteString("**1️⃣ Dungeon:**\n")
|
||||||
warn := ""
|
for _, el := range advEligibleLocations(char, equip, AdvActivityDungeon, bonuses) {
|
||||||
if el.InPenaltyZone {
|
warn := ""
|
||||||
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")
|
if char.CanDoHarvest(isHol) {
|
||||||
for _, el := range advEligibleLocations(char, equip, AdvActivityMining, bonuses) {
|
sb.WriteString("**2️⃣ Mine:**\n")
|
||||||
warn := ""
|
for _, el := range advEligibleLocations(char, equip, AdvActivityMining, bonuses) {
|
||||||
if el.InPenaltyZone {
|
warn := ""
|
||||||
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")
|
sb.WriteString("**3️⃣ Forage:**\n")
|
||||||
for _, el := range advEligibleLocations(char, equip, AdvActivityForaging, bonuses) {
|
for _, el := range advEligibleLocations(char, equip, AdvActivityForaging, bonuses) {
|
||||||
warn := ""
|
warn := ""
|
||||||
if el.InPenaltyZone {
|
if el.InPenaltyZone {
|
||||||
warn = " ⚠️"
|
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")
|
sb.WriteString("**4️⃣ Fish:**\n")
|
||||||
for _, el := range advEligibleLocations(char, equip, AdvActivityFishing, bonuses) {
|
for _, el := range advEligibleLocations(char, equip, AdvActivityFishing, bonuses) {
|
||||||
warn := ""
|
warn := ""
|
||||||
if el.InPenaltyZone {
|
if el.InPenaltyZone {
|
||||||
warn = " ⚠️"
|
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")
|
sb.WriteString("**5️⃣ Shop** — buy/sell gear and loot\n")
|
||||||
|
|||||||
@@ -96,8 +96,9 @@ func (p *AdventurePlugin) sendMorningDMs() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already acted today, skip
|
// If all actions used today, skip
|
||||||
if char.ActionTakenToday {
|
isHol, _ := isHolidayToday()
|
||||||
|
if char.AllActionsUsed(isHol) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +226,7 @@ func (p *AdventurePlugin) postDailySummary() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.ActionTakenToday {
|
if !c.HasActedToday() {
|
||||||
ps.IsResting = true
|
ps.IsResting = true
|
||||||
if len(SummaryResting) > 0 {
|
if len(SummaryResting) > 0 {
|
||||||
ps.SummaryLine = SummaryResting[time.Now().Nanosecond()%len(SummaryResting)]
|
ps.SummaryLine = SummaryResting[time.Now().Nanosecond()%len(SummaryResting)]
|
||||||
@@ -313,7 +314,7 @@ func (p *AdventurePlugin) midnightReset() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !char.ActionTakenToday {
|
if !char.HasActedToday() {
|
||||||
// If the player died today (or yesterday — covering late-night deaths
|
// If the player died today (or yesterday — covering late-night deaths
|
||||||
// that span midnight), grant a grace period: no shame, no streak reset.
|
// that span midnight), grant a grace period: no shame, no streak reset.
|
||||||
if char.LastDeathDate == today ||
|
if char.LastDeathDate == today ||
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ func (p *AdventurePlugin) distributeTwinBeeRewards(result *TwinBeeResult) TwinBe
|
|||||||
|
|
||||||
var eligible []id.UserID
|
var eligible []id.UserID
|
||||||
for _, c := range chars {
|
for _, c := range chars {
|
||||||
if c.ActionTakenToday {
|
if c.HasActedToday() {
|
||||||
eligible = append(eligible, c.UserID)
|
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).
|
// 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) {
|
func (p *XPPlugin) GrantXP(userID id.UserID, amount int, reason string) (int, bool, int) {
|
||||||
return p.grantXPInternal(userID, amount, reason)
|
return p.grantXPInternal(userID, amount, reason)
|
||||||
}
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -132,7 +132,7 @@ func main() {
|
|||||||
registry.Register(plugin.NewBlackjackPlugin(client, euroPlugin))
|
registry.Register(plugin.NewBlackjackPlugin(client, euroPlugin))
|
||||||
registry.Register(plugin.NewUnoPlugin(client, euroPlugin))
|
registry.Register(plugin.NewUnoPlugin(client, euroPlugin))
|
||||||
registry.Register(plugin.NewHoldemPlugin(client, euroPlugin))
|
registry.Register(plugin.NewHoldemPlugin(client, euroPlugin))
|
||||||
adventurePlugin := plugin.NewAdventurePlugin(client, euroPlugin)
|
adventurePlugin := plugin.NewAdventurePlugin(client, euroPlugin, xpPlugin)
|
||||||
registry.Register(adventurePlugin)
|
registry.Register(adventurePlugin)
|
||||||
wordlePlugin := plugin.NewWordlePlugin(client, euroPlugin, dictClient)
|
wordlePlugin := plugin.NewWordlePlugin(client, euroPlugin, dictClient)
|
||||||
registry.Register(wordlePlugin)
|
registry.Register(wordlePlugin)
|
||||||
|
|||||||
Reference in New Issue
Block a user