sonolus.py 0.1.9__py3-none-any.whl → 0.2.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.
- sonolus/backend/optimize/constant_evaluation.py +2 -2
- sonolus/backend/optimize/optimize.py +12 -4
- sonolus/backend/optimize/passes.py +2 -1
- sonolus/backend/place.py +95 -14
- sonolus/backend/visitor.py +51 -3
- sonolus/build/cli.py +60 -9
- sonolus/build/collection.py +68 -26
- sonolus/build/compile.py +85 -27
- sonolus/build/engine.py +166 -40
- sonolus/build/node.py +8 -1
- sonolus/build/project.py +30 -11
- sonolus/script/archetype.py +154 -32
- sonolus/script/array.py +14 -3
- sonolus/script/array_like.py +6 -2
- sonolus/script/containers.py +166 -0
- sonolus/script/debug.py +22 -4
- sonolus/script/effect.py +2 -2
- sonolus/script/engine.py +125 -17
- sonolus/script/internal/builtin_impls.py +21 -2
- sonolus/script/internal/constant.py +8 -4
- sonolus/script/internal/context.py +30 -25
- sonolus/script/internal/math_impls.py +2 -1
- sonolus/script/internal/transient.py +7 -3
- sonolus/script/internal/value.py +33 -11
- sonolus/script/interval.py +8 -3
- sonolus/script/iterator.py +17 -0
- sonolus/script/level.py +113 -10
- sonolus/script/metadata.py +32 -0
- sonolus/script/num.py +35 -15
- sonolus/script/options.py +23 -6
- sonolus/script/pointer.py +11 -1
- sonolus/script/project.py +41 -5
- sonolus/script/quad.py +55 -1
- sonolus/script/record.py +10 -5
- sonolus/script/runtime.py +78 -16
- sonolus/script/sprite.py +18 -1
- sonolus/script/text.py +9 -0
- sonolus/script/ui.py +20 -7
- sonolus/script/values.py +8 -5
- sonolus/script/vec.py +28 -0
- sonolus_py-0.2.1.dist-info/METADATA +10 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/RECORD +46 -45
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/WHEEL +1 -1
- sonolus_py-0.1.9.dist-info/METADATA +0 -9
- /sonolus/script/{print.py → printing.py} +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/entry_points.txt +0 -0
- {sonolus_py-0.1.9.dist-info → sonolus_py-0.2.1.dist-info}/licenses/LICENSE +0 -0
sonolus/script/level.py
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sonolus.build.collection import Asset, load_asset
|
|
4
10
|
from sonolus.script.archetype import PlayArchetype, StandardArchetypeName, StandardImport
|
|
11
|
+
from sonolus.script.metadata import AnyText, Tag, as_localization_text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExportedLevel:
|
|
15
|
+
"""An exported Sonolus level."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
item: dict,
|
|
21
|
+
cover: bytes,
|
|
22
|
+
bgm: bytes,
|
|
23
|
+
preview: bytes | None,
|
|
24
|
+
data: bytes,
|
|
25
|
+
):
|
|
26
|
+
self.item = item
|
|
27
|
+
self.cover = cover
|
|
28
|
+
self.bgm = bgm
|
|
29
|
+
self.preview = preview
|
|
30
|
+
self.data = data
|
|
31
|
+
|
|
32
|
+
def write_to_dir(self, path: PathLike):
|
|
33
|
+
"""Write the exported level to a directory."""
|
|
34
|
+
path = Path(path)
|
|
35
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
(path / "item.json").write_text(json.dumps(self.item, ensure_ascii=False), encoding="utf-8")
|
|
37
|
+
(path / "cover").write_bytes(self.cover)
|
|
38
|
+
(path / "bgm").write_bytes(self.bgm)
|
|
39
|
+
if self.preview is not None:
|
|
40
|
+
(path / "preview").write_bytes(self.preview)
|
|
41
|
+
(path / "data").write_bytes(self.data)
|
|
5
42
|
|
|
6
43
|
|
|
7
44
|
class Level:
|
|
@@ -16,6 +53,13 @@ class Level:
|
|
|
16
53
|
cover: The cover of the level.
|
|
17
54
|
bgm: The background music of the level.
|
|
18
55
|
data: The data of the level.
|
|
56
|
+
use_skin: The skin to use, overriding the engine skin.
|
|
57
|
+
use_background: The background to use, overriding the engine background.
|
|
58
|
+
use_effect: The effect to use, overriding the engine effect.
|
|
59
|
+
use_particle: The particle to use, overriding the engine particle.
|
|
60
|
+
tags: The tags of the level.
|
|
61
|
+
description: The description of the level.
|
|
62
|
+
meta: Additional metadata of the level.
|
|
19
63
|
"""
|
|
20
64
|
|
|
21
65
|
version = 1
|
|
@@ -24,35 +68,94 @@ class Level:
|
|
|
24
68
|
self,
|
|
25
69
|
*,
|
|
26
70
|
name: str,
|
|
27
|
-
title:
|
|
71
|
+
title: AnyText | None = None,
|
|
28
72
|
rating: int = 0,
|
|
29
|
-
artists:
|
|
30
|
-
author:
|
|
73
|
+
artists: AnyText = "Unknown",
|
|
74
|
+
author: AnyText = "Unknown",
|
|
31
75
|
cover: Asset | None = None,
|
|
32
76
|
bgm: Asset | None = None,
|
|
77
|
+
preview: Asset | None = None,
|
|
33
78
|
data: LevelData,
|
|
79
|
+
use_skin: str | None = None,
|
|
80
|
+
use_background: str | None = None,
|
|
81
|
+
use_effect: str | None = None,
|
|
82
|
+
use_particle: str | None = None,
|
|
83
|
+
tags: list[Tag] | None = None,
|
|
84
|
+
description: AnyText | None = None,
|
|
85
|
+
meta: Any = None,
|
|
34
86
|
) -> None:
|
|
35
87
|
self.name = name
|
|
36
|
-
self.title = title or name
|
|
88
|
+
self.title = as_localization_text(title or name)
|
|
37
89
|
self.rating = rating
|
|
38
|
-
self.artists = artists
|
|
39
|
-
self.author = author
|
|
90
|
+
self.artists = as_localization_text(artists)
|
|
91
|
+
self.author = as_localization_text(author)
|
|
40
92
|
self.cover = cover
|
|
41
93
|
self.bgm = bgm
|
|
94
|
+
self.preview = preview
|
|
42
95
|
self.data = data
|
|
96
|
+
self.use_skin = use_skin
|
|
97
|
+
self.use_background = use_background
|
|
98
|
+
self.use_effect = use_effect
|
|
99
|
+
self.use_particle = use_particle
|
|
100
|
+
self.tags = tags or []
|
|
101
|
+
self.description = as_localization_text(description) if description is not None else None
|
|
102
|
+
self.meta = meta
|
|
103
|
+
|
|
104
|
+
def export(self, engine_name: str) -> ExportedLevel:
|
|
105
|
+
"""Export the level in a sonolus-pack compatible format.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
engine_name: The name of the engine this level is for.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The exported level.
|
|
112
|
+
"""
|
|
113
|
+
from sonolus.build.level import package_level_data
|
|
114
|
+
from sonolus.build.project import BLANK_AUDIO, BLANK_PNG
|
|
115
|
+
|
|
116
|
+
item = {
|
|
117
|
+
"version": self.version,
|
|
118
|
+
"rating": self.rating,
|
|
119
|
+
"engine": engine_name,
|
|
120
|
+
"useSkin": {"useDefault": True} if self.use_skin is None else {"useDefault": False, "item": self.use_skin},
|
|
121
|
+
"useBackground": {"useDefault": True}
|
|
122
|
+
if self.use_background is None
|
|
123
|
+
else {"useDefault": False, "item": self.use_background},
|
|
124
|
+
"useEffect": {"useDefault": True}
|
|
125
|
+
if self.use_effect is None
|
|
126
|
+
else {"useDefault": False, "item": self.use_effect},
|
|
127
|
+
"useParticle": {"useDefault": True}
|
|
128
|
+
if self.use_particle is None
|
|
129
|
+
else {"useDefault": False, "item": self.use_particle},
|
|
130
|
+
"title": self.title,
|
|
131
|
+
"artists": self.artists,
|
|
132
|
+
"author": self.author,
|
|
133
|
+
"tags": [tag.as_dict() for tag in self.tags],
|
|
134
|
+
}
|
|
135
|
+
if self.description is not None:
|
|
136
|
+
item["description"] = self.description
|
|
137
|
+
if self.meta is not None:
|
|
138
|
+
item["meta"] = self.meta
|
|
139
|
+
return ExportedLevel(
|
|
140
|
+
item=item,
|
|
141
|
+
cover=load_asset(self.cover) if self.cover is not None else BLANK_PNG,
|
|
142
|
+
bgm=load_asset(self.bgm) if self.bgm is not None else BLANK_AUDIO,
|
|
143
|
+
preview=load_asset(self.preview) if self.preview is not None else None,
|
|
144
|
+
data=package_level_data(self.data),
|
|
145
|
+
)
|
|
43
146
|
|
|
44
147
|
|
|
45
148
|
type EntityListArg = list[PlayArchetype | EntityListArg]
|
|
46
149
|
|
|
47
150
|
|
|
48
|
-
def flatten_entities(entities: EntityListArg):
|
|
151
|
+
def flatten_entities(entities: EntityListArg) -> Iterator[PlayArchetype]:
|
|
49
152
|
"""Flatten a list of entities.
|
|
50
153
|
|
|
51
154
|
Args:
|
|
52
155
|
entities: The list of entities.
|
|
53
156
|
|
|
54
|
-
|
|
55
|
-
The flattened
|
|
157
|
+
Yields:
|
|
158
|
+
The flattened entities.
|
|
56
159
|
"""
|
|
57
160
|
if isinstance(entities, list):
|
|
58
161
|
for entity in entities:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
type LocalizationText = dict[str, str]
|
|
4
|
+
type AnyText = str | LocalizationText
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def as_localization_text(text: AnyText) -> LocalizationText:
|
|
8
|
+
if isinstance(text, str):
|
|
9
|
+
return {"en": text}
|
|
10
|
+
return text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Tag:
|
|
14
|
+
"""A tag for an engine or level.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
title: The title of the tag.
|
|
18
|
+
icon: The icon of the tag.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
title: LocalizationText
|
|
22
|
+
icon: str | None
|
|
23
|
+
|
|
24
|
+
def __init__(self, title: AnyText, icon: str | None = None) -> None:
|
|
25
|
+
self.title = as_localization_text(title)
|
|
26
|
+
self.icon = icon
|
|
27
|
+
|
|
28
|
+
def as_dict(self) -> dict[str, Any]:
|
|
29
|
+
result = {"title": self.title}
|
|
30
|
+
if self.icon is not None:
|
|
31
|
+
result["icon"] = self.icon
|
|
32
|
+
return result
|
sonolus/script/num.py
CHANGED
|
@@ -7,11 +7,11 @@ from typing import TYPE_CHECKING, Any, Self, TypeGuard, final, runtime_checkable
|
|
|
7
7
|
|
|
8
8
|
from sonolus.backend.ir import IRConst, IRGet, IRPureInstr, IRSet
|
|
9
9
|
from sonolus.backend.ops import Op
|
|
10
|
-
from sonolus.backend.place import BlockPlace
|
|
10
|
+
from sonolus.backend.place import BlockPlace
|
|
11
11
|
from sonolus.script.internal.context import ctx
|
|
12
12
|
from sonolus.script.internal.error import InternalError
|
|
13
13
|
from sonolus.script.internal.impl import meta_fn
|
|
14
|
-
from sonolus.script.internal.value import Value
|
|
14
|
+
from sonolus.script.internal.value import BackingValue, DataValue, Value
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class _NumMeta(type):
|
|
@@ -30,15 +30,17 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
30
30
|
# Since we don't support complex numbers, real is equal to the original number
|
|
31
31
|
__match_args__ = ("real",)
|
|
32
32
|
|
|
33
|
-
data:
|
|
33
|
+
data: DataValue
|
|
34
34
|
|
|
35
|
-
def __init__(self, data:
|
|
35
|
+
def __init__(self, data: DataValue):
|
|
36
36
|
if isinstance(data, complex):
|
|
37
37
|
raise TypeError("Cannot create a Num from a complex number")
|
|
38
38
|
if isinstance(data, int):
|
|
39
39
|
data = float(data)
|
|
40
40
|
if _is_num(data):
|
|
41
41
|
raise InternalError("Cannot create a Num from a Num")
|
|
42
|
+
if not isinstance(data, BlockPlace | BackingValue | float | int | bool):
|
|
43
|
+
raise TypeError(f"Cannot create a Num from {type(data)}")
|
|
42
44
|
self.data = data
|
|
43
45
|
|
|
44
46
|
def __str__(self) -> str:
|
|
@@ -78,7 +80,7 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
78
80
|
return cls(value)
|
|
79
81
|
|
|
80
82
|
def _is_py_(self) -> bool:
|
|
81
|
-
return
|
|
83
|
+
return isinstance(self.data, float | int | bool)
|
|
82
84
|
|
|
83
85
|
def _as_py_(self) -> Any:
|
|
84
86
|
if not self._is_py_():
|
|
@@ -88,11 +90,11 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
88
90
|
return self.data
|
|
89
91
|
|
|
90
92
|
@classmethod
|
|
91
|
-
def _from_list_(cls, values: Iterable[
|
|
93
|
+
def _from_list_(cls, values: Iterable[DataValue]) -> Self:
|
|
92
94
|
value = next(iter(values))
|
|
93
95
|
return Num(value)
|
|
94
96
|
|
|
95
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
97
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue]:
|
|
96
98
|
return [self.data]
|
|
97
99
|
|
|
98
100
|
@classmethod
|
|
@@ -111,10 +113,14 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
111
113
|
|
|
112
114
|
def _set_(self, value: Self):
|
|
113
115
|
if ctx():
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
match self.data:
|
|
117
|
+
case BackingValue():
|
|
118
|
+
ctx().add_statements(self.data.write(value))
|
|
119
|
+
case BlockPlace():
|
|
120
|
+
ctx().check_writable(self.data)
|
|
121
|
+
ctx().add_statements(IRSet(self.data, value.ir()))
|
|
122
|
+
case _:
|
|
123
|
+
raise ValueError("Cannot set a read-only value")
|
|
118
124
|
else:
|
|
119
125
|
self.data = value.data
|
|
120
126
|
|
|
@@ -134,13 +140,27 @@ class _Num(Value, metaclass=_NumMeta):
|
|
|
134
140
|
else:
|
|
135
141
|
return Num(-1)
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
@classmethod
|
|
144
|
+
def _zero_(cls) -> Self:
|
|
145
|
+
if ctx():
|
|
146
|
+
result_place = ctx().alloc(size=1)
|
|
147
|
+
ctx().add_statements(IRSet(result_place, IRConst(0)))
|
|
148
|
+
return cls(result_place)
|
|
140
149
|
else:
|
|
141
|
-
return
|
|
150
|
+
return cls(0)
|
|
151
|
+
|
|
152
|
+
def ir(self):
|
|
153
|
+
match self.data:
|
|
154
|
+
case BlockPlace():
|
|
155
|
+
return IRGet(self.data)
|
|
156
|
+
case BackingValue():
|
|
157
|
+
return self.data.read()
|
|
158
|
+
case _:
|
|
159
|
+
return IRConst(self.data)
|
|
142
160
|
|
|
143
161
|
def index(self) -> int | BlockPlace:
|
|
162
|
+
if isinstance(self.data, BlockPlace):
|
|
163
|
+
return self._get_().data
|
|
144
164
|
return self.data
|
|
145
165
|
|
|
146
166
|
def _bin_op(self, other: Self, const_fn: Callable[[Self, Self], Self | None], ir_op: Op) -> Self:
|
sonolus/script/options.py
CHANGED
|
@@ -15,6 +15,7 @@ from sonolus.script.num import Num
|
|
|
15
15
|
@dataclass
|
|
16
16
|
class _SliderOption:
|
|
17
17
|
name: str | None
|
|
18
|
+
description: str | None
|
|
18
19
|
standard: bool
|
|
19
20
|
advanced: bool
|
|
20
21
|
scope: str | None
|
|
@@ -35,6 +36,8 @@ class _SliderOption:
|
|
|
35
36
|
"max": self.max,
|
|
36
37
|
"step": self.step,
|
|
37
38
|
}
|
|
39
|
+
if self.description is not None:
|
|
40
|
+
result["description"] = self.description
|
|
38
41
|
if self.scope is not None:
|
|
39
42
|
result["scope"] = self.scope
|
|
40
43
|
if self.unit is not None:
|
|
@@ -45,6 +48,7 @@ class _SliderOption:
|
|
|
45
48
|
@dataclass
|
|
46
49
|
class _ToggleOption:
|
|
47
50
|
name: str | None
|
|
51
|
+
description: str | None
|
|
48
52
|
standard: bool
|
|
49
53
|
advanced: bool
|
|
50
54
|
scope: str | None
|
|
@@ -58,6 +62,8 @@ class _ToggleOption:
|
|
|
58
62
|
"advanced": self.advanced,
|
|
59
63
|
"def": int(self.default),
|
|
60
64
|
}
|
|
65
|
+
if self.description is not None:
|
|
66
|
+
result["description"] = self.description
|
|
61
67
|
if self.scope is not None:
|
|
62
68
|
result["scope"] = self.scope
|
|
63
69
|
return result
|
|
@@ -66,10 +72,11 @@ class _ToggleOption:
|
|
|
66
72
|
@dataclass
|
|
67
73
|
class _SelectOption:
|
|
68
74
|
name: str | None
|
|
75
|
+
description: str | None
|
|
69
76
|
standard: bool
|
|
70
77
|
advanced: bool
|
|
71
78
|
scope: str | None
|
|
72
|
-
default:
|
|
79
|
+
default: int
|
|
73
80
|
values: list[str]
|
|
74
81
|
|
|
75
82
|
def to_dict(self):
|
|
@@ -78,9 +85,11 @@ class _SelectOption:
|
|
|
78
85
|
"name": self.name,
|
|
79
86
|
"standard": self.standard,
|
|
80
87
|
"advanced": self.advanced,
|
|
81
|
-
"def": self.
|
|
88
|
+
"def": self.default,
|
|
82
89
|
"values": self.values,
|
|
83
90
|
}
|
|
91
|
+
if self.description is not None:
|
|
92
|
+
result["description"] = self.description
|
|
84
93
|
if self.scope is not None:
|
|
85
94
|
result["scope"] = self.scope
|
|
86
95
|
return result
|
|
@@ -89,6 +98,7 @@ class _SelectOption:
|
|
|
89
98
|
def slider_option(
|
|
90
99
|
*,
|
|
91
100
|
name: str | None = None,
|
|
101
|
+
description: str | None = None,
|
|
92
102
|
standard: bool = False,
|
|
93
103
|
advanced: bool = False,
|
|
94
104
|
default: float,
|
|
@@ -102,6 +112,7 @@ def slider_option(
|
|
|
102
112
|
|
|
103
113
|
Args:
|
|
104
114
|
name: The name of the option.
|
|
115
|
+
description: The description of the option.
|
|
105
116
|
standard: Whether the option is standard.
|
|
106
117
|
advanced: Whether the option is advanced.
|
|
107
118
|
default: The default value of the option.
|
|
@@ -111,12 +122,13 @@ def slider_option(
|
|
|
111
122
|
unit: The unit of the option.
|
|
112
123
|
scope: The scope of the option.
|
|
113
124
|
"""
|
|
114
|
-
return _SliderOption(name, standard, advanced, scope, default, min, max, step, unit)
|
|
125
|
+
return _SliderOption(name, description, standard, advanced, scope, default, min, max, step, unit)
|
|
115
126
|
|
|
116
127
|
|
|
117
128
|
def toggle_option(
|
|
118
129
|
*,
|
|
119
130
|
name: str | None = None,
|
|
131
|
+
description: str | None = None,
|
|
120
132
|
standard: bool = False,
|
|
121
133
|
advanced: bool = False,
|
|
122
134
|
default: bool,
|
|
@@ -126,20 +138,22 @@ def toggle_option(
|
|
|
126
138
|
|
|
127
139
|
Args:
|
|
128
140
|
name: The name of the option.
|
|
141
|
+
description: The description of the option.
|
|
129
142
|
standard: Whether the option is standard.
|
|
130
143
|
advanced: Whether the option is advanced.
|
|
131
144
|
default: The default value of the option.
|
|
132
145
|
scope: The scope of the option.
|
|
133
146
|
"""
|
|
134
|
-
return _ToggleOption(name, standard, advanced, scope, default)
|
|
147
|
+
return _ToggleOption(name, description, standard, advanced, scope, default)
|
|
135
148
|
|
|
136
149
|
|
|
137
150
|
def select_option(
|
|
138
151
|
*,
|
|
139
152
|
name: str | None = None,
|
|
153
|
+
description: str | None = None,
|
|
140
154
|
standard: bool = False,
|
|
141
155
|
advanced: bool = False,
|
|
142
|
-
default: str,
|
|
156
|
+
default: str | int,
|
|
143
157
|
values: list[str],
|
|
144
158
|
scope: str | None = None,
|
|
145
159
|
) -> Any:
|
|
@@ -147,13 +161,16 @@ def select_option(
|
|
|
147
161
|
|
|
148
162
|
Args:
|
|
149
163
|
name: The name of the option.
|
|
164
|
+
description: The description of the option.
|
|
150
165
|
standard: Whether the option is standard.
|
|
151
166
|
advanced: Whether the option is advanced.
|
|
152
167
|
default: The default value of the option.
|
|
153
168
|
values: The values of the option.
|
|
154
169
|
scope: The scope of the option.
|
|
155
170
|
"""
|
|
156
|
-
|
|
171
|
+
if isinstance(default, str):
|
|
172
|
+
default = values.index(default)
|
|
173
|
+
return _SelectOption(name, description, standard, advanced, scope, default, values)
|
|
157
174
|
|
|
158
175
|
|
|
159
176
|
type Options = NewType("Options", Any)
|
sonolus/script/pointer.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from sonolus.backend.place import BlockPlace
|
|
2
|
+
from sonolus.script.internal.context import ctx
|
|
2
3
|
from sonolus.script.internal.impl import meta_fn, validate_value
|
|
3
|
-
from sonolus.script.internal.value import Value
|
|
4
|
+
from sonolus.script.internal.value import BackingSource, Value
|
|
4
5
|
from sonolus.script.num import Num, _is_num
|
|
5
6
|
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ def _deref[T: Value](block: Num, offset: Num, type_: type[T]) -> T:
|
|
|
13
14
|
block = block._as_py_()
|
|
14
15
|
if not isinstance(block, int):
|
|
15
16
|
raise TypeError("block must be an integer")
|
|
17
|
+
block = ctx().blocks(block)
|
|
16
18
|
else:
|
|
17
19
|
if not _is_num(block):
|
|
18
20
|
raise TypeError("block must be a Num")
|
|
@@ -28,3 +30,11 @@ def _deref[T: Value](block: Num, offset: Num, type_: type[T]) -> T:
|
|
|
28
30
|
if not (isinstance(type_, type) and issubclass(type_, Value)):
|
|
29
31
|
raise TypeError("type_ must be a Value")
|
|
30
32
|
return type_._from_place_(BlockPlace(block, offset))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@meta_fn
|
|
36
|
+
def _backing_deref[T: Value](source: BackingSource, type_: type[T]) -> T:
|
|
37
|
+
type_ = validate_value(type_)._as_py_()
|
|
38
|
+
if not isinstance(type_, type) or not issubclass(type_, Value):
|
|
39
|
+
raise TypeError("type_ must be a Value")
|
|
40
|
+
return type_._from_backing_source_(source)
|
sonolus/script/project.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
3
5
|
from os import PathLike
|
|
4
6
|
from pathlib import Path
|
|
5
|
-
from typing import Self, TypedDict
|
|
7
|
+
from typing import ClassVar, Self, TypedDict
|
|
6
8
|
|
|
9
|
+
from sonolus.backend.optimize import optimize
|
|
10
|
+
from sonolus.backend.optimize.passes import CompilerPass
|
|
7
11
|
from sonolus.script.archetype import ArchetypeSchema
|
|
8
12
|
from sonolus.script.engine import Engine
|
|
9
13
|
from sonolus.script.level import Level
|
|
@@ -39,27 +43,30 @@ class Project:
|
|
|
39
43
|
"""
|
|
40
44
|
return Project(self.engine, levels, self.resources)
|
|
41
45
|
|
|
42
|
-
def dev(self, build_dir: PathLike, port: int = 8080):
|
|
46
|
+
def dev(self, build_dir: PathLike, port: int = 8080, config: BuildConfig | None = None):
|
|
43
47
|
"""Start a development server for the project.
|
|
44
48
|
|
|
45
49
|
Args:
|
|
46
50
|
build_dir: The path to the build directory.
|
|
47
51
|
port: The port of the development server.
|
|
52
|
+
config: The build configuration.
|
|
48
53
|
"""
|
|
49
54
|
from sonolus.build.cli import build_collection, run_server
|
|
50
55
|
|
|
51
|
-
build_collection(self, Path(build_dir))
|
|
56
|
+
build_collection(self, Path(build_dir), config)
|
|
52
57
|
run_server(Path(build_dir) / "site", port=port)
|
|
53
58
|
|
|
54
|
-
def build(self, build_dir: PathLike):
|
|
59
|
+
def build(self, build_dir: PathLike, config: BuildConfig | None = None):
|
|
55
60
|
"""Build the project.
|
|
56
61
|
|
|
57
62
|
Args:
|
|
58
63
|
build_dir: The path to the build directory.
|
|
64
|
+
config: The build configuration.
|
|
59
65
|
"""
|
|
60
66
|
from sonolus.build.cli import build_project
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
config = config or BuildConfig()
|
|
69
|
+
build_project(self, Path(build_dir), config)
|
|
63
70
|
|
|
64
71
|
def schema(self) -> ProjectSchema:
|
|
65
72
|
"""Generate the schema of the project.
|
|
@@ -74,3 +81,32 @@ class Project:
|
|
|
74
81
|
|
|
75
82
|
class ProjectSchema(TypedDict):
|
|
76
83
|
archetypes: list[ArchetypeSchema]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class BuildConfig:
|
|
88
|
+
"""A configuration for building an engine package."""
|
|
89
|
+
|
|
90
|
+
MINIMAL_PASSES: ClassVar[Sequence[CompilerPass]] = optimize.MINIMAL_PASSES
|
|
91
|
+
"""The minimal list of compiler passes."""
|
|
92
|
+
|
|
93
|
+
FAST_PASSES: ClassVar[Sequence[CompilerPass]] = optimize.FAST_PASSES
|
|
94
|
+
"""The list of compiler passes for faster builds."""
|
|
95
|
+
|
|
96
|
+
STANDARD_PASSES: ClassVar[Sequence[CompilerPass]] = optimize.STANDARD_PASSES
|
|
97
|
+
"""The standard list of compiler passes."""
|
|
98
|
+
|
|
99
|
+
passes: Sequence[CompilerPass] = optimize.STANDARD_PASSES
|
|
100
|
+
"""The list of compiler passes to use."""
|
|
101
|
+
|
|
102
|
+
build_play: bool = True
|
|
103
|
+
"""Whether to build the play package."""
|
|
104
|
+
|
|
105
|
+
build_watch: bool = True
|
|
106
|
+
"""Whether to build the watch package."""
|
|
107
|
+
|
|
108
|
+
build_preview: bool = True
|
|
109
|
+
"""Whether to build the preview package."""
|
|
110
|
+
|
|
111
|
+
build_tutorial: bool = True
|
|
112
|
+
"""Whether to build the tutorial package."""
|
sonolus/script/quad.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Protocol, Self
|
|
4
4
|
|
|
5
5
|
from sonolus.script.record import Record
|
|
6
|
+
from sonolus.script.values import zeros
|
|
6
7
|
from sonolus.script.vec import Vec2, pnpoly
|
|
7
8
|
|
|
8
9
|
|
|
@@ -27,6 +28,16 @@ class Quad(Record):
|
|
|
27
28
|
br: Vec2
|
|
28
29
|
"""The bottom-right corner of the quad."""
|
|
29
30
|
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_quad(cls, value: QuadLike, /) -> Quad:
|
|
33
|
+
"""Create a quad from a quad-like value."""
|
|
34
|
+
return cls(
|
|
35
|
+
bl=value.bl,
|
|
36
|
+
tl=value.tl,
|
|
37
|
+
tr=value.tr,
|
|
38
|
+
br=value.br,
|
|
39
|
+
)
|
|
40
|
+
|
|
30
41
|
@property
|
|
31
42
|
def center(self) -> Vec2:
|
|
32
43
|
"""The center of the quad."""
|
|
@@ -95,6 +106,44 @@ class Quad(Record):
|
|
|
95
106
|
"""Rotate the quad by the given angle about its center and return a new quad."""
|
|
96
107
|
return self.rotate_about(angle, self.center)
|
|
97
108
|
|
|
109
|
+
def permute(self, count: int = 1, /) -> Self:
|
|
110
|
+
"""Perform a cyclic permutation of the quad's vertices and return a new quad.
|
|
111
|
+
|
|
112
|
+
On a square, this operation is equivalent to rotating the square counterclockwise 90 degrees `count` times.
|
|
113
|
+
|
|
114
|
+
Negative values of `count` are allowed and will rotate the quad clockwise.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
count: The number of vertices to shift. Defaults to 1.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The permuted quad.
|
|
121
|
+
"""
|
|
122
|
+
count = int(count % 4)
|
|
123
|
+
result = zeros(Quad)
|
|
124
|
+
match count:
|
|
125
|
+
case 0:
|
|
126
|
+
result.bl @= self.bl
|
|
127
|
+
result.tl @= self.tl
|
|
128
|
+
result.tr @= self.tr
|
|
129
|
+
result.br @= self.br
|
|
130
|
+
case 1:
|
|
131
|
+
result.bl @= self.br
|
|
132
|
+
result.tl @= self.bl
|
|
133
|
+
result.tr @= self.tl
|
|
134
|
+
result.br @= self.tr
|
|
135
|
+
case 2:
|
|
136
|
+
result.bl @= self.tr
|
|
137
|
+
result.tl @= self.br
|
|
138
|
+
result.tr @= self.bl
|
|
139
|
+
result.br @= self.tl
|
|
140
|
+
case 3:
|
|
141
|
+
result.bl @= self.tl
|
|
142
|
+
result.tl @= self.tr
|
|
143
|
+
result.tr @= self.br
|
|
144
|
+
result.br @= self.bl
|
|
145
|
+
return result
|
|
146
|
+
|
|
98
147
|
def contains_point(self, point: Vec2, /) -> bool:
|
|
99
148
|
"""Check if the quad contains the given point.
|
|
100
149
|
|
|
@@ -248,7 +297,7 @@ class Rect(Record):
|
|
|
248
297
|
return self.l <= point.x <= self.r and self.b <= point.y <= self.t
|
|
249
298
|
|
|
250
299
|
|
|
251
|
-
class
|
|
300
|
+
class _QuadLike(Protocol):
|
|
252
301
|
"""A protocol for types that can be used as quads."""
|
|
253
302
|
|
|
254
303
|
@property
|
|
@@ -268,6 +317,11 @@ class QuadLike(Protocol):
|
|
|
268
317
|
"""The bottom-right corner of the quad."""
|
|
269
318
|
|
|
270
319
|
|
|
320
|
+
# PyCharm doesn't recognize attributes as satisfying the protocol.
|
|
321
|
+
type QuadLike = _QuadLike | Quad
|
|
322
|
+
"""A type that can be used as a quad."""
|
|
323
|
+
|
|
324
|
+
|
|
271
325
|
def flatten_quad(quad: QuadLike) -> tuple[float, float, float, float, float, float, float, float]:
|
|
272
326
|
bl = quad.bl
|
|
273
327
|
tl = quad.tl
|
sonolus/script/record.py
CHANGED
|
@@ -16,7 +16,7 @@ from sonolus.script.internal.generic import (
|
|
|
16
16
|
validate_type_spec,
|
|
17
17
|
)
|
|
18
18
|
from sonolus.script.internal.impl import meta_fn
|
|
19
|
-
from sonolus.script.internal.value import Value
|
|
19
|
+
from sonolus.script.internal.value import DataValue, Value
|
|
20
20
|
from sonolus.script.num import Num
|
|
21
21
|
|
|
22
22
|
|
|
@@ -182,11 +182,11 @@ class Record(GenericValue):
|
|
|
182
182
|
return self
|
|
183
183
|
|
|
184
184
|
@classmethod
|
|
185
|
-
def _from_list_(cls, values: Iterable[
|
|
185
|
+
def _from_list_(cls, values: Iterable[DataValue]) -> Self:
|
|
186
186
|
iterator = iter(values)
|
|
187
187
|
return cls(**{field.name: field.type._from_list_(iterator) for field in cls._fields})
|
|
188
188
|
|
|
189
|
-
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[
|
|
189
|
+
def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
|
|
190
190
|
result = []
|
|
191
191
|
for field in self._fields:
|
|
192
192
|
result.extend(self._value[field.name]._to_list_(level_refs))
|
|
@@ -206,8 +206,7 @@ class Record(GenericValue):
|
|
|
206
206
|
raise TypeError("Record does not support set_")
|
|
207
207
|
|
|
208
208
|
def _copy_from_(self, value: Self):
|
|
209
|
-
|
|
210
|
-
raise TypeError("Cannot copy from different type")
|
|
209
|
+
value = self._accept_(value)
|
|
211
210
|
for field in self._fields:
|
|
212
211
|
field.__set__(self, field.__get__(value))
|
|
213
212
|
|
|
@@ -221,6 +220,12 @@ class Record(GenericValue):
|
|
|
221
220
|
result._value = {field.name: field.type._alloc_() for field in cls._fields}
|
|
222
221
|
return result
|
|
223
222
|
|
|
223
|
+
@classmethod
|
|
224
|
+
def _zero_(cls) -> Self:
|
|
225
|
+
result = object.__new__(cls)
|
|
226
|
+
result._value = {field.name: field.type._zero_() for field in cls._fields}
|
|
227
|
+
return result
|
|
228
|
+
|
|
224
229
|
def __str__(self):
|
|
225
230
|
return (
|
|
226
231
|
f"{self.__class__.__name__}({", ".join(f"{field.name}={field.__get__(self)}" for field in self._fields)})"
|