aiming
This commit is contained in:
@@ -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="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="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="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"]
|
[sub_resource type="BoxMesh" id="BoxMesh_crate"]
|
||||||
size = Vector3(1, 1, 5)
|
size = Vector3(1, 1, 5)
|
||||||
@@ -21,7 +22,7 @@ mass = 5.0
|
|||||||
script = ExtResource("1_7h74f")
|
script = ExtResource("1_7h74f")
|
||||||
loadout = Dictionary[StringName, ExtResource("2_anwow")]({
|
loadout = Dictionary[StringName, ExtResource("2_anwow")]({
|
||||||
&"BowMount": ExtResource("3_henu8"),
|
&"BowMount": ExtResource("3_henu8"),
|
||||||
&"SternMount": ExtResource("3_henu8")
|
&"SternMount": ExtResource("4_jdgnk")
|
||||||
})
|
})
|
||||||
buoyancy = 4.0
|
buoyancy = 4.0
|
||||||
full_force_depth = 0.8
|
full_force_depth = 0.8
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -28,6 +28,16 @@ albedo_color = Color(0.76, 0.7, 0.5, 1)
|
|||||||
[sub_resource type="BoxShape3D" id="BoxShape_seabed"]
|
[sub_resource type="BoxShape3D" id="BoxShape_seabed"]
|
||||||
size = Vector3(200, 1, 200)
|
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="Main" type="Node3D" unique_id=1242452333]
|
||||||
|
|
||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=431656791]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=431656791]
|
||||||
@@ -69,9 +79,14 @@ script = ExtResource("6_7mycd")
|
|||||||
ocean = NodePath("../Ocean")
|
ocean = NodePath("../Ocean")
|
||||||
shift_threshold = 50.0
|
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")
|
script = ExtResource("6_272bh")
|
||||||
current_controlled = NodePath("../Boat")
|
current_controlled = NodePath("../Boat")
|
||||||
world_manager = NodePath("../WorldManager")
|
world_manager = NodePath("../WorldManager")
|
||||||
camera_rig = NodePath("../CameraRig")
|
camera_rig = NodePath("../CameraRig")
|
||||||
hud = NodePath("../Hud")
|
hud = NodePath("../Hud")
|
||||||
|
aim_marker = NodePath("../AimMarker")
|
||||||
|
|
||||||
|
[node name="AimMarker" type="MeshInstance3D" parent="." unique_id=2108290425]
|
||||||
|
physics_interpolation_mode = 2
|
||||||
|
mesh = SubResource("SphereMesh_byv2x")
|
||||||
|
|||||||
+31
-3
@@ -5,12 +5,40 @@
|
|||||||
[sub_resource type="BoxMesh" id="BoxMesh_byv2x"]
|
[sub_resource type="BoxMesh" id="BoxMesh_byv2x"]
|
||||||
size = Vector3(0.5, 0.5, 0.5)
|
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]
|
[node name="Mark12" type="Node3D" unique_id=2057435618]
|
||||||
script = ExtResource("1_pko6q")
|
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")
|
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
|
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")
|
||||||
|
|||||||
@@ -3,9 +3,18 @@
|
|||||||
[ext_resource type="Material" uid="uid://6p5bg1nyso6d" path="res://materials/stylized_water.tres" id="1_j38dg"]
|
[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"]
|
[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"]]
|
[node name="Ocean" type="MeshInstance3D" unique_id=521355215 groups=["follow_focus", "ocean"]]
|
||||||
material_override = ExtResource("1_j38dg")
|
material_override = ExtResource("1_j38dg")
|
||||||
script = ExtResource("2_nb2vx")
|
script = ExtResource("2_nb2vx")
|
||||||
sea_height = 1.0
|
sea_height = 1.0
|
||||||
sea_choppy = 8.0
|
sea_choppy = 8.0
|
||||||
metadata/follow_step = 1.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")
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ debug_possess_next={
|
|||||||
3d_physics/layer_3="enemies"
|
3d_physics/layer_3="enemies"
|
||||||
3d_physics/layer_4="projectiles"
|
3d_physics/layer_4="projectiles"
|
||||||
3d_physics/layer_5="pickups"
|
3d_physics/layer_5="pickups"
|
||||||
|
3d_physics/layer_6="water"
|
||||||
|
3d_physics/layer_7="subsystems"
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -9,4 +9,5 @@ script = ExtResource("2_q60cw")
|
|||||||
display_name = "Mark 12"
|
display_name = "Mark 12"
|
||||||
weapon_scene = ExtResource("3_omejh")
|
weapon_scene = ExtResource("3_omejh")
|
||||||
ammo = ExtResource("1_q60cw")
|
ammo = ExtResource("1_q60cw")
|
||||||
|
traverse_speed_deg = 5.0
|
||||||
metadata/_custom_type_script = "uid://vccbk7tbujhn"
|
metadata/_custom_type_script = "uid://vccbk7tbujhn"
|
||||||
|
|||||||
+19
-3
@@ -10,12 +10,13 @@ extends FloatingBody
|
|||||||
@export var loadout: Dictionary[StringName, WeaponDef] = {} # WeaponSlot Name -> WeaponDef
|
@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
|
@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 desired_throttle := 0.0 # written by controller, move_toward target for actual throttle
|
||||||
var steer_input := 0.0
|
var steer_input := 0.0 # written by controllers
|
||||||
var throttle := 0.0 # current throttle state of ship
|
var throttle := 0.0 # current throttle state of ship
|
||||||
var _slots: Array[WeaponSlot] = []
|
var _slots: Array[WeaponSlot] = []
|
||||||
var _slots_by_name: Dictionary[StringName, WeaponSlot] = {}
|
var _slots_by_name: Dictionary[StringName, WeaponSlot] = {}
|
||||||
var fire_intents: Dictionary[int, bool] = {} # written by controllers, like steer_input
|
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:
|
func _ready() -> void:
|
||||||
super()
|
super()
|
||||||
@@ -38,8 +39,11 @@ func _ready() -> void:
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
super(delta)
|
super(delta)
|
||||||
|
|
||||||
|
# Propulsion
|
||||||
throttle = move_toward(throttle, desired_throttle, throttle_response * delta)
|
throttle = move_toward(throttle, desired_throttle, throttle_response * delta)
|
||||||
|
|
||||||
|
# Weapons
|
||||||
|
_process_aim(delta)
|
||||||
_process_fire()
|
_process_fire()
|
||||||
|
|
||||||
if submerged_ratio <= 0.0:
|
if submerged_ratio <= 0.0:
|
||||||
@@ -74,3 +78,15 @@ func _process_fire() -> void:
|
|||||||
var slot: WeaponSlot = _slots_by_name.get(slot_name)
|
var slot: WeaponSlot = _slots_by_name.get(slot_name)
|
||||||
if slot != null and slot.weapon != null:
|
if slot != null and slot.weapon != null:
|
||||||
slot.weapon.try_fire()
|
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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ extends Node
|
|||||||
current_controlled.desired_throttle = 0.0
|
current_controlled.desired_throttle = 0.0
|
||||||
current_controlled.steer_input = 0.0
|
current_controlled.steer_input = 0.0
|
||||||
current_controlled.fire_intents.clear()
|
current_controlled.fire_intents.clear()
|
||||||
|
current_controlled.aim_point = null
|
||||||
current_controlled = value
|
current_controlled = value
|
||||||
if is_node_ready():
|
if is_node_ready():
|
||||||
if current_controlled != null:
|
if current_controlled != null:
|
||||||
@@ -32,6 +33,9 @@ extends Node
|
|||||||
_possess()
|
_possess()
|
||||||
|
|
||||||
@export var throttle_ramp := 0.5
|
@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
|
var desired_throttle := 0.0
|
||||||
|
|
||||||
@@ -41,6 +45,18 @@ func _ready() -> void:
|
|||||||
desired_throttle = current_controlled.throttle
|
desired_throttle = current_controlled.throttle
|
||||||
_possess()
|
_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:
|
func _process(delta: float) -> void:
|
||||||
if current_controlled == null:
|
if current_controlled == null:
|
||||||
return
|
return
|
||||||
@@ -78,3 +94,28 @@ func _possess_next() -> void:
|
|||||||
return
|
return
|
||||||
var idx := boats.find(current_controlled)
|
var idx := boats.find(current_controlled)
|
||||||
current_controlled = boats[(idx + 1) % boats.size()]
|
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
|
||||||
|
|||||||
@@ -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 muzzle_speed_scale := 1.0 # multiplier over ammo.muzzle_speed (barrel length)
|
||||||
@export var size := Size.MEDIUM
|
@export var size := Size.MEDIUM
|
||||||
@export var type := Type.BALLISTIC
|
@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
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ var shooter: PhysicsBody3D # Resolved in _ready(): nearest ancestor body
|
|||||||
var _cooldown := 0.0
|
var _cooldown := 0.0
|
||||||
|
|
||||||
@onready var _muzzle: Marker3D = %Muzzle
|
@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:
|
func _ready() -> void:
|
||||||
# Fallback: nearest ancestor body is the shooter (hand-placed weapons).
|
# 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)
|
get_tree().current_scene.add_child(p)
|
||||||
if shooter != null:
|
if shooter != null:
|
||||||
p.add_collision_exception_with(shooter)
|
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)
|
p.launch(def.ammo, _muzzle.global_transform, def.muzzle_speed_scale)
|
||||||
return true
|
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)
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ height = 0.5
|
|||||||
radius = 0.1
|
radius = 0.1
|
||||||
|
|
||||||
[node name="Shell" type="RigidBody3D" unique_id=893187332]
|
[node name="Shell" type="RigidBody3D" unique_id=893187332]
|
||||||
collision_layer = 8
|
collision_layer = 72
|
||||||
collision_mask = 7
|
collision_mask = 7
|
||||||
continuous_cd = true
|
continuous_cd = true
|
||||||
contact_monitor = true
|
contact_monitor = true
|
||||||
|
|||||||
Reference in New Issue
Block a user