big ol refactor

This commit is contained in:
2025-04-16 12:13:29 +02:00
parent b866ac879e
commit 9d60d5e9cd
10 changed files with 747 additions and 382 deletions

View File

@ -11,13 +11,9 @@ import (
rl "github.com/gen2brain/raylib-go/raylib"
)
// Local UI constants (these could be moved to a centralized constants package later)
const (
maxMessages = 50
chatMargin = 10 // Margin from screen edges
chatHeight = 200
messageHeight = 20
inputHeight = 30
runeLimit = 256
runeLimit = 256
)
type Chat struct {
@ -32,7 +28,7 @@ type Chat struct {
func NewChat() *Chat {
return &Chat{
messages: make([]types.ChatMessage, 0, maxMessages),
messages: make([]types.ChatMessage, 0, types.MaxChatMessages),
inputBuffer: make([]rune, 0, runeLimit),
}
}
@ -44,7 +40,7 @@ func (c *Chat) AddMessage(playerID int32, content string) {
Time: time.Now(),
}
if len(c.messages) >= maxMessages {
if len(c.messages) >= types.MaxChatMessages {
c.messages = c.messages[1:]
}
c.messages = append(c.messages, msg)
@ -72,14 +68,14 @@ func (c *Chat) HandleServerMessages(messages []*pb.ChatMessage) {
// Only add if it's not already in our history
if len(c.messages) == 0 || c.messages[len(c.messages)-1].Time.UnixNano() < msg.Timestamp {
if len(c.messages) >= maxMessages {
if len(c.messages) >= types.MaxChatMessages {
c.messages = c.messages[1:]
}
c.messages = append(c.messages, localMsg)
log.Printf("Added chat message from %s: %s", msg.Username, msg.Content)
// Scroll to latest message if it's not already visible
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
visibleMessages := int((types.ChatHeight - types.InputHeight) / types.MessageHeight)
if len(c.messages) > visibleMessages {
c.scrollOffset = len(c.messages) - visibleMessages
}
@ -114,16 +110,16 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
defer c.mutex.RUnlock()
// Calculate chat window width based on screen width
chatWindowWidth := screenWidth - (chatMargin * 2)
chatWindowWidth := screenWidth - (types.ChatMargin * 2)
// Draw chat window background
chatX := float32(chatMargin)
chatY := float32(screenHeight - chatHeight - chatMargin)
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, chatHeight, rl.ColorAlpha(rl.Black, 0.5))
chatX := float32(types.ChatMargin)
chatY := float32(screenHeight - types.ChatHeight - types.ChatMargin)
rl.DrawRectangle(int32(chatX), int32(chatY), chatWindowWidth, types.ChatHeight, rl.ColorAlpha(rl.Black, 0.5))
// Draw messages from oldest to newest
messageY := chatY + 5
visibleMessages := int((chatHeight - inputHeight) / messageHeight)
visibleMessages := int((types.ChatHeight - types.InputHeight) / types.MessageHeight)
// Auto-scroll to bottom if no manual scrolling has occurred
if c.scrollOffset == 0 {
@ -145,12 +141,12 @@ func (c *Chat) Draw(screenWidth, screenHeight int32) {
}
text := fmt.Sprintf("%s: %s", msg.Username, msg.Content)
rl.DrawText(text, int32(chatX)+5, int32(messageY), 20, color)
messageY += messageHeight
messageY += types.MessageHeight
}
// Draw input field
inputY := chatY + float32(chatHeight-inputHeight)
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, inputHeight, rl.ColorAlpha(rl.White, 0.3))
inputY := chatY + float32(types.ChatHeight-types.InputHeight)
rl.DrawRectangle(int32(chatX), int32(inputY), chatWindowWidth, types.InputHeight, rl.ColorAlpha(rl.White, 0.3))
if c.isTyping {
inputText := string(c.inputBuffer)
rl.DrawText(inputText, int32(chatX)+5, int32(inputY)+5, 20, rl.White)
@ -168,7 +164,7 @@ func (c *Chat) Update() (string, bool) {
if !c.isTyping {
wheelMove := rl.GetMouseWheelMove()
if wheelMove != 0 {
maxScroll := max(0, len(c.messages)-int((chatHeight-inputHeight)/messageHeight))
maxScroll := max(0, len(c.messages)-int((types.ChatHeight-types.InputHeight)/types.MessageHeight))
c.scrollOffset = clamp(c.scrollOffset-int(wheelMove), 0, maxScroll)
}

107
game/components.go Normal file
View File

@ -0,0 +1,107 @@
package game
import (
"sync"
"gitea.boner.be/bdnugget/goonscape/types"
rl "github.com/gen2brain/raylib-go/raylib"
)
// PlayerManager handles all player-related operations
type PlayerManager struct {
LocalPlayer *types.Player
OtherPlayers map[int32]*types.Player
mutex sync.RWMutex
}
// NewPlayerManager creates a new player manager
func NewPlayerManager() *PlayerManager {
return &PlayerManager{
OtherPlayers: make(map[int32]*types.Player),
}
}
// GetPlayer returns the player with the given ID, or the local player if ID matches
func (pm *PlayerManager) GetPlayer(id int32) *types.Player {
pm.mutex.RLock()
defer pm.mutex.RUnlock()
if pm.LocalPlayer != nil && pm.LocalPlayer.ID == id {
return pm.LocalPlayer
}
return pm.OtherPlayers[id]
}
// AddPlayer adds a player to the manager
func (pm *PlayerManager) AddPlayer(player *types.Player) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.OtherPlayers[player.ID] = player
}
// RemovePlayer removes a player from the manager
func (pm *PlayerManager) RemovePlayer(id int32) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
delete(pm.OtherPlayers, id)
}
// AssetManager handles all game assets
type AssetManager struct {
Models []types.ModelAsset
Music rl.Music
}
// NewAssetManager creates a new asset manager
func NewAssetManager() *AssetManager {
return &AssetManager{}
}
// GetModelForPlayer returns the appropriate model for a player
func (am *AssetManager) GetModelForPlayer(playerID int32) (types.ModelAsset, bool) {
if len(am.Models) == 0 {
return types.ModelAsset{}, false
}
// Simple model assignment based on player ID
modelIndex := int(playerID) % len(am.Models)
return am.Models[modelIndex], true
}
// UIManager manages all user interface components
type UIManager struct {
Chat *Chat
LoginScreen *LoginScreen
IsLoggedIn bool
MenuOpen bool
}
// NewUIManager creates a new UI manager
func NewUIManager() *UIManager {
return &UIManager{
Chat: NewChat(),
LoginScreen: NewLoginScreen(),
}
}
// HandleChatInput processes chat input and returns messages to send
func (ui *UIManager) HandleChatInput() (string, bool) {
return ui.Chat.Update()
}
// DrawUI renders all UI components
func (ui *UIManager) DrawUI(screenWidth, screenHeight int32) {
if !ui.IsLoggedIn {
ui.LoginScreen.Draw()
} else {
if ui.MenuOpen {
// Draw menu
}
// Draw chat always when logged in
ui.Chat.Draw(screenWidth, screenHeight)
}
}

View File

@ -1,7 +1,7 @@
package game
import (
"fmt"
"log"
"sync"
"time"
@ -13,135 +13,163 @@ import (
)
type Game struct {
Player *types.Player
OtherPlayers map[int32]*types.Player
// Component-based architecture
PlayerManager *PlayerManager
AssetManager *AssetManager
UIManager *UIManager
// Core game state
Camera rl.Camera3D
Models []types.ModelAsset
Music rl.Music
Chat *Chat
MenuOpen bool
quitChan chan struct{}
loginScreen *LoginScreen
isLoggedIn bool
cleanupOnce sync.Once
frameCounter int // For periodic logging
// Legacy fields for backward compatibility
Player *types.Player // Use PlayerManager.LocalPlayer instead
OtherPlayers map[int32]*types.Player // Use PlayerManager.OtherPlayers instead
Models []types.ModelAsset // Use AssetManager.Models instead
Music rl.Music // Use AssetManager.Music instead
Chat *Chat // Use UIManager.Chat instead
MenuOpen bool // Use UIManager.MenuOpen instead
loginScreen *LoginScreen // Use UIManager.LoginScreen instead
isLoggedIn bool // Use UIManager.IsLoggedIn instead
}
func New() *Game {
InitWorld()
game := &Game{
OtherPlayers: make(map[int32]*types.Player),
// Create managers
playerManager := NewPlayerManager()
assetManager := NewAssetManager()
uiManager := NewUIManager()
g := &Game{
PlayerManager: playerManager,
AssetManager: assetManager,
UIManager: uiManager,
Camera: rl.Camera3D{
Position: rl.NewVector3(0, 10, 10),
Target: rl.NewVector3(0, 0, 0),
Up: rl.NewVector3(0, 1, 0),
Position: rl.NewVector3(0.0, 20.0, 0.0),
Target: rl.NewVector3(0.0, 0.0, 0.0),
Up: rl.NewVector3(0.0, 1.0, 0.0),
Fovy: 45.0,
Projection: rl.CameraPerspective,
},
Chat: NewChat(),
quitChan: make(chan struct{}),
loginScreen: NewLoginScreen(),
quitChan: make(chan struct{}),
}
game.Chat.userData = game
return game
// Initialize legacy fields (for backward compatibility)
g.Player = g.PlayerManager.LocalPlayer
g.OtherPlayers = g.PlayerManager.OtherPlayers
g.Models = g.AssetManager.Models
g.Music = g.AssetManager.Music
g.Chat = g.UIManager.Chat
g.MenuOpen = g.UIManager.MenuOpen
g.loginScreen = g.UIManager.LoginScreen
g.isLoggedIn = g.UIManager.IsLoggedIn
// Set up inter-component references
g.Chat.userData = g // Pass game instance to chat for callbacks
// Initialize world
InitWorld()
return g
}
func (g *Game) LoadAssets() error {
var loadErr error
defer func() {
if r := recover(); r != nil {
loadErr = fmt.Errorf("panic during asset loading: %v", r)
// Cleanup any partially loaded assets
g.Cleanup()
return SafeExecute(func() error {
// Load models
var err error
models, err := assets.LoadModels()
if err != nil {
log.Printf("Warning: Failed to load models: %v", err)
}
}()
g.AssetManager.Models = models
// Load models with better error handling
g.Models, loadErr = assets.LoadModels()
if loadErr != nil {
return fmt.Errorf("failed to load models: %v", loadErr)
}
// Update legacy field
g.Models = models
// Verify model loading
for i, model := range g.Models {
if model.Model.Meshes == nil {
return fmt.Errorf("model %d failed to load properly", i)
// Try to load music
music, err := assets.LoadMusic("resources/audio/music.mp3")
if err != nil {
log.Printf("Warning: Failed to load music: %v", err)
} else {
g.AssetManager.Music = music
// Update legacy field
g.Music = music
}
}
// Load music with better error handling
g.Music, loadErr = assets.LoadMusic("resources/audio/GoonScape2.mp3")
if loadErr != nil {
return fmt.Errorf("failed to load music: %v", loadErr)
}
return nil
return nil
})
}
func (g *Game) Update(deltaTime float32) {
if !g.isLoggedIn {
username, password, isRegistering, submitted := g.loginScreen.Update()
if submitted {
// Legacy code to maintain compatibility
if !g.UIManager.IsLoggedIn {
// Handle login
username, password, isRegistering, doAuth := g.UIManager.LoginScreen.Update()
// Update legacy fields
g.isLoggedIn = g.UIManager.IsLoggedIn
if doAuth {
conn, playerID, err := network.ConnectToServer(username, password, isRegistering)
if err != nil {
g.loginScreen.SetError(err.Error())
g.UIManager.LoginScreen.SetError(err.Error())
return
}
g.Player = &types.Player{
g.PlayerManager.LocalPlayer = &types.Player{
Speed: 50.0,
TargetPath: []types.Tile{},
UserData: g,
QuitDone: make(chan struct{}),
ID: playerID,
}
g.AssignModelToPlayer(g.Player)
g.AssignModelToPlayer(g.PlayerManager.LocalPlayer)
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.quitChan)
g.isLoggedIn = true
go network.HandleServerCommunication(conn, playerID, g.PlayerManager.LocalPlayer, g.PlayerManager.OtherPlayers, g.quitChan)
g.UIManager.IsLoggedIn = true
}
return
}
// Handle ESC for menu
if rl.IsKeyPressed(rl.KeyEscape) {
g.MenuOpen = !g.MenuOpen
g.UIManager.MenuOpen = !g.UIManager.MenuOpen
return
}
// Don't process other inputs if menu is open
if g.MenuOpen {
if g.UIManager.MenuOpen {
return
}
if message, sent := g.Chat.Update(); sent {
g.Player.Lock()
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
if message, sent := g.UIManager.Chat.Update(); sent {
g.PlayerManager.LocalPlayer.Lock()
g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
Type: pb.Action_CHAT,
ChatMessage: message,
PlayerId: g.Player.ID,
PlayerId: g.PlayerManager.LocalPlayer.ID,
})
g.Player.Unlock()
g.PlayerManager.LocalPlayer.Unlock()
}
g.HandleInput()
if len(g.Player.TargetPath) > 0 {
g.Player.MoveTowards(g.Player.TargetPath[0], deltaTime, GetMapGrid())
if len(g.PlayerManager.LocalPlayer.TargetPath) > 0 {
g.PlayerManager.LocalPlayer.MoveTowards(g.PlayerManager.LocalPlayer.TargetPath[0], deltaTime, GetMapGrid())
}
// Periodically log information about other players
g.frameCounter++
if g.frameCounter%300 == 0 {
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.OtherPlayers))
for id, other := range g.OtherPlayers {
rl.TraceLog(rl.LogInfo, "There are %d other players", len(g.PlayerManager.OtherPlayers))
for id, other := range g.PlayerManager.OtherPlayers {
rl.TraceLog(rl.LogInfo, "Other player ID: %d, Position: (%f, %f, %f), Has model: %v",
id, other.PosActual.X, other.PosActual.Y, other.PosActual.Z, other.Model.Meshes != nil)
}
}
// Process other players
for _, other := range g.OtherPlayers {
for _, other := range g.PlayerManager.OtherPlayers {
if other == nil {
continue
}
@ -157,7 +185,19 @@ func (g *Game) Update(deltaTime float32) {
}
}
UpdateCamera(&g.Camera, g.Player.PosActual, deltaTime)
UpdateCamera(&g.Camera, g.PlayerManager.LocalPlayer.PosActual, deltaTime)
// Update music if available
if g.AssetManager.Music.Stream.Buffer != nil {
rl.UpdateMusicStream(g.AssetManager.Music)
}
// Update legacy fields
g.Player = g.PlayerManager.LocalPlayer
g.OtherPlayers = g.PlayerManager.OtherPlayers
g.Models = g.AssetManager.Models
g.Music = g.AssetManager.Music
g.MenuOpen = g.UIManager.MenuOpen
}
func (g *Game) DrawMap() {
@ -280,8 +320,8 @@ func (g *Game) Render() {
rl.ClearBackground(rl.RayWhite)
if !g.isLoggedIn {
g.loginScreen.Draw()
if !g.UIManager.IsLoggedIn {
g.UIManager.LoginScreen.Draw()
return
}
@ -289,12 +329,12 @@ func (g *Game) Render() {
g.DrawMap()
// Draw player only if valid
if g.Player != nil && g.Player.Model.Meshes != nil {
g.DrawPlayer(g.Player, g.Player.Model)
if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.Model.Meshes != nil {
g.DrawPlayer(g.PlayerManager.LocalPlayer, g.PlayerManager.LocalPlayer.Model)
}
// Draw other players with defensive checks
for _, other := range g.OtherPlayers {
for _, other := range g.PlayerManager.OtherPlayers {
if other == nil {
continue
}
@ -332,24 +372,24 @@ func (g *Game) Render() {
rl.DrawText(text, int32(pos.X)-textWidth/2, int32(pos.Y), 20, rl.Yellow)
}
if g.Player != nil && g.Player.FloatingMessage != nil {
drawFloatingMessage(g.Player.FloatingMessage)
if g.PlayerManager.LocalPlayer != nil && g.PlayerManager.LocalPlayer.FloatingMessage != nil {
drawFloatingMessage(g.PlayerManager.LocalPlayer.FloatingMessage)
}
for _, other := range g.OtherPlayers {
for _, other := range g.PlayerManager.OtherPlayers {
if other != nil && other.FloatingMessage != nil {
drawFloatingMessage(other.FloatingMessage)
}
}
// Draw menu if open
if g.MenuOpen {
if g.UIManager.MenuOpen {
g.DrawMenu()
}
// Only draw chat if menu is not open
if !g.MenuOpen && g.Chat != nil {
g.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
if !g.UIManager.MenuOpen && g.UIManager.Chat != nil {
g.UIManager.Chat.Draw(int32(rl.GetScreenWidth()), int32(rl.GetScreenHeight()))
}
rl.DrawFPS(10, 10)
@ -357,35 +397,37 @@ func (g *Game) Render() {
func (g *Game) Cleanup() {
g.cleanupOnce.Do(func() {
// Stop music first
if g.Music.Stream.Buffer != nil {
rl.StopMusicStream(g.Music)
rl.UnloadMusicStream(g.Music)
}
// Unload textures
for _, model := range g.Models {
// Cleanup models
for _, model := range g.AssetManager.Models {
rl.UnloadModel(model.Model)
if model.Texture.ID > 0 {
rl.UnloadTexture(model.Texture)
}
}
// Unload music
if g.AssetManager.Music.Stream.Buffer != nil {
rl.UnloadMusicStream(g.AssetManager.Music)
}
close(g.quitChan)
})
}
func (g *Game) HandleInput() {
clickedTile, clicked := g.GetTileAtMouse()
if clicked {
path := FindPath(GetTile(g.Player.PosTile.X, g.Player.PosTile.Y), clickedTile)
path := FindPath(GetTile(g.PlayerManager.LocalPlayer.PosTile.X, g.PlayerManager.LocalPlayer.PosTile.Y), clickedTile)
if len(path) > 1 {
g.Player.Lock()
g.Player.TargetPath = path[1:]
g.Player.ActionQueue = append(g.Player.ActionQueue, &pb.Action{
g.PlayerManager.LocalPlayer.Lock()
g.PlayerManager.LocalPlayer.TargetPath = path[1:]
g.PlayerManager.LocalPlayer.ActionQueue = append(g.PlayerManager.LocalPlayer.ActionQueue, &pb.Action{
Type: pb.Action_MOVE,
X: int32(clickedTile.X),
Y: int32(clickedTile.Y),
PlayerId: g.Player.ID,
PlayerId: g.PlayerManager.LocalPlayer.ID,
})
g.Player.Unlock()
g.PlayerManager.LocalPlayer.Unlock()
}
}
}
@ -428,7 +470,7 @@ func (g *Game) DrawMenu() {
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
switch item {
case "Resume":
g.MenuOpen = false
g.UIManager.MenuOpen = false
case "Settings":
// TODO: Implement settings
case "Exit Game":
@ -453,7 +495,7 @@ func (g *Game) Shutdown() {
}
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
g.Chat.HandleServerMessages(messages)
g.UIManager.Chat.HandleServerMessages(messages)
}
func (g *Game) AssignModelToPlayer(player *types.Player) {
@ -461,32 +503,18 @@ func (g *Game) AssignModelToPlayer(player *types.Player) {
return
}
// Defensive check for empty models array
if len(g.Models) == 0 {
rl.TraceLog(rl.LogWarning, "No models available to assign to player")
return
}
// Make sure model index is positive for consistent player appearances
// Use abs value of ID to ensure consistent appearance for negative IDs
modelIndex := abs(int(player.ID)) % len(g.Models)
if modelIndex < 0 || modelIndex >= len(g.Models) {
// Prevent out of bounds access
modelIndex = 0
}
rl.TraceLog(rl.LogInfo, "Assigning model %d to player %d", modelIndex, player.ID)
modelAsset := g.Models[modelIndex]
// Validate model before assigning
if modelAsset.Model.Meshes == nil {
rl.TraceLog(rl.LogWarning, "Trying to assign invalid model to player %d", player.ID)
modelAsset, found := g.AssetManager.GetModelForPlayer(player.ID)
if !found {
return
}
player.Model = modelAsset.Model
player.Texture = modelAsset.Texture
player.PlaceholderColor = modelAsset.PlaceholderColor
// Initialize animations if available
if len(modelAsset.Animations.Idle) > 0 || len(modelAsset.Animations.Walk) > 0 {
player.InitializeAnimations(modelAsset.Animations)
}
}
func (g *Game) QuitChan() <-chan struct{} {

View File

@ -1,91 +1,157 @@
package game
import (
"container/heap"
"fmt"
"gitea.boner.be/bdnugget/goonscape/types"
)
// Node represents a node in the A* pathfinding algorithm
type Node struct {
Tile types.Tile
Parent *Node
G, H, F float32
G, H, F float32 // G = cost from start, H = heuristic to goal, F = G + H
}
// PriorityQueue implements a min-heap for nodes ordered by F value
type PriorityQueue []*Node
// Implement the heap.Interface for PriorityQueue
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].F < pq[j].F
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
func (pq *PriorityQueue) Push(x interface{}) {
item := x.(*Node)
*pq = append(*pq, item)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[0 : n-1]
return item
}
// Helper to check if tile is in priority queue
func isInQueue(queue *PriorityQueue, tile types.Tile) (bool, *Node) {
for _, node := range *queue {
if node.Tile.X == tile.X && node.Tile.Y == tile.Y {
return true, node
}
}
return false, nil
}
// FindPath implements A* pathfinding algorithm with a priority queue
func FindPath(start, end types.Tile) []types.Tile {
openList := []*Node{}
closedList := make(map[[2]int]bool)
// Initialize open and closed sets
openSet := &PriorityQueue{}
heap.Init(openSet)
startNode := &Node{Tile: start, G: 0, H: heuristic(start, end)}
closedSet := make(map[[2]int]bool)
// Create start node and add to open set
startNode := &Node{
Tile: start,
Parent: nil,
G: 0,
H: heuristic(start, end),
}
startNode.F = startNode.G + startNode.H
openList = append(openList, startNode)
heap.Push(openSet, startNode)
for len(openList) > 0 {
current := openList[0]
currentIndex := 0
for i, node := range openList {
if node.F < current.F {
current = node
currentIndex = i
}
}
openList = append(openList[:currentIndex], openList[currentIndex+1:]...)
closedList[[2]int{current.Tile.X, current.Tile.Y}] = true
// Main search loop
for openSet.Len() > 0 {
// Get node with lowest F score
current := heap.Pop(openSet).(*Node)
// If we reached the goal, reconstruct and return the path
if current.Tile.X == end.X && current.Tile.Y == end.Y {
path := []types.Tile{}
node := current
for node != nil {
path = append([]types.Tile{node.Tile}, path...)
node = node.Parent
}
fmt.Printf("Path found: %v\n", path)
return path
return reconstructPath(current)
}
neighbors := GetNeighbors(current.Tile)
for _, neighbor := range neighbors {
if !neighbor.Walkable || closedList[[2]int{neighbor.X, neighbor.Y}] {
// Add current to closed set
closedSet[[2]int{current.Tile.X, current.Tile.Y}] = true
// Check all neighbors
for _, neighbor := range GetNeighbors(current.Tile) {
// Skip if in closed set or not walkable
if !neighbor.Walkable || closedSet[[2]int{neighbor.X, neighbor.Y}] {
continue
}
// Calculate tentative G score
tentativeG := current.G + distance(current.Tile, neighbor)
inOpen := false
var existingNode *Node
for _, node := range openList {
if node.Tile.X == neighbor.X && node.Tile.Y == neighbor.Y {
existingNode = node
inOpen = true
break
}
}
// Check if in open set
inOpen, existingNode := isInQueue(openSet, neighbor)
// If not in open set or better path found
if !inOpen || tentativeG < existingNode.G {
newNode := &Node{
Tile: neighbor,
Parent: current,
G: tentativeG,
H: heuristic(neighbor, end),
// Create or update the node
var neighborNode *Node
if inOpen {
neighborNode = existingNode
} else {
neighborNode = &Node{
Tile: neighbor,
Parent: current,
}
}
newNode.F = newNode.G + newNode.H
// Update scores
neighborNode.G = tentativeG
neighborNode.H = heuristic(neighbor, end)
neighborNode.F = neighborNode.G + neighborNode.H
neighborNode.Parent = current
// Add to open set if not already there
if !inOpen {
openList = append(openList, newNode)
heap.Push(openSet, neighborNode)
}
}
}
}
// No path found
return nil
}
// reconstructPath builds the path from goal node to start
func reconstructPath(node *Node) []types.Tile {
path := []types.Tile{}
current := node
// Follow parent pointers back to start
for current != nil {
path = append([]types.Tile{current.Tile}, path...)
current = current.Parent
}
fmt.Printf("Path found: %v\n", path)
return path
}
// heuristic estimates cost from current to goal (Manhattan distance)
func heuristic(a, b types.Tile) float32 {
return float32(abs(a.X-b.X) + abs(a.Y-b.Y))
}
// distance calculates cost between adjacent tiles
func distance(a, b types.Tile) float32 {
return 1.0 // uniform cost for now
}
// GetNeighbors returns walkable tiles adjacent to the given tile
func GetNeighbors(tile types.Tile) []types.Tile {
directions := [][2]int{
{1, 0}, {-1, 0}, {0, 1}, {0, -1},
@ -104,6 +170,7 @@ func GetNeighbors(tile types.Tile) []types.Tile {
return neighbors
}
// abs returns the absolute value of x
func abs(x int) int {
if x < 0 {
return -x

View File

@ -1,9 +1,36 @@
package game
import (
"fmt"
"log"
"runtime/debug"
rl "github.com/gen2brain/raylib-go/raylib"
)
// SafeExecute runs a function and recovers from panics
func SafeExecute(action func() error) (err error) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("Recovered from panic: %v\nStack trace:\n%s", r, stack)
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return action()
}
// SafeExecuteVoid runs a void function and recovers from panics
func SafeExecuteVoid(action func()) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("Recovered from panic: %v\nStack trace:\n%s", r, stack)
}
}()
action()
}
func RayIntersectsBox(ray rl.Ray, boxMin, boxMax rl.Vector3) bool {
tmin := (boxMin.X - ray.Position.X) / ray.Direction.X
tmax := (boxMax.X - ray.Position.X) / ray.Direction.X