Files
O2048/game.odin
T

1290 lines
39 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 {
start_pos: zephr.Vec2,
position: zephr.Vec2,
new_pos: zephr.Vec2,
anim_timer: time.Duration,
scale: zephr.Vec2,
value: u16,
new_value: u16,
merged: bool,
emitter: ParticleEmitter,
}
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: zephr.Vec2,
spawning_tile_coords: zephr.Vec2,
spawning_tile_value: u16,
quit_dialog: bool,
settings_dialog: bool,
help_dialog: bool,
icon_textures: [Icon]zephr.TextureId,
merge_particle_texture: zephr.TextureId,
palette: ColorPalette,
}
@private
TILE_ANIM_DURATION :: time.Millisecond * 250
@private
TILE_SPAWN_DURATION :: time.Millisecond * 200
@private
GAMEOVER_ANIM_DURATION :: time.Millisecond * 300
@private
neighbors := [4]zephr.Vec2 {
{-1, 0}, // left
{1, 0}, // right
{0, -1}, // up
{0, 1}, // down
}
@private
gameover_anim_time: 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 row in 0..<4 {
for col := 2; col >= 0; col -= 1 {
if (game.board[row][col].value == 0) {
continue
}
tiles_to_move := 0
x := col
src := &game.board[row][col]
dest := &game.board[row][x]
for {
if (x >= 3) do break
x += 1
dest = &game.board[row][x]
// next tile is empty so we can move there
if (dest.new_value == 0) {
tiles_to_move += 1
continue
}
// next tile has the same value so we can merge and move
if (dest.new_value == src.new_value && !dest.merged) {
tiles_to_move += 1
dest.merged = true
break
} else {
// next tile has a different value so we can't move there
x -= 1
break
}
}
dest = &game.board[row][x]
if (tiles_to_move > 0) {
if (dest.merged) {
dest.new_value *= 2
game.score += auto_cast dest.new_value
} else {
dest.new_value = src.new_value
}
src.new_value = 0
src.new_pos = zephr.Vec2{cast(f32)row, cast(f32)x}
src.anim_timer = 0
game.animating = true
game.spawning_tile_coords = get_random_available_tile_coords()
game.spawning_tile_value = get_spawned_tile_value()
}
}
}
}
move_left :: proc() {
game.last_move_dir = .LEFT
for row in 0..<4 {
for col in 1..<4 {
if (game.board[row][col].value == 0) {
continue
}
tiles_to_move := 0
x := col
src := &game.board[row][col]
dest := &game.board[row][x]
for {
if (x <= 0) do break
x -= 1
dest = &game.board[row][x]
// next tile is empty so we can move there
if (dest.new_value == 0) {
tiles_to_move += 1
continue
}
// next tile has the same value so we can merge and move
if ((dest.new_value == src.new_value) && !dest.merged) {
tiles_to_move += 1
dest.merged = true
break
} else {
// next tile has a different value so we can't move there
x += 1
break
}
}
dest = &game.board[row][x]
if (tiles_to_move > 0) {
if (dest.merged) {
dest.new_value *= 2
game.score += auto_cast dest.new_value
} else {
dest.new_value = src.new_value
}
src.new_value = 0
src.new_pos = zephr.Vec2{cast(f32)row, cast(f32)x}
src.anim_timer = 0
game.animating = true
game.spawning_tile_coords = get_random_available_tile_coords()
game.spawning_tile_value = get_spawned_tile_value()
}
}
}
}
move_down :: proc() {
game.last_move_dir = .DOWN
for row := 2; row >= 0; row -= 1 {
for col in 0..<4 {
if (game.board[row][col].value == 0) {
continue
}
tiles_to_move := 0
y := row
src := &game.board[row][col]
dest := &game.board[y][col]
for {
if (y >= 3) do break
y += 1
dest = &game.board[y][col]
if (dest.new_value == 0) {
tiles_to_move += 1
continue
}
if ((dest.new_value == src.new_value) && !dest.merged) {
tiles_to_move += 1
dest.merged = true
break
} else {
y -= 1
break
}
}
dest = &game.board[y][col]
if (tiles_to_move > 0) {
if (dest.merged) {
dest.new_value *= 2
game.score += auto_cast dest.new_value
} else {
dest.new_value = src.new_value
}
src.new_value = 0
src.new_pos = zephr.Vec2{cast(f32)y, cast(f32)col}
src.anim_timer = 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 row in 1..<4 {
for col in 0..<4 {
if (game.board[row][col].value == 0) {
continue
}
tiles_to_move := 0
y := row
src := &game.board[row][col]
dest := &game.board[y][col]
for {
if (y <= 0) do break
y -= 1
dest = &game.board[y][col]
if (dest.new_value == 0) {
tiles_to_move += 1
continue
}
if (dest.new_value == src.new_value && !dest.merged) {
tiles_to_move += 1
dest.merged = true
break
} else {
y += 1
break
}
}
dest = &game.board[y][col]
if (tiles_to_move > 0) {
if (dest.merged) {
dest.new_value *= 2
game.score += auto_cast dest.new_value
} else {
dest.new_value = src.new_value
}
src.new_value = 0
src.new_pos = zephr.Vec2{cast(f32)y, cast(f32)col}
src.anim_timer = 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() {
gameover_anim_time = 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
delete(game.board[y][x].emitter.particles)
game.board[y][x].emitter = ParticleEmitter{}
}
}
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")
game.merge_particle_texture = zephr.load_texture("assets/particles/merge.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] = Tile{
value = value,
new_value = value,
position = zephr.Vec2{cast(f32)x, cast(f32)y},
start_pos = zephr.Vec2{cast(f32)x, cast(f32)y},
new_pos = zephr.Vec2{cast(f32)x, cast(f32)y},
scale = zephr.Vec2{0, 0},
}
}
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
game.board[x][y].position = zephr.Vec2{cast(f32)x, cast(f32)y}
game.board[x][y].start_pos = zephr.Vec2{cast(f32)x, cast(f32)y}
game.board[x][y].new_pos = zephr.Vec2{cast(f32)x, cast(f32)y}
game.board[x][y].scale = zephr.Vec2{1, 1}
}
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()
} else if (e.key.scancode == .N) {
reset_game()
}
}
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)
update_emitters(delta_t)
draw_bg()
draw_board()
draw_ui()
zephr.swap_buffers()
}
}
update_emitters :: proc(delta_t: time.Duration) {
for row in &game.board {
for tile in &row {
update_emitter(delta_t, &tile.emitter)
}
}
}
update_positions :: proc(delta_t: time.Duration) {
if (game.animating) {
switch game.last_move_dir {
case .LEFT:
for i in 0..<4 {
for j in 1..<4 {
src := &game.board[i][j]
if src.value == 0 || src.start_pos == src.new_pos {
continue
}
if src.anim_timer < TILE_ANIM_DURATION {
src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.anim_timer += delta_t
} else {
dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y]
if dest.new_value == src.value * 2 {
emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200)
dest.merged = false
}
dest^ = Tile{
value = dest.new_value,
new_value = dest.new_value,
position = src.new_pos,
start_pos = src.new_pos,
new_pos = src.new_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = dest.emitter,
}
src^ = Tile{
value = src.new_value,
new_value = src.new_value,
position = src.start_pos,
start_pos = src.start_pos,
new_pos = src.start_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = src.emitter
}
game.spawning_new_tile = true
}
}
}
case .RIGHT:
for i in 0..<4 {
for j := 2; j >= 0; j -= 1 {
src := &game.board[i][j]
if src.value == 0 || src.start_pos == src.new_pos {
continue
}
if src.anim_timer < TILE_ANIM_DURATION {
src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.anim_timer += delta_t
} else {
dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y]
if dest.new_value == src.value * 2 {
emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200)
dest.merged = false
}
dest^ = Tile{
value = dest.new_value,
new_value = dest.new_value,
position = src.new_pos,
start_pos = src.new_pos,
new_pos = src.new_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = dest.emitter,
}
src^ = Tile{
value = src.new_value,
new_value = src.new_value,
position = src.start_pos,
start_pos = src.start_pos,
new_pos = src.start_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = src.emitter,
}
game.spawning_new_tile = true
}
}
}
case .UP:
for i in 1..<4 {
for j in 0..<4 {
src := &game.board[i][j]
if src.value == 0 || src.start_pos == src.new_pos {
continue
}
if src.anim_timer < TILE_ANIM_DURATION {
src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.anim_timer += delta_t
} else {
dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y]
if dest.new_value == src.value * 2 {
emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200)
dest.merged = false
}
dest^ = Tile{
value = dest.new_value,
new_value = dest.new_value,
position = src.new_pos,
start_pos = src.new_pos,
new_pos = src.new_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = dest.emitter,
}
src^ = Tile{
value = src.new_value,
new_value = src.new_value,
position = src.start_pos,
start_pos = src.start_pos,
new_pos = src.start_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = src.emitter,
}
game.spawning_new_tile = true
}
}
}
case .DOWN:
for i := 2; i >= 0; i -= 1 {
for j in 0..<4 {
src := &game.board[i][j]
if src.value == 0 || src.start_pos == src.new_pos {
continue
}
if src.anim_timer < TILE_ANIM_DURATION {
src.position.x = math.lerp(src.start_pos.x, src.new_pos.x, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.position.y = math.lerp(src.start_pos.y, src.new_pos.y, cast(f32)zephr.ease_out_cubic(cast(f64)src.anim_timer / cast(f64)TILE_ANIM_DURATION))
src.anim_timer += delta_t
} else {
dest := &game.board[cast(int)src.new_pos.x][cast(int)src.new_pos.y]
if dest.new_value == src.value * 2 {
emitter_start(&dest.emitter, 500, dest.new_pos, time.Millisecond * 200)
dest.merged = false
}
dest^ = Tile{
value = dest.new_value,
new_value = dest.new_value,
position = src.new_pos,
start_pos = src.new_pos,
new_pos = src.new_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = dest.emitter,
}
src^ = Tile{
value = src.new_value,
new_value = src.new_value,
position = src.start_pos,
start_pos = src.start_pos,
new_pos = src.start_pos,
anim_timer = 0,
scale = zephr.Vec2{1, 1},
emitter = src.emitter
}
game.spawning_new_tile = true
}
}
}
}
}
if (gameover()) {
game.has_lost = true
game.animating = true
}
if (game.spawning_new_tile) {
tile := &game.board[cast(int)game.spawning_tile_coords.x][cast(int)game.spawning_tile_coords.y]
if tile.anim_timer < TILE_SPAWN_DURATION {
game.spawning_tile_scale.x = math.lerp(cast(f32)0, 1, zephr.ease_out_circ(cast(f32)tile.anim_timer / cast(f32)TILE_SPAWN_DURATION))
game.spawning_tile_scale.y = math.lerp(cast(f32)0, 1, zephr.ease_out_circ(cast(f32)tile.anim_timer / cast(f32)TILE_SPAWN_DURATION))
tile.anim_timer += delta_t
} else {
spawn_new_tile_with_value(cast(u8)game.spawning_tile_coords.x, cast(u8)game.spawning_tile_coords.y, game.spawning_tile_value);
tile.anim_timer = 0
game.spawning_tile_coords = zephr.Vec2{0, 0}
game.spawning_tile_value = 0
game.spawning_tile_scale = 0.0
game.spawning_new_tile = false
game.animating = false
}
}
if (game.has_lost) {
if gameover_anim_time < GAMEOVER_ANIM_DURATION {
game.game_over_opacity = cast(f32)math.lerp(0.0, 255.0, cast(f64)gameover_anim_time / cast(f64)GAMEOVER_ANIM_DURATION)
game.game_over_bg_opacity = cast(f32)math.lerp(0.0, 200.0, cast(f64)gameover_anim_time / cast(f64)GAMEOVER_ANIM_DURATION)
gameover_anim_time += 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 row in 0..<4 {
for col in 0..<4 {
tile := &game.board[row][col]
if tile.value == 0 {
continue
}
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) * tile.position.x + tile_padding)
tile_x_pos := ((tile_height + tile_padding) * tile.position.y + tile_padding)
zephr.set_y_constraint(&tile_con, tile_y_pos, .FIXED)
zephr.set_x_constraint(&tile_con, tile_x_pos, .FIXED)
// Draw particles
particle_con := zephr.DEFAULT_UI_CONSTRAINTS
for particle in tile.emitter.particles {
if (particle.life <= 0) {
continue
}
zephr.set_parent_constraint(&particle_con, &tile_con)
zephr.set_x_constraint(&particle_con, particle.pos.x, .FIXED)
zephr.set_y_constraint(&particle_con, particle.pos.y, .FIXED)
particle_con.scale = particle.size
zephr.set_width_constraint(&particle_con, 30, .RELATIVE_PIXELS)
zephr.set_height_constraint(&particle_con, 1, .ASPECT_RATIO)
zephr.draw_texture(&particle_con, game.merge_particle_texture, zephr.UiStyle{
bg_color = particle.color,
fg_color = particle.color,
align = .CENTER,
})
}
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,
align = .TOP_LEFT,
}
tile_x_pos := ((tile_height + tile_padding) * game.spawning_tile_coords.y + tile_padding)
tile_y_pos := ((tile_height + tile_padding) * game.spawning_tile_coords.x + tile_padding)
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, .FIXED)
zephr.set_width_constraint(&tile_con, 1, .ASPECT_RATIO)
tile_con.scale = game.spawning_tile_scale
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)
text_con.scale = game.spawning_tile_scale
buf: [16]byte
value := fmt.bprintf(buf[:], "%d", game.spawning_tile_value)
zephr.draw_text(value, cast(u32)get_tile_font_size(game.spawning_tile_value), 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
}
}