smash_and_grab 0.0.5alpha → 0.0.6alpha

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 (67) hide show
  1. data/{CHANGELOG.txt → CHANGELOG.md} +0 -0
  2. data/Gemfile.lock +8 -4
  3. data/LICENSE.txt +20 -0
  4. data/README.md +30 -14
  5. data/Rakefile +2 -2
  6. data/config/lang/objects/entities/en.yml +134 -0
  7. data/config/lang/objects/static/en.yml +8 -0
  8. data/config/lang/objects/vehicles/en.yml +11 -0
  9. data/config/map/entities.yml +42 -38
  10. data/lib/smash_and_grab.rb +5 -0
  11. data/lib/smash_and_grab/abilities.rb +1 -1
  12. data/lib/smash_and_grab/abilities/ability.rb +45 -3
  13. data/lib/smash_and_grab/abilities/drop.rb +38 -0
  14. data/lib/smash_and_grab/abilities/melee.rb +4 -6
  15. data/lib/smash_and_grab/abilities/pick_up.rb +33 -0
  16. data/lib/smash_and_grab/abilities/ranged.rb +18 -12
  17. data/lib/smash_and_grab/abilities/sprint.rb +11 -7
  18. data/lib/smash_and_grab/chingu_ext/basic_game_object.rb +26 -0
  19. data/lib/smash_and_grab/game_window.rb +5 -1
  20. data/lib/smash_and_grab/gui/entity_panel.rb +26 -10
  21. data/lib/smash_and_grab/gui/entity_summary.rb +19 -11
  22. data/lib/smash_and_grab/gui/game_log.rb +6 -2
  23. data/lib/smash_and_grab/gui/info_panel.rb +7 -4
  24. data/lib/smash_and_grab/gui/object_panel.rb +6 -2
  25. data/lib/smash_and_grab/gui/scenario_panel.rb +4 -0
  26. data/lib/smash_and_grab/history/action_history.rb +1 -1
  27. data/lib/smash_and_grab/main.rb +7 -3
  28. data/lib/smash_and_grab/map/faction.rb +26 -8
  29. data/lib/smash_and_grab/map/map.rb +21 -12
  30. data/lib/smash_and_grab/map/tile.rb +29 -2
  31. data/lib/smash_and_grab/map/wall.rb +2 -2
  32. data/lib/smash_and_grab/mixins/has_contents.rb +38 -0
  33. data/lib/smash_and_grab/mixins/line_of_sight.rb +129 -0
  34. data/lib/smash_and_grab/mixins/pathfinding.rb +145 -0
  35. data/lib/smash_and_grab/mouse_selection.rb +107 -36
  36. data/lib/smash_and_grab/objects/entity.rb +311 -260
  37. data/lib/smash_and_grab/objects/floating_text.rb +0 -1
  38. data/lib/smash_and_grab/objects/static.rb +10 -3
  39. data/lib/smash_and_grab/objects/vehicle.rb +7 -2
  40. data/lib/smash_and_grab/objects/world_object.rb +10 -3
  41. data/lib/smash_and_grab/path.rb +38 -12
  42. data/lib/smash_and_grab/players/ai.rb +91 -0
  43. data/lib/smash_and_grab/players/human.rb +9 -0
  44. data/lib/smash_and_grab/players/player.rb +16 -65
  45. data/lib/smash_and_grab/players/remote.rb +9 -0
  46. data/lib/smash_and_grab/sprite_sheet.rb +9 -0
  47. data/lib/smash_and_grab/states/edit_level.rb +15 -4
  48. data/lib/smash_and_grab/states/main_menu.rb +16 -4
  49. data/lib/smash_and_grab/states/play_level.rb +88 -28
  50. data/lib/smash_and_grab/states/world.rb +17 -9
  51. data/lib/smash_and_grab/version.rb +1 -1
  52. data/lib/smash_and_grab/z_order.rb +6 -5
  53. data/media/images/path.png +0 -0
  54. data/media/images/tile_selection.png +0 -0
  55. data/media/images/tiles_selection.png +0 -0
  56. data/smash_and_grab.gemspec +2 -2
  57. data/tasks/create_portraits.rb +39 -0
  58. data/tasks/outline_images.rb +56 -0
  59. data/test/smash_and_grab/abilities/drop_test.rb +68 -0
  60. data/test/smash_and_grab/abilities/melee_test.rb +4 -4
  61. data/test/smash_and_grab/abilities/pick_up_test.rb +68 -0
  62. data/test/smash_and_grab/abilities/ranged_test.rb +105 -0
  63. data/test/smash_and_grab/abilities/sprint_test.rb +55 -25
  64. data/test/smash_and_grab/map/faction_test.rb +18 -16
  65. data/test/smash_and_grab/map/map_test.rb +9 -4
  66. metadata +51 -19
  67. data/lib/smash_and_grab/fidgit_ext/event.rb +0 -77
@@ -1,7 +1,6 @@
1
1
  module SmashAndGrab
2
2
  module Objects
3
3
  class FloatingText < GameObject
4
- FONT_NAME = File.expand_path("media/fonts/UnmaskedBB.ttf", EXTRACT_PATH)
5
4
  FONT_SIZE = 16
6
5
 
7
6
  def initialize(text, options = {})
@@ -12,14 +12,16 @@ 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.to_s.split("_").map(&:capitalize).join(" "); end
16
15
 
17
16
  class << self
18
17
  def config; @config ||= YAML.load_file(File.expand_path("config/map/objects.yml", EXTRACT_PATH)); end
19
18
  def types; config.keys; end
20
- def sprites; @sprites ||= SpriteSheet.new("objects.png", 64 + 2, 64 + 2, 8); end
19
+ def sprites; @sprites ||= SpriteSheet["objects.png", 64 + 2, 64 + 2, 8]; end
21
20
  end
22
21
 
22
+ # TODO: configure this value.
23
+ def pick_up?(entity); @passable; end
24
+
23
25
  def initialize(map, data)
24
26
  @type = data[:type]
25
27
  config = self.class.config[@type]
@@ -41,9 +43,14 @@ class Static < WorldObject
41
43
  :class => CLASS,
42
44
  type: @type,
43
45
  id: id,
44
- tile: grid_position,
46
+ tile: tile ? grid_position : nil,
45
47
  }.to_json(*a)
46
48
  end
49
+
50
+ def draw
51
+ # Without a tile, it has probably been picked up.
52
+ super if tile
53
+ end
47
54
  end
48
55
  end
49
56
  end
@@ -21,12 +21,11 @@ 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.to_s.split("_").map(&:capitalize).join(" "); end
25
24
 
26
25
  class << self
27
26
  def config; @config ||= YAML.load_file(File.expand_path("config/map/vehicles.yml", EXTRACT_PATH)); end
28
27
  def types; config.keys; end
29
- def sprites; @sprites ||= SpriteSheet.new("vehicles.png", (128 * 2) + 2, (128 * 2) + 2, 3); end
28
+ def sprites; @sprites ||= SpriteSheet["vehicles.png", (128 * 2) + 2, (128 * 2) + 2, 3]; end
30
29
  end
31
30
 
32
31
  def fills_tile_on_minimap?; true; end
@@ -80,6 +79,12 @@ class Vehicle < WorldObject
80
79
  end
81
80
  end
82
81
 
82
+ def draw_base
83
+ tiles do |tile|
84
+ Image["tiles_selection.png"].draw_rot tile.x, tile.y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, base_color
85
+ end
86
+ end
87
+
83
88
  def draw
84
89
  # Draw the image in sections, since it has to exist at several zorder positions in order to render correctly.
85
90
  DRAW_POSITIONS.each do |clip_width, offset_z|
@@ -20,6 +20,11 @@ class WorldObject < GameObject
20
20
  def fills_tile_on_minimap?; false; end
21
21
  def casts_shadow?; true; end
22
22
 
23
+ def t; R18n.get.t[Inflector.demodulize self.class.name][type]; end
24
+ def name; t.name; end
25
+ def colorized_name; name; end
26
+ def base_color; Color::BLUE; end
27
+
23
28
  OUTLINE_SCALE = Image::THIN_OUTLINE_SCALE
24
29
 
25
30
  def initialize(map, data, options = {})
@@ -88,14 +93,16 @@ class WorldObject < GameObject
88
93
  @image.draw_rot @x, @y + 2.5 - @z, @y, 0, 0.5, 1, OUTLINE_SCALE * @factor_x, OUTLINE_SCALE, color
89
94
  end
90
95
 
96
+ def draw_base
97
+ Image["tile_selection.png"].draw_rot x, y, ZOrder::TILE_SELECTION, 0, 0.5, 0.5, 1, 1, base_color
98
+ end
99
+
91
100
  def busy?; false; end
92
101
  def active?; false; end
93
102
 
94
103
  def destroy
95
- self.tile = nil
96
104
  map.remove self
97
-
98
- publish :changed
105
+ self.tile = nil
99
106
 
100
107
  super
101
108
  end
@@ -7,7 +7,7 @@ class Path
7
7
  TILE_SIZE = 16
8
8
 
9
9
  class << self
10
- def sprites; @sprites ||= SpriteSheet.new("path.png", 32, 16, 4); end
10
+ def sprites; @sprites ||= SpriteSheet["path.png", 32, 16, 4]; end
11
11
  end
12
12
 
13
13
  attr_reader :cost, :move_distance, :previous_path, :destination_distance, :first, :last
@@ -16,13 +16,13 @@ class Path
16
16
  def tiles; @previous_path.tiles + [@last]; end
17
17
  def sprites; self.class.sprites; end
18
18
 
19
- def initialize(previous_path, next_tile, extra_move_distance)
19
+ def initialize(previous_path, next_tile, extra_move_distance, disincentive)
20
20
  @previous_path = previous_path
21
21
  @first, @last = @previous_path.first, next_tile
22
22
 
23
23
  @move_distance = @previous_path.move_distance + extra_move_distance
24
24
  @destination_distance = @previous_path.destination_distance
25
- @cost = @move_distance + @destination_distance
25
+ @cost = @move_distance + @destination_distance + disincentive
26
26
  end
27
27
 
28
28
  # @option from [Tile] Tile to start drawing the path from.
@@ -64,7 +64,7 @@ class Path
64
64
  end
65
65
 
66
66
  color = if tile == first or tiles_within_range.include?(tile)
67
- Color::GREEN
67
+ Color.rgb(50, 50, 255)
68
68
  else
69
69
  Color::BLACK
70
70
  end
@@ -74,16 +74,20 @@ class Path
74
74
  end
75
75
  end
76
76
 
77
- def draw
77
+ def draw(move_points)
78
78
  @record.draw 0, 0, ZOrder::PATH
79
+
80
+ if last.empty? and move_distance > 0
81
+ Font[FONT_NAME, 8].draw_rel move_points - move_distance, last.x, last.y, ZOrder::BEHIND_GUI, 0.5, 0.5
82
+ end
79
83
  end
80
84
  end
81
85
 
82
86
  # A path consisting just of movement.
83
87
  class Move < Path
84
88
  def mover; first.object; end
85
- def initialize(previous_path, last, extra_move_distance)
86
- super(previous_path, last, last.movement_cost + extra_move_distance)
89
+ def initialize(previous_path, last, extra_move_distance, disincentive)
90
+ super(previous_path, last, last.movement_cost + extra_move_distance, disincentive)
87
91
  end
88
92
  end
89
93
 
@@ -92,11 +96,10 @@ class Melee < Path
92
96
  COLOR_IN_RANGE = Color::WHITE
93
97
  COLOR_OUT_OF_RANGE = Color.rgb(100, 100, 100)
94
98
 
95
- def attacker; previous_path.last.object; end
96
99
  def defender; last.object; end
97
100
  def requires_movement?; previous_path.is_a? Paths::Move; end
98
101
  def initialize(previous_path, last)
99
- super(previous_path, last, 0)
102
+ super(previous_path, last, 0, 0)
100
103
  end
101
104
 
102
105
  def prepare_for_drawing(tiles_within_range, options = {})
@@ -106,10 +109,29 @@ class Melee < Path
106
109
 
107
110
  def draw(*args)
108
111
  super(*args)
112
+ sprites[3, 1].draw_rot last.x, last.y, ZOrder::PATH, 0, 0.5, 0.5, 1, 1, @draw_color
113
+ end
114
+ end
109
115
 
110
- if last.object.is_a? Objects::Entity
111
- sprites[3, 1].draw_rot last.x, last.y, ZOrder::PATH, 0, 0.5, 0.5, 1, 1, @draw_color
112
- end
116
+ # A path consisting of melee, possibly with some movement beforehand.
117
+ class PickUp < Path
118
+ COLOR_IN_RANGE = Color::WHITE
119
+ COLOR_OUT_OF_RANGE = Color.rgb(100, 100, 100)
120
+
121
+ def object; last.object; end
122
+ def requires_movement?; previous_path.is_a? Paths::Move; end
123
+ def initialize(previous_path, last)
124
+ super(previous_path, last, 0, 0)
125
+ end
126
+
127
+ def prepare_for_drawing(tiles_within_range, options = {})
128
+ super(tiles_within_range)
129
+ @draw_color = tiles_within_range.include?(last) ? COLOR_IN_RANGE : COLOR_OUT_OF_RANGE
130
+ end
131
+
132
+ def draw(*args)
133
+ super(*args)
134
+ sprites[0, 5].draw_rot last.x, last.y, ZOrder::PATH, 0, 0.5, 0.5, 1, 1, @draw_color
113
135
  end
114
136
  end
115
137
 
@@ -127,6 +149,8 @@ end
127
149
 
128
150
  # Path where the destination is unreachable.
129
151
  class Inaccessible < Path
152
+ def cost; 0; end
153
+ def move_distance; Float::INFINITY; end
130
154
  def accessible?; false; end
131
155
  def tiles; [@last]; end
132
156
 
@@ -143,6 +167,8 @@ end
143
167
 
144
168
  # Path going to the same location as it started.
145
169
  class None < Path
170
+ def cost; 0; end
171
+ def move_distance; 0; end
146
172
  def accessible?; false; end
147
173
  def tiles; []; end
148
174
  def initialize; end
@@ -0,0 +1,91 @@
1
+ require_relative "player"
2
+
3
+ module SmashAndGrab
4
+ module Players
5
+ # Local AI.
6
+ class AI < Player
7
+ def faction=(faction)
8
+ super faction
9
+ faction.subscribe :turn_started do
10
+ @active_entities = faction.entities.find_all(&:alive?)
11
+ end
12
+ faction
13
+ end
14
+
15
+ def update
16
+ return if faction.map.busy?
17
+
18
+ if @active_entities.empty?
19
+ faction.end_turn
20
+ else
21
+ # Attempt to attack, else move, else stand around like a loon.
22
+ entity = @active_entities.first
23
+ if entity.alive?
24
+ # Try ranged, then charge into melee, then pick up, then move.
25
+ ranged = entity.potential_ranged.map(&:object).compact.find_all do |object|
26
+ object.is_a?(Objects::Entity) and entity.enemy?(object)
27
+ end
28
+
29
+ if ranged.any?
30
+ # Avoid bystanders if there are better opponents.
31
+ unless ranged.all? {|a| a.bystander? }
32
+ ranged.delete_if {|a| a.bystander? }
33
+ end
34
+
35
+ entity.use_ability :ranged, ranged.sample
36
+ # Try melee or moving next time.
37
+ else
38
+ moves, actions = entity.potential_moves.partition {|t| t.empty? }
39
+ attacks, pick_ups = actions.partition {|a| a.object.is_a? Objects::Entity }
40
+
41
+ if attacks.any?
42
+ # Avoid bystanders if there are better opponents.
43
+ unless attacks.all? {|a| a.object.bystander? }
44
+ attacks.delete_if {|a| a.object.bystander? }
45
+ end
46
+
47
+ # TODO: Pick the nearest and most dangerous/weakest attack and consider re-attacking.
48
+ path = entity.path_to(attacks.sample)
49
+ entity.use_ability :move, path.previous_path if path.requires_movement?
50
+
51
+ # Only perform melee if you weren't killed by attacks of opportunity.
52
+ target = path.last.object
53
+ entity.add_activity do
54
+ if entity.alive?
55
+ entity.use_ability :melee, target
56
+ else
57
+ @active_entities.shift
58
+ end
59
+ end
60
+
61
+ elsif pick_ups.any?
62
+ # TODO: Pick the nearest object or most valuable?
63
+ path = entity.path_to(pick_ups.sample)
64
+ entity.use_ability :move, path.previous_path if path.requires_movement?
65
+
66
+ # Only pick up if you weren't killed by attacks of opportunity.
67
+ target = path.last.object
68
+ entity.add_activity do
69
+ if entity.alive?
70
+ entity.use_ability :pick_up, target
71
+ else
72
+ @active_entities.shift
73
+ end
74
+ end
75
+
76
+ elsif moves.any?
77
+ # TODO: Wait with moves until everyone who can has attacked?
78
+ entity.use_ability :move, entity.path_to(moves.sample)
79
+ @active_entities.shift
80
+ else
81
+ # Can't do anything at all :(
82
+ # TODO: Maybe wait until other people have tried to move?
83
+ @active_entities.shift
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "player"
2
+
3
+ module SmashAndGrab
4
+ module Players
5
+ # Local human player.
6
+ class Human < Player
7
+ end
8
+ end
9
+ end
@@ -1,76 +1,27 @@
1
1
  module SmashAndGrab
2
- module Players
3
- class Player
4
- attr_reader :faction
2
+ module Players
3
+ class Player
4
+ attr_reader :faction
5
5
 
6
- def initialize
7
- @faction = nil
8
- end
9
-
10
- def faction=(faction)
11
- @faction = faction
12
-
13
- @faction.player = self
14
-
15
- @faction.subscribe :turn_started do |faction, entities|
16
- @active_entities = entities.select(&:alive?).shuffle
17
- end
18
-
19
- @faction.subscribe :turn_ended do
20
- @active_entities = nil
21
- end
22
- end
23
-
24
- def update; end
25
- end
26
-
27
- # Local human player.
28
- class Human < Player
29
- end
30
-
31
- # Remote human or AI.
32
- class Remote < Player
33
- end
6
+ def human?; self.is_a? Human; end
7
+ def ai?; self.is_a? AI; end
8
+ def remote?; self.is_a? Remote; end
34
9
 
35
- # Local AI.
36
- class AI < Player
37
- def update
38
- return if faction.map.busy?
39
-
40
- if @active_entities.empty?
41
- faction.end_turn
42
- else
43
- # Attempt to attack, else move, else stand around like a loon.
44
- entity = @active_entities.first
45
- if entity.alive?
46
- moves, attacks = entity.potential_moves.partition {|t| t.empty? }
10
+ def initialize
11
+ @faction = nil
12
+ end
47
13
 
48
- if attacks.any?
49
- # TODO: Pick the nearest attack and consider re-attacking.
50
- path = entity.path_to(attacks.sample)
51
- entity.use_ability :move, path.previous_path if path.requires_movement?
52
- # Only perform melee if you weren't killed by attacks of opportunity.
53
- target = path.last.object
54
- entity.add_activity do
55
- entity.use_ability :melee, target if entity.alive?
56
- end
14
+ def faction=(faction)
15
+ @faction = faction
57
16
 
58
- entity.add_activity do
59
- @active_entities.shift unless entity.use_ability?(:melee)
60
- end
17
+ @faction.player = self
61
18
 
62
- elsif moves.any?
63
- # TODO: Wait with moves until everyone who can has attacked?
64
- entity.use_ability :move, entity.path_to(moves.sample)
65
- @active_entities.shift
66
- else
67
- # Can't do anything at all :(
68
- # TODO: Maybe wait until other people have tried to move?
69
- @active_entities.shift
19
+ @faction.subscribe :turn_ended do
20
+ # Do nothing.
70
21
  end
71
22
  end
23
+
24
+ def update; end
72
25
  end
73
26
  end
74
- end
75
- end
76
27
  end
@@ -0,0 +1,9 @@
1
+ require_relative "player"
2
+
3
+ module SmashAndGrab
4
+ module Players
5
+ # Remote human or AI.
6
+ class Remote < Player
7
+ end
8
+ end
9
+ end
@@ -4,6 +4,15 @@ class SpriteSheet
4
4
 
5
5
  def_delegators :@sprites, :map, :each
6
6
 
7
+ class << self
8
+ def [](file, width, height, tiles_wide = 0)
9
+ @cached_sheets ||= Hash.new do |h, k|
10
+ h[k] = new(*k)
11
+ end
12
+ @cached_sheets[[file, width, height, tiles_wide]]
13
+ end
14
+ end
15
+
7
16
  def initialize(file, width, height, tiles_wide = 0)
8
17
  @sprites = Image.load_tiles($window, File.expand_path(file, Image.autoload_dirs[0]), width, height, false)
9
18
  @tiles_wide = tiles_wide