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
@@ -4,6 +4,7 @@ module SmashAndGrab
4
4
  module States
5
5
  class EditLevel < World
6
6
  PLACEMENT_COLOR = Color.rgba(255, 255, 255, 190)
7
+ FACTIONS = [:baddies, :goodies, :bystanders]
7
8
 
8
9
  def initialize(file)
9
10
  super()
@@ -13,13 +14,23 @@ class EditLevel < World
13
14
 
14
15
  @selected_wall = nil
15
16
 
16
- load_game file
17
+ factions = FACTIONS.map do |f|
18
+ Factions.const_get(f.capitalize).new
19
+ end
20
+
21
+ load_game file, factions
17
22
 
18
23
  on_input :right_mouse_button do
19
24
  @selector.pick_up(@hover_tile, @hover_wall)
20
25
  end
21
26
  end
22
27
 
28
+ def assign_entities_to_factions
29
+ map.world_objects.grep(Objects::Entity).each do |o|
30
+ o.faction = map.factions[FACTIONS.index o.default_faction_type]
31
+ end
32
+ end
33
+
23
34
  def create_gui
24
35
  @container = Fidgit::Container.new do |container|
25
36
  @minimap = Gui::Minimap.new parent: container
@@ -58,8 +69,8 @@ class EditLevel < World
58
69
  load_game @editing_file
59
70
  end
60
71
 
61
- def load_game(file)
62
- super
72
+ def load_game(file, factions)
73
+ super file, factions, start: false
63
74
  @editing_file = file
64
75
  @actions = EditorActionHistory.new
65
76
  end
@@ -194,4 +205,4 @@ class EditLevel < World
194
205
  end
195
206
  end
196
207
  end
197
- end
208
+ end
@@ -11,7 +11,7 @@ class MainMenu < Fidgit::GuiState
11
11
 
12
12
  @container.background_color = Color.rgb(0, 0, 25)
13
13
 
14
- vertical align: :center do
14
+ vertical align: :center, padding: 0, spacing: 0 do
15
15
  horizontal align: :center do
16
16
  image_frame Objects::Static.sprites[1, 0], factor: 3
17
17
 
@@ -28,9 +28,21 @@ class MainMenu < Fidgit::GuiState
28
28
  image_frame Objects::Entity.sprites[3, 1], factor: 4
29
29
 
30
30
  vertical align: :center do
31
- options = { width: 120, justify: :center }
32
- button "Play", options do
33
- push_game_state States::PlayLevel.new(GAME_FILE)
31
+ options = { width: 200, justify: :center }
32
+ button "Single Player", options do
33
+ push_game_state States::PlayLevel.new(GAME_FILE, Players::Human.new, Players::AI.new)
34
+ end
35
+
36
+ button "Co-op", options do
37
+ push_game_state States::PlayLevel.new(GAME_FILE, [Players::Human.new, Players::Human.new], Players::AI.new)
38
+ end
39
+
40
+ button "Hot-seat", options do
41
+ push_game_state States::PlayLevel.new(GAME_FILE, Players::Human.new, Players::Human.new)
42
+ end
43
+
44
+ button "AI vs AI", options do
45
+ push_game_state States::PlayLevel.new(GAME_FILE, Players::AI.new, Players::AI.new)
34
46
  end
35
47
 
36
48
  button "Edit", options do
@@ -5,63 +5,116 @@ module States
5
5
  class PlayLevel < World
6
6
  include Fidgit::Event
7
7
 
8
- attr_reader :info_panel
8
+ attr_reader :info_panel, :cursor_world_x, :cursor_world_y
9
9
 
10
10
  event :game_info
11
11
  event :game_heading
12
12
 
13
- def initialize(file)
13
+ # Players is villains
14
+ def initialize(file, baddies_players, goodies_players)
14
15
  super()
15
16
 
16
17
  add_inputs(space: :end_turn)
17
18
 
18
- @players = [Players::Human.new, Players::AI.new, Players::AI.new]
19
+ @players = {
20
+ baddies: Array(baddies_players),
21
+ goodies: Array(goodies_players),
22
+ bystanders: [Players::AI.new],
23
+ }
19
24
 
20
25
  @quicksaved = false
26
+ @cursor_world_x = @cursor_world_x = nil
21
27
 
22
- load_game file
28
+ factions = []
23
29
 
24
- @players.each.with_index do |player, i|
25
- map.factions[i].player = player
26
- player.faction = map.factions[i]
27
- player.faction.subscribe :turn_started do
28
- publish :game_heading, "=== Turn #{map.turn + 1} ===" if player == @players.first
29
- publish :game_info, ""
30
- publish :game_heading, "#{player.faction}' turn (#{Inflector.demodulize player.class})"
31
- publish :game_info, ""
30
+ @players.each_pair do |faction_name, players|
31
+ players.each do |player|
32
+ new_faction = Factions.const_get(faction_name.capitalize).new
33
+ factions << new_faction
34
+ player.faction = new_faction
32
35
  end
33
36
  end
34
37
 
38
+ load_game file, factions
39
+
40
+ # If loading a level, start the first turn, otherwise just keep going.
41
+ name = File.basename(file).chomp(File.extname(file))
42
+ if File.extname(file) == ".sgl"
43
+ publish :game_heading, "=== Started #{name} ==="
44
+ publish :game_info, ""
45
+ map.factions.first.start_turn
46
+ else
47
+ faction = map.active_faction
48
+ publish :game_heading, "=== Resuming #{name} in turn #{map.turn + 1} ==="
49
+ publish :game_info, ""
50
+ publish :game_heading,faction.class::TEXT_COLOR.colorize("#{faction.name}' turn (#{Inflector.demodulize faction.player.class})")
51
+ publish :game_info, ""
52
+ end
53
+
35
54
  save_game_as AUTOSAVE_FILE
36
55
 
37
56
  @mouse_selection = MouseSelection.new @map
38
57
  end
39
58
 
59
+ def assign_entities_to_factions
60
+ factions_by_type = map.factions.group_by {|f| Inflector.demodulize(f.class.name).downcase.to_sym }
61
+ factions_by_type.each_pair do |faction_name, factions|
62
+ entities = map.world_objects.grep(Objects::Entity).find_all {|o| o.default_faction_type == faction_name }
63
+ # Split them up as evenly as possible if more than one player is controlling them.
64
+ entities_per_faction = entities.size.fdiv(factions.size).ceil
65
+ entities.each_slice(entities_per_faction).with_index do |entities, i|
66
+ entities.each {|e| e.faction = factions[i] }
67
+ end
68
+ end
69
+ end
70
+
40
71
  def create_gui
41
72
  @container = Fidgit::Container.new do |container|
42
73
  @minimap = Gui::Minimap.new parent: container
43
74
 
44
- # Unit roster.
45
- @summary_bar = vertical parent: container, padding: 4, spacing: 4, background_color: Color::BLACK do |packer|
46
- [@map.baddies.size, 8].min.times do |i|
47
- baddy = @map.baddies[i]
48
- summary = Gui::EntitySummary.new baddy, parent: packer
49
- summary.subscribe :left_mouse_button do
50
- @mouse_selection.selected = baddy if baddy.alive?
51
- @info_panel.object = baddy
75
+ # Unit roster for each human-controlled faction.
76
+ @summaries_lists = {}
77
+
78
+ # Create a summary list for each human-controlled faction.
79
+ @map.factions.each do |faction|
80
+ if faction.player.is_a? Players::Human
81
+ @summaries_lists[faction] = Fidgit::Vertical.new padding: 4, spacing: 4 do |packer|
82
+ # Put each entity into the list.
83
+ faction.entities.each do |entity|
84
+ summary = Gui::EntitySummary.new entity, parent: packer
85
+ summary.subscribe :left_mouse_button do
86
+ @mouse_selection.selected = entity if entity.alive?
87
+ @info_panel.object = entity
88
+ end
89
+ end
52
90
  end
91
+
92
+ # At the start of the turn, change in our summary list.
93
+ # Means the last human player's list will be shown during the AI turns.
94
+ faction.subscribe :turn_started do
95
+ @summary_bar.clear
96
+ @summary_bar.add @summaries_lists[faction]
97
+ end
98
+
99
+ # TODO: Add and remove entities to the lists as unit list changes.
100
+ #faction.subscribe :entity_added do
101
+ #end
102
+ #faction.subscribe :entity_removed do
103
+ #end
53
104
  end
54
105
  end
55
106
 
107
+ # Will contain the summary lists.
108
+ @summary_bar = vertical parent: container, padding: 0, spacing: 0, background_color: Color::BLACK
109
+
56
110
  # Info panel.
57
111
  @info_panel = Gui::InfoPanel.new self, parent: container
58
- @info_panel.object = @map.baddies[0]
59
112
 
60
113
  # Button box.
61
114
  @button_box = vertical parent: container, padding: 4, spacing: 8, width: 150, background_color: Color::BLACK do
62
115
  @turn_label = label " ", font_height: 14
63
116
 
64
- button "End turn" do
117
+ @end_turn_button = button "End turn" do
65
118
  end_turn
66
119
  end
67
120
 
@@ -96,7 +149,7 @@ class PlayLevel < World
96
149
  selection = @mouse_selection.selected
97
150
  @mouse_selection.selected = nil
98
151
  @map.actions.redo if @map.actions.can_redo?
99
- @mouse_selection.selected =sd selection if selection
152
+ @mouse_selection.selected = selection if selection
100
153
  end
101
154
 
102
155
  def map=(map)
@@ -125,9 +178,13 @@ class PlayLevel < World
125
178
  $window.mouse_y >= 0 and $window.mouse_y < $window.height and
126
179
  @container.each.none? {|e| e.hit? $window.mouse_x, $window.mouse_y }
127
180
 
128
- @map.tile_at_position((@camera_offset_x + $window.mouse_x) / @zoom,
129
- (@camera_offset_y + $window.mouse_y) / @zoom)
181
+ @cursor_world_x = (@camera_offset_x + $window.mouse_x) / @zoom
182
+ @cursor_world_y = (@camera_offset_y + $window.mouse_y) / @zoom
183
+
184
+ @map.tile_at_position @cursor_world_x, @cursor_world_y
130
185
  else
186
+ @cursor_world_x = @cursor_world_y = nil
187
+
131
188
  nil
132
189
  end
133
190
 
@@ -139,17 +196,20 @@ class PlayLevel < World
139
196
 
140
197
  @turn_label.text = "Turn: #{@map.turn + 1} (#{@map.active_faction})"
141
198
 
142
- @undo_button.enabled = @map.actions.can_undo?
143
- @redo_button.enabled = @map.actions.can_redo?
199
+ usable = @map.active_faction.player.human? && !@map.busy?
200
+ @end_turn_button.enabled = usable
201
+ @undo_button.enabled = usable && @map.actions.can_undo?
202
+ @redo_button.enabled = usable && @map.actions.can_redo?
144
203
  end
145
204
 
146
205
  def quickload
147
206
  if @quicksaved
148
- switch_game_state self.class.new(QUICKSAVE_FILE)
207
+ switch_game_state self.class.new(QUICKSAVE_FILE, @players[:baddies], @players[:goodies])
149
208
  end
150
209
  end
151
210
 
152
211
  def quicksave
212
+ publish :game_heading, "<<< Quick-saved >>>"
153
213
  save_game_as QUICKSAVE_FILE
154
214
  @quicksaved = true
155
215
  end
@@ -18,14 +18,9 @@ class World < Fidgit::GuiState
18
18
  QUICKSAVE_FILE = File.expand_path("quicksave.sgs", SAVE_PATH)
19
19
  AUTOSAVE_FILE = File.expand_path("autosave.sgs", SAVE_PATH)
20
20
 
21
- def map=(map)
22
- @map = map
23
- @map.record_grid GRID_COLOR
24
-
25
- @camera_offset_x, @camera_offset_y = [0, -@map.to_rect.center_y]
26
-
27
- create_gui
21
+ def create_gui; raise NotImplementedError; end
28
22
 
23
+ def create_minimap
29
24
  @minimap.map = @map
30
25
 
31
26
  @map.subscribe :tile_contents_changed do |map, tile|
@@ -94,7 +89,7 @@ class World < Fidgit::GuiState
94
89
  File.open("#{file}.json", "w") {|f| f.write json } # DEBUG ONLY!
95
90
  end
96
91
 
97
- def load_game(file)
92
+ def load_game(file, factions, options = {})
98
93
  t = Time.now
99
94
 
100
95
  json = Zlib::GzipReader.open(file) do |gz|
@@ -103,11 +98,24 @@ class World < Fidgit::GuiState
103
98
 
104
99
  data = JSON.parse(json).symbolize
105
100
 
106
- self.map = Map.new data
101
+ @map = Map.new data, factions, options
102
+ @map.record_grid GRID_COLOR
103
+
104
+ @camera_offset_x, @camera_offset_y = [0, -@map.to_rect.center_y]
105
+
106
+ assign_entities_to_factions
107
+
108
+ # Make sure that every entity has picked up the object they had before.
109
+ map.factions.each {|f| f.entities.each {|e| e.setup_contents } }
110
+
111
+ create_gui
112
+ create_minimap
107
113
 
108
114
  log.info { "Loaded game from #{file} [#{File.size(file)} bytes] in #{"%.3f" % (Time.now - t) }s" }
109
115
  end
110
116
 
117
+ def assign_entities_to_factions; raise NotImplementedError; end
118
+
111
119
  def autosave
112
120
  save_game_as AUTOSAVE_FILE
113
121
  end
@@ -1,3 +1,3 @@
1
1
  module SmashAndGrab
2
- VERSION = "0.0.5alpha"
2
+ VERSION = "0.0.6alpha"
3
3
  end
@@ -1,12 +1,13 @@
1
1
  module SmashAndGrab
2
2
  class ZOrder
3
3
  BACKGROUND = -Float::INFINITY
4
- TILES = -99999
5
- SHADOWS = -99998
6
- TILE_SELECTION = -99997
7
- PATH = -99996
8
- # Objects -1000 .. +1000
4
+ TILES = -99_999
5
+ SHADOWS = -99_998
6
+ TILE_SELECTION = -99_997
7
+ PATH = -99_996
8
+ # Objects -1_000 .. +1_000
9
9
 
10
+ BEHIND_GUI = 1_000_000
10
11
  GUI = Float::INFINITY
11
12
  end
12
13
  end
Binary file
Binary file
Binary file
@@ -25,9 +25,9 @@ END
25
25
 
26
26
  s.add_runtime_dependency "gosu", "~> 0.7.41"
27
27
  s.add_runtime_dependency "chingu", "~> 0.9rc7"
28
- s.add_runtime_dependency "fidgit", "~> 0.1.10"
28
+ s.add_runtime_dependency "fidgit", "~> 0.2.1"
29
29
  s.add_runtime_dependency "texplay", "~> 0.3"
30
- #s.add_runtime_dependency "r18n-desktop", "~> 0.4.9"
30
+ s.add_runtime_dependency "r18n-desktop", "~> 0.4.14"
31
31
 
32
32
  s.add_development_dependency "releasy", "~> 0.2.2"
33
33
  s.add_development_dependency "rake", "~> 0.9.2.2"
@@ -0,0 +1,39 @@
1
+ PORTRAIT_IMAGES = [
2
+ # in, out, in-size, columns, out-rect.
3
+ ["entities.png", "entity_portraits.png", [66, 66], 8, [14, 10, 36, 36]],
4
+ ]
5
+
6
+ desc "Process images (cut out a portrait of each character)"
7
+ task create_portraits: :outline_images do
8
+ require 'texplay'
9
+ require_relative '../lib/texplay_ext/color'
10
+ require_relative '../lib/texplay_ext/image'
11
+ require_relative '../lib/texplay_ext/window'
12
+
13
+ puts "=== Creating portraits ===\n\n"
14
+
15
+ $window = Gosu::Window.new(100, 100, false)
16
+
17
+ PORTRAIT_IMAGES.each do |image_in_name, image_out_name, (width, height), num_columns, (rect_x, rect_y, rect_width, rect_height)|
18
+ puts "Making portraits from #{image_in_name}"
19
+
20
+ sprites = Gosu::Image.load_tiles($window, File.expand_path(image_in_name, MODIFIED_IMAGE_PATH), width, height, false)
21
+ sprites.each(&:refresh_cache)
22
+
23
+ new_image = Gosu::Image.create rect_width * num_columns,
24
+ rect_height * (sprites.size / num_columns)
25
+ new_image.refresh_cache
26
+
27
+ print "\n Splicing: "
28
+ sprites.each.with_index do |sprite, i|
29
+ row, column = i.divmod num_columns
30
+ new_image.splice sprite, column * rect_width, row * rect_height,
31
+ crop: [rect_x, rect_y, rect_x + rect_width, rect_y + rect_height]
32
+ print '.'
33
+ end
34
+
35
+ puts "\n\n"
36
+
37
+ new_image.save(File.expand_path(image_out_name, MODIFIED_IMAGE_PATH))
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ ORIGINAL_IMAGE_PATH = File.expand_path("../../raw_media/images", __FILE__)
2
+ MODIFIED_IMAGE_PATH = File.expand_path("../../media/images", __FILE__)
3
+
4
+ IMAGES = [
5
+ ["entities.png", [32, 32], 8],
6
+ ["objects.png", [32, 32], 8],
7
+ ["vehicles.png", [128, 128], 3],
8
+ ]
9
+
10
+ desc "Process images (double in size and add an outline)"
11
+ task :outline_images do
12
+ require 'texplay'
13
+ require_relative '../lib/texplay_ext/color'
14
+ require_relative '../lib/texplay_ext/image'
15
+ require_relative '../lib/texplay_ext/window'
16
+
17
+ puts "=== Processing images ===\n\n"
18
+
19
+ $window = Gosu::Window.new(100, 100, false)
20
+
21
+ IMAGES.each do |image_name, (width, height), num_columns|
22
+ puts "Processing #{image_name}"
23
+
24
+ sprites = Gosu::Image.load_tiles($window, File.expand_path(image_name, ORIGINAL_IMAGE_PATH), width, height, false)
25
+ sprites.each(&:refresh_cache)
26
+
27
+ print " Enlarging: "
28
+ large_sprites = sprites.map do |sprite|
29
+ print '.'
30
+ sprite.enlarge 2
31
+ end
32
+
33
+ print "\n Outlining: "
34
+ large_outlined = large_sprites.map do |sprite|
35
+ print '.'
36
+ sprite.outline
37
+ end
38
+
39
+ new_image = Gosu::Image.create large_outlined.first.width * num_columns,
40
+ large_outlined.first.height * large_outlined.size / num_columns
41
+ new_image.refresh_cache
42
+
43
+ print "\n Splicing: "
44
+ large_outlined.each.with_index do |sprite, i|
45
+ sprite.clear(dest_ignore: :alpha, color: Gosu::Color.rgb(50, 50, 50))
46
+ sprite.splice large_sprites[i], 1, 1, alpha_blend: true
47
+ row, column = i.divmod num_columns
48
+ new_image.splice sprite, column * sprite.width, row * sprite.height
49
+ print '.'
50
+ end
51
+
52
+ puts "\n\n"
53
+
54
+ new_image.save(File.expand_path(image_name, MODIFIED_IMAGE_PATH))
55
+ end
56
+ end