HammeriteNavigationMeshWriter

Navigation. Inherits RefCounted

Turns measured ground into Godot navigation meshes, one per room.

The polygons are constructed directly rather than baked. Godot's baker voxelises source geometry and rebuilds the walkable surface from the voxels, which is the right thing to do when all you have is triangles - it costs a cell size to choose, artefacts at that resolution, and thin ledges that quietly vanish. None of that applies here: the compiler has already decided what the ground is and cut it convex, so the polygons go straight across. One mesh per room, because the world is already chunked into rooms and their seams are the portals. Rebuilding is whole-level, though: the chunking is there so that layers and costs can differ room by room, not so that an edit can rebake one room. That was the intention once, and it is not what the code does - and the numbers say it should not be. Rebuilding everything costs about 205 ms on a 256-room level against the four and a half seconds the recompile that triggered it already spent, so machinery to rebake one room would save four per cent of a cost paid elsewhere.

Methods

ReturnsMethod
Dictionary[Vector4i, NavigationMesh]build_region_meshes(surfaces: HammeriteNavigationSurface[], profiles: HammeriteNavigationAgentProfiles = null) static
HammeriteNavigationSurface[]standing_ground(surfaces: HammeriteNavigationSurface[], profiles: HammeriteNavigationAgentProfiles = null) static
HammeriteNavigationLinkData[]settle_links(links: HammeriteNavigationLinkData[], meshes: Dictionary[Vector4i, NavigationMesh], reach: float) static
NavigationRegion3D[]attach_regions(parent: Node, meshes: Dictionary[Vector4i, NavigationMesh]) static
NavigationLink3D[]attach_links(parent: Node, links: HammeriteNavigationLinkData[], profiles: HammeriteNavigationAgentProfiles = null) static
intset_links_enabled_by_name(parent: Node, link_name: String, enabled: bool) static

Constants

DEFAULT_LAYERS

const DEFAULT_LAYERS = 1

Layer mask used when no profile table was given: bit 0, the navigation server's own default.

const LINK_NAME_META = &"hammerite_nav_link_name"

Metadata key carrying a link's authored name on the emitted node, so it can be found again.

Method descriptions

build_region_meshes()

static func build_region_meshes(surfaces: HammeriteNavigationSurface[], profiles: HammeriteNavigationAgentProfiles = null) -> Dictionary[Vector4i, NavigationMesh]

Group surfaces into navigation meshes, keyed by Vector4i(cell id, region, layer mask, travel cost x100) - one per room per set of agents that use that ground the same way. Usually that is one mesh per room, since usually every agent can walk everywhere in a room and thinks the same of it. It is more when they differ, because Godot puts navigation layers and travel cost on the REGION rather than on the polygon: ground only a rat fits down has to be its own region to carry only the rat's bit, and a crawlspace that costs more to cross needs its own to carry that. What it never does is emit the same ground twice: two regions occupying one piece of floor collide when the navigation map is synchronised, and the connections to the rest of the level break - measured, not assumed. See HammeriteNavigationAgentProfiles.layers_and_cost() for what that forces where agents disagree about a price. Ground no profile can use at all is left out entirely: a vent nothing in this game fits down is not a navigation mesh with nobody on it, it is not ground.

standing_ground()

static func standing_ground(surfaces: HammeriteNavigationSurface[], profiles: HammeriteNavigationAgentProfiles = null) -> HammeriteNavigationSurface[]

Where the middle of a body can be: surfaces drawn back from the edge of the floor by half the width of the narrowest agent that can stand on each, and cut convex again. A navigation mesh is where an agent's middle goes, not where its feet go. Drawn as the floor is, it runs right up to the walls: a path hugs every wall it passes and turns every corner on the corner itself, and a body following it has half of itself in the rock. Drawn back by half a body, a path keeps that far from the walls, and a gap narrower than a body is not ground at all - two rooms that meet at a corner are no longer joined through the corner. What it is drawn back from is the edge of the floor a body can be over: a wall, a ledge, the foot of a crate, a ceiling too low. A strip too narrow to stand on is not an edge, since a body standing beside it overhangs it, and nor is the seam where one patch or one room meets the next, so a doorway joins the rooms either side of it as before, a body's width narrower. Ground every agent shares is drawn for the narrowest of them, so that it serves the smallest too: in a table whose agents differ in width, the larger ones come that much nearer the walls than half their own width. Godot's answer to agents of different sizes is a navigation map each, and this is not that.

static func settle_links(links: HammeriteNavigationLinkData[], meshes: Dictionary[Vector4i, NavigationMesh], reach: float) -> HammeriteNavigationLinkData[]

links with each end moved onto the nearest ground of meshes in the room it is in, and without those that have none within reach. The navigation server joins a link to the ground only where an end lies within a unit of it. A link is found where a floor ends - over a ledge, through a window - and the ground has been drawn back from there, so unmoved it would join nothing, and nothing would say so.

attach_regions()

static func attach_regions(parent: Node, meshes: Dictionary[Vector4i, NavigationMesh]) -> NavigationRegion3D[]

Build a NavigationRegion3D per chunk under parent, replacing any it made before and nothing else. Named by room, mask and cost, so a region in the remote debugger says which room it is, who it is for, and what it costs them.

static func attach_links(parent: Node, links: HammeriteNavigationLinkData[], profiles: HammeriteNavigationAgentProfiles = null) -> NavigationLink3D[]

Build the NavigationLink3Ds for links under parent, replacing any it made before and nothing else. A link is not one node per link, because a NavigationLink3D carries one direction and one cost and the agents do not agree about either. A guard walks off the balcony and cannot climb back up while something nimbler uses it both ways; and dropping down is quick where hauling yourself up is slow, so even an agent that can do both does not value them alike. So the profiles are grouped by what they can do with this link AND what it costs them, and each group gets its own node carrying its own layers. Usually that is one node; where it is three, the three say something true that one could not. An asymmetric link is emitted as a PAIR of one-way nodes rather than one bidirectional node, because NavigationLink3D.enter_cost is charged whichever way the link is entered - one node cannot be cheap downhill and dear uphill. Two can. A link nothing can take is not emitted at all.

static func set_links_enabled_by_name(parent: Node, link_name: String, enabled: bool) -> int

Switch every link named link_name under parent on or off, and report how many there were. This is what a name is FOR: boards come off a window and the route through it opens, a drawbridge lowers, a grate is unlocked. The name is the aperture's own (HammeritePortal.get_portal_name()) - the same string the acoustics use - so a shutter entity that names its opening once addresses both what can be heard through it and what can be climbed through it.

Hammerite 1.0 · built from a6dd634, 2026-10-10