Created
June 17, 2026 14:27
-
-
Save amirrajan/34ce92fdaa487c230fa97cda9244e4a6 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - freeze frame
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright 2025 Scratchwork Development LLC. All right reserved. | |
| require 'app/command.rb' | |
| require 'app/command_button.rb' | |
| require 'app/location_button.rb' | |
| require 'app/player_slash.rb' | |
| require 'app/player_stab.rb' | |
| require 'app/player_air_slash.rb' | |
| require 'app/player_lightning.rb' | |
| require 'app/player_move.rb' | |
| require 'app/player_sky_fall.rb' | |
| require 'app/player.rb' | |
| require 'app/enemy.rb' | |
| require 'app/goblin.rb' | |
| require 'app/caster.rb' | |
| class Game | |
| attr_gtk | |
| attr :mode, :player, :enemies, :next_mode, :locations, :clock_debounce, :col_major_locations, | |
| :times_completed | |
| def initialize args | |
| @args = args | |
| @fps_metrics = false | |
| new_game | |
| end | |
| def new_game is_retry: false | |
| @click_fx = [] | |
| @target_camera_zoom = 1.1 | |
| @camera_zoom = 1.0 | |
| @zoom_speed = 0.1 | |
| @times_completed ||= 0 | |
| @instructions_at = Kernel.tick_count | |
| @instructions_mode = :defend | |
| @message_at = nil | |
| @message = nil | |
| @sub_message = nil | |
| @camera_shake ||= { | |
| trauma: 0, | |
| angle: 0, | |
| x_offset: 0, | |
| y_offset: 0 | |
| } | |
| @clock_f = 0 | |
| @clock_speed = 1 | |
| @clock_debounce = 0 | |
| @locations = {} | |
| 18.times do |col| | |
| 10.times do |row| | |
| center = Geometry.rect_props(x: 64 * col + 96, | |
| y: 64 * row + 64, | |
| w: 64, | |
| h: 64) | |
| @locations[row] ||= {} | |
| @locations[row][col] = { | |
| row: row, | |
| col: col, | |
| rect: { | |
| x: center.x - 32, | |
| y: center.y - 32, | |
| w: 64, | |
| h: 64, | |
| center: { **center }, | |
| }, | |
| x: center.x, | |
| y: center.y | |
| } | |
| end | |
| end | |
| @col_major_locations = {} | |
| 18.times do |col| | |
| 10.times do |row| | |
| center = Geometry.rect_props(x: 64 * col + 96, | |
| y: 64 * row + 64, | |
| w: 64, | |
| h: 64) | |
| @col_major_locations[col] ||= {} | |
| @col_major_locations[col][row] = { | |
| row: row, | |
| col: col, | |
| rect: { | |
| x: center.x - 32, | |
| y: center.y - 32, | |
| w: 64, | |
| h: 64, | |
| center: { **center }, | |
| }, | |
| x: center.x, | |
| y: center.y | |
| } | |
| end | |
| end | |
| if @times_completed == 0 | |
| first_stage! | |
| elsif @times_completed == 1 | |
| second_stage! | |
| elsif is_retry | |
| # puts "RETRYING STAGE" | |
| retry_stage! | |
| else | |
| # puts "RANDOM STAGE" | |
| random_stage! | |
| end | |
| end | |
| def tick | |
| if inputs.keyboard.key_down.forward_slash | |
| @fps_metrics = !@fps_metrics | |
| end | |
| args.audio.volume = 1 | |
| outputs.audio[:bg] ||= { | |
| input: "sounds/bg-music.ogg", | |
| looping: true | |
| } | |
| outputs.audio[:bg2] ||= { | |
| input: "sounds/bg-fx.ogg", | |
| looping: true, | |
| gain: 0.3 | |
| } | |
| # if inputs.keyboard.key_down.s | |
| # camera_shake_trauma! 0.2 | |
| # end | |
| calc_current_turn | |
| if @player.death_at && @player.death_at + 1 == clock && !@clock_debounce | |
| if @player.death_reason == :need_to_block | |
| notify! "You need to block or parry attacks!" | |
| elsif @player.death_reason == :block_break | |
| notify! "Attacked after guard broken!", "Parry attacks to conserve block!" | |
| elsif @player.death_reason == :cant_parry | |
| notify! "Fireballs can't be parried!" | |
| elsif @player.death_reason == :parry_early | |
| notify! "Parry was early!" | |
| elsif @player.death_reason == :parry_late | |
| notify! "Parry was late!" | |
| elsif @player.death_reason == :block_late | |
| notify! "Block was late!" | |
| elsif @player.death_reason == :attacked_from_behind | |
| notify! "Face the enemy to defend!" | |
| elsif @player.death_reason == :opportunity_of_attack | |
| notify! "Opportunity of attack!", "Enemy was next to you when you moved!" | |
| else | |
| puts "SCENARIO NOT HANDLED!" | |
| puts "#{@player.death_reason}" | |
| notify! "Game Over!" | |
| end | |
| camera_shake_trauma! 0.8 | |
| @clock_debounce = 30 | |
| end | |
| if @enemies.find { |e| e.death_at == clock } && !@clock_debounce | |
| camera_shake_trauma! 0.8 | |
| @clock_debounce = 45 | |
| end | |
| if @enemies.find { |e| e.damage_at == clock } | |
| camera_shake_trauma! 0.2 | |
| end | |
| if @player.action == :block_break && @player.frame.frame_index == 1 && @player.frame.frame_elapsed_time == 0 && !@clock_debounce | |
| @clock_debounce = 30 | |
| notify! "Guard Broken!" | |
| end | |
| if @player.parry_started_at == clock && !@clock_debounce | |
| camera_shake_trauma! 0.2 | |
| end | |
| if @player.block_impact_at == clock | |
| camera_shake_trauma! 0.2 | |
| end | |
| if @clock_debounce && @clock_debounce == 0 | |
| @clock_debounce = nil | |
| end | |
| if @player.dead? && @player.death_at.elapsed_time(clock) < 30 | |
| @clock_speed = 0.5 | |
| else | |
| @clock_speed = 1.0 | |
| end | |
| @turn_queue.each do |t| | |
| t.tick | |
| end | |
| outputs.background_color = [0, 0, 0, 0] | |
| if inputs.mouse.key_down.left | |
| @click_fx << { | |
| x: inputs.mouse.x, | |
| y: inputs.mouse.y, | |
| } | |
| end | |
| calc_camera_shake | |
| calc_whisps | |
| # disable_juice! | |
| render_lights | |
| render_lighted_scene | |
| render_instructions | |
| render_message | |
| render_fps_metrics | |
| @click_fx.each do |c| | |
| c.a ||= 255 | |
| c.w ||= 1 | |
| c.h ||= 1 | |
| c.d_sz ||= 30 | |
| c.d_a ||= 5 | |
| c.w += c.d_sz | |
| c.h += c.d_sz | |
| c.a -= c.d_a | |
| c.d_sz *= 0.7 | |
| c.d_a *= 1.1 | |
| end | |
| @click_fx.reject! { |c| c.a < 0 } | |
| outputs.primitives << @click_fx.map do |c| | |
| { **c, anchor_x: 0.5, anchor_y: 0.5, path: "sprites/white-ring.png" } | |
| end | |
| # @turn_queue.each_with_index do |t, i| | |
| # outputs.watch "#{t.id} dead? #{t.action} #{t.dead?} active? #{t.active?} #{i == @turn_queue_index ? '<--' : ''}" | |
| # end | |
| # outputs.watch "#{@player.menu}" | |
| # outputs.watch_fps | |
| if @clock_debounce && @clock_debounce > 0 | |
| @clock_debounce -= 1 | |
| end | |
| if (@clock_debounce || 0) <= 0 | |
| @clock_f += @clock_speed if !@paused | |
| end | |
| if @player.death_at && @player.death_at.elapsed_time(clock) > 180 | |
| new_game is_retry: true | |
| end | |
| end | |
| def notify! message, sub_message = nil, force: false | |
| return if @message == message && !force | |
| @message = message | |
| @sub_message = sub_message | |
| @message_at = Kernel.tick_count | |
| end | |
| def clear_message! | |
| @message_at = nil | |
| @message = nil | |
| @sub_message = nil | |
| end | |
| def render_message | |
| return if !@message_at | |
| return if !@message | |
| if @message_at.elapsed_time(Kernel.tick_count) > 300 | |
| @message_at = nil | |
| @message = nil | |
| @sub_message = nil | |
| return | |
| end | |
| perc = Easing.smooth_stop(start_at: @message_at, | |
| duration: 15, | |
| tick_count: Kernel.tick_count, | |
| power: 2) | |
| outputs[:message].w = 640 | |
| outputs[:message].h = 128 | |
| outputs[:message].background_color = [0, 0, 0, 0] | |
| outputs[:message].primitives << { | |
| x: 0, y: 0, w: 640, h: 128, path: :solid, r: 0, g: 0, b: 0, a: 200 | |
| } | |
| if @sub_message | |
| outputs[:message].primitives << { | |
| x: 320, y: 64, text: @message, size_px: 32, r: 255, g: 255, b: 255, a: 255, | |
| anchor_x: 0.5, anchor_y: 0.0 | |
| } | |
| outputs[:message].primitives << { | |
| x: 320, y: 64, text: @sub_message, size_px: 32, r: 255, g: 255, b: 255, a: 255, | |
| anchor_x: 0.5, anchor_y: 1.0 | |
| } | |
| else | |
| outputs[:message].primitives << { | |
| x: 320, y: 64, text: @message, size_px: 32, r: 255, g: 255, b: 255, a: 255, | |
| anchor_x: 0.5, anchor_y: 0.5 | |
| } | |
| end | |
| outputs.primitives << { | |
| x: 640, y: 720 - 64, w: 640 * perc, h: 100 * perc, path: :message, | |
| anchor_x: 0.5, anchor_y: 0.5 | |
| } | |
| end | |
| def render_fps_metrics | |
| return if !@fps_metrics | |
| outputs.primitives << { | |
| x: 640, | |
| y: 720 - 16, | |
| text: "#{DR.current_framerate}", | |
| size_px: 32, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| r: 255, g: 255, b: 255 | |
| } | |
| end | |
| def render_instructions | |
| @instructions_mode ||= :defend | |
| @instructions_at ||= 0 | |
| perc = Easing.smooth_stop(start_at: @instructions_at, | |
| duration: 30, | |
| tick_count: Kernel.tick_count, | |
| power: 2, flip: true) | |
| outputs[:defend_instructions].w = 1280 | |
| outputs[:defend_instructions].h = 128 | |
| outputs[:defend_instructions].background_color = [0, 0, 0, 0] | |
| outputs[:defend_instructions].primitives << { | |
| x: 0, y: 0, w: 1280, h: 128, path: :solid, r: 0, g: 0, b: 0, a: 200 | |
| } | |
| text = if @player.active? | |
| "Players's Turn. GO!" | |
| else | |
| "Enemy's Turn. DEFEND!" | |
| end | |
| outputs[:defend_instructions].primitives << { | |
| x: 640, y: 128 / 2, text: text, size_px: 38, r: 255, g: 255, b: 255, a: 255, | |
| anchor_x: 0.5, anchor_y: 0.5 | |
| } | |
| outputs.primitives << { | |
| x: -1280 * perc, y: -8, w: 1280, h: 128, path: :defend_instructions | |
| } | |
| end | |
| def pause! | |
| @pause = true | |
| end | |
| def unpause! | |
| @pause = false | |
| end | |
| def toggle_pause! | |
| @paused = !@paused | |
| end | |
| def calc_current_turn | |
| current_entity = @turn_queue[@turn_queue_index] | |
| any_active = @turn_queue.any? { |t| t.active? } | |
| if !current_entity.active? && !any_active | |
| @turn_queue_index += 1 | |
| if @turn_queue_index >= @turn_queue.length | |
| @turn_queue_index = 0 | |
| end | |
| if !@player.dead? | |
| next_entity = @turn_queue[@turn_queue_index] | |
| next_entity.activate! | |
| if next_entity == @player && @instructions_mode == :defend | |
| @instructions_at = Kernel.tick_count | |
| @instructions_mode = :attack | |
| elsif current_entity == @player && @instructions_mode == :attack | |
| @instructions_at = Kernel.tick_count | |
| @instructions_mode = :defend | |
| end | |
| end | |
| elsif current_entity.hp == 0 | |
| @turn_queue_index += 1 | |
| if @turn_queue_index >= @turn_queue.length | |
| @turn_queue_index = 0 | |
| end | |
| if !@player.dead? | |
| next_entity = @turn_queue[@turn_queue_index] | |
| next_entity.activate! | |
| if next_entity == @player && @instructions_mode == :defend | |
| @instructions_at = Kernel.tick_count | |
| @instructions_mode = :attack | |
| elsif current_entity == @player && @instructions_mode == :attack | |
| @instructions_at = Kernel.tick_count | |
| @instructions_mode = :defend | |
| end | |
| end | |
| end | |
| end | |
| def clock | |
| @clock_f.to_i | |
| end | |
| def calc_camera_shake | |
| next_camera_angle = 180.0 / 20.0 * @camera_shake.trauma**2 | |
| next_offset = 100.0 * @camera_shake.trauma**2 | |
| # Ensure that the camera angle always switches from | |
| # positive to negative and vice versa | |
| # which gives the effect of shaking back and forth | |
| @camera_shake.angle = @camera_shake.angle > 0 ? | |
| next_camera_angle * -1 : | |
| next_camera_angle | |
| @camera_shake.angle = 0 | |
| @camera_shake.x_offset = next_offset.randomize(:sign, :ratio) | |
| @camera_shake.y_offset = next_offset.randomize(:sign, :ratio) | |
| # Gracefully degrade trauma | |
| @camera_shake.trauma *= 0.95 | |
| end | |
| def camera_shake_trauma! value | |
| @camera_shake.trauma += value | |
| end | |
| def camera_zoom_in! | |
| # @target_camera_zoom = 1.3 | |
| @zoom_speed = 0.9 | |
| end | |
| def camera_zoom_out! | |
| # @target_camera_zoom = 0.95 | |
| @zoom_speed = 0.05 | |
| end | |
| def camera_zoom_neutral! | |
| @target_camera_zoom = 1.0 | |
| @zoom_speed = 0.1 | |
| end | |
| def disable_juice! | |
| @camera_zoom = 1.0 | |
| @target_camera_zoom = 1.0 | |
| @camera_shake = { | |
| trauma: 0, | |
| angle: 0, | |
| x_offset: 0, | |
| y_offset: 0 | |
| } | |
| @clock_debounce = nil | |
| end | |
| def render_lighted_scene | |
| outputs[:scene].primitives << { x: 0, | |
| y: 0, | |
| w: 1280, | |
| h: 720, | |
| path: "sprites/bg.png", | |
| anchor_x: 0, | |
| anchor_y: 0 } | |
| outputs[:scene].background_color = [0, 0, 0, 0] | |
| outputs[:scene].w = Grid.w | |
| outputs[:scene].h = Grid.h | |
| outputs[:scene].primitives << @turn_queue.sort_by { |e| e.active? ? 1 : 0 }.map { |e| e.primitives } | |
| outputs[:controls].background_color = [0, 0, 0, 0] | |
| outputs[:controls].primitives << @player.input_primitivess | |
| outputs[:lighted_scene].background_color = [0, 0, 0, 0] | |
| outputs[:lighted_scene].primitives << { x: 0, y: 0, w: 1280, h: 720, path: :lights, blendmode_enum: 0 } | |
| outputs[:lighted_scene].primitives << { x: 0, y: 0, w: 1280, h: 720, path: :scene, blendmode_enum: 2 } | |
| @camera_zoom = @camera_zoom.lerp(@target_camera_zoom, @zoom_speed) | |
| anchor_x = (@player.x + 32) / Grid.w | |
| anchor_y = (@player.y + 64) / Grid.h | |
| outputs.primitives << { x: (@player.x + 32) - @camera_shake.x_offset, | |
| y: (@player.y + 64) - @camera_shake.y_offset, | |
| angle: @camera_shake.angle, | |
| w: Grid.w * @camera_zoom, | |
| h: Grid.h * @camera_zoom, | |
| anchor_x: anchor_x, | |
| anchor_y: anchor_y, | |
| path: :lighted_scene, blendmode_enum: 1 } | |
| outputs.primitives << { x: 0, y: 0, w: Grid.w, h: Grid.h, path: :controls } | |
| # outputs.primitives << @locations.map do |row, cols| | |
| # cols.map do |col, loc| | |
| # [ | |
| # { **Geometry.zoom_rect(rect: loc.rect, px: -1), path: :solid, r: 0, g: 255, b: 0, a: 15 }, | |
| # { **Geometry.rect(loc.rect).center, text: "r:#{row} c:#{col}", anchor_x: 0.5, anchor_y: 0.5, size_px: 16, r: 255, g: 255, b: 255 } | |
| # ] | |
| # end | |
| # end | |
| # outputs.primitives << { x: 384 + 32, y: 216 + 64, w: 10, h: 10, anchor_x: 0.5, anchor_y: 0.5, path: :solid, r: 255, g: 255, b: 255 } | |
| # outputs.primitives << { x: 0, y: 0, w: 1280, h: 720, path: :lights } | |
| end | |
| def render_lights | |
| outputs[:lights].background_color = [0, 0, 0, 0] | |
| outputs[:lights].primitives << @turn_queue.map do |e| | |
| arrows = if e.is_a? Goblin | |
| e.arrows.map do |a| | |
| [ | |
| { | |
| x: a.x, | |
| y: a.y, | |
| w: 256, | |
| h: 256, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| path: "sprites/mask.png", | |
| r: 255, g: 0, b: 0 | |
| } | |
| ] | |
| end | |
| elsif e.is_a? Caster | |
| if e.fireball | |
| { | |
| x: e.fireball.x, | |
| y: e.fireball.y, | |
| w: 256, | |
| h: 256, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| path: "sprites/mask.png", | |
| r: 0, g: 0, b: 0 | |
| } | |
| end | |
| elsif e.is_a?(Player) && e.lightning | |
| { | |
| x: e.lightning.x, | |
| y: e.lightning.y, | |
| w: 256, | |
| h: 256, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| path: "sprites/mask.png", | |
| r: 255, g: 0, b: 0 | |
| } | |
| end | |
| [ | |
| { | |
| x: e.x, | |
| y: e.y, | |
| w: 256, | |
| h: 256, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| path: "sprites/mask2.png", | |
| a: 196 | |
| }, | |
| arrows | |
| ] | |
| end | |
| outputs[:lights].primitives << state.whisps.map do |w| | |
| w.merge(x: w.x, | |
| y: w.y, | |
| w: 640, | |
| h: 640, | |
| r: 0, | |
| g: 0, | |
| b: 0, | |
| path: "sprites/mask.png", | |
| a: w.a) | |
| end | |
| end | |
| def calc_whisps | |
| # 20 lighting points are created and then moved in a parallax fashion | |
| state.whisps ||= 40.map do | |
| d = rand + 1 | |
| w = { | |
| a: 255, | |
| x: 1280 * rand, | |
| y: 720 * rand, | |
| w: 0, | |
| h: 0, | |
| dx: d, | |
| dy: d, | |
| path: "sprites/mask2.png", | |
| a: 0, | |
| r: 0, g: 255, b: 255 | |
| } | |
| w.target_x = w.x | |
| w.target_y = w.y | |
| w | |
| end | |
| # hand wavey math for parallax lighting | |
| state.whisps.each do |w| | |
| w.target_x = w.target_x - w.dx | |
| w.target_y = w.target_y - w.dy | |
| perc = 0.1 | |
| w.x = w.x * (1 - perc) + w.target_x * perc | |
| w.y = w.y * (1 - perc) + w.target_y * perc | |
| w.a += 10 | |
| w.a = w.a.clamp(0, 255) | |
| w.w += 50 | |
| w.h += 50 | |
| w.w = w.w.clamp(0, 640) | |
| w.h = w.h.clamp(0, 360) | |
| if w.x + w.w < 0 && w.y + w.h < 0 | |
| w.target_x = 1280 * rand | |
| w.target_y = 720 * rand | |
| w.w = 0 | |
| w.h = 0 | |
| w.x = w.target_x | |
| w.y = w.target_y | |
| w.a = 0 | |
| end | |
| end | |
| end | |
| def first_stage! | |
| player_data = { row: 3, col: 5 } | |
| enemy_data = [ | |
| { row: 3, col: 17, is_ranged: true, facing: -1 }, | |
| { row: 3, col: 13, is_ranged: false, facing: 1 }, | |
| { row: 3, col: 14, is_ranged: false, facing: 1 }, | |
| { row: 6, col: 0, is_ranged: true, facing: 1 }, | |
| ] | |
| init_turn_queue!(player_data, enemy_data) | |
| end | |
| def second_stage! | |
| player_data = { row: 3, col: 5 } | |
| enemy_data = [ | |
| { row: 3, col: 17, is_caster: true, facing: -1 }, | |
| { row: 3, col: 13, is_ranged: false, facing: 1 }, | |
| { row: 3, col: 14, is_ranged: false, facing: 1 }, | |
| { row: 6, col: 0, is_ranged: true, facing: 1 }, | |
| ] | |
| init_turn_queue!(player_data, enemy_data) | |
| end | |
| def retry_stage! | |
| # puts "RETRYING WITH STORED DATA" | |
| # puts "@retry_player_data: #{@retry_player_data}" | |
| # puts "@retry_enemy_data: #{@retry_enemy_data}" | |
| init_turn_queue!(@retry_player_data, @retry_enemy_data) | |
| end | |
| def init_turn_queue!(player_data, enemy_data) | |
| @retry_player_data = { row: player_data.row, col: player_data.col } | |
| @retry_enemy_data = [*enemy_data] | |
| @player = Player.new(self, location: @locations[player_data.row][player_data.col]) | |
| @enemies = [] | |
| enemy_data.each_with_index do |ed, i| | |
| if ed.is_caster | |
| @enemies << Caster.new(self, id: i, location: @locations[ed.row][ed.col], is_ranged: ed.is_ranged) | |
| else | |
| @enemies << Goblin.new(self, id: i, location: @locations[ed.row][ed.col], is_ranged: ed.is_ranged) | |
| end | |
| end | |
| @turn_queue = [ | |
| *@enemies, | |
| @player, | |
| ] | |
| @enemies.each { |e| e.face_player! } | |
| @turn_queue.first.activate! | |
| @turn_queue_index = 0 | |
| # puts "RETRY PLAYER DATA: #{@retry_player_data}" | |
| # puts "RETRY ENEMY DATA: #{@retry_enemy_data}" | |
| end | |
| def ranged_enemy_positions | |
| [ | |
| { row: 7, col: 9 }, | |
| { row: 7, col: 10 }, | |
| { row: 7, col: 16 }, | |
| { row: 3, col: 16 }, | |
| { row: 6, col: 0 }, | |
| ] | |
| end | |
| def ground_enemy_positions(player_data) | |
| [ | |
| { row: 3, col: 13 }, | |
| { row: 3, col: player_data.col + 1 }, | |
| { row: 3, col: 0 }, | |
| { row: 3, col: player_data.col - 1 }, | |
| ] | |
| end | |
| def random_stage! | |
| player_data = { row: 3, col: 5 + Numeric.rand(-1..-1) } | |
| enemy_data = [] | |
| ranged_enemies = ranged_enemy_positions.shuffle | |
| ground_enemies = ground_enemy_positions(player_data).shuffle | |
| (2 + (@times_completed - 2)).clamp(2, 4).times do |i| | |
| loc = ranged_enemies.pop_front | |
| if rand > 0.66 | |
| loc.is_ranged = true | |
| else | |
| loc.is_caster = true | |
| end | |
| if loc | |
| enemy_data << loc | |
| end | |
| end | |
| (2 + (@times_completed - 3)).clamp(2, 4).times do |i| | |
| loc = ground_enemies.pop_front | |
| if loc | |
| enemy_data << loc | |
| end | |
| end | |
| enemy_data.shuffle! | |
| init_turn_queue!(player_data, enemy_data) | |
| end | |
| end | |
| module Main | |
| def start | |
| @game = Game.new args | |
| end | |
| def tick | |
| outputs.background_color = [0, 0, 0] | |
| if inputs.controller_one.key_down.home | |
| @game.new_game | |
| end | |
| @game.args = args | |
| @game.tick | |
| end | |
| end | |
| # DR.reset | |
| # GTK.reset_and_replay "replay.txt", speed: 1 | |
| # GTK.reset |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment