aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-15 00:55:26 -0400
committerhistoria <[not public]>2026-06-15 00:56:27 -0400
commit445c4612cc5cf2c226f85894b6ad1e2419a374e2 (patch)
treedf3c6d4702cbeb7bd1618f88e93eb9cd08bcfcf4 /internal
parentb6fdde0aa6fcefd735e7c550aaa7fca879023f1c (diff)
downloadthehouseoficarus-445c4612cc5cf2c226f85894b6ad1e2419a374e2.tar.gz
feat: walk command, improved tables and columns
Diffstat (limited to 'internal')
-rw-r--r--internal/game/action.go1
-rw-r--r--internal/game/action_state.go3
-rw-r--r--internal/game/cmd_look.go2
-rw-r--r--internal/game/cmd_score.go14
-rw-r--r--internal/game/cmd_walk.go204
-rw-r--r--internal/game/game.go16
-rw-r--r--internal/game/help.go19
-rw-r--r--internal/game/table.go50
-rw-r--r--internal/player/player.go1
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 {