From d568031b82ee33ab4561f966437a40ea4cef5d18 Mon Sep 17 00:00:00 2001 From: Nefrace Date: Mon, 9 Sep 2024 23:41:16 +0300 Subject: [PATCH] Final --- assets/ball.png | Bin 141 -> 0 bytes assets/jet.png | Bin 268 -> 0 bytes assets/pad.png | Bin 653 -> 0 bytes ball.odin | 20 +++++- brick.odin | 150 ++++++++++++++++++++++++++++++++++++++++++--- explosion.odin | 46 ++++++++++++++ game.odin | 158 ++++++++++++++++++++++++++++++++++++++---------- gameover.odin | 84 +++++++++++++++++++++++++ howtoplay.odin | 62 +++++++++++++++++++ leveldone.odin | 79 ++++++++++++++++++++++++ main.odin | 5 +- menu.odin | 8 +-- pad.odin | 9 +++ pause.odin | 8 ++- state.odin | 6 +- 15 files changed, 582 insertions(+), 53 deletions(-) delete mode 100644 assets/ball.png delete mode 100644 assets/jet.png delete mode 100644 assets/pad.png create mode 100644 explosion.odin create mode 100644 gameover.odin create mode 100644 howtoplay.odin create mode 100644 leveldone.odin diff --git a/assets/ball.png b/assets/ball.png deleted file mode 100644 index 3a2583146357e8b92da37f1db8726888f7f0f8df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|+&x_!LnNlQ z_8;VJFyLW6f4Ba^r#CFeo0*on76dKwC=;?QagpTUbg1meO^y^0-5R>Sa>beD>8Gau nNIr4*x;dlDla5bmR>|*_Y<1^HD|Tf7jb!k2^>bP0l+XkK?m8~A diff --git a/assets/jet.png b/assets/jet.png deleted file mode 100644 index 3b2ffff64c537ec25f4e84d38b985fe4b141fba2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 268 zcmV+n0rUQeP)Px##z{m$R7i=nmBAH*AP__$FTGfc2TDkFDZzuaSPeP&Heq2#mJQAYeKNyeT`-11 zIiws6ieZcFSudmZes7@hy`AGLnIJp`L}-+S*X11d1A>%6G($wZj|Q(cTA!hL>+&mt zcv}FG80+iF;sAqEJt41qKj7yp=S~Cg3OA9V3E5VJITyaChuSQ#eQPNiNDsoU4x%PU zE!O&Y50vzVO1x44y~_TX!+f{x0)5UpI7yvPx%M@d9MRA_ZbpEmV>b~4%)MgGJx5nxV4E&*bR&;x(>Y~m5iMCeKAsSgJkyvoGS zEsrb#2OqpdOF|DmQ1B8Z3B3;&ykwIEPvAQrX&%YjF;k(3HO% zE0>YUpNqF6kHjNRN;)|aFX?k8td{k)b`{)+p+6u^*2qs2syUlcT}$sT3{FJR;FRqH zmlXUOEHXPCLllp+^0|vdPOIv7wl-#8IC<-pzH#6<@3y9UfN6@^?hA4suu=AoYoD&i zb)&bhA~P!2clXxzWeIBfM(VT&G>Pl(zAyHk0EvD^mtBzBqVXjF<;y?@fAM+>z@onb z%9GIa^oN#C0T-heucs{fD;E7#mWW+7>(JUUSY}v`x?ZlMwF|8YONy3*>mJZ%@UVvj nsJQ;8(00%r9UUDV9dG;v3a~w`%v2%V00000NkvXXu0mjf#tkgV diff --git a/ball.odin b/ball.odin index 37c9e58..47efc1a 100644 --- a/ball.odin +++ b/ball.odin @@ -22,7 +22,7 @@ ball_update :: proc(ball: ^Ball, game: ^Game, delta: f32) -> bool { } else { ball_position_prev := ball.position ball.velocity = rl.Vector2Rotate(ball.velocity, ball.angular_velocity * delta) - if abs(ball.velocity.y) < 50 { + if abs(ball.velocity.y) < 100 && ball.angular_velocity == 0 { ball.velocity.y += math.sign(ball.velocity.y) * 100 * delta } ball.position += ball.velocity * delta @@ -55,7 +55,7 @@ ball_update :: proc(ball: ^Ball, game: ^Game, delta: f32) -> bool { offset_x := ball.position.x - pad.position.x dir.x = offset_x / (pad.size.x / 2) ball.velocity = rl.Vector2Normalize(dir) * rl.Vector2Length(ball.velocity) - ball.angular_velocity = -pad.velocity.x / PAD_MAX_SPEED * math.PI / 2 + ball.angular_velocity = -pad.velocity.x / PAD_MAX_SPEED * math.PI / 3 } for &brick, i in bricks { @@ -77,11 +77,17 @@ ball_update :: proc(ball: ^Ball, game: ^Game, delta: f32) -> bool { diff := clamped - ball.position normal := rl.Vector2Normalize(-diff) if rl.Vector2LengthSqr(diff) < ball.radius * ball.radius { - brick.hits -= 1 + if brick.type != .SOLID{ + brick.hits -= 1 + } if brick.hits <= 0 && !brick.is_flying { brick.is_flying = true brick.velocity = ball.velocity / 2 brick.angular_velocity = brick.velocity.x + game.score += brick.score + } + if brick.hit_callback != nil { + brick.hit_callback(&brick, game) } ball.position = clamped + normal * ball.radius ball.velocity = linalg.reflect(ball.velocity, normal) @@ -92,3 +98,11 @@ ball_update :: proc(ball: ^Ball, game: ^Game, delta: f32) -> bool { return true } +ball_draw :: proc(ball: ^Ball) { + draw_ball_virtual(ball.position, ball.radius) +} + +draw_ball_virtual :: proc(position: Vec2, radius: f32) { + rl.DrawCircleV(position, radius, rl.Color{80, 120, 255, 255}) + rl.DrawCircleV(position - radius / 3, radius / 5, rl.Color{80, 160, 255, 255}) +} diff --git a/brick.odin b/brick.odin index 1b45062..7617f26 100644 --- a/brick.odin +++ b/brick.odin @@ -1,6 +1,58 @@ package main import rl "vendor:raylib" +import "core:fmt" + +Brick_Type :: enum { + NORMAL, + EXPLOSIVE, + BONUS, + HARD, + SOLID +} + +brick_names := [Brick_Type]cstring { + .NORMAL = "обычный", + .EXPLOSIVE = "взрывной", + .BONUS = "восстанавливает здоровье", + .HARD = "крепкий", + .SOLID = "нерушимый" +} + +brick_colors := [Brick_Type]rl.Color { + .NORMAL = rl.Color{190, 190, 190, 255}, + .EXPLOSIVE = rl.Color{250, 30, 30, 255}, + .BONUS = rl.Color{30, 250, 30, 255}, + .HARD = rl.Color{190, 190, 255, 255}, + .SOLID = rl.Color{80, 80, 100, 255} +} + + +brick_scores := [Brick_Type]u32 { + .NORMAL = 100, + .HARD = 500, + .BONUS = 1000, + .EXPLOSIVE = 1000, + .SOLID = 0, +} + +brick_hits := [Brick_Type]i32 { + .NORMAL = 1, + .EXPLOSIVE = 1, + .BONUS = 1, + .HARD = 2, + .SOLID = 10000000 +} + +BrickHitCallback :: proc(brick: ^Brick, game: ^Game) + +brick_callbacks := [Brick_Type]BrickHitCallback { + .NORMAL = nil, + .EXPLOSIVE = brick_explode, + .BONUS = nil, + .HARD = nil, + .SOLID = nil +} @@ -13,32 +65,114 @@ Brick :: struct { is_flying: bool, is_fixed: bool, color: rl.Color, - hits: u8, + hits: i32, is_to_free: bool, + type: Brick_Type, + score: u32, + hit_callback: proc(brick: ^Brick, game: ^Game) } -spawn_brick :: proc(position: Vec2, size: Vec2, is_fixed: bool = true, color: rl.Color = rl.RED, hits : u8 = 1) -> Brick { +spawn_brick :: proc(position: Vec2, size: Vec2, type: Brick_Type = Brick_Type.NORMAL) -> Brick { return Brick { position = position, velocity = Vec2{}, - size = Vec2{30,20}, + size = size, is_flying = false, - is_fixed = is_fixed, - color = color, - hits = hits, + is_fixed = true, + type = type, + color = brick_colors[type], + hits = brick_hits[type], + score = brick_scores[type], + hit_callback = brick_callbacks[type], angle = 0 } } -brick_update :: proc(brick: ^Brick, camera: rl.Camera2D, delta: f32) { +brick_update :: proc(brick: ^Brick, game: ^Game, delta: f32) { if !brick.is_flying { return } brick.velocity.y += 500 * delta brick.position += brick.velocity * delta brick.angle += brick.angular_velocity * delta - screen_pos := rl.GetWorldToScreen2D(brick.position, camera) + screen_pos := rl.GetWorldToScreen2D(brick.position, game.camera) + + if brick.position.x < brick.size.x / 2 { + brick.position.x = brick.size.x / 2 + brick.velocity.x = -brick.velocity.x + } + if brick.position.x > GameField.x - brick.size.x / 2 { + brick.position.x = GameField.x - brick.size.x / 2 + brick.velocity.x = -brick.velocity.x + } + + if rl.CheckCollisionCircleRec(brick.position, brick.size.x / 2, rl.Rectangle{ + game.pad.position.x - game.pad.size.x / 2, + game.pad.position.y - game.pad.size.y / 2, + game.pad.size.x, + game.pad.size.y + }) { + if brick.type != .BONUS { + game.pad.health -= 30 + } else { + game.pad.health = min(game.pad.health + 30, 100) + } + brick.is_to_free = true + } if !rl.CheckCollisionRecs(rl.Rectangle{screen_pos.x - brick.size.x / 2, screen_pos.y - brick.size.y / 2, brick.size.x, brick.size.y}, rl.Rectangle{0, 0, WINDOWF.x, WINDOWF.y} ) { brick.is_to_free = true } } + +brick_draw :: proc(brick: ^Brick) { + rl.DrawRectanglePro( + rl.Rectangle{brick.position.x, brick.position.y, brick.size.x, brick.size.y}, + brick.size / 2, + brick.angle, + brick.color + ) + if brick.type == .HARD && brick.hits <= 1 { + rl.DrawRectanglePro( + rl.Rectangle{brick.position.x, brick.position.y, brick.size.x / 1.5, brick.size.y / 1.5}, + brick.size / 3, + brick.angle, + rl.ColorTint(brick.color, rl.Color{180, 180, 180, 255}) + ) + } +} + + + +brick_explode :: proc(brick: ^Brick, game: ^Game) { + radius := brick.size.x * 2 + brick.is_to_free = true + + explode(brick.position, radius, 0.4) + for &other, i in game.bricks { + if other.is_to_free { + continue + } + if &other == brick { + continue + } + diff := other.position - brick.position + dir := rl.Vector2Normalize(diff) + dis := rl.Vector2Length(diff) + if dis > radius { + continue + } + switch other.type { + case .NORMAL, .HARD, .SOLID: + other.hits = 0 + other.is_flying = true + other.velocity += dir * (radius - dis + 50) * 3 + other.angular_velocity = other.velocity.x * 2 + game.score += other.score + case .EXPLOSIVE, .BONUS: + if other.hit_callback != nil { + other.hit_callback(&other, game) + } + } + } +} + diff --git a/explosion.odin b/explosion.odin new file mode 100644 index 0000000..44e3690 --- /dev/null +++ b/explosion.odin @@ -0,0 +1,46 @@ +package main + +import rl "vendor:raylib" +import "core:slice" +import "core:math/ease" + +Explosion :: struct{ + position: Vec2, + radius: f32, + max_radius: f32, + time: f32, + duration: f32, + to_free: bool, +} + +explosions_buf : [256]Explosion +explosions : [dynamic]Explosion + +explosions_init :: proc() { + explosions = slice.into_dynamic(explosions_buf[:]) +} + +explode :: proc(position: Vec2, max_radius: f32, duration: f32) { + append(&explosions, Explosion { + position = position, + max_radius = max_radius, + duration = duration + }) +} + +explosions_process :: proc(delta: f32) { + #reverse for &e, i in explosions { + e.time += delta + e.radius = ease.back_out(e.time / e.duration) * e.max_radius + if e.time > e.duration { + unordered_remove(&explosions, i) + } + } +} + +explosions_draw :: proc() { + #reverse for &e, i in explosions { + color := rl.ColorAlpha(rl.YELLOW, 1 - ease.exponential_in(e.time / e.duration)) + rl.DrawCircleV(e.position, e.radius, color) + } +} diff --git a/game.odin b/game.odin index 50027c7..7d76597 100644 --- a/game.odin +++ b/game.odin @@ -5,6 +5,9 @@ import "vendor:raylib/rlgl" import "core:fmt" import "core:math" import "core:math/ease" +import "core:math/rand" +import "core:strings" +import "core:strconv" // Virtual game field dimensions GameField := Vec2{800, 600} @@ -16,42 +19,93 @@ Game :: struct { balls: [dynamic]Ball, bricks: [dynamic]Brick, camera: rl.Camera2D, + score: u32, + levels_done: u32, + + levelsize: Vec2i, + + background: rl.Texture } + game_init :: proc(prev: ^GameState = nil) -> ^GameState { state := new(Game) + state.previous = prev state.variant = state state.draw = game_draw state.update = game_update state.free = game_free - state.lives = 3 - state.previous = prev - state.camera = rl.Camera2D{ + state.levelsize = Vec2i{6, 4} + + backimage := rl.GenImageChecked(800, 600, 80, 60, rl.Color{70, 20, 120, 255}, rl.Color{80, 30, 130, 255}) + defer rl.UnloadImage(backimage) + state.background = rl.LoadTextureFromImage(backimage) + + game_setup(state) + return state +} + +game_setup :: proc(game: ^Game) { + #reverse for ball, i in game.balls { + unordered_remove(&game.balls, i) + } + #reverse for brick, i in game.bricks { + unordered_remove(&game.bricks, i) + } + game.lives = 3 + game.camera = rl.Camera2D{ zoom = 1, target = GameField / 2, } - state.pad = Pad{ + game.pad = Pad{ position = Vec2{GameField.x / 2, GameField.y - 40}, - size = {80, 10}, + size = {80, 20}, + health = 100, } - append(&state.balls, Ball{ + game_gen_level(game) +} + +game_init_ball :: proc(game: ^Game) { + game_clear_balls(game) + append(&game.balls, Ball{ radius = 8, - position = Vec2{GameField.x / 2, GameField.y - 40 - 8}, + position = Vec2{GameField.x / 2, -100}, is_on_pad = true, - pad_offset = Vec2{0, -16}, + pad_offset = Vec2{0, -18}, }) +} +game_clear_balls :: proc(game: ^Game) { + #reverse for &ball, i in game.balls { + unordered_remove(&game.balls, i) + } +} +game_clear_bricks :: proc(game: ^Game) { + #reverse for &brick, i in game.bricks { + unordered_remove(&game.bricks, i) + } +} + +game_gen_level :: proc(game: ^Game) { + explosions_init() + game_init_ball(game) + game_clear_bricks(game) + + brick_size := Vec2{40, 30} + brick_margin := brick_size + 10 + center := Vec2{GameField.x / 2, 100} offset : f32 = 0 - for y : f32 = 40; y < GameField.y / 2; y += 35 { - for x : f32 = 40 + offset; x < GameField.x - 40; x += 40 { - append(&state.bricks, spawn_brick(Vec2{x, y}, Vec2{30, 20}, color = rl.YELLOW)) + for y : i32 = 0; y < game.levelsize.y; y += 1 { + for x : i32 = 0; x < game.levelsize.x / 2; x += 1{ + choice := rand.choice([]Brick_Type{.NORMAL, .NORMAL, .NORMAL, .HARD, .HARD, .SOLID, .EXPLOSIVE, .BONUS}) + append(&game.bricks, spawn_brick(center + {-brick_margin.x / 2 - f32(x) * brick_margin.x, f32(y) * brick_margin.y}, brick_size, choice)) + append(&game.bricks, spawn_brick(center + {+brick_margin.x / 2 + f32(x) * brick_margin.x, f32(y) * brick_margin.y}, brick_size, choice)) } - if offset == 0 { offset = 20 } else { offset = 0 } } - return state + } game_update :: proc(state: ^GameState, delta: f32) { @@ -63,6 +117,11 @@ game_update :: proc(state: ^GameState, delta: f32) { pause := pause_init(game) stack_push(pause) } + if rl.IsKeyPressed(rl.KeyboardKey.P) { + for &brick in bricks { + brick.is_flying = true + } + } pad_update(&game.pad, delta) if rl.IsKeyPressed(rl.KeyboardKey.SPACE) { for &ball, i in balls { @@ -86,62 +145,97 @@ game_update :: proc(state: ^GameState, delta: f32) { } } + if pad.health <= 0 { + + go := gameover_init(game) + stack_push(go) + return + } + if all_balls_outside { + if lives == 0 { + go := gameover_init(game) + stack_push(go) + return + } lives -= 1 append(&balls, Ball{ radius = 8, position = Vec2{GameField.x / 2, GameField.y - 40 - 8}, is_on_pad = true, - pad_offset = Vec2{0, -16}, + pad_offset = Vec2{0, -18}, }) } + only_solid := true #reverse for &brick, i in bricks { - brick_update(&brick, camera, delta) + brick_update(&brick, game, delta) + if brick.type != .SOLID { + only_solid = false + } if brick.is_to_free { unordered_remove(&bricks, i) } } + if only_solid { + leveldone := leveldone_init(game) + stack_push(leveldone) + } + + explosions_process(delta) + camera.offset = WINDOWF / 2 camera.zoom = (WINDOWF.y / GameField.y) / 1.1 } game_draw :: proc(state: ^GameState) { game := transmute(^Game)state - using game - rl.BeginMode2D(camera) + rl.BeginMode2D(game.camera) + rl.DrawTextureV(game.background, {}, rl.WHITE) +// rl.DrawRectangleGradientV(0, 0, i32(GameField.x), i32(GameField.y), rl.RED, rl.RAYWHITE) + for &ball, i in game.balls { + ball_draw(&ball) + } + pad_draw(&game.pad) - rl.DrawRectangleGradientV(0, 0, i32(GameField.x), i32(GameField.y), rl.RED, rl.RAYWHITE) - rl.DrawRectanglePro(rl.Rectangle{pad.position.x, pad.position.y, pad.size.x, pad.size.y}, pad.size / 2, 0, rl.GREEN) - - for ball, i in balls { - rl.DrawCircleV(ball.position, ball.radius, rl.BLUE) + for &brick, i in game.bricks { + brick_draw(&brick) } - - for brick, i in bricks { - rl.DrawRectanglePro(rl.Rectangle{brick.position.x, brick.position.y, brick.size.x, brick.size.y}, brick.size / 2, brick.angle, brick.color) - } - - //rl.DrawText(rl.TextFormat("%f\n%f\n%f\n%f", pad.position.x, pad.position.y, pad.velocity.x, pad.velocity.y), 0, 0, 20, rl.BLACK) + explosions_draw() rl.EndMode2D() - for i : u8 = 0; i < lives; i += 1 { - rl.DrawCircleV(Vec2{20 + 20 * f32(i), 20}, 10, rl.BLUE) + game_field_zoomed := GameField * game.camera.zoom + + buf : [12]byte + score_string := strconv.itoa(buf[:], int(game.score)) + score_ui := strings.right_justify(score_string, 8, "0") + score_ui_c := strings.unsafe_string_to_cstring(score_ui) + + size := rl.MeasureTextEx(FontUI, score_ui_c, 48, 2) + rl.DrawTextPro(FontUI, score_ui_c, {WINDOWF.x / 2, WINDOWF.y - 28}, size / 2, 0, 48, 2, rl.WHITE) + + level_string := rl.TextFormat("Уровень %d", game.levels_done + 1) + size = rl.MeasureTextEx(FontUI, level_string, 48, 2) + rl.DrawTextPro(FontUI, level_string, {WINDOWF.x / 2 + game_field_zoomed.x / 2, WINDOWF.y - 28}, {size.x, size.y / 2}, 0, 48, 2, rl.WHITE) + + for i : u8 = 0; i < game.lives; i += 1 { + draw_ball_virtual({WINDOWF.x / 2 - game_field_zoomed.x / 2 + 16 + f32(i * 40), WINDOWF.y - 28}, 16) } - rl.DrawTextEx(FontUI, rl.TextFormat("%d", len(bricks)), {}, f32(48), 2, rl.BLACK) - rl.DrawTextEx(FontUI, rl.TextFormat("%d", len(balls)), {0, 100}, f32(48), 2, rl.BLACK) } + game_free :: proc(state: ^GameState) { game := transmute(^Game)state + rl.UnloadTexture(game.background) + delete(game.bricks) delete(game.balls) diff --git a/gameover.odin b/gameover.odin new file mode 100644 index 0000000..bd9a93b --- /dev/null +++ b/gameover.odin @@ -0,0 +1,84 @@ +package main + +import rl "vendor:raylib" +import "core:math/ease" + + +GameOver :: struct { + using state: GameState, + position: Vec2, + size: Vec2, + ready_to_go: bool, +} + +gameover_init :: proc(prev: ^GameState = nil) -> ^GameState { + state := new(GameOver) + state.variant = state + state.position = Vec2{WINDOWF.x / 2, WINDOWF.y + 300} + state.size = {700, 500} + state.update = gameover_update + state.draw = gameover_draw + state.free = gameover_free + + state.previous = prev + tween_to(&state.position.y, WINDOWF.y / 2, 1, ease.Ease.Back_Out, state, gameover_ready) + + return state +} + +gameover_update :: proc(state: ^GameState, delta: f32) { + gameover := transmute(^GameOver)state + if gameover.ready_to_go { + if rl.IsKeyPressed(rl.KeyboardKey.ESCAPE) { + stack_pop() + stack_pop() + return + } + if rl.IsKeyPressed(rl.KeyboardKey.ENTER) { + stack_pop() + game := transmute(^Game)stack_top() + game_setup(game) + } + } +} + + +gameover_draw :: proc(state: ^GameState) { + gameover := transmute(^GameOver)state + + if state.previous != nil { + state.previous->draw() + } + + TitleFontSize :: 96 + TitleSpacing :: 3 + TitleText :: "ИГРА ОКОНЧЕНА" + TitleSize := rl.MeasureTextEx(FontTitle, TitleText, TitleFontSize, TitleSpacing) + + SubtitleText := [3]cstring{"Нажмите Enter", "чтобы начать сначала", "Или Escape для выхода"} + SubtitleFontSize :: 48 + SubtitleSpacing :: 2 + SubtitleSizes := [3]Vec2{} + for c, i in SubtitleText { + SubtitleSizes = rl.MeasureTextEx(FontUI, c, SubtitleFontSize, SubtitleSpacing) + } + + + rl.DrawRectangleV(gameover.position - gameover.size / 2, gameover.size, rl.Color{90, 30, 150, 255}) + + rl.DrawTextPro(FontTitle, TitleText, gameover.position - {0, 100}, TitleSize / 2, 0, TitleFontSize, TitleSpacing, rl.WHITE) + for c, i in SubtitleText { + rl.DrawTextPro(FontUI, c, gameover.position - {0, f32(10 - i * 50)}, SubtitleSizes[i] / 2, 0, SubtitleFontSize, SubtitleSpacing, rl.WHITE) + } + + +} + +gameover_free :: proc(state: ^GameState) { + free(state) +} + +gameover_ready :: proc(state: rawptr) { + gameover := transmute(^GameOver)state + gameover.ready_to_go = true +} diff --git a/howtoplay.odin b/howtoplay.odin new file mode 100644 index 0000000..621e335 --- /dev/null +++ b/howtoplay.odin @@ -0,0 +1,62 @@ +package main + +import rl "vendor:raylib" +import "core:math/ease" + + +Howtoplay :: struct { + using state: GameState, +} + +howtoplay_init :: proc(prev: ^GameState = nil) -> ^GameState { + state := new(Howtoplay) + state.variant = state + state.update = howtoplay_update + state.draw = howtoplay_draw + state.free = howtoplay_free + state.previous = prev + + return state +} + +howtoplay_update :: proc(state: ^GameState, delta: f32) { + howtoplay := transmute(^Howtoplay)state + if rl.IsKeyPressed(rl.KeyboardKey.ESCAPE) { + stack_pop() + } +} + + + + +howtoplay_draw :: proc(state: ^GameState) { + howtoplay := transmute(^Howtoplay)state + + TitleFontSize :: 96 + TitleSpacing :: 3 + TitleText :: "ARKADODGE" + TitleSize := rl.MeasureTextEx(FontTitle, TitleText, TitleFontSize, TitleSpacing) + rl.DrawTextPro(FontTitle, "ARKADODGE", {WINDOWF.x - 50, 50}, {TitleSize.x, 0}, 0, 96, 3, rl.WHITE) + + Text :: [?]cstring{ + "Используй Пробел для запуска шара", + "Стрелки для перемещения биты", + "Не дай шарику упасть вниз!", + "", + "Также блоки, падающие на биту, уменьшают её здоровье!" + } + + for t, y in Text { + rl.DrawTextPro(FontUI, t, {10, 100 + f32(y) * 38}, {0, 0}, 0, 32, 1, rl.WHITE) + } + + for c, t in brick_names { + y := cast(i32)t + rl.DrawRectangleV({WINDOWF.x / 2, 100 + f32(y) * 40}, {30, 20}, brick_colors[t]) + rl.DrawTextPro(FontUI, c, {WINDOWF.x / 2 + 40, 100 + f32(y) * 40}, {0, 14}, 0, 32, 2, rl.WHITE) + } +} + +howtoplay_free :: proc(state: ^GameState) { + free(state) +} diff --git a/leveldone.odin b/leveldone.odin new file mode 100644 index 0000000..169d0a7 --- /dev/null +++ b/leveldone.odin @@ -0,0 +1,79 @@ +package main + +import rl "vendor:raylib" +import "core:math/ease" + + +LevelDone :: struct { + using state: GameState, + position: Vec2, + size: Vec2, + ready_to_go: bool, +} + +leveldone_init :: proc(prev: ^GameState = nil) -> ^GameState { + state := new(LevelDone) + state.variant = state + state.position = Vec2{WINDOWF.x / 2, WINDOWF.y + 300} + state.size = {800, 400} + state.update = leveldone_update + state.draw = leveldone_draw + state.free = leveldone_free + + state.previous = prev + tween_to(&state.position.y, WINDOWF.y / 2, 1, ease.Ease.Back_Out, state, leveldone_ready) + + return state +} + +leveldone_update :: proc(state: ^GameState, delta: f32) { + leveldone := transmute(^LevelDone)state + if leveldone.ready_to_go { + if rl.IsKeyPressed(rl.KeyboardKey.ENTER) { + stack_pop() + game := transmute(^Game)stack_top() + game.levels_done += 1 + if game.levels_done > 0 && game.levels_done % 2 == 0 { + game.levelsize += {2, 1} + game.levelsize.x = min(game.levelsize.x, 13) + game.levelsize.y = min(game.levelsize.y, 8) + } + game_gen_level(game) + } + } +} + + +leveldone_draw :: proc(state: ^GameState) { + leveldone := transmute(^LevelDone)state + + if state.previous != nil { + state.previous->draw() + } + + TitleFontSize :: 96 + TitleSpacing :: 3 + TitleText :: "УРОВЕНЬ ПРОЙДЕН" + TitleSize := rl.MeasureTextEx(FontTitle, TitleText, TitleFontSize, TitleSpacing) + + SubtitleText :: "Нажмите Enter чтобы продолжить" + SubtitleFontSize :: 48 + SubtitleSpacing :: 2 + SubtitleSize := rl.MeasureTextEx(FontUI, SubtitleText, SubtitleFontSize, SubtitleSpacing) + + + rl.DrawRectangleV(leveldone.position - leveldone.size / 2, leveldone.size, rl.Color{90, 30, 150, 255}) + + rl.DrawTextPro(FontTitle, TitleText, leveldone.position - {0, 50}, TitleSize / 2, 0, TitleFontSize, TitleSpacing, rl.WHITE) + rl.DrawTextPro(FontUI, SubtitleText, leveldone.position + {0, 20}, SubtitleSize / 2, 0, SubtitleFontSize, SubtitleSpacing, rl.WHITE) + +} + +leveldone_free :: proc(state: ^GameState) { + free(state) +} + +leveldone_ready :: proc(state: rawptr) { + leveldone := transmute(^LevelDone)state + leveldone.ready_to_go = true +} diff --git a/main.odin b/main.odin index b7a15c1..610b43b 100644 --- a/main.odin +++ b/main.odin @@ -19,7 +19,7 @@ FontUI: rl.Font WindowShouldExit := false main :: proc() { - rl.SetConfigFlags(rl.ConfigFlags{.FULLSCREEN_MODE, .VSYNC_HINT, .WINDOW_MAXIMIZED, .WINDOW_UNDECORATED}) + rl.SetConfigFlags(rl.ConfigFlags{.FULLSCREEN_MODE, .VSYNC_HINT, }) monitor := rl.GetCurrentMonitor() rl.InitWindow(0, 0, "SinePong") rl.SetTargetFPS(9999) @@ -76,6 +76,7 @@ main :: proc() { } + /* TODO: @@ -83,7 +84,7 @@ main :: proc() { [ ] Проверка завершения уровня [ ] Генерация уровней [ ] (Опционально) редактор уровней с сохранением в файл -[ ] Меню +[x] Меню [ ] Гейм овер [ ] Бонусы [ ] Визуальное оформление diff --git a/menu.odin b/menu.odin index d2937db..4d9f2d8 100644 --- a/menu.odin +++ b/menu.odin @@ -7,14 +7,12 @@ import "core:math/ease" Menu_Buttons :: enum { START, HOW_TO_PLAY, - FULLSCREEN, EXIT } menu_strings := [Menu_Buttons]cstring { .START = "Старт", .HOW_TO_PLAY = "Как играть?", - .FULLSCREEN = "Полный экран", .EXIT = "Выход" } @@ -30,7 +28,7 @@ menu_init :: proc(prev: ^GameState = nil) -> ^GameState { state.variant = state state.list = MenuList(Menu_Buttons){ state = state, - position = {300, 300}, + position = {100, WINDOWF.y / 2}, line_size = 60, font_size = 48, elements = &menu_strings, @@ -47,6 +45,7 @@ menu_init :: proc(prev: ^GameState = nil) -> ^GameState { menu_update :: proc(state: ^GameState, delta: f32) { menu := transmute(^Menu)state + menu.list.position.y = WINDOWF.y / 2 menu_list_update(&menu.list) } @@ -56,8 +55,9 @@ menu_button_pressed :: proc(state: ^GameState, el: Menu_Buttons) { case .START: game := game_init(state) stack_push(game) - case .FULLSCREEN: case .HOW_TO_PLAY: + howtoplay := howtoplay_init(state) + stack_push(howtoplay) case .EXIT: WindowShouldExit = true return diff --git a/pad.odin b/pad.odin index 6d36c3c..092dffd 100644 --- a/pad.odin +++ b/pad.odin @@ -13,10 +13,12 @@ Pad :: struct { position: Vec2, velocity: Vec2, size: Vec2, + health: f32, } pad_update :: proc(pad: ^Pad, delta: f32) { + pad.health = min(pad.health + delta, 100) target : f32 = 0 if rl.IsKeyDown(rl.KeyboardKey.LEFT) { target = -PAD_MAX_SPEED @@ -53,4 +55,11 @@ pad_update :: proc(pad: ^Pad, delta: f32) { pad.position.x = GameField.x - pad_half_width pad.velocity.x = 0 } + +} + +pad_draw :: proc(pad: ^Pad) { + rl.DrawRectanglePro(rl.Rectangle{pad.position.x, pad.position.y, pad.size.x, pad.size.y}, pad.size / 2, 0, rl.RED) + hbar_x := pad.health / 100 * pad.size.x + rl.DrawRectanglePro(rl.Rectangle{pad.position.x, pad.position.y, hbar_x, pad.size.y/2}, {hbar_x / 2, pad.size.y / 4}, 0, rl.GREEN) } diff --git a/pause.odin b/pause.odin index 6fe7402..b8fcc61 100644 --- a/pause.odin +++ b/pause.odin @@ -6,13 +6,11 @@ import "core:math/ease" Pause_Buttons :: enum { RESUME, - FULLSCREEN, EXIT } pause_strings := [Pause_Buttons]cstring { .RESUME = "Продолжить", - .FULLSCREEN = "Полный экран", .EXIT = "Выход" } @@ -46,6 +44,10 @@ pause_init :: proc(prev: ^GameState = nil) -> ^GameState { pause_update :: proc(state: ^GameState, delta: f32) { menu := transmute(^Pause)state + if rl.IsKeyPressed(rl.KeyboardKey.ESCAPE) { + stack_pop() + return + } menu_list_update(&menu.list) } @@ -53,7 +55,6 @@ pause_button_pressed :: proc(state: ^GameState, el: Pause_Buttons) { switch el { case .RESUME: stack_pop() - case .FULLSCREEN: case .EXIT: stack_pop() stack_pop() @@ -77,5 +78,6 @@ pause_draw :: proc(state: ^GameState) { } pause_free :: proc(state: ^GameState) { + pause := transmute(^Pause)state free(state) } diff --git a/state.odin b/state.odin index 29cd5b5..cddda5f 100644 --- a/state.odin +++ b/state.odin @@ -2,7 +2,7 @@ package main import "core:slice" -StateVariant :: union{^Game, ^Menu, ^Pause} +StateVariant :: union{^Game, ^Menu, ^Pause, ^GameOver, ^LevelDone, ^Howtoplay} GameState :: struct { update: proc(state: ^GameState, delta: f32), @@ -46,3 +46,7 @@ stack_pop :: proc() -> (bool) { state->free() return true } + +stack_top :: proc() -> ^GameState { + return state_stack[len(state_stack)-1] +}