summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--dragon/array_docs.rb300
-rw-r--r--dragon/attr_sprite.rb27
-rw-r--r--dragon/console.rb348
-rw-r--r--dragon/console_color.rb30
-rw-r--r--dragon/console_font_style.rb40
-rw-r--r--dragon/console_prompt.rb157
-rw-r--r--dragon/directional_input_helper_methods.rb12
-rw-r--r--dragon/docs.rb537
-rw-r--r--dragon/geometry.rb31
-rw-r--r--dragon/kernel_docs.rb71
-rw-r--r--dragon/readme_docs.rb1170
-rw-r--r--dragon/runtime_docs.rb39
12 files changed, 2485 insertions, 277 deletions
diff --git a/dragon/array_docs.rb b/dragon/array_docs.rb
new file mode 100644
index 0000000..6ce8e37
--- /dev/null
+++ b/dragon/array_docs.rb
@@ -0,0 +1,300 @@
+# coding: utf-8
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# array_docs.rb has been released under MIT (*only this file*).
+
+module ArrayDocs
+ def docs_method_sort_order
+ [:docs_class, :docs_map, :docs_each]
+ end
+
+ def docs_include_any?
+ <<-S
+* DOCS: ~Array#include_any?~
+
+Given a collection of items, the function will return
+~true~ if any of ~self~'s items exists in the collection of items passed in:
+
+S
+ end
+
+ def docs_class
+ <<-S
+* DOCS: ~Array~
+
+The Array class has been extend to provide methods that
+will help in common game development tasks. Array is one of the most
+powerful classes in Ruby and a very fundamental component of Game Toolkit.
+
+S
+ end
+
+ def docs_reject_nil
+ <<-S
+* DOCS: ~Array#reject_nil~
+
+Returns an ~Enumerable~ rejecting items that are ~nil~, this is an alias
+for ~Array#compact~:
+
+#+begin_src
+ repl do
+ a = [1, nil, 4, false, :a]
+ puts a.reject_nil
+ # => [1, 4, false, :a]
+ puts a.compact
+ # => [1, 4, false, :a]
+ end
+#+end_src
+
+S
+ end
+
+ def docs_reject_false
+ <<-S
+* DOCS: ~Array#reject_false~
+
+Returns an `Enumerable` rejecting items that are `nil` or `false`.
+
+#+begin_src
+ repl do
+ a = [1, nil, 4, false, :a]
+ puts a.reject_false
+ # => [1, 4, :a]
+ end
+#+end_src
+
+S
+ end
+
+ def docs_product
+ <<-S
+* DOCS: ~Array#product~
+
+Returns all combinations of values between two arrays.
+
+Here are some examples of using ~product~. Paste the
+following code at the bottom of main.rb and save
+the file to see the results:
+
+#+begin_src
+ repl do
+ a = [0, 1]
+ puts a.product
+ # => [[0, 0], [0, 1], [1, 0], [1, 1]]
+ end
+#+end_src
+
+#+begin_src
+ repl do
+ a = [ 0, 1]
+ b = [:a, :b]
+ puts a.product b
+ # => [[0, :a], [0, :b], [1, :a], [1, :b]]
+ end
+#+end_src
+
+S
+ end
+
+ def docs_map_2d
+ <<-S
+* DOCS: ~Array#map_2d~
+
+Assuming the array is an array of arrays, Given a block, each 2D array index invoked against the block.
+A 2D array is a common way to store data/layout for a stage.
+
+#+begin_src
+ repl do
+ stage = [
+ [:enemy, :empty, :player],
+ [:empty, :empty, :empty],
+ [:enemy, :empty, :enemy],
+ ]
+
+ occupied_tiles = stage.map_2d do |row, col, tile|
+ if tile == :empty
+ nil
+ else
+ [row, col, tile]
+ end
+ end.reject_nil
+
+ puts "Stage:"
+ puts stage
+
+ puts "Occupied Tiles"
+ puts occupied_tiles
+ end
+#+end_src
+
+S
+ end
+
+ def docs_any_intersect_rect?
+ <<-S
+* DOCS: ~Array#any_intersect_rect?~
+
+Assuming the array contains objects that respond to ~left~, ~right~, ~top~, ~bottom~,
+this method returns ~true~ if any of the elements within
+the array intersect the object being passed in. You are given an optional
+parameter called ~tolerance~ which informs how close to the other rectangles
+the elements need to be for it to be considered intersecting.
+
+The default tolerance is set to ~0.1~, which means that the primitives are not
+considered intersecting unless they are overlapping by more than ~0.1~.
+
+#+begin_src
+ repl do
+ # Here is a player class that has position and implement
+ # the ~attr_rect~ contract.
+ class Player
+ attr_rect
+ attr_accessor :x, :y, :w, :h
+
+ def initialize x, y, w, h
+ @x = x
+ @y = y
+ @w = w
+ @h = h
+ end
+
+ def serialize
+ { x: @x, y: @y, w: @w, h: @h }
+ end
+
+ def inspect
+ "\#{serialize}"
+ end
+
+ def to_s
+ "\#{serialize}"
+ end
+ end
+
+ # Here is a definition of two walls.
+ walls = [
+ [10, 10, 10, 10],
+ { x: 20, y: 20, w: 10, h: 10 },
+ ]
+
+ # Display the walls.
+ puts "Walls."
+ puts walls
+ puts ""
+
+ # Check any_intersect_rect? on player
+ player = Player.new 30, 20, 10, 10
+ puts "Is Player \#{player} touching wall?"
+ puts (walls.any_intersect_rect? player)
+ # => false
+ # The value is false because of the default tolerance is 0.1.
+ # The overlap of the player rect and any of the wall rects is
+ # less than 0.1 (for those that intersect).
+ puts ""
+
+ player = Player.new 9, 10, 10, 10
+ puts "Is Player \#{player} touching wall?"
+ puts (walls.any_intersect_rect? player)
+ # => true
+ puts ""
+ end
+#+end_src
+
+S
+ end
+
+ def docs_map
+ <<-S
+* DOCS: ~Array#map~
+
+The function given a block returns a new ~Enumerable~ of values.
+
+Example of using ~Array#map~ in conjunction with ~args.state~ and
+~args.outputs.sprites~ to render sprites to the screen.
+
+#+begin_src
+ def tick args
+ # define the colors of the rainbow in ~args.state~
+ # as an ~Array~ of ~Hash~es with :order and :name.
+ # :order will be used to determine render location
+ # and :name will be used to determine sprite path.
+ args.state.rainbow_colors ||= [
+ { order: 0, name: :red },
+ { order: 1, name: :orange },
+ { order: 2, name: :yellow },
+ { order: 3, name: :green },
+ { order: 4, name: :blue },
+ { order: 5, name: :indigo },
+ { order: 6, name: :violet },
+ ]
+
+ # render sprites diagonally to the screen
+ # with a width and height of 50.
+ args.outputs
+ .sprites << args.state
+ .rainbow_colors
+ .map do |color| # <-- ~Array#map~ usage
+ [
+ color[:order] * 50,
+ color[:order] * 50,
+ 50,
+ 50,
+ "sprites/square-\#{color[:name]}.png"
+ ]
+ end
+ end
+#+end_src
+
+S
+ end
+
+ def docs_each
+ <<-S
+* DOCS: ~Array#each~
+
+The function, given a block, invokes the block for each item in the
+~Array~. ~Array#each~ is synonymous to foreach constructs in other languages.
+
+Example of using ~Array#each~ in conjunction with ~args.state~ and
+~args.outputs.sprites~ to render sprites to the screen:
+
+#+begin_src
+ def tick args
+ # define the colors of the rainbow in ~args.state~
+ # as an ~Array~ of ~Hash~es with :order and :name.
+ # :order will be used to determine render location
+ # and :name will be used to determine sprite path.
+ args.state.rainbow_colors ||= [
+ { order: 0, name: :red },
+ { order: 1, name: :orange },
+ { order: 2, name: :yellow },
+ { order: 3, name: :green },
+ { order: 4, name: :blue },
+ { order: 5, name: :indigo },
+ { order: 6, name: :violet },
+ ]
+
+ # render sprites diagonally to the screen
+ # with a width and height of 50.
+ args.state
+ .rainbow_colors
+ .map do |color| # <-- ~Array#each~ usage
+ args.outputs.sprites << [
+ color[:order] * 50,
+ color[:order] * 50,
+ 50,
+ 50,
+ "sprites/square-\#{color[:name]}.png"
+ ]
+ end
+ end
+#+end_src
+
+S
+ end
+end
+
+class Array
+ extend Docs
+ extend ArrayDocs
+end
diff --git a/dragon/attr_sprite.rb b/dragon/attr_sprite.rb
index e47e051..81701ec 100644
--- a/dragon/attr_sprite.rb
+++ b/dragon/attr_sprite.rb
@@ -3,18 +3,7 @@
# attr_sprite.rb has been released under MIT (*only this file*).
# @private
-module AttrSprite
- include GTK::Geometry
-
- attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :tile_x,
- :tile_y, :tile_w, :tile_h, :flip_horizontally,
- :flip_vertically, :angle_anchor_x, :angle_anchor_y, :id,
- :source_x, :source_y, :source_w, :source_h
-
- def primitive_marker
- :sprite
- end
-
+module AttrRect
def left
@x
end
@@ -30,6 +19,20 @@ module AttrSprite
def top
@y + @h
end
+end
+
+module AttrSprite
+ include AttrRect
+ include GTK::Geometry
+
+ attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :tile_x,
+ :tile_y, :tile_w, :tile_h, :flip_horizontally,
+ :flip_vertically, :angle_anchor_x, :angle_anchor_y, :id,
+ :source_x, :source_y, :source_w, :source_h
+
+ def primitive_marker
+ :sprite
+ end
def sprite
self
diff --git a/dragon/console.rb b/dragon/console.rb
index 4f2abab..5d5bd93 100644
--- a/dragon/console.rb
+++ b/dragon/console.rb
@@ -7,203 +7,6 @@
module GTK
class Console
- class Color
- def initialize(color)
- @color = color
- @color << 255 if @color.size == 3
- end
-
- def mult_alpha(alpha_modifier)
- Color.new [@color[0], @color[1], @color[2], (@color[3].to_f * alpha_modifier).to_i]
- end
-
- # Support splat operator
- def to_a
- @color
- end
-
- def to_h
- { r: @color[0], g: @color[1], b: @color[2], a: @color[3] }
- end
- end
-
- class FontStyle
- attr_reader :font, :size_enum, :line_height
-
- def initialize(font:, size_enum:, line_height:)
- @font = font
- @size_enum = size_enum
- @line_height = line_height
- end
-
- def letter_size
- @letter_size ||= $gtk.calcstringbox 'W', size_enum, font
- end
-
- def line_height_px
- @line_height_px ||= letter_size.y * line_height
- end
-
- def label(x:, y:, text:, color:, alignment_enum: 0)
- {
- x: x,
- y: y.shift_up(line_height_px), # !!! FIXME: remove .shift_up(line_height_px) when we fix coordinate origin on labels.
- text: text,
- font: font,
- size_enum: size_enum,
- alignment_enum: alignment_enum,
- **color.to_h,
- }.label
- end
- end
-
- class Prompt
- attr_accessor :current_input_str, :font_style, :console_text_width
-
- def initialize(font_style:, text_color:, console_text_width:)
- @prompt = '-> '
- @current_input_str = ''
- @font_style = font_style
- @text_color = text_color
- @cursor_color = Color.new [187, 21, 6]
- @console_text_width = console_text_width
-
- @last_autocomplete_prefix = nil
- @next_candidate_index = 0
- end
-
- def <<(str)
- @current_input_str << str
- reset_autocomplete
- end
-
- def backspace
- @current_input_str.chop!
- reset_autocomplete
- end
-
- def clear
- @current_input_str = ''
- reset_autocomplete
- end
-
- def autocomplete
- if !@last_autocomplete_prefix
- @last_autocomplete_prefix = calc_autocomplete_prefix
-
- puts "* AUTOCOMPLETE CANDIDATES: #{current_input_str}.."
- pretty_print_strings_as_table method_candidates(@last_autocomplete_prefix)
- else
- candidates = method_candidates(@last_autocomplete_prefix)
- return if candidates.empty?
-
- candidate = candidates[@next_candidate_index]
- candidate = candidate[0..-2] + " = " if candidate.end_with? '='
- @next_candidate_index += 1
- @next_candidate_index = 0 if @next_candidate_index >= candidates.length
- self.current_input_str = display_autocomplete_candidate(candidate)
- end
- end
-
- def pretty_print_strings_as_table items
- if items.length == 0
- puts <<-S.strip
-+--------+
-| (none) |
-+--------+
-S
- else
- # figure out the largest string
- string_width = items.sort_by { |c| -c.to_s.length }.first
-
- # add spacing to each side of the string which represents the cell width
- cell_width = string_width.length + 2
-
- # add spacing to each side of the cell to represent the column width
- column_width = cell_width + 2
-
- # determine the max number of columns that can fit on the screen
- columns = @console_text_width.idiv column_width
- columns = items.length if items.length < columns
-
- # partition the original list of items into a string to be printed
- items.each_slice(columns).each_with_index do |cells, i|
- pretty_print_row_seperator string_width, cell_width, column_width, columns
- pretty_print_row cells, string_width, cell_width, column_width, columns
- end
-
- pretty_print_row_seperator string_width, cell_width, column_width, columns
- end
- end
-
- def pretty_print_row cells, string_width, cell_width, column_width, columns
- # if the number of cells doesn't match the number of columns, then pad the array with empty values
- cells += (columns - cells.length).map { "" }
-
- # right align each cell value
- formated_row = "|" + cells.map do |c|
- "#{" " * (string_width.length - c.length) } #{c} |"
- end.join
-
- # remove seperators between empty values
- formated_row = formated_row.gsub(" | ", " ")
-
- puts formated_row
- end
-
- def pretty_print_row_seperator string_width, cell_width, column_width, columns
- # this is a joint: +--------
- column_joint = "+#{"-" * cell_width}"
-
- # multiple joints create a row seperator: +----+----+
- puts (column_joint * columns) + "+"
- end
-
- def render(args, x:, y:)
- args.outputs.reserved << font_style.label(x: x, y: y, text: "#{@prompt}#{current_input_str}", color: @text_color)
- args.outputs.reserved << font_style.label(x: x - 2, y: y + 3, text: (" " * (@prompt.length + current_input_str.length)) + "|", color: @cursor_color)
- end
-
- private
-
- def last_period_index
- current_input_str.rindex('.')
- end
-
- def calc_autocomplete_prefix
- if last_period_index
- current_input_str[(last_period_index + 1)..-1]
- else
- current_input_str
- end
- end
-
- def current_object
- return Kernel unless last_period_index
-
- Kernel.eval(current_input_str[0...last_period_index])
- rescue NameError
- nil
- end
-
- def method_candidates(prefix)
- current_object.autocomplete_methods.map(&:to_s).select { |m| m.start_with? prefix }
- end
-
- def display_autocomplete_candidate(candidate)
- if last_period_index
- @current_input_str[0..last_period_index] + candidate.to_s
- else
- candidate.to_s
- end
- end
-
- def reset_autocomplete
- @last_autocomplete_prefix = nil
- @next_candidate_index = 0
- end
- end
-
attr_accessor :show_reason, :log, :logo, :background_color,
:text_color, :animation_duration,
:max_log_lines, :max_history, :log,
@@ -214,7 +17,7 @@ S
:font_style
def initialize
- @font_style = FontStyle.new(font: 'font.ttf', size_enum: 0, line_height: 1.1)
+ @font_style = FontStyle.new(font: 'font.ttf', size_enum: -1, line_height: 1.1)
@disabled = false
@log_offset = 0
@visible = false
@@ -484,9 +287,16 @@ S
end
@last_command_errored = false
rescue Exception => e
+ string_e = "#{e}"
@last_command_errored = true
- puts "#{e}"
- log "#{e}"
+ if (string_e.include? "wrong number of arguments")
+ method_name = (string_e.split ":")[0].gsub "'", ""
+ results = Kernel.docs_search method_name
+ if !results.include "* DOCS: No results found."
+ puts results
+ log results
+ end
+ end
end
end
end
@@ -539,43 +349,54 @@ S
@log_offset = 0 if @log_offset < 0
end
- def process_inputs args
- if console_toggle_key_down? args
- args.inputs.text.clear
- toggle
- end
+ def mouse_wheel_scroll args
+ @inertia ||= 0
- return unless visible?
+ if args.inputs.mouse.wheel && args.inputs.mouse.wheel.y > 0
+ @inertia = 1
+ elsif args.inputs.mouse.wheel && args.inputs.mouse.wheel.y < 0
+ @inertia = -1
+ end
- if !@suppress_left_arrow_behavior && args.inputs.keyboard.key_down.left && current_input_str.strip.length > 0
- log_info "Use repl.rb!", <<-S
-The Console is nice for quick commands, but for more complex edits, use repl.rb.
+ if args.inputs.mouse.click
+ @inertia = 0
+ end
-I've written the current command at the top of a file called ./repl.rb (right next to dragonruby(.exe)). Please open the the file and apply additional edits there.
-S
- if @last_command_written_to_repl_rb != current_input_str
- @last_command_written_to_repl_rb = current_input_str
- contents = $gtk.read_file 'app/repl.rb'
- contents ||= ''
- contents = <<-S + contents
-
-# Remove the x from xrepl to run the command.
-xrepl do
- #{@last_command_written_to_repl_rb}
-end
+ return if @inertia == 0
-S
- $gtk.suppress_hotload = true
- $gtk.write_file 'app/repl.rb', contents
- $gtk.reload_if_needed 'app/repl.rb', true
- $gtk.suppress_hotload = false
+ if @inertia != 0
+ @inertia = (@inertia * 0.7)
+ if @inertia > 0
+ @log_offset -= 1
+ elsif @inertia < 0
+ @log_offset += 1
end
+ end
- return
+ if @inertia.abs < 0.01
+ @inertia = 0
end
+ if @log_offset > @log.size
+ @log_offset = @log.size
+ elsif @log_offset < 0
+ @log_offset = 0
+ end
+ end
+
+ def process_inputs args
+ if console_toggle_key_down? args
+ args.inputs.text.clear
+ toggle
+ end
+
+ return unless visible?
+
args.inputs.text.each { |str| prompt << str }
args.inputs.text.clear
+ mouse_wheel_scroll args
+
+ @log_offset = 0 if @log_offset < 0
if args.inputs.keyboard.key_down.enter
eval_the_set_command
@@ -654,7 +475,7 @@ S
bottom = top - (h * percent)
args.outputs.reserved << [left, bottom, w, h, *@background_color.mult_alpha(percent)].solid
- args.outputs.reserved << [right.shift_left(210), bottom.shift_up(540), 200, 200, @logo, 0, (80.0 * percent).to_i].sprite
+ args.outputs.reserved << [right.shift_left(110), bottom.shift_up(630), 100, 100, @logo, 0, (80.0 * percent).to_i].sprite
y = bottom + 2 # just give us a little padding at the bottom.
prompt.render args, x: left.shift_right(10), y: y
@@ -680,6 +501,29 @@ S
end
render_log_offset args
+ render_help args, top if percent == 1
+ end
+
+ def render_help args, top
+ [
+ "* Prompt Commands: ",
+ "You can type any of the following ",
+ "commands in the command prompt. ",
+ "** docs: Provides API docs. ",
+ "** $gtk: Accesses the global runtime.",
+ "* Shortcut Keys: ",
+ "** full page up: ctrl + b ",
+ "** full page down: ctrl + f ",
+ "** half page up: ctrl + u ",
+ "** half page down: ctrl + d ",
+ "** clear prompt: ctrl + g ",
+ "** up arrow: next command ",
+ "** down arrow: prev command ",
+ ].each_with_index do |s, i|
+ args.outputs.reserved << [args.grid.right - 10,
+ top - 100 - line_height_px * i * 0.8,
+ s, -3, 2, 180, 180, 180].label
+ end
end
def render_log_offset args
@@ -745,9 +589,23 @@ S
end
def set_command_with_history_silent command, histories, show_reason = nil
- @command_history.concat histories
- @command_history << command if @command_history[-1] != command
- self.current_input_str = command if @command_set_at != Kernel.global_tick_count
+ set_command_extended command: command, histories: histories, show_reason: show_reason
+ end
+
+ def defaults_set_command_extended
+ {
+ command: "puts 'Hello World'",
+ histories: [],
+ show_reason: nil,
+ force: false
+ }
+ end
+
+ def set_command_extended opts
+ opts = defaults_set_command_extended.merge opts
+ @command_history.concat opts[:histories]
+ @command_history << opts[:command] if @command_history[-1] != opts[:command]
+ self.current_input_str = opts[:command] if @command_set_at != Kernel.global_tick_count || opts[:force]
@command_set_at = Kernel.global_tick_count
@command_history_index = -1
save_history
@@ -768,6 +626,22 @@ S
set_command_with_history_silent command, [], show_reason
end
+ def set_system_command command, show_reason = nil
+ if $gtk.platform == "Mac OS X"
+ set_command_silent "$gtk.system \"open #{command}\""
+ else
+ set_command_silent "$gtk.system \"start #{command}\""
+ end
+ end
+
+ def system_command
+ if $gtk.platform == "Mac OS X"
+ "open"
+ else
+ "start"
+ end
+ end
+
private
def w
@@ -802,14 +676,24 @@ S
log_entry[0] == "|"
end
- def color_for_log_entry(log_entry)
+ def include_header_marker? log_entry
+ return false if log_entry.include? "NOTIFY:"
+ return false if log_entry.include? "INFO:"
+ return true if log_entry.include? "DOCS:"
+ (log_entry.start_with? "* ") ||
+ (log_entry.start_with? "** ") ||
+ (log_entry.start_with? "*** ")
+ end
+ def color_for_log_entry(log_entry)
if include_row_marker? log_entry
@text_color
elsif include_error_marker? log_entry
@error_color
elsif include_subdued_markers? log_entry
@text_color.mult_alpha(0.5)
+ elsif include_header_marker? log_entry
+ @header_color
elsif log_entry.start_with?("====") || log_entry.include?("app") && !log_entry.include?("apple")
@header_color
else
diff --git a/dragon/console_color.rb b/dragon/console_color.rb
new file mode 100644
index 0000000..f5b164d
--- /dev/null
+++ b/dragon/console_color.rb
@@ -0,0 +1,30 @@
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# console_color.rb has been released under MIT (*only this file*).
+
+# Contributors outside of DragonRuby who also hold Copyright:
+# - Kevin Fischer: https://github.com/kfischer-okarin
+
+module GTK
+ class Console
+ class Color
+ def initialize(color)
+ @color = color
+ @color << 255 if @color.size == 3
+ end
+
+ def mult_alpha(alpha_modifier)
+ Color.new [@color[0], @color[1], @color[2], (@color[3].to_f * alpha_modifier).to_i]
+ end
+
+ # Support splat operator
+ def to_a
+ @color
+ end
+
+ def to_h
+ { r: @color[0], g: @color[1], b: @color[2], a: @color[3] }
+ end
+ end
+ end
+end
diff --git a/dragon/console_font_style.rb b/dragon/console_font_style.rb
new file mode 100644
index 0000000..8efab4f
--- /dev/null
+++ b/dragon/console_font_style.rb
@@ -0,0 +1,40 @@
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# console_font_style.rb has been released under MIT (*only this file*).
+
+# Contributors outside of DragonRuby who also hold Copyright:
+# - Kevin Fischer: https://github.com/kfischer-okarin
+
+module GTK
+ class Console
+ class FontStyle
+ attr_reader :font, :size_enum, :line_height
+
+ def initialize(font:, size_enum:, line_height:)
+ @font = font
+ @size_enum = size_enum
+ @line_height = line_height
+ end
+
+ def letter_size
+ @letter_size ||= $gtk.calcstringbox 'W', size_enum, font
+ end
+
+ def line_height_px
+ @line_height_px ||= letter_size.y * line_height
+ end
+
+ def label(x:, y:, text:, color:, alignment_enum: 0)
+ {
+ x: x,
+ y: y.shift_up(line_height_px), # !!! FIXME: remove .shift_up(line_height_px) when we fix coordinate origin on labels.
+ text: text,
+ font: font,
+ size_enum: size_enum,
+ alignment_enum: alignment_enum,
+ **color.to_h,
+ }.label
+ end
+ end
+ end
+end
diff --git a/dragon/console_prompt.rb b/dragon/console_prompt.rb
new file mode 100644
index 0000000..dd454f7
--- /dev/null
+++ b/dragon/console_prompt.rb
@@ -0,0 +1,157 @@
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# console_prompt.rb has been released under MIT (*only this file*).
+
+# Contributors outside of DragonRuby who also hold Copyright:
+# - Kevin Fischer: https://github.com/kfischer-okarin
+
+module GTK
+ class Console
+ class Prompt
+ attr_accessor :current_input_str, :font_style, :console_text_width
+
+ def initialize(font_style:, text_color:, console_text_width:)
+ @prompt = '-> '
+ @current_input_str = ''
+ @font_style = font_style
+ @text_color = text_color
+ @cursor_color = Color.new [187, 21, 6]
+ @console_text_width = console_text_width
+
+ @last_autocomplete_prefix = nil
+ @next_candidate_index = 0
+ end
+
+ def <<(str)
+ @current_input_str << str
+ reset_autocomplete
+ end
+
+ def backspace
+ @current_input_str.chop!
+ reset_autocomplete
+ end
+
+ def clear
+ @current_input_str = ''
+ reset_autocomplete
+ end
+
+ def autocomplete
+ if !@last_autocomplete_prefix
+ @last_autocomplete_prefix = calc_autocomplete_prefix
+
+ puts "* AUTOCOMPLETE CANDIDATES: #{current_input_str}.."
+ pretty_print_strings_as_table method_candidates(@last_autocomplete_prefix)
+ else
+ candidates = method_candidates(@last_autocomplete_prefix)
+ return if candidates.empty?
+
+ candidate = candidates[@next_candidate_index]
+ candidate = candidate[0..-2] + " = " if candidate.end_with? '='
+ @next_candidate_index += 1
+ @next_candidate_index = 0 if @next_candidate_index >= candidates.length
+ self.current_input_str = display_autocomplete_candidate(candidate)
+ end
+ end
+
+ def pretty_print_strings_as_table items
+ if items.length == 0
+ puts <<-S.strip
++--------+
+| (none) |
++--------+
+S
+ else
+ # figure out the largest string
+ string_width = items.sort_by { |c| -c.to_s.length }.first
+
+ # add spacing to each side of the string which represents the cell width
+ cell_width = string_width.length + 2
+
+ # add spacing to each side of the cell to represent the column width
+ column_width = cell_width + 2
+
+ # determine the max number of columns that can fit on the screen
+ columns = @console_text_width.idiv column_width
+ columns = items.length if items.length < columns
+
+ # partition the original list of items into a string to be printed
+ items.each_slice(columns).each_with_index do |cells, i|
+ pretty_print_row_seperator string_width, cell_width, column_width, columns
+ pretty_print_row cells, string_width, cell_width, column_width, columns
+ end
+
+ pretty_print_row_seperator string_width, cell_width, column_width, columns
+ end
+ end
+
+ def pretty_print_row cells, string_width, cell_width, column_width, columns
+ # if the number of cells doesn't match the number of columns, then pad the array with empty values
+ cells += (columns - cells.length).map { "" }
+
+ # right align each cell value
+ formated_row = "|" + cells.map do |c|
+ "#{" " * (string_width.length - c.length) } #{c} |"
+ end.join
+
+ # remove seperators between empty values
+ formated_row = formated_row.gsub(" | ", " ")
+
+ puts formated_row
+ end
+
+ def pretty_print_row_seperator string_width, cell_width, column_width, columns
+ # this is a joint: +--------
+ column_joint = "+#{"-" * cell_width}"
+
+ # multiple joints create a row seperator: +----+----+
+ puts (column_joint * columns) + "+"
+ end
+
+ def render(args, x:, y:)
+ args.outputs.reserved << font_style.label(x: x, y: y, text: "#{@prompt}#{current_input_str}", color: @text_color)
+ args.outputs.reserved << font_style.label(x: x - 2, y: y + 3, text: (" " * (@prompt.length + current_input_str.length)) + "|", color: @cursor_color)
+ end
+
+ private
+
+ def last_period_index
+ current_input_str.rindex('.')
+ end
+
+ def calc_autocomplete_prefix
+ if last_period_index
+ current_input_str[(last_period_index + 1)..-1]
+ else
+ current_input_str
+ end
+ end
+
+ def current_object
+ return Kernel unless last_period_index
+
+ Kernel.eval(current_input_str[0...last_period_index])
+ rescue NameError
+ nil
+ end
+
+ def method_candidates(prefix)
+ current_object.autocomplete_methods.map(&:to_s).select { |m| m.start_with? prefix }
+ end
+
+ def display_autocomplete_candidate(candidate)
+ if last_period_index
+ @current_input_str[0..last_period_index] + candidate.to_s
+ else
+ candidate.to_s
+ end
+ end
+
+ def reset_autocomplete
+ @last_autocomplete_prefix = nil
+ @next_candidate_index = 0
+ end
+ end
+ end
+end
diff --git a/dragon/directional_input_helper_methods.rb b/dragon/directional_input_helper_methods.rb
index edaf8d3..0e7c118 100644
--- a/dragon/directional_input_helper_methods.rb
+++ b/dragon/directional_input_helper_methods.rb
@@ -49,13 +49,13 @@ S
# Returns a normal vector (in the form of an Array with two values). If no directionals are held/down, the function returns nil.
#
- # Some of the possible results are:
+ # The possible results are:
#
- # - `nil` which denotes that no directional input exists.
- # - `[ 0, 1]` which denotes that only up is being held/pressed.
- # - `[ 0, -1]` which denotes that only down is being held/pressed.
- # - `[ 0.5, 0.5]` which denotes that right and up are being pressed/held.
- # - `[-0.5, -0.5]` which denotes that left and down are being pressed/held.
+ # - ~nil~ which denotes that no directional input exists.
+ # - ~[ 0, 1]~ which denotes that only up is being held/pressed.
+ # - ~[ 0, -1]~ which denotes that only down is being held/pressed.
+ # - ~[ 0.5, 0.5]~ which denotes that right and up are being pressed/held.
+ # - ~[-0.5, -0.5]~ which denotes that left and down are being pressed/held.
#
# @gtk
def directional_vector
diff --git a/dragon/docs.rb b/dragon/docs.rb
index b9d3dbf..fd0e6eb 100644
--- a/dragon/docs.rb
+++ b/dragon/docs.rb
@@ -3,45 +3,530 @@
# MIT License
# docs.rb has been released under MIT (*only this file*).
-module GTK
- class Docs
- def map_with_ys
+module DocsOrganizer
+ def self.sort_docs_classes!
+ $docs_classes.sort! do |l, r|
+ l_index = (class_sort_order.find_index l) || 50000
+ r_index = (class_sort_order.find_index r) || 50000
+ l_index = 51000 if l == :docs_classes
+ r_index = 51000 if r == :docs_classes
+ l_index <=> r_index
+ end
+ end
+
+ def self.reserved_methods
+ [
+ :docs_export_docs!,
+ :docs_all,
+ :docs_method_sort_order,
+ :docs_classes,
+ :docs_search
+ ]
+ end
+
+ def self.class_sort_order
+ [
+ GTK::ReadMe,
+ GTK::Runtime,
+ Array,
+ GTK::Outputs,
+ GTK::OpenEntity,
+ Numeric,
+ Kernel,
+ ]
+ end
+
+ def self.check_class_sort_order
+ unsorted = $docs_classes.find_all do |klass|
+ !class_sort_order.include? klass
+ end
+
+ unsorted.each do |k|
+ puts <<-S
+* WARNING: #{klass.name} is not included in DocsOrganizer::class_sort_order. Please place this
+module in it's correct topilogical order.
+S
+ end
+
+ if unsorted.length == 0
puts <<-S
-* Numeric#map_with_ys
-Numeric#map_with_ys is a helper method that is useful for working with coordinates
-or rows with columns. Here is an example usage.
+* INFO: Success. All documented classes have a sort order associated with them.
+S
+ end
+ end
+
+ def self.sort_method_delegate l, r, method_sort_order
+ l_index = (method_sort_order.find_index l) || 50000
+ r_index = (method_sort_order.find_index r) || 50000
+ l_index = 51000 if l == :docs_classes
+ r_index = 51000 if r == :docs_classes
+ l_index = -51000 if l == :docs_class
+ r_index = -51000 if r == :docs_class
+ l_index <=> r_index
+ end
+
+ def self.find_methods_with_docs klass
+ klass_method_sort_order = klass.docs_method_sort_order
+ klass.methods.find_all { |m| m.start_with? 'docs_' }
+ .reject { |m| reserved_methods.include? m }
+ .sort do |l, r|
+ sort_method_delegate l, r, klass_method_sort_order
+ end
+ end
+end
+
+module Docs
+ def self.extended klass
+ $docs_classes ||= []
+ $docs_classes << klass
+ $docs_classes.uniq!
+ end
+
+ def docs_method_sort_order
+ []
+ end
+
+ def docs_classes
+ DocsOrganizer.sort_docs_classes!
+ list = $docs_classes.map { |mod| "** #{mod.name}" }.join "\n"
+ <<-S
-Assume you have a grid with 10 rows (xs) and 5 columns (ys). You can generate
-an array of hashes with the following form:
+* DOCS:
+Here are the classes that have documentation. You can call the .docs method
+on any of these classes:
+#{list}
+S
+ end
+
+ def docs_all
+ docs_methods = DocsOrganizer.find_methods_with_docs(self).map { |d| send d }.join "\n"
+ <<-S
+#{docs_methods}
+S
+ end
+
+ def docs
+ docs_methods = [DocsOrganizer.find_methods_with_docs(self), :docs_classes].flatten.map { |d| "** #{d}" }.join "\n"
+ if self == Kernel
+ <<-S
+
+* DOCS: #{self.name}
+Some Classes in Game Toolkit have a method called docs. You can invoke this
+method interactively to see information about functions within the engine.
+For example, invoking ~Kernel.docs_tick_count~ will give you documentation
+for the Kernel.tick_count method.
+
+To export all documentation you can use ~Kernel.export_docs!~ (or just ~export_docs!~).
+
+To search docs you can use Kernel.docs_search (or just `docs_search`) by providing it a search term.
+For example:
#+begin_src
-[
- { x: 0, y: 0, some_data: "A" },
- { x: 0, y: 1, some_data: "A" },
- { x: 0, y: 2, some_data: "A" },
-...
- { x: 9, y: 4, some_data: "A" },
-]
+ docs_search "array find remove nil"
#+end_src
-Using the following code:
+You can do more advanced searches by providing a block:
-#+begin_src ruby
-array_of_hashes = 10.map_with_ys 5 do |x, y|
- { x: x, y: y, some_data: "A" }
-end
+#+begin_src
+ docs_search do |entry|
+ (entry.include? "Array") && (!entry.include? "Enumerable")
+ end
+#+end_src
+
+#{docs_methods}
+** NOTE: Invoke any of the methods above on #{self.name} to see detailed documentation.
+** NOTE: Calling the docs_classes method will give you all classes in Game Toolkit that contain docs.
+S
+ else
+ <<-S
+
+* DOCS: #{self.name}
+#{docs_methods}
+S
+ end
+ end
+
+ def self.__docs_search__ words = nil, &block
+
+ end
+
+ def docs_search words = nil, &block
+ words ||= ""
+ if words.strip.length != 0
+ each_word = words.split(' ').find_all { |w| w.strip.length > 0 }
+ block = lambda do |entry|
+ each_word.any? { |w| entry.downcase.include? w.downcase }
+ end
+ end
+
+ if !block
+ return <<-S
+* DOCS: How To Search The Docs
+To search docs you can use Kernel.docs_search (or just ~docs_search~) by providing it a search term.
+For example:
-Take a look at the "hexagon grid" sample app for a real world usage of
-this method.
+#+begin_src
+ docs_search "array find remove nil"
+#+end_src
+
+You can do more advanced searches by providing a block:
+
+#+begin_src
+ docs_search do |entry|
+ (entry.include? "Array") && (!entry.include? "Enumerable")
+ end
#+end_src
S
end
- def method_missing m, *args
- puts <<-S
-* DOCUMENTATION MISSING:
-It looks like docs are missing for :#{m}. Let the @dragonborne know about it in the Discord channel: http://discord.dragonruby.org.
+ DocsOrganizer.sort_docs_classes!
+ this_block = block
+ final_string = ""
+ if self == Kernel
+ $docs_classes.each do |k|
+ k.methods_with_docs.each do |m|
+ s = k.send m
+ final_string += s + "\n" if block.call s
+ end
+ end
+ else
+ methods_with_docs.each do |m|
+ s = send m
+ final_string += s + "\n" if block.call s
+ end
+ end
+
+ if final_string.strip.length == 0
+ final_string = "* DOCS: No results found."
+ end
+
+ $gtk.write_file "docs/search_results.txt", final_string
+ log "* INFO: Search results have been written to docs/search_results.txt."
+
+ "\n" + final_string
+ end
+
+ def __export_docs__! opts = {}
+ DocsOrganizer.sort_docs_classes!
+ opts = defaults_export_docs!.merge opts
+ opts[:methods] = methods_with_docs.reject { |m| m == :docs_classes } if opts[:methods].include? :all
+ content = opts[:methods].map do |m|
+ puts "* INFO: Getting docs for #{m}."
+ (send m).ltrim + "\n"
+ end.join "\n"
+ file_path = "docs/#{self.name}.txt"
+ $gtk.write_file "#{file_path}", content
+ puts "* INFO: Documentation for #{self.name} has been exported to #{file_path}."
+ $gtk.console.set_system_command file_path
+ nil
+ end
+
+ def export_docs! opts = {}
+ __export_docs__! opts
+ end
+
+ def __docs_append_true_line__ true_lines, true_line, parse_log
+ true_line.rstrip!
+ parse_log << "*** True Line Result\n#{true_line}"
+ true_lines << true_line
+ end
+
+ # may god have mercy on your soul if you try to expand this
+ def __docs_to_html__ string
+ parse_log = []
+ html_string = <<-S
+<html>
+ <head>
+ <title>DragonRuby Game Toolkit Documentation</title>
+ <link href="docs.css" rel="stylesheet" type="text/css" media="all">
+ <script src="docs.js"></script>
+ </head>
+ <body>
+ <div id='toc'>
+ {{toc}}
+ </div>
+ <div id='content'>
+ {{content}}
+ </div>
+ </body>
+</html>
S
+
+ true_lines = []
+ current_true_line = ""
+
+ inside_source = false
+ inside_ordered_list = false
+ inside_unordered_list = false
+
+ # PARSE TRUE LINES
+ parse_log << "* Processing True Lines"
+ string.strip.each_line do |l|
+ parse_log << "** Processing line: ~#{l.rstrip}~"
+ if l.start_with? "#+begin_src"
+ parse_log << "- Line was identified as the beginning of a code block."
+ inside_source = true
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ __docs_append_true_line__ true_lines, l, parse_log
+ elsif l.start_with? "#+end_src"
+ parse_log << "- Line was identified as the end of a code block."
+ inside_source = false
+ __docs_append_true_line__ true_lines, l, parse_log
+ current_true_line = ""
+ elsif l.start_with? "#+"
+ parse_log << "- Line was identified as a literal block."
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ __docs_append_true_line__ true_lines, l, parse_log
+ current_true_line = ""
+ elsif l.start_with? "- "
+ parse_log << "- Line was identified as a list."
+ inside_unordered_list = true
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ current_true_line = l
+ elsif l.start_with? "1. "
+ parse_log << "- Line was identified as a start of a list."
+ inside_ordered_list = true
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ current_true_line = l
+ elsif inside_ordered_list && (l[1] == "." || l[2] == "." || l[3] == ".")
+ parse_log << "- Line was identified as a continuation of a list."
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ current_true_line = l
+ elsif inside_source
+ parse_log << "- Inside source: true"
+ inside_source = true
+ __docs_append_true_line__ true_lines, l, parse_log
+ current_true_line = ""
+ elsif l.strip.length == 0
+ parse_log << "- End of paragraph detected."
+ inside_ordered_list = false
+ inside_unordered_list = false
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ current_true_line = ""
+ elsif l.start_with? "* "
+ parse_log << "- Header detected."
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ __docs_append_true_line__ true_lines, l, parse_log
+ current_true_line = ""
+ elsif l.start_with? "** "
+ parse_log << "- Header detected."
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ __docs_append_true_line__ true_lines, l, parse_log
+ current_true_line = ""
+ elsif l.start_with? "*** "
+ parse_log << "- Header detected."
+ __docs_append_true_line__ true_lines, current_true_line, parse_log
+ __docs_append_true_line__ true_lines, l, parse_log
+ current_true_line = ""
+ else
+ current_true_line += l.rstrip + " "
+ end
+ end
+
+ true_lines << current_true_line if current_true_line.length != 0
+
+ if true_lines[0].strip == ""
+ true_lines = true_lines[1..-1]
end
+
+ toc = ""
+ content_html = ""
+
+ inside_pre = false
+ inside_being_src = false
+ inside_paragraph = false
+ inside_literal = false
+ inside_h1 = false
+ inside_ordered_list = false
+ inside_ul = false
+ inside_ol = false
+
+ text_to_id = lambda do |text|
+ text = text.strip.downcase
+ text = text.gsub("*", "-")
+ text = text.gsub("~", "-")
+ text = text.gsub("[", "-")
+ text = text.gsub("]", "-")
+ text = text.gsub(":", "-")
+ text = text.gsub(" ", "-")
+ text
+ end
+
+ toc = "<h1>Table Of Contents</h1>\n<ul>\n"
+ parse_log << "* Processing Html Given True Lines"
+ true_lines.each do |l|
+ parse_log << "** Processing line: ~#{l.rstrip}~"
+ if l.start_with? "* "
+ formatted_html = __docs_line_to_html__ l, parse_log
+ link_id = text_to_id.call l
+ toc += "<li><a href='##{link_id}'>#{formatted_html}</a></li>\n"
+ parse_log << "- H1 detected."
+ content_html += "<h1 id='#{link_id}'>#{formatted_html}</h1>\n"
+ elsif l.start_with? "** "
+ formatted_html = __docs_line_to_html__ l, parse_log
+ link_id = text_to_id.call l
+ # toc += "<a href='##{link_id}'>#{formatted_html}</a></br>\n"
+ parse_log << "- H2 detected."
+ content_html += "<h2>#{__docs_line_to_html__ l, parse_log}</h2>\n"
+ elsif l.start_with? "*** "
+ formatted_html = __docs_line_to_html__ l, parse_log
+ link_id = text_to_id.call l
+ # toc += "<a href='##{link_id}'>#{formatted_html}</a></br>\n"
+ parse_log << "- H3 detected."
+ content_html += "<h3>#{__docs_line_to_html__ l, parse_log}</h3>\n"
+ elsif l.strip.length == 0 && !inside_pre
+ # do nothing
+ elsif l.start_with? "#+begin_src"
+ parse_log << "- PRE start detected."
+ inside_pre = true
+ content_html << "<pre>"
+ elsif l.start_with? "#+end_src"
+ parse_log << "- PRE end detected."
+ inside_pre = false
+ content_html << "</pre>\n"
+ elsif l.start_with? "#+begin_quote"
+ parse_log << "- BLOCKQUOTE start detected."
+ content_html << "<blockquote>\n"
+ elsif l.start_with? "#+end_quote"
+ parse_log << "- BLOCKQUOTE end detected."
+ content_html << "</blockquote>\n"
+ elsif (l.start_with? "1. ") && !inside_ol
+ parse_log << "- OL start detected."
+ inside_ol = true
+ content_html << "<ol>\n"
+
+ if l.split(".")[0].length == 1
+ l = l[2..-1]
+ elsif l.split(".")[0].length == 2
+ l = l[3..-1]
+ elsif l.split(".")[0].length == 3
+ l = l[4..-1]
+ end
+ parse_log << "- LI detected."
+ content_html << "<li>#{__docs_line_to_html__ l, parse_log}</li>\n"
+ elsif inside_ol && (l[1] == "." || l[2] == "." || l[3] == ".")
+
+ if l.split(".")[0].length == 1
+ l = l[2..-1]
+ elsif l.split(".")[0].length == 2
+ l = l[3..-1]
+ elsif l.split(".")[0].length == 3
+ l = l[4..-1]
+ end
+
+ parse_log << "- LI detected."
+ content_html << "<li>#{__docs_line_to_html__ l, parse_log}</li>\n"
+ elsif (l.start_with? "- ") && !inside_ul
+ parse_log << "- UL start detected."
+ inside_ul = true
+ content_html << "<ul>\n"
+ l = l[2..-1]
+
+ parse_log << "- LI detected."
+ content_html << "<li>#{__docs_line_to_html__ l, parse_log}</li>\n"
+ elsif (l.start_with? "- ") && inside_ul
+ l = l[2..-1]
+
+ parse_log << "- LI detected."
+ content_html << "<li>#{__docs_line_to_html__ l, parse_log}</li>\n"
+ else
+ if inside_ul
+ inside_ul = false
+ parse_log << "- UL end detected."
+ content_html << "</ul>\n"
+ end
+
+ if inside_ol
+ inside_ol = false
+ parse_log << "- OL end detected."
+ content_html << "</ol>\n"
+ end
+
+ if inside_pre
+ content_html << "#{l.rstrip[2..-1]}\n"
+ else
+ parse_log << "- P detected."
+ content_html << "<p>\n#{__docs_line_to_html__ l, parse_log}\n</p>\n"
+ end
+ end
+ end
+ toc += "</ul>"
+
+ final_html = (html_string.gsub "{{toc}}", toc)
+ final_html = (final_html.gsub "{{content}}", content_html)
+
+ {
+ original: string,
+ html: final_html,
+ parse_log: parse_log
+ }
+ end
+
+ def __docs_line_to_html__ line, parse_log
+ line = line.gsub "* DOCS: ", "" if line.start_with? "* DOCS: "
+ line = line.gsub "* ", "" if line.start_with? "* "
+ line = line.gsub "** ", "" if line.start_with? "** "
+ line = line.gsub "*** ", "" if line.start_with? "*** "
+
+ tilde_count = line.count "~"
+ line_has_link_marker = (line.include? "[[") && (line.include? "]]")
+ parse_log << "- Line's tilde count is: #{tilde_count}"
+ parse_log << "- Line contains link marker: #{line_has_link_marker}"
+
+ line_to_format = line.rstrip
+
+ # <code> logic
+ if tilde_count.even? && tilde_count != 0
+ parse_log << "- CODE detected."
+ temp = line_to_format
+ line_to_format = ""
+ in_literal = false
+ in_code = false
+ temp.each_char do |c|
+ if c == "~" && !in_code
+ in_code = true
+ line_to_format << "<code>"
+ elsif c == "~" && in_code
+ line_to_format << "</code>"
+ else
+ line_to_format << c
+ end
+ end
+ end
+
+ # <a> and <img> logic
+ if line_has_link_marker
+ line_to_format = line_to_format.gsub "[[", "["
+ line_to_format = line_to_format.gsub "]]", "]"
+ parse_log << "- LINK detected."
+ temp = line_to_format
+ line_to_format = ""
+ in_literal = false
+ in_link = false
+ link_url = ""
+ temp.each_char.with_index do |c, i|
+ next_c = temp[i + 1]
+ if !in_link && c == "["
+ in_link = true
+ link_url = ""
+ elsif in_link && c == "]"
+ if link_url.end_with? ".gif"
+ line_to_format << "<img src='#{link_url}'></img>"
+ else
+ line_to_format << "<a href='#{link_url}'>#{link_url}</a>"
+ end
+ in_link = false
+ elsif in_link
+ link_url << c
+ else
+ line_to_format << c
+ end
+ end
+ end
+
+ return line_to_format
+ rescue Exception => e
+ parse_log << "Failed to parse line: ~#{line}~, #{e}"
+ return line.rstrip
end
end
diff --git a/dragon/geometry.rb b/dragon/geometry.rb
index 67e6a08..1be0379 100644
--- a/dragon/geometry.rb
+++ b/dragon/geometry.rb
@@ -189,6 +189,10 @@ S
[x, y]
end
+ def self.contract_intersect_rect?
+ [:left, :right, :top, :bottom]
+ end
+
# @gtk
def self.intersect_rect? rect_one, rect_two, tolerance = 0.1
return false if rect_one.right - tolerance < rect_two.left + tolerance
@@ -197,7 +201,32 @@ S
return false if rect_one.bottom + tolerance > rect_two.top - tolerance
return true
rescue Exception => e
- raise e, ":intersect_rect? failed for rect_one: #{rect_one} rect_two: #{rect_two}."
+ context_help_rect_one = (rect_one.help_contract_implementation contract_intersect_rect?)[:not_implemented_methods]
+ context_help_rect_two = (rect_two.help_contract_implementation contract_intersect_rect?)[:not_implemented_methods]
+ context_help = ""
+ if context_help_rect_one && context_help_rect_one.length > 0
+ context_help += <<-S
+rect_one needs to implement the following methods: #{context_help_rect_one}
+
+You may want to try include the ~AttrRect~ module which will give you these methods.
+S
+ end
+
+ if context_help_rect_two && context_help_rect_two.length > 0
+ context_help += <<-S
+* FAILURE REASON:
+rect_two needs to implement the following methods: #{context_help_rect_two}
+NOTE: You may want to try include the ~GTK::Geometry~ module which will give you these methods.
+S
+ end
+
+ raise e, <<-S
+* ERROR:
+:intersect_rect? failed for
+- rect_one: #{rect_one}
+- rect_two: #{rect_two}
+#{context_help}
+S
end
# @gtk
diff --git a/dragon/kernel_docs.rb b/dragon/kernel_docs.rb
new file mode 100644
index 0000000..0a91b4d
--- /dev/null
+++ b/dragon/kernel_docs.rb
@@ -0,0 +1,71 @@
+# coding: utf-8
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# kernel_docs.rb has been released under MIT (*only this file*).
+
+module KernelDocs
+ def docs_method_sort_order
+ [:docs_class, :docs_tick_count, :docs_global_tick_count]
+ end
+
+ def docs_class
+ <<-S
+* DOCS: ~Kernel~
+
+Kernel in the DragonRuby Runtime has patches for how standard out is handled and also
+contains a unit of time in games called a tick.
+
+S
+ end
+
+ def docs_tick_count
+ <<-S
+* DOCS: ~Kernel::tick_count~
+
+Returns the current tick of the game. This value is reset if you call $gtk.reset.
+
+S
+ end
+
+ def docs_global_tick_count
+ <<-S
+* DOCS: ~Kernel::global_tick_count~
+
+Returns the current tick of the application from the point it was started. This value is never reset.
+
+S
+ end
+
+ def docs_export_docs!
+ <<-S
+* DOCS: ~Kernel::export_docs!~
+
+Exports all GTK documentation to txt files and saves them to a docs directory.
+
+S
+ end
+
+ def export_docs!
+ DocsOrganizer.sort_docs_classes!
+ final_string = ""
+ $docs_classes.each do |k|
+ final_string += k.docs_all
+ end
+
+ html_parse_result = (__docs_to_html__ final_string)
+
+ $gtk.write_file 'docs/docs.txt', "#{final_string}"
+ $gtk.write_file 'docs/docs.html', html_parse_result[:html]
+ $gtk.write_file 'docs/parse_log.txt', (html_parse_result[:parse_log].join "\n")
+
+ log "* INFO: All docs have been exported to docs/docs.txt."
+ log "* INFO: All docs have been exported to docs/docs.html."
+
+ nil
+ end
+end
+
+module Kernel
+ extend Docs
+ extend KernelDocs
+end
diff --git a/dragon/readme_docs.rb b/dragon/readme_docs.rb
new file mode 100644
index 0000000..cddc866
--- /dev/null
+++ b/dragon/readme_docs.rb
@@ -0,0 +1,1170 @@
+# coding: utf-8
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# readme_docs.rb has been released under MIT (*only this file*).
+
+module GTK
+ module ReadMeDocs
+ def docs_method_sort_order
+ [
+ :docs_usage,
+ :docs_hello_world,
+ :docs_deployment,
+ :docs_dragonruby_philosophy,
+ :docs_ticks_and_frames,
+ :docs_sprites,
+ :docs_labels,
+ :docs_sounds,
+ :docs_game_state,
+ :docs_faq
+ ]
+ end
+
+ def docs_usage
+ <<-S
+* DragonRuby Game Toolkit Live Docs
+
+The information contained here is all available via the DragonRuby
+Console. You can Open the DragonRuby Console by pressing [`] [~] [²]
+[^] [º] or [§] within your game.
+
+To search docs you can type ~docs_search "SEARCH TERM"~ or if you want
+to get fancy you can provide a ~lambda~ to filter documentation:
+
+#+begin_src
+ docs_search { |entry| (entry.include? "Array") && (!entry.include? "Enumerable") }
+#+end_src
+
+[[docs_search.gif]]
+S
+ end
+
+ def docs_hello_world
+<<-S
+* Hello World
+
+Welcome to DragonRuby Game Toolkit. Take the steps below to get started.
+
+* Join the Discord and Subscribe to the News Letter
+
+Our Discord channel is [[http://discord.dragonruby.org]].
+
+The News Letter will keep you in the loop with regards to current
+DragonRuby Events: [[http://dragonrubydispatch.com]].
+
+Those who use DragonRuby are called Dragon Riders. This identity is
+incredibly important to us. When someone asks you:
+
+#+begin_quote
+What game engine do you use?
+#+end_quote
+
+Reply with:
+
+#+begin_quote
+I am a Dragon Rider.
+#+end_quote
+
+* Watch Some Intro Videos
+
+Each video is only 20 minutes and all of them will fit into a lunch
+break. So please watch them:
+
+1. Beginner Introduction to DragonRuby Game Toolkit: [[https://youtu.be/ixw7TJhU08E]]
+2. Intermediate Introduction to Ruby Syntax: [[https://youtu.be/HG-XRZ5Ppgc]]
+3. Intermediate Introduction to Arrays in Ruby: [[https://youtu.be/N72sEYFRqfo]]
+
+The second and third videos are not required if you are proficient
+with Ruby, but *definitely* watch the first one.
+
+You may also want to try this free course provided at
+[[http://dragonruby.school]].
+
+* Getting Started Tutorial
+
+This is a tutorial written by Ryan C Gordon (a Juggernaut in the
+industry who has contracted to Valve, Epic, Activision, and
+EA... check out his Wikipedia page: [[https://en.wikipedia.org/wiki/Ryan_C._Gordon]]).
+
+** Introduction
+
+Welcome!
+
+Here's just a little push to get you started if you're new to
+programming or game development.
+
+If you want to write a game, it's no different than writing any other
+program for any other framework: there are a few simple rules that
+might be new to you, but more or less programming is programming no
+matter what you are building.
+
+Did you not know that? Did you think you couldn't write a game because
+you're a "web guy" or you're writing Java at a desk job? Stop letting
+people tell you that you can't, because you already have everything
+you need.
+
+Here, we're going to be programming in a language called "Ruby." In
+the interest of full disclosure, I (Ryan Gordon) wrote the C parts of
+this toolkit and Ruby looks a little strange to me (Amir Rajan wrote
+the Ruby parts, discounting the parts I mangled), but I'm going to
+walk you through the basics because we're all learning together, and
+if you mostly think of yourself as someone that writes C (or C++, C#,
+Objective-C), PHP, or Java, then you're only a step behind me right
+now.
+
+** Prerequisites
+
+Here's the most important thing you should know: Ruby lets you do some
+complicated things really easily, and you can learn that stuff
+later. I'm going to show you one or two cool tricks, but that's all.
+
+Do you know what an if statement is? A for-loop? An array? That's all
+you'll need to start.
+
+** The Game Loop
+
+Ok, here are few rules with regards to game development with GTK:
+
+- Your game is all going to happen under one function ...
+- that runs 60 times a second ...
+- and has to tell the computer what to draw each time.
+
+That's an entire video game in one run-on sentence.
+
+Here's that function. You're going to want to put this in
+mygame/app/main.rb, because that's where we'll look for it by
+default. Load it up in your favorite text editor.
+
+#+begin_src ruby
+ def tick args
+ args.outputs.labels << [580, 400, 'Hello World!']
+ end
+#+end_src
+
+Now run ~dragonruby~ ...did you get a window with "Hello World!"
+written in it? Good, you're officially a game developer!
+
+** Breakdown Of The ~tick~ Method
+
+~mygame/app/main.rb~, is where the Ruby source code is located. This
+looks a little strange, so I'll break it down line by line. In Ruby, a
+'#' character starts a single-line comment, so I'll talk about this
+inline.
+
+#+begin_src ruby
+ # This "def"ines a function, named "tick," which takes a single argument
+ # named "args". DragonRuby looks for this function and calls it every
+ # frame, 60 times a second. "args" is a magic structure with lots of
+ # information in it. You can set variables in there for your own game state,
+ # and every frame it will updated if keys are pressed, joysticks moved,
+ # mice clicked, etc.
+ def tick args
+
+ # One of the things in "args" is the "outputs" object that your game uses
+ # to draw things. Afraid of rendering APIs? No problem. In DragonRuby,
+ # you use arrays to draw things and we figure out the details.
+ # If you want to draw text on the screen, you give it an array (the thing
+ # in the [ brackets ]), with an X and Y coordinate and the text to draw.
+ # The "<<" thing says "append this array onto the list of them at
+ # args.outputs.labels)
+ args.outputs.labels << [580, 400, 'Hello World!']
+ end
+#+end_src
+
+Once your ~tick~ function finishes, we look at all the arrays you made
+and figure out how to draw it. You don't need to know about graphics
+APIs. You're just setting up some arrays! DragonRuby clears out these
+arrays every frame, so you just need to add what you need _right now_
+each time.
+
+** Rendering A Sprite
+
+Now let's spice this up a little.
+
+We're going to add some graphics. Each 2D image in DragonRuby is
+called a "sprite," and to use them, you just make sure they exist in a
+reasonable file format (png, jpg, gif, bmp, etc) and specify them by
+filename. The first time you use one, DragonRuby will load it and keep
+it in video memory for fast access in the future. If you use a
+filename that doesn't exist, you get a fun checkerboard pattern!
+
+There's a "dragonruby.png" file included, just to get you
+started. Let's have it draw every frame with our text:
+
+#+begin_src ruby
+ def tick args
+ args.outputs.labels << [580, 400, 'Hello World!']
+ args.outputs.sprites << [576, 100, 128, 101, 'dragonruby.png']
+ end
+#+end_src
+
+(Pro Tip: you don't have to restart DragonRuby to test your changes;
+when you save main.rb, DragonRuby will notice and reload your
+program.)
+
+That ~.sprites~ line says "add a sprite to the list of sprites we're
+drawing, and draw it at position (576, 100) at a size of 128x101
+pixels". You can find the image to draw at dragonruby.png.
+
+** Coordinate System and Virtual Canvas
+
+Quick note about coordinates: (0, 0) is the bottom left corner of the
+screen, and positive numbers go up and to the right. This is more
+"geometrically correct," even if it's not how you remember doing 2D
+graphics, but we chose this for a simpler reason: when you're making
+Super Mario Brothers and you want Mario to jump, you should be able to
+add to Mario's y position as he goes up and subtract as he falls. It
+makes things easier to understand.
+
+Also: your game screen is _always_ 1280x720 pixels. If you resize the
+window, we will scale and letterbox everything appropriately, so you
+never have to worry about different resolutions.
+
+Ok, now we have an image on the screen, let's animate it:
+
+#+begin_src ruby
+ def tick args
+ args.state.rotation ||= 0
+ args.outputs.labels << [580, 400, 'Hello World!' ]
+ args.outputs.sprites << [576, 100, 128, 101, 'dragonruby.png', args.state.rotation]
+ args.state.rotation -= 1
+ end
+#+end_src
+
+Now you can see that this function is getting called a lot!
+
+** Game State
+
+Here's a fun Ruby thing: ~args.state.rotation ||= 0~ is shorthand for
+"if args.state.rotation isn't initialized, set it to zero." It's a
+nice way to embed your initialization code right next to where you
+need the variable.
+
+~args.state~ is a place you can hang your own data and have it survive
+past the life of the function call. In this case, the current rotation
+of our sprite, which is happily spinning at 60 frames per second. If
+you don't specify rotation (or alpha, or color modulation, or a source
+rectangle, etc), DragonRuby picks a reasonable default, and the array
+is ordered by the most likely things you need to tell us: position,
+size, name.
+
+** There Is No Delta Time
+
+One thing we decided to do in DragonRuby is not make you worry about
+delta time: your function runs at 60 frames per second (about 16
+milliseconds) and that's that. Having to worry about framerate is
+something massive triple-AAA games do, but for fun little 2D games?
+You'd have to work really hard to not hit 60fps. All your drawing is
+happening on a GPU designed to run Fortnite quickly; it can definitely
+handle this.
+
+Since we didn't make you worry about delta time, you can just move the
+rotation by 1 every time and it works without you having to keep track
+of time and math. Want it to move faster? Subtract 2.
+
+** Handling User Input
+
+Now, let's move that image around.
+
+#+begin_src ruby
+ def tick args
+ args.state.rotation ||= 0
+ args.state.x ||= 576
+ args.state.y ||= 100
+
+ if args.inputs.mouse.click
+ args.state.x = args.inputs.mouse.click.point.x - 64
+ args.state.y = args.inputs.mouse.click.point.y - 50
+ end
+
+ args.outputs.labels << [580, 400, 'Hello World!']
+ args.outputs.sprites << [args.state.x,
+ args.state.y,
+ 128,
+ 101,
+ 'dragonruby.png',
+ args.state.rotation]
+
+ args.state.rotation -= 1
+ end
+#+end_src
+
+Everywhere you click your mouse, the image moves there. We set a
+default location for it with ~args.state.x ||= 576~, and then we
+change those variables when we see the mouse button in action. You can
+get at the keyboard and game controllers in similar ways.
+
+** Coding On A Raspberry Pi
+
+We have only tested DragonRuby on a Raspberry Pi 3, Models B and B+, but we
+believe it _should_ work on any model with comparable specs.
+
+If you're running DragonRuby Game Toolkit on a Raspberry Pi, or trying to run
+a game made with the Toolkit on a Raspberry Pi, and it's really really slow--
+like one frame every few seconds--then there's likely a simple fix.
+
+You're probably running a desktop environment: menus, apps, web browsers,
+etc. This is okay! Launch the terminal app and type:
+
+#+begin_src
+sudo raspi-config
+#+end_src
+
+It'll ask you for your password (if you don't know, try "raspberry"), and then
+give you a menu of options. Find your way to "Advanced Options", then "GL
+Driver", and change this to "GL (Full KMS)" ... not "fake KMS," which is
+also listed there. Save and reboot. In theory, this should fix the problem.
+
+If you're _still_ having problems and have a Raspberry Pi 2 or better, go back
+to raspi-config and head over to "Advanced Options", "Memory split," and give
+the GPU 256 megabytes. You might be able to avoid this for simple games, as
+this takes RAM away from the system and reserves it for graphics. You can
+also try 128 megabytes as a gentler option.
+
+Note that you can also run DragonRuby without X11 at all: if you run it from
+a virtual terminal it will render fullscreen and won't need the "Full KMS"
+option. This might be attractive if you want to use it as a game console
+sort of thing, or develop over ssh, or launch it from RetroPie, etc.
+
+** Conclusion
+
+There is a lot more you can do with DragonRuby, but now you've already
+got just about everything you need to make a simple game. After all,
+even the most fancy games are just creating objects and moving them
+around. Experiment a little. Add a few more things and have them
+interact in small ways. Want something to go away? Just don't add it
+to ~args.output~ anymore.
+
+** IMPORTANT: Go Through All Of The Sample Apps! Study Them Thoroughly!!
+
+Now that you've completed the Hello World tutorial. Head over to the
+`samples` directory. It is very very important that you study the
+sample apps thoroughly! Go through them in order. Here is a short
+description of each sample app.
+
+1. 00_beginner_ruby_primer: This is an interactive tutorial that shows how to render ~solid~s, animated ~sprite~s, ~label~s.
+2. 00_intermediate_ruby_primer: This is a set of sample Ruby snippets that give you a high level introduction to the programming language.
+3. 01_api_01_labels: Various ways to render ~label~s.
+4. 01_api_02_lines: Various ways to render ~line~s.
+5. 01_api_03_rects: Sample app shows various ways to render ~solid~s and ~border~s.
+6. 01_api_04_sprites: Sample app shows various ways to render ~sprite~s.
+7. 01_api_05_keyboard: Hows how to get keyboard input from the user.
+8. 01_api_06_mouse: Hows how to get mouse mouse position.
+9. 01_api_07_point_to_rect: How to get mouse input from the user and shows collision/hit detection.
+10. 01_api_08_rect_to_rect: Hit detection/collision between two rectangles.
+11. 01_api_10_controller: Interaction with a USB/Bluetooth controller.
+12. 01_api_99_tech_demo: All the different render primitives along with using ~render_targets~.
+13. 02_collision_01_simple: Collision detection with dynamically moving bodies.
+14. 02_collision_02_moving_objects: Collision detection between many primitives, simple platformer physics, and keyboard input.
+15. 02_collision_03_entities: Collision with entities and serves as a small introduction to ECS (entity component system).
+16. 02_collision_04_ramp_with_debugging: How ramp trajectory can be calculated.
+17. 02_collision_05_ramp_with_debugging_two: How ramp trajectory can be calculated.
+18. 02_sprite_animation_and_keyboard_input: How to animate a sprite based off of keyboard input.
+19. 03_mouse_click: How to determine what direction/vector a mouse was clicked relative to a player.
+20. 04_sounds: How to play sounds and work with buttons.
+21. 05_mouse_move: How to determine what direction/vector a mouse was clicked relative to a player.
+22. 05_mouse_move_paint_app: Represents a simple paint app.
+23. 05_mouse_move_tile_editor: A starting point for a tile editor.
+24. 06_coordinate_systems: Shows the two origin systems within Game Toolkit where the origin is in the center and where the origin is at the bottom left.
+25. 07_render_targets: Shows a powerful concept called ~render_target~s. You can use this to programatically create sprites (it's also useful for representing parts of a scene as if it was a view port/camera).
+26. 07_render_targets_advanced: Advanced usage of ~render_target~s.
+27. 08_platformer_collisions: Axis aligned collision along with platformer physics.
+28. 08_platformer_collisions_metroidvania: How to save map data and place sprites live within a game.
+29. 08_platformer_jumping_inertia: Jump physics and how inertia affects collision.
+30. 09_controller_analog_usage_advanced_sprites: Extended properties of a ~sprite~ and how to change the rotation anchor point and render a subset/tile of a sprite.
+31. 09_sprite_animation_using_tile_sheet: How to perform sprite animates using a tile sheet.
+32. 10_save_load_game: Save and load game data.
+33. 11_coersion_of_primitives: How primitives of one specific type can be rendered as another primitive type.
+34. 11_hash_primitives: How primitives can be represented using a ~Hash~.
+35. 12_controller_input_sprite_sheet_animations: How to leverage vectors to move a player around the screen.
+36. 12_top_down_area: How to render a top down map and how to manage collision of a player.
+37. 13_01_easing_functions: How to use lerping functions to define animations/movement.
+38. 13_02_cubic_bezier: How to create a bezier curve using lines.
+39. 13_03_easing_using_spline: How a collection of bezier curves can be used to define an animation.
+40. 13_04_parametric_enemy_movement: How to define the movement of enemies and projectiles using lerping/parametric functions.
+41. 14_sprite_limits: Upper limit for how many sprites can be rendered to the screen.
+42. 14_sprite_limits_static_references: Upper limit for how many sprites can be rendered to the screen using ~static~ output collections (which are updated by reference as opposed to by value).
+43. 15_collision_limits: How many collisions can be processed across many primitives.
+44. 18_moddable_game: How you can make a game where content is authored by the player (modding support).
+45. 19_lowrez_jam_01_hello_world: How to use ~render_targets~ to create a low resolution game.
+46. 19_lowrez_jam_02_buttons: How to use ~render_targets~ to create a low resolution game.
+47. 19_lowrez_jam_03_space_shooter: How to use ~render_targets~ to create a low resolution game.
+48. 20_roguelike_starting_point: A starting point for a roguelike and explores concepts such as line of sight.
+49. 20_roguelike_starting_point_two: A starting point for a roguelike where sprites are provided from a tile map/tile sheet.
+50. 21_mailbox_usage: How to do interprocess communication.
+51. 22_trace_debugging: Debugging techniques and tracing execution through your game.
+52. 22_trace_debugging_classes: Debugging techniques and tracing execution through your game.
+53. 23_hexagonal_grid: How to make a tactical grid/map made of hexagons.
+54. 23_isometric_grid: How to make a tactical grid/map made of isometric sprites.
+55. 24_http_example: How to make http requests.
+56. 25_3d_experiment_01_square: How to create 3D objects.
+57. 26_jam_craft: Starting point for crafting game. It also shows how to customize the mouse cursor.
+58. 99_sample_game_basic_gorillas: Reference implementation of a full game. Topics covered: physics, keyboard input, collision, sprite animation.
+59. 99_sample_game_clepto_frog: Reference implementation of a full game. Topics covered: camera control, spring/rope physics, scene orchestration.
+60. 99_sample_game_dueling_starships: Reference implementation that shows local multiplayer. Topics covered: vectors, particles, friction, inertia.
+61. 99_sample_game_flappy_dragon: Reference implementation that is a clone of Flappy Bird. Topics covered: scene orchestration, collision, sound, sprite animations, lerping.
+62. 99_sample_game_pong: Reference implementation of pong.
+63. 99_sample_game_return_of_serenity: Reference implementation of low resolution story based game.
+64. 99_sample_game_the_little_probe: Reference implementation of a full game. Topics covered: Arbitrary collision detection, loading map data, bounce/ball physics.
+65. 99_sample_nddnug_workshop: Reference implementation of a full game. Topics covered: vectors, controller input, sound, trig functions.
+66. 99_sample_snakemoji: Shows that Ruby supports coding with emojis.
+67. 99_zz_gtk_unit_tests: A collection of unit tests that exercise parts of DragonRuby's API.
+</ol> <!-- FIXME -->
+
+S
+ end
+
+ def docs_deployment
+<<-S
+
+* Deploying To Itch.io
+
+Once you've built your game, you're all set to deploy! Good luck in
+your game dev journey and if you get stuck, come to the Discord
+channel!
+
+** Creating Your Game Landing Page
+
+Log into Itch.io and go to [[https://itch.io/game/new]].
+
+- Title: Give your game a Title. This value represents your `gametitle`.
+- Project URL: Set your project url. This value represents your `gameid`.
+- Classification: Keep this as Game.
+- Kind of Project: Select HTML from the drop down list. Don't worry,
+ the HTML project type _also supports binary downloads_.
+- Uploads: Skip this section for now.
+
+You can fill out all the other options later.
+
+** Update Your Game's Metadata
+
+Point your text editor at mygame/metadata/game_metadata.txt and
+make it look like this:
+
+NOTE: Remove the ~#~ at the beginning of each line.
+
+#+begin_src
+devid=bob
+devtitle=Bob The Game Developer
+gameid=mygame
+gametitle=My Game
+version=0.1
+#+end_src
+
+The ~devid~ property is the username you use to log into Itch.io.
+The ~devtitle~ is your name or company name (it can contain spaces).
+The ~gameid~ is the Project URL value.
+The ~gametitle~ is the name of your game (it can contain spaces).
+The ~version~ can be any ~major.minor~ number format.
+
+** Building Your Game For Distribution
+
+Open up the terminal and run this from the command line:
+
+#+begin_src
+./dragonruby-publish --only-package mygame
+#+end_src
+
+(if you're on Windows, don't put the "./" on the front. That's a Mac and
+Linux thing.)
+
+A directory called ~./build~ will be created that contains your
+binaries. You can upload this to Itch.io manually.
+
+For the HTML version of your game after you upload it. Check the checkbox labeled
+"This file will be played in the browser".
+
+For subsequent updates you can use an automated deployment to Itch.io:
+
+#+begin_src
+./dragonruby-publish mygame
+#+end_src
+
+DragonRuby will package _and publish_ your game to itch.io! Tell your
+friends to go to your game's very own webpage and buy it!
+
+If you make changes to your game, just re-run dragonruby-publish and it'll
+update the downloads for you.
+S
+ end
+
+ def docs_dragonruby_philosophy
+ <<-S
+** DragonRuby's Philosophy
+
+The following tenants of DragonRuby are what set us apart from other
+game engines. Given that Game Toolkit is a relatively new engine,
+there are definitely features that are missing. So having a big check
+list of "all the cool things" is not this engine's forte. This is
+compensated with a strong commitment to the following principals.
+
+*** Challenge The Status Quo
+
+Game engines of today are in a local maximum and don't take into
+consideration the challenges of this day and age. Unity and GameMaker
+specifically rot your brain. It's not sufficient to say:
+
+#+begin_quote
+But that's how we've always done it.
+#+end_quote
+
+It's a hard pill to swallow, but forget blindly accepted best
+practices and try to figure out the underlying motivation for a
+specific approach to game development. Collaborate with us.
+
+*** Release Often And Quickly
+
+The biggest mistake game devs make is spending too much time in
+isolation building their game. Release something, however small, and
+release it quickly.
+
+Stop worrying about everything being pixel perfect. Don't wait until
+your game is 100% complete. Build your game publicly and
+iterate. Post in the #show-and-tell channel in the community Discord.
+You'll find a lot of support and encouragement there.
+
+Remember:
+
+#+begin_quote
+Real artists ship.
+#+end_quote
+
+*** Sustainable And Ethical Monetization
+
+We all aspire to put food on the table doing what we love. Whether it
+is building games, writing tools to support game development, or
+anything in between.
+
+Charge a fair amount of money for the things you create. It's expected
+and encouraged within the community. Give what you create away for
+free to those that can't afford it.
+
+*** Sustainable And Ethical Open Source
+
+This goes hand in hand with sustainable and ethical monetization. The
+current state of open source is not sustainable. There is an immense
+amount of contributor burnout. Users of open source expect everything
+to be free, and few give back. This is a problem we want to fix (we're
+still trying to figure out the best solution).
+
+So, don't be "that guy" in the Discord that says "DragonRuby should be
+free and open source!" You will be personally flogged by Amir.
+
+*** People Over Entities
+
+We prioritize the endorsement of real people over faceless
+entities. This game engine, and other products we create, are not
+insignificant line items of a large company. And you aren't a generic
+"commodity" or "corporate resource". So be active in the community
+Discord and you'll reap the benefits as more devs use DragonRuby.
+
+*** Building A Game Should Be Fun And Bring Happiness
+
+We will prioritize the removal of pain. The aesthetics of Ruby make it
+such a joy to work with, and we want to capture that within the
+engine.
+
+*** Real World Application Drives Features
+
+We are bombarded by marketing speak day in and day out. We don't do
+that here. There are things that are really great in the engine, and
+things that need a lot of work. Collaborate with us so we can help you
+reach your goals. Ask for features you actually need as opposed to
+anything speculative.
+
+We want DragonRuby to *actually* help you build the game you
+want to build (as opposed to sell you something piece of demoware that
+doesn't work).
+S
+ end
+
+ def docs_ticks_and_frames
+ <<-S
+* How To Determine What Frame You Are On
+
+There is a property on ~state~ called ~tick_count~ that is incremented
+by DragonRuby every time the ~tick~ method is called. The following
+code renders a label that displays the current ~tick_count~.
+
+#+begin_src ruby
+ def tick args
+ args.outputs.labels << [10, 670, "\#{args.state.tick_count}"]
+ end
+#+end_src
+
+* How To Get Current Framerate
+
+Current framerate is a top level property on the Game Toolkit Runtime
+and is accessible via ~args.gtk.current_framerate~.
+
+#+begin_src ruby
+ def tick args
+ args.outputs.labels << [10, 710, "framerate: \#{args.gtk.current_framerate.round}"]
+ end
+#+end_src
+S
+ end
+
+ def docs_sprites
+ <<-S
+* How To Render A Sprite Using An Array
+
+All file paths should use the forward slash ~/~ *not* backslash
+~\~. Game Toolkit includes a number of sprites in the ~sprites~
+folder (everything about your game is located in the ~mygame~ directory).
+
+The following code renders a sprite with a ~width~ and ~height~ of
+~100~ in the center of the screen.
+
+~args.outputs.sprites~ is used to render a sprite.
+
+#+begin_src ruby
+ def tick args
+ args.outputs.sprites << [
+ 640 - 50, # X
+ 360 - 50, # Y
+ 100, # W
+ 100, # H
+ 'sprites/square-blue.png' # PATH
+ ]
+ end
+#+end_src
+
+* More Sprite Properties As An Array
+
+Here are all the properties you can set on a sprite.
+
+#+begin_src ruby
+ def tick args
+ args.outputs.sprites << [
+ 100, # X
+ 100, # Y
+ 32, # W
+ 64, # H
+ 'sprites/square-blue.png', # PATH
+ 0, # ANGLE
+ 255, # ALPHA
+ 0, # RED_SATURATION
+ 255, # GREEN_SATURATION
+ 0 # BLUE_SATURATION
+ ]
+ end
+#+end_src
+
+* Different Sprite Representations
+
+Using ordinal positioning can get a little unruly given so many
+properties you have control over.
+
+You can represent a sprite as a ~Hash~:
+
+#+begin_src ruby
+ def tick args
+ args.outputs.sprites << {
+ x: 640 - 50,
+ y: 360 - 50,
+ w: 100,
+ h: 100,
+ path: 'sprites/square-blue.png',
+ angle: 0,
+ a: 255,
+ r: 255,
+ g: 255,
+ b: 255,
+ source_x: 0,
+ source_y: 0,
+ source_w: -1,
+ source_h: -1,
+ flip_vertically: false,
+ flip_horizontally: false,
+ angle_anchor_x: 0.5,
+ angle_anchor_y: 1.0
+ }
+ end
+#+end_src
+
+You can represent a sprite as an ~object~:
+
+#+begin_src ruby
+ # Create type with ALL sprite properties AND primitive_marker
+ class Sprite
+ attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b,
+ :source_x, :source_y, :source_w, :source_h,
+ :tile_x, :tile_y, :tile_w, :tile_h,
+ :flip_horizontally, :flip_vertically,
+ :angle_anchor_x, :angle_anchor_y
+
+ def primitive_marker
+ :sprite
+ end
+ end
+
+ class BlueSquare < Sprite
+ def initialize opts
+ @x = opts[:x]
+ @y = opts[:y]
+ @w = opts[:w]
+ @h = opts[:h]
+ @path = 'sprites/square-blue.png'
+ end
+ end
+
+ def tick args
+ args.outputs.sprites << (BlueSquare.new x: 640 - 50,
+ y: 360 - 50,
+ w: 50,
+ h: 50)
+ end
+#+end_src
+S
+ end
+
+ def docs_labels
+ <<-S
+* How To Render A Label
+
+~args.outputs.labels~ is used to render labels.
+
+Labels are how you display text. This code will go directly inside of
+the ~def tick args~ method.
+
+Here is the minimum code:
+
+#+begin_src
+ def tick args
+ # X Y TEXT
+ args.outputs.labels << [640, 360, "I am a black label."]
+ end
+#+end_src
+
+* A Colored Label
+
+#+begin_src
+ def tick args
+ # A colored label
+ # X Y TEXT, RED GREEN BLUE ALPHA
+ args.outputs.labels << [640, 360, "I am a redish label.", 255, 128, 128, 255]
+ end
+#+end_src
+
+* Extended Label Properties
+
+#+begin_src
+ def tick args
+ # A colored label
+ # X Y TEXT SIZE ALIGNMENT RED GREEN BLUE ALPHA FONT FILE
+ args.outputs.labels << [
+ 640, # X
+ 360, # Y
+ "Hello world", # TEXT
+ 0, # SIZE_ENUM
+ 1, # ALIGNMENT_ENUM
+ 0, # RED
+ 0, # GREEN
+ 0, # BLUE
+ 255, # ALPHA
+ "fonts/coolfont.ttf" # FONT
+ ]
+ end
+#+end_src
+
+A ~SIZE_ENUM~ of ~0~ represents "default size". A ~negative~ value
+will decrease the label size. A ~positive~ value will increase the
+label's size.
+
+An ~ALIGNMENT_ENUM~ of ~0~ represents "left aligned". ~1~ represents
+"center aligned". ~2~ represents "right aligned".
+
+* Rendering A Label As A ~Hash~
+
+You can add additional metadata about your game within a label, which requires you to use a `Hash` instead.
+
+#+begin_src
+ def tick args
+ args.outputs.labels << {
+ x: 200,
+ y: 550,
+ text: "dragonruby",
+ size_enum: 2,
+ alignment_enum: 1,
+ r: 155,
+ g: 50,
+ b: 50,
+ a: 255,
+ font: "fonts/manaspc.ttf",
+ # You can add any properties you like (this will be ignored/won't cause errors)
+ game_data_one: "Something",
+ game_data_two: {
+ value_1: "value",
+ value_2: "value two",
+ a_number: 15
+ }
+ }
+ end
+#+end_src
+
+* Getting The Size Of A Piece Of Text
+
+You can get the render size of any string using ~args.gtk.calcstringbox~.
+
+#+begin_src ruby
+ def tick args
+ # TEXT SIZE_ENUM FONT
+ w, h = args.gtk.calcstringbox("some string", 0, "font.ttf")
+
+ # NOTE: The SIZE_ENUM and FONT are optional arguments.
+
+ # Render a label showing the w and h of the text:
+ args.outputs.labels << [
+ 10,
+ 710,
+ # This string uses Ruby's string interpolation literal: \#{}
+ "'some string' has width: \#{w}, and height: \#{h}."
+ ]
+ end
+#+end_src
+S
+ end
+
+ def docs_sounds
+ <<-S
+* How To Play A Sound
+
+Sounds that end ~.wav~ will play once:
+
+#+begin_src ruby
+ def tick args
+ # Play a sound every second
+ if (args.state.tick_count % 60) == 0
+ args.outputs.sounds << 'something.wav'
+ end
+ end
+#+end_src
+
+Sounds that end ~.ogg~ is considered background music and will loop:
+
+#+begin_src ruby
+ def tick args
+ # Start a sound loop at the beginning of the game
+ if args.state.tick_count == 0
+ args.outputs.sounds << 'background_music.ogg'
+ end
+ end
+#+end_src
+
+If you want to play a ~.ogg~ once as if it were a sound effect, you can do:
+
+#+begin_src ruby
+ def tick args
+ # Play a sound every second
+ if (args.state.tick_count % 60) == 0
+ args.gtk.queue_sound 'some-ogg.ogg'
+ end
+ end
+#+end_src
+S
+ end
+
+ def docs_game_state
+ <<-S
+* Using ~args.state~ To Store Your Game State
+
+~args.state~ is a open data structure that allows you to define
+properties that are arbitrarily nested. You don't need to define any kind of
+~class~.
+
+To initialize your game state, use the ~||=~ operator. Any value on
+the right side of ~||=~ will only be assigned _once_.
+
+To assign a value every frame, just use the ~=~ operator, but _make
+sure_ you've initialized a default value.
+
+#+begin_src
+ def tick args
+ # initialize your game state ONCE
+ args.player.x ||= 0
+ args.player.y ||= 0
+ args.player.hp ||= 100
+
+ # increment the x position of the character by one every frame
+ args.player.x += 1
+
+ # Render a sprite with a label above the sprite
+ args.outputs.sprites << [
+ args.player.x,
+ args.player.y,
+ 32, 32,
+ "player.png"
+ ]
+
+ args.outputs.labels << [
+ args.player.x,
+ args.player.y - 50,
+ args.player.hp
+ ]
+ end
+#+end_src
+S
+ end
+
+ def animate_a_sprite
+ <<-S
+* How To Animate A Sprite Using Separate PNGs
+
+DragonRuby has a property on ~Numeric~ called ~frame_index~ that can
+be used to determine what frame of an animation to show. Here is an
+example of how to cycle through 6 sprites every 4 frames.
+
+#+begin_src ruby
+ def tick args
+ start_looping_at = 0
+ number_of_sprites = 6
+ number_of_frames_to_show_each_sprite = 4
+ does_sprite_loop = true
+
+ sprite_index =
+ start_looping_at.frame_index number_of_sprites,
+ number_of_frames_to_show_each_sprite,
+ does_sprite_loop
+
+ sprite_index ||= 0
+
+ args.outputs.sprites << [
+ 640 - 50,
+ 360 - 50,
+ 100,
+ 100,
+ "sprites/dragon-\#{sprite_index}.png"
+ ]
+ end
+#+end_src
+S
+ end
+
+ def docs_faq
+<<-S
+* Frequently Asked Questions, Comments, and Concerns
+
+Here are questions, comments, and concerns that frequently come
+up.
+
+** Frequently Asked Questions
+
+*** What is DragonRuby LLP?
+
+DragonRuby LLP is a partnership of four devs who came together
+with the goal of bringing the aesthetics and joy of Ruby, everywhere possible.
+
+Under DragonRuby LLP, we offer a number of products (with more on the
+way):
+
+- Game Toolkit (GTK): A 2D game engine that is compatible with modern
+ gaming platforms. [Home Page]() [FAQ Page]()
+- RubyMotion (RM): A compiler toolchain that allows you to build native, cross-platform mobile
+ apps. [Home Page]() [FAQ Page]()
+- Commandline Toolkit (CTK): A zero dependency, zero installation Ruby
+ environment that works on Windows, Mac, and Linux. [Home Page]() [FAQ Page]()
+
+All of the products above leverage a shared core called DragonRuby.
+
+NOTE: From an official branding standpoint each one of the products is
+suffixed with "A DragonRuby LLP Product" tagline. Also, DragonRuby is
+_one word, title cased_.
+
+NOTE: We leave the "A DragonRuby LLP Product" off of this one because
+that just sounds really weird.
+
+NOTE: Devs who use DragonRuby are "Dragon Riders/Riders of Dragons". That's a bad ass
+identifier huh?
+
+*** What is DragonRuby?
+
+The response to this question requires a few subparts. First we need
+to clarify some terms. Specifically _language specification_ vs _runtime_.
+
+*** Okay... so what is the difference between a language specification and a runtime?
+
+A runtime is an _implementation_ of a langauge specification. When
+people say "Ruby," they are usually referring to "the Ruby 3.0+ language
+specification implemented via the CRuby/MRI Runtime."
+
+But, there are many Ruby Runtimes: CRuby/MRI, JRuby, Truffle, Rubinius, Artichoke,
+and (last but certainly not least) DragonRuby.
+
+*** Okay... what language specification does DragonRuby use then?
+
+DragonRuby's goal is to be compliant with the ISO/IEC 30170:2012 standard. It's
+syntax is Ruby 2.x compatible, but also contains semantic changes that help
+it natively interface with platform specific libraries.
+
+*** So... why another runtime?
+
+The elevator pitch is:
+
+DragonRuby is a Multilevel Cross-platform Runtime. The "multiple levels"
+within the runtime allows us to target platforms no other Ruby can
+target: PC, Mac, Linux, Raspberry Pi, WASM, iOS, Android, Nintendo
+Switch, PS4, Xbox, and Scadia.
+
+*** What does Multilevel Cross-platform mean?
+
+There are complexities associated with targeting all the platforms we
+support. Because of this, the runtime had to be architected in such a
+way that new platforms could be easily added (which lead to us partitioning the
+runtime internally):
+
+- Level 1 we leverage a good portion of mRuby.
+- Level 2 consists of optimizations to mRuby we've made given that our
+ target platforms are well known.
+- Level 3 consists of portable C libraries and their Ruby
+ C-Extensions.
+
+Levels 1 through 3 are fairly commonplace in many runtime
+implemenations (with level 1 being the most portable, and level 3
+being the fastest). But the DragonRuby Runtime has taken things a
+bit further:
+
+- Level 4 consists of shared abstractions around hardware I/O and operating
+ system resources. This level leverages open source and proprietary
+ components within Simple DirectMedia Layer (a lowlevel multimedia
+ component library that has been in active development for 22 years
+ and counting).
+
+- Level 5 is a codegeneration layer which creates metadata that allows
+ for native interopability with host runtime libraries. It also
+ includes OS specific message pump orchestrations.
+
+- Level 6 is a Ahead of Time/Just in Time Ruby compiler built with LLVM. This
+ compiler outputs _very_ fast platform specific bitcode, but only
+ supports a subset of the Ruby language specification.
+
+These levels allow us to stay up to date with open source
+implementations of Ruby; provide fast, native code execution
+on proprietary platforms; ensure good seperation between these two
+worlds; and provides a means to add new platforms without going insane.
+
+*** Cool cool. So given that I understand everything to this point, can we answer the original question? What is DragonRuby?
+
+DragonRuby is a Ruby runtime implentation that takes all the lessons
+we've learned from MRI/CRuby, and merges it with the latest and greatest
+compiler and OSS technologies.
+
+** Frequent Comments
+
+*** But Ruby is dead.
+
+Let's check the official source for the answer to this question:
+isrubydead.com: [[https://isrubydead.com/]].
+
+On a more serious note, Ruby's _quantity_ levels aren't what they used
+to be. And that's totally fine. Every one chases the new and shiny.
+
+What really matters is _quality/maturity_. Here is the latest (StackOverflow
+Survey sorted by highest paid developers)[https://insights.stackoverflow.com/survey/2019#top-paying-technologies].
+
+Let's stop making this comment shall we?
+
+*** But Ruby is slow.
+
+That doesn't make any sense. A language specification can't be
+slow... it's a language spec. Sure, an _implementation/runtime_ can be slow though, but then we'd
+have to talk about which runtime.
+
+*** Dynamic langauges are slow.
+
+They are certainly slower than statically compiled languages. With the
+processing power and compiler optimizations we have today,
+dynamic languages like Ruby are _fast enough_.
+
+Unless you are writing in some form of intermediate representation by hand,
+your langauge of choice also suffers this same fallacy of slow. Like, nothing is
+faster than a low level assembly-like language. So unless you're
+writing in that, let's stop making this comment.
+
+NOTE: If you _are_ hand writing LLVM IR, we are always open to
+bringing on new partners with such a skillset. Email us ^_^.
+
+** Frequent Concerns
+
+*** DragonRuby is not open source. That's not right.
+
+The current state of open source is unsustainable. Contributors work
+for free, most all open source repositories are serverly understaffed,
+and burnout from core members is rampant.
+
+We believe in open source very strongly. Parts of DragonRuby are
+infact, open source. Just not all of it (for legal reasons, and
+because the IP we've created has value). And we promise that we are
+looking for (or creating) ways to _sustainably_ open source everything we do.
+
+If you have ideas on how we can do this, email us!
+
+If the reason above isn't sufficient, then definitely use something else.
+
+*** DragonRuby is for pay. You should offer a free version.
+
+If you can afford to pay for DragonRuby, you should (and will). We don't go
+around telling writers that they should give us their books for free,
+and only require payment if we read the entire thing. It's time we stop asking that
+of software products.
+
+That being said, we will _never_ put someone out financially. We have
+income assistance for anyone that can't afford a license to any one of
+our products.
+
+You qualify for a free, unrestricted license to DragonRuby products if
+any of the following items pertain to you:
+
+- Your income is below $2,000.00 (USD) per month.
+- You are under 18 years of age.
+- You are a student of any type: traditional public school, home
+ schooling, college, bootcamp, or online.
+- You are a teacher, mentor, or parent who wants to teach a kid how to code.
+- You work/worked in public service or at a charitable organization:
+ for example public office, army, or any 501(c)(3) organization.
+
+Just contact Amir at [email protected] with a short
+explanation of your current situation and he'll set you up. No
+questions asked.
+
+*** But still, you should offer a free version. So I can try it out and see if I like it.
+
+You can try our [web-based sandbox environment](). But it won't do the
+runtime justice. Or just come to our [Slack]() or [Discord]() channel
+and ask questions. We'd be happy to have a one on one video chat with
+you and show off all the cool stuff we're doing.
+
+Seriously just buy it. Get a refund if you don't like it. We make it
+stupid easy to do so.
+
+*** I still think you should do a free version. Think of all people who would give it a shot.
+
+Free isn't a sustainable financial model. We don't want to spam your
+email. We don't want to collect usage data off of you either. We just
+want to provide quality toolchains to quality developers (as opposed
+to a large quantity of developers).
+
+The peiple that pay for DragonRuby and make an effort to understand it are the
+ones we want to build a community around, partner with, and collaborate
+with. So having that small monetary wall deters entitled individuals
+that don't value the same things we do.
+
+*** What if I build something with DragonRuby, but DragonRuby LLP becomes insolvent.
+
+That won't happen if the development world stop asking for free stuff
+and non-trivially compensate open source developers. Look, we want to be
+able to work on the stuff we love, every day of our lives. And we'll go
+to great lengths to make that happen.
+
+But, in the event that sad day comes, our partnershiop bylaws state that
+_all_ DragonRuby IP that can be legally open sourced, will be released
+under a permissive license.
+S
+ end
+ end
+
+ class ReadMe
+ extend Docs
+ extend ReadMeDocs
+ end
+end
diff --git a/dragon/runtime_docs.rb b/dragon/runtime_docs.rb
new file mode 100644
index 0000000..09759b4
--- /dev/null
+++ b/dragon/runtime_docs.rb
@@ -0,0 +1,39 @@
+# coding: utf-8
+# Copyright 2019 DragonRuby LLC
+# MIT License
+# runtime_docs.rb has been released under MIT (*only this file*).
+
+module RuntimeDocs
+ def docs_class
+ <<-S
+* DOCS: ~GTK::Runtime~
+The GTK::Runtime class is the core of DragonRuby. It is globally accessible via ~$gtk~.
+S
+ end
+
+ def docs_reset
+ <<-S
+* DOCS: ~GTK::Runtime#reset~
+This function will reset Kernel.tick_count to 0 and will remove all data from args.state.
+S
+ end
+
+ def docs_calcstringbox
+ <<-S
+* DOCS: ~GTK::Runtime#calcstringbox~
+This function returns the width and height of a string.
+
+#+begin_src ruby
+ def tick args
+ args.state.string_size ||= args.gtk.calcstringbox "Hello World"
+ args.state.string_size_font_size ||= args.gtk.calcstringbox "Hello World"
+ end
+#+end_src
+S
+ end
+end
+
+class GTK::Runtime
+ extend Docs
+ extend RuntimeDocs
+end