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,100 @@
|
|
1
|
+
class PlayerInput < Component
|
2
|
+
# Dark green
|
3
|
+
NAME_COLOR = Gosu::Color.argb(0xee084408)
|
4
|
+
attr_reader :stats
|
5
|
+
|
6
|
+
def initialize(name, camera, object_pool)
|
7
|
+
super(nil)
|
8
|
+
@name = name
|
9
|
+
@stats = Stats.new(name)
|
10
|
+
@camera = camera
|
11
|
+
@object_pool = object_pool
|
12
|
+
end
|
13
|
+
|
14
|
+
def control(obj)
|
15
|
+
self.object = obj
|
16
|
+
obj.components << self
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_collision(with)
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_damage(amount)
|
23
|
+
@stats.add_damage(amount)
|
24
|
+
end
|
25
|
+
|
26
|
+
def update
|
27
|
+
return respawn if object.health.dead?
|
28
|
+
d_x, d_y = @camera.target_delta_on_screen
|
29
|
+
atan = Math.atan2(($window.width / 2) - d_x - $window.mouse_x,
|
30
|
+
($window.height / 2) - d_y - $window.mouse_y)
|
31
|
+
object.gun_angle = -atan * 180 / Math::PI
|
32
|
+
motion_buttons = [Gosu::KbW, Gosu::KbS, Gosu::KbA, Gosu::KbD]
|
33
|
+
|
34
|
+
if any_button_down?(*motion_buttons)
|
35
|
+
object.throttle_down = true
|
36
|
+
object.physics.change_direction(
|
37
|
+
change_angle(object.direction, *motion_buttons))
|
38
|
+
else
|
39
|
+
object.throttle_down = false
|
40
|
+
end
|
41
|
+
|
42
|
+
if Utils.button_down?(Gosu::MsLeft)
|
43
|
+
object.shoot(*@camera.mouse_coords)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw(viewport)
|
48
|
+
@name_image ||= Gosu::Image.from_text(
|
49
|
+
$window, @name, Gosu.default_font_name, 20)
|
50
|
+
@name_image.draw(
|
51
|
+
x - @name_image.width / 2 - 1,
|
52
|
+
y + object.graphics.height / 2, 100,
|
53
|
+
1, 1, Gosu::Color::WHITE)
|
54
|
+
@name_image.draw(
|
55
|
+
x - @name_image.width / 2,
|
56
|
+
y + object.graphics.height / 2, 100,
|
57
|
+
1, 1, NAME_COLOR)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def respawn
|
63
|
+
if object.health.should_respawn?
|
64
|
+
object.health.restore
|
65
|
+
object.move(*@object_pool.map.spawn_point)
|
66
|
+
@camera.x, @camera.y = x, y
|
67
|
+
PlayerSounds.respawn(object, @camera)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def any_button_down?(*buttons)
|
72
|
+
buttons.each do |b|
|
73
|
+
return true if Utils.button_down?(b)
|
74
|
+
end
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def change_angle(previous_angle, up, down, right, left)
|
79
|
+
if Utils.button_down?(up)
|
80
|
+
angle = 0.0
|
81
|
+
angle += 45.0 if Utils.button_down?(left)
|
82
|
+
angle -= 45.0 if Utils.button_down?(right)
|
83
|
+
elsif Utils.button_down?(down)
|
84
|
+
angle = 180.0
|
85
|
+
angle -= 45.0 if Utils.button_down?(left)
|
86
|
+
angle += 45.0 if Utils.button_down?(right)
|
87
|
+
elsif Utils.button_down?(left)
|
88
|
+
angle = 90.0
|
89
|
+
angle += 45.0 if Utils.button_down?(up)
|
90
|
+
angle -= 45.0 if Utils.button_down?(down)
|
91
|
+
elsif Utils.button_down?(right)
|
92
|
+
angle = 270.0
|
93
|
+
angle -= 45.0 if Utils.button_down?(up)
|
94
|
+
angle += 45.0 if Utils.button_down?(down)
|
95
|
+
end
|
96
|
+
angle = (angle + 360) % 360 if angle && angle < 0
|
97
|
+
(angle || previous_angle)
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class PlayerSounds
|
2
|
+
class << self
|
3
|
+
def respawn(object, camera)
|
4
|
+
volume, pan = Utils.volume_and_pan(object, camera)
|
5
|
+
respawn_sound.play(object.object_id, pan, volume * 0.5)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def respawn_sound
|
11
|
+
@@respawn ||= StereoSample.new(
|
12
|
+
$window, Utils.media_path('respawn.wav'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class PowerupGraphics < Component
|
2
|
+
def initialize(object, type)
|
3
|
+
super(object)
|
4
|
+
@type = type
|
5
|
+
end
|
6
|
+
|
7
|
+
def draw(viewport)
|
8
|
+
image.draw(x - 12, y - 12, 1)
|
9
|
+
Utils.mark_corners(object.box) if $debug
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def image
|
15
|
+
@image ||= images.frame("#{@type}.png")
|
16
|
+
end
|
17
|
+
|
18
|
+
def images
|
19
|
+
@@images ||= Gosu::TexturePacker.load_json(
|
20
|
+
$window, Utils.media_path('pickups.json'))
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class PowerupSounds
|
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('powerup.mp3'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class TankGraphics < Component
|
2
|
+
def initialize(game_object)
|
3
|
+
super(game_object)
|
4
|
+
@body_normal = units.frame('tank1_body.png')
|
5
|
+
@shadow_normal = units.frame('tank1_body_shadow.png')
|
6
|
+
@gun_normal = units.frame('tank1_dualgun.png')
|
7
|
+
@body_dead = units.frame('tank1_body_destroyed.png')
|
8
|
+
@shadow_dead = units.frame('tank1_body_destroyed_shadow.png')
|
9
|
+
@gun_dead = nil
|
10
|
+
update
|
11
|
+
end
|
12
|
+
|
13
|
+
def update
|
14
|
+
if object && object.health.dead?
|
15
|
+
@body = @body_dead
|
16
|
+
@gun = @gun_dead
|
17
|
+
@shadow = @shadow_dead
|
18
|
+
else
|
19
|
+
@body = @body_normal
|
20
|
+
@gun = @gun_normal
|
21
|
+
@shadow = @shadow_normal
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw(viewport)
|
26
|
+
@shadow.draw_rot(x - 1, y - 1, 0, object.direction)
|
27
|
+
@body.draw_rot(x, y, 1, object.direction)
|
28
|
+
@gun.draw_rot(x, y, 2, object.gun_angle) if @gun
|
29
|
+
Utils.mark_corners(object.box) if $debug
|
30
|
+
end
|
31
|
+
|
32
|
+
def width
|
33
|
+
@body.width
|
34
|
+
end
|
35
|
+
|
36
|
+
def height
|
37
|
+
@body.height
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def units
|
43
|
+
@@units = Gosu::TexturePacker.load_json(
|
44
|
+
$window, Utils.media_path('ground_units.json'), :precise)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class TankHealth < Health
|
2
|
+
RESPAWN_DELAY = 5000
|
3
|
+
attr_accessor :health
|
4
|
+
|
5
|
+
def initialize(object, object_pool)
|
6
|
+
super(object, object_pool, 100, true)
|
7
|
+
end
|
8
|
+
|
9
|
+
def should_respawn?
|
10
|
+
if @death_time
|
11
|
+
Gosu.milliseconds - @death_time > RESPAWN_DELAY
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def draw?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def after_death(cause)
|
22
|
+
@death_time = Gosu.milliseconds
|
23
|
+
object.reset_modifiers
|
24
|
+
object.input.stats.add_death
|
25
|
+
kill = object != cause ? 1 : -1
|
26
|
+
cause.input.stats.add_kill(kill)
|
27
|
+
Thread.new do
|
28
|
+
sleep(rand(0.1..0.3))
|
29
|
+
Explosion.new(@object_pool, x, y, cause)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
class TankPhysics < Component
|
2
|
+
attr_accessor :speed, :in_collision, :collides_with
|
3
|
+
|
4
|
+
def initialize(game_object, object_pool)
|
5
|
+
super(game_object)
|
6
|
+
@object_pool = object_pool
|
7
|
+
@map = object_pool.map
|
8
|
+
@speed = 0.0
|
9
|
+
end
|
10
|
+
|
11
|
+
def can_move_to?(x, y)
|
12
|
+
old_x, old_y = object.x, object.y
|
13
|
+
object.move(x, y)
|
14
|
+
return false unless @map.can_move_to?(x, y)
|
15
|
+
@object_pool.nearby(object, 100).each do |obj|
|
16
|
+
next if obj.class == Bullet && obj.source == object
|
17
|
+
if collides_with_poly?(obj.box)
|
18
|
+
if obj.is_a? Powerup
|
19
|
+
obj.on_collision(object)
|
20
|
+
else
|
21
|
+
@collides_with = obj
|
22
|
+
# Allow to get unstuck
|
23
|
+
old_distance = Utils.distance_between(
|
24
|
+
obj.x, obj.y, old_x, old_y)
|
25
|
+
new_distance = Utils.distance_between(
|
26
|
+
obj.x, obj.y, x, y)
|
27
|
+
return false if new_distance < old_distance
|
28
|
+
end
|
29
|
+
else
|
30
|
+
@collides_with = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
true
|
34
|
+
ensure
|
35
|
+
object.move(old_x, old_y)
|
36
|
+
end
|
37
|
+
|
38
|
+
def change_direction(new_direction)
|
39
|
+
change = (new_direction - object.direction + 360) % 360
|
40
|
+
change = 360 - change if change > 180
|
41
|
+
if change > 90
|
42
|
+
@speed = 0
|
43
|
+
elsif change > 45
|
44
|
+
@speed *= 0.33
|
45
|
+
elsif change > 0
|
46
|
+
@speed *= 0.66
|
47
|
+
end
|
48
|
+
object.direction = new_direction % 360
|
49
|
+
end
|
50
|
+
|
51
|
+
def moving?
|
52
|
+
@speed > 0
|
53
|
+
end
|
54
|
+
|
55
|
+
def box_height
|
56
|
+
@box_height ||= object.graphics.height
|
57
|
+
end
|
58
|
+
|
59
|
+
def box_width
|
60
|
+
@box_width ||= object.graphics.width
|
61
|
+
end
|
62
|
+
|
63
|
+
# Tank box looks like H. Vertices:
|
64
|
+
# 1 2 5 6
|
65
|
+
# 3 4
|
66
|
+
#
|
67
|
+
# 10 9
|
68
|
+
# 12 11 8 7
|
69
|
+
def box
|
70
|
+
w = box_width / 2 - 1
|
71
|
+
h = box_height / 2 - 1
|
72
|
+
tw = 8 # track width
|
73
|
+
fd = 8 # front depth
|
74
|
+
rd = 6 # rear depth
|
75
|
+
Utils.rotate(object.direction, x, y,
|
76
|
+
x + w, y + h, #1
|
77
|
+
x + w - tw, y + h, #2
|
78
|
+
x + w - tw, y + h - fd, #3
|
79
|
+
|
80
|
+
x - w + tw, y + h - fd, #4
|
81
|
+
x - w + tw, y + h, #5
|
82
|
+
x - w, y + h, #6
|
83
|
+
|
84
|
+
x - w, y - h, #7
|
85
|
+
x - w + tw, y - h, #8
|
86
|
+
x - w + tw, y - h + rd, #9
|
87
|
+
|
88
|
+
x + w - tw, y - h + rd, #10
|
89
|
+
x + w - tw, y - h, #11
|
90
|
+
x + w, y - h, #12
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def update
|
95
|
+
if object.throttle_down && !object.health.dead?
|
96
|
+
accelerate
|
97
|
+
else
|
98
|
+
decelerate
|
99
|
+
end
|
100
|
+
if @speed > 0
|
101
|
+
new_x, new_y = x, y
|
102
|
+
speed = apply_movement_penalty(@speed)
|
103
|
+
shift = Utils.adjust_speed(speed) * object.speed_modifier
|
104
|
+
case @object.direction.to_i
|
105
|
+
when 0
|
106
|
+
new_y -= shift
|
107
|
+
when 45
|
108
|
+
new_x += shift
|
109
|
+
new_y -= shift
|
110
|
+
when 90
|
111
|
+
new_x += shift
|
112
|
+
when 135
|
113
|
+
new_x += shift
|
114
|
+
new_y += shift
|
115
|
+
when 180
|
116
|
+
new_y += shift
|
117
|
+
when 225
|
118
|
+
new_y += shift
|
119
|
+
new_x -= shift
|
120
|
+
when 270
|
121
|
+
new_x -= shift
|
122
|
+
when 315
|
123
|
+
new_x -= shift
|
124
|
+
new_y -= shift
|
125
|
+
end
|
126
|
+
if can_move_to?(new_x, new_y)
|
127
|
+
object.move(new_x, new_y)
|
128
|
+
@in_collision = false
|
129
|
+
else
|
130
|
+
object.on_collision(@collides_with)
|
131
|
+
@speed = 0.0
|
132
|
+
@in_collision = true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def apply_movement_penalty(speed)
|
140
|
+
speed * (1.0 - @map.movement_penalty(x, y))
|
141
|
+
end
|
142
|
+
|
143
|
+
def accelerate
|
144
|
+
@speed += 0.08 if @speed < 5
|
145
|
+
end
|
146
|
+
|
147
|
+
def decelerate
|
148
|
+
if @speed > 0
|
149
|
+
@speed = [@speed - 0.5, 0].max
|
150
|
+
elsif @speed < 0
|
151
|
+
@speed = [@speed + 0.5, 0].min
|
152
|
+
end
|
153
|
+
damp_speed
|
154
|
+
end
|
155
|
+
|
156
|
+
def damp_speed
|
157
|
+
@speed = 0 if @speed < 0.01
|
158
|
+
end
|
159
|
+
|
160
|
+
def collides_with_poly?(poly)
|
161
|
+
if poly
|
162
|
+
if poly.size == 2
|
163
|
+
px, py = poly
|
164
|
+
return Utils.point_in_poly(px, py, *box)
|
165
|
+
end
|
166
|
+
poly.each_slice(2) do |x, y|
|
167
|
+
return true if Utils.point_in_poly(x, y, *box)
|
168
|
+
end
|
169
|
+
box.each_slice(2) do |x, y|
|
170
|
+
return true if Utils.point_in_poly(x, y, *poly)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
false
|
174
|
+
end
|
175
|
+
|
176
|
+
def collides_with_point?(x, y)
|
177
|
+
Utils.point_in_poly(x, y, box)
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class TankSounds < Component
|
2
|
+
def initialize(object, object_pool)
|
3
|
+
super(object)
|
4
|
+
@object_pool = object_pool
|
5
|
+
end
|
6
|
+
|
7
|
+
def update
|
8
|
+
id = object.object_id
|
9
|
+
if object.physics.moving?
|
10
|
+
move_volume = Utils.volume(
|
11
|
+
object, @object_pool.camera)
|
12
|
+
pan = Utils.pan(object, @object_pool.camera)
|
13
|
+
if driving_sound.paused?(id)
|
14
|
+
driving_sound.resume(id)
|
15
|
+
elsif driving_sound.stopped?(id)
|
16
|
+
driving_sound.play(id, pan, 0.5, 1, true)
|
17
|
+
end
|
18
|
+
driving_sound.volume_and_pan(id, move_volume * 0.5, pan)
|
19
|
+
else
|
20
|
+
if driving_sound.playing?(id)
|
21
|
+
driving_sound.pause(id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def collide
|
27
|
+
vol, pan = Utils.volume_and_pan(
|
28
|
+
object, @object_pool.camera)
|
29
|
+
crash_sound.play(self.object_id, pan, vol, 1, false)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def driving_sound
|
35
|
+
@@driving_sound ||= StereoSample.new(
|
36
|
+
$window, Utils.media_path('tank_driving.mp3'))
|
37
|
+
end
|
38
|
+
|
39
|
+
def crash_sound
|
40
|
+
@@crash_sound ||= StereoSample.new(
|
41
|
+
$window, Utils.media_path('metal_interaction2.wav'))
|
42
|
+
end
|
43
|
+
end
|