<aside> ⏰

The PLAYER script is here in three parts and finally the effect is placed via event to actor script

you can copy paste these in order to single script except the actor one.

</aside>

<aside> ⚙

the actor script is simply overpowered diminishing fatigue for test

local types = require('openmw.types')
local self = require('openmw.self')

local function modification()  
    types.Actor.stats.dynamic.fatigue(self).current = types.Actor.stats.dynamic.fatigue(self).current - 100
end

return { eventHandlers = { modification = modification } } 

</aside>

reguireds and start info

local core = require('openmw.core')
local I = require('openmw.interfaces')
local self = require('openmw.self')
local nearby = require('openmw.nearby')
local util = require('openmw.util')
local camera = require('openmw.camera')
local time = require('openmw_aux.time')
local types = require('openmw.types')
local ui = require('openmw.ui')

local count = 0 -- counting init
local R = 250  -- the circle radius
local end_seconds = 10
local used_spell = "fire bite"

the trigger spellcast, conditions

I.AnimationController.addTextKeyHandler('spellcast', function(groupname, key)

    if key.sub(key, #key - 6) == 'release' then
        
      -- spell to use
      if types.Actor.getSelectedSpell(self).id == used_spell then
      
      -- vector to screen's center
      local centervect = camera.viewportToWorldVector(util.vector2(0.5,0.5)):normalize()
      local cam_pos = camera.getPosition()
       
      -- getting the terrain or water collision  
        hitpos = nearby.castRay(cam_pos , cam_pos + centervect * 2000, 
                        { ignore = self, 
                          collisionType = nearby.COLLISION_TYPE.HeightMap + nearby.COLLISION_TYPE.Water }
                         ).hitPos
                         
      --print(hitpos) -- debug
     
      -- not in interiors for landheight usage
      if self.cell.isExterior == false then
        ui.showMessage("not inside")
      end
   
      --condition to start the spell, exteriors only
      if count == 0 and hitpos and self.cell.isExterior then 
        timer()    
      end   
    
    end
  end
end)

effects, relative to circle

function effects() -- effects to do
              local acts = nearby.actors
                for a, _ in pairs(acts) do
                
          -- for actors inside the circle
                  if (hitpos - acts[a].position):length() < R then
           
          -- the effect
                    acts[a]:sendEvent("modification")
                    print(acts[a].recordId)
                  end 
                end
end

timer for vfx spawns and effects

function timer() -- timer function to spawn vfx

   -- spell effect and it's model to use fo spawn
    local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.FireDamage].hitStatic
    local model = types.Static.records[effect].model
   
   -- timer itself     
    stop = time.runRepeatedly(function()
        print("time", count)
   
   -- counting to stop timer at certain amount
        count = count + 1 
   
   -- for eight spawns, 45 * 8 = 360 circle
        for i= 1 , 8 do     
   
   -- x and y by degrees to radians       
          local x = R * math.cos(math.rad(45)*i)
          local y = R * math.sin(math.rad(45)*i)
   
   -- start of the using x,y for positions
          local trans = util.transform
          local rotate = trans.rotateZ(self.rotation:getYaw()) * util.vector3(x, y ,0)
   
   -- the logic for z height in exterior, interior might need castray.       
          local z      
          local land = core.land.getHeightAt(hitpos + rotate, self.cell)
            if land < self.cell.waterLevel then 
              z = self.cell.waterLevel          
            else 
              z = land 
            end 
    
   -- final spawn positions and the vfx             
           local pos = util.vector3(hitpos.x,hitpos.y,z) + rotate            
           core.sendGlobalEvent('SpawnVfx', {model = model, position = pos })
         end
 
   -- the effect(s) is in separate function
   -- also the determining inside of the circle position 
   -- you can do also other resultng effects here
        effects()
  
   -- stops the timer after 10 seconds and allows new casting
      if count > end_seconds then
        stop()
        count = 0
      end
   
   -- seconds per spawn 
      end, 1 * time.second)
end