Properties on brushes

A room has things to say beyond its shape: what its floor is made of, how it sounds, how dear it is to cross. Core does not know any of that, and should not. An addon or a game declares the properties it needs, and the inspector shows them on exactly the brushes they apply to.

Derived things - rooms, portals, walls - are replaced every time the level compiles, so anything an author sets has to live on something authored: a brush, a face, or an entity.

1. Declare

A HammeriteBrushPropertySchema is one section of the inspector: a namespace, a label, which brushes it applies to, and its properties with their defaults.

var stealth := HammeriteBrushPropertySchema.new(&"stealth", "Stealth")
stealth.tier = HammeriteBrushPropertySchema.Tier.AIR
stealth.structural_only = true
stealth.add(&"floor", "Floor", "stone").set_tooltip("What footsteps on it sound like")
stealth.add(&"cover", "Cover", 0.0).set_range(0.0, 1.0, 0.05)
HammeriteBrushPropertyRegistry.register(stealth)
  • The tier filter says where it appears: AIR for rooms, SOLID for walls, ANY for both. structural_only leaves out detail brushwork - a schema about a room wants it.
  • The default decides the type, and so the control: a float makes a spin box, a string a text field, a bool a check box. add_enum() makes a list.
  • The stored key is <namespace>/<name>: stealth/floor.

Register from the running game - an autoload's _ready() - before a map is built, not only when the editor is up. The declared default is what an untouched brush reads, and a shipped game without the registration reads null.

2. Read

Storage is sparse: a brush holds only the values an author changed, and setting one back to its default erases it. So read through the brush, which falls back to the declared default:

var floor_kind: String = brush.get_property(&"stealth/floor")

never brush.properties["stealth/floor"], which is null on every brush nobody touched.

A room's properties are those of the air brush that carved it:

var cell: HammeriteCell = map.point_in_cell(map_3d.to_local(player.global_position + Vector3.UP * 32.0))
if cell:
    var cover: float = map.get_cell_property(cell, &"stealth/cover")

A change is announced on the brush (property_changed) and the map (brush_property_changed). It does not recompile the world: a property is data on the room, not part of its shape.

A side effect worth knowing: changing a declared default changes it on every map that never set the property. That is the trade sparse storage makes - choose defaults you will not want to move.

3. On a face

HammeriteFace.properties is the same kind of bag, with no registry and no inspector: the reader supplies the default. It is where a value lives that belongs to one wall, or that must outlast something derived from it - a doorway's name is stored on the faces it is punched from, because the portal itself is replaced on every compile.

face.set_property(&"stealth/creaks", true)
var creaks: bool = face.get_property(&"stealth/creaks", false)

4. In the editor

A property whose legal values are a list the game knows - patrol groups, surface kinds - gets a dropdown:

map_editor.register_brush_property_choices(&"stealth/floor",
    func(_map: HammeriteMap) -> PackedStringArray: return PackedStringArray(["stone", "wood", "carpet", "gravel"]))

A control of your own replaces the built-in one with register_brush_property_editor(key, factory), whose factory returns a HammeritePropertyControl.

To see a property across the whole level, colour the 2D floorplan by it. An overlay is offered under Cells: on the toolbar; a fully transparent colour leaves a room plain:

map_editor.register_cell_overlay(&"stealth_cover", "Cover", _cover_colour)

func _cover_colour(cell: HammeriteCell, map: HammeriteMap) -> Color:
    var cover: float = map.get_cell_property(cell, &"stealth/cover")
    return Color(0.2, 0.3, 0.8, 0.6 * cover)

map_editor is the HammeriteMapEditor that HammeriteEditor.integrate() builds.

See also

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