mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
adventure: validate race/class + clamp level in admin char-migrate
Code-review fixes on the stuck-adventurer minting path: - AdminBuildConfirmedCharacter now runs race/class through parseRace/ parseClass (same as !setup), so a typo or non-playable class errors instead of silently minting a 1-HP/AC-10/no-spell sheet; inputs are normalized too. - Clamp level to dndMaxLevel to match the L20 cap enforced elsewhere. - CLI parses/validates every spec before db.Init, so a malformed spec mid-batch no longer leaves earlier specs already committed.
This commit is contained in:
@@ -40,16 +40,31 @@ func main() {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Init(*dataDir); err != nil {
|
// Parse every spec before touching the DB, so a malformed spec in the
|
||||||
log.Fatalf("db init: %v", err)
|
// middle of the batch fails fast instead of leaving the earlier specs
|
||||||
|
// already committed to gogobee.db.
|
||||||
|
type charSpec struct {
|
||||||
|
uid id.UserID
|
||||||
|
race plugin.DnDRace
|
||||||
|
class plugin.DnDClass
|
||||||
|
level int
|
||||||
}
|
}
|
||||||
|
specs := make([]charSpec, 0, flag.NArg())
|
||||||
for _, spec := range flag.Args() {
|
for _, spec := range flag.Args() {
|
||||||
uid, race, class, level, err := parseSpec(spec)
|
uid, race, class, level, err := parseSpec(spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("bad spec %q: %v", spec, err)
|
log.Fatalf("bad spec %q: %v", spec, err)
|
||||||
}
|
}
|
||||||
c, err := plugin.AdminBuildConfirmedCharacter(uid, race, class, level)
|
specs = append(specs, charSpec{uid, race, class, level})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Init(*dataDir); err != nil {
|
||||||
|
log.Fatalf("db init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range specs {
|
||||||
|
uid := s.uid
|
||||||
|
c, err := plugin.AdminBuildConfirmedCharacter(s.uid, s.race, s.class, s.level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("build %s: %v", uid, err)
|
log.Fatalf("build %s: %v", uid, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,25 @@ import (
|
|||||||
// SaveDnDCharacter upserts on user_id, so an existing pending draft is replaced.
|
// SaveDnDCharacter upserts on user_id, so an existing pending draft is replaced.
|
||||||
// initResources / ensureSpellsForCharacter are idempotent.
|
// initResources / ensureSpellsForCharacter are idempotent.
|
||||||
func AdminBuildConfirmedCharacter(userID id.UserID, race DnDRace, class DnDClass, level int) (*DnDCharacter, error) {
|
func AdminBuildConfirmedCharacter(userID id.UserID, race DnDRace, class DnDClass, level int) (*DnDCharacter, error) {
|
||||||
|
// Validate (and normalize) race/class through the same parsers !setup uses,
|
||||||
|
// so a typo or a non-playable class fails loudly instead of silently minting
|
||||||
|
// a broken sheet — an unknown class falls through classInfo to 1 HP / AC 10
|
||||||
|
// / no spells, which looks "built" but isn't.
|
||||||
|
r, ok := parseRace(string(race))
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown race %q", race)
|
||||||
|
}
|
||||||
|
cl, ok := parseClass(string(class))
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown or non-playable class %q", class)
|
||||||
|
}
|
||||||
|
race, class = r, cl
|
||||||
if level < 1 {
|
if level < 1 {
|
||||||
level = 1
|
level = 1
|
||||||
}
|
}
|
||||||
|
if level > dndMaxLevel {
|
||||||
|
level = dndMaxLevel
|
||||||
|
}
|
||||||
scores := applyRaceMods(race, classStatPriority(class))
|
scores := applyRaceMods(race, classStatPriority(class))
|
||||||
c := &DnDCharacter{
|
c := &DnDCharacter{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
|||||||
Reference in New Issue
Block a user