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 ||= 64
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 ||= 1.0
state.sabre.x ||= state.player.x
state.sabre.y ||= state.player.y
state.actions_lookup ||= new_actions_lookup
end
def render
outputs.background_color = [32, 32, 32]
outputs[:scene].transient!
outputs[:scene].w = 128
outputs[:scene].h = 128
outputs[:scene].borders << { x: 0, y: 0, w: 128, h: 128, r: 255, g: 255, b: 255 }
render_player
render_sabre
args.outputs.sprites << { x: 320, y: 0, w: 640, h: 640, path: :scene }
args.outputs.labels << { x: 10, y: 100, text: "Controls:", r: 255, g: 255, b: 255, size_enum: -1 }
args.outputs.labels << { x: 10, y: 80, text: "Move: left/right", r: 255, g: 255, b: 255, size_enum: -1 }
args.outputs.labels << { x: 10, y: 60, text: "Jump: space | up | right click", r: 255, g: 255, b: 255, size_enum: -1 }
args.outputs.labels << { x: 10, y: 40, text: "Attack: f | j | left click", r: 255, g: 255, b: 255, size_enum: -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
outputs[:scene].sprites << { x: state.sabre.x + offset,
y: 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"
},
throw_0: {
frame_count: 8,
throw_frame: 2,
catch_frame: 6,
path: "sprites/kenobi/slash-2/:index.png"
},
throw_1: {
frame_count: 9,
throw_frame: 2,
catch_frame: 7,
path: "sprites/kenobi/slash-3/:index.png"
},
throw_2: {
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: state.player.x + 1 - 8,
y: state.player.y,
w: 16,
h: 16,
flip_horizontally: flip_horizontally }
if state.player.action == :standing
if state.player.y != 0
if state.player.jump_count <= 1
outputs[:scene].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
outputs[:scene].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
outputs[:scene].sprites << { **player_sprite, path: "sprites/kenobi/run/#{index}.png" }
else
outputs[:scene].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
outputs[:scene].sprites << { **player_sprite, path: slash_path }
end
end
def calc_input
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 += 0.1 * 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_movement
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_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.player.dx.abs < 0.09
state.player.dx = 0
end
state.player.x = 8 if state.player.x < 8
state.player.x = 120 if state.player.x > 120
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 = [:throw_0, :throw_1, :throw_2]
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.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 += 0.25 * 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 += 0.25 * state.player.facing
end
elsif state.player.action == :throw_0
if is_air_born
state.player.dy = 1.0
end
state.player.dx += 0.5 * state.player.facing
elsif state.player.action == :throw_1
if is_air_born
state.player.dy = 1.0
end
state.player.dx += 0.5 * state.player.facing
elsif state.player.action == :throw_2
if is_air_born
state.player.dy = 1.0
end
state.player.dx += 0.5 * 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.0
else
state.player.dy += 1.0
end
state.player.dx += 1.0 * 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 = -0.5
end
state.player.dx += 0.5 * state.player.facing
end
end
def tick
defaults
calc
render
end
end
$game = Game.new
def tick args
$game.args = args
$game.tick
end
$gtk.reset