Initial proof of concept commit

This commit is contained in:
Maruno17
2023-08-28 22:41:48 +01:00
parent ea7b5d56d2
commit 1041883992
16 changed files with 1358 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
class Bitmap
def outline_rect(x, y, width, height, color, thickness = 1)
fill_rect(x, y, width, thickness, color)
fill_rect(x, y, thickness, height, color)
fill_rect(x, y + height - thickness, width, thickness, color)
fill_rect(x + width - thickness, y, thickness, height, color)
end
end

View File

@@ -0,0 +1,63 @@
module GameData
class Animation
attr_reader :name
attr_reader :move, :type # Type is move's type; useful for filtering; move==nil means common animation
attr_reader :version # Hit number
# TODO: Boolean for whether user is on player's side or foe's side.
# TODO: Boolean for not played if target is on user's side.
attr_reader :particles
attr_reader :flags
# TODO: PBS filename.
# attr_reader :pbs_filename
DATA = {}
# TODO: Make sure the existence of animations.dat is optional. Currently
# it's required.
# DATA_FILENAME = "animations.dat"
# PBS_BASE_FILENAME = "animations"
extend ClassMethodsIDNumbers
include InstanceMethods
def register(hash)
DATA[DATA.keys.length] = self.new(hash)
end
# TODO: Rewrite this to query animations from other criteria. Remember that
# multiple animations could have the same move/version. Odds are this
# method won't be used much at all.
# TODO: Separate exists? methods for move and common animations?
# def exists?(other)
# end
# TODO: Rewrite this to get animations from other criteria. Remember that
# multiple animations could have the same move/version. Odds are this
# method won't be used much at all.
# TODO: Separate get methods for move and common animations?
# def get(other)
# end
# TODO: Rewrite this to get animations from other criteria. Remember that
# multiple animations could have the same move/version. Odds are this
# method won't be used much at all.
# TODO: Separate try_get methods for move and common animations?
# def try_get(other)
# end
def initialize(hash)
@name = hash[:name]
@move = hash[:move]
@type = hash[:type]
@version = hash[:version] || 0
@particles = []
# TODO: Copy particles info from hash somehow.
@flags = hash[:flags] || []
# TODO: Come up with a decent default PBS filename; likely the move's name
# (for move anims) or @name (for common anims).
end
def move_animation?
return !@move.nil?
end
end
end

View File

@@ -0,0 +1,2 @@
# Container module for control classes.
module UIControls; end

View File

@@ -0,0 +1,180 @@
# TODO: Add "disabled" greying out/non-editable.
# TODO: Add indicator of whether the control's value is "lerping" between frames
# (use yellow somehow?).
#===============================================================================
#
#===============================================================================
class UIControls::BaseControl < BitmapSprite
attr_reader :value
# attr_accessor :disabled # TODO: Make use of this.
TEXT_COLOR = Color.black
TEXT_SIZE = 18 # Default is 22 if size isn't explicitly set
HOVER_COLOR = Color.cyan # For clickable area when hovering over it
CAPTURE_COLOR = Color.pink # For area you clicked in but aren't hovering over
def initialize(width, height, viewport)
super(width, height, viewport)
self.bitmap.font.color = TEXT_COLOR
self.bitmap.font.size = TEXT_SIZE
# @disabled = false # TODO: Make use of this.
@hover_area = nil # Is a symbol from the keys for @interactions if the mouse is hovering over that interaction
@captured_area = nil # Is a symbol from the keys for @interactions (or :none) if this control is clicked in
clear_changed
invalidate
end
def width
return self.bitmap.width
end
def height
return self.bitmap.height
end
def mouse_pos
mouse_coords = Mouse.getMousePos
return nil, nil if !mouse_coords
ret_x = mouse_coords[0] - self.viewport.rect.x - self.x
ret_y = mouse_coords[1] - self.viewport.rect.y - self.y
return ret_x, ret_y
end
def set_interactive_rects
@interactions = {}
end
#-----------------------------------------------------------------------------
def invalid?
return @invalid
end
# Marks that the control must be redrawn to reflect current logic.
def invalidate
@invalid = true
end
# Makes the control no longer invalid.
def validate
@invalid = false
end
def busy?
return !@captured_area.nil?
end
def changed?
return @changed
end
def set_changed
@changed = true
end
def clear_changed
@changed = false
end
#-----------------------------------------------------------------------------
def draw_text(this_bitmap, text_x, text_y, this_text)
text_size = this_bitmap.text_size(this_text)
this_bitmap.draw_text(text_x, text_y, text_size.width, text_size.height, this_text, 0)
end
# Redraws the control only if it is invalid.
def repaint
return if !invalid?
refresh
validate
end
def refresh
# Paint over control to erase contents (intentionally not using self.bitmap.clear)
self.bitmap.clear
draw_area_highlight
end
def draw_area_highlight
return if !@interactions || @interactions.empty?
if !@captured_area || @hover_area == @captured_area
# Draw mouse hover over area highlight
rect = @interactions[@hover_area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, HOVER_COLOR) if rect
elsif @captured_area
# Draw captured area highlight
rect = @interactions[@captured_area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, CAPTURE_COLOR) if rect
end
end
#-----------------------------------------------------------------------------
# This method is only called if the mouse is in the game window and this
# control has interactive elements.
def on_mouse_press
return if !@interactions || @interactions.empty?
return if @captured_area
@captured_area = nil
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
@interactions.each_pair do |area, rect|
next if !rect.contains?(mouse_x, mouse_y)
@captured_area = area
invalidate
break
end
end
# Returns whether this control has been properly decaptured.
def on_mouse_release
@captured_area = nil
invalidate
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 !rect.contains?(mouse_x, mouse_y)
invalidate if @hover_area != area
@hover_area = area
in_area = true
break
end
if !in_area
invalidate if @hover_area
@hover_area = nil
end
end
# Updates the logic on the control, invalidating it if necessary.
def update
# TODO: Disabled control stuff.
# return if self.disabled
update_hover_highlight
# Detect a mouse press/release
if @interactions && !@interactions.empty?
if Input.trigger?(Input::MOUSELEFT)
on_mouse_press
elsif busy? && Input.release?(Input::MOUSELEFT)
on_mouse_release
end
end
end
end

View File

@@ -0,0 +1,24 @@
#===============================================================================
#
#===============================================================================
class UIControls::Label < UIControls::BaseControl
attr_reader :label
LABEL_END_X = 80
TEXT_OFFSET_Y = 7
def initialize(width, height, viewport, label)
super(width, height, viewport)
@label = label
end
def label=(value)
@label = value
refresh
end
def refresh
super
draw_text(self.bitmap, 4, TEXT_OFFSET_Y, @label)
end
end

View File

@@ -0,0 +1,68 @@
#===============================================================================
# NOTE: Strictly speaking, this is a toggle switch and not a checkbox.
#===============================================================================
class UIControls::Checkbox < UIControls::BaseControl
CHECKBOX_X = 0
CHECKBOX_WIDTH = 40
CHECKBOX_HEIGHT = 24
CHECKBOX_FILL_SIZE = CHECKBOX_HEIGHT - 8
UNCHECKED_COLOR = Color.gray
CHECKED_COLOR = Color.new(64, 255, 64) # Green
def initialize(width, height, viewport, value = false)
super(width, height, viewport)
@value = value
end
def value=(val)
return if @value == val
@value = val
invalidate
end
def set_interactive_rects
@checkbox_rect = Rect.new(CHECKBOX_X, (height - CHECKBOX_HEIGHT) / 2,
CHECKBOX_WIDTH, CHECKBOX_HEIGHT)
@interactions = {
:checkbox => @checkbox_rect
}
end
#-----------------------------------------------------------------------------
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.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,
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,
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, self.bitmap.font.color)
else
self.bitmap.fill_rect(@checkbox_rect.x + 4, @checkbox_rect.y + 4,
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, UNCHECKED_COLOR)
self.bitmap.outline_rect(@checkbox_rect.x + 4, @checkbox_rect.y + 4,
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, self.bitmap.font.color)
end
end
#-----------------------------------------------------------------------------
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Change this control's value
if @captured_area == :checkbox
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
@value = !@value # The actual change of this control's value
set_changed
end
end
super # Make this control not busy again
end
end

View File

@@ -0,0 +1,289 @@
#===============================================================================
# TODO: Support selecting part of the text by remembering the initial
# cursor position and using it and the current cursor position to
# decide which characters are selected. Maybe? Note that this method
# is only triggered upon the initial mouse press, and isn't repeated
# while it's still held down.
#===============================================================================
class UIControls::TextBox < UIControls::BaseControl
TEXT_BOX_X = 2
TEXT_BOX_WIDTH = 172
TEXT_BOX_HEIGHT = 24
TEXT_BOX_PADDING = 4 # Gap between sides of text box and text
TEXT_OFFSET_Y = 7
def initialize(width, height, viewport, value = "")
super(width, height, viewport)
@value = value
@cursor_pos = -1
@display_pos = 0
@cursor_timer = nil
@cursor_shown = false
end
def value=(new_value)
return if @value == new_value
@value = new_value
invalidate
end
def insert_char(ch)
@value.insert(@cursor_pos, ch)
@cursor_pos += 1
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def delete_at(index)
@value.slice!(index)
@cursor_pos -= 1 if @cursor_pos > index
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def cursor_pos=(val)
@cursor_pos = val
reset_display_pos
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def set_interactive_rects
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
[TEXT_BOX_WIDTH, width].min, TEXT_BOX_HEIGHT)
@interactions = {
:text_box => @text_box_rect
}
end
#-----------------------------------------------------------------------------
def busy?
return @cursor_pos >= 0 if @captured_area == :text_box
return super
end
def reset_interaction
@cursor_pos = -1
@display_pos = 0
@cursor_timer = nil
@initial_value = nil
Input.text_input = false
invalidate
end
#-----------------------------------------------------------------------------
def get_cursor_index_from_mouse_position
char_widths = []
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
mouse_x, mouse_y = mouse_pos
mouse_x -= @text_box_rect.x + TEXT_BOX_PADDING
return 0 if mouse_x < 0
(@display_pos...char_widths.length).each do |i|
mouse_x -= char_widths[i]
if mouse_x <= 0
return (mouse_x.abs >= char_widths[i] / 2) ? i : i + 1
end
end
return @value.to_s.length
end
def reset_display_pos
box_width = @text_box_rect.width - (TEXT_BOX_PADDING * 2)
char_widths = []
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
# Text isn't wider than the box
if char_widths.sum <= box_width
return false if @display_pos == 0
@display_pos = 0
return true
end
display_pos_changed = false
# Ensure the cursor hasn't gone off the left side of the text box
if @cursor_pos < @display_pos
@display_pos = @cursor_pos
display_pos_changed = true
end
# Ensure the cursor hasn't gone off the right side of the text box
if @cursor_pos > @display_pos
loop do
cursor_x = 0
(@display_pos...@cursor_pos).each do |i|
cursor_x += char_widths[i] if char_widths[i]
end
break if cursor_x < box_width
@display_pos += 1
display_pos_changed = true
break if @display_pos == @cursor_pos
end
end
# Ensure there isn't empty space on the right if the text can be moved to
# the right to fill it
if @display_pos > 0
cursor_x = 0
(@display_pos...char_widths.length).each do |i|
cursor_x += char_widths[i] if char_widths[i]
end
loop do
cursor_x += char_widths[@display_pos - 1]
break if cursor_x >= box_width
@display_pos -= 1
display_pos_changed = true
break if @display_pos == 0
end
end
return display_pos_changed
end
#-----------------------------------------------------------------------------
def draw_area_highlight
return if @captured_area == :text_box && (@hover_area == @captured_area || !Input.press?(Input::MOUSELEFT))
super
end
def draw_cursor(cursor_x)
return if !@cursor_shown || @cursor_pos < 0
cursor_y_offset = ((height - TEXT_BOX_HEIGHT) / 2) + 2
cursor_height = height - (cursor_y_offset * 2)
bitmap.fill_rect(cursor_x, cursor_y_offset, 2, cursor_height, self.bitmap.font.color)
end
def refresh
super
# Draw text box outline
self.bitmap.outline_rect(@text_box_rect.x, @text_box_rect.y,
@text_box_rect.width, @text_box_rect.height,
self.bitmap.font.color)
# Draw value
char_x = @text_box_rect.x + TEXT_BOX_PADDING
last_char_index = @display_pos
(@value.to_s.length - @display_pos).times do |i|
char = @value.to_s[@display_pos + i]
char_width = self.bitmap.text_size(char).width
cannot_display_next_char = char_x + char_width > @text_box_rect.x + @text_box_rect.width - TEXT_BOX_PADDING
draw_text(self.bitmap, char_x, TEXT_OFFSET_Y, char) if !cannot_display_next_char
# Draw cursor
draw_cursor(char_x - 1) if @display_pos + i == @cursor_pos
break if cannot_display_next_char
last_char_index = @display_pos + i
char_x += char_width
end
# Draw cursor at end
draw_cursor(char_x - 1) if @cursor_pos == @value.to_s.length
# Draw left/right arrows to indicate more text beyond the text box sides
if @display_pos > 0
bitmap.fill_rect(@text_box_rect.x, (height / 2) - 4, 1, 8, Color.white)
5.times do |i|
bitmap.fill_rect(@text_box_rect.x - 2 + i, (height / 2) - (i + 1), 1, 2 * (i + 1), self.bitmap.font.color)
end
end
if last_char_index < @value.to_s.length - 1
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width - 1, (height / 2) - 4, 1, 8, Color.white)
5.times do |i|
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width + 1 - i, (height / 2) - (i + 1), 1, 2 * (i + 1), self.bitmap.font.color)
end
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
super
if @captured_area == :text_box
# Clicked into the text box; put the text cursor in there
@cursor_pos = get_cursor_index_from_mouse_position
@cursor_timer = System.uptime
invalidate
else
set_changed if @initial_value && @value != @initial_value
reset_interaction
end
end
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Start text entry if clicked and released mouse button in the text box
if @captured_area == :text_box
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
@initial_value = @value.clone
Input.text_input = true
invalidate
return # This control is still captured
end
end
# Released mouse button outside of text box, or initially clicked outside of
# text box; end interaction with this control
set_changed if @initial_value && @value != @initial_value
reset_interaction
super # Make this control not busy again
end
def update_special_inputs
# Left/right to move cursor
if Input.triggerex?(:LEFT) || Input.repeatex?(:LEFT)
self.cursor_pos = @cursor_pos - 1 if @cursor_pos > 0
elsif Input.triggerex?(:RIGHT) || Input.repeatex?(:RIGHT)
self.cursor_pos = @cursor_pos + 1 if @cursor_pos < @value.to_s.length
end
# Home/End to jump to start/end of the text
if Input.triggerex?(:HOME) || Input.repeatex?(:HOME)
self.cursor_pos = 0
elsif Input.triggerex?(:END) || Input.repeatex?(:END)
self.cursor_pos = @value.to_s.length
end
# Backspace/Delete to remove text
if Input.triggerex?(:BACKSPACE) || Input.repeatex?(:BACKSPACE)
delete_at(@cursor_pos - 1) if @cursor_pos > 0
elsif Input.triggerex?(:DELETE) || Input.repeatex?(:DELETE)
delete_at(@cursor_pos) if @cursor_pos < @value.to_s.length
end
# Return/Escape to end text input (Escape undoes the change)
if Input.triggerex?(:RETURN) || Input.repeatex?(:RETURN) ||
Input.triggerex?(:KP_ENTER) || Input.repeatex?(:KP_ENTER)
set_changed if @initial_value && @value != @initial_value
reset_interaction
@captured_area = nil
elsif Input.triggerex?(:ESCAPE) || Input.repeatex?(:ESCAPE)
@value = @initial_value if @initial_value
reset_interaction
@captured_area = nil
end
end
def update_text_entry
ret = false
Input.gets.each_char do |ch|
insert_char(ch)
ret = true
end
return ret
end
def update
super
# TODO: Disabled control stuff.
# return if self.disabled
# Make the cursor flash
if @captured_area == :text_box
cursor_to_show = ((System.uptime - @cursor_timer) / 0.35).to_i.even?
if cursor_to_show != @cursor_shown
@cursor_shown = cursor_to_show
invalidate
end
old_cursor_pos = @cursor_pos
# Update cursor movement, deletions and ending text input
update_special_inputs
return if @cursor_pos != old_cursor_pos || !busy?
# Detect character input and add them to @value
char_inserted = update_text_entry
invalidate if reset_display_pos || char_inserted
end
end
end

View File

@@ -0,0 +1,128 @@
#===============================================================================
#
#===============================================================================
class UIControls::Slider < UIControls::BaseControl
attr_reader :min_value
attr_reader :max_value
PLUS_MINUS_SIZE = 16
SLIDER_PADDING = 6 # Gap between sides of interactive area for slider and drawn slider bar
MINUS_X = 0
SLIDER_X = MINUS_X + PLUS_MINUS_SIZE + SLIDER_PADDING
SLIDER_LENGTH = 128
PLUS_X = SLIDER_X + SLIDER_LENGTH + SLIDER_PADDING
VALUE_X = PLUS_X + PLUS_MINUS_SIZE + 5
TEXT_OFFSET_Y = 7
SLIDER_KNOB_COLOR = Color.red
def initialize(width, height, viewport, min_value, max_value, value)
super(width, height, viewport)
@min_value = min_value
@max_value = max_value
self.value = value
end
def value=(new_value)
old_val = @value
@value = new_value.to_i.clamp(self.min_value, self.max_value)
self.invalidate if @value != old_val
end
def min_value=(new_min)
return if new_min == @min_value
@min_value = new_min
@value = @value.clamp(self.min_value, self.max_value)
self.invalidate
end
def max_value=(new_max)
return if new_max == @max_value
@max_value = new_max
@value = @value.clamp(self.min_value, self.max_value)
self.invalidate
end
def set_interactive_rects
@slider_rect = Rect.new(SLIDER_X - SLIDER_PADDING, (self.height - PLUS_MINUS_SIZE) / 2, SLIDER_LENGTH + (SLIDER_PADDING * 2), PLUS_MINUS_SIZE)
@minus_rect = Rect.new(MINUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@plus_rect = Rect.new(PLUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@interactions = {
:slider => @slider_rect,
:minus => @minus_rect,
:plus => @plus_rect
}
end
#-----------------------------------------------------------------------------
def draw_area_highlight
# Don't want to ever highlight the slider with the capture color, because
# the mouse doesn't need to be on the slider to change this control's value
if @captured_area == :slider
rect = @interactions[@captured_area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, HOVER_COLOR) if rect
else
super
end
end
def refresh
super
# Draw minus button
self.bitmap.fill_rect(@minus_rect.x + 2, @minus_rect.y + (@minus_rect.height / 2) - 2, @minus_rect.width - 4, 4, self.bitmap.font.color)
# Draw slider bar
self.bitmap.fill_rect(SLIDER_X, (self.height / 2) - 1, SLIDER_LENGTH, 2, self.bitmap.font.color)
# Draw notches on slider bar
5.times do |i|
self.bitmap.fill_rect(SLIDER_X - 1 + (i * SLIDER_LENGTH / 4), (self.height / 2) - 2, 2, 4, self.bitmap.font.color)
end
# Draw slider knob
fraction = (self.value - self.min_value) / self.max_value.to_f
knob_x = (SLIDER_LENGTH * fraction).to_i
self.bitmap.fill_rect(SLIDER_X + knob_x - 4, (self.height / 2) - 6, 8, 12, SLIDER_KNOB_COLOR)
# Draw plus button
self.bitmap.fill_rect(@plus_rect.x + 2, @plus_rect.y + (@plus_rect.height / 2) - 2, @plus_rect.width - 4, 4, self.bitmap.font.color)
self.bitmap.fill_rect(@plus_rect.x + (@plus_rect.width / 2) - 2, @plus_rect.y + 2, 4, @plus_rect.height - 4, self.bitmap.font.color)
# Draw value text
draw_text(self.bitmap, VALUE_X, TEXT_OFFSET_Y, self.value.to_s)
end
#-----------------------------------------------------------------------------
def on_mouse_press
super
@initial_value = @value if @captured_area
end
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
set_changed if @initial_value && @value != @initial_value
@initial_value = nil
super
end
def update
super
# TODO: Disabled control stuff.
# return if self.disabled
case @captured_area
when :minus
# Constant decrement of value while pressing the minus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value -= 1
end
when :plus
# Constant incrementing of value while pressing the plus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value += 1
end
when :slider
# Constant updating of value depending on mouse's x position
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
self.value = lerp(self.min_value, self.max_value + (self.max_value & 1), SLIDER_LENGTH, mouse_x - SLIDER_X)
end
end
end

View File

@@ -0,0 +1,139 @@
#===============================================================================
#
#===============================================================================
class UIControls::ValueBox < UIControls::TextBox
attr_reader :min_value
attr_reader :max_value
PLUS_MINUS_SIZE = 16
CONTROL_PADDING = 2 # Gap between buttons and text box
MINUS_X = 0
TEXT_BOX_X = MINUS_X + PLUS_MINUS_SIZE + CONTROL_PADDING
TEXT_BOX_WIDTH = 64
TEXT_BOX_HEIGHT = 24
PLUS_X = TEXT_BOX_X + TEXT_BOX_WIDTH + CONTROL_PADDING
def initialize(width, height, viewport, min_value, max_value, value)
super(width, height, viewport, value)
@min_value = min_value
@max_value = max_value
self.value = value
end
def value=(new_value)
old_val = @value
@value = new_value.to_i.clamp(self.min_value, self.max_value)
self.invalidate if @value != old_val
end
def min_value=(new_min)
return if new_min == @min_value
@min_value = new_min
@value = @value.clamp(self.min_value, self.max_value)
self.invalidate
end
def max_value=(new_max)
return if new_max == @max_value
@max_value = new_max
@value = @value.clamp(self.min_value, self.max_value)
self.invalidate
end
# TODO: If current value is 0, replace it with ch instead of inserting ch?
def insert_char(ch)
self.value = @value.to_s.insert(@cursor_pos, ch).to_i
@cursor_pos += 1
@cursor_pos = @cursor_pos.clamp(0, @value.to_s.length)
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def delete_at(index)
new_val = @value.to_s
new_val.slice!(index)
self.value = new_val.to_i
@cursor_pos -= 1 if @cursor_pos > index
@cursor_pos = @cursor_pos.clamp(0, @value.to_s.length)
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def set_interactive_rects
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
TEXT_BOX_WIDTH, TEXT_BOX_HEIGHT)
@minus_rect = Rect.new(MINUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@plus_rect = Rect.new(PLUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@interactions = {
:text_box => @text_box_rect,
:minus => @minus_rect,
:plus => @plus_rect
}
end
#-----------------------------------------------------------------------------
def refresh
super
# Draw minus button
self.bitmap.fill_rect(@minus_rect.x + 2, @minus_rect.y + (@minus_rect.height / 2) - 2, @minus_rect.width - 4, 4, self.bitmap.font.color)
# Draw plus button
self.bitmap.fill_rect(@plus_rect.x + 2, @plus_rect.y + (@plus_rect.height / 2) - 2, @plus_rect.width - 4, 4, self.bitmap.font.color)
self.bitmap.fill_rect(@plus_rect.x + (@plus_rect.width / 2) - 2, @plus_rect.y + 2, 4, @plus_rect.height - 4, self.bitmap.font.color)
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
super
if @captured_area == :text_box
# Clicked into the text box; put the text cursor in there
@cursor_pos = get_cursor_index_from_mouse_position
@cursor_timer = System.uptime
invalidate
elsif @captured_area
@initial_value = @value
else
set_changed if @initial_value && @value != @initial_value
reset_interaction
end
end
def update_text_entry
ret = false
Input.gets.each_char do |ch|
next if !["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-"].include?(ch)
if ch == "-"
next if @min_value >= 0 || @cursor_pos > 1 || (@cursor_pos > 0 && @value >= 0)
if @value < 0
delete_at(0) # Remove the negative sign
ret = true
next
end
end
insert_char(ch)
ret = true
end
return ret
end
def update
super
case @captured_area
when :minus
# Constant decrement of value while pressing the minus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value -= 1
end
when :plus
# Constant incrementing of value while pressing the plus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value += 1
end
end
end
end

View File

@@ -0,0 +1,50 @@
#===============================================================================
#
#===============================================================================
class UIControls::Button < UIControls::BaseControl
BUTTON_X = 2
BUTTON_PADDING = 10
BUTTON_HEIGHT = 28
TEXT_OFFSET_Y = 7
def initialize(width, height, viewport, text = "")
super(width, height, viewport)
@text = text
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)
@interactions = {
:button => @button_rect
}
end
#-----------------------------------------------------------------------------
# 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)
# Draw button text
draw_text(self.bitmap, BUTTON_X + BUTTON_PADDING, TEXT_OFFSET_Y, @text)
end
#-----------------------------------------------------------------------------
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Change this control's value
if @captured_area == :button
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
set_changed
end
end
super # Make this control not busy again
end
end

View File

@@ -0,0 +1,13 @@
#===============================================================================
# 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.
#===============================================================================
class UIControls::List < UIControls::BaseControl
end

View File

@@ -0,0 +1,9 @@
#===============================================================================
# TODO
#===============================================================================
class UIControls::DropdownList < UIControls::BaseControl
def initialize(width, height, viewport, options, value)
# NOTE: options is a hash: keys are symbols, values are display names.
super(width, height, viewport)
end
end

View File

@@ -0,0 +1,161 @@
#===============================================================================
# Controls are arranged in a list in self's bitmap. Each control is given a
# "self's bitmap's width" x LINE_SPACING area of self's bitmap to draw itself
# in.
# TODO: The act of "capturing" a control makes other controls in this container
# not update themselves, i.e. they won't colour themselves with a hover
# highlight if the mouse happens to move over it while another control is
# captured. Is there a better way of dealing with this? I'm leaning
# towards the control itself deciding if it's captured, and it being
# treated as uncaptured once it says its value has changed, but I think
# this would require manually telling all other controls in this container
# that something else is captured and they shouldn't show a hover
# highlight when updated (perhaps as a parameter in def update), which I
# don't think is ideal. Mark self as "busy" while a control is captured.
#===============================================================================
class UIControls::ControlsContainer
attr_reader :x, :y
attr_reader :controls
LINE_SPACING = 32
OFFSET_FROM_LABEL_X = 80
OFFSET_FROM_LABEL_Y = 0
def initialize(x, y, width, height)
@viewport = Viewport.new(x, y, width, height)
@viewport.z = 99999
@x = x
@y = y
@width = width
@height = height
@controls = []
@control_rects = []
@row_count = 0
@captured = nil
end
def dispose
@controls.each { |c| c[1]&.dispose }
@controls.clear
@viewport.dispose
end
#-----------------------------------------------------------------------------
def add_label(id, label, has_label = false)
id = (id.to_s + "_label").to_sym
add_control(id, UIControls::Label.new(*control_size(has_label), @viewport, label), has_label)
end
def add_checkbox(id, value, has_label = false)
add_control(id, UIControls::Checkbox.new(*control_size(has_label), @viewport, value), has_label)
end
def add_labelled_checkbox(id, label, value)
add_label(id, label)
add_checkbox(id, value, true)
end
def add_text_box(id, value, has_label = false)
add_control(id, UIControls::TextBox.new(*control_size(has_label), @viewport, value), has_label)
end
def add_labelled_text_box(id, label, value)
add_label(id, label)
add_text_box(id, value, true)
end
def add_slider(id, min_value, max_value, value, has_label = false)
add_control(id, UIControls::Slider.new(*control_size(has_label), @viewport, min_value, max_value, value), has_label)
end
def add_labelled_slider(id, label, min_value, max_value, value)
add_label(id, label)
add_slider(id, min_value, max_value, value, true)
end
def add_value_box(id, min_value, max_value, value, has_label = false)
add_control(id, UIControls::ValueBox.new(*control_size(has_label), @viewport, min_value, max_value, value), has_label)
end
def add_labelled_value_box(id, label, min_value, max_value, value)
add_label(id, label)
add_value_box(id, min_value, max_value, value, true)
end
def add_button(id, button_text, has_label = false)
add_control(id, UIControls::Button.new(*control_size(has_label), @viewport, button_text), has_label)
end
def add_labelled_button(id, label, button_text)
add_label(id, label)
add_button(id, button_text, true)
end
def add_dropdown_list(id, options, value, has_label = false)
add_control(id, UIControls::DropdownList.new(*control_size(has_label), @viewport, options, value), has_label)
end
def add_labelled_dropdown_list(id, label, options, value)
add_label(id, label)
add_dropdown_list(id, options, value, true)
end
#-----------------------------------------------------------------------------
def repaint
@controls.each { |ctrl| ctrl[1].repaint }
end
#-----------------------------------------------------------------------------
def update
# Update controls
if @captured
# TODO: Ideally all controls will be updated here, if only to redraw
# themselves if they happen to be invalidated somehow. But that
# involves telling each control whether any other control is busy,
# to ensure that they don't show their hover colours or anything,
# which is fiddly and I'm not sure if it's the best approach.
@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 updated controls
@controls.each do |ctrl|
next if !ctrl[1].changed?
# TODO: Get the new value from ctrl and put it in a hash for the main
# editor class to notice and use.
ctrl[1].clear_changed
end
# Redraw controls if needed
repaint
end
#-----------------------------------------------------------------------------
private
def control_size(has_label = false)
if has_label
return @width - OFFSET_FROM_LABEL_X, LINE_SPACING - OFFSET_FROM_LABEL_Y
end
return @width, LINE_SPACING
end
def add_control(id, control, add_offset = false)
i = @controls.length
control_y = (add_offset ? @row_count - 1 : @row_count) * LINE_SPACING
@control_rects[i] = Rect.new(0, control_y, control.width, control.height)
control.x = @control_rects[i].x + (add_offset ? OFFSET_FROM_LABEL_X : 0)
control.y = @control_rects[i].y + (add_offset ? OFFSET_FROM_LABEL_Y : 0)
control.set_interactive_rects
@controls[i] = [id, control]
@row_count += 1 if !add_offset
repaint
end
end

View File

@@ -0,0 +1,97 @@
# TODO: Come up with a better name for this class. I'm not sure I want to merge
# this class with the editor class.
class AnimationEditorLoadScreen
WINDOW_WIDTH = Settings::SCREEN_WIDTH + (32 * 10)
WINDOW_HEIGHT = Settings::SCREEN_HEIGHT + (32 * 10)
ANIMATIONS_LIST_X = 4
ANIMATIONS_LIST_Y = 4
ANIMATIONS_LIST_WIDTH = 300
ANIMATIONS_LIST_HEIGHT = WINDOW_HEIGHT - (ANIMATIONS_LIST_Y * 2)
def initialize
@viewport = Viewport.new(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT)
@viewport.z = 99999
@screen_bitmap = BitmapSprite.new(AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, @viewport)
draw_editor_background
end
def dispose
@screen_bitmap.dispose
@viewport.dispose
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
)
# 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
)
end
def update
# TODO: Update the controls (animations list, Load button, etc.).
end
def run
Input.text_input = false
loop do
inputting_text = Input.text_input
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.
screen = AnimationEditor.new
screen.run
end
end
dispose
end
end
#===============================================================================
# Start
#===============================================================================
def test_anim_editor
Graphics.resize_screen(AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT)
pbSetResizeFactor(1)
screen = AnimationEditorLoadScreen.new
screen.run
Graphics.resize_screen(Settings::SCREEN_WIDTH, Settings::SCREEN_HEIGHT)
pbSetResizeFactor($PokemonSystem.screensize)
$game_map&.autoplay
end
#===============================================================================
# Add to Debug menu
#===============================================================================
MenuHandlers.add(:debug_menu, :use_pc, {
"name" => "Test new animation editor",
"parent" => :main,
"description" => "Test new animation editor",
"effect" => proc {
test_anim_editor
}
})

View File

@@ -0,0 +1,115 @@
# TODO: Should I split this code into visual and mechanical classes, a la the
# other UI screens?
#===============================================================================
# TODO: Need a way to recognise when text is being input into something
# (Input.text_input) and disable all keyboard shortcuts if so. If only
# this class has keyboard shortcuts in it, then it should be okay already.
#===============================================================================
class AnimationEditor
WINDOW_WIDTH = AnimationEditorLoadScreen::WINDOW_WIDTH
WINDOW_HEIGHT = AnimationEditorLoadScreen::WINDOW_HEIGHT
CANVAS_X = 4
CANVAS_Y = 32 + 4
CANVAS_WIDTH = Settings::SCREEN_WIDTH
CANVAS_HEIGHT = Settings::SCREEN_HEIGHT
SIDE_PANEL_X = CANVAS_X + CANVAS_WIDTH + 4 + 4
SIDE_PANEL_Y = CANVAS_Y
SIDE_PANEL_WIDTH = WINDOW_WIDTH - SIDE_PANEL_X - 4
SIDE_PANEL_HEIGHT = CANVAS_HEIGHT + (32 * 2)
# TODO: Add a parameter which is the animation to be edited, and also a
# parameter for that animation's ID in GameData (just for the sake of
# saving changes over the same GameData slot).
def initialize
@viewport = Viewport.new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT)
@viewport.z = 99999
@screen_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @viewport)
draw_editor_background
# Canvas
@canvas = Sprite.new(@viewport)
@canvas.x = CANVAS_X
@canvas.y = CANVAS_Y
@canvas.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", "field_bg")
# Side pane
@side_pane = ControlPane.new(SIDE_PANEL_X, SIDE_PANEL_Y, SIDE_PANEL_WIDTH, SIDE_PANEL_HEIGHT)
set_side_panel_contents
end
def dispose
@screen_bitmap.dispose
@canvas.dispose
@side_pane.dispose
@viewport.dispose
end
def draw_editor_background
# Fill the whole screen with black
@screen_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, Color.black)
# Outline around canvas
@screen_bitmap.bitmap.outline_rect(CANVAS_X - 3, CANVAS_Y - 3, CANVAS_WIDTH + 6, CANVAS_HEIGHT + 6, Color.white)
@screen_bitmap.bitmap.outline_rect(CANVAS_X - 2, CANVAS_Y - 2, CANVAS_WIDTH + 4, CANVAS_HEIGHT + 4, Color.black)
@screen_bitmap.bitmap.outline_rect(CANVAS_X - 1, CANVAS_Y - 1, CANVAS_WIDTH + 2, CANVAS_HEIGHT + 2, Color.white)
# Outline around side panel
@screen_bitmap.bitmap.outline_rect(SIDE_PANEL_X - 3, SIDE_PANEL_Y - 3, SIDE_PANEL_WIDTH + 6, SIDE_PANEL_HEIGHT + 6, Color.white)
@screen_bitmap.bitmap.outline_rect(SIDE_PANEL_X - 2, SIDE_PANEL_Y - 2, SIDE_PANEL_WIDTH + 4, SIDE_PANEL_HEIGHT + 4, Color.black)
@screen_bitmap.bitmap.outline_rect(SIDE_PANEL_X - 1, SIDE_PANEL_Y - 1, SIDE_PANEL_WIDTH + 2, SIDE_PANEL_HEIGHT + 2, Color.white)
# Fill the side panel with white
@screen_bitmap.bitmap.fill_rect(SIDE_PANEL_X, SIDE_PANEL_Y, SIDE_PANEL_WIDTH, SIDE_PANEL_HEIGHT, Color.white)
end
def set_side_panel_contents
@side_pane.add_labelled_text_box(:name, "Name", "Untitled")
@side_pane.add_labelled_value_box(:x, "X", -128, CANVAS_WIDTH + 128, 64)
@side_pane.add_labelled_value_box(:y, "Y", -128, CANVAS_HEIGHT + 128, 96)
@side_pane.add_labelled_value_box(:zoom_x, "Zoom X", 0, 1000, 100)
@side_pane.add_labelled_value_box(:zoom_y, "Zoom Y", 0, 1000, 100)
@side_pane.add_labelled_value_box(:angle, "Angle", -1080, 1080, 0)
@side_pane.add_labelled_checkbox(:visible, "Visible", true)
@side_pane.add_labelled_slider(:opacity, "Opacity", 0, 255, 255)
@side_pane.add_labelled_checkbox(:flip, "Flip", false)
@side_pane.add_labelled_dropdown_list(:priority, "Priority", { # TODO: Include sub-priority.
:behind_all => "Behind all",
:behind_user => "Behind user",
:above_user => "In front of user",
:above_all => "In front of everything"
}, :above_user)
# @side_pane.add_labelled_dropdown_list(:focus, "Focus", {
# :user => "User",
# :target => "Target",
# :user_and_target => "User and target",
# :screen => "Screen"
# }, :user)
@side_pane.add_labelled_button(:color, "Color/tone", "Edit")
@side_pane.add_labelled_button(:graphic, "Graphic", "Change")
end
def update
@canvas.update
@side_pane.update
# TODO: Check @side_pane for whether it's changed. Note that it includes
# buttons which won't themselves have a value but will flag themselves
# as changed when clicked; code here should determine what happens if
# a button is pressed (unless I put said code in a proc passed to the
# button control; said code will be lengthy).
end
def run
Input.text_input = false
loop do
inputting_text = Input.text_input
Graphics.update
Input.update
update
if !inputting_text
if Input.trigger?(Input::BACK)
# TODO: Ask to save/discard changes.
# TODO: When saving, add animation to GameData and rewrite animation's
# parent PBS file (which could include multiple animations).
break
end
end
end
dispose
end
end

View File

@@ -0,0 +1,12 @@
#===============================================================================
#
#===============================================================================
class AnimationEditor::ControlPane < UIControls::ControlsContainer
def on_control_release
# TODO: Update data for @captured control, because it may have changed.
# Gather data from all controls in this container and put them in a
# hash; it's up to the main editor screen to notice/read it, edit
# animation data accordingly, and then tell this container to nil that
# hash again.
end
end