What makes enemies spawn, stand, and (potentially) fight in a map, and whether a from-scratch "kit" level built on reused floor can have working combat. A combat encounter in a TNC map turns out to be five separate, independently-named entity layers glued together by name reference, not by containment, sitting over a baked navigation mesh that has no author path at all — nothing in this project or the retail engine's own runtime can create or rebuild one. The existing kit map places enemies on validated, reused navmesh with zero spawners and zero cover/patrol graph, which proves "exists and stands on walkable floor," not "spawns" or "fights." Everything below is static file-format research plus one in-game session unrelated to AI (an earlier collision-bug test); no AI combat behavior has been observed running.
The five entity layers
| Class | Role | Key fields |
|---|---|---|
idAI2 |
the enemy body | flags.hide (true = inert until named by a spawner/scene point; false = alive from map load), spawnPosition/spawnOrientation, aiEditable.groupName (ties it to one idEncounterGroupMgr group), aiEditable.roam.targets[0] (its home idScenePoint_Roam) |
idTarget_AISpawn |
the spawner, fired by a trigger/script | targets[] — either idScenePoint_Spawn names or bare idAI2 names directly; optional maxActiveAIs |
idScenePoint_Spawn |
named position + AI template a spawner hands off to | targets[] — the actual idAI2 names placed at this point; selection-arc fields relative to the player |
idEncounterGroupMgr |
one per map; maps a groupName string to an optional alarm |
groups[].{groupName, alarmSystem} — no member list; membership lives only in each AI's own groupName |
idAlarmSystem / idAlarm |
an alarm box group / the physical alarm prop | alarmSpawner names the idTarget_AISpawn used for reinforcements once tripped |
idScenePoint_Combat / Roam / Search / Traversal |
cover/patrol/search/special-movement graph | a separate, finer-grained graph laid directly in world space, independent of the navmesh's face graph; gated per-AI by aiTypeFlags/aiNavsize |
Chain: trigger → script node → idTarget_AISpawn.targets → (scene point or direct) idAI2 → groupName ties it into idEncounterGroupMgr → alarmSystem (if any) → its alarmSpawner handles reinforcements once an idAlarm is tripped.
Known gotcha: retyping a hidden AI's def in place (swap the donor body, keep the entity name) silently drops groupName if the donor body has none. Any recipe that transplants or clones AI must copy aiEditable.groupName (and, for roamers, aiEditable.roam.targets) by hand.
A corpus-wide census across every .entities file in the live install confirms this five-layer pattern is the norm across the large majority of maps, not a one-map curiosity (idScenePoint_Combat alone: 45,090 placements corpus-wide).
Navigation mesh (hknavmesh): reuse-only
One per map by convention (sometimes two — a secondary "combat nav section"), Kraken-compressed inside a small, generic Havok tag-file wrapper. Decoded structure: vertices (flat f32[4*N]), faces ({startEdgeIndex, numEdges}), edges ({a: vertex-index, oppositeFace}). A reader for this already exists in the project's kit-map tooling; no encoder exists anywhere.
A missing navmesh at load only produces a warning, never a load-abort — the engine tolerates a missing navmesh but does not build one. The only runtime mutation is a cutter (triggered by an isNavObstacle = true flag on an entity, 28 retail uses) that can carve a hole out of existing baked navmesh; it cannot add new walkable surface.
Conclusion: new geometry must sit exactly on top of an existing map's baked navmesh footprint. Generating navmesh data is not a viable path with anything in this project or in the retail engine's own behavior.
Kit map state
The existing from-scratch kit level places 9 idAI2 enemies, hide=false, directly on checked-reused navmesh — zero spawners, zero encounter groups/alarms, zero scene points of any kind (the skeleton-build step deletes all 290 AI-class entities in the donor map and never adds any back). This proves "exist and stand on walkable floor," nothing about spawning, patrol, or combat. It has never been confirmed in game for AI — the only recorded in-game session with this kit map was testing an unrelated static-geometry collision bug.
Minimal recipe for a sandbox map with a few enemies
- Build new geometry only where an existing map's navmesh already is, at the same world height — the kit-map tooling's navmesh-check/floor-matching logic already automates this end to end.
- Keep the skeleton minimum (world spawn, player starts) and discard the donor building's props/lights/triggers freely — safe for the one map this was corpus-checked against; the entity-class shape census suggests the same rule set transfers to another base map.
- Two ways to get enemies, increasing in what they prove:
- Cheapest: clone an
idAI2block, any already-mounted AI definition,hide = false, inside the backed navmesh area. Proves only "exists and the navmesh accepts it" — nothing about spawning or combat behavior. - More faithful: instead of deleting a retail room's spawner/group/alarm/scene-point graph, keep it intact and only retype the hidden
idAI2bodies it already wires up ("body transplant": swap the inherited definition, keep name/targets/groupName/roam-point). This reuses an intact, already-proven spawn/combat graph, at the cost of being confined to rooms that still have their old scene points — it does not compose with "build new rooms on reused navmesh footprint."
- Cheapest: clone an
- Do not attempt to author new scene points or a new navmesh — no tool exists for either, and the retail engine has no runtime fallback that creates one.
Failed hypotheses / gotchas
- A bare new map-spec/entity file alone does not make a new map (must go through the proven DLC-folder route).
refchunksis not proven to be an override mechanism.- Not every retail spawner target is actually wired — one
idTarget_AISpawntarget in retail is dead code, a reminder to pick a spawner with a traced trigger when copying one, not any one at random.
Open question
Whether AI placed with hide=false and no scene points actually moves and fights, or just stands motionless — untested in this project.
In-game test (not yet run)
Install the kit level, load it with developer console access, and walk into the room holding 3 of the 9 enemies. Any reaction beyond standing still — turning toward the player, shooting, searching for cover — would be the first runtime evidence that hide=false without scene points is enough for combat behavior; currently unproven in either direction.