mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Auto-walk: gate on fork/rest, slow cadence, credit DnD streak
Four fixes for the auto-walk loop that was re-clearing the same room every 15 min while a fork was pending, ignoring the rest lockout, and not counting expedition activity toward the daily streak: - advanceOnceWithOpts / runAutopilotWalk now short-circuit to stopFork when NodeChoices has a pending fork. Stops phantom kills + duplicate loot drops + fork re-prompts on the same cleared room. - fireExpeditionAutoRuns honors restingLockoutRemaining so the background ticker no longer walks through a long rest. - autoRunCooldown 15m -> 2h, autoRunTickInterval 5m -> 15m. Auto-walk DMs are now a once-in-a-while ping, not a steady drip. - markActedToday + HasActedToday recognise LastActionDate. Wired into !rest short/long, !expedition start/abandon, !extract, foreground !zone advance, and !zone go so DnD-side activity credits the streak even when the expedition ends before midnight. (cherry picked from commit 9e27fd8257a4c92150ad584b393bf5a72270b82c)
This commit is contained in:
@@ -311,7 +311,30 @@ func (c *AdventureCharacter) CanDoHarvest(isHoliday bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *AdventureCharacter) HasActedToday() bool {
|
func (c *AdventureCharacter) HasActedToday() bool {
|
||||||
return c.CombatActionsUsed > 0 || c.HarvestActionsUsed > 0
|
if c.CombatActionsUsed > 0 || c.HarvestActionsUsed > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// DnD-side flows (expedition / zone / rest / autopilot) don't touch
|
||||||
|
// the legacy action counters; they credit the day via LastActionDate
|
||||||
|
// instead. Honor that so a player who ran an expedition all day and
|
||||||
|
// extracted before midnight still counts as having acted.
|
||||||
|
return c.LastActionDate == time.Now().UTC().Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
// markActedToday stamps the player's LastActionDate to today so the
|
||||||
|
// midnight reset credits the day. Safe to call on every DnD-side action;
|
||||||
|
// no-ops if the date is already today.
|
||||||
|
func markActedToday(userID id.UserID) {
|
||||||
|
today := time.Now().UTC().Format("2006-01-02")
|
||||||
|
c, err := loadAdvCharacter(userID)
|
||||||
|
if err != nil || c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.LastActionDate == today {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.LastActionDate = today
|
||||||
|
_ = saveAdvCharacter(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AdventureCharacter) AllActionsUsed(isHoliday bool) bool {
|
func (c *AdventureCharacter) AllActionsUsed(isHoliday bool) bool {
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ func (p *AdventurePlugin) expeditionCmdStart(ctx MessageContext, c *DnDCharacter
|
|||||||
// Log the start with prewritten flavor.
|
// Log the start with prewritten flavor.
|
||||||
startLine := flavor.Pick(flavor.ExpeditionStart)
|
startLine := flavor.Pick(flavor.ExpeditionStart)
|
||||||
_ = appendExpeditionLog(exp.ID, 1, "narrative", "expedition started", startLine)
|
_ = appendExpeditionLog(exp.ID, 1, "narrative", "expedition started", startLine)
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(fmt.Sprintf("🗺 **Expedition begins — %s** _(T%d)_\n\n", zone.Display, int(zone.Tier)))
|
b.WriteString(fmt.Sprintf("🗺 **Expedition begins — %s** _(T%d)_\n\n", zone.Display, int(zone.Tier)))
|
||||||
@@ -484,6 +485,7 @@ func (p *AdventurePlugin) expeditionCmdAbandon(ctx MessageContext) error {
|
|||||||
if err := abandonExpedition(ctx.Sender); err != nil {
|
if err := abandonExpedition(ctx.Sender); err != nil {
|
||||||
return p.SendDM(ctx.Sender, "Couldn't abandon: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't abandon: "+err.Error())
|
||||||
}
|
}
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
_ = retireAllRegionRuns(exp)
|
_ = retireAllRegionRuns(exp)
|
||||||
_ = appendExpeditionLog(exp.ID, exp.CurrentDay, "narrative", "expedition abandoned", "")
|
_ = appendExpeditionLog(exp.ID, exp.CurrentDay, "narrative", "expedition abandoned", "")
|
||||||
if err := p.SendDM(ctx.Sender, fmt.Sprintf(
|
if err := p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||||
@@ -583,6 +585,21 @@ func (p *AdventurePlugin) runAutopilotWalk(ctx MessageContext, maxRooms int, com
|
|||||||
return autopilotWalkResult{initErr: "No active region run. Try `!region` to refresh."}
|
return autopilotWalkResult{initErr: "No active region run. Try `!region` to refresh."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Already standing at a pending fork: autopilot can't pick for the
|
||||||
|
// player. Re-emit the prompt with rooms=0 so the background DM
|
||||||
|
// suppression keeps quiet and we don't tick the rooms counter on a
|
||||||
|
// no-op walk.
|
||||||
|
if run, rerr := getActiveZoneRun(ctx.Sender); rerr == nil && run != nil {
|
||||||
|
if pf, derr := decodePendingFork(run.NodeChoices); derr == nil && pf != nil {
|
||||||
|
zone := zoneOrFallback(run.ZoneID)
|
||||||
|
return autopilotWalkResult{
|
||||||
|
finalMsg: renderForkPrompt(zone, *pf),
|
||||||
|
rooms: 0,
|
||||||
|
reason: stopFork,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var stream []string
|
var stream []string
|
||||||
var finalMsg string
|
var finalMsg string
|
||||||
rooms := 0
|
rooms := 0
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ func (p *AdventurePlugin) handleExtractCmd(ctx MessageContext, _ string) error {
|
|||||||
line := flavor.Pick(flavor.ExtractionVoluntary)
|
line := flavor.Pick(flavor.ExtractionVoluntary)
|
||||||
_ = appendExpeditionLog(updated.ID, updated.CurrentDay, "narrative",
|
_ = appendExpeditionLog(updated.ID, updated.CurrentDay, "narrative",
|
||||||
"voluntary extraction", line)
|
"voluntary extraction", line)
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(fmt.Sprintf("🚪 **Extraction — %s, Day %d**\n\n",
|
b.WriteString(fmt.Sprintf("🚪 **Extraction — %s, Day %d**\n\n",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ func (p *AdventurePlugin) handleDnDShortRest(ctx MessageContext) error {
|
|||||||
if err := SaveDnDCharacter(c); err != nil {
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
return p.SendDM(ctx.Sender, "Couldn't save rest state: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't save rest state: "+err.Error())
|
||||||
}
|
}
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
|
|
||||||
var msg string
|
var msg string
|
||||||
if c.HPCurrent > before {
|
if c.HPCurrent > before {
|
||||||
@@ -208,6 +209,7 @@ func (p *AdventurePlugin) handleDnDLongRest(ctx MessageContext) error {
|
|||||||
if err := SaveDnDCharacter(c); err != nil {
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
return p.SendDM(ctx.Sender, "Couldn't save rest state: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't save rest state: "+err.Error())
|
||||||
}
|
}
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
_ = refreshAllResources(ctx.Sender)
|
_ = refreshAllResources(ctx.Sender)
|
||||||
// Phase 9: spell slots refresh on long rest.
|
// Phase 9: spell slots refresh on long rest.
|
||||||
_ = refreshSpellSlots(ctx.Sender)
|
_ = refreshSpellSlots(ctx.Sender)
|
||||||
|
|||||||
@@ -308,6 +308,9 @@ func (p *AdventurePlugin) zoneCmdMap(ctx MessageContext) error {
|
|||||||
b.WriteString(renderZoneGraphMap(g, run))
|
b.WriteString(renderZoneGraphMap(g, run))
|
||||||
b.WriteString("\n```\n")
|
b.WriteString("\n```\n")
|
||||||
b.WriteString("_E=Entry ?=Exploration T=Trap ★=Elite ☠=Boss ⚿=Secret · ✓ cleared ▶ here · pending ╳ locked_")
|
b.WriteString("_E=Entry ?=Exploration T=Trap ★=Elite ☠=Boss ⚿=Secret · ✓ cleared ▶ here · pending ╳ locked_")
|
||||||
|
if path := renderVisitedPath(g, run); path != "" {
|
||||||
|
b.WriteString("\n**Path:** " + path)
|
||||||
|
}
|
||||||
return p.SendDM(ctx.Sender, b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
// No registered graph (defensive — every zone has one post-G8).
|
// No registered graph (defensive — every zone has one post-G8).
|
||||||
@@ -455,8 +458,23 @@ func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact bool)
|
|||||||
reason: stopBlocked,
|
reason: stopBlocked,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
// compact==true is the background auto-walk path; only credit
|
||||||
|
// player-initiated advances toward the daily streak.
|
||||||
|
if !compact {
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
|
}
|
||||||
_ = applyMoodDecayIfStale(run)
|
_ = applyMoodDecayIfStale(run)
|
||||||
zone := zoneOrFallback(run.ZoneID)
|
zone := zoneOrFallback(run.ZoneID)
|
||||||
|
// A pending fork means advanceTransitionGraph already cleared the
|
||||||
|
// current room and stopped — re-running resolveRoom would re-fire
|
||||||
|
// combat and re-drop loot on the same room. Re-emit the fork prompt
|
||||||
|
// and let the caller surface it; the player commits via !zone go <n>.
|
||||||
|
if pf, derr := decodePendingFork(run.NodeChoices); derr == nil && pf != nil {
|
||||||
|
return advanceResult{
|
||||||
|
final: renderForkPrompt(zone, *pf),
|
||||||
|
reason: stopFork,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
prev := run.CurrentRoomType()
|
prev := run.CurrentRoomType()
|
||||||
prevIdx := run.CurrentRoom
|
prevIdx := run.CurrentRoom
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ func (p *AdventurePlugin) zoneCmdGo(ctx MessageContext, rest string) error {
|
|||||||
if aerr := advanceZoneRunNode(run.RunID, chosen.To); aerr != nil {
|
if aerr := advanceZoneRunNode(run.RunID, chosen.To); aerr != nil {
|
||||||
return p.SendDM(ctx.Sender, "Couldn't advance: "+aerr.Error())
|
return p.SendDM(ctx.Sender, "Couldn't advance: "+aerr.Error())
|
||||||
}
|
}
|
||||||
|
markActedToday(ctx.Sender)
|
||||||
g, _ := loadZoneGraph(run.ZoneID)
|
g, _ := loadZoneGraph(run.ZoneID)
|
||||||
zone := zoneOrFallback(run.ZoneID)
|
zone := zoneOrFallback(run.ZoneID)
|
||||||
fromNode := g.Nodes[run.CurrentNode]
|
fromNode := g.Nodes[run.CurrentNode]
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ const (
|
|||||||
// autoRunTickInterval — how often the ticker wakes. The per-expedition
|
// autoRunTickInterval — how often the ticker wakes. The per-expedition
|
||||||
// cooldown is what actually paces; the tick just has to be frequent
|
// cooldown is what actually paces; the tick just has to be frequent
|
||||||
// enough that the cooldown is enforced cleanly.
|
// enough that the cooldown is enforced cleanly.
|
||||||
autoRunTickInterval = 5 * time.Minute
|
autoRunTickInterval = 15 * time.Minute
|
||||||
|
|
||||||
// autoRunCooldown — minimum gap between background walks for one
|
// autoRunCooldown — minimum gap between background walks for one
|
||||||
// expedition. 15 min keeps the dungeon moving while leaving room for
|
// expedition. 2h is the slow cadence: the dungeon still moves on its
|
||||||
// the player to step in and steer if they want.
|
// own while the player is away, but the auto-walk DM stays a
|
||||||
autoRunCooldown = 15 * time.Minute
|
// once-in-a-while ping instead of a steady drip.
|
||||||
|
autoRunCooldown = 2 * time.Hour
|
||||||
|
|
||||||
// autoRunMinExpeditionAge — don't auto-walk a brand-new expedition;
|
// autoRunMinExpeditionAge — don't auto-walk a brand-new expedition;
|
||||||
// let the player walk the first beat manually so the opening reads
|
// let the player walk the first beat manually so the opening reads
|
||||||
@@ -93,6 +94,14 @@ func (p *AdventurePlugin) fireExpeditionAutoRuns(now time.Time) {
|
|||||||
if hasActiveCombatSession(uid) {
|
if hasActiveCombatSession(uid) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Honor the rest lockout — short/long rest set RestingUntil and
|
||||||
|
// foreground !expedition run checks it; background must too, or
|
||||||
|
// the auto-walk ticker fires through a long rest.
|
||||||
|
if c, cerr := LoadDnDCharacter(uid); cerr == nil && c != nil {
|
||||||
|
if restingLockoutRemaining(c) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := p.tryAutoRun(e, now); err != nil {
|
if err := p.tryAutoRun(e, now); err != nil {
|
||||||
slog.Warn("expedition: autorun step", "expedition", e.ID, "err", err)
|
slog.Warn("expedition: autorun step", "expedition", e.ID, "err", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user