Files
infinitefusion-e18/Data/Scripts/016b_UI redesign/000_UI_base.rb

842 lines
27 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module UI
#=============================================================================
# The visuals class.
#=============================================================================
module SpriteContainerMixin
UI_FOLDER = "Graphics/UI/"
GRAPHICS_FOLDER = "" # Subfolder in UI_FOLDER
TEXT_COLOR_THEMES = { # These color themes are added to @sprites[:overlay]
:default => [Color.new(72, 72, 72), Color.new(160, 160, 160)] # Base and shadow colour
}
def add_overlay(overlay, overlay_width = -1, overlay_height = -1)
overlay_width = Graphics.width if overlay_width < 0
overlay_height = Graphics.height if overlay_height < 0
@sprites[overlay] = BitmapSprite.new(overlay_width, overlay_height, @viewport)
@sprites[overlay].z = 1000
self.class::TEXT_COLOR_THEMES.each_pair { |key, values| @sprites[overlay].add_text_theme(key, *values) }
pbSetSystemFont(@sprites[overlay].bitmap)
end
def add_icon_sprite(key, x, y, filename = nil)
@sprites[key] = IconSprite.new(x, y, @viewport)
@sprites[key].setBitmap(filename) if filename
end
def add_animated_arrow(key, x, y, direction)
case direction
when :up
@sprites[key] = AnimatedSprite.new(UI_FOLDER + "up_arrow", 8, 28, 40, 2, @viewport)
when :down
@sprites[key] = AnimatedSprite.new(UI_FOLDER + "down_arrow", 8, 28, 40, 2, @viewport)
when :left
@sprites[key] = AnimatedSprite.new(UI_FOLDER + "left_arrow", 8, 40, 28, 2, @viewport)
when :right
@sprites[key] = AnimatedSprite.new(UI_FOLDER + "right_arrow", 8, 40, 28, 2, @viewport)
end
@sprites[key].x = x
@sprites[key].y = y
@sprites[key].visible = false
@sprites[key].play
end
#---------------------------------------------------------------------------
def dispose
@sprites.each_value { |s| s.dispose if s && !s.disposed? }
@sprites.clear
@bitmaps.each_value { |b| b.dispose if b && !b.disposed? }
@bitmaps.clear
@disposed = true
end
def disposed?
return !!@disposed
end
#---------------------------------------------------------------------------
def graphics_folder
return UI_FOLDER + self.class::GRAPHICS_FOLDER
end
def background_filename
return gendered_filename(self.class::BACKGROUND_FILENAME)
end
def gendered_filename(base_filename)
return filename_with_appendix(base_filename, "_f") if $player&.female?
return base_filename
end
def filename_with_appendix(base_filename, appendix)
if appendix && appendix != ""
trial_filename = base_filename + appendix
return trial_filename if pbResolveBitmap(graphics_folder + trial_filename)
end
return base_filename
end
#---------------------------------------------------------------------------
# NOTE: max_width should include the width of the text shadow at the end of
# the string (because characters in the font have a blank 2 pixels
# after them for the shadow to occupy).
def crop_text(string, max_width, continue_string = "", overlay: :overlay)
return string if max_width <= 0
return string if @sprites[overlay].bitmap.text_size(string).width <= max_width
ret = string
continue_width = @sprites[overlay].bitmap.text_size(continue_string).width
loop do
ret = ret[0...-1]
break if @sprites[overlay].bitmap.text_size(ret).width <= max_width - continue_width
end
ret += continue_string
return ret
end
#---------------------------------------------------------------------------
def draw_text(string, text_x, text_y, align: :left, theme: :default, outline: :shadow, overlay: :overlay)
@sprites[overlay].draw_themed_text(string.to_s, text_x, text_y, align, theme, outline)
end
def draw_paragraph_text(string, text_x, text_y, text_width, num_lines, theme: :default, overlay: :overlay)
drawTextEx(@sprites[overlay].bitmap, text_x, text_y, text_width, num_lines,
string, *self.class::TEXT_COLOR_THEMES[theme])
end
# NOTE: This also draws string in a paragraph, but with no limit on the
# number of lines.
def draw_formatted_text(string, text_x, text_y, text_width, theme: :default, overlay: :overlay)
drawFormattedTextEx(@sprites[overlay].bitmap, text_x, text_y, text_width,
string, *self.class::TEXT_COLOR_THEMES[theme])
end
def draw_image(filename, image_x, image_y, src_x = 0, src_y = 0, src_width = -1, src_height = -1, overlay: :overlay)
@sprites[overlay].draw_image(filename, image_x, image_y, src_x, src_y, src_width, src_height)
end
# The image is assumed to be the digits 0-9 and then a "/", all the same
# width, in a horizontal row.
def draw_number_from_image(bitmap, string, text_x, text_y, align: :left, overlay: :overlay)
string = string.to_s
raise _INTL("Can't draw {1} as a number.", string) if !string.scan(/[^\d\/]/).empty?
char_width = bitmap.width / 11
char_height = bitmap.height
chars = string.split(//)
chars.reverse! if align == :right
chars.length.times do |i|
char = chars[i]
index = (char == "/") ? 10 : char.to_i
char_x = (align == :right) ? text_x - ((i + 1) * char_width) : text_x + (i * char_width)
draw_image(bitmap, char_x, text_y,
index * char_width, 0, char_width, char_height, overlay: overlay)
end
end
SLIDER_COORDS = { # Size of elements in slider graphic
:arrow_size => [24, 28],
:box_heights => [4, 8, 18] # Heights of top, middle and bottom segments of slider box
}
# slider_height includes the heights of the arrows at either end.
def draw_slider(bitmap, slider_x, slider_y, slider_height, visible_top, visible_height, total_height, hide_if_inactive: :false, overlay: :overlay)
coords = self.class::SLIDER_COORDS
bar_y = slider_y + coords[:arrow_size][1]
bar_height = slider_height - (2 * coords[:arrow_size][1])
bar_segment_height = coords[:box_heights].sum # Also minimum height of slider box
show_up_arrow = (visible_top > 0)
show_down_arrow = (visible_top + visible_height < total_height)
return if hide_if_inactive && !show_up_arrow && !show_down_arrow
# Draw up arrow
x_offset = (show_up_arrow) ? coords[:arrow_size][0] : 0
draw_image(bitmap, slider_x, slider_y,
x_offset, 0, *coords[:arrow_size], overlay: overlay)
# Draw down arrow
x_offset = (show_down_arrow) ? coords[:arrow_size][0] : 0
draw_image(bitmap, slider_x, slider_y + slider_height - coords[:arrow_size][1],
x_offset, coords[:arrow_size][1] + bar_segment_height, *coords[:arrow_size], overlay: overlay)
# Draw bar background
iterations = (bar_height / bar_segment_height.to_f).ceil
iterations.times do |i|
segment_y = bar_y + (i * bar_segment_height)
iteration_height = bar_segment_height
iteration_height = bar_height - (i * bar_segment_height) if i == iterations - 1 # Last part
draw_image(bitmap, slider_x, segment_y,
0, coords[:arrow_size][1], coords[:arrow_size][0], iteration_height, overlay: overlay)
end
# Draw slider box
if show_up_arrow || show_down_arrow
box_height = (bar_height * visible_height / total_height).floor
box_height += [(bar_height - box_height) / 2, bar_height / 6].min # Make it bigger than expected
box_height = [box_height.floor, bar_segment_height].max
box_y = bar_y
box_y += ((bar_height - box_height) * visible_top / (total_height - visible_height)).floor
# Draw slider box top
draw_image(bitmap, slider_x, box_y,
coords[:arrow_size][0], coords[:arrow_size][1],
coords[:arrow_size][0], coords[:box_heights][0], overlay: overlay)
# Draw slider box middle
middle_height = box_height - coords[:box_heights][0] - coords[:box_heights][2]
iterations = (middle_height / coords[:box_heights][1].to_f).ceil
iterations.times do |i|
segment_y = box_y + coords[:box_heights][0] + (i * coords[:box_heights][1])
iteration_height = coords[:box_heights][1]
iteration_height = middle_height - (i * coords[:box_heights][1]) if i == iterations - 1 # Last part
draw_image(bitmap, slider_x, segment_y,
coords[:arrow_size][0], coords[:arrow_size][1] + coords[:box_heights][0],
coords[:arrow_size][0], iteration_height, overlay: overlay)
end
# Draw slider box bottom
draw_image(bitmap, slider_x, box_y + box_height - coords[:box_heights][2],
coords[:arrow_size][0], coords[:arrow_size][1] + coords[:box_heights][0] + coords[:box_heights][1],
coords[:arrow_size][0], coords[:box_heights][2], overlay: overlay)
end
end
#---------------------------------------------------------------------------
# Redraw everything on the screen.
def refresh
refresh_overlay
end
def refresh_overlay
@sprites[:overlay].bitmap.clear if @sprites[:overlay]
end
#---------------------------------------------------------------------------
def update_visuals
pbUpdateSpriteHash(@sprites)
end
def update
update_visuals
end
end
#=============================================================================
# The visuals class.
#=============================================================================
class SpriteContainer
attr_reader :x, :y, :z, :visible, :color
include SpriteContainerMixin
def initialize(viewport)
@viewport = viewport
@x ||= 0
@y ||= 0
@z ||= 0
@visible = true
@color = Color.new(0, 0, 0, 0)
@bitmaps = {}
@sprites = {}
@sprites_values = {}
initialize_bitmaps
initialize_sprites
refresh_sprites_values
end
def initialize_bitmaps
end
def initialize_sprites
end
#---------------------------------------------------------------------------
# x, y, z, visible, opacity, color
def x=(value)
@x = value
@sprites.each_pair do |key, sprite|
sprite.x = @x + @sprites_values[key][:x]
end
end
def y=(value)
@y = value
@sprites.each_pair do |key, sprite|
sprite.y = @y + @sprites_values[key][:y]
end
end
def z=(value)
@z = value
@sprites.each_pair do |key, sprite|
sprite.z = @z + @sprites_values[key][:z]
end
end
def visible=(value)
@visible = value
@sprites.each_pair do |key, sprite|
sprite.visible = @visible && @sprites_values[key][:visible]
end
end
def color=(value)
@color = value
@sprites.each_pair do |key, sprite|
sprite.color = @color
end
end
#---------------------------------------------------------------------------
def record_values(key)
@sprites_values[key] ||= {}
@sprites_values[key][:x] = @sprites[key].x
@sprites_values[key][:y] = @sprites[key].y
@sprites_values[key][:z] = @sprites[key].z
@sprites_values[key][:visible] = @sprites[key].visible
end
def refresh_sprites_values
self.x = @x
self.y = @y
self.z = @z
self.visible = @visible
end
end
#=============================================================================
# The visuals class.
#=============================================================================
class BaseVisuals
attr_reader :sprites
BACKGROUND_FILENAME = "bg"
include SpriteContainerMixin
def initialize
@bitmaps = {}
@sprites = {}
initialize_viewport
initialize_bitmaps
initialize_background
initialize_overlay
initialize_message_box
initialize_sprites
refresh
end
def initialize_viewport
@viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
@viewport.z = 99999
end
def initialize_bitmaps
end
def initialize_background
addBackgroundPlane(@sprites, :background, self.class::GRAPHICS_FOLDER + background_filename, @viewport)
@sprites[:background].z = -1000
end
def initialize_overlay
add_overlay(:overlay)
end
def initialize_message_box
# TODO: It looks like :message_box isn't used anywhere.
@sprites[:message_box] = Window_AdvancedTextPokemon.new("")
@sprites[:message_box].viewport = @viewport
@sprites[:message_box].z = 2000
@sprites[:message_box].visible = false
@sprites[:message_box].letterbyletter = true
pbBottomLeftLines(@sprites[:message_box], 2)
@sprites[:speech_box] = Window_AdvancedTextPokemon.new("")
@sprites[:speech_box].viewport = @viewport
@sprites[:speech_box].z = 2001
@sprites[:speech_box].visible = false
@sprites[:speech_box].letterbyletter = true
@sprites[:speech_box].setSkin(MessageConfig.pbGetSpeechFrame)
pbBottomLeftLines(@sprites[:speech_box], 2)
end
def initialize_sprites
end
#---------------------------------------------------------------------------
def fade_in
# TODO: pbFadeInAndShow changes the colour of all sprites. Make it instead
# change the opacity of @viewport like def pbFadeOutIn.
pbFadeInAndShow(@sprites)
end
def fade_out
# TODO: pbFadeOutAndHide changes the colour of all sprites. Make it
# instead change the opacity of @viewport like def pbFadeOutIn.
pbFadeOutAndHide(@sprites)
end
def dispose
super
@viewport.dispose
end
#---------------------------------------------------------------------------
def index
return 0
end
#---------------------------------------------------------------------------
def position_speech_box(text = "")
pbBottomLeftLines(@sprites[:speech_box], 2)
end
def show_message(text)
@sprites[:speech_box].visible = true
@sprites[:speech_box].text = text
position_speech_box(text)
yielded = false
loop do
Graphics.update
Input.update
update_visuals
if @sprites[:speech_box].busy?
if Input.trigger?(Input::USE)
pbPlayDecisionSE if @sprites[:speech_box].pausing?
@sprites[:speech_box].resume
end
else
yield if !yielded && block_given?
yielded = true
if Input.trigger?(Input::USE) || Input.trigger?(Input::BACK)
break
end
end
end
@sprites[:speech_box].visible = false
end
def show_confirm_message(text)
ret = false
@sprites[:speech_box].visible = true
@sprites[:speech_box].text = text
position_speech_box(text)
using(cmd_window = Window_CommandPokemon.new([_INTL("Yes"), _INTL("No")])) do
cmd_window.z = @viewport.z + 1
cmd_window.visible = false
pbBottomRight(cmd_window)
cmd_window.y -= @sprites[:speech_box].height
cmd_window.visible = true if !@sprites[:speech_box].busy?
loop do
Graphics.update
Input.update
update_visuals
cmd_window.visible = true if !@sprites[:speech_box].busy?
cmd_window.update
if !@sprites[:speech_box].busy?
if Input.trigger?(Input::BACK)
pbPlayCancelSE
ret = false
break
elsif Input.trigger?(Input::USE) && @sprites[:speech_box].resume
pbPlayDecisionSE
ret = (cmd_window.index == 0)
break
end
end
end
end
@sprites[:speech_box].visible = false
return ret
end
def show_confirm_serious_message(text)
ret = false
@sprites[:speech_box].visible = true
@sprites[:speech_box].text = text
position_speech_box(text)
using(cmd_window = Window_CommandPokemon.new([_INTL("No"), _INTL("Yes")])) do
cmd_window.z = @viewport.z + 1
cmd_window.visible = false
pbBottomRight(cmd_window)
cmd_window.y -= @sprites[:speech_box].height
cmd_window.visible = true if !@sprites[:speech_box].busy?
loop do
Graphics.update
Input.update
update_visuals
cmd_window.visible = true if !@sprites[:speech_box].busy?
cmd_window.update
if !@sprites[:speech_box].busy?
if Input.trigger?(Input::BACK)
pbPlayCancelSE
ret = false
break
elsif Input.trigger?(Input::USE) && @sprites[:speech_box].resume
pbPlayDecisionSE
ret = (cmd_window.index == 1)
break
end
end
end
end
@sprites[:speech_box].visible = false
return ret
end
# Used for dialogue.
# align: Where the command window is in relation to the message window.
# :horizontal is side by side, :vertical is command window above.
def show_choice_message(text, options, index = 0, align: :vertical, cmd_side: :right)
ret = -1
commands = options
commands = options.values if options.is_a?(Hash)
@sprites[:speech_box].visible = true
@sprites[:speech_box].text = text
using(cmd_window = Window_AdvancedCommandPokemon.new(commands)) do
if align == :vertical
@sprites[:speech_box].resizeHeightToFit(text, Graphics.width)
else
@sprites[:speech_box].resizeHeightToFit(text, Graphics.width - cmd_window.width)
end
cmd_window.z = @viewport.z + 1
cmd_window.visible = false
cmd_window.index = index
if cmd_side == :right
pbBottomLeft(@sprites[:speech_box])
pbBottomRight(cmd_window)
else
pbBottomRight(@sprites[:speech_box])
pbBottomLeft(cmd_window)
end
if align == :vertical
cmd_window.height = [cmd_window.height, Graphics.height - @sprites[:speech_box].height].min
cmd_window.y = Graphics.height - @sprites[:speech_box].height - cmd_window.height
end
cmd_window.visible = true if !@sprites[:speech_box].busy?
loop do
Graphics.update
Input.update
update_visuals
cmd_window.visible = true if !@sprites[:speech_box].busy?
cmd_window.update
if !@sprites[:speech_box].busy?
if Input.trigger?(Input::BACK)
pbPlayCancelSE
ret = -1
break
elsif Input.trigger?(Input::USE) && @sprites[:speech_box].resume
pbPlayDecisionSE
ret = cmd_window.index
break
end
end
end
end
@sprites[:speech_box].visible = false
if options.is_a?(Hash)
ret = (ret < 0) ? nil : options.keys[ret]
end
return ret
end
def show_menu(text, options, index = 0, cmd_side: :right)
old_letter_by_letter = @sprites[:speech_box].letterbyletter
@sprites[:speech_box].letterbyletter = false
ret = show_choice_message(text, options, index, align: :horizontal, cmd_side: cmd_side)
@sprites[:speech_box].letterbyletter = old_letter_by_letter
return ret
end
def show_choice(options, index = 0)
ret = -1
commands = options
commands = options.values if options.is_a?(Hash)
using(cmd_window = Window_AdvancedCommandPokemon.new(commands)) do
cmd_window.z = @viewport.z + 1
cmd_window.index = index
pbBottomRight(cmd_window)
loop do
Graphics.update
Input.update
update_visuals
cmd_window.update
if Input.trigger?(Input::BACK)
pbPlayCancelSE
ret = -1
break
elsif Input.trigger?(Input::USE)
pbPlayDecisionSE
ret = cmd_window.index
break
end
end
end
ret = options.keys[ret] if options.is_a?(Hash)
return ret
end
# TODO: Rewrite this.
def choose_number(help_text, maximum, init_value = 1)
if maximum.is_a?(ChooseNumberParams)
return pbMessageChooseNumber(help_text, maximum) { update_visuals }
end
return UIHelper.pbChooseNumber(@sprites[:speech_box], help_text, maximum, init_value) { update_visuals }
end
def choose_number_as_money_multiplier(help_text, money_per_unit, maximum, init_value = 1)
@sprites[:speech_box].visible = true
@sprites[:speech_box].text = help_text
position_speech_box(text)
# Show the help text
loop do
Graphics.update
Input.update
update_visuals
if @sprites[:speech_box].busy?
if Input.trigger?(Input::USE)
pbPlayDecisionSE if @sprites[:speech_box].pausing?
@sprites[:speech_box].resume
end
else
break
end
end
# Choose a quantity
item_price = money_per_unit
quantity = init_value
using(num_window = Window_AdvancedTextPokemon.newWithSize(
_INTL("×{1}<r>${2}", quantity, (quantity * item_price).to_s_formatted),
0, 0, 224, 64, @viewport)) do
num_window.z = 2000
num_window.visible = true
num_window.letterbyletter = false
pbBottomRight(num_window)
num_window.y -= @sprites[:speech_box].height
loop do
Graphics.update
Input.update
update
num_window.update
# Change quantity
old_quantity = quantity
if Input.repeat?(Input::LEFT)
quantity = [quantity - 10, 1].max
elsif Input.repeat?(Input::RIGHT)
quantity = [quantity + 10, maximum].min
elsif Input.repeat?(Input::UP)
quantity += 1
quantity = 1 if quantity > maximum
elsif Input.repeat?(Input::DOWN)
quantity -= 1
quantity = maximum if quantity < 1
end
if quantity != old_quantity
num_window.text = _INTL("×{1}<r>${2}", quantity, (quantity * item_price).to_s_formatted)
pbPlayCursorSE
end
# Finish choosing a quantity
if Input.trigger?(Input::USE)
pbPlayDecisionSE
break
elsif Input.trigger?(Input::BACK)
pbPlayCancelSE
quantity = 0
break
end
end
end
@sprites[:speech_box].visible = false
return quantity
end
#---------------------------------------------------------------------------
def refresh_on_index_changed(old_index)
end
#---------------------------------------------------------------------------
def update_input
if Input.trigger?(Input::BACK)
return :quit
end
return nil
end
#---------------------------------------------------------------------------
def navigate
ret = nil
loop do
Graphics.update
Input.update
old_index = index
update_visuals
refresh_on_index_changed(old_index) if index != old_index
old_index = index
ret = update_input
refresh_on_index_changed(old_index) if index != old_index
break if ret
end
return ret
end
end
#=============================================================================
# The logic class.
#=============================================================================
class BaseScreen
attr_reader :visuals
attr_accessor :result
def initialize
@disposed = false
initialize_visuals
end
def initialize_visuals
@visuals = UI::BaseVisuals.new
end
def start_screen
@visuals.fade_in
end
def end_screen
return if @disposed
@visuals.fade_out
@visuals.dispose
@disposed = true
end
# Same as def end_screen but without fading out.
def silent_end_screen
return if @disposed
@visuals.dispose
@disposed = true
end
#-----------------------------------------------------------------------------
def sprites
return @visuals.sprites
end
def index
return @visuals.index
end
#-----------------------------------------------------------------------------
def show_message(text, &block)
@visuals.show_message(text, &block)
end
alias pbDisplay show_message
def show_confirm_message(text)
return @visuals.show_confirm_message(text)
end
alias pbConfirm show_confirm_message
def show_confirm_serious_message(text)
return @visuals.show_confirm_serious_message(text)
end
def show_choice_message(text, options, initial_index = 0)
return @visuals.show_choice_message(text, options, initial_index)
end
alias pbShowCommands show_choice_message
def show_menu(text, options, initial_index = 0, cmd_side: :right)
return @visuals.show_menu(text, options, initial_index, cmd_side: cmd_side)
end
def show_choice(options, initial_index = 0)
return @visuals.show_choice(options, initial_index)
end
def show_choice_from_menu_handler(menu_handler_id, message = nil)
commands = {}
MenuHandlers.each_available(menu_handler_id, self) do |option, _hash, name|
commands[option] = name
end
return show_menu(message, commands) if message
return show_choice(commands)
end
def choose_number(help_text, maximum, init_value = 1)
return @visuals.choose_number(help_text, maximum, init_value)
end
alias pbChooseNumber choose_number
def choose_number_as_money_multiplier(help_text, money_per_unit, maximum, init_value = 1)
return @visuals.choose_number_as_money_multiplier(help_text, money_per_unit, maximum, init_value)
end
#-----------------------------------------------------------------------------
def refresh
@visuals.refresh
end
alias pbRefresh refresh
def update
@visuals.update
end
alias pbUpdate update
#-----------------------------------------------------------------------------
def show_and_hide
start_screen
yield if block_given?
end_screen
end
def main
return if @disposed
start_screen
loop do
on_start_main_loop
command = @visuals.navigate
break if command == :quit
command = perform_action(command)
break if command == :quit
break if @disposed
end
end_screen
end
def on_start_main_loop
end
def perform_action(command)
return nil if !self.class::SCREEN_ID
action_hash = UIActionHandlers.get(self.class::SCREEN_ID, command)
return nil if !action_hash
return nil if action_hash[:condition] && !action_hash[:condition].call(self)
if action_hash[:menu]
choice = show_choice_from_menu_handler(action_hash[:menu], action_hash[:menu_message]&.call(self))
return perform_action(choice) if choice
elsif action_hash[:effect]
return perform_action_effect(action_hash)
end
return nil
end
def perform_action_effect(action_hash)
ret = action_hash[:effect].call(self)
return ret if action_hash[:returns_value]
return nil
end
end
end