Compare commits
3 Commits
353fa23d29
...
343592541a
| Author | SHA1 | Date |
|---|---|---|
|
|
343592541a | |
|
|
2aabcb969a | |
|
|
9a9f4ab3ad |
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
- [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
|
||||
|
||||
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.
|
||||
Loading…
Reference in New Issue