smash_and_grab 0.0.5alpha → 0.0.6alpha
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/{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
|