package game import "core:math" import rl "vendor:raylib" Tile_Type :: enum { // TEMP light gray FLOOR, // TEMP DARK gray WALL, // TEMP gray LOW_WALL, // TREMP GREEN PICKUP, } Tile_Def :: struct { solid: bool, collider_offset_x, collider_offset_y: f32, collider_w, collider_h: f32 } // awesome odin sets the fixed size to the ammount of enum options tile_defs: [Tile_Type]Tile_Def = { .FLOOR = { solid = false }, .PICKUP = { solid = false }, .WALL = { solid = true, collider_offset_x = 0, collider_offset_y = 0, collider_w = TILE_SIZE, collider_h = TILE_SIZE }, .LOW_WALL = { solid = true, collider_offset_x = 0, collider_offset_y = TILE_SIZE/2, collider_w = TILE_SIZE, collider_h = TILE_SIZE/2 }, } Grid_Pos :: [2]int Game_Map :: struct { tile_size: int, width: int, height: int, width_px: f32, height_px: f32, data: [400]int, // Flow Field props cost_field: [400]int, flow_field: [400]rl.Vector2, player_grid_pos: Grid_Pos } map_init :: proc() -> Game_Map { // @TODO: store maps and load them later // NOTE: flat, row-major — data[row * width + col] map_data := [400]int{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, } game := Game_Map{ width = 20, height = 20, tile_size = TILE_SIZE, data = map_data, } game.width_px = f32(game.width * game.tile_size) game.height_px = f32(game.height * game.tile_size) return game } // requires raw window-space // i.e mouse position clicked in window get that space in screen // returns screen-space pos, scaled to game resolution. window_to_screen :: proc(window_pos: rl.Vector2) -> rl.Vector2 { // window_width / game_width will give the scale back // invert it to scale down back to the render size screen_x := window_pos.x / (WINDOW_WIDTH / GAME_WIDTH) screen_y := window_pos.y / (WINDOW_HEIGHT / GAME_HEIGHT) return rl.Vector2{screen_x, screen_y} } // requires screen-space pos i.e mouse pos, scaled from window -> render size // returns world-space position (accounts for camera and its zoom) // remember uesd with raw input coordinates, not something already in world space (like player.pos). screen_to_world :: proc(screen_pos: rl.Vector2, camera: rl.Camera2D) -> rl.Vector2 { return rl.GetScreenToWorld2D(screen_pos, camera) } // @TODO: Do I need this?? Maybe for debug view?? world_to_screen :: proc(world_pos: rl.Vector2, camera: rl.Camera2D) -> rl.Vector2 { return rl.GetWorldToScreen2D(world_pos, camera) } // requires world-space position i.e player.pos, or output of screen_to_world // returns grid col/row not considering camera world_to_grid :: proc(world_pos: rl.Vector2) -> Grid_Pos { col := int(math.floor(world_pos.x / TILE_SIZE)) row := int(math.floor(world_pos.y / TILE_SIZE)) return Grid_Pos{col, row} } // returns world-space position grid pos top-left corner opposite of world_to_grid. grid_to_world :: proc(col, row: int) -> rl.Vector2 { world_x := f32(col * TILE_SIZE) world_y := f32(row * TILE_SIZE) return rl.Vector2{world_x, world_y} } grid_to_index :: proc(col, row, width: int) -> int { return (row * width) + col } // requires world x/y and then gives the rectangle in pixels that matches that get_world_tile_rect :: proc(gx, gy, offset_x, offset_y, w, h: f32) -> rl.Rectangle { return rl.Rectangle { (gx * TILE_SIZE) + offset_x, (gy * TILE_SIZE) + offset_y, w, h } } // Simple check for now is_tile_solid :: proc(tile_type: Tile_Type) -> bool { return tile_type == .WALL } // takes in the next position/rect for check compares it check_map_collision :: proc(rect: rl.Rectangle, game_map: Game_Map) -> bool { // collider top left point min_grid := world_to_grid(rl.Vector2{rect.x, rect.y}) // collider bot right point max_grid := world_to_grid(rl.Vector2{rect.x + rect.width, rect.y + rect.height}) // check all points along the x axis per y axis 'row' for gy := min_grid[1]; gy <= max_grid[1]; gy += 1 { for gx := min_grid[0]; gx <= max_grid[0]; gx += 1 { // bounds check if gx < 0 || gx >= game_map.width || gy < 0 || gy >= game_map.height do continue idx := (gy * game_map.width) + gx tile := Tile_Type(game_map.data[idx]) def := tile_defs[tile] if def.solid { tile_rect := get_world_tile_rect( f32(gx), f32(gy), def.collider_offset_x, def.collider_offset_y, def.collider_w, def.collider_h ); if rl.CheckCollisionRecs(rect, tile_rect) { return true } } } } return false } // true when collision found check_enemy_collision :: proc(rect_a: rl.Rectangle, active_enemies: int, enemies: ^[MAX_ENEMIES]Enemy) -> bool { for idx in 0..= 0 && grid_pos.x < game_map.width && grid_pos.y >= 0 && grid_pos.y < game_map.height { grid_pos_index := grid_to_index(grid_pos.x, grid_pos.y, game_map.width) if game_map.cost_field[grid_pos_index] == -1 { tile_t := Tile_Type(game_map.data[grid_pos_index]) def_t := tile_defs[tile_t] if !def_t.solid { game_map.cost_field[grid_pos_index] = current_cost + 1 append_elem(queue, grid_pos) } } } } compute_flow_directions :: proc(game_map: ^Game_Map) { // walk every tile, look at neighbor costs in cost_field, // write a direction vector into direction_field[index] for value, index in game_map.data { row := index / game_map.width column := index % game_map.width current_index := grid_to_index(column, row, game_map.width) // skip unreachable tiles if game_map.cost_field[current_index] == -1 do continue current_cost := game_map.cost_field[current_index] lowest_cost := current_cost // x = column, y = row — matches grid_to_world / screen space a := rl.Vector2{f32(column), f32(row)} b := a // Check all 8 neighbors (3x3 grid minus center): // (-1,-1) (0,-1) (1,-1) // (-1, 0) X (1, 0) // (-1, 1) (0, 1) (1, 1) for loop_y in -1..=1 { for loop_x in -1..=1 { if loop_x == 0 && loop_y == 0 do continue ncolumn := column + loop_x nrow := row + loop_y if ncolumn >= 0 && ncolumn < game_map.width && nrow >= 0 && nrow < game_map.height { cost_index := grid_to_index(ncolumn, nrow, game_map.width) neighbor_cost := game_map.cost_field[cost_index] if neighbor_cost != -1 && neighbor_cost < lowest_cost { lowest_cost = neighbor_cost b = rl.Vector2{f32(ncolumn), f32(nrow)} } } } } // point from current tile toward the lower-cost neighbor delta := b - a if delta == {0, 0} { game_map.flow_field[current_index] = {0, 0} } else { game_map.flow_field[current_index] = rl.Vector2Normalize(delta) } } }