Add overlevel penalty and per-location cooldown to prevent low-tier farming

- Overlevel penalty reduces loot and XP when effective level exceeds location minimum (gap >3: -15%/level, floor 5%)
- 3-hour per-location cooldown after successful runs, persisted via activity log
- Applies to all activity types (dungeons, mining, foraging, fishing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-31 19:20:26 -07:00
parent 3d6bc1a066
commit f61bf89c71
2 changed files with 73 additions and 0 deletions

View File

@@ -632,6 +632,19 @@ func (p *AdventurePlugin) resolveActivity(ctx MessageContext, char *AdventureCha
return p.SendDM(ctx.Sender, fmt.Sprintf("You are not eligible for %s. Your level or equipment tier is too low.", loc.Name))
}
// Per-location cooldown: 3 hours after a successful run
if remaining := advLocationCooldown(char.UserID, loc.Name); remaining > 0 {
mins := int(remaining.Minutes())
if mins < 1 {
return p.SendDM(ctx.Sender, fmt.Sprintf("🕐 %s is still being cleared out. Try again in less than a minute, or pick a different location.", loc.Name))
}
h, m := mins/60, mins%60
if h > 0 {
return p.SendDM(ctx.Sender, fmt.Sprintf("🕐 %s is still being cleared out. Try again in %dh %dm, or pick a different location.", loc.Name, h, m))
}
return p.SendDM(ctx.Sender, fmt.Sprintf("🕐 %s is still being cleared out. Try again in %d minutes, or pick a different location.", loc.Name, m))
}
// Resolve the action
result := resolveAdvAction(char, equip, loc, bonuses, inPenaltyZone)

View File

@@ -3,10 +3,15 @@ package plugin
import (
"math"
"math/rand/v2"
"time"
"gogobee/internal/db"
"maunium.net/go/mautrix/id"
)
const advLocationCooldownDuration = 3 * time.Hour
// ── Activity & Outcome Types ─────────────────────────────────────────────────
type AdvActivityType string
@@ -339,6 +344,33 @@ func computeAdvBonuses(treasures []AdvTreasureBonus, buffs []AdvBuff, streak int
return b
}
// ── Location Cooldown ───────────────────────────────────────────────────────
// advLocationCooldown returns how long until a player can run the same location
// again. Returns 0 if no cooldown is active. Only successful runs trigger cooldown.
func advLocationCooldown(userID id.UserID, location string) time.Duration {
d := db.Get()
var loggedAt string
err := d.QueryRow(
`SELECT logged_at FROM adventure_activity_log
WHERE user_id = ? AND location = ? AND outcome IN ('success', 'exceptional')
ORDER BY logged_at DESC LIMIT 1`,
string(userID), location,
).Scan(&loggedAt)
if err != nil {
return 0
}
t, err := time.Parse("2006-01-02 15:04:05", loggedAt)
if err != nil {
return 0
}
remaining := advLocationCooldownDuration - time.Since(t)
if remaining <= 0 {
return 0
}
return remaining
}
// ── Eligibility ──────────────────────────────────────────────────────────────
// advIsEligible checks if a character can enter a location.
@@ -578,6 +610,20 @@ func advCheckBrokenEquipment(equip map[EquipmentSlot]*AdvEquipment) []EquipmentS
return broken
}
// ── Overlevel Penalty ───────────────────────────────────────────────────────
// advOverlevelMultiplier returns a multiplier (0.051.0) that reduces XP and
// loot when a character's effective level far exceeds the location's minimum.
// Gap 0-3: no penalty. Gap 4+: 15% per level over 3, floor 5%.
func advOverlevelMultiplier(effectiveLevel, minLevel int) float64 {
gap := effectiveLevel - minLevel
if gap <= 3 {
return 1.0
}
mult := 1.0 - 0.15*float64(gap-3)
return math.Max(0.05, mult)
}
// ── Outcome Resolution ───────────────────────────────────────────────────────
type AdvActionResult struct {
@@ -606,6 +652,10 @@ func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEqui
probs := calculateAdvProbabilities(char, equip, loc, bonuses, inPenaltyZone)
// Overlevel penalty — reduces loot and XP for farming low-tier content
skillLevel := advEffectiveSkill(char, loc.Activity, bonuses)
overlevelMult := advOverlevelMultiplier(skillLevel, loc.MinLevel)
// Roll outcome
roll := rand.Float64() * 100
@@ -629,6 +679,12 @@ func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEqui
// Generate loot for success/exceptional
if result.Outcome == AdvOutcomeSuccess || result.Outcome == AdvOutcomeExceptional {
result.LootItems = generateAdvLoot(loc, result.Outcome == AdvOutcomeExceptional, bonuses.LootQuality)
// Apply overlevel penalty to loot values
if overlevelMult < 1.0 {
for i := range result.LootItems {
result.LootItems[i].Value = max(1, int64(float64(result.LootItems[i].Value)*overlevelMult))
}
}
for _, item := range result.LootItems {
result.TotalLootValue += item.Value
}
@@ -656,6 +712,10 @@ func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEqui
if advEquippedArenaSets(equip)["ironclad"] {
xp = int(float64(xp) * 1.05)
}
// Apply overlevel penalty to XP
if overlevelMult < 1.0 {
xp = max(1, int(float64(xp)*overlevelMult))
}
result.XPGained = xp
// Equipment degradation on bad outcomes