OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / liquid_volume.gd

82 lines · gd ref:
class_name LiquidVolume
extends Node3D

## A Quake liquid volume (water / lava / slime) recovered from a .MAP import.
##
## Renders a warped surface (faithful gl_warp.c EmitWaterPolys via liquid_warp.gdshader) at
## the volume's top face using the real WAD liquid texture, and registers its world-space
## AABB in the "liquid_volumes" group so the player can test:
##   • camera-in-liquid  → full-screen content color-shift (V_CalcBlend), and
##   • body-in-liquid    → SV_WaterMove swim physics.
## Non-solid (matches Quake) — no collision body; damage/telefrag are a later session.

const WARP_SHADER: Shader = preload("res://scenes/maps/liquid_warp.gdshader")

var liquid_class: String = "water"      # water | lava | slime | teleport
var aabb_min: Vector3 = Vector3.ZERO
var aabb_max: Vector3 = Vector3.ZERO
var top_y: float = 0.0
var visual_only: bool = false           # true = render warp surface but no swim/blend group (teleport pads)

## Build a LiquidVolume from an importer `liquid_volumes` entry.
##   tex_array   — Texture2DArray of the map's liquid textures (from QuakeWadLoader.load).
##   layer       — this liquid texture's layer index in tex_array.
##   world_scale — meters per Quake unit (same value passed to the importer).
const MIN_HALF := 0.05   # cull degenerate (near-zero-thickness) brush volumes

static func create(entry: Dictionary, tex_array: Texture2DArray, layer: int,
				   world_scale: float, visual_only: bool = false) -> LiquidVolume:
	var half: Vector3 = entry.get("half", Vector3.ONE)
	if half.x < MIN_HALF or half.y < MIN_HALF or half.z < MIN_HALF:
		return null   # degenerate sliver — no swimmable body, skip it
	var lv := LiquidVolume.new()
	lv.visual_only = visual_only
	lv.liquid_class = String(entry.get("class", "water"))
	lv.aabb_min = entry.get("aabb_min", Vector3.ZERO)
	lv.aabb_max = entry.get("aabb_max", Vector3.ZERO)
	lv.top_y = float(entry.get("top_y", 0.0))
	var center: Vector3 = entry.get("center", Vector3.ZERO)
	lv.name = "Liquid_%s" % lv.liquid_class
	lv.position = center

	# Render the whole liquid BOX (all faces, cull_disabled in the shader) so the volume reads
	# with depth from outside and warps around you when submerged — not a floating lid. Inset a
	# hair so faces don't z-fight the solid basin walls the water sits in. The warp is done
	# per-fragment from world position (face-aware), so no subdivision is needed.
	var box := BoxMesh.new()
	box.size = half * 2.0 - Vector3(0.03, 0.03, 0.03)   # half >= MIN_HALF → always positive
	var mi := MeshInstance3D.new()
	mi.name = "Body"
	mi.mesh = box
	var mat := ShaderMaterial.new()
	mat.shader = WARP_SHADER
	if tex_array != null:
		mat.set_shader_parameter("tex", tex_array)
	mat.set_shader_parameter("layer", float(layer))
	mat.set_shader_parameter("world_scale", world_scale)
	mi.material_override = mat
	lv.add_child(mi)
	return lv

func _ready() -> void:
	# Teleport pads render the warp but must NOT be swum in / trigger the screen blend.
	if not visual_only:
		add_to_group("liquid_volumes")

func contains(p: Vector3) -> bool:
	return p.x >= aabb_min.x and p.x <= aabb_max.x \
		and p.y >= aabb_min.y and p.y <= aabb_max.y \
		and p.z >= aabb_min.z and p.z <= aabb_max.z

## The liquid at `point` (highest surface wins on overlap, matching Quake's view-contents
## test), or {} if the point is in no liquid. Returns {class, top_y}.
static func content_at(tree: SceneTree, point: Vector3) -> Dictionary:
	var best: Dictionary = {}
	var best_top: float = -INF
	for n in tree.get_nodes_in_group("liquid_volumes"):
		var lv := n as LiquidVolume
		if lv != null and lv.contains(point) and lv.top_y > best_top:
			best_top = lv.top_y
			best = {"class": lv.liquid_class, "top_y": lv.top_y}
	return best