game_2d 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +111 -25
- data/game_2d.gemspec +33 -17
- data/lib/game_2d/client_connection.rb +11 -4
- data/lib/game_2d/client_engine.rb +23 -21
- data/lib/game_2d/entity.rb +103 -20
- data/lib/game_2d/entity/base.rb +38 -0
- data/lib/game_2d/entity/block.rb +75 -6
- data/lib/game_2d/entity/destination.rb +6 -0
- data/lib/game_2d/entity/gecko.rb +237 -0
- data/lib/game_2d/entity/ghost.rb +126 -0
- data/lib/game_2d/entity/hole.rb +32 -0
- data/lib/game_2d/entity/pellet.rb +1 -1
- data/lib/game_2d/entity/slime.rb +121 -0
- data/lib/game_2d/entity/teleporter.rb +7 -4
- data/lib/game_2d/entity_constants.rb +2 -2
- data/lib/game_2d/game.rb +53 -23
- data/lib/game_2d/game_client.rb +88 -46
- data/lib/game_2d/game_space.rb +114 -23
- data/lib/game_2d/game_window.rb +2 -2
- data/lib/game_2d/move/spawn.rb +67 -0
- data/lib/game_2d/player.rb +36 -213
- data/lib/game_2d/serializable.rb +2 -2
- data/lib/game_2d/server_connection.rb +32 -18
- data/lib/game_2d/server_port.rb +23 -14
- data/lib/game_2d/transparency.rb +52 -15
- data/lib/game_2d/version.rb +1 -1
- data/media/base.png +0 -0
- data/media/base.xcf +0 -0
- data/media/{player.png → gecko.png} +0 -0
- data/media/{player.xcf → gecko.xcf} +0 -0
- data/media/ghost.png +0 -0
- data/media/ghost.xcf +0 -0
- data/media/hole.png +0 -0
- data/media/hole.xcf +0 -0
- data/media/slime.png +0 -0
- data/media/slime.xcf +0 -0
- data/spec/block_spec.rb +158 -0
- data/spec/client_engine_spec.rb +49 -37
- data/spec/game_space_spec.rb +34 -0
- metadata +51 -6
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'game_2d/entity'
|
2
|
+
require 'game_2d/player'
|
3
|
+
require 'game_2d/entity/ghost'
|
4
|
+
|
5
|
+
class Entity
|
6
|
+
|
7
|
+
class Base < Entity
|
8
|
+
def should_fall?; underfoot.empty?; end
|
9
|
+
|
10
|
+
def update
|
11
|
+
if should_fall?
|
12
|
+
self.a = (direction || 180) + 180
|
13
|
+
else
|
14
|
+
slow_by 1
|
15
|
+
end
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def available?
|
20
|
+
return false unless space
|
21
|
+
|
22
|
+
# Can't use entity.entities_obstructing() here, as that only
|
23
|
+
# returns objects opaque to the receiver (the base). Players
|
24
|
+
# aren't opaque to bases. We need to ensure there are no
|
25
|
+
# solid (non-ghost) players occupying the space.
|
26
|
+
#
|
27
|
+
# This logic depends on the fact that anything transparent
|
28
|
+
# to a base is also transparent to a player. If we ever allow
|
29
|
+
# a base to go somewhere a player can't be, that's a problem.
|
30
|
+
space.entities_overlapping(x, y).find_all do |e|
|
31
|
+
e.is_a?(Player) && !e.is_a?(Entity::Ghost)
|
32
|
+
end.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def image_filename; "base.png"; end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/game_2d/entity/block.rb
CHANGED
@@ -26,9 +26,9 @@ class Block < OwnedEntity
|
|
26
26
|
when 0
|
27
27
|
true
|
28
28
|
when 1
|
29
|
-
|
29
|
+
!(supported_on_left && supported_on_right)
|
30
30
|
when 2
|
31
|
-
|
31
|
+
!(supported_on_left || supported_on_right)
|
32
32
|
when 3
|
33
33
|
empty_on_left? && empty_on_right? && empty_above?
|
34
34
|
when 4
|
@@ -36,18 +36,87 @@ class Block < OwnedEntity
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
def supported_on_left
|
40
|
+
opaque(space.entities_exactly_at_point(x - WIDTH, y)).any?
|
41
|
+
end
|
42
|
+
|
43
|
+
def supported_on_right
|
44
|
+
right_support = opaque(space.entities_exactly_at_point(x + WIDTH, y)).any?
|
45
|
+
end
|
46
|
+
|
39
47
|
def update
|
40
48
|
if should_fall?
|
41
|
-
|
49
|
+
# applies acceleration, but that's all
|
50
|
+
space.fall(self)
|
42
51
|
else
|
43
52
|
self.x_vel = self.y_vel = 0
|
44
53
|
end
|
54
|
+
# Reduce velocity if necessary, to exactly line up with
|
55
|
+
# an upcoming source of support (so we don't move past it)
|
56
|
+
if x_vel == 0 && y_vel != 0
|
57
|
+
case level
|
58
|
+
when 1
|
59
|
+
new_y = look_ahead_for_support_both_sides
|
60
|
+
self.y_vel = new_y - y if new_y
|
61
|
+
when 2
|
62
|
+
new_y = look_ahead_for_support_either_side
|
63
|
+
self.y_vel = new_y - y if new_y
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
45
67
|
move
|
46
68
|
end
|
47
69
|
|
48
|
-
|
49
|
-
|
50
|
-
|
70
|
+
# Need a source of support at the exact same
|
71
|
+
# height on both sides
|
72
|
+
def look_ahead_for_support_both_sides
|
73
|
+
look_ahead_for_support do |left, right|
|
74
|
+
left & right
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Any source of support will do, either side
|
79
|
+
# or both
|
80
|
+
def look_ahead_for_support_either_side
|
81
|
+
look_ahead_for_support do |left, right|
|
82
|
+
left + right
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Find the highest (if dropping) or lowest (if rising) height that meets
|
87
|
+
# the requirements for support
|
88
|
+
def look_ahead_for_support
|
89
|
+
support_heights = yield *possible_sources_of_support
|
90
|
+
|
91
|
+
(y_vel > 0) ? support_heights.min : support_heights.max
|
92
|
+
end
|
93
|
+
|
94
|
+
# Sources of support must intersect with the points next
|
95
|
+
# to us, and be:
|
96
|
+
# - Level with our lower edge, if dropping
|
97
|
+
# - A point above our upper edge, if rising
|
98
|
+
# - Close enough that our current velocity will
|
99
|
+
# take us past that point during this tick
|
100
|
+
#
|
101
|
+
# This just returns the heights at which we might find support
|
102
|
+
def possible_sources_of_support
|
103
|
+
target_height = if y_vel > 0 # dropping
|
104
|
+
y + HEIGHT - 1
|
105
|
+
else # rising
|
106
|
+
y - 1
|
107
|
+
end
|
108
|
+
|
109
|
+
left_support = opaque(space.entities_at_point(x - 1, target_height)).collect(&:y)
|
110
|
+
right_support = opaque(space.entities_at_point(x + WIDTH, target_height)).collect(&:y)
|
111
|
+
|
112
|
+
# Filter out heights we aren't going to reach this tick with
|
113
|
+
# our current velocity
|
114
|
+
not_too_far = lambda {|its_y| (its_y - y).abs <= y_vel.abs }
|
115
|
+
[left_support.find_all(¬_too_far), right_support.find_all(¬_too_far)]
|
116
|
+
end
|
117
|
+
|
118
|
+
def harmed_by(other, damage=1)
|
119
|
+
self.hp -= damage
|
51
120
|
@space.doom(self) if hp <= 0
|
52
121
|
end
|
53
122
|
|
@@ -6,12 +6,18 @@ class Destination < OwnedEntity
|
|
6
6
|
|
7
7
|
def should_fall?; false; end
|
8
8
|
|
9
|
+
def teleportable?; false; end
|
10
|
+
|
9
11
|
def update; end
|
10
12
|
|
11
13
|
def image_filename; "destination.png"; end
|
12
14
|
|
13
15
|
def draw_zorder; ZOrder::Destination; end
|
14
16
|
def draw_angle; space.game.tick % 360; end
|
17
|
+
|
18
|
+
def destroy!
|
19
|
+
space.doom owner
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'facets/kernel/try'
|
2
|
+
require 'game_2d/entity'
|
3
|
+
require 'game_2d/entity/pellet'
|
4
|
+
require 'game_2d/entity/block'
|
5
|
+
require 'game_2d/move/rise_up'
|
6
|
+
require 'game_2d/player'
|
7
|
+
|
8
|
+
# A player object that can stick to walls and slide around corners
|
9
|
+
# Calling update() causes a move to be dequeued and executed, applying forces
|
10
|
+
# to the game object
|
11
|
+
class Entity
|
12
|
+
|
13
|
+
class Gecko < Entity
|
14
|
+
include Player
|
15
|
+
include Comparable
|
16
|
+
|
17
|
+
MAX_HP = 1
|
18
|
+
|
19
|
+
# Game ticks it takes before a block's HP is raised by 1
|
20
|
+
BUILD_TIME = 7
|
21
|
+
|
22
|
+
# Amount to decelerate each tick when braking
|
23
|
+
BRAKE_SPEED = 4
|
24
|
+
|
25
|
+
MOVES_FOR_KEY_HELD = {
|
26
|
+
Gosu::KbLeft => :slide_left,
|
27
|
+
Gosu::KbA => :slide_left,
|
28
|
+
Gosu::KbRight => :slide_right,
|
29
|
+
Gosu::KbD => :slide_right,
|
30
|
+
Gosu::KbRightControl => :brake,
|
31
|
+
Gosu::KbLeftControl => :brake,
|
32
|
+
Gosu::KbDown => :build,
|
33
|
+
Gosu::KbS => :build,
|
34
|
+
}
|
35
|
+
|
36
|
+
attr_reader :hp, :build_block_id
|
37
|
+
|
38
|
+
def initialize(player_name = "<unknown>")
|
39
|
+
super
|
40
|
+
initialize_player
|
41
|
+
@player_name = player_name
|
42
|
+
@score = 0
|
43
|
+
@hp = MAX_HP
|
44
|
+
@build_block_id = nil
|
45
|
+
@build_level = 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def hp=(p); @hp = [[p, MAX_HP].min, 0].max; end
|
49
|
+
|
50
|
+
def sleep_now?; false; end
|
51
|
+
|
52
|
+
def should_fall?; underfoot.empty?; end
|
53
|
+
|
54
|
+
def build_block_id=(new_id)
|
55
|
+
@build_block_id = new_id.try(:to_sym)
|
56
|
+
end
|
57
|
+
|
58
|
+
def building?; @build_block_id; end
|
59
|
+
|
60
|
+
def build_block
|
61
|
+
return nil unless building?
|
62
|
+
fail "Can't look up build_block when not in a space" unless @space
|
63
|
+
@space[@build_block_id] or fail "Don't have build_block #{@build_block_id}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def harmed_by(other, damage=1)
|
67
|
+
self.hp -= damage
|
68
|
+
die if hp <= 0
|
69
|
+
end
|
70
|
+
|
71
|
+
def destroy!
|
72
|
+
build_block.owner_id = nil if building?
|
73
|
+
end
|
74
|
+
|
75
|
+
def update
|
76
|
+
fail "No space set for #{self}" unless @space
|
77
|
+
check_for_disown_block
|
78
|
+
|
79
|
+
return if perform_complex_move
|
80
|
+
|
81
|
+
if falling = should_fall?
|
82
|
+
self.a = 0
|
83
|
+
space.fall(self)
|
84
|
+
end
|
85
|
+
|
86
|
+
args = next_move
|
87
|
+
case (current_move = args.delete(:move).to_sym)
|
88
|
+
when :slide_left, :slide_right, :brake, :flip, :build, :rise_up
|
89
|
+
send current_move unless falling
|
90
|
+
when :fire
|
91
|
+
fire args[:x_vel], args[:y_vel]
|
92
|
+
else
|
93
|
+
puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
|
94
|
+
end if args
|
95
|
+
|
96
|
+
# Only go around corner if sitting on exactly one object
|
97
|
+
blocks_underfoot = underfoot
|
98
|
+
if blocks_underfoot.size == 1
|
99
|
+
# Slide around if we're at the corner; otherwise, move normally
|
100
|
+
slide_around(blocks_underfoot.first) or move
|
101
|
+
else
|
102
|
+
# Straddling two objects, or falling
|
103
|
+
move
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check again whether we've moved off of a block
|
107
|
+
# we were building
|
108
|
+
check_for_disown_block
|
109
|
+
end
|
110
|
+
|
111
|
+
def slide_left; slide(self.a - 90); end
|
112
|
+
def slide_right; slide(self.a + 90); end
|
113
|
+
|
114
|
+
def slide(dir)
|
115
|
+
if opaque(next_to(dir)).empty?
|
116
|
+
accelerate(*angle_to_vector(dir))
|
117
|
+
else
|
118
|
+
self.a = dir + 180
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def brake; slow_by BRAKE_SPEED; end
|
123
|
+
|
124
|
+
def flip; self.a += 180; end
|
125
|
+
|
126
|
+
# Create the actual pellet
|
127
|
+
def fire(x_vel, y_vel)
|
128
|
+
pellet = Entity::Pellet.new(@x, @y, 0, x_vel, y_vel)
|
129
|
+
pellet.owner = self
|
130
|
+
@space << pellet
|
131
|
+
end
|
132
|
+
|
133
|
+
# Create the actual block
|
134
|
+
def build
|
135
|
+
if building?
|
136
|
+
@build_level += 1
|
137
|
+
if @build_level >= BUILD_TIME
|
138
|
+
@build_level = 0
|
139
|
+
build_block.hp += 1
|
140
|
+
end
|
141
|
+
else
|
142
|
+
bb = Entity::Block.new(@x, @y)
|
143
|
+
bb.owner_id = registry_id
|
144
|
+
bb.hp = 1
|
145
|
+
if @space << bb # generates an ID
|
146
|
+
@build_block_id = bb.registry_id
|
147
|
+
@build_level = 0
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def disown_block; @build_block_id, @build_level = nil, 0; end
|
153
|
+
|
154
|
+
def check_for_disown_block
|
155
|
+
return unless building?
|
156
|
+
return if @space.entities_overlapping(@x, @y).include?(build_block)
|
157
|
+
build_block.owner_id = nil
|
158
|
+
build_block.wake!
|
159
|
+
disown_block
|
160
|
+
end
|
161
|
+
|
162
|
+
def rise_up
|
163
|
+
self.complex_move = Move::RiseUp.new(self)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Called by GameWindow
|
167
|
+
# Should return the move to be sent via ClientConnection
|
168
|
+
# (or nil)
|
169
|
+
def generate_move_from_click(x, y)
|
170
|
+
if y < cy # Firing up
|
171
|
+
y_vel = -Math.sqrt(2 * (cy - y)).round
|
172
|
+
x_vel = (cx - x) / y_vel
|
173
|
+
else
|
174
|
+
y_vel = 0
|
175
|
+
if y == cy
|
176
|
+
return if x == cx
|
177
|
+
x_vel = (x <=> cx) * MAX_VELOCITY
|
178
|
+
else
|
179
|
+
range = x - cx
|
180
|
+
x_vel = (Math.sqrt(1.0 / (2.0 * (y - cy))) * range).round
|
181
|
+
end
|
182
|
+
end
|
183
|
+
[:fire, {:x_vel => x_vel, :y_vel => y_vel}]
|
184
|
+
end
|
185
|
+
|
186
|
+
# Called by GameWindow
|
187
|
+
# Should return the move to be sent via ClientConnection
|
188
|
+
# (or nil)
|
189
|
+
# This is for queued keypresses, i.e. those that happen
|
190
|
+
# on key-down only (just once for a press), not continuously
|
191
|
+
# for as long as held down
|
192
|
+
def move_for_keypress(keypress)
|
193
|
+
case keypress
|
194
|
+
when Gosu::KbUp, Gosu::KbW
|
195
|
+
return building? ? :rise_up : :flip
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Called by GameWindow
|
200
|
+
# Should return a map where the keys are... keys, and the
|
201
|
+
# values are the corresponding moves to be sent via
|
202
|
+
# ClientConnection
|
203
|
+
# This is for non-queued keypresses, i.e. those that happen
|
204
|
+
# continuously for as long as held down
|
205
|
+
def moves_for_key_held
|
206
|
+
MOVES_FOR_KEY_HELD
|
207
|
+
end
|
208
|
+
|
209
|
+
def all_state
|
210
|
+
# Player name goes first, so we can sort on that
|
211
|
+
super.unshift(player_name).push(
|
212
|
+
score, @hp, build_block_id, @complex_move)
|
213
|
+
end
|
214
|
+
|
215
|
+
def as_json
|
216
|
+
super.merge!(
|
217
|
+
:player_name => player_name,
|
218
|
+
:score => score,
|
219
|
+
:hp => @hp,
|
220
|
+
:build_block => @build_block_id,
|
221
|
+
:complex_move => @complex_move.as_json
|
222
|
+
)
|
223
|
+
end
|
224
|
+
|
225
|
+
def update_from_json(json)
|
226
|
+
@player_name = json[:player_name] if json[:player_name]
|
227
|
+
@score = json[:score] if json[:score]
|
228
|
+
@hp = json[:hp] if json[:hp]
|
229
|
+
@build_block_id = json[:build_block].try(:to_sym) if json[:build_block]
|
230
|
+
@complex_move = Serializable.from_json(json[:complex_move]) if json[:complex_move]
|
231
|
+
super
|
232
|
+
end
|
233
|
+
|
234
|
+
def image_filename; "gecko.png"; end
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'game_2d/entity'
|
2
|
+
require 'game_2d/move/spawn'
|
3
|
+
require 'game_2d/player'
|
4
|
+
|
5
|
+
# A player object that represents the player when between corporeal
|
6
|
+
# incarnations
|
7
|
+
#
|
8
|
+
# Ghost can fly around and look at things, but can't touch or affect
|
9
|
+
# anything
|
10
|
+
class Entity
|
11
|
+
|
12
|
+
class Ghost < Entity
|
13
|
+
include Player
|
14
|
+
include Comparable
|
15
|
+
|
16
|
+
MOVES_FOR_KEY_HELD = {
|
17
|
+
Gosu::KbLeft => :left,
|
18
|
+
Gosu::KbA => :left,
|
19
|
+
Gosu::KbRight => :right,
|
20
|
+
Gosu::KbD => :right,
|
21
|
+
Gosu::KbUp => :up,
|
22
|
+
Gosu::KbW => :up,
|
23
|
+
Gosu::KbDown => :down,
|
24
|
+
Gosu::KbS => :down,
|
25
|
+
}
|
26
|
+
|
27
|
+
def initialize(player_name = "<unknown>")
|
28
|
+
super
|
29
|
+
initialize_player
|
30
|
+
@player_name = player_name
|
31
|
+
@score = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def sleep_now?; false; end
|
35
|
+
|
36
|
+
def should_fall?; false; end
|
37
|
+
|
38
|
+
def teleportable?; false; end
|
39
|
+
|
40
|
+
def update
|
41
|
+
fail "No space set for #{self}" unless @space
|
42
|
+
|
43
|
+
return if perform_complex_move
|
44
|
+
|
45
|
+
if args = next_move
|
46
|
+
case (current_move = args.delete(:move).to_sym)
|
47
|
+
when :left, :right, :up, :down
|
48
|
+
send current_move
|
49
|
+
when :spawn
|
50
|
+
spawn args[:x], args[:y]
|
51
|
+
else
|
52
|
+
puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
|
53
|
+
end
|
54
|
+
else
|
55
|
+
slow_by 1
|
56
|
+
end
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
def left; accelerate(-1, 0); end
|
61
|
+
def right; accelerate(1, 0); end
|
62
|
+
def up; accelerate(0, -1); end
|
63
|
+
def down; accelerate(0, 1); end
|
64
|
+
|
65
|
+
def spawn(x, y)
|
66
|
+
if base = @space.available_base_near(x, y)
|
67
|
+
warn "#{self} spawning at #{base.x}, #{base.y}"
|
68
|
+
self.complex_move = Move::Spawn.new
|
69
|
+
self.complex_move.target_id = base.registry_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Called by GameWindow
|
74
|
+
# Should return a map where the keys are... keys, and the
|
75
|
+
# values are the corresponding moves to be sent via
|
76
|
+
# ClientConnection
|
77
|
+
# This is for non-queued keypresses, i.e. those that happen
|
78
|
+
# continuously for as long as held down
|
79
|
+
def moves_for_key_held
|
80
|
+
MOVES_FOR_KEY_HELD
|
81
|
+
end
|
82
|
+
|
83
|
+
# Called by GameWindow
|
84
|
+
# Should return the move to be sent via ClientConnection
|
85
|
+
# (or nil)
|
86
|
+
# This is for queued keypresses, i.e. those that happen
|
87
|
+
# on key-down only (just once for a press), not continuously
|
88
|
+
# for as long as held down
|
89
|
+
def move_for_keypress(keypress); nil; end
|
90
|
+
|
91
|
+
# Called by GameWindow
|
92
|
+
# Should return the move to be sent via ClientConnection
|
93
|
+
# (or nil)
|
94
|
+
def generate_move_from_click(x, y)
|
95
|
+
[:spawn, {:x => x, :y => y}]
|
96
|
+
end
|
97
|
+
|
98
|
+
def all_state
|
99
|
+
# Player name goes first, so we can sort on that
|
100
|
+
super.unshift(player_name).push(score, @complex_move)
|
101
|
+
end
|
102
|
+
|
103
|
+
def as_json
|
104
|
+
super.merge!(
|
105
|
+
:player_name => player_name,
|
106
|
+
:score => score,
|
107
|
+
:complex_move => @complex_move.as_json
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
def update_from_json(json)
|
112
|
+
@player_name = json[:player_name] if json[:player_name]
|
113
|
+
@score = json[:score] if json[:score]
|
114
|
+
@complex_move = Serializable.from_json(json[:complex_move]) if json[:complex_move]
|
115
|
+
super
|
116
|
+
end
|
117
|
+
|
118
|
+
def image_filename; "ghost.png"; end
|
119
|
+
|
120
|
+
def draw_image(anim)
|
121
|
+
# Usually frame 0, occasionally frame 1
|
122
|
+
anim[((Gosu::milliseconds / 100) % 63) / 62]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|