diff --git a/.cursor/rules/gdscript-style.md b/.cursor/rules/gdscript-style.md index f816b65..205d24c 100644 --- a/.cursor/rules/gdscript-style.md +++ b/.cursor/rules/gdscript-style.md @@ -8,6 +8,8 @@ alwaysApply: true Follow the [Godot GDScript style guide](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_styleguide.html). Prefer clarity and consistency with existing scripts in the repo. +CI also enforces **`gdlint`** and **`gdformat`** (see `gdlintrc` and `.github/workflows/gdscript.yml`) on **`client/scripts/`** and **`client/test/`**. When this guide and personal preference differ, follow the **linted** version below so pushed files pass CI. + ## Naming - **Functions, variables, signals:** `snake_case`. @@ -23,18 +25,43 @@ Follow the [Godot GDScript style guide](https://docs.godotengine.org/en/stable/t ## Formatting -- **One statement per line**; break long lines for readability (Godot line length is flexible—aim for ~100 characters unless a longer string is clearer). +- **One statement per line**; CI enforces **max line length = 100**. - **Indentation:** use **tabs** for new Godot work (editor default). Do not mix tabs and spaces in the same file; match the file if it already uses spaces. - **Blank lines:** separate functions with two blank lines; sparing single blank lines inside functions for logical groups. - **Trailing commas** in multi-line collections/function args when it improves diffs. +- No trailing whitespace. ## Structure - **`class_name`** only when the type must be referenced globally; otherwise anonymous `extends` is fine. -- Order loosely: `extends` → `class_name` → `signals` → `enums` → `const` → `export/@export` → `onready` → other vars → `_ready` / lifecycle → public methods → private helpers. +- Declaration order should match `gdlint` `class-definitions-order`: + `@tool` → `class_name` → `extends` → file/class docstrings → `signal` → `enum` → `const` → static vars → `@export` vars → public vars → private vars (`_foo`) → `@onready` public vars → `@onready` private vars → everything else. - Virtual overrides (`_ready`, `_process`, `_physics_process`, etc.): keep small; extract helpers with leading `_`. - **Scene / main script shape:** For `client/`, follow [godot-client-script-organization](godot-client-script-organization.md) — thin `main.gd`, split picking, networking, and similar concerns into child scripts. +## CI-enforced lint rules + +- **Names:** keep functions, local vars, loop vars, arguments, signals, and class vars in `snake_case`; keep classes / enums in `PascalCase`; keep enum elements and true constants in `ALL_CAPS`. +- **Signal callbacks:** `_on_Node_signal` style handlers are allowed; otherwise use normal `snake_case`. +- **Preload / load identifiers:** when storing a preloaded script or type-like handle, prefer `PascalCase` names such as `CameraStateScript`; keep ordinary constants in `ALL_CAPS`. +- Avoid duplicated `preload()`s for the same path in one file. +- Avoid bare expressions that are not assigned / returned / awaited. +- Remove unnecessary `pass`. +- Keep files reasonably small: CI enforces **max 1000 lines**, **max 20 public methods**, **max 10 function arguments**, and **max 6 returns** per function. +- Prefer early-return style. `gdlint` forbids `else` / `elif` branches that directly follow a `return`. +- Keep unused arguments intentional and named clearly if you truly need them. + +## Local verification + +- Before pushing GDScript changes, run the same tools CI uses: + `pip install "gdtoolkit==4.5.0"` + `gdlint client/scripts client/test` + `gdformat --check client/scripts client/test` +- If formatting fails, run: + `gdformat client/scripts client/test` +- Install the repo’s local **pre-push** hook from the repo root to enforce those checks automatically: + `./scripts/install-git-hooks.sh` + ## Comments - Use `#` comments; document non-obvious **why**, not what the next line literally does. diff --git a/client/README.md b/client/README.md index 1744692..150b09b 100644 --- a/client/README.md +++ b/client/README.md @@ -110,4 +110,12 @@ On **Linux** (including GitHub Actions), the path must be **`gdUnit4`** with a c **Camera zoom:** Input actions **`camera_zoom_in`** / **`camera_zoom_out`** (mouse wheel, **`=`** / **`-`**, keypad **+** / **−**) are defined in **`project.godot`**. On layouts where **`+`** requires **Shift**, remap or add a binding under **Project → Project Settings → Input Map** if needed. +**Git hook (recommended):** install the repo’s local **pre-push** GDScript lint hook from the repo root: + +```bash +./scripts/install-git-hooks.sh +``` + +It runs **`gdlint client/scripts client/test`** and **`gdformat --check client/scripts client/test`** before each push, preferring the repo-local **`.venv-gd/`** tools when present. + **Reports:** GdUnit writes under **`reports/`** (gitignored); ignore locally generated HTML/XML when committing. diff --git a/client/scripts/isometric_follow_camera.gd b/client/scripts/isometric_follow_camera.gd index d36233e..3b5ac20 100644 --- a/client/scripts/isometric_follow_camera.gd +++ b/client/scripts/isometric_follow_camera.gd @@ -6,12 +6,13 @@ extends Node3D ## [member allow_yaw] is false (no rotate input bound). Future orbit: read relative input here, ## add to `_orbit_yaw_rad`, clamp with [member max_yaw_deg]. +## TODO(E9.M1): map throttled product telemetry (for example `camera_zoom_changed`) +## to this signal when schema exists. +signal zoom_band_changed(new_index: int, distance: float) + const CameraStateScript := preload("res://scripts/camera_state.gd") const ZoomBandConfigScript := preload("res://scripts/zoom_band_config.gd") -## TODO(E9.M1): map throttled product telemetry (e.g. `camera_zoom_changed`) to this signal when schema exists. -signal zoom_band_changed(new_index: int, distance: float) - ## Tuned to match pre-NEON-25 static `Camera3D` at (12,10,12) vs player at (-5,0.9,-5). ## When [member zoom_band_config] is null or has no bands, this is the sole follow distance. @export var follow_target_path: NodePath = NodePath("../Player") @@ -62,9 +63,7 @@ func _ready() -> void: var focus0: Vector3 = t.global_position + Vector3(0.0, focus_vertical_offset, 0.0) var yaw0: float = deg_to_rad(presentation_yaw_deg) + _orbit_yaw_rad var dist0: float = _current_follow_distance() - _smoothed_eye = desired_eye_world( - focus0, dist0, deg_to_rad(pitch_elevation_deg), yaw0 - ) + _smoothed_eye = desired_eye_world(focus0, dist0, deg_to_rad(pitch_elevation_deg), yaw0) global_position = _smoothed_eye camera.look_at(focus0, Vector3.UP) _sync_camera_state(focus0, dist0) diff --git a/client/scripts/zoom_band_config.gd b/client/scripts/zoom_band_config.gd index b8b1f7b..dc3d36c 100644 --- a/client/scripts/zoom_band_config.gd +++ b/client/scripts/zoom_band_config.gd @@ -10,7 +10,8 @@ func band_count() -> int: return band_distances.size() -## **True** when every entry is **> 0** (empty array is vacuously true; callers also check [method band_count]). +## **True** when every entry is **> 0**. +## Empty arrays are vacuously true; callers also check [method band_count]. func all_band_distances_positive() -> bool: for i in range(band_distances.size()): if band_distances[i] <= 0.0: diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100755 index 0000000..aab61d4 --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" + +gdlint_bin="" +gdformat_bin="" + +if [[ -x "$repo_root/.venv-gd/bin/gdlint" ]]; then + gdlint_bin="$repo_root/.venv-gd/bin/gdlint" +elif command -v gdlint >/dev/null 2>&1; then + gdlint_bin="$(command -v gdlint)" +fi + +if [[ -x "$repo_root/.venv-gd/bin/gdformat" ]]; then + gdformat_bin="$repo_root/.venv-gd/bin/gdformat" +elif command -v gdformat >/dev/null 2>&1; then + gdformat_bin="$(command -v gdformat)" +fi + +if [[ -z "$gdlint_bin" || -z "$gdformat_bin" ]]; then + echo "pre-push: missing gdtoolkit commands (gdlint/gdformat)." >&2 + echo "Install locally with:" >&2 + echo " python3 -m venv .venv-gd" >&2 + echo " .venv-gd/bin/pip install \"gdtoolkit==4.5.0\"" >&2 + exit 1 +fi + +echo "pre-push: running gdlint on client/scripts and client/test" +"$gdlint_bin" client/scripts client/test + +echo "pre-push: running gdformat --check on client/scripts and client/test" +"$gdformat_bin" --check client/scripts client/test diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100755 index 0000000..6752368 --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +hook_dir="$repo_root/.git/hooks" +hook_path="$hook_dir/pre-push" + +mkdir -p "$hook_dir" + +cat >"$hook_path" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(git rev-parse --show-toplevel)" +exec "$repo_root/scripts/git-hooks/pre-push" "$@" +EOF + +chmod +x "$hook_path" +echo "Installed pre-push hook at $hook_path"