diff options
| author | historia <[not public]> | 2026-06-14 21:07:26 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-14 21:07:26 -0400 |
| commit | 1aba2bdd23aebed4032333e74aa553c40a31fcbd (patch) | |
| tree | c004f4c2a139df5c3cf4029b2611bdfa09b40f86 /internal/game | |
| parent | a19e6b6ab01670a5932308c7bd0e0e5eed8cbcad (diff) | |
| download | thehouseoficarus-1aba2bdd23aebed4032333e74aa553c40a31fcbd.tar.gz | |
refactor: added docs, split up some components in game package
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/action_gather.go | 12 | ||||
| -rw-r--r-- | internal/game/action_talk.go | 4 | ||||
| -rw-r--r-- | internal/game/action_use.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_drop.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_get.go | 22 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 6 | ||||
| -rw-r--r-- | internal/game/cmd_map.go | 103 | ||||
| -rw-r--r-- | internal/game/cmd_move.go | 8 | ||||
| -rw-r--r-- | internal/game/cmd_wear.go | 2 | ||||
| -rw-r--r-- | internal/game/doc.go | 29 | ||||
| -rw-r--r-- | internal/game/game.go | 2 | ||||
| -rw-r--r-- | internal/game/login_account.go (renamed from internal/game/login.go) | 226 | ||||
| -rw-r--r-- | internal/game/login_char.go | 236 | ||||
| -rw-r--r-- | internal/game/map.go | 102 | ||||
| -rw-r--r-- | internal/game/types.go | 29 | ||||
| -rw-r--r-- | internal/game/utils.go | 35 |
16 files changed, 427 insertions, 394 deletions
diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index 7e53e87..201a619 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -32,7 +32,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje msg += fmt.Sprintf(" (%d ticks to respawn)", st.DepleteTimer) } sess.WriteLine(msg) - } else if cfg.SharedDeplete > 0 { + } else if cfg.DepleteTimer > 0 { sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, st.DepleteTimer)) } else { sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, st.DepleteTimer)) @@ -114,8 +114,8 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje "effective_wait": wait, "step": 0, } - if cfg.SharedDeplete > 0 { - data["shared_deplete"] = true + if cfg.DepleteTimer > 0 { + data["deplete_timer"] = true } p.Action = &action.Action{ Type: "gather", @@ -137,7 +137,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { step := p.Action.Data["step"].(int) wait := p.Action.Data["effective_wait"].(int) instanceKey := p.Action.Data["instance_key"].(string) - _, shared := p.Action.Data["shared_deplete"] + _, shared := p.Action.Data["deplete_timer"] if step == 0 { if cfg.Bait != "" { @@ -240,7 +240,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } if !shared && drop.Depletes { - delay := cfg.DepleteDelay + delay := cfg.RespawnTimer if delay <= 0 { delay = 10 } @@ -305,7 +305,7 @@ func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry func (g *Game) depleteSharedTree(sess *net.Session, p *player.Player, st *world.ObjState, cfg *action.GatherConfig, instanceKey string) { st.Depleted = true - st.DepleteTimer = cfg.DepleteDelay + st.DepleteTimer = cfg.RespawnTimer st.SharedTimer = 0 msg := cfg.ExhaustedMessage diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go index f4929e9..4980c5a 100644 --- a/internal/game/action_talk.go +++ b/internal/game/action_talk.go @@ -111,7 +111,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) { return } - idx, err := parseIndex(input) + idx, err := parseChoiceIndex(input) if err != nil || idx <= 0 { g.CancelAction(p) sess.State = net.StateGame @@ -193,7 +193,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) { g.AccountStore.SaveCharacter(p) g.World.SeedGroundItems(p.RoomID) g.seedRoomMobs(p.RoomID) - g.ensureRoomObjects(p.RoomID) + g.seedRoomObjects(p.RoomID) if g.Hub != nil { g.Hub.EnterRoom(sess, p.RoomID) } diff --git a/internal/game/action_use.go b/internal/game/action_use.go index 497ceac..c47492f 100644 --- a/internal/game/action_use.go +++ b/internal/game/action_use.go @@ -17,7 +17,7 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD return } - for itemID, qty := range cfg.Consume { + for itemID := range cfg.Consume { if !p.HasItem(itemID) { defName := itemID if def, err := g.ItemStore.Load(itemID); err == nil { @@ -26,7 +26,6 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD sess.WriteLine(fmt.Sprintf("You need %s to use this.", defName)) return } - _ = qty } if cfg.Success == nil && p.FirstFreeSlot() == -1 { diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go index cbe232d..149b86b 100644 --- a/internal/game/cmd_drop.go +++ b/internal/game/cmd_drop.go @@ -222,7 +222,7 @@ func (g *Game) handleDropAllConfirm(sess *net.Session, input string) { sess.WriteLine("\nYou have nothing to drop.") } else { sess.Write(fmt.Sprintf("\nYou drop your ")) - innerPickupReport(sess, dropped) + formatPickupList(sess, dropped) } sess.Write("\r\n> ") } diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go index f4c7426..78a9621 100644 --- a/internal/game/cmd_get.go +++ b/internal/game/cmd_get.go @@ -73,12 +73,10 @@ func (g *Game) doGet(sess *net.Session, input string) { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == itemID { - removed, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name) - if !ok { + if _, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name); !ok { sess.WriteLine("That's not yours!") return } - _ = removed slot.Quantity += qty g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, def.Name, slot.Quantity)) @@ -90,13 +88,11 @@ func (g *Game) doGet(sess *net.Session, input string) { sess.WriteLine("Your inventory is full.") return } - removed, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name) - if !ok { + if _, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name); !ok { sess.WriteLine("That's not yours!") return } - _ = removed - p.SetInvSlot(freeSlot, g.newInvSlot(itemID, qty)) + p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty)) g.AccountStore.SaveCharacter(p) if qty == 1 { sess.WriteLine(fmt.Sprintf("You pick up a %s.", def.Name)) @@ -112,11 +108,9 @@ func (g *Game) doGet(sess *net.Session, input string) { if fs == -1 { break } - removed, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, 1, p.Name) - if !ok { + if _, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, 1, p.Name); !ok { break } - _ = removed p.SetInvSlot(fs, &player.InventorySlot{ItemID: itemID, Quantity: 1}) picked++ } @@ -318,7 +312,7 @@ func (g *Game) doGetAll(sess *net.Session) { if freeSlot == -1 { if len(picked) > 0 { sess.WriteLine("You can't hold everything but you picked up:") - innerPickupReport(sess, picked) + formatPickupList(sess, picked) } else { sess.WriteLine("Your inventory is full!") } @@ -329,7 +323,7 @@ func (g *Game) doGetAll(sess *net.Session) { if !ok { break } - p.SetInvSlot(freeSlot, g.newInvSlot(itemID, qty)) + p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty)) name := itemID if def != nil { name = def.Name @@ -342,7 +336,7 @@ func (g *Game) doGetAll(sess *net.Session) { if freeSlot == -1 { if len(picked) > 0 { sess.WriteLine("You can't hold everything but you picked up:") - innerPickupReport(sess, picked) + formatPickupList(sess, picked) } else { sess.WriteLine("Your inventory is full!") } @@ -371,7 +365,7 @@ func (g *Game) doGetAll(sess *net.Session) { sess.WriteLine("Your inventory is full.") } else { sess.Write("You picked up: ") - innerPickupReport(sess, picked) + formatPickupList(sess, picked) } } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index e0e98fe..a840579 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -141,7 +141,7 @@ func (g *Game) doLook(sess *net.Session) { } var suffix string if multi && (len(timed) > 0 || len(depleted) > 0) { - suffix = fmt.Sprintf(" [%s]", intsJoin(freshIdxs)) + suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs)) } qualityTimer := "" if showTimers && len(instances) > 0 && instances[0].quality > 0 { @@ -302,7 +302,7 @@ func (g *Game) doLook(sess *net.Session) { } line += fmt.Sprintf(" (fighting %s)", name) } - } else if desc := actionDesc(op); desc != "" { + } else if desc := playerActionDescription(op); desc != "" { line += ", " + desc } sess.WriteLine(line + ".") @@ -512,7 +512,7 @@ func (g *Game) doExits(sess *net.Session) { } } -func intsJoin(nums []int) string { +func joinInts(nums []int) string { var parts []string for _, n := range nums { parts = append(parts, strconv.Itoa(n)) diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go index 7c506f9..e0413d3 100644 --- a/internal/game/cmd_map.go +++ b/internal/game/cmd_map.go @@ -1,11 +1,8 @@ package game import ( - "strings" - "thirdcollapse/internal/net" "thirdcollapse/internal/player" - "thirdcollapse/internal/world" ) func (g *Game) doMap(sess *net.Session) { @@ -38,103 +35,3 @@ func (g *Game) doMap(sess *net.Session) { sess.WriteLine(line) } } - -func stripBlankRows(lines []string) []string { - var out []string - for _, line := range lines { - if strings.TrimSpace(line) != "" { - out = append(out, line) - } - } - return out -} - -func leftTrimCommon(lines []string) []string { - min := -1 - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue - } - n := 0 - for _, r := range line { - if r == ' ' { - n++ - } else { - break - } - } - if min < 0 || n < min { - min = n - } - } - if min <= 0 { - return lines - } - result := make([]string, len(lines)) - for i, line := range lines { - if len(line) <= min { - result[i] = "" - } else { - result[i] = line[min:] - } - } - return result -} - -func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string { - mg := buildGraph(g, roomID) - - grid := make([][]rune, mapHeight) - for i := range grid { - grid[i] = make([]rune, mapWidth) - for j := range grid[i] { - grid[i][j] = ' ' - } - } - - cx := mapWidth / 2 - cy := mapHeight / 2 - - for pos, rid := range mg.posToRoom { - gr := cy + pos[1]*2 - gc := cx + pos[0]*2 - if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { - continue - } - if rid == roomID { - grid[gr][gc] = '@' - } else { - grid[gr][gc] = roomMapSymbol(g, rid) - } - } - - for pos, rid := range mg.posToRoom { - x, y := pos[0], pos[1] - - if rightID, ok := mg.posToRoom[[2]int{x + 1, y}]; ok { - if exitsConnect(g, rid, rightID, world.East, world.West) { - gr := cy + y*2 - gc := cx + x*2 + 1 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - grid[gr][gc] = '─' - } - } - } - - if bottomID, ok := mg.posToRoom[[2]int{x, y + 1}]; ok { - if exitsConnect(g, rid, bottomID, world.South, world.North) { - gr := cy + y*2 + 1 - gc := cx + x*2 - if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { - grid[gr][gc] = '│' - } - } - } - } - - lines := make([]string, mapHeight) - for i := range grid { - lines[i] = string(grid[i]) - } - return lines -} diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index d62701d..9ef7a00 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -62,7 +62,7 @@ func (g *Game) doMove(sess *net.Session, dir string) { g.World.SeedGroundItems(p.RoomID) g.seedRoomMobs(p.RoomID) - g.ensureRoomObjects(p.RoomID) + g.seedRoomObjects(p.RoomID) if g.Hub != nil { for _, other := range g.Hub.PlayersInRoom(oldRoom) { @@ -103,7 +103,7 @@ func (g *Game) seedRoomMobs(roomID int) { g.MobStore.SeedMobs(roomID, room.Mobs) } -func (g *Game) ensureRoomObjects(roomID int) { +func (g *Game) seedRoomObjects(roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil { return @@ -124,8 +124,8 @@ func (g *Game) ensureRoomObjects(roomID int) { } if def.BehaviorID != "" { cfg, err := g.BehaviorStore.LoadGather(def.BehaviorID) - if err == nil && cfg.SharedDeplete > 0 { - g.World.SetObjSharedDeplete(roomID, obj.ID, cfg.SharedDeplete) + if err == nil && cfg.DepleteTimer > 0 { + g.World.SetObjDepleteTimer(roomID, obj.ID, cfg.DepleteTimer) } } } diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go index 4554dfc..7c3515f 100644 --- a/internal/game/cmd_wear.go +++ b/internal/game/cmd_wear.go @@ -155,6 +155,6 @@ func (g *Game) doWearAll(sess *net.Session) { } else { g.AccountStore.SaveCharacter(p) sess.Write("You wear: ") - innerPickupReport(sess, equipped) + formatPickupList(sess, equipped) } } diff --git a/internal/game/doc.go b/internal/game/doc.go new file mode 100644 index 0000000..97814e9 --- /dev/null +++ b/internal/game/doc.go @@ -0,0 +1,29 @@ +// Package game is the core session handler, command dispatch, login flow, +// and action system for the MUD. +// +// The Game struct holds all world state: room/world access, item/object +// stores, mob instances, behavior definitions, the player hub, and the +// tick engine. HandleSession routes incoming input through the login +// state machine into the in-game command dispatch. +// +// Commands are dispatched via handleGameCommand in game.go, which +// resolves account aliases before switching on the command word. +// Most commands cancel the player's ongoing action (gathering, etc.). +// +// Action system: StartAction normalizes verbs, resolves object/mob +// targets, loads behaviors, and routes to type-specific handlers. +// Actions that take time (gather, use, burn, stoke) tick via +// AdvanceActions, called each 600ms tick. +// +// Tick handlers: DisconnectTick, RegenTick, WanderTick, WoodcuttingTick, +// and FireTick run each game tick to advance world simulation. +// +// File organization: +// cmd_*.go — player command handlers (doLook, doMove, doGet, etc.) +// action_*.go — action lifecycle (startGather, advanceBurn, talk, toggle, etc.) +// login.go — account creation, authentication, character management +// map.go — BFS graph builder, tiny + full-map rendering +// tick.go — per-tick world updates (disconnect, regen, wander, woodcutting) +// utils.go — shared types and helper functions +// help.go — help topic loading and display +package game diff --git a/internal/game/game.go b/internal/game/game.go index 21dee76..cecde2a 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -59,7 +59,7 @@ func (g *Game) SetHub(hub *net.Hub) { } func (g *Game) HandleSession(sess *net.Session, input string) { - input = player.StripControl(input) + input = player.StripControlCharacters(input) switch sess.State { case net.StateAccountName: g.handleAccountName(sess, input) diff --git a/internal/game/login.go b/internal/game/login_account.go index 3de70bc..7243955 100644 --- a/internal/game/login.go +++ b/internal/game/login_account.go @@ -268,91 +268,6 @@ func (g *Game) handleMenu(sess *net.Session, input string) { } } -func (g *Game) handleNewCharName(sess *net.Session, input string) { - name := strings.TrimSpace(input) - - if sess.PendingChar == "connect" && len(sess.Account.Characters) > 0 { - if idx, err := parseIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) { - sess.PendingChar = "" - g.connectCharacter(sess, sess.Account.Characters[idx-1]) - return - } - sess.WriteLine("Invalid choice.") - sess.Write("\n> ") - return - } - - if name == "" { - sess.Write("Character name: ") - return - } - - if verr := player.ValidName(name); verr != nil { - sess.WriteLine(verr.Error()) - sess.Write("Character name: ") - return - } - - if g.AccountStore.CharacterExists(name) { - sess.WriteLine("A character with that name already exists.") - sess.Write("Character name: ") - return - } - - p := player.New(name) - p.RoomID = 1 - - if err := g.AccountStore.SaveCharacter(p); err != nil { - sess.WriteLine(fmt.Sprintf("Error creating character: %v", err)) - sess.State = net.StateMenu - g.showMenu(sess) - return - } - - acc, _ := g.AccountStore.LoadAccount(sess.Account.Name) - acc.Characters = append(acc.Characters, name) - g.AccountStore.SaveAccount(acc) - sess.Account.Characters = acc.Characters - - g.connectCharacter(sess, name) -} - -func (g *Game) connectCharacter(sess *net.Session, name string) { - g.charsMu.Lock() - if existing := g.loggedInChars[name]; existing != nil { - g.charsMu.Unlock() - sess.WriteLine("This character is logged in elsewhere.") - sess.State = net.StateMenu - g.showMenu(sess) - return - } - - p, err := g.AccountStore.LoadCharacter(name) - if err != nil { - g.charsMu.Unlock() - sess.WriteLine(fmt.Sprintf("Error loading character: %v", err)) - sess.State = net.StateMenu - g.showMenu(sess) - return - } - - sess.Player = p - sess.State = net.StateGame - g.loggedInChars[name] = sess - g.charsMu.Unlock() - - g.World.SeedGroundItems(p.RoomID) - g.seedRoomMobs(p.RoomID) - g.ensureRoomObjects(p.RoomID) - - if g.Hub != nil { - g.Hub.EnterRoom(sess, p.RoomID) - } - - g.doLook(sess) - sess.Write("\r\n> ") -} - func (g *Game) handleRenameAccount(sess *net.Session, input string) { newName := strings.TrimSpace(input) if newName == "" { @@ -389,147 +304,6 @@ func (g *Game) handleRenameAccount(sess *net.Session, input string) { g.showMenu(sess) } -func (g *Game) handleRenameChar(sess *net.Session, input string) { - name := strings.TrimSpace(input) - if idx, err := parseIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) { - sess.PendingChar = sess.Account.Characters[idx-1] - } else { - sess.PendingChar = name - } - - found := false - for _, c := range sess.Account.Characters { - if c == sess.PendingChar { - found = true - break - } - } - if !found { - sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar)) - g.showMenu(sess) - return - } - - sess.State = net.StateRenameCharName - sess.WriteLine(fmt.Sprintf("Renaming %s.", sess.PendingChar)) - sess.Write("New name: ") -} - -func (g *Game) handleRenameCharName(sess *net.Session, input string) { - newName := strings.TrimSpace(input) - oldName := sess.PendingChar - if newName == "" { - sess.Write("New name: ") - return - } - if verr := player.ValidName(newName); verr != nil { - sess.WriteLine(verr.Error()) - sess.Write("New name: ") - return - } - if newName == oldName { - sess.WriteLine("That's already the character's name.") - g.showMenu(sess) - return - } - if g.AccountStore.CharacterExists(newName) { - sess.WriteLine("A character with that name already exists.") - sess.Write("New name: ") - return - } - - oldPath := g.AccountStore.CharPath(oldName) - newPath := g.AccountStore.CharPath(newName) - if err := os.Rename(oldPath, newPath); err != nil { - sess.WriteLine(fmt.Sprintf("Error renaming: %v", err)) - g.showMenu(sess) - return - } - - p, _ := g.AccountStore.LoadCharacter(newName) - if p != nil { - p.Name = newName - g.AccountStore.SaveCharacter(p) - } - - acc, _ := g.AccountStore.LoadAccount(sess.Account.Name) - for i, c := range acc.Characters { - if c == oldName { - acc.Characters[i] = newName - break - } - } - g.AccountStore.SaveAccount(acc) - sess.Account.Characters = acc.Characters - - sess.PendingChar = "" - sess.WriteLine(fmt.Sprintf("%s renamed to %s.", oldName, newName)) - g.showMenu(sess) -} - -func (g *Game) showDeleteConfirm(sess *net.Session) { - sess.State = net.StateDeleteChar - sess.WriteLine(fmt.Sprintf("\nDelete %s? Type DELETE %s to confirm, or anything else to cancel.", sess.PendingChar, sess.PendingChar)) -} - -func (g *Game) handleDeleteChar(sess *net.Session, input string) { - if sess.PendingChar == "" { - name := strings.TrimSpace(input) - if idx, err := parseIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) { - sess.PendingChar = sess.Account.Characters[idx-1] - } else { - sess.PendingChar = name - } - found := false - for _, c := range sess.Account.Characters { - if c == sess.PendingChar { - found = true - break - } - } - if !found { - sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar)) - sess.PendingChar = "" - g.showMenu(sess) - return - } - g.showDeleteConfirm(sess) - return - } - - input = strings.TrimSpace(input) - expected := "DELETE " + sess.PendingChar - if strings.ToUpper(input) != strings.ToUpper(expected) { - sess.WriteLine("Delete cancelled.") - sess.PendingChar = "" - g.showMenu(sess) - return - } - - charPath := g.AccountStore.CharPath(sess.PendingChar) - if err := os.Remove(charPath); err != nil { - sess.WriteLine(fmt.Sprintf("Error deleting: %v", err)) - sess.PendingChar = "" - g.showMenu(sess) - return - } - - acc, _ := g.AccountStore.LoadAccount(sess.Account.Name) - var newChars []string - for _, c := range acc.Characters { - if c != sess.PendingChar { - newChars = append(newChars, c) - } - } - acc.Characters = newChars - g.AccountStore.SaveAccount(acc) - sess.Account.Characters = newChars - - sess.WriteLine(fmt.Sprintf("%s has been deleted.", sess.PendingChar)) - sess.PendingChar = "" - g.showMenu(sess) -} - func (g *Game) handlePurgeAccount(sess *net.Session, input string) { input = strings.TrimSpace(input) expected := "PURGE " + sess.Account.Name diff --git a/internal/game/login_char.go b/internal/game/login_char.go new file mode 100644 index 0000000..ba034bb --- /dev/null +++ b/internal/game/login_char.go @@ -0,0 +1,236 @@ +package game + +import ( + "fmt" + "os" + "strings" + + "thirdcollapse/internal/net" + "thirdcollapse/internal/player" +) + +func (g *Game) handleNewCharName(sess *net.Session, input string) { + name := strings.TrimSpace(input) + + if sess.PendingChar == "connect" && len(sess.Account.Characters) > 0 { + if idx, err := parseChoiceIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) { + sess.PendingChar = "" + g.connectCharacter(sess, sess.Account.Characters[idx-1]) + return + } + sess.WriteLine("Invalid choice.") + sess.Write("\n> ") + return + } + + if name == "" { + sess.Write("Character name: ") + return + } + + if verr := player.ValidName(name); verr != nil { + sess.WriteLine(verr.Error()) + sess.Write("Character name: ") + return + } + + if g.AccountStore.CharacterExists(name) { + sess.WriteLine("A character with that name already exists.") + sess.Write("Character name: ") + return + } + + p := player.New(name) + p.RoomID = 1 + + if err := g.AccountStore.SaveCharacter(p); err != nil { + sess.WriteLine(fmt.Sprintf("Error creating character: %v", err)) + sess.State = net.StateMenu + g.showMenu(sess) + return + } + + acc, _ := g.AccountStore.LoadAccount(sess.Account.Name) + acc.Characters = append(acc.Characters, name) + g.AccountStore.SaveAccount(acc) + sess.Account.Characters = acc.Characters + + g.connectCharacter(sess, name) +} + +func (g *Game) connectCharacter(sess *net.Session, name string) { + g.charsMu.Lock() + if existing := g.loggedInChars[name]; existing != nil { + g.charsMu.Unlock() + sess.WriteLine("This character is logged in elsewhere.") + sess.State = net.StateMenu + g.showMenu(sess) + return + } + + p, err := g.AccountStore.LoadCharacter(name) + if err != nil { + g.charsMu.Unlock() + sess.WriteLine(fmt.Sprintf("Error loading character: %v", err)) + sess.State = net.StateMenu + g.showMenu(sess) + return + } + + sess.Player = p + sess.State = net.StateGame + g.loggedInChars[name] = sess + g.charsMu.Unlock() + + g.World.SeedGroundItems(p.RoomID) + g.seedRoomMobs(p.RoomID) + g.seedRoomObjects(p.RoomID) + + if g.Hub != nil { + g.Hub.EnterRoom(sess, p.RoomID) + } + + g.doLook(sess) + sess.Write("\r\n> ") +} + +func (g *Game) handleRenameChar(sess *net.Session, input string) { + name := strings.TrimSpace(input) + if idx, err := parseChoiceIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) { + sess.PendingChar = sess.Account.Characters[idx-1] + } else { + sess.PendingChar = name + } + + found := false + for _, c := range sess.Account.Characters { + if c == sess.PendingChar { + found = true + break + } + } + if !found { + sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar)) + g.showMenu(sess) + return + } + + sess.State = net.StateRenameCharName + sess.WriteLine(fmt.Sprintf("Renaming %s.", sess.PendingChar)) + sess.Write("New name: ") +} + +func (g *Game) handleRenameCharName(sess *net.Session, input string) { + newName := strings.TrimSpace(input) + oldName := sess.PendingChar + if newName == "" { + sess.Write("New name: ") + return + } + if verr := player.ValidName(newName); verr != nil { + sess.WriteLine(verr.Error()) + sess.Write("New name: ") + return + } + if newName == oldName { + sess.WriteLine("That's already the character's name.") + g.showMenu(sess) + return + } + if g.AccountStore.CharacterExists(newName) { + sess.WriteLine("A character with that name already exists.") + sess.Write("New name: ") + return + } + + oldPath := g.AccountStore.CharPath(oldName) + newPath := g.AccountStore.CharPath(newName) + if err := os.Rename(oldPath, newPath); err != nil { + sess.WriteLine(fmt.Sprintf("Error renaming: %v", err)) + g.showMenu(sess) + return + } + + p, _ := g.AccountStore.LoadCharacter(newName) + if p != nil { + p.Name = newName + g.AccountStore.SaveCharacter(p) + } + + acc, _ := g.AccountStore.LoadAccount(sess.Account.Name) + for i, c := range acc.Characters { + if c == oldName { + acc.Characters[i] = newName + break + } + } + g.AccountStore.SaveAccount(acc) + sess.Account.Characters = acc.Characters + + sess.PendingChar = "" + sess.WriteLine(fmt.Sprintf("%s renamed to %s.", oldName, newName)) + g.showMenu(sess) +} + +func (g *Game) showDeleteConfirm(sess *net.Session) { + sess.State = net.StateDeleteChar + sess.WriteLine(fmt.Sprintf("\nDelete %s? Type DELETE %s to confirm, or anything else to cancel.", sess.PendingChar, sess.PendingChar)) +} + +func (g *Game) handleDeleteChar(sess *net.Session, input string) { + if sess.PendingChar == "" { + name := strings.TrimSpace(input) + if idx, err := parseChoiceIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) { + sess.PendingChar = sess.Account.Characters[idx-1] + } else { + sess.PendingChar = name + } + found := false + for _, c := range sess.Account.Characters { + if c == sess.PendingChar { + found = true + break + } + } + if !found { + sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar)) + sess.PendingChar = "" + g.showMenu(sess) + return + } + g.showDeleteConfirm(sess) + return + } + + input = strings.TrimSpace(input) + expected := "DELETE " + sess.PendingChar + if strings.ToUpper(input) != strings.ToUpper(expected) { + sess.WriteLine("Delete cancelled.") + sess.PendingChar = "" + g.showMenu(sess) + return + } + + charPath := g.AccountStore.CharPath(sess.PendingChar) + if err := os.Remove(charPath); err != nil { + sess.WriteLine(fmt.Sprintf("Error deleting: %v", err)) + sess.PendingChar = "" + g.showMenu(sess) + return + } + + acc, _ := g.AccountStore.LoadAccount(sess.Account.Name) + var newChars []string + for _, c := range acc.Characters { + if c != sess.PendingChar { + newChars = append(newChars, c) + } + } + acc.Characters = newChars + g.AccountStore.SaveAccount(acc) + sess.Account.Characters = newChars + + sess.WriteLine(fmt.Sprintf("%s has been deleted.", sess.PendingChar)) + sess.PendingChar = "" + g.showMenu(sess) +} diff --git a/internal/game/map.go b/internal/game/map.go index cd1012b..e7738bd 100644 --- a/internal/game/map.go +++ b/internal/game/map.go @@ -1,6 +1,8 @@ package game import ( + "strings" + "thirdcollapse/internal/world" ) @@ -189,3 +191,103 @@ func roomMapSymbol(g *Game, roomID int) rune { } return 'o' } + +func stripBlankRows(lines []string) []string { + var out []string + for _, line := range lines { + if strings.TrimSpace(line) != "" { + out = append(out, line) + } + } + return out +} + +func leftTrimCommon(lines []string) []string { + min := -1 + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + n := 0 + for _, r := range line { + if r == ' ' { + n++ + } else { + break + } + } + if min < 0 || n < min { + min = n + } + } + if min <= 0 { + return lines + } + result := make([]string, len(lines)) + for i, line := range lines { + if len(line) <= min { + result[i] = "" + } else { + result[i] = line[min:] + } + } + return result +} + +func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string { + mg := buildGraph(g, roomID) + + grid := make([][]rune, mapHeight) + for i := range grid { + grid[i] = make([]rune, mapWidth) + for j := range grid[i] { + grid[i][j] = ' ' + } + } + + cx := mapWidth / 2 + cy := mapHeight / 2 + + for pos, rid := range mg.posToRoom { + gr := cy + pos[1]*2 + gc := cx + pos[0]*2 + if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { + continue + } + if rid == roomID { + grid[gr][gc] = '@' + } else { + grid[gr][gc] = roomMapSymbol(g, rid) + } + } + + for pos, rid := range mg.posToRoom { + x, y := pos[0], pos[1] + + if rightID, ok := mg.posToRoom[[2]int{x + 1, y}]; ok { + if exitsConnect(g, rid, rightID, world.East, world.West) { + gr := cy + y*2 + gc := cx + x*2 + 1 + if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { + grid[gr][gc] = '─' + } + } + } + + if bottomID, ok := mg.posToRoom[[2]int{x, y + 1}]; ok { + if exitsConnect(g, rid, bottomID, world.South, world.North) { + gr := cy + y*2 + 1 + gc := cx + x*2 + if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth { + grid[gr][gc] = '│' + } + } + } + } + + lines := make([]string, mapHeight) + for i := range grid { + lines[i] = string(grid[i]) + } + return lines +} diff --git a/internal/game/types.go b/internal/game/types.go new file mode 100644 index 0000000..faed9e1 --- /dev/null +++ b/internal/game/types.go @@ -0,0 +1,29 @@ +package game + +import "thirdcollapse/internal/object" + +var EquipSlots = []object.EquipSlot{ + object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs, + object.SlotHands, object.SlotFeet, object.SlotBack, object.SlotAmmo, + object.SlotMainHand, object.SlotOffHand, object.SlotRing, +} + +type itemMatch struct { + ID string + Name string + Slot int +} + +type xpGain struct { + Skill string + XP int +} + +type deathDrop struct { + itemID string + quantity int + totalVal int + isEquip bool + equipSlot object.EquipSlot + invSlot int +} diff --git a/internal/game/utils.go b/internal/game/utils.go index 969eba4..e80de24 100644 --- a/internal/game/utils.go +++ b/internal/game/utils.go @@ -7,37 +7,10 @@ import ( "strings" "thirdcollapse/internal/net" - "thirdcollapse/internal/object" "thirdcollapse/internal/player" "thirdcollapse/internal/world" ) -var EquipSlots = []object.EquipSlot{ - object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs, - object.SlotHands, object.SlotFeet, object.SlotBack, object.SlotAmmo, - object.SlotMainHand, object.SlotOffHand, object.SlotRing, -} - -type itemMatch struct { - ID string - Name string - Slot int -} - -type xpGain struct { - Skill string - XP int -} - -type deathDrop struct { - itemID string - quantity int - totalVal int - isEquip bool - equipSlot object.EquipSlot - invSlot int -} - func plural(n int) string { if n == 1 { return "" @@ -55,7 +28,7 @@ func parseQty(input string) (int, string) { return 0, input } -func parseIndex(s string) (int, error) { +func parseChoiceIndex(s string) (int, error) { var idx int _, err := fmt.Sscanf(s, "%d", &idx) return idx, err @@ -94,7 +67,7 @@ func randInt(max int) int { return rand.Intn(max) } -func innerPickupReport(sess *net.Session, picked []string) { +func formatPickupList(sess *net.Session, picked []string) { for i, name := range picked { if i == len(picked)-1 { sess.WriteLine(name) @@ -106,7 +79,7 @@ func innerPickupReport(sess *net.Session, picked []string) { } } -func actionDesc(p *player.Player) string { +func playerActionDescription(p *player.Player) string { if p.Action == nil { return "" } @@ -129,7 +102,7 @@ func actionDesc(p *player.Player) string { return "" } -func (g *Game) newInvSlot(itemID string, qty int) *player.InventorySlot { +func (g *Game) newInventorySlot(itemID string, qty int) *player.InventorySlot { slot := &player.InventorySlot{ItemID: itemID, Quantity: qty} if def, err := g.ItemStore.Load(itemID); err == nil && def.MaxQuality > 0 { slot.Quality = def.Quality |
