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,100 @@
1
+ class PlayerInput < Component
2
+ # Dark green
3
+ NAME_COLOR = Gosu::Color.argb(0xee084408)
4
+ attr_reader :stats
5
+
6
+ def initialize(name, camera, object_pool)
7
+ super(nil)
8
+ @name = name
9
+ @stats = Stats.new(name)
10
+ @camera = camera
11
+ @object_pool = object_pool
12
+ end
13
+
14
+ def control(obj)
15
+ self.object = obj
16
+ obj.components << self
17
+ end
18
+
19
+ def on_collision(with)
20
+ end
21
+
22
+ def on_damage(amount)
23
+ @stats.add_damage(amount)
24
+ end
25
+
26
+ def update
27
+ return respawn if object.health.dead?
28
+ d_x, d_y = @camera.target_delta_on_screen
29
+ atan = Math.atan2(($window.width / 2) - d_x - $window.mouse_x,
30
+ ($window.height / 2) - d_y - $window.mouse_y)
31
+ object.gun_angle = -atan * 180 / Math::PI
32
+ motion_buttons = [Gosu::KbW, Gosu::KbS, Gosu::KbA, Gosu::KbD]
33
+
34
+ if any_button_down?(*motion_buttons)
35
+ object.throttle_down = true
36
+ object.physics.change_direction(
37
+ change_angle(object.direction, *motion_buttons))
38
+ else
39
+ object.throttle_down = false
40
+ end
41
+
42
+ if Utils.button_down?(Gosu::MsLeft)
43
+ object.shoot(*@camera.mouse_coords)
44
+ end
45
+ end
46
+
47
+ def draw(viewport)
48
+ @name_image ||= Gosu::Image.from_text(
49
+ $window, @name, Gosu.default_font_name, 20)
50
+ @name_image.draw(
51
+ x - @name_image.width / 2 - 1,
52
+ y + object.graphics.height / 2, 100,
53
+ 1, 1, Gosu::Color::WHITE)
54
+ @name_image.draw(
55
+ x - @name_image.width / 2,
56
+ y + object.graphics.height / 2, 100,
57
+ 1, 1, NAME_COLOR)
58
+ end
59
+
60
+ private
61
+
62
+ def respawn
63
+ if object.health.should_respawn?
64
+ object.health.restore
65
+ object.move(*@object_pool.map.spawn_point)
66
+ @camera.x, @camera.y = x, y
67
+ PlayerSounds.respawn(object, @camera)
68
+ end
69
+ end
70
+
71
+ def any_button_down?(*buttons)
72
+ buttons.each do |b|
73
+ return true if Utils.button_down?(b)
74
+ end
75
+ false
76
+ end
77
+
78
+ def change_angle(previous_angle, up, down, right, left)
79
+ if Utils.button_down?(up)
80
+ angle = 0.0
81
+ angle += 45.0 if Utils.button_down?(left)
82
+ angle -= 45.0 if Utils.button_down?(right)
83
+ elsif Utils.button_down?(down)
84
+ angle = 180.0
85
+ angle -= 45.0 if Utils.button_down?(left)
86
+ angle += 45.0 if Utils.button_down?(right)
87
+ elsif Utils.button_down?(left)
88
+ angle = 90.0
89
+ angle += 45.0 if Utils.button_down?(up)
90
+ angle -= 45.0 if Utils.button_down?(down)
91
+ elsif Utils.button_down?(right)
92
+ angle = 270.0
93
+ angle -= 45.0 if Utils.button_down?(up)
94
+ angle += 45.0 if Utils.button_down?(down)
95
+ end
96
+ angle = (angle + 360) % 360 if angle && angle < 0
97
+ (angle || previous_angle)
98
+ end
99
+
100
+ end
@@ -0,0 +1,16 @@
1
+ class PlayerSounds
2
+ class << self
3
+ def respawn(object, camera)
4
+ volume, pan = Utils.volume_and_pan(object, camera)
5
+ respawn_sound.play(object.object_id, pan, volume * 0.5)
6
+ end
7
+
8
+ private
9
+
10
+ def respawn_sound
11
+ @@respawn ||= StereoSample.new(
12
+ $window, Utils.media_path('respawn.wav'))
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,22 @@
1
+ class PowerupGraphics < Component
2
+ def initialize(object, type)
3
+ super(object)
4
+ @type = type
5
+ end
6
+
7
+ def draw(viewport)
8
+ image.draw(x - 12, y - 12, 1)
9
+ Utils.mark_corners(object.box) if $debug
10
+ end
11
+
12
+ private
13
+
14
+ def image
15
+ @image ||= images.frame("#{@type}.png")
16
+ end
17
+
18
+ def images
19
+ @@images ||= Gosu::TexturePacker.load_json(
20
+ $window, Utils.media_path('pickups.json'))
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ class PowerupSounds
2
+ class << self
3
+ def play(object, camera)
4
+ volume, pan = Utils.volume_and_pan(object, camera)
5
+ sound.play(object.object_id, pan, volume)
6
+ end
7
+
8
+ private
9
+
10
+ def sound
11
+ @@sound ||= StereoSample.new(
12
+ $window, Utils.media_path('powerup.mp3'))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ class TankGraphics < Component
2
+ def initialize(game_object)
3
+ super(game_object)
4
+ @body_normal = units.frame('tank1_body.png')
5
+ @shadow_normal = units.frame('tank1_body_shadow.png')
6
+ @gun_normal = units.frame('tank1_dualgun.png')
7
+ @body_dead = units.frame('tank1_body_destroyed.png')
8
+ @shadow_dead = units.frame('tank1_body_destroyed_shadow.png')
9
+ @gun_dead = nil
10
+ update
11
+ end
12
+
13
+ def update
14
+ if object && object.health.dead?
15
+ @body = @body_dead
16
+ @gun = @gun_dead
17
+ @shadow = @shadow_dead
18
+ else
19
+ @body = @body_normal
20
+ @gun = @gun_normal
21
+ @shadow = @shadow_normal
22
+ end
23
+ end
24
+
25
+ def draw(viewport)
26
+ @shadow.draw_rot(x - 1, y - 1, 0, object.direction)
27
+ @body.draw_rot(x, y, 1, object.direction)
28
+ @gun.draw_rot(x, y, 2, object.gun_angle) if @gun
29
+ Utils.mark_corners(object.box) if $debug
30
+ end
31
+
32
+ def width
33
+ @body.width
34
+ end
35
+
36
+ def height
37
+ @body.height
38
+ end
39
+
40
+ private
41
+
42
+ def units
43
+ @@units = Gosu::TexturePacker.load_json(
44
+ $window, Utils.media_path('ground_units.json'), :precise)
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ class TankHealth < Health
2
+ RESPAWN_DELAY = 5000
3
+ attr_accessor :health
4
+
5
+ def initialize(object, object_pool)
6
+ super(object, object_pool, 100, true)
7
+ end
8
+
9
+ def should_respawn?
10
+ if @death_time
11
+ Gosu.milliseconds - @death_time > RESPAWN_DELAY
12
+ end
13
+ end
14
+
15
+ protected
16
+
17
+ def draw?
18
+ true
19
+ end
20
+
21
+ def after_death(cause)
22
+ @death_time = Gosu.milliseconds
23
+ object.reset_modifiers
24
+ object.input.stats.add_death
25
+ kill = object != cause ? 1 : -1
26
+ cause.input.stats.add_kill(kill)
27
+ Thread.new do
28
+ sleep(rand(0.1..0.3))
29
+ Explosion.new(@object_pool, x, y, cause)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,179 @@
1
+ class TankPhysics < Component
2
+ attr_accessor :speed, :in_collision, :collides_with
3
+
4
+ def initialize(game_object, object_pool)
5
+ super(game_object)
6
+ @object_pool = object_pool
7
+ @map = object_pool.map
8
+ @speed = 0.0
9
+ end
10
+
11
+ def can_move_to?(x, y)
12
+ old_x, old_y = object.x, object.y
13
+ object.move(x, y)
14
+ return false unless @map.can_move_to?(x, y)
15
+ @object_pool.nearby(object, 100).each do |obj|
16
+ next if obj.class == Bullet && obj.source == object
17
+ if collides_with_poly?(obj.box)
18
+ if obj.is_a? Powerup
19
+ obj.on_collision(object)
20
+ else
21
+ @collides_with = obj
22
+ # Allow to get unstuck
23
+ old_distance = Utils.distance_between(
24
+ obj.x, obj.y, old_x, old_y)
25
+ new_distance = Utils.distance_between(
26
+ obj.x, obj.y, x, y)
27
+ return false if new_distance < old_distance
28
+ end
29
+ else
30
+ @collides_with = nil
31
+ end
32
+ end
33
+ true
34
+ ensure
35
+ object.move(old_x, old_y)
36
+ end
37
+
38
+ def change_direction(new_direction)
39
+ change = (new_direction - object.direction + 360) % 360
40
+ change = 360 - change if change > 180
41
+ if change > 90
42
+ @speed = 0
43
+ elsif change > 45
44
+ @speed *= 0.33
45
+ elsif change > 0
46
+ @speed *= 0.66
47
+ end
48
+ object.direction = new_direction % 360
49
+ end
50
+
51
+ def moving?
52
+ @speed > 0
53
+ end
54
+
55
+ def box_height
56
+ @box_height ||= object.graphics.height
57
+ end
58
+
59
+ def box_width
60
+ @box_width ||= object.graphics.width
61
+ end
62
+
63
+ # Tank box looks like H. Vertices:
64
+ # 1 2 5 6
65
+ # 3 4
66
+ #
67
+ # 10 9
68
+ # 12 11 8 7
69
+ def box
70
+ w = box_width / 2 - 1
71
+ h = box_height / 2 - 1
72
+ tw = 8 # track width
73
+ fd = 8 # front depth
74
+ rd = 6 # rear depth
75
+ Utils.rotate(object.direction, x, y,
76
+ x + w, y + h, #1
77
+ x + w - tw, y + h, #2
78
+ x + w - tw, y + h - fd, #3
79
+
80
+ x - w + tw, y + h - fd, #4
81
+ x - w + tw, y + h, #5
82
+ x - w, y + h, #6
83
+
84
+ x - w, y - h, #7
85
+ x - w + tw, y - h, #8
86
+ x - w + tw, y - h + rd, #9
87
+
88
+ x + w - tw, y - h + rd, #10
89
+ x + w - tw, y - h, #11
90
+ x + w, y - h, #12
91
+ )
92
+ end
93
+
94
+ def update
95
+ if object.throttle_down && !object.health.dead?
96
+ accelerate
97
+ else
98
+ decelerate
99
+ end
100
+ if @speed > 0
101
+ new_x, new_y = x, y
102
+ speed = apply_movement_penalty(@speed)
103
+ shift = Utils.adjust_speed(speed) * object.speed_modifier
104
+ case @object.direction.to_i
105
+ when 0
106
+ new_y -= shift
107
+ when 45
108
+ new_x += shift
109
+ new_y -= shift
110
+ when 90
111
+ new_x += shift
112
+ when 135
113
+ new_x += shift
114
+ new_y += shift
115
+ when 180
116
+ new_y += shift
117
+ when 225
118
+ new_y += shift
119
+ new_x -= shift
120
+ when 270
121
+ new_x -= shift
122
+ when 315
123
+ new_x -= shift
124
+ new_y -= shift
125
+ end
126
+ if can_move_to?(new_x, new_y)
127
+ object.move(new_x, new_y)
128
+ @in_collision = false
129
+ else
130
+ object.on_collision(@collides_with)
131
+ @speed = 0.0
132
+ @in_collision = true
133
+ end
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def apply_movement_penalty(speed)
140
+ speed * (1.0 - @map.movement_penalty(x, y))
141
+ end
142
+
143
+ def accelerate
144
+ @speed += 0.08 if @speed < 5
145
+ end
146
+
147
+ def decelerate
148
+ if @speed > 0
149
+ @speed = [@speed - 0.5, 0].max
150
+ elsif @speed < 0
151
+ @speed = [@speed + 0.5, 0].min
152
+ end
153
+ damp_speed
154
+ end
155
+
156
+ def damp_speed
157
+ @speed = 0 if @speed < 0.01
158
+ end
159
+
160
+ def collides_with_poly?(poly)
161
+ if poly
162
+ if poly.size == 2
163
+ px, py = poly
164
+ return Utils.point_in_poly(px, py, *box)
165
+ end
166
+ poly.each_slice(2) do |x, y|
167
+ return true if Utils.point_in_poly(x, y, *box)
168
+ end
169
+ box.each_slice(2) do |x, y|
170
+ return true if Utils.point_in_poly(x, y, *poly)
171
+ end
172
+ end
173
+ false
174
+ end
175
+
176
+ def collides_with_point?(x, y)
177
+ Utils.point_in_poly(x, y, box)
178
+ end
179
+ end
@@ -0,0 +1,43 @@
1
+ class TankSounds < Component
2
+ def initialize(object, object_pool)
3
+ super(object)
4
+ @object_pool = object_pool
5
+ end
6
+
7
+ def update
8
+ id = object.object_id
9
+ if object.physics.moving?
10
+ move_volume = Utils.volume(
11
+ object, @object_pool.camera)
12
+ pan = Utils.pan(object, @object_pool.camera)
13
+ if driving_sound.paused?(id)
14
+ driving_sound.resume(id)
15
+ elsif driving_sound.stopped?(id)
16
+ driving_sound.play(id, pan, 0.5, 1, true)
17
+ end
18
+ driving_sound.volume_and_pan(id, move_volume * 0.5, pan)
19
+ else
20
+ if driving_sound.playing?(id)
21
+ driving_sound.pause(id)
22
+ end
23
+ end
24
+ end
25
+
26
+ def collide
27
+ vol, pan = Utils.volume_and_pan(
28
+ object, @object_pool.camera)
29
+ crash_sound.play(self.object_id, pan, vol, 1, false)
30
+ end
31
+
32
+ private
33
+
34
+ def driving_sound
35
+ @@driving_sound ||= StereoSample.new(
36
+ $window, Utils.media_path('tank_driving.mp3'))
37
+ end
38
+
39
+ def crash_sound
40
+ @@crash_sound ||= StereoSample.new(
41
+ $window, Utils.media_path('metal_interaction2.wav'))
42
+ end
43
+ end