class_name QuakeEntities
extends RefCounted
## Shared Quake entity spawners: liquids, teleporters and func_door movers.
##
## The importer already recovers all of this (liquid_volumes, teleport_triggers /
## teleport_destinations / teleport_volumes, doors) — it is only the spawning that
## used to live as private methods on map_dc_terrain.gd, so map_dc_level got none of
## it. That is the same gap rtlights had: the data was parsed, the spawner existed,
## nobody called it.
##
## Static, taking an explicit `parent`, so any map can use them. map_dc_terrain still
## has its own copies and is deliberately NOT touched here — it works, and rewriting a
## danger-flagged file to de-duplicate is a separate change with its own risk.
## Migrating it onto these is the obvious follow-up.
const WORLD_SCALE := 0.028
## Water / lava / slime. Non-solid volumes: a warped surface at the top face plus a
## registered AABB the player uses for the screen blend and swimming.
static func spawn_liquids(parent: Node3D, importer: QuakeMapImporter,
wad_path: String, pal_path: String) -> int:
var existing := parent.get_node_or_null("QuakeLiquids")
if existing != null:
existing.free()
if importer.liquid_volumes.is_empty():
return 0
var names := {}
for e in importer.liquid_volumes:
names[String(e.get("tex", "")).to_lower()] = true
var res := QuakeWadLoader.load(wad_path, pal_path, PackedStringArray(names.keys()), 64)
var arr: Texture2DArray = res.get("array", null)
var layers: Dictionary = res.get("layers", {})
var container := Node3D.new()
container.name = "QuakeLiquids"
parent.add_child(container)
var spawned := 0
for e in importer.liquid_volumes:
var layer: int = int(layers.get(String(e.get("tex", "")).to_lower(), 0))
var lv := LiquidVolume.create(e, arr, layer, WORLD_SCALE)
if lv != null: # create() returns null for degenerate slivers
container.add_child(lv)
spawned += 1
return spawned
## trigger_teleport touch volumes wired to their info_teleport_destination, plus the
## *teleport swirly pads as visual-only volumes (rendered, not swimmable).
static func spawn_teleporters(parent: Node3D, importer: QuakeMapImporter,
wad_path: String, pal_path: String) -> Array:
var existing := parent.get_node_or_null("QuakeTeleporters")
if existing != null:
existing.free()
var container := Node3D.new()
container.name = "QuakeTeleporters"
parent.add_child(container)
var wired := 0
var dests: Dictionary = importer.teleport_destinations
for trig in importer.teleport_triggers:
var tgt := String(trig.get("target", ""))
if not dests.has(tgt):
continue # changelevel-style target with no local destination
container.add_child(TeleportTrigger.create(trig, dests[tgt]))
wired += 1
var pads := 0
if not importer.teleport_volumes.is_empty():
var names := {}
for e in importer.teleport_volumes:
names[String(e.get("tex", "")).to_lower()] = true
var res := QuakeWadLoader.load(wad_path, pal_path, PackedStringArray(names.keys()), 64)
var arr: Texture2DArray = res.get("array", null)
var layers: Dictionary = res.get("layers", {})
for e in importer.teleport_volumes:
var layer: int = int(layers.get(String(e.get("tex", "")).to_lower(), 0))
var lv := LiquidVolume.create(e, arr, layer, WORLD_SCALE, true) # visual_only
if lv != null:
container.add_child(lv)
pads += 1
return [wired, pads, dests.size()]
## func_door movers — animated AnimatableBody3D, deliberately excluded from the brush
## stamp by the importer. `locked` is already computed there from spawnflags 8/16
## (gold/silver key) or the presence of a targetname; there are no keys or buttons yet,
## so those stay shut, which is Quake's own default behaviour.
static func spawn_doors(parent: Node3D, importer: QuakeMapImporter,
wad_path: String, pal_path: String) -> int:
var existing := parent.get_node_or_null("QuakeDoors")
if existing != null:
existing.free()
if importer.doors.is_empty():
return 0
var names := {}
for d in importer.doors:
names[String(d.get("tex", "")).to_lower()] = true
var res := QuakeWadLoader.load(wad_path, pal_path, PackedStringArray(names.keys()), 64)
var arr: Texture2DArray = res.get("array", null)
var layers: Dictionary = res.get("layers", {})
var container := Node3D.new()
container.name = "QuakeDoors"
parent.add_child(container)
for d in importer.doors:
var tex: Texture2D = null
if arr != null:
var layer: int = int(layers.get(String(d.get("tex", "")).to_lower(), 0))
var img := arr.get_layer_data(layer)
if img != null:
tex = ImageTexture.create_from_image(img)
container.add_child(DoorMover.create(d, tex))
return importer.doors.size()