tank_island 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,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
|
data/lib/entities/hud.rb
ADDED
@@ -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
|
data/lib/entities/map.rb
ADDED
@@ -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
|