aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_room_insert.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/cmd_room_insert.go')
-rw-r--r--internal/game/cmd_room_insert.go290
1 files changed, 290 insertions, 0 deletions
diff --git a/internal/game/cmd_room_insert.go b/internal/game/cmd_room_insert.go
new file mode 100644
index 0000000..4fbcd10
--- /dev/null
+++ b/internal/game/cmd_room_insert.go
@@ -0,0 +1,290 @@
+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) roomInsert(sess *net.Session, args []string) {
+ p := sess.Player
+ if p == nil {
+ return
+ }
+
+ if len(args) == 0 {
+ sess.WriteLine("Usage: room insert <direction> [name]")
+ return
+ }
+
+ dir := g.World.ResolveExit(args[0])
+ if dir == "" {
+ sess.WriteLine("Invalid direction. Use n/s/e/w/ne/nw/se/sw/u/d.")
+ return
+ }
+
+ var name string
+ if len(args) > 1 {
+ name = strings.Join(args[1:], " ")
+ } else {
+ name = "New Room"
+ }
+
+ 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
+ }
+
+ exitDef, hasExit := curRoom.Exits[dir]
+ if !hasExit {
+ sess.WriteLine(fmt.Sprintf("There is no exit to the %s from here.", dir))
+ return
+ }
+
+ targetID := exitDef.Room
+ targetRoom, err := g.World.LoadRoom(targetID)
+ if err != nil {
+ sess.WriteLine(fmt.Sprintf("Target room %d not found.", targetID))
+ return
+ }
+ targetName := fmt.Sprintf("#%d (%s)", targetID, targetRoom.Name)
+
+ oppositeDir := world.OppositeExit[dir]
+
+ if delta, isHorizontal := world.DirectionDeltas[dir]; isHorizontal {
+ coord, roomAt := g.buildGridFrom(p.RoomID)
+
+ sSet := g.bfsComponent(targetID, p.RoomID)
+
+ withoutEdge := g.bfsWithoutEdge(p.RoomID, dir, targetID)
+ for rid := range sSet {
+ if withoutEdge[rid] {
+ room, _ := g.World.LoadRoom(rid)
+ conflictName := fmt.Sprintf("#%d", rid)
+ if room != nil {
+ conflictName = fmt.Sprintf("#%d (%s)", rid, room.Name)
+ }
+ sess.WriteLine(fmt.Sprintf(
+ "Cannot insert: room %s would have an ambiguous grid position after insertion (reachable via an alternate path from here).",
+ conflictName))
+ return
+ }
+ }
+
+ for rid := range sSet {
+ if _, inGrid := coord[rid]; !inGrid {
+ continue
+ }
+ oldPos := coord[rid]
+ newPos := [2]int{oldPos[0] + delta[0], oldPos[1] + delta[1]}
+ if occupier, ok := roomAt[newPos]; ok && !sSet[occupier] {
+ occRoom, _ := g.World.LoadRoom(occupier)
+ occName := fmt.Sprintf("#%d", occupier)
+ if occRoom != nil {
+ occName = fmt.Sprintf("#%d (%s)", occupier, occRoom.Name)
+ }
+ sess.WriteLine(fmt.Sprintf(
+ "Cannot insert: pushing %s would cause grid collision with %s.",
+ targetName, occName))
+ return
+ }
+ }
+ }
+
+ newID, err := findNextRoomID(curPath)
+ if err != nil {
+ sess.WriteLine("Error scanning room directory.")
+ return
+ }
+
+ 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{
+ dir: {Room: targetID},
+ 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,
+ Condition: exitDef.Condition,
+ BlockedMessage: exitDef.BlockedMessage,
+ SetFlags: exitDef.SetFlags,
+ SetPlayerFlags: exitDef.SetPlayerFlags,
+ }
+ 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 targetExit, tok := targetRoom.Exits[oppositeDir]; tok && targetExit.Room == p.RoomID {
+ targetRoom.Exits[oppositeDir] = world.ExitDef{
+ Room: newID,
+ Condition: targetExit.Condition,
+ BlockedMessage: targetExit.BlockedMessage,
+ SetFlags: targetExit.SetFlags,
+ SetPlayerFlags: targetExit.SetPlayerFlags,
+ }
+ }
+ targetPath, tok2 := g.World.GetRoomPath(targetID)
+ if tok2 {
+ targetData, terr := yaml.Marshal(targetRoom)
+ if terr == nil {
+ os.WriteFile(targetPath, targetData, 0644)
+ }
+ }
+
+ 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 vanishes into the tunnel.", p.Name))
+ }
+ }
+ g.Hub.EnterRoom(sess, newID)
+ for _, other := range g.Hub.PlayersInRoom(newID) {
+ if other != sess {
+ other.WriteLine(fmt.Sprintf("%s appears.", p.Name))
+ }
+ }
+ }
+
+ g.World.RebuildRoomIndex(g.DataDir)
+
+ sess.WriteLine(fmt.Sprintf("You insert a room to the %s and create Room #%d (%s).", dir, newID, name))
+ g.doLook(sess)
+ g.runEnterSteps(sess, newID)
+ g.checkAggro(sess)
+}
+
+func (g *Game) bfsComponent(startID, excludeID int) map[int]bool {
+ roomIndex := g.World.RoomIndex()
+ visited := map[int]bool{startID: true}
+ queue := []int{startID}
+
+ for len(queue) > 0 {
+ rid := queue[0]
+ queue = queue[1:]
+ room, err := g.World.LoadRoom(rid)
+ if err != nil {
+ continue
+ }
+ for _, ed := range world.ExitOrder {
+ if ed == world.Up || ed == world.Down {
+ continue
+ }
+ exit, ok := room.Exits[ed]
+ if !ok || exit.Room <= 0 || !roomIndex[exit.Room] {
+ continue
+ }
+ target := exit.Room
+ if target == excludeID {
+ continue
+ }
+ if visited[target] {
+ continue
+ }
+ visited[target] = true
+ queue = append(queue, target)
+ }
+ }
+ return visited
+}
+
+func (g *Game) bfsWithoutEdge(fromID int, skipDir world.ExitDir, skipTo int) map[int]bool {
+ roomIndex := g.World.RoomIndex()
+ visited := map[int]bool{fromID: true}
+ queue := []int{fromID}
+
+ for len(queue) > 0 {
+ rid := queue[0]
+ queue = queue[1:]
+ room, err := g.World.LoadRoom(rid)
+ if err != nil {
+ continue
+ }
+ for _, ed := range world.ExitOrder {
+ if ed == world.Up || ed == world.Down {
+ continue
+ }
+ exit, ok := room.Exits[ed]
+ if !ok || exit.Room <= 0 || !roomIndex[exit.Room] {
+ continue
+ }
+ if rid == fromID && ed == skipDir && exit.Room == skipTo {
+ continue
+ }
+ target := exit.Room
+ if visited[target] {
+ continue
+ }
+ visited[target] = true
+ queue = append(queue, target)
+ }
+ }
+ return visited
+}