smash_and_grab 0.0.1alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/CHANGELOG.txt +0 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +61 -0
  4. data/README.md +92 -0
  5. data/Rakefile +69 -0
  6. data/bin/smash_and_grab +3 -0
  7. data/bin/smash_and_grab.rbw +3 -0
  8. data/config/gui/schema.yml +61 -0
  9. data/config/levels/01_bank.sgl +0 -0
  10. data/config/map/entities.yml +373 -0
  11. data/config/map/objects.yml +14 -0
  12. data/config/map/tiles.yml +46 -0
  13. data/config/map/vehicles.yml +19 -0
  14. data/config/map/walls.yml +77 -0
  15. data/lib/smash_and_grab/abilities/ability.rb +104 -0
  16. data/lib/smash_and_grab/abilities/area.rb +37 -0
  17. data/lib/smash_and_grab/abilities/melee.rb +39 -0
  18. data/lib/smash_and_grab/abilities/move.rb +32 -0
  19. data/lib/smash_and_grab/abilities/ranged.rb +34 -0
  20. data/lib/smash_and_grab/abilities/sprint.rb +42 -0
  21. data/lib/smash_and_grab/abilities.rb +10 -0
  22. data/lib/smash_and_grab/fidgit_ext/container.rb +10 -0
  23. data/lib/smash_and_grab/fidgit_ext/cursor.rb +7 -0
  24. data/lib/smash_and_grab/fidgit_ext/element.rb +27 -0
  25. data/lib/smash_and_grab/game_window.rb +27 -0
  26. data/lib/smash_and_grab/gosu_ext/font.rb +39 -0
  27. data/lib/smash_and_grab/gui/editor_selector.rb +174 -0
  28. data/lib/smash_and_grab/gui/entity_summary.rb +58 -0
  29. data/lib/smash_and_grab/gui/info_panel.rb +105 -0
  30. data/lib/smash_and_grab/gui/minimap.rb +92 -0
  31. data/lib/smash_and_grab/history/action_history.rb +60 -0
  32. data/lib/smash_and_grab/history/editor_action_history.rb +21 -0
  33. data/lib/smash_and_grab/history/editor_actions/erase_object.rb +20 -0
  34. data/lib/smash_and_grab/history/editor_actions/place_object.rb +34 -0
  35. data/lib/smash_and_grab/history/editor_actions/set_tile_type.rb +18 -0
  36. data/lib/smash_and_grab/history/editor_actions/set_wall_type.rb +18 -0
  37. data/lib/smash_and_grab/history/game_action_history.rb +43 -0
  38. data/lib/smash_and_grab/history/game_actions/ability.rb +26 -0
  39. data/lib/smash_and_grab/history/game_actions/end_turn.rb +18 -0
  40. data/lib/smash_and_grab/log.rb +30 -0
  41. data/lib/smash_and_grab/main.rb +75 -0
  42. data/lib/smash_and_grab/map/faction.rb +84 -0
  43. data/lib/smash_and_grab/map/map.rb +262 -0
  44. data/lib/smash_and_grab/map/tile.rb +181 -0
  45. data/lib/smash_and_grab/map/wall.rb +139 -0
  46. data/lib/smash_and_grab/mouse_selection.rb +154 -0
  47. data/lib/smash_and_grab/objects/entity.rb +359 -0
  48. data/lib/smash_and_grab/objects/floating_text.rb +27 -0
  49. data/lib/smash_and_grab/objects/static.rb +47 -0
  50. data/lib/smash_and_grab/objects/vehicle.rb +100 -0
  51. data/lib/smash_and_grab/objects/world_object.rb +94 -0
  52. data/lib/smash_and_grab/path.rb +147 -0
  53. data/lib/smash_and_grab/players/player.rb +71 -0
  54. data/lib/smash_and_grab/sprite_sheet.rb +16 -0
  55. data/lib/smash_and_grab/states/edit_level.rb +180 -0
  56. data/lib/smash_and_grab/states/main_menu.rb +71 -0
  57. data/lib/smash_and_grab/states/play_level.rb +148 -0
  58. data/lib/smash_and_grab/states/world.rb +216 -0
  59. data/lib/smash_and_grab/std_ext/array.rb +18 -0
  60. data/lib/smash_and_grab/std_ext/hash.rb +18 -0
  61. data/lib/smash_and_grab/texplay_ext/color.rb +7 -0
  62. data/lib/smash_and_grab/texplay_ext/image.rb +146 -0
  63. data/lib/smash_and_grab/texplay_ext/window.rb +10 -0
  64. data/lib/smash_and_grab/version.rb +3 -0
  65. data/lib/smash_and_grab/z_order.rb +12 -0
  66. data/lib/smash_and_grab/z_order_recorder.rb +59 -0
  67. data/lib/smash_and_grab.rb +107 -0
  68. data/media/fonts/UnmaskedBB.ttf +0 -0
  69. data/media/fonts/fontinfo.txt +25 -0
  70. data/media/icon.ico +0 -0
  71. data/media/images/entities.png +0 -0
  72. data/media/images/entity_portraits.png +0 -0
  73. data/media/images/floor_tiles.png +0 -0
  74. data/media/images/mouse_cursor.png +0 -0
  75. data/media/images/mouse_hover.png +0 -0
  76. data/media/images/mouse_hover_wall.png +0 -0
  77. data/media/images/objects.png +0 -0
  78. data/media/images/path.png +0 -0
  79. data/media/images/tile_selection.png +0 -0
  80. data/media/images/vehicles.png +0 -0
  81. data/media/images/walls.png +0 -0
  82. data/smash_and_grab.gemspec +35 -0
  83. data/test/smash_and_grab/abilities/helpers/ability_helper.rb +6 -0
  84. data/test/smash_and_grab/abilities/melee_test.rb +105 -0
  85. data/test/smash_and_grab/abilities/move_test.rb +87 -0
  86. data/test/smash_and_grab/abilities/sprint_test.rb +75 -0
  87. data/test/smash_and_grab/map/faction_test.rb +62 -0
  88. data/test/smash_and_grab/map/map_test.rb +114 -0
  89. data/test/smash_and_grab/map/tile_test.rb +17 -0
  90. data/test/smash_and_grab/map/wall_test.rb +5 -0
  91. data/test/smash_and_grab/std_ext/hash_test.rb +21 -0
  92. data/test/teststrap.rb +108 -0
  93. metadata +226 -0
@@ -0,0 +1,139 @@
1
+ module SmashAndGrab
2
+ class Wall < GameObject
3
+ SEMI_TRANSPARENT_COLOR = Color.rgba(255, 255, 255, 120)
4
+ OPAQUE_COLOR = Color::WHITE
5
+
6
+ SPRITE_WIDTH, SPRITE_HEIGHT = 32, 64
7
+
8
+ # [[x_offset, _y_offset], direction, height needed to occlude
9
+ WALL_OCCLUSION_POSITIONS = {
10
+ vertical: [
11
+ [[ 0, 0], 1], # Own tile.
12
+ [[+1, 0], 1], # Right.
13
+ [[+1, -1], 2], # Top right.
14
+ [[+2, -1], 2],
15
+ ],
16
+ horizontal: [
17
+ [[ 0, 0], 1], # Own tile.
18
+ [[ 0, -1], 1], # Top.
19
+ [[+1, -1], 2], # Top right.
20
+ [[+1, -2], 2],
21
+ ]
22
+ }
23
+
24
+ attr_reader :minimap_color, :tiles_high, :thickness, :movement_cost, :type, :tiles, :orientation
25
+
26
+ def blocks_movement?; movement_cost == Float::INFINITY; end
27
+ def allows_movement?; movement_cost < Float::INFINITY; end
28
+
29
+ def zorder; super + 0.01; end
30
+ def to_s; "<#{self.class.name}##{@type} #{@tiles[0].grid_position} <=> #{@tiles[1].grid_position}]>"; end
31
+ def occludes?; @occludes; end
32
+
33
+ def blocks_sight?; @blocks_sight; end
34
+
35
+ def self.config; @@config ||= YAML.load_file(File.expand_path("config/map/walls.yml", EXTRACT_PATH)); end
36
+ def self.sprites; @@sprites ||= SpriteSheet.new("walls.png", SPRITE_WIDTH, SPRITE_HEIGHT, 8); end
37
+
38
+ def initialize(map, data)
39
+ options = {
40
+ rotation_center: :bottom_center,
41
+ }
42
+
43
+ super(options)
44
+
45
+ @objects = []
46
+ @occludes = false # Does the wall occlude anything that should be seen?
47
+
48
+ @map = map
49
+
50
+ @tiles = data[:tiles].map {|p| map.tile_at_grid(*p) }.sort_by(&:y)
51
+
52
+ @destinations = {
53
+ @tiles.first => @tiles.last,
54
+ @tiles.last => @tiles.first,
55
+ }
56
+
57
+ self.x, self.y = @tiles.first.x, @tiles.first.y + (SPRITE_HEIGHT / 8),
58
+ self.zorder = @tiles.first.y + 0.01
59
+
60
+ if @tiles.last.grid_y > @tiles.first.grid_y
61
+ @tiles.last.add_wall :up, self
62
+ @tiles.first.add_wall :down, self
63
+ @orientation = :vertical
64
+ else
65
+ @tiles.last.add_wall :right, self
66
+ @tiles.first.add_wall :left, self
67
+ @orientation = :horizontal
68
+ end
69
+
70
+ self.type = data[:type]
71
+ end
72
+
73
+ def type=(type)
74
+ changed = defined? @type
75
+
76
+ @type = type
77
+
78
+ config = self.class.config[@type]
79
+
80
+ @minimap_color = Color.rgba(*config[:minimap_color])
81
+ @blocks_sight = config[:blocks_sight]
82
+ @movement_cost = config[:movement_cost]
83
+ @tiles_high = config[:tiles_high]
84
+
85
+ #@y -= (4 - @thickness) / 2 if @thickness
86
+ @thickness = config[:thickness]
87
+ #@y += (4 - @thickness) / 2 if @thickness
88
+
89
+ spritesheet_positions = config[:spritesheet_positions]
90
+ image = if @tiles.last.grid_y > @tiles.first.grid_y
91
+ spritesheet_positions ? self.class.sprites[*spritesheet_positions[:vertical]] : nil
92
+ else
93
+ spritesheet_positions ? self.class.sprites[*spritesheet_positions[:horizontal]] : nil
94
+ end
95
+
96
+ @map.remove self if @image
97
+ @image = image
98
+ @map << self if @image
99
+
100
+ @map.publish :wall_type_changed, self if changed
101
+
102
+ update_occlusion
103
+
104
+ type
105
+ end
106
+
107
+ # Recalculate possible occlusions from permanent objects.
108
+ def update_occlusion
109
+ grid_x, grid_y = tiles.first.grid_position
110
+
111
+ # Look at all positions that could
112
+ @occludes = WALL_OCCLUSION_POSITIONS[@orientation].any? do |(offset_x, offset_y), min_height|
113
+ if @tiles_high >= min_height
114
+ tile = @map.tile_at_grid(grid_x + offset_x, grid_y + offset_y)
115
+ tile and tile.needs_to_be_seen?
116
+ else
117
+ false
118
+ end
119
+ end
120
+
121
+ @color = occludes? ? SEMI_TRANSPARENT_COLOR : OPAQUE_COLOR
122
+ end
123
+
124
+ def destination(from)
125
+ blocks_movement? ? nil : @destinations[from]
126
+ end
127
+
128
+ def draw
129
+ @image.draw_rot @x, @y, @zorder, 0, 0.5, 1, 1, 1, @color
130
+ end
131
+
132
+ def to_json(*a)
133
+ {
134
+ type: @type,
135
+ tiles: @tiles.map(&:grid_position),
136
+ }.to_json(*a)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,154 @@
1
+ module SmashAndGrab
2
+ class MouseSelection < GameObject
3
+ attr_reader :selected_tile, :hover_tile
4
+
5
+ MOVE_COLOR = Color.rgba(0, 255, 0, 60)
6
+ MELEE_COLOR = Color.rgba(255, 0, 0, 80)
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
11
+
12
+ def initialize(map, options = {})
13
+ @map = map
14
+
15
+ @potential_moves = []
16
+
17
+ @selected_image = Image["tile_selection.png"]
18
+ @mouse_hover_image = Image["mouse_hover.png"]
19
+
20
+ @selected_tile = @hover_tile = nil
21
+ @path = nil
22
+ @moves_record = nil
23
+
24
+ super(options)
25
+
26
+ add_inputs(released_left_mouse_button: :left_click,
27
+ released_right_mouse_button: :right_click)
28
+ end
29
+
30
+ def tile=(tile)
31
+ if tile != @hover_tile
32
+ modify_occlusions [@hover_tile], -1 if @hover_tile
33
+ @hover_tile = tile
34
+ modify_occlusions [@hover_tile], +1 if @hover_tile
35
+ calculate_path
36
+ end
37
+ end
38
+
39
+ def calculate_path
40
+ if @selected_tile
41
+ modify_occlusions @path.tiles, -1 if @path
42
+
43
+ if @hover_tile
44
+ @path = @selected_tile.object.path_to(@hover_tile)
45
+ @path.prepare_for_drawing(@potential_moves)
46
+ modify_occlusions @path.tiles, +1
47
+ else
48
+ @path = nil
49
+ end
50
+ else
51
+ modify_occlusions @potential_moves, +1
52
+ @potential_moves.clear
53
+ end
54
+ end
55
+
56
+ def calculate_potential_moves
57
+ modify_occlusions @potential_moves, -1
58
+ @potential_moves = @selected_tile.object.potential_moves
59
+ modify_occlusions @potential_moves, +1
60
+
61
+
62
+ @moves_record = if @potential_moves.empty?
63
+ nil
64
+ else
65
+ $window.record(1, 1) do
66
+ @potential_moves.each do |tile|
67
+ color = if entity = tile.object and entity.enemy?(@selected_tile.object)
68
+ MELEE_COLOR
69
+ else
70
+ MOVE_COLOR
71
+ end
72
+
73
+ # Tile background
74
+ Tile.blank.draw_rot tile.x, tile.y, 0, 0, 0.5, 0.5, 1, 1, color, :additive
75
+
76
+ # 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
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def modify_occlusions(tiles, amount)
86
+ tiles.each do |tile|
87
+ tile.modify_occlusions amount
88
+ end
89
+ end
90
+
91
+ def draw
92
+ # 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
96
+
97
+ # Highlight all squares that character can travel to.
98
+ @moves_record.draw 0, 0, ZOrder::TILE_SELECTION if @moves_record
99
+ @path.draw if @path
100
+
101
+ elsif @hover_tile
102
+ color = (@hover_tile.empty? or @hover_tile.object.inactive?) ? Color::BLUE : Color::CYAN
103
+ @mouse_hover_image.draw_rot @hover_tile.x, @hover_tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, color
104
+ end
105
+ end
106
+
107
+ 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
124
+ end
125
+ elsif @hover_tile and @hover_tile.object.is_a? Objects::Entity and @hover_tile.object.active?
126
+ # Select a character to move.
127
+ select(@hover_tile.object)
128
+ end
129
+ end
130
+
131
+ def select(entity)
132
+ if entity
133
+ @selected_tile = entity.tile
134
+ @moves_record = nil
135
+ calculate_potential_moves
136
+ else
137
+ modify_occlusions @potential_moves, -1
138
+ @potential_moves.clear
139
+
140
+ modify_occlusions @path.tiles, -1 if @path
141
+ @path = nil
142
+
143
+ @selected_tile = nil
144
+ end
145
+ end
146
+
147
+ def right_click
148
+ # Deselect the currently selected character.
149
+ if @selected_tile
150
+ select nil
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,359 @@
1
+ require 'set'
2
+ require_relative "../path"
3
+ require_relative "../abilities"
4
+ require_relative "world_object"
5
+ require_relative "floating_text"
6
+
7
+ module SmashAndGrab
8
+ module Objects
9
+ class Entity < WorldObject
10
+ extend Forwardable
11
+
12
+ CLASS = :entity
13
+
14
+ SPRITE_WIDTH, SPRITE_HEIGHT = 66, 66
15
+ PORTRAIT_WIDTH, PORTRAIT_HEIGHT = 36, 36
16
+
17
+ MELEE_COST = 1
18
+ MELEE_DAMAGE = 5
19
+
20
+ def_delegators :@faction, :minimap_color, :active?, :inactive?
21
+
22
+ attr_reader :faction, :movement_points, :action_points, :health, :type, :portrait,
23
+ :max_movement_points, :max_action_points, :max_health
24
+
25
+ attr_writer :movement_points, :action_points
26
+
27
+ alias_method :max_mp, :max_movement_points
28
+ alias_method :max_ap, :max_action_points
29
+
30
+ alias_method :mp, :movement_points
31
+ alias_method :ap, :action_points
32
+
33
+ alias_method :mp=, :movement_points=
34
+ alias_method :ap=, :action_points=
35
+
36
+ def to_s; "<#{self.class.name}/#{@type}##{id} #{tile ? grid_position : "[off-map]"}>"; end
37
+ def name; @type.to_s.split("_").map(&:capitalize).join(" "); end
38
+ def alive?; @health > 0 and @tile; end
39
+
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
+ def initialize(map, data)
46
+ @type = data[:type]
47
+ config = self.class.config[data[:type]]
48
+
49
+ @faction = map.send(config[:faction])
50
+
51
+ options = {
52
+ image: self.class.sprites[*config[:spritesheet_position]],
53
+ factor_x: data[:facing].to_sym == :right ? 1 : -1,
54
+ }
55
+
56
+ @portrait = self.class.portraits[*config[:spritesheet_position]]
57
+
58
+ super(map, data, options)
59
+
60
+ raise @type unless image
61
+
62
+ @max_movement_points = config[:movement_points]
63
+ @movement_points = data[:movement_points] || @max_movement_points
64
+
65
+ @max_action_points = config[:action_points]
66
+ @action_points = data[:action_points] || @max_action_points
67
+
68
+ @max_health = config[:health]
69
+ @health = data[:health] || @max_health
70
+
71
+ # Load other abilities of the entity from config.
72
+ @abilities = {}
73
+
74
+ # Everyone who has movement_points has the ability to move, without it needing to be explicit.
75
+ @abilities[:move] = Abilities.ability(self, type: :move) if max_movement_points > 0
76
+
77
+ if config[:abilities]
78
+ config[:abilities].each do |ability_data|
79
+ ability_data = ability_data
80
+ @abilities[ability_data[:type]] = Abilities.ability(self, ability_data)
81
+ end
82
+ end
83
+
84
+ @faction << self
85
+ end
86
+
87
+ def has_ability?(type); @abilities.has_key? type; end
88
+ def ability(type); @abilities[type]; end
89
+
90
+ def health=(value)
91
+ original_health = @health
92
+ @health = [value, 0].max
93
+
94
+ # Show damage/healing as a floating number.
95
+ if original_health != @health
96
+ text, color = if @health > original_health
97
+ ["+#{@health - original_health}", Color::GREEN]
98
+ else
99
+ [(@health - original_health).to_s, Color::RED]
100
+ end
101
+
102
+ FloatingText.new(text, color: color, x: x, y: y - height / 3, zorder: y - 0.01)
103
+ end
104
+
105
+ if @health == 0 and @tile
106
+ self.tile = nil
107
+ end
108
+ end
109
+
110
+ def melee(other)
111
+ other.health -= MELEE_DAMAGE
112
+ @action_points -= MELEE_COST
113
+ end
114
+
115
+ def start_turn
116
+ @movement_points = @max_movement_points
117
+ @action_points = @max_action_points
118
+ end
119
+
120
+ def end_turn
121
+ # Do something?
122
+ end
123
+
124
+ def draw
125
+ super() if alive?
126
+ end
127
+
128
+ def friend?(character); @faction.friend? character.faction; end
129
+ def enemy?(character); @faction.enemy? character.faction; end
130
+
131
+ def exerts_zoc?; true; end
132
+ def action?; @action_points > 0; end
133
+ def move?; @movement_points > 0; end
134
+ def end_turn_on?(person); false; end
135
+ def impassable?(character); enemy? character; end
136
+ def passable?(character); friend? character; end
137
+
138
+ def destroy
139
+ @faction.remove self
140
+ super
141
+ end
142
+
143
+ # Returns a list of tiles this entity could move to (including those they could melee at) [Set]
144
+ def potential_moves
145
+ destination_tile = tile # We are sort of working backwards here.
146
+
147
+ # Tiles we've already dealt with.
148
+ closed_tiles = Set.new
149
+ # Tiles we've looked at and that are in-range.
150
+ valid_tiles = Set.new
151
+ # Paths to check { tile => path_to_tile }.
152
+ open_paths = { destination_tile => Paths::Start.new(destination_tile, destination_tile) }
153
+
154
+ while open_paths.any?
155
+ path = open_paths.each_value.min_by(&:cost)
156
+ current_tile = path.last
157
+
158
+ open_paths.delete current_tile
159
+ closed_tiles << current_tile
160
+
161
+ exits = current_tile.exits(self).reject {|wall| closed_tiles.include? wall.destination(current_tile) }
162
+ exits.each do |wall|
163
+ testing_tile = wall.destination(current_tile)
164
+ object = testing_tile.object
165
+
166
+ if object and object.is_a?(Objects::Entity) and enemy?(object)
167
+ # 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
169
+ valid_tiles << testing_tile
170
+ end
171
+
172
+ elsif testing_tile.passable?(self) and (object.nil? or object.passable?(self))
173
+ new_path = Paths::Move.new(path, testing_tile, wall.movement_cost)
174
+
175
+ # If the path is shorter than one we've already calculated, then replace it. Otherwise just store it.
176
+ if new_path.move_distance <= movement_points
177
+ if old_path = open_paths[testing_tile]
178
+ if new_path.move_distance < old_path.move_distance
179
+ open_paths[testing_tile] = new_path
180
+ end
181
+ else
182
+ open_paths[testing_tile] = new_path
183
+ valid_tiles << testing_tile if testing_tile.empty?
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ valid_tiles
191
+ end
192
+
193
+ # A* path-finding.
194
+ def path_to(destination_tile)
195
+ return Paths::None.new if destination_tile == tile
196
+ return Paths::Inaccessible.new(destination_tile) unless destination_tile.passable?(self)
197
+
198
+ closed_tiles = Set.new # Tiles we've already dealt with.
199
+ open_paths = { tile => Paths::Start.new(tile, destination_tile) } # Paths to check { tile => path_to_tile }.
200
+
201
+ while open_paths.any?
202
+ # Check the (expected) shortest path and move it to closed, since we have considered it.
203
+ path = open_paths.each_value.min_by(&:cost)
204
+ current_tile = path.last
205
+
206
+ return path if current_tile == destination_tile
207
+
208
+ open_paths.delete current_tile
209
+ closed_tiles << current_tile
210
+
211
+ next if path.is_a? Paths::Melee
212
+
213
+ # Check adjacent tiles.
214
+ exits = current_tile.exits(self).reject {|wall| closed_tiles.include? wall.destination(current_tile) }
215
+ exits.each do |wall|
216
+ testing_tile = wall.destination(current_tile)
217
+
218
+ new_path = nil
219
+
220
+ object = testing_tile.object
221
+ if object and object.is_a?(Objects::Entity) and enemy?(object)
222
+ # 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
224
+ new_path = Paths::Melee.new(path, testing_tile)
225
+ else
226
+ next
227
+ end
228
+ elsif testing_tile.passable?(self)
229
+ if (object.nil? or object.passable?(self))
230
+ new_path = Paths::Move.new(path, testing_tile, wall.movement_cost)
231
+ else
232
+ next
233
+ end
234
+ end
235
+
236
+ # 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]
238
+ if new_path.move_distance < old_path.move_distance
239
+ open_paths[testing_tile] = new_path
240
+ end
241
+ else
242
+ open_paths[testing_tile] = new_path
243
+ end
244
+ end
245
+ end
246
+
247
+ Paths::Inaccessible.new(destination_tile) # Failed to connect at all.
248
+ end
249
+
250
+ # Actually perform movement (called from GameAction::Move).
251
+ def move(tiles, movement_cost)
252
+ raise "Not enough movement points (#{self} tried to move #{movement_cost} with #{@movement_points} left #{tiles} )" unless movement_cost <= @movement_points
253
+
254
+ tiles = tiles.map {|pos| @map.tile_at_grid *pos } unless tiles.first.is_a? Tile
255
+
256
+ destination = tiles.last
257
+ @movement_points -= movement_cost
258
+
259
+ change_in_x = destination.x - tiles[-2].x
260
+
261
+ # Turn based on movement.
262
+ self.factor_x = change_in_x > 0 ? 1 : -1
263
+
264
+ self.tile = destination
265
+ end
266
+
267
+ # 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)
270
+ enemies.each do |enemy|
271
+ map.actions.do :ability, :melee, enemy, self # Only get one opportunity attack per enemy entering.
272
+ end
273
+ end
274
+
275
+ #
276
+ def line_of_sight?(tile)
277
+ !line_of_sight_blocked_by(tile)
278
+ end
279
+
280
+ # Returns the tile that blocks sight, otherwise nil.
281
+ # Implements 'Bresenham's line algorithm'
282
+ def line_of_sight_blocked_by(target_tile)
283
+ start_tile = tile
284
+
285
+ # Check for the special case of looking diagonally.
286
+ x1, y1 = tile.grid_x, tile.grid_y
287
+ x2, y2 = target_tile.grid_x, target_tile.grid_y
288
+
289
+ step_x = x1 < x2 ? 1 : -1
290
+ step_y = y1 < y2 ? 1 : -1
291
+ dx, dy = (x2 - x1).abs, (y2 - y1).abs
292
+
293
+ if dx == dy
294
+ # Special case of the diagonal line.
295
+ (dx - 1).times do
296
+ x1 += step_x
297
+ y1 += step_y
298
+
299
+ # If the centre tile is blocked, then we don't work.
300
+ tile = @map.tile_at_grid(x1, y1)
301
+ if tile.blocks_sight?
302
+ #Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::RED
303
+ return tile
304
+ else
305
+ #Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::BLUE
306
+ end
307
+ end
308
+ else
309
+ # General case, ray-trace.
310
+ error = dx - dy
311
+
312
+ # Ensure that all tiles are visited that the sight-line passes over,
313
+ # not just those that create a "drawn" line.
314
+ dx *= 2
315
+ dy *= 2
316
+
317
+ length = ((dx + dy + 1) / 2)
318
+
319
+ (length - 1).times do
320
+ # Note that this ignores the special case of error == 0
321
+ if error > 0
322
+ error -= dy
323
+ x1 += step_x
324
+ else
325
+ error += dx
326
+ y1 += step_y
327
+ end
328
+
329
+ tile = @map.tile_at_grid(x1, y1)
330
+ if tile.blocks_sight?
331
+ #Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::RED
332
+ return tile
333
+ else
334
+ #Tile.blank.draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, Color::BLUE
335
+ end
336
+ end
337
+ end
338
+
339
+ nil # Didn't hit anything.
340
+ end
341
+
342
+ def to_json(*a)
343
+ data = {
344
+ class: CLASS,
345
+ type: @type,
346
+ id: id,
347
+ health: @health,
348
+ movement_points: @movement_points,
349
+ action_points: @action_points,
350
+ facing: factor_x > 0 ? :right : :left,
351
+ }
352
+
353
+ data[:tile] = grid_position if @tile
354
+
355
+ data.to_json(*a)
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,27 @@
1
+ module SmashAndGrab
2
+ module Objects
3
+ class FloatingText < GameObject
4
+ FONT_NAME = File.expand_path("media/fonts/UnmaskedBB.ttf", EXTRACT_PATH)
5
+ FONT_SIZE = 16
6
+
7
+ def initialize(text, options = {})
8
+ super(options)
9
+
10
+ @final_y = y - 60
11
+ @text = text
12
+ @font = Font[FONT_NAME, FONT_SIZE]
13
+
14
+ parent.map.add_effect self
15
+ end
16
+
17
+ def update
18
+ self.y -= 1 # TODO: scale this with FPS.
19
+ parent.map.remove_effect(self) if y < @final_y
20
+ end
21
+
22
+ def draw
23
+ @font.draw_rel @text, x, y, zorder, 0.5, 0.5, 1, 1, color
24
+ end
25
+ end
26
+ end
27
+ end