diff --git a/.gitignore b/.gitignore index f8eeddd..f2a58a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ sineus-final.exe +*.zip diff --git a/Ragnarokkr.exe b/Ragnarokkr.exe new file mode 100644 index 0000000..08993e4 Binary files /dev/null and b/Ragnarokkr.exe differ diff --git a/assets/gfx/crosshair.png b/assets/gfx/crosshair.png new file mode 100644 index 0000000..3fa0f8d Binary files /dev/null and b/assets/gfx/crosshair.png differ diff --git a/assets/models/snake_head_top.png b/assets/models/snake_head_top.png index 1783b31..30a39e1 100644 Binary files a/assets/models/snake_head_top.png and b/assets/models/snake_head_top.png differ diff --git a/game.odin b/game.odin index 1e15998..9e01f29 100644 --- a/game.odin +++ b/game.odin @@ -25,9 +25,35 @@ Game :: struct { snake_max_health: int, snake_health: int, music: rl.Music, + tutorial_enabled: bool, + tutorial_finished: bool, + tutorial_timer: f32, + tutorial_step: TutorSteps } +TutorSteps :: enum { + THRUST, + LOOK, + SHOOT, + DODGE, + DONE +} +TutorTextsMouse := [TutorSteps]cstring{ + .THRUST = "Используйте W/Ц для ускорения", + .LOOK = "Двигайте мышью для наведения", + .SHOOT = "Левая кнопка мыши: стрельба", + .DODGE = "Правая кнопка мыши: уклонение", + .DONE = "Удачи!" +} + +TutorTextsKeyboard := [TutorSteps]cstring{ + .THRUST = "Используйте стрелку \"Вверх\" для ускорения", + .LOOK = "Стрелки \"Влево\"/\"Вправо\" для наведения", + .SHOOT = "Пробел: стрельба", + .DODGE = "Shift: уклонение", + .DONE = "Удачи!" +} game_init :: proc(prev: ^GameState = nil) -> ^GameState { state := new(Game) @@ -61,9 +87,7 @@ game_setup :: proc(game: ^Game) { game.health = 100 game.snake_max_health = 0 - snake_spawn({10, 10, 0}, math.PI, 70) - - + snake_spawn({0, -10, 0}, math.PI * 3 / 2, 100) for segment in Segments { game.snake_max_health += int(segment.health) } @@ -79,6 +103,8 @@ game_setup :: proc(game: ^Game) { game.camera.target.x = clamp(game.camera.target.x, -GameField.x/2, GameField.x/2) game.camera.target.y = clamp(game.camera.target.y, 0, GameField.y) game.camera.position = game.camera.target + vec3backward * 50 + + game.tutorial_timer = 3 } @@ -89,6 +115,30 @@ game_gen_level :: proc(game: ^Game) { game_update :: proc(state: ^GameState, delta: f32) { game := transmute(^Game)state using game + tutorial_enabled = NeedTutorial && !tutorial_finished && player.intro_timer <= 0 + if tutorial_enabled { + tutorial_timer -= delta + if tutorial_timer <= 0 { + tutorial_timer = 3 + switch tutorial_step { + case .THRUST: tutorial_step = .LOOK + case .LOOK: tutorial_step = .SHOOT + case .SHOOT: tutorial_step = .DODGE + case .DODGE: tutorial_step = .DONE; tutorial_timer = 2 + case .DONE: + tutorial_finished = true + NeedTutorial = false + SnakeActive = true + rl.PlaySound(Res.Sfx.SnakeRoarBlast) + } + } + } else { + if player.intro_timer <= 0 && !SnakeActive { + SnakeActive = true + } + } + + // if rl.IsMusicStreamPlaying(game.music) { rl.UpdateMusicStream(game.music) // } @@ -153,8 +203,24 @@ game_draw :: proc(state: ^GameState) { // hb_width := hb_health * WSize.x // rl.DrawRectangleV({WSize.x / 2 - hb_width / 2, WSize.y - height - 7}, {hb_width, height + 4}, rl.RED) // draw_text_centered(Res.Fonts.Title, hb_text, {WSize.x / 2, WSize.y - height / 2}, height) - draw_hbar(hb_text, hb_health, {0, WSize.y - height}, {WSize.x, height}, rl.RED) + if SnakeActive { + draw_hbar(hb_text, hb_health, {0, WSize.y - height}, {WSize.x, height}, rl.RED) + } draw_hbar("Þórr", f32(health) / 100.0, {0, 0}, {WSize.x, height}, rl.GREEN) + reload_color := rl.SKYBLUE + if player.reloading { + reload_color = rl.RED + } + draw_hbar("", f32(player.power) / 100.0, {0, height}, {WSize.x, 10}, reload_color) + + if tutorial_enabled { + texts := TutorTextsMouse + if KeyboardOnly { + texts = TutorTextsKeyboard + } + draw_text_centered(Res.Fonts.UI, texts[tutorial_step], WSize / 2 + vec2{0, 100}, 48, 1, rl.WHITE) + } + } } diff --git a/gameover.odin b/gameover.odin index 1250df4..ce174d6 100644 --- a/gameover.odin +++ b/gameover.odin @@ -64,7 +64,7 @@ gameover_draw :: proc(state: ^GameState) { SubtitleFontSize :: 48 - rl.DrawRectangleV(gameover.position - gameover.size / 2, gameover.size, rl.Color{90, 30, 150, 10}) + // rl.DrawRectangleV(gameover.position - gameover.size / 2, gameover.size, rl.Color{90, 30, 150, 10}) draw_text_centered(Res.Fonts.Title, TitleText, gameover.position - {0, 100}, TitleFontSize, 1, rl.WHITE) for c, i in SubtitleText { diff --git a/main.odin b/main.odin index 5e7d7f8..aeccc1d 100644 --- a/main.odin +++ b/main.odin @@ -19,6 +19,9 @@ WSizei := [2]i32{} WindowShouldExit := false +NeedTutorial := true +KeyboardOnly := false + Overlay_Opacity : f32 = 0 @@ -58,7 +61,7 @@ Res : Resources load_sfx :: proc(name: string, volume: f32 = 1) -> rl.Sound { - p := filepath.join([]string{".\\assets\\sfx\\", name}) + p := filepath.join([]string{"./assets/sfx/", name}) cstr := strings.clone_to_cstring(p) snd := rl.LoadSound(cstr) rl.SetSoundVolume(snd, volume) @@ -66,7 +69,7 @@ load_sfx :: proc(name: string, volume: f32 = 1) -> rl.Sound { } load_music :: proc(name: string, volume: f32 = 1) -> rl.Music { - p := filepath.join([]string{".\\assets\\music\\", name}) + p := filepath.join([]string{"./assets/music/", name}) cstr := strings.clone_to_cstring(p) snd := rl.LoadMusicStream(cstr) rl.SetMusicVolume(snd, volume) @@ -81,14 +84,15 @@ change_track :: proc(music: rl.Music) { rl.PlayMusicStream(current_music) } +Cursor : rl.Texture load_resources :: proc() { - Res.Fonts.Title = rl.LoadFontEx(".\\assets\\fonts\\norse.otf", 96*2, nil, 2048) - Res.Fonts.UI = rl.LoadFontEx(".\\assets\\fonts\\PTSerif-Regular.ttf", 96, nil, 2048) + Res.Fonts.Title = rl.LoadFontEx("./assets/fonts/norse.otf", 96*2, nil, 2048) + Res.Fonts.UI = rl.LoadFontEx("./assets/fonts/PTSerif-Regular.ttf", 96, nil, 2048) - Res.Models.PlayerModel = rl.LoadModel(".\\assets\\models\\chariot.glb") - Res.Models.SnakeHeadTop = rl.LoadModel(".\\assets\\models\\snake_head_top.obj") - Res.Models.SnakeHeadJaw = rl.LoadModel(".\\assets\\models\\snake_jaw.obj") - Res.Models.SnakeBody = rl.LoadModel(".\\assets\\models\\snake_body.obj") + Res.Models.PlayerModel = rl.LoadModel("./assets/models/chariot.glb") + Res.Models.SnakeHeadTop = rl.LoadModel("./assets/models/snake_head_top.obj") + Res.Models.SnakeHeadJaw = rl.LoadModel("./assets/models/snake_jaw.obj") + Res.Models.SnakeBody = rl.LoadModel("./assets/models/snake_body.obj") Res.Sfx.Drums = load_sfx("drums.ogg") Res.Sfx.Lightning = load_sfx("lightning.ogg", 0.5) @@ -107,11 +111,17 @@ load_resources :: proc() { Res.Music.Second = load_music("alexander-nakarada-the-northern-path.mp3", 0.7) } +Fullscreen := true + main :: proc() { - rl.SetConfigFlags(rl.ConfigFlags{.MSAA_4X_HINT, .WINDOW_MAXIMIZED, .WINDOW_RESIZABLE}) - rl.InitWindow(800, 480, "Ragnarøkkr") + rl.SetConfigFlags(rl.ConfigFlags{.MSAA_4X_HINT, .FULLSCREEN_MODE, .VSYNC_HINT, .WINDOW_RESIZABLE}) + rl.SetWindowMinSize(800, 480) + + rl.InitWindow(0, 0, "Ragnarøkkr") rl.InitAudioDevice() + rl.HideCursor() + Cursor = rl.LoadTexture("./assets/gfx/crosshair.png") load_resources() WSizei = {rl.GetScreenWidth(), rl.GetScreenHeight()} @@ -141,6 +151,8 @@ main :: proc() { state->draw() rl.DrawRectangleV({}, WSize, rl.Color{0, 0, 0, u8(Overlay_Opacity * 255)}) + pos := rl.GetMousePosition() + rl.DrawTextureEx(Cursor, pos - {16, 16} * 3, 0, 3, rl.WHITE) rl.EndDrawing() } } diff --git a/menu.odin b/menu.odin index 64bcc29..4892876 100644 --- a/menu.odin +++ b/menu.odin @@ -6,16 +6,28 @@ import "core:math/ease" Menu_Buttons :: enum { START, - HOW_TO_PLAY, + TUTORIAL, + KEYBOARD_ONLY, + FULLSCREEN, EXIT } menu_strings := [Menu_Buttons]cstring { .START = "Старт", - .HOW_TO_PLAY = "Как играть?", + .TUTORIAL = "Обучение", + .KEYBOARD_ONLY = "Только клавиатура", + .FULLSCREEN = "Полный экран", .EXIT = "Выход" } +menu_items := [Menu_Buttons]MenuItem { + .START = {text = menu_strings[.START]}, + .TUTORIAL = {text = menu_strings[.TUTORIAL], param = &NeedTutorial, type = MenuItemType.BOOL}, + .KEYBOARD_ONLY = {text = menu_strings[.KEYBOARD_ONLY], param = &KeyboardOnly, type = MenuItemType.BOOL}, + .FULLSCREEN = {text = menu_strings[.FULLSCREEN], param = &Fullscreen, type = MenuItemType.BOOL}, + .EXIT = {text = menu_strings[.EXIT]} +} + Menu :: struct { using state: GameState, @@ -31,9 +43,9 @@ menu_init :: proc(prev: ^GameState = nil) -> ^GameState { position = {100, WSize.y / 2}, line_size = 60, font_size = 48, - elements = &menu_strings, + elements = &menu_items, menu_pressed = menu_button_pressed, - background = rl.Color{50, 10, 110, 10} + background = rl.Color{50, 10, 110, 0} } state.update = menu_update state.draw = menu_draw @@ -56,9 +68,15 @@ menu_button_pressed :: proc(state: ^GameState, el: Menu_Buttons) { game := transmute(^Game)state.previous change_track(Res.Music.First) stack_pop() - case .HOW_TO_PLAY: - // howtoplay := howtoplay_init(state) - // stack_push(howtoplay) + case .TUTORIAL: + NeedTutorial = !NeedTutorial + case .KEYBOARD_ONLY: + KeyboardOnly = !KeyboardOnly + NeedTutorial = true + case .FULLSCREEN: + rl.ToggleFullscreen() + Fullscreen = rl.IsWindowFullscreen() + case .EXIT: WindowShouldExit = true return diff --git a/menu_list.odin b/menu_list.odin index 3eddefe..31f37d8 100644 --- a/menu_list.odin +++ b/menu_list.odin @@ -4,6 +4,23 @@ import rl "vendor:raylib" import "core:math/ease" import "core:fmt" +MenuItemType :: enum { + NONE, + BOOL, + INT, +} + +BoolStrings := map[bool]cstring { + true = "вкл", + false = "выкл" +} + +MenuItem :: struct{ + text: cstring, + param: rawptr, + type: MenuItemType, +} + MenuList :: struct($T: typeid) { state: ^GameState, position: vec2, @@ -12,7 +29,7 @@ MenuList :: struct($T: typeid) { active_element: T, active_marker: vec2, tween: ^Tween, - elements: ^[T]cstring, + elements: ^[T]MenuItem, menu_pressed: proc(state: ^GameState, element: T), background: rl.Color, mouse_pos: vec2, @@ -80,7 +97,13 @@ menu_list_draw :: proc(list: ^MenuList($T)) { rl.DrawTextEx(Res.Fonts.UI, ">", list.position + list.active_marker + {-30, 0}, 48, 2, rl.WHITE) for el, i in list.elements { pos := list.position + {0, f32(i) * list.line_size} - rl.DrawTextEx(Res.Fonts.UI, el, pos, list.font_size, 2, rl.WHITE) + text := el.text + if el.type == .BOOL { + param := transmute(^bool)el.param + value := param^ + text = rl.TextFormat("%s: %s", el.text, BoolStrings[value]) + } + rl.DrawTextEx(Res.Fonts.UI, text, pos, list.font_size, 2, rl.WHITE) } } @@ -88,7 +111,7 @@ menu_list_draw :: proc(list: ^MenuList($T)) { menu_list_get_size :: proc(list: ^MenuList($T)) -> vec2 { size := vec2{} for el, i in list.elements { - line_size := rl.MeasureTextEx(Res.Fonts.UI, el, list.font_size, 2) + line_size := rl.MeasureTextEx(Res.Fonts.UI, el.text, list.font_size, 2) if line_size.x > size.x { size.x = line_size.x } diff --git a/pause.odin b/pause.odin index b0981ef..8ffbd6f 100644 --- a/pause.odin +++ b/pause.odin @@ -14,6 +14,11 @@ pause_strings := [Pause_Buttons]cstring { .EXIT = "Прервать игру" } +pause_items := [Pause_Buttons]MenuItem { + .CONTINUE = {text = pause_strings[.CONTINUE]}, + .EXIT = {text = pause_strings[.EXIT]} +} + Pause :: struct { using state: GameState, @@ -30,9 +35,9 @@ pause_init :: proc(prev: ^GameState = nil) -> ^GameState { position = {-300, WSize.y / 2}, line_size = 60, font_size = 48, - elements = &pause_strings, + elements = &pause_items, menu_pressed = pause_button_pressed, - background = rl.Color{50, 10, 110, 10} + background = rl.Color{50, 10, 110, 0} } state.update = pause_update state.draw = pause_draw diff --git a/player.odin b/player.odin index d7572e1..f3efb6a 100644 --- a/player.odin +++ b/player.odin @@ -29,6 +29,9 @@ Player :: struct { is_invulnerable: bool, is_dead: bool, intro_timer: f32, + power: f32, + reload_timer: f32, + reloading: bool, // animation: rl.ModelAnimation, // animTime: f32, // animFrame: i32, @@ -47,6 +50,7 @@ player_spawn :: proc(position: vec3) -> Player { can_dodge = true, can_shoot = true, intro_timer = 2, + power = 100, } } @@ -82,16 +86,26 @@ player_update :: proc(player: ^Player, game: ^Game, delta: f32) { if !is_dodging { dir_vector : vec3 if intro_timer <= 0 { - dir = angle_rotate(dir, mouse_angle, math.PI * 2 * delta) - dir_vector = get_vec_from_angle(dir) - + thrust_key := rl.KeyboardKey.W + if !KeyboardOnly { + dir = angle_rotate(dir, mouse_angle, math.PI * 2 * delta) + dir_vector = get_vec_from_angle(dir) + } else { + thrust_key = rl.KeyboardKey.UP + if rl.IsKeyDown(rl.KeyboardKey.LEFT) { + dir += math.PI * 2 * delta + } + if rl.IsKeyDown(rl.KeyboardKey.RIGHT) { + dir -= math.PI * 2 * delta + } + dir_vector = get_vec_from_angle(dir) + } thrust = 0 - - if rl.IsKeyDown(rl.KeyboardKey.W) { - thrust = 70 + if rl.IsKeyDown(thrust_key) { + thrust = 110 } } else { - thrust = 70 + thrust = 110 dir_vector = vec3left dir = math.atan2(-dir_vector.y, dir_vector.x) } @@ -115,7 +129,17 @@ player_update :: proc(player: ^Player, game: ^Game, delta: f32) { } } - if rl.IsMouseButtonPressed(rl.MouseButton.RIGHT) && can_dodge && intro_timer <= 0 { + dodge := false + shoot := false + if KeyboardOnly { + dodge = rl.IsKeyPressed(rl.KeyboardKey.LEFT_SHIFT) + shoot = rl.IsKeyDown(rl.KeyboardKey.SPACE) + } else { + dodge = rl.IsMouseButtonPressed(rl.MouseButton.RIGHT) + shoot = rl.IsMouseButtonDown(rl.MouseButton.LEFT) + } + + if dodge && can_dodge && intro_timer <= 0 { is_dodging = true can_dodge = false rl.StopSound(Res.Sfx.Rocket) @@ -136,7 +160,7 @@ player_update :: proc(player: ^Player, game: ^Game, delta: f32) { // player.animTime += delta // player.animFrame = i32(player.animTime * 60) % player.animation.frameCount // rl.UpdateModelAnimation(PlayerModel, player.animation, player.animFrame) - shooting := rl.IsMouseButtonDown(rl.MouseButton.LEFT) && !is_dodging && intro_timer <= 0 + shooting := shoot && !is_dodging && intro_timer <= 0 && !reloading if shooting { if !rl.IsSoundPlaying(Res.Sfx.Lightning) { rl.PlaySound(Res.Sfx.Lightning) @@ -150,10 +174,26 @@ player_update :: proc(player: ^Player, game: ^Game, delta: f32) { player := transmute(^Player)data player.can_shoot = true }) + player.power -= 2 + player.reload_timer = 1 + if player.power <= 0 { + player.reloading = true + player.power = 0 + } } } else { rl.StopSound(Res.Sfx.Lightning) } + if !shooting && can_shoot { + reload_timer -= delta + if reload_timer <= 0 { + player.power += 20 * delta + if player.power > 100 { + player.power = 100 + player.reloading = false + } + } + } } got_hit := false diff --git a/snake.odin b/snake.odin index 06a3e56..e4229a2 100644 --- a/snake.odin +++ b/snake.odin @@ -52,19 +52,22 @@ SnakeSegment :: struct { prev: ^SnakeSegment } +SnakeActive := false snake_spawn :: proc(pos: vec3, dir: f32, length: int){ - dir_vec := rl.Vector3RotateByAxisAngle(vec3right, vec3backward, dir) - + // dir_vec := rl.Vector3RotateByAxisAngle(vec3right, vec3backward, dir) + dir_vec := vec3down + direction := math.atan2(-dir_vec.y, dir_vec.x) + SnakeActive = false Head = SnakeHead{ pos = pos, dir = dir, radius = 3, - state = .Chasing, + state = .Diving, vel = dir_vec * 20, health = 100, max_health = 100, - state_timer = 30, + state_timer = 15, } for i := 0; i < length; i += 1 { @@ -93,6 +96,9 @@ snake_clear :: proc() { } snake_process :: proc(game: ^Game, delta: f32) { + if !SnakeActive || Head.is_dead{ + return + } // Head.state = .Shot // Head.state_timer = 200 switch Head.state { @@ -124,6 +130,7 @@ snake_process :: proc(game: ^Game, delta: f32) { if Head.health <= 0 && !Head.is_dead { Head.is_dead = true explode(Head.pos, 9, 0.9, rl.YELLOW) + rl.StopMusicStream(current_music) rl.PlaySound(Res.Sfx.PlayerDead) timer_start(3, game, proc(data: rawptr) { state := transmute(^Game)data @@ -274,7 +281,9 @@ snake_shot :: proc(game: ^Game, delta: f32) { snake_draw :: proc(game: ^Game) { - + if !SnakeActive || Head.is_dead { + return + } dir_vector := get_vec_from_angle(Head.dir) roll := -math.PI / 2 + math.cos(Head.dir) * math.PI / 2 rlgl.PushMatrix()