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.
Files changed (67) hide show
  1. data/{CHANGELOG.txt → CHANGELOG.md} +0 -0
  2. data/Gemfile.lock +8 -4
  3. data/LICENSE.txt +20 -0
  4. data/README.md +30 -14
  5. data/Rakefile +2 -2
  6. data/config/lang/objects/entities/en.yml +134 -0
  7. data/config/lang/objects/static/en.yml +8 -0
  8. data/config/lang/objects/vehicles/en.yml +11 -0
  9. data/config/map/entities.yml +42 -38
  10. data/lib/smash_and_grab.rb +5 -0
  11. data/lib/smash_and_grab/abilities.rb +1 -1
  12. data/lib/smash_and_grab/abilities/ability.rb +45 -3
  13. data/lib/smash_and_grab/abilities/drop.rb +38 -0
  14. data/lib/smash_and_grab/abilities/melee.rb +4 -6
  15. data/lib/smash_and_grab/abilities/pick_up.rb +33 -0
  16. data/lib/smash_and_grab/abilities/ranged.rb +18 -12
  17. data/lib/smash_and_grab/abilities/sprint.rb +11 -7
  18. data/lib/smash_and_grab/chingu_ext/basic_game_object.rb +26 -0
  19. data/lib/smash_and_grab/game_window.rb +5 -1
  20. data/lib/smash_and_grab/gui/entity_panel.rb +26 -10
  21. data/lib/smash_and_grab/gui/entity_summary.rb +19 -11
  22. data/lib/smash_and_grab/gui/game_log.rb +6 -2
  23. data/lib/smash_and_grab/gui/info_panel.rb +7 -4
  24. data/lib/smash_and_grab/gui/object_panel.rb +6 -2
  25. data/lib/smash_and_grab/gui/scenario_panel.rb +4 -0
  26. data/lib/smash_and_grab/history/action_history.rb +1 -1
  27. data/lib/smash_and_grab/main.rb +7 -3
  28. data/lib/smash_and_grab/map/faction.rb +26 -8
  29. data/lib/smash_and_grab/map/map.rb +21 -12
  30. data/lib/smash_and_grab/map/tile.rb +29 -2
  31. data/lib/smash_and_grab/map/wall.rb +2 -2
  32. data/lib/smash_and_grab/mixins/has_contents.rb +38 -0
  33. data/lib/smash_and_grab/mixins/line_of_sight.rb +129 -0
  34. data/lib/smash_and_grab/mixins/pathfinding.rb +145 -0
  35. data/lib/smash_and_grab/mouse_selection.rb +107 -36
  36. data/lib/smash_and_grab/objects/entity.rb +311 -260
  37. data/lib/smash_and_grab/objects/floating_text.rb +0 -1
  38. data/lib/smash_and_grab/objects/static.rb +10 -3
  39. data/lib/smash_and_grab/objects/vehicle.rb +7 -2
  40. data/lib/smash_and_grab/objects/world_object.rb +10 -3
  41. data/lib/smash_and_grab/path.rb +38 -12
  42. data/lib/smash_and_grab/players/ai.rb +91 -0
  43. data/lib/smash_and_grab/players/human.rb +9 -0
  44. data/lib/smash_and_grab/players/player.rb +16 -65
  45. data/lib/smash_and_grab/players/remote.rb +9 -0
  46. data/lib/smash_and_grab/sprite_sheet.rb +9 -0
  47. data/lib/smash_and_grab/states/edit_level.rb +15 -4
  48. data/lib/smash_and_grab/states/main_menu.rb +16 -4
  49. data/lib/smash_and_grab/states/play_level.rb +88 -28
  50. data/lib/smash_and_grab/states/world.rb +17 -9
  51. data/lib/smash_and_grab/version.rb +1 -1
  52. data/lib/smash_and_grab/z_order.rb +6 -5
  53. data/media/images/path.png +0 -0
  54. data/media/images/tile_selection.png +0 -0
  55. data/media/images/tiles_selection.png +0 -0
  56. data/smash_and_grab.gemspec +2 -2
  57. data/tasks/create_portraits.rb +39 -0
  58. data/tasks/outline_images.rb +56 -0
  59. data/test/smash_and_grab/abilities/drop_test.rb +68 -0
  60. data/test/smash_and_grab/abilities/melee_test.rb +4 -4
  61. data/test/smash_and_grab/abilities/pick_up_test.rb +68 -0
  62. data/test/smash_and_grab/abilities/ranged_test.rb +105 -0
  63. data/test/smash_and_grab/abilities/sprint_test.rb +55 -25
  64. data/test/smash_and_grab/map/faction_test.rb +18 -16
  65. data/test/smash_and_grab/map/map_test.rb +9 -4
  66. metadata +51 -19
  67. 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
- require_relative "../path"
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
- extend Forwardable
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.rgb(100, 100, 255)
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.new("entities.png", SPRITE_WIDTH, SPRITE_HEIGHT, 8); end
31
- def portraits; @portraits ||= SpriteSheet.new("entity_portraits.png", PORTRAIT_WIDTH, PORTRAIT_HEIGHT, 8); end
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
- def_delegators :@faction, :minimap_color, :active?, :inactive?
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
- attr_reader :faction, :movement_points, :action_points, :health, :type, :portrait,
37
- :max_movement_points, :max_action_points, :max_health
57
+ alias_method :hp, :health_points
58
+ alias_method :max_hp, :max_health_points
38
59
 
39
- alias_method :hp, :health
40
- alias_method :max_hp, :max_health
60
+ def minimap_color; @faction.minimap_color; end
61
+ def active?; @faction.active?; end
62
+ def inactive?; @faction.inactive?; end
41
63
 
42
- attr_writer :movement_points, :action_points
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 name; @type.to_s.split("_").map(&:capitalize).join(" "); end
55
- def alive?; @health > 0 and @tile; end
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
- @faction = map.send(config[:faction])
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
- @max_health = config[:health]
81
- @health = data[:health] || @max_health
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
- @queued_activities = []
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
- @faction << self
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 health=(value)
109
- original_health = @health
110
- @health = [value, 0].max
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 != @health
114
- text, color = if @health > original_health
115
- ["+#{@health - original_health}", Color::GREEN]
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
- [(@health - original_health).to_s, Color::RED]
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 @health == 0 and @tile
125
- parent.publish :game_info, "#{name} was vanquished!"
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=, :health=
187
+ alias_method :hp=, :health_points=
131
188
 
132
- # Called from GameAction::Ability
189
+ # Called from GameActions::Ability
133
190
  # Also used to un-melee :)
134
- def melee(target, damage)
191
+ def make_melee_attack(target, damage)
135
192
  add_activity do
136
- if damage > 0 # do => wound
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, "#{name} smashed #{target.name} for {#{damage}}"
145
- target.health -= damage
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, "#{name} kicked #{target.name} while they were down"
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.health -= damage
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
- @movement_points = @max_movement_points
168
- @action_points = @max_action_points
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
- # Do something?
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 @y
309
+ draw_stat_bars if parent.zoom >= 2
182
310
  end
183
311
 
184
- def draw_stat_bars(zorder)
185
- @stat_bars_record ||= $window.record 1, 1 do
186
- # Draw background shadow.
187
- height = 1
188
- height += 1 if active?
189
- height += 1 if max_ap > 0
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
- $window.pixel.draw -0.5, -0.5, 0, STATS_WIDTH + 1, height + 1, STATS_BACKGROUND_COLOR
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
- # Health.
194
- $window.pixel.draw 0, 0, 0, STATS_WIDTH * health / max_health, 1, STATS_HP_COLOR
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
- pip_width = (STATS_WIDTH + 1 - max_ap) / max_ap
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
- $window.pixel.draw 0, 2, 0, 12.0 * mp / max_mp, 1, STATS_MP_COLOR if active?
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 @x - STATS_HALF_WIDTH, @y - 4, zorder
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
- has_activities = @queued_activities.any?
414
+ had_activities = @queued_activities.any?
370
415
  @queued_activities.clear
371
- publish :changed if has_activities # Means busy? changed from true to false.
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
- change_in_x = target.x - x
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 GameAction::Ability).
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
- trigger_zoc_melee to
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
- # TODO: Need to think of the best way to trigger this. It should only happen once, when you actually "first" move.
438
- def trigger_zoc_melee(tile)
439
- entities = tile.entities_exerting_zoc(self)
440
- enemies = entities.find_all {|e| e.enemy?(self) and e.has_ability?(:melee) and e.use_ability?(:melee) }
441
- enemies.each do |enemy|
442
- break unless alive? and enemy.alive?
443
-
444
- parent.publish :game_info, "#{enemy.name} got an attack of opportunity!"
445
-
446
- enemy.use_ability :melee, self
447
-
448
- prepend_activity do
449
- delay while enemy.busy?
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
- def line_of_sight?(tile)
456
- !line_of_sight_blocked_by(tile)
457
- end
458
-
459
- # Returns the tile that blocks sight, otherwise nil.
460
- # Implements 'Bresenham's line algorithm'
461
- def line_of_sight_blocked_by(target_tile)
462
- start_tile = tile
463
-
464
- # Check for the special case of looking diagonally.
465
- x1, y1 = tile.grid_x, tile.grid_y
466
- x2, y2 = target_tile.grid_x, target_tile.grid_y
467
-
468
- step_x = x1 < x2 ? 1 : -1
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
- else
488
- # General case, ray-trace.
489
- error = dx - dy
529
+ end
530
+ end
490
531
 
491
- # Ensure that all tiles are visited that the sight-line passes over,
492
- # not just those that create a "drawn" line.
493
- dx *= 2
494
- dy *= 2
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
- length = ((dx + dy + 1) / 2)
540
+ def overwatch?(tile)
541
+ if alive? and use_ability? :ranged
542
+ ranged = ability :ranged
543
+ range = manhattan_distance tile
497
544
 
498
- (length - 1).times do
499
- # Note that this ignores the special case of error == 0
500
- if error > 0
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
- tile = @map.tile_at_grid(x1, y1)
509
- if tile.blocks_sight?
510
- #Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::RED
511
- return tile
512
- else
513
- #Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::BLUE
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
- nil # Didn't hit anything.
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: @type,
584
+ type: type,
535
585
  id: id,
536
- health: @health,
537
- movement_points: @movement_points,
538
- action_points: @action_points,
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 @tile
593
+ data[:tile] = grid_position if tile
543
594
 
544
595
  data.to_json(*a)
545
596
  end