diff options
| -rw-r--r-- | dragon/array_docs.rb | 300 | ||||
| -rw-r--r-- | dragon/attr_sprite.rb | 27 | ||||
| -rw-r--r-- | dragon/console.rb | 348 | ||||
| -rw-r--r-- | dragon/console_color.rb | 30 | ||||
| -rw-r--r-- | dragon/console_font_style.rb | 40 | ||||
| -rw-r--r-- | dragon/console_prompt.rb | 157 | ||||
| -rw-r--r-- | dragon/directional_input_helper_methods.rb | 12 | ||||
| -rw-r--r-- | dragon/docs.rb | 537 | ||||
| -rw-r--r-- | dragon/geometry.rb | 31 | ||||
| -rw-r--r-- | dragon/kernel_docs.rb | 71 | ||||
| -rw-r--r-- | dragon/readme_docs.rb | 1170 | ||||
| -rw-r--r-- | dragon/runtime_docs.rb | 39 |
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 |
