tank_island 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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