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
@@ -43,6 +43,10 @@ TEXT
43
43
  options[:dev] = true
44
44
  end
45
45
 
46
+ parser.on('--fullscreen', "Run fullscreen at native resolution") do
47
+ options[:fullscreen] = true
48
+ end
49
+
46
50
  parser.on('--console', 'Console mode (no log file)') do
47
51
  options[:log] = nil # Write to console.
48
52
  end
@@ -75,6 +79,7 @@ TEXT
75
79
  options[:log] = DEFAULT_LOG_FILE_PATH
76
80
  end
77
81
 
82
+ FULLSCREEN = options[:fullscreen]
78
83
  LOG_FILE = options[:log]
79
84
  DEVELOPMENT_MODE = options[:dev]
80
85
 
@@ -1,4 +1,4 @@
1
- require_folder "abilities", %w[area melee move ranged sprint]
1
+ require_folder "abilities", %w[area drop melee move pick_up ranged sprint]
2
2
 
3
3
  module SmashAndGrab::Abilities
4
4
  # Create an ability based on class name.
@@ -2,14 +2,20 @@ module SmashAndGrab::Abilities
2
2
  # @abstract
3
3
  class Ability
4
4
  attr_reader :skill, :owner
5
+ NON_SKILL = 0
5
6
  SKILL_LEVEL_DESCRIPTIONS = %w[Fair Good Excellent Heroic Legendary]
6
7
 
8
+ def use?; true; end
7
9
  def can_be_undone?; true; end
8
10
  def action_cost; @action_cost == :all ? owner.max_action_points : @action_cost; end
9
11
  def type; Inflector.underscore(Inflector.demodulize(self.class.name)).to_sym; end
10
12
 
11
13
  def tip
12
- "#{SKILL_LEVEL_DESCRIPTIONS[skill - 1]} #{type.capitalize} (#{'*' * skill})"
14
+ if skill > NON_SKILL
15
+ "#{SKILL_LEVEL_DESCRIPTIONS[skill - 1]} #{type.capitalize} (#{'*' * skill})"
16
+ else
17
+ type.capitalize
18
+ end
13
19
  end
14
20
 
15
21
  protected
@@ -23,11 +29,15 @@ module SmashAndGrab::Abilities
23
29
  public
24
30
  # Data saved with the character.
25
31
  def to_json(*a)
32
+ to_hash.to_json(*a)
33
+ end
34
+
35
+ def to_hash
26
36
  {
27
37
  type: type,
28
38
  skill: skill,
29
39
  action_cost: @action_cost,
30
- }.to_json(*a)
40
+ }
31
41
  end
32
42
 
33
43
  public
@@ -57,6 +67,8 @@ module SmashAndGrab::Abilities
57
67
  # Ability that has an effect every turn.
58
68
  # @abstract
59
69
  class ContinuousAbility < Ability
70
+ def active?; @active; end
71
+
60
72
  def initialize(owner, data)
61
73
  @active = data[:active]
62
74
  super(owner, data)
@@ -74,9 +86,36 @@ module SmashAndGrab::Abilities
74
86
  # An ability that has an effect, but the player can turn it on and off. e.g. Invisibility.
75
87
  # @abstract
76
88
  class ToggleAbility < ContinuousAbility
89
+ def activate?; owner.action_points >= action_cost and not active?; end
90
+ def deactivate?; owner.movement_points >= movement_bonus and active?; end
91
+
77
92
  def initialize(owner, data)
78
93
  super(owner, { active: false }.merge!(data))
79
94
  end
95
+
96
+ def do(data)
97
+ if active?
98
+ deactivate data
99
+ else
100
+ activate data
101
+ end
102
+ end
103
+
104
+ def undo(data)
105
+ self.do data # Effect is based on current state, not on whether it is done or undone.
106
+ end
107
+
108
+ protected
109
+ def activate(data)
110
+ owner.action_points -= action_cost
111
+ @active = true
112
+ end
113
+
114
+ protected
115
+ def deactivate(data)
116
+ owner.action_points += action_cost
117
+ @active = false
118
+ end
80
119
  end
81
120
 
82
121
  # Ability that can be activated, but only on oneself.
@@ -97,10 +136,13 @@ module SmashAndGrab::Abilities
97
136
  target_position: target_tile.grid_position
98
137
  )
99
138
  end
139
+
140
+ protected
141
+ def target(data); owner.map.object_by_id(data[:target_id]); end
100
142
  end
101
143
 
102
144
  # An ability that requires that the actor be adjacent to the target
103
- # and will move them if necessary. E.g. Melee.
145
+ # and will move them if necessary. E.g. Melee or PickUp.
104
146
  # @abstract
105
147
  class TouchAbility < TargetedAbility
106
148
  end
@@ -0,0 +1,38 @@
1
+ require_relative "ability"
2
+
3
+ # Pick up an object from the ground
4
+ module SmashAndGrab::Abilities
5
+ class Drop < TargetedAbility
6
+ def can_be_undone?; true; end
7
+ def use?; !owner.contents.nil? and owner.tile.adjacent_tiles(owner).any?(&:empty?); end
8
+
9
+ def tip
10
+ "#{super} #{owner.contents ? owner.contents.name : "something" }"
11
+ end
12
+
13
+ def initialize(owner, data)
14
+ super(owner, data.merge(action_cost: 1, skill: NON_SKILL))
15
+ end
16
+
17
+ def action_data
18
+ # TODO: find a way to get a specific position.
19
+ target_tile = owner.tile.adjacent_tiles(owner).find_all(&:empty?).sample
20
+
21
+ super(target_tile).merge(
22
+ target_id: owner.contents.id
23
+ )
24
+ end
25
+
26
+ def do(data)
27
+ super(data)
28
+
29
+ owner.drop owner.map.tile_at_grid(*data[:target_position])
30
+ end
31
+
32
+ def undo(data)
33
+ owner.pick_up target(data)
34
+
35
+ super(data)
36
+ end
37
+ end
38
+ end
@@ -21,23 +21,21 @@ module SmashAndGrab::Abilities
21
21
  )
22
22
  end
23
23
 
24
- def target(data); owner.map.object_by_id(data[:target_id]); end
25
-
26
24
  def random_damage
27
- # 1..skill as damage in a bell-ish curve.
28
- (skill - 1).times.find_all { rand(2) == 1 }.size + 1
25
+ # 0..skill as damage in a bell-ish curve.
26
+ skill.times.find_all { rand(6) + 1 <= 3 }.size # 3 potential hits on each d6.
29
27
  end
30
28
 
31
29
  def do(data)
32
30
  super(data)
33
31
 
34
- owner.melee(target(data), data[:damage])
32
+ owner.make_melee_attack(target(data), data[:damage])
35
33
  end
36
34
 
37
35
  def undo(data)
38
36
  target = target(data)
39
37
  target.tile = owner.map.tile_at_grid(data[:target_position]) unless target.tile
40
- owner.melee(target(data), -data[:damage])
38
+ owner.make_melee_attack(target(data), -data[:damage])
41
39
 
42
40
  super(data)
43
41
  end
@@ -0,0 +1,33 @@
1
+ require_relative "ability"
2
+
3
+ # Pick up an object from the ground
4
+ module SmashAndGrab::Abilities
5
+ class PickUp < TouchAbility
6
+ def can_be_undone?; true; end
7
+
8
+ def tip
9
+ raise "n/a"
10
+ end
11
+
12
+ def initialize(owner, data)
13
+ super(owner, data.merge(action_cost: 1, skill: NON_SKILL))
14
+ end
15
+
16
+ def action_data(object)
17
+ super(object.tile).merge({
18
+ })
19
+ end
20
+
21
+ def do(data)
22
+ super(data)
23
+
24
+ owner.pick_up target(data)
25
+ end
26
+
27
+ def undo(data)
28
+ owner.drop owner.map.tile_at_grid(*data[:target_position])
29
+
30
+ super(data)
31
+ end
32
+ end
33
+ end
@@ -4,49 +4,55 @@ module SmashAndGrab::Abilities
4
4
  class Ranged < TargetedAbility
5
5
  attr_reader :min_range, :max_range
6
6
 
7
- def can_undo?; false; end
7
+ def can_be_undone?; false; end
8
8
 
9
9
  # TODO: Take into account min/max range and LOS.
10
10
  def target_valid?(tile); !!(tile.object.is_a?(Objects::Entity) and tile.object.enemy?(owner)); end
11
11
 
12
12
  def initialize(owner, data)
13
- super(owner, data.merge(action_cost: 1))
13
+ data = {
14
+ action_cost: 1,
15
+ min_range: 2 # Can't fire at adjacent (melee) squares.
16
+ }.merge data
17
+
18
+ @min_range = data[:min_range]
14
19
  @max_range = data[:max_range] || raise(ArgumentError, "no :max_range specified")
15
- @min_range = data[:min_range] || raise(ArgumentError, "no :min_range specified")
20
+
21
+ super(owner, data)
16
22
  end
17
23
 
18
24
  def tip
19
25
  "#{super} attack in ranged combat, at range #{min_range}..#{max_range}"
20
26
  end
21
27
 
22
- def to_json(*args)
28
+ def to_hash
23
29
  super.merge(
24
30
  min_range: @min_range,
25
31
  max_range: @max_range,
26
- ).to_json(*args)
32
+ )
27
33
  end
28
34
 
29
- def action_data(target_tile)
30
- super(target_tile).merge!(
35
+ def action_data(target)
36
+ super(target.tile).merge!(
31
37
  damage: random_damage
32
38
  )
33
39
  end
34
40
 
35
41
  def random_damage
36
- # 1..skill as damage in a bell-ish curve.
37
- (skill - 1).times.find_all { rand(2) == 1 }.size + 1
42
+ # 0..skill as damage in a bell-ish curve.
43
+ skill.times.find_all { rand(6) + 1 <= 2 }.size # 2 potential hits on each d6.
38
44
  end
39
45
 
40
46
  def do(data)
41
47
  super(data)
42
48
 
43
- owner.map.object_by_id(data[:target_id]).health -= data[:damage]
49
+ owner.make_ranged_attack(target(data), data[:damage])
44
50
  end
45
51
 
46
52
  def undo(data)
47
- target = owner.map.object_by_id(data[:target_id])
48
- target.health += data[:damage]
53
+ target = target(data)
49
54
  target.tile = owner.map.tile_at_grid(data[:target_position]) unless target.tile
55
+ owner.make_ranged_attack(target(data), -data[:damage])
50
56
 
51
57
  super(data)
52
58
  end
@@ -1,9 +1,13 @@
1
1
  require_relative "ability"
2
2
 
3
3
  module SmashAndGrab::Abilities
4
- class Sprint < SelfAbility
4
+ class Sprint < ToggleAbility
5
5
  def initialize(owner, data)
6
6
  super(owner, data.merge(action_cost: :all))
7
+
8
+ owner.subscribe :ended_turn do
9
+ @active = false
10
+ end
7
11
  end
8
12
 
9
13
  def movement_bonus
@@ -31,16 +35,16 @@ module SmashAndGrab::Abilities
31
35
  )
32
36
  end
33
37
 
34
- def do(data)
35
- super(data)
36
-
38
+ protected
39
+ def activate(data)
40
+ super
37
41
  owner.movement_points += data[:movement_bonus]
38
42
  end
39
43
 
40
- def undo(data)
44
+ protected
45
+ def deactivate(data)
46
+ super
41
47
  owner.movement_points -= data[:movement_bonus]
42
-
43
- super(data)
44
48
  end
45
49
  end
46
50
  end
@@ -0,0 +1,26 @@
1
+ module Chingu
2
+ class BasicGameObject
3
+ def initialize(options = {})
4
+ @options = options
5
+ @parent = options[:parent]
6
+
7
+ # SMASH_AND_GRAB PATCH: Do not want to store this at all.
8
+ #self.class.instances ||= Array.new
9
+ #self.class.instances << self
10
+
11
+ #
12
+ # A GameObject either belong to a GameState or our mainwindow ($window)
13
+ #
14
+ @parent = $window.current_scope if !@parent && $window
15
+
16
+ # if true, BasicGameObject#update will be called
17
+ @paused = options[:paused] || options[:pause] || false
18
+
19
+ # This will call #setup_trait on the latest trait mixed in
20
+ # which then will pass it on to the next setup_trait() with a super-call.
21
+ setup_trait(options)
22
+
23
+ setup
24
+ end
25
+ end
26
+ end
@@ -3,7 +3,11 @@ class GameWindow < Chingu::Window
3
3
  attr_reader :pixel
4
4
 
5
5
  def initialize
6
- super(800, 600, false)
6
+ if FULLSCREEN
7
+ super(screen_width, screen_height, true)
8
+ else
9
+ super(800, 600, false)
10
+ end
7
11
  end
8
12
 
9
13
  def setup
@@ -1,6 +1,8 @@
1
1
  module SmashAndGrab
2
2
  module Gui
3
3
  class EntityPanel < Fidgit::Horizontal
4
+ TITLE_COLOR = Color.rgb(150, 150, 150)
5
+
4
6
  event :info_toggled
5
7
 
6
8
  def initialize(entity, info_shown, options = {})
@@ -25,8 +27,9 @@ module SmashAndGrab
25
27
  end
26
28
  end
27
29
 
28
- vertical padding: 0, spacing: 4 do
29
- @name = label @entity.name
30
+ vertical padding: 0, spacing: 1.5 do
31
+ @name = label @entity.name, font_height: 20, color: Color::WHITE
32
+ label TITLE_COLOR.colorize("#{entity.title} - #{@entity.faction.colorized_name}"), font_height: 12
30
33
  @sub_panel_container = vertical spacing: 0, padding: 0
31
34
  end
32
35
 
@@ -36,7 +39,11 @@ module SmashAndGrab
36
39
 
37
40
  update_details @entity
38
41
 
39
- @entity.subscribe :changed, method(:update_details)
42
+ @changed_event = @entity.subscribe :changed, method(:update_details)
43
+ end
44
+
45
+ def finalize
46
+ @changed_event.unsubscribe
40
47
  end
41
48
 
42
49
  def switch_sub_panel
@@ -47,9 +54,9 @@ module SmashAndGrab
47
54
  def create_info_sub_panel
48
55
  @info_sub_panel = Fidgit::Vertical.new padding: 0, spacing: 0 do
49
56
  text = nil
50
- scroll = scroll_window width: 350, height: 72 do
57
+ scroll = scroll_window width: 350, height: 68 do
51
58
  text = text_area text: "#{@entity.name} once ate a pomegranate, but it took all day and all night... " * 5,
52
- width: 330, font_height: 14, enabled: false
59
+ width: 330, font_height: 14, editable: false
53
60
  end
54
61
  scroll.background_color = text.background_color
55
62
  end
@@ -70,20 +77,22 @@ module SmashAndGrab
70
77
 
71
78
  label_options = button_options.merge border_thickness: 2, border_color: Color.rgba(255, 255, 255, 100)
72
79
 
73
- [:melee, :ranged, :sprint].each do |ability_name|
80
+ [:melee, :ranged, :sprint, :drop].each do |ability_name|
74
81
  if @entity.has_ability? ability_name
75
82
  ability = @entity.ability ability_name
76
83
  @ability_buttons[ability_name] = button("#{ability_name.to_s[0].upcase}#{ability.skill}",
77
84
  button_options.merge(tip: ability.tip)) do
78
85
 
79
- @entity.use_ability :sprint if ability_name == :sprint
86
+ if ability_name == :sprint or ability_name == :drop
87
+ @entity.use_ability ability_name
88
+ end
80
89
  end
81
90
  else
82
91
  label "", label_options
83
92
  end
84
93
  end
85
94
 
86
- 5.times do |i|
95
+ 4.times do |i|
87
96
  label "", label_options
88
97
  end
89
98
  end
@@ -91,7 +100,9 @@ module SmashAndGrab
91
100
  end
92
101
 
93
102
  def update_details(entity)
94
- @health.text = "HP: #{entity.health} / #{entity.max_health}"
103
+ return unless entity.faction.player
104
+
105
+ @health.text = "HP: #{entity.hp} / #{entity.max_hp}"
95
106
  @movement_points.text = "MP: #{entity.mp} / #{entity.max_mp}"
96
107
  @action_points.text = "AP: #{entity.ap} / #{entity.max_ap}"
97
108
 
@@ -100,7 +111,12 @@ module SmashAndGrab
100
111
  end
101
112
 
102
113
  @ability_buttons.each do |ability, button|
103
- button.enabled = (entity.active? and (entity.has_ability?(ability) and entity.action_points >= entity.ability(ability).action_cost))
114
+ button.enabled = entity.active? && entity.faction.player.human? && entity.use_ability?(ability)
115
+
116
+ # TODO: this should be more sensible (allows un-sprinting).
117
+ if ability == :sprint && entity.active? && entity.faction.player.human? && entity.ability(ability).deactivate?
118
+ button.enabled = true
119
+ end
104
120
  end
105
121
 
106
122
  @portrait.image = entity.image