fet 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +2 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +9 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +43 -6
- data/assets/fonts/PTSans/OFL.txt +94 -0
- data/assets/fonts/PTSans/PTSans-Bold.ttf +0 -0
- data/assets/fonts/PTSans/PTSans-BoldItalic.ttf +0 -0
- data/assets/fonts/PTSans/PTSans-Italic.ttf +0 -0
- data/assets/fonts/PTSans/PTSans-Regular.ttf +0 -0
- data/assets/readme/demo.gif +0 -0
- data/bin/fet +32 -5
- data/fet.gemspec +1 -0
- data/fet.rdoc +38 -0
- data/lib/fet/cli/play/listening.rb +19 -0
- data/lib/fet/degree.rb +4 -0
- data/lib/fet/degrees.rb +19 -2
- data/lib/fet/exceptions.rb +4 -0
- data/lib/fet/generator/listening.rb +21 -24
- data/lib/fet/midi_file_generator.rb +48 -0
- data/lib/fet/midilib_interface.rb +2 -26
- data/lib/fet/score.rb +52 -0
- data/lib/fet/ui/color_scheme.rb +13 -0
- data/lib/fet/ui/custom_event.rb +45 -0
- data/lib/fet/ui/game.rb +87 -0
- data/lib/fet/ui/game_loop_handler.rb +74 -0
- data/lib/fet/ui/game_setup_helper.rb +49 -0
- data/lib/fet/ui/key.rb +44 -0
- data/lib/fet/ui/level.rb +108 -0
- data/lib/fet/ui/level_loop_handler.rb +57 -0
- data/lib/fet/ui/note_box.rb +117 -0
- data/lib/fet/ui/note_box_loop_handler.rb +107 -0
- data/lib/fet/ui/note_boxes.rb +81 -0
- data/lib/fet/ui/score.rb +85 -0
- data/lib/fet/ui/timer.rb +81 -0
- data/lib/fet/version.rb +1 -1
- data/lib/fet.rb +3 -0
- metadata +38 -3
- data/LICENSE.txt +0 -21
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "color_scheme"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Fet
         | 
| 6 | 
            +
              module Ui
         | 
| 7 | 
            +
                # Handles setting up the game before starting
         | 
| 8 | 
            +
                module GameSetupHelper
         | 
| 9 | 
            +
                  private
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def setup_window
         | 
| 12 | 
            +
                    setup_window_title
         | 
| 13 | 
            +
                    setup_window_background
         | 
| 14 | 
            +
                    setup_window_event_loop
         | 
| 15 | 
            +
                    setup_window_update_loop
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def setup_window_title
         | 
| 19 | 
            +
                    Ruby2D::Window.set(title: "FET")
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def setup_window_background
         | 
| 23 | 
            +
                    Ruby2D::Window.set(background: Fet::Ui::ColorScheme::BLACK)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # NOTE: don't test coverage for these methods because this is more of a test of the Ruby2D library
         | 
| 27 | 
            +
                  # :nocov:
         | 
| 28 | 
            +
                  def setup_window_event_loop
         | 
| 29 | 
            +
                    Ruby2D::Window.on(:key_down) do |event|
         | 
| 30 | 
            +
                      handle_event_loop(event)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    Ruby2D::Window.on(:mouse_down) do |event|
         | 
| 34 | 
            +
                      handle_event_loop(event)
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  # :nocov:
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  # NOTE: don't test coverage for these methods because this is more of a test of the Ruby2D library
         | 
| 40 | 
            +
                  # :nocov:
         | 
| 41 | 
            +
                  def setup_window_update_loop
         | 
| 42 | 
            +
                    Ruby2D::Window.update do
         | 
| 43 | 
            +
                      handle_update_loop
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                  # :nocov:
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
    
        data/lib/fet/ui/key.rb
    ADDED
    
    | @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Fet
         | 
| 4 | 
            +
              module Ui
         | 
| 5 | 
            +
                # Shows the current level's key in the UI
         | 
| 6 | 
            +
                class Key
         | 
| 7 | 
            +
                  attr_accessor :level
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(level)
         | 
| 10 | 
            +
                    self.level = level
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def start
         | 
| 14 | 
            +
                    self.text ||= generate_text
         | 
| 15 | 
            +
                    text.text = text_value
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  private
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  attr_accessor :text
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  TEXT_SIZE = 36
         | 
| 23 | 
            +
                  X_OFFSET = 60
         | 
| 24 | 
            +
                  Y_OFFSET = 90
         | 
| 25 | 
            +
                  private_constant :TEXT_SIZE, :X_OFFSET, :Y_OFFSET
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def generate_text
         | 
| 28 | 
            +
                    return Ruby2D::Text.new(
         | 
| 29 | 
            +
                      text_value,
         | 
| 30 | 
            +
                      x: X_OFFSET, y: Y_OFFSET,
         | 
| 31 | 
            +
                      font: File.join(Fet.root, "assets/fonts/PTSans/PTSans-Regular.ttf"),
         | 
| 32 | 
            +
                      size: TEXT_SIZE,
         | 
| 33 | 
            +
                      color: ColorScheme::WHITE,
         | 
| 34 | 
            +
                    )
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def text_value
         | 
| 38 | 
            +
                    key_name = level.degrees.root_name
         | 
| 39 | 
            +
                    key_type = level.game.key_type == "major" ? "Major" : "Minor"
         | 
| 40 | 
            +
                    return "#{key_name} #{key_type}"
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
    
        data/lib/fet/ui/level.rb
    ADDED
    
    | @@ -0,0 +1,108 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "custom_event"
         | 
| 4 | 
            +
            require_relative "key"
         | 
| 5 | 
            +
            require_relative "level_loop_handler"
         | 
| 6 | 
            +
            require_relative "note_boxes"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Fet
         | 
| 9 | 
            +
              module Ui
         | 
| 10 | 
            +
                # Holds state for the current level of the game
         | 
| 11 | 
            +
                class Level
         | 
| 12 | 
            +
                  attr_accessor :game, :question_number, :note_boxes, :key, :degrees
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  include LevelLoopHandler
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def initialize(game)
         | 
| 17 | 
            +
                    self.game = game
         | 
| 18 | 
            +
                    self.question_number = 0
         | 
| 19 | 
            +
                    self.note_boxes = NoteBoxes.new(self)
         | 
| 20 | 
            +
                    self.key = Key.new(self)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def start
         | 
| 24 | 
            +
                    start_self
         | 
| 25 | 
            +
                    note_boxes.start
         | 
| 26 | 
            +
                    key.start
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    # TODO: This is not ideal because we're piggybacking on the normal events at the moment,
         | 
| 29 | 
            +
                    # but there's no event here to piggyback on, so the handler has to be called manually.
         | 
| 30 | 
            +
                    game.set_level_started_event_flag
         | 
| 31 | 
            +
                    game.handle_event_loop(nil)
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def degree_indices
         | 
| 35 | 
            +
                    return midi_values.map { |midi_value| degrees.degree_index_of_midi_value(midi_value) }
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def over?
         | 
| 39 | 
            +
                    return degree_indices.all? do |degree_index|
         | 
| 40 | 
            +
                      note_boxes.note_boxes.any? { |note_box| note_box.selected && note_box.degree_index == degree_index }
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def answered_correctly?
         | 
| 45 | 
            +
                    over? && note_boxes.note_boxes.none? { |note_box| note_box.selected && !note_box.correct? }
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  private
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  attr_accessor :midi_values, :full_question_music, :chord_progression_music, :notes_music
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def start_self
         | 
| 53 | 
            +
                    self.question_number += 1
         | 
| 54 | 
            +
                    self.degrees = generate_degrees
         | 
| 55 | 
            +
                    self.midi_values = degrees.select_degrees_from_midi_values(game.note_range, game.number_of_degrees)
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    update_music_objects
         | 
| 58 | 
            +
                    play_full_question
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def play_full_question
         | 
| 62 | 
            +
                    full_question_music.play
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def generate_degrees
         | 
| 66 | 
            +
                    root_midi_values = game.key_type == "major" ? Fet::MAJOR_ROOT_MIDI_VALUES : Fet::MINOR_ROOT_MIDI_VALUES
         | 
| 67 | 
            +
                    root_name, root_midi_value = root_midi_values.to_a.sample
         | 
| 68 | 
            +
                    root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
         | 
| 69 | 
            +
                    return Degrees.new(root_name: root_name, octave_value: root_octave_value)
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def update_music_objects
         | 
| 73 | 
            +
                    self.full_question_music = generate_full_question_music
         | 
| 74 | 
            +
                    self.chord_progression_music = generate_chord_progression_music
         | 
| 75 | 
            +
                    self.notes_music = generate_notes_music
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def generate_full_question_music
         | 
| 79 | 
            +
                    filename = "tmp/chord_progression_and_question.mid"
         | 
| 80 | 
            +
                    create_midilib_object("Chord Progression + Question", filename).create_full_question
         | 
| 81 | 
            +
                    return Ruby2D::Music.new(filename)
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def generate_chord_progression_music
         | 
| 85 | 
            +
                    filename = "tmp/chord_progression.mid"
         | 
| 86 | 
            +
                    create_midilib_object("Chord Progression", filename).create_chord_progression_of_question
         | 
| 87 | 
            +
                    return Ruby2D::Music.new(filename)
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def generate_notes_music
         | 
| 91 | 
            +
                    filename = "tmp/question.mid"
         | 
| 92 | 
            +
                    create_midilib_object("Question", filename).create_notes_only
         | 
| 93 | 
            +
                    return Ruby2D::Music.new(filename)
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  def create_midilib_object(info, filename)
         | 
| 97 | 
            +
                    progression = Fet::ChordProgression.new(offset: degrees.root_midi_value, template_type: game.key_type).with_offset
         | 
| 98 | 
            +
                    Fet::MidilibInterface.new(
         | 
| 99 | 
            +
                      tempo: game.tempo,
         | 
| 100 | 
            +
                      progression: progression,
         | 
| 101 | 
            +
                      notes: midi_values,
         | 
| 102 | 
            +
                      info: info,
         | 
| 103 | 
            +
                      filename: filename,
         | 
| 104 | 
            +
                    )
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
              end
         | 
| 108 | 
            +
            end
         | 
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "custom_event"
         | 
| 4 | 
            +
            require_relative "key"
         | 
| 5 | 
            +
            require_relative "note_boxes"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Fet
         | 
| 8 | 
            +
              module Ui
         | 
| 9 | 
            +
                # Handles events for the Level class
         | 
| 10 | 
            +
                module LevelLoopHandler
         | 
| 11 | 
            +
                  def handle_event_loop(event)
         | 
| 12 | 
            +
                    handle_keyboard_event(event)
         | 
| 13 | 
            +
                    handle_level_complete_event(event)
         | 
| 14 | 
            +
                    note_boxes.handle_event_loop(event)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def handle_update_loop; end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  private
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def handle_keyboard_event(event)
         | 
| 22 | 
            +
                    return unless event.is_a?(Ruby2D::Window::KeyEvent)
         | 
| 23 | 
            +
                    return unless event.type == :down
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    handle_c_key if event.key == "c"
         | 
| 26 | 
            +
                    handle_n_key if event.key == "n"
         | 
| 27 | 
            +
                    handle_l_key if event.key == "l"
         | 
| 28 | 
            +
                    handle_return_key if event.key == "return"
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def handle_c_key
         | 
| 32 | 
            +
                    chord_progression_music.play
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def handle_n_key
         | 
| 36 | 
            +
                    notes_music.play
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def handle_l_key
         | 
| 40 | 
            +
                    full_question_music.loop = true
         | 
| 41 | 
            +
                    full_question_music.play
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def handle_return_key
         | 
| 45 | 
            +
                    return unless over?
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    start
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def handle_level_complete_event(event)
         | 
| 51 | 
            +
                    return unless event.is_a?(CustomEvent) && event.type == CustomEvent::EVENT_TYPE_LEVEL_COMPLETE
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    start if answered_correctly? && game.next_on_correct
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         | 
| @@ -0,0 +1,117 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "color_scheme"
         | 
| 4 | 
            +
            require_relative "note_boxes"
         | 
| 5 | 
            +
            require_relative "note_box_loop_handler"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Fet
         | 
| 8 | 
            +
              module Ui
         | 
| 9 | 
            +
                # Handles drawing + events for notes
         | 
| 10 | 
            +
                class NoteBox
         | 
| 11 | 
            +
                  include NoteBoxLoopHandler
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  attr_accessor :note_boxes, :degree_name, :selected
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize(note_boxes:, degree_name:)
         | 
| 16 | 
            +
                    self.note_boxes = note_boxes
         | 
| 17 | 
            +
                    self.degree_name = degree_name
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def start
         | 
| 21 | 
            +
                    self.keyboard_accidental = ""
         | 
| 22 | 
            +
                    self.square ||= generate_square
         | 
| 23 | 
            +
                    self.text ||= generate_text
         | 
| 24 | 
            +
                    self.selected = false
         | 
| 25 | 
            +
                    self.user_selected = false
         | 
| 26 | 
            +
                    self.note_music = generate_note_music
         | 
| 27 | 
            +
                    update_colors
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def correct?
         | 
| 31 | 
            +
                    return note_boxes.level.degree_indices.include?(degree_index)
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def degree_index
         | 
| 35 | 
            +
                    degree_instance.degree_index
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  private
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  attr_accessor :square, :text, :note_music
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  NOTE_BOX_SIZE = 70
         | 
| 43 | 
            +
                  TEXT_SIZE = 36
         | 
| 44 | 
            +
                  TEXT_X_FOR_NATURAL_OFFSET = 26
         | 
| 45 | 
            +
                  TEXT_X_FOR_ACCIDENTAL_OFFSET = 16
         | 
| 46 | 
            +
                  TEXT_Y_OFFSET = 13
         | 
| 47 | 
            +
                  private_constant :NOTE_BOX_SIZE, :TEXT_SIZE, :TEXT_X_FOR_NATURAL_OFFSET, :TEXT_X_FOR_ACCIDENTAL_OFFSET, :TEXT_Y_OFFSET
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def generate_square
         | 
| 50 | 
            +
                    return Ruby2D::Square.new(
         | 
| 51 | 
            +
                      x: NoteBoxes::NOTE_BOX_OFFSETS[degree_name][0],
         | 
| 52 | 
            +
                      y: NoteBoxes::NOTE_BOX_OFFSETS[degree_name][1],
         | 
| 53 | 
            +
                      size: NOTE_BOX_SIZE,
         | 
| 54 | 
            +
                      color: color,
         | 
| 55 | 
            +
                    )
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def generate_text
         | 
| 59 | 
            +
                    return Ruby2D::Text.new(
         | 
| 60 | 
            +
                      degree_name,
         | 
| 61 | 
            +
                      x: square.x + text_x_offset, y: square.y + text_y_offset,
         | 
| 62 | 
            +
                      font: File.join(Fet.root, "assets/fonts/PTSans/PTSans-Regular.ttf"),
         | 
| 63 | 
            +
                      size: TEXT_SIZE,
         | 
| 64 | 
            +
                      color: text_color,
         | 
| 65 | 
            +
                    )
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def text_x_offset
         | 
| 69 | 
            +
                    return degree_instance.degree_accidental ? TEXT_X_FOR_ACCIDENTAL_OFFSET : TEXT_X_FOR_NATURAL_OFFSET
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def text_y_offset
         | 
| 73 | 
            +
                    return TEXT_Y_OFFSET
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  def degree_instance
         | 
| 77 | 
            +
                    return Fet::Degree.new(degree_name)
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  def color
         | 
| 81 | 
            +
                    return correct? ? ColorScheme::GREEN : ColorScheme::RED if selected
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    return degree_instance.degree_accidental ? ColorScheme::GREY : ColorScheme::WHITE
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def text_color
         | 
| 87 | 
            +
                    case color
         | 
| 88 | 
            +
                    when ColorScheme::GREY, ColorScheme::GREEN, ColorScheme::RED
         | 
| 89 | 
            +
                      return ColorScheme::WHITE
         | 
| 90 | 
            +
                    when ColorScheme::WHITE
         | 
| 91 | 
            +
                      return ColorScheme::BLACK
         | 
| 92 | 
            +
                    else
         | 
| 93 | 
            +
                      raise ImplementationError
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  def level_over?
         | 
| 98 | 
            +
                    return note_boxes.level.over?
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  def generate_note_music
         | 
| 102 | 
            +
                    degrees = note_boxes.level.degrees
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    filename = "tmp/#{degree_name}.mid"
         | 
| 105 | 
            +
                    Fet::MidilibInterface.new(
         | 
| 106 | 
            +
                      tempo: note_boxes.level.game.tempo,
         | 
| 107 | 
            +
                      progression: nil,
         | 
| 108 | 
            +
                      notes: [degrees.root_midi_value + degree_instance.degree_index],
         | 
| 109 | 
            +
                      info: degree_name,
         | 
| 110 | 
            +
                      filename: filename,
         | 
| 111 | 
            +
                    ).create_notes_only
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    return Ruby2D::Music.new(filename)
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
            end
         | 
| @@ -0,0 +1,107 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "color_scheme"
         | 
| 4 | 
            +
            require_relative "note_boxes"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Fet
         | 
| 7 | 
            +
              module Ui
         | 
| 8 | 
            +
                # Handles events for NoteBox
         | 
| 9 | 
            +
                module NoteBoxLoopHandler
         | 
| 10 | 
            +
                  attr_accessor :user_selected
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def handle_event_loop(event)
         | 
| 13 | 
            +
                    handle_click_event(event)
         | 
| 14 | 
            +
                    handle_keyboard_event(event)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def handle_update_loop; end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def manually_select
         | 
| 20 | 
            +
                    handle_selected(user_selected: false)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  attr_accessor :keyboard_accidental
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def handle_click_event(event)
         | 
| 28 | 
            +
                    return unless event.is_a?(Ruby2D::Window::MouseEvent)
         | 
| 29 | 
            +
                    return unless event.type == :down
         | 
| 30 | 
            +
                    return unless event.button == :left
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    handle_selected if square.contains?(event.x, event.y)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def handle_keyboard_event(event)
         | 
| 36 | 
            +
                    return unless event.is_a?(Ruby2D::Window::KeyEvent)
         | 
| 37 | 
            +
                    return unless event.type == :down
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    input = determine_keyboard_input(event)
         | 
| 40 | 
            +
                    handle_selected if degree_instance.degree_index == input_degree_instance(input)&.degree_index
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  KEYBOARD_TO_ACCIDENTAL_MAP = {
         | 
| 44 | 
            +
                    "=" => "#",
         | 
| 45 | 
            +
                    "0" => "",
         | 
| 46 | 
            +
                    "-" => "b",
         | 
| 47 | 
            +
                  }.deep_freeze
         | 
| 48 | 
            +
                  private_constant :KEYBOARD_TO_ACCIDENTAL_MAP
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def determine_keyboard_input(event)
         | 
| 51 | 
            +
                    update_keyboard_input_accidental(event)
         | 
| 52 | 
            +
                    handle_keyboard_input_degree(event)
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def update_keyboard_input_accidental(event)
         | 
| 56 | 
            +
                    case event.key
         | 
| 57 | 
            +
                    when *KEYBOARD_TO_ACCIDENTAL_MAP.keys
         | 
| 58 | 
            +
                      self.keyboard_accidental = KEYBOARD_TO_ACCIDENTAL_MAP[event.key]
         | 
| 59 | 
            +
                    when *(1..7).map(&:to_s)
         | 
| 60 | 
            +
                      # noop
         | 
| 61 | 
            +
                    else
         | 
| 62 | 
            +
                      self.keyboard_accidental = ""
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def handle_keyboard_input_degree(event)
         | 
| 67 | 
            +
                    case event.key
         | 
| 68 | 
            +
                    when *(1..7).map(&:to_s)
         | 
| 69 | 
            +
                      input = "#{keyboard_accidental}#{event.key}"
         | 
| 70 | 
            +
                      self.keyboard_accidental = ""
         | 
| 71 | 
            +
                      return input
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def input_degree_instance(input)
         | 
| 76 | 
            +
                    return nil if input.nil?
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    return Fet::Degree.new(input)
         | 
| 79 | 
            +
                  rescue Fet::InvalidDegreeName
         | 
| 80 | 
            +
                    # TODO: alterenatively, make degrees like b1 or #1 valid degree names, though that would need a bit of a refactor
         | 
| 81 | 
            +
                    return nil
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  def handle_selected(user_selected: true)
         | 
| 85 | 
            +
                    self.keyboard_accidental = ""
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    if level_over?
         | 
| 88 | 
            +
                      play_note
         | 
| 89 | 
            +
                    else
         | 
| 90 | 
            +
                      self.user_selected = user_selected
         | 
| 91 | 
            +
                      self.selected = true
         | 
| 92 | 
            +
                      update_colors
         | 
| 93 | 
            +
                      note_boxes.level.game.set_note_selected_event_flag
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  def play_note
         | 
| 98 | 
            +
                    note_music.play
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  def update_colors
         | 
| 102 | 
            +
                    square.color = color
         | 
| 103 | 
            +
                    text.color = text_color
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
            end
         |