aboutsummaryrefslogtreecommitdiff
path: root/internal/validate
diff options
context:
space:
mode:
Diffstat (limited to 'internal/validate')
-rw-r--r--internal/validate/checks.go59
-rw-r--r--internal/validate/grid_test.go14
-rw-r--r--internal/validate/mob_attacktype_test.go31
3 files changed, 95 insertions, 9 deletions
diff --git a/internal/validate/checks.go b/internal/validate/checks.go
index a8f973f..ccf996c 100644
--- a/internal/validate/checks.go
+++ b/internal/validate/checks.go
@@ -7,6 +7,7 @@ import (
"thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/combat"
"thehouseoficarus/internal/world"
)
@@ -292,6 +293,57 @@ func validateRoomObjectCollisions(roomID int, entries []roomObjEntry) []Issue {
return issues
}
+// validateMobAttackTypes enforces the mob attack_types spec: a non-empty list
+// containing exactly one melee type (stab/slash/crush) plus at most one each of
+// the optional ranged/science types, with no invalid or duplicate entries.
+func validateMobAttackTypes(id string, types []string) []Issue {
+ var issues []Issue
+
+ if len(types) == 0 {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("Mob %q: missing attack_types (must be a list with exactly one of stab/slash/crush, optionally plus ranged/science)", id),
+ })
+ return issues
+ }
+
+ meleeCount := 0
+ seen := map[string]bool{}
+ for _, t := range types {
+ if !combat.IsValidAttackType(t) {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("Mob %q: invalid attack_types entry %q (must be stab/slash/crush/ranged/science)", id, t),
+ })
+ continue
+ }
+ if seen[t] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("Mob %q: duplicate attack_types entry %q", id, t),
+ })
+ continue
+ }
+ seen[t] = true
+ if combat.IsMeleeType(t) {
+ meleeCount++
+ }
+ }
+
+ if meleeCount != 1 {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("Mob %q: attack_types must contain exactly one melee type (stab/slash/crush), found %d", id, meleeCount),
+ })
+ }
+
+ return issues
+}
+
func validateMobs(s Source) []Issue {
var issues []Issue
itemIDs := s.Items.IDSet()
@@ -336,6 +388,10 @@ func validateMobs(s Source) []Issue {
})
}
+ if def.Combat != nil && def.Combat.Kind != "task" {
+ issues = append(issues, validateMobAttackTypes(id, def.Combat.AttackTypes)...)
+ }
+
if def.Drops.Remains != "" && !itemIDs[def.Drops.Remains] {
issues = append(issues, Issue{
Level: "ERROR",
@@ -427,7 +483,6 @@ func validateMobs(s Source) []Issue {
func validateHazards(s Source) []Issue {
var issues []Issue
itemIDs := s.Items.IDSet()
- validTypes := map[string]bool{"stab": true, "slash": true, "crush": true, "ranged": true, "science": true}
for id := range s.World.HazardIndex() {
def, err := s.World.LoadHazard(id)
@@ -439,7 +494,7 @@ func validateHazards(s Source) []Issue {
})
continue
}
- if def.AttackType != "" && !validTypes[def.AttackType] {
+ if def.AttackType != "" && !combat.IsValidAttackType(def.AttackType) {
issues = append(issues, Issue{
Level: "ERROR",
Type: "reference",
diff --git a/internal/validate/grid_test.go b/internal/validate/grid_test.go
index 716cec1..e40c50b 100644
--- a/internal/validate/grid_test.go
+++ b/internal/validate/grid_test.go
@@ -141,13 +141,13 @@ func TestGrid3DTwistUpDown(t *testing.T) {
// path2: 1 east→A(1,0,0) up→B(1,0,1) east→C(2,0,1) south→4 wants (2,1,1)
// 4 already at (1,1,1) from path1 → twist.
rooms := map[int]string{
- 1: "exits:\n up: 2\n east: 6\n",
- 2: "exits:\n south: 3\n down: 1\n",
- 3: "exits:\n east: 4\n north: 2\n",
- 4: "name: four\n",
- 6: "exits:\n up: 7\n west: 1\n",
- 7: "exits:\n east: 8\n down: 6\n",
- 8: "exits:\n south: 4\n west: 7\n",
+ 1: "exits:\n up: 2\n east: 6\n",
+ 2: "exits:\n south: 3\n down: 1\n",
+ 3: "exits:\n east: 4\n north: 2\n",
+ 4: "name: four\n",
+ 6: "exits:\n up: 7\n west: 1\n",
+ 7: "exits:\n east: 8\n down: 6\n",
+ 8: "exits:\n south: 4\n west: 7\n",
}
issues := runGridCheck(t, rooms, 1)
if !containsMsg(issues, "Grid twist") {
diff --git a/internal/validate/mob_attacktype_test.go b/internal/validate/mob_attacktype_test.go
new file mode 100644
index 0000000..1464d3f
--- /dev/null
+++ b/internal/validate/mob_attacktype_test.go
@@ -0,0 +1,31 @@
+package validate
+
+import "testing"
+
+func TestValidateMobAttackTypes(t *testing.T) {
+ cases := []struct {
+ name string
+ types []string
+ wantError bool
+ }{
+ {"single melee", []string{"crush"}, false},
+ {"melee plus ranged", []string{"stab", "ranged"}, false},
+ {"melee plus both", []string{"slash", "ranged", "science"}, false},
+ {"empty", nil, true},
+ {"no melee", []string{"ranged"}, true},
+ {"two melee", []string{"stab", "crush"}, true},
+ {"invalid entry", []string{"crush", "magic"}, true},
+ {"duplicate", []string{"crush", "ranged", "ranged"}, true},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ issues := validateMobAttackTypes("test_mob", c.types)
+ if c.wantError && len(issues) == 0 {
+ t.Errorf("validateMobAttackTypes(%v): expected error, got none", c.types)
+ }
+ if !c.wantError && len(issues) != 0 {
+ t.Errorf("validateMobAttackTypes(%v): expected no error, got %v", c.types, issues)
+ }
+ })
+ }
+}