tank_island 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +58 -0
- data/Rakefile +2 -0
- data/bin/tank_island +30 -0
- data/lib/entities/box.rb +28 -0
- data/lib/entities/bullet.rb +26 -0
- data/lib/entities/camera.rb +113 -0
- data/lib/entities/components/ai/gun.rb +114 -0
- data/lib/entities/components/ai/tank_chasing_state.rb +30 -0
- data/lib/entities/components/ai/tank_fighting_state.rb +47 -0
- data/lib/entities/components/ai/tank_fleeing_state.rb +50 -0
- data/lib/entities/components/ai/tank_motion_fsm.rb +102 -0
- data/lib/entities/components/ai/tank_motion_state.rb +84 -0
- data/lib/entities/components/ai/tank_navigating_state.rb +34 -0
- data/lib/entities/components/ai/tank_roaming_state.rb +83 -0
- data/lib/entities/components/ai/tank_stuck_state.rb +45 -0
- data/lib/entities/components/ai/vision.rb +109 -0
- data/lib/entities/components/ai_input.rb +70 -0
- data/lib/entities/components/box_graphics.rb +39 -0
- data/lib/entities/components/bullet_graphics.rb +13 -0
- data/lib/entities/components/bullet_physics.rb +65 -0
- data/lib/entities/components/bullet_sounds.rb +15 -0
- data/lib/entities/components/component.rb +32 -0
- data/lib/entities/components/damage_graphics.rb +20 -0
- data/lib/entities/components/explosion_graphics.rb +43 -0
- data/lib/entities/components/explosion_sounds.rb +16 -0
- data/lib/entities/components/health.rb +87 -0
- data/lib/entities/components/player_input.rb +100 -0
- data/lib/entities/components/player_sounds.rb +16 -0
- data/lib/entities/components/powerup_graphics.rb +22 -0
- data/lib/entities/components/powerup_sounds.rb +15 -0
- data/lib/entities/components/tank_graphics.rb +46 -0
- data/lib/entities/components/tank_health.rb +32 -0
- data/lib/entities/components/tank_physics.rb +179 -0
- data/lib/entities/components/tank_sounds.rb +43 -0
- data/lib/entities/components/tree_graphics.rb +69 -0
- data/lib/entities/damage.rb +26 -0
- data/lib/entities/explosion.rb +34 -0
- data/lib/entities/game_object.rb +54 -0
- data/lib/entities/hud.rb +79 -0
- data/lib/entities/map.rb +183 -0
- data/lib/entities/object_pool.rb +59 -0
- data/lib/entities/powerups/fire_rate_powerup.rb +14 -0
- data/lib/entities/powerups/health_powerup.rb +12 -0
- data/lib/entities/powerups/powerup.rb +35 -0
- data/lib/entities/powerups/powerup_respawn_queue.rb +23 -0
- data/lib/entities/powerups/repair_powerup.rb +14 -0
- data/lib/entities/powerups/tank_speed_powerup.rb +14 -0
- data/lib/entities/radar.rb +62 -0
- data/lib/entities/score_display.rb +35 -0
- data/lib/entities/tank.rb +64 -0
- data/lib/entities/tree.rb +18 -0
- data/lib/game_states/demo_state.rb +49 -0
- data/lib/game_states/game_state.rb +27 -0
- data/lib/game_states/menu_state.rb +60 -0
- data/lib/game_states/pause_state.rb +61 -0
- data/lib/game_states/play_state.rb +119 -0
- data/lib/misc/axis_aligned_bounding_box.rb +33 -0
- data/lib/misc/game_window.rb +30 -0
- data/lib/misc/names.rb +13 -0
- data/lib/misc/quad_tree.rb +91 -0
- data/lib/misc/stats.rb +55 -0
- data/lib/misc/stereo_sample.rb +96 -0
- data/lib/misc/utils.rb +145 -0
- data/media/armalite_rifle.ttf +0 -0
- data/media/boxes_barrels.json +60 -0
- data/media/boxes_barrels.png +0 -0
- data/media/bullet.png +0 -0
- data/media/c_dot.png +0 -0
- data/media/country_field.png +0 -0
- data/media/crash.ogg +0 -0
- data/media/damage1.png +0 -0
- data/media/damage2.png +0 -0
- data/media/damage3.png +0 -0
- data/media/damage4.png +0 -0
- data/media/decor.json +516 -0
- data/media/decor.png +0 -0
- data/media/decor.psd +0 -0
- data/media/explosion.mp3 +0 -0
- data/media/explosion.png +0 -0
- data/media/fire.mp3 +0 -0
- data/media/ground.json +492 -0
- data/media/ground.png +0 -0
- data/media/ground_units.json +900 -0
- data/media/ground_units.png +0 -0
- data/media/menu_music.mp3 +0 -0
- data/media/metal_interaction2.wav +0 -0
- data/media/names.txt +279 -0
- data/media/pickups.json +68 -0
- data/media/pickups.png +0 -0
- data/media/powerup.mp3 +0 -0
- data/media/respawn.wav +0 -0
- data/media/tank_driving.mp3 +0 -0
- data/media/top_secret.ttf +0 -0
- data/media/trees.png +0 -0
- data/media/trees_packed.json +388 -0
- data/media/trees_packed.png +0 -0
- data/media/water.png +0 -0
- data/spec/misc/aabb_spec.rb +85 -0
- data/spec/misc/quad_tree_spec.rb +137 -0
- data/tank_island.gemspec +29 -0
- metadata +223 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
class ObjectPool
|
2
|
+
attr_accessor :map, :camera, :objects, :powerup_respawn_queue
|
3
|
+
|
4
|
+
def size
|
5
|
+
@objects.size
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(box)
|
9
|
+
@tree = QuadTree.new(box)
|
10
|
+
@powerup_respawn_queue = PowerupRespawnQueue.new
|
11
|
+
@objects = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(object)
|
15
|
+
@objects << object
|
16
|
+
@tree.insert(object)
|
17
|
+
end
|
18
|
+
|
19
|
+
def tree_remove(object)
|
20
|
+
@tree.remove(object)
|
21
|
+
end
|
22
|
+
|
23
|
+
def tree_insert(object)
|
24
|
+
@tree.insert(object)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_all
|
28
|
+
@objects.each(&:update)
|
29
|
+
@objects.reject! do |o|
|
30
|
+
if o.removable?
|
31
|
+
@tree.remove(o)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@powerup_respawn_queue.respawn(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def nearby_point(cx, cy, max_distance, object = nil)
|
39
|
+
hx, hy = cx + max_distance, cy + max_distance
|
40
|
+
# Fast, rough results
|
41
|
+
results = @tree.query_range(
|
42
|
+
AxisAlignedBoundingBox.new([cx, cy], [hx, hy]))
|
43
|
+
# Sift through to select fine-grained results
|
44
|
+
results.select do |o|
|
45
|
+
o != object &&
|
46
|
+
Utils.distance_between(
|
47
|
+
o.x, o.y, cx, cy) <= max_distance
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def nearby(object, max_distance)
|
52
|
+
cx, cy = object.location
|
53
|
+
nearby_point(cx, cy, max_distance, object)
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_range(box)
|
57
|
+
@tree.query_range(box)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Powerup < GameObject
|
2
|
+
def initialize(object_pool, x, y)
|
3
|
+
super
|
4
|
+
PowerupGraphics.new(self, graphics)
|
5
|
+
end
|
6
|
+
|
7
|
+
def box
|
8
|
+
[x - 8, y - 8,
|
9
|
+
x + 8, y - 8,
|
10
|
+
x + 8, y + 8,
|
11
|
+
x - 8, y + 8]
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_collision(object)
|
15
|
+
if pickup(object)
|
16
|
+
PowerupSounds.play(object, object_pool.camera)
|
17
|
+
remove
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def pickup(object)
|
22
|
+
# override and implement application
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove
|
26
|
+
object_pool.powerup_respawn_queue.enqueue(
|
27
|
+
respawn_delay,
|
28
|
+
self.class, x, y)
|
29
|
+
mark_for_removal
|
30
|
+
end
|
31
|
+
|
32
|
+
def respawn_delay
|
33
|
+
30
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class PowerupRespawnQueue
|
2
|
+
RESPAWN_DELAY = 1000
|
3
|
+
def initialize
|
4
|
+
@respawn_queue = {}
|
5
|
+
@last_respawn = Gosu.milliseconds
|
6
|
+
end
|
7
|
+
|
8
|
+
def enqueue(delay_seconds, type, x, y)
|
9
|
+
respawn_at = Gosu.milliseconds + delay_seconds * 1000
|
10
|
+
@respawn_queue[respawn_at.to_i] = [type, x, y]
|
11
|
+
end
|
12
|
+
|
13
|
+
def respawn(object_pool)
|
14
|
+
now = Gosu.milliseconds
|
15
|
+
return if now - @last_respawn < RESPAWN_DELAY
|
16
|
+
@respawn_queue.keys.each do |k|
|
17
|
+
next if k > now # not yet
|
18
|
+
type, x, y = @respawn_queue.delete(k)
|
19
|
+
type.new(object_pool, x, y)
|
20
|
+
end
|
21
|
+
@last_respawn = now
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Radar
|
2
|
+
UPDATE_FREQUENCY = 1000
|
3
|
+
WIDTH = 150
|
4
|
+
HEIGHT = 100
|
5
|
+
PADDING = 10
|
6
|
+
# Black with 33% transparency
|
7
|
+
BACKGROUND = Gosu::Color.new(255 * 0.33, 0, 0, 0)
|
8
|
+
attr_accessor :target
|
9
|
+
|
10
|
+
def initialize(object_pool, target)
|
11
|
+
@object_pool = object_pool
|
12
|
+
@target = target
|
13
|
+
@last_update = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
if Gosu.milliseconds - @last_update > UPDATE_FREQUENCY
|
18
|
+
@nearby = nil
|
19
|
+
end
|
20
|
+
@nearby ||= @object_pool.nearby(@target, 2000).select do |o|
|
21
|
+
o.class == Tank && !o.health.dead?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw
|
26
|
+
x1, x2, y1, y2 = radar_coords
|
27
|
+
$window.draw_quad(
|
28
|
+
x1, y1, BACKGROUND,
|
29
|
+
x2, y1, BACKGROUND,
|
30
|
+
x2, y2, BACKGROUND,
|
31
|
+
x1, y2, BACKGROUND,
|
32
|
+
200)
|
33
|
+
draw_tank(@target, Gosu::Color::GREEN)
|
34
|
+
@nearby && @nearby.each do |t|
|
35
|
+
draw_tank(t, Gosu::Color::RED)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def draw_tank(tank, color)
|
42
|
+
x1, x2, y1, y2 = radar_coords
|
43
|
+
tx = x1 + WIDTH / 2 + (tank.x - @target.x) / 20
|
44
|
+
ty = y1 + HEIGHT / 2 + (tank.y - @target.y) / 20
|
45
|
+
if (x1..x2).include?(tx) && (y1..y2).include?(ty)
|
46
|
+
$window.draw_quad(
|
47
|
+
tx - 2, ty - 2, color,
|
48
|
+
tx + 2, ty - 2, color,
|
49
|
+
tx + 2, ty + 2, color,
|
50
|
+
tx - 2, ty + 2, color,
|
51
|
+
300)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def radar_coords
|
56
|
+
x1 = $window.width - WIDTH - PADDING
|
57
|
+
x2 = $window.width - PADDING
|
58
|
+
y1 = $window.height - HEIGHT - PADDING
|
59
|
+
y2 = $window.height - PADDING
|
60
|
+
[x1, x2, y1, y2]
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ScoreDisplay
|
2
|
+
def initialize(object_pool, font_size=30)
|
3
|
+
@font_size = font_size
|
4
|
+
tanks = object_pool.objects.select do |o|
|
5
|
+
o.class == Tank
|
6
|
+
end
|
7
|
+
stats = tanks.map(&:input).map(&:stats)
|
8
|
+
stats.sort! do |stat1, stat2|
|
9
|
+
stat2.kills <=> stat1.kills
|
10
|
+
end
|
11
|
+
create_stats_image(stats)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_stats_image(stats)
|
15
|
+
text = stats.map do |stat|
|
16
|
+
"#{stat.kills}: #{stat.name} "
|
17
|
+
end.join("\n")
|
18
|
+
@stats_image = Gosu::Image.from_text(
|
19
|
+
$window, text, Utils.main_font, @font_size)
|
20
|
+
end
|
21
|
+
|
22
|
+
def draw
|
23
|
+
@stats_image.draw(
|
24
|
+
$window.width / 2 - @stats_image.width / 2,
|
25
|
+
$window.height / 4 + 30,
|
26
|
+
1000)
|
27
|
+
end
|
28
|
+
|
29
|
+
def draw_top_right
|
30
|
+
@stats_image.draw(
|
31
|
+
$window.width - @stats_image.width - 20,
|
32
|
+
20,
|
33
|
+
1000)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Tank < GameObject
|
2
|
+
SHOOT_DELAY = 500
|
3
|
+
attr_accessor :throttle_down, :direction,
|
4
|
+
:gun_angle, :sounds, :physics, :graphics, :health, :input,
|
5
|
+
:fire_rate_modifier, :speed_modifier
|
6
|
+
|
7
|
+
def initialize(object_pool, input)
|
8
|
+
x, y = object_pool.map.spawn_point
|
9
|
+
super(object_pool, x, y)
|
10
|
+
@input = input
|
11
|
+
@input.control(self)
|
12
|
+
@physics = TankPhysics.new(self, object_pool)
|
13
|
+
@sounds = TankSounds.new(self, object_pool)
|
14
|
+
@health = TankHealth.new(self, object_pool)
|
15
|
+
@graphics = TankGraphics.new(self)
|
16
|
+
@direction = rand(0..7) * 45
|
17
|
+
@gun_angle = rand(0..360)
|
18
|
+
reset_modifiers
|
19
|
+
end
|
20
|
+
|
21
|
+
def box
|
22
|
+
@physics.box
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_collision(object)
|
26
|
+
return unless object
|
27
|
+
# Avoid recursion
|
28
|
+
if object.class == Tank
|
29
|
+
# Inform AI about hit
|
30
|
+
object.input.on_collision(object)
|
31
|
+
else
|
32
|
+
# Call only on non-tanks to avoid recursion
|
33
|
+
object.on_collision(self)
|
34
|
+
end
|
35
|
+
# Bullets should not slow Tanks down
|
36
|
+
if object.class != Bullet
|
37
|
+
@sounds.collide if @physics.speed > 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def shoot(target_x, target_y)
|
42
|
+
if can_shoot?
|
43
|
+
@last_shot = Gosu.milliseconds
|
44
|
+
Bullet.new(object_pool, @x, @y, target_x, target_y).fire(
|
45
|
+
self, 1500)
|
46
|
+
input.stats.add_shot
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def can_shoot?
|
51
|
+
Gosu.milliseconds - (@last_shot || 0) >
|
52
|
+
(SHOOT_DELAY / @fire_rate_modifier)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_modifiers
|
56
|
+
@fire_rate_modifier = 1
|
57
|
+
@speed_modifier = 1
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
"Tank [#{@health.health}@#{@x}:#{@y}@#{@physics.speed.round(2)}px/tick]#{@input.stats}"
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Tree < GameObject
|
2
|
+
attr_reader :health, :graphics
|
3
|
+
|
4
|
+
def initialize(object_pool, x, y, seed)
|
5
|
+
super(object_pool, x, y)
|
6
|
+
@graphics = TreeGraphics.new(self, seed)
|
7
|
+
@health = Health.new(self, object_pool, 30, false)
|
8
|
+
@angle = rand(-15..15)
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_collision(object)
|
12
|
+
@graphics.shake(object.direction)
|
13
|
+
end
|
14
|
+
|
15
|
+
def box
|
16
|
+
[@x, @y]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class DemoState < PlayState
|
2
|
+
attr_accessor :tank
|
3
|
+
|
4
|
+
def enter
|
5
|
+
# Prevent reactivating HUD
|
6
|
+
end
|
7
|
+
|
8
|
+
def update
|
9
|
+
super
|
10
|
+
@score_display = ScoreDisplay.new(
|
11
|
+
object_pool, 20)
|
12
|
+
end
|
13
|
+
|
14
|
+
def draw
|
15
|
+
super
|
16
|
+
@score_display.draw_top_right
|
17
|
+
end
|
18
|
+
|
19
|
+
def button_down(id)
|
20
|
+
super
|
21
|
+
if id == Gosu::KbSpace
|
22
|
+
target_tank = @tanks.reject do |t|
|
23
|
+
t == @camera.target
|
24
|
+
end.sample
|
25
|
+
switch_to_tank(target_tank)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def create_tanks(amount)
|
32
|
+
@map.spawn_points(amount * 3)
|
33
|
+
@tanks = []
|
34
|
+
amount.times do |i|
|
35
|
+
@tanks << Tank.new(@object_pool, AiInput.new(
|
36
|
+
@names.random, @object_pool))
|
37
|
+
end
|
38
|
+
target_tank = @tanks.sample
|
39
|
+
@hud = HUD.new(@object_pool, target_tank)
|
40
|
+
@hud.active = false
|
41
|
+
switch_to_tank(target_tank)
|
42
|
+
end
|
43
|
+
|
44
|
+
def switch_to_tank(tank)
|
45
|
+
@camera.target = tank
|
46
|
+
@hud.player = tank
|
47
|
+
self.tank = tank
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class GameState
|
2
|
+
|
3
|
+
def self.switch(new_state)
|
4
|
+
$window.state && $window.state.leave
|
5
|
+
$window.state = new_state
|
6
|
+
new_state.enter
|
7
|
+
end
|
8
|
+
|
9
|
+
def enter
|
10
|
+
end
|
11
|
+
|
12
|
+
def leave
|
13
|
+
end
|
14
|
+
|
15
|
+
def draw
|
16
|
+
end
|
17
|
+
|
18
|
+
def update
|
19
|
+
end
|
20
|
+
|
21
|
+
def needs_redraw?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def button_down(id)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
class MenuState < GameState
|
3
|
+
include Singleton
|
4
|
+
attr_accessor :play_state
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@message = Gosu::Image.from_text(
|
8
|
+
$window, "Tank Island",
|
9
|
+
Utils.title_font, 60)
|
10
|
+
end
|
11
|
+
|
12
|
+
def enter
|
13
|
+
music.play(true)
|
14
|
+
music.volume = 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def leave
|
18
|
+
music.volume = 0
|
19
|
+
music.stop
|
20
|
+
end
|
21
|
+
|
22
|
+
def music
|
23
|
+
@@music ||= Gosu::Song.new(
|
24
|
+
$window, Utils.media_path('menu_music.mp3'))
|
25
|
+
end
|
26
|
+
|
27
|
+
def update
|
28
|
+
text = "Q: Quit\nN: New Game\nD: Demo"
|
29
|
+
text << "\nC: Continue" if @play_state
|
30
|
+
@info = Gosu::Image.from_text(
|
31
|
+
$window, text,
|
32
|
+
Utils.main_font, 30)
|
33
|
+
end
|
34
|
+
|
35
|
+
def draw
|
36
|
+
@message.draw(
|
37
|
+
$window.width / 2 - @message.width / 2,
|
38
|
+
$window.height / 2 - @message.height / 2,
|
39
|
+
10)
|
40
|
+
@info.draw(
|
41
|
+
$window.width / 2 - @info.width / 2,
|
42
|
+
$window.height / 2 - @info.height / 2 + 100,
|
43
|
+
10)
|
44
|
+
end
|
45
|
+
|
46
|
+
def button_down(id)
|
47
|
+
$window.close if id == Gosu::KbQ
|
48
|
+
if id == Gosu::KbC && @play_state
|
49
|
+
GameState.switch(@play_state)
|
50
|
+
end
|
51
|
+
if id == Gosu::KbN
|
52
|
+
@play_state = PlayState.new
|
53
|
+
GameState.switch(@play_state)
|
54
|
+
end
|
55
|
+
if id == Gosu::KbD
|
56
|
+
@play_state = DemoState.new
|
57
|
+
GameState.switch(@play_state)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|