sonolus.py 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

Files changed (68) hide show
  1. sonolus/backend/allocate.py +125 -51
  2. sonolus/backend/blocks.py +756 -756
  3. sonolus/backend/coalesce.py +85 -0
  4. sonolus/backend/constant_evaluation.py +374 -0
  5. sonolus/backend/dead_code.py +80 -0
  6. sonolus/backend/dominance.py +111 -0
  7. sonolus/backend/excepthook.py +37 -37
  8. sonolus/backend/finalize.py +69 -69
  9. sonolus/backend/flow.py +121 -92
  10. sonolus/backend/inlining.py +150 -0
  11. sonolus/backend/ir.py +5 -3
  12. sonolus/backend/liveness.py +173 -0
  13. sonolus/backend/mode.py +24 -24
  14. sonolus/backend/node.py +40 -40
  15. sonolus/backend/ops.py +197 -197
  16. sonolus/backend/optimize.py +37 -9
  17. sonolus/backend/passes.py +52 -6
  18. sonolus/backend/simplify.py +47 -30
  19. sonolus/backend/ssa.py +187 -0
  20. sonolus/backend/utils.py +48 -48
  21. sonolus/backend/visitor.py +892 -882
  22. sonolus/build/cli.py +7 -1
  23. sonolus/build/compile.py +88 -90
  24. sonolus/build/level.py +24 -23
  25. sonolus/build/node.py +43 -43
  26. sonolus/script/archetype.py +23 -6
  27. sonolus/script/array.py +2 -2
  28. sonolus/script/bucket.py +191 -191
  29. sonolus/script/callbacks.py +127 -127
  30. sonolus/script/comptime.py +1 -1
  31. sonolus/script/containers.py +23 -0
  32. sonolus/script/debug.py +19 -3
  33. sonolus/script/easing.py +323 -0
  34. sonolus/script/effect.py +131 -131
  35. sonolus/script/globals.py +269 -269
  36. sonolus/script/graphics.py +200 -150
  37. sonolus/script/instruction.py +151 -151
  38. sonolus/script/internal/__init__.py +5 -5
  39. sonolus/script/internal/builtin_impls.py +144 -144
  40. sonolus/script/internal/context.py +12 -4
  41. sonolus/script/internal/descriptor.py +17 -17
  42. sonolus/script/internal/introspection.py +14 -14
  43. sonolus/script/internal/native.py +40 -38
  44. sonolus/script/internal/value.py +3 -3
  45. sonolus/script/interval.py +120 -112
  46. sonolus/script/iterator.py +214 -214
  47. sonolus/script/math.py +30 -1
  48. sonolus/script/num.py +1 -1
  49. sonolus/script/options.py +191 -191
  50. sonolus/script/particle.py +157 -157
  51. sonolus/script/pointer.py +30 -30
  52. sonolus/script/print.py +81 -81
  53. sonolus/script/random.py +14 -0
  54. sonolus/script/range.py +58 -58
  55. sonolus/script/record.py +3 -3
  56. sonolus/script/runtime.py +2 -0
  57. sonolus/script/sprite.py +333 -333
  58. sonolus/script/text.py +407 -407
  59. sonolus/script/timing.py +42 -42
  60. sonolus/script/transform.py +77 -23
  61. sonolus/script/ui.py +160 -160
  62. sonolus/script/vec.py +81 -78
  63. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/METADATA +1 -1
  64. sonolus_py-0.1.4.dist-info/RECORD +84 -0
  65. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/WHEEL +1 -1
  66. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/licenses/LICENSE +21 -21
  67. sonolus_py-0.1.3.dist-info/RECORD +0 -75
  68. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.4.dist-info}/entry_points.txt +0 -0
sonolus/script/debug.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from collections.abc import Callable
2
+ from contextvars import ContextVar
2
3
  from typing import Any, Never
3
4
 
4
5
  from sonolus.backend.flow import cfg_to_mermaid
5
6
  from sonolus.backend.mode import Mode
6
7
  from sonolus.backend.ops import Op
8
+ from sonolus.backend.passes import CompilerPass, run_passes
7
9
  from sonolus.backend.simplify import CoalesceFlow
8
10
  from sonolus.script.comptime import Comptime
9
11
  from sonolus.script.internal.context import GlobalContextState, ctx, set_ctx
@@ -12,6 +14,8 @@ from sonolus.script.internal.native import native_function
12
14
  from sonolus.script.num import Num
13
15
  from sonolus.script.values import with_default
14
16
 
17
+ debug_log_callback = ContextVar[Callable[[Num], None]]("debug_log_callback")
18
+
15
19
 
16
20
  @meta_fn
17
21
  def error(message: str | None = None) -> None:
@@ -26,9 +30,18 @@ def error(message: str | None = None) -> None:
26
30
  raise RuntimeError(message)
27
31
 
28
32
 
29
- @native_function(Op.DebugLog)
33
+ @meta_fn
30
34
  def debug_log(value: Num):
35
+ if debug_log_callback.get(None):
36
+ return debug_log_callback.get()(value)
37
+ else:
38
+ return _debug_log(value)
39
+
40
+
41
+ @native_function(Op.DebugLog)
42
+ def _debug_log(value: Num):
31
43
  print(f"[DEBUG] {value}")
44
+ return 0
32
45
 
33
46
 
34
47
  @native_function(Op.DebugPause)
@@ -62,9 +75,12 @@ def terminate():
62
75
  raise RuntimeError("Terminated")
63
76
 
64
77
 
65
- def visualize_cfg(fn: Callable[[], Any]) -> str:
78
+ def visualize_cfg(fn: Callable[[], Any], passes: list[CompilerPass] | None = None) -> str:
66
79
  from sonolus.build.compile import callback_to_cfg
67
80
 
81
+ if passes is None:
82
+ passes = [CoalesceFlow()]
83
+
68
84
  cfg = callback_to_cfg(GlobalContextState(Mode.PLAY), fn, "")
69
- cfg = CoalesceFlow().run(cfg)
85
+ cfg = run_passes(cfg, passes)
70
86
  return cfg_to_mermaid(cfg)
@@ -0,0 +1,323 @@
1
+ import math
2
+
3
+ from sonolus.backend.ops import Op
4
+ from sonolus.script.internal.native import native_function
5
+ from sonolus.script.interval import clamp
6
+
7
+
8
+ @native_function(Op.EaseInBack)
9
+ def ease_in_back(x: float) -> float:
10
+ x = clamp(x, 0, 1)
11
+ c1 = 1.70158
12
+ c3 = c1 + 1
13
+ return c3 * x**3 - c1 * x**2
14
+
15
+
16
+ @native_function(Op.EaseOutBack)
17
+ def ease_out_back(x: float) -> float:
18
+ x = clamp(x, 0, 1)
19
+ c1 = 1.70158
20
+ c3 = c1 + 1
21
+ return 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2
22
+
23
+
24
+ @native_function(Op.EaseInOutBack)
25
+ def ease_in_out_back(x: float) -> float:
26
+ x = clamp(x, 0, 1)
27
+ c1 = 1.70158
28
+ c2 = c1 * 1.525
29
+ if x < 0.5:
30
+ return ((2 * x) ** 2 * ((c2 + 1) * 2 * x - c2)) / 2
31
+ else:
32
+ return ((2 * x - 2) ** 2 * ((c2 + 1) * (2 * x - 2) + c2) + 2) / 2
33
+
34
+
35
+ @native_function(Op.EaseInCirc)
36
+ def ease_in_circ(x: float) -> float:
37
+ x = clamp(x, 0, 1)
38
+ return 1 - math.sqrt(1 - x**2)
39
+
40
+
41
+ @native_function(Op.EaseOutCirc)
42
+ def ease_out_circ(x: float) -> float:
43
+ x = clamp(x, 0, 1)
44
+ return math.sqrt(1 - (x - 1) ** 2)
45
+
46
+
47
+ @native_function(Op.EaseInOutCirc)
48
+ def ease_in_out_circ(x: float) -> float:
49
+ x = clamp(x, 0, 1)
50
+ if x < 0.5:
51
+ return (1 - math.sqrt(1 - (2 * x) ** 2)) / 2
52
+ else:
53
+ return (math.sqrt(1 - (2 * x - 2) ** 2) + 1) / 2
54
+
55
+
56
+ @native_function(Op.EaseInCubic)
57
+ def ease_in_cubic(x: float) -> float:
58
+ x = clamp(x, 0, 1)
59
+ return x**3
60
+
61
+
62
+ @native_function(Op.EaseOutCubic)
63
+ def ease_out_cubic(x: float) -> float:
64
+ x = clamp(x, 0, 1)
65
+ return 1 - (1 - x) ** 3
66
+
67
+
68
+ @native_function(Op.EaseInOutCubic)
69
+ def ease_in_out_cubic(x: float) -> float:
70
+ x = clamp(x, 0, 1)
71
+ if x < 0.5:
72
+ return 4 * x**3
73
+ else:
74
+ return 1 - (-2 * x + 2) ** 3 / 2
75
+
76
+
77
+ @native_function(Op.EaseInElastic)
78
+ def ease_in_elastic(x: float) -> float:
79
+ x = clamp(x, 0, 1)
80
+ c4 = (2 * math.pi) / 3
81
+ if x in {0, 1}:
82
+ return x
83
+ else:
84
+ return -(2 ** (10 * x - 10)) * math.sin((x * 10 - 10.75) * c4)
85
+
86
+
87
+ @native_function(Op.EaseOutElastic)
88
+ def ease_out_elastic(x: float) -> float:
89
+ x = clamp(x, 0, 1)
90
+ c4 = (2 * math.pi) / 3
91
+ if x in {0, 1}:
92
+ return x
93
+ else:
94
+ return 2 ** (-10 * x) * math.sin((x * 10 - 0.75) * c4) + 1
95
+
96
+
97
+ @native_function(Op.EaseInOutElastic)
98
+ def ease_in_out_elastic(x: float) -> float:
99
+ x = clamp(x, 0, 1)
100
+ c5 = (2 * math.pi) / 4.5
101
+ if x in {0, 1}:
102
+ return x
103
+ elif x < 0.5:
104
+ return -(2 ** (20 * x - 10) * math.sin((20 * x - 11.125) * c5)) / 2
105
+ else:
106
+ return (2 ** (-20 * x + 10) * math.sin((20 * x - 11.125) * c5)) / 2 + 1
107
+
108
+
109
+ @native_function(Op.EaseInExpo)
110
+ def ease_in_expo(x: float) -> float:
111
+ x = clamp(x, 0, 1)
112
+ return 0 if x == 0 else 2 ** (10 * x - 10)
113
+
114
+
115
+ @native_function(Op.EaseOutExpo)
116
+ def ease_out_expo(x: float) -> float:
117
+ x = clamp(x, 0, 1)
118
+ return 1 if x == 1 else 1 - 2 ** (-10 * x)
119
+
120
+
121
+ @native_function(Op.EaseInOutExpo)
122
+ def ease_in_out_expo(x: float) -> float:
123
+ x = clamp(x, 0, 1)
124
+ if x in {0, 1}:
125
+ return x
126
+ elif x < 0.5:
127
+ return 2 ** (20 * x - 10) / 2
128
+ else:
129
+ return (2 - 2 ** (-20 * x + 10)) / 2
130
+
131
+
132
+ @native_function(Op.EaseInOutQuad)
133
+ def ease_in_out_quad(x: float) -> float:
134
+ x = clamp(x, 0, 1)
135
+ if x < 0.5:
136
+ return 2 * x**2
137
+ else:
138
+ return 1 - (-2 * x + 2) ** 2 / 2
139
+
140
+
141
+ @native_function(Op.EaseInOutQuart)
142
+ def ease_in_out_quart(x: float) -> float:
143
+ x = clamp(x, 0, 1)
144
+ if x < 0.5:
145
+ return 8 * x**4
146
+ else:
147
+ return 1 - (-2 * x + 2) ** 4 / 2
148
+
149
+
150
+ @native_function(Op.EaseInOutQuint)
151
+ def ease_in_out_quint(x: float) -> float:
152
+ x = clamp(x, 0, 1)
153
+ if x < 0.5:
154
+ return 16 * x**5
155
+ else:
156
+ return 1 - (-2 * x + 2) ** 5 / 2
157
+
158
+
159
+ @native_function(Op.EaseInOutSine)
160
+ def ease_in_out_sine(x: float) -> float:
161
+ x = clamp(x, 0, 1)
162
+ return -(math.cos(math.pi * x) - 1) / 2
163
+
164
+
165
+ @native_function(Op.EaseInQuad)
166
+ def ease_in_quad(x: float) -> float:
167
+ x = clamp(x, 0, 1)
168
+ return x**2
169
+
170
+
171
+ @native_function(Op.EaseOutQuad)
172
+ def ease_out_quad(x: float) -> float:
173
+ x = clamp(x, 0, 1)
174
+ return 1 - (1 - x) ** 2
175
+
176
+
177
+ @native_function(Op.EaseInQuart)
178
+ def ease_in_quart(x: float) -> float:
179
+ x = clamp(x, 0, 1)
180
+ return x**4
181
+
182
+
183
+ @native_function(Op.EaseOutQuart)
184
+ def ease_out_quart(x: float) -> float:
185
+ x = clamp(x, 0, 1)
186
+ return 1 - (1 - x) ** 4
187
+
188
+
189
+ @native_function(Op.EaseInQuint)
190
+ def ease_in_quint(x: float) -> float:
191
+ x = clamp(x, 0, 1)
192
+ return x**5
193
+
194
+
195
+ @native_function(Op.EaseOutQuint)
196
+ def ease_out_quint(x: float) -> float:
197
+ x = clamp(x, 0, 1)
198
+ return 1 - (1 - x) ** 5
199
+
200
+
201
+ @native_function(Op.EaseInSine)
202
+ def ease_in_sine(x: float) -> float:
203
+ x = clamp(x, 0, 1)
204
+ return 1 - math.cos((x * math.pi) / 2)
205
+
206
+
207
+ @native_function(Op.EaseOutSine)
208
+ def ease_out_sine(x: float) -> float:
209
+ x = clamp(x, 0, 1)
210
+ return math.sin((x * math.pi) / 2)
211
+
212
+
213
+ @native_function(Op.EaseOutInBack)
214
+ def ease_out_in_back(x: float) -> float:
215
+ x = clamp(x, 0, 1)
216
+ c1 = 1.70158
217
+ c3 = c1 + 1
218
+ if x < 0.5:
219
+ return (1 + c3 * (2 * x - 1) ** 3 + c1 * (2 * x - 1) ** 2) / 2
220
+ else:
221
+ return (c3 * (2 * x - 1) ** 3 - c1 * (2 * x - 1) ** 2) / 2 + 0.5
222
+
223
+
224
+ @native_function(Op.EaseOutInCirc)
225
+ def ease_out_in_circ(x: float) -> float:
226
+ x = clamp(x, 0, 1)
227
+ if x < 0.5:
228
+ return (math.sqrt(1 - (2 * x - 1) ** 2)) / 2
229
+ else:
230
+ return (1 - math.sqrt(1 - (2 * x - 1) ** 2)) / 2 + 0.5
231
+
232
+
233
+ @native_function(Op.EaseOutInCubic)
234
+ def ease_out_in_cubic(x: float) -> float:
235
+ x = clamp(x, 0, 1)
236
+ if x < 0.5:
237
+ return (1 - (1 - 2 * x) ** 3) / 2
238
+ else:
239
+ return ((2 * x - 1) ** 3) / 2 + 0.5
240
+
241
+
242
+ @native_function(Op.EaseOutInElastic)
243
+ def ease_out_in_elastic(x: float) -> float:
244
+ x = clamp(x, 0, 1)
245
+ c4 = (2 * math.pi) / 3
246
+ if x < 0.5:
247
+ if x == 0:
248
+ return 0
249
+ else:
250
+ return (2 ** (-20 * x + 10) * math.sin((20 * x - 0.75) * c4)) / 2 + 0.5
251
+ elif x == 1:
252
+ return 1
253
+ else:
254
+ return (-(2 ** (10 * (2 * x - 1) - 10)) * math.sin((20 * x - 10.75) * c4)) / 2 + 0.5
255
+
256
+
257
+ @native_function(Op.EaseOutInExpo)
258
+ def ease_out_in_expo(x: float) -> float:
259
+ x = clamp(x, 0, 1)
260
+ if x in {0, 1}:
261
+ return x
262
+ elif x < 0.5:
263
+ return (1 - 2 ** (-20 * x)) / 2
264
+ else:
265
+ return (2 ** (20 * x - 20)) / 2 + 0.5
266
+
267
+
268
+ @native_function(Op.EaseOutInQuad)
269
+ def ease_out_in_quad(x: float) -> float:
270
+ x = clamp(x, 0, 1)
271
+ if x < 0.5:
272
+ return (1 - (1 - 2 * x) ** 2) / 2
273
+ else:
274
+ return ((2 * x - 1) ** 2) / 2 + 0.5
275
+
276
+
277
+ @native_function(Op.EaseOutInQuart)
278
+ def ease_out_in_quart(x: float) -> float:
279
+ x = clamp(x, 0, 1)
280
+ if x < 0.5:
281
+ return (1 - (1 - 2 * x) ** 4) / 2
282
+ else:
283
+ return ((2 * x - 1) ** 4) / 2 + 0.5
284
+
285
+
286
+ @native_function(Op.EaseOutInQuint)
287
+ def ease_out_in_quint(x: float) -> float:
288
+ x = clamp(x, 0, 1)
289
+ if x < 0.5:
290
+ return (1 - (1 - 2 * x) ** 5) / 2
291
+ else:
292
+ return ((2 * x - 1) ** 5) / 2 + 0.5
293
+
294
+
295
+ @native_function(Op.EaseOutInSine)
296
+ def ease_out_in_sine(x: float) -> float:
297
+ x = clamp(x, 0, 1)
298
+ if x < 0.5:
299
+ return (math.sin(math.pi * x)) / 2
300
+ else:
301
+ return (1 - math.cos(math.pi * x)) / 2
302
+
303
+
304
+ def linstep(x: float) -> float:
305
+ return clamp(x, 0.0, 1.0)
306
+
307
+
308
+ def smoothstep(x: float) -> float:
309
+ x = clamp(x, 0.0, 1.0)
310
+ return x * x * (3 - 2 * x)
311
+
312
+
313
+ def smootherstep(x: float) -> float:
314
+ x = clamp(x, 0.0, 1.0)
315
+ return x * x * x * (x * (x * 6 - 15) + 10)
316
+
317
+
318
+ def step_start(x: float) -> float:
319
+ return 1.0 if x >= 0 else 0.0
320
+
321
+
322
+ def step_end(x: float) -> float:
323
+ return 1.0 if x >= 1 else 0.0
sonolus/script/effect.py CHANGED
@@ -1,131 +1,131 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Annotated, Any, NewType, dataclass_transform, get_origin
5
-
6
- from sonolus.backend.ops import Op
7
- from sonolus.script.internal.introspection import get_field_specifiers
8
- from sonolus.script.internal.native import native_function
9
- from sonolus.script.record import Record
10
-
11
-
12
- class Effect(Record):
13
- id: int
14
-
15
- def is_available(self) -> bool:
16
- return _has_effect_clip(self.id)
17
-
18
- def play(self, distance: float) -> None:
19
- _play(self.id, distance)
20
-
21
- def schedule(self, time: float, distance: float) -> None:
22
- _play_scheduled(self.id, time, distance)
23
-
24
- def loop(self) -> LoopedEffectHandle:
25
- return LoopedEffectHandle(_play_looped(self.id))
26
-
27
- def schedule_loop(self, start_time: float) -> ScheduledLoopedEffectHandle:
28
- return ScheduledLoopedEffectHandle(_play_looped_scheduled(self.id, start_time))
29
-
30
-
31
- class LoopedEffectHandle(Record):
32
- id: int
33
-
34
- def stop(self) -> None:
35
- _stop_looped(self.id)
36
-
37
-
38
- class ScheduledLoopedEffectHandle(Record):
39
- id: int
40
-
41
- def stop(self, end_time: float) -> None:
42
- _stop_looped_scheduled(self.id, end_time)
43
-
44
-
45
- @native_function(Op.HasEffectClip)
46
- def _has_effect_clip(effect_id: int) -> bool:
47
- raise NotImplementedError
48
-
49
-
50
- @native_function(Op.Play)
51
- def _play(effect_id: int, distance: float) -> None:
52
- raise NotImplementedError
53
-
54
-
55
- @native_function(Op.PlayLooped)
56
- def _play_looped(effect_id: int) -> int:
57
- raise NotImplementedError
58
-
59
-
60
- @native_function(Op.PlayLoopedScheduled)
61
- def _play_looped_scheduled(effect_id: int, start_time: float) -> int:
62
- raise NotImplementedError
63
-
64
-
65
- @native_function(Op.PlayScheduled)
66
- def _play_scheduled(effect_id: int, time: float, distance: float) -> None:
67
- raise NotImplementedError
68
-
69
-
70
- @native_function(Op.StopLooped)
71
- def _stop_looped(handle: int) -> None:
72
- raise NotImplementedError
73
-
74
-
75
- @native_function(Op.StopLoopedScheduled)
76
- def _stop_looped_scheduled(handle: int, end_time: float) -> None:
77
- raise NotImplementedError
78
-
79
-
80
- @dataclass
81
- class EffectInfo:
82
- name: str
83
-
84
-
85
- def effect(name: str) -> Any:
86
- return EffectInfo(name)
87
-
88
-
89
- type Effects = NewType("Effects", Any)
90
-
91
-
92
- @dataclass_transform()
93
- def effects[T](cls: type[T]) -> T | Effects:
94
- if len(cls.__bases__) != 1:
95
- raise ValueError("Effects class must not inherit from any class (except object)")
96
- instance = cls()
97
- names = []
98
- for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
99
- if get_origin(annotation) is not Annotated:
100
- raise TypeError(f"Invalid annotation for effects: {annotation}")
101
- annotation_type = annotation.__args__[0]
102
- annotation_values = annotation.__metadata__
103
- if annotation_type is not Effect:
104
- raise TypeError(f"Invalid annotation for effects: {annotation}, expected annotation of type Effect")
105
- if len(annotation_values) != 1 or not isinstance(annotation_values[0], EffectInfo):
106
- raise TypeError(f"Invalid annotation for effects: {annotation}, expected a single string annotation value")
107
- effect_name = annotation_values[0].name
108
- names.append(effect_name)
109
- setattr(instance, name, Effect(i))
110
- instance._effects_ = names
111
- instance._is_comptime_value_ = True
112
- return instance
113
-
114
-
115
- class StandardEffect:
116
- MISS = Annotated[Effect, effect("#MISS")]
117
- PERFECT = Annotated[Effect, effect("#PERFECT")]
118
- GREAT = Annotated[Effect, effect("#GREAT")]
119
- GOOD = Annotated[Effect, effect("#GOOD")]
120
- HOLD = Annotated[Effect, effect("#HOLD")]
121
- MISS_ALTERNATIVE = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
122
- PERFECT_ALTERNATIVE = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
123
- GREAT_ALTERNATIVE = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
124
- GOOD_ALTERNATIVE = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
125
- HOLD_ALTERNATIVE = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
126
- STAGE = Annotated[Effect, effect("#STAGE")]
127
-
128
-
129
- @effects
130
- class EmptyEffects:
131
- pass
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Annotated, Any, NewType, dataclass_transform, get_origin
5
+
6
+ from sonolus.backend.ops import Op
7
+ from sonolus.script.internal.introspection import get_field_specifiers
8
+ from sonolus.script.internal.native import native_function
9
+ from sonolus.script.record import Record
10
+
11
+
12
+ class Effect(Record):
13
+ id: int
14
+
15
+ def is_available(self) -> bool:
16
+ return _has_effect_clip(self.id)
17
+
18
+ def play(self, distance: float) -> None:
19
+ _play(self.id, distance)
20
+
21
+ def schedule(self, time: float, distance: float) -> None:
22
+ _play_scheduled(self.id, time, distance)
23
+
24
+ def loop(self) -> LoopedEffectHandle:
25
+ return LoopedEffectHandle(_play_looped(self.id))
26
+
27
+ def schedule_loop(self, start_time: float) -> ScheduledLoopedEffectHandle:
28
+ return ScheduledLoopedEffectHandle(_play_looped_scheduled(self.id, start_time))
29
+
30
+
31
+ class LoopedEffectHandle(Record):
32
+ id: int
33
+
34
+ def stop(self) -> None:
35
+ _stop_looped(self.id)
36
+
37
+
38
+ class ScheduledLoopedEffectHandle(Record):
39
+ id: int
40
+
41
+ def stop(self, end_time: float) -> None:
42
+ _stop_looped_scheduled(self.id, end_time)
43
+
44
+
45
+ @native_function(Op.HasEffectClip)
46
+ def _has_effect_clip(effect_id: int) -> bool:
47
+ raise NotImplementedError
48
+
49
+
50
+ @native_function(Op.Play)
51
+ def _play(effect_id: int, distance: float) -> None:
52
+ raise NotImplementedError
53
+
54
+
55
+ @native_function(Op.PlayLooped)
56
+ def _play_looped(effect_id: int) -> int:
57
+ raise NotImplementedError
58
+
59
+
60
+ @native_function(Op.PlayLoopedScheduled)
61
+ def _play_looped_scheduled(effect_id: int, start_time: float) -> int:
62
+ raise NotImplementedError
63
+
64
+
65
+ @native_function(Op.PlayScheduled)
66
+ def _play_scheduled(effect_id: int, time: float, distance: float) -> None:
67
+ raise NotImplementedError
68
+
69
+
70
+ @native_function(Op.StopLooped)
71
+ def _stop_looped(handle: int) -> None:
72
+ raise NotImplementedError
73
+
74
+
75
+ @native_function(Op.StopLoopedScheduled)
76
+ def _stop_looped_scheduled(handle: int, end_time: float) -> None:
77
+ raise NotImplementedError
78
+
79
+
80
+ @dataclass
81
+ class EffectInfo:
82
+ name: str
83
+
84
+
85
+ def effect(name: str) -> Any:
86
+ return EffectInfo(name)
87
+
88
+
89
+ type Effects = NewType("Effects", Any)
90
+
91
+
92
+ @dataclass_transform()
93
+ def effects[T](cls: type[T]) -> T | Effects:
94
+ if len(cls.__bases__) != 1:
95
+ raise ValueError("Effects class must not inherit from any class (except object)")
96
+ instance = cls()
97
+ names = []
98
+ for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
99
+ if get_origin(annotation) is not Annotated:
100
+ raise TypeError(f"Invalid annotation for effects: {annotation}")
101
+ annotation_type = annotation.__args__[0]
102
+ annotation_values = annotation.__metadata__
103
+ if annotation_type is not Effect:
104
+ raise TypeError(f"Invalid annotation for effects: {annotation}, expected annotation of type Effect")
105
+ if len(annotation_values) != 1 or not isinstance(annotation_values[0], EffectInfo):
106
+ raise TypeError(f"Invalid annotation for effects: {annotation}, expected a single string annotation value")
107
+ effect_name = annotation_values[0].name
108
+ names.append(effect_name)
109
+ setattr(instance, name, Effect(i))
110
+ instance._effects_ = names
111
+ instance._is_comptime_value_ = True
112
+ return instance
113
+
114
+
115
+ class StandardEffect:
116
+ MISS = Annotated[Effect, effect("#MISS")]
117
+ PERFECT = Annotated[Effect, effect("#PERFECT")]
118
+ GREAT = Annotated[Effect, effect("#GREAT")]
119
+ GOOD = Annotated[Effect, effect("#GOOD")]
120
+ HOLD = Annotated[Effect, effect("#HOLD")]
121
+ MISS_ALTERNATIVE = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
122
+ PERFECT_ALTERNATIVE = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
123
+ GREAT_ALTERNATIVE = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
124
+ GOOD_ALTERNATIVE = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
125
+ HOLD_ALTERNATIVE = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
126
+ STAGE = Annotated[Effect, effect("#STAGE")]
127
+
128
+
129
+ @effects
130
+ class EmptyEffects:
131
+ pass