mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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:
@@ -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))
|
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
|
// Resolve the action
|
||||||
result := resolveAdvAction(char, equip, loc, bonuses, inPenaltyZone)
|
result := resolveAdvAction(char, equip, loc, bonuses, inPenaltyZone)
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ package plugin
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogobee/internal/db"
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const advLocationCooldownDuration = 3 * time.Hour
|
||||||
|
|
||||||
// ── Activity & Outcome Types ─────────────────────────────────────────────────
|
// ── Activity & Outcome Types ─────────────────────────────────────────────────
|
||||||
|
|
||||||
type AdvActivityType string
|
type AdvActivityType string
|
||||||
@@ -339,6 +344,33 @@ func computeAdvBonuses(treasures []AdvTreasureBonus, buffs []AdvBuff, streak int
|
|||||||
return b
|
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 ──────────────────────────────────────────────────────────────
|
// ── Eligibility ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// advIsEligible checks if a character can enter a location.
|
// advIsEligible checks if a character can enter a location.
|
||||||
@@ -578,6 +610,20 @@ func advCheckBrokenEquipment(equip map[EquipmentSlot]*AdvEquipment) []EquipmentS
|
|||||||
return broken
|
return broken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Overlevel Penalty ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// advOverlevelMultiplier returns a multiplier (0.05–1.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 ───────────────────────────────────────────────────────
|
// ── Outcome Resolution ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
type AdvActionResult struct {
|
type AdvActionResult struct {
|
||||||
@@ -606,6 +652,10 @@ func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEqui
|
|||||||
|
|
||||||
probs := calculateAdvProbabilities(char, equip, loc, bonuses, inPenaltyZone)
|
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 outcome
|
||||||
roll := rand.Float64() * 100
|
roll := rand.Float64() * 100
|
||||||
|
|
||||||
@@ -629,6 +679,12 @@ func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEqui
|
|||||||
// Generate loot for success/exceptional
|
// Generate loot for success/exceptional
|
||||||
if result.Outcome == AdvOutcomeSuccess || result.Outcome == AdvOutcomeExceptional {
|
if result.Outcome == AdvOutcomeSuccess || result.Outcome == AdvOutcomeExceptional {
|
||||||
result.LootItems = generateAdvLoot(loc, result.Outcome == AdvOutcomeExceptional, bonuses.LootQuality)
|
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 {
|
for _, item := range result.LootItems {
|
||||||
result.TotalLootValue += item.Value
|
result.TotalLootValue += item.Value
|
||||||
}
|
}
|
||||||
@@ -656,6 +712,10 @@ func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEqui
|
|||||||
if advEquippedArenaSets(equip)["ironclad"] {
|
if advEquippedArenaSets(equip)["ironclad"] {
|
||||||
xp = int(float64(xp) * 1.05)
|
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
|
result.XPGained = xp
|
||||||
|
|
||||||
// Equipment degradation on bad outcomes
|
// Equipment degradation on bad outcomes
|
||||||
|
|||||||
Reference in New Issue
Block a user