mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add forward-simulating combat engine with consumables, monster abilities, and narrative rendering
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>
This commit is contained in:
@@ -3,6 +3,7 @@ package plugin
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -152,7 +153,7 @@ func luigiShopGreeting(userID id.UserID, equip map[EquipmentSlot]*AdvEquipment,
|
||||
|
||||
sb.WriteString("⚔️ Weapons 🛡️ Armor\n")
|
||||
sb.WriteString("🪖 Helmets 👢 Boots\n")
|
||||
sb.WriteString("⛏️ Tools\n\n")
|
||||
sb.WriteString("⛏️ Tools 🧪 Supplies\n\n")
|
||||
|
||||
if showAll {
|
||||
flavor, _ := advPickFlavor(luigiShowAllComment, userID, "luigi_showall")
|
||||
@@ -324,11 +325,25 @@ func (p *AdventurePlugin) resolveShopCategoryChoice(ctx MessageContext, interact
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("*%s*", flavor))
|
||||
}
|
||||
|
||||
// Check for supplies category first
|
||||
if reply == "supplies" || reply == "supply" || reply == "consumables" || reply == "potions" {
|
||||
balance := p.euro.GetBalance(ctx.Sender)
|
||||
text := luigiSuppliesView(ctx.Sender, balance)
|
||||
p.pending.Store(string(ctx.Sender), &advPendingInteraction{
|
||||
Type: "shop_supply",
|
||||
Data: data,
|
||||
ExpiresAt: time.Now().Add(advDMResponseWindow),
|
||||
})
|
||||
p.advMarkMenuSent(ctx.Sender)
|
||||
p.shopScheduleBrowseNudge(ctx.Sender)
|
||||
return p.SendDM(ctx.Sender, text)
|
||||
}
|
||||
|
||||
slot := advParseShopCategory(reply)
|
||||
if slot == "" {
|
||||
// Re-store pending and reprompt.
|
||||
p.pending.Store(string(ctx.Sender), interaction)
|
||||
return p.SendDM(ctx.Sender, "I didn't catch that. Reply with a category: weapons, armor, helmets, boots, or tools.")
|
||||
return p.SendDM(ctx.Sender, "I didn't catch that. Reply with a category: weapons, armor, helmets, boots, tools, or supplies.")
|
||||
}
|
||||
|
||||
_, equip, err := p.ensureCharacter(ctx.Sender)
|
||||
@@ -482,6 +497,12 @@ func (p *AdventurePlugin) resolveShopConfirm(ctx MessageContext, interaction *ad
|
||||
return p.SendDM(ctx.Sender, "Transaction failed. The economy is having a moment.")
|
||||
}
|
||||
|
||||
// Community contribution: 5% of purchase price.
|
||||
if potCut := int(def.Price * 0.05); potCut > 0 {
|
||||
communityPotAdd(potCut)
|
||||
trackTaxPaid(ctx.Sender, potCut)
|
||||
}
|
||||
|
||||
// Move old gear to inventory before replacing.
|
||||
if current != nil && current.Tier > 0 {
|
||||
itemType := "ShopGear"
|
||||
@@ -681,6 +702,12 @@ func (p *AdventurePlugin) advBuyEquipment(userID id.UserID, slot EquipmentSlot,
|
||||
return "Transaction failed. The economy is having a moment."
|
||||
}
|
||||
|
||||
// Community contribution: 5% of purchase price.
|
||||
if potCut := int(def.Price * 0.05); potCut > 0 {
|
||||
communityPotAdd(potCut)
|
||||
trackTaxPaid(userID, potCut)
|
||||
}
|
||||
|
||||
// Move old gear to inventory.
|
||||
if current != nil && current.Tier > 0 {
|
||||
itemType := "ShopGear"
|
||||
@@ -767,11 +794,17 @@ func (p *AdventurePlugin) advSellAll(userID id.UserID) string {
|
||||
return "Your inventory is empty. There is nothing to sell. This is a metaphor for something but now is not the time."
|
||||
}
|
||||
|
||||
p.euro.Credit(userID, total, "adventure_sell_all")
|
||||
potCut := math.Round(total * 0.05)
|
||||
payout := total - potCut
|
||||
p.euro.Credit(userID, payout, "adventure_sell_all")
|
||||
if int(potCut) > 0 {
|
||||
communityPotAdd(int(potCut))
|
||||
trackTaxPaid(userID, int(potCut))
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Sold %d items for **€%.0f**.\n\nThe merchant took everything without comment. "+
|
||||
msg := fmt.Sprintf("Sold %d items for **%s** (%s after 5%% broker fee → community pot).\n\nThe merchant took everything without comment. "+
|
||||
"This is the most respect anyone has shown your loot collection. Take the money.",
|
||||
sold, total)
|
||||
sold, fmtEuro(total), fmtEuro(payout))
|
||||
if keptSpecial > 0 {
|
||||
msg += fmt.Sprintf("\n\n(%d special gear items kept — the merchant knows better than to touch those.)", keptSpecial)
|
||||
}
|
||||
@@ -793,8 +826,14 @@ func (p *AdventurePlugin) advSellItem(userID id.UserID, itemName string) string
|
||||
if err := removeAdvInventoryItem(item.ID); err != nil {
|
||||
return "Failed to sell that item."
|
||||
}
|
||||
p.euro.Credit(userID, float64(item.Value), "adventure_sell_"+item.Name)
|
||||
return fmt.Sprintf("Sold **%s** for **€%d**. The merchant nodded. That's it. That's the transaction.", item.Name, item.Value)
|
||||
potCut := int(math.Round(float64(item.Value) * 0.05))
|
||||
payout := int(item.Value) - potCut
|
||||
p.euro.Credit(userID, float64(payout), "adventure_sell_"+item.Name)
|
||||
if potCut > 0 {
|
||||
communityPotAdd(potCut)
|
||||
trackTaxPaid(userID, potCut)
|
||||
}
|
||||
return fmt.Sprintf("Sold **%s** for **%s** (%s after 5%% broker fee → community pot). The merchant nodded. That's it. That's the transaction.", item.Name, fmtEuro(item.Value), fmtEuro(payout))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,3 +872,116 @@ func advInventoryDisplay(userID id.UserID) string {
|
||||
sb.WriteString("\nTo equip Masterwork gear: `!adventure equip`")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ── Supplies (Consumables) ──────────────────────────────────────────────────
|
||||
|
||||
func luigiSuppliesView(_ id.UserID, balance float64) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("🧪 **Supplies** — combat consumables (auto-used)\n")
|
||||
sb.WriteString(fmt.Sprintf("💰 Balance: €%.0f\n\n", balance))
|
||||
sb.WriteString("Items are used automatically during combat. The engine picks up to 2 per fight based on threat level — it won't waste them on easy fights.\n\n")
|
||||
|
||||
defs := BuyableConsumables()
|
||||
if len(defs) == 0 {
|
||||
sb.WriteString("_Nothing in stock right now._\n\n")
|
||||
} else {
|
||||
for _, def := range defs {
|
||||
effectDesc := consumableEffectDescription(def.Effect, def.Value)
|
||||
sb.WriteString(fmt.Sprintf("**%s** (T%d) — €%d\n %s\n\n", def.Name, def.Tier, def.Price, effectDesc))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("Reply with an item name to buy, or `back` to return.\n")
|
||||
sb.WriteString("Stronger consumables drop from foraging, mining, fishing, and dungeons at T2+.")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func consumableEffectDescription(effect ConsumableEffect, value float64) string {
|
||||
switch effect {
|
||||
case EffectHeal:
|
||||
return fmt.Sprintf("Restores %d HP mid-combat (triggers at <50%% HP)", int(value))
|
||||
case EffectDefBoost:
|
||||
return fmt.Sprintf("+%.0f%% Defense for the fight", value*100)
|
||||
case EffectAtkBoost:
|
||||
return fmt.Sprintf("+%.0f%% Attack for the fight", value*100)
|
||||
case EffectWard:
|
||||
return "Blocks one hit completely"
|
||||
case EffectSpeedBoost:
|
||||
return fmt.Sprintf("+%.0f%% Speed for the fight (evasion boost)", value*100)
|
||||
case EffectCritBoost:
|
||||
return fmt.Sprintf("+%.0f%% Crit Rate for the fight", value*100)
|
||||
case EffectFlatDmg:
|
||||
return fmt.Sprintf("Deals %d damage at fight start", int(value))
|
||||
case EffectSpore:
|
||||
return fmt.Sprintf("15%% enemy miss chance for %d rounds", int(value))
|
||||
case EffectReflect:
|
||||
return fmt.Sprintf("Reflects %.0f%% of next hit back at enemy", value*100)
|
||||
case EffectAutoCrit:
|
||||
return fmt.Sprintf("First hit is auto-crit + %.0f%% Attack", value*100)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *AdventurePlugin) resolveShopSupplyChoice(ctx MessageContext, interaction *advPendingInteraction) error {
|
||||
reply := strings.ToLower(strings.TrimSpace(ctx.Body))
|
||||
|
||||
if reply == "back" {
|
||||
data := interaction.Data.(*advPendingShopCategory)
|
||||
_, equip, err := p.ensureCharacter(ctx.Sender)
|
||||
if err != nil {
|
||||
return p.SendDM(ctx.Sender, "Failed to load your character.")
|
||||
}
|
||||
balance := p.euro.GetBalance(ctx.Sender)
|
||||
text := luigiShopGreeting(ctx.Sender, equip, balance, data.ShowAll, p.chatLevel(ctx.Sender))
|
||||
p.pending.Store(string(ctx.Sender), &advPendingInteraction{
|
||||
Type: "shop_category",
|
||||
Data: &advPendingShopCategory{ShowAll: data.ShowAll},
|
||||
ExpiresAt: time.Now().Add(advDMResponseWindow),
|
||||
})
|
||||
return p.SendDM(ctx.Sender, text)
|
||||
}
|
||||
if reply == "exit" || reply == "cancel" {
|
||||
p.shopSessionEnd(ctx.Sender)
|
||||
return p.SendDM(ctx.Sender, "*Luigi nods and gestures toward the main counter.*")
|
||||
}
|
||||
|
||||
// Find matching consumable
|
||||
var match *ConsumableDef
|
||||
for i := range consumableDefs {
|
||||
if consumableDefs[i].Buyable && containsFold(consumableDefs[i].Name, reply) {
|
||||
match = &consumableDefs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if match == nil {
|
||||
p.pending.Store(string(ctx.Sender), interaction)
|
||||
return p.SendDM(ctx.Sender, "I don't have that. Reply with an item name from the list, or `back` to return.")
|
||||
}
|
||||
|
||||
balance := p.euro.GetBalance(ctx.Sender)
|
||||
if balance < float64(match.Price) {
|
||||
p.pending.Store(string(ctx.Sender), interaction)
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("You need €%d for %s but only have €%.0f.", match.Price, match.Name, balance))
|
||||
}
|
||||
|
||||
// Purchase the consumable
|
||||
p.euro.Debit(ctx.Sender, float64(match.Price), "shop_consumable")
|
||||
if potCut := int(math.Round(float64(match.Price) * 0.05)); potCut > 0 {
|
||||
communityPotAdd(potCut)
|
||||
trackTaxPaid(ctx.Sender, potCut)
|
||||
}
|
||||
item := AdvItem{
|
||||
Name: match.Name,
|
||||
Type: "consumable",
|
||||
Tier: match.Tier,
|
||||
Value: match.Price / 2,
|
||||
}
|
||||
_ = addAdvInventoryItem(ctx.Sender, item)
|
||||
|
||||
// Stay in supplies view for more purchases
|
||||
p.pending.Store(string(ctx.Sender), interaction)
|
||||
newBalance := p.euro.GetBalance(ctx.Sender)
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("Purchased **%s** for €%d. Added to inventory.\n💰 Balance: €%.0f\n\nReply with another item name or `back` to return.",
|
||||
match.Name, match.Price, newBalance))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user