mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Adv 2.0 D&D layer: Phases 1-8 + Phase 9 SP1+SP2
Combines all uncommitted D&D work into one checkpoint: Phases 1-8 (per gogobee_dnd_session_summary.md): - Race/class/stats system, !setup flow, !sheet, !respec - d20-vs-AC combat with race/class passives, auto-migration - XP/level-up, skill checks, NPC refund hooks - Equipment slot mapping, rarity, !rest short/long - Pre-arm active abilities, resource pools - Bestiary, !roll/!stats/!level, onboarding DM - Audit fixes A-I, flavor wiring under internal/flavor/ - Phase 8 weapon dice + armor AC tables (gogobee_equipment_appendix) Phase 9 SP1 — spell system foundations: - 79 SpellDefinitions (76 in-scope + 3 reaction-deferred) - dnd_known_spells, dnd_spell_slots tables - pending_cast / concentration_* columns on dnd_character - Slot tables for Mage/Cleric/Ranger, DC + attack-bonus math - Long-rest refreshes slots; respec wipes spell state - Auto-grant default known list on !setup confirm and auto-migration Phase 9 SP2 — !cast / !spells / !prepare commands: - Out-of-combat HEAL and UTILITY resolve immediately - Damage/control/buff queue as pending_cast for next combat - Audit-style save-then-debit, slot refund on save failure - !cast --drop, concentration supersession, prep-cap enforcement - Reaction spells refused with Phase 11 note SP3 (combat-time resolution of pending_cast) and SP4 (full prep/learn tests) are next. Build clean, full test suite green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package plugin
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
|
||||
"gogobee/internal/db"
|
||||
"maunium.net/go/mautrix/id"
|
||||
@@ -241,20 +242,25 @@ var advAllTreasures = map[int][]AdvTreasureDef{
|
||||
|
||||
// ── Treasure Drop Logic ──────────────────────────────────────────────────────
|
||||
|
||||
func rollAdvTreasureDrop(tier int, userID id.UserID, chatLevel int) *AdvTreasureDrop {
|
||||
rate, ok := advTreasureDropRates[tier]
|
||||
// rollAdvTreasureDropDetailed returns the drop (or nil) plus the random roll
|
||||
// and the effective drop rate, so callers can surface near-miss feedback
|
||||
// ("rolled 1.8% vs 1.5% chance — just missed"). Players never see treasure
|
||||
// math otherwise, which makes rare drops feel mythical.
|
||||
func rollAdvTreasureDropDetailed(tier int, userID id.UserID, chatLevel int) (drop *AdvTreasureDrop, roll, rate float64) {
|
||||
r, ok := advTreasureDropRates[tier]
|
||||
if !ok {
|
||||
return nil
|
||||
return nil, 0, 0
|
||||
}
|
||||
rate += chatLevelRareBonus(chatLevel)
|
||||
rate = r + chatLevelRareBonus(chatLevel)
|
||||
roll = rand.Float64()
|
||||
|
||||
if rand.Float64() >= rate {
|
||||
return nil
|
||||
if roll >= rate {
|
||||
return nil, roll, rate
|
||||
}
|
||||
|
||||
pool, ok := advAllTreasures[tier]
|
||||
if !ok || len(pool) == 0 {
|
||||
return nil
|
||||
return nil, roll, rate
|
||||
}
|
||||
|
||||
// Pick random treasure
|
||||
@@ -267,11 +273,68 @@ func rollAdvTreasureDrop(tier int, userID id.UserID, chatLevel int) *AdvTreasure
|
||||
def = &pool[rand.IntN(len(pool))]
|
||||
owns, err = advUserOwnsTreasure(userID, def.Key)
|
||||
if err != nil || owns {
|
||||
return nil // both rolls duplicated
|
||||
return nil, roll, rate // both rolls duplicated
|
||||
}
|
||||
}
|
||||
|
||||
return &AdvTreasureDrop{Def: def}
|
||||
return &AdvTreasureDrop{Def: def}, roll, rate
|
||||
}
|
||||
|
||||
// rollAdvTreasureDrop is the legacy single-return variant used by call sites
|
||||
// that don't surface near-miss feedback (auto-babysit, twinbee shares).
|
||||
func rollAdvTreasureDrop(tier int, userID id.UserID, chatLevel int) *AdvTreasureDrop {
|
||||
d, _, _ := rollAdvTreasureDropDetailed(tier, userID, chatLevel)
|
||||
return d
|
||||
}
|
||||
|
||||
// ── Treasure Comparison ─────────────────────────────────────────────────────
|
||||
|
||||
// advTreasureIrreplaceable reports whether a treasure carries a bonus type
|
||||
// that can't be replicated by another drop (e.g. monthly death bypass).
|
||||
// Such treasures are excluded from auto-swap and fall back to the manual
|
||||
// discard prompt so the player consciously chooses to give one up.
|
||||
func advTreasureIrreplaceable(def *AdvTreasureDef) bool {
|
||||
if def == nil {
|
||||
return false
|
||||
}
|
||||
for _, b := range def.Bonuses {
|
||||
if strings.HasPrefix(b.Type, "special_") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// advTreasureRank produces a comparable triple (tier, bonusCount, key) for
|
||||
// auto-swap decisions. Bonuses are heterogeneous and there's no honest
|
||||
// scalar comparator — we use tier first, then bonus count as a tiebreaker,
|
||||
// then the deterministic key so equal-rank ties prefer the existing item
|
||||
// (no churn). Higher tuple = better treasure.
|
||||
type advTreasureRankKey struct {
|
||||
Tier int
|
||||
BonusCount int
|
||||
Key string
|
||||
}
|
||||
|
||||
func advTreasureRank(def *AdvTreasureDef) advTreasureRankKey {
|
||||
if def == nil {
|
||||
return advTreasureRankKey{}
|
||||
}
|
||||
return advTreasureRankKey{Tier: def.Tier, BonusCount: len(def.Bonuses), Key: def.Key}
|
||||
}
|
||||
|
||||
// advTreasureRankBetter reports whether a is strictly better than b.
|
||||
// Equal-rank returns false (caller treats this as "keep existing").
|
||||
func advTreasureRankBetter(a, b advTreasureRankKey) bool {
|
||||
if a.Tier != b.Tier {
|
||||
return a.Tier > b.Tier
|
||||
}
|
||||
if a.BonusCount != b.BonusCount {
|
||||
return a.BonusCount > b.BonusCount
|
||||
}
|
||||
// Deterministic but neutral tiebreak — different keys aren't "better"
|
||||
// than each other, so equal Tier+BonusCount means no swap.
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Treasure DB Operations ───────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user