aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_dig.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-28 02:40:30 -0400
committerhistoria <[not public]>2026-06-28 02:40:30 -0400
commita1b6613c6c6a2f8d6e1ecd47e766bd40f92ac295 (patch)
tree4ff8fefb029c93f2aa3db7361658b0b4e9805b66 /internal/game/cmd_dig.go
parent87dacfbb3dd16e7f55a82361eace98f9a39d75c8 (diff)
downloadthehouseoficarus-a1b6613c6c6a2f8d6e1ecd47e766bd40f92ac295.tar.gz
feat: admin accounts, god mode, OLC commands like dig/room, 'inline' objects changed to 'local' objects
Diffstat (limited to 'internal/game/cmd_dig.go')
-rw-r--r--internal/game/cmd_dig.go281
1 files changed, 281 insertions, 0 deletions
diff --git a/internal/game/cmd_dig.go b/internal/game/cmd_dig.go
new file mode 100644
index 0000000..e7023b2
--- /dev/null
+++ b/internal/game/cmd_dig.go
@@ -0,0 +1,281 @@
+package game
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+ "thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/world"
+)
+
+func (g *Game) executeDig(sess *net.Session, args []string, rawInput string) {
+ if !g.checkAdmin(sess) {
+ sess.WriteLine("Unknown command.")
+ return
+ }
+ p := sess.Player
+ if p == nil {
+ return
+ }
+ if len(args) == 0 {
+ sess.WriteLine("Dig where? (north/south/east/west/up/down)")
+ return
+ }
+
+ dir := g.World.ResolveExit(args[0])
+ if dir == "" {
+ sess.WriteLine("Invalid direction. Use north/south/east/west/up/down.")
+ return
+ }
+
+ curPath, ok := g.World.GetRoomPath(p.RoomID)
+ if !ok {
+ sess.WriteLine("Error: can't find current room file.")
+ return
+ }
+
+ curRoom, err := g.World.LoadRoom(p.RoomID)
+ if err != nil {
+ sess.WriteLine("Error loading current room.")
+ return
+ }
+ if _, hasExit := curRoom.Exits[dir]; hasExit {
+ sess.WriteLine(fmt.Sprintf("There is already an exit to the %s.", dir))
+ return
+ }
+
+ if conflictID := g.findGridConflict(p.RoomID, dir); conflictID != 0 {
+ conflictRoom, err := g.World.LoadRoom(conflictID)
+ conflictName := fmt.Sprintf("#%d", conflictID)
+ if err == nil {
+ conflictName = fmt.Sprintf("#%d (%s)", conflictID, conflictRoom.Name)
+ }
+
+ oppositeDir := world.OppositeExit[dir]
+ curRoom.Exits[dir] = world.ExitDef{Room: conflictID}
+ curData, err := yaml.Marshal(curRoom)
+ if err != nil {
+ sess.WriteLine("Error updating current room YAML.")
+ return
+ }
+ if err := os.WriteFile(curPath, curData, 0644); err != nil {
+ sess.WriteLine("Error writing current room file.")
+ return
+ }
+
+ if conflictRoom != nil {
+ if _, exists := conflictRoom.Exits[oppositeDir]; !exists {
+ conflictRoom.Exits[oppositeDir] = world.ExitDef{Room: p.RoomID}
+ conflictPath, ok := g.World.GetRoomPath(conflictID)
+ if ok {
+ conflictData, cerr := yaml.Marshal(conflictRoom)
+ if cerr == nil {
+ os.WriteFile(conflictPath, conflictData, 0644)
+ }
+ }
+ }
+ }
+
+ sess.WriteLine(fmt.Sprintf("That spot is already occupied by %s. Created a two-way link instead.", conflictName))
+ return
+ }
+
+ newID, err := findNextRoomID(curPath)
+ if err != nil {
+ sess.WriteLine("Error scanning room directory.")
+ return
+ }
+
+ var name string
+ if len(args) > 1 {
+ name = strings.Join(args[1:], " ")
+ } else {
+ name = "New Room"
+ }
+
+ oppositeDir := world.OppositeExit[dir]
+ dirPath := filepath.Dir(curPath)
+ newPath := filepath.Join(dirPath, strconv.Itoa(newID)+".yaml")
+
+ newRoom := &world.Room{
+ Name: name,
+ Description: behavior.DescList{
+ {Text: "A featureless room."},
+ },
+ Exits: map[world.ExitDir]world.ExitDef{
+ oppositeDir: {Room: p.RoomID},
+ },
+ }
+
+ data, err := yaml.Marshal(newRoom)
+ if err != nil {
+ sess.WriteLine("Error creating room YAML.")
+ return
+ }
+ if err := os.MkdirAll(dirPath, 0755); err != nil {
+ sess.WriteLine("Error creating directory.")
+ return
+ }
+ if err := os.WriteFile(newPath, data, 0644); err != nil {
+ sess.WriteLine("Error writing room file.")
+ return
+ }
+
+ g.World.AddRoomPath(newID, newPath)
+
+ curRoom.Exits[dir] = world.ExitDef{Room: newID}
+ curData, err := yaml.Marshal(curRoom)
+ if err != nil {
+ sess.WriteLine("Error updating current room YAML.")
+ return
+ }
+ if err := os.WriteFile(curPath, curData, 0644); err != nil {
+ sess.WriteLine("Error writing current room file.")
+ return
+ }
+
+ oldRoom := p.RoomID
+ p.ClearMoveState()
+ if g.Combat.Get(p.Name) != nil {
+ g.stopCombat(p.Name)
+ }
+ p.Action = nil
+ g.cancelRest(p.Name)
+ g.cancelEnterSeq(p.Name)
+ if p.EnterSeqRoom != 0 && p.EnterSeqRoom == oldRoom {
+ p.EnterSeqRoom = 0
+ }
+ if ss, ok := g.safespot.Get(p.Name); ok {
+ g.forceLeaveSafespot(sess, p, &ss, "")
+ }
+
+ p.RoomID = newID
+ p.HazardTimer = 0
+ p.Stats.RecordRoomVisit(newID)
+ g.AccountStore.SaveCharacter(p)
+
+ g.World.SeedGroundItems(newID)
+ g.seedRoomMobs(newID)
+ g.seedRoomObjects(newID)
+
+ if g.Hub != nil {
+ for _, other := range g.Hub.PlayersInRoom(oldRoom) {
+ if other != sess {
+ other.WriteLine(fmt.Sprintf("%s digs to the %s and vanishes.", p.Name, dir))
+ }
+ }
+ g.Hub.EnterRoom(sess, newID)
+ for _, other := range g.Hub.PlayersInRoom(newID) {
+ if other != sess {
+ other.WriteLine(fmt.Sprintf("%s appears from a freshly dug tunnel.", p.Name))
+ }
+ }
+ }
+
+ sess.WriteLine(fmt.Sprintf("You dig %s and create Room #%d (%s).", dir, newID, name))
+ g.doLook(sess)
+ g.runEnterSteps(sess, newID)
+ g.checkAggro(sess)
+}
+
+func findNextRoomID(currentRoomPath string) (int, error) {
+ dir := filepath.Dir(currentRoomPath)
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return 0, err
+ }
+
+ used := make(map[int]bool)
+ minID := 0
+ first := true
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
+ continue
+ }
+ idStr := strings.TrimSuffix(entry.Name(), ".yaml")
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ continue
+ }
+ used[id] = true
+ if first || id < minID {
+ minID = id
+ first = false
+ }
+ }
+
+ if first {
+ return 1, nil
+ }
+
+ for id := minID; ; id++ {
+ if !used[id] {
+ return id, nil
+ }
+ }
+}
+
+var gridDeltas = map[world.ExitDir][2]int{
+ world.North: {0, -1},
+ world.South: {0, 1},
+ world.East: {1, 0},
+ world.West: {-1, 0},
+}
+
+func (g *Game) findGridConflict(fromRoomID int, dir world.ExitDir) int {
+ delta, ok := gridDeltas[dir]
+ if !ok {
+ return 0
+ }
+
+ roomIndex := g.World.RoomIndex()
+ coord := map[int][2]int{fromRoomID: {0, 0}}
+ roomAt := map[[2]int]int{{0, 0}: fromRoomID}
+ queue := []int{fromRoomID}
+
+ for len(queue) > 0 {
+ rid := queue[0]
+ queue = queue[1:]
+ room, err := g.World.LoadRoom(rid)
+ if err != nil {
+ continue
+ }
+ c := coord[rid]
+ for _, ed := range world.ExitOrder {
+ exit, ok := room.Exits[ed]
+ if !ok || exit.Room <= 0 || !roomIndex[exit.Room] {
+ continue
+ }
+ target := exit.Room
+ if ed == world.Up || ed == world.Down {
+ continue
+ }
+ gd, ok := gridDeltas[ed]
+ if !ok {
+ continue
+ }
+ want := [2]int{c[0] + gd[0], c[1] + gd[1]}
+
+ if _, exists := coord[target]; exists {
+ continue
+ }
+ if occupier, exists := roomAt[want]; exists && occupier != target {
+ continue
+ }
+ coord[target] = want
+ roomAt[want] = target
+ queue = append(queue, target)
+ }
+ }
+
+ targetCoord := [2]int{delta[0], delta[1]}
+ if occupier, ok := roomAt[targetCoord]; ok && occupier != fromRoomID {
+ return occupier
+ }
+ return 0
+}