mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
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>
118 lines
4.5 KiB
Go
118 lines
4.5 KiB
Go
package plugin
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"gogobee/internal/flavor"
|
|
)
|
|
|
|
// D&D-layer flavor wire-in. Helpers here pull from the protected pools in
|
|
// internal/flavor and are called at the relevant integration points
|
|
// (combat outputs, level-up DM, rest commands, NPC encounters, loot drops).
|
|
//
|
|
// Each helper returns "" when the corresponding pool is empty so callers can
|
|
// safely embed the result without conditional-noise everywhere.
|
|
|
|
// ── Combat narrative ─────────────────────────────────────────────────────────
|
|
|
|
// dndCombatOpeningLine returns a single CombatStart pool line for use at
|
|
// the top of a fight's combat-log render. Caller decides where to inject it
|
|
// (typically prepended to the first phase message).
|
|
func dndCombatOpeningLine() string {
|
|
return flavor.Pick(flavor.CombatStart)
|
|
}
|
|
|
|
// dndCombatClosingLine returns a single line appropriate for the fight's
|
|
// end state: CombatVictory on a non-boss win, BossDeath on a boss kill,
|
|
// PlayerDeath on a loss.
|
|
func dndCombatClosingLine(playerWon, isBoss bool) string {
|
|
switch {
|
|
case playerWon && isBoss:
|
|
return flavor.Pick(flavor.BossDeath)
|
|
case playerWon:
|
|
return flavor.Pick(flavor.CombatVictory)
|
|
default:
|
|
return flavor.Pick(flavor.PlayerDeath)
|
|
}
|
|
}
|
|
|
|
// dndZoneCompleteLine fires on dungeon completion (only on victorious clear).
|
|
func dndZoneCompleteLine() string {
|
|
return flavor.Pick(flavor.ZoneComplete)
|
|
}
|
|
|
|
// dndNat20Line / dndNat1Line — appended to the fight's roll summary when
|
|
// at least one nat 20 / nat 1 was rolled by the player. A single line per
|
|
// fight (not per roll) to avoid spam.
|
|
func dndNat20Line() string {
|
|
return flavor.Pick(flavor.Nat20)
|
|
}
|
|
|
|
func dndNat1Line() string {
|
|
return flavor.Pick(flavor.Nat1)
|
|
}
|
|
|
|
// ── Level-up + items ────────────────────────────────────────────────────────
|
|
|
|
// dndLevelUpFlavorLine — random LevelUp pool entry for the level-up DM.
|
|
func dndLevelUpFlavorLine() string {
|
|
return flavor.Pick(flavor.LevelUp)
|
|
}
|
|
|
|
// dndItemFoundLine — used when loot drops in dungeon resolution.
|
|
func dndItemFoundLine() string {
|
|
return flavor.Pick(flavor.ItemFound)
|
|
}
|
|
|
|
// ── Rest ─────────────────────────────────────────────────────────────────────
|
|
|
|
// dndRestShortFlavorLine — for !rest short.
|
|
func dndRestShortFlavorLine() string {
|
|
return flavor.Pick(flavor.RestShort)
|
|
}
|
|
|
|
// dndRestLongFlavorLine — for !rest long. The HomeLongRest pool is
|
|
// preferred when the player is at home; falls back to generic RestLong.
|
|
func dndRestLongFlavorLine(atHome bool) string {
|
|
if atHome {
|
|
if line := flavor.Pick(flavor.HomeLongRest); line != "" {
|
|
return line
|
|
}
|
|
}
|
|
return flavor.Pick(flavor.RestLong)
|
|
}
|
|
|
|
// ── NPC greetings ───────────────────────────────────────────────────────────
|
|
//
|
|
// The NPC encounter system already has legacy `mistyOpenings` and
|
|
// `arinaOpenings` slices used for prompt openings. We don't replace those —
|
|
// we union them with the new D&D-flavored greetings so players see ~2x
|
|
// variety in the openings. Helpers below return the combined pool.
|
|
|
|
// dndMistyGreetingPool returns the union of legacy and D&D pools for Misty.
|
|
// Used by the encounter-fire path to vary opening lines.
|
|
func dndMistyGreetingPool() []string {
|
|
combined := make([]string, 0, len(flavor.MistyGreeting)+len(mistyOpenings))
|
|
combined = append(combined, flavor.MistyGreeting...)
|
|
combined = append(combined, mistyOpenings...)
|
|
return combined
|
|
}
|
|
|
|
func dndArinaGreetingPool() []string {
|
|
combined := make([]string, 0, len(flavor.ArinaGreeting)+len(arinaOpenings))
|
|
combined = append(combined, flavor.ArinaGreeting...)
|
|
combined = append(combined, arinaOpenings...)
|
|
return combined
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
// dndItalicize wraps a non-empty line in markdown italics with leading
|
|
// blank line so callers can append narrative cleanly.
|
|
func dndItalicize(line string) string {
|
|
if strings.TrimSpace(line) == "" {
|
|
return ""
|
|
}
|
|
return "\n\n_" + line + "_"
|
|
}
|