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 << { x: 10, y: 710, text: "HI SCORE: #{state.hi_score}", **large_white_typeset }
outputs.primitives << { x: 10, y: 680, text: "SCORE: #{state.score}", **large_white_typeset }
outputs.primitives << { x: 10, y: 650, text: "DIFFICULTY: #{state.difficulty.upcase}", **large_white_typeset }
end
def render_menu
return unless state.scene == :menu
render_overlay
outputs.labels << { x: 640, y: 700, text: "Flappy Dragon", size_enum: 50, alignment_enum: 1, **white }
outputs.labels << { x: 640, y: 500, text: "Instructions: Press Spacebar to flap. Don't die.", size_enum: 4, alignment_enum: 1, **white }
outputs.labels << { x: 430, y: 430, text: "[Tab] Change difficulty", size_enum: 4, alignment_enum: 0, **white }
outputs.labels << { x: 430, y: 400, text: "[Enter] Start at New Difficulty ", size_enum: 4, alignment_enum: 0, **white }
outputs.labels << { x: 430, y: 370, text: "[Escape] Cancel/Resume ", size_enum: 4, alignment_enum: 0, **white }
outputs.labels << { x: 640, y: 300, text: "(mouse, touch, and game controllers work, too!) ", size_enum: 4, alignment_enum: 1, **white }
outputs.labels << { x: 640, y: 200, text: "Difficulty: #{state.new_difficulty.capitalize}", size_enum: 4, alignment_enum: 1, **white }
outputs.labels << { x: 10, y: 100, text: "Code: @amirrajan", **white }
outputs.labels << { x: 10, y: 80, text: "Art: @mobypixel", **white }
outputs.labels << { x: 10, y: 60, text: "Music: @mobypixel", **white }
outputs.labels << { x: 10, y: 40, text: "Engine: DragonRuby GTK", **white }
end
def render_overlay
overlay_rect = grid.rect.scale_rect(1.1, 0, 0)
outputs.primitives << { x: overlay_rect.x,
y: overlay_rect.y,
w: overlay_rect.w,
h: overlay_rect.h,
r: 0, g: 0, b: 0, a: 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 << { x: 638, y: 358, text: score_text, size_enum: 20, alignment_enum: 1 }
outputs.labels << { x: 635, y: 360, text: score_text, size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
outputs.labels << { x: 638, y: 428, text: countdown_text, size_enum: 20, alignment_enum: 1 }
outputs.labels << { x: 635, y: 430, text: countdown_text, size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
end
def render_background
outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: '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 scrolling_background at, path, rate, y = 0
[
{ x: 0 - at.*(rate) % 1440, y: y, w: 1440, h: 720, path: path },
{ x: 1440 - at.*(rate) % 1440, y: y, w: 1440, h: 720, path: path }
]
end
def render_walls
state.walls.each do |w|
w.sprites = [
{ x: w.x, y: w.bottom_height - 720, w: 100, h: 720, path: 'sprites/wall.png', angle: 180 },
{ x: w.x, y: w.top_y, w: 100, h: 720, path: 'sprites/wallbottom.png', angle: 0 }
]
end
outputs.sprites << state.walls.map(&:sprites)
end
def render_dragon
state.show_death = true if state.countdown == 3.seconds
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 = { x: state.x, y: state.y, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
else
sprite_name = "sprites/dragon_die.png"
state.dragon_sprite = { x: state.x, y: state.y, w: 100, h: 80, path: sprite_name, angle: 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_flash
return unless state.flash_at
outputs.primitives << { **grid.rect.to_hash,
**white,
a: 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 white
{ r: 255, g: 255, b: 255 }
end
def large_white_typeset
{ size_enum: 5, alignment_enum: 0, r: 255, g: 255, b: 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 && 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