Class: Sunniesnow::Charter

Inherits:
Object
  • Object
show all
Defined in:
lib/sscharter/cli.rb,
lib/sscharter/version.rb,
lib/sscharter.rb

Defined Under Namespace

Modules: CLI Classes: BpmChangeList, Event, OffsetError, TipPointError, TipPointStart, Transform

Constant Summary collapse

VERSION =
"0.9.0"
PROJECT_DIR =
File.expand_path(ENV['SSCHARTER_PROJECT_DIR'] ||= Dir.pwd)
COLORS =
{
  easy: '#3eb9fd',
  normal: '#f19e56',
  hard: '#e75e74',
  master: '#8c68f3',
  special: '#f156ee'
}.freeze
DIRECTIONS =
{
  right: i[r],
  up_right: i[ur ru],
  up: i[u],
  up_left: i[ul lu],
  left: i[l],
  down_left: i[dl ld],
  down: i[d],
  down_right: i[dr rd]
}.each_with_object({
  right: 0.0,
  up_right: Math::PI / 4,
  up: Math::PI / 2,
  up_left: Math::PI * 3 / 4,
  left: Math::PI,
  down_left: -Math::PI * 3 / 4,
  down: -Math::PI / 2,
  down_right: -Math::PI / 4
}) do |(direction_name, aliases), directions|
  aliases.each { directions[_1] = directions[direction_name] }
end.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

DSL methods collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ Charter

Create a new chart. Usually you should use open instead of this method.

Parameters:

  • name (String)

    the name of the chart.



355
356
357
358
359
360
# File 'lib/sscharter.rb', line 355

def initialize name
  @name = name
  init_chart_info
  init_state
  init_bookmarks
end

Class Attribute Details

.chartsHash<String, Sunniesnow::Charter> (readonly)

A hash containing all the charts opened by open. The keys are the names of the charts, and the values are the Sunniesnow::Charter objects.

Returns:



320
321
322
# File 'lib/sscharter.rb', line 320

def charts
  @charts
end

Instance Attribute Details

#eventsArray<Sunniesnow::Charter::Event> (readonly)

An array of events.

Returns:



326
327
328
# File 'lib/sscharter.rb', line 326

def events
  @events
end

Class Method Details

.open(name, &block) ⇒ Sunniesnow::Charter

Create a new chart or open an existing chart for editing. The name is used to check whether the chart already exists. If a new chart needs to be created, it is added to charts.

The given block will be evaluated in the context of the chart (inside the block, self is the same as the return value, a Sunniesnow::Charter instance). This method is intended to be called at the top level of a Ruby script to open a context for writing a Sunniesnow chart using the DSL.

In the examples in the documentation of other methods, it is assumed that they are run inside a block passed to this method.

Examples:

Sunniesnow::Charter.open 'master' do
  # write the chart here
end

Parameters:

  • name (String)

    the name of the chart.

Returns:



346
347
348
349
350
# File 'lib/sscharter.rb', line 346

def self.open name, &block
  result = @charts[name] ||= new name
  result.instance_eval &block if block
  result
end

Instance Method Details

#artist(artist) ⇒ String

Set the artist of the music for the chart. This will be reflected in the return value of #to_sunniesnow.

Parameters:

  • artist (String)

    the artist of the music.

Returns:

  • (String)

    the artist of the music, the same as the argument artist.

Raises:

  • (ArgumentError)

    if artist is not a String.

See Also:



533
534
535
536
# File 'lib/sscharter.rb', line 533

def artist artist
  raise ArgumentError, 'artist must be a string' unless artist.is_a? String
  @artist = artist
end

#at(name, preserve_beat: false, update_mark: false, &block) ⇒ Object

Raises:

  • (ArgumentError)


1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
# File 'lib/sscharter.rb', line 1037

def at name, preserve_beat: false, update_mark: false, &block
  raise ArgumentError, 'no block given' unless block
  raise ArgumentError, "unknown bookmark #{name}" unless bookmark = @bookmarks[name]
  backup = backup_state
  restore_state bookmark
  result = group &block
  mark name if update_mark
  beat_backup = backup_beat if preserve_beat
  restore_state backup
  restore_beat beat_backup if preserve_beat
  result
end

#backup_beatObject



416
417
418
# File 'lib/sscharter.rb', line 416

def backup_beat
  {current_beat: @current_beat, bpm_changes: @bpm_changes}
end

#backup_stateObject



425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/sscharter.rb', line 425

def backup_state
  {
    current_beat: @current_beat,
    bpm_changes: @bpm_changes,
    tip_point_mode_stack: @tip_point_mode_stack.dup,
    current_tip_point_stack: @current_tip_point_stack.dup,
    current_tip_point_group_stack: @current_tip_point_group_stack.dup,
    current_duplicate: @current_duplicate,
    tip_point_start_stack: @tip_point_start_stack.dup,
    tip_point_start_to_add_stack: @tip_point_start_to_add_stack.dup,
    groups: @groups.dup
  }
end

#beat(delta_beat = 0) ⇒ Rational Also known as: b

Increments the current beat by the given delta set by delta_beat. It is recommended that delta_beat be a Rational or an Integer for accuracy. Float will be converted to Rational, and a warning will be issued when a Float is used.

This method is also useful for inspecting the current beat. If the method is called without an argument, it simply returns the current beat. For this purpose, this method is equivalent to #beat!.

This method must be called after #offset.

Examples:

Increment the current beat and inspect it

offset 0.1; bpm 120
p b       # Outputs 0, this is the initial value
p b 1     # Outputs 1, because it is incremented by 1 when it was 0
p b 1/2r  # Outputs 3/2, because it is incremented by 3/2 when it was 1
p time_at # Outputs 0.85, which is offset + 60s / BPM * beat

Time the notes

offset 0.1; bpm 120
t 0, 0; b 1
t 50, 0; b 1
# Now there are two tap notes, one at beat 0, and the other at beat 1

Parameters:

  • delta_beat (Rational, Integer) (defaults to: 0)

    the delta to increment the current beat by.

Returns:

  • (Rational)

    the new current beat.

Raises:

See Also:



682
683
684
685
686
687
688
689
690
691
692
693
# File 'lib/sscharter.rb', line 682

def beat delta_beat = 0
  raise OffsetError.new __method__ unless @current_beat
  case delta_beat
  when Integer, Rational
    @current_beat += delta_beat.to_r
  when Float
    warn 'float beat is not recommended'
    @current_beat += delta_beat.to_r
  else
    raise ArgumentError, 'invalid delta_beat'
  end
end

#beat!(beat = @current_beat) ⇒ Rational Also known as: b!

Sets the current beat to the given value. It is recommended that beat be a Rational or an Integer for accuracy. Float will be converted to Rational, and a warning will be issued.

When called without an argument, this method does nothing and returns the current beat. For this purpose, this method is equivalent to #beat.

This method must be called after #offset.

Examples:

Set the current beat and inspect it

offset 0.1; bpm 120
p b!      # Outputs 0, this is the initial value
p b! 1    # Outputs 1, because it is set to 1
p b! 1/2r # Outputs 1/2, because it is set to 1/2
p time_at # Outputs 0.35, which is offset + 60s / BPM * beat

Parameters:

  • beat (Rational, Integer) (defaults to: @current_beat)

    the new current beat.

Returns:

  • (Rational)

    the new current beat.

Raises:

See Also:



714
715
716
717
718
719
720
721
722
723
724
725
# File 'lib/sscharter.rb', line 714

def beat! beat = @current_beat
  raise OffsetError.new __method__ unless @current_beat
  case beat
  when Integer, Rational
    @current_beat = beat.to_r
  when Float
    warn 'float beat is not recommended'
    @current_beat = beat.to_r
  else
    raise ArgumentError, 'invalid beat'
  end
end

#bg_note(x, y, duration_beats = 0, text = nil) ⇒ Event

Creates a background note at the given coordinates with the given duration and text. The coordinates x and y must be numbers. The argument duration_beats is the duration of the background note in beats. It needs to be a non-negative Rational or Integer. If it is a Float, it will be converted to a Rational, and a warning will be issued. The argument text is the text to be displayed on the note (it is converted to a string via to_s if it is not a string).

Both the duration_beats and the text arguments are optional. When there are three arguments given in total, the method determines whether the third is duration_beats or text based on its type.

Technically, this adds an event of type :bg_note to the chart at the current time with properties containing the information provided by x, y, duration_beats, and text.

Examples:

offset 0.1; bpm 120
bg_note 0, 0, 1, 'Hello' # duration is 1, text is 'Hello'
bg_note 50, 0, 'world'   # duration is 0, text is 'world'
bg_note -50, 0, 2        # duration is 2, text is ''

Parameters:

  • x (Numeric)

    the x-coordinate of the note.

  • y (Numeric)

    the y-coordinate of the note.

  • duration_beats (Rational, Integer) (defaults to: 0)

    the duration of the background note in beats.

  • text (String) (defaults to: nil)

    the text to be displayed on the note.

Returns:

  • (Event)

    the event representing the background note.

Raises:

  • (ArgumentError)

    if x, y, or duration_beats is not a number, or if duration_beats is negative.



882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
# File 'lib/sscharter.rb', line 882

def bg_note x, y, duration_beats = 0, text = nil
  if text.nil?
    if duration_beats.is_a? String
      text = duration_beats
      duration_beats = 0
    else
      text = ''
    end
  end
  if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
    raise ArgumentError, 'x, y, and duration_beats must be numbers'
  end
  if duration_beats < 0
    raise ArgumentError, 'duration must be non-negative'
  end
  if duration_beats.is_a? Float
    warn 'Rational is recommended over Float for duration_beats'
  end
  event :bg_note, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
end

#big_text(duration_beats = 0, text) ⇒ Event

Creates a big text. The argument duration_beats is the duration of the big text in beats. It needs to be a non-negative Rational or Integer. If it is a Float, it will be converted to a Rational, and a warning will be issued. The argument text is the text to be displayed.

Technically, this adds an event of type :big_text to the chart at the current time with properties containing the information provided by duration_beats and text.

Examples:

offset 0.1; bpm 120
big_text 1, 'Hello, world!' # duration is 1, text is 'Hello, world!'
b 1
big_text 'Goodbye!'         # duration is 0, text is 'Goodbye!'

Parameters:

  • duration_beats (Rational, Integer) (defaults to: 0)

    the duration of the big text in beats.

  • text (String)

    the text to be displayed.

Returns:

  • (Event)

    the event representing the big text.

Raises:

  • (ArgumentError)

    if duration_beats is not a number or is negative.



920
921
922
923
924
925
926
927
928
929
930
931
# File 'lib/sscharter.rb', line 920

def big_text duration_beats = 0, text
  unless duration_beats.is_a? Numeric
    raise ArgumentError, 'duration_beats must be a number'
  end
  if duration_beats < 0
    raise ArgumentError, 'duration must be non-negative'
  end
  if duration_beats.is_a? Float
    warn 'Rational is recommended over Float for duration_beats'
  end
  event :big_text, duration_beats.to_r, text: text.to_s
end

#bpm(bpm) ⇒ BpmChangeList

Set the BPM starting at the current beat. This method must be called after #offset. The method can be called multiple times, which is useful when the music changes its tempo from time to time.

Internally, this simply calls Sunniesnow::Charter::BpmChangeList#add on the BPM changes created by #offset.

Parameters:

  • bpm (Numeric)

    the BPM.

Returns:

Raises:



652
653
654
655
# File 'lib/sscharter.rb', line 652

def bpm bpm
  raise OffsetError.new __method__ unless @bpm_changes
  @bpm_changes.add @current_beat, bpm
end

#charter(charter) ⇒ String

Set the name of the chart author for the chart. This will be reflected in the return value of #to_sunniesnow.

Parameters:

  • charter (String)

    the name of the charter.

Returns:

  • (String)

    the name of the chart author, the same as the argument charter.

Raises:

  • (ArgumentError)

    if charter is not a String.

See Also:



544
545
546
547
# File 'lib/sscharter.rb', line 544

def charter charter
  raise ArgumentError, 'charter must be a string' unless charter.is_a? String
  @charter = charter
end

#check(notes_in_bound: true, bg_notes_in_bound: true) ⇒ Object



1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
# File 'lib/sscharter.rb', line 1050

def check(
  notes_in_bound: true,
  bg_notes_in_bound: true
)
  out_of_bound_events = [] if notes_in_bound || bg_notes_in_bound
  @events.each do |event|
    if i[tap hold drag flick].include?(event.type) && notes_in_bound || event.type == :bg_note && bg_notes_in_bound
      if event[:x] < -100-1e-10 || event[:x] > 100+1e-10 || event[:y] < -50-1e-10 || event[:y] > 50+1e-10
        out_of_bound_events.push event
      end
    end
  end
  if notes_in_bound || bg_notes_in_bound
    if out_of_bound_events.empty?
      puts "===== All notes are in bound ====="
    else
      puts "===== Out-of-bound notes ====="
      out_of_bound_events.each do |event|
        p event
        puts "at time #{event.time}"
        puts 'defined at:'
        puts event.backtrace
      end
    end
  end
end

#difficulty(difficulty) ⇒ String

Set the difficulty level for the chart. This will be reflected in the return value of #to_sunniesnow.

The argument difficulty should be a string representing the difficulty level. Anything other than a string will be converted to a string using to_s.

Parameters:

  • difficulty (String)

    the difficulty level.

Returns:

  • (String)

    the difficulty level (converted to a string).

See Also:



598
599
600
# File 'lib/sscharter.rb', line 598

def difficulty difficulty
  @difficulty = difficulty.to_s
end

#difficulty_color(difficulty_color) ⇒ String

Set the color of the difficulty for the chart. This will be reflected in the return value of #to_sunniesnow.

The argument difficulty_color can be a color name (a key of COLORS), an RGB color in hexadecimal format (e.g. ‘#8c68f3’, ‘#8CF’), an RGB color in decimal format (e.g. ‘rgb(140, 104, 243)’), or an integer representing an RGB color (e.g. 0x8c68f3).

Parameters:

  • difficulty_color (Symbol, String, Integer)

    the color of the difficulty.

Returns:

  • (String)

    the color of the difficulty in hexadecimal format (e.g. ‘#8c68f3’).

Raises:

  • (ArgumentError)

    if difficulty_color is not a valid color format.

See Also:



571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
# File 'lib/sscharter.rb', line 571

def difficulty_color difficulty_color
  @difficulty_color = case difficulty_color
  when Symbol
    COLORS[difficulty_color]
  when /^#[0-9a-fA-F]{6}$/
    difficulty_color
  when /^#[0-9a-fA-F]{3}$/
    _, r, g, b = difficulty_color.chars
    "##{r}#{r}#{g}#{g}#{b}#{b}"
  when /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/
    r, g, b = $1, $2, $3
    sprintf '#%02x%02x%02x', r.to_i, g.to_i, b.to_i
  when Integer
    sprintf '#%06x', difficulty_color % 0x1000000
  else
    raise ArgumentError, 'unknown format of difficulty_color'
  end
end

#difficulty_name(difficulty_name) ⇒ String

Set the name of the difficulty for the chart. This will be reflected in the return value of #to_sunniesnow.

Parameters:

  • difficulty_name (String)

    the name of the difficulty.

Returns:

  • (String)

    the name of the difficulty, the same as the argument difficulty_name.

Raises:

  • (ArgumentError)

    if difficulty_name is not a String.

See Also:



555
556
557
558
# File 'lib/sscharter.rb', line 555

def difficulty_name difficulty_name
  raise ArgumentError, 'difficulty_name must be a string' unless difficulty_name.is_a? String
  @difficulty_name = difficulty_name
end

#difficulty_sup(difficulty_sup) ⇒ String

Set the difficulty superscript for the chart. This will be reflected in the return value of #to_sunniesnow.

The argument difficulty_sup should be a string representing the difficulty superscript. Anything other than a string will be converted to a string using to_s.

Parameters:

  • difficulty_sup (String)

    the difficulty superscript.

Returns:

  • (String)

    the difficulty superscript (converted to a string).

See Also:



610
611
612
# File 'lib/sscharter.rb', line 610

def difficulty_sup difficulty_sup
  @difficulty_sup = difficulty_sup.to_s
end

#drag(x, y) ⇒ Event Also known as: d

Creates a drag note at the given coordinates. The coordinates x and y must be numbers.

Technically, this adds an event of type :drag to the chart at the current time with properties containing the information provided by x and y.

Examples:

offset 0.1; bpm 120
d 0, 0
d 50, 0
# Now there are two drag notes at (0, 0) and (50, 0)

Parameters:

  • x (Numeric)

    the x-coordinate of the note.

  • y (Numeric)

    the y-coordinate of the note.

Returns:

  • (Event)

    the event representing the drag note.

Raises:

  • (ArgumentError)

    if x or y is not a number.



805
806
807
808
809
810
# File 'lib/sscharter.rb', line 805

def drag x, y
  if !x.is_a?(Numeric) || !y.is_a?(Numeric)
    raise ArgumentError, 'x and y must be numbers'
  end
  event :drag, x: x.to_f, y: y.to_f
end

#duplicate(events, new_tip_points: true) ⇒ Array<Event>

Duplicate all events in a given array. This method is useful when you want to duplicate a set of events. The argument events is an array of events to be duplicated. The argument new_tip_points is a boolean indicating whether to create new tip points. If it is true, new tip points will be created for the duplicated events. If it is false, each duplicated event shares the same tip point as the original event.

Examples:

Duplicate a note

offset 0.1; bpm 120
duplicate [t 0, 0]

Duplicate notes that share tip points with the original notes

offset 0.1; bpm 120
duplicate tp_chain(0, 100, 1) { t 0, 0 }

Parameters:

  • events (Array<Event>)

    the events to be duplicated.

  • new_tip_points (Boolean) (defaults to: true)

    whether to create new tip points for the duplicated events.

Returns:

  • (Array<Event>)

    the duplicated events.



987
988
989
990
991
992
993
994
995
996
997
998
999
# File 'lib/sscharter.rb', line 987

def duplicate events, new_tip_points: true
  result = []
  events.each do |event|
    next if event.type == :placeholder && !new_tip_points
    result.push event = event.dup
    if event[:tip_point] && new_tip_points
      event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
    end
    @groups.each { _1.push event }
  end
  @current_duplicate += 1 if new_tip_points
  result
end

#event(type, duration_beats = nil, **properties) ⇒ Object



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/sscharter.rb', line 451

def event type, duration_beats = nil, **properties
  raise OffsetError.new __method__ unless @bpm_changes
  event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
  @groups.each { _1.push event }
  return event unless event.tip_pointable?
  case @tip_point_mode_stack.last
  when :chain
    if @tip_point_start_to_add_stack.last
      @current_tip_point_stack[-1] = @tip_point_peak
      @tip_point_peak += 1
    end
    push_tip_point_start event
    @tip_point_start_to_add_stack[-1] = nil
  when :drop
    @current_tip_point_stack[-1] = @tip_point_peak
    @tip_point_peak += 1
    push_tip_point_start event
  when :none
    # pass
  end
  event
end

#flick(x, y, direction, text = '') ⇒ Event Also known as: f

Creates a flick note at the given coordinates with the given direction and text. The coordinates x and y must be numbers. The argument direction is the direction of the flick note in radians or a symbol. If it is a symbol, it should be one of the keys of DIRECTIONS (which are :right, :up_right, etc., abbreviated as :r, :ur etc.). If it is a number, it should be a number representing the angle in radians, specifying the angle rorated anticlockwise starting from the positive x-direction. The argument text is the text to be displayed on the note (it is converted to a string via to_s if it is not a string).

Technically, this adds an event of type :flick to the chart at the current time with properties containing the information provided by x, y, direction, and text.

Examples:

offset 0.1; bpm 120
f 0, 0, :r, 'Hello'
f 50, 0, Math::PI / 4, 'world'
# Now there are two flick notes at (0, 0) and (50, 0)
# with directions right and up right and texts 'Hello' and 'world' respectively

Parameters:

  • x (Numeric)

    the x-coordinate of the note.

  • y (Numeric)

    the y-coordinate of the note.

  • direction (Numeric, Symbol)

    the direction of the flick note in radians or a symbol.

  • text (String) (defaults to: '')

    the text to be displayed on the note.

Returns:

  • (Event)

    the event representing the flick note.

Raises:

  • (ArgumentError)

    if x or y is not a number, if direction is not a symbol or a number, or if the direction is a symbol that does not represent a known direction.



839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
# File 'lib/sscharter.rb', line 839

def flick x, y, direction, text = ''
  if !x.is_a?(Numeric) || !y.is_a?(Numeric)
    raise ArgumentError, 'x and y must be numbers'
  end
  if direction.is_a? Symbol
    direction = DIRECTIONS[direction]
    raise ArgumentError, "unknown direction #{direction}" unless direction
  elsif direction.is_a? Numeric
    warn 'Are you using degrees as angle unit instead of radians?' if direction != 0 && direction % 45 == 0
    direction = direction.to_f
  else
    raise ArgumentError, 'direction must be a symbol or a number'
  end
  event :flick, x: x.to_f, y: y.to_f, angle: direction, text: text.to_s
end

#group(preserve_beat: true, &block) ⇒ Object

Raises:

  • (ArgumentError)


1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/sscharter.rb', line 1011

def group preserve_beat: true, &block
  raise ArgumentError, 'no block given' unless block
  @groups.push result = []
  beat_backup = backup_beat unless preserve_beat
  instance_eval &block
  restore_beat beat_backup unless preserve_beat
  @groups.delete_if { result.equal? _1 }
  result
end

#hold(x, y, duration_beats, text = '') ⇒ Event Also known as: h

Creates a hold note at the given coordinates with the given duration and text. The coordinates x and y must be numbers. The argument duration_beats is the duration of the hold note in beats. It needs to be a positive Rational or Integer. If it is a Float, it will be converted to a Rational, and a warning will be issued. The argument text is the text to be displayed on the note (it is converted to a string via to_s if it is not a string).

Technically, this adds an event of type :hold to the chart at the current time with properties containing the information provided by x, y, duration_beats, and text.

Examples:

offset 0.1; bpm 120
h 0, 0, 1, 'Hello'
h 50, 0, 2, 'world'
# Now there are two hold notes at (0, 0) and (50, 0)
# with durations 1 and 2 beats and texts 'Hello' and 'world' respectively

Parameters:

  • x (Numeric)

    the x-coordinate of the note.

  • y (Numeric)

    the y-coordinate of the note.

  • duration_beats (Rational, Integer)

    the duration of the hold note in beats.

  • text (String) (defaults to: '')

    the text to be displayed on the note.

Returns:

  • (Event)

    the event representing the hold note.

Raises:

  • (ArgumentError)

    if x, y, or duration_beats is not a number, or if duration_beats is not positive.



777
778
779
780
781
782
783
784
785
786
787
788
# File 'lib/sscharter.rb', line 777

def hold x, y, duration_beats, text = ''
  if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
    raise ArgumentError, 'x, y, and duration must be numbers'
  end
  if duration_beats <= 0
    raise ArgumentError, 'duration must be positive'
  end
  if duration_beats.is_a? Float
    warn 'Rational is recommended over Float for duration_beats'
  end
  event :hold, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
end

#init_bookmarksObject



373
374
375
# File 'lib/sscharter.rb', line 373

def init_bookmarks
  @bookmarks = {}
end

#init_chart_infoObject



362
363
364
365
366
367
368
369
370
371
# File 'lib/sscharter.rb', line 362

def init_chart_info
  @difficulty_name = ''
  @difficulty_color = '#000000'
  @difficulty = ''
  @difficulty_sup = ''
  @title = ''
  @artist = ''
  @charter = ''
  @events = []
end

#init_stateObject



377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/sscharter.rb', line 377

def init_state
  @current_beat = nil
  @bpm_changes = nil
  @tip_point_mode_stack = [:none]
  @current_tip_point_stack = []
  @current_tip_point_group_stack = []
  @tip_point_peak = 0
  @current_duplicate = 0
  @tip_point_start_stack = [nil]
  @tip_point_start_to_add_stack = [nil]
  @groups = [@events]
end

#inspectObject



407
408
409
# File 'lib/sscharter.rb', line 407

def inspect
  "#<Sunniesnow::Charter #@name>"
end

#mark(name) ⇒ Object



1032
1033
1034
1035
# File 'lib/sscharter.rb', line 1032

def mark name
  @bookmarks[name] = backup_state
  name
end

#method_name(duration_beats = 0) ⇒ Event

Creates a background pattern. The argument duration_beats is the duration of the background pattern in beats. It needs to be a non-negative Rational or Integer. If it is a Float, it will be converted to a Rational, and a warning will be issued.

Technically, this adds an event of type :bg_pattern to the chart at the current time with properties containing the information provided by duration_beats.

Examples:

offset 0.1; bpm 120
method_name 1 # duration is 1
b 1
method_name 0 # duration is 0

Parameters:

  • duration_beats (Rational, Integer) (defaults to: 0)

    the duration of the background pattern in beats.

Returns:

  • (Event)

    the event representing the background pattern.

Raises:

  • (ArgumentError)

    if duration_beats is not a number or is negative.



957
958
959
960
961
962
963
964
965
966
967
968
969
970
# File 'lib/sscharter.rb', line 957

i[grid hexagon checkerboard diamond_grid pentagon turntable hexagram].each do |method_name|
  define_method method_name do |duration_beats = 0|
    unless duration_beats.is_a? Numeric
      raise ArgumentError, 'duration_beats must be a number'
    end
    if duration_beats < 0
      raise ArgumentError, 'duration must be non-negative'
    end
    if duration_beats.is_a? Float
      warn 'Rational is recommended over Float for duration_beats'
    end
    event method_name, duration_beats.to_r
  end
end

#mode(duration_beats = 0) ⇒ Event

Creates a background pattern. The argument duration_beats is the duration of the background pattern in beats. It needs to be a non-negative Rational or Integer. If it is a Float, it will be converted to a Rational, and a warning will be issued.

Technically, this adds an event of type :bg_pattern to the chart at the current time with properties containing the information provided by duration_beats.

Examples:

offset 0.1; bpm 120
mode 1 # duration is 1
b 1
mode 0 # duration is 0

Parameters:

  • duration_beats (Rational, Integer) (defaults to: 0)

    the duration of the background pattern in beats.

Returns:

  • (Event)

    the event representing the background pattern.

Raises:

  • (ArgumentError)

    if duration_beats is not a number or is negative.



1025
1026
1027
1028
1029
1030
# File 'lib/sscharter.rb', line 1025

i[chain drop none].each do |mode|
  define_method "tip_point_#{mode}" do |*args, **opts, &block|
    tip_point mode, *args, **opts, &block
  end
  alias_method "tp_#{mode}", "tip_point_#{mode}"
end

#offset(offset) ⇒ BpmChangeList

Set the offset. This is the time in seconds of the zeroth beat. This method must be called before any other methods that require a beat, or an OffsetError will be raised.

After calling this method, the current beat (see #beat and #beat!) is set to zero, and a new BPM needs to be set using #bpm. Only after that can the time of any positive beat be calculated.

Though not commonly useful, this method can be called multiple times in a chart. A new call of this method does not affect the events and BPM changes set before. Technically, each event is associated with a BPM change list (see Sunniesnow::Charter::Event#bpm_changes), and each call of this method creates a new BPM change list, which is used for the events set after.

Examples:

offset 0.1
p time_at # Outputs 0.1, which is the offset
offset 0.2
p time_at # Outputs 0.2, which is the updated offset by the second call

Parameters:

  • offset (Numeric)

    the offset in seconds.

Returns:

Raises:

  • (ArgumentError)

    if offset is not a number.

See Also:



637
638
639
640
641
# File 'lib/sscharter.rb', line 637

def offset offset
  raise ArgumentError, 'offset must be a number' unless offset.is_a? Numeric
  @current_beat = 0r
  @bpm_changes = BpmChangeList.new offset.to_f
end

#output_json(**opts) ⇒ Object



403
404
405
# File 'lib/sscharter.rb', line 403

def output_json **opts
  to_sunniesnow(**opts).to_json
end

#push_tip_point_start(start_event) ⇒ Object



474
475
476
477
478
479
480
481
482
# File 'lib/sscharter.rb', line 474

def push_tip_point_start start_event
  start_event[:tip_point] = @current_tip_point_stack.last.to_s
  tip_point_start = @tip_point_start_to_add_stack.last&.get_start_placeholder start_event
  return unless tip_point_start
  @groups.each do |group|
    group.push tip_point_start
    break if group.equal?(@current_tip_point_group_stack.last) && @tip_point_mode_stack.last != :drop
  end
end

#remove(*events) ⇒ Object



1021
1022
1023
# File 'lib/sscharter.rb', line 1021

def remove *events
  events.each { |event| @groups.each { _1.delete event } }
end

#restore_beat(backup) ⇒ Object



420
421
422
423
# File 'lib/sscharter.rb', line 420

def restore_beat backup
  @current_beat = backup[:current_beat]
  @bpm_changes = backup[:bpm_changes]
end

#restore_state(backup) ⇒ Object



439
440
441
442
443
444
445
446
447
448
449
# File 'lib/sscharter.rb', line 439

def restore_state backup
  @current_beat = backup[:current_beat]
  @bpm_changes = backup[:bpm_changes]
  @tip_point_mode_stack = backup[:tip_point_mode_stack]
  @current_tip_point_stack = backup[:current_tip_point_stack]
  @current_tip_point_group_stack = backup[:current_tip_point_group_stack]
  @current_duplicate = backup[:current_duplicate]
  @tip_point_start_to_add_stack = backup[:tip_point_start_to_add_stack]
  @groups = backup[:groups]
  nil
end

#tap(x, y, text = '') ⇒ Event Also known as: t

Creates a tap note at the given coordinates with the given text. The coordinates x and y must be numbers. The argument text is the text to be displayed on the note (it is converted to a string via to_s if it is not a string).

Technically, this adds an event of type :tap to the chart at the current time with properties containing the information provided by x, y, and text.

Examples:

offset 0.1; bpm 120
t 0, 0, 'Hello'
t 50, 0, 'world'
# Now there are two simultaneous tap notes at (0, 0) and (50, 0)
# with texts 'Hello' and 'world' respectively

Parameters:

  • x (Numeric)

    the x-coordinate of the note.

  • y (Numeric)

    the y-coordinate of the note.

  • text (String) (defaults to: '')

    the text to be displayed on the note.

Returns:

  • (Event)

    the event representing the tap note.

Raises:

  • (ArgumentError)

    if x or y is not a number.



746
747
748
749
750
751
# File 'lib/sscharter.rb', line 746

def tap x, y, text = ''
  if !x.is_a?(Numeric) || !y.is_a?(Numeric)
    raise ArgumentError, 'x and y must be numbers'
  end
  event :tap, x: x.to_f, y: y.to_f, text: text.to_s
end

#time_at(beat = @current_beat) ⇒ Object



411
412
413
414
# File 'lib/sscharter.rb', line 411

def time_at beat = @current_beat
  raise OffsetError.new __method__ unless @bpm_changes
  @bpm_changes.time_at beat
end

#tip_point(mode, *args, preserve_beat: true, **opts, &block) ⇒ Object



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/sscharter.rb', line 484

def tip_point mode, *args, preserve_beat: true, **opts, &block
  @tip_point_mode_stack.push mode
  if mode == :none
    @tip_point_start_stack.push nil
    @tip_point_start_to_add_stack.push nil
    @current_tip_point_stack.push nil
  else
    if args.empty? && opts.empty?
      unless @tip_point_start_stack.last
        raise TipPointError, 'cannot omit tip point arguments at top level or inside tip_point_none'
      end
      @tip_point_start_stack.push @tip_point_start_stack.last.dup
    else
      @tip_point_start_stack.push TipPointStart.new *args, **opts
    end
    @tip_point_start_to_add_stack.push @tip_point_start_stack.last
    @current_tip_point_stack.push nil
  end
  result = group preserve_beat: do
    @current_tip_point_group_stack.push @groups.last
    instance_eval &block
  end
  @tip_point_start_stack.pop
  @tip_point_start_to_add_stack.pop
  @tip_point_mode_stack.pop
  @current_tip_point_stack.pop
  @current_tip_point_group_stack.pop
  result
end

#title(title) ⇒ String

Set the title of the music for the chart. This will be reflected in the return value of #to_sunniesnow.

Parameters:

  • title (String)

    the title of the music.

Returns:

  • (String)

    the title of the music, the same as the argument title.

Raises:

  • (ArgumentError)

    if title is not a String.

See Also:



522
523
524
525
# File 'lib/sscharter.rb', line 522

def title title
  raise ArgumentError, 'title must be a string' unless title.is_a? String
  @title = title
end

#to_sunniesnow(**opts) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/sscharter.rb', line 390

def to_sunniesnow **opts
  result = Sunniesnow::Chart.new **opts
  result.title = @title
  result.artist = @artist
  result.charter = @charter
  result.difficulty_name = @difficulty_name
  result.difficulty_color = @difficulty_color
  result.difficulty = @difficulty
  result.difficulty_sup = @difficulty_sup
  @events.each { result.events.push _1.to_sunniesnow }
  result
end

#transform(events, &block) ⇒ Object

Transform all events in a given array in time and/or space. Space transformation does not affect background patterns.

Raises:

  • (ArgumentError)


1003
1004
1005
1006
1007
1008
1009
# File 'lib/sscharter.rb', line 1003

def transform events, &block
  raise ArgumentError, 'no block given' unless block
  events = [events] if events.is_a? Event
  transform = Transform.new
  transform.instance_eval &block
  events.each { transform.apply _1 }
end