diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/render_help.go | 91 | ||||
| -rw-r--r-- | internal/game/render_help_test.go | 81 |
2 files changed, 163 insertions, 9 deletions
diff --git a/internal/game/render_help.go b/internal/game/render_help.go index e2893b5..2d3a304 100644 --- a/internal/game/render_help.go +++ b/internal/game/render_help.go @@ -13,9 +13,11 @@ import ( ) type HelpDef struct { - Name string `yaml:"name"` - Category string `yaml:"category"` - Content string `yaml:"description"` + Name string `yaml:"name"` + Aliases []string `yaml:"aliases"` + Related []string `yaml:"related"` + Category string `yaml:"category"` + Content string `yaml:"description"` } type cmdEntry struct { @@ -63,7 +65,7 @@ var commandList = []cmdEntry{ {"mine", "Active", "Mine rocks (Mining)"}, {"mix", "Active", "Mix potions (Pharmacy)"}, {"mods / modlist", "Instant", "List available science modules"}, - {"north / south / east / west / ne / nw / se / sw / up / down", "Active", "Move in a direction"}, + {"north / south / east / west / up / down", "Active", "Move in a direction"}, {"option / options", "Instant", "View or change settings"}, {"prompt", "Instant", "Set custom command prompt"}, {"pull / push", "Active", "Interact with objects"}, @@ -94,16 +96,56 @@ func LoadHelp(dataDir string) ([]HelpDef, error) { dir := filepath.Join(dataDir, "help") var helps []HelpDef behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { - var h HelpDef - if err := yaml.Unmarshal(data, &h); err != nil { + h := parseHelpDef(data) + if h == nil { return nil } - helps = append(helps, h) + helps = append(helps, *h) return nil }) return helps, nil } +// helpRaw accepts both the canonical schema (name/description) and the +// legacy variant (title/body) used by a few help files. +type helpRaw struct { + Name string `yaml:"name"` + Title string `yaml:"title"` + Aliases []string `yaml:"aliases"` + Related []string `yaml:"related"` + Category string `yaml:"category"` + Description string `yaml:"description"` + Body string `yaml:"body"` +} + +// parseHelpDef unmarshals a help definition, normalizing legacy title/body +// files onto the canonical HelpDef. Returns nil for files that are not help +// topics (no name at all). +func parseHelpDef(data []byte) *HelpDef { + var raw helpRaw + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil + } + name := raw.Name + if name == "" { + name = raw.Title + } + if name == "" { + return nil + } + content := raw.Description + if content == "" { + content = raw.Body + } + return &HelpDef{ + Name: name, + Aliases: raw.Aliases, + Related: raw.Related, + Category: raw.Category, + Content: content, + } +} + func (g *Game) doHelp(sess *net.Session, topic string) { if topic == "" { unicode := true @@ -141,16 +183,47 @@ func (g *Game) doHelp(sess *net.Session, topic string) { return } - topic = strings.ToLower(topic) + topic = strings.ToLower(strings.TrimSpace(topic)) for _, h := range helps { - if strings.ToLower(h.Name) == topic { + if matchesHelpTopic(h, topic) { sess.WriteLine(fmt.Sprintf(" %s", h.Content)) + if len(h.Related) > 0 { + sess.WriteLine("") + sess.WriteLine(fmt.Sprintf(" Related topics: %s", strings.Join(h.Related, ", "))) + } return } } + + // No dedicated help page. If it's a known command, at least show its + // summary from the command table instead of a dead end. + for _, c := range commandList { + for _, name := range strings.Split(c.Name, " / ") { + if strings.ToLower(strings.TrimSpace(name)) == topic { + sess.WriteLine(fmt.Sprintf(" %s - %s", c.Name, c.Desc)) + sess.WriteLine("") + sess.WriteLine(" (No detailed help page for this command yet.)") + return + } + } + } sess.WriteLine(fmt.Sprintf("No help found for '%s'.", topic)) } +// matchesHelpTopic reports whether the help entry answers the given topic, +// matching its name or any declared alias (case-insensitive). +func matchesHelpTopic(h HelpDef, topic string) bool { + if strings.EqualFold(h.Name, topic) { + return true + } + for _, a := range h.Aliases { + if strings.EqualFold(strings.TrimSpace(a), topic) { + return true + } + } + return false +} + func firstLine(s string) string { if idx := strings.Index(s, "\n"); idx >= 0 { return s[:idx] diff --git a/internal/game/render_help_test.go b/internal/game/render_help_test.go new file mode 100644 index 0000000..b8ad614 --- /dev/null +++ b/internal/game/render_help_test.go @@ -0,0 +1,81 @@ +package game + +import ( + "strings" + "testing" +) + +func loadHelpOrSkip(t *testing.T) []HelpDef { + t.Helper() + helps, err := LoadHelp("../../data") + if err != nil { + t.Fatal(err) + } + if len(helps) == 0 { + t.Fatal("no help topics loaded") + } + return helps +} + +func TestLoadHelpAllFilesParse(t *testing.T) { + helps := loadHelpOrSkip(t) + seen := map[string]bool{} + for _, h := range helps { + if strings.TrimSpace(h.Name) == "" { + t.Errorf("help entry with empty name: %q", h.Content) + } + if seen[h.Name] { + t.Errorf("duplicate help topic %q", h.Name) + } + seen[h.Name] = true + } +} + +func TestNewPlayerHelpExists(t *testing.T) { + helps := loadHelpOrSkip(t) + found := false + for _, h := range helps { + if h.Name == "newplayer" { + found = true + } + } + if !found { + t.Fatal("help topic 'newplayer' missing (referenced by intro room)") + } +} + +func TestHelpAliasesResolveWithoutCollision(t *testing.T) { + helps := loadHelpOrSkip(t) + owner := map[string]string{} + for _, h := range helps { + for _, a := range h.Aliases { + a = strings.ToLower(strings.TrimSpace(a)) + if a == "" { + continue + } + if prev, dup := owner[a]; dup { + t.Errorf("alias %q claimed by both %q and %q", a, prev, h.Name) + } + owner[a] = h.Name + } + } + for topic, want := range map[string]string{ + "mine": "mining", + "chop": "woodcutting", + "fish": "fishing", + "fletch": "fletch", + "newplayer": "newplayer", + "new player": "newplayer", + } { + resolved := "" + for _, h := range helps { + if matchesHelpTopic(h, topic) { + resolved = h.Name + break + } + } + if resolved != want { + t.Errorf("help %q resolved to %q, want %q", topic, resolved, want) + } + } +} |
