tank_island 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +58 -0
  7. data/Rakefile +2 -0
  8. data/bin/tank_island +30 -0
  9. data/lib/entities/box.rb +28 -0
  10. data/lib/entities/bullet.rb +26 -0
  11. data/lib/entities/camera.rb +113 -0
  12. data/lib/entities/components/ai/gun.rb +114 -0
  13. data/lib/entities/components/ai/tank_chasing_state.rb +30 -0
  14. data/lib/entities/components/ai/tank_fighting_state.rb +47 -0
  15. data/lib/entities/components/ai/tank_fleeing_state.rb +50 -0
  16. data/lib/entities/components/ai/tank_motion_fsm.rb +102 -0
  17. data/lib/entities/components/ai/tank_motion_state.rb +84 -0
  18. data/lib/entities/components/ai/tank_navigating_state.rb +34 -0
  19. data/lib/entities/components/ai/tank_roaming_state.rb +83 -0
  20. data/lib/entities/components/ai/tank_stuck_state.rb +45 -0
  21. data/lib/entities/components/ai/vision.rb +109 -0
  22. data/lib/entities/components/ai_input.rb +70 -0
  23. data/lib/entities/components/box_graphics.rb +39 -0
  24. data/lib/entities/components/bullet_graphics.rb +13 -0
  25. data/lib/entities/components/bullet_physics.rb +65 -0
  26. data/lib/entities/components/bullet_sounds.rb +15 -0
  27. data/lib/entities/components/component.rb +32 -0
  28. data/lib/entities/components/damage_graphics.rb +20 -0
  29. data/lib/entities/components/explosion_graphics.rb +43 -0
  30. data/lib/entities/components/explosion_sounds.rb +16 -0
  31. data/lib/entities/components/health.rb +87 -0
  32. data/lib/entities/components/player_input.rb +100 -0
  33. data/lib/entities/components/player_sounds.rb +16 -0
  34. data/lib/entities/components/powerup_graphics.rb +22 -0
  35. data/lib/entities/components/powerup_sounds.rb +15 -0
  36. data/lib/entities/components/tank_graphics.rb +46 -0
  37. data/lib/entities/components/tank_health.rb +32 -0
  38. data/lib/entities/components/tank_physics.rb +179 -0
  39. data/lib/entities/components/tank_sounds.rb +43 -0
  40. data/lib/entities/components/tree_graphics.rb +69 -0
  41. data/lib/entities/damage.rb +26 -0
  42. data/lib/entities/explosion.rb +34 -0
  43. data/lib/entities/game_object.rb +54 -0
  44. data/lib/entities/hud.rb +79 -0
  45. data/lib/entities/map.rb +183 -0
  46. data/lib/entities/object_pool.rb +59 -0
  47. data/lib/entities/powerups/fire_rate_powerup.rb +14 -0
  48. data/lib/entities/powerups/health_powerup.rb +12 -0
  49. data/lib/entities/powerups/powerup.rb +35 -0
  50. data/lib/entities/powerups/powerup_respawn_queue.rb +23 -0
  51. data/lib/entities/powerups/repair_powerup.rb +14 -0
  52. data/lib/entities/powerups/tank_speed_powerup.rb +14 -0
  53. data/lib/entities/radar.rb +62 -0
  54. data/lib/entities/score_display.rb +35 -0
  55. data/lib/entities/tank.rb +64 -0
  56. data/lib/entities/tree.rb +18 -0
  57. data/lib/game_states/demo_state.rb +49 -0
  58. data/lib/game_states/game_state.rb +27 -0
  59. data/lib/game_states/menu_state.rb +60 -0
  60. data/lib/game_states/pause_state.rb +61 -0
  61. data/lib/game_states/play_state.rb +119 -0
  62. data/lib/misc/axis_aligned_bounding_box.rb +33 -0
  63. data/lib/misc/game_window.rb +30 -0
  64. data/lib/misc/names.rb +13 -0
  65. data/lib/misc/quad_tree.rb +91 -0
  66. data/lib/misc/stats.rb +55 -0
  67. data/lib/misc/stereo_sample.rb +96 -0
  68. data/lib/misc/utils.rb +145 -0
  69. data/media/armalite_rifle.ttf +0 -0
  70. data/media/boxes_barrels.json +60 -0
  71. data/media/boxes_barrels.png +0 -0
  72. data/media/bullet.png +0 -0
  73. data/media/c_dot.png +0 -0
  74. data/media/country_field.png +0 -0
  75. data/media/crash.ogg +0 -0
  76. data/media/damage1.png +0 -0
  77. data/media/damage2.png +0 -0
  78. data/media/damage3.png +0 -0
  79. data/media/damage4.png +0 -0
  80. data/media/decor.json +516 -0
  81. data/media/decor.png +0 -0
  82. data/media/decor.psd +0 -0
  83. data/media/explosion.mp3 +0 -0
  84. data/media/explosion.png +0 -0
  85. data/media/fire.mp3 +0 -0
  86. data/media/ground.json +492 -0
  87. data/media/ground.png +0 -0
  88. data/media/ground_units.json +900 -0
  89. data/media/ground_units.png +0 -0
  90. data/media/menu_music.mp3 +0 -0
  91. data/media/metal_interaction2.wav +0 -0
  92. data/media/names.txt +279 -0
  93. data/media/pickups.json +68 -0
  94. data/media/pickups.png +0 -0
  95. data/media/powerup.mp3 +0 -0
  96. data/media/respawn.wav +0 -0
  97. data/media/tank_driving.mp3 +0 -0
  98. data/media/top_secret.ttf +0 -0
  99. data/media/trees.png +0 -0
  100. data/media/trees_packed.json +388 -0
  101. data/media/trees_packed.png +0 -0
  102. data/media/water.png +0 -0
  103. data/spec/misc/aabb_spec.rb +85 -0
  104. data/spec/misc/quad_tree_spec.rb +137 -0
  105. data/tank_island.gemspec +29 -0
  106. 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,14 @@
1
+ class FireRatePowerup < Powerup
2
+ def pickup(object)
3
+ if object.class == Tank
4
+ if object.fire_rate_modifier < 2
5
+ object.fire_rate_modifier += 0.25
6
+ end
7
+ true
8
+ end
9
+ end
10
+
11
+ def graphics
12
+ :straight_gun
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ class HealthPowerup < Powerup
2
+ def pickup(object)
3
+ if object.class == Tank
4
+ object.health.increase(25)
5
+ true
6
+ end
7
+ end
8
+
9
+ def graphics
10
+ :life_up
11
+ end
12
+ 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,14 @@
1
+ class RepairPowerup < Powerup
2
+ def pickup(object)
3
+ if object.class == Tank
4
+ if object.health.health < 100
5
+ object.health.restore
6
+ end
7
+ true
8
+ end
9
+ end
10
+
11
+ def graphics
12
+ :repair
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ class TankSpeedPowerup < Powerup
2
+ def pickup(object)
3
+ if object.class == Tank
4
+ if object.speed_modifier < 1.5
5
+ object.speed_modifier += 0.10
6
+ end
7
+ true
8
+ end
9
+ end
10
+
11
+ def graphics
12
+ :wingman
13
+ end
14
+ 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