package game import "core:fmt" import rl "vendor:raylib" // ------------------------------------------------------------------------------ // Scene Definitions // ------------------------------------------------------------------------------ Scene_Type :: enum { Main_Menu, Gameplay, Pause_Menu, Settings_Menu, Game_Over, } Scene :: struct { type: Scene_Type, level: Maybe(Level), canvas: Maybe(Canvas), winner_is_player: bool, } push_scene :: proc(game: ^Game, scene: Scene) { append(&game.scene_stack, scene) } pop_scene :: proc(game: ^Game) { if len(game.scene_stack) > 1 { top_index := len(game.scene_stack) - 1 destroy_scene(&game.scene_stack[top_index]) pop(&game.scene_stack) } } destroy_scene :: proc(scene: ^Scene) { if level, ok := &scene.level.?; ok { cleanup_level(level) } } set_root_scene :: proc(game: ^Game, root_scene: Scene) { for &scene in game.scene_stack { destroy_scene(&scene) } clear(&game.scene_stack) append(&game.scene_stack, root_scene) } get_active_scene :: proc(game: ^Game) -> ^Scene { if len(game.scene_stack) == 0 do return nil return &game.scene_stack[len(game.scene_stack) - 1] } // ------------------------------------------------------------------------------ // Unified Scene Dispatchers // ------------------------------------------------------------------------------ scene_update :: proc(scene: ^Scene, game: ^Game, input: Input_Event, dt: f32) { #partial switch scene.type { case .Main_Menu: update_main_menu(scene, input) case .Pause_Menu: update_pause_menu(scene, input) case .Settings_Menu: update_settings_menu(scene, input) case .Game_Over: update_game_over(scene, input) case .Gameplay: level, level_ok := &scene.level.? assert(level_ok, "scene_update: .Gameplay scene is missing a Level component") update_level(level, input, dt) if input.pause_pressed { emit_event(.Game_Pause) } } // Process events triggered during scene updates process_events(game) } scene_render :: proc(scene: ^Scene) { if level, ok := scene.level.?; ok { render_level(level) render_hud(level) } #partial switch scene.type { case .Pause_Menu: render_pause_menu(scene^) case .Settings_Menu: render_settings_menu(scene^) case .Game_Over: render_game_over(scene^) case .Main_Menu: render_main_menu(scene^) } } // ------------------------------------------------------------------------------ // UI Layout & Styling Constants // ------------------------------------------------------------------------------ UI_TITLE_FONT_SIZE :: 20 UI_LABEL_FONT_SIZE :: 10 UI_TITLE_PADDING :: 10.0 UI_ITEM_HEIGHT :: 20.0 UI_ITEM_SPACING :: 8.0 UI_ITEM_BORDER_THICK :: 2.0 UI_TOP_MARGIN_DEFAULT :: 60.0 UI_TOP_MARGIN_LIST :: 75.0 // ------------------------------------------------------------------------------ // UI Definitions // ------------------------------------------------------------------------------ Canvas :: struct { bounds: rl.Rectangle, padding: f32, item_height: f32, item_spaceing: f32, menu: Maybe(UI_Menu), } UI_Button :: struct { label: cstring, } UI_Slider :: struct { label: cstring, value: ^f32, step: f32, } UI_Selector :: struct { label: cstring, value_label: cstring, } UI_Item :: union { UI_Button, UI_Slider, UI_Selector, } UI_Menu :: struct { items: [8]UI_Item, count: int, selected_index: int, } get_canvas_item_rec :: proc(canvas: Canvas, index: int, top_margin: f32 = UI_TOP_MARGIN_DEFAULT) -> rl.Rectangle { y_pos := (canvas.bounds.y + top_margin) + (f32(index) * (canvas.item_height + canvas.item_spaceing)) width_value := (canvas.bounds.width * 0.5) return rl.Rectangle{ x = (canvas.bounds.x) + width_value * 0.5, y = y_pos, width = width_value, height = canvas.item_height, } } list_menu_items :: proc(canvas: Canvas, menu: UI_Menu) { for i in 0 ..< menu.count { item := menu.items[i] item_rec := get_canvas_item_rec(canvas, i, top_margin = UI_TOP_MARGIN_LIST) is_selected := (i == menu.selected_index) border_color := is_selected ? rl.ORANGE : rl.DARKGRAY text_color := is_selected ? rl.ORANGE : rl.WHITE rl.DrawRectangleLinesEx(item_rec, UI_ITEM_BORDER_THICK, border_color) #partial switch v in item { case UI_Button: text_width := rl.MeasureText(v.label, UI_LABEL_FONT_SIZE) text_x := i32(item_rec.x + item_rec.width * 0.5) - (text_width / 2) text_y := i32(item_rec.y + (item_rec.height * 0.5)) - (UI_LABEL_FONT_SIZE / 2) rl.DrawText(v.label, text_x, text_y, UI_LABEL_FONT_SIZE, text_color) case UI_Slider: slider_rec := item_rec text_width := rl.MeasureText(v.label, UI_LABEL_FONT_SIZE) slider_rec.x = slider_rec.x + f32(text_width * 2) slider_rec.width = (slider_rec.width - f32(text_width * 2)) rl.GuiSlider( bounds = slider_rec, textLeft = v.label, textRight = nil, value = v.value, minValue = 0.0, maxValue = 1.0, ) case UI_Selector: lbl_y := i32(item_rec.y + (item_rec.height * 0.5)) - (UI_LABEL_FONT_SIZE / 2) rl.DrawText(v.label, i32(item_rec.x + 8), lbl_y, UI_LABEL_FONT_SIZE, text_color) display_str := fmt.ctprintf("< %s >", v.value_label) str_w := rl.MeasureText(display_str, UI_LABEL_FONT_SIZE) val_x := i32(item_rec.x + item_rec.width) - str_w - 8 rl.DrawText(display_str, val_x, lbl_y, UI_LABEL_FONT_SIZE, text_color) } } } // ------------------------------------------------------------------------------ // Main Menu // ------------------------------------------------------------------------------ make_main_menu :: proc() -> Scene { ui_items:[8]UI_Item = { 0 = UI_Button{"Start Game"}, 1 = UI_Button{"Options"}, 2 = UI_Button{"Quit"}, } return Scene{ type = .Main_Menu, canvas = Canvas{ bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, padding = UI_TITLE_PADDING, item_height = UI_ITEM_HEIGHT, item_spaceing = UI_ITEM_SPACING, menu = UI_Menu{ items = ui_items, count = 3, selected_index = 0, }, }, } } render_main_menu :: proc(scene: Scene) { canvas, canvas_ok := scene.canvas.? assert(canvas_ok, "render_main_menu: Scene is missing a Canvas component") menu, menu_ok := canvas.menu.? assert(menu_ok, "render_main_menu: Canvas is missing a UI_Menu component") t_text: cstring = "PONG" t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE) t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) rl.DrawText(t_text, t_x, i32(canvas.padding), UI_TITLE_FONT_SIZE, rl.WHITE) list_menu_items(canvas, menu) } update_main_menu :: proc(scene: ^Scene, input: Input_Event) { canvas, canvas_ok := &scene.canvas.? assert(canvas_ok, "update_main_menu: Scene is missing a Canvas component") menu, menu_ok := &canvas.menu.? assert(menu_ok, "update_main_menu: Canvas is missing a UI_Menu component") if input.up_pressed { menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count emit_event(.UI_Select) } if input.down_pressed { menu.selected_index = (menu.selected_index + 1) % menu.count emit_event(.UI_Select) } if input.confirm_pressed { switch menu.selected_index { case 0: emit_event(.Game_Start) case 1: emit_event(.Game_Settings) case 2: emit_event(.Game_Quit) } } } // ------------------------------------------------------------------------------ // Pause Menu // ------------------------------------------------------------------------------ make_pause_overlay :: proc() -> Scene { ui_items:[8]UI_Item = { 0 = UI_Button{"Resume Game"}, 1 = UI_Button{"Options"}, 2 = UI_Button{"Main Menu"}, } return Scene{ type = .Pause_Menu, canvas = Canvas{ bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, padding = UI_TITLE_PADDING, item_height = UI_ITEM_HEIGHT, item_spaceing = UI_ITEM_SPACING, menu = UI_Menu{ items = ui_items, count = 3, selected_index = 0, }, }, } } render_pause_menu :: proc(scene: Scene) { canvas, canvas_ok := scene.canvas.? assert(canvas_ok, "render_pause_menu: Scene is missing a Canvas component") menu, menu_ok := canvas.menu.? assert(menu_ok, "render_pause_menu: Canvas is missing a UI_Menu component") // Semi-transparent overlay so gameplay stays visible behind menu rl.DrawRectangleRec(canvas.bounds, rl.Color{0, 0, 0, 200}) t_text: cstring = "PAUSE MENU" t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE) t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) rl.DrawText(t_text, t_x, i32(canvas.padding), UI_TITLE_FONT_SIZE, rl.WHITE) list_menu_items(canvas, menu) } update_pause_menu :: proc(scene: ^Scene, input: Input_Event) { canvas, canvas_ok := &scene.canvas.? assert(canvas_ok, "update_pause_menu: Scene is missing a Canvas component") menu, menu_ok := &canvas.menu.? assert(menu_ok, "update_pause_menu: Canvas is missing a UI_Menu component") if input.up_pressed { menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count emit_event(.UI_Select) } if input.down_pressed { menu.selected_index = (menu.selected_index + 1) % menu.count emit_event(.UI_Select) } if input.confirm_pressed { switch menu.selected_index { case 0: emit_event(.Game_Back) case 1: emit_event(.Game_Settings) case 2: emit_event(.Game_Main_Menu) } } if input.pause_pressed { emit_event(.Game_Back) } } // ------------------------------------------------------------------------------ // Settings Menu // ------------------------------------------------------------------------------ make_settings_menu :: proc(audio: ^Audio_Manager, current_res_label: cstring) -> Scene { ui_items:[8]UI_Item = { 0 = UI_Button{"Back"}, 1 = UI_Selector{ label = "Resolution", value_label = current_res_label, }, 2 = UI_Slider{ label = "Music ", value = &audio.user_music_vol, step = 0.1, }, 3 = UI_Slider{ label = "SFX ", value = &audio.sfx_vol, step = 0.1, }, } return Scene{ type = .Settings_Menu, canvas = Canvas{ bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, padding = UI_TITLE_PADDING, item_height = UI_ITEM_HEIGHT, item_spaceing = UI_ITEM_SPACING, menu = UI_Menu{ items = ui_items, count = 4, selected_index = 0, }, }, } } render_settings_menu :: proc(scene: Scene) { canvas, canvas_ok := scene.canvas.? assert(canvas_ok, "render_settings_menu: Scene is missing a Canvas component") menu, menu_ok := canvas.menu.? assert(menu_ok, "render_settings_menu: Canvas is missing a UI_Menu component") t_text: cstring = "SETTINGS" t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE) t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) rl.DrawText(t_text, t_x, i32(canvas.padding), UI_TITLE_FONT_SIZE, rl.WHITE) list_menu_items(canvas, menu) } update_settings_menu :: proc(scene: ^Scene, input: Input_Event) { canvas, canvas_ok := &scene.canvas.? assert(canvas_ok, "update_settings_menu: Scene is missing a Canvas component") menu, menu_ok := &canvas.menu.? assert(menu_ok, "update_settings_menu: Canvas is missing a UI_Menu component") if input.up_pressed { menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count emit_event(.UI_Select) } if input.down_pressed { menu.selected_index = (menu.selected_index + 1) % menu.count emit_event(.UI_Select) } current_item := &menu.items[menu.selected_index] #partial switch v in current_item { case UI_Button: if input.confirm_pressed { switch menu.selected_index { case 0: emit_event(.Game_Back) } } case UI_Selector: if input.left_pressed { emit_event(Change_Resolution_Data{-1}) } if input.right_pressed { emit_event(Change_Resolution_Data{1}) } case UI_Slider: if input.left_pressed { v.value^ = clamp(v.value^ - v.step, 0.0, 1.0) emit_event(.UI_Select) } if input.right_pressed { v.value^ = clamp(v.value^ + v.step, 0.0, 1.0) emit_event(.UI_Select) } } } // ------------------------------------------------------------------------------ // Game Over Menu // ------------------------------------------------------------------------------ make_game_over_overlay :: proc(winner_is_player: bool) -> Scene { ui_items:[8]UI_Item = { 0 = UI_Button{"Restart Game"}, 1 = UI_Button{"Main Menu"}, } return Scene{ type = .Game_Over, winner_is_player = winner_is_player, canvas = Canvas{ bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, padding = UI_TITLE_PADDING, item_height = UI_ITEM_HEIGHT, item_spaceing = UI_ITEM_SPACING, menu = UI_Menu{ items = ui_items, count = 2, selected_index = 0, }, }, } } render_game_over :: proc(scene: Scene) { canvas, canvas_ok := scene.canvas.? assert(canvas_ok, "render_game_over: Scene is missing a Canvas component") menu, menu_ok := canvas.menu.? assert(menu_ok, "render_game_over: Canvas is missing a UI_Menu component") rl.DrawRectangleRec(canvas.bounds, rl.Color{0, 0, 0, 220}) t_text: cstring = scene.winner_is_player ? "YOU WIN!" : "GAME OVER" t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE) t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) text_color := scene.winner_is_player ? rl.GREEN : rl.RED rl.DrawText(t_text, t_x, i32(canvas.padding), UI_TITLE_FONT_SIZE, text_color) list_menu_items(canvas, menu) } update_game_over :: proc(scene: ^Scene, input: Input_Event) { canvas, canvas_ok := &scene.canvas.? assert(canvas_ok, "update_game_over: Scene is missing a Canvas component") menu, menu_ok := &canvas.menu.? assert(menu_ok, "update_game_over: Canvas is missing a UI_Menu component") if input.up_pressed { menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count emit_event(.UI_Select) } if input.down_pressed { menu.selected_index = (menu.selected_index + 1) % menu.count emit_event(.UI_Select) } if input.confirm_pressed { switch menu.selected_index { case 0: emit_event(.Game_Start) case 1: emit_event(.Game_Main_Menu) } } } // ------------------------------------------------------------------------------ // Gameplay Scene // ------------------------------------------------------------------------------ make_gameplay_scene :: proc() -> Scene { return Scene{ type = .Gameplay, level = init_level(), } }