game_2d 0.0.1
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.
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +1 -0
- data/bin/game_2d_client.rb +17 -0
- data/bin/game_2d_server.rb +21 -0
- data/game_2d.gemspec +32 -0
- data/lib/game_2d/client_connection.rb +127 -0
- data/lib/game_2d/client_engine.rb +227 -0
- data/lib/game_2d/complex_move.rb +45 -0
- data/lib/game_2d/entity.rb +371 -0
- data/lib/game_2d/entity/block.rb +73 -0
- data/lib/game_2d/entity/owned_entity.rb +29 -0
- data/lib/game_2d/entity/pellet.rb +27 -0
- data/lib/game_2d/entity/titanium.rb +11 -0
- data/lib/game_2d/entity_constants.rb +14 -0
- data/lib/game_2d/game.rb +213 -0
- data/lib/game_2d/game_space.rb +462 -0
- data/lib/game_2d/game_window.rb +260 -0
- data/lib/game_2d/hash.rb +11 -0
- data/lib/game_2d/menu.rb +82 -0
- data/lib/game_2d/move/rise_up.rb +77 -0
- data/lib/game_2d/player.rb +251 -0
- data/lib/game_2d/registerable.rb +25 -0
- data/lib/game_2d/serializable.rb +69 -0
- data/lib/game_2d/server_connection.rb +104 -0
- data/lib/game_2d/server_port.rb +74 -0
- data/lib/game_2d/storage.rb +42 -0
- data/lib/game_2d/version.rb +3 -0
- data/lib/game_2d/wall.rb +21 -0
- data/lib/game_2d/zorder.rb +3 -0
- data/media/Beep.wav +0 -0
- data/media/Space.png +0 -0
- data/media/Star.png +0 -0
- data/media/Starfighter.bmp +0 -0
- data/media/brick.gif +0 -0
- data/media/cement.gif +0 -0
- data/media/crosshair.gif +0 -0
- data/media/dirt.gif +0 -0
- data/media/pellet.png +0 -0
- data/media/pellet.xcf +0 -0
- data/media/player.png +0 -0
- data/media/player.xcf +0 -0
- data/media/rock.png +0 -0
- data/media/rock.xcf +0 -0
- data/media/steel.gif +0 -0
- data/media/tele.gif +0 -0
- data/media/titanium.gif +0 -0
- data/media/unlikelium.gif +0 -0
- data/spec/client_engine_spec.rb +235 -0
- data/spec/game_space_spec.rb +347 -0
- metadata +246 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
## Author: Greg Meyers
|
2
|
+
## License: Same as for Gosu (MIT)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'gosu'
|
6
|
+
|
7
|
+
require 'game_2d/client_connection'
|
8
|
+
require 'game_2d/client_engine'
|
9
|
+
require 'game_2d/game_space'
|
10
|
+
require 'game_2d/entity'
|
11
|
+
require 'game_2d/player'
|
12
|
+
require 'game_2d/menu'
|
13
|
+
require 'game_2d/zorder'
|
14
|
+
|
15
|
+
SCREEN_WIDTH = 640 # in pixels
|
16
|
+
SCREEN_HEIGHT = 480 # in pixels
|
17
|
+
|
18
|
+
DEFAULT_PORT = 4321
|
19
|
+
|
20
|
+
# The Gosu::Window is always the "environment" of our game
|
21
|
+
# It also provides the pulse of our game
|
22
|
+
class GameWindow < Gosu::Window
|
23
|
+
attr_reader :animation, :font
|
24
|
+
attr_accessor :player_id
|
25
|
+
|
26
|
+
def initialize(player_name, hostname, port=DEFAULT_PORT, profile=false)
|
27
|
+
@conn_update_total = @engine_update_total = 0.0
|
28
|
+
@conn_update_count = @engine_update_count = 0
|
29
|
+
@profile = profile
|
30
|
+
|
31
|
+
super(SCREEN_WIDTH, SCREEN_HEIGHT, false, 16)
|
32
|
+
self.caption = "Ruby Gosu Game"
|
33
|
+
|
34
|
+
@pressed_buttons = []
|
35
|
+
|
36
|
+
@background_image = Gosu::Image.new(self, media("Space.png"), true)
|
37
|
+
@animation = Hash.new do |h, k|
|
38
|
+
h[k] = Gosu::Image::load_tiles(
|
39
|
+
self, k, Entity::CELL_WIDTH_IN_PIXELS, Entity::CELL_WIDTH_IN_PIXELS, false)
|
40
|
+
end
|
41
|
+
|
42
|
+
@cursor_anim = @animation[media("crosshair.gif")]
|
43
|
+
|
44
|
+
@beep = Gosu::Sample.new(self, media("Beep.wav")) # not used yet
|
45
|
+
|
46
|
+
@font = Gosu::Font.new(self, Gosu::default_font_name, 20)
|
47
|
+
|
48
|
+
# Local settings
|
49
|
+
@local = {
|
50
|
+
:create_npc => {
|
51
|
+
:type => 'Entity::Block',
|
52
|
+
:hp => 5,
|
53
|
+
:snap => false,
|
54
|
+
},
|
55
|
+
}
|
56
|
+
snap_text = lambda do |item|
|
57
|
+
@local[:create_npc][:snap] ? "Turn snap off" : "Turn snap on"
|
58
|
+
end
|
59
|
+
|
60
|
+
object_type_submenus = [
|
61
|
+
['Dirt', 'Entity::Block', 5],
|
62
|
+
['Brick', 'Entity::Block', 10],
|
63
|
+
['Cement', 'Entity::Block', 15],
|
64
|
+
['Steel', 'Entity::Block', 20],
|
65
|
+
['Unlikelium', 'Entity::Block', 25],
|
66
|
+
['Titanium', 'Entity::Titanium', 0]
|
67
|
+
].collect do |type_name, class_name, hp|
|
68
|
+
MenuItem.new(type_name, self, @font) do |item|
|
69
|
+
@local[:create_npc][:type] = class_name
|
70
|
+
@local[:create_npc][:hp] = hp
|
71
|
+
end
|
72
|
+
end
|
73
|
+
object_type_menu = Menu.new('Object type', self, @font,
|
74
|
+
*object_type_submenus)
|
75
|
+
|
76
|
+
object_creation_menu = Menu.new('Object creation', self, @font,
|
77
|
+
MenuItem.new('Object type', self, @font) { object_type_menu },
|
78
|
+
MenuItem.new(snap_text, self, @font) do
|
79
|
+
@local[:create_npc][:snap] = !@local[:create_npc][:snap]
|
80
|
+
end,
|
81
|
+
MenuItem.new('Save!', self, @font) { @conn.send_save }
|
82
|
+
)
|
83
|
+
main_menu = Menu.new('Main menu', self, @font,
|
84
|
+
MenuItem.new('Object creation', self, @font) { object_creation_menu },
|
85
|
+
MenuItem.new('Quit!', self, @font) { shutdown }
|
86
|
+
)
|
87
|
+
@menu = @top_menu = MenuItem.new('Click for menu', self, @font) { main_menu }
|
88
|
+
|
89
|
+
# Connect to server and kick off handshaking
|
90
|
+
# We will create our player object only after we've been accepted by the server
|
91
|
+
# and told our starting position
|
92
|
+
@conn = ClientConnection.new(hostname, port, self, player_name)
|
93
|
+
@engine = @conn.engine = ClientEngine.new(self)
|
94
|
+
@run_start = Time.now.to_f
|
95
|
+
@update_count = 0
|
96
|
+
end
|
97
|
+
|
98
|
+
def media(filename)
|
99
|
+
"#{File.dirname __FILE__}/../../media/#{filename}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def space
|
103
|
+
@engine.space
|
104
|
+
end
|
105
|
+
|
106
|
+
def player
|
107
|
+
space[@player_id]
|
108
|
+
end
|
109
|
+
|
110
|
+
def update
|
111
|
+
@update_count += 1
|
112
|
+
|
113
|
+
# Handle any pending ENet events
|
114
|
+
before_t = Time.now.to_f
|
115
|
+
@conn.update
|
116
|
+
if @profile
|
117
|
+
@conn_update_total += (Time.now.to_f - before_t)
|
118
|
+
@conn_update_count += 1
|
119
|
+
$stderr.puts "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
|
120
|
+
end
|
121
|
+
return unless @conn.online? && @engine
|
122
|
+
|
123
|
+
before_t = Time.now.to_f
|
124
|
+
@engine.update
|
125
|
+
if @profile
|
126
|
+
@engine_update_total += (Time.now.to_f - before_t)
|
127
|
+
@engine_update_count += 1
|
128
|
+
$stderr.puts "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# Player at the keyboard queues up a command
|
132
|
+
# @pressed_buttons is emptied by handle_input
|
133
|
+
handle_input if @player_id
|
134
|
+
|
135
|
+
$stderr.puts "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
|
136
|
+
end
|
137
|
+
|
138
|
+
def draw
|
139
|
+
@background_image.draw(0, 0, ZOrder::Background)
|
140
|
+
return unless @player_id
|
141
|
+
@camera_x, @camera_y = space.good_camera_position_for(player, SCREEN_WIDTH, SCREEN_HEIGHT)
|
142
|
+
translate(-@camera_x, -@camera_y) do
|
143
|
+
(space.players + space.npcs).each {|entity| entity.draw(self) }
|
144
|
+
end
|
145
|
+
|
146
|
+
space.players.sort.each_with_index do |player, num|
|
147
|
+
@font.draw("#{player.player_name}: #{player.score}", 10, 10 * (num * 2 + 1), ZOrder::Text, 1.0, 1.0, Gosu::Color::YELLOW)
|
148
|
+
end
|
149
|
+
|
150
|
+
@menu.draw
|
151
|
+
|
152
|
+
cursor_img = @cursor_anim[Gosu::milliseconds / 50 % @cursor_anim.size]
|
153
|
+
cursor_img.draw(
|
154
|
+
mouse_x - cursor_img.width / 2.0,
|
155
|
+
mouse_y - cursor_img.height / 2.0,
|
156
|
+
ZOrder::Cursor,
|
157
|
+
1, 1, Gosu::Color::WHITE, :add)
|
158
|
+
end
|
159
|
+
|
160
|
+
def draw_box_at(x1, y1, x2, y2, c)
|
161
|
+
draw_quad(x1, y1, c, x2, y1, c, x2, y2, c, x1, y2, c, ZOrder::Highlight)
|
162
|
+
end
|
163
|
+
|
164
|
+
def button_down(id)
|
165
|
+
case id
|
166
|
+
when Gosu::KbP then @conn.send_ping
|
167
|
+
when Gosu::KbEscape then @menu = @top_menu
|
168
|
+
when Gosu::MsLeft then # left-click
|
169
|
+
if new_menu = @menu.handle_click
|
170
|
+
# If handle_click returned anything, the menu consumed the click
|
171
|
+
# If it returned a menu, that's the new one we display
|
172
|
+
@menu = (new_menu.respond_to?(:handle_click) ? new_menu : @top_menu)
|
173
|
+
else
|
174
|
+
send_fire
|
175
|
+
end
|
176
|
+
when Gosu::MsRight then # right-click
|
177
|
+
send_create_npc
|
178
|
+
else @pressed_buttons << id
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def send_fire
|
183
|
+
return unless @player_id
|
184
|
+
x, y = mouse_coords
|
185
|
+
x_vel = (x - (player.x + Entity::WIDTH / 2)) / Entity::PIXEL_WIDTH
|
186
|
+
y_vel = (y - (player.y + Entity::WIDTH / 2)) / Entity::PIXEL_WIDTH
|
187
|
+
@conn.send_move :fire, :x_vel => x_vel, :y_vel => y_vel
|
188
|
+
end
|
189
|
+
|
190
|
+
# X/Y position of the mouse (center of the crosshairs), adjusted for camera
|
191
|
+
def mouse_coords
|
192
|
+
# For some reason, Gosu's mouse_x/mouse_y return Floats, so round it off
|
193
|
+
[
|
194
|
+
(mouse_x.round + @camera_x) * Entity::PIXEL_WIDTH,
|
195
|
+
(mouse_y.round + @camera_y) * Entity::PIXEL_WIDTH
|
196
|
+
]
|
197
|
+
end
|
198
|
+
|
199
|
+
def send_create_npc
|
200
|
+
x, y = mouse_coords
|
201
|
+
|
202
|
+
if @local[:create_npc][:snap]
|
203
|
+
# When snap is on, we want the upper-left corner of the cell we clicked in
|
204
|
+
x = (x / Entity::WIDTH) * Entity::WIDTH
|
205
|
+
y = (y / Entity::HEIGHT) * Entity::HEIGHT
|
206
|
+
else
|
207
|
+
# When snap is off, we want the click to be the new entity's center, not
|
208
|
+
# its upper-left corner
|
209
|
+
x -= Entity::WIDTH / 2
|
210
|
+
y -= Entity::HEIGHT / 2
|
211
|
+
end
|
212
|
+
|
213
|
+
@conn.send_create_npc(
|
214
|
+
:class => @local[:create_npc][:type],
|
215
|
+
:position => [x, y],
|
216
|
+
:velocity => [0, 0],
|
217
|
+
:angle => 0,
|
218
|
+
:moving => true,
|
219
|
+
:hp => @local[:create_npc][:hp]
|
220
|
+
)
|
221
|
+
end
|
222
|
+
|
223
|
+
def shutdown
|
224
|
+
@conn.disconnect
|
225
|
+
close
|
226
|
+
end
|
227
|
+
|
228
|
+
# Dequeue an input event
|
229
|
+
def handle_input
|
230
|
+
return if player.falling?
|
231
|
+
move = move_for_keypress
|
232
|
+
@conn.send_move move # also creates a delta in the engine
|
233
|
+
end
|
234
|
+
|
235
|
+
# Check keyboard, return a motion symbol or nil
|
236
|
+
#
|
237
|
+
#
|
238
|
+
def move_for_keypress
|
239
|
+
# Generated once for each keypress
|
240
|
+
until @pressed_buttons.empty?
|
241
|
+
button = @pressed_buttons.shift
|
242
|
+
case button
|
243
|
+
when Gosu::KbUp, Gosu::KbW
|
244
|
+
return (player.building?) ? :rise_up : :flip
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Continuously-generated when key held down
|
249
|
+
case
|
250
|
+
when button_down?(Gosu::KbLeft), button_down?(Gosu::KbA)
|
251
|
+
:slide_left
|
252
|
+
when button_down?(Gosu::KbRight), button_down?(Gosu::KbD)
|
253
|
+
:slide_right
|
254
|
+
when button_down?(Gosu::KbRightControl), button_down?(Gosu::KbLeftControl)
|
255
|
+
:brake
|
256
|
+
when button_down?(Gosu::KbDown), button_down?(Gosu::KbS)
|
257
|
+
:build
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
data/lib/game_2d/hash.rb
ADDED
data/lib/game_2d/menu.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'game_2d/zorder'
|
3
|
+
|
4
|
+
class Menu
|
5
|
+
def initialize(name, window, font, *choices)
|
6
|
+
@name, @window, @font, @choices = name, window, font, choices
|
7
|
+
|
8
|
+
@main_color, @select_color = Gosu::Color::YELLOW, Gosu::Color::CYAN
|
9
|
+
@right = window.width - 1
|
10
|
+
@choices.each_with_index do |choice, num|
|
11
|
+
choice.x = @right
|
12
|
+
choice.y = (num + 2) * 20
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def draw
|
17
|
+
str = to_s
|
18
|
+
@font.draw_rel(str, @window.width - 1, 0, ZOrder::Text, 1.0, 0.0, 1.0, 1.0,
|
19
|
+
@main_color)
|
20
|
+
x1, x2, y, c = @right - @font.text_width(str), @right, 20, @main_color
|
21
|
+
@window.draw_box_at(x1, y, x2, y+1, @main_color)
|
22
|
+
@choices.each(&:draw)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a true value if it handled the click
|
26
|
+
# May return a Menu or MenuItem to be set as the new menu to display
|
27
|
+
# May return simply 'true' if we should redisplay the top-level menu
|
28
|
+
def handle_click
|
29
|
+
@choices.collect(&:handle_click).compact.first
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
@name.respond_to?(:call) ? @name.call(self) : @name.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class MenuItem
|
38
|
+
attr_accessor :x, :y, :name
|
39
|
+
def initialize(name, window, font, &action)
|
40
|
+
@name, @window, @font, @action = name, window, font, action
|
41
|
+
@main_color, @select_color, @highlight_color =
|
42
|
+
Gosu::Color::YELLOW, Gosu::Color::BLACK, Gosu::Color::CYAN
|
43
|
+
|
44
|
+
# Default position: Upper-right corner
|
45
|
+
@x, @y = @window.width - 1, 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def mouse_over?
|
49
|
+
x, y = @window.mouse_x, @window.mouse_y
|
50
|
+
(y >= top) && (y < bottom) && (x > left)
|
51
|
+
end
|
52
|
+
|
53
|
+
def left; @x - @font.text_width(to_s); end
|
54
|
+
def right; @x; end
|
55
|
+
def top; @y; end
|
56
|
+
def bottom; @y + 20; end
|
57
|
+
|
58
|
+
def draw
|
59
|
+
selected = mouse_over?
|
60
|
+
color = choose_color(selected)
|
61
|
+
@font.draw_rel(to_s, @x, @y, ZOrder::Text, 1.0, 0.0, 1.0, 1.0, color)
|
62
|
+
if selected
|
63
|
+
@window.draw_box_at(left, top, right, bottom, @highlight_color)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def choose_color(selected)
|
68
|
+
selected ? @select_color : @main_color
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a true value if it handled the click
|
72
|
+
# May return a Menu or MenuItem to be set as the new menu to display
|
73
|
+
# May return simply 'true' if we should redisplay the top-level menu
|
74
|
+
def handle_click
|
75
|
+
return unless mouse_over?
|
76
|
+
@action.call(self) || true
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
@name.respond_to?(:call) ? @name.call(self) : @name.to_s
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'game_2d/complex_move'
|
2
|
+
require 'game_2d/entity_constants'
|
3
|
+
|
4
|
+
module Move
|
5
|
+
|
6
|
+
class RiseUp < ComplexMove
|
7
|
+
include EntityConstants
|
8
|
+
|
9
|
+
# Valid stages: :center, :rise, :reset
|
10
|
+
# Distance is how much further we need to go
|
11
|
+
# (in pixels) in stage :rise
|
12
|
+
attr_accessor :stage, :distance
|
13
|
+
def initialize(actor=nil)
|
14
|
+
super
|
15
|
+
@stage = :center
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_completion(actor)
|
19
|
+
actor.instance_exec { @x_vel = @y_vel = 0 }
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(actor)
|
23
|
+
# It's convenient to set 'self' to the Player
|
24
|
+
# object, here
|
25
|
+
actor.instance_exec(self) do |cm|
|
26
|
+
# Abort if the build_block gets destroyed
|
27
|
+
blok = build_block
|
28
|
+
return false unless blok
|
29
|
+
|
30
|
+
start_x, start_y = blok.x, blok.y
|
31
|
+
case cm.stage
|
32
|
+
when :center, :reset
|
33
|
+
if x == start_x && y == start_y
|
34
|
+
# If we're in reset, we're all done
|
35
|
+
return false if cm.stage == :reset
|
36
|
+
|
37
|
+
# Establish our velocity for :rise
|
38
|
+
cm.stage = :rise
|
39
|
+
@x_vel, @y_vel = angle_to_vector(a, PIXEL_WIDTH)
|
40
|
+
cm.distance = CELL_WIDTH_IN_PIXELS
|
41
|
+
return cm.update(self)
|
42
|
+
end
|
43
|
+
@x_vel = [[start_x - x, -PIXEL_WIDTH].max, PIXEL_WIDTH].min
|
44
|
+
@y_vel = [[start_y - y, -PIXEL_WIDTH].max, PIXEL_WIDTH].min
|
45
|
+
# move returns false: it failed somehow
|
46
|
+
return move
|
47
|
+
when :rise
|
48
|
+
# Success
|
49
|
+
return false if cm.distance.zero?
|
50
|
+
|
51
|
+
cm.distance -= 1
|
52
|
+
# move fails? Go to :reset
|
53
|
+
move || (cm.stage = :reset)
|
54
|
+
return true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def all_state
|
60
|
+
super.push @stage, @distance
|
61
|
+
end
|
62
|
+
def as_json
|
63
|
+
super.merge! :stage => @stage,
|
64
|
+
:distance => @distance
|
65
|
+
end
|
66
|
+
def update_from_json(json)
|
67
|
+
self.stage = json[:stage].to_sym
|
68
|
+
self.distance = json[:distance]
|
69
|
+
super
|
70
|
+
end
|
71
|
+
def to_s
|
72
|
+
"RiseUp[#{stage}, #{distance} to go]"
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require 'facets/kernel/try'
|
2
|
+
require 'gosu'
|
3
|
+
require 'game_2d/entity'
|
4
|
+
require 'game_2d/entity/pellet'
|
5
|
+
require 'game_2d/entity/block'
|
6
|
+
require 'game_2d/move/rise_up'
|
7
|
+
require 'game_2d/zorder'
|
8
|
+
|
9
|
+
# The base Player class representing what all Players have in common
|
10
|
+
# Moves can be enqueued by calling add_move
|
11
|
+
# Calling update() causes a move to be dequeued and executed, applying forces
|
12
|
+
# to the game object
|
13
|
+
#
|
14
|
+
# The server instantiates this class to represent each connected player
|
15
|
+
class Player < Entity
|
16
|
+
include Comparable
|
17
|
+
|
18
|
+
# Game ticks it takes before a block's HP is raised by 1
|
19
|
+
BUILD_TIME = 7
|
20
|
+
|
21
|
+
# Amount to decelerate each tick when braking
|
22
|
+
BRAKE_SPEED = 4
|
23
|
+
|
24
|
+
attr_accessor :player_name, :score
|
25
|
+
attr_reader :build_block_id
|
26
|
+
|
27
|
+
def initialize(player_name = "<unknown>")
|
28
|
+
super
|
29
|
+
@player_name = player_name
|
30
|
+
@score = 0
|
31
|
+
@moves = []
|
32
|
+
@current_move = nil
|
33
|
+
@falling = false
|
34
|
+
@build_block_id = nil
|
35
|
+
@build_level = 0
|
36
|
+
@complex_move = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def sleep_now?; false; end
|
40
|
+
|
41
|
+
def falling?; @falling; end
|
42
|
+
|
43
|
+
def build_block_id=(new_id)
|
44
|
+
@build_block_id = new_id.try(:to_sym)
|
45
|
+
end
|
46
|
+
|
47
|
+
def building?; @build_block_id; end
|
48
|
+
|
49
|
+
def build_block
|
50
|
+
return nil unless building?
|
51
|
+
fail "Can't look up build_block when not in a space" unless @space
|
52
|
+
@space[@build_block_id] or fail "Don't have build_block #{@build_block_id}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def destroy!
|
56
|
+
build_block.owner_id = nil if building?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Pellets don't hit the originating player
|
60
|
+
def transparent_to_me?(other)
|
61
|
+
super ||
|
62
|
+
(other == build_block) ||
|
63
|
+
(other.is_a?(Pellet) && other.owner == self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def update
|
67
|
+
fail "No space set for #{self}" unless @space
|
68
|
+
check_for_disown_block
|
69
|
+
|
70
|
+
if @complex_move
|
71
|
+
# returns true if more work to do
|
72
|
+
return if @complex_move.update(self)
|
73
|
+
@complex_move.on_completion(self)
|
74
|
+
@complex_move = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
underfoot = next_to(self.a + 180)
|
78
|
+
if @falling = underfoot.empty?
|
79
|
+
self.a = 0
|
80
|
+
accelerate(0, 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
args = @moves.shift
|
84
|
+
case (current_move = args.delete(:move).to_sym)
|
85
|
+
when :slide_left, :slide_right, :brake, :flip, :build, :rise_up
|
86
|
+
send current_move unless @falling
|
87
|
+
when :fire
|
88
|
+
fire args[:x_vel], args[:y_vel]
|
89
|
+
else
|
90
|
+
puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
|
91
|
+
end if args
|
92
|
+
|
93
|
+
# Only go around corner if sitting on exactly one object
|
94
|
+
if underfoot.size == 1
|
95
|
+
other = underfoot.first
|
96
|
+
# Figure out where corner is and whether we're about to reach or pass it
|
97
|
+
corner, distance, overshoot, turn = going_past_entity(other.x, other.y)
|
98
|
+
if corner
|
99
|
+
original_speed = @x_vel.abs + @y_vel.abs
|
100
|
+
original_dir = vector_to_angle
|
101
|
+
new_dir = original_dir + turn
|
102
|
+
|
103
|
+
# Make sure nothing occupies any space we're about to move through
|
104
|
+
if opaque(
|
105
|
+
@space.entities_overlapping(*corner) + next_to(new_dir, *corner)
|
106
|
+
).empty?
|
107
|
+
# Move to the corner
|
108
|
+
self.x_vel, self.y_vel = angle_to_vector(original_dir, distance)
|
109
|
+
move
|
110
|
+
|
111
|
+
# Turn and apply remaining velocity
|
112
|
+
# Make sure we move at least one subpixel so we don't sit exactly at
|
113
|
+
# the corner, and fall
|
114
|
+
self.a += turn
|
115
|
+
overshoot = 1 if overshoot.zero?
|
116
|
+
self.x_vel, self.y_vel = angle_to_vector(new_dir, overshoot)
|
117
|
+
move
|
118
|
+
|
119
|
+
self.x_vel, self.y_vel = angle_to_vector(new_dir, original_speed)
|
120
|
+
else
|
121
|
+
# Something's in the way -- possibly in front of us, or possibly
|
122
|
+
# around the corner
|
123
|
+
move
|
124
|
+
end
|
125
|
+
else
|
126
|
+
# Not yet reaching the corner -- or making a diagonal motion, for which
|
127
|
+
# we can't support going around the corner
|
128
|
+
move
|
129
|
+
end
|
130
|
+
else
|
131
|
+
# Straddling two objects, or falling
|
132
|
+
move
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check again whether we've moved off of a block
|
136
|
+
# we were building
|
137
|
+
check_for_disown_block
|
138
|
+
end
|
139
|
+
|
140
|
+
def slide_left; slide(self.a - 90); end
|
141
|
+
def slide_right; slide(self.a + 90); end
|
142
|
+
|
143
|
+
def slide(dir)
|
144
|
+
if opaque(next_to(dir)).empty?
|
145
|
+
accelerate(*angle_to_vector(dir))
|
146
|
+
else
|
147
|
+
self.a = dir + 180
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def brake
|
152
|
+
if @x_vel.zero?
|
153
|
+
self.y_vel = brake_velocity(@y_vel)
|
154
|
+
else
|
155
|
+
self.x_vel = brake_velocity(@x_vel)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def brake_velocity(v)
|
160
|
+
return 0 if v.abs < BRAKE_SPEED
|
161
|
+
sign = v <=> 0
|
162
|
+
sign * (v.abs - BRAKE_SPEED)
|
163
|
+
end
|
164
|
+
|
165
|
+
def flip
|
166
|
+
self.a += 180
|
167
|
+
end
|
168
|
+
|
169
|
+
# Create the actual pellet
|
170
|
+
def fire(x_vel, y_vel)
|
171
|
+
pellet = Entity::Pellet.new(@x, @y, 0, x_vel, y_vel)
|
172
|
+
pellet.owner = self
|
173
|
+
@space << pellet
|
174
|
+
end
|
175
|
+
|
176
|
+
# Create the actual block
|
177
|
+
def build
|
178
|
+
if building?
|
179
|
+
@build_level += 1
|
180
|
+
if @build_level >= BUILD_TIME
|
181
|
+
@build_level = 0
|
182
|
+
build_block.hp += 1
|
183
|
+
end
|
184
|
+
else
|
185
|
+
bb = Entity::Block.new(@x, @y)
|
186
|
+
bb.owner_id = registry_id
|
187
|
+
bb.hp = 1
|
188
|
+
@space << bb # generates an ID
|
189
|
+
@build_block_id = bb.registry_id
|
190
|
+
@build_level = 0
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def disown_block; $stderr.puts "#{self} disowning #{build_block}"; @build_block_id, @build_level = nil, 0; end
|
195
|
+
|
196
|
+
def check_for_disown_block
|
197
|
+
return unless building?
|
198
|
+
return if @space.entities_overlapping(@x, @y).include?(build_block)
|
199
|
+
build_block.owner_id = nil
|
200
|
+
build_block.wake!
|
201
|
+
disown_block
|
202
|
+
end
|
203
|
+
|
204
|
+
def rise_up
|
205
|
+
@complex_move = Move::RiseUp.new(self)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Accepts a hash, with a key :move => move_type
|
209
|
+
def add_move(new_move)
|
210
|
+
return unless new_move
|
211
|
+
@moves << new_move
|
212
|
+
end
|
213
|
+
|
214
|
+
def to_s
|
215
|
+
"#{player_name} (#{registry_id_safe}) at #{x}x#{y}"
|
216
|
+
end
|
217
|
+
|
218
|
+
def all_state
|
219
|
+
super.unshift(player_name).push(
|
220
|
+
score, build_block_id, @complex_move)
|
221
|
+
end
|
222
|
+
|
223
|
+
def as_json
|
224
|
+
super.merge!(
|
225
|
+
:player_name => player_name,
|
226
|
+
:score => score,
|
227
|
+
:build_block => @build_block_id,
|
228
|
+
:complex_move => @complex_move.as_json
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
def update_from_json(json)
|
233
|
+
@player_name = json[:player_name]
|
234
|
+
@score = json[:score]
|
235
|
+
@build_block_id = json[:build_block].try(:to_sym)
|
236
|
+
@complex_move = Serializable.from_json(json[:complex_move])
|
237
|
+
super
|
238
|
+
end
|
239
|
+
|
240
|
+
def image_filename; "player.png"; end
|
241
|
+
|
242
|
+
def draw_zorder; ZOrder::Player end
|
243
|
+
|
244
|
+
def draw(window)
|
245
|
+
super
|
246
|
+
window.font.draw_rel(player_name,
|
247
|
+
pixel_x + CELL_WIDTH_IN_PIXELS / 2, pixel_y, ZOrder::Text,
|
248
|
+
0.5, 1.0, # Centered X; above Y
|
249
|
+
1.0, 1.0, Gosu::Color::YELLOW)
|
250
|
+
end
|
251
|
+
end
|