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