sonolus.py 0.9.3__py3-none-any.whl → 0.10.1__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.

@@ -4,72 +4,72 @@ from sonolus.backend.ops import Op
4
4
  from sonolus.script.internal.native import native_function
5
5
 
6
6
 
7
- @native_function(Op.Sin)
7
+ @native_function(Op.Sin, const_eval=True)
8
8
  def _sin(x: float) -> float:
9
9
  return math.sin(x)
10
10
 
11
11
 
12
- @native_function(Op.Cos)
12
+ @native_function(Op.Cos, const_eval=True)
13
13
  def _cos(x: float) -> float:
14
14
  return math.cos(x)
15
15
 
16
16
 
17
- @native_function(Op.Tan)
17
+ @native_function(Op.Tan, const_eval=True)
18
18
  def _tan(x: float) -> float:
19
19
  return math.tan(x)
20
20
 
21
21
 
22
- @native_function(Op.Arcsin)
22
+ @native_function(Op.Arcsin, const_eval=True)
23
23
  def _asin(x: float) -> float:
24
24
  return math.asin(x)
25
25
 
26
26
 
27
- @native_function(Op.Arccos)
27
+ @native_function(Op.Arccos, const_eval=True)
28
28
  def _acos(x: float) -> float:
29
29
  return math.acos(x)
30
30
 
31
31
 
32
- @native_function(Op.Arctan)
32
+ @native_function(Op.Arctan, const_eval=True)
33
33
  def _atan(x: float) -> float:
34
34
  return math.atan(x)
35
35
 
36
36
 
37
- @native_function(Op.Arctan2)
37
+ @native_function(Op.Arctan2, const_eval=True)
38
38
  def _atan2(y: float, x: float) -> float:
39
39
  return math.atan2(y, x)
40
40
 
41
41
 
42
- @native_function(Op.Sinh)
42
+ @native_function(Op.Sinh, const_eval=True)
43
43
  def _sinh(x: float) -> float:
44
44
  return math.sinh(x)
45
45
 
46
46
 
47
- @native_function(Op.Cosh)
47
+ @native_function(Op.Cosh, const_eval=True)
48
48
  def _cosh(x: float) -> float:
49
49
  return math.cosh(x)
50
50
 
51
51
 
52
- @native_function(Op.Tanh)
52
+ @native_function(Op.Tanh, const_eval=True)
53
53
  def _tanh(x: float) -> float:
54
54
  return math.tanh(x)
55
55
 
56
56
 
57
- @native_function(Op.Floor)
57
+ @native_function(Op.Floor, const_eval=True)
58
58
  def _floor(x: float) -> float:
59
59
  return math.floor(x)
60
60
 
61
61
 
62
- @native_function(Op.Ceil)
62
+ @native_function(Op.Ceil, const_eval=True)
63
63
  def _ceil(x: float) -> float:
64
64
  return math.ceil(x)
65
65
 
66
66
 
67
- @native_function(Op.Trunc)
67
+ @native_function(Op.Trunc, const_eval=True)
68
68
  def _trunc(x: float) -> float:
69
69
  return math.trunc(x)
70
70
 
71
71
 
72
- @native_function(Op.Round)
72
+ @native_function(Op.Round, const_eval=True)
73
73
  def __round(x: float) -> float:
74
74
  return round(x)
75
75
 
@@ -80,12 +80,12 @@ def _round(x: float, n: int = 0) -> float:
80
80
  return __round(x * 10**n) / 10**n
81
81
 
82
82
 
83
- @native_function(Op.Frac)
83
+ @native_function(Op.Frac, const_eval=True)
84
84
  def frac(x: float) -> float:
85
85
  return x % 1
86
86
 
87
87
 
88
- @native_function(Op.Log)
88
+ @native_function(Op.Log, const_eval=True)
89
89
  def _ln(x: float) -> float:
90
90
  return math.log(x)
91
91
 
@@ -101,19 +101,19 @@ def _sqrt(x: float) -> float:
101
101
  return x**0.5
102
102
 
103
103
 
104
- @native_function(Op.Degree)
104
+ @native_function(Op.Degree, const_eval=True)
105
105
  def _degrees(x: float) -> float:
106
106
  """Convert radians to degrees."""
107
107
  return math.degrees(x)
108
108
 
109
109
 
110
- @native_function(Op.Radian)
110
+ @native_function(Op.Radian, const_eval=True)
111
111
  def _radians(x: float) -> float:
112
112
  """Convert degrees to radians."""
113
113
  return math.radians(x)
114
114
 
115
115
 
116
- @native_function(Op.Rem)
116
+ @native_function(Op.Rem, const_eval=True)
117
117
  def _remainder(x: float, y: float) -> float:
118
118
  # This is different from math.remainder in Python's math package, which could be confusing
119
119
  return math.copysign(abs(x) % abs(y), x)
@@ -20,7 +20,7 @@ def native_call(op: Op, *args: int | float | bool) -> Num:
20
20
  return Num._from_place_(result)
21
21
 
22
22
 
23
- def native_function[**P, R](op: Op) -> Callable[[Callable[P, R]], Callable[P, R]]:
23
+ def native_function[**P, R](op: Op, const_eval: bool = False) -> Callable[[Callable[P, R]], Callable[P, R]]:
24
24
  def decorator(fn: Callable[P, int | float | bool]) -> Callable[P, Num]:
25
25
  signature = inspect.signature(fn)
26
26
 
@@ -30,6 +30,12 @@ def native_function[**P, R](op: Op) -> Callable[[Callable[P, R]], Callable[P, R]
30
30
  if len(args) < sum(1 for p in signature.parameters.values() if p.default == inspect.Parameter.empty):
31
31
  raise TypeError(f"Expected {len(signature.parameters)} arguments, got {len(args)}")
32
32
  if ctx():
33
+ if const_eval:
34
+ args = tuple(validate_value(arg) for arg in args)
35
+ if not all(_is_num(arg) for arg in args):
36
+ raise RuntimeError("All arguments must be of type Num")
37
+ if all(arg._is_py_() for arg in args):
38
+ return Num._accept_(fn(*[arg._as_py_() for arg in args]))
33
39
  bound_args = signature.bind(*args)
34
40
  bound_args.apply_defaults()
35
41
  return native_call(op, *bound_args.args)
@@ -41,6 +41,9 @@ class Range(Record, ArrayLike[int]):
41
41
  def __getitem__(self, index: int) -> int:
42
42
  return self.start + get_positive_index(index, len(self)) * self.step
43
43
 
44
+ def get_unchecked(self, index: Num) -> int:
45
+ return self.start + index * self.step
46
+
44
47
  def __setitem__(self, index: int, value: int):
45
48
  raise TypeError("Range does not support item assignment")
46
49
 
@@ -374,9 +374,10 @@ def interp(
374
374
  Returns:
375
375
  The interpolated value.
376
376
  """
377
- assert len(xp) == len(fp)
378
- assert len(xp) >= 2
377
+ assert len(xp) == len(fp), "xp and fp must have the same length"
378
+ assert len(xp) >= 2, "xp and fp must have at least 2 elements"
379
379
  for i in range_or_tuple(1, len(xp) - 1):
380
+ assert xp[i] > xp[i - 1], "xp must be in increasing order"
380
381
  # At i == 1, x may be less than x[0], but since we're extrapolating, we use the first segment regardless.
381
382
  if x <= xp[i]:
382
383
  return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
@@ -402,11 +403,12 @@ def interp_clamped(
402
403
  Returns:
403
404
  The interpolated value.
404
405
  """
405
- assert len(xp) == len(fp)
406
- assert len(xp) >= 2
406
+ assert len(xp) == len(fp), "xp and fp must have the same length"
407
+ assert len(xp) >= 2, "xp and fp must have at least 2 elements"
407
408
  if x <= xp[0]:
408
409
  return fp[0]
409
410
  for i in range_or_tuple(1, len(xp)):
411
+ assert xp[i] > xp[i - 1], "xp must be in increasing order"
410
412
  if x <= xp[i]:
411
413
  return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
412
414
  return fp[-1]
sonolus/script/options.py CHANGED
@@ -191,7 +191,7 @@ class _OptionField(SonolusDescriptor):
191
191
  if sim_ctx():
192
192
  return sim_ctx().get_or_put_value((instance, self), lambda: copy(self.info.default))
193
193
  if ctx():
194
- match ctx().global_state.mode:
194
+ match ctx().mode_state.mode:
195
195
  case Mode.PLAY:
196
196
  block = ctx().blocks.LevelOption
197
197
  case Mode.WATCH:
@@ -212,7 +212,7 @@ class _OptionField(SonolusDescriptor):
212
212
  if sim_ctx():
213
213
  return sim_ctx().set_or_put_value((instance, self), lambda: copy(self.info.default), value)
214
214
  if ctx() and debug_config().unchecked_writes:
215
- match ctx().global_state.mode:
215
+ match ctx().mode_state.mode:
216
216
  case Mode.PLAY:
217
217
  block = ctx().blocks.LevelOption
218
218
  case Mode.WATCH:
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from typing import Annotated, Any, NewType, dataclass_transform, get_origin
6
6
 
7
7
  from sonolus.backend.ops import Op
8
- from sonolus.script.array_like import ArrayLike
8
+ from sonolus.script.array_like import ArrayLike, check_positive_index
9
9
  from sonolus.script.debug import static_error
10
10
  from sonolus.script.internal.introspection import get_field_specifiers
11
11
  from sonolus.script.internal.native import native_function
@@ -71,7 +71,10 @@ class ParticleGroup(Record, ArrayLike[Particle]):
71
71
  return self.size
72
72
 
73
73
  def __getitem__(self, index: int) -> Particle:
74
- assert 0 <= index < self.size
74
+ check_positive_index(index, self.size)
75
+ return Particle(self.start_id + index)
76
+
77
+ def get_unchecked(self, index: int) -> Particle:
75
78
  return Particle(self.start_id + index)
76
79
 
77
80
  def __setitem__(self, index: int, value: Particle) -> None:
sonolus/script/project.py CHANGED
@@ -8,7 +8,6 @@ from typing import ClassVar, TypedDict
8
8
 
9
9
  from sonolus.backend.optimize import optimize
10
10
  from sonolus.backend.optimize.passes import CompilerPass
11
- from sonolus.build.compile import CompileCache
12
11
  from sonolus.script.archetype import ArchetypeSchema
13
12
  from sonolus.script.engine import Engine
14
13
  from sonolus.script.level import ExternalLevelData, Level, LevelData
@@ -64,13 +63,11 @@ class Project:
64
63
  port: The port of the development server.
65
64
  config: The build configuration.
66
65
  """
67
- from sonolus.build.cli import build_collection, run_server
66
+ from sonolus.build.cli import run_server
68
67
 
69
68
  if config is None:
70
69
  config = BuildConfig()
71
70
 
72
- cache = CompileCache()
73
- build_collection(self, Path(build_dir), config, cache=cache)
74
71
  run_server(
75
72
  Path(build_dir) / "site",
76
73
  port=port,
@@ -78,7 +75,7 @@ class Project:
78
75
  core_module_names=None,
79
76
  build_dir=Path(build_dir),
80
77
  config=config,
81
- cache=cache,
78
+ project=self,
82
79
  )
83
80
 
84
81
  def build(self, build_dir: PathLike, config: BuildConfig | None = None):
sonolus/script/runtime.py CHANGED
@@ -343,7 +343,7 @@ class RuntimeUi(Record):
343
343
 
344
344
  Available in play, watch, preview, and tutorial mode.
345
345
  """
346
- match ctx().global_state.mode:
346
+ match ctx().mode_state.mode:
347
347
  case Mode.PLAY:
348
348
  return UiLayout(_PlayRuntimeUi.menu)
349
349
  case Mode.WATCH:
@@ -362,7 +362,7 @@ class RuntimeUi(Record):
362
362
 
363
363
  Available in play, watch, preview, and tutorial mode.
364
364
  """
365
- match ctx().global_state.mode:
365
+ match ctx().mode_state.mode:
366
366
  case Mode.PLAY:
367
367
  return UiConfig(_PlayRuntimeUiConfigs.menu)
368
368
  case Mode.WATCH:
@@ -381,7 +381,7 @@ class RuntimeUi(Record):
381
381
 
382
382
  Available in play and watch mode.
383
383
  """
384
- match ctx().global_state.mode:
384
+ match ctx().mode_state.mode:
385
385
  case Mode.PLAY:
386
386
  return UiLayout(_PlayRuntimeUi.judgment)
387
387
  case Mode.WATCH:
@@ -396,7 +396,7 @@ class RuntimeUi(Record):
396
396
 
397
397
  Available in play and watch mode.
398
398
  """
399
- match ctx().global_state.mode:
399
+ match ctx().mode_state.mode:
400
400
  case Mode.PLAY:
401
401
  return UiConfig(_PlayRuntimeUiConfigs.judgment)
402
402
  case Mode.WATCH:
@@ -411,7 +411,7 @@ class RuntimeUi(Record):
411
411
 
412
412
  Available in play and watch mode.
413
413
  """
414
- match ctx().global_state.mode:
414
+ match ctx().mode_state.mode:
415
415
  case Mode.PLAY:
416
416
  return UiLayout(_PlayRuntimeUi.combo_value)
417
417
  case Mode.WATCH:
@@ -426,7 +426,7 @@ class RuntimeUi(Record):
426
426
 
427
427
  Available in play and watch mode.
428
428
  """
429
- match ctx().global_state.mode:
429
+ match ctx().mode_state.mode:
430
430
  case Mode.PLAY:
431
431
  return UiLayout(_PlayRuntimeUi.combo_text)
432
432
  case Mode.WATCH:
@@ -441,7 +441,7 @@ class RuntimeUi(Record):
441
441
 
442
442
  Available in play and watch mode.
443
443
  """
444
- match ctx().global_state.mode:
444
+ match ctx().mode_state.mode:
445
445
  case Mode.PLAY:
446
446
  return UiConfig(_PlayRuntimeUiConfigs.combo)
447
447
  case Mode.WATCH:
@@ -456,7 +456,7 @@ class RuntimeUi(Record):
456
456
 
457
457
  Available in play and watch mode.
458
458
  """
459
- match ctx().global_state.mode:
459
+ match ctx().mode_state.mode:
460
460
  case Mode.PLAY:
461
461
  return UiLayout(_PlayRuntimeUi.primary_metric_bar)
462
462
  case Mode.WATCH:
@@ -471,7 +471,7 @@ class RuntimeUi(Record):
471
471
 
472
472
  Available in play and watch mode.
473
473
  """
474
- match ctx().global_state.mode:
474
+ match ctx().mode_state.mode:
475
475
  case Mode.PLAY:
476
476
  return UiLayout(_PlayRuntimeUi.primary_metric_value)
477
477
  case Mode.WATCH:
@@ -486,7 +486,7 @@ class RuntimeUi(Record):
486
486
 
487
487
  Available in play and watch mode.
488
488
  """
489
- match ctx().global_state.mode:
489
+ match ctx().mode_state.mode:
490
490
  case Mode.PLAY:
491
491
  return UiConfig(_PlayRuntimeUiConfigs.primary_metric)
492
492
  case Mode.WATCH:
@@ -501,7 +501,7 @@ class RuntimeUi(Record):
501
501
 
502
502
  Available in play and watch mode.
503
503
  """
504
- match ctx().global_state.mode:
504
+ match ctx().mode_state.mode:
505
505
  case Mode.PLAY:
506
506
  return UiLayout(_PlayRuntimeUi.secondary_metric_bar)
507
507
  case Mode.WATCH:
@@ -516,7 +516,7 @@ class RuntimeUi(Record):
516
516
 
517
517
  Available in play and watch mode.
518
518
  """
519
- match ctx().global_state.mode:
519
+ match ctx().mode_state.mode:
520
520
  case Mode.PLAY:
521
521
  return UiLayout(_PlayRuntimeUi.secondary_metric_value)
522
522
  case Mode.WATCH:
@@ -531,7 +531,7 @@ class RuntimeUi(Record):
531
531
 
532
532
  Available in play and watch mode.
533
533
  """
534
- match ctx().global_state.mode:
534
+ match ctx().mode_state.mode:
535
535
  case Mode.PLAY:
536
536
  return UiConfig(_PlayRuntimeUiConfigs.secondary_metric)
537
537
  case Mode.WATCH:
@@ -546,7 +546,7 @@ class RuntimeUi(Record):
546
546
 
547
547
  Available in watch and preview mode.
548
548
  """
549
- match ctx().global_state.mode:
549
+ match ctx().mode_state.mode:
550
550
  case Mode.WATCH:
551
551
  return UiLayout(_WatchRuntimeUi.progress)
552
552
  case Mode.PREVIEW:
@@ -561,7 +561,7 @@ class RuntimeUi(Record):
561
561
 
562
562
  Available in watch and preview mode.
563
563
  """
564
- match ctx().global_state.mode:
564
+ match ctx().mode_state.mode:
565
565
  case Mode.WATCH:
566
566
  return UiConfig(_WatchRuntimeUiConfigs.progress)
567
567
  case Mode.PREVIEW:
@@ -576,7 +576,7 @@ class RuntimeUi(Record):
576
576
 
577
577
  Available in tutorial mode.
578
578
  """
579
- match ctx().global_state.mode:
579
+ match ctx().mode_state.mode:
580
580
  case Mode.TUTORIAL:
581
581
  return UiLayout(_TutorialRuntimeUi.previous)
582
582
  case _:
@@ -589,7 +589,7 @@ class RuntimeUi(Record):
589
589
 
590
590
  Available in tutorial mode.
591
591
  """
592
- match ctx().global_state.mode:
592
+ match ctx().mode_state.mode:
593
593
  case Mode.TUTORIAL:
594
594
  return UiLayout(_TutorialRuntimeUi.next)
595
595
  case _:
@@ -602,7 +602,7 @@ class RuntimeUi(Record):
602
602
 
603
603
  Available in tutorial mode.
604
604
  """
605
- match ctx().global_state.mode:
605
+ match ctx().mode_state.mode:
606
606
  case Mode.TUTORIAL:
607
607
  return UiConfig(_TutorialRuntimeUiConfigs.navigation)
608
608
  case _:
@@ -615,7 +615,7 @@ class RuntimeUi(Record):
615
615
 
616
616
  Available in tutorial mode.
617
617
  """
618
- match ctx().global_state.mode:
618
+ match ctx().mode_state.mode:
619
619
  case Mode.TUTORIAL:
620
620
  return UiLayout(_TutorialRuntimeUi.instruction)
621
621
  case _:
@@ -628,7 +628,7 @@ class RuntimeUi(Record):
628
628
 
629
629
  Available in tutorial mode.
630
630
  """
631
- match ctx().global_state.mode:
631
+ match ctx().mode_state.mode:
632
632
  case Mode.TUTORIAL:
633
633
  return UiConfig(_TutorialRuntimeUiConfigs.instruction)
634
634
  case _:
@@ -841,7 +841,7 @@ def is_debug() -> bool:
841
841
  """Check if the game is running in debug mode."""
842
842
  if not ctx():
843
843
  return False
844
- match ctx().global_state.mode:
844
+ match ctx().mode_state.mode:
845
845
  case Mode.PLAY:
846
846
  return _PlayRuntimeEnvironment.is_debug
847
847
  case Mode.WATCH:
@@ -857,25 +857,25 @@ def is_debug() -> bool:
857
857
  @meta_fn
858
858
  def is_play() -> bool:
859
859
  """Check if the game is running in play mode."""
860
- return bool(ctx() and ctx().global_state.mode == Mode.PLAY)
860
+ return bool(ctx() and ctx().mode_state.mode == Mode.PLAY)
861
861
 
862
862
 
863
863
  @meta_fn
864
864
  def is_preview() -> bool:
865
865
  """Check if the game is running in preview mode."""
866
- return bool(ctx() and ctx().global_state.mode == Mode.PREVIEW)
866
+ return bool(ctx() and ctx().mode_state.mode == Mode.PREVIEW)
867
867
 
868
868
 
869
869
  @meta_fn
870
870
  def is_watch() -> bool:
871
871
  """Check if the game is running in watch mode."""
872
- return bool(ctx() and ctx().global_state.mode == Mode.WATCH)
872
+ return bool(ctx() and ctx().mode_state.mode == Mode.WATCH)
873
873
 
874
874
 
875
875
  @meta_fn
876
876
  def is_tutorial() -> bool:
877
877
  """Check if the game is running in tutorial mode."""
878
- return bool(ctx() and ctx().global_state.mode == Mode.TUTORIAL)
878
+ return bool(ctx() and ctx().mode_state.mode == Mode.TUTORIAL)
879
879
 
880
880
 
881
881
  @meta_fn
@@ -892,7 +892,7 @@ def aspect_ratio() -> float:
892
892
  """Get the aspect ratio of the game."""
893
893
  if not ctx():
894
894
  return 16 / 9
895
- match ctx().global_state.mode:
895
+ match ctx().mode_state.mode:
896
896
  case Mode.PLAY:
897
897
  return _PlayRuntimeEnvironment.aspect_ratio
898
898
  case Mode.WATCH:
@@ -911,7 +911,7 @@ def audio_offset() -> float:
911
911
  """
912
912
  if not ctx():
913
913
  return 0
914
- match ctx().global_state.mode:
914
+ match ctx().mode_state.mode:
915
915
  case Mode.PLAY:
916
916
  return _PlayRuntimeEnvironment.audio_offset
917
917
  case Mode.WATCH:
@@ -930,7 +930,7 @@ def input_offset() -> float:
930
930
  """
931
931
  if not ctx():
932
932
  return 0
933
- match ctx().global_state.mode:
933
+ match ctx().mode_state.mode:
934
934
  case Mode.PLAY:
935
935
  return _PlayRuntimeEnvironment.input_offset
936
936
  case Mode.WATCH:
@@ -947,7 +947,7 @@ def is_multiplayer() -> bool:
947
947
  """
948
948
  if not ctx():
949
949
  return False
950
- match ctx().global_state.mode:
950
+ match ctx().mode_state.mode:
951
951
  case Mode.PLAY:
952
952
  return _PlayRuntimeEnvironment.is_multiplayer
953
953
  case _:
@@ -962,7 +962,7 @@ def is_replay() -> bool:
962
962
  """
963
963
  if not ctx():
964
964
  return False
965
- match ctx().global_state.mode:
965
+ match ctx().mode_state.mode:
966
966
  case Mode.WATCH:
967
967
  return _WatchRuntimeEnvironment.is_replay
968
968
  case _:
@@ -977,7 +977,7 @@ def time() -> float:
977
977
  """
978
978
  if not ctx():
979
979
  return 0
980
- match ctx().global_state.mode:
980
+ match ctx().mode_state.mode:
981
981
  case Mode.PLAY:
982
982
  return _PlayRuntimeUpdate.time
983
983
  case Mode.WATCH:
@@ -996,7 +996,7 @@ def offset_adjusted_time() -> float:
996
996
  """
997
997
  if not ctx():
998
998
  return 0
999
- match ctx().global_state.mode:
999
+ match ctx().mode_state.mode:
1000
1000
  case Mode.PLAY:
1001
1001
  return _PlayRuntimeUpdate.time - _PlayRuntimeEnvironment.input_offset
1002
1002
  case Mode.WATCH:
@@ -1015,7 +1015,7 @@ def delta_time() -> float:
1015
1015
  """
1016
1016
  if not ctx():
1017
1017
  return 0
1018
- match ctx().global_state.mode:
1018
+ match ctx().mode_state.mode:
1019
1019
  case Mode.PLAY:
1020
1020
  return _PlayRuntimeUpdate.delta_time
1021
1021
  case Mode.WATCH:
@@ -1034,7 +1034,7 @@ def scaled_time() -> float:
1034
1034
  """
1035
1035
  if not ctx():
1036
1036
  return 0
1037
- match ctx().global_state.mode:
1037
+ match ctx().mode_state.mode:
1038
1038
  case Mode.PLAY:
1039
1039
  return _PlayRuntimeUpdate.scaled_time
1040
1040
  case Mode.WATCH:
@@ -1059,7 +1059,7 @@ def touches() -> ArrayLike[Touch]:
1059
1059
  """Get the current touches of the game."""
1060
1060
  if not ctx():
1061
1061
  return Array[Touch, 0]() # type: ignore
1062
- match ctx().global_state.mode:
1062
+ match ctx().mode_state.mode:
1063
1063
  case Mode.PLAY:
1064
1064
  return ArrayPointer[Touch]._raw(
1065
1065
  size=Num._accept_(_PlayRuntimeUpdate.touch_count),
@@ -1078,7 +1078,7 @@ def is_skip() -> bool:
1078
1078
  """
1079
1079
  if not ctx():
1080
1080
  return False
1081
- match ctx().global_state.mode:
1081
+ match ctx().mode_state.mode:
1082
1082
  case Mode.WATCH:
1083
1083
  return _WatchRuntimeUpdate.is_skip
1084
1084
  case _:
@@ -1093,7 +1093,7 @@ def navigation_direction() -> int:
1093
1093
  """
1094
1094
  if not ctx():
1095
1095
  return 0
1096
- match ctx().global_state.mode:
1096
+ match ctx().mode_state.mode:
1097
1097
  case Mode.TUTORIAL:
1098
1098
  return _TutorialRuntimeUpdate.navigation_direction
1099
1099
  case _:
sonolus/script/sprite.py CHANGED
@@ -4,11 +4,12 @@ from enum import StrEnum
4
4
  from typing import Annotated, Any, NewType, dataclass_transform, get_origin
5
5
 
6
6
  from sonolus.backend.ops import Op
7
- from sonolus.script.array_like import ArrayLike
7
+ from sonolus.script.array_like import ArrayLike, check_positive_index
8
8
  from sonolus.script.debug import static_error
9
9
  from sonolus.script.internal.impl import perf_meta_fn
10
10
  from sonolus.script.internal.introspection import get_field_specifiers
11
11
  from sonolus.script.internal.native import native_function
12
+ from sonolus.script.num import Num
12
13
  from sonolus.script.quad import QuadLike, flatten_quad
13
14
  from sonolus.script.record import Record
14
15
  from sonolus.script.vec import Vec2
@@ -138,7 +139,10 @@ class SpriteGroup(Record, ArrayLike[Sprite]):
138
139
  return self.size
139
140
 
140
141
  def __getitem__(self, index: int) -> Sprite:
141
- assert 0 <= index < self.size
142
+ check_positive_index(index, self.size)
143
+ return Sprite(self.start_id + index)
144
+
145
+ def get_unchecked(self, index: Num) -> Sprite:
142
146
  return Sprite(self.start_id + index)
143
147
 
144
148
  def __setitem__(self, index: int, value: Sprite) -> None:
sonolus/script/stream.py CHANGED
@@ -118,19 +118,19 @@ def streams[T](cls: type[T]) -> T:
118
118
 
119
119
  @meta_fn
120
120
  def _check_can_read_stream() -> None:
121
- if not ctx() or ctx().global_state.mode != Mode.WATCH:
121
+ if not ctx() or ctx().mode_state.mode != Mode.WATCH:
122
122
  raise RuntimeError("Stream read operations are only allowed in watch mode.")
123
123
 
124
124
 
125
125
  @meta_fn
126
126
  def _check_can_write_stream() -> None:
127
- if not ctx() or ctx().global_state.mode != Mode.PLAY:
127
+ if not ctx() or ctx().mode_state.mode != Mode.PLAY:
128
128
  raise RuntimeError("Stream write operations are only allowed in play mode.")
129
129
 
130
130
 
131
131
  @meta_fn
132
132
  def _check_can_read_or_write_stream() -> None:
133
- if not ctx() or ctx().global_state.mode not in {Mode.PLAY, Mode.WATCH}:
133
+ if not ctx() or ctx().mode_state.mode not in {Mode.PLAY, Mode.WATCH}:
134
134
  raise RuntimeError("Stream operations are only allowed in play and watch modes.")
135
135
 
136
136
 
@@ -325,6 +325,7 @@ class Transform2d(Record):
325
325
  Returns:
326
326
  A new normalized transform.
327
327
  """
328
+ assert self.a22 != 0, "Cannot normalize transform with a22 == 0"
328
329
  return Transform2d(
329
330
  self.a00 / self.a22,
330
331
  self.a01 / self.a22,
sonolus/script/vec.py CHANGED
@@ -158,6 +158,7 @@ class Vec2(Record):
158
158
  A new vector with magnitude 1.
159
159
  """
160
160
  magnitude = (self.x**2 + self.y**2) ** 0.5
161
+ assert magnitude != 0, "Cannot normalize a zero vector"
161
162
  return Vec2._quick_construct(x=self.x / magnitude, y=self.y / magnitude)
162
163
 
163
164
  @perf_meta_fn
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.9.3
3
+ Version: 0.10.1
4
4
  Summary: Sonolus engine development in Python
5
5
  Project-URL: Documentation, https://sonolus.py.qwewqa.xyz/
6
6
  Project-URL: Repository, https://github.com/qwewqa/sonolus.py