class Game
attr_gtk
def initialize
@level_scene = LevelScene.new
@shop_scene = ShopScene.new
end
def tick
defaults
current_scene.args = args
current_scene.tick
if state.next_scene
state.scene = state.next_scene
state.scene_at = state.tick_count
state.next_scene = nil
end
end
def current_scene
if state.scene == :level
@level_scene
elsif state.scene == :shop
@shop_scene
end
end
def defaults
state.shield ||= 10
state.assembly_points ||= 4
state.scene ||= :level
state.bullets ||= []
state.enemies ||= []
state.bullet_speed ||= 5
state.turret_position ||= { x: 640, y: 0 }
state.blaster_spread ||= 1
state.blaster_rate ||= 60
state.level ||= 1
state.bullet_damage ||= 1
state.enemy_spawn_rate ||= 120
state.enemy_min_health ||= 1
state.enemy_health_range ||= 2
state.enemies_to_spawn ||= 5
state.enemies_spawned ||= 0
state.enemy_dy ||= -0.2
end
end
class ShopScene
attr_gtk
def activate
state.module_selected = nil
state.available_module_1 = :blaster_spread
state.available_module_2 = :bullet_damage
state.available_module_3 = if state.blaster_rate > 3
:blaster_rate
else
nil
end
end
def tick
if state.scene_at == state.tick_count - 1
activate
end
state.next_wave_button ||= layout.rect(row: 0, col: 20, w: 4, h: 2)
state.module_1_button ||= layout.rect(row: 10, col: 0, w: 8, h: 2)
state.module_2_button ||= layout.rect(row: 10, col: 8, w: 8, h: 2)
state.module_3_button ||= layout.rect(row: 10, col: 16, w: 8, h: 2)
calc
render
end
def increase_difficulty_and_start_level
state.next_scene = :level
state.enemies_spawned = 0
state.enemies = []
state.level += 1
state.enemy_spawn_rate = (state.enemy_spawn_rate * 0.95).to_i
state.enemy_min_health = (state.enemy_min_health * 1.1).to_i + 1
state.enemy_health_range = state.enemy_min_health * 2
state.enemies_to_spawn = (state.enemies_to_spawn * 1.1).to_i + 2
state.enemy_dy *= 1.05
end
def calc
if state.module_selected
if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.next_wave_button)
increase_difficulty_and_start_level
end
else
if inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.module_1_button)
perform_upgrade state.available_module_1
state.available_module_1 = nil
state.module_selected = true
elsif inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.module_2_button)
perform_upgrade state.available_module_2
state.available_module_2 = nil
state.module_selected = true
elsif inputs.mouse.click && inputs.mouse.click.point.inside_rect?(state.module_3_button)
perform_upgrade state.available_module_3
state.available_module_3 = nil
state.module_selected = true
end
end
end
def perform_upgrade module_name
return if state.module_selected
if module_name == :bullet_damage
state.bullet_damage += 1
elsif module_name == :blaster_rate
state.blaster_rate = (state.blaster_rate * 0.85).to_i
state.blaster_rate = 3 if state.blaster_rate < 3
elsif module_name == :blaster_spread
state.blaster_spread += 2
else
raise "perform_upgade: Unknown module: #{module_name}"
end
end
def render
outputs.background_color = [0, 0, 0]
# outputs.primitives << layout.debug_primitives.map { |p| p.merge a: 80 }
outputs.labels << layout.rect(row: 0, col: 11, w: 2, h: 1)
.center
.merge(text: "Select Upgrade", anchor_x: 0.5, anchor_y: 0.5, size_px: 50, r: 255, g: 255, b: 255)
if state.module_selected
outputs.primitives << button_prefab(state.next_wave_button, "Next Wave", a: 255)
end
a = if state.module_selected
80
else
255
end
outputs.primitives << button_prefab(state.module_1_button, state.available_module_1, a: a)
outputs.primitives << button_prefab(state.module_2_button, state.available_module_2, a: a)
outputs.primitives << button_prefab(state.module_3_button, state.available_module_3, a: a)
end
def button_prefab rect, text, a: 255
return nil if !text
[
rect.merge(path: :solid, r: 255, g: 255, b: 255, a: a),
geometry.center(rect).merge(text: text.gsub("_", " "), anchor_x: 0.5, anchor_y: 0.5, r: 0, g: 0, b: 0, size_px: rect.h.idiv(4))
]
end
end
class LevelScene
attr_gtk
def tick
if inputs.keyboard.key_down.g
state.enemies_spawned = state.enemies_to_spawn
state.enemies = []
elsif inputs.keyboard.key_down.forward_slash
roll = rand
if roll < 0.33
state.bullet_damage += 1
$gtk.notify_extended! message: "bullet damage increased: #{state.bullet_damage}", env: :prod
elsif roll < 0.66
if state.blaster_rate > 3
state.blaster_rate = (state.blaster_rate * 0.85).to_i
state.blaster_rate = 3 if state.blaster_rate < 3
$gtk.notify_extended! message: "blaster rate upgraded: #{state.blaster_rate}", env: :prod
else
$gtk.notify_extended! message: "blaster rate already at fastest.", env: :prod
end
else
state.blaster_spread += 2
$gtk.notify_extended! message: "blaster spread increased: #{state.blaster_spread}", env: :prod
end
end
calc
render
end
def calc
calc_bullets
calc_enemies
calc_bullet_hits
calc_enemy_push_back
calc_deaths
end
def calc_deaths
state.enemies.reject! { |e| e.hp <= 0 }
state.bullets.reject! { |b| b.dead_at }
end
def enemy_prefab enemy
b = (enemy.hp / (state.enemy_min_health + state.enemy_health_range)) * 255
[
enemy.merge(path: :solid, r: 128, g: 0, b: b),
geometry.center(enemy).merge(text: enemy.hp, anchor_x: 0.5, anchor_y: 0.5, r: 255, g: 255, b: 255, size_px: enemy.h * 0.5)
]
end
def render
outputs.background_color = [0, 0, 0]
level_completion_perc = (state.enemies_spawned - state.enemies.length).fdiv(state.enemies_to_spawn)
outputs.primitives << { x: 30, y: 30.from_top, text: "Wave: #{state.level} (#{(level_completion_perc * 100).to_i}% complete)", r: 255, g: 255, b: 255 }
outputs.primitives << { x: 30, y: 60.from_top, text: "Press G to skip to end of the current wave.", r: 255, g: 255, b: 255 }
outputs.primitives << { x: 30, y: 90.from_top, text: "Press / to get a random upgrade immediately.", r: 255, g: 255, b: 255 }
outputs.sprites << state.bullets.map do |b|
b.merge w: 10, h: 10, path: :solid, r: 0, g: 255, b: 255
end
outputs.primitives << state.enemies.map { |e| enemy_prefab e }
end
def calc_bullets
if state.tick_count.zmod? state.blaster_rate
bullet_count = state.blaster_spread
min_degrees = state.blaster_spread.idiv(2) * -2
bullet_count.times do |i|
degree_offset = min_degrees + (i * 2)
state.bullets << { x: 640,
y: 0,
dy: (attack_angle + degree_offset).vector_y * state.bullet_speed,
dx: (attack_angle + degree_offset).vector_x * state.bullet_speed }
end
end
state.bullets.each do |b|
b.x += b.dx
b.y += b.dy
end
state.bullets.reject! { |b| b.y < 0 || b.y > 720 || b.x > 1280 || b.x < 0 }
end
def calc_enemies
if state.tick_count.zmod?(state.enemy_spawn_rate) && state.enemies_spawned < state.enemies_to_spawn
state.enemies_spawned += 1
x = rand(1280 - 96) + 48
y = 720
hp = state.enemy_min_health + rand(state.enemy_health_range)
state.enemies << { x: x,
y: y,
w: 48,
h: 48,
push_back_x: 0,
push_back_y: 0,
spawn_at: state.tick_count,
dy: state.enemy_dy,
start_hp: hp,
hp: hp }
end
state.enemies.each do |e|
if e.y + e.h > 720
e.y -= (((e.y + e.h) - 720) / e.h) * 10
end
e.y += e.dy
if e.x < 0 && e.push_back_x < 0
e.push_back_x = e.push_back_x.abs
elsif (e.x + e.w) > 1280 && e.push_back_x > 0
e.push_back_x = e.push_back_x.abs * -1
end
e.x += e.push_back_x
e.y += e.push_back_y
e.push_back_x *= 0.9
e.push_back_y *= 0.9
end
state.enemies.reject! { |e| e.y < 0 }
if state.enemies.empty? && state.enemies_spawned >= state.enemies_to_spawn
state.next_scene = :shop
state.bullets.clear
end
end
def calc_bullet_hits
state.bullets.each do |b|
state.enemies.each do |e|
if geometry.intersect_rect? b.merge(w: 4, h: 4, anchor_x: 0.5, anchor_x: 0.5), e
e.hp -= state.bullet_damage
push_back_angle = geometry.angle b, geometry.center(e)
push_back_x = push_back_angle.vector_x * state.bullet_damage * 0.1
push_back_y = push_back_angle.vector_y * state.bullet_damage * 0.1
e.push_back_x += push_back_x
e.push_back_y += push_back_y
e.hit_at = state.tick_count
b.dead_at = state.tick_count
end
end
end
end
def calc_enemy_push_back
state.enemies.sort_by { |e| -e.y }.each do |e|
has_pushed_back = false
other_enemies = geometry.find_all_intersect_rect e, state.enemies
other_enemies.each do |e2|
next if e == e2
push_back_angle = geometry.angle geometry.center(e), geometry.center(e2)
e2.push_back_x += (e.push_back_x).fdiv(other_enemies.length) * 0.7
e2.push_back_y += (e.push_back_y).fdiv(other_enemies.length) * 0.7
has_pushed_back = true
end
if has_pushed_back
e.push_back_x *= 0.2
e.push_back_y *= 0.2
end
end
end
def attack_angle
geometry.angle state.turret_position, inputs.mouse
end
end
def tick args
$game ||= Game.new
$game.args = args
$game.tick
end
def reset
$game = nil
end
$gtk.reset