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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +58 -0
- data/Rakefile +2 -0
- data/bin/tank_island +30 -0
- data/lib/entities/box.rb +28 -0
- data/lib/entities/bullet.rb +26 -0
- data/lib/entities/camera.rb +113 -0
- data/lib/entities/components/ai/gun.rb +114 -0
- data/lib/entities/components/ai/tank_chasing_state.rb +30 -0
- data/lib/entities/components/ai/tank_fighting_state.rb +47 -0
- data/lib/entities/components/ai/tank_fleeing_state.rb +50 -0
- data/lib/entities/components/ai/tank_motion_fsm.rb +102 -0
- data/lib/entities/components/ai/tank_motion_state.rb +84 -0
- data/lib/entities/components/ai/tank_navigating_state.rb +34 -0
- data/lib/entities/components/ai/tank_roaming_state.rb +83 -0
- data/lib/entities/components/ai/tank_stuck_state.rb +45 -0
- data/lib/entities/components/ai/vision.rb +109 -0
- data/lib/entities/components/ai_input.rb +70 -0
- data/lib/entities/components/box_graphics.rb +39 -0
- data/lib/entities/components/bullet_graphics.rb +13 -0
- data/lib/entities/components/bullet_physics.rb +65 -0
- data/lib/entities/components/bullet_sounds.rb +15 -0
- data/lib/entities/components/component.rb +32 -0
- data/lib/entities/components/damage_graphics.rb +20 -0
- data/lib/entities/components/explosion_graphics.rb +43 -0
- data/lib/entities/components/explosion_sounds.rb +16 -0
- data/lib/entities/components/health.rb +87 -0
- data/lib/entities/components/player_input.rb +100 -0
- data/lib/entities/components/player_sounds.rb +16 -0
- data/lib/entities/components/powerup_graphics.rb +22 -0
- data/lib/entities/components/powerup_sounds.rb +15 -0
- data/lib/entities/components/tank_graphics.rb +46 -0
- data/lib/entities/components/tank_health.rb +32 -0
- data/lib/entities/components/tank_physics.rb +179 -0
- data/lib/entities/components/tank_sounds.rb +43 -0
- data/lib/entities/components/tree_graphics.rb +69 -0
- data/lib/entities/damage.rb +26 -0
- data/lib/entities/explosion.rb +34 -0
- data/lib/entities/game_object.rb +54 -0
- data/lib/entities/hud.rb +79 -0
- data/lib/entities/map.rb +183 -0
- data/lib/entities/object_pool.rb +59 -0
- data/lib/entities/powerups/fire_rate_powerup.rb +14 -0
- data/lib/entities/powerups/health_powerup.rb +12 -0
- data/lib/entities/powerups/powerup.rb +35 -0
- data/lib/entities/powerups/powerup_respawn_queue.rb +23 -0
- data/lib/entities/powerups/repair_powerup.rb +14 -0
- data/lib/entities/powerups/tank_speed_powerup.rb +14 -0
- data/lib/entities/radar.rb +62 -0
- data/lib/entities/score_display.rb +35 -0
- data/lib/entities/tank.rb +64 -0
- data/lib/entities/tree.rb +18 -0
- data/lib/game_states/demo_state.rb +49 -0
- data/lib/game_states/game_state.rb +27 -0
- data/lib/game_states/menu_state.rb +60 -0
- data/lib/game_states/pause_state.rb +61 -0
- data/lib/game_states/play_state.rb +119 -0
- data/lib/misc/axis_aligned_bounding_box.rb +33 -0
- data/lib/misc/game_window.rb +30 -0
- data/lib/misc/names.rb +13 -0
- data/lib/misc/quad_tree.rb +91 -0
- data/lib/misc/stats.rb +55 -0
- data/lib/misc/stereo_sample.rb +96 -0
- data/lib/misc/utils.rb +145 -0
- data/media/armalite_rifle.ttf +0 -0
- data/media/boxes_barrels.json +60 -0
- data/media/boxes_barrels.png +0 -0
- data/media/bullet.png +0 -0
- data/media/c_dot.png +0 -0
- data/media/country_field.png +0 -0
- data/media/crash.ogg +0 -0
- data/media/damage1.png +0 -0
- data/media/damage2.png +0 -0
- data/media/damage3.png +0 -0
- data/media/damage4.png +0 -0
- data/media/decor.json +516 -0
- data/media/decor.png +0 -0
- data/media/decor.psd +0 -0
- data/media/explosion.mp3 +0 -0
- data/media/explosion.png +0 -0
- data/media/fire.mp3 +0 -0
- data/media/ground.json +492 -0
- data/media/ground.png +0 -0
- data/media/ground_units.json +900 -0
- data/media/ground_units.png +0 -0
- data/media/menu_music.mp3 +0 -0
- data/media/metal_interaction2.wav +0 -0
- data/media/names.txt +279 -0
- data/media/pickups.json +68 -0
- data/media/pickups.png +0 -0
- data/media/powerup.mp3 +0 -0
- data/media/respawn.wav +0 -0
- data/media/tank_driving.mp3 +0 -0
- data/media/top_secret.ttf +0 -0
- data/media/trees.png +0 -0
- data/media/trees_packed.json +388 -0
- data/media/trees_packed.png +0 -0
- data/media/water.png +0 -0
- data/spec/misc/aabb_spec.rb +85 -0
- data/spec/misc/quad_tree_spec.rb +137 -0
- data/tank_island.gemspec +29 -0
- metadata +223 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
class ObjectPool
|
2
|
+
attr_accessor :map, :camera, :objects, :powerup_respawn_queue
|
3
|
+
|
4
|
+
def size
|
5
|
+
@objects.size
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(box)
|
9
|
+
@tree = QuadTree.new(box)
|
10
|
+
@powerup_respawn_queue = PowerupRespawnQueue.new
|
11
|
+
@objects = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(object)
|
15
|
+
@objects << object
|
16
|
+
@tree.insert(object)
|
17
|
+
end
|
18
|
+
|
19
|
+
def tree_remove(object)
|
20
|
+
@tree.remove(object)
|
21
|
+
end
|
22
|
+
|
23
|
+
def tree_insert(object)
|
24
|
+
@tree.insert(object)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_all
|
28
|
+
@objects.each(&:update)
|
29
|
+
@objects.reject! do |o|
|
30
|
+
if o.removable?
|
31
|
+
@tree.remove(o)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@powerup_respawn_queue.respawn(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def nearby_point(cx, cy, max_distance, object = nil)
|
39
|
+
hx, hy = cx + max_distance, cy + max_distance
|
40
|
+
# Fast, rough results
|
41
|
+
results = @tree.query_range(
|
42
|
+
AxisAlignedBoundingBox.new([cx, cy], [hx, hy]))
|
43
|
+
# Sift through to select fine-grained results
|
44
|
+
results.select do |o|
|
45
|
+
o != object &&
|
46
|
+
Utils.distance_between(
|
47
|
+
o.x, o.y, cx, cy) <= max_distance
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def nearby(object, max_distance)
|
52
|
+
cx, cy = object.location
|
53
|
+
nearby_point(cx, cy, max_distance, object)
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_range(box)
|
57
|
+
@tree.query_range(box)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Powerup < GameObject
|
2
|
+
def initialize(object_pool, x, y)
|
3
|
+
super
|
4
|
+
PowerupGraphics.new(self, graphics)
|
5
|
+
end
|
6
|
+
|
7
|
+
def box
|
8
|
+
[x - 8, y - 8,
|
9
|
+
x + 8, y - 8,
|
10
|
+
x + 8, y + 8,
|
11
|
+
x - 8, y + 8]
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_collision(object)
|
15
|
+
if pickup(object)
|
16
|
+
PowerupSounds.play(object, object_pool.camera)
|
17
|
+
remove
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def pickup(object)
|
22
|
+
# override and implement application
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove
|
26
|
+
object_pool.powerup_respawn_queue.enqueue(
|
27
|
+
respawn_delay,
|
28
|
+
self.class, x, y)
|
29
|
+
mark_for_removal
|
30
|
+
end
|
31
|
+
|
32
|
+
def respawn_delay
|
33
|
+
30
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class PowerupRespawnQueue
|
2
|
+
RESPAWN_DELAY = 1000
|
3
|
+
def initialize
|
4
|
+
@respawn_queue = {}
|
5
|
+
@last_respawn = Gosu.milliseconds
|
6
|
+
end
|
7
|
+
|
8
|
+
def enqueue(delay_seconds, type, x, y)
|
9
|
+
respawn_at = Gosu.milliseconds + delay_seconds * 1000
|
10
|
+
@respawn_queue[respawn_at.to_i] = [type, x, y]
|
11
|
+
end
|
12
|
+
|
13
|
+
def respawn(object_pool)
|
14
|
+
now = Gosu.milliseconds
|
15
|
+
return if now - @last_respawn < RESPAWN_DELAY
|
16
|
+
@respawn_queue.keys.each do |k|
|
17
|
+
next if k > now # not yet
|
18
|
+
type, x, y = @respawn_queue.delete(k)
|
19
|
+
type.new(object_pool, x, y)
|
20
|
+
end
|
21
|
+
@last_respawn = now
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Radar
|
2
|
+
UPDATE_FREQUENCY = 1000
|
3
|
+
WIDTH = 150
|
4
|
+
HEIGHT = 100
|
5
|
+
PADDING = 10
|
6
|
+
# Black with 33% transparency
|
7
|
+
BACKGROUND = Gosu::Color.new(255 * 0.33, 0, 0, 0)
|
8
|
+
attr_accessor :target
|
9
|
+
|
10
|
+
def initialize(object_pool, target)
|
11
|
+
@object_pool = object_pool
|
12
|
+
@target = target
|
13
|
+
@last_update = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
if Gosu.milliseconds - @last_update > UPDATE_FREQUENCY
|
18
|
+
@nearby = nil
|
19
|
+
end
|
20
|
+
@nearby ||= @object_pool.nearby(@target, 2000).select do |o|
|
21
|
+
o.class == Tank && !o.health.dead?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw
|
26
|
+
x1, x2, y1, y2 = radar_coords
|
27
|
+
$window.draw_quad(
|
28
|
+
x1, y1, BACKGROUND,
|
29
|
+
x2, y1, BACKGROUND,
|
30
|
+
x2, y2, BACKGROUND,
|
31
|
+
x1, y2, BACKGROUND,
|
32
|
+
200)
|
33
|
+
draw_tank(@target, Gosu::Color::GREEN)
|
34
|
+
@nearby && @nearby.each do |t|
|
35
|
+
draw_tank(t, Gosu::Color::RED)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def draw_tank(tank, color)
|
42
|
+
x1, x2, y1, y2 = radar_coords
|
43
|
+
tx = x1 + WIDTH / 2 + (tank.x - @target.x) / 20
|
44
|
+
ty = y1 + HEIGHT / 2 + (tank.y - @target.y) / 20
|
45
|
+
if (x1..x2).include?(tx) && (y1..y2).include?(ty)
|
46
|
+
$window.draw_quad(
|
47
|
+
tx - 2, ty - 2, color,
|
48
|
+
tx + 2, ty - 2, color,
|
49
|
+
tx + 2, ty + 2, color,
|
50
|
+
tx - 2, ty + 2, color,
|
51
|
+
300)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def radar_coords
|
56
|
+
x1 = $window.width - WIDTH - PADDING
|
57
|
+
x2 = $window.width - PADDING
|
58
|
+
y1 = $window.height - HEIGHT - PADDING
|
59
|
+
y2 = $window.height - PADDING
|
60
|
+
[x1, x2, y1, y2]
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class ScoreDisplay
|
2
|
+
def initialize(object_pool, font_size=30)
|
3
|
+
@font_size = font_size
|
4
|
+
tanks = object_pool.objects.select do |o|
|
5
|
+
o.class == Tank
|
6
|
+
end
|
7
|
+
stats = tanks.map(&:input).map(&:stats)
|
8
|
+
stats.sort! do |stat1, stat2|
|
9
|
+
stat2.kills <=> stat1.kills
|
10
|
+
end
|
11
|
+
create_stats_image(stats)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_stats_image(stats)
|
15
|
+
text = stats.map do |stat|
|
16
|
+
"#{stat.kills}: #{stat.name} "
|
17
|
+
end.join("\n")
|
18
|
+
@stats_image = Gosu::Image.from_text(
|
19
|
+
$window, text, Utils.main_font, @font_size)
|
20
|
+
end
|
21
|
+
|
22
|
+
def draw
|
23
|
+
@stats_image.draw(
|
24
|
+
$window.width / 2 - @stats_image.width / 2,
|
25
|
+
$window.height / 4 + 30,
|
26
|
+
1000)
|
27
|
+
end
|
28
|
+
|
29
|
+
def draw_top_right
|
30
|
+
@stats_image.draw(
|
31
|
+
$window.width - @stats_image.width - 20,
|
32
|
+
20,
|
33
|
+
1000)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Tank < GameObject
|
2
|
+
SHOOT_DELAY = 500
|
3
|
+
attr_accessor :throttle_down, :direction,
|
4
|
+
:gun_angle, :sounds, :physics, :graphics, :health, :input,
|
5
|
+
:fire_rate_modifier, :speed_modifier
|
6
|
+
|
7
|
+
def initialize(object_pool, input)
|
8
|
+
x, y = object_pool.map.spawn_point
|
9
|
+
super(object_pool, x, y)
|
10
|
+
@input = input
|
11
|
+
@input.control(self)
|
12
|
+
@physics = TankPhysics.new(self, object_pool)
|
13
|
+
@sounds = TankSounds.new(self, object_pool)
|
14
|
+
@health = TankHealth.new(self, object_pool)
|
15
|
+
@graphics = TankGraphics.new(self)
|
16
|
+
@direction = rand(0..7) * 45
|
17
|
+
@gun_angle = rand(0..360)
|
18
|
+
reset_modifiers
|
19
|
+
end
|
20
|
+
|
21
|
+
def box
|
22
|
+
@physics.box
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_collision(object)
|
26
|
+
return unless object
|
27
|
+
# Avoid recursion
|
28
|
+
if object.class == Tank
|
29
|
+
# Inform AI about hit
|
30
|
+
object.input.on_collision(object)
|
31
|
+
else
|
32
|
+
# Call only on non-tanks to avoid recursion
|
33
|
+
object.on_collision(self)
|
34
|
+
end
|
35
|
+
# Bullets should not slow Tanks down
|
36
|
+
if object.class != Bullet
|
37
|
+
@sounds.collide if @physics.speed > 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def shoot(target_x, target_y)
|
42
|
+
if can_shoot?
|
43
|
+
@last_shot = Gosu.milliseconds
|
44
|
+
Bullet.new(object_pool, @x, @y, target_x, target_y).fire(
|
45
|
+
self, 1500)
|
46
|
+
input.stats.add_shot
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def can_shoot?
|
51
|
+
Gosu.milliseconds - (@last_shot || 0) >
|
52
|
+
(SHOOT_DELAY / @fire_rate_modifier)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_modifiers
|
56
|
+
@fire_rate_modifier = 1
|
57
|
+
@speed_modifier = 1
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
"Tank [#{@health.health}@#{@x}:#{@y}@#{@physics.speed.round(2)}px/tick]#{@input.stats}"
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Tree < GameObject
|
2
|
+
attr_reader :health, :graphics
|
3
|
+
|
4
|
+
def initialize(object_pool, x, y, seed)
|
5
|
+
super(object_pool, x, y)
|
6
|
+
@graphics = TreeGraphics.new(self, seed)
|
7
|
+
@health = Health.new(self, object_pool, 30, false)
|
8
|
+
@angle = rand(-15..15)
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_collision(object)
|
12
|
+
@graphics.shake(object.direction)
|
13
|
+
end
|
14
|
+
|
15
|
+
def box
|
16
|
+
[@x, @y]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class DemoState < PlayState
|
2
|
+
attr_accessor :tank
|
3
|
+
|
4
|
+
def enter
|
5
|
+
# Prevent reactivating HUD
|
6
|
+
end
|
7
|
+
|
8
|
+
def update
|
9
|
+
super
|
10
|
+
@score_display = ScoreDisplay.new(
|
11
|
+
object_pool, 20)
|
12
|
+
end
|
13
|
+
|
14
|
+
def draw
|
15
|
+
super
|
16
|
+
@score_display.draw_top_right
|
17
|
+
end
|
18
|
+
|
19
|
+
def button_down(id)
|
20
|
+
super
|
21
|
+
if id == Gosu::KbSpace
|
22
|
+
target_tank = @tanks.reject do |t|
|
23
|
+
t == @camera.target
|
24
|
+
end.sample
|
25
|
+
switch_to_tank(target_tank)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def create_tanks(amount)
|
32
|
+
@map.spawn_points(amount * 3)
|
33
|
+
@tanks = []
|
34
|
+
amount.times do |i|
|
35
|
+
@tanks << Tank.new(@object_pool, AiInput.new(
|
36
|
+
@names.random, @object_pool))
|
37
|
+
end
|
38
|
+
target_tank = @tanks.sample
|
39
|
+
@hud = HUD.new(@object_pool, target_tank)
|
40
|
+
@hud.active = false
|
41
|
+
switch_to_tank(target_tank)
|
42
|
+
end
|
43
|
+
|
44
|
+
def switch_to_tank(tank)
|
45
|
+
@camera.target = tank
|
46
|
+
@hud.player = tank
|
47
|
+
self.tank = tank
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class GameState
|
2
|
+
|
3
|
+
def self.switch(new_state)
|
4
|
+
$window.state && $window.state.leave
|
5
|
+
$window.state = new_state
|
6
|
+
new_state.enter
|
7
|
+
end
|
8
|
+
|
9
|
+
def enter
|
10
|
+
end
|
11
|
+
|
12
|
+
def leave
|
13
|
+
end
|
14
|
+
|
15
|
+
def draw
|
16
|
+
end
|
17
|
+
|
18
|
+
def update
|
19
|
+
end
|
20
|
+
|
21
|
+
def needs_redraw?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def button_down(id)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
class MenuState < GameState
|
3
|
+
include Singleton
|
4
|
+
attr_accessor :play_state
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@message = Gosu::Image.from_text(
|
8
|
+
$window, "Tank Island",
|
9
|
+
Utils.title_font, 60)
|
10
|
+
end
|
11
|
+
|
12
|
+
def enter
|
13
|
+
music.play(true)
|
14
|
+
music.volume = 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def leave
|
18
|
+
music.volume = 0
|
19
|
+
music.stop
|
20
|
+
end
|
21
|
+
|
22
|
+
def music
|
23
|
+
@@music ||= Gosu::Song.new(
|
24
|
+
$window, Utils.media_path('menu_music.mp3'))
|
25
|
+
end
|
26
|
+
|
27
|
+
def update
|
28
|
+
text = "Q: Quit\nN: New Game\nD: Demo"
|
29
|
+
text << "\nC: Continue" if @play_state
|
30
|
+
@info = Gosu::Image.from_text(
|
31
|
+
$window, text,
|
32
|
+
Utils.main_font, 30)
|
33
|
+
end
|
34
|
+
|
35
|
+
def draw
|
36
|
+
@message.draw(
|
37
|
+
$window.width / 2 - @message.width / 2,
|
38
|
+
$window.height / 2 - @message.height / 2,
|
39
|
+
10)
|
40
|
+
@info.draw(
|
41
|
+
$window.width / 2 - @info.width / 2,
|
42
|
+
$window.height / 2 - @info.height / 2 + 100,
|
43
|
+
10)
|
44
|
+
end
|
45
|
+
|
46
|
+
def button_down(id)
|
47
|
+
$window.close if id == Gosu::KbQ
|
48
|
+
if id == Gosu::KbC && @play_state
|
49
|
+
GameState.switch(@play_state)
|
50
|
+
end
|
51
|
+
if id == Gosu::KbN
|
52
|
+
@play_state = PlayState.new
|
53
|
+
GameState.switch(@play_state)
|
54
|
+
end
|
55
|
+
if id == Gosu::KbD
|
56
|
+
@play_state = DemoState.new
|
57
|
+
GameState.switch(@play_state)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|