diff options
| author | historia <[not public]> | 2026-06-15 00:55:26 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-15 00:56:27 -0400 |
| commit | 445c4612cc5cf2c226f85894b6ad1e2419a374e2 (patch) | |
| tree | df3c6d4702cbeb7bd1618f88e93eb9cd08bcfcf4 /internal | |
| parent | b6fdde0aa6fcefd735e7c550aaa7fca879023f1c (diff) | |
| download | thehouseoficarus-445c4612cc5cf2c226f85894b6ad1e2419a374e2.tar.gz | |
feat: walk command, improved tables and columns
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/game/action.go | 1 | ||||
| -rw-r--r-- | internal/game/action_state.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_score.go | 14 | ||||
| -rw-r--r-- | internal/game/cmd_walk.go | 204 | ||||
| -rw-r--r-- | internal/game/game.go | 16 | ||||
| -rw-r--r-- | internal/game/help.go | 19 | ||||
| -rw-r--r-- | internal/game/table.go | 50 | ||||
| -rw-r--r-- | internal/player/player.go | 1 |
9 files changed, 297 insertions, 13 deletions
diff --git a/internal/game/action.go b/internal/game/action.go index ce29559..bb4bdac 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -172,6 +172,7 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) { func (g *Game) CancelAction(p *player.Player) { p.Action = nil p.ActionState = nil + p.WalkSequence = nil } func (g *Game) AdvanceActions() { diff --git a/internal/game/action_state.go b/internal/game/action_state.go index 8db4a7b..280dd56 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -18,6 +18,7 @@ const ( ActionDropping ActionType = "dropping" ActionSearching ActionType = "searching" ActionResting ActionType = "resting" + ActionWalking ActionType = "walking" ) type ActionState struct { @@ -61,6 +62,8 @@ func (a *ActionState) Description() string { return "digging through a " + a.TargetName case ActionResting: return "resting" + case ActionWalking: + return "walking somewhere with a purpose!" } return "" } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 7efb65a..ac0c42a 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -22,7 +22,7 @@ func (g *Game) doLook(sess *net.Session) { sess.WriteLines( "", - room.Name, + fmt.Sprintf("%s (#%d)", room.Name, room.ID), ) descWidth := p.OptionInt("room_desc_width") diff --git a/internal/game/cmd_score.go b/internal/game/cmd_score.go index 9b3904a..2c62839 100644 --- a/internal/game/cmd_score.go +++ b/internal/game/cmd_score.go @@ -2,6 +2,7 @@ package game import ( "fmt" + "strconv" "thirdcollapse/internal/net" "thirdcollapse/internal/player" @@ -15,13 +16,20 @@ func (g *Game) doScore(sess *net.Session) { fmt.Sprintf("Combat Level: %d", p.CombatLevel()), fmt.Sprintf("HP: %d/%d", p.HP, p.MaxHP()), fmt.Sprintf("Credits: %d", p.Credits), - "", - "Skills:", ) + + t := &Table{Title: "Skills", Columns: []string{"Skill", "Level", "XP"}} for _, s := range player.AllSkills { level := p.Level(s) xp := p.Skills[s] next := player.XPForNextLevel(xp) - sess.WriteLine(fmt.Sprintf(" %-12s Level: %2d XP: %d / %d", s, level, xp, xp+next)) + t.Rows = append(t.Rows, []string{ + string(s), + strconv.Itoa(level), + fmt.Sprintf("%d / %d XP", xp, xp+next), + }) + } + for _, line := range t.Render(p.OptionBool("unicode")) { + sess.WriteLine(line) } } diff --git a/internal/game/cmd_walk.go b/internal/game/cmd_walk.go new file mode 100644 index 0000000..31f9e18 --- /dev/null +++ b/internal/game/cmd_walk.go @@ -0,0 +1,204 @@ +package game + +import ( + "fmt" + "strconv" + "strings" + + "thirdcollapse/internal/net" + "thirdcollapse/internal/player" + "thirdcollapse/internal/world" +) + +func (g *Game) doWalk(sess *net.Session, args []string) { + p := sess.Player.(*player.Player) + if len(args) == 0 { + sess.WriteLine("Walk where?") + return + } + + input := strings.Join(args, "") + if roomID, err := strconv.Atoi(input); err == nil { + path := g.findPathToRoom(p.RoomID, roomID) + if path == nil { + sess.WriteLine(fmt.Sprintf("No path found to room #%d.", roomID)) + return + } + g.startWalk(sess, p, path) + return + } + + dirs, err := parseWalkDirections(input) + if err != nil { + sess.WriteLine(err.Error()) + return + } + g.startWalk(sess, p, dirs) +} + +func parseWalkDirections(input string) ([]string, error) { + var dirs []string + i := 0 + for i < len(input) { + num := 0 + for i < len(input) && input[i] >= '0' && input[i] <= '9' { + num = num*10 + int(input[i]-'0') + i++ + } + if num == 0 { + num = 1 + } + if i >= len(input) { + return nil, fmt.Errorf("Unexpected end of directions.") + } + ch := input[i] + i++ + var dir string + switch ch { + case 'n': + dir = string(world.North) + case 's': + dir = string(world.South) + case 'e': + dir = string(world.East) + case 'w': + dir = string(world.West) + case 'u': + dir = string(world.Up) + case 'd': + dir = string(world.Down) + default: + return nil, fmt.Errorf("Unknown direction: %c", ch) + } + for j := 0; j < num; j++ { + dirs = append(dirs, dir) + } + } + return dirs, nil +} + +func (g *Game) startWalk(sess *net.Session, p *player.Player, dirs []string) { + agilityLevel := p.Level(player.Agility) + if agilityLevel < 1 { + agilityLevel = 1 + } + if len(dirs) > agilityLevel { + sess.WriteLine(fmt.Sprintf("You can only make %d moves at once with your current agility level!", agilityLevel)) + return + } + + g.CancelAction(p) + p.WalkSequence = dirs +} + +func (g *Game) advanceWalk(sess *net.Session, p *player.Player) { + if len(p.WalkSequence) == 0 { + return + } + + dir := p.WalkSequence[0] + oldRoom := p.RoomID + remaining := condensePath(p.WalkSequence[1:]) + p.WalkSequence = p.WalkSequence[1:] + + g.doMove(sess, dir) + + if p.RoomID == oldRoom { + p.WalkSequence = nil + p.ActionState = nil + return + } + + if len(p.WalkSequence) > 0 && remaining != "" { + p.ActionState = &ActionState{Type: ActionWalking} + sess.WriteLine(fmt.Sprintf("You're headed: %s.", remaining)) + } +} + +func condensePath(dirs []string) string { + if len(dirs) == 0 { + return "" + } + var result strings.Builder + i := 0 + for i < len(dirs) { + count := 1 + for i+count < len(dirs) && dirs[i+count] == dirs[i] { + count++ + } + if count > 1 { + result.WriteString(fmt.Sprintf("%d", count)) + } + result.WriteString(directionToChar(dirs[i])) + i += count + } + return result.String() +} + +func directionToChar(dir string) string { + switch world.ExitDir(dir) { + case world.North: + return "n" + case world.South: + return "s" + case world.East: + return "e" + case world.West: + return "w" + case world.Up: + return "u" + case world.Down: + return "d" + } + return "?" +} + +var bfsSearchDirs = []world.ExitDir{world.North, world.South, world.East, world.West, world.Up, world.Down} + +func (g *Game) findPathToRoom(fromRoom, toRoom int) []string { + if fromRoom == toRoom { + return nil + } + + type bfsNode struct { + roomID int + path []string + } + + visited := make(map[int]bool) + queue := []bfsNode{{fromRoom, nil}} + visited[fromRoom] = true + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + + room, ok := loadRoom(g, cur.roomID) + if !ok { + continue + } + + for _, dir := range bfsSearchDirs { + exitDef, exists := room.Exits[dir] + if !exists { + continue + } + if visited[exitDef.Room] { + continue + } + + newPath := make([]string, len(cur.path)+1) + copy(newPath, cur.path) + newPath[len(cur.path)] = string(dir) + + if exitDef.Room == toRoom { + return newPath + } + + visited[exitDef.Room] = true + queue = append(queue, bfsNode{exitDef.Room, newPath}) + } + } + + return nil +} diff --git a/internal/game/game.go b/internal/game/game.go index 7447215..b08c97e 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -348,6 +348,12 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI g.doSearch(sess, strings.Join(args, " ")) } return + case "walk": + if p == nil { + return + } + g.doWalk(sess, args) + return case "wear", "wield": if p == nil { return @@ -381,7 +387,7 @@ func (g *Game) ProcessQueuedCommands() { } switch as.Type { case ActionGathering, ActionCombating, ActionUsing, ActionTalking, - ActionToggling, ActionBurning, ActionStoking, ActionResting: + ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking: default: p.ActionState = nil } @@ -421,6 +427,14 @@ func (g *Game) ProcessQueuedCommands() { } g.activeQueue = make(map[string]*QueuedCommand) + for _, sess := range g.Hub.AllSessions() { + p, ok := sess.Player.(*player.Player) + if !ok || p == nil || len(p.WalkSequence) == 0 { + continue + } + g.advanceWalk(sess, p) + } + g.flushPendingDepletions() } diff --git a/internal/game/help.go b/internal/game/help.go index 1e93dcf..2e9caa4 100644 --- a/internal/game/help.go +++ b/internal/game/help.go @@ -7,6 +7,7 @@ import ( "strings" "thirdcollapse/internal/net" + "thirdcollapse/internal/player" "gopkg.in/yaml.v3" ) @@ -49,7 +50,6 @@ func (g *Game) doHelp(sess *net.Session, topic string) { } if topic == "" { - // Group by category cats := make(map[string][]HelpDef) var catOrder []string for _, h := range helps { @@ -59,14 +59,23 @@ func (g *Game) doHelp(sess *net.Session, topic string) { cats[h.Category] = append(cats[h.Category], h) } - sess.WriteLine("\nCommands:") + unicode := true + if p, _ := sess.Player.(*player.Player); p != nil { + unicode = p.OptionBool("unicode") + } + + sess.WriteLine("") for _, cat := range catOrder { - sess.WriteLine(fmt.Sprintf("\n %s:", cat)) + t := &Table{Title: cat} for _, h := range cats[cat] { - sess.WriteLine(fmt.Sprintf(" %-12s - %s", h.Name, firstLine(h.Content))) + t.Rows = append(t.Rows, []string{h.Name, firstLine(h.Content)}) + } + for _, line := range t.Render(unicode) { + sess.WriteLine(line) } + sess.WriteLine("") } - sess.WriteLine("\n Use 'help <command>' for details.") + sess.WriteLine("Use 'help <command>' for details.") return } diff --git a/internal/game/table.go b/internal/game/table.go index e3dad97..56c0079 100644 --- a/internal/game/table.go +++ b/internal/game/table.go @@ -11,6 +11,7 @@ type tableGlyphs struct { sepLeft, sepCross, sepRight rune botLeft, botSep, botRight rune fillH, fillHSep rune + titleSep rune } func tableGlyphSet(unicode bool) tableGlyphs { @@ -19,16 +20,19 @@ func tableGlyphSet(unicode bool) tableGlyphs { topLeft: '╔', topSep: '╤', topRight: '╗', side: '║', colSep: '│', sepLeft: '╟', sepCross: '┼', sepRight: '╢', botLeft: '╚', botSep: '╧', botRight: '╝', fillH: '═', fillHSep: '─', + titleSep: '┬', } } return tableGlyphs{ topLeft: '+', topSep: '+', topRight: '+', side: '|', colSep: '|', sepLeft: '+', sepCross: '+', sepRight: '+', botLeft: '+', botSep: '+', botRight: '+', fillH: '-', fillHSep: '-', + titleSep: '+', } } type Table struct { + Title string Columns []string Rows [][]string } @@ -37,6 +41,9 @@ func (t *Table) Render(unicode bool) []string { g := tableGlyphSet(unicode) nCols := len(t.Columns) + if nCols == 0 && len(t.Rows) > 0 { + nCols = len(t.Rows[0]) + } if nCols == 0 { return nil } @@ -86,14 +93,51 @@ func (t *Table) Render(unicode bool) []string { return b.String() } + hasTitle := t.Title != "" + hasCols := len(t.Columns) > 0 + var out []string - out = append(out, makeSep(g.topLeft, g.topSep, g.topRight, g.fillH)) - out = append(out, makeRow(t.Columns)) - out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) + if hasTitle { + innerWidth := 0 + for _, w := range colWidths { + innerWidth += w + 2 + } + innerWidth += nCols - 1 + + var tb strings.Builder + tb.WriteRune(g.topLeft) + tb.WriteString(strings.Repeat(string(g.fillH), innerWidth)) + tb.WriteRune(g.topRight) + out = append(out, tb.String()) + + var tr strings.Builder + tr.WriteRune(g.side) + tr.WriteString(" ") + tr.WriteString(t.Title) + padding := innerWidth - 2 - len(t.Title) + if padding < 0 { + padding = 0 + } + tr.WriteString(strings.Repeat(" ", padding)) + tr.WriteString(" ") + tr.WriteRune(g.side) + out = append(out, tr.String()) + + out = append(out, makeSep(g.sepLeft, g.titleSep, g.sepRight, g.fillHSep)) + } else { + out = append(out, makeSep(g.topLeft, g.topSep, g.topRight, g.fillH)) + } + + if hasCols { + out = append(out, makeRow(t.Columns)) + out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) + } + for _, row := range t.Rows { out = append(out, makeRow(row)) } + out = append(out, makeSep(g.botLeft, g.botSep, g.botRight, g.fillH)) return out diff --git a/internal/player/player.go b/internal/player/player.go index 95cf948..6fa22ce 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -154,6 +154,7 @@ type Player struct { RegenerateTick int Action *action.Action `yaml:"-"` ActionState any `yaml:"-"` + WalkSequence []string `yaml:"-"` } func (p *Player) OptionBool(name string) bool { |
