sscharter 0.9.0 → 0.10.1

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,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Sunniesnow::Charter
4
+
5
+ # @note Internal API.
6
+ # @!parse
7
+ # class GroupState < Data
8
+ # # @return [Array<Symbol>]
9
+ # attr_reader :tip_point_mode_stack
10
+ # # @return [Array<Integer?>]
11
+ # attr_reader :current_tip_point_stack
12
+ # # @return [Array<Array<Event>>]
13
+ # attr_reader :current_tip_point_group_stack
14
+ # # @return [Integer]
15
+ # attr_reader :current_duplicate
16
+ # # @return [Array<TipPointStart?>]
17
+ # attr_reader :tip_point_start_stack
18
+ # # @return [Array<TipPointStart?>]
19
+ # attr_reader :tip_point_start_to_add_stack
20
+ # # @return [Array<Array<Event>>]
21
+ # attr_reader :groups
22
+ # end
23
+ GroupState = Sunniesnow::Utils::Data.define(
24
+ :tip_point_mode_stack, :current_tip_point_stack,
25
+ :current_tip_point_group_stack, :current_duplicate,
26
+ :tip_point_start_stack, :tip_point_start_to_add_stack, :groups
27
+ )
28
+
29
+ # @note Internal API.
30
+ # @!parse
31
+ # class Bookmark < Data
32
+ # # @return [BeatState]
33
+ # attr_reader :beat_state
34
+ # # @return [GroupState]
35
+ # attr_reader :group_state
36
+ # end
37
+ Bookmark = Sunniesnow::Utils::Data.define :beat_state, :group_state
38
+
39
+ # @note Internal API.
40
+ # @return [void]
41
+ def init_bookmarks
42
+ @bookmarks = {}
43
+ nil
44
+ end
45
+
46
+ # @note Internal API.
47
+ # @return [void]
48
+ def init_group_state
49
+ @tip_point_mode_stack = [:none]
50
+ @current_tip_point_stack = []
51
+ @current_tip_point_group_stack = []
52
+ @tip_point_peak = 0
53
+ @current_duplicate = 0
54
+ @tip_point_start_stack = [nil]
55
+ @tip_point_start_to_add_stack = [nil]
56
+ @groups = [@events]
57
+ nil
58
+ end
59
+
60
+ # @note Internal API.
61
+ # @return [GroupState]
62
+ def current_group_state
63
+ GroupState.new(
64
+ @tip_point_mode_stack.dup,
65
+ @current_tip_point_stack.dup,
66
+ @current_tip_point_group_stack.dup,
67
+ @current_duplicate,
68
+ @tip_point_start_stack.dup,
69
+ @tip_point_start_to_add_stack.dup,
70
+ @groups.dup
71
+ )
72
+ end
73
+
74
+ # @note Internal API.
75
+ # @param backup [GroupState]
76
+ # @return [void]
77
+ def restore_group_state backup
78
+ @tip_point_mode_stack = backup.tip_point_mode_stack
79
+ @current_tip_point_stack = backup.current_tip_point_stack
80
+ @current_tip_point_group_stack = backup.current_tip_point_group_stack
81
+ @current_duplicate = backup.current_duplicate
82
+ @tip_point_start_to_add_stack = backup.tip_point_start_to_add_stack
83
+ @groups = backup.groups
84
+ nil
85
+ end
86
+
87
+ # @!group DSL Methods
88
+
89
+ # @return [Array<Event>] the events created inside +block+.
90
+ # @yieldself [Charter] the same as +self+.
91
+ def group preserve_beat: true, &block
92
+ raise ArgumentError, 'no block given' unless block
93
+ @groups.push result = []
94
+ beat_backup = current_beat_state unless preserve_beat
95
+ instance_eval &block
96
+ restore_beat_state beat_backup unless preserve_beat
97
+ @groups.delete_if { result.equal? _1 }
98
+ result
99
+ end
100
+
101
+ # @param name [Object]
102
+ # @return [Object] +name+.
103
+ def mark name
104
+ @bookmarks[name] = Bookmark.new current_beat_state, current_group_state
105
+ name
106
+ end
107
+
108
+ # @return [Array<Event>]
109
+ # @yieldself [Charter] the same as +self+.
110
+ # @param name [Object] the name of the bookmark to jump to.
111
+ # @param goto_beat [Boolean]
112
+ # @param preserve_beat [Boolean]
113
+ # @param update_mark [Boolean]
114
+ def at name, goto_beat: true, preserve_beat: false, update_mark: false, &block
115
+ raise ArgumentError, 'no block given' unless block
116
+ raise ArgumentError, "unknown bookmark #{name}" unless bookmark = @bookmarks[name]
117
+ group_backup = current_group_state
118
+ beat_backup = current_beat_state unless preserve_beat
119
+ restore_group_state bookmark.group_state
120
+ restore_beat_state bookmark.beat_state if goto_beat
121
+ result = group &block
122
+ mark name if update_mark
123
+ restore_group_state group_backup
124
+ restore_beat_state beat_backup unless preserve_beat
125
+ result
126
+ end
127
+
128
+ # @!endgroup
129
+
130
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Sunniesnow::Charter
4
+
5
+ # Aliases for some colors that can be used with {#difficulty_color}.
6
+ COLORS = {
7
+ easy: '#3eb9fd',
8
+ normal: '#f19e56',
9
+ hard: '#e75e74',
10
+ master: '#8c68f3',
11
+ special: '#f156ee'
12
+ }.freeze
13
+
14
+ # @note Internal API.
15
+ # @return [void]
16
+ def init_chart_info
17
+ @difficulty_name = ''
18
+ @difficulty_color = '#000000'
19
+ @difficulty = ''
20
+ @difficulty_sup = ''
21
+ @title = ''
22
+ @artist = ''
23
+ @charter = ''
24
+ @events = []
25
+ end
26
+
27
+ # @!group DSL Methods
28
+
29
+ # Set the title of the music for the chart.
30
+ # This will be reflected in the return value of {#to_sunniesnow}.
31
+ # @see Sunniesnow::Chart#title
32
+ # @param title [String] the title of the music.
33
+ # @return [String] the title of the music, the same as the argument +title+.
34
+ # @raise [ArgumentError] if +title+ is not a String.
35
+ def title title
36
+ raise ArgumentError, 'title must be a string' unless title.is_a? String
37
+ @title = title
38
+ end
39
+
40
+ # Set the artist of the music for the chart.
41
+ # This will be reflected in the return value of {#to_sunniesnow}.
42
+ # @see Sunniesnow::Chart#artist
43
+ # @param artist [String] the artist of the music.
44
+ # @return [String] the artist of the music, the same as the argument +artist+.
45
+ # @raise [ArgumentError] if +artist+ is not a String.
46
+ def artist artist
47
+ raise ArgumentError, 'artist must be a string' unless artist.is_a? String
48
+ @artist = artist
49
+ end
50
+
51
+ # Set the name of the chart author for the chart.
52
+ # This will be reflected in the return value of {#to_sunniesnow}.
53
+ # @see Sunniesnow::Chart#charter
54
+ # @param charter [String] the name of the charter.
55
+ # @return [String] the name of the chart author, the same as the argument +charter+.
56
+ # @raise [ArgumentError] if +charter+ is not a String.
57
+ def charter charter
58
+ raise ArgumentError, 'charter must be a string' unless charter.is_a? String
59
+ @charter = charter
60
+ end
61
+
62
+ # Set the name of the difficulty for the chart.
63
+ # This will be reflected in the return value of {#to_sunniesnow}.
64
+ # @see Sunniesnow::Chart#difficulty_name
65
+ # @param difficulty_name [String] the name of the difficulty.
66
+ # @return [String] the name of the difficulty, the same as the argument +difficulty_name+.
67
+ # @raise [ArgumentError] if +difficulty_name+ is not a String.
68
+ def difficulty_name difficulty_name
69
+ raise ArgumentError, 'difficulty_name must be a string' unless difficulty_name.is_a? String
70
+ @difficulty_name = difficulty_name
71
+ end
72
+
73
+ # Set the color of the difficulty for the chart.
74
+ # This will be reflected in the return value of {#to_sunniesnow}.
75
+ #
76
+ # The argument +difficulty_color+ can be a color name (a key of {COLORS}),
77
+ # an RGB color in hexadecimal format (e.g. +'#8c68f3'+, +'#8CF'+),
78
+ # an RGB color in decimal format (e.g. +'rgb(140, 104, 243)'+),
79
+ # or an integer representing an RGB color (e.g. +0x8c68f3+).
80
+ # @see Sunniesnow::Chart#difficulty_color
81
+ # @param difficulty_color [Symbol, String, Integer] the color of the difficulty.
82
+ # @return [String] the color of the difficulty in hexadecimal format (e.g. +'#8c68f3'+).
83
+ # @raise [ArgumentError] if +difficulty_color+ is not a valid color format.
84
+ def difficulty_color difficulty_color
85
+ @difficulty_color = case difficulty_color
86
+ when Symbol
87
+ COLORS[difficulty_color]
88
+ when /^#[0-9a-fA-F]{6}$/
89
+ difficulty_color
90
+ when /^#[0-9a-fA-F]{3}$/
91
+ _, r, g, b = difficulty_color.chars
92
+ "##{r}#{r}#{g}#{g}#{b}#{b}"
93
+ when /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/
94
+ r, g, b = $1, $2, $3
95
+ sprintf '#%02x%02x%02x', r.to_i, g.to_i, b.to_i
96
+ when Integer
97
+ sprintf '#%06x', difficulty_color % 0x1000000
98
+ else
99
+ raise ArgumentError, 'unknown format of difficulty_color'
100
+ end
101
+ end
102
+
103
+ # Set the difficulty level for the chart.
104
+ # This will be reflected in the return value of {#to_sunniesnow}.
105
+ #
106
+ # The argument +difficulty+ should be a string representing the difficulty level.
107
+ # Anything other than a string will be converted to a string using +to_s+.
108
+ # @see Sunniesnow::Chart#difficulty
109
+ # @param difficulty [String] the difficulty level.
110
+ # @return [String] the difficulty level (converted to a string).
111
+ def difficulty difficulty
112
+ @difficulty = difficulty.to_s
113
+ end
114
+
115
+ # Set the difficulty superscript for the chart.
116
+ # This will be reflected in the return value of {#to_sunniesnow}.
117
+ #
118
+ # The argument +difficulty_sup+ should be a string representing the difficulty superscript.
119
+ # Anything other than a string will be converted to a string using +to_s+.
120
+ # @see Sunniesnow::Chart#difficulty_sup
121
+ # @param difficulty_sup [String] the difficulty superscript.
122
+ # @return [String] the difficulty superscript (converted to a string).
123
+ def difficulty_sup difficulty_sup
124
+ @difficulty_sup = difficulty_sup.to_s
125
+ end
126
+
127
+ # @!endgroup
128
+
129
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class Sunniesnow::Charter
6
+
7
+ using Sunniesnow::Utils
8
+
9
+ IMAGE_LAYER_ABOVE = %i[none background bg_pattern hud fx judgement_line bg_notes notes circles tip_points fx_front].freeze
10
+
11
+ IMAGE_LAYER_ABOVE_SET = IMAGE_LAYER_ABOVE.to_set.freeze
12
+
13
+ COORDINATE_SYSTEMS = %i[canvas chart].freeze
14
+
15
+ COORDINATE_SYSTEMS_SET = COORDINATE_SYSTEMS.to_set.freeze
16
+
17
+ # @!group DSL Methods
18
+
19
+ # Creates an image event.
20
+ # @param filename [String]
21
+ # @param x [Numeric]
22
+ # @param y [Numeric]
23
+ # @param duration_beats [Integer, Rational]
24
+ # @param width [Numeric]
25
+ # @param height [Numeric?]
26
+ # @param above [IMAGE_LAYER_ABOVE_SET?]
27
+ # @param coordinate_system [COORDINATE_SYSTEMS_SET?]
28
+ # @param mirrorable [Boolean?]
29
+ # @return [Event]
30
+ def image filename, x, y, duration_beats, width, height = nil, above: nil, coordinate_system: nil, mirrorable: nil
31
+ raise ArgumentError, 'filename must be a string' unless filename.is_a? String
32
+ raise ArgumentError, 'x and y must be numbers' unless x.is_a?(Numeric) && y.is_a?(Numeric)
33
+ raise ArgumentError, 'duration_beats must be a number' unless duration_beats.is_a? Numeric
34
+ raise ArgumentError, 'duration_beats must be non-negative' if duration_beats < 0
35
+ raise ArgumentError, 'width must be a number' unless width.is_a? Numeric
36
+ raise ArgumentError, 'height must be a number' if !height.nil? && !height.is_a?(Numeric)
37
+ raise ArgumentError, "unknown coordinate_system #{coordinate_system}" if !coordinate_system.nil? && !%i[chart screen].include?(coordinate_system)
38
+ warn 'Rational is recommended over Float for duration_beats' if duration_beats.is_a? Float
39
+ raise ArgumentError, "invalid above: #{above}" if !above.nil? && !IMAGE_LAYER_ABOVE_SET.include?(above)
40
+ raise ArgumentError, "invalid coordinate_system: #{coordinate_system}" if !coordinate_system.nil? && !COORDINATE_SYSTEMS_SET.include?(coordinate_system)
41
+ raise ArgumentError, "mirrorable must be a boolean" unless [nil, true, false].include? mirrorable
42
+ additional_properties = {}
43
+ additional_properties[:above] = above.snake_to_camel if above
44
+ additional_properties[:coordinate_system] = coordinate_system.snake_to_camel if coordinate_system
45
+ additional_properties[:mirrorable] = mirrorable unless mirrorable.nil?
46
+ additional_properties[:height] = height.to_f if height
47
+ event :image, duration_beats.to_r, filename: filename, x: x.to_f, y: y.to_f, width: width.to_f, **additional_properties
48
+ end
49
+
50
+ # TODO: other story events
51
+
52
+ # @!endgroup
53
+
54
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Sunniesnow::Charter
4
+
5
+ # @note Internal API.
6
+ class TipPointStart
7
+
8
+ # @param relative [Boolean] whether the position at which a created tip point appears specified by the arguments +x+ and +y+
9
+ # is relative to the first note it visits or absolute.
10
+ # @param x [Numeric] the x-coordinate of the position at which a created tip point appears, whether relative or absolute.
11
+ # @param y [Numeric] the y-coordinate of the position at which a created tip point appears, whether relative or absolute.
12
+ # @overload initialize x, y, relative_time, relative: true
13
+ # @param relative_time [Numeric]
14
+ # The time at which a created tip point appears is the time of the first note it visits minus +relative_time+.
15
+ # @overload initialize x, y, speed:, relative: true
16
+ # @param speed [Numeric]
17
+ # The time at which a created tip point appears is the time of the first note it visits minus
18
+ # the distance between the note and the position where the tip point appears divided by +speed+.
19
+ # @overload initialize x, y, relative_beat:, relative: true
20
+ # @param relative_beat [Rational, Integer]
21
+ # The beat at which a created tip point appears is the beat of the first note it visits minus +relative_beat+.
22
+ # @overload initialize x, y, beat_speed:, relative: true
23
+ # @param beat_speed [Numeric]
24
+ # The beat at which a created tip point appears is the beat of the first note it visits minus
25
+ # the distance between the note and the position where the tip point appears divided by +beat_speed+.
26
+ def initialize x, y, relative_time = nil, relative: true, speed: nil, relative_beat: nil, beat_speed: nil
27
+ @x = x
28
+ @y = y
29
+ @relative_time = relative_time
30
+ @relative = relative
31
+ @speed = speed
32
+ @relative_beat = relative_beat
33
+ @beat_speed = beat_speed
34
+ check
35
+ end
36
+
37
+ # Checks that the parameters passed to {#initialize} is one of the valid overloads.
38
+ # @return [void]
39
+ # @raise [ArgumentError] if checks fail.
40
+ def check
41
+ if !@x.is_a?(Numeric) || !@y.is_a?(Numeric)
42
+ raise ArgumentError, 'x and y must be numbers'
43
+ end
44
+ @x = @x.to_f
45
+ @y = @y.to_f
46
+ %i[@relative_time @speed @relative_beat @beat_speed].each do |key|
47
+ value = instance_variable_get key
48
+ next unless value
49
+ raise ArgumentError, "cannot specify both #@time_key and #{key}" if @time_key
50
+ @time_key = key
51
+ end
52
+ raise ArgumentError, "must specify one of relative_time, speed, relative_beat, beat_speed" unless @time_key
53
+ end
54
+
55
+ # @param start_event [Event]
56
+ # @return [Event]
57
+ def get_start_placeholder start_event
58
+ raise ArgumentError, "start_event is not tip-pointable" unless start_event.tip_pointable?
59
+ result = Event.new :placeholder, start_event.beat, start_event.bpm_changes
60
+ if @relative
61
+ result[:x] = start_event[:x] + @x
62
+ result[:y] = start_event[:y] + @y
63
+ else
64
+ result[:x] = @x
65
+ result[:y] = @y
66
+ end
67
+ case @time_key
68
+ when :@relative_time
69
+ raise ArgumentError, "relative_time must be a number" unless @relative_time.is_a? Numeric
70
+ raise ArgumentError, "relative_time must be non-negative" if @relative_time < 0
71
+ result.offset = -@relative_time.to_f
72
+ when :@speed
73
+ raise ArgumentError, "speed must be a number" unless @speed.is_a? Numeric
74
+ raise ArgumentError, "speed must be positive" if @speed <= 0
75
+ result.offset = -Math.hypot(result[:x] - start_event[:x], result[:y] - start_event[:y]) / @speed
76
+ when :@relative_beat
77
+ raise ArgumentError, "relative_beat must be a number" unless @relative_beat.is_a? Numeric
78
+ raise ArgumentError, "relative_beat must be non-negative" if @relative_beat < 0
79
+ warn "Rational is recommended over Float for relative_beat" if @relative_beat.is_a? Float
80
+ result.beat -= @relative_beat.to_r
81
+ when :@beat_speed
82
+ raise ArgumentError, "beat_speed must be a number" unless @beat_speed.is_a? Numeric
83
+ raise ArgumentError, "beat_speed must be positive" if @beat_speed <= 0
84
+ delta_beat = Math.hypot(result[:x] - start_event[:x], result[:y] - start_event[:y]) / @beat_speed
85
+ result.beat -= delta_beat.to_r # a little weird, but fine
86
+ end
87
+ result[:tip_point] = start_event[:tip_point]
88
+ result
89
+ end
90
+ end
91
+
92
+ # @note Internal API.
93
+ # @param start_event [Event]
94
+ # @return [void]
95
+ def push_tip_point_start start_event
96
+ start_event[:tip_point] = @current_tip_point_stack.last.to_s
97
+ tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
98
+ return unless tip_point_start
99
+ @groups.each do |group|
100
+ group.push tip_point_start
101
+ break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
102
+ end
103
+ nil
104
+ end
105
+
106
+ # @note Internal API.
107
+ # @yieldself [Sunniesnow::Charter] the same as +self+.
108
+ def tip_point mode, *args, preserve_beat: true, **opts, &block
109
+ @tip_point_mode_stack.push mode
110
+ if mode == :none
111
+ @tip_point_start_stack.push nil
112
+ @tip_point_start_to_add_stack.push nil
113
+ @current_tip_point_stack.push nil
114
+ else
115
+ if args.empty? && opts.empty?
116
+ unless @tip_point_start_stack.last
117
+ raise ArgumentError, 'cannot omit tip point arguments at top level or inside tip_point_none'
118
+ end
119
+ @tip_point_start_stack.push @tip_point_start_stack.last.dup
120
+ else
121
+ @tip_point_start_stack.push TipPointStart.new *args, **opts
122
+ end
123
+ @tip_point_start_to_add_stack.push @tip_point_start_stack.last
124
+ @current_tip_point_stack.push nil
125
+ end
126
+ result = group preserve_beat: preserve_beat do
127
+ @current_tip_point_group_stack.push @groups.last
128
+ instance_eval &block
129
+ end
130
+ @tip_point_start_stack.pop
131
+ @tip_point_start_to_add_stack.pop
132
+ @tip_point_mode_stack.pop
133
+ @current_tip_point_stack.pop
134
+ @current_tip_point_group_stack.pop
135
+ result
136
+ end
137
+
138
+ # @!group DSL Methods
139
+
140
+ # @!parse
141
+ # # @!macro [attach] tip_point_mode
142
+ # # @!method tip_point_$1 x, y, relative_time = nil, relative: true, speed: nil, relative_beat: nil, beat_speed: nil, preserve_beat: true, &block
143
+ # # $2
144
+ # # There are four overloads of this method for different ways to specify the time at which the tip point appears,
145
+ # # and there is another overload that totally omits the arguments for specifying
146
+ # # when and where the tip point appears and can only be used inside another tip point block.
147
+ # # This method is otherwise the same as {#group}.
148
+ # #
149
+ # # If the methods {#tp_chain}, {#tp_drop}, and {#tp_none} are nested in +block+,
150
+ # # only the innermost one takes effect.
151
+ # # @example Nested tip points
152
+ # # offset 0.1; bpm 120
153
+ # # tp_chain 0, 100, 1 do
154
+ # # t 0, 0, 'A'; b 1 # tip point from above
155
+ # # tp_drop -100, 0, 1 do
156
+ # # t 25, 25, 'B'; b 1 # tip point from left
157
+ # # t 50, 25, 'C'; b 1 # tip point from left
158
+ # # end
159
+ # # tp_none do
160
+ # # t 75, 50, 'D'; b 1 # no tip point
161
+ # # end
162
+ # # t 100, 0, 'E'; b 1 # same tip point as note A
163
+ # # end
164
+ # # @param preserve_beat [Boolean] whether the {#current_beat} will be reset to the value before executing +block+ after it is executed.
165
+ # # @param relative [Boolean] whether the position at which a created tip point appears specified by the arguments +x+ and +y+
166
+ # # is relative to the first note it visits or absolute.
167
+ # # @param x [Numeric] the x-coordinate of the position at which a created tip point appears, whether relative or absolute.
168
+ # # @param y [Numeric] the y-coordinate of the position at which a created tip point appears, whether relative or absolute.
169
+ # # @return [Array<Event>] the events created inside +block+, similar to {#group}.
170
+ # # @raise [ArgumentError]
171
+ # # @yieldself [Charter] the same as +self+.
172
+ # # @overload tip_point_$1 x, y, relative_time, relative: true, preserve_beat: true, &block
173
+ # # @param relative_time [Numeric]
174
+ # # The time at which a created tip point appears is the time of the first note it visits minus +relative_time+.
175
+ # # @overload tip_point_$1 x, y, speed:, relative: true, preserve_beat: true, &block
176
+ # # @param speed [Numeric]
177
+ # # The time at which a created tip point appears is the time of the first note it visits minus
178
+ # # the distance between the note and the position where the tip point appears divided by +speed+.
179
+ # # @overload tip_point_$1 x, y, relative_beat:, relative: true, preserve_beat: true, &block
180
+ # # @param relative_beat [Rational, Integer]
181
+ # # The beat at which a created tip point appears is the beat of the first note it visits minus +relative_beat+.
182
+ # # @overload tip_point_$1 x, y, beat_speed:, relative: true, preserve_beat: true, &block
183
+ # # @param beat_speed [Numeric]
184
+ # # The beat at which a created tip point appears is the beat of the first note it visits minus
185
+ # # the distance between the note and the position where the tip point appears divided by +beat_speed+.
186
+ # # @overload tip_point_$1 preserve_beat: true, &block
187
+ # # This overload can only be used inside another tip point block,
188
+ # # and it creates a tip point with the same parameters as the one created by the outer block.
189
+ # tip_point_mode :chain, 'Create a tip point to connect the notes created inside +block+.'
190
+ # tip_point_mode :drop, 'A tip point is created for each note created inside +block+.'
191
+ # alias tp_chain tip_point_chain
192
+ # alias tp_drop tip_point_drop
193
+ # alias tp_none tip_point_none
194
+ # @!method tip_point_none preserve_beat: true, &block
195
+ # Notes created inside +block+ will not be visited by any tip point.
196
+ # This method is otherwise the same as {#group}.
197
+ # @yieldself [Charter] the same as +self+.
198
+ # @return [Array<Event>] the events created inside +block+, similar to {#group}.
199
+ %i[chain drop none].each do |mode|
200
+ define_method "tip_point_#{mode}" do |*args, **opts, &block|
201
+ tip_point mode, *args, **opts, &block
202
+ end
203
+ alias_method "tp_#{mode}", "tip_point_#{mode}"
204
+ end
205
+
206
+ # @!endgroup
207
+
208
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Sunniesnow::Charter
4
+
5
+ # The project directory.
6
+ PROJECT_DIR = File.expand_path(ENV['SSCHARTER_PROJECT_DIR'] ||= Dir.pwd).freeze
7
+
8
+ # @!scope class
9
+ # A hash containing all the charts opened by {::open}.
10
+ # The keys are the names of the charts, and the values are the {Sunniesnow::Charter} objects.
11
+ # @return [Hash{String => Sunniesnow::Charter}]
12
+ singleton_class.attr_reader :charts
13
+ @charts = {}
14
+
15
+ # Create a new chart or open an existing chart for editing.
16
+ # The +name+ is used to check whether the chart already exists.
17
+ # If a new chart needs to be created, it is added to {.charts}.
18
+ #
19
+ # The given +block+ will be evaluated in the context of the chart
20
+ # (inside the block, +self+ is the same as the return value, a {Charter} instance).
21
+ # This method is intended to be called at the top level of a Ruby script
22
+ # to open a context for writing a Sunniesnow chart using the DSL.
23
+ #
24
+ # In the examples in the documentation of other methods,
25
+ # it is assumed that they are run inside a block passed to this method.
26
+ #
27
+ # @param name [String] the name of the chart.
28
+ # @return [Charter] the chart.
29
+ # @yieldself [Charter] the chart, the same as the return value.
30
+ # @example
31
+ # Sunniesnow::Charter.open 'master' do
32
+ # # write the chart here
33
+ # end
34
+ def self.open name, &block
35
+ result = @charts[name] ||= new name
36
+ result.instance_eval &block if block
37
+ result
38
+ end
39
+
40
+ # Create a new chart.
41
+ # Usually you should use {.open} instead of this method.
42
+ # @param name [String] the name of the chart.
43
+ def initialize name
44
+ @name = name
45
+ init_chart_info
46
+ init_beat_state
47
+ init_group_state
48
+ init_bookmarks
49
+ end
50
+
51
+ # See {Sunniesnow::Chart#initialize} for the arguments.
52
+ # @return [Sunniesnow::Chart]
53
+ # @overload to_sunniesnow live_reload_port: 31108, production: false
54
+ def to_sunniesnow **opts
55
+ result = Sunniesnow::Chart.new **opts
56
+ result.title = @title
57
+ result.artist = @artist
58
+ result.charter = @charter
59
+ result.difficulty_name = @difficulty_name
60
+ result.difficulty_color = @difficulty_color
61
+ result.difficulty = @difficulty
62
+ result.difficulty_sup = @difficulty_sup
63
+ @events.each { result.events.push _1.to_sunniesnow }
64
+ result
65
+ end
66
+
67
+ # @return [String]
68
+ def output_json *args, **opts
69
+ to_sunniesnow(**opts).to_json *args
70
+ end
71
+
72
+ # @return [String]
73
+ def inspect
74
+ "#<Sunniesnow::Charter #@name>"
75
+ end
76
+
77
+ end
78
+
79
+ require_relative 'charter/metadata'
80
+ require_relative 'charter/beat'
81
+ require_relative 'charter/event'
82
+ require_relative 'charter/group'
83
+ require_relative 'charter/events_manip'
84
+ require_relative 'charter/basic_events'
85
+ require_relative 'charter/tip_point'
86
+ require_relative 'charter/story_events'
87
+ require_relative 'charter/check'