Final version. Fixed menus, music. Added tutorial and control schemes

This commit is contained in:
Vlad Rud 2024-10-06 10:59:07 +03:00
parent 061f255be4
commit 83ca097f8d
12 changed files with 215 additions and 41 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
sineus-final.exe
*.zip

BIN
Ragnarokkr.exe Normal file

Binary file not shown.

BIN
assets/gfx/crosshair.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 940 B

View File

@ -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)
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)
}
}
}

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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

View File

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

View File

@ -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

View File

@ -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 {
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

View File

@ -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()