summaryrefslogtreecommitdiffhomepage
path: root/samples/99_genre_platformer/the_little_probe/app/main.rb
diff options
context:
space:
mode:
authorAmir Rajan <[email protected]>2020-09-11 02:02:01 -0500
committerAmir Rajan <[email protected]>2020-09-11 02:02:57 -0500
commit33ec37b141e896b47ed642923fd33b0c658ae9fb (patch)
treea40d3e5d41beeb06508200078f6f26b0ee57d6a4 /samples/99_genre_platformer/the_little_probe/app/main.rb
parent958cf43779d2bf528869e80511c4c4f2a433b2db (diff)
downloaddragonruby-game-toolkit-contrib-33ec37b141e896b47ed642923fd33b0c658ae9fb.tar.gz
dragonruby-game-toolkit-contrib-33ec37b141e896b47ed642923fd33b0c658ae9fb.zip
synced samples
Diffstat (limited to 'samples/99_genre_platformer/the_little_probe/app/main.rb')
-rw-r--r--samples/99_genre_platformer/the_little_probe/app/main.rb887
1 files changed, 887 insertions, 0 deletions
diff --git a/samples/99_genre_platformer/the_little_probe/app/main.rb b/samples/99_genre_platformer/the_little_probe/app/main.rb
new file mode 100644
index 0000000..1c90218
--- /dev/null
+++ b/samples/99_genre_platformer/the_little_probe/app/main.rb
@@ -0,0 +1,887 @@
+class FallingCircle
+ attr_gtk
+
+ def tick
+ fiddle
+ defaults
+ render
+ input
+ calc
+ end
+
+ def fiddle
+ state.gravity = -0.02
+ circle.radius = 15
+ circle.elasticity = 0.4
+ camera.follow_speed = 0.4 * 0.4
+ end
+
+ def render
+ render_stage_editor
+ render_debug
+ render_game
+ end
+
+ def defaults
+ if state.tick_count == 0
+ outputs.sounds << "sounds/bg.ogg"
+ end
+
+ state.storyline ||= [
+ { text: "<- -> to aim, hold space to charge", distance_gate: 0 },
+ { text: "the little probe - by @amirrajan, made with DragonRuby Game Toolkit", distance_gate: 0 },
+ { text: "mission control, this is sasha. landing on europa successful.", distance_gate: 0 },
+ { text: "operation \"find earth 2.0\", initiated at 8-29-2036 14:00.", distance_gate: 0 },
+ { text: "jupiter's sure is beautiful...", distance_gate: 4000 },
+ { text: "hmm, it seems there's some kind of anomoly in the sky", distance_gate: 7000 },
+ { text: "dancing lights, i'll call them whisps.", distance_gate: 8000 },
+ { text: "#todo... look i ran out of time -_-", distance_gate: 9000 },
+ { text: "there's never enough time", distance_gate: 9000 },
+ { text: "the game jam was fun though ^_^", distance_gate: 10000 },
+ ]
+
+ load_level force: args.state.tick_count == 0
+ state.line_mode ||= :terrain
+
+ state.sound_index ||= 1
+ circle.potential_lift ||= 0
+ circle.angle ||= 90
+ circle.check_point_at ||= -1000
+ circle.game_over_at ||= -1000
+ circle.x ||= -485
+ circle.y ||= 12226
+ circle.check_point_x ||= circle.x
+ circle.check_point_y ||= circle.y
+ circle.dy ||= 0
+ circle.dx ||= 0
+ circle.previous_dy ||= 0
+ circle.previous_dx ||= 0
+ circle.angle ||= 0
+ circle.after_images ||= []
+ circle.terrains_to_monitor ||= {}
+ circle.impact_history ||= []
+
+ camera.x ||= 0
+ camera.y ||= 0
+ camera.target_x ||= 0
+ camera.target_y ||= 0
+ state.snaps ||= { }
+ state.snap_number = 10
+
+ args.state.storyline_x ||= -1000
+ args.state.storyline_y ||= -1000
+ end
+
+ def render_game
+ outputs.background_color = [0, 0, 0]
+ outputs.sprites << [-circle.x + 1100,
+ -circle.y - 100,
+ 2416 * 4,
+ 3574 * 4,
+ 'sprites/jupiter.png']
+ outputs.sprites << [-circle.x,
+ -circle.y,
+ 2416 * 4,
+ 3574 * 4,
+ 'sprites/level.png']
+ outputs.sprites << state.whisp_queue
+ render_aiming_retical
+ render_circle
+ render_notification
+ end
+
+ def render_notification
+ toast_length = 500
+ if circle.game_over_at.elapsed_time < toast_length
+ label_text = "..."
+ elsif circle.check_point_at.elapsed_time > toast_length
+ args.state.current_storyline = nil
+ return
+ end
+ if circle.check_point_at &&
+ circle.check_point_at.elapsed_time == 1 &&
+ !args.state.current_storyline
+ if args.state.storyline.length > 0 && args.state.distance_traveled > args.state.storyline[0][:distance_gate]
+ args.state.current_storyline = args.state.storyline.shift[:text]
+ args.state.distance_traveled ||= 0
+ args.state.storyline_x = circle.x
+ args.state.storyline_y = circle.y
+ end
+ return unless args.state.current_storyline
+ end
+ label_text = args.state.current_storyline
+ return unless label_text
+ x = circle.x + camera.x
+ y = circle.y + camera.y - 40
+ w = 900
+ h = 30
+ outputs.primitives << [x - w.idiv(2), y - h, w, h, 255, 255, 255, 255].solid
+ outputs.primitives << [x - w.idiv(2), y - h, w, h, 0, 0, 0, 255].border
+ outputs.labels << [x, y - 4, label_text, 1, 1, 0, 0, 0, 255]
+ end
+
+ def render_aiming_retical
+ outputs.sprites << [state.camera.x + circle.x + circle.angle.vector_x(circle.potential_lift * 10) - 5,
+ state.camera.y + circle.y + circle.angle.vector_y(circle.potential_lift * 10) - 5,
+ 10, 10, 'sprites/circle-orange.png']
+ outputs.sprites << [state.camera.x + circle.x + circle.angle.vector_x(circle.radius * 3) - 5,
+ state.camera.y + circle.y + circle.angle.vector_y(circle.radius * 3) - 5,
+ 10, 10, 'sprites/circle-orange.png', 0, 128]
+ if rand > 0.9
+ outputs.sprites << [state.camera.x + circle.x + circle.angle.vector_x(circle.radius * 3) - 5,
+ state.camera.y + circle.y + circle.angle.vector_y(circle.radius * 3) - 5,
+ 10, 10, 'sprites/circle-white.png', 0, 128]
+ end
+ end
+
+ def render_circle
+ outputs.sprites << circle.after_images.map do |ai|
+ ai.merge(x: ai.x + state.camera.x - circle.radius,
+ y: ai.y + state.camera.y - circle.radius,
+ w: circle.radius * 2,
+ h: circle.radius * 2,
+ path: 'sprites/circle-white.png')
+ end
+
+ outputs.sprites << [(circle.x - circle.radius) + state.camera.x,
+ (circle.y - circle.radius) + state.camera.y,
+ circle.radius * 2,
+ circle.radius * 2,
+ 'sprites/probe.png']
+ end
+
+ def render_debug
+ return unless state.debug_mode
+
+ outputs.labels << [10, 30, state.line_mode, 0, 0, 0, 0, 0]
+ outputs.labels << [12, 32, state.line_mode, 0, 0, 255, 255, 255]
+
+ args.outputs.lines << trajectory(circle).line.to_hash.tap do |h|
+ h[:x] += state.camera.x
+ h[:y] += state.camera.y
+ h[:x2] += state.camera.x
+ h[:y2] += state.camera.y
+ end
+
+ outputs.primitives << state.terrain.find_all do |t|
+ circle.x.between?(t.x - 640, t.x2 + 640) || circle.y.between?(t.y - 360, t.y2 + 360)
+ end.map do |t|
+ [
+ t.line.associate(r: 0, g: 255, b: 0) do |h|
+ h.x += state.camera.x
+ h.y += state.camera.y
+ h.x2 += state.camera.x
+ h.y2 += state.camera.y
+ if circle.rect.intersect_rect? t[:rect]
+ h[:r] = 255
+ h[:g] = 0
+ end
+ h
+ end,
+ t[:rect].border.associate(r: 255, g: 0, b: 0) do |h|
+ h.x += state.camera.x
+ h.y += state.camera.y
+ h.b = 255 if line_near_rect? circle.rect, t
+ h
+ end
+ ]
+ end
+
+ outputs.primitives << state.lava.find_all do |t|
+ circle.x.between?(t.x - 640, t.x2 + 640) || circle.y.between?(t.y - 360, t.y2 + 360)
+ end.map do |t|
+ [
+ t.line.associate(r: 0, g: 0, b: 255) do |h|
+ h.x += state.camera.x
+ h.y += state.camera.y
+ h.x2 += state.camera.x
+ h.y2 += state.camera.y
+ if circle.rect.intersect_rect? t[:rect]
+ h[:r] = 255
+ h[:b] = 0
+ end
+ h
+ end,
+ t[:rect].border.associate(r: 255, g: 0, b: 0) do |h|
+ h.x += state.camera.x
+ h.y += state.camera.y
+ h.b = 255 if line_near_rect? circle.rect, t
+ h
+ end
+ ]
+ end
+
+ if state.god_mode
+ border = circle.rect.merge(x: circle.rect.x + state.camera.x,
+ y: circle.rect.y + state.camera.y,
+ g: 255)
+ else
+ border = circle.rect.merge(x: circle.rect.x + state.camera.x,
+ y: circle.rect.y + state.camera.y,
+ b: 255)
+ end
+
+ outputs.borders << border
+
+ overlapping ||= {}
+
+ circle.impact_history.each do |h|
+ label_mod = 300
+ x = (h[:body][:x].-(150).idiv(label_mod)) * label_mod + camera.x
+ y = (h[:body][:y].+(150).idiv(label_mod)) * label_mod + camera.y
+ 10.times do
+ if overlapping[x] && overlapping[x][y]
+ y -= 52
+ else
+ break
+ end
+ end
+
+ overlapping[x] ||= {}
+ overlapping[x][y] ||= true
+ outputs.primitives << [x, y - 25, 300, 50, 0, 0, 0, 128].solid
+ outputs.labels << [x + 10, y + 24, "dy: %.2f" % h[:body][:new_dy], -2, 0, 255, 255, 255]
+ outputs.labels << [x + 10, y + 9, "dx: %.2f" % h[:body][:new_dx], -2, 0, 255, 255, 255]
+ outputs.labels << [x + 10, y - 5, " ?: #{h[:body][:new_reason]}", -2, 0, 255, 255, 255]
+
+ outputs.labels << [x + 100, y + 24, "angle: %.2f" % h[:impact][:angle], -2, 0, 255, 255, 255]
+ outputs.labels << [x + 100, y + 9, "m(l): %.2f" % h[:terrain][:slope], -2, 0, 255, 255, 255]
+ outputs.labels << [x + 100, y - 5, "m(c): %.2f" % h[:body][:slope], -2, 0, 255, 255, 255]
+
+ outputs.labels << [x + 200, y + 24, "ray: #{h[:impact][:ray]}", -2, 0, 255, 255, 255]
+ outputs.labels << [x + 200, y + 9, "nxt: #{h[:impact][:ray_next]}", -2, 0, 255, 255, 255]
+ outputs.labels << [x + 200, y - 5, "typ: #{h[:impact][:type]}", -2, 0, 255, 255, 255]
+ end
+
+ if circle.floor
+ outputs.labels << [circle.x + camera.x + 30, circle.y + camera.y + 100, "point: #{circle.floor_point.slice(:x, :y).values}", -2, 0]
+ outputs.labels << [circle.x + camera.x + 31, circle.y + camera.y + 101, "point: #{circle.floor_point.slice(:x, :y).values}", -2, 0, 255, 255, 255]
+ outputs.labels << [circle.x + camera.x + 30, circle.y + camera.y + 85, "circle: #{circle.hash.slice(:x, :y).values}", -2, 0]
+ outputs.labels << [circle.x + camera.x + 31, circle.y + camera.y + 86, "circle: #{circle.hash.slice(:x, :y).values}", -2, 0, 255, 255, 255]
+ outputs.labels << [circle.x + camera.x + 30, circle.y + camera.y + 70, "rel: #{circle.floor_relative_x} #{circle.floor_relative_y}", -2, 0]
+ outputs.labels << [circle.x + camera.x + 31, circle.y + camera.y + 71, "rel: #{circle.floor_relative_x} #{circle.floor_relative_y}", -2, 0, 255, 255, 255]
+ end
+ end
+
+ def render_stage_editor
+ return unless state.god_mode
+ return unless state.point_one
+ args.lines << [state.point_one, inputs.mouse.point, 0, 255, 255]
+ end
+
+ def trajectory body
+ [body.x + body.dx,
+ body.y + body.dy,
+ body.x + body.dx * 1000,
+ body.y + body.dy * 1000,
+ 0, 255, 255]
+ end
+
+ def lengthen_line line, num
+ line = normalize_line(line)
+ slope = geometry.line_slope(line, replace_infinity: 10).abs
+ if slope < 2
+ [line.x - num, line.y, line.x2 + num, line.y2].line.to_hash
+ else
+ [line.x, line.y, line.x2, line.y2].line.to_hash
+ end
+ end
+
+ def normalize_line line
+ if line.x > line.x2
+ x = line.x2
+ y = line.y2
+ x2 = line.x
+ y2 = line.y
+ else
+ x = line.x
+ y = line.y
+ x2 = line.x2
+ y2 = line.y2
+ end
+ [x, y, x2, y2]
+ end
+
+ def rect_for_line line
+ if line.x > line.x2
+ x = line.x2
+ y = line.y2
+ x2 = line.x
+ y2 = line.y
+ else
+ x = line.x
+ y = line.y
+ x2 = line.x2
+ y2 = line.y2
+ end
+
+ w = x2 - x
+ h = y2 - y
+
+ if h < 0
+ y += h
+ h = h.abs
+ end
+
+ if w < circle.radius
+ x -= circle.radius
+ w = circle.radius * 2
+ end
+
+ if h < circle.radius
+ y -= circle.radius
+ h = circle.radius * 2
+ end
+
+ { x: x, y: y, w: w, h: h }
+ end
+
+ def snap_to_grid x, y, snaps
+ snap_number = 10
+ x = x.to_i
+ y = y.to_i
+
+ x_floor = x.idiv(snap_number) * snap_number
+ x_mod = x % snap_number
+ x_ceil = (x.idiv(snap_number) + 1) * snap_number
+
+ y_floor = y.idiv(snap_number) * snap_number
+ y_mod = y % snap_number
+ y_ceil = (y.idiv(snap_number) + 1) * snap_number
+
+ if snaps[x_floor]
+ x_result = x_floor
+ elsif snaps[x_ceil]
+ x_result = x_ceil
+ elsif x_mod < snap_number.idiv(2)
+ x_result = x_floor
+ else
+ x_result = x_ceil
+ end
+
+ snaps[x_result] ||= {}
+
+ if snaps[x_result][y_floor]
+ y_result = y_floor
+ elsif snaps[x_result][y_ceil]
+ y_result = y_ceil
+ elsif y_mod < snap_number.idiv(2)
+ y_result = y_floor
+ else
+ y_result = y_ceil
+ end
+
+ snaps[x_result][y_result] = true
+ return [x_result, y_result]
+
+ end
+
+ def snap_line line
+ x, y, x2, y2 = line
+ end
+
+ def string_to_line s
+ x, y, x2, y2 = s.split(',').map(&:to_f)
+
+ if x > x2
+ x2, x = x, x2
+ y2, y = y, y2
+ end
+
+ x, y = snap_to_grid x, y, state.snaps
+ x2, y2 = snap_to_grid x2, y2, state.snaps
+ [x, y, x2, y2].line.to_hash
+ end
+
+ def load_lines file
+ data = gtk.read_file(file) || ""
+ data.each_line
+ .reject { |l| l.strip.length == 0 }
+ .map { |l| string_to_line l }
+ .map { |h| h.merge(rect: rect_for_line(h)) }
+ end
+
+ def load_terrain
+ load_lines 'level.txt'
+ end
+
+ def load_lava
+ load_lines 'level_lava.txt'
+ end
+
+ def load_level force: false
+ if force
+ state.snaps = {}
+ state.terrain = load_terrain
+ state.lava = load_lava
+ else
+ state.terrain ||= load_terrain
+ state.lava ||= load_lava
+ end
+ end
+
+ def save_lines lines, file
+ s = lines.map do |l|
+ "#{l.x1},#{l.y1},#{l.x2},#{l.y2}"
+ end.join("\n")
+ gtk.write_file(file, s)
+ end
+
+ def save_level
+ save_lines(state.terrain, 'level.txt')
+ save_lines(state.lava, 'level_lava.txt')
+ load_level force: true
+ end
+
+ def line_near_rect? rect, terrain
+ geometry.intersect_rect?(rect, terrain[:rect])
+ end
+
+ def point_within_line? point, line
+ return false if !point
+ return false if !line
+ return true
+ end
+
+ def calc_impacts x, dx, y, dy, radius
+ results = { }
+ results[:x] = x
+ results[:y] = y
+ results[:dx] = x
+ results[:dy] = y
+ results[:point] = { x: x, y: y }
+ results[:rect] = { x: x - radius, y: y - radius, w: radius * 2, h: radius * 2 }
+ results[:trajectory] = trajectory(results)
+ results[:impacts] = terrain.find_all { |t| line_near_rect? results[:rect], t }.map do |t|
+ {
+ terrain: t,
+ point: geometry.line_intersect(results[:trajectory], t),
+ type: :terrain
+ }
+ end.reject { |t| !point_within_line? t[:point], t[:terrain] }
+
+ results[:impacts] += lava.find_all { |t| line_near_rect? results[:rect], t }.map do |t|
+ {
+ terrain: t,
+ point: geometry.line_intersect(results[:trajectory], t),
+ type: :lava
+ }
+ end.reject { |t| !point_within_line? t[:point], t[:terrain] }
+
+ results
+ end
+
+ def calc_potential_impacts
+ impact_results = calc_impacts circle.x, circle.dx, circle.y, circle.dy, circle.radius
+ circle.rect = impact_results[:rect]
+ circle.trajectory = impact_results[:trajectory]
+ circle.impacts = impact_results[:impacts]
+ end
+
+ def calc_terrains_to_monitor
+ circle.impact = nil
+ circle.impacts.each do |i|
+ circle.terrains_to_monitor[i[:terrain]] ||= {
+ ray_start: geometry.ray_test(circle, i[:terrain]),
+ }
+
+ circle.terrains_to_monitor[i[:terrain]][:ray_current] = geometry.ray_test(circle, i[:terrain])
+ if circle.terrains_to_monitor[i[:terrain]][:ray_start] != circle.terrains_to_monitor[i[:terrain]][:ray_current]
+ if circle.x.between?(i[:terrain].x, i[:terrain].x2) || circle.y.between?(i[:terrain].y, i[:terrain].y2)
+ circle.impact = i
+ circle.ray_current = circle.terrains_to_monitor[i[:terrain]][:ray_current]
+ end
+ end
+ end
+ end
+
+ def impact_result body, impact
+ infinity_alias = 1000
+ r = {
+ body: {},
+ terrain: {},
+ impact: {}
+ }
+
+ r[:body][:line] = body.trajectory.dup
+ r[:body][:slope] = geometry.line_slope(body.trajectory, replace_infinity: infinity_alias)
+ r[:body][:slope_sign] = r[:body][:slope].sign
+ r[:body][:x] = body.x
+ r[:body][:y] = body.y
+ r[:body][:dy] = body.dy
+ r[:body][:dx] = body.dx
+
+ r[:terrain][:line] = impact[:terrain].dup
+ r[:terrain][:slope] = geometry.line_slope(impact[:terrain], replace_infinity: infinity_alias)
+ r[:terrain][:slope_sign] = r[:terrain][:slope].sign
+
+ r[:impact][:angle] = geometry.angle_between_lines(body.trajectory, impact[:terrain], replace_infinity: infinity_alias)
+ r[:impact][:point] = { x: impact[:point].x, y: impact[:point].y }
+ r[:impact][:same_slope_sign] = r[:body][:slope_sign] == r[:terrain][:slope_sign]
+ r[:impact][:ray] = body.ray_current
+ r[:body][:new_on_floor] = body.on_floor
+ r[:body][:new_floor] = r[:terrain][:line]
+
+ if r[:impact][:angle].abs < 90 && r[:terrain][:slope].abs < 3
+ play_sound
+ r[:body][:new_dy] = r[:body][:dy] * circle.elasticity * -1
+ r[:body][:new_dx] = r[:body][:dx] * circle.elasticity
+ r[:impact][:type] = :horizontal
+ r[:body][:new_reason] = "-"
+ elsif r[:impact][:angle].abs < 90 && r[:terrain][:slope].abs > 3
+ play_sound
+ r[:body][:new_dy] = r[:body][:dy] * 1.1
+ r[:body][:new_dx] = r[:body][:dx] * -circle.elasticity
+ r[:impact][:type] = :vertical
+ r[:body][:new_reason] = "|"
+ else
+ play_sound
+ r[:body][:new_dx] = r[:body][:dx] * -circle.elasticity
+ r[:body][:new_dy] = r[:body][:dy] * -circle.elasticity
+ r[:impact][:type] = :slanted
+ r[:body][:new_reason] = "/"
+ end
+
+ r[:impact][:energy] = r[:body][:new_dx].abs + r[:body][:new_dy].abs
+
+ if r[:impact][:energy] <= 0.3 && r[:terrain][:slope].abs < 4
+ r[:body][:new_dx] = 0
+ r[:body][:new_dy] = 0
+ r[:impact][:energy] = 0
+ r[:body][:new_on_floor] = true
+ r[:body][:new_floor] = r[:terrain][:line]
+ r[:body][:new_reason] = "0"
+ end
+
+ r[:impact][:ray_next] = geometry.ray_test({ x: r[:body][:x] - (r[:body][:dx] * 1.1) + r[:body][:new_dx],
+ y: r[:body][:y] - (r[:body][:dy] * 1.1) + r[:body][:new_dy] + state.gravity },
+ r[:terrain][:line])
+
+ if r[:impact][:ray_next] == r[:impact][:ray]
+ r[:body][:new_dx] *= -1
+ r[:body][:new_dy] *= -1
+ r[:body][:new_reason] = "clip"
+ end
+
+ r
+ end
+
+ def game_over!
+ circle.x = circle.check_point_x
+ circle.y = circle.check_point_y
+ circle.dx = 0
+ circle.dy = 0
+ circle.game_over_at = state.tick_count
+ end
+
+ def not_game_over!
+ impact_history_entry = impact_result circle, circle.impact
+ circle.impact_history << impact_history_entry
+ circle.x -= circle.dx * 1.1
+ circle.y -= circle.dy * 1.1
+ circle.dx = impact_history_entry[:body][:new_dx]
+ circle.dy = impact_history_entry[:body][:new_dy]
+ circle.on_floor = impact_history_entry[:body][:new_on_floor]
+
+ if circle.on_floor
+ circle.check_point_at = state.tick_count
+ circle.check_point_x = circle.x
+ circle.check_point_y = circle.y
+ end
+
+ circle.previous_floor = circle.floor || {}
+ circle.floor = impact_history_entry[:body][:new_floor] || {}
+ circle.floor_point = impact_history_entry[:impact][:point]
+ if circle.floor.slice(:x, :y, :x2, :y2) != circle.previous_floor.slice(:x, :y, :x2, :y2)
+ new_relative_x = if circle.dx > 0
+ :right
+ elsif circle.dx < 0
+ :left
+ else
+ nil
+ end
+
+ new_relative_y = if circle.dy > 0
+ :above
+ elsif circle.dy < 0
+ :below
+ else
+ nil
+ end
+
+ circle.floor_relative_x = new_relative_x
+ circle.floor_relative_y = new_relative_y
+ end
+
+ circle.impact = nil
+ circle.terrains_to_monitor.clear
+ end
+
+ def calc_physics
+ if args.state.god_mode
+ calc_potential_impacts
+ calc_terrains_to_monitor
+ return
+ end
+
+ if circle.y < -700
+ game_over
+ return
+ end
+
+ return if state.game_over
+ return if circle.on_floor
+ circle.previous_dy = circle.dy
+ circle.previous_dx = circle.dx
+ circle.x += circle.dx
+ circle.y += circle.dy
+ args.state.distance_traveled ||= 0
+ args.state.distance_traveled += circle.dx.abs + circle.dy.abs
+ circle.dy += state.gravity
+ calc_potential_impacts
+ calc_terrains_to_monitor
+ return unless circle.impact
+ if circle.impact && circle.impact[:type] == :lava
+ game_over!
+ else
+ not_game_over!
+ end
+ end
+
+ def input_god_mode
+ state.debug_mode = !state.debug_mode if inputs.keyboard.key_down.forward_slash
+
+ # toggle god mode
+ if inputs.keyboard.key_down.g
+ state.god_mode = !state.god_mode
+ state.potential_lift = 0
+ circle.floor = nil
+ circle.floor_point = nil
+ circle.floor_relative_x = nil
+ circle.floor_relative_y = nil
+ circle.impact = nil
+ circle.terrains_to_monitor.clear
+ return
+ end
+
+ return unless state.god_mode
+
+ circle.x = circle.x.to_i
+ circle.y = circle.y.to_i
+
+ # move god circle
+ if inputs.keyboard.left || inputs.keyboard.a
+ circle.x -= 20
+ elsif inputs.keyboard.right || inputs.keyboard.d || inputs.keyboard.f
+ circle.x += 20
+ end
+
+ if inputs.keyboard.up || inputs.keyboard.w
+ circle.y += 20
+ elsif inputs.keyboard.down || inputs.keyboard.s
+ circle.y -= 20
+ end
+
+ # delete terrain
+ if inputs.keyboard.key_down.x
+ calc_terrains_to_monitor
+ state.terrain = state.terrain.reject do |t|
+ t[:rect].intersect_rect? circle.rect
+ end
+
+ state.lava = state.lava.reject do |t|
+ t[:rect].intersect_rect? circle.rect
+ end
+
+ calc_potential_impacts
+ save_level
+ end
+
+ # change terrain type
+ if inputs.keyboard.key_down.l
+ if state.line_mode == :terrain
+ state.line_mode = :lava
+ else
+ state.line_mode = :terrain
+ end
+ end
+
+ if inputs.mouse.click && !state.point_one
+ state.point_one = inputs.mouse.click.point
+ elsif inputs.mouse.click && state.point_one
+ l = [*state.point_one, *inputs.mouse.click.point]
+ l = [l.x - state.camera.x,
+ l.y - state.camera.y,
+ l.x2 - state.camera.x,
+ l.y2 - state.camera.y].line.to_hash
+ l[:rect] = rect_for_line l
+ if state.line_mode == :terrain
+ state.terrain << l
+ else
+ state.lava << l
+ end
+ save_level
+ next_x = inputs.mouse.click.point.x - 640
+ next_y = inputs.mouse.click.point.y - 360
+ circle.x += next_x
+ circle.y += next_y
+ state.point_one = nil
+ elsif inputs.keyboard.one
+ state.point_one = [circle.x + camera.x, circle.y+ camera.y]
+ end
+
+ # cancel chain lines
+ if inputs.keyboard.key_down.nine || inputs.keyboard.key_down.escape || inputs.keyboard.key_up.six || inputs.keyboard.key_up.one
+ state.point_one = nil
+ end
+ end
+
+ def play_sound
+ return if state.sound_debounce > 0
+ state.sound_debounce = 5
+ outputs.sounds << "sounds/03#{"%02d" % state.sound_index}.wav"
+ state.sound_index += 1
+ if state.sound_index > 21
+ state.sound_index = 1
+ end
+ end
+
+ def input_game
+ if inputs.keyboard.down || inputs.keyboard.space
+ circle.potential_lift += 0.03
+ circle.potential_lift = circle.potential_lift.lesser(10)
+ elsif inputs.keyboard.key_up.down || inputs.keyboard.key_up.space
+ play_sound
+ circle.dy += circle.angle.vector_y circle.potential_lift
+ circle.dx += circle.angle.vector_x circle.potential_lift
+
+ if circle.on_floor
+ if circle.floor_relative_y == :above
+ circle.y += circle.potential_lift.abs * 2
+ elsif circle.floor_relative_y == :below
+ circle.y -= circle.potential_lift.abs * 2
+ end
+ end
+
+ circle.on_floor = false
+ circle.potential_lift = 0
+ circle.terrains_to_monitor.clear
+ circle.impact_history.clear
+ circle.impact = nil
+ calc_physics
+ end
+
+ # aim probe
+ if inputs.keyboard.right || inputs.keyboard.a
+ circle.angle -= 2
+ elsif inputs.keyboard.left || inputs.keyboard.d
+ circle.angle += 2
+ end
+ end
+
+ def input
+ input_god_mode
+ input_game
+ end
+
+ def calc_camera
+ state.camera.target_x = 640 - circle.x
+ state.camera.target_y = 360 - circle.y
+ xdiff = state.camera.target_x - state.camera.x
+ ydiff = state.camera.target_y - state.camera.y
+ state.camera.x += xdiff * camera.follow_speed
+ state.camera.y += ydiff * camera.follow_speed
+ end
+
+ def calc
+ state.sound_debounce ||= 0
+ state.sound_debounce -= 1
+ state.sound_debounce = 0 if state.sound_debounce < 0
+ if state.god_mode
+ circle.dy *= 0.1
+ circle.dx *= 0.1
+ end
+ calc_camera
+ state.whisp_queue ||= []
+ if state.tick_count.mod_zero?(4)
+ state.whisp_queue << {
+ x: -300,
+ y: 1400 * rand,
+ speed: 2.randomize(:ratio) + 3,
+ w: 20,
+ h: 20, path: 'sprites/whisp.png',
+ a: 0,
+ created_at: state.tick_count,
+ angle: 0,
+ r: 100,
+ g: 128 + 128 * rand,
+ b: 128 + 128 * rand
+ }
+ end
+
+ state.whisp_queue.each do |w|
+ w.x += w[:speed] * 2
+ w.x -= circle.dx * 0.3
+ w.y -= w[:speed]
+ w.y -= circle.dy * 0.3
+ w.angle += w[:speed]
+ w.a = w[:created_at].ease(30) * 255
+ end
+
+ state.whisp_queue = state.whisp_queue.reject { |w| w[:x] > 1280 }
+
+ if state.tick_count.mod_zero?(2) && (circle.dx != 0 || circle.dy != 0)
+ circle.after_images << {
+ x: circle.x,
+ y: circle.y,
+ w: circle.radius,
+ h: circle.radius,
+ a: 255,
+ created_at: state.tick_count
+ }
+ end
+
+ circle.after_images.each do |ai|
+ ai.a = ai[:created_at].ease(10, :flip) * 255
+ end
+
+ circle.after_images = circle.after_images.reject { |ai| ai[:created_at].elapsed_time > 10 }
+ calc_physics
+ end
+
+ def circle
+ state.circle
+ end
+
+ def camera
+ state.camera
+ end
+
+ def terrain
+ state.terrain
+ end
+
+ def lava
+ state.lava
+ end
+end
+
+# $gtk.reset
+
+def tick args
+ args.outputs.background_color = [0, 0, 0]
+ if args.inputs.keyboard.r
+ args.gtk.reset
+ return
+ end
+ # uncomment the line below to slow down the game so you
+ # can see each tick as it passes
+ # args.gtk.slowmo! 30
+ $game ||= FallingCircle.new
+ $game.args = args
+ $game.tick
+end
+
+def reset
+ $game = nil
+end