NEON-26: add pre-push GDScript lint hook

Document the repo's enforced GDScript lint rules and add a local pre-push hook so gdlint/gdformat failures are caught before CI.
pull/33/head
VinPropane 2026-04-08 23:10:40 -04:00
parent ae643bc81c
commit cdf9d59bd9
6 changed files with 97 additions and 9 deletions

View File

@ -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. 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 ## Naming
- **Functions, variables, signals:** `snake_case`. - **Functions, variables, signals:** `snake_case`.
@ -23,18 +25,43 @@ Follow the [Godot GDScript style guide](https://docs.godotengine.org/en/stable/t
## Formatting ## 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. - **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. - **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. - **Trailing commas** in multi-line collections/function args when it improves diffs.
- No trailing whitespace.
## Structure ## Structure
- **`class_name`** only when the type must be referenced globally; otherwise anonymous `extends` is fine. - **`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 `_`. - 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. - **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 repos local **pre-push** hook from the repo root to enforce those checks automatically:
`./scripts/install-git-hooks.sh`
## Comments ## Comments
- Use `#` comments; document non-obvious **why**, not what the next line literally does. - Use `#` comments; document non-obvious **why**, not what the next line literally does.

View File

@ -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. **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 repos 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. **Reports:** GdUnit writes under **`reports/`** (gitignored); ignore locally generated HTML/XML when committing.

View File

@ -6,12 +6,13 @@ extends Node3D
## [member allow_yaw] is false (no rotate input bound). Future orbit: read relative input here, ## [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]. ## 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 CameraStateScript := preload("res://scripts/camera_state.gd")
const ZoomBandConfigScript := preload("res://scripts/zoom_band_config.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). ## 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. ## 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") @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 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 yaw0: float = deg_to_rad(presentation_yaw_deg) + _orbit_yaw_rad
var dist0: float = _current_follow_distance() var dist0: float = _current_follow_distance()
_smoothed_eye = desired_eye_world( _smoothed_eye = desired_eye_world(focus0, dist0, deg_to_rad(pitch_elevation_deg), yaw0)
focus0, dist0, deg_to_rad(pitch_elevation_deg), yaw0
)
global_position = _smoothed_eye global_position = _smoothed_eye
camera.look_at(focus0, Vector3.UP) camera.look_at(focus0, Vector3.UP)
_sync_camera_state(focus0, dist0) _sync_camera_state(focus0, dist0)

View File

@ -10,7 +10,8 @@ func band_count() -> int:
return band_distances.size() 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: func all_band_distances_positive() -> bool:
for i in range(band_distances.size()): for i in range(band_distances.size()):
if band_distances[i] <= 0.0: if band_distances[i] <= 0.0:

View File

@ -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

View File

@ -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"