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
@@ -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