smash_and_grab 0.0.5alpha → 0.0.6alpha
Sign up to get free protection for your applications and to get access to all the features.
- data/{CHANGELOG.txt → CHANGELOG.md} +0 -0
- data/Gemfile.lock +8 -4
- data/LICENSE.txt +20 -0
- data/README.md +30 -14
- data/Rakefile +2 -2
- data/config/lang/objects/entities/en.yml +134 -0
- data/config/lang/objects/static/en.yml +8 -0
- data/config/lang/objects/vehicles/en.yml +11 -0
- data/config/map/entities.yml +42 -38
- data/lib/smash_and_grab.rb +5 -0
- data/lib/smash_and_grab/abilities.rb +1 -1
- data/lib/smash_and_grab/abilities/ability.rb +45 -3
- data/lib/smash_and_grab/abilities/drop.rb +38 -0
- data/lib/smash_and_grab/abilities/melee.rb +4 -6
- data/lib/smash_and_grab/abilities/pick_up.rb +33 -0
- data/lib/smash_and_grab/abilities/ranged.rb +18 -12
- data/lib/smash_and_grab/abilities/sprint.rb +11 -7
- data/lib/smash_and_grab/chingu_ext/basic_game_object.rb +26 -0
- data/lib/smash_and_grab/game_window.rb +5 -1
- data/lib/smash_and_grab/gui/entity_panel.rb +26 -10
- data/lib/smash_and_grab/gui/entity_summary.rb +19 -11
- data/lib/smash_and_grab/gui/game_log.rb +6 -2
- data/lib/smash_and_grab/gui/info_panel.rb +7 -4
- data/lib/smash_and_grab/gui/object_panel.rb +6 -2
- data/lib/smash_and_grab/gui/scenario_panel.rb +4 -0
- data/lib/smash_and_grab/history/action_history.rb +1 -1
- data/lib/smash_and_grab/main.rb +7 -3
- data/lib/smash_and_grab/map/faction.rb +26 -8
- data/lib/smash_and_grab/map/map.rb +21 -12
- data/lib/smash_and_grab/map/tile.rb +29 -2
- data/lib/smash_and_grab/map/wall.rb +2 -2
- data/lib/smash_and_grab/mixins/has_contents.rb +38 -0
- data/lib/smash_and_grab/mixins/line_of_sight.rb +129 -0
- data/lib/smash_and_grab/mixins/pathfinding.rb +145 -0
- data/lib/smash_and_grab/mouse_selection.rb +107 -36
- data/lib/smash_and_grab/objects/entity.rb +311 -260
- data/lib/smash_and_grab/objects/floating_text.rb +0 -1
- data/lib/smash_and_grab/objects/static.rb +10 -3
- data/lib/smash_and_grab/objects/vehicle.rb +7 -2
- data/lib/smash_and_grab/objects/world_object.rb +10 -3
- data/lib/smash_and_grab/path.rb +38 -12
- data/lib/smash_and_grab/players/ai.rb +91 -0
- data/lib/smash_and_grab/players/human.rb +9 -0
- data/lib/smash_and_grab/players/player.rb +16 -65
- data/lib/smash_and_grab/players/remote.rb +9 -0
- data/lib/smash_and_grab/sprite_sheet.rb +9 -0
- data/lib/smash_and_grab/states/edit_level.rb +15 -4
- data/lib/smash_and_grab/states/main_menu.rb +16 -4
- data/lib/smash_and_grab/states/play_level.rb +88 -28
- data/lib/smash_and_grab/states/world.rb +17 -9
- data/lib/smash_and_grab/version.rb +1 -1
- data/lib/smash_and_grab/z_order.rb +6 -5
- data/media/images/path.png +0 -0
- data/media/images/tile_selection.png +0 -0
- data/media/images/tiles_selection.png +0 -0
- data/smash_and_grab.gemspec +2 -2
- data/tasks/create_portraits.rb +39 -0
- data/tasks/outline_images.rb +56 -0
- data/test/smash_and_grab/abilities/drop_test.rb +68 -0
- data/test/smash_and_grab/abilities/melee_test.rb +4 -4
- data/test/smash_and_grab/abilities/pick_up_test.rb +68 -0
- data/test/smash_and_grab/abilities/ranged_test.rb +105 -0
- data/test/smash_and_grab/abilities/sprint_test.rb +55 -25
- data/test/smash_and_grab/map/faction_test.rb +18 -16
- data/test/smash_and_grab/map/map_test.rb +9 -4
- metadata +51 -19
- data/lib/smash_and_grab/fidgit_ext/event.rb +0 -77
@@ -1,15 +1,21 @@
|
|
1
1
|
require 'set'
|
2
2
|
require 'fiber'
|
3
3
|
|
4
|
-
|
4
|
+
|
5
5
|
require_relative "../abilities"
|
6
6
|
require_relative "world_object"
|
7
7
|
require_relative "floating_text"
|
8
8
|
|
9
|
+
require_relative "../mixins/line_of_sight"
|
10
|
+
require_relative "../mixins/pathfinding"
|
11
|
+
require_relative "../mixins/has_contents"
|
12
|
+
|
9
13
|
module SmashAndGrab
|
10
14
|
module Objects
|
11
15
|
class Entity < WorldObject
|
12
|
-
|
16
|
+
include Mixins::LineOfSight
|
17
|
+
include Mixins::Pathfinding
|
18
|
+
include Mixins::HasContents
|
13
19
|
|
14
20
|
CLASS = :entity
|
15
21
|
|
@@ -18,28 +24,54 @@ class Entity < WorldObject
|
|
18
24
|
|
19
25
|
STATS_BACKGROUND_COLOR = Color::BLACK
|
20
26
|
STATS_HP_COLOR = Color.rgb(0, 200, 0)
|
21
|
-
STATS_MP_COLOR = Color
|
27
|
+
STATS_MP_COLOR = Color::rgb(50, 50, 255)
|
22
28
|
STATS_AP_COLOR = Color::YELLOW
|
23
|
-
STATS_USED_COLOR = Color.rgb(100, 100, 100)
|
24
29
|
STATS_WIDTH = 12.0
|
25
30
|
STATS_HALF_WIDTH = STATS_WIDTH / 2
|
31
|
+
PIP_WIDTH, PIP_SEP_WIDTH = 2, 0.5
|
32
|
+
STATS_USED_SATURATION = 0.5
|
33
|
+
|
34
|
+
ACTOR_NAME_COLOR = Color.rgb(50, 200, 50)
|
35
|
+
TARGET_NAME_COLOR = Color.rgb(50, 200, 50)
|
36
|
+
DAMAGE_NUMBER_COLOR = Color::RED
|
37
|
+
|
38
|
+
COLOR_ACTIVE = Color::rgb(255, 255, 255)
|
39
|
+
COLOR_ACTIVE_NO_MOVE = STATS_AP_COLOR
|
40
|
+
COLOR_ACTIVE_NO_ACTION = STATS_MP_COLOR
|
41
|
+
COLOR_ACTIVE_FINISHED = Color.rgb(70, 70, 70)
|
42
|
+
COLOR_INACTIVE = Color::BLACK
|
26
43
|
|
27
44
|
class << self
|
28
45
|
def config; @config ||= YAML.load_file(File.expand_path("config/map/entities.yml", EXTRACT_PATH)); end
|
29
46
|
def types; config.keys; end
|
30
|
-
def sprites; @sprites ||= SpriteSheet
|
31
|
-
def portraits; @portraits ||= SpriteSheet
|
47
|
+
def sprites; @sprites ||= SpriteSheet["entities.png", SPRITE_WIDTH, SPRITE_HEIGHT, 8]; end
|
48
|
+
def portraits; @portraits ||= SpriteSheet["entity_portraits.png", PORTRAIT_WIDTH, PORTRAIT_HEIGHT, 8]; end
|
32
49
|
end
|
33
50
|
|
34
|
-
|
51
|
+
event :ended_turn
|
52
|
+
event :started_turn
|
53
|
+
|
54
|
+
attr_reader :faction, :movement_points, :action_points, :health_points, :type, :portrait,
|
55
|
+
:max_movement_points, :max_action_points, :max_health_points, :default_faction_type
|
35
56
|
|
36
|
-
|
37
|
-
|
57
|
+
alias_method :hp, :health_points
|
58
|
+
alias_method :max_hp, :max_health_points
|
38
59
|
|
39
|
-
|
40
|
-
|
60
|
+
def minimap_color; @faction.minimap_color; end
|
61
|
+
def active?; @faction.active?; end
|
62
|
+
def inactive?; @faction.inactive?; end
|
41
63
|
|
42
|
-
|
64
|
+
def movement_points=(movement_points)
|
65
|
+
@movement_points = movement_points
|
66
|
+
publish :changed
|
67
|
+
@movement_points
|
68
|
+
end
|
69
|
+
|
70
|
+
def action_points=(action_points)
|
71
|
+
@action_points = action_points
|
72
|
+
publish :changed
|
73
|
+
@action_points
|
74
|
+
end
|
43
75
|
|
44
76
|
alias_method :max_mp, :max_movement_points
|
45
77
|
alias_method :max_ap, :max_action_points
|
@@ -51,14 +83,20 @@ class Entity < WorldObject
|
|
51
83
|
alias_method :ap=, :action_points=
|
52
84
|
|
53
85
|
def to_s; "<#{self.class.name}/#{@type}##{id} #{tile ? grid_position : "[off-map]"}>"; end
|
54
|
-
def
|
55
|
-
def
|
86
|
+
def alive?; @health_points > 0 and @tile; end
|
87
|
+
def title; t.title; end
|
88
|
+
def colorized_name; faction.class::TEXT_COLOR.colorize name; end
|
89
|
+
|
90
|
+
def bystander?; faction.is_a? Factions::Bystanders; end
|
91
|
+
def goody?; faction.is_a? Factions::Goodies; end
|
92
|
+
def baddy?; faction.is_a? Factions::Baddies; end
|
56
93
|
|
57
94
|
def initialize(map, data)
|
58
95
|
@type = data[:type]
|
59
96
|
config = self.class.config[data[:type]]
|
60
97
|
|
61
|
-
@
|
98
|
+
@default_faction_type = config[:faction]
|
99
|
+
@faction = nil
|
62
100
|
|
63
101
|
options = {
|
64
102
|
image: self.class.sprites[*config[:spritesheet_position]],
|
@@ -77,8 +115,8 @@ class Entity < WorldObject
|
|
77
115
|
@max_action_points = config[:action_points]
|
78
116
|
@action_points = data[:action_points] || @max_action_points
|
79
117
|
|
80
|
-
@
|
81
|
-
@
|
118
|
+
@max_health_points = config[:health_points]
|
119
|
+
@health_points = data[:health_points] || @max_health_points
|
82
120
|
|
83
121
|
# Load other abilities of the entity from config.
|
84
122
|
@abilities = {}
|
@@ -92,9 +130,14 @@ class Entity < WorldObject
|
|
92
130
|
end
|
93
131
|
end
|
94
132
|
|
95
|
-
|
133
|
+
if max_ap > 0
|
134
|
+
@abilities[:pick_up] = Abilities.ability(self, type: :pick_up)
|
135
|
+
@abilities[:drop] = Abilities.ability(self, type: :drop)
|
136
|
+
end
|
96
137
|
|
97
|
-
@
|
138
|
+
@tmp_contents_id = data[:contents_id] # Need to wait until all objects are loaded before picking it up.
|
139
|
+
|
140
|
+
@queued_activities = []
|
98
141
|
|
99
142
|
@stat_bars_record = nil
|
100
143
|
subscribe :changed do
|
@@ -102,38 +145,61 @@ class Entity < WorldObject
|
|
102
145
|
end
|
103
146
|
end
|
104
147
|
|
148
|
+
def faction=(faction)
|
149
|
+
@faction.remove self if @faction
|
150
|
+
@faction = faction
|
151
|
+
@faction << self
|
152
|
+
end
|
153
|
+
|
105
154
|
def has_ability?(type); @abilities.has_key? type; end
|
106
155
|
def ability(type); @abilities[type]; end
|
107
156
|
|
108
|
-
def
|
109
|
-
original_health = @
|
110
|
-
@
|
157
|
+
def health_points=(value)
|
158
|
+
original_health = @health_points
|
159
|
+
@health_points = [value, 0].max
|
111
160
|
|
112
161
|
# Show damage/healing as a floating number.
|
113
|
-
if original_health != @
|
114
|
-
text, color = if @
|
115
|
-
["+#{@
|
162
|
+
if original_health != @health_points
|
163
|
+
text, color = if @health_points > original_health
|
164
|
+
["+#{@health_points - original_health}", Color::GREEN]
|
116
165
|
else
|
117
|
-
[(@
|
166
|
+
[(@health_points - original_health).to_s, Color::RED]
|
118
167
|
end
|
119
168
|
|
120
169
|
FloatingText.new(text, color: color, x: x, y: y - height / 3, zorder: y - 0.01)
|
121
170
|
publish :changed
|
122
171
|
end
|
123
172
|
|
124
|
-
if @
|
125
|
-
parent.publish :game_info, "#{
|
173
|
+
if @health_points == 0 and @tile
|
174
|
+
parent.publish :game_info, "#{colorized_name} was vanquished!"
|
175
|
+
|
176
|
+
# Leave the tile, then drop anything we are carrying into it.
|
177
|
+
# TODO: this is not undo/redoable!
|
178
|
+
old_tile = tile
|
126
179
|
self.tile = nil
|
180
|
+
drop old_tile if contents
|
181
|
+
|
127
182
|
@queued_activities.empty?
|
128
183
|
end
|
184
|
+
|
185
|
+
@health_points
|
129
186
|
end
|
130
|
-
alias_method :hp=, :
|
187
|
+
alias_method :hp=, :health_points=
|
131
188
|
|
132
|
-
# Called from
|
189
|
+
# Called from GameActions::Ability
|
133
190
|
# Also used to un-melee :)
|
134
|
-
def
|
191
|
+
def make_melee_attack(target, damage)
|
135
192
|
add_activity do
|
136
|
-
if damage
|
193
|
+
if damage == 0 # Missed
|
194
|
+
face target
|
195
|
+
self.z += 10
|
196
|
+
delay 0.1
|
197
|
+
self.z -= 10
|
198
|
+
|
199
|
+
parent.publish :game_info, "#{colorized_name} swung at #{target.colorized_name}, but missed"
|
200
|
+
missed target
|
201
|
+
|
202
|
+
elsif damage > 0 # do => wound
|
137
203
|
face target
|
138
204
|
self.z += 10
|
139
205
|
delay 0.1
|
@@ -141,19 +207,19 @@ class Entity < WorldObject
|
|
141
207
|
|
142
208
|
# Can be dead at this point if there were 2-3 attackers of opportunity!
|
143
209
|
if target.alive?
|
144
|
-
parent.publish :game_info, "#{
|
145
|
-
target.
|
210
|
+
parent.publish :game_info, "#{colorized_name} smashed #{target.colorized_name} for {#{DAMAGE_NUMBER_COLOR.colorize damage}}"
|
211
|
+
target.hp -= damage
|
146
212
|
|
147
213
|
target.color = Color.rgb(255, 100, 100)
|
148
214
|
delay 0.1
|
149
215
|
target.color = Color::WHITE
|
150
216
|
else
|
151
|
-
parent.publish :game_info, "#{
|
217
|
+
parent.publish :game_info, "#{colorized_name} kicked #{target.colorized_name} while they were down"
|
152
218
|
end
|
153
219
|
else # undo => heal
|
154
220
|
target.color = Color.rgb(255, 100, 100)
|
155
221
|
delay 0.1
|
156
|
-
target.
|
222
|
+
target.hp -= damage
|
157
223
|
target.color = Color::WHITE
|
158
224
|
|
159
225
|
self.z += 10
|
@@ -163,50 +229,148 @@ class Entity < WorldObject
|
|
163
229
|
end
|
164
230
|
end
|
165
231
|
|
232
|
+
def missed(target)
|
233
|
+
FloatingText.new("Miss!", color: Color::YELLOW, x: target.x, y: target.y - target.height / 3, zorder: target.y - 0.01)
|
234
|
+
end
|
235
|
+
|
236
|
+
def make_ranged_attack(target, damage)
|
237
|
+
add_activity do
|
238
|
+
if damage == 0
|
239
|
+
face target
|
240
|
+
|
241
|
+
parent.publish :game_info, "#{colorized_name} shot at #{target.colorized_name}, but missed"
|
242
|
+
|
243
|
+
missed target
|
244
|
+
elsif damage > 0 # do => wound
|
245
|
+
face target
|
246
|
+
|
247
|
+
# Can be dead at this point if there were 2-3 attackers of opportunity!
|
248
|
+
if target.alive?
|
249
|
+
parent.publish :game_info, "#{colorized_name} shot #{target.colorized_name} for {#{DAMAGE_NUMBER_COLOR.colorize damage}}"
|
250
|
+
target.hp -= damage
|
251
|
+
|
252
|
+
target.color = Color.rgb(255, 100, 100)
|
253
|
+
delay 0.1
|
254
|
+
target.color = Color::WHITE
|
255
|
+
else
|
256
|
+
parent.publish :game_info, "#{colorized_name} shot #{target.colorized_name} while they were down"
|
257
|
+
end
|
258
|
+
else # undo => heal
|
259
|
+
target.color = Color.rgb(255, 100, 100)
|
260
|
+
delay 0.1
|
261
|
+
target.hp -= damage
|
262
|
+
target.color = Color::WHITE
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def start_game
|
268
|
+
@health_points = max_hp # Has to be done directly or you could take damage or heal from it :D
|
269
|
+
self.mp = max_mp
|
270
|
+
self.ap = max_ap
|
271
|
+
end
|
272
|
+
|
166
273
|
def start_turn
|
167
|
-
|
168
|
-
|
274
|
+
self.mp = max_mp
|
275
|
+
self.ap = max_ap
|
276
|
+
publish :started_turn
|
169
277
|
publish :changed
|
170
278
|
end
|
171
279
|
|
172
280
|
def end_turn
|
173
|
-
|
281
|
+
publish :ended_turn
|
282
|
+
publish :changed
|
283
|
+
end
|
284
|
+
|
285
|
+
# Color of circular base you stand on.
|
286
|
+
def base_color
|
287
|
+
if active?
|
288
|
+
if move?
|
289
|
+
ap > 0 ? COLOR_ACTIVE : COLOR_ACTIVE_NO_ACTION
|
290
|
+
else
|
291
|
+
ap > 0 ? COLOR_ACTIVE_NO_MOVE : COLOR_ACTIVE_FINISHED
|
292
|
+
end
|
293
|
+
else
|
294
|
+
COLOR_INACTIVE
|
295
|
+
end
|
174
296
|
end
|
175
297
|
|
176
298
|
def draw
|
177
299
|
return unless alive?
|
178
300
|
|
301
|
+
if active?
|
302
|
+
color = base_color.dup
|
303
|
+
color.alpha = 60
|
304
|
+
Image["tile_selection.png"].draw_rot x, y, y, 0, 0.5, 0.5, 1, 1, color
|
305
|
+
end
|
306
|
+
|
179
307
|
super()
|
180
308
|
|
181
|
-
draw_stat_bars
|
309
|
+
draw_stat_bars if parent.zoom >= 2
|
182
310
|
end
|
183
311
|
|
184
|
-
def
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
312
|
+
def draw_stat_pips(value, max, color, y)
|
313
|
+
# Draw a background which appears between and underneath the pips.
|
314
|
+
width = (max - 1) * PIP_SEP_WIDTH + max * PIP_WIDTH
|
315
|
+
$window.pixel.draw 0, y, 0, width, 1 + PIP_SEP_WIDTH, STATS_BACKGROUND_COLOR
|
316
|
+
|
317
|
+
# Draw the pips themselves.
|
318
|
+
max.times do |i|
|
319
|
+
if i < value and alive?
|
320
|
+
pip_color = color
|
321
|
+
else
|
322
|
+
pip_color = color.dup
|
323
|
+
pip_color.red *= STATS_USED_SATURATION
|
324
|
+
pip_color.blue *= STATS_USED_SATURATION
|
325
|
+
pip_color.green *= STATS_USED_SATURATION
|
326
|
+
end
|
327
|
+
$window.pixel.draw i * (PIP_WIDTH + PIP_SEP_WIDTH), y, 0, PIP_WIDTH, 1, pip_color
|
328
|
+
end
|
329
|
+
end
|
190
330
|
|
191
|
-
|
331
|
+
def draw_stat_bars(options = {})
|
332
|
+
options = {
|
333
|
+
x: x - STATS_HALF_WIDTH,
|
334
|
+
y: y - 4,
|
335
|
+
zorder: y,
|
336
|
+
factor_x: 1,
|
337
|
+
factor_y: 1,
|
338
|
+
}.merge! options
|
192
339
|
|
193
|
-
|
194
|
-
|
340
|
+
@stat_bars_record ||= $window.record 1, 1 do
|
341
|
+
# Health. 1 or two rows of up to 5 pips. Cannot have > 10 HP!
|
342
|
+
full_rows, top_row_pips = max_hp.divmod 5
|
343
|
+
case full_rows
|
344
|
+
when 0
|
345
|
+
draw_stat_pips(hp, top_row_pips, STATS_HP_COLOR, 1.5)
|
346
|
+
when 1
|
347
|
+
draw_stat_pips(hp - 5, top_row_pips, STATS_HP_COLOR, 0) if max_hp > 5
|
348
|
+
draw_stat_pips([hp, 5].min, 5, STATS_HP_COLOR, 1.5)
|
349
|
+
else # 2
|
350
|
+
draw_stat_pips(hp - 5, 5, STATS_HP_COLOR, 0)
|
351
|
+
draw_stat_pips([hp, 5].min, 5, STATS_HP_COLOR, 1.5)
|
352
|
+
end
|
195
353
|
|
196
|
-
# Action points.
|
354
|
+
# Action points. Cannot have > 5 AP!
|
197
355
|
if max_ap > 0
|
198
|
-
|
199
|
-
max_ap.times do |i|
|
200
|
-
color = i < ap ? STATS_AP_COLOR : STATS_USED_COLOR
|
201
|
-
$window.pixel.draw i * pip_width + i, 1, 0, pip_width, 1, color
|
202
|
-
end
|
356
|
+
draw_stat_pips(ap, max_ap, STATS_AP_COLOR, 3)
|
203
357
|
end
|
204
358
|
|
205
|
-
# Movement points.
|
206
|
-
|
359
|
+
# Movement points. Just use a bar, since they aren't so critical and could be up to 20.
|
360
|
+
if active?
|
361
|
+
$window.pixel.draw 0, 4.5, 0, STATS_WIDTH, 1.5, STATS_BACKGROUND_COLOR
|
362
|
+
used_color = STATS_MP_COLOR.dup
|
363
|
+
used_color.red *= STATS_USED_SATURATION
|
364
|
+
used_color.blue *= STATS_USED_SATURATION
|
365
|
+
used_color.green *= STATS_USED_SATURATION
|
366
|
+
$window.pixel.draw 0, 4.5, 0, STATS_WIDTH, 1, used_color
|
367
|
+
|
368
|
+
width = alive? ? STATS_WIDTH * mp : 0
|
369
|
+
$window.pixel.draw 0, 4.5, 0, width / [mp, max_mp].max, 1, STATS_MP_COLOR if active?
|
370
|
+
end
|
207
371
|
end
|
208
372
|
|
209
|
-
@stat_bars_record.draw
|
373
|
+
@stat_bars_record.draw options[:x], options[:y], options[:zorder], options[:factor_x], options[:factor_y]
|
210
374
|
end
|
211
375
|
|
212
376
|
def friend?(character); @faction.friend? character.faction; end
|
@@ -224,125 +388,6 @@ class Entity < WorldObject
|
|
224
388
|
super
|
225
389
|
end
|
226
390
|
|
227
|
-
# Returns a list of tiles this entity could move to (including those they could melee at) [Set]
|
228
|
-
def potential_moves
|
229
|
-
destination_tile = tile # We are sort of working backwards here.
|
230
|
-
|
231
|
-
# Tiles we've already dealt with.
|
232
|
-
closed_tiles = Set.new
|
233
|
-
# Tiles we've looked at and that are in-range.
|
234
|
-
valid_tiles = Set.new
|
235
|
-
# Paths to check { tile => path_to_tile }.
|
236
|
-
open_paths = { destination_tile => Paths::Start.new(destination_tile, destination_tile) }
|
237
|
-
|
238
|
-
melee_cost = has_ability?(:melee) ? ability(:melee).action_cost : Float::INFINITY
|
239
|
-
|
240
|
-
while open_paths.any?
|
241
|
-
path = open_paths.each_value.min_by(&:cost)
|
242
|
-
current_tile = path.last
|
243
|
-
|
244
|
-
open_paths.delete current_tile
|
245
|
-
closed_tiles << current_tile
|
246
|
-
|
247
|
-
exits = current_tile.exits(self).reject {|wall| closed_tiles.include? wall.destination(current_tile) }
|
248
|
-
exits.each do |wall|
|
249
|
-
testing_tile = wall.destination(current_tile)
|
250
|
-
object = testing_tile.object
|
251
|
-
|
252
|
-
if object and object.is_a?(Objects::Entity) and enemy?(object)
|
253
|
-
# Ensure that the current tile is somewhere we could launch an attack from and we could actually perform it.
|
254
|
-
if (current_tile.empty? or current_tile == tile) and ap >= melee_cost
|
255
|
-
valid_tiles << testing_tile
|
256
|
-
end
|
257
|
-
|
258
|
-
elsif testing_tile.passable?(self) and (object.nil? or object.passable?(self))
|
259
|
-
new_path = Paths::Move.new(path, testing_tile, wall.movement_cost)
|
260
|
-
|
261
|
-
# If the path is shorter than one we've already calculated, then replace it. Otherwise just store it.
|
262
|
-
if new_path.move_distance <= movement_points
|
263
|
-
old_path = open_paths[testing_tile]
|
264
|
-
if old_path
|
265
|
-
if new_path.move_distance < old_path.move_distance
|
266
|
-
open_paths[testing_tile] = new_path
|
267
|
-
end
|
268
|
-
else
|
269
|
-
open_paths[testing_tile] = new_path
|
270
|
-
valid_tiles << testing_tile if testing_tile.empty?
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
valid_tiles
|
278
|
-
end
|
279
|
-
|
280
|
-
# A* path-finding.
|
281
|
-
def path_to(destination_tile)
|
282
|
-
return Paths::None.new if destination_tile == tile
|
283
|
-
return Paths::Inaccessible.new(destination_tile) unless destination_tile.passable?(self)
|
284
|
-
|
285
|
-
closed_tiles = Set.new # Tiles we've already dealt with.
|
286
|
-
open_paths = { tile => Paths::Start.new(tile, destination_tile) } # Paths to check { tile => path_to_tile }.
|
287
|
-
|
288
|
-
destination_object = destination_tile.object
|
289
|
-
destination_is_enemy = (destination_object and destination_object.is_a? Entity and destination_object.enemy?(self))
|
290
|
-
|
291
|
-
melee_cost = has_ability?(:melee) ? ability(:melee).action_cost : Float::INFINITY
|
292
|
-
|
293
|
-
while open_paths.any?
|
294
|
-
# Check the (expected) shortest path and move it to closed, since we have considered it.
|
295
|
-
path = open_paths.each_value.min_by(&:cost)
|
296
|
-
current_tile = path.last
|
297
|
-
|
298
|
-
return path if current_tile == destination_tile
|
299
|
-
|
300
|
-
open_paths.delete current_tile
|
301
|
-
closed_tiles << current_tile
|
302
|
-
|
303
|
-
next if path.is_a? Paths::Melee
|
304
|
-
|
305
|
-
# Check adjacent tiles.
|
306
|
-
exits = current_tile.exits(self).reject {|wall| closed_tiles.include? wall.destination(current_tile) }
|
307
|
-
exits.each do |wall|
|
308
|
-
testing_tile = wall.destination(current_tile)
|
309
|
-
|
310
|
-
new_path = nil
|
311
|
-
|
312
|
-
object = testing_tile.object
|
313
|
-
if testing_tile.zoc?(faction) and not (testing_tile == destination_tile or destination_is_enemy)
|
314
|
-
# Avoid tiles that have zoc, unless at the end of the path. You have to MANUALLY enter.
|
315
|
-
next
|
316
|
-
elsif object and object.is_a?(Objects::Entity) and enemy?(object)
|
317
|
-
# Ensure that the current tile is somewhere we could launch an attack from and we could actually perform it.
|
318
|
-
if (current_tile.empty? or current_tile == tile) and ap >= melee_cost
|
319
|
-
new_path = Paths::Melee.new(path, testing_tile)
|
320
|
-
else
|
321
|
-
next
|
322
|
-
end
|
323
|
-
elsif testing_tile.passable?(self)
|
324
|
-
if object.nil? or object.passable?(self)
|
325
|
-
new_path = Paths::Move.new(path, testing_tile, wall.movement_cost)
|
326
|
-
else
|
327
|
-
next
|
328
|
-
end
|
329
|
-
end
|
330
|
-
|
331
|
-
# If the path is shorter than one we've already calculated, then replace it. Otherwise just store it.
|
332
|
-
old_path = open_paths[testing_tile]
|
333
|
-
if old_path
|
334
|
-
if new_path.move_distance < old_path.move_distance
|
335
|
-
open_paths[testing_tile] = new_path
|
336
|
-
end
|
337
|
-
else
|
338
|
-
open_paths[testing_tile] = new_path
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
Paths::Inaccessible.new(destination_tile) # Failed to connect at all.
|
344
|
-
end
|
345
|
-
|
346
391
|
def update
|
347
392
|
super
|
348
393
|
|
@@ -366,9 +411,9 @@ class Entity < WorldObject
|
|
366
411
|
end
|
367
412
|
|
368
413
|
def clear_activities
|
369
|
-
|
414
|
+
had_activities = @queued_activities.any?
|
370
415
|
@queued_activities.clear
|
371
|
-
publish :changed if
|
416
|
+
publish :changed if had_activities # Means busy? changed from true to false.
|
372
417
|
end
|
373
418
|
|
374
419
|
def busy?
|
@@ -392,13 +437,14 @@ class Entity < WorldObject
|
|
392
437
|
end
|
393
438
|
end
|
394
439
|
|
395
|
-
# @param target [Tile, Objects::WorldObject]
|
440
|
+
# @param target [Tile, Objects::WorldObject, Numeric]
|
396
441
|
def face(target)
|
397
|
-
|
442
|
+
x_pos = target.is_a?(Numeric) ? target : target.x
|
443
|
+
change_in_x = x_pos - x
|
398
444
|
self.factor_x = change_in_x > 0 ? 1 : -1
|
399
445
|
end
|
400
446
|
|
401
|
-
# Actually perform movement (called from
|
447
|
+
# Actually perform movement (called from GameActions::Ability).
|
402
448
|
def move(tiles, movement_cost)
|
403
449
|
raise "Not enough movement points (#{self} tried to move #{movement_cost} with #{@movement_points} left #{tiles} )" unless movement_cost <= @movement_points
|
404
450
|
|
@@ -410,10 +456,12 @@ class Entity < WorldObject
|
|
410
456
|
tiles.each_cons(2) do |from, to|
|
411
457
|
face to
|
412
458
|
|
459
|
+
# TODO: this will be triggered _every_ time you move, even when redoing is done!
|
460
|
+
trigger_zoc_melees from
|
461
|
+
break unless alive?
|
462
|
+
|
413
463
|
delay 0.1
|
414
464
|
|
415
|
-
# TODO: this will be triggered _every_ time you move, even when redoing is done!
|
416
|
-
trigger_zoc_melee from
|
417
465
|
break unless alive?
|
418
466
|
|
419
467
|
# Skip through a tile if we are moving through something else!
|
@@ -426,7 +474,11 @@ class Entity < WorldObject
|
|
426
474
|
end
|
427
475
|
|
428
476
|
# TODO: this will be triggered _every_ time you move, even when redoing is done!
|
429
|
-
|
477
|
+
trigger_overwatches to
|
478
|
+
break unless alive?
|
479
|
+
|
480
|
+
# TODO: this will be triggered _every_ time you move, even when redoing is done!
|
481
|
+
trigger_zoc_melees to
|
430
482
|
break unless alive?
|
431
483
|
end
|
432
484
|
end
|
@@ -434,112 +486,111 @@ class Entity < WorldObject
|
|
434
486
|
nil
|
435
487
|
end
|
436
488
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
489
|
+
def potential_ranged
|
490
|
+
tiles = []
|
491
|
+
|
492
|
+
if use_ability? :ranged
|
493
|
+
ranged = ability :ranged
|
494
|
+
min, max = ranged.min_range, ranged.max_range
|
495
|
+
((grid_x - max)..(grid_x + max)).each do |x|
|
496
|
+
((grid_y - max)..(grid_y + max)).each do |y|
|
497
|
+
tile = map.tile_at_grid(x, y)
|
498
|
+
if tile and tile != self.tile and
|
499
|
+
not (tile.object.is_a?(Static)) and
|
500
|
+
not (tile.object.is_a?(Entity) and friend?(tile.object)) and
|
501
|
+
manhattan_distance(tile).between?(min, max) and line_of_sight? tile
|
502
|
+
|
503
|
+
tiles << tile
|
504
|
+
end
|
505
|
+
end
|
450
506
|
end
|
451
507
|
end
|
508
|
+
|
509
|
+
tiles
|
452
510
|
end
|
453
511
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
step_y = y1 < y2 ? 1 : -1
|
470
|
-
dx, dy = (x2 - x1).abs, (y2 - y1).abs
|
471
|
-
|
472
|
-
if dx == dy
|
473
|
-
# Special case of the diagonal line.
|
474
|
-
(dx - 1).times do
|
475
|
-
x1 += step_x
|
476
|
-
y1 += step_y
|
477
|
-
|
478
|
-
# If the centre tile is blocked, then we don't work.
|
479
|
-
tile = @map.tile_at_grid(x1, y1)
|
480
|
-
if tile.blocks_sight?
|
481
|
-
#Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::RED
|
482
|
-
return tile
|
483
|
-
else
|
484
|
-
#Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::BLUE
|
512
|
+
def manhattan_distance(tile)
|
513
|
+
(tile.grid_x - grid_x).abs + (tile.grid_y - grid_y).abs
|
514
|
+
end
|
515
|
+
|
516
|
+
# We have moved; let all our enemies shoot at us.
|
517
|
+
def trigger_overwatches(tile)
|
518
|
+
@map.factions.each do |faction|
|
519
|
+
if faction.enemy? self.faction
|
520
|
+
faction.entities.each do |enemy|
|
521
|
+
if alive?
|
522
|
+
enemy.attempt_overwatch self
|
523
|
+
prepend_activity do
|
524
|
+
delay while enemy.busy?
|
525
|
+
end
|
526
|
+
end
|
485
527
|
end
|
486
528
|
end
|
487
|
-
|
488
|
-
|
489
|
-
error = dx - dy
|
529
|
+
end
|
530
|
+
end
|
490
531
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
532
|
+
# Someone has moved into our view and we get to shoot them...
|
533
|
+
def attempt_overwatch(target)
|
534
|
+
if overwatch? target.tile
|
535
|
+
parent.publish :game_info, "#{colorized_name} made a snap shot!"
|
536
|
+
use_ability :ranged, target
|
537
|
+
end
|
538
|
+
end
|
495
539
|
|
496
|
-
|
540
|
+
def overwatch?(tile)
|
541
|
+
if alive? and use_ability? :ranged
|
542
|
+
ranged = ability :ranged
|
543
|
+
range = manhattan_distance tile
|
497
544
|
|
498
|
-
(
|
499
|
-
|
500
|
-
|
501
|
-
error -= dy
|
502
|
-
x1 += step_x
|
503
|
-
else
|
504
|
-
error += dx
|
505
|
-
y1 += step_y
|
506
|
-
end
|
545
|
+
range.between?(ranged.min_range, ranged.max_range) and line_of_sight? tile
|
546
|
+
end
|
547
|
+
end
|
507
548
|
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
549
|
+
# TODO: Need to think of the best way to trigger this. It should only happen once, when you actually "first" move.
|
550
|
+
def trigger_zoc_melees(tile)
|
551
|
+
entities = tile.entities_exerting_zoc(self)
|
552
|
+
enemies = entities.find_all {|e| e.enemy? self }
|
553
|
+
enemies.each do |enemy|
|
554
|
+
if alive?
|
555
|
+
enemy.attempt_zoc_melee self
|
556
|
+
prepend_activity do
|
557
|
+
delay while enemy.busy?
|
514
558
|
end
|
515
559
|
end
|
516
560
|
end
|
561
|
+
end
|
517
562
|
|
518
|
-
|
563
|
+
# Someone has moved into, or out of, our ZoC.
|
564
|
+
def attempt_zoc_melee(target)
|
565
|
+
if alive? and use_ability?(:melee)
|
566
|
+
parent.publish :game_info, "#{colorized_name} got an attack of opportunity!"
|
567
|
+
use_ability :melee, target
|
568
|
+
end
|
519
569
|
end
|
520
570
|
|
571
|
+
|
521
572
|
def use_ability(name, *args)
|
522
573
|
raise "#{self} does not have ability: #{name.inspect}" unless has_ability? name
|
523
574
|
map.actions.do :ability, ability(name).action_data(*args)
|
524
|
-
publish :changed
|
525
575
|
end
|
526
576
|
|
527
577
|
def use_ability?(name)
|
528
|
-
alive? and has_ability?(name) and ap >= ability(name).action_cost
|
578
|
+
alive? and has_ability?(name) and ap >= ability(name).action_cost and ability(name).use?
|
529
579
|
end
|
530
580
|
|
531
581
|
def to_json(*a)
|
532
582
|
data = {
|
533
583
|
:class => CLASS,
|
534
|
-
type:
|
584
|
+
type: type,
|
535
585
|
id: id,
|
536
|
-
health:
|
537
|
-
|
538
|
-
|
586
|
+
health: health_points,
|
587
|
+
contents_id: contents ? contents.id : nil,
|
588
|
+
movement_points: movement_points,
|
589
|
+
action_points: action_points,
|
539
590
|
facing: factor_x > 0 ? :right : :left,
|
540
591
|
}
|
541
592
|
|
542
|
-
data[:tile] = grid_position if
|
593
|
+
data[:tile] = grid_position if tile
|
543
594
|
|
544
595
|
data.to_json(*a)
|
545
596
|
end
|