sscharter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +39 -0
- data/LICENSE +21 -0
- data/README.md +30 -0
- data/Rakefile +12 -0
- data/exe/sscharter +13 -0
- data/lib/sscharter/chart.rb +58 -0
- data/lib/sscharter/cli.rb +192 -0
- data/lib/sscharter/utils.rb +41 -0
- data/lib/sscharter/version.rb +7 -0
- data/lib/sscharter.rb +587 -0
- data/tutorial/tutorial.md +1019 -0
- metadata +142 -0
data/lib/sscharter.rb
ADDED
@@ -0,0 +1,587 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'sscharter/version'
|
4
|
+
require_relative 'sscharter/utils'
|
5
|
+
require_relative 'sscharter/chart'
|
6
|
+
|
7
|
+
class Sunniesnow::Charter
|
8
|
+
|
9
|
+
using Sunniesnow::Utils
|
10
|
+
|
11
|
+
class OffsetError < StandardError
|
12
|
+
def initialize method_name
|
13
|
+
super "offset must be set before using #{method_name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class TipPointError < StandardError
|
18
|
+
def initialize *expected_state, actual_state
|
19
|
+
super "wrong tip point state: expected #{expected_state.join ' or '}, got #{actual_state}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.ensure state, *expected
|
23
|
+
raise self.new *expected, state unless expected.include? state
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class BpmChangeList
|
29
|
+
|
30
|
+
class BpmChange
|
31
|
+
attr_accessor :beat, :bps
|
32
|
+
|
33
|
+
def initialize beat, bpm
|
34
|
+
@beat = beat
|
35
|
+
@bps = bpm / 60.0
|
36
|
+
end
|
37
|
+
|
38
|
+
def <=> other
|
39
|
+
@beat <=> other.beat
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :offset
|
44
|
+
|
45
|
+
def initialize offset
|
46
|
+
@offset = offset
|
47
|
+
@list = []
|
48
|
+
end
|
49
|
+
|
50
|
+
def add beat, bpm
|
51
|
+
@list.push BpmChange.new beat, bpm
|
52
|
+
end
|
53
|
+
|
54
|
+
def time_at beat
|
55
|
+
index = @list.bisect(right: true) { _1.beat <=> beat }
|
56
|
+
raise ArgumentError, 'beat is before the first bpm change' if index < 0
|
57
|
+
bpm = @list[index]
|
58
|
+
(0...index).sum @offset + (beat - bpm.beat) / bpm.bps do |i|
|
59
|
+
bpm = @list[i]
|
60
|
+
(@list[i+1].beat - bpm.beat) / bpm.bps
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def [] index
|
65
|
+
@list[index]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Event
|
70
|
+
|
71
|
+
TIP_POINTABLE_TYPES = %i[tap hold flick drag]
|
72
|
+
|
73
|
+
attr_accessor :beat, :offset, :duration_beats, :properties
|
74
|
+
attr_reader :type, :bpm_changes
|
75
|
+
|
76
|
+
def initialize type, beat, duration_beats = nil, bpm_changes, **properties
|
77
|
+
@beat = beat
|
78
|
+
@duration_beats = duration_beats
|
79
|
+
@type = type
|
80
|
+
@bpm_changes = bpm_changes
|
81
|
+
@properties = properties
|
82
|
+
@offset = 0.0
|
83
|
+
end
|
84
|
+
|
85
|
+
def time_at_relative_beat delta_beat
|
86
|
+
@offset + @bpm_changes.time_at(@beat + delta_beat)
|
87
|
+
end
|
88
|
+
|
89
|
+
def time
|
90
|
+
time_at_relative_beat 0
|
91
|
+
end
|
92
|
+
|
93
|
+
def end_time
|
94
|
+
time_at_relative_beat @duration_beats || 0
|
95
|
+
end
|
96
|
+
|
97
|
+
def [] key
|
98
|
+
@properties[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
def []= key, value
|
102
|
+
@properties[key] = value
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_sunniesnow
|
106
|
+
t = time
|
107
|
+
properties = @properties.transform_keys &:snake_to_camel
|
108
|
+
properties[:duration] = end_time - t if @duration_beats
|
109
|
+
Sunniesnow::Event.new t, @type.snake_to_camel, **properties
|
110
|
+
end
|
111
|
+
|
112
|
+
def dup
|
113
|
+
result = super
|
114
|
+
result.properties = @properties.dup
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
def tip_pointable?
|
119
|
+
TIP_POINTABLE_TYPES.include? @type
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Implements homography
|
124
|
+
class Transform
|
125
|
+
include Math
|
126
|
+
attr_reader :xx, :xy, :xz, :yx, :yy, :yz, :zx, :zy, :zz, :tt, :t1
|
127
|
+
|
128
|
+
def initialize
|
129
|
+
@xx = @yy = @zz = 1.0
|
130
|
+
@xy = @xz = @yx = @yz = @zx = @zy = 0.0
|
131
|
+
@t1 = 0r
|
132
|
+
@tt = 1r
|
133
|
+
end
|
134
|
+
|
135
|
+
def apply event
|
136
|
+
event.beat = @t1 + @tt * event.beat
|
137
|
+
return unless x = event[:x]
|
138
|
+
return unless y = event[:y]
|
139
|
+
rx = xx*x + xy*y + xz
|
140
|
+
ry = yx*x + yy*y + yz
|
141
|
+
d = zx*x + zy*y + zz
|
142
|
+
event[:x] = xp = rx / d
|
143
|
+
event[:y] = yp = ry / d
|
144
|
+
|
145
|
+
return event unless angle = event[:angle]
|
146
|
+
dx = cos angle
|
147
|
+
dy = sin angle
|
148
|
+
cross = y*dx - x*dy
|
149
|
+
|
150
|
+
cx0 = zy*xx - xy*zx
|
151
|
+
cxx = zz*xx - xz*zx
|
152
|
+
cxy = zz*xy - xz*zy
|
153
|
+
dxp = cx0*cross + cxx*dx + cxy*dy
|
154
|
+
|
155
|
+
cy0 = zx*yy - yx*zy
|
156
|
+
cyy = zz*yy - yz*zy
|
157
|
+
cyx = zz*yx - yz*zx
|
158
|
+
dyp = cy0*-cross + cyy*dy + cyx*dx
|
159
|
+
|
160
|
+
event[:angle] = atan2 dyp, dxp
|
161
|
+
event
|
162
|
+
end
|
163
|
+
|
164
|
+
def compound_linear xx, xy, yx, yy
|
165
|
+
@xx, @xy, @xz, @yx, @yy, @yz = [
|
166
|
+
xx * @xx + xy * @yx,
|
167
|
+
xx * @xy + xy * @yy,
|
168
|
+
xx * @xz + xy * @yz,
|
169
|
+
yx * @xx + yy * @yx,
|
170
|
+
yx * @xy + yy * @yy,
|
171
|
+
yx * @xz + yy * @yz,
|
172
|
+
]
|
173
|
+
end
|
174
|
+
|
175
|
+
def translate dx, dy
|
176
|
+
raise ArgumentError, 'dx and dy must be numbers' unless dx.is_a?(Numeric) && dy.is_a?(Numeric)
|
177
|
+
@xz += dx
|
178
|
+
@yz += dy
|
179
|
+
end
|
180
|
+
|
181
|
+
def horizontal_flip
|
182
|
+
compound_linear -1, 0, 0, 1
|
183
|
+
end
|
184
|
+
|
185
|
+
def vertical_flip
|
186
|
+
compound_linear 1, 0, 0, -1
|
187
|
+
end
|
188
|
+
|
189
|
+
def rotate angle
|
190
|
+
raise ArgumentError, 'angle must be a number' unless angle.is_a? Numeric
|
191
|
+
warn 'Are you using degrees as angle unit instead of radians?' if angle != 0 && angle % 45 == 0
|
192
|
+
c = cos angle
|
193
|
+
s = sin angle
|
194
|
+
compound_linear c, -s, s, c
|
195
|
+
end
|
196
|
+
|
197
|
+
def scale sx, sy = sx
|
198
|
+
raise ArgumentError, 'sx and sy must be numbers' unless sx.is_a?(Numeric) && sy.is_a?(Numeric)
|
199
|
+
compound_linear sx, 0, 0, sy
|
200
|
+
end
|
201
|
+
|
202
|
+
def beat_translate delta_beat
|
203
|
+
raise ArgumentError, 'delta_beat must be a number' unless delta_beat.is_a? Numeric
|
204
|
+
warn 'Rational is recommended over Float for delta_beat' if delta_beat.is_a? Float
|
205
|
+
@t1 += delta_beat.to_r
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
class TipPointStart
|
210
|
+
|
211
|
+
def initialize x = 0, y = 0, relative_time = 0.0, relative: true, speed: nil,
|
212
|
+
relative_beat: nil, beat_speed: nil
|
213
|
+
@x = x
|
214
|
+
@y = y
|
215
|
+
@relative_time = relative_time
|
216
|
+
@relative = relative
|
217
|
+
@speed = speed
|
218
|
+
@relative_beat = relative_beat
|
219
|
+
@beat_speed = beat_speed
|
220
|
+
check
|
221
|
+
end
|
222
|
+
|
223
|
+
def check
|
224
|
+
%i[@relative_time @speed @relative_beat @beat_speed].each do |key|
|
225
|
+
value = instance_variable_get key
|
226
|
+
raise ArgumentError, "cannot specify both #@time_key and #{key}" if @time_key && value&.!=(0)
|
227
|
+
@time_key = key if value&.!=(0)
|
228
|
+
end
|
229
|
+
@time_key ||= :@relative_time
|
230
|
+
end
|
231
|
+
|
232
|
+
def get_start_placeholder start_event
|
233
|
+
raise ArgumentError, "start_event is not tip-pointable" unless start_event.tip_pointable?
|
234
|
+
result = Event.new :placeholder, start_event.beat, start_event.bpm_changes
|
235
|
+
if @relative
|
236
|
+
result[:x] = start_event[:x] + @x
|
237
|
+
result[:y] = start_event[:y] + @y
|
238
|
+
else
|
239
|
+
result[:x] = @x
|
240
|
+
result[:y] = @y
|
241
|
+
end
|
242
|
+
case @time_key
|
243
|
+
when :@relative_time
|
244
|
+
raise ArgumentError, "relative_time must be a number" unless @relative_time.is_a? Numeric
|
245
|
+
raise ArgumentError, "relative_time must be non-negative" if @relative_time < 0
|
246
|
+
result.offset = -@relative_time.to_f
|
247
|
+
when :@speed
|
248
|
+
raise ArgumentError, "speed must be a number" unless @speed.is_a? Numeric
|
249
|
+
raise ArgumentError, "speed must be positive" if @speed <= 0
|
250
|
+
result.offset = -Math.hypot(result[:x] - start_event[:x], result[:y] - start_event[:y]) / @speed
|
251
|
+
when :@relative_beat
|
252
|
+
raise ArgumentError, "relative_beat must be a number" unless @relative_beat.is_a? Numeric
|
253
|
+
raise ArgumentError, "relative_beat must be non-negative" if @relative_beat < 0
|
254
|
+
warn "Rational is recommended over Float for relative_beat" if @relative_beat.is_a? Float
|
255
|
+
result.beat -= @relative_beat.to_r
|
256
|
+
when :@beat_speed
|
257
|
+
raise ArgumentError, "beat_speed must be a number" unless @beat_speed.is_a? Numeric
|
258
|
+
raise ArgumentError, "beat_speed must be positive" if @beat_speed <= 0
|
259
|
+
delta_beat = Math.hypot(result[:x] - start_event[:x], result[:y] - start_event[:y]) / @beat_speed
|
260
|
+
result.beat -= delta_beat.to_r # a little weird, but fine
|
261
|
+
end
|
262
|
+
result[:tip_point] = start_event[:tip_point]
|
263
|
+
result
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
COLORS = {
|
268
|
+
easy: '#3eb9fd',
|
269
|
+
normal: '#f19e56',
|
270
|
+
hard: '#e75e74',
|
271
|
+
master: '#8c68f3',
|
272
|
+
special: '#f156ee'
|
273
|
+
}
|
274
|
+
|
275
|
+
DIRECTIONS = {
|
276
|
+
right: 0.0,
|
277
|
+
up_right: Math::PI / 4,
|
278
|
+
up: Math::PI / 2,
|
279
|
+
up_left: Math::PI * 3 / 4,
|
280
|
+
left: Math::PI,
|
281
|
+
down_left: -Math::PI * 3 / 4,
|
282
|
+
down: -Math::PI / 2,
|
283
|
+
down_right: -Math::PI / 4
|
284
|
+
}
|
285
|
+
|
286
|
+
singleton_class.attr_reader :charts
|
287
|
+
@charts = {}
|
288
|
+
|
289
|
+
def self.open name, &block
|
290
|
+
result = @charts[name] ||= new name
|
291
|
+
result.instance_eval &block if block
|
292
|
+
result
|
293
|
+
end
|
294
|
+
|
295
|
+
def initialize name
|
296
|
+
@name = name
|
297
|
+
init_chart_info
|
298
|
+
init_state
|
299
|
+
end
|
300
|
+
|
301
|
+
def init_chart_info
|
302
|
+
@difficulty_name = ''
|
303
|
+
@difficulty_color = '#000000'
|
304
|
+
@difficulty = ''
|
305
|
+
@title = ''
|
306
|
+
@artist = ''
|
307
|
+
@charter = ''
|
308
|
+
@events = []
|
309
|
+
end
|
310
|
+
|
311
|
+
def init_state
|
312
|
+
@current_offset = nil
|
313
|
+
@current_beat = nil
|
314
|
+
@bpm_changes = nil
|
315
|
+
@tip_point_mode = :none
|
316
|
+
@current_tip_point = 0
|
317
|
+
@current_duplicate = 0
|
318
|
+
@tip_point_start_to_add = nil
|
319
|
+
@groups = [@events]
|
320
|
+
end
|
321
|
+
|
322
|
+
def title title
|
323
|
+
raise ArgumentError, 'title must be a string' unless title.is_a? String
|
324
|
+
@title = title
|
325
|
+
end
|
326
|
+
|
327
|
+
def artist artist
|
328
|
+
raise ArgumentError, 'artist must be a string' unless artist.is_a? String
|
329
|
+
@artist = artist
|
330
|
+
end
|
331
|
+
|
332
|
+
def charter charter
|
333
|
+
raise ArgumentError, 'charter must be a string' unless charter.is_a? String
|
334
|
+
@charter = charter
|
335
|
+
end
|
336
|
+
|
337
|
+
def difficulty_name difficulty_name
|
338
|
+
raise ArgumentError, 'difficulty_name must be a string' unless difficulty_name.is_a? String
|
339
|
+
@difficulty_name = difficulty_name
|
340
|
+
end
|
341
|
+
|
342
|
+
def difficulty_color difficulty_color
|
343
|
+
@difficulty_color = case difficulty_color
|
344
|
+
when Symbol
|
345
|
+
COLORS[difficulty_color]
|
346
|
+
when /^#[0-9a-fA-F]{6}$/
|
347
|
+
difficulty_color
|
348
|
+
when /^#[0-9a-fA-F]{3}$/
|
349
|
+
_, r, g, b = difficulty_color.chars
|
350
|
+
"##{r}#{r}#{g}#{g}#{b}#{b}"
|
351
|
+
when /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/
|
352
|
+
r, g, b = $1, $2, $3
|
353
|
+
sprintf '#%02x%02x%02x', r.to_i, g.to_i, b.to_i
|
354
|
+
when Integer
|
355
|
+
sprintf '#%06x', difficulty_color % 0x1000000
|
356
|
+
else
|
357
|
+
raise ArgumentError, 'unknown format of difficulty_color'
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def difficulty difficulty
|
362
|
+
@difficulty = difficulty.to_s
|
363
|
+
end
|
364
|
+
|
365
|
+
def offset offset
|
366
|
+
raise ArgumentError, 'offset must be a number' unless offset.is_a? Numeric
|
367
|
+
@current_offset = offset.to_f
|
368
|
+
@current_beat = 0r
|
369
|
+
@bpm_changes = BpmChangeList.new @current_offset
|
370
|
+
end
|
371
|
+
|
372
|
+
def bpm bpm
|
373
|
+
raise OffsetError.new __method__ unless @bpm_changes
|
374
|
+
@bpm_changes.add @current_beat, bpm
|
375
|
+
end
|
376
|
+
|
377
|
+
def beat delta_beat = 0
|
378
|
+
raise OffsetError.new __method__ unless @current_beat
|
379
|
+
case delta_beat
|
380
|
+
when Integer, Rational
|
381
|
+
@current_beat += delta_beat.to_r
|
382
|
+
when Float
|
383
|
+
warn 'float beat is not recommended'
|
384
|
+
@current_beat += delta_beat.to_r
|
385
|
+
else
|
386
|
+
raise ArgumentError, 'invalid delta_beat'
|
387
|
+
end
|
388
|
+
end
|
389
|
+
alias b beat
|
390
|
+
|
391
|
+
def beat! beat = @current_beat
|
392
|
+
raise OffsetError.new __method__ unless @current_beat
|
393
|
+
case beat
|
394
|
+
when Integer, Rational
|
395
|
+
@current_beat = beat.to_r
|
396
|
+
when Float
|
397
|
+
warn 'float beat is not recommended'
|
398
|
+
@current_beat = beat.to_r
|
399
|
+
else
|
400
|
+
raise ArgumentError, 'invalid beat'
|
401
|
+
end
|
402
|
+
end
|
403
|
+
alias b! beat!
|
404
|
+
|
405
|
+
def time_at beat = @current_beat
|
406
|
+
raise OffsetError.new __method__ unless @bpm_changes
|
407
|
+
@bpm_changes.time_at beat
|
408
|
+
end
|
409
|
+
|
410
|
+
def tip_point_chain *args, preserve_beat: true, **opts, &block
|
411
|
+
raise ArgumentError, 'no block given' unless block
|
412
|
+
tip_point :chain, *args, **opts do
|
413
|
+
group preserve_beat: preserve_beat, &block
|
414
|
+
end.tap { @current_tip_point += 1 }
|
415
|
+
end
|
416
|
+
alias tp_chain tip_point_chain
|
417
|
+
|
418
|
+
def tip_point_drop *args, preserve_beat: true, **opts, &block
|
419
|
+
raise ArgumentError, 'no block given' unless block
|
420
|
+
tip_point :drop, *args, **opts do
|
421
|
+
group preserve_beat: preserve_beat, &block
|
422
|
+
end
|
423
|
+
end
|
424
|
+
alias tp_drop tip_point_drop
|
425
|
+
|
426
|
+
def group preserve_beat: true, &block
|
427
|
+
raise ArgumentError, 'no block given' unless block
|
428
|
+
@groups.push result = []
|
429
|
+
last_beat = @current_beat
|
430
|
+
instance_eval &block
|
431
|
+
beat! last_beat unless preserve_beat
|
432
|
+
@groups.delete_if { result.equal? _1 }
|
433
|
+
result
|
434
|
+
end
|
435
|
+
|
436
|
+
def clear_tip_point
|
437
|
+
TipPointError.ensure @tip_point_mode, :chain, :drop
|
438
|
+
@tip_point_start_to_add = nil
|
439
|
+
@tip_point_mode = :none
|
440
|
+
end
|
441
|
+
|
442
|
+
def tip_point mode, *args, **opts, &block
|
443
|
+
TipPointError.ensure @tip_point_mode, :none
|
444
|
+
@tip_point_mode = mode
|
445
|
+
@tip_point_start_to_add = TipPointStart.new *args, **opts
|
446
|
+
result = block.()
|
447
|
+
@tip_point_start_to_add = nil
|
448
|
+
@tip_point_mode = :none
|
449
|
+
result
|
450
|
+
end
|
451
|
+
|
452
|
+
def event type, duration_beats = nil, **properties
|
453
|
+
raise OffsetError.new __method__ unless @bpm_changes
|
454
|
+
event = Event.new type, @current_beat, duration_beats, @bpm_changes, **properties
|
455
|
+
@groups.each { _1.push event }
|
456
|
+
return event unless event.tip_pointable?
|
457
|
+
case @tip_point_mode
|
458
|
+
when :chain
|
459
|
+
push_tip_point_start event
|
460
|
+
@tip_point_start_to_add = nil
|
461
|
+
when :drop
|
462
|
+
push_tip_point_start event
|
463
|
+
@current_tip_point += 1
|
464
|
+
when :none
|
465
|
+
# pass
|
466
|
+
end
|
467
|
+
event
|
468
|
+
end
|
469
|
+
|
470
|
+
def push_tip_point_start start_event
|
471
|
+
start_event[:tip_point] = @current_tip_point.to_s
|
472
|
+
tip_point_start = @tip_point_start_to_add&.get_start_placeholder start_event
|
473
|
+
@groups.each { _1.push tip_point_start } if tip_point_start
|
474
|
+
end
|
475
|
+
|
476
|
+
def transform events, &block
|
477
|
+
raise ArgumentError, 'no block given' unless block
|
478
|
+
events = [events] if events.is_a? Event
|
479
|
+
transform = Transform.new
|
480
|
+
transform.instance_eval &block
|
481
|
+
events.each { transform.apply _1 }
|
482
|
+
end
|
483
|
+
|
484
|
+
def duplicate events
|
485
|
+
result = []
|
486
|
+
events.each do |event|
|
487
|
+
result.push event = event.dup
|
488
|
+
if event[:tip_point]
|
489
|
+
event[:tip_point] = "#@current_duplicate #{event[:tip_point]}"
|
490
|
+
end
|
491
|
+
@groups.each { _1.push event }
|
492
|
+
end
|
493
|
+
result
|
494
|
+
end
|
495
|
+
|
496
|
+
def tap x, y, text = ''
|
497
|
+
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
498
|
+
raise ArgumentError, 'x and y must be numbers'
|
499
|
+
end
|
500
|
+
event :tap, x: x.to_f, y: y.to_f, text: text.to_s
|
501
|
+
end
|
502
|
+
alias t tap
|
503
|
+
|
504
|
+
def hold x, y, duration_beats, text = ''
|
505
|
+
if !x.is_a?(Numeric) || !y.is_a?(Numeric) || !duration_beats.is_a?(Numeric)
|
506
|
+
raise ArgumentError, 'x, y, and duration must be numbers'
|
507
|
+
end
|
508
|
+
if duration_beats <= 0
|
509
|
+
raise ArgumentError, 'duration must be positive'
|
510
|
+
end
|
511
|
+
if duration_beats.is_a? Float
|
512
|
+
warn 'Rational is recommended over Float for duration_beats'
|
513
|
+
end
|
514
|
+
event :hold, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
|
515
|
+
end
|
516
|
+
alias h hold
|
517
|
+
|
518
|
+
def drag x, y
|
519
|
+
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
520
|
+
raise ArgumentError, 'x and y must be numbers'
|
521
|
+
end
|
522
|
+
event :drag, x: x.to_f, y: y.to_f
|
523
|
+
end
|
524
|
+
alias d drag
|
525
|
+
|
526
|
+
def flick x, y, direction, text = ''
|
527
|
+
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
528
|
+
raise ArgumentError, 'x and y must be numbers'
|
529
|
+
end
|
530
|
+
if direction.is_a? Symbol
|
531
|
+
direction = DIRECTIONS[direction]
|
532
|
+
elsif direction.is_a? Numeric
|
533
|
+
warn 'Are you using degrees as angle unit instead of radians?' if direction != 0 && direction % 45 == 0
|
534
|
+
direction = direction.to_f
|
535
|
+
else
|
536
|
+
raise ArgumentError, 'direction must be a symbol or a number'
|
537
|
+
end
|
538
|
+
event :flick, x: x, y: y, angle: direction, text: text.to_s
|
539
|
+
end
|
540
|
+
alias f flick
|
541
|
+
|
542
|
+
def bg_note x, y, duration_beats = 0, text = ''
|
543
|
+
if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
544
|
+
raise ArgumentError, 'x and y must be numbers'
|
545
|
+
end
|
546
|
+
if duration_beats < 0
|
547
|
+
raise ArgumentError, 'duration must be non-negative'
|
548
|
+
end
|
549
|
+
if duration_beats.is_a? Float
|
550
|
+
warn 'Rational is recommended over Float for duration_beats'
|
551
|
+
end
|
552
|
+
event :bg_note, duration_beats.to_r, x: x.to_f, y: y.to_f, text: text.to_s
|
553
|
+
end
|
554
|
+
|
555
|
+
def big_text duration_beats = 0, text
|
556
|
+
event :big_text, duration_beats.to_r, text: text.to_s
|
557
|
+
end
|
558
|
+
|
559
|
+
%i[grid hexagon checkerboard diamond_grid pentagon turntable].each do |method_name|
|
560
|
+
define_method method_name do |duration_beats = 0|
|
561
|
+
if duration_beats < 0
|
562
|
+
raise ArgumentError, 'duration must be non-negative'
|
563
|
+
end
|
564
|
+
if duration_beats.is_a? Float
|
565
|
+
warn 'Rational is recommended over Float for duration_beats'
|
566
|
+
end
|
567
|
+
event method_name, duration_beats.to_r
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def to_sunniesnow
|
572
|
+
result = Sunniesnow::Chart.new
|
573
|
+
result.title = @title
|
574
|
+
result.artist = @artist
|
575
|
+
result.charter = @charter
|
576
|
+
result.difficulty_name = @difficulty_name
|
577
|
+
result.difficulty_color = @difficulty_color
|
578
|
+
result.difficulty = @difficulty
|
579
|
+
@events.each { result.events.push _1.to_sunniesnow }
|
580
|
+
result
|
581
|
+
end
|
582
|
+
|
583
|
+
def output_json
|
584
|
+
to_sunniesnow.to_json
|
585
|
+
end
|
586
|
+
|
587
|
+
end
|