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