smash_and_grab 0.0.5alpha → 0.0.6alpha

Sign up to get free protection for your applications and to get access to all the features.
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