#free example library of lua for openmw modding

1 messages ยท Page 1 of 1 (latest)

cobalt token
#

hi.
have been making several scripts over the time and thought time ago about making tutorial library out of them and also the ideas from myself and people.
not sure if this is the right place but idk others. making this thread as there's a lot ground to cover in writing and keep the flame up in updating and interaction.

the basic structure shaping up at notion atm https://www.notion.so/220c0a3cd260804f877af4a1185ffa38?v=27ac0a3cd26080bb9799000c1ce4ea10
( atm lot's of empty pages and some scripts ) ( live updates ๐Ÿ˜… )

and in the end probably gonna be gitlab https://gitlab.com/sjek/lua_coding/-/tree/main/OpenMW-lua as for a codebase it's kinda the best.

somebody integrating something solid to openmw docs is possible but time will tell of the shape of it.

i'm learning to code at the same time so there's syntax and all
but keeping it relatively simple to use, so for the more advanced this would be more like quick start and basic preference.

current state is that need to start test and copy paste, write and clean the code to notion from computers and gitlab and can make some new ones dunmerchu

i think luckily / by practise the packages are grouped around functionality so keeping that somewhat is the cleanest and most intuitive way.
altho not strickly as the events, local, global shuffling make it's way to gameplay. like raycast have it's usability, and AI and the animations.
grouping with more advanced functionality is possible along the way.

any thoughts .?

knowing tho that there are lot's of mods what to use as a preference when making one yagrwut

contributions welcome.
this is currently a bit building the framework and discuss / suggest the future and precent content colovian

Simo K.โ€™s Notion on Notion

ideas and code for morrowind or more specifically openmw

cobalt token
#

like this one, making actor invisible when under knockout with certain amulet or gear

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

    local spell = "Invisibility" -- "chameleon" 
    local gear = "common_amulet_05" 
    -- might need equipment splot check sometimes for use case

    local equips = types.Actor.getEquipment(self)
    for c, _ in pairs(equips) do
      if equips[c].recordId == gear and groupname == "knockout" then 

      types.Actor.activeSpells(self):add({ id = spell, effects = {0}}) --fifth barrier --Invisibility
      
      print("knockout")
      
      end  
      if anim.isPlaying(self,"knockout") == false then
      
      types.Actor.activeEffects(self):remove(spell)

      end
    end
end)  

which works but

      local spells = types.Actor.activeSpells(self)
      for a, _ in pairs(spells) do
        print(spells[a])
      end

is giving me only nils tho print(types.Actor.activeSpells(self):isSpellActive(spell)) gives true
so need to remove whole effect or it's the needed temporary flag on the spell.

#

and a note that spell and effect id isn't always the same so maybe other use case

#

every use case has it's own conditions so can be pointed out for the cancel trigger too
edit: and

     local effects = core.magic.spells.records[spell].effects
       for i=1, #effects do   
        types.Actor.activeEffects(self):remove(effects[i].id)
       end
lost brook
#

Your work will help making a lot of funky mods for sure

cobalt token
#

That's the goal xD
Once you get accustomed to whole event / interface / callback system and what can be used to get stuff it's kinda straigforward to do "small" stuff like giving items, change clothing, walls, sounds, animations, summons, stats and such game event's against reading books, activation in general, meeting ghosts, quests triggers, being in specifig area, cells, etc. Like what for mwscript shines but have it's limitations.

Only that there's storage matter about dozen scripts with bigger mods but i think it will be sorted out in some time. (Or stays colovian )

Have been fun to make scripts as going to you can change majority of the game already. And despite implementation details the lua is kinda sane

oblique magnet
#

this sounds really cool, thanks! I look forward to looking through it

cobalt token
cobalt token
#

one scenario is to spam hit with fatigue or health loss to nearby actors on animation but it's really brutal and op kinda ๐Ÿ˜…

cobalt token
#

found out the problem in

      local spells = types.Actor.activeSpells(self)
      for a, _ in pairs(spells) do
        print(spells[a])
      end

you need to do it mapping the list item ie.

      local spells = types.Actor.activeSpells(self)
      for _, b in pairs(spells) do
        print(b)
      end

for some reason the index are nil

cobalt token
#

and how to hit the terrain in general

#

..... the logic is off by if result.hitObject == nil then todd

cobalt token
#

at least was xD
i need to group these along the way for basics and bigger mod use
like changing everybodys clothing upon activating something uses
more of api than simple example

#

TODO: invent better functionality yagrwut

#

using templating is not necessarily needed on this one tho

#

if not some enchantments or something

cobalt token
cobalt token
cobalt token
#

interesting find,
as the OnHitHandler doesn't seemingly work for spells
it could make the victim invulnerable to any physical attack
or with other conditions like the damage here.
also maybe some proof on the true check

local types = require('openmw.types')
local I = require('openmw.interfaces')

I.Combat.addOnHitHandler(function(attack)
  --if attack.attacker.type == types.Player then
   
    if attack.weapon then print(attack.weapon.recordId) end
    print(attack.damage.health)
    
    if attack.damage.health and attack.damage.health < 40 or attack.damage.fatigue < 40 then
    
        attack.successful = false
    end
   return false
  --end
end)
lost brook
cobalt token
#

can register getting an attack but registering delivering attack with other values in same time is complicated.

doing I.Combat.onHit{ attacker = attack.attacker, strenght = 1, damage = { health = 0, fatigue = 0, magicka = 20 } }
result to stackoverflow

or more specifically with

    if attack.damage.health and attack.damage.health > 5 then
      attack.successful = false
      if attack.attacker.recordId == "player" then
        
      I.Combat.onHit{ attacker = attack.attacker,  strenght = 1,  damage = { health = 10, fatigue = 0, magicka = 0 } }

when { health = 10, value is higher than the checked one...

i reckon that this is intricate and the logic needs to be just right.

ie. getting hit with onhithandler results easily to loop from the victim and log spam.

basically this happens because script is on both attacker and victim. ie. NPC, PLAYER

other way is to modify stats directly ie. event in same script

local function damages()
    types.Actor.stats.dynamic.health(self).current = types.Actor.stats.dynamic.health(self).current - 5
end

return { eventHandlers = { damages = damages }  } 
cobalt token
#

.... that I.Combat.onHit{ attacker = attack.attacker
is looping because of attacker = attack.attacker presumably.

the whole addonhithandler triggers at victim end so that
attacker does over the limit health damage back which means that
onhithandler is triggered ๐Ÿ˜…

#

.
or well attacker = attack.attacker instakills the attacker
and attacker = self the target because of loop
but onHit kinda cannot's be then used without some safeguards

#

same happens with attack.attacker:sendEvent('Hit', table)

#

this syntax can be neat for handlers generally

    if attack.damage.health and attack.damage.health > 6 then

      return true, 
            attack.attacker:sendEvent("damages")
      else return false
    end
limber oyster
#

This is very cool, thanks, definitely helps to have a library to at least get some foundation from.

cobalt token
#

thanks, here's so many use cases that all would be kinda impossible colovian
but for adding things there should be structure as these are a bit cryptic without.

like now with this

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

    local spell_id = "night-eye"

    if key.sub(key, #key - 6) == 'release' and types.Actor.getSelectedSpell(self).id == spell_id  then 
        print("nightime")
        
        local cell = self.cell.id
          
        core.sendGlobalEvent("sjek_add_stuff", { cell = cell, player = self })
          
          local name_callback = async:registerTimerCallback('arbitary name',
                function()
                    core.sendGlobalEvent("sjek_remove_stuff", { cell = cell, player = self })
                end)
          
          local effects = core.magic.spells.records[spell_id].effects
          local duration = effects[1].duration
          
          async:newSimulationTimer( duration , name_callback) 
         
    end
end)

i can shuffle saving the cells in but writing points at least about durations. (and of the cell saving)
and a list about different things to add and vfx

the timer is kinda better than onUpdate state machine.
otherwise sending state with an event to global maybe.

mod makers need to handle the saving, loading so that's kinda important.

#

.
and is this the best way but got axes turning above actors head while turned tip to sky with this

local function weapon(event)
      table.insert(nearbys, event.nearby_actor)
      
      local axe = world.createObject("daedric war axe",1)
      axe:teleport(event.nearby_actor.cell,event.nearby_actor.position, axe.rotation * util.transform.rotateX(-46)) 
                      --*3.14/360
      table.insert(axes, axe)
      jos = 1
end

local function onUpdate()
   if jos == 1 then
     for i, _ in pairs(axes) do
            local tab = nearbys[i]                          
            if axes[i] then
                axes[i]:teleport(
                          tab.cell, 
                          tab.position + util.vector3(0,0,200), 
                          axes[i].rotation*util.transform.rotateY(0.1) 
                          )
            end
      end
   end
end

or close to tip to sky as that's kinda tested value and idk about those really ๐Ÿ˜…

cobalt token
#

if wanting to save cells in table this seems to work

local cells

local function cellsave(cell)
    if cells == nil then
       cells = {}
       table.insert(cells,cell)
       print("add first", cell)    
    else
      for i = 1, #cells do
        print("already", cells[i])
      end  
      if not cells[cell] == true then
        table.insert(cells,cell)
        print("add table", cell)
      end
    end
    return cells
end

cells = cellsave(self.cell.id)

tho there's probably way to use under the hood timer itself, greatly simplifying the code.

cobalt token
#

can be done like this

          local name_callback = async:registerTimerCallback('arbitary name',
                function(data)
                    core.sendGlobalEvent("sjek_remove_stuff", { cell = data.cell })
                    print("remove", data.cell)
                end)
          
          local effects = core.magic.spells.records[spell_id].effects
          local duration = effects[1].duration
          
          async:newSimulationTimer( duration , name_callback, {cell = self.cell.id}) 

ie. you can inject info for timer, for the time of execution. basically info in the time of calling.
data is just a keyword and works just like events but with delay on site.
this should work for any actor.

#

ie: kinda like this if defining object but needs to choose the object type manually for removal atm.

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

    local spell_id = "night-eye"
    local object_id = "active_bubbles00"

    if key.sub(key, #key - 6) == 'release' and types.Actor.getSelectedSpell(self).id == spell_id  then 
        print("nightime")
        
        core.sendGlobalEvent("sjek_add_stuff", { cell = self.cell.id, object_id = object_id })
        
          local name_callback = async:registerTimerCallback('arbitary name',
                function(data)
                    core.sendGlobalEvent("sjek_remove_stuff", { cell = data.cell, object_id = data.object_id }) 
                    print("remove", data.cell)
                end)
          
          local effects = core.magic.spells.records[spell_id].effects
          local duration = effects[1].duration
          
          async:newSimulationTimer( duration , name_callback, {cell = self.cell.id, object_id = object_id}) 
    end
end)
#

VFX is a bit more complicated for looping and remove, for the start

#

probably juggling with global events

cobalt token
#

this is on timer and needs better logic for start position but up and down simulation basically

local time = require('openmw_aux.time')
local util = require("openmw.util")
local core = require('openmw.core')

local function sjek_hop(event)
  local timer = 0
  local pos = event.container.position.z
  time.runRepeatedly(function() 
    timer = timer + 0.1 
    event.container:teleport(event.container.cell,event.container.position + util.vector3(0,0, (10*timer - 4.9*timer^2) ))
    if event.container.position.z < pos then
      timer = 0
    end
  end, 0.1*time.second)  
end

return {
    eventHandlers = { sjek_hop = sjek_hop }
}

maybe simplifying still

#
Simo K.โ€™s Notion on Notion

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

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

elfin prairie
cobalt token
cobalt token
#

like. this implements archimede's spiral

    local count = 1 -- start
    local top_amount = 200  
    local summon_pace = 0.04 * time.second

      local positio = var.event_origin.position
      local rotatio = util.transform.rotateZ(1)
      local direction = util.transform.rotateZ(var.event_origin.rotation:getYaw()) 

      local speed = 5
      local angular_speed = 0.1

      -- timer
      stop = time.runRepeatedly(function()
          
          if count < top_amount then
            count = count + 1
          
            local summon_obj = world.createObject( summon_obj_id , 1)
                        
            local x = speed * count * math.cos(angular_speed * count)
            local y = speed * count * math.sin(angular_speed * count)
            local z = 1 * count
              
            local spiral = util.vector3(x,y,z)
                        
            summon_obj:teleport( var.event_origin.cell, positio + 
                            direction * spiral, 
                            rotatio * direction )
                                 
            world.vfx.spawn( vfx_model, positio + direction * spiral)
             
     end, summon_pace)
#

The Archimedean spiral (also known as Archimedes' spiral, the arithmetic spiral) is a spiral named after the 3rd-century BC Greek mathematician Archimedes. The term Archimedean spiral is sometimes used to refer to the more general class of spirals of this type (see below), in contrast to Archimedes' spiral (the specific arithmetic spiral of Arch...

#

altho this might be more costly than one without so much math

#

and need to figure out the rotatio for placing tip away from player

cobalt token
#

which seems kinda taunting as i need to get the rotation set before the object is placed which means that presumably need to get the spiral equation into account. probably something simple tho

cobalt token
cobalt token
lost brook
#

I want to say again thank you for your work

cobalt token
# lost brook I want to say again thank you for your work

thanks. i'm coming from mwscript so have that perspective ๐Ÿ˜…
tho it's possible that people have to find their way around which package contains what altho wrote some general guidelines and the grouping is kinda by usage.

lost brook
#

Well, the magic is in finding the workarounds to the limited apis ๐Ÿ˜„

cobalt token
#

yep.

cobalt token
# cobalt token like. this implements archimede's spiral ```lua local count = 1 -- start ...

some AI help and vector math and worked out.
one lesson is look at what the rotation rotates ie.
the original direction from getYaw was affecting all along so i set it to zero and rotations work

altho i'm not totally undestanding how this works

      local direction = util.transform.rotateZ(0) 
      
      local speed = 5
      local angular_speed = 0.1

      -- timer
      stop = time.runRepeatedly(function()
          
          if count < top_amount then
            count = count + 1
          
            local summon_obj = world.createObject( summon_obj_id , 1)
                        
            local x = speed * count * math.cos(angular_speed * count)
            local y = speed * count * math.sin(angular_speed * count)
            local z = 1 * count
         
        -- spiral     
            local spiral = util.vector3(x,y,z)
        -- placement points    
            local placement = positio + direction * spiral
        -- angle around player    
            local sp = math.atan2(y,x)

        -- directions change math around caster position, how this works even ^^   
            local new_x = positio.x * math.cos(sp) - positio.y * math.sin(sp)
            local new_y = positio.x * math.sin(sp) + positio.y * math.cos(sp)
       
        -- the axe direction     
            local new_sp = - math.atan2(new_y,new_x)
        
        -- doing transform with gotten angle    
            local rotation = util.transform.rotateZ( new_sp )
                                
            summon_obj:teleport( var.event_origin.cell, placement , rotation ) 
                                        
            world.vfx.spawn( vfx_model, positio + direction * spiral)             
cobalt token
#

maybe something in the line of direction cosines but different form. need to fiddle a bit. trigonometry for sure

#

and for ball not really much as the screen is 2D but can be done with this.

        local R = 200
        
        for i= 1 , 50 do     
          
          local theta = math.acos( 1 - 2 * i/50) 
          local phi = i

          local x = R * math.sin(theta) * math.cos(phi)
          local y = R * math.sin(theta) * math.sin(phi)
          local z = R * math.cos(theta)
          
          
          local pos = self.position + util.vector3(x,y,z)  

tho phi value is important for distribution and could do some patterncolovian

wary forge
cobalt token
#

apparently there's several by the fandom xD

#

with cylindrical coordinates

        for i= 1 , 50 do     
          
          local R = 10 + 3*i
          local theta = i 
          
          local x = R * math.cos(theta)
          local y = R * math.sin(theta)
          --local z = self.position.z

          local pos = self.position + util.vector3(x,y,0)  
#

would say a bit easier than editing nifs on simple cases yagrwut

#

animating with timers definitely hit the framerates, very probably

cobalt token
#
        local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.FireDamage].areaStatic
        local model = types.Static.records[effect].model
        
     local t = 0
        
     local stop = time.runRepeatedly(function() 
        t = t + 1
        for i= 1 , 100 do     
          
          local R = 10 + 3*i
          local theta = i + t*15*3.14/360 
          
          local x = R * math.cos(theta)
          local y = R * math.sin(theta)
          local z = theta*i*0.1

          local pos = self.position + util.vector3(x,y,z)  
           
          core.sendGlobalEvent('SpawnVfx', {model = model, position = pos })
        end
        end, 0.2 * time.second)
#

there's the tornado cylindrical. note tho that this is somewhat expensive

cobalt token
#

first time needed to use functions a bit more for readability and use and worked quite well

ie. for making spell circle on castray hitpos out of vfx spawns.
this could be fine tuned still for using interiors altho getting the land height can be tricky one.
i think the timer runrepeat is the safest way of doning this for many spawns
and somehow.. i haven't figured out why, you can change the the position mid duration.
for shuffling function positions is script the sandboxing is kinda neat.
tho idk about local / global performnce hit.

have to get myself to fine tune also exixting ones yagrwut
https://www.notion.so/Magic-circle-vfx-and-fatigue-loss-2d9c0a3cd26080a8ad21c6a13c4cc0fe

Simo K.โ€™s Notion on Notion

reguireds and start info

#

- 100 knockouts quite fast

#

for exteriors there's also the check for land height and water surface

         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 
lost brook
#

Learning something new with you as always ๐Ÿ™Œ

cobalt token
#

well, it's simply
types.Actor.stats.dynamic.fatigue(self).current = types.Actor.stats.dynamic.fatigue(self).current - 100 ๐Ÿ˜…
tho there's the point that knockout happens somewhere around 0
but the fatigue goes negative after that and need to raise above to stand up again

#

probably the damage / drain spells too tho

lost brook
#

No I mean the animation speed, with Devilish Sleep Script its -40 and the anim is like natural of the npc dropping down but in the vid you posted its like 3 times quicker ๐Ÿ˜„

cobalt token
#

must be camera angle or the video .p
having this with
anim.playQueued(self, 'knockout', { loops = 0, speed = 1 })

cobalt token
# wary forge This looks like Dragon Ball's ki blasts <:36vehks:559827884382879762>

https://2e.aonprd.com/Spells.aspx?ID=2055

You unleash your qi as a powerful blast that deals 2d6 force damage. If you use 2 actions to cast qi blast, increase the size of the cone to 30 feet and the damage to 3d6. If you use 3 actions to cast qi blast, increase the cone to 60 feet and the damage to 4d6. Each creature in the area must attempt a Fortitude save.

Critical Success The creature is unaffected.
Success The creature takes half damage.
Failure The creature takes full damage and is pushed 5 feet.
Critical Failure The creature takes double damage and is pushed 10 feet.

how about this .?
the cone could be hard to do but knockback and simple vfx are doable. tho the question is how to charge the spell or taking some incredients from inventory... making 3 different spells is kinda clunky.
at least the spells are covered xD

cobalt token
#

and this might be good example shot altho a bit complicated but need timercolovian
https://2e.aonprd.com/Spells.aspx?ID=2298

@lost brook can i mimic your line of sight code .?
i'm kinda loss in vector math on that.

lost brook
cobalt token
# lost brook You are free to use any part of SHOPs code ๐Ÿค๐Ÿ™Œ

Thanks, i'll get back to this shortly. Need to write simpler ones and shuffle handlers better.
Got the water detection working altho the logic detecting if let's say only 1 of 8 points hold true flip the state while 7 of 8 doesn't.

things like stealing spells and common detection ways.
Then should handle some settings too.

Spells along the way xD

cobalt token
# lost brook You are free to use any part of SHOPs code ๐Ÿค๐Ÿ™Œ

got the code to work.
didn't really understand the cosine so replaced it with this

local function is_in_cone(caster, target,  cone_angle_deg, max_range)
    
    local toPlayer = caster.position - target.position
    local distance = toPlayer:length()

    if distance > max_range then return false end

    local npcForward = caster.rotation:apply(util.vector3(0, 1, 0))
    local angleToPlayer = npcForward:dot(toPlayer:normalize())
    
    local cone_angle_rad = math.rad(cone_angle_deg)
   
    print(angleToPlayer)
    print( -1 + cone_angle_rad/math.pi )
    
    return angleToPlayer < -1 + cone_angle_rad/math.pi
end

this way you can have any angle from 0 to 360

lost brook
#

First versions did have 360 but it wasnโ€™t what I needed it to do

cobalt token
#

i'll put this as general case if don't mind ๐Ÿ˜…

#

preferens to you .?