package main import "core:fmt" import "core:math" import "core:math/rand" import "core:time" import "zephr" Icon :: enum { HELP_ICON, SETTINGS_ICON, CLOSE_ICON, } MoveDir :: enum { UP, DOWN, LEFT, RIGHT, } ColorPalette :: struct { bg_color: zephr.Color, board_bg_color: zephr.Color, tile_colors: [11]zephr.Color, } Tile :: struct { value: u16, new_value: u16, merged: bool, anim_x_offset_relative: f64, anim_y_offset_relative: f64, tiles_to_move: u8, } Game :: struct { board: [4][4]Tile, score: u32, last_move_dir: MoveDir, animating: bool, spawning_new_tile: bool, has_lost: bool, game_over_bg_opacity: f32, game_over_opacity: f32, spawning_tile_scale: f32, spawning_tile_coords: zephr.Vec2, spawning_tile_value: u16, quit_dialog: bool, settings_dialog: bool, help_dialog: bool, icon_textures: [Icon]zephr.TextureId, palette: ColorPalette, } @private TILE_ANIM_SPEED :: 2.5 @private TILE_SPAWN_SPEED :: 6.0 @private GAMEOVER_ANIM_DURATION :: time.Millisecond * 300 @private neighbors := [4]zephr.Vec2 { {-1, 0}, // left {1, 0}, // right {0, -1}, // up {0, 1}, // down } // nocheckin timeElapsed: time.Duration = 0 @private game : Game get_tile_color :: proc(value: u16) -> zephr.Color { switch (value) { case 2: return game.palette.tile_colors[0]; case 4: return game.palette.tile_colors[1]; case 8: return game.palette.tile_colors[2]; case 16: return game.palette.tile_colors[3]; case 32: return game.palette.tile_colors[4]; case 64: return game.palette.tile_colors[5]; case 128: return game.palette.tile_colors[6]; case 256: return game.palette.tile_colors[7]; case 512: return game.palette.tile_colors[8]; case 1024: return game.palette.tile_colors[9]; case 2048: return game.palette.tile_colors[10]; case: return game.palette.tile_colors[10]; } } get_tile_font_size :: proc(value: u16) -> u32 { switch (value) { case 2: fallthrough case 4: fallthrough case 8: return 60; case 16: fallthrough case 32: fallthrough case 64: return 55; case 128: fallthrough case 256: fallthrough case 512: return 50; case 1024: fallthrough case 2048: return 45; case: return 40; } } ///////////////////////////// // // // Game Logic // // ///////////////////////////// move_right :: proc() { game.last_move_dir = .RIGHT; for i in 0..<4 { for j := 2; j >= 0; j -= 1 { if (game.board[i][j].value == 0) { continue; } tiles_to_move := 0; x := j; src := &game.board[i][j]; loop: for { if (x >= 3) do break loop x += 1 // next tile is empty so we can move there if (game.board[i][x].new_value == 0) { tiles_to_move += 1; continue; } // next tile has the same value so we can merge and move if (game.board[i][x].new_value == src.new_value && !game.board[i][x].merged) { tiles_to_move += 1; game.board[i][x].merged = true; break; } else { // next tile has a different value so we can't move there x -= 1; break; } } src.tiles_to_move = auto_cast tiles_to_move; if (tiles_to_move > 0) { if (game.board[i][x].merged) { game.board[i][x].new_value *= 2; game.score += auto_cast game.board[i][x].new_value; } else { game.board[i][x].new_value = src.new_value; } src.new_value = 0; game.animating = true; game.spawning_tile_coords = get_random_available_tile_coords(); game.spawning_tile_value = get_spawned_tile_value(); } } } } // TODO: ideas for refactoring the animation logic and separating game state from rendering // the board is just a state that we keep in memory but never use it for rendering // and a tile is an enitity that has a position (in the board or on the screen??) that we can animate using lerp // we draw the tile entities and not the board state. the tile entities update their positions according to the board state at the end of the frame?? move_left :: proc() { game.last_move_dir = .LEFT; for i in 0..<4 { for j in 1..<4 { if (game.board[i][j].value == 0) { continue; } tiles_to_move := 0; x := j; src := &game.board[i][j]; loop: for { if (x <= 0) do break loop x -= 1; // next tile is empty so we can move there if (game.board[i][x].new_value == 0) { tiles_to_move += 1; continue; } // next tile has the same value so we can merge and move if (game.board[i][x].new_value == src.new_value && !game.board[i][x].merged) { tiles_to_move += 1; game.board[i][x].merged = true; break; } else { // next tile has a different value so we can't move there x += 1 break; } } src.tiles_to_move = auto_cast tiles_to_move; if (tiles_to_move > 0) { if (game.board[i][x].merged) { game.board[i][x].new_value *= 2; game.score += auto_cast game.board[i][x].new_value; } else { game.board[i][x].new_value = src.new_value; } src.new_value = 0; game.animating = true; game.spawning_tile_coords = get_random_available_tile_coords(); game.spawning_tile_value = get_spawned_tile_value(); } } } } print_board :: proc() { for i in 0..<4 { for j in 0..<4 { fmt.print(game.board[i][j].value, " "); } fmt.println(); } fmt.println(); } print_new_board :: proc() { for i in 0..<4 { for j in 0..<4 { fmt.print(game.board[i][j].new_value, " "); } fmt.println(); } fmt.println(); } move_down :: proc() { game.last_move_dir = .DOWN; for i := 2; i >= 0; i -= 1 { for j in 0..<4 { if (game.board[i][j].value == 0) { continue; } tiles_to_move := 0; y := i; src := &game.board[i][j]; loop: for { if (y >= 3) do break loop y += 1; if (game.board[y][j].new_value == 0) { tiles_to_move += 1; continue; } if (game.board[y][j].new_value == src.new_value && !game.board[y][j].merged) { tiles_to_move += 1; game.board[y][j].merged = true; break; } else { y -= 1; break; } } src.tiles_to_move = auto_cast tiles_to_move; if (tiles_to_move > 0) { if (game.board[y][j].merged) { game.board[y][j].new_value *= 2; game.score += auto_cast game.board[y][j].new_value; } else { game.board[y][j].new_value = src.new_value; } src.new_value = 0; game.animating = true; game.spawning_tile_coords = get_random_available_tile_coords(); game.spawning_tile_value = get_spawned_tile_value(); } } } } move_up :: proc() { game.last_move_dir = .UP; for i in 1..<4 { for j in 0..<4 { if (game.board[i][j].new_value == 0) { continue; } tiles_to_move := 0; y := i; src := &game.board[i][j]; loop: for { if (y <= 0) do break loop y -= 1; if (game.board[y][j].new_value == 0) { tiles_to_move += 1; continue; } if (game.board[y][j].new_value == src.new_value && !game.board[y][j].merged) { tiles_to_move += 1; game.board[y][j].merged = true; break; } else { y += 1; break; } } src.tiles_to_move = auto_cast tiles_to_move; if (tiles_to_move > 0) { if (game.board[y][j].merged) { game.board[y][j].new_value *= 2; game.score += auto_cast game.board[y][j].new_value; } else { game.board[y][j].new_value = src.new_value; } src.new_value = 0; game.animating = true; game.spawning_tile_coords = get_random_available_tile_coords(); game.spawning_tile_value = get_spawned_tile_value(); } } } } reset_palette :: proc() { game.palette = ColorPalette{ bg_color = zephr.Color{248, 250, 230, 255}, board_bg_color = zephr.Color{140, 154, 147, 255}, tile_colors = [11]zephr.Color{ zephr.Color{238, 228, 218, 255}, // 2 zephr.Color{237, 224, 200, 255}, // 4 zephr.Color{242, 177, 121, 255}, // 8 zephr.Color{245, 149, 99, 255}, // 16 zephr.Color{246, 124, 95, 255}, // 32 zephr.Color{246, 94, 59, 255}, // 64 zephr.Color{237, 207, 114, 255}, // 128 zephr.Color{237, 204, 97, 255}, // 256 zephr.Color{237, 200, 80, 255}, // 512 zephr.Color{237, 197, 63, 255}, // 1024 zephr.Color{237, 194, 46, 255}, // 2048 }, } } reset_game :: proc() { timeElapsed = 0 game.score = 0; game.animating = false; game.spawning_new_tile = false; game.quit_dialog = false; game.help_dialog = false; game.settings_dialog = false; game.has_lost = false; game.game_over_bg_opacity = 0; game.game_over_opacity = 0; for y in 0..<4 { for x in 0..<4 { game.board[y][x].value = 0; game.board[y][x].new_value = 0; game.board[y][x].merged = false; game.board[y][x].tiles_to_move = 0; game.board[y][x].anim_x_offset_relative = 0; game.board[y][x].anim_y_offset_relative = 0; } } for i in 0..<2 { spawn_random_tile(); } } game_attempt_quit :: proc() { game.help_dialog = false; game.settings_dialog = false; game.quit_dialog = true; } game_init :: proc() { game.icon_textures[.HELP_ICON] = zephr.load_texture("assets/icons/help.png") game.icon_textures[.SETTINGS_ICON] = zephr.load_texture("assets/icons/settings.png") game.icon_textures[.CLOSE_ICON] = zephr.load_texture("assets/icons/close.png") reset_palette() for i in 0..<2 { spawn_random_tile() } } get_spawned_tile_value :: proc() -> u16 { // 90% chance of spawning a 2, 10% chance of spawning a 4 if (rand.int_max(11) < 9) { return 2; } else { return 4; } } spawn_new_tile_with_value :: proc(x, y: u8, value: u16) { game.board[x][y].value = value; game.board[x][y].new_value = value; } spawn_new_tile :: proc(x, y: u8) { val := get_spawned_tile_value(); game.board[x][y].value = val; game.board[x][y].new_value = val; } get_random_available_tile_coords :: proc() -> zephr.Vec2 { available_tiles_count := 0; available_tiles: [16]int for i in 0..<4 { for j in 0..<4 { if (game.board[i][j].new_value == 0 && !game.board[i][j].merged) { available_tiles[available_tiles_count] = i * 4 + j; available_tiles_count += 1; } } } rand_idx := rand.int_max(available_tiles_count) x := available_tiles[rand_idx] / 4; y := available_tiles[rand_idx] % 4; return zephr.Vec2{cast(f32)x, cast(f32)y}; } spawn_random_tile :: proc() { coords := get_random_available_tile_coords(); spawn_new_tile(cast(u8)coords.x, cast(u8)coords.y); } gameover :: proc() -> bool { if (game.has_lost) do return false for y in 0..<4 { for x in 0..<4 { if (game.board[y][x].value == 0) { return false; } for i in 0..<4 { new_y := cast(int)clamp(cast(f32)y + neighbors[i].y, 0, 3); new_x := cast(int)clamp(cast(f32)x + neighbors[i].x, 0, 3); if ((new_y != y || new_x != x) && game.board[y][x].value == game.board[new_y][new_x].value) { return false; } } } } return true; } handle_keyboard_input :: proc(e: zephr.Event) { can_move := !game.quit_dialog && !game.help_dialog && !game.settings_dialog && !game.animating; if (.LEFT_CTRL in e.key.mods && e.key.scancode == .Q) { game_attempt_quit(); } else if (e.key.scancode == .ESCAPE) { game.quit_dialog = false; game.help_dialog = false; game.settings_dialog = false; } else if (e.key.scancode == .F11) { zephr.toggle_fullscreen(); } else if (e.key.scancode == .UP) { if (can_move) do move_up() } else if (e.key.scancode == .DOWN) { if (can_move) do move_down() } else if (e.key.scancode == .LEFT) { if (can_move) do move_left() } else if (e.key.scancode == .RIGHT) { if (can_move) do move_right() } } game_loop :: proc() { game_init() last_frame: time.Duration = 0 for (!zephr.should_quit()) { e: zephr.Event for (zephr.iter_events(&e)) { #partial switch (e.type) { case .WINDOW_CLOSED: if (game.quit_dialog) { zephr.quit(); } else { game_attempt_quit(); } case .KEY_PRESSED: handle_keyboard_input(e); } } now := zephr.get_time() delta_t := now - last_frame last_frame = now; update_positions(delta_t) draw_bg(); draw_board(); draw_ui(); zephr.swap_buffers() } } update_positions :: proc(delta_t: time.Duration) { // TODO: lerp the positions and use delta_t for it delta_t_f := time.duration_seconds(delta_t) // TODO: experiment; I think I can lerp the tile positions instead of doing this if (game.animating) { switch (game.last_move_dir) { case .LEFT: for i in 0..<4 { for j in 1..<4 { tiles_to_move := game.board[i][j].tiles_to_move; speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; src := &game.board[i][j]; dest := &game.board[i][j - cast(int)tiles_to_move]; if (src.tiles_to_move != 0) { src.anim_x_offset_relative -= speed * delta_t_f; if (src.anim_x_offset_relative <= -0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { if (dest.new_value == src.value * 2) { dest.merged = false; } src.value = src.new_value; dest.value = dest.new_value; src.tiles_to_move = 0; src.anim_x_offset_relative = 0; game.spawning_new_tile = true; } } } } case .RIGHT: for i in 0..<4 { for j := 2; j >= 0; j -= 1 { tiles_to_move := game.board[i][j].tiles_to_move; speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; src := &game.board[i][j]; dest := &game.board[i][j + cast(int)tiles_to_move]; if (src.tiles_to_move != 0) { src.anim_x_offset_relative += speed * delta_t_f; if (src.anim_x_offset_relative >= 0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { if (dest.new_value == src.value * 2) { dest.merged = false; } src.value = src.new_value; dest.value = dest.new_value; src.tiles_to_move = 0; src.anim_x_offset_relative = 0; game.spawning_new_tile = true; } } } } case .UP: for i in 1..<4 { for j in 0..<4 { tiles_to_move := game.board[i][j].tiles_to_move; speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; src := &game.board[i][j]; dest := &game.board[i - cast(int)tiles_to_move][j]; if (src.tiles_to_move != 0) { src.anim_y_offset_relative -= speed * delta_t_f; if (src.anim_y_offset_relative <= -0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { if (dest.new_value == src.value * 2) { dest.merged = false; } src.value = src.new_value; dest.value = dest.new_value; src.tiles_to_move = 0; src.anim_y_offset_relative = 0; game.spawning_new_tile = true; } } } } case .DOWN: for i := 2; i >= 0; i -= 1 { for j in 0..<4 { tiles_to_move := game.board[i][j].tiles_to_move; speed := TILE_ANIM_SPEED * cast(f64)tiles_to_move; src := &game.board[i][j]; dest := &game.board[i + cast(int)tiles_to_move][j]; if (src.tiles_to_move != 0) { src.anim_y_offset_relative += speed * delta_t_f; if (src.anim_y_offset_relative >= 0.240 * (cast(f64)tiles_to_move + (cast(f64)tiles_to_move * 0.02))) { if (dest.new_value == src.value * 2) { dest.merged = false; } src.value = src.new_value; dest.value = dest.new_value; src.tiles_to_move = 0; src.anim_y_offset_relative = 0; game.spawning_new_tile = true; } } } } } } if (gameover()) { game.has_lost = true; game.animating = true; } if (game.spawning_new_tile) { game.spawning_tile_scale += (TILE_SPAWN_SPEED * cast(f32)delta_t_f); if (game.spawning_tile_scale >= 1.0) { spawn_new_tile_with_value(cast(u8)game.spawning_tile_coords.x, cast(u8)game.spawning_tile_coords.y, game.spawning_tile_value); game.spawning_tile_coords = zephr.Vec2{0, 0}; game.spawning_tile_value = 0; game.spawning_new_tile = false; game.animating = false; game.spawning_tile_scale = 0.0 } } if (game.has_lost) { if timeElapsed < GAMEOVER_ANIM_DURATION { game.game_over_opacity = cast(f32)math.lerp(0.0, 255.0, cast(f64)timeElapsed / cast(f64)GAMEOVER_ANIM_DURATION) game.game_over_bg_opacity = cast(f32)math.lerp(0.0, 200.0, cast(f64)timeElapsed / cast(f64)GAMEOVER_ANIM_DURATION) timeElapsed += delta_t } else { game.game_over_bg_opacity = 255 game.game_over_bg_opacity = 200 } } } /////////////////////////////////// // // // Drawing // // /////////////////////////////////// draw_bg :: proc() { window_size := zephr.get_window_size(); board_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_width_constraint(&board_con, window_size.x, .FIXED); zephr.set_height_constraint(&board_con, window_size.y, .FIXED); // bg style := zephr.UiStyle{ bg_color = game.palette.bg_color, align = .TOP_LEFT, }; zephr.draw_quad(&board_con, style); text_batch: zephr.GlyphInstanceList reserve(&text_batch, 32) // 2048 text text_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_y_constraint(&text_con, 0.05, .RELATIVE); zephr.set_width_constraint(&text_con, 1.5, .RELATIVE_PIXELS); zephr.add_text_instance(&text_batch, "2048", 100, text_con, zephr.Color{34, 234, 82, 155}, .TOP_CENTER); // score buf: [24]byte score := fmt.bprintf(buf[:], "Score %d", game.score); zephr.set_parent_constraint(&text_con, nil); zephr.set_x_constraint(&text_con, 0.05, .RELATIVE); zephr.set_y_constraint(&text_con, 0.8, .RELATIVE); zephr.add_text_instance(&text_batch, score, 40, text_con, zephr.COLOR_BLACK, .TOP_LEFT); zephr.draw_text_batch(&text_batch); } draw_board :: proc() { // board board_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_y_constraint(&board_con, 0.05, .RELATIVE); zephr.set_height_constraint(&board_con, 0.6, .RELATIVE); zephr.set_width_constraint(&board_con, 1, .ASPECT_RATIO); style := zephr.UiStyle{ bg_color = game.palette.board_bg_color, border_radius = board_con.width * 0.02, align = .CENTER, }; zephr.draw_quad(&board_con, style); // empty tiles tile_padding := board_con.width * 0.02; tile_height := board_con.height * 0.225; tile_board_radius := tile_height * 0.15 empty_tile_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_parent_constraint(&empty_tile_con, &board_con); zephr.set_height_constraint(&empty_tile_con, tile_height, .FIXED); zephr.set_width_constraint(&empty_tile_con, 1, .ASPECT_RATIO); style = zephr.UiStyle{ bg_color = zephr.mult_color(game.palette.board_bg_color, 0.85), border_radius = tile_board_radius, align = .TOP_LEFT, }; for i in 0..<4 { zephr.set_y_constraint(&empty_tile_con, (tile_height + tile_padding) * cast(f32)i + tile_padding, .FIXED); for j in 0..<4 { zephr.set_x_constraint(&empty_tile_con, (tile_height + tile_padding) * cast(f32)j + tile_padding, .FIXED); zephr.draw_quad(&empty_tile_con, style); } } // filled tiles text_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_width_constraint(&text_con, 1.5, .RELATIVE_PIXELS); tile_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_parent_constraint(&tile_con, &board_con); zephr.set_height_constraint(&tile_con, tile_height, .FIXED); zephr.set_width_constraint(&tile_con, 1, .ASPECT_RATIO); for i in 0..<4 { for j in 0..<4 { tile := &game.board[i][j]; tile_color := get_tile_color(tile.value); style := zephr.UiStyle { bg_color = tile_color, border_radius = tile_board_radius, align = .TOP_LEFT, } buf: [16]byte value := fmt.bprintf(buf[:], "%d", tile.value) tile_y_pos := ((tile_height + tile_padding) * cast(f32)i + tile_padding + cast(f32)game.board[i][j].anim_y_offset_relative * board_con.height); tile_x_pos := ((tile_height + tile_padding) * cast(f32)j + tile_padding + cast(f32)game.board[i][j].anim_x_offset_relative * board_con.height); zephr.set_y_constraint(&tile_con, tile_y_pos, .FIXED); zephr.set_x_constraint(&tile_con, tile_x_pos, .FIXED); if (tile.value > 0) { zephr.draw_quad(&tile_con, style); // value text zephr.set_parent_constraint(&text_con, &tile_con); zephr.set_x_constraint(&text_con, 0, .FIXED); zephr.set_y_constraint(&text_con, 0, .FIXED); zephr.draw_text(value, get_tile_font_size(tile.value), text_con, zephr.mult_color(tile_color, 0.5), .CENTER); } } } if (game.spawning_new_tile) { // spawning tile tile_color := get_tile_color(game.spawning_tile_value); style := zephr.UiStyle { bg_color = tile_color, border_radius = tile_board_radius * game.spawning_tile_scale, align = .TOP_LEFT, }; tile_x_pos := ((tile_height + tile_padding) * game.spawning_tile_coords.y + tile_padding) + tile_height / 2 - game.spawning_tile_scale * tile_height / 2; tile_y_pos := ((tile_height + tile_padding) * game.spawning_tile_coords.x + tile_padding) + tile_height / 2 - game.spawning_tile_scale * tile_height / 2; zephr.set_x_constraint(&tile_con, tile_x_pos, .FIXED); zephr.set_y_constraint(&tile_con, tile_y_pos, .FIXED); zephr.set_height_constraint(&tile_con, tile_height * game.spawning_tile_scale, .FIXED); zephr.set_width_constraint(&tile_con, 1, .ASPECT_RATIO); zephr.draw_quad(&tile_con, style); // value text zephr.set_parent_constraint(&text_con, &tile_con); zephr.set_x_constraint(&text_con, 0, .FIXED); zephr.set_y_constraint(&text_con, 0, .FIXED); buf: [16]byte value := fmt.bprintf(buf[:], "%d", game.spawning_tile_value) zephr.draw_text(value, cast(u32)(cast(f32)get_tile_font_size(game.spawning_tile_value) * game.spawning_tile_scale), text_con, zephr.mult_color(tile_color, 0.5), .CENTER); } } draw_ui :: proc() { bg_btns_state: zephr.ButtonState = (game.quit_dialog || game.help_dialog || game.settings_dialog || game.has_lost) ? .INACTIVE : .ACTIVE; icon_btn_width :: 80; icon_btn_offset :: 8; icon_btns_padding :: 10; btn_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_x_constraint(&btn_con, -icon_btn_offset, .RELATIVE_PIXELS); zephr.set_y_constraint(&btn_con, icon_btn_offset, .RELATIVE_PIXELS); zephr.set_width_constraint(&btn_con, icon_btn_width, .RELATIVE_PIXELS); zephr.set_height_constraint(&btn_con, 1, .ASPECT_RATIO); style := zephr.UiStyle { bg_color = zephr.Color{201, 146, 126, 255}, fg_color = zephr.Color{40, 92, 100, 255}, border_radius = btn_con.height * 0.15, align = .TOP_RIGHT, }; if (zephr.draw_icon_button(&btn_con, game.icon_textures[.HELP_ICON], style, bg_btns_state)) { game.help_dialog = true; } zephr.set_x_constraint(&btn_con, -icon_btn_offset - icon_btn_width - icon_btns_padding, .RELATIVE_PIXELS); if (zephr.draw_icon_button(&btn_con, game.icon_textures[.SETTINGS_ICON], style, bg_btns_state)) { game.settings_dialog = true; } zephr.set_x_constraint(&btn_con, 0, .RELATIVE_PIXELS); zephr.set_y_constraint(&btn_con, -0.05, .RELATIVE); zephr.set_width_constraint(&btn_con, 200, .RELATIVE_PIXELS); zephr.set_height_constraint(&btn_con, 65, .RELATIVE_PIXELS); style = zephr.UiStyle { bg_color = zephr.Color{201, 172, 126, 255}, fg_color = zephr.COLOR_BLACK, border_radius = btn_con.height * 0.25, align = .BOTTOM_CENTER, }; if (zephr.draw_button(&btn_con, "New Game", style, bg_btns_state)) { reset_game(); } if (game.settings_dialog) { draw_settings_dialog(); } if (game.help_dialog) { draw_help_dialog(); } if (game.has_lost) { draw_game_over(); } if (game.quit_dialog) { draw_quit_dialog(); } } draw_settings_dialog :: proc() { dialog_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); zephr.set_x_constraint(&dialog_con, 0, .FIXED); zephr.set_y_constraint(&dialog_con, 0, .FIXED); style := zephr.UiStyle { bg_color = zephr.Color{0, 0, 0, 200}, align = .CENTER, }; zephr.draw_quad(&dialog_con, style); content_card_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&content_card_con, &dialog_con); zephr.set_width_constraint(&content_card_con, 0.55, .RELATIVE); zephr.set_height_constraint(&content_card_con, 0.6, .RELATIVE); style = zephr.UiStyle{ bg_color = zephr.Color{135, 124, 124, 255}, border_radius = 8, align = .CENTER, }; zephr.draw_quad(&content_card_con, style); text_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&text_con, &content_card_con); zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); zephr.set_x_constraint(&text_con, 0, .FIXED); zephr.set_y_constraint(&text_con, 40, .RELATIVE_PIXELS); zephr.draw_text("Settings", 64, text_con, zephr.COLOR_YELLOW, .TOP_CENTER); zephr.set_x_constraint(&text_con, 30, .RELATIVE_PIXELS); zephr.set_y_constraint(&text_con, 150, .RELATIVE_PIXELS); zephr.draw_text("Color Palette", 40, text_con, zephr.COLOR_WHITE, .TOP_LEFT); bg_color_text := "Background: "; font_size: u32 = 30; zephr.set_x_constraint(&text_con, 30, .RELATIVE_PIXELS); zephr.set_y_constraint(&text_con, 220, .RELATIVE_PIXELS); zephr.set_height_constraint(&text_con, 0.9, .RELATIVE); zephr.draw_text(bg_color_text, font_size, text_con, zephr.COLOR_WHITE, .TOP_LEFT); text_size := zephr.calculate_text_size(bg_color_text, font_size); color_picker_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&color_picker_con, &content_card_con); zephr.set_x_constraint(&color_picker_con, 30 + text_size.x + 20, .RELATIVE_PIXELS); zephr.set_y_constraint(&color_picker_con, 220 - text_size.y / 2 + 6, .RELATIVE_PIXELS); zephr.set_width_constraint(&color_picker_con, 100, .RELATIVE_PIXELS); zephr.set_height_constraint(&color_picker_con, 40, .RELATIVE_PIXELS); zephr.draw_color_picker(&color_picker_con, &game.palette.bg_color, .TOP_LEFT, .ACTIVE); zephr.set_y_constraint(&text_con, 270, .RELATIVE_PIXELS); zephr.draw_text("Board: ", font_size, text_con, zephr.COLOR_WHITE, .TOP_LEFT); zephr.set_x_constraint(&color_picker_con, 30 + text_size.x + 20, .RELATIVE_PIXELS); zephr.set_y_constraint(&color_picker_con, 270 - text_size.y / 2 + 6, .RELATIVE_PIXELS); zephr.set_width_constraint(&color_picker_con, 100, .RELATIVE_PIXELS); zephr.set_height_constraint(&color_picker_con, 40, .RELATIVE_PIXELS); zephr.draw_color_picker(&color_picker_con, &game.palette.board_bg_color, .TOP_LEFT, .ACTIVE); zephr.set_y_constraint(&text_con, 320, .RELATIVE_PIXELS); zephr.draw_text("Tiles: ", font_size, text_con, zephr.COLOR_WHITE, .TOP_LEFT); tile_size: f32 = 60; for i in 0..<11 { zephr.set_x_constraint(&color_picker_con, 30 + text_size.x + 20 + (cast(f32)i * (tile_size + 10)), .RELATIVE_PIXELS); zephr.set_y_constraint(&color_picker_con, 320 - text_size.y / 2 + 6, .RELATIVE_PIXELS); zephr.set_width_constraint(&color_picker_con, tile_size, .RELATIVE_PIXELS); zephr.set_height_constraint(&color_picker_con, tile_size, .RELATIVE_PIXELS); zephr.draw_color_picker(&color_picker_con, &game.palette.tile_colors[i], .TOP_LEFT, .ACTIVE, cast(u32)i); tile_val := cast(u16)math.pow(2, cast(f32)i + 1); buf: [16]byte text := fmt.bprintf(buf[:], "%d", tile_val); font_size := get_tile_font_size(tile_val) / 2; tile_color := get_tile_color(tile_val); zephr.set_parent_constraint(&text_con, &color_picker_con); zephr.set_x_constraint(&text_con, 0, .RELATIVE_PIXELS); zephr.set_y_constraint(&text_con, 0, .RELATIVE_PIXELS); zephr.draw_text(text, font_size, text_con, zephr.mult_color(tile_color, 0.5), .CENTER); } btn_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&btn_con, &content_card_con); zephr.set_x_constraint(&btn_con, 0, .RELATIVE_PIXELS); zephr.set_y_constraint(&btn_con, 400, .RELATIVE_PIXELS); zephr.set_width_constraint(&btn_con, 150, .RELATIVE_PIXELS); zephr.set_height_constraint(&btn_con, 0.35, .ASPECT_RATIO); style = zephr.UiStyle{ bg_color = zephr.Color{201, 146, 126, 255}, fg_color = zephr.COLOR_BLACK, border_radius = 8, align = .TOP_CENTER, }; if (zephr.draw_button(&btn_con, "Reset", style, .ACTIVE)) { reset_palette(); } zephr.set_width_constraint(&btn_con, 56, .RELATIVE_PIXELS); zephr.set_height_constraint(&btn_con, 1, .ASPECT_RATIO); zephr.set_y_constraint(&btn_con, -24, .RELATIVE_PIXELS); zephr.set_x_constraint(&btn_con, 24, .RELATIVE_PIXELS); style = zephr.UiStyle{ bg_color = zephr.mult_color(zephr.COLOR_WHITE, 0.8), fg_color = zephr.Color{232, 28, 36, 255}, border_radius = btn_con.height * 0.2, align = .TOP_RIGHT, } if (zephr.draw_icon_button(&btn_con, game.icon_textures[.CLOSE_ICON], style, .ACTIVE)) { game.settings_dialog = false; } } draw_help_dialog :: proc() { dialog_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); zephr.set_x_constraint(&dialog_con, 0, .FIXED); zephr.set_y_constraint(&dialog_con, 0, .FIXED); style := zephr.UiStyle { bg_color = zephr.Color{0, 0, 0, 200}, align = .CENTER, }; zephr.draw_quad(&dialog_con, style); content_card_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_parent_constraint(&content_card_con, &dialog_con); zephr.set_width_constraint(&content_card_con, 0.4, .RELATIVE); zephr.set_height_constraint(&content_card_con, 0.33, .RELATIVE); style = zephr.UiStyle{ bg_color = zephr.Color{135, 124, 124, 255}, border_radius = 8, align = .CENTER, }; zephr.draw_quad(&content_card_con, style); text_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_parent_constraint(&text_con, &content_card_con); zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); zephr.set_x_constraint(&text_con, 0, .FIXED); zephr.set_y_constraint(&text_con, 40, .RELATIVE_PIXELS); zephr.draw_text("How to play", 64, text_con, zephr.COLOR_YELLOW, .TOP_CENTER); zephr.set_y_constraint(&text_con, 30, .RELATIVE_PIXELS); zephr.set_height_constraint(&text_con, 0.9, .RELATIVE); zephr.draw_text("Use the arrow keys to move the tiles.\n" + "When two tiles with the same number touch, they\n" + "merge into one!\n\nYour goal is to reach 2048 without filling all the tiles", 30., text_con, zephr.COLOR_WHITE, .CENTER); btn_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&btn_con, &content_card_con); zephr.set_width_constraint(&btn_con, 56, .RELATIVE_PIXELS); zephr.set_height_constraint(&btn_con, 1, .ASPECT_RATIO); zephr.set_y_constraint(&btn_con, -24, .RELATIVE_PIXELS); zephr.set_x_constraint(&btn_con, 24, .RELATIVE_PIXELS); style = zephr.UiStyle{ bg_color = zephr.mult_color(zephr.COLOR_WHITE, 0.8), fg_color = zephr.Color{232, 28, 36, 255}, border_radius = btn_con.height * 0.2, align = .TOP_RIGHT, }; if (zephr.draw_icon_button(&btn_con, game.icon_textures[.CLOSE_ICON], style, .ACTIVE)) { game.help_dialog = false; } } draw_game_over :: proc() { dialog_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); zephr.set_x_constraint(&dialog_con, 0, .FIXED); zephr.set_y_constraint(&dialog_con, 0, .FIXED); style := zephr.UiStyle { bg_color = zephr.Color{0, 0, 0, cast(u8)game.game_over_bg_opacity}, align = .CENTER, }; zephr.draw_quad(&dialog_con, style); content_card_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_parent_constraint(&content_card_con, &dialog_con); zephr.set_width_constraint(&content_card_con, 0.3, .RELATIVE); zephr.set_height_constraint(&content_card_con, 0.2, .RELATIVE); style = zephr.UiStyle{ bg_color = zephr.Color{135, 124, 124, cast(u8)game.game_over_opacity}, border_radius = 8, align = .CENTER, }; zephr.draw_quad(&content_card_con, style); text_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_parent_constraint(&text_con, &content_card_con); zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); zephr.set_x_constraint(&text_con, 0, .FIXED); zephr.set_y_constraint(&text_con, 40, .RELATIVE_PIXELS); zephr.draw_text("Game Over!", 64, text_con, zephr.Color{255, 255, 0, cast(u8)game.game_over_opacity}, .TOP_CENTER); btn_con := zephr.DEFAULT_UI_CONSTRAINTS btn_state: zephr.ButtonState = game.quit_dialog ? .INACTIVE : game.game_over_opacity > 0 ? .ACTIVE : .INACTIVE; zephr.set_parent_constraint(&btn_con, &content_card_con); zephr.set_x_constraint(&btn_con, 0, .RELATIVE_PIXELS); zephr.set_y_constraint(&btn_con, -24, .RELATIVE_PIXELS); zephr.set_width_constraint(&btn_con, 0.30, .RELATIVE); zephr.set_height_constraint(&btn_con, 0.3, .ASPECT_RATIO); style = zephr.UiStyle{ bg_color = zephr.Color{201, 146, 126, cast(u8)game.game_over_opacity}, fg_color = zephr.Color{0, 0, 0, cast(u8)game.game_over_opacity}, border_radius = 8, align = .BOTTOM_CENTER, }; if (zephr.draw_button(&btn_con, "Try Again", style, btn_state)) { reset_game(); } } draw_quit_dialog :: proc() { dialog_con := zephr.DEFAULT_UI_CONSTRAINTS; zephr.set_x_constraint(&dialog_con, 0, .FIXED); zephr.set_y_constraint(&dialog_con, 0, .FIXED); zephr.set_width_constraint(&dialog_con, 1, .RELATIVE); zephr.set_height_constraint(&dialog_con, 1, .RELATIVE); style := zephr.UiStyle { bg_color = zephr.Color{0, 0, 0, 200}, align = .CENTER, }; zephr.draw_quad(&dialog_con, style); zephr.set_width_constraint(&dialog_con, 0.4, .RELATIVE); zephr.set_height_constraint(&dialog_con, 0.2, .RELATIVE); style = zephr.UiStyle{ bg_color = zephr.Color{135, 124, 124, 255}, border_radius = 8, align = .CENTER, }; zephr.draw_quad(&dialog_con, style); text_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&text_con, &dialog_con); zephr.set_x_constraint(&text_con, 0, .FIXED); zephr.set_y_constraint(&text_con, -0.25, .RELATIVE); zephr.set_width_constraint(&text_con, 1, .RELATIVE_PIXELS); zephr.draw_text("Are you sure you want to quit?", 36, text_con, zephr.COLOR_WHITE, .CENTER); btn_con := zephr.DEFAULT_UI_CONSTRAINTS zephr.set_parent_constraint(&btn_con, &dialog_con); zephr.set_width_constraint(&btn_con, 0.25, .RELATIVE); zephr.set_height_constraint(&btn_con, 0.25, .RELATIVE); zephr.set_y_constraint(&btn_con, -0.10, .RELATIVE); zephr.set_x_constraint(&btn_con, -0.20, .RELATIVE); style = zephr.UiStyle{ bg_color = zephr.mult_color(zephr.COLOR_WHITE, 0.9), fg_color = zephr.COLOR_BLACK, border_radius = btn_con.height * 0.2, align = .BOTTOM_CENTER, }; if (zephr.draw_button(&btn_con, "Yes", style, .ACTIVE)) { zephr.quit(); } zephr.set_y_constraint(&btn_con, -0.10, .RELATIVE); zephr.set_x_constraint(&btn_con, 0.20, .RELATIVE); if (zephr.draw_button(&btn_con, "No", style, .ACTIVE)) { game.quit_dialog = false; } }