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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d532027c65a76a3a675c7bddbd7ed6699bf74880
4
+ data.tar.gz: 90459a44a1e0e4f91796ac0188e54f75ac64277a
5
+ SHA512:
6
+ metadata.gz: bd1d1bd1afed378b9655e77d6b7ee5972d7c37aa0e904cecdbee7f2c35b93075c145b4ef37ef5241026f994da48e98149df7f925c9538fed83eb336c4df1d657
7
+ data.tar.gz: b85eed9ea5b0fd7e7c9ba709b502a3e1a489c92399bd0f5daea7f2c47202f5be8b09f71a537edc8bf8ee3b6d6f630390836cde81a5ac54df31869f631fe8811b
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1 @@
1
+ 2.1.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tank_island.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Tomas Varaneckas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,58 @@
1
+ # Tank Island
2
+
3
+ Tank Island is an open source 2D top down shooter game that was created with Ruby using
4
+ [Gosu](http://www.libgosu.org) game development library while writing
5
+ [this book](https://leanpub.com/developing-games-with-ruby/read).
6
+
7
+ ## Screenshots
8
+
9
+ ![Tank Island Game](https://dl.dropboxusercontent.com/u/176100/tank_island/screen1.png)
10
+
11
+ ![Tank Island Game](https://dl.dropboxusercontent.com/u/176100/tank_island/screen2.png)
12
+
13
+ [Gameplay video on YouTube](https://www.youtube.com/watch?v=PpbnCUBrXXU)
14
+
15
+ ## Book: Developing Games With Ruby
16
+
17
+ Complete process of building this game is described step by step in this free to read book:
18
+
19
+ [Developing Games With Ruby](https://leanpub.com/developing-games-with-ruby/read)
20
+
21
+ ## Installation
22
+
23
+ To install it, run
24
+
25
+ $ gem install tank_island
26
+
27
+ On Mac OSX you will have to `brew install imagemagick` to make it work.
28
+
29
+ ## Starting the game
30
+
31
+ There are several ways to start the game.
32
+
33
+ ### Running in 800x600 window mode
34
+
35
+ $ tank_island
36
+
37
+ ### Running with custom resolution
38
+
39
+ $ w=1600 h=1200 tank_island
40
+
41
+ ### Running full screen with custom resolution
42
+
43
+ $ fs=1 w=1200 h=800 tank_island
44
+
45
+ ## Controls
46
+
47
+ ### Gameplay
48
+
49
+ - `W` `A` `S` `D` moves your tank.
50
+ - Mouse `left click` shoots.
51
+ - `ESC` goes into menu and away from it.
52
+
53
+ ### Debugging
54
+
55
+ - `R` respawns your tank.
56
+ - `T` spawns an enemy tank under mouse cursor.
57
+ - `F1` enters debug mode.
58
+ - `F2` toggles profiling
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'gosu'
4
+ require 'gosu_texture_packer'
5
+ require 'perlin_noise'
6
+
7
+ root_dir = File.expand_path(File.join(
8
+ File.dirname(File.dirname(__FILE__)), 'lib'))
9
+ require_pattern = File.join(root_dir, '**/*.rb')
10
+ @failed = []
11
+
12
+ # Dynamically require everything
13
+ Dir.glob(require_pattern).each do |f|
14
+ begin
15
+ require f
16
+ rescue
17
+ # May fail if parent class not required yet
18
+ @failed << f
19
+ end
20
+ end
21
+
22
+ # Retry unresolved requires
23
+ @failed.each do |f|
24
+ require f.gsub("#{root_dir}/", '')
25
+ end
26
+
27
+ $debug = false
28
+ $window = GameWindow.new
29
+ GameState.switch(MenuState.instance)
30
+ $window.show
@@ -0,0 +1,28 @@
1
+ class Box < GameObject
2
+ attr_reader :health, :graphics, :angle
3
+
4
+ def initialize(object_pool, x, y)
5
+ super
6
+ @graphics = BoxGraphics.new(self)
7
+ @health = Health.new(self, object_pool, 10, true)
8
+ @angle = rand(-15..15)
9
+ end
10
+
11
+ def on_collision(object)
12
+ return unless object.physics.speed > 1.0
13
+ move(*Utils.point_at_distance(@x, @y, object.direction, 2))
14
+ @box = nil
15
+ end
16
+
17
+ def box
18
+ return @box if @box
19
+ w = @graphics.width / 2
20
+ h = @graphics.height / 2
21
+ # Bounding box adjusted to trim shadows
22
+ @box = [x - w + 4, y - h + 8,
23
+ x + w , y - h + 8,
24
+ x + w , y + h,
25
+ x - w + 4, y + h]
26
+ @box = Utils.rotate(@angle, @x, @y, *@box)
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ class Bullet < GameObject
2
+ attr_accessor :target_x, :target_y, :source, :speed, :fired_at
3
+
4
+ def initialize(object_pool, source_x, source_y, target_x, target_y)
5
+ super(object_pool, source_x, source_y)
6
+ @target_x, @target_y = target_x, target_y
7
+ BulletPhysics.new(self, object_pool)
8
+ BulletGraphics.new(self)
9
+ BulletSounds.play(self, object_pool.camera)
10
+ end
11
+
12
+ def box
13
+ [@x, @y]
14
+ end
15
+
16
+ def explode
17
+ Explosion.new(object_pool, @x, @y, @source)
18
+ mark_for_removal
19
+ end
20
+
21
+ def fire(source, speed)
22
+ @source = source
23
+ @speed = speed
24
+ @fired_at = Gosu.milliseconds
25
+ end
26
+ end
@@ -0,0 +1,113 @@
1
+ class Camera
2
+ attr_accessor :x, :y, :zoom
3
+ attr_reader :target
4
+
5
+ def target=(target)
6
+ @target = target
7
+ @x, @y = target.x, target.y
8
+ @zoom = 1
9
+ end
10
+
11
+ def desired_spot
12
+ if @target.physics.moving?
13
+ Utils.point_at_distance(
14
+ @target.x, @target.y,
15
+ @target.direction,
16
+ @target.physics.speed.ceil * 25)
17
+ else
18
+ [@target.x, @target.y]
19
+ end
20
+ end
21
+
22
+ def mouse_coords
23
+ x, y = target_delta_on_screen
24
+ mouse_x_on_map = @target.x +
25
+ (x + $window.mouse_x - ($window.width / 2)) / @zoom
26
+ mouse_y_on_map = @target.y +
27
+ (y + $window.mouse_y - ($window.height / 2)) / @zoom
28
+ [mouse_x_on_map, mouse_y_on_map].map(&:round)
29
+ end
30
+
31
+ def update
32
+ des_x, des_y = desired_spot
33
+ shift = Utils.adjust_speed(
34
+ @target.physics.speed).floor *
35
+ @target.speed_modifier + 1
36
+ if @x < des_x
37
+ if des_x - @x < shift
38
+ @x = des_x
39
+ else
40
+ @x += shift
41
+ end
42
+ elsif @x > des_x
43
+ if @x - des_x < shift
44
+ @x = des_x
45
+ else
46
+ @x -= shift
47
+ end
48
+ end
49
+ if @y < des_y
50
+ if des_y - @y < shift
51
+ @y = des_y
52
+ else
53
+ @y += shift
54
+ end
55
+ elsif @y > des_y
56
+ if @y - des_y < shift
57
+ @y = des_y
58
+ else
59
+ @y -= shift
60
+ end
61
+ end
62
+
63
+ zoom_delta = @zoom > 0 ? 0.01 : 1.0
64
+ zoom_delta = Utils.adjust_speed(zoom_delta)
65
+ if $window.button_down?(Gosu::KbUp)
66
+ @zoom -= zoom_delta unless @zoom < 0.7
67
+ elsif $window.button_down?(Gosu::KbDown)
68
+ @zoom += zoom_delta unless @zoom > 10
69
+ else
70
+ target_zoom = @target.physics.speed > 1.1 ? 0.75 : 1.0
71
+ if @zoom <= (target_zoom - 0.01)
72
+ @zoom += zoom_delta / 3
73
+ elsif @zoom > (target_zoom + 0.01)
74
+ @zoom -= zoom_delta / 3
75
+ end
76
+ end
77
+ end
78
+
79
+ def to_s
80
+ "FPS: #{Gosu.fps}. " <<
81
+ "#{@x}:#{@y} @ #{'%.2f' % @zoom}. " <<
82
+ 'WASD to move, arrows to zoom.'
83
+ end
84
+
85
+ def target_delta_on_screen
86
+ [(@x - @target.x) * @zoom, (@y - @target.y) * @zoom]
87
+ end
88
+
89
+ def draw_crosshair
90
+ factor = 0.5
91
+ x = $window.mouse_x
92
+ y = $window.mouse_y
93
+ c = crosshair
94
+ c.draw(x - c.width * factor / 2,
95
+ y - c.height * factor / 2,
96
+ 1000, factor, factor)
97
+ end
98
+
99
+ def viewport
100
+ x0 = @x - ($window.width / 2) / @zoom
101
+ x1 = @x + ($window.width / 2) / @zoom
102
+ y0 = @y - ($window.height / 2) / @zoom
103
+ y1 = @y + ($window.height / 2) / @zoom
104
+ [x0, x1, y0, y1]
105
+ end
106
+
107
+ private
108
+
109
+ def crosshair
110
+ @crosshair ||= Gosu::Image.new(
111
+ $window, Utils.media_path('c_dot.png'), false)
112
+ end
113
+ end
@@ -0,0 +1,114 @@
1
+ class AiGun
2
+ DECISION_DELAY = 300
3
+ attr_reader :target, :desired_gun_angle
4
+
5
+ def initialize(object, vision)
6
+ @object = object
7
+ @vision = vision
8
+ @desired_gun_angle = rand(0..360)
9
+ @retarget_speed = rand(1..5)
10
+ @accuracy = rand(0..10)
11
+ @aggressiveness = rand(1..5)
12
+ end
13
+
14
+ def adjust_angle
15
+ adjust_desired_angle
16
+ adjust_gun_angle
17
+ end
18
+
19
+ def update
20
+ if @vision.in_sight.any?
21
+ if @vision.closest_tank != @target
22
+ change_target(@vision.closest_tank)
23
+ end
24
+ else
25
+ @target = nil
26
+ end
27
+
28
+ if @target
29
+ if (0..30 - rand(0..@accuracy)).include?(
30
+ (@desired_gun_angle - @object.gun_angle).abs.round)
31
+ distance = distance_to_target
32
+ if distance - 50 <= BulletPhysics::MAX_DIST
33
+ target_x, target_y = Utils.point_at_distance(
34
+ @object.x, @object.y, @object.gun_angle,
35
+ distance + 10 - rand(0..@accuracy))
36
+ if can_make_new_decision? && @object.can_shoot? &&
37
+ should_shoot?
38
+ @object.shoot(target_x, target_y)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def draw(viewport)
46
+ if $debug
47
+ color = Gosu::Color::BLUE
48
+ x, y = @object.x, @object.y
49
+ t_x, t_y = Utils.point_at_distance(x, y, @desired_gun_angle,
50
+ BulletPhysics::MAX_DIST)
51
+ $window.draw_line(x, y, color, t_x, t_y, color, 1001)
52
+ color = Gosu::Color::RED
53
+ t_x, t_y = Utils.point_at_distance(x, y, @object.gun_angle,
54
+ BulletPhysics::MAX_DIST)
55
+ $window.draw_line(x, y, color, t_x, t_y, color, 1000)
56
+ end
57
+ end
58
+
59
+ def distance_to_target
60
+ Utils.distance_between(
61
+ @object.x, @object.y, @target.x, @target.y)
62
+ end
63
+
64
+
65
+ def should_shoot?
66
+ rand * @aggressiveness > 0.3
67
+ end
68
+
69
+ def can_make_new_decision?
70
+ now = Gosu.milliseconds
71
+ if now - (@last_decision ||= 0) > DECISION_DELAY
72
+ @last_decision = now
73
+ true
74
+ end
75
+ end
76
+
77
+ def adjust_desired_angle
78
+ @desired_gun_angle = if @target
79
+ Utils.angle_between(
80
+ @object.x, @object.y, @target.x, @target.y)
81
+ else
82
+ @object.direction
83
+ end
84
+ end
85
+
86
+ def change_target(new_target)
87
+ @target = new_target
88
+ adjust_desired_angle
89
+ end
90
+
91
+ def adjust_gun_angle
92
+ actual = @object.gun_angle
93
+ desired = @desired_gun_angle
94
+ if actual > desired
95
+ if actual - desired > 180 # 0 -> 360 fix
96
+ @object.gun_angle = (actual + @retarget_speed) % 360
97
+ if @object.gun_angle < desired
98
+ @object.gun_angle = desired # damp
99
+ end
100
+ else
101
+ @object.gun_angle = [actual - @retarget_speed, desired].max
102
+ end
103
+ elsif actual < desired
104
+ if desired - actual > 180 # 360 -> 0 fix
105
+ @object.gun_angle = (360 + actual - @retarget_speed) % 360
106
+ if @object.gun_angle > desired
107
+ @object.gun_angle = desired # damp
108
+ end
109
+ else
110
+ @object.gun_angle = [actual + @retarget_speed, desired].min
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,30 @@
1
+ class TankChasingState < TankMotionState
2
+ def initialize(object, vision, gun)
3
+ super(object, vision)
4
+ @object = object
5
+ @vision = vision
6
+ @gun = gun
7
+ end
8
+
9
+ def update
10
+ change_direction if should_change_direction?
11
+ drive
12
+ end
13
+
14
+ def change_direction
15
+ @object.physics.change_direction(
16
+ @gun.desired_gun_angle -
17
+ @gun.desired_gun_angle % 45)
18
+
19
+ @changed_direction_at = Gosu.milliseconds
20
+ @will_keep_direction_for = turn_time
21
+ end
22
+
23
+ def drive_time
24
+ 10000
25
+ end
26
+
27
+ def turn_time
28
+ rand(300..600)
29
+ end
30
+ end