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,61 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
class PauseState < GameState
|
3
|
+
include Singleton
|
4
|
+
attr_accessor :play_state
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@message = Gosu::Image.from_text(
|
8
|
+
$window, "Game Paused",
|
9
|
+
Utils.title_font, 60)
|
10
|
+
end
|
11
|
+
|
12
|
+
def enter
|
13
|
+
music.play(true)
|
14
|
+
music.volume = 1
|
15
|
+
@score_display = ScoreDisplay.new(@play_state.object_pool)
|
16
|
+
@mouse_coords = [$window.mouse_x, $window.mouse_y]
|
17
|
+
end
|
18
|
+
|
19
|
+
def leave
|
20
|
+
music.volume = 0
|
21
|
+
music.stop
|
22
|
+
$window.mouse_x, $window.mouse_y = @mouse_coords
|
23
|
+
end
|
24
|
+
|
25
|
+
def music
|
26
|
+
@@music ||= Gosu::Song.new(
|
27
|
+
$window, Utils.media_path('menu_music.mp3'))
|
28
|
+
end
|
29
|
+
|
30
|
+
def draw
|
31
|
+
@play_state.draw
|
32
|
+
@message.draw(
|
33
|
+
$window.width / 2 - @message.width / 2,
|
34
|
+
$window.height / 4 - @message.height - 50,
|
35
|
+
1000)
|
36
|
+
info.draw(
|
37
|
+
$window.width / 2 - info.width / 2,
|
38
|
+
$window.height / 4 - info.height,
|
39
|
+
1000)
|
40
|
+
@score_display.draw
|
41
|
+
end
|
42
|
+
|
43
|
+
def info
|
44
|
+
@info ||= Gosu::Image.from_text(
|
45
|
+
$window, 'Q: Quit to Main Menu',
|
46
|
+
Utils.main_font, 30)
|
47
|
+
end
|
48
|
+
|
49
|
+
def button_down(id)
|
50
|
+
if id == Gosu::KbQ
|
51
|
+
MenuState.instance.play_state = @play_state
|
52
|
+
GameState.switch(MenuState.instance)
|
53
|
+
end
|
54
|
+
if id == Gosu::KbC && @play_state
|
55
|
+
GameState.switch(@play_state)
|
56
|
+
end
|
57
|
+
if id == Gosu::KbEscape
|
58
|
+
GameState.switch(@play_state)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
class PlayState < GameState
|
2
|
+
attr_accessor :update_interval, :object_pool, :tank
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
# http://www.paulandstorm.com/wha/clown-names/
|
6
|
+
@names = Names.new(
|
7
|
+
Utils.media_path('names.txt'))
|
8
|
+
@object_pool = ObjectPool.new(Map.bounding_box)
|
9
|
+
@map = Map.new(@object_pool)
|
10
|
+
@camera = Camera.new
|
11
|
+
@object_pool.camera = @camera
|
12
|
+
create_tanks(7)
|
13
|
+
end
|
14
|
+
|
15
|
+
def update
|
16
|
+
StereoSample.cleanup
|
17
|
+
@object_pool.update_all
|
18
|
+
@camera.update
|
19
|
+
@hud.update
|
20
|
+
update_caption
|
21
|
+
end
|
22
|
+
|
23
|
+
def draw
|
24
|
+
cam_x = @camera.x
|
25
|
+
cam_y = @camera.y
|
26
|
+
off_x = $window.width / 2 - cam_x
|
27
|
+
off_y = $window.height / 2 - cam_y
|
28
|
+
viewport = @camera.viewport
|
29
|
+
x1, x2, y1, y2 = viewport
|
30
|
+
box = AxisAlignedBoundingBox.new(
|
31
|
+
[x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2],
|
32
|
+
[x1 - Map::TILE_SIZE, y1 - Map::TILE_SIZE])
|
33
|
+
$window.translate(off_x, off_y) do
|
34
|
+
zoom = @camera.zoom
|
35
|
+
$window.scale(zoom, zoom, cam_x, cam_y) do
|
36
|
+
@map.draw(viewport)
|
37
|
+
@object_pool.query_range(box).map do |o|
|
38
|
+
o.draw(viewport)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
@hud.draw
|
43
|
+
end
|
44
|
+
|
45
|
+
def button_down(id)
|
46
|
+
if id == Gosu::KbEscape
|
47
|
+
pause = PauseState.instance
|
48
|
+
pause.play_state = self
|
49
|
+
GameState.switch(pause)
|
50
|
+
end
|
51
|
+
if id == Gosu::KbT
|
52
|
+
t = Tank.new(@object_pool,
|
53
|
+
AiInput.new(@names.random, @object_pool))
|
54
|
+
t.move(*@camera.mouse_coords)
|
55
|
+
end
|
56
|
+
if id == Gosu::KbF1
|
57
|
+
$debug = !$debug
|
58
|
+
end
|
59
|
+
if id == Gosu::KbF2
|
60
|
+
toggle_profiling
|
61
|
+
end
|
62
|
+
if id == Gosu::KbR
|
63
|
+
@tank.mark_for_removal
|
64
|
+
@tank = Tank.new(@object_pool,
|
65
|
+
PlayerInput.new('Player', @camera, @object_pool))
|
66
|
+
@camera.target = @tank
|
67
|
+
@hud.player = @tank
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def leave
|
72
|
+
StereoSample.stop_all
|
73
|
+
if @profiling_now
|
74
|
+
toggle_profiling
|
75
|
+
end
|
76
|
+
@hud.active = false
|
77
|
+
end
|
78
|
+
|
79
|
+
def enter
|
80
|
+
@hud.active = true
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def create_tanks(amount)
|
86
|
+
@map.spawn_points(amount * 3)
|
87
|
+
@tank = Tank.new(@object_pool,
|
88
|
+
PlayerInput.new('Player', @camera, @object_pool))
|
89
|
+
amount.times do |i|
|
90
|
+
Tank.new(@object_pool, AiInput.new(
|
91
|
+
@names.random, @object_pool))
|
92
|
+
end
|
93
|
+
@camera.target = @tank
|
94
|
+
@hud = HUD.new(@object_pool, @tank)
|
95
|
+
end
|
96
|
+
|
97
|
+
def toggle_profiling
|
98
|
+
require 'ruby-prof' unless defined?(RubyProf)
|
99
|
+
if @profiling_now
|
100
|
+
result = RubyProf.stop
|
101
|
+
printer = RubyProf::FlatPrinter.new(result)
|
102
|
+
printer.print(STDOUT, min_percent: 0.01)
|
103
|
+
@profiling_now = false
|
104
|
+
else
|
105
|
+
RubyProf.start
|
106
|
+
@profiling_now = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def update_caption
|
111
|
+
now = Gosu.milliseconds
|
112
|
+
if now - (@caption_updated_at || 0) > 1000
|
113
|
+
$window.caption = 'Tank Island. ' <<
|
114
|
+
"[FPS: #{Gosu.fps}. " <<
|
115
|
+
"Tank @ #{@tank.x.round}:#{@tank.y.round}]"
|
116
|
+
@caption_updated_at = now
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class AxisAlignedBoundingBox
|
2
|
+
attr_reader :center, :half_dimension
|
3
|
+
def initialize(center, half_dimension)
|
4
|
+
@center = center
|
5
|
+
@half_dimension = half_dimension
|
6
|
+
@dhx = (@half_dimension[0] - @center[0]).abs
|
7
|
+
@dhy = (@half_dimension[1] - @center[1]).abs
|
8
|
+
end
|
9
|
+
|
10
|
+
def contains?(point)
|
11
|
+
return false unless (@center[0] + @dhx) >= point[0]
|
12
|
+
return false unless (@center[0] - @dhx) <= point[0]
|
13
|
+
return false unless (@center[1] + @dhy) >= point[1]
|
14
|
+
return false unless (@center[1] - @dhy) <= point[1]
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def intersects?(other)
|
19
|
+
ocx, ocy = other.center
|
20
|
+
ohx, ohy = other.half_dimension
|
21
|
+
odhx = (ohx - ocx).abs
|
22
|
+
return false unless (@center[0] + @dhx) >= (ocx - odhx)
|
23
|
+
return false unless (@center[0] - @dhx) <= (ocx + odhx)
|
24
|
+
odhy = (ohy - ocy).abs
|
25
|
+
return false unless (@center[1] + @dhy) >= (ocy - odhy)
|
26
|
+
return false unless (@center[1] - @dhy) <= (ocy + odhy)
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"c: #{@center}, h: #{@half_dimension}"
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class GameWindow < Gosu::Window
|
2
|
+
attr_accessor :state
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
super((ENV['w'] || 800).to_i,
|
6
|
+
(ENV['h'] || 600).to_i,
|
7
|
+
(ENV['fs'] ? true : false))
|
8
|
+
end
|
9
|
+
|
10
|
+
def update
|
11
|
+
Utils.track_update_interval
|
12
|
+
@state.update
|
13
|
+
end
|
14
|
+
|
15
|
+
def draw
|
16
|
+
@state.draw
|
17
|
+
end
|
18
|
+
|
19
|
+
def needs_redraw?
|
20
|
+
@state.needs_redraw?
|
21
|
+
end
|
22
|
+
|
23
|
+
def needs_cursor?
|
24
|
+
Utils.update_interval > 200
|
25
|
+
end
|
26
|
+
|
27
|
+
def button_down(id)
|
28
|
+
@state.button_down(id)
|
29
|
+
end
|
30
|
+
end
|
data/lib/misc/names.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
class QuadTree
|
2
|
+
NODE_CAPACITY = 12
|
3
|
+
attr_accessor :ne, :nw, :se, :sw, :objects
|
4
|
+
|
5
|
+
def initialize(boundary)
|
6
|
+
@boundary = boundary
|
7
|
+
@objects = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def insert(game_object)
|
11
|
+
return false unless @boundary.contains?(
|
12
|
+
game_object.location)
|
13
|
+
|
14
|
+
if @objects.size < NODE_CAPACITY
|
15
|
+
@objects << game_object
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
|
19
|
+
subdivide unless @nw
|
20
|
+
|
21
|
+
return true if @nw.insert(game_object)
|
22
|
+
return true if @ne.insert(game_object)
|
23
|
+
return true if @sw.insert(game_object)
|
24
|
+
return true if @se.insert(game_object)
|
25
|
+
|
26
|
+
# should never happen
|
27
|
+
raise "Failed to insert #{game_object}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove(game_object)
|
31
|
+
return false unless @boundary.contains?(
|
32
|
+
game_object.location)
|
33
|
+
if @objects.delete(game_object)
|
34
|
+
return true
|
35
|
+
end
|
36
|
+
return false unless @nw
|
37
|
+
return true if @nw.remove(game_object)
|
38
|
+
return true if @ne.remove(game_object)
|
39
|
+
return true if @sw.remove(game_object)
|
40
|
+
return true if @se.remove(game_object)
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def query_range(range)
|
45
|
+
result = []
|
46
|
+
unless @boundary.intersects?(range)
|
47
|
+
return result
|
48
|
+
end
|
49
|
+
|
50
|
+
@objects.each do |o|
|
51
|
+
if range.contains?(o.location)
|
52
|
+
result << o
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Not subdivided
|
57
|
+
return result unless @ne
|
58
|
+
|
59
|
+
result += @nw.query_range(range)
|
60
|
+
result += @ne.query_range(range)
|
61
|
+
result += @sw.query_range(range)
|
62
|
+
result += @se.query_range(range)
|
63
|
+
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def subdivide
|
70
|
+
cx, cy = @boundary.center
|
71
|
+
hx, hy = @boundary.half_dimension
|
72
|
+
hhx = (cx - hx).abs / 2.0
|
73
|
+
hhy = (cy - hy).abs / 2.0
|
74
|
+
@nw = QuadTree.new(
|
75
|
+
AxisAlignedBoundingBox.new(
|
76
|
+
[cx - hhx, cy - hhy],
|
77
|
+
[cx, cy]))
|
78
|
+
@ne = QuadTree.new(
|
79
|
+
AxisAlignedBoundingBox.new(
|
80
|
+
[cx + hhx, cy - hhy],
|
81
|
+
[cx, cy]))
|
82
|
+
@sw = QuadTree.new(
|
83
|
+
AxisAlignedBoundingBox.new(
|
84
|
+
[cx - hhx, cy + hhy],
|
85
|
+
[cx, cy]))
|
86
|
+
@se = QuadTree.new(
|
87
|
+
AxisAlignedBoundingBox.new(
|
88
|
+
[cx + hhx, cy + hhy],
|
89
|
+
[cx, cy]))
|
90
|
+
end
|
91
|
+
end
|
data/lib/misc/stats.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class Stats
|
2
|
+
attr_reader :name, :kills, :deaths, :shots, :changed_at
|
3
|
+
def initialize(name)
|
4
|
+
@name = name
|
5
|
+
@kills = @deaths = @shots = @damage = @damage_dealt = 0
|
6
|
+
changed
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_kill(amount = 1)
|
10
|
+
@kills += amount
|
11
|
+
changed
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_death
|
15
|
+
@deaths += 1
|
16
|
+
changed
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_shot
|
20
|
+
@shots += 1
|
21
|
+
changed
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_damage(amount)
|
25
|
+
@damage += amount
|
26
|
+
changed
|
27
|
+
end
|
28
|
+
|
29
|
+
def damage
|
30
|
+
@damage.round
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_damage_dealt(amount)
|
34
|
+
@damage_dealt += amount
|
35
|
+
changed
|
36
|
+
end
|
37
|
+
|
38
|
+
def damage_dealt
|
39
|
+
@damage_dealt.round
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"[kills: #{@kills}, " \
|
44
|
+
"deaths: #{@deaths}, " \
|
45
|
+
"shots: #{@shots}, " \
|
46
|
+
"damage: #{damage}, " \
|
47
|
+
"damage_dealt: #{damage_dealt}]"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def changed
|
53
|
+
@changed_at = Gosu.milliseconds
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
class StereoSample
|
2
|
+
MAX_POLIPHONY = 16
|
3
|
+
@@all_instances = []
|
4
|
+
|
5
|
+
def self.register_instances(instances)
|
6
|
+
@@all_instances << instances
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.cleanup
|
10
|
+
@@all_instances.each do |instances|
|
11
|
+
instances.each do |key, instance|
|
12
|
+
unless instance.playing? || instance.paused?
|
13
|
+
instances.delete(key)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.stop_all
|
20
|
+
@@all_instances.each do |instances|
|
21
|
+
instances.each do |key, instance|
|
22
|
+
if instance.playing?
|
23
|
+
instance.stop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(window, sound_l, sound_r = sound_l)
|
30
|
+
@sound_l = Gosu::Sample.new(window, sound_l)
|
31
|
+
# Use same sample in mono -> stereo
|
32
|
+
if sound_l == sound_r
|
33
|
+
@sound_r = @sound_l
|
34
|
+
else
|
35
|
+
@sound_r = Gosu::Sample.new(window, sound_r)
|
36
|
+
end
|
37
|
+
@instances = {}
|
38
|
+
self.class.register_instances(@instances)
|
39
|
+
end
|
40
|
+
|
41
|
+
def paused?(id = :default)
|
42
|
+
i = @instances["#{id}_l"]
|
43
|
+
i && i.paused?
|
44
|
+
end
|
45
|
+
|
46
|
+
def playing?(id = :default)
|
47
|
+
i = @instances["#{id}_l"]
|
48
|
+
i && i.playing?
|
49
|
+
end
|
50
|
+
|
51
|
+
def stopped?(id = :default)
|
52
|
+
@instances["#{id}_l"].nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def play(id = :default, pan = 0,
|
56
|
+
volume = 1, speed = 1, looping = false)
|
57
|
+
if @instances.size > MAX_POLIPHONY
|
58
|
+
return
|
59
|
+
end
|
60
|
+
@instances["#{id}_l"] = @sound_l.play_pan(
|
61
|
+
-0.2, 0, speed, looping)
|
62
|
+
@instances["#{id}_r"] = @sound_r.play_pan(
|
63
|
+
0.2, 0, speed, looping)
|
64
|
+
volume_and_pan(id, volume, pan)
|
65
|
+
end
|
66
|
+
|
67
|
+
def pause(id = :default)
|
68
|
+
@instances["#{id}_l"].pause
|
69
|
+
@instances["#{id}_r"].pause
|
70
|
+
end
|
71
|
+
|
72
|
+
def resume(id = :default)
|
73
|
+
@instances["#{id}_l"].resume
|
74
|
+
@instances["#{id}_r"].resume
|
75
|
+
end
|
76
|
+
|
77
|
+
def stop
|
78
|
+
@instances.delete("#{id}_l").stop
|
79
|
+
@instances.delete("#{id}_r").stop
|
80
|
+
end
|
81
|
+
|
82
|
+
def volume_and_pan(id, volume, pan)
|
83
|
+
return unless @instances["#{id}_l"]
|
84
|
+
if pan > 0
|
85
|
+
pan_l = 1 - pan * 2
|
86
|
+
pan_r = 1
|
87
|
+
else
|
88
|
+
pan_l = 1
|
89
|
+
pan_r = 1 + pan * 2
|
90
|
+
end
|
91
|
+
pan_l *= volume
|
92
|
+
pan_r *= volume
|
93
|
+
@instances["#{id}_l"].volume = [pan_l, 0.05].max
|
94
|
+
@instances["#{id}_r"].volume = [pan_r, 0.05].max
|
95
|
+
end
|
96
|
+
end
|