Skip to content

Instantly share code, notes, and snippets.

@amirrajan
Created November 21, 2023 07:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save amirrajan/606db1e18454ad0c33ddb67795a5df3c to your computer and use it in GitHub Desktop.
Save amirrajan/606db1e18454ad0c33ddb67795a5df3c to your computer and use it in GitHub Desktop.
DragonRuby Game Toolkit - Lowrez Kenobi Source
require 'app/lowrez.rb'
class Game
attr_gtk
def request_action name, at: nil
at ||= state.tick_count
state.player.requested_action = name
state.player.requested_action_at = at
end
def defaults
state.player.x ||= 32
state.player.y ||= 0
state.player.dx ||= 0
state.player.dy ||= 0
state.player.action ||= :standing
state.player.action_at ||= 0
state.player.next_action_queue ||= {}
state.player.facing ||= 1
state.player.jump_at ||= 0
state.player.jump_count ||= 0
state.player.max_speed ||= 0.7
state.enemies ||= create_enemies
state.dead_enemies ||= []
state.explosions ||= []
state.enemy_lasers ||= []
state.sabre.x ||= state.player.x
state.sabre.y ||= state.player.y
state.camera.x ||= 0
state.camera.y ||= 0
state.actions_lookup ||= new_actions_lookup
end
def lowrez
args.lowrez
end
def render
# background
lowrez.background_color = [0, 0, 0]
# letter box
args.outputs.background_color = [32, 32, 32]
if state.is_game_over && state.game_over_at.elapsed_time > 30
lowrez.labels << { x: 32, y: 32, text: "Press ENTER", alignment_enum: 1, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: 32, y: 26, text: "to go again", alignment_enum: 1, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
else
lowrez.labels << { x: vx(32), y: vy(32), text: "Go that way ->", alignment_enum: 1, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(108), y: vy(10), text: "JUMP!", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(190), y: vy(32), text: "ATTACK!", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(300), y: vy(50), text: "Help me LOWREZ Kenobi! You're my only hope!", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
end
if state.player.x > 256
lowrez.labels << { x: vx(512), y: vy(50), text: "TODO: Make more enemies and stuff...", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(724), y: vy(50), text: "This is all I was able to get done...", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(934), y: vy(50), text: "Would you like me to toast some bread for you?", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1168), y: vy(50), text: "It might be a bit on the dark side though...", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1430), y: vy(50), text: "Why are you still here? There isn't anything else >_>", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(60), text: "Never gonna ", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(55), text: "give you up.", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(45), text: "Never gonna", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(40), text: "let you down.", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(30), text: "Never gonna ", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(25), text: "run around and", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
lowrez.labels << { x: vx(1780), y: vy(20), text: "desert you.", alignment_enum: 0, size_enum: LOWREZ_FONT_SM, font: LOWREZ_FONT_PATH, r: 255, g: 255, b: 255 }
end
# lines
lowrez.lines << { x: vx(-1280), y: vy(2), x2: 2560, y2: 2, r: 255, g: 255, b: 255 }
lowrez.lines << { x: vx(0 - 8), y: vy(0), x2: vx(0 - 8), y2: (64), r: 255, g: 255, b: 255 }
render_player
render_sabre
render_enemies
render_enemy_lasers
end
def render_enemy_lasers
lowrez.sprites << state.enemy_lasers.map do |e|
e.merge x: vx(e.x), y: vy(e.y), path: :pixel, w: 3, h: 1, angle: state.tick_count * 15
end
end
def render_enemies
lowrez.sprites << state.enemies.map do |e|
e.merge x: vx(e.x), y: vy(e.y)
end
state.dead_enemies.each do |e|
e.a ||= 255
e.a -= 4
end
state.dead_enemies.reject! { |e| e.a < 0 }
lowrez.sprites << state.dead_enemies.map do |e|
e.merge x: vx(e.x), y: vy(e.y)
end
state.explosions.each do |e|
e.path ||= :pixel
e.lifetime ||= 15
e.a ||= 255
e.lifetime -= 1
e.scale_d ||= 0.1
if e.lifetime < 7
e.a -= 2
end
e.x -= e.scale_d
e.y -= e.scale_d
e.w += e.scale_d * 3
e.h += e.scale_d * 3
e.scale_d *= 1.2
end
state.explosions.reject! { |e| e.lifetime < 0 }
lowrez.sprites << state.explosions.map do |e|
e.merge x: vx(e.x), y: vy(e.y)
end
end
def queue_explosion rect
state.explosions << { x: rect.x, y: rect.y, w: 1, h: 1 }
end
def render_sabre
return if !state.sabre.is_active
sabre_index = 0.frame_index count: 4,
hold_for: 2,
repeat: true
offset = 0
offset = -8 if state.player.facing == -1
lowrez.sprites << { x: vx(state.sabre.x) + offset,
y: vy(state.sabre.y), w: 16, h: 16, path: "sprites/sabre-throw/#{sabre_index}.png" }
end
def new_actions_lookup
r = {
slash_0: {
frame_count: 6,
interrupt_count: 4,
path: "sprites/kenobi/slash-0/:index.png"
},
slash_1: {
frame_count: 6,
interrupt_count: 4,
path: "sprites/kenobi/slash-1/:index.png"
},
slash_2: {
frame_count: 8,
throw_frame: 2,
catch_frame: 6,
path: "sprites/kenobi/slash-2/:index.png"
},
slash_3: {
frame_count: 9,
throw_frame: 2,
catch_frame: 7,
path: "sprites/kenobi/slash-3/:index.png"
},
slash_4: {
frame_count: 9,
throw_frame: 2,
catch_frame: 7,
path: "sprites/kenobi/slash-4/:index.png"
},
slash_5: {
frame_count: 11,
path: "sprites/kenobi/slash-5/:index.png"
},
slash_6: {
frame_count: 8,
interrupt_count: 6,
path: "sprites/kenobi/slash-6/:index.png"
}
}
r.each.with_index do |(k, v), i|
v.name ||= k
v.index ||= i
v.hold_for ||= 5
v.duration ||= v.frame_count * v.hold_for
v.last_index ||= v.frame_count - 1
v.interrupt_count ||= v.frame_count
v.interrupt_duration ||= v.interrupt_count * v.hold_for
v.repeat ||= false
v.next_action ||= r[r.keys[i + 1]]
end
r
end
def render_player
flip_horizontally = if state.player.facing == -1
true
else
false
end
player_sprite = { x: vx(state.player.x + 1 - 8),
y: vy(state.player.y),
w: 16,
h: 16,
flip_horizontally: flip_horizontally }
if state.is_game_over
player_sprite.flip_horizontally = false
player_sprite.angle = 90
player_sprite.y -= 2
lowrez.sprites << { **player_sprite, path: "sprites/kenobi/standing.png" }
elsif state.player.action == :standing
if state.player.y != 0
if state.player.jump_count <= 1
lowrez.sprites << { **player_sprite, path: "sprites/kenobi/jumping.png" }
else
index = state.player.jump_at.frame_index count: 8, hold_for: 5, repeat: false
index ||= 7
lowrez.sprites << { **player_sprite, path: "sprites/kenobi/second-jump/#{index}.png" }
end
elsif state.player.dx != 0
index = state.player.action_at.frame_index count: 4, hold_for: 5, repeat: true
lowrez.sprites << { **player_sprite, path: "sprites/kenobi/run/#{index}.png" }
else
lowrez.sprites << { **player_sprite, path: 'sprites/kenobi/standing.png'}
end
else
v = state.actions_lookup[state.player.action]
slash_frame_index = state.player.action_at.frame_index count: v.frame_count,
hold_for: v.hold_for,
repeat: v.repeat
slash_frame_index ||= v.last_index
slash_path = v.path.sub ":index", slash_frame_index.to_s
lowrez.sprites << { **player_sprite, path: slash_path }
end
end
def calc_input
if state.is_game_over
enter_pressed = inputs.keyboard.key_down.enter ||
inputs.keyboard.key_down.j ||
inputs.keyboard.key_down.f ||
inputs.keyboard.key_down.space ||
inputs.mouse.button_left ||
inputs.mouse.button_right ||
inputs.controller_one.key_down.a ||
inputs.controller_one.key_down.b
if enter_pressed && state.game_over_at.elapsed_time > 30
state.game_over_count_down ||= 120
state.game_over_count_down = -1
end
return
end
if state.player.next_action_queue.length > 2
raise "Code in calc assums that key length of state.player.next_action_queue will never be greater than 2."
end
if inputs.controller_one.key_down.a ||
inputs.mouse.button_left ||
inputs.keyboard.key_down.j ||
inputs.keyboard.key_down.f
request_action :attack
end
should_update_facing = false
if state.player.action == :standing
should_update_facing = true
else
key_0 = state.player.next_action_queue.keys[0]
key_1 = state.player.next_action_queue.keys[1]
if state.tick_count == key_0
should_update_facing = true
elsif state.tick_count == key_1
should_update_facing = true
elsif key_0 && key_1 && state.tick_count.between?(key_0, key_1)
should_update_facing = true
end
end
if should_update_facing && inputs.left_right.sign != state.player.facing.sign
state.player.dx = 0
if inputs.left
state.player.facing = -1
elsif inputs.right
state.player.facing = 1
end
state.player.dx += state.player.max_speed * inputs.left_right
end
if state.player.action == :standing
state.player.dx += 0.1 * inputs.left_right
if state.player.dx.abs > state.player.max_speed
state.player.dx = state.player.max_speed * state.player.dx.sign
end
end
was_jump_requested = inputs.keyboard.key_down.up ||
inputs.keyboard.key_down.w ||
inputs.mouse.button_right ||
inputs.controller_one.key_down.up ||
inputs.controller_one.key_down.b ||
inputs.keyboard.key_down.space
can_jump = state.player.jump_at.elapsed_time > 20
if state.player.jump_count <= 1
can_jump = state.player.jump_at.elapsed_time > 10
end
if was_jump_requested && can_jump
if state.player.action == :slash_6
state.player.action = :standing
end
state.player.dy = 1
state.player.jump_count += 1
state.player.jump_at = state.tick_count
end
end
def calc
calc_input
calc_requested_action
calc_next_action
calc_sabre
calc_player_boxes
calc_player_movement
calc_player_attack
calc_camera
calc_enemies
calc_enemy_lasers
if state.player.y <= 0 && state.player.dy < 0
state.player.y = 0
state.player.dy = 0
state.player.jump_at = 0
state.player.jump_count = 0
end
end
def calc_player_attack
# lowrez.sprites << vsprite(state.player.hurt_box.merge(path: :pixel, a: 80))
if state.player.action != :standing
# lowrez.sprites << vsprite(state.player.hit_box.merge(path: :pixel, a: 80))
lasers = state.enemy_lasers.find_all do |l|
l.intersect_rect? state.player.hit_box
end.each do |l|
away = geometry.angle_from(l, state.player.hurt_box_center).vector
l.dx = away.x * 2
l.dy = away.y * 2
end
shooters = state.enemies.find_all do |e|
e.type == :shooter && e.intersect_rect?(state.player.hit_box)
end
state.enemies -= shooters
state.dead_enemies += shooters
shooters.each do |s|
queue_explosion s
end
end
end
def calc_enemy_lasers
state.enemy_lasers.each do |l|
l.x += l.dx
l.y += l.dy
l.lifetime ||= 600
l.lifetime -= 1
end
state.enemy_lasers.reject! { |l| l.lifetime < 0 }
end
def create_enemies
[
{ x: 160, y: 3, w: 1, h: 10, path: :pixel, dx: 0, mode: :activated, type: :wall },
{ x: 256,
y: 8,
w: 3,
h: 3,
path: :pixel,
dx: 0,
mode: :activated,
type: :shooter,
cool_down: 60,
current_cool_down: 0 },
{ x: 260,
y: 15,
w: 3,
h: 3,
path: :pixel,
dx: 0,
mode: :activated,
type: :shooter,
cool_down: 60,
current_cool_down: 0 }
]
end
def calc_wall_enemy e
if (e.x - state.player.x).between?(0, 60) && e.mode == :activated
e.dx -= 0.05
e.x += e.dx
elsif (e.x - state.player.x) < 0
e.dx -= 0.05
e.x += e.dx
e.mode = :deactivated
elsif (e.x - state.player.x).abs > 45
e.mode = :activated
end
e.dx = -2 if e.dx.abs > 2
end
def calc_shooter_enemy e
if (e.x - state.player.x).abs.between?(5, 60) && e.mode == :activated
e.current_cool_down -= 1
if e.current_cool_down < 0
to_player = geometry.angle_to(e, state.player.hurt_box_center).vector
state.enemy_lasers << { x: e.x + 1, y: e.y + 1, dx: to_player.x, dy: to_player.y, w: 3, h: 3 }
e.current_cool_down = e.cool_down
end
end
end
def calc_enemies
return if state.is_game_over
return if state.tick_count < 60
state.enemies.each do |e|
if e.type == :wall
calc_wall_enemy e
elsif e.type == :shooter
calc_shooter_enemy e
end
end
collision = state.enemies.find do |e|
if e.type == :wall
e.intersect_rect? state.player.hurt_box
end
end
collision ||= state.enemy_lasers.find do |e|
e.intersect_rect? state.player.hurt_box
end
if collision
state.is_game_over = true
state.game_over_at = state.tick_count
state.player.dx = collision.dx * 2
state.player.x += state.player.dx
state.player.facing = collision.dx.sign
if collision.type == :wall
state.player.dy = 2
elsif state.player.y == 0
state.player.dy = 0.5
end
end
end
def vsprite s
s.merge x: vx(s.x), y: vy(s.y)
end
def vx x
(x - state.camera.x).round
end
def vy y
(y - state.camera.y).round
end
def vw w
w
end
def vh h
h
end
def calc_camera
percentage = 0.03
target_x = state.player.x
target_y = state.player.y
if state.sabre.is_active
target_x = state.sabre.x + 16 * state.player.facing
else
if state.player.dx.abs > 0.2
target_x += 40 * state.player.facing
end
end
distance_x = target_x - (state.camera.x + 32)
distance_y = target_y - (state.camera.y + 32)
state.camera.x += distance_x * percentage if distance_x.abs > 5
state.camera.y += distance_y * percentage if distance_y.abs > 5
# state.camera.x = 0 if state.camera.x < 0
state.camera.y = 0 if state.camera.y < 0
end
def calc_player_boxes
if state.player.facing == 1
state.player.hurt_box = { x: state.player.x - 1, y: state.player.y + 5, w: 2, h: 4 }
else
state.player.hurt_box = { x: state.player.x + 1, y: state.player.y + 5, w: 2, h: 4 }
end
state.player.hurt_box_center = { x: state.player.x - 2 + 2, y: state.player.y + 2 + 4 }
if state.sabre.is_active
state.player.hit_box = { x: state.sabre.x + 1, y: state.sabre.y + 7, w: 14, h: 3 }
else
if state.player.facing == 1
state.player.hit_box = { x: state.player.x + 4, y: state.player.y + 4, w: 5, h: 8 }
else
state.player.hit_box = { x: state.player.x - 7, y: state.player.y + 4, w: 5, h: 8 }
end
if state.player.action == :slash_5
state.player.hit_box = { x: state.player.x - 8, y: state.player.y + 4, w: 16, h: 8 }
end
if state.player.action == :slash_6
state.player.hit_box = { x: state.player.x - 8, y: state.player.y, w: 16, h: 16 }
end
end
end
def calc_player_movement
state.player.x += state.player.dx
state.player.y += state.player.dy
state.player.dy -= 0.05
if state.player.y <= 0
state.player.y = 0
state.player.dy = 0
state.player.jump_at = 0
state.player.jump_count = 0
end
if state.is_game_over
state.player.dx *= 0.97
else
state.player.dx *= 0.95
end
if state.player.dx.abs < 0.09
state.player.dx = 0
end
state.player.x = 0 if state.player.x < 0
state.player.x = 1800 if state.player.x > 1800
end
def calc_requested_action
return if !state.player.requested_action
return if state.player.requested_action_at > state.tick_count
player_action = state.player.action
player_action_at = state.player.action_at
# first attack
if state.player.requested_action == :attack
if player_action == :standing
state.player.next_action_queue.clear
state.player.next_action_queue[state.tick_count] = :slash_0
state.player.next_action_queue[state.tick_count + state.actions_lookup.slash_0.duration] = :standing
else
current_action = state.actions_lookup[state.player.action]
state.player.next_action_queue.clear
queue_at = player_action_at + current_action.interrupt_duration
queue_at = state.tick_count if queue_at < state.tick_count
next_action = current_action.next_action
next_action ||= { name: :standing,
duration: 4 }
if next_action
state.player.next_action_queue[queue_at] = next_action.name
state.player.next_action_queue[player_action_at +
current_action.interrupt_duration +
next_action.duration] = :standing
end
end
end
state.player.requested_action = nil
state.player.requested_action_at = nil
end
def calc_sabre
can_throw_sabre = true
sabre_throws = [:slash_2, :slash_3, :slash_4]
if !sabre_throws.include? state.player.action
state.sabre.facing = nil
state.sabre.is_active = false
return
end
current_action = state.actions_lookup[state.player.action]
throw_at = state.player.action_at + (current_action.throw_frame) * 5
catch_at = state.player.action_at + (current_action.catch_frame) * 5
if !state.tick_count.between? throw_at, catch_at
state.sabre.facing = nil
state.sabre.is_active = false
return
end
state.sabre.facing ||= state.player.facing
state.sabre.is_active = true
spline = [
[ 0, 0.25, 0.75, 1.0],
[1.0, 0.75, 0.25, 0]
]
throw_duration = catch_at - throw_at
current_progress = args.easing.ease_spline throw_at,
state.tick_count,
throw_duration,
spline
farthest_sabre_x = 32
state.sabre.y = state.player.y
state.sabre.x = state.player.x + farthest_sabre_x * current_progress * state.sabre.facing
end
def calc_next_action
return if state.is_game_over
return if !state.player.next_action_queue[state.tick_count]
state.player.previous_action = state.player.action
state.player.previous_action_at = state.player.action_at
state.player.previous_action_ended_at = state.tick_count
state.player.action = state.player.next_action_queue[state.tick_count]
state.player.action_at = state.tick_count
is_air_born = state.player.y != 0
if state.player.action == :slash_0
state.player.dy = 0 if state.player.dy > 0
if is_air_born
state.player.dy = 0.5
else
state.player.dx += 1.5 * state.player.facing
end
elsif state.player.action == :slash_1
state.player.dy = 0 if state.player.dy > 0
if is_air_born
state.player.dy = 0.5
else
state.player.dx += 1.5 * state.player.facing
end
elsif state.player.action == :slash_2
if is_air_born
state.player.dy = 1.0
end
state.player.dx += 0.25 * state.player.facing
elsif state.player.action == :slash_3
if is_air_born
state.player.dy = 1.0
end
state.player.dx += 0.25 * state.player.facing
elsif state.player.action == :slash_4
if is_air_born
state.player.dy = 1.2
end
state.player.dx += 0.25 * state.player.facing
elsif state.player.action == :slash_5
state.player.dy = 0 if state.player.dy < 0
if is_air_born
state.player.dy += 1.5
else
state.player.dy += 0.5
end
state.player.dx += 3 * state.player.facing
elsif state.player.action == :slash_6
state.player.dy = 0 if state.player.dy > 0
if is_air_born
state.player.dy = -1.5
end
state.player.dx += 1.5 * state.player.facing
end
end
def tick
if state.is_game_over
if state.game_over_count_down && state.game_over_count_down < 0
$gtk.reset
else
if state.player.y == 0
state.game_over_count_down ||= 120
state.game_over_count_down -= 1
if state.game_over_count_down < 0
$gtk.reset
end
end
end
end
defaults
calc
render
args.outputs.labels << { x: 10, y: 10.from_top, text: "Controls:", r: 255, g: 255, b: 255, size_enum: -1 }
args.outputs.labels << { x: 10, y: 33.from_top, text: "Move: left/right", r: 255, g: 255, b: 255, size_enum: -1 }
args.outputs.labels << { x: 10, y: 56.from_top, text: "Jump: space | up | right click", r: 255, g: 255, b: 255, size_enum: -1 }
args.outputs.labels << { x: 10, y: 79.from_top, text: "Attack: f | j | left click", r: 255, g: 255, b: 255, size_enum: -1 }
end
def lsprites
lowrez.sprites
end
end
$game = Game.new
def tick args
if !args.inputs.keyboard.has_focus && args.gtk.production && args.state.tick_count != 0
args.outputs.background_color = [0, 0, 0]
args.outputs.labels << { x: 640, y: 360, text: "Game Paused (click to resume).", alignment_enum: 1, r: 255, g: 255, b: 255 }
else
$game.args = args
$game.tick
end
end
# $gtk.reset
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment