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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -0
- data/Rakefile +8 -2
- data/lib/sscharter/chart.rb +52 -8
- data/lib/sscharter/charter/basic_events.rb +277 -0
- data/lib/sscharter/charter/beat.rb +334 -0
- data/lib/sscharter/charter/check.rb +37 -0
- data/lib/sscharter/charter/event.rb +429 -0
- data/lib/sscharter/charter/events_manip.rb +175 -0
- data/lib/sscharter/charter/group.rb +130 -0
- data/lib/sscharter/charter/metadata.rb +129 -0
- data/lib/sscharter/charter/story_events.rb +54 -0
- data/lib/sscharter/charter/tip_point.rb +208 -0
- data/lib/sscharter/charter.rb +87 -0
- data/lib/sscharter/tools/svg_path.rb +549 -0
- data/lib/sscharter/tools.rb +6 -0
- data/lib/sscharter/utils.rb +19 -2
- data/lib/sscharter/version.rb +1 -1
- data/lib/sscharter.rb +2 -1074
- data/lock/ruby-3.0.7-bundler-2.5.23-Gemfile.lock +67 -0
- data/lock/ruby-3.1.7-bundler-2.6.9-Gemfile.lock +67 -0
- data/lock/ruby-3.2.11-bundler-4.0.10-Gemfile.lock +98 -0
- data/lock/ruby-3.3.11-bundler-4.0.10-Gemfile.lock +98 -0
- data/lock/ruby-3.4.9-bundler-4.0.10-Gemfile.lock +101 -0
- data/lock/ruby-4.0.2-bundler-4.0.10-Gemfile.lock +101 -0
- data/tutorial/advanced.md +33 -0
- data/tutorial/tools.md +26 -0
- data/tutorial/tutorial.md +6 -32
- metadata +64 -16
- data/Gemfile.lock +0 -48
|
@@ -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'
|