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.
- data/Gemfile.lock +1 -1
- data/README.md +31 -16
- data/config/gui/schema.yml +3 -2
- data/config/map/entities.yml +36 -12
- data/lib/smash_and_grab/abilities/ability.rb +5 -0
- data/lib/smash_and_grab/abilities/melee.rb +11 -5
- data/lib/smash_and_grab/abilities/ranged.rb +20 -0
- data/lib/smash_and_grab/abilities/sprint.rb +4 -0
- data/lib/smash_and_grab/abilities.rb +3 -1
- data/lib/smash_and_grab/fidgit_ext/event.rb +77 -0
- data/lib/smash_and_grab/gui/editor_selector.rb +54 -73
- data/lib/smash_and_grab/gui/entity_panel.rb +110 -0
- data/lib/smash_and_grab/gui/entity_summary.rb +9 -8
- data/lib/smash_and_grab/gui/game_log.rb +44 -0
- data/lib/smash_and_grab/gui/info_panel.rb +39 -95
- data/lib/smash_and_grab/gui/object_panel.rb +37 -0
- data/lib/smash_and_grab/gui/scenario_panel.rb +21 -0
- data/lib/smash_and_grab/history/editor_actions/place_object.rb +1 -1
- data/lib/smash_and_grab/main.rb +11 -16
- data/lib/smash_and_grab/map/map.rb +1 -0
- data/lib/smash_and_grab/map/tile.rb +6 -3
- data/lib/smash_and_grab/map/wall.rb +4 -2
- data/lib/smash_and_grab/mouse_selection.rb +103 -46
- data/lib/smash_and_grab/objects/entity.rb +219 -30
- data/lib/smash_and_grab/objects/static.rb +7 -5
- data/lib/smash_and_grab/objects/vehicle.rb +7 -5
- data/lib/smash_and_grab/objects/world_object.rb +13 -3
- data/lib/smash_and_grab/path.rb +13 -7
- data/lib/smash_and_grab/players/player.rb +15 -10
- data/lib/smash_and_grab/states/edit_level.rb +17 -0
- data/lib/smash_and_grab/states/play_level.rb +20 -10
- data/lib/smash_and_grab/version.rb +1 -1
- data/lib/smash_and_grab.rb +18 -14
- data/test/smash_and_grab/abilities/melee_test.rb +37 -39
- data/test/teststrap.rb +3 -3
- metadata +21 -16
@@ -1,13 +1,11 @@
|
|
1
1
|
module SmashAndGrab
|
2
2
|
class MouseSelection < GameObject
|
3
|
-
attr_reader :
|
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,
|
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
|
-
@
|
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
|
49
|
+
modify_occlusions @hover_tile, -1 if @hover_tile
|
33
50
|
@hover_tile = tile
|
34
|
-
modify_occlusions
|
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
|
57
|
+
if selected_can_be_controlled?
|
41
58
|
modify_occlusions @path.tiles, -1 if @path
|
42
59
|
|
43
60
|
if @hover_tile
|
44
|
-
@path =
|
45
|
-
@path.prepare_for_drawing
|
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 =
|
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
|
-
|
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(
|
78
|
-
Tile.blank.draw_rot tile.x, tile.y, 0, 0, 0.5, 0.5, 0.
|
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
|
94
|
-
selected_color =
|
95
|
-
|
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 @
|
109
|
-
path = @path
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
156
|
+
|
157
|
+
calculate_path
|
158
|
+
calculate_potential_moves
|
159
|
+
elsif @hover_tile and @hover_tile.object
|
126
160
|
# Select a character to move.
|
127
|
-
|
161
|
+
self.selected = @hover_tile.object
|
128
162
|
end
|
129
163
|
end
|
130
164
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
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 >=
|
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
|
-
|
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
|
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 >=
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
409
|
+
add_activity do
|
410
|
+
tiles.each_cons(2) do |from, to|
|
411
|
+
face to
|
260
412
|
|
261
|
-
|
262
|
-
|
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
|
-
|
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
|
269
|
-
|
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
|
-
|
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
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
94
|
+
:class => CLASS,
|
93
95
|
type: @type,
|
94
96
|
id: id,
|
95
97
|
tile: grid_position,
|