diff --git a/catacombs.exe~ b/catacombs.exe~ new file mode 100644 index 0000000..2129430 Binary files /dev/null and b/catacombs.exe~ differ diff --git a/dungeon/fov.go b/dungeon/fov.go index dad6080..f5662c0 100644 --- a/dungeon/fov.go +++ b/dungeon/fov.go @@ -23,5 +23,13 @@ func GetRoomVisibility(floor *Floor, roomIdx int) Visibility { if floor.Rooms[roomIdx].Visited { return Visited } + // Neighbors of current room are dimly visible (so player can see where to go) + if floor.CurrentRoom >= 0 && floor.CurrentRoom < len(floor.Rooms) { + for _, n := range floor.Rooms[floor.CurrentRoom].Neighbors { + if n == roomIdx { + return Visited + } + } + } return Hidden } diff --git a/dungeon/render.go b/dungeon/render.go index f591e7d..0849399 100644 --- a/dungeon/render.go +++ b/dungeon/render.go @@ -112,8 +112,18 @@ func RenderFloor(floor *Floor, currentRoom int, showFog bool) string { cx := room.X + room.W/2 cy := room.Y + room.H/2 vis := GetRoomVisibility(floor, i) - if !showFog || vis != Hidden { - sym, col := roomMarker(room, i == currentRoom) + // Show current room and its neighbors (so player knows where to go) + showRoom := !showFog || vis != Hidden + if !showRoom && currentRoom >= 0 && currentRoom < len(floor.Rooms) { + for _, n := range floor.Rooms[currentRoom].Neighbors { + if n == i { + showRoom = true + break + } + } + } + if showRoom { + sym, col := roomMarker(room, i, i == currentRoom) markers[[2]int{cy, cx}] = marker{sym, col} } } @@ -202,24 +212,26 @@ func RenderFloor(floor *Floor, currentRoom int, showFog bool) string { return string(buf) } -func roomMarker(room *Room, isCurrent bool) (string, string) { +func roomMarker(room *Room, roomIdx int, isCurrent bool) (string, string) { + // Show room index number so player knows which key to press + num := fmt.Sprintf("%d", roomIdx) if room.Cleared { - return ".", ansiFgGray + return num, ansiFgGray } switch room.Type { case RoomCombat: - return "D", ansiFgRed + return num, ansiFgRed case RoomTreasure: - return "$", ansiFgYellow + return num, ansiFgYellow case RoomShop: - return "S", ansiFgCyan + return num, ansiFgCyan case RoomEvent: - return "?", ansiFgMagenta + return num, ansiFgMagenta case RoomBoss: - return "B", ansiFgBrRed + return num, ansiFgBrRed case RoomEmpty: - return ".", ansiFgGray + return num, ansiFgGray default: - return " ", ansiReset + return num, ansiReset } } diff --git a/main.go b/main.go index adba40b..0510770 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,6 @@ package main import ( "log" "os" - "os/signal" - "syscall" "github.com/tolelom/catacombs/game" "github.com/tolelom/catacombs/server" @@ -22,16 +20,8 @@ func main() { lobby := game.NewLobby() - go func() { - if err := server.Start("0.0.0.0", 2222, lobby, db); err != nil { - log.Fatal(err) - } - }() - - log.Println("Catacombs server running on :2222") - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c - log.Println("Shutting down...") + log.Println("Catacombs server starting on :2222") + if err := server.Start("0.0.0.0", 2222, lobby, db); err != nil { + log.Fatal(err) + } } diff --git a/server/ssh.go b/server/ssh.go index 746c4e1..83b2898 100644 --- a/server/ssh.go +++ b/server/ssh.go @@ -21,6 +21,9 @@ func Start(host string, port int, lobby *game.Lobby, db *store.DB) error { wish.WithPublicKeyAuth(func(_ ssh.Context, _ ssh.PublicKey) bool { return true // accept all keys }), + wish.WithPasswordAuth(func(_ ssh.Context, _ string) bool { + return true // accept any password (game server, not secure shell) + }), wish.WithMiddleware( bubbletea.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) { pty, _, _ := s.Pty() diff --git a/ui/game_view.go b/ui/game_view.go index 1324484..8c65ec5 100644 --- a/ui/game_view.go +++ b/ui/game_view.go @@ -11,9 +11,9 @@ import ( "github.com/tolelom/catacombs/game" ) -func renderGame(state game.GameState, width, height int, targetCursor int) string { +func renderGame(state game.GameState, width, height int, targetCursor int, moveCursor int) string { mapView := renderMap(state.Floor) - hudView := renderHUD(state, targetCursor) + hudView := renderHUD(state, targetCursor, moveCursor) logView := renderCombatLog(state.CombatLog) return lipgloss.JoinVertical(lipgloss.Left, @@ -32,7 +32,7 @@ func renderMap(floor *dungeon.Floor) string { return header + "\n" + dungeon.RenderFloor(floor, floor.CurrentRoom, true) } -func renderHUD(state game.GameState, targetCursor int) string { +func renderHUD(state game.GameState, targetCursor int, moveCursor int) string { var sb strings.Builder border := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). @@ -112,7 +112,32 @@ func renderHUD(state game.GameState, targetCursor int) string { } } } else if state.Phase == game.PhaseExploring { - sb.WriteString("\nChoose a room to enter (number) or [Q] quit") + if state.Floor != nil && state.Floor.CurrentRoom >= 0 && state.Floor.CurrentRoom < len(state.Floor.Rooms) { + current := state.Floor.Rooms[state.Floor.CurrentRoom] + if len(current.Neighbors) > 0 { + sb.WriteString("\n") + selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Bold(true) + normalStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("255")) + for i, n := range current.Neighbors { + if n >= 0 && n < len(state.Floor.Rooms) { + r := state.Floor.Rooms[n] + status := r.Type.String() + if r.Cleared { + status = "Cleared" + } + marker := " " + style := normalStyle + if i == moveCursor { + marker = "> " + style = selectedStyle + } + sb.WriteString(style.Render(fmt.Sprintf("%sRoom %d: %s", marker, n, status))) + sb.WriteString("\n") + } + } + } + } + sb.WriteString("[Up/Down] Select [Enter] Move [Q] Quit") } return border.Render(sb.String()) diff --git a/ui/model.go b/ui/model.go index 51c894c..62ce826 100644 --- a/ui/model.go +++ b/ui/model.go @@ -46,6 +46,7 @@ type Model struct { classState classSelectState inputBuffer string targetCursor int + moveCursor int // selected neighbor index during exploration } func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *store.DB) Model { @@ -115,7 +116,7 @@ func (m Model) View() string { case screenClassSelect: return renderClassSelect(m.classState, m.width, m.height) case screenGame: - return renderGame(m.gameState, m.width, m.height, m.targetCursor) + return renderGame(m.gameState, m.width, m.height, m.targetCursor, m.moveCursor) case screenShop: return renderShop(m.gameState, m.width, m.height) case screenResult: @@ -312,16 +313,27 @@ func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch m.gameState.Phase { case game.PhaseExploring: - if key.String() >= "0" && key.String() <= "9" { - idx := int(key.String()[0] - '0') - if m.session != nil { - m.session.EnterRoom(idx) + neighbors := m.getNeighbors() + if isUp(key) { + if m.moveCursor > 0 { + m.moveCursor-- + } + } else if isDown(key) { + if m.moveCursor < len(neighbors)-1 { + m.moveCursor++ + } + } else if isEnter(key) { + if m.session != nil && len(neighbors) > 0 { + roomIdx := neighbors[m.moveCursor] + m.session.EnterRoom(roomIdx) m.gameState = m.session.GetState() - // If combat started, begin polling + m.moveCursor = 0 if m.gameState.Phase == game.PhaseCombat { return m, m.pollState() } } + } else if isQuit(key) { + return m, tea.Quit } case game.PhaseCombat: isPlayerDead := false @@ -361,6 +373,17 @@ func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +func (m Model) getNeighbors() []int { + if m.gameState.Floor == nil { + return nil + } + cur := m.gameState.Floor.CurrentRoom + if cur < 0 || cur >= len(m.gameState.Floor.Rooms) { + return nil + } + return m.gameState.Floor.Rooms[cur].Neighbors +} + func (m Model) updateShop(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyMsg); ok { switch key.String() {