mirror of
https://github.com/prosolis/gogobee.git
synced 2026-09-14 10:51:09 +00:00
zones: make locked doors openable and stop autopilot stalling on them
Locks were fully implemented as pass/fail gates and nothing else. A Perception or stat check rolls once per (run, edge), seeded so it can't be reload-scummed — that half shipped in G5, the counterweight never did. A bad roll simply deleted a branch of the graph for the rest of the run, worst for a solo low-WIS character who quietly loses routes they never learn existed. Three changes, one theme: a die roll should not be able to permanently wall a player. Party's best stat answers the check. evaluateEdgeLock read only the acting character's mods, which made a party's rogue and its hired scout decorative at every lock. Fold the whole roster — Pete included, since excluding him would make hiring a scout worth less than the coins it costs — and credit whoever got it open in the fork menu. Thieves' tools as the escape hatch. A utility item (not a ConsumableDef, or the fight engine would spend them for you) sold on Luigi's supplies shelf, consumed by `!zone unlock <n>`. Deliberately not a skeleton key: tools answer the two dice-driven locks only. A key lock is a quest token, a level-min lock is progression, a region-clear lock is structure — none of those are "you rolled badly", so none of them are pickable. Autopilot picks a route instead of parking. The fork timeout was 8h, which reads as "the player gets first say" and behaves as "the expedition stops for a third of a day, at every fork" — a multi-day expedition crosses a lot of forks. 30m keeps a genuine first say for anyone at the keyboard. It now ranks by unvisited-then-edge-weight rather than taking whatever the graph author happened to list first, spends tools when every route is locked, and backtracks a room when it can't do even that, rather than idling into the 24h reaper and losing the player days of progress to a roll they never saw. Sim A/B, same seeds, 90 runs/arm: 43.3% -> 42.2% clear, a single run flipping and well inside the documented noise floor. A party+companion+pet arm runs 31/32 clean through the new roster-folding path.
This commit is contained in:
@@ -1019,6 +1019,9 @@ func luigiSuppliesView(_ id.UserID, balance float64) string {
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("**%s** — €%d\n Opens one dungeon path a failed check closed (`!zone unlock <n>`). Not used in combat.\n\n",
|
||||
thievesToolsName, thievesToolsPrice))
|
||||
|
||||
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()
|
||||
@@ -1073,6 +1076,13 @@ func (p *AdventurePlugin) resolveShopSupplyChoice(ctx MessageContext, interactio
|
||||
return p.SendDM(ctx.Sender, "*Luigi nods and gestures toward the main counter.*")
|
||||
}
|
||||
|
||||
// Thieves' tools sit on the supplies shelf but are not a ConsumableDef:
|
||||
// the combat engine scans inventory against that table and would happily
|
||||
// spend them mid-fight. They get their own branch and their own item type.
|
||||
if containsFold(thievesToolsName, reply) || containsFold("thieves tools", reply) {
|
||||
return p.buyThievesTools(ctx, interaction)
|
||||
}
|
||||
|
||||
// Find matching consumable
|
||||
var match *ConsumableDef
|
||||
for i := range consumableDefs {
|
||||
@@ -1115,6 +1125,34 @@ func (p *AdventurePlugin) resolveShopSupplyChoice(ctx MessageContext, interactio
|
||||
match.Name, consumablePrice, newBalance))
|
||||
}
|
||||
|
||||
// buyThievesTools sells one set off the supplies shelf. Mirrors the consumable
|
||||
// purchase beside it — same session price factor, same 5% pot cut — and leaves
|
||||
// the player in the supplies view so they can buy a second.
|
||||
func (p *AdventurePlugin) buyThievesTools(ctx MessageContext, interaction *advPendingInteraction) error {
|
||||
price := float64(thievesToolsPrice) * p.shopSessionPriceFactor(ctx.Sender)
|
||||
balance := p.euro.GetBalance(ctx.Sender)
|
||||
if balance < price {
|
||||
p.pending.Store(string(ctx.Sender), interaction)
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("You need €%.0f for %s but only have €%.0f.",
|
||||
price, thievesToolsName, balance))
|
||||
}
|
||||
p.euro.Debit(ctx.Sender, price, "shop_thieves_tools")
|
||||
if potCut := int(math.Round(price * 0.05)); potCut > 0 {
|
||||
communityPotAdd(potCut)
|
||||
trackTaxPaid(ctx.Sender, potCut)
|
||||
}
|
||||
_ = addAdvInventoryItem(ctx.Sender, AdvItem{
|
||||
Name: thievesToolsName,
|
||||
Type: thievesToolsItemType,
|
||||
Tier: 1,
|
||||
Value: thievesToolsPrice / 2,
|
||||
})
|
||||
p.pending.Store(string(ctx.Sender), interaction)
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"Purchased **%s** for €%.0f. You carry %d.\n💰 Balance: €%.0f\n\nReply with another item name or `back` to return.",
|
||||
thievesToolsName, price, countThievesTools(ctx.Sender), p.euro.GetBalance(ctx.Sender)))
|
||||
}
|
||||
|
||||
// ── Curios (Magic Items) ────────────────────────────────────────────────────
|
||||
|
||||
// curiosStockSize — how many registry magic items Luigi stocks per day.
|
||||
|
||||
Reference in New Issue
Block a user