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,61 @@
1
+ require 'singleton'
2
+ class PauseState < GameState
3
+ include Singleton
4
+ attr_accessor :play_state
5
+
6
+ def initialize
7
+ @message = Gosu::Image.from_text(
8
+ $window, "Game Paused",
9
+ Utils.title_font, 60)
10
+ end
11
+
12
+ def enter
13
+ music.play(true)
14
+ music.volume = 1
15
+ @score_display = ScoreDisplay.new(@play_state.object_pool)
16
+ @mouse_coords = [$window.mouse_x, $window.mouse_y]
17
+ end
18
+
19
+ def leave
20
+ music.volume = 0
21
+ music.stop
22
+ $window.mouse_x, $window.mouse_y = @mouse_coords
23
+ end
24
+
25
+ def music
26
+ @@music ||= Gosu::Song.new(
27
+ $window, Utils.media_path('menu_music.mp3'))
28
+ end
29
+
30
+ def draw
31
+ @play_state.draw
32
+ @message.draw(
33
+ $window.width / 2 - @message.width / 2,
34
+ $window.height / 4 - @message.height - 50,
35
+ 1000)
36
+ info.draw(
37
+ $window.width / 2 - info.width / 2,
38
+ $window.height / 4 - info.height,
39
+ 1000)
40
+ @score_display.draw
41
+ end
42
+
43
+ def info
44
+ @info ||= Gosu::Image.from_text(
45
+ $window, 'Q: Quit to Main Menu',
46
+ Utils.main_font, 30)
47
+ end
48
+
49
+ def button_down(id)
50
+ if id == Gosu::KbQ
51
+ MenuState.instance.play_state = @play_state
52
+ GameState.switch(MenuState.instance)
53
+ end
54
+ if id == Gosu::KbC && @play_state
55
+ GameState.switch(@play_state)
56
+ end
57
+ if id == Gosu::KbEscape
58
+ GameState.switch(@play_state)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,119 @@
1
+ class PlayState < GameState
2
+ attr_accessor :update_interval, :object_pool, :tank
3
+
4
+ def initialize
5
+ # http://www.paulandstorm.com/wha/clown-names/
6
+ @names = Names.new(
7
+ Utils.media_path('names.txt'))
8
+ @object_pool = ObjectPool.new(Map.bounding_box)
9
+ @map = Map.new(@object_pool)
10
+ @camera = Camera.new
11
+ @object_pool.camera = @camera
12
+ create_tanks(7)
13
+ end
14
+
15
+ def update
16
+ StereoSample.cleanup
17
+ @object_pool.update_all
18
+ @camera.update
19
+ @hud.update
20
+ update_caption
21
+ end
22
+
23
+ def draw
24
+ cam_x = @camera.x
25
+ cam_y = @camera.y
26
+ off_x = $window.width / 2 - cam_x
27
+ off_y = $window.height / 2 - cam_y
28
+ viewport = @camera.viewport
29
+ x1, x2, y1, y2 = viewport
30
+ box = AxisAlignedBoundingBox.new(
31
+ [x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2],
32
+ [x1 - Map::TILE_SIZE, y1 - Map::TILE_SIZE])
33
+ $window.translate(off_x, off_y) do
34
+ zoom = @camera.zoom
35
+ $window.scale(zoom, zoom, cam_x, cam_y) do
36
+ @map.draw(viewport)
37
+ @object_pool.query_range(box).map do |o|
38
+ o.draw(viewport)
39
+ end
40
+ end
41
+ end
42
+ @hud.draw
43
+ end
44
+
45
+ def button_down(id)
46
+ if id == Gosu::KbEscape
47
+ pause = PauseState.instance
48
+ pause.play_state = self
49
+ GameState.switch(pause)
50
+ end
51
+ if id == Gosu::KbT
52
+ t = Tank.new(@object_pool,
53
+ AiInput.new(@names.random, @object_pool))
54
+ t.move(*@camera.mouse_coords)
55
+ end
56
+ if id == Gosu::KbF1
57
+ $debug = !$debug
58
+ end
59
+ if id == Gosu::KbF2
60
+ toggle_profiling
61
+ end
62
+ if id == Gosu::KbR
63
+ @tank.mark_for_removal
64
+ @tank = Tank.new(@object_pool,
65
+ PlayerInput.new('Player', @camera, @object_pool))
66
+ @camera.target = @tank
67
+ @hud.player = @tank
68
+ end
69
+ end
70
+
71
+ def leave
72
+ StereoSample.stop_all
73
+ if @profiling_now
74
+ toggle_profiling
75
+ end
76
+ @hud.active = false
77
+ end
78
+
79
+ def enter
80
+ @hud.active = true
81
+ end
82
+
83
+ private
84
+
85
+ def create_tanks(amount)
86
+ @map.spawn_points(amount * 3)
87
+ @tank = Tank.new(@object_pool,
88
+ PlayerInput.new('Player', @camera, @object_pool))
89
+ amount.times do |i|
90
+ Tank.new(@object_pool, AiInput.new(
91
+ @names.random, @object_pool))
92
+ end
93
+ @camera.target = @tank
94
+ @hud = HUD.new(@object_pool, @tank)
95
+ end
96
+
97
+ def toggle_profiling
98
+ require 'ruby-prof' unless defined?(RubyProf)
99
+ if @profiling_now
100
+ result = RubyProf.stop
101
+ printer = RubyProf::FlatPrinter.new(result)
102
+ printer.print(STDOUT, min_percent: 0.01)
103
+ @profiling_now = false
104
+ else
105
+ RubyProf.start
106
+ @profiling_now = true
107
+ end
108
+ end
109
+
110
+ def update_caption
111
+ now = Gosu.milliseconds
112
+ if now - (@caption_updated_at || 0) > 1000
113
+ $window.caption = 'Tank Island. ' <<
114
+ "[FPS: #{Gosu.fps}. " <<
115
+ "Tank @ #{@tank.x.round}:#{@tank.y.round}]"
116
+ @caption_updated_at = now
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,33 @@
1
+ class AxisAlignedBoundingBox
2
+ attr_reader :center, :half_dimension
3
+ def initialize(center, half_dimension)
4
+ @center = center
5
+ @half_dimension = half_dimension
6
+ @dhx = (@half_dimension[0] - @center[0]).abs
7
+ @dhy = (@half_dimension[1] - @center[1]).abs
8
+ end
9
+
10
+ def contains?(point)
11
+ return false unless (@center[0] + @dhx) >= point[0]
12
+ return false unless (@center[0] - @dhx) <= point[0]
13
+ return false unless (@center[1] + @dhy) >= point[1]
14
+ return false unless (@center[1] - @dhy) <= point[1]
15
+ true
16
+ end
17
+
18
+ def intersects?(other)
19
+ ocx, ocy = other.center
20
+ ohx, ohy = other.half_dimension
21
+ odhx = (ohx - ocx).abs
22
+ return false unless (@center[0] + @dhx) >= (ocx - odhx)
23
+ return false unless (@center[0] - @dhx) <= (ocx + odhx)
24
+ odhy = (ohy - ocy).abs
25
+ return false unless (@center[1] + @dhy) >= (ocy - odhy)
26
+ return false unless (@center[1] - @dhy) <= (ocy + odhy)
27
+ true
28
+ end
29
+
30
+ def to_s
31
+ "c: #{@center}, h: #{@half_dimension}"
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ class GameWindow < Gosu::Window
2
+ attr_accessor :state
3
+
4
+ def initialize
5
+ super((ENV['w'] || 800).to_i,
6
+ (ENV['h'] || 600).to_i,
7
+ (ENV['fs'] ? true : false))
8
+ end
9
+
10
+ def update
11
+ Utils.track_update_interval
12
+ @state.update
13
+ end
14
+
15
+ def draw
16
+ @state.draw
17
+ end
18
+
19
+ def needs_redraw?
20
+ @state.needs_redraw?
21
+ end
22
+
23
+ def needs_cursor?
24
+ Utils.update_interval > 200
25
+ end
26
+
27
+ def button_down(id)
28
+ @state.button_down(id)
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ class Names
2
+ def initialize(file)
3
+ @names = File.read(file).split("\n").reject do |n|
4
+ n.size > 12
5
+ end
6
+ end
7
+
8
+ def random
9
+ name = @names.sample
10
+ @names.delete(name)
11
+ name
12
+ end
13
+ end
@@ -0,0 +1,91 @@
1
+ class QuadTree
2
+ NODE_CAPACITY = 12
3
+ attr_accessor :ne, :nw, :se, :sw, :objects
4
+
5
+ def initialize(boundary)
6
+ @boundary = boundary
7
+ @objects = []
8
+ end
9
+
10
+ def insert(game_object)
11
+ return false unless @boundary.contains?(
12
+ game_object.location)
13
+
14
+ if @objects.size < NODE_CAPACITY
15
+ @objects << game_object
16
+ return true
17
+ end
18
+
19
+ subdivide unless @nw
20
+
21
+ return true if @nw.insert(game_object)
22
+ return true if @ne.insert(game_object)
23
+ return true if @sw.insert(game_object)
24
+ return true if @se.insert(game_object)
25
+
26
+ # should never happen
27
+ raise "Failed to insert #{game_object}"
28
+ end
29
+
30
+ def remove(game_object)
31
+ return false unless @boundary.contains?(
32
+ game_object.location)
33
+ if @objects.delete(game_object)
34
+ return true
35
+ end
36
+ return false unless @nw
37
+ return true if @nw.remove(game_object)
38
+ return true if @ne.remove(game_object)
39
+ return true if @sw.remove(game_object)
40
+ return true if @se.remove(game_object)
41
+ false
42
+ end
43
+
44
+ def query_range(range)
45
+ result = []
46
+ unless @boundary.intersects?(range)
47
+ return result
48
+ end
49
+
50
+ @objects.each do |o|
51
+ if range.contains?(o.location)
52
+ result << o
53
+ end
54
+ end
55
+
56
+ # Not subdivided
57
+ return result unless @ne
58
+
59
+ result += @nw.query_range(range)
60
+ result += @ne.query_range(range)
61
+ result += @sw.query_range(range)
62
+ result += @se.query_range(range)
63
+
64
+ result
65
+ end
66
+
67
+ private
68
+
69
+ def subdivide
70
+ cx, cy = @boundary.center
71
+ hx, hy = @boundary.half_dimension
72
+ hhx = (cx - hx).abs / 2.0
73
+ hhy = (cy - hy).abs / 2.0
74
+ @nw = QuadTree.new(
75
+ AxisAlignedBoundingBox.new(
76
+ [cx - hhx, cy - hhy],
77
+ [cx, cy]))
78
+ @ne = QuadTree.new(
79
+ AxisAlignedBoundingBox.new(
80
+ [cx + hhx, cy - hhy],
81
+ [cx, cy]))
82
+ @sw = QuadTree.new(
83
+ AxisAlignedBoundingBox.new(
84
+ [cx - hhx, cy + hhy],
85
+ [cx, cy]))
86
+ @se = QuadTree.new(
87
+ AxisAlignedBoundingBox.new(
88
+ [cx + hhx, cy + hhy],
89
+ [cx, cy]))
90
+ end
91
+ end
@@ -0,0 +1,55 @@
1
+ class Stats
2
+ attr_reader :name, :kills, :deaths, :shots, :changed_at
3
+ def initialize(name)
4
+ @name = name
5
+ @kills = @deaths = @shots = @damage = @damage_dealt = 0
6
+ changed
7
+ end
8
+
9
+ def add_kill(amount = 1)
10
+ @kills += amount
11
+ changed
12
+ end
13
+
14
+ def add_death
15
+ @deaths += 1
16
+ changed
17
+ end
18
+
19
+ def add_shot
20
+ @shots += 1
21
+ changed
22
+ end
23
+
24
+ def add_damage(amount)
25
+ @damage += amount
26
+ changed
27
+ end
28
+
29
+ def damage
30
+ @damage.round
31
+ end
32
+
33
+ def add_damage_dealt(amount)
34
+ @damage_dealt += amount
35
+ changed
36
+ end
37
+
38
+ def damage_dealt
39
+ @damage_dealt.round
40
+ end
41
+
42
+ def to_s
43
+ "[kills: #{@kills}, " \
44
+ "deaths: #{@deaths}, " \
45
+ "shots: #{@shots}, " \
46
+ "damage: #{damage}, " \
47
+ "damage_dealt: #{damage_dealt}]"
48
+ end
49
+
50
+ private
51
+
52
+ def changed
53
+ @changed_at = Gosu.milliseconds
54
+ end
55
+ end
@@ -0,0 +1,96 @@
1
+ class StereoSample
2
+ MAX_POLIPHONY = 16
3
+ @@all_instances = []
4
+
5
+ def self.register_instances(instances)
6
+ @@all_instances << instances
7
+ end
8
+
9
+ def self.cleanup
10
+ @@all_instances.each do |instances|
11
+ instances.each do |key, instance|
12
+ unless instance.playing? || instance.paused?
13
+ instances.delete(key)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.stop_all
20
+ @@all_instances.each do |instances|
21
+ instances.each do |key, instance|
22
+ if instance.playing?
23
+ instance.stop
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def initialize(window, sound_l, sound_r = sound_l)
30
+ @sound_l = Gosu::Sample.new(window, sound_l)
31
+ # Use same sample in mono -> stereo
32
+ if sound_l == sound_r
33
+ @sound_r = @sound_l
34
+ else
35
+ @sound_r = Gosu::Sample.new(window, sound_r)
36
+ end
37
+ @instances = {}
38
+ self.class.register_instances(@instances)
39
+ end
40
+
41
+ def paused?(id = :default)
42
+ i = @instances["#{id}_l"]
43
+ i && i.paused?
44
+ end
45
+
46
+ def playing?(id = :default)
47
+ i = @instances["#{id}_l"]
48
+ i && i.playing?
49
+ end
50
+
51
+ def stopped?(id = :default)
52
+ @instances["#{id}_l"].nil?
53
+ end
54
+
55
+ def play(id = :default, pan = 0,
56
+ volume = 1, speed = 1, looping = false)
57
+ if @instances.size > MAX_POLIPHONY
58
+ return
59
+ end
60
+ @instances["#{id}_l"] = @sound_l.play_pan(
61
+ -0.2, 0, speed, looping)
62
+ @instances["#{id}_r"] = @sound_r.play_pan(
63
+ 0.2, 0, speed, looping)
64
+ volume_and_pan(id, volume, pan)
65
+ end
66
+
67
+ def pause(id = :default)
68
+ @instances["#{id}_l"].pause
69
+ @instances["#{id}_r"].pause
70
+ end
71
+
72
+ def resume(id = :default)
73
+ @instances["#{id}_l"].resume
74
+ @instances["#{id}_r"].resume
75
+ end
76
+
77
+ def stop
78
+ @instances.delete("#{id}_l").stop
79
+ @instances.delete("#{id}_r").stop
80
+ end
81
+
82
+ def volume_and_pan(id, volume, pan)
83
+ return unless @instances["#{id}_l"]
84
+ if pan > 0
85
+ pan_l = 1 - pan * 2
86
+ pan_r = 1
87
+ else
88
+ pan_l = 1
89
+ pan_r = 1 + pan * 2
90
+ end
91
+ pan_l *= volume
92
+ pan_r *= volume
93
+ @instances["#{id}_l"].volume = [pan_l, 0.05].max
94
+ @instances["#{id}_r"].volume = [pan_r, 0.05].max
95
+ end
96
+ end