Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

- Python API redesign with direct handles and vector math:
- `Vec2` class: native 2D math type supporting vector operations (`+`, `-`, `*`, `/`), in-place mutation (`+=`, `-=`, `*=`, `/=`), indexing (`[0]`, `[1]`), tuple sequence unpacking (`x, y = self.position`), and `.length()` / `.normalized()`.
- Direct engine transform mutations: mutating `self.position.x` or assigning `self.position = (x, y)` directly updates `ScriptHost`, deleting the redundant end-of-frame Python `getattr` and string-allocation sync pass.
- Attribute-based input API: `pite.held.<key>`, `pite.pressed.<key>`, and `pite.released.<key>` (e.g. `pite.held.right`, `pite.pressed.space`), with `TypeError` raised on string calls like `pite.held("Space")`.
- Declarative node descriptors: `pite.child("Name")` and `pite.node("../Path")` descriptors with automatic first-access proxy caching.
- Node proxy caching: repeated `get_node()` lookups are cached in `ScriptHost` to eliminate tree walks.
- Hybrid signal callbacks: `signal.connect()` accepts arbitrary Python callables (lambdas, standalone functions, bound methods) alongside engine-dispatched signals.
- Editor pause-on-error: script exceptions during gameplay automatically pause the editor, log formatted Python tracebacks, and open the failing script.

- Download fallback for export: `pite export` automatically downloads the target platform's `pite-player` release archive when absent beside the engine binary, caching it under `~/.cache/pite/toolchains/players/` (or `PITE_PLAYER_DIR`).
- Standalone and all-platform player release assets: release workflow publishes standalone player bundles for each platform (`pite-player-vx.y.z-linux.tar.gz`, `windows.zip`, `macos.tar.gz`) alongside all-platforms player archives (`pite-player-vx.y.z-allplatforms.tar.gz` and `.zip`).
- Expanded audio formats and containers: pure-Rust decoding via `symphonia` brings support for Ogg Vorbis (`.ogg`), MP3 (`.mp3`), FLAC (`.flac`), and extended WAV (8/16/24/32-bit int and 32-bit float) to `pite.play` and `pite-audio`. `pite-assets` adds `OggImporter`, `Mp3Importer`, and `FlacImporter` for project scanning and manifest tracking, and `pite check` validates decodability across all supported audio formats.
Expand Down
50 changes: 27 additions & 23 deletions docs/scripting-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ class Player(pite.Node2D):
self.position = (100.0, 200.0)

def _process(self, delta: float):
x, y = self.position
self.position = (x + self.speed * delta, y)
if pite.held.right:
self.position.x += self.speed * delta
```

## Rules

- One file exports one class; attach with `path` + `class` in the scene file. `snake_case` everywhere.
- `_ready()` runs once when the node attaches; `_process(delta)` runs every frame (`delta` in seconds, capped at 0.1). **Missing methods are no-ops, never errors.**
- `position` is a plain `(x, y)` float tuple, synced both ways with the scene tree each frame. `Sprite2D` adds `texture: str`, `Timer` adds `wait_time: float` and an engine-owned `timeout` signal: every elapsed `wait_time` seconds the engine fires `timeout` and restarts the clock (looping). Non-positive `wait_time` never fires. Connect during `_ready` like `Button.pressed` — no declaration needed, `Label` adds `text: str` (same two-way sync — set it and the screen updates). `Button` extends `Node2D` with `text` plus a `size` prop (`[w, h]`, default `[120, 40]`).
- The `get_node` proxy also exposes `text`, so any node can drive a label: `self.get_node("../HpLabel").text = f"HP: {self.hp}"`.
- `position` is a mutable `Vec2` proxy backed directly by the engine's node transform. You can mutate `.x` and `.y` in-place (`self.position.x += 10`), reassign with a tuple or vector (`self.position = (100, 200)`), or unpack as a tuple (`x, y = self.position`). `Sprite2D` adds `texture: str`, `Timer` adds `wait_time: float` and an engine-owned `timeout` signal: every elapsed `wait_time` seconds the engine fires `timeout` and restarts the clock (looping). Non-positive `wait_time` never fires. Connect during `_ready` like `Button.pressed` — no declaration needed, `Label` adds `text: str` (same direct update — set it and the screen updates). `Button` extends `Node2D` with `text` plus a `size` prop (`[w, h]`, default `[120, 40]`).
- Declarative node descriptors: declare child or node references as class attributes:
```python
sprite = pite.child("Sprite2D")
target = pite.node("../Target")
```
Descriptors resolve once on first access and cache the proxy. A missing node raises `KeyError`.
- The `get_node` proxy also exposes `text`, so any node can drive a label: `self.get_node("../HpLabel").text = f"HP: {self.hp}"`. Repeated `get_node` calls are automatically cached.
- Talk across nodes with direct calls through `get_node`, which returns a proxy to the live node:

```python
Expand All @@ -41,14 +47,13 @@ class Player(pite.Node2D):
hit = pite.signal(int)

def _process(self, delta):
if pite.pressed("Space"):
if pite.pressed.space:
self.hit.emit(1)

```

- `pite.signal(...)` takes payload types, bare or as strings (`pite.signal(int, "str")`). Both `hit = pite.signal(int)` and the annotated `hit: pite.signal(int)` form declare the same signal.
- Emitting the wrong arity or type fails loudly; emitting an undeclared signal fails loudly. There are no untyped string signals.
- `connect(handler)` takes a bound method of a live node; when that node drops, its connections go with it — emitting afterwards simply skips it, no crash.
- `connect(handler)` connects any Python callable (bound methods, lambdas, free functions).
- `Button` clicks arrive as a zero-payload `pressed` signal fired by the engine (press-inside plus release-inside). `pressed` belongs to the `Button` type itself — just connect it, no declaration needed (re-declaring it is harmless):

```python
Expand All @@ -63,14 +68,15 @@ class HitButton(pite.Button):
## Input

```python
if pite.held("ArrowRight"):
x += self.speed * delta
if pite.pressed("Space"):
x += 10.0
if pite.held.right:
self.position.x += self.speed * delta
if pite.pressed.space:
self.position.x += 10.0
```

- `held(key)` is true while down (continuous movement); `pressed(key)` fires on the single frame it goes down (single steps); `released(key)` on the frame it comes up. `mouse()` returns the cursor `(x, y)` in pixels.
- Key names: `A`–`Z`, `0`–`9`, `ArrowLeft/Right/Up/Down`, `Space`, `Enter`, `Escape`, `Tab`, `Backspace`, `Shift`, `Control`, `Alt`, plus `MouseLeft/MouseRight/MouseMiddle`. Input is pumped on the main thread and visible inside `_process`.
- `pite.held.<key>` is true while down (continuous movement); `pite.pressed.<key>` fires on the single frame it goes down (single steps); `pite.released.<key>` on the frame it comes up. `mouse()` returns a `Vec2` with `.x` and `.y` (and tuple unpacking `x, y = pite.mouse()`).
- Key attributes: `space`, `enter`, `escape`, `tab`, `backspace`, `shift`, `ctrl`, `alt`, `left`, `right`, `up`, `down`, `mouse_left`, `mouse_right`, `mouse_middle`, `a`–`z`, `num0`–`num9`.
- String-based input calls (e.g., `pite.held("Space")`) are deprecated and raise `TypeError`. Input is pumped on the main thread and visible inside `_process` and `_physics_process`.

## Audio

Expand Down Expand Up @@ -104,21 +110,19 @@ class Player(pite.CharacterBody2D):
speed: float = 200.0

def _physics_process(self, delta: float):
vx, vy = self.velocity
if pite.held("ArrowRight"):
vx = self.speed
elif pite.held("ArrowLeft"):
vx = -self.speed
if pite.held.right:
self.velocity.x = self.speed
elif pite.held.left:
self.velocity.x = -self.speed
else:
vx = 0.0
self.velocity.x = 0.0

# Gravity
vy += 980.0 * delta
self.velocity.y += 980.0 * delta

if pite.pressed("Space") and self.is_on_floor():
vy = -400.0
if pite.pressed.space and self.is_on_floor():
self.velocity.y = -400.0

self.velocity = (vx, vy)
self.move_and_slide()
```

Expand Down
22 changes: 10 additions & 12 deletions examples/minimal-2d/scripts/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ def _ready(self):
self.position = (160.0, 240.0)

def _process(self, delta: float):
x, y = self.position
if pite.held("ArrowRight"):
x += self.speed * delta
if pite.held("ArrowLeft"):
x -= self.speed * delta
if pite.held("ArrowDown"):
y += self.speed * delta
if pite.held("ArrowUp"):
y -= self.speed * delta
if pite.pressed("Space"):
x += 10.0
if pite.held.right:
self.position.x += self.speed * delta
if pite.held.left:
self.position.x -= self.speed * delta
if pite.held.down:
self.position.y += self.speed * delta
if pite.held.up:
self.position.y -= self.speed * delta
if pite.pressed.space:
self.position.x += 10.0
self.hit.emit(1)
self.position = (x, y)
18 changes: 17 additions & 1 deletion pite-editor/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ pub struct EditorApp {

impl EditorApp {
pub fn new(scene: &Path) -> Result<Self> {
let session = GameSession::open(scene, false)?;
let mut session = GameSession::open(scene, false)?;
session.set_pause_on_error(true);
let project_dir = pite_project::find_project_root(scene);
let view_size = project_dir
.as_deref()
Expand Down Expand Up @@ -170,6 +171,21 @@ impl EditorApp {
}

pub(super) fn drain_script_errors(&mut self) {
if let Some(err) = self.session.take_last_error() {
self.playing = false;
let log_msg = if !err.traceback.is_empty() {
format!("{}\n{}", err.message, err.traceback)
} else {
err.message.clone()
};
self.log(log_msg);
if let Some(file) = &err.file {
let p = Path::new(file);
if p.exists() {
self.open_asset(p);
}
}
}
for err in self.session.errors() {
if self.seen_errors.insert(err.clone()) {
self.log(err);
Expand Down
3 changes: 2 additions & 1 deletion pite-editor/src/app/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ impl EditorApp {

pub(super) fn open_scene_file(&mut self, file: &Path) {
match GameSession::open(file, false) {
Ok(session) => {
Ok(mut session) => {
session.set_pause_on_error(true);
self.session = session;
self.scene_path = file.to_path_buf();
self.project_dir = pite_project::find_project_root(file);
Expand Down
3 changes: 2 additions & 1 deletion pite-editor/src/app/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ impl EditorApp {
pub(super) fn stop(&mut self) {
pite_script::input_clear();
match GameSession::open(&self.scene_path, false) {
Ok(session) => {
Ok(mut session) => {
session.set_pause_on_error(true);
self.session = session;
self.playing = false;
self.viewport_dirty = true;
Expand Down
13 changes: 10 additions & 3 deletions pite-editor/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1505,8 +1505,15 @@ mod tests {
}

// Completion inside `self.` — proves the open + completion round-trip.
// `x, y = self.position` is line 11 (0-based); the dot sits at column 20.
let found = smoke_await_completion(&mut client, &uri, 11, 20);
let (dot_line, dot_col) = text
.lines()
.enumerate()
.find_map(|(l, line)| {
let col = line.find("self.")? + 5;
Some((l as u32, col as u32))
})
.expect("smoke script contains `self.`");
let found = smoke_await_completion(&mut client, &uri, dot_line, dot_col);
assert!(!found.is_empty(), "smoke: completion came back empty");
assert!(
found.iter().any(|i| i.label == "position"),
Expand All @@ -1518,7 +1525,7 @@ mod tests {
// server buffer only; the dogfood file on disk is never touched.
let broken = format!("{text}\nundefined_smoke_name_xyz\n");
client.did_change(&uri, &broken).expect("didChange sends");
let after = smoke_await_completion(&mut client, &uri, 11, 20);
let after = smoke_await_completion(&mut client, &uri, dot_line, dot_col);
assert!(
after.iter().any(|i| i.label == "position"),
"smoke: completion broke after didChange, got {:?}",
Expand Down
22 changes: 10 additions & 12 deletions pite-project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,18 +278,16 @@ class Player(pite.Node2D):
self.position = (100.0, 200.0)

def _process(self, delta: float):
x, y = self.position
if pite.held("ArrowRight"):
x += self.speed * delta
if pite.held("ArrowLeft"):
x -= self.speed * delta
if pite.held("ArrowDown"):
y += self.speed * delta
if pite.held("ArrowUp"):
y -= self.speed * delta
if pite.pressed("Space"):
x += 10.0
self.position = (x, y)
if pite.held.right:
self.position.x += self.speed * delta
if pite.held.left:
self.position.x -= self.speed * delta
if pite.held.down:
self.position.y += self.speed * delta
if pite.held.up:
self.position.y -= self.speed * delta
if pite.pressed.space:
self.position.x += 10.0
"#;

/// Valid 1x1 RGBA PNG backing the `res://assets/player.png` reference in
Expand Down
Loading
Loading