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.
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