smash_and_grab 0.0.3alpha → 0.0.5alpha

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 (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,