game_2d 0.0.1 → 0.0.2
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/README.md +18 -5
- data/bin/game_2d_client.rb +3 -2
- data/lib/game_2d/client_connection.rb +75 -20
- data/lib/game_2d/client_engine.rb +35 -34
- data/lib/game_2d/complex_move.rb +1 -1
- data/lib/game_2d/encryption.rb +35 -0
- data/lib/game_2d/entity.rb +47 -31
- data/lib/game_2d/entity/block.rb +2 -6
- data/lib/game_2d/entity/destination.rb +17 -0
- data/lib/game_2d/entity/owned_entity.rb +3 -2
- data/lib/game_2d/entity/pellet.rb +0 -8
- data/lib/game_2d/entity/teleporter.rb +46 -0
- data/lib/game_2d/game.rb +82 -23
- data/lib/game_2d/game_client.rb +360 -0
- data/lib/game_2d/game_space.rb +104 -18
- data/lib/game_2d/game_window.rb +17 -216
- data/lib/game_2d/menu.rb +3 -3
- data/lib/game_2d/message.rb +37 -0
- data/lib/game_2d/move/rise_up.rb +2 -2
- data/lib/game_2d/password_dialog.rb +44 -0
- data/lib/game_2d/player.rb +15 -18
- data/lib/game_2d/registerable.rb +1 -1
- data/lib/game_2d/serializable.rb +2 -13
- data/lib/game_2d/server_connection.rb +56 -24
- data/lib/game_2d/server_port.rb +14 -8
- data/lib/game_2d/transparency.rb +59 -0
- data/lib/game_2d/version.rb +1 -1
- data/lib/game_2d/zorder.rb +1 -1
- data/media/destination.png +0 -0
- data/media/destination.xcf +0 -0
- data/spec/client_engine_spec.rb +162 -71
- data/spec/game_space_spec.rb +8 -9
- metadata +11 -2
data/lib/game_2d/entity/block.rb
CHANGED
@@ -45,12 +45,6 @@ class Block < OwnedEntity
|
|
45
45
|
move
|
46
46
|
end
|
47
47
|
|
48
|
-
def transparent_to_me?(other)
|
49
|
-
super ||
|
50
|
-
(other.registry_id == owner_id) ||
|
51
|
-
(other.is_a?(Pellet) && other.owner_id == owner_id)
|
52
|
-
end
|
53
|
-
|
54
48
|
def harmed_by(other)
|
55
49
|
puts "#{self}: Ouch!"
|
56
50
|
self.hp -= 1
|
@@ -68,6 +62,8 @@ class Block < OwnedEntity
|
|
68
62
|
end
|
69
63
|
|
70
64
|
def image_filename; "#{level_name}.gif"; end
|
65
|
+
|
66
|
+
def to_s; "#{super} (#{@hp} HP)"; end
|
71
67
|
end
|
72
68
|
|
73
69
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'game_2d/entity'
|
2
|
+
|
3
|
+
class Entity
|
4
|
+
|
5
|
+
class Destination < OwnedEntity
|
6
|
+
|
7
|
+
def should_fall?; false; end
|
8
|
+
|
9
|
+
def update; end
|
10
|
+
|
11
|
+
def image_filename; "destination.png"; end
|
12
|
+
|
13
|
+
def draw_zorder; ZOrder::Destination; end
|
14
|
+
def draw_angle; space.game.tick % 360; end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -6,7 +6,8 @@ class OwnedEntity < Entity
|
|
6
6
|
attr_reader :owner_id
|
7
7
|
|
8
8
|
def owner_id=(id)
|
9
|
-
@owner_id = id.try(:to_sym)
|
9
|
+
old_owner_id, @owner_id = @owner_id, id.try(:to_sym)
|
10
|
+
space.owner_change(registry_id, old_owner_id, @owner_id) if space && registry_id?
|
10
11
|
end
|
11
12
|
|
12
13
|
def owner
|
@@ -21,7 +22,7 @@ class OwnedEntity < Entity
|
|
21
22
|
def all_state; super.push(owner_id); end
|
22
23
|
def as_json; super.merge! :owner => owner_id; end
|
23
24
|
def update_from_json(json)
|
24
|
-
self.owner_id = json[:owner]
|
25
|
+
self.owner_id = json[:owner] if json[:owner]
|
25
26
|
super
|
26
27
|
end
|
27
28
|
end
|
@@ -7,14 +7,6 @@ class Pellet < OwnedEntity
|
|
7
7
|
def should_fall?; true end
|
8
8
|
def sleep_now?; false end
|
9
9
|
|
10
|
-
# Pellets don't hit the originating player, or other
|
11
|
-
# pellets fired by the same player
|
12
|
-
def transparent_to_me?(other)
|
13
|
-
super ||
|
14
|
-
other.registry_id == self.owner_id ||
|
15
|
-
((other.is_a?(Pellet) || other.is_a?(Block)) && other.owner_id == self.owner_id)
|
16
|
-
end
|
17
|
-
|
18
10
|
def i_hit(others)
|
19
11
|
puts "#{self}: hit #{others.inspect}. That's all for me."
|
20
12
|
others.each {|other| other.harmed_by(self)}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'game_2d/entity'
|
2
|
+
require 'game_2d/entity/owned_entity'
|
3
|
+
|
4
|
+
class Entity
|
5
|
+
|
6
|
+
class Teleporter < Entity
|
7
|
+
def should_fall?; false; end
|
8
|
+
|
9
|
+
def update
|
10
|
+
space.entities_overlapping(x, y).each do |overlap|
|
11
|
+
next if overlap == self
|
12
|
+
next if (overlap.x - x).abs > WIDTH/2
|
13
|
+
next if (overlap.y - y).abs > HEIGHT/2
|
14
|
+
dest = space.possessions(self)
|
15
|
+
case dest.size
|
16
|
+
when 1 then
|
17
|
+
dest = dest.first
|
18
|
+
if overlap.entities_obstructing(dest.x, dest.y).empty?
|
19
|
+
overlap.warp(dest.x, dest.y)
|
20
|
+
overlap.wake!
|
21
|
+
end
|
22
|
+
when 0 then
|
23
|
+
$stderr.puts "#{self}: No destination"
|
24
|
+
else
|
25
|
+
$stderr.puts "#{self}: Multiple destinations: #{dest.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy!
|
31
|
+
# destroy destination
|
32
|
+
end
|
33
|
+
|
34
|
+
def image_filename; "tele.gif"; end
|
35
|
+
|
36
|
+
def draw_zorder; ZOrder::Teleporter end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
destinations = space.possessions(self).collect do |d|
|
40
|
+
"#{d.x}x#{d.y}"
|
41
|
+
end.join(', ')
|
42
|
+
"#{super} => [#{destinations}]"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/game_2d/game.rb
CHANGED
@@ -27,8 +27,10 @@ DEFAULT_REGISTRY_BROADCAST_EVERY = TICKS_PER_SECOND / 4
|
|
27
27
|
|
28
28
|
class Game
|
29
29
|
def initialize(args)
|
30
|
-
|
31
|
-
|
30
|
+
all_storage = Storage.in_home_dir(args[:storage] || DEFAULT_STORAGE)
|
31
|
+
@player_storage = all_storage.dir('players')['players']
|
32
|
+
@levels_storage = all_storage.dir('levels')
|
33
|
+
level_storage = @levels_storage[args[:level]]
|
32
34
|
|
33
35
|
if level_storage.empty?
|
34
36
|
@space = GameSpace.new(self).establish_world(
|
@@ -83,6 +85,15 @@ class Game
|
|
83
85
|
@space.save
|
84
86
|
end
|
85
87
|
|
88
|
+
def player_data(player_name)
|
89
|
+
@player_storage[player_name]
|
90
|
+
end
|
91
|
+
|
92
|
+
def store_player_data(player_name, data)
|
93
|
+
@player_storage[player_name] = data
|
94
|
+
@player_storage.save
|
95
|
+
end
|
96
|
+
|
86
97
|
def add_player(player_name)
|
87
98
|
player = Player.new(player_name)
|
88
99
|
player.x = (@space.width - Entity::WIDTH) / 2
|
@@ -101,26 +112,36 @@ class Game
|
|
101
112
|
player_id_connection(player.registry_id)
|
102
113
|
end
|
103
114
|
|
115
|
+
def each_player_conn
|
116
|
+
get_all_players.each {|p| yield player_connection(p)}
|
117
|
+
end
|
118
|
+
|
104
119
|
def delete_entity(entity)
|
105
120
|
puts "Deleting #{entity}"
|
106
121
|
@space.doom entity
|
107
122
|
@space.purge_doomed_entities
|
108
|
-
|
123
|
+
each_player_conn {|pc| pc.delete_entity entity, @tick }
|
109
124
|
end
|
110
125
|
|
111
126
|
# Answering request from client
|
112
|
-
def
|
113
|
-
|
127
|
+
def add_npcs(npcs_json)
|
128
|
+
npcs_json.each {|json| @space << Serializable.from_json(json) }
|
114
129
|
end
|
115
130
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
131
|
+
def update_npcs(npcs_json)
|
132
|
+
npcs_json.each do |json|
|
133
|
+
id = json[:registry_id]
|
134
|
+
if entity = @space[id]
|
135
|
+
entity.update_from_json json
|
136
|
+
entity.grab!
|
137
|
+
else
|
138
|
+
$stderr.puts "Can't update #{id}, doesn't exist"
|
139
|
+
end
|
140
|
+
end
|
120
141
|
end
|
121
142
|
|
122
143
|
def send_updated_entities(*entities)
|
123
|
-
|
144
|
+
each_player_conn {|pc| pc.update_entities entities, @tick }
|
124
145
|
end
|
125
146
|
|
126
147
|
def [](id)
|
@@ -135,8 +156,8 @@ class Game
|
|
135
156
|
@space.npcs
|
136
157
|
end
|
137
158
|
|
138
|
-
def add_player_action(
|
139
|
-
at_tick = action[:at_tick]
|
159
|
+
def add_player_action(action)
|
160
|
+
at_tick, player_id = action[:at_tick], action[:player_id]
|
140
161
|
unless at_tick
|
141
162
|
$stderr.puts "Received update from #{player_id} without at_tick!"
|
142
163
|
at_tick = @tick + 1
|
@@ -145,12 +166,13 @@ class Game
|
|
145
166
|
$stderr.puts "Received update from #{player_id} #{@tick + 1 - at_tick} ticks late"
|
146
167
|
at_tick = @tick + 1
|
147
168
|
end
|
148
|
-
@player_actions[at_tick] <<
|
169
|
+
@player_actions[at_tick] << action
|
149
170
|
end
|
150
171
|
|
151
172
|
def process_player_actions
|
152
173
|
if actions = @player_actions.delete(@tick)
|
153
|
-
actions.each do |
|
174
|
+
actions.each do |action|
|
175
|
+
player_id = action.delete :player_id
|
154
176
|
player = @space[player_id]
|
155
177
|
unless player
|
156
178
|
$stderr.puts "No such player #{player_id} -- dropping move"
|
@@ -158,8 +180,12 @@ class Game
|
|
158
180
|
end
|
159
181
|
if (move = action[:move])
|
160
182
|
player.add_move move
|
161
|
-
elsif (
|
162
|
-
|
183
|
+
elsif (npcs = action[:add_npcs])
|
184
|
+
add_npcs npcs
|
185
|
+
elsif (entities = action[:update_entities])
|
186
|
+
update_npcs entities
|
187
|
+
elsif (entity_id = action[:snap_to_grid])
|
188
|
+
@space.snap_to_grid entity_id.to_sym
|
163
189
|
else
|
164
190
|
$stderr.puts "IGNORING BAD DATA from #{player}: #{action.inspect}"
|
165
191
|
end
|
@@ -173,7 +199,7 @@ class Game
|
|
173
199
|
# This will:
|
174
200
|
# 1) Queue up player actions for existing players
|
175
201
|
# (create_npc included)
|
176
|
-
# 2) Add new players in response to
|
202
|
+
# 2) Add new players in response to login messages
|
177
203
|
# 3) Remove players in response to disconnections
|
178
204
|
@port.update
|
179
205
|
|
@@ -181,14 +207,13 @@ class Game
|
|
181
207
|
process_player_actions
|
182
208
|
|
183
209
|
# Objects that exist by now will be updated
|
184
|
-
# Objects created during this
|
210
|
+
# Objects created during this update won't be updated
|
211
|
+
# themselves this tick
|
185
212
|
@space.update
|
186
213
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
:at_tick => @tick
|
191
|
-
) if @registry_broadcast_every > 0 && (@tick % @registry_broadcast_every == 0)
|
214
|
+
# Do this at the end, so the update contains all the
|
215
|
+
# latest and greatest news
|
216
|
+
send_full_updates
|
192
217
|
|
193
218
|
if @self_check
|
194
219
|
@space.check_for_grid_corruption
|
@@ -196,6 +221,40 @@ class Game
|
|
196
221
|
end
|
197
222
|
end
|
198
223
|
|
224
|
+
# New players always get a full update (with some additional
|
225
|
+
# information)
|
226
|
+
# Everyone else gets full registry dump every N ticks, where
|
227
|
+
# N == @registry_broadcast_every
|
228
|
+
def send_full_updates
|
229
|
+
# Set containing brand-new players' IDs
|
230
|
+
new_players = @port.new_players
|
231
|
+
|
232
|
+
each_player_conn do |pc|
|
233
|
+
if new_players.include? pc.player_id
|
234
|
+
response = {
|
235
|
+
:you_are => pc.player_id,
|
236
|
+
:world => {
|
237
|
+
:world_name => world_name,
|
238
|
+
:world_id => world_id,
|
239
|
+
:highest_id => world_highest_id,
|
240
|
+
:cell_width => world_cell_width,
|
241
|
+
:cell_height => world_cell_height,
|
242
|
+
},
|
243
|
+
:add_players => get_all_players,
|
244
|
+
:add_npcs => get_all_npcs,
|
245
|
+
:at_tick => tick,
|
246
|
+
}
|
247
|
+
pc.send_record response, true # answer login reliably
|
248
|
+
elsif @registry_broadcast_every > 0 && (@tick % @registry_broadcast_every == 0)
|
249
|
+
pc.send_record( {
|
250
|
+
:registry => @space.all_registered,
|
251
|
+
:highest_id => @space.highest_id,
|
252
|
+
:at_tick => @tick,
|
253
|
+
}, false, 1 )
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
199
258
|
def run
|
200
259
|
run_start = Time.now.to_r
|
201
260
|
loop do
|
@@ -0,0 +1,360 @@
|
|
1
|
+
## Author: Greg Meyers
|
2
|
+
## License: Same as for Gosu (MIT)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'facets/kernel/try'
|
6
|
+
require 'gosu'
|
7
|
+
|
8
|
+
require 'game_2d/client_connection'
|
9
|
+
require 'game_2d/client_engine'
|
10
|
+
require 'game_2d/game_space'
|
11
|
+
require 'game_2d/entity'
|
12
|
+
require 'game_2d/entity_constants'
|
13
|
+
require 'game_2d/entity/block'
|
14
|
+
require 'game_2d/entity/titanium'
|
15
|
+
require 'game_2d/entity/teleporter'
|
16
|
+
require 'game_2d/entity/destination'
|
17
|
+
require 'game_2d/player'
|
18
|
+
require 'game_2d/menu'
|
19
|
+
require 'game_2d/message'
|
20
|
+
require 'game_2d/password_dialog'
|
21
|
+
require 'game_2d/zorder'
|
22
|
+
|
23
|
+
# We put as many methods here as possible, so we can test them
|
24
|
+
# without instantiating an actual Gosu window
|
25
|
+
module GameClient
|
26
|
+
# Gosu methods we call:
|
27
|
+
# caption=(text)
|
28
|
+
# width
|
29
|
+
# mouse_x
|
30
|
+
# mouse_y
|
31
|
+
# close
|
32
|
+
# button_down?
|
33
|
+
# text_input=(Gosu::TextInput instance)
|
34
|
+
# draw_quad (from draw only)
|
35
|
+
# translate (from draw only)
|
36
|
+
#
|
37
|
+
# Gosu methods it calls on us:
|
38
|
+
# draw
|
39
|
+
# button_down
|
40
|
+
|
41
|
+
include EntityConstants
|
42
|
+
|
43
|
+
SCREEN_WIDTH = 640 # in pixels
|
44
|
+
SCREEN_HEIGHT = 480 # in pixels
|
45
|
+
|
46
|
+
DEFAULT_PORT = 4321
|
47
|
+
DEFAULT_KEY_SIZE = 1024
|
48
|
+
|
49
|
+
attr_reader :animation, :font, :top_menu
|
50
|
+
attr_accessor :player_id
|
51
|
+
|
52
|
+
def initialize_from_hash(opts = {})
|
53
|
+
player_name = opts[:name]
|
54
|
+
hostname = opts[:hostname]
|
55
|
+
port = opts[:port] || DEFAULT_PORT
|
56
|
+
key_size = opts[:key_size] || DEFAULT_KEY_SIZE
|
57
|
+
profile = opts[:profile] || false
|
58
|
+
|
59
|
+
@conn_update_total = @engine_update_total = 0.0
|
60
|
+
@conn_update_count = @engine_update_count = 0
|
61
|
+
@profile = profile
|
62
|
+
|
63
|
+
self.caption = "Game 2D"
|
64
|
+
|
65
|
+
@pressed_buttons = []
|
66
|
+
|
67
|
+
@snap_to_grid = false
|
68
|
+
|
69
|
+
@create_npc_proc = make_block_npc_proc(5)
|
70
|
+
|
71
|
+
@grabbed_entity_id = nil
|
72
|
+
|
73
|
+
@run_start = Time.now.to_f
|
74
|
+
@update_count = 0
|
75
|
+
|
76
|
+
@conn = _make_client_connection(hostname, port, self, player_name, key_size)
|
77
|
+
@engine = @conn.engine = ClientEngine.new(self)
|
78
|
+
@menu = build_top_menu
|
79
|
+
@dialog = PasswordDialog.new(self, @font)
|
80
|
+
end
|
81
|
+
|
82
|
+
def _make_client_connection(*args)
|
83
|
+
ClientConnection.new(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def display_message(*lines)
|
87
|
+
if @message
|
88
|
+
@message.lines = lines
|
89
|
+
else
|
90
|
+
@message = Message.new(self, @font, lines)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def message_drawn?; @message.try(:drawn?); end
|
95
|
+
|
96
|
+
# Ensure the message is drawn at least once
|
97
|
+
def display_message!(*lines)
|
98
|
+
display_message(*lines)
|
99
|
+
sleep 0.01 until message_drawn?
|
100
|
+
end
|
101
|
+
|
102
|
+
def clear_message
|
103
|
+
@message = nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_top_menu
|
107
|
+
@top_menu = MenuItem.new('Click for menu', self, @font) { main_menu }
|
108
|
+
end
|
109
|
+
|
110
|
+
def main_menu
|
111
|
+
Menu.new('Main menu', self, @font,
|
112
|
+
MenuItem.new('Object creation', self, @font) { object_creation_menu },
|
113
|
+
MenuItem.new('Quit!', self, @font) { shutdown }
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
def object_creation_menu
|
118
|
+
snap_text = lambda do |item|
|
119
|
+
@snap_to_grid ? "Turn snap off" : "Turn snap on"
|
120
|
+
end
|
121
|
+
|
122
|
+
Menu.new('Object creation', self, @font,
|
123
|
+
MenuItem.new('Object type', self, @font) { object_type_menu },
|
124
|
+
MenuItem.new(snap_text, self, @font) do
|
125
|
+
@snap_to_grid = !@snap_to_grid
|
126
|
+
end,
|
127
|
+
MenuItem.new('Save!', self, @font) { @conn.send_save }
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
def object_type_menu
|
132
|
+
Menu.new('Object type', self, @font, *object_type_submenus)
|
133
|
+
end
|
134
|
+
|
135
|
+
def object_type_submenus
|
136
|
+
[
|
137
|
+
['Dirt', make_block_npc_proc( 5)],
|
138
|
+
['Brick', make_block_npc_proc(10)],
|
139
|
+
['Cement', make_block_npc_proc(15)],
|
140
|
+
['Steel', make_block_npc_proc(20)],
|
141
|
+
['Unlikelium', make_block_npc_proc(25)],
|
142
|
+
['Titanium', make_block_npc_proc( 0)],
|
143
|
+
['Teleporter', make_teleporter_npc_proc],
|
144
|
+
].collect do |type_name, p|
|
145
|
+
MenuItem.new(type_name, self, @font) { @create_npc_proc = p }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def media(filename)
|
150
|
+
"#{File.dirname __FILE__}/../../media/#{filename}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def space
|
154
|
+
@engine.space
|
155
|
+
end
|
156
|
+
|
157
|
+
def tick
|
158
|
+
@engine.tick
|
159
|
+
end
|
160
|
+
|
161
|
+
def player
|
162
|
+
space[@player_id]
|
163
|
+
end
|
164
|
+
|
165
|
+
def update
|
166
|
+
@update_count += 1
|
167
|
+
|
168
|
+
return unless @conn.online?
|
169
|
+
|
170
|
+
# Handle any pending ENet events
|
171
|
+
before_t = Time.now.to_f
|
172
|
+
@conn.update
|
173
|
+
if @profile
|
174
|
+
@conn_update_total += (Time.now.to_f - before_t)
|
175
|
+
@conn_update_count += 1
|
176
|
+
$stderr.puts "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
|
177
|
+
end
|
178
|
+
return unless @engine.world_established?
|
179
|
+
|
180
|
+
before_t = Time.now.to_f
|
181
|
+
@engine.update
|
182
|
+
if @profile
|
183
|
+
@engine_update_total += (Time.now.to_f - before_t)
|
184
|
+
@engine_update_count += 1
|
185
|
+
$stderr.puts "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
|
186
|
+
end
|
187
|
+
|
188
|
+
# Player at the keyboard queues up a command
|
189
|
+
# @pressed_buttons is emptied by handle_input
|
190
|
+
handle_input if @player_id
|
191
|
+
|
192
|
+
move_grabbed_entity
|
193
|
+
|
194
|
+
$stderr.puts "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
|
195
|
+
end
|
196
|
+
|
197
|
+
def move_grabbed_entity(divide_by = ClientConnection::ACTION_DELAY)
|
198
|
+
return unless @grabbed_entity_id
|
199
|
+
return unless grabbed = space[@grabbed_entity_id]
|
200
|
+
dest_x, dest_y = mouse_entity_location
|
201
|
+
vel_x = Entity.constrain_velocity((dest_x - grabbed.x) / divide_by)
|
202
|
+
vel_y = Entity.constrain_velocity((dest_y - grabbed.y) / divide_by)
|
203
|
+
@conn.send_update_entity(
|
204
|
+
:registry_id => grabbed.registry_id,
|
205
|
+
:velocity => [vel_x, vel_y],
|
206
|
+
:moving => true)
|
207
|
+
end
|
208
|
+
|
209
|
+
def button_down(id)
|
210
|
+
case id
|
211
|
+
when Gosu::KbEnter, Gosu::KbReturn then
|
212
|
+
if @dialog
|
213
|
+
@dialog.enter
|
214
|
+
@conn.start(@dialog.password_hash)
|
215
|
+
@dialog = nil
|
216
|
+
end
|
217
|
+
when Gosu::KbP then
|
218
|
+
@conn.send_ping unless @dialog
|
219
|
+
when Gosu::KbEscape then @menu = @top_menu
|
220
|
+
when Gosu::MsLeft then # left-click
|
221
|
+
if new_menu = @menu.handle_click
|
222
|
+
# If handle_click returned anything, the menu consumed the click
|
223
|
+
# If it returned a menu, that's the new one we display
|
224
|
+
@menu = (new_menu.respond_to?(:handle_click) ? new_menu : @top_menu)
|
225
|
+
else
|
226
|
+
send_fire
|
227
|
+
end
|
228
|
+
when Gosu::MsRight then # right-click
|
229
|
+
if button_down?(Gosu::KbRightShift) || button_down?(Gosu::KbLeftShift)
|
230
|
+
@create_npc_proc.call
|
231
|
+
else
|
232
|
+
toggle_grab
|
233
|
+
end
|
234
|
+
when Gosu::KbB then @create_npc_proc.call
|
235
|
+
when Gosu::Kb1 then @create_npc_proc = make_block_npc_proc( 5).call
|
236
|
+
when Gosu::Kb2 then @create_npc_proc = make_block_npc_proc(10).call
|
237
|
+
when Gosu::Kb3 then @create_npc_proc = make_block_npc_proc(15).call
|
238
|
+
when Gosu::Kb4 then @create_npc_proc = make_block_npc_proc(20).call
|
239
|
+
when Gosu::Kb5 then @create_npc_proc = make_block_npc_proc(25).call
|
240
|
+
when Gosu::Kb6 then @create_npc_proc = make_block_npc_proc( 0).call
|
241
|
+
when Gosu::Kb7 then @create_npc_proc = make_teleporter_npc_proc.call
|
242
|
+
else @pressed_buttons << id unless @dialog
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def send_fire
|
247
|
+
return unless @player_id
|
248
|
+
x, y = mouse_coords
|
249
|
+
x_vel = (x - (player.x + WIDTH / 2)) / PIXEL_WIDTH
|
250
|
+
y_vel = (y - (player.y + WIDTH / 2)) / PIXEL_WIDTH
|
251
|
+
@conn.send_move :fire, :x_vel => x_vel, :y_vel => y_vel
|
252
|
+
end
|
253
|
+
|
254
|
+
# X/Y position of the mouse (center of the crosshairs), adjusted for camera
|
255
|
+
def mouse_coords
|
256
|
+
# For some reason, Gosu's mouse_x/mouse_y return Floats, so round it off
|
257
|
+
[
|
258
|
+
(mouse_x.round + @camera_x) * PIXEL_WIDTH,
|
259
|
+
(mouse_y.round + @camera_y) * PIXEL_WIDTH
|
260
|
+
]
|
261
|
+
end
|
262
|
+
|
263
|
+
def mouse_entity_location
|
264
|
+
x, y = mouse_coords
|
265
|
+
|
266
|
+
if @snap_to_grid
|
267
|
+
# When snap is on, we want the upper-left corner of the cell we point at
|
268
|
+
return (x / WIDTH) * WIDTH, (y / HEIGHT) * HEIGHT
|
269
|
+
else
|
270
|
+
# When snap is off, we are pointing at the entity's center, not
|
271
|
+
# its upper-left corner
|
272
|
+
return x - WIDTH / 2, y - HEIGHT / 2
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def make_block_npc_proc(hp)
|
277
|
+
type = hp.zero? ? 'Entity::Titanium' : 'Entity::Block'
|
278
|
+
proc { send_create_npc type, :hp => hp }
|
279
|
+
end
|
280
|
+
|
281
|
+
def make_teleporter_npc_proc
|
282
|
+
proc do
|
283
|
+
send_create_npc 'Entity::Teleporter', :on_create => make_destination_npc_proc
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def make_destination_npc_proc
|
288
|
+
proc do |teleporter|
|
289
|
+
send_create_npc 'Entity::Destination', :owner => teleporter.registry_id,
|
290
|
+
:on_create => make_grab_destination_proc
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def make_grab_destination_proc
|
295
|
+
proc do |destination|
|
296
|
+
grab_specific destination.registry_id
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def send_create_npc(type, args)
|
301
|
+
args.merge!(
|
302
|
+
:class => type,
|
303
|
+
:position => mouse_entity_location,
|
304
|
+
:velocity => [0, 0],
|
305
|
+
:angle => 0,
|
306
|
+
:moving => true
|
307
|
+
)
|
308
|
+
@conn.send_create_npc(args)
|
309
|
+
end
|
310
|
+
|
311
|
+
def grab_specific(registry_id)
|
312
|
+
@grabbed_entity_id = registry_id
|
313
|
+
end
|
314
|
+
|
315
|
+
def toggle_grab
|
316
|
+
if @grabbed_entity_id
|
317
|
+
@conn.send_snap_to_grid(space[@grabbed_entity_id]) if @snap_to_grid
|
318
|
+
return @grabbed_entity_id = nil
|
319
|
+
end
|
320
|
+
|
321
|
+
@grabbed_entity_id = space.near_to(*mouse_coords).nullsafe_registry_id
|
322
|
+
end
|
323
|
+
|
324
|
+
def shutdown
|
325
|
+
@conn.disconnect
|
326
|
+
close
|
327
|
+
end
|
328
|
+
|
329
|
+
# Dequeue an input event
|
330
|
+
def handle_input
|
331
|
+
return if player.falling? || @dialog
|
332
|
+
move = move_for_keypress
|
333
|
+
@conn.send_move move # also creates a delta in the engine
|
334
|
+
end
|
335
|
+
|
336
|
+
# Check keyboard, mouse, and pressed-button queue
|
337
|
+
# Return a motion symbol or nil
|
338
|
+
def move_for_keypress
|
339
|
+
# Generated once for each keypress
|
340
|
+
until @pressed_buttons.empty?
|
341
|
+
button = @pressed_buttons.shift
|
342
|
+
case button
|
343
|
+
when Gosu::KbUp, Gosu::KbW
|
344
|
+
return (player.building?) ? :rise_up : :flip
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Continuously-generated when key held down
|
349
|
+
case
|
350
|
+
when button_down?(Gosu::KbLeft), button_down?(Gosu::KbA)
|
351
|
+
:slide_left
|
352
|
+
when button_down?(Gosu::KbRight), button_down?(Gosu::KbD)
|
353
|
+
:slide_right
|
354
|
+
when button_down?(Gosu::KbRightControl), button_down?(Gosu::KbLeftControl)
|
355
|
+
:brake
|
356
|
+
when button_down?(Gosu::KbDown), button_down?(Gosu::KbS)
|
357
|
+
:build
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|