diff --git a/app/classes/scene_instance.rb b/app/classes/scene_instance.rb new file mode 100644 index 0000000..c04797d --- /dev/null +++ b/app/classes/scene_instance.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# class to represent scenes +class SceneInstance + def initialize(_args, opts = {}) + @tick_in_background = opts.tick_in_background._? false + end + + attr_reader :tick_in_background + + # called every tick of the game loop + def tick(args) end + + # custom logic to reset this scene + def reset(args) end +end diff --git a/app/main.rb b/app/main.rb index 5e2cff8..453c24a 100644 --- a/app/main.rb +++ b/app/main.rb @@ -6,6 +6,7 @@ require 'lib/coalesce.rb' # then, some basic classes required for lists of assets require 'app/classes/sprite_instance.rb' require 'app/classes/sound_instance.rb' +require 'app/classes/scene_instance.rb' # then, asset lists require 'sprites/_list.rb' @@ -23,18 +24,20 @@ require 'app/util/util.rb' require 'app/util/input.rb' require 'app/constants.rb' -require 'app/menu.rb' -require 'app/scene.rb' require 'app/game_setting.rb' require 'app/text.rb' # then, the scenes +require 'app/scenes/menu.rb' require 'app/scenes/gameplay.rb' require 'app/scenes/main_menu.rb' require 'app/scenes/paused.rb' require 'app/scenes/settings.rb' require 'app/scenes/cube_tube.rb' +require 'app/scenes/_list.rb' +require 'app/util/scene.rb' + # finally, the main tick # NOTE: add all other requires above this require 'app/tick.rb' diff --git a/app/menu.rb b/app/menu.rb deleted file mode 100644 index 1a60961..0000000 --- a/app/menu.rb +++ /dev/null @@ -1,113 +0,0 @@ -module Menu - class << self - # Updates and renders a list of options that get passed through. - # - # +options+ data structure: - # [ - # { - # text: "some string", - # on_select: -> (args) { "do some stuff in this lambda" } - # } - # ] - def tick(args, state_key, options, menu_y: 420) - args.state.send(state_key).current_option_i ||= 0 - args.state.send(state_key).hold_delay ||= 0 - menu_state = args.state.send(state_key) - - labels = [] - - spacer = mobile? ? 100 : 60 - options.each.with_index do |option, i| - text = case option.kind - when :toggle - "#{text(option[:key])}: #{text_for_setting_val(option[:setting_val])}" - else - text(option[:key]) - end - - label = label( - text, - x: args.grid.w / 2, - y: menu_y + (options.length - i * spacer), - align: ALIGN_CENTER, - size: SIZE_MD - ) - label.key = option[:key] - label_size = args.gtk.calcstringbox(label.text, label.size_enum) - labels << label - if menu_state.current_option_i == i - if !mobile? || (mobile? && args.inputs.controller_one.connected) - args.outputs.solids << { - x: label.x - (label_size[0] / 1.4) - 24 + (Math.sin(args.state.tick_count / 8) * 4), - y: label.y - 22, - w: 16, - h: 16, - }.merge(WHITE) - end - end - end - - labels.each do |l| - button_border = { w: 340, h: 80, x: l.x - 170, y: l.y - 55 }.merge(WHITE) - if mobile? - args.outputs.borders << button_border - end - - if args.inputs.mouse.up && args.inputs.mouse.inside_rect?(button_border) - o = options.find { |o| o[:key] == l[:key] } - Sound.play(args, :menu) - o[:on_select].call(args) if o - end - end - - args.outputs.labels << labels - - move = nil - if args.inputs.down - move = :down - elsif args.inputs.up - move = :up - else - menu_state.hold_delay = 0 - end - - if move - menu_state.hold_delay -= 1 - - if menu_state.hold_delay <= 0 - Sound.play(args, :menu) - index = menu_state.current_option_i - if move == :up - index -= 1 - else - index += 1 - end - - if index < 0 - index = options.length - 1 - elsif index > options.length - 1 - index = 0 - end - menu_state.current_option_i = index - menu_state.hold_delay = 10 - end - end - - if Input.pressed?(args, :primary) - Sound.play(args, :select) - options[menu_state.current_option_i][:on_select].call(args) - end - end - - def text_for_setting_val(val) - case val - when true - text(:on) - when false - text(:off) - else - val - end - end - end -end diff --git a/app/scenes/_list.rb b/app/scenes/_list.rb new file mode 100644 index 0000000..05b860c --- /dev/null +++ b/app/scenes/_list.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Scene + SCENES = { + DEFAULT: MainMenu, + main_menu: MainMenu, + settings: SettingsMenu, + #paused: PauseMenu, + #cube_tube: CubeTube + } +end diff --git a/app/scenes/cube_tube.rb b/app/scenes/cube_tube.rb index a385249..cc0ae9b 100644 --- a/app/scenes/cube_tube.rb +++ b/app/scenes/cube_tube.rb @@ -89,6 +89,7 @@ class CubeTubeGame @current_music = :music1 Music.play(@args, @current_music) + Music.set_volume(args, args.state.setting.music ? 0.8 : 0.0) end def render_grid_border x, y, w, h, color diff --git a/app/scenes/main_menu.rb b/app/scenes/main_menu.rb index 2fc0423..93a7683 100644 --- a/app/scenes/main_menu.rb +++ b/app/scenes/main_menu.rb @@ -1,50 +1,64 @@ -module Scene - class << self - # what's displayed when your game starts - def tick_main_menu(args) - draw_bg(args, DARK_PURPLE) - options = [ - { - key: :start, - on_select: -> (args) { Scene.switch(args, :cube_tube, reset: true) } - }, - { - key: :settings, - on_select: -> (args) { Scene.push(args, :settings, reset: true) } - }, - ] +# frozen_string_literal: true - if args.gtk.platform?(:desktop) - options << { - key: :quit, - on_select: -> (args) { args.gtk.request_quit } - } - end +# This is the first screen in this game, allowing access to everything else +class MainMenu < MenuScene + def initialize(args, opts = {}) + # these are the menu options for the main menu + menu_options = [ + { + key: :start, + on_select: ->(iargs) { Scene.switch(iargs, :cube_tube, reset: true) } + }, + { + key: :settings, + on_select: ->(iargs) { Scene.push(iargs, :settings, reset: true) } + } + ] - Menu.tick(args, :main_menu, options) - - labels = [] - labels << label( - "v#{version}", - x: 32.from_left, y: 32.from_top, - size: SIZE_XS, align: ALIGN_LEFT) - labels << label( - title.upcase, x: args.grid.w / 2, y: args.grid.top - 100, - size: SIZE_LG, align: ALIGN_CENTER, font: FONT_BOLD_ITALIC) - labels << label( - "#{text(:made_by)} #{dev_title}", - x: args.grid.left + 24, y: 48, - size: SIZE_XS, align: ALIGN_LEFT) - labels << label( - :controls_title, - x: args.grid.right - 24, y: 84, - size: SIZE_SM, align: ALIGN_RIGHT) - labels << label( - args.inputs.controller_one.connected ? :controls_gamepad : :controls_keyboard, - x: args.grid.right - 24, y: 48, - size: SIZE_XS, align: ALIGN_RIGHT) - - args.outputs.labels << labels + if args.gtk.platform?(:desktop) + menu_options << { + key: :quit, + on_select: ->(iargs) { iargs.gtk.request_quit } + } end + + super args, opts, menu_options + end + + # called every tick of the game loop + def tick(args) + draw_bg(args, DARK_PURPLE) + + # actual menu logic is handled by the MenuScene super class + super + + # additionally draw some labels with information about the game + labels = [] + labels << label( + "v#{version}", + x: 32.from_left, y: 32.from_top, + size: SIZE_XS, align: ALIGN_LEFT + ) + labels << label( + title.upcase, x: args.grid.w / 2, y: args.grid.top - 100, + size: SIZE_LG, align: ALIGN_CENTER, font: FONT_BOLD_ITALIC + ) + labels << label( + "#{text(:made_by)} #{dev_title}", + x: args.grid.left + 24, y: 48, + size: SIZE_XS, align: ALIGN_LEFT + ) + labels << label( + :controls_title, + x: args.grid.right - 24, y: 84, + size: SIZE_SM, align: ALIGN_RIGHT + ) + labels << label( + args.inputs.controller_one.connected ? :controls_gamepad : :controls_keyboard, + x: args.grid.right - 24, y: 48, + size: SIZE_XS, align: ALIGN_RIGHT + ) + + args.outputs.labels << labels end end diff --git a/app/scenes/menu.rb b/app/scenes/menu.rb new file mode 100644 index 0000000..64921d0 --- /dev/null +++ b/app/scenes/menu.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +# A scene which updates and renders a list of options that get passed through. +# +# +menu_options+ data structure: +# [ +# { +# text: 'some string', +# on_select: -> (args) { 'do some stuff in this lambda' } +# } +# ] +class MenuScene < SceneInstance + def initialize(args, opts = {}, menu_options = []) + super args, opts + + @menu_state ||= { + current_option_i: 0, + hold_delay: 0 + } + @spacer ||= mobile? ? 100 : 60 + @menu_options ||= menu_options + @menu_y = opts.menu_y._?(420) + end + + def render_options(args) + labels = [] + @menu_options.each.with_index do |option, i| + text = case option.kind + when :toggle + "#{text(option[:key])}: #{text_for_setting_val(args, option[:key])}" + else + text(option[:key]) + end + + l = label( + text, + x: args.grid.w / 2, + y: @menu_y + (@menu_options.length - (i * @spacer)), + align: ALIGN_CENTER, + size: SIZE_MD + ) + l.key = option[:key] + l.width, l.height = args.gtk.calcstringbox(l.text, l.size_enum) + labels << l + + if @menu_state.current_option_i == i && (!mobile? || (mobile? && args.inputs.controller_one.connected)) + args.outputs.solids << { + x: l.x - (l.width / 1.4) - 24 + (Math.sin(args.state.tick_count / 8) * 4), + y: l.y - 22, + w: 16, + h: 16 + }.merge(WHITE) + end + + button_border = { w: 340, h: 80, x: l.x - 170, y: l.y - 55 }.merge(WHITE) + (args.outputs.borders << button_border) if mobile? + if args.inputs.mouse.up && args.inputs.mouse.inside_rect?(button_border) + o = options.find { |o| o[:key] == l[:key] } + Sound.play(args, :menu) + o[:on_select].call(args) if o + end + end + + args.outputs.labels << labels + end + + # called every tick of the game loop + def tick(args) + super + + render_options(args) + + move = nil + if Input.down?(args) + move = :down + elsif Input.up?(args) + move = :up + else + @menu_state.hold_delay = 0 + end + + if move + @menu_state.hold_delay -= 1 + + if @menu_state.hold_delay <= 0 + Sound.play(args, :menu) + index = @menu_state.current_option_i + if move == :up + index -= 1 + else + index += 1 + end + + if index.negative? + index = @menu_options.length - 1 + elsif index > @menu_options.length - 1 + index = 0 + end + @menu_state.current_option_i = index + @menu_state.hold_delay = 10 + end + end + + if Input.pressed?(args, :primary) + @menu_options[@menu_state.current_option_i][:on_select].call(args) + Sound.play(args, :select) + end + end + + # custom logic to reset this scene + def reset(args) + super + + @menu_state.current_option_i = 0 + @menu_state.hold_delay = 0 + end + + def text_for_setting_val(args, key) + val = args.state.setting[key] + case val + when true + text(:on) + when false + text(:off) + else + val + end + end +end diff --git a/app/scenes/settings.rb b/app/scenes/settings.rb index a84d9d4..75be6b5 100644 --- a/app/scenes/settings.rb +++ b/app/scenes/settings.rb @@ -1,60 +1,74 @@ -module Scene - class << self - # reachable via main menu or pause menu, allows for configuring the game - # for the player's preferences. - def tick_settings(args) - draw_bg(args, DARK_GREEN) +# frozen_string_literal: true - options = [ - { - key: :sfx, - kind: :toggle, - setting_val: args.state.setting.sfx, - on_select: -> (args) do - GameSetting.save_after(args) do |args| - args.state.setting.sfx = !args.state.setting.sfx - end +# This is the settings menu, allowing for various settings to be changed +class SettingsMenu < MenuScene + def initialize(args, opts = {}) + menu_options = [ + { + key: :sfx, + kind: :toggle, + on_select: ->(args) do + puts 'toggle sfx' + GameSetting.save_after(args) do |args| + args.state.setting.sfx = !args.state.setting.sfx + puts "sfx = #{args.state.setting.sfx}" end - }, - { - key: :music, - kind: :toggle, - setting_val: args.state.setting.music, - on_select: -> (args) do - GameSetting.save_after(args) do |args| - args.state.setting.music = !args.state.setting.music - Music.set_volume(args, args.state.setting.music ? 0.8 : 0.0) - end + end + }, + { + key: :music, + kind: :toggle, + on_select: ->(args) do + GameSetting.save_after(args) do |args| + args.state.setting.music = !args.state.setting.music + Music.set_volume(args, args.state.setting.music ? 0.8 : 0.0) end - }, - { - key: :back, - on_select: -> (args) { Scene.pop(args) } - }, - ] + end + }, + { + key: :back, + on_select: ->(iargs) { Scene.pop(iargs) } + } + ] - if args.gtk.platform?(:desktop) - options.insert(options.length - 1, { - key: :fullscreen, - kind: :toggle, - setting_val: args.state.setting.fullscreen, - on_select: -> (args) do + if args.gtk.platform?(:desktop) + menu_options.insert( + menu_options.length - 1, + { + key: :fullscreen, + kind: :toggle, + on_select: ->(args) do GameSetting.save_after(args) do |args| args.state.setting.fullscreen = !args.state.setting.fullscreen args.gtk.set_window_fullscreen(args.state.setting.fullscreen) end end - }) - end - - Menu.tick(args, :settings, options) - - if Input.pressed?(args, :secondary) - Sound.play(args, :select) - options.find { |o| o[:key] == :back }[:on_select].call(args) - end - - args.outputs.labels << label(:settings, x: args.grid.w / 2, y: args.grid.top - 200, align: ALIGN_CENTER, size: SIZE_LG, font: FONT_BOLD) + } + ) end + + super args, opts, menu_options + end + + # called every tick of the game loop + def tick(args) + draw_bg(args, DARK_GREEN) + + # actual menu logic is handled by the MenuScene super class + super + + if Input.pressed?(args, :secondary) + Sound.play(args, :select) + @menu_options.find { |o| o[:key] == :back }[:on_select].call(args) + end + + args.outputs.labels << label( + :settings, + x: args.grid.w / 2, + y: args.grid.top - 200, + align: ALIGN_CENTER, + size: SIZE_LG, + font: FONT_BOLD + ) end end diff --git a/app/tick.rb b/app/tick.rb index 44a112f..56b8deb 100644 --- a/app/tick.rb +++ b/app/tick.rb @@ -1,22 +1,28 @@ +# frozen_string_literal: true + # Code that only gets run once on game start def init(args) Input.reset_swipe(args) GameSetting.load_settings(args) end +# Code that runs every game tick (mainly just calling other ticks) def tick(args) - init(args) if args.state.tick_count == 0 - + init(args) if args.state.tick_count.zero? # this looks good on non 16:9 resolutions; game background is different args.outputs.background_color = TRUE_BLACK.values args.state.has_focus ||= true - Scene.push(args, :main_menu, reset: true) if !args.state.scene + args.state.scene_stack ||= [] + Scene.push(args, Scene.default(args), reset: true) if args.state.scene_stack.empty? Input.track_swipe(args) if mobile? - Scene.send("tick_#{args.state.scene}", args) - + # Scene.send("tick_#{args.state.scene}", args) + args.state.scene_stack.each do |scene| + scene.tick(args) if scene.tick_in_background || scene == args.state.scene_stack.last + end + Music.tick(args) debug_tick(args) @@ -45,7 +51,7 @@ def debug_tick(args) if args.inputs.keyboard.key_down.i Sound.play(args, :select) Sprite.reset_all(args) - args.gtk.notify!("Sprites reloaded") + args.gtk.notify!('Sprites reloaded') end if args.inputs.keyboard.key_down.r @@ -57,9 +63,9 @@ def debug_tick(args) Sound.play(args, :select) args.state.simulate_mobile = !args.state.simulate_mobile msg = if args.state.simulate_mobile - "Mobile simulation on" + 'Mobile simulation on' else - "Mobile simulation off" + 'Mobile simulation off' end args.gtk.notify!(msg) end diff --git a/app/util/music.rb b/app/util/music.rb index 8a2c297..c52d09b 100644 --- a/app/util/music.rb +++ b/app/util/music.rb @@ -26,7 +26,7 @@ module Music end def paused(args, channel = 0) - args.audio["MUSIC_CHANNEL_#{channel}"].paused + args.audio["MUSIC_CHANNEL_#{channel}"].paused unless stopped(args, channel) end def stop(args, channel = 0) @@ -34,15 +34,15 @@ module Music end def pause(args, channel = 0) - args.audio["MUSIC_CHANNEL_#{channel}"].paused = true + args.audio["MUSIC_CHANNEL_#{channel}"].paused = true unless stopped(args, channel) end def resume(args, channel = 0) - args.audio["MUSIC_CHANNEL_#{channel}"].paused = false + args.audio["MUSIC_CHANNEL_#{channel}"].paused = false unless stopped(args, channel) end def set_volume(args, volume, channel = 0) - args.audio["MUSIC_CHANNEL_#{channel}"].gain = volume + args.audio["MUSIC_CHANNEL_#{channel}"].gain = volume unless stopped(args, channel) end def tick(args) diff --git a/app/scene.rb b/app/util/scene.rb similarity index 59% rename from app/scene.rb rename to app/util/scene.rb index 60e5c9b..48baf45 100644 --- a/app/scene.rb +++ b/app/util/scene.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # A scene represents a discreet state of gameplay. Things like the main menu, # game over screen, and gameplay. # -# Define a new scene by adding one to `app/scenes/` and defining a -# `Scene.tick_SCENE_NAME` class method. +# Define a new scene by adding one to `app/scenes/` and inheriting from +# SceneInstance # # The main `#tick` of the game handles delegating to the current scene based on # the `args.state.scene` value, which is a symbol of the current scene, ex: @@ -14,17 +16,12 @@ module Scene # ex: # Scene.switch(args, :gameplay) def switch(args, scene, reset: false, push_or_pop: false) + args.state.scene_stack ||= [] # if we're here /not/ from push or pop, clear the scene stack args.state.scene_stack.clear unless push_or_pop + args.state.scene_stack.push(scene) if args.state.scene_stack.empty? - if reset - args.state.send(scene)&.current_option_i = nil - args.state.send(scene)&.hold_delay = nil - - # you can also add custom reset logic as-needed for specific scenes - # here - args.state.game.reset_game if args.state.game - end + scene.reset(args) if reset args.state.scene = scene raise FinishTick, 'finish tick early' @@ -32,17 +29,23 @@ module Scene # Change the current scene and push the previous scene onto the stack def push(args, scene, reset: false) - puts "Pushing #{scene}" args.state.scene_stack ||= [] - args.state.scene_stack.push(args.state.scene) + the_scene = scene.is_a?(SceneInstance) ? scene : SCENES[scene].new(args) + args.state.scene_stack.push(the_scene) - switch(args, scene, reset: reset, push_or_pop: true) + switch(args, the_scene, reset: reset, push_or_pop: true) end # Return to the previous scene on the stack def pop(args, reset: false) - scene = !args.state.scene_stack || args.state.scene_stack.empty? ? :back : args.state.scene_stack.pop - switch(args, scene, reset: reset, push_or_pop: true) + scene = args.state.scene_stack&.pop + + switch(args, scene._?(default(args)), reset: reset, push_or_pop: true) + end + + def default(args) + args.state.scene_stack ||= [] + SCENES[:DEFAULT].new(args) end end end diff --git a/app/util/sound.rb b/app/util/sound.rb index 6794a35..11dbc8d 100644 --- a/app/util/sound.rb +++ b/app/util/sound.rb @@ -8,7 +8,7 @@ module Sound end def play(args, key, opts = {}) - SOUNDS.fetch(key).play(args, opts) + SOUNDS.fetch(key).play(args, opts) if args.state.setting.sfx end def stop(args, key)