class FlappyDragon attr_accessor :grid, :inputs, :state, :outputs def tick defaults render calc process_inputs end def defaults state.flap_power = 11 state.gravity = 0.9 state.ceiling = 600 state.ceiling_flap_power = 6 state.wall_countdown_length = 100 state.wall_gap_size = 100 state.wall_countdown ||= 0 state.hi_score ||= 0 state.score ||= 0 state.walls ||= [] state.x ||= 50 state.y ||= 500 state.dy ||= 0 state.scene ||= :menu state.scene_at ||= 0 state.difficulty ||= :normal state.new_difficulty ||= :normal state.countdown ||= 4.seconds state.flash_at ||= 0 end def render outputs.sounds << "sounds/flappy-song.ogg" if state.tick_count == 1 render_score render_menu render_game end def render_score outputs.primitives << [10, 710, "HI SCORE: #{state.hi_score}", large_white_typeset].label outputs.primitives << [10, 680, "SCORE: #{state.score}", large_white_typeset].label outputs.primitives << [10, 650, "DIFFICULTY: #{state.difficulty.upcase}", large_white_typeset].label end def render_menu return unless state.scene == :menu render_overlay outputs.labels << [640, 700, "Flappy Dragon", 50, 1, 255, 255, 255] outputs.labels << [640, 500, "Instructions: Press Spacebar to flap. Don't die.", 4, 1, 255, 255, 255] outputs.labels << [430, 430, "[Tab] Change difficulty", 4, 0, 255, 255, 255] outputs.labels << [430, 400, "[Enter] Start at New Difficulty ", 4, 0, 255, 255, 255] outputs.labels << [430, 370, "[Escape] Cancel/Resume ", 4, 0, 255, 255, 255] outputs.labels << [640, 300, "(mouse, touch, and game controllers work, too!) ", 4, 1, 255, 255, 255] outputs.labels << [640, 200, "Difficulty: #{state.new_difficulty.capitalize}", 4, 1, 255, 255, 255] outputs.labels << [10, 100, "Code: @amirrajan", 255, 255, 255] outputs.labels << [10, 80, "Art: @mobypixel", 255, 255, 255] outputs.labels << [10, 60, "Music: @mobypixel", 255, 255, 255] outputs.labels << [10, 40, "Engine: DragonRuby GTK", 255, 255, 255] end def render_overlay outputs.primitives << [grid.rect.scale_rect(1.1, 0, 0), 0, 0, 0, 230].solid end def render_game render_game_over render_background render_walls render_dragon render_flash end def render_game_over return unless state.scene == :game outputs.labels << [638, 358, score_text, 20, 1] outputs.labels << [635, 360, score_text, 20, 1, 255, 255, 255] outputs.labels << [638, 428, countdown_text, 20, 1] outputs.labels << [635, 430, countdown_text, 20, 1, 255, 255, 255] end def render_background outputs.sprites << [0, 0, 1280, 720, 'sprites/background.png'] scroll_point_at = state.tick_count scroll_point_at = state.scene_at if state.scene == :menu scroll_point_at = state.death_at if state.countdown > 0 scroll_point_at ||= 0 outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_back.png', 0.25) outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_middle.png', 0.50) outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_front.png', 1.00, -80) end def render_walls state.walls.each do |w| w.sprites = [ [w.x, w.bottom_height - 720, 100, 720, 'sprites/wall.png', 180], [w.x, w.top_y, 100, 720, 'sprites/wallbottom.png', 0] ] end outputs.sprites << state.walls.map(&:sprites) end def render_dragon state.show_death = true if state.countdown == 3.seconds render_debug_hitbox false if state.show_death == false || !state.death_at animation_index = state.flapped_at.frame_index 6, 2, false if state.flapped_at sprite_name = "sprites/dragon_fly#{animation_index.or(0) + 1}.png" state.dragon_sprite = [state.x, state.y, 100, 80, sprite_name, state.dy * 1.2] else sprite_name = "sprites/dragon_die.png" state.dragon_sprite = [state.x, state.y, 100, 80, sprite_name, state.dy * 1.2] sprite_changed_elapsed = state.death_at.elapsed_time - 1.seconds state.dragon_sprite.angle += (sprite_changed_elapsed ** 1.3) * state.death_fall_direction * -1 state.dragon_sprite.x += (sprite_changed_elapsed ** 1.2) * state.death_fall_direction state.dragon_sprite.y += (sprite_changed_elapsed * 14 - sprite_changed_elapsed ** 1.6) end outputs.sprites << state.dragon_sprite end def render_debug_hitbox show return unless show outputs.borders << [dragon_collision_box.rect, 255, 0, 0] if state.dragon_sprite outputs.borders << state.walls.flat_map do |w| w.sprites.map { |s| [s.rect, 255, 0, 0] } end end def render_flash return unless state.flash_at outputs.primitives << [grid.rect, white, 255 * state.flash_at.ease(20, :flip)].solid state.flash_at = 0 if state.flash_at.elapsed_time > 20 end def calc return unless state.scene == :game reset_game if state.countdown == 1 state.countdown -= 1 and return if state.countdown > 0 calc_walls calc_flap calc_game_over end def calc_walls state.walls.each { |w| w.x -= 8 } walls_count_before_removal = state.walls.length state.walls.reject! { |w| w.x < -100 } state.score += 1 if state.walls.count < walls_count_before_removal state.wall_countdown -= 1 and return if state.wall_countdown > 0 state.walls << state.new_entity(:wall) do |w| w.x = grid.right w.opening = grid.top .randomize(:ratio) .greater(200) .lesser(520) w.bottom_height = w.opening - state.wall_gap_size w.top_y = w.opening + state.wall_gap_size end state.wall_countdown = state.wall_countdown_length end def calc_flap state.y += state.dy state.dy = state.dy.lesser state.flap_power state.dy -= state.gravity return if state.y < state.ceiling state.y = state.ceiling state.dy = state.dy.lesser state.ceiling_flap_power end def calc_game_over return unless game_over? state.death_at = state.tick_count state.death_from = state.walls.first state.death_fall_direction = -1 state.death_fall_direction = 1 if state.x > state.death_from.x outputs.sounds << "sounds/hit-sound.wav" begin_countdown end def process_inputs process_inputs_menu process_inputs_game end def process_inputs_menu return unless state.scene == :menu changediff = inputs.keyboard.key_down.tab || inputs.controller_one.key_down.select if inputs.mouse.click p = inputs.mouse.click.point if (p.y >= 165) && (p.y < 200) && (p.x >= 500) && (p.x < 800) changediff = true end end if changediff case state.new_difficulty when :easy state.new_difficulty = :normal when :normal state.new_difficulty = :hard when :hard state.new_difficulty = :flappy when :flappy state.new_difficulty = :easy end end if inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start || inputs.controller_one.key_down.a state.difficulty = state.new_difficulty change_to_scene :game reset_game false state.hi_score = 0 begin_countdown end if inputs.keyboard.key_down.escape || (inputs.mouse.click && !changediff) || inputs.controller_one.key_down.b state.new_difficulty = state.difficulty change_to_scene :game end end def process_inputs_game return unless state.scene == :game clicked_menu = false if inputs.mouse.click p = inputs.mouse.click.point clicked_menu = (p.y >= 620) && (p.x < 275) end if clicked_menu || inputs.keyboard.key_down.escape || inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start change_to_scene :menu elsif (inputs.mouse.down || inputs.mouse.click || inputs.keyboard.key_down.space || inputs.controller_one.key_down.a) && state.countdown == 0 state.dy = 0 state.dy += state.flap_power state.flapped_at = state.tick_count outputs.sounds << "sounds/fly-sound.wav" end end def scrolling_background at, path, rate, y = 0 [ [ 0 - at.*(rate) % 1440, y, 1440, 720, path], [1440 - at.*(rate) % 1440, y, 1440, 720, path] ] end def white [255, 255, 255] end def large_white_typeset [5, 0, 255, 255, 255] end def at_beginning? state.walls.count == 0 end def dragon_collision_box state.dragon_sprite .scale_rect(1.0 - collision_forgiveness, 0.5, 0.5) .rect_shift_right(10) .rect_shift_up(state.dy * 2) end def game_over? return true if state.y <= 0.-(500 * collision_forgiveness) && !at_beginning? state.walls .flat_map { |w| w.sprites } .any? do |s| s.intersect_rect?(dragon_collision_box) end end def collision_forgiveness case state.difficulty when :easy 0.9 when :normal 0.7 when :hard 0.5 when :flappy 0.3 else 0.9 end end def countdown_text state.countdown ||= -1 return "" if state.countdown == 0 return "GO!" if state.countdown.idiv(60) == 0 return "GAME OVER" if state.death_at return "READY?" end def begin_countdown state.countdown = 4.seconds end def score_text return "" unless state.countdown > 1.seconds return "" unless state.death_at return "SCORE: 0 (LOL)" if state.score == 0 return "HI SCORE: #{state.score}" if state.score == state.hi_score return "SCORE: #{state.score}" end def reset_game set_flash = true state.flash_at = state.tick_count if set_flash state.walls = [] state.y = 500 state.dy = 0 state.hi_score = state.hi_score.greater(state.score) state.score = 0 state.wall_countdown = state.wall_countdown_length.fdiv(2) state.show_death = false state.death_at = nil end def change_to_scene scene state.scene = scene state.scene_at = state.tick_count inputs.keyboard.clear inputs.controller_one.clear end end $flappy_dragon = FlappyDragon.new def tick args $flappy_dragon.grid = args.grid $flappy_dragon.inputs = args.inputs $flappy_dragon.state = args.state $flappy_dragon.outputs = args.outputs $flappy_dragon.tick end