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