game_2d 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -11,6 +11,13 @@ There's not much here yet, in terms of actual gameplay. At this point I'm layin
11
11
  * Entity positions and velocities are integers. Apart from player input, all entities' behaviors are predictable. This allows the client to make accurate predictions of the server state.
12
12
  * New Entities are assigned IDs in order, again, to allow the client to predict which ID will be assigned to each new entity.
13
13
  * Clients connect using REnet. The handshaking process creates a Player object to represent the client.
14
+ * Client and server use Diffie-Hellman key exchange to establish a shared secret. This is used to encrypt sensitive information, rather like SSH (but over REnet).
15
+ * Right now, the only sensitive information is the user's password. This allows the server to remember who's who.
16
+ * On startup, the client prompts the user to enter the password, which is encrypted and then sent to the server along with the player-name.
17
+ * Server keeps a record of each player and their password.
18
+ * Nothing else interesting here yet, but I fully expect to put an inventory here, once there is anything useful to carry around.
19
+ * This is also where I plan to put authorization data - who's a god player (level editor) and who isn't.
20
+ * Player data is persisted in ~/.game_2d/players/players as JSON text.
14
21
  * Keyboard commands control the player:
15
22
  * A and D, or LeftArrow and RightArrow: Hold down to accelerate.
16
23
  * LeftControl or RightControl: Hold down to brake (slow down).
@@ -19,15 +26,19 @@ There's not much here yet, in terms of actual gameplay. At this point I'm layin
19
26
  * When not building: Flip (turn 180 degrees).
20
27
  * When building: Rise up (ascend to the top of the built block).
21
28
  * Left button: Fire a pellet. Mouse click determines the direction and speed -- closer to the player means slower speed.
22
- * Right button: Build a block where you click. *(This will eventually become part of a level-editing mode.)*
29
+ * Right button: Grab an entity and drag it around. Right-click again to release. Grab is affected by snap-to-grid. *(This will eventually become part of a level-editing mode.)*
30
+ * Shifted right button, or B: Build a block where the mouse points. *(This will eventually become part of a level-editing mode.)*
31
+ * 1 through 7: Shortcuts for building Dirt, Brick, Cement, Steel, Unlikelium, Titanium, Teleporter.
32
+ * Mousing over an entity displays some basic information about it. *(This will eventually become part of a level-editing mode.)*
23
33
  * A simple menu system is also available. Left-click to select. Esc returns to the top-level.
24
34
  * The menus include a Save feature, which tells the server to save the level's state.
25
- * Levels are persisted in ~/.game_2d/<level name> as JSON text.
35
+ * Levels are persisted in ~/.game_2d/levels/<level name> as JSON text.
26
36
  * On subsequent startup with the same name, the level is loaded.
27
37
  * The client and server communicate using JSON text. This is undoubtedly inefficient, but aids in debugging at this early stage.
28
38
  * Every frame is numbered by the server, and is referred to as a 'tick'.
29
39
  * Player actions are both executed locally, and sent to the server.
30
40
  * If other players are connected, the server sends everyone's actions to everyone else.
41
+ * This is obviously insecure. Validating user data is on my to-do list.
31
42
  * Player actions are dated six ticks in the future, to give everyone a chance to get the message early. That way everyone can execute the action at the same time.
32
43
  * The server broadcasts the complete GameSpace four times a second, just in case a client gets out of sync.
33
44
  * The client predicts the server state, but treats its own copy as advisory only -- the server's is definitive.
@@ -44,11 +55,13 @@ The physics is (intentionally) pretty simple. Unsupported objects fall, acceler
44
55
 
45
56
  When building a block, the longer you hold the S or DownArrow key, the more HP will be awarded to the block. Hitting a block with a pellet reduces its HP, until it degrades to a lower form. Dirt blocks with 1 HP will be entirely destroyed when hit.
46
57
 
47
- Whether an object is supported depends exclusively on its immediate surroundings. That means two objects can support each other, and hang suspended. For example, a dirt block sitting on a steel block will support each other. A horizontal row of brick, capped at either end with cement, will also be free-standing.
58
+ Whether a block is supported depends exclusively on its immediate surroundings. That means two blocks can support each other, and hang suspended. For example, a dirt block sitting on a steel block will support each other. A horizontal row of brick, capped at either end with cement, will also be free-standing.
48
59
 
49
- Pellets are fired from the player's center, and are affected by gravity. They damage whatever they hit first, and disappear. Pellets won't hit the player who fired them.
60
+ Pellets are fired from the player's center, and are affected by gravity. They damage whatever they hit first, and disappear. Pellets won't hit the player who fired them. So far, only blocks can take damage; players are indestructible (but you know that will change).
50
61
 
51
- There are also Titanium entities, which never fall, and are indestructible. These are intended for use in designing levels with specific shapes. They can only be created by using the menu to select Titanium, and then right-clicking.
62
+ There are also Titanium entities, which never fall, and are indestructible. These are intended for use in designing levels with specific shapes. They can only be created by using the menu to select Titanium, and then shift-right-clicking.
63
+
64
+ Teleporters never fall. Their only action is to transfer their contents (anything close enough to intersect with their center) to their single destination-point. This transfer affects location but not velocity. Transfer doesn't happen if there is something blocking the exit point.
52
65
 
53
66
  A player is considered to be supported if their "feet" are touching a block. Unsupported players will turn feet-downward and fall, until they land on something. Supported players may slide left or right, and will follow any edges they reach -- going up and down walls, or hanging under ceilings. The "flip" maneuver swaps head and feet; this becomes useful if the player's head is exactly touching another block. At all other times, a flip leads to a fall (which can be useful too).
54
67
 
@@ -6,12 +6,13 @@ require 'game_2d/game_window'
6
6
  opts = Trollop::options do
7
7
  opt :name, "Player name", :type => :string, :required => true
8
8
  opt :hostname, "Hostname of server", :type => :string, :required => true
9
- opt :port, "Port number", :default => DEFAULT_PORT
9
+ opt :port, "Port number", :default => GameClient::DEFAULT_PORT
10
+ opt :key_size, "Key size in bits", :default => 1024
10
11
  opt :profile, "Turn on profiling", :type => :boolean
11
12
  opt :debug_traffic, "Debug network traffic", :type => :boolean
12
13
  end
13
14
 
14
15
  $debug_traffic = opts[:debug_traffic] || false
15
16
 
16
- window = GameWindow.new( opts[:name], opts[:hostname], opts[:port], opts[:profile] )
17
+ window = GameWindow.new( opts )
17
18
  window.show
@@ -1,11 +1,17 @@
1
1
  require 'renet'
2
2
  require 'json'
3
+ require 'base64'
4
+ require 'openssl'
3
5
  require 'game_2d/hash'
6
+ require 'game_2d/encryption'
4
7
 
5
8
  # The client creates one of these.
6
9
  # It is then used for all communication with the server.
7
10
 
8
11
  class ClientConnection
12
+ include Encryption
13
+ include Base64
14
+
9
15
  attr_reader :player_name
10
16
  attr_accessor :engine
11
17
 
@@ -14,17 +20,31 @@ class ClientConnection
14
20
  # the fleet
15
21
  ACTION_DELAY = 6 # 1/10 of a second
16
22
 
17
- def initialize(host, port, game, player_name, timeout=2000)
23
+ def initialize(host, port, game, player_name, key_size, timeout=2000)
18
24
  # remote host address, remote host port, channels, download bandwidth, upload bandwidth
19
25
  @socket = _create_connection(host, port, 2, 0, 0)
20
- @game = game
21
- @player_name = player_name
26
+ @host, @port, @game, @player_name, @key_size, @timeout =
27
+ host, port, game, player_name, key_size, timeout
22
28
 
23
29
  @socket.on_connection(method(:on_connect))
24
30
  @socket.on_disconnection(method(:on_close))
25
31
  @socket.on_packet_receive(method(:on_packet))
26
32
 
27
- @socket.connect(timeout)
33
+ @dh = @password_hash = nil
34
+ end
35
+
36
+ def start(password_hash)
37
+ @password_hash = password_hash
38
+ Thread.new do
39
+ @game.display_message! "Establishing encryption (#{@key_size}-bit)..."
40
+ @dh = OpenSSL::PKey::DH.new(@key_size)
41
+
42
+ # Connect to server and kick off handshaking
43
+ # We will create our player object only after we've been accepted by the server
44
+ # and told our starting position
45
+ @game.display_message! "Connecting to #{@host}:#{@port} as #{@player_name}"
46
+ @socket.connect(@timeout)
47
+ end
28
48
  end
29
49
 
30
50
  def _create_connection(*args)
@@ -32,9 +52,22 @@ class ClientConnection
32
52
  end
33
53
 
34
54
  def on_connect
35
- puts "Connected to server - sending handshake"
36
- # send handshake reliably
37
- send_record( { :handshake => { :player_name => @player_name } }, true)
55
+ @game.display_message "Connected, logging in"
56
+ send_record( { :handshake => {
57
+ :player_name => @player_name,
58
+ :dh_public_key => @dh.public_key.to_pem,
59
+ :client_public_key => @dh.pub_key.to_s
60
+ } }, true) # send handshake reliably
61
+ end
62
+
63
+ def login(server_public_key)
64
+ self.key = @dh.compute_key(OpenSSL::BN.new server_public_key)
65
+ data, iv = encrypt(@password_hash)
66
+ @password_hash = nil
67
+ send_record(
68
+ :password_hash => strict_encode64(data),
69
+ :iv => strict_encode64(iv)
70
+ )
38
71
  end
39
72
 
40
73
  def on_close
@@ -46,35 +79,40 @@ class ClientConnection
46
79
  hash = JSON.parse(data).fix_keys
47
80
  debug_packet('Received', hash)
48
81
 
49
- pong = hash[:pong]
50
- if pong
82
+ if pong = hash.delete(:pong)
51
83
  stop = Time.now.to_f
52
84
  puts "Ping took #{stop - pong[:start]} seconds"
53
85
  end
54
86
 
55
- at_tick = hash[:at_tick]
56
- fail "No at_tick in #{hash.inspect}" unless at_tick
87
+ if server_public_key = hash.delete(:server_public_key)
88
+ login(server_public_key)
89
+ return
90
+ end
91
+
92
+ # Leave :at_tick intact; add_delta will reuse it
93
+ fail "No at_tick in #{hash.inspect}" unless at_tick = hash[:at_tick]
57
94
 
58
- world = hash[:world]
59
- if world
95
+ if world = hash.delete(:world)
96
+ @game.clear_message
60
97
  @engine.establish_world(world, at_tick)
61
98
  end
62
99
 
100
+ you_are = hash.delete :you_are
101
+ registry, highest_id = hash.delete(:registry), hash.delete(:highest_id)
102
+
63
103
  delta_keys = [
64
104
  :add_players, :add_npcs, :delete_entities, :update_entities, :update_score, :move
65
105
  ]
66
106
  @engine.add_delta(hash) if delta_keys.any? {|k| hash.has_key? k}
67
107
 
68
- you_are = hash[:you_are]
69
108
  if you_are
70
109
  # The 'world' response includes deltas for add_players and add_npcs
71
110
  # Need to process those first, as one of the players is us
72
- @engine.apply_all_deltas(at_tick)
111
+ @engine.apply_deltas(at_tick)
73
112
 
74
113
  @engine.create_local_player you_are
75
114
  end
76
115
 
77
- registry, highest_id = hash[:registry], hash[:highest_id]
78
116
  @engine.sync_registry(registry, highest_id, at_tick) if registry
79
117
  end
80
118
 
@@ -83,16 +121,32 @@ class ClientConnection
83
121
  end
84
122
 
85
123
  def send_move(move, args={})
86
- return unless move
124
+ return unless move && online?
87
125
  args[:move] = move.to_s
88
126
  delta = { :at_tick => send_actions_at, :move => args }
89
127
  send_record delta
90
- delta[:move][:player_id] = @engine.player_id
128
+ delta[:player_id] = @engine.player_id
91
129
  @engine.add_delta delta
92
130
  end
93
131
 
94
132
  def send_create_npc(npc)
95
- delta = { :at_tick => send_actions_at, :create_npc => npc }
133
+ return unless online?
134
+ # :on_* hooks are for our own use; we don't send them
135
+ remote_npc = npc.reject {|k,v| k.to_s.start_with? 'on_'}
136
+ send_record :at_tick => send_actions_at, :add_npcs => [ remote_npc ]
137
+ @engine.add_delta :at_tick => send_actions_at, :add_npcs => [ npc ]
138
+ end
139
+
140
+ def send_update_entity(entity)
141
+ return unless online?
142
+ delta = { :update_entities => [entity], :at_tick => send_actions_at }
143
+ send_record delta
144
+ @engine.add_delta delta
145
+ end
146
+
147
+ def send_snap_to_grid(entity)
148
+ return unless online? && entity
149
+ delta = { :at_tick => send_actions_at, :snap_to_grid => entity.registry_id }
96
150
  send_record delta
97
151
  @engine.add_delta delta
98
152
  end
@@ -106,6 +160,7 @@ class ClientConnection
106
160
  end
107
161
 
108
162
  def send_record(data, reliable=false)
163
+ return unless online?
109
164
  debug_packet('Sending', data)
110
165
  @socket.send_packet(data.to_json, reliable, 0)
111
166
  @socket.flush
@@ -123,5 +178,5 @@ class ClientConnection
123
178
  end
124
179
 
125
180
  def online?; @socket.online?; end
126
- def disconnect; @socket.disconnect(200); end
181
+ def disconnect; @socket.disconnect(200) if online?; end
127
182
  end
@@ -41,11 +41,15 @@ class ClientEngine
41
41
  @width, @height = world[:cell_width], world[:cell_height]
42
42
  highest_id = world[:highest_id]
43
43
  create_initial_space(at_tick, highest_id)
44
+ @preprocessed = at_tick
44
45
  end
45
46
 
47
+ alias :world_established? :tick
48
+
46
49
  def create_initial_space(at_tick, highest_id)
47
50
  @earliest_tick = @tick = at_tick
48
- space = @spaces[@tick] = GameSpace.new.establish_world(@world_name, @world_id, @width, @height)
51
+ space = @spaces[@tick] = GameSpace.new(@game_window).
52
+ establish_world(@world_name, @world_id, @width, @height)
49
53
  space.highest_id = highest_id
50
54
  space
51
55
  end
@@ -56,20 +60,16 @@ class ClientEngine
56
60
  fail "Can't create space at #{tick}; earliest space we know about is #{@earliest_tick}" if tick < @earliest_tick
57
61
 
58
62
  last_space = space_at(tick - 1)
59
- @spaces[tick] = new_space = GameSpace.new.copy_from(last_space)
63
+ @spaces[tick] = new_space = GameSpace.new(@game_window).copy_from(last_space)
60
64
 
61
- # Certain deltas, like add_npcs, need to be processed post-update
62
- # to match the server's behavior. An object created during tick T
63
- # does not receive its first update until T+1.
64
- apply_deltas_before_update(tick)
65
+ apply_deltas(tick)
65
66
  new_space.update
66
- apply_deltas_after_update(tick)
67
67
 
68
68
  new_space
69
69
  end
70
70
 
71
71
  def update
72
- return unless @tick # handshake not yet answered
72
+ return unless world_established?
73
73
 
74
74
  # Display the frame we received from the server as-is
75
75
  if @preprocessed == @tick
@@ -102,6 +102,7 @@ class ClientEngine
102
102
 
103
103
  def add_delta(delta)
104
104
  at_tick = delta.delete :at_tick
105
+ fail "Received delta without at_tick: #{delta.inspect}" unless at_tick
105
106
  if at_tick < @tick
106
107
  $stderr.puts "Received delta #{@tick - at_tick} ticks late"
107
108
  if at_tick <= @earliest_tick
@@ -114,42 +115,37 @@ class ClientEngine
114
115
  @deltas[at_tick] << delta
115
116
  end
116
117
 
117
- def apply_deltas_before_update(at_tick)
118
+ def apply_deltas(at_tick)
118
119
  space = space_at(at_tick)
119
120
 
120
121
  @deltas[at_tick].each do |hash|
121
- players = hash[:add_players]
122
+ players = hash.delete :add_players
122
123
  add_players(space, players) if players
123
124
 
124
- doomed = hash[:delete_entities]
125
+ doomed = hash.delete :delete_entities
125
126
  delete_entities(space, doomed) if doomed
126
127
 
127
- updated = hash[:update_entities]
128
+ updated = hash.delete :update_entities
128
129
  update_entities(space, updated) if updated
129
130
 
130
- move = hash[:move]
131
- apply_move(space, move) if move
132
-
133
- score_update = hash[:update_score]
134
- update_score(space, score_update) if score_update
135
- end
136
- end
137
-
138
- def apply_deltas_after_update(at_tick)
139
- space = space_at(at_tick)
131
+ snap = hash.delete :snap_to_grid
132
+ space.snap_to_grid(snap.to_sym) if snap
140
133
 
141
- @deltas[at_tick].each do |hash|
142
- npcs = hash[:add_npcs]
134
+ npcs = hash.delete :add_npcs
143
135
  add_npcs(space, npcs) if npcs
144
- end
145
136
 
146
- # Any later spaces are now invalid
147
- @spaces.delete_if {|key, _| key > at_tick}
148
- end
137
+ move = hash.delete :move
138
+ player_id = hash.delete :player_id
139
+ if move
140
+ fail "No player_id sent with move #{move.inspect}" unless player_id
141
+ apply_move(space, move, player_id.to_sym)
142
+ end
149
143
 
150
- def apply_all_deltas(at_tick)
151
- apply_deltas_before_update(at_tick)
152
- apply_deltas_after_update(at_tick)
144
+ score_update = hash.delete :update_score
145
+ update_score(space, score_update) if score_update
146
+
147
+ $stderr.puts "Unprocessed deltas: #{hash.keys.join(', ')}" unless hash.empty?
148
+ end
153
149
  end
154
150
 
155
151
  def add_player(space, hash)
@@ -163,15 +159,18 @@ class ClientEngine
163
159
  players.each {|json| add_player(space, json) }
164
160
  end
165
161
 
166
- def apply_move(space, move)
167
- player_id = move[:player_id]
162
+ def apply_move(space, move, player_id)
168
163
  player = space[player_id]
169
164
  fail "No such player #{player_id}, can't apply #{move.inspect}" unless player
170
165
  player.add_move move
171
166
  end
172
167
 
173
168
  def add_npcs(space, npcs)
174
- npcs.each {|json| space << Serializable.from_json(json) }
169
+ npcs.each do |json|
170
+ on_create = json.delete :on_create
171
+ space << (entity = Serializable.from_json(json))
172
+ on_create.call(entity) if on_create
173
+ end
175
174
  end
176
175
 
177
176
  def add_entity(space, json)
@@ -188,6 +187,7 @@ class ClientEngine
188
187
 
189
188
  if my_obj = space[registry_id]
190
189
  my_obj.update_from_json(json)
190
+ my_obj.grab!
191
191
  else
192
192
  add_entity(space, json)
193
193
  end
@@ -215,6 +215,7 @@ class ClientEngine
215
215
  # Discard anything we think we know, in favor of the registry
216
216
  # we just got from the server
217
217
  def sync_registry(server_registry, highest_id, at_tick)
218
+ return unless world_established?
218
219
  @spaces.clear
219
220
  # Any older deltas are now irrelevant
220
221
  @earliest_tick.upto(at_tick - 1) {|old_tick| @deltas.delete old_tick}
@@ -36,7 +36,7 @@ class ComplexMove
36
36
  Serializable.as_json(self).merge!(:actor_id => actor_id)
37
37
  end
38
38
  def update_from_json(json)
39
- self.actor_id = json[:actor_id]
39
+ self.actor_id = json[:actor_id] if json[:actor_id]
40
40
  self
41
41
  end
42
42
  def to_s
@@ -0,0 +1,35 @@
1
+ require 'openssl'
2
+
3
+ module Encryption
4
+
5
+ def key=(key)
6
+ @symmetric_key = key
7
+ end
8
+
9
+ def make_cipher
10
+ OpenSSL::Cipher::AES.new(128, :CBC)
11
+ end
12
+
13
+ # Returns [encrypted, iv]
14
+ def encrypt(data)
15
+ cipher = make_cipher.encrypt
16
+ cipher.key = @symmetric_key
17
+ iv = cipher.random_iv
18
+ [cipher.update(data) + cipher.final, iv]
19
+ end
20
+
21
+ def decrypt(data, iv)
22
+ decipher = make_cipher.decrypt
23
+ decipher.key = @symmetric_key
24
+ decipher.iv = iv
25
+ decipher.update(data) + decipher.final
26
+ end
27
+
28
+ # Same sort of thing /etc/passwd uses.
29
+ # Provides no security against snooping passwords off
30
+ # the wire, but does make it safer to handle the user
31
+ # password file.
32
+ def make_password_hash(password)
33
+ password.crypt('RB')
34
+ end
35
+ end
@@ -3,6 +3,7 @@ require 'facets/kernel/constant'
3
3
  require 'game_2d/registerable'
4
4
  require 'game_2d/serializable'
5
5
  require 'game_2d/entity_constants'
6
+ require 'game_2d/transparency'
6
7
 
7
8
  class NilClass
8
9
  # Ignore this
@@ -10,9 +11,35 @@ class NilClass
10
11
  end
11
12
 
12
13
  class Entity
14
+ include EntityConstants
15
+
16
+ module ClassMethods
17
+ include EntityConstants
18
+ # Left-most cell X position occupied
19
+ def left_cell_x_at(x); x / WIDTH; end
20
+
21
+ # Right-most cell X position occupied
22
+ # If we're exactly within a column (@x is an exact multiple of WIDTH),
23
+ # then this equals left_cell_x. Otherwise, it's one higher
24
+ def right_cell_x_at(x); (x + WIDTH - 1) / WIDTH; end
25
+
26
+ # Top-most cell Y position occupied
27
+ def top_cell_y_at(y); y / HEIGHT; end
28
+
29
+ # Bottom-most cell Y position occupied
30
+ def bottom_cell_y_at(y); (y + HEIGHT - 1) / HEIGHT; end
31
+
32
+ # Velocity is constrained to the range -MAX_VELOCITY .. MAX_VELOCITY
33
+ def constrain_velocity(vel)
34
+ [[vel, MAX_VELOCITY].min, -MAX_VELOCITY].max
35
+ end
36
+ end
37
+ include ClassMethods
38
+ extend ClassMethods
39
+
13
40
  include Serializable
14
41
  include Registerable
15
- include EntityConstants
42
+ include Transparency
16
43
 
17
44
  # X and Y position of the top-left corner
18
45
  attr_accessor :x, :y, :moving, :space
@@ -27,16 +54,16 @@ class Entity
27
54
  @x, @y, self.a = x, y, a
28
55
  self.x_vel, self.y_vel = x_vel, y_vel
29
56
  @moving = true
57
+ @grabbed = false
30
58
  end
31
59
 
32
60
  def a=(angle); @a = angle % 360; end
33
61
 
34
- # Velocity is constrained to the range -MAX_VELOCITY .. MAX_VELOCITY
35
62
  def x_vel=(xv)
36
- @x_vel = [[xv, MAX_VELOCITY].min, -MAX_VELOCITY].max
63
+ @x_vel = constrain_velocity xv
37
64
  end
38
65
  def y_vel=(yv)
39
- @y_vel = [[yv, MAX_VELOCITY].min, -MAX_VELOCITY].max
66
+ @y_vel = constrain_velocity yv
40
67
  end
41
68
 
42
69
  # True if we need to update this entity
@@ -60,6 +87,12 @@ class Entity
60
87
  @moving = true
61
88
  end
62
89
 
90
+ # Entity is under direct control by a player
91
+ # This is transitory state (not persisted or copied)
92
+ def grab!; @grabbed = true; end
93
+ def release!; @grabbed = false; end
94
+ def grabbed?; @grabbed; end
95
+
63
96
  # Give this entity a chance to perform clean-up upon destruction
64
97
  def destroy!; end
65
98
 
@@ -68,24 +101,10 @@ class Entity
68
101
  def pixel_x; @x / PIXEL_WIDTH; end
69
102
  def pixel_y; @y / PIXEL_WIDTH; end
70
103
 
71
- # Left-most cell X position occupied
72
- def self.left_cell_x_at(x); x / WIDTH; end
73
- # TODO: Find a more elegant way to call class methods in Ruby...
74
- def left_cell_x(x = @x); self.class.left_cell_x_at(x); end
75
-
76
- # Right-most cell X position occupied
77
- # If we're exactly within a column (@x is an exact multiple of WIDTH),
78
- # then this equals left_cell_x. Otherwise, it's one higher
79
- def self.right_cell_x_at(x); (x + WIDTH - 1) / WIDTH; end
80
- def right_cell_x(x = @x); self.class.right_cell_x_at(x); end
81
-
82
- # Top-most cell Y position occupied
83
- def self.top_cell_y_at(y); y / HEIGHT; end
84
- def top_cell_y(y = @y); self.class.top_cell_y_at(y); end
85
-
86
- # Bottom-most cell Y position occupied
87
- def self.bottom_cell_y_at(y); (y + HEIGHT - 1) / HEIGHT; end
88
- def bottom_cell_y(y = @y); self.class.bottom_cell_y_at(y); end
104
+ def left_cell_x(x = @x); left_cell_x_at(x); end
105
+ def right_cell_x(x = @x); right_cell_x_at(x); end
106
+ def top_cell_y(y = @y); top_cell_y_at(y); end
107
+ def bottom_cell_y(y = @y); bottom_cell_y_at(y); end
89
108
 
90
109
  # Returns an array of one, two, or four cell-coordinate tuples
91
110
  # E.g. [[4, 5], [4, 6], [5, 5], [5, 6]]
@@ -101,13 +120,8 @@ class Entity
101
120
  self.y_vel = @y_vel + y_accel
102
121
  end
103
122
 
104
- # Override to make particular entities transparent to each other
105
- def transparent_to_me?(other)
106
- other == self
107
- end
108
-
109
123
  def opaque(others)
110
- others.delete_if {|obj| transparent_to_me?(obj)}
124
+ others.delete_if {|obj| obj.equal?(self) || transparent?(self, obj)}
111
125
  end
112
126
 
113
127
  # Wrapper around @space.entities_overlapping
@@ -193,10 +207,11 @@ class Entity
193
207
  end
194
208
 
195
209
  # Update position/velocity/angle data, and tell the space about it
196
- def warp(x, y, x_vel, y_vel, angle=self.a, moving=@moving)
210
+ def warp(x, y, x_vel=nil, y_vel=nil, angle=nil, moving=nil)
197
211
  blk = proc do
198
212
  @x, @y, self.x_vel, self.y_vel, self.a, @moving =
199
- x, y, x_vel, y_vel, angle, moving
213
+ (x || @x), (y || @y), (x_vel || @x_vel), (y_vel || @y_vel), (angle || @a),
214
+ (moving.nil? ? @moving : moving)
200
215
  end
201
216
  if @space
202
217
  @space.process_moving_entity(self, &blk)
@@ -354,12 +369,13 @@ class Entity
354
369
  img.draw_rot(
355
370
  self.pixel_x + CELL_WIDTH_IN_PIXELS / 2,
356
371
  self.pixel_y + CELL_WIDTH_IN_PIXELS / 2,
357
- draw_zorder, self.a)
372
+ draw_zorder, draw_angle)
358
373
  # 0.5, 0.5, # rotate around the center
359
374
  # 1, 1, # scaling factor
360
375
  # @color, # modify color
361
376
  # :add) # draw additively
362
377
  end
378
+ def draw_angle; a; end
363
379
 
364
380
  def to_s
365
381
  "#{self.class} (#{registry_id_safe}) at #{x}x#{y}"