smash_and_grab 0.0.3alpha → 0.0.5alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/Gemfile.lock +1 -1
  2. data/README.md +31 -16
  3. data/config/gui/schema.yml +3 -2
  4. data/config/map/entities.yml +36 -12
  5. data/lib/smash_and_grab/abilities/ability.rb +5 -0
  6. data/lib/smash_and_grab/abilities/melee.rb +11 -5
  7. data/lib/smash_and_grab/abilities/ranged.rb +20 -0
  8. data/lib/smash_and_grab/abilities/sprint.rb +4 -0
  9. data/lib/smash_and_grab/abilities.rb +3 -1
  10. data/lib/smash_and_grab/fidgit_ext/event.rb +77 -0
  11. data/lib/smash_and_grab/gui/editor_selector.rb +54 -73
  12. data/lib/smash_and_grab/gui/entity_panel.rb +110 -0
  13. data/lib/smash_and_grab/gui/entity_summary.rb +9 -8
  14. data/lib/smash_and_grab/gui/game_log.rb +44 -0
  15. data/lib/smash_and_grab/gui/info_panel.rb +39 -95
  16. data/lib/smash_and_grab/gui/object_panel.rb +37 -0
  17. data/lib/smash_and_grab/gui/scenario_panel.rb +21 -0
  18. data/lib/smash_and_grab/history/editor_actions/place_object.rb +1 -1
  19. data/lib/smash_and_grab/main.rb +11 -16
  20. data/lib/smash_and_grab/map/map.rb +1 -0
  21. data/lib/smash_and_grab/map/tile.rb +6 -3
  22. data/lib/smash_and_grab/map/wall.rb +4 -2
  23. data/lib/smash_and_grab/mouse_selection.rb +103 -46
  24. data/lib/smash_and_grab/objects/entity.rb +219 -30
  25. data/lib/smash_and_grab/objects/static.rb +7 -5
  26. data/lib/smash_and_grab/objects/vehicle.rb +7 -5
  27. data/lib/smash_and_grab/objects/world_object.rb +13 -3
  28. data/lib/smash_and_grab/path.rb +13 -7
  29. data/lib/smash_and_grab/players/player.rb +15 -10
  30. data/lib/smash_and_grab/states/edit_level.rb +17 -0
  31. data/lib/smash_and_grab/states/play_level.rb +20 -10
  32. data/lib/smash_and_grab/version.rb +1 -1
  33. data/lib/smash_and_grab.rb +18 -14
  34. data/test/smash_and_grab/abilities/melee_test.rb +37 -39
  35. data/test/teststrap.rb +3 -3
  36. metadata +21 -16
@@ -1,13 +1,11 @@
1
1
  module SmashAndGrab
2
2
  class MouseSelection < GameObject
3
- attr_reader :selected_tile, :hover_tile
3
+ attr_reader :selected, :hover_tile
4
4
 
5
5
  MOVE_COLOR = Color.rgba(0, 255, 0, 60)
6
6
  MELEE_COLOR = Color.rgba(255, 0, 0, 80)
7
7
  NO_MOVE_COLOR = Color.rgba(255, 0, 0, 30)
8
- ZOC_COLOR = Color.rgba(255, 0, 0, 255)
9
-
10
- def selected; @selected_tile ? @selected_tile.object : nil; end
8
+ ZOC_COLOR = Color.rgba(255, 0, 0, 100)
11
9
 
12
10
  def initialize(map, options = {})
13
11
  @map = map
@@ -17,32 +15,51 @@ class MouseSelection < GameObject
17
15
  @selected_image = Image["tile_selection.png"]
18
16
  @mouse_hover_image = Image["mouse_hover.png"]
19
17
 
20
- @selected_tile = @hover_tile = nil
18
+ @selected = @hover_tile = nil
21
19
  @path = nil
22
20
  @moves_record = nil
21
+ @selected_changed_handler = nil
23
22
 
24
23
  super(options)
25
24
 
26
25
  add_inputs(released_left_mouse_button: :left_click,
27
26
  released_right_mouse_button: :right_click)
27
+
28
+ @map.factions.each do |faction|
29
+ faction.subscribe :turn_ended do
30
+ recalculate
31
+ end
32
+ faction.subscribe :turn_started do
33
+ recalculate
34
+ end
35
+ end
36
+ end
37
+
38
+ def update
39
+ self.selected = nil if selected and selected.tile.nil?
40
+ super
41
+ end
42
+
43
+ def selected_can_be_controlled?
44
+ selected and selected.active? and selected.faction.player.is_a?(Players::Human) and not @map.busy?
28
45
  end
29
46
 
30
47
  def tile=(tile)
31
48
  if tile != @hover_tile
32
- modify_occlusions [@hover_tile], -1 if @hover_tile
49
+ modify_occlusions @hover_tile, -1 if @hover_tile
33
50
  @hover_tile = tile
34
- modify_occlusions [@hover_tile], +1 if @hover_tile
51
+ modify_occlusions @hover_tile, +1 if @hover_tile
35
52
  calculate_path
36
53
  end
37
54
  end
38
55
 
39
56
  def calculate_path
40
- if @selected_tile
57
+ if selected_can_be_controlled?
41
58
  modify_occlusions @path.tiles, -1 if @path
42
59
 
43
60
  if @hover_tile
44
- @path = @selected_tile.object.path_to(@hover_tile)
45
- @path.prepare_for_drawing(@potential_moves)
61
+ @path = selected.path_to @hover_tile
62
+ @path.prepare_for_drawing @potential_moves
46
63
  modify_occlusions @path.tiles, +1
47
64
  else
48
65
  @path = nil
@@ -55,16 +72,16 @@ class MouseSelection < GameObject
55
72
 
56
73
  def calculate_potential_moves
57
74
  modify_occlusions @potential_moves, -1
58
- @potential_moves = @selected_tile.object.potential_moves
75
+ @potential_moves = selected.potential_moves
59
76
  modify_occlusions @potential_moves, +1
60
77
 
61
-
62
78
  @moves_record = if @potential_moves.empty?
63
79
  nil
64
80
  else
65
81
  $window.record(1, 1) do
66
82
  @potential_moves.each do |tile|
67
- color = if entity = tile.object and entity.enemy?(@selected_tile.object)
83
+ entity = tile.object
84
+ color = if entity and entity.enemy?(selected)
68
85
  MELEE_COLOR
69
86
  else
70
87
  MOVE_COLOR
@@ -74,8 +91,8 @@ class MouseSelection < GameObject
74
91
  Tile.blank.draw_rot tile.x, tile.y, 0, 0, 0.5, 0.5, 1, 1, color, :additive
75
92
 
76
93
  # ZOC indicator.
77
- if tile.entities_exerting_zoc(@selected_tile.object.faction).any?
78
- Tile.blank.draw_rot tile.x, tile.y, 0, 0, 0.5, 0.5, 0.2, 0.2, ZOC_COLOR
94
+ if tile.entities_exerting_zoc(selected.tile.object.faction).any? and tile.empty?
95
+ Tile.blank.draw_rot tile.x, tile.y, 0, 0, 0.5, 0.5, 0.7, 0.7, ZOC_COLOR
79
96
  end
80
97
  end
81
98
  end
@@ -83,16 +100,25 @@ class MouseSelection < GameObject
83
100
  end
84
101
 
85
102
  def modify_occlusions(tiles, amount)
86
- tiles.each do |tile|
103
+ Array(tiles).each do |tile|
87
104
  tile.modify_occlusions amount
88
105
  end
89
106
  end
90
107
 
91
108
  def draw
92
109
  # Draw a disc under the selected object.
93
- if @selected_tile
94
- selected_color = @selected_tile.object.move? ? Color::GREEN : Color::BLACK
95
- @selected_image.draw_rot @selected_tile.x, @selected_tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, selected_color
110
+ if selected and selected.tile
111
+ selected_color = if selected.is_a? Objects::Entity
112
+ if selected_can_be_controlled? and selected.move?
113
+ Color::GREEN
114
+ else
115
+ Color::BLACK
116
+ end
117
+ else
118
+ Color::BLUE
119
+ end
120
+
121
+ @selected_image.draw_rot selected.tile.x, selected.tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, selected_color
96
122
 
97
123
  # Highlight all squares that character can travel to.
98
124
  @moves_record.draw 0, 0, ZOrder::TILE_SELECTION if @moves_record
@@ -102,36 +128,45 @@ class MouseSelection < GameObject
102
128
  color = (@hover_tile.empty? or @hover_tile.object.inactive?) ? Color::BLUE : Color::CYAN
103
129
  @mouse_hover_image.draw_rot @hover_tile.x, @hover_tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, color
104
130
  end
131
+
132
+ # Make the stat-bars visible when hovering over something else.
133
+ if @hover_tile and not (selected and @hover_tile == selected.tile)
134
+ object = @hover_tile.object
135
+ object.draw_stat_bars 10000 if object.is_a? Objects::Entity and object.alive?
136
+ end
105
137
  end
106
138
 
107
139
  def left_click
108
- if @selected_tile
109
- path = @path
110
- # Move the character.
111
- if @potential_moves.include? @hover_tile
112
- case path
113
- when Paths::Move
114
- @map.actions.do :ability, selected.ability(:move).action_data(path)
115
- @selected_tile = @hover_tile
116
- when Paths::Melee
117
- attacker = selected
118
- @map.actions.do :ability, attacker.ability(:move).action_data(path.previous_path) if path.requires_movement?
119
- @map.actions.do :ability, attacker.ability(:melee).action_data(path.last)
120
- @selected_tile = attacker.tile
121
- end
122
- calculate_path
123
- calculate_potential_moves
140
+ if @potential_moves.include? @hover_tile
141
+ path = @path # @path will change as we move.
142
+
143
+ # Move the character, perhaps with melee at the end.
144
+ case path
145
+ when Paths::Move
146
+ selected.use_ability :move, path
147
+ when Paths::Melee
148
+ selected.use_ability :move, path.previous_path if path.requires_movement?
149
+
150
+ # Only perform melee if you weren't killed by attacks of opportunity.
151
+ attacker, target = selected, path.last.object
152
+ attacker.add_activity do
153
+ attacker.use_ability :melee, target if attacker.alive?
154
+ end
124
155
  end
125
- elsif @hover_tile and @hover_tile.object.is_a? Objects::Entity and @hover_tile.object.active?
156
+
157
+ calculate_path
158
+ calculate_potential_moves
159
+ elsif @hover_tile and @hover_tile.object
126
160
  # Select a character to move.
127
- select(@hover_tile.object)
161
+ self.selected = @hover_tile.object
128
162
  end
129
163
  end
130
164
 
131
- def select(entity)
132
- if entity
133
- @selected_tile = entity.tile
134
- @moves_record = nil
165
+ def recalculate
166
+ @moves_record = nil
167
+
168
+ if selected and selected_can_be_controlled?
169
+ calculate_path
135
170
  calculate_potential_moves
136
171
  else
137
172
  modify_occlusions @potential_moves, -1
@@ -139,16 +174,38 @@ class MouseSelection < GameObject
139
174
 
140
175
  modify_occlusions @path.tiles, -1 if @path
141
176
  @path = nil
177
+ end
178
+ end
142
179
 
143
- @selected_tile = nil
180
+ def selected=(object)
181
+ # Make sure we learn of any changes made to the selected object so we can move.
182
+ if selected
183
+ @selected_changed_handler.unsubscribe
184
+ @selected_changed_handler = nil
185
+ end
186
+
187
+ @selected = object
188
+ recalculate
189
+
190
+ if selected
191
+ tile, path, potential_moves = selected.tile, @path, @potential_moves.dup
192
+
193
+ @selected_changed_handler = selected.subscribe :changed do |entity|
194
+ recalculate
195
+
196
+ # Create a partial path while we move.
197
+ if path and entity.tile != tile
198
+ tile = entity.tile
199
+ path.prepare_for_drawing potential_moves, from: entity.tile
200
+ modify_occlusions path.tiles, +1
201
+ @path = path
202
+ end
203
+ end
144
204
  end
145
205
  end
146
206
 
147
207
  def right_click
148
- # Deselect the currently selected character.
149
- if @selected_tile
150
- select nil
151
- end
208
+ self.selected = nil if selected
152
209
  end
153
210
  end
154
211
  end
@@ -1,4 +1,6 @@
1
1
  require 'set'
2
+ require 'fiber'
3
+
2
4
  require_relative "../path"
3
5
  require_relative "../abilities"
4
6
  require_relative "world_object"
@@ -14,14 +16,29 @@ class Entity < WorldObject
14
16
  SPRITE_WIDTH, SPRITE_HEIGHT = 66, 66
15
17
  PORTRAIT_WIDTH, PORTRAIT_HEIGHT = 36, 36
16
18
 
17
- MELEE_COST = 1
18
- MELEE_DAMAGE = 5
19
+ STATS_BACKGROUND_COLOR = Color::BLACK
20
+ STATS_HP_COLOR = Color.rgb(0, 200, 0)
21
+ STATS_MP_COLOR = Color.rgb(100, 100, 255)
22
+ STATS_AP_COLOR = Color::YELLOW
23
+ STATS_USED_COLOR = Color.rgb(100, 100, 100)
24
+ STATS_WIDTH = 12.0
25
+ STATS_HALF_WIDTH = STATS_WIDTH / 2
26
+
27
+ class << self
28
+ def config; @config ||= YAML.load_file(File.expand_path("config/map/entities.yml", EXTRACT_PATH)); end
29
+ 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
32
+ end
19
33
 
20
34
  def_delegators :@faction, :minimap_color, :active?, :inactive?
21
35
 
22
36
  attr_reader :faction, :movement_points, :action_points, :health, :type, :portrait,
23
37
  :max_movement_points, :max_action_points, :max_health
24
38
 
39
+ alias_method :hp, :health
40
+ alias_method :max_hp, :max_health
41
+
25
42
  attr_writer :movement_points, :action_points
26
43
 
27
44
  alias_method :max_mp, :max_movement_points
@@ -37,11 +54,6 @@ class Entity < WorldObject
37
54
  def name; @type.to_s.split("_").map(&:capitalize).join(" "); end
38
55
  def alive?; @health > 0 and @tile; end
39
56
 
40
- def self.config; @@config ||= YAML.load_file(File.expand_path("config/map/entities.yml", EXTRACT_PATH)); end
41
- def self.types; config.keys; end
42
- def self.sprites; @@sprites ||= SpriteSheet.new("entities.png", SPRITE_WIDTH, SPRITE_HEIGHT, 8); end
43
- def self.portraits; @@portraits ||= SpriteSheet.new("entity_portraits.png", PORTRAIT_WIDTH, PORTRAIT_HEIGHT, 8); end
44
-
45
57
  def initialize(map, data)
46
58
  @type = data[:type]
47
59
  config = self.class.config[data[:type]]
@@ -76,12 +88,18 @@ class Entity < WorldObject
76
88
 
77
89
  if config[:abilities]
78
90
  config[:abilities].each do |ability_data|
79
- ability_data = ability_data
80
91
  @abilities[ability_data[:type]] = Abilities.ability(self, ability_data)
81
92
  end
82
93
  end
83
94
 
95
+ @queued_activities = []
96
+
84
97
  @faction << self
98
+
99
+ @stat_bars_record = nil
100
+ subscribe :changed do
101
+ @stat_bars_record = nil
102
+ end
85
103
  end
86
104
 
87
105
  def has_ability?(type); @abilities.has_key? type; end
@@ -100,21 +118,55 @@ class Entity < WorldObject
100
118
  end
101
119
 
102
120
  FloatingText.new(text, color: color, x: x, y: y - height / 3, zorder: y - 0.01)
121
+ publish :changed
103
122
  end
104
123
 
105
124
  if @health == 0 and @tile
125
+ parent.publish :game_info, "#{name} was vanquished!"
106
126
  self.tile = nil
127
+ @queued_activities.empty?
107
128
  end
108
129
  end
109
-
110
- def melee(other)
111
- other.health -= MELEE_DAMAGE
112
- @action_points -= MELEE_COST
130
+ alias_method :hp=, :health=
131
+
132
+ # Called from GameAction::Ability
133
+ # Also used to un-melee :)
134
+ def melee(target, damage)
135
+ add_activity do
136
+ if damage > 0 # do => wound
137
+ face target
138
+ self.z += 10
139
+ delay 0.1
140
+ self.z -= 10
141
+
142
+ # Can be dead at this point if there were 2-3 attackers of opportunity!
143
+ if target.alive?
144
+ parent.publish :game_info, "#{name} smashed #{target.name} for {#{damage}}"
145
+ target.health -= damage
146
+
147
+ target.color = Color.rgb(255, 100, 100)
148
+ delay 0.1
149
+ target.color = Color::WHITE
150
+ else
151
+ parent.publish :game_info, "#{name} kicked #{target.name} while they were down"
152
+ end
153
+ else # undo => heal
154
+ target.color = Color.rgb(255, 100, 100)
155
+ delay 0.1
156
+ target.health -= damage
157
+ target.color = Color::WHITE
158
+
159
+ self.z += 10
160
+ delay 0.1
161
+ self.z -= 10
162
+ end
163
+ end
113
164
  end
114
165
 
115
166
  def start_turn
116
167
  @movement_points = @max_movement_points
117
168
  @action_points = @max_action_points
169
+ publish :changed
118
170
  end
119
171
 
120
172
  def end_turn
@@ -122,7 +174,39 @@ class Entity < WorldObject
122
174
  end
123
175
 
124
176
  def draw
125
- super() if alive?
177
+ return unless alive?
178
+
179
+ super()
180
+
181
+ draw_stat_bars @y
182
+ end
183
+
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
190
+
191
+ $window.pixel.draw -0.5, -0.5, 0, STATS_WIDTH + 1, height + 1, STATS_BACKGROUND_COLOR
192
+
193
+ # Health.
194
+ $window.pixel.draw 0, 0, 0, STATS_WIDTH * health / max_health, 1, STATS_HP_COLOR
195
+
196
+ # Action points.
197
+ 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
203
+ end
204
+
205
+ # Movement points.
206
+ $window.pixel.draw 0, 2, 0, 12.0 * mp / max_mp, 1, STATS_MP_COLOR if active?
207
+ end
208
+
209
+ @stat_bars_record.draw @x - STATS_HALF_WIDTH, @y - 4, zorder
126
210
  end
127
211
 
128
212
  def friend?(character); @faction.friend? character.faction; end
@@ -151,6 +235,8 @@ class Entity < WorldObject
151
235
  # Paths to check { tile => path_to_tile }.
152
236
  open_paths = { destination_tile => Paths::Start.new(destination_tile, destination_tile) }
153
237
 
238
+ melee_cost = has_ability?(:melee) ? ability(:melee).action_cost : Float::INFINITY
239
+
154
240
  while open_paths.any?
155
241
  path = open_paths.each_value.min_by(&:cost)
156
242
  current_tile = path.last
@@ -165,7 +251,7 @@ class Entity < WorldObject
165
251
 
166
252
  if object and object.is_a?(Objects::Entity) and enemy?(object)
167
253
  # Ensure that the current tile is somewhere we could launch an attack from and we could actually perform it.
168
- if (current_tile.empty? or current_tile == tile) and ap >= MELEE_COST
254
+ if (current_tile.empty? or current_tile == tile) and ap >= melee_cost
169
255
  valid_tiles << testing_tile
170
256
  end
171
257
 
@@ -174,7 +260,8 @@ class Entity < WorldObject
174
260
 
175
261
  # If the path is shorter than one we've already calculated, then replace it. Otherwise just store it.
176
262
  if new_path.move_distance <= movement_points
177
- if old_path = open_paths[testing_tile]
263
+ old_path = open_paths[testing_tile]
264
+ if old_path
178
265
  if new_path.move_distance < old_path.move_distance
179
266
  open_paths[testing_tile] = new_path
180
267
  end
@@ -198,6 +285,11 @@ class Entity < WorldObject
198
285
  closed_tiles = Set.new # Tiles we've already dealt with.
199
286
  open_paths = { tile => Paths::Start.new(tile, destination_tile) } # Paths to check { tile => path_to_tile }.
200
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
+
201
293
  while open_paths.any?
202
294
  # Check the (expected) shortest path and move it to closed, since we have considered it.
203
295
  path = open_paths.each_value.min_by(&:cost)
@@ -218,15 +310,18 @@ class Entity < WorldObject
218
310
  new_path = nil
219
311
 
220
312
  object = testing_tile.object
221
- if object and object.is_a?(Objects::Entity) and enemy?(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)
222
317
  # Ensure that the current tile is somewhere we could launch an attack from and we could actually perform it.
223
- if (current_tile.empty? or current_tile == tile) and ap >= MELEE_COST
318
+ if (current_tile.empty? or current_tile == tile) and ap >= melee_cost
224
319
  new_path = Paths::Melee.new(path, testing_tile)
225
320
  else
226
321
  next
227
322
  end
228
323
  elsif testing_tile.passable?(self)
229
- if (object.nil? or object.passable?(self))
324
+ if object.nil? or object.passable?(self)
230
325
  new_path = Paths::Move.new(path, testing_tile, wall.movement_cost)
231
326
  else
232
327
  next
@@ -234,7 +329,8 @@ class Entity < WorldObject
234
329
  end
235
330
 
236
331
  # If the path is shorter than one we've already calculated, then replace it. Otherwise just store it.
237
- if old_path = open_paths[testing_tile]
332
+ old_path = open_paths[testing_tile]
333
+ if old_path
238
334
  if new_path.move_distance < old_path.move_distance
239
335
  open_paths[testing_tile] = new_path
240
336
  end
@@ -247,28 +343,111 @@ class Entity < WorldObject
247
343
  Paths::Inaccessible.new(destination_tile) # Failed to connect at all.
248
344
  end
249
345
 
250
- # Actually perform movement (called from GameAction::Move).
346
+ def update
347
+ super
348
+
349
+ unless @queued_activities.empty?
350
+ @queued_activities.first.resume if @queued_activities.first.alive?
351
+ unless @queued_activities.first.alive?
352
+ @queued_activities.shift
353
+ publish :changed if @queued_activities.empty?
354
+ end
355
+ end
356
+ end
357
+
358
+ def add_activity(&action)
359
+ @queued_activities << Fiber.new(&action)
360
+ publish :changed if @queued_activities.size == 1 # Means busy? changed from false to true.
361
+ end
362
+
363
+ def prepend_activity(&action)
364
+ @queued_activities.unshift Fiber.new(&action)
365
+ publish :changed if @queued_activities.size == 1 # Means busy? changed from false to true.
366
+ end
367
+
368
+ def clear_activities
369
+ has_activities = @queued_activities.any?
370
+ @queued_activities.clear
371
+ publish :changed if has_activities # Means busy? changed from true to false.
372
+ end
373
+
374
+ def busy?
375
+ @queued_activities.any?
376
+ end
377
+
378
+ # @overload delay(duration)
379
+ # Wait for duration (Called from an activity ONLY!)
380
+ # @param duration [Number]
381
+ #
382
+ # @overload delay
383
+ # Wait until next frame (Called from an activity ONLY!)
384
+ def delay(duration = 0)
385
+ raise if duration < 0
386
+
387
+ if duration == 0
388
+ Fiber.yield
389
+ else
390
+ finish = Time.now + duration
391
+ Fiber.yield until Time.now >= finish
392
+ end
393
+ end
394
+
395
+ # @param target [Tile, Objects::WorldObject]
396
+ def face(target)
397
+ change_in_x = target.x - x
398
+ self.factor_x = change_in_x > 0 ? 1 : -1
399
+ end
400
+
401
+ # Actually perform movement (called from GameAction::Ability).
251
402
  def move(tiles, movement_cost)
252
403
  raise "Not enough movement points (#{self} tried to move #{movement_cost} with #{@movement_points} left #{tiles} )" unless movement_cost <= @movement_points
253
404
 
254
405
  tiles = tiles.map {|pos| @map.tile_at_grid *pos } unless tiles.first.is_a? Tile
255
406
 
256
- destination = tiles.last
257
407
  @movement_points -= movement_cost
258
408
 
259
- change_in_x = destination.x - tiles[-2].x
409
+ add_activity do
410
+ tiles.each_cons(2) do |from, to|
411
+ face to
260
412
 
261
- # Turn based on movement.
262
- self.factor_x = change_in_x > 0 ? 1 : -1
413
+ delay 0.1
414
+
415
+ # TODO: this will be triggered _every_ time you move, even when redoing is done!
416
+ trigger_zoc_melee from
417
+ break unless alive?
418
+
419
+ # Skip through a tile if we are moving through something else!
420
+ if to.object
421
+ self.z = 20
422
+ self.x, self.y = to.x, to.y
423
+ else
424
+ self.tile = to
425
+ self.z = 0
426
+ end
263
427
 
264
- self.tile = destination
428
+ # TODO: this will be triggered _every_ time you move, even when redoing is done!
429
+ trigger_zoc_melee to
430
+ break unless alive?
431
+ end
432
+ end
433
+
434
+ nil
265
435
  end
266
436
 
267
437
  # TODO: Need to think of the best way to trigger this. It should only happen once, when you actually "first" move.
268
- def trigger_zoc_attacks
269
- enemies = tile.entities_exerting_zoc(self)
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) }
270
441
  enemies.each do |enemy|
271
- map.actions.do :ability, :melee, enemy, self # Only get one opportunity attack per enemy entering.
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?
450
+ end
272
451
  end
273
452
  end
274
453
 
@@ -339,9 +518,19 @@ class Entity < WorldObject
339
518
  nil # Didn't hit anything.
340
519
  end
341
520
 
521
+ def use_ability(name, *args)
522
+ raise "#{self} does not have ability: #{name.inspect}" unless has_ability? name
523
+ map.actions.do :ability, ability(name).action_data(*args)
524
+ publish :changed
525
+ end
526
+
527
+ def use_ability?(name)
528
+ alive? and has_ability?(name) and ap >= ability(name).action_cost
529
+ end
530
+
342
531
  def to_json(*a)
343
532
  data = {
344
- class: CLASS,
533
+ :class => CLASS,
345
534
  type: @type,
346
535
  id: id,
347
536
  health: @health,
@@ -356,4 +545,4 @@ class Entity < WorldObject
356
545
  end
357
546
  end
358
547
  end
359
- end
548
+ end
@@ -12,11 +12,13 @@ class Static < WorldObject
12
12
  def inactive?; true; end
13
13
 
14
14
  def to_s; "<#{self.class.name}/#{@type}##{id} #{tile ? grid_position : "[off-map]"}>"; end
15
- def name; @type.split("_").map(&:capitalize).join(" "); end
15
+ def name; @type.to_s.split("_").map(&:capitalize).join(" "); end
16
16
 
17
- def self.config; @@config ||= YAML.load_file(File.expand_path("config/map/objects.yml", EXTRACT_PATH)); end
18
- def self.types; config.keys; end
19
- def self.sprites; @@sprites ||= SpriteSheet.new("objects.png", 64 + 2, 64 + 2, 8); end
17
+ class << self
18
+ def config; @config ||= YAML.load_file(File.expand_path("config/map/objects.yml", EXTRACT_PATH)); end
19
+ def types; config.keys; end
20
+ def sprites; @sprites ||= SpriteSheet.new("objects.png", 64 + 2, 64 + 2, 8); end
21
+ end
20
22
 
21
23
  def initialize(map, data)
22
24
  @type = data[:type]
@@ -36,7 +38,7 @@ class Static < WorldObject
36
38
 
37
39
  def to_json(*a)
38
40
  {
39
- class: CLASS,
41
+ :class => CLASS,
40
42
  type: @type,
41
43
  id: id,
42
44
  tile: grid_position,
@@ -21,11 +21,13 @@ class Vehicle < WorldObject
21
21
  def inactive?; true; end
22
22
 
23
23
  def to_s; "<#{self.class.name}/#{@type}##{id} #{tile ? grid_position : "[off-map]"}>"; end
24
- def name; @type.split("_").map(&:capitalize).join(" "); end
24
+ def name; @type.to_s.split("_").map(&:capitalize).join(" "); end
25
25
 
26
- def self.config; @@config ||= YAML.load_file(File.expand_path("config/map/vehicles.yml", EXTRACT_PATH)); end
27
- def self.types; config.keys; end
28
- def self.sprites; @@sprites ||= SpriteSheet.new("vehicles.png", (128 * 2) + 2, (128 * 2) + 2, 3); end
26
+ class << self
27
+ def config; @config ||= YAML.load_file(File.expand_path("config/map/vehicles.yml", EXTRACT_PATH)); end
28
+ def types; config.keys; end
29
+ def sprites; @sprites ||= SpriteSheet.new("vehicles.png", (128 * 2) + 2, (128 * 2) + 2, 3); end
30
+ end
29
31
 
30
32
  def fills_tile_on_minimap?; true; end
31
33
 
@@ -89,7 +91,7 @@ class Vehicle < WorldObject
89
91
 
90
92
  def to_json(*a)
91
93
  {
92
- class: CLASS,
94
+ :class => CLASS,
93
95
  type: @type,
94
96
  id: id,
95
97
  tile: grid_position,