aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-20 03:51:23 -0400
committerhistoria <[not public]>2026-06-20 03:51:23 -0400
commitdde563e6810124a73de5ef12e92774c540f9d62e (patch)
tree68505a06c194d9b01b00e9d9672064b9b4b75156 /internal
parente4400c5f1e84c18d2126f2cb502ae10685977cbb (diff)
downloadthehouseoficarus-dde563e6810124a73de5ef12e92774c540f9d62e.tar.gz
feat: aps system implemented
Diffstat (limited to 'internal')
-rw-r--r--internal/action/behavior.go1
-rw-r--r--internal/game/action_talk.go4
-rw-r--r--internal/game/cmd_aps.go213
-rw-r--r--internal/game/cmd_registry.go1
4 files changed, 219 insertions, 0 deletions
diff --git a/internal/action/behavior.go b/internal/action/behavior.go
index 4071f0d..1fdc8df 100644
--- a/internal/action/behavior.go
+++ b/internal/action/behavior.go
@@ -56,6 +56,7 @@ type NodeAction struct {
ExtendTask bool `yaml:"extend_task"`
ReputationCost int `yaml:"reputation_cost"`
Sawmill bool `yaml:"sawmill"`
+ ApsNode bool `yaml:"aps_node"`
}
type ShopConfig struct {
diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go
index 2f463c7..787ce3e 100644
--- a/internal/game/action_talk.go
+++ b/internal/game/action_talk.go
@@ -2,6 +2,7 @@ package game
import (
"fmt"
+ "strconv"
"strings"
"thehouseoficarus/internal/action"
@@ -268,4 +269,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
if na.Sawmill {
g.processSawmill(sess, p)
}
+ if na.ApsNode {
+ setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true)
+ }
}
diff --git a/internal/game/cmd_aps.go b/internal/game/cmd_aps.go
new file mode 100644
index 0000000..8e20c6c
--- /dev/null
+++ b/internal/game/cmd_aps.go
@@ -0,0 +1,213 @@
+package game
+
+import (
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) executeAps(sess *net.Session, args []string, rawInput string) {
+ p := sess.Player
+
+ known := getKnownApsNodes(p)
+
+ if len(args) == 0 {
+ if len(known) == 0 {
+ sess.WriteLine("You have no APS coordinates stored in your datapad. Use an APS Tower to add some.")
+ return
+ }
+ displayApsNodes(g, sess, p, known)
+ return
+ }
+
+ if len(known) == 0 {
+ sess.WriteLine("You have no APS coordinates stored in your datapad. Use an APS Tower to add some.")
+ return
+ }
+
+ targetID := resolveApsTarget(g, sess, p, known, args)
+ if targetID == 0 {
+ return
+ }
+
+ if targetID == p.RoomID {
+ sess.WriteLine("You are already at that location.")
+ return
+ }
+
+ path := g.findPathToRoom(p.RoomID, targetID)
+ if path == nil {
+ room, err := g.World.LoadRoom(targetID)
+ roomName := fmt.Sprintf("#%d", targetID)
+ if err == nil {
+ roomName = room.Name
+ }
+ sess.WriteLine(fmt.Sprintf("No path found to %s (#%d).", roomName, targetID))
+ return
+ }
+
+ agilityLevel := p.Level(player.Agility)
+ if agilityLevel < 1 {
+ agilityLevel = 1
+ }
+ truncated := path
+ if len(path) > agilityLevel {
+ truncated = path[:agilityLevel]
+ }
+
+ room, _ := g.World.LoadRoom(targetID)
+ roomName := fmt.Sprintf("#%d", targetID)
+ if room != nil {
+ roomName = room.Name
+ }
+
+ g.cancelAction(p)
+ p.WalkSequence = truncated
+ sess.WriteLine(fmt.Sprintf("You set off towards %s (#%d) — %d move%s queued.",
+ roomName, targetID, len(truncated), plural(len(truncated))))
+}
+
+func getKnownApsNodes(p *player.Player) []int {
+ if p.Flags == nil {
+ return nil
+ }
+ var nodes []int
+ for k, v := range p.Flags {
+ if !strings.HasPrefix(k, "aps_node_") {
+ continue
+ }
+ if b, ok := v.(bool); !ok || !b {
+ continue
+ }
+ idStr := strings.TrimPrefix(k, "aps_node_")
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ continue
+ }
+ nodes = append(nodes, id)
+ }
+ sort.Ints(nodes)
+ return nodes
+}
+
+type apsNodeInfo struct {
+ ID int
+ Name string
+ Distance int
+}
+
+func displayApsNodes(g *Game, sess *net.Session, p *player.Player, known []int) {
+ var infos []apsNodeInfo
+ for _, id := range known {
+ room, err := g.World.LoadRoom(id)
+ name := fmt.Sprintf("#%d", id)
+ if err == nil {
+ name = room.Name
+ }
+ dist := 0
+ if id != p.RoomID {
+ path := g.findPathToRoom(p.RoomID, id)
+ if path != nil {
+ dist = len(path)
+ }
+ }
+ infos = append(infos, apsNodeInfo{ID: id, Name: name, Distance: dist})
+ }
+
+ sort.Slice(infos, func(i, j int) bool {
+ return infos[i].Distance < infos[j].Distance
+ })
+
+ mode := g.colorMode(sess)
+ rows := make([][]string, len(infos))
+ for i, info := range infos {
+ distStr := strconv.Itoa(info.Distance)
+ if info.Distance == 0 {
+ distStr = color.Render(mode, color.Parse("82"), "here")
+ }
+ rows[i] = []string{
+ color.Render(mode, color.Parse("245"), fmt.Sprintf("#%d", info.ID)),
+ color.Render(mode, color.Parse("75"), info.Name),
+ distStr,
+ }
+ }
+
+ t := &Table{
+ Title: "APS Datapad",
+ Columns: []string{
+ color.Render(mode, color.Parse("245"), "Room"),
+ color.Render(mode, color.Parse("75"), "Name"),
+ color.Render(mode, color.Parse("230"), "Distance"),
+ },
+ Rows: rows,
+ }
+
+ for _, line := range t.Render(p.OptionBool("unicode")) {
+ sess.WriteLine(line)
+ }
+}
+
+func resolveApsTarget(g *Game, sess *net.Session, p *player.Player, known []int, args []string) int {
+ input := strings.ToLower(strings.Join(args, " "))
+
+ if input == "nearest" {
+ bestID := 0
+ bestDist := -1
+ for _, id := range known {
+ if id == p.RoomID {
+ return id
+ }
+ path := g.findPathToRoom(p.RoomID, id)
+ if path == nil {
+ continue
+ }
+ if bestDist == -1 || len(path) < bestDist {
+ bestDist = len(path)
+ bestID = id
+ }
+ }
+ if bestID == 0 {
+ sess.WriteLine("No reachable APS nodes found.")
+ return 0
+ }
+ return bestID
+ }
+
+ if roomID, err := strconv.Atoi(input); err == nil {
+ for _, id := range known {
+ if id == roomID {
+ return roomID
+ }
+ }
+ sess.WriteLine(fmt.Sprintf("Room #%d is not in your APS datapad.", roomID))
+ return 0
+ }
+
+ var matches []int
+ for _, id := range known {
+ room, err := g.World.LoadRoom(id)
+ if err != nil {
+ continue
+ }
+ nameLower := strings.ToLower(room.Name)
+ if strings.HasPrefix(nameLower, input) || strings.HasPrefix(input, nameLower) {
+ matches = append(matches, id)
+ }
+ }
+
+ if len(matches) == 1 {
+ return matches[0]
+ }
+ if len(matches) > 1 {
+ sess.WriteLine("Multiple APS nodes match that name. Be more specific or use the room number.")
+ return 0
+ }
+
+ sess.WriteLine("That location is not in your APS datapad.")
+ return 0
+}
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
index ea741d9..db1ec81 100644
--- a/internal/game/cmd_registry.go
+++ b/internal/game/cmd_registry.go
@@ -117,6 +117,7 @@ var commandRegistry = map[string]commandDef{
"remove": {(*Game).executeRemove, ClassFree},
"unwear": {(*Game).executeRemove, ClassFree},
"unwield": {(*Game).executeRemove, ClassFree},
+ "aps": {(*Game).executeAps, ClassActive},
}
func classifyCommand(cmd string) CommandClass {