From 90bae5f3e54854458d5e13070c71004a18896b22 Mon Sep 17 00:00:00 2001 From: bee Date: Fri, 10 Jul 2026 17:26:23 +0200 Subject: [PATCH] aiming --- boat.tscn | 3 +- docs/adding-content.md | 121 +++++++++++++++++++++ main.tscn | 17 ++- mark12.tscn | 34 +++++- ocean.tscn | 9 ++ project.godot | 2 + resources/ammo/40mm_shell.tres | 12 ++ resources/weapons/bofors.tres | 12 ++ resources/weapons/mark12.tres | 1 + scripts/boat.gd | 22 +++- scripts/player_controller.gd | 41 +++++++ scripts/resource_definitions/weapon_def.gd | 4 + scripts/weapon.gd | 37 +++++++ shell.tscn | 2 +- 14 files changed, 308 insertions(+), 9 deletions(-) create mode 100644 docs/adding-content.md create mode 100644 resources/ammo/40mm_shell.tres create mode 100644 resources/weapons/bofors.tres diff --git a/boat.tscn b/boat.tscn index 67ecc1f..c9902c9 100644 --- a/boat.tscn +++ b/boat.tscn @@ -4,6 +4,7 @@ [ext_resource type="Script" uid="uid://vccbk7tbujhn" path="res://scripts/resource_definitions/weapon_def.gd" id="2_anwow"] [ext_resource type="Resource" uid="uid://cie5thl0u84ly" path="res://resources/weapons/mark12.tres" id="3_henu8"] [ext_resource type="Script" uid="uid://ba5ums3xj42h" path="res://scripts/weapon_slot.gd" id="4_1jsln"] +[ext_resource type="Resource" uid="uid://e80gqhl3f150" path="res://resources/weapons/bofors.tres" id="4_jdgnk"] [sub_resource type="BoxMesh" id="BoxMesh_crate"] size = Vector3(1, 1, 5) @@ -21,7 +22,7 @@ mass = 5.0 script = ExtResource("1_7h74f") loadout = Dictionary[StringName, ExtResource("2_anwow")]({ &"BowMount": ExtResource("3_henu8"), -&"SternMount": ExtResource("3_henu8") +&"SternMount": ExtResource("4_jdgnk") }) buoyancy = 4.0 full_force_depth = 0.8 diff --git a/docs/adding-content.md b/docs/adding-content.md new file mode 100644 index 0000000..738c26f --- /dev/null +++ b/docs/adding-content.md @@ -0,0 +1,121 @@ +# Adding content — quick checklist + +Rough working notes. Verify against the scripts if anything looks off; file +references are the source of truth. + +## 1. Adding a new weapon + +The pipeline is def-driven: `AmmoDef` (what it fires) → `WeaponDef` (the gun) +→ weapon scene (the visible mount). Scenes are shared — `mark12.tscn` is used +by both `mark12.tres` and `bofors.tres`. Only build a new scene for a new look. + +1. **AmmoDef** — new `.tres` in `resources/ammo/` (script: + `scripts/resource_definitions/ammo_def.gd`). Fields: `projectile_scene` + (usually `shell.tscn`), `damage`, `muzzle_speed`, `gravity_scale`, + `lifetime`, `display_name`. +2. **WeaponDef** — new `.tres` in `resources/weapons/` (script: + `scripts/resource_definitions/weapon_def.gd`). Fields: `weapon_scene` + (required — `WeaponSlot.can_mount()` rejects a def without one; reuse + `mark12.tscn` unless you made a new scene), `ammo`, `fire_interval`, + `muzzle_speed_scale` (barrel-length multiplier on ammo speed), `size` / + `type` (slot gating), traverse/elevation speeds and elevation limits. +3. **Weapon scene** (only if it needs a new look) — structure per + `mark12.tscn` / `scripts/weapon.gd`: + - Root `Node3D` with `scripts/weapon.gd`. + - `%Yaw` (Node3D, unique name) — rotates horizontally. **Optional**: omit + it and the weapon is a fixed mount (no aiming, fires along the muzzle). + - `%Pitch` (Node3D, unique name) under Yaw — barrel elevation. Also + optional (yaw-only mount if omitted). + - `%Muzzle` (Marker3D, unique name) at the barrel tip, **-Z pointing out + of the barrel**. **Required** — `weapon.gd` hard-references it. + - `%Hitbox` (AnimatableBody3D, unique name) under Yaw, with a + CollisionShape3D. **Sync To Physics must be UNTICKED** — with it on the + collider does not follow the moving boat. Collision layer = subsystems + (7) only, collision mask = empty. Projectiles get a collision exception + with the shooter's hitboxes at fire time. + - **Leave the root's `def` export EMPTY.** The mounting WeaponSlot assigns + it (`weapon_slot.gd` `mount()`). A scene referencing its own def is + infinite recursion (def → scene → def …). +4. **Put it on a boat** — add an entry to the Boat's `loadout` dictionary + (`scripts/boat.gd`): key = the WeaponSlot node's **exact name** + (StringName), value = the WeaponDef. `Boat._ready()` mounts everything. + The slot's `allowed_sizes` / `allowed_types` must contain the def's + `size` / `type` or `mount()` warns and refuses. + +New projectile scenes: root must use `scripts/projectile.gd` (or replicate +it) — it self-registers into `shift_with_origin` and calls +`reset_physics_interpolation()` on launch. Copy `shell.tscn`'s physics setup: +layer 4 (projectiles), mask = terrain+player+enemies, `continuous_cd` on, +`contact_monitor` on with `max_contacts_reported` ≥ 1. + +## 2. Setting up a boat scene + +Template: `boat.tscn`. + +1. Root: `RigidBody3D` with `scripts/boat.gd` (extends + `scripts/floating_body.gd`). + - Group: `shift_with_origin` (set in the scene). The `boats` group is + joined automatically in `Boat._ready()` — don't add it by hand. + - Collision layer: player (2) or enemies (3). + - Set `mass` — all handling forces scale with it, so handling stays the + same across masses; mass mostly matters for collisions. +2. Children: `MeshInstance3D` + `CollisionShape3D`. **Never scale physics + nodes** — set sizes on the mesh/shape resources themselves. +3. Buoyancy tuning (exports from `floating_body.gd`): + - `buoyancy` (multiple of gravity when fully submerged; >1 floats), + `full_force_depth`, `water_drag`, `water_angular_drag`, `probe_damping`. + - Probe placement: `probe_extents` (half-width, height, half-length) + + `probe_grid` (columns × rows). A grid axis of 1 is valid (centerline). + - **Any Marker3D direct children override the generated grid** and become + the probes — also means: don't park unrelated Marker3Ds directly under + the boat root. +4. Handling exports on `boat.gd`: `engine_power`, `reverse_ratio`, + `throttle_response`, `rudder_strength`, `keel_grip`. +5. Weapon mounts: child `Node3D`s with `scripts/weapon_slot.gd`, positioned + and rotated as the mount points (e.g. stern mount rotated 180°). Set + `allowed_sizes` / `allowed_types` per slot (defaults: MEDIUM, BALLISTIC). +6. `loadout` dict on the root: slot node name → WeaponDef (see section 1). + Keys must match the node names exactly. +7. `weapon_groups`: group id → array of slot names. Group 0 is auto-built in + `_ready()` (all slots) — any editor value for it is ignored. + +## 3. Other things to keep in mind + +- **Tabs, never spaces**, for all GDScript. +- `@export` = static configuration only. Runtime-controlled references are + plain `var` with a `# Controlled by PlayerController` style comment; + internals use `_prefix`. +- Collision layers: 1 terrain, 2 player, 3 enemies, 4 projectiles, + 5 pickups, 6 water, 7 subsystems (see `project.godot` `[layer_names]`). + Water is query-only — surface/contact logic goes through + `Ocean.get_wave_height()`, never a collider (that's how `floating_body.gd` + does buoyancy). +- **Wave math is dual-implemented**: `shaders/ocean.gdshader` (rendering) and + `scripts/ocean.gd` (physics). Any wave change goes in BOTH. Never + `set_shader_parameter` on the water material from gameplay code — use + Ocean's properties. +- **Origin shifting is active.** Free-moving objects (projectiles, debris, + anything not parented to a shifted node) must join `shift_with_origin` — + prefer `add_to_group()` in `_ready()`. Never cache world positions across + frames; for true world-space positions use `WorldManager.true_position()`. +- **Physics interpolation is ON.** Every deliberate teleport needs a + `reset_physics_interpolation()` chaser or it renders as a smear. +- **Boats never read `Input`.** Controllers write intents: + `desired_throttle`, `steer_input`, `fire_intents`, `aim_point`. +- **Inspector-stored-value trap**: once a value is edited in a scene it's + stored in the `.tscn` and overrides any later change to the script's + default. Changed a default and it "doesn't work"? Check the scene file. +- **WeaponSlots are static scene structure** — never added/removed at + runtime. The build-once caches in `Boat._ready()` depend on this. +- Scripts aren't `@tool`, so export setters don't run in the editor — never + trust an exported value to have been validated at edit time. + +--- + +*Notes / discrepancies found while verifying (2026-07):* `%Muzzle` is a hard +requirement of `weapon.gd` (plain `@onready %Muzzle`), unlike `%Yaw` / +`%Pitch` / `%Hitbox` which use `get_node_or_null`. A weapon scene isn't truly +optional for a def — `can_mount()` requires `weapon_scene != null`, so "make +a new scene" is what's optional; the def always points at one. CLAUDE.md's +layer table lists only 1–5; `project.godot` also names 6 water and +7 subsystems (the Hitbox layer value 64 in `mark12.tscn` = layer 7). diff --git a/main.tscn b/main.tscn index 6f7666d..4aba1e1 100644 --- a/main.tscn +++ b/main.tscn @@ -28,6 +28,16 @@ albedo_color = Color(0.76, 0.7, 0.5, 1) [sub_resource type="BoxShape3D" id="BoxShape_seabed"] size = Vector3(200, 1, 200) +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_j2yfa"] +emission_enabled = true +emission = Color(1, 0, 0, 1) +emission_energy_multiplier = 8.57 + +[sub_resource type="SphereMesh" id="SphereMesh_byv2x"] +material = SubResource("StandardMaterial3D_j2yfa") +radius = 0.3 +height = 0.6 + [node name="Main" type="Node3D" unique_id=1242452333] [node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=431656791] @@ -69,9 +79,14 @@ script = ExtResource("6_7mycd") ocean = NodePath("../Ocean") shift_threshold = 50.0 -[node name="PlayerController" type="Node" parent="." unique_id=2011077099 node_paths=PackedStringArray("current_controlled", "world_manager", "camera_rig", "hud")] +[node name="PlayerController" type="Node" parent="." unique_id=2011077099 node_paths=PackedStringArray("current_controlled", "world_manager", "camera_rig", "hud", "aim_marker")] script = ExtResource("6_272bh") current_controlled = NodePath("../Boat") world_manager = NodePath("../WorldManager") camera_rig = NodePath("../CameraRig") hud = NodePath("../Hud") +aim_marker = NodePath("../AimMarker") + +[node name="AimMarker" type="MeshInstance3D" parent="." unique_id=2108290425] +physics_interpolation_mode = 2 +mesh = SubResource("SphereMesh_byv2x") diff --git a/mark12.tscn b/mark12.tscn index 14a4a41..a3648e9 100644 --- a/mark12.tscn +++ b/mark12.tscn @@ -5,12 +5,40 @@ [sub_resource type="BoxMesh" id="BoxMesh_byv2x"] size = Vector3(0.5, 0.5, 0.5) +[sub_resource type="CylinderMesh" id="CylinderMesh_r1f0u"] +top_radius = 0.1 +bottom_radius = 0.1 +height = 0.9 + +[sub_resource type="BoxShape3D" id="BoxShape3D_r1f0u"] +size = Vector3(0.5, 0.5, 0.5) + [node name="Mark12" type="Node3D" unique_id=2057435618] script = ExtResource("1_pko6q") -[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=522024219] +[node name="Yaw" type="Node3D" parent="." unique_id=1821943770] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 0) + +[node name="Housing" type="MeshInstance3D" parent="Yaw" unique_id=522024219] mesh = SubResource("BoxMesh_byv2x") -[node name="Muzzle" type="Marker3D" parent="." unique_id=471595781] +[node name="Pitch" type="Node3D" parent="Yaw" unique_id=967633068] unique_name_in_owner = true -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.2726481) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Yaw/Pitch" unique_id=1633761378] +transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 0, -0.4883741) +mesh = SubResource("CylinderMesh_r1f0u") + +[node name="Muzzle" type="Marker3D" parent="Yaw/Pitch" unique_id=471595781] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.9655868) + +[node name="Hitbox" type="AnimatableBody3D" parent="Yaw" unique_id=719525719] +unique_name_in_owner = true +collision_layer = 64 +collision_mask = 0 +sync_to_physics = false + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Yaw/Hitbox" unique_id=1877916643] +shape = SubResource("BoxShape3D_r1f0u") diff --git a/ocean.tscn b/ocean.tscn index 2b5c2d2..9355965 100644 --- a/ocean.tscn +++ b/ocean.tscn @@ -3,9 +3,18 @@ [ext_resource type="Material" uid="uid://6p5bg1nyso6d" path="res://materials/stylized_water.tres" id="1_j38dg"] [ext_resource type="Script" uid="uid://co5xetl8amg4j" path="res://scripts/ocean.gd" id="2_nb2vx"] +[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_r1f0u"] + [node name="Ocean" type="MeshInstance3D" unique_id=521355215 groups=["follow_focus", "ocean"]] material_override = ExtResource("1_j38dg") script = ExtResource("2_nb2vx") sea_height = 1.0 sea_choppy = 8.0 metadata/follow_step = 1.0 + +[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=1520290282 groups=["water"]] +collision_layer = 32 +collision_mask = 0 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=629201170] +shape = SubResource("WorldBoundaryShape3D_r1f0u") diff --git a/project.godot b/project.godot index 0c1b05d..789bac1 100644 --- a/project.godot +++ b/project.godot @@ -78,6 +78,8 @@ debug_possess_next={ 3d_physics/layer_3="enemies" 3d_physics/layer_4="projectiles" 3d_physics/layer_5="pickups" +3d_physics/layer_6="water" +3d_physics/layer_7="subsystems" [physics] diff --git a/resources/ammo/40mm_shell.tres b/resources/ammo/40mm_shell.tres new file mode 100644 index 0000000..4b5518d --- /dev/null +++ b/resources/ammo/40mm_shell.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="AmmoDef" format=3 uid="uid://cyi8bgkn3wetf"] + +[ext_resource type="PackedScene" uid="uid://3us2hr60a7ke" path="res://shell.tscn" id="1_q4lve"] +[ext_resource type="Script" uid="uid://k4add7rmdxw3" path="res://scripts/resource_definitions/ammo_def.gd" id="1_ti27n"] + +[resource] +script = ExtResource("1_ti27n") +projectile_scene = ExtResource("1_q4lve") +damage = 2.0 +muzzle_speed = 60.0 +gravity_scale = 0.7 +metadata/_custom_type_script = "uid://k4add7rmdxw3" diff --git a/resources/weapons/bofors.tres b/resources/weapons/bofors.tres new file mode 100644 index 0000000..ac7ceba --- /dev/null +++ b/resources/weapons/bofors.tres @@ -0,0 +1,12 @@ +[gd_resource type="Resource" script_class="WeaponDef" format=3 uid="uid://e80gqhl3f150"] + +[ext_resource type="Script" uid="uid://vccbk7tbujhn" path="res://scripts/resource_definitions/weapon_def.gd" id="1_2tugy"] +[ext_resource type="Resource" uid="uid://cyi8bgkn3wetf" path="res://resources/ammo/40mm_shell.tres" id="1_87qo6"] +[ext_resource type="PackedScene" uid="uid://d1fb1p83uhpwv" path="res://mark12.tscn" id="3_x5y71"] + +[resource] +script = ExtResource("1_2tugy") +display_name = "BOFORS" +weapon_scene = ExtResource("3_x5y71") +ammo = ExtResource("1_87qo6") +metadata/_custom_type_script = "uid://vccbk7tbujhn" diff --git a/resources/weapons/mark12.tres b/resources/weapons/mark12.tres index 40f7ae4..41a5401 100644 --- a/resources/weapons/mark12.tres +++ b/resources/weapons/mark12.tres @@ -9,4 +9,5 @@ script = ExtResource("2_q60cw") display_name = "Mark 12" weapon_scene = ExtResource("3_omejh") ammo = ExtResource("1_q60cw") +traverse_speed_deg = 5.0 metadata/_custom_type_script = "uid://vccbk7tbujhn" diff --git a/scripts/boat.gd b/scripts/boat.gd index c456eb9..f0432d7 100644 --- a/scripts/boat.gd +++ b/scripts/boat.gd @@ -10,12 +10,13 @@ extends FloatingBody @export var loadout: Dictionary[StringName, WeaponDef] = {} # WeaponSlot Name -> WeaponDef @export var weapon_groups: Dictionary[int, Array] = {} # group id -> slot names; group 0 is auto-built (all slots), editor value ignored -var desired_throttle := 0.0 # move_toward target for actual throttle -var steer_input := 0.0 -var throttle := 0.0 # current throttle state of ship +var desired_throttle := 0.0 # written by controller, move_toward target for actual throttle +var steer_input := 0.0 # written by controllers +var throttle := 0.0 # current throttle state of ship var _slots: Array[WeaponSlot] = [] var _slots_by_name: Dictionary[StringName, WeaponSlot] = {} var fire_intents: Dictionary[int, bool] = {} # written by controllers, like steer_input +var aim_point = null # Vector3 or null — written by controllers, like steer_input func _ready() -> void: super() @@ -38,8 +39,11 @@ func _ready() -> void: func _physics_process(delta: float) -> void: super(delta) + # Propulsion throttle = move_toward(throttle, desired_throttle, throttle_response * delta) + # Weapons + _process_aim(delta) _process_fire() if submerged_ratio <= 0.0: @@ -74,3 +78,15 @@ func _process_fire() -> void: var slot: WeaponSlot = _slots_by_name.get(slot_name) if slot != null and slot.weapon != null: slot.weapon.try_fire() + +func _process_aim(delta: float) -> void: + for slot in _slots: + if slot.weapon != null: + slot.weapon.aim_at(aim_point, delta) + +func aim_ray_exclusions() -> Array[RID]: + var rids: Array[RID] = [get_rid()] + for slot in _slots: + if slot.weapon != null and slot.weapon.hitbox != null: + rids.append(slot.weapon.hitbox.get_rid()) + return rids diff --git a/scripts/player_controller.gd b/scripts/player_controller.gd index 757d0cc..0874c18 100644 --- a/scripts/player_controller.gd +++ b/scripts/player_controller.gd @@ -7,6 +7,7 @@ extends Node current_controlled.desired_throttle = 0.0 current_controlled.steer_input = 0.0 current_controlled.fire_intents.clear() + current_controlled.aim_point = null current_controlled = value if is_node_ready(): if current_controlled != null: @@ -32,6 +33,9 @@ extends Node _possess() @export var throttle_ramp := 0.5 +@export var aim_ray_length := 2000.0 +@export_flags_3d_physics var aim_collision_mask := 0b1100111 # terrain, player, enemies, water, subsystems +@export var aim_marker: Node3D var desired_throttle := 0.0 @@ -41,6 +45,18 @@ func _ready() -> void: desired_throttle = current_controlled.throttle _possess() +func _physics_process(_delta: float) -> void: + if current_controlled == null: + if aim_marker != null: + aim_marker.visible = false + return + current_controlled.aim_point = _compute_aim_point() + if aim_marker != null: + var p = current_controlled.aim_point + aim_marker.visible = p != null + if p != null: + aim_marker.global_position = p + func _process(delta: float) -> void: if current_controlled == null: return @@ -78,3 +94,28 @@ func _possess_next() -> void: return var idx := boats.find(current_controlled) current_controlled = boats[(idx + 1) % boats.size()] + +func _compute_aim_point(): + if camera_rig == null or camera_rig.cam == null: + return null + var cam := camera_rig.cam + var mouse := get_viewport().get_mouse_position() + var from := cam.project_ray_origin(mouse) + var dir := cam.project_ray_normal(mouse) + + var query := PhysicsRayQueryParameters3D.create( + from, from + dir * aim_ray_length, + aim_collision_mask, current_controlled.aim_ray_exclusions() + ) + var hit := current_controlled.get_world_3d().direct_space_state.intersect_ray(query) + if hit.is_empty(): + return from + dir * aim_ray_length # above the horizon + + if hit.collider.is_in_group(&"water"): + # Flat-plane hit → refine to the actual wave surface. + var ocean := get_tree().get_first_node_in_group(&"ocean") as Ocean + if ocean != null: + var refined = Plane(Vector3.UP, ocean.get_wave_height(hit.position)).intersects_ray(from, dir) + if refined != null: + return refined + return hit.position diff --git a/scripts/resource_definitions/weapon_def.gd b/scripts/resource_definitions/weapon_def.gd index 5b92423..094d0ca 100644 --- a/scripts/resource_definitions/weapon_def.gd +++ b/scripts/resource_definitions/weapon_def.gd @@ -13,3 +13,7 @@ enum Type {BALLISTIC, ENERGY, MISSILE, TORPEDO} @export var muzzle_speed_scale := 1.0 # multiplier over ammo.muzzle_speed (barrel length) @export var size := Size.MEDIUM @export var type := Type.BALLISTIC +@export var traverse_speed_deg := 30.0 # yaw slew rate, degrees/second +@export var elevation_speed_deg := 20.0 # pitch slew rate, degrees/second +@export var elevation_min_deg := -10.0 # how far the barrel can depress +@export var elevation_max_deg := 60.0 # how far it can elevate diff --git a/scripts/weapon.gd b/scripts/weapon.gd index 8b9d893..c45a730 100644 --- a/scripts/weapon.gd +++ b/scripts/weapon.gd @@ -8,6 +8,9 @@ var shooter: PhysicsBody3D # Resolved in _ready(): nearest ancestor body var _cooldown := 0.0 @onready var _muzzle: Marker3D = %Muzzle +@onready var _yaw: Node3D = get_node_or_null("%Yaw") +@onready var _pitch: Node3D = get_node_or_null("%Pitch") +@onready var hitbox: PhysicsBody3D = get_node_or_null("%Hitbox") func _ready() -> void: # Fallback: nearest ancestor body is the shooter (hand-placed weapons). @@ -27,5 +30,39 @@ func try_fire() -> bool: get_tree().current_scene.add_child(p) if shooter != null: p.add_collision_exception_with(shooter) + if hitbox != null: + p.add_collision_exception_with(hitbox) p.launch(def.ammo, _muzzle.global_transform, def.muzzle_speed_scale) return true + +func aim_at(point, delta: float) -> void: + if _yaw == null or def == null: + return + + var yaw_step := deg_to_rad(def.traverse_speed_deg) * delta + var pitch_step := deg_to_rad(def.elevation_speed_deg) * delta + + if point == null: + # Nobody aiming: slew back to the stowed position. + _yaw.rotation.y = rotate_toward(_yaw.rotation.y, 0.0, yaw_step) + if _pitch != null: + _pitch.rotation.x = rotate_toward(_pitch.rotation.x, 0.0, pitch_step) + return + + # Yaw: desired angle lives in the space of the node we rotate *within*, + # i.e. the Yaw node's parent. + var local: Vector3 = _yaw.get_parent_node_3d().to_local(point) + var desired_yaw := atan2(-local.x, -local.z) + _yaw.rotation.y = rotate_toward(_yaw.rotation.y, desired_yaw, yaw_step) + + if _pitch == null: + return + + # Pitch: same idea, in the Pitch node's parent space (the freshly-yawed Yaw node). + var lp: Vector3 = _pitch.get_parent_node_3d().to_local(point) + var desired_pitch := clampf( + atan2(lp.y, Vector2(lp.x, lp.z).length()), + deg_to_rad(def.elevation_min_deg), + deg_to_rad(def.elevation_max_deg) + ) + _pitch.rotation.x = rotate_toward(_pitch.rotation.x, desired_pitch, pitch_step) diff --git a/shell.tscn b/shell.tscn index 9918ae8..cd9794e 100644 --- a/shell.tscn +++ b/shell.tscn @@ -12,7 +12,7 @@ height = 0.5 radius = 0.1 [node name="Shell" type="RigidBody3D" unique_id=893187332] -collision_layer = 8 +collision_layer = 72 collision_mask = 7 continuous_cd = true contact_monitor = true