From d4077875a4d918d5d8c7aaf52a7ea69a266c243d Mon Sep 17 00:00:00 2001 From: Maruno17 Date: Thu, 31 Aug 2023 23:03:47 +0100 Subject: [PATCH] Implemented list control and basic animation-choosing screen for editor --- Data/Scripts/901_GameData/Animation.rb | 8 +- .../905_New controls/001 basic control.rb | 5 + Data/Scripts/905_New controls/003 checkbox.rb | 16 +- .../905_New controls/005 number slider.rb | 4 +- Data/Scripts/905_New controls/007 button.rb | 33 ++- Data/Scripts/905_New controls/008_list.rb | 233 +++++++++++++++++- .../910_New anim editor/001_anim selection.rb | 127 +++++++--- 7 files changed, 367 insertions(+), 59 deletions(-) diff --git a/Data/Scripts/901_GameData/Animation.rb b/Data/Scripts/901_GameData/Animation.rb index 4b26e2242..7530922b3 100644 --- a/Data/Scripts/901_GameData/Animation.rb +++ b/Data/Scripts/901_GameData/Animation.rb @@ -19,8 +19,8 @@ module GameData extend ClassMethodsIDNumbers include InstanceMethods - def register(hash) - DATA[DATA.keys.length] = self.new(hash) + def register(hash, id = -1) + DATA[(id >= 0) ? id : DATA.keys.length] = self.new(hash) end # TODO: Rewrite this to query animations from other criteria. Remember that @@ -59,5 +59,9 @@ module GameData def move_animation? return !@move.nil? end + + # TODO: Create a def to_hash or something, which returns a hash copy version + # of this Animation object which can be edited. This hash should be + # able to be passed into def register (with an ID number). end end diff --git a/Data/Scripts/905_New controls/001 basic control.rb b/Data/Scripts/905_New controls/001 basic control.rb index fea40b138..4ef26e5dc 100644 --- a/Data/Scripts/905_New controls/001 basic control.rb +++ b/Data/Scripts/905_New controls/001 basic control.rb @@ -85,6 +85,11 @@ class UIControls::BaseControl < BitmapSprite this_bitmap.draw_text(text_x, text_y, text_size.width, text_size.height, this_text, 0) end + def draw_text_centered(this_bitmap, text_x, text_y, wid, this_text) + text_size = this_bitmap.text_size(this_text) + this_bitmap.draw_text(text_x, text_y, wid, text_size.height, this_text, 1) + end + # Redraws the control only if it is invalid. def repaint return if !invalid? diff --git a/Data/Scripts/905_New controls/003 checkbox.rb b/Data/Scripts/905_New controls/003 checkbox.rb index faa1d38fc..103f74b9e 100644 --- a/Data/Scripts/905_New controls/003 checkbox.rb +++ b/Data/Scripts/905_New controls/003 checkbox.rb @@ -2,10 +2,10 @@ # NOTE: Strictly speaking, this is a toggle switch and not a checkbox. #=============================================================================== class UIControls::Checkbox < UIControls::BaseControl - CHECKBOX_X = 0 + CHECKBOX_X = 2 CHECKBOX_WIDTH = 40 CHECKBOX_HEIGHT = 24 - CHECKBOX_FILL_SIZE = CHECKBOX_HEIGHT - 8 + CHECKBOX_FILL_SIZE = CHECKBOX_HEIGHT - 4 UNCHECKED_COLOR = Color.gray CHECKED_COLOR = Color.new(64, 255, 64) # Green @@ -34,19 +34,19 @@ class UIControls::Checkbox < UIControls::BaseControl def refresh super # Draw checkbox outline - self.bitmap.outline_rect(@checkbox_rect.x + 2, @checkbox_rect.y + 2, - @checkbox_rect.width - 4, @checkbox_rect.height - 4, + self.bitmap.outline_rect(@checkbox_rect.x, @checkbox_rect.y, + @checkbox_rect.width, @checkbox_rect.height, self.bitmap.font.color) # Draw checkbox fill if @value # If checked - self.bitmap.fill_rect(@checkbox_rect.x + @checkbox_rect.width - CHECKBOX_FILL_SIZE - 4, @checkbox_rect.y + 4, + self.bitmap.fill_rect(@checkbox_rect.x + @checkbox_rect.width - CHECKBOX_FILL_SIZE - 2, @checkbox_rect.y + 2, CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, CHECKED_COLOR) - self.bitmap.outline_rect(@checkbox_rect.x + @checkbox_rect.width - CHECKBOX_FILL_SIZE - 4, @checkbox_rect.y + 4, + self.bitmap.outline_rect(@checkbox_rect.x + @checkbox_rect.width - CHECKBOX_FILL_SIZE - 2, @checkbox_rect.y + 2, CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, self.bitmap.font.color) else - self.bitmap.fill_rect(@checkbox_rect.x + 4, @checkbox_rect.y + 4, + self.bitmap.fill_rect(@checkbox_rect.x + 2, @checkbox_rect.y + 2, CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, UNCHECKED_COLOR) - self.bitmap.outline_rect(@checkbox_rect.x + 4, @checkbox_rect.y + 4, + self.bitmap.outline_rect(@checkbox_rect.x + 2, @checkbox_rect.y + 2, CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, self.bitmap.font.color) end end diff --git a/Data/Scripts/905_New controls/005 number slider.rb b/Data/Scripts/905_New controls/005 number slider.rb index e94b95b68..2e2d1e867 100644 --- a/Data/Scripts/905_New controls/005 number slider.rb +++ b/Data/Scripts/905_New controls/005 number slider.rb @@ -15,7 +15,9 @@ class UIControls::Slider < UIControls::BaseControl VALUE_X = PLUS_X + PLUS_MINUS_SIZE + 5 TEXT_OFFSET_Y = 7 - SLIDER_KNOB_COLOR = Color.red + # TODO: Is there a better knob design than a big black rectangle? I'd rather + # it not be a different colour. + SLIDER_KNOB_COLOR = Color.black def initialize(width, height, viewport, min_value, max_value, value) super(width, height, viewport) diff --git a/Data/Scripts/905_New controls/007 button.rb b/Data/Scripts/905_New controls/007 button.rb index 058a47746..e84d2acae 100644 --- a/Data/Scripts/905_New controls/007 button.rb +++ b/Data/Scripts/905_New controls/007 button.rb @@ -2,20 +2,28 @@ # #=============================================================================== class UIControls::Button < UIControls::BaseControl - BUTTON_X = 2 - BUTTON_PADDING = 10 - BUTTON_HEIGHT = 28 - TEXT_OFFSET_Y = 7 + BUTTON_X = 2 + BUTTON_Y = 2 + BUTTON_PADDING = 10 + BUTTON_HEIGHT = 28 + # TODO: This will also depend on the font size. + TEXT_BASE_OFFSET_Y = 18 # Text is centred vertically in the button def initialize(width, height, viewport, text = "") super(width, height, viewport) @text = text + @fixed_size = false + end + + def set_fixed_size + @fixed_size = true end def set_interactive_rects - text_width = self.bitmap.text_size(@text).width - @button_rect = Rect.new(BUTTON_X, (height - BUTTON_HEIGHT) / 2, - text_width + (BUTTON_PADDING * 2), BUTTON_HEIGHT) + button_width = (@fixed_size) ? width - (BUTTON_X * 2) : self.bitmap.text_size(@text).width + (BUTTON_PADDING * 2) + button_height = (@fixed_size) ? height - (2 * BUTTON_Y) : BUTTON_HEIGHT + button_height = [button_height, height - (2 * BUTTON_Y)].min + @button_rect = Rect.new(BUTTON_X, (height - button_height) / 2, button_width, button_height) @interactions = { :button => @button_rect } @@ -23,15 +31,22 @@ class UIControls::Button < UIControls::BaseControl #----------------------------------------------------------------------------- - # TODO: Make buttons look more different to text boxes? def refresh super # Draw button outline self.bitmap.outline_rect(@button_rect.x, @button_rect.y, @button_rect.width, @button_rect.height, self.bitmap.font.color) + # TODO: Make buttons look more different to text boxes? + # shade = self.bitmap.font.color.clone + # shade.alpha = 96 + # self.bitmap.outline_rect(@button_rect.x + 1, @button_rect.y + 1, + # @button_rect.width - 2, @button_rect.height - 2, + # shade, 3) # Draw button text - draw_text(self.bitmap, BUTTON_X + BUTTON_PADDING, TEXT_OFFSET_Y, @text) + draw_text_centered(self.bitmap, @button_rect.x, + @button_rect.y + (@button_rect.height - TEXT_BASE_OFFSET_Y) / 2, + @button_rect.width, @text) end #----------------------------------------------------------------------------- diff --git a/Data/Scripts/905_New controls/008_list.rb b/Data/Scripts/905_New controls/008_list.rb index 60c7881e9..5f68a98e6 100644 --- a/Data/Scripts/905_New controls/008_list.rb +++ b/Data/Scripts/905_New controls/008_list.rb @@ -1,13 +1,228 @@ #=============================================================================== -# TODO -# TODO: Click an option to select it. It remains selected indefinitely. Once an -# option is selected, there's probably no way to unselect everything; the -# selection can only be moved to a different option. -# TODO: Scrollable. -# TODO: Find some way to not redraw the entire thing if the hovered option -# changes. Maybe have another bitmap to write the text on (refreshed only -# when the list is scrolled), and self's bitmap draws the hover colour -# only. +# TODO: Do I need to split self's bitmap into two (one for highlights and one +# for text/slider)? This would be to reduce lag caused by redrawing text +# and the slider even if you're just waving the mouse over the control. +# There doesn't seem to be any lag at the moment with a tall list. #=============================================================================== class UIControls::List < UIControls::BaseControl + LIST_X = 0 + LIST_Y = 0 + ROW_HEIGHT = 24 + TEXT_PADDING_X = 4 + TEXT_OFFSET_Y = 3 + SLIDER_WIDTH = 16 + + SELECTED_ROW_COLOR = Color.green + + def initialize(width, height, viewport, values = []) + super(width, height, viewport) + @rows_count = (height / ROW_HEIGHT).floor # Number of rows visible at once + @top_row = 0 + @selected = -1 + @show_slider = false + self.values = values + end + + # Each value in @values is an array: [id, text]. + def values=(new_vals) + @values = new_vals + @show_slider = (@values.length > @rows_count) + set_interactive_rects + if @show_slider + self.top_row = @top_row + else + self.top_row = 0 + end + invalidate + end + + def top_row=(val) + old_val = @top_row + @top_row = val + if @show_slider + @top_row = @top_row.clamp(0, @values.length - @rows_count) + @slider.y = lerp(0, height - @slider.height, @values.length - @rows_count, 0, @top_row).round + else + @top_row = 0 + end + invalidate if @top_row != old_val + end + + def selected=(val) + return if @selected == val + @selected = val + invalidate + end + + # Returns the ID of the selected row. + def value + return nil if @selected < 0 + return @values[@selected][0] + end + + def set_interactive_rects + @interactions = {} + @slider = nil + if @show_slider + @slider = Rect.new(LIST_X + width - SLIDER_WIDTH, LIST_Y, + SLIDER_WIDTH, height * @rows_count / @values.length) + @interactions[:slider] = @slider + @slider_tray = Rect.new(LIST_X + width - SLIDER_WIDTH, LIST_Y, SLIDER_WIDTH, height) + @interactions[:slider_tray] = @slider_tray + end + @values.length.times do |i| + @interactions[i] = Rect.new(LIST_X, LIST_Y + (ROW_HEIGHT * i), width - LIST_X, ROW_HEIGHT) + end + end + + #----------------------------------------------------------------------------- + + def busy? + return !@captured_area.nil? + end + + #----------------------------------------------------------------------------- + + def draw_area_highlight + # If a row is captured, it will automatically be selected and the selection + # colour will be drawn over the highlight. The slider tray background + # (white) is drawn over the slider/slider tray's highlight. Either way, + # there's no point drawing a highlight at all if anything is captured. + return if @captured_area + # The slider tray background (white) is drawn over the slider/slider tray's + # highlight. There's no point drawing any highlight for the slider now; this + # is done in def refresh instead. + return if [:slider, :slider_tray].include?(@hover_area) + # Draw mouse hover over row highlight + rect = @interactions[@hover_area] + if rect + rect_y = rect.y + rect_y -= @top_row * ROW_HEIGHT if @hover_area.is_a?(Integer) + self.bitmap.fill_rect(rect.x, rect_y, rect.width, rect.height, HOVER_COLOR) + end + end + + def refresh + super + # Draw text options + @values.each_with_index do |val, i| + next if i < @top_row || i >= @top_row + @rows_count + if @selected == i + self.bitmap.fill_rect( + @interactions[i].x, + @interactions[i].y - (@top_row * ROW_HEIGHT), + @interactions[i].width, @interactions[i].height, + SELECTED_ROW_COLOR + ) + end + draw_text(self.bitmap, + @interactions[i].x + TEXT_PADDING_X, + @interactions[i].y + TEXT_OFFSET_Y - (@top_row * ROW_HEIGHT), + val[1]) + end + # Draw vertical slider + if @show_slider + self.bitmap.fill_rect(@slider_tray.x, @slider_tray.y, @slider_tray.width, @slider_tray.height, Color.white) + bar_color = self.bitmap.font.color + if @captured_area == :slider || (!@captured_area && @hover_area == :slider) + bar_color = HOVER_COLOR + end + self.bitmap.fill_rect(@slider.x + 1, @slider.y, @slider.width - 1, @slider.height, bar_color) + end + end + + #----------------------------------------------------------------------------- + + def on_mouse_press + @captured_area = nil + mouse_x, mouse_y = mouse_pos + return if !mouse_x || !mouse_y + # Check for mouse presses on slider/slider tray + @interactions.each_pair do |area, rect| + next if area.is_a?(Integer) + next if !rect.contains?(mouse_x, mouse_y) + @captured_area = area + @slider_mouse_offset = mouse_y - rect.y if area == :slider + invalidate + break + end + return if @captured_area + # Check for mouse presses on rows + mouse_y += @top_row * ROW_HEIGHT + @interactions.each_pair do |area, rect| + next if !area.is_a?(Integer) || area < @top_row || area >= @top_row + @rows_count + next if !rect.contains?(mouse_x, mouse_y) + @captured_area = area + invalidate + break + end + end + + def on_mouse_release + return if !@captured_area # Wasn't captured to begin with + set_changed if @captured_area != :slider + super + end + + def update_hover_highlight + # Remove the hover highlight if there are no interactions for this control + # or if the mouse is off-screen + mouse_x, mouse_y = mouse_pos + if !@interactions || @interactions.empty? || !mouse_x || !mouse_y + invalidate if @hover_area + @hover_area = nil + return + end + # Check each interactive area for whether the mouse is hovering over it, and + # set @hover_area accordingly + in_area = false + @interactions.each_pair do |area, rect| + next if area.is_a?(Integer) + next if !rect.contains?(mouse_x, mouse_y) + invalidate if @hover_area != area + @hover_area = area + in_area = true + break + end + if !in_area + mouse_y += @top_row * ROW_HEIGHT + @interactions.each_pair do |area, rect| + next if !area.is_a?(Integer) || area < @top_row || area >= @top_row + @rows_count + next if !rect.contains?(mouse_x, mouse_y) + invalidate if @hover_area != area + @hover_area = area + in_area = true + break + end + end + if !in_area + invalidate if @hover_area + @hover_area = nil + end + end + + def update + super + # TODO: Disabled control stuff. +# return if self.disabled + if @captured_area == :slider + # TODO: Have a display y position for the slider bar which is in pixels, + # and round it to the nearest row when setting @top_row? This is + # just to make the slider bar movement smoother. + mouse_x, mouse_y = mouse_pos + return if !mouse_x || !mouse_y + self.top_row = lerp(0, @values.length - @rows_count, height - @slider.height, 0, mouse_y - @slider_mouse_offset).round + elsif @captured_area == :slider_tray + if Input.repeat?(Input::MOUSELEFT) && @hover_area == :slider_tray + if mouse_y < @slider.y + self.top_row = @top_row - (@rows_count / 2) + else + self.top_row = @top_row + (@rows_count / 2) + end + end + elsif @captured_area + # Have clicked on a row; set the selected row to the row themouse is over + @selected = @hover_area if @hover_area.is_a?(Integer) + end + end end diff --git a/Data/Scripts/910_New anim editor/001_anim selection.rb b/Data/Scripts/910_New anim editor/001_anim selection.rb index 443235291..71691a440 100644 --- a/Data/Scripts/910_New anim editor/001_anim selection.rb +++ b/Data/Scripts/910_New anim editor/001_anim selection.rb @@ -9,11 +9,19 @@ class AnimationEditorLoadScreen ANIMATIONS_LIST_WIDTH = 300 ANIMATIONS_LIST_HEIGHT = WINDOW_HEIGHT - (ANIMATIONS_LIST_Y * 2) + LOAD_BUTTON_WIDTH = 200 + LOAD_BUTTON_HEIGHT = 48 + LOAD_BUTTON_X = ANIMATIONS_LIST_WIDTH + 100 + LOAD_BUTTON_Y = ANIMATIONS_LIST_Y + (ANIMATIONS_LIST_HEIGHT / 2) - (LOAD_BUTTON_HEIGHT / 2) + def initialize - @viewport = Viewport.new(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT) + generate_list + @viewport = Viewport.new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT) @viewport.z = 99999 - @screen_bitmap = BitmapSprite.new(AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, @viewport) + @screen_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @viewport) draw_editor_background + @load_animation_id = nil + create_controls end def dispose @@ -21,32 +29,86 @@ class AnimationEditorLoadScreen @viewport.dispose end + def generate_list + @animations = [] + # TODO: Look through GameData to populate @animations; below is temporary. + # There will be separate arrays for move animations, common animations + # and overworld animations. The move animations one will primarily be + # a list of moves that have any animations, with the actual GameData + # animations being in a sub-array for each move. + 67.times { |i| @animations.push([i, "Animation #{i + 1}"]) } + end + def draw_editor_background - # Fill the whole screen with black - @screen_bitmap.bitmap.fill_rect( - 0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, Color.black - ) + # Fill the whole screen with white + @screen_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, Color.black) # Outline around animations list - @screen_bitmap.bitmap.outline_rect( - ANIMATIONS_LIST_X - 3, ANIMATIONS_LIST_Y - 3, - ANIMATIONS_LIST_WIDTH + 6, ANIMATIONS_LIST_HEIGHT + 6, Color.white - ) - @screen_bitmap.bitmap.outline_rect( - ANIMATIONS_LIST_X - 2, ANIMATIONS_LIST_Y - 2, - ANIMATIONS_LIST_WIDTH + 4, ANIMATIONS_LIST_HEIGHT + 4, Color.black - ) - @screen_bitmap.bitmap.outline_rect( - ANIMATIONS_LIST_X - 1, ANIMATIONS_LIST_Y - 1, - ANIMATIONS_LIST_WIDTH + 2, ANIMATIONS_LIST_HEIGHT + 2, Color.white - ) - # Fill the animations list with white - @screen_bitmap.bitmap.fill_rect( - ANIMATIONS_LIST_X, ANIMATIONS_LIST_Y, ANIMATIONS_LIST_WIDTH, ANIMATIONS_LIST_HEIGHT, Color.white - ) + areas = [ + [ANIMATIONS_LIST_X, ANIMATIONS_LIST_Y, ANIMATIONS_LIST_WIDTH, ANIMATIONS_LIST_HEIGHT], + [LOAD_BUTTON_X, LOAD_BUTTON_Y, LOAD_BUTTON_WIDTH, LOAD_BUTTON_HEIGHT] + ] + areas.each do |area| + # Draw outlines around area + @screen_bitmap.bitmap.outline_rect(area[0] - 3, area[1] - 3, area[2] + 6, area[3] + 6, Color.white) + @screen_bitmap.bitmap.outline_rect(area[0] - 2, area[1] - 2, area[2] + 4, area[3] + 4, Color.black) + @screen_bitmap.bitmap.outline_rect(area[0] - 1, area[1] - 1, area[2] + 2, area[3] + 2, Color.white) + # Fill the area with white +# @screen_bitmap.bitmap.fill_rect(area[0], area[1], area[2], area[3], Color.white) + end + end + + def create_controls + @controls = {} + # TODO: Buttons to toggle between listing moves that have animations, and + # common animations (and overworld animations). + # Animations list + @list = UIControls::List.new(ANIMATIONS_LIST_WIDTH, ANIMATIONS_LIST_HEIGHT, @viewport, @animations) + @list.x = ANIMATIONS_LIST_X + @list.y = ANIMATIONS_LIST_Y + @controls[:list] = @list + # TODO: A secondary list for displaying all the animations related to the + # selected move. For common anims/overworld anims, this will only ever + # list one animation. The first animation listed in here will be + # selected by default. + # TODO: Filter text box for @list's contents. Applies the filter upon every + # change to the text box's value. Perhaps it should only do so after + # 0.5 seconds of non-typing. What exactly should the filter be applied + # to? Animation's name, move's name (if there is one), what else? + # TODO: Filter dropdown list to pick a type? Other filter options? + # "Load animation" button + @load_button = UIControls::Button.new(LOAD_BUTTON_WIDTH, LOAD_BUTTON_HEIGHT, @viewport, "Load animation") + @load_button.x = LOAD_BUTTON_X + @load_button.y = LOAD_BUTTON_Y + @load_button.set_fixed_size + @load_button.set_interactive_rects + @controls[:load] = @load_button + # TODO: "New animation" button, "Delete animation" button. + repaint + end + + def repaint + @controls.each { |ctrl| ctrl[1].repaint } end def update - # TODO: Update the controls (animations list, Load button, etc.). + # Update all controls + if @captured + @captured.update + @captured = nil if !@captured.busy? + else + @controls.each do |ctrl| + ctrl[1].update + @captured = ctrl[1] if ctrl[1].busy? + end + end + # Check for changes in controls + @list.clear_changed if @list.changed? # We don't need @list's value now + if @load_button.changed? + # TODO: This will need to get the animation ID from the sublist instead. + @load_animation_id = @list.value + @load_button.clear_changed + end + repaint # Only repaints if needed end def run @@ -56,15 +118,20 @@ class AnimationEditorLoadScreen Graphics.update Input.update update - if !inputting_text - break if Input.trigger?(Input::BACK) - end - # Open editor with animation - # TODO: If the Load button is pressed while an animation is selected. - if Input.trigger?(Input::USE) - # TODO: Add animation to be edited as an argument. + if @load_animation_id + # Open editor with animation + # TODO: Add animation to be edited as an argument. This will be + # GameData::Animation.get(@load_animation_id).to_hash. + echoln "Anim number #{@load_animation_id}: #{@animations[@load_animation_id][1]}" screen = AnimationEditor.new screen.run + @load_animation_id = nil + # TODO: Regenerate @animations in case the edited animation changed its + # name/move/version. Reapply @animations to @list and the sublist + # (this should invalidate them). + repaint + elsif !inputting_text + break if Input.trigger?(Input::BACK) end end dispose