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.
- data/{CHANGELOG.txt → CHANGELOG.md} +0 -0
- data/Gemfile.lock +8 -4
- data/LICENSE.txt +20 -0
- data/README.md +30 -14
- data/Rakefile +2 -2
- data/config/lang/objects/entities/en.yml +134 -0
- data/config/lang/objects/static/en.yml +8 -0
- data/config/lang/objects/vehicles/en.yml +11 -0
- data/config/map/entities.yml +42 -38
- data/lib/smash_and_grab.rb +5 -0
- data/lib/smash_and_grab/abilities.rb +1 -1
- data/lib/smash_and_grab/abilities/ability.rb +45 -3
- data/lib/smash_and_grab/abilities/drop.rb +38 -0
- data/lib/smash_and_grab/abilities/melee.rb +4 -6
- data/lib/smash_and_grab/abilities/pick_up.rb +33 -0
- data/lib/smash_and_grab/abilities/ranged.rb +18 -12
- data/lib/smash_and_grab/abilities/sprint.rb +11 -7
- data/lib/smash_and_grab/chingu_ext/basic_game_object.rb +26 -0
- data/lib/smash_and_grab/game_window.rb +5 -1
- data/lib/smash_and_grab/gui/entity_panel.rb +26 -10
- data/lib/smash_and_grab/gui/entity_summary.rb +19 -11
- data/lib/smash_and_grab/gui/game_log.rb +6 -2
- data/lib/smash_and_grab/gui/info_panel.rb +7 -4
- data/lib/smash_and_grab/gui/object_panel.rb +6 -2
- data/lib/smash_and_grab/gui/scenario_panel.rb +4 -0
- data/lib/smash_and_grab/history/action_history.rb +1 -1
- data/lib/smash_and_grab/main.rb +7 -3
- data/lib/smash_and_grab/map/faction.rb +26 -8
- data/lib/smash_and_grab/map/map.rb +21 -12
- data/lib/smash_and_grab/map/tile.rb +29 -2
- data/lib/smash_and_grab/map/wall.rb +2 -2
- data/lib/smash_and_grab/mixins/has_contents.rb +38 -0
- data/lib/smash_and_grab/mixins/line_of_sight.rb +129 -0
- data/lib/smash_and_grab/mixins/pathfinding.rb +145 -0
- data/lib/smash_and_grab/mouse_selection.rb +107 -36
- data/lib/smash_and_grab/objects/entity.rb +311 -260
- data/lib/smash_and_grab/objects/floating_text.rb +0 -1
- data/lib/smash_and_grab/objects/static.rb +10 -3
- data/lib/smash_and_grab/objects/vehicle.rb +7 -2
- data/lib/smash_and_grab/objects/world_object.rb +10 -3
- data/lib/smash_and_grab/path.rb +38 -12
- data/lib/smash_and_grab/players/ai.rb +91 -0
- data/lib/smash_and_grab/players/human.rb +9 -0
- data/lib/smash_and_grab/players/player.rb +16 -65
- data/lib/smash_and_grab/players/remote.rb +9 -0
- data/lib/smash_and_grab/sprite_sheet.rb +9 -0
- data/lib/smash_and_grab/states/edit_level.rb +15 -4
- data/lib/smash_and_grab/states/main_menu.rb +16 -4
- data/lib/smash_and_grab/states/play_level.rb +88 -28
- data/lib/smash_and_grab/states/world.rb +17 -9
- data/lib/smash_and_grab/version.rb +1 -1
- data/lib/smash_and_grab/z_order.rb +6 -5
- data/media/images/path.png +0 -0
- data/media/images/tile_selection.png +0 -0
- data/media/images/tiles_selection.png +0 -0
- data/smash_and_grab.gemspec +2 -2
- data/tasks/create_portraits.rb +39 -0
- data/tasks/outline_images.rb +56 -0
- data/test/smash_and_grab/abilities/drop_test.rb +68 -0
- data/test/smash_and_grab/abilities/melee_test.rb +4 -4
- data/test/smash_and_grab/abilities/pick_up_test.rb +68 -0
- data/test/smash_and_grab/abilities/ranged_test.rb +105 -0
- data/test/smash_and_grab/abilities/sprint_test.rb +55 -25
- data/test/smash_and_grab/map/faction_test.rb +18 -16
- data/test/smash_and_grab/map/map_test.rb +9 -4
- metadata +51 -19
- data/lib/smash_and_grab/fidgit_ext/event.rb +0 -77
@@ -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
|
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
|
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
|
data/lib/smash_and_grab/path.rb
CHANGED
@@ -7,7 +7,7 @@ class Path
|
|
7
7
|
TILE_SIZE = 16
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def sprites; @sprites ||= SpriteSheet
|
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
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
@@ -1,76 +1,27 @@
|
|
1
1
|
module SmashAndGrab
|
2
|
-
module Players
|
3
|
-
class Player
|
4
|
-
|
2
|
+
module Players
|
3
|
+
class Player
|
4
|
+
attr_reader :faction
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
59
|
-
@active_entities.shift unless entity.use_ability?(:melee)
|
60
|
-
end
|
17
|
+
@faction.player = self
|
61
18
|
|
62
|
-
|
63
|
-
#
|
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
|
@@ -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
|