# Basic Player Interaction Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Give players first-person movement (WASD + mouse look) and the ability to interact with objects via raycast + E key. Establish the Interactable interface for future puzzle elements.

**Architecture:** Extract PlayerController into its own script (`player_controller.gd`) attached to a `CharacterBody3D`. `tavern.gd._spawn_player()` creates it via `preload`. A lightweight `interactable.gd` base script defines the `interact(peer_id)` interface. The RayCast3D hangs off the Camera3D. Input actions are added to `project.godot`. No RPCs needed for local interaction detection; only puzzle outcomes trigger network events (out of scope for this plan).

**Tech Stack:** Godot 4.x GDScript, CharacterBody3D, RayCast3D, InputMap

---

## Context

Currently `tavern.gd._spawn_player()` creates a plain `Node3D` named `PlayerController` with a `Camera3D` child — no movement, no input, no collision. The camera transform is already broadcast to the DM every 0.1s via `_broadcast_camera_transform()`. That code references `get_node_or_null("PlayerController") as Node3D` — this still works after upgrading to `CharacterBody3D` since it extends `Node3D`.

The `_broadcast_camera_transform()` in `tavern.gd` looks for `PlayerController/Camera3D`. The new controller keeps this same node structure.

Input actions to add to `project.godot`:
- `move_forward` — W
- `move_backward` — S
- `move_left` — A
- `move_right` — D
- `interact` — E

## Files

| File | Action |
|------|--------|
| `scripts/player_controller.gd` | New — CharacterBody3D movement + mouse look + interaction |
| `scripts/interactable.gd` | New — base interface for interactable objects |
| `scripts/tavern.gd` | Modify `_spawn_player()` — use CharacterBody3D + player_controller.gd |
| `project.godot` | Add 5 input actions to `[input]` section |

---

## Task 1: Add input actions to project.godot

**Files:**
- Modify: `project.godot` — add `[input]` section entries

- [ ] **Step 1: Add input actions to project.godot**

  In the `[input]` section (create if missing), add:

  ```ini
  [input]

  move_forward={
  "deadzone": 0.5,
  "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
  ]
  }
  move_backward={
  "deadzone": 0.5,
  "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
  ]
  }
  move_left={
  "deadzone": 0.5,
  "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
  ]
  }
  move_right={
  "deadzone": 0.5,
  "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
  ]
  }
  interact={
  "deadzone": 0.5,
  "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
  ]
  }
  ```

  Alternatively: use Godot editor Project Settings → Input Map to add the actions, which writes the correct format automatically.

- [ ] **Step 2: Open Godot, verify actions appear in Project Settings → Input Map**

- [ ] **Step 3: Commit**

  ```bash
  git add ruf-der-pilze/project.godot
  git commit -m "feat: project — add WASD + interact input actions"
  ```

---

## Task 2: Create interactable.gd base script

**Files:**
- Create: `scripts/interactable.gd`

Any object in the world that players can interact with (keys, switches, doors) gets this script. The PlayerController calls `interact(peer_id)` on whatever it hits.

- [ ] **Step 1: Create scripts/interactable.gd**

  ```gdscript
  # Base class for all interactable objects in the world.
  # Attach to a StaticBody3D or Area3D that has a CollisionShape3D.
  # Override interact() in subclasses.
  class_name Interactable
  extends Node3D

  ## Called when a player interacts with this object.
  ## peer_id is the NetworkManager.my_id of the interacting player.
  func interact(_peer_id: int) -> void:
      pass
  ```

  The `class_name Interactable` declaration is required so subclasses can use `extends Interactable` instead of the fragile string path form.

- [ ] **Step 2: Commit**

  ```bash
  git add ruf-der-pilze/scripts/interactable.gd
  git commit -m "feat: add interactable base script"
  ```

---

## Task 3: Create player_controller.gd

**Files:**
- Create: `scripts/player_controller.gd`

This script is attached to the `CharacterBody3D` that `tavern.gd` creates. It handles movement, mouse look, and interaction raycast.

- [ ] **Step 1: Create scripts/player_controller.gd**

  ```gdscript
  extends CharacterBody3D

  const SPEED := 4.0
  const GRAVITY := 9.8
  const MOUSE_SENSITIVITY := 0.002
  const INTERACT_DISTANCE := 2.5

  var _camera: Camera3D
  var _raycast: RayCast3D


  func _ready() -> void:
      _camera = get_node("Camera3D") as Camera3D
      _raycast = _camera.get_node("InteractRay") as RayCast3D
      Input.mouse_mode = Input.MOUSE_MODE_CAPTURED


  func _physics_process(delta: float) -> void:
      if not is_on_floor():
          velocity.y -= GRAVITY * delta

      var input_dir := Vector2(
          Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
          Input.get_action_strength("move_backward") - Input.get_action_strength("move_forward")
      )
      var move_dir := (transform.basis.x * input_dir.x + -transform.basis.z * input_dir.y).normalized()
      velocity.x = move_dir.x * SPEED
      velocity.z = move_dir.z * SPEED
      move_and_slide()


  func _unhandled_input(event: InputEvent) -> void:
      if event is InputEventMouseMotion:
          rotate_y(-event.relative.x * MOUSE_SENSITIVITY)
          _camera.rotate_x(-event.relative.y * MOUSE_SENSITIVITY)
          _camera.rotation.x = clampf(_camera.rotation.x, -PI / 2.0, PI / 2.0)
      elif event.is_action_pressed("interact"):
          _try_interact()
      elif event.is_action_pressed("ui_cancel"):
          # Toggle mouse capture with Escape
          if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
              Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
          else:
              Input.mouse_mode = Input.MOUSE_MODE_CAPTURED


  func _try_interact() -> void:
      if not _raycast.is_colliding():
          return
      var target := _raycast.get_collider()
      if target != null and target.has_method("interact"):
          target.interact(NetworkManager.my_id)
  ```

- [ ] **Step 2: Open Godot, check for parse errors**

- [ ] **Step 3: Commit**

  ```bash
  git add ruf-der-pilze/scripts/player_controller.gd
  git commit -m "feat: add player_controller — WASD movement, mouse look, interact raycast"
  ```

---

## Task 4: Update tavern.gd _spawn_player() to use player_controller.gd

**Files:**
- Modify: `scripts/tavern.gd` — `_spawn_player()` method only

**Important:** Only replace `_spawn_player()`. Do NOT modify `_ready()`. If this plan is implemented after the Dice Roller plan, `_ready()` will already contain a `_setup_dice_ui()` call that must be preserved.

- [ ] **Step 1: Add INTERACT_DISTANCE constant to tavern.gd**

  At the top of `tavern.gd`, alongside existing constants, add:
  ```gdscript
  const INTERACT_DISTANCE := 2.5
  ```

- [ ] **Step 2: Replace _spawn_player() in tavern.gd**

  Current implementation creates `Node3D.new()`. Replace with:

  ```gdscript
  func _spawn_player(room_index: int) -> void:
      var room_name := "Room%d" % (room_index % ROOM_COUNT + 1)
      var spawn := get_node("UpperFloor/%s/SpawnPoint" % room_name) as Marker3D
      if spawn == null:
          push_error("[Tavern] SpawnPoint nicht gefunden: %s" % room_name)
          return

      var controller := CharacterBody3D.new()
      controller.name = "PlayerController"
      controller.set_script(load("res://scripts/player_controller.gd"))

      # Capsule collision shape for the player body
      var shape := CollisionShape3D.new()
      var capsule := CapsuleShape3D.new()
      capsule.radius = 0.3
      capsule.height = 1.8
      shape.shape = capsule
      shape.position = Vector3(0, 0.9, 0)  # center capsule on feet
      controller.add_child(shape)

      # Camera at eye level
      var camera := Camera3D.new()
      camera.name = "Camera3D"
      camera.current = true
      camera.position = Vector3(0, 1.6, 0)
      controller.add_child(camera)

      # Interaction raycast from camera forward
      var ray := RayCast3D.new()
      ray.name = "InteractRay"
      ray.target_position = Vector3(0, 0, -INTERACT_DISTANCE)
      ray.enabled = true
      camera.add_child(ray)

      add_child(controller)
      controller.global_position = spawn.global_position
      controller.global_rotation = spawn.global_rotation
  ```

- [ ] **Step 3: Verify _broadcast_camera_transform() still works**

  The method calls `get_node_or_null("PlayerController") as Node3D`. `CharacterBody3D` extends `Node3D`, so the cast works. It also calls `controller.get_node_or_null("Camera3D")` — still correct since camera is still named `Camera3D`.

  The camera is now at local position (0, 1.6, 0) inside the controller. The broadcast sends `cam.global_position` / `cam.global_rotation` — correct (global, not local).

- [ ] **Step 4: Open Godot, check for parse errors**

- [ ] **Step 5: Run as player client**

  - WASD moves player
  - Mouse look works
  - Player doesn't fall through floor (capsule collision)
  - Camera is at eye level (~1.6m)

- [ ] **Step 6: Commit**

  ```bash
  git add ruf-der-pilze/scripts/tavern.gd
  git commit -m "feat: tavern — upgrade PlayerController to CharacterBody3D with player_controller.gd"
  ```

---

## Task 5: Add a test interactable to the tavern

Add one interactable object in the tavern (e.g., the bar counter or a crate) to verify the full interaction chain works end-to-end.

**Files:**
- Create: `scripts/test_interactable.gd` — simple interactable that prints a message
- Modify: `scenes/tavern.tscn` — add a StaticBody3D + CollisionShape3D + test_interactable.gd to the taproom area

- [ ] **Step 1: Create scripts/test_interactable.gd**

  ```gdscript
  extends Interactable

  func interact(peer_id: int) -> void:
      var player_name: String = NetworkManager.players.get(peer_id, {}).get("name", "???")
      print("[Interactable] %s hat interagiert (Peer %d)" % [player_name, peer_id])
  ```

  `extends Interactable` works because `interactable.gd` declares `class_name Interactable` (Task 2).

- [ ] **Step 2: Add test object to tavern.tscn via Godot editor**

  In `Tavern/GroundFloor` (or directly under `Tavern`), add:
  ```
  StaticBody3D "TestInteractable"
  ├── CollisionShape3D (BoxShape3D, size 0.5×0.5×0.5)
  ├── MeshInstance3D (BoxMesh, same size — so it's visible)
  └── script = res://scripts/test_interactable.gd
  ```
  Position: somewhere visible in the taproom, e.g. (0, 0.5, -2).

- [ ] **Step 3: Run as player, walk up to test object, press E**

  Expected: console prints `[Interactable] <name> hat interagiert (Peer <id>)`.

- [ ] **Step 4: Commit**

  ```bash
  git add ruf-der-pilze/scripts/test_interactable.gd ruf-der-pilze/scenes/tavern.tscn
  git commit -m "feat: tavern — add test interactable to verify interaction chain"
  ```

---

## Task 6: Update STATUS.md

- [ ] **Step 1: Mark Schritt 9 ✅ in docs/STATUS.md and ruf-der-pilze/CLAUDE.md**

- [ ] **Step 2: Commit**

  ```bash
  git add docs/STATUS.md ruf-der-pilze/CLAUDE.md
  git commit -m "docs: mark Basic Player Interaction complete"
  ```
