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

6.9 KiB
Raw Blame History

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. Requiredweapon.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 Node3Ds 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).