Files
boatgame/docs/adding-content.md
T
2026-07-10 17:26:23 +02:00

122 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 15; `project.godot` also names 6 water and
7 subsystems (the Hitbox layer value 64 in `mark12.tscn` = layer 7).