sscharter 0.7.0 → 0.8.0
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 +0 -13
- data/Gemfile.lock +7 -11
- data/LICENSE +1 -1
- data/README.md +3 -3
- data/Rakefile +12 -6
- data/lib/sscharter/chart.rb +34 -2
- data/lib/sscharter/cli.rb +8 -13
- data/lib/sscharter/version.rb +1 -1
- data/lib/sscharter.rb +520 -178
- data/tutorial/tutorial.md +71 -6
- metadata +21 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77116185ddcbeeeff551d0584bc35b8a10da9f0d7fcbae95a520b95909c355dd
|
4
|
+
data.tar.gz: 5594955220d37210bf710314b379e88d8a23a5a5174291c7d06c91cf25e37dee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b67bc6805cf36ab1d69c4b85bbe0dce05d0db8b794caa6c8cc0cc8a7f2f28bb5c840688122527f2e2b44190740756020d4308a0ba0ca3bfbec570108cdf0a108
|
7
|
+
data.tar.gz: 6523d03c7b22171f37829136a21e4a378947eed410dd9f22c825046c6ffdb71269392d65f6b6621cb7c43521e994a8b6fc1d11351d9141432898fd12e61e5dc9
|
data/Gemfile
CHANGED
@@ -2,17 +2,4 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
# Specify your gem's dependencies in sscharter.gemspec
|
6
5
|
gemspec
|
7
|
-
|
8
|
-
group :develop do
|
9
|
-
gem 'rake', '~> 13.0'
|
10
|
-
gem 'minitest', '~> 5.0'
|
11
|
-
end
|
12
|
-
|
13
|
-
gem 'rubyzip', '~> 2.3'
|
14
|
-
gem 'launchy', '~> 2.5'
|
15
|
-
gem 'webrick', '~> 1.8'
|
16
|
-
gem 'filewatcher', '~> 2.0'
|
17
|
-
gem 'em-websocket', '~> 0.5'
|
18
|
-
gem 'concurrent-ruby', '~> 1.3'
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
sscharter (0.
|
4
|
+
sscharter (0.8.0)
|
5
5
|
concurrent-ruby (~> 1.3)
|
6
6
|
em-websocket (~> 0.5)
|
7
7
|
filewatcher (~> 2.0)
|
@@ -24,27 +24,23 @@ GEM
|
|
24
24
|
http_parser.rb (0.8.0)
|
25
25
|
launchy (2.5.2)
|
26
26
|
addressable (~> 2.8)
|
27
|
-
minitest (5.25.
|
27
|
+
minitest (5.25.4)
|
28
28
|
module_methods (0.1.0)
|
29
29
|
public_suffix (6.0.1)
|
30
30
|
rake (13.2.1)
|
31
|
-
rubyzip (2.
|
32
|
-
webrick (1.
|
31
|
+
rubyzip (2.4.1)
|
32
|
+
webrick (1.9.1)
|
33
|
+
yard (0.9.37)
|
33
34
|
|
34
35
|
PLATFORMS
|
35
36
|
x64-mingw-ucrt
|
36
37
|
x86_64-linux
|
37
38
|
|
38
39
|
DEPENDENCIES
|
39
|
-
concurrent-ruby (~> 1.3)
|
40
|
-
em-websocket (~> 0.5)
|
41
|
-
filewatcher (~> 2.0)
|
42
|
-
launchy (~> 2.5)
|
43
40
|
minitest (~> 5.0)
|
44
41
|
rake (~> 13.0)
|
45
|
-
rubyzip (~> 2.3)
|
46
42
|
sscharter!
|
47
|
-
|
43
|
+
yard (~> 0.9)
|
48
44
|
|
49
45
|
BUNDLED WITH
|
50
|
-
2.
|
46
|
+
2.6.2
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# sscharter
|
2
2
|
|
3
3
|
A Ruby DSL for writing Sunniesnow charts.
|
4
4
|
|
@@ -7,7 +7,7 @@ A Ruby DSL for writing Sunniesnow charts.
|
|
7
7
|
[Install Ruby](https://www.ruby-lang.org/en/documentation/installation/).
|
8
8
|
Then, run
|
9
9
|
|
10
|
-
```
|
10
|
+
```
|
11
11
|
gem install sscharter
|
12
12
|
```
|
13
13
|
|
@@ -27,4 +27,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/sunnie
|
|
27
27
|
|
28
28
|
## Code of Conduct
|
29
29
|
|
30
|
-
Everyone interacting in the
|
30
|
+
Everyone interacting in the sscharter project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sunniesnow/sscharter/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'yard'
|
5
6
|
|
6
|
-
Rake::TestTask.new
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
Rake::TestTask.new :test do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.test_files = FileList['test/**/test_*.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
YARD::Rake::YardocTask.new do |t|
|
14
|
+
t.files.concat %w[README.md]
|
15
|
+
t.options.concat %w[--title sscharter --output-dir doc --exclude lib/sscharter/cli.rb --readme]
|
10
16
|
end
|
11
17
|
|
12
18
|
task default: :test
|
data/lib/sscharter/chart.rb
CHANGED
@@ -2,14 +2,41 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
|
5
|
+
# A class to represent a chart.
|
6
|
+
# Basically a wrapper around the metadata and events ({Sunniesnow::Event}) of a chart.
|
7
|
+
# The main method is {#to_json}, which converts the chart to a JSON string
|
8
|
+
# that is actually recognizable by Sunniesnow.
|
5
9
|
class Sunniesnow::Chart
|
6
10
|
|
7
11
|
using Sunniesnow::Utils
|
8
12
|
|
9
|
-
|
10
|
-
|
13
|
+
# The schema URL for the output JSON.
|
14
|
+
# This will be set as the `$schema` property in the generated JSON.
|
15
|
+
SCHEMA = 'https://sunniesnow.github.io/schema/chart-1.0.json'
|
16
|
+
|
17
|
+
# The title of the music.
|
18
|
+
# This is one of the metadata of the chart which will be reflected in the generated JSON.
|
19
|
+
# Also, see
|
20
|
+
# {chart file specifications}[https://sunniesnow.github.io/doc/chart.html#title]
|
21
|
+
# for more info.
|
22
|
+
attr_accessor :title
|
23
|
+
|
24
|
+
attr_accessor :artist
|
25
|
+
attr_accessor :charter
|
26
|
+
attr_accessor :difficulty_name
|
27
|
+
attr_accessor :difficulty_color
|
28
|
+
attr_accessor :difficulty
|
29
|
+
attr_accessor :difficulty_sup
|
30
|
+
|
31
|
+
# An array of Sunniesnow::Event.
|
11
32
|
attr_reader :events
|
12
33
|
|
34
|
+
# @param [Integer] live_reload_port The port to use for live reload.
|
35
|
+
# It is useless if +production+ is +true+.
|
36
|
+
# @param [Boolean] production Whether the chart is in production or not.
|
37
|
+
# If +true+, the generated JSON (see {#to_json}) will not contain
|
38
|
+
# necessary information for sscharter integration in Sunniesnow
|
39
|
+
# (such as the live reload feature and reverse search feature).
|
13
40
|
def initialize live_reload_port: 31108, production: false
|
14
41
|
@title = ''
|
15
42
|
@artist = ''
|
@@ -23,8 +50,13 @@ class Sunniesnow::Chart
|
|
23
50
|
@production = production
|
24
51
|
end
|
25
52
|
|
53
|
+
##
|
54
|
+
# Convert to JSON.
|
55
|
+
# A Sunniesnow chart is always a JSON file in the level file.
|
56
|
+
# This method is used to generate that JSON file.
|
26
57
|
def to_json *args
|
27
58
|
hash = {
|
59
|
+
'$schema': SCHEMA,
|
28
60
|
title: @title,
|
29
61
|
artist: @artist,
|
30
62
|
charter: @charter,
|
data/lib/sscharter/cli.rb
CHANGED
@@ -22,13 +22,12 @@ module Sunniesnow
|
|
22
22
|
module_function
|
23
23
|
|
24
24
|
def config
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
puts 'No .sscharter.yml found'
|
25
|
+
filenames = %w[sscharter.yml sscharter.yaml .sscharter.yml .sscharter.yaml].map { File.join PROJECT_DIR, _1 }
|
26
|
+
unless filename = filenames.find { File.exist? _1 }
|
27
|
+
$stderr.puts 'Config file sscharter.yml not found'
|
29
28
|
return nil
|
30
29
|
end
|
31
|
-
YAML.load_file
|
30
|
+
YAML.load_file filename, symbolize_names: true
|
32
31
|
end
|
33
32
|
|
34
33
|
singleton_class.attr_reader :commands
|
@@ -87,20 +86,18 @@ Sunniesnow::Charter::CLI::Subcommand.new :init, option_parser do |project_dir =
|
|
87
86
|
FileUtils.cp_r files, files_dir
|
88
87
|
FileUtils.cd project_dir do
|
89
88
|
File.write 'Gemfile', <<~GEMFILE
|
90
|
-
# frozen_string_literal: true
|
91
89
|
source 'https://rubygems.org'
|
92
90
|
gem 'sscharter', '~> #{Sunniesnow::Charter::VERSION}'
|
93
91
|
gem 'rake', '~> #{Rake::VERSION}'
|
94
92
|
gem 'bundler', '~> #{Bundler::VERSION}'
|
95
93
|
GEMFILE
|
96
94
|
File.write 'Rakefile', <<~RAKEFILE
|
97
|
-
# frozen_string_literal: true
|
98
95
|
task default: :build
|
99
96
|
task :build do
|
100
97
|
exec 'bundle exec sscharter build'
|
101
98
|
end
|
102
99
|
task :serve do
|
103
|
-
exec 'bundle exec sscharter serve'
|
100
|
+
exec 'bundle exec sscharter serve --no-open-browser'
|
104
101
|
end
|
105
102
|
RAKEFILE
|
106
103
|
File.write '.gitignore', <<~GITIGNORE
|
@@ -108,7 +105,7 @@ Sunniesnow::Charter::CLI::Subcommand.new :init, option_parser do |project_dir =
|
|
108
105
|
/tmp/
|
109
106
|
/build/
|
110
107
|
GITIGNORE
|
111
|
-
File.write '
|
108
|
+
File.write 'sscharter.yml', <<~SSCHARTER
|
112
109
|
---
|
113
110
|
project_name: #{File.basename project_dir}
|
114
111
|
build_dir: build
|
@@ -136,21 +133,19 @@ Sunniesnow::Charter::CLI::Subcommand.new :init, option_parser do |project_dir =
|
|
136
133
|
README
|
137
134
|
FileUtils.mkdir_p 'src'
|
138
135
|
File.write 'src/master.rb', <<~CHART
|
139
|
-
# frozen_string_literal: true
|
140
|
-
|
141
136
|
Sunniesnow::Charter.open 'master' do
|
142
137
|
|
143
138
|
title 'The title of the music'
|
144
139
|
artist 'The artist of the music'
|
145
140
|
charter 'Your name'
|
146
141
|
difficulty_name 'Master'
|
147
|
-
difficulty_color
|
142
|
+
difficulty_color :master
|
148
143
|
difficulty '12'
|
149
144
|
|
150
145
|
offset 0
|
151
146
|
bpm 120
|
152
147
|
|
153
|
-
tp_chain 0,
|
148
|
+
tp_chain 0, 100, 1 do
|
154
149
|
t -50, 0, 'hello'
|
155
150
|
b 1 # proceed by 1 beat
|
156
151
|
t 50, 0, 'world'
|
data/lib/sscharter/version.rb
CHANGED
data/lib/sscharter.rb
CHANGED
@@ -30,6 +30,8 @@ class Sunniesnow::Charter
|
|
30
30
|
class BpmChangeList
|
31
31
|
|
32
32
|
class BpmChange
|
33
|
+
include Comparable
|
34
|
+
|
33
35
|
attr_accessor :beat, :bps
|
34
36
|
|
35
37
|
def initialize beat, bpm
|
@@ -51,6 +53,8 @@ class Sunniesnow::Charter
|
|
51
53
|
|
52
54
|
def add beat, bpm
|
53
55
|
@list.push BpmChange.new beat, bpm
|
56
|
+
@list.sort!
|
57
|
+
self
|
54
58
|
end
|
55
59
|
|
56
60
|
def time_at beat
|
@@ -286,16 +290,6 @@ class Sunniesnow::Charter
|
|
286
290
|
}.freeze
|
287
291
|
|
288
292
|
DIRECTIONS = {
|
289
|
-
right: 0.0,
|
290
|
-
up_right: Math::PI / 4,
|
291
|
-
up: Math::PI / 2,
|
292
|
-
up_left: Math::PI * 3 / 4,
|
293
|
-
left: Math::PI,
|
294
|
-
down_left: -Math::PI * 3 / 4,
|
295
|
-
down: -Math::PI / 2,
|
296
|
-
down_right: -Math::PI / 4
|
297
|
-
}
|
298
|
-
{
|
299
293
|
right: %i[r],
|
300
294
|
up_right: %i[ur ru],
|
301
295
|
up: %i[u],
|
@@ -304,22 +298,60 @@ class Sunniesnow::Charter
|
|
304
298
|
down_left: %i[dl ld],
|
305
299
|
down: %i[d],
|
306
300
|
down_right: %i[dr rd]
|
307
|
-
}.
|
308
|
-
|
309
|
-
|
301
|
+
}.each_with_object({
|
302
|
+
right: 0.0,
|
303
|
+
up_right: Math::PI / 4,
|
304
|
+
up: Math::PI / 2,
|
305
|
+
up_left: Math::PI * 3 / 4,
|
306
|
+
left: Math::PI,
|
307
|
+
down_left: -Math::PI * 3 / 4,
|
308
|
+
down: -Math::PI / 2,
|
309
|
+
down_right: -Math::PI / 4
|
310
|
+
}) do |(direction_name, aliases), directions|
|
311
|
+
aliases.each { directions[_1] = directions[direction_name] }
|
312
|
+
end.freeze
|
313
|
+
|
310
314
|
DIRECTIONS.freeze
|
311
315
|
|
312
|
-
|
316
|
+
class << self
|
317
|
+
# A hash containing all the charts opened by {::open}.
|
318
|
+
# The keys are the names of the charts, and the values are the {Sunniesnow::Charter} objects.
|
319
|
+
# @return [Hash<String, Sunniesnow::Charter>]
|
320
|
+
attr_reader :charts
|
321
|
+
end
|
313
322
|
@charts = {}
|
314
323
|
|
324
|
+
# An array of events.
|
325
|
+
# @return [Array<Sunniesnow::Charter::Event>]
|
315
326
|
attr_reader :events
|
316
327
|
|
328
|
+
# Create a new chart or open an existing chart for editing.
|
329
|
+
# The +name+ is used to check whether the chart already exists.
|
330
|
+
# If a new chart needs to be created, it is added to {.charts}.
|
331
|
+
#
|
332
|
+
# The given +block+ will be evaluated in the context of the chart
|
333
|
+
# (inside the block, +self+ is the same as the return value, a {Charter} instance).
|
334
|
+
# This method is intended to be called at the top level of a Ruby script
|
335
|
+
# to open a context for writing a Sunniesnow chart using the DSL.
|
336
|
+
#
|
337
|
+
# In the examples in the documentation of other methods,
|
338
|
+
# it is assumed that they are run inside a block passed to this method.
|
339
|
+
#
|
340
|
+
# @param name [String] the name of the chart.
|
341
|
+
# @return [Sunniesnow::Charter] the chart.
|
342
|
+
# @example
|
343
|
+
# Sunniesnow::Charter.open 'master' do
|
344
|
+
# # write the chart here
|
345
|
+
# end
|
317
346
|
def self.open name, &block
|
318
347
|
result = @charts[name] ||= new name
|
319
348
|
result.instance_eval &block if block
|
320
349
|
result
|
321
350
|
end
|
322
351
|
|
352
|
+
# Create a new chart.
|
353
|
+
# Usually you should use {.open} instead of this method.
|
354
|
+
# @param name [String] the name of the chart.
|
323
355
|
def initialize name
|
324
356
|
@name = name
|
325
357
|
init_chart_info
|
@@ -350,30 +382,192 @@ class Sunniesnow::Charter
|
|
350
382
|
@current_tip_point_group_stack = []
|
351
383
|
@tip_point_peak = 0
|
352
384
|
@current_duplicate = 0
|
385
|
+
@tip_point_start_stack = [nil]
|
353
386
|
@tip_point_start_to_add_stack = [nil]
|
354
387
|
@groups = [@events]
|
355
388
|
end
|
356
389
|
|
390
|
+
def to_sunniesnow **opts
|
391
|
+
result = Sunniesnow::Chart.new **opts
|
392
|
+
result.title = @title
|
393
|
+
result.artist = @artist
|
394
|
+
result.charter = @charter
|
395
|
+
result.difficulty_name = @difficulty_name
|
396
|
+
result.difficulty_color = @difficulty_color
|
397
|
+
result.difficulty = @difficulty
|
398
|
+
result.difficulty_sup = @difficulty_sup
|
399
|
+
@events.each { result.events.push _1.to_sunniesnow }
|
400
|
+
result
|
401
|
+
end
|
402
|
+
|
403
|
+
def output_json **opts
|
404
|
+
to_sunniesnow(**opts).to_json
|
405
|
+
end
|
406
|
+
|
407
|
+
def inspect
|
408
|
+
"#<Sunniesnow::Charter #@name>"
|
409
|
+
end
|
410
|
+
|
411
|
+
def time_at beat = @current_beat
|
412
|
+
raise OffsetError.new __method__ unless @bpm_changes
|
413
|
+
@bpm_changes.time_at beat
|
414
|
+
end
|
415
|
+
|
416
|
+
def backup_beat
|
417
|
+
{current_beat: @current_beat, bpm_changes: @bpm_changes}
|
418
|
+
end
|
419
|
+
|
420
|
+
def restore_beat backup
|
421
|
+
@current_beat = backup[:current_beat]
|
422
|
+
@bpm_changes = backup[:bpm_changes]
|
423
|
+
end
|
424
|
+
|
425
|
+
def backup_state
|
426
|
+
{
|
427
|
+
current_beat: @current_beat,
|
428
|
+
bpm_changes: @bpm_changes,
|
429
|
+
tip_point_mode_stack: @tip_point_mode_stack.dup,
|
430
|
+
current_tip_point_stack: @current_tip_point_stack.dup,
|
431
|
+
current_tip_point_group_stack: @current_tip_point_group_stack.dup,
|
432
|
+
current_duplicate: @current_duplicate,
|
433
|
+
tip_point_start_stack: @tip_point_start_stack.dup,
|
434
|
+
tip_point_start_to_add_stack: @tip_point_start_to_add_stack.dup,
|
435
|
+
groups: @groups.dup
|
436
|
+
}
|
437
|
+
end
|
438
|
+
|
439
|
+
def restore_state backup
|
440
|
+
@current_beat = backup[:current_beat]
|
441
|
+
@bpm_changes = backup[:bpm_changes]
|
442
|
+
@tip_point_mode_stack = backup[:tip_point_mode_stack]
|
443
|
+
@current_tip_point_stack = backup[:current_tip_point_stack]
|
444
|
+
@current_tip_point_group_stack = backup[:current_tip_point_group_stack]
|
445
|
+
@current_duplicate = backup[:current_duplicate]
|
446
|
+
@tip_point_start_to_add_stack = backup[:tip_point_start_to_add_stack]
|
447
|
+
@groups = backup[:groups]
|
448
|
+
nil
|
449
|
+
end
|
450
|
+
|
451
|
+
def event type, duration_beats = nil, **properties
|
452
|
+
raise OffsetError.new __method__ unless @bpm_changes
|
453
|
+
event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
|
454
|
+
@groups.each { _1.push event }
|
455
|
+
return event unless event.tip_pointable?
|
456
|
+
case @tip_point_mode_stack.last
|
457
|
+
when :chain
|
458
|
+
if @tip_point_start_to_add_stack.last
|
459
|
+
@current_tip_point_stack[-1] = @tip_point_peak
|
460
|
+
@tip_point_peak += 1
|
461
|
+
end
|
462
|
+
push_tip_point_start event
|
463
|
+
@tip_point_start_to_add_stack[-1] = nil
|
464
|
+
when :drop
|
465
|
+
@current_tip_point_stack[-1] = @tip_point_peak
|
466
|
+
@tip_point_peak += 1
|
467
|
+
push_tip_point_start event
|
468
|
+
when :none
|
469
|
+
# pass
|
470
|
+
end
|
471
|
+
event
|
472
|
+
end
|
473
|
+
|
474
|
+
def push_tip_point_start start_event
|
475
|
+
start_event[:tip_point] = @current_tip_point_stack.last.to_s
|
476
|
+
tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
|
477
|
+
return unless tip_point_start
|
478
|
+
@groups.each do |group|
|
479
|
+
group.push tip_point_start
|
480
|
+
break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def tip_point mode, *args, preserve_beat: true, **opts, &block
|
485
|
+
@tip_point_mode_stack.push mode
|
486
|
+
if mode == :none
|
487
|
+
@tip_point_start_stack.push nil
|
488
|
+
@tip_point_start_to_add_stack.push nil
|
489
|
+
@current_tip_point_stack.push nil
|
490
|
+
else
|
491
|
+
if args.empty? && opts.empty?
|
492
|
+
unless @tip_point_start_stack.last
|
493
|
+
raise TipPointError, 'cannot omit tip point arguments at top level or inside tip_point_none'
|
494
|
+
end
|
495
|
+
@tip_point_start_stack.push @tip_point_start_stack.last.dup
|
496
|
+
else
|
497
|
+
@tip_point_start_stack.push TipPointStart.new *args, **opts
|
498
|
+
end
|
499
|
+
@tip_point_start_to_add_stack.push @tip_point_start_stack.last
|
500
|
+
@current_tip_point_stack.push nil
|
501
|
+
end
|
502
|
+
result = group preserve_beat: do
|
503
|
+
@current_tip_point_group_stack.push @groups.last
|
504
|
+
instance_eval &block
|
505
|
+
end
|
506
|
+
@tip_point_start_stack.pop
|
507
|
+
@tip_point_start_to_add_stack.pop
|
508
|
+
@tip_point_mode_stack.pop
|
509
|
+
@current_tip_point_stack.pop
|
510
|
+
@current_tip_point_group_stack.pop
|
511
|
+
result
|
512
|
+
end
|
513
|
+
|
514
|
+
# @!group DSL methods
|
515
|
+
|
516
|
+
# Set the title of the music for the chart.
|
517
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
518
|
+
# @see Sunniesnow::Chart#title
|
519
|
+
# @param title [String] the title of the music.
|
520
|
+
# @return [String] the title of the music, the same as the argument +title+.
|
521
|
+
# @raise [ArgumentError] if +title+ is not a String.
|
357
522
|
def title title
|
358
523
|
raise ArgumentError, 'title must be a string' unless title.is_a? String
|
359
524
|
@title = title
|
360
525
|
end
|
361
526
|
|
527
|
+
# Set the artist of the music for the chart.
|
528
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
529
|
+
# @see Sunniesnow::Chart#artist
|
530
|
+
# @param artist [String] the artist of the music.
|
531
|
+
# @return [String] the artist of the music, the same as the argument +artist+.
|
532
|
+
# @raise [ArgumentError] if +artist+ is not a String.
|
362
533
|
def artist artist
|
363
534
|
raise ArgumentError, 'artist must be a string' unless artist.is_a? String
|
364
535
|
@artist = artist
|
365
536
|
end
|
366
537
|
|
538
|
+
# Set the name of the chart author for the chart.
|
539
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
540
|
+
# @see Sunniesnow::Chart#charter
|
541
|
+
# @param charter [String] the name of the charter.
|
542
|
+
# @return [String] the name of the chart author, the same as the argument +charter+.
|
543
|
+
# @raise [ArgumentError] if +charter+ is not a String.
|
367
544
|
def charter charter
|
368
545
|
raise ArgumentError, 'charter must be a string' unless charter.is_a? String
|
369
546
|
@charter = charter
|
370
547
|
end
|
371
548
|
|
549
|
+
# Set the name of the difficulty for the chart.
|
550
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
551
|
+
# @see Sunniesnow::Chart#difficulty_name
|
552
|
+
# @param difficulty_name [String] the name of the difficulty.
|
553
|
+
# @return [String] the name of the difficulty, the same as the argument +difficulty_name+.
|
554
|
+
# @raise [ArgumentError] if +difficulty_name+ is not a String.
|
372
555
|
def difficulty_name difficulty_name
|
373
556
|
raise ArgumentError, 'difficulty_name must be a string' unless difficulty_name.is_a? String
|
374
557
|
@difficulty_name = difficulty_name
|
375
558
|
end
|
376
559
|
|
560
|
+
# Set the color of the difficulty for the chart.
|
561
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
562
|
+
#
|
563
|
+
# The argument +difficulty_color+ can be a color name (a key of {COLORS}),
|
564
|
+
# an RGB color in hexadecimal format (e.g. +'#8c68f3'+, +'#8CF'+),
|
565
|
+
# an RGB color in decimal format (e.g. +'rgb(140, 104, 243)'+),
|
566
|
+
# or an integer representing an RGB color (e.g. +0x8c68f3+).
|
567
|
+
# @see Sunniesnow::Chart#difficulty_color
|
568
|
+
# @param difficulty_color [Symbol, String, Integer] the color of the difficulty.
|
569
|
+
# @return [String] the color of the difficulty in hexadecimal format (e.g. +'#8c68f3'+).
|
570
|
+
# @raise [ArgumentError] if +difficulty_color+ is not a valid color format.
|
377
571
|
def difficulty_color difficulty_color
|
378
572
|
@difficulty_color = case difficulty_color
|
379
573
|
when Symbol
|
@@ -393,25 +587,98 @@ class Sunniesnow::Charter
|
|
393
587
|
end
|
394
588
|
end
|
395
589
|
|
590
|
+
# Set the difficulty level for the chart.
|
591
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
592
|
+
#
|
593
|
+
# The argument +difficulty+ should be a string representing the difficulty level.
|
594
|
+
# Anything other than a string will be converted to a string using +to_s+.
|
595
|
+
# @see Sunniesnow::Chart#difficulty
|
596
|
+
# @param difficulty [String] the difficulty level.
|
597
|
+
# @return [String] the difficulty level (converted to a string).
|
396
598
|
def difficulty difficulty
|
397
599
|
@difficulty = difficulty.to_s
|
398
600
|
end
|
399
601
|
|
602
|
+
# Set the difficulty superscript for the chart.
|
603
|
+
# This will be reflected in the return value of {#to_sunniesnow}.
|
604
|
+
#
|
605
|
+
# The argument +difficulty_sup+ should be a string representing the difficulty superscript.
|
606
|
+
# Anything other than a string will be converted to a string using +to_s+.
|
607
|
+
# @see Sunniesnow::Chart#difficulty_sup
|
608
|
+
# @param difficulty_sup [String] the difficulty superscript.
|
609
|
+
# @return [String] the difficulty superscript (converted to a string).
|
400
610
|
def difficulty_sup difficulty_sup
|
401
611
|
@difficulty_sup = difficulty_sup.to_s
|
402
612
|
end
|
403
613
|
|
614
|
+
# Set the offset.
|
615
|
+
# This is the time in seconds of the zeroth beat.
|
616
|
+
# This method must be called before any other methods that require a beat,
|
617
|
+
# or an {OffsetError} will be raised.
|
618
|
+
#
|
619
|
+
# After calling this method, the current beat (see {#beat} and {#beat!}) is set to zero,
|
620
|
+
# and a new BPM needs to be set using {#bpm}.
|
621
|
+
# Only after that can the time of any positive beat be calculated.
|
622
|
+
#
|
623
|
+
# Though not commonly useful, this method can be called multiple times in a chart.
|
624
|
+
# A new call of this method does not affect the events and BPM changes set before.
|
625
|
+
# Technically, each event is associated with a BPM change list (see {Event#bpm_changes}),
|
626
|
+
# and each call of this method creates a new BPM change list,
|
627
|
+
# which is used for the events set after.
|
628
|
+
# @param offset [Numeric] the offset in seconds.
|
629
|
+
# @return [BpmChangeList] the BPM changes.
|
630
|
+
# @see BpmChangeList
|
631
|
+
# @raise [ArgumentError] if +offset+ is not a number.
|
632
|
+
# @example
|
633
|
+
# offset 0.1
|
634
|
+
# p time_at # Outputs 0.1, which is the offset
|
635
|
+
# offset 0.2
|
636
|
+
# p time_at # Outputs 0.2, which is the updated offset by the second call
|
404
637
|
def offset offset
|
405
638
|
raise ArgumentError, 'offset must be a number' unless offset.is_a? Numeric
|
406
639
|
@current_beat = 0r
|
407
640
|
@bpm_changes = BpmChangeList.new offset.to_f
|
408
641
|
end
|
409
642
|
|
643
|
+
# Set the BPM starting at the current beat.
|
644
|
+
# This method must be called after {#offset}.
|
645
|
+
# The method can be called multiple times,
|
646
|
+
# which is useful when the music changes its tempo from time to time.
|
647
|
+
#
|
648
|
+
# Internally, this simply calls {BpmChangeList#add} on the BPM changes created by {#offset}.
|
649
|
+
# @param bpm [Numeric] the BPM.
|
650
|
+
# @raise [OffsetError] if {#offset} has not been called.
|
651
|
+
# @return [BpmChangeList] the BPM changes.
|
410
652
|
def bpm bpm
|
411
653
|
raise OffsetError.new __method__ unless @bpm_changes
|
412
654
|
@bpm_changes.add @current_beat, bpm
|
413
655
|
end
|
414
656
|
|
657
|
+
# Increments the current beat by the given delta set by +delta_beat+.
|
658
|
+
# It is recommended that +delta_beat+ be a Rational or an Integer for accuracy.
|
659
|
+
# Float will be converted to Rational, and a warning will be issued
|
660
|
+
# when a Float is used.
|
661
|
+
#
|
662
|
+
# This method is also useful for inspecting the current beat.
|
663
|
+
# If the method is called without an argument, it simply returns the current beat.
|
664
|
+
# For this purpose, this method is equivalent to {#beat!}.
|
665
|
+
#
|
666
|
+
# This method must be called after {#offset}.
|
667
|
+
# @param delta_beat [Rational, Integer] the delta to increment the current beat by.
|
668
|
+
# @raise [OffsetError] if {#offset} has not been called.
|
669
|
+
# @return [Rational] the new current beat.
|
670
|
+
# @see #beat!
|
671
|
+
# @example Increment the current beat and inspect it
|
672
|
+
# offset 0.1; bpm 120
|
673
|
+
# p b # Outputs 0, this is the initial value
|
674
|
+
# p b 1 # Outputs 1, because it is incremented by 1 when it was 0
|
675
|
+
# p b 1/2r # Outputs 3/2, because it is incremented by 3/2 when it was 1
|
676
|
+
# p time_at # Outputs 0.85, which is offset + 60s / BPM * beat
|
677
|
+
# @example Time the notes
|
678
|
+
# offset 0.1; bpm 120
|
679
|
+
# t 0, 0; b 1
|
680
|
+
# t 50, 0; b 1
|
681
|
+
# # Now there are two tap notes, one at beat 0, and the other at beat 1
|
415
682
|
def beat delta_beat = 0
|
416
683
|
raise OffsetError.new __method__ unless @current_beat
|
417
684
|
case delta_beat
|
@@ -426,6 +693,24 @@ class Sunniesnow::Charter
|
|
426
693
|
end
|
427
694
|
alias b beat
|
428
695
|
|
696
|
+
# Sets the current beat to the given value.
|
697
|
+
# It is recommended that +beat+ be a Rational or an Integer for accuracy.
|
698
|
+
# Float will be converted to Rational, and a warning will be issued.
|
699
|
+
#
|
700
|
+
# When called without an argument, this method does nothing and returns the current beat.
|
701
|
+
# For this purpose, this method is equivalent to {#beat}.
|
702
|
+
#
|
703
|
+
# This method must be called after {#offset}.
|
704
|
+
# @param beat [Rational, Integer] the new current beat.
|
705
|
+
# @raise [OffsetError] if {#offset} has not been called.
|
706
|
+
# @return [Rational] the new current beat.
|
707
|
+
# @see #beat
|
708
|
+
# @example Set the current beat and inspect it
|
709
|
+
# offset 0.1; bpm 120
|
710
|
+
# p b! # Outputs 0, this is the initial value
|
711
|
+
# p b! 1 # Outputs 1, because it is set to 1
|
712
|
+
# p b! 1/2r # Outputs 1/2, because it is set to 1/2
|
713
|
+
# p time_at # Outputs 0.35, which is offset + 60s / BPM * beat
|
429
714
|
def beat! beat = @current_beat
|
430
715
|
raise OffsetError.new __method__ unless @current_beat
|
431
716
|
case beat
|
@@ -440,156 +725,24 @@ class Sunniesnow::Charter
|
|
440
725
|
end
|
441
726
|
alias b! beat!
|
442
727
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
@bpm_changes = backup[:bpm_changes]
|
462
|
-
end
|
463
|
-
|
464
|
-
def group preserve_beat: true, &block
|
465
|
-
raise ArgumentError, 'no block given' unless block
|
466
|
-
@groups.push result = []
|
467
|
-
beat_backup = backup_beat unless preserve_beat
|
468
|
-
instance_eval &block
|
469
|
-
restore_beat beat_backup unless preserve_beat
|
470
|
-
@groups.delete_if { result.equal? _1 }
|
471
|
-
result
|
472
|
-
end
|
473
|
-
|
474
|
-
def backup_state
|
475
|
-
{
|
476
|
-
current_beat: @current_beat,
|
477
|
-
bpm_changes: @bpm_changes,
|
478
|
-
tip_point_mode_stack: @tip_point_mode_stack.dup,
|
479
|
-
current_tip_point_stack: @current_tip_point_stack.dup,
|
480
|
-
current_tip_point_group_stack: @current_tip_point_group_stack.dup,
|
481
|
-
current_duplicate: @current_duplicate,
|
482
|
-
tip_point_start_to_add_stack: @tip_point_start_to_add_stack.dup,
|
483
|
-
groups: @groups.dup
|
484
|
-
}
|
485
|
-
end
|
486
|
-
|
487
|
-
def restore_state backup
|
488
|
-
@current_beat = backup[:current_beat]
|
489
|
-
@bpm_changes = backup[:bpm_changes]
|
490
|
-
@tip_point_mode_stack = backup[:tip_point_mode_stack]
|
491
|
-
@current_tip_point_stack = backup[:current_tip_point_stack]
|
492
|
-
@current_tip_point_group_stack = backup[:current_tip_point_group_stack]
|
493
|
-
@current_duplicate = backup[:current_duplicate]
|
494
|
-
@tip_point_start_to_add_stack = backup[:tip_point_start_to_add_stack]
|
495
|
-
@groups = backup[:groups]
|
496
|
-
nil
|
497
|
-
end
|
498
|
-
|
499
|
-
def mark name
|
500
|
-
@bookmarks[name] = backup_state
|
501
|
-
name
|
502
|
-
end
|
503
|
-
|
504
|
-
def at name, preserve_beat: false, update_mark: false, &block
|
505
|
-
raise ArgumentError, 'no block given' unless block
|
506
|
-
raise ArgumentError, "unknown bookmark #{name}" unless bookmark = @bookmarks[name]
|
507
|
-
backup = backup_state
|
508
|
-
restore_state bookmark
|
509
|
-
result = group &block
|
510
|
-
mark name if update_mark
|
511
|
-
beat_backup = backup_beat if preserve_beat
|
512
|
-
restore_state backup
|
513
|
-
restore_beat beat_backup if preserve_beat
|
514
|
-
result
|
515
|
-
end
|
516
|
-
|
517
|
-
def tip_point mode, *args, preserve_beat: true, **opts, &block
|
518
|
-
@tip_point_mode_stack.push mode
|
519
|
-
if mode == :none
|
520
|
-
@tip_point_start_to_add_stack.push nil
|
521
|
-
@current_tip_point_stack.push nil
|
522
|
-
else
|
523
|
-
@tip_point_start_to_add_stack.push TipPointStart.new *args, **opts
|
524
|
-
@current_tip_point_stack.push @tip_point_peak
|
525
|
-
@tip_point_peak += 1
|
526
|
-
end
|
527
|
-
result = group preserve_beat: do
|
528
|
-
@current_tip_point_group_stack.push @groups.last
|
529
|
-
instance_eval &block
|
530
|
-
end
|
531
|
-
@tip_point_start_to_add_stack.pop
|
532
|
-
@tip_point_mode_stack.pop
|
533
|
-
@current_tip_point_stack.pop
|
534
|
-
@current_tip_point_group_stack.pop
|
535
|
-
result
|
536
|
-
end
|
537
|
-
|
538
|
-
def remove *events
|
539
|
-
events.each { |event| @groups.each { _1.delete event } }
|
540
|
-
end
|
541
|
-
|
542
|
-
def event type, duration_beats = nil, **properties
|
543
|
-
raise OffsetError.new __method__ unless @bpm_changes
|
544
|
-
event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
|
545
|
-
@groups.each { _1.push event }
|
546
|
-
return event unless event.tip_pointable?
|
547
|
-
case @tip_point_mode_stack.last
|
548
|
-
when :chain
|
549
|
-
push_tip_point_start event
|
550
|
-
@tip_point_start_to_add_stack[-1] = nil
|
551
|
-
when :drop
|
552
|
-
push_tip_point_start event
|
553
|
-
@current_tip_point_stack[-1] = @tip_point_peak
|
554
|
-
@tip_point_peak += 1
|
555
|
-
when :none
|
556
|
-
# pass
|
557
|
-
end
|
558
|
-
event
|
559
|
-
end
|
560
|
-
|
561
|
-
def push_tip_point_start start_event
|
562
|
-
start_event[:tip_point] = @current_tip_point_stack.last.to_s
|
563
|
-
tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
|
564
|
-
return unless tip_point_start
|
565
|
-
@groups.each do |group|
|
566
|
-
group.push tip_point_start
|
567
|
-
break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
|
568
|
-
end
|
569
|
-
end
|
570
|
-
|
571
|
-
def transform events, &block
|
572
|
-
raise ArgumentError, 'no block given' unless block
|
573
|
-
events = [events] if events.is_a? Event
|
574
|
-
transform = Transform.new
|
575
|
-
transform.instance_eval &block
|
576
|
-
events.each { transform.apply _1 }
|
577
|
-
end
|
578
|
-
|
579
|
-
def duplicate events, new_tip_points: true
|
580
|
-
result = []
|
581
|
-
events.each do |event|
|
582
|
-
next if event.type == :placeholder && !new_tip_points
|
583
|
-
result.push event = event.dup
|
584
|
-
if event[:tip_point] && new_tip_points
|
585
|
-
event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
|
586
|
-
end
|
587
|
-
@groups.each { _1.push event }
|
588
|
-
end
|
589
|
-
@current_duplicate += 1 if new_tip_points
|
590
|
-
result
|
591
|
-
end
|
592
|
-
|
728
|
+
# Creates a tap note at the given coordinates with the given text.
|
729
|
+
# The coordinates +x+ and +y+ must be numbers.
|
730
|
+
# The argument +text+ is the text to be displayed on the note
|
731
|
+
# (it is converted to a string via +to_s+ if it is not a string).
|
732
|
+
#
|
733
|
+
# Technically, this adds an event of type +:tap+ to the chart at the current time
|
734
|
+
# with properties containing the information provided by +x+, +y+, and +text+.
|
735
|
+
# @param x [Numeric] the x-coordinate of the note.
|
736
|
+
# @param y [Numeric] the y-coordinate of the note.
|
737
|
+
# @param text [String] the text to be displayed on the note.
|
738
|
+
# @return [Event] the event representing the tap note.
|
739
|
+
# @raise [ArgumentError] if +x+ or +y+ is not a number.
|
740
|
+
# @example
|
741
|
+
# offset 0.1; bpm 120
|
742
|
+
# t 0, 0, 'Hello'
|
743
|
+
# t 50, 0, 'world'
|
744
|
+
# # Now there are two simultaneous tap notes at (0, 0) and (50, 0)
|
745
|
+
# # with texts 'Hello' and 'world' respectively
|
593
746
|
def tap x, y, text = ''
|
594
747
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
595
748
|
raise ArgumentError, 'x and y must be numbers'
|
@@ -598,6 +751,29 @@ class Sunniesnow::Charter
|
|
598
751
|
end
|
599
752
|
alias t tap
|
600
753
|
|
754
|
+
# Creates a hold note at the given coordinates with the given duration and text.
|
755
|
+
# The coordinates +x+ and +y+ must be numbers.
|
756
|
+
# The argument +duration_beats+ is the duration of the hold note in beats.
|
757
|
+
# It needs to be a positive Rational or Integer.
|
758
|
+
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
759
|
+
# The argument +text+ is the text to be displayed on the note
|
760
|
+
# (it is converted to a string via +to_s+ if it is not a string).
|
761
|
+
#
|
762
|
+
# Technically, this adds an event of type +:hold+ to the chart at the current time
|
763
|
+
# with properties containing the information provided by +x+, +y+, +duration_beats+, and +text+.
|
764
|
+
# @param x [Numeric] the x-coordinate of the note.
|
765
|
+
# @param y [Numeric] the y-coordinate of the note.
|
766
|
+
# @param duration_beats [Rational, Integer] the duration of the hold note in beats.
|
767
|
+
# @param text [String] the text to be displayed on the note.
|
768
|
+
# @return [Event] the event representing the hold note.
|
769
|
+
# @raise [ArgumentError] if +x+, +y+, or +duration_beats+ is not a number,
|
770
|
+
# or if +duration_beats+ is not positive.
|
771
|
+
# @example
|
772
|
+
# offset 0.1; bpm 120
|
773
|
+
# h 0, 0, 1, 'Hello'
|
774
|
+
# h 50, 0, 2, 'world'
|
775
|
+
# # Now there are two hold notes at (0, 0) and (50, 0)
|
776
|
+
# # with durations 1 and 2 beats and texts 'Hello' and 'world' respectively
|
601
777
|
def hold x, y, duration_beats, text = ''
|
602
778
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
|
603
779
|
raise ArgumentError, 'x, y, and duration must be numbers'
|
@@ -612,6 +788,20 @@ class Sunniesnow::Charter
|
|
612
788
|
end
|
613
789
|
alias h hold
|
614
790
|
|
791
|
+
# Creates a drag note at the given coordinates.
|
792
|
+
# The coordinates +x+ and +y+ must be numbers.
|
793
|
+
#
|
794
|
+
# Technically, this adds an event of type +:drag+ to the chart at the current time
|
795
|
+
# with properties containing the information provided by +x+ and +y+.
|
796
|
+
# @param x [Numeric] the x-coordinate of the note.
|
797
|
+
# @param y [Numeric] the y-coordinate of the note.
|
798
|
+
# @return [Event] the event representing the drag note.
|
799
|
+
# @raise [ArgumentError] if +x+ or +y+ is not a number.
|
800
|
+
# @example
|
801
|
+
# offset 0.1; bpm 120
|
802
|
+
# d 0, 0
|
803
|
+
# d 50, 0
|
804
|
+
# # Now there are two drag notes at (0, 0) and (50, 0)
|
615
805
|
def drag x, y
|
616
806
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
617
807
|
raise ArgumentError, 'x and y must be numbers'
|
@@ -620,6 +810,32 @@ class Sunniesnow::Charter
|
|
620
810
|
end
|
621
811
|
alias d drag
|
622
812
|
|
813
|
+
# Creates a flick note at the given coordinates with the given direction and text.
|
814
|
+
# The coordinates +x+ and +y+ must be numbers.
|
815
|
+
# The argument +direction+ is the direction of the flick note in radians or a symbol.
|
816
|
+
# If it is a symbol, it should be one of the keys of {DIRECTIONS}
|
817
|
+
# (which are +:right+, +:up_right+, etc., abbreviated as +:r+, +:ur+ etc.).
|
818
|
+
# If it is a number, it should be a number representing the angle in radians,
|
819
|
+
# specifying the angle rorated anticlockwise starting from the positive x-direction.
|
820
|
+
# The argument +text+ is the text to be displayed on the note
|
821
|
+
# (it is converted to a string via +to_s+ if it is not a string).
|
822
|
+
#
|
823
|
+
# Technically, this adds an event of type +:flick+ to the chart at the current time
|
824
|
+
# with properties containing the information provided by +x+, +y+, +direction+, and +text+.
|
825
|
+
# @param x [Numeric] the x-coordinate of the note.
|
826
|
+
# @param y [Numeric] the y-coordinate of the note.
|
827
|
+
# @param direction [Numeric, Symbol] the direction of the flick note in radians or a symbol.
|
828
|
+
# @param text [String] the text to be displayed on the note.
|
829
|
+
# @return [Event] the event representing the flick note.
|
830
|
+
# @raise [ArgumentError] if +x+ or +y+ is not a number,
|
831
|
+
# if +direction+ is not a symbol or a number,
|
832
|
+
# or if the direction is a symbol that does not represent a known direction.
|
833
|
+
# @example
|
834
|
+
# offset 0.1; bpm 120
|
835
|
+
# f 0, 0, :r, 'Hello'
|
836
|
+
# f 50, 0, Math::PI / 4, 'world'
|
837
|
+
# # Now there are two flick notes at (0, 0) and (50, 0)
|
838
|
+
# # with directions right and up right and texts 'Hello' and 'world' respectively
|
623
839
|
def flick x, y, direction, text = ''
|
624
840
|
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
625
841
|
raise ArgumentError, 'x and y must be numbers'
|
@@ -637,6 +853,32 @@ class Sunniesnow::Charter
|
|
637
853
|
end
|
638
854
|
alias f flick
|
639
855
|
|
856
|
+
# Creates a background note at the given coordinates with the given duration and text.
|
857
|
+
# The coordinates +x+ and +y+ must be numbers.
|
858
|
+
# The argument +duration_beats+ is the duration of the background note in beats.
|
859
|
+
# It needs to be a non-negative Rational or Integer.
|
860
|
+
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
861
|
+
# The argument +text+ is the text to be displayed on the note
|
862
|
+
# (it is converted to a string via +to_s+ if it is not a string).
|
863
|
+
#
|
864
|
+
# Both the +duration_beats+ and the +text+ arguments are optional.
|
865
|
+
# When there are three arguments given in total,
|
866
|
+
# the method determines whether the third is +duration_beats+ or +text+ based on its type.
|
867
|
+
#
|
868
|
+
# Technically, this adds an event of type +:bg_note+ to the chart at the current time
|
869
|
+
# with properties containing the information provided by +x+, +y+, +duration_beats+, and +text+.
|
870
|
+
# @param x [Numeric] the x-coordinate of the note.
|
871
|
+
# @param y [Numeric] the y-coordinate of the note.
|
872
|
+
# @param duration_beats [Rational, Integer] the duration of the background note in beats.
|
873
|
+
# @param text [String] the text to be displayed on the note.
|
874
|
+
# @return [Event] the event representing the background note.
|
875
|
+
# @raise [ArgumentError] if +x+, +y+, or +duration_beats+ is not a number,
|
876
|
+
# or if +duration_beats+ is negative.
|
877
|
+
# @example
|
878
|
+
# offset 0.1; bpm 120
|
879
|
+
# bg_note 0, 0, 1, 'Hello' # duration is 1, text is 'Hello'
|
880
|
+
# bg_note 50, 0, 'world' # duration is 0, text is 'world'
|
881
|
+
# bg_note -50, 0, 2 # duration is 2, text is ''
|
640
882
|
def bg_note x, y, duration_beats = 0, text = nil
|
641
883
|
if text.nil?
|
642
884
|
if duration_beats.is_a? String
|
@@ -658,6 +900,23 @@ class Sunniesnow::Charter
|
|
658
900
|
event :bg_note, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
|
659
901
|
end
|
660
902
|
|
903
|
+
# Creates a big text.
|
904
|
+
# The argument +duration_beats+ is the duration of the big text in beats.
|
905
|
+
# It needs to be a non-negative Rational or Integer.
|
906
|
+
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
907
|
+
# The argument +text+ is the text to be displayed.
|
908
|
+
#
|
909
|
+
# Technically, this adds an event of type +:big_text+ to the chart at the current time
|
910
|
+
# with properties containing the information provided by +duration_beats+ and +text+.
|
911
|
+
# @param duration_beats [Rational, Integer] the duration of the big text in beats.
|
912
|
+
# @param text [String] the text to be displayed.
|
913
|
+
# @return [Event] the event representing the big text.
|
914
|
+
# @raise [ArgumentError] if +duration_beats+ is not a number or is negative.
|
915
|
+
# @example
|
916
|
+
# offset 0.1; bpm 120
|
917
|
+
# big_text 1, 'Hello, world!' # duration is 1, text is 'Hello, world!'
|
918
|
+
# b 1
|
919
|
+
# big_text 'Goodbye!' # duration is 0, text is 'Goodbye!'
|
661
920
|
def big_text duration_beats = 0, text
|
662
921
|
unless duration_beats.is_a? Numeric
|
663
922
|
raise ArgumentError, 'duration_beats must be a number'
|
@@ -671,6 +930,30 @@ class Sunniesnow::Charter
|
|
671
930
|
event :big_text, duration_beats.to_r, text: text.to_s
|
672
931
|
end
|
673
932
|
|
933
|
+
# @!macro [attach] bg_pattern
|
934
|
+
# @!method $1(duration_beats = 0)
|
935
|
+
# Creates a $2 background pattern.
|
936
|
+
# The argument +duration_beats+ is the duration of the background pattern in beats.
|
937
|
+
# It needs to be a non-negative Rational or Integer.
|
938
|
+
# If it is a Float, it will be converted to a Rational, and a warning will be issued.
|
939
|
+
#
|
940
|
+
# Technically, this adds an event of type +:bg_pattern+ to the chart at the current time
|
941
|
+
# with properties containing the information provided by +duration_beats+.
|
942
|
+
# @param duration_beats [Rational, Integer] the duration of the background pattern in beats.
|
943
|
+
# @return [Event] the event representing the background pattern.
|
944
|
+
# @raise [ArgumentError] if +duration_beats+ is not a number or is negative.
|
945
|
+
# @example
|
946
|
+
# offset 0.1; bpm 120
|
947
|
+
# $1 1 # duration is 1
|
948
|
+
# b 1
|
949
|
+
# $1 0 # duration is 0
|
950
|
+
# @!parse bg_pattern :grid, 'grid'
|
951
|
+
# @!parse bg_pattern :hexagon, 'hexagon'
|
952
|
+
# @!parse bg_pattern :checkerboard, 'checkerboard'
|
953
|
+
# @!parse bg_pattern :diamond_grid, 'diamond grid'
|
954
|
+
# @!parse bg_pattern :pentagon, 'pentagon'
|
955
|
+
# @!parse bg_pattern :turntable, 'turntable'
|
956
|
+
# @!parse bg_pattern :hexagram, 'hexagram'
|
674
957
|
%i[grid hexagon checkerboard diamond_grid pentagon turntable hexagram].each do |method_name|
|
675
958
|
define_method method_name do |duration_beats = 0|
|
676
959
|
unless duration_beats.is_a? Numeric
|
@@ -686,25 +969,82 @@ class Sunniesnow::Charter
|
|
686
969
|
end
|
687
970
|
end
|
688
971
|
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
972
|
+
# Duplicate all events in a given array.
|
973
|
+
# This method is useful when you want to duplicate a set of events.
|
974
|
+
# The argument +events+ is an array of events to be duplicated.
|
975
|
+
# The argument +new_tip_points+ is a boolean indicating whether to create new tip points.
|
976
|
+
# If it is +true+, new tip points will be created for the duplicated events.
|
977
|
+
# If it is +false+, each duplicated event shares the same tip point as the original event.
|
978
|
+
# @param events [Array<Event>] the events to be duplicated.
|
979
|
+
# @param new_tip_points [Boolean] whether to create new tip points for the duplicated events.
|
980
|
+
# @return [Array<Event>] the duplicated events.
|
981
|
+
# @example Duplicate a note
|
982
|
+
# offset 0.1; bpm 120
|
983
|
+
# duplicate [t 0, 0]
|
984
|
+
# @example Duplicate notes that share tip points with the original notes
|
985
|
+
# offset 0.1; bpm 120
|
986
|
+
# duplicate tp_chain(0, 100, 1) { t 0, 0 }
|
987
|
+
def duplicate events, new_tip_points: true
|
988
|
+
result = []
|
989
|
+
events.each do |event|
|
990
|
+
next if event.type == :placeholder && !new_tip_points
|
991
|
+
result.push event = event.dup
|
992
|
+
if event[:tip_point] && new_tip_points
|
993
|
+
event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
|
994
|
+
end
|
995
|
+
@groups.each { _1.push event }
|
996
|
+
end
|
997
|
+
@current_duplicate += 1 if new_tip_points
|
699
998
|
result
|
700
999
|
end
|
701
1000
|
|
702
|
-
|
703
|
-
|
1001
|
+
# Transform all events in a given array in time and/or space.
|
1002
|
+
# Space transformation does not affect background patterns.
|
1003
|
+
def transform events, &block
|
1004
|
+
raise ArgumentError, 'no block given' unless block
|
1005
|
+
events = [events] if events.is_a? Event
|
1006
|
+
transform = Transform.new
|
1007
|
+
transform.instance_eval &block
|
1008
|
+
events.each { transform.apply _1 }
|
704
1009
|
end
|
705
1010
|
|
706
|
-
def
|
707
|
-
|
1011
|
+
def group preserve_beat: true, &block
|
1012
|
+
raise ArgumentError, 'no block given' unless block
|
1013
|
+
@groups.push result = []
|
1014
|
+
beat_backup = backup_beat unless preserve_beat
|
1015
|
+
instance_eval &block
|
1016
|
+
restore_beat beat_backup unless preserve_beat
|
1017
|
+
@groups.delete_if { result.equal? _1 }
|
1018
|
+
result
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
def remove *events
|
1022
|
+
events.each { |event| @groups.each { _1.delete event } }
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
%i[chain drop none].each do |mode|
|
1026
|
+
define_method "tip_point_#{mode}" do |*args, **opts, &block|
|
1027
|
+
tip_point mode, *args, **opts, &block
|
1028
|
+
end
|
1029
|
+
alias_method "tp_#{mode}", "tip_point_#{mode}"
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
def mark name
|
1033
|
+
@bookmarks[name] = backup_state
|
1034
|
+
name
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
def at name, preserve_beat: false, update_mark: false, &block
|
1038
|
+
raise ArgumentError, 'no block given' unless block
|
1039
|
+
raise ArgumentError, "unknown bookmark #{name}" unless bookmark = @bookmarks[name]
|
1040
|
+
backup = backup_state
|
1041
|
+
restore_state bookmark
|
1042
|
+
result = group &block
|
1043
|
+
mark name if update_mark
|
1044
|
+
beat_backup = backup_beat if preserve_beat
|
1045
|
+
restore_state backup
|
1046
|
+
restore_beat beat_backup if preserve_beat
|
1047
|
+
result
|
708
1048
|
end
|
709
1049
|
|
710
1050
|
def check(
|
@@ -734,4 +1074,6 @@ class Sunniesnow::Charter
|
|
734
1074
|
end
|
735
1075
|
end
|
736
1076
|
|
1077
|
+
# @!endgroup
|
1078
|
+
|
737
1079
|
end
|
data/tutorial/tutorial.md
CHANGED
@@ -107,7 +107,7 @@ big-d
|
|
107
107
|
├── README.md
|
108
108
|
├── src
|
109
109
|
│ └── master.rb
|
110
|
-
└──
|
110
|
+
└── sscharter.yml
|
111
111
|
```
|
112
112
|
|
113
113
|
Here are some explanation for each of them.
|
@@ -121,13 +121,13 @@ see [the official documentation](https://bundler.io/man/gemfile.5.html).
|
|
121
121
|
If you do not use Git as the version manager for your project, it does nothing.
|
122
122
|
- `Rakefile` contains some tasks that can be run by [Rake](https://ruby.github.io/rake/).
|
123
123
|
- `README.md` is the README file of your project.
|
124
|
-
-
|
124
|
+
- `sscharter.yml` is the sscharter configuration for your project.
|
125
125
|
- `files` is the directory whose files will be included in the final level file.
|
126
|
-
You can change the directory in
|
126
|
+
You can change the directory in `sscharter.yml`.
|
127
127
|
- `src` is the directory that contains the source codes of your project.
|
128
|
-
You can change the directory in
|
128
|
+
You can change the directory in `sscharter.yml`.
|
129
129
|
|
130
|
-
Open
|
130
|
+
Open `sscharter.yml` using your text editor.
|
131
131
|
Here are the contents that you should see:
|
132
132
|
|
133
133
|
```yaml
|
@@ -332,7 +332,7 @@ Then, edit `Rakefile` to disable browser launching and enable live restart:
|
|
332
332
|
|
333
333
|
```ruby
|
334
334
|
task :serve do
|
335
|
-
exec 'bundle exec sscharter serve --no-open-browser
|
335
|
+
exec 'bundle exec sscharter serve --no-open-browser'
|
336
336
|
end
|
337
337
|
```
|
338
338
|
|
@@ -952,6 +952,56 @@ end
|
|
952
952
|
# write something here...
|
953
953
|
```
|
954
954
|
|
955
|
+
Sometimes even `preserve_beat: false` is not convenient enough to write complicated note patterns.
|
956
|
+
The `mark` and `at` methods can help you with more complicated charts.
|
957
|
+
|
958
|
+
Use `mark` to mark a place and label it with a symbol of your choice
|
959
|
+
(e.g. `:left_hand`), and use `at` to write notes at that point.
|
960
|
+
|
961
|
+
```ruby
|
962
|
+
group, preserve_beat: false do
|
963
|
+
# write something here...
|
964
|
+
mark :left_hand # mark the current place with the label `:left_hand`
|
965
|
+
end
|
966
|
+
|
967
|
+
group do
|
968
|
+
# write something here...
|
969
|
+
mark :right_hand # mark the current place with the label `:right_hand`
|
970
|
+
end
|
971
|
+
|
972
|
+
at :left_hand do
|
973
|
+
# write notes here as if continuing where `mark :left_hand` is
|
974
|
+
end
|
975
|
+
|
976
|
+
at :right_hand do
|
977
|
+
# write notes here as if continuing where `mark :right_hand` is
|
978
|
+
end
|
979
|
+
```
|
980
|
+
|
981
|
+
You can also use `at` with `update_mark: true` to update the mark
|
982
|
+
to the place where the block in `at` is ended.
|
983
|
+
|
984
|
+
```ruby
|
985
|
+
group do
|
986
|
+
# ...
|
987
|
+
mark :x
|
988
|
+
# ...
|
989
|
+
end
|
990
|
+
|
991
|
+
at :x, update_mark: true do
|
992
|
+
# ...
|
993
|
+
end
|
994
|
+
|
995
|
+
at :x do
|
996
|
+
# as if continuing the last `at :x, update_mark: true` block
|
997
|
+
end
|
998
|
+
```
|
999
|
+
|
1000
|
+
Usually, when `at` finishes, the current beat is set
|
1001
|
+
to whatever it was before `at` was called.
|
1002
|
+
You can keep the current beat by using `preserve_beat: true`
|
1003
|
+
in the call of `at`.
|
1004
|
+
|
955
1005
|
### BPM changes
|
956
1006
|
|
957
1007
|
> [!NOTE]
|
@@ -1191,6 +1241,21 @@ transform duplicate notes, new_tip_points: false do
|
|
1191
1241
|
end
|
1192
1242
|
```
|
1193
1243
|
|
1244
|
+
When you use `mark` inside a tip point block,
|
1245
|
+
the tip point state is also preserved when you use `at` later.
|
1246
|
+
For example:
|
1247
|
+
|
1248
|
+
```ruby
|
1249
|
+
tp_chain 0, 100, 1 do
|
1250
|
+
t 0, 0; b 1
|
1251
|
+
mark :my_mark
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
at :my_mark do
|
1255
|
+
t 100, 0 # this note will be connected by the same tip point
|
1256
|
+
end
|
1257
|
+
```
|
1258
|
+
|
1194
1259
|
## Advanced charting techniques
|
1195
1260
|
|
1196
1261
|
TODO.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sscharter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ulysses Zhan
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
@@ -122,7 +122,21 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '13.0'
|
125
|
-
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: yard
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.9'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.9'
|
139
|
+
description:
|
126
140
|
email:
|
127
141
|
- ulysseszhan@gmail.com
|
128
142
|
executables:
|
@@ -148,7 +162,7 @@ licenses: []
|
|
148
162
|
metadata:
|
149
163
|
homepage_uri: https://github.com/sunniesnow/sscharter
|
150
164
|
source_code_uri: https://github.com/sunniesnow/sscharter
|
151
|
-
post_install_message:
|
165
|
+
post_install_message:
|
152
166
|
rdoc_options: []
|
153
167
|
require_paths:
|
154
168
|
- lib
|
@@ -163,8 +177,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
177
|
- !ruby/object:Gem::Version
|
164
178
|
version: '0'
|
165
179
|
requirements: []
|
166
|
-
rubygems_version: 3.5.
|
167
|
-
signing_key:
|
180
|
+
rubygems_version: 3.5.16
|
181
|
+
signing_key:
|
168
182
|
specification_version: 4
|
169
183
|
summary: A Ruby DSL for writing Sunniesnow charts
|
170
184
|
test_files: []
|