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,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