sscharter 0.1.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.
@@ -0,0 +1,1019 @@
1
+ # Tutorial for sscharter
2
+
3
+ ## Introduction
4
+
5
+ Sscharter is a Ruby DSL serving as a charting tool for
6
+ [Sunniesnow](https://sunniesnow.github.io).
7
+ Different from other charting tools, sscharter does not have a GUI.
8
+
9
+ By using a programming language, charters can write charts more creatively.
10
+ If the chart has many patterns, it can be much more efficient than using a GUI charting tool.
11
+ However, programming languages can be difficult to learn.
12
+
13
+ ## Prerequisites
14
+
15
+ Here are some prerequisites for using sscharter.
16
+ Check all of them before continuing this tutorial:
17
+
18
+ - You need to have a basic knowledge of how to use a command line interface (CLI).
19
+ If you do not, you may find [this introduction](https://tutorial.djangogirls.org/en/intro_to_command_line/) helpful.
20
+ - You need to [install Ruby](https://www.ruby-lang.org/en/documentation/installation/).
21
+ - You need to has a basic understanding of how to program.
22
+ If you have programmed before, you will be fine because Ruby is a easy-to-begin language.
23
+ If you have not, [this](https://pine.fm/LearnToProgram/)
24
+ is a good place to start.
25
+ - You need a basic audio editing software and a basic text editing software.
26
+ If you do not have any, you can use
27
+ [Audacity](https://www.audacityteam.org/) and
28
+ [Visual Studio Code](https://code.visualstudio.com/).
29
+ - You need to be familiar with the game Sunniesnow
30
+ (which I assume you do because otherwise you would not be reading this now).
31
+ Better, you should be familiar with how a level file of Sunniesnow is structured
32
+ by reading [the documentation](https://sunniesnow.github.io/doc/).
33
+
34
+ ## Install sscharter
35
+
36
+ Assume you have installed Ruby.
37
+ Then, run
38
+
39
+ ```shell
40
+ gem install sscharter
41
+ ```
42
+
43
+ You are now good to go!
44
+
45
+ ## Step 0: decide the music
46
+
47
+ You need to decide the music you want to chart for!
48
+ Because you are assumed to be familiar with rhythm games,
49
+ I do not need to tell you what kinds of music are suitable for rhythm games.
50
+ However, I need to remind you that before you start charting, either
51
+
52
+ - you make sure the artist announced publicly that anyone can chart for the music
53
+ or permitted some sorts of usage that covers charting, or
54
+ - you got contact with the artist,
55
+ explained to them that you want to chart for the music and how you would distribute your chart,
56
+ and got their permission.
57
+
58
+ If you did not do any of the above, you need to keep in mind that you are a bad boy
59
+ and may face some sort of consequences
60
+ (I do not know. I am not a lawyer).
61
+
62
+ For this tutorial, I will use
63
+ [*Big-D* by Shaolin Dub](https://freemusicarchive.org/music/Shaolin_Dub/joint-force/big-d/).
64
+ It is licensed under [CC-BY-NC-ND-4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/).
65
+ You can download it from the link or use the following command
66
+ (if you have installed [cURL](https://curl.se/)).
67
+ By downloading it, you accept its license.
68
+
69
+ ```shell
70
+ curl https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/MQ8JO0BqKl2UADzKg74rwoY7mapqBT4uWpQYciTJ.mp3 -o 'Shaolin Dub - Big-D.mp3'
71
+ ```
72
+
73
+ ## Step 1: create a project
74
+
75
+ If you have already [installed sscharter](#install-sscharter)
76
+ and downloaded the audio file for your music using the command above,
77
+ you can now create a project for your chart by running the following command:
78
+
79
+ ```shell
80
+ sscharter init big-d 'Shaolin Dub - Big-D.mp3'
81
+ ```
82
+
83
+ The parameter `big-d` is the path to the project directory,
84
+ and its basename will be set as the name of the project
85
+ (but you are free to change it later).
86
+ The parameter `Shaolin Dub - Big-D.mp3` is the path to the audio file.
87
+ You can continue to add more parameters to the command
88
+ to include more files that you want t o include in the final level file.
89
+ See [the documentation](https://sunniesnow.github.io/doc/)
90
+ for what you may want to include.
91
+ If you forget to include some of the files that you want to include
92
+ at this step, you can add them later whenever you want.
93
+
94
+ After running the command, the audio file will be copied into the project directory,
95
+ and it is no longer needed for the rest of the tutorial.
96
+ You may delete it if you want.
97
+
98
+ Open the directory `big-d`, and you will see this directory structure:
99
+
100
+ ```plain
101
+ big-d
102
+ ├── files
103
+ │   └── Shaolin Dub - Big-D.mp3
104
+ ├── Gemfile
105
+ ├── .gitignore
106
+ ├── Rakefile
107
+ ├── README.md
108
+ ├── src
109
+ │   └── master.rb
110
+ └── .sscharter.yml
111
+ ```
112
+
113
+ Here are some explanation for each of them.
114
+
115
+ - `Gemfile` specifies the gem dependencies of this project.
116
+ By default your project depends on sscharter, bundler, and rake.
117
+ You can add more dependencies if you want.
118
+ For more information about it,
119
+ see [the official documentation](https://bundler.io/man/gemfile.5.html).
120
+ - `.gitignore` specifies the files that should not be tracked by Git.
121
+ If you do not use Git as the version manager for your project, it does nothing.
122
+ - `Rakefile` contains some tasks that can be run by [Rake](https://ruby.github.io/rake/).
123
+ - `README.md` is the README file of your project.
124
+ - `.sscharter.yml` is the sscharter configuration for your project.
125
+ - `files` is the directory whose files will be included in the final level file.
126
+ You can change the directory in `.sscharter.yml`.
127
+ - `src` is the directory that contains the source codes of your project.
128
+ You can change the directory in `.sscharter.yml`.
129
+
130
+ Open `.sscharter.yml` using your text editor.
131
+ Here are the contents that you should see:
132
+
133
+ ```yaml
134
+ ---
135
+ project_name: big-d
136
+ build_dir: build
137
+ files_dir: files
138
+ sources_dir: src
139
+ include:
140
+ - README.md
141
+ ```
142
+
143
+ Here are some explanation for each of them.
144
+
145
+ - `project_name` is the name of your project.
146
+ It will be the filename without extension of the generated level file.
147
+ - `build_dir` is the directory that will contain the generated level file.
148
+ - `files_dir` is the directory whose files will be included in the final level file.
149
+ - `sources_dir` is the directory that contains the source codes of your project.
150
+ - `include` is the list of files outside `files_dir` that will be included in the final level file.
151
+ You can use wildcard characters in the filenames.
152
+
153
+ You can also use your text editor to open the file `src/master.rb` for what a source codes file look like:
154
+
155
+ ```ruby
156
+ # frozen_string_literal: true
157
+
158
+ Sunniesnow::Charter.open 'master' do
159
+
160
+ title 'The title of the music'
161
+ artist 'The artist of the music'
162
+ charter 'Your name'
163
+ difficulty_name 'Master'
164
+ difficulty_color '#8c68f3'
165
+ difficulty '12'
166
+
167
+ offset 0
168
+ bpm 120
169
+
170
+ tp_chain 0, 0, 1 do
171
+ t -50, 0, 'hello'
172
+ b 1 # proceed by 1 beat
173
+ t 50, 0, 'world'
174
+ end
175
+
176
+ end
177
+ ```
178
+
179
+ You will learn what these codes mean soon later!
180
+
181
+ ## Step 2: set up workflow
182
+
183
+ Now, in the project directory, run
184
+
185
+ ```shell
186
+ bundle install
187
+ ```
188
+
189
+ It should be done quickly if your network is good.
190
+ Change the gem source in `Gemfile` if you have trouble retrieving from the default source.
191
+
192
+ After that, run
193
+
194
+ ```shell
195
+ rake
196
+ ```
197
+
198
+ Then, you should see a file `build/big-d.ssc` generated.
199
+ This file is a level file of Sunniesnow.
200
+ You can upload it on the webpage of Sunniesnow to play it.
201
+
202
+ However, it is kind of inconvenient if you have to run `rake`
203
+ and reupload the level file every time you make a change to the source codes.
204
+ You can use the following command to make it more convenient:
205
+
206
+ ```shell
207
+ rake serve
208
+ ```
209
+
210
+ This command will open the Sunniesnow webpage in your browser for you.
211
+ The `online` field of Sunniesnow is already filled with the address to the generated level file.
212
+ Every time you save changes to the source codes,
213
+ the program will automatically rebuild the level file.
214
+ You just need to hit "load" again to reload the level file.
215
+
216
+ The port of the local server is 8011 by default.
217
+ If you need to change the port to 1314 for example, you need to run
218
+
219
+ ```shell
220
+ bundle exec sscharter serve 1314
221
+ ```
222
+
223
+ ## What does each line in `src/master.rb` mean?
224
+
225
+ Now, you are ready to write the chart!
226
+ Open `src/master.rb` using your text editor.
227
+ Here I explain what does each line mean.
228
+
229
+ ```ruby
230
+ Sunniesnow::Charter.open 'master' do
231
+ # ...
232
+ end
233
+ ```
234
+
235
+ This means that you are opening a chart called "master".
236
+ This will lead to a chart file called `master.json` in the generated level file.
237
+ You can open multiple charts in a single source codes file,
238
+ and you can also open the same chart in multiple source codes files
239
+ (but in that case the loading order matters).
240
+
241
+ The contents inside the `do`...`end` block are the contents of the chart.
242
+ You can write anything you like there.
243
+
244
+ ```ruby
245
+ title 'The title of the music'
246
+ artist 'The artist of the music'
247
+ charter 'Your name'
248
+ difficulty_name 'Master'
249
+ difficulty_color '#8c68f3'
250
+ difficulty '12'
251
+ ```
252
+
253
+ These lines are the metadata of the chart.
254
+ They already explains pretty much themselves.
255
+ Just fill them in!
256
+
257
+ By the way, there is a trick about the `difficulty_color` for your convenience.
258
+ If you want default difficulty colors from Lyrica, you can use one of these:
259
+
260
+ ```ruby
261
+ difficulty_color :easy
262
+ difficulty_color :normal
263
+ difficulty_color :hard
264
+ difficulty_color :master
265
+ difficulty_color :special
266
+ ```
267
+
268
+ Then, here comes the offset and BPM part:
269
+
270
+ ```ruby
271
+ offset 0
272
+ bpm 120
273
+ ```
274
+
275
+ This means:
276
+
277
+ - The zeroth beat of the music starts at time equaling to 0 seconds.
278
+ - The BPM of the music starting at the zeroth beat is 120.
279
+
280
+ You can change the BPM later at any beat by using `bpm`.
281
+ You can also use `offset` multiple times, but every time you run `offset`,
282
+ the beat is reset to zero, and you have to set the BPM over again.
283
+
284
+ Now here comes the main part:
285
+
286
+ ```ruby
287
+ tp_chain 0, 0, 1 do
288
+ t -50, 0, 'hello'
289
+ b 1 # proceed by 1 beat
290
+ t 50, 0, 'world'
291
+ end
292
+ ```
293
+
294
+ This means:
295
+
296
+ - Start a tip point chain.
297
+ The tip point spawns at:
298
+ - coordinates $(0,0)$ relative to the first tip-pointable event in the chain, and
299
+ - time 1 second before the first tip-pointable event in the chain.
300
+ - Add a tap note at coordinates $(-50,0)$ to the tip point chain.
301
+ The tap note has text "hello" on it.
302
+ - Proceed by 1 beat.
303
+ Now the current beat is 1 instead of 0.
304
+ - Add a tap note at coordinates $(50,0)$ to the tip point chain.
305
+ The tap note has text "world" on it.
306
+ - End the tip point chain.
307
+
308
+ They are actually very straightforward!
309
+
310
+ Now, you are ready to write your own chart.
311
+
312
+ ## Step 3: write the chart metadata
313
+
314
+ Delete all contents inside the outmost `do`...`end` block.
315
+ Then, write the metadata of the chart:
316
+
317
+ ```ruby
318
+ title 'Big-D'
319
+ artist 'Shaolin Dub'
320
+ charter 'UlyssesZhan' # replace this with your name
321
+ difficulty_name 'Master'
322
+ difficulty_color :master
323
+ difficulty '10'
324
+ ```
325
+
326
+ You are then writing the chart for the master difficulty,
327
+ with difficulty being 10.
328
+
329
+ Now, the whole source codes file should look like this:
330
+
331
+ ```ruby
332
+ # frozen_string_literal: true
333
+
334
+ Sunniesnow::Charter.open 'master' do
335
+
336
+ title 'Big-D'
337
+ artist 'Shaolin Dub'
338
+ charter 'UlyssesZhan' # replace this with your name
339
+ difficulty_name 'Master'
340
+ difficulty_color :master
341
+ difficulty '10'
342
+
343
+ end
344
+ ```
345
+
346
+ ## Step 4: determine the offset and BPM
347
+
348
+ Now, you need to determine the offset and BPM of the music
349
+ and write the information into the source codes file.
350
+ Use your favorite audio editing software to open the music
351
+ (I use Audacity here).
352
+
353
+ To determine them, inside a section of the music with consistent BPM,
354
+ find a beat that is clear to recognize (up to milliseconds precision for the best).
355
+
356
+ ![First clear beat](https://i.imgur.com/rI5UXgm.png)
357
+
358
+ Then, find the time of the beat.
359
+ You can read off the time at the "Audio Position" field in Audacity,
360
+ which says 7.023 seconds here.
361
+
362
+ Then, find the last beat that is clear to recognize.
363
+
364
+ ![Last clear beat](https://i.imgur.com/eJw6ZaJ.png)
365
+
366
+ Then, find the time of the beat.
367
+ It is 180.071 seconds.
368
+
369
+ You now need to know how many beats are there between the two beats.
370
+ You can first approximately measure the length of, say, 8 beats,
371
+ and then divide the length between the two beats by the length of 8 beats
372
+ to estimate how many "8 beats" are there.
373
+ Because we can easily hear that number of beats modulo 4 is 2,
374
+ we can use the fact to check our result of calculation
375
+ (the result of the division should be close to something point 25 or something point 75).
376
+ The result should be approximately $49.75$.
377
+ Then, we can calculate the BPM now:
378
+
379
+ ```math
380
+ 60\,\mathrm{s/min}\cdot
381
+ \frac{49.75\times8\,\mathrm{beats}}{180.071\,\mathrm s-7.023\,\mathrm s}
382
+ \approx137.996\,\mathrm{beats/min}.
383
+ ```
384
+
385
+ So the BPM is approximately 138.
386
+
387
+ To get the offset, we just need to extrapolate the time of the zeroth beat.
388
+ We can count that the first clear beat we chose is the 16th beat,
389
+ so we can use the BPM and the time of the 16th beat to extrapolate the time of the zeroth beat:
390
+
391
+ ```math
392
+ 7.023\,\mathrm s-16\,\mathrm{beats}\cdot
393
+ \frac{60\,\mathrm{s/min}}{138\,\mathrm{beats/min}}
394
+ \approx0.066\,\mathrm s.
395
+ ```
396
+
397
+ Now that we got the offset and BPM, we can write them into the source codes file:
398
+
399
+ ```ruby
400
+ offset 0.066
401
+ bpm 138
402
+ ```
403
+
404
+ The whole source codes file should now look like this:
405
+
406
+ ```ruby
407
+ # frozen_string_literal: true
408
+
409
+ Sunniesnow::Charter.open 'master' do
410
+
411
+ title 'Big-D'
412
+ artist 'Shaolin Dub'
413
+ charter 'UlyssesZhan' # replace this with your name
414
+ difficulty_name 'Master'
415
+ difficulty_color :master
416
+ difficulty '10'
417
+
418
+ offset 0.066
419
+ bpm 138
420
+
421
+ end
422
+ ```
423
+
424
+ ## Step 5: write the chart
425
+
426
+ Now, you are ready to write the main part of the chart!
427
+
428
+ ### Write notes
429
+
430
+ Write the following code:
431
+
432
+ ```ruby
433
+ hold -25, 0, 1/2r # a hold note at (-25,0) with the duration of 1/2 beats
434
+ beat 3/4r # go to beat 3/4
435
+ hold 25, 0, 1/2r # a hold note at (25,0) with the duration of 1/2 beats
436
+ beat 1/4r + 2 # go to beat 3
437
+ hold 0, 25, 1/2r # a hold note at (0,25) with the duration of 1/2 beats
438
+ beat 1 # go to beat 4
439
+
440
+ hold -25, -25, 1/2r
441
+ beat 3/4r
442
+ hold 25, -25, 1/2r
443
+ beat 1/4r + 2
444
+ hold 0, -50, 1/2r
445
+ beat 1
446
+ ```
447
+
448
+ By writing these, you added 6 hold notes to the chart.
449
+ Each `hold` adds a hold note to the chart.
450
+ The first argument is the $x$-coordinate of the hold note,
451
+ the second argument is the $y$-coordinate of the hold note,
452
+ and the third argument is the duration of the hold note in beats.
453
+ There is an optional fourth argument, which is the text on the hold note
454
+ (defaults to empty string).
455
+ Each `beat` call proceeds the current beat by the argument you specifies.
456
+
457
+ > Here is a notice for those who are not familiar with Ruby:
458
+ expression like `1/2r` means the rational number $1/2$.
459
+ In sscharter, you need to specify beats in integers or rational numbers
460
+ to avoid rounding errors.
461
+ Here is an example of how rational numbers avoid rounding errors:
462
+ >
463
+ > ```ruby
464
+ > 1/10r + 1/5r == 3/10r # => true
465
+ > 0.1 + 0.2 == 0.3 # => false
466
+ > ```
467
+ >
468
+ > The rounding error would not be huge even if being added up many times,
469
+ but it is still better to avoid it.
470
+ >
471
+ > If you are tired of appending `r` to every rational number, you may use
472
+ >
473
+ > ```ruby
474
+ > Integer.alias_method :/, :quo
475
+ > ```
476
+ >
477
+ > to make the division operation between integers automatically return rational numbers.
478
+
479
+ You can also abbreviate `hold` to `h` and `beat` to `b`, like this:
480
+
481
+ ```ruby
482
+ h -25, 0, 1/2r; b 3/4r
483
+ h 25, 0, 1/2r; b 1/4r + 2
484
+ h 0, 25, 1/2r; b 1
485
+ ```
486
+
487
+ > Here is another notice for those who are not familiar with Ruby.
488
+ In Ruby, the semicolon `;` can be used to separate multiple statements in a single line.
489
+ Therefore, `h -25, 0, 1/2r; b 3/4r` is equivalent to
490
+ >
491
+ > ```ruby
492
+ > h -25, 0, 1/2r
493
+ > b 3/4r
494
+ > ```
495
+
496
+ Here is a list of all the note types you can use:
497
+
498
+ | Note type | Syntax | Abbreviation |
499
+ |-|-|-|
500
+ | tap | `tap x, y, text=""` | `t` |
501
+ | hold | `hold x, y, duration_beats, text=""` | `h` |
502
+ | drag | `drag x, y` | `d` |
503
+ | flick | `flick x, y, direction, text=""` | `f` |
504
+ | background note | `bg_note x, y, duration_beats=0, text=""` | |
505
+
506
+ Here are some notices:
507
+
508
+ - The direction of a flick note can be a number specifying the angle in **radians**
509
+ (zero is to the right, and increasing angle is counterclockwise),
510
+ or it can be one of the following 8 symbols:
511
+ `:right`, `:up_right`, `:up`, `:up_left`, `:left`, `:down_left`, `:down`, `:down_right`.
512
+ - The duration of a background note can be zero, but that of a hold note cannot.
513
+
514
+ > Here is another notice for those who are not familiar with Ruby.
515
+ > In Ruby, the parentheses around the arguments of a method call can be omitted.
516
+ > Therefore, `tap 25, 0, 'hi'` is equivalent to `tap(25, 0, 'hi')`.
517
+
518
+ Now, the whole source codes file should look like this:
519
+
520
+ ```ruby
521
+ # frozen_string_literal: true
522
+
523
+ Sunniesnow::Charter.open 'master' do
524
+
525
+ title 'Big-D'
526
+ artist 'Shaolin Dub'
527
+ charter 'UlyssesZhan' # replace this with your name
528
+ difficulty_name 'Master'
529
+ difficulty_color :master
530
+ difficulty '10'
531
+
532
+ offset 0.066
533
+ bpm 138
534
+
535
+ h -25, 0, 1/2r; b 3/4r
536
+ h 25, 0, 1/2r; b 1/4r + 2
537
+ h 0, 25, 1/2r; b 1
538
+
539
+ h -25, -25, 1/2r; b 3/4r
540
+ h 25, -25, 1/2r; b 1/4r + 2
541
+ h 0, -50, 1/2r; b 1
542
+
543
+ end
544
+ ```
545
+
546
+ You may build the level file and play it on Sunniesnow to see what you have done!
547
+ If everything is alright, you should see a chart like this:
548
+
549
+ ![The chart](https://i.imgur.com/zPUtgGr.png)
550
+
551
+ If you see something similar, congratulations!
552
+
553
+ ### Background patterns
554
+
555
+ There are totally 7 different kinds of background patterns:
556
+
557
+ | Pattern | Syntax |
558
+ |-|-|
559
+ | big text | `big_text duration_beats=0, text` |
560
+ | grid | `grid duration_beats=0` |
561
+ | hexagon | `hexagon duration_beats=0` |
562
+ | checkerboard | `checkerboard duration_beats=0` |
563
+ | diamond grid | `diamond_grid duration_beats=0` |
564
+ | pentagon | `pentagon duration_beats=0` |
565
+ | turntable | `turntable duration_beats=0` |
566
+
567
+ Most of them only has one optional argument, specifying the duration.
568
+ The big text is different in that it needs an additional required argument,
569
+ specifying the contents of the texts.
570
+
571
+ The mathematical details about the shapes of these patterns are
572
+ specified in [the documentation](https://sunniesnow.github.io/doc/chart.html).
573
+
574
+ You may now try adding a grid pattern to the chart:
575
+
576
+ ```ruby
577
+ offset 0.066
578
+ bpm 138
579
+
580
+ grid 16 # add a grid pattern with duration 16 beats
581
+
582
+ # notes...
583
+ h -25, 0, 1/2r; b 3/4r
584
+ ```
585
+
586
+ ### Groups, duplicates, and transformations
587
+
588
+ You have already written the chart for the first 8 beats of *Big-D*.
589
+ Then, you want to have the same for the next 8 beats.
590
+ How would you do that?
591
+
592
+ Well, an obvious solution is to copy and paste the code you have written.
593
+ However, this has some drawbacks:
594
+
595
+ - If at some point you want to change the pattern,
596
+ you have to change it at multiple places.
597
+ - When you read the codes, it would not be obvious at a glance that
598
+ "hey, this part is the same as that part!"
599
+
600
+ Come on, you are programming! Are not there some better ways?
601
+ Fortunately, there are, and there are multiple ways to do that.
602
+
603
+ First, you can define a method, and call that method twice:
604
+
605
+ ```ruby
606
+ def pattern # the method name `pattern` is arbitrary, set what you like
607
+ h -25, 0, 1/2r; b 3/4r
608
+ h 25, 0, 1/2r; b 1/4r + 2
609
+ h 0, 25, 1/2r; b 1
610
+
611
+ h -25, -25, 1/2r; b 3/4r
612
+ h 25, -25, 1/2r; b 1/4r + 2
613
+ h 0, -50, 1/2r; b 1
614
+ end
615
+ pattern
616
+ pattern
617
+ ```
618
+
619
+ Another way of doing this is to create a group of notes and duplicate them.
620
+ You can write these:
621
+
622
+ ```ruby
623
+ notes = group do # the variable name `notes` is arbitrary, set what you like
624
+ h -25, 0, 1/2r; b 3/4r
625
+ h 25, 0, 1/2r; b 1/4r + 2
626
+ h 0, 25, 1/2r; b 1
627
+
628
+ h -25, -25, 1/2r; b 3/4r
629
+ h 25, -25, 1/2r; b 1/4r + 2
630
+ h 0, -50, 1/2r; b 1
631
+ end
632
+ new_notes = duplicate notes # duplicate them to get a group of new notes
633
+ transform(new_notes) { beat_translate 8 } # translate those new notes by 8 beats
634
+ b 8 # calling `transform` does not affect current beat, so proceed by 8 beats now
635
+ ```
636
+
637
+ In this example, the variable `notes` is assigned the return value of `group`,
638
+ which is actually an array of the events inside the code block in the call of `group`.
639
+ The variable `new_notes` is assigned the return value of `duplicate`,
640
+ which is another array of other events, which are copies of the events in `notes`.
641
+
642
+ > In Ruby, code blocks can be written as a `do`...`end` block **or** a curly-enclosed block (`{`...`}`).
643
+ They have equivalent effects but have different precedences,
644
+ and by convention, if the code block contains only one line of codes,
645
+ people use a curly-enclosed block to make it seem more compact.
646
+
647
+ Transformations are more powerful than just translating in time.
648
+ There are many other transformations you can apply to notes.
649
+ Here is a list:
650
+
651
+ | Syntax | Meaning |
652
+ |-|-|
653
+ | `translate dx, dy` | translating in space by vector $(\mathtt{dx},\mathtt{dy})$ |
654
+ | `horizontal_flip` | flipping horizontally |
655
+ | `vertical_flip` | flipping vertically |
656
+ | `rotate angle` | rotating by `angle` (in radians) counterclockwise |
657
+ | `scale sx, sy=sx` | scaling by `sx` in $x$ and by `sy` in $y$; negative values are also valid |
658
+ | `compound_linear xx, xy, yx, yy` | applying a linear transformation specified by numbers in a matrix |
659
+ | `beat_translate delta_beat` | translating in time by `delta_beat` beats |
660
+
661
+ > For those who are not familiar with linear algebra,
662
+ some of the transformations on a 2-dimensional shape can be specified by a $2\times2$ matrix.
663
+ Those transformations are called **linear transformations**.
664
+ The matrix is called the **transformation matrix**.
665
+ The transformation is done like this:
666
+ >
667
+ > ```math
668
+ > x'=\mathtt{xx}\cdot x+\mathtt{xy}\cdot y,\quad
669
+ > y'=\mathtt{yx}\cdot x+\mathtt{yy}\cdot y.
670
+ > ```
671
+
672
+ *Notice*: When doing spatial transformations,
673
+ you do not need to worry about the direction of flick notes.
674
+ They are taken care of as well.
675
+
676
+ *Another notice*: For background patterns,
677
+ they are ignored when doing spatial transformations.
678
+ They are still affected by temporal transformations, though.
679
+
680
+ Therefore, you can also apply, say, a horizontal flip on the duplicated notes like this:
681
+
682
+ ```ruby
683
+ notes = group do
684
+ # ...
685
+ end
686
+ transform duplicate notes do
687
+ horizontal_flip
688
+ beat_translate 8
689
+ end
690
+ ```
691
+
692
+ Transformations can also be used to bulk edit notes.
693
+ Imagine that you have already written a bunch of notes with their coordinates,
694
+ but you then decided them move them (say, translating by vector $(25,25)$).
695
+ You can use `transform` to do that:
696
+
697
+ ```ruby
698
+ notes = group do
699
+ # ...
700
+ end
701
+ transform(notes) { translate 25, 25 }
702
+ ```
703
+
704
+ ### Navigate among beats
705
+
706
+ You have already learned how to use `beat` to proceed the current beat
707
+ by a certain amount of beats.
708
+ It can also navigate backwards by using negative numbers.
709
+
710
+ You can get the current beat number by using `beat` without arguments:
711
+
712
+ ```ruby
713
+ offset 0.066 # using `offset` will reset the current beat to zero
714
+ p beat # this will output 0 in the terminal, because the current beat is 0
715
+ p b # `b` is an abbreviation of `beat`, so this will also output 0
716
+ beat 1 # proceed the current beat by 1 beat
717
+ p beat # this will output 1 in the terminal, because the current beat is 1
718
+ b 3/4r # proceed the current beat by 3/4 beats
719
+ p b # this will output 7/4 in the terminal, because the current beat is 7/4
720
+ b -1/2r # go back by 1/2 beats
721
+ p b # outputs 5/4
722
+ b -2 # go back by 2 beats, even if the current beat is only 5/4
723
+ p b # outputs -3/4
724
+ ```
725
+
726
+ > In Ruby, the method `p` is used to output any object in the terminal
727
+ in a human-readable form.
728
+ You can use it to debug your program.
729
+
730
+ You can use `beat!` (whose abbreviation is `b!`) to set the current beat directly,
731
+ instead of specifying the amount of beats to proceed relative to the current beat:
732
+
733
+ ```ruby
734
+ beat! 10 # set the current beat to 10
735
+ p beat # outputs 10
736
+ b! -5 # set the current beat to -5
737
+ p b # outputs -5
738
+ p beat! # this outputs -5 because you may also use `beat!` without arguments to get the current beat
739
+ p b! # this also outputs -5
740
+ ```
741
+
742
+ You can use `time_at` to get the time (in seconds) of a certain beat:
743
+
744
+ ```ruby
745
+ offset 0.1
746
+ bpm 120
747
+ p time_at 0 # outputs 0.1, meaning that beat 0 is at 0.1 seconds
748
+ p time_at 1 # outputs 0.6, meaning that beat 1 is at 0.6 seconds
749
+ b! 10
750
+ p time_at # when using `time_at` without arguments, it will use the current beat; outputs 5.1
751
+
752
+ offset 0.2 # this resets current beat to 0
753
+ bpm 120 # calling `offset` clears BPM info, so you need to set it again
754
+ p time_at # outputs 0.2, meaning that beat 0 is at 0.2 seconds
755
+ ```
756
+
757
+ Having `beat!`, you can now travel back to some point in time
758
+ to write some notes there:
759
+
760
+ ```ruby
761
+ checkpoint = b
762
+
763
+ # write something here...
764
+
765
+ b! checkpoint # travel back to the checkpoint
766
+
767
+ # write something here...
768
+ ```
769
+
770
+ However, there is another way of doing this.
771
+ You can encapsulate the first set of notes in a `group` that does
772
+ *not preserve beat*,
773
+ which means that the current beat is set to how it was at the start of the group when the group finishes,
774
+ like this:
775
+
776
+ ```ruby
777
+ group preserve_beat: false do
778
+ # write something here...
779
+ end
780
+ # write something here...
781
+ ```
782
+
783
+ ### BPM changes
784
+
785
+ *Notice*: because *Big-D* does not have BPM changes,
786
+ this part of the tutorial will not be used in the actual chart of *Big-D*.
787
+ You may still try it out, though.
788
+
789
+ You can use `bpm` to set the BPM starting at the current beat.
790
+
791
+ ```ruby
792
+ offset 0
793
+ bpm 120
794
+ p time_at # outputs 0.0
795
+ b 1 # proceed by 1 beat
796
+ p time_at # outputs 0.5
797
+ bpm 60 # set the BPM starting at the current beat to 60
798
+ b 1
799
+ p time_at # outputs 1.5
800
+ ```
801
+
802
+ As we can see, BPM changes only affect the beats **after** they were set.
803
+ Beats before the BPM changes are not affected.
804
+ However, this means that you cannot get the time before the first BPM you set:
805
+
806
+ ```ruby
807
+ offset 0
808
+ bpm 120
809
+ p time_at -1 # raises an error!
810
+ ```
811
+
812
+ ### Tip points
813
+
814
+ Sscharter regards tap, hold, drag, and flick as *tip-pointable* events.
815
+ Note that although Sunniesnow also regards background notes as tip-pointable,
816
+ sscharter does not.
817
+ Tip-pointable events can be connected by tip points,
818
+ and one tip-pointable event can be connected by at most one tip point.
819
+ A set of events that are connected by the same tip point compose a *tip point chain*.
820
+
821
+ There are two methods for you to write tip points:
822
+ `tip_point_chain` and `tip_point_drop`,
823
+ and they are respectively abbreviated as `tp_chain` and `tp_drop`.
824
+ Before introducing them, I need to first explain how to specify
825
+ where and when a tip point spawns.
826
+ There are two ways to specify where the tip point spawns:
827
+
828
+ - Specifying the coordinates relative to the first tip-pointable event in the tip point chain.
829
+ This is the default way.
830
+ You specify the $x$-coordinate and $y$-coordinate
831
+ by the first two arguments of `tip_point_chain` or `tip_point_drop`.
832
+ - Specifying the absolute coordinates of the position where the tip point spawns.
833
+ To specify the position in this way,
834
+ you need to add a keyword argument `relative: false` to the call of `tip_point_chain` or `tip_point_drop`.
835
+
836
+ There are four ways to specify when the tip point spawns:
837
+
838
+ - Specifying how much time, in seconds,
839
+ before the first tip-pointable event in the tip point chain the tip point will have spawned.
840
+ This is the default way.
841
+ The time is specified by the third argument in the call of `tip_point_chain` or `tip_point_drop`.
842
+ - Specifying the speed of the tip point between its spawning and the first tip-pointable event in the tip point chain.
843
+ To specify the speed, you need to use the keyword argument `speed:`
844
+ in the call of `tip_point_chain` or `tip_point_drop`.
845
+ - Specifying how many beats before the first tip-pointable event
846
+ in the tip point chain the tip point will have spawned.
847
+ To specify the beats, you need to use the keyword argument `relative_beat:`
848
+ in the call of `tip_point_chain` or `tip_point_drop`.
849
+ - Specifying the beat speed (unit length per beat)
850
+ of the tip point between its spawning and the first tip-pointable event in the tip point chain.
851
+ To specify the beat speed, you need to use the keyword argument `beat_speed:`
852
+ in the call of `tip_point_chain` or `tip_point_drop`.
853
+ The beat speed may be specified as a float number,
854
+ and this is the only case where something related to beats is not preferred to be specified as a rational number.
855
+
856
+ We can categorize the four ways into a table:
857
+
858
+ <table>
859
+ <tr>
860
+ <th>Method</th>
861
+ <td><code>tip_point_chain</code></td>
862
+ <td><code>tip_point_drop</code></td>
863
+ </tr>
864
+ <tr>
865
+ <th><code>relative_time</code></th>
866
+ <td><code>tip_point_chain x=0, y=0, relative_time=0.0, relative: true</code></td>
867
+ <td><code>tip_point_drop x=0, y=0, relative_time=0.0, relative: true</code></td>
868
+ </tr>
869
+ <tr>
870
+ <th><code>speed</code></th>
871
+ <td><code>tip_point_chain x=0, y=0, relative: true, speed:</code></td>
872
+ <td><code>tip_point_drop x=0, y=0, relative: true, speed:</code></td>
873
+ </tr>
874
+ <tr>
875
+ <th><code>relative_beat</code></th>
876
+ <td><code>tip_point_chain x=0, y=0, relative: true, relative_beat:</code></td>
877
+ <td><code>tip_point_drop x=0, y=0, relative: true, relative_beat:</code></td>
878
+ </tr>
879
+ <tr>
880
+ <th><code>beat_speed</code></th>
881
+ <td><code>tip_point_chain x=0, y=0, relative: true, beat_speed:</code></td>
882
+ <td><code>tip_point_drop x=0, y=0, relative: true, beat_speed:</code></td>
883
+ </tr>
884
+ </table>
885
+
886
+ You can only choose one of the four ways to specify the position and time of the spawning of a tip point.
887
+ If you specify more than one of them, an error will be raised.
888
+
889
+ > In Ruby, keyword arguments are specified after the normal arguments.
890
+ You need to explicitly write the name of the keyword argument followed by a colon
891
+ in the method call.
892
+ For example, in the expression
893
+ >
894
+ > ```ruby
895
+ > tp_chain 0, 100, speed: 100 do
896
+ > # ...
897
+ > end
898
+ > ```
899
+ >
900
+ > the argument `speed:` is a keyword argument in the call of `tp_chain`.
901
+ The value of the keyword argument is `100`.
902
+
903
+ Knowing how to specify the position and time of the spawning of a tip point,
904
+ then creating tip points is just as easy as filling events in the code block
905
+ in the call of `tip_point_chain` or `tip_point_drop`.
906
+
907
+ Now I need to explain the difference of `tip_point_chain` and `tip_point_drop`.
908
+ `tip_point_chain` creates only one tip point chain,
909
+ and the tip point connects every tip-pointable event in the code block;
910
+ `tip_point_drop` creates as many tip point chains as the number of tip-pointable events in the code block,
911
+ and each tip point chain connects only one tip-pointable event.
912
+
913
+ As an example, let's connect the notes in the first 8 beats of *Big-D* by tip points:
914
+
915
+ ```ruby
916
+ tp_chain 0, 100, speed: 100 do
917
+ h -25, 0, 1/2r; b 3/4r
918
+ h 25, 0, 1/2r; b 1/4r + 2
919
+ h 0, 25, 1/2r; b 1
920
+
921
+ h -25, -25, 1/2r; b 3/4r
922
+ h 25, -25, 1/2r; b 1/4r + 2
923
+ h 0, -50, 1/2r; b 1
924
+ end
925
+ ```
926
+
927
+ If you want to duplicate those tip-pointed notes, you do not need to encapsulate them with another layer of `group`.
928
+ The return value of `tip_point_chain` or `tip_point_drop`
929
+ is already an array of the events in the code block.
930
+ Therefore, you can just do this:
931
+
932
+ ```ruby
933
+ notes = tp_chain 0, 100, speed: 100 do
934
+ # notes...
935
+ end
936
+ transform duplicate notes do
937
+ # transforms...
938
+ end
939
+ ```
940
+
941
+ *Tips*:
942
+ Just like `group`, `tip_point_chain` and `tip_point_drop`
943
+ may also not preserve beat.
944
+ You can let them not preserve beat by specifying the keyword argument `preserve_beat: false`:
945
+
946
+ ```ruby
947
+ tp_chain 0, 100, speed: 100, preserve_beat: false do
948
+ # notes...
949
+ end
950
+ ```
951
+
952
+ Now, the whole source codes file should look like this:
953
+
954
+ ```ruby
955
+ # frozen_string_literal: true
956
+
957
+ Sunniesnow::Charter.open 'master' do
958
+
959
+ title 'Big-D'
960
+ artist 'Shaolin Dub'
961
+ charter 'UlyssesZhan' # replace this with your name
962
+ difficulty_name 'Master'
963
+ difficulty_color :master
964
+ difficulty '10'
965
+
966
+ offset 0.066
967
+ bpm 138
968
+
969
+ grid 16
970
+
971
+ notes = tp_chain 0, 100, speed: 100 do
972
+ h -25, 0, 1/2r; b 3/4r
973
+ h 25, 0, 1/2r; b 1/4r + 2
974
+ h 0, 25, 1/2r; b 1
975
+
976
+ h -25, -25, 1/2r; b 3/4r
977
+ h 25, -25, 1/2r; b 1/4r + 2
978
+ h 0, -50, 1/2r; b 1
979
+ end
980
+ transform duplicate notes do
981
+ horizontal_flip
982
+ beat_translate 8
983
+ end
984
+
985
+ end
986
+ ```
987
+
988
+ You may build the level file and play it on Sunniesnow to see what you have done!
989
+
990
+ ![Second chart](https://i.imgur.com/ksu5sVW.png)
991
+
992
+ *Notice*:
993
+ When duplicating tip-pointed events,
994
+ the duplicated events are not connected by the same tip point as the original events.
995
+ However, if two original events are connected by the same tip point,
996
+ their duplicates are also connected by the same tip point, too.
997
+
998
+ ## Advanced charting techniques
999
+
1000
+ TODO.
1001
+
1002
+ ## Step 6: review your chart and write the README
1003
+
1004
+ Now, you have finished writing the chart!
1005
+ Please always play the chart yourself on Sunniesnow to see if it is enjoyable to play it.
1006
+ You should also try playing it in mirror mode (horizontal flip and/or vertical flip)
1007
+ so that you do not apply too much of your charting memories to playing the chart.
1008
+
1009
+ Then, it is time to finish writing the README.
1010
+ A template for the README file should have already been generated for you by sscharter.
1011
+ Use your text editor to edit it!
1012
+
1013
+ ## Step 7: distribute the level file
1014
+
1015
+ Congratulations! You finished your chart.
1016
+ Now, you can distribute the level file to people who may want to play it.
1017
+
1018
+ If you want your chart to be available as an online level of Sunniesnow,
1019
+ you need to [contact the author of Sunniesnow](mailto:UlyssesZhan <ulysseszhan@gmail.com>).