diff options
Diffstat (limited to 'internal/game/cmd_room_insert.go')
| -rw-r--r-- | internal/game/cmd_room_insert.go | 290 |
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 +} |
