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,109 @@
1
+ class AiVision
2
+ CACHE_TIMEOUT = 500
3
+ POWERUP_CACHE_TIMEOUT = 50
4
+ attr_reader :in_sight
5
+
6
+ def initialize(viewer, object_pool, distance)
7
+ @viewer = viewer
8
+ @object_pool = object_pool
9
+ @distance = distance
10
+ end
11
+
12
+ def can_go_forward?
13
+ in_front = Utils.point_at_distance(
14
+ *@viewer.location, @viewer.direction, 40)
15
+ @object_pool.map.can_move_to?(*in_front) &&
16
+ @object_pool.nearby_point(*in_front, 40, @viewer)
17
+ .reject { |o| o.is_a? Powerup }.empty?
18
+ end
19
+
20
+ def update
21
+ @in_sight = @object_pool.nearby(@viewer, @distance)
22
+ end
23
+
24
+ def closest_free_path(away_from = nil)
25
+ paths = []
26
+ 5.times do |i|
27
+ if paths.any?
28
+ return farthest_from(paths, away_from)
29
+ end
30
+ radius = 55 - i * 5
31
+ range_x = range_y = [-radius, 0, radius]
32
+ range_x.shuffle.each do |x|
33
+ range_y.shuffle.each do |y|
34
+ x = @viewer.x + x
35
+ y = @viewer.y + y
36
+ if @object_pool.map.can_move_to?(x, y) &&
37
+ @object_pool.nearby_point(x, y, radius, @viewer)
38
+ .reject { |o| o.is_a? Powerup }.empty?
39
+ if away_from
40
+ paths << [x, y]
41
+ else
42
+ return [x, y]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ false
49
+ end
50
+
51
+ alias :closest_free_path_away_from :closest_free_path
52
+
53
+ def closest_tank
54
+ now = Gosu.milliseconds
55
+ @closest_tank = nil
56
+ if now - (@cache_updated_at ||= 0) > CACHE_TIMEOUT
57
+ @closest_tank = nil
58
+ @cache_updated_at = now
59
+ end
60
+ @closest_tank ||= find_closest_tank
61
+ end
62
+
63
+ def closest_powerup(*suitable)
64
+ now = Gosu.milliseconds
65
+ @closest_powerup = nil
66
+ if now - (@powerup_cache_updated_at ||= 0) > POWERUP_CACHE_TIMEOUT
67
+ @closest_powerup = nil
68
+ @powerup_cache_updated_at = now
69
+ end
70
+ @closest_powerup ||= find_closest_powerup(*suitable)
71
+ end
72
+
73
+ private
74
+
75
+ def farthest_from(paths, away_from)
76
+ paths.sort do |p1, p2|
77
+ Utils.distance_between(*p1, *away_from) <=>
78
+ Utils.distance_between(*p2, *away_from)
79
+ end.first
80
+ end
81
+
82
+ def find_closest_powerup(*suitable)
83
+ if suitable.empty?
84
+ suitable = [FireRatePowerup,
85
+ HealthPowerup,
86
+ RepairPowerup,
87
+ TankSpeedPowerup]
88
+ end
89
+ @in_sight.select do |o|
90
+ suitable.include?(o.class)
91
+ end.sort do |a, b|
92
+ x, y = @viewer.x, @viewer.y
93
+ d1 = Utils.distance_between(x, y, a.x, a.y)
94
+ d2 = Utils.distance_between(x, y, b.x, b.y)
95
+ d1 <=> d2
96
+ end.first
97
+ end
98
+
99
+ def find_closest_tank
100
+ @in_sight.select do |o|
101
+ o.class == Tank && !o.health.dead?
102
+ end.sort do |a, b|
103
+ x, y = @viewer.x, @viewer.y
104
+ d1 = Utils.distance_between(x, y, a.x, a.y)
105
+ d2 = Utils.distance_between(x, y, b.x, b.y)
106
+ d1 <=> d2
107
+ end.first
108
+ end
109
+ end
@@ -0,0 +1,70 @@
1
+ class AiInput < Component
2
+ # Dark red
3
+ NAME_COLOR = Gosu::Color.argb(0xeeb10000)
4
+ UPDATE_RATE = 10 # ms
5
+ attr_reader :name
6
+ attr_reader :stats
7
+
8
+ def initialize(name, object_pool)
9
+ super(nil)
10
+ @object_pool = object_pool
11
+ @stats = Stats.new(name)
12
+ @name = name
13
+ @last_update = Gosu.milliseconds
14
+ end
15
+
16
+ def control(obj)
17
+ self.object = obj
18
+ object.components << self
19
+ @vision = AiVision.new(obj, @object_pool,
20
+ rand(700..1200))
21
+ @gun = AiGun.new(obj, @vision)
22
+ @motion = TankMotionFSM.new(obj, @vision, @gun)
23
+ end
24
+
25
+ def on_collision(with)
26
+ return if object.health.dead?
27
+ @motion.on_collision(with)
28
+ end
29
+
30
+ def on_damage(amount)
31
+ @motion.on_damage(amount)
32
+ @stats.add_damage(amount)
33
+ end
34
+
35
+ def update
36
+ return respawn if object.health.dead?
37
+ @gun.adjust_angle
38
+ now = Gosu.milliseconds
39
+ return if now - @last_update < UPDATE_RATE
40
+ @last_update = now
41
+ @vision.update
42
+ @gun.update
43
+ @motion.update
44
+ end
45
+
46
+ def draw(viewport)
47
+ @motion.draw(viewport)
48
+ @gun.draw(viewport)
49
+ @name_image ||= Gosu::Image.from_text(
50
+ $window, @name, Gosu.default_font_name, 20)
51
+ @name_image.draw(
52
+ x - @name_image.width / 2 - 1,
53
+ y + object.graphics.height / 2, 100,
54
+ 1, 1, Gosu::Color::WHITE)
55
+ @name_image.draw(
56
+ x - @name_image.width / 2,
57
+ y + object.graphics.height / 2, 100,
58
+ 1, 1, NAME_COLOR)
59
+ end
60
+
61
+ private
62
+
63
+ def respawn
64
+ if object.health.should_respawn?
65
+ object.health.restore
66
+ object.move(*@object_pool.map.spawn_point)
67
+ PlayerSounds.respawn(object, @object_pool.camera)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,39 @@
1
+ class BoxGraphics < Component
2
+ def initialize(object)
3
+ super(object)
4
+ load_sprite
5
+ end
6
+
7
+ def draw(viewport)
8
+ @box.draw_rot(x, y, 0, object.angle)
9
+ Utils.mark_corners(object.box) if $debug
10
+ end
11
+
12
+ def height
13
+ @box.height
14
+ end
15
+
16
+ def width
17
+ @box.width
18
+ end
19
+
20
+ private
21
+
22
+ def load_sprite
23
+ frame = boxes.frame_list.sample
24
+ @box = boxes.frame(frame)
25
+ end
26
+
27
+ def center_x
28
+ @center_x ||= x - width / 2
29
+ end
30
+
31
+ def center_y
32
+ @center_y ||= y - height / 2
33
+ end
34
+
35
+ def boxes
36
+ @@boxes ||= Gosu::TexturePacker.load_json($window,
37
+ Utils.media_path('boxes_barrels.json'))
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ class BulletGraphics < Component
2
+ def draw(viewport)
3
+ image.draw(x - 8, y - 8, 1)
4
+ Utils.mark_corners(object.box) if $debug
5
+ end
6
+
7
+ private
8
+
9
+ def image
10
+ @@bullet ||= Gosu::Image.new(
11
+ $window, Utils.media_path('bullet.png'), false)
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ class BulletPhysics < Component
2
+ START_DIST = 20
3
+ MAX_DIST = 500
4
+
5
+ def initialize(game_object, object_pool)
6
+ super(game_object)
7
+ x, y = point_at_distance(START_DIST)
8
+ object.move(x, y)
9
+ @object_pool = object_pool
10
+ if trajectory_length > MAX_DIST
11
+ object.target_x, object.target_y = point_at_distance(MAX_DIST)
12
+ end
13
+ end
14
+
15
+ def update
16
+ fly_speed = Utils.adjust_speed(object.speed)
17
+ now = Gosu.milliseconds
18
+ @last_update ||= object.fired_at
19
+ fly_distance = (now - @last_update) * 0.001 * fly_speed
20
+ object.move(*point_at_distance(fly_distance))
21
+ @last_update = now
22
+ check_hit
23
+ object.explode if arrived?
24
+ end
25
+
26
+ def trajectory_length
27
+ Utils.distance_between(object.target_x, object.target_y, x, y)
28
+ end
29
+
30
+ def point_at_distance(distance)
31
+ if distance > trajectory_length
32
+ return [object.target_x, object.target_y]
33
+ end
34
+ distance_factor = distance.to_f / trajectory_length
35
+ p_x = x + (object.target_x - x) * distance_factor
36
+ p_y = y + (object.target_y - y) * distance_factor
37
+ [p_x, p_y]
38
+ end
39
+
40
+ private
41
+
42
+ def check_hit
43
+ @object_pool.nearby(object, 50).each do |obj|
44
+ next if obj == object.source # Don't hit source tank
45
+ if obj.class == Tree
46
+ if Utils.distance_between(x, y, obj.x, obj.y) < 10
47
+ return do_hit(obj) if obj.respond_to?(:health)
48
+ end
49
+ elsif Utils.point_in_poly(x, y, *obj.box)
50
+ # Direct hit - extra damage
51
+ return do_hit(obj) if obj.respond_to?(:health)
52
+ end
53
+ end
54
+ end
55
+
56
+ def do_hit(obj)
57
+ obj.health.inflict_damage(20, object.source)
58
+ object.target_x = x
59
+ object.target_y = y
60
+ end
61
+
62
+ def arrived?
63
+ x == object.target_x && y == object.target_y
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ class BulletSounds
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('fire.mp3'))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ class Component
2
+ attr_reader :object # better performance
3
+
4
+ def initialize(game_object = nil)
5
+ self.object = game_object
6
+ end
7
+
8
+ def update
9
+ # override
10
+ end
11
+
12
+ def draw(viewport)
13
+ # override
14
+ end
15
+
16
+ protected
17
+
18
+ def object=(obj)
19
+ if obj
20
+ @object = obj
21
+ obj.components << self
22
+ end
23
+ end
24
+
25
+ def x
26
+ @object.x
27
+ end
28
+
29
+ def y
30
+ @object.y
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ class DamageGraphics < Component
2
+ def initialize(object_pool)
3
+ super
4
+ @image = images.sample
5
+ @angle = rand(0..360)
6
+ end
7
+
8
+ def draw(viewport)
9
+ @image.draw_rot(x, y, 0, @angle)
10
+ end
11
+
12
+ private
13
+
14
+ def images
15
+ @@images ||= (1..4).map do |i|
16
+ Gosu::Image.new($window,
17
+ Utils.media_path("damage#{i}.png"), false)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ class ExplosionGraphics < Component
2
+ FRAME_DELAY = 16.66 # ms
3
+
4
+ def initialize(game_object)
5
+ super
6
+ @current_frame = 0
7
+ end
8
+
9
+ def draw(viewport)
10
+ image = current_frame
11
+ image.draw(
12
+ x - image.width / 2 + 3,
13
+ y - image.height / 2 - 35,
14
+ 20)
15
+ end
16
+
17
+ def update
18
+ now = Gosu.milliseconds
19
+ delta = now - (@last_frame ||= now)
20
+ if delta > FRAME_DELAY
21
+ @last_frame = now
22
+ end
23
+ @current_frame += (delta / FRAME_DELAY).floor
24
+ object.mark_for_removal if done?
25
+ end
26
+
27
+ private
28
+
29
+ def current_frame
30
+ animation[@current_frame % animation.size]
31
+ end
32
+
33
+ def done?
34
+ @done ||= @current_frame >= animation.size
35
+ end
36
+
37
+ def animation
38
+ @@animation ||=
39
+ Gosu::Image.load_tiles(
40
+ $window, Utils.media_path('explosion.png'),
41
+ 128, 128, false)
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ class ExplosionSounds
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('explosion.mp3'))
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,87 @@
1
+ class Health < Component
2
+ attr_accessor :health
3
+
4
+ def initialize(object, object_pool, health, explodes)
5
+ super(object)
6
+ @explodes = explodes
7
+ @object_pool = object_pool
8
+ @initial_health = @health = health
9
+ @health_updated = true
10
+ end
11
+
12
+ def restore
13
+ @health = @initial_health
14
+ @health_updated = true
15
+ end
16
+
17
+ def increase(amount)
18
+ @health = [@health + 25, @initial_health * 2].min
19
+ @health_updated = true
20
+ end
21
+
22
+ def damaged?
23
+ @health < @initial_health
24
+ end
25
+
26
+ def dead?
27
+ @health < 1
28
+ end
29
+
30
+ def update
31
+ update_image
32
+ end
33
+
34
+ def inflict_damage(amount, cause)
35
+ if @health > 0
36
+ @health_updated = true
37
+ if object.respond_to?(:input)
38
+ object.input.stats.add_damage(amount)
39
+ # Don't count damage to trees and boxes
40
+ if cause.respond_to?(:input) && cause != object
41
+ cause.input.stats.add_damage_dealt(amount)
42
+ end
43
+ end
44
+ @health = [@health - amount.to_i, 0].max
45
+ after_death(cause) if dead?
46
+ end
47
+ end
48
+
49
+ def draw(viewport)
50
+ return unless draw?
51
+ @image && @image.draw(
52
+ x - @image.width / 2,
53
+ y - object.graphics.height / 2 -
54
+ @image.height, 100)
55
+ end
56
+
57
+ protected
58
+
59
+ def draw?
60
+ $debug
61
+ end
62
+
63
+ def update_image
64
+ return unless draw?
65
+ if @health_updated
66
+ text = @health.to_s
67
+ font_size = 18
68
+ @image = Gosu::Image.from_text(
69
+ $window, text,
70
+ Gosu.default_font_name, font_size)
71
+ @health_updated = false
72
+ end
73
+ end
74
+
75
+ def after_death(cause)
76
+ if @explodes
77
+ Thread.new do
78
+ sleep(rand(0.1..0.3))
79
+ Explosion.new(@object_pool, x, y, cause)
80
+ sleep 0.3
81
+ object.mark_for_removal
82
+ end
83
+ else
84
+ object.mark_for_removal
85
+ end
86
+ end
87
+ end