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