local PLUGIN = PLUGIN

PLUGIN.name = "Emergency Sequence System"
PLUGIN.author = "Your Name"
PLUGIN.description = "Adds emergency sequence announcements with audio and screen effects."

PLUGIN.config = {
    soundVolumeGlobal = 75,
    soundVolumeSource = 95,
    soundLevelGlobal = 75,
    soundLevelSource = 95,
    soundDistance = 1500,
    screenShakeAmplitude = 5,
    screenShakeFrequency = 2,
    screenShakeDuration = 15,
    chatPrefix = "**EAS Announcer**",
    chatColor = Color(255, 50, 50),
    eventColor = Color(50, 150, 255),

    hellSound = "emergencyexpansion/hell.wav",
    hellVolume = 70,
    hellLevel = 75,
    hellLoop = true,

    --------------------
    -- FISSURE CONFIG --
    --------------------
    fissureCategories = {
        [1] = {
            scale = 0.5,
            splodeglassCount = 1,
            lifespan = 10,
            items = {}, -- Added for future use
            npcs = {},  -- Added for future use
            props = {}, -- Added for future use
        },
        [2] = {
            scale = 1.0,
            splodeglassCount = 2,
            lifespan = 20,
            items = {},
            npcs = {},
            props = {},
        },
        [3] = {
            scale = 1.5,
            splodeglassCount = 3,
            lifespan = 30,
            items = {},
            npcs = {},
            props = {},
        },
    },
    fissureMinSpawnTime = 1, -- Minimum time since last rift in seconds
    fissureMaxSpawnTime = 5, -- Maximum time since last rift in seconds
    fissureMaxCount = 30, -- Maximum number of rifts that can be spawned automatically

    -- Configuration for enhancing fissure depth and jaggedness
    numPoints = 10, -- Number of vertices for the fissure shape
    angleVariance = 60, -- Degrees of randomness in angle for each vertex
    distanceVariance = 0.8, -- Variability in distance from the center
    vertexOffsetRange = 15, -- Range for vertex movement during oscillation
    starMultiplier = 0.5, -- Multiplier for star distribution depth
    starDepthRange = {1.0, 5.0}, -- Range for star depth multipliers
    numStarsBase = 700, -- Base number of stars
    oscillationAmplitude = 1, -- Amplitude of fissure oscillation
    starSizeClamp = {50, 70}, -- Size range for stars

    earthquakeDuration = 5, -- Duration of the earthquake effect
    flashDuration = 3, -- Duration of the flash effect
    flashMaxAlpha = 255, -- Maximum alpha of the flash (0-255)
    parallaxRatio = 4, -- Set default to 4:1 ratio
    parallaxSmoothFactor = 0.1, -- Smoothing factor for camera movement
    maxParallaxOffset = 100, -- Maximum allowed parallax offset to prevent jitter
}

local SOUNDS = {
    "emergencyexpansion/sequence.wav",
    "emergencyexpansion/warning.wav",
    "emergencyexpansion/emergencytone.wav",
    "emergencyexpansion/tone1.wav",
    "emergencyexpansion/failuredetected.wav",
    "emergencyexpansion/musterpositions.wav",
    PLUGIN.config.hellSound
}

for _, soundPath in ipairs(SOUNDS) do
    util.PrecacheSound(soundPath)
end

function PLUGIN:InitializedChatClasses()
    local PLUGIN = self -- Capture plugin reference for use in chat class

    ix.chat.Register("eas", {
        format = "%s",
        color = self.config.chatColor,
        CanHear = function() return true end,
        OnChatAdd = function(self, speaker, text, bAnonymous, data)
            chat.AddText(PLUGIN.config.chatColor, PLUGIN.config.chatPrefix .. ": ", Color(255, 155, 155), text)
        end,
        description = "Emergency Alert System messages."
    })
end

if SERVER then
    util.AddNetworkString("EmergencySequenceShake")
    util.AddNetworkString("EmergencySequenceOverlay")
    util.AddNetworkString("EmergencySequenceStopShake")
    util.AddNetworkString("EmergencySequenceStopOverlay")
    util.AddNetworkString("EmergencySequenceFlash")

    PLUGIN.audioSourcePos = nil
    PLUGIN.isInEmergency = false
    PLUGIN.activeFissureCount = 0

    -- Updated SEQUENCE_DATA with explicit durations and consistent chatType
    local SEQUENCE_DATA = {
        {
            sound = "emergencyexpansion/warning.wav",
            message = "Warning",
            chatType = "eas",
            repeat_times = 2,
            global = false,
            duration = 1 -- Duration in seconds for warning sound
        },
        {
            delay = 0.5
        },
        {
            sound = "emergencyexpansion/emergencytone.wav",
            message = "Standby for emergency tone",
            chatType = "eas",
            global = false,
            duration = 2 -- Duration in seconds for emergency tone
        },
        {
            delay = 1
        },
        {
            sound = "emergencyexpansion/tone1.wav",
            message = "A shrill emergency tone begins to emit from local loudspeakers",
            chatType = "event",
            global = false,
            duration = 8 -- Duration in seconds for tone1
        },
        {
            sound = "emergencyexpansion/failuredetected.wav",
            message = "A quantum containment failure has been detected.",
            chatType = "eas",
            global = false,
            duration = 3 -- Duration in seconds for failure detected
        },
        {
            sound = "emergencyexpansion/musterpositions.wav",
            message = "Return to emergency muster positions.",
            chatType = "eas",
            global = false,
            duration = 3 -- Duration in seconds for muster positions
        }
    }

    -- Function to play the looping hell.wav
    function PLUGIN:StartHellLoop()
        if self.isInEmergency and self.config.hellLoop then
            for _, ply in ipairs(player.GetAll()) do
                ply:EmitSound(self.config.hellSound, self.config.hellLevel, 100, self.config.hellVolume, CHAN_STATIC)
            end
            -- Schedule next loop after the sound duration
            local soundDuration = 10 -- Assuming hell.wav is 10 seconds long
            timer.Create("HellLoopTimer", soundDuration, 0, function()
                if self.isInEmergency then
                    for _, ply in ipairs(player.GetAll()) do
                        ply:EmitSound(self.config.hellSound, self.config.hellLevel, 100, self.config.hellVolume, CHAN_STATIC)
                    end
                else
                    timer.Remove("HellLoopTimer")
                end
            end)
        end
    end

    function PLUGIN:StopHellLoop()
        timer.Remove("HellLoopTimer")
        for _, ply in ipairs(player.GetAll()) do
            ply:StopSound(self.config.hellSound)
            -- Force stop all channels that might be playing the sound
            ply:EmitSound(self.config.hellSound, 0, 100, 0, CHAN_STATIC)
            ply:EmitSound(self.config.hellSound, 0, 100, 0, CHAN_AUTO)
        end
    end

    function PLUGIN:PlayGlobalSound(soundPath)
        for _, ply in ipairs(player.GetAll()) do
            ply:EmitSound(soundPath, self.config.soundLevelGlobal, 100, self.config.soundVolumeGlobal, CHAN_AUTO)
        end
        return 2 -- Default duration if not overridden
    end

    function PLUGIN:PlayPositionedSound(soundPath)
        if not self.audioSourcePos then
            print("[EmergencyAlert] Audio source position not set.")
            return 2
        end
        sound.Play(soundPath, self.audioSourcePos, self.config.soundLevelSource, 100, self.config.soundVolumeSource)
        return 2 -- Default duration if not overridden
    end

    function PLUGIN:BroadcastMessage(speaker, message, chatType)
        if not IsValid(speaker) then
            speaker = player.GetAll()[1] or nil
        end

        if IsValid(speaker) then
            ix.chat.Send(speaker, chatType, message, true) -- Set bAnonymous to true
        else
            ix.chat.Send(speaker, chatType, message, true) -- Ensure anonymous if no valid speaker
        end
    end

    function PLUGIN:TriggerScreenShake()
        net.Start("EmergencySequenceShake")
        net.Broadcast()
    end

    function PLUGIN:TriggerOverlay()
        net.Start("EmergencySequenceOverlay")
        net.Broadcast()
    end

    function PLUGIN:StopScreenShake()
        net.Start("EmergencySequenceStopShake")
        net.Broadcast()
    end

    function PLUGIN:StopOverlay()
        net.Start("EmergencySequenceStopOverlay")
        net.Broadcast()
    end

    function PLUGIN:PlaySound(data, speaker, callback)
        if data.sound then
            local duration = data.duration or 2 -- Default to 2 seconds if not specified

            if data.global then
                self:PlayGlobalSound(data.sound)
            else
                self:PlayPositionedSound(data.sound)
            end

            if data.message then
                self:BroadcastMessage(speaker, data.message, data.chatType)
            end

            if data.repeat_times and data.repeat_times > 1 then
                local repeatCount = 1
                local function PlayRepeat()
                    if repeatCount >= data.repeat_times then
                        if callback then timer.Simple(duration, callback) end
                        return
                    end
                    repeatCount = repeatCount + 1

                    if data.global then
                        self:PlayGlobalSound(data.sound)
                    else
                        self:PlayPositionedSound(data.sound)
                    end

                    if data.message then
                        self:BroadcastMessage(speaker, data.message, data.chatType)
                    end

                    timer.Simple(duration, PlayRepeat)
                end
                timer.Simple(duration, PlayRepeat)
            else
                if callback then
                    timer.Simple(duration, callback)
                end
            end
        elseif data.delay then
            timer.Simple(data.delay, callback)
        else
            if callback then callback() end
        end
    end

    function PLUGIN:StartEmergencySequence(triggerPlayer)
        if self.isInEmergency then
            print("[EmergencyAlert] Emergency sequence already in progress.")
            return
        end

        self.isInEmergency = true
        self:SaveData()

        -- Play the initial sequence sound globally
        local duration = 10 -- Manually set the duration for the initial sequence sound
        self:PlayGlobalSound("emergencyexpansion/sequence.wav")
        self:TriggerScreenShake()
        self:TriggerOverlay()
        self:StartHellLoop()

        -- Trigger the earthquake and flash effects
        net.Start("EmergencySequenceShake")
        net.Broadcast()
        net.Start("EmergencySequenceFlash")
        net.Broadcast()

        -- Start spawning fissures automatically
        self:StartFissureSpawning()

        timer.Simple(duration, function()
            self:ProcessSequence(1, triggerPlayer)
        end)
    end

    function PLUGIN:StopEmergencySequence()
        if not self.isInEmergency then
            print("[EmergencyAlert] No emergency sequence is active.")
            return
        end

        self.isInEmergency = false
        self:SaveData()
        self:StopHellLoop()
        self:StopScreenShake()
        self:StopOverlay()
        self:StopFissureSpawning()
    end

    function PLUGIN:ProcessSequence(index, triggerPlayer)
        if index > #SEQUENCE_DATA then
            return
        end

        local data = SEQUENCE_DATA[index]
        self:PlaySound(data, triggerPlayer, function()
            self:ProcessSequence(index + 1, triggerPlayer)
        end)
    end

    function PLUGIN:LoadData()
        self.audioSourcePos = self:GetData("audioSourcePos", nil)
        self.isInEmergency = false
    end

    function PLUGIN:SaveData()
        self:SetData("audioSourcePos", self.audioSourcePos)
        self:SetData("isInEmergency", self.isInEmergency)
    end

    -- Admin command to start the emergency
    ix.command.Add("startemergency", {
        description = "Starts the emergency sequence.",
        adminOnly = true,
        OnRun = function(selfCommand, client)
            if not PLUGIN.audioSourcePos then
                return "Audio source position not set. Use /placeaudiosource to set it."
            end
            PLUGIN:StartEmergencySequence(client)
            return "Emergency sequence initiated."
        end
    })

    -- Admin command to stop the emergency
    ix.command.Add("stopemergency", {
        description = "Stops the emergency sequence.",
        adminOnly = true,
        OnRun = function(selfCommand, client)
            PLUGIN:StopEmergencySequence()
            return "Emergency sequence terminated."
        end
    })

    -- Admin command to toggle the emergency state
    ix.command.Add("toggleemergency", {
        description = "Toggles the emergency sequence.",
        adminOnly = true,
        OnRun = function(selfCommand, client)
            if PLUGIN.isInEmergency then
                PLUGIN:StopEmergencySequence()
                return "Emergency sequence terminated."
            else
                if not PLUGIN.audioSourcePos then
                    return "Audio source position not set. Use /placeaudiosource to set it."
                end
                PLUGIN:StartEmergencySequence(client)
                return "Emergency sequence initiated."
            end
        end
    })

    -- Command to set the audio source position
    ix.command.Add("placeaudiosource", {
        description = "Sets the position of the emergency audio source.",
        adminOnly = true,
        OnRun = function(selfCommand, client)
            PLUGIN.audioSourcePos = client:GetPos()
            PLUGIN:SaveData()
            return "Audio source position set."
        end
    })

    -- Command to spawn a fissure manually
    ix.command.Add("spawnrift", {
        description = "Spawn a fissure (rift) at your target surface.",
        adminOnly = true,
        arguments = {
            ix.type.number, -- time (lifespan)
            ix.type.number, -- category
            ix.type.number -- scale (optional)
        },
        OnRun = function(selfCommand, client, time, category, scale)
            local trace = client:GetEyeTrace()
            if not trace.Hit then
                return "You are not looking at a valid surface."
            end

            local position = trace.HitPos
            local normal = trace.HitNormal

            -- Calculate scale based on surface area or use override
            local calculatedScale = scale or PLUGIN:CalculateScaleBasedOnSurface(trace)

            -- Determine orientation based on normal
            local angle = normal:Angle()

            PLUGIN:SpawnFissure(position, category, calculatedScale, time, angle)
            return "Fissure spawned at your target."
        end
    })

    -- Function to calculate scale based on surface area (dynamic scaling)
    function PLUGIN:CalculateScaleBasedOnSurface(trace)
        -- Placeholder for dynamic scaling logic based on surface area
        -- Since exact surface area calculation can be complex, we'll use a default scale
        -- or base it on the player's aim distance

        local defaultScale = 1.0 -- Default scale for human-sized fissures

        -- Example: Scale inversely proportional to the distance from the player
        local distance = trace.HitPos:Distance(trace.StartPos)
        if distance < 100 then
            return 1.5 -- Larger scale for closer surfaces
        elseif distance < 500 then
            return 1.0
        else
            return 0.5 -- Smaller scale for distant surfaces
        end
    end

    -- Function to spawn a fissure
    function PLUGIN:SpawnFissure(position, category, scaleOverride, lifespan, angle)
        category = category or 1
        local categoryData = self.config.fissureCategories[category]
        if not categoryData then
            print("[EmergencyAlert] Invalid fissure category: " .. tostring(category))
            return
        end

        local scale = scaleOverride or categoryData.scale
        lifespan = lifespan or categoryData.lifespan
        local severity = category -- Assuming severity is related to category

        -- Send the effect to the client, including rotation
        local effectdata = EffectData()
        effectdata:SetOrigin(position)
        effectdata:SetScale(scale)
        effectdata:SetMagnitude(lifespan)
        effectdata:SetRadius(severity)
        effectdata:SetAngles(angle or Angle(0, 0, 0)) -- Set angle data for alignment
        util.Effect("rift_effect", effectdata, true, true)

        -- Decrease the active fissure count after lifespan
        timer.Simple(lifespan, function()
            self.activeFissureCount = self.activeFissureCount - 1
        end)
    end   

    -- Function to start spawning fissures automatically
    function PLUGIN:StartFissureSpawning()
        self.activeFissureCount = 0
        self.fissureSpawnTimer = "FissureSpawnTimer"

        local function SpawnNextFissure()
            if not self.isInEmergency then
                return
            end

            if self.activeFissureCount >= self.config.fissureMaxCount then
                -- Don't spawn more fissures
                return
            end

            -- Determine when to spawn the next fissure
            local minTime = self.config.fissureMinSpawnTime
            local maxTime = self.config.fissureMaxSpawnTime
            local nextSpawnTime = math.Rand(minTime, maxTime)

            timer.Create(self.fissureSpawnTimer, nextSpawnTime, 1, function()
                -- Select a random position
                local position = self:GetRandomFissurePosition()
                -- Random category
                local category = math.random(1, #self.config.fissureCategories)
                -- Random angle based on surface normal
                local trace = util.TraceLine({
                    start = position + Vector(0, 0, 1000), -- Start trace above the position
                    endpos = position - Vector(0, 0, 1000),
                    filter = function(ent) return ent:IsSolid() end
                })
                local angle = Angle(0, 0, 0)

                self:SpawnFissure(position, category, nil, nil, angle)
                self.activeFissureCount = self.activeFissureCount + 1

                -- Schedule the next fissure
                SpawnNextFissure()
            end)
        end

        SpawnNextFissure()
    end

    -- Function to stop spawning fissures
    function PLUGIN:StopFissureSpawning()
        timer.Remove(self.fissureSpawnTimer)
    end

    -- Function to get a random fissure position around the map
    function PLUGIN:GetRandomFissurePosition()
        local mapMins, mapMaxs = game.GetWorld():GetModelBounds()
        local attempts = 0
        local maxAttempts = 100

        while attempts < maxAttempts do
            local x = math.Rand(mapMins.x, mapMaxs.x)
            local y = math.Rand(mapMins.y, mapMaxs.y)
            local z = mapMaxs.z - 100 -- Spawn below the map ceiling to avoid outside map errors

            local position = Vector(x, y, z)

            -- Trace down to find the ground
            local trace = util.TraceLine({
                start = position,
                endpos = position - Vector(0, 0, 10000),
                mask = MASK_SOLID_BRUSHONLY
            })

            if trace.Hit and not trace.HitSky then
                -- Adjust position to be a certain height above ground
                local groundPos = trace.HitPos
                local spawnHeight = groundPos.z + math.Rand(100, 200)
                local spawnPos = Vector(x, y, spawnHeight)

                -- Ensure the position is not inside a wall
                local hullTrace = util.TraceHull({
                    start = spawnPos,
                    endpos = spawnPos,
                    mins = Vector(-16, -16, -16),
                    maxs = Vector(16, 16, 16),
                    mask = MASK_SOLID_BRUSHONLY
                })

                if not hullTrace.Hit then
                    return spawnPos
                end
            end

            attempts = attempts + 1
        end

        -- If no valid position found, default to a safe position
        return Vector(0, 0, mapMaxs.z - 500)
    end
end

if CLIENT then
    -- Client-side code for special effects

    net.Receive("EmergencySequenceShake", function()
        -- Trigger the earthquake effect
        local duration = PLUGIN.config.earthquakeDuration or 5
        util.ScreenShake(LocalPlayer():GetPos(), 5, 5, duration, 500)
    end)

    net.Receive("EmergencySequenceFlash", function()
        -- Trigger the flash effect
        local duration = PLUGIN.config.flashDuration or 3
        local maxAlpha = PLUGIN.config.flashMaxAlpha or 255

        -- Create a screen overlay with a white color that fades out
        local startTime = CurTime()
        hook.Add("HUDPaint", "EmergencySequenceFlashEffect", function()
            local elapsed = CurTime() - startTime
            if elapsed > duration then
                hook.Remove("HUDPaint", "EmergencySequenceFlashEffect")
                return
            end
            local alpha = maxAlpha * (1 - (elapsed / duration))
            surface.SetDrawColor(255, 255, 255, alpha)
            surface.DrawRect(0, 0, ScrW(), ScrH())
        end)
    end)

    -- Define the custom fissure effect
    do
        local EFFECT = {}

        EFFECT.Mat_Black = Material("dev/black_cheap")
        EFFECT.Mat_Star = Material("sprites/animglow02")

        function EFFECT:Init(data)
            self.Position = data:GetOrigin()
            self.Scale = data:GetScale() * 1600 -- Adjust scale multiplier as needed
            self.LifeTime = CurTime() + data:GetMagnitude()
            self.Severity = data:GetRadius()

            -- Get rotation angle for alignment
            self.Angle = data:GetAngles()
            self.Normal = self.Angle:Forward()

            -- Initialize previous camera position for parallax effect
            self.PreviousEyePos = EyePos()
            self.ParallaxOffset = Vector(0, 0, 0) -- Initialize parallax offset

            -- Access configuration variables
            local numPoints = PLUGIN.config.numPoints or 40
            local angleVariance = PLUGIN.config.angleVariance or 60 -- Degrees
            local distanceVariance = PLUGIN.config.distanceVariance or 0.3 -- 30%
            local vertexOffsetRange = PLUGIN.config.vertexOffsetRange or 15
            local starMultiplier = PLUGIN.config.starMultiplier or 4.2
            local starDepthRange = PLUGIN.config.starDepthRange or {1.0, 4.0}
            local numStarsBase = PLUGIN.config.numStarsBase or 200
            local oscillationAmplitude = PLUGIN.config.oscillationAmplitude or 7

            -- Generate the fissure shape with increased jaggedness
            self.Vertices = {}
            self.VertexOffsets = {}
            local angleStep = 360 / numPoints

            -- Create a rotation matrix based on the fissure's angle
            local rotation = Matrix()
            rotation:Rotate(self.Angle)

            for i = 1, numPoints do
                -- Increased randomness in angle and distance for more jagged spikes
                local angleOffset = math.random(-angleVariance / 2, angleVariance / 2)
                local angle = math.rad((i - 1) * angleStep + angleOffset)
                local distance = self.Scale + math.random(-self.Scale * distanceVariance, self.Scale * distanceVariance)
                local x = math.cos(angle) * distance
                local y = math.sin(angle) * distance
                local vertPos = Vector(x, y, 0)
                vertPos:Mul(rotation) -- Apply rotation
                vertPos = self.Position + vertPos
                table.insert(self.Vertices, vertPos)
                table.insert(self.VertexOffsets, Vector(math.random(-vertexOffsetRange, vertexOffsetRange), math.random(-vertexOffsetRange, vertexOffsetRange), 0))
            end

            -- Generate parallaxed stars inside the fissure with increased depth
            self.Stars = {}
            local numStars = numStarsBase + self.Severity * starMultiplier

            for i = 1, numStars do
                table.insert(self.Stars, {
                    Offset = VectorRand() * self.Scale * starMultiplier,
                    Depth = math.Rand(starDepthRange[1], starDepthRange[2]),
                    TwinkleSpeed = math.Rand(1, 5),
                    Color = Color(255, 255, 255)
                })
            end

            -- Generate nebula clouds
            self.Nebulas = {}
            local numNebulas = math.floor(numStars / 100) -- Adjust ratio as needed

            for i = 1, numNebulas do
                table.insert(self.Nebulas, {
                    Offset = VectorRand() * self.Scale * starMultiplier,
                    Depth = math.Rand(starDepthRange[1], starDepthRange[2]),
                    Size = math.Rand(100, 300),
                    Color = Color(math.random(50, 255), math.random(50, 255), math.random(50, 255), math.random(100, 200)),
                    Opacity = math.Rand(50, 150)
                })
            end
        end


        function EFFECT:Think()
            return CurTime() < self.LifeTime
        end

        function EFFECT:Render()
            -- Access configuration variables
            local oscillationAmplitude = PLUGIN.config.oscillationAmplitude or 7
            local starSizeClamp = PLUGIN.config.starSizeClamp or {30, 150}
            local parallaxRatio = PLUGIN.config.parallaxRatio or 4
            local parallaxSmoothFactor = PLUGIN.config.parallaxSmoothFactor or 0.1
            local maxParallaxOffset = PLUGIN.config.maxParallaxOffset or 100

            -- Calculate camera movement delta
            local currentEyePos = EyePos()
            local delta = currentEyePos - self.PreviousEyePos
            self.PreviousEyePos = currentEyePos

            -- Define parallax offset along the fissure's normal only
            local parallaxOffset = delta:Dot(self.Normal) * self.Normal * (parallaxRatio - 1)

            -- Smooth the parallax offset
            self.ParallaxOffset = LerpVector(parallaxSmoothFactor, self.ParallaxOffset, parallaxOffset)

            -- Clamp the parallax offset to prevent jitter
            self.ParallaxOffset = Vector(math.Clamp(self.ParallaxOffset.x, -maxParallaxOffset, maxParallaxOffset),
                                         math.Clamp(self.ParallaxOffset.y, -maxParallaxOffset, maxParallaxOffset),
                                         math.Clamp(self.ParallaxOffset.z, -maxParallaxOffset, maxParallaxOffset))

            -- Adjust the fissure's black shape based on oscillation
            local animatedVertices = {}
            for i, vert in ipairs(self.Vertices) do
                local offset = self.VertexOffsets[i]
                local oscillation = math.sin(CurTime() * (self.Severity + 2) + i) * self.Severity * oscillationAmplitude
                local animatedVert = vert + offset * oscillation
                table.insert(animatedVertices, animatedVert)
            end

            -- Step 1: Draw the fissure's black shape first
            render.SetMaterial(self.Mat_Black)
            cam.Start3D()
                mesh.Begin(MATERIAL_POLYGON, #animatedVertices)
                    for _, vert in ipairs(animatedVertices) do
                        mesh.Position(vert)
                        mesh.Color(0, 0, 0, 255)
                        mesh.AdvanceVertex()
                    end
                mesh.End()
            cam.End3D()

            -- Step 2: Setup the stencil buffer for the fissure shape
            render.ClearStencil()
            render.SetStencilEnable(true)
            render.SetStencilWriteMask(255)
            render.SetStencilTestMask(255)
            render.SetStencilReferenceValue(1)
            render.SetStencilFailOperation(STENCILOPERATION_KEEP)
            render.SetStencilZFailOperation(STENCILOPERATION_KEEP)
            render.SetStencilPassOperation(STENCILOPERATION_REPLACE)
            render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_ALWAYS)
            render.SetBlend(0) -- Don't render to the screen during stencil drawing

            -- Draw the fissure shape into the stencil buffer
            cam.Start3D()
                mesh.Begin(MATERIAL_POLYGON, #animatedVertices)
                    for _, vert in ipairs(animatedVertices) do
                        mesh.Position(vert)
                        mesh.AdvanceVertex()
                    end
                mesh.End()
            cam.End3D()

            render.SetBlend(1) -- Restore normal rendering

            -- Step 3: Configure stencil to only allow drawing within the fissure
            render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_EQUAL)
            render.SetStencilPassOperation(STENCILOPERATION_KEEP)

            -- Render the stars constrained to the fissure area with parallax offset
            render.SetMaterial(self.Mat_Star)
            local eyePos = EyePos()
            for _, star in ipairs(self.Stars) do
                -- Apply parallax offset to star position
                local starPos = self.Position + star.Offset * star.Depth + self.ParallaxOffset
                local distance = eyePos:Distance(starPos)
                -- Scale star size based on fissure scale
                local baseScale = 1.0
                local size = math.Clamp((70000 / (distance ^ 2)) * (self.Scale / baseScale), starSizeClamp[1], starSizeClamp[2])
                local alpha = 200 + 55 * math.sin(CurTime() * star.TwinkleSpeed)
                render.DrawSprite(starPos, size, size, Color(star.Color.r, star.Color.g, star.Color.b, alpha))
            end

            -- Render the nebulas constrained to the fissure area with parallax offset
            render.SetMaterial(Material("particle/particle_noisesphere")) -- Use a suitable material for nebulas
            for _, nebula in ipairs(self.Nebulas) do
                local nebulaPos = self.Position + nebula.Offset * nebula.Depth + self.ParallaxOffset
                local distance = eyePos:Distance(nebulaPos)
                local size = math.Clamp(nebula.Size / (distance ^ 1.5), 4500, 10500) -- Adjust size scaling as needed
                render.DrawSprite(nebulaPos, size, size, Color(nebula.Color.r, nebula.Color.g, nebula.Color.b, nebula.Opacity))
            end

            -- Disable stencil after drawing
            render.SetStencilEnable(false)
        end        

        effects.Register(EFFECT, "rift_effect")
    end
end

when /spawnrift is used on a wall, it's not aligning properly. Remember, a normal on a wall should be closer to (x, x, 1) since that makes it vertical. But I want this to be able to be aligned to any surface. So you have to look through and get the angles yourself FROM THE SURFACE YOU'RE LOOKING AT. So if it's on a ceiling, it adjusts appropriately to be on the ceiling, on a wall and on the wall ect.

Additionally, with the /spawnrift command, the stars are HUGE probably because of how close we are. It may need to pass its own depth and density calculations to look proper. It's up to you to find the correct way to do this. 

The stars are still popping out of the fissure. They are not PHYSICALLY outside of the boundaries on rendering, but for an example I can noclip and actually touch a star if I get close enough to the rift. That should never be the case. It should be like I'm looking INTO the rift, not things popping OUT of the rift. Here's how the logic should go.

Stars for parallax parllax BEYOND the fissures point with the goal being to make it look like you're peering into another galaxy.

Additionally, with the /spawnrift command, the stars are HUGE probably because of how close we are. It may need to pass its own depth and density calculations to look proper. It's up to you to find the correct way to do this.


Give me the full code.  I also want them (the atuomatically spawned ones) to start out small and the expand to their full size over a configured time like (config.riftgrowth 2, 10) as an example for between 2 to 10 seconds. It should be per rift category. Remember, after making the changes do a full code review to ennsure everything is good and remove anything that's redundant (do not remove placeholders). Do not forget the angle calculations for the automatically spawned ones should be (?, ?, 0) so it isn't flipped. The (0, 0, 1) was only for an example to aligning to a vertical wall. You keep giving me code with this inserted.