Files
O2048/game.odin
T
2024-01-19 23:55:07 +02:00

1178 lines
36 KiB
Odin

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;
}
}