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,69 @@
1
+ class TreeGraphics < Component
2
+ SHAKE_TIME = 100
3
+ SHAKE_COOLDOWN = 200
4
+ SHAKE_DISTANCE = [2, 1, 0, -1, -2, -1, 0, 1, 0, -1, 0]
5
+ def initialize(object, seed)
6
+ super(object)
7
+ load_sprite(seed)
8
+ end
9
+
10
+ def shake(direction)
11
+ now = Gosu.milliseconds
12
+ return if @shake_start &&
13
+ now - @shake_start < SHAKE_TIME + SHAKE_COOLDOWN
14
+ @shake_start = now
15
+ @shake_direction = direction
16
+ @shaking = true
17
+ end
18
+
19
+ def adjust_shake(x, y, shaking_for)
20
+ elapsed = [shaking_for, SHAKE_TIME].min / SHAKE_TIME.to_f
21
+ frame = ((SHAKE_DISTANCE.length - 1) * elapsed).floor
22
+ distance = SHAKE_DISTANCE[frame]
23
+ Utils.point_at_distance(x, y, @shake_direction, distance)
24
+ end
25
+
26
+ def draw(viewport)
27
+ if @shaking
28
+ shaking_for = Gosu.milliseconds - @shake_start
29
+ shaking_x, shaking_y = adjust_shake(
30
+ center_x, center_y, shaking_for)
31
+ @tree.draw(shaking_x, shaking_y, 5)
32
+ if shaking_for >= SHAKE_TIME
33
+ @shaking = false
34
+ end
35
+ else
36
+ @tree.draw(center_x, center_y, 5)
37
+ end
38
+ Utils.mark_corners(object.box) if $debug
39
+ end
40
+
41
+ def height
42
+ @tree.height
43
+ end
44
+
45
+ def width
46
+ @tree.width
47
+ end
48
+
49
+ private
50
+
51
+ def load_sprite(seed)
52
+ frame_list = trees.frame_list
53
+ frame = frame_list[(frame_list.size * seed).round]
54
+ @tree = trees.frame(frame)
55
+ end
56
+
57
+ def center_x
58
+ @center_x ||= x - @tree.width / 2
59
+ end
60
+
61
+ def center_y
62
+ @center_y ||= y - @tree.height / 2
63
+ end
64
+
65
+ def trees
66
+ @@trees ||= Gosu::TexturePacker.load_json($window,
67
+ Utils.media_path('trees_packed.json'))
68
+ end
69
+ end
@@ -0,0 +1,26 @@
1
+ class Damage < GameObject
2
+ MAX_INSTANCES = 300
3
+ @@instances = []
4
+
5
+ def initialize(object_pool, x, y)
6
+ super
7
+ DamageGraphics.new(self)
8
+ track(self)
9
+ end
10
+
11
+ def effect?
12
+ true
13
+ end
14
+
15
+ private
16
+
17
+ def track(instance)
18
+ if @@instances.size < MAX_INSTANCES
19
+ @@instances << instance
20
+ else
21
+ out = @@instances.shift
22
+ out.mark_for_removal
23
+ @@instances << instance
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ class Explosion < GameObject
2
+
3
+ def initialize(object_pool, x, y, source)
4
+ super(object_pool, x, y)
5
+ @source = source
6
+ @object_pool = object_pool
7
+ if @object_pool.map.can_move_to?(x, y)
8
+ Damage.new(@object_pool, x, y)
9
+ end
10
+ ExplosionGraphics.new(self)
11
+ ExplosionSounds.play(self, object_pool.camera)
12
+ inflict_damage
13
+ end
14
+
15
+ def effect?
16
+ true
17
+ end
18
+
19
+ def mark_for_removal
20
+ super
21
+ end
22
+
23
+ private
24
+
25
+ def inflict_damage
26
+ object_pool.nearby(self, 100).each do |obj|
27
+ if obj.respond_to?(:health)
28
+ obj.health.inflict_damage(
29
+ Math.sqrt(3 * 100 - Utils.distance_between(
30
+ obj.x, obj.y, @x, @y)), @source)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ class GameObject
2
+ attr_reader :x, :y, :location, :components
3
+ def initialize(object_pool, x, y)
4
+ @x, @y = x, y
5
+ @location = [x, y]
6
+ @components = []
7
+ @object_pool = object_pool
8
+ @object_pool.add(self)
9
+ end
10
+
11
+ def move(new_x, new_y)
12
+ return if new_x == @x && new_y == @y
13
+ @object_pool.tree_remove(self)
14
+ @x = new_x
15
+ @y = new_y
16
+ @location = [new_x, new_y]
17
+ @object_pool.tree_insert(self)
18
+ end
19
+
20
+ def update
21
+ @components.map(&:update)
22
+ end
23
+
24
+ def draw(viewport)
25
+ @components.each { |c| c.draw(viewport) }
26
+ end
27
+
28
+ def removable?
29
+ @removable
30
+ end
31
+
32
+ def mark_for_removal
33
+ @removable = true
34
+ end
35
+
36
+ def on_collision(object)
37
+ end
38
+
39
+ def effect?
40
+ false
41
+ end
42
+
43
+ def box
44
+ end
45
+
46
+ def collide
47
+ end
48
+
49
+ protected
50
+
51
+ def object_pool
52
+ @object_pool
53
+ end
54
+ end
@@ -0,0 +1,79 @@
1
+ class HUD
2
+ attr_accessor :active
3
+ def initialize(object_pool, tank)
4
+ @object_pool = object_pool
5
+ @tank = tank
6
+ @radar = Radar.new(@object_pool, tank)
7
+ end
8
+
9
+ def player=(tank)
10
+ @tank = tank
11
+ @radar.target = tank
12
+ end
13
+
14
+ def update
15
+ @radar.update
16
+ end
17
+
18
+ def health_image
19
+ if @health.nil? || @tank.health.health != @health
20
+ @health = @tank.health.health
21
+ @health_image = Gosu::Image.from_text(
22
+ $window, "Health: #{@health}", Utils.main_font, 20)
23
+ end
24
+ @health_image
25
+ end
26
+
27
+ def stats_image
28
+ stats = @tank.input.stats
29
+ if @stats_image.nil? || stats.changed_at <= Gosu.milliseconds
30
+ @stats_image = Gosu::Image.from_text(
31
+ $window, "Kills: #{stats.kills}", Utils.main_font, 20)
32
+ end
33
+ @stats_image
34
+ end
35
+
36
+ def fire_rate_image
37
+ if @tank.fire_rate_modifier > 1
38
+ if @fire_rate != @tank.fire_rate_modifier
39
+ @fire_rate = @tank.fire_rate_modifier
40
+ @fire_rate_image = Gosu::Image.from_text(
41
+ $window, "Fire rate: #{@fire_rate.round(2)}X",
42
+ Utils.main_font, 20)
43
+ end
44
+ else
45
+ @fire_rate_image = nil
46
+ end
47
+ @fire_rate_image
48
+ end
49
+
50
+ def speed_image
51
+ if @tank.speed_modifier > 1
52
+ if @speed != @tank.speed_modifier
53
+ @speed = @tank.speed_modifier
54
+ @speed_image = Gosu::Image.from_text(
55
+ $window, "Speed: #{@speed.round(2)}X",
56
+ Utils.main_font, 20)
57
+ end
58
+ else
59
+ @speed_image = nil
60
+ end
61
+ @speed_image
62
+ end
63
+
64
+ def draw
65
+ if @active
66
+ @object_pool.camera.draw_crosshair
67
+ end
68
+ @radar.draw
69
+ offset = 20
70
+ health_image.draw(20, offset, 1000)
71
+ stats_image.draw(20, offset += 30, 1000)
72
+ if fire_rate_image
73
+ fire_rate_image.draw(20, offset += 30, 1000)
74
+ end
75
+ if speed_image
76
+ speed_image.draw(20, offset += 30, 1000)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,183 @@
1
+ class Map
2
+ MAP_WIDTH = 30
3
+ MAP_HEIGHT = 30
4
+ TILE_SIZE = 128
5
+
6
+ def self.bounding_box
7
+ center = [MAP_WIDTH * TILE_SIZE / 2,
8
+ MAP_HEIGHT * TILE_SIZE / 2]
9
+ half_dimension = [MAP_WIDTH * TILE_SIZE,
10
+ MAP_HEIGHT * TILE_SIZE]
11
+ AxisAlignedBoundingBox.new(center, half_dimension)
12
+ end
13
+
14
+ def initialize(object_pool)
15
+ load_tiles
16
+ @object_pool = object_pool
17
+ object_pool.map = self
18
+ @map = generate_map
19
+ generate_trees
20
+ generate_boxes
21
+ generate_powerups
22
+ end
23
+
24
+ def spawn_points(max)
25
+ @spawn_points = (0..max).map do
26
+ find_spawn_point
27
+ end
28
+ @spawn_points_pointer = 0
29
+ end
30
+
31
+ def spawn_point
32
+ @spawn_points[(@spawn_points_pointer += 1) % @spawn_points.size]
33
+ end
34
+
35
+ def can_move_to?(x, y)
36
+ tile = tile_at(x, y)
37
+ tile && tile != @water
38
+ end
39
+
40
+ def movement_penalty(x, y)
41
+ tile = tile_at(x, y)
42
+ case tile
43
+ when @sand
44
+ 0.33
45
+ else
46
+ 0
47
+ end
48
+ end
49
+
50
+ def draw(viewport)
51
+ viewport = viewport.map { |p| p / TILE_SIZE }
52
+ x0, x1, y0, y1 = viewport.map(&:to_i)
53
+ (x0-1..x1).each do |x|
54
+ (y0-1..y1).each do |y|
55
+ row = @map[x]
56
+ map_x = x * TILE_SIZE
57
+ map_y = y * TILE_SIZE
58
+ if row
59
+ tile = @map[x][y]
60
+ if tile
61
+ tile.draw(map_x, map_y, 0)
62
+ else
63
+ @water.draw(map_x, map_y, 0)
64
+ end
65
+ else
66
+ @water.draw(map_x, map_y, 0)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def tile_at(x, y)
75
+ t_x = ((x / TILE_SIZE) % TILE_SIZE).floor
76
+ t_y = ((y / TILE_SIZE) % TILE_SIZE).floor
77
+ row = @map[t_x]
78
+ row ? row[t_y] : @water
79
+ end
80
+
81
+ def load_tiles
82
+ tiles = Gosu::Image.load_tiles(
83
+ $window, Utils.media_path('ground.png'),
84
+ 128, 128, true)
85
+ @sand = tiles[0]
86
+ @grass = tiles[8]
87
+ @water = Gosu::Image.new(
88
+ $window, Utils.media_path('water.png'), true)
89
+ end
90
+
91
+ def generate_map
92
+ noises = Perlin::Noise.new(2)
93
+ contrast = Perlin::Curve.contrast(
94
+ Perlin::Curve::CUBIC, 2)
95
+ map = {}
96
+ MAP_WIDTH.times do |x|
97
+ map[x] = {}
98
+ MAP_HEIGHT.times do |y|
99
+ n = noises[x * 0.1, y * 0.1]
100
+ n = contrast.call(n)
101
+ map[x][y] = choose_tile(n)
102
+ end
103
+ end
104
+ map
105
+ end
106
+
107
+ def generate_trees
108
+ noises = Perlin::Noise.new(2)
109
+ contrast = Perlin::Curve.contrast(
110
+ Perlin::Curve::CUBIC, 2)
111
+ trees = 0
112
+ target_trees = rand(1500..1500)
113
+ while trees < target_trees do
114
+ x = rand(0..MAP_WIDTH * TILE_SIZE)
115
+ y = rand(0..MAP_HEIGHT * TILE_SIZE)
116
+ n = noises[x * 0.001, y * 0.001]
117
+ n = contrast.call(n)
118
+ if tile_at(x, y) == @grass && n > 0.5
119
+ Tree.new(@object_pool, x, y, n * 2 - 1)
120
+ trees += 1
121
+ end
122
+ end
123
+ end
124
+
125
+ def generate_boxes
126
+ boxes = 0
127
+ target_boxes = rand(50..200)
128
+ while boxes < target_boxes do
129
+ x = rand(0..MAP_WIDTH * TILE_SIZE)
130
+ y = rand(0..MAP_HEIGHT * TILE_SIZE)
131
+ if tile_at(x, y) != @water
132
+ Box.new(@object_pool, x, y)
133
+ boxes += 1
134
+ end
135
+ end
136
+ end
137
+
138
+ def generate_powerups
139
+ pups = 0
140
+ target_pups = rand(20..30)
141
+ while pups < target_pups do
142
+ x = rand(0..MAP_WIDTH * TILE_SIZE)
143
+ y = rand(0..MAP_HEIGHT * TILE_SIZE)
144
+ if tile_at(x, y) != @water &&
145
+ @object_pool.nearby_point(x, y, 150).empty?
146
+ random_powerup.new(@object_pool, x, y)
147
+ pups += 1
148
+ end
149
+ end
150
+ end
151
+
152
+ def random_powerup
153
+ [HealthPowerup,
154
+ RepairPowerup,
155
+ FireRatePowerup,
156
+ TankSpeedPowerup].sample
157
+ end
158
+
159
+ def choose_tile(val)
160
+ case val
161
+ when 0.0..0.3 # 30% chance
162
+ @water
163
+ when 0.3..0.5 # 20% chance, water edges
164
+ @sand
165
+ else # 50% chance
166
+ @grass
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def find_spawn_point
173
+ while true
174
+ x = rand(0..MAP_WIDTH * TILE_SIZE)
175
+ y = rand(0..MAP_HEIGHT * TILE_SIZE)
176
+ if can_move_to?(x, y) &&
177
+ @object_pool.nearby_point(x, y, 150).empty?
178
+ return [x, y]
179
+ end
180
+ end
181
+ end
182
+
183
+ end