// Copyright (C) 2007 Id Software, Inc.
//

#include "BSE_Envelope.h"
#include "BSE_Particle.h"
#include "BSE_SpawnDomains.h"
#include "BSE.h"

#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include "../framework/Session.h"

struct SElecWork {
	srfTriangles_t* tri;
	idVec3* coords;
	int coordCount;
	idVec3 length;
	idVec3 forward;
	idVec3 viewPos;
	idVec4 tint;
	float size;
	float alpha;
	float step;
	float fraction;
};

namespace {
ID_INLINE int BSE_GetFrameCounterMode() {
	return cvarSystem ? cvarSystem->GetCVarInteger("bse_frameCounters") : 0;
}

ID_INLINE float Clamp01(float x) {
	return idMath::ClampFloat(0.0f, 1.0f, x);
}

ID_INLINE byte ToByte(float x) {
	return static_cast<byte>(idMath::ClampInt(0, 255, idMath::FtoiFast(Clamp01(x) * 255.0f)));
}

ID_INLINE dword PackColorLocal(const idVec4& color) {
	const dword r = static_cast<dword>(ToByte(color[0]));
	const dword g = static_cast<dword>(ToByte(color[1])) << 8;
	const dword b = static_cast<dword>(ToByte(color[2])) << 16;
	const dword a = static_cast<dword>(ToByte(color[3])) << 24;
	return r | g | b | a;
}

ID_INLINE void UnpackColor(dword rgba, byte out[4]) {
	out[0] = static_cast<byte>(rgba & 0xFF);
	out[1] = static_cast<byte>((rgba >> 8) & 0xFF);
	out[2] = static_cast<byte>((rgba >> 16) & 0xFF);
	out[3] = static_cast<byte>((rgba >> 24) & 0xFF);
}

ID_INLINE void SetDrawVert(idDrawVert& vert, const idVec3& xyz, float s, float t, dword rgba, const idVec3* normal = NULL) {
	byte color[4];
	UnpackColor(rgba, color);

	vert.Clear();
	vert.xyz = xyz;
	vert.st[0] = s;
	vert.st[1] = t;
	vert.normal = normal ? *normal : idVec3(1.0f, 0.0f, 0.0f);
	vert.tangents[0].Set(0.0f, 1.0f, 0.0f);
	vert.tangents[1].Set(0.0f, 0.0f, 1.0f);
	vert.color[0] = color[0];
	vert.color[1] = color[1];
	vert.color[2] = color[2];
	vert.color[3] = color[3];
}

ID_INLINE bool HasTriCapacity(const srfTriangles_t* tri, int addVerts, int addIndexes) {
	if (!tri || addVerts < 0 || addIndexes < 0) {
		return false;
	}
	if (tri->numAllocedVerts > 0 && tri->numVerts + addVerts > tri->numAllocedVerts) {
		return false;
	}
	if (tri->numAllocedIndices > 0 && tri->numIndexes + addIndexes > tri->numAllocedIndices) {
		return false;
	}
	return true;
}

ID_INLINE void AppendQuad(srfTriangles_t* tri, const idVec3& p0, const idVec3& p1, const idVec3& p2, const idVec3& p3, dword rgba, const idVec3* normal = NULL) {
	const int base = tri->numVerts;
	SetDrawVert(tri->verts[base + 0], p0, 0.0f, 0.0f, rgba, normal);
	SetDrawVert(tri->verts[base + 1], p1, 1.0f, 0.0f, rgba, normal);
	SetDrawVert(tri->verts[base + 2], p2, 1.0f, 1.0f, rgba, normal);
	SetDrawVert(tri->verts[base + 3], p3, 0.0f, 1.0f, rgba, normal);

	const int indexBase = tri->numIndexes;
	tri->indexes[indexBase + 0] = base + 0;
	tri->indexes[indexBase + 1] = base + 1;
	tri->indexes[indexBase + 2] = base + 2;
	tri->indexes[indexBase + 3] = base + 0;
	tri->indexes[indexBase + 4] = base + 2;
	tri->indexes[indexBase + 5] = base + 3;
	tri->numVerts += 4;
	tri->numIndexes += 6;
}

ID_INLINE idVec3 WorldFromLocal(const rvBSE* effect, const idVec3& local) {
	if (!effect) {
		return local;
	}
	return effect->GetCurrentOrigin() + effect->GetCurrentAxis() * local;
}

ID_INLINE bool IsScalarDomain(const rvParticleParms* parms) {
	if (!parms) {
		return false;
	}
	return (parms->mSpawnType & 0x3) == 1;
}

ID_INLINE void BuildPerpBasis(const idVec3& forward, idVec3& right, idVec3& up) {
	if (idMath::Fabs(forward.x) < 0.99f) {
		right = idVec3(0.0f, 0.0f, 1.0f).Cross(forward);
	}
	else {
		right = idVec3(0.0f, 1.0f, 0.0f).Cross(forward);
	}

	if (right.LengthSqr() > 1e-8f) {
		right.NormalizeFast();
	}
	else {
		right.Set(0.0f, 1.0f, 0.0f);
	}

	up = forward.Cross(right);
	if (up.LengthSqr() > 1e-8f) {
		up.NormalizeFast();
	}
	else {
		up.Set(0.0f, 0.0f, 1.0f);
	}
}

ID_INLINE idMat3 BuildInitToCurrentAxis(const rvBSE* effect, const idMat3& initAxis) {
	if (!effect) {
		return initAxis;
	}
	// Vanilla converts unlocked particle axes with matrix-divide semantics.
	return initAxis / effect->GetCurrentAxis();
}

ID_INLINE idMat3 BuildCurrentToInitAxis(const rvBSE* effect, const idMat3& initAxis) {
	if (!effect) {
		return initAxis;
	}
	// Inverse mapping of BuildInitToCurrentAxis, used when persisting
	// current-frame results back into the particle's original init frame.
	return effect->GetCurrentAxis() / initAxis;
}

ID_INLINE void BSETraceRenderDrop(const char* typeName, const rvParticle* particle, float time, const char* reason, float value0 = 0.0f, float value1 = 0.0f) {
	static int bseRenderDropTraceCount = 0;
	if (!particle || BSE_GetFrameCounterMode() < 2 || bseRenderDropTraceCount >= 256) {
		return;
	}
	const float duration = particle->GetDuration();
	const float endTime = particle->GetEndTime();
	const float startTime = endTime - duration;

	common->Printf(
		"BSE render drop %d: type=%s reason=%s time=%.4f start=%.4f end=%.4f dur=%.4f v0=%.4f v1=%.4f\n",
		bseRenderDropTraceCount,
		typeName ? typeName : "<null>",
		reason ? reason : "<null>",
		time,
		startTime,
		endTime,
		duration,
		value0,
		value1);
	++bseRenderDropTraceCount;
}

}

// ---------------------------------------------------------------------------
//  attenuation helpers
// ---------------------------------------------------------------------------
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms1& result) {
	if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
		return;
	}
	if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
		atten = 1.0f - atten;
	}
	result.Scale(atten);
}

void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms2& result) {
	if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
		return;
	}
	if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
		atten = 1.0f - atten;
	}
	result.Scale(atten);
}

void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms3& result) {
	if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
		return;
	}
	if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
		atten = 1.0f - atten;
	}
	result.Scale(atten);
}

void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms1Particle& result) {
	if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
		return;
	}
	if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
		atten = 1.0f - atten;
	}
	result.Scale(atten);
}

void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms2Particle& result) {
	if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
		return;
	}
	if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
		atten = 1.0f - atten;
	}
	result.Scale(atten);
}

void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms3Particle& result) {
	if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
		return;
	}
	if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
		atten = 1.0f - atten;
	}
	result.Scale(atten);
}

void rvLineParticle::HandleTiling(rvParticleTemplate* pt) {
	if (!pt || !GetTiled()) {
		return;
	}
	const float* len = GetInitLength();
	if (!len) {
		mTextureScale = 1.0f;
		return;
	}
	const idVec3 length(len[0], len[1], len[2]);
	mTextureScale = Max(0.001f, length.LengthFast() / Max(0.001f, pt->GetTiling()));
}

void rvLinkedParticle::HandleTiling(rvParticleTemplate* pt) {
	if (!pt || !GetTiled()) {
		return;
	}
	mTextureScale = Max(0.001f, pt->GetTiling());
}

// ---------------------------------------------------------------------------
//  array helpers
// ---------------------------------------------------------------------------
#define DEFINE_ARRAY_ENTRY(TYPE) \
	rvParticle* TYPE::GetArrayEntry(int i) const { \
		return (i < 0) ? NULL : const_cast<TYPE*>(this) + i; \
	}

#define DEFINE_ARRAY_INDEX(TYPE) \
	int TYPE::GetArrayIndex(rvParticle* p) const { \
		if (!p) { return -1; } \
		const ptrdiff_t diff = reinterpret_cast<const byte*>(p) - reinterpret_cast<const byte*>(this); \
		return static_cast<int>(diff / sizeof(TYPE)); \
	}

DEFINE_ARRAY_ENTRY(rvSpriteParticle)
DEFINE_ARRAY_INDEX(rvSpriteParticle)
DEFINE_ARRAY_ENTRY(rvLineParticle)
DEFINE_ARRAY_INDEX(rvLineParticle)
DEFINE_ARRAY_ENTRY(rvOrientedParticle)
DEFINE_ARRAY_INDEX(rvOrientedParticle)
DEFINE_ARRAY_ENTRY(rvElectricityParticle)
DEFINE_ARRAY_INDEX(rvElectricityParticle)
DEFINE_ARRAY_ENTRY(rvDecalParticle)
DEFINE_ARRAY_INDEX(rvDecalParticle)
DEFINE_ARRAY_ENTRY(rvModelParticle)
DEFINE_ARRAY_INDEX(rvModelParticle)
DEFINE_ARRAY_ENTRY(rvLightParticle)
DEFINE_ARRAY_INDEX(rvLightParticle)
DEFINE_ARRAY_ENTRY(rvLinkedParticle)
DEFINE_ARRAY_INDEX(rvLinkedParticle)
DEFINE_ARRAY_ENTRY(rvDebrisParticle)
DEFINE_ARRAY_INDEX(rvDebrisParticle)

// ---------------------------------------------------------------------------
//  spawning helpers
// ---------------------------------------------------------------------------
void rvParticle::SetOriginUsingEndOrigin(rvBSE* effect, rvParticleTemplate* pt, idVec3* normal, idVec3* centre) {
	if (!effect || !pt || !pt->mpSpawnPosition) {
		mInitPos.Zero();
		return;
	}

	// Match vanilla end-origin spawn behavior:
	// 1) seed/randomize once, 2) force fraction in X and resample.
	// Domains with linearSpacing consume that pre-seeded X value.
	pt->mpSpawnPosition->Spawn(mInitPos.ToFloatPtr(), *pt->mpSpawnPosition, NULL, NULL);
	mInitPos.x = mFraction;
	pt->mpSpawnPosition->Spawn(mInitPos.ToFloatPtr(), *pt->mpSpawnPosition, normal, centre);

	if (!effect->GetHasEndOrigin()) {
		return;
	}

	const idVec3 endLocal = effect->GetCurrentAxisTransposed() * (effect->GetCurrentEndOrigin() - effect->GetCurrentOrigin());
	idVec3 forward = endLocal;
	const float endLenSqr = forward.LengthSqr();
	if (endLenSqr <= 1e-8f) {
		return;
	}
	forward.NormalizeFast();

	idVec3 right;
	idVec3 up;
	BuildPerpBasis(forward, right, up);

	const bool linearSpacing = (pt->mpSpawnPosition->mFlags & PPFLAG_LINEARSPACING) != 0;
	const float t = linearSpacing ? Clamp01(mFraction) : Clamp01(mInitPos.x);
	const int spawnShape = pt->mpSpawnPosition->mSpawnType & ~0x3;

	// Spiral domains authored with `useEndOrigin linearSpacing` expect their
	// lateral offset to advance around the beam as spacing advances.
	const float range = pt->mpSpawnPosition->mRange;
	const bool spiralTwist = linearSpacing && (spawnShape == SPF_SPIRAL_0) && (idMath::Fabs(range) > BSE_TIME_EPSILON);
	float twistS = 0.0f;
	float twistC = 1.0f;
	if (spiralTwist) {
		const float endLength = idMath::Sqrt(endLenSqr);
		const float twist = idMath::TWO_PI * ((t * endLength) / range);
		idMath::SinCos(twist, twistS, twistC);
	}

	idVec3 local = mInitPos;
	if (spiralTwist) {
		const float y = local.y * twistC - local.z * twistS;
		const float z = local.y * twistS + local.z * twistC;
		local.y = y;
		local.z = z;
	}
	mInitPos = endLocal * t + forward * local.x + right * local.y + up * local.z;

	if (normal) {
		idVec3 localNormal = *normal;
		if (spiralTwist) {
			const float y = localNormal.y * twistC - localNormal.z * twistS;
			const float z = localNormal.y * twistS + localNormal.z * twistC;
			localNormal.y = y;
			localNormal.z = z;
		}
		*normal = forward * localNormal.x + right * localNormal.y + up * localNormal.z;
		if (normal->LengthSqr() > 1e-8f) {
			normal->NormalizeFast();
		}
		else {
			*normal = forward;
		}
	}
}

void rvParticle::HandleEndOrigin(rvBSE* effect, rvParticleTemplate* pt, idVec3* normal, idVec3* centre) {
	if (!pt || !pt->mpSpawnPosition) {
		mInitPos.Zero();
		return;
	}

	// Preserve per-particle spawn fraction for end-origin domains.
	mInitPos.x = mFraction;

	if (effect && effect->GetHasEndOrigin() && (pt->mpSpawnPosition->mFlags & PPFLAG_USEENDORIGIN)) {
		SetOriginUsingEndOrigin(effect, pt, normal, centre);
		return;
	}

	pt->mpSpawnPosition->Spawn(mInitPos.ToFloatPtr(), *pt->mpSpawnPosition, normal, centre);
}

void rvParticle::SetLengthUsingEndOrigin(rvBSE* effect, rvParticleParms& parms, float* length) {
	if (!length) {
		return;
	}
	parms.Spawn(length, parms, NULL, NULL);
	if (!effect || !effect->GetHasEndOrigin()) {
		return;
	}

	const idVec3 endLocal = effect->GetCurrentAxisTransposed() * (effect->GetCurrentEndOrigin() - effect->GetCurrentOrigin());
	idVec3 forward = endLocal;
	if (forward.LengthSqr() <= 1e-8f) {
		return;
	}
	forward.NormalizeFast();

	idVec3 right;
	idVec3 up;
	BuildPerpBasis(forward, right, up);

	// `useEndOrigin` lengths are authored as offsets around the baseline
	// vector from origin to end-origin.
	const idVec3 out = endLocal + forward * length[0] + right * length[1] + up * length[2];
	length[0] = out.x;
	length[1] = out.y;
	length[2] = out.z;
}

void rvParticle::HandleEndLength(rvBSE* effect, rvParticleTemplate* pt, rvParticleParms& parms, float* length) {
	if ((parms.mFlags & PPFLAG_USEENDORIGIN) != 0) {
		SetLengthUsingEndOrigin(effect, parms, length);
	}
	else {
		parms.Spawn(length, parms, NULL, NULL);
	}
}

void rvParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
	if (!effect || !segment) {
		return;
	}

	rvSegmentTemplate* st = segment->GetSegmentTemplate();
	if (!st) {
		return;
	}
	rvParticleTemplate* pt = st->GetParticleTemplate();
	if (!pt) {
		return;
	}

	mNext = NULL;
	mMotionStartTime = birthTime;
	mLastTrailTime = birthTime;
	mFlags = pt->GetFlags();
	mStartTime = birthTime;
	mFraction = fraction;
	mTextureScale = 1.0f;
	mTextureOffset = 0.0f;
	mInitEffectPos = vec3_origin;
	mInitAxis = mat3_identity;
	mTrailRepeat = pt->GetTrailRepeat();

	const bool generatedOriginNormal = pt->GetGeneratedOriginNormal();
	const bool generatedNormal = pt->GetGeneratedNormal();
	const bool transformByNormal = generatedOriginNormal || generatedNormal;
	const bool flipNormal = pt->GetFlippedNormal();

	idVec3 normal(1.0f, 0.0f, 0.0f);
	if (generatedOriginNormal) {
		HandleEndOrigin(effect, pt, &normal, NULL);
	}
	else if (generatedNormal) {
		idVec3 centre = pt->mCentre;
		HandleEndOrigin(effect, pt, &normal, &centre);
	}
	else {
		HandleEndOrigin(effect, pt, NULL, NULL);
		if (pt->GetCalculatedNormal() && pt->mpSpawnDirection) {
			pt->mpSpawnDirection->Spawn(normal.ToFloatPtr(), *pt->mpSpawnDirection, NULL, NULL);
		}
	}

	SetLocked(st->GetLocked());
	// Legacy particle flag bit 0x80000 is set for trail-child segments in
	// segment-template finish and controls use of parent-supplied init transform.
	const bool transformParent = (pt->GetFlags() & PTFLAG_LINKED) != 0;
	if (GetLocked()) {
		mInitEffectPos = vec3_origin;
		mInitAxis = initAxis;
	}
	else {
		mInitEffectPos = effect->GetCurrentOrigin();
		mInitAxis = effect->GetCurrentAxis();
		// Match vanilla spawn timing: compensate for owner interpolation so
		// particles start in the frame-accurate local position at birthTime.
		mInitPos -= mInitAxis * effect->GetInterpolatedOffset(birthTime);
	}
	if (pt->mpSpawnOffset && pt->mpSpawnOffset->mSpawnType != SPF_NONE_0) {
		SetHasOffset(true);
	}
	if (pt->GetTiled()) {
		SetFlag(true, PTFLAG_TILED);
	}
	if (pt->GetGeneratedLine()) {
		SetFlag(true, PTFLAG_GENERATED_LINE);
	}

	idVec3 direction = normal;
	if (direction.LengthSqr() > 1e-6f) {
		direction.NormalizeFast();
	}
	else {
		direction.Set(1.0f, 0.0f, 0.0f);
	}

	if (pt->mpSpawnVelocity) {
		pt->mpSpawnVelocity->Spawn(mVelocity.ToFloatPtr(), *pt->mpSpawnVelocity, NULL, NULL);
		if (IsScalarDomain(pt->mpSpawnVelocity)) {
			if (!transformByNormal) {
				mVelocity = direction * mVelocity.x;
			}
		}
		if (transformParent) {
			mVelocity = initAxis * mVelocity;
		}
	}
	else {
		mVelocity.Zero();
	}

	if (pt->mpSpawnAcceleration) {
		pt->mpSpawnAcceleration->Spawn(mAcceleration.ToFloatPtr(), *pt->mpSpawnAcceleration, NULL, NULL);
		if (IsScalarDomain(pt->mpSpawnAcceleration)) {
			if (!transformByNormal) {
				mAcceleration = direction * mAcceleration.x;
			}
		}
	}
	else {
		mAcceleration.Zero();
	}

	if (transformByNormal) {
		if (normal.LengthSqr() > 1e-8f) {
			normal.NormalizeFast();
		}

		const idMat3 normalAxis = normal.ToMat3();
		mVelocity = normalAxis * mVelocity;
		mAcceleration = normalAxis * mAcceleration;
	}

	if (flipNormal) {
		mVelocity = -mVelocity;
	}

	if (normal.LengthSqr() <= 1e-8f) {
		normal = mVelocity;
		if (normal.LengthSqr() > 1e-8f) {
			normal.NormalizeFast();
		}
	}

	if (pt->mpSpawnFriction) {
		float frictionParms[3] = { 0.0f, 0.0f, 0.0f };
		pt->mpSpawnFriction->Spawn(frictionParms, *pt->mpSpawnFriction, NULL, NULL);
		mFriction = Max(0.0f, frictionParms[0]);
	}
	else {
		mFriction = 0.0f;
	}

	if (pt->GetParentVelocity()) {
		mVelocity += effect->GetCurrentVelocity();
	}
	if (transformParent) {
		mInitPos += initOffset;
	}

	const float duration = idMath::ClampFloat(BSE_TIME_EPSILON, BSE_MAX_DURATION, pt->GetDuration());
	mEndTime = mStartTime + duration;

	if (pt->mpSpawnTint) {
		pt->mpSpawnTint->Spawn(mTintEnv.GetStart(), *pt->mpSpawnTint, NULL, NULL);
	}
	if (pt->mpDeathTint) {
		pt->mpDeathTint->Spawn(mTintEnv.GetEnd(), *pt->mpDeathTint, NULL, NULL);
		pt->mpDeathTint->HandleRelativeParms(mTintEnv.GetEnd(), mTintEnv.GetStart(), 3);
	}

	if (pt->mpSpawnFade) {
		pt->mpSpawnFade->Spawn(mFadeEnv.GetStart(), *pt->mpSpawnFade, NULL, NULL);
	}
	if (pt->mpDeathFade) {
		pt->mpDeathFade->Spawn(mFadeEnv.GetEnd(), *pt->mpDeathFade, NULL, NULL);
		pt->mpDeathFade->HandleRelativeParms(mFadeEnv.GetEnd(), mFadeEnv.GetStart(), 1);
	}

	if (pt->mpSpawnAngle) {
		pt->mpSpawnAngle->Spawn(mAngleEnv.GetStart(), *pt->mpSpawnAngle, NULL, NULL);
	}
	if (pt->mpDeathAngle) {
		pt->mpDeathAngle->Spawn(mAngleEnv.GetEnd(), *pt->mpDeathAngle, NULL, NULL);
		pt->mpDeathAngle->HandleRelativeParms(mAngleEnv.GetEnd(), mAngleEnv.GetStart(), 3);
	}

	if (pt->mpSpawnOffset) {
		pt->mpSpawnOffset->Spawn(mOffsetEnv.GetStart(), *pt->mpSpawnOffset, NULL, NULL);
	}
	if (pt->mpDeathOffset) {
		pt->mpDeathOffset->Spawn(mOffsetEnv.GetEnd(), *pt->mpDeathOffset, NULL, NULL);
		pt->mpDeathOffset->HandleRelativeParms(mOffsetEnv.GetEnd(), mOffsetEnv.GetStart(), 3);
	}

	if (float* initSize = GetInitSize()) {
		if (pt->mpSpawnSize) {
			pt->mpSpawnSize->Spawn(initSize, *pt->mpSpawnSize, NULL, NULL);
		}
		if (float* destSize = GetDestSize()) {
			if (pt->mpDeathSize) {
				pt->mpDeathSize->Spawn(destSize, *pt->mpDeathSize, NULL, NULL);
				pt->mpDeathSize->HandleRelativeParms(destSize, initSize, pt->mNumSizeParms);
			}
		}
	}

	if (float* initRotate = GetInitRotation()) {
		if (pt->mpSpawnRotate) {
			pt->mpSpawnRotate->Spawn(initRotate, *pt->mpSpawnRotate, NULL, NULL);
		}
		if (float* destRotate = GetDestRotation()) {
			if (pt->mpDeathRotate) {
				pt->mpDeathRotate->Spawn(destRotate, *pt->mpDeathRotate, NULL, NULL);
				pt->mpDeathRotate->HandleRelativeParms(destRotate, initRotate, pt->mNumRotateParms);
			}
		}
	}

	// Angles/rotation in decls are specified in turns; runtime evaluates radians.
	ScaleRotation(idMath::TWO_PI);
	ScaleAngle(idMath::TWO_PI);
	const idAngles normalAngles = normal.ToAngles();
	rvAngles orient(DEG2RAD(normalAngles.pitch), DEG2RAD(normalAngles.yaw), DEG2RAD(normalAngles.roll));
	HandleOrientation(orient);

	if (float* initLength = GetInitLength()) {
		if (pt->mpSpawnLength) {
			HandleEndLength(effect, pt, *pt->mpSpawnLength, initLength);
		}
		if (float* destLength = GetDestLength()) {
			if (pt->mpDeathLength) {
				pt->mpDeathLength->Spawn(destLength, *pt->mpDeathLength, NULL, NULL);
				pt->mpDeathLength->HandleRelativeParms(destLength, initLength, 3);
			}
		}
	}

	if (transformByNormal) {
		TransformLength(normal);
	}
	if (flipNormal) {
		ScaleLength(-1.0f);
	}

	mTrailTime = pt->GetTrailTime();
	mTrailCount = idMath::ClampInt(0, 128, idMath::FtoiFast(rvRandom::flrand(pt->mTrailInfo->mTrailCount.x, pt->mTrailInfo->mTrailCount.y)));
	SetModel(pt->GetModel());
	SetupElectricity(pt);

	const float attenuation = effect->GetAttenuation(st);
	if (pt->mpSpawnFade) {
		AttenuateFade(attenuation, *pt->mpSpawnFade);
	}
	if (pt->mpSpawnSize) {
		AttenuateSize(attenuation, *pt->mpSpawnSize);
	}
	if (pt->mpSpawnLength) {
		AttenuateLength(attenuation, *pt->mpSpawnLength);
	}
	const float gravityScale = pt->GetGravity();
	if (gravityScale != 0.0f) {
		// Particle gravity is authored in effect-space and must be reprojected
		// into the current local frame used by the particle simulation.
		const idVec3 gravityWorld = effect->GetGravity() * gravityScale;
		mAcceleration += effect->GetCurrentAxisTransposed() * gravityWorld;
	}

	HandleTiling(pt);
	mPosition = mInitPos;
}

void rvLineParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
	rvParticle::FinishSpawn(effect, segment, birthTime, fraction, initOffset, initAxis);
}

void rvLinkedParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
	rvParticle::FinishSpawn(effect, segment, birthTime, fraction, initOffset, initAxis);
}

void rvDebrisParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
	if (!bse_debris.GetBool() || !effect || !segment || !game || session->readDemo) {
		if (bse_debug.GetInteger() > 0 && effect) {
			common->Printf("BSE debris skipped: effect=%s enabled=%d game=%p demo=%d\n",
				effect->GetDeclName(),
				bse_debris.GetBool() ? 1 : 0,
				game,
				session && session->readDemo ? 1 : 0);
		}
		return;
	}

	rvParticle::FinishSpawn(effect, segment, birthTime, fraction, initOffset, initAxis);

	rvSegmentTemplate* st = segment->GetSegmentTemplate();
	if (!st) {
		return;
	}
	rvParticleTemplate* pt = st->GetParticleTemplate();
	if (!pt) {
		return;
	}

	const char* entityDefName = pt->GetEntityDefName();
	if (!entityDefName || entityDefName[0] == '\0') {
		// Keep compatibility with legacy data that may author debris as plain particles.
		if (bse_debug.GetInteger() > 0) {
			common->Printf("BSE debris skipped: effect=%s has no entityDef\n", effect->GetDeclName());
		}
		return;
	}

	idVec3 localPosition;
	EvaluatePosition(effect, pt, localPosition, birthTime);
	idVec3 localVelocity;
	EvaluateVelocity(effect, localVelocity, birthTime);

	idVec3 angularVelocity(vec3_origin);
	if (float* initRotate = GetInitRotation()) {
		angularVelocity.Set(initRotate[0], initRotate[1], initRotate[2]);
	}

	idVec3 worldOrigin;
	idVec3 worldVelocity;
	idMat3 worldAxis;
	if (GetLocked()) {
		worldOrigin = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * localPosition;
		worldVelocity = effect->GetCurrentAxis() * localVelocity;
		angularVelocity = effect->GetCurrentAxis() * angularVelocity;
		worldAxis = effect->GetCurrentAxis();
	}
	else {
		worldOrigin = mInitEffectPos + mInitAxis * localPosition;
		worldVelocity = mInitAxis * localVelocity;
		angularVelocity = mInitAxis * angularVelocity;
		worldAxis = mInitAxis;
	}

	const int maxLifetimeMs = idMath::FtoiFast(BSE_MAX_DURATION * 1000.0f);
	const int lifetimeMs = idMath::ClampInt(1, maxLifetimeMs, idMath::FtoiFast(GetDuration() * 1000.0f));

	if (bse_debug.GetInteger() > 0) {
		common->Printf("BSE debris: effect=%s entityDef=%s lifetime=%d origin=(%.1f %.1f %.1f)\n",
			effect->GetDeclName(),
			entityDefName,
			lifetimeMs,
			worldOrigin.x,
			worldOrigin.y,
			worldOrigin.z);
	}

	game->SpawnClientMoveable(entityDefName, lifetimeMs, worldOrigin, worldAxis, worldVelocity, angularVelocity);

	// Debris is represented by spawned client entities, not by CPU-side BSE quads.
	mEndTime = mStartTime;
	mTrailTime = 0.0f;
	mTrailCount = 0;
}

void rvLineParticle::Refresh(rvBSE* effect, rvSegmentTemplate* st, rvParticleTemplate* pt) {
	if (!effect || !pt || !pt->UsesEndOrigin()) {
		return;
	}
	if (float* initLength = GetInitLength()) {
		if (pt->mpSpawnLength) {
			HandleEndLength(effect, pt, *pt->mpSpawnLength, initLength);
		}
		if (float* destLength = GetDestLength()) {
			if (pt->mpDeathLength) {
				if ((pt->mpDeathLength->mFlags & PPFLAG_USEENDORIGIN) != 0) {
					SetLengthUsingEndOrigin(effect, *pt->mpDeathLength, destLength);
				}
				else {
					pt->mpDeathLength->Spawn(destLength, *pt->mpDeathLength, NULL, NULL);
				}
				pt->mpDeathLength->HandleRelativeParms(destLength, initLength, 3);
			}
		}
	}

	HandleTiling(pt);
	const float attenuation = effect->GetAttenuation(st);
	if (pt->mpSpawnLength) {
		AttenuateLength(attenuation, *pt->mpSpawnLength);
	}
	// Keep the refreshed start/end length values intact. Re-initializing the
	// particle length envelope here can zero out the freshly recomputed
	// useEndOrigin vector and collapse the beam to fallback directions.
}

// ---------------------------------------------------------------------------
//  simulation
// ---------------------------------------------------------------------------
bool rvParticle::GetEvaluationTime(float time, float& evalTime, bool infinite) {
	evalTime = time - mStartTime;
	if (time >= mEndTime - BSE_TIME_EPSILON) {
		evalTime = (mEndTime - mStartTime) - BSE_TIME_EPSILON;
	}
	if (infinite) {
		return true;
	}
	return (time > mStartTime - BSE_TIME_EPSILON) && (time < mEndTime);
}

void rvParticle::EvaluateVelocity(const rvBSE* effect, idVec3& velocity, float time) {
	(void)effect;

	const float t = Max(0.0f, time - mMotionStartTime);
	if (GetStationary()) {
		velocity.Set(1.0f, 0.0f, 0.0f);
		return;
	}

	const float damp = Max(0.0f, 1.0f - mFriction * t);
	velocity = (mVelocity + mAcceleration * t) * damp;
}

void rvParticle::EvaluatePosition(const rvBSE* effect, rvParticleTemplate* pt, idVec3& pos, float time) {
	const float t = Max(0.0f, time - mMotionStartTime);
	if (GetStationary()) {
		pos = mInitPos;
		mPosition = pos;
		return;
	}

	const float halfT2 = 0.5f * t * t;
	const float damp = Max(0.0f, 1.0f - mFriction * t);
	pos = mInitPos + (mVelocity * t + mAcceleration * halfT2) * damp;

	if (GetHasOffset() && pt && pt->mpAngleEnvelope && pt->mpOffsetEnvelope) {
		const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
		rvAngles angle;
		idVec3 offset;
		EvaluateAngle(pt->mpAngleEnvelope, t, oneOverDuration, angle);
		EvaluateOffset(pt->mpOffsetEnvelope, t, oneOverDuration, offset);

		idMat3 rotation;
		angle.ToMat3(rotation);
		pos += rotation * offset;
	}

	if (effect && !GetLocked()) {
		const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
		pos = initToCurrent * pos;

		const idVec3 delta = mInitEffectPos - effect->GetCurrentOrigin();
		pos += effect->GetCurrentAxisTransposed() * delta;
	}

	mPosition = pos;
}

bool rvParticle::RunPhysics(rvBSE* effect, rvSegmentTemplate* st, float time) {
	if (!effect || !st || !bse_physics.GetBool() || session->readDemo) {
		return false;
	}

	if (GetStationary()) {
		return false;
	}

	rvParticleTemplate* pt = st->GetParticleTemplate();
	if (!pt || !pt->GetHasPhysics()) {
		return false;
	}

	if (time - mMotionStartTime < BSE_PHYSICS_TIME_SAMPLE) {
		return false;
	}

	float sourceTime = time - BSE_PHYSICS_TIME_SAMPLE;
	if (sourceTime < mMotionStartTime) {
		sourceTime = mMotionStartTime;
	}

	idVec3 sourceLocal;
	idVec3 destLocal;
	EvaluatePosition(effect, pt, sourceLocal, sourceTime);
	EvaluatePosition(effect, pt, destLocal, time);

	const idVec3 sourceWorld = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * sourceLocal;
	const idVec3 destWorld = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * destLocal;

	idTraceModel* trm = NULL;
	if (pt->mTraceModelIndex >= 0) {
		trm = bse->GetTraceModel(pt->mTraceModelIndex);
	}
	trace_t trace;
	idVec3 source = sourceWorld;
	idVec3 dest = destWorld;
	game->Translation(trace, source, dest, trm, CONTENTS_SOLID | CONTENTS_OPAQUE);
	if (trace.fraction >= 1.0f) {
		return false;
	}

	if (pt->mNumImpactEffects > 0 && bse->CanPlayRateLimited(EC_IMPACT_PARTICLES)) {
		idVec3 impactPos = trace.endpos;
		if (trm) {
			const idVec3 motion = (destWorld - sourceWorld) * trace.fraction;
			CalcImpactPoint(impactPos, trace.endpos, motion, trm->bounds, trace.c.normal);
		}

		const int idx = rvRandom::irand(0, pt->mNumImpactEffects - 1);
		const rvDeclEffect* impactEffect = pt->mImpactEffects[idx];
		if (impactEffect) {
			game->PlayEffect(
				impactEffect,
				impactPos,
				trace.c.normal.ToMat3(),
				false,
				vec3_origin,
				false,
				false,
				EC_IGNORE,
				vec4_one);
		}
	}

	if (pt->mBounce > 0.0f) {
		Bounce(effect, pt, trace.endpos, trace.c.normal, time);
	}

	return pt->GetDeleteOnImpact();
}

void rvParticle::Bounce(rvBSE* effect, rvParticleTemplate* pt, idVec3 endPos, idVec3 normal, float time) {
	if (!effect || !pt) {
		return;
	}

	const idMat3 currentAxis = effect->GetCurrentAxis();
	const idMat3 currentAxisTransposed = effect->GetCurrentAxisTransposed();

	idVec3 oldVelocity;
	EvaluateVelocity(effect, oldVelocity, time);

	idVec3 worldVelocity = currentAxis * oldVelocity;
	const float proj = worldVelocity * normal;
	worldVelocity -= (proj + proj) * normal;
	worldVelocity *= pt->mBounce;

	const float speedSqr = worldVelocity.LengthSqr();
	if (speedSqr < BSE_BOUNCE_LIMIT) {
		// Match vanilla settle behavior: only stick when the impact normal is
		// sufficiently opposite gravity direction (roughly "floor" contacts).
		const float gravityDot = normal * effect->GetGravityDir();
		if (gravityDot < -idMath::SQRT_1OVER2) {
			SetStationary(true);
			worldVelocity.Zero();
		}
	}

	const idVec3 currentLocalVelocity = currentAxisTransposed * worldVelocity;
	const idVec3 currentLocalPos = currentAxisTransposed * (endPos - effect->GetCurrentOrigin());

	if (GetLocked()) {
		mVelocity = currentLocalVelocity;
		mInitPos = currentLocalPos;
	}
	else {
		// Preserve the original unlocked init frame and persist bounce results
		// via the exact inverse of EvaluatePosition/EvaluateVelocity's
		// init->current mapping.
		const idMat3 currentToInit = BuildCurrentToInitAxis(effect, mInitAxis);
		const idVec3 originDelta = mInitEffectPos - effect->GetCurrentOrigin();
		const idVec3 currentOriginOffset = currentAxisTransposed * originDelta;
		mVelocity = currentToInit * currentLocalVelocity;
		mInitPos = currentToInit * (currentLocalPos - currentOriginOffset);
	}
	mMotionStartTime = time;
}

void rvParticle::CheckTimeoutEffect(rvBSE* effect, rvSegmentTemplate* st, float time) {
	if (!effect || !st || !game) {
		return;
	}
	rvParticleTemplate* pt = st->GetParticleTemplate();
	if (!pt || pt->GetNumTimeoutEffects() <= 0) {
		return;
	}

	const int idx = rvRandom::irand(0, pt->GetNumTimeoutEffects() - 1);
	const rvDeclEffect* timeoutEffect = pt->mTimeoutEffects[idx];
	if (!timeoutEffect) {
		return;
	}

	idVec3 position;
	idVec3 velocity;
	EvaluatePosition(effect, pt, position, time);
	EvaluateVelocity(effect, velocity, time);
	if (velocity.LengthSqr() > 1e-8f) {
		velocity.NormalizeFast();
	}
	else {
		velocity.Set(1.0f, 0.0f, 0.0f);
	}

	const idVec3 worldPos = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * position;
	const idVec3 worldDir = effect->GetCurrentAxis() * velocity;
	game->PlayEffect(
		timeoutEffect,
		worldPos,
		worldDir.ToMat3(),
		false,
		vec3_origin,
		false,
		false,
		EC_IGNORE,
		vec4_one);
}

void rvParticle::CalcImpactPoint(idVec3& endPos, const idVec3& origin, const idVec3& motion, const idBounds& bounds, const idVec3& normal) {
	endPos = origin;

	if (motion.LengthSqr() <= 1e-8f || bounds.IsCleared()) {
		return;
	}

	idVec3 work = motion;
	const idVec3 size = bounds[1] - bounds[0];
	if (idMath::Fabs(size.x) > BSE_TIME_EPSILON) {
		work.x /= size.x;
	}
	if (idMath::Fabs(size.y) > BSE_TIME_EPSILON) {
		work.y /= size.y;
	}
	if (idMath::Fabs(size.z) > BSE_TIME_EPSILON) {
		work.z /= size.z;
	}

	if (work.LengthSqr() > 1e-8f) {
		work.NormalizeFast();
	}

	int axis = 0;
	const idVec3 absWork(idMath::Fabs(work.x), idMath::Fabs(work.y), idMath::Fabs(work.z));
	if (absWork.y >= absWork.x && absWork.y >= absWork.z) {
		axis = 1;
	}
	else if (absWork.z >= absWork.x && absWork.z >= absWork.y) {
		axis = 2;
	}

	const float dominant = Max(idMath::Fabs(work[axis]), 1e-6f);
	const float invLen = 0.5f / dominant;
	const idVec3 push((size.x * invLen) * work.x, (size.y * invLen) * work.y, (size.z * invLen) * work.z);
	endPos += normal + normal + push;
}

void rvParticle::EmitSmokeParticles(rvBSE* effect, rvSegment* child, rvParticleTemplate* pt, float time) {
	if (!effect || !child || !pt) {
		return;
	}

	rvSegmentTemplate* childTemplate = child->GetSegmentTemplate();
	if (!childTemplate) {
		return;
	}

	const float timeEnd = time + 0.016000001f;
	while (mLastTrailTime < timeEnd) {
		if (mLastTrailTime >= mStartTime && mLastTrailTime < mEndTime) {
			idVec3 position;
			idVec3 velocity;
			EvaluatePosition(effect, pt, position, mLastTrailTime);
			EvaluateVelocity(effect, velocity, mLastTrailTime);
			if (velocity.LengthSqr() > 1e-8f) {
				velocity.NormalizeFast();
			}
			else {
				velocity.Set(1.0f, 0.0f, 0.0f);
			}

			child->SpawnParticle(effect, childTemplate, mLastTrailTime, position, velocity.ToMat3());
		}

		const float interval = child->AttenuateInterval(effect, childTemplate);
		if (interval <= BSE_TIME_EPSILON) {
			break;
		}
		mLastTrailTime += interval;
	}
}

// ---------------------------------------------------------------------------
//  render helpers
// ---------------------------------------------------------------------------
dword rvParticle::HandleTint(const rvBSE* effect, idVec4& colour, float alpha) {
	idVec4 out = colour;
	out[3] *= alpha;
	if (effect) {
		const float bright = effect->GetBrightness();
		out[0] *= effect->GetRed() * bright;
		out[1] *= effect->GetGreen() * bright;
		out[2] *= effect->GetBlue() * bright;
		out[3] *= effect->GetAlpha();
	}

	// Additive stages author fade in the alpha envelope, but blend-add paths
	// do not consume destination alpha for attenuation. Premultiply RGB so
	// additive particles fade over time like stock BSE.
	if (GetAdditive()) {
		out[0] *= out[3];
		out[1] *= out[3];
		out[2] *= out[3];
	}

	return PackColorLocal(out);
}

void rvParticle::RenderQuadTrail(const rvBSE* effect, srfTriangles_t* tri, idVec3 offset, float fraction, idVec4& colour, idVec3& pos, bool first) {
	if (!tri) {
		return;
	}
	if (tri->numAllocedVerts > 0 && tri->numVerts + 2 > tri->numAllocedVerts) {
		return;
	}
	if (!first && tri->numAllocedIndices > 0 && tri->numIndexes + 6 > tri->numAllocedIndices) {
		return;
	}

	const dword rgba = HandleTint(effect, colour, 1.0f);
	const int base = tri->numVerts;
	SetDrawVert(tri->verts[base + 0], pos + offset, 0.0f, fraction, rgba);
	SetDrawVert(tri->verts[base + 1], pos - offset, 1.0f, fraction, rgba);

	if (!first) {
		const int indexBase = tri->numIndexes;
		tri->indexes[indexBase + 0] = base - 2;
		tri->indexes[indexBase + 1] = base - 1;
		tri->indexes[indexBase + 2] = base + 0;
		tri->indexes[indexBase + 3] = base - 1;
		tri->indexes[indexBase + 4] = base + 0;
		tri->indexes[indexBase + 5] = base + 1;
		tri->numIndexes += 6;
	}

	tri->numVerts += 2;
}

void rvParticle::RenderMotion(rvBSE* effect, rvParticleTemplate* pt, srfTriangles_t* tri, const renderEffect_s* owner, float time) {
	if (!effect || !pt || !tri || !owner) {
		return;
	}
	if (mTrailCount <= 0) {
		return;
	}

	const float startTime = Max(time - mTrailTime, mStartTime);
	const float delta = time - startTime;
	if (delta <= BSE_TIME_EPSILON) {
		return;
	}

	const float evalTime = Max(0.0f, time - mStartTime);
	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 color;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);

	idVec3 size(1.0f, 1.0f, 1.0f);
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size.ToFloatPtr());
	const float width = size.x;

	idVec3 position;
	EvaluatePosition(effect, pt, position, time);

	const idMat3 ownerAxisTranspose = owner->axis.Transpose();
	const idVec3 localView = ownerAxisTranspose * (effect->GetViewOrg() - owner->origin);
	const idVec3 toView = localView - mInitPos;
	idVec3 motionDir = mVelocity.Cross(toView);
	if (motionDir.LengthSqr() <= 1e-6f) {
		// Vanilla fallback uses world up in owner-local space when velocity and view vectors align.
		motionDir = idVec3(0.0f, 1.0f, 0.0f).Cross(toView);
	}
	if (motionDir.LengthSqr() > 1e-6f) {
		motionDir.NormalizeFast();
	}
	const idVec3 halfWidth = motionDir * (width * 0.5f);

	for (int segment = 0; segment < mTrailCount; ++segment) {
		const float t = static_cast<float>(segment) / static_cast<float>(mTrailCount);
		idVec4 segmentColor = color;
		segmentColor.w *= (1.0f - t);
		RenderQuadTrail(effect, tri, halfWidth, t, segmentColor, position, segment == 0);

		const float sampleTime = time - t * delta;
		EvaluatePosition(effect, pt, position, sampleTime);
	}

	idVec4 endColor = color;
	endColor.w = 0.0f;
	RenderQuadTrail(effect, tri, halfWidth, 1.0f, endColor, position, false);
}

void rvParticle::DoRenderBurnTrail(rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time) {
	if (mTrailCount <= 0 || mTrailTime <= 0.0f) {
		return;
	}

	const float delta = mTrailTime / Max(1, mTrailCount);
	for (int i = 1; i <= mTrailCount; ++i) {
		const float trailTime = time - static_cast<float>(i) * delta;
		if (trailTime < mStartTime || trailTime >= mEndTime) {
			continue;
		}
		const float fade = static_cast<float>(mTrailCount - i) / Max(1, mTrailCount);
		Render(effect, pt, view, tri, trailTime, fade);
	}
}

// ---------------------------------------------------------------------------
//  per-type spawn data
// ---------------------------------------------------------------------------
void rvSpriteParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
	const float* tintStart = mTintEnv.GetStart();
	tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
	const float* sizeStart = mSizeEnv.GetStart();
	size.Set(sizeStart[0], sizeStart[1], 0.0f);
	rotate.Set(mRotationEnv.GetStart()[0], 0.0f, 0.0f);
}

void rvLineParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
	const float* tintStart = mTintEnv.GetStart();
	tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
	size.Set(mSizeEnv.GetStart()[0], 0.0f, 0.0f);
	rotate.Zero();
}

void rvOrientedParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
	const float* tintStart = mTintEnv.GetStart();
	tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
	const float* sizeStart = mSizeEnv.GetStart();
	size.Set(sizeStart[0], sizeStart[1], 0.0f);
	rotate.Set(mRotationEnv.GetStart()[0], mRotationEnv.GetStart()[1], mRotationEnv.GetStart()[2]);
}

void rvModelParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
	const float* tintStart = mTintEnv.GetStart();
	tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
	size.Set(mSizeEnv.GetStart()[0], mSizeEnv.GetStart()[1], mSizeEnv.GetStart()[2]);
	rotate.Set(mRotationEnv.GetStart()[0], mRotationEnv.GetStart()[1], mRotationEnv.GetStart()[2]);
}

void rvLightParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
	const float* tintStart = mTintEnv.GetStart();
	tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
	size.Set(mSizeEnv.GetStart()[0], mSizeEnv.GetStart()[1], mSizeEnv.GetStart()[2]);
	rotate.Zero();
}

void rvDecalParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
	const float* tintStart = mTintEnv.GetStart();
	tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
	const float* sizeStart = mSizeEnv.GetStart();
	size.Set(sizeStart[0], sizeStart[1], Max(idMath::Fabs(sizeStart[0]), idMath::Fabs(sizeStart[1])));
	rotate.Set(mRotationEnv.GetStart()[0], 0.0f, 0.0f);
}

// ---------------------------------------------------------------------------
//  per-type render
// ---------------------------------------------------------------------------
bool rvSpriteParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	if (!effect || !pt || !tri) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		BSETraceRenderDrop("sprite", this, time, "eval", evalTime, GetEndTime() - time);
		return false;
	}

	idVec3 pos;
	EvaluatePosition(effect, pt, pos, time);

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 color;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
	color[3] *= override;
	if (color[3] <= 0.0f) {
		BSETraceRenderDrop("sprite", this, time, "alpha", color[3], override);
		return false;
	}

	float size[2] = { 1.0f, 1.0f };
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size);
	const idVec2& spriteSize = effect->GetSpriteSize();
	if (idMath::Fabs(spriteSize.x) > BSE_TIME_EPSILON || idMath::Fabs(spriteSize.y) > BSE_TIME_EPSILON) {
		size[0] = spriteSize.x;
		size[1] = spriteSize.y;
	}
	float rotation = 0.0f;
	EvaluateRotation(pt->mpRotateEnvelope, evalTime, oneOverDuration, &rotation);

	float s, c;
	idMath::SinCos(rotation, s, c);
	idVec3 right = (view[1] * c - view[2] * s) * size[0];
	idVec3 up = (view[1] * s + view[2] * c) * size[1];

	dword rgba = HandleTint(effect, color, 1.0f);
	AppendQuad(
		tri,
		pos - right,
		pos - up,
		pos + right,
		pos + up,
		rgba,
		&pos);
	return true;
}

bool rvLineParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	if (!effect || !pt || !tri) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		BSETraceRenderDrop("line", this, time, "eval", evalTime, GetEndTime() - time);
		return false;
	}

	idVec3 pos;
	EvaluatePosition(effect, pt, pos, time);

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 color;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
	color[3] *= override;
	if (color[3] <= 0.0f) {
		BSETraceRenderDrop("line", this, time, "alpha", color[3], override);
		return false;
	}

	float width = 1.0f;
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, &width);

	idVec3 length(0.0f, 0.0f, 1.0f);
	EvaluateLength(pt->mpLengthEnvelope, evalTime, oneOverDuration, length);
	if (!GetLocked()) {
		const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
		length = initToCurrent * length;
	}
	if (GetGeneratedLine()) {
		idVec3 velocity;
		EvaluateVelocity(effect, velocity, time);
		const float velocitySqr = velocity.LengthSqr();
		if (velocitySqr > 1e-8f) {
			velocity.NormalizeFast();
			length = velocity * length.LengthFast();
		}
	}

	const idVec3 end = pos + length;
	const idVec3 toView = view[0] - (pos + length * 0.5f);
	idVec3 side = length.Cross(toView);
	float sideLenSqr = side.LengthSqr();
	if (sideLenSqr > 1e-8f) {
		side *= idMath::InvSqrt(sideLenSqr);
	}
	side *= width;

	dword rgba = HandleTint(effect, color, 1.0f);
	const int base = tri->numVerts;
	SetDrawVert(tri->verts[base + 0], pos + side, 0.0f, 0.0f, rgba);
	SetDrawVert(tri->verts[base + 1], pos - side, 0.0f, 1.0f, rgba);
	SetDrawVert(tri->verts[base + 2], end - side, mTextureScale, 1.0f, rgba);
	SetDrawVert(tri->verts[base + 3], end + side, mTextureScale, 0.0f, rgba);

	tri->verts[base + 0].normal = pos;
	tri->verts[base + 1].normal = pos;
	tri->verts[base + 2].normal = pos;
	tri->verts[base + 3].normal = pos;

	const int indexBase = tri->numIndexes;
	tri->indexes[indexBase + 0] = base + 0;
	tri->indexes[indexBase + 1] = base + 1;
	tri->indexes[indexBase + 2] = base + 2;
	tri->indexes[indexBase + 3] = base + 0;
	tri->indexes[indexBase + 4] = base + 2;
	tri->indexes[indexBase + 5] = base + 3;
	tri->numVerts += 4;
	tri->numIndexes += 6;
	return true;
}

bool rvLinkedParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	if (!effect || !pt || !tri) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		return false;
	}

	idVec3 pos;
	EvaluatePosition(effect, pt, pos, time);

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 color;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
	color[3] *= override;
	if (color[3] <= 0.0f) {
		return false;
	}

	float size = 1.0f;
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, &size);
	size = idMath::Fabs(size);

	idVec3 up = view[1] * size;
	dword rgba = HandleTint(effect, color, 1.0f);

	const int base = tri->numVerts;
	SetDrawVert(tri->verts[base + 0], pos + up, mFraction * mTextureScale, 0.0f, rgba);
	SetDrawVert(tri->verts[base + 1], pos - up, mFraction * mTextureScale, 1.0f, rgba);
	tri->verts[base + 0].normal = pos;
	tri->verts[base + 1].normal = pos;
	if (base > 0) {
		const int indexBase = tri->numIndexes;
		tri->indexes[indexBase + 0] = base - 2;
		tri->indexes[indexBase + 1] = base - 1;
		tri->indexes[indexBase + 2] = base + 0;
		tri->indexes[indexBase + 3] = base - 1;
		tri->indexes[indexBase + 4] = base + 1;
		tri->indexes[indexBase + 5] = base + 0;
		tri->numIndexes += 6;
	}
	tri->numVerts += 2;
	return true;
}

bool sdOrientedLinkedParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	return rvLinkedParticle::Render(effect, pt, view, tri, time, override);
}

bool rvOrientedParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	if (!effect || !pt || !tri) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		BSETraceRenderDrop("oriented", this, time, "eval", evalTime, GetEndTime() - time);
		return false;
	}

	idVec3 position;
	EvaluatePosition(effect, pt, position, time);

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 tint;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
	tint[3] *= override;
	if (tint[3] <= 0.0f) {
		BSETraceRenderDrop("oriented", this, time, "alpha", tint[3], override);
		return false;
	}

	float size[2] = { 1.0f, 1.0f };
	float rotation[3] = { 0.0f, 0.0f, 0.0f };
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size);
	EvaluateRotation(pt->mpRotateEnvelope, evalTime, oneOverDuration, rotation);

	idMat3 transform;
	rvAngles(rotation[0], rotation[1], rotation[2]).ToMat3(transform);
	const idVec3 right = transform[1] * -size[0];
	const idVec3 up = transform[2] * size[1];

	dword rgba = HandleTint(effect, tint, 1.0f);
	AppendQuad(
		tri,
		position - right,
		position - up,
		position + right,
		position + up,
		rgba,
		&position);
	return true;
}

bool rvModelParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	if (!effect || !pt || !tri || !mModel || mModel->NumSurfaces() <= 0) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		return false;
	}

	idVec3 position;
	EvaluatePosition(effect, pt, position, time);

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 color;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
	color[3] *= override;
	if (color[3] <= 0.0f) {
		return false;
	}

	float size[3] = { 1.0f, 1.0f, 1.0f };
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size);

	float rotation[3] = { 0.0f, 0.0f, 0.0f };
	EvaluateRotation(pt->mpRotateEnvelope, evalTime, oneOverDuration, rotation);

	const modelSurface_t* surf = mModel->Surface(0);
	if (!surf || !surf->geometry) {
		return false;
	}

	const srfTriangles_t* src = surf->geometry;
	const int baseVert = tri->numVerts;
	const int baseIndex = tri->numIndexes;
	const dword rgba = HandleTint(effect, color, 1.0f);
	byte rgbaBytes[4];
	UnpackColor(rgba, rgbaBytes);

	idMat3 rotationMat;
	rvAngles(rotation[0], rotation[1], rotation[2]).ToMat3(rotationMat);
	idMat3 transform = rotationMat;
	if (!GetLocked()) {
		const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
		transform = transform * initToCurrent;
	}

	for (int i = 0; i < src->numVerts; ++i) {
		idDrawVert& dst = tri->verts[baseVert + i];
		dst = src->verts[i];

		idVec3 p = transform * src->verts[i].xyz;
		p.x *= size[0];
		p.y *= size[1];
		p.z *= size[2];
		dst.xyz = position + p;

		dst.normal = transform * dst.normal;
		dst.tangents[0] = transform * dst.tangents[0];
		dst.tangents[1] = transform * dst.tangents[1];
		dst.color[0] = rgbaBytes[0];
		dst.color[1] = rgbaBytes[1];
		dst.color[2] = rgbaBytes[2];
		dst.color[3] = rgbaBytes[3];
	}

	for (int i = 0; i < src->numIndexes; ++i) {
		tri->indexes[baseIndex + i] = baseVert + src->indexes[i];
	}

	tri->numVerts += src->numVerts;
	tri->numIndexes += src->numIndexes;
	return true;
}

int rvElectricityParticle::GetBoltCount(float length) {
	const int bolts = static_cast<int>(ceilf(length * 0.0625f));
	return idMath::ClampInt(3, static_cast<int>(BSE_ELEC_MAX_BOLTS), bolts);
}

static const idDeclTable* ResolveElectricityJitterTable(const idStr& tableName, const idDeclTable* fallbackTable) {
	const idDeclTable* table = NULL;

	if (!tableName.IsEmpty()) {
		table = declManager->FindTable(tableName, false);
		if (table != NULL && table->IsImplicit()) {
			table = NULL;
		}
	}

	if (table == NULL) {
		table = fallbackTable;
		if (table != NULL && table->IsImplicit()) {
			table = NULL;
		}
	}

	if (table == NULL) {
		table = declManager->FindTable("halfsintable", false);
	}

	return table;
}

void rvElectricityParticle::RenderBranch(const rvBSE* effect, struct SElecWork* work, idVec3 start, idVec3 end, const idDeclTable* jitterTable) {
	if (!effect || !work || !work->tri || !work->coords) {
		return;
	}

	const idDeclTable* resolvedJitterTable = jitterTable ? jitterTable : mJitterTable;

	const float forwardLenSqr = work->forward.LengthSqr();
	if (forwardLenSqr > 1e-8f) {
		work->forward *= idMath::InvSqrt(forwardLenSqr);
	}

	idVec3 left;
	const float planarLenSqr = work->forward.x * work->forward.x + work->forward.y * work->forward.y;
	if (planarLenSqr <= 1e-8f) {
		left.Set(1.0f, 0.0f, 0.0f);
	}
	else {
		const float invPlanarLen = idMath::InvSqrt(planarLenSqr);
		left.Set(-work->forward.y * invPlanarLen, work->forward.x * invPlanarLen, 0.0f);
	}
	const idVec3 down = left.Cross(work->forward);

	const int segmentVertStart = work->tri->numVerts;
	float fraction = work->step;
	idVec3 old = start;
	idVec3 current = start;
	idVec3 accumulatedOffset(vec3_origin);
	int pointCount = 0;
	bool evaluate = true;

	while (true) {
		if (pointCount >= 255) {
			break;
		}
		work->coords[pointCount++] = old;

		if (1.0f - work->step * 0.5f <= fraction) {
			fraction = 1.0f;
			evaluate = false;
		}

		accumulatedOffset += work->forward * rvRandom::flrand(-mJitterSize.x, mJitterSize.x);
		accumulatedOffset += left * rvRandom::flrand(-mJitterSize.y, mJitterSize.y);
		accumulatedOffset += down * rvRandom::flrand(-mJitterSize.z, mJitterSize.z);

		const float noise = resolvedJitterTable ? resolvedJitterTable->TableLookup(fraction) : 0.0f;
		current = start + (end - start) * fraction + accumulatedOffset * noise;

		work->fraction = fraction - work->step;
		ApplyShape(effect, work, old, current, 2, 0.0f, 1.0f);

		old = current;
		fraction += work->step;
		if (!evaluate) {
			break;
		}
	}

	if (segmentVertStart != work->tri->numVerts) {
		if (pointCount < 256) {
			work->coords[pointCount] = current;
			work->coordCount = pointCount + 1;
		}
		else {
			work->coordCount = pointCount;
		}

		RenderLineSegment(effect, work, current, 1.0f);

		for (int base = segmentVertStart; base < work->tri->numVerts - 2; base += 2) {
			if (!HasTriCapacity(work->tri, 0, 6)) {
				break;
			}

			const int indexBase = work->tri->numIndexes;
			work->tri->indexes[indexBase + 0] = base;
			work->tri->indexes[indexBase + 1] = base + 1;
			work->tri->indexes[indexBase + 2] = base + 2;
			work->tri->indexes[indexBase + 3] = base;
			work->tri->indexes[indexBase + 4] = base + 2;
			work->tri->indexes[indexBase + 5] = base + 3;
			work->tri->numIndexes += 6;
		}
	}
	else {
		work->coordCount = 0;
	}
}

bool rvElectricityParticle::RenderLineSegment(const rvBSE* effect, struct SElecWork* work, idVec3 start, float startFraction) {
	if (!effect || !work || !work->tri) {
		return false;
	}
	if (!HasTriCapacity(work->tri, 2, 0)) {
		return false;
	}

	idVec3 offset = work->length.Cross(work->viewPos);
	const float len2 = offset.LengthSqr();
	if (len2 != 0.0f) {
		offset *= idMath::InvSqrt(len2);
	}
	offset *= work->size;

	const dword color = HandleTint(effect, work->tint, work->alpha);
	const float s = startFraction * work->step + work->fraction;
	const int baseVert = work->tri->numVerts;
	SetDrawVert(work->tri->verts[baseVert + 0], start + offset, s, 0.0f, color);
	SetDrawVert(work->tri->verts[baseVert + 1], start - offset, s, 1.0f, color);
	work->tri->numVerts += 2;
	return true;
}

void rvElectricityParticle::ApplyShape(const rvBSE* effect, struct SElecWork* work, idVec3 start, idVec3 end, int count, float startFraction, float endFraction) {
	if (!effect || !work) {
		return;
	}

	while (count >= 1) {
		const float bendA = rvRandom::flrand(0.05f, 0.09f);
		const float bendB = rvRandom::flrand(0.05f, 0.09f);
		const float shape = rvRandom::flrand(0.56f, 0.76f);

		idVec3 forward = end - start;
		const float length = forward.LengthFast() * 0.7f;
		if (length <= 1e-6f) {
			break;
		}
		forward.NormalizeFast();

		idVec3 left;
		const float planarLenSqr = forward.x * forward.x + forward.y * forward.y;
		if (planarLenSqr <= 1e-8f) {
			left.Set(1.0f, 0.0f, 0.0f);
		}
		else {
			const float invPlanarLen = idMath::InvSqrt(planarLenSqr);
			left.Set(-forward.y * invPlanarLen, forward.x * invPlanarLen, 0.0f);
		}
		const idVec3 down = left.Cross(forward);

		const float leftOffset1 = rvRandom::flrand(-bendB - 0.02f, 0.02f - bendB) * length;
		const idVec3 point1 =
			start * shape +
			end * (1.0f - shape) +
			left * leftOffset1 +
			down * rvRandom::flrand(0.23f, 0.43f) * length;

		const float t2 = rvRandom::flrand(0.23f, 0.43f);
		const float leftOffset2 = rvRandom::flrand(-bendA - 0.02f, 0.02f - bendA) * length;
		const idVec3 point2 =
			start * t2 +
			end * (1.0f - t2) +
			left * leftOffset2 +
			down * rvRandom::flrand(-0.02f, 0.02f) * length;

		const float mid0 = startFraction * 0.6666667f + endFraction * 0.3333333f;
		const float mid1 = startFraction * 0.3333333f + endFraction * 0.6666667f;

		ApplyShape(effect, work, start, point1, count - 1, startFraction, mid0);
		ApplyShape(effect, work, point1, point2, count - 1, mid0, mid1);

		--count;
		start = point2;
		startFraction = mid1;
	}

	RenderLineSegment(effect, work, start, startFraction);
}

int rvElectricityParticle::Update(rvParticleTemplate* pt, float time) {
	if (!pt || !pt->mpLengthEnvelope) {
		mNumBolts = 0;
		return 0;
	}

	const float evalTime = Max(0.0f, time - mStartTime);
	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec3 length;
	EvaluateLength(pt->mpLengthEnvelope, evalTime, oneOverDuration, length);
	mNumBolts = GetBoltCount(length.LengthFast());
	return mNumBolts;
}

bool rvElectricityParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
	if (!effect || !pt || !tri) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		return false;
	}

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 tint;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
	tint[3] *= override;
	if (tint[3] <= 0.0f) {
		return false;
	}

	float width = 1.0f;
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, &width);
	idVec3 length;
	EvaluateLength(pt->mpLengthEnvelope, evalTime, oneOverDuration, length);

	idVec3 position;
	EvaluatePosition(effect, pt, position, time);

	if (!GetLocked()) {
		const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
		length = initToCurrent * length;
	}

	if (GetGeneratedLine()) {
		const float generatedLength = length.LengthFast();
		idVec3 velocity;
		EvaluateVelocity(effect, velocity, time);
		if (velocity.LengthSqr() != 0.0f) {
			velocity.NormalizeFast();
		}
		else {
			velocity.Zero();
		}
		length = velocity * generatedLength;
	}

	const float mainLength = length.LengthFast();
	if (mainLength < 0.1f) {
		return false;
	}

	if (mLastJitter + mJitterRate <= time) {
		mLastJitter = time;
		mSeed = rvRandom::Init();
	}

	const unsigned int previousSeed = rvRandom::GetSeed();
	rvRandom::Init(mSeed);

	const int boltCount = Max(1, (mNumBolts > 0) ? mNumBolts : GetBoltCount(mainLength));
	mNumBolts = boltCount;

	SElecWork work;
	memset(&work, 0, sizeof(work));
	idVec3 tmpCoords[256];
	work.tri = tri;
	work.coords = tmpCoords;
	work.coordCount = 0;
	work.tint = tint;
	work.size = idMath::Fabs(width);
	work.alpha = 1.0f;
	work.length = length;
	work.forward = length;
	work.viewPos = view[0];
	work.step = mTextureScale / static_cast<float>(boltCount);
	if (work.step <= BSE_TIME_EPSILON) {
		work.step = 1.0f / static_cast<float>(boltCount);
	}

	const idVec3 endPos = position + length;
	const idDeclTable* jitterTable = (pt->mElecInfo != NULL)
		? ResolveElectricityJitterTable(pt->mElecInfo->mJitterTableName, mJitterTable)
		: ResolveElectricityJitterTable(idStr(), mJitterTable);
	mJitterTable = jitterTable;
	RenderBranch(effect, &work, position, endPos, jitterTable);

	idVec3 forkBases[BSE_MAX_FORKS];
	const int forks = idMath::ClampInt(0, BSE_MAX_FORKS, mNumForks);
	for (int i = 0; i < forks; ++i) {
		if (work.coordCount > 2) {
			const int idx = rvRandom::irand(1, work.coordCount - 2);
			forkBases[i] = work.coords[idx];
		}
		else {
			forkBases[i] = position;
		}
	}

	for (int i = 0; i < forks; ++i) {
		const idVec3 mid = (forkBases[i] + endPos) * 0.5f;
		const idVec3 forkEnd(
			mid.x + rvRandom::flrand(mForkSizeMins.x, mForkSizeMaxs.x),
			mid.y + rvRandom::flrand(mForkSizeMins.y, mForkSizeMaxs.y),
			mid.z + rvRandom::flrand(mForkSizeMins.z, mForkSizeMaxs.z));

		const idVec3 dir = forkEnd - forkBases[i];
		const float forkLength = dir.LengthFast();
		if (forkLength <= 1.0f || forkLength >= mainLength) {
			continue;
		}

		work.length = dir;
		work.forward = dir;
		work.step = 1.0f / static_cast<float>(GetBoltCount(forkLength));
		RenderBranch(effect, &work, forkBases[i], forkEnd, jitterTable);
	}

	rvRandom::Init(previousSeed);
	return true;
}

void rvElectricityParticle::SetupElectricity(rvParticleTemplate* pt) {
	if (!pt || !pt->mElecInfo) {
		mNumBolts = 0;
		mNumForks = 0;
		mSeed = 0;
		mForkSizeMins.Zero();
		mForkSizeMaxs.Zero();
		mJitterSize.Zero();
		mLastJitter = 0.0f;
		mJitterRate = 0.0f;
		mJitterTable = NULL;
		return;
	}

	const rvElectricityInfo* info = pt->mElecInfo;
	mNumBolts = 0;
	mNumForks = info->mNumForks;
	mSeed = rvRandom::Init();
	mForkSizeMins = info->mForkSizeMins;
	mForkSizeMaxs = info->mForkSizeMaxs;
	mJitterSize = info->mJitterSize;
	mLastJitter = 0.0f;
	mJitterRate = info->mJitterRate;

	mJitterTable = ResolveElectricityJitterTable(info->mJitterTableName, info->mJitterTable);
}

bool rvLightParticle::InitLight(rvBSE* effect, rvSegmentTemplate* st, float time) {
	idRenderWorld* renderWorld = effect ? effect->GetRenderWorld() : NULL;
	if (!renderWorld && session) {
		renderWorld = session->rw;
	}
	if (!effect || !st || !renderWorld) {
		return false;
	}

	rvParticleTemplate* pt = st->GetParticleTemplate();
	if (!pt) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, false)) {
		return false;
	}

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 tint;
	idVec3 size;
	idVec3 position;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size.ToFloatPtr());
	EvaluatePosition(effect, pt, position, time);

	// Retail BSE light particles premultiply RGB by fade before submitting the
	// render light, rather than inheriting unrelated owner shader parms.
	if (effect) {
		const float bright = effect->GetBrightness();
		tint.x *= effect->GetRed() * bright;
		tint.y *= effect->GetGreen() * bright;
		tint.z *= effect->GetBlue() * bright;
		tint.w *= effect->GetAlpha();
	}
	const float fade = Max( 0.0f, tint.w );

	memset(&mLight, 0, sizeof(mLight));
	mLight.origin = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * position;
	mLight.lightRadius.x = Max(1.0f, idMath::Fabs(size.x));
	mLight.lightRadius.y = Max(1.0f, idMath::Fabs(size.y));
	mLight.lightRadius.z = Max(1.0f, idMath::Fabs(size.z));
	mLight.axis = effect->GetCurrentAxis();
	mLight.shaderParms[ SHADERPARM_RED ] = tint.x * fade;
	mLight.shaderParms[ SHADERPARM_GREEN ] = tint.y * fade;
	mLight.shaderParms[ SHADERPARM_BLUE ] = tint.z * fade;
	mLight.shaderParms[ SHADERPARM_ALPHA ] = fade;
	mLight.pointLight = true;
	mLight.detailLevel = 10.0f;
	mLight.noShadows = !pt->GetShadows();
	mLight.noSpecular = !pt->GetSpecular();
	mLight.suppressLightInViewID = effect->GetSuppressLightsInViewID();
	mLight.lightId = LIGHTID_EFFECT_LIGHT;
	mLight.shader = pt->GetMaterial() ? pt->GetMaterial() : declManager->FindMaterial("_default");

	mLightDefHandle = renderWorld->AddLightDef(&mLight);
	mLightRenderWorld = renderWorld;
	return mLightDefHandle != -1;
}

bool rvLightParticle::PresentLight(rvBSE* effect, rvParticleTemplate* pt, float time, bool infinite) {
	idRenderWorld* renderWorld = effect ? effect->GetRenderWorld() : NULL;
	if (!renderWorld) {
		renderWorld = mLightRenderWorld;
	}
	if (!renderWorld && session) {
		renderWorld = session->rw;
	}
	if (!effect || !pt || !renderWorld) {
		return false;
	}

	float evalTime;
	if (!GetEvaluationTime(time, evalTime, infinite)) {
		return false;
	}

	if (mLightDefHandle == -1 || mLightRenderWorld != renderWorld) {
		if (mLightDefHandle != -1 && mLightRenderWorld != NULL) {
			mLightRenderWorld->FreeLightDef(mLightDefHandle);
		}
		mLightDefHandle = renderWorld->AddLightDef(&mLight);
		if (mLightDefHandle == -1) {
			return false;
		}
		mLightRenderWorld = renderWorld;
	}

	const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
	idVec4 tint;
	idVec3 size;
	idVec3 position;
	EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
	EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size.ToFloatPtr());
	EvaluatePosition(effect, pt, position, time);

	// Keep runtime updates in sync with InitLight attenuation semantics.
	if (effect) {
		const float bright = effect->GetBrightness();
		tint.x *= effect->GetRed() * bright;
		tint.y *= effect->GetGreen() * bright;
		tint.z *= effect->GetBlue() * bright;
		tint.w *= effect->GetAlpha();
	}
	const float fade = Max( 0.0f, tint.w );

	mLight.origin = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * position;
	mLight.lightRadius.x = Max(1.0f, idMath::Fabs(size.x));
	mLight.lightRadius.y = Max(1.0f, idMath::Fabs(size.y));
	mLight.lightRadius.z = Max(1.0f, idMath::Fabs(size.z));
	mLight.axis = effect->GetCurrentAxis();
	memset( mLight.shaderParms, 0, sizeof( mLight.shaderParms ) );
	mLight.shaderParms[ SHADERPARM_RED ] = tint.x * fade;
	mLight.shaderParms[ SHADERPARM_GREEN ] = tint.y * fade;
	mLight.shaderParms[ SHADERPARM_BLUE ] = tint.z * fade;
	mLight.shaderParms[ SHADERPARM_ALPHA ] = fade;
	mLight.suppressLightInViewID = effect->GetSuppressLightsInViewID();
	renderWorld->UpdateLightDef(mLightDefHandle, &mLight);
	return true;
}

bool rvLightParticle::Destroy(void) {
	idRenderWorld* renderWorld = mLightRenderWorld;
	if (!renderWorld && session) {
		renderWorld = session->rw;
	}
	if (mLightDefHandle != -1 && renderWorld) {
		renderWorld->FreeLightDef(mLightDefHandle);
	}
	mLightDefHandle = -1;
	mLightRenderWorld = NULL;
	memset(&mLight, 0, sizeof(mLight));
	return true;
}
