diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..61153b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +run: + odin run src -out:game.exe -debug -use-separate-modules -sanitize:address + +build: + odin build src -out:game.exe -debug -use-separate-modules + +clean: + rm *.exe + rm -rf *.dSYM/ + diff --git a/assets/background.png b/assets/background.png new file mode 100644 index 0000000..216beed Binary files /dev/null and b/assets/background.png differ diff --git a/assets/player.png b/assets/player.png new file mode 100644 index 0000000..14b4c6b Binary files /dev/null and b/assets/player.png differ diff --git a/assets/sell_button.png b/assets/sell_button.png new file mode 100644 index 0000000..bfd0d56 Binary files /dev/null and b/assets/sell_button.png differ diff --git a/assets/tile_dirt_dry.png b/assets/tile_dirt_dry.png new file mode 100644 index 0000000..8432530 Binary files /dev/null and b/assets/tile_dirt_dry.png differ diff --git a/assets/tile_dirt_wet.png b/assets/tile_dirt_wet.png new file mode 100644 index 0000000..05af2ab Binary files /dev/null and b/assets/tile_dirt_wet.png differ diff --git a/assets/tomato.png b/assets/tomato.png new file mode 100644 index 0000000..266b808 Binary files /dev/null and b/assets/tomato.png differ diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..25afb1d --- /dev/null +++ b/ols.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "collections": [ + ], + "enable_references": true, + "enable_inlay_hints": true, + "enable_rename:": true, + "enable_semantic_tokens": true, + "enable_document_symbols": true, + "enable_hover": true, + "enable_snippets": true +} diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..fec4ff6 --- /dev/null +++ b/src/main.odin @@ -0,0 +1,410 @@ +package game + +import sa "core:container/small_array" +import "core:fmt" +import "core:math" +import "core:math/linalg" +import "core:slice" +import rl "vendor:raylib" + +GAME_SCREEN_WIDTH: f32 = 480 +GAME_SCREEN_HEIGHT: f32 = 270 + +SELL_BUTTON_POS :: rl.Vector2{432, 16} + +TILE_SIZE :: 16 +game_render_buffer: rl.RenderTexture2D +game_camera := rl.Camera2D { + zoom = 1, +} +ui_camera := rl.Camera2D { + zoom = 1, +} + +screen_scale: f32 +world_mouse: rl.Vector2 +screen_game_area: rl.Rectangle +game_render_buffer_area := rl.Rectangle{0, 0, GAME_SCREEN_WIDTH, -GAME_SCREEN_HEIGHT} + +player_sprite: []u8 : #load("../assets/player.png") +tomato_sprite: []u8 : #load("../assets/tomato.png") +sell_button_sprite: []u8 : #load("../assets/sell_button.png") +background_sprite: []u8 : #load("../assets/background.png") +tile_dirt_dry_sprite: []u8 : #load("../assets/tile_dirt_dry.png") +tile_dirt_wet_sprite: []u8 : #load("../assets/tile_dirt_wet.png") + +player_spritesheet_image := rl.LoadImageFromMemory( + ".png", + raw_data(player_sprite), + auto_cast len(player_sprite), +) +tomato_spritesheet_image := rl.LoadImageFromMemory( + ".png", + raw_data(tomato_sprite), + auto_cast len(tomato_sprite), +) +sell_button_image := rl.LoadImageFromMemory( + ".png", + raw_data(sell_button_sprite), + auto_cast len(sell_button_sprite), +) +background_image := rl.LoadImageFromMemory( + ".png", + raw_data(background_sprite), + auto_cast len(background_sprite), +) +sell_button_texture: rl.Texture2D +background_texture: rl.Texture2D + +tile_dirt_dry_image := rl.LoadImageFromMemory( + ".png", + raw_data(tile_dirt_dry_sprite), + auto_cast len(tile_dirt_dry_sprite), +) +tile_dirt_wet_image := rl.LoadImageFromMemory( + ".png", + raw_data(tile_dirt_wet_sprite), + auto_cast len(tile_dirt_wet_sprite), +) +tile_dirt_dry_texture: rl.Texture2D +tile_dirt_wet_texture: rl.Texture2D + +player_animation: Animation +tomato_animation: Animation + +player_handle: EntityHandle + +tiles: [dynamic]Tile +animations: [dynamic]^Animation + +entities: [256]Entity + +main :: proc() { + rl.SetConfigFlags({.VSYNC_HINT, .WINDOW_RESIZABLE, .WINDOW_HIGHDPI, .MSAA_4X_HINT}) + rl.InitWindow(1280, 720, "Raylib Minimal") + rl.SetTargetFPS(rl.GetMonitorRefreshRate(rl.GetCurrentMonitor())) + + game_render_buffer = rl.LoadRenderTexture( + auto_cast GAME_SCREEN_WIDTH, + auto_cast GAME_SCREEN_HEIGHT, + ) + rl.SetTextureFilter(game_render_buffer.texture, rl.TextureFilter.POINT) // Texture scale filter to use + + player_animation = Animation { + texture = rl.LoadTextureFromImage(player_spritesheet_image), + num_frames = 2, + frame_length = 0.5, + loop = true, + } + tomato_animation = Animation { + texture = rl.LoadTextureFromImage(tomato_spritesheet_image), + num_frames = 4, + frame_length = 2, + loop = false, + offset = {TILE_SIZE / 2, TILE_SIZE}, + } + sell_button_texture = rl.LoadTextureFromImage(sell_button_image) + background_texture = rl.LoadTextureFromImage(background_image) + + tile_dirt_dry_texture = rl.LoadTextureFromImage(tile_dirt_dry_image) + tile_dirt_wet_texture = rl.LoadTextureFromImage(tile_dirt_wet_image) + + e := create_entity( + Entity { + pos = {GAME_SCREEN_WIDTH / 2, GAME_SCREEN_HEIGHT / 2}, + kind = .Player, + data = PlayerData{animation = player_animation}, + }, + ) + player_handle = entity_to_handle(e^) + + for !rl.WindowShouldClose() { + free_all(context.temp_allocator) + update() + draw() + } + + rl.UnloadRenderTexture(game_render_buffer) // Unload render texture + rl.CloseWindow() +} + +update :: proc() { + // :scaling update + screen_scale = min( + f32(rl.GetScreenWidth()) / GAME_SCREEN_WIDTH, + f32(rl.GetScreenHeight()) / GAME_SCREEN_HEIGHT, + ) + mouse := rl.GetMousePosition() + virtual_mouse := rl.Vector2 { + (mouse.x - (f32(rl.GetScreenWidth()) - (GAME_SCREEN_WIDTH * screen_scale)) * 0.5) / + screen_scale, + (mouse.y - (f32(rl.GetScreenHeight()) - (GAME_SCREEN_HEIGHT * screen_scale)) * 0.5) / + screen_scale, + } + virtual_mouse = rl.Vector2Clamp( + virtual_mouse, + {}, + rl.Vector2{GAME_SCREEN_WIDTH, GAME_SCREEN_HEIGHT}, + ) + world_mouse = rl.GetScreenToWorld2D(virtual_mouse, game_camera) + screen_game_area = rl.Rectangle { + (f32(rl.GetScreenWidth()) - (GAME_SCREEN_WIDTH * screen_scale)) * 0.5, + (f32(rl.GetScreenHeight()) - (GAME_SCREEN_HEIGHT * screen_scale)) * 0.5, + GAME_SCREEN_WIDTH * screen_scale, + GAME_SCREEN_HEIGHT * screen_scale, + } + + // :animation update + for &e in entities { + if .Allocated not_in e.flags {continue} + + #partial switch &data in e.data { + case TileData: + update_animation(&data.animation) + case PlayerData: + update_animation(&data.animation) + } + } + + // :sell + hover_sell_button := rl.CheckCollisionPointRec( + world_mouse, + {SELL_BUTTON_POS.x, SELL_BUTTON_POS.y, TILE_SIZE * 2, TILE_SIZE * 2}, + ) + if rl.IsMouseButtonPressed(.LEFT) && hover_sell_button { + // todo: sell + fmt.println("Sell button pressed!") + } + + // :player update + player := handle_to_entity(player_handle) + if player != nil { + pd, ok := player.data.(PlayerData);if ok { + vel: rl.Vector2 + if rl.IsKeyDown(.D) do vel.x += 1 + if rl.IsKeyDown(.A) do vel.x -= 1 + if rl.IsKeyDown(.W) do vel.y -= 1 + if rl.IsKeyDown(.S) do vel.y += 1 + if vel.x == -1 do pd.animation.flip = true + if vel.x == 1 do pd.animation.flip = false + player.pos += vel * 100 * rl.GetFrameTime() + } + + } + if rl.IsMouseButtonDown(.LEFT) { + spawn_tile_under_mouse() + } +} + +draw :: proc() { + rl.BeginTextureMode(game_render_buffer) + rl.BeginMode2D(game_camera) + rl.BeginDrawing() + + rl.DrawTextureV(background_texture, {}, rl.WHITE) + // :animation draw + //for t in tiles { + // rl.DrawRectangle(auto_cast t.pos.x, auto_cast t.pos.y, TILE_SIZE, TILE_SIZE, rl.BLACK) + // draw_animation(t.animation, t.pos) + //} + for e in entities { + if .Allocated not_in e.flags {continue} + + #partial switch &data in e.data { + case TileData: + draw_tile(e) + case PlayerData: + draw_player(e) + } + } + + rl.DrawTextureV(sell_button_texture, SELL_BUTTON_POS, rl.WHITE) + rl.EndMode2D() + rl.EndTextureMode() + + rl.ClearBackground(rl.BLACK) + rl.DrawTexturePro( + game_render_buffer.texture, + game_render_buffer_area, + screen_game_area, + {}, + 0, + rl.WHITE, + ) + rl.EndDrawing() +} + +Animation :: struct { + texture: rl.Texture2D, + num_frames: int, + frame_timer: f32, + current_frame: int, + frame_length: f32, + flip: bool, + loop: bool, + done: bool, + offset: rl.Vector2, +} + +update_animation :: proc(a: ^Animation) { + a.frame_timer += rl.GetFrameTime() + + if a.frame_timer > a.frame_length && !a.done { + a.current_frame += 1 + a.frame_timer = 0 + + if a.current_frame == a.num_frames { + if a.loop { + a.current_frame = 0 + } else { + a.current_frame += -1 + a.done = true + } + } + } +} + +draw_animation :: proc(a: Animation, pos: rl.Vector2) { + width := f32(a.texture.width) + height := f32(a.texture.height) + + source := rl.Rectangle { + x = f32(a.current_frame) * width / f32(a.num_frames), + y = 0, + width = width / f32(a.num_frames), + height = height, + } + + if a.flip { + source.width = -source.width + } + + dest := rl.Rectangle { + x = pos.x + a.offset.x, + y = pos.y + a.offset.y, + width = width / f32(a.num_frames), + height = height, + } + + rl.DrawTexturePro(a.texture, source, dest, {dest.width / 2, dest.height}, 0, rl.WHITE) +} + +Tile :: struct { + id: int, + pos: rl.Vector2, + animation: Animation, +} + +spawn_tile_under_mouse :: proc() { + tile_position := get_tile_position(world_mouse) + old_tile: bool + for e in entities { + if .Allocated not_in e.flags {continue} + if e.kind == .Tile && e.pos == tile_position { + old_tile = true + } + } + if !old_tile { + create_entity( + Entity { + pos = tile_position, + kind = .Tile, + data = TileData{has_plant = true, plant_id = 0, animation = tomato_animation}, + }, + ) + } +} + +get_tile_position :: proc(wpos: rl.Vector2) -> rl.Vector2 { + return {math.floor(wpos.x / TILE_SIZE) * TILE_SIZE, math.floor(wpos.y / TILE_SIZE) * TILE_SIZE} +} + +EntityKind :: enum { + Player, + Tile, + Item, +} + +PlayerData :: struct { + animation: Animation, +} + +ItemData :: struct { + id: int, + count: int, + animation: Animation, +} + +TileData :: struct { + has_plant: bool, + is_watered: bool, + plant_id: int, + animation: Animation, +} + +EntityData :: union { + PlayerData, + TileData, + ItemData, +} + +Entity :: struct { + pos: rl.Vector2, + kind: EntityKind, + handle: EntityHandle, + flags: bit_set[EntityFlags], + data: EntityData, +} +latest_entity_handle: int + +EntityHandle :: int +EntityFlags :: enum { + nil, + Allocated, +} + +create_entity :: proc(entity: Entity) -> ^Entity { + for &e in entities { + if .Allocated not_in e.flags { + e = entity + e.flags = {.Allocated} + latest_entity_handle += 1 + return &e + } + } + assert(false, "Failed to allocate entity, not enough space!!!!") + return nil +} + +destroy_entity :: proc(e: ^Entity) { + e^ = Entity{} +} + +handle_to_entity :: proc(handle: EntityHandle) -> ^Entity { + for &e in entities { + if handle == e.handle && .Allocated in e.flags { + return &e + } + } + return nil +} + +entity_to_handle :: proc(e: Entity) -> EntityHandle { + return e.handle +} + + +draw_player :: proc(e: Entity) { + data, ok := e.data.(PlayerData) + draw_animation(data.animation, e.pos) +} +draw_tile :: proc(e: Entity) { + data, ok := e.data.(TileData) + //rl.DrawRectangle(auto_cast e.pos.x, auto_cast e.pos.y, TILE_SIZE, TILE_SIZE, rl.BLACK) + rl.DrawTextureV(tile_dirt_dry_texture, e.pos, rl.WHITE) + draw_animation(data.animation, e.pos) +} +draw_item :: proc(e: Entity) { + data, ok := e.data.(ItemData) + draw_animation(data.animation, e.pos) +}