sabrina 0.5.5

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.
@@ -0,0 +1,43 @@
1
+ module Sabrina
2
+ class Plugin
3
+ # Allows to load plugins. Add +load_plugins+ at the end of your class's
4
+ # +initialize+ call.
5
+ module Load
6
+ class << self
7
+ # Exposes +#append_features+.
8
+ def include_in(obj)
9
+ append_features(obj) unless obj.include?(self)
10
+ end
11
+ end
12
+
13
+ attr_reader :plugins
14
+
15
+ private
16
+
17
+ # Generates plugin instances. This should be called from
18
+ # the target class's init.
19
+ #
20
+ # @return [0]
21
+ def load_plugins
22
+ self.class.plugins.each do |plugin|
23
+ n = plugin.short_name
24
+
25
+ plugin.features.each do |f|
26
+ feature_all_sym = f.to_sym
27
+ feature_this_sym = "#{f}_#{n}"
28
+ unless respond_to?(feature_all_sym)
29
+ define_singleton_method(feature_all_sym, plugin.feature_all(f))
30
+ end
31
+ unless respond_to?(feature_this_sym)
32
+ define_singleton_method(feature_this_sym, plugin.feature_this(f, n))
33
+ end
34
+ end
35
+
36
+ define_singleton_method(n, -> { @plugins[n] }) unless respond_to?(n)
37
+ @plugins[n] = plugin.new(self)
38
+ end
39
+ 0
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ module Sabrina
2
+ class Plugin
3
+ # Allows to register plugins.
4
+ module Register
5
+ # Lists all currently registered plugins.
6
+ #
7
+ # @return [Set]
8
+ def plugins
9
+ @plugins.to_a
10
+ end
11
+
12
+ # Registers a new plugin for handling a specific subset of monster
13
+ # data.
14
+ #
15
+ # @return [0]
16
+ # @see Plugin
17
+ def register_plugin(plugin)
18
+ @plugins ||= Set.new
19
+ @plugins << plugin
20
+ end
21
+
22
+ # Lists all currently available features.
23
+ #
24
+ # @return [Set]
25
+ def features
26
+ s = Set.new
27
+ @plugins.each { |x| s += x.features }
28
+ s
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,196 @@
1
+ module Sabrina
2
+ module Plugins
3
+ # This {Plugin} provides an abstraction for manipulating all
4
+ # {Sprite Sprites} and {Palette Palettes} associated with a {Monster}.
5
+ #
6
+ # @see Plugin
7
+ class Spritesheet < Plugin
8
+ include ChildrenManager
9
+
10
+ # @see Plugin::ENHANCES
11
+ ENHANCES = Monster
12
+
13
+ # @see Plugin::PLUGIN_NAME
14
+ PLUGIN_NAME = 'Spritesheet'
15
+
16
+ # @see Plugin::SHORT_NAME
17
+ SHORT_NAME = 'spritesheet'
18
+
19
+ # @see Plugin::FEATURES
20
+ FEATURES = Set.new [:reread, :write, :save, :load]
21
+
22
+ # @!attribute rom
23
+ # The current working ROM file.
24
+ # @return [Rom]
25
+ # @!method rom=(r)
26
+ # The current working ROM file.
27
+ # @param [Rom] r
28
+ # @return [Rom]
29
+ # @!attribute index
30
+ # The real index of the monster.
31
+ # @return [Integer]
32
+ # @!method index=(i)
33
+ # The real index of the monster.
34
+ # @param [Integer] i
35
+ # @return [Integer]
36
+ attr_children :rom, :index
37
+
38
+ # The palettes used by the sprites.
39
+ #
40
+ # @return [Array]
41
+ attr_reader :palettes
42
+
43
+ # The sprites.
44
+ #
45
+ # @return [Array]
46
+ attr_reader :sprites
47
+
48
+ # Generates a new Spritesheet object.
49
+ #
50
+ # @return [Spritesheet]
51
+ def initialize(monster)
52
+ @monster = monster
53
+ @rom = monster.rom
54
+ @index = monster.index
55
+
56
+ @palettes = [
57
+ Palette.from_table(@monster.rom, :palette_table, @monster.index),
58
+ Palette.from_table(@monster.rom, :shinypal_table, @monster.index)
59
+ ]
60
+
61
+ @sprites = [
62
+ Sprite.from_table(@monster.rom, :front_table, @monster.index),
63
+ Sprite.from_table(@monster.rom, :back_table, @monster.index)
64
+ ]
65
+ end
66
+
67
+ # @return [Array]
68
+ # @see ChildrenManager
69
+ def children
70
+ @sprites + @palettes
71
+ end
72
+
73
+ # Justify sprites to the target ROM count as per ROM {Config}, assuming
74
+ # 64x64 frame size.
75
+ def justify
76
+ @sprites.map(&:justify)
77
+ end
78
+
79
+ # Load data from a file.
80
+ #
81
+ # @return [Array] Any return data from child methods.
82
+ def load(file = @monster.filename, dir = @monster.work_dir, *_args)
83
+ path = get_path(file, dir)
84
+ a = split_canvas(ChunkyPNG::Canvas.from_file(path))
85
+ h = { rom: @rom, index: @index }
86
+
87
+ normal_rgb = a[0].to_rgb_stream + a[2].to_rgb_stream
88
+ shiny_rgb = a[1].to_rgb_stream + a[3].to_rgb_stream
89
+
90
+ @palettes = Palette.create_synced_palettes(
91
+ normal_rgb,
92
+ shiny_rgb,
93
+ h.merge(table: :palette_table),
94
+ h.merge(table: :shinypal_table)
95
+ )
96
+
97
+ frames = @rom.special_frames.fetch(@index, @rom.frames)
98
+ front = crop_canvas(a[0], frames.first * 64)
99
+ back = crop_canvas(a[2], frames.last * 64)
100
+
101
+ @sprites = [
102
+ Sprite.from_canvas(
103
+ front,
104
+ @palettes.first,
105
+ h.merge(table: :front_table)
106
+ ),
107
+ Sprite.from_canvas(
108
+ back,
109
+ @palettes.first,
110
+ h.merge(table: :back_table)
111
+ )
112
+ ]
113
+
114
+ justify
115
+ children
116
+ end
117
+
118
+ # Save data to a file.
119
+ #
120
+ # @return [Array] Any return data from child methods.
121
+ def save(file = @monster.filename, dir = @monster.work_dir, *_args)
122
+ a = []
123
+ @sprites.product(@palettes).each do |x|
124
+ a << x.first.to_canvas(x.last)
125
+ end
126
+
127
+ path = get_path(file, dir, mkdir: true)
128
+
129
+ combine_canvas(a).save(path)
130
+ end
131
+
132
+ private
133
+
134
+ # Crops the canvas to the specified height if taller.
135
+ def crop_canvas(c, h)
136
+ return c.crop(0, 0, c.width, h) if c.height > h
137
+ c
138
+ end
139
+
140
+ # Horizontally combine a one-dimensional array of
141
+ # {http://rdoc.info/gems/chunky_png/1.2.0/ChunkyPNG/Canvas Canvas}
142
+ # objects into a single, wide canvas.
143
+ #
144
+ # @param [Array] a an array of Canvas objects.
145
+ # @return [Canvas] the combined canvas.
146
+ def combine_canvas(a)
147
+ out_c = ChunkyPNG::Canvas.new(a.first.width * a.length, a.first.height)
148
+ a.each_index { |i| out_c.replace!(a[i], a.first.width * i) }
149
+ out_c
150
+ end
151
+
152
+ # Vertically combine a one-dimensional array of
153
+ # {http://rdoc.info/gems/chunky_png/1.2.0/ChunkyPNG/Canvas Canvas}
154
+ # objects into a single, tall canvas.
155
+ #
156
+ # @param [Array] a an array of Canvas objects.
157
+ # @return [Canvas] the combined canvas.
158
+ def combine_canvas_vert(a)
159
+ out_c = ChunkyPNG::Canvas.new(a.first.width, a.first.height * a.length)
160
+ a.each_index { |i| out_c.replace!(a[i], 0, a.first.height * i) }
161
+ out_c
162
+ end
163
+
164
+ # Split a {http://rdoc.info/gems/chunky_png/1.2.0/ChunkyPNG/Canvas Canvas}
165
+ # into an array of canvases horizontally.
166
+ #
167
+ # @param [Canvas] c
168
+ # @param [Integer] w tile width.
169
+ # @return [Array] an array of Canvas objects.
170
+ def split_canvas(c, w = 64)
171
+ out_a = []
172
+ (c.width / w).times { |i| out_a << c.crop(i * w, 0, w, c.height) }
173
+ out_a
174
+ end
175
+
176
+ # Concatenate the file name and directory into a full path, optionally
177
+ # creating the directory if it doesn't exist.
178
+ #
179
+ # @param [String] file
180
+ # @param [String] dir
181
+ # @param [Hash] h
182
+ # @option h [Boolean] :mkdir If +true+, create +dir+ if it doesn't
183
+ # exist.
184
+ # @return [String]
185
+ def get_path(file, dir, h = {})
186
+ f, d = file.dup, dir.dup
187
+ d << '/' unless d.empty? || d.end_with?('/')
188
+
189
+ FileUtils.mkpath(d) if h.fetch(:mkdir, false) && !Dir.exist?(d)
190
+
191
+ path = d << f
192
+ path << '.png' unless path.downcase.end_with?('.png')
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,257 @@
1
+ module Sabrina
2
+ module Plugins
3
+ # @todo Finish this class.
4
+ # This {Plugin} aids in manipulating the basic stats of any {Monster}.
5
+ #
6
+ # @see Plugin
7
+ class Stats < Plugin
8
+ # @see Plugin::ENHANCES
9
+ ENHANCES = Monster
10
+
11
+ # @see Plugin::PLUGIN_NAME
12
+ PLUGIN_NAME = 'Stats'
13
+
14
+ # @see Plugin::SHORT_NAME
15
+ SHORT_NAME = 'stats'
16
+
17
+ # @see Plugin::FEATURES
18
+ FEATURES = Set.new [:reread, :write]
19
+
20
+ # Describes the order in which various stats appear in the byte data.
21
+ # All of these will also be converted into attributes on runtime and
22
+ # can be modified directly.
23
+ STRUCTURE = [
24
+ :hp, :attack, :defense, :speed, :sp_atk, :sp_def, :type_1, :type_2,
25
+ :catch_rate, :exp_yield, :ev_yield, :item_1, :item_2, :gender,
26
+ :egg_cycles, :friendship, :level_curve, :egg_group_1, :egg_group_2,
27
+ :ability_1, :ability_2, :safari_rate, :color_flip
28
+ ]
29
+
30
+ # Code names for level up types.
31
+ LEVEL_CURVES = [
32
+ 'Medium-Fast', 'Erratic', 'Fluctuating', 'Medium-Slow', 'Fast', 'Slow'
33
+ ]
34
+
35
+ # Code names for egg groups.
36
+ EGG_GROUPS = [
37
+ 'None', 'Monster', 'Water 1', 'Bug', 'Flying', 'Field', 'Fairy',
38
+ 'Grass', 'Human-Like', 'Water 3', 'Mineral', 'Amorphous', 'Water 2',
39
+ 'Ditto', 'Dragon', 'Undiscovered'
40
+ ]
41
+
42
+ # Where to pull descriptive names from if applicable, these can be arrays
43
+ # or ROM tables.
44
+ NAMES = {
45
+ type_1: :type_table,
46
+ type_2: :type_table,
47
+ item_1: :item_table,
48
+ item_2: :item_table,
49
+ level_curve: LEVEL_CURVES,
50
+ egg_group_1: EGG_GROUPS,
51
+ egg_group_2: EGG_GROUPS,
52
+ ability_1: :ability_table,
53
+ ability_2: :ability_table
54
+ }
55
+
56
+ # @!attribute [rw] rom
57
+ # The current working ROM file.
58
+ # @return [Rom]
59
+ # @!attribute [rw] index
60
+ # The real index of the monster.
61
+ # @return [Integer]
62
+ attr_accessor(:rom, :index, *STRUCTURE)
63
+
64
+ # Generates a new Stats object.
65
+ #
66
+ # @return [Stats]
67
+ def initialize(monster)
68
+ @monster = monster
69
+ @rom = monster.rom
70
+ @index = monster.index
71
+
72
+ @stream = Bytestream.from_table(
73
+ @rom,
74
+ :stats_table,
75
+ @monster.index,
76
+ @rom.stats_length
77
+ )
78
+
79
+ parse_stats
80
+ end
81
+
82
+ # Returns the base stats total.
83
+ #
84
+ # @return [Integer]
85
+ def total
86
+ @hp + @attack + @defense + @speed + @sp_atk + @sp_def
87
+ end
88
+
89
+ # Reloads the data from a ROM, dropping any changes.
90
+ #
91
+ # @return [self]
92
+ def reread
93
+ parse_stats
94
+ self
95
+ end
96
+
97
+ # Write data to the ROM.
98
+ def write
99
+ stream.write_to_rom
100
+ end
101
+
102
+ # Returns a {Bytestream} object representing the stats, ready to be
103
+ # written to the ROM.
104
+ #
105
+ # @return [Bytestream]
106
+ def stream
107
+ Bytestream.from_bytes(
108
+ unparse_stats,
109
+ rom: @rom,
110
+ table: :stats_table,
111
+ index: @index
112
+ )
113
+ end
114
+
115
+ # Returns a hash representation of the stats.
116
+ #
117
+ # @return [Hash]
118
+ def to_h
119
+ h = {}
120
+
121
+ STRUCTURE.each do |entry|
122
+ h[entry] = instance_variable_get('@' << entry.to_s)
123
+ end
124
+
125
+ { @index => { stats: h } }
126
+ end
127
+
128
+ # Returns a pretty hexadecimal representation of the stats byte data.
129
+ #
130
+ # @return [String]
131
+ def to_hex
132
+ stream.to_hex(true)
133
+ end
134
+
135
+ # Returns a pretty JSON representation of the stats.
136
+ #
137
+ # @return [String]
138
+ def to_json
139
+ JSON.pretty_generate(to_h)
140
+ end
141
+
142
+ # Returns a blurb containing the base stats total.
143
+ #
144
+ # @return [String]
145
+ def to_s
146
+ "<Stats (#{total})>"
147
+ end
148
+
149
+ private
150
+
151
+ # Reads stats data from the ROM.
152
+ def parse_stats
153
+ b = @stream.to_bytes.dup
154
+
155
+ STRUCTURE.each do |entry|
156
+ value =
157
+ case entry
158
+ when :ev_yield
159
+ parse_evs(b.slice!(0, 2))
160
+ when :item_1, :item_2
161
+ b.slice!(0, 2).unpack('S').first
162
+ when :color_flip
163
+ parse_color_flip(b.slice!(0))
164
+ else
165
+ b.slice!(0).unpack('C').first
166
+ end
167
+
168
+ pretty_value = prettify_stat(entry, value)
169
+ instance_variable_set('@' << entry.to_s, pretty_value)
170
+ end
171
+ end
172
+
173
+ # Converts stats data to a GBA-compatible byte string.
174
+ def unparse_stats
175
+ b = ''
176
+ STRUCTURE.each do |entry|
177
+ val = instance_variable_get('@' << entry.to_s)
178
+ case entry
179
+ when :ev_yield
180
+ b << unparse_evs(val)
181
+ when :item_1, :item_2
182
+ b << [val.to_i].pack('S')
183
+ when :color_flip
184
+ b << unparse_color_flip(val)
185
+ else
186
+ b << [val.to_i].pack('C')
187
+ end
188
+ end
189
+
190
+ b.ljust(28, "\x00")
191
+ end
192
+
193
+ # Attempts to annotate a numeric value with useful information.
194
+ def prettify_stat(entry, value)
195
+ return "#{value} (#{gender_info(value)})" if entry == :gender
196
+
197
+ return value unless NAMES.key?(entry) && value.is_a?(Numeric)
198
+
199
+ zero_is_valid = [:type_1, :type_2, :level_curve]
200
+ return value if value == 0 && !zero_is_valid.include?(entry)
201
+
202
+ return "#{value} (#{NAMES[entry][value]})" if NAMES[entry].is_a?(Array)
203
+
204
+ "#{value} (#{ @rom.read_string_from_table(NAMES[entry], value) })"
205
+ end
206
+
207
+ # Displays readable info about the gender distribution defined by the
208
+ # provided gender value.
209
+ def gender_info(i)
210
+ return 'Genderless' if i > 254
211
+ return 'Always Female' if i == 254
212
+ return 'Always Male' if i == 0
213
+ "#{(100.0 * i / 255).round}% Female"
214
+ end
215
+
216
+ # Converts a word (two bytes) into a hash of EV yield data.
217
+ def parse_evs(b)
218
+ a = b.unpack('b*').first.scan(/../).map { |x| x.reverse.to_i(2) }
219
+ h = {}
220
+
221
+ a.take(6).each_index do |i|
222
+ next if a[i] < 1
223
+ h[STRUCTURE[i]] = a[i]
224
+ end
225
+
226
+ h
227
+ end
228
+
229
+ # Converts a hash of EV yield data into a word (two bytes).
230
+ def unparse_evs(h)
231
+ a = []
232
+
233
+ STRUCTURE.take(6).each do |stat|
234
+ ev = h.fetch(stat, 0)
235
+ a << ev.to_s(2).rjust(2, '0').reverse
236
+ end
237
+
238
+ [a.join.ljust(16, '0')].pack 'b*'
239
+ end
240
+
241
+ # Converts a byte into an array of dex color and flip.
242
+ def parse_color_flip(b)
243
+ s = b.unpack('C').first.to_s(16).rjust(2, '0')
244
+
245
+ [
246
+ s[1].to_i,
247
+ s[0] == '8' ? true : false
248
+ ]
249
+ end
250
+
251
+ # Converts an array of dex color and flip into a byte.
252
+ def unparse_color_flip(a)
253
+ ((a.last ? '8' : '0') << a.first.to_s).hex.chr
254
+ end
255
+ end
256
+ end
257
+ end