mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
UX S7: sheet + menu jargon sweep — outcome-first copy
Drops D&D-handbook syntax from the player-facing caster UX in favor of
verbs and feelings (per feedback_accessibility_over_dnd_crunch).
R12 — Drop "Spell DC: N / Spell Atk: +N" line from the spellbook view.
The numbers are pure handbook noise; the engine still computes them.
R13 — renderSlotLine swaps "L1 3/4 · L2 1/3" for "Level 1 ●●●○ · Level 2
●○○○". Filled bullets = energy left, hollow = spent.
R14 — Caster passive Description strings rewritten outcome-first: no
"+5%", no "scaled by your Charisma". Verbs and texture instead. All ten
class passives reworded; mechanics in applyClassPassives unchanged.
R18 — Class menu drops the "(d8, INT/WIS)" suffix → "**Mage** — INT &
WIS". The HP-die number was leaking implementation. !setup class confirm
line gets the same treatment.
P5 — Spellbook headers: "Cantrip" → "Cantrips", "L1" → "Level 1".
P6 — Cast queue line: "_(upcast to L2)_" → "_(empowered)_". Queued-line
in the spellbook view follows suit — drops "(L2 slot)" for "(empowered)"
when the slot is above the spell's base level.
P8 — Comment-convention marker added to dnd_passives.go and
dnd_subclass_combat.go file headers: `// internal note (not user-facing)`
flags Phase 2/3 tuning history so future codegen doesn't lift it into
Description / Flavor strings.
P9 — Spellbook line drops the duplicate "(can learn N more)" trailer —
the "%d / %d" already conveys remaining capacity.
Bonus cleanup. The two Arcane-Trickster slot-ceiling errors ("Arcane
Trickster L5 only has up to L1 slots.") and the "No L1 slot available"
out-of-energy messages get the same jargon scrub: "At level 5, your
Arcane Trickster magic only reaches level-1 spells." / "You're out of
level-1 energy."
Tests. Full plugin suite green; no test pinned the old strings.
This commit is contained in:
@@ -336,7 +336,7 @@ func parseCombatCast(userID id.UserID, c *DnDCharacter, args string) (SpellDefin
|
||||
}
|
||||
if c.Class == ClassRogue && c.Subclass == SubclassArcaneTrickster {
|
||||
if mx := highestAvailableSlotForChar(c); spell.Level > mx {
|
||||
return SpellDefinition{}, 0, fmt.Sprintf("Arcane Trickster L%d only has up to L%d slots.", c.Level, mx)
|
||||
return SpellDefinition{}, 0, fmt.Sprintf("At level %d, your Arcane Trickster magic only reaches level-%d spells.", c.Level, mx)
|
||||
}
|
||||
}
|
||||
known, prepared, err := playerKnowsSpell(userID, spell.ID)
|
||||
@@ -473,7 +473,7 @@ func (p *AdventurePlugin) chargeSpellCost(userID id.UserID, spell SpellDefinitio
|
||||
return "Couldn't consume slot: " + serr.Error()
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Sprintf("No L%d slot available. %s", slotLevel, renderSlotsBrief(userID))
|
||||
return fmt.Sprintf("You're out of level-%d energy. %s", slotLevel, renderSlotsBrief(userID))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
@@ -147,7 +147,7 @@ func (p *AdventurePlugin) handleDnDCastCmd(ctx MessageContext, args string) erro
|
||||
if c.Class == ClassRogue && c.Subclass == SubclassArcaneTrickster {
|
||||
if max := highestAvailableSlotForChar(c); spell.Level > max {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"Arcane Trickster L%d only has up to L%d slots.", c.Level, max))
|
||||
"At level %d, your Arcane Trickster magic only reaches level-%d spells.", c.Level, max))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ func (p *AdventurePlugin) handleDnDCastCmd(ctx MessageContext, args string) erro
|
||||
}
|
||||
if !ok {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"No L%d slot available. %s", slotLevel, renderSlotsBrief(ctx.Sender)))
|
||||
"You're out of level-%d energy. %s", slotLevel, renderSlotsBrief(ctx.Sender)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ func (p *AdventurePlugin) queuePendingCast(ctx MessageContext, c *DnDCharacter,
|
||||
|
||||
msg := fmt.Sprintf("✨ **%s** queued for next combat.", spell.Name)
|
||||
if slotLevel > spell.Level {
|
||||
msg += fmt.Sprintf(" _(upcast to L%d)_", slotLevel)
|
||||
msg += " _(empowered)_"
|
||||
}
|
||||
if supersedes != "" {
|
||||
msg += fmt.Sprintf("\n_(Concentration shifts: %s ended.)_", displaySpellName(supersedes))
|
||||
@@ -438,9 +438,9 @@ func renderSpellsList(c *DnDCharacter) string {
|
||||
}
|
||||
sort.Ints(levels)
|
||||
for _, lvl := range levels {
|
||||
label := fmt.Sprintf("L%d", lvl)
|
||||
label := fmt.Sprintf("Level %d", lvl)
|
||||
if lvl == 0 {
|
||||
label = "Cantrip"
|
||||
label = "Cantrips"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("__%s__\n", label))
|
||||
for _, k := range byLevel[lvl] {
|
||||
@@ -460,23 +460,17 @@ func renderSpellsList(c *DnDCharacter) string {
|
||||
|
||||
slots, _ := getSpellSlots(c.UserID)
|
||||
b.WriteString("\n**Slots:** " + renderSlotLine(slots) + "\n")
|
||||
b.WriteString(fmt.Sprintf("**Spell DC:** %d **Spell Atk:** +%d\n",
|
||||
spellSaveDC(c), spellAttackBonus(c)))
|
||||
|
||||
if c.Class == ClassMage {
|
||||
count, _ := mageLeveledKnownCount(c.UserID)
|
||||
cap := mageKnownSpellsCap(c.Level)
|
||||
b.WriteString(fmt.Sprintf("**Spellbook:** %d/%d leveled spells", count, cap))
|
||||
if avail := cap - count; avail > 0 {
|
||||
b.WriteString(fmt.Sprintf(" _(can learn %d more)_", avail))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf("**Spellbook:** %d / %d leveled spells learned\n", count, cap))
|
||||
}
|
||||
|
||||
if pc, ok := decodePendingCast(c.PendingCast); ok {
|
||||
b.WriteString(fmt.Sprintf("\n_Queued: **%s**", displaySpellName(pc.SpellID)))
|
||||
if pc.SlotLevel > 0 {
|
||||
b.WriteString(fmt.Sprintf(" (L%d slot)", pc.SlotLevel))
|
||||
if s, ok := lookupSpell(pc.SpellID); ok && pc.SlotLevel > s.Level {
|
||||
b.WriteString(" (empowered)")
|
||||
}
|
||||
b.WriteString(" — fires next combat._\n")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ package plugin
|
||||
// (the combat engine is one-shot, no turn-based UI). Active/reactive
|
||||
// abilities arrive once we have a model for them — likely tied to !arena
|
||||
// pre-arming or to a turn-based PvP variant.
|
||||
//
|
||||
// Comment convention: `// internal note (not user-facing)` marks block
|
||||
// comments that document tuning history (Phase 2/3 balance notes, scaling
|
||||
// rationale, etc.). Anything inside such a block is engineering context,
|
||||
// NOT a candidate for codegen to lift into a Description string.
|
||||
|
||||
// ── Class passive definitions ────────────────────────────────────────────────
|
||||
|
||||
@@ -21,45 +26,43 @@ type DnDClassAbility struct {
|
||||
var dndClassAbilities = map[DnDClass]DnDClassAbility{
|
||||
ClassFighter: {
|
||||
Name: "Battle Trained",
|
||||
Description: "Years of weapon drill add +5% to all damage you deal.",
|
||||
Description: "Years of weapon drill make every swing hit a little harder than it has any right to.",
|
||||
},
|
||||
ClassRogue: {
|
||||
Name: "Sneak Attack",
|
||||
Description: "Your first strike each combat lands as a critical hit, doubling its damage; precision drills add +5% to all damage you deal.",
|
||||
Description: "Your opening strike each fight finds a seam and crits — and the same trained precision keeps every follow-up sharper than it looks.",
|
||||
},
|
||||
ClassMage: {
|
||||
Name: "Arcane Focus",
|
||||
Description: "Practiced channeling adds +1 to your attack rolls and a small bump (+5%) to all damage you deal.",
|
||||
Description: "Practiced channeling steadies your aim and lets a faint arcane crackle leak into every blow you land.",
|
||||
},
|
||||
ClassCleric: {
|
||||
Name: "Divine Favor",
|
||||
Description: "When you fall below half HP, divine intervention restores 5 HP. Once per combat.",
|
||||
Description: "When the fight turns against you, a sliver of divine grace patches you up. Once per combat.",
|
||||
},
|
||||
ClassRanger: {
|
||||
Name: "Hunter's Mark",
|
||||
Description: "You read your prey's weak points: +5% damage and +1 to attack rolls.",
|
||||
Description: "You read your prey's tells — openings come easier, and the hits land where it hurts.",
|
||||
},
|
||||
// Open5e caster scaffold — one signature passive each, riding existing
|
||||
// CombatModifiers channels the same way the original five do.
|
||||
ClassDruid: {
|
||||
Name: "Wild Resilience",
|
||||
Description: "The wild lends you its toughness — incoming damage is reduced 5% — and a thorn surge on engage scaled by your Wisdom.",
|
||||
Description: "The wild lends you its hide — blows land softer — and the moment you engage, a thorn surge lashes back at whatever picked the fight.",
|
||||
},
|
||||
ClassBard: {
|
||||
Name: "Bardic Inspiration",
|
||||
Description: "Quick wit keeps you a step ahead: +1 initiative, +1 attack, and +5% to all damage you deal.",
|
||||
Description: "Wit and timing put you a beat ahead of the fight: you move first, swing truer, and turn the opening line into a small, mean encore.",
|
||||
},
|
||||
ClassSorcerer: {
|
||||
Name: "Innate Sorcery",
|
||||
Description: "Raw magic spills out as the fight begins, dealing immediate damage scaled by your Charisma, and +5% to all damage you deal.",
|
||||
Description: "Raw magic boils over the moment the fight starts, scorching whatever's nearest — and every swing rides the leftover heat.",
|
||||
},
|
||||
ClassWarlock: {
|
||||
Name: "Agonizing Blast",
|
||||
Description: "Your pact-fueled eldritch power adds +12% to all damage you deal and +1 to attack rolls.",
|
||||
Description: "Your pact whispers in the back of your skull; the favor it returns lands on your enemies, harder and surer than ought to be allowed.",
|
||||
},
|
||||
ClassPaladin: {
|
||||
Name: "Divine Smite",
|
||||
Description: "You channel a burst of radiant power on engagement, dealing flat damage that grows with your level.",
|
||||
Description: "On engagement you channel a burst of radiant power — a sworn promise, paid in light — that hits harder as your oath deepens.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -207,11 +207,11 @@ func (p *AdventurePlugin) dndSetupClass(ctx MessageContext, classArg string) err
|
||||
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error())
|
||||
}
|
||||
ci, _ := classInfo(cl)
|
||||
return p.SendDM(ctx.Sender,fmt.Sprintf("Class set: **%s** (HP die d%d, primary %s/%s).\n\n"+
|
||||
return p.SendDM(ctx.Sender,fmt.Sprintf("Class set: **%s** — leans on %s & %s.\n\n"+
|
||||
"Next: assign your stats. The standard array is **15 14 13 12 10 8** — six numbers, each used once.\n"+
|
||||
"Order: STR DEX CON INT WIS CHA.\n\n"+
|
||||
"Example: `!setup stats 15 14 13 12 10 8` (all rolled into STR-first; rearrange to taste).",
|
||||
ci.Display, ci.HPDie, ci.PrimaryA, ci.PrimaryB))
|
||||
ci.Display, ci.PrimaryA, ci.PrimaryB))
|
||||
}
|
||||
|
||||
func (p *AdventurePlugin) dndSetupStats(ctx MessageContext, statsArg string) error {
|
||||
@@ -576,7 +576,7 @@ func renderClassMenu() string {
|
||||
if !ci.Playable {
|
||||
continue // Open5e caster scaffold — hidden until spell lists land.
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" • **%s** (d%d, %s/%s)\n", ci.Display, ci.HPDie, ci.PrimaryA, ci.PrimaryB))
|
||||
b.WriteString(fmt.Sprintf(" • **%s** — %s & %s\n", ci.Display, ci.PrimaryA, ci.PrimaryB))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -952,9 +952,19 @@ func renderSlotLine(slots map[int][2]int) string {
|
||||
}
|
||||
var parts []string
|
||||
for lvl := 1; lvl <= 5; lvl++ {
|
||||
if pair, ok := slots[lvl]; ok {
|
||||
parts = append(parts, fmt.Sprintf("L%d %d/%d", lvl, pair[0]-pair[1], pair[0]))
|
||||
pair, ok := slots[lvl]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
total := pair[0]
|
||||
left := pair[0] - pair[1]
|
||||
if left < 0 {
|
||||
left = 0
|
||||
}
|
||||
// Filled bullets = still available, hollow = spent. Player-friendly
|
||||
// at a glance; no fractions, no "L1" jargon.
|
||||
bullets := strings.Repeat("●", left) + strings.Repeat("○", total-left)
|
||||
parts = append(parts, fmt.Sprintf("Level %d %s", lvl, bullets))
|
||||
}
|
||||
return strings.Join(parts, " · ")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ import "math"
|
||||
// Subclass active abilities (currently just Berserker's rage) are
|
||||
// registered via init() into dndActiveAbilities below, gated by
|
||||
// DnDAbility.Subclass.
|
||||
//
|
||||
// Comment convention: `// internal note (not user-facing)` marks block
|
||||
// comments documenting tuning history (Phase 2/3 balance notes, scaling
|
||||
// rationale, etc.). They are engineering context and must NOT be lifted
|
||||
// into Description / Flavor strings by codegen.
|
||||
|
||||
func applySubclassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacter) {
|
||||
if c == nil || c.Subclass == "" {
|
||||
|
||||
Reference in New Issue
Block a user