package game import ( "fmt" "strings" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) doMove(sess *net.Session, dir string, multiplier float64) { p := sess.Player exitDir := g.World.ResolveExit(dir) if exitDir == "" { sess.WriteLine("Go where?") return } room, err := g.World.LoadRoom(p.RoomID) if err != nil { sess.WriteLine("You can't move from here.") return } exitDef, ok := room.Exits[exitDir] if !ok { sess.WriteLine("You can't go that way.") return } if exitDef.Condition != nil && !g.checkCondition(sess, exitDef.Condition) { msg := exitDef.BlockedMessage if msg == "" { msg = fmt.Sprintf("The way %s is blocked.", exitDir) } sess.WriteLine(msg) return } targetID := exitDef.Room targetRoom, err := g.World.LoadRoom(targetID) if err != nil { sess.WriteLine("That path seems blocked.") return } // Warn before walking from a safe room into a dangerous (hazardous) one. // Dangerous->dangerous never prompts (the current room is already hazardous). if targetRoom.Hazard != "" && room.Hazard == "" && p.OptionBool("danger_warning") { if p.PendingDangerDir == dir { p.PendingDangerDir = "" } else { p.PendingDangerDir = dir p.WalkSequence = nil sess.State = net.StateDangerConfirm name := targetRoom.Hazard if hz, herr := g.World.LoadHazard(targetRoom.Hazard); herr == nil && hz.Name != "" { name = hz.Name } sess.WriteLine(g.colorize(sess, "warning", fmt.Sprintf("WARNING: that area is exposed to %s.", name))) sess.WriteLine("Enter anyway? [Y/n]") return } } p.MovePendingFlags = exitDef.SetFlags p.MovePendingPlayerFlags = exitDef.SetPlayerFlags inCombat := g.Combat.Get(p.Name) != nil if !inCombat && p.Action != nil { g.cancelAction(p) } ticks := g.moveTicks(p, multiplier) p.MoveDirection = string(exitDir) p.MoveTarget = targetID if ticks <= 0 { g.completeMove(sess, p) return } p.MoveTicks = ticks if inCombat && p.OptionBool("run_countdown") { if p.MoveTicks > 1 { sess.WriteLine(fmt.Sprintf("Running in %d ticks...", p.MoveTicks)) } else { sess.WriteLine("Running next tick!") } } } var gracefulItems = map[string]bool{ "graceful_hat": true, "graceful_torso": true, "graceful_legs": true, "graceful_gloves": true, "graceful_boots": true, "graceful_cape": true, } func (g *Game) moveTicks(p *player.Player, multiplier float64) int { if id, ok := p.Equipment[item.SlotBack]; ok && id == "cape_of_agility" { return 0 } count := 0 for _, id := range p.Equipment { if gracefulItems[id] { count++ } } base := 2.0 - float64(count)*0.15 if count == 6 { base -= 0.1 } base *= multiplier return engine.ToTicks(base) - 1 } func (g *Game) completeMove(sess *net.Session, p *player.Player) { exitDir := p.MoveDirection targetID := p.MoveTarget pendingFlags := p.MovePendingFlags pendingPlayerFlags := p.MovePendingPlayerFlags p.ClearMoveState() if g.Combat.Get(p.Name) != nil { g.stopCombat(p.Name) } p.Action = nil if ss, ok := g.safespot.Get(p.Name); ok { g.forceLeaveSafespot(sess, p, &ss, "") } oldRoom := p.RoomID p.RoomID = targetID p.HazardTimer = 0 p.Stats.RecordRoomVisit(targetID) g.cancelEnterSeq(p.Name) if p.CutsceneRoom != 0 && p.CutsceneRoom == oldRoom { p.CutsceneRoom = 0 } if len(pendingFlags) > 0 || len(pendingPlayerFlags) > 0 { g.applyFlagMutations(p, pendingFlags, pendingPlayerFlags) } g.AccountStore.SaveCharacter(p) g.World.SeedGroundItems(p.RoomID) g.seedRoomMobs(p.RoomID) g.seedRoomObjects(p.RoomID) if g.Hub != nil { for _, other := range g.Hub.PlayersInRoom(oldRoom) { if other != sess { other.WriteLine(fmt.Sprintf("\n%s leaves to the %s.", p.Name, exitDir)) } } g.Hub.EnterRoom(sess, targetID) for _, other := range g.Hub.PlayersInRoom(targetID) { if other != sess { other.WriteLine(fmt.Sprintf("\n%s arrives.", p.Name)) } } } sess.WriteLine(fmt.Sprintf("You walk %s.", g.colorize(sess, "direction", exitDir))) if p.OptionBool("description") { g.doLook(sess) } else { targetRoom, _ := g.World.LoadRoom(targetID) if targetRoom != nil { sess.WriteLine(g.colorize(sess, "room_name", targetRoom.Name)) } } if p.OptionBool("automap") { g.doMap(sess) } g.runEnterSteps(sess, targetID) g.checkAggro(sess) } func (g *Game) seedRoomMobs(roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil || len(room.Mobs) == 0 { return } g.MobStore.SeedMobs(roomID, room.Mobs) } func (g *Game) executeMove(sess *net.Session, args []string, rawInput string) { g.doMove(sess, strings.TrimSpace(rawInput), 1.0) } // handleDangerConfirm processes the [Y/n] reply to the dangerous-area warning. // Pressing return (empty), "y", or "yes" enters; anything else cancels. func (g *Game) handleDangerConfirm(sess *net.Session, input string) { p := sess.Player if p == nil { sess.State = net.StateGame g.writePrompt(sess) return } dir := p.PendingDangerDir choice := strings.TrimSpace(strings.ToLower(input)) sess.State = net.StateGame if choice != "" && choice != "y" && choice != "yes" { p.PendingDangerDir = "" sess.WriteLine("You decide against it.") g.writePrompt(sess) return } if dir == "" { g.writePrompt(sess) return } g.doMove(sess, dir, 1.0) g.writePrompt(sess) } func (g *Game) seedRoomObjects(roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil { return } var ids []string for _, obj := range room.Objects { ids = append(ids, obj.ID) } g.World.EnsureObjectStates(roomID, ids) for _, obj := range room.Objects { def, err := g.ObjectStore.Load(obj.ID) if err != nil { continue } g.World.SetObjName(roomID, obj.ID, def.Name) if len(def.Aliases) > 0 { g.World.SetObjAliases(roomID, obj.ID, def.Aliases) } if len(obj.WanderRooms) > 0 { g.World.SetObjWander(roomID, obj.ID, obj.WanderRooms, obj.WanderInterval) } if def.IsGatherable() && def.Gather.DepleteTimer > 0 { g.World.SetObjDepleteTimer(roomID, obj.ID, def.Gather.DepleteTimer) } } }