mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Replaces the single-roll probability system for dungeon and arena combat with a multi-phase simulation engine where gear, buffs, pets, and NPCs are meaningful mechanical inputs. Adds consumable items (auto-crafted from forage/mine/fish drops), monster abilities for T2+ enemies, and Dragon Quest-style combat narrative with phased message delivery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
10 KiB
Go
258 lines
10 KiB
Go
package plugin
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// ── Arena Tier Menu ─────────────────────────────────────────────────────────
|
||
|
||
func renderArenaStreakEntry(char *AdventureCharacter, stats *ArenaPersonalStats, tier *ArenaTier, firstMonster *ArenaMonster) string {
|
||
var b strings.Builder
|
||
b.WriteString("⚔️ **THE ARENA**\n\n")
|
||
b.WriteString("The Arena is a streak. You start at Tier 1 and fight your way down.\n")
|
||
b.WriteString("After each tier, you have 30 seconds to bail or you auto-advance.\n")
|
||
b.WriteString("Death forfeits all accumulated rewards.\n\n")
|
||
|
||
b.WriteString(fmt.Sprintf("Combat Level: %d\n\n", char.CombatLevel))
|
||
|
||
for i := range arenaTiers {
|
||
t := &arenaTiers[i]
|
||
eligible := char.CombatLevel >= t.MinLevel
|
||
icon := "🔒"
|
||
if eligible {
|
||
icon = "⬚"
|
||
}
|
||
if stats != nil && stats.HighestTier >= t.Number {
|
||
icon = "✅"
|
||
}
|
||
mult := arenaStreakEuroMultiplier[t.Number]
|
||
b.WriteString(fmt.Sprintf("%s **Tier %d — %s** (Lv.%d+) — %.1f× euros\n", icon, t.Number, t.Name, t.MinLevel, mult))
|
||
}
|
||
|
||
if stats != nil && stats.TotalRuns > 0 {
|
||
b.WriteString(fmt.Sprintf("\nRuns: %d | Deaths: %d | Earned: €%d\n",
|
||
stats.TotalRuns, stats.TotalDeaths, stats.TotalEarnings))
|
||
}
|
||
|
||
b.WriteString("\n⛑️ _Today's helmets are provided by Brim & Battle — celebrated makers of baseball hats — and proud sponsors of the Arena. ")
|
||
b.WriteString("Their new VeriFort line of field-evaluated combat headgear represents an exciting expansion beyond their area of expertise. ")
|
||
b.WriteString("Winners may receive a complimentary sample. Brim & Battle thanks you for your participation in their ongoing verification process._\n\n")
|
||
|
||
b.WriteString(fmt.Sprintf("**Round 1 opponent: %s**\n", firstMonster.Name))
|
||
b.WriteString(fmt.Sprintf("_%s_\n\n", firstMonster.Flavor))
|
||
b.WriteString("`!arena fight` — Enter and fight Round 1\n")
|
||
b.WriteString("`!arena cancel` — Back out")
|
||
return b.String()
|
||
}
|
||
|
||
// ── Round Start (Monster Reveal) ────────────────────────────────────────────
|
||
|
||
func renderArenaRoundStart(tier *ArenaTier, round int, monster *ArenaMonster, run *ArenaRun) string {
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("⚔️ **Tier %d — %s | Round %d/4**\n\n", tier.Number, tier.Name, round))
|
||
b.WriteString(fmt.Sprintf("**%s**\n", monster.Name))
|
||
b.WriteString(fmt.Sprintf("_%s_\n\n", monster.Flavor))
|
||
|
||
sessionTotal := run.Earnings + run.TierEarnings
|
||
if sessionTotal > 0 {
|
||
b.WriteString(fmt.Sprintf("Session earnings: €%d (at risk)\n\n", sessionTotal))
|
||
}
|
||
|
||
b.WriteString("`!arena fight` — Face this opponent\n")
|
||
return b.String()
|
||
}
|
||
|
||
// renderArenaSurvival removed — survival text is built by renderArenaCombatLog + inline formatting.
|
||
|
||
// ── Tier Complete (Transition Prompt) ───────────────────────────────────────
|
||
|
||
// renderArenaTierComplete is no longer used — tier complete text is built inline
|
||
// in resolveArenaSurvival and the countdown DM handles the opt-out flow.
|
||
|
||
// renderArenaTier5Complete removed — T5 completion is part of arenaCompleteSession payout summary.
|
||
|
||
// ── Death ────────────────────────────────────────────────────────────────────
|
||
|
||
func renderArenaDeath(tier *ArenaTier, round int, monster *ArenaMonster, lostEarnings int64, deathMsg string) string {
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("💀 **DEAD** — Tier %d, Round %d\n\n", tier.Number, round))
|
||
b.WriteString(fmt.Sprintf("_%s_\n\n", deathMsg))
|
||
|
||
if lostEarnings > 0 {
|
||
b.WriteString(fmt.Sprintf("Forfeited earnings: €%d\n", lostEarnings))
|
||
}
|
||
b.WriteString("You are dead until midnight UTC. Both arena and daily adventure are blocked until you respawn.")
|
||
return b.String()
|
||
}
|
||
|
||
// renderArenaCashout and renderArenaAutoCashout removed — payout handled by arenaCompleteSession.
|
||
|
||
// ── Status ──────────────────────────────────────────────────────────────────
|
||
|
||
func renderArenaStatus(run *ArenaRun, char *AdventureCharacter) string {
|
||
tier := arenaGetTier(run.Tier)
|
||
if tier == nil {
|
||
return "No active arena run."
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString("⚔️ **Arena Streak Status**\n\n")
|
||
b.WriteString(fmt.Sprintf("Tier: %d — %s\n", tier.Number, tier.Name))
|
||
|
||
sessionTotal := run.Earnings + run.TierEarnings
|
||
|
||
switch run.Status {
|
||
case "active":
|
||
monster := arenaGetMonster(run.Tier, run.Round)
|
||
b.WriteString(fmt.Sprintf("Round: %d/4\n", run.Round))
|
||
if monster != nil {
|
||
b.WriteString(fmt.Sprintf("Opponent: %s\n", monster.Name))
|
||
}
|
||
b.WriteString(fmt.Sprintf("Session total: €%d (at risk)\n", sessionTotal))
|
||
b.WriteString(fmt.Sprintf("Rounds survived: %d\n", run.RoundsSurvived))
|
||
b.WriteString("\n`!arena fight` to continue")
|
||
case "awaiting":
|
||
b.WriteString(fmt.Sprintf("Tier %d cleared — advancing shortly\n", run.Tier))
|
||
b.WriteString(fmt.Sprintf("Session total: €%d (at risk)\n", sessionTotal))
|
||
b.WriteString(fmt.Sprintf("Rounds survived: %d\n", run.RoundsSurvived))
|
||
b.WriteString("\n`!bail` to collect your rewards")
|
||
}
|
||
|
||
return b.String()
|
||
}
|
||
|
||
// ── Leaderboard ─────────────────────────────────────────────────────────────
|
||
|
||
type ArenaLeaderboardEntry struct {
|
||
DisplayName string
|
||
TotalEarnings int64
|
||
HighestTier int
|
||
Tier5Completions int
|
||
TotalRuns int
|
||
TotalDeaths int
|
||
}
|
||
|
||
func renderArenaLeaderboard(entries []ArenaLeaderboardEntry) string {
|
||
if len(entries) == 0 {
|
||
return "⚔️ **Arena Leaderboard**\n\nNo arena runs recorded yet. Be the first."
|
||
}
|
||
|
||
var b strings.Builder
|
||
b.WriteString("⚔️ **Arena Leaderboard**\n\n")
|
||
|
||
medals := []string{"🥇", "🥈", "🥉"}
|
||
for i, e := range entries {
|
||
prefix := fmt.Sprintf("%d.", i+1)
|
||
if i < 3 {
|
||
prefix = medals[i]
|
||
}
|
||
|
||
tierLabel := fmt.Sprintf("T%d", e.HighestTier)
|
||
if e.Tier5Completions > 0 {
|
||
tierLabel = fmt.Sprintf("T5×%d", e.Tier5Completions)
|
||
}
|
||
|
||
b.WriteString(fmt.Sprintf("%s **%s** — €%d earned | %s | %d runs | %d deaths\n",
|
||
prefix, e.DisplayName, e.TotalEarnings, tierLabel, e.TotalRuns, e.TotalDeaths))
|
||
}
|
||
|
||
return b.String()
|
||
}
|
||
|
||
// ── Personal Stats ──────────────────────────────────────────────────────────
|
||
|
||
type ArenaPersonalStats struct {
|
||
TotalRuns int
|
||
TotalEarnings int64
|
||
TotalDeaths int
|
||
HighestTier int
|
||
Tier5Completions int
|
||
}
|
||
|
||
func renderArenaPersonalStats(char *AdventureCharacter, stats *ArenaPersonalStats) string {
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("⚔️ **%s's Arena Stats**\n\n", char.DisplayName))
|
||
|
||
if stats == nil || stats.TotalRuns == 0 {
|
||
b.WriteString("No arena runs yet. Type `!arena` to begin.")
|
||
return b.String()
|
||
}
|
||
|
||
b.WriteString(fmt.Sprintf("Total runs: %d\n", stats.TotalRuns))
|
||
b.WriteString(fmt.Sprintf("Total earnings: €%d\n", stats.TotalEarnings))
|
||
b.WriteString(fmt.Sprintf("Total deaths: %d\n", stats.TotalDeaths))
|
||
b.WriteString(fmt.Sprintf("Highest tier cleared: %d\n", stats.HighestTier))
|
||
|
||
if stats.TotalRuns > 0 {
|
||
survivalRate := float64(stats.TotalRuns-stats.TotalDeaths) / float64(stats.TotalRuns) * 100
|
||
b.WriteString(fmt.Sprintf("Survival rate: %.0f%%\n", survivalRate))
|
||
}
|
||
|
||
if stats.Tier5Completions > 0 {
|
||
b.WriteString(fmt.Sprintf("Tier 5 completions: %d\n", stats.Tier5Completions))
|
||
}
|
||
|
||
b.WriteString(fmt.Sprintf("\nArena W/L: %d/%d", char.ArenaWins, char.ArenaLosses))
|
||
return b.String()
|
||
}
|
||
|
||
// ── Level Gate Message ──────────────────────────────────────────────────────
|
||
|
||
func renderArenaLevelGate(tier *ArenaTier, playerLevel int) string {
|
||
return fmt.Sprintf(
|
||
"⚔️ Tier %d — %s requires Combat Level %d. You are Level %d. "+
|
||
"The Arena does not negotiate.",
|
||
tier.Number, tier.Name, tier.MinLevel, playerLevel)
|
||
}
|
||
|
||
// ── Helmet Drop ────────────────────────────────────────────────────────────
|
||
|
||
func renderArenaHelmetDrop(gear *ArenaGearSet) string {
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("⛑️ **Equipment Drop: %s**\n\n", gear.HelmetName))
|
||
b.WriteString(fmt.Sprintf("_%s_\n\n", gear.Description))
|
||
b.WriteString(fmt.Sprintf("Tier %d Arena gear — 1.5x effectiveness. ", gear.Tier))
|
||
|
||
switch gear.SetKey {
|
||
case "bloodied":
|
||
b.WriteString("Set bonus: **Survivor's Instinct** — +3% to all activity success rates, +3% critical hit rate in combat.")
|
||
case "ironclad":
|
||
b.WriteString("Set bonus: **Battle-Hardened** — +5% XP gain from all activities.")
|
||
case "tempered":
|
||
b.WriteString("Set bonus: **Seasoned** — Equipment condition degrades 25% slower across all slots.")
|
||
case "champions":
|
||
b.WriteString("Set bonus: **Commanding Presence** — +10% to equipment score in all probability calculations.")
|
||
case "sovereign":
|
||
b.WriteString("Set bonus: **Death's Reprieve** — Once per 7 days, survive a lethal outcome instead of dying.")
|
||
}
|
||
|
||
b.WriteString("\n\nEquipped automatically.")
|
||
return b.String()
|
||
}
|
||
|
||
// ── Death's Reprieve Announcement ──────────────────────────────────────────
|
||
|
||
func renderArenaDeathReprieve(playerName, location string, nextWindow time.Time) string {
|
||
return fmt.Sprintf("⚔️ **%s**'s Sovereign gear activated **Death's Reprieve**. "+
|
||
"They should be dead. They are not. %s will have to try harder. Next window: %s.",
|
||
playerName, location, nextWindow.Format("2006-01-02 15:04 UTC"))
|
||
}
|
||
|
||
// ── Already In Run Message ──────────────────────────────────────────────────
|
||
|
||
func renderArenaAlreadyInRun(run *ArenaRun) string {
|
||
switch run.Status {
|
||
case "awaiting":
|
||
return fmt.Sprintf(
|
||
"Tier %d cleared, €%d accumulated. Waiting for next tier or `!bail` to collect.\n",
|
||
run.Tier, run.Earnings)
|
||
default:
|
||
return fmt.Sprintf(
|
||
"You're in an arena streak. Tier %d, Round %d.\n\n"+
|
||
"`!arena fight` to continue.",
|
||
run.Tier, run.Round)
|
||
}
|
||
}
|