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.
- 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
|