chingu 0.9rc7 → 0.9rc8

Sign up to get free protection for your applications and to get access to all the features.
@@ -63,7 +63,11 @@ module Chingu
63
63
  objects.each do |object|
64
64
  object.each_pair do |klassname, attributes|
65
65
  begin
66
- klass = Kernel::const_get(klassname)
66
+ klass = Object
67
+ names = klassname.split('::')
68
+ names.each do |name|
69
+ klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
70
+ end
67
71
  unless klass.class == "GameObject" && !except.include?(klass)
68
72
  puts "Creating #{klassname.to_s}: #{attributes.to_s}" if debug
69
73
  object = klass.create(attributes)
@@ -31,6 +31,7 @@ module Chingu
31
31
  #
32
32
  module GameState
33
33
  extend Forwardable
34
+
34
35
  def_delegator :game_state_manager, :game_states
35
36
  def_delegator :game_state_manager, :push_game_state
36
37
  def_delegator :game_state_manager, :pop_game_state
@@ -38,6 +39,7 @@ module Chingu
38
39
  def_delegator :game_state_manager, :transitional_game_state
39
40
  def_delegator :game_state_manager, :current_game_state
40
41
  def_delegator :game_state_manager, :clear_game_states
42
+ def_delegator :game_state_manager, :pop_until_game_state
41
43
  end
42
44
 
43
45
  end
@@ -49,8 +49,8 @@ module Chingu
49
49
  elsif key.is_a? Image
50
50
  GameObject.new(options.merge!(:image => key))
51
51
  elsif key.is_a? GameObject
52
- menu_item.options.merge!(options.dup)
53
- menu_item
52
+ key.options.merge!(options.dup)
53
+ key
54
54
  end
55
55
 
56
56
  item.options[:on_select] = method(:on_select)
@@ -84,23 +84,20 @@ module Chingu
84
84
 
85
85
  if options[:background]
86
86
  @background = GameObject.new(:image => options[:background])
87
- @background.attributes = self.attributes
88
- @background.color = ::Gosu::Color::WHITE
89
- @background.zorder -= 1
90
- @background.x -= @padding
91
- @background.y -= @padding
92
- @background.width = self.width + @padding * 2
93
- @background.height = self.height + @padding * 2
87
+ bg_update
88
+ @no_bg_update = options[:no_bg_update] || false # always update background attribute
94
89
  end
95
90
 
96
91
  self.height = options[:height] if options[:height]
97
- end
98
-
92
+ end
93
+
94
+
99
95
  #
100
96
  # Set a new text, a new image is created.
101
97
  #
102
98
  def text=(text)
103
- @text = text
99
+ return if text.dup == @text # Have to make a dup for content comparison
100
+ @text = text.dup # Make a copy, again to have a different Objectid
104
101
  create_image
105
102
  end
106
103
 
@@ -111,6 +108,14 @@ module Chingu
111
108
  @gosu_font.text_width(@text, @factor_x)
112
109
  end
113
110
 
111
+ #
112
+ # Update the background attributes if necessary unless specified
113
+ #
114
+ def update
115
+ super
116
+ bg_update if @background and !@no_bg_update
117
+ end
118
+
114
119
  #
115
120
  # Draws @background if present + our text in @image
116
121
  #
@@ -131,5 +136,15 @@ module Chingu
131
136
  @image = Gosu::Image.from_text($window, @text, @font, @size)
132
137
  end
133
138
  end
139
+
140
+ def bg_update
141
+ @background.attributes = self.attributes
142
+ @background.color = ::Gosu::Color::WHITE
143
+ @background.zorder -= 1
144
+ @background.x -= @padding
145
+ @background.y -= @padding
146
+ @background.width = self.width + @padding * 2
147
+ @background.height = self.height + @padding * 2
148
+ end
134
149
  end
135
150
  end
@@ -97,9 +97,9 @@ module Chingu
97
97
  raise ArgumentError.new("No image set") if image.nil?
98
98
 
99
99
  @image = if String === image
100
- # 1) Try loading the image the normal way
101
- # 2) Try looking up the picture using Chingus Image-cache
102
- Gosu::Image.new($window, image,false) rescue Gosu::Image[image]
100
+ # 1) Try looking up the picture using Chingus Image-cache
101
+ # 2) Try loading the image the normal way
102
+ Gosu::Image[image] rescue Gosu::Image.new($window, image,false)
103
103
  elsif image.respond_to? :call
104
104
  image.call
105
105
  else
@@ -28,10 +28,10 @@ module Chingu
28
28
  #
29
29
  #
30
30
  # TODO:
31
- # Implement use of viewports angle, center_x, center_y, factor_x, factor_y
31
+ # Implement use of viewports angle, center_x, center_y
32
32
  #
33
33
  class Viewport
34
- attr_accessor :x, :y, :x_target, :y_target, :x_lag, :y_lag, :game_area
34
+ attr_accessor :x, :y, :x_target, :y_target, :x_lag, :y_lag, :factor_x, :factor_y, :game_area
35
35
 
36
36
  def initialize(options = {})
37
37
  @x = options[:x] || 0
@@ -40,6 +40,8 @@ module Chingu
40
40
  @y_target = options[:y_target] || @y
41
41
  @x_lag = options[:x_lag] || 0
42
42
  @y_lag = options[:y_lag] || 0
43
+ @factor_x = options[:factor_x] || 1
44
+ @factor_y = options[:factor_y] || 1
43
45
  @game_area = Chingu::Rect.new(options[:game_area] || [@x, @y, $window.width, $window.height])
44
46
  end
45
47
 
@@ -57,8 +59,8 @@ module Chingu
57
59
  # TODO: Add support for x,y here!
58
60
  #
59
61
  def center_around(object)
60
- self.x = object.x - $window.width / 2
61
- self.y = object.y - $window.height / 2
62
+ self.x = object.x * @factor_x - $window.width / 2
63
+ self.y = object.y * @factor_y - $window.height / 2
62
64
  end
63
65
 
64
66
  #
@@ -114,14 +116,14 @@ module Chingu
114
116
  # Modify viewports x and y from target_x / target_y and x_lag / y_lag
115
117
  # Use this to have the viewport "slide" after the player
116
118
  #
117
- def move_towards_target
119
+ def move_towards_target
118
120
  if @x_target && @x != @x_target
119
- x_step = @x_target - @x
121
+ x_step = @x_target * @factor_x - @x
120
122
  self.x = @x + x_step * (1.0 - @x_lag)
121
123
  end
122
-
124
+
123
125
  if @y_target && @y != @y_target
124
- y_step = @y_target - @y
126
+ y_step = @y_target * @factor_y - @y
125
127
  self.y = @y + y_step * (1.0 - @y_lag)
126
128
  end
127
129
  end
@@ -132,9 +134,9 @@ module Chingu
132
134
  def x=(x)
133
135
  @x = x
134
136
  if @game_area
135
- @x = @game_area.x if @x < @game_area.x
136
- @x = @game_area.width-$window.width if @x > @game_area.width-$window.width
137
- end
137
+ @x = @game_area.x * @factor_x if @x < @game_area.x * @factor_x
138
+ @x = @game_area.width * @factor_x - $window.width if @x > @game_area.width * @factor_x - $window.width
139
+ end
138
140
  end
139
141
 
140
142
  #
@@ -143,8 +145,8 @@ module Chingu
143
145
  def y=(y)
144
146
  @y = y
145
147
  if @game_area
146
- @y = @game_area.y if @y < @game_area.y
147
- @y = @game_area.height-$window.height if @y > @game_area.height-$window.height
148
+ @y = @game_area.y * @factor_y if @y < @game_area.y * @factor_y
149
+ @y = @game_area.height * @factor_y - $window.height if @y > @game_area.height * @factor_y - $window.height
148
150
  end
149
151
  end
150
152
 
@@ -152,7 +154,9 @@ module Chingu
152
154
  # Apply the X/Y viewport-translation, used by trait "viewport"
153
155
  #
154
156
  def apply(&block)
155
- $window.translate(-@x.to_i, -@y.to_i, &block)
157
+ $window.translate(-@x.to_i, -@y.to_i) do
158
+ $window.scale(@factor_x, @factor_y, 0, 0, &block)
159
+ end
156
160
  end
157
161
 
158
162
  def to_s
@@ -64,6 +64,8 @@ module Chingu
64
64
  @milliseconds_since_last_tick = 0
65
65
  @factor = 1
66
66
  @cursor = false
67
+ @times_muted = 0
68
+ @volume = DEFAULT_VOLUME
67
69
 
68
70
  setup
69
71
  end
@@ -194,5 +196,57 @@ module Chingu
194
196
 
195
197
  $window = nil
196
198
  end
199
+
200
+ # GLOBAL SOUND SETTINGS
201
+
202
+ DEFAULT_VOLUME = 1.0 # However, 0.5 is a better value for general use.
203
+
204
+ # Set the global volume of all Samples and Songs, not affected by Window being muted.
205
+ def volume=(value)
206
+ raise "Bad volume setting" unless value.is_a? Numeric
207
+
208
+ old_volume = @volume
209
+ @volume = [[value, 1.0].min, 0.0].max.to_f
210
+
211
+ Song.send(:recalculate_volumes, old_volume, @volume)
212
+
213
+ volume
214
+ end
215
+
216
+ # Volume of all Samples and Songs, not affected by the Window being muted.
217
+ attr_reader :volume
218
+
219
+ # Actual volume of all Samples and Songs, affected by the Window being muted.
220
+ def effective_volume
221
+ muted? ? 0.0 : @volume
222
+ end
223
+
224
+ # Mute the window and all Samples and Songs played.
225
+ # Muting stacks, so sound will only be heard if the number of unmutes is the same as the number of mutes.
226
+ def mute
227
+ unless muted?
228
+ Song.send(:resources).each_value {|song| song.send :mute }
229
+ end
230
+ @times_muted += 1
231
+
232
+ self
233
+ end
234
+
235
+ # Unmute the window and all Samples and Songs played.
236
+ # Muting stacks, so sound will only be heard if the number of unmutes is the same as the number of mutes.
237
+ def unmute
238
+ raise "Can't unmute when not muted" unless muted?
239
+ @times_muted -= 1
240
+ unless muted?
241
+ Song.send(:resources).each_value {|song| song.send :unmute }
242
+ end
243
+
244
+ self
245
+ end
246
+
247
+ # Is the window currently muted?
248
+ def muted?
249
+ @times_muted > 0
250
+ end
197
251
  end
198
252
  end
@@ -6,8 +6,9 @@ module Chingu
6
6
  @game = Chingu::Window.new
7
7
  @test_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'images')
8
8
  Gosu::Image.autoload_dirs << @test_dir
9
- @animation_clean = Animation.new(:file => File.join(@test_dir, "droid_11x15.bmp"))
10
- @animation = Animation.new(:file => File.join(@test_dir, "droid_11x15.bmp"), :delay => 0)
9
+ @file = "droid_11x15.bmp"
10
+ @animation_clean = Animation.new(:file => @file)
11
+ @animation = Animation.new(:file => @file, :delay => 0)
11
12
  end
12
13
 
13
14
  after :each do
@@ -33,8 +34,45 @@ module Chingu
33
34
  @anim = Animation.new(:file => "images/droid_11x15.bmp")
34
35
  @anim.frames.count.should == 14
35
36
  end
37
+
38
+ it "should load from a Gosu image" do
39
+ Dir.chdir(File.dirname(File.expand_path(__FILE__)))
40
+ @anim = Animation.new(:image => Gosu::Image["images/droid_11x15.bmp"], :size => [11, 15])
41
+ @anim.frames.count.should == 14
42
+ end
36
43
  end
37
-
44
+
45
+ describe "Animation loading using :frames" do
46
+ it "should have the same frames" do
47
+ anim = Animation.new :frames => @animation_clean.frames
48
+ anim.frames.should == @animation_clean.frames
49
+ end
50
+
51
+ it "should reject non-consistent frame sizes" do
52
+ ->{ Animation.new :frames => @animation_clean.frames + [Gosu::Image[@file]]}.should raise_error ArgumentError
53
+ end
54
+ end
55
+
56
+ describe "Animation loading using :image" do
57
+ before :each do
58
+ @anim = Animation.new :image => Gosu::Image[@file]
59
+ end
60
+
61
+ it "should have the same frames" do
62
+ @anim.frames.zip(@animation_clean.frames).all? { |a, b| a.to_blob == b.to_blob }
63
+ end
64
+ end
65
+
66
+ describe "Animation loading errors" do
67
+ it "should fail unless one of the creation params is given" do
68
+ ->{ Animation.new }.should raise_error ArgumentError
69
+ end
70
+
71
+ it "should fail if more than one creation params" do
72
+ ->{ Animation.new :image => Gosu::Image[@file], :file => @file}.should raise_error ArgumentError
73
+ end
74
+ end
75
+
38
76
  describe "Animation loading file: droid_11x15.bmp" do
39
77
  it "should detect size and frames automatically from filename" do
40
78
  @animation.size.should == [11,15]
@@ -60,22 +60,69 @@ module Chingu
60
60
  describe "switch_game_state" do
61
61
  before :each do
62
62
  @game.push_game_state(Chingu::GameStates::Pause)
63
- @game.switch_game_state(Chingu::GameStates::Edit)
63
+ @game.switch_game_state(Chingu::GameStates::Debug)
64
64
  end
65
65
 
66
66
  it "should replace current game state" do
67
- @game.current_game_state.class.should == Chingu::GameStates::Edit
67
+ @game.current_game_state.should be_a Chingu::GameStates::Debug
68
68
  end
69
69
 
70
70
  it "should not change the total amount of game states" do
71
71
  @game.game_states.count.should == 1
72
72
  end
73
73
  end
74
+
75
+ describe "pop_until_game_state" do
76
+ before :each do
77
+ @game.push_game_state(Chingu::GameStates::Pause)
78
+ @game.push_game_state(Chingu::GameStates::Debug)
79
+ @game.push_game_state(Chingu::GameStates::Debug)
80
+ @states = @game.game_state_manager.instance_variable_get(:@game_states).dup
81
+ end
82
+
83
+ describe "with class" do
84
+ it "should finalize popped states" do
85
+ @states[1].should_receive(:finalize)
86
+ @states[2].should_receive(:finalize)
87
+ @game.pop_until_game_state(Chingu::GameStates::Pause)
88
+ end
89
+
90
+ it "should setup revealed states" do
91
+ @states[0].should_receive(:setup)
92
+ @states[1].should_receive(:setup)
93
+ @game.pop_until_game_state(Chingu::GameStates::Pause)
94
+ end
95
+
96
+ it "should pop down to the given game state" do
97
+ @game.pop_until_game_state(Chingu::GameStates::Pause)
98
+ @game.game_states.should eq [@states[0]]
99
+ end
100
+ end
101
+
102
+ describe "with instance" do
103
+ it "should finalize popped states" do
104
+ @states[1].should_receive(:finalize)
105
+ @states[2].should_receive(:finalize)
106
+ @game.pop_until_game_state(@states[0])
107
+ end
108
+
109
+ it "should setup revealed states" do
110
+ @states[0].should_receive(:setup)
111
+ @states[1].should_receive(:setup)
112
+ @game.pop_until_game_state(@states[0])
113
+ end
114
+
115
+ it "should pop down to the given game state" do
116
+ @game.pop_until_game_state(@states[0])
117
+ @game.game_states.should eq [@states[0]]
118
+ end
119
+ end
120
+ end
74
121
 
75
122
  describe "clear_game_states" do
76
123
  it "should clear all game states" do
77
124
  @game.push_game_state(Chingu::GameStates::Pause)
78
- @game.switch_game_state(Chingu::GameStates::Edit)
125
+ @game.push_game_state(Chingu::GameStates::Edit)
79
126
  @game.clear_game_states
80
127
  @game.game_states.count.should == 0
81
128
  end
@@ -16,14 +16,14 @@ module Chingu
16
16
 
17
17
  describe Chingu::GameStates::NetworkServer do
18
18
  it "should open listening port on start()" do
19
- @server = described_class.new(:ip => "0.0.0.0", :port => 9999)
19
+ @server = described_class.new(:address => "0.0.0.0", :port => 9999)
20
20
  @server.should_receive(:on_start)
21
21
  @server.start
22
22
  @server.stop
23
23
  end
24
24
 
25
25
  it "client should timeout when connecting to blackhole ip" do
26
- @client = Chingu::GameStates::NetworkClient.new(:ip => "1.2.3.4", :port => 1234, :debug => true)
26
+ @client = Chingu::GameStates::NetworkClient.new(:address => "1.2.3.4", :port => 1234, :debug => true)
27
27
  @client.connect
28
28
 
29
29
  #@client.should_receive(:on_timeout) ## gives on_connection_refused instead, kind of ok.
@@ -32,15 +32,15 @@ module Chingu
32
32
  end
33
33
 
34
34
  it "should call on_start_error() if failing" do
35
- @server = described_class.new(:ip => "1.2.3.999", :port => 12345678) # crazy ip:port
35
+ @server = described_class.new(:address => "1.2.3.999", :port => 12345678) # crazy address:port
36
36
  @server.should_receive(:on_start_error)
37
37
  @server.start
38
38
  @server.stop
39
39
  end
40
40
 
41
41
  it "should call on_connect() and on_disconnect() when client connects" do
42
- @server = described_class.new(:ip => "0.0.0.0", :port => 9999)
43
- @client = Chingu::GameStates::NetworkClient.new(:ip => "127.0.0.1", :port => 9999)
42
+ @server = described_class.new(:address => "0.0.0.0", :port => 9999)
43
+ @client = Chingu::GameStates::NetworkClient.new(:address => "127.0.0.1", :port => 9999)
44
44
 
45
45
  @server.should_receive(:on_start)
46
46
  @server.should_receive(:on_connect).with(an_instance_of(TCPSocket))
@@ -58,19 +58,64 @@ module Chingu
58
58
  end
59
59
 
60
60
  describe Chingu::GameStates::NetworkClient do
61
- it "should call on_connection_refused callback when connecting to closed port" do
62
- @client = described_class.new(:ip => "127.0.0.1", :port => 55421) # closed we assume
63
- @client.should_receive(:on_connection_refused)
61
+ describe "connect" do
62
+ it "should call on_connection_refused callback when connecting to closed port" do
63
+ @client = described_class.new(:address => "127.0.0.1", :port => 55421) # closed we assume
64
+ @client.should_receive(:on_connection_refused)
65
+ @client.connect
66
+ 5.times { @client.update }
67
+ end
68
+
69
+ it "should not call on_timeout callback when unable to connect for less time than the timeout" do
70
+ @client = described_class.new(:address => "127.0.0.1", :port => 55421, :timeout => 250) # closed we assume
71
+ @client.connect
72
+ @client.should_not_receive(:on_timeout)
73
+ 5.times { @client.update; sleep 0.01 }
74
+ end
75
+
76
+ it "should call on_timeout callback when unable to connect for longer than the timeout" do
77
+ @client = described_class.new(:address => "127.0.0.1", :port => 55421, :timeout => 250) # closed we assume
78
+ @client.connect
79
+ @client.update
80
+ sleep 0.3
81
+ @client.should_receive(:on_timeout)
82
+ 5.times { @client.update }
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "Connecting" do
88
+ before :each do
89
+ @client = Chingu::GameStates::NetworkClient.new(:address => "127.0.0.1", :port => 9999)
90
+ @server = Chingu::GameStates::NetworkServer.new(:port => 9999)
91
+ end
92
+
93
+ it "should connect to the server, when the server starts before it" do
94
+ #@server.start
95
+ #@client.connect
96
+ #5.times { @client.update }
97
+ #@client.should be_connected
98
+ end
99
+
100
+ it "should connect to the server, even when the server isn't initialy available" do
64
101
  @client.connect
65
- 5.times { @client.update }
102
+ 3.times { @client.update; sleep 0.2; @server.update; @client.flush }
103
+ @server.start
104
+ 3.times { @client.update; sleep 0.2; @server.update; @client.flush }
105
+ @client.should be_connected
106
+ end
107
+
108
+ after :each do
109
+ @client.close
110
+ @server.close
66
111
  end
67
112
  end
68
113
 
69
114
  describe "Network communication" do
70
115
  before :each do
71
116
  @server = Chingu::GameStates::NetworkServer.new(:port => 9999).start
72
- @client = Chingu::GameStates::NetworkClient.new(:ip => "127.0.0.1", :port => 9999).connect
73
- @client2 = Chingu::GameStates::NetworkClient.new(:ip => "127.0.0.1", :port => 9999).connect
117
+ @client = Chingu::GameStates::NetworkClient.new(:address => "127.0.0.1", :port => 9999).connect
118
+ @client2 = Chingu::GameStates::NetworkClient.new(:address => "127.0.0.1", :port => 9999).connect
74
119
  @client.update until @client.connected?
75
120
  @client2.update until @client2.connected?
76
121
  end
@@ -123,6 +168,94 @@ module Chingu
123
168
  end
124
169
  end
125
170
  end
171
+
172
+ describe "byte and packet counters" do
173
+ before :each do
174
+ @packet = "Hello! " * 10
175
+ @packet_length = Marshal.dump(@packet).length
176
+ @packet_length_with_header = @packet_length + 4
177
+ end
178
+
179
+ it "should be zeroed initially" do
180
+ [@client, @client2, @server].each do |network|
181
+ network.packets_sent.should be 0
182
+ network.bytes_sent.should be 0
183
+ network.packets_received.should be 0
184
+ network.bytes_received.should be 0
185
+ end
186
+ end
187
+
188
+ describe "client to server" do
189
+ before :each do
190
+ @client.send_msg(@packet)
191
+ @server.update
192
+ end
193
+
194
+ describe "client" do
195
+ it "should increment counters correctly when sending a message" do
196
+ @client.packets_sent.should eq 1
197
+ @client.bytes_sent.should eq @packet_length_with_header
198
+ end
199
+ end
200
+
201
+ describe "server" do
202
+ it "should increment counters correctly when receiving a message" do
203
+ @server.packets_received.should eq 1
204
+ @server.bytes_received.should eq @packet_length_with_header
205
+ end
206
+ end
207
+ end
208
+
209
+ describe "server to client" do
210
+ before :each do
211
+ @server.update
212
+ @server.send_msg(@server.sockets[0], @packet)
213
+ @client.update
214
+ end
215
+
216
+ describe "server" do
217
+ it "should increment sent counters" do
218
+ @server.packets_sent.should eq 1
219
+ @server.bytes_sent.should eq @packet_length_with_header
220
+ end
221
+ end
222
+
223
+ describe "client" do
224
+ it "should increment received counters" do
225
+ @client.packets_received.should eq 1
226
+ @client.bytes_received.should eq @packet_length_with_header
227
+ @client2.packets_received.should eq 0
228
+ @client2.bytes_received.should eq 0
229
+ end
230
+ end
231
+ end
232
+
233
+ describe "server to clients" do
234
+ before :each do
235
+ @server.update
236
+ @server.broadcast_msg(@packet)
237
+ @client.update
238
+ @client2.update
239
+ end
240
+
241
+ describe "server" do
242
+ it "should increment sent counters" do
243
+ # Single message, broadcast to two clients.
244
+ @server.packets_sent.should eq 2
245
+ @server.bytes_sent.should eq @packet_length_with_header * 2
246
+ end
247
+ end
248
+
249
+ describe "clients" do
250
+ it "should increment received counters" do
251
+ [@client, @client2].each do |client|
252
+ client.packets_received.should eq 1
253
+ client.bytes_received.should eq @packet_length_with_header
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
126
259
  end
127
260
  end
128
261
  end