Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Last active July 22, 2025 23:31
Show Gist options
  • Save amirrajan/4abbd7941c4d54865d7b96d9398f1c40 to your computer and use it in GitHub Desktop.
Save amirrajan/4abbd7941c4d54865d7b96d9398f1c40 to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - I Have A Jet Pack (https://amirrajan.itch.io/i-have-a-jetpack)
class Game
attr :args
def tick
defaults
current_scene = state.scene
calc
render
state.clock += 1
if current_scene != state.scene
raise "Scene changed during tick. Set state.next_scene."
end
if state.next_scene
state.scene = state.next_scene
state.scene_at = state.clock
state.next_scene = nil
end
end
def new_player
{
x: 360,
y: 0,
w: 16 * 2,
h: 16 * 2,
path: :solid,
dy: 0,
dx: 0,
angle: 90,
dangle: 0,
right_rocket_power: 0,
left_rocket_power: 0,
dead: false,
max_altitude: 0,
rotation_count: 0,
rotation_accumulator: 0,
}
end
def input_player
return if state.scene != :game
if player.dead && player.dead_at.elapsed_time(state.clock) > 30 && inputs_continue?
state.player = new_player
elsif !player.dead
if inputs_right_rocket?
player.right_rocket_at ||= state.clock
else
player.right_rocket_at = nil
player.right_rocket_power = 0
end
if inputs_left_rocket?
player.left_rocket_at ||= state.clock
else
player.left_rocket_at = nil
player.left_rocket_power = 0
end
end
end
def inputs_left_rocket?
inputs.keyboard.f || inputs.controller_one.l1 || touch_controls_left_pressed?
end
def inputs_right_rocket?
inputs.keyboard.j || inputs.controller_one.r1 || touch_controls_right_pressed?
end
def calc_player
input_player
calc_player_alive
calc_player_dead
end
def calc_player_alive
return if player.dead
if player.right_rocket_at
player.right_rocket_power += 0.05
player.right_rocket_power = player.right_rocket_power.clamp(0, 1)
player.dx += 0.1 * player.angle.vector_x
player.dangle += (0.5 * player.right_rocket_power)
right_exhaust_x = player.x + 4
right_exhaust_y = (player.y + player.h / 2) - 12
state.particle_queue << { x: right_exhaust_x,
y: right_exhaust_y,
anchor_x: 0.5,
anchor_y: 0.5,
w: 6,
h: 6,
r: 180,
g: 0,
b: 0,
a: 255,
path: :solid }
end
if player.left_rocket_at
player.left_rocket_power += 0.05
player.left_rocket_power = player.left_rocket_power.clamp(0, 1)
player.dx += 0.1 * player.angle.vector_x
player.dangle -= (0.5 * player.left_rocket_power)
right_exhaust_x = player.x - 4
right_exhaust_y = (player.y + player.h / 2) - 12
state.particle_queue << { x: right_exhaust_x,
y: right_exhaust_y,
anchor_x: 0.5,
anchor_y: 0.5,
w: 6,
h: 6,
r: 180,
g: 0,
b: 0,
a: 255,
path: :solid }
end
if player.right_rocket_at || player.left_rocket_at
player.dy += 0.16 * player.angle.vector_y
end
player.dangle = player.dangle.clamp(-4, 4)
player.dy = player.dy.clamp(-3, 3)
player.y += player.dy
player.dy -= 0.10
if player.y > 2
player.on_ground = false
player.on_ground_at = nil
end
if player.y <= 0 && player.dy < 0
if player.dy < -2.1
play_hit_sound
player.dead = true
player.dead_at ||= state.clock
player.death_reason ||= :speed
player.death_speed ||= player.dy
elsif !Geometry.angle_within_range?(player.angle, 90, 15)
play_hit_sound
player.dead = true
player.dead_at ||= state.clock
player.death_reason ||= :angle
player.death_angle ||= player.angle
end
if player.dy.abs > 0.34
player.dy *= -0.5
elsif player.dy < 0
player.dy = 0
player.on_ground = true
player.on_ground_at ||= state.clock
end
player.y = 0
end
player.x += player.dx
player.dx *= 0.9
player.dangle *= 0.9
if player.on_ground
player.angle = player.angle.lerp(90, 0.9).round
else
player.angle += player.dangle
player.rotation_accumulator += player.dangle
player.angle = player.angle % 360
end
calc_hazards
calc_stats
determine_mission_success
if player.dead
state.next_scene = :dead
end
end
def calc_player_dead
return if !player.dead
player.dy = player.dy.clamp(-3, 3)
player.y += player.dy
player.dy -= 0.10
if player.y > 2
player.on_ground = false
player.on_ground_at = nil
end
if player.y <= 0 && player.dy < 0
if player.dy.abs > 0.34
play_hit_sound
player.dy *= -0.5
player.dangle = 0
if player.angle > 0 && player.angle < 90
player.angle = player.angle.lerp(0, 0.9)
elsif player.angle > 90 && player.angle < 180
player.angle = player.angle.lerp(180, 0.9)
elsif player.angle > 180 && player.angle < 270
player.angle = player.angle.lerp(180, 0.9)
elsif player.angle > 270 && player.angle < 360
player.angle = player.angle.lerp(0, 0.9)
end
elsif player.dy < 0
player.dy = 0
player.on_ground = true
player.on_ground_at ||= state.clock
end
player.right_rocket_power = 0
player.left_rocket_power = 0
player.dx *= 0.9
player.y = 0
end
player.x += player.dx
player.dx *= 0.9
if player.on_ground
player.dangle *= 0.9
else
player.angle += player.dangle
player.rotation_accumulator += player.dangle
player.angle = player.angle % 360
end
player.left_rocket_at = nil
player.right_rocket_at = nil
if player.on_ground
if player.angle > 90
player.angle = player.angle.lerp(180, 0.1)
else
player.angle = player.angle.lerp(0, 0.1)
end
end
player.dx *= 0.99
player.x += player.dx
calc_hazards
end
def player_hurt_box
Geometry.rect_props(player).merge(w: 4, h: 4, anchor_x: 0.5, anchor_y: -1.5)
end
def calc_hazards
hazard = Geometry.find_intersect_rect(player_hurt_box, state.hazards)
if hazard
lowrez.primitives << hazard.merge(path: :solid, r: 255, g: 0, b: 0)
player.killed_by ||= hazard
player.dead = true
player.dead_at ||= state.clock
angle = Geometry.angle player, hazard
player.dx = angle.vector_x * -4
player.dy = angle.vector_y * -2
if angle.vector_x != 0
player.dangle = 10 * angle.vector_x.sign
end
play_hit_sound
end
end
def calc_stats
if player.max_altitude < player.y
player.max_altitude = player.y
end
if player.rotation_accumulator.abs > 360
player.rotation_count += 1
player.rotation_accumulator = player.rotation_accumulator % 360
end
end
def determine_mission_success
return if state.scene != :game
return if player.dead
results = current_mission.successful_if.call(args)
if current_mission.successful_if.call(args)
state.next_scene = :success
end
end
def calc
calc_edit_map
calc_game_scene
calc_mission_scene
calc_success_scene
calc_dead_scene
end
def calc_success_scene
return if state.scene != :success
if state.scene_at.elapsed_time(state.clock) > 30 && inputs_continue?
state.completed_missions << current_mission.id
state.current_mission_id = current_mission.next_mission
state.next_scene = :mission
end
end
def calc_dead_scene
return if state.scene != :dead
if state.scene_at.elapsed_time(state.clock) > 30 && inputs_continue?
state.player = new_player
state.next_scene = :game
end
end
def calc_game_scene
calc_player
calc_camera
calc_particles
end
def calc_mission_scene
return if state.scene != :mission
if state.scene_at.elapsed_time(state.clock) > 30 && inputs_continue?
state.next_scene = :game
end
end
def inputs_continue?
inputs.keyboard.key_down.enter ||
inputs.controller_one.key_down.start ||
inputs.controller_one.key_down.a ||
inputs.mouse.click
end
def calc_particles
state.particle_queue.each do |particle|
# particle.x += particle.dx
# particle.y += particle.dy
# particle.dx *= 0.9
# particle.dy *= 0.9
particle.a -= 10
particle.w -= 0.15
particle.h -= 0.15
end
state.particle_queue.reject! do |particle|
particle.a < 0
end
end
def calc_camera
state.camera.target_x = player.x + player.dx * 8
state.camera.target_y = player.y + player.dy * 8
state.camera.x += (state.camera.target_x - state.camera.x) * 0.3
state.camera.y += (state.camera.target_y - state.camera.y) * 0.3
end
def player_prefab
if player.dead
player.merge(x: state.player.x.to_i,
y: state.player.y.to_i - 4 - 8,
angle: (player.angle - 90).ifloor(10),
anchor_x: 0.5,
path: "sprites/player.png")
else
path = if player.right_rocket_at && player.left_rocket_at
"sprites/player-both-rocket.png"
elsif player.right_rocket_at
"sprites/player-right-rocket.png"
elsif player.left_rocket_at
"sprites/player-left-rocket.png"
else
"sprites/player.png"
end
path = "sprites/player.png" if state.clock.zmod? 4
player.merge(x: state.player.x.to_i,
y: state.player.y.to_i - 8,
angle: (player.angle - 90).ifloor(10),
anchor_x: 0.5,
path: path)
end
end
def mouse_pos
{
x: (args.inputs.mouse.x - (Grid.w - rt_size * rt_scale) / 2).idiv(rt_scale),
y: args.inputs.mouse.y.idiv(rt_scale)
}
end
def map_size
720
end
def rt_size
64
end
def mouse_click?
inputs.mouse.click
end
def defaults
outputs.background_color = [0, 0, 0]
hud.background_color = [0, 0, 0, 0]
hud.w = rt_size
hud.h = rt_size
lowrez.w = map_size
lowrez.h = map_size
lowrez.primitives << {
x: 0,
y: 0,
w: map_size,
h: map_size,
r: 255,
g: 255,
b: 255,
path: :solid
}
state.clock ||= 0
state.completed_missions ||= []
state.player ||= new_player
state.camera ||= {
x: 0,
y: 0,
target_x: 0,
target_y: 0,
}
state.scene ||= :mission
state.scene_at ||= state.clock
state.current_mission_id ||= missions.first.id
state.hazards ||= []
state.particle_queue ||= []
state.touch_controls ||= {
left: Layout.rect(row: 19, col: 0, w: 3, h: 3),
right: Layout.rect(row: 19, col: 9, w: 3, h: 3),
}
args.audio[:bg] ||= {
input: "sounds/birds-chirping.mp3"
}
args.audio[:rocket] ||= {
input: "sounds/rocket.mp3",
gain: 0.0,
looping: true
}
if player.right_rocket_at || player.left_rocket_at
args.audio[:rocket].gain += 0.01
else
args.audio[:rocket].gain -= 0.01
end
args.audio[:rocket].gain = args.audio[:rocket].gain.clamp(0, 0.05)
end
def play_hit_sound
args.audio[:hit] ||= {
input: "sounds/hit.ogg",
gain: 0.1
}
end
def current_mission
missions.find { |m| m.id == state.current_mission_id }
end
def missions
@missions ||= [
{
id: :above_trees,
name: "mission 1:",
description_1: "fly above trees",
description_2: "land safely",
successful_if: lambda do |args|
args.state.player.max_altitude >= 60 &&
args.state.player.on_ground &&
args.state.player.on_ground_at.elapsed_time(args.state.clock) > 30
end,
next_mission: :barrel_roll
},
{
id: :barrel_roll,
name: "mission 2:",
description_1: "do a barrel roll",
description_2: "land safely",
successful_if: lambda do |args|
args.state.player.rotation_count >= 1 &&
args.state.player.on_ground &&
args.state.player.on_ground_at.elapsed_time(args.state.clock) > 30
end,
next_mission: :reach_the_stars
},
{
id: :reach_the_stars,
name: "mission 3:",
description_1: "reach the stars",
description_2: "land safely",
successful_if: lambda do |args|
args.state.player.max_altitude >= 650 &&
args.state.player.on_ground &&
args.state.player.on_ground_at.elapsed_time(args.state.clock) > 30
end,
next_mission: :saws
},
{
id: :saws,
name: "mission 4:",
description_1: "reach the stars",
description_2: "avoid the saws",
successful_if: lambda do |args|
args.state.player.max_altitude >= 650 &&
args.state.player.on_ground &&
args.state.player.on_ground_at.elapsed_time(args.state.clock) > 30
end,
hazards_path: "data/saws.txt",
next_mission: :free_roam
},
{
id: :free_roam,
name: "mission 5:",
description_1: "free roam",
description_2: "enjoy",
successful_if: lambda do |args|
false
end,
next_mission: :free_roam
},
]
@missions.each do |m|
m.hazards_path ||= "data/map.txt"
end
end
def load_hazards
contents = GTK.read_file(current_mission.hazards_path)
if !contents
GTK.write_file(path, "")
end
contents = GTK.read_file(current_mission.hazards_path)
contents.split("\n").map do |line|
x, y, w, h = line.split(",").map(&:to_i)
{ x: x,
y: y,
w: w,
h: h,
path: "sprites/saw.png",
r: 255,
g: 255,
b: 255 }
end
end
def save_hazards
contents = state.hazards.map do |hazard|
"#{hazard[:x]},#{hazard[:y]},#{hazard[:w]},#{hazard[:h]}"
end.join("\n")
GTK.write_file(current_mission.hazards_path, contents)
end
def calc_edit_map
if inputs.keyboard.key_down.tab
if state.scene == :game
state.next_scene = :edit
else
state.next_scene = :game
end
end
return if state.scene != :edit
if inputs.mouse.click || inputs.mouse.held
x = inputs.mouse.x.ifloor(4) - 4
y = (inputs.mouse.y - ((1280 - map_size) / 2)).ifloor(4) - 4
hazard_to_add = { x: x,
y: y,
w: 16,
h: 16,
path: "sprites/saw.png",
r: 255,
g: 255,
b: 255 }
hazard = Geometry.find_intersect_rect(hazard_to_add, state.hazards)
if hazard && inputs.keyboard.x
state.hazards.delete(hazard)
save_hazards
elsif !hazard && !inputs.keyboard.x
state.hazards << hazard_to_add
save_hazards
end
end
end
def render
outputs.watch player.angle
lowrez.primitives << { x: 0, y: 0, w: 720, h: 720, path: "sprites/bg.png" }
lowrez.primitives << state.hazards.map do |w|
w.merge(angle: state.clock * 10)
end
lowrez.primitives << state.particle_queue
lowrez.primitives << player_prefab
render_game_scene
render_edit_scene
render_mission_scene
render_success_scene
render_dead_scene
render_controls
lowrez.primitives << player.killed_by
end
def render_dead_scene
return if state.scene != :dead
render_viewport
hud.primitives << { x: 0, y: 0, w: 64, h: 64, path: :solid, r: 0, g: 0, b: 0, a: 128 }
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.name,
anchor_x: 0.5,
anchor_y: -5.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.description_1,
anchor_x: 0.5,
anchor_y: -3.5,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.description_2,
anchor_x: 0.5,
anchor_y: -2.0,
r: 255,
g: 255,
b: 255)
hud.primitives << md_label(x: 33,
y: 33,
text: "wasted",
anchor_x: 0.5,
anchor_y: 0.50,
r: 255,
g: 0,
b: 0)
hud.primitives << sm_label(x: 33,
y: 33,
text: "#{input_name_for_enter_key}",
anchor_x: 0.5,
anchor_y: 3.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: "to try again",
anchor_x: 0.5,
anchor_y: 4.5,
r: 255,
g: 255,
b: 255)
end
def touch_controls_left_pressed?
inputs.finger_left && inputs.finger_left.intersect_rect?(state.touch_controls.left)
end
def touch_controls_right_pressed?
inputs.finger_right && inputs.finger_right.intersect_rect?(state.touch_controls.right)
end
def touch_controls_left_prefab
if touch_controls_left_pressed?
state.touch_controls.left.merge(path: "sprites/button-pressed.png")
else
state.touch_controls.left.merge(path: "sprites/button-released.png")
end
end
def touch_controls_right_prefab
if touch_controls_right_pressed?
state.touch_controls.right.merge(path: "sprites/button-pressed.png")
else
state.touch_controls.right.merge(path: "sprites/button-released.png")
end
end
def controller_l1_prefab
state.touch_controls.left.merge(path: "sprites/controller-l1.png")
end
def controller_r1_prefab
state.touch_controls.right.merge(path: "sprites/controller-r1.png")
end
def keyboard_f_prefab
state.touch_controls.left.merge(path: "sprites/keyboard-f.png")
end
def keyboard_j_prefab
state.touch_controls.right.merge(path: "sprites/keyboard-j.png")
end
def render_controls
outputs.primitives << { x: 360,
y: 640,
text: "#{inputs_scheme_name} controls",
r: 255,
g: 255,
b: 255,
anchor_x: 0.5,
anchor_y: 7.0,
size_px: 50 }
outputs.primitives << state.touch_controls.left.center.merge(text: "left rocket",
anchor_x: 0.5,
anchor_y: 4.0,
size_px: 35,
r: 255,
g: 255,
b: 255)
outputs.primitives << state.touch_controls.right.center.merge(text: "right rocket",
anchor_x: 0.5,
anchor_y: 4.0,
size_px: 35,
r: 255,
g: 255,
b: 255)
if inputs_last_active_resolved == :mouse
outputs.primitives << touch_controls_left_prefab
outputs.primitives << touch_controls_right_prefab
elsif inputs_last_active_resolved == :controller
outputs.primitives << controller_l1_prefab
outputs.primitives << controller_r1_prefab
else
outputs.primitives << keyboard_f_prefab
outputs.primitives << keyboard_j_prefab
end
end
def render_success_scene
return if state.scene != :success
render_viewport
hud.primitives << { x: 0, y: 0, w: 64, h: 64, path: :solid, r: 0, g: 0, b: 0, a: 128 }
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.name,
anchor_x: 0.5,
anchor_y: -5.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.description_1,
anchor_x: 0.5,
anchor_y: -3.5,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.description_2,
anchor_x: 0.5,
anchor_y: -2.0,
r: 255,
g: 255,
b: 255)
hud.primitives << md_label(x: 33,
y: 33,
text: "success",
anchor_x: 0.5,
anchor_y: 0.50,
r: 0,
g: 255,
b: 0)
hud.primitives << sm_label(x: 33,
y: 33,
text: "#{input_name_for_enter_key}",
anchor_x: 0.5,
anchor_y: 3.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 32,
y: 33,
text: "for next mission",
anchor_x: 0.5,
anchor_y: 4.5,
r: 255,
g: 255,
b: 255)
end
def input_name_for_enter_key
case inputs_last_active_resolved
when :controller
"press start"
when :keyboard
"press enter"
else
"tap screen"
end
end
def inputs_last_active_resolved
r = inputs.last_active
if r == :mouse && !GTK.platform?(:touch)
r = :keyboard
end
r
end
def inputs_scheme_name
case inputs_last_active_resolved
when :controller
"gamepad"
when :mouse
"touch"
when :keyboard
"keyboard"
else
"keyboard"
end
end
def render_edit_scene
return if state.scene != :edit
render_full_map
end
def render_game_scene
return if state.scene != :game
render_viewport
end
def render_mission_scene
return if state.scene != :mission
if state.scene_at == state.clock
state.player = new_player
state.hazards = load_hazards
end
render_viewport
hud.primitives << { x: 0, y: 0, w: 64, h: 64, path: :solid, r: 0, g: 0, b: 0, a: 128 }
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.name,
anchor_x: 0.5,
anchor_y: -5.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.description_1,
anchor_x: 0.5,
anchor_y: -3.5,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: current_mission.description_2,
anchor_x: 0.5,
anchor_y: -2.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: "#{input_name_for_enter_key}",
anchor_x: 0.5,
anchor_y: 3.0,
r: 255,
g: 255,
b: 255)
hud.primitives << sm_label(x: 33,
y: 33,
text: "To Begin",
anchor_x: 0.5,
anchor_y: 4.5,
r: 255,
g: 255,
b: 255)
end
def rt_scale
11.25
end
def render_viewport
if player.y > 70
state.target_scale = 2.0
else
state.target_scale = 1.0
end
state.scale ||= 1.0
state.scale = Easing.smooth_start(initial: state.scale,
final: state.target_scale,
perc: 0.05,
power: 1)
source_x = state.camera.x - 32 * state.scale
source_y = state.camera.y - 32 * state.scale
source_x = source_x.clamp(0, 720 - 64 * state.scale)
source_y = source_y.clamp(0, 720 - 64 * state.scale)
source_x = source_x.to_i
source_y = source_y.to_i
outputs[:viewport].w = 64
outputs[:viewport].h = 64
outputs[:viewport].sprites << { x: 0,
y: 0,
w: rt_size,
h: rt_size,
source_x: source_x,
source_y: source_y,
source_w: rt_size * state.scale,
source_h: rt_size * state.scale,
path: :lowrez,
anchor_x: 0.0,
anchor_y: 0.0 }
outputs.sprites << { x: 360,
y: 1280 - rt_size * rt_scale,
w: rt_size * rt_scale,
h: rt_size * rt_scale,
anchor_x: 0.5,
anchor_y: 0.0,
path: :viewport }
outputs.sprites << { x: 360,
y: 1280 - rt_size * rt_scale,
w: rt_size * rt_scale,
h: rt_size * rt_scale,
anchor_x: 0.5,
anchor_y: 0.0,
path: :hud }
end
def render_full_map
outputs.sprites << { x: 360,
y: 640,
w: 720,
h: 720,
path: :lowrez,
anchor_x: 0.5,
anchor_y: 0.5 }
end
def sm_label opts
{ anchor_x: 0, anchor_y: 0, size_px: 5, font: "fonts/pico8.ttf", **opts }
end
def md_label opts
{ anchor_x: 0, anchor_y: 0, size_px: 10, font: "fonts/pico8.ttf", **opts }
end
def lg_label opts
{ anchor_x: 0, anchor_y: 0, size_px: 15, font: "fonts/pico8.ttf", **opts }
end
def outputs
args.outputs
end
def state
args.state ||= {}
end
def lowrez
outputs[:lowrez]
end
def hud
outputs[:hud]
end
def inputs
args.inputs
end
def player
state.player
end
end
def boot args
args.state = nil
end
def tick args
$game ||= Game.new
$game.args = args
$game.tick
end
def reset args
$game = nil
end
# GTK.reset
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment