From 9a9f4ab3ad59345db467d953c4caee380775dac9 Mon Sep 17 00:00:00 2001 From: don Date: Sun, 29 Mar 2026 21:09:43 -0400 Subject: [PATCH 1/4] docs: add NS-14 click-to-move implementation plan Made-with: Cursor --- docs/plans/NS-14-implementation-plan.md | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/plans/NS-14-implementation-plan.md diff --git a/docs/plans/NS-14-implementation-plan.md b/docs/plans/NS-14-implementation-plan.md new file mode 100644 index 0000000..0c6a742 --- /dev/null +++ b/docs/plans/NS-14-implementation-plan.md @@ -0,0 +1,82 @@ +# NS-14 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NS-14 | +| **Title** | E1.M1: Click-to-move prototype (client-only) | +| **Jira** | [NS-14](https://neon-sprawl.atlassian.net/browse/NS-14) | +| **Parent context** | [NS-10 — E1.M1 InputAndMovementRuntime](https://neon-sprawl.atlassian.net/browse/NS-10) | + +## Goal, scope, and out-of-scope + +**Goal:** On the Godot client only, prove click-to-move locomotion and the input → world-target path before any networking or server authority. + +**In scope** + +- Placeholder avatar in the prototype scene (`main.tscn` or a dedicated movement test scene; default choice: extend `main.tscn` so F5 still runs the prototype). +- Ground pick via camera raycast; on valid hit, command movement to that point. +- Flat prototype terrain and simple obstacles sufficient to validate stopping behavior. +- Brief note in README and/or scene that this is **temporary** until authoritative movement sync exists. + +**Out of scope (per Jira)** + +- Game server, persistence, `MoveCommand` / wire protocol, Protobuf contracts. + +## Acceptance criteria checklist + +- [ ] Clicking walkable ground moves the avatar to the destination without requiring WASD (mouse-only locomotion for this prototype). +- [ ] Movement stops at obstacles when using navigation; **or** if using a direct move-to approach instead, the limitation is documented (README and/or plan). +- [ ] README or an in-editor scene note states this behavior is provisional until authoritative sync lands. + +## Technical approach + +1. **Scene setup (`main.tscn`)** + - Add a **Camera3D** suitable for ground picking (angled downward at a `StaticBody3D` floor with collision). + - Add a **CharacterBody3D** placeholder (e.g. `CapsuleMesh` + `CollisionShape3D`) as the avatar. + - Add at least one **obstacle** (`StaticBody3D`) so “stop at obstacles” is observable. + - Add a **NavigationRegion3D** covering the walkable floor; bake a navigation mesh from the floor (and subtract or exclude obstacles per Godot 4 workflow). + +2. **Movement model** + - Prefer **NavigationAgent3D** on the avatar: set `target_position` from the raycast hit on the ground; each frame move the body toward the agent’s next path position (e.g. `velocity` + `move_and_slide`, or equivalent Godot 4 pattern). This satisfies the AC path that expects stopping at obstacles via navigation. + - Fallback only if navigation proves unnecessarily heavy for the first slice: direct `move_toward` / velocity toward the click point with `move_and_slide` and document that there is no pathfinding. + +3. **Input** + - `_unhandled_input` or `_input`: on left mouse button, raycast from camera through cursor into the physics world; if the collider is the ground (or a dedicated “walkable” layer/mask), set the navigation target. + +4. **Documentation** + - Update `client/README.md` with a short “Movement prototype (NS-14)” subsection: mouse click-to-move, navigation-based obstacle avoidance, **not** server-authoritative. + - Optional: `Editable Children` note on root node in scene or a comment in `main.gd` pointing to NS-14 / temporariness. + +## Files to add + +| Path | Purpose | +|------|--------| +| None required initially | If `main.gd` grows unwieldy, optionally extract `scripts/player_navigation.gd` (or similar) attached to the avatar—only if it keeps the scene readable. | + +*(Default: implement in existing `scripts/main.gd` + scene edits unless script size forces a split.)* + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scenes/main.tscn` | Add camera, terrain, obstacles, navigation region, avatar, and wire scripts. | +| `client/scripts/main.gd` | Raycast-on-click, navigation target updates, and any per-frame movement driver if kept on root. | +| `client/README.md` | Document prototype movement, temporary nature, and manual verification steps. | +| `client/project.godot` | Only if needed (e.g. input map, layer names, or feature flags)—prefer minimal diffs. | + +## Tests + +- **Automated:** None for this story; the repo has no Godot test harness yet ([testing expectations](../../.cursor/rules/testing-expectations.md)). +- **Manual verification:** + 1. Open `client/` in Godot 4.2+, run main scene. + 2. Click on the floor: avatar moves to the point without using WASD. + 3. Click a destination behind an obstacle: avatar path avoids or stops appropriately per navigation behavior. + 4. Confirm README/scene note mentions provisional client-only movement. + +## Open questions / risks + +- **Navigation baking:** First-time contributors need to bake the navigation mesh in the editor after pulling scene changes; document that in README if it is not obvious. +- **Isometric later:** Camera rig may be replaced for isometric; keep movement logic independent of camera style where possible (raycast from current camera). +- **Performance:** Single-agent prototype is sufficient; no need to optimize for crowds in NS-14. -- 2.40.1 From 2aabcb969a2572442ab2d947d892eff35d307b69 Mon Sep 17 00:00:00 2001 From: don Date: Sun, 29 Mar 2026 21:19:49 -0400 Subject: [PATCH 2/4] chore(cursor): require explicit user approval before git commit Add commit-and-review rule; align story-kickoff and git-workflow with it. Made-with: Cursor --- .cursor/rules/commit-and-review.md | 21 +++++++++++++++++++++ .cursor/rules/git-workflow.md | 2 ++ .cursor/rules/story-kickoff.md | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .cursor/rules/commit-and-review.md diff --git a/.cursor/rules/commit-and-review.md b/.cursor/rules/commit-and-review.md new file mode 100644 index 0000000..70ca3c2 --- /dev/null +++ b/.cursor/rules/commit-and-review.md @@ -0,0 +1,21 @@ +--- +description: Never git commit without explicit user instruction; user reviews before any commit +alwaysApply: true +--- + +# Commits and review (Neon Sprawl) + +## No commits without explicit instruction + +- Do **not** run **`git commit`** (or equivalent, e.g. committing via tools) unless the user **explicitly** asks—clear wording such as “commit this”, “make a commit”, or “commit with message …”. +- Phrases like “implement X”, “begin work”, or “open a PR” are **not** implicit permission to commit. +- **Default after edits:** leave changes **uncommitted** (working tree or staged only if the user asked to stage). Summarize what changed and where so the user can review in the Git / diff UI, then wait for commit instructions. + +## Review before commit + +- The user should **review** the diff before anything is committed. The agent’s job is to make the changes visible (uncommitted) and explain them; the user decides when to commit. +- If the user asks to commit, use a message that matches repo conventions; still follow [git workflow](git-workflow.md) (branch vs `main`, doc-only vs code). + +## Scope + +- Applies to **all** commits the agent might make, including **documentation-only** changes (e.g. `docs/plans/`, README), not only application source. diff --git a/.cursor/rules/git-workflow.md b/.cursor/rules/git-workflow.md index 8c31560..1d29d9c 100644 --- a/.cursor/rules/git-workflow.md +++ b/.cursor/rules/git-workflow.md @@ -5,6 +5,8 @@ alwaysApply: true # Git workflow (Neon Sprawl) +**Agent:** Never run `git commit` without explicit user instruction; the user reviews diffs first. See [commit-and-review](commit-and-review.md). + - **Documentation-only work** — Commit directly on **`main`**. This means Markdown (`.md`) under `docs/`, root and nested `README.md` files, `neon_sprawl_vision.plan.md`, and other prose docs. No feature branch required. - **Code or implementation changes** — Use a **branch** (e.g. `feature/…`, `fix/…`), then merge to `main` when ready. This includes application source (**C#**, **GDScript**), Godot scenes and project files, `docker-compose.yml`, `.csproj` / build config, CI workflows, **JSON/YAML game data** under `content/`, and JSON Schema when it ships with data pipelines—not standalone doc prose. diff --git a/.cursor/rules/story-kickoff.md b/.cursor/rules/story-kickoff.md index 3588999..6a1ec21 100644 --- a/.cursor/rules/story-kickoff.md +++ b/.cursor/rules/story-kickoff.md @@ -24,7 +24,7 @@ When the user starts work on a **Jira story** (e.g. issue key `NS-14`, phrases l ## 4. Planning document - Add **`docs/plans/{JIRA_KEY}-implementation-plan.md`** (example: `docs/plans/NS-14-implementation-plan.md`). Create `docs/plans/` if it does not exist. -- Prefer committing this plan on **`main`** so it exists even before a feature branch; it counts as documentation-style work per [git workflow](git-workflow.md). +- **Do not** `git commit` the plan unless the user explicitly asks; see [commit-and-review](commit-and-review.md). When the user commits plan-only changes, use **`main`** per [git workflow](git-workflow.md). **Required sections** in that file: -- 2.40.1 From 343592541a644008f9efc2fead44f2d80e94be7f Mon Sep 17 00:00:00 2001 From: don Date: Sun, 29 Mar 2026 21:19:52 -0400 Subject: [PATCH 3/4] feat(client): NS-14 click-to-move with NavigationAgent3D - Main scene: floor, obstacle, nav mesh, camera, CharacterBody3D + agent - Click-to-move on walkable group; pick mask avoids avatar self-hit - player.gd drives path following; README and plan AC updated Made-with: Cursor --- client/README.md | 16 +++++ client/scenes/main.tscn | 84 ++++++++++++++++++++++++- client/scripts/main.gd | 29 ++++++++- client/scripts/player.gd | 30 +++++++++ docs/plans/NS-14-implementation-plan.md | 6 +- 5 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 client/scripts/player.gd diff --git a/client/README.md b/client/README.md index b80d7cf..b05c16c 100644 --- a/client/README.md +++ b/client/README.md @@ -5,6 +5,22 @@ Open this **`client/`** directory as a project in **Godot 4.2+** (4.x recommende - Main scene: `scenes/main.tscn` (bootstrap `scripts/main.gd`). - Networking will use **WebSocket** or **TCP** to the C# server; authoritative logic stays on the server per [`docs/architecture/tech_stack.md`](../docs/architecture/tech_stack.md). +## Movement prototype (NS-14) + +The main scene includes a **client-only** click-to-move demo: + +- **Left-click** walkable ground (the large floor) to move the capsule avatar; **WASD is not required**. +- Paths use **NavigationAgent3D** over a hand-authored navigation mesh with a central obstacle, so the avatar **routes around** the block instead of sliding through it. +- The avatar lives on **physics layer 2**; the pick ray uses **mask 1** so clicks pass through the avatar and hit the floor. + +This behavior is **temporary**: when authoritative movement and `MoveCommand` / `PositionState` exist, the client will follow server state instead of driving navigation locally. + +### Manual check + +1. Run the main scene (**F5**). +2. Click the floor: the avatar walks to the point. +3. Click behind the center crate: the avatar walks around it via the nav mesh. + ## First run 1. Install [Godot 4.x](https://godotengine.org/download). diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 39b869a..d8b5dc6 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1,6 +1,88 @@ -[gd_scene load_steps=2 format=3 uid="uid://cyberpunkmmo_main"] +[gd_scene load_steps=9 format=3 uid="uid://cyberpunkmmo_main"] [ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] +[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_floor"] +size = Vector3(20, 0.2, 20) + +[sub_resource type="BoxMesh" id="BoxMesh_floor"] +size = Vector3(20, 0.2, 20) + +[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle"] +size = Vector3(2, 2, 2) + +[sub_resource type="BoxMesh" id="BoxMesh_obstacle"] +size = Vector3(2, 2, 2) + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_player"] +radius = 0.4 +height = 1.0 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_player"] +radius = 0.4 +height = 1.0 + +[sub_resource type="NavigationMesh" id="NavigationMesh_frame"] +vertices = PackedVector3Array(-10, 0.02, -10, -2, 0.02, -10, 2, 0.02, -10, 10, 0.02, -10, 10, 0.02, 10, 2, 0.02, 10, -2, 0.02, 10, -10, 0.02, 10, -2, 0.02, -2, 2, 0.02, -2, 2, 0.02, 2, -2, 0.02, 2) +polygons = [PackedInt32Array(0, 1, 6, 7), PackedInt32Array(1, 2, 9, 8), PackedInt32Array(2, 3, 4, 5), PackedInt32Array(11, 10, 5, 6)] +agent_height = 2.0 +agent_radius = 0.45 +agent_max_climb = 0.25 [node name="Main" type="Node3D"] script = ExtResource("1_main") + +[node name="World" type="Node3D" parent="."] + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="World"] +transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 6, 0) +shadow_enabled = true + +[node name="Camera3D" type="Camera3D" parent="World"] +transform = Transform3D(0.819152, -0.40558, 0.40558, 0, 0.707107, 0.707107, -0.573576, -0.579228, 0.579228, 12, 10, 12) +current = true +fov = 50.0 + +[node name="Floor" type="StaticBody3D" parent="World" groups=["walkable"]] +collision_layer = 1 + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Floor"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0) +mesh = SubResource("BoxMesh_floor") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Floor"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0) +shape = SubResource("BoxShape3D_floor") + +[node name="Obstacle" type="StaticBody3D" parent="World"] +collision_layer = 1 + +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Obstacle"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("BoxMesh_obstacle") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Obstacle"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("BoxShape3D_obstacle") + +[node name="NavigationRegion3D" type="NavigationRegion3D" parent="World"] +navigation_mesh = SubResource("NavigationMesh_frame") + +[node name="Player" type="CharacterBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0.9, -5) +collision_layer = 2 +collision_mask = 1 +script = ExtResource("2_player") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +shape = SubResource("CapsuleShape3D_player") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Player"] +mesh = SubResource("CapsuleMesh_player") + +[node name="NavigationAgent3D" type="NavigationAgent3D" parent="Player"] +path_desired_distance = 0.35 +target_desired_distance = 0.4 +path_height_offset = 0.05 diff --git a/client/scripts/main.gd b/client/scripts/main.gd index e45a4d4..aa0a81b 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -1,4 +1,29 @@ extends Node3D -func _ready() -> void: - print("Neon Sprawl client bootstrap (Godot). Connect to game server TBD.") +## NS-14: client-only click-to-move. Provisional until authoritative movement sync (server / MoveCommand). + +@onready var _camera: Camera3D = $World/Camera3D +@onready var _nav_agent: NavigationAgent3D = $Player/NavigationAgent3D + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + var mb := event as InputEventMouseButton + if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: + _try_set_move_target(mb.position) + + + +func _try_set_move_target(screen_pos: Vector2) -> void: + var origin: Vector3 = _camera.project_ray_origin(screen_pos) + var ray_dir: Vector3 = _camera.project_ray_normal(screen_pos) + var to: Vector3 = origin + ray_dir * 2000.0 + var query := PhysicsRayQueryParameters3D.create(origin, to) + query.collision_mask = 1 + var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state + var hit: Dictionary = space.intersect_ray(query) + if hit.is_empty(): + return + var collider: Variant = hit.get("collider") + if collider is Node and (collider as Node).is_in_group("walkable"): + _nav_agent.target_position = hit.position diff --git a/client/scripts/player.gd b/client/scripts/player.gd new file mode 100644 index 0000000..958e610 --- /dev/null +++ b/client/scripts/player.gd @@ -0,0 +1,30 @@ +extends CharacterBody3D + +## NS-14: drives CharacterBody3D toward NavigationAgent3D path. Client-only; not server-authoritative. + +const MOVE_SPEED: float = 5.0 + +@onready var _nav: NavigationAgent3D = $NavigationAgent3D + + +func _ready() -> void: + # Map sync: avoid querying paths before the navigation map is ready. + await get_tree().physics_frame + _nav.target_position = global_position + + + +func _physics_process(_delta: float) -> void: + if _nav.is_navigation_finished(): + velocity = Vector3.ZERO + move_and_slide() + return + + var next: Vector3 = _nav.get_next_path_position() + var dir: Vector3 = next - global_position + dir.y = 0.0 + if dir.length() < 0.05: + velocity = Vector3.ZERO + else: + velocity = dir.normalized() * MOVE_SPEED + move_and_slide() diff --git a/docs/plans/NS-14-implementation-plan.md b/docs/plans/NS-14-implementation-plan.md index 0c6a742..df8709b 100644 --- a/docs/plans/NS-14-implementation-plan.md +++ b/docs/plans/NS-14-implementation-plan.md @@ -26,9 +26,9 @@ ## Acceptance criteria checklist -- [ ] Clicking walkable ground moves the avatar to the destination without requiring WASD (mouse-only locomotion for this prototype). -- [ ] Movement stops at obstacles when using navigation; **or** if using a direct move-to approach instead, the limitation is documented (README and/or plan). -- [ ] README or an in-editor scene note states this behavior is provisional until authoritative sync lands. +- [x] Clicking walkable ground moves the avatar to the destination without requiring WASD (mouse-only locomotion for this prototype). +- [x] Movement stops at obstacles when using navigation; **or** if using a direct move-to approach instead, the limitation is documented (README and/or plan). +- [x] README or an in-editor scene note states this behavior is provisional until authoritative sync lands. ## Technical approach -- 2.40.1 From 64092566a4952393d41545ae841c3251425def60 Mon Sep 17 00:00:00 2001 From: don Date: Sun, 29 Mar 2026 22:08:25 -0400 Subject: [PATCH 4/4] fix(client): reliable NS-14 click-to-move without NavigationMesh - Drive movement with horizontal steering + move_and_slide toward click goal - Remove NavigationRegion3D / NavigationAgent3D; obstacle interaction is slide - main.gd: raycast to walkable, Player.set_move_goal; viewport camera + mouse - Scene: explicit physics layers/mask on floor, obstacle, player - Docs: Godot 4.6 baseline, README + NS-14 plan + tech_stack; Godot .import/.uid Made-with: Cursor --- README.md | 2 +- client/README.md | 12 +++-- client/icon.svg.import | 43 ++++++++++++++++++ client/project.godot | 18 +++++++- client/scenes/main.tscn | 58 +++++++++---------------- client/scripts/main.gd | 30 ++++++++++--- client/scripts/main.gd.uid | 1 + client/scripts/player.gd | 30 ++++++------- client/scripts/player.gd.uid | 1 + docs/architecture/tech_stack.md | 2 + docs/plans/NS-14-implementation-plan.md | 6 +-- 11 files changed, 134 insertions(+), 69 deletions(-) create mode 100644 client/icon.svg.import create mode 100644 client/scripts/main.gd.uid create mode 100644 client/scripts/player.gd.uid diff --git a/README.md b/README.md index d714760..c36cd3f 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ Connection (dev): host `localhost`, port `5432`, database `neon_sprawl`, user `n ### Run the client -Open the [`client/`](client/) folder in **Godot 4.2+** and run the main scene (see [`client/README.md`](client/README.md)). +Open the [`client/`](client/) folder in **Godot 4.6** and run the main scene (see [`client/README.md`](client/README.md)). diff --git a/client/README.md b/client/README.md index b05c16c..254ac1d 100644 --- a/client/README.md +++ b/client/README.md @@ -1,6 +1,6 @@ # Neon Sprawl — Godot client -Open this **`client/`** directory as a project in **Godot 4.2+** (4.x recommended). Use the **standard** Godot build (not the .NET build)—client code is **GDScript** (`.gd`). +Open this **`client/`** directory as a project in **Godot 4.6** (4.x compatible). Use the **standard** Godot build (not the .NET build)—client code is **GDScript** (`.gd`). - Main scene: `scenes/main.tscn` (bootstrap `scripts/main.gd`). - Networking will use **WebSocket** or **TCP** to the C# server; authoritative logic stays on the server per [`docs/architecture/tech_stack.md`](../docs/architecture/tech_stack.md). @@ -10,8 +10,8 @@ Open this **`client/`** directory as a project in **Godot 4.2+** (4.x recommende The main scene includes a **client-only** click-to-move demo: - **Left-click** walkable ground (the large floor) to move the capsule avatar; **WASD is not required**. -- Paths use **NavigationAgent3D** over a hand-authored navigation mesh with a central obstacle, so the avatar **routes around** the block instead of sliding through it. -- The avatar lives on **physics layer 2**; the pick ray uses **mask 1** so clicks pass through the avatar and hit the floor. +- Movement is **direct horizontal steering** plus **`move_and_slide()`**: the capsule walks toward the click and **slides along** the center crate instead of pathfinding around it. (A hand-authored `NavigationMesh` was not reliable across Godot versions; **NavigationAgent3D** can return later for routed paths.) +- The avatar is on **physics layer 2** with **mask 1** (floor + obstacle on layer 1); the pick ray uses **mask 1** so clicks pass through the avatar and hit the floor. This behavior is **temporary**: when authoritative movement and `MoveCommand` / `PositionState` exist, the client will follow server state instead of driving navigation locally. @@ -19,7 +19,11 @@ This behavior is **temporary**: when authoritative movement and `MoveCommand` / 1. Run the main scene (**F5**). 2. Click the floor: the avatar walks to the point. -3. Click behind the center crate: the avatar walks around it via the nav mesh. +3. Click behind the center crate: the avatar **slides** against the crate (no nav mesh path around it in this build). + +### Clicks still ignored? + +In the **Game** dock, **Input** must be active (not the **2D** / **3D** scene-picking tools) so events go to the game. If Input is on and clicks still do nothing, pick rays must use the **viewport’s current camera** and **mouse position** (the script does this so the embedded Game view matches the ray). ## First run diff --git a/client/icon.svg.import b/client/icon.svg.import new file mode 100644 index 0000000..fe53571 --- /dev/null +++ b/client/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clvcqwwuhaity" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/client/project.godot b/client/project.godot index 7d439a8..be34e38 100644 --- a/client/project.godot +++ b/client/project.godot @@ -1,13 +1,23 @@ -; Godot 4.x project — import this folder in the Godot project manager. +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters config_version=5 +[animation] + +compatibility/default_parent_skeleton_in_mesh_instance_3d=true + [application] config/name="Neon Sprawl" config/description="Neon Sprawl — prototype client (isometric MMO; planning docs in repo root)." run/main_scene="res://scenes/main.tscn" -config/features=PackedStringArray("4.2", "Forward Plus") +config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" [display] @@ -15,6 +25,10 @@ config/icon="res://icon.svg" window/size/viewport_width=1280 window/size/viewport_height=720 +[dotnet] + +project/assembly_name="Neon Sprawl" + [rendering] textures/canvas_textures/default_texture_filter=0 diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index d8b5dc6..2a86b14 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1,20 +1,20 @@ -[gd_scene load_steps=9 format=3 uid="uid://cyberpunkmmo_main"] +[gd_scene format=3 uid="uid://dg2g1nd82lyxm"] -[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] -[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"] - -[sub_resource type="BoxShape3D" id="BoxShape3D_floor"] -size = Vector3(20, 0.2, 20) +[ext_resource type="Script" uid="uid://bh04b3iify0hd" path="res://scripts/main.gd" id="1_main"] +[ext_resource type="Script" uid="uid://1jimgt3d4bjj" path="res://scripts/player.gd" id="2_player"] [sub_resource type="BoxMesh" id="BoxMesh_floor"] size = Vector3(20, 0.2, 20) -[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle"] -size = Vector3(2, 2, 2) +[sub_resource type="BoxShape3D" id="BoxShape3D_floor"] +size = Vector3(20, 0.2, 20) [sub_resource type="BoxMesh" id="BoxMesh_obstacle"] size = Vector3(2, 2, 2) +[sub_resource type="BoxShape3D" id="BoxShape3D_obstacle"] +size = Vector3(2, 2, 2) + [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_player"] radius = 0.4 height = 1.0 @@ -23,66 +23,50 @@ height = 1.0 radius = 0.4 height = 1.0 -[sub_resource type="NavigationMesh" id="NavigationMesh_frame"] -vertices = PackedVector3Array(-10, 0.02, -10, -2, 0.02, -10, 2, 0.02, -10, 10, 0.02, -10, 10, 0.02, 10, 2, 0.02, 10, -2, 0.02, 10, -10, 0.02, 10, -2, 0.02, -2, 2, 0.02, -2, 2, 0.02, 2, -2, 0.02, 2) -polygons = [PackedInt32Array(0, 1, 6, 7), PackedInt32Array(1, 2, 9, 8), PackedInt32Array(2, 3, 4, 5), PackedInt32Array(11, 10, 5, 6)] -agent_height = 2.0 -agent_radius = 0.45 -agent_max_climb = 0.25 - -[node name="Main" type="Node3D"] +[node name="Main" type="Node3D" unique_id=1358372723] script = ExtResource("1_main") -[node name="World" type="Node3D" parent="."] +[node name="World" type="Node3D" parent="." unique_id=1042212190] -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="World"] +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="World" unique_id=201819776] transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 6, 0) shadow_enabled = true -[node name="Camera3D" type="Camera3D" parent="World"] +[node name="Camera3D" type="Camera3D" parent="World" unique_id=1124088856] transform = Transform3D(0.819152, -0.40558, 0.40558, 0, 0.707107, 0.707107, -0.573576, -0.579228, 0.579228, 12, 10, 12) current = true fov = 50.0 -[node name="Floor" type="StaticBody3D" parent="World" groups=["walkable"]] +[node name="Floor" type="StaticBody3D" parent="World" unique_id=1800743112 groups=["walkable"]] collision_layer = 1 -[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Floor"] +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Floor" unique_id=152652175] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0) mesh = SubResource("BoxMesh_floor") -[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Floor"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Floor" unique_id=409532142] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0) shape = SubResource("BoxShape3D_floor") -[node name="Obstacle" type="StaticBody3D" parent="World"] +[node name="Obstacle" type="StaticBody3D" parent="World" unique_id=1638845763] collision_layer = 1 -[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Obstacle"] +[node name="MeshInstance3D" type="MeshInstance3D" parent="World/Obstacle" unique_id=750473628] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) mesh = SubResource("BoxMesh_obstacle") -[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Obstacle"] +[node name="CollisionShape3D" type="CollisionShape3D" parent="World/Obstacle" unique_id=1344302688] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) shape = SubResource("BoxShape3D_obstacle") -[node name="NavigationRegion3D" type="NavigationRegion3D" parent="World"] -navigation_mesh = SubResource("NavigationMesh_frame") - -[node name="Player" type="CharacterBody3D" parent="."] +[node name="Player" type="CharacterBody3D" parent="." unique_id=352931696] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0.9, -5) collision_layer = 2 collision_mask = 1 script = ExtResource("2_player") -[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +[node name="CollisionShape3D" type="CollisionShape3D" parent="Player" unique_id=1695755590] shape = SubResource("CapsuleShape3D_player") -[node name="MeshInstance3D" type="MeshInstance3D" parent="Player"] +[node name="MeshInstance3D" type="MeshInstance3D" parent="Player" unique_id=2027034386] mesh = SubResource("CapsuleMesh_player") - -[node name="NavigationAgent3D" type="NavigationAgent3D" parent="Player"] -path_desired_distance = 0.35 -target_desired_distance = 0.4 -path_height_offset = 0.05 diff --git a/client/scripts/main.gd b/client/scripts/main.gd index aa0a81b..e52f950 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -3,20 +3,23 @@ extends Node3D ## NS-14: client-only click-to-move. Provisional until authoritative movement sync (server / MoveCommand). @onready var _camera: Camera3D = $World/Camera3D -@onready var _nav_agent: NavigationAgent3D = $Player/NavigationAgent3D +@onready var _player: CharacterBody3D = $Player -func _unhandled_input(event: InputEvent) -> void: +func _input(event: InputEvent) -> void: if event is InputEventMouseButton: var mb := event as InputEventMouseButton if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: - _try_set_move_target(mb.position) + _try_set_move_target(get_viewport().get_mouse_position()) func _try_set_move_target(screen_pos: Vector2) -> void: - var origin: Vector3 = _camera.project_ray_origin(screen_pos) - var ray_dir: Vector3 = _camera.project_ray_normal(screen_pos) + var cam: Camera3D = get_viewport().get_camera_3d() + if cam == null: + cam = _camera + var origin: Vector3 = cam.project_ray_origin(screen_pos) + var ray_dir: Vector3 = cam.project_ray_normal(screen_pos) var to: Vector3 = origin + ray_dir * 2000.0 var query := PhysicsRayQueryParameters3D.create(origin, to) query.collision_mask = 1 @@ -25,5 +28,18 @@ func _try_set_move_target(screen_pos: Vector2) -> void: if hit.is_empty(): return var collider: Variant = hit.get("collider") - if collider is Node and (collider as Node).is_in_group("walkable"): - _nav_agent.target_position = hit.position + if not _collider_is_walkable(collider): + return + var hit_pos: Variant = hit.get("position") + if hit_pos is Vector3: + _player.set_move_goal(hit_pos as Vector3) + + + +func _collider_is_walkable(collider: Variant) -> bool: + var n: Node = collider as Node + while n: + if n.is_in_group("walkable"): + return true + n = n.get_parent() + return false diff --git a/client/scripts/main.gd.uid b/client/scripts/main.gd.uid new file mode 100644 index 0000000..f7bcbd9 --- /dev/null +++ b/client/scripts/main.gd.uid @@ -0,0 +1 @@ +uid://bh04b3iify0hd diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 958e610..2c42033 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -1,30 +1,30 @@ extends CharacterBody3D -## NS-14: drives CharacterBody3D toward NavigationAgent3D path. Client-only; not server-authoritative. +## NS-14: click-to-move using horizontal steering + move_and_slide (not NavigationAgent3D). +## Obstacles are handled by physics sliding, not pathfinding — see client README. const MOVE_SPEED: float = 5.0 +const ARRIVE_EPS: float = 0.35 -@onready var _nav: NavigationAgent3D = $NavigationAgent3D +var _goal: Vector3 func _ready() -> void: - # Map sync: avoid querying paths before the navigation map is ready. - await get_tree().physics_frame - _nav.target_position = global_position + _goal = global_position +func set_move_goal(world_pos: Vector3) -> void: + _goal = world_pos + _goal.y = global_position.y + + func _physics_process(_delta: float) -> void: - if _nav.is_navigation_finished(): - velocity = Vector3.ZERO - move_and_slide() - return - - var next: Vector3 = _nav.get_next_path_position() - var dir: Vector3 = next - global_position - dir.y = 0.0 - if dir.length() < 0.05: + var to_goal: Vector3 = _goal - global_position + to_goal.y = 0.0 + if to_goal.length() <= ARRIVE_EPS: velocity = Vector3.ZERO else: - velocity = dir.normalized() * MOVE_SPEED + velocity = to_goal.normalized() * MOVE_SPEED + velocity.y = 0.0 move_and_slide() diff --git a/client/scripts/player.gd.uid b/client/scripts/player.gd.uid new file mode 100644 index 0000000..68d7bd9 --- /dev/null +++ b/client/scripts/player.gd.uid @@ -0,0 +1 @@ +uid://1jimgt3d4bjj diff --git a/docs/architecture/tech_stack.md b/docs/architecture/tech_stack.md index b54568e..bdc24c4 100644 --- a/docs/architecture/tech_stack.md +++ b/docs/architecture/tech_stack.md @@ -24,6 +24,8 @@ ## Client: Godot 4 +The [`client/`](../../client/) Godot project targets **4.6** (`config/features` in `project.godot`). Older 4.x may work but 4.6 is the baseline for editor and manual testing. + **Why** - Strong fit for solo iteration: fast reload, small install, no revenue cap. diff --git a/docs/plans/NS-14-implementation-plan.md b/docs/plans/NS-14-implementation-plan.md index df8709b..0c6a742 100644 --- a/docs/plans/NS-14-implementation-plan.md +++ b/docs/plans/NS-14-implementation-plan.md @@ -26,9 +26,9 @@ ## Acceptance criteria checklist -- [x] Clicking walkable ground moves the avatar to the destination without requiring WASD (mouse-only locomotion for this prototype). -- [x] Movement stops at obstacles when using navigation; **or** if using a direct move-to approach instead, the limitation is documented (README and/or plan). -- [x] README or an in-editor scene note states this behavior is provisional until authoritative sync lands. +- [ ] Clicking walkable ground moves the avatar to the destination without requiring WASD (mouse-only locomotion for this prototype). +- [ ] Movement stops at obstacles when using navigation; **or** if using a direct move-to approach instead, the limitation is documented (README and/or plan). +- [ ] README or an in-editor scene note states this behavior is provisional until authoritative sync lands. ## Technical approach -- 2.40.1